dsh-plugin-wsl-env 0.7.4 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/CHANGELOG.md +298 -0
  2. package/CONTRIBUTING.md +129 -39
  3. package/README.md +20 -20
  4. package/docs/ARCHITECTURE.md +6 -3
  5. package/docs/LIMITATIONS.md +13 -0
  6. package/docs/README.zh.md +20 -20
  7. package/lib/agent-confined.d.ts +42 -0
  8. package/lib/agent-confined.js +25 -41
  9. package/lib/agent-errors.d.ts +31 -0
  10. package/lib/agent-errors.js +19 -16
  11. package/lib/agent-exec.d.ts +159 -0
  12. package/lib/agent-exec.js +151 -165
  13. package/lib/agent-protocol.d.ts +192 -0
  14. package/lib/agent-protocol.js +76 -100
  15. package/lib/agent-shared.d.ts +27 -0
  16. package/lib/agent-shared.js +12 -12
  17. package/lib/agent.d.ts +353 -0
  18. package/lib/agent.js +536 -552
  19. package/lib/auto-preset.d.ts +110 -0
  20. package/lib/auto-preset.js +157 -141
  21. package/lib/bwrap.d.ts +90 -0
  22. package/lib/bwrap.js +18 -18
  23. package/lib/file-reference-wsl.d.ts +160 -0
  24. package/lib/file-reference-wsl.js +192 -173
  25. package/lib/fs-decisions.d.ts +107 -0
  26. package/lib/fs-decisions.js +64 -60
  27. package/lib/fs-routing.d.ts +182 -0
  28. package/lib/fs-routing.js +173 -116
  29. package/lib/fs-substrate.d.ts +258 -0
  30. package/lib/fs-substrate.js +248 -248
  31. package/lib/fs.d.ts +10 -0
  32. package/lib/fs.js +5 -0
  33. package/lib/fsio-agent.d.ts +307 -0
  34. package/lib/fsio-agent.js +414 -410
  35. package/lib/fsio-text.d.ts +115 -0
  36. package/lib/fsio-text.js +104 -84
  37. package/lib/git-route.d.ts +100 -0
  38. package/lib/git-route.js +46 -47
  39. package/lib/index.d.ts +1003 -0
  40. package/lib/index.js +1101 -997
  41. package/lib/listing.d.ts +114 -0
  42. package/lib/listing.js +68 -55
  43. package/lib/paths.d.ts +182 -0
  44. package/lib/paths.js +136 -105
  45. package/lib/picker.d.ts +266 -0
  46. package/lib/picker.js +371 -351
  47. package/lib/ports.d.ts +44 -0
  48. package/lib/ports.js +39 -29
  49. package/lib/preset-choice.d.ts +33 -0
  50. package/lib/preset-choice.js +15 -7
  51. package/lib/probe-cache.d.ts +40 -0
  52. package/lib/probe-cache.js +35 -29
  53. package/lib/sandbox-core.d.ts +79 -0
  54. package/lib/sandbox-core.js +71 -88
  55. package/lib/sandbox.d.ts +48 -0
  56. package/lib/sandbox.js +4 -4
  57. package/lib/search-exec.d.ts +96 -0
  58. package/lib/search-exec.js +52 -79
  59. package/lib/search-route.d.ts +75 -0
  60. package/lib/search-route.js +30 -31
  61. package/lib/shell-env.d.ts +88 -0
  62. package/lib/shell-env.js +78 -70
  63. package/lib/shell.d.ts +10 -0
  64. package/lib/shell.js +5 -0
  65. package/lib/subprocess.d.ts +313 -0
  66. package/lib/subprocess.js +267 -262
  67. package/lib/terminal-activity.d.ts +98 -0
  68. package/lib/terminal-activity.js +29 -39
  69. package/lib/terminal-route.d.ts +27 -0
  70. package/lib/terminal-route.js +16 -8
  71. package/lib/watcher.d.ts +77 -0
  72. package/lib/watcher.js +120 -126
  73. package/lib/workspace-files-route.d.ts +132 -0
  74. package/lib/workspace-files-route.js +73 -81
  75. package/lib/workspace-files-wsl.d.ts +257 -0
  76. package/lib/workspace-files-wsl.js +311 -251
  77. package/lib/wsl.d.ts +266 -0
  78. package/lib/wsl.js +144 -151
  79. package/package.json +68 -20
