Agent SwarmAgent Swarm
GuidesProvider Auth

Codex OAuth

Set up ChatGPT OAuth for Codex workers in local and remote Agent Swarm deployments

Codex workers can authenticate in three different ways:

  1. OPENAI_API_KEY
  2. A pre-seeded ~/.codex/auth.json
  3. ChatGPT OAuth stored centrally in the swarm config store as codex_oauth_0, codex_oauth_1, ... (multi-credential pool)

This page covers the third option: run the login flow once (or multiple times for a pool), then let Codex workers restore the credential automatically at boot.

When To Use This

Use Codex OAuth when you want Codex workers to authenticate with your ChatGPT account instead of a raw OpenAI API key.

This is useful when:

  • you already use Codex locally with ChatGPT OAuth
  • you do not want to distribute OPENAI_API_KEY to every worker
  • you want one shared Codex OAuth credential per swarm

codex_oauth is currently stored at global swarm scope. One successful login applies to all Codex workers that can reach that same swarm API and database.

Worker Requirements

Every worker that should use this path needs:

HARNESS_PROVIDER=codex
API_KEY=your-shared-swarm-api-key
MCP_BASE_URL=http://api:3013
AGENT_ID=stable-worker-uuid

Notes:

  • OPENAI_API_KEY is not required for this flow.
  • AGENT_ID should stay stable across restarts so task resume works correctly.
  • MCP_BASE_URL must point to the same swarm API that stores codex_oauth.

How The Flow Works

The login flow happens on your machine, not inside the container:

  1. You run agent-swarm codex-login from your laptop or local terminal.
  2. The command opens a browser and completes the ChatGPT OAuth flow.
  3. Agent Swarm stores the resulting credential in the API config store as codex_oauth.
  4. Codex workers fetch that credential during container boot.
  5. The entrypoint converts it into the real ~/.codex/auth.json format expected by the Codex CLI.

That means the worker never needs a separate OAuth prompt.

Local Setup

Start the API first, then run the login flow:

bun run start:http

In another terminal:

bun run src/cli.tsx codex-login

The command prompts for:

  • swarm API URL
  • swarm API key

If your terminal supports raw-mode input, the API key is masked.

For a default local setup:

  • API URL: http://localhost:3013
  • API key: 123123

After the login completes, restart any local Codex worker containers so they restore the stored credential on boot.

Remote Docker Compose Setup

For a remote swarm, keep two URLs in mind:

  • Laptop-facing API URL: the public URL you use when running codex-login
  • Worker-facing API URL: the internal URL used by containers in MCP_BASE_URL

These can be different as long as they reach the same swarm API and database.

Example:

# run from your laptop
bun run src/cli.tsx codex-login \
  --api-url https://swarm.example.com \
  --api-key YOUR_API_KEY

Worker config inside Docker Compose might still be:

HARNESS_PROVIDER=codex
API_KEY=YOUR_API_KEY
MCP_BASE_URL=http://api:3013
AGENT_ID=c3b4d5e6-7890-12fa-bcde-345678901bcd

After the login completes, restart Codex workers:

docker compose restart worker-codex

If the remote API is not publicly reachable, use an SSH tunnel:

ssh -L 3013:localhost:3013 your-server
bun run src/cli.tsx codex-login --api-url http://localhost:3013 --api-key YOUR_API_KEY

Verification

Verify the credential was stored

curl -H "Authorization: Bearer YOUR_API_KEY" \
  "http://localhost:3013/api/config/resolved?includeSecrets=true&key=codex_oauth"

Verify the worker restored it

docker logs <codex-worker-container> 2>&1 | grep "Restored codex OAuth credentials"

Verify a task uses Codex OAuth

Run a trivial task, then inspect the task record:

curl -H "Authorization: Bearer YOUR_API_KEY" \
  "http://localhost:3013/api/tasks/<task-id>"

You should see:

  • credentialKeyType: "CODEX_OAUTH"
  • credentialKeySuffix: "..."

The API Keys dashboard should also show a codex-oauth credential entry.

Troubleshooting

Worker says Token data is not available

This usually means the worker did not get a valid Codex auth.json.

Check:

  • codex_oauth exists in the config store
  • the worker was restarted after login
  • the worker can reach MCP_BASE_URL

codex-login worked locally but workers still do not pick it up

Check that your laptop and the workers are talking to the same swarm API/database. A common mistake is logging into staging while the worker points at production, or vice versa.

I want different credentials for different workers

The Codex OAuth pool (see Multi-Credential Pool above) runs at global swarm scope — all workers share the same pool, and the runner selects a slot per task. There is no per-worker or per-agent slot affinity by design.

If you need true per-worker credential isolation, use separate swarm deployments or OPENAI_API_KEY instead.

Multi-Credential Pool

Migration 071 (src/be/migrations/071_codex_oauth_pool.sql) added api_key_status rows for CODEX_OAUTH keys, enabling the same rate-limit-aware pool logic already used for Claude OAuth tokens.

Provisioning slots

Run codex-login once per account you want to add to the pool:

# First account → stored as codex_oauth_0
bun run src/cli.tsx codex-login --api-url http://localhost:3013 --api-key YOUR_API_KEY

