bootstrap(dev): v2.3-dev.2 - Cloud PC detection on captured evidence

First W365 field test (2026-08-06) proved the assumed WMI signature wrong:
a real Cloud PC reports the generic Manufacturer='Microsoft Corporation' +
Model='Virtual Machine'; the 'Cloud PC Enterprise ...' string exists only in
Intune/Graph inventory. Probe on the live Cloud PC captured two local
markers instead: HKLM\SOFTWARE\Microsoft\Windows365 and the
CloudManagedDesktopExtension service (W365 management agent).

Get-CloudPcModel is replaced by Test-IsCloudPc requiring BOTH markers (each
alone has look-alikes: W365-Boot physical clients / legacy managed-desktop
agents). Raw identity and both marker states are always logged. Tests
updated to the marker model (26 tests, green under PS 5.1 and pwsh 7).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Oliver Kieselbach
2026-08-06 18:14:58 +02:00
parent 4c1889d011
commit acf4d0eeaa
2 changed files with 79 additions and 43 deletions
@@ -23,29 +23,44 @@ BeforeAll {
}
}
Describe 'Get-CloudPcModel' {
Describe 'Test-IsCloudPc' {
# Marker set captured 2026-08-06 from a real W365 Enterprise Cloud PC: local WMI
# is generic ('Virtual Machine'), so detection rests on the Windows365 registry
# key AND the CloudManagedDesktopExtension service (both required).
BeforeEach {
Mock Write-Log { }
Mock Get-CimInstance { [pscustomobject]@{ Manufacturer = 'Microsoft Corporation'; Model = 'Virtual Machine' } }
}
It 'returns the model string for a Cloud PC' {
Mock Get-CimInstance { [pscustomobject]@{ Manufacturer = 'Microsoft Corporation'; Model = 'Cloud PC Enterprise 2vCPU/8GB/128GB' } }
Get-CloudPcModel | Should -Be 'Cloud PC Enterprise 2vCPU/8GB/128GB'
It 'detects a Cloud PC when both markers are present' {
Mock Test-Path { $true } -ParameterFilter { $Path -eq 'HKLM:\SOFTWARE\Microsoft\Windows365' }
Mock Get-Service { [pscustomobject]@{ Name = 'CloudManagedDesktopExtension'; Status = 'Running' } }
Test-IsCloudPc | Should -BeTrue
}
It 'returns null for a physical device' {
Mock Get-CimInstance { [pscustomobject]@{ Manufacturer = 'Dell Inc.'; Model = 'Latitude 7450' } }
Get-CloudPcModel | Should -BeNullOrEmpty
It 'does not detect with the registry key alone (e.g. W365-Boot physical client)' {
Mock Test-Path { $true } -ParameterFilter { $Path -eq 'HKLM:\SOFTWARE\Microsoft\Windows365' }
Mock Get-Service { $null }
Test-IsCloudPc | Should -BeFalse
}
It 'returns null for a non-Microsoft device whose model happens to match' {
Mock Get-CimInstance { [pscustomobject]@{ Manufacturer = 'Contoso Ltd.'; Model = 'Cloud PC Clone' } }
Get-CloudPcModel | Should -BeNullOrEmpty
It 'does not detect with the service alone' {
Mock Test-Path { $false } -ParameterFilter { $Path -eq 'HKLM:\SOFTWARE\Microsoft\Windows365' }
Mock Get-Service { [pscustomobject]@{ Name = 'CloudManagedDesktopExtension'; Status = 'Running' } }
Test-IsCloudPc | Should -BeFalse
}
It 'returns null when the WMI query fails (SKIP-safe)' {
It 'does not detect a plain device (no markers)' {
Mock Test-Path { $false } -ParameterFilter { $Path -eq 'HKLM:\SOFTWARE\Microsoft\Windows365' }
Mock Get-Service { $null }
Test-IsCloudPc | Should -BeFalse
}
It 'still evaluates markers when the WMI identity query fails' {
Mock Get-CimInstance { throw 'RPC server unavailable' }
Get-CloudPcModel | Should -BeNullOrEmpty
Mock Test-Path { $true } -ParameterFilter { $Path -eq 'HKLM:\SOFTWARE\Microsoft\Windows365' }
Mock Get-Service { [pscustomobject]@{ Name = 'CloudManagedDesktopExtension'; Status = 'Running' } }
Test-IsCloudPc | Should -BeTrue
}
}
@@ -109,19 +124,19 @@ Describe 'Get-RelaxDecision' {
Mock Write-Log { }
# Defaults: physical device, OOBE long done. Individual tests override.
Mock Get-OobeState { 'Completed' }
Mock Get-CloudPcModel { $null }
Mock Test-IsCloudPc { $false }
}
Context 'Windows 365 Cloud PC' {
BeforeEach {
Mock Get-CloudPcModel { 'Cloud PC Enterprise 2vCPU/8GB/128GB' }
Mock Test-IsCloudPc { $true }
}
It 'relaxes for a single fresh profile although OOBE is Completed (W365 field case)' {
$p = New-FakeProfile 'LeonGottschalk' 0.6
$d = Get-RelaxDecision -ProfilePaths @($p) -OobeProfileMaxAgeMinutes 15
$d.Active | Should -BeTrue
$d.CloudPcModel | Should -Be 'Cloud PC Enterprise 2vCPU/8GB/128GB'
$d.IsCloudPc | Should -BeTrue
}
It 'does not relax for an old profile (productive Cloud PC)' {
@@ -176,7 +191,7 @@ Describe 'Get-BootstrapDecision' {
Mock Get-ItemProperty { $null }
Mock Get-RealUserProfilePaths { @() }
Mock Get-OobeState { 'Completed' }
Mock Get-CloudPcModel { $null }
Mock Test-IsCloudPc { $false }
Mock Get-CimInstance { [pscustomobject]@{ LastBootUpTime = (Get-Date).AddHours(-1) } } -ParameterFilter { $ClassName -eq 'Win32_OperatingSystem' }
$script:cleanAgentBin = Join-Path $TestDrive 'AgentBin'
}
@@ -206,7 +221,7 @@ Describe 'Get-BootstrapDecision' {
It 'installs on a freshly provisioned Cloud PC with one fresh profile (W365 field case)' {
$script:fakeProfile = New-FakeProfile 'LeonGottschalk' 0.6
Mock Get-RealUserProfilePaths { @($script:fakeProfile) }
Mock Get-CloudPcModel { 'Cloud PC Enterprise 2vCPU/8GB/128GB' }
Mock Test-IsCloudPc { $true }
# First-connect user is already visible in LogonUI on W365 -- must not block either.
Mock Get-ItemProperty { [pscustomobject]@{ LastLoggedOnUser = 'AzureAD\LeonGottschalk' } } -ParameterFilter { $Path -like '*LogonUI*' }
$d = Get-BootstrapDecision -MaxBootstrapWindowHours 12 -OobeProfileMaxAgeMinutes 15 -AgentBinPath $script:cleanAgentBin
@@ -37,8 +37,10 @@
complete before any user ever connects, so OutOfBoxExperienceState is NEVER
InProgress when this script runs. The assigned user's profile is created at
first connect, typically seconds before IME pulls this script. Trigger: the
device positively identifies as a Cloud PC via Win32_ComputerSystem
(Manufacturer 'Microsoft Corporation' + Model 'Cloud PC*').
device positively identifies as a Cloud PC via TWO local markers (both
required; captured from a real Cloud PC -- local WMI reports only the
generic 'Virtual Machine' model): HKLM\SOFTWARE\Microsoft\Windows365
registry key AND the CloudManagedDesktopExtension service.
Either trigger relaxes guards 2+3 ONLY when additionally:
(a) there is exactly ONE real user profile, AND
@@ -84,6 +86,13 @@
PowerShell 5.1 (IME) reads scripts without BOM as ANSI, corrupting multi-byte chars.
.CHANGELOG
2026-08-06 v2.3-dev.2 Cloud PC detection rebuilt on captured evidence: first field
test proved local WMI reports only Manufacturer='Microsoft
Corporation' + Model='Virtual Machine' (the 'Cloud PC ...' model
string exists only in Intune/Graph). Detection now requires the
HKLM\SOFTWARE\Microsoft\Windows365 registry key AND the
CloudManagedDesktopExtension service (both verified present on a
real Cloud PC); raw identity + marker states are always logged.
2026-08-05 v2.3-dev DEV variant (-dev.ps1, own log file bootstrap_agent_dev.log)
to validate on Windows 365 before promotion to v2.3:
Cloud-PC-relax -- a positively identified Cloud PC
@@ -126,7 +135,7 @@ param(
)
# Script version (bump on meaningful changes; see .CHANGELOG above)
$ScriptVersion = "2.3-dev"
$ScriptVersion = "2.3-dev.2"
# Configuration - Everything in ProgramData for easy cleanup
$AgentBasePath = "$env:ProgramData\AutopilotMonitor"
@@ -161,23 +170,35 @@ function Get-OobeState {
}
}
# Positive Windows 365 Cloud PC detection via the documented WMI identity.
# Cloud PCs report Manufacturer 'Microsoft Corporation' and a Model starting with
# 'Cloud PC' (e.g. 'Cloud PC Enterprise 2vCPU/8GB/128GB').
# Returns the model string, or $null when not a Cloud PC / on any error (SKIP-safe).
function Get-CloudPcModel {
# Positive Windows 365 Cloud PC detection.
# Evidence captured 2026-08-06 on a real, freshly provisioned W365 Enterprise Cloud PC:
# - Win32_ComputerSystem is GENERIC there: Manufacturer='Microsoft Corporation',
# Model='Virtual Machine'. The 'Cloud PC Enterprise ...' model string exists only
# in the Intune/Graph inventory, NOT in local WMI/SMBIOS -- it cannot be used here.
# - HKLM:\SOFTWARE\Microsoft\Windows365 exists.
# - Service 'CloudManagedDesktopExtension' (Microsoft Cloud Managed Desktop
# Extension, the W365 management agent that runs ON Cloud PCs) is installed.
# BOTH markers are required (AND): each alone has plausible look-alikes (W365-Boot
# physical clients may carry Windows365 policy state; the MCMD agent family also
# served other managed-desktop offerings). Raw identity and both marker states are
# always logged for field diagnosis; any error returns $false (SKIP-safe).
function Test-IsCloudPc {
try {
$cs = Get-CimInstance Win32_ComputerSystem -ErrorAction Stop
# Always log the raw identity: field diagnosis must never depend on a
# matched pattern (2026-08-06 W365 test: detection missed and the log
# could not tell why).
Write-Log "Computer identity: Manufacturer='$($cs.Manufacturer)' Model='$($cs.Model)'."
if ($cs.Manufacturer -eq 'Microsoft Corporation' -and $cs.Model -like 'Cloud PC*') {
return $cs.Model
}
}
catch { Write-Log "INFO: computer identity query failed: $($_.Exception.Message)" }
return $null
try {
$regMarker = Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows365'
$svcMarker = $null -ne (Get-Service -Name 'CloudManagedDesktopExtension' -ErrorAction SilentlyContinue)
Write-Log "Cloud PC markers: Windows365 registry key=$regMarker; CloudManagedDesktopExtension service=$svcMarker."
return ($regMarker -and $svcMarker)
}
catch {
Write-Log "INFO: Cloud PC marker check failed: $($_.Exception.Message)"
return $false
}
}
# Collects real (non-special) user profiles as full paths (WMI Special flag + filesystem).
@@ -203,7 +224,7 @@ function Get-RealUserProfilePaths {
# (2) Cloud PC identity -> Windows 365 (headless OOBE, profile at first connect)
# Either trigger additionally requires: exactly one real profile AND that profile
# created < $OobeProfileMaxAgeMinutes minutes ago. Any miss keeps the original skip.
# Returns an object with: Active (bool), OobeState (string), CloudPcModel (string or null).
# Returns an object with: Active (bool), OobeState (string), IsCloudPc (bool).
function Get-RelaxDecision {
param(
[string[]]$ProfilePaths,
@@ -214,16 +235,16 @@ function Get-RelaxDecision {
$relaxActive = $false
$oobeState = Get-OobeState
$oobeInProgress = ($oobeState -eq 'InProgress')
$cloudPcModel = Get-CloudPcModel
if ($cloudPcModel) {
Write-Log "Cloud PC detected: model '$cloudPcModel' (OOBE state: $oobeState)."
$isCloudPc = Test-IsCloudPc
if ($isCloudPc) {
Write-Log "Cloud PC detected (OOBE state: $oobeState)."
}
$relaxTrigger = $null
if ($oobeInProgress) {
$relaxTrigger = "OOBE InProgress (Windows Backup restore case)"
} elseif ($cloudPcModel) {
$relaxTrigger = "Cloud PC '$cloudPcModel' (headless provisioning, OOBE state: $oobeState)"
} elseif ($isCloudPc) {
$relaxTrigger = "Cloud PC (headless provisioning, OOBE state: $oobeState)"
}
if ($ProfilePaths.Count -eq 1 -and $relaxTrigger) {
@@ -240,13 +261,13 @@ function Get-RelaxDecision {
} elseif ($ProfilePaths.Count -eq 1) {
Write-Log "Relax NOT applied: single profile present but neither OOBE InProgress nor Cloud PC (OOBE state: $oobeState)."
} elseif ($ProfilePaths.Count -gt 1) {
Write-Log ("Relax NOT applied: {0} real profiles present (relax requires exactly one). OOBE state: {1}; CloudPC: {2}." -f $ProfilePaths.Count, $oobeState, [bool]$cloudPcModel)
Write-Log ("Relax NOT applied: {0} real profiles present (relax requires exactly one). OOBE state: {1}; CloudPC: {2}." -f $ProfilePaths.Count, $oobeState, $isCloudPc)
}
return [pscustomobject]@{
Active = $relaxActive
OobeState = $oobeState
CloudPcModel = $cloudPcModel
Active = $relaxActive
OobeState = $oobeState
IsCloudPc = $isCloudPc
}
}
@@ -282,7 +303,7 @@ function Get-BootstrapDecision {
"{0} (created {1:u}, {2:N1}min ago)" -f $leaf, $c, ((Get-Date).ToUniversalTime() - $c).TotalMinutes
} catch { "$leaf (age unknown)" }
}) | Select-Object -First 3) -join '; '
Write-Log "SKIP: Real user profile(s) found ($details). OOBE state: $($relax.OobeState). CloudPC: $([bool]$relax.CloudPcModel). Device appears productive."
Write-Log "SKIP: Real user profile(s) found ($details). OOBE state: $($relax.OobeState). CloudPC: $($relax.IsCloudPc). Device appears productive."
return [pscustomobject]@{ Install = $false; ReasonCode = 'ProfilesFound'; RelaxActive = $false }
}