File size: 15,249 Bytes
dabfdaa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
# 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 exec-specific `codex-exec-server-protocol` message
envelope on the wire.

The CLI entrypoint supports:

- `ws://IP:PORT` (default)
- `--remote URL --environment-id ID [--name NAME]`
- `forward --connect ws://HOST:PORT --remote URL --environment-id ID`

Remote mode registers the local exec-server with the environment registry,
then reconnects to the service-provided rendezvous websocket as the environment.
Remote communication uses the Noise relay contract; the registry and harness
must support it.
Forward mode uses the same registration and Noise relay, but opens an independent
WebSocket connection to the destination exec-server for each authenticated
harness stream. Complete message payloads pass unchanged in both directions;
the forwarder does not parse RPCs, initialize sessions, or execute requests.
The destination owns session IDs, processes, and session resumption.
Disconnecting either side closes its peer and resets the remote stream. The
existing harness reconnect flow can then resume a retained destination session.
The forwarder does not replay requests or persist execution state, so recovery
is limited by the destination's session and process-output retention.
It uses the standard Codex ChatGPT sign-in state; run `codex login` first when
remote registration needs authentication. Containerized callers that receive an
Agent Identity JWT in `CODEX_ACCESS_TOKEN` can opt into that auth path with
`--use-agent-identity-auth`; Codex then registers an Agent task and sends the
derived AgentAssertion headers on the registry request.

Alternatively, API users can instead use `CODEX_API_KEY`;
Codex sends it as a bearer token on the registration request. For example:

```sh
CODEX_API_KEY="$OPENAI_API_KEY" \
codex exec-server \
  --remote ... \
  --environment-id "$ENVIRONMENT_ID"
```

AWS-hosted registries can use SigV4 for registry requests and the executor
WebSocket handshake. Select the transport and authentication with executor
arguments rather than `config.toml` settings:

```sh
codex exec-server \
  --remote https://example.com \
  --environment-id "$ENVIRONMENT_ID" \
  --remote-transport direct \
  --aws-sigv4 \
  --aws-profile development \
  --aws-region us-west-2 \
  --aws-service bedrock-mantle
```

Noise remains the default transport. Direct requires `--aws-sigv4`, which
conflicts with `--use-agent-identity-auth` and is not supported for Noise.
The AWS options require `--aws-sigv4`; Direct forwarding remains unsupported.
The AWS SDK default credential and region chains are used when `--aws-profile`
or `--aws-region` is omitted. The signing service defaults to `execute-api`.
Direct mode registers `direct_jsonrpc_v1` through the AWS-owned
`/cloud/environment/{environment_id}/direct/register` endpoint and carries plain
exec-server JSON-RPC over the authenticated WebSocket. The existing Codex Noise
registration endpoint remains unchanged. Production deployments must use TLS
(`https`/`wss`).

Direct registration URLs must remain reusable across disconnects and temporary
connection failures. The executor only refreshes its registration when the
WebSocket handshake returns `409 Conflict`. Handshake `408`, `429`, and `5xx`
responses retry with backoff using the current registration; other `4xx` responses
stop the executor. A backend that issues single-use connection URLs must adapt to
this contract. If the initial registration or a registration refresh fails, the
executor returns the error without retrying registration, matching Noise.

Wire framing:

- local websocket: one JSON-RPC message per websocket message
- direct remote websocket: one JSON-RPC message per websocket message
- Noise remote websocket: binary protobuf relay frames carrying encrypted payloads

## Remote Relay Message Format

In remote mode, the harness and environment communicate through rendezvous using
`codex.exec_server.relay.v1.RelayMessageFrame`; the checked-in schema is in
`src/proto/codex.exec_server.relay.v1.proto`. The relay frame carries stream
identity plus endpoint-owned reliability metadata:

```text
version
stream_id
traceparent       // optional W3C parent on the first frame of a traced request
tracestate        // optional W3C vendor state paired with traceparent
body              // handshake | data | ack_frame | resume | reset | heartbeat
ack               // highest contiguous peer segment seq received
ack_bits          // bitset for peer segment seqs after ack
seq               // data only: segment sequence number
segment_index     // data only: 0-based index within message
segment_count     // data only: number of segments in message
payload           // handshake bytes or encrypted data record
next_seq          // resume only: next sender seq
reason            // reset only: reset reason
```

`stream_id` identifies one virtual harness/environment JSON-RPC session on the
environment websocket. The harness generates a UUIDv4 `stream_id`; the environment
demuxes frames by `stream_id` and runs an independent `ConnectionProcessor` per
stream.

Use segment-level sequence numbers for reliability:

