dsh-plugin-wsl-env 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +417 -0
  2. package/CONTRIBUTING.md +2 -2
  3. package/README.md +84 -68
  4. package/SUPPORT.md +3 -3
  5. package/agent/native/linux-aarch64/dsh-wsl-agent +0 -0
  6. package/agent/native/linux-x86_64/dsh-wsl-agent +0 -0
  7. package/agent/wsl-agent.sh +83 -16
  8. package/cordis.patch.yml +41 -35
  9. package/docs/ARCHITECTURE.md +9 -1
  10. package/docs/CONFIGURATION.md +7 -4
  11. package/docs/LIMITATIONS.md +53 -18
  12. package/docs/PARITY.md +3 -1
  13. package/docs/PEER-PARITY.md +18 -11
  14. package/docs/README.zh.md +97 -74
  15. package/docs/UPSTREAM-FSIO-EXPORT.md +3 -3
  16. package/docs/UPSTREAM-TERMINAL-TITLE.md +9 -2
  17. package/examples/preset-wsl-session-sandbox.cordis.patch.yml +178 -0
  18. package/lib/agent-protocol.d.ts +10 -4
  19. package/lib/agent-protocol.js +10 -3
  20. package/lib/agent.d.ts +84 -0
  21. package/lib/agent.js +167 -15
  22. package/lib/bwrap.d.ts +8 -0
  23. package/lib/bwrap.js +4 -1
  24. package/lib/fs-routing.d.ts +40 -4
  25. package/lib/fs-routing.js +80 -12
  26. package/lib/fsio-agent.js +1 -1
  27. package/lib/index.d.ts +134 -0
  28. package/lib/index.js +191 -3
  29. package/lib/native-agent.d.ts +124 -0
  30. package/lib/native-agent.js +198 -0
  31. package/lib/ports.d.ts +13 -2
  32. package/lib/ports.js +18 -5
  33. package/lib/preflight.js +24 -1
  34. package/lib/session-runtime.d.ts +161 -0
  35. package/lib/session-runtime.js +220 -0
  36. package/lib/session-scope.d.ts +124 -0
  37. package/lib/session-scope.js +191 -0
  38. package/lib/shell-env.js +10 -3
  39. package/lib/watcher.d.ts +25 -12
  40. package/lib/watcher.js +120 -32
  41. package/lib/workspace-files-route.d.ts +8 -4
  42. package/lib/workspace-files-route.js +42 -14
  43. package/lib/workspace-files-wsl.js +25 -8
  44. package/package.json +34 -25
