Files
felhom.eu/documentation/tests/VALIDATION-n100-rehearsal-2026-07-18.md
T
admin aa9c08f0dd docs(rehearsal): N100 S8 wrap — VALIDATION, map flips, ROADMAP R-30..R-39, R-3 draft
The 2026-07-18 N100 rehearsal ran the complete final-product flow on real metal
in one pass: RESET -> generic pairing ISO v1.20.0 -> customer self-bind -> day-0
-> managed-floor lift -> escrow ceremony -> offsite snapshots. No code changed;
every finding is recorded and ranked, none fixed.

VALIDATION-n100-rehearsal-2026-07-18.md — run context, a UTC-normalised timeline
built from the hub events stream / hub DB / controller log / bootstrap + agent
journals, per-ledger verdicts for S1-S8 + ledgers 8 and 9, 12 findings, the
not-exercised list, and 7 discrepancies against the brief.

Headline wall-clocks: bind -> credential 26 s; bind -> controller running the
current version 2 min 44 s; managed floor 0.143.0 -> 0.145.0 in 5 s unattended
(initiated_by: auto-floor); escrow ceremony -> offsite enabled 12 s; drive enrol
30.3 s. No post-bind leg stalled, which is the immediacy row's real-onboarding
proof.

Capability map (10 citations added):
- Bare-metal Felhom ISO           PARTIAL     -> PROVEN-LIVE (F1 closed on metal)
- Customer self-bind (slice 1)    IMPLEMENTED -> PROVEN-LIVE (customer_selfbind)
- Guest RAM resize (R-24)         IMPLEMENTED -> PROVEN-LIVE (shrink AND grow)
- Customer RESET                  two real firings + verified external teardown
- Escrow ceremony                 first live wizard firing
- Immediacy row                   "real-onboarding proof pending" cleared
- Publish train                   box-side floor lift proven on a fresh install
- Customer claim                  R-4 gmail half (Inbox under p=quarantine)
- Offsite orphan guard            staged live leg fired on its own
- DR tier by default              candidate PROVEN-LIVE upgrade WITHDRAWN (R-39)
Not flipped, as instructed: customer-performs-restore, BYO, DLNA, multi-user.

ROADMAP — collapsed R-1 (appliance half done, Peti half survives), R-21
(physically closed), R-24, R-27 slice 1, R-4. New ranked items:
  P2-HIGH  R-39 PBS DR applied-but-dead   R-30 liveness from the wait channel
           R-31 async offsite + status    R-32 RESET base-dir purge
           R-33 bootstrap quiet-poll
  P2       R-34 backup lifecycle          R-35 config-apply session survival
           R-36 post-RESET offsite prompt R-27c console-passphrase bind
  P3       R-37 post-RESET health card    R-38 installer GRUB slice
Plus a pre-invite checklist (golden 0.145.x rebuild, freemail.hu, C6, R-11).

R-39 is NEW and was not on the brief: the PBS DR descriptor auto-provisions and
the agent converges state=applied, but pvesm reports 401 Unauthorized/inactive
and a direct probe 401s on every endpoint including /version while WG is healthy.
The hub minted a second token secret two minutes after the agent applied the
first and consumed_at is still NULL; the converged state machine will not
re-apply, and the agent's verify loop cannot read the credential to notice it
(non-root read of a file it writes through a root wrapper). Rank is provisional
pending Viktor.

R-3 draft: all four [REFINE] slots filled, self-bind made the default path with
"send the link BEFORE the customer sees the console", the measured wall-clock
table added, and interim operator workarounds for R-31/R-36/R-39. C6 (renumbered
C7) is marked as the single unexecuted step and keeps the doc a DRAFT.

Evidence bundle: 180:~/n100-rehearsal/ (10 files + MANIFEST.md), collected before
the box was unplugged for travel. Secrets read only to run probes; recorded as
lengths and metadata, never values.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nn3VgQk9iwEGgyx6QJ2NvE
2026-07-18 19:56:17 +02:00

23 KiB
Raw Blame History

VALIDATION — N100 rehearsal: RESET → generic ISO → self-bind → customer zero, 2026-07-18

