feat(agent): complete self-deploying/kiosk sessions despite IME AccountSetup false positive

Kiosk devices (self-deploying profile) never completed: the IME logs an
AccountSetup phase line for the kioskUser0 autologon session although user
ESP never runs, which cancelled/blocked the DeviceOnlyEspDetection deadline
- the sole SelfDeploying terminal path - and the session idled to the 5h
backend timeout (reference session 320b3bf7; 436 false Failed platform-wide,
90% of all self-deploying failures).

Platform sweep (8983 sessions / 63 tenants): CloudAssignedOobeConfig bits
0x20|0x40 are exclusive to self-deploying/kiosk/shared profiles (0 of 6785
user-driven sessions carry them, bits always co-occur). The fix seeds
Mode=SelfDeploying@High from that registry marker via EnrollmentFactsObserved
and waives the AccountSetup veto at the four existing decision points
(DSPC short-circuit, deadline race guard B, EspPhaseChanged cancel,
RealmJoin deferred-release re-check) - only when the seeded profile holds
AND user ESP made no real progress. Bits absent = byte-identical behaviour.

Also registers IsSelfDeployingProfile on SessionRegistration (display
metadata; backend persistence follows separately).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Oliver Kieselbach
2026-07-02 12:50:02 +02:00
parent 0f82577606
commit eb132b11fd
20 changed files with 790 additions and 9 deletions
@@ -56,11 +56,16 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Program
// The contract under test is the payload SHAPE, not the detected values.
Assert.Contains("enrollmentType", (IDictionary<string, string>)payload!);
Assert.Contains("isHybridJoin", (IDictionary<string, string>)payload);
Assert.Contains("isSelfDeployingProfile", (IDictionary<string, string>)payload);
// Value sanity — defaults must be parseable booleans / known type literals.
var hybrid = payload["isHybridJoin"];
Assert.True(hybrid == "true" || hybrid == "false");
var type = payload["enrollmentType"];
Assert.True(type == "v1" || type == "v2");
// Session 320b3bf7 kiosk fix — the OobeConfig self-deploying marker rides the
// same facts signal (safe default "false" off-device).
var selfDeploying = payload["isSelfDeployingProfile"];
Assert.True(selfDeploying == "true" || selfDeploying == "false");
}
[Fact]
@@ -127,5 +127,100 @@ namespace AutopilotMonitor.Agent.V2.Core.Tests.Security
var actual = EnrollmentRegistryDetector.DetectHybridJoin();
Assert.IsType<bool>(actual);
}
// ============================================== self-deploying profile (OobeConfig)
// Session 320b3bf7 kiosk fix: CloudAssignedOobeConfig bits 0x20|0x40 mark a
// self-deploying profile (platform sweep 2026-07-02: exclusive to SD/kiosk/shared
// profiles, both bits always co-occur, zero user-driven false positives).
[Theory]
[InlineData(0x60, true)] // exactly the mask
[InlineData(1534, true)] // 0x5FE — Opsys Kiosk (session 320b3bf7)
[InlineData(510, true)] // 0x1FE — second observed SD value
[InlineData(0x20, false)] // TPM-attestation bit alone
[InlineData(0x40, false)] // device-auth bit alone
[InlineData(1310, false)] // 0x51E — dominant user-driven value
[InlineData(286, false)] // 0x11E — user-driven
[InlineData(0, false)]
public void TopLevel_OobeConfig_requires_both_selfdeploying_bits(int oobeConfig, bool expected)
{
var values = new Dictionary<string, object>
{
["CloudAssignedOobeConfig"] = oobeConfig,
};
Assert.Equal(expected, EnrollmentRegistryDetector.ResolveSelfDeployingFromValues(Lookup(values)));
}
[Fact]
public void TopLevel_OobeConfig_nonnumeric_returns_false_without_fallback()
{
// Top-level value is authoritative when present — an unparseable top-level does
// NOT fall through to a contradicting JSON blob (mirrors the hybrid-join contract).
var values = new Dictionary<string, object>
{
["CloudAssignedOobeConfig"] = "not-a-number",
["PolicyJsonCache"] = "{\"CloudAssignedOobeConfig\":1534}",
};
Assert.False(EnrollmentRegistryDetector.ResolveSelfDeployingFromValues(Lookup(values)));
}
[Fact]
public void Missing_TopLevel_OobeConfig_falls_back_to_PolicyJsonCache()
{
// Real-world shape from session 320b3bf7 (Opsys Kiosk): OobeConfig only inside
// the embedded JSON blob, DeploymentProfileName "Opsys Kiosk".
const string policyJson =
"{\r\n \"CloudAssignedTenantDomain\": \"contoso.onmicrosoft.com\",\r\n" +
" \"CloudAssignedOobeConfig\": 1534,\r\n" +
" \"DeploymentProfileName\": \"Contoso Kiosk\"\r\n}";
var values = new Dictionary<string, object>
{
["PolicyJsonCache"] = policyJson,
};
Assert.True(EnrollmentRegistryDetector.ResolveSelfDeployingFromValues(Lookup(values)));
}
[Fact]
public void Missing_TopLevel_OobeConfig_PolicyJsonCache_userdriven_returns_false()
{
var values = new Dictionary<string, object>
{
["PolicyJsonCache"] = "{\"CloudAssignedOobeConfig\":1310}",
};
Assert.False(EnrollmentRegistryDetector.ResolveSelfDeployingFromValues(Lookup(values)));
}
[Fact]
public void OobeConfig_absent_everywhere_returns_false()
{
var values = new Dictionary<string, object>
{
["PolicyJsonCache"] = "{\"CloudAssignedTenantDomain\":\"contoso.onmicrosoft.com\"}",
};
Assert.False(EnrollmentRegistryDetector.ResolveSelfDeployingFromValues(Lookup(values)));
}
[Fact]
public void OobeConfig_malformed_PolicyJsonCache_returns_false_without_throw()
{
var values = new Dictionary<string, object>
{
["PolicyJsonCache"] = "{not valid json",
};
Assert.False(EnrollmentRegistryDetector.ResolveSelfDeployingFromValues(Lookup(values)));
}
[Fact]
public void OobeConfig_null_lookup_returns_false()
{
Assert.False(EnrollmentRegistryDetector.ResolveSelfDeployingFromValues(null));
}
[Fact]
public void DetectSelfDeployingProfile_returns_false_when_registry_key_is_unreadable()
{
var actual = EnrollmentRegistryDetector.DetectSelfDeployingProfile();
Assert.IsType<bool>(actual);
}
}
}
@@ -18,6 +18,16 @@ namespace AutopilotMonitor.Agent.V2.Core.Security
private const string AutopilotPolicyCacheKey = @"SOFTWARE\Microsoft\Provisioning\AutopilotPolicyCache";
private const string AutopilotSettingsKey = @"SOFTWARE\Microsoft\Provisioning\AutopilotSettings";
/// <summary>
/// <c>CloudAssignedOobeConfig</c> bits 0x20 (TPM attestation) + 0x40 (AAD device-ticket
/// auth) — set together exclusively on self-deploying Autopilot profiles. Validated
/// across the full platform (2026-07-02 sweep, 8983 sessions / 63 tenants): all 1197
/// sessions carrying both bits belong to self-deploying/kiosk/shared-device profiles,
/// zero of 6785 user-driven sessions carry them, and the two bits never occur
/// individually. Session 320b3bf7 is the reference kiosk case this detection unblocks.
/// </summary>
internal const int SelfDeployingOobeConfigMask = 0x60;
/// <summary>
/// Reads <c>CloudAssignedDomainJoinMethod</c> from the Autopilot policy cache. Returns
/// <c>true</c> when the profile was deployed with Hybrid Azure AD Join
@@ -88,6 +98,75 @@ namespace AutopilotMonitor.Agent.V2.Core.Security
return false;
}
/// <summary>
/// Reads <c>CloudAssignedOobeConfig</c> from the Autopilot policy cache and returns
/// <c>true</c> when both self-deploying marker bits (<see cref="SelfDeployingOobeConfigMask"/>)
/// are set. Mirrors the <see cref="DetectHybridJoin"/> top-level-value-then-
/// <c>PolicyJsonCache</c>-JSON fallback, because the same Windows builds that omit the
/// flat <c>CloudAssignedDomainJoinMethod</c> value also omit <c>CloudAssignedOobeConfig</c>.
/// Returns <c>false</c> on any error — a missing/unreadable profile must degrade to
/// today's classic behaviour, never to a false self-deploying classification.
/// </summary>
public static bool DetectSelfDeployingProfile()
{
try
{
using (var key = Registry.LocalMachine.OpenSubKey(AutopilotPolicyCacheKey))
{
if (key == null) return false;
return ResolveSelfDeployingFromValues(key.GetValue);
}
}
catch
{
return false;
}
}
/// <summary>
/// Pure decision logic for <see cref="DetectSelfDeployingProfile"/>. Exposed internally
/// so the fallback can be exercised without a real registry (same seam contract as
/// <see cref="ResolveHybridJoinFromValues"/>).
/// </summary>
internal static bool ResolveSelfDeployingFromValues(Func<string, object> getValue)
{
if (getValue == null) return false;
// Top-level value is authoritative when present.
var topLevel = getValue("CloudAssignedOobeConfig")?.ToString();
if (topLevel != null)
{
return TryParseOobeConfig(topLevel, out var cfg) && HasSelfDeployingBits(cfg);
}
// Fallback: embedded JSON blob (mirrors the hybrid-join fallback).
var policyJson = getValue("PolicyJsonCache")?.ToString();
if (string.IsNullOrWhiteSpace(policyJson)) return false;
try
{
using (var doc = System.Text.Json.JsonDocument.Parse(policyJson))
{
if (doc.RootElement.TryGetProperty("CloudAssignedOobeConfig", out var prop))
{
return TryParseOobeConfig(prop.ToString(), out var cfg) && HasSelfDeployingBits(cfg);
}
}
}
catch
{
// Malformed PolicyJsonCache → conservative non-self-deploying default.
}
return false;
}
private static bool TryParseOobeConfig(string raw, out int cfg) =>
int.TryParse(raw, System.Globalization.NumberStyles.Integer,
System.Globalization.CultureInfo.InvariantCulture, out cfg);
private static bool HasSelfDeployingBits(int cfg) =>
(cfg & SelfDeployingOobeConfigMask) == SelfDeployingOobeConfigMask;
/// <summary>
/// Classifies the Autopilot flow based on the <c>AutopilotSettings</c> registry:
/// <list type="bullet">
@@ -161,6 +161,10 @@ namespace AutopilotMonitor.Agent.V2.Core.Security
AgentVersion = agentVersion ?? string.Empty,
EnrollmentType = EnrollmentRegistryDetector.DetectEnrollmentType(),
IsHybridJoin = EnrollmentRegistryDetector.DetectHybridJoin(),
IsSelfDeployingProfile = EnrollmentRegistryDetector.DetectSelfDeployingProfile(),
// Deliberately stays true even for self-deploying profiles: the WhiteGlove
// Part1/Part2 merge paths and existing dashboards key on IsUserDriven;
// IsSelfDeployingProfile is the additive classification signal.
IsUserDriven = true,
};
}
@@ -303,11 +303,13 @@ namespace AutopilotMonitor.Agent.V2.Runtime
{
var enrollmentType = EnrollmentRegistryDetector.DetectEnrollmentType();
var isHybridJoin = EnrollmentRegistryDetector.DetectHybridJoin();
var isSelfDeploying = EnrollmentRegistryDetector.DetectSelfDeployingProfile();
var payload = new Dictionary<string, string>(StringComparer.Ordinal)
{
["enrollmentType"] = enrollmentType,
["isHybridJoin"] = isHybridJoin ? "true" : "false",
["isSelfDeployingProfile"] = isSelfDeploying ? "true" : "false",
};
var evidence = new Evidence(
@@ -322,7 +324,7 @@ namespace AutopilotMonitor.Agent.V2.Runtime
evidence: evidence,
payload: payload);
logger.Debug($"EnrollmentFactsObserved signal posted (enrollmentType={enrollmentType}, isHybridJoin={isHybridJoin}).");
logger.Debug($"EnrollmentFactsObserved signal posted (enrollmentType={enrollmentType}, isHybridJoin={isHybridJoin}, isSelfDeployingProfile={isSelfDeploying}).");
}
catch (Exception ex)
{
@@ -115,5 +115,164 @@ namespace AutopilotMonitor.DecisionCore.Tests.Scenarios
d => d.Name == DeadlineNames.DeviceOnlyEspDetection);
Assert.Equal(HypothesisLevel.Unknown, result.FinalState.ClassifierOutcomes.DeviceOnlyDeployment.Level);
}
// ============================================================ kiosk waiver (320b3bf7)
// Session 320b3bf7: self-deploying kiosk devices never completed because the IME logs a
// false-positive AccountSetup phase line for the kioskUser0 autologon session. The
// OobeConfig 0x20|0x40 seed (EnrollmentFactsObserved isSelfDeployingProfile) plus the
// kiosk waiver keep the DeviceOnlyEspDetection terminal path alive.
[Fact]
public void KioskImeAccountSetup_falsePositiveBeforeArm_completesViaDeadline()
{
// 320b3bf7 replay shape: AccountSetup false positive lands BEFORE
// DeviceSetupProvisioningComplete — pre-fix the arm was short-circuited
// (":AccountSetupAlreadyEntered") and the session idled to the 5h timeout.
var result = RunFixture(
fixtureFilename: "selfdeploying-kiosk-ime-accountsetup-v1.jsonl",
sessionId: "session-anon-kiosk-01",
tenantId: "tenant-anon-kiosk");
Assert.Equal(SessionStage.Completed, result.FinalState.Stage);
Assert.Equal(SessionOutcome.EnrollmentComplete, result.FinalState.Outcome);
Assert.Empty(result.FinalState.Deadlines);
Assert.Equal(EnrollmentMode.SelfDeploying, result.FinalState.ScenarioProfile.Mode);
Assert.Equal(ProfileConfidence.High, result.FinalState.ScenarioProfile.Confidence);
Assert.Equal("selfdeploying_deadline_confirmed", result.FinalState.ScenarioProfile.Reason);
// The false-positive AccountSetup entry is retained as an audit fact — the census
// documents it in the enrollment_complete event rather than hiding it.
Assert.NotNull(result.FinalState.AccountSetupEnteredUtc);
Assert.Null(result.FinalState.AccountSetupProvisioningSucceededUtc);
// Desktop (kioskUser0 autologon) was seen → user_present flavor of DeviceOnly.
Assert.Equal(HypothesisLevel.Confirmed, result.FinalState.ClassifierOutcomes.DeviceOnlyDeployment.Level);
Assert.Equal(
DecisionEngine.DeviceOnlyReasons.UserPresent,
result.FinalState.ClassifierOutcomes.DeviceOnlyDeployment.Reason);
var deadlineTransition = Assert.Single(
result.Transitions,
t => t.Trigger == $"DeadlineFired:{DeadlineNames.DeviceOnlyEspDetection}");
Assert.True(deadlineTransition.Taken);
Assert.Equal(SessionStage.Completed, deadlineTransition.ToStage);
}
[Fact]
public void KioskImeAccountSetup_falsePositiveInsideWindow_deadlineSurvivesAndCompletes()
{
// Reverse ordering: deadline armed first, false positive lands inside the 5-min
// window — pre-fix HandleEspPhaseChangedV1 cancelled the armed deadline here.
var result = RunFixture(
fixtureFilename: "selfdeploying-kiosk-accountsetup-in-window-v1.jsonl",
sessionId: "session-anon-kiosk-02",
tenantId: "tenant-anon-kiosk");
Assert.Equal(SessionStage.Completed, result.FinalState.Stage);
Assert.Equal(SessionOutcome.EnrollmentComplete, result.FinalState.Outcome);
Assert.Equal(EnrollmentMode.SelfDeploying, result.FinalState.ScenarioProfile.Mode);
// No desktop/user facts in this variant → device_only flavor.
Assert.Equal(
DecisionEngine.DeviceOnlyReasons.DeviceOnly,
result.FinalState.ClassifierOutcomes.DeviceOnlyDeployment.Reason);
// The AccountSetup transition must NOT have produced a CancelDeadline for
// DeviceOnlyEspDetection — proven by the deadline actually firing terminal.
var deadlineTransition = Assert.Single(
result.Transitions,
t => t.Trigger == $"DeadlineFired:{DeadlineNames.DeviceOnlyEspDetection}");
Assert.True(deadlineTransition.Taken);
}
[Fact]
public void KioskRebootReplay_staleFireAndDuplicates_completeExactlyOnce()
{
var result = RunFixture(
fixtureFilename: "selfdeploying-kiosk-reboot-replay-v1.jsonl",
sessionId: "session-anon-kiosk-03",
tenantId: "tenant-anon-kiosk");
Assert.Equal(SessionStage.Completed, result.FinalState.Stage);
Assert.Equal(SessionOutcome.EnrollmentComplete, result.FinalState.Outcome);
// Exactly ONE taken terminal DeadlineFired transition; the stale fire (DueAtUtc
// mismatch) and the post-terminal duplicate are dead-ends.
var terminalFires = result.Transitions
.Where(t => t.Taken && t.ToStage == SessionStage.Completed)
.ToList();
Assert.Single(terminalFires);
Assert.Contains(result.Transitions, t =>
t.DeadEndReason == "device_only_esp_detection_stale_due_at_mismatch");
Assert.Contains(result.Transitions, t =>
t.DeadEndReason != null && t.DeadEndReason.StartsWith("signal_after_terminal"));
}
[Fact]
public void KioskBitsWithWhiteGloveConfirm_whiteGloveWins_noSelfDeployingTerminal()
{
// WG sealing confirm terminates as WhiteGloveSealed and clears deadlines; the late
// DeviceOnly fire must dead-end. Pins the defensive order for the two anomalous
// preProv sessions the sweep found carrying the SD bits.
var result = RunFixture(
fixtureFilename: "selfdeploying-bits-whiteglove-reclassified-v1.jsonl",
sessionId: "session-anon-kiosk-04",
tenantId: "tenant-anon-kiosk");
Assert.Equal(SessionStage.WhiteGloveSealed, result.FinalState.Stage);
Assert.Equal(SessionOutcome.WhiteGlovePart1Sealed, result.FinalState.Outcome);
Assert.Equal(EnrollmentMode.WhiteGlove, result.FinalState.ScenarioProfile.Mode);
Assert.Equal(ProfileConfidence.High, result.FinalState.ScenarioProfile.Confidence);
// The post-seal DeadlineFired is a dead-end, never a SelfDeploying terminal: the
// WG-Confirmed verdict cleared all deadlines, so stale-fire guard B catches the
// late fire (the after-terminal dispatch only intercepts Completed/Failed —
// WhiteGloveSealed routes into the handler, whose guards must hold).
Assert.Contains(result.Transitions, t =>
t.DeadEndReason == "device_only_esp_detection_stale_deadline_not_armed");
}
[Fact]
public void KioskBitsWithRealUserEspProgress_vetoReenabled_noDeadlineArmed()
{
// A genuine AccountSetupProvisioningComplete switches the kiosk waiver OFF: the
// DSPC handler takes the Classic short-circuit, no DeviceOnly deadline is armed,
// the session stays on the Classic completion path.
var result = RunFixture(
fixtureFilename: "selfdeploying-kiosk-user-esp-progress-v1.jsonl",
sessionId: "session-anon-kiosk-05",
tenantId: "tenant-anon-kiosk");
Assert.NotEqual(SessionStage.Completed, result.FinalState.Stage);
Assert.DoesNotContain(
result.FinalState.Deadlines,
d => d.Name == DeadlineNames.DeviceOnlyEspDetection);
Assert.NotNull(result.FinalState.AccountSetupProvisioningSucceededUtc);
Assert.Contains(result.Transitions, t =>
t.Trigger != null && t.Trigger.EndsWith(":AccountSetupAlreadyEntered"));
}
[Fact]
public void KioskHybridRealmJoinDeferred_releaseHonoursWaiver_completes()
{
// Deferred-release path (CompleteIfDeferredOrBookkeep): pre-fix the AccountSetup
// false positive cleared the deferred flag on RJ-resolve and the session parked
// forever. The waiver keeps the deferred SelfDeploying terminal.
var result = RunFixture(
fixtureFilename: "selfdeploying-kiosk-hybrid-rj-deferred-v1.jsonl",
sessionId: "session-anon-kiosk-06",
tenantId: "tenant-anon-kiosk");
Assert.Equal(SessionStage.Completed, result.FinalState.Stage);
Assert.Equal(SessionOutcome.EnrollmentComplete, result.FinalState.Outcome);
Assert.Equal(EnrollmentMode.SelfDeploying, result.FinalState.ScenarioProfile.Mode);
Assert.Equal(ProfileConfidence.High, result.FinalState.ScenarioProfile.Confidence);
Assert.NotNull(result.FinalState.RealmJoinFacts.ResolvedUtc);
var releaseTransition = Assert.Single(
result.Transitions,
t => t.Trigger != null && t.Trigger.EndsWith(":SelfDeployingDeferred"));
Assert.True(releaseTransition.Taken);
Assert.Equal(SessionStage.Completed, releaseTransition.ToStage);
}
}
}
@@ -154,6 +154,124 @@ namespace AutopilotMonitor.DecisionCore.Tests
Assert.DoesNotContain(step.NewState.Deadlines, d => d.Name == DeadlineNames.DeviceOnlyEspDetection);
}
// ============================================================== #3b Kiosk waiver (session 320b3bf7)
private static DecisionState SeedSelfDeployingProfile(DecisionEngine engine, DecisionState state, long ordinal)
=> engine.Reduce(state, MakeSignal(ordinal, DecisionSignalKind.EnrollmentFactsObserved, T0.AddSeconds(1),
new Dictionary<string, string>
{
[SignalPayloadKeys.EnrollmentType] = "v1",
[SignalPayloadKeys.IsHybridJoin] = "false",
[SignalPayloadKeys.IsSelfDeployingProfile] = "true",
})).NewState;
[Fact]
public void KioskWaiver_dspcArmsDeadline_despiteAccountSetupEntered()
{
// Session 320b3bf7 ordering: the IME AccountSetup false positive lands BEFORE
// DeviceSetupProvisioningComplete. With the OobeConfig seed the step-3
// short-circuit is waived and the deadline still arms.
var engine = new DecisionEngine();
var state = PrimeDeviceSetup(engine, DecisionState.CreateInitial("sd-kiosk-1", "t", T0));
state = SeedSelfDeployingProfile(engine, state, 2);
state = engine.Reduce(state, MakeSignal(3, DecisionSignalKind.EspPhaseChanged, T0.AddMinutes(2),
new Dictionary<string, string> { [SignalPayloadKeys.EspPhase] = "AccountSetup" })).NewState;
Assert.NotNull(state.AccountSetupEnteredUtc);
var step = engine.Reduce(state, MakeSignal(4, DecisionSignalKind.DeviceSetupProvisioningComplete, T0.AddMinutes(3)));
Assert.EndsWith(":DeadlineArmed", step.Transition.Trigger);
var deadline = Assert.Single(step.NewState.Deadlines, d => d.Name == DeadlineNames.DeviceOnlyEspDetection);
Assert.Equal(T0.AddMinutes(8), deadline.DueAtUtc);
// And the fire completes terminal — race guard B is waived for the seeded profile.
var terminal = engine.Reduce(step.NewState, MakeSignal(5, DecisionSignalKind.DeadlineFired, T0.AddMinutes(8),
new Dictionary<string, string> { [SignalPayloadKeys.Deadline] = DeadlineNames.DeviceOnlyEspDetection }));
Assert.Equal(SessionStage.Completed, terminal.NewState.Stage);
Assert.Equal(SessionOutcome.EnrollmentComplete, terminal.NewState.Outcome);
}
[Fact]
public void KioskWaiver_accountSetupInsideWindow_doesNotCancelDeadline()
{
// Reverse ordering: deadline armed, false positive lands inside the 5-min window.
// HandleEspPhaseChangedV1 must NOT cancel the deadline for the seeded profile.
var engine = new DecisionEngine();
var state = PrimeDeviceSetup(engine, DecisionState.CreateInitial("sd-kiosk-2", "t", T0));
state = SeedSelfDeployingProfile(engine, state, 2);
state = engine.Reduce(state, MakeSignal(3, DecisionSignalKind.DeviceSetupProvisioningComplete, T0.AddMinutes(3))).NewState;
Assert.Contains(state.Deadlines, d => d.Name == DeadlineNames.DeviceOnlyEspDetection);
var accountSetupStep = engine.Reduce(state, MakeSignal(4, DecisionSignalKind.EspPhaseChanged, T0.AddMinutes(4),
new Dictionary<string, string> { [SignalPayloadKeys.EspPhase] = "AccountSetup" }));
Assert.Contains(accountSetupStep.NewState.Deadlines, d => d.Name == DeadlineNames.DeviceOnlyEspDetection);
Assert.DoesNotContain(accountSetupStep.Effects, e => e.Kind == DecisionEffectKind.CancelDeadline);
var terminal = engine.Reduce(accountSetupStep.NewState, MakeSignal(5, DecisionSignalKind.DeadlineFired, T0.AddMinutes(8),
new Dictionary<string, string> { [SignalPayloadKeys.Deadline] = DeadlineNames.DeviceOnlyEspDetection }));
Assert.Equal(SessionStage.Completed, terminal.NewState.Stage);
}
[Fact]
public void KioskWaiver_notAppliedWithoutSeed_hotpathUnchanged()
{
// Byte-identical hotpath proof at the unit level: WITHOUT the OobeConfig seed the
// AccountSetup entry cancels the deadline exactly as before (existing behaviour,
// duplicated here as an explicit invariant next to the waiver tests).
var engine = new DecisionEngine();
var state = PrimeDeviceSetup(engine, DecisionState.CreateInitial("sd-kiosk-3", "t", T0));
state = engine.Reduce(state, MakeSignal(2, DecisionSignalKind.DeviceSetupProvisioningComplete, T0.AddMinutes(3))).NewState;
var accountSetupStep = engine.Reduce(state, MakeSignal(3, DecisionSignalKind.EspPhaseChanged, T0.AddMinutes(4),
new Dictionary<string, string> { [SignalPayloadKeys.EspPhase] = "AccountSetup" }));
Assert.DoesNotContain(accountSetupStep.NewState.Deadlines, d => d.Name == DeadlineNames.DeviceOnlyEspDetection);
Assert.Contains(accountSetupStep.Effects, e =>
e.Kind == DecisionEffectKind.CancelDeadline
&& e.CancelDeadlineName == DeadlineNames.DeviceOnlyEspDetection);
}
[Fact]
public void KioskWaiver_userEspProgress_reenablesVeto_shortCircuitsArm()
{
// A genuine AccountSetupProvisioningComplete on a seeded profile switches the
// waiver OFF — DSPC takes the Classic short-circuit and never arms the deadline.
var engine = new DecisionEngine();
var state = PrimeDeviceSetup(engine, DecisionState.CreateInitial("sd-kiosk-4", "t", T0));
state = SeedSelfDeployingProfile(engine, state, 2);
state = engine.Reduce(state, MakeSignal(3, DecisionSignalKind.EspPhaseChanged, T0.AddMinutes(2),
new Dictionary<string, string> { [SignalPayloadKeys.EspPhase] = "AccountSetup" })).NewState;
state = engine.Reduce(state, MakeSignal(4, DecisionSignalKind.AccountSetupProvisioningComplete, T0.AddMinutes(3))).NewState;
var step = engine.Reduce(state, MakeSignal(5, DecisionSignalKind.DeviceSetupProvisioningComplete, T0.AddMinutes(4)));
Assert.EndsWith(":AccountSetupAlreadyEntered", step.Transition.Trigger);
Assert.DoesNotContain(step.NewState.Deadlines, d => d.Name == DeadlineNames.DeviceOnlyEspDetection);
}
[Fact]
public void KioskWaiver_guardB_notWaivedWhenUserEspProgressedAfterArm()
{
// Deadline armed first, then genuine user-ESP progress before the fire — guard B
// must veto the terminal (unwaived) and cancel the past-due deadline state-side.
var engine = new DecisionEngine();
var state = PrimeDeviceSetup(engine, DecisionState.CreateInitial("sd-kiosk-5", "t", T0));
state = SeedSelfDeployingProfile(engine, state, 2);
state = engine.Reduce(state, MakeSignal(3, DecisionSignalKind.DeviceSetupProvisioningComplete, T0.AddMinutes(3))).NewState;
state = engine.Reduce(state, MakeSignal(4, DecisionSignalKind.EspPhaseChanged, T0.AddMinutes(4),
new Dictionary<string, string> { [SignalPayloadKeys.EspPhase] = "AccountSetup" })).NewState;
state = engine.Reduce(state, MakeSignal(5, DecisionSignalKind.AccountSetupProvisioningComplete, T0.AddMinutes(5))).NewState;
var deadline = state.Deadlines.Single(d => d.Name == DeadlineNames.DeviceOnlyEspDetection);
var step = engine.Reduce(state, MakeSignal(6, DecisionSignalKind.DeadlineFired, deadline.DueAtUtc,
new Dictionary<string, string> { [SignalPayloadKeys.Deadline] = DeadlineNames.DeviceOnlyEspDetection }));
Assert.False(step.Transition.Taken);
Assert.Equal("device_only_esp_detection_account_setup_entered", step.Transition.DeadEndReason);
Assert.DoesNotContain(step.NewState.Deadlines, d => d.Name == DeadlineNames.DeviceOnlyEspDetection);
}
// ============================================================== #5 RJ closed at signal, resolved before deadline
[Fact]
@@ -111,6 +111,118 @@ namespace AutopilotMonitor.DecisionCore.Tests.State
Assert.Equal(0, p2.EvidenceOrdinal);
}
// ------------------------------------------------ isSelfDeployingProfile seed
// Session 320b3bf7 kiosk fix: CloudAssignedOobeConfig 0x20|0x40 marker seeds
// Mode=SelfDeploying at High confidence so the IME AccountSetup false positive
// can no longer flip the session to Classic and suppress the DeviceOnly path.
[Fact]
public void EnrollmentFactsObserved_selfDeployingProfile_setsSelfDeploying_highConfidence()
{
var signal = MakeSignal(
DecisionSignalKind.EnrollmentFactsObserved,
ordinal: 0,
payload: new Dictionary<string, string>
{
[SignalPayloadKeys.EnrollmentType] = "v1",
[SignalPayloadKeys.IsHybridJoin] = "false",
[SignalPayloadKeys.IsSelfDeployingProfile] = "true",
});
var profile = EnrollmentScenarioProfileUpdater.ApplyEnrollmentFactsObserved(
EnrollmentScenarioProfile.Empty, signal);
Assert.Equal(EnrollmentMode.SelfDeploying, profile.Mode);
Assert.Equal(ProfileConfidence.High, profile.Confidence);
Assert.Equal("oobe_config_self_deploying", profile.Reason);
Assert.Equal(EnrollmentJoinMode.AzureAdJoin, profile.JoinMode);
}
[Theory]
[InlineData("false")]
[InlineData("not-a-bool")]
public void EnrollmentFactsObserved_selfDeployingProfile_falseOrUnparseable_leavesModeUnknown(string raw)
{
var signal = MakeSignal(
DecisionSignalKind.EnrollmentFactsObserved,
ordinal: 0,
payload: new Dictionary<string, string>
{
[SignalPayloadKeys.EnrollmentType] = "v1",
[SignalPayloadKeys.IsSelfDeployingProfile] = raw,
});
var profile = EnrollmentScenarioProfileUpdater.ApplyEnrollmentFactsObserved(
EnrollmentScenarioProfile.Empty, signal);
Assert.Equal(EnrollmentMode.Unknown, profile.Mode);
Assert.Equal("enrollment_facts_observed:v1", profile.Reason);
}
[Fact]
public void EnrollmentFactsObserved_selfDeployingProfile_repeatPost_isIdempotent()
{
Dictionary<string, string> Payload() => new Dictionary<string, string>
{
[SignalPayloadKeys.EnrollmentType] = "v1",
[SignalPayloadKeys.IsHybridJoin] = "false",
[SignalPayloadKeys.IsSelfDeployingProfile] = "true",
};
var p1 = EnrollmentScenarioProfileUpdater.ApplyEnrollmentFactsObserved(
EnrollmentScenarioProfile.Empty,
MakeSignal(DecisionSignalKind.EnrollmentFactsObserved, ordinal: 0, payload: Payload()));
var p2 = EnrollmentScenarioProfileUpdater.ApplyEnrollmentFactsObserved(
p1,
MakeSignal(DecisionSignalKind.EnrollmentFactsObserved, ordinal: 9, payload: Payload()));
// Every dimension already set → set-once guards short-circuit, same instance back.
Assert.Same(p1, p2);
Assert.Equal(0, p2.EvidenceOrdinal);
}
[Fact]
public void EnrollmentFactsObserved_v2_beatsSelfDeployingProfile_inSamePayload()
{
// WDP (Windows Device Preparation) has its own completion path — an enrollmentType
// "v2" in the same payload wins over the OobeConfig marker.
var signal = MakeSignal(
DecisionSignalKind.EnrollmentFactsObserved,
ordinal: 0,
payload: new Dictionary<string, string>
{
[SignalPayloadKeys.EnrollmentType] = "v2",
[SignalPayloadKeys.IsSelfDeployingProfile] = "true",
});
var profile = EnrollmentScenarioProfileUpdater.ApplyEnrollmentFactsObserved(
EnrollmentScenarioProfile.Empty, signal);
Assert.Equal(EnrollmentMode.DevicePreparation, profile.Mode);
}
[Theory]
[InlineData(EnrollmentMode.Classic)]
[InlineData(EnrollmentMode.WhiteGlove)]
[InlineData(EnrollmentMode.DevicePreparation)]
public void EnrollmentFactsObserved_selfDeployingProfile_doesNotOverrideExistingMode(EnrollmentMode existing)
{
var current = EnrollmentScenarioProfile.Empty.With(
mode: existing, confidence: ProfileConfidence.Medium,
reason: "preexisting", evidenceOrdinal: 3);
var signal = MakeSignal(
DecisionSignalKind.EnrollmentFactsObserved,
ordinal: 5,
payload: new Dictionary<string, string>
{
[SignalPayloadKeys.IsSelfDeployingProfile] = "true",
});
var profile = EnrollmentScenarioProfileUpdater.ApplyEnrollmentFactsObserved(current, signal);
Assert.Equal(existing, profile.Mode);
}
[Fact]
public void EnrollmentFactsObserved_laterSignalCannotRegressJoinMode()
{
@@ -118,6 +118,14 @@ namespace AutopilotMonitor.DecisionCore.Engine
/// <summary>On <c>SessionStarted</c>: "true" / "false" from <c>EnrollmentRegistryDetector.DetectHybridJoin()</c>.</summary>
public const string IsHybridJoin = "isHybridJoin";
/// <summary>
/// On <c>EnrollmentFactsObserved</c>: "true" / "false" from
/// <c>EnrollmentRegistryDetector.DetectSelfDeployingProfile()</c> —
/// <c>CloudAssignedOobeConfig</c> bits 0x20|0x40, the registry-deterministic
/// self-deploying profile marker (session 320b3bf7 kiosk completion fix).
/// </summary>
public const string IsSelfDeployingProfile = "isSelfDeployingProfile";
// --- InformationalEvent payload (plan §1.3, single-rail refactor) ------------
// Mirrors the EnrollmentEvent fields the reducer must reconstruct for the
// EmitEventTimelineEntry effect. EventType / Source are mandatory; the rest are
@@ -95,10 +95,22 @@ namespace AutopilotMonitor.DecisionCore.Engine
var effectsList = new List<DecisionEffect>();
if (enrollmentPhase == EnrollmentPhase.AccountSetup)
{
builder.CancelDeadline(DeadlineNames.DeviceOnlyEspDetection);
effectsList.Add(new DecisionEffect(
DecisionEffectKind.CancelDeadline,
cancelDeadlineName: DeadlineNames.DeviceOnlyEspDetection));
// Kiosk waiver (session 320b3bf7): on a registry-confirmed self-deploying
// profile (CloudAssignedOobeConfig 0x20|0x40 seed) this AccountSetup signal is
// the known IME false positive — the IME logs an AccountSetup phase line for
// the kioskUser0 autologon session although user ESP never runs. Cancelling
// DeviceOnlyEspDetection here was exactly what suppressed the SelfDeploying
// terminal path (436 five-hour-timeout failures platform-wide). The fact
// setting and ApplyAccountSetupObserved above stay unconditional: the entry
// remains visible in the enrollment_complete audit census, and the profile
// updater is a no-op because Mode is already SelfDeploying (non-Unknown).
if (!HasHighConfidenceSelfDeployingProfile(state))
{
builder.CancelDeadline(DeadlineNames.DeviceOnlyEspDetection);
effectsList.Add(new DecisionEffect(
DecisionEffectKind.CancelDeadline,
cancelDeadlineName: DeadlineNames.DeviceOnlyEspDetection));
}
// Plan §6 Fix 10 — defensive belt-and-suspenders for the premature-AwaitingHello
// bounce-back case. If Fix 7's tracker guard or Fix 8's reducer guards ever fail
@@ -378,7 +378,12 @@ namespace AutopilotMonitor.DecisionCore.Engine
state.ScenarioProfile.Confidence == ProfileConfidence.High
&& state.ScenarioProfile.Mode != EnrollmentMode.Unknown
&& state.ScenarioProfile.Mode != EnrollmentMode.SelfDeploying;
var accountSetupEntered = state.AccountSetupEnteredUtc != null;
// Kiosk waiver (session 320b3bf7): on a registry-confirmed self-deploying
// profile the AccountSetup entry is the IME false positive and must not
// abort the deferred terminal — otherwise a hybrid+self-deploying session
// would clear the deferred flag here and park forever. Same waiver as the
// DeviceOnlyEspDetection sites (AccountSetupEntryVetoesSelfDeploying).
var accountSetupEntered = AccountSetupEntryVetoesSelfDeploying(state);
if (accountSetupEntered || monotonicModeConflict)
{
@@ -31,6 +31,45 @@ namespace AutopilotMonitor.DecisionCore.Engine
public const string Ambiguous = "ambiguous";
}
/// <summary>
/// True when the scenario profile carries the registry-deterministic self-deploying
/// classification: the <c>CloudAssignedOobeConfig</c> 0x20|0x40 seed applied via
/// <see cref="DecisionSignalKind.EnrollmentFactsObserved"/> (reason
/// <c>oobe_config_self_deploying</c>). Pre-terminal, SelfDeploying@High can ONLY come
/// from that seed — <see cref="EnrollmentScenarioProfileUpdater.ApplySelfDeployingDeadlineConfirmed"/>
/// sets it exclusively inside the terminal commit of
/// <see cref="HandleDeviceOnlyEspDetectionDeadlineFired"/>.
/// <para>
/// Session 320b3bf7 (kiosk): the IME log tracker emits a false-positive
/// <c>EspPhaseChanged(AccountSetup)</c> for the kioskUser0 autologon session even
/// though user ESP never runs (AccountSetup ESP registry stays all-notStarted). When
/// this predicate holds, that signal must not suppress the DeviceOnlyEspDetection
/// completion path — platform sweep 2026-07-02: 436 of 487 failed self-deploying
/// sessions (90%) died at the 5h backend timeout through exactly this suppression.
/// Deliberately Mode/Confidence-based rather than reason-string-based:
/// <see cref="EnrollmentScenarioProfileUpdater.ApplyEspConfigDetected"/> overwrites
/// <c>Reason</c> while leaving Mode/Confidence intact.
/// </para>
/// </summary>
internal static bool HasHighConfidenceSelfDeployingProfile(DecisionState state) =>
state.ScenarioProfile.Mode == EnrollmentMode.SelfDeploying
&& state.ScenarioProfile.Confidence == ProfileConfidence.High;
/// <summary>
/// The AccountSetup-entered fact blocks the SelfDeploying path (short-circuits the
/// deadline arm, trips race guard B) — EXCEPT when the profile is registry-confirmed
/// self-deploying and user ESP never made real progress. In that combination the
/// AccountSetup entry is the known IME false positive (see
/// <see cref="HasHighConfidenceSelfDeployingProfile"/>) and must not veto completion.
/// A genuine <see cref="DecisionState.AccountSetupProvisioningSucceededUtc"/> re-enables
/// the veto: if a real user ESP succeeded on a self-deploying-bits session (anomaly),
/// the engine falls back to the Classic completion path.
/// </summary>
private static bool AccountSetupEntryVetoesSelfDeploying(DecisionState state) =>
state.AccountSetupEnteredUtc != null
&& !(HasHighConfidenceSelfDeployingProfile(state)
&& state.AccountSetupProvisioningSucceededUtc == null);
/// <summary>
/// Handle <see cref="DecisionSignalKind.DeviceSetupProvisioningComplete"/>.
/// <para>
@@ -83,7 +122,12 @@ namespace AutopilotMonitor.DecisionCore.Engine
// Step 3 — Classic-path short-circuit: AccountSetup already started. The DeviceOnly
// deadline is moot (already cancelled by HandleEspPhaseChangedV1's AccountSetup branch,
// or never armed under the new code). No deadline arm here.
if (state.AccountSetupEnteredUtc != null)
//
// Kiosk waiver (session 320b3bf7): on a registry-confirmed self-deploying profile
// the AccountSetup entry is the IME false positive — fall through to step 4 and
// arm the deadline anyway, unless user ESP genuinely progressed
// (AccountSetupEntryVetoesSelfDeploying).
if (AccountSetupEntryVetoesSelfDeploying(state))
{
var classicState = builder.Build();
var classicTransition = BuildTakenTransition(
@@ -231,7 +275,12 @@ namespace AutopilotMonitor.DecisionCore.Engine
// --- Race guard B: AccountSetup entered between deadline-arm and -fire ---
// Defensive: the AccountSetup-cancel-deadline path normally prevents this; if it
// didn't, we still must not classify as SelfDeploying.
if (state.AccountSetupEnteredUtc != null)
//
// Kiosk waiver (session 320b3bf7): on a registry-confirmed self-deploying profile
// the AccountSetup entry is the IME false positive and does not veto the terminal
// classification — unless user ESP genuinely progressed
// (AccountSetupEntryVetoesSelfDeploying).
if (AccountSetupEntryVetoesSelfDeploying(state))
{
var raceBState = state.ToBuilder()
.WithStepIndex(state.StepIndex + 1)
@@ -26,7 +26,10 @@ namespace AutopilotMonitor.DecisionCore.State
/// signal. Payload keys: <see cref="SignalPayloadKeys.EnrollmentType"/> (literal
/// "v1" / "v2" from <c>EnrollmentRegistryDetector.DetectEnrollmentType</c>),
/// <see cref="SignalPayloadKeys.IsHybridJoin"/> (boolean from
/// <c>EnrollmentRegistryDetector.DetectHybridJoin</c>).
/// <c>EnrollmentRegistryDetector.DetectHybridJoin</c>),
/// <see cref="SignalPayloadKeys.IsSelfDeployingProfile"/> (boolean from
/// <c>EnrollmentRegistryDetector.DetectSelfDeployingProfile</c> — seeds
/// Mode=SelfDeploying at High confidence, session 320b3bf7 kiosk fix).
/// <para>
/// V2 race-fix (10c8e0bf debrief, 2026-04-26) — replaces the profile-seeding side
/// of <see cref="ApplySessionStarted"/>, which lived inside the Stage-anchored
@@ -92,6 +95,30 @@ namespace AutopilotMonitor.DecisionCore.State
}
}
// Self-deploying profile marker (session 320b3bf7 kiosk fix): the agent read
// CloudAssignedOobeConfig bits 0x20|0x40 from the Autopilot policy cache —
// validated platform-wide as exclusive to self-deploying/kiosk profiles
// (2026-07-02 sweep: 1197/8556 sessions, zero user-driven false positives).
// High confidence because the marker is registry-deterministic, unlike the
// behavioural SelfDeploying inference (5-min DeviceOnlyEspDetection deadline).
//
// Gate on the LOCAL mode, not current.Mode: a "v2" enrollmentType in the SAME
// payload already promoted the local variable to DevicePreparation above and
// must win (WDP has its own completion path). An existing non-Unknown Mode on
// `current` is covered too (local mode is initialized from it), preserving the
// documented set-once/monotonic contract — including idempotency on repeat
// posts (Mode is already SelfDeploying → block skipped → no change).
if (signal.Payload.TryGetValue(SignalPayloadKeys.IsSelfDeployingProfile, out var rawSelfDeploying)
&& bool.TryParse(rawSelfDeploying, out var isSelfDeploying)
&& isSelfDeploying
&& mode == EnrollmentMode.Unknown)
{
mode = EnrollmentMode.SelfDeploying;
confidence = Max(confidence, ProfileConfidence.High);
reason = "oobe_config_self_deploying";
changed = true;
}
if (signal.Payload.TryGetValue(SignalPayloadKeys.IsHybridJoin, out var rawHybrid)
&& bool.TryParse(rawHybrid, out var isHybrid)
&& current.JoinMode == EnrollmentJoinMode.Unknown)
@@ -93,6 +93,15 @@ namespace AutopilotMonitor.Shared.Models
/// </summary>
public bool IsHybridJoin { get; set; }
/// <summary>
/// Whether the Autopilot profile carries the self-deploying/kiosk OOBE marker
/// (CloudAssignedOobeConfig bits 0x20|0x40; validated platform-wide 2026-07-02 as
/// exclusive to self-deploying profiles). Display/filter metadata only — the
/// DecisionCore behavioural gate consumes the EnrollmentFactsObserved signal, not
/// this flag.
/// </summary>
public bool IsSelfDeployingProfile { get; set; }
public SessionRegistration()
{
SessionId = Guid.NewGuid().ToString();
@@ -0,0 +1,14 @@
# Scenario: OobeConfig self-deploying bits present, but the WhiteGlove sealing classifier
# confirms Part 1 before the DeviceOnlyEspDetection deadline fires. ApplyWhiteGloveSealingConfirmed
# sets Mode=WhiteGlove@High unconditionally (WG wins over the seed), the Confirmed verdict
# terminates the session as WhiteGloveSealed and clears all deadlines — the late DeviceOnly
# fire must dead-end via the after-terminal dispatch, NOT complete as SelfDeploying.
# (Platform sweep found 2 preProv sessions carrying the bits — this pins the defensive order.)
# Expected: Stage=WhiteGloveSealed, Outcome=WhiteGlovePart1Sealed, Mode=WhiteGlove@High;
# the final DeadlineFired (ordinal 5) is NOT taken.
{"SessionSignalOrdinal":0,"SessionTraceOrdinal":0,"Kind":"SessionStarted","KindSchemaVersion":1,"OccurredAtUtc":"2026-07-02T09:00:00Z","SourceOrigin":"orchestrator","Evidence":{"Kind":"Synthetic","Identifier":"session:sd-bits-wg:started","Summary":"Session start anchor"}}
{"SessionSignalOrdinal":1,"SessionTraceOrdinal":1,"Kind":"EnrollmentFactsObserved","KindSchemaVersion":1,"OccurredAtUtc":"2026-07-02T09:00:01Z","SourceOrigin":"Program.RunAgent","Evidence":{"Kind":"Synthetic","Identifier":"enrollment_registry_facts_read","Summary":"OobeConfig self-deploying marker present"},"Payload":{"enrollmentType":"v1","isHybridJoin":"false","isSelfDeployingProfile":"true"}}
{"SessionSignalOrdinal":2,"SessionTraceOrdinal":2,"Kind":"EspPhaseChanged","KindSchemaVersion":1,"OccurredAtUtc":"2026-07-02T09:01:00Z","SourceOrigin":"ImeLogTracker","Evidence":{"Kind":"Derived","Identifier":"ime-log-tracker-v1","Summary":"IME log phase transition \u2192 DeviceSetup","DerivationInputs":{"source":"fixture"}},"Payload":{"phase":"DeviceSetup"}}
{"SessionSignalOrdinal":3,"SessionTraceOrdinal":3,"Kind":"DeviceSetupProvisioningComplete","KindSchemaVersion":1,"OccurredAtUtc":"2026-07-02T09:03:00Z","SourceOrigin":"ProvisioningStatusTracker","Evidence":{"Kind":"Derived","Identifier":"provisioning-status-tracker-v1","Summary":"Device setup provisioning complete \u2014 arms deadline at 09:08","DerivationInputs":{"source":"fixture"}}}
{"SessionSignalOrdinal":4,"SessionTraceOrdinal":4,"Kind":"ClassifierVerdictIssued","KindSchemaVersion":1,"OccurredAtUtc":"2026-07-02T09:05:00Z","SourceOrigin":"EffectRunner","Evidence":{"Kind":"Derived","Identifier":"classifier:whiteglove-sealing","Summary":"WhiteGlove sealing confirmed \u2014 technician flow, seals Part 1 and clears deadlines","DerivationInputs":{"source":"fixture"}},"Payload":{"classifier":"whiteglove-sealing","level":"Confirmed","score":"100","reason":"sealing_pattern_matched","inputHash":"fixture-wg-1"}}
{"SessionSignalOrdinal":5,"SessionTraceOrdinal":5,"Kind":"DeadlineFired","KindSchemaVersion":1,"OccurredAtUtc":"2026-07-02T09:08:00Z","SourceOrigin":"DeadlineScheduler","Evidence":{"Kind":"Synthetic","Identifier":"deadline:device_only_esp_detection:post-seal","Summary":"Stale fire after WhiteGloveSealed terminal \u2014 must dead-end, never SelfDeploying-complete."},"Payload":{"deadline":"device_only_esp_detection"}}
@@ -0,0 +1,14 @@
# Scenario: SelfDeploying kiosk — IME AccountSetup false positive lands INSIDE the 5-min window.
# Reverse ordering of selfdeploying-kiosk-ime-accountsetup-v1: DeviceSetupProvisioningComplete
# arms the DeviceOnlyEspDetection deadline FIRST, then the false-positive AccountSetup arrives.
# Pre-fix, HandleEspPhaseChangedV1 cancelled the armed deadline here (the losing side of the
# race). With the kiosk waiver the cancel is skipped for a registry-confirmed self-deploying
# profile and the deadline still fires to terminal.
# Expected: Stage=Completed, Outcome=EnrollmentComplete, Mode=SelfDeploying@High,
# DeviceOnlyDeployment=Confirmed(device_only) (no desktop/user facts).
{"SessionSignalOrdinal":0,"SessionTraceOrdinal":0,"Kind":"SessionStarted","KindSchemaVersion":1,"OccurredAtUtc":"2026-07-02T09:00:00Z","SourceOrigin":"orchestrator","Evidence":{"Kind":"Synthetic","Identifier":"session:kiosk-in-window:started","Summary":"Session start anchor"}}
{"SessionSignalOrdinal":1,"SessionTraceOrdinal":1,"Kind":"EnrollmentFactsObserved","KindSchemaVersion":1,"OccurredAtUtc":"2026-07-02T09:00:01Z","SourceOrigin":"Program.RunAgent","Evidence":{"Kind":"Synthetic","Identifier":"enrollment_registry_facts_read","Summary":"OobeConfig self-deploying marker present"},"Payload":{"enrollmentType":"v1","isHybridJoin":"false","isSelfDeployingProfile":"true"}}
{"SessionSignalOrdinal":2,"SessionTraceOrdinal":2,"Kind":"EspPhaseChanged","KindSchemaVersion":1,"OccurredAtUtc":"2026-07-02T09:01:00Z","SourceOrigin":"ImeLogTracker","Evidence":{"Kind":"Derived","Identifier":"ime-log-tracker-v1","Summary":"IME log phase transition \u2192 DeviceSetup","DerivationInputs":{"source":"fixture"}},"Payload":{"phase":"DeviceSetup"}}
{"SessionSignalOrdinal":3,"SessionTraceOrdinal":3,"Kind":"DeviceSetupProvisioningComplete","KindSchemaVersion":1,"OccurredAtUtc":"2026-07-02T09:03:00Z","SourceOrigin":"ProvisioningStatusTracker","Evidence":{"Kind":"Derived","Identifier":"provisioning-status-tracker-v1","Summary":"Device setup provisioning complete \u2014 arms deadline at 09:08","DerivationInputs":{"source":"fixture"}}}
{"SessionSignalOrdinal":4,"SessionTraceOrdinal":4,"Kind":"EspPhaseChanged","KindSchemaVersion":1,"OccurredAtUtc":"2026-07-02T09:04:00Z","SourceOrigin":"ImeLogTracker","Evidence":{"Kind":"Derived","Identifier":"ime-log-tracker-v1","Summary":"IME log phase transition \u2192 AccountSetup (kioskUser0 FALSE POSITIVE inside the armed window)","DerivationInputs":{"source":"fixture"}},"Payload":{"phase":"AccountSetup"}}
{"SessionSignalOrdinal":5,"SessionTraceOrdinal":5,"Kind":"DeadlineFired","KindSchemaVersion":1,"OccurredAtUtc":"2026-07-02T09:08:00Z","SourceOrigin":"DeadlineScheduler","Evidence":{"Kind":"Synthetic","Identifier":"deadline:device_only_esp_detection:fired","Summary":"Deadline survived the AccountSetup false positive (kiosk waiver) and fires to terminal."},"Payload":{"deadline":"device_only_esp_detection"}}
@@ -0,0 +1,17 @@
# Scenario: Self-deploying kiosk with RealmJoin (hybrid) — the deferred-completion release
# must honour the kiosk waiver. Sequence: seed → RJ detected (gate closes) → DeviceSetup →
# false-positive AccountSetup → DeviceSetupProvisioningComplete arms deadline (waiver) →
# deadline fires with RJ gate closed → SelfDeployingDeferredCompletion set → RJ resolves →
# CompleteIfDeferredOrBookkeep re-check: the AccountSetup entry must NOT demote the deferred
# terminal (pre-fix it cleared the flag and the session parked forever).
# (Sweep found 0 hybrid+SD-bits sessions; this pins the defensive path.)
# Expected: Stage=Completed, Outcome=EnrollmentComplete, Mode=SelfDeploying@High,
# deferred flag consumed, RJ resolved fact present.
{"SessionSignalOrdinal":0,"SessionTraceOrdinal":0,"Kind":"SessionStarted","KindSchemaVersion":1,"OccurredAtUtc":"2026-07-02T09:00:00Z","SourceOrigin":"orchestrator","Evidence":{"Kind":"Synthetic","Identifier":"session:kiosk-rj-deferred:started","Summary":"Session start anchor"}}
{"SessionSignalOrdinal":1,"SessionTraceOrdinal":1,"Kind":"EnrollmentFactsObserved","KindSchemaVersion":1,"OccurredAtUtc":"2026-07-02T09:00:01Z","SourceOrigin":"Program.RunAgent","Evidence":{"Kind":"Synthetic","Identifier":"enrollment_registry_facts_read","Summary":"OobeConfig self-deploying marker present (hybrid variant)"},"Payload":{"enrollmentType":"v1","isHybridJoin":"true","isSelfDeployingProfile":"true"}}
{"SessionSignalOrdinal":2,"SessionTraceOrdinal":2,"Kind":"RealmJoinDetected","KindSchemaVersion":1,"OccurredAtUtc":"2026-07-02T09:00:30Z","SourceOrigin":"RealmJoinWatcher","Evidence":{"Kind":"Derived","Identifier":"realmjoin-watcher-v1","Summary":"RealmJoin deployment detected (phase 100) \u2014 completion gate closes","DerivationInputs":{"source":"fixture"}},"Payload":{"deploymentPhase":"100"}}
{"SessionSignalOrdinal":3,"SessionTraceOrdinal":3,"Kind":"EspPhaseChanged","KindSchemaVersion":1,"OccurredAtUtc":"2026-07-02T09:01:00Z","SourceOrigin":"ImeLogTracker","Evidence":{"Kind":"Derived","Identifier":"ime-log-tracker-v1","Summary":"IME log phase transition \u2192 DeviceSetup","DerivationInputs":{"source":"fixture"}},"Payload":{"phase":"DeviceSetup"}}
{"SessionSignalOrdinal":4,"SessionTraceOrdinal":4,"Kind":"EspPhaseChanged","KindSchemaVersion":1,"OccurredAtUtc":"2026-07-02T09:02:00Z","SourceOrigin":"ImeLogTracker","Evidence":{"Kind":"Derived","Identifier":"ime-log-tracker-v1","Summary":"IME log phase transition \u2192 AccountSetup (kioskUser0 FALSE POSITIVE)","DerivationInputs":{"source":"fixture"}},"Payload":{"phase":"AccountSetup"}}
{"SessionSignalOrdinal":5,"SessionTraceOrdinal":5,"Kind":"DeviceSetupProvisioningComplete","KindSchemaVersion":1,"OccurredAtUtc":"2026-07-02T09:03:00Z","SourceOrigin":"ProvisioningStatusTracker","Evidence":{"Kind":"Derived","Identifier":"provisioning-status-tracker-v1","Summary":"Device setup provisioning complete \u2014 kiosk waiver arms deadline at 09:08 despite AccountSetup entry","DerivationInputs":{"source":"fixture"}}}
{"SessionSignalOrdinal":6,"SessionTraceOrdinal":6,"Kind":"DeadlineFired","KindSchemaVersion":1,"OccurredAtUtc":"2026-07-02T09:08:00Z","SourceOrigin":"DeadlineScheduler","Evidence":{"Kind":"Synthetic","Identifier":"deadline:device_only_esp_detection:fired","Summary":"Guards pass (kiosk waiver), RJ gate closed \u2192 SelfDeployingDeferredCompletion set"},"Payload":{"deadline":"device_only_esp_detection"}}
{"SessionSignalOrdinal":7,"SessionTraceOrdinal":7,"Kind":"RealmJoinResolved","KindSchemaVersion":1,"OccurredAtUtc":"2026-07-02T09:10:00Z","SourceOrigin":"RealmJoinWatcher","Evidence":{"Kind":"Derived","Identifier":"realmjoin-watcher-v1","Summary":"RealmJoin resolved (phase 110) \u2014 deferred SelfDeploying completion releases","DerivationInputs":{"source":"fixture"}},"Payload":{"deploymentPhase":"110"}}
@@ -0,0 +1,19 @@
# Scenario: SelfDeploying kiosk with IME AccountSetup false positive (session 320b3bf7 shape).
# Real-world failure: the IME logs an AccountSetup phase line for the kioskUser0 autologon
# session although user ESP never runs (AccountSetup ESP registry stays all-notStarted).
# Pre-fix, that EspPhaseChanged(AccountSetup) cancelled/blocked the DeviceOnlyEspDetection
# deadline and the session idled to the 5h backend timeout (436 sessions platform-wide).
# The EnrollmentFactsObserved seed (CloudAssignedOobeConfig 0x20|0x40 → isSelfDeployingProfile)
# classifies Mode=SelfDeploying@High up front; the kiosk waiver keeps the deadline path alive.
# Expected: Stage=Completed, Outcome=EnrollmentComplete, Mode=SelfDeploying@High
# (reason=selfdeploying_deadline_confirmed after terminal), AccountSetupEnteredUtc
# retained for audit, DeviceOnlyDeployment=Confirmed(user_present) (desktop seen).
{"SessionSignalOrdinal":0,"SessionTraceOrdinal":0,"Kind":"SessionStarted","KindSchemaVersion":1,"OccurredAtUtc":"2026-07-02T09:00:00Z","SourceOrigin":"orchestrator","Evidence":{"Kind":"Synthetic","Identifier":"session:kiosk-ime-accountsetup:started","Summary":"Session start anchor"}}
{"SessionSignalOrdinal":1,"SessionTraceOrdinal":1,"Kind":"EnrollmentFactsObserved","KindSchemaVersion":1,"OccurredAtUtc":"2026-07-02T09:00:01Z","SourceOrigin":"Program.RunAgent","Evidence":{"Kind":"Synthetic","Identifier":"enrollment_registry_facts_read","Summary":"Registry facts incl. OobeConfig self-deploying marker (0x5FE)"},"Payload":{"enrollmentType":"v1","isHybridJoin":"false","isSelfDeployingProfile":"true"}}
{"SessionSignalOrdinal":2,"SessionTraceOrdinal":2,"Kind":"HelloPolicyDetected","KindSchemaVersion":1,"OccurredAtUtc":"2026-07-02T09:00:02Z","SourceOrigin":"EspAndHelloTracker","Evidence":{"Kind":"Derived","Identifier":"hello-policy-detector-v1","Summary":"WHfB policy enabled via CSP (device-scoped) \u2014 kioskUser0 will never enroll Hello","DerivationInputs":{"source":"fixture"}},"Payload":{"helloEnabled":"true","policySource":"csp"}}
{"SessionSignalOrdinal":3,"SessionTraceOrdinal":3,"Kind":"EspPhaseChanged","KindSchemaVersion":1,"OccurredAtUtc":"2026-07-02T09:02:00Z","SourceOrigin":"ImeLogTracker","Evidence":{"Kind":"Derived","Identifier":"ime-log-tracker-v1","Summary":"IME log phase transition \u2192 DeviceSetup","DerivationInputs":{"source":"fixture"}},"Payload":{"phase":"DeviceSetup"}}
{"SessionSignalOrdinal":4,"SessionTraceOrdinal":4,"Kind":"EspExiting","KindSchemaVersion":1,"OccurredAtUtc":"2026-07-02T09:14:08Z","SourceOrigin":"ShellCoreWatcher","Evidence":{"Kind":"Raw","Identifier":"shell-core-62407","Summary":"CommercialOOBE_ESPProgress_Page_Exiting errorCode=0"}}
{"SessionSignalOrdinal":5,"SessionTraceOrdinal":5,"Kind":"EspPhaseChanged","KindSchemaVersion":1,"OccurredAtUtc":"2026-07-02T09:14:19Z","SourceOrigin":"ImeLogTracker","Evidence":{"Kind":"Derived","Identifier":"ime-log-tracker-v1","Summary":"IME log phase transition \u2192 AccountSetup (kioskUser0 autologon FALSE POSITIVE \u2014 user ESP never runs)","DerivationInputs":{"source":"fixture"}},"Payload":{"phase":"AccountSetup"}}
{"SessionSignalOrdinal":6,"SessionTraceOrdinal":6,"Kind":"DeviceSetupProvisioningComplete","KindSchemaVersion":1,"OccurredAtUtc":"2026-07-02T09:14:34Z","SourceOrigin":"ProvisioningStatusTracker","Evidence":{"Kind":"Derived","Identifier":"provisioning-status-tracker-v1","Summary":"Device setup provisioning complete (30s all-subcategories fallback)","DerivationInputs":{"fallbackReason":"all_subcategories_succeeded_category_unresolved"}}}
{"SessionSignalOrdinal":7,"SessionTraceOrdinal":7,"Kind":"DesktopArrived","KindSchemaVersion":1,"OccurredAtUtc":"2026-07-02T09:17:00Z","SourceOrigin":"DesktopArrivalDetector","Evidence":{"Kind":"Derived","Identifier":"desktop-arrival-v1","Summary":"explorer.exe under kioskUser0 autologon session","DerivationInputs":{"source":"fixture"}}}
{"SessionSignalOrdinal":8,"SessionTraceOrdinal":8,"Kind":"DeadlineFired","KindSchemaVersion":1,"OccurredAtUtc":"2026-07-02T09:19:34Z","SourceOrigin":"DeadlineScheduler","Evidence":{"Kind":"Synthetic","Identifier":"deadline:device_only_esp_detection:fired","Summary":"5-min window elapsed \u2014 kiosk waiver keeps race guard B open, terminal SelfDeploying."},"Payload":{"deadline":"device_only_esp_detection"}}
@@ -0,0 +1,18 @@
# Scenario: SelfDeploying kiosk — reboot noise, duplicate desktop arrivals, stale deadline fire.
# Session 320b3bf7 rebooted 8 times while parked; each restart re-posted desktop_arrived and
# could surface stale DeadlineFired signals from pre-reboot deadline incarnations. Guards must
# absorb the noise: guard C (DueAtUtc mismatch) dead-ends the stale fire WITHOUT cancelling the
# armed deadline, the real fire completes exactly once, and a post-terminal fire dead-ends via
# the after-terminal dispatch.
# Expected: Stage=Completed, Outcome=EnrollmentComplete, Mode=SelfDeploying@High; the stale
# fire (ordinal 6) and the post-terminal fire (ordinal 9) are NOT taken.
{"SessionSignalOrdinal":0,"SessionTraceOrdinal":0,"Kind":"SessionStarted","KindSchemaVersion":1,"OccurredAtUtc":"2026-07-02T09:00:00Z","SourceOrigin":"orchestrator","Evidence":{"Kind":"Synthetic","Identifier":"session:kiosk-reboot-replay:started","Summary":"Session start anchor"}}
{"SessionSignalOrdinal":1,"SessionTraceOrdinal":1,"Kind":"EnrollmentFactsObserved","KindSchemaVersion":1,"OccurredAtUtc":"2026-07-02T09:00:01Z","SourceOrigin":"Program.RunAgent","Evidence":{"Kind":"Synthetic","Identifier":"enrollment_registry_facts_read","Summary":"OobeConfig self-deploying marker present"},"Payload":{"enrollmentType":"v1","isHybridJoin":"false","isSelfDeployingProfile":"true"}}
{"SessionSignalOrdinal":2,"SessionTraceOrdinal":2,"Kind":"EspPhaseChanged","KindSchemaVersion":1,"OccurredAtUtc":"2026-07-02T09:01:00Z","SourceOrigin":"ImeLogTracker","Evidence":{"Kind":"Derived","Identifier":"ime-log-tracker-v1","Summary":"IME log phase transition \u2192 DeviceSetup","DerivationInputs":{"source":"fixture"}},"Payload":{"phase":"DeviceSetup"}}
{"SessionSignalOrdinal":3,"SessionTraceOrdinal":3,"Kind":"DeviceSetupProvisioningComplete","KindSchemaVersion":1,"OccurredAtUtc":"2026-07-02T09:03:00Z","SourceOrigin":"ProvisioningStatusTracker","Evidence":{"Kind":"Derived","Identifier":"provisioning-status-tracker-v1","Summary":"Device setup provisioning complete \u2014 arms deadline at 09:08","DerivationInputs":{"source":"fixture"}}}
{"SessionSignalOrdinal":4,"SessionTraceOrdinal":4,"Kind":"SystemRebootObserved","KindSchemaVersion":1,"OccurredAtUtc":"2026-07-02T09:05:00Z","SourceOrigin":"BootAnchor","Evidence":{"Kind":"Derived","Identifier":"boot-anchor-v1","Summary":"Prior agent run terminated by reboot (kiosk restart)","DerivationInputs":{"source":"fixture"}}}
{"SessionSignalOrdinal":5,"SessionTraceOrdinal":5,"Kind":"DesktopArrived","KindSchemaVersion":1,"OccurredAtUtc":"2026-07-02T09:06:00Z","SourceOrigin":"DesktopArrivalDetector","Evidence":{"Kind":"Derived","Identifier":"desktop-arrival-v1","Summary":"explorer.exe under kioskUser0 (post-reboot autologon)","DerivationInputs":{"source":"fixture"}}}
{"SessionSignalOrdinal":6,"SessionTraceOrdinal":6,"Kind":"DeadlineFired","KindSchemaVersion":1,"OccurredAtUtc":"2026-07-02T09:06:30Z","SourceOrigin":"DeadlineScheduler","Evidence":{"Kind":"Synthetic","Identifier":"deadline:device_only_esp_detection:stale-fire","Summary":"Stale pre-reboot fire \u2014 OccurredAtUtc mismatches the armed DueAtUtc (09:08), guard C dead-ends WITHOUT cancelling."},"Payload":{"deadline":"device_only_esp_detection"}}
{"SessionSignalOrdinal":7,"SessionTraceOrdinal":7,"Kind":"DesktopArrived","KindSchemaVersion":1,"OccurredAtUtc":"2026-07-02T09:07:00Z","SourceOrigin":"DesktopArrivalDetector","Evidence":{"Kind":"Derived","Identifier":"desktop-arrival-v1","Summary":"Duplicate desktop arrival after agent restart \u2014 DesktopArrivedUtc is set-once","DerivationInputs":{"source":"fixture"}}}
{"SessionSignalOrdinal":8,"SessionTraceOrdinal":8,"Kind":"DeadlineFired","KindSchemaVersion":1,"OccurredAtUtc":"2026-07-02T09:08:00Z","SourceOrigin":"DeadlineScheduler","Evidence":{"Kind":"Synthetic","Identifier":"deadline:device_only_esp_detection:fired","Summary":"Real fire at the armed DueAtUtc \u2014 terminal SelfDeploying."},"Payload":{"deadline":"device_only_esp_detection"}}
{"SessionSignalOrdinal":9,"SessionTraceOrdinal":9,"Kind":"DeadlineFired","KindSchemaVersion":1,"OccurredAtUtc":"2026-07-02T09:08:30Z","SourceOrigin":"DeadlineScheduler","Evidence":{"Kind":"Synthetic","Identifier":"deadline:device_only_esp_detection:post-terminal","Summary":"Late duplicate fire after terminal \u2014 after-terminal dispatch dead-ends it."},"Payload":{"deadline":"device_only_esp_detection"}}
@@ -0,0 +1,15 @@
# Scenario: OobeConfig self-deploying bits present, but user ESP GENUINELY progresses
# (AccountSetupProvisioningComplete fires). The kiosk waiver must switch OFF: a real
# AccountSetup provisioning success re-enables the AccountSetup veto, so
# DeviceSetupProvisioningComplete takes the Classic short-circuit and the DeviceOnly
# deadline is never armed — the session continues on the Classic completion path.
# (Anomaly guard: 0 such sessions observed in the sweep, defended anyway.)
# Expected: Stage=EspAccountSetup (non-terminal), no DeviceOnlyEspDetection deadline,
# Mode stays SelfDeploying@High (profile is monotonic — only the completion
# routing falls back to Classic).
{"SessionSignalOrdinal":0,"SessionTraceOrdinal":0,"Kind":"SessionStarted","KindSchemaVersion":1,"OccurredAtUtc":"2026-07-02T09:00:00Z","SourceOrigin":"orchestrator","Evidence":{"Kind":"Synthetic","Identifier":"session:kiosk-user-esp:started","Summary":"Session start anchor"}}
{"SessionSignalOrdinal":1,"SessionTraceOrdinal":1,"Kind":"EnrollmentFactsObserved","KindSchemaVersion":1,"OccurredAtUtc":"2026-07-02T09:00:01Z","SourceOrigin":"Program.RunAgent","Evidence":{"Kind":"Synthetic","Identifier":"enrollment_registry_facts_read","Summary":"OobeConfig self-deploying marker present"},"Payload":{"enrollmentType":"v1","isHybridJoin":"false","isSelfDeployingProfile":"true"}}
{"SessionSignalOrdinal":2,"SessionTraceOrdinal":2,"Kind":"EspPhaseChanged","KindSchemaVersion":1,"OccurredAtUtc":"2026-07-02T09:01:00Z","SourceOrigin":"ImeLogTracker","Evidence":{"Kind":"Derived","Identifier":"ime-log-tracker-v1","Summary":"IME log phase transition \u2192 DeviceSetup","DerivationInputs":{"source":"fixture"}},"Payload":{"phase":"DeviceSetup"}}
{"SessionSignalOrdinal":3,"SessionTraceOrdinal":3,"Kind":"EspPhaseChanged","KindSchemaVersion":1,"OccurredAtUtc":"2026-07-02T09:02:00Z","SourceOrigin":"ImeLogTracker","Evidence":{"Kind":"Derived","Identifier":"ime-log-tracker-v1","Summary":"IME log phase transition \u2192 AccountSetup","DerivationInputs":{"source":"fixture"}},"Payload":{"phase":"AccountSetup"}}
{"SessionSignalOrdinal":4,"SessionTraceOrdinal":4,"Kind":"AccountSetupProvisioningComplete","KindSchemaVersion":1,"OccurredAtUtc":"2026-07-02T09:03:00Z","SourceOrigin":"ProvisioningStatusTracker","Evidence":{"Kind":"Derived","Identifier":"provisioning-status-tracker-v1","Summary":"AccountSetup category genuinely succeeded \u2014 user ESP made real progress","DerivationInputs":{"source":"fixture"}}}
{"SessionSignalOrdinal":5,"SessionTraceOrdinal":5,"Kind":"DeviceSetupProvisioningComplete","KindSchemaVersion":1,"OccurredAtUtc":"2026-07-02T09:04:00Z","SourceOrigin":"ProvisioningStatusTracker","Evidence":{"Kind":"Derived","Identifier":"provisioning-status-tracker-v1","Summary":"Device setup resolved late \u2014 veto re-enabled by the real AccountSetup success, Classic short-circuit, no deadline arm","DerivationInputs":{"source":"fixture"}}}