AI Agents

  • Fixed

    • klaus-gateway reaches the kube-apiserver for its team-review endpoint on the bolt link store (giantswarm/agent-platform#511; towards giantswarm/klaus-gateway#273): klaus-gateway >= 1.11.0 verifies every caller of POST /reviews and POST /notices with an authentication.k8s.io/v1 TokenReview, but the connectivity chart granted the gateway pod the kube-apiserver entity only for the Secret link store (klausGateway.obo.store: secret with OBO on), so an installation that keeps its links in the bolt store and turns klausGateway.reviews.enabled on had no rule to the API server in its Cilium default-deny egress — every TokenReview timed out and the endpoint answered 503 to every caller (the OBO policy’s world/cluster :443 allowance never reaches the API server, which Cilium’s socket-LB resolves to the control plane before the policy applies). agent-platform.klausGateway.apiServerEgress is truthy also while klausGateway.reviews.enabled is true, so -klausgateway-store-egress carries toEntities: [kube-apiserver] (cilium) or the networkPolicy.kubernetes.apiServerCIDR ipBlock (kubernetes) whatever the link store — one rule, rendered once next to the Secret store’s. The connectivity chart declares klausGateway.reviews with the klaus-gateway chart’s defaults (enabled: false, audience: klaus-gateway, allowedCallers: []); the default shape renders byte-identically. make verify-klausgateway-netpol holds the rule on the bolt store in both flavours, its absence with reviews off, and that reviews next to the Secret store render the rule once; ci/test-klausgateway-reviews-values.yaml lints the shape. Lab-verified in agentlab with the connectivity chart of the PR: the in-cluster gateway with reviews.enabled and an allow-list answered POST /notices from the allowed ServiceAccount with 201 and from a ServiceAccount outside the list with 403.
  • Fixed

    • Plain muster auth login renews an expired stored ID token. The renewal decision hung on the handler’s status path, which reaches Authenticated only when the stored access token is valid for another minute by the CLI’s own reading and otherwise probes the server; the mcp-go transport connects with that same token regardless, so a session the aggregator accepted answered Already authenticated while the decision never saw the ID token (muster auth status then said No authentication required). The decision now reads the ID token’s exp from the token file alone, before the “already authenticated” answer and without a probe; a session without an ID token is renewed too. (#1268)
  • Fixed

    • The agentgateway controller can fetch the models Gateway’s JWKS from the public issuer, and a failed fetch is visible (giantswarm/agent-platform#505; proof 1 of giantswarm/giantswarm#37639). The controller — not the data plane — fetches the JWKS of every AgentgatewayPolicy and pushes the keys to the data plane over xDS; a fetch its network policy denies pushes an empty key set, and every token is refused 401 token uses the unknown key while the data plane has nothing to log. On gazelle the serving slice’s models-jwt policy (JWKS from dex.<installation> on 443, the chart’s default) was served by the platform release’s controller, whose policy admitted the API server, muster and the in-cluster Dex on 5556 and nothing public: error fetching jwks … dial tcp <edge LB>:443: i/o timeout, every id_token refused until the backend was pointed at the in-cluster Dex by hand. Two changes: (1) global.identity.issuerUrl’s host is always an external JWKS target of the controller policy (agent-platform.jwks.issuerTarget, joined into agent-platform.jwks.externalTargets): opened by name on 443 (the URL’s own port when it carries one) behind the DNS proxy clause in the cilium flavour, as port 443 of the wide rule in the kubernetes flavour, in every release that runs the controller and whatever the routes name — the controller serves the JWT policies of every release in the cluster, and the slice’s models policy beside the platform’s release is one the platform’s policy cannot see; an in-cluster issuer adds nothing (gateway.jwksEgress covers it). (2) The controller policies render wherever the controller runs (agent-platform.agentgateway.controller: an agentgateway-* ingress mode, or components.agentgateway on): the serving slice on a workload cluster (components.agentgateway.enabled: true, ingress.mode at its muster-direct default, no edge Gateway) rendered no controller policy at all, though its own controller serves the models Gateway there. The chart’s comments (models-gateway.yaml, the jwks values, gateway.jwksEgress, docs/authentication.md) now name the controller as the fetcher and its egress as the requirement. Visibility: make live-serving-slice NAMESPACE=<the slice's> (tests/verify-serving-slice-live.py) reads the models-jwt policy’s Accepted condition on every ancestor and the controller’s jwks-store entry for the backend’s URL, and fails naming the URL and the controller’s error fetching jwks lines on reason: PartiallyValid or an empty key set — the check the gazelle proof runs before a token is sent. make verify-serving-slice asserts the slice-on-workload-cluster controller policy in both flavours (rendered without an ingress mode; the issuer on 443 with the DNS proxy; an own jwks.host beside it; none beside the platform’s release); make verify-wiring asserts a public issuer is opened from the controller policy with and without a route policy of the release’s own, on the URL’s port, that an in-cluster issuer adds no rule, and holds the all-in-cluster shape byte-identical to origin/main. Every installation with the controller on and a public issuer gets one controller policy change: a toFQDNs rule for the issuer on 443 and the DNS proxy variant of the DNS rule (cilium), or 443 on the wide rule (kubernetes); no pod rolls.
  • Fixed

    • The model-serving pod policies match LLMInferenceService workload pods (giantswarm/agent-platform#506): the connectivity chart’s model-serving Kyverno mutations (redirect-model-storage, storage-initializer-memory, add-cache-init, the Deployments’ progress-deadline), its network policies and the predictors’ PolicyException selected pods by serving.kserve.io/inferenceservice, the classic InferenceService predictor’s label; the workload pod the llm-d controller creates from an LLMInferenceService carries kserve.io/component=workload, app.kubernetes.io/part-of=llminferenceservice and app.kubernetes.io/name=<model> instead and serves from a container named main, so none of it applied — on gazelle (4.28.8, proof 1 of giantswarm/giantswarm#37639) the predictor composed from qwen3-4b-instruct downloaded 8 GB into an emptyDir with its storage-initializer OOMKilled twelve times at KServe’s 1Gi default, while the policies stood Ready beside it. The two pod shapes are one list now (agent-platform.modelServing.podShapes: selector, model-name label, runtime container), and every selector of the serving namespace renders from it: the mutations match both shapes, take the model’s name from whichever label the pod carries (the cache subdirectory), anchor the /mnt/models cache mount on the shape’s runtime container (kserve-container or main; an anchored entry never adds a container) and skip a pod that names no model; the network policies render one policy per shape (-model-serving-llmisvc-workload[-ingress|-egress] next to -predictor…) and the kagent agents’ egress admits both; the PolicyException matches both shapes’ Pods, ReplicaSets and Deployments by label next to the *-kserve* name. make verify-model-serving-policies (tests/verify-model-serving-policies.py) runs kyverno apply of the rendered policies over a fixture pod of each shape (tests/fixtures/model-serving-*-pod.yaml, the llm-d one as observed on gazelle) and asserts the claim mount and subPath on the initializer and the runtime, the 4Gi limit, hf-cache-init first, a pod without an initializer and a download-Job pod untouched, the Deployments’ deadline, and that the network policies, the agents’ egress and the PolicyException select exactly the same pods; the CI job installs the kyverno CLI for it. The live half — the workload pod’s volumes on a GPU cluster and a second load of the same preset downloading nothing — is the proof’s.
  • Added

    • muster auth login renews the stored ID token. A valid session is still reused, but the session’s automatic refresh renews the access token only: the OIDC ID token in ~/.config/muster/tokens/<hash>.json kept the exp of the first sign-in, so the CLI carried an ID token expired for weeks – signed with a key the identity provider no longer publishes – while muster auth status said Authenticated. Now login signs in again through the browser when the stored ID token has expired (or expires within 30 s), and always with the new --force; the stored session is replaced only when the new sign-in completes. muster auth status and muster auth whoami show the ID token’s expiry (ID token: expired 12 days ago (renew with: muster auth login)), and the new muster auth token [--id] prints the access token or the ID token alone on stdout for other clients – a script calling an API that validates the person’s ID token – and refuses an expired ID token instead of handing it out. (#1268)
  • Fixed

    • The two L4 presets fit the smallest L4 instance (giantswarm/agent-platform#502): qwen3-4b-instruct and qwen3-8b-fp8 request 2 vCPU / 10 GiB and are limited to 4 vCPU / 12 GiB (were 4 / 16Gi and 8 / 24Gi). A g6.xlarge has 4 vCPU and 16 GiB, of which a Giant Swarm node reports 3.4 vCPU / 13.4 GiB allocatable, and the daemonsets that follow a GPU pool’s taint request ~0.4 vCPU / 1.5 GiB more — on gazelle the predictor composed from qwen3-4b-instruct sat Pending for ten minutes on a pool of xlarge nodes (could not schedule pod … no instance type has enough resources … instance-size In [xlarge]) until the pool was widened to 2xlarge at +50 %/h (giantswarm/giantswarm#37714). The predictor’s host memory is the runtime and the weights passing through the page cache (the KV cache is on the GPU), so 10 GiB requested and 12 GiB limited cover an 8–9 GiB checkpoint. Each description names the instance it is sized for; tests/verify-serving-slice.py holds the requests within what a g6.xlarge leaves a predictor (the same node model cluster-manager’s create_node_pool answers with) and tests/verify-gpu-pool.py leaves the two preset ConfigMaps out of the byte-identity check against a golden from before.
  • Fixed

    • The Substrate worker image follows the chart’s Substrate pin, never the kagent build’s stamp (giantswarm/agent-platform#466). The meta chart pins Substrate and kagent as two independent ranges, and every kagent build stamps into its chart the ateom-gvisor worker of the Substrate it was published against; chart 4.15.2’s open kagent range (>=0.11.0-gs.12 <0.11.1-0) admitted kagent 0.11.0-gs.14, which stamped a 0.0.30-gs.1 worker under the chart’s 0.0.27-gs.9 atelet — Substrate 0.0.30 had renamed the pause bundle (bundles/pause → bundles/_pause), so every golden boot failed with reading …/bundles/_pause/config.json: no such file or directory, every AgentTemplate stayed Ready=False … compiling, and nothing on any hop named the skew (seen on a fresh agentlab on the default chart, giantswarm/agentlab#187; 4.15.5 re-pinned both ranges by hand). The chart now derives kagent.substrateWorkerPool.workerImage for the kagent release from components.substrate.versionRange’s floor — <substrate.image.registry>/ateom-gvisor:<floor> (agent-platform.substrate.workerImage, over the forwarded block like the other derived values) — so the atelet the substrate release installs and the worker the WorkerPool runs are one Substrate release whatever kagent build the kagent range admits; the same 4.15.2 shape now forwards the 0.0.27-gs.9 worker. An installation’s own workerImage stands while its tag is the pinned release (a mirror by another path; substrate.image.registry moves the derived image on its own) and fails the render otherwise, naming the key, the release and the derived image. The Substrate range must confine one release (agent-platform.substrate.validateRange): an exact version or >=X.Y.Z-gs.N <X.Y.(Z+1)-0 — the worker follows the floor and the atelet what Flux resolves, so a later gs.N of the pinned release may reach the control plane ahead of the worker and a 0.0.31 never; 0.x, ~, ^, a <= ceiling or a floor alone fail the render naming the range. make verify-worker-image (tests/verify-worker-image.py) holds the derivation, the mirror, the own-value rule, the 4.15.2 shape, the exact pin, the range guards, and — against the kagent chart the range resolves to — the rendered WorkerPool’s spec.workerImage and that the build was published against the pinned release’s X.Y.Z (its Chart.yaml substrate dependency), so the meta chart’s CI names the day the kagent line moves to another Substrate before the chart’s pin does; verify-components asserts the forwarded image, verify-workerpool the range guard’s precedence. components.substrate / components.substrate-crds move to >=0.0.30-gs.4 <0.0.31-0 (the bounded golden boot, giantswarm/substrate#39, giantswarm/giantswarm#37801): the worker the fleet already runs through kagent 0.11.0-gs.20’s stamp, so an installation following the current chart sees the WorkerPool’s spec.workerImage unchanged and the pool does not roll; the BOM pins it. Not taken: a kagent minor bump on every Substrate-line change (a convention every <0.11.1-0 range in the wild depends on, nothing in the chart holds it) and reading the resolved Substrate version at render time (Flux resolves after the render; the floor is the one version the chart knows, and the ceiling guard is what makes it the runtime’s).
  • Changed

    • (verify-target) Drop the golden-side klausGateway.crd hold — origin/main carries #497, so nulling the key there inserted crd: null and made the meta default render drift in #501 by @fiunchinho

    Full Changelog: https://github.com/giantswarm/agent-platform/compare/v4.28.6...v4.28.7

  • Changed

    • klaus-gateway’s crd.install is no longer set by the meta chart, and the connectivity chart’s Kubernetes API egress for the gateway covers the Secret link store only (giantswarm/klaus-gateway#271). klaus-gateway drops the crd and configmap routing stores, the ChannelRoute CRD and the embedded controller, and its chart’s values.schema.json is closed (additionalProperties: false), so it refuses crd.* and controller.*. The meta chart forwards the whole klausGateway block to the klaus-gateway release (valuesFrom: klausGateway) and its defaults set klausGateway.crd.install: true, so that key has to go before the klaus-gateway release that drops it rolls: the component’s range >=1.10.0 <2.0.0 takes it with no PR, and a values file the schema refuses fails the Helm upgrade — the release sticks (the pods stay up). The current klaus-gateway chart defaults crd.install to true, so the render on the installations does not change: every installation’s klaus-gateway release gets one Helm revision (a values change) and no pod rolls. All three installations run klausGateway.routing.store: valkey and none sets crd or controller in its own patch. The connectivity chart follows: agent-platform.klausGateway.apiServerEgress is now the Secret link store’s gate alone (klausGateway.obo.store: secret with OBO on), so the -klausgateway-store-egress policy renders byte-identically for every shape an installation runs, and make verify-klausgateway-netpol no longer asserts the configmap / crd / controller.enabled gates.
  • Fixed

    • The serving predictors pass the fleet’s restricted Pod Security Standard (giantswarm/agent-platform#498): a PolicyException model-serving-predictors in kyvernoPolicies.policyExceptionNamespace names the four rules the llmisvc controller’s predictor violates (the upstream vLLM image and its cache init container run as root: require-drop-all, privilege-escalation, run-as-non-root, check-seccomp-strict, with their autogen copies) for the serving namespace’s predictor Pods, ReplicaSets and Deployments — on gazelle the first LLMInferenceService’s Deployment was denied at admission and no model could be served.