diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/DeviceInfoCollector.NetworkAndSecurity.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/DeviceInfoCollector.NetworkAndSecurity.cs
index 9b08da5c..16ea4d3f 100644
--- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/DeviceInfoCollector.NetworkAndSecurity.cs
+++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/DeviceInfoCollector.NetworkAndSecurity.cs
@@ -131,10 +131,11 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors
/// Note: AutopilotMode and DomainJoinMethod reflect profile capabilities (what is allowed),
/// not the actual live enrollment state.
///
- private (string enrollmentType, bool isHybridJoin) CollectAutopilotProfile()
+ private (string enrollmentType, bool isHybridJoin, int? autopilotMode) CollectAutopilotProfile()
{
var detectedType = "v1";
var isHybridJoin = false;
+ int? detectedAutopilotMode = null;
try
{
@@ -185,6 +186,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors
: null;
if (autopilotModeStr != null && int.TryParse(autopilotModeStr, out var autopilotMode))
{
+ detectedAutopilotMode = autopilotMode;
string modeLabel;
switch (autopilotMode)
{
@@ -269,7 +271,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors
_logger.Warning($"EnrollmentTracker: failed to collect autopilot profile: {ex.Message}");
}
- return (detectedType, isHybridJoin);
+ return (detectedType, isHybridJoin, detectedAutopilotMode);
}
private void CollectSecureBootStatus()
diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/DeviceInfoCollector.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/DeviceInfoCollector.cs
index 4c5de677..5cb4214d 100644
--- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/DeviceInfoCollector.cs
+++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/DeviceInfoCollector.cs
@@ -44,7 +44,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors
/// Runs all device info collectors at agent startup.
/// Returns the detected enrollment type, Hybrid Join flag, and ESP skip configuration from registry.
///
- public (string enrollmentType, bool isHybridJoin, bool? skipUserStatusPage, bool? skipDeviceStatusPage) CollectAll()
+ public (string enrollmentType, bool isHybridJoin, bool? skipUserStatusPage, bool? skipDeviceStatusPage, int? autopilotMode) CollectAll()
{
_logger.Info("EnrollmentTracker: collecting device info (at start)");
@@ -59,7 +59,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors
CollectAadJoinStatus();
CollectActiveNetworkInterfaceInfo();
- return (profileResult.enrollmentType, profileResult.isHybridJoin, espConfig.skipUserStatusPage, espConfig.skipDeviceStatusPage);
+ return (profileResult.enrollmentType, profileResult.isHybridJoin, espConfig.skipUserStatusPage, espConfig.skipDeviceStatusPage, profileResult.autopilotMode);
}
///
diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/EspAndHelloTracker.ProvisioningStatusTracking.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/EspAndHelloTracker.ProvisioningStatusTracking.cs
index 3e291a1c..b50d5817 100644
--- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/EspAndHelloTracker.ProvisioningStatusTracking.cs
+++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/EspAndHelloTracker.ProvisioningStatusTracking.cs
@@ -55,6 +55,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors
private HashSet _provisioningCategoriesResolved;
// Track subcategory states per category — detect meaningful state transitions
private Dictionary> _lastSubcategoryStates;
+ // Fire-once guard for DeviceSetupProvisioningComplete event
+ private bool _deviceSetupProvisioningCompleteFired;
private void StartProvisioningStatusWatcher()
{
@@ -64,6 +66,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors
_provisioningFailureFired = new HashSet(StringComparer.OrdinalIgnoreCase);
_provisioningCategoriesResolved = new HashSet(StringComparer.OrdinalIgnoreCase);
_lastSubcategoryStates = new Dictionary>(StringComparer.OrdinalIgnoreCase);
+ _deviceSetupProvisioningCompleteFired = false;
// Try to start immediately; if key doesn't exist yet, retry every 2s
if (!TryStartProvisioningWatcher())
@@ -228,6 +231,25 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors
_logger.Error($"EspFailureDetected handler failed for '{failureType}'", ex);
}
}
+
+ // Fire DeviceSetupProvisioningComplete when DeviceSetup category resolves with success.
+ // Used as completion signal for Self-Deploying mode where Shell-Core ESP exit
+ // and desktop arrival may never arrive.
+ if (!_deviceSetupProvisioningCompleteFired
+ && _lastCategorySucceeded.TryGetValue("DeviceSetupCategory.Status", out var dsSucceeded)
+ && dsSucceeded == true)
+ {
+ _deviceSetupProvisioningCompleteFired = true;
+ _logger.Info("Provisioning status: DeviceSetup succeeded — firing DeviceSetupProvisioningComplete");
+ try
+ {
+ DeviceSetupProvisioningComplete?.Invoke(this, EventArgs.Empty);
+ }
+ catch (Exception ex)
+ {
+ _logger.Error("DeviceSetupProvisioningComplete handler failed", ex);
+ }
+ }
}
}
}
diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/EspAndHelloTracker.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/EspAndHelloTracker.cs
index 78d8b7e9..ac98be0c 100644
--- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/EspAndHelloTracker.cs
+++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Collectors/EspAndHelloTracker.cs
@@ -77,6 +77,13 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Collectors
///
public event EventHandler EspFailureDetected;
+ ///
+ /// Fired when DeviceSetup provisioning status shows categorySucceeded=true.
+ /// Used as a completion signal for Self-Deploying mode where Shell-Core ESP exit
+ /// and desktop arrival signals may never arrive.
+ ///
+ public event EventHandler DeviceSetupProvisioningComplete;
+
///
/// Outcome of Hello provisioning. Set when Hello resolves (via events, timeout, or not configured).
/// Values: "completed", "timeout", "not_configured", "wizard_not_started", null (not yet resolved).
diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentStatePersistence.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentStatePersistence.cs
index d75ef15f..ac882415 100644
--- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentStatePersistence.cs
+++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentStatePersistence.cs
@@ -111,12 +111,16 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking
public bool? SkipUserStatusPage { get; set; }
public bool? SkipDeviceStatusPage { get; set; }
+ // Autopilot deployment mode: 0=UserDriven, 1=SelfDeploying, 2=PreProvisioning, null=unknown
+ public int? AutopilotMode { get; set; }
+
// Signal timestamps for audit trail
public DateTime? EspFirstSeenUtc { get; set; }
public DateTime? EspFinalExitUtc { get; set; }
public DateTime? DesktopArrivedUtc { get; set; }
public DateTime? HelloResolvedUtc { get; set; }
public DateTime? ImePatternSeenUtc { get; set; }
+ public DateTime? DeviceSetupProvisioningCompleteUtc { get; set; }
public List SignalsSeen { get; set; } = new List();
}
}
diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.CompletionLogic.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.CompletionLogic.cs
index d51a4a67..ba0cb9fd 100644
--- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.CompletionLogic.cs
+++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.CompletionLogic.cs
@@ -47,6 +47,74 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking
}
}
+ ///
+ /// Called when DeviceSetup provisioning status shows all subcategories succeeded.
+ /// In Self-Deploying mode, this is the primary completion signal because no user session
+ /// means no Hello, no desktop arrival, and possibly no Shell-Core ESP exit event.
+ /// In non-Self-Deploying mode, this is informational only — normal paths handle completion.
+ ///
+ private void OnDeviceSetupProvisioningComplete(object sender, EventArgs e)
+ {
+ _logger.Info("EnrollmentTracker: DeviceSetup provisioning completed successfully");
+ RecordSignal("device_setup_provisioning_complete");
+ _stateData.DeviceSetupProvisioningCompleteUtc = DateTime.UtcNow;
+ _stateDirty = true;
+
+ if (!IsSelfDeploying)
+ {
+ _logger.Info("EnrollmentTracker: DeviceSetup provisioning complete but not Self-Deploying — " +
+ "using normal completion paths (ESP exit + Hello)");
+ EmitTraceEvent("device_setup_complete_non_self_deploying",
+ "DeviceSetup provisioning complete in non-Self-Deploying mode — no action, normal paths apply",
+ new Dictionary { { "autopilotMode", _autopilotMode } });
+ return;
+ }
+
+ // Self-Deploying mode: this is our primary completion signal.
+ // No user session → no Hello, no desktop arrival, possibly no Shell-Core ESP exit.
+ _logger.Info("EnrollmentTracker: Self-Deploying mode + DeviceSetup provisioning complete — " +
+ "transitioning to FinalizingSetup and attempting completion");
+
+ // Mark ESP as seen and final exit (provisioning success implies ESP device phase completed)
+ if (!_espEverSeen)
+ {
+ _espEverSeen = true;
+ _stateData.EspEverSeen = true;
+ }
+ if (!_espFinalExitSeen)
+ {
+ _espFinalExitSeen = true;
+ _stateData.EspFinalExitSeen = true;
+ _stateData.EspFinalExitUtc = DateTime.UtcNow;
+ RecordSignal("self_deploying_esp_final_exit");
+ }
+
+ // Emit FinalizingSetup phase event
+ _emitEvent(new EnrollmentEvent
+ {
+ SessionId = _sessionId,
+ TenantId = _tenantId,
+ EventType = "esp_phase_changed",
+ Severity = EventSeverity.Info,
+ Source = "EnrollmentTracker",
+ Phase = EnrollmentPhase.FinalizingSetup,
+ Message = "ESP phase: FinalizingSetup (Self-Deploying — DeviceSetup provisioning complete, no user session expected)",
+ Data = new Dictionary
+ {
+ { "espPhase", "FinalizingSetup" },
+ { "autoDetected", true },
+ { "triggeredBy", "self_deploying_provisioning_complete" },
+ { "autopilotMode", _autopilotMode },
+ { "skipUserStatusPage", _skipUserStatusPage }
+ }
+ });
+
+ CollectDeviceInfoAtFinalizingSetup("self_deploying_provisioning_complete");
+
+ // Attempt completion — Hello guard is bypassed for Self-Deploying (IsSelfDeploying)
+ TryEmitEnrollmentComplete("self_deploying_provisioning_complete");
+ }
+
///
/// Called when WhiteGlove (Pre-Provisioning) completes successfully.
/// Emits the whiteglove_complete event. The MonitoringService handles
@@ -348,6 +416,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking
_enrollmentType = loaded.EnrollmentType ?? _enrollmentType;
_skipUserStatusPage = loaded.SkipUserStatusPage;
_skipDeviceStatusPage = loaded.SkipDeviceStatusPage;
+ _autopilotMode = loaded.AutopilotMode;
_stateData = loaded;
_logger.Info($"EnrollmentTracker: state restored — espEverSeen={_espEverSeen}, espFinalExitSeen={_espFinalExitSeen}, desktopArrived={_desktopArrived}, lastEspPhase={_lastEspPhase}, enrollmentCompleteEmitted={_enrollmentCompleteEmitted}");
@@ -410,12 +479,15 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking
_logger.Debug($"EnrollmentTracker: TryEmitEnrollmentComplete('{source}') — evaluating guards " +
$"[espEverSeen={_espEverSeen}, espFinalExitSeen={_espFinalExitSeen}, desktopArrived={_desktopArrived}, " +
$"helloCompleted={_espAndHelloTracker?.IsHelloCompleted ?? false}, helloPolicyConfigured={_espAndHelloTracker?.IsPolicyConfigured ?? false}, " +
- $"enrollmentType={_enrollmentType}]");
+ $"enrollmentType={_enrollmentType}, autopilotMode={_autopilotMode}, isSelfDeploying={IsSelfDeploying}]");
- // Hello-Check: Hello must be resolved before we can complete
+ // Hello-Check: Hello must be resolved before we can complete.
+ // Self-Deploying mode (AutopilotMode=1) has no user session, so Hello is irrelevant —
+ // bypass the Hello guard entirely regardless of whether WHfB policy is configured.
bool helloResolved = _espAndHelloTracker == null
|| _espAndHelloTracker.IsHelloCompleted
- || !_espAndHelloTracker.IsPolicyConfigured;
+ || !_espAndHelloTracker.IsPolicyConfigured
+ || IsSelfDeploying;
if (!helloResolved)
{
@@ -458,6 +530,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking
signalTimestamps["helloResolved"] = _stateData.HelloResolvedUtc.Value.ToString("o");
if (_stateData.ImePatternSeenUtc.HasValue)
signalTimestamps["imePatternSeen"] = _stateData.ImePatternSeenUtc.Value.ToString("o");
+ if (_stateData.DeviceSetupProvisioningCompleteUtc.HasValue)
+ signalTimestamps["deviceSetupProvisioningComplete"] = _stateData.DeviceSetupProvisioningCompleteUtc.Value.ToString("o");
_emitEvent(new EnrollmentEvent
{
@@ -472,6 +546,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking
{
{ "completionSource", source },
{ "helloOutcome", helloOutcome },
+ { "autopilotMode", _autopilotMode?.ToString() ?? "unknown" },
+ { "isSelfDeploying", IsSelfDeploying },
{ "signalsSeen", _stateData.SignalsSeen },
{ "signalTimestamps", signalTimestamps },
{ "agentUptimeSeconds", (DateTime.UtcNow - _agentStartTimeUtc).TotalSeconds }
@@ -521,7 +597,9 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking
{ "enrollmentType", _enrollmentType },
{ "lastEspPhase", _lastEspPhase ?? "none" },
{ "skipUserStatusPage", _skipUserStatusPage?.ToString() ?? "unknown" },
- { "skipDeviceStatusPage", _skipDeviceStatusPage?.ToString() ?? "unknown" }
+ { "skipDeviceStatusPage", _skipDeviceStatusPage?.ToString() ?? "unknown" },
+ { "autopilotMode", _autopilotMode?.ToString() ?? "unknown" },
+ { "isSelfDeploying", IsSelfDeploying }
}
});
}
@@ -615,8 +693,10 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking
// During active ESP (AccountSetup runs in background with desktop visible),
// the Hello timer must wait until ESP exits (started in OnFinalizingSetupPhaseTriggered).
// Without this guard, Hello timeout-resolves while ESP still installs apps → premature completion.
+ // Self-Deploying: no user session, Hello is irrelevant — skip timer entirely.
if (_espAndHelloTracker != null && !_espAndHelloTracker.IsHelloCompleted
- && (!_espEverSeen || _espFinalExitSeen))
+ && (!_espEverSeen || _espFinalExitSeen)
+ && !IsSelfDeploying)
{
_logger.Info("EnrollmentTracker: Desktop arrived with Hello pending (no active ESP) — starting Hello wait timer");
_espAndHelloTracker.StartHelloWaitTimer();
diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.Diagnostics.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.Diagnostics.cs
index fe6e4486..65252c86 100644
--- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.Diagnostics.cs
+++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.Diagnostics.cs
@@ -167,6 +167,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking
_espAndHelloTracker.FinalizingSetupPhaseTriggered -= OnFinalizingSetupPhaseTriggered;
_espAndHelloTracker.WhiteGloveCompleted -= OnWhiteGloveCompleted;
_espAndHelloTracker.EspFailureDetected -= OnEspFailureDetected;
+ _espAndHelloTracker.DeviceSetupProvisioningComplete -= OnDeviceSetupProvisioningComplete;
}
_espFailureTimer?.Dispose();
@@ -223,6 +224,8 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking
signalTimestamps["helloResolved"] = _stateData.HelloResolvedUtc.Value.ToString("o");
if (_stateData.ImePatternSeenUtc.HasValue)
signalTimestamps["imePatternSeen"] = _stateData.ImePatternSeenUtc.Value.ToString("o");
+ if (_stateData.DeviceSetupProvisioningCompleteUtc.HasValue)
+ signalTimestamps["deviceSetupProvisioningComplete"] = _stateData.DeviceSetupProvisioningCompleteUtc.Value.ToString("o");
// Build per-phase package states: snapshots from completed phases + current phase
var phaseSnapshots = _imeLogTracker?.PhasePackageSnapshots;
diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.EventHandlers.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.EventHandlers.cs
index cff64b3b..4699488c 100644
--- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.EventHandlers.cs
+++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.EventHandlers.cs
@@ -501,6 +501,14 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking
_debugStateTimer?.Change(Timeout.Infinite, Timeout.Infinite);
_debugStateTimerActive = false;
+ // Self-Deploying mode: skip Hello wait entirely — no user session for Hello
+ if (IsSelfDeploying)
+ {
+ _logger.Info("EnrollmentTracker: Self-Deploying mode — skipping Hello wait, proceeding to completion");
+ TryEmitEnrollmentComplete("ime_pattern");
+ return;
+ }
+
// Check if Windows Hello is configured but not yet completed
if (_espAndHelloTracker != null)
{
diff --git a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.cs b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.cs
index ca19cf3c..471f3b15 100644
--- a/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.cs
+++ b/src/Agent/AutopilotMonitor.Agent.Core/Monitoring/Tracking/EnrollmentTracker.cs
@@ -66,6 +66,10 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking
private bool? _skipUserStatusPage;
private bool? _skipDeviceStatusPage;
+ // Autopilot deployment mode: 0=UserDriven, 1=SelfDeploying, 2=PreProvisioning, null=unknown
+ private int? _autopilotMode;
+ private bool IsSelfDeploying => _autopilotMode == 1;
+
// Safety-net timer for waiting_for_hello state
private Timer _waitingForHelloSafetyTimer;
private const int WaitingForHelloSafetyTimeoutSeconds = 420; // 7 min — longer than Hello chain (330s)
@@ -190,6 +194,7 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking
_espAndHelloTracker.FinalizingSetupPhaseTriggered += OnFinalizingSetupPhaseTriggered;
_espAndHelloTracker.WhiteGloveCompleted += OnWhiteGloveCompleted;
_espAndHelloTracker.EspFailureDetected += OnEspFailureDetected;
+ _espAndHelloTracker.DeviceSetupProvisioningComplete += OnDeviceSetupProvisioningComplete;
}
}
@@ -298,13 +303,27 @@ namespace AutopilotMonitor.Agent.Core.Monitoring.Tracking
_enrollmentType = result.enrollmentType;
_skipUserStatusPage = result.skipUserStatusPage;
_skipDeviceStatusPage = result.skipDeviceStatusPage;
+ _autopilotMode = result.autopilotMode;
_stateData.EnrollmentType = _enrollmentType;
_stateData.SkipUserStatusPage = _skipUserStatusPage;
_stateData.SkipDeviceStatusPage = _skipDeviceStatusPage;
+ _stateData.AutopilotMode = _autopilotMode;
_stateDirty = true;
_logger.Info($"EnrollmentTracker: enrollment type detected: {_enrollmentType ?? "unknown"} " +
- $"(skipUserStatusPage={_skipUserStatusPage}, skipDeviceStatusPage={_skipDeviceStatusPage})");
+ $"(autopilotMode={_autopilotMode}, skipUserStatusPage={_skipUserStatusPage}, skipDeviceStatusPage={_skipDeviceStatusPage})");
+
+ if (IsSelfDeploying)
+ {
+ EmitTraceEvent("self_deploying_detected",
+ "AutopilotMode=1 (Self-Deploying) — Hello guard will be bypassed, provisioning status used as completion signal",
+ new Dictionary
+ {
+ { "autopilotMode", _autopilotMode },
+ { "skipUserStatusPage", _skipUserStatusPage },
+ { "enrollmentType", _enrollmentType }
+ });
+ }
}
///
diff --git a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt
index bf7aeeb3..26551cf1 100644
--- a/src/Agent/AutopilotMonitor.Agent/buildcounter.txt
+++ b/src/Agent/AutopilotMonitor.Agent/buildcounter.txt
@@ -1 +1 @@
-462
+463