dsh-rule-engine 0.5.7 → 0.5.8

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.8-blue)
5
5
 
6
6
  DSH 规则执行引擎 v3 的插件实现。它把 `~/.dsh/AGENTS.md` 当作唯一真相源,自动解析规则四要素与执行等级,再通过「工具守卫 + 文本检测 + 时序检查 + 审计台账」执行用户规则,而不是内置一套与用户无关的安全清单。
7
7
 
@@ -46,6 +46,13 @@ DSH 规则执行引擎 v3 的插件实现。它把 `~/.dsh/AGENTS.md` 当作唯
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.8(2026-08-26 追加)
50
+
51
+ - **白名单持久化**:"允许使用 X"(未归类工具批准)落盘 `~/.dsh/rule-engine-tools.json`——此前为内存态,热重载/重启即清(用户需反复重发,实测 esr_task/esr_close 各被清一次);
52
+ - **只读命令词表补全**:`Select-Object / Out-String / Format-Table / Measure-Object / Sort-Object / Where-Object / Group-Object / Out-Null / ConvertTo-Json` 等常见只读 cmdlet 纳入只读判定(此前 `Get-Item | Select-Object` 被误判为变更拦截)——含"管道内含写操作仍判变更"的安全回归;
53
+ - **`npm run verify`** 注册(= `scripts/verify-all.mjs` 四层体检别名);
54
+ - 工作区内低风险变更豁免(12A 正文对齐:只读诊断脚本不再被拦——0.5.7 的延续项)。
55
+
49
56
  ## 任务契约与反过度工程(可选)
50
57
 
51
58
  - 默认**关闭**;可在规则引擎设置页开启「任务边界与反过度工程」总开关。
@@ -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";
@@ -338,6 +339,12 @@ function matchRule(cfg, ctx) {
338
339
  }
339
340
  if (cls === "unknown") return null; // 物理不拦;pre-execute ask 层首调询问
340
341
  if (denyMutation) {
342
+ // v0.5.7 后续(用户拍板):工作区内低风险变更豁免——12A 正文豁免的机器实现
343
+ //(只读诊断脚本被拦的最终修复:此前 --profile 模板/多根/验证伴生已修,本项补齐)
344
+ if (isLowRiskWorkspaceNew(name, args)) {
345
+ audit?.({ kind: "allow", rule: cfg.ruleId, name: "低风险新建豁免", tool: name, reason: `工作区内低风险变更(12A 正文豁免):${describeOp(operationOf(name, args))}`, session: sessionIdOf(exec) });
346
+ return null;
347
+ }
341
348
  // LLM 意图兜底(方案 A,2026-08-24):词表判拦 + LLM 高/低置信判 execute → 放行
342
349
  //(非对称:LLM 只解救不收紧;审计在预取时已记 intent-llm)
343
350
  const verdict = verdictForDeny(session.turn, state.llmIntentCfg || {});
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/index.js CHANGED
@@ -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,15 @@ function persistVerifyPass(state) {
306
306
  }
307
307
  }
308
308
 
309
+ /** v0.5.7 后续(用户批准):unknown 工具白名单持久化(~/.dsh/rule-engine-tools.json——"允许使用 X"重启/热重载不丢) */
310
+ function persistToolWhitelist(state) {
311
+ try {
312
+ writeFileSync(toolsWhitelistFilePath(), JSON.stringify([...(state.unknownToolApproved || [])]), "utf8");
313
+ } catch {
314
+ // 持久化失败仅降级(内存态仍有效),不阻断
315
+ }
316
+ }
317
+
309
318
  export function handleSessionEvent(ctx, session, event) {
310
319
  maybeReloadIfChanged(state);
311
320
  const sid = session?.id || "global";
@@ -377,6 +386,7 @@ export function handleSessionEvent(ctx, session, event) {
377
386
  if (toolClass(toolName, {}) !== "unknown") continue; // 只对未归类工具有意义(已知工具不走 unknown 分支)
378
387
  const isNew = !state.unknownToolApproved.has(toolName);
379
388
  state.unknownToolApproved.add(toolName);
389
+ persistToolWhitelist(state); // v0.5.7 后续:白名单持久化(重启/热重载不丢——用户批准"这句不用每次发")
380
390
  audit({
381
391
  kind: "unknown-tool-whitelist",
382
392
  rule: "__unknown-tool",
@@ -710,6 +720,7 @@ export function handleSessionEvent(ctx, session, event) {
710
720
  if (!state.unknownToolApproved) state.unknownToolApproved = new Set();
711
721
  const isNew = !state.unknownToolApproved.has(wname);
712
722
  state.unknownToolApproved.add(wname);
723
+ persistToolWhitelist(state); // v0.5.7 后续:白名单持久化(见上)
713
724
  audit({
714
725
  kind: "unknown-tool-whitelist",
715
726
  rule: "__unknown-tool",
@@ -773,7 +784,15 @@ export function handleSessionEvent(ctx, session, event) {
773
784
  reason: "交付/完成类声明缺少同会话近期 verify-pass(ALL TESTS PASSED / check-plugin-load RESULT: PASS)记录",
774
785
  session: sid
775
786
  });
776
- maybeInject(ctx, sid, { ruleId: "23", reason: "规则 23④:完成/通过类声明需同会话近期验证记录(测试全绿或冷加载 PASS),缺失已记审计——请先运行验证并附证据再宣布完成" });
787
+ // v0.5.7 后续(用户拍板"暗示型统一裁决"):23 词面命中只是嫌疑——"完成"≠交付声明
788
+ //("完成社区检索"类误触的修复):送 LLM 裁决,确认违规才投递。
789
+ void deliverSuspects(ctx, sid, [{
790
+ ruleId: "23",
791
+ title: "变更与交付验证(执行等级:D 强)",
792
+ kind: "self-certify",
793
+ awaitingJudge: true,
794
+ reason: "规则 23④:完成/通过类声明需同会话近期验证记录(测试全绿或冷加载 PASS),缺失已记审计"
795
+ }], text);
777
796
  }
778
797
  }
779
798
  // 需求基线覆盖核查(RB-09,2026-08-24):方案/重构/整体完成类声明 → 基线仍存在 planned 需求 → 审计纠正(fail-closed)
@@ -1080,6 +1099,17 @@ export function apply(ctx) {
1080
1099
  // 首次运行/文件缺失:无历史记录,正常
1081
1100
  }
1082
1101
 
1102
+ // v0.5.7 后续(用户批准):加载白名单持久化("允许使用 X"重启/热重载不丢)
1103
+ try {
1104
+ const wlRaw = readFileSync(toolsWhitelistFilePath(), "utf8");
1105
+ const wlArr = JSON.parse(wlRaw);
1106
+ if (Array.isArray(wlArr)) {
1107
+ state.unknownToolApproved = new Set(wlArr.filter((n) => typeof n === "string"));
1108
+ }
1109
+ } catch {
1110
+ // 首次运行/无文件:空白名单
1111
+ }
1112
+
1083
1113
  // 0.0.1 LLM 意图兜底配置(guardDecision 为纯函数,经 state 传入)
1084
1114
  state.llmIntentCfg = pluginConfig.llmIntent || {};
1085
1115
 
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.8",
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": {