| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283 |
- 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<u64>,
- pub description: Option<String>,
- #[serde(rename = "run_in_background")]
- pub run_in_background: Option<bool>,
- #[serde(rename = "dangerouslyDisableSandbox")]
- pub dangerously_disable_sandbox: Option<bool>,
- #[serde(rename = "namespaceRestrictions")]
- pub namespace_restrictions: Option<bool>,
- #[serde(rename = "isolateNetwork")]
- pub isolate_network: Option<bool>,
- #[serde(rename = "filesystemMode")]
- pub filesystem_mode: Option<FilesystemIsolationMode>,
- #[serde(rename = "allowedMounts")]
- pub allowed_mounts: Option<Vec<String>>,
- }
- #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
- pub struct BashCommandOutput {
- pub stdout: String,
- pub stderr: String,
- #[serde(rename = "rawOutputPath")]
- pub raw_output_path: Option<String>,
- pub interrupted: bool,
- #[serde(rename = "isImage")]
- pub is_image: Option<bool>,
- #[serde(rename = "backgroundTaskId")]
- pub background_task_id: Option<String>,
- #[serde(rename = "backgroundedByUser")]
- pub backgrounded_by_user: Option<bool>,
- #[serde(rename = "assistantAutoBackgrounded")]
- pub assistant_auto_backgrounded: Option<bool>,
- #[serde(rename = "dangerouslyDisableSandbox")]
- pub dangerously_disable_sandbox: Option<bool>,
- #[serde(rename = "returnCodeInterpretation")]
- pub return_code_interpretation: Option<String>,
- #[serde(rename = "noOutputExpected")]
- pub no_output_expected: Option<bool>,
- #[serde(rename = "structuredContent")]
- pub structured_content: Option<Vec<serde_json::Value>>,
- #[serde(rename = "persistedOutputPath")]
- pub persisted_output_path: Option<String>,
- #[serde(rename = "persistedOutputSize")]
- pub persisted_output_size: Option<u64>,
- #[serde(rename = "sandboxStatus")]
- pub sandbox_status: Option<SandboxStatus>,
- }
- pub fn execute_bash(input: BashCommandInput) -> io::Result<BashCommandOutput> {
- 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<BashCommandOutput> {
- 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);
- }
- }
|