bash.rs 9.7 KB

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