Agent registry
The agent registry is the data behind caveman wrap. Each supported coding agent is one JSON profile: command names, wire protocol, and how the CLI points it at the gateway. Adding an agent should be a data change, not a CLI rewrite.
The registry is part of the MIT CLI surface and compiles into the CLI binary. There are 5 profiles today: aider, claude, codex, gemini, and opencode. profiles/schema.json defines the contract.
A profile is data
A profile names an agent, says which wire protocol it speaks, and says how to point it at the gateway. Here is a trimmed example — the Claude Code profile:
{
"schema_version": "1",
"id": "claude",
"display_name": "Claude Code",
"vendor": "Anthropic",
"homepage": "https://code.claude.com/docs",
"binary_names": ["claude"],
"install": "npm i -g @anthropic-ai/claude-code",
"wire_protocol": "anthropic-messages",
"injection": {
"method": "env",
"env": {
"ANTHROPIC_BASE_URL": "{{cave_base_url}}",
"ANTHROPIC_AUTH_TOKEN": "{{cave_api_key}}"
}
},
"attribution": { "header": "x-cave-agent" },
"fallback": "generic-env",
"maintainer": null
}The fields:
| Field | What it carries |
|---|---|
id | kebab-case, globally unique — the caveman wrap <id> selector |
display_name, vendor, homepage | human-facing labels |
binary_names[] | matched against the wrap target for auto-detect |
install | one-line install hint shown when the binary is missing |
wire_protocol | the protocol the agent speaks to the proxy |
injection | how the CLI points the agent at the gateway |
attribution.header | the header the proxy reads to attribute telemetry to this agent |
fallback | what to apply if detection/injection fails (generic-env is the safe default) |
maintainer | null for the core team, else a GitHub handle |
Wire protocol
wire_protocol is one of exactly four values — only the protocols the proxy speaks natively:
anthropic-messagesopenai-chatopenai-responsesgemini-generatecontent
There is no protocol translation in the wrap path. The proxy speaks these; the profile picks one.
Injection
injection.method is one of two ways to redirect the agent:
env— set literal environment variables. Values may use the templates{{cave_base_url}},{{cave_api_key}},{{cave_proxy_url}}, and{{cave_org_id}}. A variable whose rendered value is empty is omitted — never set to""— so an empty auth token is never injected.config-env-content— render a mode-selected inline JSON config (config_content.localfor BYOKcaveman start;config_content.managedwhen the gateway points off-loopback) and set it as one env var. This is howopencodeis wrapped, viaOPENCODE_CONFIG_CONTENT, without ever touching the user'sopencode.json.
The compiler
compile.mjs is a zero-dependency Node compiler. It validates every profile against schema.json and emits two artifacts:
agents.json— the generated, published registry (do not hand-edit).../cli/src/agents.generated.ts— the CLI's embedded copy.
It runs in the CLI build and the CLI test, so the generated TypeScript is always in sync with the profiles.
node ../agents/compile.mjs # validate every profile, emit the registryThe source format is JSON, not YAML — deliberately. The CLI imports the generated TypeScript and never reads a file at runtime, which keeps it zero-runtime-dependency.
Fail closed on unknown shapes
An unknown protocol or method fails the compile
The compiler never guesses. If a profile names a wire_protocol outside the four the proxy speaks, or an injection.method it does not recognize, the compile fails — it does not fall back to a plausible protocol. This mirrors the eval graders' default-to-fail. A profile that compiles is a profile the proxy can actually route; a guess that silently shipped would be a wrong route.
Your first agent
Adding an agent is the lowest-friction contribution in the stack: write profiles/<id>.json, run node ../agents/compile.mjs, commit the regenerated agents.json and agents.generated.ts. Then caveman wrap <id> can route it.