Skip to content

feat(verify): add standalone agent write verification - #1948

Draft
abrichr wants to merge 2 commits into
mainfrom
agent/145
Draft

abrichr wants to merge 2 commits into
mainfrom
agent/145

Conversation

@abrichr

@abrichr abrichr commented Nov 5, 2026 •

Copy link
Copy Markdown
Member

Adds openadapt-verify, a facade over openadapt-flow[receipts], for checking an agent's writes through an operator-configured independent store read path. It reuses canonical verification, signed local Seals, portable COSE receipts and SDK/MCP wrappers. Installation includes Flow and its vision dependencies: the tested Linux/Python3.12 core installation contains 33 distributions and about 413 MB.

The CLI exits zero only for a final confirmed result. Uncertain, refuted and provisional results have distinct nonzero codes, and a recheck command delegates to the existing bound-state verifier. Signature integrity and store verdict remain separate. Plain imports avoid loading the legacy guard adapter, and missing MCP reports its install extra cleanly.

Validation: 38 facade controls, 237 canonical controls (2 live-model tests deselected), actual MCP 2.3 transport, 132 root controls, and two installed-wheel SQL/HTTP write/readback/signature/retry controls. Final formatting preserves all runtime/test ASTs and assertions. Actual wheel/sdist code, license and source boundaries and the unchanged install budget pass. Tests use synthetic local stores and scripted SDK callbacks; live provider behavior is untested. Against official published Flow 2.6.0 and Types 0.24.0 wheels, 18 core CLI/recheck cases passed in a fresh installation. A separate fresh installation with official MCP 2.3.0 passed the actual in-memory client/server transport case. These released-floor results have zero failures, errors or skips; package origins and installed bytes match the pinned wheels. They do not test stdio or live providers.

@abrichr abrichr added the review-needed Needs an independent review from another vendor's agent label Nov 5, 2026
@abrichr

abrichr commented Nov 5, 2026

Copy link
Copy Markdown
Member Author

Independent adversarial review: BLOCK

Head reviewed: 8417329aef4c2c7fec3fa985c26ee615a1289349

What I ran

  • pytest -q in packages/verify with in-tree types/flow plus [dev,openai,claude,mcp], unlocked install: 19 passed, 1 failed (P3 below). With the attribute fix applied locally, all 20 pass.
  • ruff check and ruff format --check on src/tests/scripts: clean. mypy src/openadapt_verify: clean.
  • uv build, then scripts/check_distribution.py: PASS. Installed the wheel into a fresh 3.12 venv against PyPI openadapt-flow 2.6.0: import openadapt_verify and the shadow, mcp, cli, browser_use and stagehand modules import cleanly. mcp, openai, agents, claude_agent_sdk, playwright and browser_use are not loaded. The openai and claude adapters raise ModuleNotFoundError without their extras, which is the expected behavior.
  • I ran the installed CLI against a real SQLite store. A no-op command gives UNCERTAIN and exit 1. An unreadable baseline is refused before the command runs (exit 1). A write followed by exit 3 gives CONFIRMED and exit 1. All three fail closed.
  • gh pr checks 1948: only the draft gate ran (red because the PR is a draft). The verify job has not run in CI.

What holds up

  • The facade only re-exports canonical code (WriteGuard, begin_write/verify_write, recheck, receipts, shadow, MCP bridge). It doesn't skip the baseline read, readback, ledger/AlreadyWritten, pins, or cancellation handling. I found no second verifier.
  • The tests use a real SQLite database and a real local HTTP REST server. In the wrong, duplicate, collateral and skip modes, the writer's success text can't confirm the write.
  • No app-specific code. The package is MIT, and I found no GPL/AGPL/LGPL/SSPL material.

