{"record":{"id":"73065b7e849bcda4","repo":"JuliusBrussee/caveman","slug":"cave-sandbox-network-egress-unbounded","errorCode":"cave_sandbox_network_egress_unbounded","errorMessage":"cave_sandbox_network_egress_unbounded","messagePattern":"cave_sandbox_network_egress_unbounded","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/agent/src/runtime.ts","lineNumber":4745,"sourceCode":"  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));\n  const timeout = AbortSignal.timeout(timeoutMs);\n  const combined = signal ? AbortSignal.any([signal, timeout]) : timeout;\n  const args = [\n    \"--permission\",\n    // One declared source file, framework runtime, dependencies, and ephemeral\n    // workspace only. Never grant tool code a project-root read capability:\n    // repositories commonly contain .env files, credentials, and local traces.\n    ...sourceReadFlags,\n    `--allow-fs-read=${packageRoot}`,","sourceCodeStart":4727,"sourceCodeEnd":4763,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/agent/src/runtime.ts#L4727-L4763","documentation":"Requesting unrestricted network access (profile.network === true) for a sandboxed tool is refused by design: previously that flag skipped the network namespace entirely, handing the tool unbounded egress while credentials were present in its environment — an exfiltration hole. Until a scoped-egress proxy exists, unbounded egress fails closed with this error; every sandboxed tool runs under the OS network boundary.","triggerScenarios":"Executing a sandboxed tool with RunOptions.sandboxProfile set to { network: true } (runtime.ts:4745), or any code path that passes allowNetwork: profile?.network === true into the sandbox execution with network enabled.","commonSituations":"Migrating tools from an older caveman-agent version where network: true was permitted; copying sandbox profiles from other frameworks that allow network flags; a tool that legitimately needs an API call being run with raw egress enabled instead of a scoped proxy or host-side fetch.","solutions":["Set sandboxProfile.network to false (or omit it) and perform outbound calls outside the sandbox (host-side) if connectivity is required.","Wait for/adopt the scoped-egress mechanism (parent-owned CONNECT proxy with an allow-list) once available, instead of raw egress.","Move the network-dependent portion of the work into a non-sandboxed step that returns data to the sandboxed tool.","Audit tool profiles for network: true left over from older versions and remove the flag."],"exampleFix":"// before\nawait runTool(fetcher, { sandboxProfile: { network: true } });\n// after: fetch outside the sandbox, pass results in\nconst data = await hostFetch('https://api.example.com');\nawait runTool(processor, { sandboxProfile: {}, params: { data } });","handlingStrategy":"validation","validationCode":"if (profile?.network === true) {\n  throw new Error('network: true is refused by the sandbox; fetch outside and pass results in');\n}","typeGuard":"const sandboxProfileIsSafe = (profile) => profile?.network !== true;","tryCatchPattern":"try {\n  await runTool(tool, { sandboxProfile: profile });\n} catch (e) {\n  if (e.message === 'cave_sandbox_network_egress_unbounded') {\n    const data = await hostFetch(tool.needsUrl);\n    await runTool(tool, { sandboxProfile: {}, params: { data } });\n  } else throw e;\n}","preventionTips":["Never set network: true in sandbox profiles; the runtime refuses it.","Perform outbound HTTP host-side and inject results as params.","Grep profiles for network: true when upgrading from older versions."],"tags":["sandbox","security","network","egress","fail-closed"],"backgroundTag":"unsupported-operation","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}