Add a resumable PowerShell setup flow for Windows developer machines - #93
Conversation
The release-copy updates move to a separate PR, so this branch no longer touches windows-dev-config/. Co-authored-by: Copilot <[email protected]> Copilot-Session: fa27b7a2-0eb1-4970-927c-340739ecbdc1
There was a problem hiding this comment.
Pull request overview
Replaces the DSC-based Calm OS flow with a modular, idempotent PowerShell setup that supports elevation and reboot resumption.
Changes:
- Adds bootstrap, orchestration, setup phases, and shared helpers.
- Adds WSL reboot/resume, package, registry, Terminal, font, and Copilot configuration.
- Updates manifests and documentation while removing the old DSC implementation.
Reviewed changes
Copilot reviewed 32 out of 33 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
.gitignore |
Ignores setup tally state. |
README.md |
Documents the new PowerShell flow. |
src/docs/development.md |
Describes PowerShell-native flow conventions. |
src/future/cmdpal/README.md |
Notes extension launch limitations. |
src/manifest.yml |
Points Calm OS at the new entry script. |
src/tests/calm-os/probe.ps1 |
Updates smoke-test commentary. |
src/windows-dev-config/README.md |
Rewrites usage and configuration documentation. |
src/windows-dev-config/bootstrap.ps1 |
Downloads and verifies the setup payload. |
src/windows-dev-config/dev-config.ps1 |
Orchestrates setup phases and reporting. |
src/windows-dev-config/dev-config.winget |
Removes the legacy DSC configuration. |
src/windows-dev-config/install.ps1 |
Removes the legacy install shim. |
steps/_console.ps1 |
Adds logging and pause helpers. |
steps/_elevation.ps1 |
Adds elevation and single-instance handling. |
steps/_environment.ps1 |
Adds process, PATH, TLS, and file helpers. |
steps/_pwsh-bootstrap.ps1 |
Bootstraps and relaunches under PowerShell 7. |
steps/_reboot-resume.ps1 |
Registers post-reboot continuation. |
steps/_registry.ps1 |
Adds registry helpers. |
steps/_resume-wrapper.ps1 |
Relays resumed-run output. |
steps/_retry.ps1 |
Adds retry handling. |
steps/_step-runner.ps1 |
Implements check/apply/verify execution. |
steps/_terminal.ps1 |
Manages Terminal settings JSON. |
steps/_winget.ps1 |
Manages WinGet front ends and packages. |
steps/copilot.ps1 |
Configures Copilot integrations. |
steps/edge.ps1 |
Applies Edge policies. |
steps/fonts.ps1 |
Installs and configures Cascadia fonts. |
steps/packages.ps1 |
Defines the package set. |
steps/powershell-profile.ps1 |
Adds Oh My Posh initialization. |
steps/prerequisites.ps1 |
Verifies PowerShell and WinGet. |
steps/registry-explorer.ps1 |
Applies Explorer settings. |
steps/registry-system.ps1 |
Applies system settings. |
steps/registry-taskbar-search.ps1 |
Applies taskbar and search settings. |
steps/terminal.ps1 |
Configures Terminal defaults and theme. |
steps/wsl.ps1 |
Installs WSL, reboots, and installs Ubuntu. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
1703f36 to
9c0e1ea
Compare
There was a problem hiding this comment.
Went through this properly rather than skimming, mostly the resume path since that's the part that's hard to get right.
Most of what I'd have flagged is already handled. Moving Clear-DevConfigStepFlag into the check loop is the right call - doing it only in the reporting loop would have missed resumed phases that return early, which is exactly the case that matters. I also ran the JSONC change on a real box: 7.6.5 parses the comments fine, 5.1 throws, and Read-DevConfigTerminalSettings refuses rather than quietly mangling someone's settings.json. And the $Ref regex plus the .. check does genuinely keep the archive URL inside the repo.
The one thing I couldn't talk myself out of: Invoke-DevConfigNativeCommand has no timeout, while Invoke-DevConfigProcess directly below it makes TimeoutSeconds mandatory and prints a heartbeat every minute. The four copilot calls in copilot.ps1 go through the unbounded one. copilot gets installed earlier in the same run, so on a fresh machine it has never been signed in (it does have a login subcommand), and marketplace add / plugin install both do network work on top of that. The Copilot phase runs immediately before WSL, so a stall there doesn't just skip a step - it strands the run short of the restart the whole resume flow depends on. BestEffort won't catch it either, since a hang never throws.
Have you had this run against a machine that's never signed into copilot? If it fails fast there, this is a non-issue.
There was a problem hiding this comment.
🟡 Changes recommended
The verified payload remains user-writable before elevated execution, and several state checks can incorrectly accept drifted configurations.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (5)
Previously missed (5) — in code that hasn't changed since the last review.
src/windows-dev-config/steps/_registry.ps1:29
- This check compares only the coerced value and ignores the registry value kind. PowerShell considers values such as REG_SZ
"1"equal to integer1, so a policy that requires REG_DWORD can be reported as already configured even though Windows ignores the wrong type. Include the expected type in this helper's contract and compare it with the key'sGetValueKind; the Edge caller already carries a$Typevalue that should be passed through.
src/windows-dev-config/steps/copilot.ps1:18 - An empty, malformed, or drifted fragment file is treated as configured solely because the path exists, so reruns never repair the GitHub Copilot profile despite the flow's check/apply/verify contract. Parse the fragment and verify the expected GUID and profile fields; return false on parse failure so the apply step rewrites it.
src/windows-dev-config/steps/fonts.ps1:26 - This check accepts any files with the two expected names and matching registry entries, without validating their contents. An older or corrupt same-named font therefore skips installation forever, and changing the pinned version/hash later will not upgrade existing machines. Verify each installed file against the expected per-font hash (or a trustworthy installed-version marker) before returning true.
src/windows-dev-config/README.md:252 - The WSL steps are both marked
BestEffort, so this exception is caught and reported as a flag; the orchestrator then prints the normal completion summary. The run does not stop as stated here. Describe the flagged completion behavior so users know to inspect the summary and rerun after fixing virtualization or connectivity.
src/windows-dev-config/README.md:377 - The Terminal settings steps are
BestEffort, so invalid JSON flags those steps and allows all remaining phases to continue; it does not stop the setup. This troubleshooting text still contradicts the runner behavior and the corrected warning earlier in this README.
- Files reviewed: 32/33 changed files
- Comments generated: 2
- Review effort level: Balanced
Replace AI DSC acquisition with PR microsoft#93-style PowerShell setup, add AMD ROCm and Intel AI flows, centralize provider promotion metadata, and emit portable hardware acceptance reports. Co-authored-by: Copilot App <[email protected]>
ranm-msft
left a comment
There was a problem hiding this comment.
Pulled the three new commits and traced the launch paths. The post-copy Assert-CalmOsMicrosoftSigned plus AllSigned on every relaunch does close the script-replacement path the bot flagged, since the engine revalidates each file at load time inside the elevated process. Two things I'd want settled before this is the documented path:
1. The only currently working Quick start requires persistent policy changes. It sends users to Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy Bypass in both 5.1 and 7, and as the README notes, CurrentUser survives reboot. Previously the bypass was process-scoped and died with the run. Since -AllowUnsigned is already an explicit opt-in, could that path propagate a process-scoped -ExecutionPolicy Bypass through launch/elevation/pwsh/resume instead, so the weakening stays inside the setup flow rather than saved to the user's profile?
2. AllSigned doesn't trust a Microsoft signature by itself. It matches the signer against CurrentUser\TrustedPublisher (LocalMachine isn't consulted), and the Microsoft code-signing cert isn't there by default. I checked a Status=Valid, CN=Microsoft Corporation-signed script with MOTW cleared under -ExecutionPolicy AllSigned: it stops at the publisher prompt, and the default answer is "do not run" (exit 1, UnauthorizedAccess). "Always run" resolves it persistently; "Run once" doesn't carry into the elevated or pwsh relaunch.
Have you walked the signed path end-to-end on a machine that doesn't already trust the publisher, particularly the reboot resume?
| Assert-CalmOsMicrosoftSigned -Directory $setupDir | ||
| } | ||
|
|
||
| New-Item -ItemType Directory -Path $InstallRoot -Force | Out-Null |
There was a problem hiding this comment.
Is this location secured to admin only writes?
There was a problem hiding this comment.
No—it remains user-writable. In signed mode, Microsoft-signature checks are point-in-time, and AllSigned accepts other trusted publishers too. Is that acceptable for this PR, with admin-only storage handled as follow-up hardening?
There was a problem hiding this comment.
Answering only for my folded-in duplicate - John's thread is his call.
Yes, acceptable for this PR. The precondition is same-user code execution, and medium-to-high IL via UAC isn't a serviced security boundary under MSRC's Windows servicing criteria, so admin-only storage is hardening rather than a fix for a boundary that's currently defended.
What makes it sit right is that the Security section already states it plainly - "Verification does not prevent later file replacement in the writable install directory." That's the correct disclosure rather than overclaiming the signature check. Worth a tracked follow-up issue so it doesn't get lost after merge.
ranm-msft
left a comment
There was a problem hiding this comment.
On John's question at bootstrap.ps1:178 - pulled head (bbe05eb) to check.
No, %LOCALAPPDATA%\CalmOS is not admin-only. It stays writable by the setup user, so the directory ACL is not the control here.
The part I would push on is the follow-up, because AllSigned and Assert-CalmOsMicrosoftSigned do not check the same predicate. AllSigned does revalidate each .ps1 as PowerShell loads it, so a plain unsigned swap is blocked. But it accepts anything signed by a publisher the user trusts, not specifically Microsoft. In the split-token case the unelevated and elevated processes are the same account and share per-user publisher trust, and the medium-integrity side can add its own code-signing certificate without elevation. So a same-user process can trust its own cert, rewrite the copied payload between the post-copy Assert-CalmOsMicrosoftSigned and the Start-Process -Verb RunAs, and the elevated relaunch loads it: passes AllSigned, would have failed the Microsoft check.
With -AllowUnsigned there is no load-time requirement at all, which matters more than "development only" implies while it is the switch the quick start uses.
Would it work to do the copy, or at least the Microsoft-signature revalidation, inside the elevated process and into somewhere medium integrity cannot rewrite, before anything is dot-sourced?
ranm-msft
left a comment
There was a problem hiding this comment.
The three key-path corrections here are real bugs fixed - CabinetState\FullPath, Explorer\ShowFrequent, and Advanced\TaskbarDeveloperSettings\TaskbarEndTask are the documented locations, and the previous paths meant those three tweaks silently did nothing.
One thing that makes this class of bug hard to see, having now read _registry.ps1: Set-DevConfigRegistryValue creates the key when it is missing (New-Item -Path $psPath -Force), and Test-DevConfigRegistryValue reads back the same $KeyPath it was handed. So a mistyped path does not fail - it gets created, written, and then validates successfully. The in-run check can confirm the write happened, but by construction it cannot tell you whether the tweak table names the location Windows actually reads.
That makes the three new assertions in cli-checks.ps1 the only independent check, since their expected paths are hardcoded rather than sourced from the tweak table. They cover 3 of ~24 values today. Are the others validated independently somewhere, or were these three added specifically as regressions for the bugs above? A table-driven mapping test feels like reasonable follow-up rather than something to grow this PR with.
Also: was dropping NavPaneShowVersionControl intentional? It reads as removed rather than relocated, unlike the other three.
Separately - I do not see changes for the three earlier threads (1, 2, 3). Which of those are you planning to handle in this PR versus track as follow-up?
ranm-msft
left a comment
There was a problem hiding this comment.
Re-pulled at 538570c7 and traced the two new commits. The registry fixes check out, and confirming NavPaneShowVersionControl was dropped along with the Git-integration step answers my question there. Removing Set-ExecutionPolicy from the quick start also resolves my persistent-policy thread - the in-memory script block runs without touching the user's policy at all.
One consequence of that fix looks still open. bootstrap.ps1 only sets a policy on the signed path:
$arguments = @('-NoProfile')
if (-not $AllowUnsigned) {
$arguments += '-ExecutionPolicy', 'AllSigned'
}
...
$arguments += '-File', "$target"-ExecutionPolicy here is process-scoped, so the signed path is self-sufficient whatever the machine policy is. The -AllowUnsigned path passes none, so the child -File launch inherits the ambient policy. Unblock-File clears MOTW, which covers RemoteSigned, but a stock Windows 11 client is Restricted, where no .ps1 loads regardless of signature. That is the exact command the README quick start hands to someone on a fresh box.
Is -AllowUnsigned intended as the contributor path, with the signed one-liner being what the quick start should lead with?
Separately, Invoke-DevConfigNativeCommand is unchanged at head, so my timeout thread is still live - leaving it unresolved rather than restating it here.
Replace AI DSC acquisition with PR microsoft#93-style PowerShell setup, add AMD ROCm and Intel AI flows, centralize provider promotion metadata, and emit portable hardware acceptance reports. Co-authored-by: Copilot App <[email protected]>
Introduces a PowerShell implementation of the developer workstation setup that
runs from a single command, checks every change before applying it, and continues
across the reboot WSL requires.