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