doorstep: console is Felhom's (ISO 1.27.0 source), passphrase hand-over copy (hub 0.113.0 source), rulings
gates / gates (push) Successful in 19s

Phase 0: the public ISO never auto-installs by construction (no answer.toml,
G1); the operator re-affirmed the interactive installer 2026-09-14.
- felhom-bootstrap.sh: mask pvebanner.service, write a Hungarian /etc/issue
  (no :8006 admin URL); pairing banner names the Tulajdonosi jelmondat and
  paints through the CONSOLE_DEV seam (R-496). Harness: 8 checks, red first;
  fake hub now sends a pairing code (the banner was never tested, R-502).
- hub: created flash + Credentials block tell the operator to hand the phrase
  over; the self-bind mail names the operator (R-497). Tests red first.
- iso-release-gate G14-G16; domain ruling in 01-topology + CONTEXT; R-494
  narrowed to P3; R-502..R-504 filed; volunteer guide and day-0 A.2 aligned.
ISO_VERSION 1.27.0 (not built, not published).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-09-14 17:26:53 +02:00
parent 8c7f882d1c
commit 6fd8c87516
14 changed files with 269 additions and 34 deletions
+21
View File
@@ -14,6 +14,27 @@
> language, one screen, no identifiers in the prose. Same subjects, different readers; merging them
> would make one of the two audiences stop reading. `STATUS.md` is also a **view of `OPEN-ITEMS.md`**
> and holds nothing of its own; this file does hold its own content, namely the standing rulings below.
## A customer's domain is their own, and the public installer stays interactive (2026-09-14, R-494 / R-495)
**Two operator rulings, both 2026-09-14.**
1. **Every customer has their own domain, never a name under `felhom.eu`.** Free Cloudflare certificates
do not cover nested names; the domain (a few thousand forints a year) is included in the price. The
operator creates the tunnel per day-0 A.1; R-494 is narrowed to "the hub could create it" (P3).
Home: `architecture/01-topology-and-trust.md` §networking.
2. **The public ISO keeps the interactive Proxmox installer.** Offered an install-time rule ("use the only
internal disk, otherwise stop"), the operator kept the 2026-07-31 ruling: a person chooses the disk.
Why it was offered and not built: the auto-installer has no local chooser or stop page; a runtime rule
needs either an HTTP answer service or surgery inside the Proxmox installer, and USB-drive detection
was measured unsafe by udev property (SPIKE-universal-iso-1). Consequence, stated so it is not
re-litigated: **the installer's own screens stay English**; the Felhom-written text around them
(menu, console, first-boot screens) is Hungarian, and the volunteer guide answers each installer
screen. Release gate G14–G16. The spike that would inform a reversal is R-503.
**Decisions a later session must not re-derive:** the ISO's "never engaged" auto-install is not a
defect — the release build has no answer file by construction (G1); `pvebanner.service` rewrites
`/etc/issue` on every boot, so a console fix must mask it (ISO v1.27.0).
## The Update button is a guarded job, and the route back is the restore (2026-09-13, update arc slice 4 — R-448 / R-443 / R-439)
**Shipped** in controller v0.237.0 (job) + v0.238.0 (page) + v0.238.1 (nightly-leg fix), **proven live**
@@ -148,6 +148,15 @@ credentials.
DNS/routing stay intact through an outage.
- **Outbound only** for control/report/backup (poll to hub, push to PBS). No inbound control
endpoint exists in the chosen model.
- **Every customer has their OWN domain — never a name under `felhom.eu`** (operator ruling
2026-09-14). The free Cloudflare tier's certificate covers one level below a zone, so nested names
such as `felhom.<customer>.felhom.eu` are not covered; the customer's own domain is a few thousand
forints a year and is **included in the customer's price**. The dashboard is `felhom.<customer
domain>`, each app `<sub>.<customer domain>`. **The tunnel is created by the operator** per
`runbooks/day0-install.md` A.1 today; the hub creating it at customer creation (for a domain already
on Cloudflare) is register row R-494, P3, not blocking. Measured reason this was ruled now: the
2026-09-14 first-hour drill used a `*.felhom.eu` customer domain with no tunnel, and the dashboard link
in the setup-code mail did not resolve.
- **Tunnel placement: host** (resolved, Part 3 §3/§5). `cloudflared` runs on the Proxmox host
as its own **agent-managed systemd service** — not inside the guest — so the data path
survives control-plane death by construction. Geo-restriction WAF is **hub-enforced** (the
+4 -1
View File
@@ -697,7 +697,7 @@ class (an image `VOLUME` at an unmounted path) is still live — `immich-server`
| **R-489** | **[P3-LOW] `POST /api/stacks/{name}/remove` reports `volumes_removed: null` over named volumes it DID remove.** MEASURED 2026-09-13 on demo-hp five times (gokapi, actualbudget, adventurelog ×2, glance): `docker compose down --volumes` removed the app's named volumes (`docker volume ls` count 2 → 0) and the response carried `"volumes_removed":null`. The customer's confirmation dialog therefore cannot say what it deleted. Split out of R-474 (closed in v0.240.0 for the backups half). **Fix shape:** list the volumes before `down --volumes`, diff after, and report the difference (`[]` when none, never `null`). **PARTLY SHIPPED in v0.242.0 (`d698ce3`), measured live on 9202 the same night:** the difference is computed and a fresh compose-created volume IS reported (`["opengist_opengist_data"]`), but the listing filters on the compose project LABEL and a volume recreated by a unit restore (`docker volume create <name>`, `restore.go:154`) carries no labels — compose still removes it and the response says `[]` (`audits/v0242-2026-09-14/19-R489-cause.txt`). **Remaining fix:** list by the `<project>_` name prefix as well (union), or label the recreated volume as compose would. | **READY — rank P3-LOW; owner: CC (residual)** |
| **R-492** | **[P3-LOW] `cfg.Paths.HDDPath` is empty on every box and still has readers; delete it.** R-465 audited its six readers and found every one falling back; R-490 (v0.242.0) gave the last one, `systemInfo`, the same fallback. The global now carries no information on any box and its deletion was deferred twice. **Fix shape:** remove the field, its env binding and the readers' fallback branches; a build proves nothing reads it. Next controller release. | **READY — rank P3-LOW; owner: CC** |
| **R-493** | **[P1-HIGH] There are NO customer-facing install instructions, so a volunteer cannot begin — this blocks inviting anyone.** MEASURED 2026-09-14 (drill `audits/DRILL-fresh-install-0242-2026-09-14.md`, Phase 0.2), each source with what was searched: the website's 9 pages carry no mention of the installer, of `iso.felhom.eu`, or of any install step (`iso`, `letolt`, `telepit`); `https://iso.felhom.eu/` and `/index.html` both return **404** — only the exact object name `felhom-installer-1.26.1-pve9.2-1.iso` answers, so the file cannot be found without being told its name; `RUNBOOK-onboarding-draft-v4.md` is an operator-attended script; the R-11 tester one-pager was ruled 2026-07-21 and never written; no `email.md` exists in the workspace; the two hub mails a new customer receives (`Kösd össze a Felhom dobozodat`, `Elindult a Felhom szervered — beállító kód`) assume the box is already installed. **Stopgap written by the drill:** `documentation/runbooks/VOLUNTEER-first-hour.md` (Hungarian, the steps the product really needs, each addition over today listed at its top). **What it needs:** the operator to choose the channel and approve the text. | **WAITING-ON-OPERATOR — rank P1-HIGH; owner: operator (channel + text), CC (draft)** |
| **R-494** | **[P1-HIGH] A new customer's dashboard has NO reachable address unless the operator hand-makes a Cloudflare tunnel — the link in the setup-code mail is dead.** MEASURED 2026-09-14 on a fresh install from the public ISO (drill intervention **I1**): the claim mail points at `https://felhom.drill0242.felhom.eu`; that name has **no A and no AAAA** record (`dig @1.1.1.1`, control `felhom.enkisfelhom.hu` resolves); the hub has **no tunnel- or DNS-creation code** (`hub/internal/cloudflare/` holds only geo-rule removal; `cf_tunnel_token` is a pasted, optional form field, `configs.go:1478`) — day-0 runbook A.1 makes it a manual Cloudflare-dashboard step that nothing on the customer-create page asks for; the box's own split-horizon resolver on the appliance LAN IP answered `google.com` but not the dashboard name at 13:27:39Z; the agent applied the record at **13:27:44Z** (`lanresolver: applied split-horizon record … ip=192.168.0.158`, 3 m 46 s after the controller started), so the box CAN answer the name — **but only to a device that uses the box as its DNS server, and no document, screen or mail tells a household to do that**; the router and the installer-offered DNS answer nothing. The page was reachable only at the guest's LAN address with the name forced (`curl --resolve …:443:192.168.0.158`). **A volunteer could not have done that.** **What it needs:** an operator ruling — the hub creates the tunnel and DNS at customer creation, or the product gives a household a LAN address that works with no DNS change. | **WAITING-ON-OPERATOR — rank P1-HIGH; owner: operator (ruling), CC (build)** |
| **R-494** | **NARROWED 2026-09-14 by operator ruling → [P3-LOW] the hub COULD create the tunnel at customer creation, for a domain already on Cloudflare. Not blocking: every customer has their own domain and the operator creates the tunnel per day-0 A.1 (`architecture/01-topology-and-trust.md`).** *Original finding, kept:* **[P1-HIGH] A new customer's dashboard has NO reachable address unless the operator hand-makes a Cloudflare tunnel — the link in the setup-code mail is dead.** MEASURED 2026-09-14 on a fresh install from the public ISO (drill intervention **I1**): the claim mail points at `https://felhom.drill0242.felhom.eu`; that name has **no A and no AAAA** record (`dig @1.1.1.1`, control `felhom.enkisfelhom.hu` resolves); the hub has **no tunnel- or DNS-creation code** (`hub/internal/cloudflare/` holds only geo-rule removal; `cf_tunnel_token` is a pasted, optional form field, `configs.go:1478`) — day-0 runbook A.1 makes it a manual Cloudflare-dashboard step that nothing on the customer-create page asks for; the box's own split-horizon resolver on the appliance LAN IP answered `google.com` but not the dashboard name at 13:27:39Z; the agent applied the record at **13:27:44Z** (`lanresolver: applied split-horizon record … ip=192.168.0.158`, 3 m 46 s after the controller started), so the box CAN answer the name — **but only to a device that uses the box as its DNS server, and no document, screen or mail tells a household to do that**; the router and the installer-offered DNS answer nothing. The page was reachable only at the guest's LAN address with the name forced (`curl --resolve …:443:192.168.0.158`). **A volunteer could not have done that.** **What it needs:** an operator ruling — the hub creates the tunnel and DNS at customer creation, or the product gives a household a LAN address that works with no DNS change. | **READY — rank P3-LOW; owner: CC** |
| **R-495** | **[P2-MEDIUM] The public installer asks a stranger four questions nothing answers, and REFUSES its own default on one of them.** MEASURED 2026-09-14 on `felhom-installer-1.26.1` (drill screens `s04`–`s22`): every screen after the GRUB menu is **English** Proxmox (EULA, disk, locale, password, network, summary); **three disks are offered with no guidance which is the system disk** (the default happened to be right); the administrator e-mail is prefilled `mail@example.invalid`; the hostname is prefilled `pve.example.invalid` and pressing Next on it returns **„Invalid values: hostname does not look valid”** — a volunteer who accepts the defaults cannot continue; at the end, with the stick still in, the default-ticked auto-reboot boots back into the installer. None of this is wrong for Proxmox; all of it is unanswered for a Felhom household. **Fix shape (cheap first):** the volunteer instructions answer each question (done in `runbooks/VOLUNTEER-first-hour.md`); later, prefill hostname and e-mail from the ISO build. | **READY — rank P2-MEDIUM; owner: CC (ISO prefill), operator (instruction text via R-493)** |
| **R-496** | **[P2-MEDIUM] The box's console tells a stranger, in English and FIRST, to open the Proxmox admin page — and calls the owner passphrase „a jelszavadat”.** MEASURED 2026-09-14 (drill screen `s29`): above the Hungarian pairing banner the console prints Proxmox's own `Welcome to the Proxmox Virtual Environment. Please use your web browser to configure this server - connect to: https://192.168.0.134:8006/` — the operator admin UI, which a household must never be sent to; the Felhom banner below says „add meg ezt a kódot és **a jelszavadat**”, while the self-bind mail and page name the same secret **„Tulajdonosi jelmondat”** (R-323 renamed the mail; `scripts/iso/felhom-bootstrap.sh:71-72` was not renamed). **Fix shape:** replace the Proxmox `/etc/issue` block on appliance installs; rename the console word. Both live in `felhom-bootstrap.sh`, a frozen ISO payload (G9), so it ships with the next ISO. | **READY — rank P2-MEDIUM; owner: CC** |
| **R-497** | **[P2-MEDIUM] No product channel ever gives the customer the „Tulajdonosi jelmondat”, yet the self-bind mail says they received it at setup.** MEASURED 2026-09-14: the 5-word phrase is minted at customer creation (`hub/internal/web/configs.go`, `RandomPassphrase(5)`) and shown **only on the operator's customer page**; the self-bind mail (`FormatSelfBindEmail`) says „amelyet a beállításkor kaptál” and the console asks for it; nothing sends it. Day-0 runbook A.2 says the operator must dictate or hand it over — a step that exists only in an operator document. A volunteer whose operator forgets cannot bind the box and is told they already have the phrase. **Fix shape:** the operator's customer-create confirmation says, in one line, to hand this phrase to the customer now; the volunteer instructions say where it comes from (done in `VOLUNTEER-first-hour.md`). | **READY — rank P2-MEDIUM; owner: operator (process), CC (hub copy)** |
@@ -705,6 +705,9 @@ class (an image `VOLUME` at an unmounted path) is still live — `immich-server`
| **R-499** | **[P2-MEDIUM] Every app without a data drive is told its data is „already in the full system backup (PBS)" and there is „nothing to do" — on a box with no PBS, whose only whole-box copy sits on the same disk.** MEASURED 2026-09-14 on a fresh 0.242.0 box with the DR tier off (drill step 7): `GET /stacks/bookstack/backup` renders „Ennek az alkalmazásnak az adatai a belső rendszerlemezen vannak, amelyek **már szerepelnek a teljes rendszermentésben (PBS)** … ehhez az alkalmazáshoz nincs külön teendő." The sentence sits under `{{if not .IsHDDApp}}` in `controller/internal/web/templates/tier2_config.html:20-26` and consults nothing about where the whole-guest backup goes. On the same box `/backups` says, correctly, „Helyi tároló (local)" and „A rendszermentés jelenleg ugyanazon a lemezen van, mint a rendszer — így hibás fájlok ellen véd, lemezhiba ellen nem." **Two pages of one product contradict each other, and the reassuring one is the false one.** **Fix shape:** branch the sentence on the box's actual whole-guest target (PBS vs local, same-disk flag the overview already computes); a render test per branch. | **READY — rank P2-MEDIUM; owner: CC** |
| **R-500** | **[P3-LOW] The dashboard shows the last backup in UTC while every backup page shows it in local time — two different clock times for one backup.** MEASURED 2026-09-14 on a fresh 0.242.0 box (drill step 12): the same backup (`last_run 2026-09-14T13:41:37Z`) reads „Utolsó mentés: **2026-09-14 13:41**" on `/dashboard` and „Utolsó adatbázis mentés: **2026-09-14 15:41** (most)" on `/backups/apps`. Cause, from source: `controller/internal/web/templates/dashboard.html:154` renders `{{.BackupStatus.LastRun.Format "2006-01-02 15:04"}}` with no conversion to the box's zone, while the backup pages go through the zone-aware helpers in `funcmap.go`. A household comparing the two screens sees a backup two hours apart from itself. **Fix shape:** format through the same zone-aware helper; a render test that pins a non-UTC zone and asserts the local hour. | **READY — rank P3-LOW; owner: CC** |
| **R-501** | **[P3-LOW] The documented "confirm your CI run" recipe reads only the LAST page of the jobs list, and that list is not in id order — so it can report a run as missing that exists and passed.** MEASURED 2026-09-14 for felhom.eu commit `38848ff`: `actions/jobs?limit=1` → `total_count 335`; the recipe's page `T/50+1` = page 7 held ids 473…570 and **no match for 8 minutes**; a scan of all seven pages found the job on **page 6** (ids 380…581): `job id=581 name=gates status=completed conclusion=success completed_at 2026-09-14T14:18:23Z`. Pages are not sorted (page 1 ids 1…273, page 2 84…208). `actions/tasks` listed the same run first (`id 582, run_number 335, success`). **Fix shape:** the recipe in `felhom.eu/CLAUDE.md` (end-of-session checklist) scans every page and matches `head_sha`; say so in the same sentence that warns about the id offset (R-417). | **READY — rank P3-LOW; owner: CC** |
| **R-502** | **[P3-LOW] The bootstrap regression harness is run by NO gate and NO CI — and it had never exercised the pairing banner.** MEASURED 2026-09-14: `grep -rn bootstrap-modes scripts/*.py .gitea/workflows` → nothing; the harness is run by hand in `felhom-iso-assistant:trixie`. While adding the R-496 checks, its fake hub's `register` reply turned out to carry no `pairing_code`, so `print_pairing_banner` returned early in every run: the banner that a household reads first had no test at all until ISO v1.27.0. **Fix shape:** register the harness in `repo_gates.py` behind a container-available check that reports INCONCLUSIVE (never skip-as-pass) when docker is absent, with a decoy. | **READY — rank P3-LOW; owner: CC** |
| **R-503** | **[P3-LOW] SPIKE (not built): an install-time disk rule for the public ISO — "exactly one internal disk → install; otherwise stop in Hungarian".** Offered to the operator 2026-09-14 and **not chosen**: the 2026-07-31 ruling (a person chooses the disk) stands. Recorded so a reversal starts from measurements, not from the offer. **What must be measured first:** (1) whether the Proxmox auto-installer's HTTP answer mode can serve a per-machine answer from posted system info without network being a precondition a volunteer can miss; (2) whether USB transport is reliably visible in sysfs (`/sys/block/*/device` path, `removable`) where udev properties were measured blind (SPIKE-universal-iso-1 §3.2); (3) whether any refusal can be shown in Hungarian without modifying the Proxmox installer squashfs. **Reverses two rulings if built — needs an operator word.** | **WAITING-ON-OPERATOR — rank P3-LOW; owner: operator (ruling), CC (spike)** |
| **R-504** | **[P3-LOW] `iso.felhom.eu` cannot show an index page on its own — its root returns 404, and the download page lives on the website instead.** MEASURED 2026-09-14: `https://iso.felhom.eu/` and `/index.html` → 404; only named objects answer. The host is an R2 bucket behind a custom domain; whether R2 would serve an uploaded `index.html` at `/` was **not measured** (uploading anything to the public bucket is a publication). The ISO v1.27.0 task puts the Hungarian download page at `felhom.eu/letoltes` (published with the ISO, after the operator's yes). **Remaining:** a redirect from `iso.felhom.eu/` to that page needs a Cloudflare rule the session has no credential for. | **WAITING-ON-OPERATOR — rank P3-LOW; owner: operator (Cloudflare rule)** |
<!-- DUE-CHECKS-BEGIN — machine-readable. Parsed by scripts/due_checks_gate.py.
One row per dated check. The R-number must have a row above. Dates are UTC.
+15 -21
View File
@@ -8,18 +8,18 @@
> **Every step this document adds beyond what exists today** (none of these is written anywhere a
> volunteer can see):
>
> 1. **Where the installer is, by exact file name** — `iso.felhom.eu` has no index page (R-493).
> 1. **Where the installer is** — the Hungarian download page `felhom.eu/letoltes` (published with ISO
> v1.27.0 after the operator's yes; `iso.felhom.eu/` itself still has no index — R-504).
> 2. **That the installer screens are English Proxmox screens**, and what to type on each.
> 3. **A hostname to type** — the installer refuses its own default `pve.example.invalid`.
> 4. **An e-mail to type** — the installer prefills `mail@example.invalid`.
> 5. **To remove the USB stick** at „Installation finished — reboot now?".
> 6. **To ignore the English „Please use your web browser … https://…:8006/" block** on the console —
> that is the Proxmox admin page, not Felhom.
> 7. **That „a jelszavadat" on the console means the 5-word „Tulajdonosi jelmondat"**, and that it
> comes from the operator, not by mail.
> 8. **How to reach the dashboard** — **UNRESOLVED**: the setup-code mail's link does not resolve for
> a new customer unless the operator made a Cloudflare tunnel by hand (R-494). Step 5 below says
> what the operator must do before sending this document.
> 6. ~~To ignore the English Proxmox console block~~ — **removed in ISO v1.27.0** (R-496): the console
> shows Felhom's Hungarian text.
> 7. ~~That „a jelszavadat" means the Tulajdonosi jelmondat~~ — **removed in ISO v1.27.0 / hub v0.113.0**
> (R-496, R-497): console, mail and page use one name; the mail says the operator hands it over.
> 8. **How to reach the dashboard** — **ruled 2026-09-14**: every customer has their own domain and the
> operator creates the tunnel (day-0 A.1) before sending this document. R-494 is narrowed to P3.
>
> Operator prerequisites this document silently depends on (§ „Az üzemeltető előtte"):
> create the customer; **hand over the Tulajdonosi jelmondat out of band**; **create the Cloudflare
@@ -45,12 +45,8 @@
## 1. A telepítő letöltése (~1 perc)
Töltsd le ezt a fájlt (kb. 1,7 GB):
`https://iso.felhom.eu/felhom-installer-1.26.1-pve9.2-1.iso`
Ellenőrző összeg (SHA-256), ha szeretnéd ellenőrizni:
`f3cc86d5f0ec68bba4155c994b4fa84e208d50209bb6e815636c99e5441059a6`
Nyisd meg a **https://felhom.eu/letoltes** oldalt, és töltsd le az ott megadott telepítőt
(kb. 1,7 GB). Az oldal a fájl ellenőrző összegét (SHA-256) is megadja.
Írd ki USB-kulcsra (Windows: Rufus, **„DD" módban**; Mac/Linux: balenaEtcher).
@@ -62,7 +58,7 @@ Indítsd a gépet az USB-kulcsról. **A telepítő képernyői angolul vannak**
|---|---|
| Felhom logó, két sor | Válaszd a **„Felhom telepítés"** sort (vagy várj, magától elindul) |
| END USER LICENSE AGREEMENT | **I agree** |
| Target harddisk | Válaszd azt a lemezt, **amelyre a rendszer kerüljön**. Ha több lemez van, a legnagyobb nem feltétlenül a jó — a külső adatlemezeket ne válaszd. |
| Target harddisk | **A telepítő soha nem választ helyetted.** Felsorolja a gép lemezeit méretükkel és típusukkal; válaszd azt, **amelyre a rendszer kerüljön — ez a lemez törlődik.** A külső mentőlemezt a telepítés idejére húzd ki. Ha több belső lemez van és nem tudod, melyik a jó, állj meg és szólj az üzemeltetőnek. |
| Country / Timezone / Keyboard | Hagyd így: Hungary, Europe/Budapest, Hungarian |
| Root password | Adj meg egy legalább 8 karakteres jelszót kétszer. **Nem kell megjegyezned** — a Felhom az első indulás után lecseréli. |
| Administrator email | Írd be a saját e-mail címedet (a `mail@example.invalid` helyett) |
@@ -73,10 +69,8 @@ Indítsd a gépet az USB-kulcsról. **A telepítő képernyői angolul vannak**
## 3. Az első indulás (~2 perc)
A képernyőn **először egy angol szöveg** jelenik meg („Welcome to the Proxmox Virtual Environment…
connect to: https://…:8006/"). **Ezzel nincs teendőd — ne nyisd meg.**
Alatta megjelenik a Felhom:
A képernyőn ez jelenik meg: „Felhom otthoni szerver — Ezen a képernyőn nincs teendőd, bejelentkezni
sem kell." Alatta, kb. 2 perc múlva:
> Felhom — a doboz készen áll, és a párosításra vár.
> Párosító kód: XXX-XXX
@@ -89,7 +83,7 @@ Alatta megjelenik a Felhom:
hivatkozásra (7 napig érvényes).
2. Add meg:
- a **Párosító kódot** a doboz képernyőjéről;
- a **Tulajdonosi jelmondatot** (5 szó). A doboz képernyője ezt „jelszónak" hívja — ugyanaz.
- a **Tulajdonosi jelmondatot** (5 szó), amelyet a Felhom üzemeltetőjétől kaptál.
3. Hagyd bekapcsolva a dobozt. **Kb. 3 perc** múlva megérkezik a következő e-mail.
*(Megjegyzés: a doboz képernyője az összekötés után is a párosító kódot mutatja — ez ismert hiba,
@@ -99,7 +93,7 @@ nem kell újra összekötni.)*
1. Nyisd meg a **„[Felhom] Elindult a Felhom szervered — beállító kód"** tárgyú e-mailt.
2. Kattints a benne lévő címre (`https://felhom.<a-te-domained>`).
**⚠ Ha a cím nem nyílik meg, szólj az üzemeltetőnek** — ez most ismert akadály (R-494).
Ha a cím nem nyílik meg, szólj az üzemeltetőnek.
3. „A szerver beállítása" oldalon add meg a **Beállító kódot** (3 szó, pl. `szó-szó-szó`), és válassz
**legalább 12 karakteres** jelszót. Ez lesz a vezérlőpult jelszava.
4. Ha nem jött meg a kód: **„Új kód kérése"** — mindig ugyanarra az e-mail címre érkezik.
+4 -1
View File
@@ -69,7 +69,10 @@ Hub UI (`https://hub.felhom.eu`, operator password) → **Customers → New**:
On save the hub generates two credentials:
- **Retrieval passphrase** (5 Hungarian words) — the ONE secret the installer carries to the box.
Dictate or hand it to whoever runs Part C. Treat it like a password.
The customer knows it as **„Tulajdonosi jelmondat”**. **Hand this phrase to the customer now** — in
person or by phone; it is never e-mailed, and the self-bind page asks for it. (The hub's
customer-created flash and Credentials block say the same sentence since hub v0.113.0, R-497; the
self-bind mail tells the customer they received it from the Felhom operator.) Treat it like a password.
- **Customer API key** — internal (baked into the generated controller.yaml); never handled manually.
### A.3 Verify the Day-0 artifact manifest
@@ -244,6 +244,53 @@ it. The installed box registered at the hub, failed to persist the token, and th
Any future criterion of the form "the correct file is present" should be paired with one of the form
"and everything it needs at run time is too". `build-deb.sh` asserts this itself and is red-proofed.
### G14 — a PERSON chooses the disk; the image never guesses (operator rulings 2026-07-31, re-affirmed 2026-09-14)
```bash
osirrox -indev "$ISO" -find / -maxdepth 1 2>/dev/null | grep -cE "'/answer\.toml'|'/auto-installer-mode\.toml'" # 0 (= G1)
grep -c 'filter\.' <every pipeline-added text file> # 0
```
**PASS =** both `0`, **and** the proof install shows the installer's own disk screen and `Bootdisk(s)`
summary before erasing, **and** `runbooks/VOLUNTEER-first-hour.md` tells a volunteer, in Hungarian, how
to recognise the system disk on that screen.
*The rule, in one paragraph a volunteer could read:* **the installer never picks a disk for you.** It
lists every disk it sees, with its size and model; you choose the one the system goes on, and that
disk is erased. If the machine has more than one disk, the guide tells you how to tell them apart
before you press Next — and the external backup drive should not be plugged in during the install.
*Why:* SPIKE-universal-iso-1 §3.2–3.3 measured that no udev property separates an internal disk from a
USB-caddy backup drive and that a filter matching two disks silently wipes one. On 2026-09-14 the
operator was offered an install-time "use the only disk" rule and chose to keep the person as the
safety mechanism. Changing this criterion reverses two rulings; the spike that would inform it is its
own register row.
### G15 — the console is Felhom's after first boot, with no admin URL
On the proof install, after the first boot and after **one reboot**:
```bash
grep -c 8006 /etc/issue # 0
grep -c 'Felhom otthoni szerver' /etc/issue # 1
systemctl is-enabled pvebanner.service # masked
```
**PASS =** all three, **and** a console screendump shows no `https://…:8006/` line. *Why:* Proxmox's
`pvebanner.service` rewrites `/etc/issue` on every boot; the 2026-09-14 drill (screen s29) found that
text — the operator admin UI, in English — was the first thing a household read. The reboot is part of
the criterion because an overwrite without the mask passes once and fails on the next boot.
### G16 — every Felhom-authored string on the volunteer's path is Hungarian, and names each secret once
```bash
grep -n "printf ' " <extracted felhom-bootstrap.sh> | grep -viE 'á|é|í|ó|ö|ő|ú|ü|ű|Felhom|%s' # review every hit
grep -c 'jelszavad' <extracted felhom-bootstrap.sh> # 0
grep -c 'Tulajdonosi jelmondat' <extracted felhom-bootstrap.sh> # >= 1
```
**PASS =** no English sentence in the review list, `jelszavad` absent, the R-323 name present. Search
with ASCII fragments where possible and keep one positive and one negative control in the record.
*Scope, stated so it is not read wider:* the **Proxmox installer's own screens are English** and stay so
under the 2026-07-31 ruling; G14 requires the guide to answer each of them. This criterion covers what
Felhom writes: the GRUB menu, the console banner and `/etc/issue`, the first-boot screens.
---
## Result recording
+19
View File
@@ -1,3 +1,22 @@
## v0.113.0 — the operator is told to hand over the passphrase, and the mail says who hands it (2026-09-14, R-497)
**What was wrong, measured in the 2026-09-14 first-hour drill.** The five-word „Tulajdonosi jelmondat" is
minted at customer creation and shown only on the operator's customer page — nothing delivers it, and
nothing told the operator to. The self-bind mail then told the customer they had received it
„a beállításkor", which was true only if the operator had remembered.
**What changed — copy only, no acceptance logic.**
- The customer-created flash: *„Hand this phrase to the customer now — … Give it in person or by phone;
it is never e-mailed, and the self-bind page asks for it."*
- The Credentials block repeats the same sentence beside the Retrieval Password.
- The self-bind mail's item 2 now reads: *„A tulajdonosi jelmondatodat (az 5 szóból álló kifejezést),
amelyet a Felhom üzemeltetőjétől kaptál — személyesen vagy telefonon, e-mailben soha."*
- **The passphrase is still never e-mailed** — the operator has not ruled that e-mail may carry it.
**Tests (red first):** `passphrase_handover_test.go` — the created flash and the page carry the
hand-over sentence (failed before), the mail names the operator and no longer says „beállításkor"
(failed before), and the mail never carries a passphrase word (a guard, green before and after).
## v0.112.0 — the floor carries a release without a golden (2026-09-13, R-472)
**Operator ruling 2026-09-13.** Goldens are baked weekly (R-468), but the hub held every floor above
+2 -1
View File
@@ -347,7 +347,8 @@ A hivatkozás megnyitása után két adatot kell megadnod:
1. A párosító kódot, amely a doboz képernyőjén (a monitoron) látható.
2. A tulajdonosi jelmondatodat (az 5 szóból álló kifejezést), amelyet a
beállításkor kaptál. Ez igazolja, hogy a fiók a tiéd.
Felhom üzemeltetőjétől kaptál — személyesen vagy telefonon, e-mailben
soha. Ez igazolja, hogy a fiók a tiéd.
A hivatkozás 7 napig érvényes. Biztonsági okból 5 sikertelen próbálkozás után
zárolódik — ilyenkor vedd fel a kapcsolatot az ügyfélszolgálattal.
@@ -0,0 +1,68 @@
package web
// R-497 — the „Tulajdonosi jelmondat" is delivered by NOTHING in the product: it is minted at customer
// creation, shown only on the operator's customer page, and never e-mailed (the operator has not ruled
// that e-mail may carry it). Measured 2026-09-14 in the first-hour drill: the self-bind mail told the
// customer they had received it „a beállításkor" — true only if the operator remembered to hand it over,
// and nothing told the operator to.
//
// These pin the CONSEQUENCE, not a string for its own sake: the operator is told at the moment the
// phrase exists, the page repeats it where the phrase is shown, and the customer's mail names the person
// they got it from. The last test pins what must NOT change — the mail still carries no phrase.
import (
"strings"
"testing"
"gitea.dooplex.hu/admin/felhom-hub/internal/notify"
)
// handoverMarker is the operator-facing instruction. One sentence, rendered in two places.
const handoverMarker = "Hand this phrase to the customer now"
func TestCustomerPage_CreatedFlashTellsTheOperatorToHandOverThePhrase(t *testing.T) {
s, st := newTestServer(t)
seedCustomer(t, st, "acme", "")
created := renderCustomerPageWithQuery(t, s, "acme", "?flash=created")
if !strings.Contains(created, handoverMarker) {
t.Errorf("the customer-created flash does not tell the operator to hand over the phrase:\n%s", created)
}
if !strings.Contains(created, "never e-mailed") {
t.Error("the created flash must say the phrase is never e-mailed — otherwise the operator waits for a mail")
}
// Nominal page (no flash): the instruction stays beside the phrase, where the operator reveals it.
plain := renderCustomerPage(t, s, "acme")
if !strings.Contains(plain, handoverMarker) {
t.Error("the Credentials block does not repeat the hand-over sentence beside the phrase")
}
}
func TestSelfBindEmail_SaysWhoHandedOverThePhrase(t *testing.T) {
_, body := notify.FormatSelfBindEmail("acme", "https://hub.example/bind/tok")
if strings.Contains(body, "beállításkor kaptál") {
t.Error("the mail still says the customer received the phrase „a beállításkor” — nothing delivers it then")
}
if !strings.Contains(body, "üzemeltetőjétől") {
t.Errorf("the mail must name who hands over the phrase (the Felhom operator):\n%s", body)
}
// Unchanged contract: one name, and what the thing is.
if !strings.Contains(body, "tulajdonosi jelmondatodat") || !strings.Contains(body, "5 szóból álló kifejezést") {
t.Error("the mail lost the secret's name or its description")
}
}
// The phrase itself must never ride the mail. A customer with a seeded phrase gets a mail that does not
// contain it — the builder takes no phrase argument today; this pins that no future edit adds one via
// the link text or a helper.
func TestSelfBindEmail_NeverCarriesThePhrase(t *testing.T) {
const phrase = "alma korte szilva barack meggy"
_, body := notify.FormatSelfBindEmail("acme", "https://hub.example/bind/tok")
for _, w := range strings.Fields(phrase) {
if strings.Contains(body, w) {
t.Errorf("the self-bind mail contains a passphrase word %q", w)
}
}
}
@@ -44,7 +44,7 @@
{{if .Flash}}
<div class="flash flash-success">
{{if eq .Flash "created"}}Configuration created successfully.
{{if eq .Flash "created"}}Configuration created successfully. <strong>Hand this phrase to the customer now</strong> — the Retrieval Password below is the customer&#39;s „Tulajdonosi jelmondat”. Give it in person or by phone; it is never e-mailed, and the self-bind page asks for it.
{{else if eq .Flash "updated"}}Configuration updated.
{{else if eq .Flash "password_regenerated"}}Retrieval password regenerated.
{{else if eq .Flash "offsite_reissued"}}Offsite credentials re-issued — a fresh one-time password is staged; the controller picks it up on its next config refresh.
@@ -464,6 +464,7 @@
<button type="button" class="copy-btn" onclick="copySecret('retrieval-pw')" title="Copy">&#x2398;</button>
</div>
<span class="form-hint">The per-customer secret that fetches the whole config — masked by default; never place it on a command line (the installer reads it at a no-echo prompt).</span>
<span class="form-hint"><strong>Hand this phrase to the customer now</strong> — in person or by phone. The customer knows it as „Tulajdonosi jelmondat”; it is never e-mailed, and the self-bind page asks for it (R-497).</span>
</div>
<form method="POST" action="/configs/{{.CustomerID}}/regen-password" style="margin-top: 0.5rem;">
{{.CSRFField}}
+21
View File
@@ -1,3 +1,24 @@
## ISO v1.27.0 — the console is Felhom's, and names the passphrase once (2026-09-14, R-496)
**Measured in the 2026-09-14 first-hour drill (screen s29):** the first text a household read on a fresh
box was Proxmox's `Welcome to the Proxmox Virtual Environment … connect to https://<ip>:8006/` — the
operator admin UI, in English — and the Felhom banner below it asked for „a jelszavadat", while the
self-bind mail and page call the same secret „Tulajdonosi jelmondat".
**`felhom-bootstrap.sh` (the frozen ISO payload):**
- `install_felhom_issue` runs first, before the network gate: masks `pvebanner.service` (it rewrites
`/etc/issue` on every boot — overwriting alone would last one boot), writes a Hungarian Felhom text
to `/etc/issue` with no admin URL, reloads agetty. Every step best-effort; never blocks the boot.
- The pairing banner asks for „a Tulajdonosi jelmondatodat (az 5 szót a Felhom üzemeltetőjétől
kaptad)", and paints through the `CONSOLE_DEV` seam instead of a hard-coded `/dev/console`.
**Harness (`scripts/iso/test/bootstrap-modes.sh`):** five R-496 checks, **red before the change**. Found
on the way: the fake hub's register reply carried no `pairing_code`, so the pairing banner had **never
been exercised by any test**; it now carries one. Still not run by any gate or CI (recorded as a row).
**Not changed, by operator ruling 2026-09-14:** the public ISO keeps the interactive Proxmox installer
(no answer file). The 2026-07-31 ruling — disk selection is always a person's choice — stands.
## observations_gate.py reads EVERY observations section (2026-09-13, R-471)
`observation_items` returned the first `Observations` heading it found and stopped, so a report
+1 -1
View File
@@ -48,7 +48,7 @@ set -euo pipefail
# does not exist: the ISO is a frozen artifact, while felhom-host-install.sh is fetched at RUN TIME
# from the website's git-sync of `main` (R-94/R-110), so whatever version an ISO carries, the script a
# box runs is always current. Coupling them would invent a constraint. The claim is corrected instead.
ISO_VERSION="1.26.1" # the ISO's own version. INDEPENDENT of felhom-host-install.sh's SCRIPT_VERSION,
ISO_VERSION="1.27.0" # the ISO's own version. INDEPENDENT of felhom-host-install.sh's SCRIPT_VERSION,
# which is fetched at run time from main and is not frozen into the image.
IMAGE="${FELHOM_ISO_ASSISTANT_IMAGE:-felhom-iso-assistant:trixie}"
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+36 -2
View File
@@ -69,11 +69,42 @@ print_pairing_banner() {
printf ' Felhom — a doboz készen áll, és a párosításra vár.\n\n'
printf ' Párosító kód: %s\n\n' "$code"
printf ' Nyisd meg az e-mailben kapott linket, és add meg\n'
printf ' ezt a kódot és a jelszavadat.\n\n'
printf ' ezt a kódot és a Tulajdonosi jelmondatodat\n'
printf ' (az 5 szót a Felhom üzemeltetőjétől kaptad).\n\n'
printf ' Ez a képernyő magától frissül — nincs teendő a\n'
printf ' doboznál, és nyugodtan itt hagyhatod bekapcsolva.\n'
printf '================================================\n\n'
} > /dev/console 2>/dev/null || printf 'Párosító kód: %s\n' "$code"
} > "$CONSOLE_DEV" 2>/dev/null || printf 'Párosító kód: %s\n' "$code"
}
# install_felhom_issue (R-496, v1.27.0) — the text above the console login prompt is Felhom's, not
# Proxmox's. Measured 2026-09-14 (drill screen s29): the first thing a household read on a fresh box was
# `Welcome to the Proxmox Virtual Environment … connect to https://<ip>:8006/` — the operator admin UI,
# in English. pvebanner.service REWRITES /etc/issue on every boot (/usr/bin/pvebanner opens it '>'), so
# overwriting the file alone would last one boot: the unit is MASKED, then the file is written.
# Best-effort in every step — a banner must never block the boot or the pairing.
ISSUE_FILE="${FELHOM_ISSUE_FILE:-/etc/issue}"
install_felhom_issue() {
if command -v systemctl >/dev/null 2>&1; then
if systemctl mask pvebanner.service >/dev/null 2>&1; then
log "console: pvebanner.service masked (it would rewrite $ISSUE_FILE with the Proxmox admin URL on every boot)"
else
log "console: could not mask pvebanner.service — the Proxmox banner may return on the next boot"
fi
fi
local tmp="${ISSUE_FILE}.felhom-tmp"
if { printf '\n'
printf ' Felhom otthoni szerver\n\n'
printf ' Ezen a képernyőn nincs teendőd, bejelentkezni sem kell.\n'
printf ' A beállításhoz kövesd a Felhomtól kapott útmutatót.\n\n'
} > "$tmp" 2>/dev/null && mv -f "$tmp" "$ISSUE_FILE" 2>/dev/null; then
log "console: $ISSUE_FILE is the Felhom text (no admin URL)"
else
rm -f "$tmp" 2>/dev/null
log "console: could not write $ISSUE_FILE — continuing"
fi
command -v agetty >/dev/null 2>&1 && agetty --reload >/dev/null 2>&1
return 0
}
cleanup_pass() { [[ -e "$PASS_FILE" ]] && { shred -u "$PASS_FILE" 2>/dev/null || rm -f "$PASS_FILE"; }; return 0; }
@@ -528,6 +559,9 @@ emit("FELHOM_EXTRA_ARGS", d.get("extra_args"))
done
}
# --- R-496: Felhom's text above the console login, before anything can wait on the network --------
install_felhom_issue
# --- R-59/R-60 first-boot network gate: never proceed silently into a hub-unreachable install ------
network_gate
+20 -6
View File
@@ -97,7 +97,9 @@ exit 0
HI
exit 0 ;;
*"/appliance/register")
echo '{"appliance_token":"TESTTOKEN123456","poll_interval_sec":30}'
# pairing_code (R-27): without it print_pairing_banner returns early and the banner is never
# painted — which is how the banner went untested until v1.27.0 (R-496).
echo '{"appliance_token":"TESTTOKEN123456","poll_interval_sec":30,"pairing_code":"TST-CDE"}'
exit 0 ;;
*"/appliance/poll")
if [ "$mode" = "200" ]; then
@@ -113,15 +115,15 @@ exit 0
CURL
chmod +x "$FAKE/curl"
# fake systemctl (disable is a no-op)
printf '#!/bin/bash\nexit 0\n' > "$FAKE/systemctl"; chmod +x "$FAKE/systemctl"
# fake systemctl (disable is a no-op); logs every call so R-496's pvebanner mask is observable
printf '#!/bin/bash\necho "$*" >> /work/systemctl.log\nexit 0\n' > "$FAKE/systemctl"; chmod +x "$FAKE/systemctl"
reset_state() {
rm -rf /etc/felhom /run/felhom-bootstrap-pass /var/lib/felhom-install "$CALLS" /work/hostinstall.log \
/work/poll-mode /work/sleep.count /work/sleep.flip /work/sleep.abort \
/work/ip.log /work/ifreload.log /work/dhclient.log /work/hub-mode /work/dhcp-mode \
/work/sys /work/interfaces /work/interfaces.felhom-bak /work/console.out \
/run/felhom-interfaces.orig /work/run.log
/run/felhom-interfaces.orig /work/run.log /work/issue /work/systemctl.log
mkdir -p /etc/felhom
}
@@ -150,7 +152,7 @@ iface nicB inet manual
source /etc/network/interfaces.d/*
IFACES
}
GATE_ENV="FELHOM_NET_SYS=/work/sys FELHOM_INTERFACES_FILE=/work/interfaces FELHOM_CONSOLE_DEV=/work/console.out"
GATE_ENV="FELHOM_NET_SYS=/work/sys FELHOM_INTERFACES_FILE=/work/interfaces FELHOM_CONSOLE_DEV=/work/console.out FELHOM_ISSUE_FILE=/work/issue"
# ============================ Scenario D — direct mode, zero appliance calls =========================
# Runs WITH the gate fixture (NICs + fallback-shaped interfaces) and the hub reachable: the G1
@@ -179,6 +181,13 @@ check "G1: gate made zero ifreload calls" "[ ! -f /work/ifreload.log ]"
check "G1: gate made zero dhclient calls" "[ ! -f /work/dhclient.log ]"
check "G1: gate consumed zero sleeps" "[ ! -f /work/sleep.count ]"
check "G1: interfaces fixture untouched" "grep -q 'bridge-ports nicA' /work/interfaces && grep -q '192.168.100.2' /work/interfaces"
# R-496 (v1.27.0): the console's login banner is Felhom's, not Proxmox's — and it survives a reboot,
# because pvebanner.service (which rewrites /etc/issue on EVERY boot) is masked, not merely overwritten.
check "R-496: /etc/issue written" "[ -s /work/issue ]"
check "R-496: /etc/issue is the Felhom text" "grep -q 'Felhom otthoni szerver' /work/issue"
check "R-496: /etc/issue carries no admin URL (:8006)" "! grep -q '8006' /work/issue"
check "R-496: /etc/issue does not say Proxmox" "! grep -qi 'proxmox' /work/issue"
check "R-496: pvebanner.service masked" "grep -q 'mask pvebanner.service' /work/systemctl.log"
# ============ P: pairing loop (v1.21.0) — register, wait unbound INSIDE one invocation, deliver =====
say "P: pairing env -> register + in-script 204 wait -> 200 delivery -> host-install, ONE invocation"
@@ -188,7 +197,12 @@ FELHOM_HUB_URL=https://hub.example
ENV
echo 204 > /work/poll-mode
echo 3 > /work/sleep.flip # after 3 in-script waits the hub "binds" (poll flips to 200)
bash "$BSTRAP"; rc=$?
rm -f /work/console.out
env FELHOM_CONSOLE_DEV=/work/console.out FELHOM_ISSUE_FILE=/work/issue bash "$BSTRAP"; rc=$?
# R-496: the pairing banner names the secret the way the self-bind mail and page do (R-323).
check "R-496: banner painted to the console seam" "grep -q 'Párosító kód' /work/console.out"
check "R-496: banner names the Tulajdonosi jelmondat" "grep -q 'Tulajdonosi jelmondat' /work/console.out"
check "R-496: banner no longer says 'jelszavad'" "! grep -q 'jelszavad' /work/console.out"
check "single invocation ran to done (exit 0)" "[ $rc -eq 0 ]"
check "POSTed /appliance/register" "grep -q '/appliance/register' $CALLS"
check "appliance token persisted 0600" "[ -f /etc/felhom/.bootstrap-done ] || { [ -f /etc/felhom/appliance-token ] && [ \"\$(stat -c %a /etc/felhom/appliance-token)\" = 600 ]; }"