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 +8 -1
- package/lib/core/guard-core.js +7 -0
- package/lib/core/paths.js +5 -0
- package/lib/core/patterns.js +20 -1
- package/lib/index.js +32 -2
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# dsh-rule-engine
|
|
2
2
|
|
|
3
3
|

|
|
4
|
-

|
|
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
|
- 默认**关闭**;可在规则引擎设置页开启「任务边界与反过度工程」总开关。
|
package/lib/core/guard-core.js
CHANGED
|
@@ -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
|
+
}
|
package/lib/core/patterns.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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": {
|