## Why The earlier PRs add stdio transport support and the config-backed environment provider, but the feature remains inert until normal Codex entrypoints construct `EnvironmentManager` with enough context to discover `CODEX_HOME/environments.toml`. This final stack PR activates the provider while preserving the legacy `CODEX_EXEC_SERVER_URL` fallback when no environments file exists. **Stack position:** this is PR 5 of 5. It is the product wiring PR that activates the configured environment provider added in PR 4. ## What Changed - Thread `codex_home` into `EnvironmentManagerArgs`. - Change `EnvironmentManager::new(...)` to load the provider from `CODEX_HOME`. - Preserve legacy behavior by falling back to `DefaultEnvironmentProvider::from_env()` when `environments.toml` is absent. - Make `environments.toml`-backed managers start new threads with all configured environments, default first, while keeping the legacy env-var path single-default. - Update the app-server, TUI, exec, MCP server, connector, prompt-debug, and thread-manager-sample callsites to pass `codex_home` and handle provider-loading errors. ## Self-Review Notes - The multi-environment startup path is intentionally tied to the `environments.toml` provider. Using `>1` configured environment as the only signal would also expand the legacy `CODEX_EXEC_SERVER_URL` provider because it keeps `local` addressable alongside `remote`. - The startup environment list is still derived inside `EnvironmentManager`; the provider only says whether its snapshot should start new threads with all configured environments. - The thread-manager sample was updated to pass the current `ThreadManager::new(...)` installation id argument so the stack compiles under Bazel. ## Stack - 1. https://github.com/openai/codex/pull/20663 - Add stdio exec-server listener - 2. https://github.com/openai/codex/pull/20664 - Add stdio exec-server client transport - 3. https://github.com/openai/codex/pull/20665 - Make environment providers own default selection - 4. https://github.com/openai/codex/pull/20666 - Add CODEX_HOME environments TOML provider - **5. This PR:** https://github.com/openai/codex/pull/20667 - Load configured environments from CODEX_HOME Split from original draft: https://github.com/openai/codex/pull/20508 ## Validation - `just fmt` - `git diff --check` - `bazel build --config=remote --strategy=remote --remote_download_toplevel //codex-rs/thread-manager-sample:codex-thread-manager-sample` - `bazel test --config=remote --strategy=remote --remote_download_toplevel //codex-rs/exec-server:exec-server-unit-tests` - `bazel test --config=remote --strategy=remote --remote_download_toplevel --test_sharding_strategy=disabled --test_arg=default_thread_environment_selections_use_manager_default_id //codex-rs/core:core-unit-tests` - `bazel test --config=remote --strategy=remote --remote_download_toplevel --test_sharding_strategy=disabled --test_arg=start_thread_uses_all_default_environments_from_codex_home //codex-rs/core:core-unit-tests` ## Documentation This activates `CODEX_HOME/environments.toml`; user-facing documentation should be added before this stack is treated as a documented public workflow. --------- Co-authored-by: Codex <noreply@openai.com>
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)--remote URL --executor-id ID [--name NAME]
Remote mode registers the local exec-server with the executor registry,
then reconnects to the service-provided rendezvous websocket as the executor.
It requires a bearer token in CODEX_EXEC_SERVER_REMOTE_BEARER_TOKEN.
Wire framing:
- websocket: one JSON-RPC message per websocket text frame
Lifecycle
Each connection follows this sequence:
- Send
initialize. - Wait for the
initializeresponse. - Send
initialized. - 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: whentrue, spawn a PTY-backed interactive process.pipeStdin: whentrue, keep non-PTY stdin writable viaprocess/write.arg0: optional argv0 override forwarded tocodex-utils-pty.
Response:
{
"processId": "proc-1"
}
Behavior notes:
- Reusing an existing
processIdis rejected. - PTY-backed processes accept later writes through
process/write. - Non-PTY processes reject writes unless
pipeStdinistrue. - 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 byprocess/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
processIdare 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 identifierseq: per-process output sequence numberstream:"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/readFilefs/writeFilefs/createDirectoryfs/getMetadatafs/readDirectoryfs/removefs/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:
ExecServerClientExecServerErrorExecServerClientConnectOptionsRemoteExecServerConnectArgs- protocol request/response structs for process and filesystem RPCs
DEFAULT_LISTEN_URLandExecServerListenUrlParseErrorExecServerRuntimePathsrun_main()for embedding the websocket serverRemoteExecutorConfigandrun_remote_executor()for embedding remote registration mode
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"}}