从 DeepSeek Harness 看:Tool 注册成功,为什么还不等于安全可用

一个工具出现在模型的 Tool 列表里,只能证明一件事:模型可能知道它叫什么、接收什么参数。
它不能证明 Provider 已经装载,不能证明参数经过审批,不能证明 Sandbox 覆盖网络和进程,更不能证明工具执行到一半失败时,外部文件、数据库或云资源已经回滚。
这正是 Agent 工具系统最危险的错觉:把"模型能发出 Tool Call"误当成"系统安全完成了副作用"。
DeepSeek Harness 的固定源码把这条链拆得很细:模型 Schema、参数解析、pre-execute 决策、审批、monotonic guard、execute wrapper、工具 body、post policy、结果规范化、finalize 与 Session tool/result。这些位置为治理提供了抓手,但不会自动替团队完成安全设计。
本文只回答一个问题:从 Tool 注册到真实副作用,中间究竟要经过哪些门,哪些边界仍在 Harness 之外?
核心结论是:安全可用不是一个 ToolDefinition 属性,而是一条从模型协议到执行世界的证据链。任何一段只"存在"而没有行为验证,都不能替代整条链。
第一层错觉:Schema 对上了,工具就可用
固定 Tools 文档把一个注册工具拆成两部分:模型可见 ToolSchema,以及宿主内部的输出声明、execute、超时、并发分类、finalize 和 UI Presenter。
模型请求只收到白名单字段,例如名称、描述和参数 Schema;execute、输出渲染、超时、并发安全分类与 UI 回调不会泄露到 Wire。
因此,Schema 只是一份模型协议。它没有回答:
- 参数是否真的能映射到 Provider;
- Provider 缺失时返回什么稳定错误;
- Tool body 是否遵守取消;
- 输出是否满足声明的 JSON Schema;
- 真实副作用属于谁,失败后怎样补偿;
- 结果中是否泄露路径、凭据或内部元数据。
固定工具目录就提供了多个反例。LSP Schema 可以始终存在,但没有 Provider 时返回 LSP_UNAVAILABLE;read_image 的注册依赖附件服务,执行时还要求当前精确模型路由支持图像输入;Web Tool Schema 稳定,具体搜索和抓取后端则在 ctx.web 后面替换。
"模型看得见"和"运行时做得到"必须分别验证。
第二层错觉:pre-execute 可以随意修正参数
旧式中间件经常在执行前偷偷改写请求,例如把相对路径变成绝对路径,或删除危险选项。固定 DeepSeek Harness 源码刻意不允许 tools/pre-execute 改写参数。
原因是参数已经用于日志、UI 与审计。若策略看到的是 A、工具实际执行的是 B,Tool Call 与真实副作用就失去一致性。
固定 PreToolDecision 只有三种:
allow;deny(reason);ask(reason?)。
需要参数规范化的工具,应在明确的 Schema 和工具实现合同里完成,而不是让任意策略插件在审计之后悄悄换值。
这条限制很重要:安全系统首先要保证"记录的调用"和"实际执行的调用"是同一个身份,再讨论是否放行。
第三层错觉:审批通过后,后续策略就不能拒绝

固定管线先运行 tools/pre-execute。当决策为 ask 时,ctx.approval 只对本次调用返回 allowed-once 才继续;拒绝、取消、缺少服务、没有可回答通道或无 Agent 身份都会变成 deny。
但 allowed-once 不是最终无条件授权。审批之后,monotonic guards 仍会运行。
Guard 的返回类型没有 allow:
text
undefined -> 不增加限制
reason -> 拒绝
正因为没有 allow,后加载的 Guard 无法把前面的拒绝翻转成允许。它只会维持当前状态或进一步收紧。
这解决的是策略组合顺序问题,而不是所有授权问题。团队仍要决定哪些规则必须进入 Guard,哪些仅是可重排的 pre policy,审批者看到了什么参数,批准范围是单次调用、某个路径还是某段会话。
如果关键安全策略被错误地写成普通 waterfall listener,单调性保证并不会自动覆盖它。
一次工具调用的完整执行链

