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>
This commit is contained in:
@@ -3,6 +3,63 @@
|
||||
All notable changes to **felhom-agent** are recorded here. Update on every code
|
||||
change that gets pushed.
|
||||
|
||||
## v0.5.0 — slice 5 Phase B: the host-root surface (mounts + SMART + grow + destructive gate) (2026-06-09)
|
||||
|
||||
The write surface — the agent's first step outside its Proxmox API token into OS-root.
|
||||
Isolated behind a narrow, argument-validated, adversarially-tested seam, exactly like the
|
||||
slice-4 gate. Completes slice 5 (Phase A = read-only observe/report/watchdog at v0.5.0-rc1).
|
||||
|
||||
### Added
|
||||
- **`HostOps` seam + `SudoHostOps`** (`internal/storage/hostops.go`) — the one privileged
|
||||
host surface: persistent mounts via **systemd `.mount` units keyed by fs-UUID** (enabled to
|
||||
survive reboot), detach (stop+disable), SMART, and thin-pool metadata. Shells out via the
|
||||
fenced Runner (`sudo -n`, **fixed arg vectors, no shell**); a fake backs the tests (no real
|
||||
root in the suite). `NoopHostOps` is the safe fallback when the surface is unavailable.
|
||||
- **The argument validator** (`internal/storage/validate.go`) — the security boundary:
|
||||
`ValidateUUID` (strict hex), `ValidateMountPath` (absolute, no traversal, no metacharacters),
|
||||
`ValidateSMARTDevice` (raw-disk whitelist), `ValidateLVMName`, and an in-process
|
||||
`systemdEscapePath` (no `systemd-escape` shell-out). **Every argument is validated BEFORE a
|
||||
command is constructed.** Headline test (`validate_test.go`): an adversarial matrix of
|
||||
shell metacharacters / `../` traversal / malformed inputs is rejected with **zero exec**.
|
||||
- **SMART** (`internal/storage/smart.go`) — parses `smartctl -a -j` into `StorageTarget.smart`:
|
||||
**SATA** (reallocated/pending/offline-uncorrectable, temp, power-on-hours) **and NVMe**
|
||||
(critical_warning, media_errors, percentage_used, temp), degrading to `UNKNOWN` for devices
|
||||
with no SMART (USB-SATA bridges). **`lvs`** fills the lvmthin thin-pool **metadata** fill
|
||||
(the value Phase A left null). Wired into the Observer's enrichment (Observe only, not the
|
||||
watchdog's fast Known path).
|
||||
- **Watchdog re-mount response** (`internal/storage/watchdog.go`) — on a known mount-backed
|
||||
target's device returning **unmounted** (a new `DevicePresent` liveness probe), the watchdog
|
||||
**dispatches a benign by-UUID re-mount off the poll path** (a goroutine, never under the
|
||||
lock), rate-limited per target to the debounce window. The mount is routed through the gate
|
||||
as benign (`gateRemounter` in `main.go`, so `storage` stays decoupled from `reconcile`).
|
||||
- **Disk-grow executor** (`internal/reconcile`) — `ActionResize` (benign `ClassResize`), planned
|
||||
**grow-only** (desired DiskBytes > actual → `pct resize rootfs +<n>M`; a shrink is refused,
|
||||
never silently grown) + a defensive executor guard (size must start with `+`). New
|
||||
`proxmox.Client.ResizeLXC` (API; `VM.Config.Disk`+`Datastore.AllocateSpace`; async→UPID).
|
||||
Built + fixture-tested; **unfed** live (no hub spec until slice 10).
|
||||
- **Destructive storage ops through the slice-4 gate** (`internal/reconcile/storage_ops.go`) —
|
||||
`IntentForStorageMount` (benign) and `IntentForStorageDestructive` (`ClassStorageWipe`/
|
||||
`ClassDecommission`). Host/target-scoped: the op binds on the storage **target identity**
|
||||
(carried in `target.guest_id`). Reuses the existing verifier/role-scoping/binding/audit — no
|
||||
new gate, no new crypto. Storage cases added to the adversarial matrix (`storage_test.go`):
|
||||
unsigned wipe → `pending_signature`; "wipe A" signature vs "wipe B" → `binding_mismatch`;
|
||||
valid → accepted. **Inert** live.
|
||||
- **`--selftest=storage` [`-watch <dur>`]** — the live USB-runbook harness: an observe pass
|
||||
(full table incl. SMART + thin-pool data+metadata), and a bounded watchdog window with the
|
||||
re-mount response live. Runs standalone on the Proxmox host (no hub).
|
||||
- **`configs/felhom-agent.sudoers`** — the documented narrow allowlist (install unit / systemctl
|
||||
manage / smartctl / lvs), with the agent-side fine validation noted.
|
||||
- **Config**: `privileged.{unit_dir,stage_dir,systemctl,install,smartctl,lvs}` (paths must match
|
||||
the sudoers entries).
|
||||
|
||||
### Notes
|
||||
- Daemon still runs cleanly with no removable storage / no signers / no hub manifest, and a
|
||||
missing/declined sudoers entry degrades with a warning (SMART→UNKNOWN, mount→logged error),
|
||||
not a crash. `go test -race` passes (the watchdog re-mount dispatches off the poll path).
|
||||
- Slice-3/4 + Phase-A exported surfaces, goldens, and adversarial tests intact. `authz`
|
||||
untouched. The destructive-storage executor + grow are built/tested but unfed live until
|
||||
slice 10.
|
||||
|
||||
## v0.5.0-rc1 — slice 5 Phase A: storage observe + report + watchdog (read-only, live) (2026-06-09)
|
||||
|
||||
Phase A of the storage slice (doc 03 §7). Read-only and live: the agent now observes every
|
||||
|
||||
Reference in New Issue
Block a user