{"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":"error_code","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/3ee70a102609e550bd2e68004bf5990a9341c851/packages/agent/src/runtime.ts#L4901-L4937","documentation":"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).","triggerScenarios":"`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.","commonSituations":"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.","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"],"exampleFix":"// before\nsandboxSourceReadFlags(originalProjectPaths, \"/proj/.cave-stage\");\n// after\nsandboxSourceReadFlags(stagedPathsInsideStageRoot, \"/proj/.cave-stage\");","handlingStrategy":"validation","validationCode":"const rel = relative(resolve(stagingRoot), resolve(dirname(sourceFiles[0])));\nif (rel.startsWith('..') || isAbsolute(rel)) throw new Error('source files are not inside stagingRoot');","typeGuard":"function insideStagingRoot(p: string, stagingRoot: string): boolean { const rel = relative(resolve(stagingRoot), resolve(p)); return rel !== '' && !rel.startsWith('..') && !isAbsolute(rel); }","tryCatchPattern":"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; }","preventionTips":["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"],"tags":["sandbox","security","path-traversal"],"backgroundTag":"path-traversal-blocked","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}