固定源码中的主路径可以整理为:
text
model tool call
-> tool/call 记录
-> 参数 JSON 物化、冻结与身份分配
-> tools/pre-execute: allow / deny / ask
-> approval(如需要)
-> monotonic guards
-> tools/execute wrappers
-> ToolDefinition.execute body
-> tools/post-execute
-> 结果规范化
-> ToolDefinition.finalizeContent
-> tools/result 不可变观察
-> Session tool/result
tools/execute 适合围绕 body 增加超时、重试和指标;tools/post-execute 可以接受、替换结果投影或阻断;finalizeContent 是工具拥有的最后内容约束;tools/result 看到的是已经冻结的权威 outcome,观察者不能再变换它。
未知工具、参数错误、body 抛错、输出 Schema 不匹配、Renderer 失败或策略拒绝都会被规范化成结构化错误。固定文档明确:未知和抛错工具会失败,但不必因此结束整个 Turn,模型仍可能看到错误并采取下一步。
"错误能进入模型"不等于"外部副作用已经回滚"。

canonical value、模型内容与持久结果也不是一回事

工具 body 返回 canonical JSON value。注册表验证它、冻结它,再由输出 Renderer 生成模型可见 ContentBlock;可选 Presenter 生成 UI 元数据。
固定源码只把 content、结构化错误与 meta 写入持久 tool/result,canonical value 留在本次执行内。回放可以重现展示,却不保证重建当时的中间 value。
这意味着,真正需要恢复或审计的业务事实不能只藏在内存 canonical value 里。工具应把必要的稳定结果放进可持久投影,或者由自己的领域事件记录。
同时,Post Policy 只替换 content 并不是保密机制:执行内 canonical value 仍然存在。必须隐藏程序值时,应 block 或替换 value,而不是只把显示文字改成"已处理"。
Capability Seam:工具与 Provider 必须分别验收

很多 DeepSeek Harness 工具是 Consumer:文件工具消费 ctx.fs,Web 工具消费 ctx.web,LSP 工具消费 ctx.lsp,Bash 工具消费 Shell/Subprocess/Sandbox 等能力。
这种分层的好处是模型 Schema 可以稳定,Provider 可以按部署替换。代价是验收不能停在 Tool package:
- Service Definition 是否覆盖所需能力;
- Provider 是否真实存在并正确报告不可用;
- Consumer 是否处理 Provider 的错误、取消与边界;
- 多个 Provider 是否保持路径、流、权限和结果语义;
- 最终组合是否真的装载了目标 Provider。
一个"工具注册成功"的测试只覆盖 Consumer 与 Registry 的局部关系。它没有覆盖执行世界。
Sandbox 的准确边界:文件效果,不是所有安全问题

固定 Process Sandbox 文档把模式写得非常明确:
read-only;workspace-write;danger-full-access。
这套词汇治理的是文件效果。网络和进程可见性明确在其范围之外。read-only 不代表命令不能联网,workspace-write 也不代表子进程、系统调用和凭据已经隔离。
本地 Provider 可以使用 Linux bwrap/Landlock、macOS Seatbelt 或 Windows ACL Restricted Token,但执行完整性会报告为 full 或 partial。旧 Landlock ABI 与 Windows 某些 ACL/硬链接边界属于 partial,要求绝对边界的消费者不能把 partial 当 full。
若要求 confined 模式而没有可用 Provider,ctx.sandbox 必须 fail closed,不能静默裸跑。相反,danger-full-access 本来就绕过 confinement,不会调用 Sandbox Provider。
容器、microVM 与远程执行在官方架构里是完整执行能力 seam 的其他实现,不是把 ctx.sandbox 换个 Provider 就自动获得的同一种保障。
因此,生产环境至少还要单独治理:网络出口、进程/系统调用、密钥最小权限、外部 API 授权、宿主文件、审计与资源配额。
文件观察策略降低覆盖风险,但不替代事务
文件工具可以在执行路径中产生 fs/observed、fs/write-intent 和 fs/edit-intent 等事件。配套观察策略要求修改前先读到目标状态,避免模型基于过期内容直接覆盖文件。
这是一层 Harness 并发门槛,不是底层事务。读取之后文件仍可能被其他进程修改;符号链接、原子重命名、版本控制冲突、权限变化和远端文件系统一致性仍需 Provider 与存储层处理。
"先观察再编辑"证明的是意图链更清楚,不证明文件操作天然原子或无竞态。
Code Mode 不应成为绕过工具管线的后门

