diff --git a/REPORT.md b/REPORT.md index afe81a3..5242542 100644 --- a/REPORT.md +++ b/REPORT.md @@ -4,6 +4,38 @@ --- +# REPORT — SPIKE: self-serve raw-drive plug-and-enroll (GATE) (2026-06-15) + +## Type +GATE spike — report-only + one live throwaway experiment on `/dev/sdc`. **No production code, no version bump, no service restart.** Build spec is gated on the fork picks below. +Full doc: [documentation/audits/SPIKE-raw-drive-enroll-2026-06-15.md](documentation/audits/SPIKE-raw-drive-enroll-2026-06-15.md). + +## One-screen summary +The plug-and-enroll flow is mostly assemblable from primitives that already exist. The genuine **net-new** is (a) a **raw-disk discovery source** — the agent's `Observe` and localapi `GET /disks` are *both* built only from PVE storages (`ListStorage`/`NodeStorage`), so a disk that isn't already a PVE storage is invisible — and (b) a **create-storage call** (the agent creates no PVE storages today). The safety classifier **already exists** (`RoleForRawDevice`, `SystemDisks`, `RoleForStorage` in `internal/storage/role.go`) and correctly protects the live system disk, but needs **two hardening additions** before it can drive a *discovery* list. + +Validated live on `felhom-pve`: +- **sda** (ESP + sole LVM PV `pve` VG holding root, local-lvm, **guest 9201's disks**) → PROTECTED (system); pinned via `/boot/efi`=`sda2`. +- **sdb** (`felhom-usb`, ext4 `/mnt/felhom-usb`) → PROTECTED (enrolled data). +- **sdc** (removable junk iso9660+EFI+hfsplus) → OFFERABLE. +- **Premise correction:** `felhom-pbs` is a **remote** `pbs:` storage (server 192.168.0.180) — **no local PBS disk** exists; it's protected at the storage tier, not a raw-disk tier. + +**Live FORMAT experiment (serial-fenced to sdc):** `pvesh create …/disks/directory --name felhom-flash --device /dev/sdc --filesystem ext4 --add_storage 1` did partition+mkfs+mount+register in one call (fs-UUID mount unit, `is_mountpoint 1`, `/mnt/pve/felhom-flash`). It **immediately appeared in the controller's agent-drive view as user-data / data-bearing / "Nem regisztrált" with a one-click `Regisztrálás`** — no agent restart. sdc left as the clean enrolled `felhom-flash` B3 migration target. + +## The two safety-gate hardening gaps (must close) +1. **Whole-disk-granularity enrolled-subtraction** — `deviceRole` matches a registered storage by exact `BackingDevice==device`; a discovery flow asks about whole disk `/dev/sdb` while `felhom-usb` backs `/dev/sdb1`, so the match fails and sdb would be offered for format. Compare at whole-disk granularity. +2. **LVM-PV / guest-image membership** — `SystemDisks` resolves the system disk from the mount table only; a non-boot disk holding `local-lvm` PVs / guest images would be missed. Add a `disks/lvm` PV-membership check. + +## Fork list (operator decides) +1. Discovery source: `disks/list`+`disks/lvm`+mount-table+Observe (recommended) ± `lsblk` for display. +2. Safety signals: mount-table-only (ships, but can offer a non-boot LVM disk) vs **hardened** (the 2 checks above — recommended). +3. Mount path: PVE default `/mnt/pve/` vs **`/mnt/`** via `EnsureMount`+`pvesm add dir` (recommended, matches felhom-usb). +4. Format-vs-enroll-as-is: single supported-fs partition → as-is; else format; format on ambiguity. +5. Endpoints: `GET /disks/raw` + `POST /disks/init` + `POST /disks/attach-existing`, `withGuest`-scoped, agent re-derives the gate. +6. v1 scope: **format-only first** (proven live) + enroll-as-is fast-follow (recommended) vs both in v1. +7. Normalize `content` (vs PVE all-types default); typed proxmox-client create method (vs host-side `pvesh`). + +--- + # REPORT — Slice 10D (hub half): DR capstone — recovery mode + re-enroll + directive serving (hub v0.11.0) (2026-06-10) ## Type diff --git a/documentation/audits/SPIKE-raw-drive-enroll-2026-06-15.md b/documentation/audits/SPIKE-raw-drive-enroll-2026-06-15.md new file mode 100644 index 0000000..701bf79 --- /dev/null +++ b/documentation/audits/SPIKE-raw-drive-enroll-2026-06-15.md @@ -0,0 +1,270 @@ +# SPIKE — Self-serve raw-drive plug-and-enroll + +**Date:** 2026-06-15 +**Type:** GATE spike — report-only + one live throwaway experiment on `/dev/sdc`. +**Host:** `felhom-pve` (node `demo-felhom`), PVE 9, guest 9201. +**Status:** findings complete; live FORMAT path proven end-to-end; sdc left as a clean enrolled `felhom-flash` PVE storage (B3 migration target). +**No production code, no version bump.** The build spec is gated on the fork picks at the end. + +> **Goal (operator's vision):** a customer plugs in a drive and enrolls it from the controller UI with **no operator/Proxmox tinkering**. Flow: agent detects the raw drive → decides usable-as-is vs needs-format → `storage/init` shows it (model/size/serial, partitions+filesystems, recommendation) → customer chooses → controller tells agent → agent creates a PVE dir storage (formatting only if chosen) → binds the `felhom-data` namespace into the guest (existing `AttachBind`) → drive usable. + +--- + +## One-screen summary + +The flow is **mostly assemblable from primitives that already exist**; the genuine net-new is a **raw-disk discovery source** (the agent is blind to any disk that isn't already a PVE storage) and a **create-storage call** (the agent never creates PVE storages today). The security classifier is **already written** (`RoleForRawDevice`) and correctly protects the system disk live — but it needs **two hardening additions** (LVM-PV membership + whole-disk-granularity "already-enrolled" subtraction) before it can drive a *discovery* list. + +Proven live on this host: + +| Disk | What it is | disks/list `used` | Classifies | Why | +|---|---|---|---|---| +| **sda** SSD 512G | boot/root: ESP + LVM VG `pve` (root, swap, local-lvm thinpool, **guest 9201's own disks**) | `BIOS boot` | **PROTECTED (system)** | only LVM PV (`/dev/sda3`); `/boot/efi`=`sda2` pins it | +| **sdb** HDD 1T | enrolled data drive `felhom-usb`, ext4 `/mnt/felhom-usb` | `ext4` | **PROTECTED (enrolled data)** | backs registered storage `felhom-usb` | +| **sdc** USB 128G | throwaway flash, junk iso9660+EFI+hfsplus | `iso9660`→(after)`ext4` | **OFFERABLE** | removable, backs nothing, not a system disk | + +The live FORMAT experiment (Q6) turned sdc into a registered `felhom-flash` dir storage in **one PVE API call**, and it **immediately appeared in the controller's agent-drive view as user-data / data-bearing / "Nem regisztrált" with a `Regisztrálás` button** — the existing enroll path picks it up with no agent restart. + +**Spec premise correction:** there is **no local PBS disk** on this host. `felhom-pbs` is a *remote* `pbs:` storage (`server 192.168.0.180`); it has no backing block device. The "PBS disk classifies PROTECTED" assertion is satisfied at the *storage* tier (`RoleForStorage` → `RoleBackup`), not at any raw-disk tier. + +--- + +## Q1 — Current discovery (the gap), confirmed in source + +`internal/storage/observe.go:131,139` — `Observe` builds its entire view from exactly two PVE calls and nothing else: + +```go +cfg, _ := o.api.ListStorage(ctx) // observe.go:131 → GET /storage +node, _ := o.api.NodeStorage(ctx) // observe.go:139 → GET /nodes/{node}/storage +``` + +The proxmox client (`internal/proxmox/query.go`) has **no raw-disk method**. Full read surface: `Version, Nodes, NodeStatus, ListLXC, GuestStatus, GuestConfig, ListStorage(:58), NodeStorage(:64), StorageContent, LatestBackupVolID`. None call `/nodes/{node}/disks/*`. + +Critically, the localapi disk endpoint is **also** sourced from `Observe`, not from any block-device scan: + +```go +// internal/localapi/disks.go:130 (handleDisks, GET /disks) +targets, err := s.storage.Observe(r.Context()) +``` + +**Conclusion:** a raw `/dev/sdc` that is **not yet a PVE storage is invisible to both the hub-facing `Observe` and the localapi `GET /disks`.** A drive must *already* be a registered PVE storage before anything in the agent can see it. That is precisely the missing primitive. (`host.Removable`, observe.go:182, only annotates a storage that is *already* in the view — it does not discover.) + +**What's missing, exactly:** +1. A proxmox-client (or host-level) method to enumerate raw block devices. +2. A discovery path in the agent that lists *candidate* disks (not-yet-storages) and runs the safety classifier over them. +3. A create-storage primitive (Q5). + +--- + +## Q2 — Raw-disk discovery API, validated live + +`pvesh get /nodes/demo-felhom/disks/list --output-format json` (abridged): + +```json +[ + {"devpath":"/dev/sda","type":"ssd","size":512110190592,"model":"AirDisk_512GB_SSD","serial":"QDF922W009654S30EX","used":"BIOS boot","gpt":1,"health":"PASSED","osdid":-1,"by_id_link":"/dev/disk/by-id/ata-AirDisk_512GB_SSD_QDF922W009654S30EX"}, + {"devpath":"/dev/sdb","type":"hdd","size":1000204886016,"model":"TOSHIBA_MQ04ABF100","serial":"65NOP3HDT","used":"ext4","gpt":1,"rpm":"5400"}, + {"devpath":"/dev/sdc","type":"usb","size":128320801792,"model":"Flash_Drive","serial":"0375123010001586","used":"iso9660","gpt":1,"health":"UNKNOWN"} +] +``` + +`disks/list` gives: `devpath, by_id_link, type (ssd/hdd/usb), size, model, serial, wwn, health, gpt, osdid, used`. The `used` enum is the single most useful built-in signal but it is **insufficient alone**: +- It distinguishes sda (`BIOS boot`) but reports both sdb (enrolled data) and sdc (junk) as just a filesystem type (`ext4` / `iso9660`). After enrollment sdc *also* reads `ext4` — identical to the protected sdb. `used` cannot separate "offerable" from "already-enrolled data". +- It does **not** report removable, nor which disk holds LVM PVs / the guest's image / the root mount. + +Companion endpoints: +- `pvesh get /nodes/demo-felhom/disks/lvm` → VG `pve` on `/dev/sda3`, `lvcount 9`. **Authoritative LVM-PV-membership signal.** +- `pvesh get /nodes/demo-felhom/disks/directory` → `[]` **before** the experiment; **after**, lists only `felhom-flash`. **It does NOT list `felhom-usb`** — PVE's directory tracking only reflects storages *it* created via the `disks/directory` API, not the agent's own systemd mounts. So `disks/directory` is **not** a reliable enumerator of dir storages. + +**Recommendation — discovery source:** use **`disks/list` as the candidate enumerator**, cross-referenced with **`disks/lvm`** (PV membership) and the agent's **existing host-mount read** (`SystemDisks`, which already parses the mount table) and the **`Observe` storage view** (to subtract already-enrolled disks). The removable flag is already available to the agent via `/sys/block//removable` (`hostread.go`, `ProcHostReader.Removable`) and via `lsblk RM`; PVE's `disks/list` does not expose it, so keep reading it host-side. A small `lsblk -J` is still worthwhile to get serial/by-uuid/child-partition detail per disk for the `storage/init` display, but the *safety* decision is driven by `disks/list` + `disks/lvm` + mount-table + storage-view, not by labels. + +--- + +## Q3 — The safety gate (security-critical core) + +**Good news: the classifier primitive already exists and is correct for the system disk.** `internal/storage/role.go`: + +- `SystemDisks(host)` (role.go:46) — resolves the OS whole-disks from the **mount table** (`/`, `/boot`, `/boot/efi` → whole disk), root-free. On this host `/` is on `pve-root` (device-mapper, untraceable to a raw disk), but **`/boot/efi` = `sda2` pins `sda`** exactly as the code comment intends. +- `RoleForRawDevice(device, sysDisks, sysKnown)` (role.go:133) — system-vs-user-data for a raw `/dev` path; **defaults to system on ambiguity**. +- `RoleForStorage(...)` (role.go:105) — tiers a *registered* storage; PBS→backup, USB/local-dir-on-non-system-disk→user-data, everything else→system. +- `deviceRole(...)` (disks.go:836) — wraps both: prefers a matching storage target's role, else `RoleForRawDevice`. + +**Validated live** (reproducing the agent's logic; `sysDisks={/dev/sda}` via `/boot/efi`=`sda2`, `sysKnown=true`): +- `RoleForRawDevice("/dev/sda")` → `isSystemBacked` → `sysDisks["/dev/sda"]` = **system** ✓ PROTECTED. +- `RoleForRawDevice("/dev/sdc")` → not in set → **user-data** ✓ offerable. +- `felhom-pbs` (`RoleForStorage` PBS) → **backup** ✓ PROTECTED (storage tier; no device). + +Confirmed at the UI: the controller's agent-drive view renders `local`=🔒system, `local-lvm`=🔒system, `felhom-pbs`=🔒backup, `felhom-usb`/`felhom-flash`=user-data. + +### Two hardening gaps that MUST close before this drives a *discovery* list + +1. **Whole-disk-granularity "already-enrolled" subtraction.** `deviceRole` matches a registered storage by **exact** `t.BackingDevice == device` (disks.go:840). The format endpoint passes the exact backing device, so this works *there*. But a discovery flow enumerates **whole disks** (`/dev/sdb`), while `felhom-usb`'s backing device is the **partition** `/dev/sdb1`. The exact match fails → fall-through to `RoleForRawDevice("/dev/sdb")` → **user-data → would be offered for format**, even though it holds live customer data. **Fix:** the offerable filter must subtract any disk whose **whole-disk** equals `wholeDiskOf(t.BackingDevice)` for *any* `Observe` target — compare at whole-disk granularity, not device-path equality. + +2. **LVM-PV / guest-image membership.** `SystemDisks` resolves the system disk via the **mount table only**. On this host that catches `sda` because the ESP is on it. But a disk that holds `local-lvm` PVs or guest disk images **without** carrying `/boot`/`/boot/efi` would be **missed** (→ misclassified user-data). **Fix:** add a positive PV-membership check from `disks/lvm` (any disk whose partition is a PV of the `local-lvm` VG, or the VG that backs any guest's `rootfs`/`mp`), folding it into the protected set alongside `SystemDisks`. + +### Proposed offerable predicate (the gate) + +A whole disk `D` is **OFFERABLE** iff **all** of: +- `RoleForRawDevice(D) == user-data` (not the OS/boot disk; fail-safe system on ambiguity), **and** +- `D` is **not** in the LVM-PV/guest-image protected set (`disks/lvm` + guest-config backing), **and** +- `wholeDiskOf(D)` backs **no** registered `Observe` target (subtract already-enrolled, whole-disk granularity), **and** +- `D` is **not** a local PBS datastore disk (here: N/A — PBS is remote). + +Everything else (incl. on **any** ambiguity) → **PROTECTED, never listed for format**. This extends the existing *"mkfs-not-on-data-bearing-device"* discipline (`disks.go:526`, `InspectDevice` blkid+lsblk) to *"mkfs-not-on-system-disk."* + +### Non-hollow test shape + +A test that **FAILS if the system disk is ever offerable**, fed a realistic topology fixture (sda=ESP+LVM-PV+guest images; sdb=ext4 backing a registered `felhom-usb`; sdc=removable junk): + +``` +TestOfferableDisks_NeverOffersProtected: + offer := OfferableDisks(disksList, lvmView, observeTargets, hostMounts) + assert sda NOT in offer // ESP + sole LVM PV → system + assert sdb NOT in offer // backs registered felhom-usb (whole-disk subtraction) + assert sdc IN offer // removable, backs nothing, not system + // adversarial: relabel sdc's disks/list "used" to "BIOS boot" (lying label) + // → still offerable iff state says so; and relabel sda "used"="" (lying blank) + // → MUST still be protected via ESP/LVM-PV state, not the label. + // mutation guard: drop the LVM-PV check → test MUST fail on a PV-only protected disk fixture. +``` + +The mutation clause (removing either hardening check makes the test red) is what keeps it non-hollow. + +--- + +## Q4 — Usable-as-is vs needs-format + +Decision driven by the agent's **existing** `InspectDevice` (`internal/storage/hostops.go`, blkid `-p -o export` + `lsblk -J`) which already yields `HasFilesystem/FSType/HasPartitionTable/HasPartitions/Mounted`: + +- **Enroll-as-is** iff the disk presents **exactly one mountable, supported data filesystem** we can own — i.e. a single partition (or whole-disk fs) with `FSType ∈ {ext4, xfs}` (the fs types the agent's mount machinery + PVE dir storage support), not currently mounted by the host/another storage, and not the system/protected set. +- **Needs-format** iff: no filesystem, an unsupported fs (the live sdc had **iso9660** + a **vfat** ESP + **hfsplus** — none ownable), a partition table with **multiple data partitions** (ambiguous which to adopt — sdc had 4), or any signature we will not silently adopt. +- **On ambiguity → recommend format** (never silently adopt an unknown layout), mirroring the destructive-on-ambiguity invariant inverted for safety: we don't *destroy* on ambiguity, we *refuse to enroll-as-is* and require an explicit format choice. + +**What `storage/init` should display** per candidate: +- Identity: model, size, serial, bus type (ssd/hdd/usb), removable badge — all from `disks/list` + `lsblk`. +- Layout: each partition with size + detected fs (`lsblk -J -o NAME,FSTYPE,SIZE,MOUNTPOINT`), plus the data-bearing verdict + reason from `InspectDevice`. +- Recommendation: **"Enroll partition X as-is (ext4, N GB)"** when the as-is predicate holds, else **"Format required — current contents will be erased"** with the detected junk listed (e.g. "iso9660 + EFI + hfsplus"). + +--- + +## Q5 — PVE-storage creation via the agent — BOTH paths + +### FORMAT path — validated live (one call does everything) + +``` +pvesh create /nodes/demo-felhom/disks/directory \ + --name felhom-flash --device /dev/sdc --filesystem ext4 --add_storage 1 +``` + +The single call ran synchronously and performed, in order: +``` +# /sbin/sgdisk -n1 -t1:8300 /dev/sdc → partition sdc1 +# /sbin/mkfs -t ext4 /dev/sdc1 → fs UUID 81a26531-62d8-408d-812f-a178b1d35310 +# /sbin/blkid /dev/sdc1 -o export +Created symlink …/mnt-pve-felhom\x2dflash.mount → systemd mount unit, enabled +add_storage 1 → wrote storage.cfg +``` + +Resulting `storage.cfg` entry: +``` +dir: felhom-flash + path /mnt/pve/felhom-flash + content rootdir,images,backup,iso,snippets,vztmpl + is_mountpoint 1 + nodes demo-felhom +``` + +Resulting systemd unit (`mnt-pve-felhom\x2dflash.mount`): +``` +[Mount] +What=/dev/disk/by-uuid/81a26531-62d8-408d-812f-a178b1d35310 +Where=/mnt/pve/felhom-flash +Type=ext4 +Options=defaults +``` + +**Conventions check:** +- `is_mountpoint 1` ✓ (locked convention). +- Mount unit keyed by **fs-UUID** (`/dev/disk/by-uuid/…`) ✓ — matches the agent's own `mountunit.go` (`What=/dev/disk/by-uuid/`). +- Durable-id will derive **`uuid:81a26531-…`** from the storage's own fs UUID ✓ (`durableid.go:deriveDurableID`, USB/local-dir → `uuid:`). +- **Mount path = `/mnt/pve/felhom-flash`** ✗ — diverges from the felhom `/mnt/` convention (`felhom-usb` is `/mnt/felhom-usb`). PVE's `disks/directory` API has **no option to override the path**; it is always `/mnt/pve/`. → **FORK.** +- **`content` = all types** ✗ — `felhom-usb` is `content backup`. The data drive that holds the bind-mounted `felhom-data` namespace doesn't *use* PVE content types, but the entry should be normalized for consistency. → minor fork. + +### ENROLL-AS-IS path — validated by the living reference + +`felhom-usb` itself **is** the enroll-as-is proof: it is a `dir` storage with `is_mountpoint 1` at `/mnt/felhom-usb`, **absent from `disks/directory`** → it was created by **mounting an existing fs + `pvesm add dir`**, *not* by PVE's formatting `disks/directory` API. The agent already owns both halves: +- `EnsureMount` / `renderMountUnit` (`internal/storage/mountunit.go`, `HostOps.EnsureMount`) — stages a by-UUID systemd `.mount` unit at `/mnt/`, `daemon-reload`, `enable --now`. **No mkfs.** +- the missing half is the `pvesm add dir --path /mnt/ --is_mountpoint 1 --content …` call (the agent does not create PVE storages today — see Q5 net-new). + +So enroll-as-is = `EnsureMount(existing-fs-UUID, /mnt/)` + `add-dir-storage` — both primitives understood, one not-yet-wired. + +### Both paths feed the existing enroll flow + +`Observe` iterates `NodeStorage`, so **either** path's resulting storage is picked up with **no agent restart** (proven for the format path below). The customer-facing enroll (`AttachBind`, disks.go `handleDiskGuestAttach`) then binds `/felhom-data` into the guest. + +--- + +## Q6 — Live experiment (end-to-end, sets up B3) + +Every call run against `felhom-pve`, all guarded by a **serial-identity fence** (`/dev/sdc` serial must equal `0375123010001586`, must be removable `RM=1`, must have no mountpoint — abort otherwise) so sda/sdb could never be touched. + +1. **Guard + wipe.** Guard passed (serial matched, RM=1). `wipefs -a /dev/sdc` erased the iso9660 + 2×GPT + PMBR signatures; `sgdisk --zap-all` cleared the GPT. After: `lsblk` shows bare `sdc` (no fs), and `disks/list` reports `sdc used=None gpt=0` — **PVE's `used=None` is the "genuinely blank" signal.** +2. **Create (format path).** The `pvesh create …/disks/directory` call above — partition + mkfs ext4 + systemd mount + `add_storage`, one call, synchronous success (`UPID:…:dircreate:felhom-flash:…`). +3. **Verify storage.** `storage.cfg` entry written (above); `findmnt /mnt/pve/felhom-flash` → `/dev/sdc1 ext4 rw`; fs UUID `81a26531-…`; `disks/directory` now lists felhom-flash (by-uuid device); `disks/list` sdc now `used=ext4`. +4. **Verify Observe.** `pvesh get /nodes/demo-felhom/storage` → `felhom-flash dir active=1 total=125704151040` (~117 GiB) — the agent's `Observe` source now contains it. +5. **Verify enrollable in controller.** The controller settings page → *"Meghajtók (ügynök nézet)"* now shows: + > **felhom-flash** — `usb · /dev/sdc1 · /mnt/pve/felhom-flash` · **Felhasználói adat** · **Adatot tartalmaz** · **Nem regisztrált** · buttons **Regisztrálás** / Leválasztás / Törlés… + + i.e. classified **user-data**, data-bearing detected, **not yet enrolled, with a one-click `Regisztrálás` (enroll = `AttachBind`)**. End-to-end mechanism proven. + +**State left for B3:** sdc is a **clean, registered `felhom-flash` PVE storage** (ext4, mounted `/mnt/pve/felhom-flash`, is_mountpoint, in the agent view, classified user-data). It is **PVE-enrolled and "ready-to-enroll-into-guest"** — the final `AttachBind` + guest reboot was intentionally **not** run to avoid disrupting live guest 9201; that is the one remaining step a real customer enroll performs. This is the B3 migration target. + +--- + +## Q7 — Wiring map (net-new vs reused) + +### Reused (already exists, no change) +| Capability | Where | +|---|---| +| System-disk resolution (mount table → whole disk) | `storage/role.go:46 SystemDisks` | +| Raw-device role classifier (system vs user-data, fail-safe) | `storage/role.go:133 RoleForRawDevice` | +| Registered-storage role tiering (PBS=backup, etc.) | `storage/role.go:105 RoleForStorage` | +| Data-bearing inspection (blkid + lsblk) | `storage/hostops.go InspectDevice` + `disks.go:526` gate | +| Mount existing fs (by-UUID systemd unit at `/mnt/`) | `storage/mountunit.go`, `HostOps.EnsureMount` | +| Storage view the controller consumes | `storage/observe.go Observe` (iterates ListStorage/NodeStorage) | +| Bind `felhom-data` namespace into guest + reboot | `localapi/guestbind.go:48 AttachBind` | +| Enroll/assign/eject/decommission endpoints | `localapi/disks.go` (`/disks`, `/disks/assign`, `/disks/guest-attach`, …) | +| Durable-id (uuid:) derivation | `storage/durableid.go deriveDurableID` | + +### Net-new +| Capability | Lands in | Note | +|---|---|---| +| Raw-disk enumeration (`disks/list`, `disks/lvm`) | `internal/proxmox` (new `ListDisks`, `ListDisksLVM`) | the client has no `/disks/*` method today | +| Whole-disk **wipe** (`wipefs`/`sgdisk`) | `storage.HostOps` (new) | gated by the Q3 offerable predicate | +| **Create dir storage** — format path (`disks/directory` create) | `internal/proxmox` (new `CreateDirectoryStorage`) **or** a host-side `pvesh` call in `HostOps` | one call = partition+mkfs+mount+add_storage | +| Create dir storage — **enroll-as-is** (`pvesm add dir … --is_mountpoint 1`) after `EnsureMount` | `storage.HostOps` (new `AddDirStorage`) | the agent creates **no** PVE storages today | +| Raw-disk **discovery + Q3 offerable filter** (combine disks/list + disks/lvm + SystemDisks + Observe subtraction, whole-disk granularity) | `internal/storage` (new `OfferableDisks`) + a new localapi endpoint e.g. `GET /disks/raw` | the security core; needs the two Q3 hardening checks | +| New localapi endpoints: list-raw, init (format), attach-as-is | `internal/localapi/disks.go` (new handlers) | mirror the existing `/disks/*` handler shape + `withGuest` scoping | +| Controller `storage/init` UI: raw-disk list, identity/layout/recommendation, format-vs-enroll choice | controller (felhom-controller) | the *"🔧 Új meghajtó inicializálása"* / *"🔗 Meglévő meghajtó csatolása"* buttons already exist on the settings page as entry points | + +### The controller-decision → agent-task → done handshake +Lives exactly where the existing disk ops live: the controller POSTs the customer's choice to a **new localapi endpoint** (init/format or attach-as-is), `withGuest`-scoped + TLS-pinned, same as `POST /disks/assign` / `POST /disks/guest-attach` today. The agent **re-derives** the safety verdict from device state (never trusting the controller's claim — same discipline as `handleDiskFormat`), executes, and the result surfaces back through `Observe` → the controller's agent-drive view (proven live). No new transport, no hub round-trip: this is host-agent-local, exactly like the current enroll path. Final guest usability is the existing `AttachBind` + reboot. + +--- + +## FORK LIST (operator decides before the build spec) + +1. **Discovery source.** `disks/list` + `disks/lvm` + agent mount-table + Observe subtraction (recommended, no extra shell-out for the *safety* decision) — vs additionally shelling `lsblk -J` for richer per-disk display. *Recommendation: both — disks/list+lvm for the safety gate, lsblk for the init-screen detail.* +2. **Safety-gate signals — ship vs hardened.** Mount-table `SystemDisks` only (works on this host via the ESP) — vs **+LVM-PV membership +guest-image backing +whole-disk-granularity enrolled-subtraction** (required to be robust on a host where the system isn't all on one ESP-bearing disk). *Recommendation: hardened — the two Q3 checks are non-negotiable for a customer-facing format gate; the mount-table-only version can offer a non-boot LVM disk.* +3. **Mount-path convention.** Accept PVE default `/mnt/pve/` (free with `disks/directory`, but leaks PVE internals into the guest bind path) — vs roll-our-own `/mnt/` via `EnsureMount` + `pvesm add dir` (matches `felhom-usb`, reuses agent machinery, but re-implements the format steps). *Recommendation: `/mnt/` for consistency with the existing enrolled drive and a clean in-guest bind path.* +4. **Format-vs-enroll-as-is rules.** Single supported-fs partition → enroll-as-is; everything else (no fs / unsupported / multi-partition / ambiguous) → format. *Recommendation: as stated; format on any ambiguity, never silently adopt.* +5. **Endpoint shapes.** New `GET /disks/raw` (offerable list) + `POST /disks/init` (format path) + `POST /disks/attach-existing` (enroll-as-is), all `withGuest`-scoped, agent re-derives the gate. *Recommendation: as stated, mirroring `/disks/assign`.* +6. **v1 scope.** **Format-only first** (the path proven live this spike), enroll-as-is in v1.1 — vs both in v1. *Recommendation: format-only v1 (covers the plug-in-a-blank/junk-drive case, which is the common one), enroll-as-is fast-follow (the `felhom-usb`-style reuse-an-existing-ext4 case).* +7. **Content normalization.** Set `content backup` (or an explicit minimal set) on creation rather than PVE's all-types default. *Recommendation: yes, normalize.* +8. **`content`/storage create transport.** Typed proxmox-client method (`CreateDirectoryStorage`) vs host-side `pvesh` in `HostOps`. *Recommendation: typed client method, consistent with the rest of `internal/proxmox`.* + +--- + +## Live cleanup state +- `sdc` → registered PVE storage **`felhom-flash`** (ext4, `/mnt/pve/felhom-flash`, is_mountpoint, fs-UUID `81a26531-62d8-408d-812f-a178b1d35310`), classified user-data, **not** guest-bound. Intentionally left as the B3 migration target. +- `sda`, `sdb` untouched (serial-fenced throughout). +- No agent/controller code changed; no version bump; no service restarted.