工具契约、注册表与可见性
一个 ZCode 工具要声明哪些字段、其中哪些真正被读取;内置工具与 MCP 工具怎样进同一张注册表;每回合发给模型的工具清单怎样按端口、开关与模型能力层层筛选、排序并投影成 JSON Schema;参数不合格时模型收到什么。附内置工具全表。
模型每一步能调用哪些工具、每个工具有什么副作用、出了错怎样告诉模型,都写在一份声明式的“工具契约”里。契约的类型和各工具的 zod schema 放在 @zcode/contracts,具体取值(权限、结果预算、超时、取消)大多写在 core 各 handler 的 ToolEntry 里,ToolEntry 同时挂上 handler 和一组可选钩子。40 个内置条目过了注册门之后,和运行时接入的 MCP 工具一起进同一张 ToolRegistry,再经过几道筛选变成发给模型的 ModelToolContract[]。
本篇讲“声明、注册、可见”:契约有哪些字段、哪些真的有人读;注册表怎样组织;模型看到的清单怎样定下来;参数不合格时模型收到什么。执行器怎样调度、审批和收尾,留给下一篇执行器:调度、审批、超时与结果。
| 位置 | 内容 |
|---|---|
apps/zcode-cli/packages/contracts/src/tools | 契约类型 contract.ts、zod 转 JSON Schema 的 json-schema.ts、各工具的输入输出 schema 与名字常量 |
apps/zcode-cli/packages/core/src/tool/*.ts | ToolEntry 与执行上下文、注册表、可见性与排序、JSON Schema 校验器、参数错误投影、路径策略 |
apps/zcode-cli/packages/core/src/tool/handlers | 内置工具实现与注册总表 index.ts;generated/ 下只有 Bash 只读判定用的命令注册表 |
apps/zcode-cli/packages/core/src/mcp | 把 MCP 工具描述投影成 ToolEntry |
apps/zcode-cli/packages/core/src/runtime/helpers/runtime-tools.ts、tool-allowlist.ts | 运行时装配时的注册门 |
怎么用
注册表不对用户开放,能改变工具清单的入口是这些:
| 入口 | 效果 | 出处 |
|---|---|---|
--disallowedTools 或 --disallowed-tools(TUI 与 -p 都认) | 按工具名整项剔除,注册时就不装 | apps/zcode-cli/packages/cli/src/arguments.ts:125 |
配置 features.skill、features.subagent、features.mcp 设为 false | 分别去掉 Skill、Agent 一族(Agent、Task、SendMessage)、全部 MCP 工具 | apps/zcode-cli/packages/bootstrap/src/app/create-app.ts:746、apps/zcode-cli/packages/bootstrap/src/app/runtime-config.ts:156、runtime-config.ts:168 |
| 启用官方 browser-use 插件 | 打开 runtimeFeatures.nodeRepl,注册 js 工具 | apps/zcode-cli/packages/core/src/runtime/helpers/runtime-tools.ts:47 |
| 桌面、Web 等宿主创建的协议会话 | 总是注入定时任务端口(Cron 工具);闲时工具与动态工作流工具要宿主显式开放,默认关闭 | apps/zcode-cli/packages/bootstrap/src/zcode-protocol/server-operations.ts:3337、server-operations.ts:3370、server-operations.ts:3373 |
协议会话参数 toolAllowlist、toolDenylist | 会话级白名单与黑名单,内置工具和 MCP 工具都受约束 | server-operations.ts:3342 |
--disallowedTools 接受 Bash(rm:*) 这类带括号的规则,但注册层只取括号前的名字(apps/zcode-cli/packages/core/src/tool/tool-visibility.ts:5)。从代码看,这样一条规则会让整个 Bash 都不注册,而不是只禁掉 rm。权限层怎样解释这些规则,见权限模式与规则。
一个工具声明什么
契约的公共部分在 apps/zcode-cli/packages/contracts/src/tools/contract.ts:168:
export interface ToolContractDeclaration {
capability: string;
executionMode?: ToolExecutionMode;
providerNative?: ProviderNativeToolSpec;
inputSchema: Record<string, unknown>;
outputSchema: Record<string, unknown>;
/**
* 声明 `inputSchema` **有资格**走 provider 的严格模式(Anthropic `strict: true`:constrained
* decoding 保证 tool_use.input 恰好满足 schema)。只是资格,不是命令:adapter 按 provider /
* model 决定是否真的下发,并负责把 strict 子集表达不了的关键字折进 description。缺席即不严格。
* 首个使用者是 dwf mono 子代理的 typed `submit_result`。
*/
strict?: boolean;
requiresUserInteraction?: boolean;
permission: ToolPermissionSpec;
resultBudget: ToolResultBudget;
timeout: ToolTimeoutPolicy;
cancellation: ToolCancellationPolicy;
trace: ToolTracePolicy;
}core 的 ToolEntry 继承它(apps/zcode-cli/packages/core/src/tool/types.ts:269),再加上必填的 metadata(types.ts:65)、handler 和十来个可选钩子。按用途归一下类:
| 字段 | 声明什么 | 谁在读 |
|---|---|---|
metadata.name、description、modelInstructions | 模型可见名与描述;modelInstructions 拼成 Usage: 列表接在描述后 | 注册表投影(apps/zcode-cli/packages/core/src/tool/registry.ts:132) |
inputSchema、runtimeInputSchema | 发给模型的 JSON Schema;运行时用的 zod schema | 适配层;执行器的归一化与校验 |
outputSchema、runtimeOutputSchema | 输出形状 | 执行器在 handler 返回后校验 |
metadata.readOnly、destructive、concurrentSafe、sideEffectScope、riskLevel、needsApproval、requiresUserInteraction、allowedInPlanMode | 能力旗标 | 调度器、权限服务、流式执行判定 |
permission | 权限名、理由、风险、副作用范围,以及 alwaysAsk(任何模式都要问,contract.ts:105)、askOptions(能否“始终允许”,contract.ts:114) | 权限服务与审批闸 |
resultBudget | 给模型的字节阈值、截断方向、超限是否落盘(contract.ts:117) | 执行器的结果序列化 |
timeout、metadata.timeoutMs | kind: "none" 不计时,或默认值、上限、能否按调用覆盖、清理宽限(contract.ts:137) | 执行器超时 |
cancellation | 是否支持取消、清理级别、取消时给用户的话 | 只用到提示语和清理级别,supported 没有读取方 |
trace | 是否要求 trace、输入输出记录粒度 | 全仓库没有读取方 |
metadata.maxOutputBytes | 最大输出 | 只被投影进 ModelToolContract,不参与截断 |
metadata.providerVisible、aliases | 是否发给模型、兼容别名 | 注册表 |
metadata.stopTurnOnSuccess | 成功即结束回合的终态工具(types.ts:85) | 执行器的回合控制 |
strict、executionMode、providerNative | 严格模式资格;由客户端执行还是由 provider 原生执行 | 模型适配层 |
permissionCapabilityGroup、modelContentProtection | 只能由宿主验证过的来源写入的信任标记(官方 Computer Use) | 权限与结果投影 |
可选钩子让契约能“看入参说话”。全部 40 个内置条目里用到的情况如下:
| 钩子 | 作用 | 用到它的工具 |
|---|---|---|
resolveModelContract | 按当前模型能力改写描述与 schema | Read:模型支持 PDF 时才出现 pages 参数(apps/zcode-cli/packages/core/src/tool/handlers/read-pdf.ts:65) |
validateInput | 工具专属的语义校验,在钩子之前收口 | Read、TaskOutput,以及下面四个工作流创作工具 |
resolveInput | 把入参归一化成“将要发生的执行事实”(types.ts:318) | CreateWorkflow、AmendWorkflow、SaveWorkflow、EvalWorkflowSnippet |
prepareApproval | 权限已判定 ask 之后,只能放行或补一张预览,不能把 allow 变成 ask(types.ts:339) | 同上四个 |
resolveTimeoutBudgetMs | 按入参算超时 | Bash、Read |
resolvePermissionCapability、resolvePermissionRulePolicy | 按入参重算只读等能力、匹配规则 | Bash |
formatModelContent、formatPersistedModelContent | 输出怎样变成模型内容;落盘后怎样写摘要 | 前者 30 个条目用到,后者只有 Bash 和 TaskOutput |
看一个真实声明。Bash 的权限、结果预算和超时(apps/zcode-cli/packages/core/src/tool/handlers/bash.ts:470):
permission: {
permission: "bash",
reason: "Bash can run subprocesses and may affect workspace, git, network, or system state",
riskLevel: "high",
sideEffectScope: "system",
needsApproval: true,
patternSources: ["command"],
alwaysAllowPatternSources: ["command"],
denyPriority: "beforeAsk",
},
resultBudget: {
maxInlineBytes: MAX_INLINE_OUTPUT_BYTES,
maxModelBytes: 30_000,
strategy: "artifact",
preview: {
maxBytes: 30_000,
direction: "tail",
},
artifact: {
enabled: true,
retention: "session",
},
},
timeout: {
defaultMs: DEFAULT_BASH_TIMEOUT_POLICY.defaultTimeoutMs,
maxMs: DEFAULT_BASH_TIMEOUT_POLICY.maxTimeoutMs,
allowCallOverride: true,
cleanupGraceMs: 6_000,
},同一个条目的 metadata 里写着 maxOutputBytes: 10_000_000(bash.ts:455),真正卡住模型输入的却是上面的 30000 字节。另外,patternSources、alwaysAllowPatternSources、denyPriority 三个字段在全仓库都没有读取方,只在几处注释里被提到。
AGENTS.md 的要求与实际字段
apps/zcode-cli/AGENTS.md 专门有一节“工具与副作用契约”,第一条是(apps/zcode-cli/AGENTS.md:62):
每个 tool 都应声明明确的
inputSchema、outputSchema、是否只读、是否破坏性、是否并发安全、最大输出大小、超时、取消语义和权限需求。
逐条对照代码:
| AGENTS.md 的要求 | 对应字段 | 落实情况 |
|---|---|---|
inputSchema、outputSchema | 同名字段,另有 zod 版 runtimeInputSchema、runtimeOutputSchema | 类型上必填;执行器校验输入和输出 |
| 只读、破坏性、并发安全 | metadata.readOnly、destructive、concurrentSafe | 必填;调度与权限都读 |
| 最大输出大小 | metadata.maxOutputBytes(可选)与 resultBudget(必填) | 真正截断的是后者 |
| 超时、取消语义 | timeout、cancellation | 必填;cancellation.supported 无人读取 |
| 权限需求 | permission、metadata.needsApproval | 权限服务读取 |
副作用范围(apps/zcode-cli/AGENTS.md:63) | sideEffectScope,七种取值(contract.ts:7) | 调度、权限、钩子输入、动态工作流都读 |
幂等性与可恢复策略(apps/zcode-cli/AGENTS.md:64) | 没有对应字段 | 只有 MCP 的 idempotentHint 被折进 concurrentSafe(apps/zcode-cli/packages/core/src/mcp/index.ts:170) |
大结果落盘(apps/zcode-cli/AGENTS.md:65) | resultBudget.strategy: "artifact" | 执行器落盘,见下一篇 |
七种副作用范围是 none、workspace、git、network、system、session、userInteraction。contracts 在这之上给了两条判定:isWorkspaceMutatingToolCall 判“写”,只认 workspace、git、system,范围缺席按会写处理(contract.ts:30);isWorldTouchingToolCall 判“碰”,排除 session 和 userInteraction 两档协议范围,其余都算(contract.ts:57)。注释特意说明 Read 的范围是 none,但它的答案取决于工作区,所以读也算“碰”(contract.ts:54)。两条规则都是给动态工作流的导入缓存用的,见动态工作流(三):从工具调用到落库。
注册表:一张表,两类来源
ToolRegistryImpl 只有两张 Map:规范名到条目、别名到规范名(registry.ts:30)。注册时规范名永远压过别名,同名重复注册会覆盖并告警;别名撞上已有工具或别的别名时直接跳过,以免一次调用被路由到错误的权限和 handler(registry.ts:34)。get 先查别名再查规范名(registry.ts:88),list 按插入顺序返回。toContracts 把条目投影成 ModelToolContract:去掉 providerVisible: false 的条目,拼好描述,带上能力旗标、权限和结果预算,execute 一律置空(registry.ts:104、registry.ts:127),所以模型 SDK 永远不会自己执行工具。
内置工具。注册总表是 builtInTools(apps/zcode-cli/packages/core/src/tool/handlers/index.ts:76),40 个条目;registerBuiltInTools 逐个过门(tool/handlers/index.ts:195),过了门的再交给 resolveBuiltInToolEntryForBranch 按配置造变体(tool/handlers/index.ts:271):Bash 的描述和 timeout 参数说明随超时策略变化,Agent、Task 的描述带上子 Agent 档案列表,动态工作流关闭时还会去掉“工作流请求必须改用 CreateWorkflow”一句(tool/handlers/index.ts:284)。ApplyPatch 的契约还在 contracts 里,旧的 Workflow 工具也留着注册门,但两者在总表里都被注释掉了(tool/handlers/index.ts:70、tool/handlers/index.ts:80),这一版不会出现在模型面前。
注册入口有两个:运行时构造时的 registerRuntimeBuiltInTools(runtime-tools.ts:46),以及初始化 Shell 环境快照后再跑一次的 refreshBranchAwareBuiltInTools(apps/zcode-cli/packages/core/src/runtime/methods/embedded-search-branch.ts:21)。两处各写一份门的推导曾经漂过:刷新那份漏了动态工作流开关,而这个开关“缺席即开启”,结果灰度关闭的会话里模型仍然调到了 ListSavedWorkflows,于是推导被收进 tool-allowlist.ts 共用(apps/zcode-cli/packages/core/src/runtime/helpers/tool-allowlist.ts:70)。
别名。Task 是 Agent 的完整拷贝,标了 providerVisible: false,描述写明是 “Claude Code-compatible alias”(apps/zcode-cli/packages/core/src/tool/handlers/agent.ts:287);TaskStop 认 KillShell、KillBash(apps/zcode-cli/packages/core/src/tool/handlers/task-stop.ts:97),TaskOutput 认 BashOutput、AgentOutputTool 等四个名字(apps/zcode-cli/packages/contracts/src/tools/task-output.ts:5)。这些名字不发给模型,但模型或插件说明按旧名调用时照样能找到工具。钩子匹配也有一张别名表:匹配 Task 的钩子对 Agent 生效,反之亦然(apps/zcode-cli/packages/core/src/tool/compat.ts:6),钩子本身见生命周期 Hooks 与工作区信任。
MCP 工具。回合循环每一轮都会调用 initializeMcp(apps/zcode-cli/packages/core/src/runtime/methods/turn-loop.ts:106),但它每个运行时只注册一次:等待连接快照,调用 registerMcpTools 写进同一张注册表,有新工具就清掉工具缓存(apps/zcode-cli/packages/core/src/runtime/methods/mcp.ts:122)。README 说 MCP 工具“registered before the first model request”(apps/zcode-cli/README.md:180),与代码一致。名字是 mcp__<server>__<tool>,两段都把字母、数字、下划线、连字符以外的字符换成下划线(apps/zcode-cli/packages/core/src/mcp/name.ts:3)。能力旗标从 MCP 的 annotations 推出来(core/src/mcp/index.ts:108):
readOnlyHint、destructiveHint直接映射;concurrentSafe取“只读或幂等”;needsApproval一律为真,副作用范围一律记作network,只有宿主node_repl的js记作system、风险为高;- 超时取描述里的
timeoutMs,缺省 30000 毫秒(core/src/mcp/index.ts:37); - 结果预算:普通 MCP 给模型 50000 字节、超出截断;宿主
node_repl64 KiB、超出落盘并保留尾部;验明身份的官方 Computer Use 256 KiB(core/src/mcp/index.ts:125)。
官方 Computer Use 的服务在插件命名空间下,工具名本来是 mcp__plugin_zcode-cua_computer-use__*,只有通过不可伪造的宿主凭据校验后才改投成 mcp__computer-use__*,另挂一个 provider 拼写别名(core/src/mcp/index.ts:86)。MCP 的连接、传输与 OAuth 见 MCP。
可见性:从注册表到请求
模型看到的清单要过五层:
| 层 | 位置 | 做什么 |
|---|---|---|
| 注册门 | tool/handlers/index.ts:195 | 按端口是否注入、功能开关、白名单与黑名单、会话类型决定装不装 |
| 契约投影 | registry.ts:106 | 去掉 providerVisible: false 的条目(目前只有 Task) |
getTools | apps/zcode-cli/packages/core/src/runtime/methods/config.ts:136 | 搜索分支再滤一次;统一排序;WebSearch 只给支持原生搜索的模型;按模型能力投影契约;结果缓存 |
| 回合过滤 | turn-loop.ts:110 | 本回合的黑名单;定时任务执行轮去掉 Cron 写工具,闲时执行轮去掉 OffPeakCreate、SendMessage 与 Workflow(apps/zcode-cli/packages/core/src/runtime/methods/turn-loop-state.ts:34);Cron 创建上限命中后清单置空 |
| 适配层 | apps/zcode-cli/packages/adapters/src/model/tool-transform.ts:20 | provider 原生工具、严格模式 schema、引用改写 |
回合过滤的细节属于回合循环与 TurnMachine,这里只看前三层里容易误会的几点。
默认没有 Glob 和 Grep。嵌入式搜索分支的总开关写死为 true(apps/zcode-cli/packages/core/src/embedded-search/capability.ts:4),全仓库没有调用方覆盖它;只要 Bash 没被白名单或黑名单拿掉,registerBuiltInTools 就跳过 Glob 和 Grep(tool/handlers/index.ts:206),搜索改由 Bash 里的 find、grep 接管。实现见读、写、改、搜。
会话类型会收窄工具面。toolset 为 explore 时只注册探索工具集 Bash、Glob、Grep、Read、WebFetch、WebSearch、TodoWrite(apps/zcode-cli/packages/core/src/subagent/explore-tools.ts:4),另给了白名单就取交集,子 Agent 会话还会补回 RespondToCoordinator(tool-allowlist.ts:79);工作流子会话固定禁用 CreateWorkflow、AmendWorkflow、SaveWorkflow、ResumeWorkflowRun、ResolveWorkflowQuestion(tool-allowlist.ts:26)。前三个声明了 alwaysAsk,而工作流子会话被强制成 yolo、交互事件不回传父界面,确认请求会隐形挂起到超时;后两个是“子会话内不得再编排、不得替主代理作答”的结构性禁令(tool-allowlist.ts:18、tool-allowlist.ts:38)。
宿主形态靠端口表达。core 里没有“是不是桌面端”的判断,宿主注入了哪些端口,就有哪些工具。bootstrap 只是转发调用方给的定时任务端口和闲时端口(create-app.ts:775),TUI 与 -p 都不传,所以终端里没有 Cron 与 OffPeak 工具;协议会话总是注入定时任务端口,闲时端口看宿主开关(server-operations.ts:3370、server-operations.ts:3373)。动态工作流的十个工具极性相反:终端不写开关,按“缺席即开启”保留全部工具(tool/handlers/index.ts:178);协议会话必须写出显式布尔,宿主不开就是 false(server-operations.ts:3337,服务端默认值见 apps/zcode-cli/packages/bootstrap/src/zcode-protocol/server.ts:255)。
权限模式不参与筛选。Plan 模式下模型照样能看到 Write 和 Bash,模式只影响回合里插入的提醒(turn-loop.ts:126)和执行时的权限判定。拦截发生在执行器里,见权限模式与规则。
模型能力。WebSearch 只在当前模型的 supportsNativeWebSearch 为真时出现(config.ts:268),它本身是借模型的原生搜索实现的,见 WebFetch 与 WebSearch;Read 的 pages 参数只对支持 PDF 的模型出现。执行器调用前也用同一个 resolveModelContract 投影一次(apps/zcode-cli/packages/core/src/tool/model-contract.ts:4),保证校验用的 schema 与模型看到的是同一份。
发给模型的契约:schema、描述与顺序
Schema。所有内置工具的 schema 都走 toToolJsonSchema,注释说这是为了让运行时校验与 provider 参数不漂移(apps/zcode-cli/packages/contracts/src/tools/json-schema.ts:20)。它用 zod-to-json-schema 生成 JSON Schema 7,不用 $ref、按输入侧展开 effect,再做一轮归一化:删掉 $schema、$id、$ref、$defs、definitions(tools/json-schema.ts:9),没有 oneOf 时把 anyOf 改名为 oneOf(tools/json-schema.ts:66),缺 type 的节点按 properties、items、enum 等推断出类型,最后标上 draft 2020-12 的 $schema。改名有个副作用:core 自己的校验器对 oneOf 要求恰好命中一个分支(apps/zcode-cli/packages/core/src/tool/json-schema.ts:71),比 anyOf 严。
描述。metadata.description 之外,声明了 modelInstructions 的工具(CronCreate、CronUpdate、OffPeakCreate、ReadSessionContext、RespondToCoordinator)会在描述后追加一段 Usage: 列表(registry.ts:132)。WebSearch 的描述是个 getter,每次读取重新生成,因为描述里带着当前月份,写死会过期(apps/zcode-cli/packages/core/src/tool/handlers/websearch.ts:134)。
顺序。getTools 在生成缓存清单时统一排序(config.ts:264),规则在 apps/zcode-cli/packages/core/src/tool/provider-visible-order.ts:35:
export function orderProviderVisibleToolContracts<T extends { name: string }>(
tools: readonly T[],
): T[] {
const referenceTools: T[] = [];
const localTools: T[] = [];
for (const tool of tools) {
if (SORTED_PROVIDER_TOOL_NAMES.has(tool.name)) {
referenceTools.push(tool);
} else {
localTools.push(tool);
}
}
return [
...referenceTools.sort((left, right) => left.name.localeCompare(right.name)),
...localTools,
];
}名单 SORTED_PROVIDER_TOOL_NAMES 有 31 个名字(provider-visible-order.ts:1),其中 EnterWorktree、ExitWorktree、LSP、NotebookEdit、ScheduleWakeup、TaskCreate、TaskGet、TaskList、TaskUpdate、Workflow 这 10 个 ZCode 这一版并不注册。TaskCreate、TaskGet、TaskList、TaskUpdate 这一组是 Claude Code 的任务工具(见 Claude Code 中文手册),从这些名字看,名单像是参照 Claude Code 的工具集列出的。效果是:名单内的工具按字母序排在最前,其余内置工具按注册顺序跟在后面,MCP 工具最后注册,自然落在末尾。
为什么要稳定?工具清单是请求前缀的一部分,任何增删、改名、换序都会让 provider 的提示词缓存从头失效。代码里有几处印证:getTools 的结果缓存在运行时上(config.ts:137),只在 MCP 注册或搜索分支刷新时清空;submit_result 的注释把“per-ask schema 不进工具声明”归因于 “frozen-tool 缓存不变式”(apps/zcode-cli/packages/core/src/tool/handlers/submit-result.ts:9);工作流 driver 也提到 “prompt-cache 的 frozen-tools 不变式”(apps/zcode-cli/packages/bootstrap/src/app/workflow-driver.ts:547)。
到 provider 为止。toAiSdkTools 只把描述、inputSchema、严格模式标记和 needsApproval 交给 Vercel AI SDK(tool-transform.ts:35),只读、并发安全、副作用范围这些旗标都留在 ZCode 内部。严格模式要同时满足:契约声明 strict、provider 是 anthropic、模型是 Anthropic 首方模型(tool-transform.ts:85);声明了 requiresMfjsToolSchema 的模型只接受 #/$defs/ 形式的引用,适配层会把 MCP schema 里其他本地引用提升过去(tool-transform.ts:95)。适配层的其余部分见模型适配层。
参数不合格时,模型收到什么
执行器拿到模型的入参后,先归一化:顶层是 JSON 字符串就解析(有的适配器和钩子路径会把入参交成字符串),再用 zod 的 runtimeInputSchema 跑一遍 safeParse,成功就用补过默认值的结果,失败就保留原始入参和 zod 的 issue(apps/zcode-cli/packages/core/src/tool/input-normalization.ts:40)。随后用 JSON Schema 做准入校验(apps/zcode-cli/packages/core/src/tool/executor/validation.ts:71)。这个校验器是个最小子集:支持 oneOf、const、enum、type、字符串长度、数值范围、数组长度与 items、required、additionalProperties: false,不认 pattern、format(apps/zcode-cli/packages/core/src/tool/json-schema.ts:53)。正则、URL 这类约束靠 zod 的 issue 补进报错。注意准入的硬门是 JSON Schema:它通过而 zod 失败时,调用会继续往下走,交给工具的 validateInput 或 handler 兜底(validation.ts:77)。
校验失败时,给模型的内容由 apps/zcode-cli/packages/core/src/tool/input-validation-model-content.ts:12 组装:
export function createInitialInputValidationModelContent(
entry: ToolEntry,
jsonIssues: readonly ToolInputValidationIssue[],
runtimeIssues: readonly RuntimeInputValidationIssue[] | undefined,
): string {
const issues = projectInitialModelValidationIssues(entry.inputSchema, jsonIssues, runtimeIssues);
return `<tool_use_error>InputValidationError: ${formatToolInputValidationError(
entry.metadata.name,
issues,
)}</tool_use_error>`;
}
// ...
const lines: string[] = [
...missingParameters.map((parameter) => `The required parameter \`${parameter}\` is missing`),
...unexpectedParameters.map(
(parameter) => `An unexpected parameter \`${parameter}\` was provided`,
),
...wrongTypes.map(
({ expected, param, received }) =>
`The parameter \`${param}\` type is expected as \`${expected}\` but provided as \`${received}\``,
),
];缺参数、多参数、类型不对三类问题各给一句人话,有这三类问题时其他问题一概省略(input-validation-model-content.ts:107);只剩取值不在枚举里、长度超限、正则不匹配这类问题时,才把 issue 列表整个序列化成 JSON 交给模型(input-validation-model-content.ts:65)。比如调用 Read 时漏了 file_path,模型收到的是:
<tool_use_error>InputValidationError: Read failed due to the following issue:
The required parameter `file_path` is missing</tool_use_error>这些 issue 的形状值得一提:invalid_value、invalid_format、带 origin 的 too_big,以及 Invalid input: expected string, received undefined 这样的措辞(apps/zcode-cli/packages/core/src/tool/tool-input-validation-issues.ts:5、tool-input-validation-issues.ts:87),都是 Zod 4 的格式,而 contracts 依赖的是 Zod 3(apps/zcode-cli/packages/contracts/package.json:99)。投影层专门把 Zod 3 的码翻译成目标格式(input-validation-model-content.ts:331),并逐字段对齐顺序,注释反复强调这些字段的插入顺序“会直接进入 provider-visible JSON fallback,必须保持稳定”(tool-input-validation-issues.ts:1)。
另外几种失败的写法:
- 工具专属校验(
validateInput、resolveInput)或 handler 以返回值表达业务失败时,模型看到<tool_use_error>包着的那句 message,错误码另记在结果里(apps/zcode-cli/packages/core/src/tool/executor/errors.ts:20)。 - 工具名不存在时模型看到
Tool not found: <name>(apps/zcode-cli/packages/core/src/tool/executor/call-runner.ts:128);名字是空白时则是<tool_use_error>Error: No such tool available: </tool_use_error>,末尾原样保留模型返回的空白名(call-runner.ts:138)。 - PreToolUse 钩子改写过的入参会再校验一次,但失败时不再生成上面这段面向模型的说明(
validation.ts:79),因为那已经不是模型自己写出的参数。
路径策略
文件类工具都用 resolveWorkspacePath 解析路径(apps/zcode-cli/packages/core/src/tool/path-policy.ts:15):工作目录和工作区根必须是绝对路径,否则算配置错误;空路径直接报错;绝对路径只做 normalize,相对路径相对于当前工作目录解析。关键在它不做的事(path-policy.ts:32):
const resolvedPath = isAbsolute(requestedPath)
? normalize(requestedPath)
: resolve(workingDirectory, requestedPath);
// Current release intentionally does not hard-block paths outside workspaceRoot.
// Cause: subagents may need to inspect user-requested sibling repos or external files
// before the filesystem permission adapter grows explicit ask/deny rules for them.
return resolvedPath;也就是说,工具层不以工作区为边界,工作区外的读写只受权限模式约束(Bash 的工作目录另有重置规则,见 Bash:解析、只读判定与后台任务)。比较路径时用的是 normalizeToolPathForComparison(apps/zcode-cli/packages/core/src/tool/path-normalization.ts:15):Windows 上把 Git Bash 风格的 /c/... 转成 C:\...(path-normalization.ts:30),剥掉 \\?\ 长路径前缀但保留设备命名空间(path-normalization.ts:45),盘符统一大写;所有平台最后都做一次 Unicode NFC 归一。读文件状态表就用它生成键(apps/zcode-cli/packages/core/src/tool/read-file-state.ts:15),避免同一文件因写法不同被当成两个文件。
内置工具全表
下表覆盖 builtInTools 的全部 40 个条目。“只读”是 metadata.readOnly 的声明值;handler 列是 apps/zcode-cli/packages/core/src/tool/handlers/ 下的文件。
| 工具 | 用途 | 只读 | 出现条件 | handler | 详见 |
|---|---|---|---|---|---|
| Read | 读文本、图片、PDF、视频 | 是 | 默认 | read.ts 及 read-*.ts | 读、写、改、搜 |
| Write | 新建或覆盖文件 | 否 | 默认 | write.ts | 同上 |
| Edit | 精确替换文件中的文本 | 否 | 默认 | edit.ts | 同上 |
| Glob、Grep | 按模式找文件、按正则搜内容 | 是 | 默认不出现,见上文 | glob.ts、grep.ts | 同上 |
| Bash | 执行 Shell 命令 | 否,按命令重算 | 默认 | bash.ts 等 | Bash |
| WebFetch | 抓取公开 URL,转成 Markdown 后回答问题 | 是 | 默认 | webfetch.ts 等 | WebFetch 与 WebSearch |
| WebSearch | 借模型的原生搜索查网页 | 是 | 模型支持原生搜索 | websearch.ts | 同上 |
| TodoRead、TodoWrite | 读取、整表替换会话 Todo | 是、是 | 默认 | todo.ts | Todo、提问与 Plan 模式 |
| AskUserQuestion | 向用户发选择题并等答案 | 是 | 默认 | ask-user-question.ts | 同上 |
| EnterPlanMode、ExitPlanMode | 进入 Plan 模式;提交计划请用户批准 | 否 | 默认 | plan-mode.ts | 同上 |
| ReadSessionContext | 按会话 ID 读另一个会话的有界上下文 | 是 | 默认 | read-session-context.ts | 同上 |
| ListModels | 列出宿主配置的模型,给工作流挑子 Agent 模型 | 是 | 动态工作流未关闭 | list-models.ts | 同上 |
| CronCreate、CronUpdate、CronDelete | 增、改、删定时任务 | 否 | 宿主注入定时任务端口,且非子 Agent | cron.ts | 定时任务与闲时任务 |
| CronList | 列出定时任务 | 是 | 同上 | cron.ts | 同上 |
| OffPeakCreate | 创建闲时任务 | 否 | 宿主开放闲时工具,且非子 Agent | off-peak.ts | 同上 |
| OffPeakList | 列出闲时任务 | 是 | 同上 | off-peak.ts | 同上 |
| Agent | 启动子 Agent | 是 | 有子 Agent 端口 | agent.ts | 子 Agent |
| Task | Agent 的 Claude Code 兼容别名,不发给模型 | 是 | 同上 | agent.ts | 同上 |
| SendMessage | 给本地 Agent 发短消息 | 否 | 子 Agent 端口支持发消息 | send-message.ts | 同上 |
| RespondToCoordinator | 子 Agent 回复它的协调者 | 否 | 子 Agent 会话 | respond-to-coordinator.ts | 同上 |
| submit_result、escalate | 工作流 actor 提交终态结果;把阻塞问题升级给主代理 | 否 | 工作流 actor 会话 | submit-result.ts、escalate.ts | 同上,及动态工作流(三) |
| TaskOutput | 读后台任务输出,描述已标为 DEPRECATED | 是 | 默认 | task-output.ts | Bash、后台任务与通知 |
| TaskStop | 按 ID 停止后台任务 | 否 | 默认 | task-stop.ts | 同上 |
| Skill | 把技能说明载入当前上下文 | 是 | 有技能端口 | skill.ts | 技能与自定义命令 |
| js | 在常驻 Node REPL 里执行 JavaScript | 否 | browser-use 插件打开 nodeRepl | node-repl.ts | node_repl、Browser Use 与 Computer Use |
| CreateWorkflow、AmendWorkflow | 类型检查工作流脚本,确认后在后台启动或修订 run | 否 | 动态工作流未关闭 | create-workflow.ts、amend-workflow.ts | 动态工作流(三) |
| SaveWorkflow | 把脚本存成项目内可复用的定义 | 否 | 同上 | save-workflow.ts | 同上 |
| EvalWorkflowSnippet | 同步试跑一小段工作流,不留状态 | 否 | 同上 | eval-workflow-snippet.ts | 同上 |
| ListWorkflowRuns、GetWorkflowRun | 列出本项目的 run;读一个 run 的进度与结果 | 是 | 同上 | list-workflow-runs.ts、get-workflow-run.ts | 同上 |
| ResumeWorkflowRun | 以原 run ID 恢复已停止的 run | 否 | 同上 | resume-workflow-run.ts | 同上 |
| ResolveWorkflowQuestion | 回答 actor 升级上来的阻塞问题 | 否 | 同上 | resolve-workflow-question.ts | 同上 |
| ListSavedWorkflows | 列出已保存的工作流定义 | 是 | 同上 | list-saved-workflows.ts | 同上 |
几处“只读”要结合副作用范围看:TodoWrite、Agent、Task 都声明了 readOnly: true,范围却是 session(apps/zcode-cli/packages/core/src/tool/handlers/todo.ts:140、agent.ts:229),调度器只把“只读且范围为 none”的调用当作真正的只读(下一篇细说)。Bash 声明为非只读,但会按命令内容重算:判定为只读的命令(如 ls、git status)改记为只读、低风险、免审批、范围 none(bash.ts:77)。
下一篇:执行器:调度、审批、超时与结果——一批工具调用怎样分组并发,单个调用怎样走完校验、钩子、审批、执行与结果投影。