Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Bilingual-pair consistency record: the git blob hash of each side as of the last
# confirmed-consistent state. Both languages carry equal authority. Update both files
# and re-record their hashes with git hash-object after editing either side.
2026-08-15-windows-electron-acl-runner.md: 634b7005ade246e022ffa65b9f86ca9da4d12fba
2026-08-15-windows-electron-acl-runner.zh.md: ccf3ca5cd1b5bcd5416ea02f66c56d21440c18ba
2026-08-15-windows-electron-acl-runner.md: f4f3e2ac595156586012f6eb3467b14944dcaa0f
2026-08-15-windows-electron-acl-runner.zh.md: f32bf878fe647e16d545aaaa0c307836ae9eeb2d
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,25 @@ English | [中文](2026-08-15-windows-electron-acl-runner.zh.md)

The upstream Windows sandbox composes the ACL runner argv as `[process.execPath, runner.js, ...]`. This is correct under the DSH CLI because `process.execPath` is Node. DSH Desktop boots the same Host plugins inside Electron, where the value is the Electron executable during development and the installed `DSH Desktop.exe` after packaging. Starting that executable with the runner path as an argument does not establish the plain-Node process expected by the upstream runner and can instead start another desktop application instance.

The Windows sandbox has no weaker automatic provider fallback. A broken runner therefore makes ordinary workspace-write PowerShell unavailable, while silently bypassing confinement would misrepresent the selected permission mode. The upstream native spawn also deliberately omits `CREATE_NO_WINDOW` and `CREATE_NEW_CONSOLE` because restricted children fail DLL initialization with either flag. That is correct for a console Host, but an Electron GUI Host has no console for the restricted PowerShell process to inherit, so Windows allocates a visible console window for each shell execution.
The Windows sandbox has no weaker automatic provider fallback. A broken runner therefore makes ordinary workspace-write PowerShell unavailable, while silently bypassing confinement would misrepresent the selected permission mode. The upstream native spawn also deliberately omits `CREATE_NO_WINDOW` and `CREATE_NEW_CONSOLE` because restricted children fail DLL initialization with either flag. That is correct for a console Host, but the Electron Node-mode runner starts without a console. On affected Windows environments, forcing the restricted console child to create its own console fails during DLL initialization with `0xC0000142`; environments where Windows creates that console successfully still depend on behavior that differs from the console-hosted CLI path.

## Decision

The desktop package publishes `dsh-plugin-desktop/windows-pwsh-sandbox` as a Host subpath of the existing package. It is not a second npm package. The Windows desktop profile verifies that the `pwsh-sandbox` row still names the expected upstream provider, preserves its platform gate and configuration, disables that row, and inserts the desktop subpath. Compatibility and advanced presentation modes use the same Host profile and therefore receive the same execution adapter.

`DesktopWindowsPwshSandbox` extends the upstream `SandboxPwshExecutor` and uses its protected argv execution methods. It changes an invocation only when the platform is Windows, the Host is Electron, the executable equals `process.execPath`, and the next argument equals the resolved upstream Windows ACL runner. The rewritten argv inserts a private desktop trampoline between the executable and upstream runner. Direct PowerShell execution, including the explicit `danger-full-access` path, is passed through unchanged.

The adapted child receives a cloned environment with every case-insensitive `ELECTRON_RUN_AS_NODE` key removed and one `ELECTRON_RUN_AS_NODE=1` value added. The trampoline validates the exact upstream runner path, removes every form of that variable from its own environment, reconstructs the argv expected by the upstream module, and imports it. Restricted PowerShell and its descendants therefore do not inherit Electron's Node-mode switch.
The adapted child receives a cloned environment with every case-insensitive `ELECTRON_RUN_AS_NODE` key removed and one `ELECTRON_RUN_AS_NODE=1` value added. The trampoline validates the exact upstream runner path, removes every form of that variable from its own environment, and then checks its Windows console state. If no console window exists, it calls `AllocConsole()` while still unrestricted and immediately hides the allocated window with `ShowWindow(..., SW_HIDE)`. Allocation failure is fatal. Only after that host setup does the trampoline reconstruct the argv expected by the upstream module and import it. Restricted PowerShell and its descendants therefore inherit a real console without inheriting Electron's Node-mode switch.

The Electron build explicitly enables the `runAsNode` fuse because this child-launch protocol depends on it. The trampoline preserves the upstream `windows-acl-run` failure signature and exits with code 127 when its own validation or import fails, so the existing sandbox layer continues to classify runner startup failures as unavailable and fail closed.

