dsh-pwsh-plugin 0.2.18 → 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 +46 -3
- package/README.zh-CN.md +42 -3
- package/client/index.js +94 -0
- package/cordis.patch.yml +1 -0
- package/package.json +22 -4
- package/plugin/config.mjs +13 -0
- package/plugin/index.mjs +8 -0
- package/plugin/native-pwsh-guard.mjs +43 -0
package/README.md
CHANGED
|
@@ -116,7 +116,7 @@ Three constraints that cost real time to find:
|
|
|
116
116
|
will hold two of them; registration is keyed by name, and a second `pwsh_run` is a hard error at boot.
|
|
117
117
|
`--dump-config` shows the duplicate plainly — that is the check to run after switching.
|
|
118
118
|
|
|
119
|
-
The
|
|
119
|
+
The files in `plugin/` (`index.mjs`, `config.mjs`, `native-pwsh-guard.mjs`, `session.mjs`, `session-server.ps1`) must stay together —
|
|
120
120
|
`index.mjs` reads the `.ps1` at load time and ships it to the child as `-EncodedCommand`.
|
|
121
121
|
|
|
122
122
|
## The tools
|
|
@@ -146,6 +146,7 @@ requirements are explicitly refused.
|
|
|
146
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
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. |
|
|
148
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`. |
|
|
149
150
|
|
|
150
151
|
`allowUnconfined` is the one deliberate escape hatch. In a normal deployment `ctx.sandboxPolicy` is always
|
|
151
152
|
present; when it is missing the composition is incomplete, and the plugin **refuses to start** rather than
|
|
@@ -154,6 +155,39 @@ quietly running an unconstrained session.
|
|
|
154
155
|
Hot reload (`patchReload: live`) re-runs `apply`. Tool registration and process cleanup both hang off
|
|
155
156
|
`ctx.effect`, so it does not trip `tool "pwsh_run" is already registered`.
|
|
156
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
|
+
|
|
157
191
|
## Sandbox
|
|
158
192
|
|
|
159
193
|
The persistent process is wrapped **once, at launch**, in the restricted argv that `ctx.sandbox.confine`
|
|
@@ -271,10 +305,15 @@ session — lives in **[docs/runtime-notes.md](docs/runtime-notes.md)**. The sho
|
|
|
271
305
|
```
|
|
272
306
|
plugin/
|
|
273
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
|
|
274
310
|
session.mjs the session object: framing, queue, base64 encode, boot argv
|
|
275
311
|
session-server.ps1 the PowerShell side: runspace, stream tagging, sentinels
|
|
312
|
+
client/index.js localized settings page using DSH native controls
|
|
276
313
|
test/
|
|
314
|
+
settingstest.mjs persistence, conflicts, live routing and process preservation
|
|
277
315
|
support.mjs locates a DSH install; converts Windows paths to /mnt form
|
|
316
|
+
routingtest.mjs native/PTC routing, exceptions, unload and reload
|
|
278
317
|
protocoltest.mjs frame limits, UTF-8 budgets, startup cleanup
|
|
279
318
|
lifecycletest.mjs policy changes, session pool, cancellation
|
|
280
319
|
itest.mjs protocol and session core
|
|
@@ -296,6 +335,9 @@ FINDINGS.md every measurement and the reasoning behind the design
|
|
|
296
335
|
|
|
297
336
|
```bash
|
|
298
337
|
cd <checkout>
|
|
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
|
|
299
341
|
node test/protocoltest.mjs # 6: frame limits, UTF-8 budgets, startup cleanup
|
|
300
342
|
node test/lifecycletest.mjs # 16: policy, pool, concurrency, cancellation, unload
|
|
301
343
|
PWSH_BIN='pwsh' node test/itest.mjs # 30: protocol and session core
|
|
@@ -308,10 +350,11 @@ node test/denialtest.mjs # 11: sandbox-denial hint (no spawn
|
|
|
308
350
|
node test/metatest.mjs # 23: plugin-page metadata (no spawn, runs anywhere)
|
|
309
351
|
```
|
|
310
352
|
|
|
311
|
-
|
|
353
|
+
Twelve scripts, **212 assertions**, none of which needs DSH to be running — just node 24, PowerShell 7 and a
|
|
312
354
|
DSH install to borrow `node_modules` from. Tests that need a DSH runtime locate one automatically
|
|
313
355
|
(`DSH_MODULES`, `DSH_MODULES_ASAR`, or by walking up from the working directory) and print a `SKIP` hint
|
|
314
|
-
|
|
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.
|
|
315
358
|
|
|
316
359
|
### Releasing
|
|
317
360
|
|
package/README.zh-CN.md
CHANGED
|
@@ -101,7 +101,7 @@ $env:DSH_HOME = '<DSH_HOME>' # 桌面 App 自己用的那个数据根
|
|
|
101
101
|
合成后的配置树里就有两条;注册按名字查重,第二个 `pwsh_run` 在启动时是硬错误。`--dump-config` 一眼就能看出重复 ——
|
|
102
102
|
切换后跑这一条。
|
|
103
103
|
|
|
104
|
-
`plugin/`
|
|
104
|
+
`plugin/` 下的 `index.mjs`、`config.mjs`、`native-pwsh-guard.mjs`、`session.mjs`、`session-server.ps1` 必须放在一起:
|
|
105
105
|
`index.mjs` 在加载时读那个 `.ps1`,再以 `-EncodedCommand` 送给子进程。
|
|
106
106
|
|
|
107
107
|
## 工具接口
|
|
@@ -129,6 +129,7 @@ Warning、Verbose、Information、Debug 分别带类型前缀,错误和最内
|
|
|
129
129
|
| `maxSessions` | `4` | 每个插件实例的会话上限,以 DSH session ID 隔离;全部忙时拒绝新会话并提示重试。 |
|
|
130
130
|
| `maxOutputBytes` | `8388608` | 每次调用普通输出正文的 UTF-8 字节预算(含记录换行);错误另留 65536 字节。协议与显示标签不计入,截断会明确提示。 |
|
|
131
131
|
| `allowUnconfined` | `false` | `true` = 沙箱策略服务缺席时也起会话。 |
|
|
132
|
+
| `enforcePwshRun` | `false` | 开启后,在执行前拒绝普通原生 `pwsh` 调用,提示改用 `pwsh_run`。 |
|
|
132
133
|
|
|
133
134
|
`allowUnconfined` 是唯一一条「明知故犯」的开关。正常部署下 `ctx.sandboxPolicy` 一定在;缺席说明组合
|
|
134
135
|
不完整,这时插件**拒绝启动**,而不是悄悄跑一个不受约束的会话。
|
|
@@ -136,6 +137,35 @@ Warning、Verbose、Information、Debug 分别带类型前缀,错误和最内
|
|
|
136
137
|
热重载(`patchReload: live`)会重新 `apply`。工具注册与进程清理都挂在 `ctx.effect` 上,不会触发
|
|
137
138
|
`tool "pwsh_run" is already registered`。
|
|
138
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
|
+
|
|
139
169
|
## 沙箱
|
|
140
170
|
|
|
141
171
|
常驻进程在**启动时**整体包进 `ctx.sandbox.confine` 返回的受限 argv。Windows ACL 沙箱用的是
|
|
@@ -239,10 +269,15 @@ Windows 上打,那份文件到手时是 `0644` —— 探测发现它属于当
|
|
|
239
269
|
```
|
|
240
270
|
plugin/
|
|
241
271
|
index.mjs 工具注册、沙箱接线、超时
|
|
272
|
+
config.mjs 宿主配置 schema;路由开关支持热更新
|
|
273
|
+
native-pwsh-guard.mjs 可选的原生 pwsh 调用守卫
|
|
242
274
|
session.mjs 会话对象:分帧、队列、base64、启动 argv
|
|
243
275
|
session-server.ps1 PowerShell 侧:runspace、流打标、哨兵
|
|
276
|
+
client/index.js 使用宿主原生控件的双语设置页
|
|
244
277
|
test/
|
|
278
|
+
settingstest.mjs 配置持久化、冲突、即时路由与进程保留
|
|
245
279
|
support.mjs 定位 DSH 安装;Windows 路径转 /mnt 形式
|
|
280
|
+
routingtest.mjs 原生与 PTC 路由、必要例外及卸载恢复
|
|
246
281
|
protocoltest.mjs 分帧、UTF-8 预算、启动清理
|
|
247
282
|
lifecycletest.mjs 权限切换、会话池与取消
|
|
248
283
|
itest.mjs 协议与会话本体
|
|
@@ -264,6 +299,9 @@ FINDINGS.md 全部实测数据与设计理由
|
|
|
264
299
|
|
|
265
300
|
```bash
|
|
266
301
|
cd <检出目录>
|
|
302
|
+
pnpm install --ignore-scripts --config.auto-install-peers=false
|
|
303
|
+
node test/settingstest.mjs # 19:原生设置表单、持久化、路由和进程保留
|
|
304
|
+
node test/routingtest.mjs # 30:原生/PTC 路由、例外、关闭与卸载
|
|
267
305
|
node test/protocoltest.mjs # 6:帧边界、字节预算、启动清理
|
|
268
306
|
node test/lifecycletest.mjs # 16:权限切换、池、并发、取消、卸载
|
|
269
307
|
PWSH_BIN='pwsh' node test/itest.mjs # 30:协议与会话本体
|
|
@@ -276,9 +314,10 @@ node test/denialtest.mjs # 11:沙箱拒绝提示(不起
|
|
|
276
314
|
node test/metatest.mjs # 23:插件页元数据(不起子进程,哪儿都能跑)
|
|
277
315
|
```
|
|
278
316
|
|
|
279
|
-
|
|
317
|
+
十二份脚本合计 **212 项**,都不需要 DSH 在跑 —— 只要 node 24、PowerShell 7,以及一个能借
|
|
280
318
|
`node_modules` 的 DSH 安装。需要 DSH 运行时的测试会自动找(`DSH_MODULES`、`DSH_MODULES_ASAR`,
|
|
281
|
-
|
|
319
|
+
或从工作目录逐级向上),找不到时打印 `SKIP` 提示。设置集成测试缺少宿主模块时会非零退出;
|
|
320
|
+
发版验收必须跑完全部测试,不能有跳过。
|
|
282
321
|
|
|
283
322
|
### 发版
|
|
284
323
|
|
package/client/index.js
ADDED
|
@@ -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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-pwsh-plugin",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.19",
|
|
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",
|
|
@@ -24,10 +24,12 @@
|
|
|
24
24
|
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
25
25
|
"./locale/*.json": "./locale/*.json",
|
|
26
26
|
"./icon": "./icon.svg",
|
|
27
|
-
"./package.json": "./package.json"
|
|
27
|
+
"./package.json": "./package.json",
|
|
28
|
+
"./client": "./client/index.js"
|
|
28
29
|
},
|
|
29
30
|
"files": [
|
|
30
31
|
"plugin",
|
|
32
|
+
"client",
|
|
31
33
|
"wsl",
|
|
32
34
|
"docs",
|
|
33
35
|
"locale",
|
|
@@ -42,6 +44,12 @@
|
|
|
42
44
|
"dsh": {
|
|
43
45
|
"bundle": {
|
|
44
46
|
"patch": "./cordis.patch.yml"
|
|
47
|
+
},
|
|
48
|
+
"client": {
|
|
49
|
+
"platform": "web",
|
|
50
|
+
"inject": [
|
|
51
|
+
"@deepseek-ai/dsh-client-ui-settings"
|
|
52
|
+
]
|
|
45
53
|
}
|
|
46
54
|
},
|
|
47
55
|
"engines": {
|
|
@@ -51,7 +59,17 @@
|
|
|
51
59
|
"@deepseek-ai/dsh": ">=0.2.0-rc.2 <0.3.0"
|
|
52
60
|
},
|
|
53
61
|
"scripts": {
|
|
54
|
-
"test": "node test/protocoltest.mjs && node test/lifecycletest.mjs && node test/itest.mjs && node test/plugintest.mjs && node test/denialtest.mjs && node test/metatest.mjs",
|
|
55
|
-
"test:all": "node test/protocoltest.mjs && node test/lifecycletest.mjs && 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"
|
|
62
|
+
"test": "node test/settingstest.mjs && node test/routingtest.mjs && node test/protocoltest.mjs && node test/lifecycletest.mjs && node test/itest.mjs && node test/plugintest.mjs && node test/denialtest.mjs && node test/metatest.mjs",
|
|
63
|
+
"test:all": "node test/settingstest.mjs && node test/routingtest.mjs && node test/protocoltest.mjs && node test/lifecycletest.mjs && 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"
|
|
64
|
+
},
|
|
65
|
+
"dependencies": {
|
|
66
|
+
"@deepseek-ai/schemastery": "^3.19.0"
|
|
67
|
+
},
|
|
68
|
+
"devDependencies": {
|
|
69
|
+
"clsx": "2.1.1",
|
|
70
|
+
"immer": "10.1.1",
|
|
71
|
+
"react": "18.3.1",
|
|
72
|
+
"react-dom": "18.3.1",
|
|
73
|
+
"zustand": "4.5.7"
|
|
56
74
|
}
|
|
57
75
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import Schema from '@deepseek-ai/schemastery'
|
|
2
|
+
import { DEFAULT_MAX_OUTPUT_BYTES, MAX_TIMEOUT_MS } from './session.mjs'
|
|
3
|
+
|
|
4
|
+
// 只有路由开关热更新;进程参数改变仍交给 Loader 重挂实例。
|
|
5
|
+
export const Config = Schema.object({
|
|
6
|
+
pwshPath: Schema.string().default('pwsh'),
|
|
7
|
+
timeoutMs: Schema.number().min(1).max(MAX_TIMEOUT_MS).step(1).default(120000),
|
|
8
|
+
idleMs: Schema.number().min(1).step(1).default(1800000),
|
|
9
|
+
maxSessions: Schema.number().min(1).step(1).default(4),
|
|
10
|
+
maxOutputBytes: Schema.number().min(1).max(2147483647).step(1).default(DEFAULT_MAX_OUTPUT_BYTES),
|
|
11
|
+
allowUnconfined: Schema.boolean().default(false),
|
|
12
|
+
enforcePwshRun: Schema.boolean().default(false).volatile(),
|
|
13
|
+
})
|
package/plugin/index.mjs
CHANGED
|
@@ -29,6 +29,8 @@
|
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
31
|
import { readFileSync } from 'node:fs'
|
|
32
|
+
export { Config } from './config.mjs'
|
|
33
|
+
import { installNativePwshGuard } from './native-pwsh-guard.mjs'
|
|
32
34
|
import { DEFAULT_MAX_OUTPUT_BYTES, MAX_TIMEOUT_MS, bootArgv, startSession, validateTimeout, abortable } from './session.mjs'
|
|
33
35
|
|
|
34
36
|
/** 加载器诊断里显示的插件名。 */
|
|
@@ -145,6 +147,12 @@ export function apply(ctx, config) {
|
|
|
145
147
|
return sandboxPolicy.resolve(session === undefined ? {} : { session })
|
|
146
148
|
}
|
|
147
149
|
|
|
150
|
+
installNativePwshGuard(ctx, config?.enforcePwshRun, resolvePolicy)
|
|
151
|
+
// 页面复用 settings 服务的持久化与 revision 校验,不创建额外的配置文件。
|
|
152
|
+
ctx.inject?.(['settings'], (child) => {
|
|
153
|
+
child.effect(() => child.settings.configure({ auto: false }, ctx.fiber), 'pwsh-run.settings-page')
|
|
154
|
+
})
|
|
155
|
+
|
|
148
156
|
// 槽位在第一次 await 前登记;同 ID 的创建、边界切换与执行共用队列。
|
|
149
157
|
const sessions = new Map()
|
|
150
158
|
let stopped = false
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const MODE_RANK = new Map([
|
|
2
|
+
['read-only', 0], ['workspace-write', 1], ['danger-full-access', 2],
|
|
3
|
+
])
|
|
4
|
+
|
|
5
|
+
const DENIAL = 'Ordinary native pwsh calls are disabled by config.enforcePwshRun. '
|
|
6
|
+
+ 'Use pwsh_run instead: pass the PowerShell command as script, and keep workdir and timeoutMs when supplied. '
|
|
7
|
+
+ 'The native command was not executed.'
|
|
8
|
+
|
|
9
|
+
/** 守卫在宿主执行前策略之后检查;不用可被后续监听器改回 allow 的瀑布钩子。 */
|
|
10
|
+
export function installNativePwshGuard(ctx, enabled, resolvePolicy) {
|
|
11
|
+
const live = enabled !== null && typeof enabled === 'object' && typeof enabled.get === 'function'
|
|
12
|
+
const readEnabled = () => {
|
|
13
|
+
const value = live ? enabled.get() : enabled
|
|
14
|
+
if (value !== undefined && typeof value !== 'boolean') {
|
|
15
|
+
throw new Error('pwsh_run enforcePwshRun must be a boolean')
|
|
16
|
+
}
|
|
17
|
+
return value === true
|
|
18
|
+
}
|
|
19
|
+
const initial = readEnabled()
|
|
20
|
+
if (!live && !initial) return
|
|
21
|
+
if (typeof ctx.tools.guard !== 'function') {
|
|
22
|
+
throw new Error('pwsh_run enforcePwshRun requires a DSH host with tools.guard(); '
|
|
23
|
+
+ 'upgrade DSH to use this routing configuration. No routing guard was installed.')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
ctx.effect(() => ctx.tools.guard((exec) => {
|
|
27
|
+
// Loader 原地更新 volatile 引用;读取它不会销毁正在使用的常驻进程。
|
|
28
|
+
if (exec.name !== 'pwsh' || !readEnabled()) return undefined
|
|
29
|
+
// 后台 job 的句柄与逐次审批由原生工具持有,常驻会话不能代接。
|
|
30
|
+
const args = exec.arguments
|
|
31
|
+
if (args?.run_in_background === true) return undefined
|
|
32
|
+
const mode = resolvePolicy(exec.agent)?.mode
|
|
33
|
+
if (mode === 'read-only') return undefined
|
|
34
|
+
const requestedRank = MODE_RANK.get(args?.sandbox_permissions)
|
|
35
|
+
const currentRank = MODE_RANK.get(mode)
|
|
36
|
+
if (requestedRank !== undefined && currentRank !== undefined && requestedRank > currentRank
|
|
37
|
+
&& typeof args?.justification === 'string' && args.justification.trim().length > 0) {
|
|
38
|
+
// 只放行真实的提权请求;是否批准仍由原生工具的审批机制决定。
|
|
39
|
+
return undefined
|
|
40
|
+
}
|
|
41
|
+
return DENIAL
|
|
42
|
+
}), 'pwsh-run.native-guard')
|
|
43
|
+
}
|