diff --git a/CHANGELOG.md b/CHANGELOG.md index d76de9a..252166e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,29 @@ +## v0.209.0 — the box stops saying a false thing about its own recovery package (2026-08-08, R-247 / R-260) — MinAgent 0.127.0 + +**R-247, and it is the third instance of one shape: the answer was on the wire and was discarded at +the boundary.** The hub has sent `escrow_stale` in the report ACK since v0.57.0 +(`json:"escrow_stale,omitempty"`). `report.EscrowStatus` had no field for it, so `encoding/json` +dropped it, and an empty `restic_pw_sha256` had exactly one possible reading here — *hash-less +supersession*. + +On `demo-hp` that reading was **false in every clause** for four days, and the box said so in its own +words: the hub HAD the hash and was withholding it because the escrow row carries a stale flag +(R-246); there had been no supersession; and the bundle DID cover the password — the hashes matched +exactly. + +**Fixed by receiving the field.** `EscrowStatus.Stale` now decodes, and `reconcileEscrowed` tells the +two conditions apart. A withheld hash now reports that the hub has flagged the row and is withholding, +that **this box therefore cannot verify its bundle either way**, and that it is *not established* that +the bundle fails to cover the password. The genuinely hash-less case keeps its original wording. + +**Deliberately NOT changed:** the stale verdict itself (the hub's flag is still the hub's verdict, and +runs still continue), and the customer-facing card copy. Clearing the wrong flag is an operator act +hub-side and is R-246; re-wording the Hungarian card is UI work with its own review path. This change +is the wire and the diagnosis. + +`felhom.eu/scripts/wire_contract_gate.py` (G-1) now refuses any new field of this shape on the three +declared wires. + ## v0.208.0 — the last two secrets leave the page source, and a gate so there is no fourth (2026-08-08, R-254) — MinAgent 0.127.0 v0.207.0 removed a password from one page. The census that fix required found two more sites; this diff --git a/controller/internal/report/escrow_confirm.go b/controller/internal/report/escrow_confirm.go index 15f69b4..cdd5d4d 100644 --- a/controller/internal/report/escrow_confirm.go +++ b/controller/internal/report/escrow_confirm.go @@ -20,6 +20,17 @@ type EscrowStatus struct { IdentityBlobPresent bool `json:"identity_blob_present"` ResticPwSHA256 string `json:"restic_pw_sha256"` CreatedAt string `json:"created_at"` + // Stale (R-247 / R-260, v0.209.0) — the hub has FLAGGED this host's escrow row stale and is + // therefore WITHHOLDING ResticPwSHA256 rather than having no hash to send. + // + // The hub has sent this on every ACK since v0.57.0 (`json:"escrow_stale,omitempty"` on + // store.EscrowStatus). This struct had no field for it, so encoding/json discarded it on + // arrival and an empty hash had exactly one possible reading here: "hash-less supersession". + // That reading was FALSE on demo-hp for four days and the box told the customer so in its own + // words — the hub had the hash, the bundle did cover the password, and there had been no + // supersession (R-246). The answer was on the wire the whole time and was dropped at the + // boundary; a gate now refuses that shape (`felhom.eu/scripts/wire_contract_gate.py`). + Stale bool `json:"escrow_stale"` // SupersededPresent / SupersededAt (v0.201.0, R-222) — the hub is ALSO keeping an earlier sealed // package, and when it was set aside. Absent on a pre-0.97.0 hub, which reads as "no earlier // package" and simply keeps today's message: an older hub cannot make the screen say anything new. @@ -235,6 +246,12 @@ func (c *EscrowAutoConfirmer) reconcileEscrowed(es *EscrowStatus) { return } if hubHash == "" { + // TWO DIFFERENT CONDITIONS, told apart at last (R-247). Before v0.209.0 both printed the + // second sentence, and on a withheld hash every clause of it was false. + if es.Stale { + c.logf("[WARN] [escrow-confirm] STALE escrow: the hub has FLAGGED this box's escrow row stale and is WITHHOLDING the password hash, so this box cannot verify its bundle either way — it is NOT established that the bundle fails to cover the offsite password. Clearing the flag is an operator act (hub-side); a new recovery code (wizard /backup/escrow) also resolves it. State stays escrowed; runs continue") + return + } c.logf("[WARN] [escrow-confirm] STALE escrow: the hub's current blob carries NO password hash (hash-less supersession) — the stored recovery bundle does not cover the offsite password; create a new recovery code (wizard /backup/escrow). State stays escrowed; runs continue") return }