The deploy root pins the published rc.6 Windows ACL package through a repository-owned Yarn patch. Both its pipe-capture and inherited-stdio `STARTUPINFOW` values combine `STARTF_USESHOWWINDOW` with the existing `STARTF_USESTDHANDLES` and set `wShowWindow` to `SW_HIDE`. Creation flags remain unchanged, so the restricted child still owns or inherits a console and retains the upstream token, job, and stdio behavior while requesting a hidden initial state for a newly created console window. The patch is pinned to both direct and transitive rc.6 dependency descriptors; dependency tests require the sandbox provider and Desktop adapter to resolve the same patched package instance.
The deploy root pins the published rc.6 Windows ACL package through a repository-owned Yarn patch. Both its pipe-capture and inherited-stdio `STARTUPINFOW` values combine `STARTF_USESHOWWINDOW` with the existing `STARTF_USESTDHANDLES` and set `wShowWindow` to `SW_HIDE`. Creation flags remain unchanged, so the restricted child inherits the trampoline's console and retains the upstream token, job, and stdio behavior. The native show-state remains defense in depth but is not used as a substitute for host console allocation. The patch is pinned to both direct and transitive rc.6 dependency descriptors; dependency tests require the sandbox provider and Desktop adapter to resolve the same patched package instance.

## Verification

Unit tests cover the exact Windows Electron match, unchanged non-Windows, plain-Node, wrong-executable, wrong-runner, and direct-PowerShell invocations, case-insensitive environment removal, parent-environment isolation, trampoline rejection, public package exports, build entries, the enabled fuse, the exact Yarn patch resolution, both hidden show-state writes, and the absence of `CREATE_NO_WINDOW`. Profile tests verify the Windows replacement, inherited configuration and gate, unchanged macOS and Linux composition, unchanged subprocess and sandbox services, and preservation of explicitly disabled or third-party providers.
Unit tests cover the exact Windows Electron match, unchanged non-Windows, plain-Node, wrong-executable, wrong-runner, and direct-PowerShell invocations, case-insensitive environment removal, parent-environment isolation, trampoline rejection, existing-console reuse, console allocation and hiding, allocation failure, host-setup ordering, public package exports, build entries, the enabled fuse, the exact Yarn patch resolution, both hidden show-state writes, and the absence of `CREATE_NO_WINDOW`. Profile tests verify the Windows replacement, inherited configuration and gate, unchanged macOS and Linux composition, unchanged subprocess and sandbox services, and preservation of explicitly disabled or third-party providers.

The Host and Client compiler faces type-check independently. The package builds headlessly, the 197-node first-party runtime graph remains closed, and the Loader and complete-profile smokes pass. A headless Electron 43 Node-mode smoke executes the built trampoline and reaches the upstream ACL runner, which emits its signed missing-argument failure. Native Windows ACL confinement and the packaged `app.asar` path remain target-machine verification requirements.
The Host and Client compiler faces type-check independently. The package builds headlessly, the 201-node first-party runtime graph remains closed, and the Loader and complete-profile smokes pass. A headless Electron 43 Node-mode smoke executes the built trampoline and reaches the upstream ACL runner, which emits its signed missing-argument failure. Native Windows ACL confinement and the packaged `app.asar` path remain target-machine verification requirements.

## Alternatives considered

Expand All @@ -46,4 +46,4 @@ The Host and Client compiler faces type-check independently. The package builds

Both desktop presentation modes can use the ordinary upstream Windows PowerShell tool with its ACL confinement while the desktop remains an additive package. The adapter is intentionally narrow: changes to the upstream runner package, argv prefix, or provider identity fail closed instead of being guessed.

The installed executable intentionally retains Electron's RunAsNode capability for this internal child protocol. Windows release verification must execute read-only and workspace-write PowerShell commands from the packaged application and confirm the built trampoline, hidden console show state, native ACL setup, output capture, exit propagation, cancellation, and cleanup on the target operating system.
The installed executable intentionally retains Electron's RunAsNode capability for this internal child protocol. Windows release verification must execute read-only and workspace-write console commands from the packaged application and confirm that the GUI runner begins without a console, the built trampoline establishes a hidden inheritable console, native ACL setup remains effective, output is captured, exits propagate, cancellation works, and temporary ACL state is cleaned up on the target operating system.
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,25 @@ Status: implemented

上游 Windows sandbox 会把 ACL runner argv 组合为 `[process.execPath, runner.js, ...]`。在 DSH CLI 下这是正确的,因为 `process.execPath` 是 Node。DSH Desktop 会在 Electron 内启动同一批 Host plugin,此时该值在开发环境中是 Electron executable,在打包后则是已安装的 `DSH Desktop.exe`。使用 runner 路径作为参数启动该 executable,并不会建立上游 runner 所预期的普通 Node 进程,反而可能启动另一个 desktop 应用实例。

