Run an agent on Cosmonic Control
This guide deploys an agent to a Kubernetes cluster running Cosmonic Control and starts runs on it over HTTP, in the OpenAI Responses API format, and over NATS. It uses the published minimal-agent image; any agent harness that exports cosmonic:agent/run at 0.4.x works the same way.
To understand what a run is and who drives it, read Anatomy of an agent.
Before you start
- Cosmonic Control installed, with a HostGroup. If you don't have a cluster yet, start with a local cluster. For any other cluster, see Install Cosmonic Control.
kubectlandhelmpointed at that cluster, andopenssl.- An OpenAI-compatible model endpoint, and its API key if it needs one. Control posts to
<baseUrl>/chat/completions.
No cluster yet?
These commands create a local cluster with k3d, which needs Docker, then install Cosmonic Control and a HostGroup on it. The first install pulls several images, so it can take a few minutes.
k3d cluster create cosmonic \
--port "80:30080@loadbalancer" \
--port "443:30443@loadbalancer" \
--k3s-arg "--disable=traefik@server:0"
helm install cosmonic-control oci://ghcr.io/cosmonic/cosmonic-control \
--version 0.15.0 \
--namespace cosmonic-system \
--create-namespace
helm install hostgroup oci://ghcr.io/cosmonic/cosmonic-control-hostgroup \
--version 0.15.0 \
--namespace cosmonic-system
kubectl rollout status deploy -l app.kubernetes.io/instance=hostgroup -n cosmonic-system1. Create the secrets
The host encrypts each agent's session at rest. Create the key that does it, plus a second key the store's key names are derived from. The two must differ.
kubectl create secret generic agent-session-key -n cosmonic-system \
--from-literal=key="$(openssl rand -hex 32)" \
--from-literal=naming-key="$(openssl rand -hex 32)"Create a Secret for your model provider's API key. Skip this for a keyless local endpoint, and remove existingSecret from the values in step 2.
kubectl create secret generic openai-api-key -n cosmonic-system \
--from-literal=api-key="$OPENAI_API_KEY"2. Turn on agent support in the HostGroup
Save these values as agents-values.yaml:
agents:
enabled: true
models:
sources:
- name: openai
baseUrl: https://api.openai.com/v1
defaultModel: gpt-4.1-mini
existingSecret:
name: openai-api-key
key: api-key
sessions:
storage:
key:
existingSecret:
name: agent-session-key
key: key
namingKey: naming-key
backend:
nats:
credentials:
# Development clusters only. See the warning below.
allowSharedIdentity: true
run:
http:
enabled: true
nats:
enabled: trueagents.models.sourceslists the model endpoints the host calls for agents. A workload picks one byname; it never supplies an endpoint or a key. A key on a plainhttp://endpoint stops the host from starting, because the key would be sent in clear. Usehttps, or setallowPlaintextKey: trueon that source.agents.sessions.storage.keynames the Secret from step 1. Without it, the host serves no session and an agent workload fails to start.agents.run.httpandagents.run.natsturn on the two triggers: the ways a host can start a run. Each workload still chooses which of them reach its agent.
allowSharedIdentity: true stores sessions on the same NATS identity workloads use, so any workload can read, overwrite or delete any agent's session. Use it only on a single-tenant development cluster. Otherwise, give the session store its own credentials under agents.sessions.storage.backend.nats.credentials, and on Control's built-in NATS list its public key in the cosmonic-control chart's nexus.agentSessions.nkeys. The comments on those values in each chart walk through it.
Upgrade the HostGroup with the new values:
helm upgrade hostgroup oci://ghcr.io/cosmonic/cosmonic-control-hostgroup \
--version 0.15.0 \
--namespace cosmonic-system \
--reuse-values \
-f agents-values.yamlHosts with agents enabled label themselves cosmonic.io/agents: "true". Agent workloads select them by that label.
To have the operator delete a workload's session when the workload is deleted, turn on agent support in the cosmonic-control chart too and give it the naming key. Without this, deleting and reapplying a workload picks its old conversation back up. Save this as control-agents-values.yaml:
agents:
enabled: true
sessions:
storage:
key:
existingSecret:
name: agent-session-key
namingKey: naming-keyhelm upgrade cosmonic-control oci://ghcr.io/cosmonic/cosmonic-control \
--version 0.15.0 \
--namespace cosmonic-system \
--reuse-values \
-f control-agents-values.yamlBy default, Control's built-in NATS (Nexus) keeps its data in an emptyDir. If the Nexus pod restarts or is rescheduled, every run fails until the hosts restart, and the sessions are gone. To keep them, set nexus.persistence.enabled: true in the cosmonic-control chart. For an existing release, this replaces the Nexus Deployment with a StatefulSet.
3. Deploy the agent
Save this as agent.yaml. It runs minimal-agent with a model, a session and the run trigger, and routes HTTP requests for one hostname to it.
apiVersion: runtime.wasmcloud.dev/v1alpha1
kind: WorkloadDeployment
metadata:
name: support-agent
namespace: default
spec:
replicas: 1
template:
spec:
hostSelector:
cosmonic.io/agents: 'true'
hostInterfaces:
# The model. The agent asks for the alias "default"; this unnamed
# entry answers every alias with the openai source's default model.
- namespace: cosmonic
package: agent
interfaces: [models, chat]
version: 0.4.1
config:
components: agent
source: openai
# The conversation, kept by the host.
- namespace: cosmonic
package: agent
interfaces: [session]
version: 0.4.1
name: cosmonic-agent-session
config:
components: agent
# The tools. None yet: tool-components names them.
- namespace: cosmonic
package: agent
interfaces: [tools]
version: 0.4.1
name: cosmonic-agent-tools
config:
components: agent
# How runs reach the agent.
- namespace: cosmonic
package: agent
interfaces: [run]
version: 0.4.1
name: cosmonic-agent-run
config:
components: agent
triggers: http,nats
components:
- name: agent
image: ghcr.io/cosmonic-labs/components/minimal-agent:0.3.1
localResources:
allowedHosts: []
---
apiVersion: control.cosmonic.io/v1alpha2
kind: HTTPTrigger
metadata:
name: support-agent
namespace: default
spec:
hostnames:
- support-agent.localhost.cosmonic.sh
rules:
- matches:
- path:
type: PathPrefix
value: /v1/responses
timeouts:
request: 11m
backendRefs:
- name: support-agentApply it and wait for the workload to be ready:
kubectl apply -f agent.yaml
kubectl get workloadsA few things in the spec matter:
- The agent has no network.
allowedHosts: []is deny-all. The host makes the model call with the key from step 1. - Any harness on the same interfaces works here. Swap
minimal-agentfor the Pi, Codex or Goose image and the rest of the spec stays the same. - The image's tag is fine for trying it out. For production, pin the image by digest (
@sha256:…) so a pushed tag cannot change what runs. - The HTTPTrigger timeout must outlast a run. A run can stream for up to 10 minutes by default (
max-duration-secson the run entry, at most 3600). Envoy otherwise cuts a response at 15 seconds. - The spec is the approval. On Control, the workload spec decides what an agent and its tools can reach, so the admission policies that govern your other manifests govern agents too.
The Agent workload reference lists every entry and key.
4. Start a run over HTTP
Make the hostname reach the cluster's ingress the way you do for any HTTPTrigger. On a local cluster with Traefik, as in Install Cosmonic Control, apply this Ingress:
kubectl apply -f - <<EOF
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: support-agent
namespace: cosmonic-system
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: web,websecure
spec:
ingressClassName: traefik
rules:
- host: support-agent.localhost.cosmonic.sh
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: ingress
port:
number: 80
EOFThen send a Responses API request:
curl -N http://support-agent.localhost.cosmonic.sh/v1/responses \
-H 'content-type: application/json' \
-d '{"input": "In one sentence, what can you reach?", "stream": true}'The reply streams back as server-sent events: response.created, then response.output_text.delta events as the model writes, and response.completed at the end. Without "stream": true, the whole response arrives once the run ends.
Because the request is in the Responses API format, an OpenAI client library pointed at http://support-agent.localhost.cosmonic.sh/v1 can drive the agent too. The model field is accepted and ignored: the workload decides the model.
The request can carry:
| Field | Meaning |
|---|---|
input | Required. The person's message. |
instructions | Instructions for this run, ahead of the agent's own. |
stream | true streams server-sent events. |
reasoning.effort | minimal, low, medium, high, xhigh or max. |
text.format | {"type": "json_schema", "name": ..., "schema": {...}} asks for a reply that satisfies the schema. |
metadata | Up to 16 string pairs, echoed on the response. |
cosmonic | run_id, max_steps, max_total_tokens, max_duration_secs, and extensions (true adds cosmonic.* events). |
Fields that would let a caller choose the conversation, the tools or the model options, such as previous_response_id, tools and temperature, are refused with 400.
To require a token on every request, set auth-token on the run entry from a Secret with secretFrom, and send Authorization: Bearer <token>. Without it, anything that can reach the host's HTTP port can start runs. To stop a run, send POST /v1/responses/{id}/cancel, or close the stream.
5. Start a run over NATS
The NATS trigger listens on <prefix>.<namespace>.<name>.run. The prefix is cosmonic.agents unless agents.run.nats.subjectPrefix changes it, and the name is the WorkloadDeployment's. The body is the same JSON as the HTTP request. From outside the cluster, forward Control's built-in NATS (Nexus) to your machine first. The default install needs no credentials:
kubectl port-forward -n cosmonic-system svc/nexus 4222:4222In another terminal:
nats req cosmonic.agents.default.support-agent.run \
'{"input": "In one sentence, what can you reach?", "cosmonic": {"run_id": "try-1"}}' \
--replies=0 --timeout=11mEach event arrives as its own reply message, the same JSON an SSE data: line carries, ending with response.completed, response.incomplete or response.failed. nats req keeps waiting until --timeout, so stop it once the last event arrives. The request must use a reply subject under _INBOX., which nats req and NATS client libraries do by default.
To stop a run, send {"run_id": "try-1"} to cosmonic.agents.default.support-agent.stop.
Runs share one conversation
HTTP and NATS runs on the same WorkloadDeployment continue one conversation, shared by its replicas. A caller cannot pick another one, and a run_id names a run, not a conversation. One run at a time: a second request while a run is in progress gets 409 busy.
Next steps
- Give an agent a tool adds tool components to the workload, each with its own grants.
- Choose the model picks a source and model per workload.