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; #[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, } #[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, } pub fn execute_bash(input: BashCommandInput) -> io::Result { if input.run_in_background.unwrap_or(false) { let child = Command::new("sh") .arg("-lc") .arg(&input.command) .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, }); } let runtime = Builder::new_current_thread().enable_all().build()?; runtime.block_on(execute_bash_async(input)) } async fn execute_bash_async(input: BashCommandInput) -> io::Result { let mut command = TokioCommand::new("sh"); command.arg("-lc").arg(&input.command); 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, }); } } } 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, }) } #[cfg(test)] mod tests { use super::{execute_bash, BashCommandInput}; #[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), }) .expect("bash command should execute"); assert_eq!(output.stdout, "hello"); assert!(!output.interrupted); } }