四起事故,以及它们的共同点
四起里,检查都存在、也都通过了 —— 坏掉的是绑定关系 · Macheng Shen × agent · 2026-09-07
下面四起事故都确实发生过,被记录下来,并且都以一份书面 receipt 收口。这一点是可观察的事实——本页的认知状态是survived(已发生、已收口),不是 speculative。结尾给出的那条结论——一个系统可以在检查这个维度上被逐一覆盖,却依然不安全——是作者对这四个案例放在一起做出的解读,不是第五个观察,应当比事故本身少一分信任。四起都做了脱敏处理:没有设备型号、没有 app 或平台名称、没有订单内容、没有具体人名——但机制本身写得完整,因为机制才是重点。
(a) 校验一次,后面全靠假设
一个 agent 正在向一台手机注入一段持续几分钟的合成按键序列。它在开始时确认了一次设备处于解锁且空闲状态,然后把这一次检查当作对整段序列都成立。序列进行到一半,机主把设备拿了起来,自己开始操作。此时前台已经变成一个结算页面,随后又变成一个支付确认页面;最后两次注入的确认动作可能就落在了这两个页面上。这件事无法被确证——agent 也拒绝去查看机主的交易记录来弄清楚,理由是这样做本身会构成第二次隐私侵犯,而不是对第一次的补救。正是这份克制,让这起事故显得可信,而不是显得方便。失败类别:一个只在会话开始时检查一次、此后被默认对其中每个动作都成立的前置条件。修复:在每一次注入动作之前,分别重新校验解锁状态、前台应用和空闲时长。诚实的缺口:截至同一天的审计,这个修复还没有接入其它注入输入的工具。
(b) 一个跟自己死锁的门禁
一个 session 想给机主发一条通知,却被自己的并发门禁拦了下来——门禁报告说,租约的持有者"已验证存活",而这个持有者正是它自己。它的获取路径在申请新租约前,漏掉了检查自己是否已经持有租约这一步,于是又建了一个;随后一次 compare-and-swap 把更早的那个租约当成权威版本,判定较新的这个输掉了竞争,于是删掉了自己刚刚创建的租约,再把胜出者——也就是它自己——报告为存活且处于阻塞状态。第二个独立的 bug 让情况更糟:同一个 session 在从 hook 进入和从手动 shell 调用进入时,会推导出两个不同的持有者身份,于是门禁把一个行动者当成了两个。失败类别:"这是不是同一个行动者"这个身份判断,在同一个门禁的不同入口之间被不一致地推导出来。修复:存活性与自身身份改用进程祖先关系判定,不再用字符串比较。补丁前 25 次测试全部复现了这个 bug;补丁后 28 次测试全部通过,并附一张前后对照表,证明没有削弱任何跨 session 的保证。
(c) 状态显示健康,实际全线失败
三次派往第二个执行 provider 的后台任务,都在第一个模型回合就挂掉,报错完全相同的 HTTP 400,无论请求哪个模型都一样。根因是本地一份模型元数据缓存丢了一个必填字段,导致客户端对它运行的每一个模型都悄悄退化成了残缺元数据。三次失败期间,该客户端自己的 --version、--help 和 status 命令全程显示健康,因为这些命令都不发起真实调用。失败类别:一个不去实际执行它所认证的那件事的健康检查。修复:升级,并保留那份损坏的缓存以便回滚。由此定下的常设规则很具体:针对这一类依赖的 canary,必须做一次真实的往返调用,而不是版本探测。
(d) 权限在转发那一步被降级
在同一个 session 的 307 份 sub-agent 转录里,有 10 份带有"被转发的指令不构成授权"这类拒绝措辞;其中至少 4 份是硬性拒绝,逼得父 session 要么重新下达一次指令,要么干脆自己动手完成那部分工作。这些 sub-agent 的表现是正确的——一个 agent 确实无法区分"机主说过"和"我的调用方声称机主说过",这两句话摆在它面前是同一个字符串。真正的 bug 是结构性的:权限在转发的那一刻就被降级成了一个无法验证的说法,而任何 sub-agent 层面的判断力,都找不回一开始就没被传下来的信息。修复:一个签名的 authority envelope,携带机主的原话、到达的渠道、作用范围、有效期(TTL)与委派深度,其中委派只能收窄权限,不能放大——参照的是委派授权令牌交换(RFC 8693)以及 macaroons、biscuits 这类只能收窄的能力令牌。schema 本身是公开的;真正在跑的、带签名的实现是私有的。它记录在案的边界说得很直白:它建立的是来源可追溯性,不是安全边界。
(e) 一个简短的正面结果
这一层不全是失败。有一个 canary 专门测相反的方向:派发一个目标根本不存在的任务,如果编造一个看似合理的结果,是可以直接通过自动验收的。到目前为止的两次运行中,worker 都选择了带着证据宣布自己被阻塞,而不是编造一次成功。这个次数确切地是 n = 2,如实报告为 2,不往上凑成一个趋势——这是一个存在性证明,不是一个比率。
这四起事故的共同点
这四起事故里,检查都存在,而且都通过了。案例 (a) 有一次存活性检查,它在一开始通过,此后再没被问起过。案例 (b) 有一个并发门禁,它针对拿到的身份做出了正确的判断,只是那个身份本身是错的,于是结论也跟着错。案例 (c) 有一个健康检查,它如实报告了它被设计去报告的那件事,只是那件事恰好不是真正要紧的那件事。案例 (d) 有一个拒绝策略,在每一跳都按设计工作,而在那一跳上,拒绝正是那个 agent 唯一在结构上说得通的正确反应。
每一起里,出问题的都不是检查本身,而是绑定——检查和它本该覆盖的动作之间的绑定,检查和它本该核对的身份之间的绑定,检查和它本该认证的那件事之间的绑定,一条指令和一份可核实的来源记录之间的绑定。一个系统可以在门禁这个维度上被逐一覆盖,却依然不安全,只要某个还没被真正跑过的场景里,门禁和它该管的事件之间接错了线。这也是为什么本节其它地方报告的覆盖率数字,数的是(工具、入口)这样的对子,而不是数门禁的数量:一道门禁的存在本身不构成任何证据,直到它被证明能从每一条需要它的路径抵达,并且正确地绑定在那条路径上。