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)
};
}