Compare commits
3 Commits
master
...
work/cloud
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7ad69d69a | ||
|
|
20f25ba108 | ||
|
|
c676dc325e |
@ -7,17 +7,18 @@ pub use schema::{
|
|||||||
apply_runtime_proxy_to_builder, build_runtime_proxy_client,
|
apply_runtime_proxy_to_builder, build_runtime_proxy_client,
|
||||||
build_runtime_proxy_client_with_timeouts, runtime_proxy_config, set_runtime_proxy_config,
|
build_runtime_proxy_client_with_timeouts, runtime_proxy_config, set_runtime_proxy_config,
|
||||||
AgentConfig, AuditConfig, AutonomyConfig, BackupConfig, BrowserComputerUseConfig,
|
AgentConfig, AuditConfig, AutonomyConfig, BackupConfig, BrowserComputerUseConfig,
|
||||||
BrowserConfig, BuiltinHooksConfig, ChannelsConfig, ClassificationRule, ComposioConfig, Config,
|
BrowserConfig, BuiltinHooksConfig, ChannelsConfig, ClassificationRule, CloudOpsConfig,
|
||||||
CostConfig, CronConfig, DataRetentionConfig, DelegateAgentConfig, DiscordConfig,
|
ComposioConfig, Config, ConversationalAiConfig, CostConfig, CronConfig, DataRetentionConfig,
|
||||||
DockerRuntimeConfig, EdgeTtsConfig, ElevenLabsTtsConfig, EmbeddingRouteConfig, EstopConfig,
|
DelegateAgentConfig, DiscordConfig, DockerRuntimeConfig, EdgeTtsConfig, ElevenLabsTtsConfig,
|
||||||
FeishuConfig, GatewayConfig, GoogleTtsConfig, HardwareConfig, HardwareTransport,
|
EmbeddingRouteConfig, EstopConfig, FeishuConfig, GatewayConfig, GoogleTtsConfig,
|
||||||
HeartbeatConfig, HooksConfig, HttpRequestConfig, IMessageConfig, IdentityConfig, LarkConfig,
|
HardwareConfig, HardwareTransport, HeartbeatConfig, HooksConfig, HttpRequestConfig,
|
||||||
MatrixConfig, McpConfig, McpServerConfig, McpTransport, MemoryConfig, Microsoft365Config,
|
IMessageConfig, IdentityConfig, LarkConfig, MatrixConfig, McpConfig, McpServerConfig,
|
||||||
ModelRouteConfig, MultimodalConfig, NextcloudTalkConfig, NodeTransportConfig, NodesConfig,
|
McpTransport, MemoryConfig, Microsoft365Config, ModelRouteConfig, MultimodalConfig,
|
||||||
NotionConfig, ObservabilityConfig, OpenAiTtsConfig, OpenVpnTunnelConfig, OtpConfig, OtpMethod,
|
NextcloudTalkConfig, NodeTransportConfig, NodesConfig, NotionConfig, ObservabilityConfig,
|
||||||
PeripheralBoardConfig, PeripheralsConfig, ProjectIntelConfig, ProxyConfig, ProxyScope,
|
OpenAiTtsConfig, OpenVpnTunnelConfig, OtpConfig, OtpMethod, PeripheralBoardConfig,
|
||||||
QdrantConfig, QueryClassificationConfig, ReliabilityConfig, ResourceLimitsConfig,
|
PeripheralsConfig, ProjectIntelConfig, ProxyConfig, ProxyScope, QdrantConfig,
|
||||||
RuntimeConfig, SandboxBackend, SandboxConfig, SchedulerConfig, SecretsConfig, SecurityConfig,
|
QueryClassificationConfig, ReliabilityConfig, ResourceLimitsConfig, RuntimeConfig,
|
||||||
|
SandboxBackend, SandboxConfig, SchedulerConfig, SecretsConfig, SecurityConfig,
|
||||||
SecurityOpsConfig, SkillsConfig, SkillsPromptInjectionMode, SlackConfig, StorageConfig,
|
SecurityOpsConfig, SkillsConfig, SkillsPromptInjectionMode, SlackConfig, StorageConfig,
|
||||||
StorageProviderConfig, StorageProviderSection, StreamMode, SwarmConfig, SwarmStrategy,
|
StorageProviderConfig, StorageProviderSection, StreamMode, SwarmConfig, SwarmStrategy,
|
||||||
TelegramConfig, ToolFilterGroup, ToolFilterGroupMode, TranscriptionConfig, TtsConfig,
|
TelegramConfig, ToolFilterGroup, ToolFilterGroupMode, TranscriptionConfig, TtsConfig,
|
||||||
|
|||||||
@ -128,6 +128,12 @@ pub struct Config {
|
|||||||
/// Data retention and purge configuration (`[data_retention]`).
|
/// Data retention and purge configuration (`[data_retention]`).
|
||||||
pub data_retention: DataRetentionConfig,
|
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,
|
pub security: SecurityConfig,
|
||||||
|
|
||||||
/// Managed cybersecurity service configuration (`[security_ops]`).
|
/// 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<String>,
|
||||||
|
/// Supported IaC tools for review. Default: [`terraform`].
|
||||||
|
#[serde(default = "default_cloud_ops_iac_tools")]
|
||||||
|
pub iac_tools: Vec<String>,
|
||||||
|
/// 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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<String> {
|
||||||
|
vec!["aws".into(), "azure".into(), "gcp".into()]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_cloud_ops_iac_tools() -> Vec<String> {
|
||||||
|
vec!["terraform".into()]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_cloud_ops_cost_threshold() -> f64 {
|
||||||
|
100.0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_cloud_ops_waf() -> Vec<String> {
|
||||||
|
vec!["aws-waf".into()]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Conversational AI ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
fn default_conversational_ai_language() -> String {
|
||||||
|
"en".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_conversational_ai_supported_languages() -> Vec<String> {
|
||||||
|
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<String>,
|
||||||
|
/// 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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ─────────────────────────────────────────
|
// ── Security ops config ─────────────────────────────────────────
|
||||||
|
|
||||||
/// Managed Cybersecurity Service (MCSS) dashboard agent configuration (`[security_ops]`).
|
/// Managed Cybersecurity Service (MCSS) dashboard agent configuration (`[security_ops]`).
|
||||||
@ -4903,6 +5079,8 @@ impl Default for Config {
|
|||||||
autonomy: AutonomyConfig::default(),
|
autonomy: AutonomyConfig::default(),
|
||||||
backup: BackupConfig::default(),
|
backup: BackupConfig::default(),
|
||||||
data_retention: DataRetentionConfig::default(),
|
data_retention: DataRetentionConfig::default(),
|
||||||
|
cloud_ops: CloudOpsConfig::default(),
|
||||||
|
conversational_ai: ConversationalAiConfig::default(),
|
||||||
security: SecurityConfig::default(),
|
security: SecurityConfig::default(),
|
||||||
security_ops: SecurityOpsConfig::default(),
|
security_ops: SecurityOpsConfig::default(),
|
||||||
runtime: RuntimeConfig::default(),
|
runtime: RuntimeConfig::default(),
|
||||||
@ -6109,6 +6287,7 @@ impl Config {
|
|||||||
|
|
||||||
// Proxy (delegate to existing validation)
|
// Proxy (delegate to existing validation)
|
||||||
self.proxy.validate()?;
|
self.proxy.validate()?;
|
||||||
|
self.cloud_ops.validate()?;
|
||||||
|
|
||||||
// Notion
|
// Notion
|
||||||
if self.notion.enabled {
|
if self.notion.enabled {
|
||||||
@ -7153,6 +7332,8 @@ default_temperature = 0.7
|
|||||||
},
|
},
|
||||||
backup: BackupConfig::default(),
|
backup: BackupConfig::default(),
|
||||||
data_retention: DataRetentionConfig::default(),
|
data_retention: DataRetentionConfig::default(),
|
||||||
|
cloud_ops: CloudOpsConfig::default(),
|
||||||
|
conversational_ai: ConversationalAiConfig::default(),
|
||||||
security: SecurityConfig::default(),
|
security: SecurityConfig::default(),
|
||||||
security_ops: SecurityOpsConfig::default(),
|
security_ops: SecurityOpsConfig::default(),
|
||||||
runtime: RuntimeConfig {
|
runtime: RuntimeConfig {
|
||||||
@ -7498,6 +7679,8 @@ tool_dispatcher = "xml"
|
|||||||
autonomy: AutonomyConfig::default(),
|
autonomy: AutonomyConfig::default(),
|
||||||
backup: BackupConfig::default(),
|
backup: BackupConfig::default(),
|
||||||
data_retention: DataRetentionConfig::default(),
|
data_retention: DataRetentionConfig::default(),
|
||||||
|
cloud_ops: CloudOpsConfig::default(),
|
||||||
|
conversational_ai: ConversationalAiConfig::default(),
|
||||||
security: SecurityConfig::default(),
|
security: SecurityConfig::default(),
|
||||||
security_ops: SecurityOpsConfig::default(),
|
security_ops: SecurityOpsConfig::default(),
|
||||||
runtime: RuntimeConfig::default(),
|
runtime: RuntimeConfig::default(),
|
||||||
|
|||||||
@ -145,6 +145,8 @@ pub async fn run_wizard(force: bool) -> Result<Config> {
|
|||||||
autonomy: AutonomyConfig::default(),
|
autonomy: AutonomyConfig::default(),
|
||||||
backup: crate::config::BackupConfig::default(),
|
backup: crate::config::BackupConfig::default(),
|
||||||
data_retention: crate::config::DataRetentionConfig::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: crate::config::SecurityConfig::default(),
|
||||||
security_ops: crate::config::SecurityOpsConfig::default(),
|
security_ops: crate::config::SecurityOpsConfig::default(),
|
||||||
runtime: RuntimeConfig::default(),
|
runtime: RuntimeConfig::default(),
|
||||||
@ -511,6 +513,8 @@ async fn run_quick_setup_with_home(
|
|||||||
autonomy: AutonomyConfig::default(),
|
autonomy: AutonomyConfig::default(),
|
||||||
backup: crate::config::BackupConfig::default(),
|
backup: crate::config::BackupConfig::default(),
|
||||||
data_retention: crate::config::DataRetentionConfig::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: crate::config::SecurityConfig::default(),
|
||||||
security_ops: crate::config::SecurityOpsConfig::default(),
|
security_ops: crate::config::SecurityOpsConfig::default(),
|
||||||
runtime: RuntimeConfig::default(),
|
runtime: RuntimeConfig::default(),
|
||||||
|
|||||||
851
src/tools/cloud_ops.rs
Normal file
851
src/tools/cloud_ops.rs
Normal file
@ -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<ToolResult> {
|
||||||
|
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<ToolResult> {
|
||||||
|
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<ToolResult> {
|
||||||
|
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<ToolResult> {
|
||||||
|
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<ToolResult> {
|
||||||
|
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<serde_json::Value> {
|
||||||
|
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<serde_json::Value> {
|
||||||
|
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<serde_json::Value> {
|
||||||
|
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<serde_json::Value> {
|
||||||
|
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<serde_json::Value> {
|
||||||
|
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<serde_json::Value> {
|
||||||
|
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<String> {
|
||||||
|
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<String> {
|
||||||
|
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<String> {
|
||||||
|
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<String> {
|
||||||
|
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<String> {
|
||||||
|
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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
412
src/tools/cloud_patterns.rs
Normal file
412
src/tools/cloud_patterns.rs
Normal file
@ -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<String>,
|
||||||
|
pub use_case: String,
|
||||||
|
pub example_iac: String,
|
||||||
|
/// Keywords for matching against workload descriptions.
|
||||||
|
keywords: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tool that suggests cloud patterns given a workload description.
|
||||||
|
pub struct CloudPatternsTool {
|
||||||
|
patterns: Vec<CloudPattern>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<ToolResult> {
|
||||||
|
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<serde_json::Value> = 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<serde_json::Value> {
|
||||||
|
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<CloudPattern> {
|
||||||
|
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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -19,6 +19,8 @@ pub mod backup_tool;
|
|||||||
pub mod browser;
|
pub mod browser;
|
||||||
pub mod browser_open;
|
pub mod browser_open;
|
||||||
pub mod cli_discovery;
|
pub mod cli_discovery;
|
||||||
|
pub mod cloud_ops;
|
||||||
|
pub mod cloud_patterns;
|
||||||
pub mod composio;
|
pub mod composio;
|
||||||
pub mod content_search;
|
pub mod content_search;
|
||||||
pub mod cron_add;
|
pub mod cron_add;
|
||||||
@ -74,6 +76,8 @@ pub mod workspace_tool;
|
|||||||
pub use backup_tool::BackupTool;
|
pub use backup_tool::BackupTool;
|
||||||
pub use browser::{BrowserTool, ComputerUseConfig};
|
pub use browser::{BrowserTool, ComputerUseConfig};
|
||||||
pub use browser_open::BrowserOpenTool;
|
pub use browser_open::BrowserOpenTool;
|
||||||
|
pub use cloud_ops::CloudOpsTool;
|
||||||
|
pub use cloud_patterns::CloudPatternsTool;
|
||||||
pub use composio::ComposioTool;
|
pub use composio::ComposioTool;
|
||||||
pub use content_search::ContentSearchTool;
|
pub use content_search::ContentSearchTool;
|
||||||
pub use cron_add::CronAddTool;
|
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)
|
// PDF extraction (feature-gated at compile time via rag-pdf)
|
||||||
tool_arcs.push(Arc::new(PdfReadTool::new(security.clone())));
|
tool_arcs.push(Arc::new(PdfReadTool::new(security.clone())));
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user