The platform's agent runtime is an optional component, on by default
(platform.agents in agentlab.yaml; headlessly:
agentlab configure --defaults --agents=false). When enabled, the chart's
kagent component installs kagent API v2 (api.kagent.dev/v1alpha3) with
the platform's Substrate: every agent is an AgentTemplate paired by an
Agent of the same name with the platform's Go ADK Harness kagent
(spec.templateRef, spec.harnessRef; the connectivity chart renders the
Harness, digest-pinned runtime image, KAGENT_PROPAGATE_TOKEN on), compiled
by the controller into a golden snapshot and run as a Substrate actor; a
conversation is a Session of the Agent, its turns A2A messages through the
edge as the signed-in person (docs/platform.md "Dev channel" and
"Substrate"). There is no per-agent Deployment or pod; the Agent of this
line is a pairing, not the 0.x runtime CR of the same kind name.
Agent delivery runs through Flux on every installation. An agent is a
HelmRelease of the Generic agent chart
in the kagent namespace, rendering from the namespace's shared
OCIRepository agent that tracks the chart at 1.x; the platform chart's
bundled engine renders it as the tenant ServiceAccount kagent-flux (the
connectivity chart's kagent.fluxServiceAccountName). One release renders:
- the
AgentTemplatenamed after the release —spec.description,spec.systemPrompt,spec.modelConfig.name,spec.skills[], the annotationsui.giantswarm.io/display-nameandui.giantswarm.io/icon-url; - the
Agentnamed after the release, pairing that template (spec.templateRef) with the Harnessagent.harnessnames (spec.harnessRef, defaultkagent); an Agent whose Harness does not exist is created and never becomes Ready (ResolvedRefs=False); - unless the toolset is exactly
["preset:none"], oneRemoteMCPServernamed after the agent that points at muster (muster.url) and carries the toolset as the staticX-Muster-Toolsetheader (spec.headersFrom, the selectors joined by,), labelledkagent.dev/discovery: disabled; the template binds it as its tools. A release without atoolsetvalue renders the server without the header — implicit full access, the way agents that predate toolsets look.
The HelmRelease's values.toolset is the stable anchor of the toolset; skills
are {name, git: {url, commit}, path} or {name, oci: <ref@sha256:…>}
entries pinned at write time (the CRD refuses a branch, a tag or a short SHA).
Two writers put the same two Flux objects there, and the proofs use both:
- agent-manager as the signed-in person — the platform path. Backstage's
create flow and every MCP client call
x_agent-manager_create_agentthrough muster; agent-manager pins the skills (list_skillsreports the head commits;update_agentwithrefreshSkillsre-pins), validates the values against the chart'svalues.schema.json, writes the HelmRelease (and the OCIRepository when the namespace has none) with the field manageragent-manageras the caller (requestedBy), and reads readiness back from the Agent's status (get_agent_status:ready | progressing | failed— the last with the condition's message). - a HelmRelease applied directly — the operator's kubectl path, the same objects by hand.
In the lab both are one helper (internal/lab/agent.go): readyAgent
writes an agentSpec through an agentWriter (agentManagerWriter on a
muster session, portalAgentManagerWriter through the portal's muster
backend, helmReleaseWriter directly) and waits with waitAgentReady until
the Agent, on the platform Harness, reports Ready on the desired revision —
or fails fast with the reason when the release's render is refused, an Agent
condition is False for good, or the Agent references another Harness.
removeAgent takes the release and whatever render is left. Every agent
proof — agents-test, toolsets-test, models-test's turn,
backstage-test's own agent — creates its agents this way; skills-test
alone writes a raw AgentTemplate and its Agent, on purpose: it is the
golden-boot diagnostic below the chart (a private fixture's credentialRef,
the control boot without the skill).
Agents run against a default ModelConfig that the connectivity chart
renders from the lab's aiModel setting (agentlab.yaml, default
claude-sonnet-4-6 — the BOM's own default), referencing the Secret
kagent/kagent-anthropic (key ANTHROPIC_API_KEY). The API key is a real
credential, so unlike the lab's throwaway passwords it never enters
agentlab.yaml or the rendered state/ files. The Secret exists after every
agentlab up and agentlab platform, so the ModelConfig resolves on a fresh
lab too (the run waits for ResolvedRefs), from the first of (the portal's
copy for Backstage's AI chat, agent-platform/agentlab-anthropic, by the same
rules; see Backstage):
- A recorded source (
aiKey.sourceinagentlab.yaml;agentlab configure --ai-key-source op://<vault>/<item>/<field>, or<file>#<path>of a SOPS file): a reference, never a value. Everyupandplatformhand it to the operator's secret tooling,beekeeper secret copy <source> --to-secret kind-<clusterName>/kagent/kagent-anthropic/ANTHROPIC_API_KEY(beekeeper, on PATH;configurerefuses a source without it), which reads the value in its own process and writes it straight into the lab's apiserver — agentlab sees the value's length, never the value. A recreated lab (down,up) carries the key again without a manual step; a Secret already holding a real key is left alone (delete it and re-run to place the source again). A source that cannot be placed — the tooling missing, the vault not answering, no lease on the lab — fails the run naming the Secret and the source: a lab whose configuration says where the key is never runs on a placeholder. $ANTHROPIC_API_KEYon the host, when no source is recorded: the Secret is created from it in-process (left alone once it holds a real key; delete it and re-run to rotate).- A placeholder otherwise, as for an extra model without a
key: the ModelConfig resolves and agents start, but every turn on it fails
at Anthropic until the key is placed — the run says so, loudly, with the
three ways: the
beekeeper secret copy … --to-secretcall above (it replaces the placeholder; the next run leaves the real key alone), the source recorded, or the variable exported andagentlab platformre-run. Without any of those, run the agent proofs on a host model: Agent proofs without an Anthropic key.
A person without the tooling places the key with
kubectl --context kind-<clusterName> -n kagent create secret generic kagent-anthropic --from-file=ANTHROPIC_API_KEY=… (over the placeholder:
delete secret first).
The same key powers Backstage's AI chat (see
Backstage gotchas for its no-key behavior).
Further models — the host model servers through model-manager,
platform.extraModels — are their own ModelConfigs; see Models.
Without a key, the agent proofs run on a host model through --model-config:
see Agent proofs without an Anthropic key.
Two consumers of the lab resolve skills through GitHub's REST API: the
portal's skill discovery (GET /api/gs/agent-skills?repoUrl=…, the create
wizard's skills step) and agent-manager (list_skills, the commit
create_agent pins a branch to, refreshSkills, the migrate Job of the
cut-over). Without a token GitHub allows 60 requests an hour per egress
address, and the kind cluster shares this machine's — a handful of
backstage-test runs within an hour exhaust it, after which discovery comes
back truncated (the proof refuses that by name) until the window resets.
A token lifts that, from a recorded source (githubToken.source in
agentlab.yaml; agentlab configure --github-token-source op://<vault>/<item>/<field>, or <file>#<path> of a SOPS file): every up
and platform hand it to beekeeper secret copy --to-secret, which writes the
token in its own process; agentlab never reads the value, and a placement that
fails fails the run. agentlab never reads $GITHUB_TOKEN either: a token
exported in its environment reaches no consumer. Without a source discover
and platform name the Secret the lab lacks, agentlab-github-token (key
GITHUB_TOKEN) in agent-platform.
agentlab up/platform place or update the Secret agentlab-github-token (key GITHUB_TOKEN) in
agent-platform — the portal takes it through extraEnvVarsSecrets and the
lab's app-config overlay (integrations.github[].token: ${GITHUB_TOKEN}),
agent-manager through the chart's skills.github.tokenSecret — and in
kagent for the migrate Job (agentManager.migration.githubToken), which then
call GitHub authenticated (5000 requests an hour; the rate-limit headers say
so). A fine-grained token with public read access is enough; a token that
reads a private skill repository lets agent-manager resolve skills from it.
The token travels vault -> Secret through beekeeper only and never enters
agentlab.yaml, state/ (the rendered values carry the Secret's name), a
log line or a process's argv. Re-running after a rotation in the vault
rotates the Secret.
A skills GitHub App is the way a lab reaches GitHub without any token: agent-manager's chart takes a read-only App (skills.github.app.secretName) and resolves skills with its installation tokens, at the App's rate limit. agentlab configure --github-app-id <id> --github-app-installation-id <id> --github-app-private-key-source op://<vault>/<item>/<field> records it as githubApp in agentlab.yaml (the ids are public; the key is a reference). A lab that already records the workspaces GitHub App (platform.workspaces.github, docs/workspaces.md) records only its installation, agentlab configure --github-app-installation-id <id>: agent-manager then resolves skills as that App, its id and private key source taken from the workspaces entry. With the workspaces App's OAuth client recorded too (clientId and clientSecretSource, or githubApp.clientId and githubApp.clientSecretSource for an App of its own), the portal's skill discovery calls GitHub as the same App: every up and platform write the Secret agentlab-portal-github-app (GITHUB_APP_ID and GITHUB_APP_CLIENT_ID applied, GITHUB_APP_PRIVATE_KEY and GITHUB_APP_CLIENT_SECRET placed by beekeeper), the portal's extraEnvVarsSecrets take it, and the overlay's integrations.github lists the App with publicAccess: true, so a public repository of any owner gets an installation token. Every up and platform apply the ids to the Secret agentlab-github-app in agent-platform and hand the key to beekeeper secret copy --to-secret (key private-key, never read by agentlab; a failed placement fails the run), and the values name the Secret for agent-manager. The chart refuses the App together with tokenSecret, so githubApp and githubToken.source are not recorded together.
Without either the lab is as before: the values name no Secret, the
consumers call GitHub unauthenticated, and a Secret an earlier run created
stays — unreferenced — until agentlab down (a run that merely lacks the
export never deletes a credential; kubectl -n agent-platform delete secret agentlab-github-token does). backstage-test, agents-test and the
rehearsal print the window before their first skill-resolving step
(GitHub API window (unauthenticated: this machine's shared window): 12 of 60 requests remaining, resets 14:32:10 CEST) and, when it holds fewer requests
than the run spends (backstage-test: 30, its discovery and the create pin),
wait once until the reset instead of failing on a truncated listing. The
wait is skipped only when every consumer the run reads through carries a
credential: agent-manager for agents-test, agent-manager and the portal
for backstage-test.
backstage-test runs its refreshSkills steps (E4/E5) only when
agent-manager's Deployment carries a GitHub credential; without one it skips
them with a verdict naming the anonymous rate limit.
An agent whose HelmRelease a Flux Kustomization applies carries the
Kustomization's provenance labels (kustomize.toolkit.fluxcd.io/name and
/namespace); agent-manager reports it managed: gitops and refuses every
live write with gitops_owned, because the next reconciliation would undo
it. Such an agent is changed with a pull request: update_agent in
mode: commit rewrites the release's file in agent-manager's directory of the
owning repository and opens the pull request as the person. The portal's edit
page dry-runs every change with validate_agent (update: true) first and
offers the write only once the dry run is valid, so the dry run takes mode
too and answers as the write would: gitops_owned in mode apply, valid with
the composed manifests in mode commit.
./agentlab agents-gitops-test is that edit, headless and as the person: the
platform's agent-manager composes a release, the proof applies it with the
Flux labels and reads it back managed: gitops; the platform's own
agent-manager (not pinned to a GitHub App) answers the change's dry run
gitops_owned in mode apply and unsupported in mode commit, as its writes
do; then, against a temporary copy of the agent-manager release pinned to the
lab Dex as its GitHub App and to the fake GitHub API (the shape models-test
uses, see Models "Dry runs, GitOps-owned objects and commit
mode"), validate_agent in mode apply still answers gitops_owned, force
or not, validate_agent in mode commit answers a valid update whose
HelmRelease carries the change and opens nothing, and update_agent in mode
commit opens exactly one pull request authored by the person whose file is
the dry run's manifest byte for byte, while the live release's
resourceVersion stays as it was and a mode apply write is still refused.
The commit target is named (repository, branch, path): the lab's
engine runs no kustomize-controller, so no Kustomization resolves it from
the release's labels here. The agent, the copy, the fake and its Service go
away at the end of the run.
platform.tenants (agentlab configure --tenants) renders the chart's
tenants.organizations.enabled: true: agent-manager's tenant reconciler runs
(--multi-tenant) and the chart admits the agent workers of every namespace
labelled giantswarm.io/organization. The reconciler keeps in each such
namespace, always, the agent chart's OCIRepository agent, the agents' Flux
ServiceAccount and its RoleBinding (kagent-flux) and, when the platform's
network policies are on, their copies — labelled
agent-manager.giantswarm.io/tenant-object=namespace — and, while the
namespace holds an agent, copies of the platform Harness, its WorkerPool and
the pool's ServiceAccount, the default ModelConfig and its key Secret,
labelled …/tenant-object=agents. The switch needs the agents runtime and a
chart that carries it (giantswarm/agent-platform#1037 and #1050; until a
release does, a dev build through --chart-branch).
Substrate's credential provider reads a namespace's Secrets only where its
policy allows it. Until the chart selects the organizations' namespaces by
label (giantswarm/agent-platform#1041, Substrate 1.12), the lab's values list
the two namespaces tenants-test uses by hand in
substrate.credentialProvider.namespacePolicies — each read by the atespace
of its own name, and by ate-golden — next to the chart's two entries. The
list goes with #1041. A platform.valuesFiles overlay that sets that list
replaces the lab's.
./agentlab tenants-test [email email] is the proof
(#739). It creates org-agentlab-tenants-a and
org-agentlab-tenants-b, labelled, and binds one person to
agent-platform:editor in each (default: the first two users outside
platform-admins, dev@lab.local and viewer@lab.local; a platform admin is
refused, being cluster-admin). It calls agent-manager's REST API (/api/v1,
on a port-forward to its pod) as each person with their Dex token (audiences
kubernetes and agent-platform) and checks:
-
before an agent, each namespace holds only the always-kept objects;
-
a person's create in the other organization is refused (403; under a name that does not exist there, so a person with the lab's cluster-wide
viewreaches the create and does not get 409); the create in their own (201,requestedBythe person) makes the reconciler copy the Harness, the WorkerPool with agent-manager's--tenant-worker-pool-replicas, its ServiceAccount, the ModelConfig and its Secret; -
the WorkerPool's workers are Running and ready, the agent
ready, and a turn as the person (through the edge) answers, the credential provider reading for the namespace's atespace from that namespace only (its log); -
the same in the second organization;
then, with both organizations' agents up and the platform tenant's (
agentlab-tenants-platform, created by the admin inkagentat the start), the authorizer through the edge (steps E) and in Slack (steps S), as the tenant users of The tenant model — the first user in grouptenant-aand intenant-b,alice@lab.localandbob@lab.localafteragentlab configure --tenants:- E1. the first tenant user, whose group is bound nowhere yet, lists
(
ListAgentswith no namespace) the platform tenant's agents only; - E2. each group bound to
agent-platform:userin its organization (RoleBindingagentlab-tenants-test-users, subjectoidc:<group>): each person lists exactly their organization's agents and the platform tenant's, read back against the cluster; - E3. the first user's
CreateSessionand first A2A message on the second organization's agent arePermissionDenied, no Session of it is made (every creator's, listed by the admin) and the credential provider reads nothing for its atespace; on their own organization's agent theCreateSessionsucceeds; - E4. the user's
CreateAgent,UpdateAgentandDeleteAgentin their own organization arePermissionDeniedand its agent is unchanged; - E5. the first editor creates, changes (a label) and deletes an Agent in
their organization through the edge (on a Harness that does not exist,
so it never boots);
CreateAgent,UpdateAgentandDeleteAgentin the other organization arePermissionDeniedand its agent is unchanged; - E6.
ListSessionswithall_creatorsisPermissionDeniedfor both users and answered for the admin; - S. klaus-gateway on the host (klaus-gateway-test's shape:
--gateway-imageor--gateway-binary, default the component's release; it needs giantswarm/klaus-gateway#486, 4.10.0 or later), both users linked: S1.@bot agentsas both at the same time: each roster lists their organization's agent labelledorganization <name>and the platform tenant's labelledplatform, nothing of the other organization, and the first person's roster is the same when asked again; S2. the first person submits the agent picker with the other organization's agent: refused with a note naming that tenant, and nothing reaches it (noturn_dispatch, no Session, no credential read) — the picker is how a person starts another agent in Slack: a mention runs the thread's agent or the default, it names none; S3. the second person starts a conversation with their agent from the picker and it answers; S4. the first person mentions the bot in that thread (allowed by its person when the gateway asks): refused with the same note, the Session's tasks unchanged (klaus-gateway 4.10.0-rc.1 answers the held message's replay after the Allow with its generic "could not be picked up again" note; the person sends again and gets the refusal, HACKS.md U36). On macOS the image runs on thekindnetwork with its ports published on loopback (Docker Desktop's host networking is off, HACKS.md U35); - M. muster's MCP servers per tenant
(MCP servers per tenant), on a
sign-in fixture
agentlab-tenants-mcpin each organization (muster's own protected/mcp, pinned to the lab Dex with the issuer oflab-oauth-fixture, its client Secret readable by muster there): M1. through muster (core_mcpserver_list,list_tools), each tenant user lists the servers of muster's namespace and their organization's fixture, and is offered the sign-in to that one only; the second user, their group's RoleBinding gone a moment, lists muster's namespace's only; M2. the second user signs in to their fixture; the first user's call of one of its tools,core_service_startandcore_auth_loginfor it are answered word for word as for a server that does not exist; M3. anMCPServerof the first organization withauth.forwardToken,auth.requiredAudiences,auth.tokenExchange,familyorauth.type: sigv4readsFailedwith muster's refusal naming the field; M4. the first user signs in to their fixture: muster sendsredirect_uri<muster>/oauth/ns/<namespace>/callbackand the sign-in ends there;lab-oauth-fixture(muster's namespace, the same issuer) still awaits its own sign-in on/oauth/proxy/callback; the admin signs in to the first organization's fixture and the second's still awaits its own, on its namespace's callback; M5. the first editor createsagentlab-tenants-musterin the first organization (toolsetpreset:full), and a turn as the first user reports tools of their fixture and of agent-manager, none of the other organization's, and a second turn has it callcore_mcpserver_list: it lists the first user's fixture and agent-manager, not the second organization's (the agent is deleted again). The same through Slack (klaus-gateway) is not proved yet;
- E1. the first tenant user, whose group is bound nowhere yet, lists
(
-
as the admin (cluster-admin): a WorkerPool patch and delete and a pod labelled
ate.dev/worker-pool(server-side dry runs), and an exec into a worker, are refused by the chart'sagent-platform-tenant-*admission policies; the network policy copies exist — skipped while agent-manager copies none (the lab runs with network policies off); the credential provider's live grants (/statusz) give each tenant's atespace its own namespace only — skipped while its policy has noownNamespaceSelectorentry; -
deleting each namespace's last agent removes the copies and keeps the always-kept objects (giantswarm/agent-manager#175: the delete also removes the OCIRepository, which the reconciler creates again; the proof asserts the end state only).
A lab without the switch fails before any write, naming
agentlab configure --tenants and agentlab platform; so does a credential
policy that does not list the two namespaces, a lab without the tenant users,
and a kagent controller without the RBAC authorizer (its ConfigMap's
KAGENT_AUTHORIZATION_MODE not kubernetes, the groups not read from
groups, or the prefixes not the lab API server's oidc: — a chart before
agent-platform 4.151, or values without the lab's prefixes), and a muster
before 5.48.0, or one with mcpServersInAllNamespaces off in its
config.yaml. The namespaces, their agents
and bindings, and the platform tenant's agent go at the end of the run, also after a failure, and at the
start of the next one.
The kagent UI is host-published like the other components:
http://localhost:8081 (platform.agentsPort in agentlab.yaml);
agentlab open agents opens it. Being plain HTTP on loopback, it involves the
lab CA in nothing — no trust question, no browser warning. The UI does
no OAuth in this lab, so it needs none of the issuer tricks — the kagent-ui
Service is simply type: NodePort, pinned to node port 30880 by the kagent
component's postRenderers patch (the chart's Service template renders no
nodePort field — HACKS.md U9), and the kind config maps that onto the host. Like every
kind port mapping it is fixed at node-creation time, so a cluster created
before this mapping existed needs agentlab down && agentlab up; the stopgap
there is the old port-forward:
kubectl -n kagent port-forward svc/kagent-ui 8081:8080The topology is the installation's (the 4.x line, kagent API v2): the
controller serves gRPC only (kagent.api.v1alpha1 for the control plane,
lf.a2a.v1 for the turns) as a GRPCRoute on the edge with the chart's JWT
Strict policy in front — every bearer verified against the lab Dex,
x-user-id set from the verified email claim — and the controller runs the
fleet's auth.mode: trusted-proxy, re-deriving the caller from the same
bearer; agentlab platform-test asserts both (a call without a token refused
at the edge; a forged header attributed to the token's subject), and the
proofs' turns take that path as the person (internal/lab/kagentapi.go).
Every agent is an AgentTemplate paired by its Agent with the platform
Harness kagent (spec.harnessRef) and runs as an actor
on Agent Substrate — the WorkerPool kagent-default's gVisor workers in the
kagent namespace, the control plane in ate-system — which the chart ships
and the lab installs nothing of; kagent's kagent_v2 database lives on the
platform's CNPG Cluster kagent-pg next to Substrate's (see Agent Substrate
and the platform
Postgres).
The agent chart 1.x renders one RemoteMCPServer per agent pointing it at
muster with its toolset header; the Harness re-emits the caller's bearer on
every MCP call (KAGENT_PROPAGATE_TOKEN), so agent tool calls through muster
are the person's, never the platform's — they need a real Dex token on the way
in. Lab deviations on the kagent side, same spirit as the deviations
table:
the JWKS source is the lab Dex, the snapshot store the bundled RustFS, the
ServiceMonitor / OTel exporters are off (the line serves no /metrics, no
OTLP gateway in kind). A git skill of an AgentTemplate is fetched during
the golden boot, which Substrate's egress gate has to let through;
agentlab skills-test is that proof, see The skills
proof.
An agent that never becomes Ready names what Substrate's egress gateway
refused in the verdict of every proof that waits for it: each distinct
refusal (status, method, destination, the gateway's reason), and, when the
gateway answered from a failed call to the control plane (actor egress policy …, actor identity check …, credential provider …) rather than
from the destination, the endpoints of the control plane's Service
ate-system/api that are no pod of Deployment ate-api-server. A pod in
ate-system that carries the api-server's selector labels and serves
nothing takes a share of those calls and refuses them; the git fetch of the
golden boot then reads as a 403 from github.com.
The controller serves gRPC only (kagent.api.v1alpha1 for the control
plane, lf.a2a.v1 for the turns) behind the connectivity chart's
GRPCRoute kagent-controller on the edge — native gRPC over HTTP/2, the
services matched by name — with the AgentgatewayPolicy
kagent-controller-jwt validating the person's Dex id_token (JWT Strict)
and rewriting x-user-id from the token's email claim for the controller's
trusted-proxy authenticator; whatever x-user-id a caller sent is
replaced. Swarmgeist (klaus-gateway) and the Dev Portal's backend speak to
the controller this way, and so do the proofs' turns
(internal/lab/kagentapi.go): one gRPC connection on the lab's TLS
transport (agentgateway.<domain>:<gateway port>, the lab CA trusted), the
a2a-go/v2 client on its gRPC transport for the turns and the stubs
generated from kagent's protos (internal/kagent/gen, see
development.md) for the rest. Every call carries
authorization: Bearer <id_token>; every A2A call names the Agent as its
tenant (<namespace>/<name>) and the Session that holds the conversation
through the message's context id (the Session's id; created for the person
with CreateSession, idempotent on request_id, and deleted afterwards),
and carries the human-in-the-loop extension request (A2A-Extensions: https://kagent.dev/extensions/hitl/v1), so a tool bound with
requireApproval pauses the task at input-required with a decidable
tool_approval_request instead of a plain notice. The answer is consumed
as a stream (SendStreamingMessage: the submitted task, working, the
artifacts, the terminal status). The Harness re-emits the caller's bearer
on every MCP call (KAGENT_PROPAGATE_TOKEN), so muster sees the person,
never the platform — agent tool calls through muster need a real Dex token
on the way in.
agentlab a2a-test is the proof of that path, with the assertions the
surfaces rely on: the route and its policy exist and are Accepted; a call
without a token is refused at the edge (Unauthenticated — on a raw
HTTP/2 POST the gRPC frame gets the trailers-only grpc-status 16, a plain
POST gets 401); a forged x-user-id beside a valid token is replaced
(GetCurrentUser names the token's person); ListAgents lists the proof's
Agent Ready on the platform Harness with the display-name and icon-url
annotations as Swarmgeist reads them; CreateSession is idempotent; a turn
streams its answer; on the proof's agent — the Generic
chart with toolset: [preset:read-only] and muster.requireApproval: true
(chart ≥ 1.1.0) — a tool call pauses the task, the person's approval
resumes it to completed with muster logging the call under the person, a
rejection ends it without the call (every request is decided until the task
settles, within five minutes: how many tool calls the model needs is its
own; a request asked again after its decision fails the step, the decision
did not resume the task); CancelTask on a running turn ends it
server-side (GetTask reports it canceled) and the session takes a
following turn. It leaves nothing behind: the sessions over gRPC
(ListSessions lists none of its), the agent's release and render on
the cluster.