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";
43pub const CAPABILITY_ROOTS_DISCOVER_METHOD: &str = "capabilityRoots/discoverV1";
45pub const DISCOVERABLE_PLUGIN_MANIFEST_PATHS: &[&str] = &[
47 ".codex-plugin/plugin.json",
48 ".claude-plugin/plugin.json",
49 ".cursor-plugin/plugin.json",
50];
51pub const HTTP_REQUEST_METHOD: &str = "http/request";
53pub const HTTP_REQUEST_BODY_DELTA_METHOD: &str = "http/request/bodyDelta";
55pub 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
90#[serde(rename_all = "camelCase")]
91pub struct EnvironmentInfo {
92 pub shell: ShellInfo,
93 #[serde(default)]
95 pub cwd: Option<PathUri>,
96 #[serde(default)]
98 pub capabilities: EnvironmentCapabilities,
99}
100
101#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
103#[serde(rename_all = "camelCase")]
104pub struct EnvironmentCapabilities {
105 #[serde(default)]
107 pub network_proxy_launch: bool,
108}
109
110#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
116#[serde(rename_all = "camelCase")]
117pub struct EnvironmentStatus {
118 pub status: EnvironmentStatusKind,
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
123#[serde(rename_all = "camelCase")]
124pub enum EnvironmentStatusKind {
125 Ready,
127}
128
129impl EnvironmentInfo {
130 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
146#[serde(rename_all = "camelCase")]
147pub struct ShellInfo {
148 pub name: String,
150 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 pub process_id: ProcessId,
170 pub argv: Vec<String>,
171 pub cwd: PathUri,
173 #[serde(default)]
174 pub env_policy: Option<ExecEnvPolicy>,
175 pub env: HashMap<String, String>,
176 pub tty: bool,
177 #[serde(default)]
179 pub pipe_stdin: bool,
180 pub arg0: Option<String>,
183 #[serde(default)]
185 pub sandbox: Option<FileSystemSandboxContext>,
186 #[serde(default)]
188 pub enforce_managed_network: bool,
189 #[serde(default)]
194 pub managed_network: Option<ManagedNetworkSandboxContext>,
195 #[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 #[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
465#[serde(rename_all = "camelCase")]
466pub struct CapabilityRootsDiscoverParams {
467 pub roots: Vec<CapabilityRootDiscoverRequest>,
468}
469
470#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
472#[serde(rename_all = "camelCase")]
473pub struct CapabilityRootDiscoverRequest {
474 pub id: String,
476 pub path: PathUri,
478}
479
480#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
482#[serde(rename_all = "camelCase")]
483pub struct CapabilityRootsDiscoverResponse {
484 pub roots: Vec<CapabilityRootDiscovery>,
485}
486
487#[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
497#[serde(rename_all = "camelCase")]
498pub struct DiscoveredPluginFiles {
499 pub manifest: CapabilityTextFile,
500 #[serde(default)]
502 pub mcp_config: Option<CapabilityTextFile>,
503 #[serde(default)]
505 pub apps_config: Option<CapabilityTextFile>,
506}
507
508#[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#[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 #[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#[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
578#[serde(rename_all = "camelCase")]
579pub struct HttpHeader {
580 pub name: String,
582 pub value: String,
584}
585
586#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
588#[serde(rename_all = "camelCase")]
589pub enum HttpRedirectPolicy {
590 #[default]
592 Follow,
593 Stop,
595}
596
597#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
603#[serde(rename_all = "camelCase")]
604pub struct HttpRequestParams {
605 pub method: String,
607 pub url: String,
609 #[serde(default)]
611 pub headers: Vec<HttpHeader>,
612 #[serde(default, rename = "bodyBase64")]
614 pub body: Option<ByteChunk>,
615 #[serde(default, skip_serializing_if = "Option::is_none")]
620 pub timeout_ms: Option<u64>,
621 #[serde(default)]
623 pub redirect_policy: HttpRedirectPolicy,
624 pub request_id: String,
631 #[serde(default)]
633 pub stream_response: bool,
634}
635
636#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
638#[serde(rename_all = "camelCase")]
639pub struct HttpRequestResponse {
640 pub status: u16,
642 pub headers: Vec<HttpHeader>,
644 #[serde(rename = "bodyBase64")]
646 pub body: ByteChunk,
647}
648
649#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
654#[serde(rename_all = "camelCase")]
655pub struct HttpRequestBodyDeltaNotification {
656 pub request_id: String,
658 pub seq: u64,
660 #[serde(rename = "deltaBase64")]
662 pub delta: ByteChunk,
663 #[serde(default)]
665 pub done: bool,
666 #[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(¶ms).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}