随笔
Claude Code 深度源码解读 03:工具执行是一台权限状态机
按源码顺序拆解 tool_use、schema 校验、PreToolUse、权限合并、Tool.call 与 tool_result 回灌。
简化版入口:Tools / Permissions。
我的设问与回应
设问 1:模型输出 tool_use 后,工具是不是就开始执行?
回应答案: 不是。tool_use 先进入 StreamingToolExecutor 和 runToolUse,再经过 findToolByName、inputSchema.safeParse、tool.validateInput、PreToolUse Hook、权限合并,最后才可能进入 Tool.call。
证据与证明路径: 证据是第 3 期核心流程图和 checkPermissionsAndCallTool 的机制拆解。unknown tool、schema invalid、validate invalid 都会在 Tool.call 前转成错误 tool_result。
还值得继续学习或反思: 进一步要追的是并发工具、abort、fallback 发生时所有 tool_use_id 是否都有合成结果覆盖。
设问 2:Hook allow 能不能绕过权限?
回应答案: 不能。PreToolUse 的 allow 还要进入 resolveHookPermissionDecision 和常规权限规则,deny / ask 仍然优先。
证据与证明路径: 证据来自第 3 期对 resolveHookPermissionDecision、hasPermissionsToUseTool、deny-first 规则的描述,以及拓展部分对 bypassPermissions 不是无限制执行的澄清。
还值得继续学习或反思: 任何外部自动审批都不能设计成最高优先级,否则 Hook 会从治理机制变成绕过机制。
源码调研主体
本组解读基于冻结源码快照进行教育、防御和架构研究。
本期主题:Harness 如何把模型产生的
tool_use约束为可验证、可拒绝、可审计、可回灌的行动。
1. 研究问题
本期围绕一个问题展开:
Claude Code Harness 如何让模型拥有行动能力,同时把行动约束在可控范围内?
普通 coding agent 的风险点在于:模型一旦能调用 shell、读写文件、访问网络或触发外部系统,就可能把“推理错误”放大为“环境副作用”。Harness 的工具链路不是简单的函数调用,而是一条带有工具可见性过滤、输入校验、权限决策、Hook 介入、用户/宿主确认、执行调度、结果映射和审计事件的控制链。
本期不讨论每个工具的具体业务实现,而聚焦所有工具共享的控制框架。
2. 对应总览节点
对应第 1 期总览图中的节点:
Tool ExecutionPermissionsPermission UI / Host DecisionTool HooksTool.calltool_resultTranscript / Session State
这部分机制参考前文「Tools / Permissions 机制图」。
3. 核心流程图
路线图中的代表链路是:
tool_use -> executor -> permission -> hook -> Tool.call -> tool_result源码中的实际执行顺序更细。尤其是 PreToolUse Hook 发生在最终权限决策之前;Hook 可以给出 allow / ask / deny 或更新输入,但 Hook 的 allow 不会绕过 deny / ask 规则。更准确的主线是:
这部分机制参考前文「Tools / Permissions 机制图」。
图中省略了 telemetry、progress message、structured output attachment、MCP 特殊输出更新等旁路;这些会在后文分类表中标注。
4. 关键源码入口
| 职责 | 文件 |
|---|---|
| 工具接口、权限上下文、运行时上下文 | src/Tool.ts |
| 工具注册与工具池组装 | src/tools.ts |
query loop 中接收和回灌 tool_use / tool_result | src/query.ts |
| 流式工具调度与并发控制 | src/services/tools/StreamingToolExecutor.ts |
| 单个工具执行主链路 | src/services/tools/toolExecution.ts |
| PreToolUse / PostToolUse 封装 | src/services/tools/toolHooks.ts |
| 权限规则、模式、classifier、headless 决策 | src/utils/permissions/permissions.ts |
| React/UI 权限决策入口 | src/hooks/useCanUseTool.tsx |
| 权限请求上下文、持久化和拒绝/允许构造 | src/hooks/toolPermission/PermissionContext.ts |
| 权限请求 UI 组件路由 | src/components/permissions/PermissionRequest.tsx |
| Bash 细粒度权限与 speculative classifier | src/tools/BashTool/bashPermissions.ts |
| sandbox 是否适用于 Bash | src/tools/BashTool/shouldUseSandbox.ts |
4.1 主线与场景特殊处理分类
| 机制/模块 | 分类 | 为什么这样归类 |
|---|---|---|
StreamingToolExecutor | 主线机制 | query loop 收到 tool_use 后的执行调度层,负责排队、并发安全、进度消息、剩余结果收集 |
runToolUse() | 主线机制 | 单个 tool_use 的入口,负责查找工具、兜底未知工具、调用统一执行链 |
checkPermissionsAndCallTool() | 主线机制 | 工具执行的核心控制函数,串起 schema 校验、工具校验、Hook、权限、调用和结果回灌 |
Tool.inputSchema / validateInput() | 主线机制 | 在权限和执行前验证模型给出的参数,避免无效输入进入副作用链 |
runPreToolUseHooks() | 安全/治理横切 | 在最终权限决策和工具调用前介入,可阻断、要求询问、允许、更新输入或追加上下文 |
resolveHookPermissionDecision() | 安全/治理横切 | 合并 Hook 权限结果和常规权限系统,并保证 Hook allow 不绕过 deny / ask 规则 |
hasPermissionsToUseTool() | 安全/治理横切 | 执行规则、工具专属权限、权限模式、auto classifier、headless 行为等权限语义 |
useCanUseTool() | 主线机制 + 场景增强聚合点 | 常规交互式会话的权限决策入口;同时包含 coordinator、swarm worker、bridge/channel callback 等场景分支 |
PermissionRequest UI | 场景增强 + 安全/治理横切 | 只在 ask 且可交互时出现;不同工具有不同审批 UI |
PermissionRequest Hook | 场景增强 + 安全/治理横切 | headless / async agent 或交互式权限流程中可由外部规则代替用户决策 |
filterToolsByDenyRules() | 安全/治理横切 | 在模型看到工具前过滤整类 deny 工具,降低误调用面 |
assembleToolPool() | 主线机制 + 场景增强聚合点 | 内置工具是主线;MCP 工具、feature flag 工具、REPL / coordinator / worktree 工具按场景加入 |
PostToolUse / PostToolUseFailure Hooks | 安全/治理横切 + 可观测/恢复支撑 | 工具完成或失败后追加上下文、阻断继续、修改 MCP 输出或记录 Hook 反馈 |
mapToolResultToToolResultBlockParam() | 主线机制 | 把工具内部返回值映射为 API 可消费的 tool_result,保证 query loop 能继续 |
progress messages / setInProgressToolUseIDs | 可观测/恢复支撑 | 支撑 UI 进度、可中断状态、并发工具状态展示 |
| telemetry / OTel / analytics | 可观测/恢复支撑 | 记录 tool decision、tool result、duration、permission source 等审计信息 |
| auto mode classifier | 场景增强 + 安全/治理横切 | 只在相关 feature、auto/plan-auto 模式下介入,用分类器替代部分人工确认 |
| sandbox auto allow | 场景增强 + 安全/治理横切 | 只在 Bash、sandbox 开启且命令适合 sandbox 时影响 ask 规则 |
| MCP tool output update | 场景增强 | 只对 MCP 工具的 PostToolUse 输出修改路径特殊处理 |
5. 机制拆解
5.1 工具池先被“模型可见性”约束
工具控制不是从执行时才开始。src/tools.ts 先决定模型能看到哪些工具:
这部分机制参考前文「Tools / Permissions 机制图」。
这一步有两个效果:
- 整类 deny 的工具不会出现在模型工具列表里。
- 内置工具、MCP 工具、feature flag 工具、REPL 模式工具和 coordinator 模式工具在进入模型窗口前被统一组装。
这不是完整权限判断。它只是第一层可见性收缩;真正的内容级权限仍在执行时判断。
5.2 StreamingToolExecutor 把流式 tool_use 变成受控调度
query.ts 在模型流式输出中收集 tool_use block,并交给 StreamingToolExecutor。这个执行器解决三个问题:
- 模型可能一次或连续流出多个工具调用。
- 某些工具可以并发,某些工具必须独占执行。
- 即使流式重试、用户中断或兄弟工具失败,也必须给每个
tool_use_id生成匹配的tool_result或合成错误结果,避免 API 轨迹断裂。
StreamingToolExecutor 会根据工具的 isConcurrencySafe() 决定是否并发执行。并发安全工具可以一起跑;非并发工具会阻塞后续执行。Bash 工具失败时会中止兄弟 Bash 相关执行,避免依赖链继续放大错误。
5.3 runToolUse 是单个工具调用的入口
runToolUse() 做的第一步是 findToolByName():
- 如果当前工具池找不到工具,返回一个
is_error: true的tool_result。 - 如果是旧 transcript 里通过 alias 调用的 deprecated 工具,会尝试从基础工具列表中找 alias 兜底。
- 找到工具后,进入
streamedCheckPermissionsAndCallTool()。
这个设计保证模型“幻觉工具名”不会直接崩掉主循环,而是变成模型可见的工具错误结果,让 query loop 可以继续修正。
5.4 输入校验在权限和执行之前
checkPermissionsAndCallTool() 先做两级校验:
| 层级 | 作用 |
|---|---|
tool.inputSchema.safeParse(input) | 校验模型输出的数据类型和结构 |
tool.validateInput(parsedInput, context) | 校验工具自己的业务约束,例如路径、参数组合或调用条件 |
如果任一层失败,Harness 不进入权限系统,也不调用工具,而是构造 tool_result 错误回灌给模型。
这对稳定性很重要:参数错误是模型可修复的错误,不应该升级成真实副作用。
5.5 PreToolUse Hook 在最终权限决策前介入
runPreToolUseHooks() 调用 executePreToolHooks(),可以产生多种结果:
| Hook 输出 | 进入主链路后的效果 |
|---|---|
blockingError | 转为 deny 型权限结果 |
permissionBehavior: allow | 交给 resolveHookPermissionDecision(),仍需检查 deny / ask 规则 |
permissionBehavior: ask | 强制进入 ask 语义,可附带 Hook 的原因 |
permissionBehavior: deny | 阻止工具调用 |
updatedInput | 更新后续权限检查和工具调用使用的输入 |
additionalContexts | 注入 Hook 附加上下文 |
preventContinuation / stopReason | 阻止继续执行或给模型返回停止原因 |
关键边界是:PreToolUse Hook 的 allow 不是绝对通行证。resolveHookPermissionDecision() 会再次执行规则检查,deny 规则和 ask 规则仍然生效。
5.6 权限系统分为规则、工具专属逻辑和模式逻辑
hasPermissionsToUseTool() 与内部的 hasPermissionsToUseToolInner() 实现了分层权限判断:
这部分机制参考前文「Tools / Permissions 机制图」。
几个关键点:
- deny 规则优先级最高。
- 工具自己的
checkPermissions()可以做内容级判断,例如 Bash 子命令、文件路径安全检查等。 bypassPermissions不是无条件绕过;deny、内容 ask、安全检查、必须交互的工具仍可优先生效。passthrough最终会转成ask,而不是默认 allow。dontAsk会把 ask 转成 deny。- auto mode 可以用 acceptEdits 快路径、安全工具 allowlist 或 classifier 代替人工确认。
- headless / async agent 不能弹 UI 时,会先跑
PermissionRequestHook;没有 Hook 决策则自动拒绝。
5.7 useCanUseTool 把 ask 接到用户或宿主决策
useCanUseTool() 是执行链传入的 canUseTool。它把纯权限结果接到交互式或宿主式决策:
这部分机制参考前文「Tools / Permissions 机制图」。
PermissionContext 负责构造 allow / deny 结果、持久化权限更新、记录 decision source,并在用户拒绝或中断时触发 abort。PermissionRequest.tsx 根据工具类型路由到不同的 UI,例如 Bash、FileEdit、FileWrite、WebFetch、Skill、PowerShell、ExitPlanMode 等。
这里必须区分常规主线和场景增强:
- 常规 CLI 可交互路径会显示权限请求 UI。
- coordinator、swarm worker、bridge callback、channel callback、headless async agent 只在对应产品模式或 feature flag 下生效。
- auto classifier 只在相关 feature 和权限模式下介入。
5.8 Tool.call 是被包裹的副作用点
只有当权限结果是 allow 后,checkPermissionsAndCallTool() 才调用:
tool.call(callInput, ToolUseContext with toolUseId, canUseTool, assistantMessage, progress)调用前后 Harness 会处理:
- 开始和结束 tool span。
- 记录 session activity。
- 注入
toolUseId。 - 标记
userModified,说明用户审批时是否改过输入。 - 接收工具 progress 回调。
- 记录 tool duration。
- 处理 structured output attachment。
- 保存 context modifier,允许工具修改后续
ToolUseContext。
因此 Tool.call 是真正副作用点,但它不是裸调用;它被上下文、权限、Hook、telemetry 和结果映射包裹。
5.9 tool_result 是 query loop 能继续的协议边界
工具调用成功后,Harness 通过 mapToolResultToToolResultBlockParam() 或 processToolResultBlock() 把工具内部返回值映射为 tool_result。失败、拒绝、未知工具、输入错误也都会被构造成 tool_result。
这个协议边界有三层意义:
- API 轨迹完整:每个
tool_use_id都应有匹配的tool_result。 - 模型可修正:错误以模型可见消息回灌,而不是仅停留在本地异常。
- 审计可追踪:结果消息关联原 assistant message、tool use id、permission decision 和 telemetry。
5.10 PostToolUse Hook 在结果之后介入
工具成功后会执行 runPostToolUseHooks();失败路径会执行 runPostToolUseFailureHooks()。
PostToolUse 可做的事包括:
- 追加 Hook 消息。
- 返回 blocking error。
- prevent continuation。
- 追加 additional context。
- 对 MCP 工具更新输出。
对非 MCP 工具,工具结果通常先加入,再追加 PostToolUse Hook 输出。对 MCP 工具,源码中有特殊处理:PostToolUse 可以更新 MCP tool output,随后再把最终输出加入 tool_result。
6. 对稳定解决问题能力的贡献
6.1 把模型行动变成可拒绝的行动
模型只负责提出 tool_use。是否执行,由 Harness 的工具可见性、输入校验、权限规则、Hook、用户/宿主决策共同决定。这降低了模型错误直接转成环境副作用的概率。
6.2 把失败转成可恢复上下文
未知工具、schema 错误、业务校验失败、权限拒绝、Hook 阻断、用户中断、工具异常,都会尽量转换成 tool_result 或附件消息。模型下一轮可以基于这些反馈调整计划。
6.3 把权限从单点判断变成分层治理
权限不只是一个 allow/deny 开关,而是多层组合:
- 模型可见性过滤。
- 工具级
checkPermissions()。 - 规则源:settings、policy、CLI、session、command。
- 模式:default、acceptEdits、plan、bypassPermissions、dontAsk、auto。
- Hook:PreToolUse、PermissionRequest、PostToolUse。
- 场景:interactive、headless、async agent、coordinator、swarm、bridge/channel。
这让 Harness 可以在不同运行环境中保持同一套基本语义,同时调整交互方式。
6.4 把并发工具执行限制在协议安全范围内
StreamingToolExecutor 允许并发安全工具并行执行,但会维护结果顺序和 tool_use_id 匹配。对不能并发的工具,它选择串行。对中断和失败,它生成合成错误结果,避免 orphan tool_result 或 orphan tool_use 破坏后续模型请求。
6.5 把审计信息嵌入执行链
执行链记录 permission decision、decision source、tool_result、duration、MCP 信息、classifier 结果、Hook 时长等。这些信息不一定是模型主上下文的一部分,但对恢复、调试、治理和复盘非常关键。
7. 风险、边界与待验证问题
7.1 Hook allow 与规则权限的边界必须保持清晰
源码明确保证 PreToolUse Hook 的 allow 不会绕过 deny / ask 规则。后续若修改 Hook 或权限路径,需要重点验证这个不变量没有被破坏。
7.2 auto mode classifier 是场景增强,不是主线必经
auto mode classifier 只在相关 feature 和权限模式下介入。它可能 fail closed、fail open、fallback to prompt 或在 headless 下 abort。文档中不能把它写成所有工具调用必经路径。
7.3 bypassPermissions 不是无限制执行
bypassPermissions 仍受 deny、内容级 ask、安全检查、requiresUserInteraction 等优先路径限制。把 bypass 写成“完全无权限”会误导安全边界理解。
7.4 MCP 工具与内置工具结果顺序存在差异
MCP 工具的 PostToolUse 可以更新输出,导致其结果添加时机与非 MCP 工具不同。后续第 8 期安全边界复盘时,应单独检查 MCP tool output 更新、MCP 权限规则匹配、MCP server scope 和 channel permission relay。
7.5 并发工具的 contextModifier 暂不支持完整合并
StreamingToolExecutor 中注释指出 concurrent tools 当前不支持 context modifiers 的完整合并;只有非并发工具会把 modifier 应用到 executor 的 ToolUseContext。这属于后续审计并发副作用时的关注点。
7.6 文档待验证点
本期基于本地冻结快照静态阅读源码,未运行测试或启动服务。后续若需要更高置信度,可在不联网、不安装依赖的前提下做只读补充:
- 继续追踪
query.ts中StreamingToolExecutor的调用点和tool_result回灌细节。 - 对 Bash、FileEdit、WebFetch、MCPTool 各选一个工具,补充工具专属权限案例。
- 对
PermissionRequestHook 和executePermissionRequestHooks()做专门一节,放入第 5 期 Hooks 报告。
8. 下一期衔接
第 4 期建议进入“Query Loop 与错误恢复”:
长任务为什么不会轻易中断或跑飞?
本期已经说明了单个 tool_use 如何变成 tool_result。第 4 期应继续追踪 query.ts 如何处理:
- 流式模型响应。
- tool_result 追加后的下一轮 query。
- stop hook。
- prompt too long / max output / fallback model。
- compact 与 recovery。
- orphan tool_use / tool_result 的修复策略。
也就是说,第 3 期解释“行动如何被约束并回灌”,第 4 期要解释“回灌之后主循环如何继续、修正或恢复”。
9. 拓展追查:权限、缓存、工具协议、sandbox 与大结果治理
本节针对后续追问补充六个工程化问题。它们不是第 3 期主线的必经细节,但能解释 Claude Code Harness 为什么能在“工具种类多、动态变化、返回内容大、权限复杂”的情况下仍保持稳定。
9.1 Permission 是否像 Unix 一样把外设 / API 抽象为 read / write
结论:不是。
Claude Code 的 permission 没有采用 Unix “一切皆文件”的统一文件描述符模型,也没有把所有外设/API 都抽象成 read / write 两类操作。它更像是一个“工具能力对象 + 参数语义 + 规则源 + 模式 + Hook”的多维权限系统。
核心抽象在 src/Tool.ts 的 Tool 接口中。每个工具都必须给出:
| 字段/方法 | 权限意义 |
|---|---|
name | 权限规则匹配和模型工具调用的基本标识 |
inputSchema / inputJSONSchema | 规定模型可提交的参数结构 |
isReadOnly(input) | 给 UI、分类器、审计判断该调用是否读操作 |
isDestructive(input) | 标注删除、覆盖、发送等不可逆操作 |
isOpenWorld(input) | 标注是否触达开放外部世界 |
checkPermissions(input, context) | 工具专属权限判断,例如 Bash 子命令、WebFetch 域名、文件路径 |
preparePermissionMatcher(input) | 为 Hook / permission rule 的内容匹配准备工具专属 matcher |
requiresUserInteraction() | 即使在特殊模式下也必须保留交互确认的工具 |
因此,文件工具当然有 read/write 语义;但 Bash、WebFetch、MCP、Agent、Skill、Task、ScheduleCron 等工具并不会被强行压成文件读写。它们通过工具名和参数内容表达权限边界。
权限稳定性主要靠以下机制保证:
- 工具可见性先收缩:
filterToolsByDenyRules()在工具发送给模型前移除整类 deny 工具;MCP server 级规则也可在这里生效。 - 工具名是权限命名空间:规则写成
ToolName或ToolName(ruleContent);MCP 工具有mcp__server__tool命名,并支持 server-level 匹配。 - 规则按来源分层:
userSettings、projectSettings、localSettings、policySettings、flagSettings、cliArg、command、session等来源被统一装入ToolPermissionContext。 - deny / ask 优先于 allow:
hasPermissionsToUseToolInner()先处理 deny、ask、工具专属 deny、内容 ask、安全检查,再考虑 bypass/allow。 - 动态工具也走同一接口:MCP 工具被包装成
Tool,携带inputJSONSchema、isMcp、mcpInfo,进入同一工具池、同一 deny 过滤和同一执行链。 - Hook allow 不越权:
resolveHookPermissionDecision()明确再次执行规则检查,保证 PreToolUse Hook 的 allow 不绕过 deny/ask 规则。 - 工具默认偏保守:
buildTool()默认isConcurrencySafe=false、isReadOnly=false,没有声明并发安全或只读的工具不会被乐观对待。
这套模型比 Unix read/write 更复杂,但适合 agent 工具:权限不是“文件句柄能否读写”,而是“这个工具以这些参数在当前模式、规则、Hook、环境下是否可执行”。
9.2 工具章节中的模型 API cache 优化
工具系统对 prompt cache 很敏感,因为工具 schema 往往很大,并且出现在 API 请求前缀中。一个 MCP server 重连、一个 feature flag 翻转、一个工具 prompt 改变,都可能导致工具 schema 字节变化,进而破坏整段缓存前缀。
源码中的主要优化手段如下。
9.2.1 工具池排序保持 cache 前缀稳定
assembleToolPool() 会把 built-in tools 和 MCP tools 分区排序,并让 built-in tools 保持连续前缀:
- built-in tools 排序后作为前缀。
- MCP tools 排序后追加。
uniqBy(..., 'name')去重,内置工具优先。
注释明确说明:如果把 built-in 和 MCP 混在一起全局排序,新增 MCP 工具可能插入 built-in 中间,导致后续缓存 key 全部变化。
9.2.2 tool schema session cache 锁定首次渲染字节
src/utils/toolSchemaCache.ts 提供 session 级 TOOL_SCHEMA_CACHE。toolToAPISchema() 把这些字段作为 base schema 缓存:
namedescriptioninput_schemastricteager_input_streaming
这样做是为了避免会话中途 GrowthBook gate、MCP reconnect、tool.prompt() 动态内容漂移导致 schema 字节变化。对 inputJSONSchema 工具,cache key 包含 schema 内容,避免同名但不同 schema 的结构化输出或动态工具复用旧 schema。
9.2.3 Zod 到 JSON Schema 转换按对象身份缓存
src/utils/zodToJsonSchema.ts 用 WeakMap<ZodTypeAny, JsonSchema7Type> 缓存转换结果。工具 schema 由 lazySchema() 保证同一会话内对象引用稳定,因此每轮 API 请求不需要对 60 到 250 个工具反复转换。
9.2.4 动态工具延迟加载减少 schema 体积
ToolSearch 是工具 cache 的核心工程化机制之一:
isDeferredTool()将 MCP 工具和shouldDefer: true的工具标为 deferred。- API 请求里 deferred 工具可带
defer_loading: true。 - 模型先调用
ToolSearch,返回tool_referenceblocks。 extractDiscoveredToolNames()从历史消息中扫描已发现工具,后续请求只把这些工具的完整 schema 纳入tools。
这解决两个问题:
- MCP / 插件 / 场景工具数量可能非常多,不能把所有 schema 一次性塞入 prompt。
- 动态连接或断开的工具不会频繁改写主工具前缀。
9.2.5 cache_control 与 cache_reference 控制缓存边界
buildSystemPromptBlocks() 为 system prompt block 加 cache_control。addCacheBreakpoints() 在消息层只放一个 cache marker,并对 marker 之前的 tool_result 添加 cache_reference: tool_use_id。
注释强调“每次请求只保留一个 message-level cache_control marker”。这是为了避免多个 cache marker 让底层 KV 页不能及时释放,形成无用缓存尾巴。
9.2.6 beta header latch 避免中途翻转破坏 cache key
claude.ts 对 fast mode、AFK/auto mode、cache editing 等 beta header 使用 session latch:一旦某个 header 在本会话启用,就保持发送,直到 clear/compact 等清理动作。这避免中途开关导致服务端 cache key 变化。
9.2.7 prompt cache break detection 排除 defer_loading 工具
recordPromptState() 做缓存破坏检测时,会排除 defer_loading 工具,因为 API 会把它们从真实 prompt 中剥离。若把它们纳入 hash,会产生“工具 schema 改了”的误报。
9.3 工具调用如何获得、标准结构是什么、如何保证成功率和安全性
工具调用来源分两层:
- 可调用工具列表:由
getAllBaseTools()、feature flags、环境变量、settings、MCP tools、agent/team/worktree 等状态组装,再通过权限过滤后传给模型。 - 具体调用指令:由模型根据 API
toolsschema 生成 assistant content block。
模型给出的标准工具调用结构在代码中按 ToolUseBlock 处理,核心字段是:
{
type: 'tool_use',
id: string,
name: string,
input: Record<string, unknown>
}工具结果回灌结构是:
{
type: 'tool_result',
tool_use_id: string,
content: string | ContentBlock[],
is_error?: boolean
}成功率和安全性的保障链路:
| 阶段 | 保障 |
|---|---|
| 工具暴露前 | deny 规则过滤、feature flag、isEnabled、工具池排序与去重 |
| API schema 构造 | Zod/JSON Schema、strict tools、defer_loading、schema cache |
| 模型输出后 | findToolByName() 防幻觉工具名,alias 兼容旧 transcript |
| 输入执行前 | inputSchema.safeParse() 和 validateInput() |
| 权限执行前 | PreToolUse Hook 可阻断、更新输入或要求 ask |
| 权限阶段 | deny/ask/allow、工具专属 check、模式、classifier、headless fallback |
| 调用阶段 | Tool.call() 接收 scoped ToolUseContext、abort signal、progress callback |
| 结果阶段 | result block 映射、大结果持久化、empty result 占位、PostToolUse Hook |
| query loop 阶段 | ensureToolResultPairing() 修复 orphan tool_use/tool_result |
多个工具调用时,顺序和并发的分工是:
- 模型决定工具调用的出现顺序:assistant message 里
tool_useblocks 的顺序就是 executor 接收顺序。 - 工具定义决定是否并发安全:每个工具实现
isConcurrencySafe(input)。 StreamingToolExecutor决定何时启动:
- 如果当前没有执行中的工具,可以启动。
- 如果当前执行中的工具都并发安全,且新工具也并发安全,可以并发。
- 如果遇到非并发安全工具,它必须独占,后续队列等待。
- 结果输出保持接收顺序约束:即使部分工具并发执行,executor 仍维护结果缓冲和 yielded 状态,避免协议层乱序。
顺序不是由一个单独 planner 重新排序;它主要来自模型输出顺序。Harness 只判断哪些可以提前并发,哪些必须等待。
质量保障的关键是“默认不并发”:buildTool() 默认 isConcurrencySafe=false。工具作者只有显式证明安全,才会打开并发。
9.4 sandbox 机制由哪些部分整合而来
sandbox 是 @anthropic-ai/sandbox-runtime 加 Claude Code 自己的 settings/permissions 转换层。
主要入口是 src/utils/sandbox/sandbox-adapter.ts:
这部分机制参考前文「Tools / Permissions 机制图」。
整合内容包括:
- 网络域名 allow/deny:来自
sandbox.network.*和WebFetch(domain:...)permission rules。 - 文件系统 allow/deny:来自
FileEdit(...)、FileRead(...)、sandbox.filesystem.*。 - 默认可写:当前目录、Claude temp dir、additional directories。
- 默认保护:settings 文件、managed settings drop-in、
.claude/skills。 - git worktree 例外:worktree 中允许主 repo
.git需要的写入路径。 - bare repo 文件防护:对
HEAD、objects、refs、hooks、config做 deny 或 post-command scrub,防止 sandboxed command 种植 bare repo 影响后续 unsandboxed git。 - 平台/依赖检查:macOS、Linux、WSL2;依赖缺失时可提示或按配置 fail。
- settings 热更新:
settingsChangeDetector变化后BaseSandboxManager.updateConfig()。 - Bash 接入:
shouldUseSandbox()根据 sandbox 是否启用、dangerouslyDisableSandbox、excludedCommands 决定 Bash 命令是否包进 sandbox。
需要注意:excludedCommands 注释明确说它是用户便利功能,不是安全边界;真正安全边界是 sandbox runtime 和 permission prompt。
9.5 内置工具列表、功能与适配环境
以下基于 src/tools.ts#getAllBaseTools() 和各工具 isEnabled() / feature flag 条件整理。这个列表是“可能内置”,不是每次会话都必然暴露;实际可见列表还会被 permission deny、模式、MCP 状态、ToolSearch defer、agent restrictions 过滤。
| 工具 | 功能 | 适配/启用条件 |
|---|---|---|
Agent | 启动子 agent / task 类协作 | 基础工具;可受 agent 类型和 coordinator 限制 |
TaskOutput | 获取 task 输出 | deferred;任务系统启用时有意义 |
Bash | 执行 shell 命令 | 基础工具;受 Bash permissions、sandbox、read-only validation、background task 配置影响 |
Glob | 文件 glob 搜索 | 若环境有 embedded search tools 则可能隐藏 |
Grep | 文本搜索 | 若环境有 embedded search tools 则可能隐藏 |
ExitPlanMode / ExitPlanModeV2 | 请求退出 plan mode 并进入执行 | deferred;plan mode 相关 |
Read | 读取文件内容 | 基础工具;结果自限,maxResultSizeChars=Infinity |
Edit | 编辑文件片段 | 基础工具;受路径、安全检查、权限 UI 影响 |
Write | 写入文件 | 基础工具;受路径、安全检查、权限 UI 影响 |
NotebookEdit | 编辑 notebook | deferred;Jupyter/notebook 场景 |
WebFetch | 抓取 URL 内容 | deferred;受域名权限、网络/sandbox/policy 影响 |
TodoWrite | 更新待办/任务状态 | deferred;todo v1/v2 状态相关 |
WebSearch | Web 搜索 | deferred;受 WebSearch 可用性和网络/服务能力影响 |
TaskStop | 停止任务 | deferred;任务/agent 场景 |
AskUserQuestion | 让模型请求用户回答问题 | deferred;通常需要交互或宿主支持 |
Skill | 加载/使用技能 | 基础工具;受技能系统和权限 UI 影响 |
EnterPlanMode | 进入 plan mode | deferred;plan mode 可用时 |
Config | 配置相关操作 | USER_TYPE === 'ant' |
Tungsten | 内部工具 | USER_TYPE === 'ant' |
SuggestBackgroundPR | 建议后台 PR | USER_TYPE === 'ant' 且模块存在 |
WebBrowser | 浏览器工具 | WEB_BROWSER_TOOL feature |
TaskCreate / TaskGet / TaskUpdate / TaskList | task v2 管理 | isTodoV2Enabled() |
OverflowTest | 溢出测试工具 | OVERFLOW_TEST_TOOL feature |
CtxInspect | 上下文检查 | CONTEXT_COLLAPSE feature |
TerminalCapture | 终端捕获 | TERMINAL_PANEL feature |
LSP | LSP 符号/诊断能力 | ENABLE_LSP_TOOL 环境变量;初始化未完成时可 defer |
EnterWorktree / ExitWorktree | worktree 模式切换 | isWorktreeModeEnabled() |
SendMessage | 给 teammate / agent 发送消息 | 基础装配;具体可用性受团队/消息系统影响 |
ListPeers | 列出 peers | UDS_INBOX feature |
TeamCreate / TeamDelete | team/swarm 管理 | isAgentSwarmsEnabled() |
VerifyPlanExecution | 验证计划执行 | CLAUDE_CODE_VERIFY_PLAN=true |
REPL | REPL wrapper 工具 | USER_TYPE === 'ant' 且 REPL 模式可用;启用时隐藏部分 primitive tools |
Workflow | workflow scripts | WORKFLOW_SCRIPTS feature |
Sleep | 延时/等待 | PROACTIVE 或 KAIROS feature |
CronCreate / CronDelete / CronList | 定时任务管理 | AGENT_TRIGGERS feature |
RemoteTrigger | remote trigger | AGENT_TRIGGERS_REMOTE feature |
Monitor | monitor 工具 | MONITOR_TOOL feature |
Brief | brief / 文件上传型沟通 | 基础装配,但 isEnabled() 受 Kairos/Brief 配置影响 |
SendUserFile | 发送用户文件 | KAIROS feature |
PushNotification | 推送通知 | KAIROS 或 KAIROS_PUSH_NOTIFICATION |
SubscribePR | GitHub PR webhook 订阅 | KAIROS_GITHUB_WEBHOOKS |
PowerShell | 执行 PowerShell | isPowerShellToolEnabled();Windows/PowerShell 相关权限和 auto mode 限制 |
Snip | 历史 snip | HISTORY_SNIP feature |
TestingPermission | 测试权限工具 | NODE_ENV === 'test' |
ListMcpResources | 列 MCP resources | 特殊工具,通常不在普通 built-in tool 前缀中直接暴露 |
ReadMcpResource | 读 MCP resource | deferred |
ToolSearch | 动态发现 deferred tools | isToolSearchEnabledOptimistic() |
此外,MCP server 暴露的工具不在 getAllBaseTools() 静态列表里;它们从 app state 的 MCP connection 进入 assembleToolPool(),通常以 mcp__server__tool 命名,并默认 deferred,除非设置 alwaysLoad。
9.6 超大工具返回内容如何避免上下文崩溃
大工具结果治理主要在 src/utils/toolResultStorage.ts 和 src/constants/toolLimits.ts。
9.6.1 单工具结果持久化
每个工具声明 maxResultSizeChars。默认系统上限是:
DEFAULT_MAX_RESULT_SIZE_CHARS = 50_000MAX_TOOL_RESULT_TOKENS = 100_000MAX_TOOL_RESULT_BYTES = 400_000
当 tool_result 超过阈值时,maybePersistLargeToolResult() 会:
- 把完整结果保存到 session 目录下的
tool-results/{tool_use_id}.txt|json。 - 给模型只返回
<persisted-output>包裹的说明、文件路径和前 2000 bytes preview。 - 记录原始大小、压缩后大小和估算 token 节省。
这不是简单截断,而是“保留可恢复路径 + 给模型预览”。模型可以决定是否再读取文件。
例外:包含 image block 的结果不会持久化,因为图片必须原样发给模型;Read 工具也设置 maxResultSizeChars=Infinity,因为 Read 自己已经有范围/行数限制,把 Read 输出再存文件会形成 Read -> file -> Read 的循环。
9.6.2 空结果占位
空 tool_result 会被替换为:
(ToolName completed with no output)源码注释说明,空 tool_result 在某些模型/服务端渲染边界下可能导致模型误判 turn boundary 并停止输出。这个小占位是稳定性补丁。
9.6.3 同一 user message 的聚合预算
并发工具可能出现“每个结果都不大,但合并成一个 user message 后很大”的问题。例如 10 个并发工具各返回 40K,单个没超 50K,但合并后 400K。
MAX_TOOL_RESULTS_PER_MESSAGE_CHARS = 200_000 用于限制单个 wire-level user message 中所有 tool_result 的总大小。超出时会替换最大块,直到回到预算内。
9.6.4 替换决策持久化,保护 prompt cache
ContentReplacementState 记录:
seenIds:哪些 tool_use_id 已经做过预算判断。replacements:哪些结果被替换,以及模型看到的精确 replacement 字符串。
这样后续 turn 不会因为重新计算 preview、路径、格式而产生不同字节,破坏 prompt cache。resume 时也会从 transcript 记录重建 replacement 决策。
9.6.5 tool_reference 消息受到特殊保护
ToolSearch 的 tool_reference block 表示“这个工具 schema 已被发现”。compaction 时会把已发现工具集合写入 compact boundary 的 metadata;snip 则保护带 tool_reference 的消息不被移除。否则模型可能失去已加载工具的 schema 依据。
机制主线收束
站在架构师视角,我会把这组源码机制收束为三个问题:为什么需要它,什么时候必须引入它,以及真正要设计的是什么。
- why:工具权限状态机解决的是模型错误推理被放大成真实副作用的问题。
- when:只要 agent 能读写文件、跑命令、访问网络或调用外部工具,就必须把工具调用协议化。
- what:工具接口自带 schema、权限语义、只读/破坏性判断;Hook allow 不覆盖 deny;所有结果回灌为 tool_result。
Tools / Permissions 这组源码最值得带走的,不是某个函数或某个配置项,而是它如何把模型能力放进一组可验证、可拒绝、可恢复、可审计的工程边界里。读源码时,如果只记住名词,会很快散;如果抓住 why、when、what,就能把这组机制迁移到自己的 agent 架构判断中。