AI Agents

  • Changed

    • (verify-target) Drop the golden-side controller VPA hold — origin/main carries #455, so deleting kagent.controller.vpa on one side is what makes the renders drift in #530 by @fiunchinho

    Full Changelog: https://github.com/giantswarm/agent-platform/compare/v4.30.0...v4.30.1

  • Added

    • The kagent controller gets a VerticalPodAutoscaler (giantswarm/agent-platform#455). New knob kagent.controller.vpa: enabled: auto renders it where the cluster serves autoscaling.k8s.io/v1 (an explicit true / false wins). The connectivity chart renders VerticalPodAutoscaler kagent-controller in the kagent namespace on the controller Deployment — updateMode: InPlaceOrRecreate (VPA 1.5+ on Kubernetes 1.33+: the running pod’s requests are resized in place, no eviction, no roll), controlledValues: RequestsOnly, minAllowed 100m / 128Mi, maxAllowed 1900m / 480Mi, minReplicas: 1. The key never reaches the kagent release. Every installation on a cluster with the VPA gets the object — UPGRADE.md.
  • Changed

    • (verify-target) Drop the golden-side metric-labels hold — origin/main carries #528, so nulling llmRouting.metricLabels there is refused by its schema and verify-target never renders in #529 by @fiunchinho

    Full Changelog: https://github.com/giantswarm/agent-platform/compare/v4.29.0...v4.29.1

  • Added

    • Per-person metrics on the agentgateway data plane: every series of a route that verifies a bearer — the kagent controller route first — carries the caller as a user label, so a Slack turn through klaus-gateway is counted, timed and error-rated per person (giantswarm/agent-platform#528). klaus-gateway makes every controller call as the linked person — the Dex id_token in authorization, never its own identity (its pkg/a2a client; Slack is a forwarded-only channel) — and the kagent-controller-jwt policy verifies that token on the GRPCRoute in Strict mode, so the verified claims are on every request the data plane logs; agentgateway (the CRD field since upstream v1.2.0; the GS line’s v1.5.1-gs.4 the fleet runs) turns a CEL expression into a Prometheus label on every metric that carries route labels — frontend.metrics.attributes.add, a Gateway-scoped AgentgatewayPolicy, the labels evaluated when the request completes against the request as the route policies left it, so a Gateway-scoped label reads a route-scoped policy’s jwt.<claim>. The connectivity chart renders one such policy, <release>-metrics (templates/agentgateway/metrics-policy.yaml), whenever the data plane is on and an entry is enabled, from gateway.metricLabels — a map keyed by Prometheus label name, not a list: Helm merges maps and replaces lists, so an installation turns one default entry off (gateway.metricLabels.user.enabled: false) or adds one (team: {expression: jwt.groups}) without restating the rest; each entry is {expression: <one-line CEL>, enabled: <bool, absent = true>}. Three by default: agent and agent_namespace (source.unverifiedWorkload.* — the entries that were llmRouting.metricLabels, unchanged in name and expression, so the LLM usage dashboard reads on) and user, whose expression goes through tpl: jwt.{{ .Values.kagent.controller.auth.userIdClaim }}, the identity claim the controller route verifies and writes into x-user-id (email), so the metrics attribute a turn to the same person the controller acts as and follow the claim knob. An entry that reads jwt — jwt.<claim> or jwt["<claim>"] — is rendered only while a route of the Gateway verifies a bearer (the controller route with its JWT policy, agent-manager’s, model-manager’s — agent-platform.jwtRouteRendered), since without one it would read unknown on every series. On the controller route a Slack turn is one lf.a2a.v1.A2AService/SendStreamingMessage stream: sum by (user) (rate(agentgateway_requests_total{route=~".*/kagent-controller"}[1h])) is turns per person (route is <namespace>/<name>), the duration histogram the turn’s length; the portal, a CLI and the other verifying routes are labelled the same way. What it does not give: a route without a JWT policy — the MCP path under oauthMode: passthrough, the LLM listener, whose calls carry the ModelConfig’s API key and no person — renders unknown, one extra label value there and no series per person, so tokens and cost stay attributed to the agent (the harness passes no caller identity to the provider; a per-person LLM label is a kagent change). The labels moved out of the LLM path’s policy because the data plane honours one metrics policy per Gateway (custom labels replace rather than merge; of two policies one is kept and the other dropped in silence, and which one has changed between agentgateway releases — arbitrary on v1.4, the lowest policy key on v1.5, the oldest on later builds) and the labels describe every route: the LLM policy (<release>-llm) keeps the route-type map alone, and the metrics render with or without llmRouting.enabled — before, an installation without LLM routing had no labels at all. Expect a one-time break in the data plane’s series at the upgrade: a new label set is a new series, so counters such as agentgateway_requests_total restart and dashboards and recording rules on them show a step there. The schema holds every entry to {expression, enabled}. The render fails, naming the entry, on a missing or empty expression (as written or after tpl), one spanning more than one line, a name that is not a Prometheus label name, one the data plane’s own series already carry (a duplicate label name fails the whole scrape), one the scrape adds (namespace, pod, job, …: Prometheus would store it as exported_<name>), one starting with __ — the name checks run on an entry the jwt gate holds, too — and more than 16 enabled entries (the CRD’s limit). kagent.controller.auth.userIdClaim must be a bare identifier wherever it is read as CEL — the controller route’s identity header, the user entry on any verifying route, kagent on or off — since an expression that does not compile takes the whole policy down; the guard now lives in the helper that returns the claim, so it fires exactly there and no longer only with the controller route. Two entries of one name cannot be written: the key is the name. Name and expression go through toYaml, so a name YAML reads as a boolean (on) is quoted. In muster-direct there is no data plane and the map is inert. make verify-metric-labels asserts the policy in every shape, that it is the only one with a frontend.metrics section (per render and per template), the map’s merge (one entry off, one more, a renamed person label), the tpl, the gate, the meta chart’s forwarding of the expression as written and the schema’s refusal of the retired key, and the guards; verify-llm-routing now asserts the LLM policy carries no metrics section. README: “Metric labels”. Know what the default costs: an email address in Mimir is personal data, and the label multiplies the verifying routes’ HTTP series by the number of people (tens to a few hundred on an installation); gateway.metricLabels.user.enabled: false drops it.

    Removed

    • Breaking: llmRouting.metricLabels (giantswarm/agent-platform#528): the data plane’s metric labels are gateway.metricLabels (above), the same two entries keyed by name rather than listed, rendered whether or not LLM routing is on. Not aliased — both charts’ schemas refuse the old key, naming it, so a values file that still sets it fails the render at upgrade rather than losing its labels in silence. Move the entries one block over, as map keys; a file that never set it needs nothing.
  • Fixed

    • Remove helm-docs version badges from chart README in #212 by @piontec

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

  • Fixed

    • Remove helm-docs version badges from chart README in #485 by @piontec

    Full Changelog: https://github.com/giantswarm/klaus/compare/v0.1.20...v0.1.21

  • Fixed

    • The models Gateway reaches a served LLMInferenceService: the llm-d workload’s network policy admits the workload’s own port, 8000 (giantswarm/agent-platform#525; proof 1 of giantswarm/giantswarm#37639). The connectivity chart’s -model-serving-llmisvc-workload CiliumNetworkPolicy admitted ingress on modelServing.networkPolicy.predictor.port — 8080, the classic InferenceService predictor’s, the one port every shape’s policy shared since #506 — while the workload pod the llm-d controller creates is reached on 8000 — the port KServe’s <name>-kserve-workload-svc targets and the rendered HTTPRoute’s backendRef names: the template’s llm-d routing sidecar (--port=8000 --vllm-port=8001), vLLM’s main on 8001 behind it. On gazelle (4.28.18, 2026-09-17 05:39Z, the first hands-free run of the proof to reach a Ready predictor) hubble showed the gateway’s SYNs to the pod on 8000 policy-denied, and every POST …/v1/chat/completions with a person’s id_token answered 503 upstream call failed: Connect: deadline has elapsed (without a token 401: the JWT policy worked). Each pod shape now carries its port in the chart’s one shape list (agent-platform.modelServing.podShapes): the classic predictor predictor.port (8080, unchanged), the llm-d workload the new modelServing.networkPolicy.llmisvcWorkload.port (8000), declared in both charts — the meta chart forwards its whole values tree to the connectivity release, so a connectivity-only default would have been shadowed by the meta chart’s tree exactly as the #522 lists were (#524). Every rule that names the port renders per shape: the ingress from the callers and from the kubelet (cilium), the kubernetes-flavour ingress, and the kagent agents’ egress, now one rule per shape on that shape’s port instead of both shapes on 8080. make verify-model-serving-policies ties each shape’s admitted port — in every one of those rules, as rendered from the connectivity defaults and from the values the meta chart forwards — to the port of the container the shape’s fixture pod is reached on (tests/fixtures/model-serving-llmisvc-workload-pod.yaml gains the routing sidecar the pod is reached through on 8000; main keeps its 8001), refuses two fixtures on one port, and fails a render whose llm-d value is the classic port (the 4.28.18 shape) naming the shape; make verify-meta (tests/verify-mirrored-values.py) holds every networkPolicy port equal between the two charts next to the fqdns / cidrs lists, both model-serving ports required. Every installation with model serving on gets two policy changes and no pod roll: the llm-d workload’s ingress moves to 8000, and the agents’ egress splits into one rule per shape. The live half — POST …/v1/chat/completions with a person’s id_token → 200 from the served model, without a token → 401 — is the proof’s.
  • Changed

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

  • Fixed

    • The meta chart’s forwarded values carry the Hugging Face download CDN pattern, and a mirrored egress list can no longer drift between the two charts (giantswarm/agent-platform#522, round 2; proof 1 of giantswarm/giantswarm#37639). #523 (4.28.17) added *.*.*.hf.co to the connectivity chart’s defaults only. The meta chart forwards its whole values tree to the connectivity release (templates/components.yaml, forwardAllValues), and a forwarded copy of a default shadows the child’s own, so an installation’s connectivity release still rendered the four old patterns from modelServing.networkPolicy.huggingFace.fqdns and modelManager.networkPolicy.huggingFace.fqdns of helm/agent-platform/values.yaml (gazelle 2026-09-17 04:30Z: Helm storage’s supplied .config with four entries beside the chart’s five; the three serving CNPs unchanged, the download would have been dropped exactly as before; caught before a model was loaded). Both meta lists gain *.*.*.hf.co, and the meta chart’s huggingFace and policies.env comments are the connectivity chart’s (the #520 rationale corrected in #523 was still the meta copy’s). Two checks hold the two charts together: make verify-meta (tests/verify-mirrored-values.py) asserts the meta chart’s copy of every networkPolicy fqdns / cidrs list equals the connectivity chart’s default, naming the path that differs, with a negative control (a list short of one entry fails, naming modelServing.networkPolicy.huggingFace.fqdns); make verify-model-serving-policies renders the connectivity chart a second time from the values the meta chart forwards to its release (the meta chart’s defaults, the model serving switch on) and matches the HUB_HOSTS fixture against those three CNPs too — the render an installation gets, not only the child’s defaults; against the previous meta values it fails naming us.aws.cdn.hf.co. The live half — the weights on the claim, the predictor Ready, /v1/chat/completions answering — is the proof’s.
  • Fixed

    • The model pods reach the Hugging Face download CDN: the model-serving egress admits us.aws.cdn.hf.co, three labels under hf.co (giantswarm/agent-platform#522; proof 1 of giantswarm/giantswarm#37639). The connectivity chart’s -model-serving-predictor, -model-serving-llmisvc-workload and -model-serving-download CiliumNetworkPolicies admitted huggingface.co, *.huggingface.co, *.hf.co and *.*.hf.co; Cilium’s * matches one label and never a dot, so the download CDN the Hub redirects every shard request of a Xet-backed repository to (Xet client or not — the LFS path lands on the same host) matched nothing and the download’s SYNs were dropped one second after the DNS proxy had resolved it: with Xet off (#520) the storage-initializer crash-looped on us.aws.cdn.hf.co, with Xet on it hung. modelServing.networkPolicy.huggingFace.fqdns and modelManager.networkPolicy.huggingFace.fqdns gain *.*.*.hf.co; the lists stay a toFQDNs allow-list (no world, no CIDR). tests/verify-model-serving-policies.py (make verify-model-serving-policies) matches the names of the download path — the Hub, the LFS fronts, the Xet fronts, the CDN and a regional sibling — against the rendered patterns of all three policies with Cilium’s rule and keeps out a deeper name and a look-alike domain, so a new host shape fails the render, not a download on an installation. The modelServing.policies.env comment of #520 is corrected: Xet off keeps the initializer on one host path that fails fast on a policy drop; it is not what reaches the CDN.