Agent-readable docs
Codex App Server Documentation
Reference for the `codex app-server` JSON-RPC 2.0 process: transports, connection lifecycle, thread/turn APIs, config and auth RPC, notifications, schema generation, and client integration patterns for IDE and embedded hosts.
Pages
- OverviewWhat `codex app-server` exposes, who should integrate against it, supported transports, and the shortest path from `initialize` to a running turn.
- InstallationHow to obtain and launch the `codex-app-server` binary, default listen URLs, session source, logging env vars, and success signals for stdio and control-socket modes.
- QuickstartEnd-to-end first connection: `initialize` / `initialized`, `thread/start`, `turn/start`, read `item/*` and `turn/completed`, with verification and one recovery note.
- Protocol and transportJSON-RPC 2.0 wire rules, stdio JSONL vs unix-socket vs websocket listeners, health probes, backpressure code `-32001`, and bounded queue behavior.
- Threads, turns, and itemsCore primitives (`Thread`, `Turn`, `ThreadItem`), subscription model, thread status states, ephemeral threads, and how persisted rollouts relate to in-memory loaded threads.
- Connection lifecyclePer-connection `initialize` handshake, `clientInfo` requirements, notification opt-out, thread subscribe/unsubscribe idle unload, and server-initiated requests vs client RPC.
- Experimental APIRuntime opt-in via `capabilities.experimentalApi`, stable vs experimental schema generation, rejection messages, and maintainer gating patterns for fields and notifications.
- Build a JSON-RPC clientClient responsibilities: newline-delimited framing, request ids, handling server requests mid-turn, `initialized` notification ordering, and compliance-oriented `clientInfo.name` values.
- Transports and proxyChoosing `--listen` URLs, unix control socket at `$CODEX_HOME/app-server-control`, `codex app-server proxy` byte bridging, websocket auth args, and remote-control pairing endpoints.
- Stream turns and eventsConsuming `turn/started`, `item/started`, deltas (`item/agentMessage/delta`, command output, reasoning), `turn/completed`, token usage, and notification opt-out for high-volume streams.
- Approvals and server requestsInline approval flows for `commandExecution` and `fileChange`, `serverRequest/resolved` lifecycle, MCP elicitations, attestation `attestation/generate`, and `tool/requestUserInput` handling.
- Account, auth, and configAccount login flows (`apiKey`, `chatgpt`, device code), config read/write/batch RPC, requirements.toml constraints, and hot-reload behavior after `config/batchWrite`.
Complete Markdown
# Codex App Server Documentation
> Reference for the `codex app-server` JSON-RPC 2.0 process: transports, connection lifecycle, thread/turn APIs, config and auth RPC, notifications, schema generation, and client integration patterns for IDE and embedded hosts.
## Context Links
- [Agent index](https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/llms.txt)
- [Human interactive docs](https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1)
- [GitHub repository](https://github.com/openai/codex)
## Repository Metadata
- Repository: openai/codex
- Branch: main
- Generated: 2026-06-02T06:42:53.759Z
- Updated: 2026-06-02T06:53:57.380Z
- Runtime: Grok CLI
- Format: Documentation
- Pages: 22
## Page Index
- 01. [Overview](https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/pages/01-overview.md) - What `codex app-server` exposes, who should integrate against it, supported transports, and the shortest path from `initialize` to a running turn.
- 02. [Installation](https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/pages/02-installation.md) - How to obtain and launch the `codex-app-server` binary, default listen URLs, session source, logging env vars, and success signals for stdio and control-socket modes.
- 03. [Quickstart](https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/pages/03-quickstart.md) - End-to-end first connection: `initialize` / `initialized`, `thread/start`, `turn/start`, read `item/*` and `turn/completed`, with verification and one recovery note.
- 04. [Protocol and transport](https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/pages/04-protocol-and-transport.md) - JSON-RPC 2.0 wire rules, stdio JSONL vs unix-socket vs websocket listeners, health probes, backpressure code `-32001`, and bounded queue behavior.
- 05. [Threads, turns, and items](https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/pages/05-threads-turns-and-items.md) - Core primitives (`Thread`, `Turn`, `ThreadItem`), subscription model, thread status states, ephemeral threads, and how persisted rollouts relate to in-memory loaded threads.
- 06. [Connection lifecycle](https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/pages/06-connection-lifecycle.md) - Per-connection `initialize` handshake, `clientInfo` requirements, notification opt-out, thread subscribe/unsubscribe idle unload, and server-initiated requests vs client RPC.
- 07. [Experimental API](https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/pages/07-experimental-api.md) - Runtime opt-in via `capabilities.experimentalApi`, stable vs experimental schema generation, rejection messages, and maintainer gating patterns for fields and notifications.
- 08. [Build a JSON-RPC client](https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/pages/08-build-a-json-rpc-client.md) - Client responsibilities: newline-delimited framing, request ids, handling server requests mid-turn, `initialized` notification ordering, and compliance-oriented `clientInfo.name` values.
- 09. [Transports and proxy](https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/pages/09-transports-and-proxy.md) - Choosing `--listen` URLs, unix control socket at `$CODEX_HOME/app-server-control`, `codex app-server proxy` byte bridging, websocket auth args, and remote-control pairing endpoints.
- 10. [Stream turns and events](https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/pages/10-stream-turns-and-events.md) - Consuming `turn/started`, `item/started`, deltas (`item/agentMessage/delta`, command output, reasoning), `turn/completed`, token usage, and notification opt-out for high-volume streams.
- 11. [Approvals and server requests](https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/pages/11-approvals-and-server-requests.md) - Inline approval flows for `commandExecution` and `fileChange`, `serverRequest/resolved` lifecycle, MCP elicitations, attestation `attestation/generate`, and `tool/requestUserInput` handling.
- 12. [Account, auth, and config](https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/pages/12-account-auth-and-config.md) - Account login flows (`apiKey`, `chatgpt`, device code), config read/write/batch RPC, requirements.toml constraints, and hot-reload behavior after `config/batchWrite`.
- 13. [Skills, plugins, and MCP](https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/pages/13-skills-plugins-and-mcp.md) - Listing and configuring skills, plugin marketplace install flows, `mcpServerStatus/list`, OAuth login, tool calls, reload after config edits, and `skills/changed` notifications.
- 14. [In-process embedding](https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/pages/14-in-process-embedding.md) - Using `in_process` to run `MessageProcessor` without a socket, internal initialize handshake, `InProcessClientHandle` request/event channels, backpressure, and relationship to `codex-app-server-client`.
- 15. [RPC methods reference](https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/pages/15-rpc-methods-reference.md) - Grouped catalog of v2 `<resource>/<method>` RPCs for threads, turns, filesystem, models, processes, plugins, remote control, and utility commands with stable vs experimental markers.
- 16. [Notifications and events](https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/pages/16-notifications-and-events.md) - Server notification method names, `ThreadItem` union variants, per-item delta events, turn-level events, realtime thread notifications, and `CodexErrorInfo` values on failures.
- 17. [Config RPC reference](https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/pages/17-config-rpc-reference.md) - `config/read`, `config/value/write`, `config/batchWrite`, `configRequirements/read`, `config/mcpServer/reload`, external-agent detect/import, and snake_case wire fields mirroring `config.toml`.
- 18. [Schema generation](https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/pages/18-schema-generation.md) - `codex app-server generate-ts` and `generate-json-schema`, stable vs `--experimental` output, version pinning to the running binary, and when to regenerate fixtures after protocol changes.
- 19. [CLI flags and error codes](https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/pages/19-cli-flags-and-error-codes.md) - `--listen`, `--session-source`, `--strict-config`, websocket auth flags, JSON-RPC standard codes, overload `-32001`, `input_too_large`, and tracing via `RUST_LOG` / `LOG_FORMAT=json`.
- 20. [Thread lifecycle examples](https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/pages/20-thread-lifecycle-examples.md) - Copy-paste JSON-RPC sequences for `thread/start`, `thread/resume`, `thread/fork`, `thread/list` pagination, `turn/interrupt`, and `thread/unsubscribe` idle unload from README-backed tests.
- 21. [Troubleshooting](https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/pages/21-troubleshooting.md) - Diagnosing `Not initialized`, overload retries, turn failures and `codexErrorInfo`, strict config parse errors, MCP startup `failed` status, and websocket `Origin` rejections.
- 22. [Development and testing](https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/pages/22-development-and-testing.md) - Running integration suites under `tests/suite`, spawning the binary via `test_app_server`, debug env hooks, notify-capture bins, and `just test -p codex-app-server` expectations.
## Source File Index
- `BUILD.bazel`
- `Cargo.toml`
- `README.md`
- `src/app_server_tracing.rs`
- `src/attestation.rs`
- `src/bespoke_event_handling.rs`
- `src/bin/notify_capture.rs`
- `src/config_manager_service.rs`
- `src/config_manager.rs`
- `src/config/mod.rs`
- `src/connection_rpc_gate.rs`
- `src/error_code.rs`
- `src/filters.rs`
- `src/fs_watch.rs`
- `src/fuzzy_file_search.rs`
- `src/in_process.rs`
- `src/lib.rs`
- `src/main.rs`
- `src/mcp_refresh.rs`
- `src/message_processor.rs`
- `src/models.rs`
- `src/outgoing_message.rs`
- `src/request_processors.rs`
- `src/request_processors/account_processor.rs`
- `src/request_processors/command_exec_processor.rs`
- `src/request_processors/config_errors.rs`
- `src/request_processors/config_processor.rs`
- `src/request_processors/external_agent_config_processor.rs`
- `src/request_processors/feedback_processor.rs`
- `src/request_processors/fs_processor.rs`
- `src/request_processors/initialize_processor.rs`
- `src/request_processors/marketplace_processor.rs`
- `src/request_processors/mcp_processor.rs`
- `src/request_processors/plugins.rs`
- `src/request_processors/process_exec_processor.rs`
- `src/request_processors/remote_control_processor.rs`
- `src/request_processors/request_errors.rs`
- `src/request_processors/thread_lifecycle.rs`
- `src/request_processors/thread_processor.rs`
- `src/request_processors/turn_processor.rs`
- `src/request_serialization.rs`
- `src/server_request_error.rs`
- `src/skills_watcher.rs`
- `src/thread_state.rs`
- `src/thread_status.rs`
- `src/transport.rs`
- `tests/all.rs`
- `tests/common/lib.rs`
- `tests/common/responses.rs`
- `tests/common/test_app_server.rs`
- `tests/suite/auth.rs`
- `tests/suite/mod.rs`
- `tests/suite/strict_config.rs`
- `tests/suite/v2/client_metadata.rs`
- `tests/suite/v2/compaction.rs`
- `tests/suite/v2/config_rpc.rs`
- `tests/suite/v2/mcp_server_status.rs`
- `tests/suite/v2/thread_inject_items.rs`
- `tests/suite/v2/thread_list.rs`
- `tests/suite/v2/thread_name_websocket.rs`
- `tests/suite/v2/thread_rollback.rs`
- `tests/suite/v2/thread_status.rs`
---
## 01. Overview
> What `codex app-server` exposes, who should integrate against it, supported transports, and the shortest path from `initialize` to a running turn.
- Page Markdown: https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/pages/01-overview.md
- Generated: 2026-06-02T06:35:55.501Z
### Source Files
- `README.md`
- `Cargo.toml`
- `src/main.rs`
- `src/lib.rs`
- `src/message_processor.rs`
- `src/request_processors.rs`
---
title: "Overview"
description: "What `codex app-server` exposes, who should integrate against it, supported transports, and the shortest path from `initialize` to a running turn."
---
`codex app-server` is the `codex-app-server` binary and `codex_app_server` library that expose Codex agent capabilities over JSON-RPC 2.0. Each transport connection gets a per-connection `initialize` handshake, then v2 RPC methods such as `thread/start` and `turn/start` drive conversations while the server streams `turn/*` and `item/*` notifications and may issue server-initiated requests (approvals, auth refresh, attestation).
## Who integrates against app-server
| Integrator | Typical transport | Role |
| --- | --- | --- |
| IDE and desktop hosts (for example the Codex VS Code extension) | `stdio://` subprocess or `unix://` control socket | Primary product UI: threads, turns, approvals, config |
| Local control-plane clients | `unix://` at `$CODEX_HOME/app-server-control/app-server-control.sock` | Long-lived local server; optional `codex app-server proxy` byte bridge |
| In-process CLI surfaces (TUI, exec) | `in_process` module (no socket) | Same protocol semantics without a process boundary; wrapped by `codex-app-server-client` |
| Experimental remote listeners | `ws://IP:PORT` | Health probes and JSON-RPC over WebSocket frames (**experimental / unsupported for production**) |
New integrations should target the **v2** API surface in `codex-app-server-protocol` (`<resource>/<method>` names, camelCase wire fields). Identify the client in `initialize.params.clientInfo` — enterprise integrations may need registration for compliance logging (see README).
## What the server exposes
- **Wire protocol:** JSON-RPC 2.0, bidirectional (client RPCs, server notifications, and server-initiated requests). The `"jsonrpc":"2.0"` header is omitted on the wire.
- **API contract:** Typed requests, responses, and notifications in `codex-app-server-protocol`, with TypeScript and JSON Schema export via `codex app-server generate-ts` and `codex app-server generate-json-schema`.
- **Agent runtime:** `MessageProcessor` dispatches RPCs to focused request processors (`ThreadRequestProcessor`, `TurnRequestProcessor`, `ConfigRequestProcessor`, `McpRequestProcessor`, and others) backed by `codex-core` (`ThreadManager`, config, auth, rollout state).
- **Persistence:** SQLite state under the server’s Codex home (`rollout_state_db`) for thread metadata and related state when available.
<Note>
Active API development is v2 only. Do not add new surface area to v1.
</Note>
## Runtime architecture
`run_main_with_transport_options` in `lib.rs` runs two cooperating Tokio tasks:
1. **Processor loop** — accepts `TransportEvent`s, parses JSON-RPC, and calls `MessageProcessor::process_request`.
2. **Outbound router** — writes responses and notifications to per-connection writers, honoring initialization flags, experimental API opt-in, and per-connection notification opt-out.
```mermaid
flowchart TB
subgraph clients [Clients]
IDE[IDE / extension]
CTL[Control socket client]
IPC[In-process embedder]
end
subgraph transport [codex-app-server-transport]
STDIO[stdio JSONL]
UNIX[unix socket + WS upgrade]
WS[websocket listener]
end
subgraph appserver [codex-app-server lib.rs]
PROC[Processor loop]
OUT[Outbound router]
MP[MessageProcessor]
end
subgraph processors [Request processors]
INIT[InitializeRequestProcessor]
THR[ThreadRequestProcessor]
TRN[TurnRequestProcessor]
CFG[ConfigRequestProcessor]
OTH[Account / MCP / FS / ...]
end
subgraph core [codex-core + stores]
TM[ThreadManager]
DB[(SQLite state)]
end
IDE --> STDIO
CTL --> UNIX
IPC --> MP
STDIO --> PROC
UNIX --> PROC
WS --> PROC
PROC --> MP
MP --> INIT
MP --> THR
MP --> TRN
MP --> CFG
MP --> OTH
THR --> TM
TRN --> TM
TM --> DB
MP --> OUT
OUT --> STDIO
OUT --> UNIX
OUT --> WS
```
Bounded channels (`CHANNEL_CAPACITY` = 128 between transport ingress, processing, and outbound writes) provide backpressure. Saturated ingress returns JSON-RPC error code `-32001` with message `"Server overloaded; retry later."` — clients should retry with exponential backoff and jitter.
## Supported transports
| `--listen` value | Behavior | Default |
| --- | --- | --- |
| `stdio://` | Newline-delimited JSON (JSONL) on stdin/stdout; process exits when the sole stdio connection closes | **Yes** (`AppServerTransport::DEFAULT_LISTEN_URL`) |
| `unix://` or `unix://PATH` | WebSocket connections over HTTP Upgrade on `$CODEX_HOME/app-server-control/app-server-control.sock` (or custom path); startup lock under the same directory | |
| `ws://IP:PORT` | One JSON-RPC message per WebSocket text frame; `GET /readyz` and `GET /healthz` health probes | Experimental |
| `off` | No local listener (remote control may still apply when enabled and state DB is available) | |
Additional CLI context from `main.rs`:
<ParamField body="--session-source" type="string" default="vscode">
Derives product restrictions and metadata (`SessionSource::from_startup_arg`).
</ParamField>
<ParamField body="--strict-config" type="boolean" default="false">
Fail startup when `config.toml` contains unknown fields.
</ParamField>
Tracing: `RUST_LOG` filters verbosity; `LOG_FORMAT=json` emits structured logs to stderr.
## Core primitives
| Primitive | Meaning |
| --- | --- |
| **Thread** | A conversation between a user and the Codex agent; holds multiple turns and persisted rollout history |
| **Turn** | One user-driven cycle (input → agent work → completion); contains items |
| **ThreadItem** | User inputs and agent outputs (messages, reasoning, shell commands, file edits, tool calls, etc.) |
`thread/start`, `thread/resume`, and `thread/fork` create or reopen threads and auto-subscribe the connection to thread/turn/item notifications. `turn/start` attaches user input and begins generation.
## Shortest path: `initialize` to a running turn
Per connection, complete initialization before any other RPC. The server marks the session initialized when the `initialize` **request** succeeds; the client then sends the `initialized` **notification** per protocol (the server currently logs client notifications but gates RPCs on the completed `initialize` request).
```mermaid
sequenceDiagram
participant C as Client
participant T as Transport
participant P as MessageProcessor
participant Core as ThreadManager
C->>T: JSON-RPC initialize
T->>P: process_request
P->>P: InitializeRequestProcessor.initialize
P-->>C: InitializeResponse (userAgent, codexHome, platform*)
Note over C: Send initialized notification
C->>T: thread/start
T->>P: ThreadRequestProcessor
P->>Core: start thread
P-->>C: thread in result
P-->>C: notification thread/started
C->>T: turn/start (threadId, input)
T->>P: TurnRequestProcessor
P-->>C: turn in result (status inProgress)
P-->>C: notification turn/started
P-->>C: item/started, deltas, item/completed
P-->>C: notification turn/completed
```
<Steps>
<Step title="Open a transport">
Spawn `codex app-server` (default `stdio://`) or connect to the unix control socket / experimental websocket listener.
</Step>
<Step title="Initialize the connection">
Send `initialize` with `clientInfo` (and optional `capabilities` such as `experimentalApi`, `optOutNotificationMethods`, `requestAttestation`). Read `userAgent`, `codexHome`, `platformFamily`, and `platformOs` from the response.
</Step>
<Step title="Acknowledge with initialized">
Emit the `initialized` client notification (no params).
</Step>
<Step title="Start a thread">
Call `thread/start` (or `thread/resume` / `thread/fork`). Expect a `thread` in the response and a `thread/started` notification.
</Step>
<Step title="Start a turn">
Call `turn/start` with `threadId` and `input` (text, image, or local image items). Expect an `inProgress` turn in the response, then stream `turn/started`, `item/*`, and `turn/completed`.
</Step>
</Steps>
<RequestExample>
```json
{ "method": "initialize", "id": 0, "params": {
"clientInfo": { "name": "my_client", "title": "My Client", "version": "1.0.0" }
} }
```
</RequestExample>
<ResponseExample>
```json
{ "id": 0, "result": {
"userAgent": "…",
"codexHome": "/Users/me/.codex",
"platformFamily": "unix",
"platformOs": "macos"
} }
```
</ResponseExample>
<RequestExample>
```json
{ "method": "initialized" }
```
</RequestExample>
<RequestExample>
```json
{ "method": "thread/start", "id": 10, "params": {
"cwd": "/path/to/project"
} }
```
</RequestExample>
<RequestExample>
```json
{ "method": "turn/start", "id": 30, "params": {
"threadId": "thr_123",
"input": [ { "type": "text", "text": "Run tests" } ]
} }
```
</RequestExample>
### Initialization errors and capabilities
| Condition | Error |
| --- | --- |
| RPC before `initialize` completes | `"Not initialized"` (`-32600` invalid request) |
| Second `initialize` on same connection | `"Already initialized"` |
| Experimental method without `capabilities.experimentalApi` | Experimental-required message from protocol helpers |
After initialization, websocket-oriented transports mirror session state into the outbound router and emit connection-scoped `config/warning` and `remoteControl/status/changed` notifications before general broadcast traffic.
## In-process embedding
The public `in_process` module runs the same `MessageProcessor` and outbound routing without sockets. `start` performs the `initialize` / `initialized` handshake internally and returns an `InProcessClientHandle` for typed `ClientRequest` / event consumption — intended for same-process CLI surfaces; higher-level callers should use `codex-app-server-client`.
## Related pages
<CardGroup>
<Card title="Installation" href="/installation">
Launch `codex-app-server`, default listen URLs, logging env vars, and success signals.
</Card>
<Card title="Quickstart" href="/quickstart">
End-to-end first connection with verification and recovery.
</Card>
<Card title="Protocol and transport" href="/protocol-and-transport">
JSON-RPC wire rules, transport details, health probes, and backpressure.
</Card>
<Card title="Connection lifecycle" href="/connection-lifecycle">
`clientInfo`, notification opt-out, subscribe/unsubscribe, and server requests.
</Card>
<Card title="Threads, turns, and items" href="/page-threads-turns-items">
Core primitives, subscriptions, and rollout persistence.
</Card>
<Card title="RPC methods reference" href="/rpc-methods">
Grouped v2 method catalog with stable vs experimental markers.
</Card>
</CardGroup>
---
## 02. Installation
> How to obtain and launch the `codex-app-server` binary, default listen URLs, session source, logging env vars, and success signals for stdio and control-socket modes.
- Page Markdown: https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/pages/02-installation.md
- Generated: 2026-06-02T06:36:13.016Z
### Source Files
- `README.md`
- `Cargo.toml`
- `src/main.rs`
- `src/lib.rs`
- `src/transport.rs`
- `BUILD.bazel`
---
title: "Installation"
description: "How to obtain and launch the `codex-app-server` binary, default listen URLs, session source, logging env vars, and success signals for stdio and control-socket modes."
---
The `codex-app-server` crate ships a `codex-app-server` binary and is also reachable as `codex app-server` on the multitool CLI. Both entry points run the same JSON-RPC v2 server; the default transport is newline-delimited JSON on stdio (`stdio://`), and an optional Unix control socket listens at `$CODEX_HOME/app-server-control/app-server-control.sock` when you pass `--listen unix://`.
## Obtain the binary
| Path | Command / artifact | Notes |
| --- | --- | --- |
| Workspace build | `cargo build -p codex-app-server` (from `codex-rs/`) | Produces `codex-app-server` in the Cargo `target/` directory for the active profile. |
| Installed Codex CLI | `codex` package / release artifact | `codex app-server` calls `codex_app_server::run_main_with_transport_options` with the same transport stack as the standalone binary. |
| Direct invocation | `codex-app-server` on `PATH` | `src/main.rs` uses `arg0_dispatch_or_else` so the binary can run without the `codex` front-end. |
| Tests / CI | `codex_utils_cargo_bin::cargo_bin("codex-app-server")` | Integration tests spawn this path (see `tests/common/test_app_server.rs`). |
<Note>
The crate also builds `codex-app-server-test-notify-capture` for test harnesses; production integrations should use `codex-app-server` or `codex app-server`.
</Note>
## Launch the server
<Steps>
<Step title="Pick an entry point">
**Standalone binary** (full CLI surface on the binary itself):
```bash
codex-app-server
codex-app-server --listen stdio://
codex-app-server --listen unix://
codex-app-server --listen unix:///absolute/path/custom.sock
```
**Codex multitool** (recommended when Codex is already installed):
```bash
codex app-server
codex app-server --stdio
codex app-server --listen unix://
codex app-server --listen ws://127.0.0.1:4500
```
`--stdio` on `codex app-server` is equivalent to `--listen stdio://` and overrides the `--listen` default for that invocation.
</Step>
<Step title="Set Codex home (optional)">
The server resolves configuration and the default control-socket directory via `find_codex_home()` (typically `$CODEX_HOME`). Integration tests set `CODEX_HOME` explicitly when spawning the child process.
</Step>
<Step title="Choose transport">
See the listen URL table below. Stdio is the default for editor-style hosts that pipe stdin/stdout. Unix control socket mode is for local clients that speak WebSocket over the domain socket (HTTP Upgrade handshake).
</Step>
</Steps>
### Default and supported `--listen` URLs
<ParamField body="--listen" type="string" default="stdio://">
Transport endpoint URL. Parsed by `AppServerTransport::from_listen_url` in `codex-app-server-transport`.
</ParamField>
| URL | Transport | Behavior |
| --- | --- | --- |
| `stdio://` | Stdio (default) | One JSON-RPC message per line on stdin; responses and notifications on stdout. Logs go to stderr. |
| `unix://` | Unix control socket | Binds `$CODEX_HOME/app-server-control/app-server-control.sock` (mode `0600` on Unix). |
| `unix://PATH` | Unix control socket | Binds an absolute or cwd-relative socket path. |
| `ws://IP:PORT` | WebSocket + HTTP | Experimental; also serves `GET /readyz` and `GET /healthz`. Non-loopback binds require websocket auth flags. |
| `off` | None | No local listener; useful only with remote control enabled and a usable sqlite state DB. |
Constants in code: `DEFAULT_LISTEN_URL = "stdio://"`; control socket file `app-server-control.sock` under directory `app-server-control`.
### Session source
<ParamField body="--session-source" type="string" default="vscode">
**Standalone `codex-app-server` only.** Parsed by `SessionSource::from_startup_arg`. Drives product restrictions and session metadata.
</ParamField>
| Value | Maps to |
| --- | --- |
| `vscode` | `SessionSource::VSCode` (default) |
| `cli` | `SessionSource::Cli` |
| `exec` | `SessionSource::Exec` |
| `mcp`, `app-server`, `app_server`, `appserver` | `SessionSource::Mcp` |
| `unknown` | `SessionSource::Unknown` |
| Any other non-empty string | `SessionSource::Custom` (normalized to ASCII lowercase) |
<Warning>
`codex app-server` does not expose `--session-source`; the CLI path always passes `SessionSource::VSCode` into `run_main_with_transport_options`. Use the standalone binary when you need a different session source label.
</Warning>
Managed daemons started via `codex app-server daemon` use `--listen unix://` (and optionally `--remote-control`), not stdio.
### Other startup flags (standalone and `codex app-server`)
| Flag | Default | Purpose |
| --- | --- | --- |
| `--strict-config` | `false` | Exit on unknown `config.toml` fields instead of warning and loading defaults. |
| `--remote-control` | `false` (hidden) | Enable remote-control transport when sqlite state is available. |
| `--analytics-default-enabled` | `false` on binary; opt-in on `codex app-server` | Changes default analytics enablement for first-party hosts. |
| `--ws-auth`, token files, issuer/audience | unset | Required for non-loopback `ws://` listeners. |
Debug-only on **debug** `codex-app-server` builds: `--disable-plugin-startup-tasks-for-tests`, env `CODEX_APP_SERVER_MANAGED_CONFIG_PATH`, `CODEX_APP_SERVER_DISABLE_MANAGED_CONFIG`.
## Logging environment variables
Tracing is installed at startup in `run_main_with_transport_options`. Application JSON-RPC traffic is **not** mixed into log output on stdio: protocol bytes use stdout; logs use stderr.
| Variable | Effect |
| --- | --- |
| `RUST_LOG` | Controls `tracing` filter/verbosity via `EnvFilter::from_default_env()` (standard `tracing-subscriber` semantics). |
| `LOG_FORMAT=json` | Emits structured JSON logs to stderr (one event per line). Any other value (or unset) uses the default human-readable formatter. Matching is case-insensitive (`json`, `JSON`, etc.). |
Integration tests commonly set `RUST_LOG=warn` when spawning the server. OpenTelemetry uses service name `codex-app-server` (`OTEL_SERVICE_NAME` in code).
## Verify stdio mode
```mermaid
sequenceDiagram
participant Client
participant Stdin as codex-app-server stdin
participant Stdout as codex-app-server stdout
participant Stderr as codex-app-server stderr
Client->>Stdin: initialize request (JSON line)
Stdout-->>Client: initialize response (JSON line)
Client->>Stdin: initialized notification (JSON line)
Note over Stderr: RUST_LOG / LOG_FORMAT output only
Client->>Stdin: further RPCs after handshake
```
<Check>
**Process health:** The server process stays alive after config load; it exits when the stdio connection closes and no connections remain (`shutdown_when_no_connections` for stdio).
</Check>
<Steps>
<Step title="Start with stdio transport">
```bash
CODEX_HOME=/path/to/codex-home RUST_LOG=info codex-app-server --listen stdio://
```
Or: `codex app-server --stdio`
</Step>
<Step title="Send initialize">
Write a single JSON object per line to stdin (the `"jsonrpc":"2.0"` header is omitted on the wire per protocol docs). Example:
<RequestExample>
```json
{"method":"initialize","id":0,"params":{"clientInfo":{"name":"my_client","title":"My Client","version":"0.1.0"}}}
```
</RequestExample>
</Step>
<Step title="Read the response">
Expect a JSON-RPC **response** line on stdout with matching `id`. Result fields include:
<ResponseField name="userAgent" type="string">
User agent the server will present upstream (derived from `clientInfo.name`).
</ResponseField>
<ResponseField name="codexHome" type="string">
Absolute path to the server’s Codex home directory.
</ResponseField>
<ResponseField name="platformFamily" type="string">
e.g. `unix` or `windows`.
</ResponseField>
<ResponseField name="platformOs" type="string">
e.g. `macos`, `linux`, `windows`.
</ResponseField>
</Step>
<Step title="Send initialized">
After the response, send the `initialized` **notification** (required before other methods). Pre-initialize RPCs receive `"Not initialized"`; duplicate `initialize` returns `"Already initialized"`.
</Step>
</Steps>
**Failure signals on stderr:** Invalid config (non-strict mode) logs `Invalid configuration; using defaults.` via `tracing::error!`. Strict config makes startup return an I/O error instead.
## Verify control-socket mode
```text
$CODEX_HOME/
app-server-control/
app-server-control.sock # websocket upgrade endpoint (0600)
app-server-startup.lock # startup lock while unix listener initializes
```
<Steps>
<Step title="Start the listener">
```bash
codex-app-server --listen unix://
# or managed: codex app-server daemon start # uses --listen unix:// internally
```
</Step>
<Step title="Confirm bind success">
On success, stderr includes a tracing line like `app-server control socket listening` with `socket_path=...`. If another server holds the socket, startup fails with `AddrInUse` and message `app-server control socket is already in use at ...`.
</Step>
<Step title="Connect a client">
Open a WebSocket over the Unix socket using the HTTP Upgrade handshake (same framing as TCP WebSocket clients). `codex app-server proxy` bridges stdin/stdout to the default socket or `--sock PATH`.
</Step>
<Step title="Complete the JSON-RPC handshake">
Per connection: `initialize` request → response → `initialized` notification, same as stdio.
</Step>
</Steps>
<Info>
There is no HTTP `/readyz` on the Unix socket transport. Readiness for control-socket mode is the successful bind log plus an accept that completes WebSocket upgrade.
</Info>
### WebSocket TCP mode (experimental)
When using `--listen ws://127.0.0.1:PORT`, stderr prints a banner with `ws://…`, `http://…/readyz`, and `http://…/healthz`. Use `GET /readyz` → `200 OK` once the listener accepts connections; `GET /healthz` → `200 OK` only when no `Origin` header is present (requests with `Origin` get `403`).
<Warning>
README marks websocket transport as experimental and unsupported for production. Non-loopback binds refuse to start without `--ws-auth capability-token` or `--ws-auth signed-bearer-token`.
</Warning>
## Related pages
<CardGroup>
<Card title="Quickstart" href="/quickstart">
First connection: `initialize`, `thread/start`, `turn/start`, and streaming notifications.
</Card>
<Card title="Protocol and transport" href="/protocol-and-transport">
JSON-RPC wire rules, transports, health probes, and overload code `-32001`.
</Card>
<Card title="Transports and proxy" href="/transports-and-proxy">
Choosing listen URLs, control socket paths, and `codex app-server proxy`.
</Card>
<Card title="CLI flags and errors" href="/cli-flags-and-errors">
Full flag list, websocket auth, and error codes.
</Card>
<Card title="Build a JSON-RPC client" href="/build-jsonrpc-client">
Framing, request ids, and `initialized` ordering for client implementers.
</Card>
</CardGroup>
---
## 03. Quickstart
> End-to-end first connection: `initialize` / `initialized`, `thread/start`, `turn/start`, read `item/*` and `turn/completed`, with verification and one recovery note.
- Page Markdown: https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/pages/03-quickstart.md
- Generated: 2026-06-02T06:36:07.844Z
### Source Files
- `README.md`
- `src/request_processors/initialize_processor.rs`
- `src/request_processors/thread_processor.rs`
- `src/request_processors/turn_processor.rs`
- `tests/common/test_app_server.rs`
- `tests/suite/v2/thread_list.rs`
---
title: "Quickstart"
description: "End-to-end first connection: `initialize` / `initialized`, `thread/start`, `turn/start`, read `item/*` and `turn/completed`, with verification and one recovery note."
---
`codex app-server` exposes a JSON-RPC 2.0 API over newline-delimited JSON on stdio by default (`--listen stdio://`). A first integration opens one transport connection, completes the per-connection `initialize` handshake, starts a thread, runs one turn, and reads streaming `item/*` notifications until `turn/completed`.
## Prerequisites
| Requirement | Notes |
| --- | --- |
| Codex CLI | Provides `codex app-server` and the `codex-app-server` binary used in integration tests. |
| `CODEX_HOME` | Server reads config and rollouts from this directory; tests set it on the child process. |
| JSON-RPC client | Must write one JSON object per line to stdin and read the same from stdout. |
| Monotonic request ids | Integer ids are typical; match responses and errors by `id`. |
<Note>
Wire messages omit the `"jsonrpc":"2.0"` header on the wire, matching MCP-style framing described in the app-server README.
</Note>
## End-to-end flow
```mermaid
sequenceDiagram
participant Client
participant Transport as stdio JSONL transport
participant MP as MessageProcessor
participant TP as ThreadRequestProcessor
participant TurnP as TurnRequestProcessor
Client->>Transport: initialize (clientInfo)
Transport->>MP: ClientRequest::Initialize
MP-->>Client: InitializeResponse
Client->>Transport: initialized notification
Client->>Transport: thread/start
Transport->>TP: thread_start
TP-->>Client: ThreadStartResponse
TP-->>Client: thread/started notification
Client->>Transport: turn/start (threadId, input)
Transport->>TurnP: turn_start
TurnP-->>Client: TurnStartResponse (turn inProgress)
TurnP-->>Client: turn/started
TurnP-->>Client: item/started → item/* deltas → item/completed
TurnP-->>Client: turn/completed
```
After `thread/start`, the connection is auto-subscribed to turn and item events for that thread. After `turn/start`, keep reading stdout (or your transport reader) for notifications even while the RPC response has already returned.
## Run the server
Default transport is stdio:
```bash
codex app-server
```
Integration tests spawn `codex-app-server` with `CODEX_HOME` set and stdin/stdout piped. Your client should treat the child process the same way: one JSON-RPC object per line on stdin, one object per line on stdout.
<Info>
Set `RUST_LOG` for log verbosity and `LOG_FORMAT=json` for structured logs on stderr. Server diagnostics do not mix into the JSON-RPC stdout stream.
</Info>
## Step 1: Connect and initialize
<Steps>
<Step title="Send initialize">
<RequestExample>
```json
{
"method": "initialize",
"id": 0,
"params": {
"clientInfo": {
"name": "my_integration",
"title": "My Integration",
"version": "0.1.0"
}
}
}
```
</RequestExample>
<ParamField body="clientInfo.name" type="string" required>
HTTP-header-safe client identifier. Real clients use stable names such as `codex_vscode` for compliance logging. Invalid names are rejected before the connection is marked initialized.
</ParamField>
<ParamField body="clientInfo.version" type="string" required>
Client version string; combined with `name` for the upstream user agent suffix on originating clients.
</ParamField>
<ParamField body="capabilities.experimentalApi" type="boolean">
Opt in to experimental RPCs and fields for this connection.
</ParamField>
<ParamField body="capabilities.optOutNotificationMethods" type="string[]">
Exact notification method names to suppress on this connection (for example `item/agentMessage/delta`). Matching is exact; unknown names are ignored.
</ParamField>
</Step>
<Step title="Read the initialize result">
<ResponseExample>
```json
{
"id": 0,
"result": {
"userAgent": "my_integration/0.1.0 …",
"codexHome": "/Users/me/.codex",
"platformFamily": "unix",
"platformOs": "macos"
}
}
```
</ResponseExample>
<ResponseField name="userAgent" type="string">
User agent the server presents to upstream services after initialization.
</ResponseField>
<ResponseField name="codexHome" type="string">
Resolved Codex home directory for this server process.
</ResponseField>
<ResponseField name="platformFamily" type="string">
Runtime platform family (`std::env::consts::FAMILY`).
</ResponseField>
<ResponseField name="platformOs" type="string">
Runtime OS name (`std::env::consts::OS`).
</ResponseField>
**Verify:** `id` matches your request; `codexHome` points at the expected config tree; `userAgent` reflects `clientInfo.name` and `version` for originating clients.
</Step>
<Step title="Send initialized">
Acknowledge the handshake with a client notification (no `params` field):
<RequestExample>
```json
{
"method": "initialized"
}
```
</RequestExample>
The integration harness sends this immediately after a successful `initialize` response. Treat it as part of your client protocol even though the server currently logs inbound client notifications rather than gating RPCs on this message—session readiness is established when `initialize` succeeds.
</Step>
</Steps>
### Initialization errors
| Message | When | Recovery |
| --- | --- | --- |
| `Not initialized` | Any RPC before `initialize` on that connection | Run `initialize` (then `initialized`) before other methods. |
| `Already initialized` | Second `initialize` on the same connection | Open a new transport connection; initialization is once per connection. |
## Step 2: Start a thread
<RequestExample>
```json
{
"method": "thread/start",
"id": 10,
"params": {
"model": "gpt-5.1-codex",
"cwd": "/Users/me/project"
}
}
```
</RequestExample>
<ResponseExample>
```json
{
"id": 10,
"result": {
"thread": {
"id": "67e55044-10b1-426f-9247-bb680e5fe0c8",
"preview": "",
"modelProvider": "openai",
"createdAt": 1730910000
}
}
}
```
</ResponseExample>
You should also receive a server notification:
```json
{ "method": "thread/started", "params": { "thread": { } } }
```
`thread/start` creates the thread, returns it in the RPC result, emits `thread/started` (including current `thread.status`), and auto-subscribes this connection to turn and item notifications for that thread.
**Verify:** Persist `result.thread.id`; confirm `thread/started` arrives unless you opted out via `optOutNotificationMethods`.
## Step 3: Start a turn
<RequestExample>
```json
{
"method": "turn/start",
"id": 30,
"params": {
"threadId": "67e55044-10b1-426f-9247-bb680e5fe0c8",
"clientUserMessageId": "client_msg_1",
"input": [
{ "type": "text", "text": "Run tests" }
]
}
}
```
</RequestExample>
<ResponseExample>
```json
{
"id": 30,
"result": {
"turn": {
"id": "turn_456",
"status": "inProgress",
"items": [],
"error": null
}
}
}
```
</ResponseExample>
`turn/start` returns immediately with a `turn` in `inProgress` and an empty `items` array. Generation continues asynchronously; item data arrives only through notifications.
<ParamField body="threadId" type="string" required>
Target thread from `thread/start` (or `thread/resume` / `thread/fork`).
</ParamField>
<ParamField body="input" type="UserInput[]" required>
Discriminated user inputs: `text`, `image`, `localImage`, `skill`, `mention`, and related variants.
</ParamField>
<ParamField body="clientUserMessageId" type="string">
Optional id echoed on the `userMessage` item as `clientId`.
</ParamField>
## Step 4: Read item and turn notifications
Keep reading the transport after `turn/start`. A minimal successful turn typically includes:
| Order | Method | Purpose |
| --- | --- | --- |
| 1 | `turn/started` | Turn is running; `{ turn }` with `status: "inProgress"`. |
| 2 | `item/started` | New transcript item (for example `userMessage`, `agentMessage`, `commandExecution`). |
| 3 | `item/*` deltas | Streaming updates (`item/agentMessage/delta`, `item/commandExecution/outputDelta`, …). |
| 4 | `item/completed` | Final state for that item id. |
| 5 | `turn/completed` | Terminal turn; `turn.status` is `completed`, `interrupted`, or `failed`. |
Per-item lifecycle is always **`item/started` → zero or more deltas → `item/completed`**. Turn-level bookends are **`turn/started`** and **`turn/completed`**.
Example notification shapes:
```json
{ "method": "turn/started", "params": { "threadId": "…", "turn": { "id": "…", "status": "inProgress", "items": [] } } }
```
```json
{ "method": "item/started", "params": { "threadId": "…", "turnId": "…", "item": { "type": "userMessage", "id": "…" } } }
```
```json
{ "method": "turn/completed", "params": { "threadId": "…", "turn": { "id": "…", "status": "completed", "items": [] } } }
```
<Warning>
`turn/started` and `turn/completed` currently carry an empty `items` array even when item events streamed. Build UI state from `item/*` notifications until that behavior changes.
</Warning>
### Verification checklist
Use this after your first live turn (or when mirroring the integration test pattern):
- [ ] `initialize` returned `userAgent`, `codexHome`, `platformFamily`, and `platformOs`.
- [ ] `thread/start` returned a non-empty `thread.id`.
- [ ] `thread/started` matched that id (unless opted out).
- [ ] `turn/start` returned `turn.status === "inProgress"` and a non-empty `turn.id`.
- [ ] `turn/started` referenced the same `threadId` and `turn.id`.
- [ ] At least one `item/started` / `item/completed` pair appeared for work you care about (for example `userMessage` then `agentMessage`).
- [ ] `turn/completed` arrived with `turn.status === "completed"` (or `interrupted` / `failed` with `turn.error` populated).
Tests in `tests/suite/v2/turn_start.rs` assert `turn/started` then `turn/completed` with matching ids and `TurnStatus::Completed` for mock-model runs. Item-level tests loop on `item/started` until the expected `ThreadItem` variant appears.
## Recovery: `Not initialized`
If any RPC before `initialize` returns:
```json
{
"id": 2,
"error": {
"code": -32600,
"message": "Not initialized"
}
}
```
the connection has not completed the handshake. Fix:
1. Send `initialize` with valid `clientInfo`.
2. Wait for the matching result.
3. Send the `initialized` notification.
4. Retry `thread/start`, `turn/start`, or other methods.
Each transport connection maintains its own session; another client on a different socket cannot initialize for you (verified for websocket in `connection_handling_websocket.rs`). After `"Already initialized"`, spawn a new server connection instead of calling `initialize` again.
<Tip>
If requests fail with code `-32001` and message `"Server overloaded; retry later."`, back off and retry—the server uses bounded ingress queues and treats overload as retryable.
</Tip>
## Minimal client loop (stdio)
```text
spawn: codex app-server
write: {"method":"initialize","id":0,"params":{"clientInfo":{"name":"my_client","version":"0.1.0"}}}
read: initialize result line
write: {"method":"initialized"}
write: {"method":"thread/start","id":10,"params":{"cwd":"/path/to/project"}}
read: thread/start result line
read: thread/started notification line(s)
write: {"method":"turn/start","id":30,"params":{"threadId":"<id>","input":[{"type":"text","text":"Hello"}]}}
read: turn/start result line
loop: read notifications until turn/completed for turn id
```
Match each response and error to the outstanding request `id`. Interleave handling of server-initiated requests (approvals, attestation) if your config triggers them—those can arrive mid-turn.
## Related pages
<CardGroup>
<Card title="Installation" href="/installation">
Launch `codex-app-server`, default listen URLs, and stdout/stderr success signals.
</Card>
<Card title="Protocol and transport" href="/protocol-and-transport">
JSON-RPC framing, transports, health probes, and overload code `-32001`.
</Card>
<Card title="Connection lifecycle" href="/connection-lifecycle">
Per-connection handshake, `clientInfo`, notification opt-out, and subscribe semantics.
</Card>
<Card title="Stream turns and events" href="/stream-turns-and-events">
Full turn/item notification catalog and delta types.
</Card>
<Card title="Build a JSON-RPC client" href="/build-jsonrpc-client">
Framing, request ids, `initialized` ordering, and compliance-oriented client names.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
Deeper diagnosis for init failures, overload, and turn errors.
</Card>
</CardGroup>
---
## 04. Protocol and transport
> JSON-RPC 2.0 wire rules, stdio JSONL vs unix-socket vs websocket listeners, health probes, backpressure code `-32001`, and bounded queue behavior.
- Page Markdown: https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/pages/04-protocol-and-transport.md
- Generated: 2026-06-02T06:36:02.039Z
### Source Files
- `README.md`
- `src/transport.rs`
- `src/main.rs`
- `src/error_code.rs`
- `src/outgoing_message.rs`
- `src/connection_rpc_gate.rs`
---
title: "Protocol and transport"
description: "JSON-RPC 2.0 wire rules, stdio JSONL vs unix-socket vs websocket listeners, health probes, backpressure code `-32001`, and bounded queue behavior."
---
`codex app-server` speaks JSON-RPC–shaped messages over a selectable local transport (`--listen`). The default is newline-delimited JSON on stdio; optional listeners include a unix control socket (WebSocket upgrade over UDS) and an experimental TCP WebSocket bind. Ingress and egress use bounded Tokio channels; when the shared ingress queue is full, new client **requests** fail fast with `-32001` instead of blocking the reader.
## JSON-RPC on the wire
Messages follow JSON-RPC 2.0 semantics, but the `"jsonrpc":"2.0"` field is **omitted** on both send and receive. Each line or WebSocket text frame is one JSON object. The wire uses an untagged union of four shapes:
| Shape | Required fields | Notes |
| --- | --- | --- |
| Request | `id`, `method` | Optional `params`, optional W3C `trace` |
| Response | `id`, `result` | `result` is arbitrary JSON |
| Notification | `method` | Optional `params`; no `id` |
| Error | `id`, `error` | `error` has `code`, `message`, optional `data` |
<ParamField body="id" type="string | number" required>
Request/response correlation id. Server-initiated requests use integer ids allocated by the server.
</ParamField>
<ParamField body="method" type="string" required>
v2 RPCs use `<resource>/<method>` (for example `thread/start`, `turn/completed` as a notification).
</ParamField>
<RequestExample>
```json
{"id":0,"method":"initialize","params":{"clientInfo":{"name":"my_client","title":"My Client","version":"0.1.0"}}}
```
</RequestExample>
<Note>
After `initialize` returns, send an `initialized` notification before other RPCs on that connection. Calls made earlier receive `"Not initialized"` (`-32600`); repeat `initialize` receives `"Already initialized"`.
</Note>
Server output mirrors the same shapes: RPC responses and errors, method-named notifications (`turn/started`, `item/agentMessage/delta`, …), and server **requests** (approvals, elicitations) that expect a client response.
## `--listen` transports
<ParamField body="--listen" type="URL" required>
Parsed by `AppServerTransport::from_listen_url`. Default: `stdio://`.
</ParamField>
| URL | Behavior | Framing |
| --- | --- | --- |
| `stdio://` (default) | Single stdin/stdout connection; process exits when stdio closes | One JSON object per line (JSONL); trailing `\n` on write |
| `unix://` | Default path: `$CODEX_HOME/app-server-control/app-server-control.sock` | Raw byte stream → HTTP Upgrade → WebSocket frames |
| `unix://PATH` | Custom socket path; startup lock under `$CODEX_HOME/app-server-control/` | Same as `unix://` |
| `ws://IP:PORT` | TCP listener + HTTP routes (experimental) | One JSON-RPC object per WebSocket **text** frame |
| `off` | No local listener | Use with remote control or in-process embedding |
<Warning>
WebSocket transport (`ws://`) is experimental and unsupported for production. Non-loopback binds require `--ws-auth` (`capability-token` or `signed-bearer-token`).
</Warning>
### Stdio JSONL
The stdio reader uses `BufReader::lines()`: each non-empty line is deserialized as `JSONRPCMessage`. The writer serializes each outgoing message to JSON, appends `\n`, and writes to stdout. This is the default integration path for editor extensions and subprocess clients.
### Unix control socket
`unix://` accepts connections on a private socket (mode `0600` on Unix). Each accepted stream is upgraded with `tokio_tungstenite::accept_async` and then handled like a WebSocket connection (text frames, optional disconnect token). `codex app-server proxy` bridges one raw socket to stdin/stdout for tools that only speak JSONL.
### WebSocket listener
The `ws://` acceptor serves:
- `GET /readyz` → `200 OK` once the listener is accepting connections
- `GET /healthz` → `200 OK` when the request has **no** `Origin` header
- Any request **with** an `Origin` header → `403 Forbidden`
- All other HTTP requests → WebSocket upgrade (subject to auth policy)
Inbound **binary** WebSocket frames are logged and dropped. Ping frames are answered with pong when the control queue has capacity.
## Runtime data path
```mermaid
flowchart TB
subgraph clients [Clients]
STDIO[stdio JSONL]
UDS[unix socket + WS upgrade]
WS[ws:// listener]
end
subgraph transport [Transport tasks]
READ[Read / parse JSON]
WRITE[Per-connection writer queue]
end
subgraph core [app-server core]
EVT["transport_event_rx (bounded)"]
PROC[MessageProcessor + ConnectionRpcGate]
OUT["outgoing_rx router (bounded)"]
end
STDIO --> READ
UDS --> READ
WS --> READ
READ -->|try_send IncomingMessage| EVT
EVT --> PROC
PROC --> OUT
OUT --> WRITE
WRITE --> STDIO
WRITE --> UDS
WRITE --> WS
```
On startup, `run_main_with_transport_options` creates three bounded channels sized to **`CHANNEL_CAPACITY` (128)**:
1. `transport_event_tx` — ingress from all connections
2. `outgoing_tx` — envelopes to the outbound router
3. `outbound_control_tx` — per-connection registration and disconnect-all
Each connection also gets its own `mpsc` writer queue (128 for stdio/unix-socket paths; **32 768** for WebSocket outbound, so burst turn traffic can buffer without tripping ingress overload as quickly).
## Backpressure and `-32001`
When the ingress queue (`transport_event_tx`) is full:
| Incoming message | Server behavior |
| --- | --- |
| **Request** | Respond immediately on the connection with JSON-RPC error code **`-32001`**, message **`Server overloaded; retry later.`** (no `data`). Does not block the transport reader. |
| Notification, response, or client error | **Await** space on the ingress queue (`send().await`) — backpressure propagates to the reader instead of dropping |
If the per-connection outbound queue is also full when sending the overload error, the error response may be **dropped** (logged); the reader still continues.
<ResponseExample>
```json
{"id":7,"error":{"code":-32001,"message":"Server overloaded; retry later."}}
```
</ResponseExample>
<Tip>
Treat `-32001` as retryable: exponential backoff with jitter. Only **requests** get this fast-fail path; high-volume notifications use blocking ingress instead.
</Tip>
### Slow WebSocket clients
Connections opened with a `disconnect_sender` (WebSocket and unix-upgraded paths) use `try_send` on the per-connection outbound queue. If the queue is full, the server **disconnects** that connection rather than blocking broadcasts to other clients. Stdio has no disconnect token; its writer uses blocking `send().await`.
Initialized RPC handlers run under a per-connection **`ConnectionRpcGate`**: closing the gate stops new handlers after shutdown while in-flight work finishes (`TaskTracker`).
## Standard JSON-RPC error codes
Defined in `error_code.rs` and used across handlers:
| Code | Constant | Typical use |
| --- | --- | --- |
| `-32600` | invalid request | Uninitialized connection, bad handshake, duplicate request id |
| `-32601` | method not found | Unknown `method` |
| `-32602` | invalid params | Schema/validation failures; `turn/start` input size (see below) |
| `-32603` | internal error | Unexpected server failures |
| `-32001` | overloaded | Ingress queue saturated (see above) |
Application-level **`input_too_large`** is not a JSON-RPC code. It appears in `error.data` on `-32602` responses from `turn/start` and `turn/steer` when user text exceeds the protocol limit:
<ResponseExample>
```json
{
"id": 1,
"error": {
"code": -32602,
"message": "Input exceeds the maximum length of … characters.",
"data": {
"input_error_code": "input_too_large",
"max_chars": …,
"actual_chars": …
}
}
}
```
</ResponseExample>
## Logging and verification
| Variable | Effect |
| --- | --- |
| `RUST_LOG` | Tracing filter/verbosity |
| `LOG_FORMAT=json` | One JSON log event per line on stderr |
**Stdio health:** process alive and reading lines; first `initialize` may be scraped for `clientInfo.name`.
**WebSocket health:** after `ws://HOST:PORT` bind:
```bash
curl -s -o /dev/null -w "%{http_code}" "http://HOST:PORT/readyz"
curl -s -o /dev/null -w "%{http_code}" "http://HOST:PORT/healthz"
```
Expect `200` when the listener is up. Requests that include `Origin` should receive `403` on both probe paths.
**Overload:** under synthetic load, concurrent client requests should occasionally receive `-32001` while the process stays responsive (no unbounded memory growth from ingress).
## Related pages
<CardGroup>
<Card title="Installation" href="/installation">
Launch `codex-app-server`, default URLs, and environment variables.
</Card>
<Card title="Build a JSON-RPC client" href="/build-jsonrpc-client">
Newline framing, request ids, and server requests mid-turn.
</Card>
<Card title="Transports and proxy" href="/transports-and-proxy">
Choosing listeners, control socket path, and `codex app-server proxy`.
</Card>
<Card title="Connection lifecycle" href="/connection-lifecycle">
`initialize` / `initialized`, notification opt-out, and subscribe model.
</Card>
<Card title="CLI flags and error codes" href="/cli-flags-and-errors">
Full flag list, auth, strict config, and troubleshooting overload.
</Card>
</CardGroup>
---
## 05. Threads, turns, and items
> Core primitives (`Thread`, `Turn`, `ThreadItem`), subscription model, thread status states, ephemeral threads, and how persisted rollouts relate to in-memory loaded threads.
- Page Markdown: https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/pages/05-threads-turns-and-items.md
- Generated: 2026-06-02T06:37:06.676Z
### Source Files
- `README.md`
- `src/thread_state.rs`
- `src/thread_status.rs`
- `src/request_processors/thread_processor.rs`
- `src/request_processors/turn_processor.rs`
- `src/request_processors/thread_lifecycle.rs`
- `src/bespoke_event_handling.rs`
---
title: "Threads, turns, and items"
description: "Core primitives (`Thread`, `Turn`, `ThreadItem`), subscription model, thread status states, ephemeral threads, and how persisted rollouts relate to in-memory loaded threads."
---
`codex app-server` models every client conversation as a **thread** of **turns**, each turn composed of typed **items** streamed over JSON-RPC notifications. The v2 wire types live in `codex-app-server-protocol`; runtime ownership splits between in-memory `CodexThread` instances (`ThreadManager`), per-connection subscriptions (`ThreadStateManager`), and on-disk rollout JSONL (`thread_store` / `thread/list`).
## Thread, turn, and item
| Primitive | Role | Typical RPC / notification |
|-----------|------|---------------------------|
| `Thread` | One user↔agent conversation; holds metadata, optional `turns[]`, runtime `status` | `thread/start`, `thread/resume`, `thread/read`, `thread/started` |
| `Turn` | One exchange (user input → agent completion); owns `items[]` and `status` | `turn/start`, `turn/started`, `turn/completed` |
| `ThreadItem` | Atomic transcript unit inside a turn (message, tool, patch, etc.) | `item/started`, `item/completed`, `item/*/delta` |
A **thread** is identified by `id` (UUID string), shares a `sessionId` with forked siblings, and may reference `forkedFromId` or `parentThreadId` (subagents). Important fields on the wire `Thread` object:
- `preview` — usually the first user message when known
- `ephemeral` — in-memory-only session; `path` is `null` when true
- `status` — runtime load/activity state (see below)
- `path` — rollout file on disk when persisted
- `turns` — populated only on `thread/resume`, `thread/rollback`, `thread/fork`, and `thread/read` with `includeTurns: true`; otherwise an empty list on other responses
A **turn** carries `id`, `status`, timestamps, optional `error`, and `items`. `itemsView` tells clients how much history was loaded: `notLoaded`, `summary`, or `full` (default for persisted full history).
**Turn status** values:
| `TurnStatus` | Meaning |
|--------------|---------|
| `inProgress` | Turn is running (returned immediately from `turn/start`; confirmed by `turn/started`) |
| `completed` | Finished successfully |
| `interrupted` | Cancelled via `turn/interrupt` |
| `failed` | Ended with `TurnError` (message + optional `codexErrorInfo`) |
**ThreadItem** is a tagged union (`type` on the wire). Variants include `userMessage`, `agentMessage`, `reasoning`, `commandExecution`, `fileChange`, `mcpToolCall`, `dynamicToolCall`, `collabAgentToolCall`, `webSearch`, `imageView`, `imageGeneration`, `enteredReviewMode`, `exitedReviewMode`, `contextCompaction`, `hookPrompt`, and `plan` (experimental). Items are the unit of `item/started` / `item/completed` notifications and optional delta streams (`item/agentMessage/delta`, command output deltas, etc.).
```mermaid
classDiagram
class Thread {
+String id
+String sessionId
+bool ephemeral
+ThreadStatus status
+Option~PathBuf~ path
+Vec~Turn~ turns
}
class Turn {
+String id
+TurnStatus status
+Vec~ThreadItem~ items
+TurnItemsView itemsView
}
class ThreadItem {
<<union>>
userMessage
agentMessage
commandExecution
fileChange
...
}
Thread "1" --> "*" Turn : contains
Turn "1" --> "*" ThreadItem : contains
```
## Loaded threads vs persisted rollouts
App-server keeps two related views of the same logical conversation:
```text
Client RPC In-memory (app-server) On disk
----------- ---------------------- --------
thread/start ──────────────► CodexThread + listener task
thread/resume ─────────────► (loads rollout into CodexThread)
thread/read ──────────────► (optional) ───────────────────────► rollout JSONL
thread/list ──────────────────────────────────────────────────► sqlite index + rollout files
thread/loaded/list ────────► ThreadManager (ids only)
```
**Persisted threads** materialize rollout files under the Codex home sessions directory. `thread/list` pages stored rollouts (cursor pagination, filters for `cwd`, `archived`, `searchTerm`, etc.). Each listed thread includes `status`, defaulting to `notLoaded` when that thread id is not currently loaded in memory.
**Loaded threads** are live `CodexThread` instances held by `ThreadManager`. `thread/loaded/list` returns only ids currently in memory. While loaded, the server runs a per-thread **listener task** that reads `conversation.next_event()`, translates core events in `bespoke_event_handling`, and emits v2 notifications to subscribed connections.
**Read without resume:** `thread/read` fetches metadata (and optional turn history from rollout) without attaching a listener or loading into `ThreadManager`. Returned `status` is typically `notLoaded` even when history is included.
**Resume / start / fork** load (or create) the in-memory thread, attach listeners, and auto-subscribe the calling connection to turn/item notifications.
<Note>
Ephemeral threads skip disk persistence in core session init: no `LiveThread`, no state DB, and `thread.path` stays `null`. They also reject `includeTurns` on `thread/read` and `thread/turns/list`.
</Note>
## Subscription model
Subscriptions are **per transport connection**, not global. `ThreadStateManager` tracks:
- `live_connections` — initialized connections eligible to subscribe
- `threads[threadId].connection_ids` — which connections receive that thread’s notifications
- `thread_ids_by_connection` — reverse index for cleanup on disconnect
**Auto-subscribe** happens when a connection starts, resumes, or forks a thread: `ensure_conversation_listener` registers the connection, starts (or reuses) the listener task, and routes outbound events through `ThreadScopedOutgoingMessageSender`, which filters notifications to `subscribed_connection_ids` only.
```mermaid
sequenceDiagram
participant Client
participant AppServer
participant Listener
participant CodexThread
Client->>AppServer: thread/start (connection C)
AppServer->>AppServer: try_ensure_connection_subscribed
AppServer->>Listener: spawn listener task
Client->>AppServer: turn/start
CodexThread-->>Listener: next_event()
Listener->>Client: item/started (only if C subscribed)
```
**`thread/unsubscribe`** removes the current connection from `connection_ids`. Response `status`:
| Status | Meaning |
|--------|---------|
| `unsubscribed` | Connection was subscribed and is now removed |
| `notSubscribed` | Connection was not subscribed |
| `notLoaded` | Thread is not loaded |
When the **last subscriber** drops off, the thread **stays loaded**. Unload runs only after **both** no subscribers **and** no `active` thread status for **30 minutes** (`THREAD_UNLOADING_DELAY` in `thread_lifecycle.rs`). Then the server shuts down `CodexThread`, emits `thread/closed`, and `thread/status/changed` → `notLoaded`.
Disconnecting a transport removes that connection from all threads; threads with zero subscribers enter the same idle-unload path.
<Tip>
`thread/unsubscribe` during an in-flight turn does not stop the turn; it only stops notifications to that connection. Integration tests confirm the thread remains in `thread/loaded/list` until the idle timeout.
</Tip>
## Thread status
`Thread.status` on the wire is a separate concern from `Turn.status`. It describes **whether the thread is loaded in app-server** and **what it is doing right now**, maintained by `ThreadWatchManager` from runtime facts (running turn, pending approvals, pending user input, system error).
```mermaid
stateDiagram-v2
[*] --> notLoaded: thread unloaded / never tracked
notLoaded --> idle: upsert_thread (loaded, idle)
idle --> active: turn started OR pending approval/input
active --> idle: turn completed / guards released
active --> systemError: note_system_error
systemError --> active: next turn started
idle --> notLoaded: remove_thread after unload
active --> notLoaded: remove_thread after unload
```
| `ThreadStatus` | When |
|----------------|------|
| `notLoaded` | Thread not in watch state (default for `thread/list` / `thread/read` of stored-only threads) |
| `idle` | Loaded, not running, no pending server requests |
| `active` | Turn running and/or `activeFlags` non-empty |
| `systemError` | Last turn ended in a system error state until the next turn starts |
**`activeFlags`** (only on `active`):
- `waitingOnApproval` — command/file/permissions approval server request outstanding
- `waitingOnUserInput` — `tool/requestUserInput` outstanding
`thread/status/changed` notifications fire on transitions after a thread is known to the client. `thread/start` / `thread/fork` / detached review threads include current `status` on `thread/started` instead of a separate initial status notification.
`resolve_thread_status` bridges a race: if a turn is in progress but watch state still says `idle` or `notLoaded`, clients see `active` with empty `activeFlags`.
## Ephemeral threads
Pass `ephemeral: true` on `thread/start` or `thread/fork` for a session that must not be materialized on disk.
| Behavior | Persisted thread | Ephemeral thread |
|----------|------------------|------------------|
| Rollout file | Created under sessions dir | None (`path: null`) |
| `thread/list` | Appears after persistence | Not listed from disk |
| `thread/read` + `includeTurns` | Supported from rollout | **Rejected** |
| `thread/turns/list` | Supported (experimental) | **Rejected** |
| Goals / some metadata RPCs | Full sqlite-backed features | Reduced (core rejects metadata updates) |
The response field `thread.ephemeral` reflects `config_snapshot.ephemeral` from the loaded `CodexThread`.
## Turn and item streaming path
After `turn/start`, the RPC response includes an initial `turn` (usually `inProgress`). Streaming proceeds asynchronously:
<Steps>
<Step title="Subscribe and listen">
Ensure the connection is subscribed (automatic on `thread/start` / `thread/resume` / `thread/fork`). Read notifications on the transport.
</Step>
<Step title="Turn boundaries">
`turn/started` when the core emits `TurnStarted`; `turn/completed` on `TurnComplete` or interrupt handling. `ThreadWatchManager` updates status in parallel.
</Step>
<Step title="Items">
`item/started` / `item/completed` (and deltas) are produced in `bespoke_event_handling` from core `EventMsg` values. `ThreadState` tracks the active turn via `ThreadHistoryBuilder` for snapshots and interrupt/rollback ordering.
</Step>
<Step title="Server requests mid-turn">
Approvals and user-input requests go only to subscribed connections on that thread, ordered through the listener command channel when resolved.
</Step>
</Steps>
`turn/steer` appends user input to an **already running** regular turn (returns active `turnId`). Review and manual compaction turns reject steer.
## Persistence and history APIs
| Method | Loads in memory? | History source |
|--------|------------------|----------------|
| `thread/start` | Yes (new) | Empty, then live events |
| `thread/resume` | Yes | Rollout + live events |
| `thread/fork` | Yes (new id) | Copied history; may interrupt mid-turn source |
| `thread/read` | No | Rollout when `includeTurns: true` |
| `thread/turns/list` | No | Rollout pagination (experimental) |
| `thread/rollback` | Yes | Truncates in-memory context + rollout marker |
Rollout items (`RolloutItem` in core) are converted to API `Turn` / `ThreadItem` via builders such as `build_turns_from_rollout_items` / `build_api_turns_from_rollout_items`. Token usage replay and resume redaction are handled in dedicated processors without changing the wire shapes.
## Implementation map
| Concern | Primary module |
|---------|----------------|
| Per-connection subscribe/unsubscribe | `src/thread_state.rs` (`ThreadStateManager`) |
| `ThreadStatus` + `thread/status/changed` | `src/thread_status.rs` (`ThreadWatchManager`) |
| Listener, idle unload, `thread/closed` | `src/request_processors/thread_lifecycle.rs` |
| Thread CRUD, list/read/resume | `src/request_processors/thread_processor.rs` |
| `turn/start`, steer, interrupt | `src/request_processors/turn_processor.rs` |
| Event → notification translation | `src/bespoke_event_handling.rs` |
| Wire types | `app-server-protocol/src/protocol/v2/thread_data.rs`, `thread.rs`, `item.rs`, `turn.rs` |
## Related pages
<CardGroup>
<Card title="Quickstart" href="/quickstart">
End-to-end `thread/start` → `turn/start` → `item/*` → `turn/completed`.
</Card>
<Card title="Connection lifecycle" href="/connection-lifecycle">
Initialize, subscribe/unsubscribe, and server-initiated requests.
</Card>
<Card title="Stream turns and events" href="/stream-turns-and-events">
Notification catalog, deltas, and opt-out.
</Card>
<Card title="Thread lifecycle examples" href="/thread-lifecycle-examples">
Copy-paste JSON-RPC sequences including idle unload.
</Card>
<Card title="RPC methods reference" href="/rpc-methods">
Full v2 method list with experimental markers.
</Card>
</CardGroup>
---
## 06. Connection lifecycle
> Per-connection `initialize` handshake, `clientInfo` requirements, notification opt-out, thread subscribe/unsubscribe idle unload, and server-initiated requests vs client RPC.
- Page Markdown: https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/pages/06-connection-lifecycle.md
- Generated: 2026-06-02T06:37:28.781Z
### Source Files
- `README.md`
- `src/request_processors/initialize_processor.rs`
- `src/message_processor.rs`
- `src/transport.rs`
- `src/connection_rpc_gate.rs`
- `tests/suite/v2/client_metadata.rs`
---
title: "Connection lifecycle"
description: "Per-connection `initialize` handshake, `clientInfo` requirements, notification opt-out, thread subscribe/unsubscribe idle unload, and server-initiated requests vs client RPC."
---
Each transport connection to `codex app-server` is an isolated JSON-RPC session: one `initialize` handshake per connection, per-connection capability negotiation, thread event subscriptions scoped to that connection, and bidirectional RPC where the server can emit client-bound requests mid-turn. `MessageProcessor` and `ConnectionSessionState` in `src/message_processor.rs` gate client RPC until initialization completes; `src/transport.rs` applies per-connection notification filters on the outbound path.
## Per-connection state model
```mermaid
stateDiagram-v2
[*] --> Open: Transport connects
Open --> Initialized: initialize succeeds
Initialized --> Initialized: Client RPC + server requests
Initialized --> Draining: Connection closes
Draining --> [*]: ConnectionRpcGate waits for in-flight handlers
note right of Open
RPC other than initialize
returns "Not initialized"
end note
note right of Initialized
outbound_initialized enables
broadcast notifications
end note
```
| Layer | Responsibility |
| --- | --- |
| `ConnectionSessionState` | Stores `initialize` outcome: experimental API flag, opt-out set, `clientInfo`, attestation capability |
| `ConnectionRpcGate` | Serializes handler shutdown: no new work after close; in-flight handlers finish |
| `ThreadStateManager` | Maps connections ↔ loaded threads (subscriptions) |
| `OutboundConnectionState` | Per-connection outbound queue, `initialized` flag, opt-out filter, experimental API flag |
Connections do not share request IDs. A websocket integration test confirms that `initialize` responses and later RPC responses route only to the connection that sent them.
## Initialize handshake
<Steps>
<Step title="Send `initialize` (once per connection)">
Send a single JSON-RPC request with method `initialize`. This must be the first client RPC on that connection.
<RequestExample>
```json
{
"method": "initialize",
"id": 0,
"params": {
"clientInfo": {
"name": "my_integration",
"title": "My Integration",
"version": "1.0.0"
},
"capabilities": {
"experimentalApi": true,
"requestAttestation": false,
"optOutNotificationMethods": ["thread/started", "item/agentMessage/delta"]
}
}
}
```
</RequestExample>
</Step>
<Step title="Read `initialize` response">
On success the server returns:
<ResponseField name="userAgent" type="string">
User agent string presented to upstream services (derived from `clientInfo.name` and `version` for originating clients).
</ResponseField>
<ResponseField name="codexHome" type="string">
Absolute path to the server’s Codex home directory.
</ResponseField>
<ResponseField name="platformFamily" type="string">
Runtime platform family (for example `unix`, `windows`).
</ResponseField>
<ResponseField name="platformOs" type="string">
Runtime OS (for example `macos`, `linux`, `windows`).
</ResponseField>
</Step>
<Step title="Emit `initialized` notification">
After receiving the `initialize` response, send a client notification (no `id` field):
```json
{
"method": "initialized"
}
```
Official test clients and in-process embedders follow this ordering. The server’s RPC gate opens when `initialize` completes; the `initialized` notification is part of the documented client contract.
</Step>
<Step title="Receive post-init server notifications">
For socket transports, the server then enables outbound delivery on that connection and may send connection-scoped notifications such as `configWarning` (from config parse warnings) and `remoteControl/statusChanged`. Broadcast notifications are delivered only to connections whose outbound `initialized` flag is set.
</Step>
</Steps>
### Errors and constraints
| Condition | JSON-RPC error |
| --- | --- |
| Any client RPC before `initialize` succeeds | `"Not initialized"` (`-32600` invalid request) |
| Second `initialize` on the same connection | `"Already initialized"` |
| `clientInfo.name` is not a valid HTTP header value | `"Invalid clientInfo.name: '…'. Must be a valid HTTP header value."` |
| Experimental method without `capabilities.experimentalApi` | Experimental-required message from handler |
## `clientInfo` requirements
<ParamField body="clientInfo.name" type="string" required>
Stable client identifier. Must be a valid HTTP header value (validated before session state is committed). Used for upstream user-agent construction and compliance logging. Enterprise integrations should use a known, registered name.
</ParamField>
<ParamField body="clientInfo.title" type="string">
Human-readable client title (optional).
</ParamField>
<ParamField body="clientInfo.version" type="string" required>
Client version string; appended to the user-agent suffix as `{name}; {version}`.
</ParamField>
### Originator and user-agent behavior
| `clientInfo.name` | Process-global originator |
| --- | --- |
| Normal clients (for example `codex_vscode`, `xcode`) | Sets default originator and user-agent suffix |
| `codex_app_server_daemon` | Does **not** override originator (probe/daemon clients) |
| `codex-backend` | Does **not** override originator |
`CODEX_INTERNAL_ORIGINATOR_OVERRIDE` can pre-set the originator; a later `initialize` from a normal client does not replace an already-set originator.
<Note>
`clientInfo.name` is forwarded into turn notify payloads and upstream metadata. Pick a stable, unique name per product surface.
</Note>
## Capabilities negotiated at initialize
<ParamField body="capabilities.experimentalApi" type="boolean">
When `true`, experimental v2 RPCs and notification fields are allowed on this connection. When `false`, experimental server notifications are dropped on outbound and experimental RPCs are rejected.
</ParamField>
<ParamField body="capabilities.requestAttestation" type="boolean">
When `true`, this connection is eligible to receive `attestation/generate` server requests for upstream `x-oai-attestation`. The first attestation-capable subscriber on a thread is preferred.
</ParamField>
<ParamField body="capabilities.optOutNotificationMethods" type="string[]">
Exact notification method names to suppress for this connection only. Matching is exact (no wildcards or prefixes). Unknown names are accepted and ignored.
</ParamField>
### Notification opt-out
Opt-out is evaluated in `src/transport.rs` when enqueueing outbound messages:
- Suppressed if the method is listed in `optOutNotificationMethods`.
- Suppressed if the notification is experimental and `experimentalApi` is `false` on the connection.
Integration tests confirm that `thread/started` does not arrive when opted out, while the `thread/start` RPC response still returns normally.
<Warning>
Opt-out affects **notifications only**, not RPC responses. High-volume streams (`item/agentMessage/delta`, command output deltas) are typical opt-out candidates.
</Warning>
## Thread subscribe and unsubscribe
### Auto-subscribe
`thread/start`, `thread/resume`, and `thread/fork` **auto-subscribe** the calling connection to that thread’s turn/item notifications. The server attaches a per-thread listener via `ensure_conversation_listener` and records the connection in `ThreadStateManager`.
Thread-scoped events (including server-initiated approval requests) are sent only to **subscribed** connections for that `threadId`.
### `thread/unsubscribe`
Call `thread/unsubscribe` with `{ "threadId": "…" }` to stop receiving turn/item notifications on this connection without necessarily unloading the thread from memory.
| `status` | Meaning |
| --- | --- |
| `unsubscribed` | Connection was subscribed and is now removed |
| `notSubscribed` | Connection was not subscribed to that thread |
| `notLoaded` | Thread is not loaded in memory |
<Info>
`thread/unsubscribe` during an in-flight turn stops **notifications** to that connection but does not cancel the turn. Server-initiated requests for in-flight work can still require a response on the same connection if the handler already issued them.
</Info>
### Idle unload (`thread/closed`)
After the last subscriber unsubscribes, the thread **stays loaded** until **both**:
1. No connections are subscribed (`has_subscribers == false`), and
2. Thread status is not `active` (no running turn),
for a continuous **`THREAD_UNLOADING_DELAY` of 30 minutes** (`src/request_processors/thread_lifecycle.rs`).
Then the server shuts down the in-memory thread, cancels pending server→client requests for that thread, and emits `thread/closed` with `{ "threadId": "…" }`. Tests verify that `thread/closed` does **not** fire immediately after `thread/unsubscribe`; `thread/loaded/list` still lists the thread until idle timeout.
```text
thread/start ──► auto-subscribe connection
│
▼
thread/unsubscribe ──► status: unsubscribed (thread still loaded)
│
▼
30 min idle (no subscribers + not active)
│
▼
thread/closed + memory released
```
## Connection teardown
When a transport connection closes:
1. `ConnectionRpcGate::shutdown` stops accepting new handler work and waits for in-flight RPC handlers.
2. `OutgoingMessageSender::connection_closed` clears pending server-request callbacks for that connection.
3. `ThreadStateManager::remove_connection` unsubscribes the connection from all threads.
4. FS watches, command exec sessions, and process exec state for that connection are cleared.
If a connection drops while a server request is outstanding, the client should treat the request as abandoned; the server cancels thread-scoped pending requests on unload.
## Client RPC vs server-initiated requests
Codex app-server uses **bidirectional JSON-RPC 2.0** (JSONL on stdio, one message per websocket frame on socket transports).
| Direction | Initiator | Wire shape | Examples |
| --- | --- | --- | --- |
| Client → server | Client | Request with client-chosen `id` | `initialize`, `thread/start`, `turn/start`, `config/read` |
| Server → client | Server | Request with server-chosen `id` | `item/commandExecution/requestApproval`, `item/tool/call`, `mcpServer/elicitation/request` |
| Either | Peer | Response matching `id` | Initialize result; approval `{ "decision": "accept" }` |
| Server → client | Server | Notification (no `id`) | `turn/started`, `item/started`, `serverRequest/resolved` |
### Server request catalog (v2)
| Method | Purpose |
| --- | --- |
| `item/commandExecution/requestApproval` | Shell/command approval during a turn |
| `item/fileChange/requestApproval` | Patch/file-change approval |
| `item/tool/requestUserInput` | Structured user input for a tool (experimental) |
| `item/permissions/requestApproval` | Permission profile selection |
| `item/tool/call` | Dynamic tool execution on the client |
| `mcpServer/elicitation/request` | MCP elicitation (form or URL) |
| `account/chatgptAuthTokens/refresh` | ChatGPT auth token refresh |
| `attestation/generate` | Upstream attestation token (requires `requestAttestation`) |
### Handling server requests in your client
<Steps>
<Step title="Read JSON-RPC requests on the same stream as notifications">
While waiting for `turn/completed`, your reader must handle interleaved **server requests**. Do not assume the stream is notification-only.
</Step>
<Step title="Correlate with `threadId` / `turnId`">
Approval and tool requests include `threadId` and usually `turnId`. Scope UI to the active conversation.
</Step>
<Step title="Respond with a JSON-RPC response">
Reply with the same `id` and a result object matching the request’s response schema (for example `{ "decision": "accept" }` for command approval).
</Step>
<Step title="Observe `serverRequest/resolved`">
After your response—or if the turn ends or is interrupted before you answer—the server emits `serverRequest/resolved` with `{ "threadId", "requestId" }` so clients can clear pending UI state.
</Step>
</Steps>
Server requests for a thread are targeted at **subscribed** connections. On `thread/resume`, the server may **replay** still-pending server requests to the resuming connection after the resume response.
### `ConnectionRpcGate` and in-flight RPC
After `initialize`, each client RPC runs through `ConnectionRpcGate::run`. On connection close the gate rejects new handlers while allowing started handlers to finish, avoiding torn state during teardown.
Some RPCs use serialization queues (per connection or per thread) so related operations do not overlap; most handlers are spawned concurrently behind the gate.
## Transport-specific notes
| Transport | Handshake notes |
| --- | --- |
| stdio (default) | One process-wide stream; `outbound_initialized` flips when `initialize` completes in `lib.rs` |
| unix control socket / websocket | Per-connection `ConnectionState`; initialize response is connection-scoped |
| In-process (`in_process`) | `start()` sends `initialize` + `initialized` and sets `outbound_initialized` inside the shared handler |
Websocket clients must call `initialize` per websocket connection before any other RPC on that socket.
## Verification checklist
| Check | Expected signal |
| --- | --- |
| Pre-init RPC | Error message `"Not initialized"` |
| Successful init | `userAgent`, `codexHome`, `platformFamily`, `platformOs` in response |
| Opt-out | Listed notification methods never arrive on that connection |
| `thread/unsubscribe` | `status: "unsubscribed"`; no immediate `thread/closed` |
| Idle unload | `thread/closed` after 30 minutes with no subscribers and idle thread |
| Server approval | Incoming request with `id`; reply; then `serverRequest/resolved` |
## Related pages
<CardGroup>
<Card title="Quickstart" href="/quickstart">
End-to-end first connection from `initialize` through `turn/completed`.
</Card>
<Card title="Protocol and transport" href="/protocol-and-transport">
JSON-RPC framing, transports, overload `-32001`, and queue backpressure.
</Card>
<Card title="Build a JSON-RPC client" href="/build-jsonrpc-client">
Framing, request IDs, `initialized` ordering, and mid-turn server requests.
</Card>
<Card title="Threads, turns, and items" href="/threads-turns-items">
Thread subscription model and thread status states.
</Card>
<Card title="Approvals and server requests" href="/approvals-and-server-requests">
Approval flows, MCP elicitations, and `serverRequest/resolved` lifecycle.
</Card>
<Card title="Stream turns and events" href="/stream-turns-and-events">
Turn/item notifications and opt-out for high-volume streams.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
`Not initialized`, overload retries, and connection errors.
</Card>
</CardGroup>
---
## 07. Experimental API
> Runtime opt-in via `capabilities.experimentalApi`, stable vs experimental schema generation, rejection messages, and maintainer gating patterns for fields and notifications.
- Page Markdown: https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/pages/07-experimental-api.md
- Generated: 2026-06-02T06:37:14.024Z
### Source Files
- `README.md`
- `src/transport.rs`
- `src/filters.rs`
- `src/request_serialization.rs`
- `tests/suite/v2/thread_rollback.rs`
- `tests/suite/v2/compaction.rs`
---
title: "Experimental API"
description: "Runtime opt-in via `capabilities.experimentalApi`, stable vs experimental schema generation, rejection messages, and maintainer gating patterns for fields and notifications."
---
App-server v2 exposes a subset of RPC methods, request fields, server notifications, and server-initiated request payloads behind a per-connection capability negotiated at `initialize`. Clients that omit opt-in receive JSON-RPC `Invalid Request` (`-32600`) when they call gated methods or set gated fields; opted-out connections never receive experimental notifications and may receive stripped fields on certain server requests. Schema generators mirror the same split: stable artifacts by default, full surface with `--experimental`.
## Capability negotiation
`initialize.params.capabilities.experimentalApi` is a boolean on `InitializeCapabilities` (wire name `experimentalApi`, default `false` when `capabilities` is omitted).
<ParamField body="experimentalApi" type="boolean">
Opt into experimental RPC methods, experimental request/response fields, experimental server notifications, and experimental fields on server-initiated requests for this connection.
</ParamField>
<Steps>
<Step title="Send initialize with opt-in">
```json
{
"method": "initialize",
"id": 1,
"params": {
"clientInfo": {
"name": "my_client",
"title": "My Client",
"version": "0.1.0"
},
"capabilities": {
"experimentalApi": true
}
}
}
```
</Step>
<Step title="Send initialized">
Emit the standard `initialized` notification before any other RPC on the connection.
</Step>
<Step title="Use experimental surface">
Call experimental methods and set experimental fields only after the handshake completes successfully.
</Step>
</Steps>
<Note>
`experimentalApi` is stored per connection in `ConnectionSessionState` and propagated to outbound writers when initialization completes. A second `initialize` on the same connection returns `"Already initialized"`. There is an open design note to revisit instance-global first-write-wins scoping when multiple clients share threads.
</Note>
Integration tests under `tests/suite/v2/` typically opt in via `TestAppServer::initialize_with_client_info`, which passes `experimental_api: true` by default—do not assume production clients do the same.
## Three runtime enforcement layers
| Layer | When it runs | Behavior without `experimentalApi` |
| --- | --- | --- |
| Incoming client RPC | Before handler dispatch in `MessageProcessor` | Reject with `-32600` and message `{reason} requires experimentalApi capability` |
| Outgoing notifications | In `route_outgoing_envelope` / `should_skip_notification_for_connection` | Drop notifications whose type reports an experimental reason (silent skip) |
| Outgoing server requests | In `filter_outgoing_message_for_connection` | Strip known experimental fields (today: `additionalPermissions` on command-execution approvals) |
```mermaid
sequenceDiagram
participant Client
participant Transport as transport.rs
participant MP as message_processor.rs
participant Handler as request processor
Client->>Transport: initialize (experimentalApi: true/false)
Transport->>MP: store capability on session
Client->>Transport: thread/realtime/start (example)
Transport->>MP: dispatch_initialized_client_request
alt experimental_reason and not opted in
MP-->>Client: JSON-RPC error -32600
else allowed
MP->>Handler: handle request
Handler-->>Transport: result / notifications
alt experimental notification and not opted in
Transport-->>Client: (notification dropped)
else opted in or stable notification
Transport-->>Client: notification delivered
end
end
```
### Incoming RPC rejection
After `initialize`, every client request passes through `ClientRequest::experimental_reason()`. When a reason is present and `session.experimental_api_enabled()` is false, the server returns `invalid_request` with code `-32600` (JSON-RPC Invalid Request).
<RequestExample>
```json
{
"jsonrpc": "2.0",
"id": 42,
"error": {
"code": -32600,
"message": "thread/realtime/start requires experimentalApi capability"
}
}
```
</RequestExample>
Descriptor strings in `error.message` identify what triggered the gate:
| Pattern | Example |
| --- | --- |
| Whole method | `mock/experimentalMethod`, `thread/memoryMode/set`, `process/spawn` |
| Field on stable method | `thread/start.mockExperimentalField`, `thread/start.runtimeWorkspaceRoots` |
| Enum variant | `askForApproval.granular` (for `approvalPolicy: { "granular": ... }`) |
`tests/suite/v2/experimental_api.rs` locks these behaviors for representative methods and fields.
### Outgoing notification suppression
Notifications annotated with `#[experimental("...")]` in `app-server-protocol/src/protocol/common.rs` implement `ExperimentalApi`. The transport layer drops them for connections where `experimental_api_enabled` is false—clients see no error; the event simply never arrives. Examples include `thread/realtime/started`, `thread/settings/updated`, `process/outputDelta`, and `process/exited`.
`capabilities.optOutNotificationMethods` is separate: it suppresses **exact** stable or experimental method names the client lists, regardless of experimental opt-in.
### Outgoing server-request field stripping
For `item/commandExecution/requestApproval`, when the client did not opt in, app-server clears `additionalPermissions` before writing the server request to the connection (`CommandExecutionRequestApprovalParams::strip_experimental_fields`). Opted-in clients receive the full payload. This lets stable clients handle basic approvals while experimental clients consume granular permission requests.
## Stable vs experimental schema generation
`codex app-server generate-ts` and `codex app-server generate-json-schema` default to **stable-only** output: experimental methods and fields are filtered from generated TypeScript and JSON Schema bundles. Pass `--experimental` to include the full gated surface. Generated artifacts match the binary version used to run the command.
<CodeGroup>
```bash title="Stable (default)"
codex app-server generate-ts --out DIR
codex app-server generate-json-schema --out DIR
```
```bash title="Include experimental API"
codex app-server generate-ts --out DIR --experimental
codex app-server generate-json-schema --out DIR --experimental
```
</CodeGroup>
Maintainers regenerate checked-in fixtures with:
```bash
just write-app-server-schema
just write-app-server-schema --experimental
```
The protocol crate applies `filter_experimental_schema`, prunes experimental entries from `ClientRequest` unions, and removes experimental type files when `experimental_api` is false during export (`app-server-protocol/src/export.rs`).
<Warning>
Stable schemas intentionally omit experimental methods and fields. Runtime servers still accept gated calls when clients opt in at `initialize`—codegen and live behavior can diverge unless you generate with `--experimental` or read the protocol sources.
</Warning>
## Gating patterns for maintainers
Experimental surface is declared in `codex-app-server-protocol` (primarily `protocol/common.rs` and `protocol/v2/*`), enforced at runtime in `app-server`, and reflected in export filters.
### Method-level gate
Mark the `ClientRequest` variant in `client_request_definitions!`:
```rust
#[experimental("thread/memoryMode/set")]
ThreadMemoryModeSet => "thread/memoryMode/set" {
params: v2::ThreadMemoryModeSetParams,
// ...
},
```
The wire method string becomes the rejection descriptor when the whole RPC is experimental.
### Field-level gate on a mostly stable method
Annotate fields on params/response types and derive `ExperimentalApi`:
```rust
#[derive(ExperimentalApi)]
pub struct ThreadStartParams {
#[experimental("thread/start.mockExperimentalField")]
pub mock_experimental_field: Option<String>,
// ...
}
```
In `common.rs`, keep the method stable and set `inspect_params: true` so runtime checks delegate to the params type:
```rust
ThreadStart => "thread/start" {
params: v2::ThreadStartParams,
inspect_params: true,
// ...
},
```
Rejection descriptors use `method.field` (for example `thread/start.mockExperimentalField`).
### Enum variant gate
```rust
#[derive(ExperimentalApi)]
pub enum AskForApproval {
#[experimental("askForApproval.granular")]
Granular { /* ... */ },
// stable variants omit the attribute
}
```
### Nested experimental values
Use `#[experimental(nested)]` on a container field so reasons bubble from nested types (for example `approval_policy: Option<AskForApproval>` inside config shapes).
### Server notifications
Add `#[experimental("thread/realtime/started")]` on the `server_notification_definitions!` variant. No separate client opt-in beyond `experimentalApi`; non-opted connections never see the notification.
### Server-initiated request fields
Annotate experimental fields on server request param types for schema export. For runtime compatibility without opt-in, ensure app-server strips or omits those fields in `filter_outgoing_message_for_connection` (command-execution approval is the reference implementation).
### Registration inventory
The `#[derive(ExperimentalApi)]` macro registers `ExperimentalField` entries via `inventory` for schema filtering and documents type/field/reason triples used by export.
<Check>
After protocol changes: run `just write-app-server-schema` (both stable and `--experimental` when experimental fixtures change), then `just test -p codex-app-server-protocol`.
</Check>
## Representative experimental surface
The README API overview marks many v2 RPCs as experimental; the authoritative list is the `#[experimental(...)]` attributes on `ClientRequest` and `ServerNotification` in `app-server-protocol/src/protocol/common.rs`. Groupings include:
| Category | Examples (require `experimentalApi`) |
| --- | --- |
| Thread lifecycle / settings | `thread/settings/update`, `thread/memoryMode/set`, `thread/turns/list`, `thread/backgroundTerminals/clean`, `thread/search` |
| Realtime | `thread/realtime/start`, `appendAudio`, `appendText`, `stop`, `listVoices` + `thread/realtime/*` notifications |
| Process control | `process/spawn`, `writeStdin`, `kill`, `resizePty` + `process/outputDelta`, `process/exited` |
| Remote control | `remoteControl/enable`, `disable`, `status/read`, `pairing/start` |
| Other | `environment/add`, `collaborationMode/list`, `mock/experimentalMethod`, fuzzy-file-search session RPCs |
Stable methods with common experimental **fields** include `thread/start`, `thread/resume`, `thread/fork`, and `turn/start` (permissions, environments, `dynamicTools`, `runtimeWorkspaceRoots`, granular `approvalPolicy`, etc.).
## Not the same as `experimentalFeature/*`
`experimentalFeature/list` and `experimentalFeature/enablement/set` are **stable** RPCs for product feature-flag metadata and in-memory enablement (`apps`, `plugins`, `remote_control`, etc.). They do not require `capabilities.experimentalApi`. Precedence for enablement is documented in the README: cloud requirements → CLI `--enable` → `config.toml` → `experimentalFeature/enablement/set` → code default.
Use `experimentalApi` for protocol surface that may change without notice; use `experimentalFeature/*` for toggling Codex product features at runtime.
## Verification checklist
| Check | Expected signal |
| --- | --- |
| Initialize without `experimentalApi`, call `mock/experimentalMethod` | `-32600`, message ends with `requires experimentalApi capability` |
| Same client, `thread/start` with only stable fields | Success |
| Opted-in client receives `thread/realtime/started` | Notification on wire |
| Non-opted client during realtime session | No `thread/realtime/started` (dropped) |
| `generate-json-schema` without `--experimental` | No experimental method entries in `codex_app_server_protocol.schemas.json` |
| Regenerate fixtures after protocol edit | `just write-app-server-schema` succeeds; protocol tests pass |
## Related pages
<CardGroup>
<Card title="Connection lifecycle" href="/connection-lifecycle">
`initialize` handshake, `clientInfo`, notification opt-out, and per-connection state.
</Card>
<Card title="Schema generation" href="/schema-generation">
`generate-ts`, `generate-json-schema`, stable vs `--experimental`, and fixture regeneration.
</Card>
<Card title="RPC methods reference" href="/rpc-methods">
Catalog of v2 methods with stable vs experimental markers.
</Card>
<Card title="Notifications and events" href="/notifications-and-events">
Turn/item notification streams and opt-out behavior.
</Card>
<Card title="Development and testing" href="/development-and-testing">
Integration suites, `TestAppServer`, and `just test -p codex-app-server`.
</Card>
</CardGroup>
---
## 08. Build a JSON-RPC client
> Client responsibilities: newline-delimited framing, request ids, handling server requests mid-turn, `initialized` notification ordering, and compliance-oriented `clientInfo.name` values.
- Page Markdown: https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/pages/08-build-a-json-rpc-client.md
- Generated: 2026-06-02T06:37:45.907Z
### Source Files
- `README.md`
- `src/message_processor.rs`
- `src/outgoing_message.rs`
- `src/request_processors/initialize_processor.rs`
- `tests/common/test_app_server.rs`
- `tests/common/responses.rs`
---
title: "Build a JSON-RPC client"
description: "Client responsibilities: newline-delimited framing, request ids, handling server requests mid-turn, `initialized` notification ordering, and compliance-oriented `clientInfo.name` values."
---
`codex app-server` speaks JSON-RPC–style messages over stdio (default), unix control socket, or experimental websocket listeners. A correct client owns transport framing, correlates request ids per connection, completes the `initialize` / `initialized` handshake before other RPCs, and continuously reads the stream so server-initiated requests and turn notifications are handled while work is in flight.
## Wire framing
| Transport | Framing rule |
| --- | --- |
| stdio (`--stdio`, `--listen stdio://`) | One JSON object per line (JSONL): serialize the message, append `\n`, flush |
| unix control socket | WebSocket upgrade, then one JSON-RPC message per websocket text frame |
| websocket (`--listen ws://…`) | One JSON-RPC message per websocket text frame (experimental) |
Codex omits the `"jsonrpc":"2.0"` field on the wire even though the protocol is JSON-RPC 2.0–like. Each line (or frame) must deserialize to exactly one of: `request`, `response`, `error`, or `notification`.
<Note>
The integration test harness implements the canonical stdio pattern: serialize with `serde_json`, write bytes plus `\n` to stdin, and `read_line` from stdout for each inbound message.
</Note>
## Message shapes
| Direction | JSON shape | `id` field |
| --- | --- | --- |
| Client → server request | `{ "id", "method", "params"? }` | Required |
| Server → client request | `{ "id", "method", "params" }` | Required (server-assigned integer) |
| Response | `{ "id", "result" }` | Matches originating request |
| Error | `{ "id", "error": { "code", "message", "data"? } }` | Matches originating request |
| Notification | `{ "method", "params"? }` | Omitted |
`RequestId` is either an integer or a string; pick one style per client and keep ids unique among in-flight client-originated requests on that connection.
## Request id rules
**Client-originated RPCs.** Allocate monotonic integer ids (the test harness starts at 0 and increments). When a response or error arrives, match `id` before treating the RPC as complete. The server scopes ids per connection; the same numeric id on two connections is independent.
**Server-originated RPCs.** The server assigns monotonic integer ids from its own counter. Store each pending server request by `id` until you send a JSON-RPC `response` with the same `id` and a typed `result` payload.
**Responses to server requests.** Your client sends a result object on the wire, for example:
```json
{ "id": 7, "result": { "decision": "accept" } }
```
Errors use the same `id` with an `error` object.
## Connection handshake
Per connection, run this sequence before any other method:
<Steps>
<Step title="Send initialize">
Call `initialize` with `clientInfo` and optional `capabilities` (experimental API, notification opt-out, attestation).
</Step>
<Step title="Read initialize result">
Wait for the matching `response` (or `error`). On success you receive `userAgent`, `codexHome`, `platformFamily`, and `platformOs`.
</Step>
<Step title="Send initialized">
Emit the client notification `{"method":"initialized"}` with **no** `params` field.
</Step>
<Step title="Start reading notifications">
After initialize completes, the server may emit connection-scoped notifications (for example `configWarning`, `remoteControl/status/changed`). Broadcast notifications are only delivered to connections marked outbound-ready after initialize.
</Step>
</Steps>
```mermaid
sequenceDiagram
participant Client
participant Transport
participant MessageProcessor
participant Outbound
Client->>Transport: request initialize (id=N)
Transport->>MessageProcessor: process_request
MessageProcessor->>MessageProcessor: session.initialize(clientInfo)
MessageProcessor->>Outbound: response initialize (id=N)
Outbound-->>Client: response initialize
Client->>Transport: notification initialized
Note over MessageProcessor: process_notification (logged)
MessageProcessor->>Outbound: configWarning / remoteControl (connection-scoped)
Outbound-->>Client: notifications
Client->>Transport: request thread/start (id=N+1)
```
<Warning>
RPCs sent before `initialize` completes on a connection return `"Not initialized"`. A second `initialize` on the same connection returns `"Already initialized"`.
</Warning>
### Initialize params
<ParamField body="clientInfo.name" type="string" required>
HTTP-header-safe client identifier. Drives compliance logging and, for most clients, process-global originator / user-agent suffix.
</ParamField>
<ParamField body="clientInfo.version" type="string" required>
Client version string appended to the user-agent suffix as `{name}; {version}`.
</ParamField>
<ParamField body="clientInfo.title" type="string">
Optional display title; not used for originator selection.
</ParamField>
<ParamField body="capabilities.experimentalApi" type="boolean">
When `true`, enables experimental methods and fields for this connection.
</ParamField>
<ParamField body="capabilities.optOutNotificationMethods" type="string[]">
Exact notification method names to suppress for this connection (no wildcards).
</ParamField>
<ParamField body="capabilities.requestAttestation" type="boolean">
When `true`, client must answer server `attestation/generate` requests.
</ParamField>
<RequestExample>
```json
{
"method": "initialize",
"id": 0,
"params": {
"clientInfo": {
"name": "codex_vscode",
"title": "Codex VS Code Extension",
"version": "0.1.0"
},
"capabilities": {
"experimentalApi": true,
"optOutNotificationMethods": ["thread/started"]
}
}
}
```
</RequestExample>
<ResponseExample>
```json
{
"id": 0,
"result": {
"userAgent": "codex_vscode/0.1.0 …",
"codexHome": "/path/to/.codex",
"platformFamily": "unix",
"platformOs": "macos"
}
}
```
</ResponseExample>
After the response, send:
<RequestExample>
```json
{ "method": "initialized" }
```
</RequestExample>
## Compliance-oriented `clientInfo.name`
`clientInfo.name` is validated as an HTTP header value before the server commits session state. Invalid values (for example embedded `\r`) are rejected with code `-32600` and message `Invalid clientInfo.name: '…'. Must be a valid HTTP header value.`
| `clientInfo.name` | Mutates global originator / user-agent suffix? | Typical use |
| --- | --- | --- |
| Product integrations (`codex_vscode`, `xcode`, …) | Yes — sets originator and `userAgent` prefix | Desktop / IDE hosts |
| `codex_app_server_daemon` | No — probe / daemon clients | Health checks, internal daemons |
| `codex-backend` | No — backend callers | Server-side automation |
<Info>
Enterprise integrations intended for the OpenAI Compliance Logs Platform should use a known client name. Contact OpenAI to register new production client names. See the Codex admin API reference for compliance logging context (linked from the app-server README).
</Info>
Official VS Code extension example name: `codex_vscode`.
## Event loop architecture
A production client runs **one reader task** and **one writer** (or serialized writes) per connection. Never block the reader waiting for a single RPC while a turn is active — notifications and server requests arrive interleaved with late responses.
```text
┌─────────────────────────────────────────────┐
│ read loop (stdout / socket) │
│ deserialize JSONRPCMessage │
│ ├─ notification → dispatch / buffer │
│ ├─ request (server) → approval handler │
│ ├─ response/error → complete client RPC │
└─────────────────────────────────────────────┘
▲ │
│ ▼
┌────────────────┐ ┌───────────────────┐
│ pending server │ │ pending client │
│ requests[id] │ │ requests[id] │
└────────────────┘ └───────────────────┘
```
Buffer non-matching messages while waiting for a specific response or server request. Match by `id` or notification `method`, not arrival order alone.
## Server requests during an active turn
While `turn/start` is in flight, the server may emit:
- Turn/item **notifications** (`turn/started`, `item/started`, deltas, `turn/completed`, …)
- **Server-initiated requests** (approvals, MCP elicitation, attestation, dynamic tool calls, …)
### Command execution approval (representative flow)
| Order | Message | Client action |
| --- | --- | --- |
| 1 | `item/started` (`commandExecution`) | Render pending UI |
| 2 | `item/commandExecution/requestApproval` (JSON-RPC **request**) | Show approval UI |
| 3 | JSON-RPC **response** `{ "decision": "accept" \| … }` | Resume work |
| 4 | `serverRequest/resolved` | Clear pending state |
| 5 | `item/completed` | Show final status/output |
Integration tests assert `serverRequest/resolved` arrives before `turn/completed` for approval flows.
### Responding to a server request
1. Parse the incoming JSON-RPC `request` into the typed `ServerRequest` enum (generate bindings from `codex app-server generate-ts` or `generate-json-schema`).
2. Present UI scoped by `threadId` / `turnId` / `itemId` from params.
3. Send a JSON-RPC `response` with the **same** `id` and a method-specific result object.
4. Wait for `serverRequest/resolved` (or handle cleanup if the turn ends first).
Pending server requests for a thread are tracked server-side; on reconnect or subscribe the server can replay outstanding requests to that connection. Key UI state by `requestId` to avoid duplicate prompts.
### Turn transition cleanup
If a turn is interrupted or superseded before the user answers, the server cancels pending server requests with an internal error whose `data.reason` is `turn_transition_pending_request`. The server still emits `serverRequest/resolved` for lifecycle cleanup in those cases.
## Client RPC serialization
Some client methods are serialized per thread on the server. Your client may send parallel RPCs, but thread-scoped operations can be ordered server-side. Prefer awaiting `turn/completed` (or a turn error) before starting conflicting thread operations unless you know the method’s scope.
## Retryable overload
When ingress queues are saturated, new client requests receive:
| Field | Value |
| --- | --- |
| `error.code` | `-32001` |
| `error.message` | `Server overloaded; retry later.` |
Retry with exponential backoff and jitter. Do not treat overload as a terminal session failure.
## Minimal stdio client checklist
<Check>
Spawn `codex-app-server` (or `codex app-server`) with stdin/stdout piped and `CODEX_HOME` set.
</Check>
<Check>
Writer: one JSON object + `\n` per message; flush after each.
</Check>
<Check>
Reader: `read_line` loop; parse UTF-8 JSON per line.
</Check>
<Check>
Handshake: `initialize` → match response id → `initialized` notification.
</Check>
<Check>
Use a registered `clientInfo.name` for production enterprise UIs.
</Check>
<Check>
Background read loop handles server `request` messages while awaiting any client `response`.
</Check>
<Check>
Buffer unrelated messages; correlate strictly by `id`.
</Check>
<Check>
Handle `-32001` with backoff.
</Check>
## Schema generation
Generate typed bindings from the running binary so wire shapes match the server version:
```bash
codex app-server generate-ts --out DIR
codex app-server generate-json-schema --out DIR
```
## Related pages
<CardGroup>
<Card title="Protocol and transport" href="/protocol-and-transport">
JSON-RPC wire rules, transports, health probes, and backpressure.
</Card>
<Card title="Connection lifecycle" href="/connection-lifecycle">
Per-connection initialize, notification opt-out, and subscribe semantics.
</Card>
<Card title="Quickstart" href="/quickstart">
End-to-end initialize → thread/start → turn/start sequence.
</Card>
<Card title="Approvals and server requests" href="/approvals-and-server-requests">
Approval methods, MCP elicitation, and `serverRequest/resolved` lifecycle.
</Card>
<Card title="Stream turns and events" href="/stream-turns-and-events">
Consuming turn and item notifications during active work.
</Card>
</CardGroup>
---
## 09. Transports and proxy
> Choosing `--listen` URLs, unix control socket at `$CODEX_HOME/app-server-control`, `codex app-server proxy` byte bridging, websocket auth args, and remote-control pairing endpoints.
- Page Markdown: https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/pages/09-transports-and-proxy.md
- Generated: 2026-06-02T06:38:22.010Z
### Source Files
- `README.md`
- `src/main.rs`
- `src/transport.rs`
- `src/lib.rs`
- `src/request_processors/remote_control_processor.rs`
- `tests/suite/v2/thread_name_websocket.rs`
---
title: "Transports and proxy"
description: "Choosing `--listen` URLs, unix control socket at `$CODEX_HOME/app-server-control`, `codex app-server proxy` byte bridging, websocket auth args, and remote-control pairing endpoints."
---
`codex-app-server` selects its wire transport from `--listen`, exposes a local Unix control socket under `$CODEX_HOME/app-server-control/`, optionally binds an experimental TCP WebSocket listener with health probes and auth, and can run a parallel remote-control WebSocket client that enrolls against ChatGPT backend APIs while still serving JSON-RPC over the same processor.
## Listen URL reference
The `codex-app-server` binary parses `--listen` into `AppServerTransport`. The default is `stdio://`.
| `--listen` value | Transport | Wire behavior |
| --- | --- | --- |
| `stdio://` (default) | `Stdio` | Newline-delimited JSON (JSONL) on stdin/stdout; single client; process exits when that connection closes |
| `unix://` | `UnixSocket` | WebSocket upgrade over `$CODEX_HOME/app-server-control/app-server-control.sock` |
| `unix://PATH` | `UnixSocket` | WebSocket upgrade over an absolute custom socket path |
| `ws://IP:PORT` | `WebSocket` | One JSON-RPC message per WebSocket text frame (**experimental / unsupported for production**) |
| `off` | `Off` | No local listener |
<Warning>
WebSocket transport (`ws://…`) is experimental and unsupported for production workloads. Do not rely on it until the project marks it stable.
</Warning>
If both the local listener and remote control are unavailable, startup fails with `no transport configured; use --listen or enable remote control`. Remote control alone is valid when SQLite state is available.
<ParamField body="listen" type="string" required={false}>
Transport endpoint URL. Default: `stdio://`.
</ParamField>
<ParamField body="session-source" type="string" required={false}>
Session source for product restrictions and metadata. Default: `vscode`.
</ParamField>
<ParamField body="remote-control" type="boolean" required={false}>
Hidden flag on `codex-app-server` that enables remote control at process start (`AppServerRuntimeOptions.remote_control_enabled`).
</ParamField>
## Unix control socket
When `--listen unix://` or `--listen unix://PATH` is used, the server:
1. Acquires `$CODEX_HOME/app-server-control/app-server-startup.lock` so only one Unix-socket listener binds at a time.
2. Prepares the socket path (private parent directory, stale-socket cleanup, `AddrInUse` if another live server holds the path).
3. Binds `app-server-control.sock` with mode `0600` on Unix.
4. Accepts raw Unix stream connections and upgrades each to WebSocket via the standard HTTP Upgrade handshake before handing frames to the shared JSON-RPC processor.
Default socket path (empty `unix://` suffix):
```text
$CODEX_HOME/app-server-control/app-server-control.sock
```
Custom paths must be absolute (`unix:///tmp/codex.sock`). Relative paths are resolved against the process current working directory at parse time.
```mermaid
flowchart LR
subgraph client [Local client]
CLI["codex app-server proxy"]
IDE["Control-plane client"]
end
subgraph uds [Unix domain socket]
SOCK["app-server-control.sock"]
end
subgraph server [codex-app-server]
ACC["start_control_socket_acceptor"]
WS["WebSocket upgrade"]
PROC["MessageProcessor"]
end
CLI --> SOCK
IDE --> SOCK
SOCK --> ACC --> WS --> PROC
```
Multiple WebSocket clients can connect concurrently on the Unix listener (integration tests open two clients and both receive broadcasts after `initialize`).
## `codex app-server proxy`
The proxy subcommand is the supported way to attach a **stdio JSON-RPC client** to the **Unix control socket** without implementing WebSocket framing yourself.
<Steps>
<Step title="Start app-server on the control socket">
Run the server with a Unix listener, for example `codex app-server --listen unix://`.
</Step>
<Step title="Run the proxy in front of your client">
```bash
codex app-server proxy
# or
codex app-server proxy --sock /absolute/path/to.sock
```
</Step>
<Step title="Talk JSON-RPC on stdin/stdout">
Your client sends newline-delimited JSON-RPC on stdin; the proxy forwards bytes through the socket WebSocket session.
</Step>
</Steps>
Behavior (from product docs and CLI wiring):
- Opens **one** raw stream to the control socket (default `$CODEX_HOME/app-server-control/app-server-control.sock`, or `--sock PATH`).
- Proxies bytes **bidirectionally** between that socket and stdin/stdout.
- The proxied stream carries the WebSocket HTTP Upgrade handshake, then WebSocket frames—same as a native Unix WebSocket client.
- `codex app-server proxy` does not support `--strict-config` or remote-mode auth-token env overrides.
<Tip>
Use the proxy when your integration already speaks stdio JSONL (for example MCP-style subprocess clients) but the running server only exposes the control socket.
</Tip>
## TCP WebSocket listener
`--listen ws://IP:PORT` starts an Axum listener that:
- Serves `GET /readyz` → `200 OK` once accepting connections.
- Serves `GET /healthz` → `200 OK` when no `Origin` header is present.
- Rejects **any** request that includes an `Origin` header with `403 Forbidden` (including WebSocket upgrade attempts).
- Upgrades all other requests on the fallback route to WebSocket JSON-RPC.
On bind, stderr prints `ws://{addr}`, `http://{addr}/readyz`, and `http://{addr}/healthz`. Tests scrape the bound address from stderr after spawn.
WebSocket connections get a larger outbound queue (`32 * 1024` messages) than the internal `128`-slot transport channels. Slow WebSocket clients can be disconnected when their outbound queue fills.
### WebSocket authentication CLI
Non-loopback `ws://` listeners **refuse to start** without auth. Loopback listeners may run without auth; use SSH port forwarding for remote access.
<ParamField body="ws-auth" type="enum" required={false}>
`capability-token` or `signed-bearer-token`. Required when any other `ws-*` flag is set.
</ParamField>
**Capability token mode** (`--ws-auth capability-token`):
| Flag | Purpose |
| --- | --- |
| `--ws-token-file PATH` | Absolute path to the raw token file (trimmed contents) |
| `--ws-token-sha256 HEX` | 64-character hex SHA-256 of the token (mutually exclusive with `--ws-token-file`) |
Upgrade checks `Authorization: Bearer <token>` and compares SHA-256 in constant time.
**Signed bearer JWT mode** (`--ws-auth signed-bearer-token`):
| Flag | Purpose |
| --- | --- |
| `--ws-shared-secret-file PATH` | HS256 secret file (≥ 32 bytes) |
| `--ws-issuer ISSUER` | Optional expected `iss` claim |
| `--ws-audience AUDIENCE` | Optional expected `aud` claim (string or array) |
| `--ws-max-clock-skew-seconds SECONDS` | Clock skew for `exp`/`nbf` (default `30`) |
Example test invocation:
```bash
codex-app-server \
--listen ws://127.0.0.1:0 \
--ws-auth signed-bearer-token \
--ws-shared-secret-file /path/to/secret \
--ws-issuer codex-enroller \
--ws-audience codex-app-server
```
Failed upgrade returns `401 Unauthorized` with a static message (missing bearer, invalid JWT, expired token, issuer/audience mismatch).
## Runtime transport wiring
`run_main_with_transport_options` in the app-server library starts acceptors based on `AppServerTransport`, then runs two cooperating tasks:
| Task | Role |
| --- | --- |
| Processor loop | Reads `TransportEvent`, dispatches JSON-RPC, tracks per-connection session state |
| Outbound router | Writes queued responses/notifications to per-connection writers; disconnects slow WebSocket peers when queues fill |
`ConnectionOrigin` values: `Stdio`, `WebSocket`, `RemoteControl`, `InProcess`.
Analytics RPC transport mapping: stdio → `Stdio`; unix socket, WebSocket, and `off` → `Websocket`.
### Backpressure and overload
Ingress uses bounded channels (`CHANNEL_CAPACITY = 128`). When the processor ingress queue is full, **new JSON-RPC requests** receive:
```json
{
"error": {
"code": -32001,
"message": "Server overloaded; retry later."
}
}
```
Treat `-32001` as retryable with exponential backoff and jitter. Responses and notifications wait on the queue instead of being dropped.
## Remote control
Remote control is an **experimental** parallel transport: a background task maintains a WebSocket to the ChatGPT remote-control API while remote controllers send JSON-RPC through that channel with `ConnectionOrigin::RemoteControl`.
### Prerequisites
| Requirement | Behavior if missing |
| --- | --- |
| SQLite state DB (`rollout_state_db`) | `remoteControl/enable` returns unavailable; startup with only `--remote-control` and no listener fails |
| Authenticated ChatGPT account | Enrollment and pairing need `AuthManager` credentials |
| `chatgpt_base_url` from config | Base URL normalized into enroll/refresh/pair/WebSocket targets |
Server name in status snapshots comes from the local hostname (`gethostname`).
### Client RPC surface (experimental)
All methods require `capabilities.experimentalApi` at `initialize` unless your build gates them differently.
| Method | Purpose |
| --- | --- |
| `remoteControl/enable` | Enable remote control; returns status snapshot (`connecting` when enrollment starts) |
| `remoteControl/disable` | Disable remote control; does **not** revoke enrolled controllers |
| `remoteControl/status/read` | Read current `status`, `serverName`, `installationId`, `environmentId` |
| `remoteControl/pairing/start` | Start short-lived pairing; optional `manualCode: true` |
Notification: `remoteControl/status/changed` — emitted on status or environment id changes; every newly initialized client receives the current snapshot.
`status` enum: `disabled`, `connecting`, `connected`, `errored`.
### Pairing request and response
<RequestExample>
```json
{
"method": "remoteControl/pairing/start",
"id": 3,
"params": {
"manualCode": true
}
}
```
</RequestExample>
<ResponseExample>
```json
{
"id": 3,
"result": {
"pairingCode": "pairing-code",
"manualPairingCode": "ABCD-EFGH",
"environmentId": "environment-id",
"expiresAt": 33336362096
}
}
```
</ResponseExample>
Notes:
- `expiresAt` is Unix seconds.
- `serverId` from the backend is **not** exposed to clients (tests assert `result.serverId` is absent).
- Pairing requires remote control enabled **and** completed enrollment; otherwise JSON-RPC `invalid_request` (for example `"remote control pairing is unavailable until enrollment completes"`).
- When remote control is unavailable on this process, handlers return internal error `"remote control is unavailable for this app-server"`.
### Backend HTTP endpoints
Given config base URL `https://chatgpt.com/backend-api/` (trailing slash normalized), the transport derives:
| Purpose | Path |
| --- | --- |
| Remote-control WebSocket | `wss://chatgpt.com/backend-api/wham/remote/control/server` |
| Enroll | `POST …/wham/remote/control/server/enroll` |
| Refresh token | `POST …/wham/remote/control/server/refresh` |
| Pair | `POST …/wham/remote/control/server/pair` |
Supported URL classes:
- HTTPS to `chatgpt.com` or `*.chatgpt-staging.com`
- HTTP/HTTPS to `localhost` (tests use `http://127.0.0.1:…/backend-api/`)
Enrollment posts machine metadata (`name`, `os`, `arch`, `app_server_version`, `installation_id`) and stores `environment_id`, `server_id`, and `remote_control_token` in SQLite for refresh and pairing.
```mermaid
sequenceDiagram
participant Client as JSON-RPC client
participant AS as codex-app-server
participant API as ChatGPT backend-api
Client->>AS: remoteControl/enable
AS->>API: POST /wham/remote/control/server/enroll
API-->>AS: environmentId, remote_control_token
AS-->>Client: status connecting
AS-->>Client: remoteControl/status/changed
Client->>AS: remoteControl/pairing/start
AS->>API: POST /wham/remote/control/server/pair
API-->>AS: pairingCode, manualPairingCode
AS-->>Client: pairingCode, environmentId, expiresAt
```
### Typical pairing workflow
<Steps>
<Step title="Enable remote control">
Call `remoteControl/enable` and wait for `remoteControl/status/changed` with a non-null `environmentId` when enrollment succeeds.
</Step>
<Step title="Start pairing">
Call `remoteControl/pairing/start` with `manualCode: true` when you need a human-entered code.
</Step>
<Step title="Present codes to the controller">
Share `pairingCode` and optional `manualPairingCode` before `expiresAt`.
</Step>
<Step title="Monitor connection">
Watch `remoteControl/status/changed` for `connected` or `errored`.
</Step>
</Steps>
Persist whether remote control should stay enabled outside app-server (README: caller responsibility). `remoteControl/disable` only stops the local feature; enrolled devices may remain valid server-side.
## Choosing a transport
| Use case | Recommended listen / access |
| --- | --- |
| Editor extension subprocess (VS Code) | `stdio://` or `codex app-server proxy` → Unix socket |
| Local daemon / multi-client UI | `unix://` on default or custom socket |
| Health-checked LAN listener (dev only) | `ws://127.0.0.1:PORT` + auth flags |
| Mobile / web remote controller | Enable remote control + pairing RPCs |
| Headless API generation only | `off` (if another transport or remote control is active) |
<Info>
Multi-client WebSocket on `unix://` supports broadcast notifications (for example `thread/name/updated`) to all initialized connections subscribed to the thread. Stdio remains strictly single-connection.
</Info>
## Related pages
<CardGroup>
<Card title="Protocol and transport" href="/protocol-and-transport">
JSON-RPC wire rules, overload `-32001`, health probes, and queue behavior in depth.
</Card>
<Card title="Installation" href="/installation">
Launching `codex-app-server`, default listen URLs, and stdio vs control-socket success signals.
</Card>
<Card title="CLI flags and errors" href="/cli-flags-and-errors">
Full `--listen` and websocket auth flag reference plus JSON-RPC error codes.
</Card>
<Card title="Connection lifecycle" href="/connection-lifecycle">
Per-connection `initialize`, notification opt-out, and server-initiated requests.
</Card>
<Card title="RPC methods reference" href="/rpc-methods">
Grouped v2 RPC catalog including experimental `remoteControl/*` methods.
</Card>
<Card title="Experimental API" href="/experimental-api">
Opt-in via `capabilities.experimentalApi` for remote control and other gated surface.
</Card>
</CardGroup>
---
## 10. Stream turns and events
> Consuming `turn/started`, `item/started`, deltas (`item/agentMessage/delta`, command output, reasoning), `turn/completed`, token usage, and notification opt-out for high-volume streams.
- Page Markdown: https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/pages/10-stream-turns-and-events.md
- Generated: 2026-06-02T06:38:33.117Z
### Source Files
- `README.md`
- `src/request_processors/turn_processor.rs`
- `src/bespoke_event_handling.rs`
- `src/thread_status.rs`
- `src/filters.rs`
- `tests/suite/v2/thread_status.rs`
---
title: "Stream turns and events"
description: "Consuming `turn/started`, `item/started`, deltas (`item/agentMessage/delta`, command output, reasoning), `turn/completed`, token usage, and notification opt-out for high-volume streams."
---
After `turn/start` returns an in-progress `turn`, app-server streams JSON-RPC notifications on the transport while Codex runs. Core agent events are translated in `bespoke_event_handling` into v2 `turn/*`, `item/*`, and `thread/tokenUsage/updated` notifications; turn boundaries also drive `thread/status/changed` through `ThreadWatchManager`. Clients that only need final state can opt out of high-volume methods at `initialize` time.
## Prerequisites
| Requirement | Why it matters |
| --- | --- |
| Completed `initialize` / `initialized` handshake | Other RPCs are rejected with `"Not initialized"` |
| Subscribed thread | `thread/start`, `thread/resume`, and `thread/fork` auto-subscribe the connection; use `thread/unsubscribe` to stop turn/item traffic |
| Non-blocking read loop on the transport | Notifications interleave with RPC responses and server requests |
<Note>
`turn/start` returns immediately with `{ turn: { id, status: "inProgress", items: [] } }`. `turn/started` arrives later, when the core agent actually begins the turn—not when the RPC returns.
</Note>
## Turn and item lifecycle
```mermaid
sequenceDiagram
participant Client
participant Transport as JSON-RPC transport
participant TurnProc as turn_processor
participant Core as CodexThread
participant Bespoke as bespoke_event_handling
participant Watch as ThreadWatchManager
Client->>Transport: turn/start
Transport->>TurnProc: submit user input
TurnProc-->>Client: result.turn (inProgress, items [])
Core-->>Bespoke: EventMsg::TurnStarted
Bespoke->>Watch: note_turn_started
Bespoke-->>Client: turn/started
loop Per ThreadItem
Bespoke-->>Client: item/started
Bespoke-->>Client: item/.../delta (optional)
Bespoke-->>Client: item/completed
end
Core-->>Bespoke: EventMsg::TokenCount (optional)
Bespoke-->>Client: thread/tokenUsage/updated
Core-->>Bespoke: EventMsg::TurnComplete
Bespoke->>Watch: note_turn_completed
Bespoke-->>Client: turn/completed
```
Every item follows the same pattern: **`item/started` → zero or more deltas → `item/completed`**. Build UI state from `item/*` notifications; do not wait for items inside `turn/started` or `turn/completed`.
<Warning>
`turn/started` and `turn/completed` currently carry an **empty `items` array** even when item notifications were streamed. Treat `item/*` as the canonical item list until this is fixed.
</Warning>
## Turn-level notifications
| Method | Params | When emitted |
| --- | --- | --- |
| `turn/started` | `{ threadId, turn }` | Core `TurnStarted`; `turn.status` is `inProgress`, `items` is empty |
| `turn/completed` | `{ threadId, turn }` | Turn finishes: `completed`, `interrupted`, or `failed` |
| `turn/diff/updated` | `{ threadId, turnId, diff }` | After file-change items; aggregated unified diff for the turn |
| `turn/plan/updated` | `{ threadId, turnId, explanation?, plan }` | Agent plan/checklist updates |
| `model/rerouted` | `{ threadId, turnId, fromModel, toModel, reason }` | Backend model reroute |
| `model/verification` | `{ threadId, turnId, verifications }` | Additional verification required |
| `error` | `{ threadId, turnId, error, willRetry }` | Mid-turn failure (`willRetry: false`) or stream retry (`willRetry: true`) |
<ResponseField name="turn.status" type="string">
Terminal values on `turn/completed`: `completed`, `interrupted`, or `failed`. Failures include `turn.error` with `{ message, codexErrorInfo?, additionalDetails? }`.
</ResponseField>
On `turn/started`, app-server aborts pending server requests from the previous turn. On `turn/completed` or interrupt, it aborts turn-scoped server requests and resolves pending `turn/interrupt` responses.
### `turn/completed` without a prior `turn/started`
If `turn/start` is rejected (invalid permissions, environments, or input limits), the RPC may return an error and **no** `turn/started` is emitted. Integration tests assert this for rejected permission and environment selection.
## Item-level notifications
### Shared lifecycle
| Method | Role |
| --- | --- |
| `item/started` | Full `item` snapshot when work begins; `item.id` matches delta `itemId` |
| `item/completed` | Final authoritative `item` after tool/message work finishes |
| `item/autoApprovalReview/started` | [UNSTABLE] Guardian auto-review begins |
| `item/autoApprovalReview/completed` | [UNSTABLE] Guardian auto-review resolves |
`ThreadItem` variants include `userMessage`, `agentMessage`, `plan`, `reasoning`, `commandExecution`, `fileChange`, `mcpToolCall`, `collabToolCall`, `webSearch`, `imageView`, `enteredReviewMode`, `exitedReviewMode`, and `contextCompaction`.
### Streaming deltas
| Method | Concatenation key | Purpose |
| --- | --- | --- |
| `item/agentMessage/delta` | `itemId` | Agent reply text chunks |
| `item/plan/delta` | `itemId` | Plan-mode proposed plan (experimental) |
| `item/reasoning/summaryTextDelta` | `itemId` + `summaryIndex` | Readable reasoning summaries |
| `item/reasoning/summaryPartAdded` | `itemId` | Boundary between summary sections |
| `item/reasoning/textDelta` | `itemId` + `contentIndex` | Raw reasoning blocks |
| `item/commandExecution/outputDelta` | `itemId` | Live stdout/stderr |
| `item/fileChange/patchUpdated` | `itemId` | Structured patch snapshots when streaming apply-patch is enabled |
Core deltas (`AgentMessageContentDelta`, `ReasoningContentDelta`, `ExecCommandOutputDelta`, etc.) are mapped through `item_event_to_server_notification` in `bespoke_event_handling`.
### Example: agent message stream
<RequestExample>
```json
{ "method": "item/started", "params": {
"threadId": "thr_123",
"turnId": "turn_456",
"startedAtMs": 1730910001000,
"item": { "type": "agentMessage", "id": "msg_1", "text": "" }
} }
```
</RequestExample>
<RequestExample>
```json
{ "method": "item/agentMessage/delta", "params": {
"threadId": "thr_123",
"turnId": "turn_456",
"itemId": "msg_1",
"delta": "Running "
} }
```
</RequestExample>
<RequestExample>
```json
{ "method": "item/completed", "params": {
"threadId": "thr_123",
"turnId": "turn_456",
"completedAtMs": 1730910005000,
"item": { "type": "agentMessage", "id": "msg_1", "text": "Running tests now." }
} }
```
</RequestExample>
Concatenate `delta` strings in order for the same `itemId` to reconstruct streaming text before `item/completed` arrives.
## Token usage
Token accounting is **separate** from turn boundaries:
| Method | Params | Source |
| --- | --- | --- |
| `thread/tokenUsage/updated` | `{ threadId, turnId, tokenUsage }` | Core `TokenCount` events during a turn |
| `account/rateLimits/updated` | `{ rateLimits }` | Same event when rate-limit metadata is present |
`tokenUsage` includes rolling `total` and `last` token breakdowns (including `reasoningOutputTokens` and `cachedInputTokens` when reported) plus optional `modelContextWindow`.
On `thread/resume` or `thread/fork`, persisted usage may be replayed as `thread/tokenUsage/updated` immediately after the RPC response so UIs can show historical usage before the next turn. Pass `excludeTurns: true` on resume/fork to skip that replay.
## Thread status alongside turn events
`thread/status/changed` reflects loaded-thread runtime state (`notLoaded`, `idle`, `active`, `systemError`):
- `note_turn_started` → `active` (empty `activeFlags` while running)
- Pending approvals / user-input server requests add `waitingOnApproval` / `waitingOnUserInput` flags
- `note_turn_completed` or `note_turn_interrupted` → `idle` (or `systemError` after a fatal turn error)
`resolve_thread_status` can promote `idle`/`notLoaded` to `active` when a turn is in progress but status events arrived out of order—clients should not assume strict ordering between `turn/started` and the first `thread/status/changed`.
## Notification opt-out
Per-connection suppression is configured at initialize:
<ParamField body="capabilities.optOutNotificationMethods" type="string[]">
Exact method names to drop for this connection. No wildcards or prefixes. Unknown names are accepted and ignored. Does **not** apply to RPC requests, responses, or errors.
</ParamField>
```json
{
"method": "initialize",
"id": 1,
"params": {
"clientInfo": { "name": "my_client", "title": "My Client", "version": "0.1.0" },
"capabilities": {
"optOutNotificationMethods": [
"thread/started",
"item/agentMessage/delta",
"thread/status/changed"
]
}
}
}
```
| Use case | Methods to opt out |
| --- | --- |
| Headless integrations that poll `thread/read` | `thread/started`, `item/agentMessage/delta` |
| Status from RPC only | `thread/status/changed` |
| High-frequency audio (realtime) | `thread/realtime/outputAudio/delta` |
Filtered notifications are dropped in the transport layer before write—opted-out clients never see them on the wire. Integration tests in `initialize.rs` and `thread_status.rs` verify `thread/started` and `thread/status/changed` filtering.
<Info>
Opt-out applies to typed app-server notifications (`thread/*`, `turn/*`, `item/*`, `rawResponseItem/*`, etc.). Server-initiated **requests** (approvals, MCP elicitation) are unaffected.
</Info>
## Client integration pattern
<Steps>
<Step title="Subscribe to a thread">
Call `thread/start`, `thread/resume`, or `thread/fork`. The connection is auto-subscribed to that thread's event fan-out.
</Step>
<Step title="Start a turn">
Send `turn/start`; record `result.turn.id`. Begin rendering when `turn/started` confirms the same id.
</Step>
<Step title="Consume the notification stream">
Dispatch on `method`. For each `itemId`, maintain partial state from deltas; commit on `item/completed`.
</Step>
<Step title="Finish the turn">
On `turn/completed`, read `turn.status` and `turn.error`. Clear turn-scoped UI; keep accumulated items from notifications.
</Step>
<Step title="Handle parallel traffic">
Process server requests (approvals, `tool/requestUserInput`) in the same read loop—they share the turn timeline with `item/*` events.
</Step>
</Steps>
### Ordering and backpressure
- Notifications share the transport with RPC traffic; use a single reader task per connection.
- When ingress queues saturate, new **requests** receive JSON-RPC `-32001` (`"Server overloaded; retry later."`). Notifications already accepted are not individually backpressured—design UIs to tolerate bursts of deltas or opt out of noisy methods.
### Related turn entry points
| RPC | Streaming behavior |
| --- | --- |
| `review/start` | Same `item/*` + `turn/completed` pattern with review-mode items |
| `thread/compact/start` | Progress via standard turn/item notifications |
| `thread/shellCommand` | Reuses active turn items, or starts `turn/started` → items → `turn/completed` when idle |
| `turn/steer` | Injects input into an in-flight turn; no new `turn/started` |
| `turn/interrupt` | Turn ends with `turn/completed` and `status: "interrupted"` |
## Implementation map
```text
turn/start (turn_processor)
└─ submit_user_input → TurnStartResponse { turn inProgress }
Core event listener (thread_lifecycle)
└─ apply_bespoke_event_handling
├─ TurnStarted → turn/started + ThreadWatchManager::note_turn_started
├─ Item* / deltas → item_event_to_server_notification
├─ TokenCount → thread/tokenUsage/updated
├─ TurnComplete → turn/completed + note_turn_completed
└─ TurnAborted → turn/completed (interrupted)
Outgoing path (transport)
└─ per-connection optOutNotificationMethods filter
```
## Verification
Run the app-server integration suite for turn streaming:
```bash
cd codex-rs && just test -p codex-app-server turn_start
```
Useful focused tests: `tests/suite/v2/turn_start.rs` (started/completed ordering), `tests/suite/v2/thread_status.rs` (status + opt-out), `tests/suite/v2/initialize.rs` (notification filtering).
## Related pages
<CardGroup>
<Card title="Quickstart" href="/quickstart">
End-to-end first connection through `turn/start` and reading `item/*` / `turn/completed`.
</Card>
<Card title="Threads, turns, and items" href="/threads-turns-items">
Core primitives, subscription model, and persisted rollout relationship.
</Card>
<Card title="Connection lifecycle" href="/connection-lifecycle">
Initialize handshake, notification opt-out, and thread subscribe/unsubscribe.
</Card>
<Card title="Notifications and events" href="/notifications-and-events">
Full notification catalog and `ThreadItem` union reference.
</Card>
<Card title="Approvals and server requests" href="/approvals-and-server-requests">
Inline approval flows interleaved with item streaming.
</Card>
</CardGroup>
---
## 11. Approvals and server requests
> Inline approval flows for `commandExecution` and `fileChange`, `serverRequest/resolved` lifecycle, MCP elicitations, attestation `attestation/generate`, and `tool/requestUserInput` handling.
- Page Markdown: https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/pages/11-approvals-and-server-requests.md
- Generated: 2026-06-02T06:39:03.147Z
### Source Files
- `README.md`
- `src/message_processor.rs`
- `src/attestation.rs`
- `src/request_processors/turn_processor.rs`
- `src/request_processors/mcp_processor.rs`
- `src/bespoke_event_handling.rs`
---
title: "Approvals and server requests"
description: "Inline approval flows for `commandExecution` and `fileChange`, `serverRequest/resolved` lifecycle, MCP elicitations, attestation `attestation/generate`, and `tool/requestUserInput` handling."
---
During an active turn, `codex app-server` issues JSON-RPC **requests** from server to client (not notifications). The client must answer with a JSON-RPC `result` (or `error`) so the agent can continue. The server maps core agent events into v2 wire methods, tracks pending request ids per connection, and resumes the turn when `process_response` delivers the matching result. After each resolution—or when a turn boundary clears outstanding prompts—the server emits `serverRequest/resolved` to thread subscribers so UIs can drop inline prompts safely.
## Server request model
| Wire method | Purpose | Typical trigger |
| --- | --- | --- |
| `item/commandExecution/requestApproval` | Approve or deny a sandboxed shell command | `ExecApprovalRequest` during a turn |
| `item/fileChange/requestApproval` | Approve or deny a patch / file write | `ApplyPatchApprovalRequest` during a turn |
| `item/tool/requestUserInput` | Collect 1–3 structured answers for `request_user_input` (experimental) | `RequestUserInput` during a turn |
| `mcpServer/elicitation/request` | MCP `elicitation/create` (form or URL) | `ElicitationRequest` during or outside a turn |
| `item/permissions/requestApproval` | Grant a subset of requested permission profile | `request_permissions` tool |
| `attestation/generate` | Produce opaque attestation token for upstream `x-oai-attestation` | Just-in-time before ChatGPT Codex HTTP calls |
| `item/tool/call` | Run a registered dynamic tool on the client (experimental) | `DynamicToolCallRequest` |
| `account/chatgptAuthTokens/refresh` | Refresh ChatGPT auth tokens | Account layer (not turn-inline) |
v1-only `ApplyPatchApproval` and `ExecCommandApproval` remain in the schema for legacy turn APIs; new integrations should use the v2 `item/*` methods above.
<Note>
Server requests are distinct from high-volume **notifications** (`item/started`, deltas, `turn/completed`). Clients must read the transport continuously and handle interleaved server requests while a turn is in progress.
</Note>
### Routing and correlation
- Turn-inline approvals use `ThreadScopedOutgoingMessageSender`, which sends only to connections subscribed to that `threadId`.
- Attestation uses `send_request_to_connections` with the first initialized connection on that thread where `initialize.params.capabilities.requestAttestation` is `true` (lowest `connection_id` wins).
- Each server request carries a monotonic integer JSON-RPC `id` (server-assigned). Clients echo that `id` in the response.
- Params always include `threadId` and usually `turnId` + `itemId` for turn-bound prompts.
## `serverRequest/resolved` lifecycle
After the client responds—or when the server aborts a still-pending prompt—the app-server enqueues `ResolveServerRequest` on the thread listener so resolution notifications stay ordered with other thread events. Subscribers then receive:
```json
{
"method": "serverRequest/resolved",
"params": {
"threadId": "thr_123",
"requestId": 42
}
}
```
```mermaid
sequenceDiagram
participant Core as codex-core events
participant Bespoke as bespoke_event_handling
participant Out as OutgoingMessageSender
participant Client as JSON-RPC client
participant Listener as thread listener
Core->>Bespoke: ExecApprovalRequest / ElicitationRequest / ...
Bespoke->>Out: send_request(ServerRequestPayload)
Out->>Client: item/.../requestApproval or mcpServer/elicitation/request
alt Client answers in time
Client->>Out: JSON-RPC result (matching id)
Out->>Bespoke: oneshot callback
Bespoke->>Listener: resolve_server_request_on_thread_listener
Listener->>Client: serverRequest/resolved
Bespoke->>Core: Op::PatchApproval / ExecApproval / ResolveElicitation / ...
else Turn start / complete / interrupt / abort
Bespoke->>Out: abort_pending_server_requests
Out-->>Client: JSON-RPC error (reason turnTransition)
Bespoke->>Listener: serverRequest/resolved (cleanup)
end
```
### When resolution fires without a user answer
`ThreadScopedOutgoingMessageSender::abort_pending_server_requests` runs at:
- `TurnStarted` (defensive cleanup before a new turn)
- `TurnComplete`
- `TurnAborted` (including `turn/interrupt`)
Canceled callbacks receive an internal JSON-RPC error with `data.reason = "turnTransition"`. Handlers treat that as a no-op for core submission (no duplicate `decline`).
<Warning>
Integration tests assert `serverRequest/resolved` arrives **before** `turn/completed` when the client answered normally, and still arrives when `turn/interrupt` cancels an outstanding command approval.
</Warning>
Pending server requests for a thread can be replayed to a connection after resume via `replay_requests_to_connection_for_thread`.
## Command execution approvals
Approval policy comes from config and optional `thread/start` / `turn/start` overrides (`approvalPolicy`, `approvalsReviewer`). When a command needs review, the server emits the standard item lifecycle plus a server request.
<Steps>
<Step title="Render the pending item">
Receive `item/started` with `item.type = "commandExecution"` and `status = "inProgress"`. For top-level shell approvals (no `approvalId`), the server may emit this item immediately before the approval request.
</Step>
<Step title="Show the approval prompt">
Handle `item/commandExecution/requestApproval`. Key params: `threadId`, `turnId`, `itemId`, `startedAtMs`, optional `approvalId` (subcommand callbacks), `reason`, and either command fields (`command`, `cwd`, `commandActions`) or `networkApprovalContext` for network-only prompts.
</Step>
<Step title="Respond with a decision">
Send a JSON-RPC response whose `result` is `{ "decision": ... }`.
</Step>
<Step title="Observe completion">
Wait for `serverRequest/resolved`, then `item/completed` with final `status` (`completed`, `failed`, or `declined`).
</Step>
</Steps>
### Decision values
| `decision` | Effect on agent |
| --- | --- |
| `"accept"` | Run the command for this prompt |
| `"acceptForSession"` | Approve and cache for the session |
| `{ "acceptWithExecpolicyAmendment": { "execpolicyAmendment": ... } }` | Approve and persist execpolicy hint |
| `{ "applyNetworkPolicyAmendment": { "networkPolicyAmendment": { "host", "action" } } }` | Approve with network rule |
| `"decline"` | Skip command; turn continues |
| `"cancel"` | Deny and interrupt the turn |
When `initialize.params.capabilities.experimentalApi` is `true`, the request may include `additionalPermissions`, `proposedExecpolicyAmendment`, `proposedNetworkPolicyAmendments`, and `availableDecisions`. Clients without experimental API receive stripped `additionalPermissions` on outbound requests.
<RequestExample>
```json
{
"method": "item/commandExecution/requestApproval",
"id": 7,
"params": {
"threadId": "thr_abc",
"turnId": "turn_abc",
"itemId": "call_shell_1",
"startedAtMs": 1710000000000,
"command": "npm test",
"cwd": "/Users/me/project",
"commandActions": [],
"reason": "Command requires approval"
}
}
```
</RequestExample>
<ResponseExample>
```json
{
"id": 7,
"result": {
"decision": "accept"
}
}
```
</ResponseExample>
## File change approvals
Patch approvals follow the same resolved-notification pattern with a smaller decision set.
<Steps>
<Step title="Show proposed edits">
`item/started` with `item.type = "fileChange"`, `changes[]`, `status = "inProgress"`.
</Step>
<Step title="Prompt">
`item/fileChange/requestApproval` with `threadId`, `turnId`, `itemId`, `startedAtMs`, optional `reason`, optional unstable `grantRoot` for session-scoped write access.
</Step>
<Step title="Respond">
`{ "decision": "accept" | "acceptForSession" | "decline" | "cancel" }`.
</Step>
<Step title="Finalize">
`serverRequest/resolved`, then `item/completed` with updated `status`.
</Step>
</Steps>
| `decision` | Maps to core review |
| --- | --- |
| `accept` | Approved |
| `acceptForSession` | Approved for session |
| `decline` | Denied (turn continues) |
| `cancel` | Abort (turn interrupted) |
Streaming patch previews may arrive as `item/fileChange/patchUpdated` when `features.apply_patch_streaming_events` is enabled.
## `item/tool/requestUserInput`
The built-in `request_user_input` tool blocks the turn until the client answers. The wire method is `item/tool/requestUserInput` (experimental). Params include `questions[]` with `id`, `header`, `question`, optional `options`, and flags `isOther` / `isSecret`.
<ResponseField name="answers" type="object">
Map from question `id` to `{ "answers": ["selected label", ...] }`. Omitted or malformed responses are treated as empty answers; turn-transition aborts skip core submission.
</ResponseField>
<RequestExample>
```json
{
"method": "item/tool/requestUserInput",
"id": 9,
"params": {
"threadId": "thr_abc",
"turnId": "turn_abc",
"itemId": "call1",
"questions": [
{
"id": "confirm_path",
"header": "Confirm",
"question": "Proceed?",
"options": [{ "label": "yes", "description": "Continue" }]
}
]
}
}
```
</RequestExample>
<ResponseExample>
```json
{
"id": 9,
"result": {
"answers": {
"confirm_path": { "answers": ["yes"] }
}
}
}
```
</ResponseExample>
## MCP server elicitations
MCP servers call `elicitation/create`; app-server forwards them as `mcpServer/elicitation/request`. The payload is tagged by `mode`:
- **form** — `message`, `requestedSchema` (JSON Schema object), optional `_meta`
- **url** — `message`, `url`, `elicitationId`, optional `_meta`
`turnId` is nullable: present when correlated with an active turn, otherwise `null` for out-of-band elicitations. Clients can call `thread/incrementElicitation` / `thread/decrementElicitation` to pause turn progress while showing external UI.
<ResponseExample>
```json
{
"id": 12,
"result": {
"action": "accept",
"content": { "confirm": true },
"_meta": null
}
}
```
</ResponseExample>
| `action` | Server behavior |
| --- | --- |
| `accept` | Forward content to `Op::ResolveElicitation` |
| `decline` | Deny; turn may continue |
| `cancel` | Abort elicitation |
<Info>
**Compatibility:** connections identifying as Xcode `26.4` auto-deny MCP elicitations at turn start because that client predates visible elicitation requests.
</Info>
For MCP tool approval elicitations, form `_meta` may include `codex_approval_kind: "mcp_tool_call"` and `persist` hints (`session`, `always`, or both) so clients can offer durable approval choices.
## Attestation (`attestation/generate`)
Desktop hosts that can produce upstream attestation should set `capabilities.requestAttestation: true` during `initialize`. Before ChatGPT Codex requests that need `x-oai-attestation`, `AppServerAttestationProvider` issues `attestation/generate` to the first attestation-capable subscriber for that thread (100 ms timeout).
<ParamField body="token" type="string" required>
Opaque client-owned value, typically `v1.<payload>`.
</ParamField>
App-server wraps the client token into header JSON `{ "v": 1, "s": 0, "t": "<token>" }`. Failure statuses omit `t`:
| `s` | Meaning |
| --- | --- |
| `0` | Success |
| `1` | Timeout (request canceled server-side) |
| `2` | Request failed (client JSON-RPC error) |
| `3` | Request canceled |
| `4` | Malformed client response |
If no connection opted in, upstream requests omit `x-oai-attestation` entirely.
## Permission requests (related)
The `request_permissions` tool uses `item/permissions/requestApproval` with a `permissions` profile (network + filesystem paths). Clients return the **granted subset** in `result.permissions`, optionally `scope: "session"` for sticky grants within the session. Omitted permissions are denied; extra permissions in the response are ignored. Granular approval policy with `request_permissions: false` auto-denies standalone permission tool calls without a prompt.
## Client implementation checklist
- Read JSON-RPC messages concurrently: notifications, client RPC responses, and **server requests** share one transport.
- Match UI state with `threadId`, `turnId`, and `itemId`; stash `requestId` until `serverRequest/resolved`.
- Respond to every server request promptly; the agent thread blocks on the oneshot channel.
- Treat `turnTransition` errors as cleanup, not user decline.
- For attestation, implement `attestation/generate` only when the host can supply real tokens.
- Regenerate TypeScript/JSON Schema from the running binary after protocol changes (`codex app-server generate-ts` / `generate-json-schema`).
## Related pages
<CardGroup>
<Card title="Build a JSON-RPC client" href="/build-jsonrpc-client">
Handle server requests mid-turn, request ids, and `initialized` ordering.
</Card>
<Card title="Stream turns and events" href="/stream-turns-and-events">
Item lifecycle, deltas, and `turn/completed` after approvals finish.
</Card>
<Card title="Connection lifecycle" href="/connection-lifecycle">
Initialize capabilities, thread subscribe, and server vs client RPC roles.
</Card>
<Card title="Skills, plugins, and MCP" href="/skills-plugins-and-mcp">
MCP OAuth, tool calls, and elicitation-related server configuration.
</Card>
<Card title="RPC methods reference" href="/rpc-methods">
Full v2 method catalog including experimental markers.
</Card>
</CardGroup>
---
## 12. Account, auth, and config
> Account login flows (`apiKey`, `chatgpt`, device code), config read/write/batch RPC, requirements.toml constraints, and hot-reload behavior after `config/batchWrite`.
- Page Markdown: https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/pages/12-account-auth-and-config.md
- Generated: 2026-06-02T06:39:14.252Z
### Source Files
- `README.md`
- `src/request_processors/account_processor.rs`
- `src/request_processors/config_processor.rs`
- `src/config_manager.rs`
- `src/config_manager_service.rs`
- `tests/suite/v2/config_rpc.rs`
- `tests/suite/auth.rs`
---
title: "Account, auth, and config"
description: "Account login flows (`apiKey`, `chatgpt`, device code), config read/write/batch RPC, requirements.toml constraints, and hot-reload behavior after `config/batchWrite`."
---
`codex app-server` exposes account lifecycle, configuration persistence, and policy constraints as JSON-RPC v2 methods handled by `AccountRequestProcessor` and `ConfigRequestProcessor`, backed by `ConfigManager` / `ConfigManagerService` for layered config resolution and user `config.toml` writes.
```text
Client JSON-RPC
|
v
MessageProcessor
/ \
v v
AccountRequest ConfigRequest
Processor Processor
| |
v v
AuthManager ConfigManager ----> ConfigManagerService
| | |
v +--> load_config_layers / apply_edits
login_with_* |
refresh tokens +--> $CODEX_HOME/config.toml (user layer only)
```
## Account and authentication RPCs
| Method | Role |
| --- | --- |
| `account/read` | Current account (`apiKey`, `chatgpt`, `amazonBedrock`, or `null`); optional `refreshToken` |
| `account/login/start` | Start login (`apiKey`, `chatgpt`, `chatgptDeviceCode`, experimental `chatgptAuthTokens`) |
| `account/login/cancel` | Cancel in-flight browser or device-code login by `loginId` |
| `account/logout` | Revoke and clear credentials; emits `account/updated` |
| `account/rateLimits/read` | ChatGPT rate-limit snapshot (requires ChatGPT / Codex backend auth) |
| `account/sendAddCreditsNudgeEmail` | Notify workspace owner (`creditType`: `credits` or `usage_limit`) |
| `getAuthStatus` | Legacy v1 probe: `authMethod`, optional `authToken`, `requiresOpenaiAuth` |
| `account/chatgptAuthTokens/refresh` | **Server request** (not client RPC) when external auth needs new tokens |
Notifications:
| Notification | When |
| --- | --- |
| `account/login/completed` | Any login attempt finishes (`loginId`, `success`, `error`) |
| `account/updated` | Auth mode or ChatGPT `planType` changes (`authMode`: `apikey`, `chatgpt`, `chatgptAuthTokens`, `agentIdentity`, or `null`) |
| `account/rateLimits/updated` | Sparse rolling rate-limit deltas |
<Info>
`authMode` values use lowercase wire names (`apikey`, `chatgpt`, …). `account/read` returns structured `account` objects with camelCase variant tags (`apiKey`, `chatgpt`, `amazonBedrock`).
</Info>
### Login flows
:::endpoint POST account/login/start Begin authentication
**`type: "apiKey"`** — Synchronous. Persists the key under `codex_home`, reloads `AuthManager`, responds with `{ "type": "apiKey" }`, then emits `account/login/completed` and `account/updated`. Cancels any active browser/device login first.
**`type: "chatgpt"`** — Browser OAuth. Returns `{ "type": "chatgpt", "loginId", "authUrl" }` immediately; app-server runs a local callback server (`open_browser: false`). A background task waits up to **10 minutes**, then emits completion notifications. On success: reload auth, refresh cloud requirements loader, sync residency requirement, optionally refresh remote plugin cache.
**`type: "chatgptDeviceCode"`** — Device code flow. Returns `{ "loginId", "verificationUrl", "userCode" }`; polls completion in the background. Cancel via `account/login/cancel` or replacement login.
**`type: "chatgptAuthTokens"`** (experimental) — External/host-managed ChatGPT tokens. Client supplies `accessToken`, `chatgptAccountId`, optional `chatgptPlanType`. Tokens stay in memory; refresh is the client’s responsibility via another `chatgptAuthTokens` login or answering `account/chatgptAuthTokens/refresh` server requests.
Optional param on browser login: `codexStreamlinedLogin` (boolean, default omitted/false).
:::
<Steps>
<Step title="API key login">
<RequestExample>
```json
{ "method": "account/login/start", "id": 1, "params": { "type": "apiKey", "apiKey": "sk-…" } }
```
</RequestExample>
<ResponseExample>
```json
{ "id": 1, "result": { "type": "apiKey" } }
```
</ResponseExample>
Expect `account/login/completed` then `account/updated` with `"authMode": "apikey"`.
</Step>
<Step title="ChatGPT browser login">
<RequestExample>
```json
{ "method": "account/login/start", "id": 2, "params": { "type": "chatgpt" } }
```
</RequestExample>
<ResponseExample>
```json
{ "id": 2, "result": { "type": "chatgpt", "loginId": "<uuid>", "authUrl": "https://…" } }
```
</ResponseExample>
Open `authUrl` in your UI; wait for `account/login/completed` with matching `loginId`.
</Step>
<Step title="Device code login">
<RequestExample>
```json
{ "method": "account/login/start", "id": 3, "params": { "type": "chatgptDeviceCode" } }
```
</RequestExample>
<ResponseExample>
```json
{
"id": 3,
"result": {
"type": "chatgptDeviceCode",
"loginId": "<uuid>",
"verificationUrl": "https://auth.openai.com/codex/device",
"userCode": "ABCD-1234"
}
}
```
</ResponseExample>
Display `verificationUrl` and `userCode`; handle completion notifications like the browser flow.
</Step>
</Steps>
### `getAuthStatus` (v1)
Use when you need a lightweight auth probe without full account metadata.
<ParamField body="includeToken" type="boolean | null">
When `true`, include bearer token in `authToken` when available.
</ParamField>
<ParamField body="refreshToken" type="boolean | null">
When `true`, proactively refresh managed ChatGPT tokens before responding. Ignored for external `chatgptAuthTokens` auth.
</ParamField>
<ResponseField name="requiresOpenaiAuth" type="boolean | null">
`false` when the active model provider sets `requires_openai_auth = false` (local/OSS providers). Otherwise `true` and credentials may be required.
</ResponseField>
Permanent refresh failures still return `authMethod` but omit `authToken` even when `includeToken` is true.
### Policy and external-auth constraints
| Constraint | Behavior |
| --- | --- |
| `forced_login_method = "chatgpt"` in config | Rejects `apiKey` login |
| `forced_login_method = "api"` | Rejects ChatGPT login flows |
| `forced_chatgpt_workspace_id` | Restricts `chatgptAuthTokens` to listed workspace IDs |
| Active external ChatGPT auth | Blocks managed `apiKey` / `chatgpt` / `chatgptDeviceCode` until logout or `chatgptAuthTokens` update |
<Warning>
While external auth is active, managed login returns: `External auth is active. Use account/login/start (chatgptAuthTokens) to update it or account/logout to clear it.`
</Warning>
On `401` during a turn with external auth, app-server sends `account/chatgptAuthTokens/refresh` to the client (10s timeout). The client must respond with fresh tokens; mismatched workspace or invalid tokens fail the turn.
## Config RPCs
| Method | Writes disk | Hot-reloads threads |
| --- | --- | --- |
| `config/read` | No | No |
| `config/value/write` | User `config.toml` | No (clears plugin/skill caches) |
| `config/batchWrite` | User `config.toml` (atomic) | Optional via `reloadUserConfig` |
| `configRequirements/read` | No | No |
| `experimentalFeature/enablement/set` | In-memory runtime flags | Yes (always reloads) |
### Wire naming
- **Request/response envelopes** use camelCase (`includeLayers`, `keyPath`, `mergeStrategy`, `reloadUserConfig`).
- **`config` object in `config/read`** uses **snake_case** field names mirroring `config.toml` (`sandbox_mode`, `model_provider`, `forced_login_method`, …).
- **`keyPath` segments** use the same snake_case paths as on disk (`sandbox_mode`, `hooks.state`, `desktop.someKey`). Quoted segments support special characters (`profiles` tables are rejected for writes).
### `config/read`
<ParamField body="includeLayers" type="boolean">
Default `false`. When `true`, returns per-layer configs plus `origins` map (which layer won for each key).
</ParamField>
<ParamField body="cwd" type="string | null">
Optional absolute working directory to include project `.codex/` layers between `cwd` and the repo root.
</ParamField>
<ResponseField name="config" type="object">
Effective merged configuration after layer resolution.
</ResponseField>
<ResponseField name="origins" type="object">
Map from dotted key paths to winning `ConfigLayerSource` (user, project, system, MDM, enterprise-managed, session flags, …).
</ResponseField>
Runtime feature flags under `config.additional.features` are injected for supported keys (`apps`, `memories`, `mentions_v2`, `plugins`, `remote_control`, `remote_plugin`, `tool_suggest`, `tool_call_mcp_elicitation`) reflecting effective enablement.
### `config/value/write` and `config/batchWrite`
Both routes persist **only** the user layer at `$CODEX_HOME/config.toml` (or an explicit `filePath` that normalizes to the same path). Managed, project, MDM, and enterprise layers are read-only through this API.
<ParamField body="keyPath" type="string" required>
Dotted path; `null` JSON value clears the key.
</ParamField>
<ParamField body="mergeStrategy" type="replace | upsert" required>
`replace` sets the value. `upsert` deep-merges when both existing and new values are tables.
</ParamField>
<ParamField body="expectedVersion" type="string | null">
Optimistic concurrency token from the last read/write `version`. Mismatch → `configVersionConflict`.
</ParamField>
<ParamField body="reloadUserConfig" type="boolean">
**`config/batchWrite` only.** When `true`, reload effective config into every loaded thread after a successful write.
</ParamField>
<ResponseField name="status" type="ok | okOverridden">
`okOverridden` when a higher-precedence layer still wins for at least one edited key; check `overriddenMetadata` for the effective value and overriding layer message.
</ResponseField>
<ResponseField name="version" type="string">
Pass as `expectedVersion` on the next write.
</ResponseField>
Write errors attach `data.config_write_error_code`:
| Code | Typical cause |
| --- | --- |
| `configLayerReadonly` | Target path is not user config |
| `configVersionConflict` | Stale `expectedVersion` |
| `configValidationError` | Invalid TOML shape, feature requirements, legacy `profile`/`profiles` writes |
| `userLayerNotFound` | Internal layer resolution failure after write |
<RequestExample>
```json
{
"method": "config/batchWrite",
"id": 10,
"params": {
"edits": [
{
"keyPath": "sandbox_mode",
"value": "workspace-write",
"mergeStrategy": "replace"
},
{
"keyPath": "hooks.state",
"value": { "/path/to/config.toml:pre_tool_use:0:0": { "enabled": false } },
"mergeStrategy": "upsert"
}
],
"reloadUserConfig": true
}
}
```
</RequestExample>
Every successful config mutation clears the plugins and skills manager caches. MCP servers are **not** restarted automatically—use `config/mcpServer/reload` after MCP-related edits if threads are already loaded.
## `configRequirements/read`
Returns merged constraints from `requirements.toml`, MDM, and cloud loaders, or `requirements: null` when nothing is configured.
<ResponseField name="requirements" type="object | null">
Policy envelope (camelCase fields), including:
</ResponseField>
- `allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`, `allowedPermissions`
- `allowManagedHooksOnly`, `allowAppshots`, `computerUse`
- `featureRequirements` — pinned feature booleans
- `hooks` — managed lifecycle hook definitions
- `enforceResidency` — e.g. `us`
- `network` — domain/socket permissions, `managedAllowedDomainsOnly`, proxy flags
Writes validate against `feature_requirements` in the effective layer stack; violating enablement returns `configValidationError`.
Cloud requirements reload when ChatGPT login succeeds (`replace_cloud_requirements_loader` + `sync_default_client_residency_requirement`).
## Hot reload after `config/batchWrite`
When `reloadUserConfig` is `true`:
```mermaid
sequenceDiagram
participant Client
participant ConfigProcessor
participant ConfigManager
participant ThreadManager
Client->>ConfigProcessor: config/batchWrite (reloadUserConfig: true)
ConfigProcessor->>ConfigManager: batch_write → persist config.toml
ConfigProcessor->>ConfigProcessor: handle_config_mutation (clear caches)
ConfigProcessor->>ConfigManager: load_latest_config
loop each loaded thread_id
ConfigProcessor->>ThreadManager: get_thread
ThreadManager-->>ConfigProcessor: Thread
ConfigProcessor->>Thread: refresh_runtime_config(next_config)
end
ConfigProcessor-->>Client: ConfigWriteResponse
```
<Note>
`config/value/write` and `config/batchWrite` with `reloadUserConfig: false` still invalidate plugin/skill caches but **do not** push new config into active threads. Loaded sessions keep prior runtime settings until the next `reloadUserConfig: true` batch write, a new thread start/resume, or `experimentalFeature/enablement/set` (which always reloads).
</Note>
Integration tests confirm hook trust and `hooks.state` changes affect an in-flight session only when `reloadUserConfig: true`.
## Operational notes
- Debug builds honor `CODEX_APP_SERVER_LOGIN_ISSUER` for ChatGPT login issuer override.
- ChatGPT browser login timeout: **10 minutes**; device-code cancel surfaces `"Login was not completed"`.
- After login/logout, app-server may refresh remote installed plugin caches and queue MCP refresh when threads are loaded.
- For auth worked examples and rate-limit field semantics, see the app-server README auth section; for exhaustive config key reference, see the dedicated config RPC page.
## Related pages
<CardGroup>
<Card title="Config RPC reference" href="/config-rpc">
Snake_case config shapes, MCP reload, external-agent import, and write error catalog.
</Card>
<Card title="Connection lifecycle" href="/connection-lifecycle">
Initialize handshake before account or config RPCs.
</Card>
<Card title="Skills, plugins, and MCP" href="/skills-plugins-and-mcp">
Cache invalidation and `config/mcpServer/reload` after config edits.
</Card>
<Card title="Experimental API" href="/experimental-api">
`chatgptAuthTokens` login and gated `configRequirements` fields.
</Card>
</CardGroup>
---
## 13. Skills, plugins, and MCP
> Listing and configuring skills, plugin marketplace install flows, `mcpServerStatus/list`, OAuth login, tool calls, reload after config edits, and `skills/changed` notifications.
- Page Markdown: https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/pages/13-skills-plugins-and-mcp.md
- Generated: 2026-06-02T06:39:47.109Z
### Source Files
- `README.md`
- `src/skills_watcher.rs`
- `src/mcp_refresh.rs`
- `src/request_processors/mcp_processor.rs`
- `src/request_processors/marketplace_processor.rs`
- `src/request_processors/plugins.rs`
- `tests/suite/v2/mcp_server_status.rs`
---
title: "Skills, plugins, and MCP"
description: "Listing and configuring skills, plugin marketplace install flows, `mcpServerStatus/list`, OAuth login, tool calls, reload after config edits, and `skills/changed` notifications."
---
`codex app-server` exposes v2 JSON-RPC methods for discovering skills and plugins, mutating user config, and operating MCP servers configured in `config.toml`. Processors in `MessageProcessor` route catalog calls (`skills/*`, `hooks/list`), marketplace/plugin calls, and MCP calls; `SkillsWatcher` watches skill roots and emits `skills/changed`; `mcp_refresh` queues `RefreshMcpServers` on loaded threads after MCP-related config or plugin changes.
## Architecture
```mermaid
flowchart TB
subgraph client [Client]
RPC[JSON-RPC client]
end
subgraph app_server [codex app-server]
MP[MessageProcessor]
CP[CatalogProcessor]
PP[PluginRequestProcessor]
MKP[MarketplaceRequestProcessor]
MCP[McpRequestProcessor]
SW[SkillsWatcher]
MR[mcp_refresh]
end
subgraph core [ThreadManager / core]
SM[SkillsManager]
PM[PluginsManager]
MM[McpManager]
CT[CodexThread]
end
RPC --> MP
MP --> CP
MP --> PP
MP --> MKP
MP --> MCP
CP --> SM
CP --> SW
PP --> PM
PP --> MR
MCP --> MM
MCP --> MR
MR --> CT
SW --> SM
SW -->|skills/changed| RPC
MCP -->|deferred JSON-RPC result| RPC
```
| Component | Responsibility |
| --- | --- |
| `CatalogProcessor` | `skills/list`, `skills/extraRoots/set`, `skills/config/write` |
| `PluginRequestProcessor` | `plugin/*`, plugin-driven cache clears, best-effort MCP refresh |
| `MarketplaceRequestProcessor` | `marketplace/add`, `marketplace/remove`, `marketplace/upgrade` |
| `McpRequestProcessor` | `mcpServerStatus/list`, `mcpServer/oauth/login`, `mcpServer/tool/call`, `mcpServer/resource/read`, `config/mcpServer/reload` |
| `SkillsWatcher` | Registers watch roots per thread/environment; throttled file events → cache clear + `skills/changed` |
| `mcp_refresh` | Reloads config, builds `McpServerRefreshConfig`, submits `Op::RefreshMcpServers` per loaded thread |
<Note>
Several `plugin/*` methods are marked **under development** in `README.md` — avoid production clients until the API stabilizes.
</Note>
## Skills
### List and enable
`skills/list` returns skills per `cwd`, defaulting to the server session cwd when `cwds` is empty. Results merge standalone skill roots, bundled skills (when enabled), and plugin skill roots when the `Plugins` feature and workspace policy allow Codex plugins.
<ParamField body="cwds" type="PathBuf[]">
Working directories to resolve config layers and discover skills. Empty → session cwd.
</ParamField>
<ParamField body="forceReload" type="boolean" default="false">
When `true`, bypasses the per-cwd skills cache and rescans disk.
</ParamField>
Per-entry failures (invalid cwd, config load errors) appear in `errors[]` without failing the whole request.
`skills/config/write` persists enable/disable to user `config.toml` via `path` **or** `name` (exactly one required). It clears plugin and skills caches; it does not emit `skills/changed`.
### Runtime extra roots
`skills/extraRoots/set` replaces process-wide standalone skill roots (not persisted). It registers paths with `SkillsWatcher`, updates `SkillsManager`, and immediately sends `skills/changed`.
Layout: each root contains skill directories; each directory has `SKILL.md`. Missing directories are accepted and contribute no skills until they exist.
### File watching and `skills/changed`
On `thread/start` / subscribe, `SkillsWatcher.register_thread_config` watches skill roots derived from cwd, plugin effective skill roots, and environment filesystem (skips remote environments). Runtime `extraRoots` are watched separately.
On filesystem events (throttled to ~10s in production):
1. `SkillsManager.clear_cache()`
2. Server notification `skills/changed` with empty params `{}`
Treat `skills/changed` as an **invalidation signal** — re-call `skills/list` with your current `cwds` / `forceReload` when the UI needs fresh data.
<RequestExample>
```json
{ "method": "skills/list", "id": 25, "params": {
"cwds": ["/Users/me/project"],
"forceReload": true
} }
```
</RequestExample>
<RequestExample>
```json
{ "method": "skills/changed", "params": {} }
```
</RequestExample>
### Invoking skills in turns
Include `Loading wiki...lt;skill-name>` in text and add a `skill` input item with `name` and `path` from `skills/list` so the backend injects full instructions. Omitting the `skill` item forces the model to resolve the name and adds latency.
## Plugins and marketplaces
Plugin discovery is **provider-neutral**: marketplaces are local Git checkouts, repo-scoped manifests, or remote catalogs keyed off `chatgpt_base_url` and ChatGPT auth — not a single model vendor.
### Marketplace install flow
<Steps>
<Step title="Add marketplace">
`marketplace/add` accepts HTTP(S) Git URL, SSH Git URL, or GitHub `owner/repo` shorthand. It persists into user marketplace config under `codex_home` and returns `marketplaceName`, `installedRoot`, and `alreadyAdded`.
</Step>
<Step title="Upgrade or remove">
`marketplace/upgrade` upgrades configured Git marketplaces (all, or one `marketplaceName`). Response includes `selectedMarketplaces`, `upgradedRoots`, and per-marketplace `errors`.
`marketplace/remove` drops a marketplace from config and deletes its installed root when present.
</Step>
<Step title="Discover plugins">
`plugin/list` — catalog view with `marketplaceKinds` filter (`local`, `vertical`, `workspace-directory`, `shared-with-me`). Default kinds: local only, plus remote global catalog when `RemotePlugin` feature is on.
`plugin/installed` — installed rows plus optional `installSuggestionPluginNames` for mention UIs.
`plugin/read` — detail for one plugin via **exactly one** of `marketplacePath` or `remoteMarketplaceName`, plus `pluginName`. Includes bundled `skills`, `hooks`, `apps`, and MCP server names.
`plugin/skill/read` — remote skill markdown preview without installing (`remoteMarketplaceName`, `remotePluginId`, `skillName`).
</Step>
<Step title="Install or uninstall">
`plugin/install` — local: `marketplacePath` + `pluginName`. Remote: `remoteMarketplaceName` + `pluginName` (remote plugin id). Returns `authPolicy` and `appsNeedingAuth`.
After install, bundled MCP servers may trigger silent OAuth (`mcpServer/oauthLogin/completed`). Plugin caches clear and **best-effort** MCP refresh runs on loaded threads.
`plugin/uninstall` — local `pluginId` (`<plugin>@<marketplace>`) or remote backend id.
</Step>
</Steps>
<Warning>
`plugin/list`, `plugin/installed`, `plugin/read`, `plugin/install`, and `plugin/uninstall` are documented as under development in `README.md`.
</Warning>
### Plugin mentions in turns
Use `@<plugin>` in text plus a `mention` item:
```json
{ "type": "mention", "name": "Sample Plugin", "path": "plugin://sample@test" }
```
Paths come from `plugin/installed` or `plugin/list`.
### Effective plugin changes → MCP
`PluginRequestProcessor::on_effective_plugins_changed` clears plugin and skills caches. If any threads are loaded, it calls `queue_best_effort_refresh` (continues on per-thread failures). Same pattern runs after account-driven remote plugin cache refresh.
## MCP servers
MCP servers are declared under `[mcp_servers.<name>]` in layered `config.toml` (user, project `.codex/config.toml`, plugins). App-server does not start long-lived MCP connections for status listing; it probes inventory and delegates live tool calls to the target `CodexThread`.
### `mcpServerStatus/list`
Async RPC: handler returns immediately; the JSON-RPC **result** arrives on the same connection id after background work.
<ParamField body="threadId" type="string">
When set, config is resolved for that thread (includes project-local MCP entries). When omitted, uses latest global config only — project MCP servers are **not** visible without `threadId`.
</ParamField>
<ParamField body="detail" type="full | toolsAndAuthOnly" default="full">
`full` — tools, auth, server info, resources, resource templates. `toolsAndAuthOnly` — skips slow resource/template inventory (returns within ~500ms in tests).
</ParamField>
<ParamField body="cursor" type="string">
Opaque pagination index (stringified usize). Invalid cursor → invalid request.
</ParamField>
<ParamField body="limit" type="number">
Page size; defaults to all servers when omitted.
</ParamField>
<ResponseField name="data" type="McpServerStatus[]">
Each entry: `name`, `serverInfo`, `tools` (map by tool name), `resources`, `resourceTemplates`, `authStatus` (`unsupported`, `notLoggedIn`, `bearerToken`, `oauth`).
</ResponseField>
<ResponseField name="nextCursor" type="string | null">
Present when more servers remain after this page.
</ResponseField>
### OAuth login
`mcpServer/oauth/login` requires a configured server name. OAuth is supported only for **streamable HTTP** transports.
<ParamField body="name" type="string" required>
MCP server key from `config.toml`.
</ParamField>
<ParamField body="scopes" type="string[]">
Optional explicit scopes; server config scopes and discovery may apply when omitted.
</ParamField>
<ParamField body="timeoutSecs" type="number">
Optional login timeout.
</ParamField>
Returns `{ "authorizationUrl": "..." }` immediately. Completion is asynchronous:
```json
{ "method": "mcpServer/oauthLogin/completed", "params": {
"name": "my-server",
"success": true,
"error": null
} }
```
`plugin/install` may spawn silent OAuth for each bundled MCP server that supports login, using the same notification.
### Tool calls and resources
`mcpServer/tool/call` requires `threadId`, `server`, `tool`; optional `arguments` and `_meta`. The server injects `threadId` into `_meta` under key `threadId` for downstream MCP handlers. Result is deferred (spawned task → `send_result`).
`mcpServer/resource/read` accepts optional `threadId`. With `threadId`, reads via the thread; without, uses latest global MCP config and config cwd as stdio fallback.
During turns, MCP activity also surfaces as thread items (`mcpToolCall`) and notifications (`mcpServer/startupStatus/updated`, `mcpServer/elicitation/request`) — see approvals and notifications pages.
### Reload after config edits
| Method | Behavior |
| --- | --- |
| `config/mcpServer/reload` | **Strict**: `load_latest_config`, rebuild refresh config per loaded thread, queue `Op::RefreshMcpServers`. Fails the RPC if any thread cannot load refresh config. |
| `config/batchWrite` with `reloadUserConfig: true` | Hot-reloads thread runtime config via `refresh_runtime_config`; does **not** automatically queue MCP server refresh. |
| Plugin install/uninstall / effective plugin change | **Best-effort** MCP refresh per thread; warnings on individual thread failures. |
<Info>
After editing `[mcp_servers.*]` in `config.toml` while app-server stays up, call `config/mcpServer/reload`. Refresh applies on each thread's next active turn via `RefreshMcpServers`.
</Info>
<RequestExample>
```json
{ "method": "config/mcpServer/reload", "id": 40, "params": null }
{ "id": 40, "result": {} }
```
</RequestExample>
### Startup status
For loaded threads, `mcpServer/startupStatus/updated` reports `{ name, status, error }` where `status` is `starting`, `ready`, `failed`, or `cancelled`.
## RPC quick reference
| Method | Sync response | Notes |
| --- | --- | --- |
| `skills/list` | Yes | Per-cwd `data[]` with `skills`, `errors` |
| `skills/extraRoots/set` | Yes | Emits `skills/changed` |
| `skills/config/write` | Yes | `path` xor `name` |
| `marketplace/add` | Yes | Git remote → user config |
| `marketplace/remove` | Yes | |
| `marketplace/upgrade` | Yes | Partial errors in `errors[]` |
| `plugin/list` | Yes | Feature + workspace gated |
| `plugin/installed` | Yes | Narrower than `plugin/list` |
| `plugin/read` | Yes | Local or remote marketplace |
| `plugin/install` | Yes | May trigger OAuth notifications |
| `plugin/uninstall` | Yes | Local or remote id |
| `mcpServerStatus/list` | **Deferred** | Pagination; optional `threadId` |
| `mcpServer/oauth/login` | Yes (+ notify) | HTTP streamable only |
| `mcpServer/tool/call` | **Deferred** | Requires `threadId` |
| `mcpServer/resource/read` | **Deferred** | |
| `config/mcpServer/reload` | Yes | Strict MCP refresh |
Serialization groups (from protocol): skills/hooks/marketplace use `global("config")`; MCP registry methods use `global("mcp-registry")`; `mcpServer/tool/call` uses per-thread serialization.
## Client workflow (checklist)
```text
Skills UI
initialize → skills/list(cwds)
subscribe to skills/changed → skills/list(forceReload?) on notify
skills/config/write or skills/extraRoots/set for prefs/roots
Plugin UI
marketplace/add → plugin/list → plugin/read → plugin/install
plugin/installed for mention paths → turn/start with mention item
MCP UI
thread/start (for project MCP) → mcpServerStatus/list(threadId)
mcpServer/oauth/login → open authorizationUrl → wait oauthLogin/completed
edit config.toml → config/mcpServer/reload
mcpServer/tool/call(threadId, ...) for direct tool invocation
```
<Tip>
For MCP inventory in a workspace with `.codex/config.toml`, always pass the active `threadId` to `mcpServerStatus/list`. Threadless calls only see home/global config.
</Tip>
## Related pages
<CardGroup>
<Card title="Config RPC reference" href="/config-rpc">
`config/read`, `config/batchWrite`, and snake_case config fields including `[mcp_servers]`.
</Card>
<Card title="RPC methods reference" href="/rpc-methods">
Full v2 method catalog with stable vs experimental markers.
</Card>
<Card title="Approvals and server requests" href="/approvals-and-server-requests">
MCP elicitations, tool approvals, and `serverRequest/resolved`.
</Card>
<Card title="Notifications and events" href="/notifications-and-events">
`mcpServer/startupStatus/updated`, `mcpToolCall` items, and turn streaming.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
MCP `failed` startup, OAuth errors, and strict reload failures.
</Card>
</CardGroup>
---
## 14. In-process embedding
> Using `in_process` to run `MessageProcessor` without a socket, internal initialize handshake, `InProcessClientHandle` request/event channels, backpressure, and relationship to `codex-app-server-client`.
- Page Markdown: https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/pages/14-in-process-embedding.md
- Generated: 2026-06-02T06:39:53.398Z
### Source Files
- `src/in_process.rs`
- `src/lib.rs`
- `src/message_processor.rs`
- `src/error_code.rs`
- `src/transport.rs`
- `README.md`
---
title: "In-process embedding"
description: "Using `in_process` to run `MessageProcessor` without a socket, internal initialize handshake, `InProcessClientHandle` request/event channels, backpressure, and relationship to `codex-app-server-client`."
---
The `codex-app-server` crate exposes `in_process` (`src/in_process.rs`) to run the same `MessageProcessor` and outbound routing stack as socket transports, but over bounded Tokio `mpsc` channels inside the caller’s process. CLI surfaces (`codex-tui`, `codex-exec`) typically use the sibling `codex-app-server-client` facade; integration tests and advanced embedders can call `in_process::start` directly.
## When to use in-process vs a socket transport
| Approach | Process boundary | Wire format | Typical consumer |
| --- | --- | --- | --- |
| `in_process::start` | None | Typed `ClientRequest` / `ClientNotification` in; JSON-RPC result envelope out | Tests, custom embedders |
| `codex-app-server-client` | None | Same semantics + worker task + `AppServerEvent` | TUI, exec |
| `--listen stdio://` / unix / websocket | Separate process or stream | JSON-RPC JSONL or websocket frames | VS Code, proxies, remote clients |
<Note>
In-process is transport-local, not protocol-free: successful RPC results still arrive as JSON-RPC `result` values because `MessageProcessor` produces that shape internally. Callers deserialize with `serde_json` (or `request_typed` in `codex-app-server-client`).
</Note>
## Architecture
```mermaid
flowchart TB
subgraph embedder["Embedder (TUI / exec / test)"]
Facade["codex-app-server-client\n(optional)"]
Handle["InProcessClientHandle"]
end
subgraph runtime["in_process Tokio runtime task"]
ClientRx["client_rx\n(InProcessClientMessage)"]
ProcTx["processor_tx\n(ProcessorCommand)"]
MP["MessageProcessor"]
OutRouter["route_outgoing_envelope"]
WriterRx["writer_rx\n(QueuedOutgoingMessage)"]
EventTx["event_tx\n(InProcessServerEvent)"]
end
Facade --> Handle
Handle -->|request / notify / server reply| ClientRx
ClientRx --> ProcTx --> MP
MP --> OutRouter --> WriterRx
WriterRx -->|responses| ClientRx
WriterRx -->|ServerRequest / notifications| EventTx
EventTx --> Handle
```
Single logical connection: `IN_PROCESS_CONNECTION_ID` (`ConnectionId(0)`). There is no stdio/socket accept loop; `remote_control_handle` is `None` and analytics records `AppServerRpcTransport::InProcess`.
## Startup: `InProcessStartArgs` and `start`
<Steps>
<Step title="Build runtime inputs">
Populate `InProcessStartArgs` with the same ambient state `run_main_with_transport_options` assembles before `MessageProcessor::new`: `Arg0DispatchPaths`, `Arc<Config>`, CLI TOML overrides, `LoaderOverrides`, `strict_config`, cloud requirements, `ThreadConfigLoader`, `CodexFeedback`, optional `LogDbLayer` / `StateDbHandle`, `EnvironmentManager`, config warnings, `SessionSource`, `enable_codex_api_key_env`, `InitializeParams`, and `channel_capacity`.
</Step>
<Step title="Call `in_process::start`">
`start(args)` calls `start_uninitialized`, spawns the runtime task, then performs the handshake before returning:
1. `ClientRequest::Initialize` with `args.initialize` (default request id `0`).
2. On success, `ClientNotification::Initialized`.
3. Returns `InProcessClientHandle` ready for RPCs.
If initialize returns a JSON-RPC error, the runtime shuts down and `start` returns `InvalidData`.
</Step>
<Step title="Use the handle">
- `request(ClientRequest)` → `IoResult<Result<serde_json::Value, JSONRPCErrorError>>`
- `notify(ClientNotification)` → `IoResult<()>`
- `next_event(&mut self)` → `Option<InProcessServerEvent>`
- `respond_to_server_request` / `fail_server_request` for pending `ServerRequest` events
- `shutdown(self)` → bounded graceful teardown (5s timeouts, abort fallback)
</Step>
</Steps>
### Initialize handshake vs socket clients
On socket transports, `initialize` may return before outbound notifications are enabled; the client must send `initialized`, and `lib.rs` then calls `send_initialize_notifications_to_connection` and sets `outbound_initialized`.
In-process passes `Some(outbound_initialized)` into `handle_client_request`, so a successful `initialize` RPC immediately marks the connection outbound-ready. `start()` still emits `ClientNotification::Initialized` for parity; `process_client_notification` currently logs typed notifications only—the session is already live after the initialize response.
Config warnings are delivered via `send_initialize_notifications()` when the processor observes the initialized transition (`!was_initialized && is_initialized` in the runtime loop).
### Channel capacity
```text
DEFAULT_IN_PROCESS_CHANNEL_CAPACITY = CHANNEL_CAPACITY // 128 (app-server-transport)
```
`channel_capacity` in `InProcessStartArgs` is clamped with `.max(1)`. The same capacity sizes: client command queue, event queue, processor command queue, outgoing envelope queue, and per-connection writer queue.
## `InProcessClientHandle` API
| Method | Direction | Behavior |
| --- | --- | --- |
| `request` | Client → server | Async; unique `RequestId` required among concurrent calls |
| `notify` | Client → server | Fire-and-forget; `try_send` on client queue |
| `next_event` | Server → client | Async recv of `InProcessServerEvent` |
| `respond_to_server_request` | Client → server | Completes a prior `ServerRequest` event |
| `fail_server_request` | Client → server | Rejects with JSON-RPC error payload |
| `shutdown` | Control | Drains runtime; cancels in-flight server requests with internal error |
| `sender` | — | Cloneable `InProcessClientSender` for concurrent callers |
### `InProcessServerEvent` variants
<ParamField body="ServerRequest" type="ServerRequest">
Server-initiated JSON-RPC request (approvals, MCP elicitation, etc.). Must be answered with `respond_to_server_request` or `fail_server_request`; unanswered requests can stall turns.
</ParamField>
<ParamField body="ServerNotification" type="ServerNotification">
App-server notification (`turn/completed`, deltas, thread events, etc.).
</ParamField>
<ParamField body="Lagged" type="{ skipped: number }">
Transport health marker: the consumer fell behind and best-effort events were dropped. Not an application-level Codex event. Emitted by `codex-app-server-client` when forwarding saturates; the low-level runtime logs drops but does not emit `Lagged` itself.
</ParamField>
Duplicate in-flight request IDs produce `INVALID_REQUEST` (`-32600`) on the duplicate caller’s oneshot, without enqueueing a second processor command.
## Backpressure and overload
Bounded queues use `try_send` at hot paths so memory cannot grow without bound.
### Client → runtime (`InProcessClientSender`)
| Queue full | Effect |
| --- | --- |
| `request` / `notify` / server-reply messages | `IoError` with `ErrorKind::WouldBlock` and message `"in-process app-server client queue is full"` |
| Closed runtime | `BrokenPipe` |
Embedders should retry or slow producers on `WouldBlock`, especially with `channel_capacity: 0` clamped to `1` (see unit test `in_process_start_clamps_zero_channel_capacity`).
### Runtime → `MessageProcessor`
| Queue full | Effect |
| --- | --- |
| Incoming `ClientRequest` | JSON-RPC error code **`-32001`** (`OVERLOADED_ERROR_CODE`), message `"in-process app-server request queue is full"` |
| Incoming `ClientNotification` | Logged warning; notification **dropped** |
### Runtime → event consumer (`InProcessServerEvent`)
| Outbound type | Queue full behavior |
| --- | --- |
| `ServerRequest` | Fail back into `MessageProcessor` with **`-32001`** (`"in-process server request queue is full"`) so approvals do not hang |
| `TurnCompleted`, `ThreadSettingsUpdated` | Blocking `.send().await` (guaranteed delivery tier in `in_process`) |
| Other notifications | `try_send`; on full, warning + **drop** |
<Warning>
Server requests are never silently abandoned at the in-process layer. Saturated event queues surface overload errors back to the processor instead of leaving approval flows pending forever.
</Warning>
Socket transports use the same **`-32001`** / `"Server overloaded; retry later."` pattern at ingress (`app-server-transport`); in-process overload strings are more specific but share the code.
### JSON-RPC error codes (in-process path)
| Code | Constant / name | Typical cause |
| --- | --- | --- |
| `-32600` | invalid request | Duplicate request id, not initialized (via `MessageProcessor`) |
| `-32603` | internal error | Processor closed, shutdown in progress |
| `-32001` | overloaded | Processor or server-request event queue full |
Public re-exports from `codex-app-server` include `INVALID_PARAMS_ERROR_CODE` and `INPUT_TOO_LARGE_ERROR_CODE` for shared client handling; overload for embedding is primarily `-32001` on the request path.
## Relationship to `codex-app-server-client`
`codex-app-server-client` (workspace crate `codex-app-server-client`, consumed by `codex-tui` and `codex-exec`) wraps `codex_app_server::in_process` and is the recommended integration surface for product code.
```text
Caller (TUI / exec)
│
▼
InProcessAppServerClient ── mpsc ClientCommand / events
│ worker task (select: commands + handle.next_event)
▼
InProcessClientHandle ── in_process runtime (this page)
│
▼
MessageProcessor
```
### What the facade adds
- **`InProcessClientStartArgs`**: builds `InitializeParams` from `client_name`, `client_version`, `experimental_api`, `opt_out_notification_methods`, plus maps into `InProcessStartArgs` (including thread config loader discovery).
- **Worker task**: concurrent request handling (spawned per request so events keep draining during blocking server-request waits).
- **`forward_in_process_event`**: second bounded queue with **lossless** vs **best-effort** notification tiers—stricter than `in_process` alone (`TurnCompleted`, `ThreadSettingsUpdated`, transcript deltas, `ItemCompleted`, etc. block; overload rejects pending `ServerRequest` with `-32001`).
- **`InProcessServerEvent::Lagged`**: aggregates skipped best-effort events for slow consumers.
- **Typed helpers**: `request_typed`, `AppServerEvent`, shared shutdown timeout (5s).
- **Policy**: auto-rejects unsupported `chatgptAuthTokensRefresh` server requests for in-process clients.
Re-exported symbols: `DEFAULT_IN_PROCESS_CHANNEL_CAPACITY`, `InProcessServerEvent`, `StateDbHandle`, `LogDbLayer`.
<Info>
Pass both `SessionSource` and initialize `client_info.name` explicitly at startup so thread metadata (`thread/list`, `thread/read` source kinds) matches the originating runtime without hard-coding TUI/exec policy in the shared client crate.
</Info>
## Testing and verification
Integration tests under `tests/suite/` construct `InProcessStartArgs` and call `in_process::start` directly (for example `thread_read.rs`, `thread_unarchive.rs`, `remote_thread_store.rs`). Typical pattern:
- Ephemeral temp `codex_home`, `ConfigBuilder`, optional `state_db`
- `InitializeParams` with a test `client_info.name`
- `session_source` aligned with the scenario (`SessionSource::Cli`, `Exec`, etc.)
- `channel_capacity: DEFAULT_IN_PROCESS_CHANNEL_CAPACITY`
- `shutdown().await` after assertions
Unit tests in `src/in_process.rs` cover initialize + typed v2 RPC, session source propagation on `thread/start`, and zero capacity clamping.
Run focused crate tests:
```bash
cd codex-rs && just test -p codex-app-server
```
## Choosing an integration layer
| Goal | Use |
| --- | --- |
| Ship a CLI or UI in the Codex repo | `codex-app-server-client::InProcessAppServerClient::start` |
| Custom embedder, minimal dependencies | `codex_app_server::in_process::start` + `InProcessClientHandle` |
| External editor or daemon | Separate `codex-app-server` process over stdio/unix/websocket |
| Protocol compliance tests without subprocess | `in_process::start` in `tests/suite` |
## Related pages
<CardGroup>
<Card title="Connection lifecycle" href="/connection-lifecycle">
Per-connection `initialize` / `initialized`, notification opt-out, and server-initiated requests on socket transports—contrasted with in-process auto-handshake.
</Card>
<Card title="Protocol and transport" href="/protocol-and-transport">
JSON-RPC rules, bounded queues, and overload `-32001` on stdio and websocket paths.
</Card>
<Card title="Build a JSON-RPC client" href="/build-jsonrpc-client">
Framing, request ids, and `initialized` ordering for out-of-process clients.
</Card>
<Card title="Approvals and server requests" href="/approvals-and-server-requests">
Handling `ServerRequest` events from `next_event` and why overload rejection matters.
</Card>
<Card title="Stream turns and events" href="/stream-turns-and-events">
Consuming `ServerNotification` streams and lossless transcript tiers in the client facade.
</Card>
<Card title="Development and testing" href="/development-and-testing">
`test_app_server`, suite layout, and `just test -p codex-app-server`.
</Card>
</CardGroup>
---
## 15. RPC methods reference
> Grouped catalog of v2 `<resource>/<method>` RPCs for threads, turns, filesystem, models, processes, plugins, remote control, and utility commands with stable vs experimental markers.
- Page Markdown: https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/pages/15-rpc-methods-reference.md
- Generated: 2026-06-02T06:40:07.167Z
### Source Files
- `README.md`
- `src/request_processors.rs`
- `src/message_processor.rs`
- `src/request_processors/thread_processor.rs`
- `src/request_processors/fs_processor.rs`
- `src/request_processors/command_exec_processor.rs`
- `src/request_processors/process_exec_processor.rs`
---
title: "RPC methods reference"
description: "Grouped catalog of v2 `<resource>/<method>` RPCs for threads, turns, filesystem, models, processes, plugins, remote control, and utility commands with stable vs experimental markers."
---
The `codex app-server` binary exposes a JSON-RPC 2.0 API on each transport connection. Active client methods use singular `<resource>/<method>` wire names (for example `thread/start`, `turn/interrupt`, `config/batchWrite`). The authoritative registry lives in `app-server-protocol` (`client_request_definitions!` in `common.rs`); `MessageProcessor` dispatches each variant to a request processor module under `src/request_processors/`.
<Note>
Send `initialize` once per connection, then the `initialized` notification, before any other RPC. Methods documented here assume a initialized connection unless noted.
</Note>
## Wire conventions
| Convention | Rule |
| --- | --- |
| Method name | Singular resource + `/` + action: `thread/read`, `mcpServer/tool/call` |
| Request shape | `{ "method": "<wire>", "id": <id>, "params": { ... } }` |
| Params / results | camelCase on the wire (`threadId`, `nextCursor`) |
| Config RPCs | snake_case fields mirroring `config.toml` keys |
| Pagination | Cursor methods use `cursor`, `limit`, `data`, `nextCursor`, and often `backwardsCursor` |
| Thread-scoped calls | Most turn/thread mutations require `threadId` and serialize per-thread |
Regenerate client types from the running binary when shapes change:
```bash
codex app-server generate-ts --out DIR
codex app-server generate-json-schema --out DIR
codex app-server generate-ts --out DIR --experimental
```
## Experimental gating
| Level | Marker | Runtime requirement |
| --- | --- | --- |
| Stable method | No `#[experimental(...)]` on the method variant | Default after `initialize` |
| Experimental method | `#[experimental("resource/method")]` on the variant | `initialize.params.capabilities.experimentalApi: true` |
| Experimental field | `#[experimental("resource/method.field")]` on a param field | Same opt-in; method may still be stable with `inspect_params: true` |
| Rejection | — | JSON-RPC error: `<descriptor> requires experimentalApi capability` |
<Info>
Stable methods with experimental fields: `thread/start`, `thread/resume`, `thread/fork`, `thread/settings/update`, `turn/start`, `turn/steer`, `account/login/start`, and `command/exec`. Omit experimental fields unless opted in.
</Info>
## Connection and lifecycle
| Method | Stability | Summary |
| --- | --- | --- |
| `initialize` | Stable | Handshake: `clientInfo`, optional `capabilities.experimentalApi`, `optOutNotificationMethods` |
| `initialized` | Stable (notification) | Client ack after `initialize` result |
## Thread and turn RPCs
### Thread lifecycle
| Method | Stability | Key params | Response / side effects |
| --- | --- | --- | --- |
| `thread/start` | Stable (partial experimental fields) | `cwd`, `model`, `sandbox` or `permissions`, `ephemeral`, `sessionStartSource` | `thread`; notifies `thread/started`; auto-subscribes connection |
| `thread/resume` | Stable (partial experimental fields) | `threadId` or `path`, overrides, `excludeTurns`, `initialTurnsPage` | `thread`; may emit `thread/tokenUsage/updated` |
| `thread/fork` | Stable (partial experimental fields) | `threadId` or `path`, `ephemeral`, `excludeTurns` | New `thread`; `thread/started` |
| `thread/read` | Stable | `threadId`, `includeTurns` | `thread` without resuming |
| `thread/list` | Stable | `cursor`, `limit`, `cwd`, `archived`, `searchTerm`, filters | Paginated `data` |
| `thread/search` | **Experimental** | Search-oriented list params | Paginated `data` |
| `thread/loaded/list` | Stable | — | In-memory loaded thread ids |
| `thread/archive` | Stable | `threadId` | `{}`; `thread/archived` per archived thread |
| `thread/unarchive` | Stable | `threadId` | `thread`; `thread/unarchived` |
| `thread/unsubscribe` | Stable | `threadId` | `status`: `unsubscribed` / `notSubscribed` / `notLoaded` |
| `thread/name/set` | Stable | `threadId`, name | `{}`; `thread/name/updated` |
| `thread/metadata/update` | Stable | `threadId`, `gitInfo` patch | `thread` |
| `thread/settings/update` | **Experimental** (partial fields) | Partial next-turn settings | `{}`; may emit `thread/settings/updated` |
| `thread/memoryMode/set` | **Experimental** | `threadId`, mode | `{}` |
| `thread/goal/set` | Stable | `threadId`, goal | Goal; `thread/goal/updated` |
| `thread/goal/get` | Stable | `threadId` | Goal or `null` |
| `thread/goal/clear` | Stable | `threadId` | Cleared flag; `thread/goal/cleared` |
| `thread/compact/start` | Stable | `threadId` | `{}`; progress via turn/item notifications |
| `thread/shellCommand` | Stable | `threadId`, command | `{}`; unsandboxed shell via turn stream |
| `thread/rollback` | Stable | `threadId`, turn count | Updated `thread` with pruned history |
| `thread/approveGuardianDeniedAction` | Stable | `threadId`, action id | Approval result |
| `thread/backgroundTerminals/clean` | **Experimental** | `threadId` | `{}` |
| `thread/increment_elicitation` | **Experimental** | `threadId` | `{}` |
| `thread/decrement_elicitation` | **Experimental** | `threadId` | `{}` |
| `thread/inject_items` | Stable | `threadId`, raw items | `{}` |
| `thread/turns/list` | **Experimental** | `threadId`, `cursor`, `itemsView` | Paginated turns |
| `thread/turns/items/list` | **Experimental** (unsupported) | Turn item paging shape | Error: not supported yet |
### Realtime (experimental)
| Method | Summary |
| --- | --- |
| `thread/realtime/start` | Start realtime session (`outputModality`, optional WebRTC `transport`) |
| `thread/realtime/appendAudio` | Append input audio |
| `thread/realtime/appendText` | Append text input |
| `thread/realtime/stop` | Stop session |
| `thread/realtime/listVoices` | List voices (global) |
### Turns
| Method | Stability | Summary |
| --- | --- | --- |
| `turn/start` | Stable (partial experimental fields) | Begin generation; returns initial `turn`; streams `turn/started`, `item/*`, `turn/completed` |
| `turn/steer` | Stable (partial experimental fields) | Add input to in-flight regular turn |
| `turn/interrupt` | Stable | Cancel `(threadId, turnId)`; turn ends `interrupted` |
| `review/start` | Stable | Automated code review turn on a thread |
```mermaid
sequenceDiagram
participant Client
participant AppServer as MessageProcessor
participant Thread as ThreadRequestProcessor
Client->>AppServer: thread/start
AppServer->>Thread: start
Thread-->>Client: result.thread
AppServer-->>Client: notification thread/started
Client->>AppServer: turn/start
AppServer-->>Client: result.turn
AppServer-->>Client: turn/started
AppServer-->>Client: item/started / deltas
AppServer-->>Client: turn/completed
```
## Filesystem (`fs/*`)
All `fs/*` methods are **stable** and intentionally **concurrent** (no global serialization). Paths must be absolute.
| Method | Summary |
| --- | --- |
| `fs/readFile` | Read bytes as `dataBase64` |
| `fs/writeFile` | Write from `dataBase64` |
| `fs/createDirectory` | Create directory (`recursive` default true) |
| `fs/getMetadata` | `isDirectory`, `isFile`, `isSymlink`, timestamps |
| `fs/readDirectory` | List child names and types |
| `fs/remove` | Remove file or tree |
| `fs/copy` | Copy paths (`recursive` for directories) |
| `fs/watch` | Subscribe by `watchId`; emits `fs/changed` |
| `fs/unwatch` | Stop watch for `watchId` |
## Models, features, and permissions
| Method | Stability | Summary |
| --- | --- | --- |
| `model/list` | Stable | Models, reasoning options, tiers, availability metadata |
| `modelProvider/capabilities/read` | Stable | Provider capabilities for current config |
| `experimentalFeature/list` | Stable | Feature flags with stage metadata; optional `threadId` |
| `experimentalFeature/enablement/set` | Stable | Patch in-memory feature enablement |
| `permissionProfile/list` | Stable (beta catalog) | Permission profile ids; pass `cwd` for project-local entries |
| `collaborationMode/list` | **Experimental** | Collaboration mode presets |
## Command and process execution
### Sandboxed `command/*` (stable)
| Method | Summary |
| --- | --- |
| `command/exec` | Run argv under server sandbox; optional streaming `command/exec/outputDelta` |
| `command/exec/write` | Stdin (base64) or close stdin for `processId` |
| `command/exec/resize` | Resize PTY session |
| `command/exec/terminate` | Terminate session |
`command/exec` is stable but supports an experimental `permissionProfile` param when opted in.
### Host `process/*` (experimental)
| Method | Summary |
| --- | --- |
| `process/spawn` | Spawn without Codex sandbox on app-server host |
| `process/writeStdin` | Stdin for `processHandle` |
| `process/resizePty` | Resize PTY |
| `process/kill` | Terminate process |
Notifications: `process/outputDelta`, `process/exited`.
```text
command/exec --> CommandExecRequestProcessor (sandboxed)
process/spawn --> ProcessExecRequestProcessor (host, experimental)
```
## Config, MCP, and environment
| Method | Stability | Summary |
| --- | --- | --- |
| `config/read` | Stable | Effective resolved config |
| `config/value/write` | Stable | Single key to user `config.toml` |
| `config/batchWrite` | Stable | Atomic multi-edit; optional `reloadUserConfig` |
| `configRequirements/read` | Stable | `requirements.toml` / MDM constraints |
| `config/mcpServer/reload` | Stable | Reload MCP config; refresh loaded threads |
| `externalAgentConfig/detect` | Stable | Detect migratable external-agent artifacts |
| `externalAgentConfig/import` | Stable | Apply selected migrations |
| `mcpServerStatus/list` | Stable | MCP servers, tools, auth; optional `threadId`, pagination |
| `mcpServer/oauth/login` | Stable | Start OAuth; later `mcpServer/oauthLogin/completed` |
| `mcpServer/resource/read` | Stable | Read MCP resource by `server`, `uri` |
| `mcpServer/tool/call` | Stable | Call tool on thread's MCP server |
| `environment/add` | **Experimental** | Register remote environment by id + `execServerUrl` |
## Account and auth
| Method | Stability | Summary |
| --- | --- | --- |
| `account/read` | Stable | Current account snapshot |
| `account/login/start` | Stable (partial experimental fields) | `apiKey`, `chatgpt`, `chatgptDeviceCode` |
| `account/login/cancel` | Stable | Cancel pending ChatGPT login |
| `account/logout` | Stable | Clear session |
| `account/rateLimits/read` | Stable | Rate limit snapshot |
| `account/sendAddCreditsNudgeEmail` | Stable | Workspace owner nudge |
Notification: `account/login/completed`, `account/updated`.
## Skills, hooks, plugins, and apps
| Method | Stability | Notes |
| --- | --- | --- |
| `skills/list` | Stable | Skills for `cwd` values |
| `skills/extraRoots/set` | Stable | Runtime extra skill roots (not persisted) |
| `skills/config/write` | Stable | User skill config |
| `hooks/list` | Stable | Discovered hooks per `cwd` |
| `marketplace/add` | Stable | Add plugin marketplace |
| `marketplace/remove` | Stable | Remove marketplace |
| `marketplace/upgrade` | Stable | Upgrade Git marketplaces |
| `plugin/list` | Stable | Marketplaces + plugin catalog |
| `plugin/installed` | Stable | Installed rows + suggestions |
| `plugin/read` | Stable | Plugin detail by marketplace path |
| `plugin/skill/read` | Stable | Remote skill markdown preview |
| `plugin/install` | Stable | Install from marketplace |
| `plugin/uninstall` | Stable | Local or remote uninstall |
| `plugin/share/save` | Stable | Save share |
| `plugin/share/updateTargets` | Stable | Update share targets |
| `plugin/share/list` | Stable | List shares |
| `plugin/share/checkout` | Stable | Checkout share |
| `plugin/share/delete` | Stable | Delete share |
| `app/list` | Stable | Available apps |
Notification: `skills/changed`, `app/listUpdated`.
## Remote control (experimental)
| Method | Summary |
| --- | --- |
| `remoteControl/enable` | Enable remote control; return status snapshot |
| `remoteControl/disable` | Disable (does not revoke enrolled devices) |
| `remoteControl/status/read` | Read `disabled` / `connecting` / `connected` / `errored` |
| `remoteControl/pairing/start` | Pairing codes + `expiresAt` |
Notification: `remoteControl/status/changed`.
## Memory, search, sandbox, and utilities
| Method | Stability | Summary |
| --- | --- | --- |
| `memory/reset` | **Experimental** | Clear `CODEX_HOME/memories` and memory stage data |
| `fuzzyFileSearch` | Deprecated v1 wire | Legacy one-shot fuzzy search |
| `fuzzyFileSearch/sessionStart` | **Experimental** | Session-based fuzzy search |
| `fuzzyFileSearch/sessionUpdate` | **Experimental** | Update query/roots |
| `fuzzyFileSearch/sessionStop` | **Experimental** | End session |
| `windowsSandbox/setupStart` | Stable | Start Windows sandbox setup |
| `windowsSandbox/readiness` | Stable | Readiness probe |
| `feedback/upload` | Stable | Submit feedback report |
| `mock/experimentalMethod` | **Experimental** | Test-only gating validation |
## Stable methods with common experimental fields
Use `capabilities.experimentalApi: true` before sending these fields.
| Method | Experimental field examples (descriptor prefix) |
| --- | --- |
| `thread/start` | `runtimeWorkspaceRoots`, `permissions`, `environments`, `dynamicTools`, `experimentalRawEvents` |
| `thread/resume` | `path`, `history`, `excludeTurns`, `initialTurnsPage`, `runtimeWorkspaceRoots`, `permissions` |
| `thread/fork` | `path`, `excludeTurns`, `runtimeWorkspaceRoots`, `permissions` |
| `thread/settings/update` | `permissions`, `collaborationMode` |
| `turn/start` | `runtimeWorkspaceRoots`, `permissions`, `environments`, `additionalContext`, `collaborationMode` |
| `turn/steer` | `additionalContext`, `responsesapiClientMetadata` |
| `account/login/start` | `chatgptAuthTokens` |
| `command/exec` | `permissionProfile` |
Shared types may gate enum variants (for example `approvalPolicy` with `granular` → `askForApproval.granular`).
## Deprecated client methods (v1)
Do not build new integrations on these wire names. Prefer the v2 column.
| Legacy / v1 method | Replacement |
| --- | --- |
| `GetConversationSummary` | `thread/read` with `includeTurns` |
| `GitDiffToRemote` | Host git tooling outside app-server |
| `GetAuthStatus` | `account/read` |
| `FuzzyFileSearch` | `fuzzyFileSearch/session*` (experimental) or client-side search |
## Server-initiated requests (not client RPCs)
The server calls **into** the client during turns. Handle these on the same JSON-RPC connection:
| Wire method | Purpose |
| --- | --- |
| `item/commandExecution/requestApproval` | Approve sandboxed command |
| `item/fileChange/requestApproval` | Approve patch / edit |
| `item/permissions/requestApproval` | Additional permissions |
| `item/tool/requestUserInput` | Tool questions (experimental) |
| `item/tool/call` | Dynamic tool execution on client |
| `mcpServer/elicitation/request` | MCP elicitation |
| `account/chatgptAuthTokens/refresh` | Refresh ChatGPT tokens |
| `attestation/generate` | Upstream attestation when `request_attestation` enabled |
Resolve with the matching client response; the server emits `serverRequest/resolved` when done. See [Approvals and server requests](/approvals-and-server-requests).
## Concurrency and overload
| Behavior | Detail |
| --- | --- |
| Per-thread serialization | Most `thread/*` and `turn/*` mutations for the same `threadId` are serialized |
| Concurrent resources | `fs/*`, `fuzzyFileSearch*`, `model/list`, `thread/turns/list`, and similar read-heavy calls |
| Global locks | `config/*`, `account/*` auth, `remoteControl/*`, `environment/add` use named global serialization keys |
| Overload | Saturated ingress returns JSON-RPC `-32001` — retry with backoff |
## Related pages
<CardGroup>
<Card title="Experimental API" href="/experimental-api">
Opt-in, schema generation, and maintainer gating patterns.
</Card>
<Card title="Threads, turns, and items" href="/threads-turns-items">
Core primitives and subscription model.
</Card>
<Card title="Config RPC reference" href="/config-rpc">
Config read/write batch details and snake_case fields.
</Card>
<Card title="Schema generation" href="/schema-generation">
`generate-ts` / `generate-json-schema` and `--experimental`.
</Card>
<Card title="Notifications and events" href="/notifications-and-events">
Server notification catalog and item deltas.
</Card>
</CardGroup>
---
## 16. Notifications and events
> Server notification method names, `ThreadItem` union variants, per-item delta events, turn-level events, realtime thread notifications, and `CodexErrorInfo` values on failures.
- Page Markdown: https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/pages/16-notifications-and-events.md
- Generated: 2026-06-02T06:40:36.651Z
### Source Files
- `README.md`
- `src/bespoke_event_handling.rs`
- `src/filters.rs`
- `src/thread_status.rs`
- `src/fs_watch.rs`
- `src/fuzzy_file_search.rs`
- `src/request_processors/feedback_processor.rs`
---
title: "Notifications and events"
description: "Server notification method names, `ThreadItem` union variants, per-item delta events, turn-level events, realtime thread notifications, and `CodexErrorInfo` values on failures."
---
`codex app-server` streams work to clients as JSON-RPC 2.0 notifications: each message has a `method` string and a `params` object. Turn and item progress for a subscribed thread is delivered through `ThreadScopedOutgoingMessageSender`; connection-wide notifications (config warnings, filesystem watches, account updates) go to every initialized connection. The authoritative method list is generated from `ServerNotification` in `app-server-protocol`; regenerate TypeScript or JSON Schema with `codex app-server generate-ts` / `generate-json-schema` when you pin to a binary version.
## Wire shape
Notifications use the same JSON-RPC envelope as requests, without an `id`:
```json
{ "method": "item/agentMessage/delta", "params": { "threadId": "…", "turnId": "…", "itemId": "…", "delta": "…" } }
```
`ServerNotification` is defined with `#[serde(tag = "method", content = "params")]`, so the Rust variant name maps to the wire `method` (for example `TurnCompleted` → `turn/completed`). Payload fields use `camelCase`.
## Delivery and subscription
| Scope | How it reaches the client |
| --- | --- |
| Thread turn/item stream | `thread/start`, `thread/resume`, and `thread/fork` auto-subscribe the calling connection. Further subscribers are tracked in `ThreadState`; `ThreadScopedOutgoingMessageSender` fans out only to `connection_ids` for that thread. |
| Connection-wide | `OutgoingMessageSender::send_server_notification` broadcasts to all initialized connections (for example `configWarning`, `fs/changed`). |
| Per-connection watch | `fs/watch` registers a `watchId` on one connection; `fs/changed` includes that `watchId`. |
`thread/unsubscribe` removes a connection from a thread’s subscriber set. When the last subscriber leaves and the thread is idle for 30 minutes, the server unloads it and emits `thread/closed`.
<Note>
`turn/started` and `turn/completed` currently carry a `turn` object whose `items` array is empty even when `item/*` notifications were streamed. Build UI state from `item/started`, deltas, and `item/completed` until that behavior changes.
</Note>
```mermaid
sequenceDiagram
participant Client
participant AppServer as app-server
participant Core as codex-core
Client->>AppServer: turn/start
AppServer->>Client: turn/started (turn.status inProgress)
Core-->>AppServer: EventMsg stream
AppServer->>Client: item/started
AppServer->>Client: item/agentMessage/delta
AppServer->>Client: item/completed
AppServer->>Client: turn/completed (turn.status terminal)
```
Core `EventMsg` values are projected to v2 notifications in `item_event_to_server_notification` and `apply_bespoke_event_handling` (turn completion, errors, realtime, hooks, plan/diff updates).
## Notification opt-out and experimental gating
Per connection, pass exact method names in `initialize.params.capabilities.optOutNotificationMethods`:
- Matching is exact (no wildcards or prefixes).
- Unknown names are accepted and ignored.
- Applies to typed server notifications (`thread/*`, `turn/*`, `item/*`, `rawResponseItem/*`, etc.), not RPC requests, responses, or JSON-RPC errors.
- Filtering happens in `should_skip_notification_for_connection` before write.
Experimental notifications are dropped entirely unless `capabilities.experimentalApi` is true at initialize time (same transport path as opt-out).
Examples:
```json
"optOutNotificationMethods": ["thread/started", "item/agentMessage/delta"]
```
```json
"optOutNotificationMethods": ["thread/realtime/outputAudio/delta"]
```
## Turn-level notifications
| Method | Params (summary) |
| --- | --- |
| `turn/started` | `{ threadId, turn }` — `turn.status` is `inProgress`, `items` empty |
| `turn/completed` | `{ threadId, turn }` — terminal `turn.status`: `completed`, `interrupted`, or `failed` |
| `turn/diff/updated` | `{ threadId, turnId, diff }` — aggregated unified diff after file changes |
| `turn/plan/updated` | `{ threadId, turnId, explanation?, plan }` — `plan[]` entries `{ step, status }` with `status` in `pending`, `inProgress`, `completed` |
| `model/rerouted` | `{ threadId, turnId, fromModel, toModel, reason }` |
| `model/verification` | `{ threadId, turnId, verifications }` |
`TurnStatus` values: `inProgress`, `completed`, `interrupted`, `failed`.
Token usage streams separately on `thread/tokenUsage/updated` (not embedded in `turn/completed`).
### Hooks
| Method | When |
| --- | --- |
| `hook/started` | A configured hook run begins |
| `hook/completed` | A hook run finishes |
Hook transcript content appears as `hookPrompt` `ThreadItem`s, not as separate hook payload streams.
## Item lifecycle and `ThreadItem`
Every unit of turn work follows: **`item/started` → item-specific deltas (optional) → `item/completed`**.
| Method | Role |
| --- | --- |
| `item/started` | Full `item` snapshot when work begins; `item.id` matches delta `itemId`s |
| `item/completed` | Authoritative final `item` state |
| `item/autoApprovalReview/started` | **[UNSTABLE]** Guardian auto-review began |
| `item/autoApprovalReview/completed` | **[UNSTABLE]** Guardian auto-review finished |
`ThreadItem` is a tagged union (`type` field, `camelCase` variants). Variants defined in the v2 protocol:
| `type` | Purpose |
| --- | --- |
| `userMessage` | User input (`content`: `text` / `image` / `localImage`); optional `clientId` from `turn/start` / `turn/steer` |
| `hookPrompt` | Hook-injected prompt fragments |
| `agentMessage` | Assistant reply text; optional `phase`, `memoryCitation` |
| `plan` | Plan-mode content (experimental streaming via `item/plan/delta`) |
| `reasoning` | `summary` and `content` string arrays |
| `commandExecution` | Sandboxed command: `command`, `cwd`, `status`, `commandActions`, `aggregatedOutput`, `exitCode`, `durationMs` |
| `fileChange` | Patch proposal: `changes[]` with `path`, `kind`, `diff`; `status` |
| `mcpToolCall` | MCP invocation with `server`, `tool`, `arguments`, `result` / `error` |
| `dynamicToolCall` | Client-executed dynamic tool |
| `collabAgentToolCall` | Collab tools (`spawn_agent`, etc.) with `senderThreadId`, `receiverThreadIds` |
| `webSearch` | Search tool with optional `action` |
| `imageView` | Image viewer tool |
| `imageGeneration` | Image generation tool result |
| `enteredReviewMode` / `exitedReviewMode` | Automated review boundaries |
| `contextCompaction` | History compaction marker |
Statuses for executable items typically include `inProgress`, `completed`, `failed`, and `declined` where applicable.
<Warning>
`thread/compacted` (`ContextCompacted` notification) is deprecated; prefer the `contextCompaction` item type.
</Warning>
## Per-item delta notifications
| Method | Item kind | Concatenate by |
| --- | --- | --- |
| `item/agentMessage/delta` | `agentMessage` | `itemId` → full reply text |
| `item/plan/delta` | `plan` (experimental) | `itemId` |
| `item/reasoning/summaryTextDelta` | `reasoning` | `itemId` + `summaryIndex` |
| `item/reasoning/summaryPartAdded` | `reasoning` | Marks new summary section |
| `item/reasoning/textDelta` | `reasoning` | `itemId` + `contentIndex` |
| `item/commandExecution/outputDelta` | `commandExecution` | `itemId` (stdout/stderr) |
| `item/commandExecution/terminalInteraction` | `commandExecution` | PTY interaction metadata |
| `item/fileChange/patchUpdated` | `fileChange` | Structured patch snapshots when streaming is enabled |
| `item/fileChange/outputDelta` | `fileChange` | Deprecated; server no longer emits |
| `item/mcpToolCall/progress` | `mcpToolCall` | Progress events |
`rawResponseItem/completed` is internal-oriented (Codex Cloud); most clients should ignore it.
## Thread lifecycle notifications
| Method | Params (summary) |
| --- | --- |
| `thread/started` | `{ thread }` after `thread/start`, `thread/resume`, or `thread/fork` |
| `thread/status/changed` | `{ threadId, status }` — `ThreadStatus`: `notLoaded`, `idle`, `systemError`, or `active` with `activeFlags` (`waitingOnApproval`, `waitingOnUserInput`) |
| `thread/archived` | Per archived thread |
| `thread/unarchived` | Restored thread |
| `thread/closed` | After idle unload |
| `thread/name/updated` | Name change |
| `thread/goal/updated` / `thread/goal/cleared` | Persisted goal changes |
| `thread/settings/updated` | Experimental effective next-turn settings |
| `thread/tokenUsage/updated` | Restored or live token usage |
`thread/status/changed` is driven by `ThreadWatchManager` when loaded thread activity or approval/input waits change.
## Thread realtime (experimental)
Realtime notifications are **not** `ThreadItem`s and are not returned by `thread/read`, `thread/resume`, or `thread/fork`.
| Method | Params (summary) |
| --- | --- |
| `thread/realtime/started` | `{ threadId, realtimeSessionId?, version }` |
| `thread/realtime/itemAdded` | `{ threadId, item }` — raw JSON (unstable upstream schema) |
| `thread/realtime/transcript/delta` | `{ threadId, role, delta }` |
| `thread/realtime/transcript/done` | `{ threadId, role, text }` |
| `thread/realtime/outputAudio/delta` | `{ threadId, audio }` — `audio`: `{ data, sampleRate, numChannels, samplesPerChannel }` |
| `thread/realtime/sdp` | `{ threadId, sdp }` — WebRTC answer |
| `thread/realtime/error` | `{ threadId, message }` |
| `thread/realtime/closed` | `{ threadId, reason? }` |
Requires `capabilities.experimentalApi` at initialize (notifications are dropped otherwise).
## Other server notifications
| Category | Methods |
| --- | --- |
| Errors and warnings | `error`, `warning`, `configWarning`, `guardianWarning`, `deprecationNotice` |
| Account / auth | `account/updated`, `account/rateLimits/updated`, `account/login/completed` |
| MCP | `mcpServer/startupStatus/updated`, `mcpServer/oauthLogin/completed` |
| Skills / apps | `skills/changed`, `app/list/updated` |
| Filesystem | `fs/changed` (`watchId`, `changedPaths`; 200ms debounce per watch) |
| Fuzzy search (experimental) | `fuzzyFileSearch/sessionUpdated`, `fuzzyFileSearch/sessionCompleted` |
| Standalone processes | `command/exec/outputDelta`, `process/outputDelta`, `process/exited` (experimental) |
| Server requests | `serverRequest/resolved` after client answers an approval/elicitation |
| Windows | `windows/worldWritableWarning`, `windowsSandbox/setupCompleted` |
| Remote / import | `remoteControl/status/changed`, `externalAgentConfig/import/completed` |
| Internal | `rawResponseItem/completed` |
`configWarning` may appear during initialization (`{ summary, details?, path?, range? }`). `warning` carries `{ threadId?, message }` for non-fatal core warnings.
## Failures, `error`, and `CodexErrorInfo`
Two surfaces carry the same error payload shape (`TurnError`):
| Surface | When |
| --- | --- |
| `error` notification | Mid-turn failure or retryable stream error |
| `turn/completed` with `turn.status: "failed"` | Terminal turn failure |
<ResponseField name="error" type="object">
<ResponseField name="message" type="string">Human-readable error text</ResponseField>
<ResponseField name="codexErrorInfo" type="CodexErrorInfo | null">Structured failure class (see table)</ResponseField>
<ResponseField name="additionalDetails" type="string | null">Extra context (for example stream errors)</ResponseField>
</ResponseField>
`error` also includes `threadId`, `turnId`, and `willRetry`:
- `willRetry: true` — transient `StreamError`; turn may continue (automatic retry).
- `willRetry: false` — terminal for that error path; expect `turn/completed` with `failed` when the turn ends.
### `CodexErrorInfo` variants
Wire format is camelCase. Unit variants serialize as strings (for example `"cyberPolicy"`). Variants with data serialize as a single-key object (for example `{ "httpConnectionFailed": { "httpStatusCode": 503 } }`).
| Variant | Meaning |
| --- | --- |
| `contextWindowExceeded` | Context window exceeded |
| `usageLimitExceeded` | Usage / quota limit |
| `serverOverloaded` | Upstream overload |
| `cyberPolicy` | Cyber safety policy block |
| `httpConnectionFailed` | HTTP connection failure; optional `httpStatusCode` |
| `responseStreamConnectionFailed` | Could not open response SSE stream |
| `responseStreamDisconnected` | SSE stream dropped mid-turn |
| `responseTooManyFailedAttempts` | Retry limit exhausted |
| `activeTurnNotSteerable` | `turn/start` or `turn/steer` while turn is not steerable; `turnKind`: `review` or `compact` |
| `badRequest` | Invalid request |
| `unauthorized` | Auth failure |
| `sandboxError` | Sandbox execution failure |
| `threadRollbackFailed` | Rollback operation failed |
| `internalServerError` | Internal server error |
| `other` | Unclassified |
<RequestExample>
```json
{
"method": "turn/completed",
"params": {
"threadId": "thr_abc",
"turn": {
"id": "turn_1",
"items": [],
"itemsView": "notLoaded",
"status": "failed",
"error": {
"message": "Rate limit exceeded",
"codexErrorInfo": "usageLimitExceeded"
}
}
}
}
```
</RequestExample>
<RequestExample>
```json
{
"method": "error",
"params": {
"threadId": "thr_abc",
"turnId": "turn_1",
"willRetry": true,
"error": {
"message": "Stream disconnected",
"codexErrorInfo": {
"responseStreamDisconnected": { "httpStatusCode": 502 }
},
"additionalDetails": "retrying"
}
}
}
```
</RequestExample>
## Client checklist
<Steps>
<Step title="Subscribe to a thread">
Call `thread/start`, `thread/resume`, or `thread/fork` on the connection that should receive events.
</Step>
<Step title="Handle the turn envelope">
On `turn/started`, record `turn.id`. Stream items from `item/*`. Finish on `turn/completed` using `turn.status` and optional `turn.error`.
</Step>
<Step title="Reduce volume if needed">
Pass `optOutNotificationMethods` at `initialize` for high-frequency methods (for example `item/agentMessage/delta`, `item/commandExecution/outputDelta`).
</Step>
<Step title="Enable experimental streams">
Set `capabilities.experimentalApi: true` before relying on realtime, `thread/settings/updated`, or other gated notifications.
</Step>
</Steps>
## Related pages
<CardGroup>
<Card title="Stream turns and events" href="/stream-turns-and-events">
Consumer-oriented walkthrough of turn and item streaming, deltas, and opt-out patterns.
</Card>
<Card title="Threads, turns, and items" href="/threads-turns-items">
Core primitives, subscription model, and thread status semantics.
</Card>
<Card title="Connection lifecycle" href="/connection-lifecycle">
Initialize handshake, notification opt-out, and thread subscribe/unsubscribe.
</Card>
<Card title="Experimental API" href="/experimental-api">
Runtime opt-in required for gated notifications and RPC fields.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
Turn failures, `codexErrorInfo` interpretation, and overload retries.
</Card>
</CardGroup>
---
## 17. Config RPC reference
> `config/read`, `config/value/write`, `config/batchWrite`, `configRequirements/read`, `config/mcpServer/reload`, external-agent detect/import, and snake_case wire fields mirroring `config.toml`.
- Page Markdown: https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/pages/17-config-rpc-reference.md
- Generated: 2026-06-02T06:41:09.438Z
### Source Files
- `README.md`
- `src/request_processors/config_processor.rs`
- `src/request_processors/config_errors.rs`
- `src/config_manager.rs`
- `src/config/mod.rs`
- `src/request_processors/external_agent_config_processor.rs`
- `tests/suite/v2/config_rpc.rs`
---
title: "Config RPC reference"
description: "`config/read`, `config/value/write`, `config/batchWrite`, `configRequirements/read`, `config/mcpServer/reload`, external-agent detect/import, and snake_case wire fields mirroring `config.toml`."
---
App-server v2 exposes JSON-RPC methods under `config/*`, `configRequirements/read`, and `externalAgentConfig/*` so clients can read layered Codex settings, persist edits to `$CODEX_HOME/config.toml`, inspect enterprise constraints, reload MCP definitions without restarting the process, and migrate artifacts from other agent setups.
| Method | Purpose |
| --- | --- |
| `config/read` | Effective config after layer merge |
| `config/value/write` | Single-key write to user `config.toml` |
| `config/batchWrite` | Atomic multi-key write; optional thread hot-reload |
| `configRequirements/read` | Loaded `requirements.toml` / MDM constraints |
| `config/mcpServer/reload` | Re-read disk config and queue MCP refresh per loaded thread |
| `externalAgentConfig/detect` | Scan for migratable external-agent artifacts |
| `externalAgentConfig/import` | Apply selected migration items |
Implementation is split between `ConfigRequestProcessor` (RPC handlers), `ConfigManager` / `ConfigManagerService` (layer loading and disk writes), `McpRequestProcessor` + `mcp_refresh` (MCP reload), and `ExternalAgentConfigRequestProcessor` (detect/import).
## Wire naming
v2 config RPCs follow the usual split between **request envelopes** and **config payloads**:
| Surface | Casing | Examples |
| --- | --- | --- |
| Request/response wrappers | `camelCase` | `includeLayers`, `keyPath`, `mergeStrategy`, `reloadUserConfig`, `expectedVersion` |
| `config` object fields | `snake_case` (mirrors `config.toml`) | `sandbox_mode`, `model_reasoning_effort`, `forced_chatgpt_workspace_id` |
| `keyPath` segments | `config.toml` path syntax | `sandbox_mode`, `desktop.appearanceTheme`, `hooks.state` |
| Layer metadata | `camelCase` tagged union | `type: "user"`, `dotCodexFolder`, `enterpriseManaged` |
| Requirements (mostly) | `camelCase` | `allowedSandboxModes`, `allowManagedHooksOnly` |
| Managed hook event keys | PascalCase strings | `PreToolUse`, `SessionStart` |
| External-agent item types | `SCREAMING_SNAKE` strings | `CONFIG`, `MCP_SERVER_CONFIG`, `SESSIONS` |
<Note>
Pass `null` as a `value` to delete a key. Dotted `keyPath` values such as `desktop.someKey` use the same generic write surface as top-level keys.
</Note>
Unknown top-level config keys land in `config.additional` on read. The typed `Config` struct covers common fields; nested tables like `tools.web_search` deserialize into structured types when recognized.
## Config layering
Reads resolve a `ConfigLayerStack` (MDM → system → enterprise → user → project → session flags → legacy managed layers). Writes target only the **user** layer at `$CODEX_HOME/config.toml` (or an explicit `filePath` that resolves to the same canonical path).
```text
MDM / system / enterprise / project layers
│
▼ merge (higher precedence wins)
effective config ──► config/read.config
▲
│ write (user layer only)
$CODEX_HOME/config.toml
```
<ParamField body="cwd" type="string">
Optional on `config/read`. When set, project-local `.codex/` layers between `cwd` and the repo root are included in the effective view.
</ParamField>
<ParamField body="includeLayers" type="boolean" default="false">
When `true`, returns the full layer list (highest precedence first) with per-layer `version` hashes for optimistic concurrency.
</ParamField>
### `config/read` response
<ResponseField name="config" type="object">
Effective configuration after layering. Field names use `snake_case`. Includes `desktop` as an opaque key/value map for client-specific settings.
</ResponseField>
<ResponseField name="origins" type="object">
Map from dotted config paths (e.g. `model`, `tools.web_search.context_size`) to `{ name, version }` describing which layer supplied each value. Use `origins["<path>"].version` as `expectedVersion` on writes.
</ResponseField>
<ResponseField name="layers" type="array | null">
Present when `includeLayers: true`. Each entry has `name`, `version`, `config` (raw JSON), and optional `disabledReason`.
</ResponseField>
After building the response, the server injects runtime feature flags under `config.additional.features.<key>` for: `apps`, `memories`, `mentions_v2`, `plugins`, `remote_control`, `remote_plugin`, `tool_suggest`, `tool_call_mcp_elicitation`. These reflect effective enablement after cloud requirements, CLI flags, `config.toml`, and in-process `experimentalFeature/enablement/set` overrides.
<RequestExample>
```json
{
"method": "config/read",
"id": 1,
"params": {
"includeLayers": true,
"cwd": "/Users/me/my-repo"
}
}
```
</RequestExample>
<ResponseExample>
```json
{
"id": 1,
"result": {
"config": {
"model": "gpt-5.1-codex",
"sandbox_mode": "workspace-write",
"tools": {
"web_search": {
"context_size": "low",
"allowed_domains": ["example.com"]
}
}
},
"origins": {
"model": {
"name": { "type": "user", "file": "/Users/me/.codex/config.toml", "profile": null },
"version": "sha256:abc..."
}
},
"layers": [{ "name": { "type": "user", "file": "..." }, "version": "sha256:abc...", "config": {} }]
}
}
```
</ResponseExample>
Some nested fields require `capabilities.experimentalApi` at `initialize` (for example `config.apps`, `config.approvals_reviewer`). Rejections follow the standard experimental gating messages.
## Writes
Both `config/value/write` and `config/batchWrite` persist atomically to the user config file, validate the post-merge effective config (including `feature_requirements` from requirements), and return a shared write result shape.
### Shared write parameters
<ParamField body="keyPath" type="string" required>
Dotted path into `config.toml`. Supports quoted segments for special characters (e.g. `profiles."team.prod".model` is rejected for writes — see restrictions below). Bare segments like `sample@catalog` remain valid.
</ParamField>
<ParamField body="value" type="JsonValue" required>
JSON value merged into TOML. Use `null` to clear a path.
</ParamField>
<ParamField body="mergeStrategy" type="\"replace\" | \"upsert\"" required>
`replace` overwrites the target path. `upsert` deep-merges objects and replaces scalars.
</ParamField>
<ParamField body="filePath" type="string">
Defaults to the resolved user `config.toml`. Only that path is writable; other paths return `configLayerReadonly`.
</ParamField>
<ParamField body="expectedVersion" type="string">
Optimistic lock against the user layer `version` from `origins` or a prior write response. Mismatch yields `configVersionConflict`.
</ParamField>
### Write response
<ResponseField name="status" type="\"ok\" | \"okOverridden\"">
`okOverridden` means a higher-precedence layer still controls the effective value; see `overriddenMetadata`.
</ResponseField>
<ResponseField name="version" type="string">
New user-layer version hash after the write.
</ResponseField>
<ResponseField name="filePath" type="string">
Canonical path written.
</ResponseField>
<ResponseField name="overriddenMetadata" type="object | null">
When `status` is `okOverridden`, explains which layer wins and the effective value clients will observe.
</ResponseField>
### Restrictions
Writes reject:
- `profile` and any `profiles.*` path (legacy profile tables; use `--profile <name>` with `<name>.config.toml` instead)
- `filePath` outside the user config (error code `configLayerReadonly`)
- Invalid TOML shapes or values that fail post-merge validation (`configValidationError`)
### Post-write side effects
Every successful `config/value/write` or `config/batchWrite` clears the in-process **plugins** and **skills** manager caches. Plugin enablement toggles may emit analytics events.
<ParamField body="reloadUserConfig" type="boolean" default="false">
**`config/batchWrite` only.** When `true`, rebuilds config and calls `refresh_runtime_config` on every loaded thread (same hot-reload path as `experimentalFeature/enablement/set`). Does not automatically refresh MCP server processes — use `config/mcpServer/reload` after MCP table edits.
</ParamField>
:::endpoint POST config/value/write Single-key user config write
<RequestExample>
```json
{
"method": "config/value/write",
"id": 2,
"params": {
"keyPath": "model",
"value": "gpt-new",
"mergeStrategy": "replace",
"expectedVersion": "sha256:abc..."
}
}
```
</RequestExample>
:::
:::endpoint POST config/batchWrite Atomic multi-key user config write
<RequestExample>
```json
{
"method": "config/batchWrite",
"id": 3,
"params": {
"edits": [
{
"keyPath": "hooks.state",
"value": {
"/Users/me/.codex/config.toml:pre_tool_use:0:0": { "enabled": false }
},
"mergeStrategy": "upsert"
}
],
"reloadUserConfig": true
}
}
```
</RequestExample>
:::
```mermaid
sequenceDiagram
participant Client
participant ConfigProcessor as ConfigRequestProcessor
participant ConfigSvc as ConfigManagerService
participant Disk as config.toml
participant Threads as Loaded threads
Client->>ConfigProcessor: config/batchWrite
ConfigProcessor->>ConfigSvc: apply_edits (atomic)
ConfigSvc->>Disk: persist user layer
ConfigSvc-->>ConfigProcessor: ConfigWriteResponse
ConfigProcessor->>ConfigProcessor: clear plugin/skills cache
alt reloadUserConfig true
ConfigProcessor->>Threads: refresh_runtime_config each
end
ConfigProcessor-->>Client: result
```
## Write error codes
Failed writes return JSON-RPC invalid-request errors with `error.data.config_write_error_code`:
| Code | Typical cause |
| --- | --- |
| `configLayerReadonly` | `filePath` is not the user config |
| `configVersionConflict` | `expectedVersion` stale |
| `configValidationError` | Invalid value, forbidden path, or requirements violation |
| `configPathNotFound` | Target path missing when required |
| `configSchemaUnknownKey` | Unknown schema key |
| `userLayerNotFound` | Internal: user layer missing after update |
Config **load** failures (for example cloud requirements fetch) use a separate shape: `error.data.reason: "cloudRequirements"`, `errorCode`, optional `statusCode`, and `action: "relogin"` when auth is required.
## `configRequirements/read`
No parameters (`params` omitted or `undefined`). Returns `{ requirements: null }` when no `requirements.toml`, MDM, or cloud bundle constraints are loaded.
When present, `requirements` includes:
| Field | Meaning |
| --- | --- |
| `allowedApprovalPolicies` | Permitted approval policies |
| `allowedApprovalsReviewers` | Permitted reviewer routing (experimental) |
| `allowedSandboxModes` | Permitted sandbox modes (`externalSandbox` omitted from API) |
| `allowedWindowsSandboxImplementations` | `elevated` / `unelevated` |
| `allowedPermissions` | Permission profile id allow-list |
| `allowedWebSearchModes` | Web search modes (`disabled` always injected if missing) |
| `allowManagedHooksOnly` | Restrict hooks to managed sources |
| `allowAppshots` | App screenshot policy |
| `computerUse` | `allowLockedComputerUse` |
| `featureRequirements` | Pinned feature booleans |
| `hooks` | Managed hook directories and matcher groups (experimental) |
| `enforceResidency` | e.g. `us` |
| `network` | Domain/socket permissions, `managedAllowedDomainsOnly`, legacy allow/deny lists (experimental) |
<RequestExample>
```json
{ "method": "configRequirements/read", "id": 4 }
```
</RequestExample>
## `config/mcpServer/reload`
Reloads the latest config from disk, then queues `Op::RefreshMcpServers` on **every loaded thread** with per-thread MCP server snapshots. Refresh applies on each thread’s **next active turn**, not immediately mid-turn.
- Uses **strict** refresh: if any loaded thread cannot build refresh config, the RPC fails with an internal error (`failed to refresh MCP servers`).
- A separate **best-effort** refresh path exists internally for fault-tolerant scenarios; the public RPC is strict.
<RequestExample>
```json
{ "method": "config/mcpServer/reload", "id": 5 }
```
</RequestExample>
<ResponseExample>
```json
{ "id": 5, "result": {} }
```
</ResponseExample>
<Tip>
After hand-editing `config.toml` without `reloadUserConfig: true`, call `config/mcpServer/reload` so MCP servers pick up table changes without restarting app-server.
</Tip>
## External-agent migration
### `externalAgentConfig/detect`
<ParamField body="includeHome" type="boolean" default="false">
Scan home directories (`~/.claude`, `~/.codex`, etc.) for migratable artifacts.
</ParamField>
<ParamField body="cwds" type="string[]">
Optional repo roots for workspace-scoped detection.
</ParamField>
Returns `items[]` with `itemType`, `description`, `cwd` (`null` = home-scoped), and optional `details` (plugin marketplaces, session paths, MCP server names, hooks, subagents, commands).
| `itemType` | Migrates |
| --- | --- |
| `CONFIG` | Config files |
| `SKILLS` | Skill trees |
| `AGENTS_MD` | `AGENTS.md` style files |
| `PLUGINS` | Plugin marketplace entries |
| `MCP_SERVER_CONFIG` | MCP server definitions |
| `SUBAGENTS` | Subagent configs |
| `HOOKS` | Hook definitions |
| `COMMANDS` | Command definitions |
| `SESSIONS` | Session rollouts (imported as forked threads) |
### `externalAgentConfig/import`
Pass the `migrationItems` subset returned by detect (with matching `cwd` and `details` for plugins/sessions).
**Response timing:** Returns `{}` immediately. When the request includes migration items, the server emits `externalAgentConfig/import/completed` once all work finishes — synchronously if only fast paths run, or after background plugin/session imports complete.
**Runtime refresh:** Imports touching `CONFIG`, `SKILLS`, `MCP_SERVER_CONFIG`, `HOOKS`, `COMMANDS`, or `PLUGINS` clear plugin/skills caches (same as config writes). Plugin and session imports may continue in the background; session imports are serialized (semaphore limit 1).
**Sessions:** Each session path must have been seen during detect; unknown paths return invalid-params. Successful imports create threads via `thread/start`-equivalent forked history and record an import ledger entry.
<Steps>
<Step title="Detect">
Call `externalAgentConfig/detect` with `includeHome` and/or `cwds`, then present `items` to the user.
</Step>
<Step title="Import">
Call `externalAgentConfig/import` with selected `migrationItems` (preserve `details` for plugins/sessions).
</Step>
<Step title="Wait for completion">
Listen for `externalAgentConfig/import/completed`. If MCP servers changed, call `config/mcpServer/reload`.
</Step>
</Steps>
<RequestExample>
```json
{
"method": "externalAgentConfig/detect",
"id": 6,
"params": { "includeHome": true, "cwds": ["/Users/me/project"] }
}
```
</RequestExample>
## Verification checklist
| Action | Expected signal |
| --- | --- |
| Read after edit | `config/read` shows updated `snake_case` fields |
| Optimistic lock | Stale `expectedVersion` → `configVersionConflict` |
| Batch hook disable | `hooks.state` upsert + `reloadUserConfig: true` → threads pick up on next turn settings |
| MCP edit on disk | `config/mcpServer/reload` returns `{}`; MCP status updates on next thread turn |
| Requirements present | `configRequirements/read.requirements` non-null with expected allow-lists |
| External import | `externalAgentConfig/import/completed` notification fires |
<Warning>
`config/batchWrite` does not reload MCP processes unless you also call `config/mcpServer/reload`. `reloadUserConfig` only refreshes thread runtime config snapshots.
</Warning>
## Related pages
<CardGroup>
<Card title="Account, auth, and config" href="/account-auth-and-config">
Login flows, requirements constraints in product context, and hot-reload behavior after writes.
</Card>
<Card title="Skills, plugins, and MCP" href="/skills-plugins-and-mcp">
`mcpServerStatus/list`, OAuth login, and MCP tool calls after config changes.
</Card>
<Card title="RPC methods reference" href="/rpc-methods">
Full v2 method catalog with stable vs experimental markers.
</Card>
<Card title="Schema generation" href="/schema-generation">
Generate TypeScript and JSON Schema fixtures that match your app-server binary version.
</Card>
<Card title="Experimental API" href="/experimental-api">
Opt in via `capabilities.experimentalApi` for gated config and requirements fields.
</Card>
</CardGroup>
---
## 18. Schema generation
> `codex app-server generate-ts` and `generate-json-schema`, stable vs `--experimental` output, version pinning to the running binary, and when to regenerate fixtures after protocol changes.
- Page Markdown: https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/pages/18-schema-generation.md
- Generated: 2026-06-02T06:41:18.499Z
### Source Files
- `README.md`
- `src/main.rs`
- `src/models.rs`
- `src/request_serialization.rs`
- `tests/suite/strict_config.rs`
---
title: "Schema generation"
description: "`codex app-server generate-ts` and `generate-json-schema`, stable vs `--experimental` output, version pinning to the running binary, and when to regenerate fixtures after protocol changes."
---
The `codex app-server` CLI exposes schema generators that materialize the app-server JSON-RPC protocol from Rust types in `codex-app-server-protocol`. Generated TypeScript and JSON Schema artifacts reflect the exact `codex` binary you invoke—use them for client bindings, validation, and CI fixtures after protocol changes.
## Commands and entry points
Integrators and maintainers use different surfaces for the same underlying generators.
| Surface | Command | Typical use |
| --- | --- | --- |
| Installed CLI | `codex app-server generate-ts` | Ship TypeScript types to an app or extension |
| Installed CLI | `codex app-server generate-json-schema` | Ship JSON Schema for validation or codegen |
| Repo maintainer | `just write-app-server-schema` | Refresh vendored fixtures under `codex-rs/app-server-protocol/schema/` |
| Cargo (direct) | `cargo run -p codex-app-server-protocol --bin write_schema_fixtures` | Same as `just write-app-server-schema` |
| Hidden internal | `codex app-server generate-internal-json-schema` | Internal `RolloutLine` JSON Schema only |
Both public CLI subcommands are marked `[experimental]` in the `codex` CLI help. They delegate to `codex_app_server_protocol::generate_ts_with_options` and `generate_json_with_experimental`.
### `generate-ts`
```bash
codex app-server generate-ts --out DIR
codex app-server generate-ts --out DIR --experimental
codex app-server generate-ts --out DIR --prettier /path/to/prettier
```
<ParamField body="--out" type="path" required>
Output directory. TypeScript files are written here, with v2 types under `DIR/v2/`.
</ParamField>
<ParamField body="--prettier" type="path">
Optional Prettier executable. When set, all generated `.ts` files are formatted in place after generation.
</ParamField>
<ParamField body="--experimental" type="boolean" default="false">
Include experimental RPC methods and fields. Default output is stable-only.
</ParamField>
Generation uses `ts-rs` exports from protocol types (`ClientRequest`, `ServerNotification`, v2 param/response types, and dependencies). Each file is prefixed with `// GENERATED CODE! DO NOT MODIFY BY HAND!`. Root and `v2/` each get an `index.ts` that re-exports types; the root index also exposes `export * as v2 from "./v2"`.
### `generate-json-schema`
```bash
codex app-server generate-json-schema --out DIR
codex app-server generate-json-schema --out DIR --experimental
```
<ParamField body="--out" type="path" required>
Output directory for per-type JSON Schema files and bundle files.
</ParamField>
<ParamField body="--experimental" type="boolean" default="false">
Include experimental methods and fields in bundles and per-type schemas.
</ParamField>
JSON output is produced with `schemars`. Two bundle files are always written:
| File | Role |
| --- | --- |
| `codex_app_server_protocol.schemas.json` | Full definitions graph (v1 + v2 namespaces) |
| `codex_app_server_protocol.v2.schemas.json` | Flattened v2-focused bundle for consumers that want a smaller surface |
Individual schema files are also emitted under `v1/`, `v2/`, and the output root (envelopes like `ClientRequest`, `JSONRPCMessage`, server-initiated approval types, and so on).
## Stable vs experimental output
Schema generation defaults to the **stable** API surface—the same surface clients see without opting in at runtime. Experimental methods and fields are stripped unless you pass `--experimental`.
```mermaid
flowchart LR
subgraph sources [Protocol sources]
RS[Rust types in codex-app-server-protocol]
EXP["#[experimental(...)] annotations"]
end
subgraph gen [Generators]
TS[ts-rs TypeScript export]
JS[schemars JSON Schema]
end
subgraph filter [Post-processing when stable]
FM[Filter ClientRequest methods]
FF[Remove experimental fields]
FR[Drop experimental-only type files]
end
subgraph out [Output]
STABLE[Stable DIR]
FULL["--experimental DIR"]
end
RS --> TS
RS --> JS
EXP --> filter
TS --> filter
JS --> filter
filter --> STABLE
TS --> FULL
JS --> FULL
```
Stable filtering removes:
- Experimental client methods from `ClientRequest` unions (driven by `EXPERIMENTAL_CLIENT_METHODS` in protocol `common.rs`).
- Fields registered via `#[experimental("method/field")]` on protocol types.
- Generated type files that exist only for experimental-only RPCs.
<Note>
**Runtime opt-in is separate.** Even with `--experimental` schemas, a client must still send `initialize` with `capabilities.experimentalApi: true` to call experimental methods or set experimental fields. Without opt-in, the server rejects those requests with `<descriptor> requires experimentalApi capability`. See the experimental API page for descriptor examples (`thread/start.mockExperimentalField`, `mock/experimentalMethod`, and similar).
</Note>
### Vendored repo fixtures are stable-only
Committed fixtures live at `codex-rs/app-server-protocol/schema/typescript/` and `schema/json/`. CI compares them to freshly generated **stable** output (`experimental_api: false`). They do not include experimental-only wire methods such as `mock/experimentalMethod` or `thread/turns/list`.
When you change experimental protocol surface area, run `just write-app-server-schema --experimental` locally if you need a full-surface snapshot for review or downstream tooling—but the checked-in fixtures and `schema_fixtures` tests expect the stable tree unless the project explicitly expands that policy.
## Version pinning
Generated artifacts are tied to the **binary that runs the command**, not to a separate schema version string.
From the app-server README: each output is specific to the version of Codex used to run the command, so generated artifacts are guaranteed to match that build.
Practical guidance:
- **Extension or app clients** — Run `codex app-server generate-ts` or `generate-json-schema` with the same `codex` release you target in production, then commit or vendor the output.
- **In-repo development** — Prefer `just write-app-server-schema` so fixtures stay aligned with the workspace protocol crate you are editing.
- **Downstream SDKs** — The Python SDK invokes a pinned installed `codex` binary (`codex app-server generate-json-schema --out …`) so published models match a known runtime.
If you generate from `cargo run` against a local workspace build, you get the workspace protocol definitions. If you generate from a globally installed `codex`, you get whatever protocol that release shipped.
## Output layout
:::files
codex-rs/app-server-protocol/schema/
├── typescript/
│ ├── index.ts # barrel re-exports + `export * as v2`
│ ├── ClientRequest.ts
│ ├── … # shared / v1-adjacent types
│ └── v2/
│ ├── index.ts
│ └── ThreadStartParams.ts, …
└── json/
├── codex_app_server_protocol.schemas.json
├── codex_app_server_protocol.v2.schemas.json
├── ClientRequest.json
├── v1/InitializeParams.json
└── v2/ThreadStartParams.json, …
:::
`write_schema_fixtures` (used by `just write-app-server-schema`) **deletes** existing `typescript/` and `json/` subtrees before regenerating so removed types do not leave stale files.
## Maintainer workflow: when to regenerate
Regenerate fixtures whenever app-server **v2 protocol shapes** change—new RPCs, renamed fields, notification variants, or experimental annotations that affect stable output.
<Steps>
<Step title="Change protocol types">
Edit request/response/notification types in `codex-app-server-protocol` (for example `src/protocol/v2/`, `src/protocol/common.rs`). Follow v2 API conventions: camelCase on the wire, `#[ts(export_to = "v2/")]` on v2 types, and `#[experimental("resource/method")]` or field-level gates where needed.
</Step>
<Step title="Regenerate fixtures">
From the repo root:
```bash
just write-app-server-schema
```
If your change only affects experimental methods or fields (and you need a local full snapshot):
```bash
just write-app-server-schema --experimental
```
Optional Prettier for TypeScript fixtures:
```bash
cargo run -p codex-app-server-protocol --bin write_schema_fixtures -- -p /path/to/prettier
```
</Step>
<Step title="Verify">
```bash
just test -p codex-app-server-protocol
```
Fixture tests fail with a diff and instruct you to run `just write-app-server-schema` when vendored files drift from generated output.
</Step>
<Step title="Update app-server docs when behavior changes">
If RPC behavior or examples in `app-server/README.md` change, update that README in the same change. Regenerate schema fixtures in the same PR when API shapes change.
</Step>
</Steps>
### What triggers regeneration
| Change | Regenerate stable fixtures? | Notes |
| --- | --- | --- |
| New or renamed v2 `*Params` / `*Response` / `*Notification` | Yes | TS + JSON fixtures and bundles |
| New stable client method on `ClientRequest` | Yes | Updates unions and per-type files |
| New `#[experimental(...)]` method only | Stable fixtures unchanged | Use `--experimental` locally if you need full schema |
| Experimental field on otherwise stable method | Yes if field appears in stable schema paths | Stable generation strips the field; shape of surrounding types may still change |
| Server-only notification excluded from JSON | Maybe | Some notifications are excluded from JSON export (for example `rawResponseItem/completed`) by design |
Also run `just write-app-server-schema --experimental` when experimental API fixtures are explicitly part of your workflow (per repository `AGENTS.md` app-server guidance).
### Experimental gating checklist (maintainers)
When adding experimental API surface:
1. Annotate fields with `#[experimental("thread/start.myField")]` on protocol types and derive `ExperimentalApi` on params types.
2. For partial experimental fields on a stable method, use `inspect_params: true` on the method in `common.rs`; for fully experimental methods, annotate the request variant.
3. Regenerate fixtures (`just write-app-server-schema`, plus `--experimental` when needed).
4. Run `just test -p codex-app-server-protocol`.
## Consumer workflow (outside the repo)
For TypeScript or JSON Schema in your own project—not the vendored `schema/` tree:
```bash
# Match your production Codex version
codex app-server generate-ts --out ./types/codex-app-server
codex app-server generate-json-schema --out ./schemas/codex-app-server
```
Add `--experimental` only if your client sets `capabilities.experimentalApi: true` at `initialize` and you intentionally depend on unstable API.
<Warning>
Do not hand-edit generated files. Regenerate from the protocol sources. The standard TypeScript banner marks generated output; fixture tests strip it when comparing vendored trees.
</Warning>
## Internal JSON Schema
`codex app-server generate-internal-json-schema` is a hidden CLI subcommand that writes internal artifacts (for example `RolloutLine`) via `generate_internal_json_schema`. It is not part of the public app-server RPC surface documented for integrators.
## Relationship to the running server
Schema generation is **offline**. It does not require a listening app-server process (`--listen off` is irrelevant). The live server enforces experimental gating and notification filtering at runtime in `message_processor` and transport layers; generated stable schemas describe what clients can rely on without `experimentalApi`, not what the server will accept if experimental fields are sent without opt-in.
## Related pages
<CardGroup>
<Card title="Experimental API" href="/experimental-api">
Runtime `experimentalApi` capability, rejection messages, and maintainer gating patterns aligned with schema filtering.
</Card>
<Card title="Protocol and transport" href="/protocol-and-transport">
JSON-RPC wire format and transports the generated types describe.
</Card>
<Card title="RPC methods reference" href="/rpc-methods">
Catalog of v2 `<resource>/<method>` RPCs and stable vs experimental markers.
</Card>
<Card title="Development and testing" href="/development-and-testing">
Integration tests and `just test -p codex-app-server` expectations alongside protocol crate tests.
</Card>
</CardGroup>
---
## 19. CLI flags and error codes
> `--listen`, `--session-source`, `--strict-config`, websocket auth flags, JSON-RPC standard codes, overload `-32001`, `input_too_large`, and tracing via `RUST_LOG` / `LOG_FORMAT=json`.
- Page Markdown: https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/pages/19-cli-flags-and-error-codes.md
- Generated: 2026-06-02T06:42:53.755Z
### Source Files
- `src/main.rs`
- `src/error_code.rs`
- `src/server_request_error.rs`
- `src/app_server_tracing.rs`
- `README.md`
- `tests/suite/strict_config.rs`
---
title: "CLI flags and error codes"
description: "`--listen`, `--session-source`, `--strict-config`, websocket auth flags, JSON-RPC standard codes, overload `-32001`, `input_too_large`, and tracing via `RUST_LOG` / `LOG_FORMAT=json`."
---
The `codex-app-server` binary (`codex app-server`) exposes startup flags in `src/main.rs` and maps JSON-RPC failures through `src/error_code.rs`. Transport overload uses bounded queues (`CHANNEL_CAPACITY` = 128) and returns code `-32001`; turn input limits surface as `-32602` with structured `input_too_large` data. Process logs go to `stderr` via `tracing`, filtered by `RUST_LOG` and optionally formatted as JSON with `LOG_FORMAT=json`.
## Command entry
```bash
codex app-server [OPTIONS]
```
Flags are defined on `AppServerArgs` in `src/main.rs`. The binary calls `run_main_with_transport_options` in `src/lib.rs`, which loads config, installs tracing, and starts the selected transport.
## `--listen`
<ParamField body="--listen" type="URL" default="stdio://">
Transport endpoint. Parsed by `AppServerTransport::from_listen_url` in `app-server-transport`.
</ParamField>
| URL | Transport | Behavior |
|-----|-----------|----------|
| `stdio://` | Stdio (default) | Newline-delimited JSON on stdin/stdout |
| `unix://` | Unix socket | `$CODEX_HOME/app-server-control/app-server-control.sock` |
| `unix://PATH` | Unix socket | Custom absolute socket path |
| `ws://IP:PORT` | WebSocket | One JSON-RPC message per text frame; **experimental** |
| `off` | Off | No local listener |
<Warning>
`--listen off` with remote control disabled exits with `no transport configured; use --listen or enable remote control`. Use `off` for config-only validation (for example `tests/suite/strict_config.rs`).
</Warning>
Websocket listeners also serve HTTP probes on the same port:
- `GET /readyz` → `200` when accepting connections
- `GET /healthz` → `200` when no `Origin` header
- Requests with an `Origin` header → `403 Forbidden` (unless upgrade auth succeeds)
Loopback websocket without `--ws-auth` still rejects browser `Origin` headers during upgrade (`tests/suite/v2/connection_handling_websocket.rs`).
## `--session-source`
<ParamField body="--session-source" type="SOURCE" default="vscode">
Session metadata for product restrictions. Parsed by `SessionSource::from_startup_arg`.
</ParamField>
| Value | Maps to |
|-------|---------|
| `cli` | `SessionSource::Cli` |
| `vscode` | `SessionSource::VSCode` (default) |
| `exec` | `SessionSource::Exec` |
| `mcp`, `app-server`, `app_server`, `appserver` | `SessionSource::Mcp` |
| `unknown` | `SessionSource::Unknown` |
| Any other non-empty string | `SessionSource::Custom` (trimmed, lowercased) |
Empty values are rejected at parse time. Integrations that need custom product gates pass a stable lowercase label (for example `atlas` in plugin install tests).
## `--strict-config`
<ParamField body="--strict-config" type="bool" default="false">
Fail startup when `config.toml` (or managed config layers) contain unknown fields.
</ParamField>
When `false`, load errors produce a `config/warning` notification and the server falls back to defaults. When `true`, `ConfigManager::load_latest_config` errors propagate and the process exits non-zero.
Integration test expectation (`tests/suite/strict_config.rs`):
```bash
codex-app-server --strict-config --listen off
# stderr contains: unknown configuration field `foo`
```
## Websocket auth flags
Websocket auth applies only to `ws://` listeners. Flags are flattened from `AppServerWebsocketAuthArgs` (`app-server-transport/src/transport/auth.rs`).
<ParamField body="--ws-auth" type="MODE">
Required when any other `ws-*` flag is set. Values: `capability-token`, `signed-bearer-token`.
</ParamField>
### Capability token mode
Requires exactly one of:
<ParamField body="--ws-token-file" type="absolute path">
File containing the raw token (trimmed). Hashed to SHA-256 at startup.
</ParamField>
<ParamField body="--ws-token-sha256" type="64-char hex">
Precomputed SHA-256 digest of the token. Mutually exclusive with `--ws-token-file`.
</ParamField>
Clients send `Authorization: Bearer <token>`. The server compares `SHA-256(token)` to the configured digest with constant-time equality.
### Signed bearer token mode
<ParamField body="--ws-shared-secret-file" type="absolute path" required>
HS256 signing secret file. Secret must be at least 32 bytes after trim.
</ParamField>
<ParamField body="--ws-issuer" type="string">
Optional JWT `iss` claim match (trimmed).
</ParamField>
<ParamField body="--ws-audience" type="string">
Optional JWT `aud` claim match (string or array).
</ParamField>
<ParamField body="--ws-max-clock-skew-seconds" type="u64" default="30">
Clock skew for `exp` / `nbf` validation.
</ParamField>
JWT must include `exp`. `alg=none` tokens are rejected.
### Startup guards
```text
Non-loopback bind (e.g. ws://0.0.0.0:0) without --ws-auth
→ startup error: refusing to start non-loopback websocket listener ... without auth
Short shared secret (< 32 bytes)
→ startup error: must be at least 32 bytes
```
Loopback (`127.0.0.1`) may start without auth, but remote clients should still configure auth before exposing non-loopback addresses.
## Other flags
| Flag | Notes |
|------|-------|
| `--remote-control` | Hidden. Enables remote-control transport when SQLite state DB is available |
| `--disable-plugin-startup-tasks-for-tests` | Debug builds only; skips plugin startup tasks |
## JSON-RPC error codes
Standard JSON-RPC 2.0 codes are centralized in `src/error_code.rs`:
| Code | Constant | Typical use |
|------|----------|-------------|
| `-32600` | `INVALID_REQUEST_ERROR_CODE` | Malformed envelope, duplicate `initialize`, **Not initialized**, unsupported experimental method without opt-in |
| `-32601` | `METHOD_NOT_FOUND_ERROR_CODE` | Unknown or unsupported RPC (for example `thread/turns/items/list is not supported yet`) |
| `-32602` | `INVALID_PARAMS_ERROR_CODE` | Bad params, validation failures, **`input_too_large`** |
| `-32603` | `INTERNAL_ERROR_CODE` | Unexpected failures (turn start, thread load, realtime, and similar) |
| `-32001` | `OVERLOADED_ERROR_CODE` | Ingress queue saturated (retryable) |
Exported for embedders: `INVALID_PARAMS_ERROR_CODE`, `INPUT_TOO_LARGE_ERROR_CODE` (`"input_too_large"`).
### Overload (`-32001`)
```mermaid
sequenceDiagram
participant Client
participant Transport as transport ingress
participant Queue as CHANNEL_CAPACITY=128
participant Processor
Client->>Transport: JSON-RPC request
Transport->>Queue: try_send IncomingMessage
alt queue has capacity
Queue->>Processor: dispatch
Processor-->>Client: result / notifications
else queue full (request only)
Transport-->>Client: error -32001 "Server overloaded; retry later."
end
```
- Socket transports: `app-server-transport` `enqueue_incoming_message` returns the overload error on the connection writer when `transport_event_tx` is full.
- In-process API: same code with messages `in-process app-server request queue is full` or `in-process server request queue is full` (`src/in_process.rs`).
<Note>
Treat `-32001` as retryable. Use exponential backoff with jitter. Notifications and responses may block instead of failing when the queue is full.
</Note>
### `input_too_large`
`turn/start` and related paths call `validate_v2_input_limit` in `src/request_processors/turn_processor.rs`. Limit: `MAX_USER_INPUT_TEXT_CHARS` = `1 << 20` (1,048,576) summed across `V2UserInput` text.
<RequestExample>
```json
{
"jsonrpc": "2.0",
"id": 3,
"error": {
"code": -32602,
"message": "Input exceeds the maximum length of 1048576 characters.",
"data": {
"input_error_code": "input_too_large",
"max_chars": 1048576,
"actual_chars": 1048577
}
}
}
```
</RequestExample>
Clients should branch on `error.data.input_error_code == "input_too_large"` rather than parsing the message string.
### Common `-32600` messages
| Message | When |
|---------|------|
| `Not initialized` | RPC before `initialize` / `initialized` on that connection |
| `Already initialized` | Second `initialize` on same connection |
| `Invalid request: …` | JSON-RPC envelope cannot deserialize to `ClientRequest` |
### Server-request errors
`src/server_request_error.rs` documents `turnTransition` in `error.data.reason` when a pending server request is resolved because turn state changed. This is not a fixed JSON-RPC code; detect via `data.reason == "turnTransition"`.
## Tracing and logs
Tracing is installed in `run_main_with_transport_options` before transports start:
| Variable | Effect |
|----------|--------|
| `RUST_LOG` | `EnvFilter::from_default_env()` — standard `tracing-subscriber` directives (for example `codex_app_server=debug`, `warn`) |
| `LOG_FORMAT=json` | `stderr` emits one JSON object per log line with full span events |
Default format is human-readable text to `stderr`. OpenTelemetry layers may attach when configured (`OTEL_SERVICE_NAME` = `codex-app-server`).
Per-request spans (`src/app_server_tracing.rs`) include:
- `rpc.system` = `jsonrpc`
- `rpc.method`, `rpc.transport` (`stdio`, `unix_socket`, `websocket`, `in-process`)
- `rpc.request_id`, `app_server.connection_id`
- `app_server.client_name` / `app_server.client_version` after `initialize`
Inbound JSON-RPC may carry W3C `trace` fields; invalid carriers are logged and ignored.
<Steps>
<Step title="Enable debug logs for a local run">
```bash
RUST_LOG=codex_app_server=debug,info codex app-server --listen stdio://
```
</Step>
<Step title="Emit JSON logs for log aggregators">
```bash
RUST_LOG=info LOG_FORMAT=json codex app-server --listen ws://127.0.0.1:8765 \
--ws-auth capability-token --ws-token-file /absolute/path/to/token
```
</Step>
</Steps>
## Quick reference
```text
codex-app-server
--listen stdio:// | unix://[PATH] | ws://HOST:PORT | off
--session-source vscode | cli | exec | app-server | <custom>
--strict-config
--ws-auth capability-token | signed-bearer-token
+ token file OR sha256 digest
OR shared-secret file + optional issuer/audience/skew
JSON-RPC errors: -32600 .. -32603, -32001 overload
input_too_large: -32602 + data.input_error_code
Logs: RUST_LOG, LOG_FORMAT=json → stderr
```
## Related pages
<CardGroup>
<Card title="Installation" href="/installation">
Default listen URLs, session source, and logging env vars at launch.
</Card>
<Card title="Protocol and transport" href="/protocol-and-transport">
JSON-RPC wire rules, queue backpressure, and transport comparison.
</Card>
<Card title="Transports and proxy" href="/transports-and-proxy">
Choosing `--listen`, control socket paths, proxy bridging, and websocket pairing.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
`Not initialized`, overload retries, strict config failures, and Origin rejections.
</Card>
</CardGroup>
---
## 20. Thread lifecycle examples
> Copy-paste JSON-RPC sequences for `thread/start`, `thread/resume`, `thread/fork`, `thread/list` pagination, `turn/interrupt`, and `thread/unsubscribe` idle unload from README-backed tests.
- Page Markdown: https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/pages/20-thread-lifecycle-examples.md
- Generated: 2026-06-02T06:41:46.317Z
### Source Files
- `README.md`
- `tests/suite/v2/thread_list.rs`
- `tests/suite/v2/thread_rollback.rs`
- `tests/suite/v2/thread_inject_items.rs`
- `src/request_processors/thread_processor.rs`
- `tests/common/test_app_server.rs`
---
title: "Thread lifecycle examples"
description: "Copy-paste JSON-RPC sequences for `thread/start`, `thread/resume`, `thread/fork`, `thread/list` pagination, `turn/interrupt`, and `thread/unsubscribe` idle unload from README-backed tests."
---
`codex app-server` v2 exposes thread lifecycle as JSON-RPC 2.0 requests on a newline-delimited transport (stdio JSONL by default). Every connection must complete `initialize` / `initialized` before `thread/*` or `turn/*` calls. Thread RPCs auto-subscribe the caller to turn and item notifications; `thread/unsubscribe` drops that subscription without immediately evicting in-memory state.
## Wire format
Each message is one JSON object per line. Field names are **camelCase** on the wire (`threadId`, `nextCursor`, `forkedFromId`). Request ids are client-chosen integers (or strings); responses echo the same `id`.
<Note>
Integration tests in `tests/common/test_app_server.rs` spawn `codex-app-server`, send requests with `send_request`, and read responses/notifications from stdout until a matching `id` or `method` arrives.
</Note>
## Connection bootstrap
Send once per transport connection, then keep reading the stream for interleaved responses and notifications.
<Steps>
<Step title="Initialize">
<RequestExample>
```json
{"method":"initialize","id":0,"params":{"clientInfo":{"name":"my_client","title":"My Client","version":"0.1.0"}}}
```
</RequestExample>
<ResponseExample>
```json
{"id":0,"result":{"userAgent":"…","codexHome":"/path/to/.codex","platformFamily":"…","platformOs":"…"}}
```
</ResponseExample>
</Step>
<Step title="Acknowledge">
<RequestExample>
```json
{"method":"initialized","params":{}}
```
</RequestExample>
</Step>
</Steps>
<Warning>
Any RPC before `initialize` completes returns `"Not initialized"`. A second `initialize` on the same connection returns `"Already initialized"`.
</Warning>
## Lifecycle map
```mermaid
sequenceDiagram
participant Client
participant AppServer
participant Store as Rollout store
Client->>AppServer: initialize
AppServer-->>Client: result
Client->>AppServer: initialized (notification)
alt New conversation
Client->>AppServer: thread/start
AppServer-->>Client: result.thread
AppServer-->>Client: thread/started
else Continue stored session
Client->>AppServer: thread/resume
AppServer-->>Client: result.thread
AppServer-->>Client: thread/started (optional tokenUsage)
else Branch history
Client->>AppServer: thread/fork
AppServer-->>Client: result.thread
AppServer-->>Client: thread/started
end
Client->>AppServer: turn/start
AppServer-->>Client: result.turn
AppServer-->>Client: turn/started, item/*, turn/completed
opt Cancel in-flight work
Client->>AppServer: turn/interrupt
AppServer-->>Client: result {}
AppServer-->>Client: turn/completed (interrupted)
end
Client->>AppServer: thread/unsubscribe
AppServer-->>Client: result.status
Note over AppServer: Idle unload after 30m with no subscribers and no active turn
AppServer-->>Client: thread/status/changed (notLoaded)
AppServer-->>Client: thread/closed
Client->>AppServer: thread/list
AppServer-->>Client: data, nextCursor
Store-->>AppServer: persisted rollouts
```
## `thread/start`
Creates a new thread, returns the thread object, emits `thread/started`, and auto-subscribes this connection to that thread’s turn/item stream. `thread/start` does **not** emit an initial `thread/status/changed`; status is carried on `thread/started`.
<ParamField body="model" type="string">
Optional model override; otherwise config defaults apply.
</ParamField>
<ParamField body="cwd" type="string">
Workspace directory; with `workspace-write` or full-access sandbox, marks the project trusted in `config.toml`.
</ParamField>
<ParamField body="ephemeral" type="boolean">
When `true`, thread stays in-memory only and `thread.path` is `null`.
</ParamField>
<ParamField body="threadSource" type="string">
Optional analytics classification (for example `"user"`).
</ParamField>
<RequestExample>
```json
{"method":"thread/start","id":10,"params":{"model":"gpt-5.1-codex","cwd":"/Users/me/project","approvalPolicy":"never","sandbox":"workspaceWrite","personality":"friendly","threadSource":"user"}}
```
</RequestExample>
<ResponseExample>
```json
{"id":10,"result":{"thread":{"id":"thr_abc","sessionId":"thr_abc","preview":"","name":null,"ephemeral":false,"modelProvider":"openai","createdAt":1730910000,"status":{"type":"idle"},"threadSource":"user"},"model":"gpt-5.1-codex","modelProvider":"openai","cwd":"/Users/me/project","sandbox":"…"}}
{"method":"thread/started","params":{"thread":{"id":"thr_abc","sessionId":"thr_abc","preview":"","name":null,"ephemeral":false,"status":{"type":"idle"},"threadSource":"user","turns":[]}}}
```
</ResponseExample>
<Tip>
Wire contract from `tests/suite/v2/thread_start.rs`: `sessionId` lives on `result.thread`, not as a top-level field; unset titles serialize as `"name": null`; new persistent threads use `"ephemeral": false`.
</Tip>
## `thread/resume`
Reopens a stored thread by id so later `turn/start` calls append to it. Response shape matches `thread/start`. When persisted token usage exists, expect `thread/tokenUsage/updated` after the response.
<RequestExample>
```json
{"method":"thread/resume","id":11,"params":{"threadId":"thr_123","personality":"friendly"}}
```
</RequestExample>
<ResponseExample>
```json
{"id":11,"result":{"thread":{"id":"thr_123","preview":"…","status":{"type":"idle"},"turns":[…]},"model":"…","modelProvider":"openai","cwd":"/abs/path"}}
{"method":"thread/started","params":{"thread":{"id":"thr_123","status":{"type":"idle"},"turns":[]}}}
```
</ResponseExample>
### Resume without inline turns (experimental)
Pass `excludeTurns: true` to get metadata only, then page with `thread/turns/list`. Token-usage replay is skipped in this mode.
<CodeGroup>
```json title="Metadata-only resume"
{"method":"thread/resume","id":12,"params":{"threadId":"thr_123","excludeTurns":true}}
```
```json title="Response"
{"id":12,"result":{"thread":{"id":"thr_123","turns":[]}}}
```
</CodeGroup>
### Resume with bundled turns page (experimental)
`initialTurnsPage` accepts the same paging controls as `thread/turns/list` (`limit`, `sortDirection`, `itemsView`).
<RequestExample>
```json
{"method":"thread/resume","id":13,"params":{"threadId":"thr_123","excludeTurns":true,"initialTurnsPage":{"limit":20,"sortDirection":"desc","itemsView":"summary"}}}
```
</RequestExample>
<ResponseExample>
```json
{"id":13,"result":{"thread":{"id":"thr_123","turns":[]},"initialTurnsPage":{"data":[],"nextCursor":null,"backwardsCursor":null}}}
```
</ResponseExample>
## `thread/fork`
Copies stored history into a **new** thread id, emits `thread/started`, and auto-subscribes. If the source thread is mid-turn, the fork records an interruption marker (same semantics as `turn/interrupt`) instead of copying an unmarked partial suffix.
<ParamField body="threadId" type="string" required>
Source thread id (preferred identifier).
</ParamField>
<ParamField body="ephemeral" type="boolean">
In-memory fork; does not appear in `thread/list`.
</ParamField>
<ParamField body="excludeTurns" type="boolean">
Experimental; omit turn bodies from `result.thread.turns` and page via `thread/turns/list`.
</ParamField>
<RequestExample>
```json
{"method":"thread/fork","id":14,"params":{"threadId":"thr_123","threadSource":"user"}}
```
</RequestExample>
<ResponseExample>
```json
{"id":14,"result":{"thread":{"id":"thr_456","sessionId":"thr_456","forkedFromId":"thr_123","preview":"Saved user message","status":{"type":"idle"},"turns":[{"status":"interrupted","items":[{"type":"userMessage","content":[{"type":"text","text":"Saved user message"}]}]}]}}}
{"method":"thread/started","params":{"thread":{"id":"thr_456","sessionId":"thr_456","forkedFromId":"thr_123","name":null,"turns":[]}}}
```
</ResponseExample>
<Check>
`tests/suite/v2/thread_fork.rs` verifies: new id ≠ source id; `sessionId` equals the new root id; `forkedFromId` points at the source; response may include copied turns but `thread/started` always sends `"turns": []`; original rollout files are not mutated.
</Check>
Ephemeral fork:
```json
{"method":"thread/fork","id":15,"params":{"threadId":"thr_123","ephemeral":true}}
{"id":15,"result":{"thread":{"id":"thr_eph","ephemeral":true,"path":null}}}
```
## `thread/list` pagination
Lists persisted rollouts for history UIs. Default sort is `created_at` descending (newest first). Listed threads that are not loaded report `"status": {"type": "notLoaded"}`.
| Param | Role |
| --- | --- |
| `cursor` | Opaque token from a prior page; omit on first request |
| `limit` | Page size; server picks a default when omitted |
| `sortKey` | `created_at` (default) or `updated_at` |
| `sortDirection` | `desc` (default) or `asc` |
| `modelProviders` | Filter by provider; `[]` means all providers |
| `sourceKinds` | Filter sources; omit/`[]` → interactive (`cli`, `vscode`) |
| `archived` | `true` → archived only; `false`/`null` → non-archived (default) |
| `cwd` | Exact cwd match (string or string array) |
| `searchTerm` | Case-sensitive substring on extracted title |
| `useStateDbOnly` | `true` skips JSONL scan/repair |
<RequestExample>
```json
{"method":"thread/list","id":20,"params":{"limit":2,"modelProviders":["mock_provider"]}}
```
</RequestExample>
<ResponseExample>
```json
{"id":20,"result":{"data":[{"id":"thr_a","preview":"Hello","modelProvider":"mock_provider","createdAt":1730831111,"updatedAt":1730831111,"status":{"type":"notLoaded"}},{"id":"thr_b","preview":"Hello","modelProvider":"mock_provider","createdAt":1730750000,"updatedAt":1730750000,"status":{"type":"notLoaded"}}],"nextCursor":"opaque-token","backwardsCursor":"opaque-token"}}
```
</RequestExample>
Second page (from `tests/suite/v2/thread_list.rs` — three rollouts, `limit: 2`):
<RequestExample>
```json
{"method":"thread/list","id":21,"params":{"cursor":"opaque-token","limit":2,"modelProviders":["mock_provider"]}}
```
</RequestExample>
<ResponseExample>
```json
{"id":21,"result":{"data":[{"id":"thr_c","preview":"Hello","status":{"type":"notLoaded"}}],"nextCursor":null,"backwardsCursor":"opaque-token"}}
```
</ResponseExample>
<Info>
`nextCursor: null` means the final page in the current sort direction. Use `backwardsCursor` with the opposite `sortDirection` to page backward without skipping same-second updates.
</Info>
Filtered listing example from the README:
```json
{"method":"thread/list","id":22,"params":{"limit":25,"cwd":["/Users/me/project","/Users/me/project-worktree"],"sortKey":"created_at"}}
```
## `turn/interrupt`
Cancels an in-flight turn by `(threadId, turnId)`. Success is an empty object `{}`; completion is signaled by `turn/completed` with `"status": "interrupted"`. Does not kill background terminals.
<RequestExample>
```json
{"method":"turn/start","id":30,"params":{"threadId":"thr_abc","input":[{"type":"text","text":"run sleep"}]}}
{"id":30,"result":{"turn":{"id":"turn_xyz","status":"inProgress"}}}
{"method":"turn/interrupt","id":31,"params":{"threadId":"thr_abc","turnId":"turn_xyz"}}
```
</RequestExample>
<ResponseExample>
```json
{"id":31,"result":{}}
{"method":"turn/completed","params":{"threadId":"thr_abc","turn":{"id":"turn_xyz","status":"interrupted"}}}
```
</ResponseExample>
<Warning>
Interrupting a turn that already completed returns JSON-RPC error code `-32600` (`invalid request`). `tests/suite/v2/turn_interrupt.rs` asserts this for a finished assistant turn.
</Warning>
If a command approval is pending, interrupt also emits `serverRequest/resolved` before `turn/completed`.
## `thread/unsubscribe` and idle unload
Removes **this connection’s** subscription to turn/item events. The thread can remain loaded in memory until it has had **no subscribers and no active turn activity** for **30 minutes** (`THREAD_UNLOADING_DELAY` in `src/request_processors/thread_lifecycle.rs`), then the server emits `thread/closed` and `thread/status/changed` → `notLoaded`.
<ResponseField name="status" type="ThreadUnsubscribeStatus">
One of `unsubscribed`, `notSubscribed`, or `notLoaded`.
</ResponseField>
| Status | Meaning |
| --- | --- |
| `unsubscribed` | Connection was subscribed; now removed |
| `notSubscribed` | Connection was not subscribed to that thread |
| `notLoaded` | Thread is not loaded (teardown may already have run) |
<RequestExample>
```json
{"method":"thread/unsubscribe","id":40,"params":{"threadId":"thr_abc"}}
```
</RequestExample>
<ResponseExample>
```json
{"id":40,"result":{"status":"unsubscribed"}}
```
</ResponseExample>
<Note>
`tests/suite/v2/thread_unsubscribe.rs`: immediately after unsubscribe, `thread/closed` does **not** arrive within 250ms; `thread/loaded/list` still includes the thread id. A second unsubscribe on the same connection returns `"status":"notSubscribed"`. Unsubscribing during an in-flight turn does not stop the turn.
</Note>
After the idle window:
```json
{"method":"thread/status/changed","params":{"threadId":"thr_abc","status":{"type":"notLoaded"}}}
{"method":"thread/closed","params":{"threadId":"thr_abc"}}
```
Resuming before unload preserves cached status (for example `systemError` after a failed turn) — `thread/resume` returns the same `thread.status` without requiring a new failure.
## Verification checklist
| Goal | RPC sequence | Pass signal |
| --- | --- | --- |
| New thread | `thread/start` → read response + `thread/started` | `result.thread.id` set; `thread/started` has matching id; no prior `thread/status/changed` for that id |
| Continue session | `thread/resume` with stored `threadId` | Same as start; optional `thread/tokenUsage/updated` |
| Branch | `thread/fork` | New id; `forkedFromId` set; `thread/started.turns` is `[]` |
| History UI | `thread/list` with `limit` + `cursor` | First page `nextCursor` non-null when more rows exist; last page `nextCursor: null` |
| Cancel generation | `turn/interrupt` during active turn | `result: {}` then `turn/completed` with `interrupted` |
| Drop live stream | `thread/unsubscribe` | `status: unsubscribed`; no immediate `thread/closed`; thread still in `thread/loaded/list` |
## Related pages
<CardGroup>
<Card title="Connection lifecycle" href="/connection-lifecycle">
Per-connection handshake, subscribe/unsubscribe semantics, and server-initiated requests.
</Card>
<Card title="Threads, turns, and items" href="/threads-turns-items">
Core primitives, subscription model, thread status states, and ephemeral threads.
</Card>
<Card title="Quickstart" href="/quickstart">
Shortest path from `initialize` through `turn/completed`.
</Card>
<Card title="Stream turns and events" href="/stream-turns-and-events">
Notification methods and delta events after `turn/start`.
</Card>
<Card title="RPC methods reference" href="/rpc-methods">
Full v2 method catalog with stable vs experimental markers.
</Card>
<Card title="Development and testing" href="/development-and-testing">
Running `just test -p codex-app-server` and the `test_app_server` harness.
</Card>
</CardGroup>
---
## 21. Troubleshooting
> Diagnosing `Not initialized`, overload retries, turn failures and `codexErrorInfo`, strict config parse errors, MCP startup `failed` status, and websocket `Origin` rejections.
- Page Markdown: https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/pages/21-troubleshooting.md
- Generated: 2026-06-02T06:42:52.308Z
### Source Files
- `README.md`
- `src/error_code.rs`
- `src/server_request_error.rs`
- `src/request_processors/request_errors.rs`
- `src/mcp_refresh.rs`
- `tests/suite/v2/mcp_server_status.rs`
---
title: "Troubleshooting"
description: "Diagnosing `Not initialized`, overload retries, turn failures and `codexErrorInfo`, strict config parse errors, MCP startup `failed` status, and websocket `Origin` rejections."
---
`codex app-server` surfaces integration failures as JSON-RPC errors on RPC responses, mid-turn `error` notifications, terminal `turn/completed` payloads, startup stderr from `--strict-config`, and HTTP/WebSocket handshake status codes on experimental websocket listeners. Use the tables below to map symptoms to the exact wire identifiers the server emits.
## Symptom map
| Symptom | Typical surface | JSON-RPC code / HTTP status | Primary fix |
| --- | --- | --- | --- |
| RPC before handshake | Response error | `-32600`, message `Not initialized` | Run `initialize`, then send `initialized` |
| Second `initialize` on same connection | Response error | `-32600`, message `Already initialized` | Open a new transport connection |
| Saturated ingress queue | Response error | `-32001`, message `Server overloaded; retry later.` | Exponential backoff with jitter |
| Oversized `turn/start` input | Response error | `-32602` + `data.input_error_code: "input_too_large"` | Trim input to `MAX_USER_INPUT_TEXT_CHARS` |
| Turn or upstream failure | `error` notification and/or `turn/completed` with `status: "failed"` | N/A (notification) | Read `error.codexErrorInfo` and `message` |
| Unknown `config.toml` keys at startup | Process exit, stderr | N/A | Fix config or drop `--strict-config` |
| Required MCP server cannot start | `thread/start` / `thread/resume` RPC error | `-32600` or internal message | Fix `[mcp_servers.*]` transport; check `required = true` |
| Optional MCP server fails | `mcpServer/startupStatus/updated` | N/A (notification) | Read `status: "failed"` and `error` |
| Browser-like websocket client blocked | WebSocket handshake | HTTP `403 Forbidden` | Omit `Origin` or use stdio/unix; do not rely on browser fetch |
| Remote websocket without auth | WebSocket handshake | HTTP `401 Unauthorized` | Pass `Authorization: Bearer …` per `--ws-auth` mode |
```text
Connection opened
│
├─► initialize (RPC) ──► InitializeResponse
│ │
│ └─► initialized (client notification)
│
├─► other RPCs ──► rejected until initialize completes ("Not initialized")
│
└─► turn/start ──► turn/started … turn/completed
│ │
│ ├─► error (optional, same payload shape as failed turn)
│ └─► status: completed | interrupted | failed
```
## `Not initialized` and `Already initialized`
Each transport connection maintains its own session. The server rejects every client RPC except `initialize` until that connection’s `initialize` handler has committed session state.
<Steps>
<Step title="Send initialize first">
Issue one `initialize` request per connection with valid `clientInfo` (`name` must be a valid HTTP header value). Wait for the `InitializeResponse` before any other method on that connection.
</Step>
<Step title="Emit initialized">
After the response, send the client notification `initialized` (no `id`). Integration tests and embedders treat this as part of the documented handshake even though RPC gating keys off the completed `initialize` RPC.
</Step>
<Step title="Verify per-connection isolation">
On websocket transports, initialize responses are connection-scoped: a second parallel connection still returns `Not initialized` until it completes its own `initialize`. Duplicate request IDs on different connections are routed independently.
</Step>
</Steps>
| Error message | When it happens | JSON-RPC `code` |
| --- | --- | --- |
| `Not initialized` | Any RPC except `initialize` before the connection session is initialized | `-32600` (`invalid_request`) |
| `Already initialized` | Second `initialize` on the same connection | `-32600` |
| `Invalid clientInfo.name: '…'. Must be a valid HTTP header value.` | `clientInfo.name` cannot be encoded as an HTTP header | `-32600` |
<Warning>
Do not share one JSON-RPC `id` namespace across connections. Request IDs are scoped per connection; reusing IDs on a single connection returns `duplicate request id` in the in-process path and can confuse response routing on socket transports.
</Warning>
After `initialize` succeeds, the server may emit connection-scoped `config/warning` notifications (non-strict mode) and sets outbound notification delivery for that websocket connection. Thread-scoped events still require `thread/start` or `thread/resume`.
## Server overload (`-32001`)
Bounded queues sit between transport ingress, request processing, and outbound writes. When the transport event queue cannot accept another incoming **request**, the server responds on the same connection with:
```json
{
"error": {
"code": -32001,
"message": "Server overloaded; retry later."
}
}
```
<Info>
Treat `-32001` as retryable. Use exponential backoff with jitter. Avoid tight loops that replay large `turn/start` payloads while the server is saturated.
</Info>
In-process embedding uses the same numeric code with different messages when internal channels are full (`in-process app-server request queue is full`, `in-process server request queue is full`). Clients should key off `code === -32001`, not only the message string.
If the outbound queue is also full, the transport layer may drop the overload error response and log `dropping overload response`; persistent overload under extreme load can look like hung requests—reduce ingress rate and check `RUST_LOG` transport warnings.
## Turn failures and `codexErrorInfo`
Failed turns end with `turn/completed` where `turn.status` is `failed`. The nested error object matches mid-turn `error` notifications:
| Field | Type | Meaning |
| --- | --- | --- |
| `message` | string | Human-readable failure text |
| `codexErrorInfo` | `CodexErrorInfo` enum (optional) | Structured category for UI branching |
| `additionalDetails` | string (optional) | Extra provider or internal detail |
Common `codexErrorInfo` variants (wire names are camelCase):
| Variant | Typical cause |
| --- | --- |
| `contextWindowExceeded` | Context over model limit |
| `usageLimitExceeded` | Account or workspace usage cap |
| `serverOverloaded` | Upstream capacity (distinct from JSON-RPC `-32001`) |
| `httpConnectionFailed` | HTTP errors; may include `httpStatusCode` |
| `responseStreamConnectionFailed` | Cannot open Responses SSE stream |
| `responseStreamDisconnected` | SSE dropped mid-turn |
| `responseTooManyFailedAttempts` | Retry budget exhausted |
| `activeTurnNotSteerable` | `turn/start` / `turn/steer` during `/review`, manual `/compact`, etc. |
| `unauthorized` | Auth failure talking to upstream |
| `badRequest` | Invalid upstream request |
| `sandboxError` | Sandbox policy violation |
| `internalServerError` | Unclassified server fault |
| `other` | Fallback when no specific variant applies |
<Steps>
<Step title="Capture the terminal notification">
Subscribe to `turn/completed` and read `turn.error` when `turn.status === "failed"`.
</Step>
<Step title="Check for a preceding error event">
The `error` notification uses the same payload shape and may arrive before `turn/completed`. Deduplicate in UI if both are shown.
</Step>
<Step title="Branch on codexErrorInfo">
Use `httpStatusCode` when present on HTTP-related variants. For steer failures, inspect `activeTurnNotSteerable.turnKind`.
</Step>
<Step title="Inspect thread status">
After a failed turn, `thread/read` or `thread/list` may report `thread.status` as `systemError` while the thread remains loaded.
</Step>
</Steps>
Rejected `turn/start` with oversized text returns JSON-RPC `-32602` (`invalid_params`) and structured `data`:
```json
{
"input_error_code": "input_too_large",
"max_chars": <MAX_USER_INPUT_TEXT_CHARS>,
"actual_chars": <count>
}
```
The limit is enforced across all text segments in `turn/start` / `turn/steer` `input` (summed character counts).
## Strict config parse errors
<ParamField body="--strict-config" type="flag">
When set, unknown fields in `config.toml` cause startup failure instead of falling back to defaults.
</ParamField>
Without `--strict-config`, a load error produces a `config/warning` notification with summary `Invalid configuration; using defaults.` and the process continues with default config.
Strict mode failure example (integration test expectation):
```text
unknown configuration field `foo`
```
Process exits non-zero; stderr contains the unknown field name. Fix or remove the typo, or run without `--strict-config` for lenient parsing (not recommended for production integrations that must fail closed on config drift).
`config/batchWrite` followed by MCP changes can queue per-thread refresh; `queue_strict_refresh` fails the whole operation if any loaded thread cannot rebuild MCP config, while `queue_best_effort_refresh` logs warnings and continues other threads.
## MCP startup `failed` status
MCP integration problems appear in two places: live startup notifications, and RPC failures when required servers are mandatory.
### Startup notifications
During `thread/start` (and resume paths that initialize MCP), the server emits:
**Method:** `mcpServer/startupStatus/updated`
**Params:** `{ name, status, error }`
| `status` | `error` field |
| --- | --- |
| `starting` | `null` |
| `ready` | `null` |
| `failed` | non-null string (for example `MCP client for \`optional_broken\` failed to start`) |
| `cancelled` | `null` |
Optional broken servers allow `thread/start` to succeed while still emitting `failed` for the broken name. Required servers block the RPC:
```text
required MCP servers failed to initialize … required_broken …
```
In `config.toml`, mark only truly mandatory integrations with `required = true` under `[mcp_servers.<name>]`.
### Inventory RPC (`mcpServerStatus/list`)
`mcpServerStatus/list` returns tool, resource, and auth snapshots for configured servers. It does not replay startup state machines—use `mcpServer/startupStatus/updated` for `failed` transitions. Pass `threadId` when servers are defined in project-local `.codex/config.toml`; without `threadId`, project-scoped entries may be absent. Use `detail: "toolsAndAuthOnly"` to avoid slow inventory RPCs on servers with large resource catalogs.
After editing MCP config on disk, call `config/mcpServer/reload` to reload from disk and queue refresh for loaded threads (applied on each thread’s next active turn) without restarting the binary.
## Websocket `Origin` rejections and auth
Experimental websocket mode (`--listen ws://IP:PORT`) shares one TCP listener for health checks and JSON-RPC over WebSocket frames.
### Origin middleware
Any HTTP request that includes an `Origin` header is rejected with **403 Forbidden** before routing—including WebSocket upgrade attempts from browser environments. Health checks without `Origin` succeed:
| Path | Without `Origin` | With `Origin` |
| --- | --- | --- |
| `GET /healthz` | `200 OK` | `403 Forbidden` |
| `GET /readyz` | `200 OK` | `403 Forbidden` |
| WebSocket upgrade | Auth rules below | `403 Forbidden` |
<Tip>
CLI and IDE integrations should use stdio or the unix control socket (`unix://`) for local control-plane traffic. Avoid browser `fetch` / WebSocket clients that automatically attach `Origin`.
</Tip>
### Bearer auth on non-loopback binds
Binding to a non-loopback address without `--ws-auth` refuses to start. Configure one of:
| `--ws-auth` mode | CLI companions | Handshake failure |
| --- | --- | --- |
| `capability-token` | `--ws-token-file` or `--ws-token-sha256` | `401` with `missing websocket bearer token` or `invalid websocket bearer token` |
| `signed-bearer-token` | `--ws-shared-secret-file`, optional issuer/audience/skew | `401` for missing, invalid, or expired JWT |
Loopback listeners (`127.0.0.1`) may connect without a bearer token, but an `Origin` header still triggers `403` as in integration tests (`https://evil.example`).
## Logging and verification
| Variable / flag | Effect |
| --- | --- |
| `RUST_LOG` | Filter/verbosity for tracing (for example `RUST_LOG=warn` or `debug`) |
| `LOG_FORMAT=json` | One JSON object per line on stderr |
| `CODEX_HOME` | Config and state root used when reproducing strict-config failures |
<Check>
**Stdio smoke test:** spawn `codex-app-server`, send `initialize` → `initialized`, then `thread/start`. No `-32600` / `Not initialized` should appear after the handshake completes.
</Check>
## Related pages
<CardGroup>
<Card title="Connection lifecycle" href="/connection-lifecycle">
Per-connection `initialize`, `clientInfo`, and notification opt-out.
</Card>
<Card title="Protocol and transport" href="/protocol-and-transport">
JSON-RPC codes, backpressure `-32001`, and queue behavior.
</Card>
<Card title="CLI flags and error codes" href="/cli-flags-and-errors">
`--strict-config`, websocket auth flags, and tracing env vars.
</Card>
<Card title="Notifications and events" href="/notifications-and-events">
`turn/completed`, `error`, and `CodexErrorInfo` reference.
</Card>
<Card title="Skills, plugins, and MCP" href="/skills-plugins-and-mcp">
MCP listing, OAuth, reload, and configuration patterns.
</Card>
<Card title="Quickstart" href="/quickstart">
End-to-end handshake with one recovery note.
</Card>
</CardGroup>
---
## 22. Development and testing
> Running integration suites under `tests/suite`, spawning the binary via `test_app_server`, debug env hooks, notify-capture bins, and `just test -p codex-app-server` expectations.
- Page Markdown: https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/pages/22-development-and-testing.md
- Generated: 2026-06-02T06:42:50.109Z
### Source Files
- `tests/all.rs`
- `tests/common/test_app_server.rs`
- `tests/common/lib.rs`
- `tests/suite/mod.rs`
- `src/main.rs`
- `src/bin/notify_capture.rs`
- `BUILD.bazel`
---
title: "Development and testing"
description: "Running integration suites under `tests/suite`, spawning the binary via `test_app_server`, debug env hooks, notify-capture bins, and `just test -p codex-app-server` expectations."
---
The `codex-app-server` crate validates protocol behavior through one Cargo integration binary (`tests/all.rs`), a large `tests/suite` tree, shared helpers in `app_test_support`, and unit tests colocated under `src/`. Most end-to-end coverage spawns the real `codex-app-server` binary over stdio JSON-RPC; a smaller set uses the in-process API to avoid subprocess teardown races.
## Test layout
```text
codex-app-server/
├── src/ # library + main; #[cfg(test)] unit modules
├── src/bin/notify_capture.rs # test-only notify sink binary
├── tests/
│ ├── all.rs # single integration test crate → mod suite
│ ├── common/ # app_test_support (TestAppServer, mocks)
│ └── suite/
│ ├── mod.rs # auth, strict_config, fuzzy_file_search, v2, …
│ └── v2/ # v2 JSON-RPC integration modules (~70 files)
└── BUILD.bazel # Bazel: 16-way shard, long timeout, no-sandbox
```
| Layer | Entry | Role |
| --- | --- | --- |
| Integration (stdio subprocess) | `tests/all.rs` → `tests/suite/**` | Full binary, JSON-RPC over stdin/stdout |
| Integration helpers | `tests/common` (`app_test_support`) | Spawn server, mock model API, rollouts, auth fixtures |
| Unit / in-process | `src/**` `#[cfg(test)]`, `src/in_process.rs` | Fast checks without a child process when appropriate |
| Direct CLI | `tests/suite/strict_config.rs` | Spawns `codex-app-server` via `std::process::Command` |
`tests/all.rs` declares only `mod suite;`, so Cargo builds one integration test binary that includes every module registered in `tests/suite/mod.rs`. The v2 surface is grouped under `tests/suite/v2/mod.rs` (threads, turns, config RPC, MCP, plugins, websocket transports, and related areas).
## Running tests locally
<Steps>
<Step title="Prerequisites">
From the repository root, `just` recipes run with working directory `codex-rs`. Install [cargo-nextest](https://nexte.st/) if it is not already available:
```bash
cargo install --locked cargo-nextest
```
</Step>
<Step title="Run the app-server crate">
```bash
just test -p codex-app-server
```
This invokes `cargo nextest run --no-fail-fast` with `RUST_MIN_STACK=8388608` (8 MiB), then runs `just bench-smoke`. Prefer `just test` over raw `cargo test` so behavior matches CI defaults.
</Step>
<Step title="Scope a single test or module">
```bash
just test -p codex-app-server -- turn_start
just test -p codex-app-server -- suite::v2::initialize::
```
Nextest accepts standard filters after `--`.
</Step>
<Step title="After code changes">
From `codex-rs`:
```bash
just fmt
just fix -p codex-app-server # before large changes / PRs
```
Do not re-run tests after `fmt` or `fix` unless you changed behavior again.
</Step>
</Steps>
<Note>
`codex-rs/.config/nextest.toml` puts all `package(codex-app-server) & kind(test)` integration tests in the `app_server_integration` group with `max-threads = 1`, because each case spawns a fresh app-server subprocess. Library unit tests in `codex_app_server` still run in parallel.
</Note>
### Protocol-only changes
When you change `codex-app-server-protocol` types or wire shapes:
```bash
just write-app-server-schema
just write-app-server-schema --experimental # if experimental API fixtures change
just test -p codex-app-server-protocol
```
## `TestAppServer` harness
`TestAppServer` in `tests/common/test_app_server.rs` is the primary integration client. It resolves the `codex-app-server` binary with `codex_utils_cargo_bin::cargo_bin`, pipes stdio, and speaks newline-delimited JSON-RPC.
### Default child environment
On spawn, the harness typically sets:
| Setting | Value | Purpose |
| --- | --- | --- |
| `CODEX_HOME` | Temp or test dir | Isolated config and rollouts |
| `RUST_LOG` | `warn` | Quieter default logs |
| `CODEX_APP_SERVER_MANAGED_CONFIG_PATH` | `{codex_home}/managed_config.toml` | Avoid host `/etc` managed config |
| CLI args | `--disable-plugin-startup-tasks-for-tests` | Skip plugin startup background work |
It removes `CODEX_INTERNAL_ORIGINATOR_OVERRIDE_ENV_VAR` from the child environment so tests do not inherit host overrides.
### Constructors
| Method | When to use |
| --- | --- |
| `TestAppServer::new` | Default: managed-config path under `codex_home`, plugin startup disabled |
| `new_without_managed_config` | Sets `CODEX_APP_SERVER_DISABLE_MANAGED_CONFIG=1` on the child |
| `new_without_managed_config_with_env` | Same, plus extra env tuples |
| `new_with_env` / `new_with_args` | Override or remove env vars; add CLI flags |
| `new_with_program_and_env` | Point at a custom binary path (e.g. websocket tests) |
| `new_with_plugin_startup_tasks` | Enable real plugin startup (omit the disable flag) |
Env overrides use `(key, Some(value))` to set and `(key, None)` to remove a variable from the child only.
### Handshake and RPC helpers
- `initialize()` — `initialize` RPC with `clientInfo.name = "codex-app-server-tests"`, `capabilities.experimentalApi: true`, then `initialized` notification.
- Typed `send_*_request` methods — one per v2 method (e.g. `send_thread_start_request`, `send_turn_start_request`).
- Stream readers — `read_stream_until_response_message`, `read_stream_until_notification_message`, `read_stream_until_request_message` (server-initiated approvals), with a `pending_messages` buffer for out-of-order notifications.
- `interrupt_turn_and_wait_for_aborted` — Sends `turn/interrupt` and waits for `turn/completed`; avoids nextest `LEAK` when tests end mid-turn.
`DEFAULT_CLIENT_NAME` is `"codex-app-server-tests"`. `DISABLE_PLUGIN_STARTUP_TASKS_ARG` is the string `"--disable-plugin-startup-tasks-for-tests"`.
### Teardown and logging
`Drop` closes stdin, polls briefly for graceful exit, then `start_kill()` with a bounded wait so the child is reaped before nextest teardown. Child stderr is forwarded to the test process as `[mcp stderr] …`. Request/response traffic is logged with `eprintln!` when debugging a failing case.
## Debug-only hooks (integration tests)
These exist so tests can steer config and login without writing to system paths. They are compiled for **debug** builds only unless noted.
### Environment variables
<ParamField body="CODEX_APP_SERVER_MANAGED_CONFIG_PATH" type="string">
Points the server at a test-managed `managed_config.toml` instead of the host default. `TestAppServer::new` sets this to `{codex_home}/managed_config.toml`. `strict_config.rs` and config RPC tests set it explicitly when layering managed config is required.
</ParamField>
<ParamField body="CODEX_APP_SERVER_DISABLE_MANAGED_CONFIG" type="string">
When set to `1`, `true`, or `yes` (case variants accepted), `main` uses `LoaderOverrides::without_managed_config_for_tests()`. `TestAppServer::new_without_managed_config*` sets this on the child.
</ParamField>
<ParamField body="CODEX_APP_SERVER_LOGIN_ISSUER" type="string">
Overrides the ChatGPT login issuer URL in `account_processor` (debug only). Account integration tests set this to a wiremock base URL.
</ParamField>
### Hidden CLI flag
<ParamField body="--disable-plugin-startup-tasks-for-tests" type="flag">
Debug-only, hidden. Skips `PluginStartupTasks` so integration tests do not wait on plugin background startup. Passed by default from `TestAppServer::new`; omit via `new_with_plugin_startup_tasks` when testing plugin startup behavior.
</ParamField>
`src/main.rs` wires managed-config env vars into `LoaderOverrides` before `run_main_with_transport_options`.
## `codex-app-server-test-notify-capture`
Cargo declares a second binary:
```toml
[[bin]]
name = "codex-app-server-test-notify-capture"
path = "src/bin/notify_capture.rs"
```
The program takes **two** arguments: output path, then JSON payload string. It writes to `{path}.tmp`, syncs, and atomically renames into the final path so concurrent readers never see a partial file.
Turn-notification tests (for example `turn_start_notify_payload_includes_initialize_client_name` in `tests/suite/v2/initialize.rs`) register this binary in `config.toml` under `notify` alongside a JSON output file. After `turn/completed`, the test reads the captured JSON and asserts fields such as `client` from `initialize` `clientInfo.name`.
Resolve the binary in tests with:
```rust
codex_utils_cargo_bin::cargo_bin("codex-app-server-test-notify-capture")?
```
## `app_test_support` helpers
The `tests/common` crate (`app_test_support`) is re-exported for all suite modules. Common utilities:
| Category | Examples |
| --- | --- |
| Model API mocks | `create_mock_responses_server_sequence`, `create_mock_responses_server_repeating_assistant`, SSE helpers from `core_test_support::responses` |
| Config | `write_mock_responses_config_toml`, `write_mock_responses_config_toml_with_chatgpt_base_url` |
| Auth | `write_chatgpt_auth`, `ChatGptAuthFixture`, `encode_id_token` |
| Rollouts | `create_fake_rollout`, `rollout_path` |
| Response parsing | `to_response::<T>(JSONRPCResponse)` |
| Analytics | `start_analytics_events_server` |
Many v2 tests follow the same shape: `TempDir` → write `config.toml` pointing at a wiremock `/v1/responses` server → `TestAppServer::new` → `initialize()` → RPC + `timeout(DEFAULT_READ_TIMEOUT, …)` on stream reads. `DEFAULT_READ_TIMEOUT` is often 60 seconds where subprocess or auth RPC latency matters under CI load.
## In-process vs subprocess
Most integration tests use the subprocess harness because they exercise real stdio transport and process lifecycle.
Use **in-process** (`in_process::start` from the library) when subprocess teardown would flake leak detection—for example `mcp_resource_read_returns_error_for_unknown_thread` in `tests/suite/v2/mcp_resource.rs`, which documents that choice explicitly.
Unit tests under `src/` cover processors, serialization, thread state, and tracing (`message_processor_tracing_tests.rs` uses `serial_test::serial` where shared global state requires it).
## CI and Bazel expectations
`BUILD.bazel` configures the crate via `codex_rust_crate`:
| Setting | Value |
| --- | --- |
| Integration test target | `app-server-all-test` (from `tests/all.rs`) |
| Shard count | 16 |
| Timeout | `long` |
| Tags | `no-sandbox` |
| Extra test data | `//codex-rs/bwrap:bwrap` |
Bazel comments note that even one shard can run long and that failed shards retry up to three times; high shard count limits total CI time when a shard fails.
## Flaky tests and platform guards
Some modules use `#[ignore]`, `#[cfg_attr(windows, ignore = …)]`, or `serial_test::serial` for auth flows that mutate global login state. Check the module before assuming a failure is a product regression. Windows-specific process and sandbox tests may be ignored or given longer nextest timeouts via `codex-rs/.config/nextest.toml` overrides (mostly for other packages; app-server integration still benefits from the global `app_server_integration` serialization).
## Adding a new integration test
<Steps>
<Step title="Pick a module">
Add a file under `tests/suite/v2/` (or an existing top-level suite module) and register it in the parent `mod.rs`.
</Step>
<Step title="Isolate state">
Use `tempfile::TempDir` for `CODEX_HOME`, wiremock for HTTP model providers, and `TestAppServer::new_without_managed_config` when managed-config layering is not part of the scenario.
</Step>
<Step title="Drive JSON-RPC">
Call `initialize()`, then typed `send_*` helpers. Use `read_stream_until_*` with timeouts; call `interrupt_turn_and_wait_for_aborted` if the test leaves a turn running.
</Step>
<Step title="Verify locally">
```bash
just test -p codex-app-server -- your_test_name
```
</Step>
</Steps>
<Warning>
Returning from a test while a turn is still in flight without `turn/interrupt` and a terminal `turn/completed` can produce intermittent nextest `LEAK` reports. Prefer `interrupt_turn_and_wait_for_aborted` for in-flight turn scenarios.
</Warning>
## Related pages
<CardGroup>
<Card title="Build a JSON-RPC client" href="/build-jsonrpc-client">
Framing, request ids, and notification ordering—the same wire rules `TestAppServer` implements.
</Card>
<Card title="Schema generation" href="/schema-generation">
Regenerate protocol fixtures after changing v2 types exercised by these tests.
</Card>
<Card title="Experimental API" href="/experimental-api">
Why tests call `initialize` with `experimentalApi: true` by default.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
Runtime errors surfaced by integration scenarios (overload, init, strict config).
</Card>
</CardGroup>
---