This document describes how Codex authentication currently works in CoCalc, in both cocalc-plus and launchpad/project-host deployments.
This covers:
- Auth source resolution for Codex turns
- Credential storage and replication
- Project-host runtime materialization
- Site-funded Codex admission, proxy enforcement, and accounting
For each Codex turn, project-host resolves auth in this order:
- ChatGPT subscription (
subscription) - Workspace OpenAI API key (
project-api-key) - Account OpenAI API key (
account-api-key) - Site OpenAI API key (
site-api-key) - Shared
~/.codexfallback (shared-home) only when explicitly enabled
Launchpad default:
- shared-home auth is disabled by default (
COCALC_PRODUCT=launchpad) - this avoids accidental collaborator auth leakage from workspace
~/.codex/auth.json
Resolution code:
The central hub stores external credentials encrypted at rest, and exposes two API surfaces:
- User-facing system RPCs for settings UI (
hub.system.*) - Host-facing RPCs for project-hosts (
hub.hosts.*)
flowchart TD
U[User in Browser] --> SYS[hub.system RPC]
SYS --> DB[(external_credentials table)]
PH[Project Host] --> HOSTS[hub.hosts RPC]
HOSTS --> DB
DB --> ENC[encrypted_payload via server crypto settings]
Primary server modules:
- src/packages/server/conat/api/system.ts
- src/packages/server/conat/api/hosts.ts
- src/packages/server/external-credentials/store.ts
Project-host resolves auth at turn start, then runs upstream codex inside a per-project/per-auth-context podman runtime.
flowchart TD
FE[Frontend chat turn] --> ACP[ACP evaluate]
ACP --> CE[CodexAppServerAgent]
CE --> SP[project-host codex spawner]
SP --> AR[resolve auth runtime]
AR -->|subscription/api key/shared-home| CT[podman codex container]
AR -->|site-funded| FP[host-local funded proxy]
FP --> OAI[OpenAI Responses API]
FP --> SEED[seed funding ledger]
CT --> CODEX[upstream codex app-server]
AR --> REG[host RPC: credential pull/check]
REG --> HUB[central hub]
Key modules:
- src/packages/ai/acp/codex-app-server.ts
- src/packages/project-host/codex/codex-project.ts
- src/packages/project-host/codex/codex-auth.ts
- src/packages/project-host/codex/codex-auth-registry.ts
Interactive ACP requests execute directly on the project host. Their Conat subjects bind both security identities:
acp.project-<project_id>.account-<account_id>.<operation>
The project remains the second subject segment so normal project-host routing can resolve the destination. Project-host authorization then requires all of the following:
- the authenticated principal is an account, not a project identity;
- the subject account equals the authenticated account;
- the account is a locally mirrored owner or collaborator of the subject project;
- the operation is a publication; replies use the caller's private inbox.
Viewer and public-share grants do not authorize ACP. The ACP server derives
both IDs from the subject and rejects any payload or nested chat project ID
that disagrees with it. This is important because account_id selects
credential, approval, admission, and attribution paths; it must never be
trusted solely from request data.
Legacy subjects of the form acp.project-<project_id>.<operation> are accepted
only for authenticated collaborators to reach a compatibility listener. That
listener returns ACP_CLIENT_REFRESH_REQUIRED, terminates the request, and
never calls ACP execution, session, control, or automation handlers.
Key modules:
- src/packages/conat/ai/acp/subjects.ts
- src/packages/conat/ai/acp/server.ts
- src/packages/project-host/conat-auth.ts
- src/packages/server/conat/socketio/auth.ts
Two supported paths:
- Device auth from project-host runtime (
codex login --device-auth) - Fallback upload of
auth.jsongenerated on user machine
Project-host handlers:
Frontend controls:
Managed in account settings UI and stored as external credentials:
external_credentials.encrypted_payloadin Postgres- encrypted/decrypted by server credential helpers
Subscription auth cache directory:
- default root:
codexSubscriptionsPathfrom src/packages/backend/data.ts - default location on hosts:
/btrfs/data/secrets/codex-subscriptions/<account_id>/
Typical files:
auth.jsonconfig.toml(cli_auth_credentials_store = "file").last_usedmarker for GC
In project-host mode, CoCalc intentionally separates auth material from session history.
- Auth source-of-truth:
- subscription auth comes from host secrets (
/btrfs/data/secrets/codex-subscriptions/...) - API-key auth comes from credential resolution and env injection
- subscription auth comes from host secrets (
- Session history source-of-truth:
- Codex session JSONL files live under workspace storage (
/root/.codex/sessionsin the runtime container, i.e. project volume)
- Codex session JSONL files live under workspace storage (
Important behavior:
- Project-host ignores workspace
~/.codex/auth.jsonfor auth resolution in launchpad mode. - For subscription auth, project-host mounts only auth files (
auth.json,config.toml) from secrets into/root/.codex, while keeping/root/.codex/sessionsin the workspace. - Shared-home auth (
shared-home) can still be explicitly enabled viaCOCALC_CODEX_AUTH_SHARED_HOME_MODEfor single-user/plus-style deployments.
Project-host runs periodic cleanup of stale local subscription caches.
Defaults:
- TTL: 72h (
COCALC_CODEX_SUBSCRIPTION_CACHE_TTL_MS) - Sweep interval: 1h (
COCALC_CODEX_SUBSCRIPTION_CACHE_SWEEP_MS)
GC skips currently mounted /root/.codex paths from active codex-* containers.
When auth source is site-api-key, each turn uses the funded path:
- the project host requests an atomic seed-authoritative reservation;
- the project runtime receives a short-lived proxy token, never the site API key;
- a host-local OpenAI-compatible proxy forces the configured model, reasoning, service tier, output limit, request count, duration, and maximum cost;
- every provider response emits an idempotent exact usage event;
- completion, interruption, failure, and expiration all settle observed cost;
- unreported usage and finish events remain in a durable host SQLite outbox.
The initial policy is GPT-5.6 Luna, low reasoning, standard speed, no OpenAI
hosted paid tools, and a five-cent maximum reservation. All limits are dynamic
site settings. site_funded_codex_enabled disables only included access;
launch_disable_ai remains the complete AI kill switch.
Per-account 5-hour and 7-day allowances come exclusively from the account's
resolved membership ai_limits, including any account entitlement override.
There are no separate free/member fallback allowances in site settings. One AI
unit represents one cent of provider spend, so 100 units equals US$1; either
limit being zero disables included Codex for that account. The separate free
and paid weekly pools are aggregate site-wide spending circuit breakers.
Modules:
- src/packages/ai/acp/codex-site-key-governor.ts
- src/packages/project-host/codex/codex-site-metering.ts
- src/packages/project-host/codex/site-funded-proxy.ts
- src/packages/server/ai/site-funded-codex-ledger.ts
- src/packages/server/ai/site-funded-codex-policy.ts
- src/packages/server/conat/api/hosts.ts
Exact funded host RPCs:
hosts.reserveSiteFundedCodexTurnhosts.heartbeatSiteFundedCodexTurnhosts.recordSiteFundedCodexUsageEventhosts.finishSiteFundedCodexTurnhosts.getSiteFundedCodexPoolStatushosts.getSiteOpenAiApiKey
Legacy aggregate allowance/report RPCs remain only for compatibility with old site-key runtimes and are deliberately bypassed by exact funded turns.
Funded costs use integer micro-US-dollars and a versioned Luna price catalog. The ledger separately records ordinary input, cached input, cache writes, output/reasoning, long-context pricing, provider request identity, and tool fees. Prompt and response content is not stored in the funded ledger.
The admin Site Settings page shows free/member pool committed and reserved exposure. For provider reconciliation, use a dedicated OpenAI project and set:
site_funded_codex_openai_project_id;site_funded_codex_openai_admin_key(an organization admin key, not the normal provider API key).
Refresh the pool card to compare current-period local committed cost with the OpenAI Costs API. OpenAI billing data can lag, so short-lived discrepancies are expected; persistent discrepancies require investigation before widening a rollout.
Project-host caches site key fetches and refreshes on:
- cache expiry
- explicit force refresh after an auth failure retry path
Current intent:
- avoid frequent polling across many hosts
- recover quickly when key rotates or cache is stale
- The user does not get shell access to the Codex runtime container.
- For personal API-key auth, project-host injects provider config and
OPENAI_API_KEYinto the Codex runtime. - For site-funded auth, the runtime only receives a turn-scoped local proxy token. The host forwards the real API key to OpenAI.
- For subscription auth, Codex reads file-based auth from mounted
/root/.codex/auth.jsonand/root/.codex/config.tomlsourced from host secrets, not from workspace files.
- OpenAI's Costs API requires a separate organization admin key and reports daily buckets, so reconciliation is not instantaneous.
- Account-home compatibility projection into the legacy
ai_usage_logUI is separate from the exact seed ledger; the Codex UI reads exact funded remaining allowance directly. - Strong end-to-end project-host websocket authz is still a separate hardening project.
When an account must be disabled quickly (abuse, ToS violations, security response), we use a layered approach:
- New funded turns can be blocked immediately with
site_funded_codex_enabled, the global pool, an account hold, orlaunch_disable_ai. - Running funded turns remain bounded by their local signed reservation, duration, request count, and cost limit even if the seed is unavailable.
- Credential-backed turns (ChatGPT plan / user API key) are not billed to CoCalc, so the immediate financial risk is lower, but account-level enforcement still depends on connection/session auth controls.
- Central hub marks account as banned and stops issuing/refreshing project-host auth leases.
- Hub pushes a kick event to project-hosts to disconnect live sockets for that account.
- Project-host closes active Codex sessions/streams for the banned account.
- Reconnect and token refresh attempts fail, so access does not resume.
This gives both:
- low-friction long-lived sessions for legitimate users, and
- fast forced eviction for abuse cases without waiting for long token expiry windows.
If a turn uses the wrong auth source or fails unexpectedly:
- Check current payment source in account settings UI panel.
- Check host logs for resolved auth source (
project-host:codex-authandproject-host:codex-project). - Verify presence/absence of local subscription auth files in
codex-subscriptions/<account_id>. - Verify central credential existence via
hub.system.listExternalCredentials. - For site-funded mode, verify the Site Settings pool card, reservation denial
code, project-host outbox, and
project-host:site-funded-codex-proxylogs.