dsh-pwsh-plugin 0.2.14 → 0.2.16
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/FINDINGS.md +56 -0
- package/README.md +36 -149
- package/README.zh-CN.md +29 -124
- package/docs/runtime-notes.md +57 -0
- package/docs/runtime-notes.zh-CN.md +45 -0
- package/docs/update-audit.md +17 -0
- package/package.json +3 -5
- package/plugin/index.mjs +3 -201
- package/plugin/auto-update.mjs +0 -210
- package/plugin/update-apply.mjs +0 -274
- package/plugin/update-check.mjs +0 -270
package/FINDINGS.md
CHANGED
|
@@ -1822,3 +1822,59 @@ No authorization header was set for the request.
|
|
|
1822
1822
|
|
|
1823
1823
|
`updatetest` 118 项、`autoupdatetest` 46 项、`wiringtest` 24 项全部改走这个假 registry。
|
|
1824
1824
|
|
|
1825
|
+
## 三十四、把更新交回 DSH(2026-10-29)
|
|
1826
|
+
|
|
1827
|
+
### 决定
|
|
1828
|
+
|
|
1829
|
+
博士原话:「我们换成了 NPM 发版,使用 DSH 自带的版本管理和更新功能,就不再需要旧的自动更新机制。」
|
|
1830
|
+
|
|
1831
|
+
于是 `plugin/update-check.mjs`、`plugin/update-apply.mjs`、`plugin/auto-update.mjs` 与对应的三份测试
|
|
1832
|
+
(`updatetest` 118 项、`autoupdatetest` 46 项、`wiringtest` 24 项)整体删除;`pwsh_run_update` 工具、
|
|
1833
|
+
`updateCheck` / `autoUpdate` / `autoUpdateDelayMs` 三个配置项、安装闸门(`activeRuns` / `installing` /
|
|
1834
|
+
`installGate` / `waitForInstall`)、输出末尾的更新提示一并去掉。`test/support.mjs` 里的假 registry
|
|
1835
|
+
(`startRegistry` / `packageDocument`)随之删除 —— 它只服务那三份测试。
|
|
1836
|
+
|
|
1837
|
+
### 宿主这条路是完整的(源码逐行读过)
|
|
1838
|
+
|
|
1839
|
+
`@deepseek-ai/dsh-plugin-manager`:
|
|
1840
|
+
|
|
1841
|
+
| 方法 | 行为 |
|
|
1842
|
+
|---|---|
|
|
1843
|
+
| `listVersions(name)` | `askRegistries(name, ["versions","time","dist-tags"])` → `newerVersions(listing, installed)`,按 semver 过滤并倒排 |
|
|
1844
|
+
| `updateBundle(name, version)` | 先 `semver.gt(version, from)` 校验,再查 `["name","version","engines","peerDependencies","deprecated","dsh"]`,判 `facts.bundle` 与兼容性,最后 `installBundle` |
|
|
1845
|
+
| `updateRefusal(bundle, name)` | 能不能更新的全部条件 |
|
|
1846
|
+
|
|
1847
|
+
`updateRefusal` 的四道门,缺一不可:
|
|
1848
|
+
|
|
1849
|
+
1. `bundle.installed`、`availability === 'profile'`、非 `official`、有 `source`;
|
|
1850
|
+
2. 没有 `readOnlyReason`(带插件管理能力的包不可被更新);
|
|
1851
|
+
3. `bundle.version` 是合法 semver;
|
|
1852
|
+
4. `parsedForRegistry(bundle.source)` 的 `kind === 'registry'`、`name === bundle.name`、且 `range` 不含 `:`。
|
|
1853
|
+
|
|
1854
|
+
→ **只有从 registry 装、且 source 是裸名才可更新。** `github:` / `file:` / git range 一律不行。我们 profile
|
|
1855
|
+
现在记的是 `"dsh-pwsh-plugin": "0.2.14"`,四道门全过。
|
|
1856
|
+
|
|
1857
|
+
客户端侧(`dsh-client-ui-plugin-manager`):`checkVersions` → `listVersions`;`runUpdate` → `updateBundle`;
|
|
1858
|
+
`pkg.updatable` 直接来自 `updateRefusal`。文案「可更新」「可更新到 {version}」「{count} 个插件可更新」
|
|
1859
|
+
「将从 {from} 更新到 {to},期间插件可能短暂停止」「更新失败,已恢复到 {version}」都是现成的。
|
|
1860
|
+
|
|
1861
|
+
### 拆掉之后仍然成立的三件事
|
|
1862
|
+
|
|
1863
|
+
- **`minimumReleaseAge` 仍然生效**,只是现在由宿主在装的时候撞上、由宿主在页面上说明。我们不再需要自己
|
|
1864
|
+
回读磁盘、自己判 `cooling-down`。
|
|
1865
|
+
- **`already-installed` 的静默循环消失了。** 那个循环原本只存在于「插件自己按 tag 反复装同一个版本」这条
|
|
1866
|
+
路径上;版本由人按、由页面驱动时,第一次就会被拒并如实显示。
|
|
1867
|
+
- **升级仍会换掉常驻会话。** 停掉插件就 dispose 所有会话,这一点与谁发起安装无关。README 的「已知边界」
|
|
1868
|
+
保留了这条,只删掉「绝不打断在跑的调用」那半句 —— 那是我们自己加的闸门,现在没有执行者了。
|
|
1869
|
+
|
|
1870
|
+
### 代价(诚实说明)
|
|
1871
|
+
|
|
1872
|
+
宿主发起更新时不会先问有没有 `pwsh_run` 调用在跑。若正好有脚本在飞,升级会把它连同会话一起掐断,表现与
|
|
1873
|
+
断电相同:输出写了一半、调用结束时拿不到状态。这条现在只能靠人看时机 —— 页面上的「期间插件可能短暂停止」
|
|
1874
|
+
是宿主能给出的全部提示。
|
|
1875
|
+
|
|
1876
|
+
### 删后的断言数
|
|
1877
|
+
|
|
1878
|
+
十七份 → 八份,305 项 → **117 项**(itest 17 / plugintest 15 / e2e 8 / sandbox 7 / bash 25 / bash-e2e 11 /
|
|
1879
|
+
denial 11 / meta 23)。
|
|
1880
|
+
|
package/README.md
CHANGED
|
@@ -68,7 +68,7 @@ bypasses the cache but neither npm nor pnpm will fetch that URL.
|
|
|
68
68
|
$dsh = '<desktop install>\resources\runtime\cli\bin\dsh.cmd'
|
|
69
69
|
$env:DSH_HOME = '<DSH_HOME>' # the data root the desktop app itself uses
|
|
70
70
|
|
|
71
|
-
# <version> is any published version; the newest one is what the
|
|
71
|
+
# <version> is any published version; the newest one is what the Plugins page offers
|
|
72
72
|
& $dsh plugin --profile desktop add 'dsh-pwsh-plugin@<version>'
|
|
73
73
|
# or, from a local checkout
|
|
74
74
|
& $dsh plugin --profile desktop add 'file:<path to checkout>'
|
|
@@ -119,8 +119,7 @@ The three files in `plugin/` (`index.mjs`, `session.mjs`, `session-server.ps1`)
|
|
|
119
119
|
|
|
120
120
|
## The tools
|
|
121
121
|
|
|
122
|
-
`pwsh_run` runs a script.
|
|
123
|
-
plugin — see [Updates](#updates).
|
|
122
|
+
`pwsh_run` runs a script. Versions are managed by DSH itself — see [Updates](#updates).
|
|
124
123
|
|
|
125
124
|
| Argument | Required | Meaning |
|
|
126
125
|
|---|---|---|
|
|
@@ -144,9 +143,6 @@ When order matters, merge them yourself: `2>&1` or `*>&1`.
|
|
|
144
143
|
| `maxSessions` | `4` | How many sessions may live at once (one per agent workspace). |
|
|
145
144
|
| `maxOutputBytes` | `8388608` | Output collection cap per call (8 MB). Truncation is reported honestly, never silently. |
|
|
146
145
|
| `allowUnconfined` | `false` | `true` = start a session even when the sandbox policy service is missing. |
|
|
147
|
-
| `updateCheck` | `true` | Look for newer published versions periodically. Checking and installing are separate: this only checks; `pwsh_run_update` and auto-update do the install. |
|
|
148
|
-
| `autoUpdate` | `true` | Install a newer release by itself, and — where the host runs HMR — take effect without a restart. |
|
|
149
|
-
| `autoUpdateDelayMs` | `90000` | Wait before the first automatic check, so start-up is not competing with pnpm. |
|
|
150
146
|
|
|
151
147
|
`allowUnconfined` is the one deliberate escape hatch. In a normal deployment `ctx.sandboxPolicy` is always
|
|
152
148
|
present; when it is missing the composition is incomplete, and the plugin **refuses to start** rather than
|
|
@@ -182,91 +178,34 @@ runspace. The tool reports that clearly and points at the built-in `pwsh` tool,
|
|
|
182
178
|
|
|
183
179
|
## Updates
|
|
184
180
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
181
|
+
This plugin carries no updater of its own. It is an ordinary registry package, so **DSH manages its
|
|
182
|
+
version**: the Plugins page lists it, shows when a newer release exists, and installs it — the same
|
|
183
|
+
`listVersions` / `updateBundle` path used for every other plugin.
|
|
188
184
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
| `pwsh_run_update` `action: "check"` | Reads the registry once and reports `state` / `running` / `newest release` / `install spec`. Installs nothing. |
|
|
193
|
-
| `pwsh_run_update` `action: "install"` | The same install, driven by hand instead of by the clock. Reports `install: postponed` if a call is running, and `install: restart-required` / `install: broken-release` when a pre-flight check says installing would achieve nothing (see below). |
|
|
194
|
-
|
|
195
|
-
The install goes through `ctx.get('pluginManager')` — the service behind the Plugins page — which runs pnpm
|
|
196
|
-
and rolls `package.json` back on failure. The plugin never edits its own files: it is a bundle dependency
|
|
197
|
-
pinned in `package.json` and `pnpm-lock.yaml`, and writing files behind the package manager's back would
|
|
198
|
-
leave the lockfile lying.
|
|
199
|
-
|
|
200
|
-
**Why it can take effect without a restart.** A plugin's `apply` and any timer it starts run inside the HMR
|
|
201
|
-
service's transaction context. Calling `installBundle` from there runs pnpm and lands the new files, then
|
|
202
|
-
fails to recompose — `HMR transactions cannot be nested` — which leaves the worst possible state: new files
|
|
203
|
-
new files on disk, old modules running. Clearing the context first (`hmr.executing.run(undefined, …)`) lets the
|
|
204
|
-
recomposition happen, and because the freshly installed files on disk no longer match what is loaded, HMR
|
|
205
|
-
evicts the old modules and imports the new ones. Measured: 137 ms from the trigger to the new code answering.
|
|
206
|
-
|
|
207
|
-
**Why it looks before it installs.** DSH's plugin manager refuses, before running pnpm, an install whose spec
|
|
208
|
-
exactly matches what the profile already records — `already-installed`. Two situations reach that, and neither
|
|
209
|
-
should be attempted at all:
|
|
210
|
-
|
|
211
|
-
- Somebody already installed this version (a terminal `dsh plugin add`, the Plugins page) and the process was
|
|
212
|
-
not restarted yet. The files are current; a restart is all that is missing.
|
|
213
|
-
- A release was tagged without bumping `version` in `package.json`. The tag changes, the declared version does
|
|
214
|
-
not, so every attempt installs the same bytes and is refused the same way — a silent loop that never ends
|
|
215
|
-
and never says anything.
|
|
216
|
-
|
|
217
|
-
So before installing, the updater compares three facts it already has: the spec the profile records, the
|
|
218
|
-
version the installed package declares, and the version the running process reports. A version already on disk
|
|
219
|
-
reports `install: restart-required`; a tag whose package still declares the old version reports
|
|
220
|
-
`install: broken-release` and says so plainly. `scripts/release-check.mjs` catches the second case before the
|
|
221
|
-
tag is pushed; this check catches it afterwards, on every machine that tries to update.
|
|
222
|
-
|
|
223
|
-
**Why it waits for an idle moment.** An install replaces the plugin, and stopping the plugin disposes every
|
|
224
|
-
persistent session it owns. Done while a script is in flight, that is a power cut: half-written output, a call
|
|
225
|
-
that ends with no exit status, session state gone. So the updater asks the plugin whether any call is running
|
|
226
|
-
(same synchronous check-and-set the tool uses, so a call cannot slip in between), and postpones the whole
|
|
227
|
-
attempt when one is. Calls that arrive mid-install wait for it instead of racing it.
|
|
228
|
-
|
|
229
|
-
On a host **without** HMR the automatic path does nothing at all. Installing there would put the new files
|
|
230
|
-
on disk while the running process keeps the old modules — an automatic update that needs a restart is not
|
|
231
|
-
an update, it is a version number that lies. (`pwsh_run_update action: "install"` still installs in that
|
|
232
|
-
situation, because a human asked and can decide when to restart; the result says `restart-required`.)
|
|
233
|
-
Turning the automatic path off (`autoUpdate: false`) restores the older behaviour everywhere: check only,
|
|
234
|
-
and put one line at the end of the next `pwsh_run` output so a human decides.
|
|
235
|
-
|
|
236
|
-
Without `pluginManager` — a profile where the `plugin-manager` service is disabled — `install` reports
|
|
237
|
-
`no-manager` and you upgrade from a terminal instead:
|
|
238
|
-
|
|
239
|
-
```powershell
|
|
240
|
-
$env:DSH_HOME = '<DSH_HOME>'
|
|
241
|
-
& '<desktop install>\resources\runtime\cli\bin\dsh.cmd' plugin --profile desktop add 'dsh-pwsh-plugin@<version>'
|
|
242
|
-
```
|
|
185
|
+
That path is available because the profile records a bare registry spec (`"dsh-pwsh-plugin": "0.2.14"`).
|
|
186
|
+
DSH refuses to update a bundle installed from anything else: a `github:` or `file:` source, a git range,
|
|
187
|
+
a package that is not a third-party dependency of this profile, or one that declares no semver version.
|
|
243
188
|
|
|
244
|
-
|
|
245
|
-
for desktop profiles; the two are different things, and only the service is used here.
|
|
189
|
+
Two consequences worth knowing:
|
|
246
190
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
191
|
+
- **`minimumReleaseAge` still applies.** pnpm's supply-chain policy defaults to 1440 minutes, so a version
|
|
192
|
+
published today is not resolved today — pressing update installs the previous release, and the page says
|
|
193
|
+
which version it actually installed. This is the policy working, not a failure.
|
|
194
|
+
- **A version is only as good as its tag.** `package.json` is the one hard-coded version in this repository.
|
|
195
|
+
Publish a tag without bumping it and every update attempt installs the same bytes; `scripts/release-check.mjs`
|
|
196
|
+
fails the release workflow before that happens.
|
|
252
197
|
|
|
253
|
-
|
|
254
|
-
`dsh plugin add` accepts are the same string.
|
|
255
|
-
|
|
256
|
-
| Environment variable | Effect |
|
|
257
|
-
|---|---|
|
|
258
|
-
| `DSH_PWSH_UPDATE_REGISTRY` | Registry base URL, default `https://registry.npmjs.org`. Point it at a mirror, or at a local server to rehearse. |
|
|
259
|
-
| `DSH_PWSH_UPDATE_DELAY_MS` | First check delay, default 5000 |
|
|
198
|
+
Updating by hand is the same operation DSH performs:
|
|
260
199
|
|
|
261
200
|
```powershell
|
|
262
|
-
|
|
201
|
+
$env:DSH_HOME = '<DSH_HOME>'
|
|
202
|
+
& '<desktop install>\resources\runtime\cli\bin\dsh.cmd' plugin --profile desktop add 'dsh-pwsh-plugin@<version>'
|
|
263
203
|
```
|
|
264
204
|
|
|
265
|
-
|
|
205
|
+
**An update replaces the persistent session.** Stopping the plugin disposes every session it owns, so after
|
|
206
|
+
an update the next call rebuilds the pwsh process: variables, `Set-Location` and imported modules held only
|
|
207
|
+
in that session are gone. Do not keep unrecoverable state in a session across an upgrade.
|
|
266
208
|
|
|
267
|
-
A failed query never throws into the tool call: it records `state: 'failed'`, logs one line through
|
|
268
|
-
`ctx.logger`, and keeps an earlier `latest` as `stale: 'available'` — one network hiccup should not make a
|
|
269
|
-
known new version vanish.
|
|
270
209
|
|
|
271
210
|
## The `llrun` shim
|
|
272
211
|
|
|
@@ -295,7 +234,6 @@ your own machine — it derives every path from its own location and re-checks t
|
|
|
295
234
|
| `could not confine the session: SetNamedSecurityInfoW failed (Win32 5)` | The workspace root is not fully controlled by you. The sandbox writes a DACL + an integrity label there, which needs ownership plus `WRITE_DAC` and `WRITE_OWNER`. Take ownership and grant yourself full control: `icacls "<root>" /grant "%USERNAME%":(OI)(CI)F`, or start the session from a directory under your user profile. |
|
|
296
235
|
| `refusing to start: read-only mode` | Expected. Use the built-in `pwsh` tool in read-only sessions. |
|
|
297
236
|
| Script cannot write where the built-in tool can | Check that `session` reached `sandboxPolicy.resolve` (see [Sandbox](#sandbox)); a missing session silently moves the boundary to the profile directory. |
|
|
298
|
-
| Update notice never appears | The source env var must be set on the **Windows** side; WSL does not forward `DSH_*`. |
|
|
299
237
|
| Output looks reordered | stdout and stderr are merged by type, not by time. Merge them yourself with `2>&1`. |
|
|
300
238
|
| `exit N` code is not reported | Runspace semantics — use `Write-Output` or read `$LASTEXITCODE`. |
|
|
301
239
|
| 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. |
|
|
@@ -303,56 +241,14 @@ your own machine — it derives every path from its own location and re-checks t
|
|
|
303
241
|
|
|
304
242
|
## Known limits
|
|
305
243
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
-
|
|
311
|
-
-
|
|
312
|
-
-
|
|
313
|
-
|
|
314
|
-
- **Child std handles are detached from the protocol.** `Console::SetIn/SetOut` only changes the .NET layer, so
|
|
315
|
-
the server also rewrites the three Win32 slots: stdin goes to `NUL`, stdout and stderr go to a
|
|
316
|
-
`FILE_FLAG_DELETE_ON_CLOSE` temp file truncated before every call. A grandchild that writes to inherited
|
|
317
|
-
stdout can no longer inject a fake protocol line (measured: one `Write-Output '<<<BYE>>>'` from a child used
|
|
318
|
-
to kill the session), and one that reads inherited stdin gets EOF instead of the caller's request.
|
|
319
|
-
The replacement handles are created **inheritable** (`SECURITY_ATTRIBUTES.bInheritHandle`): a handle made with
|
|
320
|
-
a `NULL` security descriptor only exists in this process, so a child that trusts `STARTUPINFO.hStdInput`
|
|
321
|
-
receives a bare handle *number* that means nothing over there — `GetFileType` reports `UNKNOWN`, and once in
|
|
322
|
-
a few dozen launches that number is recycled by an unrelated object and the read returns its bytes.
|
|
323
|
-
stdout deliberately does **not** go to `NUL`: that is a character device, PowerShell then believes its output
|
|
324
|
-
is not redirected and the sandbox denial changes into a useless `StandardOutputEncoding` message.
|
|
325
|
-
If the temp directory is unwritable it falls back to `NUL` and says so once, in a warning on the first call.
|
|
326
|
-
- **A session is serial**: concurrent calls queue up and run one at a time (no interleaved output, but no
|
|
327
|
-
parallel speedup either).
|
|
328
|
-
- **No WSL is required.** `pwsh_run` never touches WSL; only `wsl/custom-bash.mjs` does. Without a usable
|
|
329
|
-
`bashPath`, applying that preset still succeeds (the executable is resolved lazily) and the `bash` tool
|
|
330
|
-
fails per call with `command "<path>" is not an executable file` — a clear failure, not a silent
|
|
331
|
-
downgrade. The default is the bare name `bash`; when PATH cannot resolve it, the Windows build retries
|
|
332
|
-
`C:\Windows\System32\bash.exe` (the same WSL shim) before giving up. That fallback exists because a
|
|
333
|
-
process can inherit an environment block that predates a PATH edit and then lacks `System32`, which
|
|
334
|
-
otherwise costs you the whole tool. An explicit absolute `bashPath` is honoured alone — a typo there
|
|
335
|
-
should fail loudly rather than be silently redirected.
|
|
336
|
-
- **UAC behaves differently from this machine.** These measurements come from a box with UAC off, where the
|
|
337
|
-
process runs elevated. With UAC on, sessions run as a standard user, and the ACL sandbox needs the
|
|
338
|
-
workspace root's DACL to give you **`WRITE_DAC` and `WRITE_OWNER`** — full control satisfies both, because
|
|
339
|
-
the integrity label it writes lives in the SACL. A directory under your user profile inherits exactly
|
|
340
|
-
that; a directory created at a drive root only inherits `Modify`, and `Modify` alone fails with
|
|
341
|
-
`SetNamedSecurityInfoW failed (Win32 5)`. Ownership is not the deciding factor: `icacls <root> /grant
|
|
342
|
-
"%USERNAME%":(OI)(CI)F` fixes a root you do not own. Confinement fails closed with that message instead of
|
|
343
|
-
running unconfined.
|
|
344
|
-
- No interactive programs and no TTY.
|
|
345
|
-
- **An update replaces the session, but never cuts a call short.** Replacing a plugin stops it, and stopping
|
|
346
|
-
this plugin disposes its sessions, so the install that lands a new version leaves the persistent pwsh
|
|
347
|
-
process gone and the next call rebuilds it: anything held only in that session (variables, `Set-Location`,
|
|
348
|
-
imported modules) is lost. Measured across a v0.2.3 → v0.2.4 install: `$PID` changed from 24820 to 15632 and a
|
|
349
|
-
`$global:` variable set before the install read back empty. What the updater does **not** do is interrupt
|
|
350
|
-
work in flight: both the automatic loop and `pwsh_run_update action: "install"` ask the plugin whether a call
|
|
351
|
-
is running, and if one is, the install is postponed (the loop retries in 3 minutes, the tool reports
|
|
352
|
-
`install: postponed` and leaves the profile untouched) until the session is idle. Calls that arrive while an
|
|
353
|
-
install is in progress wait for it to finish instead of starting a session that is about to be disposed.
|
|
354
|
-
The version is worth more than the session state, but it is not "nothing happened" — do not hold
|
|
355
|
-
unrecoverable state in it across an upgrade.
|
|
244
|
+
The full list — session semantics, detached child std handles, UAC differences, and what an update does to a
|
|
245
|
+
session — lives in **[docs/runtime-notes.md](docs/runtime-notes.md)**. The short version:
|
|
246
|
+
|
|
247
|
+
- `read-only` mode is unsupported; use the built-in `pwsh` there.
|
|
248
|
+
- A timeout kills the whole session and reaps its child process tree.
|
|
249
|
+
- `exit N` codes and stdout/stderr interleaving are not observable.
|
|
250
|
+
- A session is serial, and there is no TTY.
|
|
251
|
+
- An update replaces the session; anything held only in it is lost.
|
|
356
252
|
|
|
357
253
|
## Development
|
|
358
254
|
|
|
@@ -361,9 +257,6 @@ plugin/
|
|
|
361
257
|
index.mjs tool registration, sandbox wiring, timeouts
|
|
362
258
|
session.mjs the session object: framing, queue, base64 encode, boot argv
|
|
363
259
|
session-server.ps1 the PowerShell side: runspace, stream tagging, sentinels
|
|
364
|
-
update-check.mjs registry lookup and version comparison
|
|
365
|
-
update-apply.mjs hands the pinned spec to pluginManager, never downloads
|
|
366
|
-
auto-update.mjs the automatic path: install outside the HMR transaction, back off on failure
|
|
367
260
|
test/
|
|
368
261
|
support.mjs locates a DSH install; converts Windows paths to /mnt form
|
|
369
262
|
itest.mjs protocol and session core
|
|
@@ -372,9 +265,6 @@ test/
|
|
|
372
265
|
sandboxtest.mjs real LocalSandboxProvider, boundary + fail-closed cases
|
|
373
266
|
bashtest.mjs custom-bash fence triage (mock runtime)
|
|
374
267
|
bash-e2e.mjs custom-bash end to end (real runtime + real llrun)
|
|
375
|
-
updatetest.mjs update check against a local registry, install outcomes
|
|
376
|
-
autoupdatetest.mjs the automatic loop, its back-off, its idle gate, and the HMR-context escape it needs
|
|
377
|
-
wiringtest.mjs the update notice lands at the end of tool output, and installs wait for an idle session
|
|
378
268
|
denialtest.mjs the sandbox-denial hint fires on the right messages only
|
|
379
269
|
metatest.mjs the plugin-page metadata: locale, icon, and which document the page selects
|
|
380
270
|
wsl/
|
|
@@ -382,6 +272,7 @@ wsl/
|
|
|
382
272
|
custom-bash.mjs the bash tool replacement the bundle wires in (Windows only)
|
|
383
273
|
acceptance.sh the 54-assertion matrix
|
|
384
274
|
docs/llrun.md the shim in full
|
|
275
|
+
docs/runtime-notes.md the full list of known limits, kept out of this page
|
|
385
276
|
FINDINGS.md every measurement and the reasoning behind the design
|
|
386
277
|
```
|
|
387
278
|
|
|
@@ -393,14 +284,11 @@ node test/e2e-real-subprocess.mjs # 8: real DSH subprocess
|
|
|
393
284
|
node test/sandboxtest.mjs # 7: real sandbox boundary + fail-closed
|
|
394
285
|
node test/bashtest.mjs # 25: custom-bash fence triage, bash fallback, exec-bit repair
|
|
395
286
|
node test/bash-e2e.mjs # 11: custom-bash end to end
|
|
396
|
-
node test/updatetest.mjs # 96: update check, install outcomes, pre-flight, local git only
|
|
397
|
-
node test/autoupdatetest.mjs # 38: automatic install, back-off, idle gate, pre-flight, HMR-context escape
|
|
398
|
-
node test/wiringtest.mjs # 21: update notice wiring, idle gate, pre-flight wiring
|
|
399
287
|
node test/denialtest.mjs # 11: sandbox-denial hint (no spawn, runs anywhere)
|
|
400
288
|
node test/metatest.mjs # 23: plugin-page metadata (no spawn, runs anywhere)
|
|
401
289
|
```
|
|
402
290
|
|
|
403
|
-
|
|
291
|
+
Eight scripts, **117 assertions**, none of which needs DSH to be running — just node 24, PowerShell 7 and a
|
|
404
292
|
DSH install to borrow `node_modules` from. Tests that need a DSH runtime locate one automatically
|
|
405
293
|
(`DSH_MODULES`, `DSH_MODULES_ASAR`, or by walking up from the working directory) and print a `SKIP` hint
|
|
406
294
|
instead of failing when none is found.
|
|
@@ -414,11 +302,10 @@ the runner's short-lived GitHub identity for a token scoped to this repository a
|
|
|
414
302
|
part of the trust contract (rename it and every publish fails with 403 until the npm setting is updated), and
|
|
415
303
|
provenance is mandatory there, so the tarball carries a signed statement of which commit built it.
|
|
416
304
|
|
|
417
|
-
The version in `package.json` is the one thing in this repository that is hard-coded, and
|
|
418
|
-
compares
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
why the three cannot drift apart:
|
|
305
|
+
The version in `package.json` is the one thing in this repository that is hard-coded, and it is what DSH
|
|
306
|
+
compares against the published version. Publish a tag without bumping it and every update attempt installs
|
|
307
|
+
the same bytes: the page keeps offering the "new" version, pnpm keeps landing the old one. `npm version`
|
|
308
|
+
writes the version, commits, and tags in one step, which is why the three cannot drift apart:
|
|
422
309
|
|
|
423
310
|
```powershell
|
|
424
311
|
npm version patch -m 'chore(release): %s' # bump, commit, annotated tag
|
|
@@ -435,4 +322,4 @@ with `Script not found "version"`.
|
|
|
435
322
|
|
|
436
323
|
## License
|
|
437
324
|
|
|
438
|
-
MIT — see [LICENSE](LICENSE).
|
|
325
|
+
MIT — see [LICENSE](LICENSE).
|
package/README.zh-CN.md
CHANGED
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
$dsh = '<桌面端安装目录>\resources\runtime\cli\bin\dsh.cmd'
|
|
61
61
|
$env:DSH_HOME = '<DSH_HOME>' # 桌面 App 自己用的那个数据根
|
|
62
62
|
|
|
63
|
-
# <version>
|
|
63
|
+
# <version> 是任意已发布版本;最新的那个就是插件页会给出的那个
|
|
64
64
|
& $dsh plugin --profile desktop add 'dsh-pwsh-plugin@<version>'
|
|
65
65
|
# 或者从本地检出装
|
|
66
66
|
& $dsh plugin --profile desktop add 'file:<检出目录>'
|
|
@@ -104,7 +104,7 @@ $env:DSH_HOME = '<DSH_HOME>' # 桌面 App 自己用的那个数据根
|
|
|
104
104
|
|
|
105
105
|
## 工具接口
|
|
106
106
|
|
|
107
|
-
`pwsh_run`
|
|
107
|
+
`pwsh_run` 跑脚本。版本由 DSH 自己管 —— 见[更新](#更新)。
|
|
108
108
|
|
|
109
109
|
| 参数 | 必填 | 说明 |
|
|
110
110
|
|---|---|---|
|
|
@@ -127,9 +127,6 @@ $env:DSH_HOME = '<DSH_HOME>' # 桌面 App 自己用的那个数据根
|
|
|
127
127
|
| `maxSessions` | `4` | 同时存活的会话上限(每个 agent 工作区一个)。 |
|
|
128
128
|
| `maxOutputBytes` | `8388608` | 单次调用的输出收集上限(8MB)。截断会明确报出来,不静默。 |
|
|
129
129
|
| `allowUnconfined` | `false` | `true` = 沙箱策略服务缺席时也起会话。 |
|
|
130
|
-
| `updateCheck` | `true` | 定期查有没有新发布的版本。查与装分开:这里只查,装由 `pwsh_run_update` 与自动更新做。 |
|
|
131
|
-
| `autoUpdate` | `true` | 自己装上更新,宿主有 HMR 时**不需要重启**即生效。 |
|
|
132
|
-
| `autoUpdateDelayMs` | `90000` | 自动检查前的等待,避免与启动流程抢 pnpm。 |
|
|
133
130
|
|
|
134
131
|
`allowUnconfined` 是唯一一条「明知故犯」的开关。正常部署下 `ctx.sandboxPolicy` 一定在;缺席说明组合
|
|
135
132
|
不完整,这时插件**拒绝启动**,而不是悄悄跑一个不受约束的会话。
|
|
@@ -160,81 +157,29 @@ session,**静默退回部署默认值**(mode + 配置里的 `workspaceRoot`
|
|
|
160
157
|
|
|
161
158
|
## 更新
|
|
162
159
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
都不用做。
|
|
160
|
+
本插件**没有自己的更新器**。它就是一个普通的 registry 包,所以**版本交给 DSH 管**:「插件」页面会列出它、
|
|
161
|
+
标出有没有更新的版本、并按与其它插件相同的 `listVersions` / `updateBundle` 路径装上。
|
|
166
162
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
| 自动(`autoUpdate: true`,默认) | 查 → 装 → 就地生效。装失败按指数退避重试(5 分钟起、翻倍、封顶 2 小时);装成功过的版本不再重试。**有调用在跑时绝不装** —— 3 分钟后再看。宿主**没有** HMR 时它什么都不装,见下。 |
|
|
170
|
-
| `pwsh_run_update` 的 `action: "check"` | 只读一次 registry,报出 `state` / `running` / `newest release` / `install spec`。不装任何东西。 |
|
|
171
|
-
| `pwsh_run_update` 的 `action: "install"` | 同一条安装路径,只是由人按下去而不是由时钟按下去。有调用在跑时报 `install: postponed`;预检判定「装了也没用」时报 `install: restart-required` / `install: broken-release`(见下)。 |
|
|
172
|
-
|
|
173
|
-
安装走 `ctx.get('pluginManager')` —— 也就是「插件」页面背后那个服务 —— 由它跑 pnpm,失败时回滚
|
|
174
|
-
`package.json`。本插件**从不自己改自己的文件**:它是 profile 的 bundle 依赖,版本钉在
|
|
175
|
-
`package.json` 与 `pnpm-lock.yaml` 里,绕开包管理器写文件会立刻让 lockfile 与磁盘对不上。
|
|
176
|
-
|
|
177
|
-
**为什么能不重启就生效。** 插件的 `apply` 以及它起的定时器,都跑在 HMR 服务的事务上下文里
|
|
178
|
-
(`AsyncLocalStorage`)。在那里直接调 `installBundle`,pnpm 会跑完、磁盘会换成新版,紧接着的重组合
|
|
179
|
-
却被拒掉(`HMR transactions cannot be nested`),于是停在最难看清的状态:磁盘是新的、进程里是旧的。
|
|
180
|
-
先把上下文清空(`hmr.executing.run(undefined, …)`)再调,重组合才会真的发生;而磁盘上新装的文件与已
|
|
181
|
-
加载的模块已经对不上,HMR 于是驱逐旧模块、import 新模块。实测:从触发到新代码应答 137 ms。
|
|
182
|
-
|
|
183
|
-
**为什么先看一眼再装。** DSH 的插件管理器在跑 pnpm **之前**就会拒掉一类安装:要装的规格与 profile
|
|
184
|
-
里为该包记下的规格完全一致 —— 也就是 `already-installed`。有两种情形会走到这一步,而这两种本来就不该
|
|
185
|
-
发起安装:
|
|
186
|
-
|
|
187
|
-
- 这个版本已经装好了(终端里的 `dsh plugin add`、或者「插件」页面),只是进程还没重启。文件是新的,
|
|
188
|
-
缺的只是一次重启。
|
|
189
|
-
- 发版时忘了把 `package.json` 里的 `version` 抬上去。标签变了、声明的版本没变,于是每一次尝试装的都是
|
|
190
|
-
同一份字节、被同一句话拒掉 —— 一个永不结束、也从不吭声的循环。
|
|
191
|
-
|
|
192
|
-
所以安装之前,更新器先比对手上已有的三个事实:profile 记下的规格、已装包自己声明的版本、运行中的进程
|
|
193
|
-
报出的版本。磁盘上已经是新版的报 `install: restart-required`;标签存在但包内版本没抬的报
|
|
194
|
-
`install: broken-release` 并直说原因。`scripts/release-check.mjs` 在推标签**之前**拦第二种,运行时这道
|
|
195
|
-
预检则在**之后**、在每一台试图升级的机器上再拦一次。
|
|
196
|
-
|
|
197
|
-
**为什么非要等安静。** 安装会替换插件,而停掉本插件就要 dispose 掉它所有的常驻会话。若此刻有脚本正在
|
|
198
|
-
跑,那与直接断电无异:输出写了一半、调用结束时拿不到状态、会话状态全失。所以更新器先问插件「现在有没有
|
|
199
|
-
调用在跑」(与工具本身用同一套同步的检查加置位,中途塞不进新调用),有就整轮推迟。安装进行中到达的调用
|
|
200
|
-
则等它装完,而不是跟它抢。
|
|
201
|
-
|
|
202
|
-
宿主**没有** HMR 时,自动路径什么都不做。那种环境装下去只会「磁盘是新版、进程是旧版」—— 需要重启的
|
|
203
|
-
自动更新不算更新,只是一个会说谎的版本号。(`pwsh_run_update` 的 `install` 在这种情况下照样装,因为
|
|
204
|
-
是人按的、人能决定什么时候重启;结果里会诚实地写 `restart-required`。)
|
|
205
|
-
把自动路径关掉(`autoUpdate: false`)则在任何宿主上都回到旧行为:只查不装,在下次 `pwsh_run` 输出末尾
|
|
206
|
-
挂一行,让人来定。
|
|
207
|
-
|
|
208
|
-
若 profile 关掉了 `plugin-manager` 服务,`install` 会报 `no-manager`,改用终端升级:
|
|
209
|
-
|
|
210
|
-
```powershell
|
|
211
|
-
$env:DSH_HOME = '<DSH_HOME>'
|
|
212
|
-
& '<桌面端安装目录>\resources\runtime\cli\bin\dsh.cmd' plugin --profile desktop add 'dsh-pwsh-plugin@<version>'
|
|
213
|
-
```
|
|
163
|
+
这条路成立的前提是 profile 里记的是裸 registry 规格(`"dsh-pwsh-plugin": "0.2.14"`)。换成别的来源就更新不了:
|
|
164
|
+
`github:` / `file:` / git range、不是本 profile 的第三方依赖、或者本身不声明 semver 版本,DSH 都会拒。
|
|
214
165
|
|
|
215
|
-
|
|
216
|
-
是活的 —— 两者不是一回事,这里只用服务。
|
|
166
|
+
两件值得知道的事:
|
|
217
167
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
168
|
+
- **`minimumReleaseAge` 照样生效。** pnpm 的供应链策略默认 1440 分钟,今天发布的版本今天解析不到 ——
|
|
169
|
+
点「更新」会装到上一个版本,页面上会写清实际装的是哪个。这是策略在起作用,不是出错。
|
|
170
|
+
- **版本号与标签必须对齐。** `package.json` 是本仓库唯一写死的版本;打了标签却没抬版本号,每一次更新装的
|
|
171
|
+
都是同一份字节。`scripts/release-check.mjs` 会在 workflow 里先把这种情况拦下来。
|
|
222
172
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
| 环境变量 | 作用 |
|
|
226
|
-
|---|---|
|
|
227
|
-
| `DSH_PWSH_UPDATE_REGISTRY` | registry 基址,默认 `https://registry.npmjs.org`。可指向镜像,或指向本机服务做预演 |
|
|
228
|
-
| `DSH_PWSH_UPDATE_DELAY_MS` | 首次检查延迟,默认 5000 |
|
|
173
|
+
手动更新与 DSH 做的是同一个操作:
|
|
229
174
|
|
|
230
175
|
```powershell
|
|
231
|
-
|
|
176
|
+
$env:DSH_HOME = '<DSH_HOME>'
|
|
177
|
+
& '<桌面端安装目录>\resources\runtime\cli\bin\dsh.cmd' plugin --profile desktop add 'dsh-pwsh-plugin@<version>'
|
|
232
178
|
```
|
|
233
179
|
|
|
234
|
-
|
|
180
|
+
**更新会换掉常驻会话。** 停掉插件就要 dispose 掉它所有的常驻会话,所以升级后下一次调用是重建的 pwsh 进程:
|
|
181
|
+
只存在那个会话里的变量、`Set-Location`、已导入模块都会丢。不要把它当作跨升级保存状态的地方。
|
|
235
182
|
|
|
236
|
-
查询失败不会抛进工具调用:记 `state: 'failed'`,经 `ctx.logger` 留一行,先前查到过的 `latest` 保留成
|
|
237
|
-
`stale: 'available'` —— 一次网络抖动不该让已知的新版本消失。
|
|
238
183
|
|
|
239
184
|
## `llrun` 垫片
|
|
240
185
|
|
|
@@ -261,7 +206,6 @@ Windows 上打,那份文件到手时是 `0644` —— 探测发现它属于当
|
|
|
261
206
|
| `refusing to start: read-only mode` | 预期行为。read-only 会话里用内置 `pwsh` 工具。 |
|
|
262
207
|
| `could not confine the session: SetNamedSecurityInfoW failed (Win32 5)` | 工作区根目录的 DACL 没给你 `WRITE_DAC` + `WRITE_OWNER`(沙箱要在那里写 DACL 与完整性标签)。全控制权限两样都满足,`Modify` 不够。执行 `icacls "<根目录>" /grant "%USERNAME%":(OI)(CI)F`,或从用户目录下的目录起会话。 |
|
|
263
208
|
| 内置工具能写的地方本工具写不进 | 确认 `session` 真的传到了 `sandboxPolicy.resolve`(见「沙箱」);漏传会把边界静默挪到 profile 目录。 |
|
|
264
|
-
| 永远看不到更新提示 | 源环境变量必须在 **Windows** 侧设;WSL 不透传 `DSH_*`。 |
|
|
265
209
|
| 输出顺序看着不对 | stdout 与 stderr 按类型合并,不按时间。自己用 `2>&1` 合。 |
|
|
266
210
|
| `exit N` 的退出码拿不到 | runspace 语义 —— 用 `Write-Output` 或读 `$LASTEXITCODE`。 |
|
|
267
211
|
| 调用结束后子进程还占着文件 | 会话持有 `Process` 对象。等 `idleMs`,或者用一次超时强制重建。 |
|
|
@@ -269,44 +213,14 @@ Windows 上打,那份文件到手时是 `0644` —— 探测发现它属于当
|
|
|
269
213
|
|
|
270
214
|
## 已知边界
|
|
271
215
|
|
|
272
|
-
|
|
273
|
-
-
|
|
274
|
-
|
|
275
|
-
-
|
|
276
|
-
-
|
|
277
|
-
-
|
|
278
|
-
|
|
279
|
-
-
|
|
280
|
-
stdin 接 `NUL`,stdout / stderr 接一个带 `FILE_FLAG_DELETE_ON_CLOSE` 的临时文件、每次调用前截回零。
|
|
281
|
-
于是子进程往继承来的 stdout 写东西不会再伪造出协议行(实测:子进程一句 `Write-Output '<<<BYE>>>'`
|
|
282
|
-
曾让会话当场死亡),读继承来的 stdin 也只会读到 EOF,而不是调用方的请求。
|
|
283
|
-
替上去的句柄必须是**可继承**的(`SECURITY_ATTRIBUTES.bInheritHandle`):用 `NULL` 安全描述符建的句柄
|
|
284
|
-
只存在于本进程,子进程按 `STARTUPINFO.hStdInput` 拿到的只是一个在那边没有意义的句柄**编号** ——
|
|
285
|
-
`GetFileType` 报 `UNKNOWN`,几十次里还有一次那个编号被别的对象复用,读出来是别人的字节。
|
|
286
|
-
stdout **故意不接 `NUL`**:那是字符设备,PowerShell 会认为输出没被重定向,沙箱拒绝的报错会变成一句
|
|
287
|
-
没用的 `StandardOutputEncoding` 提示。临时目录不可写时退回 `NUL`,并在第一次调用的警告里说明一次。
|
|
288
|
-
- **会话是串行的**:并发调用排成队列逐个执行(不会串输出,但也没有并行加速)。
|
|
289
|
-
- **不依赖 WSL。** `pwsh_run` 全程不碰 WSL,只有 `wsl/custom-bash.mjs` 才碰。没有可用的 `bashPath` 时,
|
|
290
|
-
该预设照样加载成功(可执行文件是惰性解析的),`bash` 工具每次调用报
|
|
291
|
-
`command "<路径>" is not an executable file` —— 清晰失败,不会静默降级。默认值是裸名 `bash`;
|
|
292
|
-
PATH 解不出来时,Windows 上会再试 `C:\Windows\System32\bash.exe`(同一个 WSL 垫片)才放弃。
|
|
293
|
-
设这道后备是因为进程可能继承到一份早于 PATH 修改的环境块,里面没有 `System32` ——
|
|
294
|
-
没有它就等于整个工具作废。显式写出的绝对 `bashPath` 只认那一个:写错了就该响亮地失败,
|
|
295
|
-
不该被悄悄换成另一份。
|
|
296
|
-
- **UAC 开启时的行为与本机不同。** 本机实测环境是 UAC 关闭、进程提权。UAC 开着时会话是标准用户,
|
|
297
|
-
此时 ACL 沙箱要求工作区根目录的 DACL 给你 **`WRITE_DAC` 与 `WRITE_OWNER`** —— 全控制两者都满足
|
|
298
|
-
(它写的完整性标签在 SACL 里)。用户目录下的目录天然继承这两样;在盘符根新建的目录只继承
|
|
299
|
-
`Modify`,单靠 `Modify` 会以 `SetNamedSecurityInfoW failed (Win32 5)` 失败。属主不是决定因素:
|
|
300
|
-
`icacls <根目录> /grant "%USERNAME%":(OI)(CI)F` 能救。届时是 fail-closed 报错,不会退化成无边界。
|
|
301
|
-
- 不支持交互式程序与 TTY。
|
|
302
|
-
- **升级会换掉会话,但绝不打断正在跑的调用。** 替换插件会先把它停掉,而停掉本插件就会销毁它的会话 ——
|
|
303
|
-
所以真正装上新版的那一次,常驻 pwsh 进程已经不在了,下次调用重新拉起。只存在于该会话里的东西(变量、
|
|
304
|
-
`Set-Location` 过的目录、import 进来的模块)随之丢失。实测 v0.2.3 → v0.2.4 的一次安装:`$PID` 由
|
|
305
|
-
24820 变成 15632,安装前设的 `$global:` 变量读回来是空的。更新器**不会**做的事是打断在跑的活:
|
|
306
|
-
自动循环与 `pwsh_run_update` 的 `install` 都会先问插件「现在有没有调用在跑」,有就整轮推迟
|
|
307
|
-
(循环 3 分钟后再看,工具报 `install: postponed` 且不动 profile),直到会话安静下来。安装进行中到达的
|
|
308
|
-
调用会先等它装完,而不是建一个马上要被销毁的会话。版本比会话状态值钱,但这并不等于「什么都没发生」
|
|
309
|
-
—— 别把不可重建的状态放在里面跨过升级。
|
|
216
|
+
完整清单(会话语义、已断开的子进程 std handle、UAC 差异、升级对会话的影响)见
|
|
217
|
+
**[docs/runtime-notes.zh-CN.md](docs/runtime-notes.zh-CN.md)**。要点:
|
|
218
|
+
|
|
219
|
+
- 不支持 `read-only`,那里用内置 `pwsh`。
|
|
220
|
+
- 超时会杀掉整个会话并回收其子进程树。
|
|
221
|
+
- `exit N` 退出码与 stdout/stderr 的逐行时序都不可观测。
|
|
222
|
+
- 会话是串行的,不提供 TTY。
|
|
223
|
+
- 升级会换掉会话,只存在于其中的东西随之丢失。
|
|
310
224
|
|
|
311
225
|
## 开发
|
|
312
226
|
|
|
@@ -315,9 +229,6 @@ plugin/
|
|
|
315
229
|
index.mjs 工具注册、沙箱接线、超时
|
|
316
230
|
session.mjs 会话对象:分帧、队列、base64、启动 argv
|
|
317
231
|
session-server.ps1 PowerShell 侧:runspace、流打标、哨兵
|
|
318
|
-
update-check.mjs registry 查询与版本比较
|
|
319
|
-
update-apply.mjs 把钉住的规格交给 pluginManager,不自己下载
|
|
320
|
-
auto-update.mjs 自动路径:在 HMR 事务之外安装,失败退避
|
|
321
232
|
test/
|
|
322
233
|
support.mjs 定位 DSH 安装;Windows 路径转 /mnt 形式
|
|
323
234
|
itest.mjs 协议与会话本体
|
|
@@ -326,9 +237,6 @@ test/
|
|
|
326
237
|
sandboxtest.mjs 真 LocalSandboxProvider,边界 + fail-closed 用例
|
|
327
238
|
bashtest.mjs custom-bash 围栏分诊(mock runtime)
|
|
328
239
|
bash-e2e.mjs custom-bash 端到端(真 runtime + 真 llrun)
|
|
329
|
-
updatetest.mjs 更新检查与安装结果(本地 git 仓库)
|
|
330
|
-
autoupdatetest.mjs 自动更新循环、退避、空闲闸门,以及它依赖的 HMR 上下文逃逸
|
|
331
|
-
wiringtest.mjs 更新提示真的接到工具输出末尾、安装要等会话空闲
|
|
332
240
|
denialtest.mjs 沙箱拒绝提示只在该触发的报错上触发
|
|
333
241
|
metatest.mjs 插件页元数据:locale、icon,以及页面到底读哪份文档
|
|
334
242
|
wsl/
|
|
@@ -336,6 +244,7 @@ wsl/
|
|
|
336
244
|
custom-bash.mjs bundle 接进组合的 bash 工具替代实现(仅 Windows)
|
|
337
245
|
acceptance.sh 54 项验收矩阵
|
|
338
246
|
docs/llrun.md 垫片的完整说明
|
|
247
|
+
docs/runtime-notes.zh-CN.md 完整已知边界清单,从这一页挪出去
|
|
339
248
|
FINDINGS.md 全部实测数据与设计理由
|
|
340
249
|
```
|
|
341
250
|
|
|
@@ -347,14 +256,11 @@ node test/e2e-real-subprocess.mjs # 8:真 DSH subprocess
|
|
|
347
256
|
node test/sandboxtest.mjs # 7:真沙箱边界 + fail-closed
|
|
348
257
|
node test/bashtest.mjs # 25:custom-bash 围栏分诊、bash 后备、执行位自修复
|
|
349
258
|
node test/bash-e2e.mjs # 11:custom-bash 端到端
|
|
350
|
-
node test/updatetest.mjs # 96:更新检查、安装结果、安装前预检(只碰本地 git)
|
|
351
|
-
node test/autoupdatetest.mjs # 38:自动安装、退避、空闲闸门、安装前预检、HMR 上下文逃逸
|
|
352
|
-
node test/wiringtest.mjs # 21:更新提示接线、空闲闸门、安装前预检的接线
|
|
353
259
|
node test/denialtest.mjs # 11:沙箱拒绝提示(不起子进程,哪儿都能跑)
|
|
354
260
|
node test/metatest.mjs # 23:插件页元数据(不起子进程,哪儿都能跑)
|
|
355
261
|
```
|
|
356
262
|
|
|
357
|
-
|
|
263
|
+
八份脚本合计 **117 项**,都不需要 DSH 在跑 —— 只要 node 24、PowerShell 7,以及一个能借
|
|
358
264
|
`node_modules` 的 DSH 安装。需要 DSH 运行时的测试会自动找(`DSH_MODULES`、`DSH_MODULES_ASAR`,
|
|
359
265
|
或从工作目录逐级向上),找不到时打一行 `SKIP` 提示而不是失败。
|
|
360
266
|
|
|
@@ -366,9 +272,8 @@ runner 用自己那份短命的 GitHub 身份换一个只对「这个仓库 +
|
|
|
366
272
|
`NPM_TOKEN`,也不该加。两个连带后果值得记住:workflow 的文件名是信任契约的一部分(改名后每次发布都会
|
|
367
273
|
403,直到在 npm 上改设置);provenance 在那里是强制的,所以 tarball 会带上「由哪个提交构建」的签名声明。
|
|
368
274
|
|
|
369
|
-
`package.json` 里的 version
|
|
370
|
-
|
|
371
|
-
退避一路爬到上限。
|
|
275
|
+
`package.json` 里的 version 是本仓库唯一写死的东西,DSH 拿它跟 registry 上的已发布版本比。
|
|
276
|
+
不升版本就发版,每次更新装的都是同一份字节:页面一直提示有新版,pnpm 一直装回旧的。
|
|
372
277
|
`npm version` 一次完成「改版本 + 提交 + 打 annotated tag」,所以这三者不会漂开:
|
|
373
278
|
|
|
374
279
|
```powershell
|
|
@@ -385,4 +290,4 @@ npm 要用 Windows 侧那份。WSL 里 `/usr/local/bin/npm` 指向 bun 的软链
|
|
|
385
290
|
|
|
386
291
|
## 许可
|
|
387
292
|
|
|
388
|
-
MIT —— 见 [LICENSE](LICENSE)。
|
|
293
|
+
MIT —— 见 [LICENSE](LICENSE)。
|