Windows sandbox 没有更弱的自动 provider fallback。因此,损坏的 runner 会让普通 workspace-write PowerShell 不可用;静默绕过 confinement 又会错误表示当前选择的权限模式。上游原生 spawn 还会有意省略 `CREATE_NO_WINDOW` 与 `CREATE_NEW_CONSOLE`,因为受限 child 使用任一 flag 都会在 DLL 初始化时失败。这对 console Host 是正确行为,但 Electron GUI Host 没有可供受限 PowerShell 继承的 console,因此 Windows 会为每次 shell 执行分配一个可见 console 窗口
Windows sandbox 没有更弱的自动 provider fallback。因此,损坏的 runner 会让普通 workspace-write PowerShell 不可用;静默绕过 confinement 又会错误表示当前选择的权限模式。上游原生 spawn 还会有意省略 `CREATE_NO_WINDOW` 与 `CREATE_NEW_CONSOLE`,因为受限 child 使用任一 flag 都会在 DLL 初始化时失败。这对 console Host 是正确行为,但 Electron Node-mode runner 启动时没有 console。在受影响的 Windows 环境中,受限 console child 被迫自行创建 console 时会在 DLL 初始化阶段以 `0xC0000142` 失败;即使某些环境能由 Windows 成功创建该 console,其行为仍与拥有 console 的 CLI 路径不同

## Decision

Desktop package 会把 `dsh-plugin-desktop/windows-pwsh-sandbox` 发布为现有 package 的 Host 子路径,而不是第二个 npm package。Windows desktop profile 会确认 `pwsh-sandbox` row 仍指向预期的上游 provider,保留它的平台 gate 与配置,禁用该 row,再插入 desktop 子路径。兼容模式与高级呈现模式使用同一个 Host profile,因此都会使用同一个执行 adapter。

`DesktopWindowsPwshSandbox` 会继承上游 `SandboxPwshExecutor`,并使用其受保护的 argv 执行方法。只有当平台为 Windows、Host 为 Electron、executable 等于 `process.execPath`,且下一个参数等于已解析的上游 Windows ACL runner 时,它才会改变调用。改写后的 argv 会在 executable 与上游 runner 之间插入私有 desktop trampoline。直接 PowerShell 执行,包括显式 `danger-full-access` 路径,都会原样通过。

适配后的 child 会获得一份克隆环境,其中会移除每个不区分大小写的 `ELECTRON_RUN_AS_NODE` key,再加入唯一的 `ELECTRON_RUN_AS_NODE=1`。Trampoline 会校验精确的上游 runner 路径,从自己的环境中移除该变量的全部形式,重建上游 module 预期的 argv,然后导入该 module。因此,受限 PowerShell 及其后代不会继承 Electron 的 Node-mode switch。
适配后的 child 会获得一份克隆环境,其中会移除每个不区分大小写的 `ELECTRON_RUN_AS_NODE` key,再加入唯一的 `ELECTRON_RUN_AS_NODE=1`。Trampoline 会校验精确的上游 runner 路径,从自己的环境中移除该变量的全部形式,然后检查 Windows console 状态。如果不存在 console 窗口,它会在仍未受限时调用 `AllocConsole()`,并立即通过 `ShowWindow(..., SW_HIDE)` 隐藏所分配的窗口;分配失败属于致命错误。完成这项 Host 设置后,trampoline 才会重建上游 module 预期的 argv 并导入该 module。因此,受限 PowerShell 及其后代会继承真实 console,同时不会继承 Electron 的 Node-mode switch。

Electron build 会显式启用 `runAsNode` fuse,因为该 child launch protocol 依赖此能力。Trampoline 会保留上游 `windows-acl-run` 失败签名,并在自身校验或导入失败时以代码 127 退出,因此现有 sandbox layer 会继续把 runner 启动失败分类为不可用并 fail closed。

Deploy root 会通过仓库自有的 Yarn patch 固定已发布的 rc.6 Windows ACL package。它的 pipe capture 与 inherited-stdio 两组 `STARTUPINFOW` 值都会把 `STARTF_USESHOWWINDOW` 和现有的 `STARTF_USESTDHANDLES` 组合起来,并把 `wShowWindow` 设为 `SW_HIDE`。Creation flag 保持不变,因此受限 child 仍会拥有或继承 console,并保留上游 token、job 与 stdio 行为,同时为新创建的 console 窗口请求隐藏的初始状态。Patch 会同时固定直接与传递的 rc.6 dependency descriptor;依赖测试会要求 sandbox provider 与 Desktop adapter 解析到同一个 patched package instance。
Deploy root 会通过仓库自有的 Yarn patch 固定已发布的 rc.6 Windows ACL package。它的 pipe capture 与 inherited-stdio 两组 `STARTUPINFOW` 值都会把 `STARTF_USESHOWWINDOW` 和现有的 `STARTF_USESTDHANDLES` 组合起来,并把 `wShowWindow` 设为 `SW_HIDE`。Creation flag 保持不变,因此受限 child 会继承 trampoline 的 console,并保留上游 token、job 与 stdio 行为。原生 show-state 仍作为纵深防护保留,但不会代替 Host console 分配。Patch 会同时固定直接与传递的 rc.6 dependency descriptor;依赖测试会要求 sandbox provider 与 Desktop adapter 解析到同一个 patched package instance。

