Skip to content

fix(conformance): make the gate actually gate - #14

Open
tomjwxf wants to merge 1 commit into
mainfrom
fix/conformance-gate-actually-gates
Open

fix(conformance): make the gate actually gate#14
tomjwxf wants to merge 1 commit into
mainfrom
fix/conformance-gate-actually-gates

Conversation

@tomjwxf

@tomjwxf tomjwxf commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Addresses findings 1, 3 and 5 from #13, reported by @arian-gogani. He was right on every one I checked, and I verified each against a clean clone before changing anything.

Deliberately does not include the receipt-shape decision from finding 4. More on that below.

What was broken

The gate did not gate. run.sh had no exit after its loop, so the script's status was that of the last echo. CI has been green this whole time while every implementation was reported NON-CONFORMANT. On the current tree it now exits 1:

FAIL: aps-governance-hook run.sh exited 77
FAIL: protect-mcp-adk run.sh exited 77
NON-CONFORMANT: protect-mcp
FAIL: sb-runtime run.sh exited 77
  1 implementation(s) verified, 4 failed
exit=1

That is the real state of the suite, and it should have been visible months ago.

Check 2 could not tell a bad signature from a missing key. It never passed --key, so the verifier exited with no_public_key and this script reported "one or more signatures failed (exit 1 = tampered)". Those are completely different problems. It now passes the published fixture key, overridable via CONFORMANCE_KEY.

Check 3 checked almost nothing. It computed the expected parent digest and then discarded it, accepting any non-empty parent_receipt_hash. Meanwhile expected/chain.jsonl and the fixtures' expected_decision were read by no code anywhere in the repository, despite the README promising the first.

The consequence, which is the part that matters: an implementation could ignore the Cedar policy entirely, emit four correctly signed receipts with arbitrary decisions, and be reported conformant.

What check 3 does now

Compares each receipt against expected/chain.jsonl on tool_name, decision and policy_id. Compares the parent hash against the predecessor digest, accepting a prefix so truncating implementations still pass, which is what the original comment intended before the computed value was dropped on the floor. And cross-checks chain.jsonl against the fixtures' expected_decision, so if those two sources ever disagree the suite says so instead of silently picking one.

Moved to conformance/check_chain.py so it can be read and run on its own.

Demonstrated, not asserted

On a receipt set that allows the destructive rm -rf / fixture the policy forbids:

  OLD check 3 -> PASS (exit 0)
  NEW check 3 -> FAIL (exit 1)
      receipt 2: decision is 'allow', expected 'deny'

And on a set using a constant string for every parent hash:

  OLD -> PASS (exit 0)
  NEW -> FAIL (exit 1)

Also

A .gitignore for receipts/. run.sh writes there, it is untracked, and it can contain node compile caches. I nearly committed several thousand of them while testing this.

Not in this PR

Finding 4, that check 1 and check 2 accept disjoint sets of receipts, is real: the schema requires pubkey and payload.type, while this repository's own reference receipts in aps-gateway-enforcement/ carry kid and a top-level type: decision_receipt, so the schema rejects the receipts we ship.

That needs a decision about which shape the suite accepts rather than a quiet edit inside a fix PR. Answering it in #12 so the second implementation is not blocked on it.

Finding 2, that the reference driver calls protect-mcp evaluate and protect-mcp sign, is also real. There is no sign subcommand in the CLI at all. Separate fix.

Addresses the first, third and fifth findings in #13, reported by @arian-gogani.
Verified each against a clean clone before changing anything.

run.sh had no exit after its loop, so its status was that of the last echo. CI
has been green while every implementation was reported NON-CONFORMANT. It now
tracks failures and returns them, and treats a run that verified nothing as a
failure rather than a pass, since an empty run proves nothing.

Check 2 never passed a key, so the verifier exited with no_public_key and this
script reported it as "one or more signatures failed (exit 1 = tampered)". That
conflates "this receipt was altered" with "I was not told what to trust". It
now passes the published fixture key, overridable via CONFORMANCE_KEY.

Check 3 computed the expected parent digest and discarded it, accepting any
non-empty parent_receipt_hash, and expected/chain.jsonl and the fixtures'
expected_decision were read by no code in the repository. An implementation
could ignore the Cedar policy, emit four correctly signed receipts with
arbitrary decisions, and be reported conformant.

Check 3 now compares each receipt against expected/chain.jsonl on tool_name,
decision and policy_id, compares the parent hash against the predecessor digest
as the original comment intended, and cross-checks chain.jsonl against the
fixtures' expected_decision so the two sources cannot drift apart unnoticed. It
moves to conformance/check_chain.py so it can be read and run on its own.

Demonstrated on a receipt set that allows the destructive Bash fixture the
policy forbids: old check 3 exits 0, new check 3 exits 1. Same for a constant
parent hash. run.sh now exits 1 on the current tree, where it exited 0.

Adds a .gitignore for receipts/, which run.sh writes into and which can contain
node compile caches.

Not included: the receipt-shape decision from finding 4. The schema rejects this
repository's own reference receipts, and which shape wins is a call for the
maintainer rather than something to slip into a fix PR.
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.

2 participants