# 工具契约、注册表与可见性

> 一个 ZCode 工具要声明哪些字段、其中哪些真正被读取；内置工具与 MCP 工具怎样进同一张注册表；每回合发给模型的工具清单怎样按端口、开关与模型能力层层筛选、排序并投影成 JSON Schema；参数不合格时模型收到什么。附内置工具全表。

- 作者：David（道雾轩）
- 专栏：ZCode 源码解读（https://daiw.org/manual/zcode.md）
- 最后更新：2026-09-21
- 原文：https://daiw.org/manual/zcode/tool-contract
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

模型每一步能调用哪些工具、每个工具有什么副作用、出了错怎样告诉模型，都写在一份声明式的“工具契约”里。契约的类型和各工具的 zod schema 放在 `@zcode/contracts`，具体取值（权限、结果预算、超时、取消）大多写在 core 各 handler 的 `ToolEntry` 里，`ToolEntry` 同时挂上 handler 和一组可选钩子。40 个内置条目过了注册门之后，和运行时接入的 MCP 工具一起进同一张 `ToolRegistry`，再经过几道筛选变成发给模型的 `ModelToolContract[]`。

本篇讲“声明、注册、可见”：契约有哪些字段、哪些真的有人读；注册表怎样组织；模型看到的清单怎样定下来；参数不合格时模型收到什么。执行器怎样调度、审批和收尾，留给下一篇[执行器：调度、审批、超时与结果](https://daiw.org/manual/zcode/tool-executor)。

| 位置 | 内容 |
| --- | --- |
| `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`。权限层怎样解释这些规则，见[权限模式与规则](https://daiw.org/manual/zcode/permission)。

## 一个工具声明什么

契约的公共部分在 `apps/zcode-cli/packages/contracts/src/tools/contract.ts:168`：

```ts
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`）：

```ts
  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`）。两条规则都是给动态工作流的导入缓存用的，见[动态工作流（三）：从工具调用到落库](https://daiw.org/manual/zcode/dwf-tools)。

## 注册表：一张表，两类来源

`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 永远不会自己执行工具。

```mermaid
flowchart TD
  C["contracts：zod schema 与声明"] --> B["builtInTools：40 个 ToolEntry"]
  B --> R1["registerBuiltInTools：端口、开关、白名单与黑名单"]
  M["MCP 服务的工具描述"] --> R2["registerMcpTools：投影成 ToolEntry"]
  R1 --> REG["ToolRegistry"]
  R2 --> REG
  REG --> T["toContracts：去掉不可见条目"]
  T --> G["getTools：搜索分支过滤、排序、按模型投影、缓存"]
  G --> L["回合过滤：本回合黑名单与自动化限制"]
  L --> A["toAiSdkTools：发给 provider"]
  REG --> E["执行器：按名查条目"]
```

**内置工具**。注册总表是 `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 与工作区信任](https://daiw.org/manual/zcode/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_repl` 64 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](https://daiw.org/manual/zcode/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](https://daiw.org/manual/zcode/turn-loop)，这里只看前三层里容易误会的几点。

**默认没有 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` 接管。实现见[读、写、改、搜](https://daiw.org/manual/zcode/file-tools)。

**会话类型会收窄工具面**。`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`）和执行时的权限判定。拦截发生在执行器里，见[权限模式与规则](https://daiw.org/manual/zcode/permission)。

**模型能力**。WebSearch 只在当前模型的 `supportsNativeWebSearch` 为真时出现（`config.ts:268`），它本身是借模型的原生搜索实现的，见 [WebFetch 与 WebSearch](https://daiw.org/manual/zcode/web-tools)；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`：

```ts
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 中文手册](https://daiw.org/manual/claude-code/agents)），从这些名字看，名单像是参照 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`）。适配层的其余部分见[模型适配层](https://daiw.org/manual/zcode/model-adapters)。

## 参数不合格时，模型收到什么

执行器拿到模型的入参后，先归一化：顶层是 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` 组装：

```ts
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`，模型收到的是：

```text
<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`）：

```ts
  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：解析、只读判定与后台任务](https://daiw.org/manual/zcode/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` | [读、写、改、搜](https://daiw.org/manual/zcode/file-tools) |
| Write | 新建或覆盖文件 | 否 | 默认 | `write.ts` | 同上 |
| Edit | 精确替换文件中的文本 | 否 | 默认 | `edit.ts` | 同上 |
| Glob、Grep | 按模式找文件、按正则搜内容 | 是 | 默认不出现，见上文 | `glob.ts`、`grep.ts` | 同上 |
| Bash | 执行 Shell 命令 | 否，按命令重算 | 默认 | `bash.ts` 等 | [Bash](https://daiw.org/manual/zcode/bash) |
| WebFetch | 抓取公开 URL，转成 Markdown 后回答问题 | 是 | 默认 | `webfetch.ts` 等 | [WebFetch 与 WebSearch](https://daiw.org/manual/zcode/web-tools) |
| WebSearch | 借模型的原生搜索查网页 | 是 | 模型支持原生搜索 | `websearch.ts` | 同上 |
| TodoRead、TodoWrite | 读取、整表替换会话 Todo | 是、是 | 默认 | `todo.ts` | [Todo、提问与 Plan 模式](https://daiw.org/manual/zcode/interaction-tools) |
| 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` | [定时任务与闲时任务](https://daiw.org/manual/zcode/cron-offpeak) |
| CronList | 列出定时任务 | 是 | 同上 | `cron.ts` | 同上 |
| OffPeakCreate | 创建闲时任务 | 否 | 宿主开放闲时工具，且非子 Agent | `off-peak.ts` | 同上 |
| OffPeakList | 列出闲时任务 | 是 | 同上 | `off-peak.ts` | 同上 |
| Agent | 启动子 Agent | 是 | 有子 Agent 端口 | `agent.ts` | [子 Agent](https://daiw.org/manual/zcode/subagents) |
| 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` | 同上，及[动态工作流（三）](https://daiw.org/manual/zcode/dwf-tools) |
| TaskOutput | 读后台任务输出，描述已标为 DEPRECATED | 是 | 默认 | `task-output.ts` | [Bash](https://daiw.org/manual/zcode/bash)、[后台任务与通知](https://daiw.org/manual/zcode/background-tasks) |
| TaskStop | 按 ID 停止后台任务 | 否 | 默认 | `task-stop.ts` | 同上 |
| Skill | 把技能说明载入当前上下文 | 是 | 有技能端口 | `skill.ts` | [技能与自定义命令](https://daiw.org/manual/zcode/skills-commands) |
| js | 在常驻 Node REPL 里执行 JavaScript | 否 | browser-use 插件打开 nodeRepl | `node-repl.ts` | [node_repl、Browser Use 与 Computer Use](https://daiw.org/manual/zcode/node-repl-browser) |
| CreateWorkflow、AmendWorkflow | 类型检查工作流脚本，确认后在后台启动或修订 run | 否 | 动态工作流未关闭 | `create-workflow.ts`、`amend-workflow.ts` | [动态工作流（三）](https://daiw.org/manual/zcode/dwf-tools) |
| 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`）。

下一篇：[执行器：调度、审批、超时与结果](https://daiw.org/manual/zcode/tool-executor)——一批工具调用怎样分组并发，单个调用怎样走完校验、钩子、审批、执行与结果投影。
