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:
@@ -47,6 +47,41 @@ The reported `StorageTarget` shape is a cross-repo contract duplicated in `felho
|
||||
`internal/hub/testdata/host-report.golden.json` is byte-identical with the hub's copy and a
|
||||
bidirectional key-set test guards drift.
|
||||
|
||||
### The privileged `HostOps` surface (slice 5 Phase B)
|
||||
|
||||
The write side — the one place the agent steps outside its Proxmox API token into OS-root —
|
||||
is isolated behind the `HostOps` seam (`hostops.go`): production `SudoHostOps` shells out via
|
||||
a narrow **sudoers allowlist** (`configs/felhom-agent.sudoers`) with **fixed argument vectors
|
||||
and no shell**; tests use a fake (no real root in the suite).
|
||||
|
||||
- **Persistent mounts** are **systemd `.mount` units keyed by fs-UUID**
|
||||
(`What=/dev/disk/by-uuid/<UUID>`, enabled so they survive reboot) — not raw fstab or a
|
||||
transient `mount`. Benign re-mount is idempotent; **detach** (stop+disable) is destructive
|
||||
and routes through the gate.
|
||||
- **Every argument is validated before any command is constructed** (`validate.go`): UUIDs
|
||||
against a strict hex regex, mount paths confined + traversal-checked, SMART devices
|
||||
whitelisted to raw disks, LVM names charset-checked. The adversarial matrix in
|
||||
`validate_test.go` proves a hostile UUID / path / device is refused with **zero** exec.
|
||||
- **SMART** (`smart.go`) fills `StorageTarget.smart` via `smartctl -a -j` — SATA *and* NVMe
|
||||
attribute sets, degrading to `UNKNOWN` for devices that expose no SMART (e.g. a USB bridge).
|
||||
**`lvs`** fills the lvmthin thin-pool **metadata** fill (metadata exhaustion corrupts a pool
|
||||
like data exhaustion).
|
||||
- The **watchdog** gains a benign **re-mount response**: when a known mount-backed target's
|
||||
device returns unmounted, it dispatches (off the poll path) a by-UUID re-mount, routed
|
||||
through the gate as benign. The **disk-grow executor** (`pct resize`, **grow-only**) lands
|
||||
in `internal/reconcile` as a benign action; **destructive storage ops** (detach/wipe/
|
||||
data-losing-resize) construct a `ClassStorageWipe`/`ClassDecommission` intent bound to the
|
||||
storage **target identity** and go through the slice-4 gate (built + tested, inert live).
|
||||
|
||||
### `--selftest=storage` (live storage harness)
|
||||
|
||||
Runs standalone on the Proxmox host (no hub needed):
|
||||
- bare: an **observe pass** printing the full `StorageTarget` table incl. the SMART summary
|
||||
and thin-pool data+metadata fill.
|
||||
- `-watch <dur>` (e.g. `--selftest=storage -watch 3m`): runs the watchdog verbose for the
|
||||
window with the **re-mount response live**, so an operator can physically cycle a drive and
|
||||
watch detect → report → re-mount in the logs.
|
||||
|
||||
## The `proxmox` package — model
|
||||
|
||||
Two backends, one fixed routing policy (the fence is structural — `Client` never shells out,
|
||||
|
||||
Reference in New Issue
Block a user