Supervised run of runbooks/RUNBOOK-rehearsal-v3.md (S1S8). Viktor at the box as customer zero; CC collected the evidence afterwards (Phase A) before the N100 was unplugged for travel. No production code changed by this run — this document, the capability-map flips, and the ROADMAP items are its outputs. Every finding below is recorded and ranked, never fixed here.

Headline — read this first. The complete final-product flow ran end to end on real metal in one pass: blank hardware → zero-touch install → self-registration → customer self-bind → credential delivery → day-0 → managed-floor lift → drive enrolment → escrow ceremony → offsite snapshots. The chain from bind to a running current-version controller took 2 min 44 s, and the managed floor lifted the controller 0.143.0 → 0.145.0 in 5 seconds, unattended. Three things did not go to plan and are the run's real value: (1) the offsite repo came up ORPHANED, which S7 defines in advance as a finding; (2) the PBS DR tier is silently non-functional — descriptor applied, token rejected 401, and the agent's own verify loop cannot read the credential to notice; (3) pushing the offsite config restarted the controller and logged the customer out mid-flow. Net: core objectives green, 11 ranked items, one of them new and unforecast.


Run context

Hardware Intel N100 (AlderLake-N), 16 GB; the same physical box as VALIDATION-n100-baremetal-2026-07-16
Host demo-felhom, PVE 9.2.2 (kernel 7.0.2-6-pve), guest 9201 (demo-felhom)
Customer demo-felhom, domain demo-felhom.eu — reborn clean-slate on this run
Versions controller 0.145.0 · agent 0.90.0 · hub v0.66.0 · ISO scripts v1.20.0 · host-install 1.17.0
Preparation RESET ×2 (host-delete-first): demo-vm-felhom 15:49:57, demo-felhom 16:08:51 — both with all five legs ok and escrow acked
Media safety ISO first, then the real universal pairing ISO v1.20.0, --loader mkimage, Secure Boot OFF (mkimage is unsigned — the documented slice-B constraint)
Evidence 180:~/n100-rehearsal/ (10 files + MANIFEST.md)

⚠ Timezone. The PVE host journal is CEST (+0200); the guest, the controller and the hub are UTC. Every timestamp in this document is UTC. Subtract 2 h from any journalctl line before comparing it with a controller or hub timestamp. This split is live-captured in timezones.txt and is itself worth a ruling (see Observations).


Timeline (wall-clocks, all UTC)

Sourced from the hub events stream, the hub DB, the controller log, the bootstrap journal and the agent journal — not from expectation. Where the brief and the evidence disagreed, the evidence is recorded and the disagreement is called out under Discrepancies.

