dsh-plugin-wsl-env 0.10.0 → 0.12.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.
@@ -7,7 +7,7 @@ profile layer: `$DSH_HOME/profiles/<name>/cordis.patch.yml`. Start from
7
7
  reference for every value the plugin ships.
8
8
 
9
9
  Which rows a patch can reach depends on where they sit. `fs-routing`,
10
- `workspace-files-wsl`, `directory-picker-wsl`, `subprocess-wsl`, `wsl-shell-env`
10
+ `directory-picker-wsl`, `subprocess-wsl`, `wsl-shell-env`
11
11
  and `preset-wsl` are top-level rows, so `- id: <row>` in your layer patches them.
12
12
  `wsl-shell` and `wsl-fs` are not top-level: they are plugins of the `wsl` agent
13
13
  preset, nested inside the `preset-wsl` row's `config.plugins`. A patch that
@@ -27,7 +27,7 @@ has its own table at the end of this page.
27
27
  | | `loginShell` | `true` *(shipped)* | use `<shell> -lc`, which sources your profile, instead of a bare `-c`. Applies only to the POSIX family; a non-POSIX shell (fish, csh) still gets a bare `-c` |
28
28
  | | `agent` | `true` | run commands over the resident in-distro agent (one long-lived process; in-distro timeout); the one-shot `wsl.exe` path is fallen back to only when the agent is out of service. Anything else — a cwd refusal, a sandbox refusal — is the command's own verdict and is raised, not retried on the other path |
29
29
  | | `sessionSandbox` | `true` *(shipped)* | run this session's commands in one persistent systemd user scope (ADR 0001): a background dev server survives the command that started it, `/tmp` is the distro's real `/tmp` under `workspace-write`, and `ps` sees the session. The executor holds the runtime for the session and stops the scope from the preset's disposer, which reaps the cgroup. Requires `agent: true`, systemd as PID 1, a reachable user manager and `Linger` (`loginctl enable-linger <user>`); the preflight refuses with the remedy when systemd, the user manager, `systemd-run` or `Linger` is missing (a success is cached; a failure is re-probed, so following the remedy needs no app restart). On in the shipped preset; set `false` for the per-command shape |
