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

145 lines
9.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.