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:
@@ -0,0 +1,49 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user