145 lines
9.1 KiB
Markdown
145 lines
9.1 KiB
Markdown
# 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.
|