Files
Boliang ZhangandCopilot 06e6ae43e2 feat(workspaces): add list and launch CLI (#51114)
<!-- Enter a brief description/summary of your PR here. What does it
fix/what does it change/how was it tested (even manually, if necessary)?
-->
## Summary of the Pull Request

Add Workspaces **list** and synchronous **launch** through a thin native
CLI and shared GUI/CLI execution core. Related to #49900;
capture/create/CRUD are excluded. Usage and contracts:
`doc\devdocs\modules\workspaces-cli.md`.

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: N/A - partial scope of #49900.
<!-- - [ ] Closes: #yyy (add separate lines for additional resolved
issues) -->
- [x] **Communication:** I've discussed this with core contributors
already. If the work hasn't been agreed, this work might be rejected
- [x] **Tests:** Added/updated and all pass (scoped local runs below;
CI/release gates remain)
- [x] **Localization:** All end-user-facing strings can be localized
- [x] **Dev docs:** Added/updated
- [x] **New binaries:** Added on the required places
- [x] [JSON for
signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json)
for new binaries
- [x] [WXS for
installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs)
for new binaries and localization folder - current integration:
`installer\PowerToysSetupVNext\CliShims.wxs`
- [x] [YML for CI
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml)
for new test projects - existing discovery; session-peer companion
signing updated
- [x] [YML for signed
pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml)
- N/A: existing signing pipeline
- [ ] **Documentation updated:** If checked, please file a pull request
on [our docs
repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys)
and link it here: N/A - follow-up not filed.

<!-- Provide a more detailed description of the PR, other things fixed,
or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

### Shared execution, conditional process boundary

```mermaid
flowchart TB
    EDITOR["Editor process<br/>C# / WPF"] -->|"start GUI Launcher"| ADAPTER
    SHELL["Terminal / script"] -->|"CLI shim starts CLI"| ADAPTER

    subgraph CALLER["Caller process: CLI.exe OR Launcher.exe in GUI mode"]
        ADAPTER["Thin entry adapter"]
        START["WorkspacesSession::Start<br/>snapshot + context check"]
        LOCAL["WorkspacesSession::Execute()<br/>function inside the caller process"]
        ADAPTER -->|"in-process"| START
        START -->|"verified Medium: in-process"| LOCAL
        LOCAL -.->|"typed return via Start"| ADAPTER
    end

    subgraph HOST["Elevated callers only: Medium host process<br/>PowerToys.WorkspacesLauncher.exe"]
        HANDOFF["Host entry<br/>receive and validate SessionHandoff"]
        REMOTE["WorkspacesSession::Execute()<br/>function inside this host process"]
        HANDOFF -->|"function call within the same process"| REMOTE
    end

    START -->|"elevated: create host<br/>read-only snapshot + restricted handles"| HANDOFF
    REMOTE -.->|"private anonymous result pipe"| START
    START -.->|"host outcome returned"| ADAPTER
    LOCAL <-->|"named pipes"| ARRANGER["WindowArranger process"]
    REMOTE <-->|"named pipes"| ARRANGER
    LOCAL <-->|"named pipes"| UI["Existing WPF Launcher UI process"]
    REMOTE <-->|"named pipes"| UI

    classDef shared fill:#dbeafe,stroke:#2563eb,color:#172554
    class START,LOCAL,REMOTE shared
```

The caller box represents **one of the two entry executables**, not both
in one process. Blue nodes are code statically linked from
`WorkspacesLib.lib`, not a DLL or shared service. **Medium GUI/CLI
callers execute in their own process without another Launcher host.**
Only elevated callers create a verified Medium host through the existing
`run_non_elevated` helper; it calls the same
`WorkspacesLib\LaunchExecution.cpp` core. No elevated fallback.

**`Execute()` is a function, not another EXE.** The host entry and its
`Execute()` share the same process ID. Execution uses a worker thread
inside that process, not another Launcher process. The two `Execute()`
nodes represent the same library implementation running in alternative
caller/host contexts.

