This is a significant change to how layers of configuration are applied. In particular, the `ConfigLayerStack` now has two important fields: - `layers: Vec<ConfigLayerEntry>` - `requirements: ConfigRequirements` We merge `TomlValue`s across the layers, but they are subject to `ConfigRequirements` before creating a `Config`. How I would review this PR: - start with `codex-rs/app-server-protocol/src/protocol/v2.rs` and note the new variants added to the `ConfigLayerSource` enum: `LegacyManagedConfigTomlFromFile` and `LegacyManagedConfigTomlFromMdm` - note that `ConfigLayerSource` now has a `precedence()` method and implements `PartialOrd` - `codex-rs/core/src/config_loader/layer_io.rs` is responsible for loading "admin" preferences from `/etc/codex/managed_config.toml` and MDM. Because `/etc/codex/managed_config.toml` is now deprecated in favor of `/etc/codex/requirements.toml` and `/etc/codex/config.toml`, we now include some extra information on the `LoadedConfigLayers` returned in `layer_io.rs`. - `codex-rs/core/src/config_loader/mod.rs` has major changes to `load_config_layers_state()`, which is what produces `ConfigLayerStack`. The docstring has the new specification and describes the various layers that will be loaded and the precedence order. - It uses the information from `LoaderOverrides` "twice," both in the spirit of legacy support: - We use one instances to derive an instance of `ConfigRequirements`. Currently, the only field in `managed_config.toml` that contributes to `ConfigRequirements` is `approval_policy`. This PR introduces `Constrained::allow_only()` to support this. - We use a clone of `LoaderOverrides` to derive `ConfigLayerSource::LegacyManagedConfigTomlFromFile` and `ConfigLayerSource::LegacyManagedConfigTomlFromMdm` layers, as appropriate. As before, this ends up being a "best effort" at enterprise controls, but is enforcement is not guaranteed like it is for `ConfigRequirements`. - Now we only create a "user" layer if `$CODEX_HOME/config.toml` exists. (Previously, a user layer was always created for `ConfigLayerStack`.) - Similarly, we only add a "session flags" layer if there are CLI overrides. - `config_loader/state.rs` contains the updated implementation for `ConfigLayerStack`. Note the public API is largely the same as before, but the implementation is quite different. We leverage the fact that `ConfigLayerSource` is now `PartialOrd` to ensure layers are in the correct order. - A `Config` constructed via `ConfigBuilder.build()` will use `load_config_layers_state()` to create the `ConfigLayerStack` and use the associated `ConfigRequirements` when constructing the `Config` object. - That said, a `Config` constructed via `Config::load_from_base_config_with_overrides()` does _not_ yet use `ConfigBuilder`, so it creates a `ConfigRequirements::default()` instead of loading a proper `ConfigRequirements`. I will fix this in a subsequent PR. Then the following files are mostly test changes: ``` codex-rs/app-server/tests/suite/v2/config_rpc.rs codex-rs/core/src/config/service.rs codex-rs/core/src/config_loader/tests.rs ``` Again, because we do not always include "user" and "session flags" layers when the contents are empty, `ConfigLayerStack` sometimes has fewer layers than before (and the precedence order changed slightly), which is the main reason integration tests changed.
codex-app-server
codex app-server is the interface Codex uses to power rich interfaces such as the Codex VS Code extension.
Table of Contents
- Protocol
- Message Schema
- Core Primitives
- Lifecycle Overview
- Initialization
- API Overview
- Events
- Auth endpoints
Protocol
Similar to MCP, codex app-server supports bidirectional communication, streaming JSONL over stdio. The protocol is JSON-RPC 2.0, though the "jsonrpc":"2.0" header is omitted.
Message Schema
Currently, you can dump a TypeScript version of the schema using codex app-server generate-ts, or a JSON Schema bundle via codex app-server generate-json-schema. Each output is specific to the version of Codex you used to run the command, so the generated artifacts are guaranteed to match that version.
codex app-server generate-ts --out DIR
codex app-server generate-json-schema --out DIR
Core Primitives
The API exposes three top level primitives representing an interaction between a user and Codex:
- Thread: A conversation between a user and the Codex agent. Each thread contains multiple turns.
- Turn: One turn of the conversation, typically starting with a user message and finishing with an agent message. Each turn contains multiple items.
- Item: Represents user inputs and agent outputs as part of the turn, persisted and used as the context for future conversations. Example items include user message, agent reasoning, agent message, shell command, file edit, etc.
Use the thread APIs to create, list, or archive conversations. Drive a conversation with turn APIs and stream progress via turn notifications.
Lifecycle Overview
- Initialize once: Immediately after launching the codex app-server process, send an
initializerequest with your client metadata, then emit aninitializednotification. Any other request before this handshake gets rejected. - Start (or resume) a thread: Call
thread/startto open a fresh conversation. The response returns the thread object and you’ll also get athread/startednotification. If you’re continuing an existing conversation, callthread/resumewith its ID instead. - Begin a turn: To send user input, call
turn/startwith the targetthreadIdand the user's input. Optional fields let you override model, cwd, sandbox policy, etc. This immediately returns the new turn object and triggers aturn/startednotification. - Stream events: After
turn/start, keep reading JSON-RPC notifications on stdout. You’ll seeitem/started,item/completed, deltas likeitem/agentMessage/delta, tool progress, etc. These represent streaming model output plus any side effects (commands, tool calls, reasoning notes). - Finish the turn: When the model is done (or the turn is interrupted via making the
turn/interruptcall), the server sendsturn/completedwith the final turn state and token usage.
Initialization
Clients must send a single initialize request before invoking any other method, then acknowledge with an initialized notification. The server returns the user agent string it will present to upstream services; subsequent requests issued before initialization receive a "Not initialized" error, and repeated initialize calls receive an "Already initialized" error.
Applications building on top of codex app-server should identify themselves via the clientInfo parameter.
Example (from OpenAI's official VSCode extension):
{
"method": "initialize",
"id": 0,
"params": {
"clientInfo": {
"name": "codex-vscode",
"title": "Codex VS Code Extension",
"version": "0.1.0"
}
}
}
API Overview
thread/start— create a new thread; emitsthread/startedand auto-subscribes you to turn/item events for that thread.thread/resume— reopen an existing thread by id so subsequentturn/startcalls append to it.thread/list— page through stored rollouts; supports cursor-based pagination and optionalmodelProvidersfiltering.thread/archive— move a thread’s rollout file into the archived directory; returns{}on success.turn/start— add user input to a thread and begin Codex generation; responds with the initialturnobject and streamsturn/started,item/*, andturn/completednotifications.turn/interrupt— request cancellation of an in-flight turn by(thread_id, turn_id); success is an empty{}response and the turn finishes withstatus: "interrupted".review/start— kick off Codex’s automated reviewer for a thread; responds liketurn/startand emitsitem/started/item/completednotifications withenteredReviewModeandexitedReviewModeitems, plus a final assistantagentMessagecontaining the review.command/exec— run a single command under the server sandbox without starting a thread/turn (handy for utilities and validation).model/list— list available models (with reasoning effort options).skills/list— list skills for one or morecwdvalues (optionalforceReload).mcpServer/oauth/login— start an OAuth login for a configured MCP server; returns anauthorization_urland later emitsmcpServer/oauthLogin/completedonce the browser flow finishes.mcpServerStatus/list— enumerate configured MCP servers with their tools, resources, resource templates, and auth status; supports cursor+limit pagination.feedback/upload— submit a feedback report (classification + optional reason/logs and conversation_id); returns the tracking thread id.command/exec— run a single command under the server sandbox without starting a thread/turn (handy for utilities and validation).config/read— fetch the effective config on disk after resolving config layering.config/value/write— write a single config key/value to the user's config.toml on disk.config/batchWrite— apply multiple config edits atomically to the user's config.toml on disk.
Example: Start or resume a thread
Start a fresh thread when you need a new Codex conversation.
{ "method": "thread/start", "id": 10, "params": {
// Optionally set config settings. If not specified, will use the user's
// current config settings.
"model": "gpt-5.1-codex",
"cwd": "/Users/me/project",
"approvalPolicy": "never",
"sandbox": "workspaceWrite",
} }
{ "id": 10, "result": {
"thread": {
"id": "thr_123",
"preview": "",
"modelProvider": "openai",
"createdAt": 1730910000
}
} }
{ "method": "thread/started", "params": { "thread": { … } } }
To continue a stored session, call thread/resume with the thread.id you previously recorded. The response shape matches thread/start, and no additional notifications are emitted:
{ "method": "thread/resume", "id": 11, "params": { "threadId": "thr_123" } }
{ "id": 11, "result": { "thread": { "id": "thr_123", … } } }
Example: List threads (with pagination & filters)
thread/list lets you render a history UI. Pass any combination of:
cursor— opaque string from a prior response; omit for the first page.limit— server defaults to a reasonable page size if unset.modelProviders— restrict results to specific providers; unset, null, or an empty array will include all providers.
Example:
{ "method": "thread/list", "id": 20, "params": {
"cursor": null,
"limit": 25,
} }
{ "id": 20, "result": {
"data": [
{ "id": "thr_a", "preview": "Create a TUI", "modelProvider": "openai", "createdAt": 1730831111 },
{ "id": "thr_b", "preview": "Fix tests", "modelProvider": "openai", "createdAt": 1730750000 }
],
"nextCursor": "opaque-token-or-null"
} }
When nextCursor is null, you’ve reached the final page.
Example: Archive a thread
Use thread/archive to move the persisted rollout (stored as a JSONL file on disk) into the archived sessions directory.
{ "method": "thread/archive", "id": 21, "params": { "threadId": "thr_b" } }
{ "id": 21, "result": {} }
An archived thread will not appear in future calls to thread/list.
Example: Start a turn (send user input)
Turns attach user input (text or images) to a thread and trigger Codex generation. The input field is a list of discriminated unions:
{"type":"text","text":"Explain this diff"}{"type":"image","url":"https://…png"}{"type":"localImage","path":"/tmp/screenshot.png"}
You can optionally specify config overrides on the new turn. If specified, these settings become the default for subsequent turns on the same thread.
{ "method": "turn/start", "id": 30, "params": {
"threadId": "thr_123",
"input": [ { "type": "text", "text": "Run tests" } ],
// Below are optional config overrides
"cwd": "/Users/me/project",
"approvalPolicy": "unlessTrusted",
"sandboxPolicy": {
"mode": "workspaceWrite",
"writableRoots": ["/Users/me/project"],
"networkAccess": true
},
"model": "gpt-5.1-codex",
"effort": "medium",
"summary": "concise"
} }
{ "id": 30, "result": { "turn": {
"id": "turn_456",
"status": "inProgress",
"items": [],
"error": null
} } }
Example: Interrupt an active turn
You can cancel a running Turn with turn/interrupt.
{ "method": "turn/interrupt", "id": 31, "params": {
"threadId": "thr_123",
"turnId": "turn_456"
} }
{ "id": 31, "result": {} }
The server requests cancellations for running subprocesses, then emits a turn/completed event with status: "interrupted". Rely on the turn/completed to know when Codex-side cleanup is done.
Example: Request a code review
Use review/start to run Codex’s reviewer on the currently checked-out project. The request takes the thread id plus a target describing what should be reviewed:
{"type":"uncommittedChanges"}— staged, unstaged, and untracked files.{"type":"baseBranch","branch":"main"}— diff against the provided branch’s upstream (see prompt for the exactgit merge-base/git diffinstructions Codex will run).{"type":"commit","sha":"abc1234","title":"Optional subject"}— review a specific commit.{"type":"custom","instructions":"Free-form reviewer instructions"}— fallback prompt equivalent to the legacy manual review request.delivery("inline"or"detached", default"inline") — where the review runs:"inline": run the review as a new turn on the existing thread. The response’sreviewThreadIdequals the originalthreadId, and no newthread/startednotification is emitted."detached": fork a new review thread from the parent conversation and run the review there. The response’sreviewThreadIdis the id of this new review thread, and the server emits athread/startednotification for it before streaming review items.
Example request/response:
{ "method": "review/start", "id": 40, "params": {
"threadId": "thr_123",
"delivery": "inline",
"target": { "type": "commit", "sha": "1234567deadbeef", "title": "Polish tui colors" }
} }
{ "id": 40, "result": {
"turn": {
"id": "turn_900",
"status": "inProgress",
"items": [
{ "type": "userMessage", "id": "turn_900", "content": [ { "type": "text", "text": "Review commit 1234567: Polish tui colors" } ] }
],
"error": null
},
"reviewThreadId": "thr_123"
} }
For a detached review, use "delivery": "detached". The response is the same shape, but reviewThreadId will be the id of the new review thread (different from the original threadId). The server also emits a thread/started notification for that new thread before streaming the review turn.
Codex streams the usual turn/started notification followed by an item/started
with an enteredReviewMode item so clients can show progress:
{
"method": "item/started",
"params": {
"item": {
"type": "enteredReviewMode",
"id": "turn_900",
"review": "current changes"
}
}
}
When the reviewer finishes, the server emits item/started and item/completed
containing an exitedReviewMode item with the final review text:
{
"method": "item/completed",
"params": {
"item": {
"type": "exitedReviewMode",
"id": "turn_900",
"review": "Looks solid overall...\n\n- Prefer Stylize helpers — app.rs:10-20\n ..."
}
}
}
The review string is plain text that already bundles the overall explanation plus a bullet list for each structured finding (matching ThreadItem::ExitedReviewMode in the generated schema). Use this notification to render the reviewer output in your client.
Example: One-off command execution
Run a standalone command (argv vector) in the server’s sandbox without creating a thread or turn:
{ "method": "command/exec", "id": 32, "params": {
"command": ["ls", "-la"],
"cwd": "/Users/me/project", // optional; defaults to server cwd
"sandboxPolicy": { "type": "workspaceWrite" }, // optional; defaults to user config
"timeoutMs": 10000 // optional; ms timeout; defaults to server timeout
} }
{ "id": 32, "result": { "exitCode": 0, "stdout": "...", "stderr": "" } }
Notes:
- Empty
commandarrays are rejected. sandboxPolicyaccepts the same shape used byturn/start(e.g.,dangerFullAccess,readOnly,workspaceWritewith flags).- When omitted,
timeoutMsfalls back to the server default.
Events
Event notifications are the server-initiated event stream for thread lifecycles, turn lifecycles, and the items within them. After you start or resume a thread, keep reading stdout for thread/started, turn/*, and item/* notifications.
Turn events
The app-server streams JSON-RPC notifications while a turn is running. Each turn starts with turn/started (initial turn) and ends with turn/completed (final turn status). Token usage events stream separately via thread/tokenUsage/updated. Clients subscribe to the events they care about, rendering each item incrementally as updates arrive. The per-item lifecycle is always: item/started → zero or more item-specific deltas → item/completed.
turn/started—{ turn }with the turn id, emptyitems, andstatus: "inProgress".turn/completed—{ turn }whereturn.statusiscompleted,interrupted, orfailed; failures carry{ error: { message, codexErrorInfo? } }.turn/diff/updated—{ threadId, turnId, diff }represents the up-to-date snapshot of the turn-level unified diff, emitted after every FileChange item.diffis the latest aggregated unified diff across every file change in the turn. UIs can render this to show the full "what changed" view without stitching individualfileChangeitems.turn/plan/updated—{ turnId, explanation?, plan }whenever the agent shares or changes its plan; eachplanentry is{ step, status }withstatusinpending,inProgress, orcompleted.
Today both notifications carry an empty items array even when item events were streamed; rely on item/* notifications for the canonical item list until this is fixed.
Items
ThreadItem is the tagged union carried in turn responses and item/* notifications. Currently we support events for the following items:
userMessage—{id, content}wherecontentis a list of user inputs (text,image, orlocalImage).agentMessage—{id, text}containing the accumulated agent reply.reasoning—{id, summary, content}wheresummaryholds streamed reasoning summaries (applicable for most OpenAI models) andcontentholds raw reasoning blocks (applicable for e.g. open source models).commandExecution—{id, command, cwd, status, commandActions, aggregatedOutput?, exitCode?, durationMs?}for sandboxed commands;statusisinProgress,completed,failed, ordeclined.fileChange—{id, changes, status}describing proposed edits;changeslist{path, kind, diff}andstatusisinProgress,completed,failed, ordeclined.mcpToolCall—{id, server, tool, status, arguments, result?, error?}describing MCP calls;statusisinProgress,completed, orfailed.webSearch—{id, query}for a web search request issued by the agent.imageView—{id, path}emitted when the agent invokes the image viewer tool.enteredReviewMode—{id, review}sent when the reviewer starts;reviewis a short user-facing label such as"current changes"or the requested target description.exitedReviewMode—{id, review}emitted when the reviewer finishes;reviewis the full plain-text review (usually, overall notes plus bullet point findings).compacted-{threadId, turnId}when codex compacts the conversation history. This can happen automatically.
All items emit two shared lifecycle events:
item/started— emits the fullitemwhen a new unit of work begins so the UI can render it immediately; theitem.idin this payload matches theitemIdused by deltas.item/completed— sends the finalitemonce that work finishes (e.g., after a tool call or message completes); treat this as the authoritative state.
There are additional item-specific events:
agentMessage
item/agentMessage/delta— appends streamed text for the agent message; concatenatedeltavalues for the sameitemIdin order to reconstruct the full reply.
reasoning
item/reasoning/summaryTextDelta— streams readable reasoning summaries;summaryIndexincrements when a new summary section opens.item/reasoning/summaryPartAdded— marks the boundary between reasoning summary sections for anitemId; subsequentsummaryTextDeltaentries share the samesummaryIndex.item/reasoning/textDelta— streams raw reasoning text (only applicable for e.g. open source models); usecontentIndexto group deltas that belong together before showing them in the UI.
commandExecution
item/commandExecution/outputDelta— streams stdout/stderr for the command; append deltas in order to render live output alongsideaggregatedOutputin the final item. FinalcommandExecutionitems include parsedcommandActions,status,exitCode, anddurationMsso the UI can summarize what ran and whether it succeeded.
fileChange
item/fileChange/outputDelta- contains the tool call response of the underlyingapply_patchtool call.
Errors
error event is emitted whenever the server hits an error mid-turn (for example, upstream model errors or quota limits). Carries the same { error: { message, codexErrorInfo? } } payload as turn.status: "failed" and may precede that terminal notification.
codexErrorInfo maps to the CodexErrorInfo enum. Common values:
ContextWindowExceededUsageLimitExceededHttpConnectionFailed { httpStatusCode? }: upstream HTTP failures including 4xx/5xxResponseStreamConnectionFailed { httpStatusCode? }: failure to connect to the response SSE streamResponseStreamDisconnected { httpStatusCode? }: disconnect of the response SSE stream in the middle of a turn before completionResponseTooManyFailedAttempts { httpStatusCode? }BadRequestUnauthorizedSandboxErrorInternalServerErrorOther: all unclassified errors
When an upstream HTTP status is available (for example, from the Responses API or a provider), it is forwarded in httpStatusCode on the relevant codexErrorInfo variant.
Approvals
Certain actions (shell commands or modifying files) may require explicit user approval depending on the user's config. When turn/start is used, the app-server drives an approval flow by sending a server-initiated JSON-RPC request to the client. The client must respond to tell Codex whether to proceed. UIs should present these requests inline with the active turn so users can review the proposed command or diff before choosing.
- Requests include
threadIdandturnId—use them to scope UI state to the active conversation. - Respond with a single
{ "decision": "accept" | "decline" }payload (plus optionalacceptSettingson command executions). The server resumes or declines the work and ends the item withitem/completed.
Command execution approvals
Order of messages:
item/started— shows the pendingcommandExecutionitem withcommand,cwd, and other fields so you can render the proposed action.item/commandExecution/requestApproval(request) — carries the sameitemId,threadId,turnId, optionallyreasonorrisk, plusparsedCmdfor friendly display.- Client response —
{ "decision": "accept", "acceptSettings": { "forSession": false } }or{ "decision": "decline" }. item/completed— finalcommandExecutionitem withstatus: "completed" | "failed" | "declined"and execution output. Render this as the authoritative result.
File change approvals
Order of messages:
item/started— emits afileChangeitem withchanges(diff chunk summaries) andstatus: "inProgress". Show the proposed edits and paths to the user.item/fileChange/requestApproval(request) — includesitemId,threadId,turnId, and an optionalreason.- Client response —
{ "decision": "accept" }or{ "decision": "decline" }. item/completed— returns the samefileChangeitem withstatusupdated tocompleted,failed, ordeclinedafter the patch attempt. Rely on this to show success/failure and finalize the diff state in your UI.
UI guidance for IDEs: surface an approval dialog as soon as the request arrives. The turn will proceed after the server receives a response to the approval request. The terminal item/completed notification will be sent with the appropriate status.
Auth endpoints
The JSON-RPC auth/account surface exposes request/response methods plus server-initiated notifications (no id). Use these to determine auth state, start or cancel logins, logout, and inspect ChatGPT rate limits.
API Overview
account/read— fetch current account info; optionally refresh tokens.account/login/start— begin login (apiKeyorchatgpt).account/login/completed(notify) — emitted when a login attempt finishes (success or error).account/login/cancel— cancel a pending ChatGPT login byloginId.account/logout— sign out; triggersaccount/updated.account/updated(notify) — emitted whenever auth mode changes (authMode:apikey,chatgpt, ornull).account/rateLimits/read— fetch ChatGPT rate limits; updates arrive viaaccount/rateLimits/updated(notify).account/rateLimits/updated(notify) — emitted whenever a user's ChatGPT rate limits change.mcpServer/oauthLogin/completed(notify) — emitted after amcpServer/oauth/loginflow finishes for a server; payload includes{ name, success, error? }.
1) Check auth state
Request:
{ "method": "account/read", "id": 1, "params": { "refreshToken": false } }
Response examples:
{ "id": 1, "result": { "account": null, "requiresOpenaiAuth": false } } // No OpenAI auth needed (e.g., OSS/local models)
{ "id": 1, "result": { "account": null, "requiresOpenaiAuth": true } } // OpenAI auth required (typical for OpenAI-hosted models)
{ "id": 1, "result": { "account": { "type": "apiKey" }, "requiresOpenaiAuth": true } }
{ "id": 1, "result": { "account": { "type": "chatgpt", "email": "user@example.com", "planType": "pro" }, "requiresOpenaiAuth": true } }
Field notes:
refreshToken(bool): settrueto force a token refresh.requiresOpenaiAuthreflects the active provider; whenfalse, Codex can run without OpenAI credentials.
2) Log in with an API key
- Send:
{ "method": "account/login/start", "id": 2, "params": { "type": "apiKey", "apiKey": "sk-…" } } - Expect:
{ "id": 2, "result": { "type": "apiKey" } } - Notifications:
{ "method": "account/login/completed", "params": { "loginId": null, "success": true, "error": null } } { "method": "account/updated", "params": { "authMode": "apikey" } }
3) Log in with ChatGPT (browser flow)
- Start:
{ "method": "account/login/start", "id": 3, "params": { "type": "chatgpt" } } { "id": 3, "result": { "type": "chatgpt", "loginId": "<uuid>", "authUrl": "https://chatgpt.com/…&redirect_uri=http%3A%2F%2Flocalhost%3A<port>%2Fauth%2Fcallback" } } - Open
authUrlin a browser; the app-server hosts the local callback. - Wait for notifications:
{ "method": "account/login/completed", "params": { "loginId": "<uuid>", "success": true, "error": null } } { "method": "account/updated", "params": { "authMode": "chatgpt" } }
4) Cancel a ChatGPT login
{ "method": "account/login/cancel", "id": 4, "params": { "loginId": "<uuid>" } }
{ "method": "account/login/completed", "params": { "loginId": "<uuid>", "success": false, "error": "…" } }
5) Logout
{ "method": "account/logout", "id": 5 }
{ "id": 5, "result": {} }
{ "method": "account/updated", "params": { "authMode": null } }
6) Rate limits (ChatGPT)
{ "method": "account/rateLimits/read", "id": 6 }
{ "id": 6, "result": { "rateLimits": { "primary": { "usedPercent": 25, "windowDurationMins": 15, "resetsAt": 1730947200 }, "secondary": null } } }
{ "method": "account/rateLimits/updated", "params": { "rateLimits": { … } } }
Field notes:
usedPercentis current usage within the OpenAI quota window.windowDurationMinsis the quota window length.resetsAtis a Unix timestamp (seconds) for the next reset.