Files
codex/codex-rs/exec-server
Michael Bolin 789f387982 permissions: remove legacy read-only access modes (#19449)
## Why

`ReadOnlyAccess` was a transitional legacy shape on `SandboxPolicy`:
`FullAccess` meant the historical read-only/workspace-write modes could
read the full filesystem, while `Restricted` tried to carry partial
readable roots. The partial-read model now belongs in
`FileSystemSandboxPolicy` and `PermissionProfile`, so keeping it on
`SandboxPolicy` makes every legacy projection reintroduce lossy
read-root bookkeeping and creates unnecessary noise in the rest of the
permissions migration.

This PR makes the legacy policy model narrower and explicit:
`SandboxPolicy::ReadOnly` and `SandboxPolicy::WorkspaceWrite` represent
the old full-read sandbox modes only. Split readable roots, deny-read
globs, and platform-default/minimal read behavior stay in the runtime
permissions model.

## What changed

- Removes `ReadOnlyAccess` from
`codex_protocol::protocol::SandboxPolicy`, including the generated
`access` and `readOnlyAccess` API fields.
- Updates legacy policy/profile conversions so restricted filesystem
reads are represented only by `FileSystemSandboxPolicy` /
`PermissionProfile` entries.
- Keeps app-server v2 compatible with legacy `fullAccess` read-access
payloads by accepting and ignoring that no-op shape, while rejecting
legacy `restricted` read-access payloads instead of silently widening
them to full-read legacy policies.
- Carries Windows sandbox platform-default read behavior with an
explicit override flag instead of depending on
`ReadOnlyAccess::Restricted`.
- Refreshes generated app-server schema/types and updates tests/docs for
the simplified legacy policy shape.

## Verification

- `cargo check -p codex-app-server-protocol --tests`
- `cargo check -p codex-windows-sandbox --tests`
- `cargo test -p codex-app-server-protocol sandbox_policy_`


---
[//]: # (BEGIN SAPLING FOOTER)
Stack created with [Sapling](https://sapling-scm.com). Best reviewed
with [ReviewStack](https://reviewstack.dev/openai/codex/pull/19449).
* #19395
* #19394
* #19393
* #19392
* #19391
* __->__ #19449
2026-04-24 17:16:58 -07:00
..

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 shared codex-app-server-protocol message envelope on the wire.

The CLI entrypoint supports:

  • ws://IP:PORT (default)

Wire framing:

  • websocket: one JSON-RPC message per websocket text frame

Lifecycle

Each connection follows this sequence:

  1. Send initialize.
  2. Wait for the initialize response.
  3. Send initialized.
  4. Call process or filesystem RPCs.

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:

{
  "clientName": "my-client"
}

Response:

{}

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:

{
  "processId": "proc-1",
  "argv": ["bash", "-lc", "printf 'hello\\n'"],
  "cwd": "/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: 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.
  • pipeStdin: when true, keep non-PTY stdin writable via process/write.
  • arg0: optional argv0 override forwarded to codex-utils-pty.

Response:

{
  "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:

{
  "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:

{
  "chunks": [],
  "nextSeq": 1,
  "exited": false,
  "exitCode": null,
  "closed": false,
  "failure": null
}

process/write

Writes raw bytes to a running process stdin.

Request params:

{
  "processId": "proc-1",
  "chunk": "aGVsbG8K"
}

chunk is base64-encoded raw bytes. In the example above it is hello\n.

Response:

{
  "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:

{
  "processId": "proc-1"
}

Response:

{
  "running": true
}

If the process is already unknown or already removed, the server responds with:

{
  "running": false
}

Notifications

process/output

Streaming output chunk from a running process.

Params:

{
  "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:

{
  "processId": "proc-1",
  "seq": 2,
  "exitCode": 0
}

process/closed

Notification emitted after process output is closed and the process handle is removed.

Params:

{
  "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:

  • -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

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

Initialize:

{"id":1,"method":"initialize","params":{"clientName":"example-client"}}
{"id":1,"result":{}}
{"method":"initialized","params":{}}

Start a process:

{"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,"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:

{"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:

{"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}}
{"method":"process/closed","params":{"processId":"proc-1"}}