- **Shared behavior:** one preparation, launch, arrangement,
confirmation and history flow. GUI retains progress; CLI shows only
required existing confirmation UI. GUI Cancel and console cancellation
use the shared stop/result path; already-started apps survive. Arranger
callbacks are stopped and joined before their helper owner is released.
An unresponsive inline Shell call yields `outcomeUnknown` before the
one-shot adapter exits, rather than killing a thread or claiming
cancellation succeeded.
- **Contracts:** `list` is read-only; summary JSON reuses existing
names. All arranged = `completed`/0; partial progress = `partial`/10; no
progress = `failed`/10. Incomplete, fractional or invalid final states
yield `outcomeUnknown`/9. Persistence verification failure retains exit
11 and displays application results in both text and JSON modes. The
existing `LaunchedAndMoved` enum and numeric IPC values are unchanged.
- **Persistence/integration:** preserve IDs, initialize missing legacy
IDs, update history best-effort; unsaved GUI drafts do not write saved
configuration/history. Only shim `bin` goes on PATH. Editor sibling-tool
resolution is executable-relative.
- **Telemetry:** one frontend `Workspaces_CLICommand` event when
optional diagnostics are enabled, following #50507's fixed command
category + success model. No workspace names/IDs, paths, arguments or
error text; the Medium host does not double-count. Uses the existing
native telemetry channel; documented in `DATA_AND_PRIVACY.md`.
- **Release IPC:** exact session-owner PID and official CLI/Launcher
identities remain checked. CI companion signing now includes CLI,
Launcher, arranger and Launcher UI; production signature checks are not
disabled.
- **Console isolation:** console workspace targets get their own
console; inline execution does not detach, redirect or share the CLI's
console.
- **Launch and Edit:** a missing temporary draft now falls back to the
saved workspace before GUID validation; malformed existing drafts still
fail. The existing UI regression explicitly checks that no draft is
present.
- **GUI file errors:** unreadable saved files and drafts show the
existing localized file-reading dialog with the correct source filename;
malformed-file dialogs also retain that context. Public CLI error JSON
is unchanged.
- **UI-test transport:** shared value setters place target options
before `--` and pass selectors/values literally. This fixes the
leading-dash command-line inputs rejected by CI's winapp 0.7.1 without
changing Workspaces test payloads or assertions.

### Changed-file breakdown

Full PR versus main, not just the latest commit. Each file is counted
once; test projects and fixtures are grouped under tests. These are file
categories, not process or library boundaries.

| Category | Files |
|---|---:|
| CLI adapter, output, resources and project | 7 |
| Shared session/execution entry, contracts, IPC, storage and telemetry
| 19 |
| Launcher engine/host adapter, arranger and UI | 18 |
| Managed storage and Editor path compatibility | 4 |
| Common helpers | 9 |
| Tests and fixtures | 24 |
| Build, installer and signing | 6 |
| Documentation and privacy catalog | 4 |
| **Total** | **91** |

The Launcher engine sources are compiled into `WorkspacesLib` even
though their source files remain under `WorkspacesLauncher`; the table
does not imply a separate engine process.

**Scope/known follow-ups:** general Editor save/conflict redesign and
the pre-existing localized-name/package-version matching bug are
deferred. Final GUI progress presentation still needs follow-up; this
update does not claim to fix its spinner-before-close behavior. The
native C++ parser is retained by the PR author's decision; a managed
System.CommandLine migration is not included. This does not amend the
repository-wide CLI convention.

<details>
<summary>List output and shared confirmation UI</summary>

