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 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.

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 766dce6b13)

Solutions

  1. Ensure every entry of sourceFiles is the staged copy inside the per-run staging root
  2. Validate each path is inside stagingRoot before the collapse (the executor does this; direct callers must too)
  3. Keep the file count at or below 1024 so per-file flags are emitted and no collapse/ancestor logic runs
  4. Re-stage the source graph if staging produced out-of-root copies

Example fix

// before: raw project paths leak into the list
sandboxSourceReadFlags(['/home/me/proj/src/a.ts', ...], stagingRoot);

// after: only staged copies
sandboxSourceReadFlags(stagedFilesUnder(stagingRoot), stagingRoot);
Defensive patterns

Strategy: validation

Validate before calling

// Every source file must live inside the staging root before collapsing
import { relative, resolve } from 'node:path';
function allInsideStaging(files: readonly string[], stagingRoot: string): boolean {
  const root = resolve(stagingRoot);
  return files.every((f) => { const r = relative(root, resolve(f)); return r !== '' && !r.startsWith('..'); });
}

Try / catch

try {
  flags = sandboxSourceReadFlags(files, stagingRoot);
} catch (error) {
  if (error instanceof Error && error.message === 'cave_sandbox_source_read_grant_escapes_staging') {
    // a raw project path slipped in: re-stage the graph, then retry with staged copies only
  }
  throw error;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@766dce6b13 (2026-08-18). Data as JSON: /api/errors/b856fd189cd48e28. Report an issue: GitHub.