scripts/iso/: a DooPlex pipeline (build-felhom-iso.sh + Dockerfile.assistant) that
turns the official PVE ISO into a Felhom auto-install ISO whose first-boot stub
installs a retry-forever felhom-bootstrap unit which unattended-fetches
felhom-host-install.sh from the public felhom.eu/scripts channel and runs it until the
host is enrolled + a guest provisioned. host-install is UNMODIFIED (invoked only).
- build gates the answer on validate-answer OUTPUT text, never $? (spike S1 exit-0 trap)
- stub is from-iso, fully-up, exactly-once; retry unit owns all network work (S8a)
- retry-vs-resume encoded once: plain first, --resume when install state exists (v1.11.3)
- secret-bearing (embeds the retrieval passphrase): supervised/single-use; env shredded on success
Validated on VM 310: build gate + red-proof, disk-filter fail-safe, chain + retry,
resume-decision, exactly-once, no-net retry+recovery. Terminal host-install rc-0 success
operator-gated (drill customer needs the password-gated create-UI). scripts v1.16.0;
ROADMAP R-21 -> in-progress. Detail in REPORT.md.
The NAS verify pipeline (agent v0.81.0) reads mount-unit journals unprivileged
— group membership, NO sudoers grant. Fixes the v1.11.0/1.12.0 header drift.
New authoritative feature doc documentation/controller/network-storage-nas.md
(verify pipeline, §8 truth table, Q4 error taxonomy, retry=0, Route A recipes
incl. the chmod-persists nuance).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
F3: the populated-host leaf guard now fires only when a FELHOM guest exists
(felhom_guests, not `pct list`) — a fresh byo install on a host running only
the OWNER's guests proceeds without --allow-new-leaf; the pin protection for
real Felhom guests is unchanged (guard still fires on a Felhom guest).
F6: byo preflight REFUSES (never mutates) when a foreign resolver is bound
to :53, with remediation — Felhom needs the guest reachable by name and will
not stop/mask the owner's DNS (the dnsmasq analog of the break-glass rule).
Viktor's ruling: refuse+instruct only, no stop/mask path. ss failure degrades
to a warn. Appliance mode untouched (the agent's lan_resolver owns :53).
F1: uninstall now purges the agent config's .bak* siblings (one GL-6 residue
held a live hub api_key), not just agent.json; WIPED statement updated.
Harness +GL8-F3/F6/F1 static + GL8-F1b behavioural + GL8-F6 live (byo
preflight refuses on felhom-pve's leftover dnsmasq — the real F6 scenario).
Red-proofs RP-F3/F6/F1 each fail their invariant. 36/36 on felhom-pve;
shellcheck clean; GL-2 Scenario-A + GL-4 Scenario-D regression green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
Live-found in the GL-6 drill: `should_skip X || step_X` fully skips a
completed step on --resume, but token/enroll/grows produce IN-MEMORY
outputs (pve token; hub host_id/api_key; volume grows) that later steps
consume — agent_config writes them into the config, provision passes the
grows as flags. A resume that had completed token/enroll but not
agent_config wrote a config missing hub.host_id/proxmox.token (daemon
crash-loop, "hub.host_id is required"); a resume past grows passed
`-rootfs-grow ""` (flag parse error). step_token even had an internal
resume-guard the `|| step_token` dispatch defeated.
Fix: token/enroll/grows now run every pass (all idempotent — token
reuses-or-rotates from the on-disk config, enroll is mint-once-reuse,
grows is a pure recompute); the guard uses _state_has (no misleading
SKIP log). golden's GOLDEN_VOLID is re-derived from the local archive in
the resume block so provision never gets an empty -archive. Harness
+GL6-F4 invariant; 27/27; shellcheck clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
Operator ruling at the GL-6 drill's Gate 0: the Felhom artifacts are
world-readable by design; an EMPTY git.username/git.token in the customer
config now WARNS and fetches anonymously instead of dying at step 5/8
(the hub-vouched sha256 stays the integrity root; a configured credential
is still used when present; curl auth args are conditional because -u with
an empty token 401s even on public content). All 12 installer fetch
targets validated 200 anonymously before shipping. Harness: +GL6-ANON
shape case; GL4-C2 assertion updated for the v1.11.1 pinned constants
(the benign overrides-notice is not a die). Drill record carries the
deviation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
Uninstall: NEW 4b4 removes the self-update artifacts (guarded wrapper,
.prev/.new.* A/B slots, rollback unit, limits drop-in — derived from
configs/felhom-selfupdate-guarded); enrolled drives under /mnt/felhom-drives
unmounted plain-umount-only (busy = warn+guidance, never -l/-f; root bind
guarded); both modes end with a KEPT-vs-WIPED statement (drives/PBS/hub
record/escrow/vaulted recovery credential live on); guest-only mode prints
the vmid's bind-store drives.
Key-pin: OPERATOR_KEY_* constants (empty until the operator ceremony) +
--operator-pubkey-file (argv-validated; comment=key_id required; file
overrides constants) -> authz.signers per the agent SignerKey schema;
no-keys-resolved KEEPS preserved signers; verify reports armed/dormant WARN.
Harness: +13 static cases + GL4-D parity + GL4-INV + PVE-tier GL4 H-U
(full-uninstall dry transcript vs live 9201). 28/28 on felhom-pve;
red-proofs RP-1..3 run->fail->revert. GO-LIVE-PACKAGE.md ABSENT AGAIN ->
status in CONTEXT.md. Live teardown/armed-pin = GL-6.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
felhom-host-install.sh installs felhom-selfupdate-guarded (sh -n), the rollback
unit + the [Unit] start-limit drop-in (daemon-reload) so day-0 boxes get
operator-signed self-update from birth; non-fatal on pre-D1 agent repos.
03-host-agent.md §11 updated to the shipped mechanism (signed op, A/B wrapper,
OnFailure rollback, tuned start-limit backstop).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
<(wg-quick strip ...) hid the strip exit code: a corrupt head file could feed
syncconf partial input that wipes the live peer set with exit 0 (S1 REPORT
finding). Strip now writes a temp file; its failure aborts before wg runs.
Sandbox red-proof: pre-fix shape invoked wg with rc=0 despite strip exit 1;
fixed shape never reaches wg.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
Peersync script: validate-first (jq contract check before ANY state change),
head-file + generated-peers conf model, syncconf-from-tmp then atomic mv (live
conf never diverges in the failure direction), zero-peer payload valid (wipe).
hub.yaml: 0.32.0 image + WG_ENDPOINT_SSH_* env + optional Secret mount so the
pod starts before the runbook's step-6 Secret exists.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
Records the settled transport decisions (plain WG, host-side agent-managed pilot,
one hub-driven endpoint VM running WG+PBS, hub source-of-truth over WireDesiredState,
one datastore + per-customer namespaces, relay-through-DooPlex rejected). Grounded
at file:line vs felhom.eu@bf099f6 + felhom-agent@4ba1b14. Backlog line resolved to
cite spike + design doc. Docs-only; slices are separate future tasks.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
P0-P7 probe ladder on real ends (demo-felhom PVE host <-> throwaway Hetzner).
Verdict: GO at H1 — plain host-side WG, box-dials-out to a small public endpoint;
Headscale is a separable fleet layer, not a traversal necessity. Line is plain-NAT
(not CGNAT) with zero IPv6 — recorded honestly; CGNAT vantage deferred to Peti VM 110.
Real 2 GiB worst-case PBS backup over the tunnel at the full home uplink, pin intact.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
Companion to felhom-agent v0.62.0: the stale-lock reaper reads GET /pools/felhom
as its ownership registry. Pool.Allocate does NOT satisfy the read (spike T2).
Idempotent upgrade via --rescope-acl (_ensure_role modifies to the exact set).
Rescope FIRST, agent second. + REUSE §2 errata: gitea-creds is NOT out-of-band
(committed in felhom.secret.yaml, live-consumed — rotation pending).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
Live-probed on felhom-pve under the PRODUCTION scoped token vs root:
enumeration IS pool-filtered (T1: token sees [9201] of 4 guests); pool read
403s naming Pool.Audit (T2); Pool.Audit@/pool/felhom alone suffices (T3,
throwaway identity, torn down); /cluster/resources withholds the pool field
without Pool.Audit (T7); local ownership records all partial (T5).
Recommendation for the A1 impl spec (now unblocked): Pool.Audit added to
FelhomAgentGuest + GET /pools/felhom cross-check in staleLockController,
fail-safe skip on read failure. Appendix: committed-secrets rotation
micro-runbook (operator follow-up).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
- Both pages: Google Fonts links + preconnects removed; ONE stylesheet
(/assets/site.css?v=1); embedded <style> blocks deleted; body classes
page-index/page-kapcsolat; canonical nav/footer (active marker per
page; index's #szolgaltatasok href normalized to /#szolgaltatasok);
all emoji -> sprite icons (feature tiles = .ico-tile 48px bg-2
squares; headings .ico-lg; inline .ico) or plain text.
- kapcsolat: the contact form is functionally frozen — every field
name/id, the submit JS, and the /api/contact endpoint byte-identical;
only the visual layer changed (upload/paperclip + file-type icons as
sprite refs in JS strings, the x button as ×, status-message
emoji prefixes dropped).
- site.css: CSS-generated marks (content '✓'/'✗'/'⚠'/'★ …') replaced by
currentColor mask-based marks / plain text (emoji-free stylesheet;
gate now scans it too); .ico-tile is svg-as-tile (immune to container
display rules), .ico-lg added.
- Gates: zero failures for the two converted pages; the remaining five
convert in Part 3.
- website/assets/site.css: ONE stylesheet for all seven pages — vendored
@font-face (4 faces, latin+latin-ext, /assets/fonts/ paths), the
design-system v2 :root token block verbatim, a hand-written shared
base (reset, nav, two-tone heading as solid blue-bright, section/page
headers, buttons, card, footer, hamburger + mobile menu, icon-tile,
reduced-motion), and per-page sections mechanically converted from
the seven embedded style blocks (tokens renamed, radii → 2px,
box-shadows/text-gradients/hover-lifts removed, greens → blue per
exception-color) scoped under .page-<name> body classes.
- website/assets/fonts/: the 4 woff2 files copied byte-identical from
felhom-controller (self-hosted — removes the Google Fonts CDN / GDPR
exposure once the pages switch over).
- website/assets/icons.svg: 70-symbol Lucide sprite (the D0 30 + 40
marketing icons) for <use href="/assets/icons.svg?v=1#i-name">.
- scripts/site_gates.py: 8 gates (BOM bytes, Python-codepoint emoji,
nav/footer consistency after active-marker normalization, analytics
presence, no-CDN, banned legacy tokens, zero <style> blocks,
?v= cache-busting). Baseline against the unconverted pages: 84
problems, 182 emoji — goes green with the page conversion commits.
- Live site unaffected: nothing references the new assets yet.
step_agent_install now fetches configs/felhom-mkfs-guarded.sh (bash -n validated)
and installs it 0755 root to /usr/local/sbin/felhom-mkfs-guarded BEFORE the
sudoers (which — from agent v0.54.0 — allowlists only the wrapper, not raw mkfs,
plus read-only pvs/zpool). Live-validated on felhom-pve. bash -n + shellcheck clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
step_token now grants 3 scoped roles (Guest@/pool/felhom incl. Pool.Allocate,
Store@each PVE_STORAGES incl. felhom-pbs, Base=Sys.Audit+SDN.Use@/) to both user
and token, pool-before-token, and removes any pre-3b broad / grant. New
--rescope-acl migrates existing installs (add-before-remove; supervised).
--uninstall removes both scoped + old shapes. Post-provision pool_add_guest
dropped (agent restore --pool owns membership, v0.53.0). Validated dry-run on
felhom-pve; bash -n + shellcheck clean. LIVE rescope+swap is the supervised STOP.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Every managed guest joins a dedicated `felhom` pool (fleet uniformity; foundation
for the later pool-scoped ACL). All pool ops run as root@pam from the installer —
NO agent/token/ACL change (PVE_PRIVS untouched). step_provision creates the pool
+ adds the guest; new --adopt-pool retrofits existing guests non-destructively
(ours-checked, membership-only); --uninstall deletes the pool only if empty.
Confirmed pveum pool / /pools syntax live; validated dry-run + SAFE live adopt of
9201 on felhom-pve. bash -n + shellcheck clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Optional --cores N / --memory M (MiB) passed through to the agent's
--selftest=provision as -cores/-memory ONLY when set (0/unset = golden default;
avoids unknown-flag death on an agent < v0.52.0). Pre-flight soft WARN when a cap
exceeds host nproc/MemTotal. Validated dry-run on felhom-pve. bash -n + shellcheck
clean. Deploy dependency: hub artifact manifest must serve agent >= v0.52.0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Colleague-safety batch #1+#2 (script-only). Adds a guarded, dry-run-aware
--uninstall local host teardown (guest -> agent -> pveum(ACL,token,user,role)
-> golden(opt-in) -> state file), with ours-check on the /etc/felhom-bootstrap
mount, typed vmid confirm, other-Felhom-guests safe-skip, and idempotent
tolerate-absent. Records customer_id + provisioned_vmid into the install state
(_state_put/_state_get) so uninstall resolves its target automatically. Adds
three provision pre-flight guards: multi-node (--node required), archive-storage
exists, and a RAM-floor WARN. Validated dry-run-only on felhom-pve.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Part A (hub): move the global-floor + Day-0-artifacts cards from the Customers
page to the Configuration tab; routes → /configuration/{global-floor,artifacts};
redirects + flashes to /configuration. Customers page back to list + Add.
Part B: online setup command on the customer page (download-then-run, passphrase
at prompt, not templated); serve /scripts/ from the website (sparse-checkout +
nginx location) so felhom.eu/scripts/felhom-host-install.sh resolves; script
passphrase prompt reads < /dev/tty (works for pipe-to-bash too).
Part C (script): --vmid auto-detect — default 9201 in use + no --force → pick the
next free id from pct+qm and confirm; explicit --vmid stays die-unless-force.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
--preserve-state-from DIR carries the prior local-api.{crt,key}+local-tokens.log into the agent
state dir (validates the leaf parses) so the pinned fp stays stable across a reinstall — no
re-bootstrap. Populated-host guard: refuse to proceed leaf-less when the host already has guests
unless --preserve-state-from or --allow-new-leaf is given (converts the 2026-06-28 silent footgun
into a hard stop). bash -n clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pg8ANF97SEeKYSN5Jxw3qJ
A host that previously ran the agent as root+direct has no sudo package, so visudo
and runtime sudo -n are missing. step_agent_install now apt-get installs sudo before
the sudoers/unit, and resolves visudo by absolute path (non-login SSH PATH gap).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>