UTC What happened Evidence
15:49:57 → 15:50:06 RESET #1 — customer demo-vm-felhom; legs claim/db_purge/descriptor/hetzner/pbs all ok, escrow acked hub-state.txt customer_resets id=1
16:05:24 host_stale + node_stale — "no report for 30m" events
16:08:51 → 16:08:59 RESET #2 — customer demo-felhom; all five legs ok, escrow acked (8 s) customer_resets id=2
16:17:14 Blank metal first boot. felhom-bootstrap in PAIRING mode; registers as an unclaimed appliance the same second bootstrap-journal.txt; appliance_registrations.first_seen
16:17:14 → 16:28 30 s pairing-poll loop; every iteration exits 152 systemd FAILED/Failed to start lines on the customer-visible console bootstrap-journal.txt
16:28:55 Operator mints + emails the self-bind link; 7-day TTL (expires 2026-07-25 16:28:55) selfbind_tokens created_at/emailed_at/expires_at
16:29:55 Customer self-binds — token consumed, attempts=0, locked=0; appliance_bound source customer_selfbind selfbind_tokens.consumed_at; events
16:30:21 appliance_credential_delivered (26 s after bind) events; appliance_registrations.delivered_at
16:30:40 Hub hosts row created — demo-felhom-8363b5 hosts.created_at
16:30:49 Agent 0.90.0 daemon start; fast-tick armed agent-journal.txt (18:30:49 CEST)
16:30:51 WG keypair generated → registered with hub10.77.0.2/32 → tunnel applied (~2 s end to end) agent journal
16:30:54 Poke listener up, WG-confined 10.77.0.2:51822 agent journal
16:32:28 controller_started 0.143.0 (the golden's version) events
16:32:29 Managed-floor update initiated — initiated_by: auto-floor update-state.json
16:32:32 Day-0 provision SUCCESS — done-flag written, unit disabled, bootstrap secrets scrubbed bootstrap-journal.txt
16:32:34 Floor update complete: 0.143.0 → 0.145.0 in 5 s; new container created update-state.json; docker inspect .Created
16:32:39 controller_started 0.145.0 events
16:33:24 node_recovered — "Reports resumed (was stale for 27m)" events
16:41:41 Drive init starts — /mnt/hdd_1, /dev/sdb, ext4 controller log
16:41:42 Agent wipe gate: storage_wipecustomer_confirmable, allowed=false, pending_confirmation agent journal
16:41:51 Wipe gate: allowed=true, customer_confirmed (9 s of customer thinking time) agent journal
16:42:19 Device formatted ext4 agent journal
16:42:21 Mount ensured → bound into guest → storage path registered → FileBrowser mounts synced. Init 30.3 s (F6 working) controller log + agent journal
16:50:22 RAM shrink 11675 → 8192 MB, live cgroup, no reboot controller log + agent local-api: guest-memory resized
16:45:51 Agent consumes a one-time PBS token secret (secret_len=36) and applies the descriptor agent journal
16:45:53 pbsdr: converged state=applied agent journal
16:47:52 Hub mints a NEW PBS token secret — consumed_at is still NULL host_pbs_secrets
16:47:55 → Agent PBS verify loop: cannot read token secret … permission deniedrepeats every ~15 min, indefinitely agent journal
16:54:58 Config-refresh: hub config_version=11 != applied=10controller self-restarts controller log
16:55:02 Controller 0.145.0 restarts (container StartedAt 16:54:59Z — a real restart, not a reload) controller log; docker inspect
16:55:05 offsite-apply: Storage Box target configured, pending key escrow controller log
16:56:29 Escrow ceremony started via the wizard controller log
16:56:39 Recovery code claimed (one-shot, not logged) controller log
16:56:41 Hub-verified → EscrowState auto-confirmed → offsite runs enabled (12 s ceremony → confirmed) controller log
16:58:12 First offsite run starts controller log
16:58:14 ORPHAN GUARD FIRESoffbox_repo_orphaned, run skipped, card shown controller log + events
16:59:26 Operator-confirmed reset: move-aside /home/felhom-repo.orphaned-20260718 controller log
16:59:32 Fresh repo initialised; offbox_repo_reset controller log + events
16:59:38 → 16:59:50 Backup run: 0 apps toggled, 0 snapshots, 7 s controller log
17:02:17 RAM grow 8192 → 12288 MB, live cgroup, no reboot controller log + agent journal
17:05:15 Calibre-Web deployed; deploy-page memory math now reads total=12288MB (the grow rippled) events + controller log
17:06:40 Immich deployed events
17:11:16 BookStack deployed (first start attempt failed exit 1 17:11:36; healthy by 17:12:03) events + controller log
17:13:38 → 17:14:22 Offsite run: 2 apps backed up, 1 skipped, 2 snapshots, 38 s controller log
17:13:42 Snapshot 243ad08c — calibre-web restic-and-pool.txt
17:13:46 Snapshot 6df12205 — immich restic-and-pool.txt

Derived wall-clocks (the numbers R-3 needs)

Leg Wall-clock
Blank-metal boot → registered as unclaimed appliance < 1 s (same second)
Self-bind link minted → customer consumed it 1 min 0 s
Bind → credential delivered 26 s
Bind → agent enrolled + WG tunnel up 56 s
Bind → controller running the CURRENT version 2 min 44 s
Credential delivered → day-0 provision SUCCESS 2 min 11 s
Managed-floor lift 0.143.0 → 0.145.0 5 s, unattended
Drive enrol (confirm → usable, incl. mkfs) 30.3 s
Escrow ceremony → offsite runs enabled 12 s
Boot → day-0 SUCCESS (incl. ~11.7 min waiting for the operator to send the link) 15 min 18 s

Per-ledger verdicts

Ledger Verdict Evidence
S1 — stop apps, shut down, hub DOWN PASS host_stale/node_stale 16:05:24
S2 — the one BIOS visit (boot order, SB off, State-After-G3) PASS (human leg) Viktor at the box; SB-off is required by --loader mkimage
S3 — the F1 moment: mkimage GRUB boots where stock GRUB threw relocation 0x0 PASS The box reached first boot at 16:17:14 on the real board — F1 is closed on physical hardware
S4 — unattended install → pairing mode → appears under Unclaimed appliances PASS bootstrap-journal.txt "PAIRING mode … registered — appliance token stored (0600)"; appliance_registrations id=3 status=registered
S5 — the bind + the four-minute event composition, no ~15-min stall between legs PASS — and this is the immediacy proof Every post-bind leg is seconds apart: bind 16:29:55 → credential 16:30:21 → agent 16:30:49 → WG 16:30:51 → controller 16:32:28 → floor-lifted 16:32:39. No leg stalled. F5 auto-sizing landed the guest at 11675 MB
S6 — customer zero: claim → password → escrow ceremony → storage → RAM grow and shrink → deploy → enable offsite PASS on every leg, with two findings attached Ceremony 16:56:29→16:56:41; wipe gate pending→confirmed 16:41:42→16:41:51; shrink 11675→8192 at 16:50:22 and grow 8192→12288 at 17:02:17, both live with no reboot; 3 apps deployed. Findings: the config push logged the customer out (F3); offsite provisioning is Save-triggered, answering S6's open question
S7 — first snapshot into a brand-new repo; "if an orphan card appears, that's a finding" FINDING — the orphan card appeared offbox_repo_orphaned 16:58:14. Recovered by the operator-confirmed move-aside 16:59:26→16:59:32; the run then produced 2 snapshots / 48.717 MiB at 17:13:4246. The guard worked exactly as designed — the finding is that it had to
S7 (operator gauges) PARTIAL Restic tab: Viktor's screenshots. PBS DR tab was not captured, and the reason now matters — see F2
S8 — wrap: evidence, VALIDATION doc, map flips, ROADMAP, R-3 DONE — this document 180:~/n100-rehearsal/MANIFEST.md
Ledger 8 — immediacy legs, first live firing on a real onboarding PASS See S5. The "real-onboarding proof pending" note on the immediacy row can be cleared
Ledger 9 — R-24 guest RAM resize, the apply (not just the refusals) PASS, both directions Shrink and grow, live cgroup, no reboot, and the new total rippled into the deploy page's memory math at 17:05:15

Findings (recorded and ranked — not fixed)

Ranked as proposed for the ROADMAP; IDs assigned in backlog/ROADMAP.md.

F1 — Liveness presence lags reality by ~30 min (→ R-30, P2-HIGH)

The box was powered off at the start of the run, yet the hub carried it as healthy until the staleness threshold expired at 16:05:24 ("no report for 30m"), and cleared only at 16:33:24 ("was stale for 27m"). The host-delete guard compounds this: RESET refuses while any host row exists, so a stale-but-"online" host stalls a forced teardown. Direction: derive presence from Dir-2 long-poll connectedness (~90 s grace), decoupled from notification hysteresis; agent/ep0 analog later.

F2 — PBS DR tier is provisioned-but-dead, and cannot self-report it (→ R-39, P2-HIGH, NEW — not in the brief)

This was not forecast by the brief and is the run's most serious finding. Three facts:

  1. The descriptor was auto-provisioned: the agent consumed a one-time token secret at 16:45:51 and reached converged state=applied at 16:45:53. storage.cfg holds a complete pbs: felhom-pbs stanza (namespace demo-felhom, token felhom@pbs!demo-felhom, encryption key, fingerprint).
  2. The storage is non-functional. pvesm statusfelhom-pbs: error fetching datastores - 401 Unauthorized, inactive. A direct probe with the stored credential returns 401 on every endpoint including /version — so this is authentication failure, not ACL scope. The transport is healthy (WG handshake 9 s old, 27.9 ms RTT, /version reachable), so the token itself is rejected server-side.
  3. The hub minted a second secret at 16:47:52 — two minutes after the agent applied the first — and consumed_at is still NULL ~55 min later. The agent will not pick it up: it has already converged to state=applied, and its 15-minute verify loop cannot even read the credential to detect the failure (open /etc/pve/priv/storage/felhom-pbs.pw: permission denied — the agent runs non-root, writes the file through a root sudo wrapper, then tries to read it directly).

Net: a mint/consume race leaves the box on a stale secret, a converged state machine prevents re-application, and a permission bug blinds the only loop that could notice. The DR tier would have failed silently on the first real restore. This also explains why the S7 PBS DR gauge was worth capturing — it would have shown the fault.

F3 — Config apply restarts the controller and drops customer sessions (→ R-35, P2)

The offsite config push bumped config_version 10→11 at 16:54:58; the controller self-restarted (container StartedAt 16:54:59Z) and came back at 16:55:02. In-memory sessions died with it, so customer zero was force-logged-out mid-flow. Direction: hot-apply the offbox target, or persist sessions across restart.

F4 — Offsite provisioning is synchronous, with no status affordance (→ R-31, P2-HIGH)

Save runs the Hetzner sync in-request; the request can hit the nginx 504 while succeeding server-side, so the operator cannot distinguish failed from slow, and a retry races the first attempt. Direction: reuse the awaiting-card/poll idiom. Interim mitigation belongs in R-3: click once, wait, verify.

F5 — RESET destroys custody but not the ciphertext; the pool accumulates unattributed bytes (→ R-32, P2-HIGH)

The RESET ledger records "hetzner":"ok" — the sub-account was destroyed. But a Hetzner sub-account is an access-control object, not a data object: deleting it leaves its directory intact. Re-enabling offsite recreated a sub-account over the same /home, which still held the previous lifecycle's repo — under a key destroyed by that same RESET. Hence S7's unexpected orphan card. Measured on the pool box tonight:

Path Size Status
/home/felhom-repo 49 M live, attributed (2 snapshots, 48.717 MiB)
/home/felhom-repo.orphaned-20260717 1.4 G unattributed, key destroyed — from the 07-17 incident
/home/felhom-repo.orphaned-20260718 3.0 M unattributed, key destroyed — from tonight

Ruling from the run: because RESET destroys custody, leftover ciphertext is unrecoverable by design, so RESET gains a main-account purge of the customer base dir (the operator ack already covers it). The move-aside guard stays for reinstall-without-RESET, where custody survives and the card's promise is true. The operator Restic tab should show per-customer directory bytes against attributed snapshot bytes so dead data cannot hide.

F6 — Bootstrap pairing-poll spams the customer-visible console (→ R-33, P2-HIGH)

Each 30 s poll is a separate felhom-bootstrap.service run that exits 1, so systemd prints Failed to start … on the console the customer is watching. 52 FAILED lines in ~11 minutes, while nothing was actually wrong — the box was correctly waiting to be bound. Direction: poll inside the script (scripts v1.21) and reword the first-boot noise.

F7 — Design-v2 gates are unenforced; one has been red for 16 releases

Already ranked as R-29 (committed 2eccda4, 2026-07-18) — the brief asked for a new ID, but the finding already has one, including the docker_run_volume_path_gate / internal/appexport/estimate.go attribution. No new ID minted.

F8 — Backup data lifecycle is unmanaged (→ R-34, P2)

Apps with snapshots but no active backup (disabled or uninstalled) are invisible and undeletable. Direction: an "inactive backups" section on Távoli mentés — name / size / last snapshot / restorable, with an explicit double-confirmed per-app delete via restic forget --tag + nightly prune. RULING: the offsite toggle NEVER offers deletion — policy and destruction stay decoupled.

F9 — Post-RESET re-enroll leaves offsite "enabled but unprovisioned", silently (→ R-36, P2)

The hub knows the state and says nothing. Provisioning is Save-triggered (configs.go applyOffsite) — confirmed live tonight, which answers S6's open question — and the re-enroll auto-re-issue correctly skips unprovisioned targets (handler.go). Direction: flash it on the customer page. Interim: an R-3 step. Related: auto-mint the self-bind link on customer create/RESET so the console banner's "e-mailben kapott link" is always already true.

F10 — Console-passphrase bind (→ R-27c, P2)

Viktor's direction, with the session's security constraints: passphrase issued at customer creation; spray-hardened global-lookup endpoint (per-appliance + per-IP caps, constant-time, single generic failure, alerting); accent-free wordlist for console keymaps; the web capability-link path RETAINED; claim-by-email RETAINED as the delivery-channel proof. The self-bind email gains the public universal-ISO download link + two-line instructions (the DIY case). Secret-bearing per-customer ISOs are ruled OUT.

F11 — Post-RESET health card shows stale pre-RESET warnings (→ R-37, P3)

Should read „RESET óta nincs adat".

F12 — Installer GRUB menu offers interactive installers (→ R-38, P3)

Single default „Felhom telepítés" entry; interactive installers REMOVED (safety: wrong-disk manual installs); felhom background. Squashfs/theme rebranding explicitly deferred.


Observations (not findings — no ID minted)

  • Host/guest timezone split. The PVE host runs CEST while the guest, controller and hub run UTC. Nothing is broken, but every cross-tier correlation needs a manual 2 h shift, which is exactly the kind of friction that produces wrong incident timelines under pressure. Flagged for a ruling rather than ranked unilaterally.
  • The escrow awaiting-card window was 12 seconds. Ceremony → hub-confirmed took 16:56:29 → 16:56:41, so the „megerősítésre vár, legfeljebb 15 perc" card rendered only briefly. The card is correct and its ≤15-minute promise is honest, but capturing both states on screen is a matter of luck at this speed.
  • BookStack skipped its first offsite runno recovery unit found on any connected drive (17:13:42). This is the expected pre-first-nightly-dump state; the nightly job is scheduled 2026-07-19 04:15 CEST. Watch item, not a finding — it becomes one only if it still skips afterwards.
  • BookStack's first start attempt failed (exit 1, 17:11:36) and succeeded on retry by 17:12:03. Self-corrected; noted for pattern-watching only.
  • felhom-samba was not running on the reborn box — sharing was not exercised this run (consistent with What was NOT exercised).

What was NOT exercised (explicit)

  • C6 — a customer performs a restore unassisted. The single open script step in R-3, and still MISSING as evidence. Remains remote-doable on the reborn box from vacation (the dashboard is remote).
  • freemail.hu deliverability. The gmail half is evidenced (claim email → Inbox under p=quarantine); freemail.hu is Viktor's open half.
  • Sharing smoke on the reborn box — R-7/R-7b were proven on the pre-RESET box; felhom-samba was not deployed here.
  • R-19 WAN-outage drill.
  • The PBS DR operator gauge — not captured, and F2 means it would have shown a fault.
  • BookStack's offsite unit — watch item, see Observations.
  • SMB positive auth with the real household password — carried over from R-7b, unchanged.

Discrepancies vs the Phase-A brief (live evidence wins)

  1. "expect ~11675 MB ballpark" — the final pct config reads 12288 MB. Both are right at different times: F5 auto-sized the guest to 11675 MB, then customer zero shrank it to 8192 (16:50:22) and grew it to 12288 (17:02:17). The brief described the F5 value; the box ended above it because the grow was the last resize. Not a fault — and it is what completes ledger 9.
  2. "2.6 GB pool vs ~0 attributed" — measured 1.4 G + 3.0 M unattributed against 49 M attributed, across TWO orphaned directories, not one. The F5 ruling is unaffected; the numbers are.
  3. "felhom-pbs entry present" — present but 401 Unauthorized / inactive. The brief treated the PBS descriptor as done; it is provisioned but dead. See F2.
  4. "RESET ×2 … second + third live firings" — the ledger holds exactly two rows, both tonight (demo-vm-felhom 15:49:57, demo-felhom 16:08:51), on two different customers. Any earlier firing is not in customer_resets. Described here as tonight's two firings rather than as ordinals.
  5. "keep the name R-29 if the number is free" — it is not free. R-29 was committed earlier the same day (2eccda4) for the design-v2 gate rot, which is also exactly the brief's item 5. The liveness item therefore takes R-30, and no new ID was minted for the red-gate item.
  6. The brief's tip a03e142 is two commits behind bbaa844; Viktor's R-3 draft landed as runbooks/RUNBOOK-onboarding-draft-v3.md, not DRAFT-R3-onboarding-runbook.md.
  7. F2 has no counterpart in the brief at all — it was found by probing the box rather than by following the finding list.

Evidence bundle

180:~/n100-rehearsal/ — see MANIFEST.md there for the per-file map. Secrets were read only to run probes and are recorded as lengths and metadata, never as values.