The generic Helm chart for creating kagent agents on the
Giant Swarm Agent Platform. One chart release = one agent: the chart
renders the agent's Agent (api.kagent.dev/v1alpha3) with its template
inline (spec.template: what the agent is: prompt, model, skills, plugins,
tools) and the platform Harness referenced by name
(spec.harnessRef: how it runs), and, unless the agent is chat-only, the
agent's own muster RemoteMCPServer, the carrier of its toolset.
The design: one release of this chart per agent, with the agent's own muster
RemoteMCPServer carrying its toolset. Charts 1.x and 2.x target the kagent API v2
shape, where how an agent runs is a separate Harness the agent references.
The chart renders, in the release namespace and named after the agent:
- the
Agent:spec.templatecarries the description, the system prompt, theModelConfigby name, the skills and plugins (immutable sources) and the tool bindings;spec.harnessRefnames the Harness that runs it (agent.harness, defaultkagent). The object carries the annotationsui.giantswarm.io/display-nameandui.giantswarm.io/icon-urlthe Dev Portal reads. No admission label: the Agent selects its Harness, the Harness admits nothing; - the
RemoteMCPServerpointing at muster's MCP endpoint (muster.url),STREAMABLE_HTTP, with the agent's toolset as the staticX-Muster-Toolsetheader and the labelkagent.dev/discovery: disabled(muster is an OAuth resource server; the controller has no user token to discover tools with). Never anAuthorizationheader: the Harness propagates the signed-in person's token, and a static header would override it. The Agent's template binds this server. Not rendered for a chat-only agent (toolset: ["preset:none"]) or withmuster.enabled: false.
It never touches:
- the
Harness— platform-owned (rendered by the agent-platform connectivity chart); it decides the runtime image, the token propagation, the Substrate worker pool, capacity and placement. An Agent whose Harness does not exist in its namespace reportsResolvedRefs=Falseand never becomes Ready; ModelConfigCRs and theSecrets they reference (LLM credentials) — platform-owned, provisioned per tenant namespace. The chart wires the agent to one by name (modelConfig.name, defaultdefault-model-config).
Install with whatever you already use for Helm charts (Argo CD, Flux
HelmRelease, helm install). A minimal values file yields a working agent:
agent:
displayName: "SRE Assistant" # Unicode, becomes the ui.giantswarm.io/display-name annotation
description: Helps the SRE team triage incidents.
systemMessage: |
You are an SRE assistant. Be concise.
skills:
- name: sre-runbooks
git:
url: https://github.com/example/agent-skills
commit: 0123456789abcdef0123456789abcdef01234567 # a full commit id, never a branch
path: sre-runbooks
labels:
giantswarm.io/owner: sre-teamThe technical resource name defaults to the Helm release name. By default the agent is wired to the platform's muster gateway with all of its (dynamic) tools — implicit full access to everything the gateway exposes to the invoking human. Declare a toolset to compose the agent with a subset:
toolset:
- preset:read-only # a preset muster knows (built in: read-only, none, full;
- workflow:incident-triage # the platform ships infrastructure and agent-platform)
- server:mcp-kubernetes # every tool of one MCP server
- tool:x_mcp-prometheus_query # one exposed toolThe chart renders the selectors as the static X-Muster-Toolset header on the
agent's RemoteMCPServer (spec.headersFrom, joined by ,); muster resolves
it on every request, so the agent's meta-tools list and call only what the
toolset selects. Exactly ["preset:none"] renders no RemoteMCPServer
and no muster binding at all — a chat-only agent. An empty list fails the
render (say preset:none), more than 32 inline selectors fail (define a
preset), and toolset:<name> is reserved. Composition, not authorization: the
human's identity and the backends' own authorization remain the boundary. The
agent gets no wider access of its own.
muster.tools narrows the binding to a subset of muster's meta-tools
(list_tools, call_tool, ...), not the tools behind the gateway — a toolset
is the way to narrow those. A Harness enforces a partial selection only when
it can discover the server's tools; with discovery off (the default) it may
expose the whole server and report a warning in the Agent's
status.warnings.
A skill source is immutable: a git repository at a full 40- or 64-hex commit
id, or an image at a @sha256: digest. A branch, a tag, a short SHA or an
image tag fails helm template with a message naming the field — the same
rule the Agent CRD enforces at admission, applied where the values are
written. The agent-creation flows (agent-manager, the Dev Portal) resolve
the branch a skill is picked from to its head commit and re-pin on request;
the chart itself carries no branch.
A plugin is a package pinned to one immutable source, like a skill, with the names of the skills to enable from it:
plugins:
- git:
url: https://github.com/agentplugins/agent-plugins-example.git
commit: 5f3f5084a821aefa792e79500dd8f0462ab83473
skills: [migrate-agent-plugin]
- oci: registry.example.io/plugins/sre@sha256:<digest>
path: bundles/sre
skills: [k8s]Rendered into spec.template.plugins[] as {source: {git | oci, path}, skills}. The schema refuses an entry without a source, with two sources or
with a mutable source. A skill name is enabled once across skills[] and
every plugin's selection.
The format is decided by the manifest in the plugin's directory. A root
plugin.json makes it an Agent Plugins package. A .claude-plugin/plugin.json
alone makes it a Claude Code format plugin. A package with both manifests is
Agent Plugins on every Harness. Which parts load depends on the Harness:
| Part | kagent |
claude |
codex |
omp |
|---|---|---|---|---|
| Selected skills of an Agent Plugins package | yes | yes | yes | yes |
| A Claude Code format plugin, whole: its skills, hooks, commands and subagents | refused | yes | refused | not loaded |
mcp.json servers |
yes, within the agent's egress | ignored | ignored | ignored |
skillsselects from an Agent Plugins package; an empty or missing selection enables none of its skills. A Claude Code format plugin needs no selection: onclaudeit loads whole and the selection is ignored.- Do not rely on a plugin's
mcp.json. Onlykagentstarts those servers. A server an agent needs on every Harness is a muster registration, bound throughmuster.tools. kagentandcodexread the Agent Plugins format only, so they refuse a Claude Code format plugin. agent-manager refuses a git source at write time (create_agent,validate_agentandupdate_agent, a Harness change included): oncodex, and onkagentfrom agent-manager 2.8.0. An OCI source is judged at the golden boot: the Agent staysReady=Falsewith reasonActorTemplateRetrying, and its message names the format.- The fix in both cases is a root
plugin.json(the Agent Plugins manifest) beside the.claude-plugin/plugin.json. The package is then Agent Plugins everywhere: its selected skills load on every Harness, and its hooks, commands and subagents load on none,claudeincluded.
One chart value covers every private git skill and git plugin of an agent; authors do not pick a credential per skill:
skillsGitAuthSecretRef:
name: kagent-skills-token # a Secret in the agent's namespace, key `token`When set, every skills[] and plugins[] entry with git renders
source.git.credentialRef: {name, key: token}; OCI sources are unchanged.
Every git skill and plugin then needs an https:// URL: the CRD refuses a
credential on any other source and the chart fails the render first, naming
the entry. The
platform side of the contract (the egress gateway, the credential provider,
agent-manager's minted Secret) is agent-platform's
Private skill repositories
(under "The kagent line").
- Who provisions it, where. The tenant, in the agent's namespace (on the
platform: the
kagentnamespace, where the agents are compiled), labelledui.giantswarm.io/agent-skills-git-auth: "true"so the Dev Portal offers it; or agent-manager, which mints and refreshesagent-manager-skills-tokenfrom its skills GitHub App when the installation turns that on. The chart never creates it. - Shape. The key
tokenholdsbase64("<username>:<token>")— on GitHubx-access-token:<token>, a fine-grained PAT or App installation token with read access to the repositories — the value the egress gateway sends asAuthorization: Basic. - One credential per host. It goes with every request to that host, public repositories included; two Secrets for one host in an agent are refused.
- Rotation. Replace the Secret's contents, never its name. The egress gateway reads the Secret on every fetch, so a new token needs no restart; a new name is an edit of every referencing agent and a new revision of each.
- Where the token is visible. In the Secret, to whoever may read Secrets in
its namespace, and to the platform's credential provider and egress gateway,
which send it to the source's host over TLS. Not in the
Agent, the actor's environment (it holds a placeholder) or the snapshots.
What an author sees when it goes wrong (the Agent's conditions, see Readiness):
| Symptom | Cause |
|---|---|
Ready=False ActorTemplateRetrying quoting the git fetch's failure, then ActorTemplateFailed (golden boot 6 of 6 failed (…); no retries left) |
the Secret is missing, the token is wrong or lacks read access, or the namespace has no credential-provider policy (atespace "ate-golden" is not permitted to resolve secrets) |
Compatible=False UnsupportedConfiguration (conflicting credentials for <host> header authorization) |
two credentials for one host |
The field needs the kagent line 1.1.0 or later (giantswarm/kagent-upstream, the platform's line), where the egress gateway injects the credential.
An agent runs in a sandbox with no network of its own: every outbound
connection leaves through the platform's egress gateway, which allows a
destination only when the agent's revision or its egress list names it. An
agent reaches by default:
- its model (the
ModelConfignamed bymodelConfig.name); - its MCP servers, muster included (
muster.url), so a muster toolset needs no entry; - the sources of its skills and plugins (
skills[],plugins[]) and the platform's telemetry.
Nothing else. The platform adds these destinations when it compiles the agent,
so none of them goes into agent.egress.
Adding a host. List the origin in agent.egress, or the same list wherever
the agent is edited: the egress argument of agent-manager's create_agent
and update_agent, "Extra egress origins" in the Dev Portal's agent form. The
chart renders the list into the Agent's spec.egress:
agent:
egress:
- https://github.com:443
- https://*.githubusercontent.com # raw.githubusercontent.com, one label
- tls://orders-db.example.com:5432 # TLS passthrough, port required- An origin is
http(s)://<host>[:<port>]ortls://<host>:<port>, with no path. A path, a query, a bare host name or atls://entry without a port fails the render, naming the entry. - A
tls://origin is TLS that the egress gateway forwards without decrypting it, matched on the SNI server name: a database or message broker that speaks TLS from its first byte. The client must send SNI. A protocol that upgrades to TLS after a cleartext exchange (STARTTLS, PostgreSQL's SSLRequest unless the client setssslnegotiation=direct) does not pass. One host and port takes one protocol: an Agent with atls://and anhttp(s)://entry for the same host and port reportsCompatible=Falseand never becomes Ready. - The host may start with
*., which matches exactly one label, and needs two labels under it:https://*.githubusercontent.comis valid,https://*.comandhttps://*are not. No entry opens a whole top-level domain or every host. - At most 64 origins of at most 270 characters each, none twice.
- Anyone who may edit the agent may set its egress: the list is part of the agent's own spec, not a separate approval.
- An agent on a coding Harness (a runtime other than
kagent) gets its origins from the platform's per-Harness list, which agent-manager writes intoagent.egress; another origin is refused there. agent-manager also setsagent.platformPrompt.egressNote, and the chart appends a## Network access (set by the platform)section to the system prompt that names those origins and tells the agent that a refusal is policy, not an outage. The section is the chart's, not part ofagent.systemMessage: a chart release updates its wording on every agent. A note found insidesystemMessage(a line that is the heading, and the paragraph after it) is replaced by the chart's, with or without the flag, so values that still carry one render the current note once.extraAgentSpec.systemPromptreplaces the whole prompt, the note included.
Picking an EgressPolicy. A tenant's admins can bundle origins, and the
credential header the gateway adds to them, into an EgressPolicy in the
namespace (kubectl -n <namespace> get egresspolicies.api.kagent.dev). An
agent picks one by name in agent.egressPolicyRefs, beside its own
agent.egress; the chart renders the names into the Agent's
spec.egressPolicyRefs and copies none of the policy's rules:
agent:
egressPolicyRefs:
- github
- go-modules
egress:
- https://orders.example.com # typed origins still work next to them- At most 16 names, each a resource name, none twice; the schema refuses anything else, naming the entry.
- The policy is read where it lives: an origin added to it reaches every agent
that picks it from that agent's next Session, with no upgrade of the agent's
release. The Agent recompiles on the change and reports
Ready=Falseuntil its new revision is ready. - A name with no EgressPolicy in the namespace blocks the whole Agent:
ResolvedRefs=Falsewithresolve EgressPolicy "<namespace>/<name>": not found, and it is not Ready until the policy exists or the ref is removed. - An agent on a coding Harness reaches only its Harness's platform list: agent-manager refuses a ref there.
What a denial looks like. The platform answers a refused request, so it reads like an outage, not a policy:
| Traffic | What the agent sees |
|---|---|
| HTTP or HTTPS to an origin not listed | an HTTP 403 with a body that starts actor egress policy denied (or egress denied) |
| any other TCP (SSH, a database, a message queue) | the connection is closed without a reply, which clients report as a reset or an EOF |
| DNS | answered for any name, listed or not, so a name that resolves does not mean it can be reached |
A 403 or a reset on a host the agent should reach is a missing origin: add
it and the next revision of the agent has it.
Traffic kinds. HTTP(S) and TLS passthrough can be allowed, by origin as
above; a tls:// entry needs the kagent line from 1.21.0, whose Agent
accepts it in spec.egress. Raw TCP has no field in the chart yet; it follows once the
platform ships it.
The Agent reports its own readiness on the Harness it names:
kubectl -n <namespace> get agent.api.kagent.dev <name> -o jsonpath='{.status}'Conditions Accepted, ResolvedRefs, Compatible and Ready, with
desiredRevision against latestSuccessfulRevision and warnings.
ResolvedRefs=False naming the Harness means agent.harness names a Harness
that does not exist in the agent's namespace.
See the chart values reference for all available values
and their defaults. It is generated from helm/agent/values.yaml: after a
values change, make helm-docs (or pre-commit run helm-docs --all-files)
regenerates it exactly as CI does.
values.schema.json encodes the curated contract (additionalProperties: false), so bad values (including every 0.x value that has no place in 2.x)
fail with a legible error before anything hits the cluster.
Upgrades are values changes plus a re-apply; uninstalling the release removes the agent. Pin the chart version for reproducibility.
Chart 2.0 renders the kagent API that ships the Agent kind
(api.kagent.dev/v1alpha3, kagent-dev/kagent#2952): one Agent named after
the release, the template inline under spec.template, the Harness by name in
spec.harnessRef, plus the per-agent RemoteMCPServer in the new group. No
kagent.dev/v1alpha3 AgentTemplate is rendered, and the admission label is
gone with Harness.spec.allowedAgentTemplates. The chart installs only on a
platform that serves api.kagent.dev; an upgrade of a 1.x release replaces
the AgentTemplate with an Agent of the same name. The values contract is
the 1.x one plus plugins:
| 1.x value | 2.x | What changed |
|---|---|---|
agent.harness |
same name, default kagent |
now spec.harnessRef.name, a reference the Agent resolves in its namespace; the label agent-platform.giantswarm.io/harness is no longer rendered and no Harness selector is involved |
agent.name, agent.displayName, agent.description, agent.iconUrl, agent.systemMessage, modelConfig.name, skills, skillsGitAuthSecretRef, muster.*, toolset |
unchanged | the template fields move under spec.template; the annotations stay on the object |
| — | plugins[] (new, empty) |
{git | oci, path, skills[]} entries rendered into spec.template.plugins[]; same pinning rules and credential as skills |
extraTools |
same list | a sub-agent binding is {subAgent: {name, description, templateRef}} instead of {agent: {...}} |
extraAgentSpec |
unchanged | deep-merges into spec.template only, never into spec.harnessRef |
labels |
unchanged | an extra agent-platform.giantswarm.io/harness label is rendered as given and means nothing to the platform |
context.compaction |
unchanged | still accepted, still renders nothing |
Chart 1.0 renders the kagent API v2 shape (kagent.dev/v1alpha3): an
AgentTemplate plus a per-agent RemoteMCPServer instead of a
kagent.dev/v1alpha2 Agent. It installs only on a platform that serves that
API, and the values contract changes with it. Value by value:
| 0.x value | 1.x | What changed |
|---|---|---|
agent.name, agent.displayName, agent.description, agent.systemMessage |
unchanged | systemMessage renders into spec.systemPrompt; the display name stays the ui.giantswarm.io/display-name annotation |
agent.iconUrl |
unchanged | rendered as the ui.giantswarm.io/icon-url annotation (the template has no icon field) |
agent.runtime |
removed | the Harness is the runtime; the platform Harness runs the Go ADK |
| — | agent.harness (new, default kagent) |
the value of the admission label agent-platform.giantswarm.io/harness |
modelConfig.name |
unchanged | renders into spec.modelConfig.name |
skills.refs[] (image references) |
skills[].{name, oci} |
a digest-pinned reference <ref>@sha256:<digest>; a tag is refused |
skills.gitRefs[] ({url, ref, path}) |
skills[].{name, git: {url, commit}, path} |
a full commit id; a branch, tag or short SHA is refused; names are unique |
skills.gitAuthSecretRef ({name}) |
skillsGitAuthSecretRef.name (since 1.2) |
a sibling of the skills list; one Secret (key token) rendered as credentialRef on every git skill; every git skill needs an https URL |
muster.enabled |
unchanged | gates the RemoteMCPServer and the binding |
muster.serverRef.* |
removed | the chart renders the agent's own RemoteMCPServer, named after the agent, in its namespace |
muster.allowedHeaders |
removed | the Harness propagates the caller's token (KAGENT_PROPAGATE_TOKEN) |
muster.toolNames |
muster.tools |
the binding's tools (muster's meta-tools) |
muster.stsWellKnownUri |
removed | the platform's dex-only trust model has no token exchange |
| — | muster.url (new) |
muster's MCP endpoint; default http://muster.agent-platform.svc.cluster.local:8090/mcp |
| — | muster.discovery.enabled (new, default false) |
false labels the server kagent.dev/discovery: disabled |
toolset |
unchanged | the header moves to the RemoteMCPServer's spec.headersFrom; grammar and render failures unchanged |
extraTools |
same list, v1alpha3 entries | {mcp: {server: {kind: RemoteMCPServer, name}, tools, requireApproval}} or {agent: {name, description, templateRef}} instead of {type: McpServer, mcpServer: {...}} |
replicas, resources, nodeSelector, tolerations |
removed | capacity and placement are the Harness's worker pool — platform values |
labels |
unchanged | merged over the standard set on both objects |
annotations |
unchanged | merged next to the ui annotations on the AgentTemplate |
extraAgentSpec |
unchanged | deep-merges into the AgentTemplate spec (v1alpha3 fields) only — never into the RemoteMCPServer |
Every removed value is refused by the values schema, so a stale composer fails
the render instead of silently losing a field. The 0.x line lives on as chart
0.x (release-v0.x) for platforms that still run kagent 0.10.