JuliusBrussee/caveman · error

cave_sandbox_child_process_containment_unavailable

cave_sandbox_child_process_containment_unavailable

Error message

cave_sandbox_child_process_containment_unavailable

What it means

A sandbox profile requesting childProcess: true is refused. The Node-based tool sandbox has no portable mechanism to contain processes that a sandboxed tool spawns (a child could simply exit the sandbox's restrictions), so the grant fails closed instead of running a tool whose descendants would escape containment. There is no configuration that enables it in this runtime.

Source

Thrown at packages/agent/src/runtime.ts:4736

    ledger.reservedUsd = Math.max(0, ledger.reservedUsd - ceilingUsd);
  }
}

async function executeSandboxedTool(
  entryPath: string,
  sourceFiles: readonly string[],
  stagingRoot: string,
  toolName: string,
  params: unknown,
  timeoutMs: number,
  allowSideEffects: boolean,
  profile: RunOptions["sandboxProfile"],
  executionContext: InternalExecutionContext,
  toolDefinitionSha256: string,
  signal?: AbortSignal,
): Promise<unknown> {
  if (profile?.childProcess === true) {
    throw new Error("cave_sandbox_child_process_containment_unavailable");
  }
  // `network: true` used to skip the OS network namespace entirely, granting the
  // tool UNRESTRICTED egress while credentials sit in its env — an exfiltration
  // hole, not a feature. There is no scoped-egress mechanism
  // yet (a parent-owned CONNECT proxy bound to an allow-list is the tracked
  // follow-up), so unbounded egress fails closed rather than being granted. Every
  // sandboxed tool now runs under the OS boundary below.
  if (profile?.network === true) {
    throw new Error("cave_sandbox_network_egress_unbounded");
  }
  const requestedCredentialEnv = profile?.credentialEnv ?? [];
  const childEnv = buildSandboxToolEnv(requestedCredentialEnv);
  // Validate every collapsed grant before allocating per-call state. Refused
  // roots must fail without leaving a caveman-agent-tool-* workspace behind.
  const sourceReadFlags = sandboxSourceReadFlags(sourceFiles, stagingRoot);
  const workspace = await realpath(await mkdtemp(`${tmpdir()}/caveman-agent-tool-`));
  const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
  const worker = fileURLToPath(new URL("./tool-worker.js", import.meta.url));

View on GitHub (pinned to 766dce6b13)

Solutions

  1. Remove childProcess: true and restructure the tool to do its work in-process (import the library instead of shelling out)
  2. Run that specific tool under the explicit host sandbox mode where real host access is intended and reviewed (subject to your integration's policy)
  3. Keep the sandboxed variant of the tool limited to pure computation over staged files
  4. Track descendant-containment support before reintroducing the flag

Example fix

// before
tool({ sandboxProfile: { childProcess: true }, run: async (p) => execFile('git', ...) });

// after: in-process work, or explicit host mode for this tool
tool({ run: async (p) => computeInProcess(p) });
// or a host-mode tool closure for CLI work, per your integration policy
Defensive patterns

Strategy: type-guard

Validate before calling

// Strip unsupported grants from the profile before defining the tool
function sanitizeSandboxProfile(profile) {
  const { childProcess, ...rest } = profile ?? {};
  if (childProcess) logger.warn('childProcess grant is unsupported and was removed');
  return rest;
}

Type guard

function isSupportedSandboxProfile(profile: unknown): boolean {
  if (profile === undefined || profile === null) return true;
  if (typeof profile !== 'object') return false;
  const p = profile as Record<string, unknown>;
  return p.childProcess !== true; // childProcess:true always throws cave_sandbox_child_process_containment_unavailable
}

Try / catch

try {
  return await runSandboxedTool(params);
} catch (error) {
  if (error instanceof Error && error.message === 'cave_sandbox_child_process_containment_unavailable') {
    return runInProcessVariant(params); // restructured tool without spawning
  }
  throw error;
}

Prevention

When it happens

Trigger: sandboxProfile: { childProcess: true } on a tool executed through the sandboxed tool runner; porting a tool that shells out (git, compilers, test runners) into the required sandbox; assuming Docker/seatbelt-style process isolation semantics.

Common situations: Migrating tools from a container-based sandbox where spawning was allowed; build/lint tools that invoke CLIs; version upgrades where previously-tolerated profile fields became fatal fail-closed errors.

Related errors


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