# Transports and proxy

> Choosing `--listen` URLs, unix control socket at `$CODEX_HOME/app-server-control`, `codex app-server proxy` byte bridging, websocket auth args, and remote-control pairing endpoints.

- Repository: openai/codex
- GitHub: https://github.com/openai/codex
- Human docs: https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1
- Complete Markdown: https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/llms-full.txt

## Source Files

- `README.md`
- `src/main.rs`
- `src/transport.rs`
- `src/lib.rs`
- `src/request_processors/remote_control_processor.rs`
- `tests/suite/v2/thread_name_websocket.rs`

---

---
title: "Transports and proxy"
description: "Choosing `--listen` URLs, unix control socket at `$CODEX_HOME/app-server-control`, `codex app-server proxy` byte bridging, websocket auth args, and remote-control pairing endpoints."
---

`codex-app-server` selects its wire transport from `--listen`, exposes a local Unix control socket under `$CODEX_HOME/app-server-control/`, optionally binds an experimental TCP WebSocket listener with health probes and auth, and can run a parallel remote-control WebSocket client that enrolls against ChatGPT backend APIs while still serving JSON-RPC over the same processor.

## Listen URL reference

The `codex-app-server` binary parses `--listen` into `AppServerTransport`. The default is `stdio://`.

| `--listen` value | Transport | Wire behavior |
| --- | --- | --- |
| `stdio://` (default) | `Stdio` | Newline-delimited JSON (JSONL) on stdin/stdout; single client; process exits when that connection closes |
| `unix://` | `UnixSocket` | WebSocket upgrade over `$CODEX_HOME/app-server-control/app-server-control.sock` |
| `unix://PATH` | `UnixSocket` | WebSocket upgrade over an absolute custom socket path |
| `ws://IP:PORT` | `WebSocket` | One JSON-RPC message per WebSocket text frame (**experimental / unsupported for production**) |
| `off` | `Off` | No local listener |

<Warning>
WebSocket transport (`ws://…`) is experimental and unsupported for production workloads. Do not rely on it until the project marks it stable.
</Warning>

If both the local listener and remote control are unavailable, startup fails with `no transport configured; use --listen or enable remote control`. Remote control alone is valid when SQLite state is available.

<ParamField body="listen" type="string" required={false}>
Transport endpoint URL. Default: `stdio://`.
</ParamField>

<ParamField body="session-source" type="string" required={false}>
Session source for product restrictions and metadata. Default: `vscode`.
</ParamField>

<ParamField body="remote-control" type="boolean" required={false}>
Hidden flag on `codex-app-server` that enables remote control at process start (`AppServerRuntimeOptions.remote_control_enabled`).
</ParamField>

## Unix control socket

When `--listen unix://` or `--listen unix://PATH` is used, the server:

1. Acquires `$CODEX_HOME/app-server-control/app-server-startup.lock` so only one Unix-socket listener binds at a time.
2. Prepares the socket path (private parent directory, stale-socket cleanup, `AddrInUse` if another live server holds the path).
3. Binds `app-server-control.sock` with mode `0600` on Unix.
4. Accepts raw Unix stream connections and upgrades each to WebSocket via the standard HTTP Upgrade handshake before handing frames to the shared JSON-RPC processor.

Default socket path (empty `unix://` suffix):

```text
$CODEX_HOME/app-server-control/app-server-control.sock
```

Custom paths must be absolute (`unix:///tmp/codex.sock`). Relative paths are resolved against the process current working directory at parse time.

```mermaid
flowchart LR
  subgraph client [Local client]
    CLI["codex app-server proxy"]
    IDE["Control-plane client"]
  end
  subgraph uds [Unix domain socket]
    SOCK["app-server-control.sock"]
  end
  subgraph server [codex-app-server]
    ACC["start_control_socket_acceptor"]
    WS["WebSocket upgrade"]
    PROC["MessageProcessor"]
  end
  CLI --> SOCK
  IDE --> SOCK
  SOCK --> ACC --> WS --> PROC
```

Multiple WebSocket clients can connect concurrently on the Unix listener (integration tests open two clients and both receive broadcasts after `initialize`).

## `codex app-server proxy`

The proxy subcommand is the supported way to attach a **stdio JSON-RPC client** to the **Unix control socket** without implementing WebSocket framing yourself.

<Steps>
<Step title="Start app-server on the control socket">
Run the server with a Unix listener, for example `codex app-server --listen unix://`.
</Step>
<Step title="Run the proxy in front of your client">
```bash
codex app-server proxy
# or
codex app-server proxy --sock /absolute/path/to.sock
```
</Step>
<Step title="Talk JSON-RPC on stdin/stdout">
Your client sends newline-delimited JSON-RPC on stdin; the proxy forwards bytes through the socket WebSocket session.
</Step>
</Steps>

Behavior (from product docs and CLI wiring):

