use anyhow::Result;
use serde_json::{json, Value};
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::RwLock;
use tracing::{debug, error, warn};
use crate::config::TurboPropConfig;
use crate::index::PersistentChunkIndex;
use crate::mcp::error::McpError;
use super::tools::{ToolCallRequest, ToolResponse, Tools};
const TOOL_EXECUTION_TIMEOUT_SECS: u64 = 25;
pub struct ToolExecutor {
tools: Option<Tools>,
}
impl ToolExecutor {
pub fn new() -> Self {
Self { tools: None }
}
pub fn with_tools(tools: Tools) -> Self {
Self {
tools: Some(tools),
}
}
pub fn set_tools(&mut self, tools: Tools) {
self.tools = Some(tools);
}
pub async fn execute_tool(
&self,
tool_name: &str,
arguments: Value,
_index: &Arc<RwLock<Option<PersistentChunkIndex>>>,
_config: &TurboPropConfig,
_repo_path: &Path,
) -> Result<Value, ToolExecutionError> {
Self::validate_tool_parameters(&arguments)?;
match &self.tools {
Some(tools_registry) => {
self.execute_with_registry(tool_name, arguments, tools_registry)
.await
}
None => Err(ToolExecutionError::ToolNotFound(
"No tools registry configured".to_string(),
)),
}
}
async fn execute_with_registry(
&self,
tool_name: &str,
arguments: Value,
tools_registry: &Tools,
) -> Result<Value, ToolExecutionError> {
let tool_call_request = ToolCallRequest {
name: tool_name.to_string(),
arguments: serde_json::from_value(arguments).unwrap_or_default(),
};
match tokio::time::timeout(
Duration::from_secs(TOOL_EXECUTION_TIMEOUT_SECS),
tools_registry.execute_tool(tool_call_request),
)
.await
{
Ok(Ok(tool_response)) => {
if tool_response.success {
debug!("Tool executed successfully: {}", tool_name);
Ok(tool_response.content.unwrap_or(json!({})))
} else {
let error_msg = tool_response
.error
.unwrap_or_else(|| "Unknown error".to_string());
error!("Tool execution failed: {}", error_msg);
Err(ToolExecutionError::ExecutionFailed(error_msg))
}
}
Ok(Err(e)) => {
error!("Tool execution failed: {}", e);
Err(ToolExecutionError::ExecutionFailed(e.to_string()))
}
Err(_) => {
warn!("Tool execution timed out: {}", tool_name);
Err(ToolExecutionError::Timeout(TOOL_EXECUTION_TIMEOUT_SECS))
}
}
}
pub fn list_tools(&self) -> Vec<super::tools::ToolDefinition> {
match &self.tools {
Some(tools_registry) => tools_registry.list_tools(),
None => vec![], }
}
fn validate_tool_parameters(params: &Value) -> Result<(), ToolExecutionError> {
if let Some(query) = params.get("query").and_then(|q| q.as_str()) {
Self::validate_query_input(query)?;
}
if let Some(limit) = params.get("limit").and_then(|l| l.as_u64()) {
if limit > 100 {
return Err(ToolExecutionError::InvalidParameters(
"Limit too high (max 100)".to_string(),
));
}
}
if let Some(threshold) = params.get("threshold").and_then(|t| t.as_f64()) {
if threshold < 0.0 || threshold > 1.0 {
return Err(ToolExecutionError::InvalidParameters(
"Threshold must be between 0.0 and 1.0".to_string(),
));
}
}
if let Some(filter) = params.get("filter").and_then(|f| f.as_str()) {
if filter.contains("../") || filter.contains("..\\") {
return Err(ToolExecutionError::SecurityViolation(
"Suspicious filter pattern detected".to_string(),
));
}
}
Ok(())
}
fn validate_query_input(query: &str) -> Result<(), ToolExecutionError> {
if query.len() > 1000 {
return Err(ToolExecutionError::InvalidParameters(
"Query too long (max 1000 characters)".to_string(),
));
}
if query.contains("../") || query.contains("..\\") {
return Err(ToolExecutionError::SecurityViolation(
"Suspicious query pattern detected".to_string(),
));
}
if query.contains('\0') {
return Err(ToolExecutionError::SecurityViolation(
"Null bytes not allowed in query".to_string(),
));
}
if query.split_whitespace().any(|word| word.len() > 200) {
return Err(ToolExecutionError::SecurityViolation(
"Extremely long words detected".to_string(),
));
}
Ok(())
}
}
impl Default for ToolExecutor {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, thiserror::Error)]
pub enum ToolExecutionError {
#[error("Tool not found: {0}")]
ToolNotFound(String),
#[error("Tool execution failed: {0}")]
ExecutionFailed(String),
#[error("Tool execution timed out after {0} seconds")]
Timeout(u64),
#[error("Invalid parameters: {0}")]
InvalidParameters(String),
#[error("Security violation: {0}")]
SecurityViolation(String),
}
impl From<McpError> for ToolExecutionError {
fn from(error: McpError) -> Self {
match error {
McpError::InvalidPath => ToolExecutionError::InvalidParameters("Invalid path".to_string()),
McpError::PathTraversal => ToolExecutionError::SecurityViolation("Path traversal detected".to_string()),
McpError::SymlinkAttack => ToolExecutionError::SecurityViolation("Symbolic link attack detected".to_string()),
McpError::QueryTooLong { max } => ToolExecutionError::InvalidParameters(format!("Query too long (max {} characters)", max)),
McpError::SuspiciousQuery => ToolExecutionError::SecurityViolation("Suspicious query pattern detected".to_string()),
_ => ToolExecutionError::ExecutionFailed(error.to_string()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_tool_executor_creation() {
let executor = ToolExecutor::new();
drop(executor);
}
#[test]
fn test_validate_query_input_valid() {
let result = ToolExecutor::validate_query_input("normal search query");
assert!(result.is_ok());
}
#[test]
fn test_validate_query_input_path_traversal() {
let result = ToolExecutor::validate_query_input("../etc/passwd");
assert!(result.is_err());
matches!(result.unwrap_err(), ToolExecutionError::SecurityViolation(_));
}
#[test]
fn test_validate_query_input_too_long() {
let long_query = "a".repeat(1001);
let result = ToolExecutor::validate_query_input(&long_query);
assert!(result.is_err());
matches!(result.unwrap_err(), ToolExecutionError::InvalidParameters(_));
}
#[test]
fn test_validate_tool_parameters_valid() {
let params = json!({
"query": "test",
"limit": 10,
"threshold": 0.5
});
let result = ToolExecutor::validate_tool_parameters(¶ms);
assert!(result.is_ok());
}
#[test]
fn test_validate_tool_parameters_invalid_limit() {
let params = json!({
"query": "test",
"limit": 200
});
let result = ToolExecutor::validate_tool_parameters(¶ms);
assert!(result.is_err());
matches!(result.unwrap_err(), ToolExecutionError::InvalidParameters(_));
}
#[test]
fn test_list_tools_default() {
let executor = ToolExecutor::new();
let tools = executor.list_tools();
assert_eq!(tools.len(), 0); }
}