agent-platform release v4.20.0
Added
- The Substrate worker pool gets a
PodDisruptionBudget; the pool template’s Karpenter knobs are documented and verified; a spread value is refused until the Substrate line carries it (giantswarm/agent-platform#472). The platform runs every agent on the one WorkerPool (kagent.substrateWorkerPool, four gVisor workers, one actor each), and nothing guarded its workers against voluntary disruption: no budget selected them (the kagent namespace carried budgets for the controller and the database only), no annotation kept consolidation off their node — while on gazelle (2026-09-15) Karpenter had bin-packed all four onto one spot m6a.xlarge, so one consolidation or reclaim took the whole pool down: every turn in flight failed, every session waiting for a person was lost (its pause checkpoint is node-local, giantswarm/giantswarm#37795), new turns failed no free workers available. Decided 2026-09-15: the pool stays on spot, the workers get a budget only. New: kagent.substrateWorkerPool.podDisruptionBudget (enabled: true, maxUnavailable: 1, unhealthyPodEvictionPolicy: AlwaysAllow) — a PodDisruptionBudget named after the pool in the kagent namespace, rendered by the connectivity chart the way muster-valkey’s is (#439; the kagent chart has no budget template), selecting the worker pods by Substrate’s ate.dev/worker-pool: <pool> label, so a voluntary drain moves one worker at a time (ALLOWED DISRUPTIONS 1 with four Ready workers); the key never reaches the kagent release (components.kagent.omitKeys); the four budgets of the connectivity chart now share one spec helper and its guards (agent-platform.podDisruptionBudget.spec, the render unchanged). Documented in values.yaml, both READMEs and UPGRADE.md, unset by default: template.annotations karpenter.sh/do-not-disrupt: "true" (consolidation, drift and expiry never drain a worker node; a drift roll then waits on the NodePool’s terminationGracePeriod) and template.nodeSelector karpenter.sh/capacity-type: on-demand (the only knob that covers a spot interruption; Karpenter-only — never on CAPZ, on-prem or a cluster-aws node pool), and the roll every spec.template change causes (the Deployment once, one worker at a time under the budget, turns in flight on a replaced worker lost, goldens untouched). The render now refuses what WorkerPool.spec.template would prune silently (agent-platform.validateWorkerPool): topologySpreadConstraints and podAntiAffinity — carried by no published Substrate release yet (the carried patch giantswarm/giantswarm#37797) and pruned by the CRD’s structural schema, so a spread value looked applied and did nothing — are refused naming the key and the range until components.substrate.versionRange’s floor is the release that carries them (agent-platform.substrate.workerPoolSpreadFloor, empty until then) and forwarded verbatim from it on; any other key the template does not have is refused the same way (a typo was pruned in silence); the floor is read by the new agent-platform.semverRangeFloor. kagent.controller.substrate.defaultWorkerPool is marked inert (the kagent chart renders SUBSTRATE_DEFAULT_WORKERPOOL_*, nothing in the line reads them). make verify-workerpool asserts all of it: the budget from the connectivity chart of the working tree with the pool’s selector and maxUnavailable: 1, gone with enabled: false, never on the kagent release; the annotation and the capacity-type selector reaching the kagent release and the resolved kagent chart’s WorkerPool verbatim; the two spread keys and an unknown key refused; still exactly one WorkerPool. make verify-disruption counts the worker budget among the connectivity chart’s; the golden render leaves it off on both sides. Every installation with kagent on gets one new object (the budget); no pod rolls.