docs(welcome): WinPE pre-config collector design spec (SP1)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
sysadmin
2026-06-10 08:30:48 +01:00
parent 72fa329ddd
commit 59418e37c8

View File

@@ -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).