codex/commands/
sandbox.rs1use tokio::{process::Command, time};
2
3use crate::{
4 process::{spawn_with_retry, tee_stream, ConsoleTarget},
5 CodexClient, CodexError, SandboxCommandRequest, SandboxPlatform, SandboxRun, StdioToUdsRequest,
6};
7
8impl CodexClient {
9 pub fn stdio_to_uds(
15 &self,
16 request: StdioToUdsRequest,
17 ) -> Result<tokio::process::Child, CodexError> {
18 let StdioToUdsRequest {
19 socket_path,
20 working_dir,
21 } = request;
22
23 if socket_path.as_os_str().is_empty() {
24 return Err(CodexError::EmptySocketPath);
25 }
26
27 let mut command = Command::new(self.command_env.binary_path());
28 command
29 .arg("stdio-to-uds")
30 .arg(&socket_path)
31 .stdin(std::process::Stdio::piped())
32 .stdout(std::process::Stdio::piped())
33 .stderr(std::process::Stdio::piped())
34 .kill_on_drop(true)
35 .current_dir(self.sandbox_working_dir(working_dir)?);
36
37 self.command_env.apply(&mut command)?;
38
39 spawn_with_retry(&mut command, self.command_env.binary_path())
40 }
41
42 pub async fn run_sandbox(
51 &self,
52 request: SandboxCommandRequest,
53 ) -> Result<SandboxRun, CodexError> {
54 if request.command.is_empty() {
55 return Err(CodexError::EmptySandboxCommand);
56 }
57
58 let SandboxCommandRequest {
59 platform,
60 command,
61 full_auto,
62 log_denials,
63 allow_unix_socket,
64 include_managed_config,
65 permissions_profile,
66 config_overrides,
67 feature_toggles,
68 working_dir,
69 } = request;
70
71 let working_dir = self.sandbox_working_dir(working_dir)?;
72
73 let mut process = Command::new(self.command_env.binary_path());
74 process
75 .arg("sandbox")
76 .arg(platform.subcommand())
77 .stdout(std::process::Stdio::piped())
78 .stderr(std::process::Stdio::piped())
79 .kill_on_drop(true)
80 .current_dir(&working_dir);
81
82 if full_auto {
83 process.arg("--full-auto");
84 }
85
86 if log_denials && matches!(platform, SandboxPlatform::Macos) {
87 process.arg("--log-denials");
88 }
89
90 if allow_unix_socket && matches!(platform, SandboxPlatform::Macos) {
91 process.arg("--allow-unix-socket");
92 }
93
94 if include_managed_config {
95 process.arg("--include-managed-config");
96 }
97
98 if let Some(permissions_profile) = permissions_profile {
99 process
100 .arg("--permissions-profile")
101 .arg(permissions_profile);
102 }
103
104 for override_ in config_overrides {
105 process.arg("--config");
106 process.arg(format!("{}={}", override_.key, override_.value));
107 }
108
109 for feature in feature_toggles.enable {
110 process.arg("--enable");
111 process.arg(feature);
112 }
113
114 for feature in feature_toggles.disable {
115 process.arg("--disable");
116 process.arg(feature);
117 }
118
119 process.arg("--");
120 process.args(&command);
121
122 self.command_env.apply(&mut process)?;
123
124 let mut child = spawn_with_retry(&mut process, self.command_env.binary_path())?;
125
126 let stdout = child.stdout.take().ok_or(CodexError::StdoutUnavailable)?;
127 let stderr = child.stderr.take().ok_or(CodexError::StderrUnavailable)?;
128
129 let stdout_task = tokio::spawn(tee_stream(
130 stdout,
131 ConsoleTarget::Stdout,
132 self.mirror_stdout,
133 ));
134 let stderr_task = tokio::spawn(tee_stream(stderr, ConsoleTarget::Stderr, !self.quiet));
135
136 let wait_task = async move {
137 let status = child
138 .wait()
139 .await
140 .map_err(|source| CodexError::Wait { source })?;
141 let stdout_bytes = stdout_task
142 .await
143 .map_err(CodexError::Join)?
144 .map_err(CodexError::CaptureIo)?;
145 let stderr_bytes = stderr_task
146 .await
147 .map_err(CodexError::Join)?
148 .map_err(CodexError::CaptureIo)?;
149 Ok::<_, CodexError>((status, stdout_bytes, stderr_bytes))
150 };
151
152 let (status, stdout_bytes, stderr_bytes) = if self.timeout.is_zero() {
153 wait_task.await?
154 } else {
155 match time::timeout(self.timeout, wait_task).await {
156 Ok(result) => result?,
157 Err(_) => {
158 return Err(CodexError::Timeout {
159 timeout: self.timeout,
160 });
161 }
162 }
163 };
164
165 Ok(SandboxRun {
166 status,
167 stdout: String::from_utf8(stdout_bytes)?,
168 stderr: String::from_utf8(stderr_bytes)?,
169 })
170 }
171}