The privileged write surface, isolated behind a narrow, arg-validated, adversarially- tested seam (HostOps), the same discipline as the slice-4 gate. Completes slice 5. - internal/storage: HostOps seam + SudoHostOps (systemd .mount units by fs-UUID, detach, SMART, lvs) via sudoers allowlist + fixed arg vectors, no shell; NoopHostOps fallback. - validate.go: strict UUID/mount-path/device/LVM validators + in-process systemd-escape. Headline test: adversarial matrix (metacharacters/traversal/malformed) refused with zero exec. - smart.go: smartctl SATA + NVMe parse, UNKNOWN-degrade; lvs thin-pool metadata fill. - observer enrichment (Observe only): fills smart + thin-pool metadata. - watchdog: benign re-mount response off the poll path (DevicePresent probe, rate-limited). - reconcile: ActionResize (benign, grow-only) + proxmox.ResizeLXC; destructive storage ops (ClassStorageWipe/Decommission) through the slice-4 gate, target-scoped; built+tested, inert live. - --selftest=storage [-watch] live harness; configs/felhom-agent.sudoers; privileged.* knobs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5.6 KiB
REPORT — Slice 5 Phase B: the host-root surface (v0.5.0) (2026-06-09)
Overwrite-latest report (most recent significant work only). Cumulative history lives in CHANGELOG.md.
Outcome
Slice 5 is complete and pushed as v0.5.0. Phase B adds the privileged write surface —
the agent's first step outside its Proxmox API token into OS-root — isolated behind a narrow,
argument-validated, adversarially-tested seam, the same discipline as the slice-4 reversibility
gate. Phase A (read-only observe/report/watchdog, v0.5.0-rc1) is reused unchanged.
What landed
HostOpsseam +SudoHostOps— the one privileged surface: persistent mounts via systemd.mountunits keyed by fs-UUID (enabled, survive reboot), detach (stop+disable), SMART, thin-pool metadata. Shells out via the fenced Runner (sudo -n, fixed arg vectors, no shell). Tests use a fake — no real root in the suite.NoopHostOpsis the degrade-cleanly fallback.- The argument validator (the security boundary) —
ValidateUUID/ValidateMountPath(absolute, no traversal, no metacharacters) /ValidateSMARTDevice(raw-disk whitelist) /ValidateLVMName, plus an in-processsystemd-escape. Every argument is validated before a command is constructed. The headline test is an adversarial matrix (shell metacharacters,../, malformed) that must be refused with zero exec — proven both at the validators and atSudoHostOps(a recording runner asserts no command was built). - SMART —
smartctl -a -jparsed intoStorageTarget.smartfor SATA and NVMe attribute sets, degrading toUNKNOWNon devices with no SMART.lvsfills the lvmthin thin-pool metadata fill. Enrichment runs inObserveonly (not the watchdog's fastKnownpath). - Watchdog re-mount response — a new
DevicePresentprobe lets the watchdog detect a known mount-backed target whose device returned but is unmounted, and dispatch a benign by-UUID re-mount off the poll path (goroutine, never under the lock), rate-limited to the debounce window. Routed through the gate as benign (gateRemounterinmain.go, keepingstoragedecoupled fromreconcile). - Disk-grow executor —
proxmox.ResizeLXC(API, async) + a benignActionResizeplanned grow-only; a shrink is refused (never silently grown) at the planner and re-guarded at the executor. Built + fixture-tested; unfed live. - Destructive storage ops through the slice-4 gate —
IntentForStorageMount(benign) /IntentForStorageDestructive(ClassStorageWipe/ClassDecommission), host/target-scoped (binds on the storage target identity intarget.guest_id). Reuses the existing verifier/role-scoping/binding/audit. Adversarial cases: unsigned →pending_signature, wrong-target →binding_mismatch, valid → accepted. Inert live. --selftest=storage[-watch <dur>] — the live USB-runbook harness (observe pass + a bounded watchdog window with re-mount live), standalone on the Proxmox host (no hub).configs/felhom-agent.sudoers— the documented narrow allowlist + the agent-side fine validation. Newprivileged.*config knobs for the binary paths / dirs.
Tests
go test ./... green; go test -race ./... green on the build server (the watchdog
re-mount dispatch runs off the poll path concurrently). New tests: the validator adversarial
matrix (headline) + SudoHostOps "no-exec-on-hostile-arg"; HostOps mount/re-mount lifecycle
against a fake; SMART parsing SATA + NVMe + unsupported; thin-pool metadata parse; the grow
executor (grow applies, non-grow refused at the executor) + plan grow-only/shrink-refused;
the destructive-storage gate cases (unsigned / wrong-target / valid) reusing the slice-4
real-verifier harness; and the watchdog → re-mount path (device-return triggers, rate-limited,
re-armed after a successful mount).
Design decisions (flagged for the reviewer)
- Unit-file write mechanism. The agent stages the
.mountunit to an agent-owned dir (privileged.stage_dir, default/var/lib/felhom-agent/units) thensudo install -o root -g root -m 0644 -- <stage> /etc/systemd/system/<unit>. This avoids a stdin-to-root channel and keeps fixed arg vectors. The sudoers entry uses path-prefix wildcards; the agent's strict validation (unit name derived from a validated mountpoint; no traversal) is the fine gate, so the wildcard can't be abused. Swappable behind the seam if you prefer another mechanism. - Storage-op gate scoping. Host/target-scoped destructive ops carry the storage target
name (the operator handle / hub manifest key) in
target.guest_id, with VMID 0. So "wipe A" cannot authorize "wipe B" (binding_mismatch), exactly like the wrong-guest case. - Shrink handling. A desired disk < actual is refused by omission at the planner (no
action) and re-guarded at the executor (size must start with
+); a deliberate shrink would be a signed destructive op (slice 10), never a benign reconcile grow.
Live validation (for project Claude / the runbook)
Not run from here (the observer reads the local host's /proc/mounts + privileged tools, so a
meaningful run must be on the demo Proxmox host, not the build server). The
--selftest=storage -watch harness is the intended live USB-cycle test:
felhom-agent --selftest=storage -watch 3m on felhom-pve, then physically unplug/replug a USB
target and watch detect → out-of-band report → by-UUID re-mount in the logs. SMART/lvs need the
sudoers drop-in installed (configs/felhom-agent.sudoers).