[控场AI]
· 8 分钟阅读· 4,227 字

合并门禁失效六次修复启示:AI编程Agent的代码验证困境

合并门禁失效六次修复启示:AI编程Agent的代码验证困境

AI编程Agent合并门禁工具四天六版迭代复盘:所有漏洞几乎都源于'因遗漏而默认放行'。

开发者Sanflow10为AI编程Agent构建的合并门禁工具AdversaryGate,在四天内连发六个补丁版本。其核心原则是'未被测量的指标不能用来通过门槛',但几乎所有漏洞都源于同一模式——fail-open by omission:某些输入落在度量范围之外,门禁默默跳过不报,补丁便带着其余部分的'通过'结论获得MERGE。文章详述了六种欺骗门禁的方式,包括读代码而非跑代码的测试、注释金丝雀被绕过、二进制标记隐藏变更行、文档冒充代码、同行分支变异遗漏以及测试命名模块逃避检测。同时也有诚实补丁被误判为INCONCLUSIVE的误伤案例。作者主动攻击自己的工具,公开记录每个失败案例与已知局限,并将发布流程从带token发布改为签名标签加Trusted Publishing。

AI编程Agent生成的补丁能不能合并?这个问题看似简单,但真正做过合并门禁的人会告诉你:"测试通过了"这句话,可能比你想象的更不可靠。

开发者Sanflow10最近发布了一篇详细的复盘文章,记录了他为AI编程Agent构建的合并门禁工具AdversaryGate,在四天内连发六个补丁版本的经历。核心结论:几乎所有漏洞都源于同一个模式——fail-open by omission(因遗漏而默认放行)。某些输入落在了门禁度量范围之外,门禁默默跳过不报,补丁的其余部分就带着这个"通过"的结论一路绿灯。

合并门禁工具的发布日志与问题追踪

AdversaryGate的设计初衷

AdversaryGate的核心逻辑并不复杂:在基线代码和补丁上分别运行测试套件,然后给出三种裁定——MERGE(退出码0)、BLOCK(退出码1)或INCONCLUSIVE(退出码2)。

支撑整个工具的设计原则只有一句话:一个没有人实际做过的测量,不能用来通过门槛。 这句话听起来理所当然,但真正实现起来,处处是漏洞。

变异测试(Mutation Testing) 是理解本文多条漏洞的关键背景。其基本思路是:向被测代码中注入小的人为缺陷(即“变异体”),然后运行测试套件,看测试是否能捕获这些缺陷。若测试失败,说明变异体被“杀死”,表明测试对这段代码有实际约束力;若测试仍然通过,说明变异体“幸存”,意味着测试存在盲点。变异强度(mutation score)即为被杀死的变异体占全部变异体的比例,常被用作测试质量的量化指标。AdversaryGate借用这一机制来评估补丁的测试充分性。然而,如果变异体生成策略本身有缺陷——比如只对前12行变更做变异、或无法覆盖同行分支——那么高变异强度就可能是一种假象:测试并没有真正覆盖全部变更,却因为变异体未被生成而显示为满分。这正是文中多次fail-open的根源之一。

Fail-open(失败即放行) 是与fail-safe相对的安全设计模式。在安全系统中,当组件发生错误或无法判定时,fail-open意味着系统默认允许操作继续,而fail-safe则默认拒绝。AdversaryGate的裁定逻辑中,MERGE代表放行,INCONCLUSIVE代表无法判定。所谓“fail-open by omission”指的是:当某项输入落在度量范围之外时,门禁没有将其标记为INCONCLUSIVE,而是直接忽略该输入的缺失,使得其余测量结果的“通过”结论被沿用,补丁因此获得MERGE。这一模式贯穿了文中所有漏洞:二进制标记隐藏变更行、文档冒充代码、测试命名模块逃避检测,本质上都是“没有测量”被当作了“测量通过”。

六种"骗过门禁"的方式

Sanflow10在3.0.0到3.0.6的迭代中,主动攻击自己的门禁,逐一发现了多种让不该通过的补丁拿到MERGE的方法。

读代码而不跑代码的测试

AG-046(3.0.0修复):一个对变更文件做哈希的测试,会在任何编辑下失败,于是它"杀死"了所有变异体,变异强度直接算出1.0。修复方式是引入"注释金丝雀"——在变更行的末尾加注释后重新运行声明,如果某个声明因此失败,说明它在读文本而非验证行为,该强度被标记为未测量。

注释金丝雀(Comment Canary) 是AdversaryGate为识别“伪测试”而设计的一种主动探测机制。其原理是:在变更代码行的末尾插入一个无害注释(canary),然后重新运行相关测试声明。如果某个测试声明在此修改后失败,说明它实际上是在读取代码文本内容(如哈希、字符串比对),而非验证代码的运行行为——因为注释不改变程序语义,真正的行为测试不应受其影响。这类测试被标记为“未测量”,其变异强度不再计入有效评分。金丝雀机制体现了“主动攻击自己的度量工具”这一设计哲学:不假设测试就是测试,而是通过引入可控扰动来验证测试的真实性质。类似地,3.0.1版本将no-op赋值加入金丝雀,是为了同时扰动AST(抽象语法树)和字节码层面的比对,防止仅比较语法结构的声明绕过检测。但作者也坦承,若声明只读取语法树的更小片段,仍可能逃逸,这说明了度量工具的边界具有递归性。

