mirror of
https://github.com/openai/codex.git
synced 2026-05-02 12:21:26 +03:00
Run exec-server fs operations through sandbox helper (#17294)
## Summary - run exec-server filesystem RPCs requiring sandboxing through a `codex-fs` arg0 helper over stdin/stdout - keep direct local filesystem execution for `DangerFullAccess` and external sandbox policies - remove the standalone exec-server binary path in favor of top-level arg0 dispatch/runtime paths - add sandbox escape regression coverage for local and remote filesystem paths ## Validation - `just fmt` - `git diff --check` - remote devbox: `cd codex-rs && bazel test --bes_backend= --bes_results_url= //codex-rs/exec-server:all` (6/6 passed) --------- Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -1,27 +1,25 @@
|
||||
# codex-exec-server
|
||||
|
||||
`codex-exec-server` is a small standalone JSON-RPC server for spawning
|
||||
and controlling subprocesses through `codex-utils-pty`.
|
||||
`codex-exec-server` is the library backing `codex exec-server`, a small
|
||||
JSON-RPC server for spawning and controlling subprocesses through
|
||||
`codex-utils-pty`.
|
||||
|
||||
This PR intentionally lands only the standalone binary, client, wire protocol,
|
||||
and docs. Exec and filesystem methods are stubbed server-side here and are
|
||||
implemented in follow-up PRs.
|
||||
It provides:
|
||||
|
||||
It currently provides:
|
||||
|
||||
- a standalone binary: `codex-exec-server`
|
||||
- a CLI entrypoint: `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.
|
||||
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 shared `codex-app-server-protocol` message envelope on
|
||||
the wire.
|
||||
|
||||
The standalone binary supports:
|
||||
The CLI entrypoint supports:
|
||||
|
||||
- `ws://IP:PORT` (default)
|
||||
|
||||
@@ -36,7 +34,7 @@ Each connection follows this sequence:
|
||||
1. Send `initialize`.
|
||||
2. Wait for the `initialize` response.
|
||||
3. Send `initialized`.
|
||||
4. Call exec or filesystem RPCs once the follow-up implementation PRs land.
|
||||
4. Call process or filesystem RPCs.
|
||||
|
||||
If the server receives any notification other than `initialized`, it replies
|
||||
with an error using request id `-1`.
|
||||
@@ -72,7 +70,7 @@ Handshake acknowledgement notification sent by the client after a successful
|
||||
Params are currently ignored. Sending any other notification method is treated
|
||||
as an invalid request.
|
||||
|
||||
### `command/exec`
|
||||
### `process/start`
|
||||
|
||||
Starts a new managed process.
|
||||
|
||||
@@ -87,7 +85,6 @@ Request params:
|
||||
"PATH": "/usr/bin:/bin"
|
||||
},
|
||||
"tty": true,
|
||||
"outputBytesCap": 16384,
|
||||
"arg0": null
|
||||
}
|
||||
```
|
||||
@@ -100,31 +97,61 @@ Field definitions:
|
||||
- `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
|
||||
"processId": "proc-1"
|
||||
}
|
||||
```
|
||||
|
||||
Behavior notes:
|
||||
|
||||
- Reusing an existing `processId` is rejected.
|
||||
- PTY-backed processes accept later writes through `command/exec/write`.
|
||||
- PTY-backed processes accept later writes through `process/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`.
|
||||
- Output is streamed asynchronously via `process/output`.
|
||||
- Exit is reported asynchronously via `process/exited`.
|
||||
|
||||
### `command/exec/write`
|
||||
### `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 PTY-backed process stdin.
|
||||
|
||||
@@ -143,7 +170,7 @@ Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"accepted": true
|
||||
"status": "accepted"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -152,7 +179,7 @@ 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`
|
||||
### `process/terminate`
|
||||
|
||||
Terminates a running managed process.
|
||||
|
||||
@@ -182,7 +209,7 @@ If the process is already unknown or already removed, the server responds with:
|
||||
|
||||
## Notifications
|
||||
|
||||
### `command/exec/outputDelta`
|
||||
### `process/output`
|
||||
|
||||
Streaming output chunk from a running process.
|
||||
|
||||
@@ -191,6 +218,7 @@ Params:
|
||||
```json
|
||||
{
|
||||
"processId": "proc-1",
|
||||
"seq": 1,
|
||||
"stream": "stdout",
|
||||
"chunk": "aGVsbG8K"
|
||||
}
|
||||
@@ -199,10 +227,11 @@ Params:
|
||||
Fields:
|
||||
|
||||
- `processId`: process identifier
|
||||
- `stream`: `"stdout"` or `"stderr"`
|
||||
- `seq`: per-process output sequence number
|
||||
- `stream`: `"stdout"`, `"stderr"`, or `"pty"`
|
||||
- `chunk`: base64-encoded output bytes
|
||||
|
||||
### `command/exec/exited`
|
||||
### `process/exited`
|
||||
|
||||
Final process exit notification.
|
||||
|
||||
@@ -211,10 +240,43 @@ Params:
|
||||
```json
|
||||
{
|
||||
"processId": "proc-1",
|
||||
"seq": 2,
|
||||
"exitCode": 0
|
||||
}
|
||||
```
|
||||
|
||||
### `process/closed`
|
||||
|
||||
Notification emitted after process output is closed and the process handle is
|
||||
removed.
|
||||
|
||||
Params:
|
||||
|
||||
```json
|
||||
{
|
||||
"processId": "proc-1"
|
||||
}
|
||||
```
|
||||
|
||||
## Filesystem RPCs
|
||||
|
||||
Filesystem methods use absolute paths and return JSON-RPC errors for invalid
|
||||
or unavailable paths:
|
||||
|
||||
- `fs/readFile`
|
||||
- `fs/writeFile`
|
||||
- `fs/createDirectory`
|
||||
- `fs/getMetadata`
|
||||
- `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:
|
||||
@@ -231,6 +293,7 @@ Typical error cases:
|
||||
- duplicate `processId`
|
||||
- writes to unknown processes
|
||||
- writes to non-PTY processes
|
||||
- sandbox-denied filesystem operations
|
||||
|
||||
## Rust surface
|
||||
|
||||
@@ -240,10 +303,14 @@ The crate exports:
|
||||
- `ExecServerError`
|
||||
- `ExecServerClientConnectOptions`
|
||||
- `RemoteExecServerConnectArgs`
|
||||
- protocol structs `InitializeParams` and `InitializeResponse`
|
||||
- protocol request/response structs for process and filesystem RPCs
|
||||
- `DEFAULT_LISTEN_URL` and `ExecServerListenUrlParseError`
|
||||
- `run_main_with_listen_url()`
|
||||
- `run_main()` for embedding the websocket server in a binary
|
||||
- `ExecServerRuntimePaths`
|
||||
- `run_main()` for embedding the websocket server
|
||||
|
||||
Callers must pass `ExecServerRuntimePaths` to `run_main()`. The top-level
|
||||
`codex exec-server` command builds these paths from the `codex` arg0 dispatch
|
||||
state.
|
||||
|
||||
## Example session
|
||||
|
||||
@@ -258,23 +325,24 @@ Initialize:
|
||||
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"}}
|
||||
{"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":"/tmp","env":{"PATH":"/usr/bin:/bin"},"tty":true,"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":"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="}}
|
||||
{"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":"command/exec/terminate","params":{"processId":"proc-1"}}
|
||||
{"id":4,"method":"process/terminate","params":{"processId":"proc-1"}}
|
||||
{"id":4,"result":{"running":true}}
|
||||
{"method":"command/exec/exited","params":{"processId":"proc-1","exitCode":0}}
|
||||
{"method":"process/exited","params":{"processId":"proc-1","seq":3,"exitCode":0}}
|
||||
{"method":"process/closed","params":{"processId":"proc-1"}}
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user