JuliusBrussee/caveman · critical · Error

cave_host_sandbox_nested_under_required

cave_host_sandbox_nested_under_required

Error message

cave_host_sandbox_nested_under_required

What it means

Sandbox policy is monotonic: once any ancestor declares sandbox: 'required', no descendant may declare sandbox: 'host'. Host mode is an opt-in the ROOT makes for itself; a child attempting to escape an ancestor's containment by declaring host mode is a policy violation and is rejected during graph validation, before anything runs.

Source

Thrown at packages/agent/src/definition-graph.ts:27

  // reached under a sandbox-required ancestor must be re-checked, not skipped.
  const visited = [new Set<AgentDefinition>(), new Set<AgentDefinition>()];
  const active = new Set<AgentDefinition>();

  const visit = (
    definition: AgentDefinition,
    depth: number,
    sandboxRequired: boolean,
  ): void => {
    if (!definition || definition.kind !== "agent" ||
        !Array.isArray(definition.tools)) {
      throw new Error("cave_agent_definition_invalid");
    }
    if (depth > 8) throw new Error("cave_subagent_depth_limit");
    if (active.has(definition)) throw new Error("cave_subagent_definition_cycle");
    // Host mode is an opt-in the root makes for itself. A descendant cannot use
    // it to run closures outside an ancestor's required containment.
    if (sandboxRequired && definition.sandbox === "host") {
      throw new Error("cave_host_sandbox_nested_under_required");
    }
    const memo = visited[sandboxRequired ? 1 : 0]!;
    if (memo.has(definition)) return;
    active.add(definition);
    const childSandboxRequired = sandboxRequired ||
      definition.sandbox === "required";
    const names = new Set<string>();
    for (const declared of definition.tools) {
      if (!declared || declared.kind !== "tool" ||
          typeof declared.name !== "string") {
        throw new Error("cave_tool_definition_invalid");
      }
      if (names.has(declared.name)) throw new Error("cave_duplicate_tool_name");
      names.add(declared.name);
      if (declared.name.startsWith("cave_")) {
        throw new Error(`cave_reserved_tool_name:${declared.name}`);
      }
      if (typeof Reflect.get(declared, TOOL_IMPLEMENTATION_SOURCE) !== "string") {

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Change the descendant's sandbox from 'host' to 'required' (inherit containment) or remove the explicit override
  2. If host execution is genuinely needed, the ROOT agent must be the one declaring host mode, with no required ancestor above it
  3. Audit third-party agent definitions for their sandbox field before nesting them under a required root

Example fix

// before
const root = agent({ kind: "agent", sandbox: "required", tools: [
  tool({ name: "delegate", runtime: { kind: "subagent", definition: hostChild } }) // hostChild has sandbox: "host"
]});

// after
const containedChild = agent({ kind: "agent", sandbox: "required", tools: [...] });
const root = agent({ kind: "agent", sandbox: "required", tools: [
  tool({ name: "delegate", runtime: { kind: "subagent", definition: containedChild } })
]});
Defensive patterns

Strategy: validation

Validate before calling

function assertSandboxMonotonic(def: any, required = false): void {
  const nowRequired = required || def.sandbox === "required";
  if (required && def.sandbox === "host") {
    throw new Error(`agent ${def.name} declares host mode under a required-sandbox ancestor`);
  }
  for (const t of def.tools ?? []) {
    if (t.runtime?.kind === "subagent") assertSandboxMonotonic(t.runtime.definition, nowRequired);
  }
}

Prevention

When it happens

Trigger: Agent A with sandbox: 'required' has a tool whose runtime.definition (subagent) B declares sandbox: 'host'; or a deeper descendant of a required ancestor does so.

Common situations: Composing agents from libraries where one module assumed host privileges while the composition root mandated containment; copy-pasting a host-mode agent under a hardened orchestrator; attempts to get shell access beneath a sandboxed runner.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/501c0aeb57a9fd01. Report an issue: GitHub.