金丝雀本身也能被绕过

3.0.1发现,比较AST或字节码的声明仍然能通过注释金丝雀。金丝雀随后加入了no-op赋值来改变语法树和字节码。作者坦承这是部分修复:如果声明只读取语法树的更小片段,仍然能通过。README中如实记录了这一点。

.gitattributes中的二进制标记

3.0.1修复的另一个漏洞:一个.py文件在.gitattributes中被标记为binary,git diff输出Binary files ... differ,变更行从覆盖差异中消失,补丁直接进入MERGE。现在这种diff会被拒绝。

文档变更拿到满分覆盖

删除一行README就能让覆盖率变成1.0——因为文档被当成了代码。修复后,没有代码行的补丁覆盖率标记为未测量,无法凭覆盖率获得MERGE。

覆盖率(Code Coverage) 在本文语境中特指测试套件执行时实际触达的代码行或分支比例。常见的行覆盖率、分支覆盖率等指标被广泛用于判断测试充分性,但其致命弱点是:覆盖率只衡量“哪些代码被跑过”,不衡量“被跑过的代码是否被正确断言”。如果测试调用了某行代码但没有验证其输出或副作用,覆盖率仍然会上升。更极端的情况是,当覆盖率工具将非代码文件(如README)误算为可执行代码时,删除一行文档反而可能导致分母减小,覆盖率被动升至满分。AdversaryGate对此的修复是:仅当补丁包含代码行时才计算覆盖率,没有代码行的补丁覆盖率标记为“未测量”,从而切断通过文档变更刷高覆盖率来获得MERGE的路径。这再次印证了核心原则——未被实际测量的指标不能作为通过依据。

同行分支与变异覆盖不足

3.0.4修复了两个问题:一是return 999 if x == 0 else x + 1这类同行分支在5/5变异体被杀的情况下仍到达MERGE,现在门禁会生成分支变异体,幸存者成为盲点并触发INCONCLUSIVE;二是此前只对前12行变更做变异,未测试的后续变更永远不会被破坏,现在每行变更至少被破坏一次。

测试命名的产品模块

AG-047(3.0.6修复):门禁仅凭路径判断什么是测试。一个命名像测试但被被测代码导入的模块,被排除在覆盖率和变异之外,对它的变更从未被评判。现在被非测试代码导入的测试命名模块会被当作源码处理。作者诚实标注:这个修复在机制层面验证过,但未作为端到端攻击复现;通过importlib或__import__动态导入的模块仍无法检测。

不只是fail-open:误伤也在发生

并非所有问题都是放行不该放的。有些恰好相反——诚实补丁被误判为INCONCLUSIVE。\ No newline at end of file标记被当作一行导致行号偏移;MCP的900秒超时限制短于完整测试套件的运行时间;在Android(Termux)上,默认内存限制导致每个进程都被中止。这些问题在3.0.5中修复。

发布流程的加固

3.0.2和3.0.3两个版本带着API token发布,MCP包停留在3.0.0,核心文件没有attestation。作者从PyPI和GitHub移除了3.0.4之前的所有版本。现在发布流程单向执行:签名标签→Trusted Publishing→两个包都携带PyPI attestation。

已知的局限

AdversaryGate仍然有明确的边界:测量仅支持Python和pytest,其他语言一律返回INCONCLUSIVE而非猜测性MERGE;无法阻止以相同OS用户身份运行的Agent访问门禁的临时目录;金丝雀修复是部分的,每个限制都写在修复旁边。

每个发现的漏洞都记录在公开的ERRORS_AND_INCONSISTENCIES.md中,包含复现方式和修复方案。

Attestation(来源证明) 是软件供应链安全中的关键概念,指由构建系统生成的、可验证的元数据,用于证明某个制品确实由特定源码、在特定环境下、通过特定流程构建而成。在Python生态中,PyPI支持基于Trusted Publishing的attestation,构建过程由CI/CD平台(如GitHub Actions)签发短期凭证,无需长期API token。Attestation的价值在于:即使攻击者获取了发布权限,也难以伪造出与合法构建流程一致的证明链条。文中3.0.2和3.0.3因携带长期API token发布而存在供应链风险,攻击者若窃取token即可发布恶意版本。改为Trusted Publishing后,发布凭证与具体CI工作流绑定,且两个包均附带attestation,显著提升了发布的不可否认性与可审计性。这一修复与门禁本身的安全逻辑一脉相承:不仅要验证补丁,也要验证发布补丁的流程。

对AI编程工具生态的启示

这个案例的价值超出了单个工具本身。随着AI编程Agent越来越多地生成和提交代码,"如何验证Agent的产出"正在成为一个独立且紧迫的问题。AdversaryGate的经历揭示了一个深层矛盾:验证工具本身也需要被验证,而验证的边界往往是模糊的。

Sanflow10的做法值得借鉴——主动攻击自己的工具,把所有失败案例公开记录,在每个修复旁边标注已知局限。这种"把不安全的地方写下来"的态度,比宣称"我们的门禁是安全的"要有价值得多。

如果你能让3.0.6产生一个虚假的MERGE,作者表示非常想知道。

#AI编程Agent#合并门禁#代码验证#AdversaryGate#变异测试#CI/CD安全#AI代码审查
分享:

相关推荐