diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Configuration/BuiltInConfigCatalogTests.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Configuration/BuiltInConfigCatalogTests.cs new file mode 100644 index 00000000..bab9e3a2 --- /dev/null +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Configuration/BuiltInConfigCatalogTests.cs @@ -0,0 +1,62 @@ +using System.Linq; +using AutopilotMonitor.Agent.V2.Core.Configuration; +using Xunit; + +namespace AutopilotMonitor.Agent.V2.Core.Tests.Configuration +{ + /// + /// Pins the compiled-in fallback rule catalogs (): + /// the agent embeds the SAME CI-combined rules/dist artifacts the backend serves, so a + /// no-config/no-cache session still runs full IME tracking. These tests guard the + /// embedding + deserialization end-to-end — a broken LogicalName, a moved resource or a + /// shape change in the combined JSON must fail HERE, not silently strip the fallback + /// back down to zero patterns in the field. + /// + public class BuiltInConfigCatalogTests + { + [Fact] + public void ImeLogPatterns_LoadFromEmbeddedCatalog_EnabledOnly() + { + var patterns = BuiltInConfigCatalog.GetEnabledImeLogPatterns(logger: null); + + Assert.NotEmpty(patterns); + Assert.All(patterns, p => + { + Assert.True(p.Enabled); + Assert.False(string.IsNullOrWhiteSpace(p.PatternId)); + Assert.False(string.IsNullOrWhiteSpace(p.Pattern)); + Assert.False(string.IsNullOrWhiteSpace(p.Action)); + Assert.False(string.IsNullOrWhiteSpace(p.Category)); + }); + Assert.Equal(patterns.Count, patterns.Select(p => p.PatternId).Distinct().Count()); + } + + [Fact] + public void ImeLogPatterns_CarryCoreTrackingActions() + { + // The app-tracking backbone: without these two actions the ImeLogTracker cannot + // attribute installs to apps or follow ESP phases. Their presence proves the + // deserialization actually populated the action field (a shape drift in the + // combined JSON would otherwise still pass the structural checks above). + var actions = BuiltInConfigCatalog.GetEnabledImeLogPatterns(logger: null) + .Select(p => p.Action).ToHashSet(); + + Assert.Contains("setCurrentApp", actions); + Assert.Contains("espPhaseDetected", actions); + } + + [Fact] + public void GatherRules_LoadFromEmbeddedCatalog_EnabledOnly() + { + // The built-in gather-rule catalog ships mostly disabled rules — only the + // default-enabled ones may reach the fallback config. + var rules = BuiltInConfigCatalog.GetEnabledGatherRules(logger: null); + + Assert.All(rules, r => + { + Assert.True(r.Enabled); + Assert.False(string.IsNullOrWhiteSpace(r.RuleId)); + }); + } + } +} diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Configuration/RemoteConfigServiceRetryTests.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Configuration/RemoteConfigServiceRetryTests.cs index 936b652f..eea8e644 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Configuration/RemoteConfigServiceRetryTests.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Configuration/RemoteConfigServiceRetryTests.cs @@ -114,6 +114,22 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Configuration Assert.Equal(1, svc.LastFetchAttempts); } + [Fact] + public async Task FetchConfig_DefaultsFallback_CarriesBuiltInCatalogs() + { + // A no-config/no-cache session must still get the compiled-in rule catalogs — + // an empty ImeLogPatterns list kills the entire IME tracking pipeline (the + // pre-catalog behaviour this pins against regressing to). + var svc = NewService(BackendBehaviour.AlwaysTransientFailure); + + var result = await svc.FetchConfigAsync(retryOnTransientErrors: true); + + Assert.Equal(0, result.ConfigVersion); + Assert.NotEmpty(result.ImeLogPatterns); + Assert.All(result.ImeLogPatterns, p => Assert.True(p.Enabled)); + Assert.All(result.GatherRules, r => Assert.True(r.Enabled)); + } + [Fact] public async Task FetchConfig_EmptyResponse_TreatedAsTransient() { diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/AutopilotMonitor.Agent.V2.Core.csproj b/src/Agent/AutopilotMonitor.Agent.V2.Core/AutopilotMonitor.Agent.V2.Core.csproj index 71fa6ca8..4243dcd8 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/AutopilotMonitor.Agent.V2.Core.csproj +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/AutopilotMonitor.Agent.V2.Core.csproj @@ -52,6 +52,11 @@ carries the V2 assembly namespace so GatherRuleGuards locates it under "AutopilotMonitor.Agent.V2.Core.Resources.guardrails.json". --> + + + diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Configuration/BuiltInConfigCatalog.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Configuration/BuiltInConfigCatalog.cs new file mode 100644 index 00000000..6be2022c --- /dev/null +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Configuration/BuiltInConfigCatalog.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using AutopilotMonitor.Agent.V2.Core.Logging; +using AutopilotMonitor.Shared.Models; +using Newtonsoft.Json; + +namespace AutopilotMonitor.Agent.V2.Core.Configuration +{ + /// + /// Compiled-in fallback copies of the global rule catalogs, embedded from the same + /// CI-generated rules/dist artifacts the backend seeds and serves — single + /// source by construction: each agent release snapshots the catalog state of its own + /// build, so the offline fallback can never drift further than the running agent + /// version itself (no manually maintained default to forget). + /// + /// Consumed only by 's built-in default config, + /// i.e. when a session has NO live config and NO cache. Before this existed, that + /// path shipped an empty ImeLogPatterns list, which left the entire IME + /// tracking pipeline (app installs, platform/remediation scripts, health-script + /// compliance, IME ESP-phase signals) dead for the whole session — the core of + /// enrollment monitoring. A live fetch (startup, post-registration recovery or + /// rotate_config) always wins over this catalog. + /// + /// Fail-soft: any load/parse error returns an empty list (the pre-catalog behaviour). + /// + internal static class BuiltInConfigCatalog + { + /// Enabled built-in IME log patterns from the embedded catalog. + public static List GetEnabledImeLogPatterns(AgentLogger logger) + => LoadEnabled("ime-log-patterns.json", p => p.Enabled, logger); + + /// Enabled built-in gather rules from the embedded catalog. + public static List GetEnabledGatherRules(AgentLogger logger) + => LoadEnabled("gather-rules.json", r => r.Enabled, logger); + + private static List LoadEnabled(string name, Func isEnabled, AgentLogger logger) + { + try + { + var assembly = typeof(BuiltInConfigCatalog).Assembly; + using (var stream = assembly.GetManifestResourceStream( + "AutopilotMonitor.Agent.V2.Core.Resources." + name)) + { + if (stream == null) + { + logger?.Warning($"Built-in config catalog resource '{name}' not found — fallback runs without it."); + return new List(); + } + + using (var reader = new StreamReader(stream)) + { + var wrapper = JsonConvert.DeserializeObject>(reader.ReadToEnd()); + var all = wrapper?.Rules ?? new List(); + return all.Where(isEnabled).ToList(); + } + } + } + catch (Exception ex) + { + logger?.Warning($"Built-in config catalog '{name}' failed to load ({ex.Message}) — fallback runs without it."); + return new List(); + } + } + + /// Shape of the CI-combined catalog files: { "rules": [...] }. + private sealed class RulesFile + { + [JsonProperty("rules")] + public List Rules { get; set; } + } + } +} diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Configuration/RemoteConfigService.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Configuration/RemoteConfigService.cs index 37cbf064..ec62ec10 100644 --- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Configuration/RemoteConfigService.cs +++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Configuration/RemoteConfigService.cs @@ -231,9 +231,12 @@ namespace AutopilotMonitor.Agent.V2.Core.Configuration return cached; } - // Fall back to defaults - _logger.Info("No cached config available, using defaults (all optional collectors disabled)"); + // Fall back to built-in defaults (collector/analyzer defaults from the shared + // model initializers + the compiled-in rule catalogs). var defaults = CreateDefaultConfig(); + _logger.Info($"No cached config available, using built-in defaults (ConfigVersion=0, " + + $"{defaults.ImeLogPatterns?.Count ?? 0} built-in IME patterns, " + + $"{defaults.GatherRules?.Count ?? 0} built-in gather rules)"); SetConfig(defaults); LastFetchOutcome = RemoteConfigFetchOutcome.UsedDefaults; return defaults; @@ -393,6 +396,11 @@ namespace AutopilotMonitor.Agent.V2.Core.Configuration private AgentConfigResponse CreateDefaultConfig() { + // Everything not set here falls to the shared AgentConfigResponse property + // initializers — the same values the backend uses as its `??` fallbacks, so the + // scalar knobs cannot drift. The rule catalogs are the one thing initializers + // cannot supply: without them the whole IME tracking pipeline is dead, so they + // come from the compiled-in snapshot of the served catalogs (BuiltInConfigCatalog). return new AgentConfigResponse { ConfigVersion = 0, @@ -404,8 +412,8 @@ namespace AutopilotMonitor.Agent.V2.Core.Configuration EnableGatherRuleDebugLog = false, EnableEspContinueAnywayObservation = false, Collectors = CollectorConfiguration.CreateDefault(), - GatherRules = new System.Collections.Generic.List(), - ImeLogPatterns = new System.Collections.Generic.List() + GatherRules = BuiltInConfigCatalog.GetEnabledGatherRules(_logger), + ImeLogPatterns = BuiltInConfigCatalog.GetEnabledImeLogPatterns(_logger) }; }