All notable changes to this project will be documented in this file.
-
spec.auth.authorizationServer.expectedIssuer: two clients of one authorization server. A pinned authorization server'sissuerwas both the identity the grant is filed under and the value the RFC 9207issparameter of the authorization response is compared against. GitHub sendsiss=https://github.com/login/oauthon every response, so a GitHub App pinned under its own identity (https://github.com/apps/<slug>, to keep its grant apart from another App's) never completed its sign-in:iss mismatch: response carried "https://github.com/login/oauth", expected "https://github.com/apps/<slug>". The new optional field names the issuer identifier the server really publishes; a presentissis compared against it (a trailing slash tolerated) whileissuerstays the grant key, so each App keeps its own grant, consent andcore_auth_logout. It needs the pinned endpoints; with the field unset nothing changes. Carried through the CRD, the API,core_mcpserver_getand the aggregator's pin; the how-to gained "Two clients of one authorization server" and the CRD reference the pin's fields. (#1277, #1275) -
muster auth loginrenews 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>.jsonkept theexpof the first sign-in, so the CLI carried an ID token expired for weeks -- signed with a key the identity provider no longer publishes -- whilemuster auth statussaidAuthenticated. 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 statusandmuster auth whoamishow the ID token's expiry (ID token: expired 12 days ago (renew with: muster auth login)), and the newmuster 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) -
muster self-update --checkreports the running and the latest release without installing anything and exits with status 125 when a newer one exists (devctl's convention forversion check), for scripts. Every command a person runs starts with a one-line hint on stderr while a newer release is out -- a hint, never a gate: the GitHub round trip is capped at two seconds, its answer is cached for an hour under the user cache directory (~/.cache/muster/latest-release.jsonon Linux,~/Library/Caches/muster/on macOS) and a failed attempt is remembered for ten minutes, so a machine without internet is not held up.MUSTER_NO_UPDATE_CHECK=1silences the hint;serve,standalone,agent,test,version,self-update,helpandcompletionnever print it, anddevbuilds never check. The sameinternal/updatepackage as agentlab's, so the two CLIs behave alike. -
muster test: an installation-shaped scale fixture with budgets enforced in CI.internal/testing/fixtures/scaleis a committed, generated fixture -- 87 MCPServers (84 session-authenticated, in five families with aninstanceArgon 28 installations, three in-house), 18 distinct capability documents shared by the family members, 282 workflows over their tools, 450 session records; every name invented, nothing from an installation -- rendered bygo generatefrom a seeded generator, with a test that fails when the committed YAML drifts. A scenario boots from it withpre_configuration.fixture: scaleand adds or overlays on top. Budgets are hard limits with headroom over the value measured on CI's medium executor, never "faster than last run", and a failing budget names the metric, the measured value and the limit. In Go (internal/aggregator, run bymake test): a warmlist_tools,filter_tools,describe_toolorcall_toolover the fixture costs at most 100 ms (measured 1--2 ms), 2 store commands (HGETALL,HKEYS) and 0 definition reads (API-server requests) -- the catalogue is built once;list_tools' default page stays under 40 KB (measured 14 KB); the capability store holds at most 16 KB per session over 450 sessions (measured 7.2 KB: 84 references and a share of 18 shared documents). On the wire (scenarioscale-fixture-budgets, on a Valkey stand-in): the default page under 40 KB, under 32 KB of capability store per session (measured 9.9 KB over 8 sessions), a warm meta-tool request costing the store a constant 16 commands at most (measured 11: the two of the catalogue plus the OAuth middleware's per-request session bookkeeping, nothing per server), and a new session's first request answered within 2 s while its fan-out connects 84 servers two of which take four seconds. New test toolstest_measure_meta_tool(duration, response bytes, store commands by name per call) andtest_valkey_footprint(the store's content by prefix, capability bytes per session); a new expectationjson_path_maxbounds numeric response fields, and a step whose expectations are not met now reports which one and how (json_path_max "valkey_commands": measured 177, budget 16) instead of "expectations not met". The scenario fails on the release that had each bug, at the budget's step:list_toolsunbounded on v5.15.6 (response_bytes61,651 against 40,960, #1193); every session storing every server's document inline on v5.19.4 (capability_bytes_per_session105,332 against 32,768, 672 inline entries and no shared document, #1217); a listing reading every server's entry and document on its own on v5.19.10 (valkey_commands177 against 16: 84HGETand 87GETper call, #1225); a new session's first request held by the fan-out on v5.19.14 (4.02 s against a 2 smax_duration, #1226). Documented in "Installation scale: the fixture and the budgets" ofdocs/contributing/testing/scenarios.md. (#1240) -
Homebrew: the release pipeline tells the tap
giantswarm/homebrew-musterabout a release once its binaries are on the GitHub Release (amuster-releaserepository dispatch from.circleci/custom.yml); the tap's workflow verifies every binary against its Sigstore bundle before it regenerates the formula.brew install giantswarm/muster/musterfollows each release again; the tap had been stuck at v0.7.5 since June. -
muster test: the mock OAuth server takes the profile of a real authorization server.mock_oauth_servers[].profile: github | dex | probundles that server's quirks instead of one flag per bug (omit_resource_metadata,omit_token_scope,pin_authorization_server,pin_endpoints_ref,authorize_accepts_any_client, ...; the flags stay and each overrides the profile for itself).github: no RFC 8414 discovery document, no Client ID Metadata Documents and no RFC 7591 registration (a pre-registered client only),scopeandexpires_inabsent from token responses; the MCP servers that reference it answer a bare 401 without RFC 9728 metadata, file their grants under the person and are pinned to it with explicit endpoints.dex: discovery,scopeomitted, an id_token with every token, RFC 7591 registration.pro(the MCP TypeScript SDK's authorization server): discovery, registration withoutregistration_client_uri, a client it does not know answered directly at/authorizewithinvalid_client, the token endpoint refusing unregistered clients, registrations held in memory. Without a profile the mock now hands out the RFC 7592 pair (registration_client_uri,registration_access_token) and answers the client read, so muster's spec-defined registration check runs against the well-behaved default and its authorization-endpoint probe againstpro.test_restart_mock_oauth_serverreplaces a mock authorization server's process behind its port and issuer; underprothe registrations go with the old process. The scenarios that reproduce a named server's bug carry its profile and fail on the release that had the bug: a server's grant surviving the login id_token mirror on a Dex (fails on v5.12.0, #1174), a registration a pro restart forgot detected and made again (fails on v5.7.8, #1128), a pinned bare-401 GitHub-style server connectable after a muster restart with no login in the new process (fails on v5.8.3, #1150). Documented in "Authorization-server profiles" ofdocs/contributing/testing/oauth-testing.md. (#1239) -
muster test: faults are named steps and time is a clock the scenario moves.test_redeploy_mock_serverreplaces a mock backend's process behind its port -- every MCP session forgotten, tools kept, no refused connection in between, the way a rolled pod takes over behind its Service;test_set_mock_server_auth: {required: true|false}flips a running mock between answering anonymously and 401-with-metadata, the rollover of a backend to an OAuth resource server (a mock with a token validator andoauth.required: falsestarts anonymous).test_advance_clock: {duration}movesmuster serve's clock and every mock authorization server's clock forward together: the reconnect backoff of a remote MCPServer, the orchestrator's retry and health ticks and the age of the core catalogue now read a process clock (internal/clock) that the harness advances through a control socketMUSTER_TEST_CLOCKselects -- a tick that has become due fires at once, nothing waits, and production binaries keep the system time.pre_configuration.intervals: productionleaves those timers on their production defaults (30 s initial backoff, 2 min cap, 30 s ticks, 5 min catalogue age) for scenarios that assert the production schedule; the defaultshortkeeps the seconds-long environment knobs. The fault steps and the clock are documented together in "Faults and time" ofdocs/contributing/testing/scenarios.md. Four scenarios: a backend redeployed behind the same port has its session recovered on the next call, no health probe in between (fails on v5.15.0, #999); on the production schedule a remote server whose gateway answers 504 four times waits 30 s, 1 min, 2 min and again 2 min -- never 4 -- and is connected within the cap once the gateway serves (fails on v5.9.10 with the clock, #1163); the core catalogue found older than its five-minute age is served as it is and rebuilt once in the background (fails on v5.19.13 with the clock, #1231); a forwardToken backend rolled over from an anonymous pod to a 401 pod and back is never registered globally and keeps serving the signed-in session (fails on v5.7.17, #1135). (#1238) -
Documentation site.
docs/is built with MkDocs (Material) and published at https://giantswarm.github.io/muster/ by the Pages workflow; the site kept servingmuster-agent.json, the muster agent's OAuth client metadata document, at its URL. A pull request that touches the documentation builds the site in strict mode, so a broken link, a page missing from the navigation or an unknown anchor fails the check.make docs-serverenders the site locally. -
The CLI reference is generated from the command tree.
make generate-cli-docsrendersdocs/reference/cli/from the Cobra commands (hack/gen-cli-docs);make verify-cli-docs, part ofmake test, fails when the committed pages are stale. The hand-written pages, which documented flags that never existed (--port,--mcp-port) and a REST API, are gone. -
muster test: scenarios can run their instance in Kubernetes mode.pre_configuration.mode: kubernetesstarts one envtest control plane per run from the binariesKUBEBUILDER_ASSETSpoints at, installs the CRDs fromhelm/muster-crds, applies the scenario'smcp_serversandworkflowsas CRs into a namespace of its own and runsmuster servewithkubernetes: trueagainst it -- informers, reconciler, boot pass and CR status the way an installation has them. A TCP proxy the harness owns sits between muster and the API server:apiserver.reachable_afterkeeps it closed for that long after the process started (the API server late at start) andtest_set_apiserver_reachable: false|truecuts and restores it mid-run.test_patch_crapplies a merge patch to a CR (spec.suspended, labels,spec.auth.authorizationServer) so the reconciler is driven by real CR updates;test_get_crreads a CR with the status muster wrote;test_set_mcpserver_labelsandtest_pin_mcpserver_authorization_serverupdate the CR in this mode.muster test --mode filesystem|kubernetesruns one definition source; withoutKUBEBUILDER_ASSETSthe Kubernetes-mode scenarios are reported as skipped with the reason, never as passed, andmake test-envtest(thetest-envtestCI job) runs them against envtest. Four scenarios reproduce bugs the filesystem-mode suite could not express: a suspended server is not started by the boot pass (fails on v5.19.6, #1216), a suspended remote server is stopped once and not on every resync tick (fails on v5.19.0, #1212), the boot pass and the reconciler's first pass register a dozen CRs without an ERROR (fails on v5.19.8, #1222), and an API server unreachable at start is waited for instead of swapped for the filesystem client (fails on v5.7.18, #1143); two more cover a boot from CRs and an API server gone mid-run. (#1237) -
muster test: scenarios can run their instance on Valkey storage and restart it.pre_configuration.storage: {type: valkey}starts an in-process Valkey stand-in (miniredis) per instance and points every backed store -- session auth, capabilities, OAuth tokens, state, client credentials, the OAuth server's own store -- at it, the way an installation's stores outlive a pod;start_delaymakes it answer only aftermuster servestarted, for "Valkey is late" scenarios.test_restart_instancestops and startsmuster serveon the same configuration while the store and the mock servers keep running and reconnects every user client with the bearer it held, so the steps after it act as the sessions that lived through a rollout;test_stop_valkey/test_start_valkeytake the store away and bring it back with its data. Three scenarios reproduce bugs that were first seen on an installation because the suite could not express them: a family tool call after a restart without a listing (fails on v5.18.2, #1204), a Valkey that answers late at start being waited for rather than replaced by memory stores (fails on v5.19.12, #1229), and a pinned bare-401 server staying connectable and its grant revocable after a restart with no login in the new process (fails on v5.8.3, #1150, #1154). (#1236)
- The Go module is
github.com/giantswarm/muster/v5; every import path carries the suffix (github.com/giantswarm/muster/v5/pkg/oauth). The module path had no/v5while the releases were v5.x, and Go only considers v0 and v1 tags for such a path:go install github.com/giantswarm/muster@latestresolved to v1.12.0 from August and installed a month-old v1 binary, andgo install github.com/giantswarm/muster@v5.23.2was refused ("module contains a go.mod file, so module path must match major version").go install github.com/giantswarm/muster/v5@latestnow resolves to the newest release, and Go's own VCS stamping gives a build of a tagged commit its v5 tag instead of a pseudo-version of the last v1 tag. No repository outside this one imported the old path. Thegoimports -localprefix of the pre-commit hook and the contributor docs follow the module. The OpenTelemetry scope namegithub.com/giantswarm/muster(observability.TracerName, theotel_scope_namelabel, the logger scope) is an identifier dashboards filter on, not an import path, and is unchanged. muster versionandmuster --versionprint one line,muster version v5.23.5 (commit 361cdef, built 2026-09-15T19:38:51Z), with the details the binary knows; thecommit:andbuilt:lines and theirunknownplaceholder are gone, andmuster versionstill adds the aggregator's version when one is running. Ago buildfrom a checkout reports Go's own stamp -- the tag at a tag,+dirtyover local edits, a pseudo-version between tags (v5.23.6-0.20260915195350-977012d023ba: after v5.23.5, before v5.23.6) -- whichself-updateand the hint compare as what it is; a binary without any version saysdev, andself-updaterefuses it with a pointer togo install github.com/giantswarm/muster/v5@latest. The self-update messages follow agentlab's ("Nothing newer than v5.23.5 on GitHub (latest release v5.23.5)."; a newer release is announced with its URL instead of its full release notes), and its logic moved fromcmdtointernal/update.
-
A remote MCPServer whose endpoint answers the initialize with a 4xx is retried. A
forwardTokenserver registered while its backend did not serve the path yet -- a rollout in progress, the previous pod answering 404 -- failed its registration probe withserver returned 4xx for initialize POST, likely a legacy SSE server, readFailedand was never re-probed: the answer was not classified as a connection failure, so no retry was scheduled, and the sessions connecting later with their own token do not touch the CR's state. The server stayedFailed(andMusterMCPServerFailedfired after 20 minutes) although the backend had been serving the path since seconds after the registration. Such an answer now enters the reconnect schedule like a refused connection or a 5xx: retried with the same backoff, theMCPServerFailedevent andstatus.lastFailureHTTPStatusnaming the status mcp-go dropped (endpoint answered HTTP 404, next retry in 30s at ...), and the server settles on its own once the path answers -- inAuth Requiredfor a server connected per session,Connectedotherwise. The state rules of a per-session server (Failedonly while the endpoint does not answer,Auth Requireduntil the first session connects,Connectedfrom then on, a later session's failure never the server's state) are documented in the CRD reference. (#1295) -
muster agent --mcp-serverand the REPL wait for a slow tool. The CLI's MCP client bounded every request at 30 s, tool calls included, while a tool behind the aggregator may legitimately take longer -- anMCPServer'sspec.timeoutallows up to 300 s -- so a tool that answered after a minute succeeded through the aggregator and failed through the bridge withtransport error: ... context deadline exceededafter exactly 30 s;muster agent --timeoutwas bound to nothing. A tool call now runs under a timeout of its own, five minutes by default (the aggregator's largest tool timeout), for the bridge, the REPL'scalland every CLI command that calls a tool (muster callamong them); the handshake, listings, resources and prompts keep their 30 s.muster agent --timeoutsets the call timeout for the REPL and the bridge, and the bridge'scall_tooltakes an optionaltimeoutargument (seconds) that bounds one call in its place -- for a client that knows a read takes longer; the argument is the bridge's own and never reaches the aggregator. (#1293) -
A session connecting to an MCPServer no longer reconciles it. With the reconciler's duplicate cadence gone (#1285), every server was still reconciled -- and its CR status written -- once per session that connected to it: the aggregator reports each successful session connection to
api.UpdateMCPServerState, which queued a reconcile unconditionally, so every new session's SSO fan-out cost one pass per server it reached, several times per ten minutes on a busy installation, while nothing changed. The handler now only hands the state to the service. A state that actually changes (Auth Required to Connected on the first session, back on the last session's loss) reaches the CRD status through the service's state-change event and theStateChangeBridge, as every other state change does; an unchanged state is logged at debug and costs nothing.TriggerReconcileis gone from the reconcile manager's API; it had no other caller. (#1290) -
Filesystem mode: a status sync no longer rewrites the definition file. The status the reconciler records for an MCPServer or Workflow (state, last error, last connection) was written into the definition's own YAML file: the sync read the file, applied the status and renamed its copy over the original. A spec written by another process between that read and that rename -- an operator's editor, a GitOps sync -- was overwritten with the stale spec, and the reconciler then saw the old definition: the change never happened. In CI this was the flaky
oauth-auth-config-change-resets-live-sessions(the pin of an authorization server lost to a status write of the connect it followed, so the session stayed Connected and the reset never came) andoauth-pinned-authorization-server-change-takes-effect(the same loss, surfacing as the sign-in rate limit). Status now lives understatus/at the definition's relative path (status/mcpservers/<name>.yaml), the definition files are only ever written bycore_mcpserver_create/core_mcpserver_updateand their workflow counterparts, astatus:block left in a definition file by an earlier muster is ignored, and a status write no longer fires the change detector that watches the definition directories. (#1288) -
An MCPServer in sync is left alone. In Kubernetes mode the reconciler logged
Reconciling MCPServer: <name>for every server about every 14 s -- some 250 passes per server per hour, each a definition read and a status write against the API server, while nothing changed. Two periodic sources of the same length ran out of phase: every successful pass requeued itself after 30 s (the status sync from before the periodic resync existed), and the Manager's resync enqueued every server every 30 s on top. A pass that finds the server in sync now asks for nothing further -- the next one comes from a definition change, a runtime state change (StateChangeBridge) or the resync -- and the resync is a safety net for a lost event rather than a heartbeat: its default is ten minutes instead of 30 s.MUSTER_RECONCILER_RESYNC_INTERVALstill overrides it andmuster teststill runs instances at 2 s.DefaultStatusSyncIntervalis gone. (#1285) -
An OAuth server's
spec.timeoutreaches the client a tool call builds on a pool miss. #1284 gave the login-time client of a server withauth.type: oauththe server's timeout; the client the tool-call path builds when a session is already authenticated but holds no pooled connection -- after a restart of the aggregator, after an eviction -- was still built without it (and withoutspec.meta), so the first call after such a restart was cut at the default 30 s although the server declared more. Both are handed to that client too. (#1283) -
An OAuth server's
spec.timeoutreaches the client a person's grant connects. #1282 wired the budget into the token-exchange and token-forwarding clients only; the clientestablishConnectionbuilds for a server withauth.type: oauth-- thecore_auth_logincallback and the grant reuse on first use -- kept the zero timeout, so a tool call on such a server was cut after the default 30 s (no answer within the server's timeout of 30s) although its definition declared more. The entry'sspec.timeoutis read with itsspec.metaand handed to theDynamicAuthClient(newWithTimeout) and to the static-bearer client alike. (#1283) -
A remote server's
spec.timeoutgoverns each tool call. The value bounded the first connect, the health probe and the session-recovery handshake, while the requests themselves -- a tool call above all -- ran under the caller's context alone: a tool that legitimately blocks for two minutes behind a server declaringtimeout: 180was cut whenever the caller's connection gave up first, and muster reported the cut as a transport failure of the POST (transport error: failed to send request: Post "http://..."). Now every operation on a remote client -- each request, and a recovery handshake on the way -- runs under the server'sspec.timeout, the CRD's default of 30 s when unset, so the documented range (1-300) holds end to end; a call that outlasts the budget fails withno answer within the server's timeout of 60s: context deadline exceededrather than a transport error. The session-scoped clients of servers that need a sign-in get the same budget through their registration. (#1281) -
A live session follows its MCPServer's auth configuration. When an MCPServer's
spec.authchanged underneath sessions connected to it --forwardTokenreplaced by a pinnedauthorizationServer, another issuer,expectedIssuer, grant scope or client Secret, a changed token exchange -- every live session kept its state for the server from before the change:muster auth statussaidConnected, a tool call failed with the backend's 401 asTool execution failed: failed to call tool: transport error: authorization required,muster auth login --server <name>refused (already connected and does not require authentication), and nothing short of signing out of muster ended it, while the server itself readAuth Requiredall along. Three things changed. The aggregator puts the server back toauth_requiredfor every live session when the changed configuration registers (session_auth_resetin the log; a restart or a retry on an unchanged configuration leaves the sessions be); the session's cached tools stay resolvable, so its next call answers with the sign-in. A tool call the session cannot make -- it is not signed in to the server, or the backend refuses its credential with a 401 during the call -- answers theauth_requiredchallenge with the sign-in link for that server, the answercore_auth_logingives (marked as an error: the tool did not run;structuredContent.authUrlcarries the link), neveruser not authenticated to serveror a transport error; the refused connection is retired on that call, without waiting for the transport's three retries, and an SSO server's answer says to sign in to muster again. The streamable HTTP transport's bare 401 (authorization required) is now recognized as one wherever muster tests for a 401. Andmuster auth logout --server <name>signs the session out of that one server at the aggregator (core_auth_logout) and prints what happened to the person's grant; it used to print guidance and change nothing, and--servernext to--endpointsigned out of the aggregator instead. Scenariosoauth-auth-config-change-resets-live-sessionsandoauth-backend-401-answers-auth-required;test_pin_mcpserver_authorization_serverdropsforwardToken/tokenExchangewhen it pins. (#1276) -
muster call-- and every other command that connects to an aggregator -- fails fast withauth_requiredinstead of opening a browser, and a rejected token is no longer removed from the store. Without a usable token a command now exits with status 2 at once; the error starts withauth_required, namesmuster auth login --context <ctx>(or--endpoint <url>) and the new--loginflag, which opens the browser from the command itself for a person at a terminal (--auth autoandMUSTER_AUTH_MODE=autostill do the same; the default--authmode is nownone, and a stored token is used and refreshed in every mode). A script, an agent or a CI job can never complete a browser flow, and inside a tool call the old behaviour hung until killed. The 401 handling of the commands, ofauth statusand of the agent's re-authentication no longer deletes the token file first: several muster processes share~/.config/muster/tokens/, the ID token in the file serves other clients, and a sign-in replaces the file only when it completes. The store writes each token file atomically (temporary file and rename), so a process reading an endpoint's file while another refreshes it sees the old or the new token and never a truncated file it would take for a missing one; every write and removal is logged at debug with the pid and the command words of the process.auth logoutremoves the current context's token only,--allevery token. (#1273) -
Plain
muster auth loginrenews an expired stored ID token. The renewal decision hung on the handler's status path, which reachesAuthenticatedonly 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 answeredAlready authenticatedwhile the decision never saw the ID token (muster auth statusthen saidNo authentication required). The decision now reads the ID token'sexpfrom the token file alone, before the "already authenticated" answer and without a probe; a session without an ID token is renewed too. (#1268) -
No
Warning MCPServerRecoveryFailedfor 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 inAuth Required. Recovery now ends there with aNormalMCPServerRecoveryAwaitingAuthevent ("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 stayMCPServerRecoveryFailed. (#1265) -
A
forEachcan iterate a step result's field.items: "{{ .results.<id>.<field> }}"failed every workflow withitems expression … resolved to string, expected a list: a reference deeper than one key below.input,.resultsor.varswas rendered as text, so only a list passed in as a workflow argument could be iterated. Template references that are a pure path now keep their Go type at any depth -- the navigator thespec.outputtemplate already uses -- so a forEach over the pods a previous step listed works,{{ .vars.pod.name }}inside the loop body is the string it names, and a numeric field referenced as a tool argument arrives as a number. Anything more than a pure path still renders to text. The returned document also showed the loop's last result on every iteration's record (the records share the body step's ID); each record now carries its own iteration's result and aniterationindex. Scenarioworkflow-foreach-step-result-items. -
go install github.com/giantswarm/muster/v5@latestbuilds the release it resolves to. Go refusesgo install <package>@<version>for a module whose go.mod carries areplacedirective, and go.mod carried one since 2026-08-28: goldmark pinned to a fixed version for the nancy scan, because golang.org/x/tools required a version OSS Index flagged (#1106). Nothing in the build reaches x/tools or goldmark anymore (go mod why -m github.com/yuin/goldmark: the main module does not need it; neither is among the packagesgo list -deps ./...hands to nancy), so the pin changed nothing but thego installverdict and is gone. The module graph is unchanged. -
muster test: two races of the harness under--parallel 50, both seen once in CI on 2026-09-15.test_restart_mock_oauth_serverlistened again on the port the old server had just released, and any socket on the host could take it in between -- the mocks listen on kernel-assigned ephemeral ports, the range every outgoing connection draws from (failed to listen again on port 45841: bind: address already in use). The mock now keeps its socket bound across the restart and replaces only the server behind it, as a Service keeps its address while the pod is replaced. And a step whose call tomuster servenever returns (await_for_statepoll still pending when its budget ran out;oauth-subject-grant-refreshfailed that way with an instance that kept logging and a harness that kept finishing other scenarios) is reported as stalled instead of "expectations not met", and the runner records where everything was:muster servegetsSIGQUITso its goroutines end the instance stderr of the report, the harness's goroutines are stored asharness_goroutines, and the failure line points at both. The harness also runs the muster binary it is part of before consulting PATH: a stalego installfirst on PATH ran the suite against the wrongmuster serve. -
The release binaries report their release tag again.
muster versionon the v5.22.0 binary printedv1.12.1-0.20260915144925-e6c760a32b48: the CI build stamped the commit and the build time but no version, so the binary fell back to the version Go's own VCS stamping had derived -- and as the module path isgithub.com/giantswarm/musterwith no/v5suffix, Go only considers v0 and v1 tags and turned every v5 release commit into a pseudo-version of the last v1 tag. Because that version is older than every release,muster self-updatere-installed the very release it was running on each time instead of reporting "Current version is the latest." The build now links the tag into the binary (make stamp-version, run bymake testbetween the architect orb writing its link flags and linking with them; a branch build getsgit describe, e.g.v5.23.2-1-g4be8379e), a pseudo-version from the build info is no longer shown as the version, andself-updaterefuses a version that is not a release (dev, a bare commit) instead of panicking on it. (#1256) -
muster start workflow <name>no longer passes the CLI's own flags on as workflow input.--endpoint,--auth,--context,--config-path,--debugand the output flags given after the workflow name reached the workflow engine as arguments, were recorded in theWorkflowExecution(auth: none,endpoint: http://...) and would have collided with a workflow argument of the same name.muster start workflowandmuster callnow tell their own flags apart through cobra's flag set -- one grammar for both, no hand-kept list to drift -- and forward every other--key=valuepair. After--every pair is forwarded as it is, so a workflow or tool argument named like a CLI flag can be passed against a non-default muster:muster start workflow w --endpoint http://muster:8090/mcp -- --endpoint=https://target.muster calldid not leak the flags but discarded everything after--. (#1249) -
muster list tool --server <name>finds the tools of an aggregated server. The flag matched the name against the start of the exposed tool name, which for a server registered asfilesisx_files_<tool>, so--server filesprinted "No tools found" for every aggregated server, and no spelling at all selected a server whosetoolPrefixdiffers from its name.--servernow matches the server a tool belongs to aslist_toolsreports it (files,core,workflow), independent of the tool prefix, and accepts the exposed prefix (x_files) as well;-o wideand-o jsonshow that server instead of the first name segment (x). (#1248) -
muster test: the processtest_restart_instancestarts no longer dies when the restart step returns. It was bound to the step's context, so a restart step with atimeoutkilled the newmuster serveright after it had passed readiness and the next step found the connection reset. -
The DPoP replay cache's Valkey client no longer enables valkey-go's client-side cache: nothing reads through it, and its
CLIENT TRACKINGhandshake failed against a server without the feature, which left muster's OAuth server in degraded mode (service_unavailableon every request). The session stores and the mcp-oauth store already ran without it since v5.19.13. -
oauth.server.allowPrivateIPRedirectURIs(Helm:muster.oauth.server.allowPrivateIPRedirectURIs): a client's redirect URI may resolve to a private address. On a cluster whose own hostnames resolve to an internal load balancer the authorization-time check rejected every platform-hosted client's callback withredirect_uri: hostname resolves to private IP address (DNS rebinding protection)-- klaus-gateway's Slack sign-in ended inserver_error: Failed to start authorization flowright afterallowPrivateIPClientMetadatahad let the client through. Off by default; emits a startup warning when set; exact redirect-URI matching is unchanged. (#1201) -
oauth.server.allowPrivateIPClientMetadata(Helm:muster.oauth.server.allowPrivateIPClientMetadata): a CIMDclient_idURL -- the Client ID Metadata Document an OAuth client such as klaus-gateway self-hosts -- may resolve to a private, loopback or link-local address. Needed where the platform's own hostnames resolve to an internal load balancer (a management cluster reachable only over VPN): until now the SSRF guard rejected such a client withinvalid_client: ... client_id metadata URL resolves to private/internal IP addressand nothing in muster's configuration could lift it, although mcp-oauth has had the switch (AllowPrivateIPClientMetadata). Off by default; emits a startup warning when set; PKCE and redirect-URI validation are unchanged. (#1199) -
oauth.mcpClient.tokenExchange.allowPrivateIP(Helm:muster.oauth.mcpClient.tokenExchange.allowPrivateIP): the token endpoint of a remote Dex muster exchanges tokens with (spec.auth.tokenExchange.dexTokenEndpoint) may resolve to a private or loopback address -- a management or workload cluster whose Dex sits behind an internal-only load balancer. Until now only--extra-ca-filelifted the token-exchange client's SSRF guard, so an exchange against such a Dex failed with a DNS-rebinding error although the endpoint was reachable. Off by default; emits a startup warning when set; TLS verification is unchanged. -
Core tools declare MCP annotations (
readOnlyHint,destructiveHint,idempotentHint,openWorldHint). Everycore_*tool is classified where it is declared: the list/get/validate/probe tools are read-only (core_auth_loginincluded — it issues a sign-in link for the caller and changes nothing on the platform), creates are additive writes, updates/deletes/stops are destructive writes.list_tools,filter_toolsanddescribe_toolreport them like a downstream server's, so the built-inread-onlytoolset preset now includes the 16 read-only core tools and refuses the 13 writes without anytool:selector, and a workflow whose steps call a read-only core tool derivesreadOnlyHint: true. An agent onpreset:read-onlyno longer needstool:core_auth_loginto connect SSO-protected servers. The table is indocs/reference/mcp-tools.md("Core tool annotations"). (#1172) -
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, acore_auth_loginor 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 pinnedtokenEndpointand 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'sbad_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 withauthentication required: server returned 401 Unauthorizedand had to connect again. The in-memory store keeps expired tokens that carry a refresh token for 30 days instead of sweeping them. Logged assubject_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 rawsub, the identity the grant was filed under, not by atrustedIssuerssubjectClaimmapping -- refreshed first when it is due, withissued_token_typeaccess_token andexpires_inthe token's remaining lifetime, never the refresh token. A person without a grant getsinvalid_target(no_grantin muster's log).clientAudiencesgates 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:grantIssueranddexTokenEndpointare 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" indocs/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 serveagainst a local config directory, the test harness). Takes precedence overclientCredentialsSecretRef. -
muster test:muster_broker.clients(confidential broker clients with their audiences) andmuster_broker.grant_targets(audience → mock authorization server) configure brokered exchange for a scenario;test_broker_token_exchangeacceptsclient_id/client_secretfor HTTP Basic client authentication and reports an opaque released token withexpires_in,issued_token_typeandhas_refresh_token; the mock authorization server marks access tokens it issues on a refresh with arefreshed-prefix and OAuth-protected mock backends echo an opaque bearer throughecho_tokentools. Scenarios:oauth-subject-grant-refresh,oauth-subject-grant-refresh-rotation,oauth-broker-grant-release. -
Authorization servers muster cannot discover or register with (GitHub).
spec.auth.authorizationServergains three optional fields:authorizationEndpoint/tokenEndpointpin the endpoints of an authorization server that publishes no RFC 8414 document (muster then performs no discovery for that issuer and assumes S256 PKCE);clientCredentialsSecretRefnames a Secret with a client registered out of band (a GitHub App or OAuth App), which muster uses instead of its CIMD URL or a dynamic registration (clientIdMethod: preregistered);grantScope: subjectfiles the tokens under the user's identity as well as the session, so any later session of the same person reuses the grant without a new consent untilcore_auth_logouton that server or "sign out everywhere". Together they connect the hosted GitHub MCP server -- or any in-house server that verifies GitHub tokens -- to the person's own GitHub account. Seedocs/how-to/connecting-non-rfc9728-mcp-servers.md. -
Subject-scoped grants serve every session of the person on first use. For an MCPServer whose authorization server has
grantScope: subject, a session that never calledcore_auth_loginfor it -- another MCP client, a fresh CLI session, a front-end whose bearer rotated, any session after a muster restart with the grant persisted -- is connected with the person's existing grant by the tool call itself, andlist_toolsshows the server's tools to it instead of listing the server underauth_required. Before, onlycore_auth_loginreused the grant: a plain tool call from such a session failed withuser not authenticated to server <name>(ortool not foundwhile no session of the process had connected the server), so every client had to know to callcore_auth_loginon that error. Concurrent first calls from one session share a single connection; a grant the server rejects with 401 is cleared for the person, ascore_auth_logindoes, so the next login issues a fresh sign-in link. Sessions of a person without a grant fail as before. The same lookup also lets a session that is already authenticated to a server whose authorization server is pinned (spec.auth.authorizationServer, no discovery document) rebuild its connection after a muster restart from the pinned issuer; before, the tool call failed withunable to determine auth method for server <name>until the session rancore_auth_loginagain. -
core_auth_loginaccepts an optionalreset_client_registration: true. It discards muster's stored RFC 7591 dynamic client registration with the server's authorization server and registers again before issuing the sign-in link — the escape hatch for an authorization server whose refusal of the storedclient_idthe automatic checks below cannot see. Other users' tokens from that authorization server may need a fresh sign-in afterwards, since their refresh tokens are bound to the oldclient_id. -
AWS SigV4 request signing for MCPServers.
spec.auth.type: sigv4withspec.auth.sigv4(region, optionalservice, optionalroleArn) makes muster sign every request to astreamable-httpbackend with AWS Signature Version 4, as its own machine identity rather than the caller's, using the default AWS credential chain so IRSA needs no extra configuration;spec.familyplus oneroleArnper MCPServer puts many AWS accounts behind a single tool set with an account selector. Every signed request carriesX-Amz-Content-Sha256inside itsSignedHeaders, so a backend that requires the payload-hash header (S3 and the services sharing its request model, reachable becauseauth.sigv4.serviceis user-settable) accepts the signature. -
spec.metafor remote MCPServers. The map is merged into theparams._metaobject of every outbound JSON-RPC request that carriesparams, for a backend that reads call-scoped configuration from MCP metadata rather than tool arguments — the AWS-hosted server takes the region it operates in fromparams._meta.AWS_REGION, which is a different value from the SigV4 signing region, and omitting it does not fail but answers about the wrong region. Injection happens in an HTTP round tripper shared by every remote client, so it applies tostreamable-httpandssealike, with any auth type and with none, and it survives the switch to the per-session connection that an OAuth server's tool calls run on; for a SigV4 server the round tripper sits in front of the signer, because the body has to be rewritten before it is signed.type: stdiorejects the field (CEL rule plus the same check in filesystem mode): a stdio server speaks over a pipe, so no HTTP transport can inject the entries, and a dropped entry fails nowhere. -
Helm: setting
muster.observability.metrics.prometheus.serviceMonitor.enabled: trueis now sufficient on its own to expose metrics — it implicitly appendsprometheustomuster.observability.metrics.exporter, so the chart serves/metrics, adds themetricscontainer/Service port, and renders the ServiceMonitor from that single toggle (previously the exporter had to be set separately, and forgetting it silently rendered nothing). Explicit exporter lists are preserved:exporter: "otlp"plus the ServiceMonitor toggle yieldsotlp,prometheus; the"none"no-op sentinel is dropped when the toggle is on, soexporter: "none"yields justprometheus. -
OAuth proxy start endpoint (
/oauth/proxy/start): auth-challenge login URLs now point the browser at this muster-hosted endpoint, which redirects to the upstream authorization server. The login flow is unchanged for users; the URL incore_auth_loginresults is now a short muster URL instead of the full upstream authorization URL. -
Browser-consent connector logins now push
tools/resources/prompts list_changednotifications to the user's live sessions once the OAuth callback connects the backend, matching the SSO connect path. Clients that honorlist_changed(e.g. Claude Code) see the newly available tools without re-runningcore_auth_login. -
oauth.mcpClient.postLoginRedirectAllowlist: a list of absolute http(s) URL prefixes. A caller may append aredirectquery parameter to the start URL; when the target matches an allowlist entry (exact scheme and host, path extended at a segment boundary; targets with dot segments are rejected), a successful callback redirects the browser there (with the connected server's name appended as aserverquery parameter) instead of rendering the static success page. Lets a front-end observe connector login completion for the flows it initiated, without affecting other clients of the same muster. Empty (default) rejects all redirect requests; a rejected target is dropped and the login still proceeds. Failed callbacks always render the error page. The Helm chart renders this frommuster.oauth.mcpClient.postLoginRedirectAllowlist(default empty). -
muster test --readiness-timeout: the per-instance deadline for expected resources (tools, workflows, MCP servers) to become available after startup is now configurable (default15s, previously hardcoded). (#1101) -
muster testno longer starts every scenario's instance at t=0: at high--parallelthe cold-start herd (50 Go processes at once) is what starved small CI containers into intermittent 15s readiness timeouts and scenario-timeout kills. Startups are now bounded (--startup-parallel, default 8) without limiting steady-state scenario parallelism, and spawned test instances run withGOMAXPROCS=2so a container that reports the host's core count (CircleCI docker executors allocate CPU by cgroup shares, which Go cannot right-size against) no longer produces fifty host-sized runtimes on a 4-vCPU slice. The suite banner printsGOMAXPROCS/NumCPUso CI logs show the runtime sizing. (#1101) -
MCP
structuredContentfrom downstream tools is now preserved. Thecall_toolmeta-tool propagatesstructuredContentfrom the wrapped tool both natively on its own result and as astructuredContentfield inside its JSON envelope (previously it was silently dropped), and the agent client restores it when unwrapping. Core tools can opt in via the newStructuredContentfield onapi.CallToolResult; none set it yet. Text-only consumers are unaffected.
-
README and documentation reworked. The README describes muster as it is today: an aggregating MCP server with meta-tool discovery, toolsets, OAuth 2.1 protection and single sign-on through Dex, MCPServer and Workflow custom resources, workflows and observability; installation is from the signed release binaries,
go installor the Helm chart in the Giant Swarm catalog. The documentation lost its duplicated getting-started and IDE pages, the invented REST API, service resources,create service, port 8080 and Homebrew instructions, and gained a quick start whose every command is verified, a tutorial for servers and workflows, guides for connecting MCP clients and authenticating the CLI, an installation guide for the chart with Dex and Valkey, an HTTP endpoints reference including the admin listener, and a rewritten contributing guide. The product is spelledmusterthroughout.muster --helpandmuster serve --helpdescribe the current product instead of a Giant Swarm port-forwarding helper. -
describe_toolstates that the tool it describes is only reachable throughcall_tool. Its response gained aninvocationline —Call it through the call_tool meta-tool: call_tool with {"name": "<tool>", "arguments": {...}}, arguments per inputSchema. Tools inside muster are not callable by name directly — only the meta-tools are.— which the meta-tool's own description names, next to what it returns, andcall_tool's own description now states the rule itself ("Aggregated tools are callable only this way, never by their own name"), so a model reads it when it decides how to execute rather than only after a lookup. An MCP client sees only the meta-tools, so a model that took a name out ofdescribe_tooland issued it as a tool call of its own got an error with nothing in the detail it had just read to recover from. The field is additive:description,inputSchema,server,kindandannotationsare unchanged, and the consumers that parse the response (themusterCLI, the REPLdescribe) ignore what they do not know. Scenario:list-tools-paged. -
list_toolsanswers one bounded page instead of the whole catalogue. It takeslimit(default 50) andoffset, reportstotalandtruncated, and projects every entry the wayfilter_toolsdoes —name, one-linesummary,server,kind,annotations— with the full description and the input schema behinddescribe_tool. Before, it returned every tool of the caller's catalogue with its full description in one response: measured on an internal installation, 423 KB unscoped and 402 KB (about 135k tokens) underX-Muster-Toolset: preset:read-only, growing the calling model's next prompt by 105k–120k tokens and staying in its history for the rest of the session. The tool's description now says what a page costs and points atfilter_toolsfor discovery anddescribe_toolfor detail.servers_requiring_authis still present (neither paged nor narrowed by a toolset), a header-declared toolset still bounds the listing (preset:nonelists nothing) and is echoed intoolsetasfilter_toolsechoes it, the refusal texts are unchanged andlist_core_toolsis untouched. ThemusterCLI and REPL listings, tab completion and the test harness page through the catalogue, so they keep listing every tool; the localmuster agentMCP server now advertises the aggregator's meta-tool definitions verbatim instead of a hand-maintained copy, solimit/offsetand the new description reach the assistant through it. (#1193) -
BREAKING (Kubernetes mode):
type: stdioMCPServers are rejected. A stdio server is started as a subprocess of the muster process, so in a deployed aggregator "may write MCPServer resources" implied "may execute code in the muster pod as muster's ServiceAccount" (which holdsgeton Secrets in its namespace and cluster-wide write on the muster CRDs). Kubernetes mode now refuses stdio at every layer:mcpserver_validate/mcpserver_create/mcpserver_updatefail the tool call with a message namingstreamable-http/sseas the alternative; a stdio MCPServer applied straight through the API server reconciles tostatus.state: Failedwith the same message instatus.lastErrorand is never started (an already-running one from an older muster is torn down); and the orchestrator and service layers refuse to build a stdio client at all, so no path reachesNewStdioMCPClient. Deployments with stdio MCPServers must run those servers as their own workload and re-register them withtype: streamable-httportype: sse. Filesystem mode —muster serveagainst a local config directory, i.e. the CLI, where launching subprocesses is the point — is unchanged. (#1067) -
Outbound OAuth flows now send the RFC 8707
resourceparameter on both the authorization request and the token request, for backends and for themuster agentlogin. The value is theresourcefield of the target's RFC 9728 metadata, sent exactly as declared; when that metadata omits the field, orspec.auth.authorizationServeropts out of discovery, it is derived from the configured URL by dropping the query and the fragment and changing nothing else, which is the value mcp-go derives for the refresh request. A token muster obtains for one backend is no longer accepted at another backend that trusts the same issuer, provided the authorization server honors the parameter. An authorization server that rejects an unknownresourcewill fail the login. -
Authorization server metadata is now rejected when its
issuerdoes not identify the server the document was fetched from (RFC 8414 §3.3, trailing slash ignored), and when it carries noissuerat all. Deployments whose authorization server reports an issuer other than the URL muster fetches it from must correct the configured issuer. -
RFC 9728 protected resource metadata advertised by a backend is now bound to that backend. A
resource_metadata=pointer in aWWW-Authenticateheader is followed only when it is on the backend's own scheme and host, and aresourcethe document declares is used only when it is on that same origin (the path may differ, so a backend serving at<base>/mcpmay declare<base>). A pointer to another origin is ignored and discovery falls through to the well-known path; a foreign declared resource is dropped and the indicator is derived from the backend URL. Without these checks a backend could name a document that binds muster's token to a resource the backend does not own. -
Breaking (Go API):
pkg/oauth.Client.BuildAuthorizationURLtakes anAuthorizationRequeststruct instead of six positional arguments, andpkg/oauth.Client.ExchangeCodetakes a trailingresourceargument. Callers pass the same values through the new shapes; an emptyResourceomits the parameter. -
OAuth callbacks are now validated per RFC 9207 before anything else on the response is acted on. A present
issmust equal the issuer the authorization server publishes in its own metadata (simple string comparison, no normalization), and an absentissis refused when that server advertisesauthorization_response_iss_parameter_supported. When the metadata is unreachable the comparison falls back to the issuer recorded with the flow and then ignores a trailing slash on either side, because that value is operator-configured rather than published by the server. A response that fails the check is rejected whole: itserroranderror_descriptionare neither acted on nor displayed. An error response that carries no valid state now renders "missing required parameters" instead of the OAuth denial page. -
The
musterandmuster-crdschart READMEs no longer render a version badge (chart.badgesSectionremoved from bothREADME.md.gotmplfiles): a release PR bumpingChart.yaml'sversionorappVersionno longer changes the checked-inREADME.md, so the helm-docs pre-commit hook no longer fails on it. (giantswarm/devctl#2180)
- BREAKING: JWT mode. muster is not an identity provider: dex is the sole SSO authority. The
enableJWTModeandjwtSigningKey/jwtSigningKeyFileconfig keys, the signing-key loading, and the chart'sjwt-signing-keySecret plumbing are removed; muster issues only opaque access tokens and has no signing key, so no muster-signed token can exist.forwardTokenbackends receive the upstream dex ID token (or, for sessions established by a trusted-issuer bearer, that IdP-issued token) byte-identical — never a muster-signed token — and validate it against the IdP's issuer/JWKS, never against muster. The self-issued RFC 8693 exchange at/oauth/token(which signed muster tokens) is permanently disabled and refuses issuance; agent/OBO flows use dex-issued tokens (exchange at dex, e.g. per-backendtokenExchange/auth.mode: exchange). Deployments that setenableJWTModemust drop the key (the chart schema now rejects it) and reconfigure any backend that trusted muster's JWKS to trust dex instead. This reverses the muster-signed-token direction of the forward-token refactor and supersedes its "backends must trust muster's issuer/JWKS" deployment note; the BDD scenariosobo-token-forward*are replaced bydex-token-forward*, which pin that the forwarded delegation chain is dex-minted and that muster refuses to issue tokens.
-
A new session's first request no longer waits for the SSO fan-out (#1226). A session that arrives with a forwarded or trusted-issuer token -- an agent's session -- had muster connect every session-authenticated server (one RFC 8693 token exchange or ID-token forward per server, in parallel, bounded by the slowest remote authorization server) inside its first request:
initializeand the agent's firsttools/listanswered only after the last server had connected, 2.7-3.2 s on an installation with 81 such servers, althoughtools/listreturns the meta-tools and needs none of the connections. The fan-out now starts on the first request and runs in the background;initializeandtools/listanswer at once. Acall_tool(or resource read, or prompt) for a server still connecting waits for that server's connect alone, resolved from the tool name's prefix or its family, not for the whole fan-out;list_tools,filter_toolsanddescribe_toolwait for the fan-out so a session's first listing is complete. The person's subject-scoped grants (GitHub-style connectors) are adopted in the same fan-out instead of sequentially on the first call that misses them. Requests arriving while the fan-out runs share it; the sign-in flow still connects before it issues the access token. The fan-out logs its total duration, outcome counts and slowest server (SSO: fan-out finished … duration_s=… slowestServer=…), each connect logs its duration, and a held request logs how long it waited (SSO: tool call waited for its server's connect).muster test: a step'smax_durationbounds one invocation and fails the step when it ran longer; an OAuth-protected mock takesconnect_delayto stand in for a backend that is slow to connect. Scenario:oauth-sso-session-start-answers-during-fan-out. -
A muster whose configured Valkey is unreachable at startup no longer serves sessions on in-memory stores for the life of the pod (#1229). The session auth and capability stores were built once at startup; when the Valkey configured in
oauth.server.storagedid not answer at that moment -- muster and Valkey starting together, a Valkey OOM or a node roll while muster restarts -- muster loggedFailed to create Valkey client for session stores, falling back to in-memoryand kept in-memory stores:muster:cap:*andmuster:auth:*never appeared in Valkey while sessions worked, the next restart lost every session's view and SSO state, and the OAuth token store, which retries its own connection, ended up in Valkey regardless. Now muster dials the configured Valkey again with backoff (1 s doubling to 8 s) for up to 20 s (under the chart's 30 s liveness window), loggingValkey for the session stores did not answer, retryingin between and serving nothing until it answers; when it stays away the start fails withConfigured Valkey did not answer; refusing to serve sessions on in-memory storesandmuster serveexits non-zero, so the kubelet restarts the pod. In-memory stores remain the behaviour of a deployment that configured no Valkey. The backend in use is published as the gaugemuster_session_store_backend{backend="valkey|memory"}(1 for the backend in use, 0 for the other). The Valkey client's client-side cache, which no store reads through, is off (CLIENT TRACKINGis no longer requested per connection). -
The core catalogue is refreshed in the background, not on the call that finds it old (#1231). The aggregator's own tools (
core_*,workflow_<name>) are kept for five minutes as the backstop for a definition change no invalidation saw; the meta-tool call that found them older paid the rebuild -- one list of every Workflow resource from the API server plus their conversion, 1.9-2.05 s with 282 workflows -- while a warm session had been answering the same calls in 7-20 ms. Now that call is served the catalogue it has and starts one rebuild in its own goroutine; concurrent calls share it, and the rebuilt catalogue is swapped in when it lands. A call without a catalogue -- the first of the process, or after a workflow was created, updated or deleted -- still waits for the rebuild, so a definition change stays visible on the very next call; a change that arrives while a rebuild runs makes it build again before anything is served. A change in the server registry -- a backend server registered, lost, or re-registered by the reconciler's status sync -- no longer invalidates the core catalogue either: it depends on the tool providers alone, not on which servers are registered, yet every registry change ran the capability refresh that threw it away, so on an installation with 87 servers the meta-tool calls after a muster restart paid the rebuild again and again (1.2-3.3 s calls minutes apart, none of them at an age boundary). Every rebuild is logged at debug (Core catalogue rebuilt, with its trigger andduration_s).MUSTER_CORE_CATALOGUE_MAX_AGEoverrides the age (the integration test harness runs it at 3 s). -
Meta-tool calls no longer rebuild the session's catalogue from the definition source and the capability store on every call (#1225).
call_tool,describe_tool,filter_tools,list_toolsand the resource/prompt meta-tools each listed the caller's catalogue afresh: every workflow definition was fetched from the API server twice per call (once for the tool's arguments, once to derive its read-only hint -- 8182GET workflowsin the two minutes of one agent turn with 282 workflows), and every session-authenticated server's capabilities were read from Valkey with their ownHGETandGET(~160 round trips per call with 81 such servers). Together that was a floor of about 3 s per meta-tool call on a warm session while the backend answered in 22 ms. Now the aggregator's own tools (core_*,workflow_<name>) are built once and kept until a workflow definition changes -- through muster's tools or, for a resource applied outside them, through the workflow reconciler -- with the step tools carried on the tool so the read-only hint is derived in-process; a session's capabilities are read with oneHGETALL, and the content-addressed documents behind the references are decoded once per process and shared by every session that references them; the servers a session is authenticated to are read with oneHKEYS. A warm meta-tool call costs two Valkey commands regardless of how many servers are registered. Toolset scoping, session-scoped visibility and the capability freshness mechanisms are unchanged. -
tools/call responseandErrorlog lines carryduration_s, the time since the request reached the transport, so a slow call is visible in the log without subtracting the request line's timestamp (#1225). -
The boot pass no longer reports an MCPServer the reconciler registered a moment earlier as a failure (#1222). The orchestrator's boot pass and the MCPServer reconciler's first pass run concurrently: for a definition the boot loop had not reached yet, the reconciler found no service and
StartServiceregistered it lazily, and when the loop then got there the refused duplicate registration was logged asERROR Failed to create MCPServer service: <name> ... service <name> already registered-- one misleading error per affected server per boot (two of 87 definitions on one installation) for a service that existed and was being started by the caller that registered it. The registry's duplicate error now wrapsservices.ErrServiceAlreadyRegisteredand the boot pass treats it as a debug-level no-op (MCPServer <name> was registered through StartService while the boot pass ran; that caller starts it); every other error is reported as before and the suspended-server skip is unchanged. -
mcp-oauth's metrics reach muster's metrics endpoint, the token store's included. The OAuth server's instrumentation now records through muster's own OpenTelemetry meter provider (mcp-oauth 1.4.0,
instrumentation.Config.MeterProvider), and the Valkey and in-memory token stores share it. Until now mcp-oauth built a Prometheus collector of its own on the default registerer, which the:9464endpoint -- served from a registry of its own -- never gathered: not oneoauth_*series was ever exported, and the token store was not instrumented at all. Nowoauth_http_requests_total,oauth_token_endpoint_failures_total{grant_type,error_code}and the otheroauth_*series,storage_operation_total{operation,result}(resultissuccess,errorortimeout), thestorage_operation_duration_millisecondshistogram and thestorage_*_countsize gauges are served next to themuster_*series. A Valkey outage shows asstorage_operation_total{result="timeout"}andoauth_token_endpoint_failures_total{error_code="temporarily_unavailable"}. -
A suspended MCPServer is not started when muster boots (#1216). The orchestrator's boot pass created and started a service for every definition with
autoStart: truewithout looking atspec.suspended(whatcore_service_stopand a portal's Deactivate write), and the reconciler's first pass then stopped it again: per restart and per suspended server oneCreating MCPServer service/Starting MCP server service/Suspending MCPServer service <name> (spec.suspended=true)sequence in the log, one connection attempt against a backend the operator had switched off, aMCPServerStartingand aMCPServerStoppedevent,status.lastAttemptmoved to the boot time, and -- when the connect won the race against the stop -- a window in which the deactivated server's tools were registered in the aggregator and callable. The boot pass now skips a suspended definition the way it skipsautoStart: false(Skipping MCPServer <name>: Suspended=trueat debug level); the server keeps readingDisconnected(stdio:Stopped) through the reconciler's no-service branch, its status keeps its pre-restartlastAttempt, andspec.suspended: falsestill starts it.muster test: a pre-configured mock server acceptssuspended: true, which also takes it out of the readiness gate. Scenario:mcpserver-suspended-not-started-at-boot. -
The Valkey capability store keeps each capability document once, content-addressed, instead of once per session (#1217). A session's hash
{prefix}cap:{sessionID}held, per connected server, the server's full tool/resource/prompt list with every input schema — 9–180 KB a server, 0.9 MB a session on one installation — for 30 days, and every forwarded bearer (a Dev Portal, kagent or Backstage ID token) is a session of its own, so identical documents were written again and again: 448 session hashes, 462 MB, and muster-valkey was OOM-killed 19 times in 90 minutes with every token refresh blocked meanwhile. The hash field is now a 71-byte referencesha256:<hex>; the document lives once under{prefix}capblob:<hex>with the store TTL, refreshed by everySetthat references it. Reads resolve the references (one pipelinedGETper distinct document); a reference whose document has expired is a cache miss, which the aggregator already answers by listing the server again on the next connect. Fields written by an earlier muster still read, and on start the aggregator rewrites them to references in the background (MigrateInlineEntries: oneSCAN, idempotent, logged with the session, field, document and byte counts) — an installation shrinks on its first start with this release instead of after 30 days. The store is proportional to the number of distinct documents now (that installation: 84 documents ≈ 1.3 MB, the references ≈ 3.6 MB). Unit tests run the store against an in-process miniredis. -
A suspended remote MCPServer is stopped once, not on every resync tick (#1212). The stop of a remote server (
streamable-http,sse) settles inDisconnected, never inStopped, but the reconciler's suspend path only recognisedStoppedandStoppingas done -- so a server withspec.suspended: truewas stopped again every 30 s for as long as it stayed suspended:Suspending MCPServer service <name> (spec.suspended=true)andStopped service: <name>in the log, anMCPServerStoppedKubernetes event and a second reconcile from the state change on each tick, 13 days of it for one server on a management cluster.reconcileSuspendnow returns early forDisconnectedas well;Failed,ErrorandUnreachableare still stopped, so suspending a server whose endpoint is down still ends its reconnect schedule.Service.Stophas the same guard: called on a remote server that is alreadyDisconnectedit returns without a state write and without an event, as it always did for a local server that isStopped. Resume (spec.suspended: false) is unchanged. Scenario:mcpserver-suspend-remote-stops-once;muster testruns instances withMUSTER_RECONCILER_RESYNC_INTERVAL=2s(a new Go-duration override of the reconciler's 30 s resync) andinstance_logsgainedoccurrences, an exact line count per substring. -
A deactivated MCPServer refuses sign-ins and reports itself down (#1211). An OAuth MCPServer with
spec.suspended: true(whatcore_service_stopand the portal's Deactivate write) keeps its pending-auth registry entry so that sign-ins work again once it is activated -- but that entry also letcore_auth_loginhand out a sign-in link while the server was suspended. The person completed the flow, the session connected, the event handler failed a global registration (Failed to register MCP server … no MCP client available (service state inconsistent)), the reconciler stopped the service again within milliseconds, andauth://statusthen saidconnectedwith a tools count while the session's tool list had nothing for the server; a session that had signed in before the deactivation saw the same contradiction. Nowcore_auth_loginrefuses a suspended server withServer '<name>' is deactivated (spec.suspended=true); activate it with core_service_start before signing in.and creates no challenge; the OAuth callback for a server deactivated between challenge and callback stores the token and establishes no connection (the firstcore_auth_loginaftercore_service_startconnects with it, no browser needed);auth://statusreports a server whose service is down asdisconnectedwhatever the session's auth mark says, with a new"suspended": trueflag naming the reason (muster auth statusprintsDeactivated); andlist_toolsno longer names a down server underservers_requiring_auth. The spec is read through the MCPServer manager -- the source the reconciler acts on -- so the aggregator keeps no copy of it. Theservice state inconsistentERROR turned out not to be the suspension's doing: the event handler tried a global registration on every per-session OAuth sign-in (the skip covered SSO servers only), failed because such a service holds no client, and emitted aMCPServerToolsUnavailableevent the moment a person connected; it now skips a server that authenticates per session the way it skips SSO servers. Scenario:mcpserver-suspended-refuses-auth-login;test_simulate_oauth_callbackgains anauth_urlargument to complete a challenge an earlier step obtained. -
Duration histograms use bucket boundaries for seconds (#995).
muster_tool_call_duration_seconds,muster_downstream_tool_call_duration_secondsandmuster_workflow_execution_duration_secondsare recorded in seconds but inherited the OTel SDK's default boundaries (0...10000), which are spaced for milliseconds: every observation landed in the first bucket and the other 14 series stayed empty, sohistogram_quantileover them could resolve nothing finer than "under 5 seconds". Asdkmetric.Viewmatched on unitsnow applies0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10, 30, 60, 120, 300to every seconds-unit histogram, current and future. The boundaries are documented indocs/explanation/observability.mdnext to the p95 example query. -
A family tool is callable by a session that has not listed tools (#1204). The family routing index is process-global and in memory, and the tools of members that require per-session authentication entered it only when a session authenticated to them ran
list_tools. A muster restart empties the index while the sessions' tokens and cached capabilities persist, so every session that held on across a rollout gotTool execution failed: tool not found: x_prometheus_get_rulesfromcall_toolfor each family tool -- withcore_mcpserver_listreporting the memberConnected-- until it listed tools again; a session that signed in and called without ever listing hit the same.call_toolnow fills the index from the session's view, the passlist_toolsruns, when the name lies in a declared family's name space and the index does not know it, and the call goes through. When the name still cannot be routed, the error says so in terms of the family and the session:tool x_kubernetes_list_podz is not exposed under family "kubernetes" by any member this session is connected to (k8s-a), or, for a session connected to no member, which members exist and thatcore_auth_loginconnects one -- instead oftool not found. Scenario:mcpserver-family-tool-call-without-listing. -
Helm:
MusterMCPServerFailedandMusterMCPServerFlappingsurvive a muster rollout (#1203). Both expressions now aggregate overcluster_id, installation, pipeline, provider, namespace, mcpserver_namespace, mcpserver_name(max byfor the state gauge,sum byfor the transition counter) instead of carrying the scrape labelspod,instance,container,endpoint,service,jobandotel_scope_nameinto the alert. Before, every rollout ended the old pod's series -- Alertmanager sent RESOLVED -- and the new pod's series started a freshforwindow that fired again 20 minutes later: four rollouts in one day produced seven notifications for a single backend that never came back. The alert's identity is now the MCPServer on a cluster; theforwindows, labels and annotations are unchanged, andstateis no longer an alert label (both rules only selectFailed). The promtool suite covers the series moving from one pod to the next mid-window. -
A connected MCPServer whose backend stops answering is detected and reconnected (#493, #999). The orchestrator now probes every connected or running MCPServer with an MCP ping every 30 s (
MUSTER_ORCHESTRATOR_HEALTH_CHECK_INTERVAL, a Go duration); each probe gets the server'sspec.timeout(default 30 s), and a backend that negotiated protocol 2026-07-28, where ping no longer exists, is probed withtools/list. Three failed probes in a row close the client and move the server tofailedwith a reconnect due at once: its tools are withdrawn from every session, oneMCPServerHealthCheckFailedevent names the count and the error, the CR status showsFailedwithlastErrorandnextRetryAfter, and the reconnect loop restarts it on its next tick (MCPServerRecoveryStarted). A reconnect that fails follows the usualMCPServerFailedbackoff schedule whatever the endpoint answered -- a 404 from a route being reprogrammed as much as a refused connection -- until the backend serves again. A passing probe, a successful start or a stop reset the count, so a single slow ping costs nothing; a JSON-RPC error reply, a probe cancelled by shutdown, and a server that is being stopped or restarted are not counted. Servers served per session (forwardToken,tokenExchange, OAuth) have no shared client and are not probed. Before, a server that reachedconnectedstayedconnected/healthywith its tools listed whatever happened to the connection afterwards -- a redeployed, hung or dead backend meant every call failed until an operator rancore_service_restart; the service'sCheckHealth()existed but nothing called it.core_service_statusexposes the running count asmetadata.consecutiveHealthCheckFailures. The aggregator no longer emits a secondMCPServerHealthCheckFailedfor the same transition. Scenarios:mcpserver-health-probe-restarts-dead-backend,mcpserver-health-probe-restart-failure-is-retried,mcpserver-health-probe-spares-session-auth-server,mcpserver-health-probe-spares-per-session-oauth-server. -
filter_toolsechoes a header-declared toolset (#1194). The response'stoolsetfield names the toolset the returned tools were resolved within: thetoolsetargument when one is given, else the request'sX-Muster-Toolset-- as declared, e.g.["preset:read-only"]. Before, only the argument was echoed: an agent whose toolset the platform sets by header gottotal/filtered_countscoped andtoolset_unmatchedfilled but notoolset, so it had no way to learn which toolset bounds it short of a refusedcall_tool, and a transcript did not show which toolset a discovery call ran under. With both header and argument the argument keeps winning (it resolves within the header's toolset).presetsstays behindinclude_presetsor the argument, so a header-scoped discovery call does not grow.list_core_tools, which shares the engine, echoes the header the same way. Scenario:toolset-filter-tools-echo. -
A remote MCPServer recovers its MCP session after its backend is redeployed (#999). A stateful
streamable-httpbackend hands out anMcp-Session-Idat initialize; a redeployed pod does not know it and answers 404, mcp-go drops the id and from then on every request went out with no session, so every tool call failed (Bad Request: Missing session IDfrom a Python backend,Invalid session IDfrom mcp-go) whilecore_service_statuskept reportingconnected/healthy, until an operator rancore_service_restart. On gazelle this took everyx_pd_*tool away for hours. The client now recognises a lost session -- the typedErrSessionTerminated, or a transport whose session id is gone because the notification listener met the 404 first -- re-runs initialize once and retries the call; concurrent callers on the same dead session share one handshake whether it succeeds or fails, a handshake is bounded to the server'sspec.timeout(30 s when unset), one that fails is reported to every caller that waited on it together with the lost-session error (a 401 from the backend still reads asauthentication required), and is retried by the next operation, and a client that was closed on purpose never reconnects. Tool calls, listings, resource reads, prompts and the health ping all go through the recovery, forstreamable-httpservers with and without OAuth; stdio and SSE have no session to lose and are unchanged. Scenario:mcpserver-session-recovery-after-redeploy. -
A restart request on an MCPServer whose endpoint is down is processed by one attempt (#1166).
spec.restartRequestedAtis mirrored intostatus.lastRestartedAtafter 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 -- 131Restarting MCPServer service <name> (restartRequestedAt=...)/Connection failure #Npairs in 53 s on a management cluster whose tunnel was scaled to 0, oneMCPServerFailedevent each, throttled only by the 500 ms debounce, whilestatus.consecutiveFailuresclimbed to 131 andstatus.nextRetryAftersaid "in 2 minutes" the whole time; the loop stopped only when the field was removed from the CR.core_service_startandcore_service_restartwrite 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 aspec.suspended: falseresume 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 logsRestart 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. -
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(default2m; 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.MCPServerFailedevents 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): ..., orno HTTP responsefor a refused connection, a DNS failure or a timeout -- and every failure below the threshold emitsconnection failure N of 3 before unreachable (...). The aggregator no longer emits a second, bareMCPServerFailedon 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.lastAttemptandstatus.nextRetryAfterexisted in the CRD but were never written; they are now mirrored from the service on every status sync, together with the newstatus.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_statusexposes the same fields undermetadata,core_mcpserver_getreturnslastFailureHTTPStatus, and a Failed server whose error is an HTTP 5xx gets the status messageUpstream error (HTTP 5xx) - the server or a gateway in front of it is failing; muster retries with backoff. Scenario:mcpserver-remote-backoff-cap;muster testgainedtest_set_mock_server_outage(a mock server answers its next N requests with a fixed HTTP status) and runs instances withMUSTER_MCPSERVER_MAX_BACKOFF=3s. -
The family tool surface follows what members currently offer (#1162). A
spec.familymember 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/listreturned 40x_capi_*names while every member reported 24,describe_toolnamed members that no longer offered the tool, and calling one failed withtool 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 receivelist_changednotifications when a capability list shrinks, as they already did when it grew. Scenario:mcpserver-family-member-relists-subset;muster testgainedtest_stop_mock_server/test_start_mock_server. -
muster testno longer terminates the instances of anothermuster testrunning at the same time. The start-up sweep for instances left behind by an earlier run matched everymuster serve --config-path .../muster-test-...process on the machine and sent each one SIGTERM, so a second suite started in another terminal -- or the harness's own unit tests undergo test ./internal/testing/, which called the sweep for real -- killed the first suite's instances: scenarios then failed withmuster instance process exited (code 0) before becoming ready, or an instance shut down cleanly mid-scenario. The sweep now terminates only instances whose parentmuster testprocess is gone (re-parented after a crash or kill), which is the case it exists for; instances of a live harness are left alone, and the unit tests run the sweep against a fabricated process table plus two stand-in processes of their own. -
core_auth_logoutnow finds the server's authorization server when the registry entry does not know it yet. The entry learns the issuer from the connection probe's 401 or from a login's discovery, so after a muster restart it is empty for a backend that publishes no RFC 9728 metadata until someone logs in -- while the person's tokens from before the restart are still in the store. Logout skipped every token step in that state and answered "Successfully logged out" with the tokens (for agrantScope: subjectissuer: the person's grant) in place, and did not recognise sibling servers on the same issuer. It now resolves the issuer like a login does --spec.auth.authorizationServer.issuer, else the server's resource metadata -- and matches siblings by their pin as well. Test framework:mcp_servers[].config.oauth.omit_resource_metadatamakes the mock backend answer a bare 401 and serve no well-known document. -
core_auth_logouton a server whose authorization server hasgrantScope: subjectnow revokes the person's grant even when other MCPServers share the issuer. The shared-issuer guard -- which rightly leaves a session-scoped token alone when several servers depend on it -- also skipped the clear for a subject-scoped issuer, so with GitHub's hosted MCP server and an in-house server both pinned tohttps://github.com/login/oauth, logging out of either answered "Successfully logged out" while the session token and the person's grant stayed in the store and the nextcore_auth_loginreconnected without a prompt; the grant could only expire or be revoked on GitHub's side. The logout now deletes the tokens for the issuer from every session of the person and from the grant, disconnects every server of the issuer (auth mark, cached capabilities, pooled connection) in the calling session and in the person's other live sessions, tells those sessions their tool lists changed, and names the sibling servers it disconnected in the tool result. A session-scoped shared issuer keeps the previous behaviour. Test framework:mcp_servers[].config.oauth.grant_scopepins the referenced mock OAuth server as the MCPServer's authorization server with that grant scope. -
The aggregator's registry entry for an MCPServer now carries the resource's own namespace. No registration path passed
metadata.namespaceon, so every server was filed underdefault: aclientCredentialsSecretRef(spec.auth.authorizationServerorspec.auth.tokenExchange) that named the MCPServer's own namespace logged a spuriousCross-namespace secret access ... from MCPServer in namespace defaultwarning on everycore_auth_login, and the token-forwarding, token-exchange and authentication-loss events muster files for the server were attached to an MCPServer of that name indefault. The warning now fires only when the Secret is read from a namespace other than the MCPServer's; which Secret is read is unchanged.core_mcpserver_getandcore_mcpserver_listreport the namespace in Kubernetes mode. -
muster servewithkubernetes: trueno longer falls back to filesystem mode when the Kubernetes client cannot be created at startup. The fallback was silent (one debug-level line) and left muster split in two: the reconciler's change detector still watched the apiserver, so every existingMCPServerCR arrived as a create event, was looked up in the empty config directory as "not found" and had its service deleted (Deleting MCPServer service: …for each CR), while the orchestrator's auto-start listed zero definitions — muster served no server tools, and the CRs kept their staleConnectedstatus because the status writes went to the filesystem as well. It happened after a node restart, when the muster container came back before the apiserver (and the kube-proxy rules in front of it) did and the CRD discovery call failed. Startup now retries the client for about half a minute (client.KubernetesClientBackoff, each attempt logged) and then exits withkubernetes mode is configured but no Kubernetes client could be created … refusing to fall back to filesystem mode, so the kubelet restarts the container; the change detector also takes its mode from the client that was actually created rather than from the configuration flag, so the two can no longer disagree. Automatic detection (no configured mode) keeps the fallback, now logged at warning level. (#1143) -
A forwarded-token rejection is now attributed to the token's audiences, not only its issuer. When a
forwardTokenbackend answers the forwarded ID token with 401, the log line and theauth://statusdetail used to name the token'sissand hint at the issuer's JWKS — which sent an investigation towards JWKS and egress when the backend had in fact trusted the issuer and refused the token's audience (agent-manager on gazelle: the Backstage client's audiences were not in its trusted set). The diagnostic now readsforwarded token iss=…, aud=[…]; the backend must trust this issuer's JWKS and one of these audiences, and when the backend's 401 carries aWWW-Authenticatechallenge itserroranderror_descriptionare quoted as well (agent-manager sentno identity token to act with towards the Kubernetes API; muster used to drop the header at the transport).auth://statusreturns the same account in the server'serrorfield next toreauth_required/sso_attempt_failed, so a client can show why SSO failed instead of a generic pointer to an administrator;muster auth status --server <name>prints it asReason:. Audiences are client identifiers, and the token itself is never logged. (#1141) -
An MCPServer configured for session-level auth (
spec.auth.forwardToken, ortokenExchange.enabled) is no longer registered globally with a token-less client when its backend answers muster's connection probe anonymously — for example the old, anonymous pod during a rollover to an OAuth-protected release. The decision to handle a server per session now comes from the server's own definition rather than from the aggregator registry entry that only exists after a registration (and is gone while the reconciler restarts a changed server): the service treats the accepted anonymous probe like a 401 — the probe client is closed, the server reportsAuth Required, and every session connects with the caller's own token — and the aggregator refuses to attach a shared client to such a server on any path. Previously the shared client 401-looped once the protected pod took over and every session's tool call failed withauthorization requireduntil muster was restarted. A behavioral scenario (oauth-sso-forwarding-anonymous-backend) covers both a server created withforwardTokenand one that gains it throughcore_mcpserver_update. (#1135) -
A pending-auth registration is no longer dropped by the aggregator's handling of a stale service state-change event. The auth-required hook registers the entry synchronously on the server's 401, while the
startingevent from the same start is still queued for the aggregator's event handler; the handler declines to deregister a server whose entry says auth_required, but that check and the removal were two steps and the registration could land in between (theRegisteredAtguard did not catch it when the entry was created before the deregistration was timestamped).core_auth_loginthen answered "Server not found" until the periodic pending-auth heal ran, up to 5 s later. State-change-driven deregistration now decides under the registry lock and keeps entries that require per-session authentication; the paths for a deleted service are unchanged. -
The workflow executor's debug logging no longer prints workflow arguments, resolved step arguments, step results, template results or the final result. Those are user data -- a token passed as a workflow input, a credential returned by a step -- and were dumped verbatim with
%+vat debug level. The lines now carry the workflow name, step id, argument and result key names, value shapes and sizes only. A behavioral scenario (workflow-args-never-logged) runs a secret through a workflow and asserts it never reachesmuster serve's output. -
Stored RFC 7591 dynamic client registrations now heal themselves when the authorization server forgets them. Registrations without an expiry lived forever in muster's credential store, so an authorization server that lost its client registry (pro keeps registered clients in memory; its pod was evicted during a node roll) answered every sign-in in the user's browser with
400 invalid_client— a failure muster never saw — until an operator deleted the Valkey entry by hand. Before a sign-in link is issued with stored DCR credentials muster now verifies them: via the RFC 7592 client read when the registration response providedregistration_client_uri, otherwise by asking the authorization endpoint with a client_id/redirect_uri-only request whose RFC 6749 §4.1.2.1 failure shape reveals whether the client is still known (a redirect to muster's own redirect_uri: alive; a directinvalid_client: gone; anything else: inconclusive, credentials kept). A registration found dead is dropped and re-created within the samecore_auth_login, logged at INFO.invalid_clientfrom the token endpoint during the code exchange, orerror=invalid_clienton the callback, drops the stored registration too, so the user's retry registers again. -
Test framework: expectations are now evaluated by a single implementation for both step kinds, so an assertion means the same thing whether it is written on an MCP tool step or a
test_*step. The two paths each carried their own copy of the checks and had drifted three times —json_path(#1036),not_containsandwait_for_state(#1078) were each implemented on one path and silently ignored on the other, so a scenario declaring one on the wrong kind of step passed regardless of the response. Each path now only adapts its own response shape and shares the checks. Two behaviours are unified in the process:error_containsis evaluated whenever it is declared (the test-tool path previously skipped it unlesssuccess: false), and asuccess: falsepayload now fails an MCP step that expected success (previously checked only fortest_*steps). All 186 scenarios are unaffected by both. A new test asserts that every field ofTestExpectationis enforced on both paths, and fails the build when an expectation kind is added without being wired up — the class of bug behind #1038. -
Test framework:
status_codein a step'sexpectedblock is now rejected at load time, with a message pointing atjson_path. It was accepted by the schema and printed by the reporter but never evaluated by either path, so a scenario asserting an HTTP status asserted nothing. No scenario in the repository used it. -
Test framework: a step's
retryblock (count,delay,backoff_multiplier) is now rejected at load time, with a message pointing atexpected.wait_for_state. It was declared onTestStepand range checked by the loader, and nothing else ever read it — so a step declaringretrygot exactly one attempt while reading as if it polled, andTestStepResult.RetryCount(printed by the reporter) was always zero. The three steps in thedex-token-forwardscenario that used it to poll for eventually-consistent state — backend registration, the OBO reconnect, and the forwarded-token assertion — were therefore flake-prone in precisely the place their author had covered; they now usewait_for_state, which is the mechanism that works.RetryConfigandRetryCountare removed. A new test asserts that every field ofTestStepnames the file that acts on it, and fails the build when a field is declared and validated but never honoured — the same class of bug as #1038, one level up from expectation kinds. -
Test framework:
wait_for_stateon an MCP tool step no longer discards a first response that already satisfies the step's expectations. The polling loop ignored the response its caller had just obtained and judged the step only from the first tick onward, so every such step paid a mandatory poll interval, and a step was failed outright when re-invoking the tool did not return the same answer — reported against the first, passing response, so the output showed a satisfied payload on a failed step. Steps whose invocation is the assertion (a token-forwarding call, anything not idempotent) were the ones at risk. Thetest_*path already judged the first response; the two now agree, which is the parityTestEveryExpectationKindIsEnforcedOnBothStepKindsexists to defend.dex-token-forwarddrops from 2.5s to 0.5s as a result. -
Helm:
values.schema.jsonno longer rejects valid chart values.podDisruptionBudget.minAvailableaccepts a percentage string ("50%") as well as an integer,podDisruptionBudget.maxUnavailableis now a declared value (the PDB template already honoured it, but the schema refused it as an unknown key), and the free-formgatewayAPI.httpRoute.{labels,annotations}/gatewayAPI.backendTrafficPolicy.{labels,annotations}maps accept arbitrary keys. Setting any of these previously failed athelm install/helm templatetime withvalues don't meet the specifications of the schema(s). -
Security (aggregator): a muster without inbound OAuth no longer shares one client's downstream logins with every other client. Without a bearer token there is no token family to key a session by, so the transport layer injects the
default-userplaceholder — and that constant was used as the session key, collapsing every concurrent MCP connection onto a single session. Session-scoped state is keyed by that ID: the OAuth token store, the capability store that backs session-scoped tool visibility (ADR 006), and the session connection pool. So once any one client rancore_auth_loginagainst an OAuth-protected backend, that backend's tools appeared inlist_toolsfor — and were callable by — every other client of the same muster, using the first client's token. Requests that carry no authenticated identity are now keyed by their MCP transport session instead (one key per client connection, per ADR 006 §4.1); stdio reports a constant session ID, so the inherently single-user stdio transport keeps one stable session. Consequence for unauthenticated HTTP: a session ends with the connection, so a reconnecting client re-runscore_auth_loginfor its downstream backends. Enable muster's own OAuth to get durable per-user sessions that survive reconnects. Covered by thesession-multi-user-progressive-authandsession-multi-user-tool-isolationscenarios, whose isolation assertions previously passed vacuously (see below). -
Test framework:
expected.not_containsis now evaluated fortest_*steps. The scenario loader accepted the field and the regular tool path implemented it, but the test-tool path only logged a warning and passed, so every absence assertion written on atest_*step was vacuous — including the session-isolation assertions insession-multi-user-progressive-authandsession-multi-user-tool-isolation, which is why the leak above went unnoticed. It now fails the step when the response contains an unexpected string, matching the non-test-tool path. -
Dex cross-client audience scopes are resolved per authorization request instead of once when the OAuth server is built. An MCPServer with
forwardToken: truethat registers after muster starts now reaches the next login, so the forwarded ID token carries the audience its backend expects. Existing sessions are not repaired: affected users must log in again once. A background refresher re-reads the set every 10 seconds, so no login costs an MCPServer list call, and a read that fails serves the last known set instead of an empty one. -
A change in the cross-client audiences muster requests from Dex is recorded as a
dex_audiences_changedsecurity audit event, alongside a log line. The set comes from MCPServerrequiredAudiencesand now changes without a muster restart. -
Listing MCPServers reports a read failure instead of reporting an empty list.
core_mcpserver_listnow reports a failure when the MCPServer definitions cannot be read, auto-start reports the failure instead of starting no servers silently, and periodic resync logs it instead of treating it as "no MCPServers exist". -
Helm: the NetworkPolicy and CiliumNetworkPolicy now allow ingress to the prometheus metrics port when the prometheus exporter is enabled. Previously both policies allowed ingress only to the aggregator port, so Prometheus / Alloy scrapes of the
/metricsendpoint were blocked (up= 0) even with the ServiceMonitor in place. -
oauth.mcpClient.cimdsettings now reach the OAuth proxy: operator-configuredcimd.scopesare advertised in the served CIMD document (previously always the defaults), and a customcimd.pathno longer breaks CIMD self-hosting (the client ID was derived with the configured path while the OAuth manager recomputed it with the dropped, defaulted one, so the document could be skipped or mounted at the wrong path). The CIMD block was lost in the same config conversions as thepostLoginRedirectAllowlistfix below; that duplication is now collapsed — the aggregator carries the mergedOAuthMCPClientConfigunconverted (theaggregator.OAuthProxyConfigmirror struct is removed), so futureoauth.mcpClientfields reach the OAuth manager without per-field plumbing. -
oauth.mcpClient.postLoginRedirectAllowlistis now honored in deployments. The parsed value was dropped in the two field-by-field config conversions between the YAML config and the OAuth manager (internal/app→aggregator.OAuthProxyConfig→oauth.NewManager), so the handler's allowlist was always empty and everyredirectrequest on the start URL was rejected withRejecting post-login redirect target not in allowlist, degrading connector logins to the static success page. A configured allowlist now reaches the handler; thePost-login redirect allowlist enabled with N entriesstartup log confirms it. -
Reading an aggregated resource or prompt from a backend that reported connected but had no live client crashed muster with a nil-pointer panic (
AggregatorServer.ReadResource/GetPromptdereferenced the client).GetClientnow returns an error in that state instead of handing back a nil client, so the tool call fails cleanly rather than aborting the connection. -
SSO sessions on token-exchange backends no longer deauthenticate under a re-exchange rotation storm. A long-lived SSO session reconnecting after its login-time ID token expired re-inited SSO in
initSSOForSessionfrom the live request context but never persisted that token to the OAuth-proxy store, so muster's background re-exchange (running on a detachedcontext.Background()that can only read the store) fell back to the in-process refresher and rotated the client's mcp refresh token on every ~1s continuous-listen retry (~56×/min for ~15 min); two rotations eventually collided and OAuth 2.1 reuse detection revoked the whole token family → deauth.initSSOForSessionnow persists the request-context ID token, and the background SSO refresher is routed to mcp-oauth's provider-onlyRefreshSessionProvider, which repopulates the upstream provider token without rotating the client-facing refresh token. (#37164)Deploy note: the accompanying mcp-oauth bump (
v1.0.10→v1.2.0) changes the Valkey provider-token storage layout to a single shared entry per user with no legacy read-fallback, so every user re-authenticates exactly once on rollout (existing sessions do not carry over). Optionally flush the affected token keys (token:*,refresh:*, and relatedmeta:*/family:*/ user-client set keys under the configured prefix) for a clean cutover; skipping the flush is safe — leftover keys are never read and just linger until TTL. -
Team ownership label.
application.giantswarm.io/teamnow rendersbumblebeeinstead of an empty string: the labels helper looked up the annotation under the wrong key (application.giantswarm.io/team) instead of the OCI keyio.giantswarm.application.teamset inChart.yaml.
-
localMint downstream auth. The
auth.localMintMCPServer CRD field and its admission rules, thelocal-mintbroker target type and the targettypekey, and theoauth.server.tokenExchangeBroker.delegateToSelfconfig key are removed. Backends that used localMint switch toforwardToken: trueand validate the forwarded token against muster's JWKS.Upgrade note: applying the new CRD makes the Kubernetes API server silently prune
spec.auth.localMintfrom existing MCPServer resources — the schema is structural, so there is no validation error and a GitOps apply succeeds. The affected backend is then left with no downstream-auth mode: muster's calls reach it unauthenticated, the backend answers 401, and its tools disappear from sessions without any apply-time failure. Migrate every MCPServer that setsauth.localMinttoauth.forwardToken: true(with the backend configured to trust muster's issuer/JWKS) before or together with this upgrade. -
The
X-Actor-Tokenrequest header. The actor token is presented once as the RFC 8693actor_tokenparameter at/oauth/token;/mcprequests carry only the issued on-behalf-of token as the bearer.
-
muster asks for MCP protocol version
2025-11-25instead of2024-11-05whenever it acts as an MCP client: every downstream transport (stdio, SSE, streamable HTTP, dynamic-auth) and the agent client. A backend that supports only an older revision answers with that one and keeps working. (#1029) -
core_service_statusandcore_mcpserver_getreportprotocolVersionfor a connected MCP server: the revision that backend answered with during the handshake. The value can differ per backend. -
The aggregator no longer advertises the
resources.subscribecapability.resources/subscribenow returnsMETHOD_NOT_FOUND.resources.listChangedis unchanged and still fires. (#1030) -
muster refuses to start when a
tokenExchangeBrokertarget lacksdexTokenEndpoint, naming the misconfigured audience, instead of surfacing an unattributed error on the first exchange request. The chart'svalues.schema.jsonrequires the key as well. -
A token-forwarding connect failure now logs the forwarded token's issuer (the
issclaim only — never the token) with a hint that the backend must trust that issuer's JWKS, so a backend that does not yet trust muster's issuer is attributable from the log instead of a bare initialize error. -
The aggregator forwards the validated inbound bearer to
forwardTokenbackends on each request instead of issuing a per-backend token, so the on-behalf-of token (including its nestedactdelegation chain) reaches the backend byte-identical. When the request carries no forwardable bearer (the background listen stream, opaque-token sessions), the session's stored upstream ID token is forwarded as before. -
On-behalf-of token exchange at
/oauth/tokenno longer requires a broker target: a request without anaudiencetakes mcp-oauth's self-issued path and the issued token'sauddefaults to muster'sresourceIdentifier. Requests with anaudiencekeep the brokered downstream Dex exchange. -
The self-issued exchange only mints tokens for muster's own audience:
TokenExchangeAllowedResourcesis pinned to theresourceIdentifier, so a request naming any other RFC 8707resourceis refused withinvalid_target. Previously the allowlist was unset (disabled) and any caller holding a trusted-issuer token could obtain a muster-signed token for an arbitrary audience. Tokens for other audiences go through the brokered path, which requires client authentication and a per-client audience allowlist. -
M2M (machine-to-machine) token exchange. The
oauth.server.tokenExchangeBroker.workloadAudiences,workloadGroupGrants, andactorDelegationPolicyconfig keys are removed, along with broker-granted identity injection (granted.subject/granted.groups). On-behalf-of (OBO) delegation is unchanged and now accepts any actor validated against the trusted issuers; the impersonated subject's downstream authorization governs access. -
The
github-appbroker target type and itsgithubAppconfig block. The only remaining broker target type isoidc-exchange.
-
muster agent --mcp-servernow honorsMUSTER_OAUTH_CALLBACK_PORT. The agent's OAuth flow hardcoded port 3000 for itsAuthManager, while the environment variable was read in exactly one place that this path never consulted — so with 3000 occupied, setting the variable mademuster auth loginwork while the agent still bound 3000 and failed, and the port-in-use message interpolated the env-derived port, naming a port the failing code never used.cli.GetCallbackPortis now the single resolver for every OAuth entry point, and thecmdpackage's duplicateDefaultOAuthCallbackPortconstant is removed.docs/reference/cli/agent.mdlists the variable now, instead of leaving it documented as belonging tomuster auth loginalone. The re-auth failure message the MCP server returns no longer names port 3000 either: the number was written into its text, so it stated the default even when the flow had been configured onto another port -- and when the cause really is a bind failure, the wrapped error printed above it already reports the port that was tried. (#1111) -
The CLI auth handler registry no longer serves a closed adapter, and get-or-create no longer orphans one. Get-or-create was a check-then-act --
GetAuthHandler, construct,Register,GetAuthHandleragain:handlerMutexmakes the individual registry calls atomic but leaves the composite racy, so concurrent callers each built and published their own adapter and every loser was orphaned withoutClose(). Both sites that spelled it that way -- the auth commands and theToolExecutorbehindmuster list/get/call-- now go through the new primitive. (noSilentRefreshstill lives on the shared adapter, so the caller that registers it fixes the value for the process; that is unchanged here, and no muster command runs a second registrant.) Separately,AuthAdapter.Close()tore down its managers but never unpublished, soapi.GetAuthHandler()kept returning a closed adapter that — carrying no closed flag — silently re-created managers on itself instead of failing. Newapi.GetOrRegisterAuthHandlerruns the check, the construction and the publish under one write lock so the factory runs exactly once, andClose()now clears the registration throughapi.UnregisterAuthHandler(a no-op when the adapter is not the registered one). (#1111) -
Token re-exchange on the persistent oidc-exchange connection now uses the resolved per-connection config. The initial-connection path handed the refresh closure the shared spec-only
TokenExchangepointer (which, since #940, deliberately never carries the runtime-resolved credentials or appendedrequiredAudiencesscopes), so every re-exchange after the first token neared expiry ran without client credentials — failing outright against a Dex requiring client auth and evicting the session back toAuth Required— and without the required audiences, minting tokens the downstream server rejects. (#942) -
Data race in the token-forwarding connection header function, which mutates a per-connection failure counter that mcp-go invokes concurrently from the listener goroutine and tool-call goroutines, so under load the stale-connection eviction could double-fire (double eviction/revoke). Counter access is now synchronized. (#939)
-
Token-exchange (oidc-exchange) backends no longer get stuck in
Auth Requiredonce their exchanged token expires. The persistent aggregator connection now re-exchanges a fresh token before expiry, and evicts itself only when the subject token can no longer be refreshed. -
MCPServers using RFC 8693 token exchange no longer restart on every reconcile pass (~every 10-15s), which previously caused intermittent
authentication required/user not authenticated to servererrors on tool calls landing in the restart window. (#37060) -
filter_toolsquery ranking now weights tool-name matches over description matches and down-weights the ubiquitous CRUD verbs (list,get), so an intent query is driven by its discriminating noun rather than a generic verb. Previously name and description were concatenated into one BM25 document scored token-equally, solist podssurfaced pagerdutyx_pd_list_*andcore_*_listabovex_kubernetes_listand the pod workflows (the genericlisttoken dominated). The ranker is now a BM25F-style field-weighted lexical scorer: name and description are scored as separate length-normalised fields, name matches weigh higher, and stop-verb contributions are scaled down (down-weighted, not dropped, so alist-only query still ranks list-shaped tools). (#931) -
Workflow validation no longer requires a top-level
toolon parallel/forEach container steps. The reconciler'svalidateWorkflow(the path that setsstatus.valid/status.validationErrorsfor CRD-applied workflows) unconditionally demandedstep.tool, so any workflow whose only tool-less step was aparallelgroup orforEachloop was wrongly marked invalid (step '<id>': tool is required) even though it executed correctly. Validation now mirrors the structured create/validate path: a step must specify exactly one oftool/forEach/parallel, container sub-steps still require their own tool, andstatus.referencedToolsnow includes sub-step tools. (#928) -
Workflow listing no longer rebuilds the session tool set once per workflow.
getWorkflowsevaluated each workflow's availability independently, and every check resolved the caller's full session-scoped tool set (GetAllToolsForSessionacross all backend MCP servers) from scratch — an O(workflows) blow-up that madecore_workflow_listtake ~30 s for ~280 workflows. The list path now installs a request-scoped memo (api.SessionToolMemo) so the session tool set is resolved once for the whole request and shared across all per-workflow availability checks. Single-workflow paths are unchanged (no memo, same per-call behavior). -
local-mint backends are now treated as session-based for tool registration.
isServerSSOBaseddid not recognize the local-mint auth mode, so the aggregator event handler attempted global registration for a local-mint backend (which has no global persistent client), failed on 401, and the backend's tools never reached the caller. local-mint now joins token forwarding and token exchange as a per-session auth mode. -
localMint backends connected during SSO bootstrap now mint on the on-behalf-of delegation path. When
initSSOForSessionrebuilt its detached background context it carried the subject bearer and ID token but dropped the inboundX-Actor-Token, so a bootstrap-established connection minted with no actor (the human subject alone) rather than the agent acting on the human's behalf. The per-request credential tokens (subject bearer, actor token, ID token) are now carried into the bootstrap context as one unit, so the bootstrap and live-request paths mint identically. -
Connected MCP clients now receive
notifications/{tools,resources,prompts}/list_changedwhen a backend connects after the session was opened (localMint/OBO background bootstrap, post-restart re-init), so late-connecting backends no longer stay invisible for the session's lifetime. -
OBO sessions now connect localMint backends. After the emission fix that adds
emailto muster-minted OBO JWTs,fireOnAuthenticatedfires for OBO requests, but theonAuthenticatedcallback returned early at theidToken == ""guard (written to avoid 403-spam for post-restart Dex sessions). The guard is now narrowed: OBO sessions (detected viauserInfo.ActorSubject) are allowed through. The inbound OBO bearer is threaded into the detachedinitSSOForSessionbackground context and used as the RFC 8693 subject token inEstablishConnectionWithLocalMint, falling back from the Dex ID-token lookup when none is present. -
On-behalf-of tool calls now reach the backend instead of failing closed. A muster self-issued OBO bearer (
sub=human,act=agent) re-presented at the front door is signature-validated with no token-store entry, so it previously carried no session ID; backend tools that require session auth then rejected it and the connection fell back to the agent ServiceAccount. Bumping mcp-oauth to v0.18.0 makes theValidateTokenmiddleware assign a session to every validated token (FamilyIDwhen present, else the deterministic bearer-derived ID), so the existing session lookup resolves the OBO bearer and the per-backend localMint runs as the human. No muster code change.
-
Durable workflow execution tracking (#930, supersedes the closed-but-unimplemented #35): workflow execution records now survive process restarts and are visible across replicas when muster runs in Kubernetes.
WorkflowExecutionCRD (muster.giantswarm.io/v1alpha1, kindWorkflowExecution, short namewfe). Each workflow run is persisted as one immutable record (metadata.nameis the execution UUID) carrying the workflow name, status, timings, input, result, per-step records, and atruncatedflag. It has no status subresource — it is an append-only record, not a reconciled resource. The chart's ClusterRole gainsworkflowexecutionspermissions and the CRD ships in both themuster-crdschart and the app chart'scrds/directory.- Backend selected by deployment mode. In Kubernetes mode the existing
ExecutionStorageseam is backed by theWorkflowExecutionCRD (via the controller-runtime client already embedded inMusterClient); standalonemuster servekeeps the filesystem backend.core_workflow_execution_list/getbehave identically against either backend. List filtering usesmuster.giantswarm.io/workflowandmuster.giantswarm.io/statuslabel selectors, sorting and paginating in memory. - Payload guard. Before persisting, an execution record whose marshaled size exceeds 256 KB (well under etcd's object limit) has its oversized result and step results replaced with a truncation marker and is flagged
truncated, so a large workflow result can never make a record unpersistable in either backend. - Retention GC. A background goroutine prunes execution records that are older than 21 days or beyond the newest 10000 (burst safety cap), keeping the store bounded without manual cleanup. The clock is injectable so the prune logic is deterministically unit-tested. Retention bounds are constants for v1.
- Execution metrics. New OTel instruments
muster.workflow_executions(→muster_workflow_executions_total),muster.workflow_execution.duration(→muster_workflow_execution_duration_seconds, keyed byworkflow+status), andmuster.workflow_execution.store_errors(→muster_workflow_execution_store_errors_total) are exported via the existing OTel→Prometheus pipeline. A persistence failure is now logged at error level and counted instead of being silently warned, so empty dashboards become an observable signal.
-
oauth.server.trustedIssuers[].allowPrivateIPJWKSHosts([]string): host-scoped alternative toallowPrivateIPJWKS. The issuer'sjwksUrlmay resolve to a private IP only when its hostname matches one of these values; all other hosts keep the SSRF guard. Maps to mcp-oauth'sTrustedIssuer.AllowPrivateIPJWKSHosts. Prefer it over the blanket bool for a known in-cluster JWKS endpoint (e.g. a Dex fronted by an internal LB whose public hostname resolves to a private VIP). -
oauth.server.tokenExchangeBroker.delegateToSelf(default false): when enabled, a delegated (on-behalf-of) token exchange that carries anactor_tokenbut omits the RFC 8707resourceis bound to muster's ownresourceIdentifier, so an agent STS client that cannot set a resource itself still receives a token muster accepts back and re-mints per backend on the localMint path. Only the delegation path is affected; a resource-less plain exchange still errors. Requires mcp-oauth v0.16.0. -
Chart values now document the
muster-valkeypersistence model as a deliberate cache-only choice (RDB-only is intentional; every critical record is reconstructable at startup), with a per-record-type recovery table, and explain why AOF is not enabled (it does not survive PVC loss, and a config-flip restart is a data-loss footgun). Documentation only; no behaviour change. (#884) -
App-owned CRDs: the
musterapplication chart now ships its ownMCPServerandWorkflowCRDs inhelm/muster/crds/(Helm 3crds/directory), withhelm.sh/resource-policy: keepbaked in. Combined with Fluxinstall.crds: CreateReplace/upgrade.crds: CreateReplaceon the musterHelmRelease, the CRDs travel with the app at the same version and upgrade atomically on every release, removing CRD-vs-app drift. The standalonemuster-crdschart is retained for non-Flux/standalone consumers;make generate-crdsnow writes both locations from the same Go-type source. Chart docs (NOTES, README, values) now explain the CRD handling for plain-Helm users: freshhelm installincludes the CRDs automatically, while CRD upgrades must be applied out-of-band (helm show crds giantswarm/muster | kubectl apply --server-side -f -) because Helm does not managecrds/upgrades natively — an intentional, still-current Helm design decision (HIP-0011), unchanged in Helm 4. The previous bundle-split docs that claimed the app chart "no longer ships the CRDs" were corrected. -
Workflow result model overhaul (#873, #874, #875): step result referencing, LLM-facing output, and result shaping are now decoupled and use one expression language.
- Referencing decoupled from output (#873). Every step result is now always referenceable by later steps as
{{ .results.<id>.<field> }}— regardless of any flag — so chaining a single value from one step into the next no longer forces that step's entire result into the response. A new per-step/sub-stepoutputflag controls whether a result is included in the returned document;storeremains as a deprecated, backwards-compatible alias.forEach,parallel, and failure-path results are all referenceable consistently. Workflows that still usestorenow log a one-line deprecation warning naming the affected steps — both on the structured create/validate path and on the CRD reconciler (so akubectl apply-ed workflow is nudged too). - Output template (#874). A workflow may declare a workflow-level
outputtemplate: a templated object rendered once after all steps complete, against.input/.results/.vars, and returned in place of the default{execution_id, workflow, status, input, steps[], ...}response. It can select nested fields and combine values across steps while preserving JSON structure (numbers stay numbers, arrays stay arrays), letting a workflow return a small, shaped response. When omitted, the default response is returned unchanged. Because the output template replaces the response, per-stepoutput/storeflags no longer affect the returned document when an output template is declared; this is flagged with a one-line authoring warning naming the now-inert flags. Type preservation (no lossy coercion): an output template leaf's type comes from the value it evaluates to, never from how its rendered text looks. A bare reference path (e.g."{{ .results.pods.items }}") keeps its original JSON type; a single-action computed leaf keeps the real type of its result, so a numeric expression like"{{ len .results.events.items }}"is a number while a computed string keeps its exact string form. This means values whose form matters — versions, IDs, zero-padded values like"08"or"1.20"— are preserved as-is, with no coercion and noquoteworkaround. A leaf that mixes literal text with actions (e.g."v{{ .v }}") renders to a string. Non-finite values (NaN/Inf) are kept as strings. - Unified expression language (#875). Condition
jsonPath/expectNot.jsonPathpaths now use the same path navigator as step args and templates, gaining array indexing (e.g.items[0].name) and an optional full Go-template form where the result is exposed as.result(e.g."{{ (index .result.items 0).name }}"). Existing dotted paths keep working. The duplicateengine.resolvePathandgetValueFromPathnavigators were collapsed onto a single implementation. - Debug response escape hatch and non-discarding output-template errors (#877). A workflow that declares an
outputtemplate can now be inspected without temporarily removing the template: pass the reserved_debug: trueexecution argument and the full response (execution_id,status, andsteps[]with every recorded step result, not just output-flagged ones) is returned with the rendered output template alongside it underoutput. The_debugarg is stripped before validation and step execution, so it never collides with a workflow's own arguments and is not passed to step tools. Separately, an output-template render error no longer discards the results of steps that already succeeded: the workflow still fails loud (the error is returned and the result is flaggedisError), but the response now carries every recorded step result plus anoutput_errormessage, so the underlying data stays recoverable for debugging an output-template typo. Default (non-debug, template-renders-cleanly) behaviour is unchanged: only the rendered output template is returned whenoutputis set, the default response otherwise.
- Referencing decoupled from output (#873). Every step result is now always referenceable by later steps as
-
Cheap, ranked, faceted tool discovery tier (#868):
filter_toolsis now a discovery tier distinct from execution, so finding a tool no longer scales with the full descriptive weight of every candidate. Against a ~280-workflow fleet a broadfilter_tools(pattern="*workflow*")call returns a bounded summary page (~3 KB) instead of the full-catalogue dump (~330 KB) — measured ~100x smaller.- Bounded + summarised pages.
filter_toolsgainslimit(default 25) andoffset, and the response carriestotaland atruncatedflag. Discovery now defaults to a one-linesummaryper tool with no input schema; the authoritative full description and schema remain available viadescribe_tool, or by passinginclude_schema=true. - Ranked query mode. A new
queryargument relevance-ranks matches with a dependency-free lexical ranker (Okapi BM25 over name + summary) and returns them best-first with ascore, dropping non-matching tools. Lexical ranking needs no embedding index; embeddings can be a later upgrade. - Label facets.
WorkflowCRDmetadata.labelsare now propagated onto the workflow's execution tool and can be filtered in discovery via alabelsfacet (key=value; all must match), letting clients scope a lookup to a labelled subset. - Agent REPL parity. The
filtercommand (aliasesfind/search) now exposes the discovery tier: options are given askey=valuepairs (pattern,description,query,labelsask=v,k2=v2,case_sensitive,detailed,limit,offset), with a bare token still accepted as the name pattern. It now reports the page size against the total match count and the catalogue size, and prints the nextoffsetwhen more matches exist (previously it mislabelled the page size as the match count and hid truncation). Unknown options and stray extra arguments now fail loudly with the list of valid keys instead of being silently ignored, the name column auto-sizes to the longest tool name on the page, and tab-completion only offers options that have not been supplied yet. - Existing
list_tools/call_tool/describe_toolbehaviour is unchanged, as islist_core_tools(which keeps full descriptions and schemas).
- Bounded + summarised pages.
-
Workflow control flow (#865): four genuinely useful constructs are now implemented end to end (CRD types, internal API types, executor, structured create/validate path, and step JSON schema):
condition.template— a boolean Go-template gate evaluated in-process (e.g.condition: {template: "{{ eq .input.env \"production\" }}"}).forEach— a sequential loop that runs a flat body of sub-steps once per item of a list, binding the current item to{{ .vars.<as> }}(defaultitem) and the index to{{ .vars.<as>_index }}. A stored sub-step is addressable per iteration as{{ .results.<id>_<index> }}(the plain{{ .results.<id> }}keeps the last iteration's result).parallel— a group of sub-steps executed concurrently; siblings are independent (each resolves arguments from the pre-group state).spec.onFailure— best-effort cleanup/rollback sub-steps run when the workflow fails on a step that does not allow failure.- The "exactly one of
tool,forEach, orparallel" rule is enforced by the CRD itself via a CEL validation rule, so a malformed step is rejected atkubectl applytime, not only through the structured create path. - Step/sub-step conditions are now validated structurally on every authoring path: a condition must set exactly one of
template,tool, orfromStep, and atool/fromStepcondition must declareexpectorexpectNot. Both rules are enforced atkubectl applytime via CEL on theWorkflowCRD and in the structuredworkflow_create/workflow_validatepath. Previously atool/fromStepcondition without an expectation silently fell back to "expect the call to fail", and a kubectl-applied condition was not checked at all.
-
GET /healthnow responds 200 on the aggregator port regardless of OAuth configuration, so Kubernetes liveness/readiness probes work without patching the chart. -
RegisterServerandDeregisterServeraggregator events and MCPServer reconcile entry are now logged at Info level, making freshly-restarted pod lifecycle visible without--debug. -
oauth.server.allowedOrigins(comma-separated) is now wired into the mcp-oauth CORSAllowedOriginslist. Previously declared but never read; empty value keeps CORS disabled (default). -
oauth.server.trustedIssuers[].acceptedTypHeaders: accepted JWTtypheader values for Bearer tokens from a trusted issuer. Empty keeps the RFC 9068 default (at+jwt). Kubernetes ServiceAccount tokens carry notypheader; use[""]to accept them. -
oauth.server.trustedIssuers[].subjectClaim: sources the canonical subject (thesubof any token minted from the identity) from a claim other thansub. Empty keeps the standardsub. Set it toemailfor Dex, whosesubis opaque, so an OBO token minted from a Dex subject token carries the user's email. -
Brokered RFC 8693 token exchange (#831): external confidential clients can POST a token-exchange request with an
audienceparameter to/oauth/tokenand receive a token minted by the audience's downstream Dex. Newoauth.server.tokenExchangeBrokerconfig block (per-client audience allowlist, audience → downstream Dex target mapping with per-target scopes and credential secret refs). Requires mcp-oauth >= v0.3.0; subject tokens are validated againsttrustedIssuers. -
oauth.server.tokenExchangeBroker.targets[].type: credential provider discriminator for broker targets. Defaults tooidc-exchange(downstream Dex RFC 8693 exchange) when omitted; additional provider types will be added in future releases. -
oauth.server.tokenExchangeBroker.targets[].type: github-appmints GitHub App installation tokens. Configure viagithubApp.appId,githubApp.installationId(orgithubApp.owner+githubApp.repofor auto-discovery),githubApp.privateKeyRef(RSA PEM in a Kubernetes Secret), and optionalgithubApp.repositories/githubApp.permissionsscope restriction. -
oauth.server.tokenExchangeBroker.targets[].type: local-mintmints a muster-signed RFC 9068 JWT locally. RequiresenableJWTMode: true. The issued token carriessub= the validated human subject, the subject'semailandgroupsclaims (plus any broker-granted groups), andact= the validated agent SA nested over any prior delegation chain on the subject token, signed by muster's own access-token key. Downstream services that trust muster as an issuer receive the subject's identity and the full delegation chain without a separate Dex exchange, so group-to-tenant and email-to-org routing resolve correctly. -
muster events --follownow honors--output:jsonstreams newline-delimited JSON (one object per line, ready forjq),yamlstreams one YAML document per event, and the default table/wide formats stream an aligned one-line-per-event view. Previously the output format was silently ignored in follow mode. The human follow line now orders fields to match the staticmuster eventstable (timestamp, type, resource, reason, message) and highlightsWarningevents when stdout is a terminal.
- Kubernetes events now carry their structured detail. The
api.EventManagerHandlerboundary previously dropped everything but the object name/namespace, so every rendered event message lost its contextual fields — failure events showed no error, "created with N steps" showed no count, and step events had empty step IDs/tools. A newCreateEventWithDatacarries structuredEventData(error, step count, duration, step ID/tool, execution ID, condition result, tool names) end-to-end so the message template renders the real detail; all emitters (MCPServer service/aggregator, workflow, token exchange/forwarding) were updated. The event type (Normal/Warning) is now consistently derived from the reason. - Kubernetes events for MCPServer runtime lifecycle (start/stop/fail/health/recovery) are now associated with the MCPServer CRD in the configured muster namespace instead of being hardcoded to
default. On a real cluster the events previously landed indefaultwith an empty object UID, orphaned from the CRD and invisible tokubectl describe mcpserver. muster events --follownow works via real server-side streaming. It previously blocked forever waiting on an MCP notification the server never sent (the streaming path was an unimplemented stub). Follow is now implemented end-to-end:core_eventswithfollow=truereturns the events seen so far and registers a per-session watch on the aggregator; subsequent events are pushed to the client asnotifications/muster/eventMCP notifications, sourced from a native Kubernetes watch (Kubernetes mode) or an fsnotify watch on the on-disk event log (filesystem mode) — no client-side polling. The stream is torn down when the client disconnects, starts a new follow, or the server shuts down.- localMint now refuses to mint when the subject token carries a non-empty
emailwhoseemail_verifiedis not true, on both the delegation (X-Actor-Token) path and the no-actor path. Previously the check ran only on the delegation path, so a pre-exchanged subject token already carrying anactchain (a single bearer, no actor header) could mint with an unverified email. A ServiceAccount token, which carries no email, is unaffected. - Workflow CRD CEL cost budget (#865 follow-up): the
WorkflowCRD from 0.8.0 was rejected by the Kubernetes API server (k8s >= 1.34) with "x-kubernetes-validations estimated rule cost total for entire OpenAPIv3 schema exceeds budget" — theWorkflowStepandWorkflowCondition"exactly one of" guards used a[...].filter(x, x).size() == 1list-comprehension form whose estimated cost, multiplied across the four nested condition sites (steps[],steps[].forEach.steps[],steps[].parallel[],onFailure[]), blew the schema-wide budget (3.2x over). Rewritten to the cheap additive form(has(self.a)?1:0) + (has(self.b)?1:0) + (has(self.c)?1:0) == 1, which estimates far lower and applies cleanly on k8s 1.34 while enforcing identical semantics. Without this,muster-crds0.8.0 cannot install and the muster 0.8.0 rollout stalls. - Workflow documentation (#865):
docs/how-to/workflow-creation.mdanddocs/how-to/ai-workflow-optimization.mdwere rewritten to describe only features the engine implements; all example templates now use the correct{{ .input.<arg> }}context (the engine renders withmissingkey=error, so the previously documented{{ .<arg> }}form errored at runtime).docs/reference/crds.mdtemplate syntax was corrected, the strayspec.nameremoved from examples, and the deadoutputsfield replaced withstore: trueguidance. - Documentation: removed all references to a hallucinated
muster configure ...CLI fromdocs/how-to/ai-troubleshooting.mdanddocs/how-to/ai-agent-integration.md. muster has no configuration CLI — configuration is file-based (~/.config/muster/config.yaml), entities are created withmuster create, and debugging usesmuster serve --debug. The invented context/tool-suggestion and alert/cache/limit configuration blocks built around that command were dropped. - Documentation: completed a repo-wide accuracy pass removing the remaining hallucinated CLI surface and config from the docs.
docs/how-to/ai-troubleshooting.mdanddocs/how-to/troubleshooting.mdwere rewritten around the real command set; the inventedmuster status,muster logs,muster describe,muster validate,muster restart,muster metrics,muster backup/restore,muster support-bundle,muster config show, the fictionalmuster serve --port/--hostflags, and thekind: Configlogging CRD were replaced with the real equivalents (muster get/list/check/call,muster serve --debug/--silent, the/healthendpoint, OpenTelemetry/OTLP for metrics and traces, and file-based config). The same fixes were applied indocs/how-to/mcp-server-management.md(muster metrics/muster logsmcpserver, and a CursormcpServersentry that usedcurlas the MCP command) anddocs/reference/events.md(muster restart mcpserver). - Documentation: corrected workflow template/feature hallucinations beyond #865 in
docs/how-to/advanced-scenarios.md,docs/how-to/ai-agent-integration.md,docs/reference/mcp-tools.md,docs/reference/configuration.md,docs/reference/api.md,docs/explanation/orchestration.md, anddocs/explanation/design-principles.md: example templates now use the real{{ .input.<arg> }}/{{ .results.<step-id> }}context (engine renders withmissingkey=error), the unsupportedenum/examples/patternarg keywords and strayspec.name/outputsfields were removed, and the fabricatedspec.triggersevent-driven workflows, customTemplateFuncsmap (templates use the Sprig library), inventedmuster_*metric names, and non-existent per-stepretry/on_failure/error_handlingfields were replaced with the realcondition.template, workflow-levelonFailure, and OpenTelemetry behaviour. - Cross-cluster RFC 8693 token exchange now requests an
id_token(wasaccess_token). The exchanged token is forwarded as the downstream bearer and must serve as the user's OIDC identity; Dex's default access token is opaque, somcp-kubernetes(strict--downstream-oauth) could not use it for Kubernetes OIDC and denied tool calls withauthentication required: please log in to access this resource, even though the connection reportedConnected [SSO: Exchanged]. Requesting anid_tokenyields a JWT whoseaudcarries the configuredrequiredAudiences, somcp-oauthaccepts it via the forwarded-ID-token (SSO) path — mirroring the token-forwarding behaviour.mcp-prometheusand other identity-only downstreams were unaffected and keep working. - localMint now carries a human on-behalf-of identity to the backend. When an agent reaches muster with a muster-issued on-behalf-of token (
sub=human, act=agent), localMint presents it as the exchange subject and the broker re-binds it to the backend audience, preserving the delegation chain, so the backend seessub=human, act=agentinstead of falling back to the agent ServiceAccount. - localMint connections no longer 401-loop on their background listen stream. The streaming listener runs on a context with no inbound headers, so the mint previously failed closed and the backend rejected the unauthenticated stream once per second. The connection's subject and actor are now bound at creation and used as the fallback when the request context carries none.
- Token-exchange spec-vs-runtime handling consolidated onto
api.TokenExchangeConfig(#942): the newWithResolvedRuntimemethod is the single place that stamps the per-connection runtime state (resolved client credentials, appendedrequiredAudiencesscopes) onto a value copy, andSpecOnlyis the single place that strips it for CR comparison. Replaces the duplicated build logic in the aggregator's connection and tool-call paths and the hand-rolled field clearing in the reconciler comparison.WithResolvedRuntimereturns a distinctResolvedTokenExchangeConfigtype which the token-refresh closures now require, so handing them the shared spec-only registry definition (the #944 bug) no longer compiles; the tool-call path's refresh wiring is now pinned by its own regression test as well. No behavior change. - Kubernetes event emission is now always on. The previous opt-in gate (
--enable-eventsflag andevents:config field) and its "disabled by default" framing were removed; events are a core observability feature that works in both Kubernetes (native Events) and filesystem (on-disk log) modes. Themuster serve --enable-eventsflag is retained as a hidden, deprecated no-op so existing scripts and unit files keep working after upgrade (it prints a deprecation notice and has no effect); the removedevents:config key is silently ignored. - Kubernetes event spam reduction: the high-volume per-session
MCPServerTokenForwarded/MCPServerTokenExchangedNormal events now log at debug level instead of writing a Kubernetes Event on every session's connection to every SSO server. Token failures are still surfaced as Warning events. The Kubernetes backend now emits events through a client-goEventRecorder/EventBroadcasterso duplicate events aggregate into a single object with aCountand get per-key rate limiting, and the per-pollMCPServerHealthCheckFailedemission is gated on the healthy→unhealthy transition rather than firing every 30s for every unhealthy server. These were the dominant event-volume sources that previously made the feature noisy. - Update mcp-oauth to v0.13.2: the trusted-issuer JWKS client used for
allowPrivateIPJWKSnow honors the process CA bundle, so a backend validating muster's in-cluster JWKS over TLS with an internal CA no longer fails withx509: unknown authority. - Workflow field-name casing is now consistent across authoring surfaces (#865): the structured
workflow_create/workflow_update/workflow_validatetool path now accepts the canonical camelCase names used by the CRD and the documentation (allowFailure,fromStep,expectNot,jsonPath) in addition to the previously released snake_case aliases (allow_failure,from_step,expect_not,json_path), and the advertised tool JSON schema now lists the camelCase names. Previously a workflow authored from the (camelCase) documentation was silently mis-parsed through the MCP tool path — e.g.allowFailurewas dropped, so a step meant to tolerate failure halted the workflow. - Broker credential minting extracted behind a
CredentialProviderinterface and anoidc-exchangeprovider dispatched through a registry (internal/oauth). No behaviour change; the oidc-exchange provider preserves per-(endpoint, connector, user) token caching. - Update mcp-oauth to v0.9.0:
server.TrustedIssuer.SubjectClaimsources the canonical subject from a configurable claim, wired throughoauth.server.trustedIssuers[].subjectClaim. - Update mcp-oauth to v0.8.0:
server.AcceptTrustedIssuerTokenfor accepting a TrustedIssuers-validated bearer as a forwarded credential with the sameext-<hex>session-ID derivation asAcceptForwardedIDToken. - Update mcp-oauth to v0.7.1:
server.LocalMintExchangerfor local RFC 9068 JWT minting; RFC 8693actor_tokenvalidation andactclaim support;providers.UserInfo.ActorIssuer/ActorSubject;oidc.IDTokenClaims.Actauto-decoded fromact. - Update mcp-oauth to v0.4.0.
- Update mcp-oauth to v0.3.1: forwarded ID tokens (
trustedAudiences) are no longer hard-rejected by the trusted-issuer Bearer branch when the same issuer is also configured intrustedIssuers— fixes Backstage AI-chat SSO token forwarding returning 401 (typ header is "", expected "at+jwt") on deployments with the token-exchange broker enabled. - Update mcp-oauth to v0.3.0 (server-side RFC 8693 token-exchange grant with pluggable
Exchanger). muster versionnow derives its version from the Go build info (runtime/debug) stamped from the VCS tag, instead of a hand-maintained literal inpkg/project. Release builds run on the tagged commit, so the binaries report the clean tag version; off-tag builds report a pseudo-version and tag-less builds reportdev.gitSHA/buildTimestampare still injected by architect'sgo-buildldflags. Removes the need to bump the version literal on every release. The scratch files architect'sgo-buildwrites into the worktree (the per-archmuster-<os>-<arch>binaries,.ldflags, and.platforms) are now gitignored so an untracked artifact doesn't mark the build+dirtyin the embedded version. Validated end-to-end in CI: all six architectures embed the clean tag version withvcs.modified=false.
- Removed the
--enable-eventsflag onmuster serveand theevents:config/Helm value. Event emission is no longer gated; the flag and field are gone (existing configs that still setevents:are harmlessly ignored). - Pruned six event reasons that were defined, templated, and documented but never emitted anywhere:
MCPServerReconnected,WorkflowAvailable,WorkflowToolsDiscovered,WorkflowToolsMissing,WorkflowToolUnregistered, and the legacyWorkflowExecuted. Their constants, message templates, and reference-doc entries were removed so the documented event set matches what muster actually emits. - Removed the dead
CreateEvent/CreateEventForCRDmethods from theapi.EventManagerHandlerinterface (replaced byCreateEventWithData); the documenteddoc.goexamples that called them with the invalid reason"Created"were corrected. WorkflowStep.outputs(#865): the field was present on the CRD and copied by the adapter but never read by the executor (dead code). Removed from the CRD, internal types, conversion, and step JSON schema. Usestore: trueand reference the result as{{ .results.<step_id> }}instead.MCPServer.status.consecutiveFailures,.lastAttempt, and.nextRetryAfterare no longer updated by the reconciler; the retry state machine that drove them was removed in a prior release. The fields remain on the CRD for forward compatibility.oauth.server.enableHSTS,oauth.server.tlsCertFile, andoauth.server.tlsKeyFileconfig fields removed; they were declared and YAML-parsed but never read anywhere in the codebase.
forwardToken: true(andtokenExchange) MCPServers now work for callers presenting a forwarded ServiceAccount token with no browser auth-code flow. The first request for such a new session connects the session's SSO backends synchronously before the request's MCP handler runs, so the caller's initialtools/listandcall_toolsucceed instead of returning no tools (auth_required/ "authentication context missing, no active session"). Concurrent first requests for the same session are deduplicated.- Raw Kubernetes ServiceAccount projected tokens validated via
oauth.server.trustedIssuersnow drive per-target RFC 8693 token exchange directly. Previously,injectExternalIDTokenonly tried theTrustedAudiencespath (AcceptForwardedIDToken), which returnsErrTrustedAudienceMismatchfor SA tokens (theiraudis muster's own resource identifier, not a TrustedAudiences entry); the bearer was dropped and the downstream exchange failed with an empty subject token. The fix adds a fallback to the newAcceptTrustedIssuerTokenAPI (mcp-oauth >= v0.8.0) on mismatch. The sameext-<hex>session-ID derivation is used, preserving cross-hop audit-log correlation. Closes #805 Issue 3. - Bump
mcp-oauthto v0.4.2, which makes the trusted-issuer JWKS cache rotation-safe: a subject token presenting akidabsent from the cached JWKS now triggers a single bounded refetch (rate-limited per JWKS URI) and retries verification before rejecting. Previously, a Dex signing-key rotation made the token-exchange broker reject every current user token withsubject_token_validation_faileduntil the muster pod was restarted (the shared broker took down all downstream audiences at once). Closes #847. - Bump
mcp-oauthto v0.4.1, which RFC 6749 §2.3.1-encodes client credentials in token-exchange Basic auth. Cross-cluster SSO token exchange previously failed withinvalid_clientfor downstream clusters whosemuster-token-exchange-*Dex client secret contained+(decoded to a space on the wire); base64-std secrets with only/and=were unaffected, which is why some clusters worked and others did not. ssoPoolMissNeedingInitnow detects pool misses for token-forwarding servers in addition to token-exchange servers, so warm sessions (authAlive=true after pod restart) triggerinitSSOForSessionfor forwarding servers with empty connection pools. Previously, forwarding servers registered during a restart were inaccessible until the user manually re-authenticated to muster.establishSSOConnectionnow treats a pool miss as stale state for token-forwarding servers (clearing the Valkey auth entry and re-establishing the connection), matching the existing behaviour for token-exchange servers.- After an idle period,
getIDTokenForForwardingnow attempts an in-process upstream provider refresh (Server.RefreshSession) when the proxy store has no valid ID token. On success the store is repopulated byTokenRefreshHandlerand the fresh token is forwarded, avoiding401 Unauthorizederrors without requiring re-authentication. Closes #549.
0.3.12 - 2026-06-10
- Release binaries now include darwin/amd64, darwin/arm64, windows/amd64, and windows/arm64 alongside the existing linux targets. Windows binaries are named
muster-windows-<arch>.exe.
- Update mcp-oauth to v0.2.199: JWT access tokens issued for grants without an RFC 8707
resourceparameter now carry anaudclaim defaulting to the server's resource identifier (RFC 9068 §2.2), instead of an empty audience that JWT-validating gateways (e.g. agentgateway) reject with401 InvalidAudience. Existing grants self-heal on their next token refresh.
- AllowedClaims in TrustedIssuer, drop KubernetesSATrusts, fix JWT signing key wiring (#772) (04b5bd2)
muster.oauth.server.dex.allowPrivateIPOIDC: allows Dex OIDC discovery to reach issuer URLs that resolve to private/loopback IPs (e.g. Azure internal load balancers). Requires mcp-oauth#427. Emits a CWE-918 startup warning.
- deps: update module github.com/giantswarm/mcp-oauth to v0.2.186 (ca46984)
- deps: update module github.com/giantswarm/mcp-toolkit to v0.2.5 (#780) (bcce33a)
- CiliumNetworkPolicy egress now reaches an OIDC issuer (Dex) / HTTP MCP server fronted by a Cilium-managed ingress gateway VIP (LB-IPAM / L2, typical on-prem). New
networkPolicy.cilium.ingressGatewayrule allows egress to the gateway backend endpoints on their target ports (default:10080/10443, selector:app.kubernetes.io/name=envoyinenvoy-gateway-system).
- attach release binaries to GitHub releases (#785) (77dbb0f)
- deps: update go toolchain directive to v1.26.4 (#783) (ba9c3fd)
0.1.231 (2026-06-03)
- deps: update module github.com/giantswarm/mcp-oauth to v0.2.185 (#769) (ca46984)
- deps: update module github.com/giantswarm/mcp-toolkit to v0.2.5 (#780) (bcce33a)
- attach release binaries to GitHub releases (#785) (77dbb0f)
- deps: update dependency architect to v9 (#768) (a4c790a)
- deps: update go toolchain directive to v1.26.4 (#783) (ba9c3fd)
- main: release 0.1.227 (#779) (84fca35)
- main: release 0.1.228 (#781) (05f5aeb)
- main: release 0.1.229 (#782) (3cf5800)
- main: release 0.1.230 (#784) (7975401)
0.1.230 (2026-06-03)
- deps: update module github.com/giantswarm/mcp-oauth to v0.2.185 (#769) (ca46984)
- deps: update module github.com/giantswarm/mcp-toolkit to v0.2.5 (#780) (bcce33a)
- deps: update dependency architect to v9 (#768) (a4c790a)
- deps: update go toolchain directive to v1.26.4 (#783) (ba9c3fd)
- main: release 0.1.226 (#778) (a0ea312)
- main: release 0.1.227 (#779) (84fca35)
- main: release 0.1.228 (#781) (05f5aeb)
- main: release 0.1.229 (#782) (3cf5800)
0.1.229 (2026-06-02)
- deps: update module github.com/giantswarm/mcp-oauth to v0.2.185 (#769) (ca46984)
- deps: update module github.com/giantswarm/mcp-toolkit to v0.2.4 (#777) (9f915d6)
- deps: update module github.com/giantswarm/mcp-toolkit to v0.2.5 (#780) (bcce33a)
- deps: update dependency architect to v9 (#768) (a4c790a)
- main: release 0.1.226 (#778) (a0ea312)
- main: release 0.1.227 (#779) (84fca35)
- main: release 0.1.228 (#781) (05f5aeb)
0.1.228 (2026-06-02)
- deps: update module github.com/giantswarm/mcp-oauth to v0.2.185 (#769) (ca46984)
- deps: update module github.com/giantswarm/mcp-toolkit to v0.2.4 (#777) (9f915d6)
- deps: update dependency architect to v9 (#768) (a4c790a)
- main: release 0.1.225 (#776) (d00cc90)
- main: release 0.1.226 (#778) (a0ea312)
- main: release 0.1.227 (#779) (84fca35)
0.1.227 (2026-06-02)
- deps: update module github.com/giantswarm/mcp-toolkit to v0.2.3 (#770) (7649ee8)
- deps: update module github.com/giantswarm/mcp-toolkit to v0.2.4 (#777) (9f915d6)
- deps: update dependency architect to v9 (#768) (a4c790a)
- main: release 0.1.225 (#776) (d00cc90)
- main: release 0.1.226 (#778) (a0ea312)
0.1.226 (2026-06-02)
- deps: update module github.com/giantswarm/mcp-toolkit to v0.2.3 (#770) (7649ee8)
- deps: update module github.com/giantswarm/mcp-toolkit to v0.2.4 (#777) (9f915d6)
0.1.225 (2026-06-02)
- deps: update actions/checkout action to v6.0.3 (#774) (59a91dc)
- main: release 0.1.224 (#775) (311f7b5)
0.1.224 (2026-06-02)
- deps: update actions/checkout action to v6.0.3 (#774) (59a91dc)
- main: release 0.1.223 (#771) (4b20779)
0.1.223 (2026-06-02)
enableJWTMode: truenow issues RFC 9068 signed JWT access tokens. Setmuster.oauth.server.jwtSigningKey(PEM-encoded EC P-256 or RSA key) orexistingSecretwith keyjwt-signing-key;helm templatefails if neither is provided whenenableJWTMode: true.- CiliumNetworkPolicy egress now reaches an OIDC issuer (Dex) / HTTP MCP server that is fronted by a Cilium-managed ingress gateway VIP (LB-IPAM / L2, typical on-prem). With
kube-proxy-replacement, Cilium DNATs the LoadBalancer VIP to the gateway backend pod on its target port (e.g.443→10443) before egress policy is evaluated, so neithertoEntities: worldnortoEntities: clusteron443matched and OIDC discovery failed withcontext deadline exceeded. A newnetworkPolicy.cilium.ingressGatewayrule allows egress to the gateway backend endpoints on their target ports (default: Giant Swarmenvoy-gatewayproxies on10080/10443). Clusters whose gateway VIP is an external cloud LB (e.g. AWS ELB) were already covered by theworldrule and are unaffected (the new rule is a no-op there); setingressGateway: nullto disable. Fixes the OAuth/OIDC discovery failedstartup warning on affected clusters. - Workflow availability (
core_workflow_available/core_workflow_list/workflow_available) is now session-aware. Previously, availability for SSO / auth-protected family tools (e.g. multi-instancekubernetes/prometheusservers) was computed from the process-global family routing index, which is unioned across sessions and only populated as a side effect of a priorlist_toolscall. This produced two symmetric defects: a false negative —muster list workflows/muster get workflowreported workflowsUnavailableuntil some session listed tools, whilemuster agent(which lists tools on connect) reported them available, so the answer depended on call ordering; and a false positive — once any session listed tools, the family entry leaked process-wide, so a session that never authenticated to the family still saw the workflow as available. When the request carries a session, availability now resolves each step tool against that session's own accessible tools (hydrated from theCapabilityStore); core / meta tools resolve by name, and only session-less calls fall back to the process-global view. Closes #764. - Workflow availability is now transitive across nested workflows. A workflow step that calls another workflow (
workflow_<name>) was always treated as available because the availability check matched theworkflow_prefix without consulting the registry, so a workflow referencing a non-existent or transitively broken nested workflow was wrongly reportedAvailableand only failed at execution time. Nested workflow steps now require the referenced workflow to exist and to be itself available; the check descends through the whole chain (with cycle detection) and reports the actual unavailable tool. Theworkflow_management meta-tools (workflow_list,workflow_available, ...) are unaffected.
- Bump
giantswarm/mcp-oauthtov0.2.184. New Helm valuesmuster.oauth.server.{trustedIssuers,trustedProxyCIDRs,enableJWTMode,resourceIdentifier}wire trusted external OIDC issuers for RFC 8693 token exchange (id_token / access_token / jwt), DPoP trusted-proxy CIDRs, RFC 9068 JWT access tokens, and RFC 8707 resource-server audience binding.trustedIssuersentries now supportallowedClaims(claim name to glob-pattern map) for Kubernetes ServiceAccount and GitHub Actions trust. Also enables the OIDC userinfo endpoint, PII-redacted audit logging, and CIMD metadata-fetch rate limiting. Encryption-at-rest is now wired on the store constructor (valkey.WithEncryptor/memory.WithEncryptor) rather than as a server option.
- New standalone
muster-crdsHelm chart (helm/muster-crds) shipping theMCPServerandWorkflowCustomResourceDefinitions. The CRDs are loaded fromfiles/crds/*.yamlbytemplates/crds.yaml(regular chart templates, not the Helm 3crds/directory), so they remain upgradable onhelm upgradeand keep thehelm.sh/resource-policy: keepannotation. This decouples the CRD lifecycle from the application chart so a downstreamagentic-platform-crdsumbrella can own it independently. Install or upgrademuster-crdsbeforemuster. - Degraded-mode startup when the Dex/OIDC issuer is unreachable at boot time. muster now starts immediately and serves MCP aggregation, reconcilers, and all non-OAuth paths regardless of Dex availability. A background goroutine retries OIDC discovery with exponential backoff (1 s → 30 s cap); once discovery succeeds the OAuth server activates transparently. Until then, OAuth and MCP-over-OAuth endpoints return
503 Service Unavailablewith aRetry-After: 30header. The/healthendpoint always returns200with{"status":"degraded","reason":"oidc-discovery-pending"}during the window. Closes #730. networkPolicy.flavorselects betweencilium(CiliumNetworkPolicy) andkubernetes(networking.k8s.io/v1 NetworkPolicy). The kubernetes flavor is best-effort: no entity selectors, no FQDN egress. CIDR replacements live undernetworkPolicy.kubernetes.{apiServerCIDR,clusterCIDR,worldExcludedCIDRs}.clusterCIDR: ""disables the in-cluster ingress egress rule (kubernetes-flavor equivalent of ciliumallowClusterIngress).crds.annotations(object) is merged into each CRD'smetadata.annotationsby the loader. Default{helm.sh/resource-policy: keep}keeps CRDs (and theMCPServer/WorkflowCRs that depend on them) around onhelm uninstall.revisionHistoryLimit(default3) on the muster Deployment.resources.{requests,limits}.ephemeral-storage(50Mi / 100Mi) — Kyverno's resource-limits policy on Giant Swarm workload clusters audits / rejects pods without explicit ephemeral-storage when/tmpis an emptyDir.- Egress to
app.kubernetes.io/name=agentgateway:8080in the release namespace so muster can dial the agentgateway data-plane on the upstream-proxy path (both NetworkPolicy flavors). No-op when agentgateway isn't deployed.
-
muster.oauth.server.kubernetesSATrustsHelm value andK8sSATrustConfigGo type are removed. Kubernetes ServiceAccount trust is now expressed viatrustedIssuerswith anallowedClaimsentry (sub: "system:serviceaccount:<namespace>:*") andallowPrivateIPJWKS: truewhen the JWKS endpoint is in-cluster. Thejwtsubject_token_type covers projected SA tokens without a separate trust list. -
ciliumNetworkPolicy.*is replaced bynetworkPolicy.*.ciliumNetworkPolicy.enabled→networkPolicy.enabled+networkPolicy.flavor: cilium(default).ciliumNetworkPolicy.allowClusterIngress→networkPolicy.cilium.allowClusterIngress.ciliumNetworkPolicy.{labels,annotations}→networkPolicy.{labels,annotations}.
-
The muster application chart no longer renders the CRDs.
helm/muster/templates/crds.yamlwas removed and the CRDs moved to the newmuster-crdschart.crds.installnow defaults tofalseand the wholecrdsblock is deprecated (inert compatibility shim, removed next release) — it is kept only so a downstream that explicitly setsmuster.crds.install: falsestill validates. Operators must install/upgrademuster-crdsbeforemuster. -
CRD source files moved from
helm/muster/files/crds/tohelm/muster-crds/files/crds/.files/has no Helm 3 special-case, so the CRDs stay upgradable onhelm upgrade.controller-genoutput path updated inMakefile.crd.mk; CI drift check in.github/workflows/ci.yamlfollows the new path. -
Container image build no longer compiles the Go binary inside
docker buildx.go-buildnow produces bothmuster-linux-amd64andmuster-linux-arm64in one job (architect-orbarchitecturesparameter) and the Dockerfile copies the matching binary from the workspace. Removes the duplicate compile and the QEMU-emulated arm64 cross-build on tag releases;push-to-registriesauto-derives--platformfrom the workspace.platformsfile. -
Build identifiers (
version,gitSHA,buildTimestamp) now live inpkg/projectinstead ofmain. Both injection paths populate the same vars: goreleaser writes the semver tag + short commit + date for release archives, architect-orb'sgo-buildwrites the commit SHA + UTC timestamp for container images.muster versionprefers the tag, falls back to the SHA, falls back todev, and additionally prints the commit SHA and build timestamp on dedicated lines. -
Bump
giantswarm/architectorb to8.2.2and re-enable cosign keyless chart signing (sign: falseremoved from everypush-to-app-catalog*invocation). v8.2.2 ships architect-orb#772 which upgrades theapp-build-suiteexecutor image from1.8.0-circlecito1.8.1-circleci-- the new image includes thecosignbinary that v8.2.0's chart signing defaults require. Closes architect-orb#769. -
Bump
giantswarm/architectorb to8.2.1to pick up architect-orb#767:image-login-to-registriesis now POSIX-portable, unblockingarchitect/sync-china-registry(the gsoci -> Aliyun mirror via the in-Chinagiantswarm/galaxy-runner). The v8.1.0 refactor accidentally introduced bash-only${!var}indirect expansion in the shared login command, which BusyBox/bin/sh(used by the regctl executor) rejected withbad substitution-- so no Aliyun mirror has been happening since the migration tosplit-china-push: true. v8.2.x also enables cosign keyless signing, SLSA provenance, and SBOM attestations by default for public images and charts. -
Disable cosign keyless chart signing on the
push-to-app-catalog*jobs (sign: false). The architect orb'spush-to-app-catalogdefaultssigntotruesince v8.2.0 and shells out tocosign, but this repo usesexecutor: app-build-suite(so theapp_build_suitePython CLI is available to package the chart with metadata) and theapp-build-suiteimage doesn't shipcosign. Without this opt-out, every chart push fails on theMint Sigstore OIDC tokenstep withcosign: command not found. To be removed once architect-orb makescosign-prepareresilient to a missing binary (or ships cosign in theapp-build-suiteexecutor) -- tracked in architect-orb#769. -
Replace the
push-to-gsoci-release+push-to-all-registries-releaseworkaround pair with a singlepush-to-registries-releasejob usingsplit-china-push: trueand a companionsync-china-registryjob. The cross-Pacificdocker buildxpush to the Aliyun mirror is replaced withregctl image copy(gsoci -> Aliyun) executed on the in-Chinagiantswarm/galaxy-runnerself-hosted CircleCI runner via the Singapore geo-replica. The chart catalog publish still does not gate on Aliyun. -
Migrate image pushes from the deprecated
architect/push-to-registries-multiarchjob topush-to-registrieswithmultiarch: true. Picks up the orb v8.1.0 QEMU/binfmt auto-registration, hardened buildx bootstrap, and standard OCI image labels.
muster serve --extra-ca-file <path>flag: appends a PEM file to the system trust pool at startup, so outbound HTTP (MCP backends, token exchange, OAuth proxy) trusts an internal CA without per-MCPServer plumbing. Exposed in the chart asmuster.extraCaFile.{path,secret.name,secret.key}; the chart mounts the named Secret and passes the flag whensecret.nameis set. Use case: tunnelport's SPIFFE-issued tunnel certificates on a Giant Swarm consumer MC.MCPServer.spec.family— optional object{name, instanceArg}grouping equivalent MCPServers under a shared exposed surface. When set, the aggregator exposes tools asx_<family.name>_<tool>with a required parameter (named byfamily.instanceArg) selecting the providing instance. Both fields are required whenfamilyis set. The parameter is always required even for single-instance families so skills written against the family name remain stable as instances are added or removed. When unset, today's per-server prefixing applies (no behavior change for existing CRs).MCPServer.spec.familyis configurable via thecore_mcpserver_create/core_mcpserver_update/core_mcpserver_validatetools.muster.oauth.server.trustedPublicRegistrationRedirectURIs— HTTPS redirect-URI allowlist for unauthenticated dynamic client registration, passed through to mcp-oauth (Config.TrustedPublicRegistrationRedirectURIs). Strict exact-match after RFC 3986 normalization. Default:[](opt-in per URI).oauth-secretfailguard accepts a non-emptytrustedPublicRegistrationRedirectURIsas a third valid escape valve.
- The shared OpenTelemetry identifiers (
TracerName,AttrToolName) move topkg/observability, a leaf package with no internal/* dependencies that any package can import without going through the service locator. Theinternal/aggregator/instrumentsubpackage is flattened intointernal/aggregator:Logging,Metrics,StartToolSpan, and the formerly-exportedMCPServerOptions(now unexportedmcpServerOptions) all live alongsideserver.goso the aggregator's MCP-server middleware sits in one place. External imports ofgithub.com/giantswarm/muster/internal/aggregator/instrumentmove togithub.com/giantswarm/muster/pkg/observability(constants only). aggregator.Register/aggregator.RegisterPendingAuthand their manager-level /api.AggregatorHandlercounterparts now take aServerRegistration/PendingAuthRegistrationstruct rather than five-to-six positional(name, url, toolPrefix, family, authInfo, authConfig)arguments. The previousRegisterServerPendingAuthWithConfigis collapsed into the singleRegisterServerPendingAuth(registration)form —AuthConfigis now a nullable field inside the struct. Internal API change; no behavior change for existing CRs.- Aggregator OpenTelemetry tracing adopts mcp-go's native server-tracing hooks via the
github.com/mark3labs/mcp-go/oteladapter, replacing muster's per-tool-handler middleware. The aggregator now emitsmcp.<method>spans (server kind) around every dispatched JSON-RPC method andtool.<name>spans (internal kind) around tool handlers, with W3C trace-context propagation extracted from inbound headers. Custominstrument.Tracing()middleware is removed;instrument.StartToolSpanis retained for the internalCallToolInternaldispatch path used by workflows and direct API entries. - Outbound MCP clients (stdio, SSE, streamable-http, dynamic-auth) install mcp-go's OTEL tracer via the
mcp-go/otel.WithClientTracingadapter so the muster → backend leg inherits the inbound trace context and a W3Ctraceparentis emitted on every outgoing JSON-RPC frame. Combined with server-side tracing, a single trace now covers the whole caller → muster → backend chain. - mcp-oauth bumped to
v0.2.140. The OAuth HTTP handler (Handler,New,OAuthRoutesOptions,UserInfoFromContext,SessionIDFromContext) moved to ahandlersubpackage; muster'sinternal/serverandinternal/aggregatorimport the new path. No user-facing config change. - mcp-oauth bumped to
v0.2.125. Internal API migrated to functional options;server.NewOAuthHTTPServernow takes...oauth.ServerOption. Security-event log emission is rate-limited (1/s, burst 5). No user-facing config change.
MCPServer.spec.familytool emission now deep-copies nested JSON schema sub-trees (objectproperties, arrayitems, nestedrequired) so caller mutations of an exposed tool's schema no longer leak into the per-server cache and corrupt latertools/listresults.tools/listorder is now deterministic across calls for family-grouped tools. Previously the assembly iterated Go maps directly, producing shifting orders between calls and spurioustools/list_changeddiffs downstream.- When
family.instanceArgcollides with a property name already declared in the tool's ownInputSchema.Properties, the aggregator now falls back to per-server prefixing for that specific tool. Previously the family-grouped emission silently overwrote the operator-declared property with the instance-selector enum, losing the original property's description, type, and constraints. - Workflow execution tools were advertised twice — both as the documented
workflow_<workflow-name>and ascore_action_<workflow-name>— throughlist_tools/list_core_tools/filter_tools. Thecore_action_*variant is not part of the public surface and the aggregator's call routing does not recognize it (calls fail with "no handler found"), so clients that picked it up from discovery hit non-functional tools. The aggregator now rewrites the workflow provider's internalaction_<name>tools toworkflow_<name>(nocore_prefix) when listing, matching the architecture spec; management tools (workflow_list,workflow_get, …) continue to be advertised ascore_workflow_*. Pure listing fix — execution routing was already correct. WorkflowandServiceClassCRD validation rejected scalar values in step args (spec.steps[*].args.<key>: must be of type object), making the documented YAML form (namespace: kube-system,limit: 30,allNamespaces: false) unusable throughkubectl apply. Step args, condition args, JSONPath maps, andArgDefinition.Defaultnow useapiextensionsv1.JSONinstead ofruntime.RawExtension, which controller-tools emits asadditionalProperties: {x-kubernetes-preserve-unknown-fields: true}(notype: objectconstraint), so scalars, objects, and arrays all validate. Wire format and stored values are unchanged; existing workflows with object-only args keep working.- OAuth server initialisation built its own text-format
slog.Loggerwriting to stdout when--debugwas set, so in-pod log lines from themcp-oauthlibrary (Valkey storage, redirect-URI security, OIDC discovery, rate limiters, audit, instrumentation) appeared as text on stdout instead of flowing through the project's JSON handler.createOAuthServernow usesslog.Default()and inherits the level set bylogging.Init, so all in-pod log lines share one format and one writer.
- Breaking (MCPServer CRD): Teleport authentication support removed from muster — moved to a separate operator.
MCPServerAuth.typeno longer acceptsteleport; theteleportfield (TeleportAuthConfigwithidentityDir/identitySecretName/identitySecretNamespace/appName) is removed from the CRD. Existing CRs withauth.type: teleportor anauth.teleportblock will be rejected by validation and must be migrated to the new operator. Theinternal/teleportpackage, theapi.TeleportClientHandler/api.RegisterTeleportClient/api.GetTeleportClient/api.TeleportClientConfig/api.TeleportAuth/api.AuthTypeTeleportAPI surface, theOAuthHandler.ExchangeTokenForRemoteClusterWithClientmethod, theTokenExchanger.ExchangeWithClientmethod, themcpserver.MCPClientConfig.HTTPClientfield, and theNewStreamableHTTPClientWithHTTPClient/NewStreamableHTTPClientWithHeaderFuncAndHTTPClientconstructors are removed. - Breaking (external consumers of
pkg/oauth):pkg/oauth.IDTokenClaimsstruct andParseIDTokenClaimsfunction removed. Replaced by typed accessors inpkg/oauth/jwt.go—Subject,Email,Expiry,Issuer,IsExpired— each returning(value, error)so callers can distinguish "missing claim" from "decode failed". - Per-config CA-file knobs removed:
OAuthMCPClientConfig.CAFile,DexConfig.CAFile(Go config), and themuster.oauth.server.dex.caFileHelm value. These were redundant after--extra-ca-file(which augments the system trust pool) and the OAuth one was a footgun: it built a fresh cert pool from a single file, silently dropping system roots and narrowing trust to that one CA. Operator caveat — trust scope changes: the removed paths narrowed trust to a single CA; the replacement--extra-ca-file/muster.extraCaFileis additive on top of the system pool. Anyone who deliberately relied on the narrower scope no longer has that option here. None of giantswarm's deployed configs set these values, so no migration is required.
- Logging bootstrap now lives in
cmd/serve.go. The serve command callslogging.Initonce at startup, defers theShutdown, then constructs the application.NewApplicationno longer touches the logger; non-servemustersubcommands rely on the nil-guard inpkg/logging(the previous in-bootstrap init was vestigial there too). app.NewConfigsignature drops thesilentparameter and the correspondingConfig.Silentfield. Both were set but never read —--silentis enforced by swapping the writer toio.Discarddirectly incmd/serve.go. Module-internal change (internal/appis not importable from outside the module).- In-pod muster logs are now JSON by default (auto-detected via
KUBERNETES_SERVICE_HOST) instead of text. The text path remains for localmusterCLI invocations and tests. - The aggregator's
Hooks(AddAfterInitialize,AddAfterListTools,AddBeforeCallTool,AddAfterCallTool,AddOnError) emit log lines via the new*WithAttrsCtxvariants so per-request trace correlation lands on theMCP-Protocolsubsystem. - The Valkey storage URL in startup logs is now redacted via
mcp-toolkit/logging.RedactHost, which strips IPv4/IPv6 addresses and URL userinfo. The localredactURLhelper, which only stripped userinfo, is removed. - Consolidated scattered JWT-claim decoders into typed accessors in
pkg/oauth/jwt.go:Subject,Email,Expiry,Issuer,IsExpired, plusErrTokenExpMissingfor callers that need to distinguish "missing exp" from "decode failed". The accessors share a singlegolang-jwt/jwt/v5parser; consumers ininternal/aggregator,internal/cli, andinternal/oauthno longer touchencoding/base64,encoding/json, or the JWT library directly. The admin diagnostic UI keeps its own segment decoder (different concern: lenient display of operator-pasted tokens, including 2-part inputs missing the signature segment). The previous defensiveRawStdEncodingfallback for non-spec base64 is intentionally dropped — every IdP muster integrates with emits RFC 7515-compliantRawURLEncoding.
pkg/logging.Init(ctx, level, output, serviceName, serviceVersion) (Shutdown, error)initialises logging viamcp-toolkit/loggingand returns aShutdownfor the OpenTelemetryLoggerProvider. When any ofOTEL_EXPORTER_OTLP_LOGS_ENDPOINT,OTEL_EXPORTER_OTLP_ENDPOINT, orOTEL_LOGS_EXPORTERis set, log records flow through OTLP and carry the active span's TraceID/SpanID for log ↔ trace correlation in Grafana. Otherwise the handler auto-selects JSON (in a Kubernetes pod) or text (local dev) and theShutdownis a no-op.InitForCLIstays as a non-OTLP convenience.pkg/logging.{Debug,Info,Warn,Error}Ctxand{Debug,Info,Warn}WithAttrsCtxthread acontext.Contextthrough the slog handler so the OTLP path can pull the active span's TraceID/SpanID off the call's context. Existing ctx-less variants remain.- OpenTelemetry tracing for every MCP tool call. The aggregator's mcp-go middleware opens a
tool.<name>span at the meta-tool layer, andCallToolInternalopens an inner span carrying the real workload tool name (x_kubernetes_*,workflow_*,service_*, …) so Tempo trace trees pivot on the actual tool. W3C TraceContext + Baggage propagators are installed unconditionally so inboundtraceparentheaders are honoured even when no exporter is configured. Tracer- and meter-provider lifecycles are handled bymcp-toolkit/tracingandmcp-toolkit/metricsat the composition root. - OpenTelemetry metrics for every MCP tool call:
muster.tool_calls(counter, exports asmuster_tool_calls_total) andmuster.tool_call.duration(histogram, exports asmuster_tool_call_duration_seconds), each withtoolandoutcomeattributes (ok/error/error_result). - Helm:
muster.observability.metrics.exporterswitches the metric backend (otlp,prometheus,console,none, comma-combinations). Selectingprometheusexposes/metricson port 9464 and (whenmuster.observability.metrics.prometheus.serviceMonitor.enabled) renders aServiceMonitor. - Structured per-tool-call log line on subsystem
MCP-Toolwithtool,outcome,duration_s, anderrorfields, for log/metric/trace correlation in dashboards. muster.observability.otel.{endpoint,protocol,headers,resourceAttributes}Helm values configuring the OTLP exporter. Emptyendpoint(default) leaves muster in propagator-only mode; setting it enables both traces and metrics over the same OTLP endpoint.K8S_NODE_NAMEis now exposed via the downward API alongside the existingK8S_NAMESPACE/K8S_POD_NAMEso resource attributes carryk8s.node.name.OTEL_RESOURCE_ATTRIBUTESis set whenever either OTLP or a metrics exporter is configured (previously OTLP-only), so Prometheus-only mode also getsk8s.namespace.name/k8s.pod.name/k8s.node.nameresource attribution.docs/explanation/observability.mddocumenting the trace contributions, signal configuration, instrument and log-field shape, and a Tempo/Mimir/Loki query catalog.- Add
muster callcommand for direct MCP tool invocation from the CLI. Supports--key=valuearguments and--jsonfor complex payloads, with tab completion for tool names. - Add
ciliumNetworkPolicy.allowClusterIngressHelm value to allow egress to in-cluster services on HTTP/HTTPS ports (e.g. Dex OIDC via ingress LoadBalancer IP). - OAuth encryption keys can now be supplied as either base64 (
openssl rand -base64 32) or hex (openssl rand -hex 32); the format is auto-detected. - Agent OAuth client now validates the RFC 9207
issparameter on the authorization callback (defense-in-depth against AS mix-up attacks). Servers that omitissare still accepted. - Authorization-server discovery now also serves
/.well-known/openid-configurationand per-path Protected Resource Metadata at/.well-known/oauth-protected-resource/mcp(additive — RFC 9728 / OpenID Connect Discovery). - BDD scenarios
workflow-conditional-staticandservice-state-staticto preserve coverage of workflow conditional features (inline tool conditions,from_step,allow_failure,expect_not,condition_evaluation, step skipping) and static-service state-machine semantics (core_service_restarthappy-path,core_service_starton already-running,core_service_stopon already-stopped). The deleted ServiceClass-based scenarios bundled this coverage with SC-instance lifecycle; the replacements drive the same workflow-engine and orchestrator code paths against a static MCPServer service. MCPServer.spec.auth.authorizationServerlets operators pin the OAuth issuer when the backend doesn't publish RFC 9728 metadata (Atlassian's hosted MCP being the prompting case). The override applies tocore_auth_loginonly and is verified against the AS metadata'sissuerfield per RFC 8414 §3.3 to fail closed on a wrong pin. Fixes #599.
- Extracted validation and template-resolution helpers from the 930-line
internal/workflow/executor.gointo dedicatedvalidation.goandtemplate.gofiles.executor.gois now ~600 lines;ExecuteWorkflowitself remains 500 lines and a follow-up tracks breaking it into per-concern helpers (executeStep,evaluateCondition,processStepResult) — that's a behavioral refactor with test impact, not a file split. (#140) - Move the 547-line
internal/client/kubernetes_client.gointo a newinternal/client/kubernetes/subpackage, split per domain (client.go,mcpserver.go,serviceclass.go,workflow.go,events.go). The core file keeps the type, constructor, scheme, lifecycle methods, and discovery-based CRD validation. Pure refactor: theMusterClientinterface stays in the parentclientpackage, the dispatcher callskubernetes.New(restConfig)directly, and external consumers are unaffected. (#140) - Move the 1233-line
internal/client/filesystem_client.gointo a newinternal/client/filesystem/subpackage, split per domain (client.go,mcpserver.go,serviceclass.go,workflow.go,events.go). Each file is under 400 lines. Pure refactor: theMusterClientinterface stays in the parentclientpackage, the dispatcher callsfilesystem.New(basePath)directly, and external consumers are unaffected. (#140) - Collapse per-CRD duplication in both client adapters into shared
store.gohelpers built onclient.Object/client.ObjectList. Each per-CRD file shrinks from ~165 LOC (filesystem) / ~70 LOC (kubernetes) to ~40 LOC of thin wrappers; error wrapping and namespace handling are now uniform across both surviving CRDs. The kubernetesCreateEventForCRDdouble switch (kind→GVK + kind→Get-method) collapses to a singlecrdFactoriesmap. Pure refactor: no public method signatures change, no behaviour change. (#140) - Restore
groupsscope inDefaultOAuthCIMDScopes-- required for group-based RBAC in downstream services. Provider-level scope filtering in mcp-oauth (e.g.,filterGoogleScopes,filterDexScopes) handles provider differences. - Bump
mcp-oauthto v0.2.117. Adoptsoauth.NewServerWithCombinedandHandler.RegisterOAuthRoutesto simplify server wiring; the authorization callback now includes the RFC 9207issparameter automatically. Operational note: mcp-oauth now rejects low-entropy AES-256 token-encryption keys (fewer than 16 distinct byte values). Real keys generated withopenssl rand -base64 32oropenssl rand -hex 32are unaffected; placeholder keys (all zeros, repeated bytes) will fail at startup with a clear error — rotate before upgrading.
-
ServiceClass CRD, API types, and Helm RBAC narrowing (third PR of the ServiceClass removal — see #632).
-
Deleted:
pkg/apis/muster/v1alpha1/serviceclass_types.go,helm/muster/crds/muster.giantswarm.io_serviceclasses.yaml,internal/api/{serviceclass,serviceinstance}.go,ServiceClassManagerHandlerinterface andRegister/GetServiceClassManager. -
Helm RBAC drops
serviceclassesandserviceclasses/statusfrom the ClusterRole'sresourceslists. -
Operational note (REQUIRED before upgrading past this PR): delete any
ServiceClasscustom resources in your cluster — they will be orphaned when the CRD is removed:kubectl delete serviceclasses.muster.giantswarm.io --all -AMCPServerandWorkflowCRs are unaffected.
-
-
ServiceClass-related MCP tools and CLI surface (first PR of the ServiceClass removal — see #632 for the rest).
- MCP tools:
core_serviceclass_*,core_service_create,core_service_delete,core_service_get,core_service_validate. Service inspection still works viacore_service_status. - CLI subcommands:
muster create service,muster create serviceclass,muster check serviceclass,muster get serviceclass,muster list serviceclass. Theserviceandserviceclassvalues formuster events --resource-typeandmuster test --conceptare also gone. - BDD scenarios: 22
serviceclass-*/serviceclass_*scenarios, 20service-*scenarios that exercised user-creatable service instances (service-create-*,service-delete*,service-get*,service-validate,service-lifecycle,service-persistence,service-restart,service-state-transitions,service-{start,stop}*), and 6 cross-cutting end-to-end scenarios that depended on ServiceClass (behavior-developer-onboarding-journey,example_with_mock,reconciler-status-sync,user-journey-platform-setup,workflow-conditional-service-check,workflow-run-with-serviceclass) — 48 scenarios total.service-get-non-existentis renamed toservice-status-non-existentand now exercisescore_service_status.
Note: the
ServiceClassruntime, CRD, and Helm RBAC are still in place after this PR; they are removed in subsequent PRs tracked in #632. - MCP tools:
-
api.RegisterConfigandapi.GetConfigdeprecated wrappers (useRegisterConfigHandler/GetConfigHandlerdirectly). All call sites already suppressed with//nolint:staticcheck; both are gone now along with the suppressions. (#140)
- Aggregator-side PRM discovery (used by
core_auth_login) now follows the MCP 2025-11-25 spec: it parsesWWW-Authenticate: ... resource_metadata=from a 401, probes the path-based well-known URL (<host>/.well-known/oauth-protected-resource<path>— using the raw MCP URL path so/v1/mcpis preserved) before the root form, and exposes the RFC 9728resourcefield on the parsed result. The previous implementation was root-only and silently dropped both signals. pkg/oauth.Client.DiscoverMetadatanow handles path-bearing issuer URLs (e.g.https://login.microsoftonline.com/<tenant>/v2.0, Auth0 / Okta orgs with paths) per MCP 2025-11-25 §"Authorization Server Metadata Discovery": tries RFC 8414 path-insert, OIDC path-insert, then OIDC append. Previously these issuers fell through to a single no-path probe and failed; now they succeed.- SSO token forwarding no longer hands downstream MCP servers a JWT whose
expis past the current time. Both ID-token storage paths —storeIDTokenForSSO(muster-issued tokens) and the forwarded-bearer mirroring ininjectExternalIDToken(SSO-passthrough) — read the JWT'sexpclaim and persist it as the entry'sExpiresAt, soIsExpiredWithMarginevicts stale tokens after idle periods instead of treating zeroExpiresAtas never-expiring. Tokens without a parseableexpare refused (logged at warn level) — they were always malformed for muster's flow but the previous shape would have stored them with zeroExpiresAt, recreating the same leak. (#549) - Bump
mcp-oauthto v0.2.86 with Dex scope filtering: non-standard client scopes likeclaudeai(sent by Claude) are now stripped before forwarding to Dex, preventinginvalid_scopeerrors. Also includes Google scope filtering andopenidforce-merge from v0.2.84. - CRD validation now uses the discovery API instead of listing
MCPServerresources in thedefaultnamespace. With namespace-scoped RBAC (aRolelimited to muster's own namespace), the previous probe failed withForbidden, silently fell back to filesystem mode, and left configuredMCPServerCRs unstarted (visible in logs asFound 0 MCPServer definitions for auto-start processingfollowed byDeleting MCPServer service: <name>). call_toolmeta-tool now forwards the underlying tool'sisErrorflag on the outer response. Previously the top-levelisErrorwas alwaysfalseeven when the wrapped tool returned an error, which was misleading for MCP clients that only inspect the top-level flag.- Per-server OAuth flow and agent OAuth flow both now refuse to proceed when the authorization server's metadata does not advertise S256 in
code_challenge_methods_supported, per MCP 2025-11-25 §"Authorization Code Protection". Previously an absent list was treated as "S256 OK" (the OAuth 2.1 default), which let muster start a flow that the AS could silently downgrade or reject at the token endpoint with a confusing error.Metadata.SupportsPKCEis renamed toSupportsS256PKCEto match the new semantics — onlypkg/oauth-internal callers existed.
0.1.0 - 2026-02-23
- Session duration reduced from 90 days to 30 days. The refresh token TTL now
matches Dex's
absoluteLifetime(720h). Previously, muster's 90-day refresh token outlived Dex's 30-day session, causing confusing failures when auto-refresh silently stopped working after day 30. Users who were logging in once every ~2 months will now need to re-authenticate every 30 days. muster auth statusnow shows session expiry. Instead ofRefresh: Available, the output now showsSession: ~29 days remaining (auto-refresh), giving users a concrete estimate of when re-authentication will be required.- Access token TTL is now explicitly set to 30 minutes (matching Dex's
idTokensexpiry) instead of relying on the library default of 1 hour. - Session duration is now configurable via
oauth.server.sessionDurationinconfig.yaml(default:720h/ 30 days). - Kubernetes event emission is now disabled by default (alpha feature). Use
--enable-eventsflag onmuster serveor setevents: trueinconfig.yamlto opt in. - Switch CI to
push-to-registries-multiarch(architect-orb@6.14.0) with amd64-only on branches for faster PR feedback and full multi-arch on release tags. Chart tests now run before publishing to the app catalog. - Update Dockerfile to multi-stage build with native cross-compilation support for multi-architecture images.
Note: The Server-Side Meta-Tools Migration below is a breaking change that will be released as part of the next major version. External integrations should prepare for this change.
Meta-tools (list_tools, call_tool, describe_tool, etc.) have moved from the agent to the aggregator server. This is a fundamental architectural change.
What Changed:
| Component | Before | After |
|---|---|---|
| Agent | Exposed 11 meta-tools + bridged to aggregator | Transport bridge only (OAuth shim + stdio↔HTTP) |
| Aggregator | Exposed 36+ core tools directly | Exposes ONLY meta-tools - no direct tool access |
| Tool Access | Direct tool calls to aggregator | All tool calls go through call_tool meta-tool |
What Continues Working (Transparent Migration):
- CLI commands (
muster list,muster get, etc.) - client wraps calls automatically - Agent REPL (
muster agent --repl) - uses same client with transparent wrapping - BDD test scenarios - test client wraps calls automatically
- MCP native protocol methods (
tools/list,resources/list) - not affected
What Breaks (Requires Update):
- External integrations calling tools directly via HTTP
- Custom clients connecting directly to aggregator
Migration for External Clients:
// Before: Direct tool call
{"method": "tools/call", "params": {"name": "core_service_list", "arguments": {}}}
// After: Wrap through call_tool
{"method": "tools/call", "params": {
"name": "call_tool",
"arguments": {"name": "core_service_list", "arguments": {}}
}}Benefits:
- OAuth-capable clients can connect directly to server without agent
- Simpler agent architecture (~200 lines vs ~700 lines)
- Consistent tool visibility across all clients
- Centralized meta-tool logic
See ADR-010 for design details.
Known External Integrations Affected:
- Any HTTP clients calling the aggregator directly
- Custom MCP clients not using
muster agent - CI/CD pipelines with direct tool calls
Recommended Migration Timeline:
- Review your integration code for direct tool calls
- Update to wrap calls through
call_toolmeta-tool - Test with the new Muster version before deploying
- MCPServer CRD State Exposes Auth Required - The MCPServer CRD now shows
Auth Requiredstate when a remote server returns 401 Unauthorized (#337)- Before: 401 response mapped to
Connected(hiding auth requirement) - After: 401 response shows as
Auth Requiredin CRD state - This gives operators clear visibility into which servers need authentication
- CLI output updated:
muster list mcpservernow showsAuth Requiredstate - SESSION column values updated:
OK→Authenticated,Required→Pending Auth - Column header renamed:
AUTH→SESSIONto matchmuster auth statusoutput
- Before: 401 response mapped to
- Reconciliation Framework - Automatic synchronization between resource definitions (CRDs/YAML) and running services
- Supports both Kubernetes mode (using controller-runtime informers) and filesystem mode (using fsnotify)
- Auto-detects operating mode based on environment
- Configurable per-resource-type enable/disable
- Work queue with deduplication and exponential backoff
- Status tracking and API for observability
- See ADR 007 for design details
- StateChangeBridge - Real-time sync of runtime state changes to CRD status subresources
- Subscribes to orchestrator service state changes
- Triggers reconciliation to update CRD status when services start/stop/crash
- BREAKING: Consolidated OAuth Configuration Naming - OAuth configuration structure has been reorganized for clarity (#324)
- Before:
aggregator.oauth(client/proxy) +aggregator.oauthServer(server protection) - After:
aggregator.oauth.mcpClient(MCP client/proxy) +aggregator.oauth.server(server protection) - Both OAuth roles now live under a single
oauthsection with explicitmcpClient/serversub-sections - The
mcpClientname makes it clear this is for authenticating TO remote MCP servers - CLI flags renamed:
--oauth→--oauth-mcp-client,--oauth-public-url→--oauth-mcp-client-public-url - Helm values updated:
muster.oauth.*→muster.oauth.mcpClient.*,muster.oauthServer.*→muster.oauth.server.* - CIMD configuration moved to nested structure:
cimdPath/cimdScopes→cimd.path/cimd.scopes - Migration: Update configuration files and Helm values to use the new structure
- Before:
- BREAKING: CRD Status Field Changes - Status fields have been redesigned for session-aware tool availability
- MCPServerStatus: Removed
availableTools(session-dependent), addedlastConnectedandrestartCount - ServiceClassStatus: Replaced
available/requiredTools/missingTools/toolAvailabilitywithvalid/validationErrors/referencedTools - WorkflowStatus: Replaced
available/requiredTools/missingTools/stepValidationwithvalid/validationErrors/referencedTools/stepCount - Tool availability is now computed per-session at runtime, not stored in CRs
- Existing CRs will have stale status fields that will be updated on first reconciliation
- MCPServerStatus: Removed
- Added Chart annotations to support OCI repositories.
- Helm CiliumNetworkPolicy: Fixed incorrect values path for OAuth storage check (now uses
.Values.muster.oauth.server.storage)
- Remote MCP Server Support for Kubernetes Environments
- Added comprehensive support for
stdio,streamable-httpandssetransport protocols - Enhanced CRD Schema: Updated
MCPServerSpecto support all MCP server types- Added new config for
streamable-httpandsse:url,headersandtimeoutfields - Added mutual exclusion validation and required field validation using kubebuilder annotations
- Added new config for
- New CLI Commands: Added subcommands to use new type system
muster create mcpserver <name> --type stdiofor local MCP serversmuster create mcpserver <name> --type streamable-httpfor HTTP remote serversmuster create mcpserver <name> --type ssefor SSE remote servers
- Updated Examples: Enhanced example files to demonstrate both local and remote configurations
- Kubernetes Deployment Ready: Enables deployment patterns where Muster aggregator runs in cluster and connects to MCP servers deployed as separate Kubernetes services
- Added comprehensive support for
- Systemd Socket Activation Support
- Added
muster.socketunit file for socket-activated systemd deployment - Modified
muster.serviceto use socket activation on localhost:8090 - Updated
scripts/setup-systemd.shandscripts/dev-restart.shto handle socket activation - Make use of new dependency
github.com/coreos/go-systemdto handle socket activation
- Added
- Service Health Monitoring
- Added health checks for MCP servers using the
tools/listJSON-RPC method - Added health checks for port forwards by testing TCP connectivity
- Health checks run every 30 seconds for all running services
- Health status is reported through the StateStore and displayed in the TUI
- Created
ServiceHealthCheckerinterface for extensible health checking
- Added health checks for MCP servers using the
- Improved State Reconciliation
- Implemented proper
ReconcileState()method that syncs TUI state with StateStore - Updates service statuses, ports, PIDs, and error states from centralized store
- Synchronizes cluster health information from K8sStateManager
- Ensures UI consistency after startup and state changes
- Implemented proper
- K8s Connections as Services
- Kubernetes connections are now modeled as services in the dependency graph
- K8s connection health monitoring is now handled by dedicated K8s connection services
- Unified service management architecture - all services (K8s, port forwards, MCPs) follow the same lifecycle
- K8s connections can be stopped/restarted like other services with proper cascade handling
- Cascading stop functionality: stopping a service automatically stops all dependent services
- K8s connection health monitoring with automatic service lifecycle management
- Port forwards now depend on their kubernetes context being authenticated and healthy
- The kubernetes MCP server depends on the management cluster connection
- When k8s connections become unhealthy, dependent services are automatically stopped
- Manual stop (x key) now uses cascading stop to cleanly shut down dependent services
- New
StartServicesDependingOnmethod in ServiceManager to restart services when dependencies recover - New
orchestratorpackage that manages application state and service lifecycle for both TUI and non-TUI modes - New
HealthStatusUpdateandReportHealthfor proper health status reporting - Health-aware startup: Services now wait for their K8s dependencies to be healthy before starting
- Add comprehensive dependency management system for services
- Services now track why they were stopped (manual vs dependency cascade)
- Automatically restart services when their dependencies recover
- Ensure correct startup order based on dependency graph
- Prevent manually stopped services from auto-restarting
- Phase 1 of Issue #45: Message Handling Architecture Improvements
- Added correlation ID support to
ManagedServiceUpdatefor tracing related messages and cascading effects - Implemented configurable buffer strategies for TUI message channels:
BufferActionDrop: Drop messages when buffer is fullBufferActionBlock: Block until space is availableBufferActionEvictOldest: Remove oldest message to make room for new ones
- Added priority-based buffer strategies to handle different message types differently
- Introduced
BufferedChannelwith metrics tracking (messages sent, dropped, blocked, evicted) - Enhanced orchestrator with correlation tracking for health checks and cascading operations
- Updated service manager to use new correlation ID system for better debugging
- Added comprehensive test coverage for buffer strategies and correlation tracking
- Added correlation ID support to
- Phase 2 of Issue #45: State Consolidation
- Implemented centralized
StateStoreas single source of truth for all service states - Added
ServiceStateSnapshotfor complete state information with correlation tracking - Introduced state change subscriptions with
StateSubscriptionfor reactive updates - Enhanced
ServiceReporterinterface withGetStateStore()method for direct state access - Updated
TUIReporterandConsoleReporterto use centralized state management - Migrated
ServiceManagerfrom local state tracking to centralizedStateStore - Added comprehensive metrics tracking for state changes and subscription performance
- Implemented state change event system with old/new state tracking
- Added support for filtering services by type and state
- Maintained full backwards compatibility while eliminating state duplication
- Implemented centralized
- Phase 3 of Issue #45: Structured Event System
- Implemented comprehensive event hierarchy with semantic event types:
ServiceStateEventfor service lifecycle changes with old/new state trackingHealthEventfor cluster health status updatesDependencyEventfor cascade start/stop operationsUserActionEventfor user-initiated actionsSystemEventfor system-level operations
- Added
EventBusinterface with publish/subscribe functionality - Implemented flexible event filtering system with composable filters:
- Filter by event type, source, severity, correlation ID
- Combine filters with AND/OR logic for complex subscriptions
- Created
EventBusAdapterfor backwards compatibility with existingServiceReporterinterface - Added comprehensive event metrics tracking (published, delivered, dropped events)
- Implemented both handler-based and channel-based event subscriptions
- Added event severity levels (trace, debug, info, warn, error, fatal) for better categorization
- Enhanced correlation tracking with event metadata support
- Provided thread-safe concurrent event publishing and subscription management
- Added extensive test coverage for all event types and bus functionality
- Implemented comprehensive event hierarchy with semantic event types:
- Phase 4 of Issue #45: Testing and Polish
- Added comprehensive integration tests covering end-to-end event flows
- Implemented performance monitoring utilities with
PerformanceMonitorand metrics tracking - Created event batching system with
EventBatchProcessorfor high-volume scenarios - Built
OptimizedEventBuswith configurable performance optimizations - Added object pooling system with
EventPoolManagerto reduce GC pressure - Implemented extensive error recovery testing including panic handling
- Added memory usage monitoring and subscription cleanup verification
- Created comprehensive documentation covering architecture, usage, and best practices
- Fixed race conditions in event bus concurrent access patterns
- Enhanced thread safety across all components with proper synchronization
- Provided migration guides and troubleshooting documentation
- Achieved high test coverage with robust integration and unit tests
- Improved Dependency Management for Service Restarts
- When restarting a service, its dependencies are now automatically restarted if they're not active
- This ensures services always have their requirements satisfied (e.g., restarting Grafana MCP will also restart its port forward if needed)
- Dependencies are restarted regardless of their stop reason to guarantee service requirements
- Clear manual stop reason when restarting a service to allow proper dependency management
- Implemented Issue #46: Improved State Management Between TUI and Orchestrator
- Phase 1: Unified State Management
- Added helper methods to TUI Model to use StateStore as single source of truth
- Implemented state reconciliation on TUI startup to ensure consistency
- Updated TUI controller to use StateStore instead of directly updating model maps
- Eliminated state duplication between TUI Model and StateStore
- Phase 2: Message Sequencing
- Added sequence numbers to
ManagedServiceUpdatefor proper message ordering - Implemented
MessageBufferfor handling out-of-order messages - Added global sequence counter with atomic operations for thread safety
- Added sequence numbers to
- Phase 3: Enhanced Correlation Tracking
- Added
CascadeInfotype for tracking cascade relationships between services - Added
StateTransitiontype for tracking state changes with full context - Enhanced StateStore to record state transitions and cascade operations automatically
- Updated orchestrator to record cascade operations for better observability
- Added
- Phase 4: Improved Error Handling
- Added retry logic for critical updates that are dropped due to buffer overflow
- Implemented
BackpressureNotificationMsgfor user notifications about dropped messages - Added configurable retry attempts with exponential backoff
- Enhanced TUIReporter with retry queue processing and user feedback
- Phase 1: Unified State Management
- Comprehensive Documentation Suite
- Added Architecture Overview documenting system design, components, and principles
- Created Quick Start Guide for new users to get up and running quickly
- Added Troubleshooting Guide with common issues and solutions
- Enhanced development documentation with recent architectural improvements
- Documented dependency management, state management, and message flow in detail
- Configurable Namespace for CR Discovery
- Added
namespaceconfiguration option toconfig.yamlfor Kubernetes CR discovery - Allows specifying which namespace to use for MCPServer, ServiceClass, and Workflow resources
- Defaults to
"default"when not specified - Enables muster to work properly in multi-namespace Kubernetes environments
- Added
- Aggregator Config
- Drop the "Enabled" field (always enabled in modes where it's used)
- Service Manager Refactoring
- ServiceManager now accepts an optional KubeManager parameter for K8s connection services
- Added support for K8s connection services in the service lifecycle management
- Improved service stop handling to report "Stopping" state before closing channels
- Orchestrator Improvements
- Removed old health monitoring methods in favor of K8s connection services
- Updated dependency graph to use service labels for K8s connections (e.g., "k8s-mc-mymc" instead of "k8s:context-name")
- Improved service restart logic to properly handle dependencies
- Dependency graph now includes K8sConnection nodes as fundamental dependencies
- Service manager's StopServiceWithDependents method handles cascading stops
- Health check failures trigger automatic cleanup of dependent services
- Non-TUI mode now uses the orchestrator for health monitoring and dependency management
- TUI mode no longer performs its own health checks - the orchestrator handles all health monitoring and the TUI only displays results
- Proper separation of concerns: orchestrator manages health checks and service lifecycle, TUI only displays status
- Orchestrator now performs initial health check before starting services
- Refactored TUI message handling system
- Introduced specialized controller/dispatcher for better separation of concerns
- Controllers now focus on single responsibilities
- Better error handling and logging throughout the message flow
- Improved startup behavior - the UI now shows loading state until all clusters are fully loaded
- Port forwards no longer start before K8s health checks pass - orchestrator now checks K8s health before starting dependent services
ManagedServiceUpdatenow includesCorrelationID,CausedBy, andParentIDfields for tracingTUIReporternow uses configurable buffered channels instead of simple channels- Service state updates now include correlation information in logs
- Orchestrator operations (stop/restart) now generate and track correlation IDs
- Removed unused
DependsOnServicesfield fromMCPServerDefinition- MCP servers never depend on other MCP servers - Enhanced
RestartServiceto use the newstartServiceWithDependenciesmethod for dependency-aware restarts - Updated
handleServiceStateUpdateto properly restart services with their dependencies - Improved Service Monitoring
- Fixed
monitorAndStartServicesto respectStopReasonDependency- services stopped due to dependency failure won't be restarted until their dependencies are restored - Added automatic restart of dependent services when a dependency becomes healthy again
- Added 1-second delay before restarting services to ensure ports are properly released
- Fixed
- Exit CLI on standalone server failure
- When the mcp-aggregator service (server) fails, the CLI now terminates gracefully
- Port Forwarding State Issue
- Fixed issue where port forwarding services would get stuck in "Stopping" state
- ServiceManager now properly reports the "Stopping" state before closing the stop channel
- Port forwarding processes correctly transition to "Stopped" state
- Code Cleanup
- Removed commented-out
mcpServerProcessstruct that was marked for deletion - Removed duplicate
updatePortForwardFromSnapshotandupdateMcpServerFromSnapshotmethods - Cleaned up unused code and improved code organization
- Removed commented-out
- Dependency-Related Fixes
- Fixed issue where MCP servers would restart even when their port forward dependencies were stopped
- Services with
StopReasonDependencynow properly wait for their dependencies to be restored - When a service becomes healthy, its dependent services that were stopped due to dependency failure are automatically restarted
- Fixed "address already in use" errors by adding proper restart delay
- Fixed spurious error logs when stopping MCP servers
- Suppressed expected "file already closed" errors that occurred when stopping MCP server processes
- Added proper error handling for both stdout and stderr pipe closures during shutdown
- These were harmless errors but created unnecessary noise in the logs
- Fixed cascade stops not triggering when K8s connections fail
- When a K8s connection transitions to Failed state (e.g., due to network issues), all dependent services (port forwards and MCP servers) are now properly stopped
- This prevents orphaned services from continuing to run when their underlying K8s connection is no longer healthy
- Services will automatically restart when the K8s connection recovers
- Set config directory early to avoid bugs handling the empty string (those should be fixed with this change as well)
- Added comprehensive documentation about dependency graph implementation
- Enhanced dependency management documentation with detailed examples
- Added explanation of dependency rules and startup/restart behavior
- Documented the relationship between stop reasons and automatic recovery
- Created comprehensive architecture documentation covering all major components
- Added troubleshooting guide with detailed debugging techniques
- Created quick start guide for new users
- Updated development guide with recent architectural improvements
- Documented the entire dependency management system with visual diagrams
- Updated outdated documentation sections
- Removed obsolete "Package Design for Shared Core Logic" section from development.md
- Updated development.md to reference the unified service architecture
- Fixed test examples in development.md to match current implementation
- Updated README.md prerequisites to remove mcp-proxy requirement
- Clarified non-TUI mode behavior in README.md
- Rewritten MCP Integration Notes in README.md to reflect YAML configuration system
- New helper functions:
NewManagedServiceUpdate(),WithCause(),WithError(),WithServiceData() - New types:
BufferStrategy,BufferedChannel,ChannelMetrics,ChannelStats - Backwards compatibility maintained for existing interfaces
- All existing tests updated and new comprehensive test suite added
- Enhanced MCP server configuration and management capabilities
- MCP server configuration now only supports
localCommandtype for simplicity and reliability
- Streamlined MCP server architecture by removing container support
- Simplified MCP server lifecycle management