What this is for (the 30-second version for everyone)
Increasingly, the person "doing the work" is an agent, and the person "checking the work" is also an agent. The moment you pay, merge, deploy, or ship on an agent's say-so, you have a new question: who verifies the verifier?
OpenWorkProof is a Python protocol layer that answers that question with evidence instead of vibes. It takes one unit of agent work — a task, a git change, a test run — and freezes four things into a signed, append-only, offline-replayable chain:
- Authorization — who was allowed to do this work, signed by keys.
- Scope — exactly which files and which tests were claimed, and what was actually observed; the two must be byte-identical.
- Population completeness — if the engine saw N eligible tests but the selector picked zero, that run does not pass. (Call this the "tested nothing, reported green" blind spot.)
- Negative-control semantics — a deliberately injected mutation must not merely fail; it must fail for the registered reason (error code / predicate signature). "Failed by accident" ≠ "failed as designed".
The verdict is deliberately three-valued and monotonic: VERIFIED, REFUTED, UNKNOWN — where UNKNOWN is a safe conclusion, not a crash. The CLI is fail-closed: VERIFIED=0, UNKNOWN=3, REFUTED=4.
For business readers: this is not a payment rail, not an auto-settlement system, and not a claim that any customer adopted it. It is a protocol capability, demonstrated on our own work item (Rich #4196). What it buys you is a concrete answer to "what exactly was verified, by whom, against which inputs — and can I replay that myself without trusting anyone's server?"
Why v0.5 is designed this way
v0.3 proved "claimed scope == observed scope, exactly." The design of v0.5 follows from two attacks v0.3 could not see:
-
The population blind spot. An executor sees many eligible tests; a selector may legally choose a subset — or zero. A run that selects nothing ends successfully and, under older semantics, looked green. v0.5 therefore binds what was eligible, what was excluded, and why into the signed selector spec, and requires a positive match before
VERIFIEDis reachable. -
Negative-control corruption. A registered mutation "failing" is meaningless unless the failure signature matches the registered one. v0.5 stores the signature and requires
observed == signed == expectedbefore a control arm isproven.
Two engineering principles carry the rest of the design:
- Never trust the agent's self-report. Every decision is recomposed from canonical signed rows and re-signed; a plain JSON report with a self-hash is treated as untrusted input, not truth.
- Evidence must survive offline. Deliveries are bundles you can re-verify with no network and no original ledger, against an immutable candidate inventory — a supply-chain record bound to exact source bytes and fully-qualified image digests. Any source change invalidates the old inventory by construction.
The audit: we invited someone to break it
After the implementation was "done," we commissioned an independent external review — and it declined to take our word for anything. It reproduced attacks by hand and returned 2 Critical + 5 Important + minors:
-
Critical 1 — offline package authenticity. The
scope-coverage-report.jsonwas self-hashed JSON. The customer-private view replayed the signed objects and then threw away the recomputed decision, returning fields from the report instead; the public/diagnostic views had no signed objects at all yet could still returnREADY_FOR_ACCEPTANCE. Reproduction: tamper the report's decision, re-sync the manifest hash/size — signature verification still passes; a public package can be forged intoVERIFIEDfrom zero. -
Critical 2 — selector inputs not frozen. The git selector's spec digest omitted allowlist/excluded/required locators; pytest's omitted
selector_args/required_node_ids; the pytest adapter treated any stdout line containing::as an authoritative node id. Reproduction: two different allowlists (or argument sets) with identical output produced byte-identical observations and evidence — both satisfied. -
Important 1 — decision loading checked only the Decision ↔ parent ID link; it never loaded or validated Arm Results, never recomposed, never checked
committed_at. Deleting every Arm Result row still loadedVERIFIED. -
Important 2 — control evidence
PROVENrequired only non-empty refs + a self-reported matching signature: any{"arm": "negative"}JSON could be proven. -
Important 3 —
Path.resolve()dereferenced.venv/bin/pythoninto the base interpreter;-m pytestlost site-packages and died with ModuleNotFoundError. -
Important 4 — the CLI could exit 0 on
UNKNOWN/REFUTED, and audit-explain/compare bypassed the v0.5 derived functions. - Important 5 — several matrix entries "existed as test names" without covering what they claimed; the plan referenced phantom test filenames.
How we closed it: attack tests first (RED → minimal fix → dual review)
Every finding was handled the same way: write an attack-shaped test, confirm it is RED against the old code, apply the smallest fix, commit, then have two independent sub-agents review — one against the spec, one for quality/security. Critical and Important findings blocked progress until closed.
-
Batch A — offline packages trust only replayed signed truth. The customer-private verdict now comes solely from the recomposed, signature-verified
Decision; the report is compared field-by-field against the replay and any divergence fails closed. Public/diagnostic views — which carry no signed redacted attestation — returnUNAUTHENTICATED/NOT_READY. Attack tests: forged report decision + synced manifest; from-zero forgery of a public package. -
Batch B — freeze every selector input into the digest. Allow/exclude/require parameters are canonicalized into
selector_spec_digestfor both selectors; node ids come only from a controlled canonical collector (a closed JSON file written atpytest_collection_finish); stdout is never parsed. Reviewers then demonstrated two further bypasses — a nestedconftest.py(trylast hook) and a root-levelpytest.pyshadowing the real pytest — closed with conftest-free refusal and-Iisolation, each with its own regression test. -
Batch C — decision history is fully replayed at every entry point.
_load_current_decision_v05now loads canonical Arm Results by parent chain, validates each row (id/digest/authority/signature/evidence), recomposes every chain link from its predecessor requiring identical signing bytes, and enforces canonicalcommitted_atplus causal/monotonic ordering (leap seconds rejected). Attack tests: deleted parent rows, row swaps, timestamp tampering, relation drift. -
Batch D — a closed control-evidence resolver. A 9-key canonical document (
openworkproof-control-evidence/0.5) is now the only accepted shape; ledger and offline package share one resolver;provenrequiresevidence fact == signed observation == registered expectation. Attack tests: legacy blob shapes, contradictory facts, missing evidence. -
Batch E — respect the venv launcher. Keep the absolute invocation path (no final-symlink dereference), bind the real target,
pyvenv.cfg, and executable digest separately; a real.venvregression test drives a real pytest collection. -
Batch F — CLI parity. Unified exit map (
VERIFIED=0 / UNKNOWN=3 / REFUTED=4, unrecognized values fail closed to 3); audit-explain/compare reuse the v0.5 derived functions; end-to-end CLI tests cover all three verdicts. -
Batch G — matrix, supply chain, and plan truth. Added fault injection (insert / before-COMMIT / readback) and same-id concurrency for Profile/Arm/Decision, physical tampering across all six v0.5 table families; the archive converter rejects duplicate tar members, absolute paths, and
.., and derives platform from the real config; eliminated pytest tmpdir cleanup noise; corrected phantom test filenames and the_ledger_delivery_protocoltext in the plan.
Re-freezing the release: immutable inventory + five gates
Because any source change invalidates the old inventory (by design), we rebuilt the candidate after the minors closed:
- Candidate inventory:
supply-chain/images/candidates/d460c876a7f3046fd1d338951d964bce6d1a6be1.json(fully qualifieddocker.io/openworkproof/execution-test@sha256:2acf4820…) - focused v0.5: 370 passed
- frozen v0.2–v0.4 compatibility: 216 passed
- portable full suite: 3348 passed / 0 failed / 6 skipped
- candidate live-Docker suites: 173 passed
- required-live full run: 3456 passed / 0 failed / 0 skipped, zero warnings
- Rich #4196 offline delivery bundle: VERIFICATION PASSED (no network, no original ledger)
What this does not claim
Green tests and offline replays do not equal customer adoption, paid SOW, deposits, upstream adoption, or commercial validation — those states remain not_evidenced. OpenWorkProof is a complementary layer: it freezes authorization, scope evidence, population completeness, and negative-control semantics for one unit of work. It does not replace MCP/A2A interoperability or identity, and it does not execute payment or settlement.
If you have been burned by "the agent said it verified it," we'd like to hear from you — and we invite independent reproduction of both the attack tests and the candidate supply chain.
Top comments (2)
The UNKNOWN exit code is the bit I wish more agent tooling copied. Most systems still treat missing evidence as a weird internal failure, which quietly teaches callers to retry until they get a green answer. Making uncertainty a first-class verdict is much easier to wire into CI and payment gates.
Ran a follow-up adversarial pass on v0.5, focused on the audit process itself — three findings, none breaking Batches A-G as engineering, but each narrows an implicit claim.
Attack 7 — Reviewer correlation (dual sub-agent review isn't independence)
Every finding was closed via "two independent sub-agents review." But independence in role/prompt isn't independence in failure mode — if both are the same model family, they share blind spots by construction. Same shape as Attack 5's split-view, one layer up.
Attack 8 — Build-time drift under "immutable candidate inventory"
Pinning by final image digest freezes the output, not the build process. Unpinned base tags or lockfile-less deps at build time mean a supply-chain compromise between builds doesn't register as an inventory change.
Attack 9 — Negative-control signature: self-registered expectations
observed == signed == expected depends on who registers expected. If the same agent authors the mutation and its expected failure signature, the check can't structurally fail — the "proof" and the "spec of what counts as proof" share an author.
This last one is the same failure shape as the evidence-quality result from my own memory-verification writeup (post): a verifier given weak evidence (bare token, or here, a self-registered expected signature) isn't being paranoid or negligent when it can't discriminate — it structurally has nothing to discriminate with. Green in both cases means "insufficient evidence to fail," not "verified."