- Opens **one** raw stream to the control socket (default `$CODEX_HOME/app-server-control/app-server-control.sock`, or `--sock PATH`).
- Proxies bytes **bidirectionally** between that socket and stdin/stdout.
- The proxied stream carries the WebSocket HTTP Upgrade handshake, then WebSocket frames—same as a native Unix WebSocket client.
- `codex app-server proxy` does not support `--strict-config` or remote-mode auth-token env overrides.

<Tip>
Use the proxy when your integration already speaks stdio JSONL (for example MCP-style subprocess clients) but the running server only exposes the control socket.
</Tip>

## TCP WebSocket listener

`--listen ws://IP:PORT` starts an Axum listener that:

- Serves `GET /readyz` → `200 OK` once accepting connections.
- Serves `GET /healthz` → `200 OK` when no `Origin` header is present.
- Rejects **any** request that includes an `Origin` header with `403 Forbidden` (including WebSocket upgrade attempts).
- Upgrades all other requests on the fallback route to WebSocket JSON-RPC.

On bind, stderr prints `ws://{addr}`, `http://{addr}/readyz`, and `http://{addr}/healthz`. Tests scrape the bound address from stderr after spawn.

WebSocket connections get a larger outbound queue (`32 * 1024` messages) than the internal `128`-slot transport channels. Slow WebSocket clients can be disconnected when their outbound queue fills.

### WebSocket authentication CLI

Non-loopback `ws://` listeners **refuse to start** without auth. Loopback listeners may run without auth; use SSH port forwarding for remote access.

<ParamField body="ws-auth" type="enum" required={false}>
`capability-token` or `signed-bearer-token`. Required when any other `ws-*` flag is set.
</ParamField>

**Capability token mode** (`--ws-auth capability-token`):

| Flag | Purpose |
| --- | --- |
| `--ws-token-file PATH` | Absolute path to the raw token file (trimmed contents) |
| `--ws-token-sha256 HEX` | 64-character hex SHA-256 of the token (mutually exclusive with `--ws-token-file`) |

Upgrade checks `Authorization: Bearer <token>` and compares SHA-256 in constant time.

**Signed bearer JWT mode** (`--ws-auth signed-bearer-token`):

| Flag | Purpose |
| --- | --- |
| `--ws-shared-secret-file PATH` | HS256 secret file (≥ 32 bytes) |
| `--ws-issuer ISSUER` | Optional expected `iss` claim |
| `--ws-audience AUDIENCE` | Optional expected `aud` claim (string or array) |
| `--ws-max-clock-skew-seconds SECONDS` | Clock skew for `exp`/`nbf` (default `30`) |

Example test invocation:

```bash
codex-app-server \
  --listen ws://127.0.0.1:0 \
  --ws-auth signed-bearer-token \
  --ws-shared-secret-file /path/to/secret \
  --ws-issuer codex-enroller \
  --ws-audience codex-app-server
```

Failed upgrade returns `401 Unauthorized` with a static message (missing bearer, invalid JWT, expired token, issuer/audience mismatch).

## Runtime transport wiring

`run_main_with_transport_options` in the app-server library starts acceptors based on `AppServerTransport`, then runs two cooperating tasks:

| Task | Role |
| --- | --- |
| Processor loop | Reads `TransportEvent`, dispatches JSON-RPC, tracks per-connection session state |
| Outbound router | Writes queued responses/notifications to per-connection writers; disconnects slow WebSocket peers when queues fill |

`ConnectionOrigin` values: `Stdio`, `WebSocket`, `RemoteControl`, `InProcess`.

Analytics RPC transport mapping: stdio → `Stdio`; unix socket, WebSocket, and `off` → `Websocket`.

### Backpressure and overload

Ingress uses bounded channels (`CHANNEL_CAPACITY = 128`). When the processor ingress queue is full, **new JSON-RPC requests** receive:

```json
{
  "error": {
    "code": -32001,
    "message": "Server overloaded; retry later."
  }
}
```

Treat `-32001` as retryable with exponential backoff and jitter. Responses and notifications wait on the queue instead of being dropped.

## Remote control

Remote control is an **experimental** parallel transport: a background task maintains a WebSocket to the ChatGPT remote-control API while remote controllers send JSON-RPC through that channel with `ConnectionOrigin::RemoteControl`.

### Prerequisites

| Requirement | Behavior if missing |
| --- | --- |
| SQLite state DB (`rollout_state_db`) | `remoteControl/enable` returns unavailable; startup with only `--remote-control` and no listener fails |
| Authenticated ChatGPT account | Enrollment and pairing need `AuthManager` credentials |
| `chatgpt_base_url` from config | Base URL normalized into enroll/refresh/pair/WebSocket targets |

Server name in status snapshots comes from the local hostname (`gethostname`).

### Client RPC surface (experimental)

All methods require `capabilities.experimentalApi` at `initialize` unless your build gates them differently.

| Method | Purpose |
| --- | --- |
| `remoteControl/enable` | Enable remote control; returns status snapshot (`connecting` when enrollment starts) |
| `remoteControl/disable` | Disable remote control; does **not** revoke enrolled controllers |
| `remoteControl/status/read` | Read current `status`, `serverName`, `installationId`, `environmentId` |
| `remoteControl/pairing/start` | Start short-lived pairing; optional `manualCode: true` |