## Verification

单元测试覆盖精确的 Windows Electron match、不变的非 Windows、普通 Node、错误 executable、错误 runner 与直接 PowerShell 调用、不区分大小写的环境移除、父进程环境隔离、trampoline 拒绝、公开 package export、build entry、已启用 fuse、准确的 Yarn patch resolution、两处隐藏 show-state 写入,以及没有使用 `CREATE_NO_WINDOW`。Profile 测试验证 Windows 替换、继承的配置与 gate、不变的 macOS 和 Linux 组合、不变的 subprocess 与 sandbox service,以及显式禁用或第三方 provider 会被保留。
单元测试覆盖精确的 Windows Electron match、不变的非 Windows、普通 Node、错误 executable、错误 runner 与直接 PowerShell 调用、不区分大小写的环境移除、父进程环境隔离、trampoline 拒绝、复用已有 console、console 分配与隐藏、分配失败、Host 设置顺序、公开 package export、build entry、已启用 fuse、准确的 Yarn patch resolution、两处隐藏 show-state 写入,以及没有使用 `CREATE_NO_WINDOW`。Profile 测试验证 Windows 替换、继承的配置与 gate、不变的 macOS 和 Linux 组合、不变的 subprocess 与 sandbox service,以及显式禁用或第三方 provider 会被保留。

Host 与 Client compiler face 会独立通过 typecheck。Package 可以 headless build,197 节点的第一方 runtime graph 仍然闭合,Loader 与完整 profile smoke 均通过。一次 headless Electron 43 Node-mode smoke 会执行已构建 trampoline 并到达上游 ACL runner,后者会输出带签名的缺少参数错误。原生 Windows ACL confinement 与打包后的 `app.asar` 路径仍需要在目标机器上验证。
Host 与 Client compiler face 会独立通过 typecheck。Package 可以 headless build,201 节点的第一方 runtime graph 仍然闭合,Loader 与完整 profile smoke 均通过。一次 headless Electron 43 Node-mode smoke 会执行已构建 trampoline 并到达上游 ACL runner,后者会输出带签名的缺少参数错误。原生 Windows ACL confinement 与打包后的 `app.asar` 路径仍需要在目标机器上验证。

## Alternatives considered

Expand All @@ -46,4 +46,4 @@ Host 与 Client compiler face 会独立通过 typecheck。Package 可以 headles

两种 desktop 呈现模式都可以使用普通上游 Windows PowerShell tool 及其 ACL confinement,desktop 仍保持为纯新增 package。该 adapter 有意保持窄范围:上游 runner package、argv prefix 或 provider 身份发生变化时会 fail closed,而不会猜测新行为。

已安装 executable 会有意保留 Electron RunAsNode 能力,用于这个内部 child protocol。Windows release 验证必须从打包后的应用执行 read-only 与 workspace-write PowerShell 命令,并在目标操作系统上确认已构建 trampoline、隐藏 console show state、原生 ACL 设置、输出捕获、退出传播、取消与清理行为
已安装 executable 会有意保留 Electron RunAsNode 能力,用于这个内部 child protocol。Windows release 验证必须从打包后的应用执行 read-only 与 workspace-write console 命令,并在目标操作系统上确认 GUI runner 启动时没有 console、已构建 trampoline 会建立隐藏且可继承的 console、原生 ACL 设置仍然有效、输出能够捕获、退出能够传播、取消可用,并且临时 ACL 状态会被清理
4 changes: 2 additions & 2 deletions dsh-plugin-desktop/README.i18n.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Bilingual-pair consistency record: the git blob hash of each side as of the last
# confirmed-consistent state. Both languages carry equal authority; after editing
# either side, bring the other along and record both new Git blob hashes below.
README.md: f38f398a9c6bf84fd1b6711963e87b83a43479cf
README.zh.md: ec3de0797943e65e7647381d3ef5fb9329671d3f
README.md: c4a98885d1e210e5910c5826d8540d5dbfdae41c
README.zh.md: 71f0203d09c91423a0f3d3af6c944ea4ac0923c3
Loading