a62bb3874b
gates / gates (push) Successful in 9s
CONTEXT gains the rule so it outlives the bug: on the unlock path the customer is blamed only after a real attempt REFUSED their code; every other outcome, including an unclassifiable one, says something else. Plus the two things that must not be 'fixed' into it — elapsed time is never a classifier, and the error TEXT is never read (when the distinction was not a value, the agent was changed to provide one). REPORT states the split honestly: the AGENT half is proven live on the venue (400 -> 502 -> 400, same wrong code, only the hub's reachability changed), while the controller's message selection rests on handler tests and red-proofs, because /recovery correctly redirects since F7 set the old data aside and restoring that state is the reconfiguration §11 forbids. Also records that the correct codes were shredded by the previous session, so the live re-run used a WRONG code — which makes the test harder, not weaker. Two venue changes stated because they were not asked for, both restorations: a fresh dashboard password (the previous session shredded it, leaving the box impossible to log into) set through the supported --print-reset-code escape hatch, and one normal off-site run to populate stats_known.
181 lines
12 KiB
Markdown
181 lines
12 KiB
Markdown
# REPORT — felhom-controller v0.202.0 (+ felhom-agent v0.126.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.
|
||
|
||
## 1. The venue's state at the end — WORKING
|
||
|
||
`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).
|
||
|
||
**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.
|
||
|
||
## 2. Scenario A live, and its red-proof — the session's headline
|
||
|
||
**Live, on the venue, with the SAME wrong code and only the hub's reachability changed:**
|
||
|
||
```
|
||
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)
|
||
```
|
||
|
||
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.
|
||
|
||
**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.
|
||
|
||
## 3. Elapsed times, beside CAMPAIGN-11's originals
|
||
|
||
| | 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 |
|
||
|
||
**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.
|
||
|
||
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.
|
||
|
||
## 4. Every changed Hungarian string, for review as copy
|
||
|
||
**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."
|
||
|
||
**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."
|
||
|
||
**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."
|
||
|
||
**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."
|
||
|
||
**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."
|
||
|
||
**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."
|
||
|
||
**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."
|
||
|
||
**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"
|
||
|
||
**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"
|
||
|
||
**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."
|
||
|
||
## 5. How `rerr` was classified, and what it cost
|
||
|
||
**From the value. It could not be done in this repo alone, and that is the cost.**
|
||
|
||
`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.
|
||
|
||
**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.
|
||
|
||
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.
|