Files
felhom-controller/REPORT.md
T
2026-08-05 18:03:50 +02:00

9.1 KiB
Raw Blame History

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 172175 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.