dsh-plugin-wsl-env 0.16.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.
- package/CHANGELOG.md +57 -1
- package/agent/native/linux-aarch64/dsh-wsl-agent +0 -0
- package/agent/native/linux-x86_64/dsh-wsl-agent +0 -0
- package/agent/wsl-agent.sh +47 -7
- package/cordis.patch.yml +15 -9
- package/docs/ARCHITECTURE.md +14 -6
- package/docs/CONFIGURATION.md +23 -0
- package/docs/CONFIGURATION.zh.md +288 -0
- package/docs/LIMITATIONS.md +133 -299
- package/docs/LIMITATIONS.zh.md +161 -0
- package/docs/QUICKSTART.md +2 -0
- package/docs/QUICKSTART.zh.md +168 -0
- package/docs/UPSTREAM-TERMINAL-TITLE.md +10 -12
- package/lib/agent-exec.d.ts +5 -0
- package/lib/agent-protocol.d.ts +18 -5
- package/lib/agent-protocol.js +11 -6
- package/lib/agent.d.ts +9 -1
- package/lib/agent.js +2 -2
- package/lib/git-exec.d.ts +117 -0
- package/lib/git-exec.js +105 -0
- package/lib/git-route.d.ts +43 -13
- package/lib/git-route.js +54 -15
- package/lib/subprocess.d.ts +21 -7
- package/lib/subprocess.js +63 -9
- package/package.json +4 -1
- package/scripts/doctor.mjs +20 -0
|
@@ -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)。
|
package/docs/QUICKSTART.md
CHANGED
|
@@ -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
|
+

|
|
63
|
+
|
|
64
|
+
终端面板运行在同一个发行版中——`uname -a` 返回的是
|
|
65
|
+
发行版的内核,而不是 Windows 的:
|
|
66
|
+
|
|
67
|
+

|
|
68
|
+
|
|
69
|
+
侧边栏的预览也能就地渲染 WSL 文件:
|
|
70
|
+
|
|
71
|
+

|
|
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
|
+

