|
Download codex-rs/exec-server/README.md from SaylorTwift/codex: direct link, hf CLI and curl.
- Browser
- Download file 15.2 kB
-
https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/exec-server/README.md
- Command line
-
hf download hf://SaylorTwift/codex/codex-rs/exec-server/README.md
-
curl -L -o README.md https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/exec-server/README.md
15.2 kB
| # codex-exec-server | |
| `codex-exec-server` is the library backing `codex exec-server`, a small | |
| JSON-RPC server for spawning and controlling subprocesses through | |
| `codex-utils-pty`. | |
| It provides: | |
| - a CLI entrypoint: `codex exec-server` | |
| - a Rust client: `ExecServerClient` | |
| - a small protocol module with shared request/response types | |
| This crate owns the transport, protocol, and filesystem/process handlers. The | |
| top-level `codex` binary owns hidden helper dispatch for sandboxed | |
| filesystem operations and `codex-linux-sandbox`. | |
| ## Transport | |
| The server speaks the exec-specific `codex-exec-server-protocol` message | |
| envelope on the wire. | |
| The CLI entrypoint supports: | |
| - `ws://IP:PORT` (default) | |
| - `--remote URL --environment-id ID [--name NAME]` | |
| - `forward --connect ws://HOST:PORT --remote URL --environment-id ID` | |
| Remote mode registers the local exec-server with the environment registry, | |
| then reconnects to the service-provided rendezvous websocket as the environment. | |
| Remote communication uses the Noise relay contract; the registry and harness | |
| must support it. | |
| Forward mode uses the same registration and Noise relay, but opens an independent | |
| WebSocket connection to the destination exec-server for each authenticated | |
| harness stream. Complete message payloads pass unchanged in both directions; | |
| the forwarder does not parse RPCs, initialize sessions, or execute requests. | |
| The destination owns session IDs, processes, and session resumption. | |
| Disconnecting either side closes its peer and resets the remote stream. The | |
| existing harness reconnect flow can then resume a retained destination session. | |
| The forwarder does not replay requests or persist execution state, so recovery | |
| is limited by the destination's session and process-output retention. | |
| It uses the standard Codex ChatGPT sign-in state; run `codex login` first when | |
| remote registration needs authentication. Containerized callers that receive an | |
| Agent Identity JWT in `CODEX_ACCESS_TOKEN` can opt into that auth path with | |
| `--use-agent-identity-auth`; Codex then registers an Agent task and sends the | |
| derived AgentAssertion headers on the registry request. | |
| Alternatively, API users can instead use `CODEX_API_KEY`; | |
| Codex sends it as a bearer token on the registration request. For example: | |
| ```sh | |
| CODEX_API_KEY="$OPENAI_API_KEY" \ | |
| codex exec-server \ | |
| --remote ... \ | |
| --environment-id "$ENVIRONMENT_ID" | |
| ``` | |
| AWS-hosted registries can use SigV4 for registry requests and the executor | |
| WebSocket handshake. Select the transport and authentication with executor | |
| arguments rather than `config.toml` settings: | |
| ```sh | |
| codex exec-server \ | |
| --remote https://example.com \ | |
| --environment-id "$ENVIRONMENT_ID" \ | |
| --remote-transport direct \ | |
| --aws-sigv4 \ | |
| --aws-profile development \ | |
| --aws-region us-west-2 \ | |
| --aws-service bedrock-mantle | |
| ``` | |
| Noise remains the default transport. Direct requires `--aws-sigv4`, which | |
| conflicts with `--use-agent-identity-auth` and is not supported for Noise. | |
| The AWS options require `--aws-sigv4`; Direct forwarding remains unsupported. | |
| The AWS SDK default credential and region chains are used when `--aws-profile` | |
| or `--aws-region` is omitted. The signing service defaults to `execute-api`. | |
| Direct mode registers `direct_jsonrpc_v1` through the AWS-owned | |
| `/cloud/environment/{environment_id}/direct/register` endpoint and carries plain | |
| exec-server JSON-RPC over the authenticated WebSocket. The existing Codex Noise | |
| registration endpoint remains unchanged. Production deployments must use TLS | |
| (`https`/`wss`). | |
| Direct registration URLs must remain reusable across disconnects and temporary | |
| connection failures. The executor only refreshes its registration when the | |
| WebSocket handshake returns `409 Conflict`. Handshake `408`, `429`, and `5xx` | |
| responses retry with backoff using the current registration; other `4xx` responses | |
| stop the executor. A backend that issues single-use connection URLs must adapt to | |
| this contract. If the initial registration or a registration refresh fails, the | |
| executor returns the error without retrying registration, matching Noise. | |
| Wire framing: | |
| - local websocket: one JSON-RPC message per websocket message | |
| - direct remote websocket: one JSON-RPC message per websocket message | |
| - Noise remote websocket: binary protobuf relay frames carrying encrypted payloads | |
| ## Remote Relay Message Format | |
| In remote mode, the harness and environment communicate through rendezvous using | |
| `codex.exec_server.relay.v1.RelayMessageFrame`; the checked-in schema is in | |
| `src/proto/codex.exec_server.relay.v1.proto`. The relay frame carries stream | |
| identity plus endpoint-owned reliability metadata: | |
| ```text | |
| version | |
| stream_id | |
| traceparent // optional W3C parent on the first frame of a traced request | |
| tracestate // optional W3C vendor state paired with traceparent | |
| body // handshake | data | ack_frame | resume | reset | heartbeat | |
| ack // highest contiguous peer segment seq received | |
| ack_bits // bitset for peer segment seqs after ack | |
| seq // data only: segment sequence number | |
| segment_index // data only: 0-based index within message | |
| segment_count // data only: number of segments in message | |
| payload // handshake bytes or encrypted data record | |
| next_seq // resume only: next sender seq | |
| reason // reset only: reset reason | |
| ``` | |
| `stream_id` identifies one virtual harness/environment JSON-RPC session on the | |
| environment websocket. The harness generates a UUIDv4 `stream_id`; the environment | |
| demuxes frames by `stream_id` and runs an independent `ConnectionProcessor` per | |
| stream. | |
| Use segment-level sequence numbers for reliability: | |
| ```text | |
| seq = 0, 1, 2, 3, ... | |
| ``` | |
| Use contiguous segment sequence ranges to identify and stitch a segmented | |
| application message: | |
| ```text | |
| message_start_seq = seq - segment_index | |
| segment_index = 0 | |
| segment_count = 1 | |
| ``` | |
| `message_start_seq` is derived by the receiver, not sent on the wire. For | |
| unsplit messages, `message_start_seq == seq`, `segment_index == 0`, and | |
| `segment_count == 1`. | |
| Use cumulative `ack` plus fixed-size `ack_bits` instead of variable ack ranges: | |
| ```text | |
| ack = highest contiguous received segment seq | |
| bit i in ack_bits acknowledges seq = ack + 1 + i | |
| ``` | |
| Send `ack` and `ack_bits` redundantly on every outbound frame. Acks are not | |
| themselves acked. Acks, retries, duplicate suppression, segmentation, and | |
| reassembly are endpoint responsibilities; rendezvous only routes relay frames | |
| by `stream_id`. | |
| ## Lifecycle | |
| Each connection follows this sequence: | |
| 1. Send `initialize`. | |
| 2. Wait for the `initialize` response. | |
| 3. Send `initialized`. | |
| 4. Call process or filesystem RPCs. | |
| Requests run sequentially by default. Pass `--concurrent-requests <COUNT>` to | |
| enable concurrent processing. | |
| If the server receives any notification other than `initialized`, it replies | |
| with an error using request id `-1`. | |
| If the websocket connection closes, the server terminates any remaining managed | |
| processes for that client connection. | |
| ## API | |
| ### `initialize` | |
| Initial handshake request. | |
| Request params: | |
| ```json | |
| { | |
| "clientName": "my-client" | |
| } | |
| ``` | |
| Response: | |
| ```json | |
| { | |
| "sessionId": "00000000-0000-4000-8000-000000000001", | |
| "environmentInfo": { | |
| "shell": { "name": "bash", "path": "/bin/bash" }, | |
| "executorVersion": "1.2.3-alpha.4", | |
| "providerId": "sha256:fb4f62da3e84f6864dcec8ede7bc66f1c96ecaeaf55f8a786b85df994057c8ac", | |
| "cwd": "file:///workspace" | |
| } | |
| } | |
| ``` | |
| `environmentInfo` contains the same executor metadata returned by | |
| `environment/info`, so clients can use it without a second request. | |
| `executorVersion` is the executor's package release version, or `0.0.0` when unknown. | |
| The executor caches optional `providerId` at startup using | |
| `codex_build_info::build_id(commit, target)`, which CI can also call for an | |
| explicit build target. This opaque compatibility key excludes package version | |
| and requires no manifest. It identifies a standard build configuration, not exact | |
| executable bytes. Unstamped and legacy executors may omit it. | |
| Rust clients cache this metadata for the client's lifetime, including session | |
| resumption. If initialization omits it, the first metadata request fetches and | |
| caches `environment/info`. | |
| ### `initialized` | |
| Handshake acknowledgement notification sent by the client after a successful | |
| `initialize` response. | |
| Params are currently ignored. Sending any other notification method is treated | |
| as an invalid request. | |
| ### `process/start` | |
| Starts a new managed process. | |
| Request params: | |
| ```json | |
| { | |
| "processId": "proc-1", | |
| "argv": ["bash", "-lc", "printf 'hello\\n'"], | |
| "cwd": "file:///absolute/working/directory", | |
| "env": { | |
| "PATH": "/usr/bin:/bin" | |
| }, | |
| "tty": true, | |
| "pipeStdin": false, | |
| "arg0": null | |
| } | |
| ``` | |
| Field definitions: | |
| - `processId`: caller-chosen stable id for this process within the connection. | |
| - `argv`: command vector. It must be non-empty. | |
| - `cwd`: `file:` URI for the child process working directory. | |
| - `env`: environment variables passed to the child process. | |
| - `tty`: when `true`, spawn a PTY-backed interactive process. | |
| - `pipeStdin`: when `true`, keep non-PTY stdin writable via `process/write`. | |
| - `arg0`: optional argv0 override forwarded to `codex-utils-pty`. | |
| Response: | |
| ```json | |
| { | |
| "processId": "proc-1" | |
| } | |
| ``` | |
| Behavior notes: | |
| - Reusing an existing `processId` is rejected. | |
| - PTY-backed processes accept later writes through `process/write`. | |
| - Non-PTY processes reject writes unless `pipeStdin` is `true`. | |
| - Output is streamed asynchronously via `process/output`. | |
| - Exit is reported asynchronously via `process/exited`. | |
| ### `process/read` | |
| Reads buffered output and terminal state for a managed process. | |
| Request params: | |
| ```json | |
| { | |
| "processId": "proc-1", | |
| "afterSeq": null, | |
| "maxBytes": 65536, | |
| "waitMs": 1000 | |
| } | |
| ``` | |
| Field definitions: | |
| - `processId`: managed process id returned by `process/start`. | |
| - `afterSeq`: optional sequence number cursor; when present, only newer chunks | |
| are returned. | |
| - `maxBytes`: optional response byte budget. | |
| - `waitMs`: optional long-poll timeout in milliseconds. | |
| Response: | |
| ```json | |
| { | |
| "chunks": [], | |
| "nextSeq": 1, | |
| "exited": false, | |
| "exitCode": null, | |
| "closed": false, | |
| "failure": null | |
| } | |
| ``` | |
| ### `process/write` | |
| Writes raw bytes to a running process stdin. | |
| Request params: | |
| ```json | |
| { | |
| "processId": "proc-1", | |
| "chunk": "aGVsbG8K" | |
| } | |
| ``` | |
| `chunk` is base64-encoded raw bytes. In the example above it is `hello\n`. | |
| Response: | |
| ```json | |
| { | |
| "status": "accepted" | |
| } | |
| ``` | |
| Behavior notes: | |
| - Writes to an unknown `processId` are rejected. | |
| - Writes to a non-PTY process are rejected unless it started with `pipeStdin`. | |
| ### `process/terminate` | |
| Terminates a running managed process. | |
| Request params: | |
| ```json | |
| { | |
| "processId": "proc-1" | |
| } | |
| ``` | |
| Response: | |
| ```json | |
| { | |
| "running": true | |
| } | |
| ``` | |
| If the process is already unknown or already removed, the server responds with: | |
| ```json | |
| { | |
| "running": false | |
| } | |
| ``` | |
| ## Notifications | |
| ### `process/output` | |
| Streaming output chunk from a running process. | |
| Params: | |
| ```json | |
| { | |
| "processId": "proc-1", | |
| "seq": 1, | |
| "stream": "stdout", | |
| "chunk": "aGVsbG8K" | |
| } | |
| ``` | |
| Fields: | |
| - `processId`: process identifier | |
| - `seq`: per-process output sequence number | |
| - `stream`: `"stdout"`, `"stderr"`, or `"pty"` | |
| - `chunk`: base64-encoded output bytes | |
| ### `process/exited` | |
| Final process exit notification. | |
| Params: | |
| ```json | |
| { | |
| "processId": "proc-1", | |
| "seq": 2, | |
| "exitCode": 0, | |
| "sandboxDenied": false | |
| } | |
| ``` | |
| `sandboxDenied` lets streaming clients preserve executor-side sandbox denial | |
| detection without issuing a final `process/read` request. Clients recover it | |
| with `process/read` when an older server omits the field. | |
| ### `process/closed` | |
| Notification emitted after process output is closed and the process handle is | |
| removed. | |
| Params: | |
| ```json | |
| { | |
| "processId": "proc-1", | |
| "seq": 3 | |
| } | |
| ``` | |
| ## Filesystem RPCs | |
| Filesystem methods require valid `file:` URI strings and return JSON-RPC errors | |
| for invalid or unavailable paths. Native absolute path strings are rejected; | |
| callers must convert them to `file:` URIs before sending requests: | |
| - `fs/readFile` | |
| - `fs/open`, `fs/readBlock`, and `fs/close` (internal transport for | |
| `ExecutorFileSystem::read_file_stream`) | |
| - `fs/writeFile` | |
| - `fs/createDirectory` | |
| - `fs/getMetadata` | |
| - `fs/canonicalize` | |
| - `fs/readDirectory` | |
| - `fs/remove` | |
| - `fs/copy` | |
| Each filesystem request accepts an optional `sandbox` object. When `sandbox` | |
| contains a `ReadOnly` or `WorkspaceWrite` policy, the operation runs in a | |
| hidden helper process launched from the top-level `codex` executable and | |
| prepared through the shared sandbox transform path. Helper requests and | |
| responses are passed over stdin/stdout. | |
| ## Errors | |
| The server returns JSON-RPC errors with these codes: | |
| - `-32600`: invalid request | |
| - `-32602`: invalid params | |
| - `-32603`: internal error | |
| Typical error cases: | |
| - unknown method | |
| - malformed params | |
| - empty `argv` | |
| - duplicate `processId` | |
| - writes to unknown processes | |
| - writes to non-PTY processes | |
| - sandbox-denied filesystem operations | |
| ## Rust surface | |
| The crate exports: | |
| - `ExecServerClient` | |
| - `ExecServerError` | |
| - `ExecServerClientConnectOptions` | |
| - `RemoteExecServerConnectArgs` | |
| - protocol request/response structs for process and filesystem RPCs | |
| - `DEFAULT_LISTEN_URL` and `ExecServerListenUrlParseError` | |
| - `ExecServerRuntimePaths` | |
| - `run_main()` for embedding the websocket server | |
| - `RemoteEnvironmentConfig` and `run_remote_environment()` for embedding remote | |
| registration mode | |
| Callers must pass `ExecServerRuntimePaths` and an explicitly configured | |
| `HttpClientFactory` to `run_main()`. The top-level `codex exec-server` command | |
| builds these paths from the `codex` arg0 dispatch state and resolves its HTTP | |
| client factory from the effective Codex configuration. | |
| `RemoteEnvironmentConfig::new(...)` also takes the auth provider and HTTP client | |
| factory that remote registration mode should use; the CLI builds the auth | |
| provider from Codex auth state before starting remote mode. | |
| ## Example session | |
| Initialize: | |
| ```json | |
| {"id":1,"method":"initialize","params":{"clientName":"example-client"}} | |
| {"id":1,"result":{"sessionId":"00000000-0000-4000-8000-000000000001","environmentInfo":{"shell":{"name":"bash","path":"/bin/bash"},"cwd":"file:///tmp"}}} | |
| {"method":"initialized","params":{}} | |
| ``` | |
| Start a process: | |
| ```json | |
| {"id":2,"method":"process/start","params":{"processId":"proc-1","argv":["bash","-lc","printf 'ready\\n'; while IFS= read -r line; do printf 'echo:%s\\n' \"$line\"; done"],"cwd":"file:///tmp","env":{"PATH":"/usr/bin:/bin"},"tty":true,"pipeStdin":false,"arg0":null}} | |
| {"id":2,"result":{"processId":"proc-1"}} | |
| {"method":"process/output","params":{"processId":"proc-1","seq":1,"stream":"stdout","chunk":"cmVhZHkK"}} | |
| ``` | |
| Write to the process: | |
| ```json | |
| {"id":3,"method":"process/write","params":{"processId":"proc-1","chunk":"aGVsbG8K"}} | |
| {"id":3,"result":{"status":"accepted"}} | |
| {"method":"process/output","params":{"processId":"proc-1","seq":2,"stream":"stdout","chunk":"ZWNobzpoZWxsbwo="}} | |
| ``` | |
| Terminate it: | |
| ```json | |
| {"id":4,"method":"process/terminate","params":{"processId":"proc-1"}} | |
| {"id":4,"result":{"running":true}} | |
| {"method":"process/exited","params":{"processId":"proc-1","seq":3,"exitCode":0,"sandboxDenied":false}} | |
| {"method":"process/closed","params":{"processId":"proc-1","seq":4}} | |
| ``` | |