{"record":{"id":"b856fd189cd48e28","repo":"JuliusBrussee/caveman","slug":"cave-sandbox-source-read-grant-escapes-staging","errorCode":"cave_sandbox_source_read_grant_escapes_staging","errorMessage":"cave_sandbox_source_read_grant_escapes_staging","messagePattern":"cave_sandbox_source_read_grant_escapes_staging","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/agent/src/runtime.ts","lineNumber":4919,"sourceCode":"}\n\nexport function sandboxSourceReadFlags(\n  sourceFiles: readonly string[],\n  stagingRoot?: string,\n): string[] {\n  if (sourceFiles.length <= SANDBOX_FS_READ_FLAG_THRESHOLD) {\n    return sourceFiles.map((path) => `--allow-fs-read=${path}`);\n  }\n  if (stagingRoot === undefined) {\n    throw new Error(\"cave_sandbox_source_staging_root_required\");\n  }\n  const resolvedStagingRoot = resolve(stagingRoot);\n  if (dirname(resolvedStagingRoot) === resolvedStagingRoot) {\n    throw new Error(\"cave_sandbox_source_read_root_refused\");\n  }\n  const ancestor = commonAncestorDir(sourceFiles);\n  if (escapesRoot(relative(resolvedStagingRoot, ancestor))) {\n    throw new Error(\"cave_sandbox_source_read_grant_escapes_staging\");\n  }\n  return [`--allow-fs-read=${ancestor}`];\n}\n\n/** Decode the length-prefixed result frame delivered on the worker's fd 3. */\nfunction decodeResultFrame(\n  buffer: Buffer,\n): { ok: boolean; value?: unknown; code?: string } | undefined {\n  if (buffer.byteLength < 4) return undefined;\n  const length = buffer.readUInt32BE(0);\n  if (buffer.byteLength < 4 + length) return undefined;\n  try {\n    return JSON.parse(buffer.subarray(4, 4 + length).toString(\"utf8\"));\n  } catch {\n    return undefined;\n  }\n}\n","sourceCodeStart":4901,"sourceCodeEnd":4937,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/766dce6b1394ebb56a3090748d5a0240a5aefb36/packages/agent/src/runtime.ts#L4901-L4937","documentation":"The common ancestor computed for the collapsed --allow-fs-read grant lies outside the resolved staging root (escapesRoot(relative(stagingRoot, ancestor))). A grant anchored there would read paths beyond the per-run staged copy - the staged copy exists precisely so the sandbox never reads the real project root with its .env and credentials - so the call refuses rather than widen the grant.","triggerScenarios":"sourceFiles containing raw project paths instead of the staged copies under stagingRoot; one stray path (or a symlink resolving outside) pulling the common ancestor above the staging root; staging that left some files outside the per-run workspace.","commonSituations":"Passing original workspace paths to the flag builder instead of the staged graph; symlinked dependencies resolving outside tmpdir; partial staging failures leaving a mixed list.","solutions":["Ensure every entry of sourceFiles is the staged copy inside the per-run staging root","Validate each path is inside stagingRoot before the collapse (the executor does this; direct callers must too)","Keep the file count at or below 1024 so per-file flags are emitted and no collapse/ancestor logic runs","Re-stage the source graph if staging produced out-of-root copies"],"exampleFix":"// before: raw project paths leak into the list\nsandboxSourceReadFlags(['/home/me/proj/src/a.ts', ...], stagingRoot);\n\n// after: only staged copies\nsandboxSourceReadFlags(stagedFilesUnder(stagingRoot), stagingRoot);","handlingStrategy":"validation","validationCode":"// Every source file must live inside the staging root before collapsing\nimport { relative, resolve } from 'node:path';\nfunction allInsideStaging(files: readonly string[], stagingRoot: string): boolean {\n  const root = resolve(stagingRoot);\n  return files.every((f) => { const r = relative(root, resolve(f)); return r !== '' && !r.startsWith('..'); });\n}","typeGuard":null,"tryCatchPattern":"try {\n  flags = sandboxSourceReadFlags(files, stagingRoot);\n} catch (error) {\n  if (error instanceof Error && error.message === 'cave_sandbox_source_read_grant_escapes_staging') {\n    // a raw project path slipped in: re-stage the graph, then retry with staged copies only\n  }\n  throw error;\n}","preventionTips":["Pass only the staged copies produced by the executor - never original project paths","Validate containment (no '..' relative to stagingRoot) before the collapse path runs","Keep file lists at or below 1024 so per-file flags avoid the ancestor computation","Beware symlinks resolving outside the staging root"],"tags":["sandbox","filesystem","path-containment","staging"],"backgroundTag":"sandbox-path-outside-allowed-root","analyzedSha":"766dce6b1394ebb56a3090748d5a0240a5aefb36","analyzedAt":"2026-08-18T03:14:35.516Z","contentChangedAt":"2026-08-18T03:14:35.516Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}