From ae10f648066e3a7535ab76eeada097f94b038985 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Wed, 12 Aug 2026 18:49:56 +0200 Subject: [PATCH] =?UTF-8?q?REPORT:=20controller=20v0.214.0=20=E2=80=94=20t?= =?UTF-8?q?he=20screen=20stops=20hedging,=20and=20the=20claim=20guard=20gr?= =?UTF-8?q?ew=20a=20surface?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- REPORT.md | 139 +++++++++++++++++++++--------------------------------- 1 file changed, 55 insertions(+), 84 deletions(-) diff --git a/REPORT.md b/REPORT.md index 4146fc0..abce01c 100644 --- a/REPORT.md +++ b/REPORT.md @@ -1,104 +1,75 @@ -# REPORT — v0.213.0: the banner promises only what the box can still see is true (2026-08-12) +# REPORT — controller v0.214.0: the screen stops hedging about a code it can now check (R-311, 2026-08-12) -Commit `8971256`. Green gate: build/vet clean, **28 packages ok, 0 FAIL**; all **eleven** controller -gates OK (one is new). Golden 0.213.0 baked, published and round-trip verified. **The Day-0 vouch is -the operator's and was not made here.** `--no-verify` used nowhere. +## The premise, corrected before anything was written -## 1. Is the pinned fingerprint provably the right key? — NO. It is a recorded assumption. +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. -Asked first, as instructed. **Nothing on the box records which key wrote the set-aside copies** — that -key is gone, which is why they were set aside. What is pinned is the package the hub held at the -decision, which in the ordinary rebuilt-box story IS the pre-rebuild escrow covering the pre-rebuild -repo password. On a **twice-rebuilt** box that presumption can be wrong: the hub may hold rebuild #2's -package while the copies are rebuild #1's, and no key on file opens them. **The pin cannot detect that.** +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. -Recorded as an assumption in the field comment on `settings.AbandonPinnedEscrowKeySHA256`, in the test -header, and in R-302 — so it can be narrowed later rather than hardening into a fact. +Verified at `file:line` against live source, per rule 1, including the claims in the prompt itself. -What it *does* detect, and what the rejected proxy could not: the package being **replaced** after the -decision — the exact act that cost both demo boxes their history on 2026-08-04. +## What changed -## 2. Red-proofs — mutation asserted applied each time +- `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. -| # | Mutation | Outcome | -|---|---|---| -| 1 | **the rejected proxy** (hub fingerprint vs the CURRENT key, at render) | **FAIL** — Scenario B *and* Scenario D flip back to promising. **Yes, the false promise reproduced.** This is the evidence for choosing the pin | -| 2 | pin re-read at render instead of used from the decision | **FAIL** — B and D promise again | -| 3 | empty pin backfilled from the current value | **FAIL** — a legacy countdown gets promised at | -| 4 | emptiness guards dropped | **PASSED — and that was a finding about my own test**, see below | -| 4b | same mutation, against a new both-sides-empty test | **FAIL** — two absences compared equal and became a promise | -| 5 | condition forced false | **FAIL** — Scenario A loses the clause; the guard is reachable in both directions, not merely always-off | +## The message, and what it deliberately will not say -**Red-proof 4 is worth reading.** My Scenario E (pin present, hub hash empty) **passed with the -`cur != ""` guard removed**, because its pin is non-empty so the equality fails on its own. The test -guarded the sentence, not the guard. The state where emptiness actually bites is **both sides empty** — -a legacy countdown on a box whose hub reports a legacy package. Added `ScenarioE2`, which fails as it -should. Without running the mutation I would have shipped a guard nothing pinned. +> „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á." -## 3. The three strings as shipped +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. -Always, both surfaces: -> A korábbi távoli mentéseidet **{{N}} nap múlva** ({{dátum}}) véglegesen töröljük, a kérésed szerint. +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. -Retrieval clause, only when the pin matches: -> Addig még visszaszerezheted őket a helyreállítási kóddal. +**An older agent keeps the hedged sentence.** Unknown → claims less → heals itself on update. -Cautious branch, everything else: -> Hogy ezek még visszaszerezhetők-e a helyreállítási kóddal, azt innen nem tudjuk megállapítani — ha -> vissza szeretnéd kapni őket, **írj nekünk a törlés előtt**. +## The claim guard grew a surface, and immediately convicted something -Composed from explicit codepoints and hex-verified end to end: `é=c3a9 ő=c591 á=c3a1 í=c3ad ó=c3b3 -—=e28094 ü=c3bc ö=c3b6`, no double-encoding, no replacement characters, in both templates. +`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. -## 4. A legacy countdown renders the cautious branch +**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. -A countdown started before v0.213.0 has no pin, so it renders the deletion sentence plus *„Hogy ezek -még visszaszerezhetők-e … írj nekünk a törlés előtt."* Pinned by `ScenarioD`, and deliberately **not** -backfilled. +## Tests and red-proofs -## 5. The stem guard — and why it became a claim guard +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: -**The stem cannot be banned, and this is the honest answer to §5's last question.** The replacement copy -*contains* `visszaszerezhet`, inside a question about knowability. A guard forbidding the stem would -force the product to avoid a normal Hungarian verb — a guard shaping the product around itself. - -So `scripts/retrieval_promise_gate.py` **registers each claim with a reason** and fails on unregistered -ones. Plant → convict → remove → pass run for **all three** historical wordings: each convicted (`rc=1`), -all cleared on removal (`rc=0`). - -**The full sweep of all 36 templates found five sites, and I do NOT believe "nothing else" would have -been the right answer** — given the same promise surfaced one at a time across three sessions, the sweep -finding two *new* instances is exactly what the history predicts: - -| site | verdict | +| Mutation | Result | |---|---| -| `backups.html`, `backups_apps.html` | local backup/restore, unrelated to the escrow claim — allowlisted | -| `backups_remote.html` (RecoveryOffer entry) | true where it renders; deliberately left, per the previous session | -| `backups_remote.html` (abandon block) | **FOURTH instance — same condition applied.** Fixing the strip and not the page would have left one contradicting the other | -| `layout.html` (the banner) | the subject of this session | -| `recovery.html` (abandon confirmation) | **FIFTH instance — deliberately LEFT.** It renders at the moment of the decision, where the promise is true by construction: that is the package about to be pinned | +| 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 | -One instrument defect found and fixed on the way: the gate's first draft matched by proximity window and -reported two live entries as stale, because the promise and the disclaimer sit ~100 characters apart. It -now matches by span. +## Deployed -## 6. Was R-296 correctly closed? Yes. - -Its sentence was `backups_remote.html:98`; v0.212.0 replaced it and the stem guard covers it. Verified -before closing, not assumed. - -## 7. Observations, not acted on - -- **R-303 — `markOrphaned` has no guard against an active countdown** (`offbox.go:804`). **This session - makes the co-render harmless, not impossible.** **Ranked LOW**, deliberately: it is a coherence - question, not a correctness one, and the tempting fix — suppressing the orphan card during a countdown - — would hide a genuine second fault (the box's *new* store also being unopenable). -- The cautious sentence now appears on both the strip and the page below it when both render. Each is - self-contained; I read it as reinforcement rather than a defect, and did not churn copy to avoid it. - -## 8. Out of scope, so it does not read as forgotten - -The drill session unblocking the two unpublished installer fixes; the hub half of the code naming (its -`file:line` enumeration is still owed); the CI runs that fail with no log; the twenty unread facts; the -nine grey claims; and **proving that a retained key can actually open an old store** — still unshown, -and the same knowledge this whole design turns on. +`gitea.dooplex.hu/admin/felhom-controller:0.214.0` built, pushed, deployed to guest 9201 — +`Up … (healthy)`. Live validation of the customer screen was **endpoint/handler-level, not a +click-through**: the dashboard password of record no longer opens this box (R-308).