v0.32.0: self-serve decommission endpoint + intent-aware re-assert (B2a)

POST /disks/decommission mirrors eject (withGuest, user-data role gate) — no
operator signature, non-destructive (never formats): sets IntentDecommissioned,
prunes the GuestBindStore entry, unmounts. ReassertGuestBinds is now intent-aware
(skip non-enrolled) so a decommissioned-but-present drive never auto-rebinds on
agent restart — the load-bearing F9-reconnect fix. GuestBindStore.Remove added.
Operator-signed DecommissionExecutor + classify untouched. Non-hollow tests incl.
the intent-aware reassert companion (mutation-proven to fail on intent-blind code).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-14 19:55:17 +02:00
parent 8e6d00a57f
commit f43697c881
6 changed files with 397 additions and 49 deletions
+16 -15
View File
@@ -193,16 +193,16 @@ func NewServer(o Options) (*Server, error) {
cadence = defaultBackupCadence
}
s := &Server{
addr: o.ListenAddr,
cert: o.Cert,
guests: o.Guests,
backups: o.Backups,
store: o.Store,
storage: o.Storage,
tokens: o.Tokens,
cadence: cadence,
logger: o.Logger,
now: func() time.Time { return time.Now().UTC() },
addr: o.ListenAddr,
cert: o.Cert,
guests: o.Guests,
backups: o.Backups,
store: o.Store,
storage: o.Storage,
tokens: o.Tokens,
cadence: cadence,
logger: o.Logger,
now: func() time.Time { return time.Now().UTC() },
disks: o.Disks,
diskGate: o.DiskGate,
guestList: o.Guests2,
@@ -237,6 +237,7 @@ func (s *Server) Handler() http.Handler {
mux.HandleFunc("GET /disks", s.withGuest(s.handleDisks))
mux.HandleFunc("POST /disks/assign", s.withGuest(s.handleDiskAssign))
mux.HandleFunc("POST /disks/eject", s.withGuest(s.handleDiskEject))
mux.HandleFunc("POST /disks/decommission", s.withGuest(s.handleDiskDecommission))
mux.HandleFunc("POST /disks/format", s.withGuest(s.handleDiskFormat))
mux.HandleFunc("GET /disks/format/status", s.withGuest(s.handleDiskFormatStatus))
// Guest data-drive passthrough (slice 10 P2): bind an enrolled drive's felhom-data namespace in.
@@ -583,11 +584,11 @@ func (s *Server) handleBackupDue(w http.ResponseWriter, r *http.Request, vmid in
// BackupStatusResponse is GET /backup/status (slice 8B): the current/last job phase + the latest
// recorded backup. Phase is idle when no job has run this process lifetime.
type BackupStatusResponse struct {
VMID int `json:"vmid"`
Phase string `json:"phase"` // idle | running | done | failed
JobID string `json:"job_id,omitempty"`
Error string `json:"error,omitempty"`
Backup *hub.Backup `json:"backup,omitempty"` // latest recorded backup for this guest
VMID int `json:"vmid"`
Phase string `json:"phase"` // idle | running | done | failed
JobID string `json:"job_id,omitempty"`
Error string `json:"error,omitempty"`
Backup *hub.Backup `json:"backup,omitempty"` // latest recorded backup for this guest
}
func (s *Server) handleBackupStatus(w http.ResponseWriter, r *http.Request, vmid int) {