2026-07-11 13:16:10 +07:00
|
|
|
use std::process::Command;
|
|
|
|
|
use std::time::Duration;
|
|
|
|
|
use serde_json::{json, Value};
|
|
|
|
|
use anyhow::{Result, anyhow};
|
|
|
|
|
use super::Tool;
|
|
|
|
|
use super::ToolCtx;
|
|
|
|
|
|
|
|
|
|
pub struct Bash;
|
|
|
|
|
|
|
|
|
|
impl Tool for Bash {
|
|
|
|
|
fn name(&self) -> &'static str {
|
|
|
|
|
"bash"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn description(&self) -> &'static str {
|
|
|
|
|
"Execute a shell command via bash -c with catastrophic guard protection"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn parameters(&self) -> Value {
|
|
|
|
|
json!({
|
|
|
|
|
"type": "object",
|
|
|
|
|
"properties": {
|
|
|
|
|
"command": {
|
|
|
|
|
"type": "string",
|
|
|
|
|
"description": "Shell command to execute"
|
|
|
|
|
},
|
|
|
|
|
"description": {
|
|
|
|
|
"type": "string",
|
|
|
|
|
"description": "Human-readable description of what the command does"
|
|
|
|
|
},
|
|
|
|
|
"timeout": {
|
|
|
|
|
"type": "integer",
|
|
|
|
|
"description": "Timeout in milliseconds (default 120000, max 600000)"
|
2026-07-11 20:21:59 +07:00
|
|
|
},
|
|
|
|
|
"run_in_background": {
|
|
|
|
|
"type": "boolean",
|
|
|
|
|
"description": "Run the command in the background and return immediately with a job ID"
|
2026-07-11 13:16:10 +07:00
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
"required": ["command"]
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
|
|
|
|
|
let cmd = args.get("command")
|
|
|
|
|
.and_then(|v| v.as_str())
|
|
|
|
|
.ok_or_else(|| anyhow!("missing required argument: command"))?
|
|
|
|
|
.to_string();
|
|
|
|
|
let _description = args.get("description").and_then(|v| v.as_str()).unwrap_or("");
|
|
|
|
|
let timeout_ms = args.get("timeout").and_then(|v| v.as_u64()).unwrap_or(120000).min(600000);
|
|
|
|
|
let workspace_roots: Vec<&std::path::Path> = ctx.workspaces.iter().map(|p| p.as_path()).collect();
|
|
|
|
|
crate::app::catastrophic::CatastrophicGuard::check_all(&cmd, &workspace_roots)
|
|
|
|
|
.map_err(|e| anyhow!("catastrophic guard blocked: {}", e))?;
|
2026-07-12 01:25:52 +07:00
|
|
|
super::shell_filter::credentials::check_credential_read(&cmd)
|
|
|
|
|
.map_err(|e| anyhow!("blocked: {}", e))?;
|
|
|
|
|
super::shell_filter::git::check_git_destructive(&cmd)
|
|
|
|
|
.map_err(|e| anyhow!("blocked: {}", e))?;
|
2026-07-11 20:21:59 +07:00
|
|
|
let run_in_background = args.get("run_in_background").and_then(|v| v.as_bool()).unwrap_or(false);
|
|
|
|
|
if run_in_background {
|
|
|
|
|
let job = crate::app::bgbash::job::spawn_bash_job(cmd);
|
|
|
|
|
return Ok(format!("Background job: {}", job.id));
|
|
|
|
|
}
|
2026-07-11 13:16:10 +07:00
|
|
|
let mut child = Command::new("bash")
|
|
|
|
|
.arg("-c")
|
|
|
|
|
.arg(&cmd)
|
|
|
|
|
.stdout(std::process::Stdio::piped())
|
|
|
|
|
.stderr(std::process::Stdio::piped())
|
|
|
|
|
.spawn()
|
|
|
|
|
.map_err(|e| anyhow!("failed to spawn bash: {}", e))?;
|
|
|
|
|
let start = std::time::Instant::now();
|
|
|
|
|
let timeout = Duration::from_millis(timeout_ms);
|
|
|
|
|
loop {
|
|
|
|
|
match child.try_wait() {
|
|
|
|
|
Ok(Some(status)) => {
|
|
|
|
|
let elapsed = start.elapsed().as_secs_f64();
|
|
|
|
|
let output = child.wait_with_output()
|
|
|
|
|
.map_err(|e| anyhow!("failed to collect output: {}", e))?;
|
|
|
|
|
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
|
|
|
|
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
|
|
|
|
let combined = if stderr.is_empty() { stdout } else { format!("{}\n{}", stdout, stderr) };
|
|
|
|
|
let trimmed = combined.trim().to_string();
|
|
|
|
|
if status.success() {
|
|
|
|
|
return Ok(if trimmed.is_empty() {
|
|
|
|
|
format!("Command completed in {:.2}s (exit code 0)", elapsed)
|
|
|
|
|
} else {
|
|
|
|
|
format!("{}\n\nExit code: 0 ({:.2}s)", trimmed, elapsed)
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
return Ok(format!("{}\n\nExit code: {} ({:.2}s)", trimmed, status.code().unwrap_or(-1), elapsed));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Ok(None) => {
|
|
|
|
|
if start.elapsed() > timeout {
|
|
|
|
|
let _ = child.kill();
|
|
|
|
|
let _ = child.wait();
|
|
|
|
|
anyhow::bail!("command timed out after {}ms", timeout_ms);
|
|
|
|
|
}
|
|
|
|
|
std::thread::sleep(Duration::from_millis(10));
|
|
|
|
|
}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
anyhow::bail!("failed to wait for command: {}", e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|