package signedjobs import ( "context" "encoding/json" "fmt" "log/slog" ) // opDecommission is the op class this executor serves (mirrors reconcile.ClassDecommission; the // literal avoids importing reconcile here just for the string). const opDecommission = "decommission" // IntentDecommissioner records a PERMANENT drive decommission keyed by the drive's storage // durable-id. Satisfied by *storage.IntentStore (SetDecommissioned). Decommission is the operator's // "permanent removal" intent: once recorded, the self-heal watchdog never auto-mounts the drive // again (it SURVIVES absent/present), cleared only by an explicit re-commission. type IntentDecommissioner interface { SetDecommissioned(durableID string) error } // decommissionParams is the verified params of a decommission op. The drive is named by its STORAGE // durable-id — the SAME key the storage observer / watchdog use (e.g. "uuid:"), NOT the // device-level byid:/byuuid: scheme the storage_wipe op uses. That is deliberate: decommission // manipulates the intent MAP the watchdog reads, so the signed id must be the watchdog's key or the // recorded intent would never gate a remount. type decommissionParams struct { DurableID string `json:"durable_id"` } // DecommissionExecutor is the operator "permanent removal" consumer (slice 10 P3 completion). It // makes the previously-unreachable IntentDecommissioned state reachable — but ONLY via a gate- // VERIFIED operator signature (decommission is classified destructive in reconcile.Classify, so the // signed-jobs gate refuses it pending_signature without a valid operator signature). It is distinct // from a customer-confirmable safe eject: permanent removal is never customer-authorizable. // // The binding IS the durable-id: the signed op authorizes decommissioning exactly that drive on // exactly the signed host (host_scope is checked by the gate). No /dev resolution is needed — and // none is wanted, because a drive can legitimately be decommissioned while physically ABSENT. type DecommissionExecutor struct { intent IntentDecommissioner logger *slog.Logger } // NewDecommissionExecutor wires the executor to the agent's intent store. func NewDecommissionExecutor(intent IntentDecommissioner, logger *slog.Logger) *DecommissionExecutor { if logger == nil { logger = slog.Default() } return &DecommissionExecutor{intent: intent, logger: logger} } // Execute implements signedjobs.Executor for the decommission op class. func (d *DecommissionExecutor) Execute(_ context.Context, op string, params json.RawMessage) error { if op != opDecommission { return ErrNoExecutor // not ours — the runner leaves it queued for the owning executor } if d.intent == nil { // No intent store on this host (open failure) → the op can't be honored. Don't clear it as // "done"; surface a hard error so the runner logs it and the operator can retry once fixed. return fmt.Errorf("decommission: no intent store wired on this host — cannot record decommission") } var p decommissionParams if err := json.Unmarshal(params, &p); err != nil { return fmt.Errorf("decommission: bad params: %w", err) } if p.DurableID == "" { // The durable-id IS the resource binding; a decommission with no id is unbound — refuse. return fmt.Errorf("decommission: op has no durable_id — refusing an unbound decommission") } // The gate has already verified the operator signature + burned the nonce durably BEFORE this // point. This log line is the audit trail of an operator-authorized permanent decommission. d.logger.Warn("decommission: executing operator-signed PERMANENT decommission", "durable_id", p.DurableID) if err := d.intent.SetDecommissioned(p.DurableID); err != nil { return fmt.Errorf("decommission: record intent for %q: %w", p.DurableID, err) } d.logger.Warn("decommission: operator-signed decommission complete (drive will never auto-mount)", "durable_id", p.DurableID) return nil }