dsh-rule-engine 0.5.7 → 0.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # dsh-rule-engine
2
2
 
3
3
  ![npm](https://img.shields.io/npm/v/dsh-rule-engine)
4
- ![version](https://img.shields.io/badge/version-0.5.7-blue)
4
+ ![version](https://img.shields.io/badge/version-0.5.9-blue)
5
5
 
6
6
  DSH 规则执行引擎 v3 的插件实现。它把 `~/.dsh/AGENTS.md` 当作唯一真相源,自动解析规则四要素与执行等级,再通过「工具守卫 + 文本检测 + 时序检查 + 审计台账」执行用户规则,而不是内置一套与用户无关的安全清单。
7
7
 
@@ -42,10 +42,31 @@ DSH 规则执行引擎 v3 的插件实现。它把 `~/.dsh/AGENTS.md` 当作唯
42
42
  ## 质量与验证(2026-08-26,对齐官方 docs/testing.zh.md)
43
43
 
44
44
  - `npm run test`(全量单测;`test/run-all.mjs` 统一入口,注意 ESM 缓存顺序约定);
45
- - `node scripts/verify-all.mjs` —— 交付前四层体检:语法(lib 全文件 `node --check`)→ 单元(run-all)→ **组合冒烟**(`test/loader-smoke.e2e.mjs`:真实引擎代码 + 真实审计文件,仅 mock LLM 边界,断言**外部世界**——审计文件里真的出现 judge-false/judge-pass 记录,而非自我报告)→ **真实判例**(近 24h 台账 judge-pass/false 记录数,0 条 = WARN 提示需实弹);
45
+ - `node scripts/verify-all.mjs` —— 交付前**五层**体检:语法(lib 全文件 `node --check`)→ 单元(run-all)→ **组合冒烟**(`test/loader-smoke.e2e.mjs`:真实引擎代码 + 真实审计文件,仅 mock LLM 边界,断言**外部世界**——审计文件里真的出现 judge-false/judge-pass 记录,而非自我报告)→ **工具箱覆盖**(`scripts/check-tool-coverage.mjs`:官方 tool-catalog 全集 vs 分类表,出现 unknown 即红)→ **真实判例**(近 24h 台账 judge-pass/false 记录数,0 条 = WARN 提示需实弹);
46
46
  - `node scripts/health-audit.mjs` —— 找茬清单:近 24h 失败/降级类统计(intent-llm 失败、judge-unavailable、verify-gap、inject-skip…)+ 关键导出接线交叉(疑似未接线 = 告警)——"失败可见化",不再有静默躺 20 小时的降级;
47
47
  - 执行协议(本仓库自身交付纪律):方案冻结单(范围/影响面/测试计划/失败预测)→ todo 化 → 小步闭环(每改动立即 `node --check`)→ 对账交付(计划×实际逐项 ✅/❌/跳过原因)。
48
48
 
49
+ ## 0.5.9(2026-08-27 追加)
50
+
51
+ > 修复方向(用户"整体审查"要求,依据手册知识条目 K-01~K-06):静态工具清单漏分类官方工具
52
+ > (`run_code` 事故——code 模式会话一切被拦)→ 单一真源 + 前缀规则 + 覆盖门禁 + 白名单可视化。
53
+
54
+ - **工具分类单真源**:规则 24④ 守卫与 unknown 处置统一读 `lib/core/tool-catalog.js` 唯一分类表(废弃双表——run_code 正是双表都漏的受害者);
55
+ - **官方工具全集补全**:59 个官方 tool-catalog 工具名全覆盖(`cordis_*`/`terminal_*`/`session_*`/goal/jobs/子代理/团队/plan-mode/官方保留传输 `run_code` 等)——Code Mode 会话(模型只能直呼 `run_code`)不再被"未知工具"拦死;
56
+ - **前缀规则**:`mcp__`(变更类走授权,照 Claude Code `mcp__*` 范式)、`esr_`/`dev_` 等生态命名空间自动归类——**将来新增插件工具遵循惯例即被覆盖**(不再依赖静态枚举);
57
+ - **unknown 首调处置 `unknownPolicy`**:默认 `deny`(保守:`ask` 弹窗模式需真实场景实弹验证后才可作默认;配置 `"ask"` 可切换官方弹窗——approval 弹窗 allowed-once,无审批通道自动拒绝,官方 fail-closed 语义);
58
+ - **白名单 v2 带元数据**:`~/.dsh/rule-engine-tools.json` 升级为 `[{name,time,session}]`(**谁、何时、在哪个会话被放行**——可视化基础);旧 `["name"]` 格式兼容加载;历史条目显示"时间/来源未知";
59
+ - **`/guard tools` 命令**:查看白名单(永久 + 本会话新增,含时间/来源会话)+ `/guard tools revoke <工具名>` 撤销(持久化+会话集同步);
60
+ - **工具箱覆盖门禁**:`scripts/check-tool-coverage.mjs`(官方 tool-catalog vs 分类表,任一 unknown 即红——`run_code` 事故同类的机器防线)已挂入 `verify-all` 第五层;
61
+ - **测试**:全量单测(新增 whitelist 格式 5 用例)+ loader-smoke 新增 E 场景(白名单 v2 真实落盘断言:对象数组/时间/来源会话/审计留痕)+ verify-all **五层**全绿。
62
+
63
+ ## 0.5.8(2026-08-26 追加)
64
+
65
+ - **白名单持久化**:"允许使用 X"(未归类工具批准)落盘 `~/.dsh/rule-engine-tools.json`——此前为内存态,热重载/重启即清(用户需反复重发,实测 esr_task/esr_close 各被清一次);
66
+ - **只读命令词表补全**:`Select-Object / Out-String / Format-Table / Measure-Object / Sort-Object / Where-Object / Group-Object / Out-Null / ConvertTo-Json` 等常见只读 cmdlet 纳入只读判定(此前 `Get-Item | Select-Object` 被误判为变更拦截)——含"管道内含写操作仍判变更"的安全回归;
67
+ - **`npm run verify`** 注册(= `scripts/verify-all.mjs` 四层体检别名);
68
+ - 工作区内低风险变更豁免(12A 正文对齐:只读诊断脚本不再被拦——0.5.7 的延续项)。
69
+
49
70
  ## 任务契约与反过度工程(可选)
50
71
 
51
72
  - 默认**关闭**;可在规则引擎设置页开启「任务边界与反过度工程」总开关。
@@ -70,6 +91,8 @@ DSH 规则执行引擎 v3 的插件实现。它把 `~/.dsh/AGENTS.md` 当作唯
70
91
  | `/guard budget ...` | 设置预算(agents=N files=... deps=allow hash=allow) |
71
92
  | `/guard contract` | 查看当前任务契约 |
72
93
  | `/guard label <id> <label>` | 给审计记录打标(correct/incorrect/inconclusive) |
94
+ | `/guard tools` | 查看工具放行白名单(永久+本会话,含时间/来源会话) |
95
+ | `/guard tools revoke <名>` | 撤销白名单条目(持久化+会话集同步移除) |
73
96
 
74
97
  ## 装配方式
75
98
 
@@ -21,6 +21,7 @@ import {
21
21
  isSensitiveToolCall,
22
22
  isVariablePath,
23
23
  isVerificationCommand,
24
+ isLowRiskWorkspaceNew,
24
25
  pathTarget
25
26
  } from "./patterns.js";
26
27
  import { authMatches, describeAuth, describeOp, describeScopes, findMatchingAuth, operationOf, askQuestionText, inferPathPrefixFromText, inferTypeFromText, scopesFromIntents } from "./authorization.js";
@@ -64,16 +65,9 @@ function makeHit(cfg, reason) {
64
65
  };
65
66
  }
66
67
 
67
- export const COVERED_MUTATION_TOOLS = new Set([
68
- "edit", "write", "str_replace_editor", "pwsh", "bash",
69
- "dev_stage_add", "dev_stage_call", "dev_stage_promote", "dev_stage_demote"
70
- ]);
71
- export const SAFE_UNCOVERED_TOOLS = new Set([
72
- "ask_user_question", "todo_write", "subagent", "workflow", "visualize", "skill",
73
- "read", "grep", "glob", "read_image", "job_list", "job_output", "list_agents",
74
- "get_goal", "dev_plugin_status", "dev_reload_package", "dev_injected_list",
75
- "dev_stage_list", "dev_router_status", "dev_self_test"
76
- ]);
68
+ // 0.5.9(单真源):COVERED_MUTATION_TOOLS / SAFE_UNCOVERED_TOOLS 独立覆盖表已废弃——
69
+ // 工具分类以 lib/core/tool-catalog.js 的唯一分类表为准(24④/22/unknown 全部读它),
70
+ // 双表漂移即 run_code 事故根因(K-01/K-02/K-06 依据)。
77
71
 
78
72
  function looksLikeFileMutation(name, args) {
79
73
  const a = args || {};
@@ -327,9 +321,8 @@ function matchRule(cfg, ctx) {
327
321
  //("你的执行方案难道没问题吗?"虽含"执行"仍是疑问句——同形词不构成指令,不豁免)
328
322
  if (cfg.handler === "rule22-7-direct") {
329
323
  // 机制 B(2026-08-24):工具分类制(纯函数,覆盖现有与未来插件注册的全部工具)——
330
- // control(SAFE 无条件放行)/ analysis(只读分析放行)/ artifact(产物放行+审计)/
331
- // unknown(物理不拦,由 pre-execute ask 层首调询问,防新插件绕过);mutating 维持原有严格逻辑。
332
- if (SAFE_UNCOVERED_TOOLS.has(name)) return null; // 授权/委托/技能/查询展示类 → 无条件放行
324
+ // analysis(只读分析放行)/ artifact(产物放行+审计)/ unknown(物理不拦,由 pre-execute
325
+ // ask/deny 层首调处置,防新插件绕过);mutating 维持原有严格逻辑。0.5.9 单真源:唯一分类表。
333
326
  const cls = toolClass(name, args);
334
327
  if (cls === "analysis") return null;
335
328
  if (cls === "artifact") {
@@ -338,6 +331,12 @@ function matchRule(cfg, ctx) {
338
331
  }
339
332
  if (cls === "unknown") return null; // 物理不拦;pre-execute ask 层首调询问
340
333
  if (denyMutation) {
334
+ // v0.5.7 后续(用户拍板):工作区内低风险变更豁免——12A 正文豁免的机器实现
335
+ //(只读诊断脚本被拦的最终修复:此前 --profile 模板/多根/验证伴生已修,本项补齐)
336
+ if (isLowRiskWorkspaceNew(name, args)) {
337
+ audit?.({ kind: "allow", rule: cfg.ruleId, name: "低风险新建豁免", tool: name, reason: `工作区内低风险变更(12A 正文豁免):${describeOp(operationOf(name, args))}`, session: sessionIdOf(exec) });
338
+ return null;
339
+ }
341
340
  // LLM 意图兜底(方案 A,2026-08-24):词表判拦 + LLM 高/低置信判 execute → 放行
342
341
  //(非对称:LLM 只解救不收紧;审计在预取时已记 intent-llm)
343
342
  const verdict = verdictForDeny(session.turn, state.llmIntentCfg || {});
@@ -352,7 +351,7 @@ function matchRule(cfg, ctx) {
352
351
  // 无用户消息/状态信号/ask 答复后回合仍不做规则 22 判定(由 12A/13A 把关)。
353
352
  if (!turnIntents || !turnIntents.hasExecute) return null;
354
353
  if (isReadOnlyTool(name, args)) return null;
355
- if (!COVERED_MUTATION_TOOLS.has(name) && !looksLikeFileMutation(name, args)) return null;
354
+ if (cls === "mutating" || looksLikeFileMutation(name, args)) {} else return null;
356
355
  const op = operationOf(name, args);
357
356
  // v0.5.7 P0-1(用户拍板 2026-08-26):验证类命令伴生放行——本回合已有 write/any/command
358
357
  // 授权(用户已批"修改/执行")时,运行测试/冷加载/审计/语法检查属于该变更的验证闭环,
@@ -550,10 +549,11 @@ function matchRule(cfg, ctx) {
550
549
  return makeHit(cfg, `【硬拦截】${p} 的 dsh.profile.bundles 包含非 bundle/无法确认类型:${badBundles.join(";")}(规则 24)`);
551
550
  }
552
551
  // 规则 24④:所有能产生文件写入/删除/移动效果的工具都必须纳入统一守卫——
553
- // 未覆盖的变更类工具运行时直接拒绝(防"改个新工具就绕过守卫")
552
+ // 0.5.9 单真源:已分类(tool-catalog 唯一表)= 已纳入守卫,放行;未分类且疑似变更 = 运行时拒绝
553
+ // (防"改个新工具就绕过守卫";与工具覆盖门禁 K-01/K-06 互补)
554
554
  if (isReadOnlyTool(name, args)) return null;
555
- if (COVERED_MUTATION_TOOLS.has(name)) return null;
556
- if (SAFE_UNCOVERED_TOOLS.has(name)) return null;
555
+ const cls24 = toolClass(name, args);
556
+ if (cls24 !== "unknown") return null;
557
557
  if (looksLikeFileMutation(name, args)) {
558
558
  return makeHit(cfg, `【硬拦截】未覆盖的变更类工具 ${name},违反规则 24④:请先纳入统一守卫覆盖`);
559
559
  }
package/lib/core/paths.js CHANGED
@@ -38,3 +38,8 @@ export function baselineFilePath() {
38
38
  export function verifyFilePath() {
39
39
  return join(dshHome(), "rule-engine-verify.json");
40
40
  }
41
+
42
+ /** v0.5.7 后续(用户批准):unknown 工具白名单持久化文件("允许使用 X" → 重启不丢) */
43
+ export function toolsWhitelistFilePath() {
44
+ return join(dshHome(), "rule-engine-tools.json");
45
+ }
@@ -237,7 +237,7 @@ export function isGetDateCommand(command) {
237
237
 
238
238
  const READ_ONLY_TOOLS = new Set(["read", "grep", "glob", "read_image"]);
239
239
  const READONLY_CMD_RE =
240
- /(?:\b(?:Get-Content|Get-ChildItem|Get-Item|Get-Command|Get-Date|Select-String|Find-String|Test-Path|Get-Process|Get-Service|cat|type|dir|ls|grep|findstr|more|netstat|where|echo|Write-Output|Write-Host|Get-Location|pwd|cwd|Get-Help|help)\b)|(?:\bgit\s+(?:-[^\s]+\s+[^\s]+\s+|--[^\s]+\s+)*\b(?:status|log|diff|show|branch|tag|remote|rev-parse|diff-tree|ls-files)(?:\s|$))|(?:\bdsh\s+(?:--version|--help|--dump-config|plugin\s+(?:list|show|status))(?:\s|$))|(?:\bdsh\s+--profile\s+[^\s]+\s+(?:--dump-config|plugin\s+(?:list|show|status))(?:\s|$))|(?:\bnode\s+(?:--check|--test|--version)(?:\s|$))|(?:\bnpm\s+(?:test|run\s+test)(?:\s|$))|(?:\bpnpm\s+(?:test|run\s+test)(?:\s|$))|(?:\bgh\s+(?:auth\s+status|repo\s+view)(?:\s|$))|(?:\bnpm\s+(?:ls|view)(?:\s|$))/i;
240
+ /(?:\b(?:Get-Content|Get-ChildItem|Get-Item|Get-Command|Get-Date|Select-String|Find-String|Test-Path|Get-Process|Get-Service|cat|type|dir|ls|grep|findstr|more|netstat|where|echo|Write-Output|Write-Host|Get-Location|pwd|cwd|Get-Help|help|Select-Object|Out-String|Format-Table|Format-List|Format-Wide|Measure-Object|Sort-Object|Where-Object|Group-Object|Out-Null|ConvertTo-Json|ConvertFrom-Json)\b)|(?:\bgit\s+(?:-[^\s]+\s+[^\s]+\s+|--[^\s]+\s+)*\b(?:status|log|diff|show|branch|tag|remote|rev-parse|diff-tree|ls-files)(?:\s|$))|(?:\bdsh\s+(?:--version|--help|--dump-config|plugin\s+(?:list|show|status))(?:\s|$))|(?:\bdsh\s+--profile\s+[^\s]+\s+(?:--dump-config|plugin\s+(?:list|show|status))(?:\s|$))|(?:\bnode\s+(?:--check|--test|--version)(?:\s|$))|(?:\bnpm\s+(?:test|run\s+test)(?:\s|$))|(?:\bpnpm\s+(?:test|run\s+test)(?:\s|$))|(?:\bgh\s+(?:auth\s+status|repo\s+view)(?:\s|$))|(?:\bnpm\s+(?:ls|view)(?:\s|$))/i;
241
241
  const MUTATING_CMD_RE =
242
242
  /(?:Set-Content|Add-Content|Out-File|Remove-Item|Move-Item|Copy-Item|Rename-Item|New-Item|Clear-Content|git\s+(?:push|commit)|rm\s+-r|rmdir\s+\/s|del\s+\/s|(?:^|[^0-9])>>|(?:^|[^0-9])>)/i;
243
243
 
@@ -445,6 +445,25 @@ export function auditOutputFailed(output) {
445
445
  return /DUPLICATES FOUND|INCONSISTENT|\[MISSING\]/i.test(String(output || ""));
446
446
  }
447
447
 
448
+ /**
449
+ * v0.5.7 后续(2026-08-26 用户拍板,承诺兑现):工作区内低风险变更判定——规则 12A 正文
450
+ * "工作区内低风险新建"豁免的机器实现(此前只修了相邻项:--profile 模板/多工作区根/验证伴生;
451
+ * 本项未修——只读诊断脚本被拦的承诺闭环于此)。edit/write 且:目标在工作区内 / 非受保护路径 /
452
+ * 非插件装配文件 / 非高风险启动入口 → 低风险变更(22-7 无执行分点回合也放行;12A 侧
453
+ * isSensitiveToolCall 本就对工作区普通文件返回 false)。
454
+ */
455
+ export function isLowRiskWorkspaceNew(toolName, args) {
456
+ const name = String(toolName || "");
457
+ if (name !== "edit" && name !== "write") return false;
458
+ const p = pathTarget(args);
459
+ if (!p) return false;
460
+ if (isProtectedConfigPath(p)) return false;
461
+ if (isOutsideWorkspace(p)) return false;
462
+ if (isAssemblyMutationTool(name, args)) return false;
463
+ if (isHighRiskEntryFile(p)) return false;
464
+ return true;
465
+ }
466
+
448
467
  /** 判断是否命中敏感操作(需要授权证据;只读操作永远不算)
449
468
  * v0.5.7 P0-3(复查修复 2026-08-26):透传 sessionId——"工作区外"判定需按会话精确根(多工作区
450
469
  * 模型:会话根优先;此前遗漏透传导致会话级根从未在运行时激活,只有全局兜底层生效)。 */
package/lib/core/state.js CHANGED
@@ -54,7 +54,9 @@ export function createState() {
54
54
  llmIntentLast: null, // { text, verdict, at }(最近一次 LLM 采用/失败,/guard status 观测)
55
55
  verifyPass: [], // { at, sessionId, what }(测试通过/冷加载探针输出采集,交付闸门 M3 用)
56
56
  deniedKeys: new Set(), // 引擎 deny 的调用 key(规则 1 计数解耦)
57
- unknownToolApproved: new Set() // 机制 B:已 ask 过的未归类工具(本会话内批准后不再重复询问)
57
+ unknownToolApproved: new Set(), // 机制 B:已 ask 过的未归类工具(本会话内批准后不再重复询问)
58
+ unknownToolApprovedMeta: new Map(), // v0.5.9:name → {name,time,session}(持久化元数据,/guard tools 可视化)
59
+ unknownToolSessionAdded: new Set() // v0.5.9:本会话新增的批准(与永久加载区分,/guard tools 展示)
58
60
  };
59
61
  }
60
62
 
@@ -1,15 +1,14 @@
1
- // tool-catalog.js - 工具分类制(机制 B,2026-08-24)
1
+ // tool-catalog.js - 工具分类制(机制 B,2026-08-24;0.5.9 升级 2026-08-27
2
2
  // 任何插件(现有/未来)注册的工具都按此分类:
3
3
  // analysis = 只读分析(无条件放行)
4
- // artifact = 写产物(低风险:放行 + 审计留痕;受保护路径仍由 13A/__self-protect 把关)
4
+ // artifact = 写产物/会话态(低风险:放行 + 审计留痕;受保护路径仍由 13A/__self-protect 把关)
5
5
  // mutating = 变更类(维持现有严格授权:规则 22 粒度 / 12A / 13A)
6
- // unknown = 未归类(guard 不物理拦;由 index.js 的 pre-execute deny 层首调拦截,
7
- // 批次 3:ask 被 approval 层自动放行 → 改 deny,用户"允许使用 X"白名放行)
8
- // 设计原则:
9
- // 1. 不依赖 ctx.tools 运行时 API(纯静态表 + 命令文本分析,可独立测试);
10
- // 2. 方向性修正:旧逻辑"按参数名猜写不写、猜不出就放行"(反向漏洞)→
11
- // "未知默认 ask"(fail-closed,用户 2026-08-24 确认);
12
- // 3. 分类表随实测/新插件逐步扩充(机制允许快速扩展,无需改逻辑)。
6
+ // unknown = 未归类(由 index.js 的 pre-execute 层处置:unknownPolicy=ask(官方弹窗)或 deny + 提示)
7
+ // 0.5.9 改动(用户"整体审查"要求,K-01~K-06 依据):
8
+ // 1. 补全官方全集(tool-catalog 镜像交叉:cordis_*/terminal_*/session_*/goal/jobs/子代理/团队/保留工具等);
9
+ // 2. 前缀规则(mcp__=mutating;esr_ 细化)——生态命名空间新工具自动覆盖,不再依赖静态枚举;
10
+ // 3. 单一真源:24④ 守卫与 unknown 决策均读本表(废弃 guard-core 的独立覆盖表,杜绝双表漂移);
11
+ // 设计原则:工具名不是安全边界,内容才是(K-04)——归类只决定"识别",内容审查(12C/13A)不因归类豁免。
13
12
  import { isReadOnlyTool } from "./patterns.js";
14
13
 
15
14
  // 只读分析类:无任何文件副作用(或仅有会话内状态),无条件放行
@@ -26,7 +25,15 @@ const ANALYSIS_TOOLS = new Set([
26
25
  "web_fetch", "web_search",
27
26
  // 子代理交付工具(官方 dsh-tool-subagent-report,2026-08-24 实测发现):仅子代理环境注册,
28
27
  // 参数仅 output 文本 → 直接交付父代理,无文件副作用 → analysis(避免子代理交付被 unknown deny 阻断)
29
- "report"
28
+ "report",
29
+ // 官方 0.5.9 补全(tool-catalog 镜像 2026-08-27 交叉)
30
+ "lsp",
31
+ "session_event_read", "session_event_search", "session_event_trace", "session_search", "session_trace",
32
+ "terminal_list", "terminal_read",
33
+ "cordis_inspect_list", "cordis_inspect_query", "cordis_inspect_self",
34
+ "ask_user_question",
35
+ // ESR 状态查询(读工作区记忆状态,无持久副作用)
36
+ "esr_status", "esr_ready", "esr_model"
30
37
  ]);
31
38
 
32
39
  // 产物类:工具自然行为会写文件(artifact/工作区/会话存储),低风险——放行 + 审计。
@@ -35,24 +42,56 @@ const ARTIFACT_TOOLS = new Set([
35
42
  // 视觉产物类(裁剪/截图/长图 OCR/矢量化/抠图/物化/展示 → 写 artifact)
36
43
  "vision_crop", "vision_pixel_diff", "vision_trace", "vision_extract_foreground",
37
44
  "vision_html_screenshot", "vision_long_screenshot_ocr", "vision_materialize", "vision_present",
38
- // 记忆/日程(写引擎存储,低风险)
45
+ // 记忆/日程/目标/技能(写引擎/会话存储,低风险)
39
46
  "engram_store", "schedule_create", "schedule_delete",
47
+ "create_goal", "update_goal",
48
+ "skill", "todo_write", "visualize",
49
+ // 子代理/工作流协调(会话/子会话态,无文件副作用;内容审查另有其责)
50
+ "subagent", "subagent_fork", "send_message", "interrupt_agent", "workflow", "ralph",
51
+ // 官方 plan-mode 交互工具(参数为 markdown 计划,批准/继续规划,无文件副作用)
52
+ "exit_plan_mode",
40
53
  // 开发侧 staging(写 staging 区,dev_* 工具的自然行为)
41
- "dev_stage_add", "dev_stage_call", "dev_stage_promote", "dev_stage_demote"
54
+ "dev_stage_add", "dev_stage_call", "dev_stage_promote", "dev_stage_demote",
55
+ // ESR 工程状态写操作(写工作区记忆存储,engram_store 同族)
56
+ "esr_task", "esr_node", "esr_link", "esr_claim", "esr_close", "esr_unclaim", "esr_dep", "esr_gc"
42
57
  ]);
43
58
 
44
59
  // 变更类:维持现有严格授权(规则 22 粒度比对 / 12A 敏感授权 / 13A 备份)
45
60
  const MUTATING_TOOLS = new Set([
46
- "edit", "write", "str_replace_editor", "pwsh", "bash",
61
+ "edit", "write", "str_replace_editor", "pwsh", "bash", "upload_file",
47
62
  // 插件装配类(规则 24/27 管辖)
48
63
  "dev_install_package", "dev_inject_plugin", "dev_uninject_plugin", "dev_reload_package",
49
64
  "dev_fix_patch", "dev_heal_links",
50
- // 提交/网络写类由命令文本与 12C 管辖,此处不重复枚举
51
- "upload_file"
65
+ "dev_scaffold_plugin", "dev_build_plugin", "dev_release_plugin", "dev_clear_routes",
66
+ // 官方 0.5.9 补全(tool-catalog 镜像 2026-08-27 交叉)
67
+ "run_code", // 官方保留传输(Code Mode 唯一入口,语义"不可限制"——归类=纳入统一守卫而非拦死)
68
+ "cordis_define", "cordis_run", "cordis_stop", "cordis_undefine",
69
+ "terminal_open", "terminal_send", "terminal_close", "terminal_signal",
70
+ "job_kill",
71
+ "followup_task", "spawn_teammate", "team_task_create", "team_task_get",
72
+ "team_task_list", "team_task_update", "wait_agent",
73
+ // 本机命名空间兜底(dev_ 前缀 = 开发侧工具,行为可写可管理;具体已枚举,前缀兜底未来新增)
74
+ "dev_router_status"
52
75
  ]);
53
76
 
77
+ // 前缀规则:生态命名空间的工具自动归类(0.5.9 新)——将来的新插件遵循惯例即被覆盖。
78
+ // 安全注记(K-03):mcp__ 服务器段不做通配(Claude Code 同款约束);mcp 工具按变更类走授权(保守)。
79
+ const PREFIX_RULES = [
80
+ { prefix: "mcp__", cls: "mutating" },
81
+ { prefix: "esr_", cls: "artifact" },
82
+ { prefix: "vision_", cls: "analysis" },
83
+ { prefix: "engram_", cls: "artifact" },
84
+ { prefix: "dev_", cls: "mutating" },
85
+ { prefix: "schedule_", cls: "artifact" },
86
+ { prefix: "job_", cls: "analysis" },
87
+ { prefix: "terminal_", cls: "mutating" },
88
+ { prefix: "cordis_", cls: "mutating" },
89
+ { prefix: "session_", cls: "analysis" },
90
+ { prefix: "team_", cls: "mutating" }
91
+ ];
92
+
54
93
  /**
55
- * 工具分类判定(纯函数)。
94
+ * 工具分类判定(纯函数)。0.5.9 单一真源:24④ 守卫与 unknown 处置均读此结果。
56
95
  * @param {string} name 工具名
57
96
  * @param {object} args 工具参数(pwsh/bash 需要命令文本判读写)
58
97
  * @returns {'analysis'|'artifact'|'mutating'|'unknown'}
@@ -65,14 +104,19 @@ export function toolClass(name, args = {}) {
65
104
  if (ANALYSIS_TOOLS.has(n)) return "analysis";
66
105
  if (ARTIFACT_TOOLS.has(n)) return "artifact";
67
106
  if (MUTATING_TOOLS.has(n)) return "mutating";
107
+ // 前缀规则(细粒度集合优先;esr_ 状态查询放宽为 analysis)
108
+ if (n.startsWith("esr_") && ["esr_status", "esr_ready", "esr_model"].includes(n)) return "analysis";
109
+ for (const rule of PREFIX_RULES) {
110
+ if (n.startsWith(rule.prefix)) return rule.cls;
111
+ }
68
112
  return "unknown";
69
113
  }
70
114
 
71
115
  /**
72
116
  * unknown 工具的处置决策(pre-execute 层调用)。
73
- * 批次 3(2026-08-24,用户拍板分支 A):ask deny——实测 DSH approval 层自动放行 ask
74
- *(guardian ask unknown 首调均无弹窗直接执行 = fail-open 洞),未知工具首调必须物理拦截;
75
- * 批准路径 = 用户明确"允许使用 X"(handleSessionEvent 写入会话白名);白名后本会话放行。
117
+ * 0.5.9 修订(K-02/K-03 依据):unknown 首调从物理 deny 改为由 index.js unknownPolicy 走
118
+ * 官方 ask(弹窗,allowed-once)或 deny;本函数保留"未分类且未白名 deny(需要处置)"的
119
+ * 对外契约(调用方翻译策略),已知分类或已白名 null(放行)。
76
120
  * @param {string} name 工具名
77
121
  * @param {object} args 工具参数
78
122
  * @param {Set<string>|null} whitelist 会话内已批准工具集(可选)
@@ -0,0 +1,34 @@
1
+ // whitelist.js - 工具白名单持久化 v2(0.5.9):纯函数,可独立测试。
2
+ // 存储格式:v2 = [{"name","time","session"}](带元数据,可视化"谁/何时/哪个会话放行");
3
+ // v1 = ["name"](字符串数组,兼容读取)。v1 升级=v2 时旧条目 time/session 为 null("未知")。
4
+ export function parseWhitelist(raw) {
5
+ try {
6
+ const arr = JSON.parse(raw || "[]");
7
+ if (!Array.isArray(arr)) return [];
8
+ return arr
9
+ .map((it) => {
10
+ if (typeof it === "string") return { name: it, time: null, session: null };
11
+ if (it && typeof it.name === "string") {
12
+ return {
13
+ name: it.name,
14
+ time: typeof it.time === "number" ? it.time : null,
15
+ session: typeof it.session === "string" ? it.session : null
16
+ };
17
+ }
18
+ return null;
19
+ })
20
+ .filter(Boolean);
21
+ } catch {
22
+ return [];
23
+ }
24
+ }
25
+
26
+ /** 合并既有元数据与新增名称(新增带当前时间和会话来源),保持既有条目历史不变 */
27
+ export function mergeWhitelist(prevRows, names, sid, now = Date.now()) {
28
+ const map = new Map(prevRows.map((r) => [r.name, r]));
29
+ return names.map((n) => map.get(n) || { name: n, time: now, session: sid || null });
30
+ }
31
+
32
+ export function serializeWhitelist(rows) {
33
+ return JSON.stringify(rows);
34
+ }
package/lib/index.js CHANGED
@@ -54,8 +54,8 @@ import {
54
54
  reloadRules,
55
55
  resetTurn
56
56
  } from "./core/state.js";
57
- import { SAFE_UNCOVERED_TOOLS } from "./core/guard-core.js";
58
57
  import { toolClass } from "./core/tool-catalog.js";
58
+ import { parseWhitelist, mergeWhitelist, serializeWhitelist } from "./core/whitelist.js";
59
59
  import { state } from "./core/runtime.js";
60
60
  import { detectViolations, extractAssistantText } from "./core/text-detect.js";
61
61
  import { shouldDetectTurn, shouldDeliver } from "./core/semantic.js";
@@ -79,7 +79,7 @@ import {
79
79
  recordAction
80
80
  } from "./core/overengineering.js";
81
81
  import { writeUnderstanding } from "./core/understanding-store.js";
82
- import { agentsFilePath, auditFilePath, verifyFilePath } from "./core/paths.js";
82
+ import { agentsFilePath, auditFilePath, verifyFilePath, toolsWhitelistFilePath } from "./core/paths.js";
83
83
  import { isVersionedFile, validateEditedFile } from "./core/version-guard.js";
84
84
  import { computeMountSignature, profileNameFromArgs } from "./core/mount-signature.js";
85
85
  import { detectSilentError, extractToolOutput } from "./core/silent-error.js";
@@ -306,6 +306,29 @@ function persistVerifyPass(state) {
306
306
  }
307
307
  }
308
308
 
309
+ /** v0.5.7 后续(用户批准):unknown 工具白名单持久化(~/.dsh/rule-engine-tools.json——"允许使用 X"重启/热重载不丢)
310
+ * 0.5.9 升级:存储格式 v2 带元数据 [{name,time,session}]——可视化"谁、何时、哪个会话放行";
311
+ * 旧格式(字符串数组)兼容读取;持久化时合并既有 meta(历史条目不丢时间/来源)。 */
312
+ function persistToolWhitelist(state, sid) {
313
+ try {
314
+ const names = [...(state.unknownToolApproved || [])];
315
+ const prev = readWhitelistMeta();
316
+ const rows = mergeWhitelist([...prev.values()], names, sid);
317
+ writeFileSync(toolsWhitelistFilePath(), serializeWhitelist(rows), "utf8");
318
+ } catch {
319
+ // 持久化失败仅降级(内存态仍有效),不阻断
320
+ }
321
+ }
322
+
323
+ /** 读取白名单文件并按元数据归 Map(v1 字符串数组 → {name,time:null,session:null};v2 对象数组原样) */
324
+ function readWhitelistMeta() {
325
+ const m = new Map();
326
+ let raw = null;
327
+ try { raw = readFileSync(toolsWhitelistFilePath(), "utf8"); } catch {}
328
+ for (const row of parseWhitelist(raw || "")) m.set(row.name, row);
329
+ return m;
330
+ }
331
+
309
332
  export function handleSessionEvent(ctx, session, event) {
310
333
  maybeReloadIfChanged(state);
311
334
  const sid = session?.id || "global";
@@ -377,6 +400,11 @@ export function handleSessionEvent(ctx, session, event) {
377
400
  if (toolClass(toolName, {}) !== "unknown") continue; // 只对未归类工具有意义(已知工具不走 unknown 分支)
378
401
  const isNew = !state.unknownToolApproved.has(toolName);
379
402
  state.unknownToolApproved.add(toolName);
403
+ if (isNew) {
404
+ if (!state.unknownToolSessionAdded) state.unknownToolSessionAdded = new Set();
405
+ state.unknownToolSessionAdded.add(toolName); // /guard tools 区分"本会话新增"
406
+ }
407
+ persistToolWhitelist(state, sid); // v0.5.7 后续:白名单持久化(重启/热重载不丢——用户批准"这句不用每次发")
380
408
  audit({
381
409
  kind: "unknown-tool-whitelist",
382
410
  rule: "__unknown-tool",
@@ -710,6 +738,11 @@ export function handleSessionEvent(ctx, session, event) {
710
738
  if (!state.unknownToolApproved) state.unknownToolApproved = new Set();
711
739
  const isNew = !state.unknownToolApproved.has(wname);
712
740
  state.unknownToolApproved.add(wname);
741
+ if (isNew) {
742
+ if (!state.unknownToolSessionAdded) state.unknownToolSessionAdded = new Set();
743
+ state.unknownToolSessionAdded.add(wname);
744
+ }
745
+ persistToolWhitelist(state, sid); // v0.5.7 后续:白名单持久化(见上)
713
746
  audit({
714
747
  kind: "unknown-tool-whitelist",
715
748
  rule: "__unknown-tool",
@@ -773,7 +806,15 @@ export function handleSessionEvent(ctx, session, event) {
773
806
  reason: "交付/完成类声明缺少同会话近期 verify-pass(ALL TESTS PASSED / check-plugin-load RESULT: PASS)记录",
774
807
  session: sid
775
808
  });
776
- maybeInject(ctx, sid, { ruleId: "23", reason: "规则 23④:完成/通过类声明需同会话近期验证记录(测试全绿或冷加载 PASS),缺失已记审计——请先运行验证并附证据再宣布完成" });
809
+ // v0.5.7 后续(用户拍板"暗示型统一裁决"):23 词面命中只是嫌疑——"完成"≠交付声明
810
+ //("完成社区检索"类误触的修复):送 LLM 裁决,确认违规才投递。
811
+ void deliverSuspects(ctx, sid, [{
812
+ ruleId: "23",
813
+ title: "变更与交付验证(执行等级:D 强)",
814
+ kind: "self-certify",
815
+ awaitingJudge: true,
816
+ reason: "规则 23④:完成/通过类声明需同会话近期验证记录(测试全绿或冷加载 PASS),缺失已记审计"
817
+ }], text);
777
818
  }
778
819
  }
779
820
  // 需求基线覆盖核查(RB-09,2026-08-24):方案/重构/整体完成类声明 → 基线仍存在 planned 需求 → 审计纠正(fail-closed)
@@ -858,6 +899,8 @@ const USAGE = [
858
899
  " /guard bypass [N] 临时整体放行 N 分钟(默认 5,仅用户)",
859
900
  " /guard lock 立即恢复全部守卫(取消解锁/放行)",
860
901
  " /guard revoke 撤销全部授权记录",
902
+ " /guard tools 工具放行白名单(永久+本会话,含时间/来源会话)",
903
+ " /guard tools revoke <工具名> 撤销白名单条目(持久化+会话集)",
861
904
  " /guard reload 强制重解析 AGENTS.md",
862
905
  " /guard mode <模式> 设置任务契约模式(review/answer/change/monitor/watch/off)",
863
906
  " /guard budget ... 设置预算(agents=N files=... deps=allow hash=allow)",
@@ -885,6 +928,9 @@ function parseCommand(rawInput) {
885
928
  m = text.match(/^log\s*(\d+)?$/i);
886
929
  if (m) return { kind: "log", n: m[1] ? Number(m[1]) : 10 };
887
930
  if (/^reload$/i.test(text)) return { kind: "reload" };
931
+ if (/^tools$/i.test(text)) return { kind: "tools", action: "list" };
932
+ m = text.match(/^tools revoke\s+([A-Za-z_][A-Za-z0-9_:.-]*)$/i);
933
+ if (m) return { kind: "tools", action: "revoke", target: m[1] };
888
934
  if (/^mode\b/i.test(text)) {
889
935
  const parsed = parseModeCommand(text);
890
936
  if (parsed) return { kind: "mode", ...parsed };
@@ -1015,6 +1061,40 @@ async function executeGuard(ctx, invocation) {
1015
1061
  (saved.ok ? `\n理解产物已写入:${saved.path}` : `\n理解产物写入失败:${saved.error}`)
1016
1062
  };
1017
1063
  }
1064
+ case "tools": {
1065
+ // v0.5.9:白名单可视化——永久(元数据)+ 本会话新增;revoke = 从持久化文件与会话集移除
1066
+ if (command.action === "revoke") {
1067
+ const target = command.target;
1068
+ const prev = readWhitelistMeta();
1069
+ const existed = prev.delete(target);
1070
+ writeFileSync(toolsWhitelistFilePath(), JSON.stringify([...prev.values()]), "utf8");
1071
+ state.unknownToolApproved?.delete(target);
1072
+ state.unknownToolSessionAdded?.delete(target);
1073
+ audit({
1074
+ kind: "guard-command",
1075
+ rule: "__whitelist",
1076
+ name: "白名单撤销",
1077
+ event: "command",
1078
+ reason: `/guard tools revoke ${target}(持久化+会话集同步移除)`,
1079
+ session: invocation?.session?.id || "global"
1080
+ });
1081
+ return { kind: "success", text: `已撤销 ${target}${existed ? "" : "(原不在白名单)"}。` };
1082
+ }
1083
+ const meta = state.unknownToolApprovedMeta || new Map();
1084
+ const sessionAdded = state.unknownToolSessionAdded || new Set();
1085
+ const rows = [...(state.unknownToolApproved || [])].map((n) => {
1086
+ const m = meta.get(n) || {};
1087
+ let when = "未知";
1088
+ if (typeof m.time === "number") {
1089
+ try { when = new Date(m.time).toISOString(); } catch { when = "非法时间"; }
1090
+ }
1091
+ return `- ${n}${sessionAdded.has(n) ? "(本会话新增)" : ""} 时间=${when} 来源会话=${m.session || "未知"}`;
1092
+ });
1093
+ const text = rows.length
1094
+ ? `工具放行白名单(/guard tools):\n${rows.join("\n")}\n\n撤销:/guard tools revoke <工具名>`
1095
+ : "工具放行白名单为空。";
1096
+ return { kind: "success", text };
1097
+ }
1018
1098
  case "mode": {
1019
1099
  if (!state.taskContract?.taskContractEnabled) return { kind: "error", text: "任务契约未启用:请先在规则引擎设置页开启总开关。" };
1020
1100
  const sid = invocation?.session?.id || "global";
@@ -1080,6 +1160,17 @@ export function apply(ctx) {
1080
1160
  // 首次运行/文件缺失:无历史记录,正常
1081
1161
  }
1082
1162
 
1163
+ // v0.5.7 后续(用户批准):加载白名单持久化("允许使用 X"重启/热重载不丢)
1164
+ // 0.5.9:v2 对象数组(带 meta)与 v1 字符串数组兼容;meta 供 /guard tools 可视化
1165
+ try {
1166
+ const wlRaw = readFileSync(toolsWhitelistFilePath(), "utf8");
1167
+ const rows = parseWhitelist(wlRaw);
1168
+ state.unknownToolApproved = new Set(rows.map((r) => r.name));
1169
+ state.unknownToolApprovedMeta = new Map(rows.map((r) => [r.name, r]));
1170
+ } catch {
1171
+ // 首次运行/无文件:空白名单
1172
+ }
1173
+
1083
1174
  // 0.0.1 LLM 意图兜底配置(guardDecision 为纯函数,经 state 传入)
1084
1175
  state.llmIntentCfg = pluginConfig.llmIntent || {};
1085
1176
 
@@ -1237,30 +1328,40 @@ export function apply(ctx) {
1237
1328
  return next();
1238
1329
  });
1239
1330
 
1240
- // 0.9 机制 B(2026-08-24):未归类工具(新插件/新工具)首调 deny——fail-closed,防"approval 层自动放行 ask"的洞。
1241
- // 批次 3(2026-08-24 用户拍板分支 A):实测未知工具首调 ask DSH approval 层自动放行(无弹窗直接执行),
1242
- // 因此 unknown 物理 deny;批准路径 = 用户明确"允许使用 X"(handleSessionEvent 写入会话白名),本会话内放行。
1331
+ // 0.9 机制 B(2026-08-24;0.5.9 修订——K-02/K-03 依据):未归类工具首调按 unknownPolicy 处置:
1332
+ // "ask" = 官方 pre-execute ask 语义(approval 弹窗,allowed-once 才续行;无审批通道 官方自动 deny,
1333
+ // fail-closed 由官方保证);"deny" = 物理拒绝 + 提示("允许使用 X" 会话白名路径恒可用)。
1334
+ // 历史教训(08-24 实测 ask 被自动放行 → 当时改 deny):0.5.9 发布前必须实弹验证 ask 弹窗真实出现
1335
+ // (K-02 门禁),验证不过则发布时默认保持 deny。
1243
1336
  ctx.on("tools/pre-execute", async (exec, next) => {
1244
1337
  try {
1245
- if (SAFE_UNCOVERED_TOOLS.has(exec?.name)) return next(); // 控制类不进 unknown 判定(避免 ask 自身被 ask)
1338
+ if ((state.unknownToolApproved || new Set()).has(exec?.name)) return next(); // 会话内用户已批准
1246
1339
  if (toolClass(exec?.name, exec?.arguments || {}) !== "unknown") return next();
1247
- if ((state.unknownToolApproved || new Set()).has(exec.name)) return next(); // 会话内用户已批准
1340
+ // 0.5.9:unknownPolicy 默认 "deny"(K-02 门禁:ask 需实弹验证弹窗真实出现后才可默认;
1341
+ // 当前无真实 unknown 工具可触发实弹——分类表已全覆盖,等待未来真实场景验证后经配置切换)
1342
+ const policy = pluginConfig.unknownPolicy === "ask" ? "ask" : "deny";
1248
1343
  audit({
1249
1344
  kind: "unknown-tool",
1250
1345
  rule: "__unknown-tool",
1251
- name: "未归类工具拦截",
1346
+ name: "未归类工具首调处置",
1252
1347
  event: "tools/pre-execute",
1253
1348
  tool: exec?.name,
1254
1349
  args: summarizeArgs(exec?.arguments),
1255
- reason: `工具 ${exec.name} 不在规则引擎分类表(可能是新装的插件):已拦截(unknown→deny)。批准路径:用户明确"允许使用 ${exec.name}"后本会话放行;长期规则请补充工具分类表`,
1350
+ reason: `工具 ${exec.name} 不在规则引擎分类表:按 unknownPolicy=${policy} 首调处置(批准路径:官方弹窗批准 / 用户明确"允许使用 ${exec.name}"写入会话白名单;长期规则请补充工具分类表)`,
1256
1351
  session: sessionIdOfExec(exec)
1257
1352
  });
1353
+ if (policy === "ask") {
1354
+ return {
1355
+ kind: "ask",
1356
+ reason: `工具 ${exec.name} 未登记在规则引擎分类表:批准本次后放行;如需本会话持续放行请说"允许使用 ${exec.name}"。`
1357
+ };
1358
+ }
1258
1359
  return {
1259
1360
  kind: "deny",
1260
1361
  reason: `工具 ${exec.name} 尚未归类(新装插件?):已拦截。如需使用请向用户说明用途并获取其"允许使用 ${exec.name}"的明确答复后重试(批准后本会话内放行)。`
1261
1362
  };
1262
1363
  } catch (error) {
1263
- ctx.logger?.warn?.("[dsh-rule-engine] unknown-tool deny error", error);
1364
+ ctx.logger?.warn?.("[dsh-rule-engine] unknown-tool handling error", error);
1264
1365
  return next();
1265
1366
  }
1266
1367
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-rule-engine",
3
- "version": "0.5.7",
3
+ "version": "0.5.9",
4
4
  "description": "DSH 规则执行引擎 v3:容器解析 AGENTS.md + 理解器 + 匹配机 + 执行框架",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -43,6 +43,7 @@
43
43
  "scripts": {
44
44
  "test": "node test/run-all.mjs",
45
45
  "check": "node --check lib/index.js",
46
+ "verify": "node scripts/verify-all.mjs",
46
47
  "audit:mount": "node scripts/audit-mount-consistency.mjs --profile web"
47
48
  },
48
49
  "peerDependencies": {
@@ -0,0 +1,41 @@
1
+ // check-tool-coverage.mjs — 0.5.9 工具覆盖门禁(K-01/K-06 依据,仿官方 verify-tool-catalog):
2
+ // 官方 tool-catalog(生成器产物=权威全集)中的每个工具名都必须在分类表/前缀规则内被识别,
3
+ // 任何一个 unknown = 该工具调用会被未知工具首调处置(ask/deny)→ 工程失败。
4
+ // 用法:node scripts/check-tool-coverage.mjs
5
+ import { readFileSync } from "node:fs";
6
+ import { toolClass } from "../lib/core/tool-catalog.js";
7
+
8
+ const CATALOG = "D:\\DeepSeek harness\\dsh-project\\docs-site-text\\en\\reference\\tool-catalog.txt";
9
+
10
+ function parseNames() {
11
+ const names = new Set();
12
+ for (const line of readFileSync(CATALOG, "utf8").split("\n")) {
13
+ if (!line.includes("@deepseek-ai/dsh-")) continue;
14
+ const cols = line.split("|").map((c) => c.trim());
15
+ // 表列格式:| | <package> | <model-visible names> | <requires> | <writes> | <shipped aliases> | <note> |
16
+ if (!cols[2] || !cols[2].startsWith("@deepseek-ai")) continue;
17
+ const add = (s) => {
18
+ if (!s || s === "-") return;
19
+ for (const x of s.split(",")) {
20
+ const n = x.trim();
21
+ if (/^[A-Za-z_][A-Za-z0-9_:.-]*$/.test(n)) names.add(n);
22
+ }
23
+ };
24
+ add(cols[3]);
25
+ add(cols[6]);
26
+ }
27
+ return names;
28
+ }
29
+
30
+ const official = parseNames();
31
+ const missing = [];
32
+ for (const n of official) {
33
+ if (toolClass(n, {}) === "unknown") missing.push(n);
34
+ }
35
+
36
+ if (missing.length) {
37
+ console.error(`COVERAGE-FAIL:官方 tool-catalog 有 ${missing.length} 个工具未分类(会被未知工具首调处置):`);
38
+ for (const n of missing) console.error(" - " + n);
39
+ process.exit(1);
40
+ }
41
+ console.log(`COVERAGE-OK:官方 tool-catalog 工具全覆盖(${official.size} 个工具名均被分类表/前缀规则识别)`);
@@ -1,9 +1,11 @@
1
- // verify-all.mjs - 交付前体检报告(2026-08-26,对齐官方 docs/testing.zh.md 分层验证)。
2
- // 四层:① 语法(lib 全部 .js node --check)
1
+ // verify-all.mjs - 交付前体检报告(2026-08-26,对齐官方 docs/testing.zh.md 分层验证;0.5.9 增第⑤层)。
2
+ // 五层:① 语法(lib 全部 .js node --check)
3
3
  // ② 单元(test/run-all.mjs——单元+机制层,不含 e2e)
4
4
  // ③ 组合冒烟(test/loader-smoke.e2e.mjs——真实接线 + 外部世界断言)
5
5
  // ④ 真实判例(外部世界:近 24h 台账中 judge-pass/judge-false 记录数;
6
6
  // 0 条 = WARN 提示需实弹,≥1 条 = 有真实裁决证据——读文件,不是自我报告)
7
+ // ⑤ 工具箱覆盖(scripts/check-tool-coverage.mjs——官方 tool-catalog 全集 vs 分类表,
8
+ // 任何 unknown = 未知工具首调处置会拦用户 → 红色(run_code 事故同类,K-01/K-06 门禁))
7
9
  // 任一 ❌ → exit 1;⚠️(WARN)不阻塞但必须明示。
8
10
  // 注意:子进程输出捕获需完整权限运行(受限模式 EPERM)。
9
11
  import { execFileSync } from "node:child_process";
@@ -53,6 +55,9 @@ step("单元(test/run-all.mjs)", process.execPath, [join(root, "test", "run-
53
55
  // ── ③ 组合冒烟 ──
54
56
  step("组合冒烟(loader-smoke,真实接线+外部世界断言)", process.execPath, [join(root, "test", "loader-smoke.e2e.mjs")]);
55
57
 
58
+ // ── ⑤ 工具箱覆盖(0.5.9 门禁) ──
59
+ step("工具箱覆盖(官方 tool-catalog vs 分类表,缺失即红)", process.execPath, [join(root, "scripts", "check-tool-coverage.mjs")]);
60
+
56
61
  // ── ④ 真实判例(外部世界) ──
57
62
  let judgeReal = 0;
58
63
  const auditPath = join(process.env.DSH_HOME || join(process.env.USERPROFILE || "", ".dsh"), "rule-engine.log.jsonl");