From 4d349d1106b52502b0a35e8d66cd7142f300f9b2 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Thu, 6 Aug 2026 13:05:30 +0200 Subject: [PATCH] REPORT + CONTEXT for v0.203.0: the retry shape, the marker answer, R-220's shape MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- CONTEXT.md | 24 ++++++ REPORT.md | 223 +++++++++++++++++------------------------------------ 2 files changed, 96 insertions(+), 151 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index da6796b..5c34f7f 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -1776,6 +1776,30 @@ Last updated: 2026-06-13 (v0.60.0 backlog-Medium cleanup) --- +## THE TWO RULES THE RECOVERY JOURNEY LEANS ON (v0.203.0, 2026-08-06) + +> **1. A credential the hub stages is collected by the box, not waited for.** The reconcile that +> collects runs on a tick for exactly as long as the box's own declaration says it needs one — and +> stops the instant a target exists. It is driven from `OffboxReportStatus().State`, the same statement +> the hub acts on, so the two can never disagree about whether a retry is wanted. +> +> **2. A mount Felhom itself made is not "something else".** Enrolment mounts a drive twice — the +> managed path and a raw `/mnt/` on the host — and the host survives a guest rebuild while the +> guest's registry does not. The claimed check forgives a non-managed mount **only when corroborated** +> by the same device also being mounted under the managed path. **A genuinely foreign mount is still +> refused, and that fence has its own test.** + +**Why both are stated here rather than left in the code:** each was a dead end that kept the unaided +recovery journey failing, and each looked correct in isolation. R-218's declaration half shipped and +worked while nothing consumed what it asked for; R-220's check was right about foreign disks and wrong +about our own. **Neither is a bug in the thing it guards — both are about what runs, and when.** + +Two things that must not be "simplified" back: +- **The settle gate stays.** The retry goes through `ReconcileWhenSettled`, so the day-0 floor race is + unchanged. A retry that skipped it would trade one defect for another. +- **The R-220 exemption is corroborated, never a prefix.** Widening it to any `/mnt/*` path offers a + disk another system is using for formatting — the red-proof shows exactly that. + ## THE UNLOCK PATH'S RULE (v0.202.0, 2026-08-06) — state it before changing anything there > **On the recovery unlock path the customer is blamed only after a real attempt REFUSED their code. diff --git a/REPORT.md b/REPORT.md index 50a1e28..1be6eb2 100644 --- a/REPORT.md +++ b/REPORT.md @@ -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 `/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 → Meghajtók 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.