Files
felhom-agent/REPORT.md
T
admin 9d6e49236c v0.5.0: slice 5 Phase B — the host-root surface (mounts + SMART + grow + destructive gate)
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>
2026-06-09 10:53:38 +02:00

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

  • HostOps seam + SudoHostOps — the one privileged surface: persistent mounts via systemd .mount units 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. NoopHostOps is 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-process systemd-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 at SudoHostOps (a recording runner asserts no command was built).
  • SMARTsmartctl -a -j parsed into StorageTarget.smart for SATA and NVMe attribute sets, degrading to UNKNOWN on devices with no SMART. lvs fills the lvmthin thin-pool metadata fill. Enrichment runs in Observe only (not the watchdog's fast Known path).
  • Watchdog re-mount response — a new DevicePresent probe 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 (gateRemounter in main.go, keeping storage decoupled from reconcile).
  • Disk-grow executorproxmox.ResizeLXC (API, async) + a benign ActionResize planned 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 gateIntentForStorageMount (benign) / IntentForStorageDestructive (ClassStorageWipe/ClassDecommission), host/target-scoped (binds on the storage target identity in target.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. New privileged.* 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)

  1. Unit-file write mechanism. The agent stages the .mount unit to an agent-owned dir (privileged.stage_dir, default /var/lib/felhom-agent/units) then sudo 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.
  2. 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.
  3. 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).