Notification: `remoteControl/status/changed` — emitted on status or environment id changes; every newly initialized client receives the current snapshot.

`status` enum: `disabled`, `connecting`, `connected`, `errored`.

### Pairing request and response

<RequestExample>
```json
{
  "method": "remoteControl/pairing/start",
  "id": 3,
  "params": {
    "manualCode": true
  }
}
```
</RequestExample>

<ResponseExample>
```json
{
  "id": 3,
  "result": {
    "pairingCode": "pairing-code",
    "manualPairingCode": "ABCD-EFGH",
    "environmentId": "environment-id",
    "expiresAt": 33336362096
  }
}
```
</ResponseExample>

Notes:

- `expiresAt` is Unix seconds.
- `serverId` from the backend is **not** exposed to clients (tests assert `result.serverId` is absent).
- Pairing requires remote control enabled **and** completed enrollment; otherwise JSON-RPC `invalid_request` (for example `"remote control pairing is unavailable until enrollment completes"`).
- When remote control is unavailable on this process, handlers return internal error `"remote control is unavailable for this app-server"`.

### Backend HTTP endpoints

Given config base URL `https://chatgpt.com/backend-api/` (trailing slash normalized), the transport derives:

| Purpose | Path |
| --- | --- |
| Remote-control WebSocket | `wss://chatgpt.com/backend-api/wham/remote/control/server` |
| Enroll | `POST …/wham/remote/control/server/enroll` |
| Refresh token | `POST …/wham/remote/control/server/refresh` |
| Pair | `POST …/wham/remote/control/server/pair` |

Supported URL classes:

- HTTPS to `chatgpt.com` or `*.chatgpt-staging.com`
- HTTP/HTTPS to `localhost` (tests use `http://127.0.0.1:…/backend-api/`)

Enrollment posts machine metadata (`name`, `os`, `arch`, `app_server_version`, `installation_id`) and stores `environment_id`, `server_id`, and `remote_control_token` in SQLite for refresh and pairing.

```mermaid
sequenceDiagram
  participant Client as JSON-RPC client
  participant AS as codex-app-server
  participant API as ChatGPT backend-api
  Client->>AS: remoteControl/enable
  AS->>API: POST /wham/remote/control/server/enroll
  API-->>AS: environmentId, remote_control_token
  AS-->>Client: status connecting
  AS-->>Client: remoteControl/status/changed
  Client->>AS: remoteControl/pairing/start
  AS->>API: POST /wham/remote/control/server/pair
  API-->>AS: pairingCode, manualPairingCode
  AS-->>Client: pairingCode, environmentId, expiresAt
```

### Typical pairing workflow

<Steps>
<Step title="Enable remote control">
Call `remoteControl/enable` and wait for `remoteControl/status/changed` with a non-null `environmentId` when enrollment succeeds.
</Step>
<Step title="Start pairing">
Call `remoteControl/pairing/start` with `manualCode: true` when you need a human-entered code.
</Step>
<Step title="Present codes to the controller">
Share `pairingCode` and optional `manualPairingCode` before `expiresAt`.
</Step>
<Step title="Monitor connection">
Watch `remoteControl/status/changed` for `connected` or `errored`.
</Step>
</Steps>

Persist whether remote control should stay enabled outside app-server (README: caller responsibility). `remoteControl/disable` only stops the local feature; enrolled devices may remain valid server-side.

## Choosing a transport

| Use case | Recommended listen / access |
| --- | --- |
| Editor extension subprocess (VS Code) | `stdio://` or `codex app-server proxy` → Unix socket |
| Local daemon / multi-client UI | `unix://` on default or custom socket |
| Health-checked LAN listener (dev only) | `ws://127.0.0.1:PORT` + auth flags |
| Mobile / web remote controller | Enable remote control + pairing RPCs |
| Headless API generation only | `off` (if another transport or remote control is active) |

<Info>
Multi-client WebSocket on `unix://` supports broadcast notifications (for example `thread/name/updated`) to all initialized connections subscribed to the thread. Stdio remains strictly single-connection.
</Info>

## Related pages

<CardGroup>
<Card title="Protocol and transport" href="/protocol-and-transport">
JSON-RPC wire rules, overload `-32001`, health probes, and queue behavior in depth.
</Card>
<Card title="Installation" href="/installation">
Launching `codex-app-server`, default listen URLs, and stdio vs control-socket success signals.
</Card>
<Card title="CLI flags and errors" href="/cli-flags-and-errors">
Full `--listen` and websocket auth flag reference plus JSON-RPC error codes.
</Card>
<Card title="Connection lifecycle" href="/connection-lifecycle">
Per-connection `initialize`, notification opt-out, and server-initiated requests.
</Card>
<Card title="RPC methods reference" href="/rpc-methods">
Grouped v2 RPC catalog including experimental `remoteControl/*` methods.
</Card>
<Card title="Experimental API" href="/experimental-api">
Opt-in via `capabilities.experimentalApi` for remote control and other gated surface.
</Card>
</CardGroup>
