v0.201.0 — a correct recovery code is never called wrong again (CAMPAIGN-11) — MinAgent 0.125.0
gates / gates (push) Successful in 9s

R-216: the offsite key recovery is a coupled feature and now says so. featureProbes +
featureMinAgent 0.125.0 + a Supports gate at the unlock entry point, FAILING CLOSED — an
agent that cannot answer is named as such instead of the customer's code being blamed.
Measured live: a 404 from agent 0.120.0 came back as "we did not accept your recovery
code, check that all ten words", in 0.134 s, against a perfect code.

R-218: delete the repo-password short-circuit in needsOffsiteCredential. The declaration
stops when the TIER WORKS, not when a key exists — installing a key is the recovery
screen's whole job, so succeeding at recovery was switching off the mechanism that would
have delivered the coordinates to use it.

R-219: the unlock finishes the job — place the key, bring the tier up, then list. Without
it the promised listing could never render on the shape the screen exists for.

R-217: an unreadable store no longer claims to have opened with unattributable content
(the OffsiteInventory{} zero value). Opened / empty / unreadable are three states.

R-222: a code that is right about a RETAINED earlier package is named, not blamed. States
what the hub knows and promises nothing — no read path exists.

R-215: GET /recovery is gated on the same predicate as the interception.

Five red-proofs, each demonstrated failing and restored.
This commit is contained in:
2026-08-05 17:48:08 +02:00
parent a315d623b8
commit a3499d1807
12 changed files with 778 additions and 20 deletions
+20 -1
View File
@@ -20,6 +20,11 @@ type EscrowStatus struct {
IdentityBlobPresent bool `json:"identity_blob_present"`
ResticPwSHA256 string `json:"restic_pw_sha256"`
CreatedAt string `json:"created_at"`
// 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.
SupersededPresent bool `json:"superseded_present"`
SupersededAt string `json:"superseded_at"`
}
// EscrowAutoConfirmer runs the auto-confirm check on each report ACK. Long-lived (one per process) so
@@ -54,7 +59,13 @@ type EscrowAutoConfirmer struct {
// immediately, so the one fact that distinguishes a REBUILT box from a box that never had
// off-site backups was thrown away on every cycle. nil → not recorded (older wiring, tests).
RecordPresence func(present bool) error
Logger *log.Logger
// RecordSuperseded persists the ACK's `superseded_present` / `superseded_at` (v0.201.0, R-222) —
// whether the hub is ALSO keeping an EARLIER sealed package for this box. Wired here for the same
// reason as RecordPresence: this is the one place the ACK's escrow object already arrives, and a
// second wiring point in main.go is how this project accumulated six features that were built and
// never wired. nil → not recorded (older wiring, tests).
RecordSuperseded func(present bool, at string) error
Logger *log.Logger
mu sync.Mutex
warnedHash string // last mismatched hub hash we warned about (dedupe; shared by both branches)
@@ -111,6 +122,14 @@ func (c *EscrowAutoConfirmer) Reconcile(es *EscrowStatus) {
c.logf("[WARN] [escrow-confirm] could not record the hub's identity-blob presence (present=%v): %v", es.IdentityBlobPresent, err)
}
}
// Same discipline, same place, same reason (R-222): recorded unconditionally, because the box that
// needs it most is the unconfigured rebuilt one every gate below skips. Failure is logged, never
// swallowed, and never blocks the auto-confirm.
if c.RecordSuperseded != nil {
if err := c.RecordSuperseded(es.SupersededPresent, es.SupersededAt); err != nil {
c.logf("[WARN] [escrow-confirm] could not record the hub's superseded-package state (present=%v): %v", es.SupersededPresent, err)
}
}
c.mu.Lock()
c.sealedAt = es.CreatedAt // in-memory only; a timestamp, never a secret
c.mu.Unlock()