different-ai/openwork · error · ApiError
invalid_payload
invalid_payload
Error message
No authorized workspace roots are available.
What it means
resolveAuthorizedFile resolves a requested path for direct cloud upload against the server's authorized workspace roots. If no roots are configured, there is nothing a request could be authorized against, so it throws a 400 invalid_payload ApiError.
Source
Thrown at apps/server/src/extensions/cloud-uploads.ts:114
for (const workspace of config.workspaces) pushUniqueResolvedPath(roots, workspace.path);
for (const root of config.authorizedRoots) pushUniqueResolvedPath(roots, root);
return roots;
}
function searchRoots(config: ServerConfig, context: Record<string, unknown>, roots: string[]) {
const candidates: string[] = [];
const directory = readString(context, "directory");
const worktree = readString(context, "worktree");
if (directory) pushUniqueResolvedPath(candidates, directory);
if (worktree) pushUniqueResolvedPath(candidates, worktree);
for (const workspace of config.workspaces) pushUniqueResolvedPath(candidates, workspace.path);
for (const root of roots) pushUniqueResolvedPath(candidates, root);
return candidates.filter((candidate) => roots.some((root) => isWithinRoot(candidate, root)));
}
async function resolveAuthorizedFile(config: ServerConfig, context: Record<string, unknown>, requested: string) {
const roots = allowedRoots(config);
if (!roots.length) throw new ApiError(400, "invalid_payload", "No authorized workspace roots are available.");
const realRoots: string[] = [];
for (const root of roots) {
try {
pushUniqueResolvedPath(realRoots, await realpath(root));
} catch (error) {
if (!isRecord(error) || error.code !== "ENOENT") throw error;
}
}
const candidates = isAbsolute(requested)
? [resolve(requested)]
: searchRoots(config, context, roots).map((root) => resolve(root, requested));
for (const candidate of candidates) {
if (!roots.some((root) => isWithinRoot(candidate, root))) continue;
try {
const realCandidate = await realpath(candidate);
if (!realRoots.some((root) => isWithinRoot(realCandidate, root))) continue;
const info = await stat(realCandidate);
if (!info.isFile()) continue;View on GitHub (pinned to 2b7df46e8a)
Solutions
- Configure at least one authorized workspace root in the server config and restart
- Verify the config file's workspace/root settings are being loaded (check for typos in keys or wrong config path)
- Confirm allowedRoots() is not filtering out existing roots (e.g. non-existent directories being dropped)
Example fix
// before
{ } // no workspace configured
// after
{ "workspaceRoot": "/Users/me/projects" } Defensive patterns
Strategy: validation
Validate before calling
if (allowedRoots(config).length === 0) return badRequest("no workspace roots"); Try / catch
try { await uploadCloud(p); } catch (e) { if (e.code === "invalid_payload") { openWorkspaceSettings(); } else throw e; } Prevention
- Startup check: fail fast when no roots configured
- Gate upload features on roots presence
- Test config loading in CI
When it happens
Trigger: Calling the cloud-upload endpoint when the server config yields an empty allowedRoots() list — no workspace roots were configured or all were filtered out.
Common situations: Server started without a workspace directory configured; workspace root path removed from config; misconfigured config file resulting in an empty roots array.
Related errors
- External URL protocol "${parsed.protocol}" is not allowed.
- file_too_large
- file_not_found
- Attachment hostname ${hostname} resolved to a private or res
- protocol "${url.protocol}" is not allowed
AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01).
Data as JSON: /api/errors/7fc08ddd7a149a1a.
Report an issue: GitHub.