侧边栏壁纸
博主头像
一笑痕

人生若只如初见,
是可喜亦或者是可悲?

  • 累计撰写 259 篇文章
  • 累计收到 7 条评论

Agent 执行前,审批必须绑定完整调用内容

2026-11-4 / 0 评论 / 2 阅读

Agent 执行前,审批必须绑定完整调用内容

审批框显示「归档记录 R-17」,确认之后,执行器手里的参数却被换成了「删除记录 R-17」。如果审批状态只记着工具名 record,调用前再从可变对象里读取参数,审批者看到的内容和实际执行的内容就不是一回事。

本地桩只做一件事:审批时对工具名和参数生成 SHA-256 指纹,分发前重新计算。工具处理器不会访问文件、网络或数据库。

approved fingerprint: 89bbf06ca109
unchanged call: EXECUTED:archive:R-17
altered call: DENIED: approved call changed
assertions: passed

验证脚本是 tools/verify-agent-approval-binding.mjs。输出的指纹只截取前 12 个十六进制字符用于显示;比较时使用完整摘要。未变更的调用抵达固定桩,参数改写后则在执行器查找处理器之前被拒绝。

审批的是哪一份内容

审批凭据至少要绑定工具名与完整参数。只绑定工具名,无法区分同一个 record 工具的 archive 和 delete;只绑定参数,调用名也可能换到另一种操作。实验把两者一起放进指纹输入:

import { createHash } from 'node:crypto';

function fingerprint(call) {
  return createHash('sha256')
    .update(JSON.stringify({ name: call.name, arguments: call.arguments }))
    .digest('hex');
}

批准后若参数对象被改动,下一次计算结果不同。执行器可以拒绝此次调用,要求重新展示并审批新内容。这里检查的是数据有没有变化,不是审批者是否看懂了风险。界面仍需把关键参数展示成可读形式,不能让一串摘要替代确认内容。

变更要发生在检查之前

把检查放在工具分发前是关键。若先调用处理器、再比较摘要,副作用已经发生,拒绝消息只是在事后补记。固定桩用返回字符串代替真实副作用,并用断言确保变更调用没有走进 record 处理器。

另一种实现是审批后冻结参数对象,避免后续代码改写。但浅冻结只保护最外层;arguments 内还有对象或数组时,嵌套值仍能变。冻结也不能保证从其他副本取出的新参数与展示内容一致。把审批快照和即将执行的调用在执行边界比较,定位更明确;冻结可作为减少意外修改的附加措施,不能代替复核。

指纹也有序列化边界

脚本把结构固定为 name 与 arguments,并直接对 JSON.stringify 的文本计算摘要。对来自 JSON 的普通对象,同一份对象在这段小测试里会得到稳定结果;若系统把参数重新构造、改变属性顺序,等价对象也可能形成不同文本。拒绝这种变更是安全的,只是会多要求一次审批。

若系统重建参数时改变属性顺序,等价对象也可能形成不同文本;拒绝这种变更是安全的,只是会多要求一次审批。数组顺序若有业务含义,就必须按实际顺序进入指纹,不能在规范化时擅自排序。输入若允许非 JSON 值,还要先拒绝或转成明确表示,避免 undefined 等值在序列化后消失。摘要也不是授权机制:知道某个输入的 SHA-256 值并不代表有权执行,若攻击者能同时改写调用和审批凭据,单独的摘要挡不住篡改。凭据要保存在调用方不能随意替换的位置,权限判断还要独立进行。

实验没有接入模型、用户界面或真实工具,也没有声称任意 Agent 框架会自动发生参数替换。它验证的边界很窄:工具名或参数一旦在审批后变化,执行前的指纹比较能识别这次变化。对会写入、删除或发送内容的工具,审批对象应覆盖实际执行的全部参数;若执行时还会补默认值,就应先补完再展示和签名。我会让审批界面引用的那份参数对象直接进入分发;确实需要重建时,就在重建后再做一次确认。否则,人看到的仍然不是将要运行的那一份调用。

    🤞 分享