验证验证者:独立审计如何强化 OpenWorkProof v0.5

Dev.to AI 2026-08-16T01:54:03.877611

这个东西是干嘛的(30 秒看懂版)

现在越来越多的情况是:"干活的人"是一个 agent(AI 智能体),"检查工作的人"也是一个 agent。当你因为一个 agent 的"保证"就去付款、合并代码、部署或发布,一个新的问题就出现了:谁来验证验证者?

OpenWorkProof 是一个 Python 协议层,它用证据回答这个问题,而不是用感觉。它把一份 agent 的工作——一个任务、一次 git 改动、一次测试运行——凝固成一条"签名、只能追加、可离线回放"的链,链上冻结了四样东西:

结论故意设计成三值且单调的:VERIFIED(已验证)、REFUTED(被推翻)、UNKNOWN(未知)。其中 UNKNOWN 是安全结论,不是程序崩溃。命令行按"失败即关闭"处理:VERIFIED 对应退出码 0,UNKNOWN 对应 3,REFUTED 对应 4。

给业务读者的话:这不是支付通道,不是自动结算系统,也不代表有客户已经采用了它。它是一个协议能力,在我们自己的一个工作项(Rich #4196)上做了演示。它能给你一个具体答案:"到底验证了什么、是谁验证的、针对哪些输入——以及,我能不能不信任任何人的服务器,自己重放一遍这个过程?"

v0.5 为什么这样设计

v0.3 证明了"声称的范围 == 观察到的范围,而且精确一致"。v0.5 的设计则源自两个 v0.3 看不见的攻击路径:

一个已登记的“失败”变更,如果其失败签名与登记的不符,则毫无意义。v0.5 存储签名,并要求在证明某个控制臂之前,observed == signed == expected 必须成立。其余设计由两条工程原则支撑:绝不信任代理的自报信息。每个决策都由规范签名行重新组合并重新签名;一份带自哈希的普通 JSON 报告被视为不可信输入,而非事实。证据必须能在离线状态下存活。交付物是捆绑包,无需网络、无需原始账本,就可以对照一个不可变的候选清单重新验证——该清单是一份供应链记录,绑定到精确的源代码字节和完整的镜像摘要。任何源代码变更都会从构造上使旧清单失效。

审计:我们请人来攻破它。
在实现“完成”后,我们委托了一次独立的外部审查——而对方拒绝只凭我们的一面之词。他们手工复现攻击,最终给出 2 个严重 + 5 个重要 + 若干次要问题:

严重问题 1——离线包真实性。
scope-coverage-report.json 使用了自哈希 JSON。customer-private 视图重放了签名对象,却丢弃了重新计算出的决策,直接返回报告中的字段;public/diagnostic 视图根本没有签名对象,却仍能返回 READY_FOR_ACCEPTANCE。复现方法:篡改报告中的决策,重新同步清单的哈希/大小——签名验证仍然通过;一个公共包可以被从零伪造为 VERIFIED

严重问题 2——选择器输入未冻结。
git 选择器的规格摘要遗漏了 allowlist/excluded/required 定位器;pytest 的规格摘要遗漏了 selector_args/required_node_ids;pytest 适配器将任何包含 :: 的 stdout 行都当作权威节点 ID。复现方法:两个不同的 allowlist(或参数集)产生相同输出,进而产生字节完全一致的观测结果和证据——两者都满足条件。

重要问题 1——决策加载只检查了 Decision 与父 ID 的链接。
它从未加载或验证 Arm Results,从未重新组合,也从未检查 committed_at。删除所有 Arm Result 行后,仍然加载出 VERIFIED

重要事项2——控制证据 PROVEN 仅要求非空引用和一个自报的匹配签名:任何形如 {"arm": "negative"} 的 JSON 都能被证明成立。重要事项3——Path.resolve().venv/bin/python 解引用为基础解释器;-m pytest 因此丢失 site-packages,并以 ModuleNotFoundError 崩溃。重要事项4——CLI 在 UNKNOWN / REFUTED 状态下也能以退出码 0 结束,而且 audit-explain/compare 绕过了 v0.5 的派生函数。重要事项5——几个矩阵条目“作为测试名存在”,却并未覆盖其声称的内容;计划还引用了根本不存在的测试文件。

我们如何解决:先攻击测试(红 → 最小修复 → 双重审查)。每个发现都按同一流程处理:编写一个攻击形态的测试,确认它在旧代码上为红,应用最小修复,提交,然后由两个独立子代理审查——一个对照规范,一个负责质量与安全。关键和重要发现会一直阻塞进度,直到问题关闭。

批次A——离线包只信任重放后的签名真实数据。客户私有判定现在仅来自重组并经签名验证的 Decision;报告逐字段与重放比对,任何差异都会按失败处理。公开/诊断视图——不携带签名脱敏证明——返回 UNAUTHENTICATED / NOT_READY。攻击测试:伪造报告决策 + 同步清单;从零伪造一个公共包。

批次B——将所有选择器输入冻结到摘要中。allow/exclude/require 参数被规范化为两个选择器的 selector_spec_digest;节点 id 只来自受控的规范化收集器(在 pytest_collection_finish 时写入的封闭 JSON 文件);从不解析 stdout。审查者随后又演示了两种绕过——嵌套 conftest.py(trylast 钩子)和根级 pytest.py 遮蔽真实 pytest——分别通过拒绝使用 conftest 和 -I 隔离来关闭,每种都有对应的回归测试。

批次C——决策历史在每个入口点都被完整重放。

_load_current_decision_v05 现在通过父链加载规范的 Arm 结果,逐行校验 id、digest、authority、signature、evidence 等字段,并从每个前序记录重新组合链环,要求签名字节保持一致;同时强制使用规范的 committed_at,并遵循因果/单调排序(拒绝闰秒)。攻击测试覆盖:删除父行、行交换、时间戳篡改、关系漂移。Batch D——封闭式控制-证据解析器:现在只接受一种 9 键规范文档格式(openworkproof-control-evidence/0.5),账本和离线包共用同一个解析器,proven 状态要求“证据事实 == 已签名观测 == 登记预期”。攻击测试覆盖:旧版 blob 形状、矛盾事实、缺失证据。Batch E——尊重 venv 启动器:保留绝对调用路径(不做最终符号链接解引用),把真实目标、pyvenv.cfg 和可执行文件摘要分开绑定;一个真实的 .venv 回归测试会驱动一次真实的 pytest 收集。Batch F——CLI 对齐:统一退出码映射(VERIFIED=0 / UNKNOWN=3 / REFUTED=4,无法识别的值一律按默认值 3 处理);audit-explain 和 compare 复用 v0.5 的派生函数;端到端 CLI 测试覆盖全部三种判定结果。Batch G——矩阵、供应链与计划真实性:新增故障注入(插入 / 提交前 / 回读)以及 Profile、Arm、Decision 的同 ID 并发场景,物理篡改测试覆盖全部六类 v0.5 表族;归档转换器会拒绝重复的 tar 成员、绝对路径和 .. 路径,并依据真实配置推导平台;同时消除了 pytest tmpdir 清理时的噪音,修正了幽灵测试文件名和计划中的 _ledger_delivery_protocol 文本。

重新冻结发布:不可变清单 + 五道门禁

由于任何源码改动都会使旧清单失效(这是设计使然),我们在次要问题修复后重新构建了候选版本:

这些结果不构成什么声明

测试通过和离线回放并不等于客户采纳、付费工作说明书(SOW)、预付款、上游采纳或商业验证——这些状态仍然处于“尚无证据”(not_evidenced)阶段。

OpenWorkProof 是一个补充层:它为单个工作单元锁定授权、范围证据、总体完整性和阴性对照语义。它不替代 MCP/A2A 的互操作性或身份认证,也不负责执行支付或结算。

如果你曾因“代理说它验证过了”而上过当,我们很乐意听听你的经历——同时也欢迎你独立复现攻击测试和候选版本的供应链。

查看原文