1d6f1c522d
gates / gates (push) Successful in 23s
The walk finished. All four planted files came back byte-identical out of snapshot 41c830db, including two Hungarian accented filenames verified as RAW NAME BYTES (NFC preserved) — the discriminator the Gate 0 positive control was built for, having been watched failing on an NFC->NFD rename that renders the same. Unlock 21s, restore 13.2s. It finished only because a terminal was available twice: - R-273 CLOSED. v0.128.0 was published as a package and never git-tagged, so every install died at 5/8. Tag pushed on operator instruction after an INDEPENDENT download proved the package sha equalled the vouched value; --resume then reached Day-0 SUCCESS in 3m49s on controller 0.210.0. The two guards that would stop the class recurring are still owed. - R-280 NEW, rank 1. A reinstalled box cannot re-attach its own data drive by any dashboard route: /api/disks/candidates returns empty because both lists are built from the UNCLAIMED-disk scan, and the drive is claimed precisely because it is also the backup target. Correct for "initialise", over-broad for "attach", which is non-destructive by definition. The restore page meanwhile says "Ez ket kattintas" and points at that empty list. Cleared by POSTing /mnt/sys_drive — an internal path no household could produce. Also new: R-281 the hub said NOTHING through the entire reinstall and the sealed-backup tripwire did not fire on a real unseal (positive control: 2 events all day fleet-wide); R-282 one code with three names and a mail pointing at a page the box does not show; R-283 hub reads "Claimed 18d ago" while the box serves its setup page; R-284 "almost full" over a 93%-free store. R-274 NARROWED by measurement rather than left as written: the resume path fetched the vouched golden correctly, because --resume skips the preflight that does local discovery. What survives is real — discovery is sort|tail -1 with no manifest comparison — but a FRESH install taking a stale golden is still not observed, and the row says so. Two of my own claims were refuted by test and are recorded as refuted, not quietly dropped: the leftover sudoers file is inert (sudo skips dotted names), and demo-hp's off-site tier was healthy all along.
713 lines
41 KiB
Markdown
713 lines
41 KiB
Markdown
# REHEARSAL — the BYO reinstall walk (2026-08-09)
|
||
|
||
> **Status: COMPLETE.** All phases walked; the integrity verdict is **BYTE-IDENTICAL**. §1–§8 were
|
||
> written before the destructive phase deliberately — a finding that exists only in a session that
|
||
> later crashes is a finding nobody has — and are left as written, including one claim later refuted
|
||
> by test (§8.3 → §9a) and one by measurement (§7.2 F-9).
|
||
|
||
**Venue: `demo-hp` (HP t740, `felhom-host`, guest 9201, customer `demo-hp`).** Operator-approved at
|
||
STOP 1. **Driven from DooPlex.** All times UTC unless marked; the host runs CEST (UTC+2).
|
||
|
||
---
|
||
|
||
## 1. Baselines — re-confirmed live on arrival, not taken from the spec
|
||
|
||
| | spec said | live reading | source |
|
||
|---|---|---|---|
|
||
| `felhom-agent` | v0.128.0 @ `28ba8593b8` | **0.128.0** on demo-felhom, **0.127.0** on demo-hp; HEAD == `origin/main` == `28ba8593b8` | `felhom-agent --version` on both nodes; `git rev-parse` |
|
||
| `felhom-controller` | v0.210.0 @ `c732fe1283` | **0.210.0** demo-felhom, **0.208.0** demo-hp; HEAD == `origin/main` == `c732fe1283` | hub `/configs`; `git rev-parse` |
|
||
| hub | v0.101.0 @ `56f8aa611c` | **0.101.0** (deployed image tag matches) | `kubectl get deploy hub`; page footer |
|
||
| Day-0 manifest | golden 0.210.0 / agent 0.128.0 / min 0.127.0 | **all three already saved** — golden `0.210.0` (`b9f701fa…`), agent `0.128.0` (`c6eba73b…`), min agent `0.127.0`, wrapper `104db0a4…` | hub `/configuration`, selected `<option>` values |
|
||
|
||
All three repos were clean and at `origin/main` before anything ran.
|
||
|
||
**The fleet is not uniform, and the venue is the box that is behind** — demo-hp runs agent 0.127.0 and
|
||
controller 0.208.0 against a manifest vouching 0.128.0/0.210.0. That is an argument *for* the venue: the
|
||
reinstall genuinely exercises the manifest rather than re-installing what is already there.
|
||
|
||
---
|
||
|
||
## 2. Venue comparison — the fidelity table that decided it
|
||
|
||
The runbook's criterion is *"the box whose current state most resembles what the tester will have"*.
|
||
|
||
| criterion | `demo-felhom` (N100) | `demo-hp` (t740) |
|
||
|---|---|---|
|
||
| customer apps deployed | 1 — opengist | **3 — privatebin, opengist, calibre-web** |
|
||
| enrolled user-data drive | **none registered.** `/mnt/felhom-drives` empty; controller logs `[WARN] Storage paths: no storage paths registered` | `/mnt/felhom-drives/Felhom-Share` + NVMe enrolled 2026-07-22; one registered storage path `/mnt/sys_drive` |
|
||
| off-site tier has actually run | **NO** — `offsite.state=needs_credential`, no run has ever succeeded | **YES** — 18 restic snapshots, daily, unbroken |
|
||
| escrow / recovery ceremony | present, generation 2 | present, generation 2 |
|
||
| install shape | mkimage loader, **Secure Boot OFF** (firmware workaround) | shim loader, **Secure Boot ENABLED** — the customer shape |
|
||
| collateral on the box | none | `drill-r50` (VM 300); leftover `c11-scratch` storage |
|
||
|
||
**Chosen: demo-hp.** It wins every fidelity criterion that distinguishes them.
|
||
|
||
**`drill-r50` is not at risk, and this was checked rather than assumed:** VM 300 is **not** in the
|
||
`felhom` pool (`pvesh get /pools/felhom` → members `[9201]`), its disks are on `local-lvm`, and
|
||
`--uninstall` removes no storage definitions and no non-pool guest — its own end-of-teardown statement
|
||
says the enrolled drives are *"unmounted only, NEVER wiped"* and that it never deletes PBS backups, hub
|
||
records or escrow blobs.
|
||
|
||
**Correction recorded, because it was part of the comparison put in front of the operator:** the
|
||
enrolled-drive row was first reported as *"real files on an enrolled drive"*. `Felhom-Share` is
|
||
**empty** — the share exists and is enrolled, but holds no files. demo-hp still wins the row (demo-hp
|
||
has a registered storage path; demo-felhom has none), on a narrower margin than first stated.
|
||
|
||
---
|
||
|
||
## 3. Pre-phase — R-268, the leaked per-guest local-API token
|
||
|
||
**Done first and independently, on `demo-felhom` guest 9201, as the runbook required.** No token value
|
||
reached stdout at any point: old value captured file→file from `bootstrap.json`, new value minted into
|
||
a file, both shredded at the end.
|
||
|
||
### 3.1 The rotation, and its proof
|
||
|
||
| step | old token | new token |
|
||
|---|---|---|
|
||
| before rotation | **HTTP 200** | — |
|
||
| bogus token (negative control) | **HTTP 401** | — |
|
||
| after the on-disk rotation, before any reload | **HTTP 200** ← *see 3.2* | 200 |
|
||
| after that lookup forced a reload | **HTTP 401** | — |
|
||
| after `systemctl restart felhom-agent` | **HTTP 401** | **HTTP 200** |
|
||
|
||
**Positive observable for the controller half:** `local-api: channel up (agent 169.254.253.1:8443) —
|
||
guest 9201, 3 mount(s) visible`, enumerating mp8/mp9/mp0 — data only the agent can supply. An absent
|
||
error would not have been evidence.
|
||
|
||
**R-268 is CLOSED.** The old token is refused; the new one works; the controller's channel is up.
|
||
|
||
### 3.2 FINDING — an out-of-process token rotation does not revoke the old token
|
||
|
||
`localapi.TokenStore.Mint`'s doc comment states *"last-write wins — any previous token for this guest is
|
||
revoked"*. **Across processes that is false until something else forces a reload.** The long-lived agent
|
||
serves `Lookup` from an in-memory index and re-reads the store **only on a miss** (the B3
|
||
reload-on-miss optimisation). A superseded token is still a direct map **hit**, so it authorises.
|
||
|
||
**Red-proved twice.** A temporary unit probe (Scenario C with the lookup order reversed — present the
|
||
rotated-out token *first*) fails on the claim; the shipped
|
||
`TestTokenStore_ReloadOnMiss_RemintCoherence` passes **only because it looks up the NEW token first**,
|
||
which is what triggers the reload that then evicts the old hash. Then proved on real hardware: the row
|
||
marked ← above is a live 200 from a token that the store on disk had already superseded.
|
||
|
||
This is the `CLAUDE.md` case exactly — a comment asserting an invariant with a test that appears to pin
|
||
it and does not. The honest fix is either a test that pins the reversed order, or an eviction that does
|
||
not depend on an unrelated lookup missing first.
|
||
|
||
### 3.3 FINDING — R-268's own stated rotation recipe is incomplete
|
||
|
||
The register row says the new plaintext *"must also be written into the guest's
|
||
`/etc/felhom-bootstrap/bootstrap.json` or the in-guest controller loses its agent access"*. **Writing
|
||
bootstrap.json is not sufficient.** `bootstrap.ensureLocalAPI` returns early when
|
||
`cfg.LocalAPI.Endpoint != ""` — it fills an *absent* block and never refreshes a present one — so after
|
||
a rotation the controller keeps serving the old token from its own `controller.yaml` **across a
|
||
restart**. Proved live: two controller restarts, still HTTP 401. The rotation only completed once
|
||
`local_api.token` was written into `controller.yaml` itself.
|
||
|
||
The neighbouring `DetectEndpointDrift` compares the **endpoint** and deliberately does not compare the
|
||
token (*"a token mismatch is a different failure"*). So this shape is known to be unmodelled, and R-78
|
||
("which file is authoritative") is its parent.
|
||
|
||
### 3.4 FINDING — the agent-channel alarm never closes, and its own prescribed remedy is why
|
||
|
||
`channelhealth` alerted correctly and promptly: `agent_channel_unauthorized`, no debounce, pushed to
|
||
the hub — *"agent rejected the controller token (HTTP 401) — token stale/rotated (**re-bootstrap**)"*.
|
||
|
||
The channel then recovered, and **no recovery event was ever sent**. In `Checker.Check`'s UP branch the
|
||
notify is guarded by `prev != "" && prev != "up"`, and a controller restart resets `state` to `""`. The
|
||
alert's own instruction is to re-bootstrap — i.e. restart the controller — so **following the alarm's
|
||
advice structurally guarantees the recovery is silent.** The down side is deliberately asymmetric
|
||
(F2: a born-down channel alerts on cycle 1); the up side is not.
|
||
|
||
Operator-visible result on the hub: two `agent_channel_unauthorized` errors, one `sent` and one
|
||
`suppressed` by cooldown, and nothing afterwards. The customer dashboard is fine — `SetDashboard`
|
||
reflects current state every cycle. It is the operator's trail that ends on "down".
|
||
|
||
---
|
||
|
||
## 4. Gate 0
|
||
|
||
### 4.1 The recovery code — CONFIRMED IN THE OPERATOR'S HANDS (STOP 1)
|
||
Not printed, not logged, not requested. It will be supplied at the screen at STOP 3.
|
||
|
||
### 4.2 The dataset — planted and fingerprinted
|
||
|
||
Planted at `/var/lib/felhom/sys_drive/felhom-data/userdata/media/books/rehearsal-2026-08-09/` — inside
|
||
the **Calibre library**, which is real app-owned customer data and is **demonstrably** carried off-site
|
||
(the path appears in every `felhom-offbox,calibre-web` snapshot). Chosen after checking coverage rather
|
||
than assuming it: the per-app tier-1 unit carries only the named docker volume, so a file dropped in an
|
||
arbitrary place would not have travelled.
|
||
|
||
| sha256 (16) | bytes | name | name bytes (hex) |
|
||
|---|---|---|---|
|
||
| `54b773c46bbfd994…` | 3 145 728 | `binary-3mb.bin` | `62696e6172792d336d622e62696e` |
|
||
| `52a5c5ebfcac247f…` | 59 | **`árvíztűrő-tükörfúrógép.txt`** | `c3a17276c3ad7a74c5b172c5912d74c3bc6bc3b67266c3ba72c3b367c3a9702e747874` |
|
||
| `15d2024dfc224162…` | 25 | **`nested/őszibarack.md`** | `6e65737465642fc591737a6962617261636b2e6d64` |
|
||
| `924497918e55fe6d…` | 21 | `plain.txt` | `706c61696e2e747874` |
|
||
|
||
Manifest: `documentation/audits/evidence/` → recorded in the session scratchpad as
|
||
`GATE0-before-manifest.json`. The manifest keys on **raw name bytes**, not rendered text.
|
||
|
||
### 4.3 The positive control — the comparator was watched failing, three ways
|
||
|
||
| control | expected | result |
|
||
|---|---|---|
|
||
| identical copy | PASS | **PASS**, exit 0 |
|
||
| one bit flipped at byte 1 500 000 of the 3 MB binary | FAIL | **FAIL** — named the file and both sha256s |
|
||
| **NFC→NFD rename of the accented name** (renders *identically*) | FAIL | **FAIL** — caught as MISSING+EXTRA on the name bytes |
|
||
| one file deleted | FAIL | **FAIL** — named the missing file |
|
||
|
||
The NFD case is the one that matters: a comparison of *rendered* filenames would have passed it. Every
|
||
altered copy was discarded after the control ran.
|
||
|
||
### 4.4 The off-site tier — has run since the dataset was planted
|
||
|
||
Triggered through the product's own dashboard button (`POST /backup/offbox/run` → flash *„A távoli
|
||
mentés elindult"*), not by invoking restic by hand.
|
||
|
||
```
|
||
[offbox] backup run started (3 app(s) toggled)
|
||
[offbox] pre-push dump leg completed in 7.329s — snapshot pair is coherent
|
||
[offbox] backed up calibre-web (…/backups/primary/calibre-web, 1 mandatory path(s))
|
||
[offbox] backup OK: 3 app(s) backed up, 18 snapshot(s), 1m22s
|
||
```
|
||
|
||
**The restore point, by identity — never by size:**
|
||
|
||
| snapshot | time (UTC) | tag | paths |
|
||
|---|---|---|---|
|
||
| **`41c830db`** | 2026-08-09 08:30:38 | `felhom-offbox,calibre-web` | `…/backups/primary/calibre-web` + `…/userdata/media/books` |
|
||
| `9e38b84c` | 2026-08-09 08:30:49 | `felhom-offbox,opengist` | `…/backups/primary/opengist` |
|
||
| `78b93f04` | 2026-08-09 08:30:53 | `felhom-offbox,privatebin` | `…/backups/primary/privatebin` |
|
||
|
||
`restic ls 41c830db` confirms all four planted files are present, **both accented filenames included**.
|
||
|
||
### 4.5 The box, captured
|
||
`GATE0-demo-hp-before.txt` (282 lines): agent version, `pveversion`, `pct`/`qm` lists, pool membership,
|
||
`pvesm status`, `storage.cfg`, `df`, `lsblk` with serials, the agent unit + service user, sudoers, the
|
||
pveum users/tokens/roles/ACL, the guest hook, the WireGuard tunnel, the OOB belt, the agent state dir,
|
||
the install state file, PBS snapshots, local vzdump archives, and the enrolled-drive tree.
|
||
|
||
---
|
||
|
||
## 5. Findings so far, ranked by what they cost the person in front of you
|
||
|
||
Nothing yet is rank 1 or 2 — the walk has not started.
|
||
|
||
### Rank 3 — misleads
|
||
|
||
- **F-1 · A rotated-out local-API token still authorises** (§3.2). *A customer would do nothing — they
|
||
cannot see it. An operator rotating a leaked token would believe it revoked when it is not.* The
|
||
shipped test passes for the wrong reason.
|
||
- **F-2 · R-268's rotation recipe is incomplete** (§3.3). *Whoever follows it gets a box whose agent
|
||
channel is dead across restarts, with a correct-sounding alarm and no hint that `controller.yaml` is
|
||
the file that matters.*
|
||
- **F-3 · The agent-channel alarm never closes** (§3.4). *The operator fixes it as instructed and is
|
||
never told it worked; the hub's last word stays "down".*
|
||
- **F-4 · Three hub surfaces jointly misrepresent a healthy off-site tier as absent.** The customer page
|
||
reads `Snapshots 0 / Repo Size 0 MB / Integrity Unknown` (that panel renders the **local** tier, while
|
||
the healthy `offsite` object — `snapshot_count: 18`, `last_status: ok` — sits unrendered in the same
|
||
report); the Offsite page shows `0.0 GB` (true, but a 162 KB repo rounds to nothing); and a stale
|
||
`offsite_delivery_stuck` event from 2026-08-07 reads as current. *Three independent surfaces agreeing
|
||
on a wrong picture is how a working backup gets "fixed".* **This one caught me** — it produced a wrong
|
||
statement to the operator at STOP 1, corrected before it changed anything.
|
||
- **F-5 · There is no operator-triggerable off-site backup.** The only route is the customer's dashboard
|
||
button; `signed_jobs` needs a signing key the hub does not hold. Sibling of R-177.
|
||
|
||
### Rank 4 — cosmetic / hygiene
|
||
|
||
- **F-6 · Leave-behinds from earlier sessions**, found while capturing state:
|
||
- demo-hp host: storage `c11-scratch` (Campaign 11), and `felhom-backup:…vzdump-lxc-9100-2026_08_03…`
|
||
for a guest that no longer exists;
|
||
- demo-hp guest 9201 `/root`: `.dpw` (13 bytes), `.h`, `.sec.html`, all 2026-08-07 16:08;
|
||
- demo-felhom guest 9201: `/root/r204-backup/offbox.orig`.
|
||
- **F-7 · The managed-update floor is `0.200.0`, not `0.156.0`** — the runbook's §8.3 premise is stale.
|
||
Confirmed twice: hub `/configuration` reads *"Effective floor v0.200.0 — source: DB (hub_settings);
|
||
env fallback would be v0.120.0"*, and both boxes log `settle-gate: GO — at/above floor 0.200.0`. The
|
||
ruling's **substance** still needs recording; its **number** must not be written down as 0.156.0.
|
||
|
||
### Not a finding, checked and cleared
|
||
- The two `userdata` trees under `sys_drive` are the **same directory** surfaced twice, not a
|
||
backed-up copy beside an unprotected one. Checked against calibre-web's actual bind mounts.
|
||
|
||
---
|
||
|
||
## 6. Steps taken off-path, and what they cost
|
||
|
||
Rule 2 (no shell after the install command returns) does not bind yet — the walk has not begun. These
|
||
are Gate 0 preparation acts by the operator, recorded because they are mutations:
|
||
|
||
1. **R-268 rotation on demo-felhom** — token store appended, `bootstrap.json` and `controller.yaml`
|
||
rewritten, agent and controller restarted. Required by the runbook's pre-phase. *Not the venue.*
|
||
2. **demo-hp's dashboard password was re-set to the value in the operator's credentials file.** The
|
||
customer-owned password was not known to this session, and no operator-side route to the off-site
|
||
button exists (F-5). The prior `password_hash` is preserved at `/root/.orighash` and
|
||
`/root/settings.json.rehearsal-orig` in guest 9201; `claimed` was left `true`. **Done on operator
|
||
instruction.** Cost to fidelity: none for the walk — P1 destroys this guest, and P4 re-claims with a
|
||
password the operator chooses.
|
||
3. **The off-site run was started by a script pressing the dashboard's own endpoint**, with a real
|
||
session and a real CSRF token, rather than by a person clicking. The server path is identical; only
|
||
the click is synthetic.
|
||
|
||
---
|
||
|
||
## 7. P1 — removing Felhom from the host
|
||
|
||
`--uninstall`, the product's own path, dry-run first. **60 seconds** (08:37:23 → 08:38:23 UTC), exit 0.
|
||
The typed-vmid confirmation was satisfied, not bypassed.
|
||
|
||
### 7.1 The reversal, proved rather than trusted
|
||
|
||
| component | before | after |
|
||
|---|---|---|
|
||
| agent binary | `/usr/local/bin/felhom-agent` 0.127.0 | **ABSENT** |
|
||
| `felhom-agent` service user | present | **ABSENT** |
|
||
| `felhom-op` (OOB) user | present | **ABSENT** |
|
||
| `/etc/felhom-sshd` (OOB belt) | present | **ABSENT** |
|
||
| systemd units | agent, mgmt-watchdog, felhom-sshd, oob-nft, shared-parent | **all removed** |
|
||
| pveum roles | `FelhomAgentBase/Guest/Store` | **none** |
|
||
| pveum user + token | `felhom-agent@pve!agent` | **gone**; user list is `root@pam` only |
|
||
| pveum ACL | pool + 9 scratch-vmid + 3 storage grants | **empty** |
|
||
| `felhom` pool | members `[9201]` | **`[]` — pool removed** |
|
||
| guest 9201 | running | **destroyed** |
|
||
| guest-hook snippet | present | **removed** |
|
||
| dnsmasq snippets | present | **removed** |
|
||
| `/var/lib/felhom-agent`, `/var/lib/felhom-install` | present | **removed** |
|
||
| **KEPT as declared** | | PBS snapshots (2), local vzdump (4), `/mnt/felhom-drives/Felhom-Share`, storage defs |
|
||
|
||
Space returned: `local-lvm` 20 904 790 → 12 355 143 KiB (**≈8.5 GiB**); `local` ≈64 MiB; the NVMe
|
||
unchanged (backups deliberately kept). `drill-r50` (VM 300) untouched, as predicted.
|
||
|
||
### 7.2 What it left behind that it did not declare
|
||
|
||
**F-8 · Five orphaned `agent.json.*` backups survive, each carrying a hub API key and a Proxmox
|
||
token.** `/etc/felhom-agent/` remains with `agent.json.campaign8-before`, `.campaign9-before`,
|
||
`.campaign9-prev`, `.pre-e-target-move`, `.pre-prunegate.bak` — all 0600, three owned by the **now-deleted
|
||
service user's uid 999**, each holding a 64-char `hub.api_key` and a 59-char `proxmox.token`.
|
||
|
||
The uninstall's own WIPED statement claims it removes *"config (+ its .bak backups)"*, and
|
||
`scripts/CHANGELOG` records F1 as *"uninstall now purges the agent config's `.bak*` siblings (one held a
|
||
live hub api_key)"*. **That fix does not match the filenames in use** — and note it misses
|
||
`agent.json.pre-prunegate.bak`, a file that literally ends in `.bak`.
|
||
|
||
**Assessed rather than assumed: these particular secrets are SUPERSEDED, not live.** The orphaned
|
||
`hub.api_key` hashes to `a5d2222a…`; the hub's current key for demo-hp is `8c59d1b6…`. The Proxmox token
|
||
was deleted by this same uninstall. **The exposure is timing-dependent, not absent** — a backup taken
|
||
shortly before an uninstall would hold a current key, which is exactly what R-179's precedent records.
|
||
The broken thing is the stated guarantee.
|
||
|
||
**And the reinstall hands them to the new service account.** The rebuilt box recreates `felhom-agent` at
|
||
**uid 999 — the same uid the deleted one had** — so the three backups owned by uid 999 become the new
|
||
account's files. Verified: `su -s /bin/sh felhom-agent -c 'test -r …'` succeeds on
|
||
`agent.json.campaign8-before` and `agent.json.pre-prunegate.bak`. A fresh install's service account
|
||
inherits read access to the previous install's credentials. Superseded here; live if the backups are recent.
|
||
|
||
**F-9 · A stale sudoers file survives — and it is INERT. Hypothesis raised, then refuted by test.**
|
||
`/etc/sudoers.d/felhom-agent.bak-pre-e2a` (21 833 bytes, 2026-07-28, 0440 root:root) survives the
|
||
uninstall and grants `felhom-agent ALL=(root) NOPASSWD: FELHOM_MOUNT, FELHOM_DISK, FELHOM_PROVISION, …`,
|
||
and `visudo -c -f` on it says **`parsed OK`**.
|
||
|
||
The obvious worry — that the reinstall recreates the `felhom-agent` user and the box silently regains a
|
||
pre-E-2a privilege set — **was tested against the rebuilt box and is FALSE.** With the user recreated
|
||
(uid 999) and this as the *only* felhom file in `/etc/sudoers.d`, `sudo -l -U felhom-agent` returns
|
||
**"User felhom-agent is not allowed to run sudo on felhom-host."** sudo ignores files in `sudoers.d`
|
||
whose names contain a dot, so `….bak-pre-e2a` is never loaded. That is a robust reason, not luck —
|
||
`visudo -c -f <file>` checks a file explicitly and therefore does **not** tell you whether sudo loads it.
|
||
|
||
**What is left is rank 4:** 21 KB of the privilege grammar lingering on a host Felhom was told to leave,
|
||
inside a teardown that claims to have removed the sudoers file.
|
||
|
||
**F-10 · The WireGuard tunnel to Felhom's off-site endpoint stays up.** After an uninstall,
|
||
`wg-quick@wg-felhom` is **enabled and active**, `/etc/wireguard/wg-felhom.conf` present, with a live
|
||
handshake to `167.233.158.164:443` 52 seconds old and 5.86 GiB received / 2.48 GiB sent on the counter.
|
||
It appears in **neither** the WIPED nor the KEPT list. A box told to leave Felhom keeps a live inbound
|
||
route into Felhom's infrastructure, and the operator is not told.
|
||
|
||
**F-11 · Smaller undeclared residue:** `/etc/felhom/` (`.bootstrap-done`, `appliance-pairing-code` —
|
||
the console pairing code); `/etc/systemd/system/felhom-bootstrap.service` + `/usr/local/sbin/felhom-bootstrap.sh`;
|
||
the `vmbr9` island-bridge stanza (169.254.253.1/30) still in `/etc/network/interfaces`.
|
||
|
||
**F-12 · A teardown-ordering warning:** `user config - ignore invalid acl role 'FelhomAgentStore'` —
|
||
the role is deleted before an ACL that still references it. Harmless here; noise that would mask a real one.
|
||
|
||
---
|
||
|
||
## 8. P2 — the preflight, as the owner runs it
|
||
|
||
### 8.1 The refusals, judged on their messages
|
||
|
||
**First run, no caps:** `byo mode requires explicit --cores and --memory (noisy-neighbor caps on a host
|
||
you do not own)`. **Good** — names the missing thing and the reason.
|
||
|
||
**Second run:** it refused again —
|
||
|
||
```
|
||
[ERROR] a resolver is already bound to :53 on this host — Felhom needs the guest reachable by name on your LAN.
|
||
Stop or reconfigure that resolver, OR point your LAN DNS at the guest's address, then re-run.
|
||
(Felhom does NOT touch DNS services on a host it does not own — this is a refusal, not a change.)
|
||
```
|
||
|
||
As a *message* this is excellent: the finding, the evidence, two routes out, and an explicit promise
|
||
about what it will not do.
|
||
|
||
### 8.2 F-13 (RANK 1 — STOPS THE VISIT) · Felhom's uninstall leaves the exact condition that makes Felhom's reinstall refuse
|
||
|
||
The resolver it is refusing over **is Felhom's own**, and the chain is fully evidenced:
|
||
|
||
| fact | evidence |
|
||
|---|---|
|
||
| Felhom installed `dnsmasq` | `/var/lib/dpkg/info/dnsmasq.list` dated **2026-07-21 18:24 CEST** — demo-hp's Felhom day-0 |
|
||
| it was constrained by a Felhom snippet | `/etc/dnsmasq.d/` held Felhom snippets before; only `README` after |
|
||
| **the uninstall restarted it unconstrained** | the running process started **2026-08-09 10:37:39 CEST — inside the uninstall window (10:37:23–10:38:23)** |
|
||
| unconstrained, it grabs the wildcard | `0.0.0.0:53` and `[::]:53`, `--local-service`, empty `dnsmasq.conf` |
|
||
| it is not PVE's | `/etc/pve/sdn/` empty; unit is stock `/usr/lib/systemd/system/dnsmasq.service`, **enabled** |
|
||
|
||
The uninstall mentions it only as *"the 'sudo' and 'dnsmasq' packages were left installed (**system
|
||
packages**)"* — but dnsmasq is not a system package here; Felhom installed it, and it left it **enabled
|
||
and running as a wildcard resolver**.
|
||
|
||
**What a customer would do next:** read a message that says a resolver is in the way, conclude their own
|
||
network setup is at fault, and start debugging DNS they never configured. On a BYO box with no shell
|
||
guidance, this is where the visit stops.
|
||
|
||
**Confirmed by counterfactual:** `systemctl stop dnsmasq && systemctl disable dnsmasq` → `host DNS (:53):
|
||
free` → **PRE-FLIGHT PASS**. Nothing else changed. *(Recorded as an intervention — it is the action the
|
||
message tells the owner to take.)*
|
||
|
||
### 8.3 F-14 (RANK 2) · A local golden is adopted with NO version and NO checksum check
|
||
|
||
Preflight reported `golden (local): local:backup/vzdump-lxc-9100-2026_08_03-07_33_00.tar.zst`. Step 7 is:
|
||
|
||
```sh
|
||
if [[ -n "$GOLDEN_VOLID" ]] && ! $FORCE_GITEA_GOLDEN; then
|
||
log_skip " using local golden: $GOLDEN_VOLID"
|
||
_state_mark golden; return 0
|
||
fi
|
||
```
|
||
|
||
**The hub manifest's `golden.sha256` — the whole point of the hub vouching a different trust root than
|
||
Gitea — is consulted only on the fetch path.** A locally-present archive bypasses the vouch entirely: no
|
||
version comparison, no digest, no warning.
|
||
|
||
**What that archive actually contains, read out of the tar:**
|
||
`gitea.dooplex.hu/admin/felhom-controller:`**`0.192.0`**, against a vouched golden of **0.210.0**.
|
||
|
||
Two consequences, and the second is the serious one:
|
||
|
||
1. The rebuilt box comes up **18 controller releases stale**, reporting success.
|
||
2. **0.192.0 is below 0.200.0 — the release where R-193's off-site recovery SCREEN shipped.** So a
|
||
customer reinstalled today returns on a controller that cannot run the recovery ceremony their data
|
||
depends on. It is also born below the managed-update floor (0.200.0), and the updater's auto-target
|
||
is the floor, never the newest — so it converges to 0.200.0, still not the vouched build.
|
||
|
||
**This is the R-111/R-115/R-120 drift family, one layer down:** the R-120 gate guards what the operator
|
||
may *vouch*; nothing guards what an install actually *takes*. It also compounds with P1 — the uninstall
|
||
deliberately keeps the old golden (*"golden vzdump left in place"*), and the reinstall then adopts it.
|
||
|
||
**The walk continues unflagged** (`--force-gitea-golden` NOT passed), because measuring the path a real
|
||
reinstall takes is the point. §9 records which controller version actually came up.
|
||
|
||
### 8.4 The BYO mutation disclosure — read as a customer
|
||
|
||
Comprehensive and honest: every pveum artifact, the sudoers file, the state and config paths, the
|
||
packages it will install, the guest's capped size, the self-update authority, and — creditably — the
|
||
**outbound WireGuard tunnel**, in plain words, with the note that the operator can disable it per
|
||
customer. It closes with `NOT touched in byo mode: root@pam (no break-glass), host DNS (:53), OOB sshd.`
|
||
|
||
**One material omission, given §8.3:** it does not say that a pre-existing local golden may be used
|
||
instead of the vouched one. The disclosure lists everything the install will *create* and nothing about
|
||
what it may *reuse*.
|
||
|
||
---
|
||
|
||
## 9. P3 — the install. IT FAILED, AND THE WALK STOPS HERE
|
||
|
||
Started 08:51:33 UTC, **failed 08:52:17 UTC (44 s), exit 1, at step 5/8.** Run unflagged, as a real
|
||
owner would. The typed hostname acknowledgement was given, not bypassed.
|
||
|
||
```
|
||
[INFO] manifest: agent v0.128.0 (sha c6eba73bf9b9ad69…), golden v0.210.0
|
||
[OK] verified sha256 c6eba73bf9b9ad69… matches the hub manifest
|
||
[OK] installed /usr/local/bin/felhom-agent (felhom-agent 0.128.0)
|
||
[OK] created service user felhom-agent
|
||
curl: (22) The requested URL returned error: 404
|
||
[ERROR] raw fetch failed: configs/felhom-mkfs-guarded.sh (agent tag v0.128.0 — is that version tagged in felhom-agent?)
|
||
```
|
||
|
||
### 9.1 F-15 (RANK 1 — STOPS EVERY INSTALL, FLEET-WIDE) · The vouched agent version was never tagged
|
||
|
||
**`v0.128.0` does not exist as a git tag** — not locally, not on Gitea. The newest tag is `v0.127.0`.
|
||
The *binary* was published to the Gitea package registry (which is why it is selectable in the hub's
|
||
artifact dropdown and why its sha256 verified), but the release's tag step never happened.
|
||
|
||
| ref | HTTP |
|
||
|---|---|
|
||
| `raw/branch/main/configs/felhom-mkfs-guarded.sh` | **200** |
|
||
| `raw/tag/v0.127.0/configs/felhom-mkfs-guarded.sh` | **200** |
|
||
| **`raw/tag/v0.128.0/configs/felhom-mkfs-guarded.sh`** | **404** |
|
||
|
||
The commit that *is* 0.128.0 exists and is on `main`: `28ba8593b8 — v0.128.0 — R-221: the escrow seed is
|
||
asserted every tick, not remembered once`. Only the tag is missing.
|
||
|
||
**This is R-184 happening.** R-184 reads: *"Nothing prevents the hub from vouching an agent version that
|
||
was never released. The R-115 gate proves every RELEASED version is installable, but it works from git
|
||
tags."* It was filed as a gap. It is now a live outage.
|
||
|
||
**It is the collision of a good fix with an open gap.** R-183 correctly hardened the installer to pin
|
||
config fetches to the vouched tag (`hostinstall_gates.py` even asserts the pin exists —
|
||
`ok: fetch_raw pins the agent configs to the vouched agent version`). Nothing asserts that the pinned ref
|
||
**resolves**. So the first vouch naming an untagged version fails every install closed.
|
||
|
||
**The release script already knows.** `felhom-agent/scripts/release-agent.sh:23` says in as many words:
|
||
*"`raw/tag/v<version>`, a released version without a git tag 404s a box mid-install, as root."* The
|
||
hazard was understood, documented, and the tag step was still missed.
|
||
|
||
**Blast radius — stated precisely, not inflated:**
|
||
- **Every fresh install and every reinstall, on every host, fails at step 5/8** for as long as the Day-0
|
||
manifest vouches agent 0.128.0. That manifest was approved on 2026-08-09; the approval is what armed it.
|
||
- **Existing boxes are unaffected.** demo-felhom runs 0.128.0 happily — the self-update path takes the
|
||
binary from the package registry, not from a tag.
|
||
- So the damage is confined to the install path. That is also the only path a new customer, or a
|
||
reinstalling tester, ever walks.
|
||
|
||
**What a customer would do next:** nothing. `curl: (22) … 404` and a question about a git tag is not a
|
||
message a household can act on. The visit ends here and a developer is required.
|
||
|
||
### 9.2 The box's state, left as found for inspection
|
||
|
||
`--uninstall` is not re-run and no manual repair is attempted (rule 1). demo-hp is mid-install:
|
||
|
||
```
|
||
state.json completed: [preflight, token, grows, enroll] mode: byo
|
||
agent 0.128.0 installed, unit NOT created (not-found), inactive
|
||
user felhom-agent uid=999 created
|
||
pveum 2 users, 3 roles, 63 ACL lines, pool 'felhom' recreated (empty)
|
||
guest none
|
||
```
|
||
|
||
The documented `--resume` path exists and **was deliberately not used: it cannot help.** Resume replays
|
||
from `enroll`, hits the same 404, and fails identically — the missing tag is not a transient.
|
||
|
||
### 9.3 The one-line unblock, for the operator to decide
|
||
|
||
```sh
|
||
cd /mnt/5_hdd/felhom.eu/git/felhom-agent
|
||
git tag v0.128.0 28ba8593b8 && git push origin v0.128.0
|
||
```
|
||
|
||
This publishes the tag for a commit that is already on `main` and already released as a package — it
|
||
completes a half-finished release rather than changing any code. The alternative, rolling the manifest
|
||
back to agent 0.127.0, costs R-221 — the fix `STATUS.md` calls *"the last item blocking a customer from
|
||
something we promise them"* — so it is the worse option.
|
||
|
||
**Not done in this session.** Publishing a release tag is an outward-facing act and the runbook is
|
||
explicit: *"If a defect is so severe that the run cannot continue, stop and report; do not fix it here."*
|
||
|
||
### 9.4 What F-14 still owes
|
||
|
||
Step 7 never ran, so **whether the install would actually have adopted the stale 0.192.0 golden is not
|
||
yet observed** — F-14 rests on the code path plus the preflight's `golden (local): …9100…2026_08_03…`
|
||
line and the archive's own `felhom-controller:0.192.0` marker. That is strong, but it is not the
|
||
observation. **When the tag is published and the walk resumes, the first thing to record at step 7/8 is
|
||
which golden it takes.**
|
||
|
||
---
|
||
|
||
## 9a. P3 resumed — and the install succeeded
|
||
|
||
**Unblocked on operator instruction ("proceed").** `v0.128.0` was annotated at `28ba8593b8` and pushed
|
||
after an **independent download** confirmed the published package's sha256 equals the hub's vouched
|
||
`c6eba73b…`. Both config URLs then served 200. `--resume` completed in **3 m 49 s**
|
||
(09:30:30 → 09:34:19 UTC): `Day-0 provision SUCCESS`, controller **0.210.0**, agent **0.128.0**, guest
|
||
9201 onboot, pool member, all 16 ACL assertions green, controller healthy in ~18 s.
|
||
|
||
**R-274 partly refuted, and recorded as such.** Step 7 fetched the **vouched 0.210.0** — because
|
||
`--resume` skips preflight, and preflight is where local auto-discovery sets `GOLDEN_VOLID`. So the
|
||
fresh and resume paths disagree, and the resume path is the safe one. What survives: discovery is
|
||
`sort | tail -1` (newest local) with **no manifest comparison**, so a box whose newest local golden
|
||
predates the vouched one still installs stale. Full text in the register.
|
||
|
||
## 10. P4 — first contact, as the customer
|
||
|
||
**A clean pass, and worth saying so plainly.** Within four minutes of the install finishing, the
|
||
customer's own URL — fetched from outside the box, over the public internet — served:
|
||
|
||
> **A szerver beállítása** · Demo HP
|
||
> *Add meg az e-mailben kapott beállító kódot, majd válassz saját jelszót a vezérlőpult védelméhez.*
|
||
> Beállító kód · Új jelszó (min. 12 karakter) · Új jelszó megerősítése
|
||
> *Nem kaptad meg a kódot? Új kód kérése*
|
||
|
||
Unprompted, in Hungarian, naming the customer, with a self-service route if the code never arrived,
|
||
and nothing anywhere asking for a command line. The hub showed the host **ONLINE** at agent 0.128.0.
|
||
|
||
**Two findings here, neither fatal:** the hub still read *"Claimed 18d ago"* while the box was serving
|
||
its first-run page (**R-283**), and the code that arrives is named three different things across the
|
||
three surfaces, with the mail pointing at an „Elfelejtett jelszó" page the box does not show
|
||
(**R-282**). The reset code was nonetheless **accepted on the setup page** — 302 and a session — so
|
||
this is naming, not function.
|
||
|
||
## 11. P5 — getting the machine back to work
|
||
|
||
### 11.1 The recovery screen, unsought — the headline pass
|
||
|
||
The **first thing** on the dashboard after claiming, with nothing sought:
|
||
|
||
> **Adatok visszaszerzése** — *Ezt a gépet újratelepítették. A korábbi, házon kívüli mentéseid
|
||
> megvannak* — a sealed package held centrally, **sealed at 2026-08-04T11:11:37Z**, openable only with
|
||
> the customer's code; *nobody can replace it — not Felhom, not support, not the operator*; and
|
||
> **„Ebben a lépésben semmit nem állítunk vissza és semmi nem változik."**
|
||
> Field: **Helyreállítási kód (tíz szó)**.
|
||
|
||
R-193's screen meeting reality on a genuinely rebuilt box. It answered all three of its questions and
|
||
its seal date matches `host_escrow.created_at` exactly.
|
||
|
||
*(Nit: the seal date is rendered raw as `2026-08-04T11:11:37Z` to a Hungarian household — ISO-8601 in
|
||
UTC where a localised date belongs.)*
|
||
|
||
### 11.2 STOP 3 — the unlock
|
||
|
||
Entered at the box's own screen, from a file, never on a command line. **21 seconds**, and it listed
|
||
what it found without restoring anything:
|
||
|
||
| app | legutóbbi mentés | méret |
|
||
|---|---|---|
|
||
| **calibre-web** | 2026-08-09 10:30 (CEST) | **3.8 MB** |
|
||
| felhom-offbox | ″ | 6.8 KB |
|
||
| opengist | ″ | 182.3 KB |
|
||
| privatebin | ″ | 6.8 KB |
|
||
|
||
That is exactly the Gate 0 snapshot set, seen from the customer's side.
|
||
|
||
### 11.3 The wall — R-280, and it is rank 1
|
||
|
||
The restore page diagnoses the situation perfectly and then sends the customer to an empty page:
|
||
|
||
> *„Előbb csatold vissza az adatmeghajtót. … **Ez két kattintás:** Tárhely → Meghajtók, »Meglévő
|
||
> meghajtó csatolása«. Utána gyere vissza ide."*
|
||
|
||
**It is not two clicks; it is zero possible clicks.** `GET /api/disks/candidates` →
|
||
`{"initialize":[],"attach":[]}`. The agent is fine — `GET /api/disks` returns the NVMe in full — but
|
||
`handleDiskCandidates` builds *both* lists from the **unclaimed-disk** scan, and demo-hp's NVMe is
|
||
deliberately both the user-data drive and the `felhom-backup` target, so it is claimed and never
|
||
offered. Correct for `initialize`; over-broad for `attach`, which is non-destructive by definition.
|
||
|
||
It cascades: no store → Calibre-Web's install page degrades to *„Nincs regisztrált adattároló — adja
|
||
meg kézzel az útvonalat"*; no app → every restore row reads „Nincs telepítve".
|
||
|
||
**Escape hatch used, and recorded as off-path:** `POST /settings/storage/add` with
|
||
`storage_path=/mnt/sys_drive` succeeded first try — an internal path, the very one registered before
|
||
the wipe, that no household customer could produce. Everything unblocked immediately afterwards and
|
||
the deploy form became a proper picker („Tárhely (sys_drive) — 64.2 GB szabad").
|
||
|
||
### 11.4 App redeploy
|
||
|
||
`calibre-web` deployed from the catalogue through the dashboard's own API in **1 m 36 s**, running and
|
||
healthy. The catalogue itself was complete and in Hungarian, every app „Nincs telepítve".
|
||
|
||
*(Also observed: the deploy page warned „A kiválasztott tárhely majdnem megtelt." over an option
|
||
carrying `data-free-percent="93"` and „64.2 GB szabad" — an apparent inverted threshold, **R-284**.)*
|
||
|
||
## 12. P6 — getting the data back
|
||
|
||
Three options, each explained, none starting without a question. **R-204 item 3 is visibly fixed:**
|
||
option 1 states in as many words that it does *not* bring the customer's own files back.
|
||
|
||
| step | wall clock |
|
||
|---|---|
|
||
| full-restore **prepare** (3.8 MB) | **8 s** |
|
||
| full-restore **execute** | **13.2 s** (10:27:32.08 → 10:27:45.29 UTC) |
|
||
|
||
> *„A(z) calibre-web teljes mentése visszaállítva ellenőrző mappába:
|
||
> `/mnt/sys_drive/felhom-data/backups/offsite-restore/calibre-web` — a saját fájljaiddal együtt. A
|
||
> meglévő adatok változatlanok."*
|
||
|
||
Restored to a **verification folder**, not into place — honest about it, and R-213's territory.
|
||
|
||
### THE INTEGRITY VERDICT — **BYTE-IDENTICAL**
|
||
|
||
```
|
||
expected 4 file(s); found 4
|
||
VERDICT: BYTE-IDENTICAL
|
||
```
|
||
|
||
Compared against `GATE0-before-manifest.json`, keyed on **raw name bytes**:
|
||
|
||
| sha256 | bytes | name |
|
||
|---|---|---|
|
||
| `54b773c46bbfd994…` | 3 145 728 | `binary-3mb.bin` |
|
||
| `52a5c5ebfcac247f…` | 59 | **`árvíztűrő-tükörfúrógép.txt`** — name bytes `c3a1…` identical, NFC preserved |
|
||
| `15d2024dfc224162…` | 25 | **`nested/őszibarack.md`** — name bytes identical |
|
||
| `924497918e55fe6d…` | 21 | `plain.txt` |
|
||
|
||
Four expected, four restored, zero differences. The accented filenames survived as **bytes**, not
|
||
merely as rendered text — the discriminator the Gate 0 positive control was built to enforce.
|
||
|
||
## 13. P7 — what the hub thought was happening
|
||
|
||
**It thought nothing.** Across the whole reinstall the hub recorded **zero events and zero
|
||
notifications** for demo-hp. Positive control (standing rule 3): the hub logged **2 events all day
|
||
across all customers**, newest `db_dump_completed` at 00:30:07 — the store is reachable and the
|
||
silence is real.
|
||
|
||
- **The good half:** no false alarm fired during a legitimate reinstall, which is what P7 watches for.
|
||
- **The owed half (R-281):** `escrow_blob_served` exists as the tripwire for exactly this moment —
|
||
*"If no recovery is in progress on that box, investigate"* — and **has fired for demo-hp before**
|
||
(last 2026-08-04 20:12:54). Today's real unseal fired it not at all. A reinstall and a stolen
|
||
machine look identical to the operator.
|
||
|
||
## 14. Teardown — all four layers
|
||
|
||
1. **The machine** — my instruments (`fp.py`, `restored.json`, the installer, the passphrase file)
|
||
removed. Guest 9201 running and healthy. **`drill-r50` (VM 300) untouched throughout**, as predicted.
|
||
2. **The host** — `local-lvm` 36.97 % → 21.85 % (bare) → **32.35 %** (rebuilt). Pre-existing leftovers
|
||
found and deliberately **not** removed, recorded instead: `c11-scratch`, the orphaned
|
||
`vzdump-lxc-9100` archives (now three).
|
||
3. **The hub** — **no customer or appliance record created**; `demo-hp` retained deliberately per the
|
||
runbook. Nothing to delete.
|
||
4. **The off-site side** — **18 snapshots, newest still `9e38b84c` / `78b93f04` at 08:30 UTC**, i.e.
|
||
unchanged since Gate 0. The restore was a pure read. **No prune, no forget, no delete by me**; the
|
||
only retention that ran was inside the product's own backup call at Gate 0, which reported
|
||
`18 snapshot(s)` itself.
|
||
|
||
## 15. The answer to §2's question
|
||
|
||
**Yes — the data comes back, byte for byte. No — not in one sitting, and not without a shell.**
|
||
|
||
The walk completed: **P1 ✓ P2 ✓ P3 ✓ P4 ✓ P5 ✓ P6 ✓ P7 ✓**, and the verdict is BYTE-IDENTICAL.
|
||
But it completed only because two hard stops were cleared by someone who could open a terminal and
|
||
read source code:
|
||
|
||
1. **R-273** — the install died at 5/8 on a git tag that was never pushed. Cleared by a release action.
|
||
2. **R-280** — the data drive could not be re-attached through any dashboard route, while the page
|
||
promised „két kattintás". Cleared by POSTing an internal path a customer could not know.
|
||
|
||
Neither is a data-integrity problem. Both are **journey** problems, and both stop a household dead.
|
||
This is the same shape the R-201 walks kept finding: **the data half passes, the journey half fails.**
|
||
|
||
**What the product did beautifully, and should not be lost in the finding count:** the box came up on
|
||
its own at the vouched version; the setup page appeared unprompted, in Hungarian, naming the customer;
|
||
the recovery screen appeared **without being sought** and stated plainly that nothing would be changed
|
||
by unlocking; the unlock took **21 s**; the restore took **13.2 s** and said honestly that it had put
|
||
the files in a verification folder rather than back in place. Sixteen ACL assertions verified
|
||
themselves. No false alarm fired.
|
||
|
||
### Wall clocks
|
||
|
||
| segment | |
|
||
|---|---|
|
||
| P1 uninstall | **60 s** |
|
||
| P2 preflight (2 refusals, then pass) | ~6 min |
|
||
| P3 install — first attempt, FAILED | 44 s |
|
||
| P3 install — resumed, SUCCESS | **3 m 49 s** |
|
||
| P4 first contact (box live on its own URL) | within ~4 min of install |
|
||
| STOP 3 unlock | **21 s** |
|
||
| app redeploy (calibre-web) | **1 m 36 s** |
|
||
| restore prepare + execute | **8 s + 13.2 s** |
|
||
| **bare machine → verified files** | **1 h 49 m 22 s** (08:38:23 → 10:27:45 UTC) |
|
||
| — of which the product's own work | **≈ 7 m 47 s** |
|
||
|
||
**The 1 h 49 m must not be quoted as the customer number** — it is dominated by the R-273 diagnosis and
|
||
release fix (~38 min) and by two waits on a human. **The ≈ 7 m 47 s must not be quoted either**: it is
|
||
what the product costs when someone already knows every answer. The honest figure for an unaided
|
||
household is **undefined, because an unaided household does not finish.**
|