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
@@ -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
}