{"record":{"id":"c0184cfe41575098","repo":"JuliusBrussee/caveman","slug":"cave-sandbox-child-process-containment-unavailable","errorCode":"cave_sandbox_child_process_containment_unavailable","errorMessage":"cave_sandbox_child_process_containment_unavailable","messagePattern":"cave_sandbox_child_process_containment_unavailable","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/agent/src/runtime.ts","lineNumber":4736,"sourceCode":"    ledger.reservedUsd = Math.max(0, ledger.reservedUsd - ceilingUsd);\n  }\n}\n\nasync function executeSandboxedTool(\n  entryPath: string,\n  sourceFiles: readonly string[],\n  stagingRoot: string,\n  toolName: string,\n  params: unknown,\n  timeoutMs: number,\n  allowSideEffects: boolean,\n  profile: RunOptions[\"sandboxProfile\"],\n  executionContext: InternalExecutionContext,\n  toolDefinitionSha256: string,\n  signal?: AbortSignal,\n): Promise<unknown> {\n  if (profile?.childProcess === true) {\n    throw new Error(\"cave_sandbox_child_process_containment_unavailable\");\n  }\n  // `network: true` used to skip the OS network namespace entirely, granting the\n  // tool UNRESTRICTED egress while credentials sit in its env — an exfiltration\n  // hole, not a feature. There is no scoped-egress mechanism\n  // yet (a parent-owned CONNECT proxy bound to an allow-list is the tracked\n  // follow-up), so unbounded egress fails closed rather than being granted. Every\n  // sandboxed tool now runs under the OS boundary below.\n  if (profile?.network === true) {\n    throw new Error(\"cave_sandbox_network_egress_unbounded\");\n  }\n  const requestedCredentialEnv = profile?.credentialEnv ?? [];\n  const childEnv = buildSandboxToolEnv(requestedCredentialEnv);\n  // Validate every collapsed grant before allocating per-call state. Refused\n  // roots must fail without leaving a caveman-agent-tool-* workspace behind.\n  const sourceReadFlags = sandboxSourceReadFlags(sourceFiles, stagingRoot);\n  const workspace = await realpath(await mkdtemp(`${tmpdir()}/caveman-agent-tool-`));\n  const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));\n  const worker = fileURLToPath(new URL(\"./tool-worker.js\", import.meta.url));","sourceCodeStart":4718,"sourceCodeEnd":4754,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/766dce6b1394ebb56a3090748d5a0240a5aefb36/packages/agent/src/runtime.ts#L4718-L4754","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Remove childProcess: true and restructure the tool to do its work in-process (import the library instead of shelling out)","Run that specific tool under the explicit host sandbox mode where real host access is intended and reviewed (subject to your integration's policy)","Keep the sandboxed variant of the tool limited to pure computation over staged files","Track descendant-containment support before reintroducing the flag"],"exampleFix":"// before\ntool({ sandboxProfile: { childProcess: true }, run: async (p) => execFile('git', ...) });\n\n// after: in-process work, or explicit host mode for this tool\ntool({ run: async (p) => computeInProcess(p) });\n// or a host-mode tool closure for CLI work, per your integration policy","handlingStrategy":"type-guard","validationCode":"// Strip unsupported grants from the profile before defining the tool\nfunction sanitizeSandboxProfile(profile) {\n  const { childProcess, ...rest } = profile ?? {};\n  if (childProcess) logger.warn('childProcess grant is unsupported and was removed');\n  return rest;\n}","typeGuard":"function isSupportedSandboxProfile(profile: unknown): boolean {\n  if (profile === undefined || profile === null) return true;\n  if (typeof profile !== 'object') return false;\n  const p = profile as Record<string, unknown>;\n  return p.childProcess !== true; // childProcess:true always throws cave_sandbox_child_process_containment_unavailable\n}","tryCatchPattern":"try {\n  return await runSandboxedTool(params);\n} catch (error) {\n  if (error instanceof Error && error.message === 'cave_sandbox_child_process_containment_unavailable') {\n    return runInProcessVariant(params); // restructured tool without spawning\n  }\n  throw error;\n}","preventionTips":["Never set childProcess: true - there is no supported descendant containment in this runtime","Rewrite shelling-out tools as in-process library calls before sandboxing them","Validate sandbox profiles against supported keys at config load time","Keep CLI-invoking tools in explicitly reviewed host-mode integrations instead"],"tags":["sandbox","child-process","containment","fail-closed"],"backgroundTag":"sandbox-capability-unsupported","analyzedSha":"766dce6b1394ebb56a3090748d5a0240a5aefb36","analyzedAt":"2026-08-18T03:14:35.516Z","contentChangedAt":"2026-08-18T03:14:35.516Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}