Skip to main content

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.
  • kubectl and helm pointed at that cluster, and openssl.
  • 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-system

1. 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: true
  • agents.models.sources lists the model endpoints the host calls for agents. A workload picks one by name; it never supplies an endpoint or a key. A key on a plain http:// endpoint stops the host from starting, because the key would be sent in clear. Use https, or set allowPlaintextKey: true on that source.
  • agents.sessions.storage.key names the Secret from step 1. Without it, the host serves no session and an agent workload fails to start.
  • agents.run.http and agents.run.nats turn on the two triggers: the ways a host can start a run. Each workload still chooses which of them reach its agent.
Give sessions their own NATS identity in production

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.yaml

Hosts 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-key
helm upgrade cosmonic-control oci://ghcr.io/cosmonic/cosmonic-control \
  --version 0.15.0 \
  --namespace cosmonic-system \
  --reuse-values \
  -f control-agents-values.yaml
Enable Nexus persistence to keep sessions

By 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-agent

Apply it and wait for the workload to be ready:

kubectl apply -f agent.yaml
kubectl get workloads

A 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-agent for 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-secs on 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
EOF

Then 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:

FieldMeaning
inputRequired. The person's message.
instructionsInstructions for this run, ahead of the agent's own.
streamtrue streams server-sent events.
reasoning.effortminimal, low, medium, high, xhigh or max.
text.format{"type": "json_schema", "name": ..., "schema": {...}} asks for a reply that satisfies the schema.
metadataUp to 16 string pairs, echoed on the response.
cosmonicrun_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:4222

In 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=11m

Each 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​