JuliusBrussee/caveman · error · Error

cave_untrusted_tool_definition

cave_untrusted_tool_definition

Error message

cave_untrusted_tool_definition:${declared.name}

What it means

Every tool handed to `validateAgentGraph` must carry the hidden `Symbol.for("@caveman-ai/agent:tool-implementation-source")` string, which only the package's `tool()` factory stamps. An entry without it was not created by the factory — an object literal, a copy, or JSON-deserialized data — so its closure cannot be accounted or locked, and it is rejected as untrusted with the offending name in the message.

Source

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

    }
    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") {
        throw new Error(`cave_untrusted_tool_definition:${declared.name}`);
      }
      if (declared.runtime?.kind !== "subagent") continue;
      const child = declared.runtime.definition as AgentDefinition;
      visit(child, depth + 1, childSandboxRequired);
    }
    active.delete(definition);
    memo.add(definition);
  };

  visit(root, 0, false);
}

/**
 * True when any agent in the graph opts into host mode.
 *
 * Lock eligibility is a property of the whole graph, not of its root: a host
 * subagent runs its tool closures in the host process just as a host root does,
 * so its evidence shows no containment either. Follows the same subagent edges

View on GitHub (pinned to 766dce6b13)

Solutions

  1. Construct every tool with the package's `tool()` factory from `@caveman-ai/agent` primitives.
  2. Never persist/rehydrate tool objects via JSON — rebuild them from source at startup.
  3. If wrapping a tool, build the wrapper with `tool()` rather than copying properties onto a literal.

Example fix

// before
const search = { kind: "tool", name: "search", schema, run: doSearch };
// after
import { tool } from "@caveman-ai/agent";
const search = tool({ name: "search", schema, run: doSearch });
Defensive patterns

Strategy: type-guard

Validate before calling

const untrusted = definition.tools.filter((declared) => !isTrustedTool(declared));
if (untrusted.length > 0) {
  throw new Error(
    `tools not built by tool(): ${untrusted.map((tool) => tool.name).join(", ")}`,
  );
}

Type guard

const TOOL_SOURCE = Symbol.for("@caveman-ai/agent:tool-implementation-source");
function isTrustedTool(value: unknown): boolean {
  return typeof value === "object" && value !== null &&
    typeof Reflect.get(value, TOOL_SOURCE) === "string";
}

Prevention

When it happens

Trigger: Writing `{ kind: "tool", name: "x", ... }` object literals instead of `tool(...)`; rehydrating tools from JSON (symbols do not survive JSON); rebuilding tools by spreading an existing one into a plain object.

Common situations: Bypassing the factory for convenience; persisting agent definitions and loading them in a new process; wrapping tools via object spread instead of the factory's own wrapping support.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@766dce6b13 (2026-08-18). Data as JSON: /api/errors/a9e00a5356cf0e53. Report an issue: GitHub.