Skip to content

Deploy: agent teams against an external broker

Guide (informative) · For: operators · Prereqs: Quickstart

The deploy/ tree runs a team of agents in an isolated container that dials out to an existing Cotal broker. The container gets no host file access; only NATS traffic crosses the wall. One image, configured entirely by env and mounts; add or reshape a team by editing the roster and agent files, never the image.

deploy/README.md is the full walkthrough (a local quickstart plus production notes). This page is the map: what the tree provides, what you need, and how creds flow.

file what it is
deploy/docker/Dockerfile Builds one image (cotal-runner) bundling the cotal, claude, and opencode CLIs, installing the mesh plugin (cotal setup), and pre-completing Claude’s first-run onboarding for unattended use.
deploy/docker/entrypoint.sh Waits for the broker to be reachable, then runs cotal <cmd> --server $COTAL_SERVERS.
deploy/docker/compose.yaml Two example services: team-a (a manager + roster) and solo (one agent).
deploy/docker/roster.example.yaml A roster template to copy.

What it does not provide: the broker (external: you point at it), and, per the README, host-side per-agent cred provisioning and stronger sandbox isolation are called out as later hardening; they are not built yet. What ships today is the phase-1 container boundary described under Isolation.

The image supports Claude Code and OpenCode agents only; it does not bundle uv/hermes-agent, so Hermes cannot run in a container today.

The container’s command picks the shape:

command shape
supervise --space <s> --roster /workspace/roster.yaml a manager that boots every agent in the roster (all in one container, pty runtime)
spawn <name> one foreground agent, loading .cotal/agents/<name>.md

Mix connector types freely within a roster (agent: claude / agent: opencode per entry). See Define a team for the roster and persona files.

  • Docker.
  • An external broker, reachable from the container. cotal up binds loopback by default; a broker containers dial out to needs cotal up --host 0.0.0.0 (and auth, the default). Point COTAL_SERVERS at it: nats://host.docker.internal:4222 for a broker on your machine, or tls://broker.host:4222 for a hosted one. The deploy tree never runs the broker.
  • The account signer: on the host beside your broker, cotal mint --signer writes signer.json: account signing material with no operator key.
  • A model credential per connector type (see below).

Build once, from the repo root:

Terminal window
docker build -f deploy/docker/Dockerfile -t cotal-runner .

Then run a team. With compose, paths are relative to docker/, so put signer.json, team-a/roster.yaml, and team-a/agents/*.md there:

Terminal window
cp deploy/docker/roster.example.yaml deploy/docker/team-a/roster.yaml # then edit; add agents + signer.json
COTAL_SERVERS=tls://broker.host:4222 \
CLAUDE_CODE_OAUTH_TOKEN=<token> OPENCODE_API_KEY=<key> \
docker compose -f deploy/docker/compose.yaml up team-a

The README’s quickstart shows the equivalent single docker run (with the mounts spelled out) and a local-broker variant. Watch the team join with cotal console --plain --space <s>.

Two independent credentials, both set from outside the container:

Broker auth (the NATS mesh). Mount the stripped signer.json read-only at /workspace/.cotal/auth/auth.json. Inside the container, each agent’s own scoped creds are minted from it into a tmpfs (/workspace/.cotal/auth/creds, RAM only). The operator root-of-trust never enters a container; the worst a leaked signer allows is minting users within that one NATS account, which the account boundary already contains. See Identity and auth.

Model auth (the LLM provider). Set each connector’s credential as an env var; the supervisor forwards the named vars and each CLI reads only the ones it understands:

connector env notes
claude CLAUDE_CODE_OAUTH_TOKEN from claude setup-token on your host; runs on your Claude Pro/Max subscription, same as local
opencode the env var of the provider behind each agent’s model: per provider: OPENCODE_API_KEY for OpenCode’s hosted models, OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.

Every var set on a team container reaches every agent in it; the container is the team’s trust boundary, so secrets are not isolated between agents in the same container. For hard per-agent isolation, run one agent per container (the solo service: same image, spawn <name>).

/workspace is the working directory:

path mode holds
/workspace/.cotal/auth/auth.json ro mount the stripped signer
/workspace/.cotal/agents/*.md ro mount personas
/workspace/roster.yaml ro mount the roster (supervise mode)
/workspace/.cotal/auth/creds/ tmpfs (mode=01777) minted per-agent creds, RAM only

Phase 1 is a non-root user (uid 10001), cap_drop: ALL, no host mounts beyond the read-only ones above, and an ephemeral writable fs. Egress is the broker plus each agent’s model API. Stronger isolation (a fully read-only rootfs, or gVisor / Kata via --runtime) is a later swap with no app change.