Files
felhom-agent/internal/reconcile/storage_ops.go
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

50 lines
2.2 KiB
Go

package reconcile
import "encoding/json"
// Storage operations (slice 5 Phase B) flow through the SAME reversibility gate as guest
// ops — no new gate, no new crypto. They are HOST/TARGET-scoped (no guest), so the op binds
// on the STORAGE TARGET IDENTITY rather than a vmid.
//
// Scoping decision (documented): the scoped resource id is carried in the op's
// target.guest_id (and the Intent.GuestID) as the storage target's NAME — the operator-
// facing handle and the hub manifest key. VMID is 0 (host-scoped; no queue routing by
// guest). So a signature for "wipe target A" (guest_id="A") cannot authorize "wipe target
// B" (guest_id="B") — the gate's op-to-action binding rejects it (binding_mismatch),
// exactly as it does for the wrong guest on a guest op.
//
// Benign storage ops (re-mount, slice 5) use IntentForStorageMount and pass the gate
// unsigned. Destructive storage ops (detach/wipe/decommission, inert until slice 10) use
// IntentForStorageDestructive and require a verified, role-scoped, target-bound operator
// signature — else pending_signature.
// IntentForStorageMount builds the benign re-mount intent for a known target (additive, no
// data loss → benign by classification). targetID is the storage target name.
func IntentForStorageMount(hostID, targetID string) Intent {
return Intent{
Class: ClassStorageMount,
HostID: hostID,
GuestID: targetID, // storage target identity (host-scoped op)
VMID: 0,
Provenance: Provenance{}, // never hub-sourced
Source: SourceDesiredDelta,
}
}
// IntentForStorageDestructive builds a destructive storage intent (detach/wipe via
// ClassStorageWipe, or ClassDecommission). It carries the target identity in GuestID and the
// canonical params for op-to-action binding. Provenance is the zero value — a destructive
// storage op is NOT made benign by hub-supplied evidence (only agent-internal provenance
// could, and storage detach/wipe carries none here).
func IntentForStorageDestructive(class OpClass, hostID, targetID string, params json.RawMessage, source SourceKind) Intent {
return Intent{
Class: class,
HostID: hostID,
GuestID: targetID,
VMID: 0,
ParamsJSON: params,
Provenance: Provenance{},
Source: source,
}
}