{"record":{"id":"a9e00a5356cf0e53","repo":"JuliusBrussee/caveman","slug":"cave-untrusted-tool-definition","errorCode":"cave_untrusted_tool_definition","errorMessage":"cave_untrusted_tool_definition:${declared.name}","messagePattern":"cave_untrusted_tool_definition:(.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/agent/src/definition-graph.ts","lineNumber":46,"sourceCode":"    }\n    const memo = visited[sandboxRequired ? 1 : 0]!;\n    if (memo.has(definition)) return;\n    active.add(definition);\n    const childSandboxRequired = sandboxRequired ||\n      definition.sandbox === \"required\";\n    const names = new Set<string>();\n    for (const declared of definition.tools) {\n      if (!declared || declared.kind !== \"tool\" ||\n          typeof declared.name !== \"string\") {\n        throw new Error(\"cave_tool_definition_invalid\");\n      }\n      if (names.has(declared.name)) throw new Error(\"cave_duplicate_tool_name\");\n      names.add(declared.name);\n      if (declared.name.startsWith(\"cave_\")) {\n        throw new Error(`cave_reserved_tool_name:${declared.name}`);\n      }\n      if (typeof Reflect.get(declared, TOOL_IMPLEMENTATION_SOURCE) !== \"string\") {\n        throw new Error(`cave_untrusted_tool_definition:${declared.name}`);\n      }\n      if (declared.runtime?.kind !== \"subagent\") continue;\n      const child = declared.runtime.definition as AgentDefinition;\n      visit(child, depth + 1, childSandboxRequired);\n    }\n    active.delete(definition);\n    memo.add(definition);\n  };\n\n  visit(root, 0, false);\n}\n\n/**\n * True when any agent in the graph opts into host mode.\n *\n * Lock eligibility is a property of the whole graph, not of its root: a host\n * subagent runs its tool closures in the host process just as a host root does,\n * so its evidence shows no containment either. Follows the same subagent edges","sourceCodeStart":28,"sourceCodeEnd":64,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/766dce6b1394ebb56a3090748d5a0240a5aefb36/packages/agent/src/definition-graph.ts#L28-L64","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nconst search = { kind: \"tool\", name: \"search\", schema, run: doSearch };\n// after\nimport { tool } from \"@caveman-ai/agent\";\nconst search = tool({ name: \"search\", schema, run: doSearch });","handlingStrategy":"type-guard","validationCode":"const untrusted = definition.tools.filter((declared) => !isTrustedTool(declared));\nif (untrusted.length > 0) {\n  throw new Error(\n    `tools not built by tool(): ${untrusted.map((tool) => tool.name).join(\", \")}`,\n  );\n}","typeGuard":"const TOOL_SOURCE = Symbol.for(\"@caveman-ai/agent:tool-implementation-source\");\nfunction isTrustedTool(value: unknown): boolean {\n  return typeof value === \"object\" && value !== null &&\n    typeof Reflect.get(value, TOOL_SOURCE) === \"string\";\n}","tryCatchPattern":null,"preventionTips":["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."],"tags":["security","tool-definition","trust","factory"],"backgroundTag":"untrusted-definition-rejected","analyzedSha":"766dce6b1394ebb56a3090748d5a0240a5aefb36","analyzedAt":"2026-08-18T03:14:35.516Z","contentChangedAt":"2026-08-18T03:14:35.516Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}