R-543 closed: the household is asked for the recovery code (controller v0.245.0)
gates / gates (push) Successful in 21s
gates / gates (push) Successful in 21s
The tier-3 pause is the zero-knowledge escrow design and is untouched. What was missing was the ASK, while the backup page promised the copy that had never run. - VOLUNTEER-first-hour.md: a new step 6, right after the dashboard password and before the first app - what the code is, where, write it on PAPER, and that Felhom cannot get it back for them. Sections 6..12 renumbered to 7..13. - day0-install.md A.2b: the operator step for a REBUILT box, which was missing. Acknowledged delete -> the hub re-issues by itself; otherwise ONE press of "Re-issue PBS credentials" (F-14 ruling 2026-07-13, hub/internal/web/pbsdr.go). This is the correction to last night's "zero presses" note. - 07-backup-architecture.md: 6.1 records tier-3's paused state as a DESIGN, and 2 records that the household is asked from first login. - capability map: the first-hour row's last gap closed, with what it still does not claim (no volunteer has walked the ask from the written guide). - register: R-543 CLOSED with the live measurements; R-545 filed (nothing un-configures an off-site target). R-511 was already closed yesterday. - STATUS: the answered publish question removed (1.28.0 is live), readiness yes. - evidence: red-proofs, the two-box live validation, teardown on three layers, and both of my own mistakes in this session. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
## R-543 live validation — controller v0.245.0, 2026-09-16, demo-hp
|
||||
## Method: endpoint-level. Every request below is the exact request the browser makes (same handler,
|
||||
## same session cookie, same CSRF); only rendering is skipped. claude-in-chrome is not available here.
|
||||
## Reachability note: neither guest answers on its LXC address from DooPlex, and the controller does
|
||||
## not listen on the guest's 127.0.0.1 — it answers on the container address 172.17.0.2:8080 with the
|
||||
## mandatory Host header. All requests therefore run INSIDE the guest via `pct exec`.
|
||||
## Secrets: the dashboard password was read with scripts/read_credential.py (value never printed,
|
||||
## file->file, 0600) and passed to curl as --data-urlencode password@<file>.
|
||||
|
||||
### THE SEAM, STATED
|
||||
The two halves of this proof are on two boxes, not one box before and after a ceremony:
|
||||
* PAUSED half — guest 9202 (scratch), off-site configured and escrow NOT complete.
|
||||
* ESCROWED half — guest 9201 (demo), off-site configured, escrowed, running.
|
||||
Running a real escrow ceremony on a fresh target would provision off-site storage, and this task's
|
||||
fences put ep0 out of bounds. So the escrowed side is OBSERVED on a box that is already escrowed
|
||||
rather than produced here. What is NOT weakened by the seam: both boxes run the same binary
|
||||
(0.245.0), and the paused box's state was produced through the product's own configuration endpoint.
|
||||
|
||||
## ── 1. ESCROWED + ACTIVE — guest 9201 (0.245.0) ────────────────────────────────────────────────
|
||||
login OK
|
||||
GET /dashboard -> 200 bytes=60816 escrow-bar-hits=0
|
||||
GET /launcher -> 200 bytes=44462 escrow-bar-hits=0
|
||||
GET /backups/apps -> 200 bytes=106304 escrow-bar-hits=0
|
||||
tier-1 file sentence, as rendered (2 class-A apps):
|
||||
"Az alkalmazás fájljait a távoli másolat (és a második meghajtó) védi — ez a helyi mentés a
|
||||
beállításokat és az adatbázist tartalmazza."
|
||||
word counts on /backups/apps: Kulcslet=0 szünetel=0 védi=2 védené=0
|
||||
=> a box whose recovery code exists is NOT nagged, and the promise it prints is a true one.
|
||||
|
||||
## ── 2. PAUSED — guest 9202 (0.245.0) ───────────────────────────────────────────────────────────
|
||||
The state was produced through the product's own endpoint, POST /backup/offbox/config, with a
|
||||
throwaway ed25519 key and a pinned known_hosts line. 9202 has no agent local API, so the escrow
|
||||
STAGE could not run — the handler said so and saved the target anyway:
|
||||
|
||||
POST /backup/offbox/config -> 302
|
||||
flash: "A távoli mentési cél elmentve. — a kulcs letéti előkészítése nem sikerült
|
||||
(az ügynök nem elérhető); próbáld újra."
|
||||
|
||||
Persisted state afterwards (read from the guest's own settings.json):
|
||||
enabled=True host=192.168.0.162 repo_path=/mnt/nvme-1tb/r543-paused-proof
|
||||
escrow_state=pending last_run=None last_status=None snapshot_count=None
|
||||
data/offbox/: known_hosts 95 B (0644) | repo_password 64 B (0600) | ssh_key 411 B (0600)
|
||||
=> OffboxConfigured() is genuinely true (valid target + both secret files), and the escrow is NOT
|
||||
complete. This is the state a fresh box lands in on day one.
|
||||
|
||||
### 2a. The bar is on EVERY page, not on the three someone remembered
|
||||
GET /dashboard -> 200 escrow-bar-hits=1
|
||||
GET /launcher -> 200 escrow-bar-hits=1
|
||||
GET /backups/apps -> 200 escrow-bar-hits=1
|
||||
GET /settings -> 200 escrow-bar-hits=1
|
||||
GET /apps -> 404 escrow-bar-hits=0 (no such route; a 404 carries no bar)
|
||||
|
||||
Quoted from /dashboard:
|
||||
"A távoli mentés szünetel, amíg nem hozod létre a helyreállítási kódot."
|
||||
<a href="/backup/escrow"> (the route out, on the bar itself)
|
||||
|
||||
### 2b. A manual off-site run, while paused — refused, FOR THE RIGHT REASON
|
||||
POST /backup/offbox/run -> 302
|
||||
flash: "A távoli mentés a kulcs letétbe helyezésére vár."
|
||||
after the attempt: last_run=None last_status=None snapshot_count=None
|
||||
=> No snapshot, and the refusal came from the fork-4 escrow gate (offbox_handlers.go:224
|
||||
OffboxRunnable), NOT from an unreachable target — the target was never contacted. This matters:
|
||||
a run that failed to connect would have proven nothing about the pause.
|
||||
|
||||
### 2c. „Most nem" is for the visit only
|
||||
POST /backup/escrow/banner/dismiss -> 302
|
||||
Set-Cookie: felhom_escrow_banner=1; Path=/; HttpOnly; SameSite=Lax
|
||||
^ no Max-Age and no Expires => a browser SESSION cookie, exactly as R-241 does it
|
||||
same visit, cookie sent -> escrow-bar-hits=0
|
||||
next visit, no cookie -> escrow-bar-hits=1
|
||||
=> the off-site tier is still paused tomorrow, so the question is still asked tomorrow.
|
||||
|
||||
### 2d. The tier-1 FILE sentence, live, with the tier paused
|
||||
The first pass of this phase could not show it: 9202 had no class-A app, so the sentence had nothing
|
||||
to render („védi"=0 AND „védené"=0 — the „szünetel"=1 on that page was the BAR in the layout, not the
|
||||
sentence). Recorded because it was briefly written down as a limit and it was not one. A throwaway
|
||||
class-A app was then deployed on the box's own drive and the off-site copy turned on for it:
|
||||
|
||||
POST /api/stacks/calibre-web/deploy -> 202 {"ok":true,"message":"Telepítés elindítva ..."}
|
||||
(HDD_PATH=/mnt/felhom-drives/scratch_hdd — the guest's own registered data drive, 938 G)
|
||||
POST /backup/offbox/toggle app=calibre-web enabled=true -> 302
|
||||
|
||||
Rendered on /backups/apps, with the tier configured and PAUSED:
|
||||
|
||||
"Az alkalmazás fájljait a távoli másolat védené — a távoli mentés a helyreállítási kód
|
||||
létrehozásáig szünetel."
|
||||
...followed by the route: "Helyreállítási kód létrehozása →"
|
||||
|
||||
word counts on /backups/apps: Kulcslet=1 szünetel=3 védi=0 védené=1
|
||||
Kulcslet=1 is the tier-3 row's own state („Kulcsletétre vár").
|
||||
szünetel=3 is the bar + the sentence + the tier row.
|
||||
védi=0 is the point: the page no longer claims a protection that has never run.
|
||||
|
||||
=> Both wordings are now observed LIVE on real boxes running 0.245.0: „védi" on the escrowed box
|
||||
(9201, §1) and „védené … szünetel" on the paused box (9202, here).
|
||||
|
||||
## ── 3. TEARDOWN, three layers ──────────────────────────────────────────────────────────────────
|
||||
Recorded after the evidence above was already written to DooPlex (R-320: evidence leaves the machine
|
||||
at the END OF THE PHASE, before any revert — not at the end of the session).
|
||||
|
||||
MACHINE (guest 9202, scratch):
|
||||
* the throwaway class-A app removed WITH its data and its backups:
|
||||
POST /api/stacks/calibre-web/stop -> 200
|
||||
POST /api/stacks/calibre-web/remove -> 200 {"removed":"calibre-web",
|
||||
"volumes_removed":["calibre-web_calibre_web_config"],"hdd_paths_removed":[],
|
||||
"hdd_note":"Az alkalmazás nem tárolt saját adatot…"}
|
||||
verified after: containers named calibre = 0, drive folders = 0
|
||||
* the off-site target this proof created is GONE:
|
||||
before: offbox present=True, data/offbox/ held ssh_key, repo_password, known_hosts
|
||||
after : offbox present=False, data/offbox/ ABSENT, all three files `shred -u`'d
|
||||
MY OWN MISTAKE, recorded: the first teardown pass used the CONTAINER's view of the data path
|
||||
(/opt/docker/felhom-controller/data) from a shell running in the GUEST, where that path does
|
||||
not exist. It printed „offbox dir now: ABSENT" — which was TRUE of a path that never existed
|
||||
and FALSE of the thing being claimed. The target was still fully configured. The same wrong
|
||||
path had already produced three FileNotFoundError tracebacks earlier in this phase; I read
|
||||
those as noise instead of as the instrument telling me it was pointed at nothing. The re-run
|
||||
stops the controller first, edits the real file, shreds the secrets, restarts, and RE-READS
|
||||
the state to confirm — a teardown asserted is not a teardown observed.
|
||||
* controller restarted and healthy: felhom-controller:0.245.0 Up (healthy)
|
||||
* temp files: none left (`ls /tmp/.r543*` empty in the guest)
|
||||
* NOT touched: filebrowser, traefik, the box's registered data drive, its password, its claim state.
|
||||
|
||||
HOST (demo-hp): /tmp/.r543pw and /tmp/.r543key shredded; .r543kh, the scripts and one stray
|
||||
/tmp/.r543out removed. Verified: 0 files matching /tmp/.r543* remain.
|
||||
|
||||
HUB: provisioned nothing. Guest 9202 has hub reporting OFF and no tunnel, so no customer, no host
|
||||
record, no escrow row and no event was created by any of this. Nothing to tear down.
|
||||
|
||||
NOT torn down, deliberately: guest 9201 now runs controller 0.245.0. That is the release being
|
||||
shipped, not a drill artifact, and its own off-site tier was never touched (still escrowed, active).
|
||||
@@ -0,0 +1,48 @@
|
||||
## R-543 red-proofs — controller v0.245.0, 2026-09-16, DooPlex
|
||||
## Each fix was BROKEN first and the test was watched convicting it. A test never seen failing has
|
||||
## not been shown to test anything.
|
||||
|
||||
### RED-PROOF 1 — the reminder bar
|
||||
Break: delete `s.addEscrowBanner(data, r)` from executeTemplate (internal/web/server.go).
|
||||
That is the whole wiring: the bar hangs off the single render choke point, so removing one line
|
||||
returns the product to the measured 2026-09-16 state (a paused off-site tier, and silence).
|
||||
|
||||
--- FAIL: TestR543_A_PausedBoxAsksOnEveryPage (0.20s)
|
||||
r543_escrow_banner_test.go:72: R-543: /dashboard does not tell the household the off-site copy is PAUSED. The tier is on, nothing is running, and the page is silent about it
|
||||
r543_escrow_banner_test.go:76: R-543: /dashboard states the pause but names no route to end it — a reminder without its door is the shape that left a fresh box waiting indefinitely
|
||||
r543_escrow_banner_test.go:72: R-543: /launcher does not tell the household the off-site copy is PAUSED. The tier is on, nothing is running, and the page is silent about it
|
||||
r543_escrow_banner_test.go:76: R-543: /launcher states the pause but names no route to end it — a reminder without its door is the shape that left a fresh box waiting indefinitely
|
||||
FAIL gitea.dooplex.hu/admin/felhom-controller/internal/web 0.208s
|
||||
|
||||
Note BOTH pages fail. That is the point of the hook placement: a per-handler helper would have
|
||||
covered the three pages someone remembered, which is the seam-built-but-never-wired shape.
|
||||
|
||||
### RED-PROOF 2 — the tier-1 sentence
|
||||
Break: return the v0.244.0 wording from driveFilesNoteFor before the state switch, i.e. compute the
|
||||
sentence from the app's shape alone, exactly as v0.244.0 shipped it.
|
||||
|
||||
--- FAIL: TestR543_Tier1Sentence_PausedStateDoesNotPromise (0.00s)
|
||||
r543_tier1_sentence_test.go:38: R-543: the sentence claims the files ARE protected while the copy is paused for the recovery code. This is the exact promise a fresh box read for its whole first day: "Az alkalmazás fájljait a távoli másolat (és a második meghajtó) védi — ez a helyi mentés a beállításokat és az adatbázist tartalmazza."
|
||||
r543_tier1_sentence_test.go:42: R-543: the paused sentence must say the copy WOULD protect them and that it is waiting; got "Az alkalmazás fájljait a távoli másolat (és a második meghajtó) védi — ez a helyi mentés a beállításokat és az adatbázist tartalmazza."
|
||||
r543_tier1_sentence_test.go:46: R-543: the paused sentence names no route out (link="" text="")
|
||||
FAIL gitea.dooplex.hu/admin/felhom-controller/internal/web 0.007s
|
||||
|
||||
### RESTORED
|
||||
go test ./internal/web/ -run 'R543'
|
||||
ok gitea.dooplex.hu/admin/felhom-controller/internal/web 0.895s
|
||||
|
||||
### The filter was proven to match (the `-run` trap: a pattern matching nothing prints `ok`, exit 0)
|
||||
=== RUN TestR543_A_PausedBoxAsksOnEveryPage --- PASS (0.29s)
|
||||
=== RUN TestR543_B_EscrowedBoxIsNotNagged --- PASS (0.19s)
|
||||
=== RUN TestR543_C_UnconfiguredBoxIsNotNagged --- PASS (0.20s)
|
||||
=== RUN TestR543_D_DismissIsForThisVisitOnly --- PASS (0.28s)
|
||||
=== RUN TestR543_Tier1Sentence_ActiveStateKeepsThePromise --- PASS
|
||||
=== RUN TestR543_Tier1Sentence_PausedStateDoesNotPromise --- PASS
|
||||
=== RUN TestR543_Tier1Sentence_NoCopyAtAllSaysSo --- PASS
|
||||
=== RUN TestR543_Tier1Sentence_SecondDriveCounts --- PASS
|
||||
=== RUN TestR543_Tier1Sentence_NoFileLegsNoSentence --- PASS
|
||||
|
||||
### Fixture validity (an instrument that can lose its precondition measures nothing)
|
||||
escrowServer asserts backupMgr.OffboxConfigured() itself before any assertion runs: the target is
|
||||
enabled and valid AND the ssh_key + repo_password files exist on disk. A fixture that silently fell
|
||||
back to "not configured" would make every one of these tests pass for the wrong reason.
|
||||
Reference in New Issue
Block a user