Local HashiCorp stack for cat-agent deploy: Consul + Vault (-dev) +
Nomad (server+client) + LiteLLM gateway (+ optional Zot).
Agent packaging / CLI live in the sibling cat-agent library
(pip install 'cat-agent[platform]'). This repo is only the compose fixture
and sample operator config.
- Docker Desktop (macOS) or Docker Engine (Linux)
pip install 'cat-agent[platform]'(providescat-agent stack/deploy)- A real LLM backend for agent calls (default: host Ollama on Mac)
cd /path/to/cat-agent-stack
cp .env.example .env
# edit .env — VAULT_TOKEN=root + Ollama/OpenAI
export CAT_AGENT_STACK_DIR=$PWD
export CAT_AGENT_CONFIG=$PWD/cat-agent.config.toml
cat-agent stack bootstrap # compose up --build -d + Vault seed + demo team key
cat-agent doctor # must print docker_network: cat-agent-stack_hashicorp
# deploy an agent from the cat-agent checkout:
cat-agent deploy --dir /path/to/cat-agent/examples/serve_fastapi
curl -sS http://demo-calculator.localhost:8088/readyzOptional Zot registry profile:
cat-agent stack bootstrap --profile registry --registrycat-agent rm demo/calculator --yes # optional
cat-agent stack downDo not run bare docker compose — HOST_NOMAD_DATA / HOST_ZOT_DATA
must be absolute paths; cat-agent stack sets them.
There are two different kinds of secrets. Mixing them up feels like “aptallık”; they are not the same thing.
laptop .env ──seed──► Vault platform LLM secret
│
▼
LiteLLM gateway ◄── uses Ollama + OpenAI provider keys
▲
│ (virtual key as OPENAI_API_KEY)
deployed agent job ─────────┘
▲
└── reads ONE team virtual key from Vault
secret/.../llm/teams/{team}
Written by seed into secret/data/platform/llm.
| Field | Role |
|---|---|
OLLAMA_API_BASE / OLLAMA_API_KEY |
Upstream Ollama (host or cloud) |
OPENAI_API_BASE / OPENAI_API_KEY |
Upstream OpenAI (optional; can coexist with Ollama) |
LITELLM_MASTER_KEY |
Admin key for LiteLLM (mint/delete virtual keys) |
Yes: Ollama and OpenAI keys can both live here at once. LiteLLM picks which
upstream to call from the model id in agent.yaml (model.alias), not from
“which person deployed”.
These are org/stack credentials — not Ali’s vs Ayşe’s personal OpenAI keys.
Minted by LiteLLM (key/generate), stored only as api_key at
secret/data/platform/llm/teams/{team} (default team: demo).
- Every agent job for that team injects this same key as
OPENAI_API_KEY. - The job’s
OPENAI_BASE_URLpoints at the gateway, not at Ollama/OpenAI. - Purpose: gateway auth + TPM/RPM budget for that team, not identity of a person.
So today: 1 team → 1 virtual key → all that team’s deployed agents share it.
| Expectation | Reality |
|---|---|
| Each person on the team has their own OpenAI/Ollama key in Vault | Not supported for deploy. Provider keys are platform-shared (Layer A). |
| Each person has their own LiteLLM virtual key | Not the default. Seed mints per team, not per user. |
Laptop .env is what the Nomad job uses |
No. Jobs only see Vault (Layer B for the call key; LiteLLM uses Layer A). |
If you need per-person billing / keys, that is a different product shape, e.g.:
- mint LiteLLM keys per user (
--teamis a stand-in; you’d useteams/{user}or a users path), or - run
cat-agent servelocally with that person’s own env keys (no Nomad), or - put provider keys behind a corporate gateway account and keep Layer B as team quotas only.
Local demo intent: one shared Ollama/OpenAI for the stack + one demo virtual key.
Containers alone are not enough. Vault starts empty (-dev); seed fills Layer A
- Layer B from this repo’s
.env. It does not start/stop Docker.
| Step | What it does |
|---|---|
| 1. Layer A | Writes provider + master key into secret/data/platform/llm |
| 2. Layer B | Mints LiteLLM virtual key for --team (default demo) → …/teams/{team} + Vault read policy |
| 3. Optional | --registry → Zot push/pull secrets |
Re-run seed after changing .env, recreating Vault, or adding another --team.
Commands (related)
| Command | Meaning |
|---|---|
cat-agent stack up |
Start compose only |
cat-agent stack seed |
Fill Vault + mint team key from .env (stack already up) |
cat-agent stack bootstrap |
First-time: up --build -d then seed |
cat-agent stack down |
Stop compose |
cat-agent stack seed # refresh Layer A + demo team key
cat-agent stack seed --team billing # also mint a second team’s virtual keySecrets are never printed; seed only logs bases / key lengths.
| File | Role |
|---|---|
cat-agent.config.toml |
Sample for CAT_AGENT_CONFIG (Nomad, gateway, Mac docker_network) |
.env / .env.example |
Provider keys + VAULT_TOKEN=root for seed |
docker-compose.yml |
Project name pinned: cat-agent-stack → network cat-agent-stack_hashicorp |
Bridge CNI fails with unknown FS magic on "/var/run/docker/netns/…".
This stack sets:
docker_network = "cat-agent-stack_hashicorp"
consul_dns = "10.32.0.2"cat-agent doctor must show those lines before deploy.
Platform config lives here (not in the cat-agent library repo):
CAT_AGENT_CONFIG$CAT_AGENT_STACK_DIR/cat-agent.config.toml- cwd only if this directory is the stack root (
docker-compose.ymlpresent) - sibling
../cat-agent-stack/cat-agent.config.tomlnext to a cat-agent checkout ~/.cat-agent/config.toml
*.localhost resolves to 127.0.0.1 on the client machine. Other PCs and phones
on the same network cannot use http://demo-calculator.localhost:8088.
Traefik + Consul Catalog already front agents. For a corporate deployment, use your internal DNS (Active Directory / Bind / CoreDNS / cloud private zone) — do not rely on public wildcard DNS helpers.
# Must match what clients resolve via company DNS
ingress_host_template = "{team}-{name}.agents.example.internal"
public_url_template = "https://{team}-{name}.agents.example.internal"Then:
- Create a DNS record or wildcard zone so
*.agents.example.internal→ the Traefik / load-balancer VIP that publishes the agents (HTTP/HTTPS). - Redeploy agents so Nomad job tags pick up the new
Host(…)rule (cat-agent deployagain). - Clients open the URL from
public_url_template(any machine on the corp network that can resolve that name).
Placeholders: {team}, {name}, {agent} (agent == name).
Env overrides: CAT_AGENT_INGRESS_HOST_TEMPLATE, CAT_AGENT_PUBLIC_URL_TEMPLATE.
Local laptop default stays {team}-{name}.localhost — no DNS required on that Mac.
cat-agent ls
cat-agent status demo/calculator
cat-agent logs demo/calculator
cat-agent rm demo/calculator --yesTraefik (host :8088): hostname from ingress_host_template
(local default http://{team}-{name}.localhost:8088)
Dashboard: http://127.0.0.1:8089 · Nomad: http://127.0.0.1:4646
| Knob | Local value |
|---|---|
llm_gateway |
http://llm-gateway.service.consul:4000/v1 |
vault_addr |
http://127.0.0.1:8200 |
VAULT_TOKEN |
root (-dev only) |
| Team key | secret/data/platform/llm/teams/{team} |
docker_network |
cat-agent-stack_hashicorp |
consul_dns |
10.32.0.2 |
ingress_host_template |
{team}-{name}.localhost |
public_url_template |
http://{team}-{name}.localhost:8088 |
base_image |
cat-agent-runtime:latest (publish/pull in a later phase) |
docker-compose.yml # pinned name: cat-agent-stack
cat-agent.config.toml # sample CAT_AGENT_CONFIG
.env.example
config/ # consul, nomad, vault, litellm, zot
jobs/ # gate Nomad jobs
Dockerfile.* # consul / nomad / vault images
docker-entrypoint-*.sh # container entrypoints (not operator scripts)
Omit or retarget Mac-only knobs (docker_network, consul_dns=10.32.0.2,
host.docker.internal). See comments in cat-agent.config.toml and compose.