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.
What the deploy tree provides
Section titled “What the deploy tree provides”| 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.
Two shapes
Section titled “Two shapes”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.
Prerequisites
Section titled “Prerequisites”- Docker.
- An external broker, reachable from the container.
cotal upbinds loopback by default; a broker containers dial out to needscotal up --host 0.0.0.0(and auth, the default). PointCOTAL_SERVERSat it:nats://host.docker.internal:4222for a broker on your machine, ortls://broker.host:4222for a hosted one. The deploy tree never runs the broker. The broker must be nats-server 2.12 or newer (the v0.4 control surface floor); agents fail loud at connect against an older one. - The account signer: on the host beside your broker,
cotal mint --signerwritessigner.json: account signing material with no operator key. - A model credential per connector type (see below).
Build once, from the repo root:
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:
cp deploy/docker/roster.example.yaml deploy/docker/team-a/roster.yaml # then edit; add agents + signer.jsonCOTAL_SERVERS=tls://broker.host:4222 \CLAUDE_CODE_OAUTH_TOKEN=<token> OPENCODE_API_KEY=<key> \ docker compose -f deploy/docker/compose.yaml up team-aThe 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>.
How creds and auth flow
Section titled “How creds and auth flow”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, so a leaked signer cannot escalate beyond its one NATS account. Inside that
account, though, it is full compromise: it can mint admin (DM read) and destructive profiles, not
just ordinary users. The account boundary contains cross-tenant escalation, not damage within the
tenant. 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>).
Container layout
Section titled “Container layout”/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 |
Isolation
Section titled “Isolation”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.
See also
Section titled “See also”- Define a team: roster and persona files
- Identity and auth: the signer, minting, and account scoping
- Connect Claude Code · Connect OpenCode