fix(core): subscription logins win over API keys for their CLI

Merged
KJ
8d ago
+296-4

Summary

A subscription login now wins over an API key for the CLI it belongs to. Requested by Khaliq for self-hosted Coding agents; it applies to any run.

A run can receive both kinds of credential for the same provider. For example, a Claude setup token (CLAUDE_CODE_OAUTH_TOKEN) for the coding agent and ANTHROPIC_API_KEY for programmatic LLM calls. The Claude CLI prefers the API key when both are set, so the run billed the API instead of using the selected subscription.

CLISubscription detected byDropped from that CLI's agents
claudeCLAUDE_CODE_OAUTH_TOKEN (non-empty), or claude in AGENT_RELAY_SUBSCRIPTION_LOGINSANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN
codexcodex in AGENT_RELAY_SUBSCRIPTION_LOGINS (a materialised $CODEX_HOME/auth.json ChatGPT login)OPENAI_API_KEY, CODEX_API_KEY
grokgrok in AGENT_RELAY_SUBSCRIPTION_LOGINS ($GROK_HOME/auth.json)XAI_API_KEY

AGENT_RELAY_SUBSCRIPTION_LOGINS is set by agent-relay cloud worker when it mounts login files (AgentWorkforce/relay, feat/cloud-worker-cli-credentials).

Where the rule is applied (packages/core/src/credential-precedence.ts)

  • Broker env (HarnessDriverClient.spawn): every shadowed key is set to undefined. The broker spawns all PTY agents and inherits process.env, so this explicitly removes the inherited value; leaving the key out would let it leak back.
  • Interactive agents (spawnPty):
    • the agent's own env drops the keys shadowed for its CLI;
    • agentCredentialEnv gives the other keys back, so an opencode agent in the same run still gets ANTHROPIC_API_KEY and OPENAI_API_KEY.
  • Non-interactive agents and process-spawner runCommand: the keys shadowed for that CLI are stripped.
  • Unchanged:
    • The runner's own api steps read process.env, so ctx.llm keeps the API key.
    • Proxy-mode overrides are still applied last.

Tests

  • credential-precedence.test.ts:
    • Claude, Codex and Grok rules;
    • no subscription means no change;
    • a whitespace-only token is ignored;
    • the broker drops the keys, and each agent gets back only what its CLI may use.
  • completion-pipeline.test.ts: a run with both a Claude setup token and an Anthropic key.
    • The broker env has ANTHROPIC_API_KEY/OPENAI_API_KEY explicitly undefined.
    • The Claude agent's spawn env has no ANTHROPIC_API_KEY but keeps CLAUDE_CODE_OAUTH_TOKEN.
    • The opencode agent keeps both keys.
  • On sf-frame: npm ci, npm run typecheck, npm test (65 files, 1129 tests) pass.

馃 Generated with Claude Code


Autofix with [code]smith
Open in [code]smith 路 Need help on this PR? Tag @codesmith-bot with what you need.


Agent Relay sessions

  • claude session a19e66d9 路 opened via gh pr create 路 last active 2026-11-04