98e40dac97
CLI Smoke Test / smoke-test-linux (20) (push) Has been cancelled
CLI Smoke Test / smoke-test-linux (24) (push) Has been cancelled
CLI Smoke Test / smoke-test-windows (20) (push) Has been cancelled
CLI Smoke Test / smoke-test-windows (24) (push) Has been cancelled
Expo App TypeScript typecheck / typecheck (push) Has been cancelled
85 lines
4.8 KiB
Markdown
85 lines
4.8 KiB
Markdown
# Codex: app-server integration
|
|
|
|
## How we run Codex
|
|
|
|
Codex is a **system-wide CLI** (`npm install -g @openai/codex`). We don't bundle it.
|
|
|
|
At startup, `CodexAppServerClient` spawns `codex app-server --listen stdio://` as a child process and talks JSON-RPC 2.0 over stdin/stdout (newline-delimited JSON). The Codex process manages its own model inference, sandbox, and tool execution. We just send prompts and react to events.
|
|
|
|
Version check: `codex --version` must report >= 0.100 for app-server support.
|
|
|
|
## Why app-server (not MCP)
|
|
|
|
The old `codex mcp-server` integration had three unfixable problems:
|
|
|
|
1. **Model change = context loss.** `codex-reply` only accepts `{ prompt, threadId }`. No model param. Changing model meant restarting the session.
|
|
2. **Permission cancel hangs forever.** MCP SDK's `callTool` waits for a response that never comes after `turn_aborted`. Our AbortController workaround was brittle.
|
|
3. **Session ID confusion.** Three different ID fields (`sessionId`, `conversationId`, `threadId`) — only `threadId` worked, and it was undocumented.
|
|
|
|
`codex app-server` solves all three: per-turn model/policy overrides, clean `turn/interrupt` RPC, single `threadId`.
|
|
|
|
## Architecture
|
|
|
|
```
|
|
Mobile App → Happy Server → CLI (runCodex.ts) → CodexAppServerClient → codex app-server (child process)
|
|
↕ JSON-RPC 2.0 over stdio
|
|
Events ← codex/event/* notifications
|
|
Approvals ← item/commandExecution/requestApproval (server→client RPC)
|
|
```
|
|
|
|
The client has three responsibilities:
|
|
- **Lifecycle**: `initialize` handshake → `thread/start` → `turn/start` per message → `turn/interrupt` on abort
|
|
- **Events**: Route `codex/event/*` notifications to the event handler (same EventMsg types as old MCP)
|
|
- **Approvals**: Respond to server→client RPC requests for command/patch approval
|
|
|
|
## Key protocol findings (learned the hard way)
|
|
|
|
These aren't in any docs. Discovered by trial and error:
|
|
|
|
| What | Expected | Actual |
|
|
|------|----------|--------|
|
|
| Thread ID location | `result.conversationId` | `result.thread.id` |
|
|
| Turn params | `conversationId`, `items` | `threadId`, `input` |
|
|
| Input item format | `{ type: "text", data: { text } }` | `{ type: "text", text }` (flat) |
|
|
| Sandbox policy | `"read-only"`, `"workspace-write"` | `{ type: "readOnly" }`, `{ type: "workspaceWrite" }` (camelCase objects) |
|
|
| Approval method | `execCommandApproval` | `item/commandExecution/requestApproval` |
|
|
| Approval decisions | `approved`, `denied`, `abort` | `accept`, `decline`, `cancel` (wire format differs from internal) |
|
|
| Event routing | `codex/event` with type in params | `codex/event/<type>` (type in method name) |
|
|
| Empty model string | Ignored | Error: "model '' not supported" (must omit, not send empty) |
|
|
|
|
## Design decisions
|
|
|
|
### Per-turn overrides (no restart needed)
|
|
Each `turn/start` RPC accepts optional `model`, `approvalPolicy`, `sandboxPolicy`. The thread keeps context across policy changes. This eliminated the mode-change restart block and `experimental_resume` dead code.
|
|
|
|
### Turn completion tracking
|
|
`sendTurnAndWait()` creates a Promise resolved when `task_complete` or `turn_aborted` arrives. Safety nets: 10-minute timeout, process exit handler, disconnect handler. This replaced the AbortController hack.
|
|
|
|
### Duplicate tool call fix
|
|
The old mapper generated `tool-call-start` for both `exec_approval_request` AND `exec_command_begin`. Since the permission handler already renders approval UI via agent state, this created duplicate cards. Fix: only `exec_command_begin` generates `tool-call-start`.
|
|
|
|
### Approval translation layer
|
|
Our internal types use `approved`/`denied`/`abort`. The wire protocol uses `accept`/`decline`/`cancel`. `mapDecisionToWire()` translates between them so the rest of the codebase doesn't need to know about wire format.
|
|
|
|
## Files
|
|
|
|
- `codexAppServerClient.ts` — JSON-RPC client, turn tracking, approval handling
|
|
- `codexAppServerTypes.ts` — Cherry-picked types from the protocol
|
|
- `runCodex.ts` — Main loop, event/approval handler wiring
|
|
- `executionPolicy.ts` — Maps permission modes to approval/sandbox policies
|
|
- `sessionProtocolMapper.ts` — Events → session protocol envelopes (shared with old code)
|
|
|
|
## What we don't handle yet
|
|
|
|
The app-server sends ~60 event types we ignore. Notable ones for future:
|
|
- `collab_*` — multi-agent collaboration events
|
|
- `web_search_*` — web search tool results
|
|
- `planning_*` — planning mode events
|
|
- `streaming_content_delta` — finer-grained streaming
|
|
- `mcp_*` — MCP server lifecycle (we do use `mcp_startup_complete`)
|
|
|
|
## References
|
|
|
|
- [Codex app-server README](https://github.com/openai/codex/blob/main/codex-rs/app-server/README.md)
|
|
- [experimental_resume broken — issue #4393](https://github.com/openai/codex/issues/4393)
|