slice 10A: activate the control envelope (Down channel) + hub-backed desired provider (v0.15.0)

The control envelope becomes live: the agent caches the hub's desired-state +
generation and re-fetches GET /hosts/{id}/desired-state only when the
generation advances. A new internal/desired Syncer maps the wire shape into a
reconcile.CachingProvider feeding the engine; benign deltas reconcile, an
explicit guest decommission is gated pending_signature (exec is 10B). Adds the
DesiredStateResponse/WireDesiredState wire types + Client.FetchDesiredState +
the loop EnvelopeObserver seam. Cross-repo golden (envelope + desired-state)
byte-identical with the hub.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-10 19:02:59 +02:00
parent aa4dfb75ea
commit 8ecf8929fb
19 changed files with 836 additions and 59 deletions
+19
View File
@@ -24,6 +24,11 @@ const (
// emits it only when desired DiskBytes > actual; a shrink is data-losing and is refused
// (never silently applied as a grow). Slice 5 Phase B; unfed live until slice 10.
ActionResize ActionKind = "resize"
// ActionDecommission tears a guest down — the canonical DESTRUCTIVE delta (slice 10A). The
// planner emits it for an explicit DesiredGuest.Decommission; it classifies ClassDecommission
// → Destructive, so the gate refuses it `pending_signature` (no signer in 10A → never
// executed). Its EXECUTOR is slice 10B; 10A only plans + gates it.
ActionDecommission ActionKind = "decommission"
)
// growRoundMiB rounds a positive byte delta UP to whole MiB for the Proxmox `+<n>M` grow
@@ -88,6 +93,20 @@ func Plan(desired DesiredState, actual ActualState, norm FieldNormalizers) []Act
continue
}
// EXPLICIT decommission (slice 10A) — the destructive delta. Emit it as a single
// ActionDecommission and emit NOTHING else for this guest (no point reconciling cores
// on a guest the operator wants torn down). It is classified Destructive downstream, so
// the gate refuses it pending_signature in 10A (executor is 10B). Only emitted when the
// guest actually exists (decommissioning an absent guest is a no-op).
if d.Decommission {
actions = append(actions, Action{
VMID: vmid,
Kind: ActionDecommission,
Reason: "decommission requested (destructive — requires operator signature)",
})
continue
}
// Benign spec/description changes → a single SetConfig, only when we could
// read the current config (else we'd write blind).
if a.SpecKnown {