dsh-pwsh-plugin 0.2.14 → 0.2.17

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/README.zh-CN.md CHANGED
@@ -60,7 +60,7 @@
60
60
  $dsh = '<桌面端安装目录>\resources\runtime\cli\bin\dsh.cmd'
61
61
  $env:DSH_HOME = '<DSH_HOME>' # 桌面 App 自己用的那个数据根
62
62
 
63
- # <version> 是任意已发布版本;最新的那个就是插件自己会装的那个
63
+ # <version> 是任意已发布版本;最新的那个就是插件页会给出的那个
64
64
  & $dsh plugin --profile desktop add 'dsh-pwsh-plugin@<version>'
65
65
  # 或者从本地检出装
66
66
  & $dsh plugin --profile desktop add 'file:<检出目录>'
@@ -104,7 +104,7 @@ $env:DSH_HOME = '<DSH_HOME>' # 桌面 App 自己用的那个数据根
104
104
 
105
105
  ## 工具接口
106
106
 
107
- `pwsh_run` 跑脚本;`pwsh_run_update` 查更新、必要时安装本插件的新版本 —— 见[自动更新检查](#自动更新检查)。
107
+ `pwsh_run` 跑脚本。版本由 DSH 自己管 —— 见[更新](#更新)。
108
108
 
109
109
  | 参数 | 必填 | 说明 |
110
110
  |---|---|---|
@@ -127,9 +127,6 @@ $env:DSH_HOME = '<DSH_HOME>' # 桌面 App 自己用的那个数据根
127
127
  | `maxSessions` | `4` | 同时存活的会话上限(每个 agent 工作区一个)。 |
128
128
  | `maxOutputBytes` | `8388608` | 单次调用的输出收集上限(8MB)。截断会明确报出来,不静默。 |
129
129
  | `allowUnconfined` | `false` | `true` = 沙箱策略服务缺席时也起会话。 |
130
- | `updateCheck` | `true` | 定期查有没有新发布的版本。查与装分开:这里只查,装由 `pwsh_run_update` 与自动更新做。 |
131
- | `autoUpdate` | `true` | 自己装上更新,宿主有 HMR 时**不需要重启**即生效。 |
132
- | `autoUpdateDelayMs` | `90000` | 自动检查前的等待,避免与启动流程抢 pnpm。 |
133
130
 
134
131
  `allowUnconfined` 是唯一一条「明知故犯」的开关。正常部署下 `ctx.sandboxPolicy` 一定在;缺席说明组合
135
132
  不完整,这时插件**拒绝启动**,而不是悄悄跑一个不受约束的会话。
@@ -160,81 +157,29 @@ session,**静默退回部署默认值**(mode + 配置里的 `workspaceRoot`
160
157
 
161
158
  ## 更新
162
159
 
163
- 默认情况下插件自己升级:启动 90 秒后查一次 npm registry 的 `latest` 标签,之后每 12 小时一次;查到
164
- 更高的版本就直接装上。宿主跑着 DSH 的 HMR 服务时,**装上的那一刻新代码就已经在跑** —— 不用重启,不用弹窗,用户什么
165
- 都不用做。
160
+ 本插件**没有自己的更新器**。它就是一个普通的 registry 包,所以**版本交给 DSH 管**:「插件」页面会列出它、
161
+ 标出有没有更新的版本、并按与其它插件相同的 `listVersions` / `updateBundle` 路径装上。
166
162
 
167
- | 方式 | 行为 |
168
- |---|---|
169
- | 自动(`autoUpdate: true`,默认) | 查 → 装 → 就地生效。装失败按指数退避重试(5 分钟起、翻倍、封顶 2 小时);装成功过的版本不再重试。**有调用在跑时绝不装** —— 3 分钟后再看。宿主**没有** HMR 时它什么都不装,见下。 |
170
- | `pwsh_run_update` 的 `action: "check"` | 只读一次 registry,报出 `state` / `running` / `newest release` / `install spec`。不装任何东西。 |
171
- | `pwsh_run_update` 的 `action: "install"` | 同一条安装路径,只是由人按下去而不是由时钟按下去。有调用在跑时报 `install: postponed`;预检判定「装了也没用」时报 `install: restart-required` / `install: broken-release`(见下)。 |
172
-
173
- 安装走 `ctx.get('pluginManager')` —— 也就是「插件」页面背后那个服务 —— 由它跑 pnpm,失败时回滚
174
- `package.json`。本插件**从不自己改自己的文件**:它是 profile 的 bundle 依赖,版本钉在
175
- `package.json` 与 `pnpm-lock.yaml` 里,绕开包管理器写文件会立刻让 lockfile 与磁盘对不上。
176
-
177
- **为什么能不重启就生效。** 插件的 `apply` 以及它起的定时器,都跑在 HMR 服务的事务上下文里
178
- (`AsyncLocalStorage`)。在那里直接调 `installBundle`,pnpm 会跑完、磁盘会换成新版,紧接着的重组合
179
- 却被拒掉(`HMR transactions cannot be nested`),于是停在最难看清的状态:磁盘是新的、进程里是旧的。
180
- 先把上下文清空(`hmr.executing.run(undefined, …)`)再调,重组合才会真的发生;而磁盘上新装的文件与已
181
- 加载的模块已经对不上,HMR 于是驱逐旧模块、import 新模块。实测:从触发到新代码应答 137 ms。
182
-
183
- **为什么先看一眼再装。** DSH 的插件管理器在跑 pnpm **之前**就会拒掉一类安装:要装的规格与 profile
184
- 里为该包记下的规格完全一致 —— 也就是 `already-installed`。有两种情形会走到这一步,而这两种本来就不该
185
- 发起安装:
186
-
187
- - 这个版本已经装好了(终端里的 `dsh plugin add`、或者「插件」页面),只是进程还没重启。文件是新的,
188
- 缺的只是一次重启。
189
- - 发版时忘了把 `package.json` 里的 `version` 抬上去。标签变了、声明的版本没变,于是每一次尝试装的都是
190
- 同一份字节、被同一句话拒掉 —— 一个永不结束、也从不吭声的循环。
191
-
192
- 所以安装之前,更新器先比对手上已有的三个事实:profile 记下的规格、已装包自己声明的版本、运行中的进程
193
- 报出的版本。磁盘上已经是新版的报 `install: restart-required`;标签存在但包内版本没抬的报
194
- `install: broken-release` 并直说原因。`scripts/release-check.mjs` 在推标签**之前**拦第二种,运行时这道
195
- 预检则在**之后**、在每一台试图升级的机器上再拦一次。
196
-
197
- **为什么非要等安静。** 安装会替换插件,而停掉本插件就要 dispose 掉它所有的常驻会话。若此刻有脚本正在
198
- 跑,那与直接断电无异:输出写了一半、调用结束时拿不到状态、会话状态全失。所以更新器先问插件「现在有没有
199
- 调用在跑」(与工具本身用同一套同步的检查加置位,中途塞不进新调用),有就整轮推迟。安装进行中到达的调用
200
- 则等它装完,而不是跟它抢。
201
-
202
- 宿主**没有** HMR 时,自动路径什么都不做。那种环境装下去只会「磁盘是新版、进程是旧版」—— 需要重启的
203
- 自动更新不算更新,只是一个会说谎的版本号。(`pwsh_run_update` 的 `install` 在这种情况下照样装,因为
204
- 是人按的、人能决定什么时候重启;结果里会诚实地写 `restart-required`。)
205
- 把自动路径关掉(`autoUpdate: false`)则在任何宿主上都回到旧行为:只查不装,在下次 `pwsh_run` 输出末尾
206
- 挂一行,让人来定。
207
-
208
- 若 profile 关掉了 `plugin-manager` 服务,`install` 会报 `no-manager`,改用终端升级:
163
+ 这条路成立的前提是 profile 里记的是裸 registry 规格(`"dsh-pwsh-plugin": "0.2.14"`)。换成别的来源就更新不了:
164
+ `github:` / `file:` / git range、不是本 profile 的第三方依赖、或者本身不声明 semver 版本,DSH 都会拒。
209
165
 
210
- ```powershell
211
- $env:DSH_HOME = '<DSH_HOME>'
212
- & '<桌面端安装目录>\resources\runtime\cli\bin\dsh.cmd' plugin --profile desktop add 'dsh-pwsh-plugin@<version>'
213
- ```
214
-
215
- 注意 DSH 的默认组合关掉的是**工具** `plugin_manager`,而桌面 profile 上的**服务** `plugin-manager`
216
- 是活的 —— 两者不是一回事,这里只用服务。
166
+ 两件值得知道的事:
217
167
 
218
- 发布源就是这个包在 npm registry 上的条目:`GET https://registry.npmjs.org/dsh-pwsh-plugin`,请求头带
219
- `accept: application/vnd.npm.install-v1+json`。它的 `dist-tags.latest` 就是要推的版本,必须是纯
220
- `X.Y.Z`;被标了 `deprecated` 的 `latest` 永不推送;没有 `dist-tags` 时回落到版本清单里的最高正式版。
221
- 返回 `404` 时报的是「not published on <registry>」而不是「没有新版本」—— 这两件事不同,只有一件是临时的。
168
+ - **`minimumReleaseAge` 照样生效。** pnpm 的供应链策略默认 1440 分钟,今天发布的版本今天解析不到 ——
169
+ 点「更新」会装到上一个版本,页面上会写清实际装的是哪个。这是策略在起作用,不是出错。
170
+ - **版本号与标签必须对齐。** `package.json` 是本仓库唯一写死的版本;打了标签却没抬版本号,每一次更新装的
171
+ 都是同一份字节。`scripts/release-check.mjs` 会在 workflow 里先把这种情况拦下来。
222
172
 
223
- 安装规格用 registry 形式 `dsh-pwsh-plugin@X.Y.Z`,所以插件请求的字符串与 `dsh plugin add` 接受的是同一个。
224
-
225
- | 环境变量 | 作用 |
226
- |---|---|
227
- | `DSH_PWSH_UPDATE_REGISTRY` | registry 基址,默认 `https://registry.npmjs.org`。可指向镜像,或指向本机服务做预演 |
228
- | `DSH_PWSH_UPDATE_DELAY_MS` | 首次检查延迟,默认 5000 |
173
+ 手动更新与 DSH 做的是同一个操作:
229
174
 
230
175
  ```powershell
231
- & $dsh plugin --profile desktop add 'dsh-pwsh-plugin@<version>'
176
+ $env:DSH_HOME = '<DSH_HOME>'
177
+ & '<桌面端安装目录>\resources\runtime\cli\bin\dsh.cmd' plugin --profile desktop add 'dsh-pwsh-plugin@<version>'
232
178
  ```
233
179
 
234
- 手动安装与插件自己做的事是同一条路径,同样由 pnpm 施加同样的策略。
180
+ **更新会换掉常驻会话。** 停掉插件就要 dispose 掉它所有的常驻会话,所以升级后下一次调用是重建的 pwsh 进程:
181
+ 只存在那个会话里的变量、`Set-Location`、已导入模块都会丢。不要把它当作跨升级保存状态的地方。
235
182
 
236
- 查询失败不会抛进工具调用:记 `state: 'failed'`,经 `ctx.logger` 留一行,先前查到过的 `latest` 保留成
237
- `stale: 'available'` —— 一次网络抖动不该让已知的新版本消失。
238
183
 
239
184
  ## `llrun` 垫片
240
185
 
@@ -246,6 +191,11 @@ $env:DSH_HOME = '<DSH_HOME>'
246
191
  不需要手动装:二进制随包发布,探测候选的最后一个就是它,所以一次 `plugin add` 就够。由于发布包在
247
192
  Windows 上打,那份文件到手时是 `0644` —— 探测发现它属于当前用户却不可执行时,会自己补上执行位再试。
248
193
 
194
+ 同一行还会注入 `SSH_AUTH_SOCK`。`bash -s` 是非交互的,`~/.bashrc` 末尾那句 `eval $(wsl2-ssh-agent)`
195
+ 永远执行不到,变量因此是空的 —— 围栏不是原因,显式给出的 socket 在围栏内一样能用。现在每条命令前
196
+ 都带一段三行探测:`$HOME/.ssh/wsl2-ssh-agent.sock` 是真 socket 且当前没设过值时导出它;
197
+ `sshAgentSocket: false` 关掉。agent 守护进程本身必须在**围栏外**(它要 `/init`),通常做成 systemd 用户单元。
198
+
249
199
  完整说明、`-r` 档语义表、54 项验收矩阵与接入方式:**[docs/llrun.md](docs/llrun.md)**。想在自己机器上
250
200
  复验就 `bash wsl/acceptance.sh`(要在未被 `custom-bash` 包裹的 WSL 里跑:套第二层围栏会让四项断言
251
201
  因错误的原因变红)—— 它的每个路径都从自身位置推导,并在结尾复核磁盘有没有泄漏。
@@ -261,7 +211,6 @@ Windows 上打,那份文件到手时是 `0644` —— 探测发现它属于当
261
211
  | `refusing to start: read-only mode` | 预期行为。read-only 会话里用内置 `pwsh` 工具。 |
262
212
  | `could not confine the session: SetNamedSecurityInfoW failed (Win32 5)` | 工作区根目录的 DACL 没给你 `WRITE_DAC` + `WRITE_OWNER`(沙箱要在那里写 DACL 与完整性标签)。全控制权限两样都满足,`Modify` 不够。执行 `icacls "<根目录>" /grant "%USERNAME%":(OI)(CI)F`,或从用户目录下的目录起会话。 |
263
213
  | 内置工具能写的地方本工具写不进 | 确认 `session` 真的传到了 `sandboxPolicy.resolve`(见「沙箱」);漏传会把边界静默挪到 profile 目录。 |
264
- | 永远看不到更新提示 | 源环境变量必须在 **Windows** 侧设;WSL 不透传 `DSH_*`。 |
265
214
  | 输出顺序看着不对 | stdout 与 stderr 按类型合并,不按时间。自己用 `2>&1` 合。 |
266
215
  | `exit N` 的退出码拿不到 | runspace 语义 —— 用 `Write-Output` 或读 `$LASTEXITCODE`。 |
267
216
  | 调用结束后子进程还占着文件 | 会话持有 `Process` 对象。等 `idleMs`,或者用一次超时强制重建。 |
@@ -269,44 +218,14 @@ Windows 上打,那份文件到手时是 `0644` —— 探测发现它属于当
269
218
 
270
219
  ## 已知边界
271
220
 
272
- - **不支持 `read-only` 模式**(见上),用内置 `pwsh`。
273
- - **沙箱策略服务缺席时拒绝启动**,除非 `allowUnconfined: true`。
274
- - **超时会杀掉整个会话**(连带回收子进程树)。状态丢失,下次调用自动重建。
275
- - **`exit N` 的退出码不可观测**(runspace 语义)。
276
- - **stdout / stderr 的逐行时序不保留** —— 全部流经 `*>&1` 合并后按类型打标。
277
- - **`[Console]::Error.WriteLine` 会被吞掉**(服务端为防协议污染把 `Console.Error` 指向内存流)。
278
- 子进程的 stderr、`Write-Error`、`throw` 都正常回来。
279
- - **子进程的 std handle 已与协议流断开。** `Console::SetIn/SetOut` 只改 .NET 层,服务端还改写了三个 Win32 槽位:
280
- stdin 接 `NUL`,stdout / stderr 接一个带 `FILE_FLAG_DELETE_ON_CLOSE` 的临时文件、每次调用前截回零。
281
- 于是子进程往继承来的 stdout 写东西不会再伪造出协议行(实测:子进程一句 `Write-Output '<<<BYE>>>'`
282
- 曾让会话当场死亡),读继承来的 stdin 也只会读到 EOF,而不是调用方的请求。
283
- 替上去的句柄必须是**可继承**的(`SECURITY_ATTRIBUTES.bInheritHandle`):用 `NULL` 安全描述符建的句柄
284
- 只存在于本进程,子进程按 `STARTUPINFO.hStdInput` 拿到的只是一个在那边没有意义的句柄**编号** ——
285
- `GetFileType` 报 `UNKNOWN`,几十次里还有一次那个编号被别的对象复用,读出来是别人的字节。
286
- stdout **故意不接 `NUL`**:那是字符设备,PowerShell 会认为输出没被重定向,沙箱拒绝的报错会变成一句
287
- 没用的 `StandardOutputEncoding` 提示。临时目录不可写时退回 `NUL`,并在第一次调用的警告里说明一次。
288
- - **会话是串行的**:并发调用排成队列逐个执行(不会串输出,但也没有并行加速)。
289
- - **不依赖 WSL。** `pwsh_run` 全程不碰 WSL,只有 `wsl/custom-bash.mjs` 才碰。没有可用的 `bashPath` 时,
290
- 该预设照样加载成功(可执行文件是惰性解析的),`bash` 工具每次调用报
291
- `command "<路径>" is not an executable file` —— 清晰失败,不会静默降级。默认值是裸名 `bash`;
292
- PATH 解不出来时,Windows 上会再试 `C:\Windows\System32\bash.exe`(同一个 WSL 垫片)才放弃。
293
- 设这道后备是因为进程可能继承到一份早于 PATH 修改的环境块,里面没有 `System32` ——
294
- 没有它就等于整个工具作废。显式写出的绝对 `bashPath` 只认那一个:写错了就该响亮地失败,
295
- 不该被悄悄换成另一份。
296
- - **UAC 开启时的行为与本机不同。** 本机实测环境是 UAC 关闭、进程提权。UAC 开着时会话是标准用户,
297
- 此时 ACL 沙箱要求工作区根目录的 DACL 给你 **`WRITE_DAC` 与 `WRITE_OWNER`** —— 全控制两者都满足
298
- (它写的完整性标签在 SACL 里)。用户目录下的目录天然继承这两样;在盘符根新建的目录只继承
299
- `Modify`,单靠 `Modify` 会以 `SetNamedSecurityInfoW failed (Win32 5)` 失败。属主不是决定因素:
300
- `icacls <根目录> /grant "%USERNAME%":(OI)(CI)F` 能救。届时是 fail-closed 报错,不会退化成无边界。
301
- - 不支持交互式程序与 TTY。
302
- - **升级会换掉会话,但绝不打断正在跑的调用。** 替换插件会先把它停掉,而停掉本插件就会销毁它的会话 ——
303
- 所以真正装上新版的那一次,常驻 pwsh 进程已经不在了,下次调用重新拉起。只存在于该会话里的东西(变量、
304
- `Set-Location` 过的目录、import 进来的模块)随之丢失。实测 v0.2.3 → v0.2.4 的一次安装:`$PID` 由
305
- 24820 变成 15632,安装前设的 `$global:` 变量读回来是空的。更新器**不会**做的事是打断在跑的活:
306
- 自动循环与 `pwsh_run_update` 的 `install` 都会先问插件「现在有没有调用在跑」,有就整轮推迟
307
- (循环 3 分钟后再看,工具报 `install: postponed` 且不动 profile),直到会话安静下来。安装进行中到达的
308
- 调用会先等它装完,而不是建一个马上要被销毁的会话。版本比会话状态值钱,但这并不等于「什么都没发生」
309
- —— 别把不可重建的状态放在里面跨过升级。
221
+ 完整清单(会话语义、已断开的子进程 std handle、UAC 差异、升级对会话的影响)见
222
+ **[docs/runtime-notes.zh-CN.md](docs/runtime-notes.zh-CN.md)**。要点:
223
+
224
+ - 不支持 `read-only`,那里用内置 `pwsh`。
225
+ - 超时会杀掉整个会话并回收其子进程树。
226
+ - `exit N` 退出码与 stdout/stderr 的逐行时序都不可观测。
227
+ - 会话是串行的,不提供 TTY。
228
+ - 升级会换掉会话,只存在于其中的东西随之丢失。
310
229
 
311
230
  ## 开发
312
231
 
@@ -315,9 +234,6 @@ plugin/
315
234
  index.mjs 工具注册、沙箱接线、超时
316
235
  session.mjs 会话对象:分帧、队列、base64、启动 argv
317
236
  session-server.ps1 PowerShell 侧:runspace、流打标、哨兵
318
- update-check.mjs registry 查询与版本比较
319
- update-apply.mjs 把钉住的规格交给 pluginManager,不自己下载
320
- auto-update.mjs 自动路径:在 HMR 事务之外安装,失败退避
321
237
  test/
322
238
  support.mjs 定位 DSH 安装;Windows 路径转 /mnt 形式
323
239
  itest.mjs 协议与会话本体
@@ -326,9 +242,6 @@ test/
326
242
  sandboxtest.mjs 真 LocalSandboxProvider,边界 + fail-closed 用例
327
243
  bashtest.mjs custom-bash 围栏分诊(mock runtime)
328
244
  bash-e2e.mjs custom-bash 端到端(真 runtime + 真 llrun)
329
- updatetest.mjs 更新检查与安装结果(本地 git 仓库)
330
- autoupdatetest.mjs 自动更新循环、退避、空闲闸门,以及它依赖的 HMR 上下文逃逸
331
- wiringtest.mjs 更新提示真的接到工具输出末尾、安装要等会话空闲
332
245
  denialtest.mjs 沙箱拒绝提示只在该触发的报错上触发
333
246
  metatest.mjs 插件页元数据:locale、icon,以及页面到底读哪份文档
334
247
  wsl/
@@ -336,6 +249,7 @@ wsl/
336
249
  custom-bash.mjs bundle 接进组合的 bash 工具替代实现(仅 Windows)
337
250
  acceptance.sh 54 项验收矩阵
338
251
  docs/llrun.md 垫片的完整说明
252
+ docs/runtime-notes.zh-CN.md 完整已知边界清单,从这一页挪出去
339
253
  FINDINGS.md 全部实测数据与设计理由
340
254
  ```
341
255
 
@@ -345,16 +259,13 @@ PWSH_BIN='pwsh' node test/itest.mjs # 17:协议与会话本体
345
259
  PWSH_BIN='pwsh' node test/plugintest.mjs # 15:插件完整路径
346
260
  node test/e2e-real-subprocess.mjs # 8:真 DSH subprocess
347
261
  node test/sandboxtest.mjs # 7:真沙箱边界 + fail-closed
348
- node test/bashtest.mjs # 25:custom-bash 围栏分诊、bash 后备、执行位自修复
262
+ node test/bashtest.mjs # 31:custom-bash 围栏分诊、bash 后备、执行位自修复、SSH agent
349
263
  node test/bash-e2e.mjs # 11:custom-bash 端到端
350
- node test/updatetest.mjs # 96:更新检查、安装结果、安装前预检(只碰本地 git)
351
- node test/autoupdatetest.mjs # 38:自动安装、退避、空闲闸门、安装前预检、HMR 上下文逃逸
352
- node test/wiringtest.mjs # 21:更新提示接线、空闲闸门、安装前预检的接线
353
264
  node test/denialtest.mjs # 11:沙箱拒绝提示(不起子进程,哪儿都能跑)
354
265
  node test/metatest.mjs # 23:插件页元数据(不起子进程,哪儿都能跑)
355
266
  ```
356
267
 
357
- 十一份脚本合计 **305 项**,都不需要 DSH 在跑 —— 只要 node 24、PowerShell 7,以及一个能借
268
+ 八份脚本合计 **123 项**,都不需要 DSH 在跑 —— 只要 node 24、PowerShell 7,以及一个能借
358
269
  `node_modules` 的 DSH 安装。需要 DSH 运行时的测试会自动找(`DSH_MODULES`、`DSH_MODULES_ASAR`,
359
270
  或从工作目录逐级向上),找不到时打一行 `SKIP` 提示而不是失败。
360
271
 
@@ -366,9 +277,8 @@ runner 用自己那份短命的 GitHub 身份换一个只对「这个仓库 +
366
277
  `NPM_TOKEN`,也不该加。两个连带后果值得记住:workflow 的文件名是信任契约的一部分(改名后每次发布都会
367
278
  403,直到在 npm 上改设置);provenance 在那里是强制的,所以 tarball 会带上「由哪个提交构建」的签名声明。
368
279
 
369
- `package.json` 里的 version 是本仓库唯一写死的东西,而更新检查拿它跟 registry 上的已发布版本比。
370
- 不升版本就发版,检查会永远报有新版:装上去的文件读到的还是旧数字,于是每轮都请求同一个版本,
371
- 退避一路爬到上限。
280
+ `package.json` 里的 version 是本仓库唯一写死的东西,DSH 拿它跟 registry 上的已发布版本比。
281
+ 不升版本就发版,每次更新装的都是同一份字节:页面一直提示有新版,pnpm 一直装回旧的。
372
282
  `npm version` 一次完成「改版本 + 提交 + 打 annotated tag」,所以这三者不会漂开:
373
283
 
374
284
  ```powershell
@@ -385,4 +295,4 @@ npm 要用 Windows 侧那份。WSL 里 `/usr/local/bin/npm` 指向 bun 的软链
385
295
 
386
296
  ## 许可
387
297
 
388
- MIT —— 见 [LICENSE](LICENSE)。
298
+ MIT —— 见 [LICENSE](LICENSE)。
package/cordis.patch.yml CHANGED
@@ -25,3 +25,7 @@
25
25
  disabled: !!js process.platform !== 'win32'
26
26
  config:
27
27
  timeoutMs: 300000
28
+ # 非交互 shell 不读 ~/.bashrc,用户终端里能用的 SSH_AUTH_SOCK 在这里是空的。
29
+ # 这段探测让 bash 工具也能用 Windows 侧的 ssh-agent(socket 由 wsl2-ssh-agent 提供,
30
+ # 守护进程要另行保活,例如一个 systemd 用户单元)。设 false 关掉。
31
+ sshAgentSocket: $HOME/.ssh/wsl2-ssh-agent.sock
package/docs/llrun.md CHANGED
@@ -184,6 +184,7 @@ Configuration:
184
184
  config:
185
185
  llrunPath: '/mnt/c/Users/<you>/.local/bin/llrun' # omit to probe the candidates
186
186
  fence: true # false = no fence (not recommended)
187
+ sshAgentSocket: '$HOME/.ssh/wsl2-ssh-agent.sock' # false = do not inject
187
188
  ```
188
189
 
189
190
  `llrunPath` is only for overriding. Left out, the plugin probes `$LLRUN` / `$DSH_LLRUN`,
@@ -192,8 +193,27 @@ to `custom-bash.mjs` inside the package — first executable wins, and the resul
192
193
  install works without a restart. Yours come first: a system-level `llrun` may be newer, the packaged one only
193
194
  guarantees that the fence is there at all.
194
195
 
195
- Tests: `node test/bashtest.mjs` (25, mock runtime) + `node test/bash-e2e.mjs` (11, real runtime + real llrun).
196
+ Tests: `node test/bashtest.mjs` (31, mock runtime) + `node test/bash-e2e.mjs` (11, real runtime + real llrun).
196
197
  Three of the 25 pin the executable-bit repair: a `0644` copy is made executable and the call succeeds, an
197
198
  unusable copy is named in the error, and an unusable candidate earlier in the list does not shadow a usable
198
199
  one later. They only assert the mode on POSIX hosts — NTFS reports every file as `0666`, so there the
199
200
  assertion is that the call still works.
201
+
202
+ ### The SSH agent socket
203
+
204
+ `bash -s` is a **non-interactive** shell: it never reads `~/.bashrc`, so the usual
205
+ `eval $(wsl2-ssh-agent)` line at the bottom of that file never runs and `SSH_AUTH_SOCK` stays empty. The
206
+ fence is not the cause — a socket given explicitly works inside it just as well as outside.
207
+
208
+ `custom-bash` therefore prepends a three-line probe to every command:
209
+
210
+ ```sh
211
+ if [ -z "$SSH_AUTH_SOCK" ] && [ -S "$HOME/.ssh/wsl2-ssh-agent.sock" ]; then
212
+ export SSH_AUTH_SOCK="$HOME/.ssh/wsl2-ssh-agent.sock"
213
+ fi
214
+ ```
215
+
216
+ It tests `-S` (a real socket, not merely an existing path) and never overwrites a value you set yourself —
217
+ so a script that exports its own socket still wins. `sshAgentSocket: false` turns the injection off; any
218
+ other string replaces the path. The agent itself is still yours to keep running: the fence blocks
219
+ `/init`, so `wsl2-ssh-agent` cannot start **inside** it and must be a user service outside.
@@ -0,0 +1,57 @@
1
+ # Persistent-session runtime notes
2
+
3
+ [Back to the README](../README.md)
4
+
5
+ Implementation details and historical measurements follow. Behavior depends on the host, permissions and PowerShell environment.
6
+
7
+ ## Known limits
8
+
9
+ - **`read-only` mode is unsupported** (see above); use the built-in `pwsh` tool there.
10
+ - **Refuses to start when the sandbox policy service is absent**, unless `allowUnconfined: true`.
11
+ - **A timeout kills the whole session** (and reaps its child process tree). State is lost; the next call
12
+ rebuilds automatically.
13
+ - **`exit N` exit codes are not observable** (runspace semantics).
14
+ - **stdout / stderr interleaving is not preserved** — everything goes through `*>&1` and is tagged by type.
15
+ - **`[Console]::Error.WriteLine` is swallowed** (the server points `Console.Error` at an in-memory stream so
16
+ the protocol cannot be polluted). A child process's stderr, `Write-Error` and `throw` all come back fine.
17
+ - **Child std handles are detached from the protocol.** `Console::SetIn/SetOut` only changes the .NET layer, so
18
+ the server also rewrites the three Win32 slots: stdin goes to `NUL`, stdout and stderr go to a
19
+ `FILE_FLAG_DELETE_ON_CLOSE` temp file truncated before every call. A grandchild that writes to inherited
20
+ stdout can no longer inject a fake protocol line (measured: one `Write-Output '<<<BYE>>>'` from a child used
21
+ to kill the session), and one that reads inherited stdin gets EOF instead of the caller's request.
22
+ The replacement handles are created **inheritable** (`SECURITY_ATTRIBUTES.bInheritHandle`): a handle made with
23
+ a `NULL` security descriptor only exists in this process, so a child that trusts `STARTUPINFO.hStdInput`
24
+ receives a bare handle *number* that means nothing over there — `GetFileType` reports `UNKNOWN`, and once in
25
+ a few dozen launches that number is recycled by an unrelated object and the read returns its bytes.
26
+ stdout deliberately does **not** go to `NUL`: that is a character device, PowerShell then believes its output
27
+ is not redirected and the sandbox denial changes into a useless `StandardOutputEncoding` message.
28
+ If the temp directory is unwritable it falls back to `NUL` and says so once, in a warning on the first call.
29
+ - **A session is serial**: concurrent calls queue up and run one at a time (no interleaved output, but no
30
+ parallel speedup either).
31
+ - **No WSL is required.** `pwsh_run` never touches WSL; only `wsl/custom-bash.mjs` does. Without a usable
32
+ `bashPath`, applying that preset still succeeds (the executable is resolved lazily) and the `bash` tool
33
+ fails per call with `command "<path>" is not an executable file` — a clear failure, not a silent
34
+ downgrade. The default is the bare name `bash`; when PATH cannot resolve it, the Windows build retries
35
+ `C:\Windows\System32\bash.exe` (the same WSL shim) before giving up. That fallback exists because a
36
+ process can inherit an environment block that predates a PATH edit and then lacks `System32`, which
37
+ otherwise costs you the whole tool. An explicit absolute `bashPath` is honoured alone — a typo there
38
+ should fail loudly rather than be silently redirected.
39
+ - **UAC behaves differently from this machine.** These measurements come from a box with UAC off, where the
40
+ process runs elevated. With UAC on, sessions run as a standard user, and the ACL sandbox needs the
41
+ workspace root's DACL to give you **`WRITE_DAC` and `WRITE_OWNER`** — full control satisfies both, because
42
+ the integrity label it writes lives in the SACL. A directory under your user profile inherits exactly
43
+ that; a directory created at a drive root only inherits `Modify`, and `Modify` alone fails with
44
+ `SetNamedSecurityInfoW failed (Win32 5)`. Ownership is not the deciding factor: `icacls <root> /grant
45
+ "%USERNAME%":(OI)(CI)F` fixes a root you do not own. Confinement fails closed with that message instead of
46
+ running unconfined.
47
+ - No interactive programs and no TTY.
48
+ - **An update replaces the session.** This plugin carries no updater of its own; DSH manages its
49
+ version, and replacing a plugin stops it. Stopping this plugin disposes its sessions, so the install that
50
+ lands a new version leaves the persistent pwsh process gone and the next call rebuilds it: anything held
51
+ only in that session (variables, `Set-Location`, imported modules) is lost. Measured across a
52
+ v0.2.3 → v0.2.4 install: `$PID` changed from 24820 to 15632 and a `$global:` variable set before the
53
+ install read back empty.
54
+
55
+ Nothing asks whether a call is in flight. The host performs the update; this plugin does not get a say and
56
+ does not gate it. The version is worth more than the session state, but it is not "nothing happened" — do
57
+ not hold unrecoverable state in a session across an upgrade.
@@ -0,0 +1,45 @@
1
+ # 常驻会话的运行边界
2
+
3
+ [返回 README](../README.zh-CN.md)
4
+
5
+ 以下保留实现细节与历史测量,具体表现取决于宿主、权限和 PowerShell 环境。
6
+
7
+ ## 已知边界
8
+
9
+ - **不支持 `read-only` 模式**(见上),用内置 `pwsh`。
10
+ - **沙箱策略服务缺席时拒绝启动**,除非 `allowUnconfined: true`。
11
+ - **超时会杀掉整个会话**(连带回收子进程树)。状态丢失,下次调用自动重建。
12
+ - **`exit N` 的退出码不可观测**(runspace 语义)。
13
+ - **stdout / stderr 的逐行时序不保留** —— 全部流经 `*>&1` 合并后按类型打标。
14
+ - **`[Console]::Error.WriteLine` 会被吞掉**(服务端为防协议污染把 `Console.Error` 指向内存流)。
15
+ 子进程的 stderr、`Write-Error`、`throw` 都正常回来。
16
+ - **子进程的 std handle 已与协议流断开。** `Console::SetIn/SetOut` 只改 .NET 层,服务端还改写了三个 Win32 槽位:
17
+ stdin 接 `NUL`,stdout / stderr 接一个带 `FILE_FLAG_DELETE_ON_CLOSE` 的临时文件、每次调用前截回零。
18
+ 于是子进程往继承来的 stdout 写东西不会再伪造出协议行(实测:子进程一句 `Write-Output '<<<BYE>>>'`
19
+ 曾让会话当场死亡),读继承来的 stdin 也只会读到 EOF,而不是调用方的请求。
20
+ 替上去的句柄必须是**可继承**的(`SECURITY_ATTRIBUTES.bInheritHandle`):用 `NULL` 安全描述符建的句柄
21
+ 只存在于本进程,子进程按 `STARTUPINFO.hStdInput` 拿到的只是一个在那边没有意义的句柄**编号** ——
22
+ `GetFileType` 报 `UNKNOWN`,几十次里还有一次那个编号被别的对象复用,读出来是别人的字节。
23
+ stdout **故意不接 `NUL`**:那是字符设备,PowerShell 会认为输出没被重定向,沙箱拒绝的报错会变成一句
24
+ 没用的 `StandardOutputEncoding` 提示。临时目录不可写时退回 `NUL`,并在第一次调用的警告里说明一次。
25
+ - **会话是串行的**:并发调用排成队列逐个执行(不会串输出,但也没有并行加速)。
26
+ - **不依赖 WSL。** `pwsh_run` 全程不碰 WSL,只有 `wsl/custom-bash.mjs` 才碰。没有可用的 `bashPath` 时,
27
+ 该预设照样加载成功(可执行文件是惰性解析的),`bash` 工具每次调用报
28
+ `command "<路径>" is not an executable file` —— 清晰失败,不会静默降级。默认值是裸名 `bash`;
29
+ PATH 解不出来时,Windows 上会再试 `C:\Windows\System32\bash.exe`(同一个 WSL 垫片)才放弃。
30
+ 设这道后备是因为进程可能继承到一份早于 PATH 修改的环境块,里面没有 `System32` ——
31
+ 没有它就等于整个工具作废。显式写出的绝对 `bashPath` 只认那一个:写错了就该响亮地失败,
32
+ 不该被悄悄换成另一份。
33
+ - **UAC 开启时的行为与本机不同。** 本机实测环境是 UAC 关闭、进程提权。UAC 开着时会话是标准用户,
34
+ 此时 ACL 沙箱要求工作区根目录的 DACL 给你 **`WRITE_DAC` 与 `WRITE_OWNER`** —— 全控制两者都满足
35
+ (它写的完整性标签在 SACL 里)。用户目录下的目录天然继承这两样;在盘符根新建的目录只继承
36
+ `Modify`,单靠 `Modify` 会以 `SetNamedSecurityInfoW failed (Win32 5)` 失败。属主不是决定因素:
37
+ `icacls <根目录> /grant "%USERNAME%":(OI)(CI)F` 能救。届时是 fail-closed 报错,不会退化成无边界。
38
+ - 不支持交互式程序与 TTY。
39
+ - **升级会换掉会话。** 本插件不带自己的更新器,版本由 DSH 管理;替换插件会先把它停掉,
40
+ 而停掉本插件就会销毁它的会话 —— 所以真正装上新版的那一次,常驻 pwsh 进程已经不在了,下次调用重新拉起。
41
+ 只存在于该会话里的东西(变量、`Set-Location` 过的目录、import 进来的模块)随之丢失。实测
42
+ v0.2.3 → v0.2.4 的一次安装:`$PID` 由 24820 变成 15632,安装前设的 `$global:` 变量读回来是空的。
43
+
44
+ 这里没有人问「现在有没有调用在跑」。更新是宿主执行的,本插件既不参与也不设闸门。版本比会话状态值钱,
45
+ 但这并不等于「什么都没发生」—— 别把不可重建的状态放在里面跨过升级。
@@ -0,0 +1,17 @@
1
+ # 更新机制核对(2026-10-29)
2
+
3
+ 本插件不再带自己的更新器:版本完全交给 DSH 原生更新管理。
4
+
5
+ - [plugin/index.mjs](../plugin/index.mjs) 不注册 `pwsh_run_update`,也没有 `autoUpdate` / `updateCheck` 配置项。
6
+ 工具面只有 `pwsh_run` 一个。
7
+ - `plugin/auto-update.mjs`、`plugin/update-check.mjs`、`plugin/update-apply.mjs` 已删除,
8
+ 对应的 `test/autoupdatetest.mjs`、`test/updatetest.mjs`、`test/wiringtest.mjs` 一并删除。
9
+ - 版本来源是 npm;profile 记录裸 registry 规格(`"dsh-pwsh-plugin": "X.Y.Z"`),
10
+ DSH 的 `listVersions` / `updateBundle` 因此可用 —— 与其余插件同一条路径。
11
+ - DSH 拒绝更新非 registry 来源的 bundle(`github:`、`file:`、git 范围、非本 profile 的第三方依赖、
12
+ 没有 semver 版本号),所以用这几种方式装的实例不会自动更新。
13
+ - pnpm 的 `minimumReleaseAge` 默认 1440 分钟:当天发布的版本当天装不上,页面会说明实际装的是哪个版本。
14
+ - 更新仍然会换掉常驻会话,且没有人检查当前是否有调用在跑。详见
15
+ [运行边界](runtime-notes.zh-CN.md) 与 [英文版](runtime-notes.md)。
16
+
17
+ 详见 [FINDINGS.md](../FINDINGS.md) 第三十四节。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-pwsh-plugin",
3
- "version": "0.2.14",
3
+ "version": "0.2.17",
4
4
  "type": "module",
5
5
  "description": "Persistent pwsh session tool (pwsh_run) for DeepSeek Harness: run PowerShell scripts with zero escaping, no 32K command-line cap, and state that survives across calls.",
6
6
  "icon": "./icon.svg",
@@ -20,7 +20,6 @@
20
20
  "exports": {
21
21
  ".": "./plugin/index.mjs",
22
22
  "./session": "./plugin/session.mjs",
23
- "./update": "./plugin/update-check.mjs",
24
23
  "./wsl/custom-bash.mjs": "./wsl/custom-bash.mjs",
25
24
  "./cordis.patch.yml": "./cordis.patch.yml",
26
25
  "./locale/*.json": "./locale/*.json",
@@ -52,8 +51,7 @@
52
51
  "@deepseek-ai/dsh": ">=0.2.0-rc.2 <0.3.0"
53
52
  },
54
53
  "scripts": {
55
- "test": "node test/itest.mjs && node test/plugintest.mjs && node test/updatetest.mjs && node test/autoupdatetest.mjs && node test/wiringtest.mjs && node test/denialtest.mjs && node test/metatest.mjs",
56
- "test:all": "node test/itest.mjs && node test/plugintest.mjs && node test/e2e-real-subprocess.mjs && node test/sandboxtest.mjs && node test/bashtest.mjs && node test/bash-e2e.mjs && node test/updatetest.mjs && node test/autoupdatetest.mjs && node test/wiringtest.mjs && node test/denialtest.mjs && node test/metatest.mjs",
57
- "test:update": "node test/updatetest.mjs && node test/autoupdatetest.mjs && node test/wiringtest.mjs"
54
+ "test": "node test/itest.mjs && node test/plugintest.mjs && node test/denialtest.mjs && node test/metatest.mjs",
55
+ "test:all": "node test/itest.mjs && node test/plugintest.mjs && node test/e2e-real-subprocess.mjs && node test/sandboxtest.mjs && node test/bashtest.mjs && node test/bash-e2e.mjs && node test/denialtest.mjs && node test/metatest.mjs"
58
56
  }
59
57
  }