Files
felhom-controller/REPORT.md
T

5.6 KiB

REPORT — controller v0.214.0: the screen stops hedging about a code it can now check (R-311, 2026-08-12)

The premise, corrected before anything was written

The task described the customer being told "the recovery code did not open the sealed bundle". That is the agent's local-API reply — machine-facing English. The customer-facing screen already hedged (R-222/R-226): it named both possible causes, named the kept package and its date, and said „innen nem tudjuk megkülönböztetni őket" — we cannot tell them apart from here.

That sentence was honest. It was also a statement about our own incuriosity: it could not tell them apart because nothing ever looked. Agent v0.129.0 looks, so the hedge becomes an answer. That is what shipped — a smaller and more precise change than "stop the lie", and stated as such.

Verified at file:line against live source, per rule 1, including the claims in the prompt itself.

What changed

  • internal/agentapi/features.goFeatureRetainedRecoveryClass, MinAgent 0.129.0, with the featureProbes row the version path requires (a featureMinAgent row without one is never consulted).
  • internal/agentapi/escrow.go — class RecoveryCodeOpensRetained on 422. ClassifyRecoveryFailure now takes (err, trustRefusal, trustRetained). Two gates, not one: they name different agent versions (0.126.0, 0.129.0) and a box can sit between them, where a 422 is a shape we did not design. The compiler found every call site.
  • internal/web/recovery_handlers.go — the new case and its message; recoveryRetainedTrusted.
  • internal/web/server.go — the test seam field.
  • scripts/retrieval_promise_gate.py — see below.

The message, and what it deliberately will not say

„A kódod helyes, de egy korábbi csomagot nyit meg, nem azt, amit most őrzünk ehhez a géphez. A géped időközben új mentési kulcsot kapott. A korábbi csomagot (dátum) nem töröltük, megőrizzük — a mostani mentéseidet ez nem érinti, azokkal semmi nem történt. A régebbi előzményed visszanyitásához a Felhom ügyfélszolgálatának segítsége kell: írj nekik, és add meg, hogy a régi mentéseidhez szeretnél hozzáférni. A kódodat tedd el, szükség lesz rá."

Bytes verified as hex: á c3 a1 · é c3 a9 · í c3 ad · ó c3 b3 · ö c3 b6 · ú c3 ba · ü c3 bc · ő c5 91 · — e2 80 94. No emoji. 443 chars / 487 bytes.

It states the code is correct, names the date, says the package is kept, and denies the assumption it otherwise creates — that the current backups are affected. It promises no restore: there is no in-product route to a set-aside store (R-312), and a retained package may itself predate the repository-password field. A conditional promise that turns out false on this screen is worse than saying less — R-202, on the highest-stakes copy in the product. It routes to support, which can do it: yesterday's drill did exactly that by hand.

An older agent keeps the hedged sentence. Unknown → claims less → heals itself on update.

The claim guard grew a surface, and immediately convicted something

retrieval_promise_gate.py scanned internal/web/templates only — while every recovery message is a Go string in a handler. The highest-stakes copy in the product had never been scanned. It now also scans recovery_handlers.go (Go comments stripped, as template comments are), and visszanyit joins the stems because the new message uses a fourth verb for the same claim — the gate's own history is what happens when it chases words instead of claims.

On its first run it found a PRE-EXISTING unregistered claim (RecoverRefused's "reopening would overwrite it"), now registered as an explanation rather than a promise. 8 → 10 registered claims.

Tests and red-proofs

Six handler tests asserting which sentence the customer sees. Full suite green; all 11 controller gates OK. Red-proofs, each mutation asserted to have applied:

Mutation Result
delete the RecoveryCodeOpensRetained case FAILS — customer gets the neutral message (R-224's safe default catches it; the lie itself lives in the agent)
make 422 unconditional (ignore the gate) FAILS — an agent that never looked is read as having looked
route 400 to the new class FAILS — a mistype is congratulated

Deployed

gitea.dooplex.hu/admin/felhom-controller:0.214.0 built, pushed, deployed to guest 9201 — Up … (healthy).

Live, on hardware: the agent returns HTTP 422 for the old code (opens_retained: true, superseded_at: 2026-08-12T15:18:55Z) and 400 for a wrong one. The hub serves the retained packages (200, count=2, unopenable_count=1).

What was NOT walked: the customer's rendered sentence. recoveryUnlockHandler redirects to /backups/remote when !recoveryOffer(), and this box holds its own repository password again, so it is correctly not in the offered state. Forcing it would mean removing that password to fake a rebuilt box — destabilising a healthy machine to render a sentence pinned by six handler tests whose upstream 422 is proven live. Method: endpoint-level for the agent/hub, handler-level for the message.

Correction — R-308 is WITHDRAWN. I previously reported the stored dashboard password as stale. It is not: the ~/.config/credentials values are wrapped in SINGLE quotes and I stripped only double ones, so I was sending a literal '. Unquoted correctly it logs in first try (HTTP 302 + session cookie). The same bug made this session's first live R-311 test read as a failure for twenty minutes.