# REPORT — felhom-agent v0.23.0: device-ROLE classification + tiered storage-wipe gate ## What this implements The **agent half** of the storage-authorization redesign (CC SPEC, Part A). Destructive storage wipes are now **tiered by the device's protection ROLE**, classified by the agent from its own inspection — never the controller's or hub's claim. ### A1 — Device-ROLE classification (authoritative) - New `internal/storage/role.go`: `DeviceRole` = `system` | `backup` | `user-data`. - `SystemDisks(host)` resolves the OS whole-disks (the disks backing `/`, `/boot`, `/boot/efi`) from the mount table + world-readable `/dev` symlinks (root-free; the root-CLI fence is untouched). - `RoleForStorage(typ, backingDevice, …)` tiers a storage-view target; `RoleForRawDevice(device, …)` tiers a raw device (a fresh disk in the init flow, not yet a PVE storage). - `pbs` → **backup**; `lvmthin` / builtin `local` / nfs / cifs / unknown → **system**; `usb` / `local-dir` on a **non-system external device** → **user-data**. - **Fail-safe**: any ambiguity → **system** (most-protected) — consistent with destructive-on-ambiguity. - `GET /disks` → each `DiskInfo` now carries `role`. ### A2 — Gate tier (`internal/reconcile`) - New `CustomerConfirmable` disposition + `StorageRole{System,Backup,UserData}` (string mirror of `storage.DeviceRole`, kept in reconcile to avoid a storage→reconcile import edge). - `Gate.AuthorizeStorageWipe(StorageWipeAuthz, signed)`: - **user-data** → allowed iff `Confirmed` AND the confirmation's durable id matches the device's agent-re-resolved durable id (`ReasonCustomerConfirmed`); unconfirmed → `ReasonPendingConfirmation`; mismatch / missing durable → `ReasonBindingMismatch`. Audited with the durable id. - **system/backup/unknown** → the standard operator-signature path (`pending_signature`); `Confirmed` is ignored — refused **by role**. - `AuditRecord.DurableID` added (the customer-visible "who/what/when + durable id"). ### A3 — Endpoint plumbing (`internal/localapi`) - `POST /disks/format` accepts `confirmed` + `durable_id`. The data-bearing path: - resolves the device's **authoritative role** (`deviceRole` — prefers a matching storage target's role so PBS is recognized as backup, else raw classification), re-resolves the durable id, and calls the tiered gate. - user-data confirmed → `mkfs` (200); user-data unconfirmed/mismatch → 403 with `needs_confirmation` + the durable id (no opsign); system/backup → 403 with the operator-signature pending op. - blank devices stay benign `mkfs` (unchanged). - `StorageGate` interface widened: `AuthorizeWipe(WipeRequest) WipeDecision`. Adapter in `main.go` bridges to `Gate.AuthorizeStorageWipe`. - Eject stays **benign** (unmount, data preserved) — its customer confirmation is a controller-side UI concern, not a gate tier (per the hard rule: confirmation walks the gate ONLY for user-data `storage_wipe`). ## Tests (non-hollow) - `internal/storage/role_test.go` — demo-storage mapping (`local`/`local-lvm`/`felhom-pbs` protected, `felhom-usb` on `/dev/sdb1` user-data) + fail-safe when system disks are unknown. - `internal/reconcile/storage_wipe_test.go` — a `confirmed:true` wipe on **system/backup is refused** (falls to pending_signature, no exec); user-data durable-id **mismatch refused**; missing durable refused; unconfirmed → pending_confirmation; unknown role fails safe. - `internal/localapi/disks_test.go` — format-handler branches: user-data confirmed → mkfs; user-data unconfirmed → 403 needs_confirmation with **no** operator-signature pending op; confirmed-but-protected → still refused (role beats confirmation); blank → mkfs without the gate. `go build ./... && go vet ./... && go test ./...` all green (Go 1.26, local). ## Version `v0.22.0 → v0.23.0` (`version` var in `cmd/felhom-agent/main.go`). ## Notes / residuals - The one accepted residual (per spec): a compromised controller could `mkfs` a **user-data** drive it could already `rm -rf` — same data loss, no new reach. System/backup remain operator-only. - LVM-root caveat: when `/` is on device-mapper, the OS whole-disk is pinned via `/boot`/`/boot/efi` (a raw ESP partition on the OS disk on standard Proxmox/Debian installs); the fail-safe default covers anything that cannot be resolved.