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
+16 -1
View File
@@ -494,6 +494,10 @@ func main() {
// updater's floor/update-running state and defer the one-time-password consume past a managed
// day-0 floor-update (the F10 race). The gate is wired ONLY when an updater exists — with no update
// mechanism there is no floor-update to race, so the bridge reconciles immediately (Settle nil).
// offsiteBridge is hoisted out of the block below ONLY so the recovery screen can drive one
// reconcile between placing a recovered key and reading the repository (R-219). nil when off-site
// is not configured for this customer, which the web seam treats as "skip".
var offsiteBridge *offsiteapply.Bridge
if backupMgr != nil && cfg.Offsite.Enabled && cfg.Hub.URL != "" && cfg.Hub.APIKey != "" {
bridge := &offsiteapply.Bridge{
Cfg: cfg,
@@ -526,6 +530,7 @@ func main() {
return Version, floor, u.IsUpdateRunning(), floor != ""
})
}
offsiteBridge = bridge
go func() {
// ReconcileWhenSettled runs the settle-gate FIRST (its own bounds), then Reconcile under a
// fresh 3-minute context — the gate's wait never eats the reconcile budget.
@@ -670,7 +675,10 @@ func main() {
// is precisely the case that needs it. Half of the stranded-rebuild predicate in
// backup.needsOffsiteCredential; the other half (no repository password) is local.
RecordPresence: sett.SetHubEscrowIdentityPresent,
Logger: logger,
// v0.201.0 (R-222): whether the hub is ALSO keeping an EARLIER sealed package, so the
// recovery screen can name that situation instead of blaming the customer's typing.
RecordSuperseded: sett.SetHubEscrowSuperseded,
Logger: logger,
}
// Wire hub verification: update settings when hub reports customer status
hubPusher.OnPushResponse = func(resp *report.PushResponse) {
@@ -1268,6 +1276,13 @@ func main() {
// before a code is entered. A timestamp, never a secret.
webServer.SetEscrowSealedAt(escrowConfirmer.SealedAt)
}
// R-219 / R-218: the unlock brings the off-site tier up between placing the recovered key and
// reading the repository. Without it the listing the screen promises can never render on the
// pristine rebuilt box — no key ⇒ no target ⇒ no inventory — and the apply-bridge's only other
// trigger is a config refresh or a restart, neither of which the customer can cause.
if offsiteBridge != nil {
webServer.SetRecoveryTierUp(offsiteBridge.Reconcile)
}
webServer.SetIntegrationManager(integrationMgr)
if reportTrigger != nil {
// Out-of-cycle report push after hub-relevant user actions (escrow claim, settings