Files
SilverMetal/windows/welcome/src/SilverOS.Welcome.Core/Apply/BootstrapService.cs
sysadmin e83ce6bcf0
All checks were successful
Build SilverMetal Enhanced - Windows ISO / build (pull_request) Successful in 5m0s
fix(kiosk): keyboard filter covers admins + taskbar auto-hide + disable sm-bootstrap in-session
Live e2e: in the sm-bootstrap session the taskbar showed and Win/Start worked.
- Keyboard Filter EXEMPTS administrators by default and sm-bootstrap is an admin, so
  Win/Start/Alt-Tab etc. were never blocked. Set WEKF_Settings
  DisableKeyboardFilterForAdministrators=false so the filter applies to it.
- Auto-hide the taskbar (default-user StuckRects3, inherited by sm-bootstrap) so it
  doesn't peek over the fullscreen wizard.
- TearDownAsync now Disable-LocalUser's sm-bootstrap in-session (immediate) so it's
  unusable at once; the deferred SYSTEM task still deletes it on next boot (SAM-confirmed
  the delete works now).

Verified: Configure-Kiosk parses under Windows PowerShell 5.1 (ASCII-clean); welcome 29/29.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 23:30:43 +01:00

64 lines
4.6 KiB
C#

namespace SilverOS.Welcome.Core.Apply;
public sealed class BootstrapService(IProcessRunner runner) : IBootstrapService
{
// Lockdown revert is BEST-EFFORT (like TearDownAsync): -EA SilentlyContinue throughout.
// Don't fail the apply over a missing WMI class / key. Must run BEFORE TearDownAsync.
public async Task RevertKioskAsync(CancellationToken ct = default)
{
// Disable the Keyboard Filter rules so the real end-user's Win key / task-switch /
// Alt+F4 etc. work again (Explorer is already the shell — nothing to undo there).
await Ps(
"$c='root\\\\standardcimv2\\\\embedded';" +
"foreach($k in @('Win','Win+L','Ctrl+Esc','Ctrl+Win+F','Win+R','Alt+Tab','Ctrl+Shift+Esc','Alt+F4')){" +
"$p=Get-CimInstance -Namespace $c -ClassName WEKF_PredefinedKey -Filter \"Id='$k'\" -EA SilentlyContinue;" +
"if($p){$p.Enabled=$false; Set-CimInstance -InputObject $p -EA SilentlyContinue}" +
"}",
ct);
// Revert escape policies set by Configure-Kiosk.ps1.
await Ps(
"$s='HKLM:\\\\SOFTWARE\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Policies\\\\System';" +
"Remove-ItemProperty $s -Name DisableTaskMgr,DisableLockWorkstation,HideFastUserSwitching -EA SilentlyContinue;" +
// Restore SECURE UAC for the real end-user (the kiosk auto-approved unsigned elevation).
"Set-ItemProperty $s -Name ConsentPromptBehaviorAdmin -Value 2 -Type DWord -EA SilentlyContinue;" +
"Set-ItemProperty $s -Name PromptOnSecureDesktop -Value 1 -Type DWord -EA SilentlyContinue",
ct);
}
// Teardown is BEST-EFFORT (unlike Account/BitLocker which are strict): the answer file's
// AutoLogon LogonCount=1 already neutralises auto-logon after the first logon (Windows clears
// AutoAdminLogon itself), so these Winlogon cleanups must not fail the whole apply. The op that
// matters — removing the sm-bootstrap account — runs regardless and is tolerant too.
public async Task TearDownAsync(string bootstrapUser, CancellationToken ct = default)
{
const string w = "'HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon'";
await Ps($"Set-ItemProperty -Path {w} -Name AutoAdminLogon -Value '0' -EA SilentlyContinue; " +
$"Remove-ItemProperty -Path {w} -Name DefaultPassword -EA SilentlyContinue; " +
$"Remove-ItemProperty -Path {w} -Name DefaultUserName -EA SilentlyContinue; " +
$"Remove-ItemProperty -Path {w} -Name DefaultDomainName -EA SilentlyContinue", ct);
var u = Esc(bootstrapUser);
// Best-effort in-session removal (usually no-ops — you can't delete the account
// you're logged in as), THEN defer the real removal to a SYSTEM startup task that
// runs on next boot, when sm-bootstrap is no longer logged on. It removes the
// account + profile, then unregisters itself.
// Disable immediately (in-session, takes effect at once so the account is unusable
// and shows as disabled), then best-effort delete; the deferred task does the real
// delete on next boot when it isn't logged on.
await Ps($"Disable-LocalUser -Name '{u}' -EA SilentlyContinue; Remove-LocalUser -Name '{u}' -EA SilentlyContinue", ct);
var cleanup =
$"Remove-LocalUser -Name '{u}' -ErrorAction SilentlyContinue; " +
$"Get-CimInstance Win32_UserProfile | Where-Object {{ $_.LocalPath -like '*\\{u}' }} | Remove-CimInstance -ErrorAction SilentlyContinue; " +
"Unregister-ScheduledTask -TaskName 'SilverMetalBootstrapCleanup' -Confirm:$false -ErrorAction SilentlyContinue";
var b64 = Convert.ToBase64String(System.Text.Encoding.Unicode.GetBytes(cleanup));
// Register-ScheduledTask (not schtasks.exe) — schtasks /tr caps at 261 chars and
// silently failed with the encoded payload, so the task was never created.
await Ps("$a=New-ScheduledTaskAction -Execute 'powershell.exe' -Argument " +
$"'-NoProfile -ExecutionPolicy Bypass -EncodedCommand {b64}'; " +
"$t=New-ScheduledTaskTrigger -AtStartup; " +
"$p=New-ScheduledTaskPrincipal -UserId 'SYSTEM' -RunLevel Highest; " +
"Register-ScheduledTask -TaskName 'SilverMetalBootstrapCleanup' -Action $a -Trigger $t -Principal $p -Force | Out-Null", ct);
}
private static string Esc(string s) => s.Replace("'", "''");
private Task Ps(string s, CancellationToken ct) =>
runner.RunAsync("powershell.exe", $"-NoProfile -ExecutionPolicy Bypass -Command \"{s}\"", ct);
}