dsh-pwsh-plugin 0.2.17 → 0.2.18
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.md +46 -20
- package/README.zh-CN.md +39 -15
- package/docs/dsh-windows-schannel.zh-CN.md +118 -0
- package/docs/pwsh-run-audit.zh-CN.md +183 -0
- package/docs/runtime-notes.md +23 -19
- package/docs/runtime-notes.zh-CN.md +17 -15
- package/package.json +3 -3
- package/plugin/index.mjs +155 -193
- package/plugin/session-server.ps1 +166 -202
- package/plugin/session.mjs +160 -152
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# dsh-pwsh-plugin
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**English** · [简体中文](README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/dsh-pwsh-plugin) [](LICENSE) [](https://github.com/topics/dsh-plugin)
|
|
4
6
|
|
|
5
7
|
A `pwsh_run` tool for DeepSeek Harness: PowerShell script text goes to a **persistent** pwsh 7 process
|
|
6
8
|
over stdin, so nothing is escaped on the way in, there is no 32K command-line cap, the ~400 ms cold start
|
|
@@ -119,19 +121,20 @@ The three files in `plugin/` (`index.mjs`, `session.mjs`, `session-server.ps1`)
|
|
|
119
121
|
|
|
120
122
|
## The tools
|
|
121
123
|
|
|
122
|
-
`pwsh_run` runs a script. Versions are managed by DSH itself — see [
|
|
124
|
+
`pwsh_run` runs a script. Versions are managed by DSH itself — see [Updating](#updating).
|
|
123
125
|
|
|
124
126
|
| Argument | Required | Meaning |
|
|
125
127
|
|---|---|---|
|
|
126
128
|
| `script` | yes | PowerShell script text. Reaches the server **verbatim** — do not escape `$`, do not wrap it in `pwsh -c`, do not add outer quotes. |
|
|
127
129
|
| `workdir` | no | `Set-Location` first. The session is stateful, so later calls inherit it. An invalid path fails this call without running the script. |
|
|
128
|
-
| `timeoutMs` | no |
|
|
130
|
+
| `timeoutMs` | no | Execution timeout, excluding queue waiting; default 120000. Integer milliseconds from 1 to 2147478647. |
|
|
129
131
|
|
|
130
|
-
Output
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
132
|
+
Output uses PowerShell formatting at width 200; objects appear as tables or lists. Use `ConvertTo-Json`
|
|
133
|
+
when structured fields are needed. Warning, Verbose, Information and Debug have separate labels; errors
|
|
134
|
+
and innermost exceptions follow ordinary output. Streams are grouped by type, so cross-stream order is
|
|
135
|
+
not preserved; merge them in the script with `2>&1` or `*>&1` when needed. The script stays at top level
|
|
136
|
+
for `using` and `param`. `#requires` is checked before the body; unsupported `-ShellId` / `-Assembly`
|
|
137
|
+
requirements are explicitly refused.
|
|
135
138
|
|
|
136
139
|
## Configuration
|
|
137
140
|
|
|
@@ -140,8 +143,8 @@ When order matters, merge them yourself: `2>&1` or `*>&1`.
|
|
|
140
143
|
| `pwshPath` | `pwsh` | Executable; resolved through `PATH` by default. |
|
|
141
144
|
| `timeoutMs` | `120000` | Default per-call timeout. |
|
|
142
145
|
| `idleMs` | `1800000` | Recycle an idle session (30 minutes). |
|
|
143
|
-
| `maxSessions` | `4` |
|
|
144
|
-
| `maxOutputBytes` | `8388608` |
|
|
146
|
+
| `maxSessions` | `4` | Per-plugin process limit, isolated by DSH session ID. If all are busy, new sessions are refused with a retry message. |
|
|
147
|
+
| `maxOutputBytes` | `8388608` | UTF-8 payload budget per call, including record separators; errors have a separate 65536-byte budget. Framing and display labels are excluded. Truncation is reported. |
|
|
145
148
|
| `allowUnconfined` | `false` | `true` = start a session even when the sandbox policy service is missing. |
|
|
146
149
|
|
|
147
150
|
`allowUnconfined` is the one deliberate escape hatch. In a normal deployment `ctx.sandboxPolicy` is always
|
|
@@ -158,9 +161,12 @@ returns. The Windows ACL sandbox uses a `WRITE_RESTRICTED` token, and a token is
|
|
|
158
161
|
after that single confine, every later read and write in the session obeys the same workspace boundary as
|
|
159
162
|
the built-in `pwsh` tool.
|
|
160
163
|
|
|
161
|
-
|
|
162
|
-
`sandboxPolicy.resolve`)
|
|
163
|
-
|
|
164
|
+
Every call resolves policy against the **current real session** (`session` must reach
|
|
165
|
+
`sandboxPolicy.resolve`) and checks the effective policy, workspace and sandbox backend. A change
|
|
166
|
+
disposes the old process before any new script; read-only refuses the body. Permission or workspace
|
|
167
|
+
changes therefore clear persistent state too. Working-directory service errors are surfaced instead
|
|
168
|
+
of falling back to stale paths. Creation and execution share one queue per ID; only sessions with no
|
|
169
|
+
executing, queued or starting requests can be evicted.
|
|
164
170
|
|
|
165
171
|
A trap worth knowing: passing the ToolExecution instead (`agent` lives on `exec.agent`) means `resolve`
|
|
166
172
|
never sees a session and **silently falls back to the deployment default** (mode + configured
|
|
@@ -176,7 +182,7 @@ Under `danger-full-access` nothing is confined, exactly like the built-in tool.
|
|
|
176
182
|
**`read-only` does not start.** In that mode pwsh runs in ConstrainedLanguage and cannot even create a
|
|
177
183
|
runspace. The tool reports that clearly and points at the built-in `pwsh` tool, which works there.
|
|
178
184
|
|
|
179
|
-
##
|
|
185
|
+
## Updating
|
|
180
186
|
|
|
181
187
|
This plugin carries no updater of its own. It is an ordinary registry package, so **DSH manages its
|
|
182
188
|
version**: the Plugins page lists it, shows when a newer release exists, and installs it — the same
|
|
@@ -244,9 +250,12 @@ your own machine — it derives every path from its own location and re-checks t
|
|
|
244
250
|
| Output looks reordered | stdout and stderr are merged by type, not by time. Merge them yourself with `2>&1`. |
|
|
245
251
|
| `exit N` code is not reported | Runspace semantics — use `Write-Output` or read `$LASTEXITCODE`. |
|
|
246
252
|
| A child process keeps a file locked after the call | The session holds the `Process` object. Wait for `idleMs`, or force a rebuild with a timeout. |
|
|
247
|
-
| `Program 'node.exe' failed to run: Access is denied.` (or `拒绝访问`) |
|
|
253
|
+
| `Program 'node.exe' failed to run: Access is denied.` (or `拒绝访问`) | The current Windows restricted token cannot create some child pipes, so `& exe` can fail. For output and an exit code, use `[Diagnostics.Process]::Start` on `cmd.exe /c "<exe> <args> > out.txt 2> err.txt"` with `UseShellExecute=$false`; or `Start-Process -NoNewWindow -PassThru -Wait -RedirectStandardOutput/-RedirectStandardError <file>` (the exit code may be empty). Avoid a new console; directly inherited output goes to `NUL`. |
|
|
254
|
+
|
|
255
|
+
## Known limitations
|
|
248
256
|
|
|
249
|
-
|
|
257
|
+
The [audit and local fix record](docs/pwsh-run-audit.zh-CN.md) covers the nine issue groups.
|
|
258
|
+
The host TLS failure has a separate [Schannel investigation](docs/dsh-windows-schannel.zh-CN.md).
|
|
250
259
|
|
|
251
260
|
The full list — session semantics, detached child std handles, UAC differences, and what an update does to a
|
|
252
261
|
session — lives in **[docs/runtime-notes.md](docs/runtime-notes.md)**. The short version:
|
|
@@ -266,6 +275,8 @@ plugin/
|
|
|
266
275
|
session-server.ps1 the PowerShell side: runspace, stream tagging, sentinels
|
|
267
276
|
test/
|
|
268
277
|
support.mjs locates a DSH install; converts Windows paths to /mnt form
|
|
278
|
+
protocoltest.mjs frame limits, UTF-8 budgets, startup cleanup
|
|
279
|
+
lifecycletest.mjs policy changes, session pool, cancellation
|
|
269
280
|
itest.mjs protocol and session core
|
|
270
281
|
plugintest.mjs the whole plugin path against a mock context
|
|
271
282
|
e2e-real-subprocess.mjs real DSH subprocess runtime
|
|
@@ -285,17 +296,19 @@ FINDINGS.md every measurement and the reasoning behind the design
|
|
|
285
296
|
|
|
286
297
|
```bash
|
|
287
298
|
cd <checkout>
|
|
288
|
-
|
|
299
|
+
node test/protocoltest.mjs # 6: frame limits, UTF-8 budgets, startup cleanup
|
|
300
|
+
node test/lifecycletest.mjs # 16: policy, pool, concurrency, cancellation, unload
|
|
301
|
+
PWSH_BIN='pwsh' node test/itest.mjs # 30: protocol and session core
|
|
289
302
|
PWSH_BIN='pwsh' node test/plugintest.mjs # 15: full plugin path
|
|
290
303
|
node test/e2e-real-subprocess.mjs # 8: real DSH subprocess
|
|
291
|
-
node test/sandboxtest.mjs #
|
|
304
|
+
node test/sandboxtest.mjs # 12: real sandbox, downgrade, root switch, first concurrency
|
|
292
305
|
node test/bashtest.mjs # 31: custom-bash fence triage, bash fallback, exec-bit repair
|
|
293
306
|
node test/bash-e2e.mjs # 11: custom-bash end to end
|
|
294
307
|
node test/denialtest.mjs # 11: sandbox-denial hint (no spawn, runs anywhere)
|
|
295
308
|
node test/metatest.mjs # 23: plugin-page metadata (no spawn, runs anywhere)
|
|
296
309
|
```
|
|
297
310
|
|
|
298
|
-
|
|
311
|
+
Ten scripts, **163 assertions**, none of which needs DSH to be running — just node 24, PowerShell 7 and a
|
|
299
312
|
DSH install to borrow `node_modules` from. Tests that need a DSH runtime locate one automatically
|
|
300
313
|
(`DSH_MODULES`, `DSH_MODULES_ASAR`, or by walking up from the working directory) and print a `SKIP` hint
|
|
301
314
|
instead of failing when none is found.
|
|
@@ -327,6 +340,19 @@ new content). With no tag on HEAD it prints the exact name to create.
|
|
|
327
340
|
Use the Windows-side npm. In WSL, `/usr/local/bin/npm` resolves to a bun symlink and `npm version` fails
|
|
328
341
|
with `Script not found "version"`.
|
|
329
342
|
|
|
343
|
+
## Related projects
|
|
344
|
+
|
|
345
|
+
More DeepSeek Harness (DSH) projects from [@citydirector](https://github.com/citydirector):
|
|
346
|
+
|
|
347
|
+
| Project | Kind | What it does |
|
|
348
|
+
|---|---|---|
|
|
349
|
+
| [DSH-build](https://github.com/citydirector/DSH-build) | Build | Portable and desktop Windows builds of DSH, patched, smoke-tested and released daily |
|
|
350
|
+
| [dsh-remote-access-proxy](https://github.com/citydirector/dsh-remote-access-proxy) | Plugin | Use the DSH Web GUI from your phone, tablet, or another computer |
|
|
351
|
+
| [deepinfra-fusion-proxy](https://github.com/citydirector/deepinfra-fusion-proxy) | Plugin | DeepInfra Standard / Flex switching with automatic fallback, per session |
|
|
352
|
+
| [dsh-loop-rescue](https://github.com/citydirector/dsh-loop-rescue) | Plugin | Cut model repetition loops mid-stream, prune them from the log, and resume |
|
|
353
|
+
| [ui-locate-mcp](https://github.com/citydirector/ui-locate-mcp) | MCP plugin | Pixel-exact coordinates for on-screen text and icons, for desktop automation |
|
|
354
|
+
| [dsh-session-manager](https://github.com/citydirector/dsh-session-manager) | Offline tool | Upgrade, archive, trash, loop cleanup and repair for DSH sessions |
|
|
355
|
+
|
|
330
356
|
## License
|
|
331
357
|
|
|
332
|
-
MIT
|
|
358
|
+
[MIT](LICENSE)
|
package/README.zh-CN.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) · **简体中文**
|
|
4
4
|
|
|
5
|
+
[](https://www.npmjs.com/package/dsh-pwsh-plugin) [](LICENSE) [](https://github.com/topics/dsh-plugin)
|
|
6
|
+
|
|
5
7
|
给 DeepSeek Harness 的 `pwsh_run` 工具:PowerShell 脚本正文**经 stdin** 送进一个常驻 pwsh 7 进程 ——
|
|
6
8
|
路上不做任何转义、没有 32K 命令行上限、~400ms 冷启动只付一次,变量 / 当前目录 / 已加载模块跨调用保留。
|
|
7
9
|
|
|
@@ -110,12 +112,12 @@ $env:DSH_HOME = '<DSH_HOME>' # 桌面 App 自己用的那个数据根
|
|
|
110
112
|
|---|---|---|
|
|
111
113
|
| `script` | 是 | PowerShell 脚本正文。**原样**送达 —— 不要转义 `$`,不要包 `pwsh -c`,不要加外层引号。 |
|
|
112
114
|
| `workdir` | 否 | 先 `Set-Location` 过去。会话有状态,后续调用会继承。路径无效则本次脚本不执行。 |
|
|
113
|
-
| `timeoutMs` | 否 |
|
|
115
|
+
| `timeoutMs` | 否 | 实际执行的超时,默认 120000;不含排队等待。必须为 1–2147478647 的整数毫秒。 |
|
|
114
116
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
输出使用 PowerShell 标准格式化(宽度 200),对象按表格或列表显示;需要结构化字段时显式 `ConvertTo-Json`。
|
|
118
|
+
Warning、Verbose、Information、Debug 分别带类型前缀,错误和最内层异常排在普通输出之后。各流按类型分组,
|
|
119
|
+
不保留跨流时序;需要合并时在脚本中使用 `2>&1` 或 `*>&1`。正文保持顶层执行,支持 `using`、`param`;
|
|
120
|
+
`#requires` 会在正文前核验,暂不支持的 `-ShellId` / `-Assembly` 会明确拒绝。
|
|
119
121
|
|
|
120
122
|
## 配置
|
|
121
123
|
|
|
@@ -124,8 +126,8 @@ $env:DSH_HOME = '<DSH_HOME>' # 桌面 App 自己用的那个数据根
|
|
|
124
126
|
| `pwshPath` | `pwsh` | 可执行文件;默认经 `PATH` 解析。 |
|
|
125
127
|
| `timeoutMs` | `120000` | 单次调用默认超时。 |
|
|
126
128
|
| `idleMs` | `1800000` | 空闲会话回收(30 分钟)。 |
|
|
127
|
-
| `maxSessions` | `4` |
|
|
128
|
-
| `maxOutputBytes` | `8388608` |
|
|
129
|
+
| `maxSessions` | `4` | 每个插件实例的会话上限,以 DSH session ID 隔离;全部忙时拒绝新会话并提示重试。 |
|
|
130
|
+
| `maxOutputBytes` | `8388608` | 每次调用普通输出正文的 UTF-8 字节预算(含记录换行);错误另留 65536 字节。协议与显示标签不计入,截断会明确提示。 |
|
|
129
131
|
| `allowUnconfined` | `false` | `true` = 沙箱策略服务缺席时也起会话。 |
|
|
130
132
|
|
|
131
133
|
`allowUnconfined` 是唯一一条「明知故犯」的开关。正常部署下 `ctx.sandboxPolicy` 一定在;缺席说明组合
|
|
@@ -140,8 +142,10 @@ $env:DSH_HOME = '<DSH_HOME>' # 桌面 App 自己用的那个数据根
|
|
|
140
142
|
`WRITE_RESTRICTED` 受限令牌,而令牌是**进程级**的 —— 所以 confine 一次之后,该会话的每一次读 / 写都
|
|
141
143
|
受同一个 workspace 边界约束,与内置 `pwsh` 工具等价。
|
|
142
144
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
+
每次调用先按**当前真实 session** 重新解析策略(`session` 必须传进 `sandboxPolicy.resolve`),并核对
|
|
146
|
+
有效策略、工作区和沙箱后端。任一项变化就销毁旧进程,再按新边界创建;切到只读时拒绝正文。
|
|
147
|
+
因此权限或工作区切换也会清空常驻状态。工作目录服务出错时直接报错,不回退到旧目录。
|
|
148
|
+
同一 ID 的创建与执行共用队列;只淘汰没有执行、排队或启动请求的空闲会话。
|
|
145
149
|
|
|
146
150
|
一个值得记住的坑:早先传的是 ToolExecution(`agent` 挂在 `exec.agent` 上),`resolve` 拿不到
|
|
147
151
|
session,**静默退回部署默认值**(mode + 配置里的 `workspaceRoot`),边界于是跑到 profile 目录去了 ——
|
|
@@ -214,10 +218,13 @@ Windows 上打,那份文件到手时是 `0644` —— 探测发现它属于当
|
|
|
214
218
|
| 输出顺序看着不对 | stdout 与 stderr 按类型合并,不按时间。自己用 `2>&1` 合。 |
|
|
215
219
|
| `exit N` 的退出码拿不到 | runspace 语义 —— 用 `Write-Output` 或读 `$LASTEXITCODE`。 |
|
|
216
220
|
| 调用结束后子进程还占着文件 | 会话持有 `Process` 对象。等 `idleMs`,或者用一次超时强制重建。 |
|
|
217
|
-
| `Program 'node.exe' failed to run: 拒绝访问。` |
|
|
221
|
+
| `Program 'node.exe' failed to run: 拒绝访问。` | 当前 Windows 受限令牌无法创建部分子进程管道,`& exe` 可能失败。需要输出和退出码时,用 `[Diagnostics.Process]::Start` 执行 `cmd.exe /c "<exe> <参数> > out.txt 2> err.txt"` 且 `UseShellExecute=$false`;或 `Start-Process -NoNewWindow -PassThru -Wait -RedirectStandardOutput/-RedirectStandardError <文件>`(退出码可能为空)。不要新建控制台;直接继承的输出会送往 `NUL`。 |
|
|
218
222
|
|
|
219
223
|
## 已知边界
|
|
220
224
|
|
|
225
|
+
九组问题及修复验收见[审计报告](docs/pwsh-run-audit.zh-CN.md);宿主 HTTPS 问题单独记在
|
|
226
|
+
[Schannel 兼容性记录](docs/dsh-windows-schannel.zh-CN.md)。
|
|
227
|
+
|
|
221
228
|
完整清单(会话语义、已断开的子进程 std handle、UAC 差异、升级对会话的影响)见
|
|
222
229
|
**[docs/runtime-notes.zh-CN.md](docs/runtime-notes.zh-CN.md)**。要点:
|
|
223
230
|
|
|
@@ -236,6 +243,8 @@ plugin/
|
|
|
236
243
|
session-server.ps1 PowerShell 侧:runspace、流打标、哨兵
|
|
237
244
|
test/
|
|
238
245
|
support.mjs 定位 DSH 安装;Windows 路径转 /mnt 形式
|
|
246
|
+
protocoltest.mjs 分帧、UTF-8 预算、启动清理
|
|
247
|
+
lifecycletest.mjs 权限切换、会话池与取消
|
|
239
248
|
itest.mjs 协议与会话本体
|
|
240
249
|
plugintest.mjs 插件完整路径(mock 上下文)
|
|
241
250
|
e2e-real-subprocess.mjs 真 DSH subprocess 运行时
|
|
@@ -255,17 +264,19 @@ FINDINGS.md 全部实测数据与设计理由
|
|
|
255
264
|
|
|
256
265
|
```bash
|
|
257
266
|
cd <检出目录>
|
|
258
|
-
|
|
267
|
+
node test/protocoltest.mjs # 6:帧边界、字节预算、启动清理
|
|
268
|
+
node test/lifecycletest.mjs # 16:权限切换、池、并发、取消、卸载
|
|
269
|
+
PWSH_BIN='pwsh' node test/itest.mjs # 30:协议与会话本体
|
|
259
270
|
PWSH_BIN='pwsh' node test/plugintest.mjs # 15:插件完整路径
|
|
260
271
|
node test/e2e-real-subprocess.mjs # 8:真 DSH subprocess
|
|
261
|
-
node test/sandboxtest.mjs #
|
|
272
|
+
node test/sandboxtest.mjs # 12:真沙箱边界、降权、切根、首次并发
|
|
262
273
|
node test/bashtest.mjs # 31:custom-bash 围栏分诊、bash 后备、执行位自修复、SSH agent
|
|
263
274
|
node test/bash-e2e.mjs # 11:custom-bash 端到端
|
|
264
275
|
node test/denialtest.mjs # 11:沙箱拒绝提示(不起子进程,哪儿都能跑)
|
|
265
276
|
node test/metatest.mjs # 23:插件页元数据(不起子进程,哪儿都能跑)
|
|
266
277
|
```
|
|
267
278
|
|
|
268
|
-
|
|
279
|
+
十份脚本合计 **163 项**,都不需要 DSH 在跑 —— 只要 node 24、PowerShell 7,以及一个能借
|
|
269
280
|
`node_modules` 的 DSH 安装。需要 DSH 运行时的测试会自动找(`DSH_MODULES`、`DSH_MODULES_ASAR`,
|
|
270
281
|
或从工作目录逐级向上),找不到时打一行 `SKIP` 提示而不是失败。
|
|
271
282
|
|
|
@@ -293,6 +304,19 @@ HEAD 上还没有 tag 时,它把该打的标签名打印出来。
|
|
|
293
304
|
npm 要用 Windows 侧那份。WSL 里 `/usr/local/bin/npm` 指向 bun 的软链,`npm version` 会报
|
|
294
305
|
`Script not found "version"`。
|
|
295
306
|
|
|
296
|
-
##
|
|
307
|
+
## 相关项目
|
|
308
|
+
|
|
309
|
+
[@citydirector](https://github.com/citydirector) 的其它 DeepSeek Harness(DSH)项目:
|
|
310
|
+
|
|
311
|
+
| 项目 | 类型 | 用途 |
|
|
312
|
+
|---|---|---|
|
|
313
|
+
| [DSH-build](https://github.com/citydirector/DSH-build) | 构建 | DSH 的 Windows 便携版 / 桌面版绿色构建,每天跟随上游打补丁、冒烟、发布 |
|
|
314
|
+
| [dsh-remote-access-proxy](https://github.com/citydirector/dsh-remote-access-proxy) | 插件 | 在手机、平板或其它电脑上用浏览器访问 DSH Web GUI |
|
|
315
|
+
| [deepinfra-fusion-proxy](https://github.com/citydirector/deepinfra-fusion-proxy) | 插件 | DeepInfra Standard / Flex 按会话切换与自动回退 |
|
|
316
|
+
| [dsh-loop-rescue](https://github.com/citydirector/dsh-loop-rescue) | 插件 | 模型复读时从流内切断、剪掉循环并自动续跑 |
|
|
317
|
+
| [ui-locate-mcp](https://github.com/citydirector/ui-locate-mcp) | MCP 插件 | 按界面文字和图标给出精确像素坐标,用于桌面操作 |
|
|
318
|
+
| [dsh-session-manager](https://github.com/citydirector/dsh-session-manager) | 离线工具 | 离线升级、归档、回收站、清洗循环、修复烂尾 / 隐藏会话 |
|
|
319
|
+
|
|
320
|
+
## 许可证
|
|
297
321
|
|
|
298
|
-
MIT
|
|
322
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# DSH Windows 沙箱与 Schannel 兼容性记录
|
|
2
|
+
|
|
3
|
+
记录日期:2026-11-12。来源为本机隔离实验及上游代码追溯;与本插件九组修复分开维护。
|
|
4
|
+
|
|
5
|
+
## 补充核实:curl 联网与 Windows PATH(2026-11-11)
|
|
6
|
+
|
|
7
|
+
本段核实 DSH 沙箱里的 curl 联网与 Windows PATH 是否符合既定语义。本段所有网络请求均为公开站点的只读请求,未修改网络、凭据、PATH 或沙箱配置。
|
|
8
|
+
|
|
9
|
+
### 结论
|
|
10
|
+
|
|
11
|
+
1. **不是全面断网。** 当前 workspace-write 的 Windows 沙箱可以建立 TCP,也能使用 HTTP;HTTPS 故障发生在系统 curl / Invoke-WebRequest 使用的 Windows Schannel 凭据获取阶段。
|
|
12
|
+
2. **这是当前沙箱令牌与 Schannel 的兼容性问题,不是设计上禁止联网。** 已安装的 `@deepseek-ai/dsh-sandbox-windows-acl` 0.2.2-rc.1 的中文边界说明(第 127 行)明确写“写入与删除受限;读取、网络与进程可见性不受限”。
|
|
13
|
+
3. **当前 Windows PATH 正常。** 旧的 Machine PATH 整段丢失没有复现。
|
|
14
|
+
4. **WSL 围栏禁止执行 Windows exe 是预期限制。** 借 cmd.exe / pwsh.exe 从 WSL 查询 Windows 环境会被拦,与 Windows 侧读取 PATH 的问题不同。
|
|
15
|
+
|
|
16
|
+
### 本轮对照矩阵
|
|
17
|
+
|
|
18
|
+
| 环境 / 工具 | 请求 | 结果 |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| workspace-write,pwsh_run,系统 curl 8.22.0 / Schannel | http://example.com/ | HTTP 200,exit 0 |
|
|
21
|
+
| 同上 | https://example.com/ | TCP 约 0.048s 建立;TLS 未建立,HTTP 000,exit 35 |
|
|
22
|
+
| 同上 | https://registry.npmjs.org/ | TCP 约 0.053s 建立;TLS 未建立,HTTP 000,exit 35 |
|
|
23
|
+
| workspace-write,原生 pwsh,系统 curl | https://example.com/ | 同样 exit 35,排除本插件独有故障 |
|
|
24
|
+
| workspace-write,pwsh_run,Invoke-WebRequest | https://example.com/ | 最内层 Win32Exception:安全包中没有可用的凭证 |
|
|
25
|
+
| 正常令牌,原生 pwsh,**同一系统 curl** | https://example.com/ | HTTP 200,exit 0 |
|
|
26
|
+
| 正常令牌,Invoke-WebRequest | https://example.com/ | HTTP 200 |
|
|
27
|
+
| workspace-write,Windows Node.js 的 https 模块 | https://example.com/ | HTTP 200,exit 0,未关闭证书校验 |
|
|
28
|
+
| 当前 WSL 围栏内,Linux curl 8.14.1 / OpenSSL 3.5.6 | https://example.com/ | HTTP 200,exit 0 |
|
|
29
|
+
|
|
30
|
+
Windows curl 的原始错误:
|
|
31
|
+
|
|
32
|
+
> curl: (35) schannel: AcquireCredentialsHandle failed: SEC_E_NO_CREDENTIALS (0x8009030e)
|
|
33
|
+
|
|
34
|
+
受限进程的完整性 SID 为 S-1-16-4096(Low),正常对照为 S-1-16-12288(High)。这个对照确认问题随当前 Windows 沙箱令牌出现;本轮没有进一步分离 Low 完整性和 WRITE_RESTRICTED 各自的贡献,也没有证明所有 Windows 主机必然如此。此前记忆中有类似结论,本段以本轮本机实测为依据。
|
|
35
|
+
|
|
36
|
+
### PATH 核对
|
|
37
|
+
|
|
38
|
+
- 当前 PowerShell 7.6.8。Process PATH 长 1087,Machine PATH 长 471,User PATH 长 588;三者可读取。
|
|
39
|
+
- 逐项展开 Machine PATH 中的变量,再与进程 PATH 忽略大小写和尾斜杠比对:**缺失项 0**,没有未展开的变量引用。
|
|
40
|
+
- System32 存在;Get-Command 能解析 curl.exe、cmd.exe、wsl.exe、git.exe。curl 落在 Windows System32,后端是 Schannel。
|
|
41
|
+
- WSL 自己的 PATH 也含 System32,但执行 Windows cmd.exe 返回 126 / Permission denied。这是 llrun 阻止 Windows interop 的既定行为,不能据此判定 PATH 缺失。
|
|
42
|
+
|
|
43
|
+
### 目前可用的路径与范围
|
|
44
|
+
|
|
45
|
+
Windows 沙箱内需要 HTTPS 时,Windows Node.js 的 https 已验证可用;纯 Linux 网络操作可使用 WSL curl;读取网页也可以用宿主提供的抓取工具。Windows 路径与文件操作仍走 Windows 工具。
|
|
46
|
+
|
|
47
|
+
如果程序必须依赖 Schannel,应作为 Windows 沙箱后端的兼容性问题单独处理,不能通过修改 PATH 解决,也不应默认放开全部文件权限来掩盖。只读模式的独立进程、本机其它 curl 构建、UAC 标准用户和其它主机未在本段补测;不把这张矩阵扩大成所有环境的保证。
|
|
48
|
+
|
|
49
|
+
## 归因追溯:Schannel 故障由谁引入(2026-11-12)
|
|
50
|
+
|
|
51
|
+
本段追溯预期外 HTTPS 故障的引入层与来源提交。本段只做归因、记录与一次性对照,不修改生产沙箱或插件实现。
|
|
52
|
+
|
|
53
|
+
### 归属结论
|
|
54
|
+
|
|
55
|
+
**故障触发点在 DSH 上游 Windows ACL 后端的受限令牌构造。** 当前系统 curl 使用 Schannel,在带 restricting SIDs 的 WRITE_RESTRICTED 令牌下无法取得出站 TLS 凭据。这是该安全机制在本机暴露的兼容性缺陷;不能解释成产品有意禁止联网。
|
|
56
|
+
|
|
57
|
+
- pwsh_run 只是委托宿主:plugin/index.mjs:204 解析策略,:226 调用 sandbox.confine(plain, policy, signal),:230 使用返回的 confined.argv 启动。它没有实现 CreateRestrictedToken。
|
|
58
|
+
- 原生 pwsh 在同一策略下同样失败,排除插件独有的会话协议或编码处理。
|
|
59
|
+
- 当前桌面归档的 dsh/node_modules/@deepseek-ai/dsh-sandbox-windows-acl/lib/types-Cl07j3lG.js 中,createRestrictedToken 函数与本轮测试使用的便携包逐字相同;两者包版本均为 0.2.2-rc.1,桌面 VERSION 为 fcc0ad462172522aaf5c93a7b2e01f6b9f0b0159。
|
|
60
|
+
- 本地 DSH-build 的 portable/patch-windows-acl-runner-console.mjs 只调整 ffi.ts / runner.ts 的控制台处理,没有改 token.ts。本轮直接调用 AclSandbox 的进程启动接口也能复现,未经过本插件的 PowerShell 服务端。
|
|
61
|
+
|
|
62
|
+
### 隔离变量,而非只比较高低完整性
|
|
63
|
+
|
|
64
|
+
同一台 Windows、同一个 System32 curl.exe、同一个 https://example.com/,均为公开页面只读请求。探针父进程经审批使用正常令牌;仅在该一次性进程中调整 Win32 API 的实参,测试后恢复。所有 AclSandbox 实例均设 manageDacls:false,未改变磁盘 ACL、注册表或生产配置。
|
|
65
|
+
|
|
66
|
+
| 对照令牌 | 完整性 | CreateRestrictedToken flags / SID 数 | curl 结果 |
|
|
67
|
+
|---|---|---|---|
|
|
68
|
+
| 正常父进程 | High | 不创建受限令牌 | HTTP 200 / exit 0 |
|
|
69
|
+
| 仅降完整性,不加 restricting SID | Low | 0 / 0 | HTTP 200 / exit 0 |
|
|
70
|
+
| 仅裁剪特权,不加 restricting SID | High | DISABLE_MAX_PRIVILEGE = 1 / 0 | HTTP 200 / exit 0 |
|
|
71
|
+
| 写限制与 restricting SID,不裁剪特权 | High | WRITE_RESTRICTED = 8 / 2 | HTTP 000 / exit 35 |
|
|
72
|
+
| 当前正式构造,仅保留 High | High | 9 / 2 | HTTP 000 / exit 35 |
|
|
73
|
+
| 当前正式构造 | Low | 9 / 2 | HTTP 000 / exit 35 |
|
|
74
|
+
| 恢复 Authenticated Users | High | 9 / 3 | HTTP 000 / exit 35 |
|
|
75
|
+
| 重建初版广 SID 列表和 flags | High | 13 / 6 | HTTP 000 / exit 35 |
|
|
76
|
+
|
|
77
|
+
flags 9 = DISABLE_MAX_PRIVILEGE | WRITE_RESTRICTED;flags 13 另含历史 LUA_TOKEN。当前 read-only 的两个 SID 是 logon SID、Everyone;初版广列表另含 Authenticated Users、INTERACTIVE、LOCAL 与一个写能力 SID。
|
|
78
|
+
|
|
79
|
+
所有 exit 35 的有效对照均返回:
|
|
80
|
+
|
|
81
|
+
> curl: (35) schannel: AcquireCredentialsHandle failed: SEC_E_NO_CREDENTIALS (0x8009030e)
|
|
82
|
+
|
|
83
|
+
由此排除“只要 Low 就不能用 Schannel”和“仅因裁剪特权而失败”;恢复 Authenticated Users 也不能修复。这里定位到的是**写限制令牌与 restricting SID 的组合**,没有声称限制 SID 中某一个成员是单独根因,也没有定位 Windows 内部哪个访问检查返回拒绝。
|
|
84
|
+
|
|
85
|
+
额外尝试进一步拆开该组合时,WRITE_RESTRICTED / 零限制 SID 的进程退出 3221225794(0xC0000142),flags 0 / 两个限制 SID 的进程退出 3221225781(0xC0000135)。这两组连 whoami / curl 都未正常启动,**属于无效 TLS 对照,不计为网络失败证据**;本轮不继续扩大这些变体。随后查询 System / Application Popup,最近 12 分钟没有新事件。
|
|
86
|
+
|
|
87
|
+
### 来源提交链
|
|
88
|
+
|
|
89
|
+
以下日期均为 UTC;作者为 Git 提交记录中的 author,不把提交元数据解释成人为故障责任。
|
|
90
|
+
|
|
91
|
+
| 上游提交 | 日期 | 内容及本轮判断 |
|
|
92
|
+
|---|---|---|
|
|
93
|
+
| [f64ba40](https://github.com/deepseek-ai/deepseek-harness/commit/f64ba40f43961fc65b7e1724132708a0b6d163cf) | 2026-08-07 17:24:33 | Huanqi Cao,feat(sandbox): Windows ACL write-restriction sandbox (restricted-token runner)。首次加入该后端及受限令牌构造;本机重建初版参数也复现同一 TLS 错误,是可追溯的机制来源。 |
|
|
94
|
+
| [441927c](https://github.com/deepseek-ai/deepseek-harness/commit/441927c5268e1abd2be024d3b5bb69e3869b6f06) | 2026-08-08 | 为封堵 C 盘根的越界写入而移除 Authenticated Users;本轮恢复该 SID 仍失败,不能把本次故障归给这一处收紧。 |
|
|
95
|
+
| [d5ad3ba](https://github.com/deepseek-ai/deepseek-harness/commit/d5ad3baeb5d531e31da5e586c3ec3ebedd825169) | 2026-09-18 | 增加 Low 完整性及目录标签,堵 FILE_DELETE_CHILD 删除旁路;仅 Low 的 TLS 对照成功,不能把它当作本次故障引入点。 |
|
|
96
|
+
| [bdd75ca](https://github.com/deepseek-ai/deepseek-harness/commit/bdd75ca1a0ce122a4801fa0e24d26f19159c071b) | 2026-10-26 | Huo Yaoyuan,去掉 LUA_TOKEN 以支持内置 Administrator / WDAGUtilityAccount;带/不带 LUA 的已测组合均失败,不是该修补造成。 |
|
|
97
|
+
|
|
98
|
+
首次提交的父提交为 4c2d20dbf7709ce88da5f01b163c8daaa1f087aa。初版 token.ts 的关键调用是 DISABLE_MAX_PRIVILEGE | LUA_TOKEN | WRITE_RESTRICTED,列表为 world / logonSid / authUser / interactive / local / writeSid。当前桌面编译件的调用为 flags 9,read-only 列表为 logonSid / world,workspace-write 另加写能力 SID。
|
|
99
|
+
|
|
100
|
+
这是一条**代码来源 + 本机参数复现**的归因链;未安装并运行全部历史 DSH 版本,不能宣称已证明每个旧版本在当时 Windows 系统上的表现。当前版本上游 runner.spec.ts 中未检出 curl / Schannel / HTTPS / Invoke-WebRequest 用例;这只能说明该测试文件未覆盖,不能扩大成全仓所有测试都没有。
|
|
101
|
+
|
|
102
|
+
### 记录更正与后续方向
|
|
103
|
+
|
|
104
|
+
此前记忆 dsh-sandbox-lowil-schannel-no-credentials 将完整沙箱与 High 的差异直接归因于 Low,证据不成立。本轮仅 Low 成功、High + 写限制失败,已据此更正;前段“未分离变量”的观察保留为历史记录,以本段为进一步结论。
|
|
105
|
+
|
|
106
|
+
后续应在 DSH Windows ACL 后端增加 Schannel / curl / Invoke-WebRequest 回归,并研究保留文件写入与删除边界的兼容方案。不能直接撤 Low(会回开删除旁路,且本轮 High + 写限制仍然失败),也不能恢复宽 SID 列表(本轮无效,历史上还会回开越界写入)。Node.js HTTPS / WSL OpenSSL 的可用性已在前段实测,可作为沙箱内现有的网络路径。
|
|
107
|
+
|
|
108
|
+
本轮没有修改上游、应用归档、生产权限或系统网络配置,没有创建公开 issue / PR;归因实验只覆盖本机 Windows 构建,其他系统及 UAC 标准用户仍需独立验证。
|
|
109
|
+
|
|
110
|
+
## 新版上游复核(2026-11-12)
|
|
111
|
+
|
|
112
|
+
联网核对时,最新标签为 [dsh-v0.2.2-rc.2](https://github.com/deepseek-ai/deepseek-harness/tree/dsh-v0.2.2-rc.2),commit ddac96760b6edf8fa0fe17cbdcf0b512f4c12b2c;master 为 e8f7b93a344964de3dca1ace9e72b6e4d6de87cb,提交时间2026-11-11T02:49:46Z。发布标签与master的Windows ACL后端src树相同。
|
|
113
|
+
|
|
114
|
+
与本机fcc0ad462172522aaf5c93a7b2e01f6b9f0b0159相比,token.ts、runner.ts、spawn.ts、ffi.ts、acl.ts、grant.ts均为相同Git blob;tests树也完全相同。src仅index.ts新增导出isTempRootInsideWorkspace,path-boundary.ts把既有临时目录包含关系判断抽成该函数;没有改变Schannel、令牌、进程启动或凭据处理。token.ts仍为DISABLE_MAX_PRIVILEGE | WRITE_RESTRICTED加logon SID / Everyone / 写能力SID。
|
|
115
|
+
|
|
116
|
+
结论:截至该次核对,最新发布与master中没有针对此故障的修复,不能指望升级到rc.2恢复系统curl/IWR。此为源码与历史差异核对,没有安装新版运行;仍不泛化所有Windows主机。API查询dev分支失败,未对不存在或不可取的dev分支作结论。
|
|
117
|
+
|
|
118
|
+
上游已有相同现象报告:[讨论3207](https://github.com/deepseek-ai/deepseek-harness/discussions/3207)(2026-08-18)及[讨论4163](https://github.com/deepseek-ai/deepseek-harness/discussions/4163)(2026-08-23;2026-10-02回复建议OpenSSL)。帖子是旁证,结论以实际源码差异为主;不沿用帖子里未经隔离的特权归因或把pip/git一概判定为Schannel客户端。
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# pwsh_run 审计:权限、多会话和命令兼容性
|
|
2
|
+
|
|
3
|
+
审计日期:2026-11-11(America/Los_Angeles)。审计对象:0.2.17,提交 `2ea7783103328cbdf568aeae596aa9d299f661fc`。
|
|
4
|
+
|
|
5
|
+
结论:普通串行调用的主要功能正常,但目前不能宣称权限切换安全、多会话互不影响。复用了旧权限的进程、淘汰忙会话、首次并发创建和队列超时是优先处理项。本轮只审计,未修改运行实现、安装件、全局配置或发布版本。
|
|
6
|
+
|
|
7
|
+
> A1–A9 以下记录的是上述提交的审计基线。当前工作区的修复与复验见文末「本地修复验收」。
|
|
8
|
+
|
|
9
|
+
## 方法与验证范围
|
|
10
|
+
|
|
11
|
+
- Windows 11、PowerShell 7.6.8、Node.js 24.22.1。直接加载仓库模块,通过隔离的工具注册上下文调用真实 apply/execute;协议边界另用模拟子进程做确定性验证。
|
|
12
|
+
- 权限边界使用真实 DSH LocalSandboxProvider。测试目录 A/B 都位于本仓库临时区;“越界”指离开子进程获准的 A,仍未离开本仓库。
|
|
13
|
+
- 运行当前相关回归:itest 17、plugintest 15、e2e-real-subprocess 8、sandboxtest 7、denialtest 11、metatest 23,共 **81 条断言通过**。这不代表下列新增场景已被原测试覆盖。
|
|
14
|
+
- 为避免既有测试往仓库外落探针,回归在临时副本运行,导入仍指向未修改的生产模块。e2e 副本的 Start-Process 补了 -NoNewWindow,避免已知的控制台初始化弹窗。未修改原测试。
|
|
15
|
+
- 当前受限令牌无法支持 Node 内部的部分子进程管道;真实沙箱与进程测试使用经审批的正常令牌启动,再由 DSH confine 构造被测受限进程。这不是在受限调用失败后绕过生产边界。
|
|
16
|
+
- 未在 Linux/macOS 的 PowerShell、不同 PowerShell 7 小版本、UAC 标准用户、全部第三方原生命令上重做环境矩阵。没有证据支持“各种环境全部正常”。
|
|
17
|
+
|
|
18
|
+
## A1:复用会话不重新核对权限与工作区(高,优先修)
|
|
19
|
+
|
|
20
|
+
位置:[会话缓存命中](https://github.com/citydirector/dsh-pwsh-plugin/blob/2ea7783103328cbdf568aeae596aa9d299f661fc/plugin/index.mjs#L182)、[只在创建时解析 policy](https://github.com/citydirector/dsh-pwsh-plugin/blob/2ea7783103328cbdf568aeae596aa9d299f661fc/plugin/index.mjs#L204)。
|
|
21
|
+
|
|
22
|
+
会话缓存键只有 session ID。命中活进程就直接返回;本次 policy、workspaceRoot 以及策略服务是否仍存在均未重新验证。Windows ACL 令牌确实能约束进程,但它约束的是创建时的边界。
|
|
23
|
+
|
|
24
|
+
真实沙箱结果:
|
|
25
|
+
|
|
26
|
+
| 同一个 ID 的操作 | 结果 |
|
|
27
|
+
|---|---|
|
|
28
|
+
| workspace-write,根 A,在 A 写 | WROTE |
|
|
29
|
+
| 同上,在 B 写 | DENIED,磁盘上没有该文件 |
|
|
30
|
+
| 将 policy 切为 read-only,在 A 写新文件 | **WROTE,文件存在**;未再次解析 policy |
|
|
31
|
+
| 使用新 ID 启动 read-only | 正确拒绝启动,提示改用原生 pwsh |
|
|
32
|
+
| 将旧 ID 的工作区根改为 B,再在 A 写 | **WROTE** |
|
|
33
|
+
| 同上,在 B 写 | **DENIED** |
|
|
34
|
+
|
|
35
|
+
模拟上下文还复现了 danger-full-access → read-only 后仍执行脚本。以上是工具复用逻辑的缺陷;没有宣称它绕过了 DSH 所有外层授权机制。
|
|
36
|
+
|
|
37
|
+
建议:每次 acquire 先解析当前有效 policy,以 mode、规范化 workspaceRoot、执行后端与其它影响边界的配置核对进程身份。边界变化时不能复用旧进程;新策略只读或不可解析时应先拒绝本次执行。同步保护正在创建和正在执行的旧实例,不能仅换 Map 的键。
|
|
38
|
+
|
|
39
|
+
## A2:容量淘汰和空闲回收会杀正在执行的会话(高)
|
|
40
|
+
|
|
41
|
+
位置:[容量淘汰](https://github.com/citydirector/dsh-pwsh-plugin/blob/2ea7783103328cbdf568aeae596aa9d299f661fc/plugin/index.mjs#L235)、[空闲回收](https://github.com/citydirector/dsh-pwsh-plugin/blob/2ea7783103328cbdf568aeae596aa9d299f661fc/plugin/index.mjs#L256)、[lastUsed 更新](https://github.com/citydirector/dsh-pwsh-plugin/blob/2ea7783103328cbdf568aeae596aa9d299f661fc/plugin/session.mjs#L175)。
|
|
42
|
+
|
|
43
|
+
两条回收路径都未检查执行或排队状态。lastUsed 记录开始使用的时间,不代表真正闲置的起点。
|
|
44
|
+
|
|
45
|
+
真实 PowerShell 复现:
|
|
46
|
+
|
|
47
|
+
- 使用生产默认 maxSessions=4,预热 A/B/C/D;让 A 执行任务,再访问 B/C/D,最后创建 E。**E 淘汰了仍在执行的 A**,A 返回 no response,未完成原任务。
|
|
48
|
+
- maxSessions=1 时同样复现 A 被 B 杀掉。
|
|
49
|
+
- 将 idleMs 缩短为 150ms,正在运行 600ms 的任务被“空闲”回收。默认 30 分钟时相同代码会影响持续超过阈值且没有后续访问的调用;这个默认时长场景没有实际等待 30 分钟。
|
|
50
|
+
|
|
51
|
+
建议:维护 active/queued/pending-start 计数,只回收真正空闲的实例,完成调用后再开始计算闲置时间。池满且全部忙时应等待容量或清楚拒绝新请求,不能腾位置就杀旧任务。
|
|
52
|
+
|
|
53
|
+
## A3:首次并发会创建两个进程,状态分叉(高)
|
|
54
|
+
|
|
55
|
+
位置:[读取 Map 后先 await 目录](https://github.com/citydirector/dsh-pwsh-plugin/blob/2ea7783103328cbdf568aeae596aa9d299f661fc/plugin/index.mjs#L182)、[之后才写 Promise 占位](https://github.com/citydirector/dsh-pwsh-plugin/blob/2ea7783103328cbdf568aeae596aa9d299f661fc/plugin/index.mjs#L199)。
|
|
56
|
+
|
|
57
|
+
注释声称 Promise 占位避免重复创建,但占位前已有 await。首次不带 workdir 的两个并发请求都可能看到缓存为空。
|
|
58
|
+
|
|
59
|
+
真实沙箱复现:同一新 ID 的两个 Promise.all 请求分别返回 PID 28584 和 13440,spawn 数为 2。两个请求写入不同的全局变量值;下一次调用只看到最终保留的那个进程的值。
|
|
60
|
+
|
|
61
|
+
因此问题是状态分叉,不是协议输出混到一起。被覆盖的进程还可能失去池的追踪;本轮探针显式回收了自己创建的所有句柄。
|
|
62
|
+
|
|
63
|
+
建议:在第一个 await 前原子登记“正在创建”的槽位,让所有并发调用等待同一个 Promise。失败和淘汰时须核对槽位代次,旧实例的回调不得删掉新实例。
|
|
64
|
+
|
|
65
|
+
## A4:排队请求的超时会杀前面的任务,预取消请求仍执行(高)
|
|
66
|
+
|
|
67
|
+
位置:[在 session.run 排队前创建外层定时器](https://github.com/citydirector/dsh-pwsh-plugin/blob/2ea7783103328cbdf568aeae596aa9d299f661fc/plugin/index.mjs#L296)、[串行队列](https://github.com/citydirector/dsh-pwsh-plugin/blob/2ea7783103328cbdf568aeae596aa9d299f661fc/plugin/session.mjs#L163)、[中止后整会话淘汰](https://github.com/citydirector/dsh-pwsh-plugin/blob/2ea7783103328cbdf568aeae596aa9d299f661fc/plugin/index.mjs#L321)。
|
|
68
|
+
|
|
69
|
+
真实复现:
|
|
70
|
+
|
|
71
|
+
1. 同一 ID 先运行 Start-Sleep 7 秒,timeoutMs=9000;这条任务本应正常完成。
|
|
72
|
+
2. 50ms 后排入另一条 timeoutMs=100 的短任务。
|
|
73
|
+
3. 短任务尚未执行,外层 100+5000ms 的预算先到,便销毁整个会话。
|
|
74
|
+
4. 两个请求都返回 no response。长任务约 14012ms 才返回,正常结束输出丢失;短任务文案还将 100ms 四舍五入为“within 0s”。
|
|
75
|
+
|
|
76
|
+
另用真实 PowerShell 和已 aborted 的 AbortController 调用,仍得到 RAN-DESPITE-CANCEL。监听器只接收未来的 abort,没有先处理既有中止状态。
|
|
77
|
+
|
|
78
|
+
建议:分开排队等待和脚本运行的预算;排队取消只移除那一个请求,不终止当前任务。开始 acquire、入队和实际发送前都检查 signal.aborted。实际正在执行的调用需要取消时,再按已声明语义销毁其会话。
|
|
79
|
+
|
|
80
|
+
## A5:dispose 不结算在途 Promise(中,与 A2/A4 叠加)
|
|
81
|
+
|
|
82
|
+
位置:[dispose](https://github.com/citydirector/dsh-pwsh-plugin/blob/2ea7783103328cbdf568aeae596aa9d299f661fc/plugin/session.mjs#L146)、[#fail 的 dead 短路](https://github.com/citydirector/dsh-pwsh-plugin/blob/2ea7783103328cbdf568aeae596aa9d299f661fc/plugin/session.mjs#L135)。
|
|
83
|
+
|
|
84
|
+
dispose 先设置 dead=true,没有 reject pending。后续 handle.done 再调用 #fail 时直接返回。模拟协议中等待的 run 在 dispose 后仍 pending;真实调用往往要等外层超时后才结束,而不能及时收到准确的“会话被回收”原因。
|
|
85
|
+
|
|
86
|
+
建议:显式拒绝所有在途请求并清空状态,再终止/等待进程;自然死亡和主动销毁共用幂等的结算路径。
|
|
87
|
+
|
|
88
|
+
## A6:常见 PowerShell 对象输出和顶层语法不完全兼容(中)
|
|
89
|
+
|
|
90
|
+
位置:[用脚本块包装正文并逐对象强转字符串](https://github.com/citydirector/dsh-pwsh-plugin/blob/2ea7783103328cbdf568aeae596aa9d299f661fc/plugin/session-server.ps1#L127)。
|
|
91
|
+
|
|
92
|
+
| 真实命令 | 返回 |
|
|
93
|
+
|---|---|
|
|
94
|
+
| [pscustomobject]@{Name='audit';Value=42} | @{Name=audit; Value=42} |
|
|
95
|
+
| Get-Process -Id $PID | System.Diagnostics.Process (pwsh),缺少通常表格里的 Id/CPU 等字段 |
|
|
96
|
+
| 显式 Select-Object 后 ConvertTo-Json -Compress | 字段完整 |
|
|
97
|
+
| using namespace System.Text 后使用 StringBuilder | A 'using' statement must appear before any other statements in a script. |
|
|
98
|
+
|
|
99
|
+
脚本经过字符串传输没有转义损坏,但“原样送达”不等于在普通脚本顶层执行的语义相同。Write-Host、Information、Debug 均可见,但被并为普通输出;Warning/Verbose/Error 保留各自类别。
|
|
100
|
+
|
|
101
|
+
建议:定义对象输出契约,显式按 PowerShell 格式化系统输出或选定结构化表示,不能用逐对象 [string] 替代默认显示;另处理 using/param/#requires 等顶层结构。这里只实测了 using namespace,没有把其它顶层结构都判成已复现缺陷。
|
|
102
|
+
|
|
103
|
+
## A7:输出上限不是实际字节上限,也不能限制服务端内存(中)
|
|
104
|
+
|
|
105
|
+
位置:[客户端计数与截断](https://github.com/citydirector/dsh-pwsh-plugin/blob/2ea7783103328cbdf568aeae596aa9d299f661fc/plugin/session.mjs#L111)、[隐藏 Console 内存流](https://github.com/citydirector/dsh-pwsh-plugin/blob/2ea7783103328cbdf568aeae596aa9d299f661fc/plugin/session-server.ps1#L49)、[输出集合累计](https://github.com/citydirector/dsh-pwsh-plugin/blob/2ea7783103328cbdf568aeae596aa9d299f661fc/plugin/session-server.ps1#L175)、[按索引读取但不移除](https://github.com/citydirector/dsh-pwsh-plugin/blob/2ea7783103328cbdf568aeae596aa9d299f661fc/plugin/session-server.ps1#L194)。
|
|
106
|
+
|
|
107
|
+
- 计数使用 text.length,即 UTF-16 代码单元。模拟协议设置 maxOutputBytes=4,中文中文实际 UTF-8 为 12 字节,仍完整交付且 truncated=false。
|
|
108
|
+
- 所有流共用同一个预算;注释“每个流各自计”与代码不符。达到上限后后续 Error/Exception/Fatal 也会被丢弃,虽仍保留状态哨兵,但错误原因可能不见。
|
|
109
|
+
- 客户端截断不等于服务端停止累积。PSDataCollection 本次调用内仍保存已发出的编码结果。
|
|
110
|
+
- [Console]::Write('X' * 4000000) 的文本被吞进 SafeOut 的 MemoryStream,并未按调用清空。一次真实测量中,强制 GC 后的托管内存从 14895168 增至 19368784 字节;源码同时确认了无清理路径。这里不把内存差值全部精确归因给单一对象。
|
|
111
|
+
- 继承 stdout/stderr 的原生子进程写入 drain 文件,源码仅在下一次调用开始时截零;单次调用期间没有文件大小预算。这一点是静态发现,没有用填盘实验验证。
|
|
112
|
+
|
|
113
|
+
建议:按 Buffer.byteLength(text, 'utf8') 统计并约束协议帧大小;给错误保留独立预算;服务端消费后移除集合内容;吞掉的 Console 输出用空输出流或受限环形缓存;明确 drain 的资源上限。
|
|
114
|
+
|
|
115
|
+
## A8:启动失败诊断和加固警告会丢失(中)
|
|
116
|
+
|
|
117
|
+
位置:[启动握手](https://github.com/citydirector/dsh-pwsh-plugin/blob/2ea7783103328cbdf568aeae596aa9d299f661fc/plugin/session.mjs#L194)、[首次警告发送](https://github.com/citydirector/dsh-pwsh-plugin/blob/2ea7783103328cbdf568aeae596aa9d299f661fc/plugin/session-server.ps1#L167)。
|
|
118
|
+
|
|
119
|
+
- 模拟首次握手返回 TAKEOVER-FAILED 警告,startSession 将其丢弃,用户第一次调用的 warn 数组为空。服务端把“第一次调用”当成用户调用,实际上先被内部版本探针消费。
|
|
120
|
+
- 握手中的 run 抛错会跳过后面的 dispose 和 stderrTail 拼接分支。模拟退出 code=127,terminate 调用数为 0;对已退出进程无需再杀,但其它握手失败类型仍缺统一清理。
|
|
121
|
+
- 真实基线曾因测试 TEMP 放在沙箱 workspace 内而被 runner 拒绝。runner 的明确原因是 Windows ACL temp root must be outside the workspace,而工具只报告 session died code=127。将测试 TEMP 移到获准的同级目录后,原七条沙箱回归全过。这是测试设置错误,未计为插件沙箱创建失败;丢诊断则确实来自插件。
|
|
122
|
+
- 20 秒握手 race 的定时器未 clear,也未 unref;成功启动后仍留到到期。这是源码确认的资源问题。
|
|
123
|
+
|
|
124
|
+
建议:try/catch/finally 统一握手失败清理、补 stderr 尾巴、撤销定时器;将启动警告存为会话属性并在第一条用户调用显示;句柄隔离未成立时,应明确决定是否拒绝继续使用。
|
|
125
|
+
|
|
126
|
+
## A9:超时参数没有合法范围上限(低)
|
|
127
|
+
|
|
128
|
+
位置:[只做正数和取整](https://github.com/citydirector/dsh-pwsh-plugin/blob/2ea7783103328cbdf568aeae596aa9d299f661fc/plugin/index.mjs#L277)、[加 5000ms 后交给 AbortSignal.timeout](https://github.com/citydirector/dsh-pwsh-plugin/blob/2ea7783103328cbdf568aeae596aa9d299f661fc/plugin/index.mjs#L296)。
|
|
129
|
+
|
|
130
|
+
模拟工具传入 timeoutMs=4294967295 后,抛出:
|
|
131
|
+
|
|
132
|
+
> The value of "delay" is out of range. It must be >= 0 && <= 4294967295. Received 4_294_972_295
|
|
133
|
+
|
|
134
|
+
参数 schema 允许 number,没有 maximum。小于 1 的正小数还会被截为 0。这些错误发生在会话可能已经启动之后。
|
|
135
|
+
|
|
136
|
+
建议:在 acquire 前验证为受支持范围内的正整数;为 RESPONSE_GRACE_MS 留足上界,返回明确参数错误。
|
|
137
|
+
|
|
138
|
+
## 另一个需要约定的接口边界
|
|
139
|
+
|
|
140
|
+
[缺少 session ID 时使用 default](https://github.com/citydirector/dsh-pwsh-plugin/blob/2ea7783103328cbdf568aeae596aa9d299f661fc/plugin/index.mjs#L171):所有没有身份的调用会共用进程。真实 DSH 正常请求带 session ID,本轮没有发现真实会话 ID 冲突;这不是“正常两个会话会串变量”的证据。若支持其它调用方,应明确拒绝无 ID,或给调用方分配不会相撞的身份。
|
|
141
|
+
|
|
142
|
+
## 已确认正常和既定限制
|
|
143
|
+
|
|
144
|
+
正常项:不同真实 ID 的变量、环境变量分别保持 A/B;热会话的串行输出不混线;字面 here-string、中文 UTF-8 原生 stdout/stderr 与 stdin、非零 LASTEXITCODE、异常后恢复、普通超时及重建正常。ThreadJob 的简单 Wait/Receive 流程返回 THREAD-OK。
|
|
145
|
+
|
|
146
|
+
既定限制:[运行边界说明](runtime-notes.zh-CN.md)。Read-Host 的实测结果为不支持交互的明确错误;exit 7 结束本次 runspace 调用,但不提供该 exit 数字;Console.WriteLine 被吞;受限令牌下部分原生程序的管道创建被拒;升级/卸载插件会销毁常驻会话。它们应清楚告知调用方,不能全部当成新发现的 bug。
|
|
147
|
+
|
|
148
|
+
本轮没有更改凭据、登录状态、PATH、WSL 服务、系统注册表或用户目录。也没有测试破坏性命令或执行填盘、失控输出实验。
|
|
149
|
+
|
|
150
|
+
## 推荐修复顺序及验收条件
|
|
151
|
+
|
|
152
|
+
1. 先修 A1/A3:权限身份和创建槽位。验收同 ID 切只读/切根时绝不继续使用旧边界;首次并发只 spawn 一次。
|
|
153
|
+
2. 再修 A2/A4/A5:忙会话保护、排队取消与超时、销毁结算。验收第五个会话不能杀前四个中的忙任务;排队短任务超时不能影响当前任务;dispose 后请求立即结束。
|
|
154
|
+
3. 再修 A7/A8:资源上限与启动诊断。验收中文字节预算、错误保留、Console 无持续积存、握手失败可诊断且无遗漏清理。
|
|
155
|
+
4. 最后处理 A6/A9 与文档契约:常见对象字段、顶层 using、参数边界。补测各受支持 OS/PowerShell 版本和 UAC 标准用户。
|
|
156
|
+
|
|
157
|
+
本报告是修复输入;现有 81 条回归全部通过,不消除上述可复现缺陷。临时探针完成后回收,生产源码保持本次审计的原状。
|
|
158
|
+
|
|
159
|
+
## 独立的宿主网络问题
|
|
160
|
+
|
|
161
|
+
Schannel / curl / Invoke-WebRequest 的对照矩阵、令牌变量隔离、来源提交和新版核对,已移至 [DSH Windows 沙箱与 Schannel 兼容性记录](dsh-windows-schannel.zh-CN.md)。这是宿主后端问题,不计入本插件 A1–A9 修复。
|
|
162
|
+
|
|
163
|
+
## 本地修复验收(2026-11-12)
|
|
164
|
+
|
|
165
|
+
以下结果记录修复发版前的本地验收;安装件需更新到包含这些修复的版本后才会使用新实现。原始审计结果保留为历史基线。
|
|
166
|
+
|
|
167
|
+
| 组 | 已实现行为 | 验证 |
|
|
168
|
+
|---|---|---|
|
|
169
|
+
| A1 | 每次解析当前策略、工作区和沙箱后端,变化就销毁旧进程;只读拒绝正文;无 ID、工作目录服务错误、无效 confine 结果均拒绝 | 真实沙箱同 ID 降权、A→B 切根;模拟后端变化和错误分支 |
|
|
170
|
+
| A2 | 容量与 idle 只回收没有执行、排队或启动请求的实例;满池全忙明确拒绝;结束后计算闲置 | 默认四个实例同时忙,第五个不能淘汰它们;缩短 idle 不打断长调用 |
|
|
171
|
+
| A3 | 第一次 await 前登记 ID 槽位,创建与执行共用队列 | 真实沙箱首次并发返回同一 PID,状态累计到 2 |
|
|
172
|
+
| A4 | 从实际发送起计超时;预取消不执行;队列取消只影响自身;执行中取消才销毁 | 5150ms 的队列等待不触发旧的 100+5000ms 误杀;三种取消分支 |
|
|
173
|
+
| A5 | 销毁先结算 pending,再幂等终止;卸载也中止仍在握手的启动 | dispose 立即拒绝在途调用;卸载握手进程被终止且未执行正文 |
|
|
174
|
+
| A6 | 正文保持顶层,workdir 单独设置;对象标准格式化;信息、调试分流;显式核验 requires | 真 PowerShell 的 using、param、对象表格、Get-Process、模块及版本要求 |
|
|
175
|
+
| A7 | 两侧 UTF-8 预算、错误独立 64KiB、帧限长;消费后清集合;Console 与继承输出接空设备 | 中文截断保留错误、空记录限额、帧拆分重组;连续大段 Console 写入后保留内存增量约 28KB |
|
|
176
|
+
| A8 | 启动统一清理和计时器撤销,保留 stderr 尾部与首次警告;隔离失败拒绝运行 | 模拟握手失败、超时、警告一次性显示;真实隔离与原生 UTF-8 输出 |
|
|
177
|
+
| A9 | acquire 前校验整数毫秒 1–2147478647;压缩启动源码并检查命令行预算 | 非法/越界超时在 spawn 前拒绝;gzip 启动与超限检测 |
|
|
178
|
+
|
|
179
|
+
完整回归:protocoltest 6、lifecycletest 16、itest 30、plugintest 15、e2e-real-subprocess 8、sandboxtest 12、bashtest 31、bash-e2e 11、denialtest 11、metatest 23,合计 **163 项**。回归失败会返回非零退出码。
|
|
180
|
+
|
|
181
|
+
平台复验仍限 Windows 11 / PowerShell 7.6.8 / Node 24.22.1 与本机 DSH LocalSandboxProvider。Linux/macOS、其它 PowerShell 小版本、UAC 标准用户尚未重测。`#requires -RunAsAdministrator` 不能用“这是受限令牌”作为必然拒绝的测试前提:受限令牌仍可能保留管理员组身份,因此沙箱负向用例采用确定不满足的版本 999.0。
|
|
182
|
+
|
|
183
|
+
宿主 Schannel 故障不由这些修复解决,仍见独立记录。
|