AI Agents

  • Added

    • modelManager.networkPolicy.registeredBackends: the egress an installation allows for backends registered at runtime (giantswarm/agent-platform#478; follows #329; bumblebee-plans#46 D3/D4/D5/D7; epic giantswarm/giantswarm#37639). model-manager starts with no backend (#329) and a backend — an in-cluster Ollama on :11434, an LM Studio on a node address, a Lemonade Server — is registered while it runs, as a labelled ConfigMap the chart never sees; the connectivity chart’s egress policy for model-manager is rendered from the static backend inputs, so it did not open the registered one: under the cilium flavour add_backend succeeded, the backend showed on the Serving page and every model call was dropped, and the kubernetes flavour opened 443 only (modelManager.networkPolicy.egress.cidrs too). The new list — {cidr, port} and {fqdn, port} entries — renders one more egress rule per entry on its port in both flavours, after the static-backend rules (unchanged): toCIDR / ipBlock for a block, toFQDNs plus the cluster entity for a name (the cilium flavour only, exactly as the static hostname rule — so an in-cluster Service is listed by name there, Cilium’s CIDR rules matching neither pods nor nodes). Empty by default, the default render is byte-identical. Guards fail the render on a cidr that is not an IPv4 CIDR, a missing or non-TCP port, an entry with both or neither destination, an fqdn that is not a hostname, and an fqdn under the kubernetes flavour with policies on, naming the cilium flavour. make verify-managers asserts the rule in both flavours, the untouched default and every guard; UPGRADE.md tells an operator with an enforcing policy to list a backend’s destination when registering it.
  • Fixed

    Full Changelog: https://github.com/giantswarm/klaus/compare/v0.1.18...v0.1.19

  • Changed

    Full Changelog: https://github.com/giantswarm/klaus/compare/v0.1.17...v0.1.18

  • Fixed

    Full Changelog: https://github.com/giantswarm/klaus-operator/compare/v0.0.160...v0.0.161

  • Changed

    • The agentgateway data plane runs two replicas behind a PodDisruptionBudget, spread across nodes; the controller runs two replicas. Every MCP call and, with llmRouting on, every model call of the platform crosses the data-plane Deployment the agentgateway controller reconciles from the Gateway; at one replica with no budget it was one node reboot or drain away from taking every agent offline. The connectivity chart’s AgentgatewayParameters now carries deployment.spec.replicas from gateway.parameters.replicas (2), a podDisruptionBudget.spec from gateway.parameters.podDisruptionBudget (enabled: true; the keys other than enabled are the PDB spec as written, and with neither minAvailable nor maxUnavailable set the template emits maxUnavailable: 1 — maxUnavailable so a lone replica stays evictable, in the template so minAvailable can be chosen through the meta chart, where a null never reaches the connectivity defaults), and one topologySpreadConstraint per gateway.parameters.spread.topologyKeys entry (kubernetes.io/hostname; maxSkew: 1, whenUnsatisfiable: ScheduleAnyway, so a single-node lab still schedules both pods, and matchLabelKeys: [pod-template-hash] so a rollout spreads the new ReplicaSet against itself — without it the surge pod is balanced against the revision being replaced and both survivors can be left on one node, which ScheduleAnyway never corrects), selecting the data-plane pods by gateway.name. The budget’s third pass-through key is unhealthyPodEvictionPolicy, and the maxUnavailable: 1 default fills in the missing budget field rather than replacing the spec, so a policy set on its own survives it. The render refuses a budget with both fields, a key that is not one of the three passed through (keeping the two budget fields out of values.yaml is what leaves the block additionalProperties: true, so a misspelt minAvailabe would otherwise be dropped in silence), a string that is not a percentage from 0% to 100%, a fractional or negative number, an unhealthyPodEvictionPolicy outside the API’s enum, every budget that allows no eviction (an integer minAvailable at or above replicas, a percentage minAvailable that rounds up to every replica, a zero maxUnavailable — it would hang every node drain on the data plane), and spread.enabled with no topology key or an empty key in it; the schema holds replicas and maxSkew at 1 or more, and the template additionally refuses replicas, spread.maxSkew and spread.whenUnsatisfiable when they are unset — a null set through the meta chart is consumed by Helm at that layer, so the chart sees a deleted key that no schema keyword can floor, and replicas alone would otherwise render deployment.spec.replicas: 0 and scale the data plane to zero. The meta chart declares the same keys at the same defaults, and its forwarded agentgateway.controller.replicaCount is 2. Leader election is on — the controller’s manager sets LeaderElection: !DisableLeaderElection and that setting defaults to false (agentgateway controller/api/settings/settings.go), the chart grants the lease in the controller namespace and sets no AGW_DISABLE_LEADER_ELECTION — so the two pods do not both reconcile the data plane or write Gateway status: the status syncer’s NeedLeaderElection() is true (the leader alone) and the xDS Syncer’s is false (every replica serves xDS). What the second replica buys is a pod-level failure and a rolling restart, not node redundancy: the packaging chart’s strict schema declares controller.podDisruptionBudget, controller.strategy and affinity as empty objects with additionalProperties: false and declares no topologySpreadConstraints at all, so the two controller pods cannot be spread and often land on one node — a budget and a spread for the controller wait for a schema fix there (giantswarm/agentgateway#51). make verify-dataplane-ha and tests/verify-agentgateway-wiring.py hold the shape. Every installation with the agentgateway component on rolls the data plane once (a second pod, the PDB, the spread constraint) and the controller once (a second pod); gateway.parameters.replicas: 1 with podDisruptionBudget.enabled: false and spread.enabled: false restores the previous shape. On a single-node cluster a drain of that node now waits on the budget (the replacement pod cannot schedule) — turn the budget off there. Legacy SSE MCP sessions are pod-local, and two replicas break them outright rather than only across a pod change: the session lives on the pod that answered /sse, the ClusterIP Service balances each later POST on its own, and a POST that lands on the other pod finds no session. Streamable-HTTP sessions (what the platform’s own RemoteMCPServers use) survive both. gateway.parameters.podAnnotations is now empty by default: the karpenter.sh/do-not-disrupt: "true" 4.15.0 (#431) put on the data plane was the answer to a single pod, and next to two replicas, the budget and the spread it only pinned both of the pods’ nodes against Karpenter’s consolidation, drift and expiry — while the budget, which is what holds a drain to one pod at a time, was never reached. The knob is unchanged and an installation that would rather keep the streams open on a pod over node churn sets the annotation back on it; what it buys is the streams that outlive the data-plane shutdown window (10 s still accepting, draining to the 55 s deadline).
  • Fixed

    • make verify-gpu-pool passes on a GOLDEN_REF that carries modelServing.gpuPool (#482). The untainted case asserts the serving render is byte-identical to GOLDEN_REF (origin/main by default) but for the discovery block — with the golden rendered at its own defaults. From the commit that merged #482 onto main that golden is tainted, so an untainted head could never equal it and test-ingress-modes failed on every branch cut from main, main included. The golden now takes the same modelServing.gpuPool.taint.key= override when its values.yaml declares the key, and the discovery block is cut from both sides: the assertion is that equal inputs render alike, which is what it was always meant to be. A golden from before #482 still renders at its defaults, unchanged.
  • Added

    • modelServing.gpuPool: the GPU node pool’s taint as a toleration and its label as a node selector on everything the serving layer schedules onto the pool, published for model-manager; the model-manager line moves to >=0.23.0 (giantswarm/agent-platform#315; bumblebee-plans#46, the plan’s D3/D5; epic giantswarm/giantswarm#37639). A GPU node pool created through the platform (the gpu-node-pool chart) arrives tainted nvidia.com/gpu NoSchedule and labelled giantswarm.io/machine-pool=<cluster>-<pool>; the serving layer tolerated no taint, so the vLLM runtime and every preset’s predictor would land nowhere on such a pool. New: modelServing.gpuPool.taint (key, value, effect; the default is the pool chart’s taint — nvidia.com/gpu NoSchedule with no value, tolerated with operator: Exists, so a pool whose taint carries a value is covered too; a value narrows it to Equal; an empty key is an untainted pool and renders nothing) and modelServing.gpuPool.nodeSelector (the pool’s label, per pool and empty by default — the serving slice’s values set it). The connectivity chart applies both to the ClusterServingRuntime (KServe copies them onto every predictor pod of the runtime; runtime.tolerations come after, runtime.nodeSelector keys win) and to every published preset’s scheduling block (the pool’s toleration first, the preset’s own after it, an equal entry once; the pool’s selector under the preset’s own keys) through one helper (agent-platform.modelServing.poolScheduling), and publishes them in the discovery ConfigMap as spec.gpuPool.taint.{key,value,effect} and spec.gpuPool.nodeSelector — the keys model-manager 0.23.0 reads (giantswarm/model-manager#86) to schedule the LLMInferenceServices it composes, its download Jobs and its inventory scan pods by the same input (a registered backend document may override it). Platform components never carry it. Three render guards: the effect is one of the three taint effects, the key a qualified name, every selector value a string. components.model-manager.versionRange moves to >=0.23.0 <1.0.0 (the floor: the release that reads the keys) and the example BOM pins 0.23.0. make verify-gpu-pool (tests/verify-gpu-pool.py, fixture ci/test-model-serving-gpu-pool-values.yaml) asserts the three sites in the default, pool-selected, valued and untainted shapes, the guards, the meta chart’s forwarding, and that an empty taint key leaves the serving render byte-identical to origin/main but for the discovery block. The default render is unchanged (model serving is off by default). Every installation with model serving on rolls its predictors once (the runtime gains the toleration and KServe reconciles every InferenceService of it; the models reload from the cache) and model-manager once (0.23.0). README of the connectivity chart, “The GPU node pool”.
  • Added

    • components.cluster-manager: the Agent Platform’s cluster write surface as a chart component, off by default (giantswarm/agent-platform#316; giantswarm/giantswarm#37637; bumblebee-plans#42 and #46; epic giantswarm/giantswarm#37639). One OCIRepository + HelmRelease of the catalog’s cluster-manager chart (>=0.4.2 <1.0.0, dependsOn: [muster] — the floor is the first release that tolerates a cluster without the Cluster API group: list_clusters answers an empty list with a note, a dry run on a cluster that does not exist is refused naming it, get_info reports clusterApi and the real version) in model-manager’s mold: the cluster-manager: block forwarded verbatim — the pinned Service name, the OAuth 2.1 resource server acting as the caller (oauth.downstream), the chart’s own MCPServer CR with forwardToken and the dex-k8s-authenticator audience the kube-apiserver trusts, so muster registers x_cluster-manager_* and the portal’s Clusters pages call the tools through muster as the signed-in user (MCP is its only surface: no route, no JWT policy, no proxy entry) — with global.* injected and modelManager.namespace derived from the platform’s own namespace (an own value must agree). The connectivity chart renders the wiring from the clusterManager: block: network policies in both flavors (ingress from muster and the kubelet’s probes; egress to DNS, this installation’s Kubernetes API, the identity provider, the workload clusters’ API servers — clusterManager.networkPolicy.workloadClusters: names, addresses, ports: [443, 6443] — and clusterManager.networkPolicy.egress on 443) and the render-time guards (muster off with the MCPServer on, a missing identity input, a bad CIDR, an empty port list, clusterManager.flux.requireApi). The two blocks travel to the connectivity release only while the component is on (gatedValues, vm-manager’s shape); the default render is unchanged but for the roster entry. examples/customer-bom.yaml pins 0.4.2; make verify-cluster-manager (tests/verify-cluster-manager.py) covers all of it; README “Cluster manager”. The flip to default-on is a decision for after the first lab proof.
  • Added

    • The serving slice: components.kserve-runtime-configs, the models Gateway with one Dex JWT policy, examples/serving-slice.yaml, two 24 GB presets (giantswarm/agent-platform#326; bumblebee-plans#46, the plan’s D4/D6/D8/D9; epic giantswarm/giantswarm#37639). components.kserve-runtime-configs (giantswarm/kserve, 0.2.x, dependsOn: [kserve-llmisvc-crd], into the release namespace — where the llm-d controller resolves well-known configs from, besides the LLMInferenceService’s own) installs the well-known LLMInferenceServiceConfigs with kserve.llmisvcConfigs.enabled: true, kserve.servingruntime.enabled: false and no registry value (the chart’s gsoci default, giantswarm/kserve#78); its block is held back from connectivity. modelServing.modelsGateway (off by default — the Gateway is the slice’s) renders one agentgateway Gateway models on models.<global.domain> with the platform’s wildcard (or a Certificate from tls.issuerRef), the external-dns hostname, and one AgentgatewayPolicy (Strict, inheritance: Override, audience dex-k8s-authenticator, JWKS from the issuer’s host on 443); KServe’s kserve.controller.gateway.ingressGateway.kserveGateway is derived onto the kserve-resources release so every LLMInferenceService route attaches to it, and the discovery ConfigMap names the endpoint and the /<namespace>/<model>/v1 convention. examples/serving-slice.yaml is the one-release-per-target-cluster profile: the five kserve components and the switch, runtimeClassName: nvidia, agentgateway off beside the platform’s release and on for a workload cluster (nothing is detected). Presets qwen3-4b-instruct and qwen3-8b-fp8 (one L4-class GPU, tools on, no image; the schema gains spec.template). The default render is byte-identical but for the roster entry. make verify-serving-slice; README “The serving slice and the models Gateway”; docs/authentication.md section 6.
  • Changed

    • model-manager ships with every installation by default, with no backend; the model-manager line moves to >=0.22.0 (giantswarm/agent-platform#329, bumblebee-plans#46 D3/D4/D5/D7, epic giantswarm/giantswarm#37639): components.model-manager.enabled defaults to true and the forwarded block names no backend — neither model-manager.backend nor model-manager.backends — so the release starts with zero backends (model-manager 0.22.0’s zero-backend start-up) and backends are registered at runtime as labelled ConfigMaps (the add_backend tool, the portal’s Serving page, cluster-manager for a GPU node pool; model-manager’s docs/backends.md). The connectivity guards demand an endpoint only for a backend that is listed statically, so backend: ollama / backends: [ollama] without ollama.endpoint fail as before; the static forms render as before (the golden holds them equal). Model-manager’s OAuth guard follows the muster guard’s convention exactly: a model-manager.oauth.dex.* / existingSecret value that disagrees with global.identity fails the render (one login provider), checked only where both sides are set; a missing input is reported by the child release as for muster, so a render that sets no global.identity (or global.domain / global.identity.issuerUrl alone) is untouched. With components.kagent off the meta chart derives kagent.disableWiring: true for the model-manager release (it must not wire ModelConfigs into a kagent the installation does not run), with components.muster off muster.mcpServer.enabled: false (the MCPServer CRD ships with muster, so the MCP surface follows it; the REST API stays), and with muster’s OAuth server off (muster.muster.oauth.server.enabled, the platform’s one login — the lab shape of examples/kind-lab-dex.yaml) oauth.enabled: false (no issuer to trust, the way the muster discovery label derives from the same toggle); tests/verify-components.py asserts all three, both ways, and the two connectivity guards that failed the render for those cases are gone. Opt out with components.model-manager.enabled: false. The network policy for a backend registered at runtime is giantswarm/agent-platform#478.