CodexAppServer
Elixir client for the OpenAI Codex app-server JSON-RPC 2.0 protocol over stdio.
This library provides a clean, dependency-free interface for spawning a Codex CLI process in app-server mode and driving coding agent sessions programmatically. It handles the full protocol lifecycle: subprocess management, session handshake, turn execution, approval/tool/input dispatch, and structured event callbacks.
Extracted and generalized from the Symphony orchestration service.
Installation
Add codex_app_server to your mix.exs dependencies:
def deps do
[
{:codex_app_server, github: "fun-fx/codex_app_server"}
]
endPrerequisites
- Codex CLI installed and available on
$PATH bashavailable (used to spawn the subprocess)
Quick Start
{:ok, result} = CodexAppServer.run("/path/to/workspace", "Fix the authentication bug in lib/auth.ex",
title: "ABC-123: Fix auth bug",
approval_policy: "never",
sandbox: "workspace-write",
sandbox_policy: %{"type" => "workspaceWrite"}
)
IO.inspect(result.session_id) # "thread-abc-turn-1"
IO.inspect(result.result) # :turn_completedMulti-Turn Sessions
For multi-turn conversations, manage the session lifecycle explicitly:
{:ok, session} = CodexAppServer.start_session("/path/to/workspace",
command: "codex app-server",
approval_policy: "never",
sandbox: "workspace-write"
)
{:ok, turn1} = CodexAppServer.run_turn(session, "Fix the bug in auth.ex",
title: "ABC-123: Fix auth",
sandbox_policy: %{"type" => "workspaceWrite"}
)
{:ok, turn2} = CodexAppServer.run_turn(session, "Now add tests for the fix",
title: "ABC-123: Fix auth",
sandbox_policy: %{"type" => "workspaceWrite"}
)
CodexAppServer.stop_session(session)The same Codex subprocess and thread stay alive across turns, preserving conversation context.
Dynamic Tools
Register client-side tools that Codex can invoke during a turn:
tools = [
%{
"name" => "query_database",
"description" => "Run a read-only SQL query against the project database.",
"inputSchema" => %{
"type" => "object",
"required" => ["sql"],
"properties" => %{
"sql" => %{"type" => "string", "description" => "SQL SELECT statement"}
}
}
}
]
tool_executor = fn
"query_database", %{"sql" => sql} ->
case MyApp.Repo.query(sql) do
{:ok, result} ->
%{
"success" => true,
"contentItems" => [%{"type" => "inputText", "text" => Jason.encode!(result.rows)}]
}
{:error, reason} ->
%{
"success" => false,
"contentItems" => [%{"type" => "inputText", "text" => "Query failed: #{inspect(reason)}"}]
}
end
unknown, _args ->
CodexAppServer.Protocol.unsupported_tool_result(unknown)
end
CodexAppServer.run(workspace, prompt,
tools: tools,
tool_executor: tool_executor
)Event Callbacks
Monitor session events with the :on_message callback:
CodexAppServer.run(workspace, prompt,
on_message: fn message ->
case message.event do
:session_started -> Logger.info("Session #{message.session_id} started")
:turn_completed -> Logger.info("Turn completed")
:turn_failed -> Logger.error("Turn failed: #{inspect(message.details)}")
:approval_auto_approved -> Logger.debug("Auto-approved: #{message.decision}")
:tool_call_completed -> Logger.debug("Tool call succeeded")
:tool_call_failed -> Logger.warning("Tool call failed")
:notification -> Logger.debug("Notification: #{inspect(message.payload)}")
_ -> :ok
end
end
)Event Types
| Event | Description |
|---|---|
:session_started | Turn handshake completed, session IDs available |
:turn_completed | Turn finished successfully |
:turn_failed | Turn failed (model error, etc.) |
:turn_cancelled | Turn was cancelled |
:turn_ended_with_error | Turn ended with a client-side error |
:turn_input_required | Codex requested user input (hard failure in non-interactive mode) |
:approval_required | Approval needed but auto-approve is off |
:approval_auto_approved | Request was automatically approved |
:tool_call_completed | Dynamic tool call succeeded |
:tool_call_failed | Dynamic tool call returned failure |
:unsupported_tool_call | Unknown tool was called (error returned to Codex) |
:tool_input_auto_answered | Freeform input prompt was auto-answered |
:notification | Informational protocol notification |
:malformed | Non-JSON line received from subprocess |
:other_message | Unrecognized JSON message |
:startup_failed | Session or turn startup failed |
Configuration Options
Session Options (start_session/2)
| Option | Type | Default | Description |
|---|---|---|---|
:command | String.t() | "codex app-server" | Codex CLI command |
:approval_policy | String.t() | map() | %{"reject" => ...} | Approval policy for the thread |
:sandbox | String.t() | "workspace-write" | Thread sandbox mode |
:tools | [map()] | [] | Dynamic tool specs to register |
:read_timeout_ms | pos_integer() | 5_000 | Timeout for protocol responses |
:client_info | map() | %{name: "codex_app_server_elixir", ...} |
Client identity for initialize |
Turn Options (run_turn/3)
| Option | Type | Default | Description |
|---|---|---|---|
:title | String.t() | "" |
Turn title (e.g., "ABC-123: Fix bug") |
:approval_policy | String.t() | map() | session default | Override approval policy for this turn |
:sandbox_policy | map() | %{"type" => "workspaceWrite"} | Sandbox policy for this turn |
:tool_executor | (String.t(), map() -> map()) | returns unsupported error | Tool call handler |
:on_message | (map() -> any()) | no-op | Event callback |
:turn_timeout_ms | pos_integer() | 3_600_000 (1 hour) | Total turn timeout |
:auto_approve | boolean() | based on policy | Override auto-approve behavior |
Architecture
┌──────────────────────────────────────────────────┐
│ CodexAppServer (public API) │
│ run/3 · start_session/2 · run_turn/3 · stop/1 │
└───────────────────┬──────────────────────────────┘
│
┌───────────────────▼──────────────────────────────┐
│ CodexAppServer.Session │
│ Lifecycle: handshake, turn dispatch, cleanup │
└───────────────────┬──────────────────────────────┘
│
┌───────────────────▼──────────────────────────────┐
│ CodexAppServer.Protocol │
│ JSON-RPC message construction & stream parsing │
│ Approval / tool / input dispatch │
└───────────────────┬──────────────────────────────┘
│
┌───────────────────▼──────────────────────────────┐
│ CodexAppServer.Transport │
│ Erlang Port: spawn, send, receive, stop │
└──────────────────────────────────────────────────┘
│
▼
codex app-server
(stdio JSON-RPC)Protocol Reference
This client implements the Codex app-server protocol:
initialize— capability negotiationinitialized— notification confirming readinessthread/start— create a thread with approval policy, sandbox, and optional toolsturn/start— submit a prompt and begin a coding turn- Stream processing — handle
turn/completed,turn/failed,turn/cancelled, approval requests, tool calls, and user input requests
Development
mix deps.get
mix test
mix credo --strictLicense
MIT — see LICENSE.