@@ -29,6 +29,12 @@ crosses the 9p share. What to know about the one substrate there is:
29
29
  syscall before the rename: a concurrent writer wins and the stale write
30
30
  refuses with `FS_STALE_VERSION`. The residual stat-then-rename pair is the
31
31
  same sliver a Linux-host session's own rename has.
32
+ - **A write is durable against process death, not machine death.** The
33
+ publication fsyncs the staged file before its atomic rename, but the
34
+ containing directory is not fsynced afterwards: a kernel panic or power loss
35
+ immediately after a successful write can lose the rename, and the file
36
+ reverts to its pre-write content. This is stronger than the peer backend,
37
+ which fsyncs nothing, and short of a crash-consistent store.
32
38
  - **A Linux filename containing a backslash cannot be addressed.** The UNC
33
39
  display form (`\\wsl.localhost\<distro>\...`) is the file's identity
34
40
  everywhere - the session header, the GUI, the caches - and the backslash is
@@ -71,6 +77,13 @@ crosses the 9p share. What to know about the one substrate there is:
71
77
  - **`bubblewrap` must be installed**, or both providers fail immediately. The
72
78
  usability probe re-runs after a failure, so installing bubblewrap while the app
73
79
  runs is believed on the next command; only a success is cached.
80
+ - **The port poller keeps the resident agent permanently warm.** The
81
+ `DSH_WSL_PORTS` snapshot refreshes through the shared resident agent every
82
+ `portsRefreshMs` (10 s by default, well inside the agent's idle timeout), so
83
+ while the app runs the agent never idles out: one `wsl.exe` and one
84
+ in-distro agent process stay resident per pinned distro even with no command
85
+ in flight. Raising `portsRefreshMs` past the agent's idle timeout restores
86
+ idle shutdown, at the cost of staler port facts.
74
87
  - **A confined command's `/tmp` is not the write tool's `/tmp`.** `workspace-write`
75
88
  mounts a fresh tmpfs at `/tmp` for the command — that is what makes it ephemeral —
76
89
  while the file tools' fence grants the distro's *real* `/tmp`. Both are writable, so
package/docs/README.zh.md CHANGED
@@ -28,7 +28,7 @@ dsh --profile wsl # 4. 启动
28
28
 
29
29
  然后在 GUI 里打开 `\\wsl.localhost\<子系统>\...` 下的文件夹。选择器会在根一级列出每个已安装的子系统,**New terminal** 会在子系统里打开 shell。
30
30
 
31
- 还需要在子系统里安装 **bubblewrap**:运行 `npm run bootstrap -- <子系统> --install`(只读检测去掉 `--install`),或直接执行 `wsl.exe -d <子系统> -u root -- apt-get install -y bubblewrap`。没有它每条命令都会失败关闭,见[沙箱](#沙箱)。
31
+ 还需要在子系统里安装 **bubblewrap**:运行 `pnpm run bootstrap -- <子系统> --install`(只读检测去掉 `--install`),或直接执行 `wsl.exe -d <子系统> -u root -- apt-get install -y bubblewrap`。没有它每条命令都会失败关闭,见[沙箱](#沙箱)。
32
32
 
33
33
  卸载:`dsh plugin --profile wsl remove dsh-plugin-wsl-env`。升级:再执行一次同样的 `add` 命令。
34
34
 
@@ -68,7 +68,7 @@ dsh --profile wsl # 4. 启动
68
68
  - **Git 凭据共享**:让子系统里的 git 使用 Windows 侧的 Git Credential Manager,避免每次输密码:
69
69
  `git config --global credential.helper "/mnt/c/Program\ Files/Git/mingw64/bin/git-credential-manager.exe"`
70
70
  (路径按 Windows 侧 Git 的安装位置调整;WSL2 的 localhost 转发是平台行为,子系统内监听的端口 Windows 直接可达。)
71
- - **路径与性能**:模型看到并操作的是子系统内的 Linux 路径(`/home/...`),写入子系统自身的 ext4;`/mnt/c` 通向 Windows 磁盘但走 9p,大批量小文件操作明显慢——重 IO 的项目请放在子系统文件系统内。`npm run bootstrap -- <子系统>` 会一并报告 ripgrep、git、inotifywait(搜索、快照与监视的后端)是否就位。
71
+ - **路径与性能**:模型看到并操作的是子系统内的 Linux 路径(`/home/...`),写入子系统自身的 ext4;`/mnt/c` 通向 Windows 磁盘但走 9p,大批量小文件操作明显慢——重 IO 的项目请放在子系统文件系统内。`pnpm run bootstrap -- <子系统>` 会一并报告 ripgrep、git、inotifywait(搜索、快照与监视的后端)是否就位。
72
72
  - **WSLENV 透传**:WSL 只导入 `WSLENV` 中列出的变量。本插件按前缀放行托管的 `DSH_*` 命名空间,其中带 Windows 路径的两个(`DSH_HOME`、`DSH_PROFILE_DIR`)加 `/p` 让 WSL 翻译成 `/mnt/c/...`。`PATH` 故意不透传——否则 Windows 的 PATH 会覆盖子系统自身的 PATH。
73
73
 
74
74
  ## 架构
@@ -105,7 +105,7 @@ preset-wsl(wsl agent preset;其服务运行在 isolate realm 内)
105
105
 
106
106
  **bubblewrap 是必需项,缺失即失败关闭。** 没有它时,每条受限命令都报 `SANDBOX_UNAVAILABLE`,而不是不受约束地运行。要退出约束就在任一 provider 上设 `sandbox: false`,工具层会如实告诉模型这些操作没有沙箱。
107
107
 
108
- **上报的强制程度是 `partial` 而不是 `full`。** 子系统里的进程仍可经 WSL interop 执行 Windows 程序(例如 `/mnt/c/.../*.exe`),bubblewrap 管不到它。`npm run probe:sandbox` 会在你的机器上演示这条边界。
108
+ **上报的强制程度是 `partial` 而不是 `full`。** 子系统里的进程仍可经 WSL interop 执行 Windows 程序(例如 `/mnt/c/.../*.exe`),bubblewrap 管不到它。`pnpm run probe:sandbox` 会在你的机器上演示这条边界。
109
109
 
110
110
  设计见 [docs/ARCHITECTURE.md](ARCHITECTURE.md#sandbox),插件不做的事情见 [docs/LIMITATIONS.md](LIMITATIONS.md)。
111
111
 
@@ -113,37 +113,37 @@ preset-wsl(wsl agent preset;其服务运行在 isolate realm 内)
113
113
 
114
114
  | 现象 | 原因 | 处理 |
115
115
  |---|---|---|
116
- | 每条命令都报 `SANDBOX_UNAVAILABLE` | 子系统里没有 `bubblewrap` | 执行 `npm run bootstrap -- <子系统> --install`,或在两个 provider 上都设 `sandbox: false` |
116
+ | 每条命令都报 `SANDBOX_UNAVAILABLE` | 子系统里没有 `bubblewrap` | 执行 `pnpm run bootstrap -- <子系统> --install`,或在两个 provider 上都设 `sandbox: false` |
117
117
  | 命令或写入在会话目录之外被拒绝 | `workspace-write` 的预期行为 | 接受工具给出的放宽权限提示,或把会话直接开在需要的目录上 |
118
118
  | 连工作区内的写入也被拒绝 | 会处在 `read-only` 模式 | 切换权限选择器 |
119
119
  | `dsh plugin add` 提示"没有激活任何层" | 依赖之前已经装过,`add` 没有需要记录的内容 | 先执行 `dsh plugin --profile wsl remove dsh-plugin-wsl-env`,再装一次 |
120
120
  | 终端打开后仍然是 `cmd.exe` | 这一层里的 `terminal-controller` 行没有生效 | 用 `dsh --profile wsl --dump-config` 确认能看到 `shell: { path: wsl.exe, name: WSL }` |
121
121
  | 改了 `lib/` 但不生效 | ES module 缓存 | 重启应用 |
122
122
  | 用 `link:\\wsl.localhost\...` 安装后符号链接是坏的 | pnpm 无法链接 UNC 路径 | 改成链接 Windows 路径;在子系统内开发时用运行时镜像,见[开发](#开发) |
123
- | `glob`/`grep` 很慢 | 子系统内没有 rg 时,搜索只能报 rg 自己的 "command not found";Windows 目录的搜索走宿主原生 rg,不受影响 | 运行 `npm run bootstrap -- <子系统> --install`(安装 ripgrep);子系统工作区的搜索始终跑子系统内的 rg,绝不遍历 9p 共享 |
123
+ | `glob`/`grep` 很慢 | 子系统内没有 rg 时,搜索只能报 rg 自己的 "command not found";Windows 目录的搜索走宿主原生 rg,不受影响 | 运行 `pnpm run bootstrap -- <子系统> --install`(安装 ripgrep);子系统工作区的搜索始终跑子系统内的 rg,绝不遍历 9p 共享 |
124
124
  | 终端活动显示 `unknown` | 仅在常驻代理不可用时出现——distro 终端从子系统内部观测(`/proc` 中扫描 `DSH_TERMINAL_ID` 标记:shell 独处为 `idle`,运行任何命令为 `busy`) | 确认子系统在运行;空闲终端会在控制器的无人值守超时(默认 2 小时)后自动回收,`terminalIdleReclaim: false` 恢复手动关闭 |
125
125
  | 结果里出现 `FS_*` 码 | 码本身说明了是谁拒绝的、以及怎样解除 | 见 [docs/ARCHITECTURE.md](ARCHITECTURE.md#error-codes) 的错误码表 |
126
126
 
127
127
  ## 开发
128
128
 
129
129
  ```bash
130
- npm test # 风格与打包检查、语法检查、单元测试
131
- npm run test:coverage # 带覆盖率阈值的单元测试(需 Node 22.8+)
132
- npm run probe:sandbox # 在子系统里实测 bubblewrap 能约束什么、不能约束什么
133
- npm run probe # 文件系统探针,需要真实子系统(仅 Windows + WSL)
134
- npm run probe:sandbox-shell # 启动真实 harness,驱动受限执行器
135
- npm run probe:terminal # 通过终端 provider 打开一个 PTY
136
- npm run probe:substrate # 用真实 wsl.exe 传输驱动 agent 文件基座(在子系统内运行)
137
- npm run probe:watch # 在真实目录上装上 distro 内轮询 watcher(在子系统内运行)
138
- npm run probe:agent # resident 与 one-shot 的回退不变量对比腿(在子系统内运行)
139
- npm run probe:exec # agent 执行句柄:超时、杀停、cwd 失败(在子系统内运行)
140
- npm run probe:missing-wsl # 启动一个 wslPath 无法启动的 profile(仅 Windows + WSL)
141
- npm run probe:picker # 列出选择器的根级、拒绝路径与上限(仅 Windows + WSL)
142
- npm run probe:mode # 哪些 POSIX 权限事实能穿过共享层(仅需 Windows node)
143
- npm run probe:sandbox-off # 验证 sandbox: false 确实解除两侧约束(仅 Windows + WSL)
130
+ pnpm test # 风格与打包检查、语法检查、单元测试
131
+ pnpm run test:coverage # 带覆盖率阈值的单元测试(需 Node 22.8+)
132
+ pnpm run probe:sandbox # 在子系统里实测 bubblewrap 能约束什么、不能约束什么
133
+ pnpm run probe # 文件系统探针,需要真实子系统(仅 Windows + WSL)
134
+ pnpm run probe:sandbox-shell # 启动真实 harness,驱动受限执行器
135
+ pnpm run probe:terminal # 通过终端 provider 打开一个 PTY
136
+ pnpm run probe:substrate # 用真实 wsl.exe 传输驱动 agent 文件基座(在子系统内运行)
137
+ pnpm run probe:watch # 在真实目录上装上 distro 内轮询 watcher(在子系统内运行)
138
+ pnpm run probe:agent # resident 与 one-shot 的回退不变量对比腿(在子系统内运行)
139
+ pnpm run probe:exec # agent 执行句柄:超时、杀停、cwd 失败(在子系统内运行)
140
+ pnpm run probe:missing-wsl # 启动一个 wslPath 无法启动的 profile(仅 Windows + WSL)
141
+ pnpm run probe:picker # 列出选择器的根级、拒绝路径与上限(仅 Windows + WSL)
142
+ pnpm run probe:mode # 哪些 POSIX 权限事实能穿过共享层(仅需 Windows node)
143
+ pnpm run probe:sandbox-off # 验证 sandbox: false 确实解除两侧约束(仅 Windows + WSL)
144
144
  ```
145
145
 
146
- 这个包没有依赖,被测模块只 import Node 内置模块,所以不需要先安装任何东西。CI 在 Linux 和 Windows 上用 Node 20、22、24 运行 `npm test`,并在 Node 24 上运行 `npm run test:coverage`。
146
+ 首次运行前先 `pnpm install` 一次,复原钉版的开发依赖:0.7.x 的测试会 import 钉版的 `@deepseek-ai/*` 包(组合与启动测试跑的是 loader 的真实 patch 算法),运行时包本身仍然保持零依赖。pnpm 由 `packageManager` 字段解析,任何开了 corepack 的 Node 都能直接用。CI 在 Linux 和 Windows 上用 Node 22、24 运行 `pnpm test`,并在 Node 24 上运行 `pnpm run test:coverage`;`engines` 与 harness 宿主自身的下限(`^22.19.0 || >=24.0.0`)保持一致。
147
147
 
148
148
  [docs/ARCHITECTURE.md](ARCHITECTURE.md) 有文件布局、挂载方式和沙箱设计;[CONTRIBUTING.md](../CONTRIBUTING.md) 有开发循环、完整质量门清单和已验证内容。
149
149
 
@@ -0,0 +1,42 @@
1
+ import { WslAgent } from "./agent.js";
2
+ /** One confinement request. */
3
+ export interface ConfinedAgentRequest {
4
+ /** The distro to run in. */
5
+ distro: string;
6
+ /** The `wsl.exe` path. */
7
+ wslPath?: string;
8
+ /**
9
+ * The RESOLVED file-effect policy: the profile binds exactly what the mode
10
+ * grants.
11
+ */
12
+ policy: {
13
+ mode: string;
14
+ workspaceRoot: string;
15
+ };
16
+ /**
17
+ * Shadow `/mnt` with an empty tmpfs in the resident's profile; part of the
18
+ * cache key, since it changes the mount table the resident lives under.
19
+ */
20
+ maskWindowsDrive?: boolean;
21
+ /**
22
+ * Default TRUE: workspace-write binds the distro's real `/tmp` read-write
23
+ * into the resident's profile, so a write the fence approved lands where
24
+ * every reader reads. `false` restores the ephemeral tmpfs — for tests and
25
+ * probes, not production: a `/tmp` write behind an ephemeral mount reports
26
+ * success into a directory no reader can see, which is the bug this default
27
+ * exists to prevent.
28
+ */
29
+ realTmp?: boolean;
30
+ }
31
+ /**
32
+ * The confined agent for one distro and one resolved policy, created on first
33
+ * use and shared for the process lifetime. A success is remembered; a failed
34
+ * start is not (the agent's own rebuild rules take over from there).
35
+ *
36
+ * @param request - the confinement request.
37
+ * @returns the confined resident for this key.
38
+ */
39
+ export declare function confinedAgent({ distro, wslPath, policy, maskWindowsDrive, realTmp }: ConfinedAgentRequest): WslAgent;
40
+ /** Test hook: forget every confined resident (the plain agent has no factory state). */
41
+ export declare function resetConfinedAgents(): void;
42
+ export default confinedAgent;
@@ -25,63 +25,47 @@
25
25
  * a third copy, so nothing compared production's command with a working one and
26
26
  * every confined mutation failed the resident's handshake.
27
27
  *
28
+ * This is a TypeScript source built to `lib/agent-confined.js`; edit THIS file
29
+ * and run `pnpm run build` — the artifact under `lib/` is generated, and
30
+ * `pnpm test` fails when it drifts.
31
+ *
28
32
  * @module dsh-plugin-wsl/agent-confined
29
33
  */
30
34
  import { bwrapArgvPrefix } from "./bwrap.js";
31
35
  import { toLinuxPath } from "./paths.js";
32
36
  import { agentScriptDigest, agentScriptPath } from "./agent-shared.js";
33
37
  import { WslAgent } from "./agent.js";
34
-
35
38
  const confinedAgents = new Map();
36
-
37
39
  /**
38
40
  * The confined agent for one distro and one resolved policy, created on first
39
41
  * use and shared for the process lifetime. A success is remembered; a failed
40
42
  * start is not (the agent's own rebuild rules take over from there).
41
43
  *
42
- * @param {object} request - the confinement request.
43
- * @param {string} request.distro - the distro to run in.
44
- * @param {string} [request.wslPath] - the `wsl.exe` path.
45
- * @param {{mode: string, workspaceRoot: string}} request.policy - the RESOLVED
46
- * file-effect policy: the profile binds exactly what the mode grants.
47
- * @param {boolean} [request.maskWindowsDrive] - shadow `/mnt` with an empty
48
- * tmpfs in the resident's profile; part of the cache key, since it changes
49
- * the mount table the resident lives under.
50
- * @param {boolean} [request.realTmp] - default TRUE: workspace-write binds the
51
- * distro's real `/tmp` read-write into the resident's profile, so a write
52
- * the fence approved lands where every reader reads. `false` restores the
53
- * ephemeral tmpfs — for tests and probes, not production: a `/tmp` write
54
- * behind an ephemeral mount reports success into a directory no reader can
55
- * see, which is the bug this default exists to prevent.
56
- * @returns {WslAgent} the confined resident for this key.
44
+ * @param request - the confinement request.
45
+ * @returns the confined resident for this key.
57
46
  */
58
47
  export function confinedAgent({ distro, wslPath = "wsl.exe", policy, maskWindowsDrive = false, realTmp = true }) {
59
- const key = `${wslPath}\0${distro}\0${policy.mode}\0${policy.workspaceRoot}\0${maskWindowsDrive ? "masked" : "visible"}\0${realTmp ? "real" : "ephemeral"}`;
60
- let agent = confinedAgents.get(key);
61
- if (agent === undefined) {
62
- agent = new WslAgent({
63
- distro,
64
- wslPath,
65
- scriptPath: toLinuxPath(agentScriptPath),
66
- expectedDigest: agentScriptDigest,
67
- // The prefix is a whole command from the distro's point of view — program,
68
- // profile, `--` — because the transport inserts it between
69
- // `wsl.exe --exec` and the `sh <script>` pair (`lib/agent.js`'s
70
- // `spawnDefault`). Both confinement sites share that assembly in
71
- // `lib/bwrap.js`, whose module doc records what happened when they did not.
72
- argvPrefix: bwrapArgvPrefix(
73
- { mode: policy.mode, workspaceRoot: toLinuxPath(policy.workspaceRoot, { distro }) },
74
- { maskWindowsDrive, realTmp },
75
- ),
76
- });
77
- confinedAgents.set(key, agent);
78
- }
79
- return agent;
48
+ const key = `${wslPath}\0${distro}\0${policy.mode}\0${policy.workspaceRoot}\0${maskWindowsDrive ? "masked" : "visible"}\0${realTmp ? "real" : "ephemeral"}`;
49
+ let agent = confinedAgents.get(key);
50
+ if (agent === undefined) {
51
+ agent = new WslAgent({
52
+ distro,
53
+ wslPath,
54
+ scriptPath: toLinuxPath(agentScriptPath),
55
+ expectedDigest: agentScriptDigest,
56
+ // The prefix is a whole command from the distro's point of view — program,
57
+ // profile, `--` — because the transport inserts it between
58
+ // `wsl.exe --exec` and the `sh <script>` pair (`lib/agent.js`'s
59
+ // `spawnDefault`). Both confinement sites share that assembly in
60
+ // `lib/bwrap.js`, whose module doc records what happened when they did not.
61
+ argvPrefix: bwrapArgvPrefix({ mode: policy.mode, workspaceRoot: toLinuxPath(policy.workspaceRoot, { distro }) }, { maskWindowsDrive, realTmp }),
62
+ });
63
+ confinedAgents.set(key, agent);
64
+ }
65
+ return agent;
80
66
  }
81
-
82
67
  /** Test hook: forget every confined resident (the plain agent has no factory state). */
83
68
  export function resetConfinedAgents() {
84
- confinedAgents.clear();
69
+ confinedAgents.clear();
85
70
  }
86
-
87
71
  export default confinedAgent;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The error a {@link WslAgent} rejects with when the resident process is out
3
+ * for this host process — the executor's cue to fall back to the one-shot
4
+ * `wsl.exe` path. Deliberately its own class: callers must not confuse
5
+ * "the agent cannot serve" with a failure of the command itself.
6
+ *
7
+ * This is a TypeScript source built to `lib/agent-errors.js`; edit THIS file
8
+ * and run `pnpm run build` — the artifact under `lib/` is generated, and
9
+ * `pnpm test` fails when it drifts.
10
+ *
11
+ * @module dsh-plugin-wsl/agent-errors
12
+ */
13
+ export declare class AgentUnavailableError extends Error {
14
+ /**
15
+ * @param message - why the agent is out, for the log.
16
+ */
17
+ constructor(message: string);
18
+ }
19
+ /**
20
+ * The request's workdir could not be entered inside the distro. Carries the
21
+ * Linux path so a caller can synthesize the same diagnostic the one-shot
22
+ * `wsl.exe` path produces from the relay's `chdir(...) failed` text.
23
+ */
24
+ export declare class CwdError extends Error {
25
+ /** The path that could not be entered, in the distro's own spelling. */
26
+ linuxPath: string;
27
+ /**
28
+ * @param linuxPath - the path that could not be entered.
29
+ */
30
+ constructor(linuxPath: string);
31
+ }
@@ -4,30 +4,33 @@
4
4
  * `wsl.exe` path. Deliberately its own class: callers must not confuse
5
5
  * "the agent cannot serve" with a failure of the command itself.
6
6
  *
7
+ * This is a TypeScript source built to `lib/agent-errors.js`; edit THIS file
8
+ * and run `pnpm run build` — the artifact under `lib/` is generated, and
9
+ * `pnpm test` fails when it drifts.
10
+ *
7
11
  * @module dsh-plugin-wsl/agent-errors
8
12
  */
9
13
  export class AgentUnavailableError extends Error {
10
- /**
11
- * @param {string} message - why the agent is out, for the log.
12
- */
13
- constructor(message) {
14
- super(message);
15
- this.name = "AgentUnavailableError";
16
- }
14
+ /**
15
+ * @param message - why the agent is out, for the log.
16
+ */
17
+ constructor(message) {
18
+ super(message);
19
+ this.name = "AgentUnavailableError";
20
+ }
17
21
  }
18
-
19
22
  /**
20
23
  * The request's workdir could not be entered inside the distro. Carries the
21
24
  * Linux path so a caller can synthesize the same diagnostic the one-shot
22
25
  * `wsl.exe` path produces from the relay's `chdir(...) failed` text.
23
26
  */
24
27
  export class CwdError extends Error {
25
- /**
26
- * @param {string} linuxPath - the path that could not be entered.
27
- */
28
- constructor(linuxPath) {
29
- super(`could not enter working directory "${linuxPath}"`);
30
- this.name = "CwdError";
31
- this.linuxPath = linuxPath;
32
- }
28
+ /**
29
+ * @param linuxPath - the path that could not be entered.
30
+ */
31
+ constructor(linuxPath) {
32
+ super(`could not enter working directory "${linuxPath}"`);
33
+ this.name = "CwdError";
34
+ this.linuxPath = linuxPath;
35
+ }
33
36
  }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * An agent-backed implementation of the shell execution handle.
3
+ *
4
+ * `LocalBashExecutor.executeArgv` builds its handle around a subprocess spawn:
5
+ * live output readers with byte offsets, spill files beyond the collect
6
+ * budget, a deadline that kills the Windows process tree. This module builds
7
+ * the SAME handle shape around one agent `exec` — the shape `dsh-tool-bash`
8
+ * consumes, so the tool layer cannot tell which path ran. The deliberate
9
+ * deviations, documented here rather than hidden:
10
+ *
11
+ * - **No spill files; the capture cap is the agent's.** The EXEC frame
12
+ * carries the caller's per-stream budget and the AGENT cuts stdout and
13
+ * stderr at it, so a chatty command costs bounded distro memory AND bounded
14
+ * host memory; the RES line's flags say which streams were cut and the
15
+ * handle reports them as `truncated: true`. What the budget spares is the
16
+ * upstream spill machinery: the capture is still whole-in-memory, just
17
+ * whole-at-a-bounded-size.
18
+ * - **Killed commands report `SIGTERM`**, not a raw exit status: the
19
+ * in-distro timeout and `kill()` both mean "we stopped it", and upstream
20
+ * reports its own kills the same way.
21
+ *
22
+ * Pure shaping over an injected runner, so the handle semantics are
23
+ * unit-testable without a distro.
24
+ *
25
+ * This is a TypeScript source built to `lib/agent-exec.js`; edit THIS file and
26
+ * run `pnpm run build` — the artifact under `lib/` is generated, and `pnpm test`
27
+ * fails when it drifts.
28
+ *
29
+ * @module dsh-plugin-wsl/agent-exec
30
+ */
31
+ /** The request one agent `exec` carries. */
32
+ export interface AgentExecRequest {
33
+ /** Linux workdir. */
34
+ cwd: string;
35
+ /** The distro-side argv. */
36
+ argv: string[];
37
+ /** Whole-command budget. */
38
+ timeoutMs?: number;
39
+ /** Extra environment for this request only. */
40
+ env?: Record<string, string>;
41
+ /** Per-stream capture ceiling. */
42
+ maxOutputBytes?: number;
43
+ /** The caller's cancellation. */
44
+ signal?: AbortSignal;
45
+ }
46
+ /** What an agent `exec` resolves with. */
47
+ export interface AgentExecResult {
48
+ /** The command's exit code. */
49
+ exitCode: number;
50
+ /** The captured stdout. */
51
+ stdout: Buffer;
52
+ /** The captured stderr. */
53
+ stderr: Buffer;
54
+ /** The agent's per-stream capture-cap flags. */
55
+ truncated?: {
56
+ stdout: boolean;
57
+ stderr: boolean;
58
+ };
59
+ }
60
+ /** The {@link WslAgent}-like runner this module drives. */
61
+ export interface AgentExecRunner {
62
+ /** Run one command. */
63
+ exec: (request: AgentExecRequest) => Promise<AgentExecResult>;
64
+ }
65
+ /** How the exec promise settled: the result, or the failure to propagate. */
66
+ export type SettledExec = {
67
+ ok: true;
68
+ result: AgentExecResult;
69
+ error?: undefined;
70
+ } | {
71
+ ok: false;
72
+ result?: undefined;
73
+ error: unknown;
74
+ };
75
+ /** The memory-reader slice of one output stream. */
76
+ export interface OutputReader {
77
+ /** Read the stream from a byte offset. */
78
+ readFrom: (fromByte: number) => {
79
+ text: string;
80
+ lossy: boolean;
81
+ nextOffset: number;
82
+ };
83
+ }
84
+ /** One settled foreground command, the shape `dsh-tool-bash` consumes. */
85
+ export interface AgentExecutionHandle {
86
+ /** The lifecycle state. */
87
+ status: "running" | "completed" | "killed";
88
+ /** The exit code; null until settled or when killed. */
89
+ exitCode: number | null;
90
+ /** The terminating signal; null on a clean exit. */
91
+ signal: string | null;
92
+ /** The live output readers. */
93
+ observed: {
94
+ stdout: OutputReader;
95
+ stderr: OutputReader;
96
+ };
97
+ /** Settles when the command settles. */
98
+ done: Promise<void>;
99
+ /** The incremental delta reader. */
100
+ readOutput: () => {
101
+ delta: string;
102
+ lossy: boolean;
103
+ };
104
+ /** Stop the command; true on the first call only. */
105
+ kill: () => boolean;
106
+ /** The canonical bash result. */
107
+ result: () => Promise<{
108
+ exitCode: number | null;
109
+ signal: string | null;
110
+ timedOut: boolean;
111
+ aborted: boolean;
112
+ timeoutMs: number;
113
+ stdout: {
114
+ text: string;
115
+ lossy: boolean;
116
+ truncated: boolean;
117
+ };
118
+ stderr: {
119
+ text: string;
120
+ lossy: boolean;
121
+ truncated: boolean;
122
+ };
123
+ }>;
124
+ }
125
+ /**
126
+ * Run one command through the agent and present it as an execution handle.
127
+ *
128
+ * @param request - the execution request.
129
+ * @returns the execution handle (`status`, `exitCode`, `signal`, `observed`,
130
+ * `done`, `readOutput`, `kill`, `result`).
131
+ */
132
+ export declare function agentExecutionHandle({ agent, cwd, argv, timeoutMs, maxOutputBytes, signal, onStarted }: {
133
+ /**
134
+ * The {@link WslAgent}-like runner: `exec` accepting
135
+ * `{cwd, argv, timeoutMs, signal}` and resolving `{exitCode, stdout, stderr}`;
136
+ * throws `AgentUnavailableError` when out.
137
+ */
138
+ agent: AgentExecRunner;
139
+ /** Linux workdir. */
140
+ cwd: string;
141
+ /** The distro-side argv, bwrap-wrapped when confined. */
142
+ argv: string[];
143
+ /**
144
+ * Whole-command budget; the agent TERMs at the ceiling second and KILLs
145
+ * after its grace.
146
+ */
147
+ timeoutMs?: number;
148
+ /**
149
+ * Per-stream capture ceiling the agent enforces; the handle reports a cut
150
+ * stream as `truncated: true`.
151
+ */
152
+ maxOutputBytes?: number;
153
+ /** The caller's cancellation. */
154
+ signal?: AbortSignal;
155
+ /**
156
+ * Provider-facts hook, called synchronously before the handle can settle.
157
+ */
158
+ onStarted?: (proc: object) => void;
159
+ }): AgentExecutionHandle;