From 5c2db107809c701a8a660001f3bfb1dcd9f637d4 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Tue, 30 Dec 2025 22:06:07 +0000 Subject: [PATCH] fix: Gate AI subsystems and intelligence endpoints on AI enabled state. Related to #885 - Add IsAIEnabled() method to AISettingsHandler for consistent checks - Gate baseline learning, pattern detector, and correlation detector initialization in StartPatrol() on AI being enabled - Add AI enabled checks to all /api/ai/intelligence/* endpoints as defense-in-depth - Return empty results with "AI is not enabled" message when AI is disabled This ensures no AI-related data is collected, persisted, or returned when AI is disabled, preventing the "undismissable alerts" issue where old AI findings would appear. --- internal/api/ai_handlers.go | 5 + internal/api/ai_intelligence_handlers.go | 90 +++++++++++++++ internal/api/router.go | 140 ++++++++++++----------- 3 files changed, 169 insertions(+), 66 deletions(-) diff --git a/internal/api/ai_handlers.go b/internal/api/ai_handlers.go index e7f37bfba..911296d45 100644 --- a/internal/api/ai_handlers.go +++ b/internal/api/ai_handlers.go @@ -78,6 +78,11 @@ func (h *AISettingsHandler) StartPatrol(ctx context.Context) { h.aiService.StartPatrol(ctx) } +// IsAIEnabled returns true if AI features are enabled +func (h *AISettingsHandler) IsAIEnabled() bool { + return h.aiService.IsEnabled() +} + // SetPatrolThresholdProvider sets the threshold provider for the patrol service func (h *AISettingsHandler) SetPatrolThresholdProvider(provider ai.ThresholdProvider) { h.aiService.SetPatrolThresholdProvider(provider) diff --git a/internal/api/ai_intelligence_handlers.go b/internal/api/ai_intelligence_handlers.go index 8cc175fc2..5d9865cbd 100644 --- a/internal/api/ai_intelligence_handlers.go +++ b/internal/api/ai_intelligence_handlers.go @@ -20,6 +20,17 @@ func (h *AISettingsHandler) HandleGetPatterns(w http.ResponseWriter, r *http.Req return } + // AI must be enabled to return intelligence data + if !h.aiService.IsEnabled() { + if err := utils.WriteJSONResponse(w, map[string]interface{}{ + "patterns": []interface{}{}, + "message": "AI is not enabled", + }); err != nil { + log.Error().Err(err).Msg("Failed to write patterns response") + } + return + } + patrol := h.aiService.GetPatrolService() if patrol == nil { if err := utils.WriteJSONResponse(w, map[string]interface{}{ @@ -92,6 +103,17 @@ func (h *AISettingsHandler) HandleGetPredictions(w http.ResponseWriter, r *http. return } + // AI must be enabled to return intelligence data + if !h.aiService.IsEnabled() { + if err := utils.WriteJSONResponse(w, map[string]interface{}{ + "predictions": []interface{}{}, + "message": "AI is not enabled", + }); err != nil { + log.Error().Err(err).Msg("Failed to write predictions response") + } + return + } + patrol := h.aiService.GetPatrolService() if patrol == nil { if err := utils.WriteJSONResponse(w, map[string]interface{}{ @@ -166,6 +188,17 @@ func (h *AISettingsHandler) HandleGetCorrelations(w http.ResponseWriter, r *http return } + // AI must be enabled to return intelligence data + if !h.aiService.IsEnabled() { + if err := utils.WriteJSONResponse(w, map[string]interface{}{ + "correlations": []interface{}{}, + "message": "AI is not enabled", + }); err != nil { + log.Error().Err(err).Msg("Failed to write correlations response") + } + return + } + patrol := h.aiService.GetPatrolService() if patrol == nil { if err := utils.WriteJSONResponse(w, map[string]interface{}{ @@ -244,6 +277,17 @@ func (h *AISettingsHandler) HandleGetRecentChanges(w http.ResponseWriter, r *htt return } + // AI must be enabled to return intelligence data + if !h.aiService.IsEnabled() { + if err := utils.WriteJSONResponse(w, map[string]interface{}{ + "changes": []interface{}{}, + "message": "AI is not enabled", + }); err != nil { + log.Error().Err(err).Msg("Failed to write changes response") + } + return + } + patrol := h.aiService.GetPatrolService() if patrol == nil { if err := utils.WriteJSONResponse(w, map[string]interface{}{ @@ -322,6 +366,17 @@ func (h *AISettingsHandler) HandleGetBaselines(w http.ResponseWriter, r *http.Re return } + // AI must be enabled to return intelligence data + if !h.aiService.IsEnabled() { + if err := utils.WriteJSONResponse(w, map[string]interface{}{ + "baselines": []interface{}{}, + "message": "AI is not enabled", + }); err != nil { + log.Error().Err(err).Msg("Failed to write baselines response") + } + return + } + patrol := h.aiService.GetPatrolService() if patrol == nil { if err := utils.WriteJSONResponse(w, map[string]interface{}{ @@ -395,6 +450,17 @@ func (h *AISettingsHandler) HandleGetRemediations(w http.ResponseWriter, r *http return } + // AI must be enabled to return intelligence data + if !h.aiService.IsEnabled() { + if err := utils.WriteJSONResponse(w, map[string]interface{}{ + "remediations": []interface{}{}, + "message": "AI is not enabled", + }); err != nil { + log.Error().Err(err).Msg("Failed to write remediations response") + } + return + } + patrol := h.aiService.GetPatrolService() if patrol == nil { if err := utils.WriteJSONResponse(w, map[string]interface{}{ @@ -539,6 +605,17 @@ func (h *AISettingsHandler) HandleGetAnomalies(w http.ResponseWriter, r *http.Re return } + // AI must be enabled to return intelligence data + if !h.aiService.IsEnabled() { + if err := utils.WriteJSONResponse(w, map[string]interface{}{ + "anomalies": []interface{}{}, + "message": "AI is not enabled", + }); err != nil { + log.Error().Err(err).Msg("Failed to write anomalies response") + } + return + } + patrol := h.aiService.GetPatrolService() if patrol == nil { if err := utils.WriteJSONResponse(w, map[string]interface{}{ @@ -771,6 +848,19 @@ func (h *AISettingsHandler) HandleGetLearningStatus(w http.ResponseWriter, r *ht return } + // AI must be enabled to return learning status + if !h.aiService.IsEnabled() { + if err := utils.WriteJSONResponse(w, map[string]interface{}{ + "resources_baselined": 0, + "total_metrics": 0, + "status": "ai_disabled", + "message": "AI is not enabled", + }); err != nil { + log.Error().Err(err).Msg("Failed to write learning status response") + } + return + } + patrol := h.aiService.GetPatrolService() if patrol == nil { if err := utils.WriteJSONResponse(w, map[string]interface{}{ diff --git a/internal/api/router.go b/internal/api/router.go index d720af4a1..bbe5a965f 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -1588,18 +1588,22 @@ func (r *Router) StartPatrol(ctx context.Context) { r.aiSettingsHandler.SetMetricsHistoryProvider(adapter) } - // Initialize baseline store for anomaly detection - // Uses config dir for persistence - baselineCfg := ai.DefaultBaselineConfig() - if r.persistence != nil { - baselineCfg.DataDir = r.persistence.DataDir() - } - baselineStore := ai.NewBaselineStore(baselineCfg) - if baselineStore != nil { - r.aiSettingsHandler.SetBaselineStore(baselineStore) + // Only initialize baseline learning if AI is enabled + // This prevents anomaly data from being collected and displayed when AI is disabled + if r.aiSettingsHandler.IsAIEnabled() { + // Initialize baseline store for anomaly detection + // Uses config dir for persistence + baselineCfg := ai.DefaultBaselineConfig() + if r.persistence != nil { + baselineCfg.DataDir = r.persistence.DataDir() + } + baselineStore := ai.NewBaselineStore(baselineCfg) + if baselineStore != nil { + r.aiSettingsHandler.SetBaselineStore(baselineStore) - // Start background baseline learning loop - go r.startBaselineLearning(ctx, baselineStore, metricsHistory) + // Start background baseline learning loop + go r.startBaselineLearning(ctx, baselineStore, metricsHistory) + } } } } @@ -1626,63 +1630,67 @@ func (r *Router) StartPatrol(ctx context.Context) { r.aiSettingsHandler.SetRemediationLog(remediationLog) } - // Initialize pattern detector for failure prediction - patternDetector := ai.NewPatternDetector(ai.PatternDetectorConfig{ - MaxEvents: 5000, - MinOccurrences: 3, - PatternWindow: 90 * 24 * time.Hour, - PredictionLimit: 30 * 24 * time.Hour, - DataDir: dataDir, - }) - if patternDetector != nil { - r.aiSettingsHandler.SetPatternDetector(patternDetector) + // Only initialize pattern and correlation detectors if AI is enabled + // This prevents these subsystems from collecting data and displaying findings when AI is disabled + if r.aiSettingsHandler.IsAIEnabled() { + // Initialize pattern detector for failure prediction + patternDetector := ai.NewPatternDetector(ai.PatternDetectorConfig{ + MaxEvents: 5000, + MinOccurrences: 3, + PatternWindow: 90 * 24 * time.Hour, + PredictionLimit: 30 * 24 * time.Hour, + DataDir: dataDir, + }) + if patternDetector != nil { + r.aiSettingsHandler.SetPatternDetector(patternDetector) - // Wire alert history to pattern detector for event tracking - if alertManager := r.monitor.GetAlertManager(); alertManager != nil { - alertManager.OnAlertHistory(func(alert alerts.Alert) { - // Convert alert type to trackable event - patternDetector.RecordFromAlert(alert.ResourceID, alert.Type+"_"+string(alert.Level), alert.StartTime) - }) - log.Info().Msg("AI Pattern Detector: Wired to alert history for failure prediction") - } - } - - // Initialize correlation detector for multi-resource relationships - correlationDetector := ai.NewCorrelationDetector(ai.CorrelationConfig{ - MaxEvents: 10000, - CorrelationWindow: 10 * time.Minute, - MinOccurrences: 3, - RetentionWindow: 30 * 24 * time.Hour, - DataDir: dataDir, - }) - if correlationDetector != nil { - r.aiSettingsHandler.SetCorrelationDetector(correlationDetector) - - // Wire alert history to correlation detector - if alertManager := r.monitor.GetAlertManager(); alertManager != nil { - alertManager.OnAlertHistory(func(alert alerts.Alert) { - // Record as correlation event - eventType := ai.CorrelationEventType(ai.CorrelationEventAlert) - switch alert.Type { - case "cpu": - eventType = ai.CorrelationEventHighCPU - case "memory": - eventType = ai.CorrelationEventHighMem - case "disk": - eventType = ai.CorrelationEventDiskFull - case "offline", "connectivity": - eventType = ai.CorrelationEventOffline - } - correlationDetector.RecordEvent(ai.CorrelationEvent{ - ResourceID: alert.ResourceID, - ResourceName: alert.ResourceName, - ResourceType: alert.Type, - EventType: eventType, - Timestamp: alert.StartTime, - Value: alert.Value, + // Wire alert history to pattern detector for event tracking + if alertManager := r.monitor.GetAlertManager(); alertManager != nil { + alertManager.OnAlertHistory(func(alert alerts.Alert) { + // Convert alert type to trackable event + patternDetector.RecordFromAlert(alert.ResourceID, alert.Type+"_"+string(alert.Level), alert.StartTime) }) - }) - log.Info().Msg("AI Correlation Detector: Wired to alert history for multi-resource analysis") + log.Info().Msg("AI Pattern Detector: Wired to alert history for failure prediction") + } + } + + // Initialize correlation detector for multi-resource relationships + correlationDetector := ai.NewCorrelationDetector(ai.CorrelationConfig{ + MaxEvents: 10000, + CorrelationWindow: 10 * time.Minute, + MinOccurrences: 3, + RetentionWindow: 30 * 24 * time.Hour, + DataDir: dataDir, + }) + if correlationDetector != nil { + r.aiSettingsHandler.SetCorrelationDetector(correlationDetector) + + // Wire alert history to correlation detector + if alertManager := r.monitor.GetAlertManager(); alertManager != nil { + alertManager.OnAlertHistory(func(alert alerts.Alert) { + // Record as correlation event + eventType := ai.CorrelationEventType(ai.CorrelationEventAlert) + switch alert.Type { + case "cpu": + eventType = ai.CorrelationEventHighCPU + case "memory": + eventType = ai.CorrelationEventHighMem + case "disk": + eventType = ai.CorrelationEventDiskFull + case "offline", "connectivity": + eventType = ai.CorrelationEventOffline + } + correlationDetector.RecordEvent(ai.CorrelationEvent{ + ResourceID: alert.ResourceID, + ResourceName: alert.ResourceName, + ResourceType: alert.Type, + EventType: eventType, + Timestamp: alert.StartTime, + Value: alert.Value, + }) + }) + log.Info().Msg("AI Correlation Detector: Wired to alert history for multi-resource analysis") + } } }