Giant Swarm's installation manager: enables, reconciles and verifies platform capabilities on opted-in installations as the person, through MCP tools behind muster. Every write is a pull request to the installation's GitOps repository, opened as the person; nothing is applied to a cluster directly.
giantswarm-platform-manager is the working name. The giantswarm- prefix marks a manager specific
to the giantswarm org — like giantswarm-repo-manager, unlike agent-manager, model-manager and
cluster-manager — and installation manager is its alias in the platform's plans. The final name
is the team's; a rename is one repository rename plus the GitHub App's.
It ships as its own app on the hub installation, never as a component of the agent-platform meta
chart.
The server holds no token of its own, verifies no Dex token and is no muster broker client.
- The caller is the person. Behind muster every request carries the person's GitHub user token —
their one-time authorization of the GitHub App
giantswarm-platform-manager, which muster stores, refreshes and puts on every call. The server verifies the bearer withGET /user(once per token, then from a bounded cache) and every GitHub call of the request runs with it. A request without a bearer, or with one GitHub refuses, is a bare401with the RFC 6750 challenge naming the protected-resource metadata; there is no other path in. - A write's effective rights are the person's own ∩ the App's. The pull request is the person's; GitHub bounds it to what the person may do and to what the App is installed on and permitted.
- Unattended runs, when they come, act as the platform's App identity, never as a person.
The chart's MCPServer (with muster.mcpServer.enabled and oauth.enabled) renders auth.type: oauth pinned to the App as the authorization server — issuer https://github.com/apps/giantswarm-platform-manager,
GitHub's authorize and token endpoints, the App's client credentials from a Secret, grantScope: subject so every session of a person carries the same grant — the pattern of the github and pro
servers on the platform.
Behind muster the tools appear as x_giantswarm-platform-manager_<tool>.
| Tool | Kind | What it does |
|---|---|---|
get_info |
read | The version, the caller (login and id), the pinned authorization server, the capability definitions with their input schemas, the write modes, the write tools, the approval channel configuration and the tools still to come. Call first. |
list_installations |
read | Every installation of the registry with, per capability, its state, the inputs on record and the last action; the opt-in declaration read at call time. installations (names) and customer narrow the answer. |
enable_capability |
write | Enable a capability on one installation (installation) or a set (installations): with dryRun: true the plan — files per repository with the change each one is against the repository now, pull requests in dependency order, generated secrets by name, Dex clients and redirect URIs, the secrets the person supplies at commit (by field), customer actions, probes. inputs are the definition's typed inputs over the facts on record. With mode: "commit" and one installation: the opt-in gate, the Action in pending approval, the pull requests as the person (see The commit); secrets carries the supplied values by field. |
reconcile_capability |
write | The same render over a set (empty: every installation of the registry), every file compared with the repository: all unchanged is an empty diff. Installations not opted in are listed as skipped. |
get_action, list_actions |
read | The Action records on the hub: one per enablement or reconcile a person commits — actor, installations, capability, inputs, pull requests, approval, rollout, probes, result. |
verify_capability |
read | One installation against a capability's definition, grouped into the definition's features with one mark each — as defined, differs by input, drifted — and expanded to its dimensions: the owning repositories' files, read as the person, against the render from the inputs on record (every difference names the file, the path and the input that drives it or drift), and the definition's anonymous HTTP probes. The live dimensions read not checked here: they are verify_installation's. |
verify_installation |
read, live registration | The same installation's running objects against the definition's probes — HelmReleases Ready, workloads Available, Secrets and MCPServer objects present, conditions, logs, the live values against the render — read through muster's kubernetes tools as the person, with the ID token muster forwards to the second registration giantswarm-platform-manager-live (muster.liveServer). What the person may read decides what is checked: an object they may not read is not checked, forbidden for them, an installation they are not connected to answers with muster's own sign-in. The result is recorded on the installation's newest action and feeds list_installations: drifted, or waiting for the customer when the only red dimension is the one the customer's action holds up. A portal or platformctl shows the two verifies as one result. |
watch_action |
live registration | The rollout watch of an action whose pull requests are merged, as the person calling — the manager holds no token beyond a call, so the watch is a call (the portal's page, platformctl action watch, an agent), never a loop. Reads the Flux objects the definition names on the installation rolling out (the HelmReleases with their Ready condition and revision) through muster's kubernetes tools and answers the picture; once every one is Ready it runs the definition's probes — the live dimensions as the person, the anonymous HTTP probes direct — and the stage moves to enabled, waiting for the customer (the customer's own action is the only thing open) or failed (a probe is red, named). The report — pull requests, rollout per object, each probe, the open customer actions — goes into the review's thread and onto the Action (status.rollout.installations[], status.probes, status.result). Nothing is waited for or hurried: call again while it is rolling out. Anyone signed in may watch; the reads are theirs, and the state follows the picture whoever read it. An action waiting for the customer or enabled is re-read: the customer's action done flips it to enabled. |
mode: "commit" of the two write tools takes one installation (a set is the dry run's) and runs, in
this order, writing nothing before the gate:
- The opt-in gate — the one condition the manager checks itself:
management-clusters/<name>/platform-manager.yamlin the installation's management-clusters repository, read as the person at call time, never cached. Absent,optIn: falseor unreadable refuses the commit naming the installation and the file, and records the refusal as an Action in state refused (the installation's state read from its repositories stands). A withdrawn opt-in stops the next action. - The plan, as the dry run renders it; a definition's refusal, a file that could not be compared as the
person, a generated value frozen where no rotation is possible (below), or a supplied secret left out of
secrets(or one the plan does not ask for) refuses the commit before any write. Every file on record already: nothing to commit, no Action. - The Action — created in pending approval with the actor, the capability, the installation and the
inputs (never a secret value:
secretsis its own argument and lands nowhere but the encrypted files). - The files: the plan rendered again with the supplied values; a plain file must be byte-identical to the
plan (a value never lands outside a secret file), the secret files get their generated values and are
encrypted with gitops-commit's
sopsencfor the recipients of the repository's.sops.yaml(read as the person; a repository without one refuses the commit). A secret file on record is kept as long as the render changes nothing outside its values: the manager decrypts nothing, so the two are compared as YAML with the values the record holds encrypted (the fields under the repository'sencrypted_regex) and the values the commit fills in left out — same keys, same metadata, same plaintext fields →unchanged, the values in it frozen and its generated nameskept; nothing is written. A reconcile of an installation the manager enabled therefore rewrites no secret and rotates nothing. A name rotates only when a file of the name has to be written — a file to create (a server's Dex client Secret next to its existing credentials file), an existing file whose plaintext skeleton the render changes (a field added to its template), or a plain file to write carrying a key pair's public half: one new value is drawn and written into every file that holds it, the kept files rewritten and encrypted anew — and every other value a rewritten file holds rotates with it, down to the files sharing those (the server's Valkey password into its Valkey Secret). The dry run says so (generatedSecrets[].frozenIn,kept,rotateswithforcedBy, the file that forced it), the Action records the rotated names (status.rotated), the pull request names them. For the running installation a rotation means both sides roll: the server and the Dex client take the new value with their Secrets, and the client is unusable between the two rollouts. Unseen by the comparison: a literal the render changes under an encrypted field — the record holds it encrypted. A value frozen in a file the definition does not own whole (one with several owners) cannot rotate and refuses the commit naming the file. - The pull requests through gitops-commit, as the person, in dependency order (configs before
management-clusters), one commit per repository, on branch
platform/<action>/<installation>, titled in conventional-commit form —feat(<installation>): enable <capability> (<action>),fix(<installation>): reconcile <capability> (<action>)— so the repositories' semantic-pull-request check passes as opened, the action id in the title and body. The Action records them and stays in pending approval: the approval, the merge and the rollout follow. A failure on the way moves the Action to failed and closes the pull requests opened so far as the person, branches deleted, recorded closed with the reason on the Action; one the remote refused to close stays open on the record, anddeny_action— which takes a failed action too — closes it, records the reason and leaves the action failed.
The answer is the Action, the pull requests and the plan; no secret value appears in it, in a log or in a
pull request. The manager holds no token of its own: GITHUB_API_URL is the GitHub the person's token goes to.
enable_capability and reconcile_capability with dryRun: true render an installation through the
capability's definition (the render library) from the facts on record — the
registry's and the installation's config.yaml.patch — with the person's typed inputs over them. The
schema is the contract: a typed installation.* key overrides the record, an unknown key refuses with its
name, a required choice left out (kagent.enabled, portal.enabled, toolAccess.agentManager,
federation.targets/hubs) refuses naming it — nothing is chosen for the person. A refusal is the
installation's answer in the plan, not a tool error, so a set still answers for the others.
The plan per installation: its state and opt-in, the effective inputs, the files with their repository
(the registry's, not the definition's giantswarm/<customer>-… names), path, rendered content and change
(create, update, unchanged, unknown when the current file could not be read as the person), the
shared-kustomization includes, the generated secrets by name, kind and length — with frozenIn, the files
on record that hold the value already, kept when the value on record stands and no file of the name is
written, and rotates with forcedBy, the file that has to be written, when the commit draws a new value
into every file of the name (see The commit) — the secret values the person
supplies at commit by field (rendered as SUPPLIED(<field>) markers — no secret value ever appears in a dry
run), the Dex clients with their redirect URIs from the rendered dex patch, the customer actions (Secrets the
definition references and never renders) and the probes (the definition's live dimensions). Over the set:
the pull requests, one per repository in dependency order — an installation's configs before its
management-clusters, the hub's pair after, teleport-fleet last — with the files, changes and generated
secrets each carries; the wave's order (Giant Swarm's own test installations, the hub, then the
customers' installations); and skipped with the reason (not opted in, unreadable, no repositories on
record). An installation named as installation is rendered whether or not it is opted in, with
commitRefused saying why a commit would be refused and how its owners opt in. order names another
rollout order for the set (every rendered installation exactly once).
reconcile_capability with mode: "commit" over a set is one wave: one Action, one review listing the
targets in the rollout order and the skipped, the pull requests per installation on their own branches.
merge_action (the actor's) advances it one stage per call — merge the first installation's pull requests
once green; watch_action (the live registration, anyone's) carries the installation rolling out to
enabled; only then does the next merge_action take the next installation's pull requests — and a red
probe stops it: that installation failed, the stages after it not started, their pull requests open, the
result naming where and why. A stage waiting for the customer holds the wave too: merge_action says
so, and the customer's action done flips the stage on the next watch. A wave carries no supplied secret
values; an installation whose secret files are not on record is enabled alone.
Every enablement or reconcile a person commits is an Action — platform-manager.giantswarm.io/v1alpha1,
namespaced, on the hub in the manager's namespace, read and written with the manager's own ServiceAccount:
the record is the manager's, not the person's. spec is written once (actor, capability, kind
enable|reconcile, installations in the wave's order, inputs); status is a subresource (state — one
of pending approval, rolling out, waiting for the customer, enabled, drifted, failed —
pullRequests, approval, rollout, probes, result; refused is the gate's own state). get_action
and list_actions read it; mode: "commit" creates it and moves its state;
list_installations carries the newest Action of an installation and capability as lastAction, and an
unfinished or failed action's state stands over the state read from the files. The chart renders the CRD,
a Role over actions and actions/status in the release namespace and its binding (actions.enabled,
actions.installCRD), and hands the namespace to the server as ACTIONS_NAMESPACE; without it
get_action, list_actions and mode: "commit" refuse with the reason and get_info reports
actions.configured: false.
Every write tool is registered through one framework, which owns two arguments:
dryRun: truereturns the rendered change and writes nothing;mode: "commit"opens the pull request as the person — the only write mode;mode: "apply"is refused for every write tool, present and future, before the tool runs: every target of this manager is GitOps-owned, and a change applied without its commit is drift the next reconcile reverts.get_inforeportscapabilities: {commit: true, apply: false, applyRefused: true}.
list_installations knows every installation from two sources, both read as the caller at call
time:
- the installations catalog —
catalog/installations.yamlin the registry repository (registry.repository/--registry-repository,giantswarm/githubby default): a Backstage catalog file with oneResourceofspec.type: installationper installation. The manager reads its name, the labelsgiantswarm.io/customer,giantswarm.io/provider,giantswarm.io/pipelineandgiantswarm.io/region, the annotationsgiantswarm.io/base(the base domain is<name>.<base>) andgiantswarm.io/account-engineer, and the links of typeCCR(the<customer>-configsrepository) andCMC(the<customer>-management-clustersrepository). An installation whose entry names no repositories is listed and read no further. - the Dev Portal's app-config —
management-clusters/<hub>/extras/backstage/backstage/app-config.yamlin the hub's management-clusters repository, as the catalog names it: thegs.installationsblock (base domain, providers, auth provider). The hub ishub/--hub, the installation this manager runs on.
Because the catalog lives in a repository of its own, the GitHub App giantswarm-platform-manager
must be installed on the registry repository with contents read — next to the customer
-configs/-management-clusters pairs, the hub pair and teleport-fleet — and the caller must be
able to read it. Without that, list_installations refuses with the requirement; the manager never
steps in with a token of its own.
Per installation the manager then reads, as the caller:
- the opt-in declaration
management-clusters/<name>/platform-manager.yamlin its management-clusters repository (below); - the facts on record in
installations/<name>/config.yaml.patchof its configs repository — the meta chart line (agentPlatform.kagentApiV2selects4),services.muster.clientId— which, with the registry's name, base domain, customer and provider and the portals' app-configs (which portals sign people in, whose broker exchanges tokens into it, whether a portal reaches it through the tunnel:private), are the definitions'installation.*inputs: read, never typed; - the enabled marker of each capability: for
agent-platform,installations/<name>/apps/agent-platform/configmap-values.yaml.patch.
The state per capability is not opted in (no declaration, or optIn: false), not enabled (opted
in, marker absent) or enabled (opted in, marker present). Pending approval, rolling out,
waiting for the customer, drifted and failed come from the Action record and the last verify
once those exist; the answer's states block separates the two groups. An installation whose
repositories the caller cannot read is unknown and listed under unreadable, with the reason.
The installation's owners' standing consent that the manager may open pull requests for the
installation's platform capabilities. It is landed by the owners' own pull request in the
installation's management-clusters repository — never by this manager, which would be consenting
on their behalf — at management-clusters/<name>/platform-manager.yaml, read at call time and
never cached. Absent or optIn: false, the installation is not opted in and every answer names
the file's path and the pull request that would add it. The file is not in the directory's
kustomization.yaml: Flux never applies it.
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "platform-manager.yaml — an installation's opt-in for giantswarm-platform-manager",
"type": "object",
"additionalProperties": false,
"required": ["optIn"],
"properties": {
"optIn": {
"type": "boolean",
"description": "true: the manager may open pull requests for this installation's platform capabilities, as the person calling it. false or absent: every tool is read-only for the installation."
}
}
}Flags, each with an environment variable (--listen / LISTEN, --mcp-path / MCP_PATH,
--github-api-url / GITHUB_API_URL, --enable-oauth / OAUTH_ENABLED, --oauth-base-url /
OAUTH_BASE_URL, --oauth-authorization-server / OAUTH_AUTHORIZATION_SERVER, --approvals-url /
APPROVALS_URL, --approvals-channel / APPROVALS_CHANNEL, --registry-repository / REGISTRY_REPOSITORY,
--registry-path / REGISTRY_PATH, --hub / HUB_INSTALLATION); giantswarm-platform-manager -h lists
them. The chart in helm/giantswarm-platform-manager
sets them from its values.
platformctl is the laptop and CI surface, built from cmd/platformctl in this repository and attached
to every release as platformctl-<os>-<arch> (Linux, macOS and Windows on amd64 and arm64, each with its
cosign bundle) next to the image and the chart. It has no logic of its own:
platformctl template <shape> --inputs <file> [--out <dir>]renders a capability's fileset locally by importing the render library — no token, no network. The inputs document carriesinput(the definition's inputs) andsecrets(the values you supply); it is the document of the golden filesets, and the output is their tree —<owner>/<repo>/<path>per file,includes.txtwith the shared kustomization entries — sotemplatereproduces the goldens byte for byte. Shapes:agent-platform.platformctl installation list [<installation>…] [--customer <name>],platformctl installation enable <installation> <capability> --dry-run|--commit [--input k=v]… [--secret f=src]… [--content],platformctl installation reconcile <installation>|--all <capability> --dry-run|--commit [--input k=v]… [--secret f=src]… [--content],platformctl installation verify <installation> <capability>,platformctl action get <name>,platformctl action list [--installation <name>] [--capability <name>],platformctl action approve <name>,platformctl action deny <name> --reason <text>,platformctl action merge <name>andplatformctl action watch <name>call the manager's tools and format the answers for a terminal;--output jsonprints the manager's answer as it is, for CI.--input kagent.enabled=truenests dotted keys into the tool'sinputs; a value that parses as JSON is that value, anything else a string.--dry-runis the tool'sdryRun;--commititsmode: commit— the pull requests opened as you, the Team review asked: for one installation the action, forreconcile --allthe wave over the set, one action rolled out a stage per merge.installation verifycallsverify_capabilityon the App-pinned registration andverify_installationon the live one and prints the two as one result — per dimension the side that checked it; when the live registration does not answer (not registered, not connected), the repository result stands and the live line says why.--secret <field>=@<file>,<field>=env:<NAME>or<field>=-(stdin, one field) supplies a secret the plan'ssuppliedSecretsname; the value is sent once in the call'ssecrets, never printed, and never taken from the command line — a value typed there is refused naming only the field.verifyprints the definition's features with their marks and dimensions;approve,denyandmergeare the review's tools called as you, the manager's answer saying what follows;watchiswatch_actionon the live registration — the rollout picture object by object, the dimensions that decided, the report, what follows — called again while it is rolling out.- The calls go through
muster agent --mcp-server, muster's own bridge: it takes the aggregator from muster's configuration (--endpointnames another) and signs you in to muster when needed. The bridge exposes muster's meta tools only, so every manager tool is called through itscall_tooland the answer read out of the documentcall_toolreturns. A manager you have not connected yet answers with its sign-in URL and exit code 3;muster auth login --server giantswarm-platform-manageris the same sign-in.
render/ turns an installation's capability inputs into the files of its GitOps repositories, with no I/O of its own; see render/README.md.
make test— the Go tests, among them the scenarios ininternal/e2eagainst a fake GitHub: the identity chain (no bearer is a bare 401, a refused bearer isinvalid_token,get_infonames the person, the bearer is verified once per token, the framework refusesmode: apply) andlist_installationsover an invented registry — an installation in each state the repositories can show, one whose repositories the caller may not read, one the portal alone knows, the opt-in read on every call, the filters, a registry the caller cannot read, a call without a caller.make scenario-test— the muster half in muster's own scenario harness (tests/scenarios, needs themusterbinary onPATH): a mock authorization server standing in for GitHub as the App, the consent once, the token on every call, a second session adopting the grant, a person without a grant told where to sign in. CI runs both in thescenario-testjob (.circleci/custom.yml).make helm-lint,make helm-template,make helm-schema— the chart with its default, platform (tests/oauth-values.yaml) and lab (tests/lab-oauth-values.yaml) values.make docker-build— a local image; CI builds and publishes the multi-arch image and the chart on every tag.make build-platformctl— the CLI for this machine; CI cross-compiles it and attaches the binaries to the release (go-build-platformctlandupload-platformctlin.circleci/custom.yml).
Public repository: every fixture uses invented installation names and placeholder values.