feat: WinPE pre-config collector + simplified first-boot toolbox (SP1) #21
@@ -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).
|
||||
Reference in New Issue
Block a user