use std::env; use std::io; use std::process::{Command, Stdio}; use std::time::Duration; use serde::{Deserialize, Serialize}; use tokio::process::Command as TokioCommand; use tokio::runtime::Builder; use tokio::time::timeout; use crate::sandbox::{ build_linux_sandbox_command, resolve_sandbox_status_for_request, FilesystemIsolationMode, SandboxConfig, SandboxStatus, }; use crate::ConfigLoader; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct BashCommandInput { pub command: String, pub timeout: Option, pub description: Option, #[serde(rename = "run_in_background")] pub run_in_background: Option, #[serde(rename = "dangerouslyDisableSandbox")] pub dangerously_disable_sandbox: Option, #[serde(rename = "namespaceRestrictions")] pub namespace_restrictions: Option, #[serde(rename = "isolateNetwork")] pub isolate_network: Option, #[serde(rename = "filesystemMode")] pub filesystem_mode: Option, #[serde(rename = "allowedMounts")] pub allowed_mounts: Option>, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct BashCommandOutput { pub stdout: String, pub stderr: String, #[serde(rename = "rawOutputPath")] pub raw_output_path: Option, pub interrupted: bool, #[serde(rename = "isImage")] pub is_image: Option, #[serde(rename = "backgroundTaskId")] pub background_task_id: Option, #[serde(rename = "backgroundedByUser")] pub backgrounded_by_user: Option, #[serde(rename = "assistantAutoBackgrounded")] pub assistant_auto_backgrounded: Option, #[serde(rename = "dangerouslyDisableSandbox")] pub dangerously_disable_sandbox: Option, #[serde(rename = "returnCodeInterpretation")] pub return_code_interpretation: Option, #[serde(rename = "noOutputExpected")] pub no_output_expected: Option, #[serde(rename = "structuredContent")] pub structured_content: Option>, #[serde(rename = "persistedOutputPath")] pub persisted_output_path: Option, #[serde(rename = "persistedOutputSize")] pub persisted_output_size: Option, #[serde(rename = "sandboxStatus")] pub sandbox_status: Option, } pub fn execute_bash(input: BashCommandInput) -> io::Result { let cwd = env::current_dir()?; let sandbox_status = sandbox_status_for_input(&input, &cwd); if input.run_in_background.unwrap_or(false) { let mut child = prepare_command(&input.command, &cwd, &sandbox_status, false); let child = child .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()) .spawn()?; return Ok(BashCommandOutput { stdout: String::new(), stderr: String::new(), raw_output_path: None, interrupted: false, is_image: None, background_task_id: Some(child.id().to_string()), backgrounded_by_user: Some(false), assistant_auto_backgrounded: Some(false), dangerously_disable_sandbox: input.dangerously_disable_sandbox, return_code_interpretation: None, no_output_expected: Some(true), structured_content: None, persisted_output_path: None, persisted_output_size: None, sandbox_status: Some(sandbox_status), }); } let runtime = Builder::new_current_thread().enable_all().build()?; runtime.block_on(execute_bash_async(input, sandbox_status, cwd)) } async fn execute_bash_async( input: BashCommandInput, sandbox_status: SandboxStatus, cwd: std::path::PathBuf, ) -> io::Result { let mut command = prepare_tokio_command(&input.command, &cwd, &sandbox_status, true); let output_result = if let Some(timeout_ms) = input.timeout { match timeout(Duration::from_millis(timeout_ms), command.output()).await { Ok(result) => (result?, false), Err(_) => { return Ok(BashCommandOutput { stdout: String::new(), stderr: format!("Command exceeded timeout of {timeout_ms} ms"), raw_output_path: None, interrupted: true, is_image: None, background_task_id: None, backgrounded_by_user: None, assistant_auto_backgrounded: None, dangerously_disable_sandbox: input.dangerously_disable_sandbox, return_code_interpretation: Some(String::from("timeout")), no_output_expected: Some(true), structured_content: None, persisted_output_path: None, persisted_output_size: None, sandbox_status: Some(sandbox_status), }); } } } else { (command.output().await?, false) }; let (output, interrupted) = output_result; let stdout = String::from_utf8_lossy(&output.stdout).into_owned(); let stderr = String::from_utf8_lossy(&output.stderr).into_owned(); let no_output_expected = Some(stdout.trim().is_empty() && stderr.trim().is_empty()); let return_code_interpretation = output.status.code().and_then(|code| { if code == 0 { None } else { Some(format!("exit_code:{code}")) } }); Ok(BashCommandOutput { stdout, stderr, raw_output_path: None, interrupted, is_image: None, background_task_id: None, backgrounded_by_user: None, assistant_auto_backgrounded: None, dangerously_disable_sandbox: input.dangerously_disable_sandbox, return_code_interpretation, no_output_expected, structured_content: None, persisted_output_path: None, persisted_output_size: None, sandbox_status: Some(sandbox_status), }) } fn sandbox_status_for_input(input: &BashCommandInput, cwd: &std::path::Path) -> SandboxStatus { let config = ConfigLoader::default_for(cwd).load().map_or_else( |_| SandboxConfig::default(), |runtime_config| runtime_config.sandbox().clone(), ); let request = config.resolve_request( input.dangerously_disable_sandbox.map(|disabled| !disabled), input.namespace_restrictions, input.isolate_network, input.filesystem_mode, input.allowed_mounts.clone(), ); resolve_sandbox_status_for_request(&request, cwd) } fn prepare_command( command: &str, cwd: &std::path::Path, sandbox_status: &SandboxStatus, create_dirs: bool, ) -> Command { if create_dirs { prepare_sandbox_dirs(cwd); } if let Some(launcher) = build_linux_sandbox_command(command, cwd, sandbox_status) { let mut prepared = Command::new(launcher.program); prepared.args(launcher.args); prepared.current_dir(cwd); prepared.envs(launcher.env); return prepared; } let mut prepared = Command::new("sh"); prepared.arg("-lc").arg(command).current_dir(cwd); if sandbox_status.filesystem_active { prepared.env("HOME", cwd.join(".sandbox-home")); prepared.env("TMPDIR", cwd.join(".sandbox-tmp")); } prepared } fn prepare_tokio_command( command: &str, cwd: &std::path::Path, sandbox_status: &SandboxStatus, create_dirs: bool, ) -> TokioCommand { if create_dirs { prepare_sandbox_dirs(cwd); } if let Some(launcher) = build_linux_sandbox_command(command, cwd, sandbox_status) { let mut prepared = TokioCommand::new(launcher.program); prepared.args(launcher.args); prepared.current_dir(cwd); prepared.envs(launcher.env); return prepared; } let mut prepared = TokioCommand::new("sh"); prepared.arg("-lc").arg(command).current_dir(cwd); if sandbox_status.filesystem_active { prepared.env("HOME", cwd.join(".sandbox-home")); prepared.env("TMPDIR", cwd.join(".sandbox-tmp")); } prepared } fn prepare_sandbox_dirs(cwd: &std::path::Path) { let _ = std::fs::create_dir_all(cwd.join(".sandbox-home")); let _ = std::fs::create_dir_all(cwd.join(".sandbox-tmp")); } #[cfg(test)] mod tests { use super::{execute_bash, BashCommandInput}; use crate::sandbox::FilesystemIsolationMode; #[test] fn executes_simple_command() { let output = execute_bash(BashCommandInput { command: String::from("printf 'hello'"), timeout: Some(1_000), description: None, run_in_background: Some(false), dangerously_disable_sandbox: Some(false), namespace_restrictions: Some(false), isolate_network: Some(false), filesystem_mode: Some(FilesystemIsolationMode::WorkspaceOnly), allowed_mounts: None, }) .expect("bash command should execute"); assert_eq!(output.stdout, "hello"); assert!(!output.interrupted); assert!(output.sandbox_status.is_some()); } #[test] fn disables_sandbox_when_requested() { let output = execute_bash(BashCommandInput { command: String::from("printf 'hello'"), timeout: Some(1_000), description: None, run_in_background: Some(false), dangerously_disable_sandbox: Some(true), namespace_restrictions: None, isolate_network: None, filesystem_mode: None, allowed_mounts: None, }) .expect("bash command should execute"); assert!(!output.sandbox_status.expect("sandbox status").enabled); } }