REPORT + CONTEXT for v0.203.0: the retry shape, the marker answer, R-220's shape
gates / gates (push) Successful in 10s

Records the decisions rather than only the code:
- POLL not ACK, decided on Scenario B against the ACTUAL promises — the
  no-target message gives no deadline and the card says 'within a day', so a
  5-minute tick is inside both and no text needed changing. If either promise
  tightens to minutes, go ACK-driven.
- The marker question: applied_marker lives in the guest's DataDir, which a
  rebuild destroys, so it cannot suppress a legitimate re-run. Left alone.
- R-220 candidate (b), corroborated rather than a wider prefix, reading
  /proc/mounts because the lsblk args are pinned in sudoers.

Live: Scenario C proven on demo-hp WITH a positive control — the job ran once
and logged nothing. A first reading counted 2 lines that turned out to be the
start-up reconcile, not the retry; the instrument was corrected before the
conclusion. Scenarios A and E are deliberately NOT live-proven here: both need a
rebuilt box, and that state arises naturally in Part 4.
This commit is contained in:
2026-08-06 13:05:30 +02:00
parent 9dc26459ea
commit 4d349d1106
2 changed files with 96 additions and 151 deletions
+72 -151
View File
@@ -1,180 +1,101 @@
# REPORT — felhom-controller v0.202.0 (+ felhom-agent v0.126.0)
# REPORT — felhom-controller v0.203.0 (+ felhom-agent v0.127.0)
**The customer is blamed only after a real attempt refused their code.** R-224, R-226, R-225, R-227,
R-228 — all five closed. No campaign venue was rebuilt, re-ceremonied or reconfigured.
**The two dead ends that kept the recovery journey failing are closed in code.** R-218's consume half
and R-220 (both halves: the host-side detection and the customer-facing refusal).
## 1. The venue's state at the end — WORKING
## 1. R-218 — which retry shape, and why (§7.1)
`c11-36d660` ONLINE, controller **0.202.0** healthy, agent **0.126.0**, off-site `last_status: ok`,
**2 snapshots**, `stats_known: true`, the set-aside history intact at
`/home/felhom-repo.orphaned-20260805`. Every injected fault reverted; the hub route restored and
confirmed (`http=302`, `OUTPUT` chain empty).
**Poll**, on a 5-minute scheduler tick, driven from the box's own published declaration
(`OffboxReportStatus().State`) — **the very statement the hub acts on**, so the two cannot disagree
about whether a retry is wanted.
**Two changes to the venue, both restorations rather than reconfigurations, and both stated because
they were not asked for.** The previous session shredded the venue's dashboard password along with
the recovery codes, leaving the box **impossible to log into** — which a future re-walk needs. A fresh
password was set through the supported escape hatch (`--print-reset-code` → the real `/claim` form,
R-204 item 1) and stored `0600` in `~/.config/campaign11/`. And a normal off-site run was triggered to
populate the new `stats_known` field. Neither touches the recovery/escrow state the re-walk depends on.
**Decided on Scenario B, measured against the actual promises:**
## 2. Scenario A live, and its red-proof — the session's headline
| surface | what it promises |
|---|---|
| the no-target unlock message | „…**amint megvannak**, a mentéseid listája megjelenik…" — **no deadline** |
| the backups card | „…**Ha egy napon belül nem áll be**, jelezd az üzemeltetőnek." — **within a day** |
**Live, on the venue, with the SAME wrong code and only the hub's reachability changed:**
A 5-minute tick is inside both by a wide margin, and it needs **no hub change** (which §0 expected).
**No text needed changing.** If either promise ever tightens to minutes, the ACK-driven shape is the
one to reach for.
```
hub reachable → HTTP 400 "the recovery code did not open the sealed bundle — nothing was written"
hub REJECTed → HTTP 502 "the sealed recovery bundle could not be fetched from the hub —
the recovery code was NOT used and nothing was written"
hub restored → HTTP 400 (back to the first)
```
**It stops by construction** (Scenario C) and **the settle gate is kept** (Scenario D) — the retry goes
through `ReconcileWhenSettled`, unchanged.
Controls: the hub read `http=302` before, `http=000 exit=7` while blocked, `302` after. **Before this
change both answered 400 with one merged sentence** — that is CAMPAIGN-11 F3, which told a customer
holding a *correct* code that it did not open their package.
## 2. The marker question (§7.2) — answered, and left alone
**Red-proof (unit):** deleting the `ErrBundleFetch` case reproduces it exactly —
`status: got 400, want 502 — {"error":"the recovery code did not open the sealed bundle…"}`. Deleting
the controller's `RecoveryHubUnreachable` case fails Scenario A with the accusation restored.
`applied_marker` lives at `<DataDir>/offbox/applied_marker` — **inside the guest's data dir, which a
rebuild destroys.** It therefore cannot suppress a legitimate post-rebuild re-run, and it is not part
of this defect. Untouched.
## 3. Elapsed times, beside CAMPAIGN-11's originals
## 3. R-220's shape (§7.3) — corroboration, not a wider prefix
| | 2026-08-05 (before) | now |
|---|---|---|
| hub unreachable, correct code | **0.0556 s** → accusation | **502**, classified, no unseal attempted |
| agent stopped, correct code | **0.0299 s** → accusation | **transport error** → agent-unreachable class |
| genuinely wrong code ×3 | 1.194 / 1.004 / 1.014 s → accusation *without* the typing hint | **400** → the merged message **with** the ten-words prompt |
**Candidate (b)**, the claimed check distinguishing our own mount from a foreign one — because the host
and its knowledge survive the rebuild while the guest's registry does not. Candidate (a) (a cleanup
that clears the raw mounts) would have made correctness depend on a step running, and a step that does
not run leaves exactly today's defect.
**A limit, stated plainly.** The customer-facing messages were **not** re-driven end-to-end on the
venue, because `/recovery` correctly **redirects**: the previous session's F7 set the old data aside,
which retires the offer by design. Restoring that state would be exactly the "reconfigure the venue"
§11 forbids. **The agent half is proven live (above); the controller's message selection is proven by
handler tests and their red-proofs.** That split is the honest description of what was verified how.
**The discriminator is corroborated:** a non-managed mountpoint is forgiven **only when the same device
is ALSO mounted under `/mnt/felhom-drives`** — a pairing only our enrolment produces.
**`/proc/mounts`, not `lsblk MOUNTPOINTS`**, because the lsblk invocation is pinned verbatim in the
sudoers file and changing it would have coupled this fix to a config rollout.
Similarly, the correct recovery codes were shredded at the end of the previous session per its own
§7, so the live re-run used a **wrong** code. That makes the test *harder*, not weaker: with the hub
down and a genuinely wrong code, the system must still resist the easy accusation — and it does.
**The fence holds and has its own test** (Scenario F): `/srv/data`, `/media/photos`,
`/mnt/someone-elses-disk`, `/var/lib/other` are all still refused. **Fail-safe:** an unreadable mount
table corroborates nothing.
## 4. Every changed Hungarian string, for review as copy
## 4. Every changed Hungarian string
**New — hub unreachable:**
> „Most nem sikerült elérni a Felhom központi rendszerét, ezért a mentéseidet nem tudtuk megnyitni. A
> kódodat NEM használtuk fel, és semmi nem változott — tedd el biztonságos helyen, és próbáld újra
> néhány perc múlva. Ha egy óra múlva sem megy, szólj a Felhom ügyfélszolgálatának."
**The deploy refusal (R-220, Scenario G).** Was:
**New — the machine's own service unreachable:**
> „A gép házon belüli szolgáltatása most nem válaszol, ezért a mentéseidet nem tudtuk megnyitni. A
> kódodat NEM használtuk fel, és semmi nem változott — tedd el biztonságos helyen. A gép magától
> rendbe jöhet; próbáld újra néhány perc múlva, és ha egy óra múlva sem megy, szólj a Felhom
> ügyfélszolgálatának."
> „A megadott tárhely nem azonosítható regisztrált meghajtóként, ezért alkalmazás adatkönyvtáraként nem
> használható. **Válasszon a listából csatlakoztatott meghajtót.**"
**New — the hub holds no package:**
> „Ehhez a géphez nem őrzünk lezárt csomagot, ezért nincs mit megnyitni. Ez nem a kódoddal van
> összefüggésben. Ha korábban készültek házon kívüli mentéseid, szólj a Felhom ügyfélszolgálatának."
Now:
**New — the package predates the field:**
> „A kódod megnyitotta a csomagot, de az még nem tartalmazza a házon kívüli tárhely kulcsát — régebben
> készült, mint amikor ezt elkezdtük belerakni, és utólag nem pótolható. A kódoddal semmi baj. Keresd
> a Felhom ügyfélszolgálatát."
> „A megadott tárhely nem azonosítható regisztrált meghajtóként, ezért alkalmazás adatkönyvtáraként nem
> használható. **Ha a gépet nemrég telepítettük újra, a meghajtóid megvannak, de még nincsenek újra
> csatlakoztatva ehhez a géphez — a Tárhely → Meghajk oldalon csatlakoztathatod őket, és utána
> indítsd újra a telepítést. Ha ott sem látszanak, keresd a Felhom ügyfélszolgálatát.**"
**New — the neutral default:**
> „A művelet nem fejeződött be, és nem tudjuk biztosan, miért. Semmi nem változott, és a mentéseid
> érintetlenek. Próbáld újra néhány perc múlva — ha másodszorra sem sikerül, szólj a Felhom
> ügyfélszolgálatának."
**No other customer-facing string changed.** The NAS refusal is a different situation and is untouched.
**CHANGED — the retained-earlier-package message (R-226 adds the mistype clause):**
> „Ez a kód **nem nyitotta meg** azt a csomagot, amit most őrzünk ehhez a géphez. **Két oka lehet, és
> innen nem tudjuk megkülönböztetni őket. Lehet elgépelés: ellenőrizd, hogy mind a tíz szót pontosan,
> szóközökkel elválasztva írtad-e be — a kis- és nagybetűk nem számítanak. Vagy** 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. **Ha újrapróbálod és úgy sem megy,** és 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."
## 5. Tests and red-proofs — every one demonstrated failing, then restored
**NEW — the set-aside notice (R-228):**
> „**A korábbi mentéseid félre vannak téve — nem töröltük őket.** Amikor új mentési kulcsot kapott a
> géped, a régebbi előzményt átmozgattuk a távoli tárhelyen, és ott is maradt. **Megnyitni innen
> egyelőre nem lehet**, és ez nem a kódodon múlik. Ha szükséged van rá, keresd a Felhom
> ügyfélszolgálatát."
| Scenario | Test | Red-proof mutation | Result |
|---|---|---|---|
| **A** | a credential staged after start-up is collected | make `RetryIfDeclared` return `(false, nil)` — the pre-v0.203.0 world | **FAILS**: "the box declared a need and no reconcile was attempted" |
| **C** | a healthy box does nothing, and logs nothing | drop the `!declared()` stop condition | **FAILS**: a healthy box reconciles |
| **D** | the settle gate holds on the retry path | call `Reconcile` instead of `ReconcileWhenSettled` | **FAILS**: "SETTLE GATE BYPASSED" |
| **E** | the customer's own drive is offered after a rebuild | remove the `felhomOwnedMounts` exemption | **FAILS**: "device is mounted at /mnt/adatok (sdb)" |
| **F** | a genuinely foreign mount is still refused | over-widen to any `/mnt/*` path | **FAILS**: "/mnt/someone-elses-disk was offered for formatting" |
| **G** | an empty list explains itself | restore the old sentence | **FAILS**: "the refusal tells the customer to choose from a list that may be empty" |
**CHANGED — the set-aside confirmation bullet (§7.6, it over-promised):**
> was: „a helyreállítási kód nélkül **többé nem lesznek megnyithatók**"
> now: „a félretett mentések **innen többé nem nyithatók meg** — sem kóddal, sem anélkül"
Plus: a nil bridge is a silent no-op; the corroboration requires both mounts of the **same** device,
and another device's managed mount does not vouch for a foreign one.
**NEW — R-225's unknown states:** „a pillanatképek száma még ismeretlen" ·
„még nem tudjuk, mennyi van a tárolóban — legfeljebb N GB"
**Green:** controller **28 packages ok**, agent **29 packages ok**, both `go vet` clean, all gates OK
in both repos.
**NEW — R-227's gateway message:** „A gép éppen újraindul, ezért most nem tudtuk befejezni a
műveletet. Semmi nem változott. Várj néhány másodpercet, és próbáld újra — a kódodra továbbra is
szükséged lesz, úgyhogy tartsd kéznél."
## 6. Live checks (§7.4)
## 5. How `rerr` was classified, and what it cost
**Deployed to demo-hp** from the **published** bytes (agent sha verified against the artifact before
installing): agent **0.127.0**, controller **0.203.0**, healthy.
**From the value. It could not be done in this repo alone, and that is the cost.**
**Scenario C — PROVEN LIVE, with a positive control.** The retry job is registered
(`Registered periodic job: offsite-credential-retry (every 5m0s)`), **ran once**
(`Running job: offsite-credential-retry` = 1) and produced **zero** log lines on a box whose tier is
configured. The positive control matters: without it, silence could have meant the job never fired.
*(A first reading counted 2 lines and looked like noise; they were the START-UP reconcile's settle-gate
lines, not the retry job — the instrument was corrected before the conclusion.)*
`RecoverInstallCore` returns the agent's error verbatim, and the agent answered **HTTP 400 with one
sentence** for both a failed fetch and a wrong code. **No value available to the controller separated
them** — Scenario A and Scenario C were mutually unsatisfiable. The task scoped `felhom-agent` as
untouched; its §5 (*"if the step is not recoverable from the value, make it so — and say what that
cost"*) and §4.3 (*the source wins*) authorise the expansion, and the source forced it.
**Scenarios A and E are NOT live-proven yet, and deliberately so.** Both need a *rebuilt* box: A needs
one declaring `needs_credential` (no target), E needs one whose registry lost its drives while the raw
mounts survived. Contriving either on a demo box means dismantling its real off-site state; the state
arises **naturally in Part 4**, which is where they belong and where the last re-walk measured them.
**The cost:** a second repo, **agent v0.126.0**, published and installed on the venue
(`7ecf8e9cdba237bc…`, verified by independent download); a new `escrow.ErrBundleFetch` sentinel; a
`MinAgent 0.126.0` coupling; and **an agent version that is deliberately NOT vouched** — so new
installs are unaffected until the operator vouches, and boxes on 0.125.0 simply degrade to the neutral
message. Verified live: demo-hp runs controller 0.202.0 on agent **0.125.0**, healthy.
## 7. Not changed
Within the controller, `agentapi.RecoveryRefusal` carries the status; `ClassifyRecoveryFailure` is the
single mapping point; **no branch reads the error text.** A test asserts the same sentence under two
statuses classifies two ways.
## 6. §7.5 — which layer answers the gateway error
**traefik**, and **this repo generates its config** (`internal/infra/templates/traefik*.tmpl`). So the
fix *could* live here — but traefik v3 serves no static files, so a branded page would need a **new
always-up container** holding an error page for every 502 on the box. That is out of proportion to
this finding and is **scoped, not built**. Shipped instead: the sanctioned client-side alternative —
the unlock posts via `fetch` and answers a gateway failure in Hungarian in-page. **Progressive
enhancement: with no JS the plain POST is unchanged and still shows the proxy's error.**
## 7. What is true about the set-aside store today (§7.6)
**It cannot be opened by anyone** — not the customer, not the operator. Serving a superseded package
is an unbuilt link (R-199's inventory). The screen therefore states two facts and stops, and the
**confirmation copy was corrected** because it implied that *with* the code the history could be
reopened. The field's own comment called it "recovery-code-recoverable" — the same over-promise in the
code, also corrected.
## 8. Tests and red-proofs
`go build` · `go vet` clean · **28 controller packages ok** · **29 agent packages ok** · all controller
gates OK · all agent gates OK.
| Scenario | Red-proof mutation | Result |
|---|---|---|
| A | delete the `RecoveryHubUnreachable` case | **FAIL** — accusation returns |
| A (agent) | remove the `%w` join / delete the handler case | **FAIL** — 400 with the wrong-code sentence |
| C | remove the mistype clause | **FAIL** — ten-words prompt unreachable again |
| D | default to the accusing message | **FAIL** — unclassifiable blames the customer |
| E | route an instant transport failure to the typing message | **FAIL** on that row |
| F | remove the `StatsKnown` guards | **FAIL** — "an unread store reports a snapshot COUNT of zero" |
| H | delete the set-aside block | **FAIL** — "the set-aside history is not mentioned at all" |
**Two existing tests encoded the defect and were corrected, not deleted** — the web fake returned a
*bare* error for "wrong code" (the shape of an unclassifiable failure), and R-222's test forbade **any**
mention of typing on a superseded box, half of which R-226 deliberately reverses.
**A third test named the defect and did not prevent it.** The agent's
`TestRecoveryOffsiteRepoPassword_FetchErrorIsDistinct` has asserted since v0.125.0 that *"the operator
must not be sent to re-read their recovery code because the hub was unreachable"* — and passed
throughout, because it checked an error **string** one layer below where the merge happened. Mechanism
asserted, consequence unpinned.
## 9. Observations, not acted on
- **The Day-0 manifest still vouches agent 0.125.0.** A new box therefore lands on the old agent and
gets the neutral message rather than the typing hint. Vouching is deliberately the operator's act.
- **R-202's orphan card still promises recoverability** („a hozzá tartozó helyreállítási kóddal később
visszaállítható lehet"), which is the same over-promise this session corrected two doors away. It is
explicitly left open and is now the last place on that surface still saying it.
- The recovery screen's seal date still renders a raw RFC3339 string to a Hungarian household, in two
different formats on the same screen. Cosmetic; recorded in CAMPAIGN-11 and untouched.
No hub change. No sudoers, no allowlisted command, no PVE surface. The settle gate, the marker, every
other claim signal, and the NAS refusal are untouched.