dsh-pwsh-plugin 0.2.17 → 0.2.19

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 CHANGED
@@ -1,6 +1,8 @@
1
1
  # dsh-pwsh-plugin
2
2
 
3
- [English](README.md) · [简体中文](README.zh-CN.md)
3
+ **English** · [简体中文](README.zh-CN.md)
4
+
5
+ [![npm](https://img.shields.io/npm/v/dsh-pwsh-plugin)](https://www.npmjs.com/package/dsh-pwsh-plugin) [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE) [![DSH plugin](https://img.shields.io/badge/DSH%20plugin-%E2%89%A50.2.0--rc.2-blue)](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
@@ -114,24 +116,25 @@ Three constraints that cost real time to find:
114
116
  will hold two of them; registration is keyed by name, and a second `pwsh_run` is a hard error at boot.
115
117
  `--dump-config` shows the duplicate plainly — that is the check to run after switching.
116
118
 
117
- The three files in `plugin/` (`index.mjs`, `session.mjs`, `session-server.ps1`) must stay together —
119
+ The files in `plugin/` (`index.mjs`, `config.mjs`, `native-pwsh-guard.mjs`, `session.mjs`, `session-server.ps1`) must stay together —
118
120
  `index.mjs` reads the `.ps1` at load time and ships it to the child as `-EncodedCommand`.
119
121
 
120
122
  ## The tools
121
123
 
122
- `pwsh_run` runs a script. Versions are managed by DSH itself — see [Updates](#updates).
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 | Per-call timeout, default 120000. |
130
+ | `timeoutMs` | no | Execution timeout, excluding queue waiting; default 120000. Integer milliseconds from 1 to 2147478647. |
129
131
 
130
- Output: stdout as-is, warnings prefixed `WARNING: `, verbose prefixed `VERBOSE: `, a thrown exception
131
- carries `[script threw]` plus the innermost message. Errors (`Write-Error`, a child process's stderr)
132
- come **after** stdout — the server merges every stream through `*>&1` and tags lines by type, so
133
- **line-by-line interleaving of stdout and stderr is not preserved** (the built-in tool keeps two columns).
134
- When order matters, merge them yourself: `2>&1` or `*>&1`.
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,9 +143,10 @@ 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` | How many sessions may live at once (one per agent workspace). |
144
- | `maxOutputBytes` | `8388608` | Output collection cap per call (8 MB). Truncation is reported honestly, never silently. |
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. |
149
+ | `enforcePwshRun` | `false` | Refuse ordinary native `pwsh` calls before execution and direct the model to `pwsh_run`. |
146
150
 
147
151
  `allowUnconfined` is the one deliberate escape hatch. In a normal deployment `ctx.sandboxPolicy` is always
148
152
  present; when it is missing the composition is incomplete, and the plugin **refuses to start** rather than
@@ -151,6 +155,39 @@ quietly running an unconstrained session.
151
155
  Hot reload (`patchReload: live`) re-runs `apply`. Tool registration and process cleanup both hang off
152
156
  `ctx.effect`, so it does not trip `tool "pwsh_run" is already registered`.
153
157
 
158
+ ### Enforce pwsh_run
159
+
160
+ This is a profile-wide plugin setting. Configure it once for all presets and sessions in that profile;
161
+ do not add it to each preset or insert a second `pwsh-run` entry when the bundle already provides one.
162
+
163
+ Open **pwsh settings** in the settings sidebar, enable **Require pwsh_run**, then select **Save**. The plugin row's configuration button opens the same form. Changes stay as a draft until saved; leaving the page discards the draft. **Restore configuration default** removes the profile override.
164
+
165
+ Saving updates only the routing switch through DSH's settings service and preserves running PowerShell processes and session state. The page reports the value accepted by the host, protects against conflicting writes, and disables editing on read-only deployments. It follows the host's Chinese or English language setting.
166
+
167
+ For hosts without the settings UI, override the existing plugin entry in the profile's `cordis.patch.yml`:
168
+
169
+ ```yaml
170
+ - id: pwsh-run
171
+ config:
172
+ enforcePwshRun: true
173
+ ```
174
+
175
+ Ordinary native `pwsh` calls are refused before execution. The response tells the model to move
176
+ `command` into `pwsh_run`'s `script` and preserve `workdir` and `timeoutMs`.
177
+ The command is neither executed first nor replayed automatically. Both native dispatch and
178
+ PTC's `tools.pwsh(...)` pass through this check.
179
+
180
+ Three native entry points remain available: read-only sessions (unsupported by the persistent
181
+ session), jobs with `run_in_background: true`, and per-call escalation requests with a nonempty
182
+ `justification` and a requested mode strictly wider than the current one. Escalation still needs
183
+ host approval; a same-mode flag does not qualify. The guard resolves the actual session policy
184
+ on each call, so mode changes take effect on the next invocation.
185
+
186
+ Disabling the option, removing the guard through hot reload, or uninstalling restores native
187
+ `pwsh` behavior. The option defaults to off. Live configuration requires `ctx.tools.guard()`;
188
+ a host without this API is rejected explicitly so enabling the setting cannot silently go unprotected.
189
+ The guard covers the DSH `pwsh` tool entry, not PowerShell launched inside bash, Node or other scripts.
190
+
154
191
  ## Sandbox
155
192
 
156
193
  The persistent process is wrapped **once, at launch**, in the restricted argv that `ctx.sandbox.confine`
@@ -158,9 +195,12 @@ returns. The Windows ACL sandbox uses a `WRITE_RESTRICTED` token, and a token is
158
195
  after that single confine, every later read and write in the session obeys the same workspace boundary as
159
196
  the built-in `pwsh` tool.
160
197
 
161
- Policy is resolved against the **real session at launch time** (`session` must be passed into
162
- `sandboxPolicy.resolve`): the session's `sandbox/mode` wins, and the boundary root comes from the
163
- session's `header.cwd`.
198
+ Every call resolves policy against the **current real session** (`session` must reach
199
+ `sandboxPolicy.resolve`) and checks the effective policy, workspace and sandbox backend. A change
200
+ disposes the old process before any new script; read-only refuses the body. Permission or workspace
201
+ changes therefore clear persistent state too. Working-directory service errors are surfaced instead
202
+ of falling back to stale paths. Creation and execution share one queue per ID; only sessions with no
203
+ executing, queued or starting requests can be evicted.
164
204
 
165
205
  A trap worth knowing: passing the ToolExecution instead (`agent` lives on `exec.agent`) means `resolve`
166
206
  never sees a session and **silently falls back to the deployment default** (mode + configured
@@ -176,7 +216,7 @@ Under `danger-full-access` nothing is confined, exactly like the built-in tool.
176
216
  **`read-only` does not start.** In that mode pwsh runs in ConstrainedLanguage and cannot even create a
177
217
  runspace. The tool reports that clearly and points at the built-in `pwsh` tool, which works there.
178
218
 
179
- ## Updates
219
+ ## Updating
180
220
 
181
221
  This plugin carries no updater of its own. It is an ordinary registry package, so **DSH manages its
182
222
  version**: the Plugins page lists it, shows when a newer release exists, and installs it — the same
@@ -244,9 +284,12 @@ your own machine — it derives every path from its own location and re-checks t
244
284
  | Output looks reordered | stdout and stderr are merged by type, not by time. Merge them yourself with `2>&1`. |
245
285
  | `exit N` code is not reported | Runspace semantics — use `Write-Output` or read `$LASTEXITCODE`. |
246
286
  | 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 `拒绝访问`) | Expected under `workspace-write`: the restricted token cannot create anonymous pipes, so **any** launch that captures a child's stdio fails — `& exe` included. Use `Start-Process -RedirectStandardOutput/-RedirectStandardError <file>` (captures output, no exit code), `[Diagnostics.Process]::Start` with `UseShellExecute=$false` and no redirection (exit code, child inherits stdout), or a `.cmd` wrapper that writes stdout, stderr and `%ERRORLEVEL%` to three files. The tool appends this same advice to the output. |
287
+ | `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`. |
288
+
289
+ ## Known limitations
248
290
 
249
- ## Known limits
291
+ The [audit and local fix record](docs/pwsh-run-audit.zh-CN.md) covers the nine issue groups.
292
+ The host TLS failure has a separate [Schannel investigation](docs/dsh-windows-schannel.zh-CN.md).
250
293
 
251
294
  The full list — session semantics, detached child std handles, UAC differences, and what an update does to a
252
295
  session — lives in **[docs/runtime-notes.md](docs/runtime-notes.md)**. The short version:
@@ -262,10 +305,17 @@ session — lives in **[docs/runtime-notes.md](docs/runtime-notes.md)**. The sho
262
305
  ```
263
306
  plugin/
264
307
  index.mjs tool registration, sandbox wiring, timeouts
308
+ config.mjs native schema; the routing switch is volatile
309
+ native-pwsh-guard.mjs optional native pwsh invocation guard
265
310
  session.mjs the session object: framing, queue, base64 encode, boot argv
266
311
  session-server.ps1 the PowerShell side: runspace, stream tagging, sentinels
312
+ client/index.js localized settings page using DSH native controls
267
313
  test/
314
+ settingstest.mjs persistence, conflicts, live routing and process preservation
268
315
  support.mjs locates a DSH install; converts Windows paths to /mnt form
316
+ routingtest.mjs native/PTC routing, exceptions, unload and reload
317
+ protocoltest.mjs frame limits, UTF-8 budgets, startup cleanup
318
+ lifecycletest.mjs policy changes, session pool, cancellation
269
319
  itest.mjs protocol and session core
270
320
  plugintest.mjs the whole plugin path against a mock context
271
321
  e2e-real-subprocess.mjs real DSH subprocess runtime
@@ -285,20 +335,26 @@ FINDINGS.md every measurement and the reasoning behind the design
285
335
 
286
336
  ```bash
287
337
  cd <checkout>
288
- PWSH_BIN='pwsh' node test/itest.mjs # 17: protocol and session core
338
+ pnpm install --ignore-scripts --config.auto-install-peers=false
339
+ node test/settingstest.mjs # 19: native settings form, persistence, routing and state
340
+ node test/routingtest.mjs # 30: native/PTC routing, exceptions, disable and unload
341
+ node test/protocoltest.mjs # 6: frame limits, UTF-8 budgets, startup cleanup
342
+ node test/lifecycletest.mjs # 16: policy, pool, concurrency, cancellation, unload
343
+ PWSH_BIN='pwsh' node test/itest.mjs # 30: protocol and session core
289
344
  PWSH_BIN='pwsh' node test/plugintest.mjs # 15: full plugin path
290
345
  node test/e2e-real-subprocess.mjs # 8: real DSH subprocess
291
- node test/sandboxtest.mjs # 7: real sandbox boundary + fail-closed
346
+ node test/sandboxtest.mjs # 12: real sandbox, downgrade, root switch, first concurrency
292
347
  node test/bashtest.mjs # 31: custom-bash fence triage, bash fallback, exec-bit repair
293
348
  node test/bash-e2e.mjs # 11: custom-bash end to end
294
349
  node test/denialtest.mjs # 11: sandbox-denial hint (no spawn, runs anywhere)
295
350
  node test/metatest.mjs # 23: plugin-page metadata (no spawn, runs anywhere)
296
351
  ```
297
352
 
298
- Eight scripts, **123 assertions**, none of which needs DSH to be running — just node 24, PowerShell 7 and a
353
+ Twelve scripts, **212 assertions**, none of which needs DSH to be running — just node 24, PowerShell 7 and a
299
354
  DSH install to borrow `node_modules` from. Tests that need a DSH runtime locate one automatically
300
355
  (`DSH_MODULES`, `DSH_MODULES_ASAR`, or by walking up from the working directory) and print a `SKIP` hint
301
- instead of failing when none is found.
356
+ when none is found. The settings integration test exits unsuccessfully if its host modules are unavailable;
357
+ a release validation must complete every suite without skips.
302
358
 
303
359
  ### Releasing
304
360
 
@@ -327,6 +383,19 @@ new content). With no tag on HEAD it prints the exact name to create.
327
383
  Use the Windows-side npm. In WSL, `/usr/local/bin/npm` resolves to a bun symlink and `npm version` fails
328
384
  with `Script not found "version"`.
329
385
 
386
+ ## Related projects
387
+
388
+ More DeepSeek Harness (DSH) projects from [@citydirector](https://github.com/citydirector):
389
+
390
+ | Project | Kind | What it does |
391
+ |---|---|---|
392
+ | [DSH-build](https://github.com/citydirector/DSH-build) | Build | Portable and desktop Windows builds of DSH, patched, smoke-tested and released daily |
393
+ | [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 |
394
+ | [deepinfra-fusion-proxy](https://github.com/citydirector/deepinfra-fusion-proxy) | Plugin | DeepInfra Standard / Flex switching with automatic fallback, per session |
395
+ | [dsh-loop-rescue](https://github.com/citydirector/dsh-loop-rescue) | Plugin | Cut model repetition loops mid-stream, prune them from the log, and resume |
396
+ | [ui-locate-mcp](https://github.com/citydirector/ui-locate-mcp) | MCP plugin | Pixel-exact coordinates for on-screen text and icons, for desktop automation |
397
+ | [dsh-session-manager](https://github.com/citydirector/dsh-session-manager) | Offline tool | Upgrade, archive, trash, loop cleanup and repair for DSH sessions |
398
+
330
399
  ## License
331
400
 
332
- MIT — see [LICENSE](LICENSE).
401
+ [MIT](LICENSE)
package/README.zh-CN.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  [English](README.md) · **简体中文**
4
4
 
5
+ [![npm](https://img.shields.io/npm/v/dsh-pwsh-plugin)](https://www.npmjs.com/package/dsh-pwsh-plugin) [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE) [![DSH plugin](https://img.shields.io/badge/DSH%20plugin-%E2%89%A50.2.0--rc.2-blue)](https://github.com/topics/dsh-plugin)
6
+
5
7
  给 DeepSeek Harness 的 `pwsh_run` 工具:PowerShell 脚本正文**经 stdin** 送进一个常驻 pwsh 7 进程 ——
6
8
  路上不做任何转义、没有 32K 命令行上限、~400ms 冷启动只付一次,变量 / 当前目录 / 已加载模块跨调用保留。
7
9
 
@@ -99,7 +101,7 @@ $env:DSH_HOME = '<DSH_HOME>' # 桌面 App 自己用的那个数据根
99
101
  合成后的配置树里就有两条;注册按名字查重,第二个 `pwsh_run` 在启动时是硬错误。`--dump-config` 一眼就能看出重复 ——
100
102
  切换后跑这一条。
101
103
 
102
- `plugin/` 下三个文件(`index.mjs`、`session.mjs`、`session-server.ps1`)必须放在一起:
104
+ `plugin/` 下的 `index.mjs`、`config.mjs`、`native-pwsh-guard.mjs`、`session.mjs`、`session-server.ps1` 必须放在一起:
103
105
  `index.mjs` 在加载时读那个 `.ps1`,再以 `-EncodedCommand` 送给子进程。
104
106
 
105
107
  ## 工具接口
@@ -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` | 否 | 单次超时,默认 120000。 |
115
+ | `timeoutMs` | 否 | 实际执行的超时,默认 120000;不含排队等待。必须为 1–2147478647 的整数毫秒。 |
114
116
 
115
- 输出:stdout 原样,warning 前缀 `WARNING: `,verbose 前缀 `VERBOSE: `,抛出的异常带 `[script threw]`
116
- 与最内层消息。错误(`Write-Error`、子进程的 stderr)排在 stdout **之后** —— 服务端用 `*>&1` 合并全部
117
- 流并按类型打标,所以 **stdout 与 stderr 的逐行时序不保留**(内置工具是分开两栏的)。需要顺序时自己
118
- 合并:`2>&1` 或 `*>&1`。
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,9 +126,10 @@ $env:DSH_HOME = '<DSH_HOME>' # 桌面 App 自己用的那个数据根
124
126
  | `pwshPath` | `pwsh` | 可执行文件;默认经 `PATH` 解析。 |
125
127
  | `timeoutMs` | `120000` | 单次调用默认超时。 |
126
128
  | `idleMs` | `1800000` | 空闲会话回收(30 分钟)。 |
127
- | `maxSessions` | `4` | 同时存活的会话上限(每个 agent 工作区一个)。 |
128
- | `maxOutputBytes` | `8388608` | 单次调用的输出收集上限(8MB)。截断会明确报出来,不静默。 |
129
+ | `maxSessions` | `4` | 每个插件实例的会话上限,以 DSH session ID 隔离;全部忙时拒绝新会话并提示重试。 |
130
+ | `maxOutputBytes` | `8388608` | 每次调用普通输出正文的 UTF-8 字节预算(含记录换行);错误另留 65536 字节。协议与显示标签不计入,截断会明确提示。 |
129
131
  | `allowUnconfined` | `false` | `true` = 沙箱策略服务缺席时也起会话。 |
132
+ | `enforcePwshRun` | `false` | 开启后,在执行前拒绝普通原生 `pwsh` 调用,提示改用 `pwsh_run`。 |
130
133
 
131
134
  `allowUnconfined` 是唯一一条「明知故犯」的开关。正常部署下 `ctx.sandboxPolicy` 一定在;缺席说明组合
132
135
  不完整,这时插件**拒绝启动**,而不是悄悄跑一个不受约束的会话。
@@ -134,14 +137,45 @@ $env:DSH_HOME = '<DSH_HOME>' # 桌面 App 自己用的那个数据根
134
137
  热重载(`patchReload: live`)会重新 `apply`。工具注册与进程清理都挂在 `ctx.effect` 上,不会触发
135
138
  `tool "pwsh_run" is already registered`。
136
139
 
140
+ ### 强制使用 pwsh_run
141
+
142
+ 这是 profile 的公共插件配置:同一 profile 只需设置一次,覆盖其中不同 preset 和会话,
143
+ 不需要在每个 preset 中重复添加。插件已由 bundle 加载时,也不要重复插入 `pwsh-run` 条目。
144
+
145
+ 在设置侧栏打开 **pwsh 设置**,开启 **强制使用 pwsh_run**,再点 **保存**。插件列表的配置按钮也会打开同一张表单。编辑只产生草稿,离开页面会丢弃未保存的修改;**恢复配置默认值** 会移除当前 profile 的覆盖。
146
+
147
+ 保存通过 DSH 设置服务热更新路由开关,保留正在运行的 PowerShell 进程和会话状态。页面显示宿主确认的已保存值,校验配置修订冲突,并在只读部署下禁用修改。页面随宿主语言显示中文或英文。
148
+
149
+ 没有设置界面的宿主仍可在 profile 的 `cordis.patch.yml` 中覆盖已有插件条目:
150
+
151
+ ```yaml
152
+ - id: pwsh-run
153
+ config:
154
+ enforcePwshRun: true
155
+ ```
156
+
157
+ 守卫会在命令执行前拒绝普通原生 `pwsh` 调用,并提示模型把 `command` 改为
158
+ `pwsh_run` 的 `script`;`workdir` 和 `timeoutMs` 保持原值。原生命令不会先执行一次,
159
+ 也不会自动重放。原生工具和 PTC 中的 `tools.pwsh(...)` 都经过同一道检查。
160
+
161
+ 三个必要入口仍交给原生工具:只读会话(常驻会话不支持)、`run_in_background: true`
162
+ 的后台任务、以及带非空 `justification` 且申请权限确实高于当前模式的逐次提权请求。
163
+ 提权仍需宿主审批;同级权限标记不放行。每次按实际会话读取模式,切换权限后立即按新模式判断。
164
+
165
+ 配置关闭、热重载移除守卫或卸载插件后,原生 `pwsh` 恢复原有行为。
166
+ 开关默认关闭。热更新配置需要宿主提供 `ctx.tools.guard()`;缺少此 API 时会明确拒绝加载,避免设置开启后实际没有拦截。
167
+ 它只管理 DSH 的 `pwsh` 工具入口,不解析 `bash`、Node 或其他脚本内启动的 PowerShell。
168
+
137
169
  ## 沙箱
138
170
 
139
171
  常驻进程在**启动时**整体包进 `ctx.sandbox.confine` 返回的受限 argv。Windows ACL 沙箱用的是
140
172
  `WRITE_RESTRICTED` 受限令牌,而令牌是**进程级**的 —— 所以 confine 一次之后,该会话的每一次读 / 写都
141
173
  受同一个 workspace 边界约束,与内置 `pwsh` 工具等价。
142
174
 
143
- 策略按**启动会话时的真实 session** 解析(`session` 必须传进 `sandboxPolicy.resolve`):会话的
144
- `sandbox/mode` 优先,边界根取会话的 `header.cwd`。
175
+ 每次调用先按**当前真实 session** 重新解析策略(`session` 必须传进 `sandboxPolicy.resolve`),并核对
176
+ 有效策略、工作区和沙箱后端。任一项变化就销毁旧进程,再按新边界创建;切到只读时拒绝正文。
177
+ 因此权限或工作区切换也会清空常驻状态。工作目录服务出错时直接报错,不回退到旧目录。
178
+ 同一 ID 的创建与执行共用队列;只淘汰没有执行、排队或启动请求的空闲会话。
145
179
 
146
180
  一个值得记住的坑:早先传的是 ToolExecution(`agent` 挂在 `exec.agent` 上),`resolve` 拿不到
147
181
  session,**静默退回部署默认值**(mode + 配置里的 `workspaceRoot`),边界于是跑到 profile 目录去了 ——
@@ -214,10 +248,13 @@ Windows 上打,那份文件到手时是 `0644` —— 探测发现它属于当
214
248
  | 输出顺序看着不对 | stdout 与 stderr 按类型合并,不按时间。自己用 `2>&1` 合。 |
215
249
  | `exit N` 的退出码拿不到 | runspace 语义 —— 用 `Write-Output` 或读 `$LASTEXITCODE`。 |
216
250
  | 调用结束后子进程还占着文件 | 会话持有 `Process` 对象。等 `idleMs`,或者用一次超时强制重建。 |
217
- | `Program 'node.exe' failed to run: 拒绝访问。` | `workspace-write` 下的正常现象:受限令牌建不了匿名管道,凡是「捕获子进程 stdio」的起法都失败,`& exe` 也在其中。改用 `Start-Process -RedirectStandardOutput/-RedirectStandardError <文件>`(能拿到输出,拿不到退出码)、`[Diagnostics.Process]::Start`(`UseShellExecute=$false`、不重定向,能拿退出码但子进程继承 stdout),或一个把 stdout / stderr / `%ERRORLEVEL%` 分别写进三个文件的 `.cmd` 包装器。工具也会把这段说明附在输出末尾。 |
251
+ | `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
252
 
219
253
  ## 已知边界
220
254
 
255
+ 九组问题及修复验收见[审计报告](docs/pwsh-run-audit.zh-CN.md);宿主 HTTPS 问题单独记在
256
+ [Schannel 兼容性记录](docs/dsh-windows-schannel.zh-CN.md)。
257
+
221
258
  完整清单(会话语义、已断开的子进程 std handle、UAC 差异、升级对会话的影响)见
222
259
  **[docs/runtime-notes.zh-CN.md](docs/runtime-notes.zh-CN.md)**。要点:
223
260
 
@@ -232,10 +269,17 @@ Windows 上打,那份文件到手时是 `0644` —— 探测发现它属于当
232
269
  ```
233
270
  plugin/
234
271
  index.mjs 工具注册、沙箱接线、超时
272
+ config.mjs 宿主配置 schema;路由开关支持热更新
273
+ native-pwsh-guard.mjs 可选的原生 pwsh 调用守卫
235
274
  session.mjs 会话对象:分帧、队列、base64、启动 argv
236
275
  session-server.ps1 PowerShell 侧:runspace、流打标、哨兵
276
+ client/index.js 使用宿主原生控件的双语设置页
237
277
  test/
278
+ settingstest.mjs 配置持久化、冲突、即时路由与进程保留
238
279
  support.mjs 定位 DSH 安装;Windows 路径转 /mnt 形式
280
+ routingtest.mjs 原生与 PTC 路由、必要例外及卸载恢复
281
+ protocoltest.mjs 分帧、UTF-8 预算、启动清理
282
+ lifecycletest.mjs 权限切换、会话池与取消
239
283
  itest.mjs 协议与会话本体
240
284
  plugintest.mjs 插件完整路径(mock 上下文)
241
285
  e2e-real-subprocess.mjs 真 DSH subprocess 运行时
@@ -255,19 +299,25 @@ FINDINGS.md 全部实测数据与设计理由
255
299
 
256
300
  ```bash
257
301
  cd <检出目录>
258
- PWSH_BIN='pwsh' node test/itest.mjs # 17:协议与会话本体
302
+ pnpm install --ignore-scripts --config.auto-install-peers=false
303
+ node test/settingstest.mjs # 19:原生设置表单、持久化、路由和进程保留
304
+ node test/routingtest.mjs # 30:原生/PTC 路由、例外、关闭与卸载
305
+ node test/protocoltest.mjs # 6:帧边界、字节预算、启动清理
306
+ node test/lifecycletest.mjs # 16:权限切换、池、并发、取消、卸载
307
+ PWSH_BIN='pwsh' node test/itest.mjs # 30:协议与会话本体
259
308
  PWSH_BIN='pwsh' node test/plugintest.mjs # 15:插件完整路径
260
309
  node test/e2e-real-subprocess.mjs # 8:真 DSH subprocess
261
- node test/sandboxtest.mjs # 7:真沙箱边界 + fail-closed
310
+ node test/sandboxtest.mjs # 12:真沙箱边界、降权、切根、首次并发
262
311
  node test/bashtest.mjs # 31:custom-bash 围栏分诊、bash 后备、执行位自修复、SSH agent
263
312
  node test/bash-e2e.mjs # 11:custom-bash 端到端
264
313
  node test/denialtest.mjs # 11:沙箱拒绝提示(不起子进程,哪儿都能跑)
265
314
  node test/metatest.mjs # 23:插件页元数据(不起子进程,哪儿都能跑)
266
315
  ```
267
316
 
268
- 八份脚本合计 **123 项**,都不需要 DSH 在跑 —— 只要 node 24、PowerShell 7,以及一个能借
317
+ 十二份脚本合计 **212 项**,都不需要 DSH 在跑 —— 只要 node 24、PowerShell 7,以及一个能借
269
318
  `node_modules` 的 DSH 安装。需要 DSH 运行时的测试会自动找(`DSH_MODULES`、`DSH_MODULES_ASAR`,
270
- 或从工作目录逐级向上),找不到时打一行 `SKIP` 提示而不是失败。
319
+ 或从工作目录逐级向上),找不到时打印 `SKIP` 提示。设置集成测试缺少宿主模块时会非零退出;
320
+ 发版验收必须跑完全部测试,不能有跳过。
271
321
 
272
322
  ### 发版
273
323
 
@@ -293,6 +343,19 @@ HEAD 上还没有 tag 时,它把该打的标签名打印出来。
293
343
  npm 要用 Windows 侧那份。WSL 里 `/usr/local/bin/npm` 指向 bun 的软链,`npm version` 会报
294
344
  `Script not found "version"`。
295
345
 
296
- ## 许可
346
+ ## 相关项目
347
+
348
+ [@citydirector](https://github.com/citydirector) 的其它 DeepSeek Harness(DSH)项目:
349
+
350
+ | 项目 | 类型 | 用途 |
351
+ |---|---|---|
352
+ | [DSH-build](https://github.com/citydirector/DSH-build) | 构建 | DSH 的 Windows 便携版 / 桌面版绿色构建,每天跟随上游打补丁、冒烟、发布 |
353
+ | [dsh-remote-access-proxy](https://github.com/citydirector/dsh-remote-access-proxy) | 插件 | 在手机、平板或其它电脑上用浏览器访问 DSH Web GUI |
354
+ | [deepinfra-fusion-proxy](https://github.com/citydirector/deepinfra-fusion-proxy) | 插件 | DeepInfra Standard / Flex 按会话切换与自动回退 |
355
+ | [dsh-loop-rescue](https://github.com/citydirector/dsh-loop-rescue) | 插件 | 模型复读时从流内切断、剪掉循环并自动续跑 |
356
+ | [ui-locate-mcp](https://github.com/citydirector/ui-locate-mcp) | MCP 插件 | 按界面文字和图标给出精确像素坐标,用于桌面操作 |
357
+ | [dsh-session-manager](https://github.com/citydirector/dsh-session-manager) | 离线工具 | 离线升级、归档、回收站、清洗循环、修复烂尾 / 隐藏会话 |
358
+
359
+ ## 许可证
297
360
 
298
- MIT —— 见 [LICENSE](LICENSE)。
361
+ [MIT](LICENSE)
@@ -0,0 +1,94 @@
1
+ window.__ModuleLoader__.load({
2
+ id: 'dsh-pwsh-plugin',
3
+ factory: (require) => {
4
+ const React = require('react')
5
+ const { SettingsForm, SettingsFormModel, Switch, Button } = require('@deepseek-ai/dsh-client-ui-primitives')
6
+ const h = React.createElement
7
+ const NS = 'pwsh-run'
8
+ const FIELD = 'enforcePwshRun'
9
+ const COPY = {
10
+ zh: {
11
+ title: 'pwsh 设置', group: '工具路由', label: '强制使用 pwsh_run',
12
+ summary: '控制模型是否必须使用常驻 PowerShell 工具。',
13
+ intro: '统一管理当前 profile 的 PowerShell 调用方式。',
14
+ hint: '开启后,普通 pwsh 调用会在执行前被拒绝,并提示模型改用 pwsh_run。',
15
+ scope: '对当前 profile 的所有预设和会话生效。保存后立即应用,正在运行的 pwsh_run 任务和常驻状态会保留。',
16
+ exceptions: '只读会话、明确的后台任务和需要宿主审批的有效提权请求,仍可使用原生 pwsh。',
17
+ on: '已开启', off: '已关闭', pending: '有尚未保存的修改', inherited: '恢复配置默认值',
18
+ form: { unavailable: '配置暂不可用,请确认插件已在当前主机加载。', readOnly: '当前部署的配置只读,无法保存修改。', saveFailed: '保存失败,请刷新配置后重试。', save: '保存', saving: '保存中…' },
19
+ },
20
+ en: {
21
+ title: 'pwsh settings', group: 'Tool routing', label: 'Require pwsh_run',
22
+ summary: 'Choose whether models must use persistent PowerShell.',
23
+ intro: 'Manage PowerShell routing for the current profile.',
24
+ hint: 'When enabled, ordinary pwsh calls are blocked before execution and the model is directed to pwsh_run.',
25
+ scope: 'Applies to every preset and session in this profile. Saving takes effect immediately and preserves running pwsh_run tasks and session state.',
26
+ exceptions: 'Read-only sessions, explicit background jobs, and valid elevation requests that require Host approval retain native pwsh.',
27
+ on: 'Enabled', off: 'Disabled', pending: 'Unsaved changes', inherited: 'Restore configuration default',
28
+ form: { unavailable: 'Configuration unavailable. Check that the plugin is loaded on this Host.', readOnly: 'Configuration is read-only in this deployment.', saveFailed: 'Not saved. Refresh the configuration and try again.', save: 'Save', saving: 'Saving…' },
29
+ },
30
+ }
31
+ const copy = () => (document.documentElement.lang || navigator.language || 'en').toLowerCase().startsWith('zh') ? COPY.zh : COPY.en
32
+ const style = {
33
+ page: { display: 'grid', gap: 24, width: '100%', maxWidth: 680, padding: '8px 0' },
34
+ title: { margin: 0, fontSize: 21, fontWeight: 600, lineHeight: 1.4 },
35
+ intro: { margin: '8px 0 0', fontSize: 13, opacity: 0.65, lineHeight: 1.65 },
36
+ section: { display: 'grid', gap: 16 },
37
+ group: { margin: 0, fontSize: 12, fontWeight: 600, opacity: 0.65 },
38
+ row: { display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 24 },
39
+ field: { display: 'grid', gap: 6 },
40
+ label: { fontSize: 14, fontWeight: 500 },
41
+ note: { margin: 0, fontSize: 13, lineHeight: 1.7, opacity: 0.65 },
42
+ status: { display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: 12, fontSize: 12, opacity: 0.65 },
43
+ reset: { marginLeft: 'auto' },
44
+ }
45
+ function SettingsPage(props) {
46
+ const t = copy()
47
+ if (props.view === 'summary') return t.summary
48
+ const state = props.usePwshSettings((value) => value)
49
+ const disabled = !state.writable || state.saving
50
+ return h('div', { style: style.page, 'data-pwsh-settings': '' },
51
+ h('header', null, h('h2', { style: style.title }, t.title), h('p', { style: style.intro }, t.intro)),
52
+ h(SettingsForm, { labels: t.form, state, onSave: props.save, onDiscard: props.discard },
53
+ h('section', { style: style.section },
54
+ h('h3', { style: style.group }, t.group),
55
+ h('div', { style: style.row },
56
+ h('div', { style: style.field }, h('span', { style: style.label }, t.label), h('p', { style: style.note }, t.hint)),
57
+ h(Switch, { label: t.label, checked: state.field.text === 'true', disabled, onChange: (value) => props.edit(FIELD, value ? 'true' : 'false') })),
58
+ h('div', { style: style.status, role: 'status' },
59
+ h('span', null, state.accepted ? t.on : t.off),
60
+ state.dirty ? h('span', null, t.pending) : null,
61
+ state.field.overridden ? h(Button, { style: style.reset, variant: 'ghost', size: 'sm', disabled, onClick: () => props.resetField(FIELD) }, t.inherited) : null),
62
+ h('p', { style: style.note }, t.scope),
63
+ h('p', { style: style.note }, t.exceptions))))
64
+ }
65
+ function apply(ctx) {
66
+ const scope = ctx.configForms.get(NS)
67
+ const form = new SettingsFormModel(scope, [{
68
+ field: FIELD,
69
+ format: (value) => value === true ? 'true' : 'false',
70
+ parse: (text) => text === 'true' || text === 'false' ? { kind: 'set', value: text === 'true' } : undefined,
71
+ }])
72
+ const store = form.bind(() => ({ ...form.shell(), field: form.field(FIELD), accepted: scope.getSnapshot().value?.[FIELD] === true }))
73
+ const inject = () => ({ hooks: { pwshSettings: store }, ...form.actions() })
74
+ ctx.effect(() => () => form.dispose(), 'pwsh-run.form')
75
+ ctx.effect(() => {
76
+ let dispose = () => {}
77
+ const register = () => {
78
+ dispose()
79
+ dispose = ctx.configForms.whileServed([NS], () => {
80
+ const section = ctx.slots.inject('settings.section', () => ctx.slots.register({ name: 'settings.section', id: 'pwsh-run', order: 43, label: copy().title, inject }, SettingsPage))
81
+ const row = ctx.slots.inject('plugins.row.config', () => ctx.slots.register({ name: 'plugins.row.config', key: 'dsh-pwsh-plugin#pwsh-run', inject }, SettingsPage))
82
+ return () => { row(); section() }
83
+ })
84
+ }
85
+ register()
86
+ // 导航文字由注册项持有,语言改变时重新登记,让宿主刷新入口标题。
87
+ const observer = new MutationObserver(register)
88
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ['lang'] })
89
+ return () => { observer.disconnect(); dispose() }
90
+ }, 'pwsh-run.settings-page')
91
+ }
92
+ return { name: 'pwsh-run-settings', inject: ['slots', 'configForms'], apply }
93
+ },
94
+ })
package/cordis.patch.yml CHANGED
@@ -13,6 +13,7 @@
13
13
  timeoutMs: 120000
14
14
  idleMs: 1800000
15
15
  maxSessions: 4
16
+ enforcePwshRun: false
16
17
 
17
18
  # Windows 上的 bash:官方 persistent bash 依赖 PTY,PTY 后端只支持 linux/darwin,
18
19
  # 而官方 bash 工具在 win32 上被禁用,Windows 会话因此没有 bash。这一行补上它,
@@ -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客户端。