bash.rs 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. use std::io;
  2. use std::process::{Command, Stdio};
  3. use std::time::Duration;
  4. use serde::{Deserialize, Serialize};
  5. use tokio::process::Command as TokioCommand;
  6. use tokio::runtime::Builder;
  7. use tokio::time::timeout;
  8. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
  9. pub struct BashCommandInput {
  10. pub command: String,
  11. pub timeout: Option<u64>,
  12. pub description: Option<String>,
  13. #[serde(rename = "run_in_background")]
  14. pub run_in_background: Option<bool>,
  15. #[serde(rename = "dangerouslyDisableSandbox")]
  16. pub dangerously_disable_sandbox: Option<bool>,
  17. }
  18. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
  19. pub struct BashCommandOutput {
  20. pub stdout: String,
  21. pub stderr: String,
  22. #[serde(rename = "rawOutputPath")]
  23. pub raw_output_path: Option<String>,
  24. pub interrupted: bool,
  25. #[serde(rename = "isImage")]
  26. pub is_image: Option<bool>,
  27. #[serde(rename = "backgroundTaskId")]
  28. pub background_task_id: Option<String>,
  29. #[serde(rename = "backgroundedByUser")]
  30. pub backgrounded_by_user: Option<bool>,
  31. #[serde(rename = "assistantAutoBackgrounded")]
  32. pub assistant_auto_backgrounded: Option<bool>,
  33. #[serde(rename = "dangerouslyDisableSandbox")]
  34. pub dangerously_disable_sandbox: Option<bool>,
  35. #[serde(rename = "returnCodeInterpretation")]
  36. pub return_code_interpretation: Option<String>,
  37. #[serde(rename = "noOutputExpected")]
  38. pub no_output_expected: Option<bool>,
  39. #[serde(rename = "structuredContent")]
  40. pub structured_content: Option<Vec<serde_json::Value>>,
  41. #[serde(rename = "persistedOutputPath")]
  42. pub persisted_output_path: Option<String>,
  43. #[serde(rename = "persistedOutputSize")]
  44. pub persisted_output_size: Option<u64>,
  45. }
  46. pub fn execute_bash(input: BashCommandInput) -> io::Result<BashCommandOutput> {
  47. if input.run_in_background.unwrap_or(false) {
  48. let child = Command::new("sh")
  49. .arg("-lc")
  50. .arg(&input.command)
  51. .stdin(Stdio::null())
  52. .stdout(Stdio::null())
  53. .stderr(Stdio::null())
  54. .spawn()?;
  55. return Ok(BashCommandOutput {
  56. stdout: String::new(),
  57. stderr: String::new(),
  58. raw_output_path: None,
  59. interrupted: false,
  60. is_image: None,
  61. background_task_id: Some(child.id().to_string()),
  62. backgrounded_by_user: Some(false),
  63. assistant_auto_backgrounded: Some(false),
  64. dangerously_disable_sandbox: input.dangerously_disable_sandbox,
  65. return_code_interpretation: None,
  66. no_output_expected: Some(true),
  67. structured_content: None,
  68. persisted_output_path: None,
  69. persisted_output_size: None,
  70. });
  71. }
  72. let runtime = Builder::new_current_thread().enable_all().build()?;
  73. runtime.block_on(execute_bash_async(input))
  74. }
  75. async fn execute_bash_async(input: BashCommandInput) -> io::Result<BashCommandOutput> {
  76. let mut command = TokioCommand::new("sh");
  77. command.arg("-lc").arg(&input.command);
  78. let output_result = if let Some(timeout_ms) = input.timeout {
  79. match timeout(Duration::from_millis(timeout_ms), command.output()).await {
  80. Ok(result) => (result?, false),
  81. Err(_) => {
  82. return Ok(BashCommandOutput {
  83. stdout: String::new(),
  84. stderr: format!("Command exceeded timeout of {timeout_ms} ms"),
  85. raw_output_path: None,
  86. interrupted: true,
  87. is_image: None,
  88. background_task_id: None,
  89. backgrounded_by_user: None,
  90. assistant_auto_backgrounded: None,
  91. dangerously_disable_sandbox: input.dangerously_disable_sandbox,
  92. return_code_interpretation: Some(String::from("timeout")),
  93. no_output_expected: Some(true),
  94. structured_content: None,
  95. persisted_output_path: None,
  96. persisted_output_size: None,
  97. });
  98. }
  99. }
  100. } else {
  101. (command.output().await?, false)
  102. };
  103. let (output, interrupted) = output_result;
  104. let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
  105. let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
  106. let no_output_expected = Some(stdout.trim().is_empty() && stderr.trim().is_empty());
  107. let return_code_interpretation = output.status.code().and_then(|code| {
  108. if code == 0 {
  109. None
  110. } else {
  111. Some(format!("exit_code:{code}"))
  112. }
  113. });
  114. Ok(BashCommandOutput {
  115. stdout,
  116. stderr,
  117. raw_output_path: None,
  118. interrupted,
  119. is_image: None,
  120. background_task_id: None,
  121. backgrounded_by_user: None,
  122. assistant_auto_backgrounded: None,
  123. dangerously_disable_sandbox: input.dangerously_disable_sandbox,
  124. return_code_interpretation,
  125. no_output_expected,
  126. structured_content: None,
  127. persisted_output_path: None,
  128. persisted_output_size: None,
  129. })
  130. }
  131. #[cfg(test)]
  132. mod tests {
  133. use super::{execute_bash, BashCommandInput};
  134. #[test]
  135. fn executes_simple_command() {
  136. let output = execute_bash(BashCommandInput {
  137. command: String::from("printf 'hello'"),
  138. timeout: Some(1_000),
  139. description: None,
  140. run_in_background: Some(false),
  141. dangerously_disable_sandbox: Some(false),
  142. })
  143. .expect("bash command should execute");
  144. assert_eq!(output.stdout, "hello");
  145. assert!(!output.interrupted);
  146. }
  147. }