Skip to content

Schema-4 gate-battery verification in the release contract (#611 A3) - #663

Draft
juaristi22 wants to merge 1 commit into
mainfrom
uk-schema4-verifier-a3
Draft

Schema-4 gate-battery verification in the release contract (#611 A3)#663
juaristi22 wants to merge 1 commit into
mainfrom
uk-schema4-verifier-a3

Conversation

@juaristi22

Copy link
Copy Markdown
Collaborator

The verifier half of microcosm#611's UK consumer flip (A3 of the plan on #611, concurrent with #662): releases whose terminal_gates.json is a schema-4 gate-battery report become verifiable. microcosm-data only; 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: 3 runs the legacy aggregator checker untouched, 4 runs 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:

  • Mirror constants (the data shard still never imports the build shard): producer microcosm.build.gate_battery, schema 4 / attestation 6, the MICROCOSM_UK_TERMINAL_GATE_SIGNING_KEY trust root (the legacy POPULACE variable stays with the schema-3 path), and vintage pins over the committed uk/gates.jsonpolicy_sha256, gates_manifest_sha256, spec_fingerprint, and the exact 13-entry membership.
  • Recomputed shippability: every release_blocking entry must be passed/not_applicable; the shippable flag is asserted but never load-bearing. release_candidate must be true — a staging report produced off the candidate posture excused absent evidence and can never be promoted into a release dir. blocked_at_phase must be null and every declared phase evaluated; unreached entries are a contradiction and refused.
  • Observables, reused not rewritten: the battery re-keys the report by spec entry id but runs the same gate implementations, so the evaluated entries are projected back onto the legacy names and the existing detail-schema checks — including source_year reviewed exclusion + schema-2 approval receipts with expiry (#630, #610) #658's one-exclusion-clock rule — apply verbatim.
  • Evidence discipline: evidence_sha256 may 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 carries terminal_gate_evidence it must match exactly. Build manifests may use either evidence vocabulary — legacy stage keys or battery entry ids — never a mixture.
  • Diagnostics linkage: release_evidence.calibration_diagnostics_sha256 (the executor's signed slot) must match the shipped calibration_diagnostics.json bytes — the schema-3 attestation field's successor.
  • Authentication: attestation key set is exact (signing_error is deliberately outside it — an unsigned report can never verify), identity fields must equal their body counterparts, gate_outcomes_sha256 is 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 constructed GateBatteryRun, entry membership from load_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 while GateBatteryRun lacks the release_evidence slot and arms itself the day the consumer PR (#662) merges — the single cross-PR coordination point.

The tamper matrix in test_contract.py exercises 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

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 vahid-ahmadi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_error deliberately 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.

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