This commit is contained in:
+39
-1
@@ -7,7 +7,45 @@
|
|||||||
>
|
>
|
||||||
> Ask Claude Code: "Please update CONTEXT.md with what we did today"
|
> Ask Claude Code: "Please update CONTEXT.md with what we did today"
|
||||||
|
|
||||||
Last updated: 2026-08-05 (v0.199.0 — R-204 item 4: a rebuilt box declares it needs a credential)
|
Last updated: 2026-08-05 (v0.200.0 — R-193: the recovery screen, unlocking only)
|
||||||
|
|
||||||
|
> **2026-08-05 — v0.200.0 (R-193 CLOSED).** The customer-facing recovery screen. Until now a customer
|
||||||
|
> whose machine was rebuilt had everything needed to get their data back and no way to find out — the
|
||||||
|
> only route was a command line.
|
||||||
|
>
|
||||||
|
> **IT UNLOCKS AND ONLY UNLOCKS** (operator ruling). Explains, takes the recovery code, opens the
|
||||||
|
> repository, lists what is in it (apps, dates, sizes). **Restores nothing** — restore is per-app and
|
||||||
|
> lives in the backups area; the put-back is **R-213** and its stated requirement is a
|
||||||
|
> live-versus-backup comparison.
|
||||||
|
>
|
||||||
|
> **ONE CORE, TWO CALLERS.** `backup.RecoverInstallCore` is the only fetch→unseal→compare→install path.
|
||||||
|
> `RecoverAndInstall` is now a thin CLI wrapper — exit codes and printed lines byte-identical, every
|
||||||
|
> pre-existing CLI test passed unchanged — and the handler calls the same function. Asserted from
|
||||||
|
> source by AST on BOTH sides, plus a test that the routes and the landing-page interception exist.
|
||||||
|
>
|
||||||
|
> **THE TRIGGER HAS TWO SHAPES and the second is the one that matters.** `OffsiteRecoveryOffer` = the
|
||||||
|
> hub holds a package AND (no repository password OR the tier is orphaned). The literal "no repository
|
||||||
|
> password" alone is a window that CLOSES BY ITSELF — `WriteOffboxSecrets` auto-generates one on
|
||||||
|
> re-apply (R-193's own orphaning mechanism) and hub v0.96.0's self-heal re-applies within ~15–30 min.
|
||||||
|
> Shape (b) is also what the shipped move-aside requires, which is why the discard choice can reach it.
|
||||||
|
>
|
||||||
|
> **CLAIMED is part of the predicate** — a legacy-open box passes through `RequireAuth`, so without an
|
||||||
|
> explicit `authEnabled()` check the interception fired for an unauthenticated visitor. A test caught it.
|
||||||
|
>
|
||||||
|
> **„Most nem" suppresses the FULL PAGE ONLY.** The backups-area entry point is bound to
|
||||||
|
> `recoveryOffer`, never to the postpone flag.
|
||||||
|
>
|
||||||
|
> **The code:** POST body only, never logged/persisted/echoed, cleared on every path, `no-store`,
|
||||||
|
> `autocomplete=off`. **No lockout** — a ten-word phrase is not guessable and locking a customer out of
|
||||||
|
> their own data for a typo is worse; failures are logged locally without the code, and NO operator
|
||||||
|
> alert is raised (reasoning in REPORT.md §4).
|
||||||
|
>
|
||||||
|
> *Live:* demo-felhom is genuinely in shape (b), so validation needed no arrangement — `/launcher` →
|
||||||
|
> 302 `/recovery`, both mandatory sentences rendered, three wrong codes refused with the `offbox/`
|
||||||
|
> listing byte-identical and no lockout, and the code found in no file, log or ring **with a
|
||||||
|
> planted-copy positive control that first exposed a mis-aimed sweep**. **NOT proven live: a CORRECT
|
||||||
|
> code** — none was kept for demo-felhom's orphaned history and demo-hp's is operator-held.
|
||||||
|
|
||||||
|
|
||||||
> **2026-08-05 — v0.199.0 (R-204 item 4 / R-193).** The last of the four manual interventions the
|
> **2026-08-05 — v0.199.0 (R-204 item 4 / R-193).** The last of the four manual interventions the
|
||||||
> 2026-08-04 drill needed. **Operator ruling: automate it, and the trigger is a state the BOX
|
> 2026-08-04 drill needed. **Operator ruling: automate it, and the trigger is a state the BOX
|
||||||
|
|||||||
@@ -1,95 +1,222 @@
|
|||||||
# REPORT — R-204 item 4 (controller v0.199.0), 2026-08-05
|
# REPORT — R-193: the recovery screen (controller v0.200.0), 2026-08-05
|
||||||
|
|
||||||
**A rebuilt box declares that it needs a credential.** The box half of the last manual intervention
|
**A customer whose machine was rebuilt can now find out, and unlock their backups, without a command
|
||||||
the 2026-08-04 drill needed. Hub half: `felhom.eu` v0.96.0, reported in that repo's
|
line.** R-193's remaining half. No hub change; `felhom.eu` carries registers and docs only.
|
||||||
`REPORT-r204-item4.md`.
|
|
||||||
|
|
||||||
## 1. Baselines
|
## 1. Baselines, and §3's five pieces
|
||||||
|
|
||||||
`felhom-controller` at `68f195676b91` / v0.198.0 — **exact match**, tree clean, `HEAD == origin/main`.
|
`felhom-controller` at `be3c5fa7f6a1` / v0.199.0 — **exact match**, tree clean, `HEAD == origin/main`.
|
||||||
§4's landmarks held: `backup.OffboxReportStatus` returned `nil` when no target existed (the ambiguity),
|
`felhom.eu` at hub v0.96.0, unchanged. **All five pieces of §3's table were where it said**, at the
|
||||||
and the ACK's `escrow` object was discarded on an unconfigured box because
|
cited landmarks: `RunRecoveryCheck` (`:55`), `RecoverAndInstall` (`:140`), `offboxResetHandler`
|
||||||
`EscrowAutoConfirmer.Reconcile` returns early when the box is neither pending nor escrowed.
|
(`:245`) with its `confirm=1` gate and orphaned precondition, `OffboxOrphaned` (`:244`), and the
|
||||||
|
per-app snapshot lookup (`offbox_restore.go:84`). **No mechanism was invented.**
|
||||||
|
|
||||||
**Register drift worth flagging:** the task states R-204 is the highest ID in use. It is **R-211**;
|
**Register:** the highest ID was **R-212** as stated, and still is — re-checked at the end for the
|
||||||
grepped before minting, and the hub session mints R-212.
|
second writer. This session mints **R-213**.
|
||||||
|
|
||||||
## 2. What changed
|
## 2. The one thing the task under-specified, and what I did about it
|
||||||
|
|
||||||
**The ACK field stopped being discarded.** `RecordPresence` is called FIRST in `Reconcile`, before
|
§8.1 says the page appears on the same two facts the box uses to declare `needs_credential`: **a fresh
|
||||||
every gate. It lives in the auto-confirmer because that is already the one place the ACK escrow object
|
data area** (no repository password) and **a hub-held package**. Taken literally that is a window that
|
||||||
arrives and is already wired — a second consumer would be a second wiring point, and this project's
|
**closes by itself**: `WriteOffboxSecrets` AUTO-GENERATES a repository password on re-apply — which is
|
||||||
built-but-never-wired count is six. Last-write-wins, not set-only, so a customer RESET turns the
|
R-193's own orphaning mechanism — and since hub v0.96.0's credential self-heal the re-apply now happens
|
||||||
declaration back off; a nil ACK escrow records nothing (absence of a statement is not a statement of
|
unaided within ~15–30 minutes. The customer who logs in the next morning would never have seen the page.
|
||||||
absence); a record failure is logged and does not block the auto-confirm.
|
|
||||||
|
|
||||||
**The declaration needs BOTH halves** (`needsOffsiteCredential`): a fresh data area (no repository
|
So the predicate is **the hub holds a package AND (no repository password OR the tier is orphaned)**.
|
||||||
password) AND a hub-held recovery package. Freshness alone is a box that never had off-site backups —
|
Shape (b) is the state a real rebuilt box is in, and it is also the state the shipped move-aside
|
||||||
declaring on it would make every un-configured box in the fleet ask for a credential. A merely
|
requires — which is what lets "I do not want the old data" reach the existing handler instead of
|
||||||
DISABLED target is the customer's own choice and never declares.
|
needing a new one. **Scenario B is unaffected:** a healthy box has its own key and is not orphaned; a
|
||||||
|
box that never had off-site backups fails the package condition. **demo-felhom is in shape (b) right
|
||||||
|
now**, which is why validation 1 needed no arrangement at all.
|
||||||
|
|
||||||
**Inert to every existing reader**, established from the hub's code: `enabled:false` + zero sizes, so
|
## 3. Every Hungarian string, quoted for review as copy
|
||||||
`isStale` short-circuits on `!Enabled` and `fillBand` returns OK on a zero quota/size; an unknown
|
|
||||||
`state` string is ignored by `encoding/json`. **A configured box's JSON is byte-identical to
|
|
||||||
v0.198.0's.** The one reader that would have misread it is the hub's `reportHasOffsite`, tightened in
|
|
||||||
hub v0.96.0.
|
|
||||||
|
|
||||||
## 3. Files modified
|
**The two MANDATORY sentences of §8.2** — both verified in the live rendered page:
|
||||||
|
|
||||||
|
> **A helyreállítási kódot senki nem tudja pótolni** — sem a Felhom, sem az ügyfélszolgálat, sem az
|
||||||
|
> üzemeltető. Ez szándékos: így a mentéseidet rajtad kívül senki nem tudja megnyitni. Ha a kód
|
||||||
|
> elveszett, a korábbi mentések nem nyithatók meg többé.
|
||||||
|
|
||||||
|
> Ha megadod a kódot, **feloldjuk a mentéseid zárolását és megmutatjuk, mi van bennük** — melyik
|
||||||
|
> alkalmazás, mikorról, mekkora. **Ebben a lépésben semmit nem állítunk vissza és semmi nem változik.**
|
||||||
|
> A visszaállítást utána, alkalmazásonként külön választhatod.
|
||||||
|
|
||||||
|
**The situation, before any code:**
|
||||||
|
|
||||||
|
> Ezt a gépet újratelepítették. A korábbi, **házon kívüli mentéseid megvannak** — a Felhom központi
|
||||||
|
> rendszere őriz hozzájuk egy lezárt csomagot, amelyet **<dátum>** zártunk le. A csomagot csak a
|
||||||
|
> **te helyreállítási kódoddal** lehet kinyitni.
|
||||||
|
|
||||||
|
**The form:** `Helyreállítási kód (tíz szó)` · placeholder `tíz szó, szóközökkel elválasztva` ·
|
||||||
|
buttons `Mentések feloldása` / `Most nem`.
|
||||||
|
|
||||||
|
**What "most nem" means:**
|
||||||
|
|
||||||
|
> A „Most nem" csak azt jelenti, hogy nem zavarunk vele többet a kezdőlapon. A mentéseid ettől
|
||||||
|
> megmaradnak, és ez az oldal a **Biztonsági mentés → Távoli mentés** oldalról bármikor újra elérhető.
|
||||||
|
|
||||||
|
**A wrong code** (quoted from the LIVE response):
|
||||||
|
|
||||||
|
> 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.
|
||||||
|
|
||||||
|
**Refused (a different key is already present):**
|
||||||
|
|
||||||
|
> Ezen a gépen már van egy másik mentési kulcs. A régi előzmény visszanyitása felülírná azt, ezért nem
|
||||||
|
> hajtottuk végre. Vedd fel a kapcsolatot a Felhom ügyfélszolgálatával.
|
||||||
|
|
||||||
|
**After the unlock:**
|
||||||
|
|
||||||
|
> Ezek a **te** mentéseid, a lent jelzett időpontokból. Nézd át, hogy tényleg azt találod-e itt, amire
|
||||||
|
> számítasz — **semmit nem állítottunk vissza és semmi nem változott.**
|
||||||
|
|
||||||
|
> A mentéseid zárolása feloldva. Az alábbiakat találtuk a tárolóban — semmit nem állítottunk vissza.
|
||||||
|
|
||||||
|
Table headings: `Alkalmazás` · `Legutóbbi mentés` · `Méret`. Next step:
|
||||||
|
|
||||||
|
> A visszaállítás alkalmazásonként történik, a **Biztonsági mentés → Visszaállítás** oldalon. Ott
|
||||||
|
> választhatod ki, melyik alkalmazás mit hozzon vissza.
|
||||||
|
|
||||||
|
**An EMPTY store, stated plainly (§8.6):**
|
||||||
|
|
||||||
|
> A tároló megnyílt, de **nincs benne egyetlen mentés sem**. Ez azt jelenti, hogy a kulcs jó volt, de
|
||||||
|
> ehhez a géphez nem tartozik korábbi mentés. Ha korábban biztosan készültek mentések, keresd a Felhom
|
||||||
|
> ügyfélszolgálatát, mielőtt bármit tennél.
|
||||||
|
|
||||||
|
**Unlocked but no coordinates yet** (the pristine rebuilt shape, added after a test found the crash):
|
||||||
|
|
||||||
|
> A kulcs visszakerült. A gép még most kapcsolódik újra a házon kívüli tárhelyhez — a mentéseid listája
|
||||||
|
> néhány perc múlva jelenik meg a Biztonsági mentés oldalon.
|
||||||
|
|
||||||
|
**The exceptional path — first mention, then the SECOND confirmation:**
|
||||||
|
|
||||||
|
> Ha a helyreállítási kódod véglegesen elveszett, és tudomásul veszed, hogy a korábbi mentések így nem
|
||||||
|
> nyithatók meg többé: *nem kérem vissza a korábbi adatokat*.
|
||||||
|
|
||||||
|
> **Biztosan nem kéred vissza a korábbi mentéseket?** Ha megerősíted:
|
||||||
|
> • a korábbi mentéseket **félretesszük — nem töröljük**;
|
||||||
|
> • a helyreállítási kód nélkül **többé nem lesznek megnyithatók**;
|
||||||
|
> • a gép **új, üres mentési tárolót kezd**, és mostantól oda ment;
|
||||||
|
> • ez az oldal **többé nem jelenik meg**.
|
||||||
|
> Ha csak most nincs kéznél a kódod, válaszd inkább a „Most nem" lehetőséget.
|
||||||
|
|
||||||
|
Button: `Igen, félretehetitek a korábbi mentéseket` / `Mégsem`.
|
||||||
|
|
||||||
|
**The permanent entry point in the backups area:**
|
||||||
|
|
||||||
|
> **A korábbi, házon kívüli mentéseid visszaszerezhetők.** Ezt a gépet újratelepítették, és a Felhom
|
||||||
|
> központi rendszere őriz hozzá egy lezárt csomagot. A helyreállítási kódoddal feloldhatod a korábbi
|
||||||
|
> mentéseidet, és megnézheted, mi van bennük. → `Adatok visszaszerzése`
|
||||||
|
|
||||||
|
## 4. §8.4 — repeated failures: LOG LOCALLY, DO NOT ALERT. The reasoning.
|
||||||
|
|
||||||
|
**No lockout**, as instructed. On repeated failures I chose to **log locally and raise no operator
|
||||||
|
signal**, and the reasoning is:
|
||||||
|
|
||||||
|
- The realistic generator of repeated failures is **the owner mistyping a ten-word phrase**, which is
|
||||||
|
the single most likely event on this page. An alert there is noise on every real recovery.
|
||||||
|
- The threat it would guard against — someone guessing R — is not credible: a 10-word EFF phrase is
|
||||||
|
~129 bits, and an attacker who could reach this page already holds the household password.
|
||||||
|
- **Silence is not total:** every failure writes a `[WARN] [web] recovery: unlock failed: …` line
|
||||||
|
carrying the agent's step and no code, so it reaches the debug ring and any operator log pull. A box
|
||||||
|
being probed is visible; it just does not page anyone.
|
||||||
|
|
||||||
|
The deliberate residual: a slow, patient prober produces only ring entries nobody reads unprompted.
|
||||||
|
That is accepted, and stated here rather than left implied.
|
||||||
|
|
||||||
|
## 5. Files created / modified
|
||||||
|
|
||||||
| File | Change |
|
| File | Change |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `controller/internal/settings/settings.go` | **new** `HubEscrowIdentityPresent` field + accessors (saves only on a change) |
|
| `internal/backup/offbox_recovery_cli.go` | **new** `RecoverInstallCore` + its three outcomes; `RecoverAndInstall` reduced to a wrapper (exit codes and printed lines unchanged) |
|
||||||
| `controller/internal/report/escrow_confirm.go` | **new** `RecordPresence` seam, called before every gate |
|
| `internal/backup/offbox_inventory.go` | **new** — the read-only listing, the explicit empty case, `ErrNoOffsiteTarget`, `HumanizeBytes` |
|
||||||
| `controller/internal/backup/offbox.go` | **new** `OffsiteStateNeedsCredential`, `needsOffsiteCredential`; `State` on the report object |
|
| `internal/backup/offbox.go` | **new** `OffsiteRecoveryOffer` (the two-shape predicate) |
|
||||||
| `controller/cmd/controller/main.go` | wires `RecordPresence` |
|
| `internal/settings/settings.go` | **new** `RecoveryNoticePostponed` + accessors |
|
||||||
| `controller/internal/backup/offbox_declare_test.go` | **new** — Scenarios A and B + the two exclusions + the byte-compat assertion |
|
| `internal/report/escrow_confirm.go` | **new** `SealedAt()` — the ACK's escrow `created_at` |
|
||||||
| `controller/internal/report/escrow_presence{,_wiring}_test.go` | **new** — the recorder and its AST wiring test |
|
| `internal/web/recovery_handlers.go` | **new** — the page, unlock, postpone, the seams |
|
||||||
| `.githooks/pre-push` | the workspace-root assertion (rider) |
|
| `internal/web/templates/recovery.html` | **new** — the full page |
|
||||||
|
| `internal/web/{server,handlers,funcmap}.go`, `templates/backups_remote.html` | routes, the landing-page interception, the permanent entry point, `humanBytes` |
|
||||||
|
| `cmd/controller/main.go` | wires `SetEscrowSealedAt` |
|
||||||
|
| `internal/web/recovery_test.go`, `recovery_wiring_test.go` | **new** — Scenarios A–H + three AST seam tests |
|
||||||
|
|
||||||
**Commits on `main`:** `1214bae` (the declaration) · `a91f055` (rider) · `992803c` (CHANGELOG).
|
**Commits on `main`:** `636c51e` (implementation) · `62b85ec` (CHANGELOG). **Deployed:**
|
||||||
**Deployed:** `felhom-controller:0.199.0` on demo-felhom 9201 and demo-hp 9201, both healthy.
|
`gitea.dooplex.hu/admin/felhom-controller:0.200.0` on demo-felhom 9201 and demo-hp 9201.
|
||||||
|
|
||||||
## 4. Tests and red-proofs
|
## 6. Tests and red-proofs
|
||||||
|
|
||||||
Green gate: `go build ./... && go vet ./... && go test ./...` — **rc=0**.
|
Green gate: `go build ./... && go vet ./... && go test ./...` — **full suite rc=0**;
|
||||||
`python3 controller/scripts/controller_gates.py --fast` — **all eight gates OK**.
|
`controller_gates.py --fast` — **all eight OK**.
|
||||||
|
|
||||||
| Scenario | Test | Result | Red-proof — what was mutated | Outcome |
|
| Scenario | Test | Result | Red-proof — what was mutated | Outcome |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| A | `TestOffsiteDeclare_RebuiltBoxDeclaresNeedsCredential` | PASS | dropped the escrow condition from `needsOffsiteCredential` | **Scenario B FAILED** — *"a box that never had off-site backups DECLARED a need… every un-configured box in the fleet would now ask for a credential"*. This is the plausible wrong fix, and B is the guard that catches it. |
|
| A | `TestRecovery_A_PageAppearsForARebuiltBox` | PASS | — | — |
|
||||||
| B | `TestOffsiteDeclare_NeverHadOffsiteSaysNothing` | PASS | (as above) | **FAILED** |
|
| B | `TestRecovery_B_DoesNotAppearForAnyoneElse` (3 sub-cases) | PASS | dropped the hub-package condition from `OffsiteRecoveryOffer` | **FAILED** — *"a box that never had off-site backups was offered a recovery screen for data it never had"* |
|
||||||
| — | `TestOffsiteDeclare_BoxThatStillHoldsItsRepoPasswordDoesNotDeclare` | PASS | — | — |
|
| C | `TestRecovery_C_UnlockOpensAndLists` + `…EmptyStoreSaysSo` | PASS | — | — |
|
||||||
| — | `TestOffsiteDeclare_DisabledTargetIsNotStranded` | PASS | — | — |
|
| D | `TestRecovery_D_WrongCodeFailsClosedAndIsKind` | PASS | — | — |
|
||||||
| — | `TestOffsiteDeclare_ConfiguredBoxJSONIsUnchanged` | PASS | — | — |
|
| E | `TestRecovery_E_PostponeKeepsTheEntryPoint` | PASS | bound the backups-page entry point to `recoveryInterrupts` instead of `recoveryOffer` | **FAILED** — *"the backups page no longer offers the route… a customer who clicked past it once has lost the way to their own data"* |
|
||||||
| — | `TestEscrowConfirm_RecordsPresenceEvenWhenOffboxUnconfigured` (+ nil-ACK, record-failure) | PASS | — | — |
|
| F | `TestRecovery_F_SetAsideNeedsTwoConfirmations` | PASS | removed the `ConfirmSetAside` gate in the template | **FAILED** — *"the set-aside form is on the FIRST view — one click would set the customer\'s history aside"* |
|
||||||
| seam | `TestMainWiresRecordPresence` | PASS | commented out `RecordPresence:` in main.go | **FAILED** — *"EscrowAutoConfirmer is constructed WITHOUT RecordPresence — R-204 item 4 ships inert"* |
|
| G | `TestRecovery_G_PageAndCLIShareOneCore` + `TestRecoveryHandlerDrivesTheSharedCore` | PASS | gave the handler its own inline fetch→compare→install | **FAILED** — *"recoveryUnlockHandler does NOT call backup.RecoverInstallCore"* |
|
||||||
|
| H | `TestRecovery_H_CodeLeavesNoTrace` | PASS | (carries its own positive control) | — |
|
||||||
|
| I | `TestCLIWrapperDrivesTheSharedCore`, `TestRecoveryRoutesAreWired` | PASS | — | — |
|
||||||
|
|
||||||
## 5. Live validation
|
**Scenario E's assertion was STRENGTHENED mid-session** because the first version asserted only
|
||||||
|
`recoveryOffer()`, which the mutation did not touch — it would have passed against the very defect it
|
||||||
|
names. It now asserts the **rendered** backups page contains `href="/recovery"`, and only then did the
|
||||||
|
mutation fail. That is the red-proof-that-passes trap, caught and closed rather than reported as green.
|
||||||
|
|
||||||
1. **The recorder works on a HEALTHY box** — the case that used to return early. Both demo boxes now
|
**Two real defects the tests caught before shipping:**
|
||||||
carry `hub_escrow_identity_present = true` in `settings.json`.
|
|
||||||
2. **The declaration, produced live without wiping a box.** demo-felhom 9201 was arranged
|
|
||||||
**reversibly** into the stranded shape (settings + `offbox/` backed up first). The hub received
|
|
||||||
report **id=16743**: `{"enabled":false,"escrow_state":"","state":"needs_credential",
|
|
||||||
"snapshot_count":0,"repo_size_bytes":0,"quota_gb":0}`. Restored the same minute — report id=16744 is
|
|
||||||
healthy. **The single declaration was absorbed by the hub's debounce; no self-heal event fired.**
|
|
||||||
3. **A push from outside the workspace is refused**, naming the expected root, before the gates run;
|
|
||||||
with the assertion removed the same push succeeds. Both directions demonstrated.
|
|
||||||
4. **Part 4 (the deletion) HALTED at its STOP** — the measured paths do not match the register. See
|
|
||||||
`felhom.eu/REPORT-r204-item4.md` §8 and R-212.
|
|
||||||
|
|
||||||
**Teardown:** demo-felhom restored and verified (`enabled=True`, `escrow_state=escrowed`,
|
1. **An UNCLAIMED (legacy-open) box would have been shown the page.** `RequireAuth` passes a
|
||||||
`repo_password present=True`); the pre-change backup remains at `/root/r204-backup` on that guest.
|
password-less box through, so `ServeHTTP`'s interception fired for an unauthenticated visitor.
|
||||||
Scratch clones removed. Nothing provisioned.
|
Fixed by an explicit `authEnabled()` check in `recoveryOffer`.
|
||||||
|
2. **The inventory nil-dereferenced when no off-site target was configured** — exactly the pristine
|
||||||
|
rebuilt shape. Now a named error with its own honest customer message.
|
||||||
|
|
||||||
## 6. CI and hooks
|
## 7. Live validation (endpoint-level; `claude-in-chrome` is not available on DooPlex)
|
||||||
|
|
||||||
**`--no-verify` was NOT used.** Every push ran the pre-push gate, including the new workspace-root
|
| # | What | Observable |
|
||||||
assertion. CI runs were confirmed green by head_sha for each push.
|
|---|---|---|
|
||||||
|
| 1 | **The page rendered on a genuinely stranded box — no arrangement needed** | demo-felhom 9201 is really in shape (b) (`orphaned=True, status=error, snapshots=0`). `GET /launcher` → **`302 Location: /recovery`**; `GET /recovery` → 200 with the code form; both MANDATORY sentences present in the rendered bytes (transported base64 so the ssh→pct chain could not mangle an accented assertion). |
|
||||||
|
| 2 | **A healthy box shows no page** | demo-hp 9201 (`orphaned=False, status=ok, 6 snapshots`, image `0.200.0`): `GET /launcher` → **no redirect**, and the backups page carries **0** occurrences of `href="/recovery"`. |
|
||||||
|
| 3 | **A wrong code is refused, nothing written, no lockout** | Three consecutive wrong codes → `200` each time, the form still present each time, the Hungarian above rendered, **0** occurrences of `age:` / `passphrase` / `unseal` in the page, and the `offbox/` directory listing **byte-identical before and after**. The controller log shows the failure came from the AGENT (`POST /escrow/recover-offsite-password: HTTP 400: the recovery code did not open the sealed bundle`), i.e. the whole chain — handler → agent → hub fetch → age KDF — ran live. |
|
||||||
|
| 4 | **The correct code unlocking + the listing** | **NOT RUN LIVE, and it cannot be here.** No recovery code was kept for demo-felhom's orphaned history (`RECON-offsite-dr-chain-2026-08-04.md` §12.3: *"no recovery code was kept"*), and demo-hp's drill code is operator-held out of band. It rests on `TestRecovery_C_UnlockOpensAndLists` (install + listing + "restores nothing"), `…EmptyStoreSaysSo`, and `TestRecovery_G_PageAndCLIShareOneCore` (installed → unchanged on re-run). |
|
||||||
|
| 5 | **The code persists nowhere — with the positive control** | 0 hits in every file under the data volume, 0 in the container log, 0 in the debug ring. **The positive control earned its keep:** the first sweep ran against the CONTAINER path, which does not exist in the guest, so its "0 hits" proved nothing — the planted copy was NOT found, which exposed the mis-aimed sweep. Re-run against the real path: plant found (**1**), removed (**0**), and the real sweeps stayed at 0. |
|
||||||
|
|
||||||
## 7. Observations — noticed, NOT acted on
|
**Teardown:** nothing was arranged, so nothing needed reversing — demo-felhom is exactly as found
|
||||||
|
(`recovery_notice_postponed` unset, offbox enabled, `repo_state=orphaned`, repository password
|
||||||
|
present). Helper scripts removed from both guests.
|
||||||
|
|
||||||
- The workspace-root assertion is per-clone like the rest of the hook, so it binds only an armed
|
## 8. Registers
|
||||||
clone; CI remains the half that is neither per-clone nor skippable (R-168).
|
|
||||||
- `felhom-agent`'s `reuse-refs` gate incidentally also fails outside the workspace (the shared checker
|
- **R-193 → CLOSED** (credential half v0.199.0/hub v0.96.0; the screen v0.200.0), with the two
|
||||||
lives in the `felhom.eu` sibling). Repo-specific and not a substitute for the assertion.
|
operator rulings recorded and the shape-(b) reasoning stated.
|
||||||
|
- **R-213 → NEW**: putting files back in place, with the **live-versus-backup comparison** named as its
|
||||||
|
requirement. Deliberately not started.
|
||||||
|
- **R-202 stays open**, untouched.
|
||||||
|
- The register's highest ID was R-212 on arrival **and still is** — re-checked for the second writer.
|
||||||
|
|
||||||
|
## 9. The capability-map row as it now reads
|
||||||
|
|
||||||
|
All four drill interventions closed **and** the customer is now offered the recovery. The old
|
||||||
|
*"needs someone who knows to look"* qualifier is **gone**. What remains, stated narrowly: (1) the final
|
||||||
|
unlock has never been driven with a **correct** code through the page; (2) **putting files back is
|
||||||
|
deliberately not part of this** (→ R-213); (3) **the journey has not been re-walked end to end** since
|
||||||
|
these fixes — one more drill is what is owed.
|
||||||
|
|
||||||
|
## 10. CI and hooks
|
||||||
|
|
||||||
|
CI green by `head_sha` for both pushes. **`--no-verify` was NOT used**; every push ran the pre-push
|
||||||
|
gate, including the workspace-root assertion added yesterday.
|
||||||
|
|
||||||
|
## 11. Observations — noticed, NOT acted on
|
||||||
|
|
||||||
|
- **The postpone flag is never cleared.** If a box leaves and re-enters the stranded state, a customer
|
||||||
|
who once chose „most nem" will not be interrupted again. The entry point still shows, so the route
|
||||||
|
is intact; clearing it on a transition is a judgement call I did not make unasked.
|
||||||
|
- **`/backup/offbox/reset` redirects to `/backups/remote`, not back to the recovery page.** Correct —
|
||||||
|
the situation has ended — but the customer lands on a page whose recovery banner has just
|
||||||
|
disappeared, with no explicit "done" message. Worth a copy pass with the put-back work.
|
||||||
|
- **`RunRecoveryCheck` was left alone**, as instructed, so it still contains its own fetch+compare. It
|
||||||
|
is compare-only and writes nothing, so it cannot diverge dangerously — but it is now the ONE recovery
|
||||||
|
path not routed through `RecoverInstallCore`.
|
||||||
|
|||||||
Reference in New Issue
Block a user