Cloud: let Claude Code, Codex, and OpenCode use your own sign-in, with a per-machine team/own switch

Merged
AU
24d ago
+3384-145

Summary

On a cmux Cloud machine, signing in to a coding agent with your own account didn't work. Every agent was forced through CodeRouter, and when the team had no shared account it failed with CodeRouter's "no account" 403:

  • Claude Code: every shell exported a CodeRouter placeholder ANTHROPIC_API_KEY/ANTHROPIC_BASE_URL. Claude Code ranks that key above /login, so a successful /login still went to CodeRouter.
  • Codex: every shell generated ~/.codex/config.toml with model_provider = "cmux", so codex login was ignored.
  • OpenCode: it sends an opencode auth login key to whatever OPENAI_BASE_URL/ANTHROPIC_BASE_URL name, and the shell sets both to CodeRouter. So a user's own Anthropic or OpenAI key was sent to CodeRouter. I reproduced this with the pinned OpenCode 1.18.30.

Now each agent on a machine signs in one of two ways. Team uses the team's CodeRouter accounts, which keeps the zero-setup default. Own uses the agent's own sign-in, and CodeRouter stays out of the way.

  • cr use own|team|auto [claude|codex|opencode]... [--json] records the choice. It applies to all three agents when none is named. cr use shows each agent's mode and why. A switch takes effect the next time the agent starts, and the command says so.
  • If the user hasn't chosen:
    • An existing own sign-in on the machine counts as choosing own: Claude /login or CLAUDE_CODE_OAUTH_TOKEN, codex login or CODEX_API_KEY, opencode auth login.
    • Otherwise the agent uses team when the team has an account for it, and own when it has none, so the agent shows its normal sign-in instead of a 403. The check is the new GET /api/coderouter/agent-access, which returns booleans only and reads metadata only.
    • If that check gets no answer, the agent falls back to team, the old behavior.
  • CMUX_<AGENT>_AUTH=team|own pins a single launch, for automation and image verification. A user-set endpoint or key always wins and is never moved or persisted.
  • On a Cloud machine, the no-account 403 for each agent now names both ways out: cr add <agent>, or cr use own <agent> plus the agent's login. For older machines it gives the equivalent workaround. OpenCode's team-config fetch shows that message instead of "retry in one minute".

Mechanism

  • web/services/vms/images/devbox/agent-auth.sh is the single resolver. Both the launchers and cr use read it, so the status report and an actual launch can't disagree.
  • /usr/local/bin/<agent> and nvm's entry (login PATHs reach nvm first) point at a launcher, cmux-claude, cmux-codex or cmux-opencode. Each launcher execs /usr/local/libexec/cmux-<agent>-real.
  • agent-config.sh defines shell functions that go through the launcher, but defer to PATH when it already leads to the launcher or to a cmux-tui agent shim. That keeps the shim's per-pane hook context (#18792).
  • Team mode:
    • Claude Code gets the CodeRouter pair. Shells keep it aside as CMUX_CLAUDE_TEAM_*, and only that pair is ever persisted to model-plane.env.
    • Codex keeps the generated provider.
    • OpenCode gets its CodeRouter providers through OPENCODE_CONFIG (~/.config/cmux/opencode-team.json) and no longer through its global config.
  • Own mode:
    • Codex starts with -c model_provider="openai", but only over the config cmux generated.
    • Every launch drops the CodeRouter endpoints and the placeholder.
    • OpenCode team mode also drops the endpoints and keeps only the placeholder key its providers read.
  • guestAgentUpdates.ts keeps all three agents behind their launchers on machines that opted into agent updates.

Trust and credential policy

This changes credential precedence on Cloud machines. CodeRouter no longer outranks a user's own sign-in, and no own credential is sent to CodeRouter. Flagging it for #13742 per repo rules.

Rollout and limits

  • New images are promoted in this PR: run 38352413630 baked them from 062ed97db85 and reported "ALL CHECKS PASSED", and manifest.json points at them. The image changes reach new machines only. Running machines keep their baked behavior until they are recreated.
  • cr use needs a guest CLI re-pin after merge. The cmux-cloud-cli facade change ships in guestCliDistribution.json with the next Cloud CLI release. Until then, cr use reaches the official CodeRouter CLI, which doesn't know it. The 403 text gives the older-machine workaround.
  • Overlap with #18726 (Cloud VM agent login onboarding): it covers the same ground with shell functions and a login marker. Whichever lands second should drop its version.
  • Follow-up, not in this PR: a cmux app Cloud settings toggle for the same per-machine mode. Pi is unchanged; it reads its endpoint from ~/.pi/agent/models.json, not from the env.

Testing

  • Failing tests first: the Claude regression was committed at 727f72b7e67, red with 16 fail, then green.
  • Focused tests:
    • tests/vm-devbox-agent-auth.test.ts (37) runs the real agent-config.sh, agent-auth.sh, launchers and the guest adapter's cr use against fake agents, a fake curl and fake shims. It covers every mode and default, user-set values, persistence, the PATH and shim orderings, the cache, and the error paths.
    • Also coderouter-agent-access, coderouter-claude-upstream (hasActive), coderouter-no-account-message, the proxy tests, vm-devbox-image (including the OpenCode 403 path), vm-guest-cli and vm-agent-updates.
    • Result: 213 of 214 pass locally. The one failure, "concurrent OpenCode starts", hits bun's 5 s limit under load on this Mac and passes alone; it also failed once on this branch before the OpenCode change. CI runs it on Linux.
  • Real binaries, locally: Codex 0.154 own mode used the ChatGPT login at chatgpt.com, and team mode went to CodeRouter. OpenCode 1.18.30 own mode sent no request to CodeRouter, and team mode layered the providers. Claude Code 2.1.267 showed "Not logged in" when started without a key.
  • Live Cloud machine: production plus this branch's guest files.
    • Team mode: Claude Code and Codex reach CodeRouter, and its no-account 403 is correct for a team with no accounts.
    • Own mode: Claude Code says "Not logged in · Please run /login", and Codex goes to api.openai.com and gets 401 without a login.
    • OpenCode: own mode loads no CodeRouter providers, and team mode with no account now prints the server's no-account message.
  • Bake verification in run 38352413630: launchers on the login PATH for all three agents, the launcher checks, and Claude Code and Codex reaching their composers as root and as the work user in team mode.
  • Dev app: tagged dev build own-login at f79397fe044. The app is unchanged by this PR; it's for driving a Cloud machine.
  • bun run typecheck, bun run lint:complexity and scripts/verify-local.py pass. The Rust dispatch test runs in the Cloud CLI job.

Changelog

Fixed: Claude Code, Codex, and OpenCode on a cmux Cloud machine can use your own sign-in, and cr use own|team chooses between it and the team's CodeRouter accounts

Proof

No UI change. Shell and CLI behavior is covered by the tests above, and live machine behavior by the Cloud machine checks and the bake verification.

🤖 Generated with Claude Code