prvng_platform/crates/rag/src/tools.rs
Jesús Pérez 09a97ac8f5
chore: update platform submodule to monorepo crates structure
Platform restructured into crates/, added AI service and detector,
       migrated control-center-ui to Leptos 0.8
2026-01-08 21:32:59 +00:00

706 lines
21 KiB
Rust

//! Claude function calling / tool use support for RAG system
//!
//! Provides a framework for Claude to invoke system operations safely with:
//! - Tool trait for extensible tool implementations
//! - Tool registry for managing available tools
//! - Security validation and rate limiting
//! - Audit logging for all tool invocations
//! - Result aggregation and error handling
use std::collections::HashMap;
use std::sync::Arc;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;
use tracing::{info, warn};
use crate::error::{RagError, Result};
/// Tool input/output types
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolInput {
/// Tool parameters as JSON
pub params: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolOutput {
/// Tool execution result
pub result: String,
/// Whether execution was successful
pub success: bool,
/// Optional error message
pub error: Option<String>,
}
/// Tool definition for Claude to understand
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolDefinition {
/// Tool name (used by Claude)
pub name: String,
/// Tool description for Claude
pub description: String,
/// JSON schema for input parameters
pub input_schema: serde_json::Value,
}
/// Tool implementation trait
#[async_trait]
pub trait RagTool: Send + Sync {
/// Get tool definition (name, description, schema)
fn definition(&self) -> ToolDefinition;
/// Execute the tool with given input
async fn execute(&self, input: ToolInput) -> Result<ToolOutput>;
/// Validate if user is authorized to call this tool
async fn validate_access(&self, user_id: &str) -> Result<bool> {
// Default: allow all authenticated users
Ok(!user_id.is_empty())
}
/// Check if tool is currently rate-limited
async fn check_rate_limit(&self, _user_id: &str) -> Result<bool> {
// Default: no rate limiting
Ok(true)
}
/// Record audit log for tool invocation
async fn audit_log(&self, user_id: &str, _input: &ToolInput, output: &ToolOutput) {
info!(
target: "rag::tools::audit",
tool = %self.definition().name,
user = user_id,
success = output.success,
"Tool execution logged"
);
}
}
/// Tool registry for managing available tools
pub struct ToolRegistry {
tools: Arc<RwLock<HashMap<String, Arc<dyn RagTool>>>>,
call_stats: Arc<RwLock<ToolCallStats>>,
}
/// Statistics for tool calls
#[derive(Debug, Clone, Default)]
pub struct ToolCallStats {
/// Total tool calls
pub total_calls: u64,
/// Successful tool calls
pub successful_calls: u64,
/// Failed tool calls
pub failed_calls: u64,
/// Blocked calls (auth/rate limit)
pub blocked_calls: u64,
/// Calls by tool name
pub calls_by_tool: HashMap<String, u64>,
}
impl ToolRegistry {
/// Create a new tool registry
pub fn new() -> Self {
Self {
tools: Arc::new(RwLock::new(HashMap::new())),
call_stats: Arc::new(RwLock::new(ToolCallStats::default())),
}
}
/// Register a new tool
pub async fn register(&self, tool: Arc<dyn RagTool>) -> Result<()> {
let def = tool.definition();
let mut tools = self.tools.write().await;
if tools.contains_key(&def.name) {
return Err(RagError::ToolError(format!(
"Tool '{}' already registered",
def.name
)));
}
tools.insert(def.name.clone(), tool);
info!("Tool registered: {}", def.name);
Ok(())
}
/// Get tool by name
pub async fn get_tool(&self, name: &str) -> Result<Arc<dyn RagTool>> {
let tools = self.tools.read().await;
tools
.get(name)
.cloned()
.ok_or_else(|| RagError::ToolError(format!("Tool '{}' not found", name)))
}
/// Get all tool definitions
pub async fn get_definitions(&self) -> Vec<ToolDefinition> {
let tools = self.tools.read().await;
tools.values().map(|t| t.definition()).collect()
}
/// Call a tool with security validation
pub async fn call_tool(
&self,
name: &str,
input: ToolInput,
user_id: &str,
) -> Result<ToolOutput> {
let tool = self.get_tool(name).await?;
// 1. Validate access
if !tool.validate_access(user_id).await? {
warn!(user = user_id, tool = name, "Tool access denied");
let mut stats = self.call_stats.write().await;
stats.blocked_calls += 1;
return Ok(ToolOutput {
result: "Access denied".to_string(),
success: false,
error: Some("User not authorized for this tool".to_string()),
});
}
// 2. Check rate limiting
if !tool.check_rate_limit(user_id).await? {
warn!(user = user_id, tool = name, "Tool rate limited");
let mut stats = self.call_stats.write().await;
stats.blocked_calls += 1;
return Ok(ToolOutput {
result: "Rate limited".to_string(),
success: false,
error: Some("Tool call rate limit exceeded".to_string()),
});
}
// 3. Execute tool
let output = tool.execute(input.clone()).await?;
// 4. Record audit log
tool.audit_log(user_id, &input, &output).await;
// 5. Update statistics
let mut stats = self.call_stats.write().await;
stats.total_calls += 1;
*stats.calls_by_tool.entry(name.to_string()).or_insert(0) += 1;
if output.success {
stats.successful_calls += 1;
} else {
stats.failed_calls += 1;
}
Ok(output)
}
/// Get tool call statistics
pub async fn get_stats(&self) -> ToolCallStats {
self.call_stats.read().await.clone()
}
/// Reset statistics
pub async fn reset_stats(&self) {
let mut stats = self.call_stats.write().await;
*stats = ToolCallStats::default();
}
}
impl Default for ToolRegistry {
fn default() -> Self {
Self::new()
}
}
// ============================================================================
// CORE TOOL IMPLEMENTATIONS
// ============================================================================
/// Create server tool - allows Claude to provision infrastructure
pub struct CreateServerTool {
description: String,
}
impl CreateServerTool {
pub fn new() -> Self {
Self {
description: "Provision a new server with specified configuration".to_string(),
}
}
}
impl Default for CreateServerTool {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl RagTool for CreateServerTool {
fn definition(&self) -> ToolDefinition {
ToolDefinition {
name: "create_server".to_string(),
description: "Create a new server with specified CPU, memory, and location".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"hostname": {
"type": "string",
"description": "Server hostname"
},
"cores": {
"type": "integer",
"description": "Number of CPU cores (1-128)",
"minimum": 1,
"maximum": 128
},
"memory_gb": {
"type": "integer",
"description": "Memory in GB (1-2048)",
"minimum": 1,
"maximum": 2048
},
"region": {
"type": "string",
"description": "Server region/zone"
}
},
"required": ["hostname", "cores", "memory_gb", "region"]
}),
}
}
async fn execute(&self, input: ToolInput) -> Result<ToolOutput> {
// Extract parameters
let hostname = input
.params
.get("hostname")
.and_then(|v| v.as_str())
.ok_or_else(|| RagError::ToolError("Missing hostname".to_string()))?;
let cores = input
.params
.get("cores")
.and_then(|v| v.as_i64())
.ok_or_else(|| RagError::ToolError("Missing cores".to_string()))?
as i32;
let memory_gb = input
.params
.get("memory_gb")
.and_then(|v| v.as_i64())
.ok_or_else(|| RagError::ToolError("Missing memory_gb".to_string()))?
as i32;
let region = input
.params
.get("region")
.and_then(|v| v.as_str())
.ok_or_else(|| RagError::ToolError("Missing region".to_string()))?;
// Validate parameters
if !(1..=128).contains(&cores) {
return Ok(ToolOutput {
result: format!("Invalid cores: {}", cores),
success: false,
error: Some("Cores must be between 1 and 128".to_string()),
});
}
if !(1..=2048).contains(&memory_gb) {
return Ok(ToolOutput {
result: format!("Invalid memory: {}GB", memory_gb),
success: false,
error: Some("Memory must be between 1 and 2048 GB".to_string()),
});
}
// In production, this would call the provisioning API
// For now, return simulation
let result = format!(
"Server '{}' created: {} cores, {}GB memory in region {}",
hostname, cores, memory_gb, region
);
info!("Server creation simulated: {}", result);
Ok(ToolOutput {
result,
success: true,
error: None,
})
}
async fn validate_access(&self, user_id: &str) -> Result<bool> {
// Only admin users can create servers
Ok(user_id.ends_with("@admin"))
}
}
/// Workspace status tool - allows Claude to check workspace info
pub struct WorkspaceStatusTool {
workspace_name: String,
}
impl WorkspaceStatusTool {
pub fn new(workspace_name: String) -> Self {
Self { workspace_name }
}
}
#[async_trait]
impl RagTool for WorkspaceStatusTool {
fn definition(&self) -> ToolDefinition {
ToolDefinition {
name: "get_workspace_status".to_string(),
description: "Get current workspace status and metrics".to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"include_metrics": {
"type": "boolean",
"description": "Include detailed metrics"
}
}
}),
}
}
async fn execute(&self, input: ToolInput) -> Result<ToolOutput> {
let include_metrics = input
.params
.get("include_metrics")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let status = if include_metrics {
format!(
"Workspace '{}': 5 servers, 3 taskservs, 2 clusters. Metrics: 99.8% uptime",
self.workspace_name
)
} else {
format!(
"Workspace '{}': 5 servers, 3 taskservs, 2 clusters",
self.workspace_name
)
};
info!("Workspace status retrieved: {}", status);
Ok(ToolOutput {
result: status,
success: true,
error: None,
})
}
}
/// Taskserv management tool - allows Claude to manage infrastructure services
pub struct TaskservManagementTool {
operations: Vec<String>,
}
impl TaskservManagementTool {
pub fn new() -> Self {
Self {
operations: vec![
"create".to_string(),
"delete".to_string(),
"update".to_string(),
"restart".to_string(),
],
}
}
}
impl Default for TaskservManagementTool {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl RagTool for TaskservManagementTool {
fn definition(&self) -> ToolDefinition {
ToolDefinition {
name: "manage_taskserv".to_string(),
description: "Manage infrastructure task services (create, delete, update, restart)"
.to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"operation": {
"type": "string",
"enum": ["create", "delete", "update", "restart"],
"description": "Operation to perform"
},
"service": {
"type": "string",
"description": "Service name (e.g., kubernetes, containerd)"
},
"version": {
"type": "string",
"description": "Service version (optional)"
}
},
"required": ["operation", "service"]
}),
}
}
async fn execute(&self, input: ToolInput) -> Result<ToolOutput> {
let operation = input
.params
.get("operation")
.and_then(|v| v.as_str())
.ok_or_else(|| RagError::ToolError("Missing operation".to_string()))?;
let service = input
.params
.get("service")
.and_then(|v| v.as_str())
.ok_or_else(|| RagError::ToolError("Missing service".to_string()))?;
// Validate operation
if !self.operations.contains(&operation.to_string()) {
return Ok(ToolOutput {
result: format!("Invalid operation: {}", operation),
success: false,
error: Some(format!(
"Operation must be one of: {}",
self.operations.join(", ")
)),
});
}
let result = format!("Taskserv '{}' operation '{}' completed", service, operation);
info!("Taskserv operation simulated: {}", result);
Ok(ToolOutput {
result,
success: true,
error: None,
})
}
async fn validate_access(&self, _user_id: &str) -> Result<bool> {
// Delete operation requires admin
Ok(true) // Simplified for now
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tool_input_creation() {
let input = ToolInput {
params: serde_json::json!({
"hostname": "server-01",
"cores": 4
}),
};
assert_eq!(input.params["hostname"], "server-01");
assert_eq!(input.params["cores"], 4);
}
#[test]
fn test_tool_output_success() {
let output = ToolOutput {
result: "Operation successful".to_string(),
success: true,
error: None,
};
assert!(output.success);
assert_eq!(output.result, "Operation successful");
assert!(output.error.is_none());
}
#[test]
fn test_tool_output_failure() {
let output = ToolOutput {
result: "Operation failed".to_string(),
success: false,
error: Some("Invalid parameters".to_string()),
};
assert!(!output.success);
assert!(output.error.is_some());
}
#[tokio::test]
async fn test_registry_creation() {
let registry = ToolRegistry::new();
let definitions = registry.get_definitions().await;
assert_eq!(definitions.len(), 0);
}
#[tokio::test]
async fn test_tool_registration() {
let registry = ToolRegistry::new();
let tool = Arc::new(CreateServerTool::new());
registry.register(tool).await.unwrap();
let definitions = registry.get_definitions().await;
assert_eq!(definitions.len(), 1);
assert_eq!(definitions[0].name, "create_server");
}
#[tokio::test]
async fn test_duplicate_registration() {
let registry = ToolRegistry::new();
let tool: Arc<dyn RagTool> = Arc::new(CreateServerTool::new());
registry.register(Arc::clone(&tool)).await.unwrap();
let result = registry.register(Arc::clone(&tool)).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_create_server_tool() {
let tool = CreateServerTool::new();
let def = tool.definition();
assert_eq!(def.name, "create_server");
assert!(def.description.contains("server"));
}
#[tokio::test]
async fn test_create_server_valid_input() {
let tool = CreateServerTool::new();
let input = ToolInput {
params: serde_json::json!({
"hostname": "web-01",
"cores": 4,
"memory_gb": 8,
"region": "us-east-1"
}),
};
let output = tool.execute(input).await.unwrap();
assert!(output.success);
assert!(output.result.contains("web-01"));
}
#[tokio::test]
async fn test_create_server_invalid_cores() {
let tool = CreateServerTool::new();
let input = ToolInput {
params: serde_json::json!({
"hostname": "web-01",
"cores": 256,
"memory_gb": 8,
"region": "us-east-1"
}),
};
let output = tool.execute(input).await.unwrap();
assert!(!output.success);
assert!(output.error.is_some());
}
#[tokio::test]
async fn test_workspace_status_tool() {
let tool = WorkspaceStatusTool::new("production".to_string());
let input = ToolInput {
params: serde_json::json!({"include_metrics": true}),
};
let output = tool.execute(input).await.unwrap();
assert!(output.success);
assert!(output.result.contains("production"));
}
#[tokio::test]
async fn test_taskserv_management_tool() {
let tool = TaskservManagementTool::new();
let input = ToolInput {
params: serde_json::json!({
"operation": "create",
"service": "kubernetes",
"version": "1.28.0"
}),
};
let output = tool.execute(input).await.unwrap();
assert!(output.success);
assert!(output.result.contains("kubernetes"));
}
#[tokio::test]
async fn test_taskserv_invalid_operation() {
let tool = TaskservManagementTool::new();
let input = ToolInput {
params: serde_json::json!({
"operation": "invalid",
"service": "kubernetes"
}),
};
let output = tool.execute(input).await.unwrap();
assert!(!output.success);
}
#[tokio::test]
async fn test_tool_registry_call() {
let registry = ToolRegistry::new();
let tool = Arc::new(WorkspaceStatusTool::new("test".to_string()));
registry.register(tool).await.unwrap();
let input = ToolInput {
params: serde_json::json!({"include_metrics": false}),
};
let output = registry
.call_tool("get_workspace_status", input, "user@example.com")
.await
.unwrap();
assert!(output.success);
}
#[tokio::test]
async fn test_create_server_access_control() {
let tool = CreateServerTool::new();
// Admin user should have access (user_id ends with @admin)
let admin_access = tool.validate_access("admin@admin").await.unwrap();
assert!(admin_access);
// Non-admin should not have access
let user_access = tool.validate_access("user@example.com").await.unwrap();
assert!(!user_access);
}
#[tokio::test]
async fn test_tool_statistics() {
let registry = ToolRegistry::new();
let tool = Arc::new(WorkspaceStatusTool::new("test".to_string()));
registry.register(tool).await.unwrap();
let input = ToolInput {
params: serde_json::json!({"include_metrics": false}),
};
registry
.call_tool("get_workspace_status", input.clone(), "user@test.com")
.await
.unwrap();
let stats = registry.get_stats().await;
assert_eq!(stats.total_calls, 1);
assert_eq!(stats.successful_calls, 1);
assert_eq!(stats.failed_calls, 0);
}
#[test]
fn test_tool_call_stats_default() {
let stats = ToolCallStats::default();
assert_eq!(stats.total_calls, 0);
assert_eq!(stats.successful_calls, 0);
assert_eq!(stats.failed_calls, 0);
assert_eq!(stats.blocked_calls, 0);
}
}