v0.207.0 — R-249: the retrieval passphrase leaves the page body; R-252/R-253: two refusals learn to say what to do
gates / gates (push) Successful in 18s

R-249. settings_security.html rendered the passphrase into a display:none
span behind a Megjelenit button. That toggle stops a browser DRAWING the value
and nothing else — the plaintext was in the response body of every render, so a
curl of the page returned it. Found by exactly that: it landed in a session
transcript while driving the documented rebuild path.

The codebase already stated this rule for the recovery code and this page did not
follow it (escrow_handlers.go: 'reveal (claim XHR only — R is NEVER templated
server-side into HTML)'). The page now carries only HasRetrievalPassword; the
value comes from POST /settings/retrieval-password/reveal — CSRF-covered because
POST, no-store, and LOGGED as an act, which reading it off the markup never was.

The tests assert the RAW RESPONSE BODY. Every test that asked what the customer
sees passed while the bytes carried the secret; that is why this survived.

Census: the render-then-hide pattern appears twice more — app_info.html (a real
per-install app password in a hidden span) and deploy.html. Filed as R-254, NOT
fixed here.

R-252. A rebuilt box keeps its drives but loses their REGISTRATION. The restore
page now states that before the customer presses anything, says the backups and
drives are both still there, and links to Tarhely > Meghajtok. Page and resolver
ask ONE question — HasRestoreDestination() reads the same
GetSchedulableStoragePaths() the scratch resolver reads.

R-253. The list promised 'a visszaallitas elobb ujratelepiti' three lines above a
refusal that fired BECAUSE the app was not installed. The promise was the wrong
half: reconstitution writes to the app's own GetStackHDDPath, which exists only
once the CUSTOMER has chosen a drive at deploy time. Auto-reinstalling would mean
the product making that choice for them. Copy now says to install first and routes
to /stacks/<app>/deploy.

Both notices are conditional — a healthy box renders as before, pinned by a test
that fails if either becomes unconditional.
This commit is contained in:
2026-08-07 18:04:26 +02:00
parent 3d3b4496f3
commit 8dbbc98ff2
14 changed files with 554 additions and 15 deletions
+58
View File
@@ -1,3 +1,61 @@
## v0.207.0 — a password stops living in the page source, and two refusals learn to say what to do (2026-08-08, R-249/R-252/R-253) — MinAgent 0.127.0
Three items the fifth walk exposed by passing. None of them touches the recovery path it proved; all
three are about what the product *says*.
### 1. The retrieval passphrase leaves the page body (R-249)
`settings_security.html` rendered the passphrase into a `display:none` span behind a „Megjelenít"
button. **That toggle stops a browser DRAWING the value and nothing else** — the plaintext was in the
response body of every render, so a `curl` of the page returned it. It was found by doing exactly
that: it landed in a session transcript on 2026-08-07 while driving the documented rebuild path.
**The product already had this rule and this page did not follow it.** `escrow_handlers.go` states it
for the recovery code — *"reveal (claim XHR only — R is NEVER templated server-side into HTML)"*. The
passphrase now follows the same shape: the page carries only `HasRetrievalPassword`, and the value
comes from **`POST /settings/retrieval-password/reveal`**, behind the same RequireAuth + CsrfProtect
every other POST sits behind, `Cache-Control: no-store`, **and logged as an act** — reading it off the
markup left no trace anywhere, where the hub's equivalent break-glass reveal has always emitted an
event.
**POST for a read, deliberately:** a GET would be re-fetchable from history, pre-fetchable, cacheable,
and — since CsrfProtect only covers unsafe methods — uncovered by CSRF.
**The test asserts the raw response body, not a rendered view**, because that is precisely why this
survived: every test that asked what the customer *sees* passed while the bytes carried the secret.
**Census (§7.1), reported not fixed:** the render-then-hide pattern appears **twice more**
`deploy.html` (an auto-generated app secret in a `type="password"` input's `value=`; unavoidable on
the pre-deploy form, which must post it, but not on an already-deployed app's page) and
**`app_info.html`, which puts a per-install generated app password inside a `hidden` span** — the same
shape with a real secret. Filed as **R-254**.
### 2. „nincs elérhető adatmeghajtó" now names the reason and the route (R-252)
A rebuilt box's drives survive; their **registration** does not. Every restore then refused with a
sentence that named no next step and read like data loss. The restore page now states the precondition
**before** the customer presses anything, says the backups and the drives are both still there, and
links to Tárhely → Meghajtók. The refusal string says the same.
The page asks the question through the backup manager's own `HasRestoreDestination()`, which reads the
**same** `GetSchedulableStoragePaths()` the resolver reads — a second copy of that predicate is exactly
how a page ends up promising what the handler refuses, which is the next item.
### 3. The page no longer promises a reinstall the restore cannot do (R-253)
The restore list said **„Nincs telepítve — a visszaállítás előbb újratelepíti."** Three lines later the
restore refused *because* the app was not installed. Two shipped sentences, in the customer's own
language, contradicting each other at the last step of a recovery.
**The promise was the wrong half, and this is why:** reconstitution writes to the app's own data path
(`GetStackHDDPath`), which exists only once the customer has chosen a drive at deploy time. An
automatic reinstall would mean the product picking that drive for them — the one decision this whole
recovery path exists to leave with the customer. So the copy now says to install it first and routes
to `/stacks/<app>/deploy`; the refusal was reworded to match.
**A healthy box renders exactly as before** — both notices are conditional, and a test fails if either
becomes unconditional.
## v0.206.0 — the box does not mint a key over a sealed package, and abandoning ends the question (2026-08-07, R-241) — MinAgent 0.127.0
**R-241 was ruled a MINTING defect, not a screen-predicate defect** (`SPIKE-r241-recovery-offer-2026-08-07.md`),