
检查通过,为什么还不能立刻放心?从启动准入到文件落盘的最后一段距离
批准一份文件操作时,源文件里是内容 A。执行器真正读取它时,还一定是 A 吗?
这个问题看起来像文件系统细节,却会影响 Agent 的整个执行链。批准可能发生在前一轮,任务可能经过排队,执行环境还可能需要等待资源。如果中间的对象或条件改变,“之前检查通过”究竟还能允许什么?
一种直觉是:再加一次检查,尽量放到执行之前。这个方向有价值,但还需要追问:检查的是哪件事,检查之后到真实动作之间,还有什么可能变化?
本轮我们把外部的启动准入与精确文件更新放在一起比较,再用 CodeFlowMu 的真实审批服务和文件执行器做时间对照。我们没有复现生产并发事故;其中检查后的变化是研究代码明确注入的。实验要回答的是检查的职责边界,而不是先宣布一个漏洞。
1. 同样叫“执行前检查”,保护的对象可能完全不同
Paperclip 是组织 Agent 工作和运行状态的开源系统。其 PR #12949 提议在一次运行已被认领、但适配器尚未启动时增加准入判断,让宿主依据模型、服务提供方和近期容量信息决定是否开始。
它区分观察模式与强制模式:前者记录判断,后者可以阻止启动;强制模式下,容量信息缺失、过期或无效会阻止继续。该提案在本轮核验时仍未合并,不能写成已经交付的可靠能力。
这种检查首先回答的是“现在能不能启动这次运行”,不是“运行里未来每条文件操作是否仍获授权”。即使容量检查完全正确,也不能替代某次写入的目标、内容和权限核对。
而 GitHub MCP Server 的 Draft PR #3232 面对的是具体文件修改。MCP 可以理解为 Agent 调用外部工具的一种接口协议;这个服务端把 GitHub 操作提供成工具。提案要求调用方给出预期分支提交和目标文件内容对象的 SHA 标识,再进行精确文本替换。
两者不是谁更安全的问题,而是各自保护不同对象:前者约束一次运行的启动条件,后者约束一次仓库文件变更的输入事实。把它们都叫“门禁”,很容易掩盖这些差别。
2. 我们把源文件变化,放在检查的两侧
CodeFlowMu 是我们在开发的本地多 Agent 协作系统。它的一类文件操作会把工作区、任务、源和目标快照组成具体请求,批准之后,执行前再构造当前请求并核对摘要。摘要是内容与结构的指纹,用来发现批准对象发生了变化。
此前的研究已经验证目标变化会使旧批准失效。这次我们没有重述同一结论,而是进一步问:源内容在不同时间变化,得到的结果是否一样?
我们使用固定提交 c008d9db91a21136fc61a4f60314e22db395d5d2 的真实组件。两组实验都执行复制,目标开始时有旧内容,且都明确设置允许覆盖。这样,覆盖本身不是争议变量;唯一要区分的是源内容变化与摘要检查的先后。
| 对照 | 源内容何时改变 | 实际结果,两轮一致 | 文件效果 |
|---|---|---|---|
| 检查之前改变 | 批准后改源,再重建当前请求并执行 | APPROVAL_STALE,即旧批准已不适用;执行回调 0 次 | 目标旧内容保留 |
| 检查之后改变 | 请求摘要校验通过后,在研究回调中改源,再调用真实文件执行器 | 操作记录成功;执行回调 1 次 | 目标收到改变后的源内容 |
第二行需要把实验操作说透:我们没有制造一个真实用户恰好抢写的现场,而是在审批服务调用执行回调后,由研究代码修改合成源文件,再进入产品已有的复制函数。这个介入点是人为控制的,不是已经证明在线调用者可以获得的暂停窗口。
因此,第一行支持既有保护有效,第二行支持受测函数组合在这类时序下会使用变化后的字节。它们还不能回答实际部署中谁能改变源、哪条入口允许同样的交错、竞争有多大概率发生。
这两种证据强度必须分开,不能因为第二行足够醒目,就把第一行的保护或实验自身的介入删掉。

