Schema-4 gate-battery verification in the release contract (#611 A3) - #663
Schema-4 gate-battery verification in the release contract (#611 A3)#663juaristi22 wants to merge 1 commit into
Conversation
Exact-k UK releases now dispatch on the terminal report's own schema version: 3 keeps the legacy aggregator checker byte-for-byte (and the published June release keeps its grandfathered identity path), 4 runs a new checker for the shared executor's reports, and anything else is refused by name. The schema-4 checker trusts nothing it can recompute or pin: the executor's identity (producer, schema numbers, signature algorithm, the MICROCOSM_UK_TERMINAL_GATE_SIGNING_KEY trust root) and the committed UK spec's digests (policy, manifest, fingerprint, entry membership) are hand-mirrored vintage constants — the data shard still never imports the build shard — shippability is recomputed from the recorded outcomes instead of trusting the shippable flag, the release-candidate posture is required so a staging report can never be promoted, the legacy observable detail checks (including the one-exclusion-clock rule) are re-applied through a projection onto the shared gate names, the diagnostics linkage rides the signed release_evidence slot, and the complete report is authenticated against the canonical payload with the signature slot nulled. Build manifests may carry either evidence vocabulary — legacy stage keys or battery entry ids — never a mixture. Cross-shard sync tests (in the build shard, where tests may import both packages) hold every mirror to the live producer: constants, spec-derived pins, the wrapped input-mass evidence digest, and the canonical-JSON signature scheme; a full producer-to-verifier round trip arms itself the day the consumer PR's release_evidence slot lands. The tamper matrix exercises one named refusal per check over a valid signed fixture. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
vahid-ahmadi
left a comment
There was a problem hiding this comment.
Reviewing as a draft. The framing that makes this PR good is stated in one line — "nothing it can recompute or pin is trusted from the report" — and the implementation actually holds to it: shippable asserted but never load-bearing, gate_outcomes_sha256 recomputed, shippability re-derived from the entries rather than read. Requiring release_candidate to be true so a staging report that excused absent evidence can never be promoted into a release dir is the sharp one; that's the failure a verifier exists to catch.
Keeping the data shard from importing the build shard, and then putting the mirror-drift tests in the build shard where both imports are legal, is the right division. The byte-level check that the data shard's canonical JSON reproduces the producer's signed bytes is the load-bearing one, as you say — everything else is downstream of the two shards agreeing on canonicalisation.
One thing to change, and it's the piece you've identified as most important.
The single cross-PR coordination point is implemented with the one construct that fails silently.
if "release_evidence" in inspect.signature(GateBatteryRun.__init__).parameters:
arguments["release_evidence"] = {...}
else: # pragma: no cover - pre-consumer-flip executors only
pytest.skip(
"GateBatteryRun has no release_evidence slot yet; flip when "
"the consumer PR (uk-battery-consumer-a2) merges."
)A skip is invisible in a green run. If the probe is ever subtly wrong — #662 lands release_evidence through **kwargs, or a decorator wraps __init__ so inspect.signature doesn't report the parameter, or the slot arrives under a different name — this test stays skipped forever and the full producer→verifier round trip, the thing that proves the two halves actually compose, silently never runs. The # pragma: no cover means coverage won't surface it either.
pytest.mark.xfail(strict=True) inverts that: the day the capability appears the test starts passing, strict xfail turns a pass into a failure, and someone must come remove the marker. That converts "remember to flip this" into a build error rather than an intention.
Related, and probably worth reconciling before either merges: #662's body says whichever merges second "flips the round-trip xfail noted there", while this is a skip. Two descriptions of one coordination point suggests it hasn't been pinned down between the PRs — and it's the one place where getting it wrong is invisible rather than loud.
Smaller
- The tamper matrix is the right shape — one named refusal per check over a valid signed fixture, so a check that stops biting fails its own test rather than quietly passing everything. Fifteen named refusals against roughly that many checks is good coverage.
signing_errordeliberately outside the exact attestation key set, so an unsigned report can never verify, is worth the comment it has. That's the kind of exclusion that looks like an oversight to the next reader.- "Build manifests may use either evidence vocabulary — legacy stage keys or battery entry ids — never a mixture" is a good rule. Is the mixture refusal keyed on the manifest, or could a manifest legitimately span a build that straddled the flip? If the latter is possible even in principle, the refusal message should say which vocabulary it saw first.
- Vintage dispatch on the report's own
schema_version, with anything unknown refused by name, reuses #658's pattern well. Since that's now the second consumer of "dispatch on a declared vintage, refuse unknowns", it might deserve a shared helper rather than a second hand-rolled branch.
Nothing here blocks; the xfail change is the one I'd want before merge given what it protects.
The verifier half of microcosm#611's UK consumer flip (A3 of the plan on #611, concurrent with #662): releases whose
terminal_gates.jsonis a schema-4 gate-battery report become verifiable.microcosm-dataonly; no file overlap with the consumer PR, so merge order between them is free.What the dispatch does
Exact-k UK releases dispatch on the report's own
schema_version:3runs the legacy aggregator checker untouched,4runs the new battery checker, anything else is refused by name. The published June release keeps its grandfathered path unchanged (its id is not exact-k, so its report was never checked — preserved exactly). This reuses the vintage-aware pin pattern #658 established.What the schema-4 checker enforces
Nothing it can recompute or pin is trusted from the report:
microcosm.build.gate_battery, schema 4 / attestation 6, theMICROCOSM_UK_TERMINAL_GATE_SIGNING_KEYtrust root (the legacy POPULACE variable stays with the schema-3 path), and vintage pins over the committeduk/gates.json—policy_sha256,gates_manifest_sha256,spec_fingerprint, and the exact 13-entry membership.release_blockingentry must bepassed/not_applicable; theshippableflag is asserted but never load-bearing.release_candidatemust be true — a staging report produced off the candidate posture excused absent evidence and can never be promoted into a release dir.blocked_at_phasemust be null and every declared phase evaluated;unreachedentries are a contradiction and refused.evidence_sha256may carry only the three evidence-bearing entries, each present exactly when its entry evaluated; the input-mass line must equal the canonical wrap of the reviewed enhanced-FRS reference digest (derived pin); when the build manifest carriesterminal_gate_evidenceit must match exactly. Build manifests may use either evidence vocabulary — legacy stage keys or battery entry ids — never a mixture.release_evidence.calibration_diagnostics_sha256(the executor's signed slot) must match the shippedcalibration_diagnostics.jsonbytes — the schema-3 attestation field's successor.signing_erroris deliberately outside it — an unsigned report can never verify), identity fields must equal their body counterparts,gate_outcomes_sha256is recomputed, and the signature is verified over the canonical payload with the signature slot nulled.Keeping the mirrors honest
test_gate_battery_contract_pins.py(build shard, where tests may import both packages) holds every mirror to the live producer: executor constants, spec-derived vintage pins from a freshly constructedGateBatteryRun, entry membership fromload_country_spec("uk"), the wrapped input-mass digest, and — the load-bearing one — a byte-level check that the data shard's canonical JSON reproduces the producer's signed bytes. The full producer→verifier round trip is capability-gated: it skips whileGateBatteryRunlacks therelease_evidenceslot and arms itself the day the consumer PR (#662) merges — the single cross-PR coordination point.The tamper matrix in
test_contract.pyexercises one named refusal per check over a valid signed fixture: unknown schema, non-candidate posture, blocked phase, failed/excused blocking entries, missing entry, moved policy/manifest/fingerprint pins, broken diagnostics link, mixed exclusion clock, unsigned tamper, forged key, absent trust root, recorded signing error, unpinned input-mass evidence, mixed manifest vocabulary. The June fixture and the full schema-3 suite pass unchanged.Delete-or-justify
Nothing is deleted: the schema-3 checker, its hand-mirrored gate-name tuples, and the POPULACE env-var read all stay for vintage dispatch (retirement trigger: when the legacy report format is formally retired, after S3's real-data comparison and the first schema-4 release). The hand-mirrored tuples play no role on the schema-4 path — membership follows the pinned manifest there.
Part of #611. Concurrent with #662 (the consumer flip); an exact-k release assembly requires both.
🤖 Generated with Claude Code