Skip to main content

codineer_runtime/
bash.rs

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        apply_proxy_env(&mut prepared);
217        prepared
218    }
219    #[cfg(not(windows))]
220    {
221        let mut prepared = Command::new("sh");
222        prepared.arg("-lc").arg(command).current_dir(cwd);
223        if sandbox_status.filesystem_active {
224            prepared.env("HOME", cwd.join(".sandbox-home"));
225            prepared.env("TMPDIR", cwd.join(".sandbox-tmp"));
226        }
227        apply_proxy_env(&mut prepared);
228        prepared
229    }
230}
231
232fn prepare_tokio_command(
233    command: &str,
234    cwd: &std::path::Path,
235    sandbox_status: &SandboxStatus,
236    create_dirs: bool,
237) -> TokioCommand {
238    if create_dirs {
239        prepare_sandbox_dirs(cwd);
240    }
241
242    if let Some(launcher) = build_sandbox_command(command, cwd, sandbox_status) {
243        let mut prepared = TokioCommand::new(launcher.program);
244        prepared.args(launcher.args);
245        prepared.current_dir(cwd);
246        prepared.envs(launcher.env);
247        apply_tokio_proxy_env(&mut prepared);
248        return prepared;
249    }
250
251    #[cfg(windows)]
252    {
253        let mut prepared = TokioCommand::new("cmd");
254        prepared.arg("/C").arg(command).current_dir(cwd);
255        apply_tokio_proxy_env(&mut prepared);
256        prepared
257    }
258    #[cfg(not(windows))]
259    {
260        let mut prepared = TokioCommand::new("sh");
261        prepared.arg("-lc").arg(command).current_dir(cwd);
262        if sandbox_status.filesystem_active {
263            prepared.env("HOME", cwd.join(".sandbox-home"));
264            prepared.env("TMPDIR", cwd.join(".sandbox-tmp"));
265        }
266        apply_tokio_proxy_env(&mut prepared);
267        prepared
268    }
269}
270
271fn apply_proxy_env(cmd: &mut Command) {
272    let env_map = env::vars().collect();
273    let proxy_env = inherited_upstream_proxy_env(&env_map);
274    cmd.envs(proxy_env);
275}
276
277fn apply_tokio_proxy_env(cmd: &mut TokioCommand) {
278    let env_map = env::vars().collect();
279    let proxy_env = inherited_upstream_proxy_env(&env_map);
280    cmd.envs(proxy_env);
281}
282
283fn prepare_sandbox_dirs(cwd: &std::path::Path) {
284    let _ = std::fs::create_dir_all(cwd.join(".sandbox-home"));
285    let _ = std::fs::create_dir_all(cwd.join(".sandbox-tmp"));
286}
287
288#[cfg(test)]
289#[cfg(unix)]
290mod tests {
291    use super::{execute_bash, BashCommandInput};
292    use crate::sandbox::FilesystemIsolationMode;
293
294    #[test]
295    fn executes_simple_command() {
296        let output = execute_bash(BashCommandInput {
297            command: String::from("printf 'hello'"),
298            timeout: Some(1_000),
299            description: None,
300            run_in_background: Some(false),
301            dangerously_disable_sandbox: Some(false),
302            namespace_restrictions: Some(false),
303            isolate_network: Some(false),
304            filesystem_mode: Some(FilesystemIsolationMode::WorkspaceOnly),
305            allowed_mounts: None,
306        })
307        .expect("bash command should execute");
308
309        assert_eq!(output.stdout, "hello");
310        assert!(!output.interrupted);
311        assert!(output.sandbox_status.is_some());
312    }
313
314    #[test]
315    fn disables_sandbox_when_requested() {
316        let output = execute_bash(BashCommandInput {
317            command: String::from("printf 'hello'"),
318            timeout: Some(1_000),
319            description: None,
320            run_in_background: Some(false),
321            dangerously_disable_sandbox: Some(true),
322            namespace_restrictions: None,
323            isolate_network: None,
324            filesystem_mode: None,
325            allowed_mounts: None,
326        })
327        .expect("bash command should execute");
328
329        assert!(!output.sandbox_status.expect("sandbox status").enabled);
330    }
331}