JuliusBrussee/caveman · error
cave_sandbox_source_read_grant_escapes_staging
cave_sandbox_source_read_grant_escapes_staging
Error message
cave_sandbox_source_read_grant_escapes_staging
What it means
The common ancestor of the >1024 source files lies outside the declared staging root — the single `--allow-fs-read` grant that would be emitted covers a path not contained by staging. The runtime fails closed because granting read on that ancestor would exceed the intended staging containment (e.g. reaching into the real project tree or system paths).
Solutions
- Pass the staged copies' paths (inside stagingRoot), not the original source paths
- Set stagingRoot to the actual directory that contains all the source paths being granted
- Verify with `relative(stagingRoot, eachPath)` that no path starts with `..` before calling
Example fix
// before sandboxSourceReadFlags(originalProjectPaths, "/proj/.cave-stage"); // after sandboxSourceReadFlags(stagedPathsInsideStageRoot, "/proj/.cave-stage");
Defensive patterns
Strategy: validation
Validate before calling
const rel = relative(resolve(stagingRoot), resolve(dirname(sourceFiles[0])));
if (rel.startsWith('..') || isAbsolute(rel)) throw new Error('source files are not inside stagingRoot'); Type guard
function insideStagingRoot(p: string, stagingRoot: string): boolean { const rel = relative(resolve(stagingRoot), resolve(p)); return rel !== '' && !rel.startsWith('..') && !isAbsolute(rel); } Try / catch
try { const flags = sandboxSourceReadFlags(files, stagingRoot); } catch (e) { if (String((e as Error).message).includes('cave_sandbox_source_read_grant_escapes_staging')) { /* pass staged copies, not originals */ } else throw e; } Prevention
- Always pass staged-copy paths, never original project paths, when stagingRoot is set
- Check every source path with relative() against stagingRoot before the call
- Keep one canonical staging directory and derive all paths from it
When it happens
Trigger: `sandboxSourceReadFlags(sourceFiles, stagingRoot)` where `escapesRoot(relative(resolvedStagingRoot, ancestor))` is true — i.e. the staged source paths resolve to a directory tree that is not inside `stagingRoot`, such as passing original project paths while stagingRoot points at a copy directory.
Common situations: Mixing staged and unstaged paths in the source list, a staging copy that relocated files under a different subtree while the caller still passes original absolute paths, or a typo/wrong env value for stagingRoot.
Understand the failure class
Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.
Related errors
- cave_live_eval_sandbox_profile_escapes_root
- cave_tool_sandbox_entry_escapes_root
- cave_tool_sandbox_source_escapes_root
- cave_host_sandbox_nested_under_required
- cave_sandbox_child_process_containment_unavailable
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/b856fd189cd48e28.
Report an issue: GitHub.
Appendix: source
Thrown at packages/agent/src/runtime.ts:4919
}
export function sandboxSourceReadFlags(
sourceFiles: readonly string[],
stagingRoot?: string,
): string[] {
if (sourceFiles.length <= SANDBOX_FS_READ_FLAG_THRESHOLD) {
return sourceFiles.map((path) => `--allow-fs-read=${path}`);
}
if (stagingRoot === undefined) {
throw new Error("cave_sandbox_source_staging_root_required");
}
const resolvedStagingRoot = resolve(stagingRoot);
if (dirname(resolvedStagingRoot) === resolvedStagingRoot) {
throw new Error("cave_sandbox_source_read_root_refused");
}
const ancestor = commonAncestorDir(sourceFiles);
if (escapesRoot(relative(resolvedStagingRoot, ancestor))) {
throw new Error("cave_sandbox_source_read_grant_escapes_staging");
}
return [`--allow-fs-read=${ancestor}`];
}
/** Decode the length-prefixed result frame delivered on the worker's fd 3. */
function decodeResultFrame(
buffer: Buffer,
): { ok: boolean; value?: unknown; code?: string } | undefined {
if (buffer.byteLength < 4) return undefined;
const length = buffer.readUInt32BE(0);
if (buffer.byteLength < 4 + length) return undefined;
try {
return JSON.parse(buffer.subarray(4, 4 + length).toString("utf8"));
} catch {
return undefined;
}
}
View on GitHub (pinned to 3ee70a1026)