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
+27 -27
View File
@@ -35,38 +35,38 @@ import (
applog "gitea.dooplex.hu/admin/felhom-agent/internal/log"
"gitea.dooplex.hu/admin/felhom-agent/internal/pbs"
"gitea.dooplex.hu/admin/felhom-agent/internal/provision"
"gitea.dooplex.hu/admin/felhom-agent/internal/signedjobs"
"gitea.dooplex.hu/admin/felhom-agent/internal/proxmox"
"gitea.dooplex.hu/admin/felhom-agent/internal/reconcile"
"gitea.dooplex.hu/admin/felhom-agent/internal/signedjobs"
"gitea.dooplex.hu/admin/felhom-agent/internal/storage"
)
// version is the agent version. Overridable at build time with
// -ldflags "-X main.version=<v>"; defaults to the in-repo CHANGELOG version.
var version = "0.31.0"
var version = "0.32.0"
func main() {
var (
cfgPath string
selftest selftestFlag
vmid int
watch time.Duration
archive string
mode string
hostname string
keep bool
rootfsGrow int
dataVolGrow int
dataVolMount string
pbsStorage string
paperkey bool
offline bool
upload bool
custID string
custDomain string
custName string
custEmail string
hubPassword string
cfgPath string
selftest selftestFlag
vmid int
watch time.Duration
archive string
mode string
hostname string
keep bool
rootfsGrow int
dataVolGrow int
dataVolMount string
pbsStorage string
paperkey bool
offline bool
upload bool
custID string
custDomain string
custName string
custEmail string
hubPassword string
blobPath string
expectedFP string
keyDest string
@@ -490,7 +490,7 @@ func runDaemon(cfg config.Config, logger *slog.Logger) int {
}
err = <-errc
stop() // tear down the siblings on the first exit
stop() // tear down the siblings on the first exit
for i := 0; i < 4+localServers+lanServers; i++ { // wait for the other goroutines
<-errc
}
@@ -1087,8 +1087,8 @@ type provisionArgs struct {
archive string
vmid int
hostname string
customerID string // baked into bootstrap (the hub config-pull target)
hubPassword string // the customer's hub retrieval passphrase (SECRET) — baked into bootstrap
customerID string // baked into bootstrap (the hub config-pull target)
hubPassword string // the customer's hub retrieval passphrase (SECRET) — baked into bootstrap
sizing bringUpSizing // OS-rootfs / Docker-data sizing for the bring-up half
}
@@ -1352,7 +1352,7 @@ func runSelftestEscrowConsume(ctx context.Context, logger *slog.Logger, blobPath
logger.Info("escrow: consuming R-wrapped escrow (Unwrap → fingerprint-gate → install)",
"blob_bytes", len(blob), "key_dest", keyDest) // R is NOT logged
if err := escrow.Consume(ctx, blob, R, expectedFP, keyDest); err != nil {
R = "" // drop the reference
R = "" // drop the reference
fmt.Fprintln(os.Stderr, " [FAIL] consume:", err) // the error never contains R or key bytes
return 1
}
@@ -1410,7 +1410,7 @@ type escrowUploadRequest struct {
// Slice 10D.1 — optional DR bundle (identity escrow + non-secret directive). Omitted in slice-7.
IdentityBlobB64 string `json:"identity_blob_b64,omitempty"`
DirectiveJSON json.RawMessage `json:"directive,omitempty"`
CreatedAt string `json:"created_at"` // RFC3339
CreatedAt string `json:"created_at"` // RFC3339
}
// uploadEscrowBlob PUTs the opaque blob (and, for 10D, the identity blob + non-secret directive) to