Set Up a New ACPX Agent Key
Configures the credential for ONE ACPX agent_id so acp_spawn(agent_id=...)
launches the child CLI authenticated. The runtime never asks the LLM for a key:
each child reads its canonical env var, injected via
subprocess.Popen(env={**os.environ, **spec.env}) in
agent/acpx/runtime.py::AcpSession.spawn_child() (and _oneshot_send_turn).
Canonical env-var map
agent_id |
Env var the CLI reads | Top-level config.json key? |
|---|---|---|
claude |
ANTHROPIC_API_KEY |
YES — also read by imaging/image_interpreter.py + opus_client/claude_opus_client.py. |
gemini |
GEMINI_API_KEY (+ GOOGLE_API_KEY alias) |
YES — see _section_gemini in config.json. |
codex |
OPENAI_API_KEY |
NO. |
qwen |
DASHSCOPE_API_KEY |
NO. |
cursor |
none (uses own login) | NO. |
copilot |
none (gh auth login) |
NO. |
pi, droid, iflow, kilocode, kimi, kiro, opencode |
per upstream — check each CLI's docs | NO. |
tlamatini |
n/a (self-host) | NO. |
If unsure: agent/acpx/agent_registry.py::DEFAULT_ACP_AGENTS.
Procedure
1. Verify the CLI is resolvable
The runtime calls windows_spawn.resolve_command(spec.command) at spawn
time and surfaces AGENT_NOT_FOUND if it cannot find the binary.
where ${cli_command} # Windows
which ${cli_command} # POSIX
If absent: install the CLI per its upstream docs, or pass
command_override and add it to the agent block in step 3.
2. Add the secret to data.keys (gitignored vault)
Open data.keys at the repo root and add — or update — a line
KEY=VALUE. Use the canonical name from the table:
ANTHROPIC_API_KEY=sk-ant-api03-... # claude
GEMINI_API_KEY=AIzaSy... # gemini
GOOGLE_API_KEY=AIzaSy... # gemini alias (keep in sync)
OPENAI_API_KEY=sk-proj-... # codex
DASHSCOPE_API_KEY=sk-... # qwen
data.keys is in .gitignore (line 265). Never commit it.
3. Update Tlamatini/agent/config.json
Two layers, applied based on the table above:
Layer A — top-level key (claude / gemini only):
{
"ANTHROPIC_API_KEY": "<paste real value>",
"GEMINI_API_KEY": "<paste real value>"
}
Layer B — per-agent env injection (every agent_id that needs a key):
{
"acpx": {
"agents": {
"${agent_id}": {
"command": "<optional absolute path; omit when CLI is on PATH>",
"env": { "<CANONICAL_ENV_NAME>": "<paste real value>" }
}
}
}
}
Real worked examples (mirror these exactly):
"claude": {
"command": "C:/Users/<user>/AppData/Roaming/npm/claude.cmd",
"env": { "ANTHROPIC_API_KEY": "sk-ant-api03-..." }
},
"gemini": {
"command": "C:/Users/<user>/AppData/Roaming/npm/gemini.cmd",
"env": {
"GEMINI_API_KEY": "AIzaSy...",
"GOOGLE_API_KEY": "AIzaSy..."
}
},
"codex": { "command": "codex", "env": { "OPENAI_API_KEY": "sk-proj-..." } },
"qwen": { "command": "qwen-code", "env": { "DASHSCOPE_API_KEY": "sk-..." } }
How the merge resolves at spawn (read once, never wonder again):
agent/acpx/config.py::_coerce_agents_env()plucks eachenvdict out ofacpx.agents.<id>intoAcpxConfig.agents_env.agent/acpx/agent_registry.py::build_agent_registry()merges that dict on top ofDEFAULT_ACP_AGENTS[<id>].env(override wins).AcpSession.spawn_child()builds the child env as{**os.environ, **self.spec.env}— explicitacpx.agents.<id>.envwins over an exported shell variable.
4. Wire regen_secrets.py (only when introducing a brand-new key)
regen_secrets.py is the toggle between "push-able" placeholders and
real "keyed" values. If you added a data.keys entry that the script
does not already handle, extend it.
Patch patch_config_json() in regen_secrets.py (around lines
128-134):
# Top-level (claude / gemini only — skip for everything else)
set_top("<TOP_KEY>", "<DATA_KEYS_KEY>")
# Per-agent env injection — required for every agent that needs a key
set_acpx_env("<agent_id>", "<CANONICAL_ENV_NAME>", "<DATA_KEYS_KEY>")
Also list the new key in the docstring at the top of regen_secrets.py
(lines 12-16) so the file's own self-documentation stays accurate.
Existing wired keys (do NOT duplicate):
ANTHROPIC_API_KEY— top +acpx.agents.claude.envGEMINI_API_KEY,GOOGLE_API_KEY— top (GEMINI only) +acpx.agents.gemini.env(both)OLLAMA_TOKEN— top only
5. Apply the changes
If you edited config.json directly (Layer A + Layer B), you are done.
If you only edited data.keys (and updated regen_secrets.py in step 4
when needed), run the regen script to splat the values into config.json:
python regen_secrets.py --mode keyed --dry-run # preview
python regen_secrets.py --mode keyed # apply
Always verify with git diff Tlamatini/agent/config.json that no
unexpected keys flipped to placeholders or vice versa.
6. Restart Django
config.json is read at startup by agent/config_loader.py and the
ACPX runtime caches the resolved registry. The new key does NOT take
effect until the server restarts. Stop and restart manage.py runserver --noreload (or relaunch Tlamatini.exe).
7. Verify
In the Tlamatini chat with Multi-Turn enabled, ask the LLM to:
- Call
acp_doctorand confirm the row for${agent_id}showsresolvable: true. - Call
acp_spawn(agent_id="${agent_id}", task="...")and confirm the spawn returns asession_id(noAGENT_NOT_FOUND). - Call
acp_send_and_wait(session_id, "Reply with the literal token ALIVE."). The transcript at<acpx-state>/<session>.transcript.ndjsonmust contain a non-empty assistant turn — proves the key was injected and the CLI authenticated. Empty transcript = key not reaching the child. - Call
acp_kill(session_id).
Or run the seeded GEMINI LIVE REASONING SHOWCASE prompt (idPrompt 31
from migration 0073_acpx_demo_gemini_uplift.py) for an end-to-end
reasoning round-trip when agent_id=gemini.
Common pitfalls
- Top-level vs per-agent: the top-level
ANTHROPIC_API_KEY/GEMINI_API_KEYis read by Tlamatini's own internal callers (image_interpreter.py,opus_client.py); the per-agentacpx.agents.<id>.envis read by the spawned child. Setting only ONE half breaks the other half. For claude/gemini, set BOTH. - Forgetting the alias: Gemini CLI versions vary; some only read
GEMINI_API_KEY, others onlyGOOGLE_API_KEY. Always inject BOTH underacpx.agents.gemini.envto avoid the fragility. - Empty
envdict survives merge:_coerce_agents_env()skips entries whereenvis missing or empty, so the dict shape"agents": { "claude": {} }does NOT inject anything. Make sure theenvkey actually contains the env vars. - Stale process env: if you exported the key in your shell BEFORE
starting Django and now want config.json to win, remember the merge
order: explicit
acpx.agents.<id>.envalways wins overos.environ. - CLI not on PATH: symptom is
AGENT_NOT_FOUNDfromacp_doctor, not an auth error. Fix thecommandfield, not theenvfield. - Pushing keys: before
git push, runpython regen_secrets.py --mode push-able, push, thenpython regen_secrets.py --mode keyedto restore. Never commit real values into config.json.
Output
{
"files_changed": [
"data.keys",
"Tlamatini/agent/config.json",
"regen_secrets.py"
],
"doctor_ok": true
}
regen_secrets.py is included only when step 4 added a brand-new
set_top / set_acpx_env rule.