工具数量增多时,把全部 Schema 直接放入模型请求会增加 token 与选择噪声。Code Mode 可以只向模型提供保留传输工具 run_code 和 SDK 能力描述,由程序通过 binding 发起子调用。
固定工具目录和 Tools 文档明确,这些 sub-call 会携带父执行身份,重新进入完整的受保护管线,并产生 tool/code-dispatch 记录。并发安全的 body 可以在上限内重叠,但策略和提交顺序仍由 Runtime 管理。
因此,Code Mode 缩小的是模型协议表面,不是安全门禁。若某个实现让 run_code 直接调用本地函数、跳过审批与 Guard,它已经破坏了固定契约。
从注册到副作用的安全验收表

| 层级 | 必须验证 | 不能用什么替代 |
|---|---|---|
| Model Schema | 名称、描述、参数与部署能力一致 | 工具出现在目录里 |
| Registration | 重复注册、卸载、Scope 与 Provider 缺失 | TypeScript 编译通过 |
| Arguments | JSON 物化、冻结、Schema 校验与审计一致 | pre hook 偷偷改参数 |
| Approval | 谁批准、看到什么、范围与时效 | UI 出现过确认框 |
| Guard | 安全拒绝单调、顺序不可翻转 | 普通 middleware 返回 allow |
| Provider | 错误、取消、路径、流式与平台差异 | 稳定 Tool Schema |
| Sandbox | 文件模式、full/partial、fail closed | read-only 等于无网络/无进程 |
| Side effect | 幂等键、超时、部分成功、补偿与重试 | 结构化 isError |
| Result | value/content/meta、泄密、截断与持久事实 | UI 卡片显示成功 |
| Replay | tool/call/result 配对及必要领域事实 | 单次本地 smoke |
| Code Mode | 子调用回到完整管线并关联父执行 | run_code 本身被审批一次 |
这张表是本文的主要产物。它要求每一层提供自己的证据,而不是用最上层的 Schema 或最下层的一次成功执行替代整条链。
结论
DeepSeek Harness 的工具系统把模型协议、策略决策、单调拒绝、执行包装、Provider、结果规范化和持久事实分开。它为安全组合提供了明确位置,也暴露了"工具注册成功"到底有多局部。
真正的安全可用必须一直追到副作用:参数是否与审计一致,审批和 Guard 是否覆盖正确范围,Provider 是否可用,Sandbox 究竟约束什么,部分失败能否补偿,结果是否成为稳定事实。
尤其要记住两个边界:固定 SandboxMode 只描述文件效果;结构化错误只描述 outcome,不自动回滚已经发生的外部动作。
所以,最合理的验收问题不是"模型能不能调用这个工具",而是"这条调用在每个阶段由谁拥有、能被谁拒绝、留下什么事实、失败后还有哪些副作用"。
参考资料
- 固定 Commit Tools Subsystem:github.com/deepseek-ai...
- 固定 Commit Tool Execution Pipeline:github.com/deepseek-ai...
- 固定 Commit Tool Catalog:github.com/deepseek-ai...
- 固定 Commit Process Sandbox:github.com/deepseek-ai...
- 固定 Commit Architecture:github.com/deepseek-ai...
证据与推导边界
- ToolDefinition、Schema、管线、Guard、结果字段:固定 Commit 官方文档事实。
- Sandbox 文件效果、full/partial 与 fail closed:固定 Commit 官方文档事实。
- 安全验收表与外部副作用治理:作者工程建议。
- 研究包示例代码:NOT-RUN 模板,未作为独立扩展编译或联网运行。
- 真实模型 Tool Call、跨平台 Sandbox、外部系统幂等与生产恢复:未验证,不作结论。