package/docs/README.zh.md CHANGED
@@ -6,11 +6,11 @@
6
6
  [![npm](https://img.shields.io/npm/v/dsh-plugin-wsl-env)](https://www.npmjs.com/package/dsh-plugin-wsl-env)
7
7
  [![license](https://img.shields.io/npm/l/dsh-plugin-wsl-env)](../LICENSE)
8
8
 
9
- 这个插件让 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 把 WSL 子系统当作工作环境:命令在子系统里执行,模型的文件工具读写子系统里的真实文件,文件夹选择器可以直接打开子系统目录,GUI 终端也开在子系统里。
9
+ 这个插件让 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 在 WSL 子系统里干活:模型的命令在子系统里执行,文件工具读写的是子系统的真实文件,文件夹选择器能打开子系统目录,GUI 终端开的也是子系统里的 shell。
10
10
 
11
- **只支持 Windows + WSL2。** 安装只需一条命令,插件自身没有依赖。环境按会话生效:打开 Windows 文件夹的会话继续使用原来的 Windows 环境。
11
+ **只支持 Windows + WSL2。** 安装只需一条命令,插件自身没有依赖。环境按会话生效:打开 Windows 文件夹的会话,用的还是原来的 Windows 工具。
12
12
 
13
- [安装](#安装) · [使用](#使用) · [配置](#配置) · [配方](#配方) · [架构](#架构) · [沙箱](#沙箱) · [常见问题](#常见问题) ·
13
+ [安装](#安装) · [使用](#使用) · [方案对比](#方案对比) · [配置](#配置) · [配方](#配方) · [架构](#架构) · [沙箱](#沙箱) · [常见问题](#常见问题) ·
14
14
  [开发](#开发) · [文档](#文档)
15
15
 
16
16
  ## 安装
@@ -18,108 +18,128 @@
18
18
  在 Windows 终端里执行这四条命令:
19
19
 
20
20
  ```powershell
21
- dsh wsl --from-default-profile web --dump-config # 1. 用 Web 模板创建 profile
22
- dsh plugin --profile wsl add dsh-plugin-wsl-env # 2. 安装代码和配置层
23
- dsh --profile wsl --dump-config # 3. 只做组合检查,不启动(最快)
21
+ dsh wsl --from-default-profile web --dump-config # 1. 创建 profile(dsh 的一套命名配置),模板用 Web
22
+ dsh plugin --profile wsl add dsh-plugin-wsl-env # 2. 安装插件,配置自动带上
23
+ dsh --profile wsl --dump-config # 3. 快速检查:不启动,只看拼好的配置
24
24
  dsh --profile wsl # 4. 启动
25
25
  ```
26
26
 
27
- 第 3 步应该打印出名为 `# == dsh-plugin-wsl-env` 的层,并且 `- id: terminal-controller` 上应出现 `shell: { path: wsl.exe, name: WSL }`。这个包是 DSH bundle,所以第 2 步会把 [`cordis.patch.yml`](../cordis.patch.yml) 作为配置层施加进去,没有任何需要手工合并的补丁。
27
+ 第 3 步应该能看到一行 `# == dsh-plugin-wsl-env`——那就是插件的配置已经进了你的 profile。插件自带配置,第 2 步会自动接好,基本安装不需要手动改任何文件。
28
28
 
29
- **第一条命令之前,先给子系统装上 bubblewrap。** 每条命令都由 [bubblewrap](#沙箱) 约束执行,而多数子系统不预装它;没有它每条命令都会失败关闭——这是设计,绝不放行为不受限执行:
29
+ **第一条命令之前,先给子系统安装 bubblewrap。** 每条命令都运行在 bubblewrap 沙箱内(默认还包在会话级 systemd scope 里),而多数子系统不预装它。缺失时,命令直接失败,不会脱箱运行:
30
30
 
31
31
  ```powershell
32
32
  wsl.exe -d <子系统> -u root -- apt-get install -y bubblewrap # Debian/Ubuntu
33
33
  ```
34
34
 
35
- 在本插件的检出目录里,`pnpm run bootstrap -- <子系统>` 会检查插件用到的全部四个工具(bubblewrap、ripgrep、git、inotify-tools),并按子系统自己的包管理器家族打印对应的安装命令;加 `--install` 则直接执行(`bootstrap` 是包内 pnpm 脚本,需要 `pnpm` 与 `bash`)。漏了这一步它会自己找上门:distro 会话打开时插件会探测 bubblewrap 并带完整修复指引发出警告——第一条命令不必再充当发现时刻。
35
+ 在插件检出目录里,`pnpm run bootstrap -- <子系统>` 检查插件用到的四个工具(bubblewrap、ripgrep、git、inotify-tools),并按子系统的包管理器打印安装命令;加 `--install` 直接执行。若跳过此步,会话打开时会报告缺失的包及确切的安装命令。
36
36
 
37
- 然后在 GUI 里打开 `\\wsl.localhost\<子系统>\...` 下的文件夹。选择器会在根一级列出每个已安装的子系统,**New terminal** 会在子系统里打开 shell。
37
+ **默认 preset 还要求 systemd。** 命令在会话级的 systemd user scope 内执行(后台 dev server 存活、`/tmp` 共享、`ps` 能看到整个会话)。在 `/etc/wsl.conf` 里设 `[boot] systemd=true`,重启 WSL,并执行一次 `loginctl enable-linger "$USER"`。缺任何一项,会话会带修复命令拒绝;在 preset 的 `wsl-shell` 上设 `sessionSandbox: false` 可回到每命令沙箱。
38
+
39
+ 然后在 GUI 里打开一个 `\\wsl.localhost\<子系统>\...` 下的文件夹。选择器会列出每个已安装的子系统,**New terminal** 开的也是子系统里的 shell。
38
40
 
39
41
  卸载:`dsh plugin --profile wsl remove dsh-plugin-wsl-env`。升级:再执行一次同样的 `add` 命令。
40
42
 
41
- > **改了 `lib/` 下的任何文件?先 `pnpm run sync:windows`,再重启应用。** 应用加载的是 Windows 侧运行时镜像,不先同步,重启也只是重新载入旧代码;运行中的进程还会缓存 ES module。
43
+ > **改了 `lib/` 下的文件?先同步,再重启应用。** 应用加载的是 Windows 侧那份代码副本,所以先跑 `pnpm run sync:windows`。不同步就重启,加载的还是旧代码;运行中的进程还会缓存模块。见[开发](#开发)。
42
44
 
43
45
  ## 使用
44
46
 
45
- 把 `\\wsl.localhost\ubuntu\home\you\project` 作为工作区打开,然后问模型"我在什么内核上,`/etc/os-release` 里有什么"。它会在子系统里执行 `uname -r` 并读取该文件,不经过 `/mnt/c`,也不复制文件。
47
+ 把一个子系统文件夹作为工作区打开——比如 `\\wsl.localhost\ubuntu\home\you\project`——然后问模型:"我现在在什么内核上,`/etc/os-release` 里写了什么?" 它会在子系统里执行 `uname -r` 并读取这个文件。不复制文件,也不经过 `/mnt/c`。
48
+
49
+ - **子系统文件夹自动获得子系统环境。** 会话一打开子系统文件夹,就会被自动配上 WSL 环境。第一条命令就已经是对的。
50
+ - **命令在你期望的地方运行。** 命令在子系统里、用你自己的登录 shell 执行——所以你的 `PATH`、`nvm`、`cargo`、`pyenv` 和 rc 配置全部生效。shell 用的是子系统配置的那个,不是写死的 bash。
51
+ - **文件就是真实的文件。** `/home/you/x` 和 `\\wsl.localhost\ubuntu\home\you\x` 是同一个文件。`/mnt/c/...` 照常通向 Windows 磁盘。
52
+ - **终端也是。** 右侧栏 → *New terminal*:子系统里的 shell,落在会话所在目录。
53
+ - **端口可见性。** 模型能看到子系统内正在监听的端口(约每 10 秒刷新),并给出它启动的 dev server 的确切地址。WSL2 将 localhost 转发给 Windows,浏览器直接可访问。
54
+ - **权限和 Linux 主机一致。** 权限选择器有三档:`read-only`、`workspace-write`(默认)和 `danger-full-access`。有操作被拒绝时,模型会收到一次"用刚好够用的权限重试"的提议。只有 `danger-full-access` 是不带沙箱运行的。
55
+
56
+ ## 方案对比
57
+
58
+ Windows 上的编程工具要在 WSL 项目上干活,一共三条路:
59
+
60
+ 1. **把工具装进 WSL。** Codex CLI、Claude Code、ZCode CLI 的官方建议都是这条,而且没错——如果一款 CLI 能覆盖你的工作,它就是最简单的路。前提是整个工具都能搬进 WSL;DeepSeek Harness 搬不进去,所以只剩下面两条。
61
+ 2. **桌面应用远程连进 WSL。** VS Code(Remote-WSL,开源镜像 [open-remote-wsl](https://github.com/jeanp413/open-remote-wsl))和 [ZCode](https://github.com/zai-org/ZCode) 这类 agent 桌面端把应用留在 Windows,往子系统里装一个服务程序。很成熟。代价:服务目录(`~/.vscode-server`、`~/.zcode/server`)、一个端口,命令无沙箱。
62
+ 3. **本插件。** Harness 留在 Windows。子系统内不安装任何内容、不监听端口,命令运行在 bubblewrap 沙箱内。缺失时,会话打开即报告缺失项与安装命令。
46
63
 
47
- - **子系统文件夹自动获得子系统环境。** 创建会话时就会绑定 `wsl` preset,所以第一次工具调用就已经正确。
48
- - **命令**在子系统里由你的登录 shell 执行,所以 `PATH`、`nvm`、`cargo`、`pyenv` 和 rc 配置都生效;shell 不是硬编码的 bash。默认走常驻子系统内代理(每个子系统一个长驻进程);代理不可用时(或设了 `agent: false`)同一条命令回退为一次性 `wsl.exe -d <子系统> --cd <Linux 目录> --exec <你的登录 shell> -lc <命令>` 进程。
49
- - **文件就是子系统的真实文件。** `/home/you/x` 与 `\\wsl.localhost\ubuntu\home\you\x` 是同一个文件,`/mnt/c/...` 通向 Windows 磁盘。
50
- - **终端**(右侧栏 → *New terminal*)在子系统里打开会话目录下的 shell。
51
- - **端口可见性**:子系统里监听中的端口经 `DSH_WSL_PORTS` 暴露给模型(约每 10 秒刷新);子系统内启动的 dev server 可由模型直接告知确切 URL(WSL2 的 localhost 转发是平台行为,Windows 侧直接可达)。
52
- - **模型能看到自己的 shell 环境。** 插件向受管的 `DSH_*` 命名空间注册 `DSH_WSL_DISTRO`、`DSH_WSL_SHELL`、`DSH_WSL_HOME` 和端口快照 `DSH_WSL_PORTS`。
53
- - **权限与 Linux 主机一致。** 权限选择器在 `read-only`、`workspace-write`(默认)和 `danger-full-access` 之间切换。被拒绝的命令或写入会带回"放宽权限"的提议;批准后,这一次调用不加沙箱执行。
64
+ | | 第二条(桌面远程) | 第三条(本插件) |
65
+ |---|---|---|
66
+ | 往子系统里装了什么 | 一个服务目录 | 什么都不装 |
67
+ | 是否监听端口 | 是 | 否 |
68
+ | 命令沙箱 | 无 | bubblewrap + systemd 会话 scope |
69
+ | 卸载之后 | 手动删除服务目录 | 无残留 |
70
+ | WSL 升级出了问题 | 服务可能需要修复 | 下一条命令自动恢复 |
71
+
72
+ **各方案的取舍。** 桌面远程重开工作区是即时的——其服务常驻运行;VS Code 的远程有多年生产打磨。装进 WSL 的 CLI 不需要任何额外机制。本插件只针对一种场景:DeepSeek Harness 运行在 Windows 上,同时要求 WSL 内的工作在沙箱中执行。
54
73
 
55
74
  ## 配置
56
75
 
57
- 在自己的 profile 层里按 id 覆盖某一行:`$DSH_HOME/profiles/<名字>/cordis.patch.yml`。值得知道的键:
76
+ 每个设置都挂在一个有名字的"行"上。想改哪个,就在 `$DSH_HOME/profiles/<名字>/cordis.patch.yml` 里加一行同 id 的覆盖。值得知道的键:
58
77
 
59
78
  | 行 | 键 | 默认值 | 说明 |
60
79
  |---|---|---|---|
61
- | `wsl-shell` | `distro` | `''` | 子系统名;空表示用 WSL 的默认子系统 |
62
- | | `sandbox` | `true` | 用 `bubblewrap` 约束命令;`false` 表示不用沙箱 |
63
- | | `maskWindowsDrive` | `false` | 用空 tmpfs 遮蔽 `/mnt`,收窄 interop 洞(`wsl-fs`、`fs-routing` 上同名);只是收窄、不是关闭,见[沙箱](#沙箱) |
80
+ | `wsl-shell` | `distro` | `''` | 用哪个子系统;空表示 WSL 的默认子系统 |
81
+ | | `sandbox` | `true` | 命令跑在 `bubblewrap` 沙箱里;`false` 关掉沙箱 |
82
+ | | `sessionSandbox` | `true` | 会话内的命令跑在同一个常驻 systemd user scope 里:后台 dev server 跨命令存活、`/tmp` 共享、`ps` 能看到整个会话。需要 systemd 为 PID 1、user manager 可达、`Linger` 打开;预检失败会带修复命令拒绝。设 `false` 回到每命令沙箱 |
83
+ | | `agentBackend` | `auto` | 会话 runtime 的 in-distro agent:`auto` 在存在已校验来源(打包二进制或 `DSH_WSL_AGENT_NATIVE` 开发覆盖)时用 native,否则用 shell;`native` 准备失败时让首条命令失败而不是静默回退;`shell` 强制 shell。命令和受限写入共用该选择。native 是默认后端,shell agent 是回退 |
84
+ | | `maskWindowsDrive` | `false` | 让沙箱里的命令看不到 `/mnt`,收窄互操作这个洞(只是收窄,关不死,见[沙箱](#沙箱)) |
64
85
  | `wsl-fs` | `distro` | `''` | 同上 |
65
- | | `restrictToDistro` | `true` | 拒绝指向**其它子系统**共享的路径;`/mnt/c` 属于本子系统内部,不受影响。拒绝码为 `FS_OUTSIDE_DISTRO`,它不是沙箱拒绝,放宽权限也无法解除 |
66
- | | `sandbox` | `true` | 写入时按策略检查 `writeText` 和 `editText` |
67
- | | `substrate` | `agent` | 文件工具使用哪种 I/O 基座。只剩常驻子系统内代理一种:读写与路径解析直接在 ext4 上完成(原生符号链接、原生权限位,写入 guard 存活到发布时刻,受限策略下由内核强制)。原 `"share"` 退出项——Windows 侧宿主文件栈走 9p 共享——已在启动时拒绝,文件工具不再经过共享 |
68
- | `directory-picker-wsl` | `includeHostHome` | `true` | 同时列出 Windows 家目录 |
86
+ | | `restrictToDistro` | `true` | 拒绝属于**别的子系统**的路径(`/mnt/c` 属于本子系统,不受影响)。拒绝码是 `FS_OUTSIDE_DISTRO`,放宽权限也解不开 |
87
+ | | `sandbox` | `true` | 文件写入按同一份策略检查 |
88
+ | | `sessionSandbox` | `true` | 受限写入跑在与命令相同的会话 runtime 里(同一个真实 `/tmp`、同一份 workspace mount);需要同样的 systemd/linger 配置;`false` 回到每策略受限 resident |
89
+ | | `substrate` | `agent` | 文件操作如何到达子系统。只剩一种:子系统内的 agent,读写直接落在子系统自己的文件系统上(原生符号链接与权限位)。旧 `"share"` 选项走 9p 共享,启动时即被拒绝 |
90
+ | `directory-picker-wsl` | `includeHostHome` | `true` | 选择器里同时列出 Windows 家目录 |
69
91
  | `subprocess-wsl` | `distro` | `''` | GUI 终端开在哪个子系统 |
70
92
 
71
- `wsl-shell` 与 `wsl-fs` 恰恰是**按 id 覆盖不到**的两行:它们是 `wsl` agent preset 的插件,嵌在 `preset-wsl` 行的 `config.plugins` 里,所以在自己的层里写 `- id: wsl-shell` 不会生效——loader 只会警告 `patch: entry "wsl-shell" not found`,实际值仍是随包的那份。要改就整块改写 `preset-wsl` 行;配方见 [docs/CONFIGURATION.md](CONFIGURATION.md#overriding-the-rows-inside-preset-wsl)。
93
+ `wsl-shell` 和 `wsl-fs` 这两行**按 id 覆盖不到**:它们嵌在 `wsl` preset 自己的配置里(preset 是 dsh 能发给会话的一套命名环境),在自己的覆盖文件里写 `- id: wsl-shell` 不会生效——loader 只会警告 `patch: entry "wsl-shell" not found`,实际值不变。要改,就把整个 `preset-wsl` 行复制过去改写;配方见 [docs/CONFIGURATION.md](CONFIGURATION.md#overriding-the-rows-inside-preset-wsl)。
72
94
 
73
- [`cordis.patch.yml`](../cordis.patch.yml) 是每个随包值的注释参考。[docs/CONFIGURATION.md](CONFIGURATION.md) 列出其余键,包括 `shell`、`loginShell`、`cwd`、`timeoutMs`、`preferredDistro` 和 `maxEntries`;[examples/profile.cordis.patch.yml](../examples/profile.cordis.patch.yml) 是一份可照抄的本机层。
95
+ [`cordis.patch.yml`](../cordis.patch.yml) 是每个随包值的注释参考。[docs/CONFIGURATION.md](CONFIGURATION.md) 列出其余键(`shell`、`loginShell`、`cwd`、`timeoutMs`、`preferredDistro`、`maxEntries` 等);[examples/profile.cordis.patch.yml](../examples/profile.cordis.patch.yml) 是一份可以照抄修改的本机配置。
74
96
 
75
97
  ## 配方
76
98
 
77
- - **Git 凭据共享**:让子系统里的 git 使用 Windows 侧的 Git Credential Manager,避免每次输密码:
78
- `git config --global credential.helper "/mnt/c/Program\ Files/Git/mingw64/bin/git-credential-manager.exe"`
79
- (路径按 Windows 侧 Git 的安装位置调整。)
80
- - **路径与性能**:模型看到并操作的是子系统内的 Linux 路径(`/home/...`),写入子系统自身的 ext4;`/mnt/c` 通向 Windows 磁盘但走 9p,大批量小文件操作明显慢——重 IO 的项目请放在子系统文件系统内。`pnpm run bootstrap <子系统>` 会一并报告 ripgrep、git、inotifywait(搜索、快照与监视的后端)是否就位。
81
- - **WSLENV 透传**:WSL 只导入 `WSLENV` 中列出的变量。本插件按前缀放行托管的 `DSH_*` 命名空间,其中带 Windows 路径的两个(`DSH_HOME`、`DSH_PROFILE_DIR`)加 `/p` 让 WSL 翻译成 `/mnt/c/...`。`PATH` 故意不透传——否则 Windows 的 PATH 会覆盖子系统自身的 PATH。
99
+ - **让子系统里的 git 用 Windows 的已存凭据**:`git config --global credential.helper "/mnt/c/Program\ Files/Git/mingw64/bin/git-credential-manager.exe"`(路径按你 Windows 侧 Git 的安装位置调整)。设置后,子系统里 `git push` 用的就是 Windows 保存的那份凭据,不用再输密码。
100
+ - **项目放在子系统的磁盘上。** 模型操作的是子系统自己磁盘上的 Linux 路径(`/home/...`),快。它也能通过 `/mnt/c` 访问 Windows 文件,但那条桥在大量小文件时明显慢。`pnpm run bootstrap <子系统>` 会告诉你 ripgrep、git、inotifywait(分别用于搜索和文件监视)装没装。
101
+ - **环境变量**:WSL 只转发 `WSLENV` 里点名的变量。本插件转发自己的 `DSH_*` 值,其中两个装着 Windows 路径的(`DSH_HOME`、`DSH_PROFILE_DIR`)会被翻译成 Linux 路径。你的 `PATH` 永远不转发——转发了会盖住子系统自己的 PATH。
82
102
 
83
103
  ## 架构
84
104
 
85
- 一个 DSH 进程同时服务两类会话:工作区开在 Windows 目录的,和工作区在子系统内的。能做到这一点,是因为 provider 分两层挂载:
105
+ 一个 DSH 进程可以同时服务两类会话:工作区在 Windows 目录的,和工作区在子系统里的。插件的部件分两层放置,原因很简单——有的东西属于某一个会话,有的属于整个应用:
86
106
 
87
107
  ```text
88
108
  组合层(composition,每个进程一份)
89
- ├─ subprocess-wsl GUI 终端的执行世界
109
+ ├─ subprocess-wsl GUI 终端的执行环境
90
110
  │ 子系统目录会话 → 子系统 shell,落在会话的 Linux 目录
91
111
  │ Windows 目录会话 → powershell.exe,落在会话自己的目录
92
112
  ├─ directory-picker-wsl 在 Windows 家目录旁列出已安装的子系统
93
- ├─ workspace-files-wsl GUI 文件树与预览由子系统内提供,供子系统工作区使用
94
- ├─ fs-routing 根级文件系统,按坐标路由
95
- │ 子系统 UNC → 常驻代理;盘符路径 → 宿主后端
113
+ ├─ workspace-files-wsl 子系统工作区的 GUI 文件树与预览,由子系统内部提供
114
+ ├─ fs-routing 根级文件系统,按路径坐标分流
115
+ │ 子系统 UNC → 常驻助手;盘符路径 → 宿主原生后端
96
116
  ├─ wsl-shell-env 向模型暴露 DSH_WSL_DISTRO / _SHELL / _HOME / _PORTS
97
- └─ auto-preset 会话打开子系统目录时自动绑定 wsl preset
117
+ └─ auto-preset 会话打开子系统目录时,自动绑定 wsl preset
98
118
 
99
- preset-wsl(wsl agent preset;其服务运行在 isolate realm 内)
119
+ preset-wsl(wsl agent preset;这个环境里的服务相互独立)
100
120
  ├─ wsl-shell ctx.shell — wsl.exe --exec <登录 shell>,在子系统内经 bubblewrap 约束
101
- └─ wsl-fs ctx.fs — 子系统的真实文件;常驻代理基座落在 ext4
121
+ └─ wsl-fs ctx.fs — 子系统的真实文件,经常驻助手落在 ext4
102
122
  ```
103
123
 
104
- **为什么分两层。** `wsl-shell` 和 `wsl-fs` 位于 `wsl` agent preset 内,`auto-preset` 在会话工作区位于子系统内时自动绑定它:Windows 目录的会话继续使用原生 provider,子系统会话拿到 WSL provider——环境是会话的属性,而不是进程的。终端控制器是例外:它经根上下文解析执行世界,永远看不到 preset 的 isolate realm,所以 `subprocess-wsl` 必须挂在组合层。反过来,`workspace-files-wsl` 与 `fs-routing` 挂在组合层是同样的道理:GUI 文件树与根级 `ctx.fs` 没有会话,无法从 preset 内提供服务。二者直接替换随包的 `workspace-files` 与根级 `fs-sandbox` 行;根级路由的设计见 [docs/root-fs-routing.md](root-fs-routing.md)。
124
+ **为什么分两层。** `wsl-shell` 和 `wsl-fs` 是随会话变化的两件事,所以放在 `wsl` preset 里:会话打开子系统目录时,`auto-preset` 给它绑上这个 preset;打开 Windows 目录的会话继续用原生 Windows 工具。一个进程,每个会话各有各的环境。GUI 终端是例外:它读的是应用层的配置,永远看不到 preset,所以 `subprocess-wsl` 挂在应用层。GUI 文件树和根级文件系统正好反过来——每个会话都用它们,单个 preset 拥有不了——所以 `workspace-files-wsl` 和 `fs-routing` 也挂在应用层。它们替换了随包的两行;根级路由的设计见 [docs/root-fs-routing.md](root-fs-routing.md)。
125
+
126
+ **命令的执行路径。** 一个会话有一个常驻 agent,运行在 systemd user scope 内。默认它是 native `dsh-wsl-agent`——按 distro 架构选择的包内静态 musl 二进制,由 `agentBackend: auto` 选中;二进制缺失或 copy/digest/exec 校验失败时回退到 runtime 的 shell agent,`agentBackend: shell` 强制 shell,`native` 则拒绝回退。agent 在你的登录 shell 中、bubblewrap mount profile 内执行每条命令。scope 比命令活得久,所以后台 dev server 存活、`/tmp` 是子系统真实 `/tmp`、`ps` 能看到整个会话。设 `sessionSandbox: false` 时,agent 仍是每子系统一个、每条命令一套 bwrap profile(`--unshare-pid`、临时 `/tmp`),后台进程不存活,且不使用 native 后端。agent 不可用时(或 `agent: false`),改走一次性 `wsl.exe`——结果一致、略慢、仍是每命令隔离。
105
127
 
106
- **命令与文件。** 默认情况下命令走常驻子系统内代理——每个子系统一个长驻进程,cwd 随请求下发,不再逐命令 `--cd`。它执行的仍是 `<登录 shell> -lc <命令>`,外层是子系统内的 bubblewrap profile,参数与 DSH 自家 Linux runner 完全一致——约束语义与报错文案都和 Linux 主机相同;代理不可用时(或设了 `agent: false`)回退为一次性 `wsl.exe -d <子系统> --cd <Linux 目录> --exec <登录 shell> -lc <命令>` 进程。文件工具读写子系统的真实文件,基座只有常驻子系统内代理一种——读写与路径解析落在 ext4,原生符号链接与权限位;文件搜索的 spawn 以同样的方式改写:子系统工作区的搜索跑子系统自己的 `rg`(`wsl.exe --exec`),绝不让 Windows 侧二进制走 9p 共享。写入与命令沙箱使用同一份策略检查。
128
+ **文件操作的执行路径。** 读、写、搜索同样发生在子系统内部:使用子系统自己的磁盘与工具。子系统工作区的搜索运行子系统内的 `rg`,不使用 Windows 侧副本走 9p 共享。文件写入与命令沙箱使用同一份策略检查;`wsl-fs` 的 `sessionSandbox` 打开时(默认),受限写入跑在与命令相同的会话 runtime 里(同一个 native/shell agent),看到相同的 `/tmp` 与 workspace mount。读操作走共享的 shell agent。
107
129
 
108
130
  ## 沙箱
109
131
 
110
- 命令由**子系统内的 `bubblewrap`** 约束,文件写入按同一份策略检查。Windows ACL 沙箱在这里用不了:它的受限令牌完全到不了 WSL。
132
+ 命令跑在子系统内的 `bubblewrap` 沙箱里,文件写入按同一份策略检查。Windows 自带的沙箱在这里用不上:它的受限账户根本连不到 WSL。
111
133
 
112
134
  | 模式 | 子系统里的命令可以做什么 |
113
135
  |---|---|
114
- | `read-only` | 整个子系统只读;`/dev` 是新建的可写挂载(`/dev/null` 与 `/dev/shm` 可用),其余不可写 |
115
- | `workspace-write` | 在上一行基础上,把会话工作区绑定为可写,并把 `/tmp` 挂成临时目录 |
116
- | `danger-full-access` | 不加沙箱;用于批准后的放宽权限请求 |
136
+ | `read-only` | 整个子系统只读;会挂一个新建的 `/dev`,`/dev/null` 和 `/dev/shm` 可用,其余不可写 |
137
+ | `workspace-write` | 上面全部,加上会话工作区可写;`/tmp` 在会话 runtime 里是子系统真实 `/tmp`(`sessionSandbox: false` 时每命令 profile 挂临时 tmpfs) |
138
+ | `danger-full-access` | 不加沙箱;用于你批准的放宽权限请求 |
117
139
 
118
- **bubblewrap 是必需项,缺失即失败关闭。** 没有它时,每条受限命令都报 `SANDBOX_UNAVAILABLE`,而不是不受约束地运行。要退出约束:把 `sandbox` 设为 `false` 的行要选对——命令归 `wsl-shell`,写入栅栏归 `wsl-fs`,根级路由的 `fs-routing` 行另有自己的一份;注意前两行要经 `preset-wsl` 整块改写,按 id 改不到(见[配置](#配置))。工具层会如实告诉模型这些操作没有沙箱。
140
+ **bubblewrap 是必需项,缺了就宁可不做。** 没有它,每条受限命令都报 `SANDBOX_UNAVAILABLE`——命令不会脱着沙箱运行。想关掉沙箱,在管这件事的行上设 `sandbox: false`:命令归 `wsl-shell`,文件写入归 `wsl-fs`,根级写入归顶层的 `fs-routing`(应用自己用的那套文件系统)。(前两行要经 `preset-wsl` 改,见[配置](#配置)。)关掉后,模型会如实收到"这些操作没有沙箱"的说明。
119
141
 
120
- **上报的强制程度是 `partial` 而不是 `full`。** 子系统里的进程仍可经 WSL interop 执行 Windows 程序(例如 `/mnt/c/.../*.exe`),bubblewrap 管不到它。`pnpm run probe:sandbox` 会在你的机器上演示这条边界。
121
-
122
- `maskWindowsDrive`(在 `wsl-shell`、`wsl-fs` 与 `fs-routing` 三个行上,默认 `false`)是这套 profile 能给出的唯一收窄手段:它为每个受限 profile 用空 tmpfs 遮蔽 `/mnt`,Windows 磁盘上的文件读不到也带不走,其上的可执行文件也无法经 interop 启动;但这只是收窄而非关闭——命令仍可把可执行文件写进工作区再运行(`binfmt` 按内容分发),所以 enforcement 仍是 `partial`,结果里如实上报 `windowsDrive: "masked"`。完全关闭是子系统级的:`wsl.conf` 里的 `[interop] enabled=false`,见 [docs/CONFIGURATION.md](CONFIGURATION.md)。
142
+ **已知边界。** 子系统内的命令仍可启动 Windows 程序(`/mnt/c/.../*.exe`),bubblewrap 不覆盖 Windows 进程。`pnpm run probe:sandbox` 会在你的机器上演示这条边界。在三个行上设 `maskWindowsDrive: true` 能收窄它:`/mnt` 会从命令的视野里消失,Windows 磁盘的文件读不到、程序启动不了。但并未完全关闭——命令仍可将 Windows 程序复制进工作区后运行——因此上报仍为 `partial`。彻底关闭的方法在子系统层面:`wsl.conf` 里设 `[interop] enabled=false`(见 [docs/CONFIGURATION.md](CONFIGURATION.md))。
123
143
 
124
144
  设计见 [docs/ARCHITECTURE.md](ARCHITECTURE.md#sandbox),插件不做的事情见 [docs/LIMITATIONS.md](LIMITATIONS.md)。
125
145
 
@@ -127,43 +147,45 @@ preset-wsl(wsl agent preset;其服务运行在 isolate realm 内)
127
147
 
128
148
  | 现象 | 原因 | 处理 |
129
149
  |---|---|---|
130
- | 每条命令都报 `SANDBOX_UNAVAILABLE` | 子系统里没有 `bubblewrap`(或存在但不可用,错误消息会区分这两种情况并给出对应处置) | 按 `wsl-shell`(命令)与 `wsl-fs`(写入)错误消息里的指引安装,或 `pnpm run bootstrap -- <子系统> --install`,或在两个 provider 上都设 `sandbox: false`(根级路由的 `fs-routing` 行另有自己的一份;前两行怎么改见[配置](#配置)) |
131
- | 命令或写入在会话目录之外被拒绝 | `workspace-write` 的预期行为 | 接受工具给出的放宽权限提示,或把会话直接开在需要的目录上 |
132
- | 连工作区内的写入也被拒绝 | 会处在 `read-only` 模式 | 切换权限选择器 |
133
- | 重复执行 `dsh plugin add` 没有任何输出,`--dump-config` 里也没有变化 | 依赖之前已经装过;`add` 对已存在的依赖是静默 no-op,没有需要记录的内容 | 无需任何操作——层已经就位,`dsh --profile wsl --dump-config` 仍能看到 `# == dsh-plugin-wsl-env` |
134
- | 终端打开后仍然是 `cmd.exe` | 这一层里的 `terminal-controller` 行没有生效 | 用 `dsh --profile wsl --dump-config` 确认能看到 `shell: { path: wsl.exe, name: WSL }` |
135
- | 改了 `lib/` 但不生效 | 应用加载的是 Windows 侧运行时镜像,且运行中的进程会缓存 ES module | 先 `pnpm run sync:windows` 同步到镜像,再重启应用 |
136
- | 用 `link:\\wsl.localhost\...` 安装后符号链接是坏的 | pnpm 无法链接 UNC 路径 | 改成链接 Windows 路径;在子系统内开发时用运行时镜像,见[开发](#开发) |
137
- | `glob`/`grep` 很慢 | 子系统内没有 rg 时,搜索只能报 rg 自己的 "command not found";Windows 目录的搜索走宿主原生 rg,不受影响 | 运行 `pnpm run bootstrap -- <子系统> --install`(安装 ripgrep);子系统工作区的搜索始终跑子系统内的 rg,绝不遍历 9p 共享 |
138
- | 终端活动显示 `unknown` | 仅在常驻代理不可用时出现——distro 终端从子系统内部观测(`/proc` 中扫描 `DSH_TERMINAL_ID` 标记:shell 独处为 `idle`,运行任何命令为 `busy`) | 确认子系统在运行;空闲终端会在控制器的无人值守超时(默认 2 小时)后自动回收,`terminalIdleReclaim: false` 恢复手动关闭 |
139
- | 结果里出现 `FS_*` 码 | 码本身说明了是谁拒绝的、以及怎样解除 | 见 [docs/ARCHITECTURE.md](ARCHITECTURE.md#error-codes) 的错误码表 |
150
+ | 每条命令都报 `SANDBOX_UNAVAILABLE` | 子系统里没有 `bubblewrap`,或者装了但坏的——错误消息会告诉你是哪种、该怎么办 | 按错误消息里的指引处理,或 `pnpm run bootstrap -- <子系统> --install`,或在 `wsl-shell`(命令)与 `wsl-fs`(写入)上都设 `sandbox: false`(根级写入的 `fs-routing` 行另有一份;前两行怎么改见[配置](#配置)) |
151
+ | 命令或写入在会话目录外被拒绝 | 正常现象:`workspace-write` 只允许写会话目录 | 接受工具给出的放宽权限提示,或把会话开在你需要的目录上 |
152
+ | 会话目录内的写入也被拒绝 | 会话处于 `read-only` 模式 | 切换权限选择器 |
153
+ | 重复执行 `dsh plugin add` 没有任何输出,`--dump-config` 也没变化 | 插件已经装过了,没有新东西可报告——这是成功,不是失败 | 不用做任何事:层已经就位,`dsh --profile wsl --dump-config` 仍能看到 `# == dsh-plugin-wsl-env` |
154
+ | 终端打开后还是 `cmd.exe` | 这一层里的 `terminal-controller` 行没有生效 | 用 `dsh --profile wsl --dump-config` 确认能看到 `shell: { path: wsl.exe, name: WSL }` |
155
+ | 改了 `lib/` 但不生效 | 应用跑的是 Windows 侧那份代码副本,运行中的进程还会缓存模块 | 先 `pnpm run sync:windows` 更新副本,再重启应用 |
156
+ | 用 `link:\\wsl.localhost\...` 安装后符号链接是坏的 | pnpm 无法链接 UNC 路径 | 改链接 Windows 路径;在子系统里开发请用运行时镜像,见[开发](#开发) |
157
+ | `glob`/`grep` 很慢 | 子系统里没装 `rg`;Windows 目录的搜索不受影响 | 运行 `pnpm run bootstrap -- <子系统> --install`(装 ripgrep);子系统工作区的搜索永远用子系统里的 rg,不走慢桥 |
158
+ | 终端活动显示 `unknown` | 仅在子系统内 agent 暂时不可用时出现——distro 终端是从子系统内部观察的(`/proc` 里扫 `DSH_TERMINAL_ID` 标记:只有 shell 是 `idle`,在跑任何命令是 `busy`) | 确认子系统在运行;空闲终端会在控制器的空闲超时(默认 2 小时)后自动关闭,`terminalIdleReclaim: false` 可以改成手动关 |
159
+ | 结果里出现 `FS_*` 码 | 码本身说明了是谁拒绝的、怎么解除 | 见 [docs/ARCHITECTURE.md](ARCHITECTURE.md#error-codes) 的错误码表 |
140
160
 
141
161
  ## 开发
142
162
 
143
163
  ```bash
144
- pnpm run build # 从 src/ 生成 lib/(产物提交进仓,改 src/ 后必须重新构建)
145
- pnpm test # 风格、语法、构建一致性、类型检查与单元测试
146
- pnpm run sync:windows # 把 lib/ 同步到 Windows 侧运行时镜像(改 lib/ 后必跑,再重启应用)
164
+ pnpm run build # 从 src/ 生成 lib/(产物提交进仓,改了 src/ 必须重新构建)
165
+ pnpm test # 风格、语法、构建一致性、类型检查、单元测试
166
+ pnpm run sync:windows # 把检出复制到应用加载的 Windows 侧副本(改 lib/ 后必跑,再重启应用)
147
167
  pnpm run test:coverage # 带覆盖率阈值的单元测试(需 Node 22.8+)
148
168
  pnpm run diagnose # 只读诊断报告(可直接贴进 issue):版本、工具、bwrap 探测
149
169
  pnpm run bootstrap -- <子系统> # 检查四个子系统侧工具;加 --install 安装缺失项
150
- pnpm run probe:sandbox # 在子系统里实测 bubblewrap 能约束什么、不能约束什么
170
+ pnpm run probe:sandbox # 在子系统里实测 bubblewrap 能管什么、管不了什么
151
171
  pnpm run probe # 文件系统探针,需要真实子系统(仅 Windows + WSL)
152
172
  pnpm run probe:sandbox-shell # 启动真实 harness,驱动受限执行器
153
173
  pnpm run probe:terminal # 通过终端 provider 打开一个 PTY
154
- pnpm run probe:substrate # 用真实 wsl.exe 传输驱动 agent 文件基座(在子系统内运行)
155
- pnpm run probe:watch # 在真实目录上装上 distro 内轮询 watcher(在子系统内运行)
156
- pnpm run probe:agent # resident 与 one-shot 的回退不变量对比腿(在子系统内运行)
157
- pnpm run probe:exec # agent 执行句柄:超时、杀停、cwd 失败(在子系统内运行)
174
+ pnpm run probe:substrate # 用真实 wsl.exe 驱动文件助手(在子系统内运行)
175
+ pnpm run probe:watch # 在真实目录上装子系统内监视器(在子系统内运行)
176
+ pnpm run probe:agent # 常驻路径与回退路径的对比(在子系统内运行)
177
+ pnpm run probe:exec # agent 执行:超时、杀停、cwd 失败(在子系统内运行)
158
178
  pnpm run probe:missing-wsl # 启动一个 wslPath 无法启动的 profile(仅 Windows + WSL)
159
179
  pnpm run probe:picker # 列出选择器的根级、拒绝路径与上限(仅 Windows + WSL)
160
- pnpm run probe:mode # 哪些 POSIX 权限事实能穿过共享层(需 Windows node 与已安装的 harness,不启动 profile)
161
- pnpm run probe:sandbox-off # 验证 sandbox: false 确实解除两侧约束(仅 Windows + WSL)
180
+ pnpm run probe:mode # 哪些 POSIX 权限信息能穿过共享层(需 Windows node 与已装的 harness,不启动 profile)
181
+ pnpm run probe:sandbox-off # 验证 sandbox: false 确实关掉两侧沙箱(仅 Windows + WSL)
162
182
  ```
163
183
 
164
- 应用加载的是 Windows 侧运行时镜像,不是仓库检出:改了 `lib/` 要先 `pnpm run sync:windows` 同步过去,再重启应用才会生效(`pnpm run build` 负责从 `src/` 生成 `lib/`,`pnpm test` 里的构建一致性检查会校验它)。首次运行前先 `pnpm install` 一次,复原钉版的开发依赖:测试会 import 钉版的 `@deepseek-ai/*` 包(组合与启动测试跑的是 loader 的真实 patch 算法),运行时包本身仍然保持零依赖。pnpm 由 `packageManager` 字段解析,任何开了 corepack 的 Node 都能直接用。CI 在 Linux 和 Windows 上用 Node 22、24 运行 `pnpm test`,并在 Node 24 上运行 `pnpm run test:coverage`;`engines` 与 harness 宿主自身的下限(`^22.19.0 || >=24.0.0`)保持一致。
184
+ 应用跑的是 Windows 侧的代码副本,不是你的检出。改了 `lib/` 下任何文件,先 `pnpm run sync:windows` 再重启应用——否则重启加载的还是旧代码。`pnpm run build` 负责从 `src/` 生成 `lib/`;产物提交进仓,`pnpm test` 会检查两者一致。
185
+
186
+ 首次运行前 `pnpm install` 一次装好开发依赖。测试用到钉版的 `@deepseek-ai/*` 包;插件本身发布时不带任何依赖。CI 在 Linux 和 Windows 上用 Node 22、24 跑测试,Node 24 另跑覆盖率。
165
187
 
166
- [docs/ARCHITECTURE.md](ARCHITECTURE.md) 有文件布局、挂载方式和沙箱设计;[CONTRIBUTING.md](../CONTRIBUTING.md) 有开发循环、运行时镜像、完整质量门清单和已验证内容。
188
+ [docs/ARCHITECTURE.md](ARCHITECTURE.md) 有文件布局、挂载方式和沙箱设计;[CONTRIBUTING.md](../CONTRIBUTING.md) 有开发循环、镜像、完整质量门清单和已验证内容。
167
189
 
168
190
  ## 文档
169
191
 
@@ -172,6 +194,7 @@ pnpm run probe:sandbox-off # 验证 sandbox: false 确实解除两侧约束
172
194
  | [docs/CONFIGURATION.md](CONFIGURATION.md) | 每个配置键,以及如何覆盖 |
173
195
  | [docs/ARCHITECTURE.md](ARCHITECTURE.md) | provider、挂载方式、路径坐标、沙箱、测试分层、文件布局 |
174
196
  | [docs/LIMITATIONS.md](LIMITATIONS.md) | 插件不做的事情,以及原因 |
197
+ | [docs/VALUE.zh.md](VALUE.zh.md) | 本插件的价值、代价,以及什么情况下不该用它 |
175
198
  | [CONTRIBUTING.md](../CONTRIBUTING.md) | 开发循环、质量门、约定、已验证内容 |
176
199
  | [SECURITY.md](../SECURITY.md) | 如何私下上报漏洞 |
177
200
  | [SUPPORT.md](../SUPPORT.md) | 支持范围,以及去哪里提问 |
@@ -2,8 +2,8 @@
2
2
 
3
3
  Status: draft, not yet filed upstream — no discussion link yet (the filing
4
4
  convention is [UPSTREAM-TERMINAL-TITLE.md](UPSTREAM-TERMINAL-TITLE.md)).
5
- Targeting `deepseek-harness` @ `packages/fs/fs-local`, the `0.2.0-rc.x` line
6
- this plugin pins exactly (`@deepseek-ai/dsh-fs-local@0.2.0-rc.2`, in both the
5
+ Targeting `deepseek-harness` @ `packages/fs/fs-local`, the `0.2.1-rc.x` line
6
+ this plugin pins exactly (`@deepseek-ai/dsh-fs-local@0.2.1-rc.2`, in both the
7
7
  dev and peer dependency blocks of `package.json`). This is the Shape-A
8
8
  upstream half of the in-distro I/O refactor: with the fsio text mechanics
9
9
  importable, `dsh-plugin-wsl-env` deletes its copies of them.
@@ -67,7 +67,7 @@ same surface.)
67
67
  Both files are generated; the change itself belongs in the sources that emit
68
68
  them (`src/index.ts` re-exporting from the fsio module, and that module
69
69
  exporting `detectLineEndings`). The artifact diffs are shown because they are
70
- what a consumer resolves — and they apply as-is to the published `0.2.0-rc.2`
70
+ what a consumer resolves — and they apply as-is to the published `0.2.1-rc.2`
71
71
  files.
72
72
 
73
73
  `detectLineEndings` is the one genuinely new name: it is not in
@@ -2,7 +2,11 @@
2
2
 
3
3
  Status: proposed upstream as
4
4
  [deepseek-ai/deepseek-harness#8769](https://github.com/deepseek-ai/deepseek-harness/discussions/8769)
5
- (targeting `packages/api/terminal-controller`, version line `0.2.0-rc.x`). The
5
+ (targeting `packages/api/terminal-controller`, version line `0.2.1-rc.x`; the
6
+ gap was re-checked against `dsh-v0.2.1-rc.2` on 2026-10-17 and is unchanged —
7
+ `packages/api/terminal-controller/src/index.ts:327` still builds the terminal
8
+ info with `title: shell.name`, and the spawn spec still carries no display-name
9
+ field). The
6
10
  plugin worked around the execution half of this gap — a Windows-folder
7
11
  session's terminal runs a host shell (`subprocess-wsl.hostSessions`) — and
8
12
  could not work around the title half. This is the record: the GUI titles every
@@ -63,7 +67,10 @@ Three escape hatches were evaluated against `0.2.0-rc.2` and each is closed:
63
67
  @deepseek-ai/dsh-client-ui-sidebar-terminal: pending (waiting for service: webTerminals)
64
68
  ```
65
69
 
66
- Verified live against `dsh-desktop 0.2.0-rc.2` (Electron 44.0.0).
70
+ Verified live against `dsh-desktop 0.2.0-rc.2` (Electron 44.0.0); the
71
+ row-id and `inject` facts the argument rests on are readable in the
72
+ `dsh-v0.2.1-rc.2` tree and did not change with the terminal controller's
73
+ 0.2.1 rework (`attach`/`write` → sequenced `TerminalInput`).
67
74
  3. **Per-session config.** The `shell` profile is composition state; `!!js`
68
75
  expressions evaluate once at config resolution and reach no Session
69
76
  identity.
@@ -0,0 +1,178 @@
1
+ # A profile-patch fragment for `$DSH_HOME/profiles/<name>/cordis.patch.yml`.
2
+ # It is the plugin's ENTIRE `preset-wsl` row (a row's config is replaced whole,
3
+ # never deep-merged) with `sessionSandbox: true` on the `wsl-shell` AND
4
+ # `wsl-fs` plugins.
5
+ #
6
+ # Paste it in place of any existing `- id: preset-wsl` entry in that profile
7
+ # layer, then restart the app. Regenerate it from the plugin's
8
+ # `cordis.patch.yml` whenever the shipped preset changes.
9
+
10
+ - id: preset-wsl
11
+ name: '@deepseek-ai/dsh-agent-preset'
12
+ config:
13
+ id: wsl
14
+ order: 2
15
+ plugins:
16
+ - id: wsl-env
17
+ name: cordis:group
18
+ group: true
19
+ isolate:
20
+ shell: true
21
+ fs: true
22
+ config:
23
+ # ── the WSL environment for this preset only ────────────────────
24
+ # No `cwd`: an empty default workdir means the distro user's home,
25
+ # asked of the distro itself. Pin a directory here to override it —
26
+ # a value that is right on one machine is wrong on every other.
27
+ #
28
+ # Commands run in one persistent systemd user scope per session
29
+ # (ADR 0001): a background dev server survives the command that
30
+ # started it, `/tmp` is the distro's real `/tmp`, and `ps` sees the
31
+ # session. This needs systemd as PID 1 (`[boot] systemd=true` in
32
+ # /etc/wsl.conf) and a lingering user manager (`loginctl
33
+ # enable-linger <user>`); the preflight refuses with those remedies
34
+ # when one is missing. Set `sessionSandbox: false` for the old
35
+ # per-command shape. Bubblewrap is still required for the mount
36
+ # profile; without it the providers fail closed.
37
+ - id: wsl-shell
38
+ name: 'dsh-plugin-wsl-env'
39
+ config:
40
+ distro: ''
41
+ loginShell: true
42
+ timeoutMs: 120000
43
+ maxTimeoutMs: 600000
44
+ sessionSandbox: true
45
+ agentBackend: auto
46
+ - id: wsl-fs
47
+ name: 'dsh-plugin-wsl-env/fs'
48
+ config:
49
+ distro: ''
50
+ restrictToDistro: true
51
+ # agent (the default): reads/writes/identities run inside the
52
+ # distro over the resident agent (lib/fs-substrate.js) —
53
+ # native symlinks and mode bits, the write guard survives to
54
+ # publication, and confined mutations are kernel-enforced.
55
+ # share: the opt-out — the Windows-side host fs stack over
56
+ # the 9p share.
57
+ substrate: agent
58
+ # Confined writes ride the session runtime (ADR 0001), so they
59
+ # share the command's real /tmp and workspace mounts.
60
+ sessionSandbox: true
61
+ - id: persona
62
+ name: '@deepseek-ai/dsh-persona'
63
+ config:
64
+ prefix: You are a coding agent powered by the {{model}} model.
65
+ - id: agent-instructions
66
+ name: '@deepseek-ai/dsh-agent-instructions'
67
+ config:
68
+ maxBytes: 65536
69
+ - id: time-context
70
+ name: '@deepseek-ai/dsh-time-context'
71
+ - id: tool-bash
72
+ name: '@deepseek-ai/dsh-tool-bash'
73
+ disabled: false
74
+ - id: tool-pwsh
75
+ name: '@deepseek-ai/dsh-tool-pwsh'
76
+ disabled: true
77
+ - id: tool-fs
78
+ name: '@deepseek-ai/dsh-tool-fs'
79
+ - id: tool-fs-search
80
+ name: '@deepseek-ai/dsh-tool-fs-search'
81
+ config:
82
+ sampleOverCapGlobResults: false
83
+ - id: tool-jobs
84
+ name: '@deepseek-ai/dsh-tool-jobs'
85
+ - id: tool-schedule
86
+ name: '@deepseek-ai/dsh-tool-schedule'
87
+ - id: skill-filesystem
88
+ name: '@deepseek-ai/dsh-skill-filesystem'
89
+ - id: tool-skill
90
+ name: '@deepseek-ai/dsh-tool-skill'
91
+ - id: command-goal
92
+ name: '@deepseek-ai/dsh-command-goal'
93
+ - id: tool-goal
94
+ name: '@deepseek-ai/dsh-tool-goal'
95
+ - id: planning
96
+ name: cordis:group
97
+ group: true
98
+ isolate:
99
+ planMode: true
100
+ config:
101
+ - id: plan-mode
102
+ name: '@deepseek-ai/dsh-plan-mode'
103
+ config:
104
+ section: |
105
+ You are in plan mode. Stay in plan mode until the user approves your plan through exit_plan_mode or switches the session mode. Imperative language to implement changes means plan the implementation, not execute it. A user's conversational agreement — including an answer confirming something you asked — approves nothing and does not end plan mode; fold the confirmed decision into the plan and submit it through exit_plan_mode.
106
+
107
+ Explore first. Use non-mutating reads, searches, static analysis, and checks to ground the plan in the actual repository. Do not edit or write files, change configuration, run formatters or code generation that rewrites tracked files, commit, or otherwise carry out the plan. Prefer existing functions and patterns over new machinery.
108
+
109
+ The tool catalog stays the same across modes for request-cache stability. These plan-mode rules override any later tool description or guidance that suggests using mutation tools; those tools remain listed to keep the tool catalog unchanged. Do not use todo_write to track this planning phase: it tracks implementation after an approved plan, while the plan itself belongs in exit_plan_mode.
110
+
111
+ Resolve discoverable facts by inspection. Use ask_user_question only for user-owned choices or material ambiguity that inspection cannot answer. Do not ask the user where code lives or how current behavior works when you can find out.
112
+
113
+ Make the plan decision-complete: state the goal and success criteria; group implementation changes by subsystem; identify public API, schema, and data-flow changes; cover edge cases, failure modes, tests, acceptance criteria, and explicit assumptions. Keep it concise enough to review but detailed enough that another engineer can implement it without making design decisions.
114
+
115
+ When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins only in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch modes manually; do not proceed with implementation.
116
+ - id: compaction
117
+ name: cordis:group
118
+ group: true
119
+ isolate:
120
+ compaction: true
121
+ toolResultPruner: true
122
+ config:
123
+ - id: compaction-basic
124
+ name: '@deepseek-ai/dsh-compaction-basic'
125
+ - id: command-compact
126
+ name: '@deepseek-ai/dsh-command-compact'
127
+ - id: tool-result-pruner
128
+ name: '@deepseek-ai/dsh-compaction-tool-result-pruner'
129
+ config:
130
+ thresholdChars: 8192
131
+ headChars: 4096
132
+ tailChars: 1024
133
+ - id: delegation
134
+ name: cordis:group
135
+ group: true
136
+ isolate:
137
+ workflowEngine: true
138
+ config:
139
+ - id: tool-subagent-control
140
+ name: '@deepseek-ai/dsh-tool-subagent-control'
141
+ - id: tool-subagent-list-agents
142
+ name: '@deepseek-ai/dsh-tool-subagent-control/list-agents'
143
+ - id: tool-subagent
144
+ name: '@deepseek-ai/dsh-tool-subagent'
145
+ config:
146
+ provider: spawn
147
+ toolName: subagent
148
+ modelSelectionSettings: true
149
+ - id: tool-subagent-fork
150
+ name: '@deepseek-ai/dsh-tool-subagent'
151
+ config:
152
+ provider: fork
153
+ toolName: subagent_fork
154
+ - id: workflow-ptc
155
+ name: '@deepseek-ai/dsh-workflow-ptc'
156
+ config:
157
+ provider: spawn
158
+ - id: tool-workflow
159
+ name: '@deepseek-ai/dsh-tool-workflow'
160
+ - id: tool-ask-user
161
+ name: '@deepseek-ai/dsh-tool-ask-user'
162
+ - id: tool-todo
163
+ name: '@deepseek-ai/dsh-tool-todo'
164
+ config:
165
+ allowParallelInProgress: true
166
+ - id: tool-web
167
+ name: '@deepseek-ai/dsh-tool-web'
168
+ config:
169
+ fetch: true
170
+ searchTimeoutMs: 60000
171
+ - id: present
172
+ name: '@deepseek-ai/dsh-tool-present'
173
+ - id: tool-plugin-manager
174
+ name: '@deepseek-ai/dsh-plugin-manager/tools'
175
+ disabled: true
176
+ - id: tool-plugin-market
177
+ name: '@deepseek-ai/dsh-plugin-market/tools'
178
+ disabled: true
@@ -14,7 +14,7 @@
14
14
  * @module dsh-plugin-wsl/agent-protocol
15
15
  */
16
16
  /** The protocol version this host side speaks; the agent must match. */
17
- export declare const PROTOCOL_VERSION = 4;
17
+ export declare const PROTOCOL_VERSION = 5;
18
18
  /**
19
19
  * Upper bound on one `read` op's payload, set by the HOST looping over windowed
20
20
  * requests. Keeping each frame's decoded payload here means the agent's RES
@@ -127,15 +127,21 @@ export interface FsFrameRequest {
127
127
  */
128
128
  export declare function encodeFsFrame({ id, op, args, timeoutMs }: FsFrameRequest): string[];
129
129
  /**
130
- * `HELLO|name|version|digest` — `digest` is the sha256 of the script file the
131
- * agent is executing (empty when it could not hash it); the host compares it
132
- * against its own shipped copy.
130
+ * `HELLO|name|version|digest|pid` — `digest` is the sha256 of the script file
131
+ * the agent is executing (empty when it could not hash it); the host compares
132
+ * it against its own shipped copy. `pid` is the agent shell's PID inside the
133
+ * distro, present from protocol 5 on: the host uses it to deliver an
134
+ * out-of-band `USR1` cancellation while the agent is blocked in `wait`, which
135
+ * the serial stdin loop cannot do by frame. It is absent when the agent
136
+ * itself runs inside a PID namespace (the confined resident), where that PID
137
+ * is not addressable from a second `wsl.exe`.
133
138
  */
134
139
  export interface HelloMessage {
135
140
  type: "hello";
136
141
  name: string;
137
142
  version: number;
138
143
  digest: string;
144
+ pid?: number;
139
145
  }
140
146
  /** `PONG` — the liveness answer to `PING`. */
141
147
  export interface PongMessage {
@@ -14,7 +14,7 @@
14
14
  * @module dsh-plugin-wsl/agent-protocol
15
15
  */
16
16
  /** The protocol version this host side speaks; the agent must match. */
17
- export const PROTOCOL_VERSION = 4;
17
+ export const PROTOCOL_VERSION = 5;
18
18
  /**
19
19
  * Upper bound on one `read` op's payload, set by the HOST looping over windowed
20
20
  * requests. Keeping each frame's decoded payload here means the agent's RES
@@ -124,8 +124,15 @@ export function encodeFsFrame({ id, op, args, timeoutMs = 0 }) {
124
124
  */
125
125
  export function parseAgentLine(line) {
126
126
  if (line.startsWith("HELLO|")) {
127
- const [, name, version, digest] = line.split("|");
128
- return { type: "hello", name: name ?? "", version: Number(version), digest: digest ?? "" };
127
+ const [, name, version, digest, pid] = line.split("|");
128
+ const agentPid = Number(pid);
129
+ return {
130
+ type: "hello",
131
+ name: name ?? "",
132
+ version: Number(version),
133
+ digest: digest ?? "",
134
+ ...(Number.isSafeInteger(agentPid) && agentPid > 0 ? { pid: agentPid } : {}),
135
+ };
129
136
  }
130
137
  if (line === "PONG")
131
138
  return { type: "pong" };