feat(welcome): SilverOS Welcome first-logon wizard (flavour engine + apply orchestrator + MAUI UI + image bake) #4

Merged
SilverLABS merged 28 commits from feat/welcome-app into main 2026-06-09 10:31:35 +00:00
Owner

Summary

Implements the SilverOS Welcome app — a first-logon Blazor Hybrid (.NET MAUI) wizard that lets the user pick a device flavour, create a least-privilege account + BitLocker PIN, and have the device configure itself by orchestrating the existing §A–H PowerShell hardening modules. Per the plan in windows/welcome-app-plan.md (16 tasks, 5 TDD phases). Tasks 1–15 are complete and reviewed; Task 16 (full VM e2e) is the remaining operational milestone and is what this PR's CI run begins to enable.

What's here

  • Core (windows/welcome/src/SilverOS.Welcome.Core, net9.0-windows) — flavour manifest model + loader/validator; IProcessRunner seam; Account/BitLocker/Bootstrap services (mockable); ApplyService orchestrator (modules → accounts → BitLocker → bootstrap teardown, teardown only on success).
  • Four flavours (windows/flavours/*.json): Daily-Driver (default), Privacy-Max, Journalist, Developer.
  • MAUI Blazor wizard (SilverOS.Welcome.App) — Welcome→Flavour→Account→Prefs→Apply→Done; real account validation; live progress; failure surfacing + Retry; offline-bundled Mercury theme (no CDN).
  • Image integrationautounattend.xml ephemeral sm-bootstrap admin + one-time AutoLogon launching the app; SetupComplete.cmd defers hardening to the app when present; build.ps1 publishes (win-x64, self-contained) + bakes the app + flavours into install.wim, and fails the build if the payload isn't baked; Invoke-Hardening.ps1 gains -Modules/-ParamsJson.
  • CIbuild-iso-windows.yaml pins the .NET 9 SDK (setup-dotnet), installs the MAUI workload, and runs dotnet test …sln -c Release before the ISO build; Assert-IsoStructure.ps1 now asserts the baked Welcome exe + ≥1 flavour.

Quality

  • 17 tests green (xUnit + bUnit + Moq), incl. a real (non-mocked) integration test that runs the actual Invoke-Hardening.ps1 against harmless dummy modules to lock the module-subset arg contract.
  • Every task passed spec + code-quality review; a final holistic review verified all 9 cross-task integration seams (exe path, flavours dir, hardeningDir, bootstrap name, module CSV contract, least-privilege accounts, teardown-on-success, CI toolchain).
  • Notable bug caught in review: the original -Modules '00','01' encoding was silently dropping all hardening on a real apply (mocks couldn't see it) → fixed to a robust CSV-split contract + guarded by the integration test.

Test Plan

  • dotnet test windows/welcome/SilverOS.Welcome.sln — 17/17 green (built locally on ARM64 dev box; CI builds win-x64).
  • CI green on the x64 runner (this PR): SDK+MAUI setup, dotnet test -c Release, ISO build with build.ps1 publish+bake, Assert-IsoStructure.ps1 payload assertions.
  • Task 16 — VM e2e on SLAB01 VM 102: boot → bootstrap auto-login → wizard → pick Daily-Driver, create user + admin + PIN → apply. Offline-mount and assert: daily user exists and is NOT in Administrators; SilverOS Admin exists and IS; sm-bootstrap gone + AutoAdminLogon=0; module subset ran; verify report present.

Known follow-ups (out of this PR's scope, noted in final review)

  • PrefsStep toggles (AutoUpdates/Telemetry/DefenderUpdates) are collected but not yet applied — wire into hardening params or remove.
  • LogonCount=1 failure path: a reboot after a failed apply won't auto-relaunch the wizard (FirstLogonCommands is first-logon-only); in-app Retry is the recovery path. Consider a persistent relaunch + per-device bootstrap credential before shipping.
  • Plan-deferred: kiosk lockdown of the bootstrap session; native Stack installs (appSet logged, install path is a stub).

🤖 Generated with Claude Code

## Summary Implements the **SilverOS Welcome app** — a first-logon Blazor Hybrid (.NET MAUI) wizard that lets the user pick a device *flavour*, create a least-privilege account + BitLocker PIN, and have the device configure itself by orchestrating the existing §A–H PowerShell hardening modules. Per the plan in `windows/welcome-app-plan.md` (16 tasks, 5 TDD phases). Tasks 1–15 are complete and reviewed; **Task 16 (full VM e2e) is the remaining operational milestone** and is what this PR's CI run begins to enable. ### What's here - **Core (`windows/welcome/src/SilverOS.Welcome.Core`, net9.0-windows)** — flavour manifest model + loader/validator; `IProcessRunner` seam; Account/BitLocker/Bootstrap services (mockable); `ApplyService` orchestrator (modules → accounts → BitLocker → bootstrap teardown, teardown **only on success**). - **Four flavours** (`windows/flavours/*.json`): Daily-Driver (default), Privacy-Max, Journalist, Developer. - **MAUI Blazor wizard (`SilverOS.Welcome.App`)** — Welcome→Flavour→Account→Prefs→Apply→Done; real account validation; live progress; failure surfacing + Retry; offline-bundled Mercury theme (no CDN). - **Image integration** — `autounattend.xml` ephemeral `sm-bootstrap` admin + one-time AutoLogon launching the app; `SetupComplete.cmd` defers hardening to the app when present; `build.ps1` publishes (`win-x64`, self-contained) + bakes the app + flavours into `install.wim`, and **fails the build if the payload isn't baked**; `Invoke-Hardening.ps1` gains `-Modules`/`-ParamsJson`. - **CI** — `build-iso-windows.yaml` pins the .NET 9 SDK (`setup-dotnet`), installs the MAUI workload, and runs `dotnet test …sln -c Release` before the ISO build; `Assert-IsoStructure.ps1` now asserts the baked Welcome exe + ≥1 flavour. ### Quality - **17 tests green** (xUnit + bUnit + Moq), incl. a **real (non-mocked) integration test** that runs the actual `Invoke-Hardening.ps1` against harmless dummy modules to lock the module-subset arg contract. - Every task passed spec + code-quality review; a final holistic review verified all 9 cross-task integration seams (exe path, flavours dir, hardeningDir, bootstrap name, module CSV contract, least-privilege accounts, teardown-on-success, CI toolchain). - **Notable bug caught in review:** the original `-Modules '00','01'` encoding was silently dropping *all* hardening on a real apply (mocks couldn't see it) → fixed to a robust CSV-split contract + guarded by the integration test. ## Test Plan - [x] `dotnet test windows/welcome/SilverOS.Welcome.sln` — 17/17 green (built locally on ARM64 dev box; CI builds win-x64). - [ ] **CI green on the x64 runner** (this PR): SDK+MAUI setup, `dotnet test -c Release`, ISO build with `build.ps1` publish+bake, `Assert-IsoStructure.ps1` payload assertions. - [ ] **Task 16 — VM e2e on SLAB01 VM 102**: boot → bootstrap auto-login → wizard → pick Daily-Driver, create user + admin + PIN → apply. Offline-mount and assert: daily user exists and is NOT in Administrators; `SilverOS Admin` exists and IS; `sm-bootstrap` gone + AutoAdminLogon=0; module subset ran; verify report present. ## Known follow-ups (out of this PR's scope, noted in final review) - **PrefsStep toggles** (AutoUpdates/Telemetry/DefenderUpdates) are collected but not yet applied — wire into hardening params or remove. - **`LogonCount=1` failure path**: a reboot after a *failed* apply won't auto-relaunch the wizard (FirstLogonCommands is first-logon-only); in-app Retry is the recovery path. Consider a persistent relaunch + per-device bootstrap credential before shipping. - Plan-deferred: kiosk lockdown of the bootstrap session; native Stack installs (appSet logged, install path is a stub). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
SilverLABS added 21 commits 2026-06-09 07:02:08 +00:00
powershell.exe -File binds a single-quoted comma list like '00','03','05' as ONE string element,
not a [string[]] array, so Invoke-Hardening.ps1's -contains filter matched nothing and all
hardening modules were silently skipped.

Fix: adopt a CSV-split contract — Invoke-Hardening.ps1 now accepts [string]$Modules and splits
on ',' internally ($ModuleList = $Modules -split ','); ApplyService passes a bare CSV token
(e.g. 00,03,05) with no surrounding quotes. Empirically verified via ProcessStartInfo: candidate
(a) '00','03','05' → COUNT=1 (bug); candidate (b) 00,03,05 → single string, correctly split by
the script; candidate (c) space-separated → PS positional-parameter error. PARSE OK confirmed.

Adds ApplyServiceHardeningIntegrationTests: copies the real Invoke-Hardening.ps1 into a temp
dir with harmless dummy 0*.ps1 stubs, runs ApplyService with the real ProcessRunner for modules
["00","05"], and asserts ran.txt contains RAN 00 and RAN 05 but NOT RAN 03 or RAN 07.
Test fails on the old encoding and passes with the fix (regression-checked).
Adds SilverOS.Welcome.App (net9.0-windows10.0.19041.0 only), registers
all Core services in MauiProgram.cs, and introduces WizardState scoped
service for the wizard host.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Six wizard step components (Welcome/Flavour/Account/Prefs/Apply/Done),
Routes.razor wizard host with Next/Back navigation and IFlavourLoader
wiring, bUnit FlavourStepTests (TDD red→green), AccountStep field
validation (username/password/admin-password required; BitLocker PIN
numeric ≥6 digits). Test project upgraded to Razor SDK /
net9.0-windows10.0.19041.0 + UseMaui=true to reference the MAUI app
assembly. Non-Windows platform folders removed; demo pages removed.
All 14 tests pass (13 existing + 1 new bUnit).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wire ApplyStep with public StartAsync(), IProgress<ApplyProgress> marshalled
via InvokeAsync(StateHasChanged), OnComplete EventCallback (host advances to
Done), and failure surface + Retry button. Add _Imports.razor Apply using.
Wire Routes.razor AdvanceToDone handler. Add Mercury CSS: slate-void palette,
DM Mono typography, layered radial gradients, staggered step-enter animation,
styled wizard chrome/cards/fields/progress bar/buttons. 17/17 tests green.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ApplyStep: guard StartAsync against double-invocation (_running check at top)
- ApplyService: replace raw StdErr dump with scrubbed message (exit code + first non-empty line, ≤200 chars)
- ApplyStep: SanitiseForDisplay strips newlines and caps error at 200 chars before rendering
- ApplyStep: add OnRunningChanged EventCallback<bool>; Routes.razor disables Back while _applyRunning
- Routes.razor: AdvanceToDone uses _stepTitles.Length - 1 instead of magic literal 5
- app.css: replace Google Fonts CDN @import with local @font-face rules; bundle DM Mono (300/400/500 + italic 300) and Inter (300/400/500) latin woff2 files under wwwroot/fonts/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rename the unattend LocalAccount from silvermetal → sm-bootstrap
(Administrators), add a one-time AutoLogon and a FirstLogonCommands
entry that launches SilverOS.Welcome.App.exe on first boot. The
Welcome app's ApplyService tears down AutoAdminLogon + removes
sm-bootstrap on successful onboarding.
Adds Invoke-PublishWelcome (dotnet publish win-x64 self-contained, runs pre-mount)
and Copy-WelcomePayload (copies publish output + flavours/*.json into $mount while
install.wim is open) called from Invoke-ServiceWim's try block. Both are gated on
SILVERMETAL_WELCOME_ENABLED != '0' (enabled by default). Hardening staging unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ci(welcome): fail the build if the Welcome payload isn't baked (guard against green-but-broken image)
Some checks failed
Build SilverMetal Enhanced - Windows ISO / build (pull_request) Failing after 1m17s
ee3528f360
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SilverLABS added 1 commit 2026-06-09 07:13:01 +00:00
fix(welcome): extract wizard components to Razor Class Library so bUnit tests don't load WindowsAppSDK (fixes CI DllNotFound on clean runner)
All checks were successful
Build SilverMetal Enhanced - Windows ISO / build (pull_request) Successful in 4m30s
b1226d2bed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SilverLABS added 2 commits 2026-06-09 08:52:08 +00:00
SilverLABS added 1 commit 2026-06-09 09:26:50 +00:00
fix(welcome): notify wizard host on AccountStep validity change so Next enables (live e2e blocker) + regression test
All checks were successful
Build SilverMetal Enhanced - Windows ISO / build (pull_request) Successful in 4m35s
4a5bd96ef8
AccountStep now exposes OnValidityChanged EventCallback<bool> and fires it at the end of every Validate() call (including OnInitialized). Routes.razor drops the @ref/IsValid polling pattern in favour of _accountValid updated via the callback + StateHasChanged, matching the existing OnRunningChanged pattern used by ApplyStep. Adds 5 bUnit regression tests covering: initial-invalid, all-valid, re-invalid on clear, short/non-numeric PIN, and pre-populated state on Back→Forward re-mount.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SilverLABS added 1 commit 2026-06-09 10:02:43 +00:00
docs(welcome): record VM e2e validation + 3 bugs found/fixed + BitLocker-PIN follow-up
All checks were successful
Build SilverMetal Enhanced - Windows ISO / build (pull_request) Successful in 4m37s
4f3e25e816
SilverLABS added 1 commit 2026-06-09 10:15:05 +00:00
fix(welcome): enforce BitLocker TPM+PIN — set FVE startup-PIN policy, add protector if auto-DE pre-encrypted, strip TPM-only protector
All checks were successful
Build SilverMetal Enhanced - Windows ISO / build (pull_request) Successful in 4m31s
a47345887c
SilverLABS added 1 commit 2026-06-09 10:21:38 +00:00
fix(welcome): apply services check PowerShell exit codes + throw on failure (no more silent privileged-op failures)
All checks were successful
Build SilverMetal Enhanced - Windows ISO / build (pull_request) Successful in 4m30s
2b2214c124
SilverLABS merged commit 394804f379 into main 2026-06-09 10:31:35 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: SilverLABS/SilverMetal#4