7.4 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 and re-record (below).
Generated types
go generate ./internal/codex runs internal/codex/gen: it asks the CLI for the experimental schema, then writes protocol.gen.go with a Go type for every params and response type of the client methods listed in clientMethods (internal/codex/gen/main.go), every server notification and every server request, and everything they reference. The experimental schema is used because the server sends experimental fields (such as environments on a thread) even to clients that don't opt into them.
- A
oneOf/anyOfwhose alternatives are all objects becomes one struct with every alternative's fields (a field whose type differs between alternatives isjson.RawMessage), plus<Type>Type<Value>constants for itstypediscriminator, as forThreadItem. - Alternatives that are all strings become a string type with a constant per value; a mix of strings and objects (such as
AskForApproval) isjson.RawMessage. - Optional and nullable fields get
omitempty; optional structs are pointers. Method<Name>constants name every method;clientResults,notificationParams,serverRequestParamsandserverRequestResponsesmap methods to their types.
Launch and framing
codex app-server -c check_for_update_on_startup=false
Run in the session's working directory, with GIT_AUTHOR_*/GIT_COMMITTER_* set to the commit identity and anything in Options.Env (byre's agent manager adds the task's ports and CODEX_HOME, see docs/operations.md); shell commands inherit them. 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,dynamicToolsforOptions.MCPServers), orthread/resumewiththreadIdand the same settings but no tools, orthread/fork(Options.ForkFromasthreadId,Options.ForkAtaslastTurnId, 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.Session.IDis the thread id. 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. WithOptions.Reminderset, everyturn/startcarries it asadditionalContext{"byre":{"kind":"application","value":...}}.modelon it switches the model for that turn and every later one (Session.SetModelholds the model for the nextSend);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 (Session.Steer); 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.Sessiontracks the active turn fromturn/startedandturn/completed.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
Each mcp.Server in Options.MCPServers becomes one dynamicTools entry of type namespace, named after the server, holding its 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); Session runs that tool's Handler and 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.- Lines that aren't a JSON object, don't parse, or carry a notification method outside
notificationParamsgo toOptions.OnBadLine; parsed notifications are still delivered as events.Event.Decodereturns the notification's generated params type.
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.
Transcripts
Options.Transcript records every line in both directions as {"dir":"send"|"recv","msg":...}, deltas included, with any "email" value replaced by [redacted]. After a CLI update, run go generate ./internal/codex to regenerate the types.