# Second account → stored as codex_oauth_1
bun run src/cli.tsx codex-login --api-url http://localhost:3013 --api-key YOUR_API_KEY

Each successive codex-login call increments the slot index automatically (codex_oauth_0, codex_oauth_1, codex_oauth_2, …). You can provision as many slots as you have ChatGPT accounts.

How slot selection works

At task-spawn time the runner:

  1. Calls loadAllCodexOAuthSlots to enumerate all codex_oauth_N keys from the config store.
  2. Queries GET /api/keys/available?keyType=CODEX_OAUTH&totalKeys=<N> to get the list of non-rate-limited slot indices.
  3. Picks randomly from the available slots. If the availability endpoint is unreachable, falls back to random pick across all slots (best-effort).
  4. Calls materializeCodexAuthJson to write the selected slot's credentials into ~/.codex/auth.json atomically (tmp → rename).
  5. Passes codexSlot through to the adapter so token refreshes write back to the correct slot key.

Selection is global — there is no per-agent or per-task affinity. Any worker can pick any slot.

Token-refresh write-back

When the Codex CLI refreshes an OAuth token mid-session, the adapter writes the new credentials back to codex_oauth_<picked-slot> via persistCodexOAuth. The legacy codex_oauth key and other slots are never touched by the refresh path.

Rate-limit detection

The adapter's formatTerminalError method matches incoming Codex error messages against two patterns:

  • [rate-limit] — HTTP 429 / per-minute / per-hour API rate limit. Prefix added by the rate limit, rate_limit, too many requests, or similar patterns.
  • [usage-limit] — Monthly quota exhausted (usage limit, upgrade to pro, usagelimitexceeded).

When the runner sees either prefix in a task's failureReason, it calls POST /api/keys/report-rate-limit to mark the used slot as unavailable. The rateLimitedUntil timestamp is:

  • Parsed from the error message if it contains an ISO 8601 or Unix-epoch reset time.
  • Set to now + CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS (default 2 hours) when the error is a workspace-credits-exhausted message ("Your workspace is out of credits…"). See Workspace credits exhausted below.
  • Otherwise set to now + 5 minutes as a conservative fallback for other unparseable rate-limit errors.

The slot is excluded from future availableIndices responses until rateLimitedUntil passes.

Workspace credits exhausted

Codex returns a distinct error when a workspace's credit balance is depleted:

"Your workspace is out of credits. Ask your workspace owner to refill in order to continue."

This error does not match standard rate-limit patterns, so the runner applies a dedicated cooldown rather than the 5-minute fallback. The default cooldown is 2 hours — workspace credits typically refill on a weekly cadence, so a short fallback would keep re-handing the dead slot every 5 minutes and burn retries until the next refill.

You can tune this cooldown via the swarm config store:

Config keyTypeDefaultValid rangeDescription
CODEX_CREDITS_EXHAUSTED_COOLDOWN_MSpositive integer (ms)7200000 (2 h)5 min – 7 daysHow long a credits-exhausted Codex OAuth slot is held out of the pool before being retried.

Set it via the API:

curl -X PUT http://localhost:3013/api/config \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"scope":"global","key":"CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS","value":"14400000"}'
# Sets cooldown to 4 hours

Or via the set-config MCP tool:

set-config key=CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS value=14400000 scope=global

The value must be a positive integer of milliseconds (e.g. "7200000" — no units suffix, no decimal point). The runner clamps the configured value to the [5m, 7d] range; values outside that range are silently brought to the nearest bound at runtime, but the write-time validator at PUT /api/config rejects clearly invalid inputs immediately.

Backwards compatibility

Single-credential deploys keep working unchanged:

  • The Docker entrypoint seeds codex_oauth_0 from the legacy codex_oauth config key at boot (if codex_oauth_0 does not yet exist). Workers never see the legacy key name at runtime.
  • If only one slot is provisioned, the pool still runs correctly — slot 0 is always selected and the availability filter is a no-op.

Known limitations

refresh_token_reused errors in multi-credential pools. When two runners independently pick the same slot at the same moment and the stored token is expired, both will call the OAuth refresh endpoint concurrently. The second call fails with a refresh_token_reused / 401 error because OpenAI's token rotation invalidates the previous refresh token on first use. materializeCodexAuthJson is atomic at the filesystem level, but token refresh is not distributed-locked across workers. The random slot selection in availableIndices reduces collision probability as pool size grows — the simplest mitigation is to provision more slots so the odds of two workers landing on the same expired slot simultaneously are negligible. A distributed lock for token refresh would fully eliminate this race; that is tracked as a separate follow-up.

Verification

# List all provisioned slots
curl -H "Authorization: Bearer YOUR_API_KEY" \
  "http://localhost:3013/api/config/resolved?includeSecrets=false" \
  | jq '[.configs[] | select(.key | test("^codex_oauth_\\d+$")) | {slot: .key, id: .id}]'

# Check rate-limit status for CODEX_OAUTH slots
curl -H "Authorization: Bearer YOUR_API_KEY" \
  "http://localhost:3013/api/keys/available?keyType=CODEX_OAUTH&totalKeys=3"

On this page