use async_trait::async_trait;
use std::process::Stdio;
use tokio::process::Command;
use crate::integration::HostIntegration;
use crate::tools::{Tool, ToolResponse, ToolError, Permission, validation};
pub struct ShellCommandTool;
impl ShellCommandTool {
pub fn new() -> Self {
Self
}
}
#[async_trait]
impl Tool for ShellCommandTool {
async fn execute(
&self,
parameters: serde_json::Value,
_host: &dyn HostIntegration,
) -> Result<ToolResponse, ToolError> {
let command = validation::require_string(¶meters, "command")?;
let working_dir = validation::optional_path(¶meters, "working_dir");
let dangerous_commands = [
"rm -rf", "del /f", "format", "fdisk", "mkfs",
"shutdown", "reboot", "halt", "poweroff",
"sudo rm", "sudo del", "sudo format",
];
for dangerous in &dangerous_commands {
if command.to_lowercase().contains(dangerous) {
return Err(ToolError::SecurityViolation(
format!("Dangerous command detected: {}", dangerous)
));
}
}
let (shell, shell_arg) = if cfg!(target_os = "windows") {
("cmd", "/C")
} else {
("sh", "-c")
};
let mut cmd = Command::new(shell);
cmd.arg(shell_arg)
.arg(&command)
.stdout(Stdio::piped())
.stderr(Stdio::piped());
if let Some(dir) = working_dir {
cmd.current_dir(dir);
}
let output = cmd.output().await
.map_err(|e| ToolError::ExecutionFailed(format!("Failed to execute command: {}", e)))?;
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let mut result = String::new();
if !stdout.is_empty() {
result.push_str("STDOUT:\n");
result.push_str(&stdout);
}
if !stderr.is_empty() {
if !result.is_empty() {
result.push_str("\n\n");
}
result.push_str("STDERR:\n");
result.push_str(&stderr);
}
if result.is_empty() {
result = "Command executed successfully (no output)".to_string();
}
let metadata = serde_json::json!({
"exit_code": output.status.code(),
"success": output.status.success(),
"command": command,
});
Ok(ToolResponse::with_metadata(result, metadata))
}
fn requires_permission(&self) -> Permission {
Permission::ExecuteShell
}
fn description(&self) -> &str {
"Execute a shell command"
}
fn name(&self) -> &str {
"shell_command"
}
fn parameter_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "The shell command to execute"
},
"working_dir": {
"type": "string",
"description": "Optional working directory for the command"
}
},
"required": ["command"]
})
}
fn clone_box(&self) -> Box<dyn Tool> {
Box::new(ShellCommandTool::new())
}
}