AI Agents

  • Fixed

    • The agentgateway controller admits xDS from every data plane of its GatewayClass, in any namespace (giantswarm/agent-platform#495): its ingress policy (both flavours) selected only the platform edge’s data plane by Gateway name in the release namespace, so the serving slice’s models Gateway in org-<org> connected to nothing — on gazelle its data plane was denied at 9978 (Policy denied DROPPED) and restarted by its startup probe forever. A second peer selects gateway.networking.k8s.io/gateway-class-name with the namespace label present, on 9978 only.
  • Fixed

    • model-manager reaches the Hugging Face Hub for a kserve backend cluster-manager registers at runtime (giantswarm/agent-platform#494): the hub egress of its network policy rendered only for a static modelManager.backends kserve entry; with components.cluster-manager on it renders as well — on gazelle check_fit hung on the hub lookup until muster’s deadline.
    • The models Gateway’s data plane passes the fleet’s admission policies and gets its DNS record (giantswarm/agent-platform#492): the Gateway references a models-specific AgentgatewayParameters (the platform’s gateway.parameters.podSecurityContext / containerSecurityContext — seccomp RuntimeDefault, non-root, no capabilities —, dataPlaneResources, agentgateway.proxy.image, modelServing.modelsGateway.replicas, a LoadBalancer Service); on gazelle the controller’s default Deployment was denied by restrict-seccomp-strict and the Gateway never programmed. Its infrastructure annotations carry modelServing.modelsGateway.externalDns.annotations (default giantswarm.io/external-dns: managed, the filter Giant Swarm’s external-dns selects Services by) next to the hostname, so models.<domain> is created instead of falling through to the wildcard edge.
  • Fixed

    • No Warning MCPServerRecoveryFailed for the expected 401 of an OAuth-protected server. When an MCPServer whose callers bring their own credentials (auth.forwardToken, auth.tokenExchange, or an OAuth login through muster) came up together with muster, automatic recovery restarted it as soon as it answered and reported its 401 – the answer such a server is configured to give a token-less probe – as a failed recovery, one Warning per OAuth-protected server on every platform install, right after the same reconciler had put the server in Auth Required. Recovery now ends there with a Normal MCPServerRecoveryAwaitingAuth event (“automatic recovery reached the server; it waits for a signed-in caller”) and the server connects on the first call that carries a token, as before. A 401 from a machine identity (auth.type: sigv4), a 5xx and a refused connection stay MCPServerRecoveryFailed. (#1265)
  • Fixed

    • The serving slice renders without an edge Gateway (giantswarm/agent-platform#490): connectivity’s validateIngress required a public Gateway for the muster / route and a muster-direct-consistent components.agentgateway toggle in every release, muster on or off — so the <cluster>-agent-platform slice (examples/serving-slice.yaml: no muster, its own models Gateway; agentgateway on where the target runs no controller) failed its render on gazelle with no public Gateway for ingress.parentRefs. Both checks now apply only while components.muster is on; the mode enum, the agentgateway-direct refusal, the “agentgateway-* mode needs the component” rule and the per-component muster-direct guards are unchanged. tests/verify-serving-slice.py renders the slice without any ingress.parentRefs and holds the guard for a release with muster on.
  • Fixed

    • The Hugging Face cache claim no longer fails the connectivity release under a WaitForFirstConsumer StorageClass (giantswarm/agent-platform#483; follows #326; epic giantswarm/giantswarm#37639). With components.modelServing on, modelServing.cache.pvc (hf-cache) rendered as a release resource with no consumer of its own: the first predictor (or download Job) that mounts it is what Binds it, so under kind’s standard and the fleet’s default gp3 it stayed Pending, helm-controller’s wait counted it as not ready and the release failed on every install and upgrade (timeout waiting for: [PersistentVolumeClaim/model-serving/hf-cache status: 'InProgress'], five attempts, then stalled) — with kserve-resources before it and substrate, kagent, agent-manager and model-manager behind it. The claim is now applied by a post-install,post-upgrade hook Job (templates/model-serving/cache-pvc.yaml; the chart’s hook include and its identity <release>-hooks, whose ClusterRole carries get, create, patch on persistentvolumeclaims while the claim is the chart’s — never delete): kubectl apply --server-side under the field manager agent-platform-connectivity, the log says created or present and the phase, and nothing waits for the claim to Bind — it binds where its first consumer schedules, which is what a zonal volume needs (the GPU pool’s zone). The applied spec is the one the chart rendered (hf-cache in the serving namespace, helm.sh/resource-policy: keep, accessModes, size, storageClassName incl. "-", volumeName), and so is what a cache.enabled flip or an uninstall does — the claim stays, now because it is not Helm’s. An installation upgrading from a chart that rendered the claim keeps it (the keep policy) and the hook takes its fields over (--force-conflicts); a size change is applied by the next upgrade’s hook (the StorageClass has to allow expansion); an immutable field (storageClassName, accessModes, volumeName) fails the hook naming it. Installations without the switch render byte-identically (make verify-modes); make verify-serving-slice asserts the hook, its claim and identity, the knobs, and that cache.enabled: false or an existingClaim render none of it; make verify-wiring the serving shape without a PersistentVolumeClaim object; make verify-gpu-pool compares both sides without the claim. Not taken: a consumer pod of the chart’s own (it would pin a zonal volume to its own zone and hold an RWO claim), an Immediate-binding class (the same zone lottery, and not the fleet’s default), a release-wide disableWait (blind to the platform’s dependency chain).
  • 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).