diff --git a/src/config/mod.rs b/src/config/mod.rs index 1cf2960a0..05f7d12eb 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -7,17 +7,18 @@ pub use schema::{ apply_runtime_proxy_to_builder, build_runtime_proxy_client, build_runtime_proxy_client_with_timeouts, runtime_proxy_config, set_runtime_proxy_config, AgentConfig, AuditConfig, AutonomyConfig, BackupConfig, BrowserComputerUseConfig, - BrowserConfig, BuiltinHooksConfig, ChannelsConfig, ClassificationRule, ComposioConfig, Config, - CostConfig, CronConfig, DataRetentionConfig, DelegateAgentConfig, DiscordConfig, - DockerRuntimeConfig, EdgeTtsConfig, ElevenLabsTtsConfig, EmbeddingRouteConfig, EstopConfig, - FeishuConfig, GatewayConfig, GoogleTtsConfig, HardwareConfig, HardwareTransport, - HeartbeatConfig, HooksConfig, HttpRequestConfig, IMessageConfig, IdentityConfig, LarkConfig, - MatrixConfig, McpConfig, McpServerConfig, McpTransport, MemoryConfig, Microsoft365Config, - ModelRouteConfig, MultimodalConfig, NextcloudTalkConfig, NodeTransportConfig, NodesConfig, - NotionConfig, ObservabilityConfig, OpenAiTtsConfig, OpenVpnTunnelConfig, OtpConfig, OtpMethod, - PeripheralBoardConfig, PeripheralsConfig, ProjectIntelConfig, ProxyConfig, ProxyScope, - QdrantConfig, QueryClassificationConfig, ReliabilityConfig, ResourceLimitsConfig, - RuntimeConfig, SandboxBackend, SandboxConfig, SchedulerConfig, SecretsConfig, SecurityConfig, + BrowserConfig, BuiltinHooksConfig, ChannelsConfig, ClassificationRule, CloudOpsConfig, + ComposioConfig, Config, ConversationalAiConfig, CostConfig, CronConfig, DataRetentionConfig, + DelegateAgentConfig, DiscordConfig, DockerRuntimeConfig, EdgeTtsConfig, ElevenLabsTtsConfig, + EmbeddingRouteConfig, EstopConfig, FeishuConfig, GatewayConfig, GoogleTtsConfig, + HardwareConfig, HardwareTransport, HeartbeatConfig, HooksConfig, HttpRequestConfig, + IMessageConfig, IdentityConfig, LarkConfig, MatrixConfig, McpConfig, McpServerConfig, + McpTransport, MemoryConfig, Microsoft365Config, ModelRouteConfig, MultimodalConfig, + NextcloudTalkConfig, NodeTransportConfig, NodesConfig, NotionConfig, ObservabilityConfig, + OpenAiTtsConfig, OpenVpnTunnelConfig, OtpConfig, OtpMethod, PeripheralBoardConfig, + PeripheralsConfig, ProjectIntelConfig, ProxyConfig, ProxyScope, QdrantConfig, + QueryClassificationConfig, ReliabilityConfig, ResourceLimitsConfig, RuntimeConfig, + SandboxBackend, SandboxConfig, SchedulerConfig, SecretsConfig, SecurityConfig, SecurityOpsConfig, SkillsConfig, SkillsPromptInjectionMode, SlackConfig, StorageConfig, StorageProviderConfig, StorageProviderSection, StreamMode, SwarmConfig, SwarmStrategy, TelegramConfig, ToolFilterGroup, ToolFilterGroupMode, TranscriptionConfig, TtsConfig, diff --git a/src/config/schema.rs b/src/config/schema.rs index ba9b6aa43..8cc0e57e0 100644 --- a/src/config/schema.rs +++ b/src/config/schema.rs @@ -128,6 +128,12 @@ pub struct Config { /// Data retention and purge configuration (`[data_retention]`). pub data_retention: DataRetentionConfig, + /// Cloud transformation accelerator configuration (`[cloud_ops]`). + pub cloud_ops: CloudOpsConfig, + + /// Conversational AI agent builder configuration (`[conversational_ai]`). + pub conversational_ai: ConversationalAiConfig, + pub security: SecurityConfig, /// Managed cybersecurity service configuration (`[security_ops]`). @@ -4820,6 +4826,176 @@ impl Default for NotionConfig { } } +/// +/// Controls the read-only cloud transformation analysis tools: +/// IaC review, migration assessment, cost analysis, and architecture review. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +pub struct CloudOpsConfig { + /// Enable cloud operations tools. Default: false. + #[serde(default)] + pub enabled: bool, + /// Default cloud provider for analysis context. Default: "aws". + #[serde(default = "default_cloud_ops_cloud")] + pub default_cloud: String, + /// Supported cloud providers. Default: [`aws`, `azure`, `gcp`]. + #[serde(default = "default_cloud_ops_supported_clouds")] + pub supported_clouds: Vec, + /// Supported IaC tools for review. Default: [`terraform`]. + #[serde(default = "default_cloud_ops_iac_tools")] + pub iac_tools: Vec, + /// Monthly USD threshold to flag cost items. Default: 100.0. + #[serde(default = "default_cloud_ops_cost_threshold")] + pub cost_threshold_monthly_usd: f64, + /// Well-Architected Frameworks to check against. Default: [`aws-waf`]. + #[serde(default = "default_cloud_ops_waf")] + pub well_architected_frameworks: Vec, +} + +impl Default for CloudOpsConfig { + fn default() -> Self { + Self { + enabled: false, + default_cloud: default_cloud_ops_cloud(), + supported_clouds: default_cloud_ops_supported_clouds(), + iac_tools: default_cloud_ops_iac_tools(), + cost_threshold_monthly_usd: default_cloud_ops_cost_threshold(), + well_architected_frameworks: default_cloud_ops_waf(), + } + } +} + +impl CloudOpsConfig { + pub fn validate(&self) -> Result<()> { + if self.enabled { + if self.default_cloud.trim().is_empty() { + anyhow::bail!( + "cloud_ops.default_cloud must not be empty when cloud_ops is enabled" + ); + } + if self.supported_clouds.is_empty() { + anyhow::bail!( + "cloud_ops.supported_clouds must not be empty when cloud_ops is enabled" + ); + } + for (i, cloud) in self.supported_clouds.iter().enumerate() { + if cloud.trim().is_empty() { + anyhow::bail!("cloud_ops.supported_clouds[{i}] must not be empty"); + } + } + if !self.supported_clouds.contains(&self.default_cloud) { + anyhow::bail!( + "cloud_ops.default_cloud '{}' is not in cloud_ops.supported_clouds {:?}", + self.default_cloud, + self.supported_clouds + ); + } + if self.cost_threshold_monthly_usd < 0.0 { + anyhow::bail!( + "cloud_ops.cost_threshold_monthly_usd must be non-negative, got {}", + self.cost_threshold_monthly_usd + ); + } + if self.iac_tools.is_empty() { + anyhow::bail!("cloud_ops.iac_tools must not be empty when cloud_ops is enabled"); + } + } + Ok(()) + } +} + +fn default_cloud_ops_cloud() -> String { + "aws".into() +} + +fn default_cloud_ops_supported_clouds() -> Vec { + vec!["aws".into(), "azure".into(), "gcp".into()] +} + +fn default_cloud_ops_iac_tools() -> Vec { + vec!["terraform".into()] +} + +fn default_cloud_ops_cost_threshold() -> f64 { + 100.0 +} + +fn default_cloud_ops_waf() -> Vec { + vec!["aws-waf".into()] +} + +// ── Conversational AI ────────────────────────────────────────────── + +fn default_conversational_ai_language() -> String { + "en".into() +} + +fn default_conversational_ai_supported_languages() -> Vec { + vec!["en".into(), "de".into(), "fr".into(), "it".into()] +} + +fn default_conversational_ai_escalation_threshold() -> f64 { + 0.3 +} + +fn default_conversational_ai_max_turns() -> usize { + 50 +} + +fn default_conversational_ai_timeout_secs() -> u64 { + 1800 +} + +/// Conversational AI agent builder configuration (`[conversational_ai]` section). +/// +/// Controls language detection, escalation behavior, conversation limits, and +/// analytics for conversational agent workflows. Disabled by default. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +pub struct ConversationalAiConfig { + /// Enable conversational AI features. Default: false. + #[serde(default)] + pub enabled: bool, + /// Default language for conversations (BCP-47 tag). Default: "en". + #[serde(default = "default_conversational_ai_language")] + pub default_language: String, + /// Supported languages for conversations. Default: [`en`, `de`, `fr`, `it`]. + #[serde(default = "default_conversational_ai_supported_languages")] + pub supported_languages: Vec, + /// Automatically detect user language from message content. Default: true. + #[serde(default = "default_true")] + pub auto_detect_language: bool, + /// Intent confidence below this threshold triggers escalation. Default: 0.3. + #[serde(default = "default_conversational_ai_escalation_threshold")] + pub escalation_confidence_threshold: f64, + /// Maximum conversation turns before auto-ending. Default: 50. + #[serde(default = "default_conversational_ai_max_turns")] + pub max_conversation_turns: usize, + /// Conversation timeout in seconds (inactivity). Default: 1800. + #[serde(default = "default_conversational_ai_timeout_secs")] + pub conversation_timeout_secs: u64, + /// Enable conversation analytics tracking. Default: false (privacy-by-default). + #[serde(default)] + pub analytics_enabled: bool, + /// Optional tool name for RAG-based knowledge base lookup during conversations. + #[serde(default)] + pub knowledge_base_tool: Option, +} + +impl Default for ConversationalAiConfig { + fn default() -> Self { + Self { + enabled: false, + default_language: default_conversational_ai_language(), + supported_languages: default_conversational_ai_supported_languages(), + auto_detect_language: true, + escalation_confidence_threshold: default_conversational_ai_escalation_threshold(), + max_conversation_turns: default_conversational_ai_max_turns(), + conversation_timeout_secs: default_conversational_ai_timeout_secs(), + analytics_enabled: false, + knowledge_base_tool: None, + } + } +} + // ── Security ops config ───────────────────────────────────────── /// Managed Cybersecurity Service (MCSS) dashboard agent configuration (`[security_ops]`). @@ -4903,6 +5079,8 @@ impl Default for Config { autonomy: AutonomyConfig::default(), backup: BackupConfig::default(), data_retention: DataRetentionConfig::default(), + cloud_ops: CloudOpsConfig::default(), + conversational_ai: ConversationalAiConfig::default(), security: SecurityConfig::default(), security_ops: SecurityOpsConfig::default(), runtime: RuntimeConfig::default(), @@ -6109,6 +6287,7 @@ impl Config { // Proxy (delegate to existing validation) self.proxy.validate()?; + self.cloud_ops.validate()?; // Notion if self.notion.enabled { @@ -7153,6 +7332,8 @@ default_temperature = 0.7 }, backup: BackupConfig::default(), data_retention: DataRetentionConfig::default(), + cloud_ops: CloudOpsConfig::default(), + conversational_ai: ConversationalAiConfig::default(), security: SecurityConfig::default(), security_ops: SecurityOpsConfig::default(), runtime: RuntimeConfig { @@ -7498,6 +7679,8 @@ tool_dispatcher = "xml" autonomy: AutonomyConfig::default(), backup: BackupConfig::default(), data_retention: DataRetentionConfig::default(), + cloud_ops: CloudOpsConfig::default(), + conversational_ai: ConversationalAiConfig::default(), security: SecurityConfig::default(), security_ops: SecurityOpsConfig::default(), runtime: RuntimeConfig::default(), diff --git a/src/onboard/wizard.rs b/src/onboard/wizard.rs index c1acf17f9..00b643005 100644 --- a/src/onboard/wizard.rs +++ b/src/onboard/wizard.rs @@ -145,6 +145,8 @@ pub async fn run_wizard(force: bool) -> Result { autonomy: AutonomyConfig::default(), backup: crate::config::BackupConfig::default(), data_retention: crate::config::DataRetentionConfig::default(), + cloud_ops: crate::config::CloudOpsConfig::default(), + conversational_ai: crate::config::ConversationalAiConfig::default(), security: crate::config::SecurityConfig::default(), security_ops: crate::config::SecurityOpsConfig::default(), runtime: RuntimeConfig::default(), @@ -511,6 +513,8 @@ async fn run_quick_setup_with_home( autonomy: AutonomyConfig::default(), backup: crate::config::BackupConfig::default(), data_retention: crate::config::DataRetentionConfig::default(), + cloud_ops: crate::config::CloudOpsConfig::default(), + conversational_ai: crate::config::ConversationalAiConfig::default(), security: crate::config::SecurityConfig::default(), security_ops: crate::config::SecurityOpsConfig::default(), runtime: RuntimeConfig::default(), diff --git a/src/tools/cloud_ops.rs b/src/tools/cloud_ops.rs new file mode 100644 index 000000000..3d7ce8f7f --- /dev/null +++ b/src/tools/cloud_ops.rs @@ -0,0 +1,851 @@ +//! Cloud operations advisory tool for cloud transformation analysis. +//! +//! Provides read-only analysis capabilities: IaC review, migration assessment, +//! cost analysis, and Well-Architected Framework architecture review. +//! This tool does NOT create, modify, or delete cloud resources. + +use super::traits::{Tool, ToolResult}; +use crate::config::CloudOpsConfig; +use crate::util::truncate_with_ellipsis; +use async_trait::async_trait; +use serde_json::json; + +/// Read-only cloud operations advisory tool. +/// +/// Actions: `review_iac`, `assess_migration`, `cost_analysis`, `architecture_review`. +pub struct CloudOpsTool { + config: CloudOpsConfig, +} + +impl CloudOpsTool { + pub fn new(config: CloudOpsConfig) -> Self { + Self { config } + } +} + +#[async_trait] +impl Tool for CloudOpsTool { + fn name(&self) -> &str { + "cloud_ops" + } + + fn description(&self) -> &str { + "Cloud transformation advisory tool. Analyzes IaC plans, assesses migration paths, \ + reviews costs, and checks architecture against Well-Architected Framework pillars. \ + Read-only: does not create or modify cloud resources." + } + + fn parameters_schema(&self) -> serde_json::Value { + json!({ + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": ["review_iac", "assess_migration", "cost_analysis", "architecture_review"], + "description": "The analysis action to perform." + }, + "input": { + "type": "string", + "description": "For review_iac: IaC plan text or JSON content to analyze. For assess_migration: current architecture description text. For cost_analysis: billing data as CSV/JSON text. For architecture_review: architecture description text. Note: provide text content directly, not file paths." + }, + "cloud": { + "type": "string", + "description": "Target cloud provider (aws, azure, gcp). Uses configured default if omitted." + } + }, + "required": ["action", "input"] + }) + } + + async fn execute(&self, args: serde_json::Value) -> anyhow::Result { + let action = match args.get("action") { + Some(v) => v + .as_str() + .ok_or_else(|| anyhow::anyhow!("'action' must be a string, got: {}", v))?, + None => { + return Ok(ToolResult { + success: false, + output: String::new(), + error: Some("'action' parameter is required".into()), + }); + } + }; + let input = match args.get("input") { + Some(v) => v + .as_str() + .ok_or_else(|| anyhow::anyhow!("'input' must be a string, got: {}", v))?, + None => "", + }; + let cloud = match args.get("cloud") { + Some(v) => v + .as_str() + .ok_or_else(|| anyhow::anyhow!("'cloud' must be a string, got: {}", v))?, + None => &self.config.default_cloud, + }; + + if input.is_empty() { + return Ok(ToolResult { + success: false, + output: String::new(), + error: Some("'input' parameter is required and cannot be empty".into()), + }); + } + + if !self.config.supported_clouds.contains(&cloud.to_string()) { + return Ok(ToolResult { + success: false, + output: String::new(), + error: Some(format!( + "Cloud provider '{}' is not in supported_clouds: {:?}", + cloud, self.config.supported_clouds + )), + }); + } + + match action { + "review_iac" => self.review_iac(input, cloud).await, + "assess_migration" => self.assess_migration(input, cloud).await, + "cost_analysis" => self.cost_analysis(input, cloud).await, + "architecture_review" => self.architecture_review(input, cloud).await, + _ => Ok(ToolResult { + success: false, + output: String::new(), + error: Some(format!( + "Unknown action '{}'. Valid: review_iac, assess_migration, cost_analysis, architecture_review", + action + )), + }), + } + } +} + +#[allow(clippy::unused_async)] +impl CloudOpsTool { + async fn review_iac(&self, input: &str, cloud: &str) -> anyhow::Result { + let mut findings = Vec::new(); + + // Detect IaC type from content + let iac_type = detect_iac_type(input); + + // Security findings + for finding in scan_iac_security(input) { + findings.push(finding); + } + + // Best practice findings + for finding in scan_iac_best_practices(input, cloud) { + findings.push(finding); + } + + // Cost implications + for finding in scan_iac_cost(input, cloud, self.config.cost_threshold_monthly_usd) { + findings.push(finding); + } + + let output = json!({ + "iac_type": iac_type, + "cloud": cloud, + "findings_count": findings.len(), + "findings": findings, + "supported_iac_tools": self.config.iac_tools, + }); + + Ok(ToolResult { + success: true, + output: serde_json::to_string_pretty(&output)?, + error: None, + }) + } + + async fn assess_migration(&self, input: &str, cloud: &str) -> anyhow::Result { + let recommendations = assess_migration_recommendations(input, cloud); + + let output = json!({ + "cloud": cloud, + "source_description": truncate_with_ellipsis(input, 200), + "recommendations": recommendations, + }); + + Ok(ToolResult { + success: true, + output: serde_json::to_string_pretty(&output)?, + error: None, + }) + } + + async fn cost_analysis(&self, input: &str, cloud: &str) -> anyhow::Result { + let opportunities = + analyze_cost_opportunities(input, self.config.cost_threshold_monthly_usd); + + let output = json!({ + "cloud": cloud, + "threshold_usd": self.config.cost_threshold_monthly_usd, + "opportunities_count": opportunities.len(), + "opportunities": opportunities, + }); + + Ok(ToolResult { + success: true, + output: serde_json::to_string_pretty(&output)?, + error: None, + }) + } + + async fn architecture_review(&self, input: &str, cloud: &str) -> anyhow::Result { + let frameworks = &self.config.well_architected_frameworks; + let pillars = review_architecture_pillars(input, cloud, frameworks); + + let output = json!({ + "cloud": cloud, + "frameworks": frameworks, + "pillars": pillars, + }); + + Ok(ToolResult { + success: true, + output: serde_json::to_string_pretty(&output)?, + error: None, + }) + } +} + +// ── Analysis helpers ────────────────────────────────────────────── + +fn detect_iac_type(input: &str) -> &'static str { + let lower = input.to_lowercase(); + if lower.contains("resource \"") || lower.contains("terraform") || lower.contains(".tf") { + "terraform" + } else if lower.contains("awstemplatebody") + || lower.contains("cloudformation") + || lower.contains("aws::") + { + "cloudformation" + } else if lower.contains("pulumi") { + "pulumi" + } else { + "unknown" + } +} + +/// Scan IaC content for common security issues. +fn scan_iac_security(input: &str) -> Vec { + let lower = input.to_lowercase(); + let mut findings = Vec::new(); + + let security_patterns: &[(&str, &str, &str)] = &[ + ( + "0.0.0.0/0", + "high", + "Unrestricted ingress (0.0.0.0/0) detected. Restrict CIDR ranges to known networks.", + ), + ( + "::/0", + "high", + "Unrestricted IPv6 ingress (::/0) detected. Restrict CIDR ranges.", + ), + ( + "public_access", + "medium", + "Public access setting detected. Verify this is intentional and necessary.", + ), + ( + "publicly_accessible", + "medium", + "Resource marked as publicly accessible. Ensure this is required.", + ), + ( + "encrypted = false", + "high", + "Encryption explicitly disabled. Enable encryption at rest.", + ), + ( + "\"*\"", + "medium", + "Wildcard permission detected. Follow least-privilege principle.", + ), + ( + "password", + "medium", + "Hardcoded password reference detected. Use secrets manager instead.", + ), + ( + "access_key", + "high", + "Access key reference in IaC. Use IAM roles or secrets manager.", + ), + ( + "secret_key", + "high", + "Secret key reference in IaC. Use IAM roles or secrets manager.", + ), + ]; + + for (pattern, severity, message) in security_patterns { + if lower.contains(pattern) { + findings.push(json!({ + "category": "security", + "severity": severity, + "message": message, + })); + } + } + + findings +} + +/// Scan for IaC best practice violations. +fn scan_iac_best_practices(input: &str, cloud: &str) -> Vec { + let lower = input.to_lowercase(); + let mut findings = Vec::new(); + + // Tagging + if !lower.contains("tags") && !lower.contains("tag") { + findings.push(json!({ + "category": "best_practice", + "severity": "low", + "message": "No resource tags detected. Add tags for cost allocation and resource management.", + })); + } + + // Versioning + if lower.contains("s3") && !lower.contains("versioning") { + findings.push(json!({ + "category": "best_practice", + "severity": "medium", + "message": "S3 bucket without versioning detected. Enable versioning for data protection.", + })); + } + + // Logging + if !lower.contains("logging") && !lower.contains("log_group") && !lower.contains("access_logs") + { + findings.push(json!({ + "category": "best_practice", + "severity": "low", + "message": format!("No logging configuration detected for {}. Enable access logging.", cloud), + })); + } + + // Backup + if lower.contains("rds") && !lower.contains("backup_retention") { + findings.push(json!({ + "category": "best_practice", + "severity": "medium", + "message": "RDS instance without backup retention configuration. Set backup_retention_period.", + })); + } + + findings +} + +/// Scan for cost-related observations in IaC. +/// +/// Only emits findings for resources whose estimated monthly cost exceeds +/// `threshold`. AWS-specific patterns (NAT Gateway, Elastic IP, ALB) are +/// gated behind `cloud == "aws"`. +fn scan_iac_cost(input: &str, cloud: &str, threshold: f64) -> Vec { + let lower = input.to_lowercase(); + let mut findings = Vec::new(); + + // (pattern, message, estimated_monthly_usd, aws_only) + let expensive_patterns: &[(&str, &str, f64, bool)] = &[ + ("instance_type", "Review instance sizing. Consider right-sizing or spot/preemptible instances.", 50.0, false), + ("nat_gateway", "NAT Gateway detected. These incur hourly + data transfer charges. Consider VPC endpoints for AWS services.", 45.0, true), + ("elastic_ip", "Elastic IP detected. Unused EIPs incur charges.", 5.0, true), + ("load_balancer", "Load balancer detected. Verify it is needed; consider ALB over NLB/CLB for cost.", 25.0, true), + ]; + + for (pattern, message, estimated_cost, aws_only) in expensive_patterns { + if *aws_only && cloud != "aws" { + continue; + } + if *estimated_cost < threshold { + continue; + } + if lower.contains(pattern) { + findings.push(json!({ + "category": "cost", + "severity": "info", + "message": message, + "estimated_monthly_usd": estimated_cost, + })); + } + } + + findings +} + +/// Generate migration recommendations based on architecture description. +fn assess_migration_recommendations(input: &str, cloud: &str) -> Vec { + let lower = input.to_lowercase(); + let mut recs = Vec::new(); + + let migration_patterns: &[(&str, &str, &str, &str)] = &[ + ("monolith", "Decompose into microservices or modular containers.", + "high", "Consider containerizing with ECS/EKS (AWS), AKS (Azure), or GKE (GCP)."), + ("vm", "Migrate VMs to containers or serverless where feasible.", + "medium", "Evaluate lift-and-shift to managed container services."), + ("on-premises", "Assess workloads for cloud readiness using 6 Rs framework (rehost, replatform, refactor, repurchase, retire, retain).", + "high", "Start with rehost for quick migration, then optimize."), + ("database", "Evaluate managed database services for reduced operational overhead.", + "medium", &format!("Consider managed options: RDS/Aurora (AWS), Azure SQL (Azure), Cloud SQL (GCP) for {}.", cloud)), + ("batch", "Consider serverless compute for batch workloads.", + "low", "Evaluate Lambda (AWS), Azure Functions, or Cloud Functions for event-driven batch."), + ("queue", "Evaluate managed message queue services.", + "low", "Consider SQS/SNS (AWS), Service Bus (Azure), or Pub/Sub (GCP)."), + ("storage", "Evaluate tiered object storage for cost optimization.", + "medium", "Use lifecycle policies for infrequent access data."), + ("legacy", "Assess modernization path: replatform or refactor.", + "high", "Legacy systems carry tech debt; prioritize incremental modernization."), + ]; + + for (keyword, recommendation, effort, detail) in migration_patterns { + if lower.contains(keyword) { + recs.push(json!({ + "trigger": keyword, + "recommendation": recommendation, + "effort_estimate": effort, + "detail": detail, + "target_cloud": cloud, + })); + } + } + + if recs.is_empty() { + recs.push(json!({ + "trigger": "general", + "recommendation": "Provide more detail about current architecture components for targeted recommendations.", + "effort_estimate": "unknown", + "detail": "Include details about compute, storage, networking, and data layers.", + "target_cloud": cloud, + })); + } + + recs +} + +/// Analyze billing/cost data for optimization opportunities. +fn analyze_cost_opportunities(input: &str, threshold: f64) -> Vec { + let lower = input.to_lowercase(); + let mut opportunities = Vec::new(); + + // General cost patterns + let cost_patterns: &[(&str, &str, &str)] = &[ + ("reserved", "Review reserved instance utilization. Unused reservations waste budget.", "high"), + ("on-demand", "On-demand instances detected. Evaluate savings plans or reserved instances for stable workloads.", "high"), + ("data transfer", "Data transfer costs detected. Use VPC endpoints, CDN, or regional placement to reduce.", "medium"), + ("storage", "Storage costs detected. Implement lifecycle policies and tiered storage.", "medium"), + ("idle", "Idle resources detected. Identify and terminate unused resources.", "high"), + ("unattached", "Unattached resources (volumes, IPs) detected. Clean up to reduce waste.", "medium"), + ("snapshot", "Snapshot costs detected. Review retention policies and delete stale snapshots.", "low"), + ]; + + for (pattern, suggestion, priority) in cost_patterns { + if lower.contains(pattern) { + opportunities.push(json!({ + "pattern": pattern, + "suggestion": suggestion, + "priority": priority, + "threshold_usd": threshold, + })); + } + } + + if opportunities.is_empty() { + opportunities.push(json!({ + "pattern": "general", + "suggestion": "Provide billing CSV/JSON data with service and cost columns for detailed analysis.", + "priority": "info", + "threshold_usd": threshold, + })); + } + + opportunities +} + +/// Review architecture against Well-Architected Framework pillars. +fn review_architecture_pillars( + input: &str, + cloud: &str, + _frameworks: &[String], +) -> Vec { + let lower = input.to_lowercase(); + + let pillars = vec![ + ("security", review_pillar_security(&lower, cloud)), + ("reliability", review_pillar_reliability(&lower, cloud)), + ("performance", review_pillar_performance(&lower, cloud)), + ("cost_optimization", review_pillar_cost(&lower, cloud)), + ( + "operational_excellence", + review_pillar_operations(&lower, cloud), + ), + ]; + + pillars + .into_iter() + .map(|(name, findings)| { + json!({ + "pillar": name, + "findings_count": findings.len(), + "findings": findings, + }) + }) + .collect() +} + +fn review_pillar_security(input: &str, _cloud: &str) -> Vec { + let mut findings = Vec::new(); + if !input.contains("iam") && !input.contains("identity") { + findings.push( + "No IAM/identity layer described. Define identity and access management strategy." + .into(), + ); + } + if !input.contains("encrypt") { + findings + .push("No encryption mentioned. Implement encryption at rest and in transit.".into()); + } + if !input.contains("firewall") && !input.contains("waf") && !input.contains("security group") { + findings.push( + "No network security controls described. Add WAF, security groups, or firewall rules." + .into(), + ); + } + if !input.contains("audit") && !input.contains("logging") { + findings.push( + "No audit logging described. Enable CloudTrail/Azure Monitor/Cloud Audit Logs.".into(), + ); + } + findings +} + +fn review_pillar_reliability(input: &str, _cloud: &str) -> Vec { + let mut findings = Vec::new(); + if !input.contains("multi-az") && !input.contains("multi-region") && !input.contains("redundan") + { + findings + .push("No redundancy described. Consider multi-AZ or multi-region deployment.".into()); + } + if !input.contains("backup") { + findings.push("No backup strategy described. Define RPO/RTO and backup schedules.".into()); + } + if !input.contains("auto-scal") && !input.contains("autoscal") { + findings.push( + "No auto-scaling described. Implement scaling policies for variable load.".into(), + ); + } + if !input.contains("health check") && !input.contains("monitor") { + findings.push("No health monitoring described. Add health checks and alerting.".into()); + } + findings +} + +fn review_pillar_performance(input: &str, _cloud: &str) -> Vec { + let mut findings = Vec::new(); + if !input.contains("cache") && !input.contains("cdn") { + findings + .push("No caching layer described. Consider CDN and application-level caching.".into()); + } + if !input.contains("load balanc") { + findings + .push("No load balancing described. Add load balancer for distributed traffic.".into()); + } + if !input.contains("metric") && !input.contains("benchmark") { + findings.push( + "No performance metrics described. Define SLIs/SLOs and baseline benchmarks.".into(), + ); + } + findings +} + +fn review_pillar_cost(input: &str, _cloud: &str) -> Vec { + let mut findings = Vec::new(); + if !input.contains("budget") && !input.contains("cost") { + findings + .push("No cost controls described. Set budget alerts and cost allocation tags.".into()); + } + if !input.contains("reserved") && !input.contains("savings plan") && !input.contains("spot") { + findings.push("No cost optimization strategy described. Evaluate RIs, savings plans, or spot instances.".into()); + } + if !input.contains("rightsiz") && !input.contains("right-siz") { + findings.push( + "No right-sizing mentioned. Regularly review instance utilization and downsize.".into(), + ); + } + findings +} + +fn review_pillar_operations(input: &str, _cloud: &str) -> Vec { + let mut findings = Vec::new(); + if !input.contains("iac") + && !input.contains("terraform") + && !input.contains("infrastructure as code") + { + findings.push( + "No IaC mentioned. Manage all infrastructure as code for reproducibility.".into(), + ); + } + if !input.contains("ci") && !input.contains("pipeline") && !input.contains("deploy") { + findings.push("No CI/CD described. Automate build, test, and deployment pipelines.".into()); + } + if !input.contains("runbook") && !input.contains("incident") { + findings.push( + "No incident response described. Create runbooks and incident procedures.".into(), + ); + } + findings +} + +#[cfg(test)] +mod tests { + use super::*; + + fn test_config() -> CloudOpsConfig { + CloudOpsConfig::default() + } + + #[tokio::test] + async fn review_iac_detects_security_findings() { + let tool = CloudOpsTool::new(test_config()); + let result = tool + .execute(json!({ + "action": "review_iac", + "input": "resource \"aws_security_group\" \"open\" { ingress { cidr_blocks = [\"0.0.0.0/0\"] } }" + })) + .await + .unwrap(); + + assert!(result.success); + assert!(result.output.contains("Unrestricted ingress")); + assert!(result.output.contains("high")); + } + + #[tokio::test] + async fn review_iac_detects_terraform_type() { + let tool = CloudOpsTool::new(test_config()); + let result = tool + .execute(json!({ + "action": "review_iac", + "input": "resource \"aws_instance\" \"test\" { instance_type = \"t3.micro\" tags = { Name = \"test\" } }" + })) + .await + .unwrap(); + + assert!(result.success); + assert!(result.output.contains("\"iac_type\": \"terraform\"")); + } + + #[tokio::test] + async fn review_iac_detects_encrypted_false() { + let tool = CloudOpsTool::new(test_config()); + let result = tool + .execute(json!({ + "action": "review_iac", + "input": "resource \"aws_ebs_volume\" \"vol\" { encrypted = false tags = {} }" + })) + .await + .unwrap(); + + assert!(result.success); + assert!(result.output.contains("Encryption explicitly disabled")); + } + + #[tokio::test] + async fn cost_analysis_detects_on_demand() { + let tool = CloudOpsTool::new(test_config()); + let result = tool + .execute(json!({ + "action": "cost_analysis", + "input": "service,cost\nEC2 On-Demand,5000\nS3 Storage,200" + })) + .await + .unwrap(); + + assert!(result.success); + assert!(result.output.contains("on-demand")); + assert!(result.output.contains("storage")); + } + + #[tokio::test] + async fn architecture_review_returns_all_pillars() { + let tool = CloudOpsTool::new(test_config()); + let result = tool + .execute(json!({ + "action": "architecture_review", + "input": "Web app with EC2, RDS, S3. No caching layer." + })) + .await + .unwrap(); + + assert!(result.success); + assert!(result.output.contains("security")); + assert!(result.output.contains("reliability")); + assert!(result.output.contains("performance")); + assert!(result.output.contains("cost_optimization")); + assert!(result.output.contains("operational_excellence")); + } + + #[tokio::test] + async fn assess_migration_detects_monolith() { + let tool = CloudOpsTool::new(test_config()); + let result = tool + .execute(json!({ + "action": "assess_migration", + "input": "Legacy monolith application running on VMs with on-premises database." + })) + .await + .unwrap(); + + assert!(result.success); + assert!(result.output.contains("monolith")); + assert!(result.output.contains("microservices")); + } + + #[tokio::test] + async fn empty_input_returns_error() { + let tool = CloudOpsTool::new(test_config()); + let result = tool + .execute(json!({ + "action": "review_iac", + "input": "" + })) + .await + .unwrap(); + + assert!(!result.success); + assert!(result.error.is_some()); + } + + #[tokio::test] + async fn unsupported_cloud_returns_error() { + let tool = CloudOpsTool::new(test_config()); + let result = tool + .execute(json!({ + "action": "review_iac", + "input": "some content", + "cloud": "alibaba" + })) + .await + .unwrap(); + + assert!(!result.success); + assert!(result.error.unwrap().contains("not in supported_clouds")); + } + + #[tokio::test] + async fn unknown_action_returns_error() { + let tool = CloudOpsTool::new(test_config()); + let result = tool + .execute(json!({ + "action": "deploy_everything", + "input": "some content" + })) + .await + .unwrap(); + + assert!(!result.success); + assert!(result.error.unwrap().contains("Unknown action")); + } + + #[test] + fn detect_iac_type_identifies_cloudformation() { + assert_eq!(detect_iac_type("AWS::EC2::Instance"), "cloudformation"); + } + + #[test] + fn detect_iac_type_identifies_pulumi() { + assert_eq!(detect_iac_type("import pulumi"), "pulumi"); + } + + #[test] + fn scan_iac_security_finds_wildcard_permission() { + let findings = scan_iac_security("Action: \"*\" Effect: Allow"); + assert!(!findings.is_empty()); + let msg = findings[0]["message"].as_str().unwrap(); + assert!(msg.contains("Wildcard permission")); + } + + #[test] + fn scan_iac_cost_gates_aws_patterns_for_non_aws() { + // NAT Gateway / Elastic IP / Load Balancer are AWS-only; should not appear for azure + let findings = scan_iac_cost( + "nat_gateway elastic_ip load_balancer instance_type", + "azure", + 0.0, // threshold 0 so all cost-eligible items pass + ); + for f in &findings { + let msg = f["message"].as_str().unwrap(); + assert!( + !msg.contains("NAT Gateway") && !msg.contains("Elastic IP") && !msg.contains("ALB"), + "AWS-specific finding leaked for azure: {}", + msg + ); + } + // instance_type is cloud-agnostic and should still appear + assert!(findings + .iter() + .any(|f| f["message"].as_str().unwrap().contains("instance sizing"))); + } + + #[test] + fn scan_iac_cost_respects_threshold() { + // With a high threshold, low-cost patterns should be filtered out + let findings = scan_iac_cost( + "nat_gateway elastic_ip instance_type", + "aws", + 200.0, // above all estimated costs + ); + assert!( + findings.is_empty(), + "expected no findings above threshold 200, got {:?}", + findings + ); + } + + #[tokio::test] + async fn non_string_action_returns_error() { + let tool = CloudOpsTool::new(test_config()); + let result = tool + .execute(json!({ + "action": 42, + "input": "some content" + })) + .await; + + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("'action' must be a string")); + } + + #[tokio::test] + async fn non_string_input_returns_error() { + let tool = CloudOpsTool::new(test_config()); + let result = tool + .execute(json!({ + "action": "review_iac", + "input": 123 + })) + .await; + + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("'input' must be a string")); + } + + #[tokio::test] + async fn non_string_cloud_returns_error() { + let tool = CloudOpsTool::new(test_config()); + let result = tool + .execute(json!({ + "action": "review_iac", + "input": "some content", + "cloud": true + })) + .await; + + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("'cloud' must be a string")); + } +} diff --git a/src/tools/cloud_patterns.rs b/src/tools/cloud_patterns.rs new file mode 100644 index 000000000..f6649a7ed --- /dev/null +++ b/src/tools/cloud_patterns.rs @@ -0,0 +1,412 @@ +//! Cloud pattern library for recommending cloud-native architectural patterns. +//! +//! Provides a built-in set of cloud migration and modernization patterns, +//! with pattern matching against workload descriptions. + +use super::traits::{Tool, ToolResult}; +use crate::util::truncate_with_ellipsis; +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; +use serde_json::json; + +/// A cloud architecture pattern with metadata. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CloudPattern { + pub name: String, + pub description: String, + pub cloud_providers: Vec, + pub use_case: String, + pub example_iac: String, + /// Keywords for matching against workload descriptions. + keywords: Vec, +} + +/// Tool that suggests cloud patterns given a workload description. +pub struct CloudPatternsTool { + patterns: Vec, +} + +impl CloudPatternsTool { + pub fn new() -> Self { + Self { + patterns: built_in_patterns(), + } + } +} + +#[async_trait] +impl Tool for CloudPatternsTool { + fn name(&self) -> &str { + "cloud_patterns" + } + + fn description(&self) -> &str { + "Cloud pattern library. Given a workload description, suggests applicable cloud-native \ + architectural patterns (containerization, serverless, database modernization, etc.)." + } + + fn parameters_schema(&self) -> serde_json::Value { + json!({ + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": ["match", "list"], + "description": "Action: 'match' to find patterns for a workload, 'list' to show all patterns." + }, + "workload": { + "type": "string", + "description": "Description of the workload to match patterns against (required for 'match')." + }, + "cloud": { + "type": "string", + "description": "Filter patterns by cloud provider (aws, azure, gcp). Optional." + } + }, + "required": ["action"] + }) + } + + async fn execute(&self, args: serde_json::Value) -> anyhow::Result { + let action = args + .get("action") + .and_then(|v| v.as_str()) + .unwrap_or_default(); + let workload = args + .get("workload") + .and_then(|v| v.as_str()) + .unwrap_or_default(); + let cloud_filter = args.get("cloud").and_then(|v| v.as_str()); + + match action { + "list" => { + let filtered = self.filter_by_cloud(cloud_filter); + let summaries: Vec = filtered + .iter() + .map(|p| { + json!({ + "name": p.name, + "description": p.description, + "cloud_providers": p.cloud_providers, + "use_case": p.use_case, + }) + }) + .collect(); + + let output = json!({ + "patterns_count": summaries.len(), + "patterns": summaries, + }); + + Ok(ToolResult { + success: true, + output: serde_json::to_string_pretty(&output)?, + error: None, + }) + } + "match" => { + if workload.trim().is_empty() { + return Ok(ToolResult { + success: false, + output: String::new(), + error: Some("'workload' parameter is required for 'match' action".into()), + }); + } + + let matched = self.match_patterns(workload, cloud_filter); + + let output = json!({ + "workload_summary": truncate_with_ellipsis(workload, 200), + "matched_count": matched.len(), + "matched_patterns": matched, + }); + + Ok(ToolResult { + success: true, + output: serde_json::to_string_pretty(&output)?, + error: None, + }) + } + _ => Ok(ToolResult { + success: false, + output: String::new(), + error: Some(format!("Unknown action '{}'. Valid: match, list", action)), + }), + } + } +} + +impl CloudPatternsTool { + fn filter_by_cloud(&self, cloud: Option<&str>) -> Vec<&CloudPattern> { + match cloud { + Some(c) => self + .patterns + .iter() + .filter(|p| p.cloud_providers.iter().any(|cp| cp == c)) + .collect(), + None => self.patterns.iter().collect(), + } + } + + fn match_patterns(&self, workload: &str, cloud: Option<&str>) -> Vec { + let lower = workload.to_lowercase(); + let candidates = self.filter_by_cloud(cloud); + + let mut scored: Vec<(&CloudPattern, usize)> = candidates + .into_iter() + .filter_map(|p| { + let score: usize = p + .keywords + .iter() + .filter(|kw| lower.contains(kw.as_str())) + .count(); + if score > 0 { + Some((p, score)) + } else { + None + } + }) + .collect(); + + scored.sort_by(|a, b| b.1.cmp(&a.1)); + + // Built-in IaC examples are AWS Terraform only; include them only when + // the cloud filter is unset or explicitly "aws". + let include_example = cloud.is_none() || cloud == Some("aws"); + + scored + .into_iter() + .map(|(p, score)| { + let mut entry = json!({ + "name": p.name, + "description": p.description, + "cloud_providers": p.cloud_providers, + "use_case": p.use_case, + "relevance_score": score, + }); + if include_example { + entry["example_iac"] = json!(p.example_iac); + } + entry + }) + .collect() + } +} + +fn built_in_patterns() -> Vec { + vec![ + CloudPattern { + name: "containerization".into(), + description: "Package applications into containers for portability and consistent deployment.".into(), + cloud_providers: vec!["aws".into(), "azure".into(), "gcp".into()], + use_case: "Modernizing monolithic applications, improving deployment consistency, enabling microservices.".into(), + example_iac: r#"# Terraform ECS Fargate example +resource "aws_ecs_cluster" "main" { + name = "app-cluster" +} +resource "aws_ecs_service" "app" { + cluster = aws_ecs_cluster.main.id + task_definition = aws_ecs_task_definition.app.arn + launch_type = "FARGATE" + desired_count = 2 +}"#.into(), + keywords: vec!["container".into(), "docker".into(), "monolith".into(), "microservice".into(), "ecs".into(), "aks".into(), "gke".into(), "kubernetes".into(), "k8s".into()], + }, + CloudPattern { + name: "serverless_migration".into(), + description: "Migrate event-driven or periodic workloads to serverless compute.".into(), + cloud_providers: vec!["aws".into(), "azure".into(), "gcp".into()], + use_case: "Batch jobs, API backends, event processing, cron tasks with variable load.".into(), + example_iac: r#"# Terraform Lambda example +resource "aws_lambda_function" "handler" { + function_name = "event-handler" + runtime = "python3.12" + handler = "main.handler" + filename = "handler.zip" + memory_size = 256 + timeout = 30 +}"#.into(), + keywords: vec!["serverless".into(), "lambda".into(), "function".into(), "event".into(), "batch".into(), "cron".into(), "api".into(), "webhook".into()], + }, + CloudPattern { + name: "database_modernization".into(), + description: "Migrate self-managed databases to cloud-managed services for reduced ops overhead.".into(), + cloud_providers: vec!["aws".into(), "azure".into(), "gcp".into()], + use_case: "Self-managed MySQL/PostgreSQL/SQL Server migration, NoSQL adoption, read replica scaling.".into(), + example_iac: r#"# Terraform RDS example +resource "aws_db_instance" "main" { + engine = "postgres" + engine_version = "15" + instance_class = "db.t3.medium" + allocated_storage = 100 + multi_az = true + backup_retention_period = 7 + storage_encrypted = true +}"#.into(), + keywords: vec!["database".into(), "mysql".into(), "postgres".into(), "sql".into(), "rds".into(), "nosql".into(), "dynamo".into(), "mongodb".into(), "migration".into()], + }, + CloudPattern { + name: "api_gateway".into(), + description: "Centralize API management with rate limiting, auth, and routing.".into(), + cloud_providers: vec!["aws".into(), "azure".into(), "gcp".into()], + use_case: "Public API exposure, microservice routing, API versioning, throttling.".into(), + example_iac: r#"# Terraform API Gateway example +resource "aws_apigatewayv2_api" "main" { + name = "app-api" + protocol_type = "HTTP" +} +resource "aws_apigatewayv2_stage" "prod" { + api_id = aws_apigatewayv2_api.main.id + name = "prod" + auto_deploy = true +}"#.into(), + keywords: vec!["api".into(), "gateway".into(), "rest".into(), "graphql".into(), "routing".into(), "rate limit".into(), "throttl".into()], + }, + CloudPattern { + name: "service_mesh".into(), + description: "Implement service mesh for observability, traffic management, and security between microservices.".into(), + cloud_providers: vec!["aws".into(), "azure".into(), "gcp".into()], + use_case: "Microservice communication, mTLS, traffic splitting, canary deployments.".into(), + example_iac: r#"# AWS App Mesh example +resource "aws_appmesh_mesh" "main" { + name = "app-mesh" +} +resource "aws_appmesh_virtual_service" "app" { + name = "app.local" + mesh_name = aws_appmesh_mesh.main.name +}"#.into(), + keywords: vec!["mesh".into(), "istio".into(), "envoy".into(), "sidecar".into(), "mtls".into(), "canary".into(), "traffic".into(), "microservice".into()], + }, + ] +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn built_in_patterns_are_populated() { + let patterns = built_in_patterns(); + assert_eq!(patterns.len(), 5); + let names: Vec<&str> = patterns.iter().map(|p| p.name.as_str()).collect(); + assert!(names.contains(&"containerization")); + assert!(names.contains(&"serverless_migration")); + assert!(names.contains(&"database_modernization")); + assert!(names.contains(&"api_gateway")); + assert!(names.contains(&"service_mesh")); + } + + #[tokio::test] + async fn match_returns_containerization_for_monolith() { + let tool = CloudPatternsTool::new(); + let result = tool + .execute(json!({ + "action": "match", + "workload": "We have a monolith Java application running on VMs that we want to containerize." + })) + .await + .unwrap(); + + assert!(result.success); + assert!(result.output.contains("containerization")); + } + + #[tokio::test] + async fn match_returns_serverless_for_batch_workload() { + let tool = CloudPatternsTool::new(); + let result = tool + .execute(json!({ + "action": "match", + "workload": "Batch processing cron jobs that handle event data" + })) + .await + .unwrap(); + + assert!(result.success); + assert!(result.output.contains("serverless_migration")); + } + + #[tokio::test] + async fn match_filters_by_cloud_provider() { + let tool = CloudPatternsTool::new(); + let result = tool + .execute(json!({ + "action": "match", + "workload": "Container deployment with Kubernetes", + "cloud": "aws" + })) + .await + .unwrap(); + + assert!(result.success); + assert!(result.output.contains("containerization")); + } + + #[tokio::test] + async fn list_returns_all_patterns() { + let tool = CloudPatternsTool::new(); + let result = tool + .execute(json!({ + "action": "list" + })) + .await + .unwrap(); + + assert!(result.success); + assert!(result.output.contains("\"patterns_count\": 5")); + } + + #[tokio::test] + async fn match_with_empty_workload_returns_error() { + let tool = CloudPatternsTool::new(); + let result = tool + .execute(json!({ + "action": "match", + "workload": "" + })) + .await + .unwrap(); + + assert!(!result.success); + assert!(result.error.is_some()); + } + + #[tokio::test] + async fn match_database_workload_finds_db_modernization() { + let tool = CloudPatternsTool::new(); + let result = tool + .execute(json!({ + "action": "match", + "workload": "Self-hosted PostgreSQL database needs migration to managed service" + })) + .await + .unwrap(); + + assert!(result.success); + assert!(result.output.contains("database_modernization")); + } + + #[test] + fn pattern_matching_scores_correctly() { + let tool = CloudPatternsTool::new(); + let matches = + tool.match_patterns("microservice container docker kubernetes deployment", None); + // containerization should rank highest (most keyword matches) + assert!(!matches.is_empty()); + assert_eq!(matches[0]["name"], "containerization"); + } + + #[tokio::test] + async fn unknown_action_returns_error() { + let tool = CloudPatternsTool::new(); + let result = tool + .execute(json!({ + "action": "deploy" + })) + .await + .unwrap(); + + assert!(!result.success); + assert!(result.error.unwrap().contains("Unknown action")); + } +} diff --git a/src/tools/mod.rs b/src/tools/mod.rs index 8f1f73b01..830c0f720 100644 --- a/src/tools/mod.rs +++ b/src/tools/mod.rs @@ -19,6 +19,8 @@ pub mod backup_tool; pub mod browser; pub mod browser_open; pub mod cli_discovery; +pub mod cloud_ops; +pub mod cloud_patterns; pub mod composio; pub mod content_search; pub mod cron_add; @@ -74,6 +76,8 @@ pub mod workspace_tool; pub use backup_tool::BackupTool; pub use browser::{BrowserTool, ComputerUseConfig}; pub use browser_open::BrowserOpenTool; +pub use cloud_ops::CloudOpsTool; +pub use cloud_patterns::CloudPatternsTool; pub use composio::ComposioTool; pub use content_search::ContentSearchTool; pub use cron_add::CronAddTool; @@ -405,6 +409,12 @@ pub fn all_tools_with_runtime( ))); } + // Cloud operations advisory tools (read-only analysis) + if root_config.cloud_ops.enabled { + tool_arcs.push(Arc::new(CloudOpsTool::new(root_config.cloud_ops.clone()))); + tool_arcs.push(Arc::new(CloudPatternsTool::new())); + } + // PDF extraction (feature-gated at compile time via rag-pdf) tool_arcs.push(Arc::new(PdfReadTool::new(security.clone())));