224 lines
13 KiB
Markdown
224 lines
13 KiB
Markdown
# 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).
|