5.5 KiB
Codex app-server protocol
How internal/codex drives codex app-server. The server's own schema is the reference: codex app-server generate-json-schema --experimental --out DIR writes it, and codex_app_server_protocol.schemas.json in DIR is the whole bundle (top-level definitions plus a nested v2 set).
Binary and version
byre runs codex from PATH. On the owner's machine that is the CLI inside the ChatGPT desktop app (ls -la ~/.local/bin/codex shows the link), so the version moves when the app updates itself; codex --version prints it. byre passes -c check_for_update_on_startup=false so the CLI never checks for or offers updates of its own. SchemaVersion in internal/codex/protocol.gen.go names the version the types were generated from; when it differs from codex --version, regenerate the types (below).
Generated types
go generate ./internal/codex regenerates protocol.gen.go from the CLI's schema; run it after a CLI update. It uses the experimental schema because the server sends experimental fields (such as environments on a thread) even to clients that don't opt into them.
Launch and framing
codex app-server -c check_for_update_on_startup=false
Shell commands inherit the app-server's environment. NDJSON both ways on stdio. Messages are JSON-RPC 2.0 without the "jsonrpc" member: requests {"id","method","params"}, responses {"id","result"} or {"id","error":{"code","message"}}, notifications {"method","params"}. The server adds emittedAtMs to its notifications. The server reads config.toml from CODEX_HOME (default ~/.codex), so the MCP servers, plugins and notify program configured there apply to the session; -c mcp_servers={} doesn't remove them, a separate CODEX_HOME does.
Session lifecycle
initializewithclientInfoandcapabilities.experimentalApitrue (dynamic tools need it: without itthread/startwithdynamicToolsfails withthread/start.dynamicTools requires experimentalApi capability), then theinitializednotification.thread/start(cwd,model,approvalPolicynever,sandboxdanger-full-access,developerInstructionsfor byre's guidance,dynamicToolsfor the tool schemas), orthread/resumewiththreadIdand the same settings but no tools, orthread/fork(threadId,lastTurnId, the last turn the fork keeps; the turn must not be in progress) with the same settings. The server picks the thread id, returned asthread.id; resuming keeps it, forking makes a new one. A thread's dynamic tools are stored with it, so resumed and forked threads keep them.turn/startwiththreadIdandinput, a list ofUserInput:{"type":"text","text":...}, and for each image{"type":"image","url":"data:<type>;base64,..."}before the text. Additional turn context usesadditionalContext, for example{"byre":{"kind":"application","value":...}}.modelon it switches the model for that turn and every later one;thread/readreports the thread's currentmodel. The response carries the new turn;turn/startedfollows.turn/steerwiththreadId,expectedTurnId(the active turn) andinputadds input to the running turn; the model reads it at its next step without the turn ending.turn/interruptwiththreadIdand the activeturnIdends the turn:turn/completedarrives withstatusinterruptedwithin milliseconds, a running command is killed, and the thread stays usable.thread/compact/startruns compaction as a turn of its own:turn/started, an item of typecontextCompaction,turn/completed. The thread keeps its id and remembers the conversation in summary.
Dynamic tools
A dynamicTools entry of type namespace holds tools as functions with their names, descriptions and input schemas. Names and namespaces starting with mcp__ are reserved (dynamic tool name is reserved), so byre's tools are ask_human and so on in the namespace byre; the model refers to them as byre__ask_human. A call arrives as the server request item/tool/call (namespace, tool, arguments, callId, turnId); the client answers {"success":true,"contentItems":[{"type":"inputText","text":...}]}, or success false with the error text. The call also shows as a dynamicToolCall item.
Closing stdin ends the process.
Events
turn/started,turn/completed(turn.statuscompleted,interruptedorfailedwithturn.error). A failed request to the model also sends anerrornotification first, withwillRetry.item/startedanditem/completedwith aThreadItem:userMessage,agentMessage(text),reasoning,commandExecution(command,aggregatedOutput,exitCode),fileChange,mcpToolCall,contextCompactionand others.item/agentMessage/delta(itemId,delta) streams an agent message; the deltas join to the completed item'stext.item/commandExecution/outputDeltaand the reasoning deltas stream the same way.thread/tokenUsage/updatedcarrieslastandtotaltoken counts andmodelContextWindow;account/rateLimits/updatedthe account's rate-limit windows (the same snapshotaccount/rateLimits/readreturns on request, asrateLimits);thread/status/changed,thread/readState/changed,mcpServer/startupStatus/updated,account/updatedandremoteControl/status/changedalso arrive.
Besides item/tool/call, server requests (approvals, user input) shouldn't arrive with approvals off; if one does, command and file-change approvals are accepted and anything else gets a JSON-RPC error.