agent-platform release v4.29.0

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.