9.1 KiB
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
- 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."
- 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."
- 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."
- 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
SupportsWithSourcelooks upfeatureProbesFIRST and returnsSupportUnknownon a table gap, before the version path runs. SoFeatureBackupAgeState, which has afeatureMinAgentrow and nofeatureProbesrow, 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).- The recovery screen renders
SealedAtas a raw RFC3339 UTC string (2026-08-05T13:11:11Z) to a Hungarian household;recoverySealedAt()is documented as "the human date". - 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.