
giantswarm/agent-manager
Developer toolsAgent lifecycle service for the Agent Platform: create, update, delete and inspect kagent agents (Flux HelmReleases of the agent chart) via REST and MCP
Overview
Agent lifecycle service for the Giant Swarm Agent Platform: the write surface for , the sibling of model-manager (models) next to muster's own management tools (MCP servers, workflows). On the platform an agent is a Flux HelmRelease of the Generic agent chart (2.x). One release renders one api.kagent.dev/v1alpha3 Agent (the template inline under spec.template, the Harness by name in spec.harnessRef) plus the agent's own muster RemoteMCPServer, from the shared per-namespace OCIRepository of that chart, tracking it at 2.x. That is exactly what the portal's create flow composes; agent-manager composes the same two objects from a small, curated argument set, pins every skill to an immutable source, validates the values against the chart's values.schema.json anything is applied, and reads an agent back from its Agent object, its RemoteMCPServer and its owning HelmRelease (Flux provenance labels). Readiness is the Agent's own status.
README
agent-manager
Agent lifecycle service for the Giant Swarm Agent Platform: the write surface for agents themselves, the sibling of model-manager (models) next to muster’s own management tools (MCP servers, workflows).
On the platform an agent is a Flux HelmRelease of the Generic
agent chart (2.x). One release
renders one api.kagent.dev/v1alpha3 Agent (the template inline under
spec.template, the Harness by name in spec.harnessRef) plus the agent’s
own muster RemoteMCPServer, from the shared per-namespace OCIRepository of
that chart, tracking it at 2.x. That is exactly what the portal’s create
flow composes; agent-manager composes the same two objects from a small,
curated argument set, pins every skill to an immutable source, validates the
values against the chart’s values.schema.json before anything is applied,
and reads an agent back from its Agent object, its RemoteMCPServer and its
owning HelmRelease (Flux provenance labels). Readiness is the Agent’s own
status.conditions; there is no per-agent Deployment or pod, agents run as
Substrate actors.
The same operations are exposed twice from one process:
- REST/JSON under
/api/v1for the portal backend — contract inapi/openapi.yaml, also served at/api/v1/openapi.yaml. - MCP (streamable HTTP,
/mcp) for muster — toolsget_info,list_agents,get_agent,create_agent,update_agent,delete_agent,get_agent_status,validate_agent,list_model_configs,list_skills,list_sessions,get_session,get_session_turn,start_session(through muster:x_agent-manager_). Every tool description says whether it writes and what it writes, and every tool sets all four annotations: read-only, destructive, idempotent and open-world (true for the tools that resolve a caller-named GitHub repository or OCI reference).
Part of the Agent Control Plane epic
(“create and manage versioned agents”): the reconciler half is the agent
chart plus Flux helm-controller and the kagent controller, this is the
MCP-server-writer half.
API at a glance
| Operation | REST | MCP tool | Writes |
|---|---|---|---|
Version, chart (OCI URL, 2.x range, latest version, schema in use), managed namespaces, capabilities, API versions, platform Harness, muster URL, identity |
GET /api/v1/info |
get_info |
no |
Agents of a namespace (Agent objects + HelmReleases of the chart not rendered yet), one summary each: display name, description, model config, Harness, toolset, readiness, management mode, and a suspended, deleting or failing release; details reports each in full |
GET /api/v1/agents?namespace=[&details=true] |
list_agents |
no |
| One agent with its HelmRelease values, Harness, pinned skills and plugins, toolset and the Agent’s status | GET /api/v1/agents/{ns}/{name} |
get_agent |
no |
| Create: OCIRepository (when missing) + HelmRelease, after skill pinning, schema, ModelConfig and Harness validation | POST /api/v1/agents |
create_agent |
HelmRelease, OCIRepository |
Update: merge into the HelmRelease values (refreshSkills re-pins git skills), validate, update |
PATCH /api/v1/agents/{ns}/{name}[?force=true] |
update_agent |
HelmRelease |
| Delete: the HelmRelease; the OCIRepository only when nothing else references it | DELETE /api/v1/agents/{ns}/{name}[?force=true&mode=&dryRun=] |
delete_agent |
HelmRelease, OCIRepository, (bare Agent with force) |
| Status verdict: the Agent’s conditions, HelmRelease conditions/history, Warning events | GET /api/v1/agents/{ns}/{name}/status |
get_agent_status |
no |
| Dry run of create/update: pinned skills, composed manifests + every violation | POST /api/v1/agents/validate |
validate_agent |
no |
| kagent ModelConfigs of a namespace | GET /api/v1/modelconfigs?namespace= |
list_model_configs |
no |
Skills (SKILL.md) of the configured GitHub repositories, grouped by repository with its head commit |
GET /api/v1/skills[?repository=&ref=&refresh=] |
list_skills |
no |
Sessions of an agent, as the caller (SessionService/ListSessions; allCreators when the controller grants it) |
GET /api/v1/agents/{ns}/{name}/sessions[?allCreators=true] |
list_sessions |
no |
Start a session: create it (in a workspace with workspaceGrant), send the first message when one is given and answer once the turn is accepted; without message only create it |
POST /api/v1/agents/{ns}/{name}/sessions |
start_session |
a kagent Session |
One session and its conversation, one turn per A2A task of its context, oldest first, within maxBytes: a summary per turn by default, a transcript or the raw tasks on request |
GET /api/v1/sessions/{id}[?detail=&lastTurns=&maxBytes=] |
get_session |
no |
One turn of a session: its messages, paged within maxBytes, or one tool call whole |
GET /api/v1/sessions/{id}/turns/{turn}[?detail=&toolCall=&fromMessage=&maxBytes=] |
get_session_turn |
no |
| Delete one of the caller’s sessions and its runtime state | DELETE /api/v1/sessions/{id} |
delete_session |
the kagent Session |
| Health | GET /healthz, GET /readyz |
— | no |
The three writes take dryRun and mode (apply | commit, see
Write modes); in commit mode the “Writes”
column is files of a pull request, nothing live. validate_agent takes
mode too and answers as the write would.
Errors are {"error":{"code":"not_found|invalid_request|conflict|gitops_owned|forbidden|unauthenticated|auth_required|unsupported|backend_error","message":"…"}};
conflict (409) covers “exists already”, “suspended” and “bare
Agent”, the cases force overrides where documented;
gitops_owned (409) is an apply-mode write to a release applied from git,
auth_required (401) a commit without the caller’s GitHub authorization.
What an agent is made of
create_agent takes the technical name (a DNS-1123 label the caller
chose and confirmed — the service never derives one from a display name, per
the creating-agents PRD), the modelConfig (must exist in the namespace;
the error lists the valid ones), the toolset (required, see below), and
optionally harness, displayName, description, systemMessage,
iconUrl, skills and plugins (see below), egress (the HTTP(S) origins
the agent may reach besides what its revision compiles, such as
https://github.com:443 or https://*.githubusercontent.com; chart
agent.egress, the Agent’s spec.egress), labels, annotations,
namespace. It emits only what was set so the chart’s defaults apply to
everything else (the portal’s rule) plus the platform’s own two values, the
Harness (agent.harness) and, when configured, the muster URL (muster.url),
and composes:
apiVersion: source.toolkit.fluxcd.io/v1
kind: OCIRepository
metadata: {name: agent, namespace: kagent}
spec: {interval: 30m, url: oci://gsoci.azurecr.io/charts/giantswarm/agent, ref: {semver: 2.x}}
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata: {name: coder, namespace: kagent}
spec:
interval: 10m
chartRef: {kind: OCIRepository, name: agent, namespace: kagent}
values:
agent:
name: coder
displayName: Coder
systemMessage: …
harness: claude
egress: [https://github.com:443, "https://*.githubusercontent.com"]
modelConfig: {name: default-model-config}
skills:
- {name: runbooks, path: runbooks, git: {url: https://github.com/giantswarm/agent-skills, commit: 0123456789abcdef0123456789abcdef01234567}}
- {name: kubectl, oci: ghcr.io/giantswarm/skills/kubectl@sha256:5b0b…1270}
plugins:
- {git: {url: https://github.com/giantswarm/agent-skills, commit: fedcba9876543210fedcba9876543210fedcba98}, path: bundles/sre, skills: [triage, postmortem]}
toolset: [preset:read-only, workflow:incident-triage]
muster: {url: http://muster.agent-platform.svc.cluster.local:8090/mcp}
The chart renders the Agent (spec.harnessRef.name: ,
spec.egress: ; under
spec.template the description, systemPrompt, modelConfig,
skills[], plugins[] and the muster binding; the annotations
ui.giantswarm.io/display-name and ui.giantswarm.io/icon-url) and the
agent’s own RemoteMCPServer (named after the agent, pointing at muster,
carrying the toolset header). No admission label is involved: the Agent
selects its Harness by name. ModelConfigs, their Secrets and the Harnesses are
platform-admin owned: agent-manager only reads them.
The Harness and plugins
An agent runs on the platform Harness (kagent, the Go ADK) unless
create_agent names another Harness of the namespace in harness; the name
must be a Harness that exists there (the refusal lists the ones that do) and
it is fixed at create. A Harness’s runtime is the one key of its spec:
kagent (the Go ADK), claude (Claude Code), codex (Codex) or byo (an
image of its own, such as omp). On every runtime but kagent the agent is a
coding agent: it runs a coding CLI as a session, and its skills and plugins
are that CLI’s.
Guards on a coding Harness
A coding agent runs a coding CLI with a shell in its sandbox, so
validate_agent, create_agent and update_agent hold an agent on a
Harness whose runtime is not kagent to these rules, so a Harness added later
inherits them (an agent on a kagent Harness is not touched). Its toolset is
the person’s choice, as on any other agent: any muster tool, preset, server or
workflow, write tools included, and muster authorizes every call as the
person.
- No
requireApprovaland no bindings beside muster’s (muster.requireApproval,extraTools,extraAgentSpec.tools): it reaches its tools through muster only, and approval is no boundary against a coding CLI, which can answer it from its own sandbox. - The platform’s egress.
--harness-egress(chartharness.egress) lists the origins per coding Harness, the installation’s forge and git hosts and the toolchain’s registries. agent-manager writes that list intoagent.egress(the Agent’sspec.egress) on every write and refuses any other origin. A coding Harness without an entry is refused with a message naming the missing entry; an empty list is how it reaches no origin. - The egress note. The system prompt ends with a short section, from its
heading
## Network access (set by the platform)on, naming those origins and saying that a denied request is policy, not an outage; it is rewritten on every write. - A model the runtime reaches, by the rules kagent’s compiler for it
enforces: on
claudean Anthropic, Bedrock or AnthropicVertexAI ModelConfig; oncodexan OpenAI ModelConfig withopenAI.apiFormat: responsesor a Bedrockgpt-*one. Abyoruntime’s model is its image’s to check (omp takes the Anthropic-protocol ModelConfig claude uses). - No Claude-format plugin on
codex: a git plugin with a.claude-plugin/plugin.jsonmanifest and no rootplugin.jsonis refused, never converted; Codex reads the Agent Plugins format only. An OCI plugin’s format is reported by the runtime. MCP servers oncodexspeak Streamable HTTP only; a coding agent binds none but muster’s.
The guards are write-time: a HelmRelease applied directly bypasses them.
pluginsis a list of Agent Plugins bundles,{git: {url, commit}, path, skills}or{oci: @sha256:, path, skills}(chartplugins[],spec.template.plugins[]). A plugin is written as given and must come pinned: a full commit id or a digest, never a branch, tag or image tag.skillsnames the bundle’s skills to enable (at least one, unique); nothing else of the bundle reaches the agent. A private git plugin reads with the same credential as a private git skill.
There is no runtime argument: a request still carrying runtime is refused
with the reason, and the 0.x skills.gitAuthSecretName is refused naming its
replacement, the top-level gitAuthSecretName.
Skills are pinned
kagent API v2 reads skills from immutable sources only: a git repository at a
full commit id, or an OCI image by digest. skills is a list of
{name, path, git: {url, ref | commit}}— a git skill.commit(40 or 64 hex characters;list_skillsreports it per repository) is the pin.ref— a branch or a tag — is resolved to its head commit at write time through the GitHub API (--skills-github-api,GITHUB_TOKEN); neither means the head of the repository’s default branch. Give one, not both.{name, oci: /: | @sha256:}— an OCI skill; a tag is resolved to its digest against the registry.
What is written is always the pin, and get_agent reports it. A reference
that cannot be resolved — a private repository without a token, an unknown
tag — is a 400 naming the repository or image; nothing is written.
update_agent with refreshSkills: true re-resolves every git skill of the
agent to the head of its repository’s default branch (a skill passed in
skills with a ref in the same call goes to that ref’s head) and changes
nothing else on the release — the way to pick up new skill commits. name
defaults to the last path segment (else the repository or image name) and
must be unique per agent; path is relative without ...
Private skills
Two credentials are involved, neither of them a person’s token:
- Discovery and pinning (
list_skills, resolving aref) read GitHub with the read-only skills GitHub App’s short-lived installation tokens (--skills-github-app-id,--skills-github-app-installation-id,--skills-github-app-private-key-file; chartskills.github.app.secretName, a Secret withapp-id,installation-idandprivate-key), at the App’s rate limit. A staticGITHUB_TOKEN(chartskills.github.tokenSecret) is the alternative; the two are mutually exclusive. - Boot: the platform fetches an agent’s git skills and git plugins with
the Secret named by the chart value
skillsGitAuthSecretRef.name(keytoken, in the agent’s namespace).create_agent,update_agentandvalidate_agenttakegitAuthSecretName; omitted, every agent with a git skill or plugin gets the installation’s (--skills-git-auth-secret-name, chartskills.gitAuthSecretName, reported byget_infoasskillsGitAuthSecretName). An update keeps the credential in step with the skills and plugins (an agent written before the credential existed gets it on its next update) and drops it when no git source is left. With--skills-git-auth-mint(chartskills.mintGitAuthSecret) that Secret carries the skills GitHub App’s installation token too: the chart renderstenant-credential--skills-tokenwithout data in every managed namespace with a Role that may only get and update it, and agent-manager writes the token (the Basic form ofx-access-token:) and replaces it well before its one-hour expiry. The egress gateway reads the Secret on every fetch, so a replaced token needs no restart.get_inforeports the last refresh and a failed one asskillsGitAuthMint. - Credential names: the installation’s skills credential carries the
tenant-credential-prefix, the one Secret name an agent’s gitcredentialRefmay carry; agent-manager warns at start when it does not. After a rename,--skills-git-auth-previous-secret-names(chartskills.previousGitAuthSecretNames; the minted Secret’s earlier name-skills-tokenis always one) lists the old names: an agent naming one is composed with the current name on its next update,agent-manager migratemoves every other one once the Secret of the current name holds its token, waiting up to--skills-git-auth-waitfor agent-manager’s rollout to create and fill it (a GitOps-owned release gets the diff), and while minting the old Secrets are rendered and filled too, so an agent not yet moved keeps booting.
A private repository is only for the people who can read it themselves:
list_skills shows it only to a caller whose GitHub login has read access
(GET /repos/{owner}/{repo}/collaborators/{login}/permission), and
create_agent, update_agent and validate_agent refuse a skill from it
otherwise (403 forbidden, naming the repository). The login is the one the
GitHub App pin verifies (see “Write modes”); a caller without one can use
public repositories only.
The toolset
Every agent declares a toolset: the list of selectors that bounds which of
the gateway’s tools its meta-tools can see and call. It is the agent chart’s
top-level toolset value, which the chart renders as the X-Muster-Toolset
header on the agent’s own RemoteMCPServer; muster resolves it per request
and per caller. agent-manager validates the inline grammar and composes the
list exactly as given — it never resolves a toolset, and it never writes
muster.tools (that key narrows the MCP binding, not the toolset; the former
toolNames argument narrowed nothing and a caller still passing it is told
so).
- Selectors:
preset:,server:,workflow:,tool:— exact, case-sensitive names; at most 32 inline (define a preset for more).toolset:is reserved;label:selectors exist inside presets only. - Presets every installation has:
read-only,none,infrastructure,agent-platform,full.create_agentrefuses a request without a toolset and names them:preset:nonefor a chat-only agent without tools,preset:fullfor the deliberate choice of every tool the gateway exposes. An empty list is refused too, so “no tools” is never confused with implicit full access. update_agentreplaces the whole list — the edit path, and the way agents that predate toolsets get one.get_agent/list_agentsreport the declaredtoolset, orimplicitFullAccess: truefor a release without one (a bare Agent’s toolset is read from its RemoteMCPServer’s header).
It is composition, not authorization: the invoking human’s identity and the backends’ own authorization remain the boundary.
Agents on a workload cluster
An agent can run on a workload cluster of the installation, managed from the
installation: create_agent (and POST /api/v1/agents) take organization
and cluster, and every read and write tool takes the same pair (REST: the
query parameters organization and cluster). namespace is then the
namespace on the workload cluster. get_info reports capabilities.targetCluster.
- The HelmRelease stays on the installation, in the organization
namespace
org-, named-and labelledagent-platform.giantswarm.io/cluster:, beside the shared OCIRepository of the agent chart there. It carriesspec.kubeConfig.secretRef(-kubeconfig, keyvalue),releaseName:andtargetNamespace/storageNamespace:, so the installation’s helm-controller installs the chart on the workload cluster. It carries noserviceAccountName: Flux would impersonate it on the workload cluster, where it does not exist. A helm-controller with the multi-tenancy lockdown (--default-service-account) impersonates that default account oforg-on the workload cluster instead: it must exist there with the rights to install the agent chart. - The kubeconfig Secret contract is Cluster API’s: the Secret
-kubeconfiginorg-with the kubeconfig under the keyvalue. agent-manager reads it as the caller and keeps only its server address and CA; the credentials in it are never used when the server acts as the caller (oauth.downstream). Running as the ServiceAccount, the kubeconfig is used as it is. - The agent, its ModelConfig and its Harness are on the workload cluster:
the Agent, its RemoteMCPServer, the ModelConfigs
modelConfigmust name, the Harnesses and the agent’s events are read there. The cluster runs the agent runtime slice of theagent-platformchart (kagent and its CRDs, Substrate, agentgateway, the platform Harness). - Identity precondition: the reads on the workload cluster present the caller’s token, so the workload cluster’s apiserver must trust the installation’s identity provider (OIDC issuer and client id set when the cluster is created) and grant the caller read access to the kagent resources and events of the namespace. Otherwise the call answers 401 and names that precondition.
- Egress: the agent reaches muster at
--target-muster-url(AGENT_TARGET_MUSTER_URL, chartmuster.targetUrl), the installation’s public muster endpoint; unset, a target is refused. The installation’s skills credential exists on the installation only and is not composed: an agent on a workload cluster with private git skills names its own Secret there (gitAuthSecretName).
Without organization and cluster nothing changes: the objects are the
ones composed for the installation’s own cluster.
Status
get_agent_status folds three sources into ready | progressing | failed | unknown and one sentence:
- the Agent’s
status.conditions, reported by the Harness it references (spec.harnessRef.name):readywhenReadyis True anddesiredRevisionequalslatestSuccessfulRevision;progressingwhile a revision compiles (desiredRevisionahead, orReadyFalse with reasonActorTemplatePending);failedwith the condition’s message whenAccepted,ResolvedRefsorCompatibleis False (a Harness that does not exist isResolvedRefsFalse) or whenReadyis False for another reason. The Agent’sstatus.warningsride along. - the owning HelmRelease’s conditions and recent history (a failed render is
failed; not yet reconciled isprogressing); - the namespace’s recent Warning events on the agent’s objects.
Ownership and the meta agent’s rule
list_agents says how each agent is managed:
helmrelease— a HelmRelease agent-manager (or the portal, or a hand-applied manifest) owns: writable here.gitops— the HelmRelease carrieskustomize.toolkit.fluxcd.io/name: its desired state lives in git and a live write would be undone on the next reconciliation.update_agentanddelete_agentrefuse it in modeapplywithgitops_owned,forceor not; modecommitchanges it with a pull request in the repository that owns it, andvalidate_agent(update: true) follows the same rule, so the dry run of such a change names modecommit. The release may live in another namespace (the fleet’ssre-agentreleases sit influx-giantswarmwithtargetNamespace: kagent); the Agent’s provenance labels lead to it.none— a bare Agent with no HelmRelease behind it: nothing to write to;delete_agentremoves it only withforce.
A suspended HelmRelease is refused the same way: Flux drops its finalizer
without uninstalling, so deleting it would leave the rendered objects behind
(with force the Agent and the agent’s RemoteMCPServer are deleted too).
Validation
Every create and update is validated against the agent chart’s
values.schema.json before it is applied. The schema comes from the chart
registry (agentChart.ociUrl, the newest version in agentChart.semver,
2.x, the same resolution Flux’s OCIRepository performs, so a pre-release
build never counts) and is re-read every agentChart.refresh; when the
registry cannot be reached the copy compiled into the binary (the chart 2.x
contract) validates and get_info reports chart.schemaSource: embedded with
the error. The schema refuses every 0.x key the contract removed
(agent.runtime, replicas, resources, nodeSelector, tolerations,
muster.serverRef, muster.allowedHeaders, muster.stsWellKnownUri,
skills.gitAuthSecretRef; muster.toolNames became muster.tools), and
agent-manager never composes one. validate_agent returns the composed
manifests — skills pinned — and every violation without writing, a name that
is already taken among them. A check it cannot make, such as the name check
for a caller who may not read the namespace’s HelmReleases, is listed in
notes instead of failing the dry run.
Write modes: apply and commit
Every write tool (create_agent, update_agent, delete_agent) renders the
standard manifests a kubectl or GitOps user would apply, returns them on
dryRun: true and writes nothing then, and takes mode:
apply(default) writes live as the caller. It never writes a release applied from git: that answer isgitops_owned, namingmode: commitand the repository and directory it would write.commitwrites the manifests as files and opens a pull request as the caller; nothing is written live. The target is derived from Flux: thekustomize.toolkit.fluxcd.io/name|namespacelabels of the release (update, delete) or, for a new agent, of the namespace’s chart source, one of its agent releases or the Namespace → the Kustomization’sspec.path→ its GitRepository’s URL and branch. Where nothing GitOps-owned lives yet,repository(owner/name),branchandpathname it. The files follow gitops-commit’slayout, the one cluster-manager and model-manager write: anagent-manager/directory under that path, one file per object (.yamlfor the HelmRelease,agent.yamlfor the chart source when the namespace has none yet), itskustomization.yamland the parent’s entry. An update rewrites the release’s file and a delete removes it (and the chart source’s when no other release uses it); a release defined elsewhere in the repository is refused. A Kustomization that does not prune leaves a removed release in place, which the answer’sliveStepsnames. The result carriescommit: {repository, base, directory, kustomization, prune, branch, files: [{path, action, content (dry run)}], pullRequest, number, author}.
Commit mode needs the GitHub App pin (github.enabled, below) and reports
itself in get_info as capabilities.commit; without it mode: commit
answers unsupported. A call that carries no GitHub authorization answers
auth_required, naming the one-time consent (core_auth_login server=agent-manager).
Identity
The caller, not the ServiceAccount. With --enable-oauth agent-manager is an
OAuth 2.1 resource server (mcp-oauth)
in front of both the MCP endpoint and the REST API; providers dex (with
--dex-ca-file and --allow-private-oauth-urls for an in-cluster Dex) and
google. Nobody logs in to agent-manager itself: muster forwards the session’s
IdP id_token byte-identical (MCPServer auth: {type: oauth, forwardToken: true, requiredAudiences}, rendered by the chart under muster.mcpServer.auth) and
the portal sends the signed-in user’s id_token through the gateway; both are
validated against the IdP’s JWKS because their audience is in
--oauth-trusted-audiences. Which audience that is depends on the session:
muster’s own sessions and MCP clients sign in through the platform OAuth
client, a portal session forwards the id_token of the portal’s own client —
and every forwarded token carries the audiences the MCPServer requires, the
ones the kube-apiserver trusts. So the chart trusts both: oauth.trustedAudiences
(default [global.identity.clientId]) plus muster.mcpServer.auth.requiredAudiences,
always. A token for none of them is refused with 401; the log line and the
WWW-Authenticate description name its aud next to the trusted audiences
(muster shows the description in its session hint). The caller
(internal/identity: subject, email, groups, source sso|oauth) is on every
write’s log line (caller=) and on every create/update/delete result as
requestedBy.
--downstream-oauth presents the caller’s token to the kube-apiserver for
everything a request does (the HelmRelease and OCIRepository writes, the
Agent, RemoteMCPServer, Harness, ModelConfig and event reads) through
per-caller clients (internal/kube.CallerProvider, built from
rest.AnonymousClientConfig + the caller’s bearer, cached until the token’s
exp). The user’s RBAC governs; the ServiceAccount holds no permissions
(the chart renders no Role or RoleBinding with oauth.downstream.enabled) and
there is no fallback: a request without an IdP token is refused with 401, a
token that expires mid-request keeps being presented and the apiserver’s 401
fails the request, attributed to the caller. Only background work runs as the
ServiceAccount, with RBAC of its own: the tenant reconciler (below) and the
skills boot Secret; API version discovery at startup is what every
authenticated principal may read. The apiserver must trust the token: with Dex the
audience it trusts (dex-k8s-authenticator on Giant Swarm clusters,
kubernetes in agentlab) is requested as a cross-client scope through
requiredAudiences; a Google IdP has no cross-client scopes and the platform
client id is the apiserver’s --oidc-client-id (requiredAudiences: []).
get_info reports identity: caller and capabilities.writesAsCaller: true.
With --github-authorization-server (chart github.enabled) the MCP
registration is pinned to agent-manager’s own user-to-server GitHub App,
giantswarm-agent-manager: the MCPServer’s auth names the App as the
authorization server with forwardIdentity: true, so muster runs the App’s
consent once per person and puts the person’s App user token on every MCP call
as the bearer, and the person’s IdP ID token in X-Muster-Id-Token. The server
verifies the GitHub token with GET /user (cached for 15 minutes), keeps it
for commit mode’s pull request, and validates the ID token exactly as a
forwarded bearer, so apply mode and every read still act on Kubernetes as the
person. The REST API keeps taking the ID token as its bearer. No GitHub
credential other than the App’s OAuth client (read by muster) exists in the
pod or the chart. Operator precondition: the App installed with contents and
pull-request write on every repository a commit may target.
Without --enable-oauth the service checks no identity and acts as its
ServiceAccount (the Role per managed namespace: HelmReleases and
OCIRepositories read/write; Agents, RemoteMCPServers, Harnesses,
ModelConfigs and events read; Agents and RemoteMCPServers delete for
the forced cases) — only for a server nothing but a trusted proxy (the
agentgateway JWT policy, muster) can reach.
Tenants
With --multi-tenant (chart tenants.enabled) every namespace labelled
giantswarm.io/organization (an Organization’s org-; on an
installation without Organizations the platform team labels it) is a managed
namespace beside --kagent-namespace and --managed-namespaces, discovered at
run time; --kagent-namespace itself never is one. The tenant reconciler
(internal/tenants, a controller-runtime controller in the same process) runs
as the ServiceAccount and keeps in each tenant what follows. It reconciles a
tenant at once when the namespace, an agent in it, one of its copies or a
platform object they are copied from changes, and every tenant every
--tenant-interval (default 10m) as a safety net. Of several replicas, the
one that holds the Lease agent-manager-tenants in the pod’s namespace
reconciles; every replica knows the tenants. Its cache holds the labelled
namespaces, its own copies, the objects of --kagent-namespace it copies
from, and of the Secrets only --tenant-model-secret. Outside a cluster, set
--tenant-leader-election-namespace. Every kind it keeps must be served
(Agent, Harness, ModelConfig, WorkerPool, HelmRelease, OCIRepository and the
--tenant-network-policy-flavor kind): without one, agent-manager does not
start, and its error names each missing kind. When the reconciler stops at run
time (a lost Lease, say), the API keeps serving: the replica logs the error,
get_info reports it in tenantReconciler (running, error, failedAt),
and a new reconciler starts after 10 seconds (the delay doubles, up to 5
minutes, while new ones fail). Until it starts, that replica’s API uses the
tenant namespaces known at the failure. A new reconciler reconciles only once
it holds the Lease, so one replica reconciles at a time.
- always: the agent chart’s OCIRepository, the
--helmrelease-service-accountServiceAccount with a RoleBinding toagent-platform-tenant-flux(Secrets (see below),agentsandremotemcpservers), and the network policies--tenant-network-policiesnames, copied from--kagent-namespace; - while the namespace holds an agent (an Agent, or a HelmRelease of the agent
chart that runs on this cluster): copies of the platform Harness, its
WorkerPool with
--tenant-worker-pool-replicasreplicas and the ServiceAccount its workers run as, and the default ModelConfig (--tenant-model-config) with its key Secret (--tenant-model-secret, the only Secret it may copy). They go with the last agent, so a tenant without agents runs no workers.
It writes only objects it labels agent-manager.giantswarm.io/tenant-object
and never changes or deletes another, so a tenant’s own ModelConfig stays. A
create in a tenant may name the default ModelConfig before its copy exists.
It never writes memberships: who may use a tenant is declared in RoleBindings
of the namespace.
Tenants need --downstream-oauth: a request in a tenant runs as the caller,
whose RoleBindings there grant it. An agent in a tenant gets no installation
skills credential (--skills-git-auth-secret-name): that Secret exists in the
configured namespaces only, so it names its own. The reconciler creates a
tenant’s OCIRepository once and never moves its range; agent-manager migrate
does not cover the tenants yet.
The chart renders the reconciler’s ClusterRole (bind on
agent-platform-tenant-flux only, no escalate), and a
ValidatingAdmissionPolicy that refuses the ServiceAccount’s writes of those
kinds outside labelled namespaces, a RoleBinding to any other role or
subject than the agents’ Flux ServiceAccount, and any other Secret: RBAC cannot select namespaces by label.
The agents’ Flux identity can read and write every Secret of the tenant.
helm-controller keeps an agent’s Helm release Secrets
(sh.helm.release.v1..v) in the HelmRelease’s namespace, and
Helm’s Secret storage driver calls get, list, create, update and
delete on them (agent-platform-tenant-flux grants these and no more). RBAC
cannot narrow a list, so that identity can read every Secret of an
organization namespace, the CAPI cluster kubeconfigs included, and a chart
rendered through it can copy such a Secret with Helm lookup into an object
it renders. With create, update and delete such a chart can also
overwrite or delete any Secret there, and get the token of any ServiceAccount
there (a kubernetes.io/service-account-token Secret it renders, read back
with lookup), automation (cluster-admin in an org namespace, from
rbac-operator) included. Whoever may create or change a HelmRelease or an
OCIRepository in the namespace can trigger that: on a Giant Swarm management
cluster, the organization’s admins (cluster-admin there) and the members
bound to agent-platform:editor or agent-platform:admin, which grant those
writes but no Secret read. This is accepted: those members can already name
automation as a HelmRelease’s ServiceAccount and so reach cluster-admin in
the namespace without this role. The plan for giantswarm/giantswarm#36698
tracks closing that path.
Every create records the caller as agent-platform.giantswarm.io/created-by
on the HelmRelease and, through the chart’s annotations value, on the
Agent; an update keeps it and nobody can set or change it. createdBy on
get_agent and list_agents is for display and filtering only, never for
authorization. get_info lists in namespaces.managed only the managed
namespaces in which the caller may create a HelmRelease.
Sessions
list_sessions, get_session, get_session_turn, start_session and
delete_session let a caller, a meta agent working through muster for
instance, talk to an agent, read what it did and remove the conversation. A
conversation is a kagent Session of the Agent (kagent 1.3, SessionService),
held in the controller’s database. agent-manager calls the controller’s gRPC
API through agentgateway at --kagent-target (AGENT_MANAGER_KAGENT_TARGET,
chart kagent.controllerTarget, default the in-cluster
grpc://agentgateway.agent-platform.svc.cluster.local:8080) with the caller’s
IdP token as the bearer and no other identity: agentgateway validates the token
on the controller route and sets the identity the controller reads from it. The
calls never run as the ServiceAccount, so the target needs --enable-oauth,
and without the caller’s token a session operation is 401 unauthenticated.
-
start_sessionisSessionService/CreateSessionfor the Agent (409 conflictwhile the Agent has no ready revision), then the first message overA2AService/SendStreamingMessagewith the Agent astenant(/) and the session asmessage.contextId. It answers once the agent has accepted the turn; the turn keeps running when the stream closes, andget_sessionshows it finish.requestIdmakes a retry idempotent: the samerequestIdanswers the session the first attempt created, and when its first turn already carries the first message (the message id is derived from therequestId) it answers that turn’s latest state and sends nothing, so a retry after a lost answer starts no second turn. -
messageis optional: without itstart_sessiononly creates the session and answers{session, workspace}with noturn. The caller then sends the first message itself over A2A, with the Agent astenantand the session’scontextIdasmessage.contextId, for instance one that carries file parts or that the caller streams. A whitespace-onlymessageis refused. -
workspaceGrantonstart_sessionstarts the session in a workspace: the grant is the signed, short-lived token a workspace manager’sauthorize_workspacereturned to the caller once they may read every repository of the workspace, naming the person (sub, their email), the workspace, its volume and the directories of it a session may mount. agent-manager holds no key: it decodes the grant without trusting it, refuses one that names another person, has expired or cannot be read (403or400with the reason, nothing created), and composes the request’svolume_sourcefrom it, the session’s own directorysessions/${SESSION_ID}read-write at/workspaceand the workspace’s git mirrors read-only at/mirrors, kagent’s fixed layout, with the grant as the request’sadmission_token. kagent’s admission verifies the grant against the workspace manager’s published keys and refuses with its reason (403). The session shows itsvolumeSource, and the answer names theworkspace. Without a grant the request is exactly the one the surfaces send today. The grant appears in no log line, error or response. -
get_sessionisSessionService/GetSessionplusA2AService/ListTasksaddressed to the session’s Agent and filtered by its context. A turn is one task, indexed from 0 oldest first. The tasks are listed without their history, and only the turns answered are read whole (A2AService/GetTask), newest first. A session’s tasks carry every tool call and response, so the answer is shaped to fit a caller’s context:detail:summary(default) is per turn the task id, state, timestamp, the user’s message, the agent’s reply (the task’s status message, else its artifacts’ text), one stub per tool call (index,id,name,error,pending) and the number of artifacts, never tool arguments or responses.transcriptis per turn every message and, after them, every artifact (role: artifact), with tool call arguments clipped and tool results as stubs carrying their size.fullanswers the raw tasks (tasks, with history and artifacts).lastTurnsanswers only the last n turns;lastTurns: 1is how to poll a running turn, and reads only that turn.maxBytes(4096 to 1048576, default 100000) bounds the answer. The newest turns that fit are kept, at most 200, and the older ones are counted inomittedTurnswithtruncated: true. A newest turn too big on its own is clipped (clipped: true), or infullleft out.
Tool calls are read from the A2A data parts the ADK writes, typed
function_callandfunction_responseunder the metadata keyadk_typeorkagent_type. A response’serroris anisError: true, or anerrorholding something (true, a message, an object with fields), in it or in any envelope it is wrapped in. -
get_session_turnreads one turn, named by its task id, its index orlast, withA2AService/GetTask; a task of another session is404.detailtranscript(default) answers its messages and then its artifacts fromfromMessageon,fullthe same as A2A messages and{artifact: …}objects (history). What does not fitmaxBytesis left for the next call:nextMessageis thefromMessageto continue with, and a single message or artifact too big on its own is clipped (transcript) or replaced by a note of its size (full).toolCall, an index or id of a tool call, answers that call instead: its arguments and its response unwrapped from the envelopes it travelled in (a runtime’s{result, isError}, an MCP result’s single text content, JSON in a string, as muster nests them). When they do not fitmaxBytes, the smaller stays whole and the larger is clipped (argsText,responseText). -
Both read conversations of up to 10000 turns; a longer one is
409 conflict, as its newest turns could not be named. -
list_sessionsisSessionService/ListSessionsfor one Agent. -
delete_sessionisSessionService/DeleteSession: the controller ends the session’s runtime, removes its state and answers it inRUNTIME_STATE_DELETED; it is gone fromlist_sessionsandget_sessionafter. The tool is annotated destructive. kagent scopes the delete to the session’s creator, so a session of another person is404 not_found, as one that does not exist; while another operation on the session runs the delete is409 conflict, worth a retry.
Session and task bodies are kagent’s proto3 JSON, passed through: session
state is RUNTIME_STATE_*, task status.state is TASK_STATE_*. The
x-kagent-agent-instance-id header is the gateway’s to the runtime and is
never sent. The stubs are generated from giantswarm/kagent-upstream
(hack/kagent-proto/, make generate-kagent); the A2A stubs are the ones
github.com/a2aproject/a2a-go/v2 ships, as in kagent. get_info reports
capabilities.sessions.
Running
agent-manager serve \
--kubeconfig ~/.kube/config --kube-context kind-agentlab \
--kagent-namespace kagent --harness-name kagent \
--agent-chart-oci-url oci://gsoci.azurecr.io/charts/giantswarm/agent \
--skills-repositories https://github.com/giantswarm/agent-skills
Every flag has an environment variable (agent-manager serve --help).
Kubernetes access is required; the api.kagent.dev API version is discovered
from the server on agents (--kagent-api-version auto, fallback
v1alpha3), the Flux API versions on their resources. --muster-url
(AGENT_MUSTER_URL) composes the platform’s muster MCP URL into every agent as
muster.url; unset, nothing is composed and the chart default applies.
--target-muster-url (AGENT_TARGET_MUSTER_URL) is the one agents on
workload clusters reach (see “Agents on a workload cluster”).
Tracing
With OTEL_EXPORTER_OTLP_ENDPOINT set (chart: observability.otel.endpoint)
the server exports traces over OTLP, configured by the standard OTEL_*
variables (OTEL_EXPORTER_OTLP_PROTOCOL, OTEL_EXPORTER_OTLP_HEADERS,
OTEL_TRACES_SAMPLER, OTEL_RESOURCE_ATTRIBUTES). Every request but the
probes is a server span named after its route (POST /mcp, GET /api/v1/),
joined to an inbound traceparent. Under it an MCP request is an
mcp. server span; a tool call’s mcp.tools/call span carries
gen_ai.tool.name and mcp.tool.name, with a tool. span around the
handler. The chart samples parent-based (parentbased_traceidratio, 0.1 of
the traces that start here). Unset, nothing is exported and traceparent
still propagates.
Migrating an installation: agent-manager migrate
agent-manager migrate moves an installation’s agents to the next Generic
chart major. Helm upgrades a release to the new chart, but nothing else moves
existing agents: the namespace’s shared OCIRepository keeps tracking the old
range, and values the new chart or kagent API refuses are never rewritten.
create_agent reuses a namespace’s OCIRepository and only warns when its
range differs. The connectivity chart of the
agent-platform meta chart
runs migrate once per installation as a Job under the platform’s Flux
identity, and it runs by hand with a kubeconfig the same way. Every phase is
idempotent and gated on the previous one; re-running is always safe, and a
second run on a migrated installation changes nothing and says so.
The target range (--agent-chart-semver, default 2.x) picks the path by its
major. A cluster serves one kagent line at a time and a namespace’s releases
share one chart source, so the path belongs to the installation, not to a
release. Each release’s deployed chart is checked against it: a release on a
chart older than the path’s source major is reported as failed and never
rewritten. A range that admits two majors (>=1.0.0) is refused.
| Target | Path | Old line → new line | Precondition |
|---|---|---|---|
2.x |
1.x → 2.x | kagent.dev/v1alpha3 AgentTemplate → api.kagent.dev/v1alpha3 Agent |
agents.api.kagent.dev served (kagent 1.3 line); --dry-run runs before it |
1.x |
0.x → 1.x | kagent.dev/v1alpha2 Agent → kagent.dev/v1alpha3 AgentTemplate |
agenttemplates.kagent.dev served (kagent 1.0 to 1.2 line) |
agent-manager migrate --dry-run \
--kubeconfig ~/.kube/config --kagent-namespace kagent --harness-name kagent \
--agent-chart-oci-url oci://gsoci.azurecr.io/charts/giantswarm/agent --agent-chart-semver 2.x \
--gitops-namespaces flux-giantswarm
agent-manager migrate --kubeconfig ~/.kube/config # writes
It works on every Generic-chart release that renders into a managed namespace
(--kagent-namespace, --managed-namespaces — the same flags and variables
serve takes): the HelmReleases of the namespace whose chartRef is an
OCIRepository of the agent chart (--agent-chart-oci-url), plus the releases
elsewhere that the rendered objects’ Flux provenance labels
(helm.toolkit.fluxcd.io/name, helm.toolkit.fluxcd.io/namespace) point at —
the fleet’s GitOps-owned sre-agent releases live in flux-giantswarm with
targetNamespace: kagent — and any Generic-chart release in
--gitops-namespaces with a targetNamespace in the managed set. Releases
outside the managed namespaces, and releases applied by a Flux Kustomization
(kustomize.toolkit.fluxcd.io/name), are read and never written.
1.x → 2.x values
The 2.x values schema adds agent.egress and plugins and changes nothing
else, so 1.x values pass it. The change is under keys the schema leaves open,
where only the api.kagent.dev CRD would refuse them:
- A sub-agent binding
{agent: {name, description, templateRef, isolation}}becomes{subAgent: {name, description, templateRef}}, inextraTools[]and inextraAgentSpec.tools[](the escape hatch replaces the curated tools).isolation: Sharedis dropped, since every sub-agent is Shared onapi.kagent.dev.isolation: Dedicatedhas no 2.x form: the release fails and is left as it is. labels["agent-platform.giantswarm.io/harness"]is dropped. On 1.x it chose the Harness (the Harness admitted templates by it, and extra labels won over the chart’s); on 2.x the Agent names its Harness inspec.harnessReffromagent.harness. A label naming another Harness thanagent.harnessbecomesagent.harness.agent.harnessis set when--harness-nameis not the chart default and the values leave it unset.
A subAgent.templateRef resolves to an api.kagent.dev AgentTemplate in the
agent’s namespace. Chart 2.x renders an Agent with its template inline and
never an AgentTemplate, so a binding to another Generic-chart agent finds
nothing after the move. The rewrite is still written, and the release’s
warnings name the missing template; the parent Agent reports
ResolvedRefs False, and the wait phase lists it, until an AgentTemplate of
that name exists.
0.x → 1.x values
An installation that moves from the 0.10 platform to kagent API v2 has agents
as Generic chart 0.x releases rendering kagent.dev/v1alpha2 Agent objects.
Chart 1.x refuses their values, upstream ships no migration, and the old CRD
and its objects survive the kagent upgrade untouched (helm.sh/resource-policy: keep). The removed keys are dropped (agent.runtime, replicas,
resources, nodeSelector, tolerations, the whole muster.serverRef,
muster.allowedHeaders, muster.stsWellKnownUri, skills.gitAuthSecretRef:
the composer’s own list), muster.toolNames becomes muster.tools, the 0.x
skills object (refs[], gitRefs[]) becomes the 1.x list with every git ref
resolved to that ref’s head commit and every tagged image to its digest (the
same path create_agent pins with, through --skills-github-api and
GITHUB_TOKEN), agent.harness is set when --harness-name is not the chart
default, the driftDetection.ignore paths under /spec/declarative (v1alpha2
fields) are dropped from the HelmRelease, and everything else (agent.*,
modelConfig, toolset, muster.enabled, extraTools, extraAgentSpec,
labels, annotations) is kept. A skill reference that cannot be resolved
(a private repository without a token) leaves the release pending.
The phases
- Expand. Every release’s rewrite is computed and validated against the
target chart’s
values.schema.json(the registry’s, else the embedded 2.x copy) before anything is written, a release that is already on target values included. The releases of one chart source are written only when every one of them can be: a failed or pending release holds its writable siblings (pending,held: …), since the source cannot move and target values under the old chart would fail their render. A writable release is updated in place; a GitOps-owned or external release gets its rewrite as a unified diff of the manifest in the report, and its OCIRepository’s range move too, for the pull request in the owning repository. Last, the namespace’s agent-chartOCIRepositorymoves to the target range (--agent-chart-semver, and--agent-chart-semver-filterwhen given), only when every release it serves is on target values and the registry has a version in that range. Nothing is written while the registry has no version in the target range. - Wait. The contract runs only when the cluster serves the target line’s
resource, every release rendering into the namespace is deployed from a
chart in the target range, every object the target chart renders is Ready
(2.x: the
Agent, by theget_agent_statusverdict; 1.x: theAgentTemplate’s entry for--harness-nameinstatus.harnesses[]), and no object of the old line is still rendered by a release Flux has yet to upgrade. Until then the report names what is pending and the command exits 0. - Contract. The old line’s leftover agent objects in the managed
namespaces are deleted: on 2.x the
kagent.dev/v1alpha3AgentTemplateobjects no release renders any more (Helm removes the rendered ones when a release upgrades), on 1.x thekagent.dev/v1alpha2Agentobjects (the bundled example agents of the kagent 0.10 chart among them). On 1.x the CRDsagents,sandboxagents,agentharnesses,memoriesandtoolserversofkagent.devfollow, once every managed namespace has passed the gate and no v1alpha2Agentexists outside the managed namespaces. On 2.x no CRD is deleted: thekagent.devCRDs are the platform’s to remove, and other components (model-manager’sModelConfig) may still use them.
The report
Every run writes a ConfigMap agent-manager-migrate-report
(--report-configmap) in each managed namespace and prints the same to
stdout; --dry-run prints and writes nothing, not even the report. Keys:
| Key | Content |
|---|---|
phase |
expand (a release still on the old major’s values, a diff not merged, a source not on the range), wait (expand done, Flux/kagent catching up), contract (the gate passed; leftovers deleted this run), complete (nothing of the old line left) |
summary |
one sentence |
report.yaml |
the structured report below |
namespace: kagent
phase: expand
summary: "expand phase (1.x -> 2.x): 1 release(s) rewritten, 1 unchanged, 1 with a diff for the owning repository, 0 pending, 0 failed; 1 item(s) gate the next phase"
run: {at: 2026-10-06T02:00:00Z, dryRun: false, version: 2.0.0, harness: kagent, path: "1.x -> 2.x",
chart: {ociUrl: oci://gsoci.azurecr.io/charts/giantswarm/agent, targetSemver: 2.x, latestVersion: 2.0.0, schemaVersion: 2.0.0, schemaSource: registry}}
changed: true # this run wrote something in the namespace
releases:
- name: lead
namespace: kagent
ownership: helmrelease # helmrelease | gitops | external
action: rewritten # rewritten | unchanged | diff | pending | failed
chartVersion: 1.5.3 # deployed chart version
changes:
removed: ["extraTools[1].agent.isolation"]
renamed: ["extraTools[1].agent -> extraTools[1].subAgent"]
set: ["agent.harness=claude"] # 0.x -> 1.x also: skills, driftIgnoreRemoved
warnings: # what the write does not wait for
- "sub-agent templateRef \"helper\" names no agenttemplates.api.kagent.dev in namespace kagent; …"
template: {name: lead, exists: true, verdict: ready, summary: "…"} # wait phase: the Agent (2.x) or AgentTemplate (1.x)
- {name: sre, namespace: kagent, ownership: helmrelease, action: unchanged, reason: "already on the 2.x values", chartVersion: 1.5.3}
- name: sre-agent
namespace: flux-giantswarm
targetNamespace: kagent
ownership: external
action: diff
reason: "never written by this command: …"
diff: | # unified diff of the manifest (runtime metadata and Flux labels stripped)
--- a/helmrelease-flux-giantswarm-sre-agent.yaml
+++ b/helmrelease-flux-giantswarm-sre-agent.yaml
…
sources: # the agent-chart OCIRepositories
- {name: agent, namespace: kagent, ownership: helmrelease, action: moved, from: ">=1.5.0 <2.0.0", to: 2.x} # moved | unchanged | not-moved | diff
agents: # the old line's agent objects found
- {name: by-hand, namespace: kagent, kind: kagent.dev/v1alpha3 AgentTemplate, action: not-migratable, reason: "no Flux provenance labels: …"}
- {name: sre, namespace: kagent, kind: kagent.dev/v1alpha3 AgentTemplate, owner: kagent/sre, action: awaiting-upgrade} # awaiting-upgrade | not-migratable | deleted
pending: # what gates the next phase
- "release flux-giantswarm/sre-agent: its rewrite (the diff in this report) is not applied in the owning repository yet"
contract: # once the gate passed
agentsDeleted: [by-hand]
crds: # 0.x -> 1.x only
- {name: agents.kagent.dev, action: deleted} # deleted | absent | kept
Exit codes and permissions
The exit code is 0 whenever the run did what the cluster’s state admits
(pending or failed releases included, they are in the report) and non-zero
only for what needs an operator: no cluster access, a target range with no
migration path, the target line’s kagent API not served (on 2.x a dry run goes
ahead and reports it), a read or write the API server refused, the report not
writable.
The Job’s identity (the connectivity chart binds the platform’s Flux
ServiceAccount) needs, in the managed namespaces: helmreleases and
ocirepositories get/list/update/patch, configmaps get/create/update,
events list (diagnostics; optional), and per path:
- 1.x → 2.x:
agents,agenttemplates,harnessesandremotemcpserversofapi.kagent.devget/list;agenttemplatesofkagent.devlist/delete. - 0.x → 1.x:
agenttemplates,harnessesandremotemcpserversofkagent.devget/list,agents(kagent.dev/v1alpha2) list/delete; and cluster-widecustomresourcedefinitionsget/delete for the five names above and, best effort (the run goes on without it),agentslist to refuse the CRD deletion while objects exist outside the managed namespaces.
In the namespaces the provenance labels and --gitops-namespaces name:
helmreleases and ocirepositories get/list. Skill refs (0.x → 1.x) are
resolved through the GitHub API (--skills-github-api, GITHUB_TOKEN) as
list_skills does; without a token public repositories resolve and a private
ref is reported as pending. The Job also needs egress to the chart registry,
and to the GitHub API on 0.x → 1.x.
Helm chart
helm/agent-manager — see its README. Keys
the giantswarm/agent-platform
meta chart sets for its agent-manager component: kagent.namespace,
harness.name, agentChart.*, muster.url, mcp.enabled, oauth.*,
muster.mcpServer.*, skills.repositories, and
flux.helmReleaseServiceAccount (derived from kagent.fluxServiceAccountName).
Optional, off by default: muster.mcpServer.enabled (renders an
mcpservers.muster.giantswarm.io CR), httpRoute.enabled,
networkPolicy.enabled.
Development
See docs/development.md.
Install
This server does not publish a one-line install command.
Open the repository installation guide