docs(spike): bake in locked fork decisions (B0 gate cleared)
1) keep operator-signed decommission; 2) Q4 both — intent-aware ReassertGuestBinds load-bearing/sufficient, GuestBindStore.Remove hygiene (re-commission re-Records via recordGuestBind, B2 to state); 3) data move = controller via rsync copy-verify-remove (not cp -a); 4) decommission role-gated unmount. Gate cleared; B1/B2 may proceed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -49,10 +49,12 @@ Both enrolled storage paths (`/mnt/felhom-usb`, `/mnt/sys_drive/felhom-data`) ar
|
||||
inside the controller container.
|
||||
|
||||
**Conclusion — chosen layer = CONTROLLER.** The controller already holds RW access to every enrolled drive
|
||||
under /mnt, so it can perform an app's data move (`cp -a /mnt/X/... → /mnt/Y/...`) **itself**, in-process,
|
||||
no agent round-trip for the bytes. The agent is needed only for the privileged side-effects that bracket the
|
||||
move: **unmount** (eject/decommission) and **intent** recording. This matches the existing app-data backup
|
||||
design (the controller already rsyncs between drives for Tier-2 off-drive copies).
|
||||
under /mnt, so it can perform an app's data move **itself**, in-process, no agent round-trip for the bytes.
|
||||
The agent is needed only for the privileged side-effects that bracket the move: **unmount**
|
||||
(eject/decommission) and **intent** recording. **The move primitive is `rsync`, not `cp -a`** (see locked
|
||||
decision #3): B1 needs copy→verify→remove that is idempotent/resumable — `rsync -a --checksum` (copy +
|
||||
verify) → confirm → only then remove the source; a crash mid-copy just re-runs. rsync is already the
|
||||
controller's cross-drive primitive (Tier-2 off-drive backup).
|
||||
|
||||
---
|
||||
|
||||
@@ -247,15 +249,34 @@ rendszerből" remains the deliberate hard-remove when they're truly done.
|
||||
|
||||
| Q | Finding | file:line | Recommendation |
|
||||
|---|---|---|---|
|
||||
| Q1 | Controller mount is `-v /mnt:/mnt:rslave`, de-privileged; `rslave` is RW for content. **Live:** controller wrote+deleted markers under both enrolled /mnt paths. | `build-golden.sh:184` (`:173-178`); live 9201 | **Data move = CONTROLLER** (in-process `cp -a` between /mnt drives); agent only for unmount + intent. |
|
||||
| Q1 | Controller mount is `-v /mnt:/mnt:rslave`, de-privileged; `rslave` is RW for content. **Live:** controller wrote+deleted markers under both enrolled /mnt paths. | `build-golden.sh:184` (`:173-178`); live 9201 | **Data move = CONTROLLER** (in-process **`rsync -a --checksum`**, copy→verify→remove, between /mnt drives); agent only for unmount + intent. |
|
||||
| Q2 | Migrate button is a vestigial greyed `<span>`, no handler/route/JS; `SetDecommissioned` never called. | `settings.html:337`; `settings.go:821` | Build the whole round-trip: data-move executor, HDD_PATH rewrite+redeploy, `POST /api/storage/migrate`, source decommission, registry mark. |
|
||||
| Q3 | Decommission today = operator-signed only (Destructive in `Classify`); executor only sets intent; controller has no endpoint/agentapi method. Eject is the self-serve precedent. | `classify.go:36,103-107`; `decommission.go:54-79`; `main.go:437`; eject `server.go:239`+`disks.go:209` | Add self-serve `POST /disks/decommission` (withGuest, user-data role-gate, durable-id resolved server-side, intent+bind-remove+unmount, **no wipe**). Hub can't reach local API (8443 LAN, per-guest token, outbound-only); compromised controller gains nothing beyond eject/wipe. **Keep** operator-signed path for absent-drive/DR. |
|
||||
| Q4 | `GuestBindStore` has no `Remove`; `ReassertGuestBinds` ignores intent; watchdog gates host-mount to `enrolled`. **Live:** an `ejected` drive still sits in `guest-binds.json`. Gap: a decommissioned-but-still-mounted drive would be re-bound on agent restart. | `guestbindstore.go` (no Remove); `disks.go:643,649-654,673-677`; `watchdog.go:174-178`; live intent/bind state | **Both:** intent-aware `ReassertGuestBinds` (skip non-`enrolled`) [must-have] + decommission endpoint `Unmount` + new `GuestBindStore.Remove` [hygiene/DiD]. |
|
||||
| Q4 | `GuestBindStore` has no `Remove`; `ReassertGuestBinds` ignores intent; watchdog gates host-mount to `enrolled`. **Live:** an `ejected` drive still sits in `guest-binds.json`. Gap: a decommissioned-but-still-mounted drive would be re-bound on agent restart. | `guestbindstore.go` (no Remove); `disks.go:643,649-654,673-677`; `watchdog.go:174-178`; live intent/bind state | **Both:** intent-aware `ReassertGuestBinds` (skip non-`enrolled`) = **load-bearing correctness fix, sufficient alone** (covers self-serve + signed paths) + `GuestBindStore.Remove` = hygiene/DiD (re-commission re-`Record`s via `recordGuestBind` — B2 must state). |
|
||||
| Q5 | `SetDecommissioned` keeps the entry as a soft marker (Decommissioned=true, Schedulable/IsDefault=false, MigratedTo); UI renders it. | `settings.go:821-839`; `handlers.go:855-857`; `settings.html:242-256` | **Keep soft marker** — it blocks A1-additive resurrection (skip-by-presence) + preserves audit. Hard-remove would risk re-registration if any reference lingers. |
|
||||
|
||||
## Forks needing the product owner's call (GATE)
|
||||
## Decisions — LOCKED by product owner (2026-06-14). Gate cleared; B1/B2 may proceed.
|
||||
|
||||
1. **Operator-signed decommission** — keep as DR/absent-drive path (**recommended**) vs remove. → *recommend keep.*
|
||||
2. **Q4 fix shape** — intent-aware `ReassertGuestBinds` only, vs `GuestBindStore.Remove` only, vs **both** (**recommended**). → *recommend both.*
|
||||
3. **Data-move layer** — controller-side `cp -a` (**recommended**, proven live) vs agent-side mover. → *recommend controller.*
|
||||
4. **Decommission = unmount?** — endpoint unmounts the drive (mirror eject, so it's physically removable; **recommended**) vs leaves it host-mounted. → *recommend unmount.*
|
||||
1. **Operator-signed decommission — KEEP.** Retained as the DR / absent-drive path alongside the new
|
||||
self-serve endpoint. (Q3.)
|
||||
|
||||
2. **Q4 fix — BOTH, with a clear load-bearing split:**
|
||||
- **Load-bearing (correctness):** make `ReassertGuestBinds` **intent-aware** — skip any durable-id whose
|
||||
intent is not `enrolled`. **This alone is sufficient** for the invariant: it covers BOTH the self-serve
|
||||
decommission AND the operator-signed (absent-drive) path, because both routes set
|
||||
`IntentDecommissioned` and the guard keys on intent, not on who set it.
|
||||
- **Hygiene (not correctness):** `GuestBindStore.Remove` keeps `guest-binds.json` honest for *present*
|
||||
drives (no lingering records like the live `ejected` one). It is defense-in-depth, not the guard.
|
||||
- **Nuance the B2 spec MUST state:** if `Remove` is implemented, **re-commission must re-`Record`** the
|
||||
bind. This is already covered — `recordGuestBind` runs on every enroll/guest-attach
|
||||
(`disks.go:290,309`) — but B2 must call it out explicitly so a future change doesn't break it.
|
||||
|
||||
3. **Data-move layer — CONTROLLER, via `rsync` (NOT `cp -a`).** This is a **B1 implementation choice, not a
|
||||
fork.** B1 requires **copy → verify → remove**, idempotent/resumable; `cp -a` gives neither resumability
|
||||
nor verification. `rsync` is already an existing controller primitive (cross-drive Tier-2 backup, per
|
||||
CONTEXT). B1 contract: `rsync -a --checksum` (copy + verify) → confirm success → **only then** remove the
|
||||
source. A crash mid-copy simply re-runs (idempotent). [Supersedes the `cp -a` wording above in Q1 / the
|
||||
summary table — same layer, better primitive.]
|
||||
|
||||
4. **Decommission UNMOUNTS — agreed.** The endpoint performs a **role-gated** unmount (mirror eject) so the
|
||||
drive is physically removable. (Q3 / Q4 belt.)
|
||||
|
||||
Reference in New Issue
Block a user