{"record":{"id":"847c94030e377d81","repo":"JuliusBrussee/caveman","slug":"cave-sandbox-source-read-root-refused","errorCode":"cave_sandbox_source_read_root_refused","errorMessage":"cave_sandbox_source_read_root_refused","messagePattern":"cave_sandbox_source_read_root_refused","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/agent/src/runtime.ts","lineNumber":4893,"sourceCode":" * files to their common ancestor directory. A large project would\n * otherwise blow the OS argument limit (E2BIG) and the tool could not spawn at\n * all. The collapse is safe here: `sourceFiles` are paths inside the per-run\n * STAGED COPY, which already contains only the reachable source graph — never\n * the real project root with its .env and credentials.\n */\nconst SANDBOX_FS_READ_FLAG_THRESHOLD = 1024;\n\nfunction commonAncestorDir(paths: readonly string[]): string {\n  const dirs = paths.map((path) => resolve(dirname(path)));\n  const first = dirs[0];\n  if (first === undefined) {\n    throw new Error(\"cave_sandbox_source_staging_root_required\");\n  }\n  let ancestor = first;\n  while (dirs.some((path) => escapesRoot(relative(ancestor, path)))) {\n    const parent = dirname(ancestor);\n    if (parent === ancestor) {\n      throw new Error(\"cave_sandbox_source_read_root_refused\");\n    }\n    ancestor = parent;\n  }\n  if (dirname(ancestor) === ancestor) {\n    throw new Error(\"cave_sandbox_source_read_root_refused\");\n  }\n  return ancestor;\n}\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\");","sourceCodeStart":4875,"sourceCodeEnd":4911,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/766dce6b1394ebb56a3090748d5a0240a5aefb36/packages/agent/src/runtime.ts#L4875-L4911","documentation":"While walking up from the first staged file's directory toward a common ancestor for the collapsed --allow-fs-read grant, the walk reached the filesystem root (parent === ancestor) while some path was still judged to escape it. Since nothing can escape '/', this branch indicates a path/normalization inconsistency and is refused rather than granting a read rooted at the entire filesystem. Defensive invariant guard.","triggerScenarios":"Path normalization mismatches (mixed absolute/relative or symlink-resolved inputs) passed directly into commonAncestorDir; synthetic or malformed paths in tests; not reachable via the public executor with a correctly staged graph.","commonSituations":"Direct imports of commonAncestorDir with hand-built path lists; platform-specific path separators (Windows backslash vs POSIX) mixed into one list; internal bug in staging.","solutions":["Pass consistently resolved absolute paths (resolve() each path first)","Use the staged copies produced by the executor instead of hand-built path lists","If hit through the public API, capture the sourceFiles list and report it as a staging bug"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Pre-check: consistently resolved absolute paths sharing a non-root ancestor\nimport { resolve, dirname } from 'node:path';\nfunction precheckSourceFiles(files: readonly string[]): void {\n  const dirs = files.map((f) => resolve(dirname(f)));\n  if (new Set(dirs.map((d) => d.split('/')[1])).size > 1) {\n    throw new Error('source files span multiple top-level trees; re-stage under one root');\n  }\n}","typeGuard":null,"tryCatchPattern":"try {\n  flags = sandboxSourceReadFlags(sourceFiles, stagingRoot);\n} catch (error) {\n  if (error instanceof Error && error.message === 'cave_sandbox_source_read_root_refused') {\n    // ancestor walk hit the filesystem root: capture the path list and report a staging bug\n  }\n  throw error;\n}","preventionTips":["Resolve every path to an absolute POSIX form before passing it in","Avoid mixing hand-built path lists with the executor's staged copies","If reached via the public API, capture sourceFiles - it indicates a staging/normalization bug"],"tags":["sandbox","invariant","filesystem","path-normalization"],"backgroundTag":"sandbox-root-grant-refused","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"}