|
|
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) 中跟踪。
|
|
@@ -107,17 +107,15 @@ next touched:
|
|
|
107
107
|
|
|
108
108
|
## Workarounds until then
|
|
109
109
|
|
|
110
|
+
- The plugin now ships the candidate list itself: `shellCandidates` is
|
|
111
|
+
`[wsl.exe, powershell, cmd]` in [`cordis.patch.yml`](../cordis.patch.yml),
|
|
112
|
+
with the `shell` block kept verbatim. `wsl.exe` dedupes into the default's
|
|
113
|
+
`wsl` kind; selecting `powershell` or `cmd` starts a shell the terminal
|
|
114
|
+
really launches (host sessions do outright) and titles the tab after itself.
|
|
115
|
+
Nothing remains for a user layer to add unless it wants different entries.
|
|
110
116
|
- Double-click a tab's title to rename it (a shipped client feature; the
|
|
111
117
|
controller's `rename` Remote API accepts 1–120 characters).
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
to carry this plugin's `shell` block verbatim — `path: wsl.exe`, `name: WSL`,
|
|
117
|
-
`args: []` — or the terminal stops being a distro terminal. Expect the menu
|
|
118
|
-
to grow the host shells discovery finds on the root provider, which receives
|
|
119
|
-
no directory: executables this terminal never starts (the second bullet of
|
|
120
|
-
the GUI-terminal comment in [`cordis.patch.yml`](../cordis.patch.yml)).
|
|
121
|
-
[LIMITATIONS.md](LIMITATIONS.md) still carries the older one-line form of
|
|
122
|
-
this tip, which drops the `shell` block; the version above is the one that
|
|
123
|
-
works.
|
|
118
|
+
|
|
119
|
+
Both cover only manually selected shells. The DEFAULT entry — what a new tab
|
|
120
|
+
gets before anyone chooses — is still titled from the static profile, which is
|
|
121
|
+
exactly the gap the upstream change closes.
|
package/lib/agent-exec.d.ts
CHANGED
|
@@ -40,6 +40,11 @@ export interface AgentExecRequest {
|
|
|
40
40
|
env?: Record<string, string>;
|
|
41
41
|
/** Per-stream capture ceiling. */
|
|
42
42
|
maxOutputBytes?: number;
|
|
43
|
+
/**
|
|
44
|
+
* The child's entire stdin (protocol v6); the child sees EOF at the
|
|
45
|
+
* payload's end. Omitted or empty keeps the v5 shape.
|
|
46
|
+
*/
|
|
47
|
+
stdin?: Uint8Array;
|
|
43
48
|
/** The caller's cancellation. */
|
|
44
49
|
signal?: AbortSignal;
|
|
45
50
|
}
|
package/lib/agent-protocol.d.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* @module dsh-plugin-wsl/agent-protocol
|
|
15
15
|
*/
|
|
16
16
|
/** The protocol version this host side speaks; the agent must match. */
|
|
17
|
-
export declare const PROTOCOL_VERSION =
|
|
17
|
+
export declare const PROTOCOL_VERSION = 6;
|
|
18
18
|
/**
|
|
19
19
|
* Upper bound on one `read` op's payload, set by the HOST looping over windowed
|
|
20
20
|
* requests. Keeping each frame's decoded payload here means the agent's RES
|
|
@@ -65,17 +65,30 @@ export interface ExecFrameRequest {
|
|
|
65
65
|
* default) keeps the historical unbounded capture.
|
|
66
66
|
*/
|
|
67
67
|
maxOutputBytes?: number;
|
|
68
|
+
/**
|
|
69
|
+
* The child's entire stdin, delivered before the first read. Protocol v6:
|
|
70
|
+
* when present the EXEC header gains a `1` seventh field and one extra
|
|
71
|
+
* base64 line follows the argv words; the agent feeds those bytes to the
|
|
72
|
+
* child and closes its stdin (the child sees EOF at the payload's end). The
|
|
73
|
+
* payload rides one protocol line, so it is bounded by `MAX_FRAME_BYTES` —
|
|
74
|
+
* the shape of every known consumer (`git check-ignore --stdin` pathspecs),
|
|
75
|
+
* none of which streams an unbounded feed. Absent or empty means the child
|
|
76
|
+
* reads an empty stdin and immediate EOF, exactly the v5 behaviour.
|
|
77
|
+
*/
|
|
78
|
+
stdin?: Uint8Array;
|
|
68
79
|
}
|
|
69
80
|
/**
|
|
70
81
|
* Build the request lines for one EXEC frame: a header line naming the cwd,
|
|
71
|
-
* the whole-request timeout in whole seconds (0 = none), the argv word count
|
|
72
|
-
*
|
|
73
|
-
*
|
|
82
|
+
* the whole-request timeout in whole seconds (0 = none), the argv word count,
|
|
83
|
+
* the per-stream output capture budget in bytes (0 = unlimited) and — since
|
|
84
|
+
* protocol v6 — the stdin flag (1 when a payload line follows the argv
|
|
85
|
+
* words), followed by one base64 line per argv word and, when flagged, one
|
|
86
|
+
* base64 stdin payload line.
|
|
74
87
|
*
|
|
75
88
|
* @param request - the execution request.
|
|
76
89
|
* @returns the lines to write to the agent's stdin, in order.
|
|
77
90
|
*/
|
|
78
|
-
export declare function encodeExecFrame({ id, cwd, argv, timeoutMs, maxOutputBytes }: ExecFrameRequest): string[];
|
|
91
|
+
export declare function encodeExecFrame({ id, cwd, argv, timeoutMs, maxOutputBytes, stdin }: ExecFrameRequest): string[];
|
|
79
92
|
/**
|
|
80
93
|
* Build a SETENV request line. Note the agent's decode strips trailing
|
|
81
94
|
* newlines from the value — the same leniency the shell itself applies.
|
package/lib/agent-protocol.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* @module dsh-plugin-wsl/agent-protocol
|
|
15
15
|
*/
|
|
16
16
|
/** The protocol version this host side speaks; the agent must match. */
|
|
17
|
-
export const PROTOCOL_VERSION =
|
|
17
|
+
export const PROTOCOL_VERSION = 6;
|
|
18
18
|
/**
|
|
19
19
|
* Upper bound on one `read` op's payload, set by the HOST looping over windowed
|
|
20
20
|
* requests. Keeping each frame's decoded payload here means the agent's RES
|
|
@@ -54,20 +54,25 @@ export function decodeB64(value) {
|
|
|
54
54
|
}
|
|
55
55
|
/**
|
|
56
56
|
* Build the request lines for one EXEC frame: a header line naming the cwd,
|
|
57
|
-
* the whole-request timeout in whole seconds (0 = none), the argv word count
|
|
58
|
-
*
|
|
59
|
-
*
|
|
57
|
+
* the whole-request timeout in whole seconds (0 = none), the argv word count,
|
|
58
|
+
* the per-stream output capture budget in bytes (0 = unlimited) and — since
|
|
59
|
+
* protocol v6 — the stdin flag (1 when a payload line follows the argv
|
|
60
|
+
* words), followed by one base64 line per argv word and, when flagged, one
|
|
61
|
+
* base64 stdin payload line.
|
|
60
62
|
*
|
|
61
63
|
* @param request - the execution request.
|
|
62
64
|
* @returns the lines to write to the agent's stdin, in order.
|
|
63
65
|
*/
|
|
64
|
-
export function encodeExecFrame({ id, cwd, argv, timeoutMs = 0, maxOutputBytes = 0 }) {
|
|
66
|
+
export function encodeExecFrame({ id, cwd, argv, timeoutMs = 0, maxOutputBytes = 0, stdin }) {
|
|
65
67
|
const timeoutSeconds = timeoutMs > 0 ? Math.ceil(timeoutMs / 1000) : 0;
|
|
66
68
|
const cap = maxOutputBytes > 0 ? Math.floor(maxOutputBytes) : 0;
|
|
69
|
+
const hasStdin = stdin !== undefined && stdin.length > 0;
|
|
67
70
|
const lines = [
|
|
68
|
-
`EXEC|${id}|${encodeB64(cwd)}|${timeoutSeconds}|${argv.length}|${cap}`,
|
|
71
|
+
`EXEC|${id}|${encodeB64(cwd)}|${timeoutSeconds}|${argv.length}|${cap}|${hasStdin ? 1 : 0}`,
|
|
69
72
|
...argv.map((word) => encodeB64(word)),
|
|
70
73
|
];
|
|
74
|
+
if (hasStdin)
|
|
75
|
+
lines.push(encodeB64(stdin));
|
|
71
76
|
return lines;
|
|
72
77
|
}
|
|
73
78
|
/**
|
package/lib/agent.d.ts
CHANGED
|
@@ -431,7 +431,7 @@ export declare class WslAgent {
|
|
|
431
431
|
* @returns the settled result.
|
|
432
432
|
* @throws {AgentUnavailableError} when the agent is out for this process.
|
|
433
433
|
*/
|
|
434
|
-
exec({ cwd, argv, timeoutMs, maxOutputBytes, env, signal }: {
|
|
434
|
+
exec({ cwd, argv, timeoutMs, maxOutputBytes, env, stdin, signal }: {
|
|
435
435
|
/** Linux path to run in. */
|
|
436
436
|
cwd: string;
|
|
437
437
|
/** The argv, already bwrap-wrapped when the caller confines. */
|
|
@@ -453,6 +453,14 @@ export declare class WslAgent {
|
|
|
453
453
|
* keeps the unbounded capture.
|
|
454
454
|
*/
|
|
455
455
|
maxOutputBytes?: number;
|
|
456
|
+
/**
|
|
457
|
+
* The child's entire stdin, delivered before its first read (protocol
|
|
458
|
+
* v6); the child sees EOF at the payload's end. Omitted or empty keeps
|
|
459
|
+
* the v5 shape — an empty stdin and immediate EOF. Note the shell agent
|
|
460
|
+
* decodes the payload through command substitution, which strips
|
|
461
|
+
* trailing newlines; the native agent delivers the bytes verbatim.
|
|
462
|
+
*/
|
|
463
|
+
stdin?: Uint8Array;
|
|
456
464
|
/** Cancels the request, sending KILL if it was already dispatched. */
|
|
457
465
|
signal?: AbortSignal;
|
|
458
466
|
}): Promise<{
|
package/lib/agent.js
CHANGED
|
@@ -780,7 +780,7 @@ export class WslAgent {
|
|
|
780
780
|
* @returns the settled result.
|
|
781
781
|
* @throws {AgentUnavailableError} when the agent is out for this process.
|
|
782
782
|
*/
|
|
783
|
-
async exec({ cwd, argv, timeoutMs = 0, maxOutputBytes = 0, env, signal }) {
|
|
783
|
+
async exec({ cwd, argv, timeoutMs = 0, maxOutputBytes = 0, env, stdin, signal }) {
|
|
784
784
|
const assignments = Object.entries(env ?? {}).map(([key, value]) => {
|
|
785
785
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key) || value.includes("\0")) {
|
|
786
786
|
throw new Error(`invalid environment variable: ${key}`);
|
|
@@ -793,7 +793,7 @@ export class WslAgent {
|
|
|
793
793
|
const id = `r${this.nextRequestId++}`;
|
|
794
794
|
const commandArgv = argv[0]?.includes("=") ? ["/bin/sh", "-c", "exec \"$@\"", "env", ...argv] : argv;
|
|
795
795
|
const requestArgv = assignments.length > 0 ? ["env", "--", ...assignments, ...commandArgv] : argv;
|
|
796
|
-
const lines = encodeExecFrame({ id, cwd, argv: requestArgv, timeoutMs, maxOutputBytes });
|
|
796
|
+
const lines = encodeExecFrame({ id, cwd, argv: requestArgv, timeoutMs, maxOutputBytes, stdin });
|
|
797
797
|
return new Promise((resolve, reject) => {
|
|
798
798
|
this.track({ id, kind: "exec", lines, resolve, reject, timeoutMs, signal });
|
|
799
799
|
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The agent-backed execution handle for the git-snapshot spawn.
|
|
3
|
+
*
|
|
4
|
+
* `workspace-changes`' `GitRunner` consumes a small slice of the subprocess
|
|
5
|
+
* handle: `handle.done` for `{exitCode, signal}`, then
|
|
6
|
+
* `handle.collected.stdout/stderr.readFrom(0)` for `{text, lossy}` — with
|
|
7
|
+
* per-stream caps applied by the caller's `stdio` spec (a generous stdout
|
|
8
|
+
* budget and a 16 KiB stderr tail). This module builds exactly that surface
|
|
9
|
+
* around one resident-agent `exec`, so a distro snapshot pays the warm agent's
|
|
10
|
+
* ~2 ms round trip per git call instead of the one-shot `wsl.exe`'s ~230 ms
|
|
11
|
+
* process startup — measured 2026-11-01 on `ubuntu`; a full snapshot's 6-8
|
|
12
|
+
* calls (plus per-file `cat-file`) used to spend over a second inside
|
|
13
|
+
* `wsl.exe` alone.
|
|
14
|
+
*
|
|
15
|
+
* The caps map onto the agent's single per-request capture ceiling as the
|
|
16
|
+
* larger of the two stream budgets; the reader then slices each stream to its
|
|
17
|
+
* own budget and reports `lossy` from that slice, so the caller's contract
|
|
18
|
+
* (per-stream cut flags) holds even though the wire carries one cap.
|
|
19
|
+
*
|
|
20
|
+
* Environment rides the agent's per-request `env` (an `env --` argv prefix —
|
|
21
|
+
* no `SETENV` global pollution), holding only the translated `GIT_*` names:
|
|
22
|
+
* the one-shot path forwards exactly those through `WSLENV`, and forwarding a
|
|
23
|
+
* host `PATH` into the distro would be wrong, not just different.
|
|
24
|
+
*
|
|
25
|
+
* The agent being out is the fallback cue, exactly as `search-exec.ts`:
|
|
26
|
+
* `spawnFallback` (the caller's one-shot `wsl.exe` handle) is consulted
|
|
27
|
+
* lazily inside `done`, and from that point the facade proxies the delegate
|
|
28
|
+
* verbatim. Cancellation shares one channel: the caller's `signal` and
|
|
29
|
+
* `kill()` abort the agent exec, settling `done` as killed.
|
|
30
|
+
*
|
|
31
|
+
* Pure shaping over injected collaborators, unit-testable without a distro.
|
|
32
|
+
*
|
|
33
|
+
* This is a TypeScript source built to `lib/git-exec.js`; edit THIS file and
|
|
34
|
+
* run `pnpm run build` — the artifact under `lib/` is generated, and
|
|
35
|
+
* `pnpm test` fails when it drifts.
|
|
36
|
+
*
|
|
37
|
+
* @module dsh-plugin-wsl/git-exec
|
|
38
|
+
*/
|
|
39
|
+
import type { AgentExecRunner } from "./agent-exec.js";
|
|
40
|
+
/** One collected-stream reader with the seam's standard shape. */
|
|
41
|
+
export interface GitStreamReader {
|
|
42
|
+
/** Read the stream from a byte offset; `lossy` says the cap cut it. */
|
|
43
|
+
readFrom: (from: number) => {
|
|
44
|
+
text: string;
|
|
45
|
+
lossy: boolean;
|
|
46
|
+
nextOffset: number;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The slice of the subprocess handle `GitRunner` consumes: settle on `done`,
|
|
51
|
+
* then read the collected streams.
|
|
52
|
+
*/
|
|
53
|
+
export interface GitExecutionHandle {
|
|
54
|
+
/** Settles with the exit code and signal; rejects on a genuine failure. */
|
|
55
|
+
done: Promise<{
|
|
56
|
+
exitCode: number | null;
|
|
57
|
+
signal: string | null;
|
|
58
|
+
}>;
|
|
59
|
+
/** The collected stream readers; a delegated handle's own on fallback. */
|
|
60
|
+
collected: {
|
|
61
|
+
stdout?: GitStreamReader | undefined;
|
|
62
|
+
stderr?: GitStreamReader | undefined;
|
|
63
|
+
};
|
|
64
|
+
/** Cooperative kill; the runner relies on its `signal` for cancellation. */
|
|
65
|
+
kill?: () => boolean | void;
|
|
66
|
+
}
|
|
67
|
+
/** Per-stream capture budgets the caller declared on its `stdio` spec. */
|
|
68
|
+
export interface GitStreamCaps {
|
|
69
|
+
/** The stdout budget in bytes; the snapshot's `outputMaxBytes`. */
|
|
70
|
+
stdout: number;
|
|
71
|
+
/** The stderr tail budget in bytes. */
|
|
72
|
+
stderr: number;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Run one git command through the agent and present it as a git-snapshot
|
|
76
|
+
* handle.
|
|
77
|
+
*
|
|
78
|
+
* @param object - the handle's collaborators.
|
|
79
|
+
* @returns the snapshot handle (`done`, `collected`, `kill`).
|
|
80
|
+
*/
|
|
81
|
+
export declare function gitExecutionHandle({ agent, cwd, argv, env, stdin, caps, distro, discovery, signal, spawnFallback }: {
|
|
82
|
+
/** The {@link WslAgent}-like runner; throws `AgentUnavailableError` when out. */
|
|
83
|
+
agent: AgentExecRunner;
|
|
84
|
+
/** The Linux working directory to snapshot from. */
|
|
85
|
+
cwd: string;
|
|
86
|
+
/** The distro-side argv (`git` and its arguments). */
|
|
87
|
+
argv: string[];
|
|
88
|
+
/**
|
|
89
|
+
* The distro name; required when `discovery` is set (absolute path lines
|
|
90
|
+
* become `\\wsl.localhost\<distro><path>`).
|
|
91
|
+
*/
|
|
92
|
+
distro?: string;
|
|
93
|
+
/**
|
|
94
|
+
* True for a SENTINEL discovery call (`--show-toplevel` /
|
|
95
|
+
* `--absolute-git-dir` / `--git-path` answered by the distro git): the
|
|
96
|
+
* captured stdout's absolute Linux path lines are translated into UNC form
|
|
97
|
+
* before the caller reads them. Snapshot calls leave this unset.
|
|
98
|
+
*/
|
|
99
|
+
discovery?: boolean;
|
|
100
|
+
/**
|
|
101
|
+
* Per-request environment: the caller's entries with path-shaped values
|
|
102
|
+
* translated into distro form. Empty entries are dropped; nothing but
|
|
103
|
+
* `GIT_*` names should reach here.
|
|
104
|
+
*/
|
|
105
|
+
env: Record<string, string>;
|
|
106
|
+
/** The child's entire stdin, or undefined for an empty one. */
|
|
107
|
+
stdin?: Uint8Array;
|
|
108
|
+
/** Per-stream capture budgets from the caller's `stdio` spec. */
|
|
109
|
+
caps: GitStreamCaps;
|
|
110
|
+
/** The caller's cancellation (its own timeout races in through `AbortSignal.any`). */
|
|
111
|
+
signal?: AbortSignal;
|
|
112
|
+
/**
|
|
113
|
+
* Builds the one-shot handle the facade delegates to when the agent is out;
|
|
114
|
+
* called at most once, lazily.
|
|
115
|
+
*/
|
|
116
|
+
spawnFallback: () => GitExecutionHandle;
|
|
117
|
+
}): GitExecutionHandle;
|