diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/EventTypeConstantGuardTests.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/EventTypeConstantGuardTests.cs new file mode 100644 index 00000000..1a85951b --- /dev/null +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/EventTypeConstantGuardTests.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; +using AutopilotMonitor.Shared; +using Xunit; + +namespace AutopilotMonitor.Agent.V2.Core.Tests +{ + /// + /// Drift guard for the single-source event-type catalog (consolidation 2026-05-29). + /// Every event type emitted by the V2 agent MUST be defined as a const in + /// — that class is the ONE canonical source the MCP + /// catalog/search and the backend derive from. This test scans the V2 source for the known + /// emit shapes and fails if any uses a raw string literal not present in Constants.EventTypes. + /// + /// Scope: V2 only (Agent.V2.Core, Agent.V2, DecisionCore). V1 is decommissioning and is not + /// guarded. Known boundary: gather-rule OutputEventType values are data-driven (rule + /// JSON), not code literals, so they are out of scope by design. + /// + /// + public class EventTypeConstantGuardTests + { + // Emit shapes that carry an event-type string. Capture group 1 = the literal. + private static readonly Regex[] EmitPatterns = + { + new Regex("\\bEventType\\s*=\\s*\"([a-z][a-z0-9_]*)\"", RegexOptions.Compiled), + new Regex("\\beventType\\s*=\\s*\"([a-z][a-z0-9_]*)\"", RegexOptions.Compiled), + new Regex("\\beventType\\s*==\\s*\"([a-z][a-z0-9_]*)\"", RegexOptions.Compiled), + new Regex("\\.Emit\\(\\s*\"([a-z][a-z0-9_]*)\"", RegexOptions.Compiled), + new Regex("EmitDeviceInfoEvent\\(\\s*\"([a-z][a-z0-9_]*)\"", RegexOptions.Compiled), + new Regex("\\[\"eventType\"\\]\\s*=\\s*\"([a-z][a-z0-9_]*)\"", RegexOptions.Compiled), + }; + + // The reducer/param key name itself ("eventType") and the like are not event types. + private static readonly HashSet NotEventTypes = new(StringComparer.Ordinal) + { + "eventType", + }; + + private static readonly string[] V2SourceDirs = + { + Path.Combine("src", "Agent", "AutopilotMonitor.Agent.V2.Core"), + Path.Combine("src", "Agent", "AutopilotMonitor.Agent.V2"), + Path.Combine("src", "Shared", "AutopilotMonitor.DecisionCore"), + }; + + [Fact] + public void All_V2_emitted_event_types_are_defined_in_Constants() + { + var known = KnownEventTypes(); + var offenders = new List(); + + foreach (var dir in V2SourceDirs) + { + var full = Path.Combine(RepoRoot(), dir); + Assert.True(Directory.Exists(full), $"V2 source dir not found: {full}"); + + foreach (var file in EnumerateSourceFiles(full)) + { + var lines = File.ReadAllLines(file); + for (int i = 0; i < lines.Length; i++) + { + var line = lines[i]; + var trimmed = line.TrimStart(); + + // Skip comments and const DEFINITIONS (Constants/DeadlineNames key names etc.). + if (trimmed.StartsWith("//") || trimmed.StartsWith("*") || trimmed.StartsWith("///")) + continue; + if (line.Contains("const ")) continue; + + foreach (var pattern in EmitPatterns) + { + foreach (Match m in pattern.Matches(line)) + { + var literal = m.Groups[1].Value; + if (NotEventTypes.Contains(literal)) continue; + if (!known.Contains(literal)) + { + offenders.Add( + $"{Path.GetFileName(file)}:{i + 1} \"{literal}\" -> {line.Trim()}"); + } + } + } + } + } + } + + Assert.True( + offenders.Count == 0, + "Found V2 event-type string literals NOT defined in Constants.EventTypes. " + + "Add a const (exact value) and reference it at the emit site:\n " + + string.Join("\n ", offenders)); + } + + /// All string-const values declared on . + private static HashSet KnownEventTypes() + { + var values = typeof(Constants.EventTypes) + .GetFields(BindingFlags.Public | BindingFlags.Static) + .Where(f => f.IsLiteral && !f.IsInitOnly && f.FieldType == typeof(string)) + .Select(f => (string)f.GetRawConstantValue()!) + .ToHashSet(StringComparer.Ordinal); + + Assert.True(values.Count > 50, $"Expected many event-type consts, found {values.Count}."); + return values; + } + + private static IEnumerable EnumerateSourceFiles(string root) => + Directory.EnumerateFiles(root, "*.cs", SearchOption.AllDirectories) + .Where(p => !p.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}") + && !p.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}")); + + private static string RepoRoot() + { + var dir = AppContext.BaseDirectory; + for (int i = 0; i < 12; i++) + { + if (File.Exists(Path.Combine(dir, "AutopilotMonitor.sln"))) return dir; + var parent = Directory.GetParent(dir)?.FullName; + if (parent == null || parent == dir) break; + dir = parent; + } + throw new DirectoryNotFoundException( + "Could not locate repo root (AutopilotMonitor.sln) from " + AppContext.BaseDirectory); + } + } +} diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Diagnostics/PendingCrashReporter.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Diagnostics/PendingCrashReporter.cs index 8d64e20d..e89c46ea 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Diagnostics/PendingCrashReporter.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Diagnostics/PendingCrashReporter.cs @@ -5,6 +5,7 @@ using System.IO; using AutopilotMonitor.Agent.V2.Core.Configuration; using AutopilotMonitor.Agent.V2.Core.Logging; using AutopilotMonitor.Agent.V2.Core.Orchestration; +using AutopilotMonitor.Shared; using AutopilotMonitor.Shared.Models; using Newtonsoft.Json; @@ -89,7 +90,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Diagnostics { SessionId = configuration.SessionId, TenantId = configuration.TenantId, - EventType = "previous_crash_detected", + EventType = Constants.EventTypes.PreviousCrashDetected, Severity = EventSeverity.Warning, Source = "PendingCrashReporter", Phase = EnrollmentPhase.Unknown, diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Enrollment/Ime/ImeProcessWatcher.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Enrollment/Ime/ImeProcessWatcher.cs index 223335eb..3d0a07f8 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Enrollment/Ime/ImeProcessWatcher.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Enrollment/Ime/ImeProcessWatcher.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Threading; using AutopilotMonitor.Agent.V2.Core.Logging; using AutopilotMonitor.Agent.V2.Core.Orchestration; +using AutopilotMonitor.Shared; using AutopilotMonitor.Shared.Models; namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.Ime @@ -110,7 +111,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.Ime SessionId = _sessionId, TenantId = _tenantId, Timestamp = DateTime.UtcNow, - EventType = "ime_process_exited", + EventType = Constants.EventTypes.ImeProcessExited, Severity = EventSeverity.Warning, Source = "ImeProcessWatcher", Phase = EnrollmentPhase.Unknown, diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Enrollment/SystemSignals/HelloTracker.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Enrollment/SystemSignals/HelloTracker.cs index eeb1571e..5dfa8670 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Enrollment/SystemSignals/HelloTracker.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Enrollment/SystemSignals/HelloTracker.cs @@ -4,6 +4,7 @@ using System.Diagnostics.Eventing.Reader; using System.Text.RegularExpressions; using AutopilotMonitor.Agent.V2.Core.Logging; using AutopilotMonitor.Agent.V2.Core.Orchestration; +using AutopilotMonitor.Shared; using AutopilotMonitor.Shared.Models; using Microsoft.Win32; @@ -325,7 +326,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals { SessionId = _sessionId, TenantId = _tenantId, - EventType = "hello_policy_detected", + EventType = Constants.EventTypes.HelloPolicyDetected, Severity = EventSeverity.Info, Source = "EspAndHelloTracker", Phase = EnrollmentPhase.Unknown, @@ -520,7 +521,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals return; case EventId_NgcKeyRegistered: // 300 - eventType = "hello_provisioning_completed"; + eventType = Constants.EventTypes.HelloProvisioningCompleted; severity = EventSeverity.Info; message = "Windows Hello for Business provisioned successfully - NGC key registered"; shouldTriggerHelloCompleted = MarkHelloCompleted(); @@ -528,7 +529,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals break; case EventId_NgcKeyRegistrationFailed: // 301 - eventType = "hello_provisioning_failed"; + eventType = Constants.EventTypes.HelloProvisioningFailed; severity = EventSeverity.Error; message = "Windows Hello for Business provisioning failed - NGC key registration error"; shouldTriggerHelloCompleted = MarkHelloCompleted(); @@ -542,7 +543,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals return; case EventId_ProvisioningBlocked: // 362 - eventType = "hello_provisioning_blocked"; + eventType = Constants.EventTypes.HelloProvisioningBlocked; severity = EventSeverity.Warning; message = "Windows Hello for Business provisioning blocked"; shouldTriggerHelloCompleted = MarkHelloCompleted(); @@ -550,7 +551,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals break; case EventId_PinStatus: // 376 - eventType = "hello_pin_status"; + eventType = Constants.EventTypes.HelloPinStatus; severity = EventSeverity.Info; message = "Windows Hello PIN status update"; break; @@ -591,7 +592,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals // (willlaunch, willnotlaunch, pin_status) are not decision-relevant and can // flip state multiple times — keep batched. ImmediateUpload = shouldTriggerHelloCompleted - || eventType == "hello_provisioning_failed" + || eventType == Constants.EventTypes.HelloProvisioningFailed }); _logger.Info($"Hello event detected: {eventType} (EventID {eventId}{(isBackfill ? ", backfill" : "")})"); @@ -632,7 +633,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals SessionId = _sessionId, TenantId = _tenantId, Timestamp = timestamp, - EventType = "hello_policy_detection_mismatch", + EventType = Constants.EventTypes.HelloPolicyDetectionMismatch, Severity = EventSeverity.Warning, Source = source, Phase = EnrollmentPhase.Unknown, @@ -780,7 +781,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals switch (eventId) { case EventId_HelloForBusiness_ProcessingStarted: // 3024 - eventType = "hello_processing_started"; + eventType = Constants.EventTypes.HelloProcessingStarted; severity = EventSeverity.Info; message = "Windows Hello for Business processing started"; _logger.Info("Hello for Business processing started (event 3024)"); @@ -791,7 +792,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals if (string.Equals(hresult, HResult_UserSkippedHello, StringComparison.OrdinalIgnoreCase)) { - eventType = "hello_skipped"; + eventType = Constants.EventTypes.HelloSkipped; severity = EventSeverity.Warning; message = $"Windows Hello for Business skipped by user ({HResult_UserSkippedHello})"; shouldTriggerHelloCompleted = MarkHelloSkipped(); @@ -799,7 +800,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals } else { - eventType = "hello_processing_stopped"; + eventType = Constants.EventTypes.HelloProcessingStopped; severity = EventSeverity.Info; message = $"Windows Hello for Business processing stopped (HRESULT: {hresult ?? "unknown"})"; _logger.Info($"Hello for Business processing stopped (event 6045, HRESULT {hresult ?? "unknown"}) - not treated as terminal"); @@ -961,7 +962,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals { SessionId = _sessionId, TenantId = _tenantId, - EventType = "hello_wait_timeout", + EventType = Constants.EventTypes.HelloWaitTimeout, Severity = EventSeverity.Info, Source = "EspAndHelloTracker", Phase = EnrollmentPhase.Unknown, @@ -992,7 +993,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals { SessionId = _sessionId, TenantId = _tenantId, - EventType = "hello_wait_timeout", + EventType = Constants.EventTypes.HelloWaitTimeout, Severity = EventSeverity.Info, Source = "EspAndHelloTracker", Phase = EnrollmentPhase.Unknown, @@ -1051,7 +1052,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals { SessionId = _sessionId, TenantId = _tenantId, - EventType = "hello_completion_timeout", + EventType = Constants.EventTypes.HelloCompletionTimeout, Severity = EventSeverity.Warning, Source = "EspAndHelloTracker", Phase = EnrollmentPhase.Unknown, @@ -1085,7 +1086,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals { SessionId = _sessionId, TenantId = _tenantId, - EventType = "hello_policy_detected", + EventType = Constants.EventTypes.HelloPolicyDetected, Severity = EventSeverity.Info, Source = "EspAndHelloTracker", Phase = EnrollmentPhase.Unknown, diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Enrollment/SystemSignals/ModernDeploymentTracker.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Enrollment/SystemSignals/ModernDeploymentTracker.cs index 334db999..8a830214 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Enrollment/SystemSignals/ModernDeploymentTracker.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Enrollment/SystemSignals/ModernDeploymentTracker.cs @@ -330,7 +330,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals { SessionId = _sessionId, TenantId = _tenantId, - EventType = "whiteglove_started", + EventType = Constants.EventTypes.WhiteGloveStarted, Severity = EventSeverity.Info, Source = "ModernDeploymentWatcher", Phase = EnrollmentPhase.Unknown, diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Enrollment/SystemSignals/ProvisioningStatusTracker.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Enrollment/SystemSignals/ProvisioningStatusTracker.cs index 168463d9..37e9c9e8 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Enrollment/SystemSignals/ProvisioningStatusTracker.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Enrollment/SystemSignals/ProvisioningStatusTracker.cs @@ -1092,7 +1092,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals { SessionId = _sessionId, TenantId = _tenantId, - EventType = "esp_failure_settle_started", + EventType = Constants.EventTypes.EspFailureSettleStarted, Severity = EventSeverity.Info, Source = "EspAndHelloTracker", Phase = EnrollmentPhase.Unknown, @@ -1258,7 +1258,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals { SessionId = _sessionId, TenantId = _tenantId, - EventType = "esp_provisioning_raw", + EventType = Constants.EventTypes.EspProvisioningRaw, Severity = EventSeverity.Trace, Source = "EspAndHelloTracker", Phase = EnrollmentPhase.Unknown, diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Enrollment/SystemSignals/ShellCoreTracker.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Enrollment/SystemSignals/ShellCoreTracker.cs index 01093a02..ddaf24e7 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Enrollment/SystemSignals/ShellCoreTracker.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Enrollment/SystemSignals/ShellCoreTracker.cs @@ -4,6 +4,7 @@ using System.Diagnostics.Eventing.Reader; using System.Text.RegularExpressions; using AutopilotMonitor.Agent.V2.Core.Logging; using AutopilotMonitor.Agent.V2.Core.Orchestration; +using AutopilotMonitor.Shared; using AutopilotMonitor.Shared.Models; namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals @@ -168,7 +169,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals case EventId_ShellCore_WebAppStarted: // 62404 if (description.Contains("AADHello") || description.Contains("'NGC'")) { - eventType = "hello_wizard_started"; + eventType = Constants.EventTypes.HelloWizardStarted; message = "Windows Hello wizard started (CloudExperienceHost)"; triggerFinalizingSetup = true; finalizingSetupReason = "hello_wizard_started"; @@ -193,7 +194,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals _whiteGloveDetected = true; } - eventType = "whiteglove_complete"; + eventType = Constants.EventTypes.WhiteGloveComplete; message = "WhiteGlove (Pre-Provisioning) completed successfully"; // No FinalizingSetup transition — WhiteGlove terminates pre-provisioning entirely @@ -202,14 +203,14 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals else if (HasEspFailurePattern(description)) { detectedFailureType = ExtractEspFailureType(description); - eventType = "esp_failure"; + eventType = Constants.EventTypes.EspFailure; severity = EventSeverity.Error; message = $"ESP (Enrollment Status Page) reported a failure: {detectedFailureType}"; _logger.Warning($"ESP failure detected via Shell-Core event 62407: {detectedFailureType}"); } else if (EspExitingPattern.IsMatch(description)) { - eventType = "esp_exiting"; + eventType = Constants.EventTypes.EspExiting; message = "ESP (Enrollment Status Page) phase exiting"; triggerFinalizingSetup = true; finalizingSetupReason = "esp_exiting"; @@ -244,7 +245,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals { "eventTime", timestamp.ToString("o") } }; - if (eventType == "esp_failure" && detectedFailureType != null) + if (eventType == Constants.EventTypes.EspFailure && detectedFailureType != null) { eventData["failureType"] = detectedFailureType; } @@ -279,7 +280,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals // Fire WhiteGloveCompleted AFTER event emission so the whiteglove_complete event // is in the spool before the agent exits. - if (eventType == "whiteglove_complete") + if (eventType == Constants.EventTypes.WhiteGloveComplete) { try { WhiteGloveCompleted?.Invoke(this, EventArgs.Empty); } catch (Exception ex) { _logger.Error("WhiteGloveCompleted handler failed", ex); } @@ -287,7 +288,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals // Fire EspFailureDetected AFTER event emission so the esp_failure event is in the // spool before the agent potentially shuts down. - if (eventType == "esp_failure" && detectedFailureType != null) + if (eventType == Constants.EventTypes.EspFailure && detectedFailureType != null) { try { EspFailureDetected?.Invoke(this, detectedFailureType); } catch (Exception ex) { _logger.Error($"EspFailureDetected handler failed for '{detectedFailureType}'", ex); } @@ -297,7 +298,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.SystemSignals // a DecisionSignalKind.EspExiting so the engine can arm HelloSafety on the genuine // post-AccountSetup exit. Engine-side guard (ShouldTransitionToAwaitingHello) // distinguishes intermediate exits from the real one. - if (eventType == "esp_exiting") + if (eventType == Constants.EventTypes.EspExiting) { try { EspExited?.Invoke(this, new EspExitedEventArgs(timestamp)); } catch (Exception ex) { _logger.Error("EspExited handler failed", ex); } diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Runtime/ServerActionDispatcher.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Runtime/ServerActionDispatcher.cs index c5f11b4a..25c2ecef 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Runtime/ServerActionDispatcher.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Runtime/ServerActionDispatcher.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using AutopilotMonitor.Agent.V2.Core.Configuration; using AutopilotMonitor.Agent.V2.Core.Logging; using AutopilotMonitor.Agent.V2.Core.Orchestration; +using AutopilotMonitor.Shared; using AutopilotMonitor.Shared.Models; namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Runtime @@ -179,7 +180,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Runtime private void EmitReceived(ServerAction action) { PostEvent( - eventType: "server_action_received", + eventType: Constants.EventTypes.ServerActionReceived, severity: EventSeverity.Info, message: $"Received server action '{action.Type}'", data: BuildTelemetryData(action)); @@ -195,7 +196,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Runtime } PostEvent( - eventType: "server_action_executed", + eventType: Constants.EventTypes.ServerActionExecuted, severity: EventSeverity.Info, message: $"Executed server action '{action.Type}'", data: data); @@ -207,7 +208,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Runtime data["failureReason"] = reason ?? string.Empty; PostEvent( - eventType: "server_action_failed", + eventType: Constants.EventTypes.ServerActionFailed, severity: EventSeverity.Warning, message: $"Failed to execute server action '{action.Type}': {reason}", data: data); diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Analyzers/IntegrityBypassAnalyzer.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Analyzers/IntegrityBypassAnalyzer.cs index 09e7abce..0be36a26 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Analyzers/IntegrityBypassAnalyzer.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Analyzers/IntegrityBypassAnalyzer.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Management; using AutopilotMonitor.Agent.V2.Core.Logging; using AutopilotMonitor.Agent.V2.Core.Orchestration; +using AutopilotMonitor.Shared; using AutopilotMonitor.Shared.Models; using Microsoft.Win32; @@ -225,7 +226,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Analyzers { SessionId = _sessionId, TenantId = _tenantId, - EventType = "integrity_bypass_analysis", + EventType = Constants.EventTypes.IntegrityBypassAnalysis, Severity = severity, Source = Name, Phase = EnrollmentPhase.Unknown, diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Analyzers/LocalAdminAnalyzer.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Analyzers/LocalAdminAnalyzer.cs index c58f87c3..6a55bfae 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Analyzers/LocalAdminAnalyzer.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Analyzers/LocalAdminAnalyzer.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Management; using AutopilotMonitor.Agent.V2.Core.Logging; using AutopilotMonitor.Agent.V2.Core.Orchestration; +using AutopilotMonitor.Shared; using AutopilotMonitor.Shared.Models; using Microsoft.Win32; @@ -208,7 +209,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Analyzers { SessionId = _sessionId, TenantId = _tenantId, - EventType = "local_admin_analysis", + EventType = Constants.EventTypes.LocalAdminAnalysis, Severity = severity, Source = Name, Phase = phase, diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Analyzers/SoftwareInventoryAnalyzer.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Analyzers/SoftwareInventoryAnalyzer.cs index c7d4da26..c5ad6bef 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Analyzers/SoftwareInventoryAnalyzer.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Analyzers/SoftwareInventoryAnalyzer.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading; using AutopilotMonitor.Agent.V2.Core.Logging; using AutopilotMonitor.Agent.V2.Core.Orchestration; +using AutopilotMonitor.Shared; using AutopilotMonitor.Shared.Models; using Microsoft.Win32; using static AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Analyzers.SoftwareInventoryNormalization; @@ -641,7 +642,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Analyzers { SessionId = _sessionId, TenantId = _tenantId, - EventType = "software_inventory_analysis", + EventType = Constants.EventTypes.SoftwareInventoryAnalysis, Severity = EventSeverity.Info, Source = Name, Phase = phase, diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/DeviceInfo/DeviceInfoCollector.HardwareSpec.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/DeviceInfo/DeviceInfoCollector.HardwareSpec.cs index 81bc841d..093d1aed 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/DeviceInfo/DeviceInfoCollector.HardwareSpec.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/DeviceInfo/DeviceInfoCollector.HardwareSpec.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Management; +using AutopilotMonitor.Shared; namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.DeviceInfo { @@ -27,7 +28,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.DeviceInfo CollectGpuInfo(data); var message = BuildHardwareSpecMessage(data); - EmitDeviceInfoEvent("hardware_spec", message, data); + EmitDeviceInfoEvent(Constants.EventTypes.HardwareSpec, message, data); } catch (Exception ex) { diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/DeviceInfo/DeviceInfoCollector.NetworkAndSecurity.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/DeviceInfo/DeviceInfoCollector.NetworkAndSecurity.cs index 38f47fdb..2f279c38 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/DeviceInfo/DeviceInfoCollector.NetworkAndSecurity.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/DeviceInfo/DeviceInfoCollector.NetworkAndSecurity.cs @@ -12,6 +12,7 @@ using AutopilotMonitor.Agent.V2.Core.Orchestration; using AutopilotMonitor.Agent.V2.Core.Security; using AutopilotMonitor.DecisionCore.Engine; using AutopilotMonitor.DecisionCore.Signals; +using AutopilotMonitor.Shared; using AutopilotMonitor.Shared.Models; using Microsoft.Win32; @@ -88,14 +89,14 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.DeviceInfo } } - EmitDeviceInfoEvent("network_adapters", "Network adapters configuration", + EmitDeviceInfoEvent(Constants.EventTypes.NetworkAdapters, "Network adapters configuration", new Dictionary { { "adapterCount", adapters.Count }, { "adapters", adapters } }); - EmitDeviceInfoEvent("dns_configuration", "DNS server configuration", + EmitDeviceInfoEvent(Constants.EventTypes.DnsConfiguration, "DNS server configuration", new Dictionary { { "dnsEntries", dnsServers } @@ -138,7 +139,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.DeviceInfo data["proxyType"] = proxyType; - EmitDeviceInfoEvent("proxy_configuration", $"Proxy configuration: {proxyType}", data); + EmitDeviceInfoEvent(Constants.EventTypes.ProxyConfiguration, $"Proxy configuration: {proxyType}", data); } catch (Exception ex) { @@ -283,14 +284,14 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.DeviceInfo // Include enrollment type in autopilot_profile event data data["enrollmentType"] = detectedType; - EmitDeviceInfoEvent("autopilot_profile", "Autopilot profile configuration", data); + EmitDeviceInfoEvent(Constants.EventTypes.AutopilotProfile, "Autopilot profile configuration", data); // Emit dedicated enrollment_type_detected event for easy filtering _post.Emit(new EnrollmentEvent { SessionId = _sessionId, TenantId = _tenantId, - EventType = "enrollment_type_detected", + EventType = Constants.EventTypes.EnrollmentTypeDetected, Severity = EventSeverity.Info, Source = "EnrollmentTracker", Phase = EnrollmentPhase.Unknown, @@ -360,7 +361,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.DeviceInfo } } - EmitDeviceInfoEvent("secureboot_status", $"SecureBoot: {data["uefiSecureBootEnabled"]}, CA2023: {data["uefiCA2023Status"]}", data); + EmitDeviceInfoEvent(Constants.EventTypes.SecureBootStatus, $"SecureBoot: {data["uefiSecureBootEnabled"]}, CA2023: {data["uefiCA2023Status"]}", data); } catch (Exception ex) { @@ -395,7 +396,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.DeviceInfo data["systemDriveProtected"] = volumes.Any(v => v["driveLetter"]?.ToString() == "C:" && v["protectionStatus"]?.ToString() == "1"); - EmitDeviceInfoEvent("bitlocker_status", "BitLocker encryption status", data); + EmitDeviceInfoEvent(Constants.EventTypes.BitLockerStatus, "BitLocker encryption status", data); } catch (Exception ex) { @@ -433,12 +434,12 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.DeviceInfo if (data.Count > 0) { - EmitDeviceInfoEvent("tpm_status", $"TPM: {data["manufacturerName"]} v{data["manufacturerVersion"]}", data); + EmitDeviceInfoEvent(Constants.EventTypes.TpmStatus, $"TPM: {data["manufacturerName"]} v{data["manufacturerVersion"]}", data); } else { data["available"] = false; - EmitDeviceInfoEvent("tpm_status", "TPM: not available", data); + EmitDeviceInfoEvent(Constants.EventTypes.TpmStatus, "TPM: not available", data); } } catch (Exception ex) @@ -497,7 +498,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.DeviceInfo object joinTypeValue; var joinType = data.TryGetValue("joinType", out joinTypeValue) ? joinTypeValue?.ToString() ?? "Unknown" : "Unknown"; - EmitDeviceInfoEvent("aad_join_status", $"AAD join: {joinType}", data); + EmitDeviceInfoEvent(Constants.EventTypes.AadJoinStatus, $"AAD join: {joinType}", data); } catch (Exception ex) { @@ -548,7 +549,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.DeviceInfo var summary = BuildSummary(snapshot); _logger.Info($"EnrollmentTracker: ESP configuration detected — {summary}"); - EmitDeviceInfoEvent("esp_config_detected", $"ESP configuration: {summary}", data); + EmitDeviceInfoEvent(Constants.EventTypes.EspConfigDetected, $"ESP configuration: {summary}", data); PostEspConfigDetectedSignal(snapshot); } catch (Exception ex) @@ -595,7 +596,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.DeviceInfo if (activeNic == null) { - EmitDeviceInfoEvent("network_interface_info", "No active network interface found", + EmitDeviceInfoEvent(Constants.EventTypes.NetworkInterfaceInfo, "No active network interface found", new Dictionary { { "status", "no_active_interface" } }); return; } @@ -623,7 +624,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.DeviceInfo } var message = $"Active interface: {activeNic.Description} ({data["connectionType"]}, {data["linkSpeedMbps"]} Mbps)"; - EmitDeviceInfoEvent("network_interface_info", message, data); + EmitDeviceInfoEvent(Constants.EventTypes.NetworkInterfaceInfo, message, data); // Fire-and-forget WiFi signal collection — separate event, never blocks if (isWifi) @@ -739,7 +740,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.DeviceInfo if (data.ContainsKey("wifiRadioType")) message += $" ({data["wifiRadioType"]})"; - EmitDeviceInfoEvent("wifi_signal_info", message, data); + EmitDeviceInfoEvent(Constants.EventTypes.WifiSignalInfo, message, data); } } catch (Exception ex) diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/DeviceInfo/DeviceInfoCollector.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/DeviceInfo/DeviceInfoCollector.cs index 97be2f82..9aadb3e1 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/DeviceInfo/DeviceInfoCollector.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/DeviceInfo/DeviceInfoCollector.cs @@ -10,6 +10,7 @@ using AutopilotMonitor.Agent.V2.Core.Logging; using AutopilotMonitor.Agent.V2.Core.Orchestration; using AutopilotMonitor.DecisionCore.Engine; using AutopilotMonitor.DecisionCore.Signals; +using AutopilotMonitor.Shared; using AutopilotMonitor.Shared.Models; using Microsoft.Win32; using AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment; @@ -186,7 +187,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.DeviceInfo : $" (Build {currentBuild}.{buildRevision})"; } - EmitDeviceInfoEvent("os_info", message, data); + EmitDeviceInfoEvent(Constants.EventTypes.OsInfo, message, data); } catch (Exception ex) { @@ -201,7 +202,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.DeviceInfo bootData["note"] = "Boot time could not be determined"; } - EmitDeviceInfoEvent("boot_time", + EmitDeviceInfoEvent(Constants.EventTypes.BootTime, bootData.ContainsKey("bootTime") ? $"Last boot: {bootData["bootTime"]}" : "Boot time unavailable", diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Gather/Collectors/CommandCollector.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Gather/Collectors/CommandCollector.cs index 01bf9bdc..fa3a4500 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Gather/Collectors/CommandCollector.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Gather/Collectors/CommandCollector.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Text; using AutopilotMonitor.Agent.V2.Core.Security; +using AutopilotMonitor.Shared; using AutopilotMonitor.Shared.Models; namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Gather.Collectors @@ -32,7 +33,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Gather.Collectors SessionId = context.SessionId, TenantId = context.TenantId, Timestamp = DateTime.UtcNow, - EventType = "security_warning", + EventType = Constants.EventTypes.SecurityWarning, Severity = EventSeverity.Warning, Source = "GatherRuleExecutor", Message = $"Blocked command not on allowlist: {command} (Rule: {rule.RuleId})", diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Gather/GatherRuleContext.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Gather/GatherRuleContext.cs index 6411c094..2d131d55 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Gather/GatherRuleContext.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Gather/GatherRuleContext.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using AutopilotMonitor.Agent.V2.Core.Logging; +using AutopilotMonitor.Shared; using AutopilotMonitor.Shared.Models; using AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment; using AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.Ime; @@ -58,7 +59,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Gather SessionId = SessionId, TenantId = TenantId, Timestamp = DateTime.UtcNow, - EventType = "security_warning", + EventType = Constants.EventTypes.SecurityWarning, Severity = EventSeverity.Warning, Source = "GatherRuleExecutor", Message = $"Blocked {collectorType} target not on allowlist: {target} (Rule: {rule.RuleId})", diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Gather/GatherRuleExecutor.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Gather/GatherRuleExecutor.cs index b744b66c..0ff8c842 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Gather/GatherRuleExecutor.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Gather/GatherRuleExecutor.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading; using AutopilotMonitor.Agent.V2.Core.Logging; using AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Gather.Collectors; +using AutopilotMonitor.Shared; using AutopilotMonitor.Shared.Models; using AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment; using AutopilotMonitor.Agent.V2.Core.Monitoring.Enrollment.Ime; @@ -194,7 +195,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Gather result["ruleId"] = rule.RuleId; result["ruleTitle"] = rule.Title; - var eventType = !string.IsNullOrEmpty(rule.OutputEventType) ? rule.OutputEventType : "gather_result"; + var eventType = !string.IsNullOrEmpty(rule.OutputEventType) ? rule.OutputEventType : Constants.EventTypes.GatherResult; // Allow collectors to override severity via _severityOverride in result object severityOverride; diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Periodic/AgentSelfMetricsCollector.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Periodic/AgentSelfMetricsCollector.cs index e581b085..35069214 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Periodic/AgentSelfMetricsCollector.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Periodic/AgentSelfMetricsCollector.cs @@ -7,6 +7,7 @@ using AutopilotMonitor.Agent.V2.Core.Logging; using AutopilotMonitor.Agent.V2.Core.Monitoring.Transport; using AutopilotMonitor.Agent.V2.Core.Orchestration; using AutopilotMonitor.Agent.V2.Core.Transport.Telemetry; +using AutopilotMonitor.Shared; using AutopilotMonitor.Shared.Models; namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Periodic @@ -176,7 +177,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Periodic SessionId = SessionId, TenantId = TenantId, Timestamp = now, - EventType = "agent_metrics_snapshot", + EventType = Constants.EventTypes.AgentMetricsSnapshot, Severity = EventSeverity.Debug, Source = "AgentSelfMetricsCollector", Phase = EnrollmentPhase.Unknown, @@ -213,7 +214,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Periodic SessionId = SessionId, TenantId = TenantId, Timestamp = now, - EventType = "spool_pressure_detected", + EventType = Constants.EventTypes.SpoolPressureDetected, Severity = EventSeverity.Warning, Source = "AgentSelfMetricsCollector", Phase = EnrollmentPhase.Unknown, diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Periodic/DeliveryOptimizationCollector.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Periodic/DeliveryOptimizationCollector.cs index 4705ec8d..4419fd73 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Periodic/DeliveryOptimizationCollector.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Periodic/DeliveryOptimizationCollector.cs @@ -8,6 +8,7 @@ using System.Text; using System.Threading; using AutopilotMonitor.Agent.V2.Core.Logging; using AutopilotMonitor.Agent.V2.Core.Orchestration; +using AutopilotMonitor.Shared; using AutopilotMonitor.Shared.Models; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -342,7 +343,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Periodic { SessionId = SessionId, TenantId = TenantId, - EventType = "download_progress", + EventType = Constants.EventTypes.DownloadProgress, Severity = EventSeverity.Debug, Source = "DeliveryOptimizationCollector", Phase = EnrollmentPhase.Unknown, diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Periodic/PerformanceCollector.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Periodic/PerformanceCollector.cs index 1584883e..c80d78b0 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Periodic/PerformanceCollector.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Monitoring/Telemetry/Periodic/PerformanceCollector.cs @@ -7,6 +7,7 @@ using System.Threading; using AutopilotMonitor.Agent.V2.Core.Logging; using AutopilotMonitor.Agent.V2.Core.Monitoring.Interop; using AutopilotMonitor.Agent.V2.Core.Orchestration; +using AutopilotMonitor.Shared; using AutopilotMonitor.Shared.Models; namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Periodic @@ -245,7 +246,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Monitoring.Telemetry.Periodic SessionId = SessionId, TenantId = TenantId, Timestamp = DateTime.UtcNow, - EventType = "performance_snapshot", + EventType = Constants.EventTypes.PerformanceSnapshot, Severity = EventSeverity.Debug, Source = "PerformanceCollector", Phase = EnrollmentPhase.Unknown, diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Orchestration/EnrollmentOrchestrator.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Orchestration/EnrollmentOrchestrator.cs index a32fee11..133a1376 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Orchestration/EnrollmentOrchestrator.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Orchestration/EnrollmentOrchestrator.cs @@ -14,6 +14,7 @@ using AutopilotMonitor.DecisionCore.Classifiers; using AutopilotMonitor.DecisionCore.Engine; using AutopilotMonitor.DecisionCore.Signals; using AutopilotMonitor.DecisionCore.State; +using AutopilotMonitor.Shared; namespace AutopilotMonitor.Agent.V2.Core.Orchestration { @@ -728,7 +729,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Orchestration var lifecyclePost = new InformationalEventPost(_ingress, _clock); var resumedAtUtc = _clock.UtcNow; lifecyclePost.Emit( - eventType: "whiteglove_resumed", + eventType: Constants.EventTypes.WhiteGloveResumed, source: "Agent", message: "WhiteGlove Part 2 resumed after reseal-reboot; Part-1 state archived.", severity: Shared.Models.EventSeverity.Info, diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Runtime/StartupEnvironmentProbes.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Runtime/StartupEnvironmentProbes.cs index 70cda681..d2e13ee9 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Runtime/StartupEnvironmentProbes.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Runtime/StartupEnvironmentProbes.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using AutopilotMonitor.Agent.V2.Core.Configuration; using AutopilotMonitor.Agent.V2.Core.Logging; using AutopilotMonitor.Agent.V2.Core.Monitoring.Transport; +using AutopilotMonitor.Shared; using AutopilotMonitor.Shared.Models; namespace AutopilotMonitor.Agent.V2.Core.Runtime @@ -124,7 +125,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Runtime { SessionId = configuration.SessionId, TenantId = configuration.TenantId, - EventType = "device_location", + EventType = Constants.EventTypes.DeviceLocation, Severity = EventSeverity.Info, Source = "StartupEnvironmentProbes", Phase = EnrollmentPhase.Unknown, @@ -139,7 +140,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Runtime { SessionId = configuration.SessionId, TenantId = configuration.TenantId, - EventType = "agent_trace", + EventType = Constants.EventTypes.AgentTrace, Severity = EventSeverity.Warning, Source = "StartupEnvironmentProbes", Phase = EnrollmentPhase.Unknown, @@ -162,7 +163,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Runtime { SessionId = configuration.SessionId, TenantId = configuration.TenantId, - EventType = "timezone_auto_set", + EventType = Constants.EventTypes.TimezoneAutoSet, Severity = tz.Success ? EventSeverity.Info : EventSeverity.Warning, Source = "StartupEnvironmentProbes", Phase = EnrollmentPhase.Unknown, @@ -189,7 +190,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Runtime { SessionId = configuration.SessionId, TenantId = configuration.TenantId, - EventType = "ntp_time_check", + EventType = Constants.EventTypes.NtpTimeCheck, Severity = severity, Source = "StartupEnvironmentProbes", Phase = EnrollmentPhase.Unknown, @@ -209,7 +210,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Runtime { SessionId = configuration.SessionId, TenantId = configuration.TenantId, - EventType = "ntp_time_check", + EventType = Constants.EventTypes.NtpTimeCheck, Severity = EventSeverity.Warning, Source = "StartupEnvironmentProbes", Phase = EnrollmentPhase.Unknown, @@ -288,7 +289,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Runtime { SessionId = configuration.SessionId, TenantId = configuration.TenantId, - EventType = "power_state_check", + EventType = Constants.EventTypes.PowerStateCheck, Severity = severity, Source = "StartupEnvironmentProbes", Phase = EnrollmentPhase.Unknown, diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/SignalAdapters/ImeLogTrackerAdapter.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/SignalAdapters/ImeLogTrackerAdapter.cs index 198116df..d40f8cf9 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/SignalAdapters/ImeLogTrackerAdapter.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/SignalAdapters/ImeLogTrackerAdapter.cs @@ -863,7 +863,7 @@ namespace AutopilotMonitor.Agent.V2.Core.SignalAdapters if (!string.IsNullOrEmpty(script.PolicyId)) data["policyId"] = script.PolicyId!; _post.Emit( - eventType: "agent_trace", + eventType: SharedEventTypes.AgentTrace, source: SourceLabel, message: $"Autopilot-Monitor bootstrap v{version} detected via Platform Script stdout", severity: EventSeverity.Info, diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Telemetry/Events/BackPressureEventObserver.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Telemetry/Events/BackPressureEventObserver.cs index 3feb94f1..a46b6bd2 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Telemetry/Events/BackPressureEventObserver.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Telemetry/Events/BackPressureEventObserver.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using AutopilotMonitor.Agent.V2.Core.Orchestration; using AutopilotMonitor.DecisionCore.Engine; +using AutopilotMonitor.Shared; using AutopilotMonitor.Shared.Models; namespace AutopilotMonitor.Agent.V2.Core.Telemetry.Events @@ -18,7 +19,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Telemetry.Events /// internal sealed class BackPressureEventObserver : IBackPressureObserver { - internal const string EventType = "ingress_backpressure"; + internal const string EventType = Constants.EventTypes.IngressBackpressure; internal const string SourceId = "signal_ingress"; private readonly TelemetryEventEmitter _emitter; diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Termination/EnrollmentTerminationHandler.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Termination/EnrollmentTerminationHandler.cs index 2e406c40..f7863f88 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Termination/EnrollmentTerminationHandler.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Termination/EnrollmentTerminationHandler.cs @@ -291,7 +291,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Termination { SessionId = _configuration.SessionId, TenantId = _configuration.TenantId, - EventType = "whiteglove_part1_complete", + EventType = Constants.EventTypes.WhiteGlovePart1Complete, Severity = EventSeverity.Info, Source = "EnrollmentTerminationHandler", Phase = EnrollmentPhase.Unknown, @@ -424,7 +424,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Termination { SessionId = _configuration.SessionId, TenantId = _configuration.TenantId, - EventType = "enrollment_summary_shown", + EventType = Constants.EventTypes.EnrollmentSummaryShown, Severity = EventSeverity.Info, Source = "EnrollmentTerminationHandler", Phase = EnrollmentPhase.Unknown, @@ -617,7 +617,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Termination { SessionId = _configuration.SessionId, TenantId = _configuration.TenantId, - EventType = "diagnostics_collecting", + EventType = Constants.EventTypes.DiagnosticsCollecting, Severity = EventSeverity.Info, Source = "EnrollmentTerminationHandler", Phase = EnrollmentPhase.Unknown, @@ -648,7 +648,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Termination { SessionId = _configuration.SessionId, TenantId = _configuration.TenantId, - EventType = "diagnostics_uploaded", + EventType = Constants.EventTypes.DiagnosticsUploaded, Severity = EventSeverity.Info, Source = "EnrollmentTerminationHandler", Phase = EnrollmentPhase.Unknown, @@ -674,7 +674,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Termination { SessionId = _configuration.SessionId, TenantId = _configuration.TenantId, - EventType = "diagnostics_upload_failed", + EventType = Constants.EventTypes.DiagnosticsUploadFailed, Severity = EventSeverity.Warning, Source = "EnrollmentTerminationHandler", Phase = EnrollmentPhase.Unknown, @@ -730,7 +730,7 @@ namespace AutopilotMonitor.Agent.V2.Core.Termination { SessionId = _configuration.SessionId, TenantId = _configuration.TenantId, - EventType = "reboot_triggered", + EventType = Constants.EventTypes.RebootTriggered, Severity = EventSeverity.Info, Source = "EnrollmentTerminationHandler", Phase = EnrollmentPhase.Unknown, diff --git a/src/Agent/AutopilotMonitor.Agent.V2/Program.GatherRulesMode.cs b/src/Agent/AutopilotMonitor.Agent.V2/Program.GatherRulesMode.cs index cf5a6cef..14b4a082 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2/Program.GatherRulesMode.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2/Program.GatherRulesMode.cs @@ -209,7 +209,7 @@ namespace AutopilotMonitor.Agent.V2 SessionId = sessionId, TenantId = tenantId, Timestamp = DateTime.UtcNow, - EventType = "gather_rules_collection_started", + EventType = Constants.EventTypes.GatherRulesCollectionStarted, Severity = EventSeverity.Info, Source = "Agent", Phase = EnrollmentPhase.Start, @@ -250,7 +250,7 @@ namespace AutopilotMonitor.Agent.V2 SessionId = sessionId, TenantId = tenantId, Timestamp = DateTime.UtcNow, - EventType = "gather_rules_collection_completed", + EventType = Constants.EventTypes.GatherRulesCollectionCompleted, Severity = EventSeverity.Info, Source = "Agent", Phase = EnrollmentPhase.Start, diff --git a/src/Agent/AutopilotMonitor.Agent.V2/Runtime/LifecycleEmitters.cs b/src/Agent/AutopilotMonitor.Agent.V2/Runtime/LifecycleEmitters.cs index 80181bac..65581195 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2/Runtime/LifecycleEmitters.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2/Runtime/LifecycleEmitters.cs @@ -134,7 +134,7 @@ namespace AutopilotMonitor.Agent.V2.Runtime { SessionId = agentConfig.SessionId, TenantId = agentConfig.TenantId, - EventType = "remote_config_fetch_failed", + EventType = SharedConstants.EventTypes.RemoteConfigFetchFailed, Severity = EventSeverity.Warning, Source = "Agent", Phase = EnrollmentPhase.Unknown, @@ -205,7 +205,7 @@ namespace AutopilotMonitor.Agent.V2.Runtime { SessionId = agentConfig.SessionId, TenantId = agentConfig.TenantId, - EventType = "agent_unrestricted_mode_changed", + EventType = SharedConstants.EventTypes.AgentUnrestrictedModeChanged, Severity = newValue ? EventSeverity.Warning : EventSeverity.Info, Source = "RemoteConfigMerger", Phase = EnrollmentPhase.Unknown, @@ -623,7 +623,7 @@ namespace AutopilotMonitor.Agent.V2.Runtime { SessionId = agentConfig.SessionId, TenantId = agentConfig.TenantId, - EventType = "enrollment_failed", + EventType = SharedConstants.EventTypes.EnrollmentFailed, Severity = EventSeverity.Error, Source = "EnrollmentOrchestrator", Phase = EnrollmentPhase.Unknown, diff --git a/src/Agent/AutopilotMonitor.Agent.V2/Runtime/ServerControlPlane.cs b/src/Agent/AutopilotMonitor.Agent.V2/Runtime/ServerControlPlane.cs index 960a670c..0cc51858 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2/Runtime/ServerControlPlane.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2/Runtime/ServerControlPlane.cs @@ -7,6 +7,7 @@ using AutopilotMonitor.Agent.V2.Core.Logging; using AutopilotMonitor.Agent.V2.Core.Monitoring.Runtime; using AutopilotMonitor.Agent.V2.Core.Orchestration; using AutopilotMonitor.Agent.V2.Core.Termination; +using AutopilotMonitor.Shared; using AutopilotMonitor.Shared.Models; namespace AutopilotMonitor.Agent.V2.Runtime @@ -177,7 +178,7 @@ namespace AutopilotMonitor.Agent.V2.Runtime { SessionId = agentConfig.SessionId, TenantId = agentConfig.TenantId, - EventType = "admin_marked_session", + EventType = Constants.EventTypes.AdminMarkedSession, Severity = outcome == EnrollmentTerminationOutcome.Succeeded ? EventSeverity.Info : EventSeverity.Warning, Source = "WireTelemetryServerResponse", Phase = EnrollmentPhase.Unknown, diff --git a/src/McpServer/autopilot-monitor-mcp/src/__tests__/event-types-drift.test.ts b/src/McpServer/autopilot-monitor-mcp/src/__tests__/event-types-drift.test.ts new file mode 100644 index 00000000..ceb5c70b --- /dev/null +++ b/src/McpServer/autopilot-monitor-mcp/src/__tests__/event-types-drift.test.ts @@ -0,0 +1,79 @@ +/** + * Drift guard: the MCP event-type catalog (resource-catalog.ts) MUST stay in sync + * with the canonical C# source `Constants.EventTypes`. The agent-side guard test + * (EventTypeConstantGuardTests) keeps Constants complete; this test keeps the MCP + * catalog equal to it, so the catalog never advertises a phantom type nor omits a + * real one (which would silently degrade event-TYPE-driven cross-session search). + * + * It reads Constants.cs directly — no codegen, no runtime coupling. Same-repo only; + * skipped gracefully if the C# source can't be located (e.g. an isolated package). + */ +import { describe, it, expect } from 'vitest'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; +import { existsSync, readFileSync } from 'node:fs'; +import { EVENT_TYPES_CATALOG, INTERNAL_EVENT_TYPES, ALL_EVENT_TYPES } from '../resource-catalog.js'; + +/** Walk up from this test file to the repo root (marked by AutopilotMonitor.sln). */ +function findConstantsCs(): string | null { + let dir = dirname(fileURLToPath(import.meta.url)); + for (let i = 0; i < 12; i++) { + if (existsSync(join(dir, 'AutopilotMonitor.sln'))) { + const p = join(dir, 'src', 'Shared', 'AutopilotMonitor.Shared', 'Constants.cs'); + return existsSync(p) ? p : null; + } + const parent = dirname(dir); + if (parent === dir) break; + dir = parent; + } + return null; +} + +/** Extract every event-type string value declared inside `class EventTypes`. */ +function canonicalEventTypes(source: string): Set { + const start = source.indexOf('class EventTypes'); + expect(start, 'class EventTypes not found in Constants.cs').toBeGreaterThan(-1); + // The EventTypes body ends at the next `class ` declaration (EventSources). + const after = source.indexOf('class ', start + 'class EventTypes'.length); + const body = source.slice(start, after === -1 ? undefined : after); + const values = new Set(); + for (const m of body.matchAll(/=\s*"([a-z][a-z0-9_]+)"/g)) values.add(m[1]); + return values; +} + +describe('event-type catalog drift vs C# Constants.EventTypes', () => { + const constantsPath = findConstantsCs(); + const itOrSkip = constantsPath ? it : it.skip; + + itOrSkip('catalog + internal exactly equals the canonical C# event types', () => { + const canonical = canonicalEventTypes(readFileSync(constantsPath!, 'utf8')); + const ours = new Set(ALL_EVENT_TYPES); + + const missingInTs = [...canonical].filter((t) => !ours.has(t)).sort(); + const phantomInTs = [...ours].filter((t) => !canonical.has(t)).sort(); + + expect( + missingInTs, + 'Event types defined in Constants.EventTypes but ABSENT from the MCP catalog ' + + '(add them to a group in resource-catalog.ts — search/catalog recall is degraded without them):', + ).toEqual([]); + expect( + phantomInTs, + 'Event types in the MCP catalog with NO matching Constants.EventTypes const ' + + '(remove them or add the const — the model is told these are valid when they are not):', + ).toEqual([]); + }); + + it('public catalog excludes internal/TEMP types', () => { + const flat = Object.values(EVENT_TYPES_CATALOG).flat(); + for (const internal of INTERNAL_EVENT_TYPES) { + expect(flat, `${internal} is internal — keep it out of the public catalog`).not.toContain(internal); + } + }); + + it('no event type appears in more than one catalog group', () => { + const flat = Object.values(EVENT_TYPES_CATALOG).flat(); + const dupes = flat.filter((t, i) => flat.indexOf(t) !== i); + expect(dupes, 'duplicate event types across catalog groups').toEqual([]); + }); +}); diff --git a/src/McpServer/autopilot-monitor-mcp/src/resource-catalog.ts b/src/McpServer/autopilot-monitor-mcp/src/resource-catalog.ts index 9b33bd94..f5b4a946 100644 --- a/src/McpServer/autopilot-monitor-mcp/src/resource-catalog.ts +++ b/src/McpServer/autopilot-monitor-mcp/src/resource-catalog.ts @@ -5,14 +5,53 @@ * whose resource discovery is broken). */ +// Single source of truth for the model-facing event-type catalog. MUST stay in +// sync with C# `Constants.EventTypes` (Shared) — enforced by the vitest drift test +// in __tests__/event-types-drift.test.ts, which reads Constants.cs and asserts +// (all catalog values ∪ INTERNAL_EVENT_TYPES) === the C# const values. When the +// agent adds a new event type to Constants.EventTypes, add it to a group here too. export const EVENT_TYPES_CATALOG = { phase_events: [ 'phase_transition', 'esp_state_change', + 'esp_phase_changed', + 'esp_provisioning_status', + 'esp_ui_state', + 'esp_provisioning_raw', + 'esp_failure', + 'esp_failure_advisory', + 'esp_failure_settle_started', + 'esp_exiting', + 'esp_config_detected', 'completion_check', 'enrollment_complete', 'enrollment_failed', 'desktop_arrived', + 'desktop_detector_started', + 'desktop_detector_first_poll', + 'desktop_detector_no_candidate', + ], + whiteglove_events: [ + 'whiteglove_complete', + 'whiteglove_started', + 'whiteglove_resumed', + 'whiteglove_part1_complete', + 'whiteglove_classification', + ], + hello_events: [ + 'hello_policy_detected', + 'hello_policy_detection_mismatch', + 'hello_wait_timeout', + 'hello_completion_timeout', + 'hello_wizard_started', + 'hello_processing_started', + 'hello_processing_stopped', + 'hello_provisioning_completed', + 'hello_provisioning_failed', + 'hello_provisioning_blocked', + 'hello_pin_status', + 'hello_skipped', + 'waiting_for_hello', ], app_events: [ 'app_install_started', @@ -20,33 +59,118 @@ export const EVENT_TYPES_CATALOG = { 'app_install_failed', 'app_download_started', 'app_install_skipped', + 'download_progress', + 'do_telemetry', + 'all_apps_completed', + 'app_tracking_summary', ], - network_events: ['network_state_change', 'network_connectivity_check'], - device_info_events: [ - 'os_info', - 'hardware_spec', - 'tpm_status', - 'autopilot_profile', - 'secureboot_status', - 'bitlocker_status', + script_events: ['script_started', 'script_completed', 'script_failed'], + network_events: [ + 'network_state_change', + 'network_connectivity_check', 'network_adapters', 'network_interface_info', - 'aad_join_status', + 'dns_configuration', + 'proxy_configuration', + 'wifi_signal_info', + ], + device_info_events: [ + 'os_info', + 'boot_time', + 'hardware_spec', + 'tpm_status', + 'secureboot_status', + 'bitlocker_status', + 'autopilot_profile', 'enrollment_type_detected', + 'aad_join_status', + 'cert_validation', + 'device_location', + 'timezone_auto_set', + 'ntp_time_check', + 'power_state_check', ], - error_events: ['error_detected'], - vulnerability_events: ['software_inventory_analysis', 'vulnerability_report'], - other: [ - 'performance_snapshot', - 'log_entry', - 'gather_result', - 'script_started', - 'script_completed', - 'script_failed', + identity_events: [ + 'aad_placeholder_user_detected', + 'aad_user_joined_observed', + 'hybrid_login_pending', + ], + realmjoin_events: [ + 'realmjoin_detected', + 'realmjoin_phase_changed', + 'realmjoin_resolved', + 'realmjoin_timeout', + 'realmjoin_package_started', + 'realmjoin_package_completed', + ], + deployment_events: [ + 'modern_deployment_log', + 'modern_deployment_warning', + 'modern_deployment_error', + 'configmgr_client_detected', + ], + lifecycle_events: [ + 'agent_started', + 'agent_shutting_down', + 'agent_shutdown', + 'agent_version_check', 'ime_agent_version', + 'ime_user_session_completed', + 'ime_session_change', + 'ime_process_exited', + 'system_reboot_detected', + 'prior_run_died_with_state', + 'previous_crash_detected', + 'performance_collector_stopped', + 'agent_metrics_collector_stopped', + 'agent_unrestricted_mode_changed', + 'remote_config_fetch_failed', + 'agent_trace', ], + health_events: [ + 'stall_probe_check', + 'stall_probe_result', + 'session_stalled', + 'spool_pressure_detected', + ], + metrics_events: ['performance_snapshot', 'agent_metrics_snapshot', 'ingress_backpressure'], + security_events: [ + 'integrity_bypass_analysis', + 'local_admin_analysis', + 'security_warning', + 'security_audit', + ], + vulnerability_events: ['software_inventory_analysis', 'vulnerability_report'], + diagnostics_events: ['diagnostics_collecting', 'diagnostics_uploaded', 'diagnostics_upload_failed'], + server_action_events: [ + 'server_action_received', + 'server_action_executed', + 'server_action_failed', + 'admin_marked_session', + ], + gather_events: ['gather_result', 'gather_rules_collection_started', 'gather_rules_collection_completed'], + termination_events: ['enrollment_summary_shown', 'reboot_triggered'], + classification_events: ['enrollment_type_mismatch', 'decision_process_completion'], + other: ['error_detected', 'log_entry', 'esp_resumed', 'esp_provisioning_settle_started'], } as const; +/** + * Event types that exist on the wire but are intentionally NOT advertised in the + * public catalog (TEMP / internal). They ARE searchable (kept in ALL_EVENT_TYPES) + * so historical recall works, but the model shouldn't treat them as a normal type. + */ +export const INTERNAL_EVENT_TYPES = ['shadow_discrepancy'] as const; + +/** + * Flat list of every known event type (public catalog + internal). Used by the + * search tools for keyword→event-type candidate matching, where complete recall + * matters more than hiding TEMP types. + */ +export const ALL_EVENT_TYPES: readonly string[] = [ + ...Object.values(EVENT_TYPES_CATALOG).flat(), + ...INTERNAL_EVENT_TYPES, +]; + export const DEVICE_PROPERTIES_CATALOG = { _usage: { note: 'Use these keys in the deviceProperties parameter of search_sessions.', diff --git a/src/McpServer/autopilot-monitor-mcp/src/tools/search.ts b/src/McpServer/autopilot-monitor-mcp/src/tools/search.ts index 62aeba69..3bc3782e 100644 --- a/src/McpServer/autopilot-monitor-mcp/src/tools/search.ts +++ b/src/McpServer/autopilot-monitor-mcp/src/tools/search.ts @@ -5,6 +5,7 @@ import { withToolTelemetry } from '../telemetry.js'; import type { SearchProvider } from '../search-provider.js'; import { READ_ONLY, MAX_RESULT_SIZE_CHARS, toolResultText, SessionIdSchema } from './shared.js'; import { toolError } from './error-handler.js'; +import { ALL_EVENT_TYPES } from '../resource-catalog.js'; // ── Helpers ───────────────────────────────────────────────────────────── @@ -15,22 +16,12 @@ type EventEntry = { sessionId?: string; }; -/** All known event type strings — used for index-based pre-filtering. */ -const KNOWN_EVENT_TYPES = [ - 'phase_transition', 'esp_state_change', 'completion_check', - 'enrollment_complete', 'enrollment_failed', 'desktop_arrived', - 'app_install_started', 'app_install_completed', 'app_install_failed', - 'app_download_started', 'app_install_skipped', - 'network_state_change', 'network_connectivity_check', - 'os_info', 'hardware_spec', 'tpm_status', 'autopilot_profile', - 'secureboot_status', 'bitlocker_status', 'network_adapters', - 'network_interface_info', 'aad_join_status', 'enrollment_type_detected', - 'error_detected', 'software_inventory_analysis', 'vulnerability_report', - 'performance_snapshot', 'log_entry', 'gather_result', - 'script_started', 'script_completed', 'script_failed', 'ime_agent_version', - 'do_telemetry', 'download_progress', 'agent_started', - 'esp_phase_changed', 'shadow_discrepancy', -]; +/** + * All known event-type strings — used for index-based pre-filtering. Derived from + * the single-source catalog (resource-catalog.ts), which is drift-tested against the + * canonical C# `Constants.EventTypes`. Includes internal/TEMP types for full recall. + */ +const KNOWN_EVENT_TYPES = ALL_EVENT_TYPES; /** Match query keywords against known event types using prefix-aware matching. */ function extractEventTypeCandidates(keywords: string[]): string[] { @@ -392,20 +383,16 @@ export function registerSearchTools(server: McpServer, knowledgeBase: SearchProv title: 'Search Events (Semantic)', description: 'TIER 1 — FAST EVENT SEARCH (try this first). ' + - 'Searches enrollment events by matching keywords against event type, message, source, severity, and data fields. ' + - 'Uses prefix-aware matching (e.g. "install" matches "installation", "installed", "app_install_failed") ' + - 'and weighted field scoring (matches in eventType rank higher than in data). ' + - 'When the query implies a problem (error/fail/stuck/timeout…), ranking lifts failure/Warning events and damps ' + - 'benign Info/Trace events. `matchedSessionIds` lists the distinct sessions behind the ranked hits (drill into ' + - 'these next); `sessionsSearchedCount` is how many sessions were scanned. ' + - 'IMPORTANT — cross-session recall is event-TYPE-driven: with no sessionId, the scan first maps your keywords to ' + - 'known event types (see the event_types catalog) and only fetches events of those types. A concept that has no ' + - 'event type of its own (e.g. "certificate") will NOT surface cross-session even when it lives inside another ' + - 'event\'s data/message — so if a keyword maps to no event type, that info is likely buried elsewhere: pass a ' + - 'sessionId (which scans EVERY field of EVERY event in that session) or search by a related event type instead. ' + - 'Provide sessionId to search within one session, or omit for a fast (single-page-per-type) cross-session scan. ' + - 'This is a bounded scan: check the returned `truncated` flag — if true, recall is incomplete, so escalate to ' + - 'deep_search_events for a broader multi-page scan or narrow the query as `recallNote` suggests.', + 'Matches keywords against event type, message, source, severity, and data, prefix-aware (e.g. "install" ' + + 'matches "app_install_failed") with weighted scoring (eventType > data). Problem queries ' + + '(error/fail/stuck/timeout…) lift failure/Warning events and damp benign Info/Trace. ' + + '`matchedSessionIds` are the sessions behind the ranked hits (drill in next); `sessionsSearchedCount` is how ' + + 'many were scanned. ' + + 'IMPORTANT — cross-session recall is event-TYPE-driven: without a sessionId the scan maps keywords to known ' + + 'event types (see event_types catalog) and fetches only those. A term with no event type of its own (e.g. ' + + '"certificate") never surfaces cross-session even when it sits in another event\'s data — if `keywordsUsed` ' + + 'shows it mapped to none, pass a sessionId (scans EVERY field of EVERY event) or search by a related type. ' + + 'Bounded scan: if `truncated` is true, recall is incomplete — escalate to deep_search_events or narrow per `recallNote`.', inputSchema: { query: z.string().describe('Natural language description of what to find (e.g. "app download stuck", "certificate error", "disk space low")'), sessionId: SessionIdSchema.optional().describe('Search within a specific session. If omitted, searches across recent failed sessions.'), @@ -546,24 +533,17 @@ export function registerSearchTools(server: McpServer, knowledgeBase: SearchProv title: 'Deep Search Events', description: 'TIER 3 — DEEP SEARCH (thorough, use when accuracy is critical). ' + - 'Uses the same weighted keyword scoring as search_events_semantic but with lower thresholds and a broader, ' + - 'paginated scan: for each event type matched from the query it walks the cross-session index, following pages ' + - 'up to a generous budget, then ranks the matches. Scores across ALL event fields including full DataJson content. ' + - 'Returns the top `topK` ranked matches (NOT every matching event) — compare `resultCount` vs `eventsMatched`, ' + - 'and use get_session_events / search_sessions_by_event for full per-event recall. ' + - 'When the query implies a problem (error/fail/stuck/timeout…), ranking lifts failure/Warning events and damps ' + - 'benign Info/Trace events so a successful enrollment can\'t outrank a real error. ' + - '`matchedSessionIds` lists the distinct sessions behind the ranked hits (drill into these next); ' + - '`sessionsSearchedCount` is how many sessions were scanned. ' + - 'IMPORTANT — cross-session recall is event-TYPE-driven: with no sessionId the scan maps your keywords to known ' + - 'event types (see the event_types catalog) and only fetches events of those types; it then scores ALL their ' + - 'fields including full DataJson, but a concept with no event type of its own (e.g. "certificate") is never ' + - 'reached cross-session even when it lives inside another event\'s data. If `keywordsUsed` shows your key term ' + - 'mapped to no event type, that info is likely buried elsewhere — pass a sessionId (scans EVERY field of EVERY ' + - 'event in the session, complete and untruncated) or search by a related event type. ' + - 'Check the `truncated` flag: if true, the scan hit its page/time budget (or fell back to recent failed ' + - 'sessions) and recall is incomplete — narrow the query as `recallNote` suggests. ' + - 'Provide sessionId to search within one session (complete, no truncation), or omit to search across sessions.' + + 'Same weighted scoring as search_events_semantic but lower thresholds and a broader multi-page scan per event ' + + 'type, ranking across ALL fields incl. full DataJson. Problem queries (error/fail/stuck/timeout…) lift ' + + 'failure/Warning events and damp benign Info/Trace. Returns the top `topK` (NOT every match) — compare ' + + '`resultCount` vs `eventsMatched`; use get_session_events / search_sessions_by_event for full per-event recall. ' + + '`matchedSessionIds` are the sessions behind the ranked hits (drill in next); `sessionsSearchedCount` is how ' + + 'many were scanned. ' + + 'IMPORTANT — cross-session recall is event-TYPE-driven: without a sessionId the scan maps keywords to known ' + + 'event types (see event_types catalog) and fetches only those. A term with no event type of its own (e.g. ' + + '"certificate") never surfaces cross-session even when it sits in another event\'s data — if `keywordsUsed` ' + + 'shows it mapped to none, pass a sessionId (scans EVERY field of EVERY event, complete) or search by a related type. ' + + 'If `truncated` is true, recall is incomplete — narrow per `recallNote`.' + (ga ? ' Omit tenantId for cross-tenant search (Global Admin), or specify tenantId for single-tenant.' : ''), inputSchema: { query: z.string().describe('Natural language description of what to find (e.g. "app download stuck", "certificate error", "disk space low")'), diff --git a/src/Shared/AutopilotMonitor.DecisionCore/Engine/DecisionEngine.Classic.cs b/src/Shared/AutopilotMonitor.DecisionCore/Engine/DecisionEngine.Classic.cs index f86f1cfe..2cb57b52 100644 --- a/src/Shared/AutopilotMonitor.DecisionCore/Engine/DecisionEngine.Classic.cs +++ b/src/Shared/AutopilotMonitor.DecisionCore/Engine/DecisionEngine.Classic.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using AutopilotMonitor.DecisionCore.Signals; using AutopilotMonitor.DecisionCore.State; using AutopilotMonitor.Shared.Models; +using SharedConstants = AutopilotMonitor.Shared.Constants; namespace AutopilotMonitor.DecisionCore.Engine { @@ -664,7 +665,7 @@ namespace AutopilotMonitor.DecisionCore.Engine kind: DecisionEffectKind.EmitEventTimelineEntry, parameters: new Dictionary { - ["eventType"] = "phase_transition", + ["eventType"] = SharedConstants.EventTypes.PhaseTransition, ["phase"] = phase.ToString(), ["source"] = "DecisionEngine", ["message"] = $"Phase: {phase}", @@ -694,7 +695,7 @@ namespace AutopilotMonitor.DecisionCore.Engine kind: DecisionEffectKind.EmitEventTimelineEntry, parameters: new Dictionary { - ["eventType"] = "enrollment_complete", + ["eventType"] = SharedConstants.EventTypes.EnrollmentComplete, }, typedPayload: DecisionAuditTrailBuilder.Build( postState: state, diff --git a/src/Shared/AutopilotMonitor.DecisionCore/Engine/DecisionEngine.Edge.cs b/src/Shared/AutopilotMonitor.DecisionCore/Engine/DecisionEngine.Edge.cs index 8b48a38c..806b3499 100644 --- a/src/Shared/AutopilotMonitor.DecisionCore/Engine/DecisionEngine.Edge.cs +++ b/src/Shared/AutopilotMonitor.DecisionCore/Engine/DecisionEngine.Edge.cs @@ -315,7 +315,7 @@ namespace AutopilotMonitor.DecisionCore.Engine { ["eventType"] = advisoryPath ? SharedConstants.EventTypes.EspFailureAdvisory - : "enrollment_failed", + : SharedConstants.EventTypes.EnrollmentFailed, ["reason"] = reason, }; diff --git a/src/Shared/AutopilotMonitor.DecisionCore/Engine/DecisionEngine.Shared.cs b/src/Shared/AutopilotMonitor.DecisionCore/Engine/DecisionEngine.Shared.cs index 793d551d..79c609c4 100644 --- a/src/Shared/AutopilotMonitor.DecisionCore/Engine/DecisionEngine.Shared.cs +++ b/src/Shared/AutopilotMonitor.DecisionCore/Engine/DecisionEngine.Shared.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using AutopilotMonitor.DecisionCore.Signals; using AutopilotMonitor.DecisionCore.State; using AutopilotMonitor.Shared.Models; +using SharedConstants = AutopilotMonitor.Shared.Constants; namespace AutopilotMonitor.DecisionCore.Engine { @@ -122,7 +123,7 @@ namespace AutopilotMonitor.DecisionCore.Engine DecisionEffectKind.EmitEventTimelineEntry, parameters: new Dictionary(StringComparer.Ordinal) { - ["eventType"] = "enrollment_failed", + ["eventType"] = SharedConstants.EventTypes.EnrollmentFailed, ["reason"] = reason, }, typedPayload: DecisionAuditTrailBuilder.Build( @@ -264,7 +265,7 @@ namespace AutopilotMonitor.DecisionCore.Engine var succeeded = string.Equals(adminOutcome, "Succeeded", StringComparison.OrdinalIgnoreCase); var toStage = succeeded ? SessionStage.Completed : SessionStage.Failed; - var eventType = succeeded ? "enrollment_complete" : "enrollment_failed"; + var eventType = succeeded ? SharedConstants.EventTypes.EnrollmentComplete : SharedConstants.EventTypes.EnrollmentFailed; var nextStep = state.StepIndex + 1; var newState = state.ToBuilder() diff --git a/src/Shared/AutopilotMonitor.DecisionCore/Engine/DecisionEngine.WhiteGlove.cs b/src/Shared/AutopilotMonitor.DecisionCore/Engine/DecisionEngine.WhiteGlove.cs index 02a13b2a..e694a69b 100644 --- a/src/Shared/AutopilotMonitor.DecisionCore/Engine/DecisionEngine.WhiteGlove.cs +++ b/src/Shared/AutopilotMonitor.DecisionCore/Engine/DecisionEngine.WhiteGlove.cs @@ -4,6 +4,7 @@ using AutopilotMonitor.DecisionCore.Classifiers; using AutopilotMonitor.DecisionCore.Signals; using AutopilotMonitor.DecisionCore.State; using AutopilotMonitor.Shared.Models; +using SharedConstants = AutopilotMonitor.Shared.Constants; namespace AutopilotMonitor.DecisionCore.Engine { @@ -114,7 +115,7 @@ namespace AutopilotMonitor.DecisionCore.Engine { new DecisionEffect( DecisionEffectKind.EmitEventTimelineEntry, - parameters: new Dictionary { ["eventType"] = "whiteglove_complete" }, + parameters: new Dictionary { ["eventType"] = SharedConstants.EventTypes.WhiteGloveComplete }, typedPayload: DecisionAuditTrailBuilder.Build( postState: sealedState, decidedStage: SessionStage.WhiteGloveSealed, @@ -231,7 +232,7 @@ namespace AutopilotMonitor.DecisionCore.Engine { new DecisionEffect( DecisionEffectKind.EmitEventTimelineEntry, - parameters: new Dictionary { ["eventType"] = "whiteglove_complete" }, + parameters: new Dictionary { ["eventType"] = SharedConstants.EventTypes.WhiteGloveComplete }, typedPayload: DecisionAuditTrailBuilder.Build( postState: sealedState, decidedStage: SessionStage.WhiteGloveSealed, diff --git a/src/Shared/AutopilotMonitor.Shared/Constants.cs b/src/Shared/AutopilotMonitor.Shared/Constants.cs index b115a451..62f7173d 100644 --- a/src/Shared/AutopilotMonitor.Shared/Constants.cs +++ b/src/Shared/AutopilotMonitor.Shared/Constants.cs @@ -324,6 +324,110 @@ namespace AutopilotMonitor.Shared public const string RealmJoinTimeout = "realmjoin_timeout"; public const string RealmJoinPackageStarted = "realmjoin_package_started"; public const string RealmJoinPackageCompleted = "realmjoin_package_completed"; + + // ===================================================================== + // Single-source consolidation (2026-05-29): event-type string literals + // that were previously emitted inline across V2 collectors/trackers are + // now defined here so Constants.EventTypes is the ONE canonical catalog. + // Values are byte-identical to the former literals (persisted strings — + // rules/UI/MCP match on them). A drift guard test enforces that no V2 + // emit site uses a raw literal not defined here. + // ===================================================================== + + // Device info / environment (DeviceInfoCollector + StartupEnvironmentProbes) + public const string OsInfo = "os_info"; + public const string BootTime = "boot_time"; + public const string HardwareSpec = "hardware_spec"; + public const string NetworkAdapters = "network_adapters"; + public const string DnsConfiguration = "dns_configuration"; + public const string ProxyConfiguration = "proxy_configuration"; + public const string AutopilotProfile = "autopilot_profile"; + public const string EnrollmentTypeDetected = "enrollment_type_detected"; + public const string SecureBootStatus = "secureboot_status"; + public const string BitLockerStatus = "bitlocker_status"; + public const string TpmStatus = "tpm_status"; + public const string AadJoinStatus = "aad_join_status"; + public const string EspConfigDetected = "esp_config_detected"; + public const string NetworkInterfaceInfo = "network_interface_info"; + public const string WifiSignalInfo = "wifi_signal_info"; + public const string DeviceLocation = "device_location"; + public const string TimezoneAutoSet = "timezone_auto_set"; + public const string NtpTimeCheck = "ntp_time_check"; + public const string PowerStateCheck = "power_state_check"; + public const string AgentTrace = "agent_trace"; + + // Crash / process lifecycle + public const string PreviousCrashDetected = "previous_crash_detected"; + public const string ImeProcessExited = "ime_process_exited"; + + // White Glove / pre-provisioning + public const string WhiteGloveStarted = "whiteglove_started"; + public const string WhiteGloveResumed = "whiteglove_resumed"; + public const string WhiteGlovePart1Complete = "whiteglove_part1_complete"; + + // Windows Hello for Business provisioning + public const string HelloPolicyDetected = "hello_policy_detected"; + public const string HelloPolicyDetectionMismatch = "hello_policy_detection_mismatch"; + public const string HelloWaitTimeout = "hello_wait_timeout"; + public const string HelloCompletionTimeout = "hello_completion_timeout"; + public const string HelloWizardStarted = "hello_wizard_started"; + public const string HelloProcessingStarted = "hello_processing_started"; + public const string HelloProcessingStopped = "hello_processing_stopped"; + public const string HelloProvisioningCompleted = "hello_provisioning_completed"; + public const string HelloProvisioningFailed = "hello_provisioning_failed"; + public const string HelloProvisioningBlocked = "hello_provisioning_blocked"; + public const string HelloPinStatus = "hello_pin_status"; + public const string HelloSkipped = "hello_skipped"; + + // ESP provisioning detail + public const string EspFailureSettleStarted = "esp_failure_settle_started"; + public const string EspProvisioningRaw = "esp_provisioning_raw"; + public const string EspFailure = "esp_failure"; + public const string EspExiting = "esp_exiting"; + + // Security / inventory analyzers + public const string IntegrityBypassAnalysis = "integrity_bypass_analysis"; + public const string LocalAdminAnalysis = "local_admin_analysis"; + public const string SecurityWarning = "security_warning"; + + // Termination / diagnostics / server actions + public const string EnrollmentSummaryShown = "enrollment_summary_shown"; + public const string DiagnosticsCollecting = "diagnostics_collecting"; + public const string DiagnosticsUploaded = "diagnostics_uploaded"; + public const string DiagnosticsUploadFailed = "diagnostics_upload_failed"; + public const string RebootTriggered = "reboot_triggered"; + public const string ServerActionReceived = "server_action_received"; + public const string ServerActionExecuted = "server_action_executed"; + public const string ServerActionFailed = "server_action_failed"; + public const string AdminMarkedSession = "admin_marked_session"; + public const string RemoteConfigFetchFailed = "remote_config_fetch_failed"; + public const string AgentUnrestrictedModeChanged = "agent_unrestricted_mode_changed"; + + // Gather-rules collection mode + public const string GatherRulesCollectionStarted = "gather_rules_collection_started"; + public const string GatherRulesCollectionCompleted = "gather_rules_collection_completed"; + + // Agent self-metrics / back-pressure + public const string SpoolPressureDetected = "spool_pressure_detected"; + public const string AgentMetricsSnapshot = "agent_metrics_snapshot"; + public const string IngressBackpressure = "ingress_backpressure"; + + // --------------------------------------------------------------------- + // V1 (legacy) event types — emitted ONLY by the decommissioning V1 agent + // (Agent.Core / Agent). Defined here so the canonical catalog and the MCP + // search/catalog stay complete for historical V1 session analysis. V1 emit + // sites are intentionally NOT refactored to use these (V1 is going away). + // --------------------------------------------------------------------- + public const string ConfigMgrClientDetected = "configmgr_client_detected"; + public const string EnrollmentTypeMismatch = "enrollment_type_mismatch"; + public const string DecisionProcessCompletion = "decision_process_completion"; + public const string WhiteGloveClassification = "whiteglove_classification"; + public const string EspResumed = "esp_resumed"; + public const string WaitingForHello = "waiting_for_hello"; + public const string EspProvisioningSettleStarted = "esp_provisioning_settle_started"; + public const string ImeSessionChange = "ime_session_change"; + public const string SecurityAudit = "security_audit"; + public const string AgentShutdown = "agent_shutdown"; // distinct from V2 AgentShuttingDown } // -----------------------------------------------------------------------