Platform restructured into crates/, added AI service and detector,
migrated control-center-ui to Leptos 0.8
706 lines
21 KiB
Rust
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);
|
|
}
|
|
}
|