use async_trait::async_trait;
use limit_agent::error::AgentError;
use limit_agent::Tool;
use serde_json::Value;
use std::path::Path;
use std::time::Duration;
use tokio::process::Command;
use tokio::time::timeout;
pub struct BashTool;
impl BashTool {
pub fn new() -> Self {
BashTool
}
const DEFAULT_TIMEOUT_SECS: u64 = 60;
fn is_dangerous_command(command: &str) -> bool {
let lower_cmd = command.to_lowercase();
let dangerous_patterns = [
"rm -rf /", "rm -rf /*", ":(){ :|:& };:", "dd if=/dev/zero", "mkfs.", "mv / /dev/null", "chmod -R 777 /", "chown -R", "killall -9", "kill -9 -1", "shred", "> /dev/sda", "wget http://", "curl http://", ];
dangerous_patterns
.iter()
.any(|pattern| lower_cmd.contains(pattern))
}
}
impl Default for BashTool {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl Tool for BashTool {
fn name(&self) -> &str {
"bash"
}
async fn execute(&self, args: Value) -> Result<Value, AgentError> {
let command = args
.get("command")
.and_then(|v| v.as_str())
.ok_or_else(|| AgentError::ToolError("Missing 'command' argument".to_string()))?;
if Self::is_dangerous_command(command) {
return Err(AgentError::ToolError(format!(
"Dangerous command blocked: {}",
command
)));
}
let workdir = args.get("workdir").and_then(|v| v.as_str()).unwrap_or(".");
if !Path::new(workdir).exists() {
return Err(AgentError::ToolError(format!(
"Working directory does not exist: {}",
workdir
)));
}
let timeout_secs = args
.get("timeout")
.and_then(|v| v.as_u64())
.unwrap_or(Self::DEFAULT_TIMEOUT_SECS);
let result = timeout(
Duration::from_secs(timeout_secs),
Command::new("sh")
.args(["-c", command])
.current_dir(workdir)
.output(),
)
.await;
match result {
Ok(output) => {
let output = output.map_err(|e| {
AgentError::ToolError(format!("Failed to execute command: {}", e))
})?;
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let exit_code = output.status.code().unwrap_or(-1);
Ok(serde_json::json!({
"stdout": stdout,
"stderr": stderr,
"exit_code": exit_code
}))
}
Err(_) => Err(AgentError::ToolError(format!(
"Command timed out after {} seconds",
timeout_secs
))),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_bash_tool_name() {
let tool = BashTool::new();
assert_eq!(tool.name(), "bash");
}
#[tokio::test]
async fn test_bash_tool_default() {
let tool = BashTool;
assert_eq!(tool.name(), "bash");
}
#[tokio::test]
async fn test_bash_tool_execute_simple() {
let tool = BashTool::new();
let args = serde_json::json!({
"command": "echo 'hello world'"
});
let result = tool.execute(args).await.unwrap();
assert_eq!(result["stdout"], "hello world\n");
assert_eq!(result["exit_code"], 0);
assert!(result["stderr"].as_str().unwrap().is_empty());
}
#[tokio::test]
async fn test_bash_tool_execute_with_stderr() {
let tool = BashTool::new();
let args = serde_json::json!({
"command": "echo 'error' >&2; exit 1"
});
let result = tool.execute(args).await.unwrap();
assert_eq!(result["stderr"], "error\n");
assert_eq!(result["exit_code"], 1);
}
#[tokio::test]
async fn test_bash_tool_missing_command() {
let tool = BashTool::new();
let args = serde_json::json!({});
let result = tool.execute(args).await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("Missing 'command'"));
}
#[tokio::test]
async fn test_bash_tool_dangerous_command_blocked() {
let tool = BashTool::new();
let args = serde_json::json!({
"command": "rm -rf /"
});
let result = tool.execute(args).await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("Dangerous command blocked"));
}
#[tokio::test]
async fn test_bash_tool_fork_bomb_blocked() {
let tool = BashTool::new();
let args = serde_json::json!({
"command": ":(){ :|:& };:"
});
let result = tool.execute(args).await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("Dangerous command blocked"));
}
#[tokio::test]
async fn test_bash_tool_timeout() {
let tool = BashTool::new();
let args = serde_json::json!({
"command": "sleep 10",
"timeout": 1
});
let result = tool.execute(args).await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("timed out"));
}
#[tokio::test]
async fn test_bash_tool_invalid_workdir() {
let tool = BashTool::new();
let args = serde_json::json!({
"command": "echo test",
"workdir": "/nonexistent/directory"
});
let result = tool.execute(args).await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("Working directory does not exist"));
}
#[tokio::test]
async fn test_bash_tool_current_dir() {
let tool = BashTool::new();
let args = serde_json::json!({
"command": "pwd"
});
let result = tool.execute(args).await.unwrap();
assert!(!result["stdout"].as_str().unwrap().is_empty());
assert_eq!(result["exit_code"], 0);
}
}