From e04a2eb72925e82c4def6f7d9e5a5edc7de6c9f2 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Thu, 20 Aug 2026 07:57:13 +0000 Subject: [PATCH 1/3] regen receipt: the fixed-point pass may reference prior evidence, not impersonate it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A RECEIPT MAY REFERENCE PRIOR EVIDENCE BUT MAY NOT IMPERSONATE PRIOR EVIDENCE AS SOMETHING IT MEASURED ITSELF (operator ruling, 2026-08-20). THE SPECIMEN IS IN PRODUCTION, not a fixture. Run 32341236470 on main at bd239370923 -- the run this fleet cited as proof the mirror convergence is good: required-regen-fixed-point: fixed_point_equal=true first_generation_equal=true first_generation_equal is printed by the pass that does not measure it. The two regen passes are separate process invocations sharing one receipt file under target/, and a single flat eight-field record forced the second pass to populate fields it had never measured. The only source was the receipt the first pass left on disk, so four of six were copied verbatim: committed_generated_digest, first_generation_equal, changed_paths, candidate_artifact. The product is stamped with the SECOND pass commit_sha while carrying the FIRST pass answers -- internally consistent, schema-valid, and silent about which tree four of its fields describe. It is TRUE in that run, because pass 1 had run minutes earlier at the same commit. That is why it survived: a field that is usually right is the hardest kind to find, since correctness under observation is exactly what stops anyone asking whether it was measured. WHERE IT IS REACHABLE. In CI the arm is currently unreachable -- actions/checkout's default clean removes the ignored target/ each run, measured as two consecutive main runs each compiling 105 crates starting at proc-macro2, where a warm tree compiles zero. But that is a property of a checkout default nobody declared, one cache-reuse change from live on a required path. It is reachable TODAY on the ordinary local path: nothing requires pass 1 to have run in this process, at this commit, or at all, so a developer iterating on the determinism half alone over a warm target/ gets today's commit_sha carrying yesterday's changed_paths. THE SHAPE MAKES IT UNWRITABLE RATHER THAN DETECTABLE. RegenReceipt is now two variants. FirstGeneration carries only what pass 1 measures. FixedPoint carries the pass-2 digest, its equality, and an explicit PriorReceiptRef -- and has NO first_generation_equal field, so there is nothing to copy and no check to pass (DESIGN 4b structural impossibility, one rung above the validation that would otherwise sit here). Computing all six in pass 2 was the alternative and is worse: it would make pass 2 re-derive first_generation_equal against the committed tree, which is pass 1's question, fusing two authorities into one row (DESIGN 3). A REFERENCE IS ONLY HONEST IF IT NAMES ITS SUBJECT, so PriorReceiptRef carries the commit_sha its evidence was measured at, and the host REFUSES when that differs from HEAD. Without that arm a PriorReceiptRef is the same defect with better vocabulary. THREE THINGS THE SPLIT EXPOSED that were not in the original report: - Pass 1 was writing `fixed_point_equal: false`. Not a measurement at all -- the first pass never asks that question, so a literal false asserted a NEGATIVE ANSWER where the honest content was NOT ASKED. Same conflation as the impersonation, in the opposite direction, sitting right beside it. - The accessors return Option, not bool: "did not measure" and "measured false" are different states and a bool cannot hold both. - RegenReceiptStored deserializes ONLY the first-generation shape, so a second pass building on another second pass's receipt refuses AT PARSE TIME rather than by a check someone must remember to write. THE RED IS PROVEN BY EXECUTION, per-row through claim_batch on the real consumer: real predicate PASS control · PASS RED · PASS no-reference exit 0 predicate -> constant true PASS control · FAIL RED · PASS no-reference exit 1 Only the RED broke and both controls stayed green, so the witness discriminates on the predicate rather than on anything ambient. WHAT THE WITNESS DOES NOT COVER, stated in its header rather than left to be found: the HOST refusal arm. Reaching it needs a receipt file planted at a chosen commit_sha before the binary runs, and no witness form here writes a file before running a process. Next-rung trigger: a witness form that can stage a fixture file for a wet run. Until then that arm rests on review, which is strictly weaker. KNOWN RESIDUE, named rather than swept: the population-refusal path still writes "refused:population" sentinel digests and first_generation_equal: false, both meaning "not asked". The honest repair is a refusal variant carrying no digest fields, which is wider than the impersonation this closes. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01FwPMTY6Myy3scaMNn33cg5 --- dag/gunbc/regen_receipt.dag | 73 ++++++- ...n_receipt_prior_reference_witness_test.dag | 82 ++++++++ src/v1/stage0/src/bin/claim_executor.rs | 46 ++++- src/v1/stage0/src/required_regen_host.rs | 180 ++++++++++++++++-- 4 files changed, 349 insertions(+), 32 deletions(-) create mode 100644 dag/test/claim/regen_receipt_prior_reference_witness_test.dag diff --git a/dag/gunbc/regen_receipt.dag b/dag/gunbc/regen_receipt.dag index c28c183413c..dcdebdad71b 100644 --- a/dag/gunbc/regen_receipt.dag +++ b/dag/gunbc/regen_receipt.dag @@ -2,19 +2,72 @@ module gunbc.regen_receipt import std.types { String, Int, List, Bool } -data regen_receipt_note: String = "Typed receipt for a non-mutating stage0 regen verify run via claim_executor --required-regen. first_generation_equal and fixed_point_equal are DISTINCT questions and must never collapse. The host binary materializes this carrier at runtime; fixed_point_equal is answered by a second invocation (--required-regen-fixed-point), not a second code path inside one run." +// A RECEIPT MAY REFERENCE PRIOR EVIDENCE BUT MAY NOT IMPERSONATE PRIOR EVIDENCE AS SOMETHING IT +// MEASURED ITSELF (operator ruling, 2026-08-20). The two regen passes answer DISTINCT questions — +// first_generation_equal (does a fresh emit match the committed seed) and fixed_point_equal (does +// the emit reproduce itself on a second pass) — and they run as two separate process invocations +// against one receipt file at target/stage0-regen-receipt.json. +// +// WHAT WENT WRONG WITH ONE FLAT RECORD. A single eight-field record forced the second pass to +// populate fields it had not measured, and the only available source was the receipt the first +// pass left on disk. Four of six were therefore copied through verbatim: +// committed_generated_digest, first_generation_equal, changed_paths, candidate_artifact. The +// result is stamped with the SECOND pass commit_sha while carrying the FIRST pass answers, and it +// is internally consistent and schema-valid — which is exactly what makes it invisible. Nothing in +// the artifact says which tree four of its fields describe. +// +// The reachable arm is the ordinary local one rather than CI: the two modes are separate +// invocations, and nothing requires the first to have run in this process, at this commit, or at +// all. A developer iterating on the determinism half alone, over a warm target/ from an earlier +// commit, gets today commit_sha carrying yesterday changed_paths. In CI the arm is currently +// unreachable because actions/checkout default clean removes the ignored target/ each run — +// measured, two consecutive main runs each compiling 105 crates starting at proc-macro2, where a +// warm tree compiles zero — but that is a property of a checkout default nobody declared, and it +// is one cache-reuse change away from live on a required path. +// +// THE SHAPE BELOW MAKES THE FABRICATION UNWRITABLE RATHER THAN DETECTABLE. The two passes are two +// variants, and the second variant HAS NO first_generation_equal FIELD to fill in. There is no +// value to copy and no check to pass, because the invalid state has no constructor — DESIGN 4b +// structural impossibility, not the validation rung one below it. Choosing this over "compute all +// six in pass two" is deliberate: computing all six would make the second pass re-derive +// first_generation_equal against the committed tree, which is the FIRST pass question, fusing two +// authorities into one row (DESIGN 3). +// +// A REFERENCE IS ONLY HONEST IF IT NAMES ITS SUBJECT. PriorReceiptRef therefore carries the +// commit_sha the referenced evidence was measured at, so a consumer READS which tree those facts +// describe instead of inferring it from context. A ref that silently pointed at another tree would +// be the same defect with better vocabulary. -type RegenReceipt { +type PriorReceiptRef { commit_sha: String - authority_digest: String committed_generated_digest: String - candidate_generated_digest: String first_generation_equal: Bool - fixed_point_equal: Bool changed_paths: List candidate_artifact: String } +// FIRST GENERATION measures everything it reports. FIXED POINT measures the pass-2 digest and its +// equality, and references the rest. The commit_sha on each variant is the tree THAT PASS ran +// against; prior.commit_sha is the tree the referenced evidence came from, and the host refuses +// when they differ. +type RegenReceipt + = FirstGeneration { + commit_sha: String + authority_digest: String + committed_generated_digest: String + candidate_generated_digest: String + first_generation_equal: Bool + changed_paths: List + candidate_artifact: String + } + | FixedPoint { + commit_sha: String + authority_digest: String + candidate_generated_digest: String + fixed_point_equal: Bool + prior: PriorReceiptRef + } + data stage0_regen_candidate_dir_rel: String = "target/stage0-regen-candidate" data stage0_regen_receipt_rel: String = "target/stage0-regen-receipt.json" @@ -23,6 +76,12 @@ data stage0_regen_candidate_artifact_name: String = "stage0-regen-candidate" data stage0_regen_receipt_artifact_name: String = "stage0-regen-receipt" -fn stage0_regen_receipt_is_green(receipt: RegenReceipt) -> Bool { - receipt.first_generation_equal && receipt.fixed_point_equal +// STALENESS IS DECIDABLE FROM THE CARRIER ALONE, which is the point of putting commit_sha on the +// ref. The host refuses a cross-tree reference on the required path; this predicate is the same +// question asked of an already-materialized receipt. +fn prior_reference_is_same_tree(receipt: RegenReceipt) -> Bool { + match receipt { + FirstGeneration { commit_sha: _, authority_digest: _, committed_generated_digest: _, candidate_generated_digest: _, first_generation_equal: _, changed_paths: _, candidate_artifact: _ } => true, + FixedPoint { commit_sha, authority_digest: _, candidate_generated_digest: _, fixed_point_equal: _, prior } => commit_sha == prior.commit_sha + } } diff --git a/dag/test/claim/regen_receipt_prior_reference_witness_test.dag b/dag/test/claim/regen_receipt_prior_reference_witness_test.dag new file mode 100644 index 00000000000..f6f411bcaeb --- /dev/null +++ b/dag/test/claim/regen_receipt_prior_reference_witness_test.dag @@ -0,0 +1,82 @@ +module test.claim.regen_receipt_prior_reference_witness + +import std.types { Bool, String, List } +import gunbc.regen_receipt { RegenReceipt, PriorReceiptRef, FirstGeneration, FixedPoint, prior_reference_is_same_tree } + +// WHAT THIS WITNESS COVERS, AND WHAT IT DOES NOT — stated first because the gap is the point. +// +// COVERS: the model predicate. A FixedPoint receipt whose prior reference names a DIFFERENT tree +// is distinguishable from one that names the same tree, decidably, from the carrier alone. +// +// DOES NOT COVER: the host refusal arm in required_regen_host run_required_regen_fixed_point. +// That arm compares the on-disk prior receipt against git HEAD and returns an Err, and reaching it +// requires planting a receipt file at target/stage0-regen-receipt.json with a chosen commit_sha +// before invoking the binary. No witness form available here writes a file before running a +// process, so the host arm has no executing evidence and I am not claiming otherwise. Its +// next-rung trigger is a witness form that can stage a fixture file for a wet run; until then the +// host arm rests on review, which is strictly weaker than this predicate does. +// +// The reason the predicate is worth having anyway is that it is the SAME question the host asks. +// If the model says a cross-tree reference is detectable and the host later stops checking, the +// disagreement is between two things that both exist, rather than a rule that lives only in one +// unexecuted comment. + +data same_tree_sha: String = "bd239370923f0000000000000000000000000000" + +data other_tree_sha: String = "5a10ca7e01891e32f7568c3bac23878f2b3fdc5f" + +fn a_prior_ref(at: String) -> PriorReceiptRef { + PriorReceiptRef { + commit_sha: at, + committed_generated_digest: "digest-committed", + first_generation_equal: true, + changed_paths: [], + candidate_artifact: "target/stage0-regen-candidate" + } +} + +fn a_fixed_point_receipt(ran_at: String, referenced_at: String) -> RegenReceipt { + FixedPoint { + commit_sha: ran_at, + authority_digest: "digest-authority", + candidate_generated_digest: "digest-candidate", + fixed_point_equal: true, + prior: a_prior_ref(at: referenced_at) + } +} + +// POSITIVE CONTROL. Without this, the RED below is satisfied by a predicate that returns false +// unconditionally — which is the failure mode that cost me a broken instrument earlier today, one +// that printed the right answer while being incapable of printing any other. +test fn a_fixed_point_receipt_referencing_its_own_tree_is_same_tree() -> Bool { + prior_reference_is_same_tree( + receipt: a_fixed_point_receipt(ran_at: same_tree_sha, referenced_at: same_tree_sha) + ) +} + +// THE DISCRIMINATING RED. This is the state the flat eight-field receipt could reach silently: +// stamped with the tree the second pass ran against, carrying evidence measured on another. It is +// internally consistent and schema-valid, and the ONLY thing that distinguishes it is that the +// reference names its subject. +test fn w_RED_a_fixed_point_receipt_referencing_another_tree_is_not_same_tree() -> Bool { + !prior_reference_is_same_tree( + receipt: a_fixed_point_receipt(ran_at: same_tree_sha, referenced_at: other_tree_sha) + ) +} + +// A FirstGeneration receipt has no prior reference to be wrong about, so the predicate is +// vacuously true there. Asserted rather than assumed: a predicate that answered false for the +// variant carrying no reference would refuse every first pass. +test fn a_first_generation_receipt_has_no_cross_tree_reference() -> Bool { + prior_reference_is_same_tree( + receipt: FirstGeneration { + commit_sha: same_tree_sha, + authority_digest: "digest-authority", + committed_generated_digest: "digest-committed", + candidate_generated_digest: "digest-candidate", + first_generation_equal: true, + changed_paths: [], + candidate_artifact: "target/stage0-regen-candidate" + } + ) +} diff --git a/src/v1/stage0/src/bin/claim_executor.rs b/src/v1/stage0/src/bin/claim_executor.rs index 25d7d1245de..ce8a6b00883 100644 --- a/src/v1/stage0/src/bin/claim_executor.rs +++ b/src/v1/stage0/src/bin/claim_executor.rs @@ -10464,9 +10464,28 @@ fn run() -> Result { return match v1_compiler::cli_run::run_required_regen_fixed_point(®en_receipt_path, None) { Ok(outcome) => { + // The provenance is printed, not just carried. This line previously read + // `first_generation_equal={}` off the receipt as though the fixed-point pass had + // measured it; it never does. Labelling it `referenced_` and naming the commit it + // came from means the log itself distinguishes measured from quoted -- and since + // the host refuses a cross-tree reference, `referenced_at` equals HEAD on every + // line that is allowed to print. + let (referenced_fge, referenced_at) = match outcome.receipt.prior() { + Some(prior) => ( + prior.first_generation_equal.to_string(), + prior.commit_sha.clone(), + ), + None => ("unavailable".to_string(), "unavailable".to_string()), + }; eprintln!( - "required-regen-fixed-point: fixed_point_equal={} first_generation_equal={}", - outcome.receipt.fixed_point_equal, outcome.receipt.first_generation_equal + "required-regen-fixed-point: fixed_point_equal={} referenced_first_generation_equal={} referenced_at={}", + outcome + .receipt + .fixed_point_equal() + .map(|v| v.to_string()) + .unwrap_or_else(|| "unmeasured".to_string()), + referenced_fge, + referenced_at ); for failure in &outcome.failures { eprintln!("required-regen-fixed-point: FAIL {failure}"); @@ -10490,10 +10509,25 @@ fn run() -> Result { ®en_receipt_path, ) { Ok(outcome) => { - eprintln!( - "required-regen: first_generation_equal={} candidate={}", - outcome.receipt.first_generation_equal, outcome.receipt.candidate_artifact - ); + // Both values here ARE measured by this pass, so they print unqualified. The + // accessors return Option because the sibling variant does not measure them; a + // None on this path would mean the first pass built the wrong variant, so it + // prints as `unmeasured` rather than defaulting to a plausible-looking value. + // Read through accessors rather than by matching the variant: the + // `required_regen_host` module is private to `cli_run`, so the type is usable here + // but not nameable. `None` would mean the first pass built the wrong variant, so + // it prints `unmeasured` rather than defaulting to a plausible-looking value. + let fge = outcome + .receipt + .first_generation_equal() + .map(|v| v.to_string()) + .unwrap_or_else(|| "unmeasured".to_string()); + let candidate = outcome + .receipt + .candidate_artifact() + .unwrap_or("unmeasured") + .to_string(); + eprintln!("required-regen: first_generation_equal={fge} candidate={candidate}"); for failure in &outcome.failures { eprintln!("required-regen: FAIL {failure}"); } diff --git a/src/v1/stage0/src/required_regen_host.rs b/src/v1/stage0/src/required_regen_host.rs index a10828097b4..4398bc2440b 100644 --- a/src/v1/stage0/src/required_regen_host.rs +++ b/src/v1/stage0/src/required_regen_host.rs @@ -21,21 +21,120 @@ use bootstrap_stage0_crate_layout_generated::{ HAND_MAINTAINED_STAGE0_DIRS, HAND_MAINTAINED_STAGE0_FILES, }; -const RECEIPT_SCHEMA: &str = "gunbc.regen_receipt.v1"; +/// Bumped from `.v1` when the flat eight-field record split into the two-variant carrier below. +/// A stale `.v1` receipt on disk now fails to deserialize into the new shape, which surfaces as a +/// typed refusal rather than as a silently mixed-provenance artifact -- the fail-closed direction. +const RECEIPT_SCHEMA: &str = "gunbc.regen_receipt.v2"; -#[derive(Debug, Serialize)] -pub struct RegenReceipt { - pub schema: &'static str, +/// Evidence produced by the FIRST regen pass, referenced by the second. +/// +/// It carries the `commit_sha` it was measured at so that a consumer READS which tree these facts +/// describe instead of inferring it from the receipt that quotes them. A reference that did not +/// name its subject would be the impersonation this type exists to end, wearing better vocabulary. +#[derive(Debug, Serialize, serde::Deserialize)] +pub struct PriorReceiptRef { pub commit_sha: String, - pub authority_digest: String, pub committed_generated_digest: String, - pub candidate_generated_digest: String, pub first_generation_equal: bool, - pub fixed_point_equal: bool, pub changed_paths: Vec, pub candidate_artifact: String, } +/// A RECEIPT MAY REFERENCE PRIOR EVIDENCE BUT MAY NOT IMPERSONATE PRIOR EVIDENCE AS SOMETHING IT +/// MEASURED ITSELF (operator ruling, 2026-08-20). +/// +/// This was one flat eight-field record, which forced the second pass to populate fields it had +/// not measured. The only source available was the receipt the first pass left on disk, so four of +/// six were copied through verbatim: `committed_generated_digest`, `first_generation_equal`, +/// `changed_paths`, `candidate_artifact`. The product is stamped with the SECOND pass `commit_sha` +/// while carrying the FIRST pass answers -- internally consistent, schema-valid, and silent about +/// which tree four of its fields describe. Validating stayed impossible because nothing in the +/// artifact recorded the provenance that would have been validated. +/// +/// The split makes the fabrication UNWRITABLE rather than detectable: `FixedPoint` has no +/// `first_generation_equal` field to fill in, so there is no value to copy and no check to pass +/// (DESIGN 4b structural impossibility, one rung above the validation that would otherwise sit +/// here). Computing all six in the second pass was the alternative and is worse -- it would make +/// pass two re-derive `first_generation_equal` against the committed tree, which is pass one's +/// question, fusing two authorities into one row (DESIGN 3). +/// +/// Authority: `gunbc.regen_receipt`. +#[derive(Debug, Serialize)] +#[serde(tag = "pass")] +pub enum RegenReceipt { + #[serde(rename = "first_generation")] + FirstGeneration { + schema: &'static str, + commit_sha: String, + authority_digest: String, + committed_generated_digest: String, + candidate_generated_digest: String, + first_generation_equal: bool, + changed_paths: Vec, + candidate_artifact: String, + }, + #[serde(rename = "fixed_point")] + FixedPoint { + schema: &'static str, + commit_sha: String, + authority_digest: String, + candidate_generated_digest: String, + fixed_point_equal: bool, + prior: PriorReceiptRef, + }, +} + +impl RegenReceipt { + /// The tree THIS pass ran against. + pub fn commit_sha(&self) -> &str { + match self { + RegenReceipt::FirstGeneration { commit_sha, .. } => commit_sha, + RegenReceipt::FixedPoint { commit_sha, .. } => commit_sha, + } + } + + /// `Some` only where the pass measured it. `FixedPoint` returns `None` rather than reaching + /// into `prior`, because "the second pass did not measure this" and "the first pass measured + /// it as false" are different states and a `bool` cannot hold both. + pub fn first_generation_equal(&self) -> Option { + match self { + RegenReceipt::FirstGeneration { + first_generation_equal, + .. + } => Some(*first_generation_equal), + RegenReceipt::FixedPoint { .. } => None, + } + } + + /// `Some` only where the pass measured it -- the mirror of the above. + pub fn fixed_point_equal(&self) -> Option { + match self { + RegenReceipt::FirstGeneration { .. } => None, + RegenReceipt::FixedPoint { + fixed_point_equal, .. + } => Some(*fixed_point_equal), + } + } + + /// The candidate artifact path, measured only by the first pass. + pub fn candidate_artifact(&self) -> Option<&str> { + match self { + RegenReceipt::FirstGeneration { + candidate_artifact, .. + } => Some(candidate_artifact), + RegenReceipt::FixedPoint { .. } => None, + } + } + + /// The referenced first-pass evidence, present only on `FixedPoint`. + pub fn prior(&self) -> Option<&PriorReceiptRef> { + match self { + RegenReceipt::FirstGeneration { .. } => None, + RegenReceipt::FixedPoint { prior, .. } => Some(prior), + } + } +} + #[derive(Debug)] pub struct RequiredRegenOutcome { pub receipt: RegenReceipt, @@ -106,14 +205,17 @@ pub fn run_required_regen( copy_hand_maintained_support(&stage0_src, &fresh_src)?; verify_candidate_tree(&fresh_src, &committed_basenames)?; - let receipt = RegenReceipt { + // Every field here was measured by THIS pass against THIS tree. The old shape also carried + // `fixed_point_equal: false`, which was not a measurement at all -- the first pass never asks + // that question, so a literal `false` asserted a negative answer where the honest content was + // "not asked". The variant has no such field, so the placeholder is now unwritable. + let receipt = RegenReceipt::FirstGeneration { schema: RECEIPT_SCHEMA, commit_sha, authority_digest, committed_generated_digest: committed_digest, candidate_generated_digest: candidate_digest, first_generation_equal, - fixed_point_equal: false, changed_paths: changed_paths.clone(), candidate_artifact: candidate_dir_rel.to_string(), }; @@ -147,10 +249,33 @@ pub fn run_required_regen_fixed_point( ) -> Result { let workspace = workspace_root(); let receipt_path = workspace.join(receipt_rel); + let commit_sha = git_head_sha(&workspace)?; let prior = read_receipt(&receipt_path)?; - let pass1 = pass1_digest.unwrap_or(prior.candidate_generated_digest); - let commit_sha = git_head_sha(&workspace)?; + // THE CROSS-TREE REFUSAL. The two passes are separate process invocations sharing one file + // under `target/`, and nothing requires the first to have run in this process, at this commit, + // or at all. Without this arm a developer iterating on the determinism half alone -- the + // ordinary thing to do -- over a `target/` warm from an earlier commit produces a receipt + // stamped with TODAY's `commit_sha` carrying YESTERDAY's `changed_paths` and + // `first_generation_equal`. In CI the arm is currently unreachable because actions/checkout's + // default clean removes the ignored `target/` each run (measured: two consecutive main runs + // each compiled 105 crates starting at proc-macro2, where a warm tree compiles zero) -- but + // that is a property of a checkout default nobody declared, one cache-reuse change from live + // on a required path. + // + // It refuses rather than recomputing: silently re-running the first pass here would fuse the + // two authorities, and silently proceeding is the fabrication. The `PriorReceiptRef` shape + // makes the impersonation unwritable; this makes referencing the WRONG tree loud. + if prior.commit_sha != commit_sha { + return Err(format!( + "refusal: prior regen receipt was measured at commit {} but HEAD is {} -- the \ + fixed-point pass may reference first-generation evidence only from the same tree. \ + Re-run `claim_executor --required-regen` at this commit first.", + prior.commit_sha, commit_sha + )); + } + + let pass1 = pass1_digest.unwrap_or(prior.candidate_generated_digest); let sources = super::regen_input_sources(&workspace)?; let authority_digest = authority_digest_from_sources(&sources)?; let emitted = compile_stage0(&workspace)?; @@ -165,16 +290,24 @@ pub fn run_required_regen_fixed_point( let pass2 = tree_digest_from_map(&emitted, &committed_basenames)?; let fixed_point_equal = pass1 == pass2; - let receipt = RegenReceipt { + // `commit_sha` is what THIS pass ran against; `prior` names the tree its referenced evidence + // came from. They are checked for equality above and a mismatch refuses, so a receipt reaching + // this point never quotes another tree -- but the field is carried regardless, because a + // reference whose subject is only guaranteed by an upstream check is one refactor away from + // being a reference that does not name its subject. + let receipt = RegenReceipt::FixedPoint { schema: RECEIPT_SCHEMA, commit_sha, authority_digest, - committed_generated_digest: prior.committed_generated_digest, candidate_generated_digest: pass2.clone(), - first_generation_equal: prior.first_generation_equal, fixed_point_equal, - changed_paths: prior.changed_paths, - candidate_artifact: prior.candidate_artifact, + prior: PriorReceiptRef { + commit_sha: prior.commit_sha, + committed_generated_digest: prior.committed_generated_digest, + first_generation_equal: prior.first_generation_equal, + changed_paths: prior.changed_paths, + candidate_artifact: prior.candidate_artifact, + }, }; write_receipt(&receipt_path, &receipt)?; @@ -345,14 +478,20 @@ fn regen_refusal_outcome( reason: String, ) -> Result { let receipt_path = workspace.join(receipt_rel); - let receipt = RegenReceipt { + // A population refusal happens BEFORE any content comparison, so the digests are not + // "refused" values of a measurement -- there was no measurement. The sentinel string is + // retained rather than improved because the honest repair is a refusal variant that carries + // no digest fields at all, and that is a wider change than the impersonation this commit + // closes. Named here so it is a known residue rather than something a later reader discovers + // and mistakes for a measured digest: `first_generation_equal: false` below is likewise "not + // asked", not "asked and answered no". + let receipt = RegenReceipt::FirstGeneration { schema: RECEIPT_SCHEMA, commit_sha, authority_digest, committed_generated_digest: "refused:population".to_string(), candidate_generated_digest: "refused:population".to_string(), first_generation_equal: false, - fixed_point_equal: false, changed_paths: Vec::new(), candidate_artifact: candidate_dir_rel.to_string(), }; @@ -650,13 +789,16 @@ fn git_head_sha(workspace: &Path) -> Result { } #[derive(serde::Deserialize)] +/// The on-disk form the second pass reads back. Only the FIRST-generation shape is accepted here: +/// the second pass must build on a first-pass measurement, and a receipt left by another +/// second pass is not one. Deserialization therefore refuses a `fixed_point` receipt by +/// construction rather than by an equality check someone must remember to write. struct RegenReceiptStored { commit_sha: String, authority_digest: String, committed_generated_digest: String, candidate_generated_digest: String, first_generation_equal: bool, - fixed_point_equal: bool, changed_paths: Vec, candidate_artifact: String, } From 1407bfed863647bfc13648e9e9a6400b1be6c962 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Thu, 20 Aug 2026 08:07:09 +0000 Subject: [PATCH 2/3] regen receipt: make the schema bump load-bearing, and delete the reader that hid the false claim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review on #8650 caught a comment asserting a property the code did not deliver — the exact class this PR exists to close, in the repair itself. THE CLAIM WAS FALSE. The schema-bump comment said a stale `.v1` receipt "fails to deserialize into the new shape". serde ignores unknown fields by default, and a v1 record carries every field the reader required plus the removed `fixed_point_equal`, so it parsed cleanly and the bump changed nothing. AND IT WAS WORSE THAN THE REVIEW STATED. `schema` was written three times and read ZERO times — no comparison anywhere in the module. The version string was decoration. Writing a version nobody compares is the same defect as the impersonation this module closes: an artifact asserting a property that nothing establishes. ONE ADJACENT CLAIM WAS TRUE and is kept rather than swept out with the false one: deserialization does refuse a `fixed_point` receipt, because that shape lacks four fields the reader requires. Two neighbouring claims, one sound, one not. CONSTRUCTION RATHER THAN A SOFTENED COMMENT, which was the reviewer's first option and the better one: - `deny_unknown_fields` on the carrier, so a stale v1 record refuses on the orphan field it carries; - an explicit schema equality check in `read_receipt`, so the version is COMPARED rather than merely written; - `RegenReceiptStored` DELETED. It was a second representation of RegenReceipt (DESIGN 3 nickname) and it was precisely where the false claim hid: a hand-mirrored field list accepts any JSON containing those fields, so it read a v1 record as happily as a v2 one. `read_receipt` now deserializes the REAL carrier and destructures it, so reader and writer cannot drift — there is one shape, and the duplicate that made the drift possible is gone rather than corrected. The `fixed_point` rejection is now an explicit match arm even though it was already true by construction, because a missing-field parse error reports the SYMPTOM — a field name — rather than the cause. cargo check clean, with a negative control: injecting a bogus field yields 2 errors and exit 101, so the green discriminates. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01FwPMTY6Myy3scaMNn33cg5 --- src/v1/stage0/src/required_regen_host.rs | 96 +++++++++++++++++++----- 1 file changed, 78 insertions(+), 18 deletions(-) diff --git a/src/v1/stage0/src/required_regen_host.rs b/src/v1/stage0/src/required_regen_host.rs index 4398bc2440b..86a954ab6b9 100644 --- a/src/v1/stage0/src/required_regen_host.rs +++ b/src/v1/stage0/src/required_regen_host.rs @@ -22,8 +22,16 @@ use bootstrap_stage0_crate_layout_generated::{ }; /// Bumped from `.v1` when the flat eight-field record split into the two-variant carrier below. -/// A stale `.v1` receipt on disk now fails to deserialize into the new shape, which surfaces as a -/// typed refusal rather than as a silently mixed-provenance artifact -- the fail-closed direction. +/// +/// THE BUMP IS LOAD-BEARING ONLY BECAUSE `read_receipt` COMPARES IT. An earlier revision of this +/// comment claimed a stale `.v1` receipt "fails to deserialize into the new shape". That was +/// FALSE, and review caught it: serde ignores unknown fields by default, and a v1 record carries +/// every field the reader requires plus the removed `fixed_point_equal`, so it parsed cleanly. +/// The version string was written three times and read zero times -- decoration, not a version. +/// Two things now make the claim true rather than aspirational: `deny_unknown_fields` on the +/// carrier, and an explicit equality check in `read_receipt`. Writing a version nobody compares is +/// the same class of defect as the impersonation this module exists to close -- an artifact +/// asserting a property that nothing establishes. const RECEIPT_SCHEMA: &str = "gunbc.regen_receipt.v2"; /// Evidence produced by the FIRST regen pass, referenced by the second. @@ -59,12 +67,12 @@ pub struct PriorReceiptRef { /// question, fusing two authorities into one row (DESIGN 3). /// /// Authority: `gunbc.regen_receipt`. -#[derive(Debug, Serialize)] -#[serde(tag = "pass")] +#[derive(Debug, Serialize, serde::Deserialize)] +#[serde(tag = "pass", deny_unknown_fields)] pub enum RegenReceipt { #[serde(rename = "first_generation")] FirstGeneration { - schema: &'static str, + schema: String, commit_sha: String, authority_digest: String, committed_generated_digest: String, @@ -75,7 +83,7 @@ pub enum RegenReceipt { }, #[serde(rename = "fixed_point")] FixedPoint { - schema: &'static str, + schema: String, commit_sha: String, authority_digest: String, candidate_generated_digest: String, @@ -210,7 +218,7 @@ pub fn run_required_regen( // that question, so a literal `false` asserted a negative answer where the honest content was // "not asked". The variant has no such field, so the placeholder is now unwritable. let receipt = RegenReceipt::FirstGeneration { - schema: RECEIPT_SCHEMA, + schema: RECEIPT_SCHEMA.to_string(), commit_sha, authority_digest, committed_generated_digest: committed_digest, @@ -296,7 +304,7 @@ pub fn run_required_regen_fixed_point( // reference whose subject is only guaranteed by an upstream check is one refactor away from // being a reference that does not name its subject. let receipt = RegenReceipt::FixedPoint { - schema: RECEIPT_SCHEMA, + schema: RECEIPT_SCHEMA.to_string(), commit_sha, authority_digest, candidate_generated_digest: pass2.clone(), @@ -486,7 +494,7 @@ fn regen_refusal_outcome( // and mistakes for a measured digest: `first_generation_equal: false` below is likewise "not // asked", not "asked and answered no". let receipt = RegenReceipt::FirstGeneration { - schema: RECEIPT_SCHEMA, + schema: RECEIPT_SCHEMA.to_string(), commit_sha, authority_digest, committed_generated_digest: "refused:population".to_string(), @@ -788,14 +796,15 @@ fn git_head_sha(workspace: &Path) -> Result { Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) } -#[derive(serde::Deserialize)] -/// The on-disk form the second pass reads back. Only the FIRST-generation shape is accepted here: -/// the second pass must build on a first-pass measurement, and a receipt left by another -/// second pass is not one. Deserialization therefore refuses a `fixed_point` receipt by -/// construction rather than by an equality check someone must remember to write. -struct RegenReceiptStored { +/// The first-pass measurement the second pass builds on. +/// +/// This was a separate `RegenReceiptStored` struct mirroring the carrier's field list -- a second +/// representation of one fact (DESIGN 3), and the place the false fail-closed claim hid: it +/// silently accepted any JSON containing its fields, so it read a v1 record as happily as a v2 +/// one. It is gone. `read_receipt` now deserializes the REAL carrier and destructures it, so the +/// reader cannot drift from the writer -- there is only one shape. +struct PriorMeasurement { commit_sha: String, - authority_digest: String, committed_generated_digest: String, candidate_generated_digest: String, first_generation_equal: bool, @@ -803,10 +812,61 @@ struct RegenReceiptStored { candidate_artifact: String, } -fn read_receipt(path: &Path) -> Result { +/// Read the prior receipt, refusing everything that is not a first-generation measurement written +/// by this version of the carrier. +/// +/// THREE REFUSALS, each closing a state the previous shape accepted silently: +/// +/// * `deny_unknown_fields` on the carrier rejects a record carrying a field this shape does not +/// know -- which is exactly a stale `.v1` receipt, whose removed `fixed_point_equal` serde +/// would otherwise ignore; +/// * the schema equality check rejects a record whose version differs, so the version string is +/// compared rather than merely written; +/// * the variant match rejects a `fixed_point` receipt, because the second pass must build on a +/// FIRST-pass measurement and a receipt left by another second pass is not one. +/// +/// The third was already true by construction (a `fixed_point` record lacks four required fields), +/// but it is stated as an explicit arm rather than left to a missing-field parse error, because a +/// parse error would report the symptom -- a missing field name -- instead of the cause. +fn read_receipt(path: &Path) -> Result { let bytes = fs::read_to_string(path).map_err(|e| format!("read receipt {}: {e}", path.display()))?; - serde_json::from_str(&bytes).map_err(|e| format!("parse receipt {}: {e}", path.display())) + let receipt: RegenReceipt = serde_json::from_str(&bytes) + .map_err(|e| format!("parse receipt {}: {e}", path.display()))?; + match receipt { + RegenReceipt::FirstGeneration { + schema, + commit_sha, + authority_digest: _, + committed_generated_digest, + candidate_generated_digest, + first_generation_equal, + changed_paths, + candidate_artifact, + } => { + if schema != RECEIPT_SCHEMA { + return Err(format!( + "refusal: prior receipt {} declares schema {schema} but this reader is \ + {RECEIPT_SCHEMA} -- re-run `claim_executor --required-regen` to rewrite it", + path.display() + )); + } + Ok(PriorMeasurement { + commit_sha, + committed_generated_digest, + candidate_generated_digest, + first_generation_equal, + changed_paths, + candidate_artifact, + }) + } + RegenReceipt::FixedPoint { .. } => Err(format!( + "refusal: prior receipt {} is a fixed-point receipt, not a first-generation \ + measurement -- the fixed-point pass cannot build on another fixed-point pass. \ + Re-run `claim_executor --required-regen` first.", + path.display() + )), + } } fn write_receipt(path: &Path, receipt: &RegenReceipt) -> Result<(), String> { From 0afa41d3eb68c42f6c6c2ad87417b95bb9175d7d Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Thu, 20 Aug 2026 13:25:15 +0000 Subject: [PATCH 3/3] regen receipt: de-duplicate the accessor comment on the FirstGeneration arm review 54058 (non-blocking): the sentence "None would mean the first pass built the wrong variant, so it prints `unmeasured`" appeared verbatim in two adjacent comment blocks. Merged into one block preserving all four distinct facts: both values are measured by this pass; the read goes through accessors rather than a variant match because `required_regen_host` is private to `cli_run` (usable, not nameable); the accessors return Option because the sibling variant does not measure these fields; and a None therefore prints `unmeasured` rather than defaulting to a plausible-looking value. Comment-only. No semantic change, so no mirror moves (DESIGN 4c: semantic passes receive the annotation-erased projection). Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01FwPMTY6Myy3scaMNn33cg5 --- src/v1/stage0/src/bin/claim_executor.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/v1/stage0/src/bin/claim_executor.rs b/src/v1/stage0/src/bin/claim_executor.rs index d49c29bcd54..615a8e9f5da 100644 --- a/src/v1/stage0/src/bin/claim_executor.rs +++ b/src/v1/stage0/src/bin/claim_executor.rs @@ -10509,14 +10509,13 @@ fn run() -> Result { ®en_receipt_path, ) { Ok(outcome) => { - // Both values here ARE measured by this pass, so they print unqualified. The - // accessors return Option because the sibling variant does not measure them; a - // None on this path would mean the first pass built the wrong variant, so it - // prints as `unmeasured` rather than defaulting to a plausible-looking value. - // Read through accessors rather than by matching the variant: the + // Both values here ARE measured by this pass, so they print unqualified. Read + // through accessors rather than by matching the variant: the // `required_regen_host` module is private to `cli_run`, so the type is usable here - // but not nameable. `None` would mean the first pass built the wrong variant, so - // it prints `unmeasured` rather than defaulting to a plausible-looking value. + // but not nameable. The accessors return Option because the sibling variant does + // not measure these fields; a `None` on this path would mean the first pass built + // the wrong variant, so it prints `unmeasured` rather than defaulting to a + // plausible-looking value. let fge = outcome .receipt .first_generation_equal()