{"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":"exception","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/766dce6b1394ebb56a3090748d5a0240a5aefb36/packages/agent/src/runtime.ts#L4727-L4763","documentation":"A sandbox profile requesting network: true is refused. This flag historically skipped the OS network namespace entirely, granting the tool unrestricted egress while credentials sat in its environment - an exfiltration hole. No scoped-egress mechanism exists yet (a parent-owned CONNECT proxy bound to an allow-list is the tracked follow-up), so unbounded egress fails closed and every sandboxed tool runs under the OS network boundary instead.","triggerScenarios":"sandboxProfile: { network: true } on any tool run through the sandbox executor; configs migrated from an older version where the flag granted raw network access; tools that call HTTP APIs from inside the sandbox.","commonSituations":"Upgrading from a version that honored network: true; porting API-calling tools into the sandbox; assuming an allow-list exists like in firewalled container sandboxes.","solutions":["Drop network: true - sandboxed tools run with no network by design","Move network-dependent work to an explicitly approved host-mode tool outside the required sandbox","Fetch data before the sandboxed call and pass it in as staged input files","Watch for the scoped-egress CONNECT-proxy feature instead of re-enabling the flag"],"exampleFix":"// before\ntool({ sandboxProfile: { network: true }, run: async (p) => fetch(url) });\n\n// after: fetch outside, stage the payload, compute inside\ntool({ sandboxProfile: {}, run: async (p) => analyze(p.stagedData) });","handlingStrategy":"type-guard","validationCode":"// Remove the legacy network grant before running the tool\nfunction stripNetworkGrant(profile) {\n  const { network, ...rest } = profile ?? {};\n  if (network) logger.warn('network:true is refused; sandboxed tools run without network');\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.network !== true; // network:true always throws cave_sandbox_network_egress_unbounded\n}","tryCatchPattern":"try {\n  return await runSandboxedTool(params);\n} catch (error) {\n  if (error instanceof Error && error.message === 'cave_sandbox_network_egress_unavailable' ||\n      error.message === 'cave_sandbox_network_egress_unbounded') {\n    return fetchOutsideThenRunSandboxed(params); // fetch first, stage payload, compute inside\n  }\n  throw error;\n}","preventionTips":["Treat sandboxed tools as network-less by design; drop network: true from all profiles","Fetch required data before the sandboxed call and pass it in as staged input","Audit configs migrated from older versions for the removed network grant","Track the scoped-egress CONNECT-proxy feature rather than re-enabling the flag"],"tags":["sandbox","network","security","egress","fail-closed"],"backgroundTag":"sandbox-network-blocked","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"}