5.0 KiB
Agent Note: 工具执行前输入重写——一致性设计
Status: proposed
English | 中文
问题
拦截扩展点 Agent Note 将 tools/pre-execute 定义为一道针对执行的允许/拒绝/询问门禁,此时执行的身份标识已受保护、参数已被深度冻结。Claude Code 的 PreToolUse 钩子还提供了 updatedInput,因此忠实的桥接需要一个显式的重写机制。重写不能是对现有执行对象的可变逃逸口:它必须保持持久化历史、审计记录、展示层与实际执行值之间的一致性。
问题本质:执行前参数的三个读取方
在 agent loop(智能体循环)中,工具调用的参数在工具执行之前就已提交到日志并被实时消费方读取:
assistant/message在工具分发之前追加——它是deriveMessages()回放时的模型历史来源,因此携带模型自身输出的工具调用参数。tool/call是持久化的审计记录,在ctx.tools.execute()之前追加。- 面向人类的展示读取
tool/call.arguments:UI 渲染器将这些参数传给presentResult;dsh-tool-bash从中派生卡片标题、rawInput、cwd 以及终端/后台处理方式。
如果只做执行层面的重写,UI 会显示一条命令而实际运行的是另一条,并且结果会对着错误的参数渲染。注册表目前通过以下方式防止这种失败模式:对 arguments 做 structured-clone 并深度冻结,将执行身份属性设为不可写,且不暴露任何可替换它们的测试 shim 或监听路径。重写设计必须维护这一受保护的身份边界,而非削弱它。
提案
重写是一个「身份标识创建前的一致性事务」。当钩子提供 updatedInput 时,有效值必须在注册表构造其不可变的 ToolExecution 之前确定,并且必须原子地反映到全部三个读取方:
tool/call审计事件记录重写后的参数(原始参数保留在一个伴随字段中,作为审计线索——钩子修改了调用,原始参数与生效参数都是值得保留的事实)。- 派生历史中的
assistant/message必须与实际执行一致。待评估的选项:就地重写 assistant 消息中的工具调用块(改变模型「看到自己说了什么」),或记录一条单独的修正让下一次请求携带。Claude Code 的模型是让模型看到重写已生效。 - 展示层(
presentCall/presentResult)读取重写后的参数,使 UI 显示实际运行的内容。
在 PreToolDecision 当前的触发点上做扩展是不够的:此时两条持久化记录已经存在,执行身份已受保护。实现必须将相关决策移到日志提交之前,或者增加一个专门的、更早的重写决策点来处理待定的模型调用。agent loop 将生效参数提交到历史和审计之后,再构造普通的不可变执行对象,并照常运行现有的允许/拒绝/询问和工具流水线。
曾考虑的替代方案
为什么不直接修改执行对象?
允许 pre-execute 监听器赋值 exec.arguments 只能提供执行层面的重写,模型历史、审计和展示层不会随之改变。保持身份标识受保护使得这种局部行为不可表达。在一致性事务实现之前,CC/Codex 桥接对 updatedInput 记录日志并发出警告,而非声称已兑现;循环分发点的 TODO(pre-tool-input-rewrite) 标记了缺失的更早阶段。
验收标准
- 请求的重写在
ToolExecution身份标识创建之前解决,并原子地反映到全部三个读取方:tool/call审计记录重写后的参数(原始参数保留在伴随字段中)、派生历史与实际执行一致、展示层渲染重写后的参数。 - 生效的
ToolExecution.arguments在 pre-policy、守卫、分发、post-policy 和最终观测全程保持深度冻结且不可写;不引入任何可变 shim。 - CC/Codex 桥接兑现
updatedInput,不再记录忠实但降级的警告。
风险
- 重写
assistant/message中的工具调用块会改变模型「看到自己说了什么」;是否有提供方在回放时拒绝这种改动,是一个需要通过实验确定的开放问题,必须在决策结构定型之前解决。 - 更早的重写阶段改变了
assistant/message、tool/call、钩子审计事件与执行之间的顺序关系;设计必须固定这一顺序,同时不削弱轮次封闭性或调用/结果邻接性。
开放问题
- 重写
assistant/message中的工具调用块是否会破坏某些提供方在回放时的预期?还是单独的修正更安全? - 原始参数是否应保留在
tool/call事件(审计)上?如果是,放在什么字段? - 重写决策是移到日志提交之前,还是成为一个专门的更早扩展点?现有的 pre-tool 允许/拒绝钩子如何避免运行两次?
- 这与未来的权限
ask流程(用户批准一个被重写的调用)如何交互?