How Does the OpenAI Codex Plugin Communicate with Codex via the App Server Protocol?
The Codex plugin communicates with Codex through a JSON-RPC-based App Server protocol implemented by the CodexAppServerClient class, using STDIN/STDOUT transport for bidirectional request-response and notification handling.
The openai/codex-plugin-cc repository implements a lightweight, line-delimited JSON-RPC protocol that enables plugin commands to spawn Codex processes, execute turns, run code reviews, and receive asynchronous updates. This article explains the protocol architecture, transport mechanisms, and implementation details drawn directly from the source code.
App Server Protocol Architecture
The protocol stack consists of three layers: the TypeScript interface definitions, the core client implementation, and high-level workflow helpers. This design separates protocol semantics from transport concerns, allowing the same client code to work with both spawned processes and brokered connections.
Protocol Definition Layer
All method signatures, parameter shapes, and notification types are defined in plugins/codex/scripts/lib/app-server-protocol.d.ts. This file serves as the contract between the plugin and the Codex service.
Key RPC methods include:
initialize— Handshake withInitializeParamsandInitializeResponsethread/start— Begin a conversation threadturn/start— Execute a normal Codex turn withTurnStartParamsturn/interrupt— Cancel an in-progress turnreview/start— Initiate a code review session
The protocol also defines AppServerNotification for server-pushed updates like progress events and message deltas.
Core Client Implementation
The CodexAppServerClient class in plugins/codex/scripts/lib/app-server.mjs encapsulates all communication logic. It extends AppServerClientBase, which handles raw I/O operations.
Connection Establishment
The static connect() method determines whether to spawn a new app-server process or connect through a persistent broker:
export class CodexAppServerClient {
// Decision point: spawn vs. broker
// https://github.com/openai/codex-plugin-cc/blob/main/plugins/codex/scripts/lib/app-server.mjs#L335
}
Broker mode activates when the environment variable CODEX_COMPANION_APP_SERVER_ENDPOINT is set. The broker lifecycle management resides in plugins/codex/scripts/lib/broker-lifecycle.mjs, while endpoint parsing is handled by broker-endpoint.mjs.
Transport and Framing
AppServerClientBase manages low-level communication over the child process's STDIN/STDOUT streams:
- Each request receives a monotonically increasing numeric
idvianextId - Pending requests are tracked in
this.pendingas promise resolvers - Responses are matched to pending requests by
id - Notifications route to a handler set via
setNotificationHandler
class AppServerClientBase {
// JSON-RPC framing and pending-request map
// https://github.com/openai/codex-plugin-cc/blob/main/plugins/codex/scripts/lib/app-server.mjs#L57-L66
}
Request and Notification API
The client exposes two primary methods for communication:
client.request(method, params) // Returns Promise<RpcResult>
client.notify(method, params) // Fire-and-forget notification
These serialize payloads to line-delimited JSON, write to STDIN, and parse responses:
// https://github.com/open/openai/codex-plugin-cc/blob/main/plugins/codex/scripts/lib/app-server.mjs#L80-L98
High-Level Workflow Helpers
The companion script plugins/codex/scripts/codex-companion.mjs provides convenience wrappers that plugin commands invoke directly.
runAppServerTurn
Executes a standard Codex turn with automatic client lifecycle management:
export async function runAppServerTurn(cwd, options = {}) {
return withAppServer(cwd, async (client) => {
// https://github.com/openai/codex-plugin-cc/blob/main/plugins/codex/scripts/lib/app-server.mjs#L1095-L1101
});
}
runAppServerReview
Initiates a code review session with streaming updates:
export async function runAppServerReview(cwd, options = {}) {
return withAppServer(cwd, async (client) => {
// https://github.com/openai/codex-plugin-cc/blob/main/plugins/codex/scripts/lib/app-server.mjs#L1002-L1008
});
}
Both helpers use withAppServer() to ensure proper cleanup regardless of success or failure.
Handling Asynchronous Notifications
Codex pushes progress updates, message fragments, and state changes via notifications. The plugin registers a handler to process these:
client.setNotificationHandler((msg) => {
if (msg.method === "item/agentMessage/delta") {
console.log("Agent message:", msg.params);
}
});
The msg type is AppServerNotification from the protocol definition:
// Notification type definitions
// https://github.com/openai/codex-plugin-cc/blob/main/plugins/codex/scripts/lib/app-server-protocol.d.ts#L74-L75
Complete Usage Example
Below is a minimal implementation showing initialization, turn execution, and notification handling:
import { CodexAppServerClient } from "./lib/app-server.mjs";
async function startTurn(workspaceRoot: string) {
// Establish connection (spawn or broker)
const client = await CodexAppServerClient.connect(workspaceRoot);
// Protocol handshake
const init = await client.request("initialize", {
clientInfo: {
title: "My Plugin",
name: "MyAgent",
version: "1.0.0"
},
capabilities: {
experimentalApi: false,
requestAttestation: false,
optOutNotificationMethods: []
}
});
// Start the turn
const turn = await client.request("turn/start", {
/* TurnStartParams */
});
console.log("Turn started:", turn);
}
Key Source Files
| File | Purpose |
|---|---|
plugins/codex/scripts/lib/app-server.mjs |
Core CodexAppServerClient implementation, transport layer, RPC framing |
plugins/codex/scripts/lib/app-server-protocol.d.ts |
TypeScript definitions for all protocol methods and types |
plugins/codex/scripts/codex-companion.mjs |
High-level helpers: runAppServerTurn, runAppServerReview |
plugins/codex/scripts/lib/broker-endpoint.mjs |
Environment variable parsing for broker connections |
plugins/codex/scripts/lib/broker-lifecycle.mjs |
Persistent broker session management |
Summary
- JSON-RPC over STDIN/STDOUT forms the transport substrate for all Codex plugin communication
CodexAppServerClient.connect()abstracts away spawn vs. broker connection modes- Request/response matching uses numeric IDs with a pending promise map
- Notifications enable asynchronous server-pushed updates via configurable handlers
- High-level helpers in
codex-companion.mjsprovide the interface that plugin commands actually invoke
Frequently Asked Questions
What triggers broker mode instead of spawning a new process?
Broker mode activates when the CODEX_COMPANION_APP_SERVER_ENDPOINT environment variable is set. This variable specifies the broker address, causing CodexAppServerClient.connect() to establish a persistent connection rather than spawning a fresh app-server process. Broker mode reduces startup latency when multiple plugin instances run concurrently.
How does the client handle malformed or unexpected JSON-RPC messages?
The AppServerClientBase implementation validates incoming JSON against expected structures. Messages lacking required fields or containing mismatched response IDs are logged and discarded. The pending request map ensures that orphaned promises eventually timeout rather than hanging indefinitely.
Can multiple concurrent requests be in flight simultaneously?
Yes. The client maintains this.pending as a Map from request ID to promise resolver, allowing multiple overlapping requests. Each request receives a unique ID from nextId, and responses are dispatched to the correct resolver by matching that ID. This enables parallel operations like initializing while streaming notifications.
Where are notification handler types defined?
Notification handler signatures and the AppServerNotification discriminated union are declared in app-server-protocol.d.ts. The runtime handler is registered via client.setNotificationHandler(), and the TypeScript compiler ensures type safety for the callback parameter based on the protocol definitions.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →