From 8ff75622c601ab156f59aac70fd6a86d31d7b4f6 Mon Sep 17 00:00:00 2001 From: Huanqi Cao Date: Fri, 7 Aug 2026 00:15:55 +0800 Subject: [PATCH 01/11] feat(bundle): default Windows hosts to the pwsh shell stack win32 hosts booting a shipped profile now get pwsh-local as the ctx.bash executor and tool-pwsh as the shell tool through the base bundle's new windows.cordis.patch.yml platform layer, injected by the launcher between the bundle layers and the user layers on win32. bash-sandbox, tool-bash, permission, and ui-permission are disabled there: the POSIX-only executor cannot run on Windows, and dsh-permission requires a confining executor. Overriding the default is a composition decision through the user's cordis.patch.yml; there is no environment override channel. apps/cli and dsh-base re-declare dsh-pwsh-local/dsh-tool-pwsh so the profile module fallback links them for cold starts (the profiles rework had dropped them from the CLI closure). Promotes the windows-pwsh-default Agent Note from proposed to implemented and documents the platform layer in the base bundle README. --- ...026-08-01-pwsh-tool-and-executor.i18n.yaml | 4 +- .../2026-08-01-pwsh-tool-and-executor.md | 2 +- .../2026-08-01-pwsh-tool-and-executor.zh.md | 2 +- .../2026-08-01-windows-pwsh-default.i18n.yaml | 6 ++ .../2026-08-01-windows-pwsh-default.md | 42 ++++++++++++ .../2026-08-01-windows-pwsh-default.zh.md | 42 ++++++++++++ .../2026-08-05-pwsh-ui-bash-parity.i18n.yaml | 4 +- .../feature/2026-08-05-pwsh-ui-bash-parity.md | 2 +- .../2026-08-05-pwsh-ui-bash-parity.zh.md | 2 +- .../2026-08-01-windows-pwsh-default.i18n.yaml | 6 -- .../2026-08-01-windows-pwsh-default.md | 39 ----------- .../2026-08-01-windows-pwsh-default.zh.md | 39 ----------- apps/cli/package.json | 2 + apps/cli/src/dump-config.ts | 7 ++ apps/cli/src/profile-boot.ts | 28 +++++--- apps/cli/src/windows-shell.ts | 55 ++++++++++++++++ apps/cli/tests/windows-shell.spec.ts | 64 +++++++++++++++++++ packages/bundle/base/README.i18n.yaml | 4 +- packages/bundle/base/README.md | 5 +- packages/bundle/base/README.zh.md | 5 +- packages/bundle/base/package.json | 4 ++ packages/bundle/base/windows.cordis.patch.yml | 34 ++++++++++ pnpm-lock.yaml | 12 ++++ 23 files changed, 306 insertions(+), 104 deletions(-) create mode 100644 .agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.i18n.yaml create mode 100644 .agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.md create mode 100644 .agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.zh.md delete mode 100644 .agents/notes/proposed/feature/2026-08-01-windows-pwsh-default.i18n.yaml delete mode 100644 .agents/notes/proposed/feature/2026-08-01-windows-pwsh-default.md delete mode 100644 .agents/notes/proposed/feature/2026-08-01-windows-pwsh-default.zh.md create mode 100644 apps/cli/src/windows-shell.ts create mode 100644 apps/cli/tests/windows-shell.spec.ts create mode 100644 packages/bundle/base/windows.cordis.patch.yml diff --git a/.agents/notes/implemented/feature/2026-08-01-pwsh-tool-and-executor.i18n.yaml b/.agents/notes/implemented/feature/2026-08-01-pwsh-tool-and-executor.i18n.yaml index d4ec255235..efd7d56dae 100644 --- a/.agents/notes/implemented/feature/2026-08-01-pwsh-tool-and-executor.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-01-pwsh-tool-and-executor.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-01-pwsh-tool-and-executor.md -2026-08-01-pwsh-tool-and-executor.md: 7206f8ffe6640f8499f8453c40ab5846b23112c6 -2026-08-01-pwsh-tool-and-executor.zh.md: 5a48adb79fed209d2d2ecb9514fd51538491f04c +2026-08-01-pwsh-tool-and-executor.md: 77f0a13d0efa55c91b4e58ee2474ac6877947c80 +2026-08-01-pwsh-tool-and-executor.zh.md: 95c0d8f087f11d192350cf92ed866bb8a4ea33b4 diff --git a/.agents/notes/implemented/feature/2026-08-01-pwsh-tool-and-executor.md b/.agents/notes/implemented/feature/2026-08-01-pwsh-tool-and-executor.md index 7206f8ffe6..77f0a13d0e 100644 --- a/.agents/notes/implemented/feature/2026-08-01-pwsh-tool-and-executor.md +++ b/.agents/notes/implemented/feature/2026-08-01-pwsh-tool-and-executor.md @@ -17,7 +17,7 @@ Two new packages under `packages/bash/`: Windows vitest coverage is deliberately NOT part of this change: the repo's Windows CI lane owns build/static gates, and unit coverage runs on Linux, where both packages' suites run against a real `pwsh` (preinstalled on the GitHub-hosted runners) or self-skip when absent. The vitest `windowsUnsupportedPackages` exclusion narrows from `packages/bash/*` to the bash-requiring packages so the pwsh suites can also run natively on Windows dev machines. -The roadmap beyond this decision — defaulting Windows hosts to `pwsh` (bash off), and pwsh TUI/GUI rendering — is recorded separately as [a proposal](../../proposed/feature/2026-08-01-windows-pwsh-default.md). +The roadmap beyond this decision — defaulting Windows hosts to `pwsh` (bash off), and pwsh TUI/GUI rendering — is recorded separately as [the Windows pwsh default decision](2026-08-01-windows-pwsh-default.md). ## Alternatives considered diff --git a/.agents/notes/implemented/feature/2026-08-01-pwsh-tool-and-executor.zh.md b/.agents/notes/implemented/feature/2026-08-01-pwsh-tool-and-executor.zh.md index 5a48adb79f..95c0d8f087 100644 --- a/.agents/notes/implemented/feature/2026-08-01-pwsh-tool-and-executor.zh.md +++ b/.agents/notes/implemented/feature/2026-08-01-pwsh-tool-and-executor.zh.md @@ -17,7 +17,7 @@ harness 在每个平台只说一种 shell 方言:`bash`。Windows 主机只能 Windows vitest 覆盖率刻意不属本次改动:仓库的 Windows CI 通道负责构建/静态门禁,单元覆盖在 Linux 上运行,两个包的套件在那里以真实 `pwsh` 运行(GitHub 托管 runner 预装)或缺失时自行跳过。vitest 的 `windowsUnsupportedPackages` 排除从 `packages/bash/*` 收窄为真正需要 bash 的包,使 pwsh 套件也能在 Windows 开发机上原生运行。 -本决策之后的路线图——让 Windows 主机默认 `pwsh`(关闭 bash)与 pwsh TUI/GUI 渲染——另行记录为[提案](../../proposed/feature/2026-08-01-windows-pwsh-default.md)。 +本决策之后的路线图——让 Windows 主机默认 `pwsh`(关闭 bash)与 pwsh TUI/GUI 渲染——已落地为 [Windows 默认 pwsh 决策](2026-08-01-windows-pwsh-default.md)。 ## 备选方案 diff --git a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.i18n.yaml b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.i18n.yaml new file mode 100644 index 0000000000..e26d1ca354 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): 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 re-record with: +# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.md +2026-08-01-windows-pwsh-default.md: 1fa30ef757ccb81544479a08887355839fb86d46 +2026-08-01-windows-pwsh-default.zh.md: c6e1361dba29cff8340ef6e26bf071f81544642b diff --git a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.md b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.md new file mode 100644 index 0000000000..1fa30ef757 --- /dev/null +++ b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.md @@ -0,0 +1,42 @@ +# Agent Note: Windows defaults to pwsh + +Status: implemented + +English | [中文](2026-08-01-windows-pwsh-default.zh.md) + +## Problem + +The harness's shipped execution profile is bash-first on every platform. Windows hosts must install a bash shim (WSL or Git-Bash) or fall back to the POSIX-only `dsh-bash-local` behavior (hardcoded `bash -c` argv, process-group semantics); the model-facing bash tool teaches the bash dialect. The Windows-native foundation shipped in the [pwsh executor and tool decision](2026-08-01-pwsh-tool-and-executor.md) — a PowerShell implementation of the `ctx.bash` seam and a parity `pwsh` tool — but shipped compositions still mounted the bash stack on Windows, so a Windows host without a shim could not run the shipped shell. + +## Decision + +Windows hosts booting a shipped profile (`dsh web`, `dsh --profile headless`, one-shot tasks) get the PowerShell stack by default; POSIX hosts are unchanged. + +- **The platform layer is a data file, not a roster rewrite.** `@deepseek-ai/dsh-base` ships [`windows.cordis.patch.yml`](../../../../packages/bundle/base/windows.cordis.patch.yml) alongside its universal `cordis.patch.yml`: it disables `bash-sandbox`/`tool-bash` (the POSIX-only executor and its dialect tool), disables `permission`/`ui-permission` (dsh-permission requires a confining executor — presets bundle a sandbox mode the unconfined pwsh executor cannot honor; see its constructor guard — and the client knob would advertise a shell it cannot enforce), and inserts `pwsh-local`/`tool-pwsh`. The fs tools keep the sandbox policy and approval service, so file confinement and escalation still apply on Windows. +- **The launcher injects the layer by platform.** `apps/cli/src/windows-shell.ts` resolves it from the base bundle layer's `packageDir` between the bundle layers and the user layers on `win32` hosts, in every composition path (boot, config-only HMR recomposition, config dumps). Overriding the shipped default is a composition decision: a Windows host that prefers the bash stack re-enables the bash rows through its profile or home `cordis.patch.yml`. Custom profiles without the base bundle are skipped (they own their shell stack); a base bundle that ships no Windows shell patch fails loud. +- **Module resolution is restored for cold starts.** The profiles-rework CLI dropped the pwsh packages from `apps/cli`'s dependency closure, so `healProfilesModuleFallback` never linked them into `$DSH_HOME/profiles/node_modules` and a fresh Windows host could not resolve the inserted rows. `apps/cli` and `dsh-base` re-declare `dsh-pwsh-local`/`dsh-tool-pwsh`; the base bundle lists every row plugin as a dependency by house style. + +The pwsh GUI rendering stage (stage 2 of the original roadmap) shipped earlier with the [pwsh UI presentation matches bash decision](2026-08-05-pwsh-ui-bash-parity.md); the [pwsh tool bash parity decision](2026-08-02-pwsh-tool-bash-parity.md) ships the tool's surface. Nothing in this decision changes POSIX behavior. + +## Alternatives considered + +**Default Windows to pwsh inside `dsh-bash-local` (one executor, dialect switch).** Rejected for the same reason the executor decision rejected a mode switch: the executor's identity is the shell it spawns, and platform-gated composition is a deployment choice, not an executor config. + +**Ship the platform layer from `apps/cli` code instead of a bundle data file.** Rejected: the patch belongs next to the rows it replaces, in the bundle that owns them, so the shipped roster stays visible as composition data and dumps carry its provenance; the launcher contributes only the win32 gate. + +**Keep `permission`/`ui-permission` on Windows.** Rejected: `dsh-permission` hard-requires `ctx.bash.sandboxMode` and fails loud at load over an unconfined executor; making it tolerate an unconfined shell would advertise presets the shell cannot honor. File confinement keeps working through the fs stack. + +**Ship a `DSH_WINDOWS_SHELL` environment escape hatch.** Rejected: decisive behavior changes belong in composition config, which already overrides the platform layer row by id; a second override channel would split the single source of truth for roster decisions. + +## Consequences + +- A Windows host running a shipped `dsh` surface gets `pwsh` as its shell tool and PowerShell as the `ctx.bash` executor without configuration; `bash` is absent from the model-visible roster there (its tool row is disabled). +- POSIX hosts are unchanged: the platform layer never applies, and the bash stack remains the universal `cordis.patch.yml` rows. +- Windows hosts that prefer the bash stack (e.g. with WSL/Git-Bash on PATH) override the shipped default through their profile or home `cordis.patch.yml` — composition config is the one override channel. +- The permission switcher leaves the Windows roster; session permission facts pin the composition defaults through the approval service and sandbox policy. + +## Verification + +- Unit: `apps/cli/tests/windows-shell.spec.ts` pins the win32 default, the custom-profile skip, and the missing-patch failure, with the platform injected. +- Keyless: a win32 `dsh --profile --dump-config` shows the pwsh rows with `windows.cordis.patch.yml` provenance and the bash rows disabled; the POSIX dump (CI Linux) is unchanged. +- The real-composition smoke boots the web profile on win32 with the pwsh stack mounted (the exact roster this note describes). diff --git a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.zh.md b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.zh.md new file mode 100644 index 0000000000..c6e1361dba --- /dev/null +++ b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.zh.md @@ -0,0 +1,42 @@ +# Agent Note: Windows 默认改用 pwsh + +Status: implemented + +[English](2026-08-01-windows-pwsh-default.md) | 中文 + +## 问题 + +harness 交付的执行画像在每个平台都是 bash 优先。Windows 主机必须安装 bash 垫片(WSL 或 Git-Bash),或退回到仅 POSIX 的 `dsh-bash-local` 行为(硬编码 `bash -c` argv、进程组语义);面向模型的 bash 工具教的是 bash 方言。Windows 原生基础已随 [pwsh 执行器与工具决策](2026-08-01-pwsh-tool-and-executor.md) 交付——`ctx.bash` seam 的 PowerShell 实现与对等的 `pwsh` 工具——但交付组合在 Windows 上仍然挂载 bash 栈,没有垫片的 Windows 主机跑不了交付的 shell。 + +## 决策 + +启动交付 profile(`dsh web`、`dsh --profile headless`、一次性任务)的 Windows 主机默认获得 PowerShell 栈;POSIX 主机不变。 + +- **平台层是数据文件,不是清单重写。** `@deepseek-ai/dsh-base` 随通用 `cordis.patch.yml` 一起交付 [`windows.cordis.patch.yml`](../../../../packages/bundle/base/windows.cordis.patch.yml):它禁用 `bash-sandbox`/`tool-bash`(仅 POSIX 的执行器及其方言工具)、禁用 `permission`/`ui-permission`(dsh-permission 要求有限权能力的执行器——preset 捆绑的是无限制 pwsh 执行器无法兑现的 sandbox 模式;见其构造函数守卫——客户端旋钮会宣传一个它无法强制执行的 shell),并插入 `pwsh-local`/`tool-pwsh`。fs 工具保留 sandbox 策略与批准服务,因此 Windows 上的文件限制与升级仍然生效。 +- **启动器按平台注入该层。** `apps/cli/src/windows-shell.ts` 在 `win32` 主机上从 base bundle 层的 `packageDir` 解析它,置于 bundle 层与用户层之间,覆盖所有组合路径(启动、config-only HMR 重组合、配置转储)。覆盖交付默认是组合决策:偏好 bash 栈的 Windows 主机通过其 profile 或 home 的 `cordis.patch.yml` 重新启用 bash 行。未挂 base bundle 的自定义 profile 被跳过(它们自己拥有 shell 栈);base bundle 缺 `windows.cordis.patch.yml` 时 fail loud。 +- **冷启动的模块解析已恢复。** profiles 重构把 pwsh 包从 `apps/cli` 的依赖闭包中删掉了,`healProfilesModuleFallback` 因此从未把它们链接进 `$DSH_HOME/profiles/node_modules`,新 Windows 主机解析不到插入的行。`apps/cli` 与 `dsh-base` 重新声明 `dsh-pwsh-local`/`dsh-tool-pwsh`;按仓库惯例,base bundle 把每个行插件都列为依赖。 + +原路线图的阶段 2(pwsh GUI 渲染)已随 [pwsh UI 呈现与 bash 对齐决策](2026-08-05-pwsh-ui-bash-parity.md) 先行交付;[pwsh 工具与 bash 对齐决策](2026-08-02-pwsh-tool-bash-parity.md) 交付了工具表面。本决策不改变任何 POSIX 行为。 + +## 备选方案 + +**在 `dsh-bash-local` 内部让 Windows 默认 pwsh(一个执行器,方言开关)。** 否决,理由与执行器决策否决模式开关相同:执行器的身份就是它 spawn 的 shell,而按平台门控的组合是部署选择,不是执行器配置。 + +**从 `apps/cli` 代码而非 bundle 数据文件交付平台层。** 否决:patch 应放在它替换的行旁边、属于拥有这些行的 bundle,让交付清单作为组合数据保持可见、转储带有出处;启动器只贡献 win32 门控。 + +**在 Windows 上保留 `permission`/`ui-permission`。** 否决:`dsh-permission` 硬性要求 `ctx.bash.sandboxMode`,在无限制执行器上加载即 fail loud;让它容忍无限制 shell 会宣传 shell 无法兑现的 preset。文件限制继续经由 fs 栈生效。 + +**交付 `DSH_WINDOWS_SHELL` 环境变量逃生门。** 否决:决定性的行为变更应集中在组合配置中,而组合配置已能按行 id 覆盖平台层;第二条覆盖通道会分裂清单决策的单一事实来源。 + +## 后果 + +- 运行交付版 `dsh` 表面的 Windows 主机无需配置即获得 `pwsh` 作为 shell 工具、PowerShell 作为 `ctx.bash` 执行器;那里的模型可见清单中没有 `bash`(其工具行被禁用)。 +- POSIX 主机不变:平台层永不生效,bash 栈仍是通用 `cordis.patch.yml` 的行。 +- 偏好 bash 栈的 Windows 主机(例如 PATH 上有 WSL/Git-Bash 时)通过其 profile 或 home 的 `cordis.patch.yml` 覆盖交付默认——组合配置是唯一的覆盖通道。 +- 权限切换器离开 Windows 清单;会话权限事实通过批准服务与 sandbox 策略固定组合默认值。 + +## 验证 + +- 单元:`apps/cli/tests/windows-shell.spec.ts` 固定 win32 默认、自定义 profile 跳过与缺文件失败,平台注入。 +- Keyless:win32 上的 `dsh --profile --dump-config` 显示带 `windows.cordis.patch.yml` 出处的 pwsh 行、被禁用的 bash 行;POSIX 转储(CI Linux)不变。 +- 真实组合冒烟在 win32 上启动 web profile,pwsh 栈挂载成功(即本笔记描述的确切清单)。 diff --git a/.agents/notes/implemented/feature/2026-08-05-pwsh-ui-bash-parity.i18n.yaml b/.agents/notes/implemented/feature/2026-08-05-pwsh-ui-bash-parity.i18n.yaml index dcb3a9406b..204560cfb5 100644 --- a/.agents/notes/implemented/feature/2026-08-05-pwsh-ui-bash-parity.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-05-pwsh-ui-bash-parity.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-05-pwsh-ui-bash-parity.md -2026-08-05-pwsh-ui-bash-parity.md: 6bbdb0e6bc69ef1af03a6a9146f83b84754cb2a6 -2026-08-05-pwsh-ui-bash-parity.zh.md: 75f3a3ddec002acaa1755c81114b0f122ab80593 +2026-08-05-pwsh-ui-bash-parity.md: 23693c833a779da7a0f52ab1c959bf7eb8649310 +2026-08-05-pwsh-ui-bash-parity.zh.md: ef44e9ff043c0a533d2cc34bb776c6eb3f0566f8 diff --git a/.agents/notes/implemented/feature/2026-08-05-pwsh-ui-bash-parity.md b/.agents/notes/implemented/feature/2026-08-05-pwsh-ui-bash-parity.md index 6bbdb0e6bc..23693c833a 100644 --- a/.agents/notes/implemented/feature/2026-08-05-pwsh-ui-bash-parity.md +++ b/.agents/notes/implemented/feature/2026-08-05-pwsh-ui-bash-parity.md @@ -6,7 +6,7 @@ English | [中文](2026-08-05-pwsh-ui-bash-parity.zh.md) ## Problem -The [pwsh tool bash parity decision](../../implemented/feature/2026-08-02-pwsh-tool-bash-parity.md) made `dsh-tool-pwsh` behaviorally interchangeable with `dsh-tool-bash` for execution, markers, and background tasks, but explicitly deferred the human-visible half: a completed pwsh foreground call presented as a generic `console`-fenced card while the bash tool's completed call presented as a terminal card with a parsed exit-status pill. The roadmap that owned this gap ([Windows defaults to pwsh](../../proposed/feature/2026-08-01-windows-pwsh-default.md)) named "pwsh TUI/GUI rendering" as stage 2 — but the TUI package was removed ([`ed30088adb`](https://github.com/deepseek-harness/deepseek-harness/commit/ed30088adb)), leaving the Web surface as the only UI the gap affects. +The [pwsh tool bash parity decision](../../implemented/feature/2026-08-02-pwsh-tool-bash-parity.md) made `dsh-tool-pwsh` behaviorally interchangeable with `dsh-tool-bash` for execution, markers, and background tasks, but explicitly deferred the human-visible half: a completed pwsh foreground call presented as a generic `console`-fenced card while the bash tool's completed call presented as a terminal card with a parsed exit-status pill. The roadmap that owned this gap ([Windows defaults to pwsh](../../implemented/feature/2026-08-01-windows-pwsh-default.md)) named "pwsh TUI/GUI rendering" as stage 2 — but the TUI package was removed ([`ed30088adb`](https://github.com/deepseek-harness/deepseek-harness/commit/ed30088adb)), leaving the Web surface as the only UI the gap affects. ## Decision diff --git a/.agents/notes/implemented/feature/2026-08-05-pwsh-ui-bash-parity.zh.md b/.agents/notes/implemented/feature/2026-08-05-pwsh-ui-bash-parity.zh.md index 75f3a3ddec..ef44e9ff04 100644 --- a/.agents/notes/implemented/feature/2026-08-05-pwsh-ui-bash-parity.zh.md +++ b/.agents/notes/implemented/feature/2026-08-05-pwsh-ui-bash-parity.zh.md @@ -6,7 +6,7 @@ Status: implemented ## Problem -[pwsh 工具与 bash 对齐决策](../../implemented/feature/2026-08-02-pwsh-tool-bash-parity.md) 让 `dsh-tool-pwsh` 在执行、marker 与后台任务上行为可互换,但明确推迟了面向人类的一半:完成的 pwsh 前台调用呈现为通用 `console` 围栏卡片,而 bash 工具的完成调用呈现为带解析退出状态 pill 的 terminal 卡。拥有此缺口的路线图([Windows 默认改用 pwsh](../../proposed/feature/2026-08-01-windows-pwsh-default.md))把 "pwsh TUI/GUI 渲染" 列为阶段 2——但 TUI 包已被移除([`ed30088adb`](https://github.com/deepseek-harness/deepseek-harness/commit/ed30088adb)),Web 表面成为该缺口唯一影响的 UI。 +[pwsh 工具与 bash 对齐决策](../../implemented/feature/2026-08-02-pwsh-tool-bash-parity.md) 让 `dsh-tool-pwsh` 在执行、marker 与后台任务上行为可互换,但明确推迟了面向人类的一半:完成的 pwsh 前台调用呈现为通用 `console` 围栏卡片,而 bash 工具的完成调用呈现为带解析退出状态 pill 的 terminal 卡。拥有此缺口的路线图([Windows 默认改用 pwsh](../../implemented/feature/2026-08-01-windows-pwsh-default.md))把 "pwsh TUI/GUI 渲染" 列为阶段 2——但 TUI 包已被移除([`ed30088adb`](https://github.com/deepseek-harness/deepseek-harness/commit/ed30088adb)),Web 表面成为该缺口唯一影响的 UI。 ## Decision diff --git a/.agents/notes/proposed/feature/2026-08-01-windows-pwsh-default.i18n.yaml b/.agents/notes/proposed/feature/2026-08-01-windows-pwsh-default.i18n.yaml deleted file mode 100644 index fa7ea8e141..0000000000 --- a/.agents/notes/proposed/feature/2026-08-01-windows-pwsh-default.i18n.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): 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 re-record with: -# pnpm run verify-translation-pairing --write .agents/notes/proposed/feature/2026-08-01-windows-pwsh-default.md -2026-08-01-windows-pwsh-default.md: 1c3ccef23bb5cd9bc37237bd69aac2e2c56649a8 -2026-08-01-windows-pwsh-default.zh.md: 3958d21eb8a9d306009b11d6e9806a1654a8958e diff --git a/.agents/notes/proposed/feature/2026-08-01-windows-pwsh-default.md b/.agents/notes/proposed/feature/2026-08-01-windows-pwsh-default.md deleted file mode 100644 index 1c3ccef23b..0000000000 --- a/.agents/notes/proposed/feature/2026-08-01-windows-pwsh-default.md +++ /dev/null @@ -1,39 +0,0 @@ -# Agent Note: Windows defaults to pwsh (roadmap) - -Status: proposed - -English | [中文](2026-08-01-windows-pwsh-default.zh.md) - -## Problem - -The harness's shipped execution profile is bash-first on every platform. Windows hosts must install a bash shim (WSL or Git-Bash) or fall back to the POSIX-only `dsh-bash-local` behavior; the model-facing bash tool teaches the bash dialect, and the TUI/Web surfaces render terminal output in bash-shaped expectations. The first Windows-native foundation shipped in the [pwsh executor and tool decision](../../implemented/feature/2026-08-01-pwsh-tool-and-executor.md): a PowerShell implementation of the `ctx.bash` seam and a parity `pwsh` tool — but nothing yet defaults Windows hosts to them. - -## Proposal - -Two follow-up stages, each independently shippable. The former stage 2 (bash-tool parity twin) shipped with the [pwsh tool bash parity decision](../../implemented/feature/2026-08-02-pwsh-tool-bash-parity.md): `tool-pwsh` now mirrors `tool-bash` for foreground and background work minus the sandbox surface, shares the `DSH_*` environment through `dsh-bash-env`, and carries a keyless application snapshot of its assembled surface. - -1. **Windows default composition** — the shipped CLI compositions mount `dsh-pwsh-local` as the `ctx.bash` executor and `dsh-tool-pwsh` as the model-facing shell tool on Windows hosts (bash unmounted there), while POSIX hosts keep the bash stack. This is a composition/roster decision in `base.cordis.yml` and the surface overlays, gated by platform; it makes the shipped Windows experience PowerShell-native end to end. -2. **pwsh GUI rendering** — the Web surface renders pwsh calls with the bash-shaped terminal presentation (terminal card with exit-status pill), the counterpart of the bash terminal cards. Shipped in the [pwsh UI presentation matches bash decision](../../implemented/feature/2026-08-05-pwsh-ui-bash-parity.md) with a keyless web lane; the TUI was removed, so no terminal twin remains. A PowerShell-aware presentation beyond bash parity (native path display, `$env:` facts) remains unclaimed. - -The stages are ordered by dependency only where one exists: the rendering stage shipped first with the [pwsh UI presentation matches bash decision](../../implemented/feature/2026-08-05-pwsh-ui-bash-parity.md) because it is platform-independent and its keyless web lane runs on any host, while the Windows default composition remains the only unshipped stage. Nothing in this proposal changes POSIX behavior. - -## Alternatives considered - -**Default Windows to pwsh inside `dsh-bash-local` (one executor, dialect switch).** Rejected for the same reason the executor decision rejected a mode switch: the executor's identity is the shell it spawns, and platform-gated composition is a deployment choice, not an executor config. - -**Ship the Windows default in the same change as the executor/tool.** Rejected: the roster change needs its own evidence (what breaks when the shipped Windows tree stops mounting bash, which tools depend on bash semantics), and it belongs to a composition decision with the approval/PTY surface visible. - -**Keep bash on Windows via a shim and skip PowerShell defaults.** Rejected: it perpetuates the install-tax and the dialect mismatch the roadmap exists to remove; the shim is a deployment requirement, not a product behavior. - -## Acceptance criteria - -- A Windows host running the shipped `dsh` TUI/Web gets `pwsh` as its shell tool and PowerShell as the `ctx.bash` executor without configuration, and `bash` is absent from the model-visible roster there. -- POSIX hosts are byte-for-byte unaffected (same roster, same executor). -- The shipped-composition e2es assert the platform-gated roster on both families. -- Stage 1 lands with the keyless pwsh-tool snapshot already in place from the parity change; stage 2 landed with the web `pwsh-terminal` rendering lane (the TUI's removal left no terminal surface to snapshot). - -## Risks - -- **Bash-dependent composition rows** — any shipped plugin that assumes `bash` semantics (hook bridges executing shell hooks, workspace tooling) must be audited per stage; the audit may force a staged rollout rather than one switch. -- **Windows CI coverage gap** — unit coverage runs on Linux; Windows-only regressions in the pwsh stack surface through the Windows build/static lane and e2es, which must be extended per stage rather than assumed. -- **Rendering conventions** — the bash-shaped terminal twin shipped with the Web lane; a PowerShell-aware presentation beyond bash parity (native path display, `$env:` facts) remains a UI design decision with snapshot surface, deferred with stage 1. diff --git a/.agents/notes/proposed/feature/2026-08-01-windows-pwsh-default.zh.md b/.agents/notes/proposed/feature/2026-08-01-windows-pwsh-default.zh.md deleted file mode 100644 index 3958d21eb8..0000000000 --- a/.agents/notes/proposed/feature/2026-08-01-windows-pwsh-default.zh.md +++ /dev/null @@ -1,39 +0,0 @@ -# Agent Note: Windows 默认改用 pwsh(路线图) - -Status: proposed - -[English](2026-08-01-windows-pwsh-default.md) | 中文 - -## 问题 - -harness 交付的执行画像在每个平台都是 bash 优先。Windows 主机必须安装 bash 垫片(WSL 或 Git-Bash),或退回到仅 POSIX 的 `dsh-bash-local` 行为;面向模型的 bash 工具教的是 bash 方言,TUI/Web 表面以 bash 形状的预期渲染终端输出。第一块 Windows 原生基础已随 [pwsh 执行器与工具决策](../../implemented/feature/2026-08-01-pwsh-tool-and-executor.md) 交付:`ctx.bash` seam 的 PowerShell 实现与对等的 `pwsh` 工具——但还没有任何东西让 Windows 主机默认使用它们。 - -## 提案 - -两个阶段,各自可独立交付。原阶段 2(bash 工具对等孪生)已随 [pwsh 工具与 bash 对齐决策](../../implemented/feature/2026-08-02-pwsh-tool-bash-parity.md) 交付:`tool-pwsh` 现在在前台与后台工作(减 sandbox 面)上镜像 `tool-bash`,通过 `dsh-bash-env` 共享 `DSH_*` 环境,并携带其组装表面的 keyless 应用快照。 - -1. **Windows 默认组合**——交付的 CLI 组合在 Windows 主机上挂载 `dsh-pwsh-local` 作为 `ctx.bash` 执行器、`dsh-tool-pwsh` 作为面向模型的 shell 工具(那里不挂载 bash),POSIX 主机保持 bash 栈。这是 `base.cordis.yml` 与 surface 覆盖层里按平台门控的组合/清单决策;它让交付的 Windows 体验端到端 PowerShell 原生。 -2. **pwsh GUI 渲染**——Web 表面以 bash 形状的终端呈现渲染 pwsh 调用(带退出状态 pill 的 terminal 卡),即 bash 终端卡片的对应物。已随 [pwsh UI 呈现与 bash 对齐决策](../../implemented/feature/2026-08-05-pwsh-ui-bash-parity.md) 及 keyless web 通道交付;TUI 已移除,不再有终端孪生。超出 bash 对齐的 PowerShell 感知呈现(原生路径显示、`$env:` 实情)仍无人认领。 - -各阶段仅在有依赖关系时排序:渲染阶段已随 [pwsh UI 呈现与 bash 对齐决策](../../implemented/feature/2026-08-05-pwsh-ui-bash-parity.md) 先行交付(平台无关,其 keyless web 通道可在任意宿主运行),而 Windows 默认组合仍是唯一未交付的阶段。本提案不改变任何 POSIX 行为。 - -## 备选方案 - -**在 `dsh-bash-local` 内部让 Windows 默认 pwsh(一个执行器,方言开关)。** 否决,理由与执行器决策否决模式开关相同:执行器的身份就是它 spawn 的 shell,而按平台门控的组合是部署选择,不是执行器配置。 - -**把 Windows 默认与执行器/工具一起交付。** 否决:清单变更需要自己的证据(交付的 Windows 树停挂 bash 后什么会坏、哪些工具依赖 bash 语义),并且它属于带批准/PTY 表面可见的组合决策。 - -**用垫片在 Windows 上保留 bash,跳过 PowerShell 默认。** 否决:这延续了安装税与路线图要消除的方言错配;垫片是部署要求,不是产品行为。 - -## 验收标准 - -- 运行交付版 `dsh` TUI/Web 的 Windows 主机无需配置即获得 `pwsh` 作为其 shell 工具、PowerShell 作为 `ctx.bash` 执行器,且那里的模型可见清单中没有 `bash`。 -- POSIX 主机逐字节不受影响(清单相同,执行器相同)。 -- 交付组合 e2e 在两个平台族上断言按平台门控的清单。 -- 阶段 1 落地时,parity 变更带来的 keyless pwsh 工具快照已经就位;阶段 2 已随 web `pwsh-terminal` 渲染通道落地(TUI 的移除让终端表面无快照可做)。 - -## 风险 - -- **依赖 bash 的组合行**——任何假设 bash 语义的交付插件(执行 shell hooks 的 hooks 桥、工作区工具)必须按阶段审计;审计可能迫使分阶段推出而非一次切换。 -- **Windows CI 覆盖缺口**——单元覆盖在 Linux 上运行;pwsh 栈里仅 Windows 的回归通过 Windows 构建/静态通道与 e2e 浮出,必须按阶段扩展而不是想当然。 -- **渲染约定**——bash 形状的终端孪生已随 web 通道交付;超出 bash 对齐的 PowerShell 感知呈现(原生路径显示、`$env:` 实情)仍是带快照表面的 UI 设计决策,随阶段 1 一起延期。 diff --git a/apps/cli/package.json b/apps/cli/package.json index 87677ce1f9..11078458e6 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -24,11 +24,13 @@ "@deepseek-ai/dsh-paths": "workspace:^", "@deepseek-ai/dsh-pty": "workspace:^", "@deepseek-ai/dsh-pty-local": "workspace:^", + "@deepseek-ai/dsh-pwsh-local": "workspace:^", "@deepseek-ai/dsh-session-reference": "workspace:^", "@deepseek-ai/dsh-tmux-context": "workspace:^", "@deepseek-ai/dsh-tool-ask-user": "workspace:^", "@deepseek-ai/dsh-tool-bash-persistent": "workspace:^", "@deepseek-ai/dsh-tool-cordis": "workspace:^", + "@deepseek-ai/dsh-tool-pwsh": "workspace:^", "@deepseek-ai/dsh-web-app": "workspace:^", "commander": "^15.0.0", "cordis": "^4.0.0-rc.7", diff --git a/apps/cli/src/dump-config.ts b/apps/cli/src/dump-config.ts index 9de7a55f60..29605878de 100644 --- a/apps/cli/src/dump-config.ts +++ b/apps/cli/src/dump-config.ts @@ -15,6 +15,7 @@ import { type ConfigDumpLayer, } from '@deepseek-ai/dsh-app-boot' import { homePatchPath, prepareProfile, PROFILE_ROOT_FILENAME } from './profile-boot.ts' +import { resolveWindowsShellLayer } from './windows-shell.ts' const NAME = 'dsh' @@ -33,6 +34,12 @@ export function runDumpConfig(profile: string, defaultOnly: boolean, patches: re label: layer.packageName, patches: layer.patches, })) + // The win32 shell platform layer rides between bundles and user layers, + // exactly where the boot applies it. + const windowsShellLayer = resolveWindowsShellLayer(process.platform, loaded.layers, NAME) + if (windowsShellLayer !== undefined) { + layers.push({ label: windowsShellLayer.label, patches: windowsShellLayer.patches }) + } if (!defaultOnly) { if (existsSync(loaded.patchPath)) { layers.push({ label: loaded.patchPath, patches: loaded.patches }) diff --git a/apps/cli/src/profile-boot.ts b/apps/cli/src/profile-boot.ts index b69a938213..7115f1208a 100644 --- a/apps/cli/src/profile-boot.ts +++ b/apps/cli/src/profile-boot.ts @@ -27,6 +27,7 @@ import { import { resolveDshHome } from '@deepseek-ai/dsh-paths' import type { HeadlessIo } from '@deepseek-ai/dsh-headless' import { createProcessShutdown, type ProcessShutdown } from './process-shutdown.ts' +import { resolveWindowsShellLayer } from './windows-shell.ts' const NAME = 'dsh' @@ -103,6 +104,8 @@ interface ComposedProfile { profile: Profile /** Bundle layers concatenated — the part below the user layers on a live reload. */ bundlePatches: PatchOptions[] + /** The win32 shell platform layer (the base bundle's `windows.cordis.patch.yml`), between bundles and user layers. */ + windowsShellPatches: PatchOptions[] /** The home-level user layer (`$DSH_HOME/cordis.patch.yml`), applied after the profile's own. */ homePatches: PatchOptions[] /** Layers above the user layers on a live reload: --patch overlays, flag patches, the telemetry switch. */ @@ -117,16 +120,23 @@ interface ComposedProfile { /** The full patch stack of one composed profile, in application order. */ function allPatches(composed: ComposedProfile): PatchOptions[] { - return [...composed.bundlePatches, ...composed.profile.patches, ...composed.homePatches, ...composed.overlayAndFlags] + return [ + ...composed.bundlePatches, + ...composed.windowsShellPatches, + ...composed.profile.patches, + ...composed.homePatches, + ...composed.overlayAndFlags, + ] } /** * Load `name` and compose its effective patch stack: bundle layers in - * `dsh.profile.bundles` order, the profile's user layer, the home-level user layer - * (`$DSH_HOME/cordis.patch.yml` — machine-local preferences that apply to - * every profile, so it outranks the per-profile layer), `--patch` overlays, - * then flag patches derived from the composed rows, then the telemetry - * switch. + * `dsh.profile.bundles` order, the win32 shell platform layer (when the host + * is Windows), the + * profile's user layer, the home-level user layer (`$DSH_HOME/cordis.patch.yml` + * — machine-local preferences that apply to every profile, so it outranks the + * per-profile layer), `--patch` overlays, then flag patches derived from the + * composed rows, then the telemetry switch. * @param name - the profile name. * @param patchFiles - `--patch` overlay paths, in argv order. * @param deriveFlagPatches - launcher hook turning composed rows into flag patches. @@ -141,14 +151,15 @@ function composeProfile( const homePatches = loadOptionalPatches(NAME, homePatchPath()) ?? [] const overlays = patchFiles.flatMap(file => loadOverlayPatches(NAME, resolve(file))) const bundlePatches = profile.layers.flatMap(layer => layer.patches) + const windowsShellPatches = resolveWindowsShellLayer(process.platform, profile.layers, NAME)?.patches ?? [] const rows = new Map() - for (const row of composeEntries([bundlePatches, profile.patches, homePatches, overlays])) { + for (const row of composeEntries([bundlePatches, windowsShellPatches, profile.patches, homePatches, overlays])) { if (typeof row.id === 'string') rows.set(row.id, row) } const overlayAndFlags = [...overlays, ...deriveFlagPatches(rows)] const telemetryPatch = resolveTelemetryPatch(process.env.DSH_TELEMETRY_DISABLED, rows.has(TELEMETRY_ROW_ID)) if (telemetryPatch !== undefined) overlayAndFlags.push(telemetryPatch) - return { profile, bundlePatches, homePatches, overlayAndFlags, rows } + return { profile, bundlePatches, windowsShellPatches, homePatches, overlayAndFlags, rows } } /** Options for {@link runProfile}. */ @@ -215,6 +226,7 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con // removing the override could never revert the row to the bundle default. const composeLive = (): PatchOptions[] => structuredClone([ ...composed.bundlePatches, + ...composed.windowsShellPatches, ...loadOptionalPatches(NAME, composed.profile.patchPath) ?? [], ...loadOptionalPatches(NAME, homePatchPath()) ?? [], ...composed.overlayAndFlags, diff --git a/apps/cli/src/windows-shell.ts b/apps/cli/src/windows-shell.ts new file mode 100644 index 0000000000..425699ef4f --- /dev/null +++ b/apps/cli/src/windows-shell.ts @@ -0,0 +1,55 @@ +/** + * The Windows shell platform layer: on win32 hosts the shipped profile + * compositions swap the POSIX-only bash stack for the PowerShell stack + * (`@deepseek-ai/dsh-pwsh-local` + `@deepseek-ai/dsh-tool-pwsh`), matching + * the Windows-pwsh-default roadmap. The layer is the base bundle's + * `windows.cordis.patch.yml`, injected by the launcher between the bundle + * layers and the user layers so a user patch can still override it — the + * only override channel is composition config, like every other roster + * decision. POSIX hosts never receive the layer. + * @module @deepseek-ai/dsh/windows-shell + */ + +import { existsSync } from 'node:fs' +import { join } from 'node:path' +import type { PatchOptions } from '@cordisjs/plugin-include' +import { loadOverlayPatches, type ProfileLayer } from '@deepseek-ai/dsh-app-boot' + +/** The base bundle whose package carries the Windows shell patch. */ +export const BASE_BUNDLE = '@deepseek-ai/dsh-base' + +/** The Windows shell patch filename inside the base bundle package. */ +export const WINDOWS_SHELL_PATCH_FILENAME = 'windows.cordis.patch.yml' + +/** One Windows shell platform layer: its patch file and parsed patches. */ +export interface WindowsShellLayer { + /** The patch file path, used as the config-dump provenance label. */ + label: string + /** The parsed patch entries, applied after the bundle layers. */ + patches: PatchOptions[] +} + +/** + * Resolve the Windows shell platform layer for a profile composition. + * @param platform - the host platform (`process.platform` at call sites). + * @param layers - the profile's bundle layers, in application order. + * @param binName - the diagnostic prefix on thrown errors (`dsh`). + * @returns the pwsh layer on win32, else `undefined`. A custom profile that + * mounts no base bundle is skipped (it owns its shell stack); a base + * bundle that ships no Windows shell patch fails loud — the shipped + * package always carries it, so a miss is a broken installation. + */ +export function resolveWindowsShellLayer( + platform: NodeJS.Platform, + layers: readonly ProfileLayer[], + binName: string, +): WindowsShellLayer | undefined { + if (platform !== 'win32') return undefined + const base = layers.find(layer => layer.packageName === BASE_BUNDLE) + if (base === undefined) return undefined + const label = join(base.packageDir, WINDOWS_SHELL_PATCH_FILENAME) + if (!existsSync(label)) { + throw new Error(`${binName}: ${BASE_BUNDLE} ships no ${WINDOWS_SHELL_PATCH_FILENAME}`) + } + return { label, patches: loadOverlayPatches(binName, label) } +} diff --git a/apps/cli/tests/windows-shell.spec.ts b/apps/cli/tests/windows-shell.spec.ts new file mode 100644 index 0000000000..f0cfddbaae --- /dev/null +++ b/apps/cli/tests/windows-shell.spec.ts @@ -0,0 +1,64 @@ +import { afterEach, describe, expect, it } from 'vitest' +import { mkdtempSync, writeFileSync, rmSync, mkdirSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import type { ProfileLayer } from '@deepseek-ai/dsh-app-boot' +import { + BASE_BUNDLE, + resolveWindowsShellLayer, + WINDOWS_SHELL_PATCH_FILENAME, +} from '../src/windows-shell.ts' + +const WINDOWS_PATCH = `- id: bash-sandbox + disabled: true +- insert: + - id: pwsh-local + name: '@deepseek-ai/dsh-pwsh-local' +` + +/** One fake bundle layer rooted in a temp directory. */ +function fakeLayer(packageName: string, dir: string): ProfileLayer { + return { packageName, packageDir: dir, patchPath: join(dir, 'cordis.patch.yml'), patches: [] } +} + +/** A base bundle layer whose package carries the Windows shell patch. */ +function baseLayerWithPatch(dir: string): ProfileLayer { + writeFileSync(join(dir, WINDOWS_SHELL_PATCH_FILENAME), WINDOWS_PATCH) + return fakeLayer(BASE_BUNDLE, dir) +} + +describe('resolveWindowsShellLayer', () => { + let base: string + afterEach(() => { if (base !== undefined) rmSync(base, { recursive: true, force: true }) }) + const tempBase = (): string => { + base = mkdtempSync(join(tmpdir(), 'dsh-windows-shell-')) + return base + } + + it('never applies on POSIX hosts', () => { + expect(resolveWindowsShellLayer('linux', [baseLayerWithPatch(tempBase())], 'dsh')).toBeUndefined() + expect(resolveWindowsShellLayer('darwin', [baseLayerWithPatch(tempBase())], 'dsh')).toBeUndefined() + }) + + it('defaults Windows hosts to the pwsh platform layer', () => { + const layer = resolveWindowsShellLayer('win32', [baseLayerWithPatch(tempBase())], 'dsh') + expect(layer).toBeDefined() + expect(layer?.label.endsWith(WINDOWS_SHELL_PATCH_FILENAME)).toBe(true) + expect(layer?.patches).toEqual([ + { id: 'bash-sandbox', disabled: true }, + { insert: [{ id: 'pwsh-local', name: '@deepseek-ai/dsh-pwsh-local' }] }, + ]) + }) + + it('skips custom profiles without a base bundle', () => { + const other = fakeLayer('@deepseek-ai/dsh-custom', tempBase()) + expect(resolveWindowsShellLayer('win32', [other], 'dsh')).toBeUndefined() + }) + + it('fails loud when the base bundle ships no Windows shell patch', () => { + const base = tempBase() + mkdirSync(base, { recursive: true }) + expect(() => resolveWindowsShellLayer('win32', [fakeLayer(BASE_BUNDLE, base)], 'dsh')) + .toThrow(/@deepseek-ai\/dsh-base ships no windows\.cordis\.patch\.yml/) + }) +}) diff --git a/packages/bundle/base/README.i18n.yaml b/packages/bundle/base/README.i18n.yaml index 2ae7df0bdc..a47ac9cbc7 100644 --- a/packages/bundle/base/README.i18n.yaml +++ b/packages/bundle/base/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/bundle/base/README.md -README.md: 301d397d4c87687b382665cf63af47ab5e3f85be -README.zh.md: f007bc817b6cbad84725fe8abe72549cf67d8cd7 +README.md: a4f220c956f7f67d7810827d4488d54a071ad3d6 +README.zh.md: eaf96f8c0640481163e7a1c1533a07e35ecc294f diff --git a/packages/bundle/base/README.md b/packages/bundle/base/README.md index 301d397d4c..a4f220c956 100644 --- a/packages/bundle/base/README.md +++ b/packages/bundle/base/README.md @@ -2,7 +2,9 @@ English | [中文](README.zh.md) -The shared dsh core as a profile bundle: [`cordis.patch.yml`](cordis.patch.yml) inserts every base plugin row — model adapters, tools, persistence, policy, settings/credentials, repository Plugins, telemetry — over the empty profile root, as the first layer of every profile's `dsh.profile.bundles` list. Later bundle layers (e.g. [`dsh-web-app`](../web-app/README.md)) and the user's profile `cordis.patch.yml` override these rows by id; a patch replaces a row's whole `config`, so mode-specific values live in mode bundles, not here. The package has no runtime API; the profile composer resolves the patch through the `dsh.bundle.patch` manifest field, never through code. +The shared dsh core as a profile bundle: [`cordis.patch.yml`](cordis.patch.yml) inserts every base plugin row — model adapters, tools, persistence, policy, settings/credentials, repository Plugins, telemetry — over the empty profile root, as the first layer of every profile's `dsh.profile.bundles` list. Later bundle layers (e.g. [`dsh-web-app`](../web-app/README.md)) and the user's profile `cordis.patch.yml` override these rows by id; a patch replaces a row's whole `config`, so mode-specific values live in mode bundles, not here. The package has no runtime API; the profile composer resolves the universal patch through the `dsh.bundle.patch` manifest field, and the launcher reads the Windows platform layer below from code on win32 hosts. + +Windows hosts booting a shipped profile additionally receive [`windows.cordis.patch.yml`](windows.cordis.patch.yml): it disables the POSIX-only bash executor/tool and the permission stack (dsh-permission requires a confining executor), and inserts the PowerShell executor and tool (`@deepseek-ai/dsh-pwsh-local`, `@deepseek-ai/dsh-tool-pwsh`). The launcher applies it between the bundle layers and the user layers on win32 hosts; a Windows host that prefers the bash stack overrides these rows through its profile or home `cordis.patch.yml`. POSIX hosts never receive it. The row set and its rationale are documented inline in the patch file; the [generated composition graph](../../../apps/cli/composition.md) renders it. @@ -17,3 +19,4 @@ None directly; each inserted row's package owns its effect. ## Known Limitations and Deferred Work - **A patch replaces whole row configs** — profile overrides must restate every field a row keeps; there is no deep-merge layer. +- **Windows loses the permission switcher** — `dsh-permission` hard-requires a confining `ctx.bash` executor, so the Windows platform layer disables `permission`/`ui-permission` with the bash stack. The fs tools keep the sandbox policy and the approval service, so file confinement and escalation still apply on Windows. diff --git a/packages/bundle/base/README.zh.md b/packages/bundle/base/README.zh.md index f007bc817b..eaf96f8c06 100644 --- a/packages/bundle/base/README.zh.md +++ b/packages/bundle/base/README.zh.md @@ -2,7 +2,9 @@ [English](README.md) | 中文 -以 profile 组合包形式交付的共享 dsh 核心:[`cordis.patch.yml`](cordis.patch.yml) 在空的 profile 根之上插入全部基础插件行——模型适配器、工具、持久化、策略、settings/credentials、repository 插件、遥测——作为每个 profile 的 `dsh.profile.bundles` 列表中的第一层。后续的组合包层(例如 [`dsh-web-app`](../web-app/README.md))和用户 profile 的 `cordis.patch.yml` 按 id 覆盖这些行;patch 会替换目标行的整个 `config`,因此模式专属的值放在各模式组合包中,而不是这里。该包没有运行时 API;profile 组合器通过 manifest(元数据清单)的 `dsh.bundle.patch` 字段解析 patch,绝不通过代码。 +以 profile 组合包形式交付的共享 dsh 核心:[`cordis.patch.yml`](cordis.patch.yml) 在空的 profile 根之上插入全部基础插件行——模型适配器、工具、持久化、策略、settings/credentials、repository 插件、遥测——作为每个 profile 的 `dsh.profile.bundles` 列表中的第一层。后续的组合包层(例如 [`dsh-web-app`](../web-app/README.md))和用户 profile 的 `cordis.patch.yml` 按 id 覆盖这些行;patch 会替换目标行的整个 `config`,因此模式专属的值放在各模式组合包中,而不是这里。该包没有运行时 API;profile 组合器通过 manifest(元数据清单)的 `dsh.bundle.patch` 字段解析通用 patch,启动器在 win32 主机上通过代码读取下面的 Windows 平台层。 + +启动交付 profile 的 Windows 主机还会额外收到 [`windows.cordis.patch.yml`](windows.cordis.patch.yml):它禁用仅 POSIX 的 bash 执行器/工具与权限栈(dsh-permission 要求有限权能力的执行器),并插入 PowerShell 执行器与工具(`@deepseek-ai/dsh-pwsh-local`、`@deepseek-ai/dsh-tool-pwsh`)。启动器在 win32 主机上把它应用于 bundle 层与用户层之间;偏好 bash 栈的 Windows 主机通过其 profile 或 home 的 `cordis.patch.yml` 覆盖这些行。POSIX 主机永远不会收到它。 行集合及其设计依据以行内注释写在 patch 文件里;[生成的组合图](../../../apps/cli/composition.md)负责渲染它。 @@ -17,3 +19,4 @@ ## 已知限制与延期工作 - **patch 会替换整行 `config`**:profile 覆盖必须重述该行需要保留的每个字段;不存在深度合并层。 +- **Windows 上失去权限切换器**:`dsh-permission` 硬性要求有限权能力的 `ctx.bash` 执行器,因此 Windows 平台层随 bash 栈一起禁用 `permission`/`ui-permission`。fs 工具保留 sandbox 策略与批准服务,Windows 上的文件限制与升级仍然生效。 diff --git a/packages/bundle/base/package.json b/packages/bundle/base/package.json index 95e169cabb..cb2b7178f7 100644 --- a/packages/bundle/base/package.json +++ b/packages/bundle/base/package.json @@ -16,6 +16,7 @@ "default": "./lib/invariant.js" }, "./cordis.patch.yml": "./cordis.patch.yml", + "./windows.cordis.patch.yml": "./windows.cordis.patch.yml", "./src/*": "./src/*", "./package.json": "./package.json" }, @@ -23,6 +24,7 @@ "lib/index.js", "lib/invariant.js", "cordis.patch.yml", + "windows.cordis.patch.yml", "lib/types/**/*.d.ts" ], "license": "BSD-3-Clause", @@ -54,6 +56,7 @@ "@deepseek-ai/dsh-llm-retry": "workspace:^", "@deepseek-ai/dsh-permission": "workspace:^", "@deepseek-ai/dsh-plan-mode": "workspace:^", + "@deepseek-ai/dsh-pwsh-local": "workspace:^", "@deepseek-ai/dsh-repeat-tool-guard": "workspace:^", "@deepseek-ai/dsh-repository-plugin": "workspace:^", "@deepseek-ai/dsh-sandbox-local": "workspace:^", @@ -82,6 +85,7 @@ "@deepseek-ai/dsh-tool-fs": "workspace:^", "@deepseek-ai/dsh-tool-fs-search": "workspace:^", "@deepseek-ai/dsh-tool-goal": "workspace:^", + "@deepseek-ai/dsh-tool-pwsh": "workspace:^", "@deepseek-ai/dsh-tool-ralph": "workspace:^", "@deepseek-ai/dsh-tool-skill": "workspace:^", "@deepseek-ai/dsh-tool-str-replace-editor": "workspace:^", diff --git a/packages/bundle/base/windows.cordis.patch.yml b/packages/bundle/base/windows.cordis.patch.yml new file mode 100644 index 0000000000..59d5e81582 --- /dev/null +++ b/packages/bundle/base/windows.cordis.patch.yml @@ -0,0 +1,34 @@ +# The dsh-base Windows platform layer: applied by the dsh launcher on win32 +# hosts, between the bundle layers and the user layers, replacing the +# POSIX-only bash stack with the PowerShell stack. The launcher reads THIS +# file from the base bundle package (never through dsh.bundle.patch — that +# field names the one universal layer). A Windows host that prefers bash +# overrides the rows here through its profile or home cordis.patch.yml. +# +# Windows hosts cannot run the shipped bash executor (POSIX-only: hardcoded +# `bash -c` argv and process-group semantics), so the shipped Windows +# experience is PowerShell-native: pwsh-local backs `ctx.bash` and tool-pwsh +# is the model-facing shell tool. dsh-permission requires a confining +# executor (its presets bundle a sandbox mode the unconfined pwsh executor +# cannot honor), so the permission service and its client knob leave the +# Windows roster with the bash stack; the fs tools keep the sandbox policy +# and the approval service, so file confinement and escalation still apply. + +- id: bash-sandbox + disabled: true + +- id: tool-bash + disabled: true + +- id: permission + disabled: true + +- id: ui-permission + disabled: true + +- insert: + - id: pwsh-local + name: '@deepseek-ai/dsh-pwsh-local' + + - id: tool-pwsh + name: '@deepseek-ai/dsh-tool-pwsh' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b826046bea..453109bcae 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -161,6 +161,9 @@ importers: '@deepseek-ai/dsh-pty-local': specifier: workspace:^ version: link:../../packages/pty/pty-local + '@deepseek-ai/dsh-pwsh-local': + specifier: workspace:^ + version: link:../../packages/bash/pwsh-local '@deepseek-ai/dsh-session-reference': specifier: workspace:^ version: link:../../packages/context/session-reference @@ -176,6 +179,9 @@ importers: '@deepseek-ai/dsh-tool-cordis': specifier: workspace:^ version: link:../../packages/cordis/tool-cordis + '@deepseek-ai/dsh-tool-pwsh': + specifier: workspace:^ + version: link:../../packages/bash/tool-pwsh '@deepseek-ai/dsh-web-app': specifier: workspace:^ version: link:../../packages/bundle/web-app @@ -922,6 +928,9 @@ importers: '@deepseek-ai/dsh-plan-mode': specifier: workspace:^ version: link:../../plan/plan-mode + '@deepseek-ai/dsh-pwsh-local': + specifier: workspace:^ + version: link:../../bash/pwsh-local '@deepseek-ai/dsh-repeat-tool-guard': specifier: workspace:^ version: link:../../guard/repeat-tool-guard @@ -1006,6 +1015,9 @@ importers: '@deepseek-ai/dsh-tool-goal': specifier: workspace:^ version: link:../../goal/tool-goal + '@deepseek-ai/dsh-tool-pwsh': + specifier: workspace:^ + version: link:../../bash/tool-pwsh '@deepseek-ai/dsh-tool-ralph': specifier: workspace:^ version: link:../../workflow/tool-ralph From acac2149a5f8b3cbbbd26cd848ef73ac8792e7d2 Mon Sep 17 00:00:00 2001 From: Huanqi Cao Date: Fri, 7 Aug 2026 01:20:40 +0800 Subject: [PATCH 02/11] feat(bundle): degrade the Windows shell layer to danger-full-access MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Windows platform layer previously kept fs path-rule confinement (sandbox-policy + fs-sandbox) next to the unconfined pwsh shell. Windows has no OS sandbox runner (landlock/bwrap/seatbelt are POSIX-only), so the shell can bypass fs-only path rules with one command — the policy was theater. The layer now removes the whole sandbox stack (sandbox, sandbox-policy, fs-sandbox disabled), mounts the unconfined dsh-fs-local, and degrades to danger-full-access: permission/ui-permission leave the roster and the approval policy is 'never'. dsh-base declares dsh-fs-local so the profile module fallback links it for cold starts; base.spec.ts pins the shipped Windows roster (disables, inserts, approval policy); the Agent Note records the rejected fs-only confinement alternative. --- .../2026-08-01-windows-pwsh-default.i18n.yaml | 4 +- .../2026-08-01-windows-pwsh-default.md | 14 ++--- .../2026-08-01-windows-pwsh-default.zh.md | 14 ++--- packages/bundle/base/README.i18n.yaml | 4 +- packages/bundle/base/README.md | 4 +- packages/bundle/base/README.zh.md | 4 +- packages/bundle/base/package.json | 1 + packages/bundle/base/tests/base.spec.ts | 53 +++++++++++++++++-- packages/bundle/base/windows.cordis.patch.yml | 42 ++++++++++----- 9 files changed, 103 insertions(+), 37 deletions(-) diff --git a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.i18n.yaml b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.i18n.yaml index e26d1ca354..711699aa0b 100644 --- a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.md -2026-08-01-windows-pwsh-default.md: 1fa30ef757ccb81544479a08887355839fb86d46 -2026-08-01-windows-pwsh-default.zh.md: c6e1361dba29cff8340ef6e26bf071f81544642b +2026-08-01-windows-pwsh-default.md: ceffd825c2316841dfbe01f4510243ba8748e500 +2026-08-01-windows-pwsh-default.zh.md: ea8058878fe268bdf1317620c853b8279c543532 diff --git a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.md b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.md index 1fa30ef757..ceffd825c2 100644 --- a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.md +++ b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.md @@ -12,9 +12,9 @@ The harness's shipped execution profile is bash-first on every platform. Windows Windows hosts booting a shipped profile (`dsh web`, `dsh --profile headless`, one-shot tasks) get the PowerShell stack by default; POSIX hosts are unchanged. -- **The platform layer is a data file, not a roster rewrite.** `@deepseek-ai/dsh-base` ships [`windows.cordis.patch.yml`](../../../../packages/bundle/base/windows.cordis.patch.yml) alongside its universal `cordis.patch.yml`: it disables `bash-sandbox`/`tool-bash` (the POSIX-only executor and its dialect tool), disables `permission`/`ui-permission` (dsh-permission requires a confining executor — presets bundle a sandbox mode the unconfined pwsh executor cannot honor; see its constructor guard — and the client knob would advertise a shell it cannot enforce), and inserts `pwsh-local`/`tool-pwsh`. The fs tools keep the sandbox policy and approval service, so file confinement and escalation still apply on Windows. -- **The launcher injects the layer by platform.** `apps/cli/src/windows-shell.ts` resolves it from the base bundle layer's `packageDir` between the bundle layers and the user layers on `win32` hosts, in every composition path (boot, config-only HMR recomposition, config dumps). Overriding the shipped default is a composition decision: a Windows host that prefers the bash stack re-enables the bash rows through its profile or home `cordis.patch.yml`. Custom profiles without the base bundle are skipped (they own their shell stack); a base bundle that ships no Windows shell patch fails loud. -- **Module resolution is restored for cold starts.** The profiles-rework CLI dropped the pwsh packages from `apps/cli`'s dependency closure, so `healProfilesModuleFallback` never linked them into `$DSH_HOME/profiles/node_modules` and a fresh Windows host could not resolve the inserted rows. `apps/cli` and `dsh-base` re-declare `dsh-pwsh-local`/`dsh-tool-pwsh`; the base bundle lists every row plugin as a dependency by house style. +- **The platform layer is a data file, not a roster rewrite.** `@deepseek-ai/dsh-base` ships [`windows.cordis.patch.yml`](../../../../packages/bundle/base/windows.cordis.patch.yml) alongside its universal `cordis.patch.yml`: it disables `bash-sandbox`/`tool-bash` (the POSIX-only executor and its dialect tool) and inserts `pwsh-local`/`tool-pwsh`. Windows has no OS sandbox runner (landlock/bwrap/seatbelt are POSIX-only), so the layer drops the sandbox stack entirely — `sandbox`, `sandbox-policy`, and `fs-sandbox` are disabled and the unconfined `dsh-fs-local` provides `ctx.fs` — and degrades to danger-full-access: `permission`/`ui-permission` leave the roster (dsh-permission requires a confining executor — presets bundle a sandbox mode the unconfined executor cannot honor; see its constructor guard — and the client knob would advertise a boundary that does not exist), and the `approval` policy is `never`. Keeping fs-only path rules would be theater: the unconfined shell can bypass them with one command, so the honest Windows posture is full access rather than a boundary only the fs tools pretend to enforce. +- **The launcher injects the layer by platform.** `apps/cli/src/windows-shell.ts` resolves it from the base bundle layer's `packageDir` between the bundle layers and the user layers on `win32` hosts, in every composition path (boot, config-only HMR recomposition, config dumps). Overriding the shipped default is a composition decision: a Windows host that prefers the bash stack — or confinement — re-enables the bash rows through its profile or home `cordis.patch.yml`. Custom profiles without the base bundle are skipped (they own their shell stack); a base bundle that ships no Windows shell patch fails loud. +- **Module resolution is restored for cold starts.** The profiles-rework CLI dropped the pwsh packages from `apps/cli`'s dependency closure, so `healProfilesModuleFallback` never linked them into `$DSH_HOME/profiles/node_modules` and a fresh Windows host could not resolve the inserted rows. `apps/cli` and `dsh-base` re-declare `dsh-pwsh-local`/`dsh-tool-pwsh`, and `dsh-base` also declares `dsh-fs-local`; the base bundle lists every row plugin as a dependency by house style. The pwsh GUI rendering stage (stage 2 of the original roadmap) shipped earlier with the [pwsh UI presentation matches bash decision](2026-08-05-pwsh-ui-bash-parity.md); the [pwsh tool bash parity decision](2026-08-02-pwsh-tool-bash-parity.md) ships the tool's surface. Nothing in this decision changes POSIX behavior. @@ -24,19 +24,21 @@ The pwsh GUI rendering stage (stage 2 of the original roadmap) shipped earlier w **Ship the platform layer from `apps/cli` code instead of a bundle data file.** Rejected: the patch belongs next to the rows it replaces, in the bundle that owns them, so the shipped roster stays visible as composition data and dumps carry its provenance; the launcher contributes only the win32 gate. -**Keep `permission`/`ui-permission` on Windows.** Rejected: `dsh-permission` hard-requires `ctx.bash.sandboxMode` and fails loud at load over an unconfined executor; making it tolerate an unconfined shell would advertise presets the shell cannot honor. File confinement keeps working through the fs stack. +**Keep `permission`/`ui-permission` on Windows.** Rejected: `dsh-permission` hard-requires `ctx.bash.sandboxMode` and fails loud at load over an unconfined executor; making it tolerate an unconfined shell would advertise presets the shell cannot honor. + +**Keep fs path-rule confinement on Windows (`sandbox-policy` + `fs-sandbox` without OS runners).** Rejected: the shell is the model's primary tool and unconfined on Windows, so fs-only path rules are trivially bypassable and would overstate the boundary; the honest posture is full degradation to danger-full-access. **Ship a `DSH_WINDOWS_SHELL` environment escape hatch.** Rejected: decisive behavior changes belong in composition config, which already overrides the platform layer row by id; a second override channel would split the single source of truth for roster decisions. ## Consequences - A Windows host running a shipped `dsh` surface gets `pwsh` as its shell tool and PowerShell as the `ctx.bash` executor without configuration; `bash` is absent from the model-visible roster there (its tool row is disabled). +- Windows has no sandbox at all: the fs tools run unconfined (`dsh-fs-local`), the approval policy is `never`, and the permission switcher is gone. The model-visible posture is honest full access rather than a boundary the shell can bypass. - POSIX hosts are unchanged: the platform layer never applies, and the bash stack remains the universal `cordis.patch.yml` rows. - Windows hosts that prefer the bash stack (e.g. with WSL/Git-Bash on PATH) override the shipped default through their profile or home `cordis.patch.yml` — composition config is the one override channel. -- The permission switcher leaves the Windows roster; session permission facts pin the composition defaults through the approval service and sandbox policy. ## Verification -- Unit: `apps/cli/tests/windows-shell.spec.ts` pins the win32 default, the custom-profile skip, and the missing-patch failure, with the platform injected. +- Unit: `apps/cli/tests/windows-shell.spec.ts` pins the win32 default, the custom-profile skip, and the missing-patch failure, with the platform injected; `packages/bundle/base/tests/base.spec.ts` pins the shipped Windows roster (disables, inserts, and the `never` approval policy). - Keyless: a win32 `dsh --profile --dump-config` shows the pwsh rows with `windows.cordis.patch.yml` provenance and the bash rows disabled; the POSIX dump (CI Linux) is unchanged. - The real-composition smoke boots the web profile on win32 with the pwsh stack mounted (the exact roster this note describes). diff --git a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.zh.md b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.zh.md index c6e1361dba..ea8058878f 100644 --- a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.zh.md +++ b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.zh.md @@ -12,9 +12,9 @@ harness 交付的执行画像在每个平台都是 bash 优先。Windows 主机 启动交付 profile(`dsh web`、`dsh --profile headless`、一次性任务)的 Windows 主机默认获得 PowerShell 栈;POSIX 主机不变。 -- **平台层是数据文件,不是清单重写。** `@deepseek-ai/dsh-base` 随通用 `cordis.patch.yml` 一起交付 [`windows.cordis.patch.yml`](../../../../packages/bundle/base/windows.cordis.patch.yml):它禁用 `bash-sandbox`/`tool-bash`(仅 POSIX 的执行器及其方言工具)、禁用 `permission`/`ui-permission`(dsh-permission 要求有限权能力的执行器——preset 捆绑的是无限制 pwsh 执行器无法兑现的 sandbox 模式;见其构造函数守卫——客户端旋钮会宣传一个它无法强制执行的 shell),并插入 `pwsh-local`/`tool-pwsh`。fs 工具保留 sandbox 策略与批准服务,因此 Windows 上的文件限制与升级仍然生效。 -- **启动器按平台注入该层。** `apps/cli/src/windows-shell.ts` 在 `win32` 主机上从 base bundle 层的 `packageDir` 解析它,置于 bundle 层与用户层之间,覆盖所有组合路径(启动、config-only HMR 重组合、配置转储)。覆盖交付默认是组合决策:偏好 bash 栈的 Windows 主机通过其 profile 或 home 的 `cordis.patch.yml` 重新启用 bash 行。未挂 base bundle 的自定义 profile 被跳过(它们自己拥有 shell 栈);base bundle 缺 `windows.cordis.patch.yml` 时 fail loud。 -- **冷启动的模块解析已恢复。** profiles 重构把 pwsh 包从 `apps/cli` 的依赖闭包中删掉了,`healProfilesModuleFallback` 因此从未把它们链接进 `$DSH_HOME/profiles/node_modules`,新 Windows 主机解析不到插入的行。`apps/cli` 与 `dsh-base` 重新声明 `dsh-pwsh-local`/`dsh-tool-pwsh`;按仓库惯例,base bundle 把每个行插件都列为依赖。 +- **平台层是数据文件,不是清单重写。** `@deepseek-ai/dsh-base` 随通用 `cordis.patch.yml` 一起交付 [`windows.cordis.patch.yml`](../../../../packages/bundle/base/windows.cordis.patch.yml):它禁用 `bash-sandbox`/`tool-bash`(仅 POSIX 的执行器及其方言工具)并插入 `pwsh-local`/`tool-pwsh`。Windows 上没有 OS 级 sandbox runner(landlock/bwrap/seatbelt 均为 POSIX 专属),因此该层整体移除 sandbox 栈——`sandbox`、`sandbox-policy`、`fs-sandbox` 被禁用,由不限权的 `dsh-fs-local` 提供 `ctx.fs`——并完全退化为 danger-full-access:`permission`/`ui-permission` 离开清单(dsh-permission 要求有限权能力的执行器——preset 捆绑的是无限制执行器无法兑现的 sandbox 模式;见其构造函数守卫——客户端旋钮会宣传一个并不存在的边界),`approval` 策略为 `never`。保留仅限 fs 的路径规则是摆设:不限权的 shell 一条命令即可绕过,因此诚实的 Windows 姿态是全权访问,而不是一个只有 fs 工具假装执行的边界。 +- **启动器按平台注入该层。** `apps/cli/src/windows-shell.ts` 在 `win32` 主机上从 base bundle 层的 `packageDir` 解析它,置于 bundle 层与用户层之间,覆盖所有组合路径(启动、config-only HMR 重组合、配置转储)。覆盖交付默认是组合决策:偏好 bash 栈(或偏好有限权)的 Windows 主机通过其 profile 或 home 的 `cordis.patch.yml` 重新启用 bash 行。未挂 base bundle 的自定义 profile 被跳过(它们自己拥有 shell 栈);base bundle 缺 `windows.cordis.patch.yml` 时 fail loud。 +- **冷启动的模块解析已恢复。** profiles 重构把 pwsh 包从 `apps/cli` 的依赖闭包中删掉了,`healProfilesModuleFallback` 因此从未把它们链接进 `$DSH_HOME/profiles/node_modules`,新 Windows 主机解析不到插入的行。`apps/cli` 与 `dsh-base` 重新声明 `dsh-pwsh-local`/`dsh-tool-pwsh`,`dsh-base` 还声明 `dsh-fs-local`;按仓库惯例,base bundle 把每个行插件都列为依赖。 原路线图的阶段 2(pwsh GUI 渲染)已随 [pwsh UI 呈现与 bash 对齐决策](2026-08-05-pwsh-ui-bash-parity.md) 先行交付;[pwsh 工具与 bash 对齐决策](2026-08-02-pwsh-tool-bash-parity.md) 交付了工具表面。本决策不改变任何 POSIX 行为。 @@ -24,19 +24,21 @@ harness 交付的执行画像在每个平台都是 bash 优先。Windows 主机 **从 `apps/cli` 代码而非 bundle 数据文件交付平台层。** 否决:patch 应放在它替换的行旁边、属于拥有这些行的 bundle,让交付清单作为组合数据保持可见、转储带有出处;启动器只贡献 win32 门控。 -**在 Windows 上保留 `permission`/`ui-permission`。** 否决:`dsh-permission` 硬性要求 `ctx.bash.sandboxMode`,在无限制执行器上加载即 fail loud;让它容忍无限制 shell 会宣传 shell 无法兑现的 preset。文件限制继续经由 fs 栈生效。 +**在 Windows 上保留 `permission`/`ui-permission`。** 否决:`dsh-permission` 硬性要求 `ctx.bash.sandboxMode`,在无限制执行器上加载即 fail loud;让它容忍无限制 shell 会宣传 shell 无法兑现的 preset。 + +**在 Windows 上保留 fs 路径规则限制(无 OS runner 的 `sandbox-policy` + `fs-sandbox`)。** 否决:shell 是模型的主工具且在 Windows 上不限权,仅限 fs 的路径规则一行命令即可绕过,会夸大边界;诚实的姿态是完全退化到 danger-full-access。 **交付 `DSH_WINDOWS_SHELL` 环境变量逃生门。** 否决:决定性的行为变更应集中在组合配置中,而组合配置已能按行 id 覆盖平台层;第二条覆盖通道会分裂清单决策的单一事实来源。 ## 后果 - 运行交付版 `dsh` 表面的 Windows 主机无需配置即获得 `pwsh` 作为 shell 工具、PowerShell 作为 `ctx.bash` 执行器;那里的模型可见清单中没有 `bash`(其工具行被禁用)。 +- Windows 上没有任何沙箱:fs 工具不限权运行(`dsh-fs-local`)、`approval` 策略为 `never`、权限切换器消失。模型可见的姿态是诚实的全权访问,而不是一个 shell 可以绕过的边界。 - POSIX 主机不变:平台层永不生效,bash 栈仍是通用 `cordis.patch.yml` 的行。 - 偏好 bash 栈的 Windows 主机(例如 PATH 上有 WSL/Git-Bash 时)通过其 profile 或 home 的 `cordis.patch.yml` 覆盖交付默认——组合配置是唯一的覆盖通道。 -- 权限切换器离开 Windows 清单;会话权限事实通过批准服务与 sandbox 策略固定组合默认值。 ## 验证 -- 单元:`apps/cli/tests/windows-shell.spec.ts` 固定 win32 默认、自定义 profile 跳过与缺文件失败,平台注入。 +- 单元:`apps/cli/tests/windows-shell.spec.ts` 固定 win32 默认、自定义 profile 跳过与缺文件失败,平台注入;`packages/bundle/base/tests/base.spec.ts` 固定交付的 Windows 清单(禁用、插入与 `never` approval 策略)。 - Keyless:win32 上的 `dsh --profile --dump-config` 显示带 `windows.cordis.patch.yml` 出处的 pwsh 行、被禁用的 bash 行;POSIX 转储(CI Linux)不变。 - 真实组合冒烟在 win32 上启动 web profile,pwsh 栈挂载成功(即本笔记描述的确切清单)。 diff --git a/packages/bundle/base/README.i18n.yaml b/packages/bundle/base/README.i18n.yaml index a47ac9cbc7..0cc7f47a1b 100644 --- a/packages/bundle/base/README.i18n.yaml +++ b/packages/bundle/base/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/bundle/base/README.md -README.md: a4f220c956f7f67d7810827d4488d54a071ad3d6 -README.zh.md: eaf96f8c0640481163e7a1c1533a07e35ecc294f +README.md: 81932442fe9e2edac82bda88bbe28ce56fd8b6c1 +README.zh.md: b0af307b7655a8729a8face324a2750e2e480651 diff --git a/packages/bundle/base/README.md b/packages/bundle/base/README.md index a4f220c956..81932442fe 100644 --- a/packages/bundle/base/README.md +++ b/packages/bundle/base/README.md @@ -4,7 +4,7 @@ English | [中文](README.zh.md) The shared dsh core as a profile bundle: [`cordis.patch.yml`](cordis.patch.yml) inserts every base plugin row — model adapters, tools, persistence, policy, settings/credentials, repository Plugins, telemetry — over the empty profile root, as the first layer of every profile's `dsh.profile.bundles` list. Later bundle layers (e.g. [`dsh-web-app`](../web-app/README.md)) and the user's profile `cordis.patch.yml` override these rows by id; a patch replaces a row's whole `config`, so mode-specific values live in mode bundles, not here. The package has no runtime API; the profile composer resolves the universal patch through the `dsh.bundle.patch` manifest field, and the launcher reads the Windows platform layer below from code on win32 hosts. -Windows hosts booting a shipped profile additionally receive [`windows.cordis.patch.yml`](windows.cordis.patch.yml): it disables the POSIX-only bash executor/tool and the permission stack (dsh-permission requires a confining executor), and inserts the PowerShell executor and tool (`@deepseek-ai/dsh-pwsh-local`, `@deepseek-ai/dsh-tool-pwsh`). The launcher applies it between the bundle layers and the user layers on win32 hosts; a Windows host that prefers the bash stack overrides these rows through its profile or home `cordis.patch.yml`. POSIX hosts never receive it. +Windows hosts booting a shipped profile additionally receive [`windows.cordis.patch.yml`](windows.cordis.patch.yml): it disables the POSIX-only sandboxed stacks — the bash executor/tool, the permission switcher (dsh-permission requires a confining executor), and the sandbox/fs-policy stack — and inserts the PowerShell executor and tool (`@deepseek-ai/dsh-pwsh-local`, `@deepseek-ai/dsh-tool-pwsh`) plus the unconfined `dsh-fs-local`, with the approval policy set to `never`. Windows has no OS sandbox runner (landlock/bwrap/seatbelt are POSIX-only), so the shipped posture is honest danger-full-access rather than a boundary only the fs tools pretend to enforce. The launcher applies the layer between the bundle layers and the user layers on win32 hosts; a Windows host that prefers the bash stack overrides these rows through its profile or home `cordis.patch.yml`. POSIX hosts never receive it. The row set and its rationale are documented inline in the patch file; the [generated composition graph](../../../apps/cli/composition.md) renders it. @@ -19,4 +19,4 @@ None directly; each inserted row's package owns its effect. ## Known Limitations and Deferred Work - **A patch replaces whole row configs** — profile overrides must restate every field a row keeps; there is no deep-merge layer. -- **Windows loses the permission switcher** — `dsh-permission` hard-requires a confining `ctx.bash` executor, so the Windows platform layer disables `permission`/`ui-permission` with the bash stack. The fs tools keep the sandbox policy and the approval service, so file confinement and escalation still apply on Windows. +- **Windows has no sandbox** — no OS runner exists on win32 (landlock/bwrap/seatbelt are POSIX-only), so the Windows platform layer removes the whole sandbox stack: `sandbox`/`sandbox-policy`/`fs-sandbox` are disabled, `dsh-fs-local` provides `ctx.fs`, the permission switcher leaves the roster, and the approval policy is `never`. Everything degrades to danger-full-access: the shell is unconfined and the fs tools make no confinement claims. diff --git a/packages/bundle/base/README.zh.md b/packages/bundle/base/README.zh.md index eaf96f8c06..b0af307b76 100644 --- a/packages/bundle/base/README.zh.md +++ b/packages/bundle/base/README.zh.md @@ -4,7 +4,7 @@ 以 profile 组合包形式交付的共享 dsh 核心:[`cordis.patch.yml`](cordis.patch.yml) 在空的 profile 根之上插入全部基础插件行——模型适配器、工具、持久化、策略、settings/credentials、repository 插件、遥测——作为每个 profile 的 `dsh.profile.bundles` 列表中的第一层。后续的组合包层(例如 [`dsh-web-app`](../web-app/README.md))和用户 profile 的 `cordis.patch.yml` 按 id 覆盖这些行;patch 会替换目标行的整个 `config`,因此模式专属的值放在各模式组合包中,而不是这里。该包没有运行时 API;profile 组合器通过 manifest(元数据清单)的 `dsh.bundle.patch` 字段解析通用 patch,启动器在 win32 主机上通过代码读取下面的 Windows 平台层。 -启动交付 profile 的 Windows 主机还会额外收到 [`windows.cordis.patch.yml`](windows.cordis.patch.yml):它禁用仅 POSIX 的 bash 执行器/工具与权限栈(dsh-permission 要求有限权能力的执行器),并插入 PowerShell 执行器与工具(`@deepseek-ai/dsh-pwsh-local`、`@deepseek-ai/dsh-tool-pwsh`)。启动器在 win32 主机上把它应用于 bundle 层与用户层之间;偏好 bash 栈的 Windows 主机通过其 profile 或 home 的 `cordis.patch.yml` 覆盖这些行。POSIX 主机永远不会收到它。 +启动交付 profile 的 Windows 主机还会额外收到 [`windows.cordis.patch.yml`](windows.cordis.patch.yml):它禁用仅 POSIX 的受限栈——bash 执行器/工具、权限切换器(dsh-permission 要求有限权能力的执行器)与 sandbox/fs 策略栈——并插入 PowerShell 执行器与工具(`@deepseek-ai/dsh-pwsh-local`、`@deepseek-ai/dsh-tool-pwsh`)以及不限权的 `dsh-fs-local`,`approval` 策略设为 `never`。Windows 上没有 OS 级 sandbox runner(landlock/bwrap/seatbelt 均为 POSIX 专属),因此交付姿态是诚实的 danger-full-access,而不是一个只有 fs 工具假装执行的边界。启动器在 win32 主机上把它应用于 bundle 层与用户层之间;偏好 bash 栈的 Windows 主机通过其 profile 或 home 的 `cordis.patch.yml` 覆盖这些行。POSIX 主机永远不会收到它。 行集合及其设计依据以行内注释写在 patch 文件里;[生成的组合图](../../../apps/cli/composition.md)负责渲染它。 @@ -19,4 +19,4 @@ ## 已知限制与延期工作 - **patch 会替换整行 `config`**:profile 覆盖必须重述该行需要保留的每个字段;不存在深度合并层。 -- **Windows 上失去权限切换器**:`dsh-permission` 硬性要求有限权能力的 `ctx.bash` 执行器,因此 Windows 平台层随 bash 栈一起禁用 `permission`/`ui-permission`。fs 工具保留 sandbox 策略与批准服务,Windows 上的文件限制与升级仍然生效。 +- **Windows 上没有沙箱**:win32 上不存在 OS 级 runner(landlock/bwrap/seatbelt 均为 POSIX 专属),因此 Windows 平台层移除整个 sandbox 栈——`sandbox`/`sandbox-policy`/`fs-sandbox` 被禁用,由 `dsh-fs-local` 提供 `ctx.fs`,权限切换器离开清单,`approval` 策略为 `never`。一切退化为 danger-full-access:shell 不限权,fs 工具也不做任何限权声明。 diff --git a/packages/bundle/base/package.json b/packages/bundle/base/package.json index cb2b7178f7..d2f93e2bd3 100644 --- a/packages/bundle/base/package.json +++ b/packages/bundle/base/package.json @@ -46,6 +46,7 @@ "@deepseek-ai/dsh-compact-basic": "workspace:^", "@deepseek-ai/dsh-compact-tool-result-prune": "workspace:^", "@deepseek-ai/dsh-credentials-local": "workspace:^", + "@deepseek-ai/dsh-fs-local": "workspace:^", "@deepseek-ai/dsh-fs-policy": "workspace:^", "@deepseek-ai/dsh-fs-sandbox": "workspace:^", "@deepseek-ai/dsh-goal": "workspace:^", diff --git a/packages/bundle/base/tests/base.spec.ts b/packages/bundle/base/tests/base.spec.ts index 24ee2a1ba3..6adcab1715 100644 --- a/packages/bundle/base/tests/base.spec.ts +++ b/packages/bundle/base/tests/base.spec.ts @@ -13,13 +13,60 @@ import { entryListSchema } from '@cordisjs/plugin-include' describe('dsh-base bundle', () => { it('declares a parseable patch list through the dsh.bundle.patch manifest field', () => { const root = fileURLToPath(new URL('..', import.meta.url)) - const manifest = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8')) as { dsh?: { bundle?: { patch?: string } } } + const manifest = JSON.parse( + readFileSync(resolve(root, 'package.json'), 'utf8'), + ) as { dsh?: { bundle?: { patch?: string } } } expect(manifest.dsh?.bundle?.patch).toBe('./cordis.patch.yml') - const parsed = yaml.load(readFileSync(resolve(root, manifest.dsh!.bundle!.patch!), 'utf8'), { schema: entryListSchema }) + const parsed = yaml.load( + readFileSync(resolve(root, manifest.dsh!.bundle!.patch!), 'utf8'), + { schema: entryListSchema }, + ) expect(Array.isArray(parsed)).toBe(true) // The base layer is one insert list over the empty profile root. - const rows = (parsed as { insert?: { id?: string }[] }[]).flatMap(patch => patch.insert ?? []) + const rows = (parsed as { insert?: { id?: string }[] }[]).flatMap( + patch => patch.insert ?? [], + ) expect(rows.length).toBeGreaterThan(50) expect(rows.some(row => row.id === 'agent-loop')).toBe(true) }) + + it('ships the Windows platform layer as the documented danger-full-access roster', () => { + const root = fileURLToPath(new URL('..', import.meta.url)) + const parsed = yaml.load( + readFileSync(resolve(root, 'windows.cordis.patch.yml'), 'utf8'), + { schema: entryListSchema }, + ) as { + id?: string + disabled?: boolean + insert?: { id?: string; name?: string }[] + config?: { policy?: string } + }[] + const disables = parsed + .filter(patch => patch.disabled === true) + .map(patch => patch.id) + // The POSIX-only sandboxed stacks leave the Windows roster as one unit: + // shell (bash-sandbox/tool-bash), the permission switcher it requires, + // and the fs/sandbox policy stack whose OS runners do not exist on win32. + expect(disables).toEqual( + expect.arrayContaining([ + 'bash-sandbox', + 'tool-bash', + 'permission', + 'ui-permission', + 'sandbox', + 'sandbox-policy', + 'fs-sandbox', + ]), + ) + const inserted = parsed + .flatMap(patch => patch.insert ?? []) + .map(row => row.id) + expect(inserted).toEqual( + expect.arrayContaining(['pwsh-local', 'tool-pwsh', 'fs-local']), + ) + // Full danger-full-access degradation: no approval prompts on Windows. + expect(parsed.find(patch => patch.id === 'approval')?.config).toEqual({ + policy: 'never', + }) + }) }) diff --git a/packages/bundle/base/windows.cordis.patch.yml b/packages/bundle/base/windows.cordis.patch.yml index 59d5e81582..50dbe8d9e5 100644 --- a/packages/bundle/base/windows.cordis.patch.yml +++ b/packages/bundle/base/windows.cordis.patch.yml @@ -1,18 +1,16 @@ # The dsh-base Windows platform layer: applied by the dsh launcher on win32 -# hosts, between the bundle layers and the user layers, replacing the -# POSIX-only bash stack with the PowerShell stack. The launcher reads THIS -# file from the base bundle package (never through dsh.bundle.patch — that -# field names the one universal layer). A Windows host that prefers bash -# overrides the rows here through its profile or home cordis.patch.yml. -# -# Windows hosts cannot run the shipped bash executor (POSIX-only: hardcoded -# `bash -c` argv and process-group semantics), so the shipped Windows -# experience is PowerShell-native: pwsh-local backs `ctx.bash` and tool-pwsh -# is the model-facing shell tool. dsh-permission requires a confining -# executor (its presets bundle a sandbox mode the unconfined pwsh executor -# cannot honor), so the permission service and its client knob leave the -# Windows roster with the bash stack; the fs tools keep the sandbox policy -# and the approval service, so file confinement and escalation still apply. +# hosts, between the bundle layers and the user layers. Windows cannot run +# the POSIX-only sandboxed stacks, so this layer swaps the shipped bash stack +# for the PowerShell stack AND drops the sandbox entirely: no OS runner +# exists on Windows (landlock/bwrap/seatbelt are POSIX-only), so any policy +# would be theater — the unconfined shell could bypass fs-only path rules +# with one command. Windows therefore degrades to danger-full-access: +# unconfined pwsh + unconfined fs (`dsh-fs-local`), no permission switcher +# (dsh-permission requires a confining executor), approval policy `never`. +# The launcher reads THIS file from the base bundle package (never through +# dsh.bundle.patch — that field names the one universal layer). A Windows +# host that prefers bash or confinement overrides these rows through its +# profile or home cordis.patch.yml. - id: bash-sandbox disabled: true @@ -26,9 +24,25 @@ - id: ui-permission disabled: true +- id: sandbox + disabled: true + +- id: sandbox-policy + disabled: true + +- id: fs-sandbox + disabled: true + +- id: approval + config: + policy: never + - insert: - id: pwsh-local name: '@deepseek-ai/dsh-pwsh-local' - id: tool-pwsh name: '@deepseek-ai/dsh-tool-pwsh' + + - id: fs-local + name: '@deepseek-ai/dsh-fs-local' From 73c6f351714762ae57f10317ff7687da6bbe0b7e Mon Sep 17 00:00:00 2001 From: Huanqi Cao Date: Fri, 7 Aug 2026 01:41:32 +0800 Subject: [PATCH 03/11] fix(bundle): drop the approval service from the Windows layer entirely MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Windows layer previously kept the approval service with policy 'never', which made the approval plugin inject 'Approval prompts are disabled in this session: actions that require approval are rejected automatically' into the model context. On Windows nothing asks for approval — the escalation surfaces (sandbox_permissions) do not exist — so the sentence described a rejection surface that is not there. The layer now disables the approval row too: the service is absent, the model is never told approval exists, and the danger-full-access degradation is complete. base.spec.ts pins approval among the Windows disables; the Agent Note and bundle README record the absent service. --- .../2026-08-01-windows-pwsh-default.i18n.yaml | 4 ++-- .../feature/2026-08-01-windows-pwsh-default.md | 6 +++--- .../feature/2026-08-01-windows-pwsh-default.zh.md | 6 +++--- packages/bundle/base/README.i18n.yaml | 4 ++-- packages/bundle/base/README.md | 4 ++-- packages/bundle/base/README.zh.md | 4 ++-- packages/bundle/base/tests/base.spec.ts | 11 ++++++----- packages/bundle/base/windows.cordis.patch.yml | 15 ++++++++------- pnpm-lock.yaml | 3 +++ 9 files changed, 31 insertions(+), 26 deletions(-) diff --git a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.i18n.yaml b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.i18n.yaml index 711699aa0b..7ab92d8199 100644 --- a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.md -2026-08-01-windows-pwsh-default.md: ceffd825c2316841dfbe01f4510243ba8748e500 -2026-08-01-windows-pwsh-default.zh.md: ea8058878fe268bdf1317620c853b8279c543532 +2026-08-01-windows-pwsh-default.md: 75a27b5140abacf77b5f20028752a4903a50eaec +2026-08-01-windows-pwsh-default.zh.md: 8b940f1973835f8143bde05b37eba6f00e27f8f4 diff --git a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.md b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.md index ceffd825c2..75a27b5140 100644 --- a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.md +++ b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.md @@ -12,7 +12,7 @@ The harness's shipped execution profile is bash-first on every platform. Windows Windows hosts booting a shipped profile (`dsh web`, `dsh --profile headless`, one-shot tasks) get the PowerShell stack by default; POSIX hosts are unchanged. -- **The platform layer is a data file, not a roster rewrite.** `@deepseek-ai/dsh-base` ships [`windows.cordis.patch.yml`](../../../../packages/bundle/base/windows.cordis.patch.yml) alongside its universal `cordis.patch.yml`: it disables `bash-sandbox`/`tool-bash` (the POSIX-only executor and its dialect tool) and inserts `pwsh-local`/`tool-pwsh`. Windows has no OS sandbox runner (landlock/bwrap/seatbelt are POSIX-only), so the layer drops the sandbox stack entirely — `sandbox`, `sandbox-policy`, and `fs-sandbox` are disabled and the unconfined `dsh-fs-local` provides `ctx.fs` — and degrades to danger-full-access: `permission`/`ui-permission` leave the roster (dsh-permission requires a confining executor — presets bundle a sandbox mode the unconfined executor cannot honor; see its constructor guard — and the client knob would advertise a boundary that does not exist), and the `approval` policy is `never`. Keeping fs-only path rules would be theater: the unconfined shell can bypass them with one command, so the honest Windows posture is full access rather than a boundary only the fs tools pretend to enforce. +- **The platform layer is a data file, not a roster rewrite.** `@deepseek-ai/dsh-base` ships [`windows.cordis.patch.yml`](../../../../packages/bundle/base/windows.cordis.patch.yml) alongside its universal `cordis.patch.yml`: it disables `bash-sandbox`/`tool-bash` (the POSIX-only executor and its dialect tool) and inserts `pwsh-local`/`tool-pwsh`. Windows has no OS sandbox runner (landlock/bwrap/seatbelt are POSIX-only), so the layer drops the sandbox stack entirely — `sandbox`, `sandbox-policy`, and `fs-sandbox` are disabled and the unconfined `dsh-fs-local` provides `ctx.fs` — and degrades to danger-full-access: `permission`/`ui-permission` leave the roster (dsh-permission requires a confining executor — presets bundle a sandbox mode the unconfined executor cannot honor; see its constructor guard — and the client knob would advertise a boundary that does not exist), and the `approval` service is disabled — nothing in the Windows roster asks for approval, so the model is never told approval exists or that asks are auto-rejected. Keeping fs-only path rules would be theater: the unconfined shell can bypass them with one command, so the honest Windows posture is full access rather than a boundary only the fs tools pretend to enforce. - **The launcher injects the layer by platform.** `apps/cli/src/windows-shell.ts` resolves it from the base bundle layer's `packageDir` between the bundle layers and the user layers on `win32` hosts, in every composition path (boot, config-only HMR recomposition, config dumps). Overriding the shipped default is a composition decision: a Windows host that prefers the bash stack — or confinement — re-enables the bash rows through its profile or home `cordis.patch.yml`. Custom profiles without the base bundle are skipped (they own their shell stack); a base bundle that ships no Windows shell patch fails loud. - **Module resolution is restored for cold starts.** The profiles-rework CLI dropped the pwsh packages from `apps/cli`'s dependency closure, so `healProfilesModuleFallback` never linked them into `$DSH_HOME/profiles/node_modules` and a fresh Windows host could not resolve the inserted rows. `apps/cli` and `dsh-base` re-declare `dsh-pwsh-local`/`dsh-tool-pwsh`, and `dsh-base` also declares `dsh-fs-local`; the base bundle lists every row plugin as a dependency by house style. @@ -33,12 +33,12 @@ The pwsh GUI rendering stage (stage 2 of the original roadmap) shipped earlier w ## Consequences - A Windows host running a shipped `dsh` surface gets `pwsh` as its shell tool and PowerShell as the `ctx.bash` executor without configuration; `bash` is absent from the model-visible roster there (its tool row is disabled). -- Windows has no sandbox at all: the fs tools run unconfined (`dsh-fs-local`), the approval policy is `never`, and the permission switcher is gone. The model-visible posture is honest full access rather than a boundary the shell can bypass. +- Windows has no sandbox at all: the fs tools run unconfined (`dsh-fs-local`), the approval service is absent (nothing asks for approval, and the model is never told approval exists), and the permission switcher is gone. The model-visible posture is honest full access rather than a boundary the shell can bypass. - POSIX hosts are unchanged: the platform layer never applies, and the bash stack remains the universal `cordis.patch.yml` rows. - Windows hosts that prefer the bash stack (e.g. with WSL/Git-Bash on PATH) override the shipped default through their profile or home `cordis.patch.yml` — composition config is the one override channel. ## Verification -- Unit: `apps/cli/tests/windows-shell.spec.ts` pins the win32 default, the custom-profile skip, and the missing-patch failure, with the platform injected; `packages/bundle/base/tests/base.spec.ts` pins the shipped Windows roster (disables, inserts, and the `never` approval policy). +- Unit: `apps/cli/tests/windows-shell.spec.ts` pins the win32 default, the custom-profile skip, and the missing-patch failure, with the platform injected; `packages/bundle/base/tests/base.spec.ts` pins the shipped Windows roster (disables, inserts, and the absent approval service). - Keyless: a win32 `dsh --profile --dump-config` shows the pwsh rows with `windows.cordis.patch.yml` provenance and the bash rows disabled; the POSIX dump (CI Linux) is unchanged. - The real-composition smoke boots the web profile on win32 with the pwsh stack mounted (the exact roster this note describes). diff --git a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.zh.md b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.zh.md index ea8058878f..8b940f1973 100644 --- a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.zh.md +++ b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.zh.md @@ -12,7 +12,7 @@ harness 交付的执行画像在每个平台都是 bash 优先。Windows 主机 启动交付 profile(`dsh web`、`dsh --profile headless`、一次性任务)的 Windows 主机默认获得 PowerShell 栈;POSIX 主机不变。 -- **平台层是数据文件,不是清单重写。** `@deepseek-ai/dsh-base` 随通用 `cordis.patch.yml` 一起交付 [`windows.cordis.patch.yml`](../../../../packages/bundle/base/windows.cordis.patch.yml):它禁用 `bash-sandbox`/`tool-bash`(仅 POSIX 的执行器及其方言工具)并插入 `pwsh-local`/`tool-pwsh`。Windows 上没有 OS 级 sandbox runner(landlock/bwrap/seatbelt 均为 POSIX 专属),因此该层整体移除 sandbox 栈——`sandbox`、`sandbox-policy`、`fs-sandbox` 被禁用,由不限权的 `dsh-fs-local` 提供 `ctx.fs`——并完全退化为 danger-full-access:`permission`/`ui-permission` 离开清单(dsh-permission 要求有限权能力的执行器——preset 捆绑的是无限制执行器无法兑现的 sandbox 模式;见其构造函数守卫——客户端旋钮会宣传一个并不存在的边界),`approval` 策略为 `never`。保留仅限 fs 的路径规则是摆设:不限权的 shell 一条命令即可绕过,因此诚实的 Windows 姿态是全权访问,而不是一个只有 fs 工具假装执行的边界。 +- **平台层是数据文件,不是清单重写。** `@deepseek-ai/dsh-base` 随通用 `cordis.patch.yml` 一起交付 [`windows.cordis.patch.yml`](../../../../packages/bundle/base/windows.cordis.patch.yml):它禁用 `bash-sandbox`/`tool-bash`(仅 POSIX 的执行器及其方言工具)并插入 `pwsh-local`/`tool-pwsh`。Windows 上没有 OS 级 sandbox runner(landlock/bwrap/seatbelt 均为 POSIX 专属),因此该层整体移除 sandbox 栈——`sandbox`、`sandbox-policy`、`fs-sandbox` 被禁用,由不限权的 `dsh-fs-local` 提供 `ctx.fs`——并完全退化为 danger-full-access:`permission`/`ui-permission` 离开清单(dsh-permission 要求有限权能力的执行器——preset 捆绑的是无限制执行器无法兑现的 sandbox 模式;见其构造函数守卫——客户端旋钮会宣传一个并不存在的边界),`approval` 服务也被禁用——Windows 清单里没有任何动作需要审批,模型也不会被告知"审批存在"或"请求会被自动拒绝"。保留仅限 fs 的路径规则是摆设:不限权的 shell 一条命令即可绕过,因此诚实的 Windows 姿态是全权访问,而不是一个只有 fs 工具假装执行的边界。 - **启动器按平台注入该层。** `apps/cli/src/windows-shell.ts` 在 `win32` 主机上从 base bundle 层的 `packageDir` 解析它,置于 bundle 层与用户层之间,覆盖所有组合路径(启动、config-only HMR 重组合、配置转储)。覆盖交付默认是组合决策:偏好 bash 栈(或偏好有限权)的 Windows 主机通过其 profile 或 home 的 `cordis.patch.yml` 重新启用 bash 行。未挂 base bundle 的自定义 profile 被跳过(它们自己拥有 shell 栈);base bundle 缺 `windows.cordis.patch.yml` 时 fail loud。 - **冷启动的模块解析已恢复。** profiles 重构把 pwsh 包从 `apps/cli` 的依赖闭包中删掉了,`healProfilesModuleFallback` 因此从未把它们链接进 `$DSH_HOME/profiles/node_modules`,新 Windows 主机解析不到插入的行。`apps/cli` 与 `dsh-base` 重新声明 `dsh-pwsh-local`/`dsh-tool-pwsh`,`dsh-base` 还声明 `dsh-fs-local`;按仓库惯例,base bundle 把每个行插件都列为依赖。 @@ -33,12 +33,12 @@ harness 交付的执行画像在每个平台都是 bash 优先。Windows 主机 ## 后果 - 运行交付版 `dsh` 表面的 Windows 主机无需配置即获得 `pwsh` 作为 shell 工具、PowerShell 作为 `ctx.bash` 执行器;那里的模型可见清单中没有 `bash`(其工具行被禁用)。 -- Windows 上没有任何沙箱:fs 工具不限权运行(`dsh-fs-local`)、`approval` 策略为 `never`、权限切换器消失。模型可见的姿态是诚实的全权访问,而不是一个 shell 可以绕过的边界。 +- Windows 上没有任何沙箱:fs 工具不限权运行(`dsh-fs-local`)、`approval` 服务不存在(没有任何动作需要审批,模型也不会被告知审批存在)、权限切换器消失。模型可见的姿态是诚实的全权访问,而不是一个 shell 可以绕过的边界。 - POSIX 主机不变:平台层永不生效,bash 栈仍是通用 `cordis.patch.yml` 的行。 - 偏好 bash 栈的 Windows 主机(例如 PATH 上有 WSL/Git-Bash 时)通过其 profile 或 home 的 `cordis.patch.yml` 覆盖交付默认——组合配置是唯一的覆盖通道。 ## 验证 -- 单元:`apps/cli/tests/windows-shell.spec.ts` 固定 win32 默认、自定义 profile 跳过与缺文件失败,平台注入;`packages/bundle/base/tests/base.spec.ts` 固定交付的 Windows 清单(禁用、插入与 `never` approval 策略)。 +- 单元:`apps/cli/tests/windows-shell.spec.ts` 固定 win32 默认、自定义 profile 跳过与缺文件失败,平台注入;`packages/bundle/base/tests/base.spec.ts` 固定交付的 Windows 清单(禁用、插入与缺席的 approval 服务)。 - Keyless:win32 上的 `dsh --profile --dump-config` 显示带 `windows.cordis.patch.yml` 出处的 pwsh 行、被禁用的 bash 行;POSIX 转储(CI Linux)不变。 - 真实组合冒烟在 win32 上启动 web profile,pwsh 栈挂载成功(即本笔记描述的确切清单)。 diff --git a/packages/bundle/base/README.i18n.yaml b/packages/bundle/base/README.i18n.yaml index 0cc7f47a1b..687c86ae0b 100644 --- a/packages/bundle/base/README.i18n.yaml +++ b/packages/bundle/base/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/bundle/base/README.md -README.md: 81932442fe9e2edac82bda88bbe28ce56fd8b6c1 -README.zh.md: b0af307b7655a8729a8face324a2750e2e480651 +README.md: 84e7f43aaa6cfead0ff28e30063e71062d358f0a +README.zh.md: 30c7c259f8893ebd34da6cab362dedac067ce099 diff --git a/packages/bundle/base/README.md b/packages/bundle/base/README.md index 81932442fe..84e7f43aaa 100644 --- a/packages/bundle/base/README.md +++ b/packages/bundle/base/README.md @@ -4,7 +4,7 @@ English | [中文](README.zh.md) The shared dsh core as a profile bundle: [`cordis.patch.yml`](cordis.patch.yml) inserts every base plugin row — model adapters, tools, persistence, policy, settings/credentials, repository Plugins, telemetry — over the empty profile root, as the first layer of every profile's `dsh.profile.bundles` list. Later bundle layers (e.g. [`dsh-web-app`](../web-app/README.md)) and the user's profile `cordis.patch.yml` override these rows by id; a patch replaces a row's whole `config`, so mode-specific values live in mode bundles, not here. The package has no runtime API; the profile composer resolves the universal patch through the `dsh.bundle.patch` manifest field, and the launcher reads the Windows platform layer below from code on win32 hosts. -Windows hosts booting a shipped profile additionally receive [`windows.cordis.patch.yml`](windows.cordis.patch.yml): it disables the POSIX-only sandboxed stacks — the bash executor/tool, the permission switcher (dsh-permission requires a confining executor), and the sandbox/fs-policy stack — and inserts the PowerShell executor and tool (`@deepseek-ai/dsh-pwsh-local`, `@deepseek-ai/dsh-tool-pwsh`) plus the unconfined `dsh-fs-local`, with the approval policy set to `never`. Windows has no OS sandbox runner (landlock/bwrap/seatbelt are POSIX-only), so the shipped posture is honest danger-full-access rather than a boundary only the fs tools pretend to enforce. The launcher applies the layer between the bundle layers and the user layers on win32 hosts; a Windows host that prefers the bash stack overrides these rows through its profile or home `cordis.patch.yml`. POSIX hosts never receive it. +Windows hosts booting a shipped profile additionally receive [`windows.cordis.patch.yml`](windows.cordis.patch.yml): it disables the POSIX-only sandboxed stacks — the bash executor/tool, the permission switcher (dsh-permission requires a confining executor), the sandbox/fs-policy stack, and the approval service — and inserts the PowerShell executor and tool (`@deepseek-ai/dsh-pwsh-local`, `@deepseek-ai/dsh-tool-pwsh`) plus the unconfined `dsh-fs-local`. Windows has no OS sandbox runner (landlock/bwrap/seatbelt are POSIX-only), so the shipped posture is honest danger-full-access rather than a boundary only the fs tools pretend to enforce; nothing in the roster asks for approval, so the approval service is absent. The launcher applies the layer between the bundle layers and the user layers on win32 hosts; a Windows host that prefers the bash stack overrides these rows through its profile or home `cordis.patch.yml`. POSIX hosts never receive it. The row set and its rationale are documented inline in the patch file; the [generated composition graph](../../../apps/cli/composition.md) renders it. @@ -19,4 +19,4 @@ None directly; each inserted row's package owns its effect. ## Known Limitations and Deferred Work - **A patch replaces whole row configs** — profile overrides must restate every field a row keeps; there is no deep-merge layer. -- **Windows has no sandbox** — no OS runner exists on win32 (landlock/bwrap/seatbelt are POSIX-only), so the Windows platform layer removes the whole sandbox stack: `sandbox`/`sandbox-policy`/`fs-sandbox` are disabled, `dsh-fs-local` provides `ctx.fs`, the permission switcher leaves the roster, and the approval policy is `never`. Everything degrades to danger-full-access: the shell is unconfined and the fs tools make no confinement claims. +- **Windows has no sandbox and no approval** — no OS runner exists on win32 (landlock/bwrap/seatbelt are POSIX-only), so the Windows platform layer removes the whole sandbox stack (`sandbox`/`sandbox-policy`/`fs-sandbox` disabled, `dsh-fs-local` provides `ctx.fs`), the permission switcher leaves the roster, and the approval service is disabled — nothing on Windows asks for approval, so the model is never told approval exists. Everything degrades to danger-full-access: the shell is unconfined and the fs tools make no confinement claims. diff --git a/packages/bundle/base/README.zh.md b/packages/bundle/base/README.zh.md index b0af307b76..30c7c259f8 100644 --- a/packages/bundle/base/README.zh.md +++ b/packages/bundle/base/README.zh.md @@ -4,7 +4,7 @@ 以 profile 组合包形式交付的共享 dsh 核心:[`cordis.patch.yml`](cordis.patch.yml) 在空的 profile 根之上插入全部基础插件行——模型适配器、工具、持久化、策略、settings/credentials、repository 插件、遥测——作为每个 profile 的 `dsh.profile.bundles` 列表中的第一层。后续的组合包层(例如 [`dsh-web-app`](../web-app/README.md))和用户 profile 的 `cordis.patch.yml` 按 id 覆盖这些行;patch 会替换目标行的整个 `config`,因此模式专属的值放在各模式组合包中,而不是这里。该包没有运行时 API;profile 组合器通过 manifest(元数据清单)的 `dsh.bundle.patch` 字段解析通用 patch,启动器在 win32 主机上通过代码读取下面的 Windows 平台层。 -启动交付 profile 的 Windows 主机还会额外收到 [`windows.cordis.patch.yml`](windows.cordis.patch.yml):它禁用仅 POSIX 的受限栈——bash 执行器/工具、权限切换器(dsh-permission 要求有限权能力的执行器)与 sandbox/fs 策略栈——并插入 PowerShell 执行器与工具(`@deepseek-ai/dsh-pwsh-local`、`@deepseek-ai/dsh-tool-pwsh`)以及不限权的 `dsh-fs-local`,`approval` 策略设为 `never`。Windows 上没有 OS 级 sandbox runner(landlock/bwrap/seatbelt 均为 POSIX 专属),因此交付姿态是诚实的 danger-full-access,而不是一个只有 fs 工具假装执行的边界。启动器在 win32 主机上把它应用于 bundle 层与用户层之间;偏好 bash 栈的 Windows 主机通过其 profile 或 home 的 `cordis.patch.yml` 覆盖这些行。POSIX 主机永远不会收到它。 +启动交付 profile 的 Windows 主机还会额外收到 [`windows.cordis.patch.yml`](windows.cordis.patch.yml):它禁用仅 POSIX 的受限栈——bash 执行器/工具、权限切换器(dsh-permission 要求有限权能力的执行器)、sandbox/fs 策略栈与 approval 服务——并插入 PowerShell 执行器与工具(`@deepseek-ai/dsh-pwsh-local`、`@deepseek-ai/dsh-tool-pwsh`)以及不限权的 `dsh-fs-local`。Windows 上没有 OS 级 sandbox runner(landlock/bwrap/seatbelt 均为 POSIX 专属),因此交付姿态是诚实的 danger-full-access,而不是一个只有 fs 工具假装执行的边界;清单里没有任何动作需要审批,因此 approval 服务缺席。启动器在 win32 主机上把它应用于 bundle 层与用户层之间;偏好 bash 栈的 Windows 主机通过其 profile 或 home 的 `cordis.patch.yml` 覆盖这些行。POSIX 主机永远不会收到它。 行集合及其设计依据以行内注释写在 patch 文件里;[生成的组合图](../../../apps/cli/composition.md)负责渲染它。 @@ -19,4 +19,4 @@ ## 已知限制与延期工作 - **patch 会替换整行 `config`**:profile 覆盖必须重述该行需要保留的每个字段;不存在深度合并层。 -- **Windows 上没有沙箱**:win32 上不存在 OS 级 runner(landlock/bwrap/seatbelt 均为 POSIX 专属),因此 Windows 平台层移除整个 sandbox 栈——`sandbox`/`sandbox-policy`/`fs-sandbox` 被禁用,由 `dsh-fs-local` 提供 `ctx.fs`,权限切换器离开清单,`approval` 策略为 `never`。一切退化为 danger-full-access:shell 不限权,fs 工具也不做任何限权声明。 +- **Windows 上没有沙箱、没有 approval**:win32 上不存在 OS 级 runner(landlock/bwrap/seatbelt 均为 POSIX 专属),因此 Windows 平台层移除整个 sandbox 栈——`sandbox`/`sandbox-policy`/`fs-sandbox` 被禁用,由 `dsh-fs-local` 提供 `ctx.fs`——权限切换器离开清单,approval 服务也被禁用:Windows 上没有任何动作需要审批,模型也不会被告知审批存在。一切退化为 danger-full-access:shell 不限权,fs 工具也不做任何限权声明。 diff --git a/packages/bundle/base/tests/base.spec.ts b/packages/bundle/base/tests/base.spec.ts index 6adcab1715..dc18224dda 100644 --- a/packages/bundle/base/tests/base.spec.ts +++ b/packages/bundle/base/tests/base.spec.ts @@ -46,7 +46,9 @@ describe('dsh-base bundle', () => { .map(patch => patch.id) // The POSIX-only sandboxed stacks leave the Windows roster as one unit: // shell (bash-sandbox/tool-bash), the permission switcher it requires, - // and the fs/sandbox policy stack whose OS runners do not exist on win32. + // the fs/sandbox policy stack whose OS runners do not exist on win32, + // and the approval service — nothing on Windows asks for approval, so + // the model is never told approval exists or that asks auto-reject. expect(disables).toEqual( expect.arrayContaining([ 'bash-sandbox', @@ -56,6 +58,7 @@ describe('dsh-base bundle', () => { 'sandbox', 'sandbox-policy', 'fs-sandbox', + 'approval', ]), ) const inserted = parsed @@ -64,9 +67,7 @@ describe('dsh-base bundle', () => { expect(inserted).toEqual( expect.arrayContaining(['pwsh-local', 'tool-pwsh', 'fs-local']), ) - // Full danger-full-access degradation: no approval prompts on Windows. - expect(parsed.find(patch => patch.id === 'approval')?.config).toEqual({ - policy: 'never', - }) + // Full danger-full-access degradation: no approval surface at all. + expect(parsed.find(patch => patch.id === 'approval')?.config).toBeUndefined() }) }) diff --git a/packages/bundle/base/windows.cordis.patch.yml b/packages/bundle/base/windows.cordis.patch.yml index 50dbe8d9e5..439861f922 100644 --- a/packages/bundle/base/windows.cordis.patch.yml +++ b/packages/bundle/base/windows.cordis.patch.yml @@ -1,12 +1,14 @@ # The dsh-base Windows platform layer: applied by the dsh launcher on win32 # hosts, between the bundle layers and the user layers. Windows cannot run # the POSIX-only sandboxed stacks, so this layer swaps the shipped bash stack -# for the PowerShell stack AND drops the sandbox entirely: no OS runner -# exists on Windows (landlock/bwrap/seatbelt are POSIX-only), so any policy -# would be theater — the unconfined shell could bypass fs-only path rules -# with one command. Windows therefore degrades to danger-full-access: +# for the PowerShell stack AND drops the whole permission surface: no OS +# runner exists on Windows (landlock/bwrap/seatbelt are POSIX-only), so any +# policy would be theater — the unconfined shell could bypass fs-only path +# rules with one command. Windows therefore degrades to danger-full-access: # unconfined pwsh + unconfined fs (`dsh-fs-local`), no permission switcher -# (dsh-permission requires a confining executor), approval policy `never`. +# (dsh-permission requires a confining executor), and no approval service — +# nothing in the roster asks for approval, and the model is never told +# approval exists or that requests are auto-rejected. # The launcher reads THIS file from the base bundle package (never through # dsh.bundle.patch — that field names the one universal layer). A Windows # host that prefers bash or confinement overrides these rows through its @@ -34,8 +36,7 @@ disabled: true - id: approval - config: - policy: never + disabled: true - insert: - id: pwsh-local diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 453109bcae..e99ec0f602 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -898,6 +898,9 @@ importers: '@deepseek-ai/dsh-credentials-local': specifier: workspace:^ version: link:../../credentials/credentials-local + '@deepseek-ai/dsh-fs-local': + specifier: workspace:^ + version: link:../../fs/fs-local '@deepseek-ai/dsh-fs-policy': specifier: workspace:^ version: link:../../fs/fs-policy From 94f61d29fa7da8021735688aafb04cb528648133 Mon Sep 17 00:00:00 2001 From: Huanqi Cao Date: Fri, 7 Aug 2026 19:21:43 +0800 Subject: [PATCH 04/11] fix(scripts): register dsh-base windows.cordis.patch.yml in workspace constraints --- scripts/check-workspace-constraints.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/check-workspace-constraints.ts b/scripts/check-workspace-constraints.ts index e0b9344cdf..f0cea80064 100644 --- a/scripts/check-workspace-constraints.ts +++ b/scripts/check-workspace-constraints.ts @@ -102,8 +102,9 @@ function workspaceManifests(): WorkspaceManifest[] { } const packageFileExtras: Readonly> = { - // Profile bundles publish their dsh.bundle.patch layer beside the lib. - '@deepseek-ai/dsh-base': ['cordis.patch.yml'], + // Profile bundles publish their dsh.bundle.patch layer beside the lib; + // dsh-base also ships the win32 shell platform layer the launcher reads. + '@deepseek-ai/dsh-base': ['cordis.patch.yml', 'windows.cordis.patch.yml'], '@deepseek-ai/dsh-web-app': ['cordis.patch.yml'], '@deepseek-ai/dsh-headless': ['cordis.patch.yml'], '@deepseek-ai/dsh-client-ui-theme': ['lib/styles'], From 1ee773317cccb88ddd35aec6ab49678ae69552c4 Mon Sep 17 00:00:00 2001 From: Huanqi Cao Date: Fri, 7 Aug 2026 19:21:49 +0800 Subject: [PATCH 05/11] docs(bundle): complete the Windows pwsh default contract per review - apps/cli/reference/README: state the win32 permission/sandbox/approval degradation so the workspace-write promise no longer misleads Windows users - bundle README + agent note: give the complete bash-restore recipe (disable pwsh-local/tool-pwsh and re-enable bash-sandbox/tool-bash), since both executors register the same bash service and an incomplete recipe fails loud at load - windows.cordis.patch.yml: header comment notes the recipe and that the ui-permission row belongs to dsh-web-app (base-only profiles get a harmless no-match warning) - profile-boot.ts: rewrap composeProfile JSDoc - re-record i18n hashes for the touched bilingual pairs --- .../feature/2026-08-01-windows-pwsh-default.i18n.yaml | 4 ++-- .../feature/2026-08-01-windows-pwsh-default.md | 2 +- .../feature/2026-08-01-windows-pwsh-default.zh.md | 2 +- apps/cli/reference/README.i18n.yaml | 4 ++-- apps/cli/reference/README.md | 2 +- apps/cli/reference/README.zh.md | 2 +- apps/cli/src/profile-boot.ts | 10 +++++----- packages/bundle/base/README.i18n.yaml | 4 ++-- packages/bundle/base/README.md | 2 +- packages/bundle/base/README.zh.md | 2 +- packages/bundle/base/windows.cordis.patch.yml | 8 ++++++++ 11 files changed, 25 insertions(+), 17 deletions(-) diff --git a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.i18n.yaml b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.i18n.yaml index 7ab92d8199..48f7c38aa5 100644 --- a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.md -2026-08-01-windows-pwsh-default.md: 75a27b5140abacf77b5f20028752a4903a50eaec -2026-08-01-windows-pwsh-default.zh.md: 8b940f1973835f8143bde05b37eba6f00e27f8f4 +2026-08-01-windows-pwsh-default.md: 4cd17497c0185ba61fb4994a9167d114640b4866 +2026-08-01-windows-pwsh-default.zh.md: 201e5dd2c7c0b791bf6474261846bc7f4061cc5b diff --git a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.md b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.md index 75a27b5140..4cd17497c0 100644 --- a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.md +++ b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.md @@ -35,7 +35,7 @@ The pwsh GUI rendering stage (stage 2 of the original roadmap) shipped earlier w - A Windows host running a shipped `dsh` surface gets `pwsh` as its shell tool and PowerShell as the `ctx.bash` executor without configuration; `bash` is absent from the model-visible roster there (its tool row is disabled). - Windows has no sandbox at all: the fs tools run unconfined (`dsh-fs-local`), the approval service is absent (nothing asks for approval, and the model is never told approval exists), and the permission switcher is gone. The model-visible posture is honest full access rather than a boundary the shell can bypass. - POSIX hosts are unchanged: the platform layer never applies, and the bash stack remains the universal `cordis.patch.yml` rows. -- Windows hosts that prefer the bash stack (e.g. with WSL/Git-Bash on PATH) override the shipped default through their profile or home `cordis.patch.yml` — composition config is the one override channel. +- Windows hosts that prefer the bash stack (e.g. with WSL/Git-Bash on PATH) override the shipped default through their profile or home `cordis.patch.yml` — disabling `pwsh-local`/`tool-pwsh` and re-enabling `bash-sandbox`/`tool-bash` (both executors register the same `bash` service, so an incomplete recipe fails loud at load) — composition config is the one override channel. ## Verification diff --git a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.zh.md b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.zh.md index 8b940f1973..201e5dd2c7 100644 --- a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.zh.md +++ b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.zh.md @@ -35,7 +35,7 @@ harness 交付的执行画像在每个平台都是 bash 优先。Windows 主机 - 运行交付版 `dsh` 表面的 Windows 主机无需配置即获得 `pwsh` 作为 shell 工具、PowerShell 作为 `ctx.bash` 执行器;那里的模型可见清单中没有 `bash`(其工具行被禁用)。 - Windows 上没有任何沙箱:fs 工具不限权运行(`dsh-fs-local`)、`approval` 服务不存在(没有任何动作需要审批,模型也不会被告知审批存在)、权限切换器消失。模型可见的姿态是诚实的全权访问,而不是一个 shell 可以绕过的边界。 - POSIX 主机不变:平台层永不生效,bash 栈仍是通用 `cordis.patch.yml` 的行。 -- 偏好 bash 栈的 Windows 主机(例如 PATH 上有 WSL/Git-Bash 时)通过其 profile 或 home 的 `cordis.patch.yml` 覆盖交付默认——组合配置是唯一的覆盖通道。 +- 偏好 bash 栈的 Windows 主机(例如 PATH 上有 WSL/Git-Bash 时)通过其 profile 或 home 的 `cordis.patch.yml` 覆盖交付默认——禁用 `pwsh-local`/`tool-pwsh` 并重新启用 `bash-sandbox`/`tool-bash`(两个执行器注册同一个 `bash` 服务,配方不完整会在加载时 fail loud)——组合配置是唯一的覆盖通道。 ## 验证 diff --git a/apps/cli/reference/README.i18n.yaml b/apps/cli/reference/README.i18n.yaml index 07d7810529..29cdfac304 100644 --- a/apps/cli/reference/README.i18n.yaml +++ b/apps/cli/reference/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write apps/cli/reference/README.md -README.md: 8b8a0e7dbebafedd6a4f8d988adb3fd11c7bd026 -README.zh.md: d1d6d5a594596a8be5db30021163f0fcea4a95bf +README.md: 07b162517b90215a6552810124f15c435144f7d4 +README.zh.md: 80030707474e59e76583ed7edc5ddce56334cc03 diff --git a/apps/cli/reference/README.md b/apps/cli/reference/README.md index 8b8a0e7dbe..07b162517b 100644 --- a/apps/cli/reference/README.md +++ b/apps/cli/reference/README.md @@ -51,7 +51,7 @@ Process shutdown gives the plugin tree up to five seconds to dispose. The first All modes treat the invoking directory as the default workspace root, load applicable `AGENTS.md` or `CLAUDE.md` instructions with a 65,536-byte render budget, and use an in-memory SQLite session content index. Long-lived surfaces watch valid edits of both `cordis.patch.yml` layers (profile and home) and reapply them transactionally; one-shot runs read the files once at startup. -New sessions default to the `workspace-write` permission preset. Bash and filesystem mutations are restricted to the session workspace and platform temporary roots; reads, network access, and process visibility are not confined. `DSH_PERMISSION_MODE` changes the process fallback. Stored General-settings permissions affect later Web sessions, not an already-open one. +New sessions default to the `workspace-write` permission preset. Bash and filesystem mutations are restricted to the session workspace and platform temporary roots; reads, network access, and process visibility are not confined. `DSH_PERMISSION_MODE` changes the process fallback. Stored General-settings permissions affect later Web sessions, not an already-open one. On win32 hosts booting a shipped profile, the Windows platform layer removes the permission, sandbox, and approval rows entirely: the pwsh shell and the fs tools run unconfined with no workspace boundary (Windows has no OS sandbox runner — landlock/bwrap/seatbelt are POSIX-only — so the shipped posture is honest danger-full-access rather than a boundary the shell could bypass), and `DSH_PERMISSION_MODE` and stored permission settings have no effect there. `DSH_TOOLS_MODE` selects `native`, `code`, or `both` for the process; another value fails at boot. [`config/core-web.cordis.yml`](../config/core-web.cordis.yml) is an optional RL-compatible `--patch` overlay that pins native mode, renders only `DSH_SYSTEM_PROMPT` or `You are a helpful software engineer assistant.` as the system prompt, disables Workspace instructions and every Web runtime prompt contribution, and exposes only persistent `bash` and `str_replace_editor` while retaining the shipped host, browser, workspace, persistence, and permission composition. diff --git a/apps/cli/reference/README.zh.md b/apps/cli/reference/README.zh.md index d1d6d5a594..8003070747 100644 --- a/apps/cli/reference/README.zh.md +++ b/apps/cli/reference/README.zh.md @@ -51,7 +51,7 @@ dsh web --dump-config 所有模式都将调用目录作为默认 workspace 根目录,以 65,536 字节渲染预算加载适用的 `AGENTS.md` 或 `CLAUDE.md` 指令,并使用内存 SQLite 会话内容索引。常驻 surface 监视两个 `cordis.patch.yml` 层(profile 与 home)的有效编辑并以事务方式重新应用;一次性运行只在启动时读取这些文件一次。 -新会话默认使用 `workspace-write` 权限预设。Bash 和文件系统修改仅限于会话 workspace 与平台临时根目录;读取、网络访问和进程可见性不受限制。`DSH_PERMISSION_MODE` 更改进程后备值。General settings 中存储的权限影响后续 Web 会话,不改变已打开的会话。 +新会话默认使用 `workspace-write` 权限预设。Bash 和文件系统修改仅限于会话 workspace 与平台临时根目录;读取、网络访问和进程可见性不受限制。`DSH_PERMISSION_MODE` 更改进程后备值。General settings 中存储的权限影响后续 Web 会话,不改变已打开的会话。在 win32 主机启动交付 profile 时,Windows 平台层会整体移除 permission、sandbox 与 approval 行:pwsh shell 与 fs 工具不受限运行,不存在 workspace 边界(Windows 上没有 OS 级 sandbox runner——landlock/bwrap/seatbelt 均为 POSIX 专属——因此交付姿态是诚实的 danger-full-access,而不是一个 shell 可以绕过的边界),`DSH_PERMISSION_MODE` 与存储的权限设置在彼处也不生效。 `DSH_TOOLS_MODE` 为进程选择 `native`、`code` 或 `both`;其他值会导致启动失败。[`config/core-web.cordis.yml`](../config/core-web.cordis.yml) 是可选的 RL 兼容 `--patch` overlay:它固定使用 `native` 模式,仅将 `DSH_SYSTEM_PROMPT` 或 `You are a helpful software engineer assistant.` 渲染为系统提示词,禁用 Workspace 指令与所有 Web 运行时提示词贡献,并且在保留随附宿主、浏览器、workspace、持久化和权限组合的同时,仅暴露持久 `bash` 和 `str_replace_editor`。 diff --git a/apps/cli/src/profile-boot.ts b/apps/cli/src/profile-boot.ts index 7115f1208a..6dab902845 100644 --- a/apps/cli/src/profile-boot.ts +++ b/apps/cli/src/profile-boot.ts @@ -132,11 +132,11 @@ function allPatches(composed: ComposedProfile): PatchOptions[] { /** * Load `name` and compose its effective patch stack: bundle layers in * `dsh.profile.bundles` order, the win32 shell platform layer (when the host - * is Windows), the - * profile's user layer, the home-level user layer (`$DSH_HOME/cordis.patch.yml` - * — machine-local preferences that apply to every profile, so it outranks the - * per-profile layer), `--patch` overlays, then flag patches derived from the - * composed rows, then the telemetry switch. + * is Windows), the profile's user layer, the home-level user layer + * (`$DSH_HOME/cordis.patch.yml` — machine-local preferences that apply to + * every profile, so it outranks the per-profile layer), `--patch` overlays, + * then flag patches derived from the composed rows, then the telemetry + * switch. * @param name - the profile name. * @param patchFiles - `--patch` overlay paths, in argv order. * @param deriveFlagPatches - launcher hook turning composed rows into flag patches. diff --git a/packages/bundle/base/README.i18n.yaml b/packages/bundle/base/README.i18n.yaml index 687c86ae0b..cc58dd3f8f 100644 --- a/packages/bundle/base/README.i18n.yaml +++ b/packages/bundle/base/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/bundle/base/README.md -README.md: 84e7f43aaa6cfead0ff28e30063e71062d358f0a -README.zh.md: 30c7c259f8893ebd34da6cab362dedac067ce099 +README.md: 9e2a6305fff592eb63f927dfcc4f32437edeb1cc +README.zh.md: 6159c23047bba4ced3345125901cc0c3ff7cd960 diff --git a/packages/bundle/base/README.md b/packages/bundle/base/README.md index 84e7f43aaa..9e2a6305ff 100644 --- a/packages/bundle/base/README.md +++ b/packages/bundle/base/README.md @@ -4,7 +4,7 @@ English | [中文](README.zh.md) The shared dsh core as a profile bundle: [`cordis.patch.yml`](cordis.patch.yml) inserts every base plugin row — model adapters, tools, persistence, policy, settings/credentials, repository Plugins, telemetry — over the empty profile root, as the first layer of every profile's `dsh.profile.bundles` list. Later bundle layers (e.g. [`dsh-web-app`](../web-app/README.md)) and the user's profile `cordis.patch.yml` override these rows by id; a patch replaces a row's whole `config`, so mode-specific values live in mode bundles, not here. The package has no runtime API; the profile composer resolves the universal patch through the `dsh.bundle.patch` manifest field, and the launcher reads the Windows platform layer below from code on win32 hosts. -Windows hosts booting a shipped profile additionally receive [`windows.cordis.patch.yml`](windows.cordis.patch.yml): it disables the POSIX-only sandboxed stacks — the bash executor/tool, the permission switcher (dsh-permission requires a confining executor), the sandbox/fs-policy stack, and the approval service — and inserts the PowerShell executor and tool (`@deepseek-ai/dsh-pwsh-local`, `@deepseek-ai/dsh-tool-pwsh`) plus the unconfined `dsh-fs-local`. Windows has no OS sandbox runner (landlock/bwrap/seatbelt are POSIX-only), so the shipped posture is honest danger-full-access rather than a boundary only the fs tools pretend to enforce; nothing in the roster asks for approval, so the approval service is absent. The launcher applies the layer between the bundle layers and the user layers on win32 hosts; a Windows host that prefers the bash stack overrides these rows through its profile or home `cordis.patch.yml`. POSIX hosts never receive it. +Windows hosts booting a shipped profile additionally receive [`windows.cordis.patch.yml`](windows.cordis.patch.yml): it disables the POSIX-only sandboxed stacks — the bash executor/tool, the permission switcher (dsh-permission requires a confining executor), the sandbox/fs-policy stack, and the approval service — and inserts the PowerShell executor and tool (`@deepseek-ai/dsh-pwsh-local`, `@deepseek-ai/dsh-tool-pwsh`) plus the unconfined `dsh-fs-local`. Windows has no OS sandbox runner (landlock/bwrap/seatbelt are POSIX-only), so the shipped posture is honest danger-full-access rather than a boundary only the fs tools pretend to enforce; nothing in the roster asks for approval, so the approval service is absent. The launcher applies the layer between the bundle layers and the user layers on win32 hosts; a Windows host that prefers the bash stack restores it through its profile or home `cordis.patch.yml` (disable `pwsh-local`/`tool-pwsh` and re-enable `bash-sandbox`/`tool-bash` — both executors register the same `bash` service, so an incomplete recipe fails loud at load). POSIX hosts never receive it. The row set and its rationale are documented inline in the patch file; the [generated composition graph](../../../apps/cli/composition.md) renders it. diff --git a/packages/bundle/base/README.zh.md b/packages/bundle/base/README.zh.md index 30c7c259f8..6159c23047 100644 --- a/packages/bundle/base/README.zh.md +++ b/packages/bundle/base/README.zh.md @@ -4,7 +4,7 @@ 以 profile 组合包形式交付的共享 dsh 核心:[`cordis.patch.yml`](cordis.patch.yml) 在空的 profile 根之上插入全部基础插件行——模型适配器、工具、持久化、策略、settings/credentials、repository 插件、遥测——作为每个 profile 的 `dsh.profile.bundles` 列表中的第一层。后续的组合包层(例如 [`dsh-web-app`](../web-app/README.md))和用户 profile 的 `cordis.patch.yml` 按 id 覆盖这些行;patch 会替换目标行的整个 `config`,因此模式专属的值放在各模式组合包中,而不是这里。该包没有运行时 API;profile 组合器通过 manifest(元数据清单)的 `dsh.bundle.patch` 字段解析通用 patch,启动器在 win32 主机上通过代码读取下面的 Windows 平台层。 -启动交付 profile 的 Windows 主机还会额外收到 [`windows.cordis.patch.yml`](windows.cordis.patch.yml):它禁用仅 POSIX 的受限栈——bash 执行器/工具、权限切换器(dsh-permission 要求有限权能力的执行器)、sandbox/fs 策略栈与 approval 服务——并插入 PowerShell 执行器与工具(`@deepseek-ai/dsh-pwsh-local`、`@deepseek-ai/dsh-tool-pwsh`)以及不限权的 `dsh-fs-local`。Windows 上没有 OS 级 sandbox runner(landlock/bwrap/seatbelt 均为 POSIX 专属),因此交付姿态是诚实的 danger-full-access,而不是一个只有 fs 工具假装执行的边界;清单里没有任何动作需要审批,因此 approval 服务缺席。启动器在 win32 主机上把它应用于 bundle 层与用户层之间;偏好 bash 栈的 Windows 主机通过其 profile 或 home 的 `cordis.patch.yml` 覆盖这些行。POSIX 主机永远不会收到它。 +启动交付 profile 的 Windows 主机还会额外收到 [`windows.cordis.patch.yml`](windows.cordis.patch.yml):它禁用仅 POSIX 的受限栈——bash 执行器/工具、权限切换器(dsh-permission 要求有限权能力的执行器)、sandbox/fs 策略栈与 approval 服务——并插入 PowerShell 执行器与工具(`@deepseek-ai/dsh-pwsh-local`、`@deepseek-ai/dsh-tool-pwsh`)以及不限权的 `dsh-fs-local`。Windows 上没有 OS 级 sandbox runner(landlock/bwrap/seatbelt 均为 POSIX 专属),因此交付姿态是诚实的 danger-full-access,而不是一个只有 fs 工具假装执行的边界;清单里没有任何动作需要审批,因此 approval 服务缺席。启动器在 win32 主机上把它应用于 bundle 层与用户层之间;偏好 bash 栈的 Windows 主机通过其 profile 或 home 的 `cordis.patch.yml` 恢复 bash 栈(禁用 `pwsh-local`/`tool-pwsh` 并重新启用 `bash-sandbox`/`tool-bash`——两个执行器注册同一个 `bash` 服务,配方不完整会在加载时 fail loud)。POSIX 主机永远不会收到它。 行集合及其设计依据以行内注释写在 patch 文件里;[生成的组合图](../../../apps/cli/composition.md)负责渲染它。 diff --git a/packages/bundle/base/windows.cordis.patch.yml b/packages/bundle/base/windows.cordis.patch.yml index 439861f922..1abd4ba568 100644 --- a/packages/bundle/base/windows.cordis.patch.yml +++ b/packages/bundle/base/windows.cordis.patch.yml @@ -13,6 +13,14 @@ # dsh.bundle.patch — that field names the one universal layer). A Windows # host that prefers bash or confinement overrides these rows through its # profile or home cordis.patch.yml. +# The bash-restore recipe must be complete: disable pwsh-local and tool-pwsh +# AND re-enable bash-sandbox and tool-bash (plus permission/ui-permission only +# if the switcher is wanted) — both executors register the same 'bash' +# service, so re-enabling the bash rows while pwsh-local stays inserted fails +# loud at load on a duplicate registration. +# The ui-permission disable targets a row owned by dsh-web-app, not dsh-base: +# a base-only profile (e.g. the `dsh plugin --profile` default template) has +# no such row, and the no-match logs a harmless warning on every load. - id: bash-sandbox disabled: true From 431783426e62fa5897a503546ba39f705260e173 Mon Sep 17 00:00:00 2001 From: Huanqi Cao Date: Fri, 7 Aug 2026 20:50:36 +0800 Subject: [PATCH 06/11] test(cli): compose the real Windows shell roster through the shipped bundle layers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The resolver suite previously exercised only fixture patch lists, leaving the real shipped windows.cordis.patch.yml and the bundle→windows→user composition ordering untested on Linux CI. The new cases load a temp profile whose bundle layers resolve from the real dsh-base/dsh-web-app packages (app installation anchor), apply the platform layer through the boot's own composeEntries algorithm with the platform injected, and assert the win32 danger-full-access roster (eight disables, three inserts, no warnings on the web profile). A second case pins POSIX unchanged and the base-only-profile ui-permission no-match warning as warned-but-harmless, matching the patch header's documented contract. --- .../2026-08-01-windows-pwsh-default.i18n.yaml | 4 +- .../2026-08-01-windows-pwsh-default.md | 2 +- .../2026-08-01-windows-pwsh-default.zh.md | 2 +- apps/cli/tests/windows-shell.spec.ts | 60 +++++++++++++++++++ 4 files changed, 64 insertions(+), 4 deletions(-) diff --git a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.i18n.yaml b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.i18n.yaml index 48f7c38aa5..af9f64208f 100644 --- a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.md -2026-08-01-windows-pwsh-default.md: 4cd17497c0185ba61fb4994a9167d114640b4866 -2026-08-01-windows-pwsh-default.zh.md: 201e5dd2c7c0b791bf6474261846bc7f4061cc5b +2026-08-01-windows-pwsh-default.md: ea45040a0eb6f1325ed8f3a02a477d5dfd614696 +2026-08-01-windows-pwsh-default.zh.md: 41324afccdd775cac1629ed69217cff4237be091 diff --git a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.md b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.md index 4cd17497c0..ea45040a0e 100644 --- a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.md +++ b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.md @@ -39,6 +39,6 @@ The pwsh GUI rendering stage (stage 2 of the original roadmap) shipped earlier w ## Verification -- Unit: `apps/cli/tests/windows-shell.spec.ts` pins the win32 default, the custom-profile skip, and the missing-patch failure, with the platform injected; `packages/bundle/base/tests/base.spec.ts` pins the shipped Windows roster (disables, inserts, and the absent approval service). +- Unit: `apps/cli/tests/windows-shell.spec.ts` pins the win32 default, the custom-profile skip, and the missing-patch failure with the platform injected, and composes the REAL shipped bundle layers (dsh-base + dsh-web-app resolved from the app installation) through the boot's patch algorithm to assert the win32 danger-full-access roster and the base-only-profile warning; `packages/bundle/base/tests/base.spec.ts` pins the shipped Windows patch file shape (disables, inserts, and the absent approval service). - Keyless: a win32 `dsh --profile --dump-config` shows the pwsh rows with `windows.cordis.patch.yml` provenance and the bash rows disabled; the POSIX dump (CI Linux) is unchanged. - The real-composition smoke boots the web profile on win32 with the pwsh stack mounted (the exact roster this note describes). diff --git a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.zh.md b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.zh.md index 201e5dd2c7..41324afccd 100644 --- a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.zh.md +++ b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.zh.md @@ -39,6 +39,6 @@ harness 交付的执行画像在每个平台都是 bash 优先。Windows 主机 ## 验证 -- 单元:`apps/cli/tests/windows-shell.spec.ts` 固定 win32 默认、自定义 profile 跳过与缺文件失败,平台注入;`packages/bundle/base/tests/base.spec.ts` 固定交付的 Windows 清单(禁用、插入与缺席的 approval 服务)。 +- 单元:`apps/cli/tests/windows-shell.spec.ts` 以平台注入固定 win32 默认、自定义 profile 跳过与缺文件失败,并通过启动所用的 patch 算法组合真实交付的 bundle 层(从应用安装解析的 dsh-base + dsh-web-app)断言 win32 danger-full-access 清单与 base-only profile 警告;`packages/bundle/base/tests/base.spec.ts` 固定交付的 Windows patch 文件形状(禁用、插入与缺席的 approval 服务)。 - Keyless:win32 上的 `dsh --profile --dump-config` 显示带 `windows.cordis.patch.yml` 出处的 pwsh 行、被禁用的 bash 行;POSIX 转储(CI Linux)不变。 - 真实组合冒烟在 win32 上启动 web profile,pwsh 栈挂载成功(即本笔记描述的确切清单)。 diff --git a/apps/cli/tests/windows-shell.spec.ts b/apps/cli/tests/windows-shell.spec.ts index f0cfddbaae..80ba40cc34 100644 --- a/apps/cli/tests/windows-shell.spec.ts +++ b/apps/cli/tests/windows-shell.spec.ts @@ -2,7 +2,9 @@ import { afterEach, describe, expect, it } from 'vitest' import { mkdtempSync, writeFileSync, rmSync, mkdirSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' +import { fileURLToPath } from 'node:url' import type { ProfileLayer } from '@deepseek-ai/dsh-app-boot' +import { composeEntries, initProfile, loadProfile, PROFILES_DIR } from '@deepseek-ai/dsh-app-boot' import { BASE_BUNDLE, resolveWindowsShellLayer, @@ -62,3 +64,61 @@ describe('resolveWindowsShellLayer', () => { .toThrow(/@deepseek-ai\/dsh-base ships no windows\.cordis\.patch\.yml/) }) }) + +describe('the shipped Windows composition (real bundle layers)', () => { + let home: string + afterEach(() => { if (home !== undefined) rmSync(home, { recursive: true, force: true }) }) + // The app installation anchor, mirroring profile-boot.ts: the bundle layers + // resolve from the REAL dsh-base/dsh-web-app packages through it, so this + // suite composes the shipped patch files, not test fixtures. + const anchor = fileURLToPath(new URL('../package.json', import.meta.url)) + + it('composes the win32 danger-full-access roster through the real patch layers', () => { + home = mkdtempSync(join(tmpdir(), 'dsh-windows-home-')) + initProfile(join(home, PROFILES_DIR, 'web'), ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app']) + const profile = loadProfile('dsh', 'web', anchor, home) + const warnings: string[] = [] + const win32 = resolveWindowsShellLayer('win32', profile.layers, 'dsh') + expect(win32).toBeDefined() + const rows = composeEntries( + [...profile.layers.map(layer => layer.patches), win32!.patches], + message => warnings.push(message), + ) + const byId = new Map(rows.map(row => [row.id, row])) + for (const id of ['bash-sandbox', 'tool-bash', 'permission', 'ui-permission', + 'sandbox', 'sandbox-policy', 'fs-sandbox', 'approval']) { + expect(byId.get(id)?.disabled, `row ${id}`).toBe(true) + } + for (const id of ['pwsh-local', 'tool-pwsh', 'fs-local']) { + expect(byId.has(id), `inserted row ${id}`).toBe(true) + } + // The web-app layer provides ui-permission, so the full web profile + // composes without any no-match warning. + expect(warnings).toEqual([]) + }) + + it('leaves POSIX untouched and base-only profiles warned but harmless', () => { + home = mkdtempSync(join(tmpdir(), 'dsh-windows-home-')) + initProfile(join(home, PROFILES_DIR, 'web'), ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app']) + const profile = loadProfile('dsh', 'web', anchor, home) + // POSIX: no platform layer, the bash stack stays enabled. + const posixRows = composeEntries(profile.layers.map(layer => layer.patches)) + const posixById = new Map(posixRows.map(row => [row.id, row])) + expect(posixById.get('bash-sandbox')?.disabled).not.toBe(true) + expect(posixById.has('pwsh-local')).toBe(false) + + // A base-only custom profile (the DEFAULT_PROFILE_BUNDLES template): + // ui-permission has no row to patch, so the shipped layer warns once per + // composition — never fails — exactly as its header comment documents. + initProfile(join(home, PROFILES_DIR, 'base-only'), ['@deepseek-ai/dsh-base']) + const baseOnly = loadProfile('dsh', 'base-only', anchor, home) + const baseWarnings: string[] = [] + const win32 = resolveWindowsShellLayer('win32', baseOnly.layers, 'dsh') + expect(win32).toBeDefined() + composeEntries( + [...baseOnly.layers.map(layer => layer.patches), win32!.patches], + message => baseWarnings.push(message), + ) + expect(baseWarnings.some(message => message.includes('ui-permission'))).toBe(true) + }) +}) From f1af1831efeb1106860ddc09ddd4a53f0d4b7c6c Mon Sep 17 00:00:00 2001 From: Huanqi Cao Date: Sat, 8 Aug 2026 18:10:21 +0800 Subject: [PATCH 07/11] fix(docs): point the parity note at the implemented windows-pwsh-default note and re-record the pair --- .../feature/2026-08-05-pwsh-ui-bash-parity.i18n.yaml | 4 ++-- .../implemented/feature/2026-08-05-pwsh-ui-bash-parity.md | 2 +- .../implemented/feature/2026-08-05-pwsh-ui-bash-parity.zh.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.agents/notes/implemented/feature/2026-08-05-pwsh-ui-bash-parity.i18n.yaml b/.agents/notes/implemented/feature/2026-08-05-pwsh-ui-bash-parity.i18n.yaml index 51401adfdc..5ad32eb099 100644 --- a/.agents/notes/implemented/feature/2026-08-05-pwsh-ui-bash-parity.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-05-pwsh-ui-bash-parity.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-05-pwsh-ui-bash-parity.md -2026-08-05-pwsh-ui-bash-parity.md: 815b448b894e9c53b4c4a2076f6b94fdd316dd35 -2026-08-05-pwsh-ui-bash-parity.zh.md: 967c5a9e1409028043dc5028fdca640ddfeb1acc +2026-08-05-pwsh-ui-bash-parity.md: 592c3c922308d53f1bb83ff3ea371c38b99d5be7 +2026-08-05-pwsh-ui-bash-parity.zh.md: 82a8dbb87ed60ca7528b76c6b0d66872d9edc7f2 diff --git a/.agents/notes/implemented/feature/2026-08-05-pwsh-ui-bash-parity.md b/.agents/notes/implemented/feature/2026-08-05-pwsh-ui-bash-parity.md index 815b448b89..592c3c9223 100644 --- a/.agents/notes/implemented/feature/2026-08-05-pwsh-ui-bash-parity.md +++ b/.agents/notes/implemented/feature/2026-08-05-pwsh-ui-bash-parity.md @@ -6,7 +6,7 @@ English | [中文](2026-08-05-pwsh-ui-bash-parity.zh.md) ## Problem -The [pwsh tool bash parity decision](../../implemented/feature/2026-08-02-pwsh-tool-bash-parity.md) made `dsh-tool-pwsh` behaviorally interchangeable with `dsh-tool-bash` for execution, markers, and background tasks, but explicitly deferred the human-visible half: a completed pwsh foreground call presented as a generic `console`-fenced card while the bash tool's completed call presented as a terminal card with a parsed exit-status pill. The roadmap that owned this gap ([Windows defaults to pwsh](../../proposed/feature/2026-08-01-windows-pwsh-default.md)) named "pwsh TUI/GUI rendering" as stage 2, but the TUI package was removed, leaving the Web surface as the only UI the gap affects. +The [pwsh tool bash parity decision](../../implemented/feature/2026-08-02-pwsh-tool-bash-parity.md) made `dsh-tool-pwsh` behaviorally interchangeable with `dsh-tool-bash` for execution, markers, and background tasks, but explicitly deferred the human-visible half: a completed pwsh foreground call presented as a generic `console`-fenced card while the bash tool's completed call presented as a terminal card with a parsed exit-status pill. The roadmap that owned this gap ([Windows defaults to pwsh](../../implemented/feature/2026-08-01-windows-pwsh-default.md)) named "pwsh TUI/GUI rendering" as stage 2, but the TUI package was removed, leaving the Web surface as the only UI the gap affects. ## Decision diff --git a/.agents/notes/implemented/feature/2026-08-05-pwsh-ui-bash-parity.zh.md b/.agents/notes/implemented/feature/2026-08-05-pwsh-ui-bash-parity.zh.md index 967c5a9e14..82a8dbb87e 100644 --- a/.agents/notes/implemented/feature/2026-08-05-pwsh-ui-bash-parity.zh.md +++ b/.agents/notes/implemented/feature/2026-08-05-pwsh-ui-bash-parity.zh.md @@ -6,7 +6,7 @@ Status: implemented ## Problem -[pwsh 工具与 bash 对齐决策](../../implemented/feature/2026-08-02-pwsh-tool-bash-parity.md) 让 `dsh-tool-pwsh` 在执行、marker 与后台任务上行为可互换,但明确推迟了面向人类的一半:完成的 pwsh 前台调用呈现为通用 `console` 围栏卡片,而 bash 工具的完成调用呈现为带解析退出状态 pill 的 terminal 卡。拥有此缺口的路线图([Windows 默认改用 pwsh](../../proposed/feature/2026-08-01-windows-pwsh-default.md))把 "pwsh TUI/GUI 渲染" 列为阶段 2,但 TUI 包已被移除,使 Web 表面成为该缺口唯一影响的 UI。 +[pwsh 工具与 bash 对齐决策](../../implemented/feature/2026-08-02-pwsh-tool-bash-parity.md) 让 `dsh-tool-pwsh` 在执行、marker 与后台任务上行为可互换,但明确推迟了面向人类的一半:完成的 pwsh 前台调用呈现为通用 `console` 围栏卡片,而 bash 工具的完成调用呈现为带解析退出状态 pill 的 terminal 卡。拥有此缺口的路线图([Windows 默认改用 pwsh](../../implemented/feature/2026-08-01-windows-pwsh-default.md))把 "pwsh TUI/GUI 渲染" 列为阶段 2,但 TUI 包已被移除,使 Web 表面成为该缺口唯一影响的 UI。 ## Decision From 69fd67bfd826e143449cd778926e39de7584a99f Mon Sep 17 00:00:00 2001 From: Huanqi Cao Date: Sat, 8 Aug 2026 18:25:48 +0800 Subject: [PATCH 08/11] fix(docs): restore the cli reference pairing record to the multi-line contract after the merge-forward --- apps/cli/reference/README.i18n.yaml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/cli/reference/README.i18n.yaml b/apps/cli/reference/README.i18n.yaml index 722725bbdf..f11f4de700 100644 --- a/apps/cli/reference/README.i18n.yaml +++ b/apps/cli/reference/README.i18n.yaml @@ -1 +1,6 @@ -# Bilingual-pair consistency record (docs/i18n/README.md): 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 re-record with:# pnpm run verify-translation-pairing --write apps/cli/reference/README.mdREADME.md: 2a0a0d5ef62e3e89c6cdaf04f9fd63de2f882ce1README.zh.md: 69d3f4f78c50cce35e82039bedd10506c4d0a9a1 \ No newline at end of file +# Bilingual-pair consistency record (docs/i18n/README.md): 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 re-record with: +# pnpm run verify-translation-pairing --write apps/cli/reference/README.md +README.md: 2a0a0d5ef62e3e89c6cdaf04f9fd63de2f882ce1 +README.zh.md: 69d3f4f78c50cce35e82039bedd10506c4d0a9a1 From 428f84f710787a821f49f80a1cf69e383b9a22e5 Mon Sep 17 00:00:00 2001 From: Huanqi Cao Date: Sun, 9 Aug 2026 17:11:18 +0800 Subject: [PATCH 09/11] test(docs): split placed-image paths on either separator The placer tests receive absolute paths; on Windows the POSIX-only split kept the whole path as the basename. The windows-acl branch already carries this fix from an earlier merge-forward. --- scripts/project-doc-site.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/project-doc-site.spec.ts b/scripts/project-doc-site.spec.ts index d3f00f3408..124656a4a1 100644 --- a/scripts/project-doc-site.spec.ts +++ b/scripts/project-doc-site.spec.ts @@ -148,7 +148,7 @@ describe('rewriteMarkdown', () => { repoRoot: root, repositoryRef: 'abc123', placeImage: (absPath) => { - const name = absPath.split('/').pop() ?? '' + const name = absPath.split(/[\\/]/u).pop() ?? '' placed.push(name) return `./${name}` }, @@ -167,7 +167,7 @@ describe('rewriteMarkdown', () => { pages, repoRoot: root, repositoryRef: 'abc123', - placeImage: absPath => `./${absPath.split('/').pop() ?? ''}`, + placeImage: absPath => `./${absPath.split(/[\\/]/u).pop() ?? ''}`, })).toBe('![logo](./logo.svg#view)\n') }) From 63ad5d6d9810961eef240ccba2ea093b190b557a Mon Sep 17 00:00:00 2001 From: Huanqi Cao Date: Sun, 9 Aug 2026 18:53:38 +0800 Subject: [PATCH 10/11] refactor(cli): drop the redundant existence pre-check in resolveWindowsShellLayer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit loadOverlayPatches already throws on a missing file (the caller named it, so absence is a misconfiguration) — the existsSync guard was a second fail-loud mechanism for the same miss with a prettier message. The loader's throw keeps the fail-loud contract the Windows-default note records. --- apps/cli/src/windows-shell.ts | 9 +++------ apps/cli/tests/windows-shell.spec.ts | 4 +++- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/apps/cli/src/windows-shell.ts b/apps/cli/src/windows-shell.ts index 425699ef4f..c36dc0519c 100644 --- a/apps/cli/src/windows-shell.ts +++ b/apps/cli/src/windows-shell.ts @@ -10,7 +10,6 @@ * @module @deepseek-ai/dsh/windows-shell */ -import { existsSync } from 'node:fs' import { join } from 'node:path' import type { PatchOptions } from '@cordisjs/plugin-include' import { loadOverlayPatches, type ProfileLayer } from '@deepseek-ai/dsh-app-boot' @@ -36,8 +35,9 @@ export interface WindowsShellLayer { * @param binName - the diagnostic prefix on thrown errors (`dsh`). * @returns the pwsh layer on win32, else `undefined`. A custom profile that * mounts no base bundle is skipped (it owns its shell stack); a base - * bundle that ships no Windows shell patch fails loud — the shipped - * package always carries it, so a miss is a broken installation. + * bundle whose Windows shell patch is missing fails loud in + * {@link loadOverlayPatches} — the shipped package always carries it, so + * a miss is a broken installation. */ export function resolveWindowsShellLayer( platform: NodeJS.Platform, @@ -48,8 +48,5 @@ export function resolveWindowsShellLayer( const base = layers.find(layer => layer.packageName === BASE_BUNDLE) if (base === undefined) return undefined const label = join(base.packageDir, WINDOWS_SHELL_PATCH_FILENAME) - if (!existsSync(label)) { - throw new Error(`${binName}: ${BASE_BUNDLE} ships no ${WINDOWS_SHELL_PATCH_FILENAME}`) - } return { label, patches: loadOverlayPatches(binName, label) } } diff --git a/apps/cli/tests/windows-shell.spec.ts b/apps/cli/tests/windows-shell.spec.ts index 80ba40cc34..fc562b7008 100644 --- a/apps/cli/tests/windows-shell.spec.ts +++ b/apps/cli/tests/windows-shell.spec.ts @@ -60,8 +60,10 @@ describe('resolveWindowsShellLayer', () => { it('fails loud when the base bundle ships no Windows shell patch', () => { const base = tempBase() mkdirSync(base, { recursive: true }) + // The overlay loader owns the fail-loud contract: the caller named this + // file, so its absence is a misconfiguration, not "no overlay". expect(() => resolveWindowsShellLayer('win32', [fakeLayer(BASE_BUNDLE, base)], 'dsh')) - .toThrow(/@deepseek-ai\/dsh-base ships no windows\.cordis\.patch\.yml/) + .toThrow(/dsh: failed to read overlay .*windows\.cordis\.patch\.yml/) }) }) From 4cfe4366d7458513553bb1a4de346acdbbb81379 Mon Sep 17 00:00:00 2001 From: Huanqi Cao Date: Sun, 9 Aug 2026 22:58:10 +0800 Subject: [PATCH 11/11] docs(note): drop the stage-numbering residual from the windows-default note The roadmap stage reference dated the decision record; the purge standard removes change-history narration and stage numbering from implemented notes. --- .../feature/2026-08-01-windows-pwsh-default.i18n.yaml | 4 ++-- .../implemented/feature/2026-08-01-windows-pwsh-default.md | 2 +- .../implemented/feature/2026-08-01-windows-pwsh-default.zh.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.i18n.yaml b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.i18n.yaml index af9f64208f..49b713f534 100644 --- a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.md -2026-08-01-windows-pwsh-default.md: ea45040a0eb6f1325ed8f3a02a477d5dfd614696 -2026-08-01-windows-pwsh-default.zh.md: 41324afccdd775cac1629ed69217cff4237be091 +2026-08-01-windows-pwsh-default.md: f0da86e52bcdd53a10b60164d7cc12261cfc5c49 +2026-08-01-windows-pwsh-default.zh.md: 41a6429eab8f86a8960ac4aa372aeacfda4661c4 diff --git a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.md b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.md index ea45040a0e..f0da86e52b 100644 --- a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.md +++ b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.md @@ -16,7 +16,7 @@ Windows hosts booting a shipped profile (`dsh web`, `dsh --profile headless`, on - **The launcher injects the layer by platform.** `apps/cli/src/windows-shell.ts` resolves it from the base bundle layer's `packageDir` between the bundle layers and the user layers on `win32` hosts, in every composition path (boot, config-only HMR recomposition, config dumps). Overriding the shipped default is a composition decision: a Windows host that prefers the bash stack — or confinement — re-enables the bash rows through its profile or home `cordis.patch.yml`. Custom profiles without the base bundle are skipped (they own their shell stack); a base bundle that ships no Windows shell patch fails loud. - **Module resolution is restored for cold starts.** The profiles-rework CLI dropped the pwsh packages from `apps/cli`'s dependency closure, so `healProfilesModuleFallback` never linked them into `$DSH_HOME/profiles/node_modules` and a fresh Windows host could not resolve the inserted rows. `apps/cli` and `dsh-base` re-declare `dsh-pwsh-local`/`dsh-tool-pwsh`, and `dsh-base` also declares `dsh-fs-local`; the base bundle lists every row plugin as a dependency by house style. -The pwsh GUI rendering stage (stage 2 of the original roadmap) shipped earlier with the [pwsh UI presentation matches bash decision](2026-08-05-pwsh-ui-bash-parity.md); the [pwsh tool bash parity decision](2026-08-02-pwsh-tool-bash-parity.md) ships the tool's surface. Nothing in this decision changes POSIX behavior. +The pwsh GUI rendering shipped earlier with the [pwsh UI presentation matches bash decision](2026-08-05-pwsh-ui-bash-parity.md); the [pwsh tool bash parity decision](2026-08-02-pwsh-tool-bash-parity.md) ships the tool's surface. Nothing in this decision changes POSIX behavior. ## Alternatives considered diff --git a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.zh.md b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.zh.md index 41324afccd..41a6429eab 100644 --- a/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.zh.md +++ b/.agents/notes/implemented/feature/2026-08-01-windows-pwsh-default.zh.md @@ -16,7 +16,7 @@ harness 交付的执行画像在每个平台都是 bash 优先。Windows 主机 - **启动器按平台注入该层。** `apps/cli/src/windows-shell.ts` 在 `win32` 主机上从 base bundle 层的 `packageDir` 解析它,置于 bundle 层与用户层之间,覆盖所有组合路径(启动、config-only HMR 重组合、配置转储)。覆盖交付默认是组合决策:偏好 bash 栈(或偏好有限权)的 Windows 主机通过其 profile 或 home 的 `cordis.patch.yml` 重新启用 bash 行。未挂 base bundle 的自定义 profile 被跳过(它们自己拥有 shell 栈);base bundle 缺 `windows.cordis.patch.yml` 时 fail loud。 - **冷启动的模块解析已恢复。** profiles 重构把 pwsh 包从 `apps/cli` 的依赖闭包中删掉了,`healProfilesModuleFallback` 因此从未把它们链接进 `$DSH_HOME/profiles/node_modules`,新 Windows 主机解析不到插入的行。`apps/cli` 与 `dsh-base` 重新声明 `dsh-pwsh-local`/`dsh-tool-pwsh`,`dsh-base` 还声明 `dsh-fs-local`;按仓库惯例,base bundle 把每个行插件都列为依赖。 -原路线图的阶段 2(pwsh GUI 渲染)已随 [pwsh UI 呈现与 bash 对齐决策](2026-08-05-pwsh-ui-bash-parity.md) 先行交付;[pwsh 工具与 bash 对齐决策](2026-08-02-pwsh-tool-bash-parity.md) 交付了工具表面。本决策不改变任何 POSIX 行为。 +pwsh GUI 渲染已随 [pwsh UI 呈现与 bash 对齐决策](2026-08-05-pwsh-ui-bash-parity.md) 先行交付;[pwsh 工具与 bash 对齐决策](2026-08-02-pwsh-tool-bash-parity.md) 交付了工具表面。本决策不改变任何 POSIX 行为。 ## 备选方案