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