工具执行的三道关口
模型说,我要执行这条命令。这句话之后发生的事,决定了这套系统安不安全、可不可控。dsh 把模型发出一次工具调用到结果回到模型之间的路程,做成了一条有三个站点的管道,全部以 waterfall 事件实现,任何插件都能在任何站点介入。第 3 章讲过 waterfall 的机制,本章看它在一个具体管道上的完整应用。
管道的起点是 tool/call 事件入日志,这是第 4 章那条纪律的要求,模型可见的内容必须先成为事实。随后的三站分别是 tools/pre-execute、tools/execute、tools/post-execute,最后以 tool/result 事件收尾,回到模型。
第一站管准入。权限插件检查这次调用的目标是否越界,审批策略决定要不要人先点头,敏感操作可以在这里被直接拒绝。因为这是 waterfall,拒绝的方式就是不去调用 next(),链条终止,模型收到一个说明原因的失败结果。第 3 章强调过那个约定,监听者必须调用 next() 完成委托,反过来,不调用就是拦截,这两个权力是一体两面。
第二站是真实执行。命令真正跑起来,文件真正被读写。沙箱提供者在 ctx.sandbox 上注册,消费者在起进程前把参数包进沙箱约束里。第 6 章讲过,fs 与 subprocess 共享执行世界,沙箱的约束因此对 Bash、终端、语言服务器一体生效。执行阶段的另一个主题是超时,守卫插件确保一次调用不会无限挂着,时间到了体面地终止并把原因记下来。
第三站管出口。真实执行的结果在这里做最后一道加工。输出可能超长,截断策略在这里生效。用量可能超限,字节与条目的上限在这里核对。需要脱敏的内容在这里处理。这一站的存在说明一个设计立场,限额要施加在完整的产出上,包装和元数据一并计算,测过恰好卡线和超大单块的边界情形。
三站之后,tool/result 事件入日志,工具结果进入模型历史,第 5 章的周期图里那个循环继续转动。若模型判断还需要更多步骤,下一个 Step 的请求里就带着这份结果。
这条管道值得和学习者自己的系统对照。多数自建 Agent 的工具执行是一个函数调用,权限检查、沙箱、限额硬编码在函数里,或者干脆散落在各处。想加一种新的检查,改函数。想临时关掉一种检查,也改函数。dsh 的做法把执行路径本身留薄,三站全是事件,策略全是插件。想加一道内容扫描,写一个监听 tools/pre-execute 的插件。想换一种沙箱,注册新的 ctx.sandbox 提供者。执行的核心代码不为任何具体策略留开关。
这样设计还有一个不太显眼的好处,关口是可观测的。三站都是事件,遥测插件在每一段都能拿到上下文,一次调用在哪里被拦、耗时多少、产出多大,全部有统一的位置可以观察。排查一次失败的调用,读日志里的事件序列就够了,不需要猜。
仓库里有一组插件专门守这条管道,叫 guard,循环卫生与工具超时。工具超时插件给每次调用套上时间上限,超时即终止并给出明确的原因。循环卫生插件盯着更隐蔽的一类问题,模型陷入重复调用同一个工具、或者空转不产出,这类行为消耗预算却毫无进展,守卫识别出来并干预。它们和权限、沙箱一样,都是挂在管道上的普通插件,谁都可以用同样的方式再添一道自己的守卫。
工具的呈现方式也在这条管道的管辖之内。dsh 有一个明确的立场,一个工具最终长什么样,模型看到的使用说明是一半,人类看到的呈现意图是另一半。呈现意图在设计工具时就定下来,是普通文本、终端输出还是差异对比,渲染逻辑是参数的纯函数。把这个决定前置到设计阶段,避免了一类常见的烂尾,工具能用了,但输出在界面上没人看得懂。
三道关口讲完,执行周期的主线全部走通。但还剩一个维度的自由度没有讲,不同的 Agent 如何拥有不同的性格。下一章讲作用域与同名遮蔽。