1use std::env;
2use std::io;
3use std::process::{Command, Stdio};
4use std::time::Duration;
5
6use serde::{Deserialize, Serialize};
7use tokio::process::Command as TokioCommand;
8use tokio::runtime::Builder;
9use tokio::time::timeout;
10
11use crate::remote::inherited_upstream_proxy_env;
12use crate::sandbox::{
13 build_sandbox_command, resolve_sandbox_status_for_request, FilesystemIsolationMode,
14 SandboxConfig, SandboxStatus,
15};
16use crate::ConfigLoader;
17
18#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
19pub struct BashCommandInput {
20 pub command: String,
21 pub timeout: Option<u64>,
22 pub description: Option<String>,
23 #[serde(rename = "run_in_background")]
24 pub run_in_background: Option<bool>,
25 #[serde(rename = "dangerouslyDisableSandbox")]
26 pub dangerously_disable_sandbox: Option<bool>,
27 #[serde(rename = "namespaceRestrictions")]
28 pub namespace_restrictions: Option<bool>,
29 #[serde(rename = "isolateNetwork")]
30 pub isolate_network: Option<bool>,
31 #[serde(rename = "filesystemMode")]
32 pub filesystem_mode: Option<FilesystemIsolationMode>,
33 #[serde(rename = "allowedMounts")]
34 pub allowed_mounts: Option<Vec<String>>,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
38pub struct BashCommandOutput {
39 pub stdout: String,
40 pub stderr: String,
41 #[serde(rename = "rawOutputPath")]
42 pub raw_output_path: Option<String>,
43 pub interrupted: bool,
44 #[serde(rename = "isImage")]
45 pub is_image: Option<bool>,
46 #[serde(rename = "backgroundTaskId")]
47 pub background_task_id: Option<String>,
48 #[serde(rename = "backgroundedByUser")]
49 pub backgrounded_by_user: Option<bool>,
50 #[serde(rename = "assistantAutoBackgrounded")]
51 pub assistant_auto_backgrounded: Option<bool>,
52 #[serde(rename = "dangerouslyDisableSandbox")]
53 pub dangerously_disable_sandbox: Option<bool>,
54 #[serde(rename = "returnCodeInterpretation")]
55 pub return_code_interpretation: Option<String>,
56 #[serde(rename = "noOutputExpected")]
57 pub no_output_expected: Option<bool>,
58 #[serde(rename = "structuredContent")]
59 pub structured_content: Option<Vec<serde_json::Value>>,
60 #[serde(rename = "persistedOutputPath")]
61 pub persisted_output_path: Option<String>,
62 #[serde(rename = "persistedOutputSize")]
63 pub persisted_output_size: Option<u64>,
64 #[serde(rename = "sandboxStatus")]
65 pub sandbox_status: Option<SandboxStatus>,
66}
67
68pub fn execute_bash(input: BashCommandInput) -> io::Result<BashCommandOutput> {
69 let cwd = env::current_dir()?;
70 let sandbox_status = sandbox_status_for_input(&input, &cwd);
71
72 if input.run_in_background.unwrap_or(false) {
73 let mut child = prepare_command(&input.command, &cwd, &sandbox_status, false);
74 let mut child = child
75 .stdin(Stdio::null())
76 .stdout(Stdio::null())
77 .stderr(Stdio::null())
78 .spawn()?;
79
80 let pid = child.id();
81 std::thread::spawn(move || {
82 let _ = child.wait();
83 });
84
85 return Ok(BashCommandOutput {
86 stdout: String::new(),
87 stderr: String::new(),
88 raw_output_path: None,
89 interrupted: false,
90 is_image: None,
91 background_task_id: Some(pid.to_string()),
92 backgrounded_by_user: Some(false),
93 assistant_auto_backgrounded: Some(false),
94 dangerously_disable_sandbox: input.dangerously_disable_sandbox,
95 return_code_interpretation: None,
96 no_output_expected: Some(true),
97 structured_content: None,
98 persisted_output_path: None,
99 persisted_output_size: None,
100 sandbox_status: Some(sandbox_status),
101 });
102 }
103
104 let runtime = Builder::new_current_thread().enable_all().build()?;
105 runtime.block_on(execute_bash_async(input, sandbox_status, cwd))
106}
107
108async fn execute_bash_async(
109 input: BashCommandInput,
110 sandbox_status: SandboxStatus,
111 cwd: std::path::PathBuf,
112) -> io::Result<BashCommandOutput> {
113 let mut command = prepare_tokio_command(&input.command, &cwd, &sandbox_status, true);
114
115 let output_result = if let Some(timeout_ms) = input.timeout {
116 let child = command
117 .stdout(std::process::Stdio::piped())
118 .stderr(std::process::Stdio::piped())
119 .kill_on_drop(true)
120 .spawn()?;
121 match timeout(Duration::from_millis(timeout_ms), child.wait_with_output()).await {
122 Ok(result) => (result?, false),
123 Err(_) => {
124 return Ok(BashCommandOutput {
125 stdout: String::new(),
126 stderr: format!("Command exceeded timeout of {timeout_ms} ms"),
127 raw_output_path: None,
128 interrupted: true,
129 is_image: None,
130 background_task_id: None,
131 backgrounded_by_user: None,
132 assistant_auto_backgrounded: None,
133 dangerously_disable_sandbox: input.dangerously_disable_sandbox,
134 return_code_interpretation: Some(String::from("timeout")),
135 no_output_expected: Some(true),
136 structured_content: None,
137 persisted_output_path: None,
138 persisted_output_size: None,
139 sandbox_status: Some(sandbox_status),
140 });
141 }
142 }
143 } else {
144 (command.output().await?, false)
145 };
146
147 let (output, interrupted) = output_result;
148 let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
149 let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
150 let no_output_expected = Some(stdout.trim().is_empty() && stderr.trim().is_empty());
151 let return_code_interpretation = output.status.code().and_then(|code| {
152 if code == 0 {
153 None
154 } else {
155 Some(format!("exit_code:{code}"))
156 }
157 });
158
159 Ok(BashCommandOutput {
160 stdout,
161 stderr,
162 raw_output_path: None,
163 interrupted,
164 is_image: None,
165 background_task_id: None,
166 backgrounded_by_user: None,
167 assistant_auto_backgrounded: None,
168 dangerously_disable_sandbox: input.dangerously_disable_sandbox,
169 return_code_interpretation,
170 no_output_expected,
171 structured_content: None,
172 persisted_output_path: None,
173 persisted_output_size: None,
174 sandbox_status: Some(sandbox_status),
175 })
176}
177
178fn sandbox_status_for_input(input: &BashCommandInput, cwd: &std::path::Path) -> SandboxStatus {
179 let config = ConfigLoader::default_for(cwd).load().map_or_else(
180 |_| SandboxConfig::default(),
181 |runtime_config| runtime_config.sandbox().clone(),
182 );
183 let request = config.resolve_request(
184 input.dangerously_disable_sandbox.map(|disabled| !disabled),
185 input.namespace_restrictions,
186 input.isolate_network,
187 input.filesystem_mode,
188 input.allowed_mounts.clone(),
189 );
190 resolve_sandbox_status_for_request(&request, cwd)
191}
192
193fn prepare_command(
194 command: &str,
195 cwd: &std::path::Path,
196 sandbox_status: &SandboxStatus,
197 create_dirs: bool,
198) -> Command {
199 if create_dirs {
200 prepare_sandbox_dirs(cwd);
201 }
202
203 if let Some(launcher) = build_sandbox_command(command, cwd, sandbox_status) {
204 let mut prepared = Command::new(launcher.program);
205 prepared.args(launcher.args);
206 prepared.current_dir(cwd);
207 prepared.envs(launcher.env);
208 apply_proxy_env(&mut prepared);
209 return prepared;
210 }
211
212 #[cfg(windows)]
213 {
214 let mut prepared = Command::new("cmd");
215 prepared.arg("/C").arg(command).current_dir(cwd);
216 if sandbox_status.filesystem_active {
217 let codineer_dir = crate::codineer_runtime_dir(cwd);
218 prepared.env("USERPROFILE", codineer_dir.join("sandbox-home"));
219 prepared.env("TEMP", codineer_dir.join("sandbox-tmp"));
220 prepared.env("TMP", codineer_dir.join("sandbox-tmp"));
221 }
222 apply_proxy_env(&mut prepared);
223 prepared
224 }
225 #[cfg(not(windows))]
226 {
227 let mut prepared = Command::new("sh");
228 prepared.arg("-lc").arg(command).current_dir(cwd);
229 if sandbox_status.filesystem_active {
230 let codineer_dir = crate::codineer_runtime_dir(cwd);
231 prepared.env("HOME", codineer_dir.join("sandbox-home"));
232 prepared.env("TMPDIR", codineer_dir.join("sandbox-tmp"));
233 }
234 apply_proxy_env(&mut prepared);
235 prepared
236 }
237}
238
239fn prepare_tokio_command(
240 command: &str,
241 cwd: &std::path::Path,
242 sandbox_status: &SandboxStatus,
243 create_dirs: bool,
244) -> TokioCommand {
245 if create_dirs {
246 prepare_sandbox_dirs(cwd);
247 }
248
249 if let Some(launcher) = build_sandbox_command(command, cwd, sandbox_status) {
250 let mut prepared = TokioCommand::new(launcher.program);
251 prepared.args(launcher.args);
252 prepared.current_dir(cwd);
253 prepared.envs(launcher.env);
254 apply_tokio_proxy_env(&mut prepared);
255 return prepared;
256 }
257
258 #[cfg(windows)]
259 {
260 let mut prepared = TokioCommand::new("cmd");
261 prepared.arg("/C").arg(command).current_dir(cwd);
262 if sandbox_status.filesystem_active {
263 let codineer_dir = crate::codineer_runtime_dir(cwd);
264 prepared.env("USERPROFILE", codineer_dir.join("sandbox-home"));
265 prepared.env("TEMP", codineer_dir.join("sandbox-tmp"));
266 prepared.env("TMP", codineer_dir.join("sandbox-tmp"));
267 }
268 apply_tokio_proxy_env(&mut prepared);
269 prepared
270 }
271 #[cfg(not(windows))]
272 {
273 let mut prepared = TokioCommand::new("sh");
274 prepared.arg("-lc").arg(command).current_dir(cwd);
275 if sandbox_status.filesystem_active {
276 let codineer_dir = crate::codineer_runtime_dir(cwd);
277 prepared.env("HOME", codineer_dir.join("sandbox-home"));
278 prepared.env("TMPDIR", codineer_dir.join("sandbox-tmp"));
279 }
280 apply_tokio_proxy_env(&mut prepared);
281 prepared
282 }
283}
284
285fn apply_proxy_env(cmd: &mut Command) {
286 let env_map = env::vars().collect();
287 let proxy_env = inherited_upstream_proxy_env(&env_map);
288 cmd.envs(proxy_env);
289}
290
291fn apply_tokio_proxy_env(cmd: &mut TokioCommand) {
292 let env_map = env::vars().collect();
293 let proxy_env = inherited_upstream_proxy_env(&env_map);
294 cmd.envs(proxy_env);
295}
296
297fn prepare_sandbox_dirs(cwd: &std::path::Path) {
298 let codineer_dir = crate::codineer_runtime_dir(cwd);
299 let _ = std::fs::create_dir_all(codineer_dir.join("sandbox-home"));
300 let _ = std::fs::create_dir_all(codineer_dir.join("sandbox-tmp"));
301}
302
303#[cfg(test)]
304#[cfg(unix)]
305mod tests {
306 use super::{execute_bash, BashCommandInput};
307 use crate::sandbox::FilesystemIsolationMode;
308
309 #[test]
310 fn executes_simple_command() {
311 let output = execute_bash(BashCommandInput {
312 command: String::from("printf 'hello'"),
313 timeout: Some(1_000),
314 description: None,
315 run_in_background: Some(false),
316 dangerously_disable_sandbox: Some(false),
317 namespace_restrictions: Some(false),
318 isolate_network: Some(false),
319 filesystem_mode: Some(FilesystemIsolationMode::WorkspaceOnly),
320 allowed_mounts: None,
321 })
322 .expect("bash command should execute");
323
324 assert_eq!(output.stdout, "hello");
325 assert!(!output.interrupted);
326 assert!(output.sandbox_status.is_some());
327 }
328
329 #[test]
330 fn disables_sandbox_when_requested() {
331 let output = execute_bash(BashCommandInput {
332 command: String::from("printf 'hello'"),
333 timeout: Some(1_000),
334 description: None,
335 run_in_background: Some(false),
336 dangerously_disable_sandbox: Some(true),
337 namespace_restrictions: None,
338 isolate_network: None,
339 filesystem_mode: None,
340 allowed_mounts: None,
341 })
342 .expect("bash command should execute");
343
344 assert!(!output.sandbox_status.expect("sandbox status").enabled);
345 }
346}