v0.5.0-rc1: slice 5 Phase A — storage observe/report + watchdog (read-only, live)

Fill the slice-3 storage_targets stub and add the fast-poll storage watchdog.
Read-only this phase; the host-root surface (mounts/SMART/grow/destructive gate)
is Phase B. Hub-owned desired manifest is slice 10, so reconcile against it is
built-but-unfed.

- internal/storage: StorageTarget wire contract, durable_id derivation per type,
  HostReader seam (procfs/sysfs, root-free), Observer (storage_targets from
  ListStorage/NodeStorage + host reads, lvmthin thin-pool fill), and the watchdog
  (third daemon goroutine; debounced out-of-band report on a known target's
  attach/disconnect transition).
- proxmox.Storage: additive parse-only config fields (durable_id sources).
- collector StorageObserver seam; Loop.SetTrigger out-of-band report; daemon runs
  the watchdog as a third goroutine; StorageConfig knobs.
- cross-repo golden kept byte-identical with felhom.eu/hub; bidirectional key-set test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-09 09:59:05 +02:00
parent 1af21a6cac
commit 27b68f043b
22 changed files with 2129 additions and 103 deletions
+52
View File
@@ -3,6 +3,58 @@
All notable changes to **felhom-agent** are recorded here. Update on every code
change that gets pushed.
## 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
host storage target, reports it into the host-report's `storage_targets` (previously an empty
stub), and runs a fast-poll watchdog that pushes a disconnect to the hub in seconds. No
host-root writes this phase (mounts/SMART/grow/destructive-gate are Phase B). The hub-owned
desired manifest (class/role/policy/creds) is not served until slice 10, so reconcile against
it is built-but-unfed — this phase ships only the genuinely-useful read-only footprint.
### Added
- **`internal/storage` package** (new):
- **`StorageTarget` wire contract** (`internal/hub/report.go`) — filled the slice-3 stub:
`name`/`type`/`durable_id`/`state`/`reachable`, usage (`total`/`used`/`avail`/
`used_fraction`), `content`, `mount_path`/`backing_device`, `class_hint` (rotational HINT
— never authoritative; class is hub-owned), `role` (empty until slice 10), a `thin_pool`
sub-object (lvmthin data fill; metadata fill is Phase B/`lvs`), and a `smart` sub-object
(`UNKNOWN` until Phase B). Cross-repo golden kept byte-identical with `felhom.eu/hub` and
guarded by the bidirectional key-set test (`contract_test.go`).
- **`durable_id` derivation** (`durableid.go`) — deterministic per type (the DR-load-bearing
re-attach key): fs-UUID (usb/local-dir), `server:export` (nfs/cifs), `repo+fingerprint`
(pbs), `vg/pool` (lvmthin); never empty (falls back to a stable store id).
- **`HostReader` seam + `ProcHostReader`** (`hostread.go`) — non-privileged `/proc/mounts`,
`/dev/disk/by-uuid`, `/sys/.../rotational` + `removable` reads. Root-free by construction.
- **`Observer`** (`observe.go`) — builds `[]hub.StorageTarget` from `ListStorage`/`NodeStorage`
joined with host reads; surfaces the lvmthin thin-pool data fill prominently (warns ≥85%).
- **Storage watchdog** (`watchdog.go`) — a third daemon goroutine fast-polling the *known*
target set (a defined Proxmox storage and/or a previously-seen one) for
`attached↔disconnected` transitions; on a transition it triggers an immediate, **debounced**
out-of-band host-report. Only flags a *known* target's change (never a never-attached
device); coalesces flaps within the debounce window (leading + trailing edge).
`CachingKnownTargets` rate-limits the Proxmox-derived known set; `HostLiveness` probes
device/mount presence (local) + a reachability dial (network), all non-privileged.
- **Proxmox `Storage` type** (`internal/proxmox/types.go`) — additive parse-only config fields
(`server`/`export`/`share`/`datastore`/`fingerprint`/`vgname`/`thinpool`) feeding durable_id.
- **Collector `StorageObserver` seam** (`internal/hub/collect.go`) — populates `storage_targets`
via the observer; a nil observer or an observe error degrades to empty (never sinks the
heartbeat). Hub does not import storage (storage imports hub for the wire type).
- **Out-of-band report trigger** (`internal/hub/loop.go`) — `Loop.SetTrigger`: a watchdog
signal runs one extra collect→report immediately without disturbing the regular cadence.
- **`StorageConfig`** (`internal/config`) — watchdog interval / debounce / known-refresh knobs
(all optional; package defaults otherwise).
- **Hub ingest** (`felhom.eu/hub`) — `hostReportPayload` now parses `storage_targets`
(full mirror struct), persists them via `report_json`, counts + warns on disconnected
targets, and has its own half of the bidirectional golden key-set test.
### Notes
- The daemon still runs cleanly with no removable storage, no signers, and no hub manifest —
the watchdog finds nothing to flag; storage reporting is best-effort.
- `proxmox`/`hub`/`authz`/`reconcile` exported surfaces + their golden/adversarial tests are
intact. No host-root writes, no destructive paths, no SMART this phase (all Phase B).
- Version: **v0.5.0-rc1** at the Phase-A checkpoint; **v0.5.0** when Phase B lands.
## v0.4.0 — slice 4 Phase B: reversibility gate + signed-op consuming layer (2026-06-08)
The security core of slice 4: hub-supplied intent stops being trusted for destructive