agent-manager

command module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 11, 2026 License: Apache-2.0 Imports: 1 Imported by: 0

README

agent-manager

CircleCI

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 (1.x) — one release renders one kagent.dev/v1alpha3 AgentTemplate plus the agent's own muster RemoteMCPServer — that renders from the shared per-namespace OCIRepository of that chart, tracking it at 1.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 AgentTemplate, its RemoteMCPServer and its owning HelmRelease (Flux provenance labels). Readiness is what the platform Harness reports on the template — 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/v1 for the portal backend — contract in api/openapi.yaml, also served at /api/v1/openapi.yaml.
  • MCP (streamable HTTP, /mcp) for muster — tools get_info, list_agents, get_agent, create_agent, update_agent, delete_agent, get_agent_status, validate_agent, list_model_configs, list_skills (through muster: x_agent-manager_<tool>). Every tool description says whether it writes and what it writes; the read-only and destructive annotations are set.

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, 1.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 (AgentTemplates + HelmReleases of the chart not rendered yet) GET /api/v1/agents?namespace= list_agents no
One agent with its HelmRelease values, pinned skills, toolset and per-Harness status GET /api/v1/agents/{ns}/{name} get_agent no
Create: OCIRepository (when missing) + HelmRelease, after skill pinning, schema and ModelConfig 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] delete_agent HelmRelease, OCIRepository, (bare AgentTemplate with force)
Status verdict: the platform Harness's entry on the AgentTemplate, 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, each with its head commit GET /api/v1/skills[?repository=&ref=&refresh=] list_skills no
Health GET /healthz, GET /readyz — no

