第 7 章:工具与安全 —— Agent 的手脚、权限和输出边界¶
1. 一个工具至少跨过四个边界¶
OpenCode 的 tool 不是简单的 (args) => string,它要跨过:
模型 ToolDefinition
→ Tool Registry materialization
→ 参数 Schema decode
→ Permission / Question gate
→ side effect(filesystem / shell / network / child agent)
→ bounded output + metadata
→ Session tool part / next provider turn
每个阶段都可能失败,而且失败必须能被模型、UI 和用户理解。
2. Tool Registry:按当前上下文物化工具¶
packages/opencode/src/tool/registry.ts 和 packages/core/src/tool/registry.ts 负责把 built-in、plugin、MCP、code mode 等工具合并成当前 agent 可见的工具集。
Registry 不只是一个 map,它还处理:
- tool 初始化和惰性加载;
- permission 过滤;
- model-specific tool 选择,例如 apply_patch 与 edit/write 的差异;
- agent step limit 下的最后一步禁用工具;
- plugin 对 tool.definition 的修改;
- unknown / plugin tool 的稳定 ID;
- code mode 对 MCP tool catalog 的描述。
因此 tools 是 provider turn 的投影,每一轮都可能因为 agent、model、permission 或 feature flag 不同而变化。
3. 参数验证:错误要回到模型¶
packages/opencode/src/tool/tool.ts 中的 InvalidArgumentsError 把 Schema decode 失败变成模型可理解的错误:哪个 tool、哪一项参数、怎样重写。
设计原则是:
- 调用前 decode,不要让业务函数接受任意 unknown;
- 验证错误是工具结果语义的一部分,不是整个 session 的未处理异常;
- 结果统一截断,即使某个自定义工具忘记做 output limit,wrapper 仍能治理。
4. Permission 与 HTTP Authorization 不是一回事¶
| 机制 | 保护对象 | 典型问题 |
|---|---|---|
| HTTP Authorization | 谁能访问 Server API | 这个客户端能否调用 endpoint? |
| Tool Permission | 当前 agent 是否能执行某类动作 | 是否允许 shell / write / task? |
| Question | 需要用户一次性回答的互动 gate | 用户选择允许、拒绝或修改参数? |
Permission 规则会按 tool name / pattern 合并 agent ruleset、session ruleset 和用户选择。Permission.evaluate 的结果通常是 allow / ask / deny;ask 会通过 Event V2 或 Question API 等待用户响应。
5. Side effect 前后的状态机¶
stateDiagram-v2
[*] --> Pending: persist tool call
Pending --> Asking: permission = ask
Asking --> Running: user approves
Asking --> Error: user rejects
Pending --> Running: permission = allow
Running --> Completed: side effect succeeded
Running --> Error: exception / abort / invalid result
Completed --> OutputProjection: truncate + metadata
Error --> OutputProjection: model-facing error
OutputProjection --> [*]: persist tool part
这里的“persist pending before side effect”让崩溃、取消和 UI 显示都有明确落点。
6. Built-in 工具的领域分组¶
源码中可以按能力分成:
- 文件:
read、write、edit、apply_patch; - 搜索:
glob、grep、websearch、webfetch; - 进程:
shell/ PTY; - 编排:
task、todo、question、plan; - 知识与能力:
skill、lsp、MCP tools; - 高级执行:
code-mode,在受约束的 runtime 中调用 schema-described tools。
工具名是跨模型、Session、事件和 UI 的稳定 vocabulary。UI 不应该从 backend tool class 的名字推断显示逻辑,而应依赖 SDK wire name 和 metadata。
7. 大输出治理:完整性与窗口预算同时满足¶
工具输出处理有三个目标:
- 让模型收到可用的摘要 / 截断内容;
- 让用户能找到完整输出;
- 不让一次
git diff或日志命令耗尽上下文。
Core 的 ToolOutputStore 和 OpenCode 的 truncate 组合出 bounded model projection;被截断时写出 outputPath 等 metadata。后续工具或 UI 可以继续读取文件,而不是强迫模型重复运行副作用命令。
8. Subagent / Task:工具系统里的递归边界¶
task 不是普通 shell wrapper。它会:
- 选择一个非 primary agent;
- 创建或关联子 Session;
- 为子任务提供更窄的权限和 prompt;
- 把子任务的状态和结果投影到父 Session;
- 避免子 agent 误用父级所有能力。
这使 OpenCode 的“多 agent”仍然落在 Session / Permission / Tool 这些已有抽象上,而不是额外造一套隐式线程模型。
本章小结¶
工具系统的核心不是工具数量,而是把 schema、权限、side effect、输出预算和 durable projection 串成一个可恢复管道。对模型来说,工具是 ToolDefinition;对运行时来说,工具是带授权、观测、截断和事件的服务调用。