REPORT: v0.207.0 — R-249 before/after on a live box, the census, and what was NOT proven live
gates / gates (push) Successful in 18s
gates / gates (push) Successful in 18s
Records the deliverables: the raw response body before (1 occurrence, v0.206.0) and after (0, v0.207.0) with a positive control in both directions; the §7.1 census finding two more instances of the render-then-hide pattern (R-254, one a real per-install secret); §7.2's decision and why the promise was the wrong half; every changed Hungarian string; all eight tests with their red-proof outcomes. States plainly what was NOT proven live: R-252/R-253's notices could not be rendered on VM 325 because both states are rebuild-only and the box re-registers a drive on restart — the live run therefore exercised Scenario E instead, and the notices are pinned at the template + predicate level with red-proofs. Also records that red-proof D caught a fault in my own work: the explanatory HTML comment quoted the old sentence, and HTML comments ship in the response body, so the contradiction was still on the page and the assertion forbidding it could never fail.
This commit is contained in:
@@ -1,278 +1,252 @@
|
||||
# REPORT — R-241 fixed: the box does not mint over a sealed package (2026-08-07)
|
||||
# REPORT — a secret leaves the page source, and two refusals learn to say what to do (v0.207.0, 2026-08-08)
|
||||
|
||||
**controller `v0.206.0` · hub `v0.98.0` · deployed to both demo boxes · `felhom-agent` untouched**
|
||||
|
||||
Implements the ruling in `SPIKE-r241-recovery-offer-2026-08-07.md`. **No STOP was required and none was
|
||||
taken: nothing here deletes customer data.**
|
||||
**R-249, R-252, R-253 — implementation. R-201's record corrected, R-254 filed. Controller
|
||||
`v0.206.0` → `v0.207.0`, built, pushed and deployed to `demo-hp` guest 9201 and VM 325.** No hub
|
||||
change was needed. Baselines re-read on arrival and all four repos matched the task exactly
|
||||
(`felhom-controller 3d3b4496f377`, `felhom.eu 3f4fb3825f06`); highest register ID was **R-253**.
|
||||
|
||||
---
|
||||
|
||||
## 1. SCENARIO A — the live result, and its red-proof
|
||||
## 1. Scenario A — the raw response body, before and after
|
||||
|
||||
**THE SESSION, in one measurement.** On demo-hp, controller `0.206.0`, with the hub holding a sealed
|
||||
package: the repository key was moved aside to create the rebuilt shape, and the controller restarted
|
||||
through its real start-up off-site-apply path.
|
||||
**This is the deliverable that matters, and it was measured on a live box with a positive control in
|
||||
both directions.** A known passphrase was planted on VM 325 (a throwaway due for destruction the same
|
||||
hour) and the security settings page fetched exactly as the defect was originally found — with `curl`.
|
||||
|
||||
| | v0.206.0 (the shipped defect) | v0.207.0 (the fix) |
|
||||
|---|---|---|
|
||||
| page bytes | 50 208 | 51 115 |
|
||||
| **positive control** — card renders (a passphrase IS stored) | **1** | **1** |
|
||||
| reveal control present | — | **2** |
|
||||
| **passphrase occurrences in the response body** | **1** | **0** |
|
||||
|
||||
**The positive control is load-bearing.** The first attempt at this check returned `0` on a box that
|
||||
had no passphrase stored at all — a zero that meant nothing. That is why the card's presence is
|
||||
asserted first: *an empty listing is not evidence of emptiness.*
|
||||
|
||||
**Scenario B, live:** `POST /settings/retrieval-password/reveal` → **200**, `Cache-Control: no-store`,
|
||||
body 85 bytes carrying the correct passphrase (**1** match against the planted value). The controller
|
||||
logged `retrieval passphrase revealed via the security page from 192.168.0.100 (value never logged)`,
|
||||
and a grep of five minutes of controller log for the value itself returned **0**.
|
||||
|
||||
### The red-proof — the most important one here
|
||||
|
||||
Both halves of the fix were reverted (`data["RetrievalPassword"] = …` restored in `handlers.go`, and
|
||||
`{{.RetrievalPassword}}` put back inside a `display:none` span), **each mutation asserted to have
|
||||
applied** before running:
|
||||
|
||||
```
|
||||
=== 1. was a key minted on the startup offsite-apply path? ===
|
||||
NO repo_password — the guard held
|
||||
applied_marker known_hosts repo_password.r241-live-check ssh_key
|
||||
BOTH MUTATIONS APPLIED — verified by string change
|
||||
--- FAIL: TestSecurityPage_DoesNotContainTheRetrievalPassphrase (0.08s)
|
||||
R-249: the retrieval passphrase is in the response body of the security page …
|
||||
```
|
||||
|
||||
**And the state it reports instead**, read from the hub's stored report, not from the box:
|
||||
Restored; residue checked (`{{.RetrievalPassword}}` occurrences: **0**); green again.
|
||||
|
||||
```json
|
||||
{"enabled": false, "escrow_state": "escrowed", "state": "awaiting_recovery_key",
|
||||
"snapshot_count": 0, "repo_size_bytes": 0, "quota_gb": 0}
|
||||
```
|
||||
|
||||
`enabled:false` is what keeps every existing hub reader inert; the string is what names the difference.
|
||||
**The key was then restored and re-hashed: `8a9e33aa4da6769c5aea1831f87759e10930e2ec1dea0062576484e0598d080a`
|
||||
— byte-identical to before.** The box is healthy on `0.206.0`.
|
||||
|
||||
**RED-PROOF A — the fresh key returns under the mutation.** The guard block was deleted from
|
||||
`WriteOffboxSecrets`, the mutation was confirmed present in the file, and both Scenario A tests failed:
|
||||
|
||||
```
|
||||
offbox_mintguard_r241_test.go:82: R-241 REGRESSION: apply minted a repository password over the sealed package
|
||||
--- FAIL: TestR241_ScenarioA_NoMintWhenHubHoldsSealedPackage
|
||||
--- FAIL: TestR241_ScenarioA_ApplyOffsiteTargetHoldsInsteadOfMinting
|
||||
--- PASS: TestR241_ScenarioB_FirstTimeBoxStillMints ← the mutation is SPECIFIC
|
||||
```
|
||||
|
||||
**RED-PROOF B — the over-broad fix.** Dropping the `GetHubEscrowIdentityPresent()` conjunct made
|
||||
Scenario B fail (*"a first-time box must mint exactly as before"*) while Scenario A still passed. The
|
||||
guard is a conjunction because both failure directions are real.
|
||||
**Why the tests assert the body and not a view:** the old markup hid the value with CSS. Every test
|
||||
that asked *what does the customer see* passed while the bytes carried the secret. A test that cannot
|
||||
see a `display:none` span cannot see this defect at all.
|
||||
|
||||
---
|
||||
|
||||
## 2. Q4's seven rows, mapped to the three states
|
||||
## 2. §7.1's census — the pattern found once, looked for everywhere
|
||||
|
||||
| # | Q4 state | resolves to | note |
|
||||
**It was worth doing: two more instances, one with a real secret.**
|
||||
|
||||
| Where | What | Verdict |
|
||||
|---|---|---|
|
||||
| **`app_info.html:185`** | an app's auto-generated first-login password in `<span … hidden>{{.InitialCreds.Password}}</span>` beside a „Megjelenítés" button | **the same defect with a real per-install secret** — `ReadInitialCredentials` reads it live out of the deployed container, so it is *not* the catalog's published `default_creds` |
|
||||
| `deploy.html:482` | an auto-generated `type: secret` field in `<input type="password" … value="{{$val}}">` | weaker — the **pre-deploy** form must post the value; on an **already-deployed** app's page the exposure is gratuitous |
|
||||
|
||||
**Filed as R-254, not fixed** — scope was R-249/R-252/R-253, and each needs its own reveal endpoint
|
||||
and its own body-asserting test rather than a shared quick edit. **Recommended next**, because R-249
|
||||
proved the pattern is not theoretical.
|
||||
|
||||
**Rotation advice, left to the operator:** the `walk5` instance is compromised (it reached a
|
||||
transcript) and dies with that customer's teardown. **No other instance is known to have been read —
|
||||
but nothing records a read, which is itself part of the defect**, and that is the honest state of the
|
||||
evidence. If any customer page has been screen-shared, saved or proxied, treat that passphrase as
|
||||
exposed.
|
||||
|
||||
---
|
||||
|
||||
## 3. §7.2's decision — which half of the reinstall promise was wrong
|
||||
|
||||
**The promise was wrong, and the reason is structural rather than a matter of effort.**
|
||||
|
||||
`reconstituteOffbox` refuses when `GetStackHDDPath(stack)` is empty, because **the destination of a
|
||||
restore is the app's own data path — a drive the CUSTOMER chooses at deploy time.** An automatic
|
||||
reinstall would mean the product picking that drive for them, which is precisely the decision this
|
||||
recovery path exists to leave with them (`DOMAIN` and `SUBDOMAIN` would have to be invented too).
|
||||
|
||||
So the smaller change is also the correct one: the copy now says to install first and routes there.
|
||||
The row's own code comment already read *"restore in place vs. reinstall first"* — the behaviour was
|
||||
right and the sentence had drifted.
|
||||
|
||||
---
|
||||
|
||||
## 4. Every changed Hungarian string, for review as copy
|
||||
|
||||
**Removed** (`backups_restore.html`) — the promise the restore could not keep:
|
||||
|
||||
> „Nincs telepítve — a visszaállítás előbb újratelepíti."
|
||||
|
||||
**Added** (`backups_restore.html`, the not-installed row):
|
||||
|
||||
> „Nincs telepítve — előbb **telepítsd újra**, utána hozhatod vissza az adatait." *(„telepítsd újra"
|
||||
> links to `/stacks/<app>/deploy`)*
|
||||
|
||||
**Added** (`backups_restore.html`, the precondition notice — renders only when no drive is registered):
|
||||
|
||||
> „**Előbb csatold vissza az adatmeghajtót.** A mentéseid megvannak, és a meghajtók is megvannak —
|
||||
> újratelepítés után viszont a gép még nem ismeri őket, ezért most nincs hová visszaállítani. Ez két
|
||||
> kattintás: **Tárhely → Meghajtók**, „Meglévő meghajtó csatolása". Utána gyere vissza ide."
|
||||
|
||||
**Changed** (`offbox_restore.go`, the drive refusal):
|
||||
|
||||
> was: „nincs elérhető adatmeghajtó a visszaállításhoz"
|
||||
> now: „nincs regisztrált adatmeghajtó, ezért nincs hová visszaállítani — a meghajtók megvannak, csak
|
||||
> újra kell csatolni őket a Tárhely → Meghajtók oldalon, utána ez a visszaállítás működni fog"
|
||||
|
||||
**Changed** (`offbox_reconstitute.go`, the not-installed refusal):
|
||||
|
||||
> was: „a(z) %s nincs telepítve — előbb állítsd helyre az alkalmazást, utána az adatokat"
|
||||
> now: „a(z) %s nincs telepítve, ezért nincs hová visszaállítani az adatait — telepítsd újra az
|
||||
> alkalmazást (Alkalmazások), utána ez a visszaállítás működni fog"
|
||||
|
||||
**Added** (`settings_security.html`, reveal failure): „A visszaállítási jelszó lekérése nem sikerült."
|
||||
**Added** (reveal endpoint, nothing stored): „Ezen a gépen nincs tárolt visszaállítási jelszó."
|
||||
|
||||
---
|
||||
|
||||
## 5. Tests and red-proofs — each mutation asserted to have applied
|
||||
|
||||
| Test | Scenario | Result | Red-proof (mutation → observed) |
|
||||
|---|---|---|---|
|
||||
| 1 | never had off-site backups | **settled** | fact 1 fails; nothing offered |
|
||||
| 2 | pristine rebuild, credential not yet arrived | **offered** (shape a) | **no longer a closing window** — the mint guard means it does not end by itself |
|
||||
| 3 | self-healed with a fresh key, hub holds the older package ← **the venue** | **offered** (shape c) | **the row R-241 was, and it now cannot be entered at all** — the guard prevents the key |
|
||||
| 4 | healthy, key matches | **settled** | shape (c) compares and matches |
|
||||
| 5 | re-escrowed, old package retained | **settled** | unchanged; the retained package is still unreadable (R-199) |
|
||||
| 6 | orphaned (a run proved it) | **offered** (shape b, corroborated by c) | (b) retained as a corroborator |
|
||||
| 7 | customer set the old data aside | **abandoning**, then **settled** | the spike's trap: (c) alone would re-offer for ever. The countdown resolves it by removing both halves |
|
||||
| `TestSecurityPage_DoesNotContainTheRetrievalPassphrase` | **A** | PASS | value re-rendered into the page → **FAIL**, printing the exposure |
|
||||
| `TestSecurityPage_NoCardWhenNoPassphraseStored` | A | PASS | — (gate is on existence, not the value) |
|
||||
| `TestRevealEndpoint_ReturnsThePassphraseToAnAuthenticatedCaller` | **B** | PASS | remove the route/handler → customer cannot obtain it at all |
|
||||
| `TestRevealEndpoint_404sWhenNothingStored` | B | PASS | — |
|
||||
| `TestRestorePage_NoRegisteredDrive_NamesTheReasonAndTheRoute` | **C** | PASS | precondition block deleted → **FAIL** |
|
||||
| `TestRestorePage_HealthyBox_HasNoPreconditionNotice` | **E** | PASS | notice made unconditional (`{{if true}}`) → **FAIL** |
|
||||
| `TestRestorePage_NotInstalled_DoesNotPromiseAReinstall` | **D** | PASS | old promise restored → **FAIL on all three assertions** |
|
||||
| `TestHasRestoreDestination_AgreesWithTheResolver` | seam | PASS | predicate forced `true` → **FAIL** |
|
||||
|
||||
**Every row fits.** Row 3 is the interesting one: it is now unreachable rather than merely handled —
|
||||
the fix removes the state instead of describing it.
|
||||
**Red-proof D caught a fault in my own work.** On its first run only two of its three assertions
|
||||
failed, because **the explanatory HTML comment I had written quoted the old sentence — and HTML
|
||||
comments ship in the response body.** The contradiction was still on the page, and the assertion that
|
||||
forbids it could never fail. The comment was rewritten to describe the old string without quoting it;
|
||||
re-run, all three assertions fail under the mutation. *This is the same class as the defect being
|
||||
fixed, and it was found only because the red-proof was actually run.*
|
||||
|
||||
**One pre-existing test was updated, not deleted:** `TestOffsiteRestoreList_RenderedForRebuiltBox`
|
||||
pinned the old sentence. Its intent — *state the not-installed consequence, do not hide it* — is
|
||||
unchanged; it now asserts the consequence **and** the route.
|
||||
|
||||
**Green gate:** `go build ./... && go vet ./... && go test ./...` → **rc 0**.
|
||||
**`controller_gates.py`:** all nine **OK**.
|
||||
|
||||
---
|
||||
|
||||
## 3. §7.2 — what a stale comparison resolves to
|
||||
|
||||
**A KNOWN DIFFERENCE OFFERS, however old the reading. An ABSENT HASH falls back to (a)/(b).**
|
||||
|
||||
Age is deliberately **not** gated on. Both sides of the comparison are local; only the hub's half can
|
||||
be stale, and what the hub holds does not change without a ceremony *this box* runs — which refreshes
|
||||
the hash on the next ACK. Gating on age would add a second failure mode (a box offline from the hub
|
||||
silently stops offering) to fix a window that closes itself. `CheckedAt` is persisted for diagnosis.
|
||||
|
||||
An empty hash is **not an unknown**: it is the hub positively saying its package seals no repository
|
||||
password (a legacy hash-less escrow). Offering on it would put a permanent screen in front of every
|
||||
legacy box.
|
||||
|
||||
**This path was exercised on real hardware, unplanned.** demo-hp's escrow row carries
|
||||
`stale_at = 2026-08-04 20:15:49` from the R-201 drill, so the hub withholds the hash — and the live box
|
||||
recorded `hub_escrow_key_sha256 = ""` with `checked_at` set. It correctly did **not** offer. On
|
||||
demo-felhom, where the hub does serve it, the recorded hash is **byte-identical to the local key**:
|
||||
## 6. §7.5 — the currency gate went red, as predicted
|
||||
|
||||
```
|
||||
local key hash = c60c8bc737a6b7c6647c7849283f52087f650a885babedb4ef5fdf9a5c9543cb
|
||||
hub_escrow_key_sha256 = c60c8bc737a6b7c6647c7849283f52087f650a885babedb4ef5fdf9a5c9543cb
|
||||
newest released controller : 0.207.0
|
||||
newest golden baked : 0.206.0
|
||||
GOLDEN CURRENCY GATE FAILED
|
||||
```
|
||||
|
||||
**The fact that was computed on every ACK and kept nowhere is now on disk, on a live box.**
|
||||
**The `felhom-controller` push needed no bypass** — that repo's own gates passed. **The `felhom.eu`
|
||||
push used `git push --no-verify`**, declared in that commit message, in this report, and in
|
||||
**R-242's row**.
|
||||
|
||||
**A bypass and NOT a waiver, deliberately.** The gate offers a waiver for a release that *deliberately
|
||||
needs no golden*; this release needs one. Recording a waiver would have made the register say
|
||||
something untrue. **Owed: bake golden 0.207.0 and vouch it.**
|
||||
|
||||
---
|
||||
|
||||
## 4. §7.3 — the „Helyreállítási kód létrehozása" button
|
||||
## 7. Part 3 — the record
|
||||
|
||||
**Made UNAVAILABLE while a recovery is outstanding, not merely captioned.** Creating a new code seals
|
||||
the current key, demotes the package that opens the earlier history to retained custody no shipped
|
||||
path can read (R-199), **and re-enables the recovery screen through the orphan route while
|
||||
invalidating the code that screen accepts** — a trap that looks like progress.
|
||||
**R-201's state field read `PASSED + PROVEN-LIVE 2026-08-04` — the *data* half, from the drill** —
|
||||
while the row body carried the fifth walk's journey pass. Corrected to
|
||||
**`BOTH HALVES PASSED — DATA 2026-08-04, JOURNEY 2026-08-07`**, with what it does **not** claim stated
|
||||
in the field itself: the journey is not smooth, and the discriminator's positive half is unproven.
|
||||
|
||||
A warning beside a button is a warning people click past. The card now explains and points at
|
||||
`/recovery`, where both real choices live.
|
||||
**The census asked for: a sweep of all 146 register rows found NO OTHER state field disagreeing with
|
||||
its own body.** Two candidates (R-229, R-230) were **false positives** — per-*leg* closes on rows that
|
||||
legitimately remain open.
|
||||
|
||||
**So the pattern is real but not currently widespread**: R-218 on 2026-08-06, R-201 now, nothing else
|
||||
outstanding. **The mechanism is worth naming, because it will recur:** a row states status in *two*
|
||||
places — a bold phrase early in the What column and the State cell at the end — and a session that
|
||||
closes an item updates the body and the end cell, while the early phrase, **which is what a reader
|
||||
sees first**, goes stale.
|
||||
|
||||
---
|
||||
|
||||
## 5. Every changed Hungarian string
|
||||
## 8. Live validation — what was proven on a box, and what was not
|
||||
|
||||
**The abandon confirmation** (`recovery.html`) — §2.4. It used to promise *„félretesszük — nem
|
||||
töröljük"*, which after this change would be false:
|
||||
|
||||
> „a korábbi mentéseket **most félretesszük**, és **{N} nap múlva véglegesen töröljük** — a lezárt
|
||||
> helyreállítási csomaggal együtt;"
|
||||
> „a {N} nap alatt **meggondolhatod magad**: ha előkerül a helyreállítási kódod, a mentéseid
|
||||
> visszaszerezhetők, és a törlés elmarad;"
|
||||
> „a pontos dátumot a **Távoli mentés** oldalon végig látni fogod, és emlékeztetni is fogunk;"
|
||||
> „a gép **új, üres mentési tárolót kezd**, és mostantól oda ment;"
|
||||
> „a törlés után **ez a kérdés nem jön vissza többé** — mert nem marad mit visszaszerezni."
|
||||
> „Ha csak most nincs kéznél a kódod, válaszd inkább a „Most nem" lehetőséget — az semmit nem indít el."
|
||||
|
||||
*(N is rendered from `backup.AbandonGraceDays`, never a literal in prose.)*
|
||||
|
||||
**The blocked new-code card** (`backups_remote.html`):
|
||||
|
||||
> „Ehhez a géphez **egy korábbi helyreállítási kód tartozik**, és a korábbi mentéseid még megvannak.
|
||||
> Új kód létrehozása **a régi mentéseidet elérhetetlenné tenné**, ezért most nem indítható. Előbb
|
||||
> add meg a meglévő kódodat — vagy ott jelezheted, ha nem kéred vissza a korábbi adatokat."
|
||||
|
||||
**The countdown card** (`backups_remote.html`):
|
||||
|
||||
> „**A korábbi mentések törlése folyamatban**"
|
||||
> „A kérésed szerint a korábbi távoli mentéseidet **{dátum}** napján véglegesen töröljük (még **{N}
|
||||
> nap**). Addig meggondolhatod magad: ha megvan a helyreállítási kódod, a mentéseid visszaszerezhetők,
|
||||
> és a törlés elmarad."
|
||||
> „Mégis visszaszerzem a kóddal"
|
||||
|
||||
**Post-deletion** (`backups_remote.html`):
|
||||
|
||||
> „A korábbi távoli mentéseid törlése megtörtént. A hozzájuk tartozó lezárt helyreállítási csomag
|
||||
> eltávolítása még folyamatban van."
|
||||
|
||||
**The reminder bar** (`layout.html`) — abandoning, then the undecided ladder:
|
||||
|
||||
> „A korábbi távoli mentéseidet **{N} nap múlva** ({dátum}) véglegesen töröljük, a kérésed szerint.
|
||||
> Addig még visszaszerezheted őket a helyreállítási kóddal."
|
||||
> „**Két hete** várnak rád a korábbi távoli mentéseid, és még nem adtad meg a helyreállítási kódodat.
|
||||
> Amíg nem teszed, ezekhez a mentésekhez nem férsz hozzá." *(14 days)*
|
||||
> „Már **egy hete** megvannak a korábbi távoli mentéseid, de a helyreállítási kódod nélkül nem tudjuk
|
||||
> megnyitni őket." *(7 days)*
|
||||
> „A korábbi távoli mentéseid megvannak — a megnyitásukhoz a helyreállítási kódod szükséges." *(3 days)*
|
||||
> „A korábbi távoli mentéseid megvannak, de ehhez a géphez a helyreállítási kódod szükséges." *(base)*
|
||||
> Buttons: „Megnézem" · „Most nem" · „Ne emlékeztessen újra"
|
||||
|
||||
**Hub operator event** (`offsite_abandon_purged`):
|
||||
|
||||
> „Az ügyfél korábbi távoli mentései és a hozzájuk tartozó megőrzött helyreállítási csomag is törölve
|
||||
> ({n} csomag). Az ügyfél döntése alapján, a 14 napos türelmi idő lejárta után."
|
||||
|
||||
**Controller operator event** (`offbox_abandon_completed`):
|
||||
|
||||
> „A korábbi távoli mentések a türelmi idő lejártával törlésre kerültek, az ügyfél döntése alapján. A
|
||||
> hozzájuk tartozó lezárt helyreállítási csomag eltávolítását is kértük."
|
||||
|
||||
---
|
||||
|
||||
## 6. Tests and red-proofs
|
||||
|
||||
**33 R-241 tests across three packages; full suite green in both repos.**
|
||||
|
||||
| Group | Scenario | Result |
|
||||
| # | Check | Result |
|
||||
|---|---|---|
|
||||
| A | no key minted over a sealed package; apply holds and stages nothing | PASS |
|
||||
| B | a first-time box still mints | PASS |
|
||||
| C | a differing key offers recovery (both proxies asserted false first) | PASS |
|
||||
| D | a matching key offers nothing | PASS |
|
||||
| E | abandon: aside, package kept, countdown, offer still reachable, **nothing deleted** | PASS |
|
||||
| F | the terminal step removes both halves; the declaration repeats; the close-out | PASS |
|
||||
| G | recovery inside the window cancels the countdown | PASS |
|
||||
| H | per-visit banner; opt-out silences the banner only | PASS |
|
||||
| I | the operator can extend or stop a countdown | PASS |
|
||||
| — | §7.2 both halves; shape (a) intact; fact 1 intact; Scenario-E carve-out; nil-settings fail-safe; idempotency; no-op sweep silent; unclaimed auto-reset starts no countdown; failure leaves the countdown due; levers refuse after deletion; same-site redirect | PASS |
|
||||
| 1 | `curl` the security page, passphrase absent | **PASS, live, with before/after** (§1) |
|
||||
| 2 | the reveal works for a customer who asks | **PASS, live** — 200, `no-store`, correct value, logged |
|
||||
| 3 | the two refusals rendered on VM 325 | **NOT PROVEN LIVE — stated plainly** (below) |
|
||||
| 4 | a healthy box's restore page unchanged | **PASS, live** — no notice, no not-installed hint |
|
||||
|
||||
**Nine red-proofs. Every mutation was confirmed present in the file before its result was trusted.**
|
||||
**Check 3 is the honest gap.** R-252's state is **rebuild-only**: the customer API **refuses to
|
||||
deregister the last usable drive** („ez az egyetlen használható tárhely — a leszerelés megtagadva"),
|
||||
and when `storage_paths` was emptied directly the controller re-registered one on restart, so
|
||||
`HasRestoreDestination()` was correctly `true` and the notice correctly did not render — **which is
|
||||
Scenario E passing live, not a failure.** R-253 likewise: after removing the app the controller still
|
||||
reports `deployed=true` (state `stopped`), so the row's `Installed` stays true.
|
||||
|
||||
| # | Mutation | Outcome |
|
||||
|---|---|---|
|
||||
| 1 | mint guard block deleted | Scenario A FAILS (`R-241 REGRESSION: apply minted…`); B still passes |
|
||||
| 2 | guard over-widened (hub-package conjunct dropped) | Scenario B FAILS (first-time box cannot start); A still passes |
|
||||
| 3 | `hubHash != localHash` conjunct dropped | Scenario D FAILS (healthy box offered for ever); C still passes |
|
||||
| 4 | **`RecordEscrowKeyHash` unwired in `main.go`** | wiring test FAILS — **the ships-inert shape**: everything compiles, every package test passes, the auto-confirm still works, and shape (c) reads an empty hash for ever |
|
||||
| 5 | store deletion skipped in the sweep | Scenario F FAILS (no `rm` issued) |
|
||||
| 6 | `AbandonPurgeRequested` dropped from the report | Scenario F FAILS (the hub is never asked; the package would outlive the store) |
|
||||
| 7 | `CancelAbandon` made a no-op | Scenario G FAILS (uncancellable countdown) |
|
||||
| 8 | *(covered by 5/6 — the two halves are independently proved)* | — |
|
||||
| 9 | *(covered by 2 — the guard's own failure direction)* | — |
|
||||
|
||||
**The countdown is driven by an injected clock throughout (§7.4). No live timer was shortened, and the
|
||||
terminal step has only ever run against fakes.**
|
||||
|
||||
### Two real bugs, caught by tests rather than by review
|
||||
|
||||
1. **`OffboxAwaitingRecoveryKey` omitted `t.Enabled`** — a customer who had switched off-site *off*
|
||||
would have declared a holding state. Caught by the **existing**
|
||||
`TestOffsiteDeclare_DisabledTargetIsNotStranded`. Now pinned from the new predicate's side too.
|
||||
2. **`recoveryInterrupts` returned early when the offer was false**, so the **falling** edge was never
|
||||
recorded and the next entry counted as a continuation — **the exact defect the epoch exists to fix,
|
||||
reintroduced inside the fix.** Caught by `TestR241_FullPageAppearsOncePerEntryNotOnceEver`.
|
||||
**Both notices are therefore pinned at the template + predicate level** — by tests that drive the real
|
||||
template and the real predicate, each with a demonstrated red-proof — **and not by a live render.** I
|
||||
would rather say that than dress the template test up as a live one.
|
||||
|
||||
---
|
||||
|
||||
## 7. §7.5 — the automatic ending: recorded, NOT built → **R-245**
|
||||
## 9. Files, commits, deployed version
|
||||
|
||||
**The operator's proposal:** a box offered recovery for 30 days without a decision is auto-abandoned
|
||||
into the 14-day grace.
|
||||
**`felhom-controller` — `8dbbc98`** (v0.207.0):
|
||||
`internal/web/handlers.go` · `internal/web/server.go` · `internal/web/templates/settings_security.html` ·
|
||||
`internal/web/templates/backups_restore.html` · `internal/backup/offbox_restore.go` ·
|
||||
`internal/backup/offbox_reconstitute.go` · `internal/web/offsite_restore_list_test.go` ·
|
||||
**new:** `internal/web/retrieval_password_exposure_test.go`, `internal/web/restore_preconditions_test.go`,
|
||||
`internal/backup/restore_destination_test.go` · `CHANGELOG.md` · `CONTEXT.md` · `REUSE.md` ·
|
||||
`controller/README.md`
|
||||
|
||||
**The reasoning against it, recorded with it so it can be revisited properly:** (1) **nobody is
|
||||
absent** — a box does not reinstall itself, so whoever rebuilt it met the recovery question; **a
|
||||
reinstall implies a person**. (2) **A customer who cannot find their code gets in touch**, so the
|
||||
automation would fire at people we are already talking to — which is why the *levers* were the thing
|
||||
worth building. (3) **The cost is theirs**: the old history sits in their own storage allowance. (4)
|
||||
**The real harm is QUOTA**, and **that is a condition, not a calendar** — an automatic ending should
|
||||
trigger on the harm with a dated warning, never on a date alone.
|
||||
**`felhom.eu` — `1fc3876`** (pushed `--no-verify`, declared): `documentation/backlog/OPEN-ITEMS.md`
|
||||
(R-249/R-252/R-253 closed, **R-254** filed, R-201's state field corrected, R-242 updated) · `STATUS.md`
|
||||
(93 lines).
|
||||
|
||||
**Built instead:** escalating reminders, and `--abandon-status` / `--abandon-extend=N` /
|
||||
`--abandon-stop`, both of which **refuse rather than no-op** when nothing is running or the store is
|
||||
already gone. A silent success is what an operator most easily mistakes for "handled".
|
||||
**Deployed:** `gitea.dooplex.hu/admin/felhom-controller:0.207.0` — healthy on **demo-hp guest 9201**
|
||||
and on **VM 325**.
|
||||
|
||||
**Register:** highest ID moved **R-253 → R-254**.
|
||||
|
||||
---
|
||||
|
||||
## 8. Files and commits
|
||||
## 10. What remains open
|
||||
|
||||
**`felhom-controller`** — `763de3a025a5` (mint guard) · `a491abef6c20` (discriminator) ·
|
||||
`a5d90ff80120` (countdown) · `de39e47f53be` (surface) · `72368654e421` (reminders + levers) ·
|
||||
`0a9158d53eb8` (docs). Deployed: **`0.206.0`**.
|
||||
- **R-254** — the two remaining render-then-hide instances. **The recommended next item.**
|
||||
- **R-242** — the bake + vouch of golden 0.207.0 this release now needs; and R-242's own half, that
|
||||
nothing gates the *vouch*.
|
||||
- **The discriminator's positive half** — the fifth walk exercised R-241's mint guard positively and
|
||||
the fingerprint comparison only negatively. **Proving the positive half needs a deliberate fixture
|
||||
(a box holding a divergent key), not a walk** — and v0.206.0's guard now prevents that state arising
|
||||
by itself, which is exactly why it needs constructing.
|
||||
- Untouched by design: **R-250, R-251, R-243, R-244, R-240, R-213, R-202, R-214, R-247, R-248.**
|
||||
|
||||
New: `backup/offbox_abandon.go`, `backup/offbox_mintguard_r241_test.go`,
|
||||
`backup/offbox_offer_shapec_r241_test.go`, `backup/offbox_abandon_r241_test.go`,
|
||||
`web/recovery_surface_r241_test.go`.
|
||||
Modified: `backup/offbox.go`, `backup/backup.go`, `settings/settings.go`, `report/escrow_confirm.go`,
|
||||
`report/escrow_presence_wiring_test.go`, `cmd/controller/main.go`, `web/{recovery_handlers,handlers,server,auth}.go`,
|
||||
`web/recovery_test.go`, `templates/{layout,recovery,backups_remote}.html`,
|
||||
`CHANGELOG.md`, `CONTEXT.md`, `REUSE.md`, `controller/README.md`.
|
||||
## 11. Observations — noticed, not acted on
|
||||
|
||||
**`felhom.eu`** — `ac4b2a4ba934` (hub purge) · `9657334fb72a` (registers, map, STATUS, hub CHANGELOG) ·
|
||||
`b12f8ec2f32f` (manifest). Deployed: **`felhom-hub:0.98.0`**, Synced/Healthy.
|
||||
|
||||
## 9. Registers
|
||||
|
||||
**R-241 FIXED.** **R-243 UPDATED, not closed** — the state can no longer be entered and what replaces
|
||||
it is *visible* rather than silent, but **the alarm gap is untouched**: a box whose customer never acts
|
||||
still stops backing up with no operator signal. **R-245 NEW** (WAITING-ON-OPERATOR). **R-242 stays
|
||||
recorded-not-built.** **R-244 untouched.** Still open and named: **R-240, R-213, R-202, R-214**.
|
||||
|
||||
**Highest register ID moves R-244 → R-245.**
|
||||
|
||||
## 10. The capability-map row
|
||||
|
||||
**Still FAIL.** These are fixes, not a walk — nothing here walked a customer end to end, and the row
|
||||
goes green only when one completes **with no operator intervention AND a byte-identical sentinel**.
|
||||
|
||||
## 11. CI
|
||||
|
||||
`felhom-controller` run **237** (`0a9158d53eb8`) success · `felhom.eu` runs **238** (`9657334fb72a`)
|
||||
and **239** (`b12f8ec2f32f`) success — matched by `head_sha`, pulled rather than assumed.
|
||||
**`--no-verify` was NOT used**; the pre-push hook ran the gates on every push.
|
||||
|
||||
## 12. Observations — noticed, NOT acted on
|
||||
|
||||
- **demo-hp's escrow blob is flagged stale** (`stale_at = 2026-08-04 20:15:49`, from the R-201 drill),
|
||||
so the hub withholds its hash and shape (c) can never fire there. Correct per §7.2, and it gave the
|
||||
fallback path a free live exercise — but that box's escrow **has been stale for three days** and
|
||||
nothing has surfaced it. Possibly worth a row; not filed, because it is R-198/R-196 territory and I
|
||||
did not measure whether the staleness is real or an artefact of the drill.
|
||||
- **The offer epoch advances on a landing-page visit, not on the report cycle.** Correct for the
|
||||
interruption and the banner (both only matter when someone visits), but it means the undecided
|
||||
reminder ladder starts from the first *visit* rather than from the first *report*. Deliberate;
|
||||
stated here because it is a design choice a reader could mistake for an oversight.
|
||||
- **`--abandon-*` runs inside the guest**, i.e. a guest command line. Acceptable for operators (it sits
|
||||
beside the existing operator subcommands) but it is not a hub surface, so it is unavailable to an
|
||||
operator who cannot reach the box.
|
||||
- **`shares_restore.go:62,81` carries the same „nincs elérhető adatmeghajtó" string** with no route,
|
||||
for the SMB-shares restore path. Out of scope (R-252 is the app path); the same one-line treatment
|
||||
would fix it.
|
||||
- **The reveal endpoint has no rate limit.** It is behind session auth + CSRF, so it is not a
|
||||
brute-force surface, but the escrow start handler deliberately rides the login limiter and this does
|
||||
not. Deliberate omission, flagged.
|
||||
- **The Hetzner token in `~/.config/credentials` cannot see storage box 611421** — `GET
|
||||
/v1/storage_boxes/611421/subaccounts` → 404 and `GET /v1/storage_boxes` → 200 with **0 entries**. It
|
||||
is scoped to a different project than the hub's. Recorded so the next teardown does not re-derive it.
|
||||
|
||||
Reference in New Issue
Block a user