From 59418e37c8d29c58c60f1a92c5a9efaa48604625 Mon Sep 17 00:00:00 2001 From: sysadmin Date: Wed, 10 Jun 2026 08:30:48 +0100 Subject: [PATCH] docs(welcome): WinPE pre-config collector design spec (SP1) Co-Authored-By: Claude Opus 4.8 --- ...-06-10-winpe-preconfig-collector-design.md | 223 ++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 windows/docs/superpowers/specs/2026-06-10-winpe-preconfig-collector-design.md diff --git a/windows/docs/superpowers/specs/2026-06-10-winpe-preconfig-collector-design.md b/windows/docs/superpowers/specs/2026-06-10-winpe-preconfig-collector-design.md new file mode 100644 index 0000000..fd1775a --- /dev/null +++ b/windows/docs/superpowers/specs/2026-06-10-winpe-preconfig-collector-design.md @@ -0,0 +1,223 @@ +# SilverMetal WinPE Pre-Config Collector + +> **Status**: design — 2026-06-10. Approved in brainstorming. Adds a branded WinPE +> collector that runs *before* Windows Setup, captures identity + install-shaping +> choices, generates the answer file so Setup creates the real account natively +> (eliminating the `sm-bootstrap` account + its teardown), and hands the rest to a +> simplified run-once-then-persist first-boot toolbox (the existing MAUI Welcome app, +> trimmed). Builds on `windows/installer/build.ps1`, `windows/installer/autounattend/`, +> and `windows/welcome/` (the MAUI wizard). + +## 1. Goal + +Let the user pre-configure the installation **before** the unattended installer runs. +A WinPE collector gathers **identity + install-shaping** choices, writes them into a +generated answer file (native account, computer name, locale, auto-logon) plus a +carried-forward config file (flavour, BitLocker PIN, app defaults). Windows Setup then +creates the real local administrator itself — so the first-boot app no longer creates +an account or removes a bootstrap user, and becomes a **config/toolbox**: it applies the +remaining post-install config once (apps, BitLocker), shows the recovery key, then +persists as a launchable tool. + +## 2. Decisions (locked in brainstorming) + +- **Collector scope = identity + install shaping.** It owns: the user **account** + (display name, username, password), **computer name**, **locale/keyboard**, + **flavour**, and the **BitLocker** choice + PIN. The toolbox owns **apps and ongoing + config**; hardening runs headless via `SetupComplete.cmd` (see the last bullet). +- **Collector UI = PowerShell + WinForms.** WinPE cannot run the MAUI/WebView2 app (no + Edge/Chromium, no modern .NET), so the collector is a separate, branded full-screen + WinForms form. `WinPE-NetFx` + `WinPE-PowerShell` (and dependencies) are added to + `boot.wim`. +- **Single real admin account.** One local **Administrators** account that the user + defines — replacing both the ephemeral `sm-bootstrap` *and* today's "daily user + + SilverOS Admin" split. (The two-account split can return later as a flavour option; + out of scope here.) +- **Toolbox = run-once apply, then persist.** On first logon (as the real user) it + auto-runs once, applies the collected config (apps, BitLocker PIN, flavour config), + shows progress + the BitLocker recovery key, reaches Done — then stays installed as a + launchable "SilverMetal" app. The heavy kiosk lockdown (Shell Launcher / Keyboard + Filter / `sm-bootstrap` teardown) is dropped. +- **Handoff = generated answer file + embedded config (Approach 1).** Setup keeps owning + the disk. The collector generates the answer file and embeds the non-OS config as + base64 that a `specialize`-pass command writes to `C:` once it exists. +- **Hardening is canonical in SetupComplete.** Hardening runs headless from + `SetupComplete.cmd` as SYSTEM (as the answer file already intends). The toolbox Apply + focuses on **apps + BitLocker** — no duplicate hardening — so hardening is guaranteed + even if the toolbox is closed. (A read-only hardening-status view in the toolbox is SP2.) + +## 3. Architecture / flow + +``` +Boot ISO -> WinPE (boot.wim) + winpeshl.ini -> SilverMetal Collector (PowerShell + WinForms) + collects: account . computer name . locale . flavour . BitLocker PIN + -> New-SmAnswerFile -> X:\sm\unattend.generated.xml (embeds preconfig.json base64) + -> setup.exe /unattend:X:\sm\unattend.generated.xml +Windows Setup: + windowsPE pass : wipe disk0 (WillWipeDisk) -> install LTSC index 1 -> locale + specialize pass: decode embedded preconfig.json -> C:\ProgramData\SilverMetal\preconfig.json + oobeSystem pass: create REAL local admin -> AutoLogon(once) -> ComputerName + SetupComplete.cmd (SYSTEM): hardening (canonical) + scrub C:\Windows\Panther\unattend.xml +First logon (real user, auto, once): + Toolbox run-once -> read preconfig -> install apps + enrol BitLocker + -> show recovery key -> Done -> clear PIN -> set "configured" marker + Subsequent launches -> toolbox-home mode +``` + +## 4. Components + +### 4a. WinPE collector (`windows/collector/`) +- **Launch**: `boot.wim` `winpeshl.ini` runs `X:\sm\Start-Collector.cmd` (instead of the + default Setup shell), which calls `powershell -ExecutionPolicy Bypass -File X:\sm\Collector.ps1`. +- **UI** (`Collector.ps1`): a branded full-screen WinForms form (dark theme + SilverMetal + logo) with fields: + - Account: display name, username, password + confirm. + - Computer name. + - Locale / keyboard (defaults to today's en-GB / `0809:00000809`). + - Flavour (the existing flavours, read from a small bundled list). + - BitLocker: enable toggle + PIN + confirm (or skip). + - A **"Use defaults"** fast path that fills sensible defaults so a user can click through. +- **Validation** (`Test-SmInput.ps1`, separated from the WinForms shell so it is + unit-testable headless): username rules (non-empty, valid local-account chars, not a + reserved name), password present + confirm-match (+ minimum length), PIN numeric + + length (>= 6) + confirm-match when BitLocker enabled, computer-name rules + (<= 15 chars, valid NetBIOS charset). +- **Output**: calls `New-SmAnswerFile` then launches Setup with the generated file. + +### 4b. Answer-file generator (`windows/collector/New-SmAnswerFile.ps1`) +Pure function: collected values (a hashtable) -> answer XML string. Mirrors the current +`autounattend.xml` structure with these differences: +- **windowsPE**: keep `DiskConfiguration` (wipe disk 0, EFI/MSR/Primary) + `ImageInstall` + index 1; locale from the collector. +- **oobeSystem**: emit **one** `LocalAccount` in `Administrators` (the user's + account); `AutoLogon` Enabled with `LogonCount=1` as that user; `ComputerName`; + `Microsoft-Windows-International-Core` locale; the existing `OOBE` hide-pages block. + **No `sm-bootstrap`.** +- **specialize**: a `RunSynchronousCommand` that base64-decodes the embedded + `preconfig.json` to `C:\ProgramData\SilverMetal\preconfig.json` (creating the dir). +- **FirstLogonCommands**: launch the toolbox elevated (as today, but as the real user). +- The **scrub** runs from `SetupComplete.cmd` (SYSTEM, end of Setup — guaranteed after + account creation): it deletes `C:\Windows\Panther\unattend.xml` and the cached answer + copy. + +### 4c. `preconfig.json` contract +Written to `C:\ProgramData\SilverMetal\preconfig.json`: +```json +{ + "schemaVersion": 1, + "flavour": "developer", + "bitlocker": { "enable": true, "pin": "246810" }, + "apps": { "useFlavourDefaults": true } +} +``` +- `flavour`: the chosen flavour id (drives the toolbox's app defaults + flavour config). +- `bitlocker.pin`: consumed by the toolbox to enrol TPM+PIN, then **the field is cleared** + (rewritten without `pin`) once enrolment succeeds. +- `apps`: `useFlavourDefaults:true` (toolbox pre-checks the flavour defaults) or an + explicit `selected: ["id", ...]` list (future: collector app picking — not in SP1; SP1 + always emits `useFlavourDefaults:true`). + +### 4d. Simplified first-boot toolbox (`windows/welcome/`, trimmed) +- **Remove**: the Account step + `AccountService` account creation; `BootstrapService` + `sm-bootstrap` teardown; the heavy kiosk lockdown (`Configure-Kiosk.ps1` Keyboard + Filter / DisableTaskMgr / Shell Launcher path). Branding stays (baked offline; online + re-apply unaffected). +- **Add**: a `PreconfigLoader` that reads `C:\ProgramData\SilverMetal\preconfig.json` and + pre-seeds `WizardState` (flavour, app selection = flavour defaults, BitLocker PIN). On + **first run** (no "configured" marker) the toolbox auto-applies: apps install + + BitLocker enrol (using the preconfig PIN) + recovery-key display + Done; then it clears + the PIN from preconfig and writes a `configured` marker + (`C:\ProgramData\SilverMetal\configured`). +- **Apply pipeline** (trimmed): `apps -> bitlocker -> done`. No account step, no + hardening (SetupComplete owns it), no teardown. Idempotent / re-runnable. +- **Persist**: the app stays installed with a Start-menu shortcut "SilverMetal". + Subsequent launches (marker present) open **toolbox-home** (a simple landing page; rich + ongoing-config surfaces are SP2). +- **Run mode selection**: marker present -> toolbox-home; marker absent + preconfig + present -> first-run auto-apply; neither -> toolbox-home with flavour defaults + (fail-open). + +### 4e. `build.ps1` changes +- **Stage 2b (boot.wim servicing)**: in addition to forcing legacy Setup, add the WinPE + optional components (`WinPE-NetFx`, `WinPE-PowerShell`, and their deps `WinPE-WMI`, + `WinPE-Scripting`) via `Add-WindowsPackage`; copy `windows/collector/*` to the mounted + boot.wim at `\sm\`; write `winpeshl.ini` to launch `\sm\Start-Collector.cmd`. +- **Fallback**: keep the static `autounattend.xml` on the ISO as the **default** answer + file. The collector uses it as its template and as the fallback if cancelled. An env/marker + `SM_UNATTENDED=1` (or a build flag) makes `Start-Collector.cmd` skip the UI and launch + Setup with the static default — so **CI ISO builds remain non-interactive**. + +## 5. Data flow / handoff + +Single source of truth = the collector's captured values. From them: +- **OS-native fields** (account, computer name, locale, auto-logon) -> the generated + answer file -> applied by Setup. +- **Non-OS config** (flavour, BitLocker PIN, app defaults) -> base64-embedded in the + answer file -> written to `C:\ProgramData\SilverMetal\preconfig.json` in the + `specialize` pass -> read by the toolbox. + +No extra partition; Setup still owns disk partitioning (kept simplest + safest). + +## 6. Error handling (must never brick boot) + +- **Collector cancelled / crashes** -> launch Setup with the bundled **default** answer + file (sensible defaults) so install still proceeds. The collector wraps its UI in + try/catch and always has a path to Setup. +- **Generated XML fails validation** (schema/parse check before launch) -> use the + default answer file. +- **`preconfig.json` missing / corrupt** at first boot -> toolbox proceeds with + **flavour defaults** (fail-open, mirroring the app-catalog loader). +- **winget unavailable** -> already handled (graceful skip; PR #19). +- **BitLocker enrol fails** -> toolbox surfaces it but still reaches Done (apps/onboarding + not blocked); PIN retained in preconfig only on failure so a re-run can retry. + +## 7. Security + +- **Local account only** (no Microsoft account / no cloud key escrow) — unchanged. +- **Account password** transits the generated answer file briefly; `SetupComplete.cmd` + **scrubs `C:\Windows\Panther\unattend.xml`** (and the cached copy) after account + creation. +- **BitLocker PIN** transits `preconfig.json`; the toolbox **clears it after enrolment**. +- **Residual (documented):** plaintext secrets exist transiently in WinPE memory and on + the freshly-formatted disk until the scrub/clear runs. Acceptable for a local, + operator-present install; a future hardening pass could DPAPI-wrap the PIN or prompt it + interactively at first logon instead. + +## 8. Testing + +- **Pester — `New-SmAnswerFile`**: given inputs, the XML contains the real + `LocalAccount` in Administrators, `AutoLogon` once as that user, the `ComputerName`, + the locale, the `specialize` base64-write command, and the Panther scrub; and contains + **no `sm-bootstrap`**. Base64 round-trips back to the original `preconfig.json`. +- **Pester — `Test-SmInput`**: username/password/PIN/computer-name validation rules + (valid + each invalid case). +- **xUnit — toolbox `PreconfigLoader`**: parses a sample, fails open to flavour defaults + on missing/corrupt, clears the PIN after a simulated enrol, honours the `configured` + marker for run-mode selection. +- **`Assert-IsoStructure.ps1`**: boot.wim contains `winpeshl.ini` + `\sm\Collector.ps1`; + WinPE-NetFx present (e.g. `Get-WindowsPackage` shows the package or a marker file); the + toolbox payload no longer references `sm-bootstrap`. +- **VM e2e**: collector form renders in WinPE -> fill in account/flavour/PIN -> install -> + first logon auto-applies (apps skipped if no network, BitLocker per env) -> recovery key + shown -> Done -> relaunch shows toolbox-home. + +## 9. Scope / phasing + +- **SP1 (this spec):** WinPE collector (account, flavour, computer name, locale, BitLocker + PIN) + answer-file generation + `preconfig.json` handoff + simplified run-once-then-persist + toolbox; remove `sm-bootstrap` + account step + heavy kiosk; hardening canonical in + SetupComplete. +- **SP2 (later):** rich toolbox-home — ongoing config surfaces (re-run/extend apps, change + settings, view hardening status). +- **SP3 (later):** install-time disk target / partitioning + BitLocker pre-provisioning on + the blank drive (the deferred "maximal" collector scope). + +## 10. Out of scope (SP1) + +- App selection inside the collector (SP1 emits `useFlavourDefaults:true`; the toolbox + remains the place to pick apps). +- The two-account (daily + admin) split — SP1 is single admin. +- Disk target / partition UI; BitLocker pre-provision in WinPE. +- DPAPI-wrapping or interactive-at-first-logon secret handling (noted as a future + hardening option in §7).