From AUDIT-blast-radius-hostroot-localapi-2026-07-02.md. Each fix ships with a
non-hollow test + a companion red-proof (shown failing on the pre-fix impl).
Sudoers install-source grants became globs — deploy the sudoers drop-in with
the binary. A1 (stale-lock pool-membership) deliberately excluded (spike).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
Campaign pool-effects F1 (HIGH): the bring-up compensating rollback and the
restore-test teardown destroyed the target vmid even when RestoreLXC failed
synchronously without creating anything — destroying a guest the transaction
never made (only the pool ACL 403 contained it). A RestoreLXC UPID is now the
sole destroy authorization in all three destroy paths (in-process bring-up
defer, in-process restore-test teardown, Recover). F2: the restore-test
advances past an 'already exists' band vmid (invisible squatter) instead of
failing + false-alerting; a fully-occupied band Skips.
Red-proof verified: with the gates reverted, the four new tests fail with the
innocent-guest destroy. go build/vet/test clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Use ResolveStorageDevice (EvalSymlinks) so a registry drive shows /dev/sdd, not
the /dev/disk/by-uuid/<uuid> symlink path — matching Observe-sourced rows.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Agent-view showed "—" device + no size for a raw (no-PVE-storage) drive because
the registry union row never set backing_device/total_bytes/used_bytes (Observe
drives get those from pvesm status). Resolve BackingDevice via ByUUIDDevicePath +
read capacity via statfsCapacity (build-tagged syscall.Statfs; no-op off-Linux).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Impl-2a registry union row omitted GuestPath + BoundUnderParent, so the
controller read a registry-only (raw) drive as "Leválasztva" despite being mounted
+ bound live. Populate them like the Observe path (StablePathForRaw +
boundUnderParent). Last piece of first-class raw-drive support.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
buildLocalAPIServer never passed Options.HostReader, so s.host was nil — the
v0.56.0 durableIDForMount raw-mount fallback + the role gate's host classification
silently no-op'd. Wire storage.NewProcHostReader(). This makes the raw-drive
intent/guest-bind resolution actually fire live.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ReassertGuestBinds mapped durable-id->mount from Observe() only, so a raw enrolled
drive was never found and its in-guest bind wasn't re-asserted after reboot/re-mount
(drive showed "Leválasztva"). Augment the map from the mount table (raw /mnt/<name>
-> device fs-UUID via HostReader), skipping the /mnt/felhom-drives bind; Observe
failure no longer aborts. Completes v0.56.0's raw-drive durability.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
durableIDForMount was Observe-only, so a raw (non-PVE-storage) drive's enroll
intent + guest-bind went unrecorded ("durable-id unresolved") — it mounted+bound
but wasn't intent-tracked (RegistryKnownTargets skips intent==new). Fall back to
resolving the mount's device fs-UUID (HostReader.Mounts + ResolveUUID) →
uuid:<fs-uuid>, same scheme as Observe. Test + red-proof. Residual:
ReassertGuestBinds still Observe-based (raw guest-bind not re-asserted post-reboot).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GET /disks/candidates enumerates host disks the Impl-1 unclaimed filter proves
free (init/attach split). RegistryKnownTargets sources the watchdog's known-drive
set from the intent registry + Felhom .mount units (not Observe/PVE storages) —
decouples drive health from PVE storage (closes the registry-only false-detach
class); Observe kept for real PVE storages + a deduped /disks union. Idempotent
existing-drive migration at start. Tests + red-proof (Observe misses a
registry-only drive; registry provider tracks it). go build/vet/test clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Impl-1. Format now runs a mandatory unclaimed-disk guard (internal/storage/claim.go:
SystemDisks + lsblk member-FSTYPE + foreign-mount + RO + pvs/zpool; fail-safe →
CLAIMED) before any mkfs — refuses the OS disk / LVM PV / ZFS-mdraid member /
foreign-mounted device even when non-data-bearing (guard sits in Format, not the
handler). Below the agent, mkfs goes ONLY through configs/felhom-mkfs-guarded.sh
(sudoers no longer allowlists raw mkfs.*), which re-checks the catastrophic cases
as root. Read-only pvs/zpool added to FELHOM_DISK. Tests + red-proof; capability
manifest updated. go build/vet/test clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
RestoreLXCOptions.Pool → pct restore --pool (omit-when-empty). New
reconcile.DefaultPool="felhom"; BringUpSpec.Pool threaded to the bring-up
restore; BOTH restore sites pool the guest (provision/DR via spec.Pool set to
DefaultPool by the CLI; restore-test scratch via DefaultPool = SPIKE residual
#2). No agent ACL change (ships in host-install v1.6.0); the pool param is inert
until the token has Pool.Allocate + the pool exists, so publishing is safe ahead
of the coordinated swap. Tests + red-proofs; go build/vet/test clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New -cores N / -memory M (MiB) flags for --selftest=bring-up|provision (0 = keep
golden default), flowed through bringUpSizing into reconcile.BringUpSpec so the
existing buildBringUpConfig emits cores/memory into the pre-start config PUT (the
cap lands before first boot). No engine change. New pure-function test
TestBuildBringUpConfig_ResourceCaps asserts the set + omit-when-zero cases
(red-proof run + reverted). go build/vet/test clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The preventive counterpart to host_disk + storage_fill detectors: the periodic local
whole-guest vzdump now prunes its own old archives (keep-last=3, clamped >=1) so a box
can't refill its own root via its own backups. Local target only — PBS never pruned
(resolved via ListStorage; fail-safe skip on unknown). Seeded in host-install.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxLA1mZurFq9kt8hneFeCs
Host-side NFS/SMB automount of a bulk-media NAS share under /mnt/felhom-drives/<name>
(propagates into the guest via the existing shared bind), the +100000 uid recipe,
per-share liveness, and add/list/remove local-API endpoints. A NAS is a distinct class
that bypasses the drive enroll/eject/decommission/SMART/watchdog machinery.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HxLA1mZurFq9kt8hneFeCs
F2-a red-proof PASS: parent came up own peer group (shared:94, not
shared:1), no doubling, guest sees both drives. F2-b PASS: reboot with the
exact snapshot-delete stuck state → agent auto-recovered (unlock → removed
real dangling vzdump snapshot → started CT). Zero spurious pages. Two
live-caught fixes noted (?source=active, AllowWarnings).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0162BnMpUXscPsUB1cU8Tr6K
Live on 9999: the recovery's start succeeded (guest running) but WaitTask
false-failed on "WARNINGS: 1" (the systemd-nesting notice). Mirror the
restore-test's start step (AllowWarnings) so a booted guest isn't logged
as a start error.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0162BnMpUXscPsUB1cU8Tr6K
Live test on felhom-pve (PVE 9.2.2) caught GET /nodes/{node}/tasks?running=1
returning HTTP 400 "property not defined in schema". The fail-safe correctly
left the lock, but the BackupRunning guard always errored. source=active is
the supported filter and returns the RUNNING tasks.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0162BnMpUXscPsUB1cU8Tr6K
F2-b: at startup, recover a guest left with a stale vzdump lock by a
reboot-during-backup — pct unlock -> delete dangling vzdump snapshot ->
start iff onboot, guarded by a no-vzdump-running invariant (fail-safe).
New internal/localapi/stalelock.go; proxmox GuestConfig.Lock()/OnBoot(),
ListSnapshots, ListRunningTasks, Snapshot type. New narrow sudoers grant
FELHOM_STALELOCK (pct unlock) + Critical capability stalelock-unlock.
F2-a: EnsureSharedParent only redeployed the boot script when the UNIT
differed, so the v0.36.6 make-private fix never reached hosts whose unit
was current -> /mnt/felhom-drives stayed in root's shared:1 and doubled
every drive bind. New sharedParentInstallStale compares BOTH script and
unit. Boot-time-only; never churns the live mount.
Both root causes confirmed live on felhom-pve before fixing. Green gate
(build/vet/test) all pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0162BnMpUXscPsUB1cU8Tr6K
HostReport.LeafFingerprint rides the served fp (from EnsureLeaf) on every report; empty when local
API disabled. Collector.SetLeafFingerprint threads it like Capabilities. Hub watches it for a re-key.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pg8ANF97SEeKYSN5Jxw3qJ
controllerHealthy reads RestartCount (running&&rc>0 -> not ok) + signals needsDwell for no-healthcheck;
verify requires verifyDwell(=3) consecutive ok polls for a no-healthcheck image (real healthcheck
trusted immediately). Closes the F1 hole (alpine crash-loop passed the point-in-time check). Red-proof
+ dwell + real-image tests. No sudoers/orchestration change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pg8ANF97SEeKYSN5Jxw3qJ
writeImage drops bash -c/printf for GuestExecStdin(img+\n -> tee /etc/felhom-controller-image);
new Runner.RunStdin/GuestExecStdin route stdin through the fenced sudo -n runner. 5 narrow,
auditable sudoers grants (no general pct exec, no bash -c) + capability manifest entries (Critical)
so the self-probe watches them and the build-test asserts coverage (companion red-proof). No
controller change; swap orchestration/rollback/state unchanged. Spike GO.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EPZ4GJ8L5Jqf8UiPwbn1kt
New internal/capability: Manifest of required sudo -n grants + Prober that LISTS each
via 'sudo -n -l' (never executes) + binary-exists check → ok/degraded snapshot on the hub
report. Build-time test asserts manifest⊆sudoers (red-proof: dropping lxc-info FAILs the
gate). Startup logs N/N ok + ERROR per degraded. Serve-degraded; no allowlist change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EPZ4GJ8L5Jqf8UiPwbn1kt
Root cause of multi-drive flapping: lxc-info absent from the allowlist → empty guest
PID → bound_under_parent=false for every drive → controller gate stops apps. Audit-driven
completeness pass; surfaces GuestExec/pct-exec (controller-swap) for operator decision.
No binary change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EPZ4GJ8L5Jqf8UiPwbn1kt
Bare commas are command separators in sudoers; the lvs/lsblk -o option lists need
escaped commas. The file had never been visudo-validated live (the demo host ran the
agent root+direct). Surfaced by the BUNDLE host-install visudo -cf gate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- configs/felhom-agent.service: canonical non-root unit (User=felhom-agent, sudo model);
deliberately NO NoNewPrivileges (breaks sudo) and NO mount-namespacing hardening (breaks
the intermediary-mount drive propagation into guests) — documented inline.
- scripts/publish-agent.sh: build (optional) + PUT binary to Gitea generic + sha256 +
GET round-trip. Pinned version, idempotent (delete-then-PUT).
- configs/build-golden.sh: after vzdump, compute sha256 + PUT golden.tar.zst to Gitea
generic (version = baked controller version). Opt-in; local auto-discovery stays fallback.
- cmd/felhom-agent/main.go: version 0.42.0 -> 0.43.0.
- README: process model now canonical (non-root + publish/install).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The CONTROLLER_IMAGE default (arg 6) was a stale :0.43.0, so an argument-less
golden build baked an ancient controller (fresh Day-0 boxes started at 0.77).
Bumped the default to the current :0.85.1; always pass it explicitly per rebuild.
Golden rebuilt at 0.85.1 on felhom-pve (volid vzdump-lxc-9100-2026_06_27-11_42_51);
baked-image verify confirmed :0.85.1 in the build guest. No agent binary change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FSZmmSFVzGwEzhYmxbkgBK
New local-API POST /controller/swap (+ GET /controller/swap/status), withGuest-
scoped: the agent records the previous image, confirms the target is present,
rewrites /etc/felhom-controller-image, restarts felhom-controller-bootstrap.service,
verifies the new controller is healthy (docker inspect, <=90s), and ROLLS BACK to
the previous image if not. Single-flight per guest; strict image-ref validation;
crash-safety state file. GuestBinder.GuestExec is the pct-exec seam.
--selftest=controller-swap exercises it directly.
Tests: happy/rollback-on-unhealthy(+red-proof)/image-absent/no-healthcheck/
bad-image-400/single-flight-409.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TtXesNa2LGbMmE4DNL6SE7
Destroyed + re-provisioned 9201 through the real provision chain with v0.41.0: fresh pct config
showed onboot:1 with no manual set. Host reboot of felhom-pve -> 9201 auto-started (running, no
manual pct start); onboot:0 scratch guests stayed stopped; drives re-bound at stable, sys_drive
separate, controller + infra healthy. Demo controller redeployed to v0.82.0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FpBYrZCt9sFDqLgbG5GRGD
The golden bakes --onboot 0 (template safety) and the back-half never overrode it, so every
provisioned customer guest was onboot:0 -> after a host reboot/power-cut the customer's whole
home-server stayed stopped until a manual pct start. Add a fatal 'pct set <vmid> -onboot 1' step
to BackHalf.Provision (right after the config-mount attach), mirroring the existing pct set ops.
No startup/boot-order: the v0.75 mountpoint-gate covers the drive-bind race at boot.
Golden build-golden.sh unchanged (templates must not auto-start). Unit-tested
(TestProvision_SetsOnbootOne + red-proof). RUNBOOK note added.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FpBYrZCt9sFDqLgbG5GRGD