Codex OAuth
Set up ChatGPT OAuth for Codex workers in local and remote Agent Swarm deployments
Codex workers can authenticate in three different ways:
OPENAI_API_KEY- A pre-seeded
~/.codex/auth.json - 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_KEYto 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-uuidNotes:
OPENAI_API_KEYis not required for this flow.AGENT_IDshould stay stable across restarts so task resume works correctly.MCP_BASE_URLmust point to the same swarm API that storescodex_oauth.
How The Flow Works
The login flow happens on your machine, not inside the container:
- You run
agent-swarm codex-loginfrom your laptop or local terminal. - The command opens a browser and completes the ChatGPT OAuth flow.
- Agent Swarm stores the resulting credential in the API config store as
codex_oauth. - Codex workers fetch that credential during container boot.
- The entrypoint converts it into the real
~/.codex/auth.jsonformat 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:httpIn another terminal:
bun run src/cli.tsx codex-loginThe 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_KEYWorker 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-345678901bcdAfter the login completes, restart Codex workers:
docker compose restart worker-codexIf 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_KEYVerification
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_oauthexists 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_KEYEach 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:
- Calls
loadAllCodexOAuthSlotsto enumerate allcodex_oauth_Nkeys from the config store. - Queries
GET /api/keys/available?keyType=CODEX_OAUTH&totalKeys=<N>to get the list of non-rate-limited slot indices. - Picks randomly from the available slots. If the availability endpoint is unreachable, falls back to random pick across all slots (best-effort).
- Calls
materializeCodexAuthJsonto write the selected slot's credentials into~/.codex/auth.jsonatomically (tmp → rename). - Passes
codexSlotthrough 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 therate 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 key | Type | Default | Valid range | Description |
|---|---|---|---|---|
CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS | positive integer (ms) | 7200000 (2 h) | 5 min – 7 days | How 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 hoursOr via the set-config MCP tool:
set-config key=CODEX_CREDITS_EXHAUSTED_COOLDOWN_MS value=14400000 scope=globalThe 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_0from the legacycodex_oauthconfig key at boot (ifcodex_oauth_0does 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"Related
- Harness Configuration — Configure which harness type (Claude Code, Codex, pi) workers use
- Environment Variables — Full reference for
OPENAI_API_KEYand all other configuration variables - Deployment Guide — Deploy agents to production with Docker Compose, including credential setup
- Getting Started — Initial setup for the Agent Swarm platform
Worker Credential Recovery
Workers wait for harness credentials at runtime instead of crash-looping the container — how the boot loop, dispatcher gating, and dashboard surface fit together
Slack Integration
Connect Agent Swarm to Slack — @mention agents to create tasks, use swarm# syntax to target specific workers, and manage multi-agent workflows via direct messages, channels, and the Slack Assistant sidebar.