AI Agents

  • Added

    • kagent.oauth2ProxyIngress.additionalPeers (default []), in both charts: extra callers the kagent oauth2-proxy ingress policy admits on the proxy port (4180) next to the public Gateway’s Envoy pods — a list of pod label maps (matchLabels), each rendered as one more fromEndpoints entry (cilium flavor; the kubernetes flavor renders no oauth2-proxy policy). A peer in another namespace carries its own io.kubernetes.pod.namespace; an item that is not a non-empty label map fails the render. For an installation whose kagent hostname sits on a private edge LB and whose kagent API is reached from elsewhere through a Teleport tunnel: the local teleport-kube-agent (app: teleport-kube-agent in kube-system) proxies the tunnel’s requests to oauth2-proxy and was dropped by the policy, so the tunnel hung without an HTTP answer. make verify-kagent-netpol covers it. No installation needs to act: the default render is unchanged.
  • Added

    • gitops.forbidInlineSecrets (default false): with true the meta-package render fails when a credential is set inline in the values — kagent.providers.<name>.apiKey, kagent.oauth2-proxy.config.clientSecret / .cookieSecret, muster.muster.oauth.server.dex.clientSecret / .google.clientSecret / .registrationToken / .encryptionKeyValue / .storage.valkey.password, valkey.valkey.auth.aclUsers.<user>.password, klausGateway.slack.botToken / .signingSecret / .appToken, klausGateway.obo.stateKey / .storeKey, model-manager.oauth.dex.clientSecret, agent-manager.oauth.dex.clientSecret — and names the offending key paths (never their values). Such a value is forwarded verbatim into the component’s HelmRelease spec.values (Argo: valuesObject) and into Helm’s release storage, readable by anyone allowed to get HelmReleases in gitops.namespace; every component chart takes the same credential from a pre-created Secret instead (kagent providers.<name>.apiKeySecretRef with an empty apiKey and oauth2-proxy.config.existingSecret, muster oauth.server.existingSecret and .storage.valkey.existingSecret, valkey auth.usersExistingSecret, klaus-gateway slack.secretName with an empty botToken and obo.existingSecret (klaus-gateway >= 0.39.0), model-manager / agent-manager oauth.existingSecret). The values.yaml guidance for those keys now recommends the pre-created Secret, and klausGateway.obo.existingSecret is documented next to the keys it replaces. make verify-secrets (in CI) pins the default, the failure and the referenced-Secret render. No installation needs to act: the flag is off and the default render is unchanged; an installation that has moved its credentials into Secrets sets it to true so they cannot come back.
  • Changed

    • The CNPG Cluster’s CiliumNetworkPolicy (<clusterName>-cluster) selects the pods of a Cluster named <clusterName>-restore as well — the scratch Cluster a restore test bootstraps from the backup store (bootstrap.recovery + externalClusters[].plugin). Its full-recovery Job needs the Kubernetes API and the object store, which the CNPG operator’s cluster-wide policies (instance, initdb, join pods) do not grant to recovery Jobs; without this the restore Job times out on the API server. Name a scratch restore Cluster exactly <clusterName>-restore (the Crossplane IAM role already trusts <clusterName>-restore*). No installation needs to act.

    Fixed

    • Documented in the README: enabling postgres.backup on a Cluster that is already running rolls its instance pods to add the plugin sidecar, and the immediate first Backup fires before the sidecar exists and fails with requested plugin is not available; the next scheduled run (or a manual Backup with method: plugin) succeeds. A Cluster created with the block from the start does not hit this.
  • Added

    • postgres.backup.* in both charts: backups for the CNPG Cluster the connectivity chart renders, off by default. method: plugin (the default) wires the Barman Cloud plugin — CNPG’s supported object-store path since 1.26, the one the in-tree barmanObjectStore is deprecated in favour of — as the Cluster’s WAL archiver and renders an ObjectStore (<clusterName>-backup: destinationPath, optional endpointURL for S3-compatible stores, exactly one credential source out of s3.inheritFromIAMRole, static S3 keys, azure.inheritFromAzureAD, an Azure connection string or account+key Secret references, gzip WAL/data compression, retentionPolicy default 30d, sidecar resources) and a ScheduledBackup (<clusterName>-scheduled, default daily at 02:00 UTC, immediate, backupOwnerReference: self). method: volumeSnapshot renders backup.volumeSnapshot on the Cluster instead (no WAL archive). serverName overrides the folder in the store, objectStore.existingName reuses a store, serviceAccount.annotations binds the Cluster’s ServiceAccount to a cloud identity (IRSA role, workload identity) for the sidecar. In the cilium flavor the CNPG policy gains egress for the store (DNS, world on networkPolicy.ports, the cluster entity for an in-cluster endpointURL, optional fqdns and cidrs). postgres.backup.crossplane.* (off) provisions the store the way the fleet provisions its own object storage: on aws an S3 Bucket with a lifecycle rule, a public-access block and a TLS-only policy plus an IAM Role whose trust policy names the cluster’s IRSA OIDC provider for the Cluster’s ServiceAccount and <clusterName>-restore* (scratch restore clusters), with destinationPath, s3.inheritFromIAMRole and the role annotation derived; on azure a Storage Account, a Container, a lifecycle ManagementPolicy and, on private installations, a PrivateEndpoint in the node subnet, with destinationPath and azure.connectionString derived from the Account’s connection Secret (<clusterName>-backup-store). Bucket, Account and Container are never deleted by Crossplane and carry helm.sh/resource-policy: keep. Guards fail the render on a missing or foreign destinationPath, zero or several credential sources, IRSA without a role annotation, a bad method, retentionPolicy or storage-account name, a snapshot method without a class and missing Crossplane inputs. make verify-postgres covers it; ci/test-postgres-values.yaml (MinIO shape), ci/test-postgres-backup-aws-values.yaml and ci/test-postgres-backup-azure-values.yaml render it. No installation needs to act, but every installation with postgres.enabled and no postgres.backup should: its Cluster now carries agent-platform.giantswarm.io/backup: none and NOTES warns that the PVCs are the only copy of the platform database. A CNPG Cluster without a backup still reports ContinuousArchiving=True/ContinuousArchivingSuccess — the operator’s wal-archive command exits 0 when there is nowhere to archive to — so that condition is not a signal.
  • Fixed

    • The kagent cilium network policies open egress from the kagent controller and from the kagent-managed agent pods to kagent’s built-in tool server when it is enabled (kagent.kagent-tools.enabled: true): the app.kubernetes.io/name: kagent-tools pods in the tools namespace on the tools port (8084; an explicit kagent.kagent-tools.namespaceOverride / service.ports.tools.targetPort is honoured). kagent-controller-egress and kagent-agent-muster-egress are default-deny egress lists, so the controller’s tool discovery (MCP initialize + tools/list on the kagent-tool-server RemoteMCPServer URL) and the agents’ tool calls were dropped as Policy denied: the RemoteMCPServer stayed Accepted=False and every agent that referenced it ran without tools. Nothing renders for installations with the tool server off (the default). make verify-kagent-netpol (in CI) pins the rules both ways.
  • Fixed

    • A restart request on an MCPServer whose endpoint is down is processed by one attempt (#1166). spec.restartRequestedAt is mirrored into status.lastRestartedAt after the reconciler’s first start or restart attempt, whether it succeeded or not; the failure is reported once in the reconcile result, and the following attempts are the service’s own, on the reconnect backoff the failed start has already scheduled (status.nextRetryAfter, capped at 2 minutes since #1163). Before, the value was recorded only when the restart succeeded: the failed start moved the service starting -> failed, the state-change bridge queued a reconcile for each transition, the request was still pending, and the reconciler restarted the service again – 131 Restarting MCPServer service <name> (restartRequestedAt=...) / Connection failure #N pairs in 53 s on a management cluster whose tunnel was scaled to 0, one MCPServerFailed event each, throttled only by the 500 ms debounce, while status.consecutiveFailures climbed to 131 and status.nextRetryAfter said “in 2 minutes” the whole time; the loop stopped only when the field was removed from the CR. core_service_start and core_service_restart write exactly this field in Kubernetes mode, so any restart of an unreachable server from the CLI or the UI triggered it. The same rule applies to a spec.suspended: false resume whose start fails (its marker used to be kept, so every state change resumed again) and to a pending request whose create or update in the same pass already attempted a start: one attempt, then the service’s schedule. A request on a server with no registered service (autoStart=false, never started) whose start fails before anything is registered stays pending and is retried on the reconcile queue’s own backoff, since no state change can re-trigger it. The reconciler logs Restart of MCPServer <name> requested at <time> failed: <error>; the request is processed, further attempts follow the service's reconnect backoff (next attempt at <time>) once per processed request. Scenario: mcpserver-restart-request-endpoint-down.
  • Changed

    Full Changelog: https://github.com/giantswarm/klaus/compare/v0.0.329...v0.0.330

  • Fixed

    • The reconnect backoff of a remote MCPServer is capped at 2 minutes (#1163). The wait between connection attempts still doubles from 30 s on every consecutive failure, but it stops at MUSTER_MCPSERVER_MAX_BACKOFF (default 2m; a Go duration) instead of 30 minutes, so a server whose upstream has healed is retried within the cap plus the 30 s retry tick, however many attempts the outage cost. Before, four 504s from a tunnel in front of a healthy server scheduled the fifth attempt 4.5 minutes out and the sixth 8.5, and users saw the server down for that long after the outage had ended. MCPServerFailed events now name the HTTP status the endpoint answered with and the scheduled retry – server unreachable after 4 consecutive failures (endpoint answered HTTP 504, next retry in 2m0s at 2026-09-05T15:57:34Z): ..., or no HTTP response for a refused connection, a DNS failure or a timeout – and every failure below the threshold emits connection failure N of 3 before unreachable (...). The aggregator no longer emits a second, bare MCPServerFailed on the same transition, so there is one event per failed attempt and it is the one with the schedule. The MCPServer CR status now carries the schedule too: status.consecutiveFailures, status.lastAttempt and status.nextRetryAfter existed in the CRD but were never written; they are now mirrored from the service on every status sync, together with the new status.lastFailureHTTPStatus, and cleared once the server connects or answers a 401 (which also clears the in-memory schedule, so Auth Required no longer sits next to a stale retry forecast). core_service_status exposes the same fields under metadata, core_mcpserver_get returns lastFailureHTTPStatus, and a Failed server whose error is an HTTP 5xx gets the status message Upstream error (HTTP 5xx) - the server or a gateway in front of it is failing; muster retries with backoff. Scenario: mcpserver-remote-backoff-cap; muster test gained test_set_mock_server_outage (a mock server answers its next N requests with a fixed HTTP status) and runs instances with MUSTER_MCPSERVER_MAX_BACKOFF=3s.
  • Fixed

    • The family tool surface follows what members currently offer (#1162). A spec.family member that kept its registration but re-listed fewer tools – mcp-capi 0.3.x hides its mutating tools when it runs read-only – kept advertising the tools it had dropped: tools/list returned 40 x_capi_* names while every member reported 24, describe_tool named members that no longer offered the tool, and calling one failed with tool not found. The family routing index only grew (providers were unioned on every listing and removed only on deregistration) and the per-session listing served the capability store regardless of the member’s state. The index is now re-synced from every listing – a contributing member’s entries are replaced by what it contributed, tools it no longer offers lose it as a provider, a tool no member offers disappears, and members that did not take part in a per-session listing keep their entries – and a member whose service is down (failed, unreachable, stopped, disconnected) contributes nothing to any session’s tools, resources or prompts, even though its pending-auth registry entry and cached capabilities survive. A failed SSO reconnection drops the session’s cached capabilities for that server, and the person’s sessions receive list_changed notifications when a capability list shrinks, as they already did when it grew. Scenario: mcpserver-family-member-relists-subset; muster test gained test_stop_mock_server / test_start_mock_server.
  • Fixed

    • Subject-scoped grants refresh themselves. For an MCPServer whose authorization server is pinned with grantScope: subject (GitHub’s hosted MCP server and in-house servers verifying GitHub tokens), muster now redeems the grant’s refresh token itself: every lookup that serves a tool call, a core_auth_login or a broker release finds the person’s grant, refreshes it when its access token has expired or will within five minutes (capped at half the token’s lifetime) using the pinned tokenEndpoint and the pre-registered client, and stores the rotated tokens under the person and under every session copy. Refreshes are single-flighted per person and issuer and re-read the store first, so a rotating refresh token (GitHub) is redeemed exactly once however many sessions hit the expiry together. A refresh the authorization server rejects (invalid_grant, GitHub’s bad_refresh_token) removes the grant so the next use asks for a sign-in; a transient failure keeps it. Before, the store hid the token 30 s before expiry and nothing redeemed the refresh token: eight hours after connecting GitHub every session lost the server with authentication required: server returned 401 Unauthorized and had to connect again. The in-memory store keeps expired tokens that carry a refresh token for 30 days instead of sweeping them. Logged as subject_grant_refreshed / subject_grant_refresh_rejected / subject_grant_refresh_failed.
    • Token broker grant targets (tokenExchangeBroker.targets.<audience>.grantIssuer). A broker target can now release a person’s subject-scoped grant instead of exchanging at a downstream Dex: an allow-listed confidential broker client exchanges the person’s ID token (audience=<audience>) and receives the access token of the person’s own grant from that issuer – looked up by the subject token’s raw sub, the identity the grant was filed under, not by a trustedIssuers subjectClaim mapping – refreshed first when it is due, with issued_token_type access_token and expires_in the token’s remaining lifetime, never the refresh token. A person without a grant gets invalid_target (no_grant in muster’s log). clientAudiences gates it and every release is audited (subject_grant_released / subject_grant_release_refused). A target is either a grant target or a Dex exchange target: grantIssuer and dexTokenEndpoint are mutually exclusive and a grant target rejects the exchange settings at startup. Lets a developer portal implement its standard GitHub auth API on the grant muster already holds, with GitHub’s own client libraries. See “Releasing a person’s grant to a trusted relying party” in docs/how-to/connecting-non-rfc9728-mcp-servers.md.
    • tokenExchangeBroker.brokerClients.<id>.clientSecretFile: seed a confidential broker client from a file holding its secret, for deployments without a Kubernetes Secret store (muster serve against a local config directory, the test harness). Takes precedence over clientCredentialsSecretRef.
    • muster test: muster_broker.clients (confidential broker clients with their audiences) and muster_broker.grant_targets (audience → mock authorization server) configure brokered exchange for a scenario; test_broker_token_exchange accepts client_id/client_secret for HTTP Basic client authentication and reports an opaque released token with expires_in, issued_token_type and has_refresh_token; the mock authorization server marks access tokens it issues on a refresh with a refreshed- prefix and OAuth-protected mock backends echo an opaque bearer through echo_token tools. Scenarios: oauth-subject-grant-refresh, oauth-subject-grant-refresh-rotation, oauth-broker-grant-release.