dsh-plugin-wsl-env 0.17.0 → 0.17.1

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.
@@ -1,5 +1,7 @@
1
1
  # Configuration
2
2
 
3
+ *A Chinese translation lives at [CONFIGURATION.zh](CONFIGURATION.zh.md); the English version is authoritative.*
4
+
3
5
  ## Profile patches
4
6
 
5
7
  Store profile settings in `$DSH_HOME/profiles/<name>/cordis.patch.yml`. Use
@@ -0,0 +1,288 @@
1
+ # Configuration
2
+
3
+ _本文是 [CONFIGURATION](CONFIGURATION.md) 的中文译本,以英文版为准。_
4
+
5
+ ## Profile patch
6
+
7
+ profile 设置存放在 `$DSH_HOME/profiles/<name>/cordis.patch.yml`。可以用
8
+ [`examples/profile.cordis.patch.yml`](../examples/profile.cordis.patch.yml)
9
+ 作为起点。随包的
10
+ [`cordis.patch.yml`](../cordis.patch.yml) 列出了本插件附带的行和值。
11
+
12
+ profile 层可以按 `id` 指向顶层行。这一层的插件行包括 `fs-routing`、
13
+ `directory-picker-wsl`、`subprocess-wsl`、`wsl-shell-env`、`wsl-preflight`、
14
+ `auto-preset` 和 `preset-wsl`。`wsl-shell` 和 `wsl-fs`
15
+ 两行嵌套在 `preset-wsl.config.plugins` 内;直接按这两个 id 配置的
16
+ profile 条目会被忽略,并产生 `patch: entry <id> not found` 警告。
17
+
18
+ 一行的 `config` 是整体替换。如果你想保留随包行中的非默认值,
19
+ 必须把它们一并写入。这对 `preset-wsl` 尤其重要:
20
+ 它的 config 包含完整的 preset 及其嵌套插件。
21
+
22
+ 在下表中,**shipped** 表示该值在随包行中被显式设置。
23
+ 其他值是 schema 默认值。省略的键使用 schema 默认值。
24
+
25
+ ## 配置 `wsl-shell` 和 `wsl-fs`
26
+
27
+ 这两行嵌套在 `preset-wsl` 内。使用辅助脚本修改它们的设置:
28
+
29
+ ```bash
30
+ pnpm run preset --distro Ubuntu --session-sandbox false
31
+ ```
32
+
33
+ 支持的修改是 `--distro`、`--session-sandbox`、`--sandbox`、
34
+ `--mask-windows-drive` 和 `--agent-backend`。不带 `--out` 时,辅助脚本
35
+ 把完整的 `preset-wsl` 块打印出来,供你手动合并进 profile。
36
+ 使用 `pnpm run preset --check <file>` 可以检测直接指向嵌套行的
37
+ profile 层;检测到时会打印应使用的块。`--out` 会整体替换目标文件,
38
+ 包括其中的其他覆盖,即使它已经包含 `preset-wsl` 行。
39
+ 在不包含该行的文件已存在时,替换会被拒绝,除非传入 `--force`。
40
+ 对已有 profile 建议手动合并。
41
+
42
+ 要手动编辑 preset,需替换整个 `preset-wsl` 行。它的 config
43
+ 不做深度合并:保留 `id`、`order` 以及 `plugins` 中的每一项。
44
+ 从 `cordis.patch.yml` 或 `dsh --profile <name> --dump-config`
45
+ 复制当前块,再修改嵌套的值。插件升级改变了 preset 后要重新复制。
46
+
47
+ ### `wsl-shell`
48
+
49
+ | 键 | 默认 / shipped 值 | 说明 |
50
+ |---|---|---|
51
+ | `distro` | `''`(shipped) | 空或 `auto` 跟随请求工作目录所属的发行版。`default` 选择 WSL 默认发行版;给出名字则把命令固定到该发行版。 |
52
+ | `shell` | `''` | 空则解析发行版用户的登录 shell。设置绝对 Linux 路径可固定一个 shell。 |
53
+ | `loginShell` | `true`(shipped) | 对 POSIX shell 使用登录语义。非 POSIX shell 使用各自的命令形式。 |
54
+ | `agent` | `true` | 使用常驻 agent。不可用时,命令走一次性的 `wsl.exe` 路径。设为 `false` 则总是走该路径。Cwd 与沙箱拒绝会被直接返回。 |
55
+ | `agentBackend` | `auto`(shipped) | `auto` 选择包内 `agent/native/linux-<arch>/dsh-wsl-agent` 的静态 musl 二进制或 `DSH_WSL_AGENT_NATIVE` 覆盖;否则使用 shell agent。`native` 在准备失败时报错;`shell` 强制使用 shell agent。适用于命令、受限写入和共享读取。 |
56
+ | `sessionSandbox` | `false`;shipped 为 `true` | `true` 把会话跑在一个持久的 systemd user scope 中。要求 `agent: true`、systemd 为 PID 1(`/etc/wsl.conf` 中 `[boot] systemd=true`)、一个 user manager、`systemd-run` 和 Linger(`loginctl enable-linger <user>`)。`false` 使用按命令的沙箱。`auto` 在可用时使用 scope,否则按命令约束。结果会报告 `sessionScope: 'session'` 或 `'per-command'`。 |
57
+ | `sandbox` | `true` | 用 `bwrap` 约束命令。`bwrap` 缺失或不可用时关闭(fail closed)。`false` 则不加约束地运行命令。 |
58
+ | `interopVisibility` | 未设置;有效默认值为 `visible` | `visible`、`masked` 或 `disabled`。设置后优先于 `maskWindowsDrive`。见 [Windows interop](#windows-interop)。 |
59
+ | `maskWindowsDrive` | `false` | 对受限命令,在 `/mnt` 上挂载空的 tmpfs。见 [Windows interop](#windows-interop)。 |
60
+ | `cwd` | `''` | 默认工作目录。空则使用发行版用户家目录。 |
61
+ | `timeoutMs` / `maxTimeoutMs` | `120000` / `600000`(shipped) | 默认的单次调用超时,以及一次调用可请求的最大超时。 |
62
+ | `wslPath` | `wsl.exe` | `wsl.exe` 的路径。 |
63
+ | `hostCwd` | 未设置 | `wsl.exe` 进程的 Windows 工作目录。可用时默认为 `%SystemRoot%`。 |
64
+ | `forwardEnv` | `[]` | 额外通过 `WSLENV` 转发的宿主环境变量名。见 [环境变量转发](#environment-forwarding)。 |
65
+
66
+ ### `wsl-fs`
67
+
68
+ | 键 | 默认 / shipped 值 | 说明 |
69
+ |---|---|---|
70
+ | `distro` | `''`(shipped) | 空或 `auto` 跟随目标路径所属的发行版。`default` 选择 WSL 默认发行版;给出名字则固定文件系统。 |
71
+ | `restrictToDistro` | `true`(shipped) | 固定发行版时,以 `FS_OUTSIDE_DISTRO` 拒绝属于其他发行版共享的路径。 |
72
+ | `sandbox` | `true` | 按沙箱策略检查写入。`bwrap` 不可用时,受限写入关闭(fail closed)。 |
73
+ | `sessionSandbox` | `false`;shipped 为 `true` | 使用与 `wsl-shell` 相同的会话 runtime。`auto` 在会话 runtime 不可用时回退到按策略约束的常驻 agent。 |
74
+ | `maskWindowsDrive` | `false` | 对受限变更隐藏 `/mnt`。读取不受影响。 |
75
+ | `substrate` | `agent`(shipped) | 文件操作使用发行版内 agent。旧值 `share` 在启动时被拒绝。 |
76
+ | `cwd` | `''` | 相对路径的基准目录。空则使用发行版用户家目录。 |
77
+ | `watchMaxDepth` | `0` | 监视整棵树。正值深度使用有界轮询,可能遗漏边界以下的变更,直到边界之上发生变更触发回调。 |
78
+ | `wslPath` | `wsl.exe` | `wsl.exe` 的路径。 |
79
+ | `diffBasisMaxBytes` | `10485760` | 覆盖写入 diff 每一侧的 UTF-8 字节上限。更大的文件不带 diff 直接写入。 |
80
+
81
+ ### `fs-routing`
82
+
83
+ 这个顶层行为根级 `ctx.fs` 调用提供服务。它把发行版 UNC 路径
84
+ 路由到 agent 支撑的文件系统,把盘符路径路由到宿主文件系统。它的
85
+ 发行版设置独立于 `wsl-fs`。
86
+ Loader 在挂载该行之前会校验宿主和发行版设置;
87
+ 被 schema 拒绝的值会阻止路由器启动。字段值为 `null`
88
+ 时使用其 schema 默认值。
89
+
90
+ | 键 | 默认 / shipped 值 | 说明 |
91
+ |---|---|---|
92
+ | `distro` | `''`(shipped) | 空或 `auto` 把每条 UNC 路径路由到其命名的发行版。`default` 选择 WSL 默认发行版;给出名字则固定路由。 |
93
+ | `wslPath` | `wsl.exe`(shipped) | `wsl.exe` 的路径,用于解析默认发行版。 |
94
+ | `sandbox` | `true`(shipped) | 按 `ctx.sandboxPolicy` 检查被路由的变更;`bwrap` 不可用时关闭(fail closed)。无论该围栏启用与否,该行都报告 `sandboxMode: undefined`。 |
95
+ | `maskWindowsDrive` | `false`(shipped) | 对受限的被路由变更隐藏 `/mnt`。 |
96
+ | `restrictToDistro` | `true`(shipped) | `distro` 被固定时,拒绝属于其他发行版共享的路径。 |
97
+ | `watchMaxDepth` | `0`(shipped) | 监视整棵树。正值深度使用有界轮询。 |
98
+ | `distroCwd` | `''` | 默认发行版工作目录;空则使用发行版用户家目录。 |
99
+ | `cwd` | `process.cwd()` | 宿主文件系统后端的工作目录。 |
100
+ | `diffBasisMaxBytes` | `10485760` | 宿主与被路由后端共享的覆盖写入 diff 上限。 |
101
+
102
+ 根路由器是应用层的,不属于会话,因此不使用
103
+ `sessionSandbox`。
104
+
105
+ ### 选择器与终端行
106
+
107
+ | 行 | 键 | 默认 / shipped 值 | 说明 |
108
+ |---|---|---|---|
109
+ | `directory-picker-wsl` | `preferredDistro` | `''`(shipped) | 首位展示的发行版,也是 Home 位置使用的发行版。 |
110
+ | | `includeHostHome` | `true`(shipped) | 包含 Windows 家目录。 |
111
+ | | `maxEntries` | `1000`(shipped) | 单个目录返回的最大条目数。 |
112
+ | | `wslPath` | `wsl.exe` | `wsl.exe` 的路径。 |
113
+ | | `distroCacheMs` | `5000` | 发行版列表的缓存时长。 |
114
+ | `subprocess-wsl` | `distro` | `''`(shipped) | 空或 `auto` 跟随启动目录所属的发行版。`default` 选择 WSL 默认;给出名字则固定发行版。 |
115
+ | | `shell` | `''` | 在发行版内固定的 shell。空则由 `wsl.exe` 选择登录 shell。 |
116
+ | | `loginShell` | `true` | 固定 shell 时的登录语义。 |
117
+ | | `hostSessions` | `true` | 在 Windows 文件夹上打开的会话,会在该文件夹中获得宿主 PowerShell。WSL 文件夹会话使用发行版 shell。设为 `false` 则所有会话都使用 composition 拥有的终端。 |
118
+ | | `terminalIdleReclaim` | `true` | 依据被标记的进程回收空闲的发行版终端。活动状态未知时暂停回收。 |
119
+ | | `cwd` | `''` | 未提供启动目录时的回退目录。 |
120
+ | | `wslPath` | `wsl.exe` | `wsl.exe` 的路径。 |
121
+ | | `hostCwd` | 未设置 | `wsl.exe` 进程的 Windows 工作目录。可用时默认为 `%SystemRoot%`。 |
122
+ | | `forwardEnv` | `[]` | 额外通过 `WSLENV` 转发的宿主环境变量名。 |
123
+
124
+ ### 其他顶层行
125
+
126
+ | 行 | 键 | 默认值 | 说明 |
127
+ |---|---|---|---|
128
+ | `auto-preset` | `preset` | `wsl` | 会话工作区位于发行版内时绑定的 preset。操作者的显式选择会被保留。 |
129
+ | | `attempts` | `6` | 绑定 preset 的尝试次数。 |
130
+ | | `retryMs` | `150` | 两次尝试之间的延迟,单位毫秒。 |
131
+ | `wsl-preflight` | `distro` | `''` | 要检查的发行版。空或 `auto` 跟随会话工作区。 |
132
+ | | `wslPath` | `wsl.exe` | `wsl.exe` 的路径。 |
133
+ | | `sandbox` | `true` | 运行会话启动时的 `bwrap` 检查。 |
134
+ | | `sessionSandbox` | `true` | 同时检查 systemd、user manager、`systemd-run` 和 Linger。profile 不使用会话 runtime 时设为 false。 |
135
+ | `wsl-shell-env` | `distro` | `''` | 用于填充 `DSH_WSL_*` 环境事实的发行版。 |
136
+ | | `portsRefreshMs` | `10000` | 监听端口快照的刷新间隔,单位毫秒。 |
137
+
138
+ GUI 文件树和预览通过 `fs-routing` 使用随包的 `workspace-files` 行;
139
+ 它们不使用单独的 WSL 提供者行。要修改其 peer 定义的限额,
140
+ 在 profile 层按 id 覆盖 `workspace-files`。
141
+
142
+ ## 共享的命令设置
143
+
144
+ shell 行从 `@deepseek-ai/dsh-bash-local` 继承输出与进程设置:
145
+
146
+ | 键 | 默认值 | 说明 |
147
+ |---|---|---|
148
+ | `maxOutputBytes` | `64000` | 每条流的捕获输出上限。agent 在发行版内执行该限制,并标记被截断的结果。 |
149
+ | `maxSpillBytes` | `67108864` | 一次性路径上每条流的 spill 文件上限。 |
150
+ | `graceMs` | `3000` | SIGTERM 与 SIGKILL 之间的宽限期。 |
151
+
152
+ `agent: true` 时,输出在发行版内被限制,且不写 spill 文件。
153
+ `maxSpillBytes` 和 `graceMs` 适用于 `agent: false`,或常驻 agent
154
+ 不可用而改走一次性 `wsl.exe` 的情况。
155
+
156
+ ### 模型运行命令的 zsh/fnm 配方
157
+
158
+ 命令以 `<shell> -lc` 运行,因此 `~/.zshrc` 永远不会被读取——原因见
159
+ [LIMITATIONS](LIMITATIONS.md#shell-startup-semantics-wsl-shell)。把 fnm 的
160
+ hook 放进 `~/.zprofile`,并为沙箱准备一个回退——沙箱中 `/run` 是只读的,
161
+ fnm 无法创建它的 multishell 符号链接目录:
162
+
163
+ ```zsh
164
+ # ~/.zprofile — fnm for non-interactive login shells (mirrors the ~/.zshrc hook)
165
+ path=("$HOME/.local/share/fnm" $path) # the fnm binary itself
166
+ _fnm_env="$(command -v fnm &>/dev/null && fnm env --shell zsh 2>/dev/null)" && eval "$_fnm_env"
167
+ if [[ -z "${FNM_MULTISHELL_PATH:-}" || ! -e "${FNM_MULTISHELL_PATH}/bin/node" ]]; then
168
+ path=("$HOME/.local/share/fnm/aliases/default/bin" $path) # sandbox fallback
169
+ fi
170
+ unset _fnm_env
171
+ ```
172
+
173
+ 交互式终端仍使用 `~/.zshrc` 中的 hook,不受影响;
174
+ `aliases/default` 回退只在 multishell 链接无法存在时接管。
175
+
176
+ ## 环境变量转发
177
+
178
+ 只有名字列在 `WSLENV` 中的宿主环境变量才会被 WSL 导入。各提供者
179
+ 默认转发 `NO_COLOR`、`TERM`、`PAGER`、`GIT_PAGER`、`LANG`、`LC_ALL`
180
+ 以及托管的 `DSH_*` 名字。`forwardEnv` 向该列表追加名字;
181
+ `PATH` 永远不会被转发。
182
+
183
+ 对 `wsl-shell` 而言,`forwardEnv` 只作用于一次性 `wsl.exe` 路径。
184
+ 常驻 agent 只接收托管的 `DSH_*` 环境,因此 `forwardEnv` 里的自定义名字
185
+ 不会到达经由该 agent 运行的命令。`subprocess-wsl` 的设置
186
+ 作用于它的 WSL 启动。
187
+
188
+ ## 发行版选择
189
+
190
+ 对命令与终端行而言,空的 `distro` 或 `distro: auto` 跟随
191
+ 请求的 UNC 工作目录。当请求未指明发行版时,使用 WSL 的
192
+ 默认发行版。对 `wsl-fs`,`auto` 跟随每个目标路径。对
193
+ `fs-routing`,每条 UNC 目标各自选择自己的发行版。
194
+
195
+ `distro: default` 总是选择 WSL 的默认发行版。给出发行版名字则把该行
196
+ 固定到那个发行版。`restrictToDistro: true` 时,属于其他发行版共享的路径
197
+ 会被以 `FS_OUTSIDE_DISTRO` 拒绝。挂载在 `/mnt/c` 的 Windows 盘
198
+ 位于所选发行版之内,不会被这个围栏拒绝。
199
+
200
+ ## Windows interop
201
+
202
+ `interopVisibility` 控制是否对受限命令遮蔽 `/mnt`;
203
+ 它本身不会禁用 Windows interop。未设置时,`maskWindowsDrive: false`
204
+ 给出 `visible` 行为。
205
+
206
+ | 值 | 效果 |
207
+ |---|---|
208
+ | `visible` | 保持 `/mnt` 可见。受限命令可以读取 Windows 文件,并通过 WSL interop 启动 Windows 可执行文件。 |
209
+ | `masked` | 在受限 profile 中,把空的 tmpfs 挂载到 `/mnt` 上。命令无法访问 `/mnt` 下的路径;其他挂载与不受限的文件读取不在覆盖范围内。 |
210
+ | `disabled` | 施加与 `masked` 相同的 `/mnt` 遮蔽;它不会禁用 interop,也不会修改 `wsl.conf`。请按下文单独配置发行版。 |
211
+
212
+ `maskWindowsDrive: true` 等价于 `interopVisibility: masked`。
213
+ 设置了 `interopVisibility` 时,它优先于
214
+ `maskWindowsDrive`。
215
+
216
+ 遮蔽只是收窄访问,并不关闭 interop。WSL 仍能运行被写入工作区的
217
+ Windows 可执行文件,因为 `binfmt_misc` 按文件内容分发。
218
+ 因此强制级别仍是 `partial`,结果会报告 `windowsDrive: 'masked'`。
219
+ 要在该发行版上禁用 interop,把下面这段加进 `/etc/wsl.conf`:
220
+
221
+ ```ini
222
+ [interop]
223
+ enabled=false
224
+ ```
225
+
226
+ 然后运行 `wsl.exe --shutdown`。要关闭 interop,必须做这一发行版级修改。
227
+
228
+ 要对两个嵌套行都应用 masked profile:
229
+
230
+ ```bash
231
+ pnpm run preset --sandbox true --mask-windows-drive true
232
+ ```
233
+
234
+ 把打印出的块合并进 profile。`visible` 与 `masked` 的行为
235
+ 由 `test/probe/interop-shell.sh` 对常驻与一次性两种命令都有覆盖。
236
+ 发行版级的 disabled 状态需要手动检查,因为它会改动 WSL 配置。
237
+
238
+ ## 沙箱与预检
239
+
240
+ `wsl-shell`、`wsl-fs` 和 `fs-routing` 的默认值都是 `sandbox: true`。
241
+ 受限命令与变更要求发行版内有可用的 `bwrap`。缺失或不可用时,
242
+ 命令以 `SANDBOX_UNAVAILABLE` 失败,写入以 `FS_IO_ERROR` 失败;
243
+ 两种操作都不会回退到无约束运行。错误信息会区分
244
+ 包缺失与 `bwrap` 存在但不可用两种情况。
245
+
246
+ 用 bootstrap 辅助脚本安装该前置依赖:
247
+
248
+ ```bash
249
+ scripts/bootstrap.sh <distro> --install
250
+ ```
251
+
252
+ 不带 `--install` 时,辅助脚本只做只读检查。在基于 apt 的
253
+ 发行版上,直接安装命令是:
254
+
255
+ ```bash
256
+ wsl.exe -d <distro> -u root -- apt-get install -y bubblewrap
257
+ ```
258
+
259
+ 会话打开时,`wsl-preflight` 行会运行这些检查。
260
+ `sessionSandbox: true` 时,它还会检查会话 runtime 的前置条件。
261
+ 该警告仅供参考:它不会安装软件包,也不会改变提供者关闭(fail-closed)
262
+ 的行为。在该行设置 `sandbox: false` 或移除该行可让沙箱警告静音。
263
+ profile 不使用会话 runtime 时,设置 `sessionSandbox: false`。
264
+
265
+ 在 `wsl-shell` 或 `wsl-fs` 上设置 `sandbox: false` 后,命令或写入
266
+ 将无约束地运行;其 `sandboxMode` 变为 `undefined`。工具层
267
+ 会报告没有沙箱生效。在 `fs-routing` 上,该设置会禁用变更围栏;
268
+ 无论开关如何,该行都报告 `sandboxMode: undefined`。
269
+
270
+ ## 诊断配置问题与拒绝
271
+
272
+ 使用 `dsh --profile <name> --dump-config` 检查合成后的 profile,
273
+ 无需真正启动。输出会显示某行的覆盖是否生效。
274
+ 使用 `scripts/bootstrap.sh <distro>` 检查发行版前置依赖。
275
+
276
+ | 现象 | 结果 | 处理 |
277
+ |---|---|---|
278
+ | 命令沙箱不可用 | `SANDBOX_UNAVAILABLE`;消息区分 `bwrap` 缺失或不可用。 | 安装 `bubblewrap`、修复用户命名空间,或在 `wsl-shell` 上设置 `sandbox: false`。 |
279
+ | 受限写入不可用 | `FS_IO_ERROR`,附带 `bwrap` 补救方法。 | 安装或修复 `bubblewrap`;要禁用围栏,在 `wsl-fs` 或 `fs-routing` 上设置 `sandbox: false`。 |
280
+ | 发行版已停止时文件操作失败 | `FS_IO_ERROR`:发行版 substrate 不可用。 | 启动该发行版。若 agent 仍不可用,重启会话。 |
281
+ | 命令无法在所选发行版中启动 | `WSL_E_*` 错误码,指明发行版。 | 启动该发行版或修改 `distro`。更宽的权限无法解决发行版已停止的问题。 |
282
+ | `wsl.exe` 无法启动 | 错误指明可执行文件与该行的 `wslPath`。 | 修正 `wslPath`,并确认已安装 WSL。 |
283
+ | 命令执行中 WSL 挂起 | 命令到达截止时间仍未完成。 | 重试一次;仍不行则运行 `wsl.exe --shutdown` 并重新打开会话。 |
284
+ | 被监视的变更没有通知 | 轮询可能遗漏保留所观察元数据的变更;不保证报错。 | 查看 [LIMITATIONS.md](LIMITATIONS.md) 中的监视器限制。 |
285
+ | 其他发行版的路径被拒绝 | `FS_OUTSIDE_DISTRO`。 | 使用 `distro: ''` 或 `auto` 跟随工作区,或在目标发行版中打开会话。 |
286
+
287
+ 完整错误参考见 [ARCHITECTURE.md — Error codes](ARCHITECTURE.md#error-codes),
288
+ 设计边界见 [LIMITATIONS.md](LIMITATIONS.md)。
@@ -1,377 +1,170 @@
1
1
  # Limitations
2
2
 
3
+ *A Chinese translation lives at [LIMITATIONS.zh](LIMITATIONS.zh.md); the English version is authoritative.*
4
+
3
5
  What the plugin does not do, and why. A limitation that is documented is a design
4
- boundary; one that is not is a bug report waiting to happen.
6
+ boundary; one that is not is a bug report waiting to happen. Each entry states the
7
+ limit and the remedy; the reasoning lives in [ARCHITECTURE.md](ARCHITECTURE.md) and
8
+ the documents it links.
5
9
 
6
10
  ## The filesystem substrate (`wsl-fs`)
7
11
 
8
- The WSL-routed file tools use one substrate: the resident in-distro agent —
9
- reads, writes and identities happen on the filesystem backing the target path
10
- (typically ext4 for a distro's own files, but other mounts are possible). The former
11
- Windows-side share backend (`substrate: "share"`) is retired: a profile still
12
- carrying the value is refused at boot with the migration, and WSL-routed file
13
- operations do not use the Windows-side 9p share. What to know about the substrate:
14
-
15
- - **A new file publishes with mode 0600.** That is the host backend's own POSIX
16
- publication semantics (a private 0600 staging file, renamed into place), which
17
- a Linux-host DSH session has always had; the share dropped mode sets and took
18
- the umask default instead, and that behaviour is gone with the share.
19
- Overwrites and edits keep the original bits. Run `chmod` (or ask the model to)
20
- when a new file must be executable.
21
- - **When the agent is out, file operations fail closed.** The distro stopped, or
22
- the resident crashed through its revive budget: `FS_IO_ERROR` naming the
23
- substrate, with the recovery path in the message. A single crash rides the
24
- one rebuild (read-only ops only); a spent rebuild enters the bounded revival
25
- (2/4/8 s), and three deaths make the state permanent with the restart remedy. There is no fallback I/O to degrade
26
- into — deliberately; the old failure message pointed at the share, and the
27
- share is what this plugin exists to keep the model off.
28
- - **The mutation guard survives to the write.** `createIfAbsent` publishes with
29
- a no-replace link inside the distro, which closes the check-then-write window
30
- for creates outright. An overwrite or edit carries the version it was based
31
- on into the write op, and the agent re-verifies it against a fresh stat before
32
- the rename. A mismatch returns `FS_STALE_VERSION`, but a change between the
33
- check and rename can still be overwritten; this is not an atomic
34
- compare-and-swap.
35
- - **A cancelled write can have an unknown outcome.** Cancellation is reported
36
- before the agent necessarily finishes. A write already being published may
37
- still take effect; inspect the file before retrying. The native agent refuses
38
- queued writes when it processes their cancellation before they start. The
39
- serial shell fallback removes requests cancelled while waiting on the host;
40
- a request already sent to the agent may still take effect even if it has not
41
- yet acknowledged the request.
42
- - **A successful write is not guaranteed durable across a crash.** The native
43
- agent syncs the staged file before publication; the shell agent attempts
44
- `sync -f` but ignores failure. Neither syncs the containing directory after
45
- the rename or link. A kernel panic or power loss can therefore lose data or
46
- the published directory entry; the operation is not a crash-consistent store.
47
- - **A Linux filename containing a backslash cannot be addressed.** The UNC
48
- display form (`\\wsl.localhost\<distro>\...`) is the file's identity
49
- everywhere - the session header, the GUI, the caches - and the backslash is
50
- the UNC's separator, so `a\b.ts` in the distro round-trips to `a/b.ts`: the
51
- tools would silently address a different path. This is the coordinate
52
- system's own limit — a backslash-delimited identity cannot spell a name
53
- containing a backslash — not a defect of the substrate; a rename away from
54
- backslash names is the workaround.
55
- - **Stage two routes mutations through a confined resident.** A write or edit
56
- under a confined policy runs on its own long-lived agent, spawned inside the
57
- bwrap profile that binds exactly what the mode grants — so the kernel, not
58
- just the host-side check, refuses a write outside the workspace, and the
59
- refusal carries `FS_SANDBOX_DENIED` (the command path's dialect). Costs to
60
- know: `bubblewrap` becomes required for confined writes (a distro without it
61
- refuses them with the bootstrap command, rather than downgrading); a policy
62
- change addresses a different resident, so the first write after switching the
63
- Permissions selector pays one agent startup; and a write the policy grants to
64
- the distro's `/tmp` lands in the distro's real `/tmp`, because the
65
- file-writes resident binds it read-write — where the read tools, the plain
66
- resident and the user's own shell all see it. The ephemeral tmpfs belongs to
67
- the command side alone, so a file handed over from a command still goes
68
- through the session workspace.
69
- - **`glob` and `grep` run the distro's own rg** (`lib/search-route.js`): the
70
- tool's arguments are forwarded verbatim, so results, output format and exit
71
- codes are rg's own — at ext4 speed, with the distro's `.gitignore` rules.
72
- A distro without rg fails the search outright: exit 127, with the message
73
- from the exec launcher and never from rg itself (which is the missing
74
- program) — `setsid: failed to execute rg: No such file or directory` where
75
- the distro has `setsid`, the script shell's `rg: not found` where it does
76
- not. The install command `scripts/bootstrap.sh` prints — `wsl.exe -d <distro>
77
- -u root -- apt-get install -y bubblewrap git ripgrep inotify-tools`, or the
78
- detected family's spelling — installs it. `--install` runs that command only
79
- when `bubblewrap` itself is missing, so a distro that has bwrap but not rg
80
- needs the printed line run by hand. A search whose directory is a Windows
81
- folder keeps the host binary, which is native there.
82
- - **`watch()` prefers inotify, with a polling fallback.** When `inotifywait`
83
- is installed and the target is an unbounded directory, the watcher uses
84
- recursive inotify watches and coalesces events to one callback per 0.5s.
85
- Otherwise — a file target, a positive `maxDepth`, or no `inotify-tools` — it
86
- is the `find -newer` poll loop (a `stat` fingerprint for a file), so the
87
- coarse-invalidation caveats below apply.
88
- - **With `sessionSandbox: false`, a confined command's `/tmp` is not the write
89
- tool's `/tmp`.** `workspace-write` mounts a fresh tmpfs at `/tmp` for the
90
- command — that is what makes it ephemeral — while the file tools' fence grants
91
- the distro's *real* `/tmp`. Both are writable, so neither side is refused, but a
92
- file the write tool puts in `/tmp` is invisible to `bash` and vice versa. Hand
93
- a file between them through the session workspace. The shipped preset turns
94
- the session runtime on, and `wsl-fs` shares it for confined writes, so
95
- commands and writes bind the same real `/tmp` and workspace.
96
- - A distro that is stopped or unregistered while the app runs takes the file
97
- tools' substrate with it: reads and writes report the substrate-unavailable
98
- error naming the cause. A death with nothing in flight costs nothing — the
99
- next call starts a fresh transport, so a distro that comes back is served
100
- again. A death with a request in flight spends the resident's one permitted
101
- rebuild, and when that rebuild cannot complete — the distro is still down, or
102
- gone — the resident is permanently out: every later file operation fails the
103
- same way until the app restarts.
104
- - `watch()` observes from inside the distro, so callbacks are coarse
105
- invalidation, not per-file notifications. With inotify they arrive as events
106
- (coalesced to 0.5s); with the polling fallback they arrive on the poll
107
- cadence, not instantly. In the polling path a creation is seen even when the
108
- new file's own mtime is old: creating it bumps the containing directory's
109
- mtime, and the scan includes the watched directory itself (deletions are seen
110
- the same way). What escapes the polling stamp is a change that leaves every
111
- scanned mtime older than it — an in-place `cp -p` onto an existing file (new
112
- content, a copied-back mtime), or a `tar -x` whose archive restores the
113
- containing directory's own mtime. That blind spot does not apply to the
114
- inotify path. A FILE target is watched by its `mtime:size` fingerprint: a
115
- rename-over or an in-place write fires, a removal reports the target as gone,
116
- and a change that preserves both mtime and size escapes it. Watching a 9p
117
- share from the Windows side remains unsupported — the loop runs where the
118
- kernel can actually report change.
12
+ One substrate: the resident in-distro agent (ext4 for distro files). The retired
13
+ Windows-side share backend (`substrate: "share"`) is refused at boot; WSL-routed
14
+ operations never use the 9p share.
15
+
16
+ - **A new file publishes with mode 0600.** Overwrites and edits keep the original
17
+ bits; `chmod` when a new file must be executable.
18
+ - **When the agent is out, file operations fail closed** — `FS_IO_ERROR` naming the
19
+ substrate, with the recovery path. One crash rides a single rebuild; a spent
20
+ rebuild enters bounded revival (2/4/8 s); three deaths are permanent until the app
21
+ restarts. There is no fallback I/O — deliberately.
22
+ - **The mutation guard is not atomic compare-and-swap.** Creates use a no-replace
23
+ link; overwrites re-verify the version before the rename. A mismatch returns
24
+ `FS_STALE_VERSION`, but a change between check and rename can still be overwritten.
25
+ - **A cancelled write can have an unknown outcome** — inspect the file before
26
+ retrying.
27
+ - **A successful write is not crash-durable.** The staged file is synced, the
28
+ containing directory is not: a power loss can lose the entry.
29
+ - **A Linux filename containing a backslash cannot be addressed** — the backslash is
30
+ the UNC separator, so `a\b.ts` round-trips to `a/b.ts`. Rename away from backslash
31
+ names; this is the coordinate system's own limit.
32
+ - **Confined writes run on their own bwrap-confined resident** — kernel-enforced,
33
+ refusing outside the workspace with `FS_SANDBOX_DENIED`. Costs: `bubblewrap`
34
+ required; a policy change pays one agent startup; a write granted to `/tmp` lands
35
+ in the distro's real `/tmp`.
36
+ - **`glob`/`grep` run the distro's rg**; a distro without rg fails the search
37
+ (exit 127). Install: `wsl.exe -d <distro> -u root -- apt-get install -y bubblewrap
38
+ git ripgrep inotify-tools` (or the detected family's spelling). `--install` runs it
39
+ only for a missing `bubblewrap`.
40
+ - **`watch()` is coarse invalidation, not per-file notifications**: inotify events
41
+ coalesced to 0.5 s, or a `find -newer` poll loop otherwise. The polling path misses
42
+ an in-place `cp -p` or a `tar -x` that restores older mtimes; a FILE target watches
43
+ an `mtime:size` fingerprint. 9p watching from Windows stays unsupported.
44
+ - **With `sessionSandbox: false`, a confined command's `/tmp` differs from the write
45
+ tools' `/tmp`** — hand files over through the session workspace. The shipped
46
+ preset (`sessionSandbox: true`) shares one real `/tmp` and has no such split.
119
47
  - `editText` reads the whole file into memory before writing it back.
48
+ - A distro that dies with a request in flight spends the resident's one rebuild;
49
+ still down, the resident stays out until the app restarts.
120
50
 
121
51
  ## The GUI workspace-files service
122
52
 
123
- - **The GUI file tree and previews use the shipped `workspace-files` row.** The
124
- plugin deliberately does not replace it: its client half is the browser `file`
125
- resource provider the Sidebar previews need, and a server-only replacement
126
- drops it (the earlier `workspace-files-wsl` takeover did exactly that). The
127
- shipped service reads WSL paths through the routed root filesystem
128
- (`fs-routing`), so the file tree and previews still run inside the distro.
129
-
130
- - **That path costs extra agent round trips.** A listing, `stat` or page read
131
- goes through `ctx.fs` before reaching the resident agent: roughly four to six
132
- round trips instead of one, and a deep page read streams the preceding lines
133
- through the pipe before cutting. Expect the GUI file tree and previews to be
134
- slower than a direct agent call, especially on large files; the model's own
135
- file operations are unaffected — they use the preset filesystem and the
136
- resident agent directly. Measured 2026-10-19 with
137
- [`test/probe/gui-preview.sh`](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/test/probe/gui-preview.sh) (20 iterations on
138
- this machine): the routed backend's `stat` p95 2.7 ms, `listDir` p95 5.1 ms,
139
- and a 1 MiB `readByteRange` p95 **63.5 ms/MiB** — the service's own round
140
- trips sit on top of those, and the 1 s/MiB budget has a wide margin. No
141
- optimization is justified by these numbers; revisit only with a measured
142
- regression.
143
-
144
- - **`fs-routing` is the seam.** The shipped service depends on the routed root
145
- filesystem staying enabled and faithful to the `ctx.fs` contract (`resolve`,
146
- `lstat`, `stat`, `listDir`, `streamText`, `readBytes`, `watch`, `contains`,
147
- `processPath`). Disabling `fs-routing` restores the host 9p-backed root
148
- filesystem, and the GUI file service then crosses 9p for WSL workspaces. The
149
- shipped row's own caps (`maxBytes`, `maxFileBytes`, `maxLines`, `maxEntries`)
150
- can be overridden on the `workspace-files` row.
53
+ - The GUI file tree and previews use the shipped `workspace-files` row, reading WSL
54
+ paths through the routed root filesystem — deliberately not replaced (a server-only
55
+ takeover would drop the browser `file` provider the Sidebar previews need).
56
+ - **Expect extra round trips** (roughly 4-6 per listing/stat/page; measured p95:
57
+ `stat` 2.7 ms, `listDir` 5.1 ms, 1 MiB read 63.5 ms/MiB — 2026-10-19,
58
+ [probe](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/test/probe/gui-preview.sh)).
59
+ The model's own file operations are unaffected.
60
+ - `fs-routing` is the seam: disabling it restores the host 9p-backed root filesystem.
61
+ The shipped row's caps can be overridden on the `workspace-files` row.
151
62
 
152
63
  ## `@` completion
153
64
 
154
- - **`@` references still walk the 9p share.** The shipped
155
- `file-reference-local` service uses `node:fs/promises` from the Windows
156
- harness process, so a WSL workspace pays a 9p `readdir`/`lstat` per
157
- directory. On a 20-directory / 520-file tree the walk was **84.7 ms**
158
- median over 9p versus **1.5 ms** on ext4 (~56×;
159
- [`probe:file-reference`](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/test/probe/file-reference-9p.sh), 2026-10-19).
160
- [`src/file-reference-wsl.ts`](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/src/file-reference-wsl.ts) is the distro-side
161
- traversal (built and unit-tested) but is **not** in `cordis.patch.yml`: the
162
- funnels it would replace are module-internal. The fallback is the 9p walk
163
- until upstream exposes `FileReferenceTraversal`. Cost, seam and wire
164
- trigger: [UPSTREAM-FILE-REFERENCE-SEAM.md](UPSTREAM-FILE-REFERENCE-SEAM.md).
65
+ - **`@` references walk the 9p share** (~56× slower than ext4 on a probed tree,
66
+ 2026-10-19). The distro-side traversal is built but not mounted — it needs the
67
+ upstream `FileReferenceTraversal` seam:
68
+ [UPSTREAM-FILE-REFERENCE-SEAM.md](UPSTREAM-FILE-REFERENCE-SEAM.md).
165
69
 
166
70
  ## The sandbox
167
71
 
168
- - **The one-shot fallback has only the host deadline.** With `agent: false`, or
169
- when the resident is out of service, a command runs through a fresh
170
- `wsl.exe`. The host kills `wsl.exe` at the timeout, but WSL does not relay
171
- that kill into the distro, so the Linux-side process tree can outlive the
172
- call. The session runtime path — the shipped default — is cleaned by its
173
- scope. A systemd-scoped one-shot (with explicit environment pass-through) is
174
- not shipped; prefer `agent: true` and the session runtime for long commands.
175
- - **Batch file reads share one agent per distro.** Since A7 the shared read
176
- agent is the native binary, so 16 concurrent `stat`/`read`/`list` calls
177
- complete in 2.4-4.1 ms (A6 measured 342-614 ms on the shell). The first call
178
- pays the native preparation/cache cost (~70 ms once); `agentBackend: shell`
179
- forces the shell reads. v6's FS concurrency would only shave the native
180
- runtime's 7.7 ms/16 writes and is deferred.
181
- - **The native backend is the default; aarch64 is executed under qemu in CI.**
182
- The `native packages` job builds static-musl x86_64 and aarch64 binaries and
183
- runs the 18-check suite against both the freshly built aarch64 artifact and
184
- the committed `agent/native/linux-aarch64/dsh-wsl-agent` under qemu-user.
185
- Physical aarch64 WSL is out of scope (no Windows-on-ARM machine in reach);
186
- see [COMPATIBILITY.md](COMPATIBILITY.md#scope-decisions).
187
- `agentBackend: auto` still selects the packaged binary on an aarch64 distro;
188
- `native` refuses if preparation fails, and `shell` forces the shell agent.
189
- - **The session runtime needs systemd and a lingering user manager.** The
190
- shipped preset sets `sessionSandbox: true`; a distro without systemd as PID 1,
191
- a reachable `systemd --user`, `systemd-run`, or `Linger` is refused on the
192
- first command with the remedy. Set `sessionSandbox: false` on the preset's
193
- `wsl-shell` to keep the per-command shape. A **hard crash** of the app can
194
- leave a stale `dsh-wsl-*.scope` and its background processes behind; a
195
- graceful session end stops the scope (validated). The session preflight
196
- sweeps stale scopes when the next WSL session opens: it stops a
197
- `dsh-wsl-*.scope` only when its cgroup has no agent process (`wsl-agent.sh`
198
- or the native `dsh-wsl-agent`), so another live session's runtime is never
199
- touched.
200
- - **The sandbox does not govern WSL interop.** A confined command can still run a
201
- Windows program, which escapes the Linux-side boundary. The plugin reports this as
202
- `enforcement: partial`. See [ARCHITECTURE.md](ARCHITECTURE.md#reported-enforcement).
203
- The `maskWindowsDrive` key on `wsl-shell`, `wsl-fs` and `fs-routing` narrows the
204
- hole: the confined profile shadows `/mnt` with an empty tmpfs, so files and
205
- executables at paths under `/mnt` are hidden. Other mounts, alternate access
206
- paths and unrestricted file reads are not covered by this mask. It is not a
207
- closure — a command that can write the workspace can write an executable there and
208
- run it (binfmt interop dispatches on file content, not location) — so enforcement
209
- stays `partial`, the result's sandbox fact reports `windowsDrive: "masked"`, and
210
- distro-level disabling of WSL interop (`[interop] enabled=false` in
211
- `wsl.conf`) must be configured separately. On `wsl-shell`, an explicitly set
212
- `interopVisibility` takes precedence over `maskWindowsDrive`.
213
- - **`bubblewrap` must be installed**, or both providers fail immediately. The
214
- usability probe re-runs after a failure, so installing bubblewrap while the app
215
- runs is believed on the next command; only a success is cached. The refusal
216
- classifies the probe's failure — missing versus present-but-failing — and the
217
- direct install command follows the distro's package family; a probe failure
218
- with no recognisable shape falls back to the install remedy with the raw
219
- output quoted. A session-start preflight (`wsl-preflight`) surfaces the same
220
- text as a warning; it is advisory and never installs.
221
- - `workspace-write` binds the workspace root as writable, and bubblewrap refuses a
222
- bind whose source directory does not exist, so a session whose workspace directory was
223
- deleted fails with a runner error; the plugin does not recreate it. A workdir that
224
- does not exist — a configured `cwd` pointing at a deleted directory, for instance — is
225
- a separate case and is now reported as an error naming that directory, because
226
- `wsl.exe` would otherwise run the command in `/` and exit 0.
72
+ - **The one-shot fallback has only the host deadline** — a killed `wsl.exe` does not
73
+ relay the kill into the distro, so the Linux process tree can outlive the call.
74
+ Prefer `agent: true` and the session runtime for long commands.
75
+ - **Batch reads share one native agent per distro** (16 concurrent calls in
76
+ 2.4-4.1 ms; first call pays ~70 ms preparation).
77
+ - **aarch64 runs under qemu in CI only** (no Windows-on-ARM machine in reach —
78
+ [COMPATIBILITY.md](COMPATIBILITY.md#scope-decisions)). `auto` still selects the
79
+ packaged binary; `native` refuses on preparation failure; `shell` forces the shell
80
+ agent.
81
+ - **The session runtime needs systemd and a lingering user manager** — refused on the
82
+ first command with the remedy otherwise; `sessionSandbox: false` keeps the old
83
+ per-command shape. A hard app crash can leave a stale `dsh-wsl-*.scope`; the
84
+ preflight sweeps stale scopes on the next WSL session (never another live
85
+ session's).
86
+ - **The sandbox does not govern WSL interop** — reported as `enforcement: partial`.
87
+ `maskWindowsDrive` narrows `/mnt` but is not a closure; distro-level
88
+ `[interop] enabled=false` in `wsl.conf` must be configured separately.
89
+ [ARCHITECTURE.md](ARCHITECTURE.md#reported-enforcement).
90
+ - **`bubblewrap` must be installed**, or both providers fail with the install
91
+ command; a session-start preflight surfaces the same text as an advisory warning.
92
+ - A workspace directory deleted mid-session fails with a runner error; the plugin
93
+ does not recreate it. A configured `cwd` that does not exist is reported as an
94
+ error naming the directory.
227
95
 
228
96
  ## Shell startup semantics (`wsl-shell`)
229
97
 
230
- Commands run as `<shell> -lc <command>` — a login, **non-interactive** shell.
231
- The login flag is deliberate: it sources the profile scripts, which is what
232
- makes a version manager's PATH (nvm, cargo, pyenv) visible to model-run
233
- commands. Non-interactivity is equally deliberate: interactive startup files
234
- print prompts and plugin output into the streams the model reads, define
235
- aliases that shadow real executables, and can keep the shell alive after the
236
- command finishes. What follows from that split:
98
+ Commands run as `<shell> -lc <command>` — login (profile scripts sourced, so version
99
+ managers work) and **non-interactive** (no prompt/plugin output in the model's
100
+ streams).
237
101
 
238
- - **`~/.zshrc` and `~/.bashrc` are never read.** A login shell sources
239
- `/etc/profile` and the user's profile file — `~/.profile` or `~/.zprofile`
240
- for bash and zsh — and nothing else. zsh's ecosystem convention of putting
241
- every hook in `.zshrc` therefore leaves model-run commands without it: a
242
- tool installed through an interactive-only hook exists in the user's
243
- terminal and does not exist in the model's. Move machine-level PATH setup
244
- (`eval "$(fnm env)"`, nvm, cargo) into `~/.zprofile` or `~/.zshenv`, which
245
- login shells do read; keep prompt and completion plugins in `.zshrc`.
246
- - **The confined sandbox mounts `/run` read-only, so fnm's multishell PATH
247
- cannot form there.** `fnm env` works by creating a per-shell symlink
248
- directory under `/run/user/<uid>/fnm_multishells/`; inside `bwrap` that
249
- creation fails, and with it the PATH entry `fnm env` exists to provide.
250
- A profile that must work in both worlds should fall back to fnm's stable
251
- alias directory when the multishell link did not materialize — see the
252
- recipe in [Configuration](CONFIGURATION.md#zshfnm-recipe-for-model-run-commands).
253
- The plugin does not relax the sandbox for this: a writable `/run` inside the
254
- fence would be a real weakening of it, traded for one tool's convenience.
102
+ - **`~/.zshrc` / `~/.bashrc` are never read.** Move machine-level PATH setup
103
+ (`fnm`, nvm, cargo) into `~/.zprofile` or `~/.zshenv`; keep prompts and completion
104
+ in the rc file.
105
+ - **fnm's multishell PATH cannot form inside the sandbox** (`/run` is read-only) —
106
+ use the fallback recipe in
107
+ [Configuration](CONFIGURATION.md#zshfnm-recipe-for-model-run-commands).
255
108
 
256
109
  ## Agent diagnostics
257
110
 
258
- - **The doctor's `agent.handshake` check probes a fresh resident, not the live
259
- one.** It starts `agent/wsl-agent.sh` in the distro, reads its HELLO (pid,
260
- protocol, script digest) and stops it, so it proves that the agent path can
261
- start and that the file the distro reads is the shipped one. It cannot read
262
- the running app resident's last error — there is no IPC channel from a plugin
263
- script to the app process — so that error stays in the app log. In-process,
264
- `WslAgent.status()` exposes state, pid, lastError, deaths and pending.
265
- - **`dsh-wsl-*.scope` counts are point-in-time.** The doctor lists and inspects
266
- the user scopes once; a scope created or stopped a moment later is not
267
- reflected. `--fix` runs the same sweep the session preflight uses.
268
-
269
- ## The doctor's sessions table
270
-
271
- - **The sessions table shows distro-side facts only.** The cross-distro view
272
- (`distro → scope → state → resident pids`) attests what the distro can see:
273
- which scopes exist and which pids inside them are agent processes. A
274
- resident's idle countdown and its `lastError` live in the host process that
275
- owns it — there is no IPC channel from a plugin script to the app, so the
276
- table cannot show them (`WSL-313`'s upstream ask). The agent.handshake
277
- check notes a live resident's presence when the scope survey sees one, and
278
- says that its own probe starts a fresh one.
111
+ - **The doctor's `agent.handshake` probes a fresh resident**, not the live one —
112
+ a running resident's `lastError` stays in the app log (`WslAgent.status()` exposes
113
+ it in-process).
114
+ - **`dsh-wsl-*.scope` counts are point-in-time**; `--fix` runs the preflight's sweep.
115
+ - **The doctor's `git.win-safe` probe reports the Windows git trust through interop
116
+ `git.exe`** — the `safe.directory` whitelist that WSL change review needs when
117
+ Windows git is installed (see Terminals below).
279
118
 
280
119
  ## Session-start advisories
281
120
 
282
- - **There is no session-UI notice seam in DSH 0.2.1.** When a distro session
283
- opens, `wsl-preflight` checks `bubblewrap`, systemd as PID 1, the user
284
- manager, `systemd-run` and Linger. A failure is advisory: it never installs
285
- anything, never cancels the session and never changes the fail-closed
286
- behavior of the first command. The finding goes to the host log
287
- (`logger.warn`) and into the session's runtime context as a `wsl-preflight`
288
- entry — model-visible on the next assembly — but the GUI today has no
289
- toast/banner service a plugin can publish to. `pnpm run doctor` is the
290
- deterministic human-facing check; the exact upstream ask is
121
+ - **There is no session-UI notice seam in DSH 0.2.1.** `wsl-preflight` failures go
122
+ to the host log and the session's runtime context (model-visible next assembly) —
123
+ advisory only, never installing or cancelling. The upstream ask:
291
124
  [UPSTREAM-SESSION-NOTICE.md](UPSTREAM-SESSION-NOTICE.md).
292
125
  - **The runtime-context entry is a repeated snapshot, not a dismissible
293
- notification.** It appears in every assembly until the environment is fixed;
294
- it has no severity decoration and no dismiss control. Removing the
295
- `wsl-preflight` row (or setting `sandbox: false` on it) silences the probe
296
- entirely.
126
+ notification.** Removing the `wsl-preflight` row silences the probe entirely.
297
127
 
298
128
  ## Ports
299
129
 
300
- - **The port poller keeps the resident agent permanently warm.** The
301
- `DSH_WSL_PORTS` snapshot refreshes through the shared resident agent every
302
- `portsRefreshMs` (10 s by default, well inside the agent's idle timeout), so
303
- while a distro's session is active the agent never idles out: one `wsl.exe`
304
- and one in-distro agent process stay resident per active distro even with no
305
- command in flight. The poller starts only for a distro a session actually
306
- uses; raising `portsRefreshMs` past the agent's idle timeout restores idle
307
- shutdown, at the cost of staler port facts.
130
+ - **The port poller keeps the resident agent permanently warm** per active distro
131
+ (refresh 10 s). Raising `portsRefreshMs` past the agent's idle timeout restores
132
+ idle shutdown, at the cost of staler port facts.
308
133
 
309
134
  ## The directory picker
310
135
 
311
- - The directory picker lists distro levels from inside the distro — the
312
- resident's `find -L <dir> -maxdepth 1 -mindepth 1 -type d -print0` in one
313
- round trip, a one-shot `wsl.exe --exec find` when it is out, never the 9p
314
- walk the host used to pay per entry. Entries arrive NUL-separated, so a
315
- directory name containing a newline lists as one exact row (WSL-306 retired
316
- the `ls -1ALp` line parse that used to split such a name into two rows or
317
- drop it).
136
+ - Distro levels list from inside the distro in one round trip (NUL-separated, so
137
+ newline-bearing directory names list exactly — WSL-306); a one-shot `wsl.exe --exec
138
+ find` when the resident is out.
318
139
 
319
140
  ## Terminals
320
141
 
321
- - Terminal routing is per-session by workspace coordinates (`subprocess-wsl.hostSessions`,
322
- default on): a WSL-folder session gets the distro shell, a Windows-folder session gets
323
- `powershell.exe` in its own directory. The shell MENU's default entry remains the
324
- configured profile, and a directory-less launch still lands in the distro.
325
- - **The Windows-side git must trust the `\\wsl.localhost` share** (`safe.directory`),
326
- or the change-review pipeline silently degrades: the discovery call answers
327
- `fatal: detected dubious ownership`, the recorder falls back to
328
- "git is unavailable", and changed-files cards never list a WSL workspace's
329
- diffs. Remedy (one-time, Windows side):
330
- `git config --global --add safe.directory "//wsl.localhost/*"`, then restart
331
- the app (the resolution is memoized for the process lifetime). Host-side
332
- (`C:\...`) workspaces are unaffected. A future doctor check could probe this
333
- directly. The prerequisite is also OPTIONAL: on a machine without Windows
334
- git the plugin resolves git to a distro sentinel and routes the discovery
335
- call too, translating its output into UNC form — full WSL change review
336
- with no Windows git and no whitelist.
337
- - A NEW tab's title still reads `WSL`, the composition's shell profile name, whatever
338
- it runs — including a host session's PowerShell. The controller titles a tab from
339
- that profile before the provider rewrites the launch, and a plugin cannot correct
340
- the default: a patch layer asserts rather than renames a row's module, and the
341
- row's id is load-bearing for the web composition (disabling it fails web boot with
342
- "waiting for service: webTerminals"). The full account and the proposed upstream
343
- change are in [UPSTREAM-TERMINAL-TITLE.md](UPSTREAM-TERMINAL-TITLE.md).
344
- The plugin now ships host `shellCandidates` (`wsl.exe` dedupes into the default's
345
- kind; `powershell`/`cmd` appear as themselves), and a manually selected shell is
346
- titled after itself — pick PowerShell from the shell menu and the tab reads
347
- `powershell`. Double-clicking a title to rename also works.
348
- - Distro terminals ARE reclaimed when idle — the observation is distro-side:
349
- each terminal is launched with a `DSH_TERMINAL_ID` marker, and the handle's
350
- `inspectActivity` counts the marked processes through the resident agent
351
- (`lib/terminal-activity.js`). Exactly one (the shell at its prompt) is
352
- `idle`; more than one (a command, a pipeline, a nested shell) is `busy`;
353
- `unknown` — the agent out, the distro stopped — pauses reclamation, because
354
- a probe that cannot answer must not authorize a close. Consequences to
355
- know: a nested shell keeps the terminal busy by design (closing it would
356
- lose that shell), so does any process the shell's startup files spawn with
357
- the inherited marker (an `ssh-agent` launched in `.bashrc` reads as busy);
358
- the idle threshold is the controller's `unattendedTimeoutMs` (2 h by
359
- default); and the host-side shell-activity integration is still not armed
360
- for these launches — `wsl.exe` on win32 fails its gate — which is why the
361
- observation is a scan rather than shell cooperation. `terminalIdleReclaim:
362
- false` on the `subprocess-wsl` row restores close-by-hand: the launch then
363
- carries no `DSH_TERMINAL_ID` marker, and the handle keeps the shipped
364
- `unknown` activity, which never accumulates idle.
365
- - Windows-folder host terminals (the `powershell.exe` branch) keep the shipped
366
- behaviour: `unknown` activity, no reclamation.
142
+ - Terminal routing is per-session by workspace coordinates: a WSL-folder session gets
143
+ the distro shell, a Windows-folder session gets `powershell.exe` in its own
144
+ directory. The shell MENU's default entry stays the configured profile; a
145
+ directory-less launch lands in the distro.
146
+ - **Windows git must trust the share** (`safe.directory`), or WSL workspaces'
147
+ changed-files cards silently disappear (the discovery call answers
148
+ `detected dubious ownership`). One-time remedy on Windows:
149
+ `git config --global --add safe.directory "//wsl.localhost/*"`, then restart the
150
+ app. Two alternatives: the doctor's `git.win-safe` probe reports the state, and a
151
+ machine **without** Windows git needs nothing — the provider routes discovery into
152
+ the distro and translates the output into UNC form.
153
+ - **A NEW tab's title reads `WSL`** whatever it runs — the controller titles from the
154
+ composition's profile before the provider rewrites the launch, and a plugin cannot
155
+ correct the default ([UPSTREAM-TERMINAL-TITLE.md](UPSTREAM-TERMINAL-TITLE.md)).
156
+ The menu ships host candidates (`powershell`, `cmd`): a manually selected shell is
157
+ titled after itself; double-click rename also works.
158
+ - **Distro terminals ARE reclaimed when idle** — the handle counts the marked
159
+ processes through the resident agent (exactly one at its prompt = `idle`; a nested
160
+ shell or an `ssh-agent` from startup files reads `busy` by design). Threshold: the
161
+ controller's `unattendedTimeoutMs` (2 h). `terminalIdleReclaim: false` restores
162
+ close-by-hand. Windows-folder host terminals keep the shipped no-reclamation
163
+ behaviour.
367
164
 
368
165
  ## Host "Open in app"
369
166
 
370
- - **Opening VS Code (or another Electron editor) from the host "Open in app"
371
- menu can fail independently of this plugin.** The DSH Host process runs with
372
- `ELECTRON_RUN_AS_NODE=1`. The open-in-app launcher inherits that variable, so
373
- `Code.exe` starts as Node, exits 1, and the UI reports launch-failed. File
374
- Explorer and other non-Electron launchers are unaffected; the failure is the
375
- same on a Windows folder and on a WSL UNC workspace. This repository does
376
- not own that spawn path. The record and the upstream fix are in
167
+ - **Opening VS Code from "Open in app" can fail independently of this plugin** — the
168
+ Host runs with `ELECTRON_RUN_AS_NODE=1`, which the launcher inherits, so
169
+ `Code.exe` starts as Node and exits 1. Not owned by this repository:
377
170
  [UPSTREAM-OPEN-IN-APP.md](UPSTREAM-OPEN-IN-APP.md).
@@ -0,0 +1,161 @@
1
+ # Limitations
2
+
3
+ _本文是 [LIMITATIONS](LIMITATIONS.md) 的中文译本,以英文版为准。_
4
+
5
+ 插件不做什么,以及为什么。写进文档的限制是设计边界;
6
+ 没写进文档的迟早会变成 bug 报告。每一条都说明限制本身和补救方法;
7
+ 推理过程在 [ARCHITECTURE.md](ARCHITECTURE.md) 及其链接的文档中。
8
+
9
+ ## 文件系统 substrate(`wsl-fs`)
10
+
11
+ 只有一个 substrate:发行版内的常驻 agent(发行版文件用 ext4)。已退役的
12
+ Windows 侧 share 后端(`substrate: "share"`)在启动时被拒绝;WSL 路由的
13
+ 操作永远不会走 9p 共享。
14
+
15
+ - **新建文件以 0600 权限发布。** 覆盖与编辑保留原有权限位;
16
+ 新文件需要可执行时请 `chmod`。
17
+ - **agent 不在时,文件操作关闭(fail closed)** —— 报 `FS_IO_ERROR`,指明
18
+ substrate,并附带恢复路径。一次崩溃可用一次重建;重建耗尽后进入有界复活
19
+ (2/4/8 s);三次死亡则永久退出,直到应用重启。没有回退 I/O——这是有意为之。
20
+ - **变更守卫不是原子的比较并交换。** 创建使用不可替换的 link;覆盖在 rename 前
21
+ 会重新校验版本。不匹配时返回 `FS_STALE_VERSION`,但检查与 rename 之间发生的
22
+ 变更仍可能被覆盖。
23
+ - **被取消的写入结果可能是未知的** —— 重试前先检查文件。
24
+ - **写入成功不等于崩溃持久。** 被暂存的文件会 sync,但所在目录不会:
25
+ 断电可能丢失该目录项。
26
+ - **含反斜杠的 Linux 文件名无法被寻址** —— 反斜杠是 UNC 分隔符,
27
+ `a\b.ts` 会往返成 `a/b.ts`。请把文件名改掉,不要用反斜杠;
28
+ 这是坐标系自身的限制。
29
+ - **受限写入运行在各自的 bwrap 约束常驻 agent 上** —— 由内核强制,
30
+ 工作区之外以 `FS_SANDBOX_DENIED` 拒绝。代价:需要 `bubblewrap`;
31
+ 策略变更要付出一次 agent 启动;授予 `/tmp` 的写入落在发行版真实的
32
+ `/tmp` 中。
33
+ - **`glob`/`grep` 使用发行版的 rg**;没有 rg 的发行版搜索失败
34
+ (退出码 127)。安装:`wsl.exe -d <distro> -u root -- apt-get install -y bubblewrap
35
+ git ripgrep inotify-tools`(或检测到的包管理族对应的写法)。`--install` 只在
36
+ 缺少 `bubblewrap` 时才执行安装。
37
+ - **`watch()` 是粗粒度失效,不是逐文件通知**:inotify 事件合并到 0.5 s,
38
+ 否则使用 `find -newer` 轮询循环。轮询路径会遗漏原地的 `cp -p`,以及恢复
39
+ 较旧 mtime 的 `tar -x`;FILE 目标监视的是 `mtime:size` 指纹。从 Windows
40
+ 侧做 9p 监视仍然不支持。
41
+ - **`sessionSandbox: false` 时,受限命令的 `/tmp` 与写入工具的 `/tmp`
42
+ 不同** —— 请通过会话工作区交接文件。随包 preset(`sessionSandbox: true`)
43
+ 共享同一个真实 `/tmp`,不存在这种分裂。
44
+ - `editText` 在写回之前把整个文件读进内存。
45
+ - 发行版在请求进行中死亡,会耗尽常驻 agent 的唯一一次重建;
46
+ 仍未恢复时,常驻 agent 将保持不可用,直到应用重启。
47
+
48
+ ## GUI 的 workspace-files 服务
49
+
50
+ - GUI 文件树和预览使用随包的 `workspace-files` 行,通过被路由的根文件系统
51
+ 读取 WSL 路径——这是有意不替换的(服务端单方面接管会丢掉侧边栏预览
52
+ 所依赖的浏览器 `file` 资源提供者)。
53
+ - **预期多出几轮往返**(每次 listDir/stat/分页约 4-6 次;实测 p95:
54
+ `stat` 2.7 ms、`listDir` 5.1 ms、1 MiB 读取 63.5 ms/MiB —— 2026-10-19,
55
+ [探针](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/test/probe/gui-preview.sh))。
56
+ 模型自己的文件操作不受影响。
57
+ - `fs-routing` 是接缝:禁用它即可恢复宿主 9p 支撑的根文件系统。
58
+ 随包行的上限可以在 `workspace-files` 行上覆盖。
59
+
60
+ ## `@` 补全
61
+
62
+ - **`@` 引用走 9p 共享**(在已探针的目录树上比 ext4 慢约 56 倍,
63
+ 2026-10-19)。发行版侧的遍历已经写好,但尚未挂载——它需要上游的
64
+ `FileReferenceTraversal` 接缝:
65
+ [UPSTREAM-FILE-REFERENCE-SEAM.md](UPSTREAM-FILE-REFERENCE-SEAM.md)。
66
+
67
+ ## 沙箱
68
+
69
+ - **一次性回退路径只有宿主截止时间** —— `wsl.exe` 被杀时不会把 kill 传进
70
+ 发行版,因此 Linux 进程树可能比调用活得更久。长命令建议
71
+ `agent: true` 并使用会话 runtime。
72
+ - **批量读取按发行版共享一个原生 agent**(16 个并发调用 2.4-4.1 ms;
73
+ 第一次调用要付出约 70 ms 的准备)。
74
+ - **aarch64 只在 CI 中于 qemu 下运行**(身边没有 Windows-on-ARM 机器——
75
+ [COMPATIBILITY.md](COMPATIBILITY.md#scope-decisions))。`auto` 仍会选择
76
+ 包内二进制;`native` 在准备失败时拒绝;`shell` 强制使用 shell agent。
77
+ - **会话 runtime 需要 systemd 和带 linger 的 user manager** —— 否则在
78
+ 第一条命令时被拒绝并给出补救方法;`sessionSandbox: false` 保留旧的
79
+ 按命令形态。应用硬崩溃可能留下过期的 `dsh-wsl-*.scope`;预检会在下一个
80
+ WSL 会话时清扫过期 scope(从不清扫其他活跃会话的)。
81
+ - **沙箱不管辖 WSL interop** —— 上报为 `enforcement: partial`。
82
+ `maskWindowsDrive` 收窄 `/mnt` 但不是关闭;发行版级 `wsl.conf` 中的
83
+ `[interop] enabled=false` 需要另行配置。
84
+ [ARCHITECTURE.md](ARCHITECTURE.md#reported-enforcement)。
85
+ - **必须安装 `bubblewrap`**,否则两个提供者都会带着安装命令失败;
86
+ 会话启动预检会以建议性警告的形式给出同样的文本。
87
+ - 会话中途被删除的工作区目录会以 runner 错误失败;插件不会重建它。
88
+ 配置的 `cwd` 不存在时,报错会指明该目录。
89
+
90
+ ## shell 启动语义(`wsl-shell`)
91
+
92
+ 命令以 `<shell> -lc <command>` 运行——登录 shell(会 source profile 脚本,
93
+ 因此版本管理器可用)且**非交互**(模型的输出流中没有提示符/插件输出)。
94
+
95
+ - **`~/.zshrc` / `~/.bashrc` 永远不会被读取。** 把机器级的 PATH 设置
96
+ (`fnm`、nvm、cargo)移进 `~/.zprofile` 或 `~/.zshenv`;提示符和补全
97
+ 留在 rc 文件里。
98
+ - **fnm 的 multishell PATH 在沙箱内无法形成**(`/run` 只读)——
99
+ 使用 [Configuration](CONFIGURATION.md#zshfnm-recipe-for-model-run-commands)
100
+ 中的回退配方。
101
+
102
+ ## Agent 诊断
103
+
104
+ - **doctor 的 `agent.handshake` 探测的是一个新起的常驻 agent**,而不是
105
+ 在跑的那个——运行中常驻 agent 的 `lastError` 留在应用日志里
106
+ (进程内可通过 `WslAgent.status()` 查看)。
107
+ - **`dsh-wsl-*.scope` 计数是某一时刻的快照**;`--fix` 会执行预检的清扫。
108
+ - **doctor 的 `git.win-safe` 探针通过 interop 的 `git.exe` 报告 Windows git
109
+ 的信任状态** —— 装了 Windows git 时,WSL 变更审查需要那份
110
+ `safe.directory` 白名单(见下文"终端")。
111
+
112
+ ## 会话启动建议
113
+
114
+ - **DSH 0.2.1 没有会话 UI 通知接缝。** `wsl-preflight` 的失败进入宿主日志
115
+ 和会话的 runtime context(下次组装时对模型可见)——仅供参考,从不安装、
116
+ 从不取消。对上游的请求见:
117
+ [UPSTREAM-SESSION-NOTICE.md](UPSTREAM-SESSION-NOTICE.md)。
118
+ - **runtime context 条目是重复的快照,不是可关闭的通知。**
119
+ 移除 `wsl-preflight` 行即可让该探针完全静音。
120
+
121
+ ## 端口
122
+
123
+ - **端口轮询让常驻 agent 在每个活跃发行版上永久保温**
124
+ (10 s 刷新)。把 `portsRefreshMs` 调到超过 agent 的空闲超时即可恢复
125
+ 空闲关停,代价是端口事实更陈旧。
126
+
127
+ ## 目录选择器
128
+
129
+ - 发行版层级从发行版内部一次往返完成列举(NUL 分隔,因此含换行的
130
+ 目录名也能精确列出——WSL-306);常驻 agent 不在时用一次性的
131
+ `wsl.exe --exec find`。
132
+
133
+ ## 终端
134
+
135
+ - 终端路由按会话的工作区坐标进行:WSL 文件夹会话获得发行版 shell,
136
+ Windows 文件夹会话在其自身目录获得 `powershell.exe`。shell 菜单的
137
+ 默认条目保持为配置的 profile;不带目录的启动落在发行版内。
138
+ - **Windows git 必须信任该共享**(`safe.directory`),否则 WSL 工作区的
139
+ changed-files 卡片会无声消失(发现调用返回 `detected dubious ownership`)。
140
+ Windows 上的一次性补救:
141
+ `git config --global --add safe.directory "//wsl.localhost/*"`,然后重启
142
+ 应用。两个替代方案:doctor 的 `git.win-safe` 探针可报告该状态;而
143
+ **没有** Windows git 的机器什么都不用做——提供者把发现请求路由进
144
+ 发行版,并把输出转写成 UNC 形式。
145
+ - **NEW 标签页的标题显示 `WSL`**,无论它实际运行什么——控制器在提供者
146
+ 改写启动命令之前就按 composition 的 profile 命名了,而插件无法纠正
147
+ 这个默认值([UPSTREAM-TERMINAL-TITLE.md](UPSTREAM-TERMINAL-TITLE.md))。
148
+ 菜单附带宿主候选(`powershell`、`cmd`):手动选择的 shell 会以自身命名;
149
+ 双击重命名也可行。
150
+ - **发行版终端空闲时确实会被回收** —— 通过常驻 agent 对被标记的进程计数
151
+ (提示符处恰好一个进程 = `idle`;嵌套 shell 或启动文件拉起的
152
+ `ssh-agent` 按设计读作 `busy`)。阈值是控制器的
153
+ `unattendedTimeoutMs`(2 h)。`terminalIdleReclaim: false` 恢复手动关闭。
154
+ Windows 文件夹的宿主终端保持随包的不回收行为。
155
+
156
+ ## 宿主的"在应用中打开"
157
+
158
+ - **从"在应用中打开"启动 VS Code 可能独立于本插件而失败** ——
159
+ 宿主带着 `ELECTRON_RUN_AS_NODE=1` 运行,启动器会继承它,于是
160
+ `Code.exe` 以 Node 身份启动并以退出码 1 结束。不属于本仓库管:
161
+ [UPSTREAM-OPEN-IN-APP.md](UPSTREAM-OPEN-IN-APP.md)。
@@ -1,5 +1,7 @@
1
1
  # Quickstart
2
2
 
3
+ *A Chinese translation lives at [QUICKSTART.zh](QUICKSTART.zh.md); the English version is authoritative.*
4
+
3
5
  Steps from a Windows + WSL 2 machine to a working WSL session.
4
6
  This page is the entry point; every claim links to the document that owns it.
5
7
 
@@ -0,0 +1,168 @@
1
+ # Quickstart
2
+
3
+ _本文是 [QUICKSTART](QUICKSTART.md) 的中文译本,以英文版为准。_
4
+
5
+ 从一台 Windows + WSL 2 机器到可用的 WSL 会话,只需以下步骤。
6
+ 本页是入口;每条说明都链接到负责该主题的文档。
7
+
8
+ ## 1. 前置条件
9
+
10
+ | 需求 | 如何检查 | 缺失时 |
11
+ |---|---|---|
12
+ | Windows 10/11,启用 WSL 2 | `wsl.exe -l -v` 显示 `VERSION 2` | `wsl.exe --install` |
13
+ | 一个发行版 | 上面列表中有一行 | `wsl.exe --install -d Ubuntu` |
14
+ | `bubblewrap` | `wsl.exe -d <distro> -- which bwrap` | `wsl.exe -d <distro> -u root -- apt-get install -y bubblewrap` |
15
+ | systemd + Linger(会话 runtime 需要) | `node <plugin>/scripts/doctor.mjs`(已安装)或 `pnpm run doctor`(检出目录) | 在 `/etc/wsl.conf` 设置 `[boot] systemd=true`,`wsl --shutdown`,然后 `loginctl enable-linger "$USER"`;或设置 `sessionSandbox: auto`/`false` |
16
+ | Node `^22.19.0` 或 `>=24.0.0` | 仅源码/开发工作需要 | 使用已发布的插件时,发行版内不需要 Node |
17
+
18
+ doctor(`scripts/doctor.mjs`,或在检出目录中运行 `pnpm run doctor`)会检查
19
+ 环境并针对检测到的失败打印补救方法。发行版的 WSL 版本请单独用
20
+ `wsl.exe -l -v` 确认。doctor 默认只读,除非传入 `--fix`(清理过期 scope)或
21
+ `--fix --install`(安装软件包,需确认)。
22
+
23
+ ## 2. 安装
24
+
25
+ 在一个可以使用 DSH CLI 的 Windows PowerShell 中:
26
+
27
+ ```powershell
28
+ dsh plugin --profile <profile> add dsh-plugin-wsl-env
29
+ # 若解析到旧版本(发布当天的元数据缓存),显式指定:
30
+ # dsh plugin --profile <profile> add dsh-plugin-wsl-env@<latest>
31
+ dsh --profile <profile> --dump-config # 输出中必须包含插件的行
32
+ ```
33
+
34
+ 如果 profile 已覆盖 preset,请保持覆盖为完整的
35
+ `preset-wsl` 块——当某个层直接按 id 指向 `wsl-shell`/`wsl-fs` 时,
36
+ `pnpm run preset --check <patch>` 会明确报错。
37
+
38
+ ## 3. 打开 WSL 工作区
39
+
40
+ 在 GUI 中,通过选择器打开文件夹(`\\wsl.localhost\<distro>\home\<you>\proj`),
41
+ 或直接打开 UNC 路径。会话的命令、文件工具、终端和预览
42
+ 都在该发行版中运行;打开 Windows 文件夹则保留正常的 Windows 环境。
43
+ 两个发行版可以同时打开,各自拥有自己的环境。
44
+
45
+ ### 在 VS Code 中打开工作区(Remote-WSL)
46
+
47
+ 把 WSL 文件夹当作 Windows UNC 共享打开也可行,但属于降级模式:
48
+ 文件走 9p,终端是 PowerShell,扩展运行在 Windows 上。完整的
49
+ 集成方式是 VS Code 的 Remote-WSL——server 运行在发行版内,集成终端
50
+ 是发行版的 shell,文件操作直接落在本地 ext4 上:
51
+
52
+ ```powershell
53
+ # from the plugin checkout:
54
+ scripts/open-wsl-vscode.ps1 -Path "\\wsl.localhost\ubuntu\home\me\proj"
55
+ # or by hand:
56
+ code --folder-uri "vscode-remote://wsl+ubuntu/home/me/proj"
57
+ ```
58
+
59
+ 要求 PATH 上有 `code` shim(VS Code 安装器会添加),以及
60
+ Remote-WSL 扩展。
61
+
62
+ ![The directory picker listing a WSL distro's folders](screenshots/picker.png)
63
+
64
+ 终端面板运行在同一个发行版中——`uname -a` 返回的是
65
+ 发行版的内核,而不是 Windows 的:
66
+
67
+ ![The GUI terminal running inside the distro](screenshots/terminal.png)
68
+
69
+ 侧边栏的预览也能就地渲染 WSL 文件:
70
+
71
+ ![The GUI preview rendering a WSL file](screenshots/preview.png)
72
+
73
+ ## 4. 验证
74
+
75
+ 让模型执行一条命令,例如 `pwd && uname -a`。预期结果:Linux 路径和
76
+ 发行版内核。如果第一条命令失败,错误信息会给出补救方法;
77
+ [doctor](CONFIGURATION.md) 和[故障排查表](../README.md#troubleshooting)
78
+ 覆盖了常见情况。
79
+
80
+ 下面的探针会实时运行随包的、由内核强制的 bwrap profile——
81
+ 工作区内的写入生效,测试性的区外写入被以 `EROFS` 拒绝,
82
+ 严格 profile 则把 `/mnt` 完全隐藏
83
+ ([`test/probe/sandbox.sh`](https://github.com/Big-Dao/dsh-plugin-wsl-env/blob/main/test/probe/sandbox.sh)):
84
+
85
+ 这是部分隔离,不是 Windows 安全边界:Windows interop
86
+ 仍可启动 `/mnt` 之外的可执行文件。见 [LIMITATIONS.md](LIMITATIONS.md)。
87
+
88
+ ![The sandbox probe: every expectation holds, the kernel refuses what the profile does not grant](screenshots/sandbox.png)
89
+
90
+ ## 配方
91
+
92
+ 命令在模型的会话中运行,位于发行版内,从工作区根目录出发。
93
+
94
+ ### Node / TypeScript
95
+
96
+ ```bash
97
+ node -v && pnpm -v
98
+ pnpm install
99
+ pnpm test
100
+ pnpm dev # a dev server survives the command that started it with sessionSandbox: true
101
+ ```
102
+
103
+ 快照成功后(默认每 10 s 刷新一次),server 的端口可能出现在 `DSH_WSL_PORTS`
104
+ 中;从 Windows 通过 `http://localhost:<port>` 访问转发的端口。
105
+
106
+ ### Python
107
+
108
+ ```bash
109
+ python3 -m venv .venv && . .venv/bin/activate
110
+ pip install -r requirements.txt
111
+ python -m pytest
112
+ ```
113
+
114
+ 使用发行版的解释器,而不是 Windows 的:会话的 `python3` 是
115
+ 发行版 `PATH` 上的那个(包括 pyenv/conda shim)。
116
+
117
+ ### Rust
118
+
119
+ ```bash
120
+ cargo build --release
121
+ cargo test
122
+ ```
123
+
124
+ target 会针对发行版的架构(x86_64 或 aarch64)原生编译;
125
+ 不要复用 Windows 的 `target/` 目录。
126
+
127
+ ### Go
128
+
129
+ ```bash
130
+ go build ./...
131
+ go test ./...
132
+ ```
133
+
134
+ 模块缓存通常位于发行版用户的 `$GOPATH` 下;其实际位置和底层文件系统
135
+ 取决于 Go 环境和挂载情况。
136
+
137
+ ### Monorepo
138
+
139
+ 把仓库根目录作为工作区打开。每个包的命令都在同一个发行版中运行;
140
+ `pnpm -r`、`turbo`、`nx` 和 `make` 看到的是同一个 Linux 文件系统。
141
+ 当 `inotifywait` 可用时,无上限目录的文件监视使用 inotify;
142
+ 否则回退到轮询。如果遗漏了某次变更,见
143
+ [CONFIGURATION.md](CONFIGURATION.md) 中的监视器边界。
144
+
145
+ ## 兼容性
146
+
147
+ 诚实的现状,不是承诺。"已探针"指本仓库记录了一次真机运行;
148
+ "目标"指设计上支持,但这条腿尚未在这里执行过。
149
+
150
+ | 层级 | 配置 | 状态 |
151
+ |---|---|---|
152
+ | 1 | Ubuntu / Debian,systemd,x86_64 | 已探针。Debian 的 harness 级探针针对 `wslfs-debian` profile 通过(2026-10-26)。 |
153
+ | 2 | Fedora、openSUSE、Arch、Alpine;aarch64 原生 agent | 发行版矩阵见 [COMPATIBILITY.md](COMPATIBILITY.md)。aarch64 的 18 项检查在 CI 中于 qemu 下运行;物理 aarch64 WSL 不在范围内。 |
154
+ | 不支持 | WSL 1、Linux/macOS 宿主、无 WSL 2 的 Windows | 不支持;插件依附于 `wsl.exe` |
155
+
156
+ 设置 `sessionSandbox: auto` 时,没有 systemd/Linger 的发行版仍能获得
157
+ 按命令的 `bubblewrap` 沙箱,且结果会报告更窄的 scope;
158
+ `true` 则直接拒绝。沙箱取舍见
159
+ [CONFIGURATION.md](CONFIGURATION.md#sandbox-and-preflight),实测延迟见
160
+ [PERFORMANCE.md](PERFORMANCE.md)。
161
+
162
+ ## 演示
163
+
164
+ 上面的截图展示了选择器、终端、预览和沙箱探针。
165
+ 目前还没有 GIF。[故障排查表](../README.md#troubleshooting)
166
+ 和探针输出(`pnpm run probe:sandbox`、`pnpm run probe:gui-preview`)
167
+ 提供了可复现的证据;其余演示工作在
168
+ [ROADMAP-1.0.md](ROADMAP-1.0.md#wsl-302--discoverability-and-docs-entry) 中跟踪。
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-plugin-wsl-env",
3
- "version": "0.17.0",
4
- "description": "\u5728 WSL \u7684 Linux \u5b9e\u4f8b\u4e2d\u6253\u5f00\u9879\u76ee\u5de5\u4f5c\u533a\uff0c\u5728\u5bf9\u5e94 Linux \u7a7a\u95f4\u4e2d\u8fdb\u884c\u547d\u4ee4\u6267\u884c\u3001\u8fd0\u884c shell \u811a\u672c\u548c\u6587\u4ef6\u8bbf\u95ee\uff0c\u4ee5\u83b7\u5f97\u5168\u5957 Linux Shell \u5de5\u5177\u94fe\u6536\u76ca\uff0c\u5e76\u901a\u8fc7 bubblewrap \u5bf9\u547d\u4ee4\u8fdb\u884c\u6c99\u7bb1\u9694\u79bb",
3
+ "version": "0.17.1",
4
+ "description": "在 WSL 的 Linux 实例中打开项目工作区,在对应 Linux 空间中进行命令执行、运行 shell 脚本和文件访问,以获得全套 Linux Shell 工具链收益,并通过 bubblewrap 对命令进行沙箱隔离",
5
5
  "keywords": [
6
6
  "deepseek-harness",
7
7
  "dsh",
@@ -100,6 +100,9 @@
100
100
  "docs/PERFORMANCE.md",
101
101
  "docs/COMPATIBILITY.md",
102
102
  "docs/CONFIGURATION.md",
103
+ "docs/LIMITATIONS.zh.md",
104
+ "docs/CONFIGURATION.zh.md",
105
+ "docs/QUICKSTART.zh.md",
103
106
  "docs/LIMITATIONS.md",
104
107
  "docs/RELEASING.md",
105
108
  "docs/REQUIREMENTS.zh.md",