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 config_overrides,
65 feature_toggles,
66 working_dir,
67 } = request;
68
69 let working_dir = self.sandbox_working_dir(working_dir)?;
70
71 let mut process = Command::new(self.command_env.binary_path());
72 process
73 .arg("sandbox")
74 .arg(platform.subcommand())
75 .stdout(std::process::Stdio::piped())
76 .stderr(std::process::Stdio::piped())
77 .kill_on_drop(true)
78 .current_dir(&working_dir);
79
80 if full_auto {
81 process.arg("--full-auto");
82 }
83
84 if log_denials && matches!(platform, SandboxPlatform::Macos) {
85 process.arg("--log-denials");
86 }
87
88 if allow_unix_socket && matches!(platform, SandboxPlatform::Macos) {
89 process.arg("--allow-unix-socket");
90 }
91
92 for override_ in config_overrides {
93 process.arg("--config");
94 process.arg(format!("{}={}", override_.key, override_.value));
95 }
96
97 for feature in feature_toggles.enable {
98 process.arg("--enable");
99 process.arg(feature);
100 }
101
102 for feature in feature_toggles.disable {
103 process.arg("--disable");
104 process.arg(feature);
105 }
106
107 process.arg("--");
108 process.args(&command);
109
110 self.command_env.apply(&mut process)?;
111
112 let mut child = spawn_with_retry(&mut process, self.command_env.binary_path())?;
113
114 let stdout = child.stdout.take().ok_or(CodexError::StdoutUnavailable)?;
115 let stderr = child.stderr.take().ok_or(CodexError::StderrUnavailable)?;
116
117 let stdout_task = tokio::spawn(tee_stream(
118 stdout,
119 ConsoleTarget::Stdout,
120 self.mirror_stdout,
121 ));
122 let stderr_task = tokio::spawn(tee_stream(stderr, ConsoleTarget::Stderr, !self.quiet));
123
124 let wait_task = async move {
125 let status = child
126 .wait()
127 .await
128 .map_err(|source| CodexError::Wait { source })?;
129 let stdout_bytes = stdout_task
130 .await
131 .map_err(CodexError::Join)?
132 .map_err(CodexError::CaptureIo)?;
133 let stderr_bytes = stderr_task
134 .await
135 .map_err(CodexError::Join)?
136 .map_err(CodexError::CaptureIo)?;
137 Ok::<_, CodexError>((status, stdout_bytes, stderr_bytes))
138 };
139
140 let (status, stdout_bytes, stderr_bytes) = if self.timeout.is_zero() {
141 wait_task.await?
142 } else {
143 match time::timeout(self.timeout, wait_task).await {
144 Ok(result) => result?,
145 Err(_) => {
146 return Err(CodexError::Timeout {
147 timeout: self.timeout,
148 });
149 }
150 }
151 };
152
153 Ok(SandboxRun {
154 status,
155 stdout: String::from_utf8(stdout_bytes)?,
156 stderr: String::from_utf8(stderr_bytes)?,
157 })
158 }
159}