## Why
Token refresh needs the issuer and client ID used at login. Unverified token claims cannot establish that authority, and OAuth error bodies may echo credentials.
## What changed
- Save `oauth_provenance` and a new `login_owner_id` on browser and device-code login. Preserve the login identifier through token rotation.
- Refresh using the recorded issuer and client ID, retaining the existing endpoint override and fallback behavior for legacy records. Clear provenance when an override changes the endpoint or when a response without token rotation encounters a concurrently replaced refresh token.
- Keep both metadata fields optional for older records without inferring provenance from token claims.
- Remove backend token error details from structured logs and refresh error messages.
- Box large futures in token refresh, MCP initialization, and embedded app-server test startup to reduce surrounding async frame sizes.
## Testing
Add coverage for legacy record compatibility and metadata round trips, browser and device-code login persistence, refresh using recorded OAuth settings, metadata preservation through rotation, and provenance clearing for a superseded refresh token.
GitOrigin-RevId: b6b5d2fcbe2a284a171a242c1a17128af55f1024
## What changed
Add `features.app_server_websocket_compression`, disabled by default. TCP WebSocket and Unix-domain socket listeners now negotiate zstd compression only when the feature is enabled and the client offers the supported protocol. Otherwise, connections use plain text messages.
Include the feature in the configuration schema and allow its override during TUI daemon startup.
## Testing
Extend transport tests to cover enabled and disabled compression with supported, unsupported, and absent client offers on both transports. Add an app-server integration test that verifies startup configuration controls negotiation and the message format used for initialization responses.
GitOrigin-RevId: 6001d65f66f9c48f099b85380c2cad8ffee04de7
## What changed
- Negotiate compression over TCP and local sockets with `x-codex-app-server-compression: zstd-stream-v1`. Keep JSON text messages when compression is not negotiated.
- Send compressed JSON as binary messages, retaining a separate zstd stream per direction across messages. Export reusable negotiation and codec helpers.
- Limit decompressed messages to 64 MiB and compression windows to 1 MiB. Close compressed connections on decoding errors or unexpected text messages.
- Add audit and deny ignores for the unmaintained `zvariant`, `zbus_names`, and `zvariant_derive` versions pulled in through `zbus`.
## Testing
Add codec tests for retained history, fallback, size limits, malformed data, and oversized compression windows. Add TCP and local-socket round-trip tests covering negotiation, persistent streams, and ping/pong handling, plus TCP fallback for absent or unsupported offers.
GitOrigin-RevId: 50d3861e8fe4a9c523599a5ab773bec7cb883bf2
## What changed
Wait for the rejected socket to close before advancing Tokio's clock through
capped retry delays. Keep socket I/O on real time so pending handshakes cannot
auto-advance their own timeouts.
Return bodyless HTTP 409 responses in the test server, check that neither
rejection restarts fast retries, and assert that recovery preserves the expected
reconnect attempt count. Both cursor and cursorless cases use the updated helper.
GitOrigin-RevId: da167312f28b7e24b864e6929be749865ea70081
## Why
Explicitly selecting the default daemon socket previously classified the TUI connection as remote, changing how local configuration and thread parameters were handled. A socket at that path may also be an SSH forward, so the path alone cannot establish that the daemon is local.
## What changed
- Verify the default daemon endpoint before classifying an explicit connection as local. On Unix, check the expected symlink target and the private socket directory's ownership and permissions, then connect to the protected endpoint directly.
- Preserve local workspace behavior and local thread parameters for verified attachments while keeping directly bound Unix sockets classified as remote.
- Report a missing explicitly requested socket, disable embedded fallback for explicit attachments, and restrict daemon auto-start to implicit connections.
## Testing
Extend tests to cover explicit and implicit local target selection, verified socket resolution, and rejection of a directly bound socket as a local daemon endpoint.
GitOrigin-RevId: ac2226ca1c5f60b26aad9571f945e3325d3bebfa
## Why
The updater bound its socket directly beneath `CODEX_HOME`, which can exceed Unix socket path limits. Bootstrap could also report success before the updater's manual-update socket was ready.
## What changed
- Reuse the control socket's protected binding and cleanup guard for the updater, with a symlink at the advertised socket path.
- Remove stale updater socket entries before startup while preserving regular files.
- Wait up to 30 seconds for supported automatic updaters to publish their socket, without connecting as a readiness probe. Report updater exit or timeout with stderr log context.
- Preserve compatibility with older updaters that lack the manual-update command.
## Testing
Add Unix integration tests for updater readiness with a long `CODEX_HOME` path and bootstrap failure when a regular file blocks the updater socket, verifying that the file is preserved and the daemon remains running.
GitOrigin-RevId: 57fdcbe4e6907ece68ba66178cf81f258527560c
## Why
An interrupt or turn replacement can invalidate a pending-work wakeup while its turn context is being constructed. Task startup could then recreate a turn whose reservation was no longer active.
## What changed
- Recheck that the wakeup still owns the active turn reservation immediately before calling `start_task`, and return if it does not.
- Allow the remote-control re-enrollment test up to 45 seconds for the connection following reconnect backoff.
## Testing
Add a regression test that pauses wakeup context construction, processes an interrupt, and then resumes construction. It verifies that no additional turn starts and only the initial model request is sent.
GitOrigin-RevId: c857fdd9f0fac3790d35a95a1f21a6d41977bb29
## Why
Unix control-socket shutdown removes the socket and rendezvous link but leaves the startup lock file behind.
## What changed
Keep a removable startup lock with the control-socket guard and release the lock after publishing the rendezvous link. On cleanup, reacquire the lock and verify that the path still identifies the same file before removing it.
Use nonblocking acquisition with asynchronous retries, checking file identity after acquisition to handle lock-file replacement.
## Testing
Add tests for nonblocking lock contention and dangling symlink rejection, and extend the control-socket lifecycle test to verify that the lock file is removed on shutdown.
GitOrigin-RevId: 469ed5269f32ae1c0655d40d28519ef5e94ba6be
## Why
Closed WebSocket connections previously triggered immediate reconnects, and successful handshakes reset backoff even when connections were short-lived.
## What changed
- Delay automatic reconnects before token refresh or enrollment, and report `Connecting` while waiting.
- Use exponential backoff with jitter between half and all of the delay, starting at 5 seconds and capped at 30 seconds.
- Retain backoff across short-lived connections and reset it after a connection lasts at least 60 seconds.
- Interrupt the reconnect delay on shutdown, auth owner invalidation, or enable/disable changes, including an immediate disable/enable pair.
## Testing
Add connection tests for delayed token refresh, backoff retention and reset, explicit re-enabling, and prompt shutdown. Update retry tests to check jitter bounds, saturation during sustained failures, and backoff retention after a successful handshake.
GitOrigin-RevId: 0b9055ff64ff7d96f2d974b73ced8d4ffd02ac9e
## Why
Reaching the reconnect backoff cap reset the attempt counter, restarting fast retries during sustained failures such as HTTP 409 responses indicating that the remote app server is already online.
## What changed
Keep the reconnect delay at the 30-second cap and stop advancing the backoff exponent once capped. Successful connections and auth changes still reset backoff. Remove logging for the obsolete reset-at-cap behavior.
## Testing
Add regression tests for long failure streaks and repeated HTTP 409 handshakes, with and without a subscribe cursor. Verify capped retry intervals, recovery after a successful handshake, backoff reset on success, and preservation of enrollment and cursor state.
GitOrigin-RevId: 56e3f10ec5ceeced5a71397a99ca22b75dd5ef52
## Why
An advertised control socket path can exceed the Unix socket path limit even when its symlink target is short enough to connect to.
## What changed
On Unix, resolve the socket path and retry the connection when the initial attempt returns `InvalidInput`.
## Testing
Add a regression test that creates control sockets under two long home paths, checks that their symlink targets are distinct, and verifies that clients can connect through both advertised paths.
GitOrigin-RevId: 2541f0a9b02d85035fa89fe26bbcd36d5cd0cc1e
## Why
Startup prewarm waited for tool discovery before opening its WebSocket connection. Slow MCP startup therefore delayed connection setup even though the handshake could proceed independently.
## What changed
- Run the WebSocket handshake concurrently with tool capture, then send `generate=false` with the discovered tools and finalized request metadata.
- Include model and effective service-tier routing hints in preconnect, including the inherited tier for spawned subagents. Omit routing hints for guardian reviewer requests.
- Recover authentication failures during preconnect and switch to HTTP fallback on `426 Upgrade Required`.
## Testing
Add and extend regression coverage for handshake overlap with delayed MCP discovery, cancellation before warmup, finalized Responses Lite tool metadata, socket reuse and reconnection, service-tier changes and inheritance, and authentication recovery during preconnect.
GitOrigin-RevId: ecb53540765df8227e55e79910a13a05260a4c8d
## What changed
Move WebSocket authentication arguments, settings, credential loading, and upgrade authorization into a shared crate using `http` types. Update the app server, transport, and CLI to consume it directly, and remove the `AppServer` prefix from the shared types.
Hide token digests and JWT verification secrets from authentication policy `Debug` output.
## Testing
Move the existing argument validation, capability-token authorization, and signed bearer-token verification tests into the new crate.
GitOrigin-RevId: 1dd20043a3f8efbca55af7eda338b62a62e8fd68
## What changed
- Route remote-control enrollment, pairing, client management, and WebSocket connections through the authenticated HTTP client factory. Keep policy denials distinct from authentication failures and wait for policy changes before reconnecting.
- Preserve policy denials through exec-server connection failures so later connection attempts and capability discovery can recover. Keep direct executors running through temporary policy outages while rejecting permanent policy failures.
- Build cached HTTP transports on blocking workers, serialize construction, and retain successful builds after callers time out so request deadlines cover queued construction.
## Testing
Add regression coverage for policy recovery during remote startup, direct registration, accepted WebSocket reconnection, and capability discovery; queued transport construction timeouts; and remote-control WebSocket fallback with invalid custom CA files.
GitOrigin-RevId: c81db18e83de957175bb213d165281ac701089dd
## Why
Clients need to know the incoming size limit for single-frame requests so they can reject oversized requests before the socket closes.
## What changed
Add `x-codex-websocket-max-unfragmented-message-bytes` to successful control socket WebSocket handshake responses. Derive the advertised byte limit from the minimum configured frame and message size limits, using the same `WebSocketConfig` for the connection.
## Testing
Extend the control socket upgrade test to verify that the header contains a numeric value matching the effective default limit.
GitOrigin-RevId: c4f46d12e00e20f56d4c14a3e84f9fe6e2f020fa
## Why
Network access and Unix-socket allowlists must not let commands with filesystem restrictions reach the privileged app-server RPC transport.
## What changed
- Bind Unix control sockets in a fixed, user-owned directory with mode `0700`, independent of environment settings, and expose the advertised paths as symlinks. Preserve existing parent permissions, reject unsafe parents, and serialize socket setup and publication.
- Mask the socket directory in Linux bubblewrap sandboxes after each bind that exposes it. Reject host mount aliases and nested mounts that compromise isolation.
- Deny access to the directory and outbound connections to its sockets in macOS Seatbelt policies, including when network access or Unix-socket allowlists grant broader access.
- Require bubblewrap for filesystem-restricted Linux execution. Users with `features.use_legacy_landlock` enabled must disable it for these policies.
## Testing
Add regression coverage for direct and symlink socket access, hardlink attempts, Linux host-process links and bind-mount aliases, and continued use of unrelated and sandbox-local sockets. Add transport coverage for parent permissions, concurrent restart after a stale symlink, and cleanup that preserves a replacement at the advertised path.
GitOrigin-RevId: 53372c27eea278d964f2cf68aed24323ef3b7082
## Why
Daemon recovery reloads threads but leaves interrupted work unfinished. Resume eligible work automatically from the saved conversation, even without a connected client.
## What changed
- Attempt one new continuation turn immediately after restoration, including in Plan mode. Mark the old turn interrupted and supply recovery context without creating a user message or granting new authorization.
- Require an idle thread, matching permissions, and the same single local environment configured by the thread. Skip completed, aborted, or superseded work and snapshots without environment identity.
- Add `continue_turn_if_idle` with an atomic previous-turn check so newer tasks or standalone settings changes invalidate pending continuation.
- Preserve the output schema, service tier, and root turn ID, and emit a “Resuming interrupted work” warning when continuation starts.
## Testing
Add daemon restart coverage for continuation without a client, Plan mode, permission and environment mismatches, and legacy snapshots. Add core coverage for preserved continuation metadata, absence of user-message events, rejection of superseded continuations, and exclusion of remote execution from recovery snapshots.
GitOrigin-RevId: 2b290b75d9399fc58258bcda2e85c74e9b5b3b09
## Why
Managed daemon recovery snapshots previously saved only loaded thread IDs, without identifying active turns or preserving their turn-specific options.
## What changed
- Capture regular, uncanceled turns after their input is recorded, saving the turn ID, output schema, service tier, and cyber access program alongside persisted thread IDs.
- Store interruption metadata atomically in the existing candidate array format so older servers can still read thread candidates.
- Begin snapshotting once admitted operations drain, while turns may still be running. Run snapshot collection and thread listener attachment independently of the event loop to keep forced shutdown responsive.
## Testing
Add coverage for running, completed, canceled, and compacting turns; recovery readiness for automatic and user turns; admitted resumes during shutdown; forced shutdown with a blocked rollout writer and child listener attachment; and legacy candidate-array compatibility.
GitOrigin-RevId: ed46342c3a5c71b09c48fa9acece2f15ae748e2f
## Why
Stdio shutdown can hang on open pipes or blocked I/O, and Unix `SIGTERM` needs to run connection cleanup so owned commands terminate and session-end hooks execute.
## What changed
- Handle `SIGTERM` on a dedicated signal thread and close the stdio connection through the existing cleanup path.
- Move blocking stdin and stdout operations to dedicated threads so they do not hold the Tokio runtime open. Cancel message forwarding on `SIGTERM` to release blocked producers.
- When shutdown signal handling is enabled on Unix, start a shared 45-second watchdog on EOF or `SIGTERM`. Preserve the first deadline and exit with status `1` if cleanup or runtime teardown stalls, even when logging is blocked.
## Testing
Add Unix integration coverage for shutdown with open pipes, blocked stdout, blocked stderr, and a stalled filesystem write. Verify command and child-process termination, preserve the EOF deadline when `SIGTERM` follows, and extend session-end hook coverage to `SIGTERM`.
GitOrigin-RevId: 43a80b8277eb50519cdc293eb3b5dc2cad00b3aa
## Why
Remote-control connections and queued operations must not carry over to a different signed-in user or account. Token refreshes for the same identity should preserve the live relay connection.
## What changed
- Scope relay state and authentication recovery to a login lifetime. Retire the session on logout or identity changes and leave remote control disabled until enabled again.
- Reject stale incoming messages and queued RPCs, and cancel pending remote-control operations when their authentication owner changes.
- Start replacement sessions with fresh client, replay, and enrollment state, and prevent retired sessions from publishing status into their replacements.
- Serialize enrollment and preference persistence across sessions, retaining write permits through caller cancellation and draining admitted writes on shutdown.
## Testing
Add regression coverage for same-owner refreshes, user and account changes, stale pairing and client-revocation work, unauthorized enrollment recovery after logging in again, and persistence during cancellation.
GitOrigin-RevId: 5e6ecd23588aab1e2e6168a21f575fb24bbb341c
## Why
Remote-control requests could bypass a server's `Retry-After` deadline through pairing, auth changes, or reconnects. Proactive token refresh could also continue using a valid token despite an explicit server delay.
## What changed
- Share retry deadlines across enrollment, token refresh, pairing, pairing status, and WebSocket handshakes, preserving them across enrollment replacement and disable/enable transitions.
- Honor `Retry-After` on HTTP `429` and `503` pairing and handshake responses, including incomplete pairing response bodies.
- Add up to 30 seconds of jitter once per response without shortening the server's deadline, including for `Retry-After: 0`.
- Defer new requests while retaining valid tokens, and prevent auth changes from shortening the server delay while keeping shutdown responsive.
## Testing
Add regression coverage for shared deadlines, concurrent pairing and handshake failures, auth recovery, disable/enable transitions, retries after expiry, prompt shutdown, and jitter bounds.
GitOrigin-RevId: 69a5a46559031709a4558190c3142eb65e2ae634
## What changed
- Atomically save successfully persisted, loaded root thread IDs to `app-server-daemon/loaded-threads.json` during graceful managed shutdown, excluding ephemeral threads and threads pending unload.
- Clear stale recovery state at startup and around daemon lifecycle operations, including explicit stops and fresh starts.
- Probe support for `--managed-daemon` before launching managed app servers, and clear recovery state best-effort when launching older binaries.
- Keep forced shutdown responsive while saving the snapshot and log snapshot failures without preventing shutdown.
## Testing
Add coverage for snapshots after active turns finish, forced shutdown without a snapshot, excluded threads, snapshot write failures, and recovery-state cleanup for stops, fresh starts, and legacy launches.
GitOrigin-RevId: 422dc32b85355510f7e4c57b7540597b47d9f0b2
## What changed
Route managed Windows app-server shutdown through `/daemon/shutdown` on the local control socket. Require the server's PID and acknowledge it before triggering shutdown through the existing drain logic. Enable the endpoint only for managed Windows launches via `CODEX_DAEMON_SHUTDOWN_SOCKET`.
Watch running-turn changes whenever shutdown is requested so socket-triggered shutdown can finish draining. Keep the updater's file-based shutdown mechanism and retain forced termination as the manager's timeout fallback.
## Testing
Add a transport test covering rejection of unmanaged shutdown requests and mismatched PIDs, plus acknowledgment before emitting the shutdown event.
GitOrigin-RevId: 3e95dc1914f61e8a131f9b6fd56b6c63688d1744
## Why
A pending remote control enrollment could prevent the app server from exiting
after stdio EOF, leaving resources such as thread writers held by the process.
## What changed
- Give remote control its own child shutdown token and cancel it before draining
RPCs when the stdio connection closes.
- Interrupt enrollment requests that require network access after shutdown has
begun, while still allowing in-memory or persisted enrollments to be enabled
and durably saved.
## Testing
- Cover stdio shutdown during a blocked enrollment and verify that another app
server can acquire the released thread writer.
- Cover durable enablement after shutdown with in-memory, persisted, and missing
enrollments.
GitOrigin-RevId: ef9ab49672f273a4cf8257188454a154c03088bd
## Why
Detached Windows processes cannot receive console signals, so app-server daemons
need another way to enter the existing graceful and forced shutdown flow.
## What changed
- Watch the path in `CODEX_DAEMON_SHUTDOWN_FILE` for a request addressed to the
current process, and consume only a matching PID.
- Race daemon shutdown requests with Ctrl-C on Windows while leaving unmanaged
launches on the existing console-signal path.
- Exercise the SIGTERM-equivalent graceful and forced WebSocket shutdown cases
on Windows.
## Testing
Added unit coverage ensuring inherited control paths and malformed requests
cannot consume another process's shutdown request.
GitOrigin-RevId: 426a3c12edc344802395d4d26288f2bb015c484f
## Why
Windows control sockets need a rendezvous directory that cannot be accessed or
replaced by another user while the listener is active.
## What changed
- Create socket directories with a protected, inheritable, current-user-only
DACL, and reject existing directories with broader permissions.
- Validate socket paths without following a junction at the rendezvous
directory, and pin the directory through listener cleanup to prevent
replacement after validation.
- Add a Windows peer check that verifies the kernel-reported peer process uses
the current user's non-elevated token.
- Document the Windows requirements for custom control socket paths.
## Testing
Add Windows coverage for ACL inheritance and rejection, junction and volume-root
rejection, extended-length paths, directory pinning, and elevated peer rejection.
GitOrigin-RevId: cc8dd76e5732c288f6346cfd6138051188533bfa
## What changed
- Add the experimental `amazonBedrockAccessKeys` app-server login flow, persist its credentials in the configured auth store, and use them for SigV4-signed Bedrock requests.
- Report `bedrockAccessKeys` as a distinct auth mode and document access-key login alongside AWS profile and environment credential setup.
- Clear the selected Bedrock provider, AWS settings, and Bedrock-specific model on logout while leaving external AWS credentials and `.env` untouched.
- Reject managed access-key login when higher-precedence configuration would prevent its region or profile settings from taking effect.
## Testing
- Cover access-key login, storage, request signing, configuration conflicts, status reporting, and logout cleanup.
GitOrigin-RevId: bbf297887c6503d3651ceb2aad85c4d626e32723
## What changed
- On macOS, inspect the hardware profile before opening a remote-control WebSocket and send `x-codex-host-device-kind: mac_mini` when the machine name is exactly `Mac mini`.
- Cache successful detection results, bound the profile lookup to two seconds, and omit the header on other platforms or when detection fails.
## Testing
- Add parser coverage for Mac mini, other machine names, empty profiles, and malformed profile data.
GitOrigin-RevId: d2a4589bec0c879957f14ab4a195fd54747cc122
## What changed
- Keep `ClientResponsePayload` typed through the app-server outgoing queue and serialize it at the transport boundary, avoiding an intermediate `serde_json::Value` while preserving the JSON-RPC wire shape.
- Return a JSON-RPC internal error when a response cannot be serialized, including over remote control, and skip unserializable analytics responses.
- Scan reverse JSONL input a chunk at a time with a larger buffer instead of processing each byte individually.
## Testing
- Cover typed response serialization and serialization failures for direct and remote-control transports.
- Cover filtering unserializable responses from analytics.
GitOrigin-RevId: cc4fd7a43461a273315bb95261bd1ae3ff65fbf7
## What changed
- Replace the `isPinned` thread metadata and filters with an optional persisted
`section` and `sectionId`.
- Add the paginated `threadSection/list` app-server method so clients can
discover sections even when they contain no threads.
- Seed a stable `Pinned` section, validate section assignments, and support
filtering for a specific section or for unsectioned threads.
## Testing
- Cover section protocol serialization, listing and pagination, metadata
updates, filtering, persistence, migration compatibility, and operation
without SQLite state.
GitOrigin-RevId: 7972b5471d29317b9387bfd90aa9f573f691ad4c
## Why
Codex and SQLite data can use separate home directories, but state consumers
could reconstruct database paths from the Codex home instead of consistently
using the resolved SQLite configuration.
## What changed
- Pass `SqliteConfig` through the core, rollout, state runtime, and thread store
instead of passing a directory and rebuilding the configuration downstream.
- Use that shared configuration for state, logs, memories, goals, and paginated
thread-history database access, including integrity checks and cleanup.
- Reject state database handles whose SQLite configuration does not match the
requesting store.
## Testing
Add coverage with separate Codex and SQLite homes that verifies startup
backfill, thread listing, and paginated history all use the configured SQLite
directory.
GitOrigin-RevId: 1de1cdd1d6ff1d70bbb6c360c8352e6543fb8ebf
## What changed
- Decode `JSONRPCRequest` into `ClientRequest` without serializing the entire request back into an intermediate JSON value.
- Define every client method's wire name explicitly and use it directly for request, response, and tracing method lookups.
- Serialize outgoing transport messages directly to JSON strings instead of first building a `serde_json::Value`.
## Testing
- Add regression coverage that compares direct request conversion with Serde decoding, including omitted and null parameters, legacy methods, and unknown methods.
- Verify that direct outgoing-message serialization preserves the notification wire shape.
GitOrigin-RevId: 304e14bcaea24e32743bf852a60a208e2f273db3
## Why
Auth managers should use the application's resolved HTTP client factory instead
of silently falling back to the transport's default proxy behavior.
## What changed
- Make `AuthRouteConfig` required when constructing an `AuthManager` or
`AuthConfig`.
- Pass each production caller's resolved routing configuration through without
wrapping it in an optional value.
- Add a test helper that explicitly selects the transport-default proxy policy
for callers that do not exercise custom routing.
GitOrigin-RevId: d89a3b1f8b5d4007650cdac0aae241c94d598580
## Why
`codex-http-client` is the intended owner of direct `reqwest` usage.
## What changed
- Build Codex's default and route-aware clients with `HttpClientBuilder`, and return `HttpClient` directly to API, model discovery, auth, remote control, skills, and extension callers.
- Preserve custom CA, proxy, Cloudflare cookie, sandbox, and request-logging policies while removing `codex-login`'s direct `reqwest` dependency.
- Use shared HTTP types for auth headers, status codes, transport errors, and login-server tests.
## Testing
- Add a Linux integration test verifying that a regular Responses API turn traverses the configured system proxy.
GitOrigin-RevId: 714695fb80762d2083e2f7a6414183e2b3b63854
## What changed
- Add an optional `emittedAtMs` field to server notification envelopes and expose the envelope in the generated JSON and TypeScript schemas.
- Populate the Unix timestamp when app-server emits a notification, before transport routing, so every connection in a fan-out receives the same value.
- Keep the field optional for clients decoding notifications from older app-server versions.
## Testing
- Verify notification serialization includes `emittedAtMs`.
- Verify targeted fan-out reuses one timestamp across connections.
GitOrigin-RevId: b379cb11e8e91c558bcd3d46ce459311a6e81e54
## Why
Remote-control websocket reconnects and pairing requests proactively
refresh their server token. When `/server/refresh` returns a transient
error such as `502`, the still-valid token was discarded as a usable
connection path, causing reconnect failures and repeated refresh
attempts that could amplify an upstream incident.
## What Changed
- Start proactive refresh five minutes before token expiry and
distinguish it from a required refresh for missing or expired tokens.
- Continue websocket and pairing operations with the existing valid
token after `429`, `5xx`, or timeout failures.
- Share an in-memory `next_refresh_at` throttle across websocket and
pairing callers, honoring both `Retry-After` formats and otherwise using
a jittered 24–36 second delay.
- Keep required refreshes strict, preserve `404` enrollment replacement,
and clear token/throttle state for `401` and `403` auth recovery.
- Preserve refresh response metadata internally and add focused
wire-level and integration coverage.
## Verification
Added behavioral coverage proving that:
- a valid near-expiry token still completes websocket and pairing
requests after transient refresh failures;
- `Retry-After` suppresses a subsequent refresh across websocket and
pairing callers;
- request and response-body timeouts are classified as transient;
- an expired token, including one that expires during refresh, cannot
proceed to websocket connection;
- auth failures clear the attempted token without overwriting a
concurrently rotated token.
## Why
Remote-control HTTP requests applied the authentication headers and then
appended `ChatGPT-Account-ID` again with
`reqwest::RequestBuilder::header`. Since reqwest appends, the wire
request could contain the same header twice. Intermediaries may coalesce
duplicate values into `uuid,uuid`, which is not a valid account ID.
## What changed
- Build remote-control request authentication headers in one place.
- Apply provider headers first, then use `HeaderMap::insert` for the
explicit account ID. This preserves the current account-ID precedence
and all other authentication headers while ensuring exactly one account
header is sent.
- Preserve duplicate HTTP headers in the test harness and assert exactly
one account header for enroll, refresh, list, and revoke requests.
## Validation
Added focused coverage for:
- Adding the explicit account header when the auth provider omits it.
- Replacing multiple provider-supplied account values, including a
differently cased header name.
- Preserving authorization and routing headers while replacing only the
account header.
- Rejecting invalid account header values before sending a request.
- Emitting exactly one account header for enroll, refresh, list, and
revoke requests.
- Maintaining header uniqueness across unauthorized recovery, retry, and
error-response paths.
- Emitting exactly one installation header for enroll and refresh
requests.
Checks run:
- `just test -p codex-app-server-transport request_headers`: 3 passed
- `just test -p codex-app-server-transport remote_control_http_mode`: 6
passed
- `just test -p codex-app-server-transport clients_tests`: 6 passed
- `just test -p codex-app-server-transport`: 123 passed
- `cargo test -p codex-app-server-transport`: 123 passed
- `just clippy -p codex-app-server-transport`
- `just fmt-check`
- `bazel test
//codex-rs/app-server-transport:app-server-transport-unit-tests`
## Why
Authentication mode is a domain concept used by login, model selection,
telemetry, and transports. Keeping the canonical type in app-server
protocol forces those lower-level crates to depend on an unrelated wire
API.
## What changed
- Added canonical `codex_protocol::auth::AuthMode` domain values.
- Kept the app-server wire DTO unchanged and added an explicit app-side
conversion.
- Removed production app-server-protocol dependencies from login,
model-provider-info, models-manager, and otel call paths.
## Stack
This is PR 2 of 6, stacked on [PR
#29714](https://github.com/openai/codex/pull/29714). Review only the
delta from `codex/split-json-rpc-protocols`. Next: [PR
#29722](https://github.com/openai/codex/pull/29722).
## Validation
- Auth and login coverage passed in the focused protocol/domain test
run.
- App-server account and auth conversion coverage passed.
## Summary
Stacked on #26706.
Adds the shared auth/system-proxy contract that later platform resolver
PRs plug into. This PR moves Codex-owned auth and startup HTTP clients
through a common route-aware boundary, but does not yet add Windows or
macOS system proxy resolution.
The default path remains unchanged when `respect_system_proxy` is absent
or disabled.
## Implementation
- Adds `codex-client/src/outbound_proxy.rs` with the shared
route-selection model:
- `OutboundProxyConfig`;
- `ClientRouteClass`;
- `RouteFailureClass`;
- `build_reqwest_client_for_route`.
- Preserves the existing reqwest/default-client behavior when no route
config is supplied.
- Uses the fixed MVP routing policy when route config is supplied:
platform system/PAC/WPAD discovery, then explicit env proxy variables,
then direct connection.
- Keeps platform-specific system discovery behind the shared client
boundary. This PR provides the contract and fallback behavior; later
resolver PRs plug in Windows and macOS discovery.
- Adds `login::AuthRouteConfig` so auth call sites depend on a small
policy type instead of platform resolver details.
- Maps the resolved `Config.respect_system_proxy` boolean into
`AuthRouteConfig` for auth-owned clients.
- Wires the route config through browser login, device-code login,
access-token login, login status, logout/revoke, token refresh, API-key
exchange, app-server account login, TUI/app startup, cloud-config
bootstrap, cloud tasks, plugin auth, and exec startup config loading.
## End-user behavior
- No behavior changes by default.
- When `respect_system_proxy = true`, auth-owned clients opt into the
shared route-aware client path.
- On platforms without a resolver implementation in this PR, system
discovery is unavailable and the route-aware path falls back to explicit
env proxy handling, then direct connection.
- Custom CA handling remains separate from proxy route selection and
still runs through the shared client builder.
- No proxy URLs, PAC contents, or resolved platform details are exposed
through the public config surface introduced here.
## Tests
Adds or updates coverage for:
- preserving default auth-client fallback behavior when no route config
is provided;
- injected environment-proxy fallback without mutating process
environment;
- existing login-server E2E flows using explicit `auth_route_config:
None` to guard unchanged default behavior;
- updated auth manager, login, logout, cloud-config, startup, and
plugin-auth call sites passing route config explicitly.
## Summary
The remote-control initialization timeout test used a 50 ms wall-clock
deadline around a 10 ms transport timeout. A busy CI runner could miss
that outer deadline even when the rollback behavior was correct.
Pause Tokio time and advance it explicitly through the transport timeout
instead. The test still verifies that initialization fails and emits the
matching connection-closed event, without depending on scheduler speed.
## 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`
## Why
Managed deployments need a reliable deny gate for remote control.
Persisted enablement and explicit startup requests currently remain able
to start the transport, while the removed `features.remote_control` key
is intentionally only a compatibility no-op.
This adds a dedicated requirement that administrators can use to force
remote control off without deleting the user's persisted preference.
Removing the requirement and restarting restores the prior choice.
## What Changed
- Added top-level `allow_remote_control` requirements parsing, sourced
layer precedence, debug output, and `configRequirements/read` exposure
as `allowRemoteControl`.
- Added a typed transport policy captured from the startup requirements
snapshot. Managed disable forces the initial state to disabled and
prevents enrollment, refresh, connection, and persisted-preference
mutation.
- Rejected every `remoteControl/*` RPC before parameter deserialization
with JSON-RPC `-32600` and `remote control is disabled by managed
requirements`.
- Preserved the existing disabled status notification and the previous
behavior when the requirement is `true` or omitted.
- Regenerated app-server protocol schemas and documented the new
requirement.
## Verification
- Confirmed all remote-control RPCs, including a malformed request,
return the managed-policy error while the initial status notification
remains `disabled`.
- Confirmed explicit ephemeral startup and persisted enablement make no
backend connection and leave the SQLite preference unchanged.
- Confirmed `allow_remote_control = true` does not enable or block
remote control and `configRequirements/read` returns
`allowRemoteControl: false` for the deny policy.
Related issue: N/A (managed-policy hardening).
## 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
Remote-control runtime enablement and persisted enrollment preference
were represented by separate flags. That made startup rehydration, RPC
persistence, and new-enrollment seeding race with one another, and it
did not cleanly distinguish runtime-only CLI or daemon starts from
durable app-server RPC changes.
## What Changed
- Replace the parallel enablement, seed, and rehydration flags with one
transport-owned `RemoteControlDesiredState`.
- Add nullable enrollment-scoped persistence and preserve existing
preferences during enrollment upserts.
- Rehydrate plain startup only after auth and client scope resolve,
without overwriting a concurrent RPC transition.
- Make ordinary `remoteControl/enable` and `remoteControl/disable`
durable while retaining `ephemeral: true` for runtime-only callers.
- Have the daemon explicitly request ephemeral enablement and regenerate
the app-server schemas.
## Verification
- Covered migration and `NULL`/`0`/`1` persistence round trips.
- Covered plain-start rehydration and runtime-only versus durable
enrollment seeding.
- Covered durable enable, durable disable, and ephemeral enable through
app-server RPC.
- Covered the daemon's exact `{ "ephemeral": true }` request payload.
Related issue: N/A (internal remote-control persistence architecture
change).
## 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.
## Why
A remote-control WebSocket handshake can receive a generic HTTP 404 when
an intermediary routes the request without preserving the WebSocket
upgrade. Treating every 404 as proof that the remote app server is gone
clears valid enrollment and causes repeated re-enrollment, new
environment and server IDs, Habitat churn, and noisy `/server/enroll`
traffic.
## What Changed
- Clear enrollment only when a 404 JSON response explicitly contains
`{"detail":"Remote app server not found"}`.
- Preserve enrollment for empty, plain-text, malformed, or otherwise
unrecognized 404 responses, return the transport error, and retry with
the existing reconnect backoff.
- Log the status, correlation headers (`request-id` or
`x-oai-request-id`, plus `cf-ray`), and bounded/redacted response body
for unrecognized 404s.
- Cover both explicit missing-server re-enrollment and generic 404
enrollment preservation/reconnect behavior.
## Verification
`just test -p codex-app-server-transport` passes all 114 tests on the
rebased branch, including the targeted explicit and generic WebSocket
404 scenarios.
Related issue: N/A
## 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.
## What
Adds transport support for checking remote-control pairing status
against the backend.
- Adds the normalized `server/pair/status` backend URL.
- Adds backend request/response structs for exactly one lookup key:
`pairing_code` or `manual_pairing_code`, returning `{ claimed }`.
- Adds `RemoteControlEnrollment::pairing_status` and
`RemoteControlHandle::pairing_status`.
- Preserves auth refresh/retry behavior and backend error mapping.
- Adds transport coverage for pending, claimed, manual-code payloads,
token refresh, mapped backend errors, malformed responses, and URL
normalization.
## Why
Desktop needs a host-authenticated way to poll whether a QR or manual
pairing code has been claimed.
Related backend change: https://github.com/openai/openai/pull/990244
## Verification
- `cargo test --manifest-path app-server-transport/Cargo.toml
remote_control::tests::pairing_tests`
- `cargo fmt --all --check`
- `git diff --check`
## Why
`remoteControl/pairing/start` creates authorization for future
remote-control connections, so it should not require the live websocket
to already be enabled. Requiring enable first made pairing depend on
presence instead of the persisted server enrollment that pairing
actually uses.
Pairing also needs to recover when that persisted server row is stale.
If `/server/pair` returns `404`, making the first pairing attempt fail
forces a manual retry even though the client can clear the stale row and
create a replacement enrollment immediately.
## What Changed
- Allow `remoteControl/pairing/start` to reuse or create the persisted
remote-control server enrollment while remote control is disabled.
- Keep the selected in-memory enrollment across disable and share it
with websocket connect so a later enable uses the same selected server.
- Thread the app-server client name through pairing so stdio persistence
keeps using the websocket-owned enrollment key.
- Recover pairing server-token auth failures through the existing
refresh/auth-recovery path.
- Recover stale pairing enrollment on `/server/pair` `404` by clearing
the stale selected enrollment, re-enrolling once, and retrying pairing
once.
- Add focused disabled-pairing and stale-pairing recovery coverage.
## Verification
-
`remote_control_pairing_start_returns_pairing_artifacts_while_disabled`
exercises pairing before enable.
- `remote_control_handle_reenrolls_after_stale_pairing_enrollment`
exercises stale `/server/pair` `404` recovery without a manual retry.
Related: N/A
## Why
Remote-control clients need to list and revoke controller-device grants
without enabling or enrolling the local relay. These are signed-in
account-management operations, so coupling them to websocket, pairing,
enrollment, or persisted relay state would prevent clients from managing
stale grants from the picker.
Related enhancement request: N/A. This adds the Codex app-server surface
for the planned upstream environment-scoped revoke endpoint.
## What Changed
- Added experimental app-server v2 RPCs:
- `remoteControl/client/list`
- `remoteControl/client/revoke`
- Added picker-oriented protocol types and standard generated schema
fixtures. The list response intentionally omits backend account id,
enrollment status, and location fields.
- Added `app-server-transport/src/transport/remote_control/clients.rs`
for environment-scoped GET and DELETE requests. It builds escaped URL
path segments, forwards optional pagination query fields, sends ChatGPT
auth plus `chatgpt-account-id`, converts RFC3339 `last_seen_at` values
to Unix seconds, accepts `204 No Content` revoke responses, and retries
once after a `401`.
- Extracted shared ChatGPT auth loading and recovery into
`app-server-transport/src/transport/remote_control/auth.rs` so
websocket, pairing, and client management use the same account-auth
boundary.
- Retained the configured remote-control base URL on
`RemoteControlHandle` and resolve management URLs lazily, preserving
deferred validation while relay startup is disabled.
- Registered list as `global_shared_read("remote-control-clients")` and
revoke as `global("remote-control-clients")`.
## Verification
- Added transport coverage proving list and revoke work while relay
state is disabled, IDs are escaped, picker-only fields are returned,
timestamps are converted, revoke accepts `204`, auth headers are
forwarded, `401` retries exactly once, `403` is not retried, and
malformed list payloads retain decode context.
- Added an app-server integration test proving both JSON-RPC methods
work before relay enablement and successful revoke returns `{}`.
- Regenerated and validated experimental and standard app-server schema
fixtures.