![Workspaces CLI
list](https://github.com/user-attachments/assets/3755c810-0b85-4afe-b80f-f17f12e7f07d)
![Shared confirmation UI; controlled probe, no target
executed](https://github.com/user-attachments/assets/fbd09160-725d-47b7-907e-307bb2d749dc)

</details>

<!-- Describe how you validated the behavior. Add automated tests
wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed

x64 Debug builds of the affected product and UI-test/fixture projects
passed; Release core, ModuleInterface, arranger, Launcher and CLI builds
also passed. The pre-GUI-error scoped native run passed **160 tests**.
Two deterministic paused-dispatch regressions reproduced `0xC0000005`
before the teardown fix (snapshot and final-result callbacks); both now
return acknowledged cancellation. Test hooks are compiled only into the
fixture, not production builds. **48 signing-selection** tests passed
earlier against unchanged selection logic. The preceding missing-draft
fix passed **39 focused tests** plus the complete local-host `winapp`
Launch and Edit flow, with settings/pointer/Editor state restored. This
is targeted host validation, not full local OS-matrix sign-off.

The GUI error follow-up `8ea5619586` passed **33 focused native tests in
each of Debug and Release**, including a regression that failed before
the fix. Both real saved-file/draft error dialogs were driven locally
with UIA; original configuration bytes and draft absence were restored.

The CI transport follow-up `e0945376e6` passed **19 focused
shared-harness tests in each of Debug and Release**. Using the
hash-verified winapp **0.7.1** archive from the failing CI revision, the
original invocation reproduced `invalid_arguments`; the fixed wrappers
passed **nine owned-UI setter checks** for leading options, quoted text,
empty text, and negative numeric values. The TextBox, editable ComboBox,
NumberBox, and Slider setters share the corrected argument boundary.
This is focused wrapper validation, not a claim that the full Workspaces
suite ran locally.

<details>
<summary>Commands and results</summary>

Used the verified side-by-side SDK 10.0.303; no global/repository SDK or
package-source change. With `$root='D:\PowerToys-workspace-cli'`, from
each affected project directory:

```powershell
& "$root\tools\build\build.ps1" -Platform x64 -Configuration Debug -ExtraArgs @("/p:SolutionDir=$root\", '/p:BuildProjectReferences=false', '/p:WorkspacesCliLiveTests=true')
# Passed, exit 0.
& "$root\tools\build\build.ps1" -Platform x64 -Configuration Release -ExtraArgs @("/p:SolutionDir=$root\", '/p:BuildProjectReferences=false')
# Passed, exit 0, for core/arranger/Launcher/CLI.
```

Missing Release UI dependencies were first built with project references
enabled:

```powershell
# From src\modules\Workspaces\WorkspacesLauncherUI:
& "$root\tools\build\build.ps1" -Platform x64 -Configuration Release -ExtraArgs @("/p:SolutionDir=$root\")
# Passed, exit 0.
```

Pre-GUI-error scoped native run (`$evidence` is the local report
directory):

```powershell
$env:WORKSPACES_CLI_LIVE_TESTS='1'
& 'C:\Program Files\Microsoft Visual Studio\18\Enterprise\Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe' "$root\x64\Debug\tests\Workspaces\Workspaces.Lib.UnitTests.dll" '/TestCaseFilter:FullyQualifiedName~CliContractTests|FullyQualifiedName~CliJsonOutputTests|FullyQualifiedName~CliRuntimeTests|FullyQualifiedName~CliHandoffTests|FullyQualifiedName~CliWorkerTests|FullyQualifiedName~WorkspaceStoreTests|FullyQualifiedName~SignatureVerificationTests|FullyQualifiedName~SignatureTrustTests|FullyQualifiedName~ExecutableTargetTests|FullyQualifiedName~ElevatedLaunchGateTests|FullyQualifiedName~PendingLaunchApprovalTests' "/ResultsDirectory:$evidence" '/Logger:trx;LogFileName=teardown-race-green.trx' '/Logger:console;verbosity=minimal'
# Passed: 160; failed/skipped: 0; exit 0.
& 'C:\Program Files\Microsoft Visual Studio\18\Enterprise\Common7\IDE\CommonExtensions\Microsoft\TestWindow\vstest.console.exe' "$root\x64\Debug\tests\Workspaces\Workspaces.Lib.UnitTests.dll" '/TestCaseFilter:FullyQualifiedName~CliContractTests|FullyQualifiedName~JsonUtilsTests' "/ResultsDirectory:$evidence" '/Logger:trx;LogFileName=missing-draft-headless.trx' '/Logger:console;verbosity=minimal'
# Passed after the draft fix: 39; failed/skipped: 0; exit 0.
Invoke-Pester -Script 'D:\PowerToys-workspace-cli\.pipelines\tests\uiTestSigningSelection.Tests.ps1' -PassThru
# Passed: 48; failed: 0.
git diff --check
# Passed, exit 0.
```

Telemetry lifecycle tests use a recording sink and synthetic commands;
they neither transmit telemetry nor change diagnostic consent.

Recorded host UI actions (3892 was this run's owned Editor PID, not a
reusable PID):

```powershell
winapp ui 'invoke' 'EditButton' '-a' '3892' '--json'
winapp ui 'invoke' 'LaunchEditButton' '-a' '3892' '--json'
winapp ui 'invoke' 'SnapshotButton' '-a' '3892' '--json'
winapp ui 'invoke' 'SaveButton' '-a' '3892' '--json'
# All passed, exit 0; fixture startup, exact placement, recapture and persisted IDs were asserted.
```

</details>

Latest shared-harness build/test commands, from
`src\common\UITestAutomation.Next.UnitTests`:

```powershell
& "$root\tools\build\build.ps1" -Platform x64 -Configuration Debug -ExtraArgs @("/p:SolutionDir=$root\")
& "$root\tools\build\build.ps1" -Platform x64 -Configuration Release -ExtraArgs @("/p:SolutionDir=$root\")
# Both passed, exit 0, including analyzers.
& "$root\x64\Debug\tests\UITestAutomation.Next.UnitTests\net10.0-windows10.0.26100.0\UITestAutomation.Next.UnitTests.exe" --filter 'FullyQualifiedName~WinappCliTests|FullyQualifiedName~SessionTests' --report-trx --report-trx-filename ci-set-value-unit.trx --results-directory $evidence
& "$root\x64\Release\tests\UITestAutomation.Next.UnitTests\net10.0-windows10.0.26100.0\UITestAutomation.Next.UnitTests.exe" --filter 'FullyQualifiedName~WinappCliTests|FullyQualifiedName~SessionTests' --report-trx --report-trx-filename ci-set-value-unit-release.trx --results-directory $evidence
# Each passed: 19; failed/skipped: 0; exit 0.
```

**CI status:**
[391837](https://dev.azure.com/shine-oss/62769a65-d989-45e3-803c-9ad21fcf2ea2/_build/results?buildId=391837)
**succeeded** on current head `e0945376e6`, tested as merge `e27f537028`
with main `79ceeee01c`. SDK/x64/ARM64 builds passed, and **34/34
Workspaces UI tests passed on each of Windows 10 x64, Windows 11 x64,
and Windows 11 ARM64**, with zero failures or skips. Finished 2026-10-26
16:18:46 UTC.

The preceding
[391820](https://dev.azure.com/shine-oss/62769a65-d989-45e3-803c-9ad21fcf2ea2/_build/results?buildId=391820)
on `8ea5619586` passed product builds but had 29/34 UI tests pass on
each platform. All five failures were leading-dash textbox values
rejected by winapp 0.7.1; the shared helper was identical to main and
predates this PR. The setter fix resolves those failures without
changing Workspaces product code or weakening assertions. Fresh
High-parent/UAC, installer servicing and broader mixed-DPI validation
remain release gates.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 664ee900-ce2d-4112-ac57-395314d818dc
2026-10-27 11:29:49 +08:00

17 KiB

Workspaces command-line interface

Related issue: #49900. This interface lists saved workspaces and launches one through the existing Workspaces engine. It does not capture, create, import, delete or edit workspace configurations.

Installation

The installer adds only <PowerToys installation>\bin to PATH. PowerToys.Workspaces.CLI.exe in that folder is the shared native CLI shim, not the application payload. It launches ..\PowerToys.WorkspacesCLI.exe using the installed-relative mapping.

Do not add the installation root or WinUI3Apps to PATH. Those folders contain application/runtime DLLs and are not command directories. The wrapper avoids advertising those DLL locations through PATH; it does not replace the operating system's DLL security rules.

The manifest and WiX component list are checked together. The real CLI and shared shim are included in release signing, and installer shutdown recognizes both process names. See CLI conventions.

Commands

PowerToys.Workspaces.CLI.exe list [--json]
PowerToys.Workspaces.CLI.exe list --id <guid> [--details] [--json]
PowerToys.Workspaces.CLI.exe list --name <name> [--details] [--json]
PowerToys.Workspaces.CLI.exe launch --id <guid> [--timeout <seconds>] [--json]
PowerToys.Workspaces.CLI.exe launch --name <name> [--timeout <seconds>] [--json]

Help/version do not require an enabled module. Both operational commands reject user-disabled or policy-disabled Workspaces. Configured GPO overrides the user toggle exactly as in Settings. The Runner need not be running when the effective module configuration is enabled.

Use exactly one selector for launch or detail. GUID matching accepts braces and either case, without rewriting stored IDs. Name matching is exact, ordinal and case-insensitive; ambiguous names fail rather than selecting the first workspace. An explicit missing selector target is an error; an enabled module with no workspace file has an empty unfiltered list.

Data contract

--json returns one UTF-8 document. Successful list responses contain only view and workspaces at the top level, without a schemaVersion, command, state or result wrapper. Launch responses and errors keep the envelope with schemaVersion, command, state and result or error. Failed queries return a structured error and a nonzero exit code, never an empty successful list. Public JSON is formatted with line breaks and two-space indentation, followed by a newline, including when stdout is redirected. It is one JSON object, not a quoted JSON string or newline-delimited JSON records. Formatting does not alter stored field names or values. The private worker/confirmation protocol is unchanged. Warnings are structured separately and never replace an application failure.

Summary entries are a subset of the existing stored workspace JSON:

{
  "view": "summary",
  "workspaces": [
    {
      "id": "{6CF910A2-D2E0-436D-A50E-41432A88452A}",
      "name": "Development",
      "applications": [
        { "application": "Notepad" },
        { "application": "Microsoft Edge" }
      ]
    }
  ]
}

Both unfiltered and selected lists contain view: "summary" or "detail" and a top-level workspaces array, even for one selected workspace or no workspaces. Scripts can read .workspaces directly. Summary preserves application order, duplicates, field names, types and values; no apps, per-app name, or count alias is added.

Detail reuses the native workspace serializer, including existing kebab-case names, timestamp units and monitor/DPI configuration. Paths and command arguments may contain sensitive data: request detail deliberately and protect redirected output. Runtime handles/process IDs are not part of stored workspace JSON.

Launch results contain the workspace and operation IDs, per-application results and a separate persistence status. An observed arranged state does not certify that a document/tab has loaded or that an application is ready for business use. If launch-history verification fails with persistenceUnverified, exit code 11 is preserved: text mode writes the error to stderr and the observed application results to stdout; JSON mode includes both in its single response. Do not rerun the workspace merely to recover those results.

Per-application launched preserves the native Launched state: the application launch succeeded, but window arrangement was not confirmed. A localized message explains this distinction. arrangeFailed is reserved for a native failed result without a recorded application-launch error; recorded launch errors remain launchFailed. arranged means the engine confirmed arrangement. Aggregate state distinguishes launch progress from fully confirmed restoration:

Application outcomes Aggregate state Exit code
Every application is arranged completed 0
At least one is launched or arranged, but not all are arranged partial 10
None is launched or arranged (for example, all skipped or failed) failed 10

Thus skipped plus launched is partial, not failed. Exit codes remain unchanged: unconfirmed arrangement is not counted as a fully restored workspace. Final arranger responses must contain a known outcome for every requested application. Waiting, fractional/out-of-range numeric states, missing/duplicate applications and invalid results are rejected before applying or acknowledging completion. They produce outcomeUnknown (exit 9), not an application-level unknown with exit 10. Native Launched remains valid and is reported as launched.

Launch behavior

  • Launch waits for an explicit final arranger result. There is no accepted-only/no-wait mode.
  • The default total deadline is 120 seconds; --timeout accepts 1 through 600.
  • At the deadline the shared launcher cancels its pending confirmation and allows up to two seconds for execution to acknowledge completion. An acknowledged timeout reports timeout; an absent, malformed or unresponsive final result reports outcomeUnknown. This does not extend app-launch or approval time. An unresponsive in-process operation retains its owned state until the Shell call returns or the one-shot adapter exits after reporting the error; no unsafe thread termination is used.
  • Workspaces progress/editor windows are suppressed. The existing Workspaces signature confirmation dialog, application windows and normal Windows UAC remain possible.
  • Console applications launched as workspace targets get their own console rather than sharing the CLI's console. The CLI keeps its original console and standard streams.
  • Both ordinary and administrator terminals use the same command. The shared session layer runs directly inside a verified Medium caller; only an elevated caller starts a Medium Launcher host in the same user's desktop session. Existing saved elevation flags still determine individual app/UAC behavior; the administrator caller does not implicitly elevate every app.
  • Existing windows follow the saved move-existing-windows setting. Another launch in the same session returns busy rather than queueing or taking over.
  • Ctrl+C requests stopping further launches. Already-started applications are not killed and window positions are not rolled back. A blocked operation without acknowledgment is reported as unknown outcome, not canceled.
  • A timeout does not mean that a Windows consent dialog was dismissed. Do not automatically retry: launch is not idempotent and repeating it may open more applications.
  • The shared executor and arranger use the same immutable selected snapshot. Final history writes read the current file under a shared writer lock and do not replace it with the old launch snapshot. The initial snapshot exchange waits for both IPC endpoints within its startup deadline before sending the first request, so asynchronous pipe creation cannot discard it or its reply.
  • Before launching legacy workspaces, the Medium executor assigns and safely persists only missing application IDs. Existing IDs are preserved; malformed or duplicate existing IDs are errors. Concurrent edits or failed ID persistence stop the launch before applications start. Listing remains read-only, and a later canceled/timed-out launch does not undo completed ID initialization.
  • The foreground CLI changes the console output code page only while writing output, flushes those writes and restores it before returning to other work; launch waits leave it unchanged. Elevated callers receive the host result through a private pipe, independent of console output and CLI localization.
  • File logging is best-effort. An unavailable log directory/file does not prevent launching; the final launch result includes a loggingUnavailable warning without changing application outcomes.

Shared session and administrator routing

The CLI and GUI launcher are adapters over WorkspacesSession::Start. The entry and execution core are statically linked from WorkspacesLib, including the existing Launcher/AppLauncher implementation. Start selects an immutable snapshot and checks context. A Medium caller invokes the shared Execute in the same process, without another Launcher process, snapshot mapping or result pipe. An elevated caller creates a verified Medium PowerToys.WorkspacesLauncher.exe host that invokes the same Execute. Unsupported contexts fail; no path executes the core at High IL. Execute owns one preparation/launch/history flow and returns a typed outcome; only the CLI adapter creates the public JSON, text and exit-code representation.

flowchart TD
    C["GUI Launcher / CLI"] --> S["WorkspacesSession::Start<br/>WorkspacesLib.lib"]
    S --> M{"Execution context"}
    M -->|"Verified Medium"| E["Execute in the caller process"]
    M -->|"Elevated"| H["Create a verified Medium Launcher host"]
    H --> HE["Same Execute implementation in the host"]
    M -->|"Unsupported"| F["Fail without launching"]
    E --> R["Typed result returned in-process"]
    HE --> P["Private result pipe back to Start"]

The GUI retains progress presentation and its invocation context, including unsaved Launch-and-Edit drafts; CLI mode only displays required confirmation UI. No Editor save-workflow changes are added. Launch and Edit can start an already-saved workspace without a temporary draft. A missing temp-workspaces.json falls back to saved configuration; an existing malformed draft remains an error, and a valid matching draft takes precedence. The GUI displays the existing localized file-read or invalid-file dialog when configuration loading fails, identifying the saved workspace file or temporary draft that caused the error. This local diagnostic context does not change the public CLI error JSON.

The progress window's Cancel button and CLI cancellation both stop the shared launch wait and return an acknowledged canceled result without requiring normal arranger completion. Pending application launches stop, already-started applications remain open, and operation-owned workers and the launch lock are released. Non-canceled launches still require a complete, validated arranger result. Teardown stops and joins arranger callbacks before releasing their helper owner, so an in-flight snapshot or final-result callback cannot access an already-cleared helper pointer.

For elevated callers, shared session routing validates the desktop shell's user/session/Medium integrity, creates a suspended host through the common Explorer-parent helper, and verifies the actual child's token before allowing execution.

The child receives its operation handles through a bounded, read-only bootstrap pipe. The frontend pins the exact created PID; both sides verify the expected module image/version and Microsoft signature in Release. User SID/session must match, and the child must be non-elevated. Only a read-only snapshot, wait-only cancellation/owner-lifetime handles and the existing result writer are duplicated. No token handle or general administrator process capability is handed to the worker.

The existing run_non_elevated helper owns Explorer selection, startup attributes and process creation. Its owning overload supports suspended creation and parent validation; the existing PID-only API delegates to that implementation. The module's shared SessionHandoff owns bootstrap identity/capability transfer for both GUI and CLI; neither adapter maintains an independent de-elevation or worker implementation. Other users of RunNonElevatedEx retain their existing COM-based behavior.

No normal same-user Explorer, a different-user administrator, wrong session, identity mismatch or failed transfer yields deElevationFailed; the CLI never falls back to elevated execution. No extra console is opened. UAC for individual configured elevated apps remains separate. The arranger accepts only the exact caller PID with a matching installed directory/version and one of the official CLI/Launcher executable names. Release retains signature verification. PR UI-test setup signs the CLI, Launcher, arranger and Launcher UI using the existing companion-signing mechanism; it does not disable production authentication.

Unverified executable confirmation

Workspaces' existing shared launch gate checks eligible EXE targets configured to run as administrator. This is separate from Windows UAC and from IPC peer authentication. The CLI does not change the signature policy or create a permanent trust list.

When a decision is required, both GUI and CLI use Launcher::requestApproval, PendingLaunchApproval, LauncherUIHelper and the existing SignatureWarningWindow. There is no independent CLI approval state machine or terminal A/S prompt.

  • The CLI lazily starts the existing launcher UI in confirmation-only mode. It shows the same warning dialog without opening the launch-progress window.
  • Allow is enabled only after the existing rendered acknowledgement. It authorizes only the current checked target; Windows UAC may still follow.
  • Skip, Escape or closing the dialog skips the application. Ctrl+C cancels the CLI operation.
  • The launch deadline includes decision time. Cancellation/expiry dismisses the pending warning and never approves it. The confirmation host exits if its owning launch worker exits.
  • Redirecting stdin/stdout/stderr does not suppress the dialog or automatically approve anything. A usable interactive desktop is still required, and unavailable UI is reported explicitly.
  • The shared launcher retains the checked file throughout confirmation and launch. Existing stale/duplicate reply handling and signature/UAC policies are unchanged.

Only the elevated caller's host uses private NUL standard streams; inline execution leaves the caller's streams intact. Confirmation uses the existing launcher UI message protocol on operation-scoped pipes, separate from the CLI result stream. The native endpoint verifies the UI module image/version and requires its Microsoft signature in Release.

Exit codes

Code Meaning
0 Requested operation completed, possibly with nonessential history-save warnings
1 Unexpected command/runtime error
2 Invalid command, options or selector syntax
3 Workspace not found
4 Ambiguous workspace name
5 Another launch owns the session
6 Disabled by user/policy, policy lookup failure or component unavailable; inspect error.code
7 Unsupported caller context, failed de-elevation, arranger startup failure or required consent declined
8 Invalid/unreadable saved configuration, conflicting legacy ID initialization or failed ID persistence
9 Deadline or unknown outcome
10 One or more applications failed, were skipped, or lack confirmed arrangement/required approval
11 Persistence replacement could not be verified; app results remain separate
12 Acknowledged cancellation
9009-9011 Shared shim could not resolve/find/start its target

Stable machine error codes distinguish reasons such as disabledByUser, disabledByPolicy, workspaceNotFound, deElevationFailed, consentDenied and outcomeUnknown; human messages are resource-backed. Per-app skipped, confirmationRequired, confirmationTimedOut, confirmationFailed and canceled are not aliases for a Windows UAC denial. Skipping an app means the requested workspace was not fully restored, so it is not reported as all-success.

CLI telemetry

When optional PowerToys diagnostics are enabled, the foreground CLI emits one Workspaces_CLICommand usage event per invocation, following the Settings CLI convention. Only a fixed CommandName (list, launch, help, version, none, or unknown) and Successful (exit code zero) are recorded. Partial restoration, cancellation and command failures are not counted as successful. No workspace names/IDs, paths, argument values or error text are included. The Medium host does not emit this command event, so elevated calls are not counted twice. Existing GUI launch telemetry is unchanged.

Compatibility boundaries

The CLI does not enable modules, migrate storage to another format, or add a permanent service. General Editor snapshot-conflict handling and save-workflow redesign are not included. Basic atomic file IO and targeted launch metadata updates remain, but an older Editor snapshot is not merged with concurrent configuration/application metadata changes when the Editor saves. The public CLI contract covers the reused detail shape: future breaking storage changes must not silently change that public output, including unversioned successful list responses. Future creation APIs may reuse appropriate payload types, but query/result envelopes and system-owned IDs are not automatically writable input.