Files
codex/codex-rs/exec-server
Michael Bolin 4b55979755 permissions: remove cwd special path (#19841)
## Why

The experimental `PermissionProfile` API had both `:cwd` and
`:project_roots` special filesystem paths, which made the permission
root ambiguous. This PR removes the unstable `current_working_directory`
special path before the permissions API is stabilized, so callers use
`:project_roots` for symbolic project-root access.

## What changed

- Removes `FileSystemSpecialPath::CurrentWorkingDirectory` from protocol
and app-server protocol models, plus regenerated app-server
JSON/TypeScript schemas.
- Replaces internal `:cwd` permission entries with `:project_roots`
entries.
- Keeps the existing cwd-update behavior for legacy-shaped
workspace-write profiles, while removing the deleted
`CurrentWorkingDirectory` case from that compatibility path.
- Keeps `PermissionProfile::workspace_write()` as the reusable symbolic
workspace-write helper, with docs noting that `:project_roots` entries
resolve at enforcement time.
- Updates app-server docs/examples and approval UI labeling to stop
advertising `:cwd` as a permission token.

## Compatibility

Persisted rollout items may contain the old
`{"kind":"current_working_directory"}` tag from earlier experimental
`permissionProfile` snapshots. This PR keeps that tag as a
deserialize-only alias for `ProjectRoots { subpath: None }`, while
continuing to serialize only the new `project_roots` tag.

## Follow-up

This PR intentionally does not introduce an explicit project-root set on
`SessionConfiguration` or runtime sandbox resolution. Today, the
resolver still uses the active cwd as the single implicit project root.
A follow-up should model project roots separately from tool cwd so
`:project_roots` entries can resolve against the configured project
roots, and resolve to no entries when there are no project roots.

## Verification

- `cargo test -p codex-protocol permissions:: --lib`
- `cargo test -p codex-app-server-protocol`
- `cargo test -p codex-sandboxing -p codex-exec-server --lib`
- `cargo test -p codex-core session_configuration_apply_ --lib`
- `cargo test -p codex-app-server
command_exec_permission_profile_project_roots_use_command_cwd --test
all`
- `cargo test -p codex-tui
thread_read_session_state_does_not_reuse_primary_permission_profile
--lib`
- `cargo test -p codex-tui
preset_matching_accepts_workspace_write_with_extra_roots --lib`
- `cargo test -p codex-config --lib`
2026-04-27 13:41:27 -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"}}