From 588ef08fdf3bc896d3adb920d7ee85b31a46f2ae Mon Sep 17 00:00:00 2001 From: Oliver Kieselbach Date: Thu, 25 Jun 2026 21:04:49 +0200 Subject: [PATCH] fix(bootstrap): correct scalar-unwrap in OOBE-relax profile age check The $profilePaths assignment wrapped only the inner subexpression in @(), so the trailing Where/Select-Object -Unique pipeline unwrapped a single result back to a scalar string. With exactly one user profile present, $profilePaths[0] then indexed into the string and returned its first char ('C'), making Get-Item fail with "Cannot find path 'C'". The OOBE age check crashed and the Windows Backup for Organizations restore case never relaxed guards 2+3. Force an outer @() so $profilePaths is always an array. Also surface the profile age and the exact failing condition in the SKIP/NOT-applied logs for diagnosing future OOBE attempts. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Install-AutopilotMonitor-dev.ps1 | 21 +++++++++----- .../Test-ShouldBootstrapAgent-dev.ps1 | 29 +++++++++++-------- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/scripts/Bootstrap/Install-AutopilotMonitor-dev.ps1 b/scripts/Bootstrap/Install-AutopilotMonitor-dev.ps1 index cde00d83..030e373f 100644 --- a/scripts/Bootstrap/Install-AutopilotMonitor-dev.ps1 +++ b/scripts/Bootstrap/Install-AutopilotMonitor-dev.ps1 @@ -143,16 +143,21 @@ try { # Collect real (non-special) user profiles as full paths (WMI Special flag + filesystem). $excludePattern = '^(defaultuser\d*|Public|Default( User)?|All Users|WDAGUtilityAccount)$' + # NOTE: the outer @() is required. Without it the trailing Where/Select pipeline + # unwraps a single result back to a scalar string, so $profilePaths[0] would index + # into the string and return its first char ('C') instead of the full path. $profilePaths = @( - try { - Get-CimInstance Win32_UserProfile -ErrorAction Stop | - Where-Object { -not $_.Special -and $_.LocalPath -like 'C:\Users\*' } | - ForEach-Object { $_.LocalPath } - } catch { Write-Log "INFO: WMI profile query failed, continuing with filesystem check." } + @( + try { + Get-CimInstance Win32_UserProfile -ErrorAction Stop | + Where-Object { -not $_.Special -and $_.LocalPath -like 'C:\Users\*' } | + ForEach-Object { $_.LocalPath } + } catch { Write-Log "INFO: WMI profile query failed, continuing with filesystem check." } - (Get-ChildItem 'C:\Users' -Directory -ErrorAction SilentlyContinue).FullName - ) | Where-Object { $_ -and (Split-Path $_ -Leaf) -notmatch $excludePattern } | - Select-Object -Unique + (Get-ChildItem 'C:\Users' -Directory -ErrorAction SilentlyContinue).FullName + ) | Where-Object { $_ -and (Split-Path $_ -Leaf) -notmatch $excludePattern } | + Select-Object -Unique + ) # OOBE-relax: the ONLY exemption to guards 2+3. Matches exactly the Windows Backup for # Organizations restore (one user signs in + restores DURING OOBE). All three must hold: diff --git a/scripts/Bootstrap/Test-ShouldBootstrapAgent-dev.ps1 b/scripts/Bootstrap/Test-ShouldBootstrapAgent-dev.ps1 index 1c71b7d7..706ff49d 100644 --- a/scripts/Bootstrap/Test-ShouldBootstrapAgent-dev.ps1 +++ b/scripts/Bootstrap/Test-ShouldBootstrapAgent-dev.ps1 @@ -78,20 +78,25 @@ if ($deployed) { $excludePattern = '^(defaultuser\d*|Public|Default( User)?|All Users|WDAGUtilityAccount)$' $wmiProfileQueryFailed = $false +# NOTE: the outer @() is required. Without it the trailing Where/Select pipeline +# unwraps a single result back to a scalar string, so $profilePaths[0] would index +# into the string and return its first char ('C') instead of the full path. $profilePaths = @( - try { - Get-CimInstance Win32_UserProfile -ErrorAction Stop | - Where-Object { -not $_.Special -and $_.LocalPath -like 'C:\Users\*' } | - ForEach-Object { $_.LocalPath } - } - catch { - $wmiProfileQueryFailed = $true - } + @( + try { + Get-CimInstance Win32_UserProfile -ErrorAction Stop | + Where-Object { -not $_.Special -and $_.LocalPath -like 'C:\Users\*' } | + ForEach-Object { $_.LocalPath } + } + catch { + $wmiProfileQueryFailed = $true + } - (Get-ChildItem 'C:\Users' -Directory -ErrorAction SilentlyContinue).FullName -) | -Where-Object { $_ -and (Split-Path $_ -Leaf) -notmatch $excludePattern } | -Select-Object -Unique + (Get-ChildItem 'C:\Users' -Directory -ErrorAction SilentlyContinue).FullName + ) | + Where-Object { $_ -and (Split-Path $_ -Leaf) -notmatch $excludePattern } | + Select-Object -Unique +) if ($wmiProfileQueryFailed) { Write-Step -Status 'WARN' -Message 'Profiles: WMI query failed, filesystem check still applied.'