```text
seq = 0, 1, 2, 3, ...
```

Use contiguous segment sequence ranges to identify and stitch a segmented
application message:

```text
message_start_seq = seq - segment_index
segment_index = 0
segment_count = 1
```

`message_start_seq` is derived by the receiver, not sent on the wire. For
unsplit messages, `message_start_seq == seq`, `segment_index == 0`, and
`segment_count == 1`.

Use cumulative `ack` plus fixed-size `ack_bits` instead of variable ack ranges:

```text
ack = highest contiguous received segment seq
bit i in ack_bits acknowledges seq = ack + 1 + i
```

Send `ack` and `ack_bits` redundantly on every outbound frame. Acks are not
themselves acked. Acks, retries, duplicate suppression, segmentation, and
reassembly are endpoint responsibilities; rendezvous only routes relay frames
by `stream_id`.

## Lifecycle

Each connection follows this sequence:

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

Requests run sequentially by default. Pass `--concurrent-requests <COUNT>` to
enable concurrent processing.

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:

```json
{
  "clientName": "my-client"
}
```

Response:

```json
{
  "sessionId": "00000000-0000-4000-8000-000000000001",
  "environmentInfo": {
    "shell": { "name": "bash", "path": "/bin/bash" },
    "executorVersion": "1.2.3-alpha.4",
    "providerId": "sha256:fb4f62da3e84f6864dcec8ede7bc66f1c96ecaeaf55f8a786b85df994057c8ac",
    "cwd": "file:///workspace"
  }
}
```

`environmentInfo` contains the same executor metadata returned by
`environment/info`, so clients can use it without a second request.

`executorVersion` is the executor's package release version, or `0.0.0` when unknown.

The executor caches optional `providerId` at startup using
`codex_build_info::build_id(commit, target)`, which CI can also call for an
explicit build target. This opaque compatibility key excludes package version
and requires no manifest. It identifies a standard build configuration, not exact
executable bytes. Unstamped and legacy executors may omit it.

Rust clients cache this metadata for the client's lifetime, including session
resumption. If initialization omits it, the first metadata request fetches and
caches `environment/info`.

### `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:

```json
{
  "processId": "proc-1",
  "argv": ["bash", "-lc", "printf 'hello\\n'"],
  "cwd": "file:///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`: `file:` URI for the child process working directory.
- `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:

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

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

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

### `process/output`

Streaming output chunk from a running process.

Params:

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

```json
{
  "processId": "proc-1",
  "seq": 2,
  "exitCode": 0,
  "sandboxDenied": false
}
```

`sandboxDenied` lets streaming clients preserve executor-side sandbox denial
detection without issuing a final `process/read` request. Clients recover it
with `process/read` when an older server omits the field.

### `process/closed`

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

Params:

```json
{
  "processId": "proc-1",
  "seq": 3
}
```

## Filesystem RPCs

Filesystem methods require valid `file:` URI strings and return JSON-RPC errors
for invalid or unavailable paths. Native absolute path strings are rejected;
callers must convert them to `file:` URIs before sending requests:

- `fs/readFile`
- `fs/open`, `fs/readBlock`, and `fs/close` (internal transport for
  `ExecutorFileSystem::read_file_stream`)
- `fs/writeFile`
- `fs/createDirectory`
- `fs/getMetadata`
- `fs/canonicalize`
- `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
- `RemoteEnvironmentConfig` and `run_remote_environment()` for embedding remote
  registration mode

Callers must pass `ExecServerRuntimePaths` and an explicitly configured
`HttpClientFactory` to `run_main()`. The top-level `codex exec-server` command
builds these paths from the `codex` arg0 dispatch state and resolves its HTTP
client factory from the effective Codex configuration.
`RemoteEnvironmentConfig::new(...)` also takes the auth provider and HTTP client
factory that remote registration mode should use; the CLI builds the auth
provider from Codex auth state before starting remote mode.

## Example session

Initialize:

```json
{"id":1,"method":"initialize","params":{"clientName":"example-client"}}
{"id":1,"result":{"sessionId":"00000000-0000-4000-8000-000000000001","environmentInfo":{"shell":{"name":"bash","path":"/bin/bash"},"cwd":"file:///tmp"}}}
{"method":"initialized","params":{}}
```

Start a process:

```json
{"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":"file:///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:

```json
{"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":"process/terminate","params":{"processId":"proc-1"}}
{"id":4,"result":{"running":true}}
{"method":"process/exited","params":{"processId":"proc-1","seq":3,"exitCode":0,"sandboxDenied":false}}
{"method":"process/closed","params":{"processId":"proc-1","seq":4}}
```