# REPORT — CAMPAIGN 11 fix pass: a correct recovery code is never called wrong again **2026-08-05 · controller `v0.201.0` · commit `a3499d1`** (paired with hub `v0.97.1`) Campaign 11 walked the customer's recovery journey end to end for the first time. **The data came back byte-identical; the journey did not exist.** This pass closes six of the nine findings. ## Scenario A and its red-proof — the session's headline Measured live on the campaign venue 2026-08-05: agent 0.120.0 answers `POST /escrow/recover-offsite-password` with 404, the unlock was attempted anyway, and a **correct** recovery code came back — in **0.134 s**, far too fast for `age`'s scrypt — as: > *„A megadott helyreállítási kódot nem fogadtuk el. Ellenőrizd, hogy mind a tíz szót pontosan, > szóközökkel elválasztva írtad be…"* **RED-PROOF.** Deleting the `if support := s.recoverySupport(...)` block from `recoveryUnlockHandler`: ``` --- FAIL: TestRecoveryGate_A_OldAgentIsNamed_NeverTheCode/agent_predates_the_route_(404) R-216 RETURNED: an agent that cannot answer is still reported as a wrong recovery code --- FAIL: TestRecoveryGate_A_OldAgentIsNamed_NeverTheCode/agent_cannot_be_asked_at_all R-216 RETURNED: an agent that cannot answer is still reported as a wrong recovery code ``` The accusation returns verbatim, on the assertion that names the finding. Restored; green. *(The first attempt at this red-proof failed the test for weaker reasons — the fake recoverer succeeded regardless of the agent verdict, so the accusation could not appear. The fixture now models the real agent: one that cannot answer the route also fails the call.)* ## The four messages, quoted for review as copy 1. **The code did not open it** — the only one that mentions typing: > „A megadott helyreállítási kódot nem fogadtuk el. Ellenőrizd, hogy mind a tíz szót pontosan, > szóközökkel elválasztva írtad be — a kis- és nagybetűk nem számítanak. Semmi nem változott, > nyugodtan próbáld újra." 2. **The machine cannot ask** (R-216): > „Ez a gép még nem tudja megnyitni a mentéseidet — a hozzá tartozó házon belüli szolgáltatás > régebbi, mint amit ehhez a lépéshez használunk. A kódoddal semmi baj, és nem is használtuk fel: > tedd el biztonságos helyen. A gép magától frissül; próbáld újra később, vagy szólj a Felhom > ügyfélszolgálatának, ha egy nap múlva sem működik." 3. **The store could not be read** (R-217) — two shapes: > „A kulcs visszakerült, de a mentések listáját most nem sikerült beolvasni. A mentéseid nincsenek > veszélyben — nézd meg a Biztonsági mentés oldalt néhány perc múlva." > „A kulcs visszakerült, és biztonságban van. A gép még várja a házon kívüli tárhely kapcsolódási > adatait — amint megvannak, a mentéseid listája megjelenik a Biztonsági mentés oldalon. Nincs > teendőd." 4. **A retained earlier package** (R-222) — states two facts and promises nothing: > „Ez a kód nem nyitja meg azt a csomagot, amit most őrzünk ehhez a géphez. Ha egy korábbi kódot > adtál meg: a géped azóta új mentési kulcsot kapott, és a régebbi csomagot (…) nem töröltük — > megőrizzük. Megnyitni viszont innen egyelőre nem lehet, ezért ha a régebbi mentéseidre van > szükséged, keresd a Felhom ügyfélszolgálatát. A kódoddal semmi nem történt, és semmi nem > változott." **Live proof of message 4**, on the exact box and the exact code that produced the defect in Phase 3 step 7: rendered verbatim with the real supersession timestamp `2026-08-05 15:03:14`, and `blames typing = False`. ## Part 1b — the shape chosen, and why nobody types a number twice `ResolveManagedFloor` compared the box's agent against `ArtifactManifest.MinAgent`, which by its own doc comment describes **the golden's** controller, while `publish-train-rules.md` rule 3 states the rule about **the floor's**. They are the same number only while the floor sits at or below the golden. **Chosen: a floor ABOVE the vouched golden is HELD**, with its own reason. **No new operator input at all** — the vouched MinAgent keeps its exact meaning and the guard simply stops applying it to a version it does not describe. The rejected alternative was a second "floor MinAgent" field, which would have the operator type the same CHANGELOG number twice for the same release. Live, immediately after deploy: ``` managed floor HELD for c11: held: floor 0.200.0 is ABOVE the vouched golden 0.192.0, so its agent requirement is unknown — vouch a golden carrying the floor's controller (publish-train rule 1) ``` **⚠ What it does NOT do:** with the guard corrected and the Day-0 agent unchanged, a new box is **held, not served**. It stops being lied to; the feature works for it only after R-223. **A slip caught by the deploy itself, and fixed in v0.97.1:** the first deployed build logged `agent "0.125.0" < MinAgent 0.113.0` for a box held for the NEW reason — a comparison that is false. That is `CLAUDE.md`'s corollary (a verdict that changes which field it counts from must change its alarm text). Both surfaces now come from one `ManagedFloorDecision.HoldReason()`, pinned by a test. ## R-218's live proof — stated with its limit **Proven by test + red-proof**, not yet by a live rebuild-shaped run. Restoring the deleted short-circuit fails `TestOffsiteDeclare_StillDeclaresAfterARecoveredKeyIsPlaced` with *"the box went SILENT after recovering its key while still having no off-site target"*. **The live half is deliberately not taken**: reproducing the stranded shape means reconfiguring the campaign venue, and §11 forbids that because Phase 2 needs it as it is. What IS verified live on the venue is Scenario E's side — the configured tier is silent — plus the whole R-222 ACK chain end to end (hub 0.97.1 → ACK → controller 0.201.0 → `settings.json`: `hub_escrow_superseded_present: True`, `hub_escrow_superseded_at: 2026-08-05 15:03:14`). ## Tests and red-proofs | Scenario | Test | Red-proof — what was mutated | Result | |---|---|---|---| | A | `TestRecoveryGate_A_OldAgentIsNamed_NeverTheCode` | the capability gate deleted | **FAILED as required** | | A | `TestRecoveryGate_A_SupportedAgentProceeds`, `_CouplingIsDeclared` | — | pass | | B/C | `TestResolveManagedFloor_R216_FloorAboveGolden` (5 sub-cases) | the floor-above-golden branch deleted | **FAILED as required** | | — | `TestResolveManagedFloor_HoldReasonMatchesTheCause` | — | pass | | D | `TestOffsiteDeclare_StillDeclaresAfterARecoveredKeyIsPlaced` | the short-circuit restored | **FAILED as required** | | E | `TestOffsiteDeclare_ConfiguredTierIsSilent`, `_DisabledTargetIsNotStranded` | — | pass | | F | `TestRecoveryGate_F_UnlockBringsTheTierUpBeforeListing`, `_TierNotUpYetSaysPendingNotFailed` | — | pass | | G | `TestRecoveryGate_G_UnreadableStoreNeverClaimsToHaveOpened`, `_ThreeDistinctStates` | the zero-value inventory restored | **FAILED as required** | | H | `TestRecoveryGate_H_SupersededPackageIsNamed`, `_WithoutASuperseded…` | — | pass | | I | `TestRecoveryGate_I_DirectGetRefusedOnABoxThatNeverHadBackups`, `_StillWorksWhenOffered` | the direct-GET gate deleted | **FAILED as required** | Green gates: `go build ./... && go vet ./... && go test ./...` rc=0 in both repos, read separately from every commit. `controller_gates.py --fast` and `repo_gates.py --fast` both all-OK. The `-run` filter was proven to match (13 `=== RUN` lines) rather than trusted. ## Deployed | | | |---|---| | controller `0.201.0` | campaign venue (c11 guest 9201) **and** demo-felhom guest 9201 — both `healthy` | | hub `0.97.1` | GitOps: manifest bump → ArgoCD hard-refresh + deliberate sync → `Synced Healthy`, rollout complete, image verified | | agent | **untouched** — v0.125.0 is what R-223 asks the operator to vouch, not what this changed | CI: controller task **171** (`a3499d18`) success; felhom.eu tasks **172–175** success. No `--no-verify`; every pre-push gate ran and passed. ## Still open, deliberately **R-214** (console pairing banner), **R-220** (drives unenrollable after a rebuild — the deploy refuses and the wizard's list is empty), **R-221** (a rebuilt box cannot run the escrow ceremony at all). R-221 in particular is a real blocker for a different flow. **R-213** and **R-202** untouched. ## Observations, not acted on 1. **`SupportsWithSource` looks up `featureProbes` FIRST and returns `SupportUnknown` on a table gap, before the version path runs.** So `FeatureBackupAgeState`, which has a `featureMinAgent` row and no `featureProbes` row, can never be decided by version — it is always Unknown. This fix works around it by registering a probe that returns a sentinel; the ordering itself is untouched (out of scope). 2. The recovery screen renders `SealedAt` as a raw RFC3339 UTC string (`2026-08-05T13:11:11Z`) to a Hungarian household; `recoverySealedAt()` is documented as "the human date". 3. The escrow wizard's form says *„tíz szó"* while the issued code is hyphen-joined, and the wrong-code message says *szóközökkel elválasztva* (space-separated). Both forms should be accepted or the copy aligned; not investigated.