docs(welcome): role app-recipes design spec
Per-role app-install picker for the Welcome wizard: catalog.json + AppsStep + winget install engine (phased, swappable source for a future curated mirror). Stack stays auto-installed; picker adds role apps + privacy-trimmed essentials. Approved in brainstorming. Next: writing-plans. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
# SilverOS Welcome — Role App Recipes
|
||||
|
||||
> **Status**: design — 2026-06-09. Approved in brainstorming. Adds a per-role app-install
|
||||
> picker to the first-logon Welcome wizard. Builds on the wizard in
|
||||
> `windows/welcome/` and the flavour model in `windows/flavours/`.
|
||||
|
||||
## 1. Goal
|
||||
|
||||
Make the wizard's role/flavour selection *do something*: after choosing a role, the user
|
||||
sees a grouped, pre-checked list of apps to install during onboarding — role-relevant tools
|
||||
plus privacy-trimmed essentials — and the chosen apps are installed as part of Apply.
|
||||
|
||||
## 2. Decisions (locked in brainstorming)
|
||||
|
||||
- **Install engine: winget, phased.** v1 uses winget (`winget install --id <id> --silent`).
|
||||
IoT Enterprise LTSC ships *without* winget, so the engine bootstraps the App Installer at
|
||||
apply time. Every catalog app has a winget id. The catalog's per-app `source` object is
|
||||
**swappable** to a curated SilverMetal mirror later (the air-gap/privacy end-state) without
|
||||
changing the UI or engine contract.
|
||||
- **Stack stays auto-installed.** The SilverLABS Stack (`SilverBrowser`=ungoogled-chromium
|
||||
rebrand, `SilverVPN`, `SilverKeys`, …) continues to install automatically via each flavour's
|
||||
`appSet` and is **not** in the picker (shown as "included"). The picker adds **role apps**
|
||||
plus **opt-in privacy-trimmed third-party** apps (incl. vanilla ungoogled-chromium and a
|
||||
Thunderbird email option).
|
||||
- **Per-role lists + Essentials defaults** as in §4 (operator-approved; editable in the JSON).
|
||||
- **WDAC caveat**: Developer/Daily-Driver run app-control in *audit* (apps run); Privacy-Max/
|
||||
Journalist run *enforce* (third-party apps blocked until allow-listed). v1 installs anyway and
|
||||
surfaces a clear note for enforce-mode roles; full WDAC allow-listing is a follow-up.
|
||||
|
||||
## 3. Architecture (four small units)
|
||||
|
||||
```
|
||||
windows/apps/catalog.json # the app catalog (staged into the image like flavours/)
|
||||
SilverOS.Welcome.Core/Apps/
|
||||
AppCatalog.cs # record + loader (mirrors FlavourManifest)
|
||||
AppCatalogEntry.cs
|
||||
IAppInstaller.cs / AppInstaller.cs # winget bootstrap + per-app install + configure
|
||||
SilverOS.Welcome.UI/Components/Steps/
|
||||
AppsStep.razor # new wizard step: grouped checkboxes
|
||||
WizardState.cs # + SelectedApps (ids)
|
||||
ApplyService.cs # calls IAppInstaller after the Stack/hardening
|
||||
```
|
||||
|
||||
### 3a. Catalog schema (`catalog.json`)
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"apps": [
|
||||
{
|
||||
"id": "vscodium",
|
||||
"name": "VSCodium",
|
||||
"description": "Telemetry-free VS Code build.",
|
||||
"source": { "winget": "VSCodium.VSCodium" },
|
||||
"group": "developer",
|
||||
"roles": ["developer"],
|
||||
"defaultFor": ["developer"],
|
||||
"configure": null
|
||||
},
|
||||
{
|
||||
"id": "ungoogled-chromium",
|
||||
"name": "ungoogled-chromium",
|
||||
"description": "Chromium with Google integration stripped.",
|
||||
"source": { "winget": "eloston.ungoogled-chromium" },
|
||||
"group": "essentials",
|
||||
"roles": ["essentials"],
|
||||
"defaultFor": [],
|
||||
"configure": "ungoogled-chromium.ps1"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
- `roles`: which roles are *offered* this app (`essentials` = all roles).
|
||||
- `defaultFor`: roles where the checkbox is **pre-checked**.
|
||||
- `source.winget`: the winget id (only `source` key in v1; a future `mirror` key is additive).
|
||||
- `configure`: optional post-install script (relative to `windows/apps/configure/`), e.g. the
|
||||
ungoogled-chromium policy that enables the Chrome Web Store + sets safe search/suggestions.
|
||||
|
||||
### 3b. AppCatalog loader
|
||||
Mirrors `FlavourManifest`/`IFlavourLoader`: `AppCatalogEntry` record + `AppCatalog.Load(dir)`
|
||||
reading `catalog.json` with the same `JsonSerializerOptions` (case-insensitive, comments,
|
||||
trailing commas). A pure function `AppsForRole(role)` returns the entries to display grouped,
|
||||
and `DefaultSelectionForRole(role)` returns the pre-checked ids.
|
||||
|
||||
### 3c. AppsStep (new wizard step, after Flavour)
|
||||
- Renders **Essentials** group first, then the chosen role's group; each app a checkbox with
|
||||
name + description; pre-checked from `DefaultSelectionForRole`.
|
||||
- Writes the set of selected ids into `WizardState.SelectedApps`.
|
||||
- Always valid (zero apps is allowed) — Next is never blocked. Notifies the host like the other
|
||||
steps (so Next state is correct immediately — same `OnSelected`/`StateHasChanged` pattern as
|
||||
the FlavourStep fix).
|
||||
- `Routes.razor` gains a step between Flavour (1) and Account; step indices/titles shift by one.
|
||||
|
||||
### 3d. AppInstaller (Apply-step engine)
|
||||
`IAppInstaller.InstallAsync(IReadOnlyList<AppCatalogEntry> apps, IProgress<ApplyProgress>, ct)`:
|
||||
1. **Bootstrap winget** if absent: install `Microsoft.DesktopAppInstaller` + deps
|
||||
(`Microsoft.VCLibs…`, `Microsoft.UI.Xaml…`) via `Add-AppxProvisionedPackage`/staged msix.
|
||||
2. **Per app**: `winget install --id <source.winget> --silent --accept-package-agreements
|
||||
--accept-source-agreements`; capture exit code; **continue on failure** (a bad app must not
|
||||
fail onboarding); run `configure` script if present and the install succeeded.
|
||||
3. Return a per-app result list (id → installed/failed); ApplyService stows a summary for the
|
||||
Done step ("Installed N of M apps; failed: …").
|
||||
|
||||
Runs **after** hardening + Stack + accounts, **before** BitLocker (so encryption is last), via a
|
||||
new `progress.Report(new("Installing apps", …))` stage.
|
||||
|
||||
## 4. Catalog contents (v1 — editable)
|
||||
|
||||
| Group | App | winget id | default-checked for |
|
||||
|---|---|---|---|
|
||||
| essentials | Thunderbird | `Mozilla.Thunderbird` | all |
|
||||
| essentials | VLC | `VideoLAN.VLC` | all |
|
||||
| essentials | 7-Zip | `7zip.7zip` | all |
|
||||
| essentials | LibreOffice | `TheDocumentFoundation.LibreOffice` | all |
|
||||
| essentials | ungoogled-chromium | `eloston.ungoogled-chromium` | — (opt-in; configured) |
|
||||
| essentials | KeePassXC | `KeePassXCTeam.KeePassXC` | — |
|
||||
| developer | VSCodium | `VSCodium.VSCodium` | developer |
|
||||
| developer | Git | `Git.Git` | developer |
|
||||
| developer | .NET 9 SDK | `Microsoft.DotNet.SDK.9` | developer |
|
||||
| developer | Node.js LTS | `OpenJS.NodeJS.LTS` | developer |
|
||||
| developer | Windows Terminal | `Microsoft.WindowsTerminal` | developer |
|
||||
| developer | PowerShell 7 | `Microsoft.PowerShell` | developer |
|
||||
| developer | Claude Desktop | `Anthropic.Claude` | developer |
|
||||
| developer | Visual Studio 2022 | `Microsoft.VisualStudio.2022.Community` | — |
|
||||
| developer | JetBrains Rider | `JetBrains.Rider` | — |
|
||||
| developer | Docker Desktop | `Docker.DockerDesktop` | — |
|
||||
| developer | Claude Code (CLI) | *(npm `@anthropic-ai/claude-code`, needs Node)* | — |
|
||||
| developer | Google Chrome | `Google.Chrome` | — |
|
||||
| developer | PostgreSQL | `PostgreSQL.PostgreSQL` | — |
|
||||
| developer | Bruno (API client) | `Bruno.Bruno` | — |
|
||||
| journalist | VeraCrypt | `IDRIX.VeraCrypt` | journalist |
|
||||
| journalist | KeePassXC | `KeePassXCTeam.KeePassXC` | journalist |
|
||||
| journalist | Joplin | `Joplin.Joplin` | journalist |
|
||||
| journalist | OBS Studio | `OBSProject.OBSStudio` | — |
|
||||
| journalist | Standard Notes | `StandardNotes.StandardNotes` | — |
|
||||
| journalist | Signal | `OpenWhisperSystems.Signal` | — |
|
||||
| journalist | Tor Browser | `TorProject.TorBrowser` | — |
|
||||
| daily-driver | Spotify | `Spotify.Spotify` | — |
|
||||
| daily-driver | Zoom | `Zoom.Zoom` | — |
|
||||
| daily-driver | Discord | `Discord.Discord` | — |
|
||||
| privacy-max | VeraCrypt | `IDRIX.VeraCrypt` | — |
|
||||
|
||||
> Claude Code (CLI) installs via npm, not winget — modelled with a `source.npm` variant the
|
||||
> engine handles separately (and only if Node is selected/present). Listed but lower priority.
|
||||
|
||||
## 5. Build wiring
|
||||
- `build.ps1`: stage `windows/apps/` (catalog.json + configure/) into the image
|
||||
(`C:\Program Files\SilverOS\Welcome\apps\` next to the flavours), same as flavours.
|
||||
- The Welcome app reads the catalog from `AppContext.BaseDirectory\apps\catalog.json`.
|
||||
|
||||
## 6. Error handling
|
||||
- Catalog missing/!parse → the Apps step shows an empty/"no extra apps" state and onboarding
|
||||
continues (never blocks).
|
||||
- winget bootstrap fails (offline) → log it, skip the install stage with a Done-step note;
|
||||
onboarding still completes.
|
||||
- Per-app install failure → recorded, surfaced in the Done summary, never throws.
|
||||
|
||||
## 7. Testing
|
||||
- `AppCatalog` deserialization + `AppsForRole`/`DefaultSelectionForRole` unit tests (xUnit).
|
||||
- `AppInstaller` against a fake `IProcessRunner`: asserts winget-bootstrap when absent, the exact
|
||||
`winget install` invocation per selected app, continue-on-failure, and configure-script run.
|
||||
- `AppsStep` selection/validity (bUnit, matching the existing step tests' style).
|
||||
|
||||
## 8. Out of scope (follow-ups)
|
||||
- Curated SilverMetal mirror (the `source.mirror` end-state) + signing for WDAC-enforce.
|
||||
- WDAC allow-listing of installed apps for Privacy-Max/Journalist enforce mode.
|
||||
- Per-app version pinning / update policy.
|
||||
Reference in New Issue
Block a user