30
- | | `agentBackend` | `auto` | the session runtime's in-distro agent. `auto` uses the native agent when a verified source is available — the packaged per-arch static-musl binary at `agent/native/linux-<arch>/dsh-wsl-agent` (x86_64 and aarch64; runs on glibc and musl distros), or the `DSH_WSL_AGENT_NATIVE` development override — and otherwise the shipped shell agent; `native` fails the first command when it cannot prepare instead of silently falling back; `shell` forces the shell agent. Confined writes share the runtime, so they follow the same selection. The native backend is the default (static musl x86_64/aarch64); the shell agent is the fallback, and `native` refuses instead of falling back |
30
+ | | `agentBackend` | `auto` | the session runtime's in-distro agent. `auto` uses the native agent when a verified source is available — the packaged per-arch static-musl binary at `agent/native/linux-<arch>/dsh-wsl-agent` (x86_64 and aarch64; runs on glibc and musl distros), or the `DSH_WSL_AGENT_NATIVE` development override — and otherwise the shipped shell agent; `native` fails the first command when it cannot prepare instead of silently falling back; `shell` forces the shell agent. Confined writes share the runtime, so they follow the same selection. The native backend is the default (static musl x86_64/aarch64); the shell agent is the fallback, and `native` refuses instead of falling back. This key covers the session runtime (commands and confined writes) and, since A7, the shared read agent (unconfined reads), which also follows this selection with the shell fallback |
31
31
  | | `sandbox` | `true` | confine commands with `bubblewrap`; `false` disables the sandbox. Needs a usable `bwrap` inside the distro and fails closed without one (`SANDBOX_UNAVAILABLE`, never an unconfined run) — see [Turning the sandbox off](#turning-the-sandbox-off) |
32
32
  | | `maskWindowsDrive` | `false` | shadow `/mnt` with an empty tmpfs in every confined profile: the Windows drive's files cannot be read or exfiltrated and its executables cannot be launched through interop — the hole that lets a "sandboxed" command reach your full Windows token. A narrowing, not a closure (a command can still write an executable into the workspace and run it — binfmt dispatches on content, not location), so enforcement stays `partial` and the result's sandbox fact reports `windowsDrive: "masked"`. The complete closure is distro-level: `[interop] enabled=false` in `wsl.conf` |
33
33
  | | `cwd` | `''` | default working directory; empty means the distro user's home |
@@ -47,10 +47,6 @@ has its own table at the end of this page.
47
47
  | | `restrictToDistro` | `true` *(shipped)* | refuse a path in another distro's share (`FS_OUTSIDE_DISTRO`) |
48
48
  | | `watchMaxDepth` | `0` *(shipped)* | how deep the routed watch scan walks; `0` walks the whole tree and enables the inotify backend; a positive value keeps the polling scan |
49
49
  | | `distroCwd` | `''` | the routed backend's default workdir; empty means the distro user's home. `cwd` belongs to the host backend only — the distro side reads `distroCwd` — while `diffBasisMaxBytes` is shared: it bounds the overwrite diff on both sides |
50
- | `workspace-files-wsl` | `maxBytes` | `2097152` *(shipped)* | byte cap on one page of a text read, and the default byte window of a byte read |
51
- | | `maxFileBytes` | `33554432` *(shipped)* | full-file byte cap for the byte reader |
52
- | | `maxLines` | `5000` *(shipped)* | line cap on one text read |
53
- | | `maxEntries` | `2000` *(shipped)* | directory entries listed per level before the listing is flagged truncated |
54
50
  | `directory-picker-wsl` | `preferredDistro` | `''` *(shipped)* | the distro listed first in the picker |
55
51
  | | `includeHostHome` | `true` *(shipped)* | also list the Windows home directory |
56
52
  | | `maxEntries` | `1000` *(shipped)* | maximum entries listed per directory |
@@ -60,6 +56,11 @@ has its own table at the end of this page.
60
56
  | | `hostSessions` | `true` | give a session opened on a Windows folder a host `powershell.exe` in its own directory instead of the distro shell at `/mnt/<drive>/…`; `false` restores the composition-owned distro terminal for every session |
61
57
  | | `terminalIdleReclaim` | `true` | let the controller reclaim an idle distro terminal: the launch is marked with a per-terminal `DSH_TERMINAL_ID`, and the handle's `inspectActivity` counts the marked processes through the resident agent — one (the shell at its prompt) is idle, more than one (a running command or a nested shell) is busy, `unknown` (agent out) pauses reclamation. `false` restores close-by-hand |
62
58
 
59
+ The GUI file tree and previews use the shipped `workspace-files` row, not a plugin
60
+ row. Its caps are the peer defaults (`maxBytes` 2 MiB, `maxFileBytes` 32 MiB,
61
+ `maxLines` 5000, `maxEntries` 2000); override them with `- id: workspace-files`
62
+ in your own layer.
63
+
63
64
  ## Overriding the rows inside `preset-wsl`
64
65
 
65
66
  `wsl-shell` and `wsl-fs` are mounted as plugins of the `wsl` agent preset, inside
@@ -110,6 +110,31 @@ crosses the 9p share. What to know about the one substrate there is:
110
110
  kernel can actually report change.
111
111
  - `editText` reads the whole file into memory before writing it back.
112
112
 
113
+ ## The GUI workspace-files service
114
+
115
+ - **The GUI file tree and previews use the shipped `workspace-files` row.** The
116
+ plugin deliberately does not replace it: its client half is the browser `file`
117
+ resource provider the Sidebar previews need, and a server-only replacement
118
+ drops it (the earlier `workspace-files-wsl` takeover did exactly that). The
119
+ shipped service reads WSL paths through the routed root filesystem
120
+ (`fs-routing`), so the file tree and previews still run inside the distro.
121
+
122
+ - **That path costs extra agent round trips.** A listing, `stat` or page read
123
+ goes through `ctx.fs` before reaching the resident agent: roughly four to six
124
+ round trips instead of one, and a deep page read streams the preceding lines
125
+ through the pipe before cutting. Expect the GUI file tree and previews to be
126
+ slower than a direct agent call, especially on large files; the model's own
127
+ file operations are unaffected — they use the preset filesystem and the
128
+ resident agent directly.
129
+
130
+ - **`fs-routing` is the seam.** The shipped service depends on the routed root
131
+ filesystem staying enabled and faithful to the `ctx.fs` contract (`resolve`,
132
+ `lstat`, `stat`, `listDir`, `streamText`, `readBytes`, `watch`, `contains`,
133
+ `processPath`). Disabling `fs-routing` restores the host 9p-backed root
134
+ filesystem, and the GUI file service then crosses 9p for WSL workspaces. The
135
+ shipped row's own caps (`maxBytes`, `maxFileBytes`, `maxLines`, `maxEntries`)
136
+ can be overridden on the `workspace-files` row.
137
+
113
138
  ## The sandbox
114
139
 
115
140
  - **The one-shot fallback has only the host deadline.** With `agent: false`, or
@@ -119,6 +144,12 @@ crosses the 9p share. What to know about the one substrate there is:
119
144
  call. The session runtime path — the shipped default — is cleaned by its
120
145
  scope. A systemd-scoped one-shot (with explicit environment pass-through) is
121
146
  not shipped; prefer `agent: true` and the session runtime for long commands.
147
+ - **Batch file reads share one agent per distro.** Since A7 the shared read
148
+ agent is the native binary, so 16 concurrent `stat`/`read`/`list` calls
149
+ complete in 2.4-4.1 ms (A6 measured 342-614 ms on the shell). The first call
150
+ pays the native preparation/cache cost (~70 ms once); `agentBackend: shell`
151
+ forces the shell reads. v6's FS concurrency would only shave the native
152
+ runtime's 7.7 ms/16 writes and is deferred.
122
153
  - **The native backend is the default; aarch64 is build-tested only.** The
123
154
  packaged x86_64 musl binary runs the 18-check suite in CI. The same job
124
155
  builds the aarch64 binary, but the runner cannot execute it, so its
package/docs/README.zh.md CHANGED
@@ -6,200 +6,179 @@
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 终端开的也是子系统里的 shell。
9
+ 一个 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 插件,让会话在 Windows 的 WSL 2 发行版中运行。
10
10
 
11
- **只支持 Windows + WSL2。** 安装只需一条命令,插件自身没有依赖。环境按会话生效:打开 Windows 文件夹的会话,用的还是原来的 Windows 工具。
11
+ Harness 进程仍在 Windows 上。命令、文件操作、文件夹选择器和 GUI 终端都使用发行版环境。发行版内不安装辅助服务或守护进程,也不开放端口,受限命令在 `bubblewrap` 中运行。
12
12
 
13
- [安装](#安装) · [使用](#使用) · [方案对比](#方案对比) · [配置](#配置) · [配方](#配方) · [架构](#架构) · [沙箱](#沙箱) · [常见问题](#常见问题) ·
14
- [开发](#开发) · [文档](#文档)
13
+ 仅支持 Windows + WSL 2。
15
14
 
16
- ## 安装
15
+ ## 为什么用这个插件
17
16
 
18
- 在 Windows 终端里执行这四条命令:
17
+ Windows 上的编程工具要在 WSL 项目中工作,有三条路:把整个工具装进发行版、连接发行版内的辅助服务,或由插件直接提供发行版环境。本插件是第三条。两类会话共用一套流程,同时保留发行版的真实工具和命令沙箱。
19
18
 
20
- ```powershell
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
- dsh --profile wsl # 4. 启动
25
- ```
19
+ - **真实的 Linux 环境。** 命令在发行版自己的登录 shell 中运行,`PATH`、`nvm`、`cargo`、`pyenv` 和 rc 文件都生效。文件工具直接读写发行版 ext4 上的真实文件。
20
+ - **发行版内没有服务。** 没有辅助服务、没有守护进程、没有监听端口。唯一持久化的内容是 `~/.cache/dsh-wsl/agent/` 下按内容寻址的原生 agent 副本。
21
+ - **默认有沙箱。** 受限命令在发行版内的 `bubblewrap` 中运行。缺少 `bubblewrap` 时命令直接失败,不会脱箱运行。
22
+ - **按会话生效。** 打开发行版文件夹的会话获得发行版环境;打开 Windows 文件夹的会话保留 Windows 工具。两者可同时运行在一个 harness 进程中。
23
+ - **与 GUI 集成。** 文件夹选择器列出已安装的发行版,终端在发行版内打开 shell,模型能看到监听端口。
26
24
 
27
- 第 3 步应该能看到一行 `# == dsh-plugin-wsl-env`——那就是插件的配置已经进了你的 profile。插件自带配置,第 2 步会自动接好,基本安装不需要手动改任何文件。
25
+ ### 为什么是 Linux,而不是 PowerShell
28
26
 
29
- **第一条命令之前,先给子系统安装 bubblewrap。** 每条命令都运行在 bubblewrap 沙箱内(默认还包在会话级 systemd scope 里),而多数子系统不预装它。缺失时,命令直接失败,不会脱箱运行:
27
+ 多数编程 agent 的训练材料以 Linux 为主,开源工具链也大多先在 Linux 上构建和测试。让 agent 在 Windows shell 里工作,等于要求它在每次调用时做一层翻译:
30
28
 
31
- ```powershell
32
- wsl.exe -d <子系统> -u root -- apt-get install -y bubblewrap # Debian/Ubuntu
33
- ```
29
+ - **训练先验。** 示例、CI 脚本、Dockerfile 和大多数公开答案用的是 POSIX shell 和 GNU 工具;模型的第一反应是 `bash`,不是 PowerShell。
30
+ - **工具链支持。** Node、Python、Rust、Go、C/C++ 和多数构建系统先在 Linux 上验证。Windows 支持往往是移植,引号、路径、换行和文件锁都不一样。
31
+ - **Shell 语义。** PowerShell 和 `cmd` 在引号、转义、管道、环境变量、退出码和路径分隔符上都与 POSIX 不同。混用两套语法,很容易产生看起来像模型犯的低级错误。
32
+ - **环境漂移。** Windows 侧的 PowerShell 版本、执行策略、PATH 和已装工具因人而异;Linux 环境更容易复现和容器化。
33
+
34
+ 结果是更多失败的命令、更多重试,以及更多时间花在语法而不是任务上。生成的代码也一样:shell 片段、构建命令和 CI 风格的一行命令通常按 POSIX 编写,发行版才是它们原样运行的地方。真实的 Linux 登录 shell 去掉了这层翻译。现代 PowerShell 和 Windows 原生工具在缩小差距,但默认先验和多数项目脚本仍然假设 POSIX。
34
35
 
35
- 在插件检出目录里,`pnpm run bootstrap -- <子系统>` 检查插件用到的四个工具(bubblewrap、ripgrep、git、inotify-tools),并按子系统的包管理器打印安装命令;加 `--install` 直接执行。若跳过此步,会话打开时会报告缺失的包及确切的安装命令。
36
+ 本插件把 GUI 留在 Windows,只把执行环境移进发行版,让 agent 得到它的训练和工具链都假设的那个 shell。
36
37
 
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
+ ## 方案对比
38
39
 
39
- 然后在 GUI 里打开一个 `\\wsl.localhost\<子系统>\...` 下的文件夹。选择器会列出每个已安装的子系统,**New terminal** 开的也是子系统里的 shell。
40
+ | | 桌面远程(VS Code、ZCode) | 本插件 |
41
+ |---|---|---|
42
+ | 发行版内 | 辅助服务 | 一个缓存的 agent 可执行文件 |
43
+ | 网络 | 监听端口 | 无 |
44
+ | 命令沙箱 | 无 | `bubblewrap` |
45
+ | 卸载 | 删除服务 | 删除缓存 |
40
46
 
41
- 卸载:`dsh plugin --profile wsl remove dsh-plugin-wsl-env`。升级:再执行一次同样的 `add` 命令。
47
+ 桌面远程重连更快、更成熟。如果 CLI agent 能覆盖你的工作,直接装进 WSL 更简单。本插件面向的场景是:harness 留在 Windows,同时要求命令有沙箱。
42
48
 
43
- > **改了 `lib/` 下的文件?先同步,再重启应用。** 应用加载的是 Windows 侧那份代码副本,所以先跑 `pnpm run sync:windows`。不同步就重启,加载的还是旧代码;运行中的进程还会缓存模块。见[开发](#开发)。
49
+ ## 要求
44
50
 
45
- ## 使用
51
+ - Windows 10 或 11,启用 WSL 2,并至少有一个发行版。
52
+ - 发行版内安装 `bubblewrap`,必需。
53
+ - 默认的会话沙箱需要 systemd:在 `/etc/wsl.conf` 设置 `[boot] systemd=true`,重启 WSL,然后执行一次 `loginctl enable-linger "$USER"`。没有 systemd 时,在 `wsl-shell` 和 `wsl-fs` 上都设置 `sessionSandbox: false`。
54
+ - 可选:`ripgrep` 用于搜索,`git` 用于变更快照,`inotify-tools` 用于文件监视。
46
55
 
47
- 把一个子系统文件夹作为工作区打开——比如 `\\wsl.localhost\ubuntu\home\you\project`——然后问模型:"我现在在什么内核上,`/etc/os-release` 里写了什么?" 它会在子系统里执行 `uname -r` 并读取这个文件。不复制文件,也不经过 `/mnt/c`。
56
+ 发行版内不需要 Node.js。
48
57
 
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` 是不带沙箱运行的。
58
+ ## 安装
55
59
 
56
- ## 方案对比
60
+ 在 Windows 终端执行:
57
61
 
58
- Windows 上的编程工具要在 WSL 项目上干活,一共三条路:
62
+ ```powershell
63
+ dsh wsl --from-default-profile web --dump-config # 创建 profile
64
+ dsh plugin --profile wsl add dsh-plugin-wsl-env # 安装插件及其配置
65
+ dsh --profile wsl --dump-config # 检查:输出 "# == dsh-plugin-wsl-env"
66
+ dsh --profile wsl # 启动
67
+ ```
59
68
 
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 沙箱内。缺失时,会话打开即报告缺失项与安装命令。
69
+ 第一条命令之前安装 `bubblewrap`:
63
70
 
64
- | | 第二条(桌面远程) | 第三条(本插件) |
65
- |---|---|---|
66
- | 往子系统里装了什么 | 一个服务目录 | 什么都不装 |
67
- | 是否监听端口 | 是 | 否 |
68
- | 命令沙箱 | 无 | bubblewrap + systemd 会话 scope |
69
- | 卸载之后 | 手动删除服务目录 | 无残留 |
70
- | WSL 升级出了问题 | 服务可能需要修复 | 下一条命令自动恢复 |
71
+ ```powershell
72
+ wsl.exe -d <发行版> -u root -- apt-get install -y bubblewrap
73
+ ```
71
74
 
72
- **各方案的取舍。** 桌面远程重开工作区是即时的——其服务常驻运行;VS Code 的远程有多年生产打磨。装进 WSL 的 CLI 不需要任何额外机制。本插件只针对一种场景:DeepSeek Harness 运行在 Windows 上,同时要求 WSL 内的工作在沙箱中执行。
75
+ 在插件检出目录中,`pnpm run bootstrap -- <发行版>` 检查四个发行版工具,并按包管理器打印安装命令;加 `--install` 直接执行。
73
76
 
74
- ## 配置
77
+ 升级:再次执行 `add` 命令。卸载:`dsh plugin --profile wsl remove dsh-plugin-wsl-env`;之后可删除 `~/.cache/dsh-wsl`。如果你为了恢复预览在 profile 里加过 `workspace-files-wsl` 的临时覆盖,升级后请删掉:插件不再挂载该行。
75
78
 
76
- 每个设置都挂在一个有名字的"行"上。想改哪个,就在 `$DSH_HOME/profiles/<名字>/cordis.patch.yml` 里加一行同 id 的覆盖。值得知道的键:
79
+ ## 使用
77
80
 
78
- | 行 | 键 | 默认值 | 说明 |
79
- |---|---|---|---|
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`,收窄互操作这个洞(只是收窄,关不死,见[沙箱](#沙箱)) |
85
- | `wsl-fs` | `distro` | `''` | 同上 |
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 家目录 |
91
- | `subprocess-wsl` | `distro` | `''` | GUI 终端开在哪个子系统 |
81
+ 打开一个发行版文件夹作为工作区,例如 `\\wsl.localhost\ubuntu\home\you\project`。
92
82
 
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)。
83
+ - 命令在发行版登录 shell 中运行。`/home/you/x` 与 `\\wsl.localhost\ubuntu\home\you\x` 是同一个文件。
84
+ - **New terminal** 在同一发行版、同一目录打开 shell。
85
+ - 模型能看到监听端口,并返回 dev server 的 URL。WSL 2 会把 localhost 转发到 Windows。
86
+ - 权限选择器:`read-only`、`workspace-write`(默认)、`danger-full-access`。只有 `danger-full-access` 不经过沙箱。
94
87
 
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) 是一份可以照抄修改的本机配置。
88
+ ## 配置
96
89
 
97
- ## 配方
90
+ 配置写在 `$DSH_HOME/profiles/<名字>/cordis.patch.yml` 的命名行上。主要设置:
98
91
 
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。
92
+ | 行 | 键 | 默认值 | 作用 |
93
+ |---|---|---|---|
94
+ | `wsl-shell` | `distro` | `''` | 使用的发行版;空表示 WSL 默认 |
95
+ | | `sandbox` | `true` | 用 `bubblewrap` 约束命令 |
96
+ | | `sessionSandbox` | `true` | 会话命令跑在同一个 systemd user scope 中 |
97
+ | | `agentBackend` | `auto` | 发行版内 agent:`auto`、`native` 或 `shell` |
98
+ | | `maskWindowsDrive` | `false` | 让受限命令看不到 `/mnt` |
99
+ | `wsl-fs` | `distro` | `''` | 文件操作使用的发行版 |
100
+ | | `restrictToDistro` | `true` | 拒绝属于其他发行版的路径 |
101
+ | | `sandbox` | `true` | 按策略检查受限写入 |
102
+ | | `sessionSandbox` | `true` | 受限写入跑在会话 runtime 中 |
103
+ | | `substrate` | `agent` | 发行版内 agent;`share` 在启动时被拒绝 |
104
+ | `directory-picker-wsl` | `includeHostHome` | `true` | 同时列出 Windows 家目录 |
105
+ | `subprocess-wsl` | `distro` | `''` | GUI 终端使用的发行版 |
106
+
107
+ `wsl-shell` 和 `wsl-fs` 位于随包的 `wsl` preset 中,按 id 覆盖无效。要改就覆盖整个 `preset-wsl` 行;配方和其余键见 [docs/CONFIGURATION.md](CONFIGURATION.md)。[`cordis.patch.yml`](../cordis.patch.yml) 是随包值的注释参考。
102
108
 
103
109
  ## 架构
104
110
 
105
- 一个 DSH 进程可以同时服务两类会话:工作区在 Windows 目录的,和工作区在子系统里的。插件的部件分两层放置,原因很简单——有的东西属于某一个会话,有的属于整个应用:
111
+ 一个 harness 进程可同时服务 Windows 文件夹会话和发行版文件夹会话。服务分两层挂载:
106
112
 
107
113
  ```text
108
- 组合层(composition,每个进程一份)
109
- ├─ subprocess-wsl GUI 终端的执行环境
110
- │ 子系统目录会话 → 子系统 shell,落在会话的 Linux 目录
111
- │ Windows 目录会话 → powershell.exe,落在会话自己的目录
112
- ├─ directory-picker-wsl 在 Windows 家目录旁列出已安装的子系统
113
- ├─ workspace-files-wsl 子系统工作区的 GUI 文件树与预览,由子系统内部提供
114
- ├─ fs-routing 根级文件系统,按路径坐标分流
115
- │ 子系统 UNC → 常驻助手;盘符路径 → 宿主原生后端
116
- ├─ wsl-shell-env 向模型暴露 DSH_WSL_DISTRO / _SHELL / _HOME / _PORTS
117
- └─ auto-preset 会话打开子系统目录时,自动绑定 wsl preset
118
-
119
- preset-wsl(wsl agent preset;这个环境里的服务相互独立)
120
- ├─ wsl-shell ctx.shell — wsl.exe --exec <登录 shell>,在子系统内经 bubblewrap 约束
121
- └─ wsl-fs ctx.fs — 子系统的真实文件,经常驻助手落在 ext4
114
+ 应用层(每个进程一份)
115
+ ├─ directory-picker-wsl、workspace-files、subprocess-wsl
116
+ ├─ fs-routing:根级文件系统,按路径分流
117
+ └─ auto-preset:会话打开发行版文件夹时绑定 wsl preset
118
+
119
+ wsl preset(每个会话)
120
+ ├─ wsl-shell:命令
121
+ └─ wsl-fs:文件操作
122
122
  ```
123
123
 
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`——结果一致、略慢、仍是每命令隔离。
127
-
128
- **文件操作的执行路径。** 读、写、搜索同样发生在子系统内部:使用子系统自己的磁盘与工具。子系统工作区的搜索运行子系统内的 `rg`,不使用 Windows 侧副本走 9p 共享。文件写入与命令沙箱使用同一份策略检查;`wsl-fs` 的 `sessionSandbox` 打开时(默认),受限写入跑在与命令相同的会话 runtime 里(同一个 native/shell agent),看到相同的 `/tmp` 与 workspace mount。读操作走共享的 shell agent。
124
+ - `wsl-shell` 通过 systemd user scope 内的一个常驻 agent 执行命令。agent 默认是发行版架构(`x86_64` 或 `aarch64`)对应的包内静态 musl `dsh-wsl-agent`,shell agent 作为回退。每条命令在登录 shell 中、`bubblewrap` mount profile 下运行。scope 比命令活得久,所以后台服务存活,`ps` 能看到整个会话。设置 `sessionSandbox: false` 后,agent 仍按发行版常驻,但每条命令使用自己的 profile。
125
+ - `wsl-fs` 通过同一个发行版内 agent 提供读写。受限写入运行在 mount profile 与策略一致的常驻 agent 中,由内核执行写入边界。没有宿主机侧的文件回退:agent 不可用时文件操作直接失败。
126
+ - GUI 文件树和预览使用随包的 `workspace-files` 行,而不是插件的替换行:它的客户端半边是浏览器的 `file` 资源提供者。WSL 路径经根级路由读取,仍在发行版内,但会多几次 agent 往返。
127
+ - 应用层服务按路径分流:发行版 UNC 路径走发行版,Windows 盘符路径留在宿主。见 [docs/root-fs-routing.md](root-fs-routing.md)。
129
128
 
130
129
  ## 沙箱
131
130
 
132
- 命令跑在子系统内的 `bubblewrap` 沙箱里,文件写入按同一份策略检查。Windows 自带的沙箱在这里用不上:它的受限账户根本连不到 WSL。
133
-
134
- | 模式 | 子系统里的命令可以做什么 |
131
+ | 模式 | 命令可以做什么 |
135
132
  |---|---|
136
- | `read-only` | 整个子系统只读;会挂一个新建的 `/dev`,`/dev/null` 和 `/dev/shm` 可用,其余不可写 |
137
- | `workspace-write` | 上面全部,加上会话工作区可写;`/tmp` 在会话 runtime 里是子系统真实 `/tmp`(`sessionSandbox: false` 时每命令 profile 挂临时 tmpfs) |
138
- | `danger-full-access` | 不加沙箱;用于你批准的放宽权限请求 |
133
+ | `read-only` | 读整个发行版;挂载新的 `/dev`,其余不可写 |
134
+ | `workspace-write` | 上面的权限,加上会话工作区;会话 runtime 下 `/tmp` 是发行版真实 `/tmp` |
135
+ | `danger-full-access` | 不经过沙箱 |
139
136
 
140
- **bubblewrap 是必需项,缺了就宁可不做。** 没有它,每条受限命令都报 `SANDBOX_UNAVAILABLE`——命令不会脱着沙箱运行。想关掉沙箱,在管这件事的行上设 `sandbox: false`:命令归 `wsl-shell`,文件写入归 `wsl-fs`,根级写入归顶层的 `fs-routing`(应用自己用的那套文件系统)。(前两行要经 `preset-wsl` 改,见[配置](#配置)。)关掉后,模型会如实收到"这些操作没有沙箱"的说明。
137
+ 设置 `sessionSandbox: false` 时,每条命令使用私有的 `/tmp`,而不是发行版的真实 `/tmp`。
141
138
 
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))。
139
+ `bubblewrap` 是必需项。缺失或损坏时,受限命令报 `SANDBOX_UNAVAILABLE`,不会运行。在对应的行设置 `sandbox: false` 可关闭沙箱;模型会收到"该操作无沙箱"的说明。
143
140
 
144
- 设计见 [docs/ARCHITECTURE.md](ARCHITECTURE.md#sandbox),插件不做的事情见 [docs/LIMITATIONS.md](LIMITATIONS.md)。
141
+ 隔离上报为 `partial`。发行版内的命令仍可通过 `/mnt/c` 启动 Windows 程序,`bubblewrap` 无法控制 Windows 进程。`maskWindowsDrive: true` 让受限视图看不到 `/mnt`,能收窄这个洞但不能关死。彻底关闭要在发行版层面设置 `[interop] enabled=false`(`/etc/wsl.conf`)。见 [docs/ARCHITECTURE.md](ARCHITECTURE.md#sandbox) 和 [SECURITY.md](../SECURITY.md)。
145
142
 
146
143
  ## 常见问题
147
144
 
148
- | 现象 | 原因 | 处理 |
149
- |---|---|---|
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) 的错误码表 |
145
+ | 现象 | 处理 |
146
+ |---|---|
147
+ | 每条命令都报 `SANDBOX_UNAVAILABLE` | 在发行版内安装 `bubblewrap`,或设置 `sandbox: false` |
148
+ | 会话在第一条命令前就被拒绝 | 启用 systemd 和 linger,或在 `wsl-shell`、`wsl-fs` 上设置 `sessionSandbox: false` |
149
+ | 发行版内 `glob`、`grep` 很慢 | 运行 `pnpm run bootstrap -- <发行版> --install` 安装 `ripgrep` |
150
+ | `dsh plugin add` 没有输出 | 插件已安装;这是成功 |
151
+ | 改了 `lib/` 但不生效 | 运行 `pnpm run sync:windows`,然后重启应用 |
152
+
153
+ 更多错误码和拒绝原因见 [docs/ARCHITECTURE.md](ARCHITECTURE.md#error-codes),插件不做的事见 [docs/LIMITATIONS.md](LIMITATIONS.md)。
160
154
 
161
155
  ## 开发
162
156
 
163
157
  ```bash
164
- pnpm run build # 从 src/ 生成 lib/(产物提交进仓,改了 src/ 必须重新构建)
165
- pnpm test # 风格、语法、构建一致性、类型检查、单元测试
166
- pnpm run sync:windows # 把检出复制到应用加载的 Windows 侧副本(改 lib/ 后必跑,再重启应用)
167
- pnpm run test:coverage # 带覆盖率阈值的单元测试(需 Node 22.8+)
168
- pnpm run diagnose # 只读诊断报告(可直接贴进 issue):版本、工具、bwrap 探测
169
- pnpm run bootstrap -- <子系统> # 检查四个子系统侧工具;加 --install 安装缺失项
170
- pnpm run probe:sandbox # 在子系统里实测 bubblewrap 能管什么、管不了什么
171
- pnpm run probe # 文件系统探针,需要真实子系统(仅 Windows + WSL)
172
- pnpm run probe:sandbox-shell # 启动真实 harness,驱动受限执行器
173
- pnpm run probe:terminal # 通过终端 provider 打开一个 PTY
174
- pnpm run probe:substrate # 用真实 wsl.exe 驱动文件助手(在子系统内运行)
175
- pnpm run probe:watch # 在真实目录上装子系统内监视器(在子系统内运行)
176
- pnpm run probe:agent # 常驻路径与回退路径的对比(在子系统内运行)
177
- pnpm run probe:exec # agent 执行:超时、杀停、cwd 失败(在子系统内运行)
178
- pnpm run probe:missing-wsl # 启动一个 wslPath 无法启动的 profile(仅 Windows + WSL)
179
- pnpm run probe:picker # 列出选择器的根级、拒绝路径与上限(仅 Windows + WSL)
180
- pnpm run probe:mode # 哪些 POSIX 权限信息能穿过共享层(需 Windows node 与已装的 harness,不启动 profile)
181
- pnpm run probe:sandbox-off # 验证 sandbox: false 确实关掉两侧沙箱(仅 Windows + WSL)
158
+ pnpm install # 开发依赖
159
+ pnpm run build # 从 src/ 生成 lib/(lib/ 提交进仓)
160
+ pnpm test # 风格、语法、构建一致性、类型、单元测试
161
+ pnpm run test:coverage # 带覆盖率阈值的单元测试
162
+ pnpm run sync:windows # 更新应用加载的 Windows 侧副本
182
163
  ```
183
164
 
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
+ 工具链跟随 harness 宿主:Node `^22.19.0 || >=24.0.0`。`src/` 是 TypeScript,`lib/` 是生成并提交的产物,必须保持同步。应用加载的是 Windows 侧的 `lib/` 副本,所以构建后要运行 `sync:windows` 并重启应用。
187
166
 
188
- [docs/ARCHITECTURE.md](ARCHITECTURE.md) 有文件布局、挂载方式和沙箱设计;[CONTRIBUTING.md](../CONTRIBUTING.md) 有开发循环、镜像、完整质量门清单和已验证内容。
167
+ 真机探针仅在 Windows + WSL 下运行。见 [CONTRIBUTING.md](../CONTRIBUTING.md) 和 `test/probe/`。
189
168
 
190
169
  ## 文档
191
170
 
192
171
  | 文档 | 内容 |
193
172
  |---|---|
194
- | [docs/CONFIGURATION.md](CONFIGURATION.md) | 每个配置键,以及如何覆盖 |
195
- | [docs/ARCHITECTURE.md](ARCHITECTURE.md) | provider、挂载方式、路径坐标、沙箱、测试分层、文件布局 |
196
- | [docs/LIMITATIONS.md](LIMITATIONS.md) | 插件不做的事情,以及原因 |
197
- | [docs/VALUE.zh.md](VALUE.zh.md) | 本插件的价值、代价,以及什么情况下不该用它 |
198
- | [CONTRIBUTING.md](../CONTRIBUTING.md) | 开发循环、质量门、约定、已验证内容 |
199
- | [SECURITY.md](../SECURITY.md) | 如何私下上报漏洞 |
200
- | [SUPPORT.md](../SUPPORT.md) | 支持范围,以及去哪里提问 |
173
+ | [docs/CONFIGURATION.md](CONFIGURATION.md) | 所有配置键及覆盖方法 |
174
+ | [docs/ARCHITECTURE.md](ARCHITECTURE.md) | 服务、路径路由、沙箱、错误码 |
175
+ | [docs/LIMITATIONS.md](LIMITATIONS.md) | 插件不做的事 |
176
+ | [docs/ROADMAP.md](ROADMAP.md) | 当前计划和推迟项 |
177
+ | [docs/VALUE.zh.md](VALUE.zh.md) | 价值与取舍 |
178
+ | [CONTRIBUTING.md](../CONTRIBUTING.md) | 开发循环、质量门、探针 |
179
+ | [SECURITY.md](../SECURITY.md) | 私下上报漏洞 |
180
+ | [SUPPORT.md](../SUPPORT.md) | 支持范围与提问渠道 |
201
181
  | [docs/RELEASING.md](RELEASING.md) | 发布检查单 |
202
- | [docs/archive/README.en.md](archive/README.en.md) | 归档工程记录的英文索引 |
203
182
 
204
183
  ## 许可
205
184
 
@@ -0,0 +1,79 @@
1
+ # Roadmap
2
+
3
+ The native in-distro agent is the default session-runtime backend since 0.10.0
4
+ ([ADR 0002](adr/0002-native-agent.md)). This plan was updated after the A6 FS
5
+ measurement; the numbers are in the ADR's
6
+ [FS queue measurement](adr/0002-native-agent.md#fs-queue-measurement-a6-2026-10-18)
7
+ section.
8
+
9
+ ## Done
10
+
11
+ | Step | Outcome |
12
+ |---|---|
13
+ | A1 | `wsl-shell.agentBackend` (`auto`/`native`/`shell`): the native session-runtime agent, the shell fallback, the strict `native` mode |
14
+ | A2 | packaged static-musl `agent/native/linux-x86_64` and `linux-aarch64` binaries, selected by the distro's `uname -m`; the dev override |
15
+ | A3 | the native HELLO carries `/proc/self/exe`'s sha256; the host binds it with `expectedDigest` |
16
+ | A4 | the native-session probe (scope, EXEC, FS, KILL, digest, teardown, 8/8) in the real-machine suite; the warm-cache and stdin-EPIPE fixes |
17
+ | A5 | the native agent is the documented default; 0.10.0 published (npm + GitHub Release) |
18
+ | A6 | `WslAgent.metrics()` and `test/probe/fs-queue.mjs`; the shared shell read agent is the measured batch bottleneck |
19
+
20
+ ## Next
21
+
22
+ ### A7 — native shared read agent (v5) — done
23
+
24
+ The shared unconfined read agent was the only remaining measured bottleneck
25
+ (16 concurrent reads 342-614 ms, queue up to 583 ms, versus native FS execution
26
+ 0.2-0.3 ms). A7 swaps its shell for the native binary (v5) while keeping the
27
+ shell as the fallback:
28
+
29
+ - **selection:** the same `agentBackend` semantics — `auto` prepares the
30
+ packaged binary for the distro's architecture, `native` refuses instead of
31
+ falling back, `shell` forces the shell; the dev override still wins;
32
+ - **preparation:** `ensureNativeBackend` once per distro (cache/digest/exec
33
+ probe); the `WslAgent` with `expectedDigest` and `signalable: false`; on a
34
+ preparation or HELLO failure the shell agent serves the reads — fail-open to
35
+ the shell, never fail-closed;
36
+ - **surface:** the same six v5 FS ops and `dsh-fs` side channel; the
37
+ `fs-substrate`/`DistroFs` parse is unchanged; the A2 differential covers the
38
+ records;
39
+ - **result:** `WslAgent` gained a `prepare()` hook and the shared agent now
40
+ selects the native entry through it. `test/probe/fs-queue.mjs` measures 16
41
+ concurrent shared reads at **2.4-4.1 ms** (from 342-614 ms); the first start
42
+ pays the ensure/cache cost (~70 ms once). The shell still serves the reads
43
+ under `agentBackend: shell` and after a native failure.
44
+
45
+ ### A8 — aarch64 functional validation
46
+
47
+ The aarch64 binary is built by CI but never executed there. Run the 18-check
48
+ spike suite and the native-session probe on real aarch64 WSL or qemu; record
49
+ the result in the ADR.
50
+
51
+ ### A9 — maintenance
52
+
53
+ - Release A7 as a minor (0.11.0) through the tag workflow — done;
54
+ - `pnpm run sync:windows` after each release, then restart the app;
55
+ - keep the probes and the documentation current.
56
+
57
+ ## Deferred
58
+
59
+ - **v6 (FS concurrency / ERR frames / per-request env).** The A6 measurement
60
+ shows the lock-removal gain is marginal (~7.7 -> ~5 ms per 16 writes, ~12.2
61
+ -> ~8 ms per 16 reads). Revisit only if the native shared v5 still queues
62
+ measurably or the ERR/env framing is needed for another reason.
63
+ - **Event stream.** No current consumer: the `fs.watch` contract is coarse and
64
+ the inotify watcher satisfies it; terminal activity and ports poll.
65
+ - **Shell retirement.** Not planned. The shell agent remains the fallback and
66
+ the shared read path until A7 lands, and the protocol/script stays supported.
67
+
68
+ ## Not planned
69
+
70
+ - Streaming command output (the harness has no consumer for it).
71
+ - A per-subagent agent: subagents share the session runtime by workspace and
72
+ policy; separate runtimes only if a measurable need appears.
73
+
74
+ ## Decision rules
75
+
76
+ A step moves out of Deferred only with a measurement or a concrete consumer,
77
+ not because the protocol makes it possible. A7 itself is conditional on the
78
+ batch-read demand (the GUI file tree, parallel subagents) being real; the
79
+ single-op latency is the ~22 ms host round trip, not the FS backend.
@@ -14,10 +14,12 @@
14
14
  * @module dsh-plugin-wsl/agent-shared
15
15
  */
16
16
  import { createHash } from "node:crypto";
17
+ import { spawn } from "node:child_process";
17
18
  import { readFileSync } from "node:fs";
18
19
  import { fileURLToPath } from "node:url";
19
20
  import { toLinuxPath } from "./paths.js";
20
- import { WslAgent } from "./agent.js";
21
+ import { pinnedWindowsEnv, WslAgent } from "./agent.js";
22
+ import { configuredAgentBackend, ensureNativeBackend, NATIVE_AGENT_ENV } from "./native-agent.js";
21
23
  const AGENT_SCRIPT_PATH = fileURLToPath(new URL("../agent/wsl-agent.sh", import.meta.url));
22
24
  // The script the distro reads must be byte-identical to the copy this
23
25
  // package shipped: the deployed layout reads it from the /mnt/c mirror, and
@@ -25,6 +27,9 @@ const AGENT_SCRIPT_PATH = fileURLToPath(new URL("../agent/wsl-agent.sh", import.
25
27
  // Both resident factories (plain and confined) pin it.
26
28
  const AGENT_SCRIPT_DIGEST = createHash("sha256").update(readFileSync(AGENT_SCRIPT_PATH)).digest("hex");
27
29
  const sharedAgents = new Map();
30
+ // Distros where the native shared agent failed at runtime; the reads fail open
31
+ // to the shell instead of retrying the same broken binary on every call.
32
+ const nativeFailed = new Set();
28
33
  /** The agent script as a host-side path, for factories that need both spellings. */
29
34
  export const agentScriptPath = AGENT_SCRIPT_PATH;
30
35
  /** The sha256 of the shipped agent script; every resident verifies its HELLO against it. */
@@ -37,9 +42,32 @@ export const agentScriptDigest = AGENT_SCRIPT_DIGEST;
37
42
  */
38
43
  export function sharedAgent(distro) {
39
44
  let agent = sharedAgents.get(distro);
40
- if (agent === undefined) {
41
- agent = new WslAgent({ distro, scriptPath: toLinuxPath(AGENT_SCRIPT_PATH), expectedDigest: AGENT_SCRIPT_DIGEST });
42
- sharedAgents.set(distro, agent);
43
- }
45
+ if (agent !== undefined && !agent.unavailable)
46
+ return agent;
47
+ if (agent !== undefined)
48
+ nativeFailed.add(distro);
49
+ agent = new WslAgent({
50
+ distro,
51
+ scriptPath: toLinuxPath(AGENT_SCRIPT_PATH),
52
+ expectedDigest: AGENT_SCRIPT_DIGEST,
53
+ prepare: async () => {
54
+ if (configuredAgentBackend() === "shell" || nativeFailed.has(distro))
55
+ return undefined;
56
+ const prepared = await ensureNativeBackend({ wslPath: "wsl.exe", distro, source: process.env[NATIVE_AGENT_ENV] ?? "" });
57
+ if (prepared === undefined)
58
+ return undefined;
59
+ return {
60
+ scriptPath: prepared.path,
61
+ expectedDigest: prepared.digest,
62
+ signalable: false,
63
+ spawnTransport: ({ wslPath, distro: target, argvPrefix }) => spawn(wslPath, ["-d", target, "--exec", ...argvPrefix, prepared.path], {
64
+ stdio: ["pipe", "pipe", "pipe"],
65
+ windowsHide: true,
66
+ env: pinnedWindowsEnv(process.env),
67
+ }),
68
+ };
69
+ },
70
+ });
71
+ sharedAgents.set(distro, agent);
44
72
  return agent;
45
73
  }