"It works on my machine" is a config problem
A multi-agent setup usually accumulates a hardcoded IP here, an absolute path there, a model ID baked into a prompt file, until moving it to a second server means hand-editing half the repo. The brief for this one was blunt: it has to run unchanged on any Linux host, with only env/<station>.yaml and environment variables allowed to differ.
No hardcoded values
No IPs, absolute paths, API keys, model IDs, or ports anywhere in the pack. Every one is ${VAR} or ${VAR:default}.
Transport-agnostic MCP
One server binary. stdio for a local subprocess, streamable HTTP for a containerized one, chosen by a single env var.
Per-agent models
Each agent names a gateway alias, never a provider model. Swapping GPT-4o for Claude Sonnet is a gateway edit, not an agent edit.
Four layers, generated together
Everything a station needs ships inside the pack, including the model gateway. No manual pip install, no hand-edited YAML outside env/.
gateway/
Eight model aliases (GPT-4o, Claude Sonnet, Gemini, local Ollama, OpenRouterβ¦) behind one OpenAI-compatible endpoint. A validating entrypoint drops any alias whose credential is missing, at boot, with a warning, not a crash.
mcp/tools-server/
search_web, get_weather, run_shell_safe. Pydantic-validated inputs, and a tool call never raises: every failure comes back as a structured {ok: false, error: {...}} instead of killing the session.
agents/ + prompts/
Each agent is a YAML file: a model alias, a tool allowlist, iteration limits, and a JSON output contract enforced by the orchestrator. The reviewer runs on the cheapest alias, at temperature 0, and gets no tools at all — it judges what it's handed, nothing more.
orchestrator/ + config_loader.py
Loads the pack, exposes /health, /agents, /run, and runs the tool-use loop against whichever MCP transport the station configured, over the same code path either way.
A config loader that reports everything at once
config_loader.py merges config/base.yaml → env/<station>.yaml → the environment, substitutes every placeholder, loads each agent, and validates the result against a JSON Schema. Click a stage below.
See it flow
A scripted, client-side recreation of one /run call, matching the sequence diagram below beat for beat. It loops through a hardcoded sequence to give a feel for a real tool-use round trip.
Demo replay, not a live feed
Trace
Not a mockup: a live deploy
The pack's own scripts/deploy.sh runs a preflight check, brings up all three containers, polls every health endpoint, then prints exactly this — captured from a real local deploy.
$ ./scripts/deploy.sh local
4. agent wiring
agent model alias contract tools status
---------- ------------- -------- ---------------------- ------
coder gpt-4o json run_shell_safe ready
researcher claude-sonnet json search_web,get_weather ready
reviewer claude-haiku json - ready
gateway http://gateway:4000 [ok]
mcp tools-server http http://tools-server:8081/mcp
deployed
ok station 'local' is up
Every service runs read-only-root, non-root, with the pack itself bind-mounted read-only into every container. 63 tests cover the config loader: merge precedence, every ${VAR:default} form, cross-reference checks (an agent naming a tool no server exposes fails the load), and that every problem in a run is reported together, never one at a time.
Diagrams
One request through the whole stack, and the same system at rest.
Sequence: one agent turn
The orchestrator never knows or cares whether the MCP session is a subprocess or an HTTP connection — same code path either way. The same steps drive the scripted trace above.
sequenceDiagram
actor Caller
participant Orch as orchestrator (/run)
participant MCP as tools-server (stdio or HTTP)
participant GW as gateway (LiteLLM)
participant LLM as Provider (alias-resolved)
Caller->>Orch: POST /run {agent, input}
Orch->>MCP: list_tools() for this agent's allowlist
MCP-->>Orch: tool schemas
Orch->>GW: chat completion + tool schemas
GW->>LLM: resolved alias -> provider model
LLM-->>GW: tool_use: search_web(...)
GW-->>Orch: tool call requested
Orch->>MCP: call_tool(search_web, args)
MCP-->>Orch: {ok, data, error}
Orch->>GW: tool result appended
GW->>LLM: continue
LLM-->>GW: final JSON answer
GW-->>Orch: response
Orch-->>Caller: contract-checked JSON
Architecture: at rest
Three containers, each read-only-root, sharing one read-only mount of the pack.
flowchart LR
subgraph Pack["Pack (read-only mount: /pack)"]
Cfg["config_loader.py\nconfig/ + env/ + agents/"]
end
Cfg -. validates .-> Orch["orchestrator\nFastAPI"]
Cfg -. validates .-> GW["gateway\nLiteLLM proxy"]
Cfg -. validates .-> MCP["tools-server\nFastMCP"]
Orch <-->|stdio or HTTP| MCP
Orch -->|OpenAI-compatible| GW
GW --> OpenAI[("OpenAI")]
GW --> Anthropic[("Anthropic")]
GW --> Gemini[("Gemini")]
GW --> Ollama[("local Ollama")]
GW --> OpenRouter[("OpenRouter")]