图 1:E4/E5 时序对照,每组两轮,均允许覆盖。E5 的变化由研究代码在检查后明确注入,不是生产竞态复现;来源:正式受控实验记录,见证据说明。
3. 摘要没算错,问题是它代表哪个时点
源码解释了为什么会出现这种差别。
审批服务接收“当前请求”,计算其摘要,与批准记录中的摘要比较。一致之后,它把操作记为执行中,再把批准记录交给执行回调。复制执行器随后从源路径读取当时的文件内容。
在检查前改变源时,重建请求得到新的源快照,所以摘要不同,执行回调没有被调用。
在检查后注入变化时,审批服务已经完成比较。执行器仍读取同一个路径,但这个路径里的字节已经不同。记录中的源快照是 source-before 的摘要,实际目标读回是 source-after,两份摘要不一致;正式两轮结果相同。
这里没有必要假设摘要算法失效。摘要正确表示了被检查的输入,它却不能阻止输入背后的可变对象继续变化。
这类问题常被称为 TOCTOU,即“检查时与使用时不一致”。这个术语说明要研究的时间关系,不自动证明某个生产路径存在可利用的竞争。快照能够描述事实,不能仅靠自身冻结事实。
同样,临时文件加重命名可以解决一类目标发布问题,但不能单独证明源字节仍是批准时那份内容。它们是不同的保证,需要不同的证据。
4. 精确前置条件,还需要精确的提交规则
GitHub MCP 的提案提供了一个有价值的对照:它不只要求预期分支和文件 SHA,还把新提交建立在指定父提交上,最后以非强制方式更新分支。这把“原来检查了什么”与“最终允许怎样提交”连接起来,而不只是增加一个事前提示。
但是,非强制更新限制的是非快进行为,不能不加验证就称为覆盖一切分支移动、回退和特殊并发场景的严格比较并交换。该提案仍为 Draft,作者也明确说明未在其环境中跑完整 Go 测试;我们没有独立完成它的远端集成验证。这里借用的是代码所表达的设计对照,而不是借用一个“已验证安全”的结论。提案与限制
它的返回结果已经包含修改前后提交和文件内容对象的 SHA。值得研究的下一步不是要求再返回一次这些字段,而是调用方是否把结果保存下来,与批准对象、实际执行及后续重试关联。
Paperclip 的启动准入则提醒我们另一个问题:检查的位置正确,还需要检查适用范围正确。已开始运行后的重新接入,与尚未开始的新启动,并不是同一件事;容量判断也需要对应实际选中的提供方和模型。#12949 的变更与评审 已在讨论这些边界。本轮没有复跑其数据库并发及恢复场景,不把评审提出的问题当作我们独立确认的故障。
外部方案与本地实验可以相互启发,但不能相互代证。Git 分支更新不是本地复制,运行容量准入也不是操作批准。真正能抽出的共同问题是:检查、执行和提交,是否仍然指向同一个对象,以及同一组仍然成立的条件?
5. 不先造新框架,先验证最后一段距离
面对这样的研究结果,直接增加一个“再检查一次”函数并不一定足够。新的检查之后仍然可能发生变化;不同资源也不一定适合用同一种锁。
下一步更有价值的是把现有链路问清楚:
- 批准的是什么? 是路径、具体字节、某个版本,还是这几者的组合?
- 最终动作使用了什么? 使用已绑定的数据,还是重新读取一个可能改变的路径或配置?
- 什么机制保护检查到提交之间的条件? 是排他所有权、条件更新、不可变对象,还是仅仅先查一次?
- 条件变化时留下什么? 是零效果拒绝、已产生部分效果,还是证据不足?这些不能统一压成一个失败。
真实入口实验应同时覆盖无变化、检查前变化和检查后变化,记录源/目标摘要、实际调用次数与回执。没有变化时正常动作必须通过;变化被发现时应检查是否真的没有产生目标效果,而不只是看到一条错误消息。
对于我们目前的证据,合理处置是继续核查真实注册调用路径及文件竞争条件,不是立即宣布发现线上竞态,也不是据此授权开发通用事务引擎。已有宿主、存储系统或执行器如果提供条件提交能力,应先核查能否复用,而不是重新造一份。
可靠执行不只是“在动作前检查过”,而是能够解释:检查绑定的条件,为什么在真正动作发生时仍然成立。
证据与范围
本文使用正式数据中的 E4/E5,各两轮,共四条观测;配套文章使用 E0–E3,合计属于同一批十二条正式观测。中文证据说明、逐轮脱敏记录和英文说明已随本文公开提供。
预跑曾让两个时间场景也设置“不覆盖”,会混合参数适用性与时间差两个问题。正式复跑把两组都改为允许覆盖,只比较源变化时点;预跑保留但不计入正式数量。
这是 Windows 上的真实产品组件加合成时序实验,没有运行在线 HTTP、面板、真实 Agent、多进程文件竞争或远端 GitHub 修改。公开准备包可核对记录与摘要;完整产品复跑仍需相应源码和依赖。写作阶段没有新做一轮产品实验,也没有修改产品、通过独立 QA 或实施修复。