use anyhow::Result;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::time::Duration;
use tokio::process::Command;
use super::{Tool, ToolOutput};
const MAX_OUTPUT_BYTES: usize = 16_384;
#[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<String>,
#[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<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<Value>,
}
fn truncate_output(s: &str) -> String {
if s.len() <= MAX_OUTPUT_BYTES {
return s.to_string();
}
let mut end = MAX_OUTPUT_BYTES;
while end > 0 && !s.is_char_boundary(end) {
end -= 1;
}
let mut truncated = s[..end].to_string();
truncated.push_str("\n\n[output truncated — exceeded 16384 bytes]");
truncated
}
pub struct BashTool;
impl BashTool {
pub fn new() -> Self {
Self
}
}
impl Default for BashTool {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl Tool for BashTool {
fn name(&self) -> &str {
"bash"
}
fn description(&self) -> &str {
"Execute a shell command in the current workspace."
}
fn parameters_schema(&self) -> Value {
serde_json::json!({
"type": "object",
"properties": {
"command": { "type": "string" },
"timeout": { "type": "integer", "minimum": 1 },
"description": { "type": "string" },
"run_in_background": { "type": "boolean" },
"dangerouslyDisableSandbox": { "type": "boolean" },
"namespaceRestrictions": { "type": "boolean" },
"isolateNetwork": { "type": "boolean" },
"filesystemMode": { "type": "string", "enum": ["off", "workspace-only", "allow-list"] },
"allowedMounts": { "type": "array", "items": { "type": "string" } }
},
"required": ["command"],
"additionalProperties": false
})
}
async fn execute(&self, input: Value) -> Result<ToolOutput> {
let bash_input: BashCommandInput = serde_json::from_value(input)?;
let timeout_ms = bash_input.timeout.unwrap_or(30_000);
let (shell, flag) = if cfg!(target_os = "windows") {
("cmd", "/C")
} else {
("sh", "-c")
};
if bash_input.run_in_background.unwrap_or(false) {
let session_dir = std::env::var("MYAPP_SESSION_DIR")
.map(std::path::PathBuf::from)
.unwrap_or_else(|_| std::path::PathBuf::from(".sessions"));
let task_id = crate::tools::task_manager::TaskManager::global()
.spawn_task(bash_input.command, &session_dir)?;
let log_path = session_dir.join("logs").join(format!("{}.log", task_id));
let output = BashCommandOutput {
stdout: String::new(),
stderr: String::new(),
raw_output_path: None,
interrupted: false,
is_image: None,
background_task_id: Some(task_id),
backgrounded_by_user: Some(false),
assistant_auto_backgrounded: Some(false),
dangerously_disable_sandbox: bash_input.dangerously_disable_sandbox,
return_code_interpretation: None,
no_output_expected: Some(true),
structured_content: None,
persisted_output_path: Some(log_path.to_string_lossy().to_string()),
persisted_output_size: None,
sandbox_status: None,
};
let serialized = serde_json::to_string_pretty(&output)?;
return Ok(ToolOutput::success(serialized));
}
let result = tokio::time::timeout(
Duration::from_millis(timeout_ms),
Command::new(shell).arg(flag).arg(&bash_input.command).output(),
)
.await;
match result {
Ok(Ok(output)) => {
let stdout = truncate_output(&String::from_utf8_lossy(&output.stdout));
let stderr = truncate_output(&String::from_utf8_lossy(&output.stderr));
let return_code_interpretation = output.status.code().and_then(|code| {
if code == 0 {
None
} else {
Some(format!("exit_code:{code}"))
}
});
let no_output_expected = Some(stdout.trim().is_empty() && stderr.trim().is_empty());
let command_output = BashCommandOutput {
stdout,
stderr,
raw_output_path: None,
interrupted: false,
is_image: None,
background_task_id: None,
backgrounded_by_user: None,
assistant_auto_backgrounded: None,
dangerously_disable_sandbox: bash_input.dangerously_disable_sandbox,
return_code_interpretation,
no_output_expected,
structured_content: None,
persisted_output_path: None,
persisted_output_size: None,
sandbox_status: None,
};
let serialized = serde_json::to_string_pretty(&command_output)?;
if output.status.success() {
Ok(ToolOutput::success(serialized))
} else {
Ok(ToolOutput::error(serialized))
}
}
Ok(Err(e)) => {
let command_output = BashCommandOutput {
stdout: String::new(),
stderr: format!("Failed to execute command: {e}"),
raw_output_path: None,
interrupted: false,
is_image: None,
background_task_id: None,
backgrounded_by_user: None,
assistant_auto_backgrounded: None,
dangerously_disable_sandbox: bash_input.dangerously_disable_sandbox,
return_code_interpretation: Some("execution_failed".to_string()),
no_output_expected: Some(false),
structured_content: None,
persisted_output_path: None,
persisted_output_size: None,
sandbox_status: None,
};
let serialized = serde_json::to_string_pretty(&command_output)?;
Ok(ToolOutput::error(serialized))
}
Err(_) => {
let command_output = 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: bash_input.dangerously_disable_sandbox,
return_code_interpretation: Some("timeout".to_string()),
no_output_expected: Some(false),
structured_content: None,
persisted_output_path: None,
persisted_output_size: None,
sandbox_status: None,
};
let serialized = serde_json::to_string_pretty(&command_output)?;
Ok(ToolOutput::error(serialized))
}
}
}
}