dsh-jev-guard 0.5.3 → 0.5.4
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 +14 -0
- package/CHANGELOG.zh-CN.md +14 -0
- package/DEPLOY.md +1 -1
- package/DEPLOY.zh-CN.md +1 -1
- package/README.md +7 -1
- package/README.zh-CN.md +7 -1
- package/bin/guard.mjs +6 -1
- package/docs/MEASUREMENTS.md +5 -1
- package/docs/MEASUREMENTS.zh-CN.md +5 -1
- package/package.json +1 -1
- package/tools/check-doc-pairs.mjs +1 -0
- package/tools/smoke-dsh-adapter.mjs +14 -3
- package/tools/smoke-dsh-pipeline.mjs +25 -4
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,20 @@
|
|
|
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.4] — 2026-09-30
|
|
9
|
+
|
|
10
|
+
**On Windows a live judgment — one whose verdict was completely correct — ended the process with a crash exit code.** Exiting now lets the event loop drain instead, and two smoke scripts had their Windows behaviour corrected. Nothing on the judgment path changed: `lib/` and `adapters/` are identical to 0.5.3.
|
|
11
|
+
|
|
12
|
+
**What happened.** Running against a real session on the Windows desktop build of DSH 0.2.0-rc.2: whenever a command reached the **online semantic judgment**, `node bin/guard.mjs judge …` printed the right verdict (`block 0.91 jev`) and then, on the way out, hit libuv's `Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), file src\win\async.c, line 76`. The process aborted with `0xC0000409`, so the exit code went from the real one (`3` for a block) to `-1073740791`, plus a libuv assertion on stderr that means nothing to the user. The cause is not in the judgment: at that instant the process still holds undici's keep-alive sockets (measured — `process._getActiveHandles()` returns `["Socket","Socket"]` alongside one pending `FSReqCallback`), and forcing an exit while they are closing trips that Windows libuv assertion. **The verdict was always right; the exit path was wrong** — which is exactly why this is nearly invisible if you only read stdout.
|
|
13
|
+
|
|
14
|
+
**What changed:**
|
|
15
|
+
|
|
16
|
+
1. **`bin/guard.mjs` and the two smoke scripts set `process.exitCode` instead of calling `process.exit()`.** The loop drains and the process leaves with the verdict's real code, no assertion. Measured: `guard judge` returns to exit code 3 on a `block`, and letting the loop drain costs about a second.
|
|
17
|
+
2. **`JEV_GUARD_ROOT` in `tools/smoke-dsh-pipeline.mjs` now takes a plain filesystem path.** The value was interpolated straight into `import()`, and the ESM loader parses a specifier as a URL first: Windows' `T:\…` came out as protocol `t:` and failed with `ERR_UNSUPPORTED_ESM_URL_SCHEME`. It is converted to a `file://` URL now, so `/mnt/t/…` and `T:\…` both work as given, and a value that already is a URL is left alone.
|
|
18
|
+
3. **The "no key" section of `tools/smoke-dsh-adapter.mjs` now really runs with no key.** The adapter resolves credentials in the order `ctx.credentials` → `apiKeyEnv` (default `TYPESAFE_API_KEY`) → `apiKeyFile`, and those cases only mocked the first layer — so a real key in the environment made the premise false. With one present, three notice assertions failed falsely (measured: "no key → injected a notice" reported `["user-1"]`, i.e. nothing was injected at all). The environment variable is cleared for those cases and restored before the online ones.
|
|
19
|
+
|
|
20
|
+
**Acceptance** (Windows, Node 24.12.0, on the DSH 0.2.0-rc.2 desktop host): `node bin/guard.mjs selftest` 12/12; the seven offline suites 20 / 30 / 34 / 109 / 54 / 48 / 17 assertions, all passing (`selftest-entry` and `selftest-reason` branch on platform and are one and two cases shorter on Windows than on Linux); the DSH adapter smoke test **23/23 without a key and 25/25 with one**, where the with-key run used to be "3 failures plus an abort"; the real tool-pipeline integration test 6/6 offline and 7/7 with `TYPESAFE_API_KEY`, exit code 0. `guard judge` on a live `block` verdict measured exit code 3. Each fix was confirmed in both directions: with `process.exit()` restored, the CLI and the with-key adapter smoke ended at `-1073740791` every time (the pipeline script reproduced twice in a row); a bare `JEV_GUARD_ROOT` raised `ERR_UNSUPPORTED_ESM_URL_SCHEME` until the conversion was added; and with the environment variable left in place the notice section failed its three assertions. Measured on Windows; the change itself is platform-neutral (setting `exitCode` rather than forcing an exit) and Linux is covered by the CI workflow, which runs both.
|
|
21
|
+
|
|
8
22
|
## [0.5.3] — 2026-09-23
|
|
9
23
|
|
|
10
24
|
**A `403` no longer means "your key is bad": an edge block is its own class now, and it does not degrade.** This is a behaviour change — one failure category was split in two, and the misreporting in `guard status --clear` was fixed. `lib/`, `bin/`, `adapters/` and the default config are otherwise unchanged from 0.5.2.
|
package/CHANGELOG.zh-CN.md
CHANGED
|
@@ -5,6 +5,20 @@
|
|
|
5
5
|
本项目遵循「按日期记录事实」的写法:每条都写清**改了什么、为什么、以及怎么验证的**。
|
|
6
6
|
完整的设计取舍见 [`docs/DECISIONS.md`](./docs/DECISIONS.md),实测数据见 [`docs/MEASUREMENTS.md`](./docs/MEASUREMENTS.md)。
|
|
7
7
|
|
|
8
|
+
## [0.5.4] — 2026-09-30
|
|
9
|
+
|
|
10
|
+
**Windows 上,一次判定结论完全正确的联网判定,会以崩溃退出码结束。** 现在退出改为让事件循环自然排空;另外修掉两个冒烟脚本在 Windows 上的实测偏差。判定路径一行未动 —— `lib/` 与 `adapters/` 相对 0.5.3 完全一致。
|
|
11
|
+
|
|
12
|
+
**出了什么事。** 在 Windows 桌面版 DSH 0.2.0-rc.2 的真实会话上跑出来:只要那条命令走到**联网语义判定**,`node bin/guard.mjs judge …` 会先打印出正确的判定(`block 0.91 jev`),然后在退出那一刻撞上 libuv 的 `Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), file src\win\async.c, line 76`,进程以 `0xC0000409` abort —— 退出码从真值(block 是 `3`)变成 `-1073740791`,stderr 上还多一行与用户无关的断言。根因不在判定里:退出这一刻进程仍持有 undici 的 keep-alive socket(实测 `process._getActiveHandles()` 拿到 `["Socket","Socket"]` 外加一个未决的 `FSReqCallback`),在它们正被关闭时强退,Windows 的 libuv 就会崩在这个断言上。**判定的结论一直是对的,错的是退出路径** —— 也正因如此,只盯着 stdout 看的话这条 bug 几乎不可见。
|
|
13
|
+
|
|
14
|
+
**改了什么:**
|
|
15
|
+
|
|
16
|
+
1. **`bin/guard.mjs` 与两个冒烟脚本改用 `process.exitCode`,不再调用 `process.exit()`。** 事件循环自然排空后退出,退出码是判定的真值,断言不再出现。实测:`guard judge` 的 block 判定退出码恢复为 3,自然排空的额外耗时在 1 秒量级。
|
|
17
|
+
2. **`tools/smoke-dsh-pipeline.mjs` 的 `JEV_GUARD_ROOT` 现在接受裸文件系统路径。** 这个值原本被直接拼进 `import()`,而 ESM 加载器先按 URL 解析说明符:Windows 的 `T:\…` 被读成协议 `t:`,报 `ERR_UNSUPPORTED_ESM_URL_SCHEME`。现在统一转成 `file://` URL,`/mnt/t/…` 与 `T:\…` 都能照原样用,而本身就是 URL 的值原样保留。
|
|
18
|
+
3. **`tools/smoke-dsh-adapter.mjs` 的「没有密钥」段落现在真的在没有密钥的条件下跑。** 适配器的凭据解析顺序是 `ctx.credentials` → `apiKeyEnv`(默认 `TYPESAFE_API_KEY`)→ `apiKeyFile`,而那段用例只 mock 掉了第一层 —— 环境变量里若有真钥匙,这个前提就不成立。带着钥匙跑时,notice 的 3 组断言会假失败(实测:「没有密钥 → 注入了一条 notice」报出 `["user-1"]`,即根本没有注入)。现在这几段用例期间环境变量被摘掉,联网用例之前再放回。
|
|
19
|
+
|
|
20
|
+
**验收**(Windows、Node 24.12.0,宿主为 DSH 0.2.0-rc.2 桌面版):`node bin/guard.mjs selftest` 12/12;七套离线自检 20 / 30 / 34 / 109 / 54 / 48 / 17 条断言全过(`selftest-entry` 与 `selftest-reason` 按平台分支,Windows 上比 Linux 少一例与两例);DSH 适配器冒烟**无密钥 23/23、带密钥 25/25** —— 修复前带密钥跑是「3 组失败 + 一次 abort」;真实工具管线集成测试离线 6/6、带 `TYPESAFE_API_KEY` 7/7,退出码 0。`guard judge` 的联网 block 判定实测退出码 3。三处修复都做了双向确认:把 `process.exit()` 放回去,CLI 与带密钥的适配器冒烟每次都停在 `-1073740791`(管线脚本连跑两次都复现);`JEV_GUARD_ROOT` 用裸路径时在加上转换之前一直报 `ERR_UNSUPPORTED_ESM_URL_SCHEME`;环境变量留着不放,notice 那段的三组断言就继续失败。本轮在 Windows 上实测;改动本身是平台中性的(设 `exitCode` 而不是强退),Linux 侧由同时跑两个平台的 CI 工作流覆盖。
|
|
21
|
+
|
|
8
22
|
## [0.5.3] — 2026-09-23
|
|
9
23
|
|
|
10
24
|
**`403` 不再等于"你的密钥坏了":边缘拦截现在是一个独立分类,而且不降级。** 这是一处行为变更 —— 一个失败分类被拆成两个,并修掉了 `guard status --clear` 的误报。除此之外 `lib/`、`bin/`、`adapters/` 与默认配置相对 0.5.2 未变。
|
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` | `node bin/guard.mjs judge 'pnpm test'` |
|
|
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** —
|
|
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` |
|
|
36
36
|
|
|
37
37
|
## 2. Install and configure
|
|
38
38
|
|
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` | `node bin/guard.mjs judge 'pnpm test'` |
|
|
34
34
|
| DSH | 能装本地插件(profile 的 `package.json` 有 `dsh.profile` / bundles) | `dsh --profile <名> --dump-config` |
|
|
35
|
-
| DSH 版本 | **0.1.6-alpha.2**
|
|
35
|
+
| DSH 版本 | **0.1.6-alpha.2**、**0.1.7-rc.2** 与 **0.2.0-rc.2** 均已完整验证;跑了哪些见 README 的「已验证的宿主版本」段。`package.json` 未声明宿主要求,所以插件市场不会因版本不同而阻拦安装 | `dsh --version` |
|
|
36
36
|
|
|
37
37
|
## 2. 安装与配置
|
|
38
38
|
|
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
[](#platform-support)
|
|
9
9
|
[](https://github.com/7starsseeker/dsh-jev-guard/tags)
|
|
10
10
|
[](https://www.npmjs.com/package/dsh-jev-guard)
|
|
11
|
+
[](https://www.npmjs.com/package/dsh-jev-guard)
|
|
11
12
|
[](https://github.com/7starsseeker/dsh-jev-guard/actions/workflows/selftest.yml)
|
|
12
13
|
[](https://github.com/7starsseeker/dsh-jev-guard/commits/main)
|
|
13
14
|
[](https://github.com/7starsseeker/dsh-jev-guard/stargazers)
|
|
@@ -69,7 +70,11 @@ Three layers, always in this order:
|
|
|
69
70
|
|
|
70
71
|
Requires **Node ≥ 20** (it uses the global `fetch`). **Zero runtime dependencies** — no `npm install` needed.
|
|
71
72
|
|
|
72
|
-
**Verified host
|
|
73
|
+
**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
|
+
|
|
75
|
+
**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.
|
|
76
|
+
|
|
77
|
+
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.
|
|
73
78
|
|
|
74
79
|
```bash
|
|
75
80
|
# 1. Put this repository somewhere permanent, e.g. T:\dsh-jev-guard (/mnt/t/dsh-jev-guard in WSL)
|
|
@@ -280,6 +285,7 @@ adapters/dsh/index.js The native DSH Cordis plugin (the only adapter)
|
|
|
280
285
|
cordis.patch.yml DSH bundle patch (mount declaration + every tunable)
|
|
281
286
|
tools/ Offline self-checks, smoke tests, verification helpers
|
|
282
287
|
docs/ Mechanics, trade-offs, measurements, acceptance checklist
|
|
288
|
+
measurements/ Raw records behind docs/MEASUREMENTS.md (not in the npm package)
|
|
283
289
|
```
|
|
284
290
|
|
|
285
291
|
## Security and privacy
|
package/README.zh-CN.md
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
[](#平台支持)
|
|
9
9
|
[](https://github.com/7starsseeker/dsh-jev-guard/tags)
|
|
10
10
|
[](https://www.npmjs.com/package/dsh-jev-guard)
|
|
11
|
+
[](https://www.npmjs.com/package/dsh-jev-guard)
|
|
11
12
|
[](https://github.com/7starsseeker/dsh-jev-guard/actions/workflows/selftest.yml)
|
|
12
13
|
[](https://github.com/7starsseeker/dsh-jev-guard/commits/main)
|
|
13
14
|
[](https://github.com/7starsseeker/dsh-jev-guard/stargazers)
|
|
@@ -69,7 +70,11 @@
|
|
|
69
70
|
|
|
70
71
|
要求 **Node ≥ 20**(用到全局 `fetch`)。**零运行时依赖**,不需要 `npm install`。
|
|
71
72
|
|
|
72
|
-
**已验证的宿主版本:DSH 0.1.6-alpha.2。**
|
|
73
|
+
**已验证的宿主版本: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
|
+
|
|
75
|
+
**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 装配路径都放行。
|
|
76
|
+
|
|
77
|
+
这些版本都**刻意不在 `package.json` 里声明为宿主要求**:插件市场会从 npm manifest 读这个字段,一旦声明就会在其他所有 DSH 版本上拦住安装与更新。未列入的版本是**没验过,而不是被禁止**;换版本后请重跑下面的自检。
|
|
73
78
|
|
|
74
79
|
```bash
|
|
75
80
|
# 1. 把本仓库放到一个固定的位置,例如 T:\dsh-jev-guard(WSL 里是 /mnt/t/dsh-jev-guard)
|
|
@@ -281,6 +286,7 @@ adapters/dsh/index.js DSH 原生 Cordis 插件(唯一的适配器)
|
|
|
281
286
|
cordis.patch.yml DSH bundle patch(装载声明 + 全部可调参数)
|
|
282
287
|
tools/ 离线自检、冒烟测试、验证辅助
|
|
283
288
|
docs/ 机制、取舍、实测、验收清单
|
|
289
|
+
measurements/ docs/MEASUREMENTS.md 背后的原始记录(不进 npm 包)
|
|
284
290
|
```
|
|
285
291
|
|
|
286
292
|
## 安全与隐私
|
package/bin/guard.mjs
CHANGED
|
@@ -637,4 +637,9 @@ const code = sub === 'selftest' ? selftest()
|
|
|
637
637
|
: sub === 'key' ? await cmdKey()
|
|
638
638
|
: sub === 'judge' ? await cmdJudge()
|
|
639
639
|
: (process.stderr.write(`${t('cli.usage')}\n`), 2)
|
|
640
|
-
process.exit(code
|
|
640
|
+
// **不要**写成 `process.exit(code)`。`judge` 走联网语义判定时,退出这一刻进程里还留着
|
|
641
|
+
// undici 的 keep-alive socket;Windows 上在这个时刻强退会撞进 libuv 的
|
|
642
|
+
// `Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), file src\win\async.c, line 76`,
|
|
643
|
+
// 进程以 0xC0000409 abort —— 判定本身完全正确,退出码却从真值变成 -1073740791,stderr 上
|
|
644
|
+
// 还多一行和用户无关的 libuv 断言(2026-09-30 实测)。设 `exitCode` 让事件循环自然排空即可。
|
|
645
|
+
process.exitCode = code ?? 0
|
package/docs/MEASUREMENTS.md
CHANGED
|
@@ -27,7 +27,7 @@ These numbers are not estimates — they were produced on **this machine** on 20
|
|
|
27
27
|
## 2. Three-arm calibration experiment (114 judgments, Chinese vs translated)
|
|
28
28
|
|
|
29
29
|
Sample: 40 cases / 114 judgments, covering ticket triage, dangerous commands, code changes, search-result labelling.
|
|
30
|
-
Reproduce:
|
|
30
|
+
Reproduce: `measurements/calibration-114/run_calibration.py` (in this repository; it reads the key from the environment) — the raw records of that run are in `measurements/calibration-114/`.
|
|
31
31
|
|
|
32
32
|
| Arm | Accuracy | noul | choice | score |
|
|
33
33
|
|---|---|---|---|---|
|
|
@@ -78,6 +78,8 @@ Reproduce: `node tools/extract-commands.mjs --stats`, then run `node tools/gate-
|
|
|
78
78
|
| p distribution | P50 = 0.01, P90 = 0.13, max = 0.82 (extremely polarised) |
|
|
79
79
|
| Hits added by filling in the script body | 18 entries (2.4%), **0 new false positives** |
|
|
80
80
|
|
|
81
|
+
Raw results: `measurements/offline-report-737.json` (threshold 0.5) and `measurements/offline-report-737-inline.json` (threshold 0.6 with the script bodies filled in — the three-way split above is that run's). The corpus itself cannot be regenerated; `measurements/README.md` says what every figure here reconciles to.
|
|
82
|
+
|
|
81
83
|
All 5 entries blocked (threshold 0.7) are real destructive events: `git reset --hard`, `git checkout --`, `rm -rf` on a real directory ×2, `cp backup→target`.
|
|
82
84
|
|
|
83
85
|
## 4. Probing the script blind spot (18 cases)
|
|
@@ -95,6 +97,8 @@ All 5 entries blocked (threshold 0.7) are real destructive events: `git reset --
|
|
|
95
97
|
|
|
96
98
|
Other single measurements: `truncate -s 0` 0.95 · `find -delete` 0.92 · inline `node -e rmSync` 0.91 · `dd of=~/data.db` 0.88 · `rsync --delete` 0.88 · `kubectl delete ns` 0.80 · `git clean -fdx` 0.65 · `git checkout .` 0.64 · `sudo rm -rf /var/lib/docker` 0.65 · `docker compose down` (no -v) 0.35 (low is correct, no volume deleted) · **`terraform apply -auto-approve` 0.48 (a known blind spot)** · `npm publish` 0.03.
|
|
97
99
|
|
|
100
|
+
Raw results: `measurements/probe-scripts.json` (all 18 cases, both arms) and `measurements/probe-scripts.md`.
|
|
101
|
+
|
|
98
102
|
## 5. Other measured constraints
|
|
99
103
|
|
|
100
104
|
| Item | Value | Impact |
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
## 2. 三臂校准实验(114 个判断,中文 vs 翻译)
|
|
28
28
|
|
|
29
29
|
样本:40 个 case / 114 个判断,覆盖工单分流、危险命令、代码改动、搜索结果打标。
|
|
30
|
-
|
|
30
|
+
复现:`measurements/calibration-114/run_calibration.py`(在本仓库内,密钥只从环境变量取)—— 那一次的原始记录在 `measurements/calibration-114/`。
|
|
31
31
|
|
|
32
32
|
| 臂 | 准确率 | noul | choice | score |
|
|
33
33
|
|---|---|---|---|---|
|
|
@@ -80,6 +80,8 @@
|
|
|
80
80
|
| p 分布 | P50 = 0.01,P90 = 0.13,max = 0.82(极度两极) |
|
|
81
81
|
| 脚本补齐命中 | 18 条(2.4%),**新增误报 0 条** |
|
|
82
82
|
|
|
83
|
+
原始结果:`measurements/offline-report-737.json`(threshold 0.5)与 `measurements/offline-report-737-inline.json`(threshold 0.6、补齐脚本正文那次;上面的三分取自后者)。语料本身无法重生成;每个数字对到什么,见 `measurements/README.md`。
|
|
84
|
+
|
|
83
85
|
被拦下的 5 条(阈值 0.7)全部是真实破坏事件:`git reset --hard`、`git checkout --`、
|
|
84
86
|
真实目录 `rm -rf`×2、`cp 备份→目标`。
|
|
85
87
|
|
|
@@ -102,6 +104,8 @@
|
|
|
102
104
|
`docker compose down`(无 -v)0.35(低分正确,没删卷)·
|
|
103
105
|
**`terraform apply -auto-approve` 0.48(已知漏网点)** · `npm publish` 0.03。
|
|
104
106
|
|
|
107
|
+
原始结果:`measurements/probe-scripts.json`(18 个用例,两臂全量)与 `measurements/probe-scripts.md`。
|
|
108
|
+
|
|
105
109
|
## 5. 其它实测约束
|
|
106
110
|
|
|
107
111
|
| 项 | 值 | 影响 |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-jev-guard",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
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",
|
|
@@ -180,6 +180,10 @@ async function main() {
|
|
|
180
180
|
const d = mockContext(undefined) // 没有凭据
|
|
181
181
|
applySmoke(d.ctx)
|
|
182
182
|
const handlerNoKey = d.handlers.get('tools/pre-execute')
|
|
183
|
+
// 从这里到「联网用例」之前,环境变量必须一直是空的。适配器的凭据解析顺序是
|
|
184
|
+
// ctx.credentials → `apiKeyEnv`(默认 `TYPESAFE_API_KEY`)→ `apiKeyFile`,而这一段测的正是
|
|
185
|
+
// **没有密钥**时的行为:环境里留着真钥匙,前提就直接不成立(2026-09-30 实测:带着
|
|
186
|
+
// `TYPESAFE_API_KEY` 跑本文件时,notice 那 3 组断言会全部假失败)。摘掉一次,联网用例前再放回。
|
|
183
187
|
process.env.TYPESAFE_API_KEY = ''
|
|
184
188
|
// 命令必须绕开预筛(/tmp 之类会被当"可重建内容"直接放行,那样根本到不了需要密钥的语义层)。
|
|
185
189
|
expect('无密钥 + 需要语义判定 → allow(fail-open)',
|
|
@@ -189,7 +193,6 @@ async function main() {
|
|
|
189
193
|
expectTrue('无密钥 → 状态是粘性的、且作用域只限本入口',
|
|
190
194
|
nkState?.sticky === true && nkState?.scope === 'dsh-adapter',
|
|
191
195
|
JSON.stringify({ sticky: nkState?.sticky, scope: nkState?.scope }))
|
|
192
|
-
if (apiKey) process.env.TYPESAFE_API_KEY = apiKey
|
|
193
196
|
await rm(degradedPath, { force: true })
|
|
194
197
|
|
|
195
198
|
// ---- 探测成功、但状态文件清不掉(只读文件系统):原因必须有人能发现(2026-09-23)----
|
|
@@ -290,7 +293,9 @@ async function main() {
|
|
|
290
293
|
JSON.stringify(withFileKey.messages?.map(m => m.id)))
|
|
291
294
|
|
|
292
295
|
// ---- 联网用例(有密钥时才跑) ----
|
|
296
|
+
// 「没有密钥」的场景到此结束,把环境变量还回去。
|
|
293
297
|
if (apiKey) {
|
|
298
|
+
process.env.TYPESAFE_API_KEY = apiKey
|
|
294
299
|
const e = mockContext(apiKey)
|
|
295
300
|
applySmoke(e.ctx)
|
|
296
301
|
const h = e.handlers.get('tools/pre-execute')
|
|
@@ -327,10 +332,16 @@ async function main() {
|
|
|
327
332
|
expectTrue('审计里记下了权限 preset(danger-full-access)', auditText.includes('"preset":"danger-full-access"'), auditText.slice(0, 200))
|
|
328
333
|
await rm(audited, { force: true })
|
|
329
334
|
|
|
330
|
-
await flush() //
|
|
335
|
+
await flush() // 等审计日志落盘,否则退出时会丢掉尾部记录
|
|
331
336
|
await rm(dir, { recursive: true, force: true }) // 整个测试的产物都在这个临时目录里
|
|
332
337
|
process.stdout.write(`\n${failures === 0 ? `全部通过(${checks} 组断言)` : `${failures} 组失败 / 共 ${checks} 组`}\n`)
|
|
333
|
-
|
|
338
|
+
|
|
339
|
+
// **不要**改成 `process.exit(code)`。联网用例跑完后进程里还留着 undici 的 keep-alive
|
|
340
|
+
// socket,Windows 上在这个时刻强退会撞进 libuv 的
|
|
341
|
+
// `Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), file src\win\async.c, line 76`,
|
|
342
|
+
// 进程以 0xC0000409 abort,退出码从 0 变成 -1073740791(2026-09-30 实测)。设 `exitCode`
|
|
343
|
+
// 让事件循环自然排空即可,实测退出码与耗时都正常。
|
|
344
|
+
process.exitCode = failures === 0 ? 0 : 1
|
|
334
345
|
}
|
|
335
346
|
|
|
336
347
|
await main()
|
|
@@ -21,6 +21,12 @@
|
|
|
21
21
|
* cp <本文件> $T/ && cd $T && JEV_GUARD_ROOT=/mnt/t/jev-guard node smoke-dsh-pipeline.mjs
|
|
22
22
|
* # 加 TYPESAFE_API_KEY 则第 4 项会真的走一次联网判定(7/7)
|
|
23
23
|
*
|
|
24
|
+
* Windows 上同一件事(2026-09-30 实测:离线 6/6、带密钥 7/7):临时目录同样要备齐 ① 和 ②
|
|
25
|
+
* (目录联接,或直接用 npm 装宿主包),然后 `JEV_GUARD_ROOT` **给裸路径就行** —— 脚本自己
|
|
26
|
+
* 转成 `file://` URL,不用手写 `file:///T:/...`:
|
|
27
|
+
*
|
|
28
|
+
* $T = <临时目录>; $env:JEV_GUARD_ROOT = "T:\dsh-jev-guard"; node smoke-dsh-pipeline.mjs
|
|
29
|
+
*
|
|
24
30
|
* 三个**别照抄**的旧配方:从检出根直接跑会以 `ERR_MODULE_NOT_FOUND` 崩在下面的 import 上;
|
|
25
31
|
* 从 `packages/core/agent-loop` 跑则第 6 项会假报 FAIL(那个 cwd 下没有 `./packages/...`);
|
|
26
32
|
* 用绝对路径跑本文件(不复制)同样在 import 那一步就崩 —— 裸说明符不认 cwd。
|
|
@@ -39,7 +45,15 @@ import { join } from 'node:path'
|
|
|
39
45
|
import { pathToFileURL } from 'node:url'
|
|
40
46
|
|
|
41
47
|
const ROOT = process.env.JEV_GUARD_ROOT ?? '/mnt/t/dsh-jev-guard'
|
|
42
|
-
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* `ROOT` 作为**模块说明符**。裸文件系统路径不能直接拼进 `import()`:ESM 加载器先按 URL
|
|
51
|
+
* 解析,Windows 的 `T:\jev-guard` 会被读成协议 `t:`,报 `ERR_UNSUPPORTED_ESM_URL_SCHEME`
|
|
52
|
+
* (2026-09-30 实测)。所以统一转成 `file://` URL —— POSIX 的 `/mnt/t/...` 与 Windows 的
|
|
53
|
+
* `T:\...` 都适用;已经是 URL 的原样保留,免得把 `file:///T:/...` 二次转义。
|
|
54
|
+
*/
|
|
55
|
+
const ROOT_URL = /^[a-z][a-z0-9+.-]*:\/\//i.test(ROOT) ? ROOT : pathToFileURL(ROOT).href
|
|
56
|
+
const mod = await import(`${ROOT_URL}/adapters/dsh/index.js`)
|
|
43
57
|
|
|
44
58
|
/** 审计日志与降级状态都落在这里 —— 跑测试不该改动本机真实的阀门状态。 */
|
|
45
59
|
const SANDBOX = await mkdtemp(join(tmpdir(), 'jev-guard-pipeline-'))
|
|
@@ -162,9 +176,9 @@ async function main() {
|
|
|
162
176
|
|
|
163
177
|
// 收尾与 smoke-dsh-adapter 同规矩:先等审计队列落盘(record() 是 fire-and-forget,
|
|
164
178
|
// 不等它 process.exit 会丢尾部记录),再把整个临时目录删掉。
|
|
165
|
-
// 审计模块从
|
|
179
|
+
// 审计模块从 ROOT_URL 动态导入 —— 本文件可能被复制到临时目录里运行,相对路径在那里是无效的。
|
|
166
180
|
try {
|
|
167
|
-
const audit = await import(`${
|
|
181
|
+
const audit = await import(`${ROOT_URL}/lib/audit.js`)
|
|
168
182
|
await audit.flush()
|
|
169
183
|
} catch {
|
|
170
184
|
// 清理失败不该改变测试结论
|
|
@@ -172,7 +186,14 @@ async function main() {
|
|
|
172
186
|
await rm(SANDBOX, { recursive: true, force: true }).catch(() => {})
|
|
173
187
|
|
|
174
188
|
process.stdout.write(`\n${failures === 0 ? `全部通过(${checks} 组断言)` : `${failures} 组失败 / 共 ${checks} 组`}\n`)
|
|
175
|
-
|
|
189
|
+
|
|
190
|
+
// **不要**改成 `process.exit(code)`。联网用例跑完后进程里还留着 undici 的 keep-alive
|
|
191
|
+
// socket(以及一个未决的 fs 请求);Windows 上在这个时刻强退会撞进 libuv 的
|
|
192
|
+
// `Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), file src\win\async.c, line 76`,
|
|
193
|
+
// 进程以 0xC0000409 abort,退出码从 0 变成 -1073740791 —— 一次"全部通过"被报成失败
|
|
194
|
+
// (2026-09-30 实测,连跑两次都复现)。设 `exitCode` 让事件循环自然排空即可:同一份用例
|
|
195
|
+
// 自然退出的实测耗时约 1.0s,退出码 0,连跑三次稳定。
|
|
196
|
+
process.exitCode = failures === 0 ? 0 : 1
|
|
176
197
|
}
|
|
177
198
|
|
|
178
199
|
await main()
|