diff --git a/codex-rs/exec-server/README.md b/codex-rs/exec-server/README.md new file mode 100644 index 0000000000..8aeb3a73ec --- /dev/null +++ b/codex-rs/exec-server/README.md @@ -0,0 +1,285 @@ +# codex-exec-server + +`codex-exec-server` is a small standalone stdio JSON-RPC server for spawning +and controlling subprocesses through `codex-utils-pty`. + +It currently provides: + +- a standalone binary: `codex-exec-server` +- a Rust client: `ExecServerClient` +- a small protocol module with shared request/response types + +This crate is intentionally narrow. It is not wired into the main Codex CLI or +unified-exec in this PR; it is only the standalone transport layer. + +## Transport + +The server speaks newline-delimited JSON-RPC 2.0 over stdio. + +- `stdin`: one JSON-RPC message per line +- `stdout`: one JSON-RPC message per line +- `stderr`: reserved for logs / process errors + +Like the app-server transport, messages on the wire omit the `"jsonrpc":"2.0"` +field and use the shared `codex-app-server-protocol` envelope types. + +The current protocol version is: + +```text +exec-server.v0 +``` + +## Lifecycle + +Each connection follows this sequence: + +1. Send `initialize`. +2. Wait for the `initialize` response. +3. Send `initialized`. +4. Start and manage processes with `command/exec`, `command/exec/write`, and + `command/exec/terminate`. +5. Read streaming notifications from `command/exec/outputDelta` and + `command/exec/exited`. + +If the server receives any notification other than `initialized`, it replies +with an error using request id `-1`. + +If the stdio connection closes, the server terminates any remaining managed +processes before exiting. + +## API + +### `initialize` + +Initial handshake request. + +Request params: + +```json +{ + "clientName": "my-client" +} +``` + +Response: + +```json +{ + "protocolVersion": "exec-server.v0" +} +``` + +### `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. + +### `command/exec` + +Starts a new managed process. + +Request params: + +```json +{ + "processId": "proc-1", + "argv": ["bash", "-lc", "printf 'hello\\n'"], + "cwd": "/absolute/working/directory", + "env": { + "PATH": "/usr/bin:/bin" + }, + "tty": true, + "outputBytesCap": 16384, + "arg0": null +} +``` + +Field definitions: + +- `processId`: caller-chosen stable id for this process within the connection. +- `argv`: command vector. It must be non-empty. +- `cwd`: absolute working directory used for the child process. +- `env`: environment variables passed to the child process. +- `tty`: when `true`, spawn a PTY-backed interactive process; when `false`, + spawn a pipe-backed process with closed stdin. +- `outputBytesCap`: maximum retained stdout/stderr bytes per stream for the + in-memory buffer. Defaults to `codex_utils_pty::DEFAULT_OUTPUT_BYTES_CAP`. +- `arg0`: optional argv0 override forwarded to `codex-utils-pty`. + +Response: + +```json +{ + "processId": "proc-1", + "running": true, + "exitCode": null, + "stdout": null, + "stderr": null +} +``` + +Behavior notes: + +- Reusing an existing `processId` is rejected. +- PTY-backed processes accept later writes through `command/exec/write`. +- Pipe-backed processes are launched with stdin closed and reject writes. +- Output is streamed asynchronously via `command/exec/outputDelta`. +- Exit is reported asynchronously via `command/exec/exited`. + +### `command/exec/write` + +Writes raw bytes to a running PTY-backed 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 +{ + "accepted": true +} +``` + +Behavior notes: + +- Writes to an unknown `processId` are rejected. +- Writes to a non-PTY process are rejected because stdin is already closed. + +### `command/exec/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 + +### `command/exec/outputDelta` + +Streaming output chunk from a running process. + +Params: + +```json +{ + "processId": "proc-1", + "stream": "stdout", + "chunk": "aGVsbG8K" +} +``` + +Fields: + +- `processId`: process identifier +- `stream`: `"stdout"` or `"stderr"` +- `chunk`: base64-encoded output bytes + +### `command/exec/exited` + +Final process exit notification. + +Params: + +```json +{ + "processId": "proc-1", + "exitCode": 0 +} +``` + +## 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 + +## Rust surface + +The crate exports: + +- `ExecServerClient` +- `ExecServerLaunchCommand` +- `ExecServerProcess` +- `ExecServerError` +- protocol structs such as `ExecParams`, `ExecResponse`, + `WriteParams`, `TerminateParams`, `ExecOutputDeltaNotification`, and + `ExecExitedNotification` +- `run_main()` for embedding the stdio server in a binary + +## Example session + +Initialize: + +```json +{"id":1,"method":"initialize","params":{"clientName":"example-client"}} +{"id":1,"result":{"protocolVersion":"exec-server.v0"}} +{"method":"initialized","params":{}} +``` + +Start a process: + +```json +{"id":2,"method":"command/exec","params":{"processId":"proc-1","argv":["bash","-lc","printf 'ready\\n'; while IFS= read -r line; do printf 'echo:%s\\n' \"$line\"; done"],"cwd":"/tmp","env":{"PATH":"/usr/bin:/bin"},"tty":true,"outputBytesCap":4096,"arg0":null}} +{"id":2,"result":{"processId":"proc-1","running":true,"exitCode":null,"stdout":null,"stderr":null}} +{"method":"command/exec/outputDelta","params":{"processId":"proc-1","stream":"stdout","chunk":"cmVhZHkK"}} +``` + +Write to the process: + +```json +{"id":3,"method":"command/exec/write","params":{"processId":"proc-1","chunk":"aGVsbG8K"}} +{"id":3,"result":{"accepted":true}} +{"method":"command/exec/outputDelta","params":{"processId":"proc-1","stream":"stdout","chunk":"ZWNobzpoZWxsbwo="}} +``` + +Terminate it: + +```json +{"id":4,"method":"command/exec/terminate","params":{"processId":"proc-1"}} +{"id":4,"result":{"running":true}} +{"method":"command/exec/exited","params":{"processId":"proc-1","exitCode":0}} +```