v0.209.0 — R-247: the box stops saying a false thing about its own recovery package
gates / gates (push) Successful in 17s

The answer was on the wire and was discarded at the boundary, for the third time.

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 told the customer 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 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 wording.

Deliberately NOT changed, and said rather than skipped: the stale verdict itself (the hub's flag is
still the hub's verdict; runs still continue), and the customer-facing Hungarian card copy. Clearing
the wrong flag is an operator act hub-side (R-246); re-wording the card is UI work with its own
review path. This change is the wire and the diagnosis.

Found by felhom.eu/scripts/wire_contract_gate.py (G-1), which was built first and seen failing on 40
fields before anything was fixed, and which now refuses any new field of this shape.

go build / go vet / go test ./... green, run separately from this commit.
This commit is contained in:
2026-08-08 08:46:29 +02:00
parent 37b5ba08a7
commit fcffaf573a
2 changed files with 43 additions and 0 deletions
+26
View File
@@ -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
@@ -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
}