JuliusBrussee/caveman · error

cave_sandbox_network_egress_unbounded

cave_sandbox_network_egress_unbounded

Error message

cave_sandbox_network_egress_unbounded

What it means

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.

Solutions

  1. Set sandboxProfile.network to false (or omit it) and perform outbound calls outside the sandbox (host-side) if connectivity is required.
  2. Wait for/adopt the scoped-egress mechanism (parent-owned CONNECT proxy with an allow-list) once available, instead of raw egress.
  3. Move the network-dependent portion of the work into a non-sandboxed step that returns data to the sandboxed tool.
  4. Audit tool profiles for network: true left over from older versions and remove the flag.

Example fix

// before
await runTool(fetcher, { sandboxProfile: { network: true } });
// after: fetch outside the sandbox, pass results in
const data = await hostFetch('https://api.example.com');
await runTool(processor, { sandboxProfile: {}, params: { data } });
Defensive patterns

Strategy: validation

Validate before calling

if (profile?.network === true) {
  throw new Error('network: true is refused by the sandbox; fetch outside and pass results in');
}

Type guard

const sandboxProfileIsSafe = (profile) => profile?.network !== true;

Try / catch

try {
  await runTool(tool, { sandboxProfile: profile });
} catch (e) {
  if (e.message === 'cave_sandbox_network_egress_unbounded') {
    const data = await hostFetch(tool.needsUrl);
    await runTool(tool, { sandboxProfile: {}, params: { data } });
  } else throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/73065b7e849bcda4. Report an issue: GitHub.

Appendix: source

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

  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));
  const timeout = AbortSignal.timeout(timeoutMs);
  const combined = signal ? AbortSignal.any([signal, timeout]) : timeout;
  const args = [
    "--permission",
    // One declared source file, framework runtime, dependencies, and ephemeral
    // workspace only. Never grant tool code a project-root read capability:
    // repositories commonly contain .env files, credentials, and local traces.
    ...sourceReadFlags,
    `--allow-fs-read=${packageRoot}`,

View on GitHub (pinned to 3ee70a1026)