The signed decommission prunes the drive from every guest's bind record (found live on demo-hp, R-1124 proof)
The self-serve decommission already dropped the drive from the guest-bind record; the operator-signed one did
not, so a forgotten drive left a record the 20-second re-assert warned about forever ("skipping non-enrolled
drive"). DecommissionExecutor.SetGuestBinds; red-proved. Code only; the CHANGELOG follows the release (0.159.1).
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -1092,6 +1092,9 @@ func runDaemon(cfg config.Config, logger *slog.Logger, logRing *applog.Ring) int
|
||||
decommIntent = intentStore
|
||||
}
|
||||
decommExec := signedjobs.NewDecommissionExecutor(decommIntent, logger)
|
||||
if guestBindStore != nil { // typed-nil guard, as above
|
||||
decommExec.SetGuestBinds(guestBindStore) // v0.159.1: prune the bind record like the self-serve decommission
|
||||
}
|
||||
|
||||
// Agent self-update (TASK D1): the agent_update executor downloads the operator-signed binary,
|
||||
// verifies it against the SIGNED sha256, and hands it to the root guarded wrapper (A/B flip +
|
||||
|
||||
@@ -39,9 +39,21 @@ type decommissionParams struct {
|
||||
// none is wanted, because a drive can legitimately be decommissioned while physically ABSENT.
|
||||
type DecommissionExecutor struct {
|
||||
intent IntentDecommissioner
|
||||
binds GuestBindPruner // optional (v0.159.1): drop the drive from every guest's bind record, as the self-serve path does
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
// GuestBindPruner is the per-guest enrolled-bind record (*localapi.GuestBindStore). The self-serve decommission prunes
|
||||
// the drive from it; the signed one did not, so a forgotten drive (R-1124) left a record the 20-second guest-bind
|
||||
// re-assert warned about forever ("skipping non-enrolled drive"), measured on demo-hp 2026-10-17.
|
||||
type GuestBindPruner interface {
|
||||
Guests() map[int][]string
|
||||
Remove(vmid int, durableID string) error
|
||||
}
|
||||
|
||||
// SetGuestBinds wires the bind record (nil-safe: no record, nothing pruned).
|
||||
func (d *DecommissionExecutor) SetGuestBinds(b GuestBindPruner) { d.binds = b }
|
||||
|
||||
// NewDecommissionExecutor wires the executor to the agent's intent store.
|
||||
func NewDecommissionExecutor(intent IntentDecommissioner, logger *slog.Logger) *DecommissionExecutor {
|
||||
if logger == nil {
|
||||
@@ -75,6 +87,20 @@ func (d *DecommissionExecutor) Execute(_ context.Context, op string, params json
|
||||
if err := d.intent.SetDecommissioned(p.DurableID); err != nil {
|
||||
return fmt.Errorf("decommission: record intent for %q: %w", p.DurableID, err)
|
||||
}
|
||||
if d.binds != nil {
|
||||
for vmid, ids := range d.binds.Guests() {
|
||||
for _, id := range ids {
|
||||
if id != p.DurableID {
|
||||
continue
|
||||
}
|
||||
if err := d.binds.Remove(vmid, id); err != nil {
|
||||
d.logger.Warn("decommission: guest-bind record not pruned", "vmid", vmid, "durable_id", id, "err", err)
|
||||
} else {
|
||||
d.logger.Info("decommission: drive removed from the guest's bind record", "vmid", vmid, "durable_id", id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
d.logger.Warn("decommission: operator-signed decommission complete (drive will never auto-mount)", "durable_id", p.DurableID)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -89,3 +89,37 @@ func TestExecutorChain_DispatchesToOwner(t *testing.T) {
|
||||
t.Errorf("want ErrNoExecutor for an unowned op, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
type fakeBinds struct{ m map[int][]string }
|
||||
|
||||
func (f *fakeBinds) Guests() map[int][]string { return f.m }
|
||||
func (f *fakeBinds) Remove(vmid int, id string) error {
|
||||
var kept []string
|
||||
for _, x := range f.m[vmid] {
|
||||
if x != id {
|
||||
kept = append(kept, x)
|
||||
}
|
||||
}
|
||||
f.m[vmid] = kept
|
||||
return nil
|
||||
}
|
||||
|
||||
// v0.159.1: the signed decommission drops the drive from EVERY guest's bind record, as the self-serve path does — and
|
||||
// only that drive. RED-PROOF (felhom.eu audits/agent-allowlist-rotation-2026-10-17/P1/redproof-bindprune.txt): with
|
||||
// the prune loop removed, the record still holds the drive and this test fails.
|
||||
func TestDecommissionExecutor_PrunesTheGuestBindRecord(t *testing.T) {
|
||||
fd := &fakeDecommissioner{}
|
||||
fb := &fakeBinds{m: map[int][]string{9201: {"uuid:keep", "uuid:gone"}, 9202: {"uuid:gone"}}}
|
||||
ex := NewDecommissionExecutor(fd, nil)
|
||||
ex.SetGuestBinds(fb)
|
||||
params, _ := json.Marshal(decommissionParams{DurableID: "uuid:gone"})
|
||||
if err := ex.Execute(context.Background(), opDecommission, params); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := fb.m[9201]; len(got) != 1 || got[0] != "uuid:keep" {
|
||||
t.Fatalf("9201 record = %v, want [uuid:keep]", got)
|
||||
}
|
||||
if got := fb.m[9202]; len(got) != 0 {
|
||||
t.Fatalf("9202 record = %v, want empty", got)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user