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 edgesView on GitHub (pinned to 766dce6b13)
Solutions
- Construct every tool with the package's `tool()` factory from `@caveman-ai/agent` primitives.
- Never persist/rehydrate tool objects via JSON — rebuild them from source at startup.
- 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
- Always construct tools with the package's tool() factory; never object literals or JSON copies.
- Rebuild tool definitions from source code at process startup instead of persisting them.
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
- cave_live_eval_sandbox_profile_escapes_root
- caveman-code: path escapes the workspace: ${candidate}
- caveman agent: file source escapes project root
- cave_host_sandbox_nested_under_required
- cave_tool_definition_invalid
AI-assisted analysis of JuliusBrussee/caveman@766dce6b13 (2026-08-18).
Data as JSON: /api/errors/a9e00a5356cf0e53.
Report an issue: GitHub.