Current section

Files

Jump to
codex_sdk guides 05-app-server-transport.md
Raw

guides/05-app-server-transport.md

# App-server Transport (JSON-RPC over stdio or websocket)
This guide covers using the **stateful** `codex app-server` transport from Elixir via `Codex.AppServer`.
The SDK supports two external Codex transports:
- **Exec JSONL (default `:exec` compatibility selector)**: `codex exec --json`
- **App-server JSON-RPC (optional)**: managed local `codex app-server` over stdio or managed remote app-server over websocket
Use app-server when you need upstream v2 APIs that are not exposed via exec JSONL (threads list/archive, skills/models/config APIs, server-driven approvals, etc.).
Do not use app-server `fs/*` or `plugin/*` as the default authoring backend for
local plugin work. `Codex.Plugins.*` owns local manifest, marketplace, and
scaffold flows. `Codex.AppServer.plugin_*` remains the runtime verification and
install/discovery surface.
## Prerequisites
- A `codex` CLI install that supports `codex app-server` (run `codex app-server --help`).
- Auth via either:
- `CODEX_API_KEY` (or `auth.json` `OPENAI_API_KEY`), or
- a Codex CLI login under `CODEX_HOME` (default `~/.codex`).
Native OAuth is also available when you want the SDK to manage ChatGPT login
instead of relying on a pre-existing CLI login:
- `oauth: [storage: :file | :auto]` ensures persistent auth exists under the
effective child `CODEX_HOME` before the child launches
- `oauth: [storage: :memory]` performs external `chatgptAuthTokens` login after
initialization and optionally auto-answers refresh requests
The SDK resolves the `codex` executable via `codex_path_override``CODEX_PATH``System.find_executable("codex")`.
When that path is a version-manager shim, the SDK resolves it to a stable installed
binary when possible before launching the child process.
If you need the literal command surface instead of the managed JSON-RPC connection,
`Codex.CLI.app_server/1` launches a raw `codex app-server` subprocess session and
`Codex.CLI.run/2` can be used for one-shot passthrough commands.
`Codex.CLI.app_server/1` also forwards the current websocket auth flags:
`ws_auth`, `ws_token_file`, `ws_shared_secret_file`, `ws_issuer`,
`ws_audience`, and `ws_max_clock_skew_seconds`.
## Connect / Disconnect
`Codex.AppServer.connect/2` starts a supervised `codex app-server` subprocess and performs the required `initialize``initialized` handshake automatically.
The JSON-RPC semantics stay in `codex_sdk`, while the managed subprocess
lifecycle is owned by `cli_subprocess_core`.
If the application supervision tree is unavailable, `connect/2` returns `{:error, :supervisor_unavailable}`.
Pass `experimental_api: true` when you need upstream experimental fields such as
`approvals_reviewer`, granular approval policies, or memory-mode external OAuth auth.
```elixir
{:ok, codex_opts} = Codex.Options.new(%{api_key: System.get_env("CODEX_API_KEY")})
{:ok, conn} = Codex.AppServer.connect(codex_opts, experimental_api: true)
# ... use conn ...
:ok = Codex.AppServer.disconnect(conn)
```
For a managed remote websocket connection, use `Codex.AppServer.connect_remote/2`:
```elixir
{:ok, conn} =
Codex.AppServer.connect_remote(
"wss://app-server.example/ws",
auth_token_env: "CODEX_REMOTE_AUTH_TOKEN",
client_name: "my_app",
experimental_api: true
)
:ok = Codex.AppServer.disconnect(conn)
```
Remote mode does not take `Codex.Options` because it does not spawn a local
`codex` child. The returned pid stays compatible with the rest of the
`Codex.AppServer` API surface, including `subscribe/2`, `unsubscribe/1`,
`respond/3`, request helpers, and `disconnect/1`.
### Child cwd and environment isolation
`Codex.AppServer.connect/2` can also isolate the managed child process itself:
```elixir
tmp_home = Path.join(System.tmp_dir!(), "codex-sdk-app-server-home")
{:ok, conn} =
Codex.AppServer.connect(codex_opts,
cwd: "/path/to/project",
process_env: %{
"CODEX_HOME" => tmp_home,
"HOME" => Path.dirname(tmp_home),
"USERPROFILE" => Path.dirname(tmp_home)
}
)
```
Use this when you need hermetic plugin/config examples or a temporary `CODEX_HOME`
without mutating the caller's shell state. `process_env` is the preferred name;
`env` is accepted as an alias for parity with `Codex.CLI.start/2`.
These launch options apply to the app-server child process. Per-thread working
directories still belong on `thread/start`, `thread/resume`, or
`Codex.Thread.Options`.
For `connect_remote/2`, `cwd` and `process_env` are only used to resolve auth
context for SDK-managed OAuth helpers. They do not launch or mutate a child
process because remote mode has no local child.
### OAuth-aware connect
Persistent child auth:
```elixir
{:ok, conn} =
Codex.AppServer.connect(codex_opts,
process_env: %{"CODEX_HOME" => "/tmp/codex-home"},
oauth: [mode: :auto, storage: :file, interactive?: true]
)
```
Memory-only external auth:
```elixir
{:ok, conn} =
Codex.AppServer.connect(codex_opts,
experimental_api: true,
process_env: %{"CODEX_HOME" => "/tmp/codex-home"},
oauth: [mode: :auto, storage: :memory, auto_refresh: true]
)
```
Notes:
- `storage: :file | :auto` resolves auth relative to the child `cwd` and
`process_env`, then launches the child with that same environment
- `storage: :memory` keeps tokens in memory, calls `account/login/start` with
`chatgptAuthTokens`, and starts a connection-owned refresh responder
- set `auto_refresh: false` when you want to handle
`account/chatgptAuthTokens/refresh` yourself via `subscribe/2`
- remote websocket mode only supports `storage: :memory`; persistent
`:file` / `:auto` child-login preflight is rejected because there is no child
`CODEX_HOME` to prepare
### Remote auth-token transport policy
Remote websocket auth supports both `auth_token:` and `auth_token_env:`.
Bearer headers are only attached when the websocket URL is:
- `wss://...`
- loopback `ws://127.0.0.1/...`
- loopback `ws://localhost/...`
If you configure an auth token for a non-loopback plain `ws://` URL,
`connect_remote/2` returns `{:error, {:invalid_remote_auth_transport, url}}`
instead of sending credentials over an unsafe transport.
When `governed_authority:` refs are supplied, `auth_token_env:` is resolved
only from the provided `process_env` / `env` map. Shell env fallback is
standalone-only. Governed local app-server launch also requires `clear_env?:
true` and rejects unmanaged ambient Codex auth/base-url/model env before the
child process starts.
### Client identity
You can identify your application in the handshake:
```elixir
{:ok, conn} =
Codex.AppServer.connect(codex_opts,
client_name: "my_app",
client_title: "My App",
client_version: "1.2.3"
)
```
## Use app-server as a transport for threads/turns
To keep your existing `Codex.Thread.*` usage but switch the underlying transport, set `transport: {:app_server, conn}` in thread options:
```elixir
{:ok, conn} = Codex.AppServer.connect(codex_opts, experimental_api: true)
{:ok, thread} =
Codex.start_thread(codex_opts, %{
transport: {:app_server, conn},
working_directory: "/path/to/project",
ephemeral: true,
service_name: "my_app",
service_tier: :flex,
ask_for_approval: %{
type: :granular,
sandbox_approval: true,
rules: true,
request_permissions: true
},
approvals_reviewer: :auto_review,
sandbox: :workspace_write
})
{:ok, result} = Codex.Thread.run(thread, "List files and summarize what you see")
```
Streaming works the same way:
```elixir
{:ok, stream} =
Codex.Thread.run_streamed(
thread,
"List the top-level files and summarize them",
service_tier: :priority
)
Enum.each(stream, &IO.inspect/1)
```
## Dynamic host tools
The app-server transport can advertise host-owned dynamic tools on
`thread/start` and `thread/resume`. Add `dynamic_tools:` to
`Codex.Thread.Options` or to `Codex.start_thread/2` attrs. Each spec is the
upstream app-server tool map; at minimum provide `name` and `inputSchema`.
Snake-case `input_schema` / `output_schema` keys are normalized to
`inputSchema` / `outputSchema`.
```elixir
dynamic_tools = [
%{
"name" => "echo_json",
"description" => "Echo JSON arguments back to the model.",
"inputSchema" => %{
"type" => "object",
"properties" => %{"message" => %{"type" => "string"}},
"required" => ["message"]
}
}
]
{:ok, thread} =
Codex.start_thread(codex_opts, %{
transport: {:app_server, conn},
working_directory: "/path/to/project",
dynamic_tools: dynamic_tools
})
```
When the model invokes a dynamic tool, the SDK emits
`%Codex.Events.DynamicToolCallRequested{}` with `id`, `thread_id`, `turn_id`,
`call_id`, `tool_name`, and `arguments`. Execute the host tool and respond to
the request id:
```elixir
case event do
%Codex.Events.DynamicToolCallRequested{} ->
output = Jason.encode!(%{"arguments" => event.arguments})
:ok =
Codex.AppServer.respond(conn, event.id, %{
"success" => true,
"output" => output,
"contentItems" => [
%{"type" => "inputText", "text" => output}
]
})
_other ->
:ok
end
```
Resume behavior is explicit: the transport re-sends non-empty `dynamic_tools`
as `dynamicTools` on `thread/resume`. Hosts should provide the same current
tool registry when rebuilding a resumed thread; do not rely on app-server
process-local state retaining the registry across host restarts.
`examples/live_app_server_dynamic_tools.exs` is the live-only end-to-end proof.
It advertises an `echo_json` tool, handles
`DynamicToolCallRequested`, responds through `Codex.AppServer.respond/3`, and
fails if no dynamic tool call is observed.
## Call app-server v2 APIs directly
App-server enables additional APIs that are not available via exec JSONL. Examples:
```elixir
{:ok, conn} = Codex.AppServer.connect(codex_opts)
{:ok, %{"data" => skills}} =
Codex.AppServer.skills_list(conn, cwds: ["/path/to/project"], force_reload: true)
{:ok, %{"data" => models}} = Codex.AppServer.model_list(conn, limit: 25)
```
When you need feature-flag gating or to load the underlying `SKILL.md` contents,
use `Codex.Skills.list/2` and `Codex.Skills.load/2`, which honor `features.skills`.
```elixir
{:ok, %{"config" => config}} = Codex.AppServer.config_read(conn, include_layers: false)
{:ok, _} = Codex.AppServer.config_write(conn, "features.web_search_request", true)
{:ok, _} = Codex.AppServer.experimental_feature_enablement_set(conn, apps: true, plugins: false)
{:ok, %{"data" => threads, "nextCursor" => cursor}} = Codex.AppServer.thread_list(conn, limit: 10)
{:ok, %{"files" => files}} =
Codex.AppServer.fuzzy_file_search(conn, "readme", roots: ["/path/to/project"])
encoded = Base.encode64("hello from app-server")
{:ok, _} = Codex.AppServer.fs_write_file(conn, "/tmp/demo.txt", encoded)
{:ok, %{"dataBase64" => encoded_back}} = Codex.AppServer.fs_read_file(conn, "/tmp/demo.txt")
IO.puts(Base.decode64!(encoded_back))
alias Codex.Protocol.Plugin
{:ok, %Plugin.ListResponse{marketplaces: marketplaces}} =
Codex.AppServer.plugin_list_typed(conn, cwds: [File.cwd!()])
{:ok, %Plugin.ReadResponse{plugin: plugin}} =
Codex.AppServer.request_typed(
conn,
"plugin/read",
%Plugin.ReadParams{
marketplace_path: List.first(marketplaces).path,
plugin_name: List.first(List.first(marketplaces).plugins).name
},
Plugin.ReadResponse
)
{:ok, _} = Codex.AppServer.thread_shell_command(conn, "thr_123", "git status --short")
```
Additional v2 APIs include:
- `Codex.AppServer.experimental_feature_list/2` and `experimental_feature_enablement_set/2`
- `Codex.AppServer.thread_read/3`, `thread_turns_list/3`, `thread_inject_items/3`, `thread_fork/3`, `thread_shell_command/3`, `thread_rollback/3`, `thread_loaded_list/2`, `thread_memory_mode_set/3`, and `memory_reset/1`
- `Codex.AppServer.fs_read_file/2`, `fs_write_file/3`, `fs_create_directory/3`, `fs_get_metadata/2`, `fs_read_directory/2`, `fs_remove/3`, `fs_copy/4`, `fs_watch/3`, and `fs_unwatch/2`
- `Codex.AppServer.marketplace_add/3`, `marketplace_remove/2`, and `marketplace_upgrade/2`
- `Codex.AppServer.device_key_create/2`, `device_key_public/2`, and `device_key_sign/3`
- raw plugin wrappers: `Codex.AppServer.plugin_list/2`, `plugin_read/3`, `plugin_install/4`, `plugin_uninstall/3`
- typed plugin wrappers: `Codex.AppServer.plugin_list_typed/2`, `plugin_read_typed/3`, `plugin_install_typed/4`, `plugin_uninstall_typed/3`
- `Codex.AppServer.request_typed/5` for `Codex.Protocol.Plugin.*` request/response structs
- local authoring remains on `Codex.Plugins.*`; app-server plugin wrappers are not file-authoring helpers
- `Codex.AppServer.collaboration_mode_list/1` and `Codex.AppServer.apps_list/2`
- `Codex.AppServer.config_requirements/1` and `Codex.AppServer.skills_config_write/3`
- `Codex.AppServer.Mcp.list_servers/2`, `resource_read/4`, and `tool_call/5`
Current upstream routing and sync controls are also covered:
- `ephemeral`, `service_name`, and `service_tier` flow through thread lifecycle calls
- per-turn `service_tier` can be passed through `Codex.Thread.run/3`
- `plugin_install/4` and `plugin_uninstall/3` accept `force_remote_sync: true`
- raw plugin maps preserve newer auth metadata such as `needsAuth`
- typed plugin structs preserve forward-compatible upstream fields in `extra`
`thread_shell_command/3` is a thin wrapper over the app-server's thread-bound
`!` workflow, so treat it with the same care you would give shell access in the
interactive CLI.
`experimental_feature_enablement_set/2` forwards the `enablement` map as given.
The SDK does not keep a stale local allowlist; the connected app-server remains
the source of truth for supported feature keys.
When `include_layers: true`, `config_read/2` returns a `layers` list. Recent Codex versions encode each layer's `name` as a tagged union (`ConfigLayerSource`), for example:
```elixir
%{
"name" => %{"type" => "user", "file" => "/home/me/.codex/config.toml"},
"version" => "sha256:…",
"config" => %{}
}
```
See `Codex.AppServer`, Codex.AppServer.Account, and Codex.AppServer.Mcp for the full request surface.
## Thread management
Common thread-history operations are exposed via:
- `Codex.AppServer.thread_list/2` (supports `sort_key`, `sort_direction`, `archived`, and `use_state_db_only`)
- `Codex.AppServer.thread_archive/2`
- `Codex.AppServer.thread_unarchive/2`
- `Codex.AppServer.thread_compact/2` (uses upstream `thread/compact/start`)
- `Codex.AppServer.thread_read/3` (with optional `include_turns`)
- `Codex.AppServer.thread_turns_list/3` for paged turn history reads
- `Codex.AppServer.thread_inject_items/3` for raw response-item injection
- `Codex.AppServer.thread_fork/3` and `Codex.AppServer.thread_rollback/3`
- `Codex.AppServer.thread_loaded_list/2`
- `Codex.AppServer.thread_memory_mode_set/3` for experimental per-thread memory control
- `Codex.AppServer.thread_resume/3` accepts optional `history`, `path`, `service_tier`, `permission_profile`, `exclude_turns`, and `persist_extended_history` overrides
`thread_memory_mode_set/3` and `memory_reset/1` are experimental. Connect with
`experimental_api: true` and prefer an isolated `CODEX_HOME` when you are
testing or demonstrating global memory resets.
## Marketplace, MCP, and filesystem runtime controls
Recent upstream builds also expose:
- `Codex.AppServer.marketplace_add/3`, `marketplace_remove/2`, and
`marketplace_upgrade/2` for marketplace acquisition and maintenance
- `Codex.AppServer.Mcp.list_servers/2` `detail: :tools_and_auth_only` for
lighter MCP inventory reads
- `Codex.AppServer.Mcp.resource_read/4` and `tool_call/5` for thread-scoped MCP
access
- `Codex.AppServer.fs_watch/3` and `fs_unwatch/2` for `fs/changed`
notification workflows
Example:
```elixir
{:ok, conn} =
Codex.AppServer.connect(codex_opts,
experimental_api: true,
cwd: "/tmp/workspace",
process_env: %{"CODEX_HOME" => "/tmp/codex-home"}
)
{:ok, _} = Codex.AppServer.marketplace_add(conn, "./source-marketplace")
{:ok, _} = Codex.AppServer.marketplace_upgrade(conn, marketplace_name: "source-marketplace")
{:ok, %{"data" => servers}} = Codex.AppServer.Mcp.list_servers(conn, detail: :tools_and_auth_only)
{:ok, %{"path" => watched}} = Codex.AppServer.fs_watch(conn, "watch_1", "/tmp/demo.txt")
```
For a dedicated walkthrough of these newer parity surfaces, see
`guides/12-operational-workflows.md`.
## Subagent host controls
When a parent turn spawns child threads, the deterministic host-side control
surface lives in `Codex.Subagents`.
In the current vendored runtime, child spawning is still gated behind the
experimental `features.multi_agent` config flag, so enable that before you
expect a parent turn to create children.
Use it for:
- listing subagent threads with `Codex.Subagents.list/2`
- discovering spawned children for a known parent with `Codex.Subagents.children/3`
- reading a known child thread with `Codex.Subagents.read/3`
- parsing typed source metadata with `Codex.Subagents.source/1`
- extracting the parent id with `Codex.Subagents.parent_thread_id/1`
- confirming whether a thread is a spawned child with `Codex.Subagents.child_thread?/1`
- polling a known child thread to a terminal turn state with `Codex.Subagents.await/3`
The typed source structs are:
- `Codex.Protocol.SessionSource`
- `Codex.Protocol.SubAgentSource`
This surface is intentionally limited to inspection and polling over existing
threads. Decisions such as whether to delegate, how many children to create, or
which role to use still belong in the parent prompt rather than helper APIs.
For a runnable live flow that combines prompt-mediated delegation with the full
host-side helper surface, see `examples/live_subagent_host_controls.exs`.
## Legacy v1 APIs
Older app-server builds only implement the v1 conversation endpoints. Use
`Codex.AppServer.V1` for those flows:
```elixir
{:ok, conn} = Codex.AppServer.connect(codex_opts)
{:ok, convo} = Codex.AppServer.V1.new_conversation(conn, %{})
{:ok, _} = Codex.AppServer.V1.send_user_message(conn, convo["conversationId"], "Hello!")
```
## Notifications and server requests (approvals)
App-server is bidirectional: the server can send notifications at any time, and it can also send **requests** that require a response (approvals).
Subscribe from any process:
```elixir
:ok = Codex.AppServer.subscribe(conn)
```
Messages arrive as:
- Notifications: `{:codex_notification, method, params}`
- Server requests: `{:codex_request, id, method, params}`
You can filter by thread id and/or method list:
```elixir
:ok = Codex.AppServer.subscribe(conn,
thread_id: "thr_123",
methods: [
"turn/completed",
"item/completed",
"item/commandExecution/requestApproval",
"item/permissions/requestApproval",
"item/autoApprovalReview/started",
"item/autoApprovalReview/completed",
"serverRequest/resolved"
]
)
```
### Raw response items and deprecations
When `experimental_raw_events` is enabled on `thread/start` or
`Codex.AppServer.V1.add_conversation_listener/3`, the server emits
`rawResponseItem/completed` notifications. The SDK maps these to
`%Codex.Events.RawResponseItemCompleted{}` and parses known item types such as
ghost snapshots and compaction payloads. Deprecation warnings are surfaced as
`%Codex.Events.DeprecationNotice{}` from `deprecationNotice` notifications.
Config warnings are surfaced as `%Codex.Events.ConfigWarning{}` from
`configWarning` notifications.
Current upstream builds also emit `mcpServer/startupStatus/updated`. The SDK
maps that notification to `%Codex.Events.McpServerStartupStatusUpdated{}`,
normalizing the startup `status` and any optional error payload.
### Terminal turn timing and errors
The `turn/completed` adapter projects optional terminal metadata onto
`%Codex.Events.TurnCompleted{}`:
- `started_at`, `completed_at`, and `duration_ms`
- `status` and the terminal `error` payload
The unified event struct also has `time_to_first_token_ms` for terminal frames
that provide it; current app-server `turn/completed` does not, so that field is
normally `nil` on this lane.
Use `Codex.Events.turn_duration_ms/1` to prefer an explicit duration and fall
back to the start/completion timestamps. Every field is absence-tolerant.
Authenticated `codex-cli 0.144.1` app-server capture already emitted
`startedAt`, `completedAt`, and `durationMs`; its exec JSONL completion remained
usage-only. A failed app-server completion carries failure detail in `error`,
whereas current exec JSONL reports failure through its separate `TurnFailed`
event.
### Request user input
When the agent calls `request_user_input`, app-server sends an
`item/tool/requestUserInput` request. The SDK emits `%Codex.Events.RequestUserInput{}`.
Respond with a `Codex.Protocol.RequestUserInput.Response` payload:
```elixir
response = %Codex.Protocol.RequestUserInput.Response{
answers: %{
"q1" => %Codex.Protocol.RequestUserInput.Answer{answers: ["yes"]}
}
}
:ok = Codex.AppServer.respond(conn, id, Codex.Protocol.RequestUserInput.Response.to_map(response))
```
Question payloads now include `is_other` and `is_secret` when upstream sets them.
The parsed `Codex.Protocol.RequestUserInput.Question` and nested `Option`
structs preserve forward-compatible unknown fields in `extra`, and
`to_map/1` merges them back into the outgoing payload.
### Manual approval handling (UI loop)
When Codex needs approval for a command, file change, or extra permissions during a `turn/start`,
it sends a server request:
- `item/commandExecution/requestApproval`
- `item/fileChange/requestApproval`
- `item/permissions/requestApproval`
Respond by echoing the request `id` back with a result payload containing `decision`.
```elixir
receive do
{:codex_request, id, "item/commandExecution/requestApproval", _params} ->
:ok = Codex.AppServer.respond(conn, id, %{decision: "accept"})
{:codex_request, id, "item/permissions/requestApproval", params} ->
requested =
params["permissions"]
|> Codex.Protocol.RequestPermissions.RequestPermissionProfile.from_map()
response =
%Codex.Protocol.RequestPermissions.Response{
permissions:
requested
|> Codex.Protocol.RequestPermissions.RequestPermissionProfile.to_map()
|> Codex.Protocol.RequestPermissions.GrantedPermissionProfile.from_map(),
scope: :turn
}
|> Codex.Protocol.RequestPermissions.Response.to_map()
:ok = Codex.AppServer.respond(conn, id, response)
end
```
Supported `decision` values include:
- `"accept"`
- `"acceptForSession"`
- `"decline"`
- `"cancel"`
- `%{"acceptWithExecpolicyAmendment" => %{"execpolicyAmendment" => ["git", "status"]}}`
Note: request `id` can be an integer or a string.
Permissions approvals are different: they do not use string decisions. Reply with a structured
payload containing `"permissions"` and `"scope"` (`"turn"` or `"session"`). Denials are encoded as
an empty granted-permissions profile, not `"decline"`.
### Additional request families
Current upstream builds can also send these server requests:
- `mcpServer/elicitation/request`
- `item/permissions/requestApproval`
- `item/tool/call`
- `account/chatgptAuthTokens/refresh`
And these notifications for review lifecycle / request correlation:
- `item/autoApprovalReview/started`
- `item/autoApprovalReview/completed`
- `serverRequest/resolved`
The SDK's app-server streaming transport surfaces these as typed
`%Codex.Events.*{}` structs, so callers do not need to parse raw JSON-RPC
methods manually. In particular, `%Codex.Events.CommandApprovalRequested{}` now
preserves upstream command approval metadata such as `approval_id`,
`command_actions`, `network_approval_context`, `additional_permissions`,
`proposed_network_policy_amendments`, and `available_decisions`, while
`%Codex.Events.FileApprovalRequested{}` surfaces `grant_root` when the server
includes it. Use `Codex.AppServer.respond/3` with the corresponding protocol
payload maps.
### Headless auto-approval via `Codex.Approvals.Hook`
When running turns via `Codex.Thread.*`, you can auto-respond to app-server approvals using `approval_hook` on thread options.
Supported hook returns (backwards compatible):
- `:allow``"accept"`
- `{:allow, for_session: true}``"acceptForSession"`
- `{:allow, execpolicy_amendment: ["cmd", "arg"]}``"acceptWithExecpolicyAmendment"`
- `{:deny, reason}``"decline"`
Permissions approvals use `review_permissions/3` when implemented:
- `:allow` → grant the full requested profile for the current turn
- `{:allow, permissions: subset}` → grant the intersected subset for the current turn
- `{:allow, permissions: subset, scope: :session}` → grant the intersected subset for the session
- `{:deny, reason}` → respond with an empty granted-permissions profile and turn scope
To see live `item/permissions/requestApproval` requests from Codex itself, prefer a granular
approval policy with `request_permissions: true`; the legacy string policies are not enough to
reliably exercise that request path on newer builds. That path also requires the connection to be
initialized with `experimental_api: true`, and stock CLI installs still keep
`request_permissions_tool`, `exec_permission_approvals`, and `guardian_approval`
disabled by default.
See `examples/live_app_server_filesystem.exs` for a runnable `fs/*` walkthrough
and `examples/live_app_server_plugins.exs` for `plugin/list` + `plugin/read`.
Those live scripts probe the connected build first and self-skip when current
Codex binaries do not advertise the older parity methods. The plugin example
creates that disposable repo-local marketplace fixture first through
`Codex.Plugins.scaffold/1`, launches the child process with an isolated
temporary `CODEX_HOME`, and therefore does not need an existing plugin install, does not require a
prior Codex login, does not mutate your real `$CODEX_HOME`, and prints
`needsAuth` whenever the connected runtime includes that field.
`examples/live_app_server_approvals.exs` demonstrates command/file approvals, enables live
permissions approvals with granular `request_permissions: true`, launches the
child inside a disposable temp workspace plus temporary `CODEX_HOME`, enables
the under-development approval feature flags only in that isolated home, retries
without `experimentalApi` when the connected build rejects it, and prints a
deterministic structured-grant fallback when live permissions requests are still
unavailable.
The SDK accepts both `%{type: :granular, ...}` and `%{granular: %{...}}` for these approval
policies and now rejects malformed granular maps instead of silently omitting `approvalPolicy`.
MCP-qualified tool names shown to OpenAI are sanitized to ASCII alphanumerics plus `_` and `-`
before hash/truncation, while original MCP server/tool names are preserved for actual MCP calls.
## Turn diffs
On app-server, `turn/diff/updated` provides a **unified diff string**. The SDK surfaces it on `Codex.Events.TurnDiffUpdated.diff`.
## Skills
Skills require the experimental feature flag to be enabled in your codex config:
```toml
# ~/.codex/config.toml
[features]
skills = true
```
Current upstream skill scopes are `user`, `repo`, `system`, and `admin`.
### Skills caveat
App-server v2 input blocks support both `skill` and `mention`, so you can send
them directly via `thread/start` or `turn/start` payloads after discovering the
target with `skills/list`, `plugin/list`, or app metadata APIs.
## Sandbox Notes
Under `workspace-write` sandbox mode, both `.git/` and `.codex/` directories are automatically marked read-only to prevent privilege escalation.
## Troubleshooting
### `skills/list` returns `-32600` “unknown variant”
If you see a JSON-RPC error like:
- `code: -32600`
- `message: "Invalid request: unknown variant `skills/list` ..."`
your installed `codex app-server` is running a protocol version that does not implement `skills/list` yet. Upgrade the Codex CLI and retry.
## Working live examples
Runnable scripts (against a real `codex` install) live under `examples/`:
- `examples/live_app_server_basic.exs`
- `examples/live_app_server_streaming.exs`
- `examples/live_app_server_dynamic_tools.exs`
- `examples/live_app_server_approvals.exs`
- `examples/live_app_server_mcp.exs`
Run them with:
```bash
mix run examples/live_app_server_basic.exs
mix run examples/live_app_server_streaming.exs "Reply with exactly ok and nothing else."
mix run examples/live_app_server_dynamic_tools.exs
mix run examples/live_app_server_approvals.exs
mix run examples/live_app_server_mcp.exs
```