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

  1. Pass the staged copies' paths (inside stagingRoot), not the original source paths
  2. Set stagingRoot to the actual directory that contains all the source paths being granted
  3. 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

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


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)