Errors are {"error":{"code":"not_found|invalid_request|conflict|forbidden|unsupported|backend_error","message":"…"}}; conflict (409) covers "exists already", "GitOps-owned", "suspended" and "bare AgentTemplate" — the cases force overrides where documented.

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 displayName, description, systemMessage, iconUrl, skills (see below), 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: 1.x}}
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata: {name: sre, namespace: kagent}
spec:
  interval: 10m
  chartRef: {kind: OCIRepository, name: agent, namespace: kagent}
  values:
    agent: {name: sre, displayName: SRE Assistant, systemMessage: …, harness: kagent}
    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}
    toolset: [preset:read-only, workflow:incident-triage]
    muster: {url: http://muster.agent-platform.svc.cluster.local:8090/mcp}

The chart renders the AgentTemplate (spec.description, spec.systemPrompt, spec.modelConfig, spec.skills[], the annotations ui.giantswarm.io/display-name and ui.giantswarm.io/icon-url, the admission label agent-platform.giantswarm.io/harness: <agent.harness>) and the agent's own RemoteMCPServer (named after the agent, pointing at muster, carrying the toolset header). ModelConfigs, their Secrets and the platform Harness are platform-admin owned: agent-manager only reads them.

There is no runtime argument: on kagent API v2 the platform Harness (the Go ADK) is the runtime of every agent. A request still carrying runtime, or the 0.x skills.gitAuthSecretName, is refused with the reason.

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_skills reports it) 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: <registry>/<repository>:<tag> | @sha256:<digest>} — 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 ...

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:<name>, server:<name>, workflow:<name>, tool:<name> — exact, case-sensitive names; at most 32 inline (define a preset for more). toolset:<name> is reserved; label: selectors exist inside presets only.
  • Presets every installation has: read-only, none, infrastructure, agent-platform, full. create_agent refuses a request without a toolset and names them: preset:none for a chat-only agent without tools, preset:full for 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_agent replaces the whole list — the edit path, and the way agents that predate toolsets get one. get_agent / list_agents report the declared toolset, or implicitFullAccess: true for a release without one (a bare template'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.

Status

get_agent_status folds three sources into ready | progressing | failed | unknown and one sentence:

  • the AgentTemplate's status.harnesses[] entry for the platform Harness (--harness-name, default kagent): ready when Ready is True and desiredRevision equals latestSuccessfulRevision; progressing while a revision compiles (desiredRevision ahead, or Ready False with reason ActorTemplatePending); failed with the condition's message when Accepted, ResolvedRefs or Compatible is False, when Ready is False for another reason, or when no Harness admits the template — the answer then lists the namespace's Harnesses and what their selectors admit. The Harness's warnings ride along.
  • the owning HelmRelease's conditions and recent history (a failed render is failed; not yet reconciled is progressing);
  • 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 carries kustomize.toolkit.fluxcd.io/name: its desired state lives in git and a live write would be undone on the next reconciliation. update_agent and delete_agent refuse it unless force. A meta agent opens a pull request in the GitOps repository instead. The release may live in another namespace (the fleet's sre-agent releases sit in flux-giantswarm with targetNamespace: kagent); the template's provenance labels lead to it.
  • none — a bare AgentTemplate with no HelmRelease behind it: nothing to write to; delete_agent removes it only with force.

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 AgentTemplate 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 — 1.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 1.x contract) validates and get_info reports chart.schemaSource: embedded with the error. The schema refuses every 0.x key the 1.x 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.

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 AgentTemplate, 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. agent-manager has no background Kubernetes work — API version discovery at startup is what every authenticated principal may read — so the ServiceAccount token stays mounted only for the in-cluster address and CA. 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.

Without --enable-oauth the service checks no identity and acts as its ServiceAccount (the Role per managed namespace: HelmReleases and OCIRepositories read/write; AgentTemplates, RemoteMCPServers, Harnesses, ModelConfigs and events read; AgentTemplates and RemoteMCPServers delete for the forced cases) — only for a server nothing but a trusted proxy (the agentgateway JWT policy, muster) can reach.

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 kagent.dev API version is discovered from the server on agenttemplates (--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.

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.

Documentation

The Go Gopher

There is no documentation for this package.

Directories

Path Synopsis
Package api publishes the agent-manager API contract.
Package api publishes the agent-manager API contract.
Package cmd holds the agent-manager CLI.
Package cmd holds the agent-manager CLI.
internal
agents
Package agents is the domain of agent-manager: an agent on the platform is a Flux HelmRelease of the Generic agent chart (1.x) — one release renders one kagent.dev/v1alpha3 AgentTemplate plus the agent's own muster RemoteMCPServer, the toolset carrier — sharing a per-namespace OCIRepository that tracks the chart.
Package agents is the domain of agent-manager: an agent on the platform is a Flux HelmRelease of the Generic agent chart (1.x) — one release renders one kagent.dev/v1alpha3 AgentTemplate plus the agent's own muster RemoteMCPServer, the toolset carrier — sharing a per-namespace OCIRepository that tracks the chart.
api
Package api exposes the service twice from one process: REST/JSON under /api/v1 for the portal backend, and MCP tools for muster.
Package api exposes the service twice from one process: REST/JSON under /api/v1 for the portal backend, and MCP tools for muster.
chart
Package chart knows the agent chart: where it lives, which version a Flux OCIRepository tracking the configured semver range resolves to right now, and the values.schema.json of that version — the contract every agent's values are validated against before anything is applied.
Package chart knows the agent chart: where it lives, which version a Flux OCIRepository tracking the configured semver range resolves to right now, and the values.schema.json of that version — the contract every agent's values are validated against before anything is applied.
identity
Package identity carries the authenticated caller through a request: who the OAuth layer validated (subject, email, groups) and, when the server acts on the caller's behalf towards the Kubernetes API, the caller's own IdP token.
Package identity carries the authenticated caller through a request: who the OAuth layer validated (subject, email, groups) and, when the server acts on the caller's behalf towards the Kubernetes API, the caller's own IdP token.
kube
Package kube builds the Kubernetes clients agent-manager reads and writes with, and hides them behind an interface so the identity a call runs under is a deployment choice: one shared client authenticated as the pod's ServiceAccount (a server nothing but a trusted proxy can reach), or — with OAuth and downstream OAuth on — a client per caller that presents the caller's own IdP token, so the apiserver authenticates the real user and the user's RBAC decides.
Package kube builds the Kubernetes clients agent-manager reads and writes with, and hides them behind an interface so the identity a call runs under is a deployment choice: one shared client authenticated as the pod's ServiceAccount (a server nothing but a trusted proxy can reach), or — with OAuth and downstream OAuth on — a client per caller that presents the caller's own IdP token, so the apiserver authenticates the real user and the user's RBAC decides.
oci
Package oci is the minimal OCI distribution client agent-manager needs: anonymous pulls with the registry's bearer-token challenge (RFC 6750 style Www-Authenticate), the tag list of a repository, one file out of a Helm chart archive, and the digest a tag resolves to — the way Flux's source-controller reads the agent chart, and the way a tagged skill image is pinned to the digest kagent requires.
Package oci is the minimal OCI distribution client agent-manager needs: anonymous pulls with the registry's bearer-token challenge (RFC 6750 style Www-Authenticate), the tag list of a repository, one file out of a Helm chart archive, and the digest a tag resolves to — the way Flux's source-controller reads the agent chart, and the way a tagged skill image is pinned to the digest kagent requires.
oci/ocitest
Package ocitest is a fake OCI distribution registry for tests: one repository behind a bearer challenge, a tag list, one manifest per tag with a Docker-Content-Digest, and a chart archive per tag as the single blob.
Package ocitest is a fake OCI distribution registry for tests: one repository behind a bearer challenge, a tag list, one manifest per tag with a Docker-Content-Digest, and a chart archive per tag as the single blob.
server
Package server assembles the single HTTP listener: health endpoints, the REST API and the MCP streamable-HTTP endpoint, both behind the same OAuth guard when configured.
Package server assembles the single HTTP listener: health endpoints, the REST API and the MCP streamable-HTTP endpoint, both behind the same OAuth guard when configured.
skills
Package skills discovers agent skills in git repositories the way the portal backend's /agent-skills endpoint does: every SKILL.md in a GitHub repository is one skill, its frontmatter name and description describe it, and its directory is the skill an agent mounts (url + path).
Package skills discovers agent skills in git repositories the way the portal backend's /agent-skills endpoint does: every SKILL.md in a GitHub repository is one skill, its frontmatter name and description describe it, and its directory is the skill an agent mounts (url + path).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL