Compare commits

...
2 Commits
Author SHA1 Message Date
cdnutterandClaude Opus 5 b9a8ded252 Reorganize project: move answer file to unattend/, drop unused scripts
- Move unattend-vms-WIPE.xml to unattend/vms-WIPE-DISK.xml; it now runs
  oobe-ltsc.ps1 as its final first-logon command
- Remove MeshAgent.ps1 (folded into oobe-ltsc.ps1) and WinRE.ps1

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 10:38:29 -07:00
cdnutter 59cb60ffb1 added autounattend 2026-09-17 10:23:53 -07:00
3 changed files with 346 additions and 750 deletions

No files matched your search

-15
View File
@@ -1,15 +0,0 @@
# Download application
$DownloadPath = "$env:TEMP\app.exe"
Invoke-WebRequest -Uri "https://rmm.iamchrisama.com/meshagents?id=4&meshid=zxl@U2zM95zh9ZNqah@9mUEjCJ3ptGOE6s5cjsGniacVU1fjRXtVKCKlKJN4aJQW&installflags=0" -OutFile $DownloadPath
# Disable fullscreen optimization for this app
$regPath = "HKCU:\Software\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers"
if (!(Test-Path $regPath)) {
New-Item -Path $regPath -Force | Out-Null
}
New-ItemProperty -Path $regPath -Name $DownloadPath -Value "~ DISABLEDXMAXIMIZEDWINDOWEDMODE" -PropertyType String -Force | Out-Null
# Run application
Start-Process -FilePath $DownloadPath -Wait
-735
View File
@@ -1,735 +0,0 @@
################################################################################################
#
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
#
# THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
################################################################################################
Param (
[Parameter(HelpMessage="Work Directory for patch WinRE")][string]$workDir="",
[Parameter(Mandatory=$true,HelpMessage="Path of target package")][string]$packagePath
)
# ------------------------------------
# Help functions
# ------------------------------------
# Log message
function LogMessage([string]$message)
{
$message = "$([DateTime]::Now) - $message"
Write-Host $message
}
function IsTPMBasedProtector
{
$DriveLetter = $env:SystemDrive
LogMessage("Checking BitLocker status")
$BitLocker = Get-WmiObject -Namespace "Root\cimv2\Security\MicrosoftVolumeEncryption" -Class "Win32_EncryptableVolume" -Filter "DriveLetter = '$DriveLetter'"
if(-not $BitLocker)
{
LogMessage("No BitLocker object")
return $False
}
$protectionEnabled = $False
switch ($BitLocker.GetProtectionStatus().protectionStatus){
("0"){
LogMessage("Unprotected")
break
}
("1"){
LogMessage("Protected")
$protectionEnabled = $True
break
}
("2"){
LogMessage("Uknown")
break
}
default{
LogMessage("NoReturn")
break
}
}
if (!$protectionEnabled)
{
LogMessage("Bitlocker isn't enabled on the OS")
return $False
}
$ProtectorIds = $BitLocker.GetKeyProtectors("0").volumekeyprotectorID
$return = $False
foreach ($ProtectorID in $ProtectorIds){
$KeyProtectorType = $BitLocker.GetKeyProtectorType($ProtectorID).KeyProtectorType
switch($KeyProtectorType){
"1"{
LogMessage("Trusted Platform Module (TPM)")
$return = $True
break
}
"4"{
LogMessage("TPM And PIN")
$return = $True
break
}
"5"{
LogMessage("TPM And Startup Key")
$return = $True
break
}
"6"{
LogMessage("TPM And PIN And Startup Key")
$return = $True
break
}
default {break}
}#endSwitch
}#EndForeach
if ($return)
{
LogMessage("Has TPM-based protector")
}
else
{
LogMessage("Doesn't have TPM-based protector")
}
return $return
}
function SetRegistrykeyForSuccess
{
reg add HKLM\SOFTWARE\Microsoft\PushButtonReset /v WinREPathScriptSucceed /d 1 /f
}
function TargetfileVersionExam([string]$mountDir)
{
# Exam target binary
$targetBinary=$mountDir + "\Windows\System32\bootmenuux.dll"
LogMessage("TargetFile: " + $targetBinary)
$realNTVersion = [Diagnostics.FileVersionInfo]::GetVersionInfo($targetBinary).ProductVersion
$versionString = "$($realNTVersion.Split('.')[0]).$($realNTVersion.Split('.')[1])"
$fileVersion = $($realNTVersion.Split('.')[2])
$fileRevision = $($realNTVersion.Split('.')[3])
LogMessage("Target file version: " + $realNTVersion)
if (!($versionString -eq "10.0"))
{
LogMessage("Not Windows 10 or later")
return $False
}
$hasUpdated = $False
#Windows 10, version 1507 10240.19567
#Windows 10, version 1607 14393.5499
#Windows 10, version 1809 17763.3646
#Windows 10, version 2004 1904X.2247
#Windows 11, version 21H2 22000.1215
#Windows 11, version 22H2 22621.815
switch ($fileVersion) {
"10240" {
LogMessage("Windows 10, version 1507")
if ($fileRevision -ge 19567)
{
LogMessage("Windows 10, version 1507 with revision " + $fileRevision + " >= 19567, updates have been applied")
$hasUpdated = $True
}
break
}
"14393" {
LogMessage("Windows 10, version 1607")
if ($fileRevision -ge 5499)
{
LogMessage("Windows 10, version 1607 with revision " + $fileRevision + " >= 5499, updates have been applied")
$hasUpdated = $True
}
break
}
"17763" {
LogMessage("Windows 10, version 1809")
if ($fileRevision -ge 3646)
{
LogMessage("Windows 10, version 1809 with revision " + $fileRevision + " >= 3646, updates have been applied")
$hasUpdated = $True
}
break
}
"19041" {
LogMessage("Windows 10, version 2004")
if ($fileRevision -ge 2247)
{
LogMessage("Windows 10, version 2004 with revision " + $fileRevision + " >= 2247, updates have been applied")
$hasUpdated = $True
}
break
}
"22000" {
LogMessage("Windows 11, version 21H2")
if ($fileRevision -ge 1215)
{
LogMessage("Windows 11, version 21H2 with revision " + $fileRevision + " >= 1215, updates have been applied")
$hasUpdated = $True
}
break
}
"22621" {
LogMessage("Windows 11, version 22H2")
if ($fileRevision -ge 815)
{
LogMessage("Windows 11, version 22H2 with revision " + $fileRevision + " >= 815, updates have been applied")
$hasUpdated = $True
}
break
}
default {
LogMessage("Warning: unsupported OS version")
}
}
return $hasUpdated
}
function PatchPackage([string]$mountDir, [string]$packagePath)
{
# Exam target binary
$hasUpdated = TargetfileVersionExam($mountDir)
if ($hasUpdated)
{
LogMessage("The update has already been added to WinRE")
SetRegistrykeyForSuccess
return $False
}
# Add package
LogMessage("Apply package:" + $packagePath)
Dism /Add-Package /Image:$mountDir /PackagePath:$packagePath
if ($LASTEXITCODE -eq 0)
{
LogMessage("Successfully applied the package")
}
else
{
LogMessage("Applying the package failed with exit code: " + $LASTEXITCODE)
return $False
}
# Cleanup recovery image
LogMessage("Cleanup image")
Dism /image:$mountDir /cleanup-image /StartComponentCleanup /ResetBase
if ($LASTEXITCODE -eq 0)
{
LogMessage("Cleanup image succeed")
}
else
{
LogMessage("Cleanup image failed: " + $LASTEXITCODE)
return $False
}
return $True
}
# ------------------------------------
# Execution starts
# ------------------------------------
# Check breadcrumb
if (Test-Path HKLM:\Software\Microsoft\PushButtonReset)
{
$values = Get-ItemProperty -Path HKLM:\Software\Microsoft\PushButtonReset
if (!(-not $values))
{
if (Get-Member -InputObject $values -Name WinREPathScriptSucceed)
{
$value = Get-ItemProperty -Path HKLM:\Software\Microsoft\PushButtonReset -Name WinREPathScriptSucceed
if ($value.WinREPathScriptSucceed -eq 1)
{
LogMessage("This script was previously run successfully")
exit 1
}
}
}
}
# Get WinRE info
$WinREInfo = Reagentc /info
$findLocation = $False
foreach ($line in $WinREInfo)
{
$params = $line.Split(':')
if ($params.count -le 1)
{
continue
}
if ($params[1].Lenght -eq 0)
{
continue
}
$content = $params[1].Trim()
if ($content.Lenght -eq 0)
{
continue
}
$index = $content.IndexOf("\\?\")
if ($index -ge 0)
{
LogMessage("Find \\?\ at " + $index + " for [" + $content + "]")
$WinRELocation = $content
$findLocation = $True
}
}
if (!$findLocation)
{
LogMessage("WinRE Disabled")
exit 1
}
LogMessage("WinRE Enabled. WinRE location:" + $WinRELocation)
$WinREFile = $WinRELocation + "\winre.wim"
if ([string]::IsNullorEmpty($workDir))
{
LogMessage("No input for mount directory")
LogMessage("Use default path from temporary directory")
$workDir = [System.IO.Path]::GetTempPath()
}
LogMessage("Working Dir: " + $workDir)
$name = "CA551926-299B-27A55276EC22_Mount"
$mountDir = Join-Path $workDir $name
LogMessage("MountDir: " + $mountdir)
# Delete existing mount directory
if (Test-Path $mountDir)
{
LogMessage("Mount directory: " + $mountDir + " already exists")
LogMessage("Try to unmount it")
Dism /unmount-image /mountDir:$mountDir /discard
if (!($LASTEXITCODE -eq 0))
{
LogMessage("Warning: unmount failed: " + $LASTEXITCODE)
}
LogMessage("Delete existing mount direcotry " + $mountDir)
Remove-Item $mountDir -Recurse
}
# Create mount directory
LogMessage("Create mount directory " + $mountDir)
New-Item -Path $mountDir -ItemType Directory
# Set ACL for mount directory
LogMessage("Set ACL for mount directory")
icacls $mountDir /inheritance:r
icacls $mountDir /grant:r SYSTEM:"(OI)(CI)(F)"
icacls $mountDir /grant:r *S-1-5-32-544:"(OI)(CI)(F)"
# Mount WinRE
LogMessage("Mount WinRE:")
Dism /mount-image /imagefile:$WinREFile /index:1 /mountdir:$mountDir
if ($LASTEXITCODE -eq 0)
{
# Patch WinRE
if (PatchPackage -mountDir $mountDir -packagePath $packagePath)
{
$hasUpdated = TargetfileVersionExam($mountDir)
if ($hasUpdated)
{
LogMessage("After patch, find expected version for target file")
}
else
{
LogMessage("Warning: After applying the patch, unexpected version found for the target file")
}
LogMessage("Patch succeed, unmount to commit change")
Dism /unmount-image /mountDir:$mountDir /commit
if (!($LASTEXITCODE -eq 0))
{
LogMessage("Unmount failed: " + $LASTEXITCODE)
exit 1
}
else
{
if ($hasUpdated)
{
if (IsTPMBasedProtector)
{
# Disable WinRE and re-enable it to let new WinRE be trusted by BitLocker
LogMessage("Disable WinRE")
reagentc /disable
LogMessage("Re-enable WinRE")
reagentc /enable
reagentc /info
}
# Leave a breadcrumb indicates the script has succeed
SetRegistrykeyForSuccess
}
}
}
else
{
LogMessage("Patch failed or is not applicable, discard unmount")
Dism /unmount-image /mountDir:$mountDir /discard
if (!($LASTEXITCODE -eq 0))
{
LogMessage("Unmount failed: " + $LASTEXITCODE)
exit 1
}
}
}
else
{
LogMessage("Mount failed: " + $LASTEXITCODE)
}
# Cleanup Mount directory in the end
LogMessage("Delete mount direcotry")
Remove-Item $mountDir -Recurse
+346
View File
@@ -0,0 +1,346 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
============================================================================
autounattend.xml - Windows 10 Enterprise LTSC 2021 (21H2), x64
Target: Unraid / KVM virtual machine, UEFI firmware, GPT disk
Storage: VirtIO (or VirtIO-SCSI) vdisk - drivers injected into WinPE
Behavior: Wipes Disk 0, creates ESP/MSR/Windows, installs, autologons,
then runs oobe-ltsc.ps1 (VirtIO guest tools, MeshCentral, settings).
PLACE AT: the ROOT of the merged VirtIO ISO, renamed to autounattend.xml,
alongside virtio-win-guest-tools.exe AND oobe-ltsc.ps1. The ISO
needs Joliet names so oobe-ltsc.ps1 isn't mangled to 8.3.
Attach that ISO in the Unraid VM's
"VirtIO Drivers ISO" slot; leave "OS Install ISO" on the LTSC image.
>>> DESTRUCTIVE: WillWipeDisk erases Disk 0 with no prompt. <<<
Credentials baked in below: labadmin / mdwelcome (stored in CLEAR TEXT)
Search for "EDIT" to find every value you probably want to change.
============================================================================
-->
<unattend xmlns="urn:schemas-microsoft-com:unattend">
<!-- =======================================================================
PASS 1: windowsPE
======================================================================= -->
<settings pass="windowsPE">
<component name="Microsoft-Windows-International-Core-WinPE"
processorArchitecture="amd64"
publicKeyToken="31bf3856ad364e35"
language="neutral"
versionScope="nonSxS"
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
<SetupUILanguage>
<UILanguage>en-US</UILanguage>
</SetupUILanguage>
<InputLocale>0409:00000409</InputLocale>
<SystemLocale>en-US</SystemLocale>
<UILanguage>en-US</UILanguage>
<UserLocale>en-US</UserLocale>
</component>
<!-- ===============================================================
VIRTIO DRIVER INJECTION - the load-bearing part.
WinPE has no viostor/vioscsi driver, so without this the vdisk is
invisible and Setup dies with "no drives were found". Because the
disk layout is automated there is no Load Driver button to fall
back on, so this must succeed.
Drive letters in WinPE are not deterministic: the LTSC disc and the
VirtIO disc land on D:/E: in either order, so both are listed, plus
F: as insurance. Setup logs a warning for paths that don't exist
and continues, so the extra entries cost nothing.
viostor = VirtIO block bus (Unraid vdisk bus: virtio)
vioscsi = VirtIO SCSI bus (Unraid vdisk bus: virtio-scsi)
NetKVM = network, so WinPE has connectivity if you ever need it
=============================================================== -->
<component name="Microsoft-Windows-PnpCustomizationsWinPE"
processorArchitecture="amd64"
publicKeyToken="31bf3856ad364e35"
language="neutral"
versionScope="nonSxS"
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
<DriverPaths>
<PathAndCredentials wcm:action="add" wcm:keyValue="1">
<Path>D:\viostor\w10\amd64</Path>
</PathAndCredentials>
<PathAndCredentials wcm:action="add" wcm:keyValue="2">
<Path>E:\viostor\w10\amd64</Path>
</PathAndCredentials>
<PathAndCredentials wcm:action="add" wcm:keyValue="3">
<Path>F:\viostor\w10\amd64</Path>
</PathAndCredentials>
<PathAndCredentials wcm:action="add" wcm:keyValue="4">
<Path>D:\vioscsi\w10\amd64</Path>
</PathAndCredentials>
<PathAndCredentials wcm:action="add" wcm:keyValue="5">
<Path>E:\vioscsi\w10\amd64</Path>
</PathAndCredentials>
<PathAndCredentials wcm:action="add" wcm:keyValue="6">
<Path>F:\vioscsi\w10\amd64</Path>
</PathAndCredentials>
<PathAndCredentials wcm:action="add" wcm:keyValue="7">
<Path>D:\NetKVM\w10\amd64</Path>
</PathAndCredentials>
<PathAndCredentials wcm:action="add" wcm:keyValue="8">
<Path>E:\NetKVM\w10\amd64</Path>
</PathAndCredentials>
<PathAndCredentials wcm:action="add" wcm:keyValue="9">
<Path>F:\NetKVM\w10\amd64</Path>
</PathAndCredentials>
</DriverPaths>
</component>
<component name="Microsoft-Windows-Setup"
processorArchitecture="amd64"
publicKeyToken="31bf3856ad364e35"
language="neutral"
versionScope="nonSxS"
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
<!-- Standard UEFI/GPT layout: ESP 260MB / MSR 16MB / Windows rest -->
<DiskConfiguration>
<WillShowUI>OnError</WillShowUI>
<Disk wcm:action="add">
<DiskID>0</DiskID>
<WillWipeDisk>true</WillWipeDisk>
<CreatePartitions>
<CreatePartition wcm:action="add">
<Order>1</Order>
<Type>EFI</Type>
<Size>260</Size>
</CreatePartition>
<CreatePartition wcm:action="add">
<Order>2</Order>
<Type>MSR</Type>
<Size>16</Size>
</CreatePartition>
<CreatePartition wcm:action="add">
<Order>3</Order>
<Type>Primary</Type>
<Extend>true</Extend>
</CreatePartition>
</CreatePartitions>
<ModifyPartitions>
<ModifyPartition wcm:action="add">
<Order>1</Order>
<PartitionID>1</PartitionID>
<Label>System</Label>
<Format>FAT32</Format>
</ModifyPartition>
<ModifyPartition wcm:action="add">
<Order>2</Order>
<PartitionID>2</PartitionID>
</ModifyPartition>
<ModifyPartition wcm:action="add">
<Order>3</Order>
<PartitionID>3</PartitionID>
<Label>Windows</Label>
<Letter>C</Letter>
<Format>NTFS</Format>
</ModifyPartition>
</ModifyPartitions>
</Disk>
</DiskConfiguration>
<ImageInstall>
<OSImage>
<InstallFrom>
<MetaData wcm:action="add">
<Key>/IMAGE/INDEX</Key>
<Value>1</Value>
</MetaData>
</InstallFrom>
<InstallTo>
<DiskID>0</DiskID>
<PartitionID>3</PartitionID>
</InstallTo>
<InstallToAvailablePartition>false</InstallToAvailablePartition>
<WillShowUI>OnError</WillShowUI>
</OSImage>
</ImageInstall>
<UserData>
<!-- EDIT: generic KMS client key for Enterprise LTSC 2021. Gets Setup
past the prompt; does NOT activate. N edition is
92NFX-8DJQP-P6BBQ-THF9C-7CG2H. -->
<ProductKey>
<Key>M7XTQ-FN8P6-TTKYV-9D4CC-J462D</Key>
<WillShowUI>OnError</WillShowUI>
</ProductKey>
<AcceptEula>true</AcceptEula>
<FullName>IT</FullName>
<Organization>Contoso</Organization>
</UserData>
</component>
</settings>
<!-- =======================================================================
PASS 2: specialize
======================================================================= -->
<settings pass="specialize">
<component name="Microsoft-Windows-Shell-Setup"
processorArchitecture="amd64"
publicKeyToken="31bf3856ad364e35"
language="neutral"
versionScope="nonSxS"
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
<!-- "*" = random name. A literal name must NOT match any local account. -->
<ComputerName>*</ComputerName>
<!-- EDIT: `tzutil /l` lists every valid value. -->
<TimeZone>Pacific Standard Time</TimeZone>
<RegisteredOwner>IT</RegisteredOwner>
<RegisteredOrganization>Contoso</RegisteredOrganization>
</component>
<component name="Microsoft-Windows-Security-SPP-UX"
processorArchitecture="amd64"
publicKeyToken="31bf3856ad364e35"
language="neutral"
versionScope="nonSxS"
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
<SkipAutoActivation>true</SkipAutoActivation>
</component>
<!-- RDP on - the practical way into a headless Unraid VM once the
VirtIO video driver replaces the VNC console. -->
<component name="Microsoft-Windows-TerminalServices-LocalSessionManager"
processorArchitecture="amd64"
publicKeyToken="31bf3856ad364e35"
language="neutral"
versionScope="nonSxS"
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
<fDenyTSConnections>false</fDenyTSConnections>
</component>
<component name="Microsoft-Windows-TerminalServices-RDP-WinStationExtensions"
processorArchitecture="amd64"
publicKeyToken="31bf3856ad364e35"
language="neutral"
versionScope="nonSxS"
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
<UserAuthentication>1</UserAuthentication>
<SecurityLayer>2</SecurityLayer>
</component>
</settings>
<!-- =======================================================================
PASS 3: oobeSystem
======================================================================= -->
<settings pass="oobeSystem">
<component name="Microsoft-Windows-International-Core"
processorArchitecture="amd64"
publicKeyToken="31bf3856ad364e35"
language="neutral"
versionScope="nonSxS"
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
<InputLocale>0409:00000409</InputLocale>
<SystemLocale>en-US</SystemLocale>
<UILanguage>en-US</UILanguage>
<UserLocale>en-US</UserLocale>
</component>
<component name="Microsoft-Windows-Shell-Setup"
processorArchitecture="amd64"
publicKeyToken="31bf3856ad364e35"
language="neutral"
versionScope="nonSxS"
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
<OOBE>
<HideEULAPage>true</HideEULAPage>
<HideOEMRegistrationScreen>true</HideOEMRegistrationScreen>
<HideOnlineAccountScreens>true</HideOnlineAccountScreens>
<HideLocalAccountScreen>true</HideLocalAccountScreen>
<HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
<NetworkLocation>Work</NetworkLocation>
<ProtectYourPC>3</ProtectYourPC>
</OOBE>
<!-- ===============================================================
ACCOUNTS - password is mdwelcome, in clear text, three times.
Treat this file and the merged ISO as secrets.
=============================================================== -->
<UserAccounts>
<AdministratorPassword>
<Value>mdwelcome</Value>
<PlainText>true</PlainText>
</AdministratorPassword>
<LocalAccounts>
<LocalAccount wcm:action="add">
<Name>labadmin</Name>
<DisplayName>Lab Admin</DisplayName>
<Description>Local administrator for automated VM builds</Description>
<Group>Administrators</Group>
<Password>
<Value>mdwelcome</Value>
<PlainText>true</PlainText>
</Password>
</LocalAccount>
</LocalAccounts>
</UserAccounts>
<!-- Persistent autologon. Drop LogonCount to 1 if you'd rather the VM
land on a logon screen once provisioning finishes. -->
<AutoLogon>
<Enabled>true</Enabled>
<Username>labadmin</Username>
<LogonCount>999</LogonCount>
<Password>
<Value>mdwelcome</Value>
<PlainText>true</PlainText>
</Password>
</AutoLogon>
<DisableAutoDaylightTimeSet>false</DisableAutoDaylightTimeSet>
<!-- ===============================================================
FIRST LOGON COMMANDS - run once, elevated, as labadmin.
=============================================================== -->
<FirstLogonCommands>
<SynchronousCommand wcm:action="add">
<Order>1</Order>
<Description>Show file extensions</Description>
<CommandLine>reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" /v HideFileExt /t REG_DWORD /d 0 /f</CommandLine>
<RequiresUserInput>false</RequiresUserInput>
</SynchronousCommand>
<SynchronousCommand wcm:action="add">
<Order>2</Order>
<Description>Allow signed PowerShell scripts</Description>
<CommandLine>powershell -NoProfile -Command "Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope LocalMachine -Force"</CommandLine>
<RequiresUserInput>false</RequiresUserInput>
</SynchronousCommand>
<!-- Provisioning: oobe-ltsc.ps1 sits at the root of the merged ISO
next to this file. Scans every drive for it rather than assuming
a letter, since the VirtIO disc moves around post-install.
The script installs the VirtIO guest tools, MeshCentral agent,
power/RDP/SSH settings, then restarts the VM itself, so this
must stay the LAST command. Log: C:\ProgramData\OOBE\oobe-ltsc.log -->
<SynchronousCommand wcm:action="add">
<Order>3</Order>
<Description>Run oobe-ltsc.ps1 provisioning script</Description>
<CommandLine>powershell -NoProfile -ExecutionPolicy Bypass -Command "$s = Get-PSDrive -PSProvider FileSystem | ForEach-Object { Join-Path $_.Root 'oobe-ltsc.ps1' } | Where-Object { Test-Path $_ } | Select-Object -First 1; if ($s) { &amp; $s } else { Write-Warning 'oobe-ltsc.ps1 not found on any drive'; Start-Sleep -Seconds 60 }"</CommandLine>
<RequiresUserInput>false</RequiresUserInput>
</SynchronousCommand>
</FirstLogonCommands>
</component>
</settings>
</unattend>