## Stack
This is PR 2 of the simplified HAI single-run-task stack:
- [#19047](https://github.com/openai/codex/pull/19047) Agent Identity
assertion and task-registration primitives, including the shared
run-task helper used by existing Agent Identity JWT auth.
- [#19049](https://github.com/openai/codex/pull/19049)
Disabled-by-default ChatGPT auth opt-in that provisions/reuses persisted
Agent Identity runtime auth and its single run task.
- [#19051](https://github.com/openai/codex/pull/19051) Run-scoped
provider auth that uses one backend-owned task id for first-party
inference and compaction requests.
[#19054](https://github.com/openai/codex/pull/19054) collapsed out of
the active stack because the simplified design no longer needs a
separate background/control-plane task helper.
## Summary
This PR adds the disabled-by-default path for normal ChatGPT-login Codex
sessions to obtain Agent Identity runtime auth through the Codex
backend. Existing Agent Identity JWT startup mode remains a separate
path and does not require the feature flag.
What changed:
- adds the experimental `use_agent_identity` feature flag and config
schema entry
- adds an explicit `AgentIdentityAuthPolicy` so call sites choose
`JwtOnly` or `ChatGptAuth` instead of passing a bare boolean
- stores standalone Agent Identity JWT credentials separately from
backend-registered Agent Identity records
- persists the registered Agent Identity record, private key, and single
run task id in `auth.json` so process restarts reuse the same identity
- derives the agent/task registration base URL from ChatGPT/Codex auth
config while keeping JWT JWKS lookup separate
- provisions and caches ChatGPT-derived Agent Identity runtime auth when
`use_agent_identity` is enabled
- reuses the shared run-task registration helper from PR1 rather than
adding a second task-registration path
This PR intentionally does not switch model inference over to
`AgentAssertion` auth. The provider-auth integration lands in the next
PR.
## Testing
- `just test -p codex-login`
The workspace denies `clippy::expect_used` in production. Although
`clippy.toml` allows `expect` in tests, Bazel Clippy compiles
integration-test helper code in a way that does not receive that
exemption, which encouraged verbose `unwrap_or_else(... panic!(...))`
and equivalent `match`/`let else` forms.
This allows `clippy::expect_used` once at each integration-test crate
root (including aggregated suites and test-support libraries), then
replaces manual panic-based Result and Option unwraps with
`expect`/`expect_err`. Standalone `tests/*.rs` files remain their own
crate roots. Intentional assertion and unexpected-variant panics remain
unchanged, and the production `expect_used = "deny"` lint remains in
place.
The cleanup is mechanical and net-negative in line count.
## Why
Windows Credential Manager limits generic credential blobs to 2,560
bytes. Large serialized ChatGPT auth payloads can exceed that limit, so
keyring-mode CLI auth needs a backend that keeps only the encryption key
in the OS keyring and stores the payload in Codex's encrypted
local-secrets file.
This is the third PR in the encrypted-auth stack:
1. #27504 — feature and config selection
2. #27535 — auth-specific local-secrets namespaces
3. This PR — CLI auth implementation and activation
4. MCP OAuth implementation and activation
## What Changed
- Added encrypted CLI-auth storage using the `CliAuth` secrets
namespace.
- Preserved direct keyring storage for platforms/configurations where it
remains selected.
- Selected the backend consistently for login, logout, refresh,
device-code login, auth loading, and login restrictions.
- Threaded resolved bootstrap/full config through CLI, exec, TUI,
app-server account handling, cloud config, and cloud tasks.
- Removed stale `auth.json` fallback data after successful encrypted
saves and removed encrypted, direct-keyring, and fallback data during
logout.
- Added storage and integration coverage for both direct and encrypted
keyring modes.
MCP OAuth persistence is intentionally left to the next PR.
## Validation
- `just test -p codex-login` — 131 passed
- `just test -p codex-cli` — 280 passed
- `just test -p codex-app-server v2::account` — 25 passed
- `just test -p codex-cloud-config service` — 21 passed, 7 skipped
- `just fix -p codex-login`
- `just fix -p codex-cli`
- `just fmt`
## Why
`codex login` previously persisted newly issued OAuth credentials and
only then attempted to revoke the superseded refresh token. The old
credential must be revoked before a replacement browser or device-code
flow starts, and successful login must not perform any post-login
revocation attempt.
## What changed
- Revoke and clear existing stored auth before browser or device-code
CLI login begins.
- Remove superseded-token detection and revocation from the shared token
persistence path; successful login now only saves the new credentials.
- Read the raw configured auth store during CLI cleanup so
environment-provided auth cannot mask the stored refresh token.
- Preserve `auto` storage fallback semantics when keyring deletion fails
by clearing the fallback auth file.
- Add a process-level CLI regression test that requires the revoke
request to precede every device-login request and occur exactly once.
If replacement login is canceled or fails, the previous local
credentials have already been cleared. Remote revocation remains best
effort, matching explicit logout behavior.
## Validation
### Process-level before/after reproduction
I compiled the real `codex` CLI from the pre-fix parent (`14df0e8833`)
and from the PR implementation (`25c002f23b`; the login behavior is
unchanged at the current head), then ran the same device-code flow
against a local HTTP mock OAuth authority.
Each run:
1. Used a fresh temporary `CODEX_HOME` configured with
`cli_auth_credentials_store = "file"`.
2. Seeded that temporary home with managed ChatGPT auth containing
`old-access` and `old-refresh` tokens.
3. Pointed `CODEX_REVOKE_TOKEN_URL_OVERRIDE` at the mock `/oauth/revoke`
endpoint.
4. Ran the compiled CLI as:
```shell
CODEX_HOME=<temporary-home> \
CODEX_REVOKE_TOKEN_URL_OVERRIDE=<mock-issuer>/oauth/revoke \
<compiled-codex> login --device-auth --experimental_issuer <mock-issuer>
```
5. Recorded every request received by the mock authority. The mock
marked `new-access` valid when `/oauth/token` issued it and invalidated
it if `/oauth/revoke` arrived afterward, reproducing the observed
session-invalidating failure mode. After login exited, the harness also
verified the persisted refresh token and probed a protected endpoint
with `new-access`.
| Build | Observed request order | CLI/persistence result | `new-access`
probe |
| --- | --- | --- | --- |
| Pre-fix | `usercode → device token → OAuth token →
revoke(old-refresh)` | Exit `0`; `new-refresh` persisted | `401` |
| PR | `revoke(old-refresh) → usercode → device token → OAuth token` |
Exit `0`; `new-refresh` persisted | `200` |
The PR run therefore issued exactly one revocation request, before any
request that initiated the replacement login, and issued no revocation
after token exchange.
### Regression coverage
`codex-rs/cli/tests/login.rs::device_login_revokes_existing_auth_before_requesting_new_tokens`
runs the real first-party `codex` binary against a `wiremock` OAuth
server with an isolated temporary `CODEX_HOME`. It asserts:
- the exact request sequence is `/oauth/revoke`,
`/api/accounts/deviceauth/usercode`, `/api/accounts/deviceauth/token`,
then `/oauth/token`;
- there is exactly one revoke request and its body contains
`old-refresh` with the `refresh_token` hint;
- the completed login persists `new-refresh`.
Local validation:
- `just test -p codex-login` — 130 passed
- `just test -p codex-cli` — 280 passed, including the new process-level
regression test
- `just bazel-lock-check`
## Why
Codex needs to manage Amazon Bedrock API key credentials through the
existing auth lifecycle instead of introducing a separate auth manager
or provider-specific credential file. Treating Bedrock API key login as
a primary auth mode gives it the same persistence, keyring, reload, and
logout behavior as the existing OpenAI API key and ChatGPT modes.
The credential is valid only for the `amazon-bedrock` model provider.
OpenAI-compatible providers must reject this auth mode rather than
treating the Bedrock key as an OpenAI bearer token.
## What changed
- Added `bedrockApiKey` as an app-server `AuthMode` and
`CodexAuth::BedrockApiKey` as a primary `AuthManager` mode.
- Added `BedrockApiKeyAuth`, containing the API key and AWS region, to
the existing `AuthDotJson` payload stored in `$CODEX_HOME/auth.json` or
the configured keyring backend.
- Added `login_with_bedrock_api_key(...)`, parallel to
`login_with_api_key(...)`, which replaces the current stored login with
Bedrock credentials.
- Reused generic auth reload and logout behavior instead of adding a
Bedrock-specific auth manager or logout path.
- Updated login restrictions, status reporting, diagnostics, telemetry
classification, generated app-server schemas, and auth fixtures for the
new mode.
- Added explicit errors when Bedrock API key auth is selected with an
OpenAI-compatible model provider.
This PR establishes managed storage and auth-mode behavior. Routing the
managed key and region into Amazon Bedrock requests will be in follow-up
PRs.
## Summary
- add v2 personal access token support for `codex login
--with-access-token` and `CODEX_ACCESS_TOKEN`
- classify opaque `at-` tokens separately from legacy Agent Identity
JWTs
- hydrate required ChatGPT account metadata through AuthAPI
`/v1/user-auth-credential/whoami`
- use PATs directly as bearer tokens while preserving existing ChatGPT
account surfaces
- expose PAT-backed auth as the explicit `personalAccessToken`
app-server auth mode
## Implementation
PAT auth is intentionally small and stateless. Loading a PAT performs
one AuthAPI metadata request, stores the hydrated metadata in the
in-memory auth object, and redacts the secret from debug output. Legacy
Agent Identity JWT handling remains unchanged. The shared access-token
classifier lives in a private neutral module because it dispatches
between both credential types.
PAT hydration fails closed when AuthAPI omits any required metadata,
including email. Hydrated metadata is intentionally not persisted:
startup performs a live `whoami` preflight so revoked tokens or changed
account metadata are not accepted from a stale cache.
## Workspace restriction scope
This change intentionally does **not** apply
`forced_chatgpt_workspace_id` to PAT authentication. The setting is a
client-side config guardrail, not an authorization boundary, and PAT
does not currently require workspace-ID parity. The PAT login and
`CODEX_ACCESS_TOKEN` paths therefore validate through AuthAPI without
threading workspace-restriction state through access-token loading.
Existing workspace checks for non-PAT auth remain on their established
paths.
## App-server compatibility
The public app-server `AuthMode` is shared across v1 and v2, and
PAT-backed auth reports `personalAccessToken` through both APIs.
Following human review, this intentionally removes the temporary v1
compatibility mapping that reported PATs as `chatgpt`; the deprecated v1
API is kept in parity with v2 rather than maintaining a separate closed
enum. Clients with exhaustive auth-mode handling in either API version
must add the new case and should generally treat it as ChatGPT-backed
unless they need PAT-specific behavior.
The v1 auth-status response still omits the raw PAT when `includeToken`
is requested because that response cannot carry the account metadata
needed to reuse the credential safely. Persisted PAT auth also omits the
new enum value so older Codex builds can deserialize `auth.json` and
infer PAT auth from the credential field after a rollback.
## Validation
Latest review-fix validation:
- `CARGO_INCREMENTAL=0 just test -p codex-login` (126 passed)
- `CARGO_INCREMENTAL=0 just test -p codex-cli` (263 passed)
- `CARGO_INCREMENTAL=0 just test -p codex-cli
stored_auth_validation_handles_personal_access_token`
- `CARGO_INCREMENTAL=0 just test -p codex-app-server-protocol` (226
passed)
- `CARGO_INCREMENTAL=0 just test -p codex-models-manager
refresh_available_models_uses_remote_only_catalog_for_chatgpt_auth`
- `CARGO_INCREMENTAL=0 just test -p codex-tui
existing_non_oauth_chatgpt_login_counts_as_signed_in`
- `CARGO_INCREMENTAL=0 just fix -p codex-login -p
codex-app-server-protocol -p codex-models-manager -p codex-tui -p
codex-cli`
- `just fmt`
- `git diff --check`
The broader `codex-tui` suite previously compiled and ran 2,834 tests.
Three unrelated environment-sensitive guardian/IDE-socket tests failed
after retries; the PAT-relevant TUI coverage passed.
## Summary
- refresh managed ChatGPT auth during auth resolution when its access
token is inside ChatGPT web's five-minute near-expiry window
- cover refresh-window decisions while preserving the existing
expired-token refresh path
## Why
Codex already resolves managed ChatGPT auth before outbound requests and
refreshes expired access tokens there. This change adjusts the existing
predicate to refresh a still-valid access token once it is within the
same five-minute refresh window used by ChatGPT web, avoiding a request
with a token about to expire.
A cross-process serialization follow-up was explored in #24663 and
closed for now; we do not currently suspect cross-process refresh races
are a root cause of the refresh errors under investigation.
External-token, API-key, and Agent Identity auth modes remain unchanged.
## Validation
- `bazel test //codex-rs/login:login-all-test`
- `just fmt` runs Rust formatting successfully, then its Python SDK Ruff
step cannot install `openai-codex-cli-bin==0.131.0a4` on this Linux
environment because no compatible wheel is published.
## Summary
- classify known refresh-token terminal failures from `/oauth/token` as
permanent even when the backend returns `400`
- preserve the existing relogin-required message for
`refresh_token_reused` instead of retrying and collapsing into a generic
cloud requirements error
- add regression coverage for `400 refresh_token_reused`
## Testing
- `just fmt`
- `cargo test -p codex-login`
## Summary
This change lets `forced_chatgpt_workspace_id` accept multiple workspace
IDs instead of a single value.
It keeps the existing config key name, adds backward-compatible parsing
for a single string in `config.toml`, and normalizes the setting into an
allowed workspace list across login enforcement, app-server config
surfaces, and local ChatGPT auth helpers.
## Why
Workspace-restricted deployments may need to allow more than one ChatGPT
workspace without dropping the guardrail entirely.
## Server-side impact
Codex's local server and app-server protocol needed changes because they
previously assumed a single workspace ID. The local login flow now
matches the auth backend interface by sending the allowed workspace list
as a single comma-separated `allowed_workspace_id` query parameter.
## Validation
This was tested with:
- A single workspace config
- With multi-workspace configs
- With multiple workspaces in the config
- The user only being a part of a subset of them
All were successful.
Automated coverage:
- `cargo test -p codex-login`
- `cargo test -p codex-app-server-protocol`
- `cargo test -p codex-tui local_chatgpt_auth`
- `cargo test --locked -p codex-app-server
login_account_chatgpt_includes_forced_workspace_allowlist_query_param`
## Summary
update the local login success page to match the Codex desktop auth UX
use theme-aware colors and an inline 20px Codex mark
keep the actual localhost success page aligned with the browser auth UX
PR
## Tests
<img width="1728" height="1117" alt="Screenshot 2026-04-29 at 12 00
34 PM"
src="https://github.com/user-attachments/assets/76a40c3f-07c3-452c-97da-e7c43717cd2c"
/>
## Summary
- Keep the preferred ChatGPT login callback port `1455` first.
- Preserve the existing `/cancel` recovery for stale Codex login
servers.
- Fall back to the registered localhost callback port `1457` when `1455`
remains unavailable.
## Why
Cursor and Codex Desktop both use the ChatGPT account login callback
server. On Windows, Cursor can already be listening on `127.0.0.1:1455`
/ `[::1]:1455`, causing Codex Desktop sign-in to fail with:
`Local callback port 1455 is already in use on this machine.`
Codex already attempted to cancel a stale Codex login server on that
port, but if the listener does not release the port, the old behavior
was to fail. The new behavior falls back to `1457`, which matches the
fixed redirect URI being registered server-side in
`openai/openai#863817`. This keeps the OAuth `redirect_uri` inside
Hydra's exact allow-list instead of choosing an arbitrary ephemeral
port.
## Validation
- `just fmt`
- `cargo test -p codex-login`
- `git diff --check HEAD~1..HEAD`
## Summary
Auth loading used to expose synchronous construction helpers in several
places even though some auth sources now need async work. This PR makes
the auth-loading surface async and updates the callers to await it.
This is intentionally only plumbing. It does not change how
AgentIdentity tokens are decoded, how task runtime ids are allocated, or
how JWT signatures are verified.
## Stack
1. **This PR:** [refactor: make auth loading
async](https://github.com/openai/codex/pull/19762)
2. [refactor: load AgentIdentity runtime
eagerly](https://github.com/openai/codex/pull/19763)
3. [feat: verify AgentIdentity JWTs with
JWKS](https://github.com/openai/codex/pull/19764)
## Important call sites
| Area | Change |
| --- | --- |
| `codex-login` auth loading | `CodexAuth` and `AuthManager`
construction paths now await auth loading. |
| app-server startup | Auth manager construction is awaited during
initialization. |
| CLI/TUI/exec/MCP/chatgpt callers | Existing auth-loading calls now
await the same behavior. |
| cloud requirements storage loader | The loader becomes async so it can
share the same auth construction path. |
| auth tests | Tests that load auth now run in async contexts. |
## Testing
Tests: targeted Rust auth test compilation, formatter, scoped Clippy
fix, and Bazel lock check.
## Summary
This PR adds `CodexAuth::AgentIdentity` as an explicit auth mode.
An AgentIdentity auth record is a standalone `auth.json` mode. When
`AuthManager::auth().await` loads that mode, it registers one
process-scoped task and stores it in runtime-only state on the auth
value. Header creation stays synchronous after that because the task is
initialized before callers receive the auth object.
This PR also removes the old feature flag path. AgentIdentity is
selected by explicit auth mode, not by a hidden flag or lazy mutation of
ChatGPT auth records.
Reference old stack: https://github.com/openai/codex/pull/17387/changes
## Design Decisions
- AgentIdentity is a real auth enum variant because it can be the only
credential in `auth.json`.
- The process task is ephemeral runtime state. It is not serialized and
is not stored in rollout/session data.
- Account/user metadata needed by existing Codex backend checks lives on
the AgentIdentity record for now.
- `is_chatgpt_auth()` remains token-specific.
- `uses_codex_backend()` is the broader predicate for ChatGPT-token auth
and AgentIdentity auth.
## Stack
1. https://github.com/openai/codex/pull/18757: full revert
2. https://github.com/openai/codex/pull/18871: isolated Agent Identity
crate
3. This PR: explicit AgentIdentity auth mode and startup task allocation
4. https://github.com/openai/codex/pull/18811: migrate Codex backend
auth callsites through AuthProvider
5. https://github.com/openai/codex/pull/18904: accept AgentIdentity JWTs
and load `CODEX_AGENT_IDENTITY`
## Testing
Tests: targeted Rust checks, cargo-shear, Bazel lock check, and CI.
## Summary
This PR fully reverts the previously merged Agent Identity runtime
integration from the old stack:
https://github.com/openai/codex/pull/17387/changes
It removes the Codex-side task lifecycle wiring, rollout/session
persistence, feature flag plumbing, lazy `auth.json` mutation,
background task auth paths, and request callsite changes introduced by
that stack.
This leaves the repo in a clean pre-AgentIdentity integration state so
the follow-up PRs can reintroduce the pieces in smaller reviewable
layers.
## Stack
1. This PR: full revert
2. https://github.com/openai/codex/pull/18871: move Agent Identity
business logic into a crate
3. https://github.com/openai/codex/pull/18785: add explicit
AgentIdentity auth mode and startup task allocation
4. https://github.com/openai/codex/pull/18811: migrate auth callsites
through AuthProvider
## Testing
Tests: targeted Rust checks, cargo-shear, Bazel lock check, and CI.
## Summary
This changes Codex logout so managed ChatGPT auth is revoked against
AuthAPI before local auth state is removed. CLI logout, TUI `/logout`,
and the app-server account logout path now use the token-revoking logout
flow instead of only deleting `auth.json` / credential store state.
## Root Cause
Logout previously cleared only local auth storage. That removed Codex's
local credentials but did not ask the backend to invalidate the
refresh/access token state associated with a managed ChatGPT login.
## Behavior
For managed ChatGPT auth, logout sends the stored refresh token to
`https://auth.openai.com/oauth/revoke` with `token_type_hint:
refresh_token` and the Codex OAuth client id, then deletes all local
auth stores after revocation succeeds. If only an access token is
available, it falls back to revoking that access token. API key auth and
externally supplied `chatgptAuthTokens` are still only cleared locally
because Codex does not own a refresh token for those modes.
Revocation failures are fail-closed: if Codex cannot load stored auth or
the backend revoke call fails, logout returns an error and leaves local
auth in place so the user can retry instead of silently clearing local
state while backend tokens remain valid.
## Validation
ran local version of `codex-cli` with staging overrides/harness for auth
ran `codex login` then `codex logout`:
saw auth.json clear and backend revocation endpoints were called
```
POST /oauth/revoke
status: 200
revoking access token
should clear auth session
clearing auth session due to token revocation
successfully revoked session and access token
CANONICAL-API-LINE Response: status='200' method='POST' path='/oauth/revoke
```
---------
Co-authored-by: Codex <noreply@openai.com>
## Summary
Stack PR 2 of 4 for feature-gated agent identity support.
This PR adds agent identity registration behind
`features.use_agent_identity`. It keeps the app-server protocol
unchanged and starts registration after ChatGPT auth exists rather than
requiring a client restart.
## Stack
- PR1: https://github.com/openai/codex/pull/17385 - add
`features.use_agent_identity`
- PR2: https://github.com/openai/codex/pull/17386 - this PR
- PR3: https://github.com/openai/codex/pull/17387 - register agent tasks
when enabled
- PR4: https://github.com/openai/codex/pull/17388 - use `AgentAssertion`
downstream when enabled
## Validation
Covered as part of the local stack validation pass:
- `just fmt`
- `cargo test -p codex-core --lib agent_identity`
- `cargo test -p codex-core --lib agent_assertion`
- `cargo test -p codex-core --lib websocket_agent_task`
- `cargo test -p codex-api api_bridge`
- `cargo build -p codex-cli --bin codex`
## Notes
The full local app-server E2E path is still being debugged after PR
creation. The current branch stack is directionally ready for review
while that follow-up continues.
## Summary
- split `models-manager` out of `core` and add `ModelsManagerConfig`
plus `Config::to_models_manager_config()` so model metadata paths stop
depending on `core::Config`
- move login-owned/auth-owned code out of `core` into `codex-login`,
move model provider config into `codex-model-provider-info`, move API
bridge mapping into `codex-api`, move protocol-owned types/impls into
`codex-protocol`, and move response debug helpers into a dedicated
`response-debug-context` crate
- move feedback tag emission into `codex-feedback`, relocate tests to
the crates that now own the code, and keep broad temporary re-exports so
this PR avoids a giant import-only rewrite
## Major moves and decisions
- created `codex-models-manager` as the owner for model
cache/catalog/config/model info logic, including the new
`ModelsManagerConfig` struct
- created `codex-model-provider-info` as the owner for provider config
parsing/defaults and kept temporary `codex-login`/`codex-core`
re-exports for old import paths
- moved `api_bridge` error mapping + `CoreAuthProvider` into
`codex-api`, while `codex-login::api_bridge` temporarily re-exports
those symbols and keeps the `auth_provider_from_auth` wrapper
- moved `auth_env_telemetry` and `provider_auth` ownership to
`codex-login`
- moved `CodexErr` ownership to `codex-protocol::error`, plus
`StreamOutput`, `bytes_to_string_smart`, and network policy helpers to
protocol-owned modules
- created `codex-response-debug-context` for
`extract_response_debug_context`, `telemetry_transport_error_message`,
and related response-debug plumbing instead of leaving that behavior in
`core`
- moved `FeedbackRequestTags`, `emit_feedback_request_tags`, and
`emit_feedback_request_tags_with_auth_env` to `codex-feedback`
- deferred removal of temporary re-exports and the mechanical import
rewrites to a stacked follow-up PR so this PR stays reviewable
## Test moves
- moved auth refresh coverage from `core/tests/suite/auth_refresh.rs` to
`login/tests/suite/auth_refresh.rs`
- moved text encoding coverage from
`core/tests/suite/text_encoding_fix.rs` to
`protocol/src/exec_output_tests.rs`
- moved model info override coverage from
`core/tests/suite/model_info_overrides.rs` to
`models-manager/src/model_info_overrides_tests.rs`
---------
Co-authored-by: Codex <noreply@openai.com>
## Why
`argument-comment-lint` was green in CI even though the repo still had
many uncommented literal arguments. The main gap was target coverage:
the repo wrapper did not force Cargo to inspect test-only call sites, so
examples like the `latest_session_lookup_params(true, ...)` tests in
`codex-rs/tui_app_server/src/lib.rs` never entered the blocking CI path.
This change cleans up the existing backlog, makes the default repo lint
path cover all Cargo targets, and starts rolling that stricter CI
enforcement out on the platform where it is currently validated.
## What changed
- mechanically fixed existing `argument-comment-lint` violations across
the `codex-rs` workspace, including tests, examples, and benches
- updated `tools/argument-comment-lint/run-prebuilt-linter.sh` and
`tools/argument-comment-lint/run.sh` so non-`--fix` runs default to
`--all-targets` unless the caller explicitly narrows the target set
- fixed both wrappers so forwarded cargo arguments after `--` are
preserved with a single separator
- documented the new default behavior in
`tools/argument-comment-lint/README.md`
- updated `rust-ci` so the macOS lint lane keeps the plain wrapper
invocation and therefore enforces `--all-targets`, while Linux and
Windows temporarily pass `-- --lib --bins`
That temporary CI split keeps the stricter all-targets check where it is
already cleaned up, while leaving room to finish the remaining Linux-
and Windows-specific target-gated cleanup before enabling
`--all-targets` on those runners. The Linux and Windows failures on the
intermediate revision were caused by the wrapper forwarding bug, not by
additional lint findings in those lanes.
## Validation
- `bash -n tools/argument-comment-lint/run.sh`
- `bash -n tools/argument-comment-lint/run-prebuilt-linter.sh`
- shell-level wrapper forwarding check for `-- --lib --bins`
- shell-level wrapper forwarding check for `-- --tests`
- `just argument-comment-lint`
- `cargo test` in `tools/argument-comment-lint`
- `cargo test -p codex-terminal-detection`
## Follow-up
- Clean up remaining Linux-only target-gated callsites, then switch the
Linux lint lane back to the plain wrapper invocation.
- Clean up remaining Windows-only target-gated callsites, then switch
the Windows lint lane back to the plain wrapper invocation.
- Move the auth implementation and token data into codex-login.
- Keep codex-core re-exporting that surface from codex-login for
existing callers.
---------
Co-authored-by: Codex <noreply@openai.com>
## Stacked PRs
This work is now effectively split across two steps:
- #14178: add custom CA support for browser and device-code login flows,
docs, and hermetic subprocess tests
- #14239: extend that shared custom CA handling across Codex HTTPS
clients and secure websocket TLS
Note: #14240 was merged into this branch while it was stacked on top of
this PR. This PR now subsumes that websocket follow-up and should be
treated as the combined change.
Builds on top of #14178.
## Problem
Custom CA support landed first in the login path, but the real
requirement is broader. Codex constructs outbound TLS clients in
multiple places, and both HTTPS and secure websocket paths can fail
behind enterprise TLS interception if they do not honor
`CODEX_CA_CERTIFICATE` or `SSL_CERT_FILE` consistently.
This PR broadens the shared custom-CA logic beyond login and applies the
same policy to websocket TLS, so the enterprise-proxy story is no longer
split between “HTTPS works” and “websockets still fail”.
## What This Delivers
Custom CA support is no longer limited to login. Codex outbound HTTPS
clients and secure websocket connections can now honor the same
`CODEX_CA_CERTIFICATE` / `SSL_CERT_FILE` configuration, so enterprise
proxy/intercept setups work more consistently end-to-end.
For users and operators, nothing new needs to be configured beyond the
same CA env vars introduced in #14178. The change is that more of Codex
now respects them, including websocket-backed flows that were previously
still using default trust roots.
I also manually validated the proxy path locally with mitmproxy using:
`CODEX_CA_CERTIFICATE=~/.mitmproxy/mitmproxy-ca-cert.pem
HTTPS_PROXY=http://127.0.0.1:8080 just codex`
with mitmproxy installed via `brew install mitmproxy` and configured as
the macOS system proxy.
## Mental model
`codex-client` is now the owner of shared custom-CA policy for outbound
TLS client construction. Reqwest callers start from the builder
configuration they already need, then pass that builder through
`build_reqwest_client_with_custom_ca(...)`. Websocket callers ask the
same module for a rustls client config when a custom CA bundle is
configured.
The env precedence is the same everywhere:
- `CODEX_CA_CERTIFICATE` wins
- otherwise fall back to `SSL_CERT_FILE`
- otherwise use system roots
The helper is intentionally narrow. It loads every usable certificate
from the configured PEM bundle into the appropriate root store and
returns either a configured transport or a typed error that explains
what went wrong.
## Non-goals
This does not add handshake-level integration tests against a live TLS
endpoint. It does not validate that the configured bundle forms a
meaningful certificate chain. It also does not try to force every
transport in the repo through one abstraction; it extends the shared CA
policy across the reqwest and websocket paths that actually needed it.
## Tradeoffs
The main tradeoff is centralizing CA behavior in `codex-client` while
still leaving adoption up to call sites. That keeps the implementation
additive and reviewable, but it means the rule "outbound Codex TLS that
should honor enterprise roots must use the shared helper" is still
partly enforced socially rather than by types.
For websockets, the shared helper only builds an explicit rustls config
when a custom CA bundle is configured. When no override env var is set,
websocket callers still use their ordinary default connector path.
## Architecture
`codex-client::custom_ca` now owns CA bundle selection, PEM
normalization, mixed-section parsing, certificate extraction, typed
CA-loading errors, and optional rustls client-config construction for
websocket TLS.
The affected consumers now call into that shared helper directly rather
than carrying login-local CA behavior:
- backend-client
- cloud-tasks
- RMCP client paths that use `reqwest`
- TUI voice HTTP paths
- `codex-core` default reqwest client construction
- `codex-api` websocket clients for both responses and realtime
websocket connections
The subprocess CA probe, env-sensitive integration tests, and shared PEM
fixtures also live in `codex-client`, which is now the actual owner of
the behavior they exercise.
## Observability
The shared CA path logs:
- which environment variable selected the bundle
- which path was loaded
- how many certificates were accepted
- when `TRUSTED CERTIFICATE` labels were normalized
- when CRLs were ignored
- where client construction failed
Returned errors remain user-facing and include the relevant env var,
path, and remediation hint. That same error model now applies whether
the failure surfaced while building a reqwest client or websocket TLS
configuration.
## Tests
Pure unit tests in `codex-client` cover env precedence and PEM
normalization behavior. Real client construction remains in subprocess
tests so the suite can control process env and avoid the macOS seatbelt
panic path that motivated the hermetic test split.
The subprocess coverage verifies:
- `CODEX_CA_CERTIFICATE` precedence over `SSL_CERT_FILE`
- fallback to `SSL_CERT_FILE`
- single-cert and multi-cert bundles
- malformed and empty-file errors
- OpenSSL `TRUSTED CERTIFICATE` handling
- CRL tolerance for well-formed CRL sections
The websocket side is covered by the existing `codex-api` / `codex-core`
websocket test suites plus the manual mitmproxy validation above.
---------
Co-authored-by: Ivan Zakharchanka <3axap4eHko@gmail.com>
Co-authored-by: Codex <noreply@openai.com>
## Stacked PRs
This work is split across three stacked PRs:
- #14178: add custom CA support for browser and device-code login flows,
docs, and hermetic subprocess tests
- #14239: broaden the shared custom CA path from login to other outbound
`reqwest` clients across Codex
- #14240: extend that shared custom CA handling to secure websocket TLS
so websocket connections honor the same CA env vars
Review order: #14178, then #14239, then #14240.
Supersedes #6864.
Thanks to @3axap4eHko for the original implementation and investigation
here. Although this version rearranges the code and history
significantly, the majority of the credit for this work belongs to them.
## Problem
Login flows need to work in enterprise environments where outbound TLS
is intercepted by an internal proxy or gateway. In those setups, system
root certificates alone are often insufficient to validate the OAuth and
device-code endpoints used during login. The change adds a
login-specific custom CA loading path, but the important contracts
around env precedence, PEM compatibility, test boundaries, and
probe-only workarounds need to be explicit so reviewers can understand
what behavior is intentional.
For users and operators, the behavior is simple: if login needs to trust
a custom root CA, set `CODEX_CA_CERTIFICATE` to a PEM file containing
one or more certificates. If that variable is unset, login falls back to
`SSL_CERT_FILE`. If neither is set, login uses system roots. Invalid or
empty PEM files now fail with an error that points back to those
environment variables and explains how to recover.
## What This Delivers
Users can now make Codex login work behind enterprise TLS interception
by pointing `CODEX_CA_CERTIFICATE` at a PEM bundle containing the
relevant root certificates. If that variable is unset, login falls back
to `SSL_CERT_FILE`, then to system roots.
This PR applies that behavior to both browser-based and device-code
login flows. It also makes login tolerant of the PEM shapes operators
actually have in hand: multi-certificate bundles, OpenSSL `TRUSTED
CERTIFICATE` labels, and bundles that include well-formed CRLs.
## Mental model
`codex-login` is the place where the login flows construct ad hoc
outbound HTTP clients. That makes it the right boundary for a narrow CA
policy: look for `CODEX_CA_CERTIFICATE`, fall back to `SSL_CERT_FILE`,
load every parseable certificate block in that bundle into a
`reqwest::Client`, and fail early with a clear user-facing error if the
bundle is unreadable or malformed.
The implementation is intentionally pragmatic about PEM input shape. It
accepts ordinary certificate bundles, multi-certificate bundles, OpenSSL
`TRUSTED CERTIFICATE` labels, and bundles that also contain CRLs. It
does not validate a certificate chain or prove a handshake; it only
constructs the root store used by login.
## Non-goals
This change does not introduce a general-purpose transport abstraction
for the rest of the product. It does not validate whether the provided
bundle forms a real chain, and it does not add handshake-level
integration tests against a live TLS server. It also does not change
login state management or OAuth semantics beyond ensuring the existing
flows share the same CA-loading rules.
## Tradeoffs
The main tradeoff is keeping this logic scoped to login-specific client
construction rather than lifting it into a broader shared HTTP layer.
That keeps the review surface smaller, but it also means future
login-adjacent code must continue to use `build_login_http_client()` or
it can silently bypass enterprise CA overrides.
The `TRUSTED CERTIFICATE` handling is also intentionally a local
compatibility shim. The rustls ecosystem does not currently accept that
PEM label upstream, so the code normalizes it locally and trims the
OpenSSL `X509_AUX` trailer bytes down to the certificate DER that
`reqwest` can consume.
## Architecture
`custom_ca.rs` is now the single place that owns login CA behavior. It
selects the CA file from the environment, reads it, normalizes PEM label
shape where needed, iterates mixed PEM sections with `rustls-pki-types`,
ignores CRLs, trims OpenSSL trust metadata when necessary, and returns
either a configured `reqwest::Client` or a typed error.
The browser login server and the device-code flow both call
`build_login_http_client()`, so they share the same trust-store policy.
Environment-sensitive tests run through the `login_ca_probe` helper
binary because those tests must control process-wide env vars and cannot
reliably build a real reqwest client in-process on macOS seatbelt runs.
## Observability
The custom CA path logs which environment variable selected the bundle,
which file path was loaded, how many certificates were accepted, when
`TRUSTED CERTIFICATE` labels were normalized, when CRLs were ignored,
and where client construction failed. Returned errors remain user-facing
and include the relevant path, env var, and remediation hint.
This gives enough signal for three audiences:
- users can see why login failed and which env/file caused it
- sysadmins can confirm which override actually won
- developers can tell whether the failure happened during file read, PEM
parsing, certificate registration, or final reqwest client construction
## Tests
Pure unit tests stay limited to env precedence and empty-value handling.
Real client construction lives in subprocess tests so the suite remains
hermetic with respect to process env and macOS sandbox behavior.
The subprocess tests verify:
- `CODEX_CA_CERTIFICATE` precedence over `SSL_CERT_FILE`
- fallback to `SSL_CERT_FILE`
- single-certificate and multi-certificate bundles
- malformed and empty-bundle errors
- OpenSSL `TRUSTED CERTIFICATE` handling
- CRL tolerance for well-formed CRL sections
The named PEM fixtures under `login/tests/fixtures/` are shared by the
tests so their purpose stays reviewable.
---------
Co-authored-by: Ivan Zakharchanka <3axap4eHko@gmail.com>
Co-authored-by: Codex <noreply@openai.com>
## Problem
Users without Codex access can hit a confusing local login loop. In the
denial case, the callback could fall through to generic behavior
(including a plain "Missing authorization code" page) instead of clearly
explaining that access was denied.
<img width="842" height="464" alt="Screenshot 2026-02-19 at 11 43 45 PM"
src="https://github.com/user-attachments/assets/f7a25e1d-e480-4ac2-b0ff-8bfe31003e66"
/>
<img width="842" height="464" alt="Screenshot 2026-02-19 at 11 44 53 PM"
src="https://github.com/user-attachments/assets/8a4fe6e4-b27b-483c-9f0c-60164933221d"
/>
## Scope
This PR improves local login error clarity only. It does not change
entitlement policy, RBAC rules, or who is allowed to use Codex.
## What Changed
- The local OAuth callback handler now parses `error` and
`error_description` on `/auth/callback` and exits the callback loop with
a real failure.
- Callback failures render a branded local Codex error page instead of a
generic/plain page.
- `access_denied` + `missing_codex_entitlement` is now mapped to an
explicit user-facing message telling the user Codex is not enabled for
their workspace and to contact their workspace administrator for access.
- Unknown OAuth callback errors continue to use a generic error page
while preserving the OAuth error code/details for debugging.
- Added the login error page template to Bazel assets so the local
binary can render it in Bazel builds.
## Non-goals
- No TUI onboarding/toast changes in this PR.
- No backend entitlement or policy changes.
## Tests
- Added an end-to-end `codex-login` test for `access_denied` +
`missing_codex_entitlement` and verified the page shows the actionable
admin guidance.
- Added an end-to-end `codex-login` test for a generic `access_denied`
reason to verify we keep a generic fallback page/message.
This PR is a follow-up to #5591. It allows users to choose which auth
storage mode they want by using the new
`cli_auth_credentials_store_mode` config.
This PR introduces a new `Auth Storage` abstraction layer that takes
care of read, write, and load of auth tokens based on the
AuthCredentialsStoreMode. It is similar to how we handle MCP client
oauth
[here](https://github.com/openai/codex/blob/main/codex-rs/rmcp-client/src/oauth.rs).
Instead of reading and writing directly from disk for auth tokens, Codex
CLI workflows now should instead use this auth storage using the public
helper functions.
This PR is just a refactor of the current code so the behavior stays the
same. We will add support for keyring and hybrid mode in follow-up PRs.
I have read the CLA Document and I hereby sign the CLA
# External (non-OpenAI) Pull Request Requirements
Before opening this Pull Request, please read the dedicated
"Contributing" markdown file or your PR may be closed:
https://github.com/openai/codex/blob/main/docs/contributing.md
If your PR conforms to our contribution guidelines, replace this text
with a detailed and high quality description of your changes.
# External (non-OpenAI) Pull Request Requirements
Before opening this Pull Request, please read the dedicated
"Contributing" markdown file or your PR may be closed:
https://github.com/openai/codex/blob/main/docs/contributing.md
If your PR conforms to our contribution guidelines, replace this text
with a detailed and high quality description of your changes.
# test
```
codex-rs % export CODEX_DEVICE_AUTH_BASE_URL=http://localhost:3007
codex-rs % cargo run --bin codex login --experimental_use-device-code
Compiling codex-login v0.0.0 (/Users/rakesh/code/codex/codex-rs/login)
Compiling codex-mcp-server v0.0.0 (/Users/rakesh/code/codex/codex-rs/mcp-server)
Compiling codex-tui v0.0.0 (/Users/rakesh/code/codex/codex-rs/tui)
Compiling codex-cli v0.0.0 (/Users/rakesh/code/codex/codex-rs/cli)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.90s
Running `target/debug/codex login --experimental_use-device-code`
To authenticate, enter this code when prompted: 6Q27-KBVRF with interval 5
^C
```
The error in the last line is since the poll endpoint is not yet
implemented
This changes the reqwest client used in tests to be sandbox-friendly,
and skips a bunch of other tests that don't work inside the
sandbox/without network.
When logging in using ChatGPT using the `codex login` command, a
successful login should write a new `auth.json` file with the ChatGPT
token information. The old code attempted to retain the API key and
merge the token information into the existing `auth.json` file. With the
new simplified login mechanism, `auth.json` should have auth information
for only ChatGPT or API Key, not both.
The `codex login --api-key <key>` code path was already doing the right
thing here, but the `codex login` command was incorrect. This PR fixes
the problem and adds test cases for both commands.
The previous config approach had a few issues:
1. It is part of the config but not designed to be used externally
2. It had to be wired through many places (look at the +/- on this PR
3. It wasn't guaranteed to be set consistently everywhere because we
don't have a super well defined way that configs stack. For example, the
extension would configure during newConversation but anything that
happened outside of that (like login) wouldn't get it.
This env var approach is cleaner and also creates one less thing we have
to deal with when coming up with a better holistic story around configs.
One downside is that I removed the unit test testing for the override
because I don't want to deal with setting the global env or spawning
child processes and figuring out how to introspect their originator
header. The new code is sufficiently simple and I tested it e2e that I
feel as if this is still worth it.
This PR addresses an issue that several users have reported. If the
local oauth login server in one codex instance is left running (e.g. the
user abandons the oauth flow), a subsequent codex instance will receive
an error when attempting to log in because the localhost port is already
in use by the dangling web server from the first instance.
This PR adds a cancelation mechanism that the second instance can use to
abort the first login attempt and free up the port.
this dramatically improves time to run `cargo test -p codex-core` (~25x
speedup).
before:
```
cargo test -p codex-core 35.96s user 68.63s system 19% cpu 8:49.80 total
```
after:
```
cargo test -p codex-core 5.51s user 8.16s system 63% cpu 21.407 total
```
both tests measured "hot", i.e. on a 2nd run with no filesystem changes,
to exclude compile times.
approach inspired by [Delete Cargo Integration
Tests](https://matklad.github.io/2021/02/27/delete-cargo-integration-tests.html),
we move all test cases in tests/ into a single suite in order to have a
single binary, as there is significant overhead for each test binary
executed, and because test execution is only parallelized with a single
binary.
https://github.com/openai/codex/pull/2373 introduced
`ServerOptions.login_timeout` and `spawn_timeout_watcher()` to use an
extra thread to manage the timeout for the login server. Now that we
have asyncified the login stack, we can use `tokio::time::timeout()`
from "outside" the login library to manage the timeout rather than
having to a commit to a specific "timeout" concept from within.
---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/2395).
* #2399
* #2398
* #2396
* __->__ #2395
* #2394
* #2393
* #2389
This PR adds two new APIs for the MCP server: 1) loginChatGpt, and 2)
cancelLoginChatGpt. The first starts a login server and returns a local
URL that allows for browser-based authentication, and the second
provides a way to cancel the login attempt. If the login attempt
succeeds, a notification (in the form of an event) is sent to a
subscriber.
I also added a timeout mechanism for the existing login server. The
loginChatGpt code path uses a 10-minute timeout by default, so if the
user fails to complete the login flow in that timeframe, the login
server automatically shuts down. I tested the timeout code by manually
setting the timeout to a much lower number and confirming that it works
as expected when used e2e.
This PR:
* Added the clippy.toml to configure allowable expect / unwrap usage in
tests
* Removed as many expect/allow lines as possible from tests
* moved a bunch of allows to expects where possible
Note: in integration tests, non `#[test]` helper functions are not
covered by this so we had to leave a few lingering `expect(expect_used`
checks around