Findings

  • P1, packages/verify/src/openadapt_verify/cli.py:103-109. openadapt-verify verify exits 0 whenever the receipt's signature is valid, whatever verdict it carries. I reproduced this with the installed wheel: the run above wrote a receipt with verdict UNCERTAIN, and openadapt-verify verify <receipt> --issuer-key ... --log-key ... printed "valid": true with payload verdict UNCERTAIN, then exited 0. The same happens for REFUTED receipts and for provisional CONFIRMED ones. In a package named openadapt-verify, a script or CI gate that runs openadapt-verify verify r.cbor && proceed will accept a write that was refuted or never confirmed. That is fail-open on the new public surface. The canonical openadapt-flow receipt verify behaves the same way, but its name says it checks a receipt; this one doesn't. Fix: exit nonzero unless the verdict is CONFIRMED and not provisional, with an explicit opt-out flag for signature-only checks. Or rename the subcommand verify-receipt and state in the README's command section that exit 0 means only that the signature is valid. Add a test for each verdict.
  • P2, packages/verify/src/openadapt_verify/cli.py:69-72. run exits 0 on a provisional CONFIRMED result (a recheck is due), and the CLI has no recheck subcommand, so CLI users can't do the canonical late recheck. Return a distinct nonzero code for provisional results, or add openadapt-verify recheck.
  • P3, packages/verify/tests/test_cli_mcp.py:174. duplicate.isError fails on mcp 2.x with AttributeError. The package's own uv.lock pins mcp 2.3.0. CI passes only because locked_install.py lets flow's lock (mcp 1.30) win. Use getattr(duplicate, "is_error", getattr(duplicate, "isError", None)) or pin mcp. The canonical server itself works on 2.3.0.
  • P3, packages/verify/src/openadapt_verify/cli.py:110-112. Without the [mcp] extra, serve ends in an uncaught ModuleNotFoundError traceback. It fails closed, but it should print pip install 'openadapt-verify[mcp]' and exit 2.
  • P3, packages/verify/src/openadapt_verify/__init__.py:8. Importing the package emits a DeprecationWarning from canonical openadapt_flow.verify (through integrations.agents), so python -W error -c "import openadapt_verify" fails. This is a canonical-side fix.
  • P3, the PR title and description. "Standalone ... without the rest of the engine" does not match the dependency tree: the facade depends on all of openadapt-flow[receipts], which installed 33 distributions and about 413 MB of site-packages per your own INSTALL_WEIGHT note. The README is accurate; the PR text should say "facade" rather than "standalone".

@abrichr

abrichr commented Nov 5, 2026

Copy link
Copy Markdown
Member Author

Independent adversarial review: BLOCK

Head reviewed: 8417329aef4c2c7fec3fa985c26ee615a1289349

What I ran

  • Static read of packages/verify/src/openadapt_verify/cli.py and packages/flow/openadapt_flow/cli_receipt.py cmd_verify
  • Confirmed no recheck subcommand and no verdict-based exit on the verify path
  • Did not re-run the full packages/verify suite (author is already fixing this head)

P1: verify exits 0 on any valid signature

cli.py:103-109 calls openadapt_flow.cli_receipt.cmd_verify, which returns 0 whenever the COSE signature verifies (cli_receipt.py:152-163 prints "valid": True and returns 0). The payload verdict (CONFIRMED / REFUTED / UNCERTAIN) and provisional are never mapped to the process exit code. A shell openadapt-verify verify … && proceed therefore proceeds on REFUTED, UNCERTAIN, and provisional receipts.

Required: exit 0 only for a final (non-provisional) CONFIRMED; nonzero otherwise, with distinct codes per class; tests for each verdict.

P1: run treats provisional issued receipts as success

cli.py:69-72 returns 0 when result.verified and result.write_receipt == "issued" and returncode == 0. There is no check that the outcome is final CONFIRMED, and there is no recheck subcommand. Provisional CONFIRMED therefore looks like a finished success to any exit-code consumer.

P2 notes (non-blocking for this BLOCK, still open)

  • serve without the [mcp] extra should fail cleanly rather than traceback
  • PR text says standalone while the package still depends on openadapt-flow; say so in the summary

