diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Runtime/StartupEnvironmentProbesTests.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Runtime/StartupEnvironmentProbesTests.cs
new file mode 100644
index 00000000..a6722f01
--- /dev/null
+++ b/src/Agent/AutopilotMonitor.Agent.V2.Core.Tests/Runtime/StartupEnvironmentProbesTests.cs
@@ -0,0 +1,182 @@
+#nullable enable
+using System.Collections.Generic;
+using AutopilotMonitor.Agent.V2.Core.Configuration;
+using AutopilotMonitor.Agent.V2.Core.Monitoring.Transport;
+using AutopilotMonitor.Agent.V2.Core.Runtime;
+using AutopilotMonitor.Shared.Models;
+using Xunit;
+
+namespace AutopilotMonitor.Agent.V2.Core.Tests.Runtime
+{
+ ///
+ /// M4.6.γ — event-shape tests for the Legacy-parity startup probes. We cover the pure event
+ /// builders directly; the end-to-end probe runner itself needs live HTTP/UDP/tzutil (no value
+ /// in unit testing — excercised via the V2-agent test-VM smoke run deferred to Follow-Up).
+ ///
+ public sealed class StartupEnvironmentProbesTests
+ {
+ private static AgentConfiguration Config(bool geoEnabled = true, bool tzAutoSet = false, string? ntpServer = null) =>
+ new AgentConfiguration
+ {
+ SessionId = "S1",
+ TenantId = "T1",
+ EnableGeoLocation = geoEnabled,
+ EnableTimezoneAutoSet = tzAutoSet,
+ NtpServer = ntpServer,
+ };
+
+ // ================================================================= Geo success
+
+ [Fact]
+ public void BuildGeoEvent_returns_info_device_location_event_with_all_fields()
+ {
+ var cfg = Config();
+ var loc = new GeoLocationResult
+ {
+ Country = "DE",
+ Region = "BW",
+ City = "Stuttgart",
+ Loc = "48.7,9.1",
+ Timezone = "Europe/Berlin",
+ Source = "ipinfo.io",
+ };
+
+ var evt = StartupEnvironmentProbes.BuildGeoEvent(cfg, loc);
+
+ Assert.Equal("device_location", evt.EventType);
+ Assert.Equal(EventSeverity.Info, evt.Severity);
+ Assert.Equal("Network", evt.Source);
+ Assert.Equal("S1", evt.SessionId);
+ Assert.Equal("T1", evt.TenantId);
+ Assert.True(evt.ImmediateUpload);
+ Assert.Contains("Stuttgart", evt.Message);
+ Assert.Equal("DE", evt.Data["country"]);
+ Assert.Equal("ipinfo.io", evt.Data["source"]);
+ }
+
+ // ================================================================= Geo failure
+
+ [Fact]
+ public void BuildGeoFailureEvent_returns_warning_agent_trace_with_provider_errors()
+ {
+ var cfg = Config();
+ var attempt = new GeoLocationAttemptResult
+ {
+ PrimaryError = "connection refused",
+ PrimaryRetryError = "DNS failure",
+ FallbackError = "timeout",
+ };
+
+ var evt = StartupEnvironmentProbes.BuildGeoFailureEvent(cfg, attempt);
+
+ Assert.Equal("agent_trace", evt.EventType);
+ Assert.Equal(EventSeverity.Warning, evt.Severity);
+ Assert.Equal("geo_location_failed", evt.Data["decision"]);
+ Assert.Equal("connection refused", evt.Data["primaryError"]);
+ Assert.Equal("DNS failure", evt.Data["primaryRetryError"]);
+ Assert.Equal("timeout", evt.Data["fallbackError"]);
+ }
+
+ [Fact]
+ public void BuildGeoFailureEvent_handles_null_attempt_gracefully()
+ {
+ var evt = StartupEnvironmentProbes.BuildGeoFailureEvent(Config(), attempt: null);
+
+ Assert.Equal("agent_trace", evt.EventType);
+ Assert.Equal("unknown", evt.Data["primaryError"]);
+ Assert.Equal("unknown", evt.Data["primaryRetryError"]);
+ Assert.Equal("unknown", evt.Data["fallbackError"]);
+ }
+
+ // ================================================================= Timezone
+
+ [Fact]
+ public void BuildTimezoneEvent_info_on_success()
+ {
+ var tz = new TimezoneSetResult
+ {
+ Success = true,
+ IanaTimezone = "Europe/Berlin",
+ WindowsTimezoneId = "W. Europe Standard Time",
+ PreviousTimezone = "UTC",
+ };
+
+ var evt = StartupEnvironmentProbes.BuildTimezoneEvent(Config(), tz);
+
+ Assert.Equal("timezone_auto_set", evt.EventType);
+ Assert.Equal(EventSeverity.Info, evt.Severity);
+ Assert.Contains("W. Europe Standard Time", evt.Message);
+ Assert.Equal(true, evt.Data["success"]);
+ }
+
+ [Fact]
+ public void BuildTimezoneEvent_warning_on_failure()
+ {
+ var tz = new TimezoneSetResult
+ {
+ Success = false,
+ IanaTimezone = "Europe/Berlin",
+ Error = "tzutil failed",
+ };
+
+ var evt = StartupEnvironmentProbes.BuildTimezoneEvent(Config(), tz);
+
+ Assert.Equal(EventSeverity.Warning, evt.Severity);
+ Assert.Contains("tzutil failed", evt.Message);
+ Assert.Equal("tzutil failed", evt.Data["error"]);
+ }
+
+ // ================================================================= NTP
+
+ [Fact]
+ public void BuildNtpEvent_info_for_small_offset()
+ {
+ var r = new NtpCheckResult
+ {
+ Success = true,
+ OffsetSeconds = 2.3,
+ NtpTime = new System.DateTime(2026, 4, 21, 10, 0, 0, System.DateTimeKind.Utc),
+ LocalTime = new System.DateTime(2026, 4, 21, 10, 0, 2, System.DateTimeKind.Utc),
+ };
+
+ var evt = StartupEnvironmentProbes.BuildNtpEvent(Config(), "time.windows.com", r);
+
+ Assert.Equal("ntp_time_check", evt.EventType);
+ Assert.Equal(EventSeverity.Info, evt.Severity);
+ Assert.Equal("time.windows.com", evt.Data["ntpServer"]);
+ Assert.Equal(2.3, evt.Data["offsetSeconds"]);
+ }
+
+ [Fact]
+ public void BuildNtpEvent_warning_for_large_offset()
+ {
+ var r = new NtpCheckResult
+ {
+ Success = true,
+ OffsetSeconds = 120.0,
+ NtpTime = System.DateTime.UtcNow,
+ LocalTime = System.DateTime.UtcNow,
+ };
+
+ var evt = StartupEnvironmentProbes.BuildNtpEvent(Config(), "time.windows.com", r);
+
+ Assert.Equal(EventSeverity.Warning, evt.Severity);
+ }
+
+ [Fact]
+ public void BuildNtpEvent_warning_on_failure_with_error_detail()
+ {
+ var r = new NtpCheckResult
+ {
+ Success = false,
+ Error = "DNS resolution failed",
+ };
+
+ var evt = StartupEnvironmentProbes.BuildNtpEvent(Config(), "time.windows.com", r);
+
+ Assert.Equal(EventSeverity.Warning, evt.Severity);
+ Assert.Contains("DNS resolution failed", evt.Message);
+ Assert.Equal("DNS resolution failed", evt.Data["error"]);
+ }
+ }
+}
diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Orchestration/DefaultComponentFactory.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Orchestration/DefaultComponentFactory.cs
index d490f41e..2d6fd300 100644
--- a/src/Agent/AutopilotMonitor.Agent.V2.Core/Orchestration/DefaultComponentFactory.cs
+++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Orchestration/DefaultComponentFactory.cs
@@ -166,6 +166,21 @@ namespace AutopilotMonitor.Agent.V2.Core.Orchestration
_networkMetrics, _agentVersion, collectors.AgentSelfMetricsIntervalSeconds));
}
+ // M4.6.γ — Delivery-Optimization telemetry. Dormant-by-default: only polls when the
+ // IME log tracker reports an app entering Downloading/Installing (see AppStateChanged
+ // chain below). Needs the IME tracker's PackageStates + OnDoTelemetryReceived hook.
+ if (collectors.EnableDeliveryOptimizationCollector && _imeLogHost != null)
+ {
+ var doHost = new DeliveryOptimizationHost(
+ sessionId: sessionId,
+ tenantId: tenantId,
+ onEnrollmentEvent: onEnrollmentEvent,
+ logger: logger,
+ intervalSeconds: collectors.DeliveryOptimizationIntervalSeconds,
+ imeHost: _imeLogHost);
+ hosts.Add(doHost);
+ }
+
return hosts;
}
@@ -287,10 +302,17 @@ namespace AutopilotMonitor.Agent.V2.Core.Orchestration
///
/// Exposes the tracker's live package-state list for peripheral consumers
- /// (M4.6.β FinalStatusBuilder).
+ /// (M4.6.β FinalStatusBuilder, M4.6.γ DeliveryOptimizationHost).
///
public Monitoring.Enrollment.Ime.AppPackageStateList PackageStates => _tracker.PackageStates;
+ ///
+ /// Reference to the wrapped IME tracker for co-collector wiring. Used by
+ /// DeliveryOptimizationHost to set OnDoTelemetryReceived and to chain
+ /// OnAppStateChanged for dormant/wake-up transitions.
+ ///
+ internal Monitoring.Enrollment.Ime.ImeLogTracker Tracker => _tracker;
+
public ImeLogHost(
string sessionId,
string tenantId,
@@ -457,6 +479,81 @@ namespace AutopilotMonitor.Agent.V2.Core.Orchestration
}
}
+ private sealed class DeliveryOptimizationHost : ICollectorHost
+ {
+ public string Name => "DeliveryOptimizationCollector";
+
+ private readonly DeliveryOptimizationCollector _collector;
+ private readonly ImeLogHost _imeHost;
+ private readonly AgentLogger _logger;
+ private Action? _prevStateChanged;
+ private int _disposed;
+
+ public DeliveryOptimizationHost(
+ string sessionId,
+ string tenantId,
+ Action onEnrollmentEvent,
+ AgentLogger logger,
+ int intervalSeconds,
+ ImeLogHost imeHost)
+ {
+ _imeHost = imeHost;
+ _logger = logger;
+
+ _collector = new DeliveryOptimizationCollector(
+ sessionId: sessionId,
+ tenantId: tenantId,
+ emitEvent: onEnrollmentEvent,
+ logger: logger,
+ intervalSeconds: intervalSeconds,
+ getPackageStates: () => imeHost.PackageStates,
+ onDoTelemetryReceived: pkg =>
+ {
+ try { imeHost.Tracker.OnDoTelemetryReceived?.Invoke(pkg); }
+ catch (Exception ex) { logger.Warning($"DeliveryOptimizationHost: OnDoTelemetryReceived invocation threw: {ex.Message}"); }
+ },
+ logDirectory: Environment.ExpandEnvironmentVariables(Shared.Constants.LogDirectory));
+ }
+
+ public void Start()
+ {
+ // Chain ourselves into the tracker's OnAppStateChanged so the DO collector wakes
+ // up on the first Downloading/Installing transition (Legacy parity). We preserve
+ // any existing handler — the IME adapter's own listener must keep running.
+ _prevStateChanged = _imeHost.Tracker.OnAppStateChanged;
+ _imeHost.Tracker.OnAppStateChanged = (pkg, oldState, newState) =>
+ {
+ try { _prevStateChanged?.Invoke(pkg, oldState, newState); }
+ catch (Exception ex) { _logger.Warning($"DeliveryOptimizationHost: previous OnAppStateChanged handler threw: {ex.Message}"); }
+
+ if (newState >= Monitoring.Enrollment.Ime.AppInstallationState.Downloading &&
+ newState <= Monitoring.Enrollment.Ime.AppInstallationState.Installing)
+ {
+ try { _collector.WakeUp(); }
+ catch (Exception ex) { _logger.Warning($"DeliveryOptimizationHost: WakeUp threw: {ex.Message}"); }
+ }
+ };
+
+ _collector.Start();
+ _logger.Info($"DeliveryOptimizationHost: started dormant (interval={_collector}, wakes on Downloading/Installing).");
+ }
+
+ public void Stop()
+ {
+ // Restore the previous handler only if we're still the current one — otherwise
+ // someone else replaced us and owns the slot now.
+ if (object.ReferenceEquals(_imeHost.Tracker.OnAppStateChanged, _imeHost.Tracker.OnAppStateChanged))
+ _imeHost.Tracker.OnAppStateChanged = _prevStateChanged;
+ _collector.Stop();
+ }
+
+ public void Dispose()
+ {
+ if (Interlocked.Exchange(ref _disposed, 1) == 1) return;
+ try { _collector.Dispose(); } catch { }
+ }
+ }
+
private sealed class AgentSelfMetricsHost : ICollectorHost
{
public string Name => "AgentSelfMetricsCollector";
diff --git a/src/Agent/AutopilotMonitor.Agent.V2.Core/Runtime/StartupEnvironmentProbes.cs b/src/Agent/AutopilotMonitor.Agent.V2.Core/Runtime/StartupEnvironmentProbes.cs
new file mode 100644
index 00000000..5814367f
--- /dev/null
+++ b/src/Agent/AutopilotMonitor.Agent.V2.Core/Runtime/StartupEnvironmentProbes.cs
@@ -0,0 +1,229 @@
+#nullable enable
+using System;
+using System.Collections.Generic;
+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.Agent.V2.Core.Telemetry.Events;
+using AutopilotMonitor.Shared.Models;
+
+namespace AutopilotMonitor.Agent.V2.Core.Runtime
+{
+ ///
+ /// Fire-and-forget startup probes ported from the Legacy MonitoringService.
+ /// Plan §4.x M4.6.γ.
+ ///
+ /// Emits three diagnostic events when the orchestrator is live:
+ ///
+ ///
+ /// - device_location (or agent_trace on failure) — IP-based geo via
+ /// . Skipped when EnableGeoLocation=false.
+ /// - timezone_auto_set — only when EnableTimezoneAutoSet=true AND the
+ /// geo lookup returned an IANA timezone. Uses (tzutil).
+ /// - ntp_time_check — NTP offset from NtpServer (default time.windows.com).
+ /// Always emitted — offset >60s is Warning, smaller is Info, failure is Warning.
+ ///
+ ///
+ /// Each probe runs best-effort: failures are logged + optionally emitted as a Warning event,
+ /// never thrown. Designed to run on a background task so they never block the critical path.
+ ///
+ ///
+ public static class StartupEnvironmentProbes
+ {
+ ///
+ /// Runs all three probes sequentially and emits the resulting events via
+ /// . Returns a that completes when all
+ /// probes have finished (typical runtime <10s; longer on slow/blocked networks).
+ ///
+ public static async Task RunAsync(
+ AgentConfiguration configuration,
+ AgentLogger logger,
+ TelemetryEventEmitter emitter)
+ {
+ if (configuration == null) throw new ArgumentNullException(nameof(configuration));
+ if (logger == null) throw new ArgumentNullException(nameof(logger));
+ if (emitter == null) throw new ArgumentNullException(nameof(emitter));
+
+ if (configuration.EnableGeoLocation)
+ await RunGeoAndTimezone(configuration, logger, emitter).ConfigureAwait(false);
+ else
+ logger.Debug("StartupEnvironmentProbes: EnableGeoLocation=false — skipping geo + timezone probes.");
+
+ await RunNtpCheck(configuration, logger, emitter).ConfigureAwait(false);
+ }
+
+ private static async Task RunGeoAndTimezone(
+ AgentConfiguration configuration,
+ AgentLogger logger,
+ TelemetryEventEmitter emitter)
+ {
+ GeoLocationAttemptResult? attempt = null;
+ try
+ {
+ attempt = await GeoLocationService.GetLocationAsync(logger).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ logger.Warning($"StartupEnvironmentProbes: geo-location lookup threw: {ex.Message}");
+ }
+
+ if (attempt?.Location != null)
+ {
+ SafeEmit(emitter, logger, BuildGeoEvent(configuration, attempt.Location));
+ }
+ else
+ {
+ SafeEmit(emitter, logger, BuildGeoFailureEvent(configuration, attempt));
+ }
+
+ if (configuration.EnableTimezoneAutoSet && !string.IsNullOrEmpty(attempt?.Location?.Timezone))
+ {
+ try
+ {
+ var tzResult = TimezoneService.TrySetTimezone(attempt!.Location!.Timezone, logger);
+ SafeEmit(emitter, logger, BuildTimezoneEvent(configuration, tzResult));
+ }
+ catch (Exception tzEx)
+ {
+ logger.Warning($"StartupEnvironmentProbes: timezone auto-set threw: {tzEx.Message}");
+ }
+ }
+ }
+
+ private static async Task RunNtpCheck(
+ AgentConfiguration configuration,
+ AgentLogger logger,
+ TelemetryEventEmitter emitter)
+ {
+ try
+ {
+ var ntpServer = string.IsNullOrEmpty(configuration.NtpServer) ? "time.windows.com" : configuration.NtpServer;
+
+ // NtpTimeCheckService.CheckTime is synchronous — wrap on a background thread so the
+ // probe runner stays async-friendly (avoids blocking the continuation thread when
+ // Program.cs awaits RunAsync with ConfigureAwait(false)).
+ var result = await Task.Run(() => NtpTimeCheckService.CheckTime(ntpServer, logger))
+ .ConfigureAwait(false);
+
+ SafeEmit(emitter, logger, BuildNtpEvent(configuration, ntpServer, result));
+ }
+ catch (Exception ex)
+ {
+ logger.Warning($"StartupEnvironmentProbes: NTP check threw: {ex.Message}");
+ }
+ }
+
+ // --------------------------------------------------------------- Event builders (internal for tests)
+
+ internal static EnrollmentEvent BuildGeoEvent(AgentConfiguration configuration, GeoLocationResult location) =>
+ new EnrollmentEvent
+ {
+ SessionId = configuration.SessionId,
+ TenantId = configuration.TenantId,
+ EventType = "device_location",
+ Severity = EventSeverity.Info,
+ Source = "Network",
+ Phase = EnrollmentPhase.Unknown,
+ Timestamp = DateTime.UtcNow,
+ Message = $"Device location: {location.City}, {location.Region}, {location.Country} (via {location.Source})",
+ Data = location.ToDictionary(),
+ ImmediateUpload = true,
+ };
+
+ internal static EnrollmentEvent BuildGeoFailureEvent(AgentConfiguration configuration, GeoLocationAttemptResult? attempt) =>
+ new EnrollmentEvent
+ {
+ SessionId = configuration.SessionId,
+ TenantId = configuration.TenantId,
+ EventType = "agent_trace",
+ Severity = EventSeverity.Warning,
+ Source = "Network",
+ Phase = EnrollmentPhase.Unknown,
+ Timestamp = DateTime.UtcNow,
+ Message = "Geo-location lookup failed: all providers unreachable",
+ Data = new Dictionary
+ {
+ { "decision", "geo_location_failed" },
+ { "reason", "All geo-location providers failed after retry" },
+ { "primaryError", attempt?.PrimaryError ?? "unknown" },
+ { "primaryRetryError", attempt?.PrimaryRetryError ?? "unknown" },
+ { "fallbackError", attempt?.FallbackError ?? "unknown" },
+ { "primaryProvider", "ipinfo.io" },
+ { "fallbackProvider", "ifconfig.co" },
+ },
+ };
+
+ internal static EnrollmentEvent BuildTimezoneEvent(AgentConfiguration configuration, TimezoneSetResult tz) =>
+ new EnrollmentEvent
+ {
+ SessionId = configuration.SessionId,
+ TenantId = configuration.TenantId,
+ EventType = "timezone_auto_set",
+ Severity = tz.Success ? EventSeverity.Info : EventSeverity.Warning,
+ Source = "Network",
+ Phase = EnrollmentPhase.Unknown,
+ Timestamp = DateTime.UtcNow,
+ Message = tz.Success
+ ? $"Timezone set to {tz.WindowsTimezoneId} (from {tz.IanaTimezone})"
+ : $"Timezone auto-set failed: {tz.Error}",
+ Data = new Dictionary
+ {
+ { "ianaTimezone", tz.IanaTimezone ?? "" },
+ { "windowsTimezoneId", tz.WindowsTimezoneId ?? "unknown" },
+ { "previousTimezone", tz.PreviousTimezone ?? "unknown" },
+ { "success", tz.Success },
+ { "error", tz.Error ?? "" },
+ },
+ };
+
+ internal static EnrollmentEvent BuildNtpEvent(AgentConfiguration configuration, string ntpServer, NtpCheckResult result)
+ {
+ if (result.Success)
+ {
+ var severity = Math.Abs(result.OffsetSeconds) > 60 ? EventSeverity.Warning : EventSeverity.Info;
+ return new EnrollmentEvent
+ {
+ SessionId = configuration.SessionId,
+ TenantId = configuration.TenantId,
+ EventType = "ntp_time_check",
+ Severity = severity,
+ Source = "Network",
+ Phase = EnrollmentPhase.Unknown,
+ Timestamp = DateTime.UtcNow,
+ Message = $"NTP time check: offset {result.OffsetSeconds:F2}s from {ntpServer}",
+ Data = new Dictionary
+ {
+ { "ntpServer", ntpServer },
+ { "offsetSeconds", result.OffsetSeconds },
+ { "ntpTimeUtc", result.NtpTime?.ToString("o") ?? "" },
+ { "localTimeUtc", result.LocalTime?.ToString("o") ?? "" },
+ },
+ };
+ }
+
+ return new EnrollmentEvent
+ {
+ SessionId = configuration.SessionId,
+ TenantId = configuration.TenantId,
+ EventType = "ntp_time_check",
+ Severity = EventSeverity.Warning,
+ Source = "Network",
+ Phase = EnrollmentPhase.Unknown,
+ Timestamp = DateTime.UtcNow,
+ Message = $"NTP time check failed: {result.Error}",
+ Data = new Dictionary
+ {
+ { "ntpServer", ntpServer },
+ { "error", result.Error ?? "unknown" },
+ },
+ };
+ }
+
+ private static void SafeEmit(TelemetryEventEmitter emitter, AgentLogger logger, EnrollmentEvent evt)
+ {
+ try { emitter.Emit(evt); }
+ catch (Exception ex) { logger.Warning($"StartupEnvironmentProbes: emit failed for '{evt.EventType}': {ex.Message}"); }
+ }
+ }
+}
diff --git a/src/Agent/AutopilotMonitor.Agent.V2/Program.cs b/src/Agent/AutopilotMonitor.Agent.V2/Program.cs
index 7377ee5e..ec35faab 100644
--- a/src/Agent/AutopilotMonitor.Agent.V2/Program.cs
+++ b/src/Agent/AutopilotMonitor.Agent.V2/Program.cs
@@ -10,6 +10,7 @@ using AutopilotMonitor.Agent.V2.Core.Logging;
using AutopilotMonitor.Agent.V2.Core.Monitoring.Runtime;
using AutopilotMonitor.Agent.V2.Core.Monitoring.Transport;
using AutopilotMonitor.Agent.V2.Core.Orchestration;
+using AutopilotMonitor.Agent.V2.Core.Runtime;
using AutopilotMonitor.Agent.V2.Core.Security;
using AutopilotMonitor.Agent.V2.Core.Termination;
using AutopilotMonitor.Agent.V2.Core.Transport.Telemetry;
@@ -259,7 +260,37 @@ namespace AutopilotMonitor.Agent.V2
logger: logger,
agentVersion: GetAgentVersion());
- var remoteConfigService = new RemoteConfigService(backendApiClient, agentConfig.TenantId, logger);
+ // M4.6.γ — Emergency + Distress reporters. Plumbed into RemoteConfigService so
+ // Config-fetch failures (auth vs network) flow to the correct channel. Also fires
+ // an initial AuthCertificateMissing distress when the MDM cert was expected but
+ // not found (Legacy parity — surfaces pre-MDM-enrollment dead-ends to the backend
+ // via the cert-less distress channel).
+ var hardwareForReporters = HardwareInfo.GetHardwareInfo(logger);
+ var distressReporter = new DistressReporter(
+ baseUrl: agentConfig.ApiBaseUrl,
+ tenantId: agentConfig.TenantId,
+ manufacturer: hardwareForReporters.Manufacturer,
+ model: hardwareForReporters.Model,
+ serialNumber: hardwareForReporters.SerialNumber,
+ agentVersion: GetAgentVersion(),
+ logger: logger);
+
+ var emergencyReporter = new EmergencyReporter(
+ apiClient: backendApiClient,
+ sessionId: agentConfig.SessionId,
+ tenantId: agentConfig.TenantId,
+ agentVersion: GetAgentVersion(),
+ logger: logger);
+
+ if (agentConfig.UseClientCertAuth && backendApiClient.ClientCertificate == null)
+ {
+ _ = distressReporter.TrySendAsync(
+ DistressErrorType.AuthCertificateMissing,
+ "MDM certificate not found in LocalMachine or CurrentUser store");
+ }
+
+ var remoteConfigService = new RemoteConfigService(
+ backendApiClient, agentConfig.TenantId, logger, emergencyReporter, distressReporter);
var remoteConfig = remoteConfigService.FetchConfigAsync().GetAwaiter().GetResult();
logger.SetLogLevel(agentConfig.LogLevel);
@@ -299,14 +330,13 @@ namespace AutopilotMonitor.Agent.V2
try
{
- var hardware = HardwareInfo.GetHardwareInfo(logger);
uploader = new BackendTelemetryUploader(
httpClient: mtlsHttpClient,
baseUrl: agentConfig.ApiBaseUrl,
tenantId: agentConfig.TenantId,
- manufacturer: hardware.Manufacturer,
- model: hardware.Model,
- serialNumber: hardware.SerialNumber,
+ manufacturer: hardwareForReporters.Manufacturer,
+ model: hardwareForReporters.Model,
+ serialNumber: hardwareForReporters.SerialNumber,
bootstrapToken: agentConfig.UseBootstrapTokenAuth ? agentConfig.BootstrapToken : null,
agentVersion: GetAgentVersion());
}
@@ -433,6 +463,22 @@ namespace AutopilotMonitor.Agent.V2
// VersionCheckEventBuilder.TryBuild is a no-op when no markers are present.
EmitVersionCheckEventIfAny(orchestrator, agentConfig, logger);
+ // M4.6.γ — fire-and-forget startup probes (geo / timezone / NTP). Runs on
+ // the ThreadPool so a slow network never delays the critical path.
+ _ = Task.Run(async () =>
+ {
+ try
+ {
+ await StartupEnvironmentProbes
+ .RunAsync(agentConfig, logger, orchestrator.EventEmitter)
+ .ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ logger.Warning($"StartupEnvironmentProbes: outer exception: {ex.Message}");
+ }
+ });
+
logger.Info($"V2 agent runtime ready (session={agentConfig.SessionId}, tenant={agentConfig.TenantId}).");
if (consoleMode)
Console.Out.WriteLine("AutopilotMonitor.Agent.V2 running. Press Ctrl+C to stop.");
diff --git a/src/Agent/AutopilotMonitor.Agent.V2/buildcounter.txt b/src/Agent/AutopilotMonitor.Agent.V2/buildcounter.txt
index f5c89552..8f92bfdd 100644
--- a/src/Agent/AutopilotMonitor.Agent.V2/buildcounter.txt
+++ b/src/Agent/AutopilotMonitor.Agent.V2/buildcounter.txt
@@ -1 +1 @@
-32
+35
diff --git a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt
index b9650fd8..48c6edb2 100644
--- a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt
+++ b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt
@@ -1 +1 @@
-1068
+1069