90 lines
5.6 KiB
Markdown
90 lines
5.6 KiB
Markdown
# 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.go` — `FeatureRetainedRecoveryClass`, **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.
|