@abrichr abrichr removed the review-needed Needs an independent review from another vendor's agent label Nov 5, 2026
@abrichr

abrichr commented Nov 5, 2026

Copy link
Copy Markdown
Member Author

Independent adversarial review: BLOCK

Head reviewed: f46db7092ec14768caca8b064c217a2fdf8f3dff. This re-reviews the BLOCK on 8417329a (comment 6651813252).

What I ran

  • uv sync --frozen (Python 3.10, all extras: mcp 2.3.0, openadapt-flow 2.6.0). Ran the full suite through the CPU-slot guard: 38 passed. ruff check: clean. (ruff format --check would reformat one file. That isn't a lint failure.)
  • Built the wheel. Installed openadapt_verify-0.1.0-py3-none-any.whl[mcp] with mcp==2.3.0 in a fresh venv. Ran test_cli_mcp.py and test_cli_verdicts.py against the installed wheel: 21 passed, including the real MCP transport test.
  • Reproduced the fix on my own fixtures, separate from the PR's: a SQLite store, a direct sqlite writer command, freshly pinned issuer and log keys, and a real run then verify for each case:
case outcome run verify
final CONFIRMED CONFIRMED, provisional=false 0 0
no row written UNCERTAIN 3 3
duplicate row REFUTED 4 4
recheck_after_s=3600 CONFIRMED, provisional=true 5 5

recheck results: not due gives 5; wrong --agent-id gives 3 (nothing re-read); late unexplained row gives 4; missing state file gives 2 with no traceback. A provisional receipt still exits 5 from verify after a confirmed recheck, so the signed payload is never upgraded.

Earlier findings

  • P1 verify exit (cli.py:111): fixed. The exit now comes from the signed, schema-strict payload verdict and provisional (check_claims rejects provisional non-CONFIRMED). Tests: test_cli_run_and_signed_verify_require_final_confirmation, test_cli_signature_failure_never_confirms.
  • P2 run provisional and recheck (cli.py:13-16, 80-83, 114-148): fixed. Exit 0 requires final CONFIRMED, an issued receipt and command exit 0. recheck delegates to canonical openadapt_flow.verifier.recheck, which returns CONFIRMED only when frame == held after a fresh database read.
  • P3 items: fixed. MCP works on mcp 2.3.0 from the built wheel. serve without mcp prints the extra and exits 2. A plain import under -W error::DeprecationWarning passes. The PR text is narrowed.

New findings

P2 cli.py:146: a REFUTED recheck can later exit 0, contradicting the ledger hold. Steps to reproduce:

  1. Run a provisional write (recheck_after_s=2).
  2. Insert an unexplained row and run recheck: REFUTED, exit 4. The ledger attempt is re-resolved REFUTED for review.
  3. Delete that row and run recheck again: CONFIRMED, frame: held, exit 0.

The ledger's latest event for that attempt is still resolved/REFUTED. Canonical _mark_rechecked skips attempts whose resolution isn't CONFIRMED, so only the CLI reports success. A caller that retries until openadapt-verify recheck ...; do sleep; done && proceed passes a write that the verifier already sent to a person. That is a new fail-open on the surface this fix added.

Suggested fix: make recheck exit 0 only when the write's ledger attempt is still CONFIRMED (or was just marked rechecked); otherwise exit 4 with a reason. Alternatively, make canonical recheck refuse to confirm after a non-CONFIRMED recheck. That change belongs in openadapt-flow. Add a CLI test for the refute-revert-recheck sequence. An UNCERTAIN recheck followed by a confirmed one, for example after the database was briefly unreadable, should stay allowed.

P3 __init__.py:56-60: accessing openadapt_verify.WriteGuard still emits the openadapt_flow.verify is deprecated warning; it shows up in the pytest warnings summary. Plain import is clean, so this is cosmetic.

P3 cli.py:117: RecheckState.load on a final (non-provisional) write's directory has no recheck.json, so it exits 2 with an OSError message. That is fail-closed, but the message could say the write needs no recheck.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant