Skip to main content

codex_exec_server_protocol/
protocol.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3
4use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
5use codex_file_system::FileSystemSandboxContext;
6pub use codex_file_system::WalkOptions;
7pub use codex_file_system::WalkOutcome;
8use codex_network_proxy::ManagedNetworkSandboxContext;
9use codex_network_proxy::RemoteNetworkProxyLaunchConfig;
10use codex_protocol::capabilities::SelectedCapabilityRoot;
11use codex_protocol::config_types::ShellEnvironmentPolicyInherit;
12use codex_shell_command::shell_detect::DetectedShell;
13use codex_utils_path_uri::PathUri;
14use serde::Deserialize;
15use serde::Serialize;
16
17use crate::ProcessId;
18
19pub const INITIALIZE_METHOD: &str = "initialize";
20pub const INITIALIZED_METHOD: &str = "initialized";
21pub const EXEC_METHOD: &str = "process/start";
22pub const EXEC_READ_METHOD: &str = "process/read";
23pub const EXEC_WRITE_METHOD: &str = "process/write";
24pub const EXEC_SIGNAL_METHOD: &str = "process/signal";
25pub const EXEC_TERMINATE_METHOD: &str = "process/terminate";
26pub const EXEC_OUTPUT_DELTA_METHOD: &str = "process/output";
27pub const EXEC_EXITED_METHOD: &str = "process/exited";
28pub const EXEC_CLOSED_METHOD: &str = "process/closed";
29pub const ENVIRONMENT_INFO_METHOD: &str = "environment/info";
30pub const ENVIRONMENT_STATUS_METHOD: &str = "environment/status";
31pub const FS_READ_FILE_METHOD: &str = "fs/readFile";
32pub const FS_OPEN_METHOD: &str = "fs/open";
33pub const FS_READ_BLOCK_METHOD: &str = "fs/readBlock";
34pub const FS_CLOSE_METHOD: &str = "fs/close";
35pub const FS_WRITE_FILE_METHOD: &str = "fs/writeFile";
36pub const FS_CREATE_DIRECTORY_METHOD: &str = "fs/createDirectory";
37pub const FS_GET_METADATA_METHOD: &str = "fs/getMetadata";
38pub const FS_CANONICALIZE_METHOD: &str = "fs/canonicalize";
39pub const FS_READ_DIRECTORY_METHOD: &str = "fs/readDirectory";
40pub const FS_WALK_METHOD: &str = "fs/walk";
41pub const FS_REMOVE_METHOD: &str = "fs/remove";
42pub const FS_COPY_METHOD: &str = "fs/copy";
43/// Discovers capability manifests below selected roots using executor-local filesystem access.
44pub const CAPABILITY_ROOTS_DISCOVER_METHOD: &str = "capabilityRoots/discoverV1";
45/// Ordered plugin manifest paths recognized beneath a plugin root.
46pub const DISCOVERABLE_PLUGIN_MANIFEST_PATHS: &[&str] = &[
47    ".codex-plugin/plugin.json",
48    ".claude-plugin/plugin.json",
49    ".cursor-plugin/plugin.json",
50];
51/// JSON-RPC request method for executor-side HTTP requests.
52pub const HTTP_REQUEST_METHOD: &str = "http/request";
53/// JSON-RPC notification method for streamed executor HTTP response bodies.
54pub const HTTP_REQUEST_BODY_DELTA_METHOD: &str = "http/request/bodyDelta";
55/// Maximum decoded response-body bytes carried by one streamed HTTP notification.
56pub const MAX_HTTP_BODY_DELTA_BYTES: usize = 1024 * 1024;
57
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59#[serde(transparent)]
60pub struct ByteChunk(#[serde(with = "base64_bytes")] pub Vec<u8>);
61
62impl ByteChunk {
63    pub fn into_inner(self) -> Vec<u8> {
64        self.0
65    }
66}
67
68impl From<Vec<u8>> for ByteChunk {
69    fn from(value: Vec<u8>) -> Self {
70        Self(value)
71    }
72}
73
74#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
75#[serde(rename_all = "camelCase")]
76pub struct InitializeParams {
77    pub client_name: String,
78    #[serde(default)]
79    pub resume_session_id: Option<String>,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
83#[serde(rename_all = "camelCase")]
84pub struct InitializeResponse {
85    pub session_id: String,
86}
87
88/// Information about an execution/filesystem environment.
89#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
90#[serde(rename_all = "camelCase")]
91pub struct EnvironmentInfo {
92    pub shell: ShellInfo,
93    /// Working directory inherited by the exec-server process.
94    #[serde(default)]
95    pub cwd: Option<PathUri>,
96    /// Optional executor features that clients must gate before sending newer request fields.
97    #[serde(default)]
98    pub capabilities: EnvironmentCapabilities,
99}
100
101/// Features supported by the selected exec-server environment.
102#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
103#[serde(rename_all = "camelCase")]
104pub struct EnvironmentCapabilities {
105    /// Whether `exec` accepts instructions for launching an executor-local network proxy.
106    #[serde(default)]
107    pub network_proxy_launch: bool,
108}
109
110/// Status returned by an initialized exec-server connection.
111///
112/// The response is intentionally small today. New status details can be added
113/// without changing the method used by clients to verify that an initialized
114/// exec-server connection is still responsive.
115#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
116#[serde(rename_all = "camelCase")]
117pub struct EnvironmentStatus {
118    pub status: EnvironmentStatusKind,
119}
120
121/// High-level status reported by exec-server itself.
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
123#[serde(rename_all = "camelCase")]
124pub enum EnvironmentStatusKind {
125    /// The connection is initialized and exec-server can handle requests.
126    Ready,
127}
128
129impl EnvironmentInfo {
130    /// Returns information about the current local exec-server process.
131    pub fn local() -> Self {
132        Self {
133            shell: codex_shell_command::shell_detect::default_user_shell().into(),
134            cwd: std::env::current_dir()
135                .ok()
136                .and_then(|cwd| PathUri::from_host_native_path(cwd).ok()),
137            capabilities: EnvironmentCapabilities {
138                network_proxy_launch: true,
139            },
140        }
141    }
142}
143
144/// Shell detected for an execution/filesystem environment.
145#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
146#[serde(rename_all = "camelCase")]
147pub struct ShellInfo {
148    /// Stable shell name, for example `zsh`, `bash`, `powershell`, `sh`, or `cmd`.
149    pub name: String,
150    /// Target-native shell executable path or command name. Fallbacks such as `cmd.exe` need not
151    /// be absolute, so this is not a [`PathUri`].
152    pub path: String,
153}
154
155impl From<DetectedShell> for ShellInfo {
156    fn from(shell: DetectedShell) -> Self {
157        Self {
158            name: shell.name().to_string(),
159            path: shell.shell_path.to_string_lossy().into_owned(),
160        }
161    }
162}
163
164#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
165#[serde(rename_all = "camelCase")]
166pub struct ExecParams {
167    /// Client-chosen logical process handle scoped to this connection/session.
168    /// This is a protocol key, not an OS pid.
169    pub process_id: ProcessId,
170    pub argv: Vec<String>,
171    /// Working directory URI, interpreted using the exec-server host's path rules at launch time.
172    pub cwd: PathUri,
173    #[serde(default)]
174    pub env_policy: Option<ExecEnvPolicy>,
175    pub env: HashMap<String, String>,
176    pub tty: bool,
177    /// Keep non-tty stdin writable through `process/write`.
178    #[serde(default)]
179    pub pipe_stdin: bool,
180    /// Optional process-visible argv0 override. Values such as `codex-linux-sandbox` are command
181    /// names rather than paths, so this is not a [`PathUri`].
182    pub arg0: Option<String>,
183    /// Portable sandbox intent. Concrete wrapper argv is resolved by the exec-server.
184    #[serde(default)]
185    pub sandbox: Option<FileSystemSandboxContext>,
186    /// Whether the eventual executor-side sandbox must enforce managed networking.
187    #[serde(default)]
188    pub enforce_managed_network: bool,
189    /// Optional details for enforcing managed networking without a live proxy object.
190    ///
191    /// When `enforce_managed_network` is true and these details are absent, the executor must
192    /// continue to fail closed. This preserves compatibility with older clients.
193    #[serde(default)]
194    pub managed_network: Option<ManagedNetworkSandboxContext>,
195    /// Optional instructions for starting an executor-local managed-network proxy.
196    #[serde(default, skip_serializing_if = "Option::is_none")]
197    pub network_proxy: Option<RemoteNetworkProxyLaunchConfig>,
198}
199
200#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
201#[serde(rename_all = "camelCase")]
202pub struct ExecEnvPolicy {
203    pub inherit: ShellEnvironmentPolicyInherit,
204    pub ignore_default_excludes: bool,
205    pub exclude: Vec<String>,
206    pub r#set: HashMap<String, String>,
207    pub include_only: Vec<String>,
208}
209
210#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
211#[serde(rename_all = "camelCase")]
212pub struct ExecResponse {
213    pub process_id: ProcessId,
214}
215
216#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
217#[serde(rename_all = "camelCase")]
218pub struct ReadParams {
219    pub process_id: ProcessId,
220    pub after_seq: Option<u64>,
221    pub max_bytes: Option<usize>,
222    pub wait_ms: Option<u64>,
223}
224
225#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
226#[serde(rename_all = "camelCase")]
227pub struct ProcessOutputChunk {
228    pub seq: u64,
229    pub stream: ExecOutputStream,
230    pub chunk: ByteChunk,
231}
232
233#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
234#[serde(rename_all = "camelCase")]
235pub struct ReadResponse {
236    pub chunks: Vec<ProcessOutputChunk>,
237    pub next_seq: u64,
238    pub exited: bool,
239    pub exit_code: Option<i32>,
240    pub closed: bool,
241    pub failure: Option<String>,
242    /// Whether the executor classified the process failure as a sandbox denial.
243    #[serde(default)]
244    pub sandbox_denied: bool,
245}
246
247#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
248#[serde(rename_all = "camelCase")]
249pub struct WriteParams {
250    pub process_id: ProcessId,
251    pub chunk: ByteChunk,
252    pub write_id: String,
253}
254
255#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
256#[serde(rename_all = "camelCase")]
257pub enum WriteStatus {
258    Accepted,
259    UnknownProcess,
260    StdinClosed,
261    Starting,
262}
263
264#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
265#[serde(rename_all = "camelCase")]
266pub struct WriteResponse {
267    pub status: WriteStatus,
268}
269
270#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
271#[serde(rename_all = "camelCase")]
272pub enum ProcessSignal {
273    Interrupt,
274}
275
276#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
277#[serde(rename_all = "camelCase")]
278pub struct SignalParams {
279    pub process_id: ProcessId,
280    pub signal: ProcessSignal,
281}
282
283#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
284#[serde(rename_all = "camelCase")]
285pub struct SignalResponse {}
286
287#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
288#[serde(rename_all = "camelCase")]
289pub struct TerminateParams {
290    pub process_id: ProcessId,
291}
292
293#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
294#[serde(rename_all = "camelCase")]
295pub struct TerminateResponse {
296    pub running: bool,
297}
298
299#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
300#[serde(rename_all = "camelCase")]
301pub struct FsReadFileParams {
302    pub path: PathUri,
303    pub sandbox: Option<FileSystemSandboxContext>,
304}
305
306#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
307#[serde(rename_all = "camelCase")]
308pub struct FsReadFileResponse {
309    pub data_base64: String,
310}
311
312#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
313#[serde(rename_all = "camelCase")]
314pub struct FsOpenParams {
315    pub handle_id: String,
316    pub path: PathUri,
317    pub sandbox: Option<FileSystemSandboxContext>,
318}
319
320#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
321#[serde(rename_all = "camelCase")]
322pub struct FsOpenResponse {
323    pub handle_id: String,
324}
325
326#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
327#[serde(rename_all = "camelCase")]
328pub struct FsReadBlockParams {
329    pub handle_id: String,
330    pub offset: u64,
331    pub len: usize,
332}
333
334#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
335#[serde(rename_all = "camelCase")]
336pub struct FsReadBlockResponse {
337    pub chunk: ByteChunk,
338    pub eof: bool,
339}
340
341#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
342#[serde(rename_all = "camelCase")]
343pub struct FsCloseParams {
344    pub handle_id: String,
345}
346
347#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
348#[serde(rename_all = "camelCase")]
349pub struct FsCloseResponse {}
350
351#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
352#[serde(rename_all = "camelCase")]
353pub struct FsWriteFileParams {
354    pub path: PathUri,
355    pub data_base64: String,
356    pub sandbox: Option<FileSystemSandboxContext>,
357}
358
359#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
360#[serde(rename_all = "camelCase")]
361pub struct FsWriteFileResponse {}
362
363#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
364#[serde(rename_all = "camelCase")]
365pub struct FsCreateDirectoryParams {
366    pub path: PathUri,
367    pub recursive: Option<bool>,
368    pub sandbox: Option<FileSystemSandboxContext>,
369}
370
371#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
372#[serde(rename_all = "camelCase")]
373pub struct FsCreateDirectoryResponse {}
374
375#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
376#[serde(rename_all = "camelCase")]
377pub struct FsGetMetadataParams {
378    pub path: PathUri,
379    pub sandbox: Option<FileSystemSandboxContext>,
380}
381
382#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
383#[serde(rename_all = "camelCase")]
384pub struct FsGetMetadataResponse {
385    pub is_directory: bool,
386    pub is_file: bool,
387    pub is_symlink: bool,
388    pub size: u64,
389    pub created_at_ms: i64,
390    pub modified_at_ms: i64,
391}
392
393#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
394#[serde(rename_all = "camelCase")]
395pub struct FsCanonicalizeParams {
396    pub path: PathUri,
397    pub sandbox: Option<FileSystemSandboxContext>,
398}
399
400#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
401#[serde(rename_all = "camelCase")]
402pub struct FsCanonicalizeResponse {
403    pub path: PathUri,
404}
405
406#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
407#[serde(rename_all = "camelCase")]
408pub struct FsReadDirectoryParams {
409    pub path: PathUri,
410    pub sandbox: Option<FileSystemSandboxContext>,
411}
412
413#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
414#[serde(rename_all = "camelCase")]
415pub struct FsReadDirectoryEntry {
416    pub file_name: String,
417    pub is_directory: bool,
418    pub is_file: bool,
419}
420
421#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
422#[serde(rename_all = "camelCase")]
423pub struct FsReadDirectoryResponse {
424    pub entries: Vec<FsReadDirectoryEntry>,
425}
426
427#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
428#[serde(rename_all = "camelCase")]
429pub struct FsWalkParams {
430    pub path: PathUri,
431    pub options: WalkOptions,
432    pub sandbox: Option<FileSystemSandboxContext>,
433}
434
435pub type FsWalkResponse = WalkOutcome;
436
437#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
438#[serde(rename_all = "camelCase")]
439pub struct FsRemoveParams {
440    pub path: PathUri,
441    pub recursive: Option<bool>,
442    pub force: Option<bool>,
443    pub sandbox: Option<FileSystemSandboxContext>,
444}
445
446#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
447#[serde(rename_all = "camelCase")]
448pub struct FsRemoveResponse {}
449
450#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
451#[serde(rename_all = "camelCase")]
452pub struct FsCopyParams {
453    pub source_path: PathUri,
454    pub destination_path: PathUri,
455    pub recursive: bool,
456    pub sandbox: Option<FileSystemSandboxContext>,
457}
458
459#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
460#[serde(rename_all = "camelCase")]
461pub struct FsCopyResponse {}
462
463/// Roots to inspect for plugin and skill capability manifests.
464#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
465#[serde(rename_all = "camelCase")]
466pub struct CapabilityRootsDiscoverParams {
467    pub roots: Vec<CapabilityRootDiscoverRequest>,
468}
469
470/// One caller-selected capability root.
471#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
472#[serde(rename_all = "camelCase")]
473pub struct CapabilityRootDiscoverRequest {
474    /// Opaque caller identity returned unchanged in the response.
475    pub id: String,
476    /// Absolute root URI interpreted using the exec-server host's path rules.
477    pub path: PathUri,
478}
479
480/// Executor-local discovery results in request order.
481#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
482#[serde(rename_all = "camelCase")]
483pub struct CapabilityRootsDiscoverResponse {
484    pub roots: Vec<CapabilityRootDiscovery>,
485}
486
487/// Recognized UTF-8 capability file materialized by the exec-server.
488#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
489#[serde(rename_all = "camelCase")]
490pub struct CapabilityTextFile {
491    pub path: PathUri,
492    pub contents: String,
493}
494
495/// Plugin files declared directly by a selected root.
496#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
497#[serde(rename_all = "camelCase")]
498pub struct DiscoveredPluginFiles {
499    pub manifest: CapabilityTextFile,
500    /// File-backed MCP declarations, including the conventional `.mcp.json` fallback.
501    #[serde(default)]
502    pub mcp_config: Option<CapabilityTextFile>,
503    /// File-backed connector declarations.
504    #[serde(default)]
505    pub apps_config: Option<CapabilityTextFile>,
506}
507
508/// A skill instructions file and its optional sibling metadata.
509#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
510#[serde(rename_all = "camelCase")]
511pub struct DiscoveredSkillFiles {
512    pub instructions: CapabilityTextFile,
513    #[serde(default)]
514    pub metadata: Option<CapabilityTextFile>,
515}
516
517/// Manifest bundle for one selected root.
518///
519/// Discovery failures are root-local so one broken package does not discard valid siblings.
520#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
521#[serde(rename_all = "camelCase")]
522pub struct CapabilityRootDiscovery {
523    pub id: String,
524    pub path: PathUri,
525    #[serde(default)]
526    pub plugin: Option<DiscoveredPluginFiles>,
527    #[serde(default)]
528    pub skills: Vec<DiscoveredSkillFiles>,
529    /// Plugin manifests found while scanning the root, used to namespace nested skills.
530    #[serde(default)]
531    pub namespace_manifests: Vec<CapabilityTextFile>,
532    #[serde(default)]
533    pub warnings: Vec<String>,
534    #[serde(default)]
535    pub error: Option<String>,
536}
537
538/// Immutable results for the selected capability roots visible in one model step.
539#[derive(Clone, Debug)]
540pub struct ExecutorCapabilityDiscoverySnapshot {
541    roots: Arc<[ExecutorCapabilityDiscoverySnapshotEntry]>,
542}
543
544#[derive(Clone, Debug)]
545pub struct ExecutorCapabilityDiscoverySnapshotEntry {
546    pub selected_root: SelectedCapabilityRoot,
547    pub result: Result<Arc<CapabilityRootDiscovery>, String>,
548}
549
550impl ExecutorCapabilityDiscoverySnapshot {
551    pub fn new(
552        selected_roots: &[SelectedCapabilityRoot],
553        discoveries: Vec<Result<Arc<CapabilityRootDiscovery>, String>>,
554    ) -> Self {
555        debug_assert_eq!(selected_roots.len(), discoveries.len());
556        Self {
557            roots: selected_roots
558                .iter()
559                .cloned()
560                .zip(discoveries)
561                .map(
562                    |(selected_root, result)| ExecutorCapabilityDiscoverySnapshotEntry {
563                        selected_root,
564                        result,
565                    },
566                )
567                .collect(),
568        }
569    }
570
571    pub fn roots(&self) -> &[ExecutorCapabilityDiscoverySnapshotEntry] {
572        &self.roots
573    }
574}
575
576/// HTTP header represented in the executor protocol.
577#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
578#[serde(rename_all = "camelCase")]
579pub struct HttpHeader {
580    /// Header name as it appears on the HTTP wire.
581    pub name: String,
582    /// Header value after UTF-8 conversion.
583    pub value: String,
584}
585
586/// Redirect behavior for an executor-side HTTP request.
587#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
588#[serde(rename_all = "camelCase")]
589pub enum HttpRedirectPolicy {
590    /// Follow redirects using the HTTP client's normal limits.
591    #[default]
592    Follow,
593    /// Return the redirect response without following its location.
594    Stop,
595}
596
597/// Executor-side HTTP request envelope.
598///
599/// This intentionally stays transport-shaped rather than MCP-shaped so callers
600/// can use it for Streamable HTTP, OAuth discovery, and future executor-owned
601/// HTTP probes without introducing one protocol method per higher-level use.
602#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
603#[serde(rename_all = "camelCase")]
604pub struct HttpRequestParams {
605    /// HTTP method, for example `GET`, `POST`, or `DELETE`.
606    pub method: String,
607    /// Absolute `http://` or `https://` URL.
608    pub url: String,
609    /// Ordered request headers. Repeated header names are preserved.
610    #[serde(default)]
611    pub headers: Vec<HttpHeader>,
612    /// Optional request body bytes.
613    #[serde(default, rename = "bodyBase64")]
614    pub body: Option<ByteChunk>,
615    /// Request timeout in milliseconds.
616    ///
617    /// Omitted or `null` disables the timeout. A number applies that exact
618    /// millisecond deadline.
619    #[serde(default, skip_serializing_if = "Option::is_none")]
620    pub timeout_ms: Option<u64>,
621    /// Whether the executor should follow HTTP redirects.
622    #[serde(default)]
623    pub redirect_policy: HttpRedirectPolicy,
624    /// Caller-chosen stream id for `http/request/bodyDelta` notifications.
625    ///
626    /// The id must remain unique on a connection until the terminal body delta
627    /// arrives, even if the caller stops reading the stream earlier. Buffered
628    /// requests still send an id so callers can keep one consistent request
629    /// envelope shape.
630    pub request_id: String,
631    /// Return after response headers and stream the response body as deltas.
632    #[serde(default)]
633    pub stream_response: bool,
634}
635
636/// HTTP response envelope returned from an executor `http/request` call.
637#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
638#[serde(rename_all = "camelCase")]
639pub struct HttpRequestResponse {
640    /// Numeric HTTP response status code.
641    pub status: u16,
642    /// Ordered response headers. Repeated header names are preserved.
643    pub headers: Vec<HttpHeader>,
644    /// Buffered response body bytes. Empty when `streamResponse` is true.
645    #[serde(rename = "bodyBase64")]
646    pub body: ByteChunk,
647}
648
649/// Ordered response-body frame for `streamResponse` HTTP requests.
650///
651/// Headers are returned in the `http/request` response so the caller can choose
652/// a parser immediately; body bytes then arrive on this notification stream.
653#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
654#[serde(rename_all = "camelCase")]
655pub struct HttpRequestBodyDeltaNotification {
656    /// Request id from the streamed `http/request` call.
657    pub request_id: String,
658    /// Monotonic one-based body frame sequence number.
659    pub seq: u64,
660    /// Response-body bytes carried by this frame.
661    #[serde(rename = "deltaBase64")]
662    pub delta: ByteChunk,
663    /// Marks response-body EOF. No later deltas are expected for this request.
664    #[serde(default)]
665    pub done: bool,
666    /// Terminal stream error. Set only on the final notification.
667    #[serde(default)]
668    pub error: Option<String>,
669}
670
671#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
672#[serde(rename_all = "camelCase")]
673pub enum ExecOutputStream {
674    Stdout,
675    Stderr,
676    Pty,
677}
678
679#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
680#[serde(rename_all = "camelCase")]
681pub struct ExecOutputDeltaNotification {
682    pub process_id: ProcessId,
683    pub seq: u64,
684    pub stream: ExecOutputStream,
685    pub chunk: ByteChunk,
686}
687
688#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
689#[serde(rename_all = "camelCase")]
690pub struct ExecExitedNotification {
691    pub process_id: ProcessId,
692    pub seq: u64,
693    pub exit_code: i32,
694    #[serde(default)]
695    pub sandbox_denied: Option<bool>,
696}
697
698#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
699#[serde(rename_all = "camelCase")]
700pub struct ExecClosedNotification {
701    pub process_id: ProcessId,
702    pub seq: u64,
703}
704
705mod base64_bytes {
706    use super::BASE64_STANDARD;
707    use base64::Engine as _;
708    use serde::Deserialize;
709    use serde::Deserializer;
710    use serde::Serializer;
711
712    pub fn serialize<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
713    where
714        S: Serializer,
715    {
716        serializer.serialize_str(&BASE64_STANDARD.encode(bytes))
717    }
718
719    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
720    where
721        D: Deserializer<'de>,
722    {
723        let encoded = String::deserialize(deserializer)?;
724        BASE64_STANDARD
725            .decode(encoded)
726            .map_err(serde::de::Error::custom)
727    }
728}
729
730#[cfg(test)]
731mod tests {
732    use super::EnvironmentCapabilities;
733    use super::EnvironmentInfo;
734    use super::ExecExitedNotification;
735    use super::ExecParams;
736    use super::FsReadFileParams;
737    use super::HttpRequestParams;
738    use super::ProcessId;
739    use super::ShellInfo;
740    use codex_file_system::FileSystemSandboxContext;
741    use codex_network_proxy::ManagedNetworkSandboxContext;
742    use codex_network_proxy::NetworkProxyAuditMetadata;
743    use codex_network_proxy::NetworkProxyConfig;
744    use codex_network_proxy::RemoteNetworkProxyConfig;
745    use codex_network_proxy::RemoteNetworkProxyLaunchConfig;
746    use codex_protocol::models::ManagedFileSystemPermissions;
747    use codex_protocol::models::PermissionProfile;
748    use codex_protocol::permissions::FileSystemAccessMode;
749    use codex_protocol::permissions::FileSystemPath;
750    use codex_protocol::permissions::FileSystemSandboxEntry;
751    use codex_protocol::permissions::FileSystemSandboxPolicy;
752    use codex_protocol::permissions::FileSystemSpecialPath;
753    use codex_protocol::permissions::NetworkSandboxPolicy;
754    use codex_utils_path_uri::PathUri;
755    use pretty_assertions::assert_eq;
756    use std::collections::HashMap;
757
758    #[test]
759    fn exec_params_keeps_proxy_launch_separate_from_sandbox_facts() {
760        let cwd =
761            PathUri::from_host_native_path(std::env::current_dir().expect("current directory"))
762                .expect("cwd URI");
763        let params = ExecParams {
764            process_id: ProcessId::from("managed-network"),
765            argv: vec!["true".to_string()],
766            cwd,
767            env_policy: None,
768            env: HashMap::new(),
769            tty: false,
770            pipe_stdin: false,
771            arg0: None,
772            sandbox: None,
773            enforce_managed_network: true,
774            managed_network: Some(ManagedNetworkSandboxContext {
775                loopback_ports: vec![43123, 48081],
776                allow_local_binding: false,
777            }),
778            network_proxy: Some(
779                RemoteNetworkProxyLaunchConfig::new(
780                    RemoteNetworkProxyConfig::from_effective_config(&NetworkProxyConfig::default())
781                        .expect("supported remote config"),
782                )
783                .with_audit_metadata(NetworkProxyAuditMetadata {
784                    conversation_id: Some("conversation-1".to_string()),
785                    ..NetworkProxyAuditMetadata::default()
786                })
787                .for_execution("remote".to_string(), "execution-1".to_string()),
788            ),
789        };
790
791        let mut serialized = serde_json::to_value(&params).expect("serialize exec params");
792        assert_eq!(
793            serialized["managedNetwork"],
794            serde_json::json!({
795                "loopbackPorts": [43123, 48081],
796                "allowLocalBinding": false,
797            })
798        );
799        assert_eq!(
800            serialized["networkProxy"]["auditMetadata"]["conversationId"],
801            "conversation-1"
802        );
803        let round_trip: ExecParams =
804            serde_json::from_value(serialized.clone()).expect("deserialize exec params");
805        assert_eq!(round_trip, params);
806
807        serialized
808            .as_object_mut()
809            .expect("exec params object")
810            .remove("managedNetwork");
811        serialized
812            .as_object_mut()
813            .expect("exec params object")
814            .remove("networkProxy");
815        let legacy: ExecParams =
816            serde_json::from_value(serialized).expect("deserialize legacy exec params");
817        assert!(legacy.enforce_managed_network);
818        assert_eq!(legacy.managed_network, None);
819        assert_eq!(legacy.network_proxy, None);
820        let legacy_serialized =
821            serde_json::to_value(&legacy).expect("serialize exec params without proxy launch");
822        assert!(legacy_serialized.get("networkProxy").is_none());
823    }
824
825    #[test]
826    fn environment_info_accepts_legacy_response_without_cwd() {
827        let info: EnvironmentInfo = serde_json::from_value(serde_json::json!({
828            "shell": { "name": "zsh", "path": "/bin/zsh" }
829        }))
830        .expect("legacy environment info should deserialize");
831
832        assert_eq!(
833            info,
834            EnvironmentInfo {
835                shell: ShellInfo {
836                    name: "zsh".to_string(),
837                    path: "/bin/zsh".to_string(),
838                },
839                cwd: None,
840                capabilities: EnvironmentCapabilities::default(),
841            }
842        );
843    }
844
845    #[test]
846    fn filesystem_protocol_rejects_native_absolute_paths() {
847        let native_path = std::env::current_dir()
848            .expect("current directory")
849            .join("native-file.txt");
850        let native_cwd = std::env::current_dir().expect("current directory");
851
852        serde_json::from_value::<FsReadFileParams>(serde_json::json!({
853            "path": native_path.to_string_lossy(),
854            "sandbox": null,
855        }))
856        .expect_err("native absolute path should not deserialize as a URI");
857
858        let sandbox = FileSystemSandboxContext::from_permission_profile_with_cwd(
859            PermissionProfile::default(),
860            PathUri::from_host_native_path(&native_cwd).expect("cwd URI"),
861        );
862        let mut native_path_sandbox =
863            serde_json::to_value(sandbox).expect("sandbox should serialize");
864        native_path_sandbox["cwd"] = serde_json::json!(native_cwd.to_string_lossy());
865
866        serde_json::from_value::<FsReadFileParams>(serde_json::json!({
867            "path": PathUri::from_host_native_path(native_path)
868                .expect("path URI")
869                .to_string(),
870            "sandbox": native_path_sandbox,
871        }))
872        .expect_err("native absolute sandbox cwd should not deserialize as a URI");
873    }
874
875    #[test]
876    fn filesystem_protocol_round_trips_permission_entries() {
877        let native_cwd = std::env::current_dir().expect("current directory");
878        let cwd = PathUri::from_host_native_path(&native_cwd).expect("cwd URI");
879        let file_system = ManagedFileSystemPermissions::Restricted {
880            entries: vec![
881                FileSystemSandboxEntry {
882                    path: FileSystemPath::Path {
883                        path: native_cwd.clone().try_into().expect("absolute cwd"),
884                    },
885                    access: FileSystemAccessMode::Read,
886                    missing_path_behavior: None,
887                },
888                FileSystemSandboxEntry::skip_missing_path(
889                    FileSystemPath::Path {
890                        path: native_cwd.join(".git").try_into().expect("absolute path"),
891                    },
892                    FileSystemAccessMode::Read,
893                ),
894                FileSystemSandboxEntry::skip_missing_path(
895                    FileSystemPath::Special {
896                        value: FileSystemSpecialPath::ProjectRoots {
897                            subpath: Some(".codex".into()),
898                        },
899                    },
900                    FileSystemAccessMode::Read,
901                ),
902            ],
903            glob_scan_max_depth: Some(2.try_into().expect("non-zero depth")),
904        };
905        let permissions = PermissionProfile::Managed {
906            file_system,
907            network: NetworkSandboxPolicy::Restricted,
908        };
909        let sandbox =
910            FileSystemSandboxContext::from_permission_profile_with_cwd(permissions, cwd.clone());
911
912        let serialized = serde_json::to_value(&sandbox).expect("serialize sandbox");
913
914        assert_eq!(
915            serialized["permissions"]["file_system"]["entries"][0]["path"]["path"],
916            serde_json::json!(cwd.to_string())
917        );
918        assert_eq!(
919            serialized["permissions"]["file_system"]["entries"][1]["path"]["type"],
920            serde_json::json!("path")
921        );
922        assert_eq!(
923            serialized["permissions"]["file_system"]["entries"][1]["missing_path_behavior"],
924            serde_json::json!("skip")
925        );
926        assert_eq!(
927            serialized["permissions"]["file_system"]["entries"][2]["path"]["type"],
928            serde_json::json!("special")
929        );
930        assert_eq!(
931            serialized["permissions"]["file_system"]["entries"][2]["missing_path_behavior"],
932            serde_json::json!("skip")
933        );
934        assert!(!serialized.to_string().contains("generated_default_path"));
935        assert!(!serialized.to_string().contains("generated_default_special"));
936        assert_eq!(
937            serde_json::from_value::<FileSystemSandboxContext>(serialized)
938                .expect("deserialize sandbox"),
939            sandbox
940        );
941    }
942
943    #[test]
944    fn filesystem_protocol_round_trips_legacy_policy_paths_as_uris() {
945        let native_cwd = std::env::current_dir().expect("current directory");
946        let cwd = PathUri::from_host_native_path(&native_cwd).expect("cwd URI");
947        let mut file_system_policy =
948            FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
949                path: FileSystemPath::Path {
950                    path: native_cwd.try_into().expect("absolute cwd"),
951                },
952                access: FileSystemAccessMode::Read,
953                missing_path_behavior: None,
954            }]);
955        file_system_policy.glob_scan_max_depth = Some(2);
956        let permissions = PermissionProfile::from_runtime_permissions(
957            &file_system_policy,
958            NetworkSandboxPolicy::Restricted,
959        );
960        let sandbox =
961            FileSystemSandboxContext::from_permission_profile_with_cwd(permissions, cwd.clone());
962
963        let serialized = serde_json::to_value(&sandbox).expect("serialize sandbox");
964
965        assert_eq!(
966            serialized["permissions"]["file_system"]["entries"][0]["path"]["path"],
967            serde_json::json!(cwd.to_string())
968        );
969        assert_eq!(
970            serde_json::from_value::<FileSystemSandboxContext>(serialized)
971                .expect("deserialize sandbox"),
972            sandbox
973        );
974    }
975
976    #[test]
977    fn http_request_timeout_treats_omitted_and_null_as_no_timeout() {
978        let omitted: HttpRequestParams = serde_json::from_value(serde_json::json!({
979            "method": "GET",
980            "url": "https://example.test",
981            "requestId": "req-omitted-timeout",
982        }))
983        .expect("omitted timeout should deserialize");
984        let null_timeout: HttpRequestParams = serde_json::from_value(serde_json::json!({
985            "method": "GET",
986            "url": "https://example.test",
987            "requestId": "req-null-timeout",
988            "timeoutMs": null,
989        }))
990        .expect("null timeout should deserialize");
991        let explicit_timeout: HttpRequestParams = serde_json::from_value(serde_json::json!({
992            "method": "GET",
993            "url": "https://example.test",
994            "requestId": "req-explicit-timeout",
995            "timeoutMs": 1234,
996        }))
997        .expect("numeric timeout should deserialize");
998
999        assert_eq!(
1000            (omitted.request_id.as_str(), omitted.timeout_ms),
1001            ("req-omitted-timeout", None)
1002        );
1003        assert_eq!(
1004            (null_timeout.request_id.as_str(), null_timeout.timeout_ms),
1005            ("req-null-timeout", None)
1006        );
1007        assert_eq!(
1008            (
1009                explicit_timeout.request_id.as_str(),
1010                explicit_timeout.timeout_ms
1011            ),
1012            ("req-explicit-timeout", Some(1234))
1013        );
1014    }
1015
1016    #[test]
1017    fn exited_notification_accepts_legacy_payload_without_sandbox_denied() {
1018        let notification: ExecExitedNotification = serde_json::from_value(serde_json::json!({
1019            "processId": "proc-1",
1020            "seq": 3,
1021            "exitCode": 1,
1022        }))
1023        .expect("legacy exited notification should deserialize");
1024
1025        assert_eq!(notification.sandbox_denied, None);
1026    }
1027}