v0.23.0: device-ROLE classification + tiered storage-wipe gate (user-data customer-confirmable; system/backup operator-only)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,30 +1,65 @@
|
||||
# REPORT — agent v0.22.0: expose durable_id in GET /disks (2026-06-11)
|
||||
# REPORT — felhom-agent v0.23.0: device-ROLE classification + tiered storage-wipe gate
|
||||
|
||||
**Repo:** `felhom-agent` · **Version:** 0.22.0 · **Pushed commit:** `4734d4a` · paired with
|
||||
`felhom-controller` v0.43.0 (the controller-side storage-management rebuild that needs this).
|
||||
## What this implements
|
||||
|
||||
## What shipped (one read-only field)
|
||||
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.
|
||||
|
||||
`internal/localapi/DiskInfo` gains `durable_id` (mapped directly from `StorageTarget.DurableID` — e.g.
|
||||
`"uuid:<fs-uuid>"` for usb/local-dir, `"path:…"`/`"store:…"` otherwise). That's the entire change.
|
||||
### 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`.
|
||||
|
||||
**Why it's necessary:** the de-privileged controller cannot read a device's filesystem UUID itself, yet
|
||||
`POST /disks/assign` mounts **strictly by fs UUID** (`EnsureMount` → `/dev/disk/by-uuid/<UUID>`). Without
|
||||
the UUID surfaced somewhere the controller can reach, the guided init/attach flows could not complete the
|
||||
mount. `GET /disks` already carried everything else (name/type/state/device/mount/class/data_bearing) but
|
||||
dropped the durable id. The controller strips the `uuid:` prefix to get the assign key.
|
||||
### 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").
|
||||
|
||||
**Security:** no new privilege, no subprocess, no behaviour change to `format`/`assign`/`eject` or the
|
||||
data-bearing signature gate. The agent still inspects the device itself at format time and refuses a
|
||||
data-bearing wipe with a `pending_op` (the controller surfaces the `felhom-opsign` command).
|
||||
### 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`).
|
||||
|
||||
## Validation (live, guest 9201)
|
||||
`GET /disks` (via the controller's `/api/disks` proxy) now returns `durable_id` per disk, e.g.
|
||||
`felhom-usb` → `/dev/sdb1`, `data_bearing:true`, `durable_id:uuid:277a2179-a764-4758-b840-9ea741517914`.
|
||||
The controller's guided init on `sdb` correctly drove the agent to a **data-bearing refusal** (HTTP 403 →
|
||||
`pending_op{op:storage_wipe, host_scope:demo-felhom-01, durable_id:byid:wwn-0x5000039ddb108568-part1}`),
|
||||
which the controller surfaced as the operator `felhom-opsign` command — the gate held end-to-end.
|
||||
## 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.
|
||||
|
||||
## Notes
|
||||
This pairs with the controller's storage-management rebuild (build-golden default bumped to controller
|
||||
0.43.0; golden rebaked). No other agent change.
|
||||
`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.
|
||||
|
||||
Reference in New Issue
Block a user