dsh-rule-engine 0.5.16 → 0.6.0

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/lib/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // dsh-rule-engine —— DSH 规则执行引擎 v3(host 插件,纯 Node)
2
2
  // 容器:解析 AGENTS.md → 理解器 → 匹配机 → 执行框架。
3
3
  // 执行框架:ctx.tools.guard() 硬拦 + session/event 文本纠察 + 审计台账 + /guard 命令。
4
- import { readFileSync, watch, writeFileSync } from "node:fs";
4
+ import { readFileSync, watch, writeFileSync, existsSync } from "node:fs";
5
5
  import { randomUUID } from "node:crypto";
6
6
  import { audit, readAuditLog } from "./core/audit.js";
7
7
  import { loadPluginConfig } from "./core/config.js";
@@ -61,6 +61,7 @@ import {
61
61
  import { toolClass } from "./core/tool-catalog.js";
62
62
  import { parseWhitelist, mergeWhitelist, serializeWhitelist } from "./core/whitelist.js";
63
63
  import { state } from "./core/runtime.js";
64
+ import { APPROVE_TYPES, normalizePath, setTypeHints } from "./core/authorization.js";
64
65
  import { DELIVERY_RE, detectViolations, extractAssistantText } from "./core/text-detect.js";
65
66
  import { buildTurnCard } from "./core/turn-card.js";
66
67
  import { shouldDetectTurn, shouldDeliver } from "./core/semantic.js";
@@ -101,6 +102,8 @@ export const name = "dsh-rule-engine";
101
102
  export const inject = ["tools", "commands", "agents", "workspaceRegistry", "skills", "llm"];
102
103
 
103
104
  const pluginConfig = loadPluginConfig();
105
+ // C4:启动时合并配置层 typeHints(本机扩展授权类型;默认空=仅内置 8+archive 类)
106
+ setTypeHints(pluginConfig.typeHints);
104
107
  state.enabled = pluginConfig.enabled;
105
108
  applyTaskContractConfig(state, pluginConfig);
106
109
  // T3 通用化(2026-08-31):声明式绑定覆盖表注入(rule-engine.json 可选键 handlerOverrides;缺省 {})
@@ -108,6 +111,44 @@ state.handlerOverrides = pluginConfig?.handlerOverrides || {};
108
111
  // 残余1 剥离(2026-08-31):本机默认偏好表注入(rule-engine.json 可选键 handlerDefaultMap;
109
112
  // 通用部署=空表——代码零本机编号,本机偏好不随包走、升级不丢)
110
113
  state.handlerDefaultMap = pluginConfig?.handlerDefaultMap || {};
114
+ // P2(2026-09-04):本机集成参数化层(localIntegrations)——默认空=通用行为不变;
115
+ // 本机专属约定(统一入口脚本名/M8 双通道/手册路径豁免扩展)由配置注入,代码零本机字面量
116
+ state.localIntegrations = pluginConfig?.localIntegrations || {};
117
+ // A-8(0.6.0,启动自检硬验收):localIntegrations 存在但 entryScript 指向的脚本文件在磁盘不存在
118
+ // → 启动即审计告警(失败不阻断加载,但留下可观测证据——无声守卫消失风险的对策)
119
+ // 0.6.0 修正:裸文件名(无路径段)视为脚本名——可能在工作区/全局 PATH,
120
+ // 不告警(防误报);仅当 entryScript 含明确路径段(\ / 盘符)且文件不存在时才告警。
121
+ {
122
+ const li = state.localIntegrations;
123
+ const script = li?.entryScript;
124
+ const isBareName = typeof script === "string" && /^[A-Za-z0-9._-]+$/.test(script.replace(/\\/g, "/"));
125
+ if (typeof script === "string" && script && !isBareName) {
126
+ let found = false;
127
+ try { found = existsSync(script); } catch { found = false; }
128
+ if (!found) {
129
+ audit({
130
+ kind: "entry-script-missing",
131
+ rule: "__entry-script-missing",
132
+ name: "启动自检:统一入口脚本不存在",
133
+ event: "startup",
134
+ reason: `localIntegrations.entryScript 指向的脚本(${script})在磁盘不存在——本机守卫仍按配置工作,但入口通道可能失效;请检查配置或部署该脚本`,
135
+ session: "global"
136
+ });
137
+ }
138
+ }
139
+ }
140
+ // li-skipped(0.6.0 A2-2 补充):无 localIntegrations.entryScript → 启动登记一次 skipped 审计
141
+ // (验证"默认无"留痕;登录于启动/配置加载时,而非每次工具调用——修正此前写类分支被 12A 前置拦截的缺陷)
142
+ if (!state.localIntegrations?.entryScript) {
143
+ audit({
144
+ kind: "li-skipped",
145
+ rule: "19",
146
+ name: "本地集成未配置(守卫无对象)",
147
+ event: "startup",
148
+ reason: "skipped: no localIntegrations.entryScript——该守卫在此环境不存在(0.6.0 默认无)",
149
+ session: "global"
150
+ });
151
+ }
111
152
  // reloadRules 内部已统一刷新理解产物(P0-3),此处不再重复写
112
153
  reloadRules(state);
113
154
 
@@ -327,7 +368,8 @@ async function deliverSuspects(ctx, sessionId, suspects, text) {
327
368
  name: "违规裁决",
328
369
  event: "assistant/message",
329
370
  reason: `${verdict.action}:${verdict.note || ""}${verdict.model ? `(model=${verdict.model})` : ""}`,
330
- session: sessionId
371
+ session: sessionId,
372
+ verdictSource: "judge"
331
373
  });
332
374
  return verdict.action === "deliver" ? v : null;
333
375
  } catch (error) {
@@ -433,15 +475,18 @@ export function handleSessionEvent(ctx, session, event) {
433
475
  return;
434
476
  }
435
477
  if (event.type === "turn/end") {
436
- // M8 双通道机制:本回合用 dsh-manual-write 落盘手册/AGENTS 后,必须同轮 engram_store 沉淀记忆;
478
+ // M8 双通道机制:本回合用统一入口落盘手册/AGENTS 后,必须同轮 engram_store 沉淀记忆;
437
479
  // 缺失 → 审计 + 注入纠正(用户明确要求机制化,2026-08-24)
438
- if (s.turn.manualWriteSeen && !s.turn.engramStoreSeen) {
480
+ // A3-1(0.6.0):默认关闭 显式 enabled:true + 配置 entryMarker 才生效(无配置 = M8 机制整体不存在)
481
+ const m8Cfg = state.localIntegrations?.m8 || {};
482
+ const m8Marker = m8Cfg.entryMarker;
483
+ if (m8Cfg.enabled === true && m8Marker && s.turn.manualWriteSeen && !s.turn.engramStoreSeen) {
439
484
  audit({
440
485
  kind: "engram-gap",
441
486
  rule: "__engram-gap",
442
487
  name: "双通道记忆缺失",
443
488
  event: "turn/end",
444
- reason: "本回合 dsh-manual-write 落盘了手册/AGENTS,但未同轮调用 engram_store;按规则 19/77/M8 应在同一回合完成记忆沉淀",
489
+ reason: "本回合经本机配置的统一入口落盘了手册/AGENTS,但未同轮调用 engram_store;按规则 19/77/M8 应在同一回合完成记忆沉淀",
445
490
  session: sid
446
491
  });
447
492
  maybeInject(ctx, sid, {
@@ -658,7 +703,7 @@ export function handleSessionEvent(ctx, session, event) {
658
703
  pendingCall.backupPaths.push(bpTool);
659
704
  }
660
705
  }
661
- if (isManualReadTool(toolName, args)) s.manualReadSeen = true;
706
+ if (isManualReadTool(toolName, args, state.localIntegrations?.manualExempt?.paths || [])) s.manualReadSeen = true;
662
707
  // 规则 5/31 扩展(2026-09-01):查询类工具调用记下回合号("近 3 回合有据"判定)
663
708
  if (isReadOnlyTool(toolName, args)) s.lastQueryTurn = s.turn.number;
664
709
  if (toolName === "skill" && args?.name) s.turn.skillNames.push(args.name);
@@ -771,10 +816,13 @@ export function handleSessionEvent(ctx, session, event) {
771
816
  }
772
817
  }
773
818
 
774
- // M8 双通道机制(2026-08-24):dsh-manual-write 落盘成功 → 标记;同轮 engram_store 成功 → 标记
819
+ // M8 双通道机制(2026-08-24):统一入口落盘成功 → 标记;同轮 engram_store 成功 → 标记
820
+ // A3-1(0.6.0):默认关闭 → 显式 enabled:true + 配置 entryMarker 才生效(无配置 = M8 机制整体不存在)
775
821
  if (!isError && pendingCall) {
776
822
  const cmd = pendingCall.args?.command || pendingCall.args?.code || "";
777
- if ((pendingCall.name === "pwsh" || pendingCall.name === "bash") && /\bdsh-manual-write\.mjs/.test(cmd)) {
823
+ const m8Cfg2 = state.localIntegrations?.m8 || {};
824
+ const marker = m8Cfg2.entryMarker;
825
+ if (m8Cfg2.enabled === true && marker && (pendingCall.name === "pwsh" || pendingCall.name === "bash") && cmd.includes(marker)) {
778
826
  s.turn.manualWriteSeen = true;
779
827
  }
780
828
  if (pendingCall.name === "engram_store") {
@@ -1035,7 +1083,7 @@ export function handleSessionEvent(ctx, session, event) {
1035
1083
  rule: "23",
1036
1084
  name: "交付声明缺验证闸门记录",
1037
1085
  event: "assistant/message",
1038
- reason: "交付/完成类声明缺少同会话近期 verify-pass(ALL TESTS PASSED / check-plugin-load RESULT: PASS)记录",
1086
+ reason: "交付/完成类声明缺少同会话近期 verify-pass(测试全绿或冷加载探针 RESULT: PASS)记录",
1039
1087
  session: sid
1040
1088
  });
1041
1089
  // v0.5.7 后续(用户拍板"暗示型统一裁决"):23④ 词面命中只是嫌疑——"完成"≠交付声明
@@ -1080,7 +1128,7 @@ export function handleSessionEvent(ctx, session, event) {
1080
1128
  }
1081
1129
  }
1082
1130
  // F1(2026-08-28 阶段三):规则 2 违规不在此时投递——标记 pendingRule2,turn/end 复核(Get-Date 定案)①
1083
- let violations = detectViolations({ configs: state.configs, session: s, text, reasoningText: s.turn.reasoningText, mountRevision: state.mountRevision });
1131
+ let violations = detectViolations({ configs: state.configs, session: s, text, reasoningText: s.turn.reasoningText, mountRevision: state.mountRevision, rule5Window: pluginConfig.rule5SourceWindow ?? 3 });
1084
1132
  const rule2s = violations.filter((v) => v.ruleId === "2");
1085
1133
  for (const v of rule2s) {
1086
1134
  if (!s.turn.pendingRule2) s.turn.pendingRule2 = v.reason;
@@ -1093,7 +1141,9 @@ export function handleSessionEvent(ctx, session, event) {
1093
1141
  name: v.title,
1094
1142
  event: "assistant/message",
1095
1143
  reason: v.reason,
1096
- session: sid
1144
+ session: sid,
1145
+ // B2(2026-09-03):判定来源可溯源(判例回灌前置:区分词表直判与 LLM 裁决分流)
1146
+ verdictSource: v.awaitingJudge ? "judge" : "lexicon"
1097
1147
  });
1098
1148
  }
1099
1149
  // v0.5.7 分流水线:B 级机器型(correct,词表=确定证据)立即投递;
@@ -1145,7 +1195,8 @@ const COMMAND_SPECS = [
1145
1195
  { name: "budget", args: "...", desc: "设置预算(agents=N files=... deps=allow hash=allow)" },
1146
1196
  { name: "contract", args: "", desc: "查看当前任务契约" },
1147
1197
  { name: "contract categories", args: "...", desc: "设定契约类别白名单(0.5.12)" },
1148
- { name: "label", args: "<id> <label>", desc: "给审计记录打标(correct/incorrect/inconclusive)" }
1198
+ { name: "label", args: "<id> <label>", desc: "给审计记录打标(correct/incorrect/inconclusive)" },
1199
+ { name: "approve", args: "<type> <路径> [min]", desc: "物理确认:授予指定类型+路径的临时授权(仅用户输入;默认 10 分钟)" }
1149
1200
  ];
1150
1201
 
1151
1202
  const USAGE = [
@@ -1201,6 +1252,17 @@ function parseCommand(rawInput) {
1201
1252
  if (m) return { kind: "label", eventId: m[1], label: m[2].toLowerCase() };
1202
1253
  m = text.match(/^label\s+clear\s+(.+)$/i);
1203
1254
  if (m) return { kind: "label-clear", fingerprint: m[1].trim() };
1255
+ // C4:/guard approve <type> <路径> [min]——物理确认(类型在 executeGuard 校验枚举)
1256
+ m = text.match(/^approve\s+([A-Za-z_][A-Za-z0-9_-]*)\s*("(?:[^"]*)"|'(?:[^']*)'|\S+)(?:\s+(\d+))?$/i);
1257
+ if (m) {
1258
+ const p = m[2];
1259
+ return {
1260
+ kind: "approve",
1261
+ type: m[1].toLowerCase(),
1262
+ path: p.replace(/^["']|["']$/g, ""),
1263
+ minutes: m[3] ? Number(m[3]) : 10
1264
+ };
1265
+ }
1204
1266
  return { kind: "invalid" };
1205
1267
  }
1206
1268
 
@@ -1278,6 +1340,30 @@ async function executeGuard(ctx, invocation) {
1278
1340
  text: `已临时放行全部守卫 ${minutes} 分钟。到期自动恢复,也可 /guard reload 后立即恢复。`
1279
1341
  };
1280
1342
  }
1343
+ case "approve": {
1344
+ // C4(2026-09-03)物理确认:用户亲手输入=词表无法误读;最小范围(类型+路径+时长);无全局通配
1345
+ if (!loadPluginConfig().approveEnabled) {
1346
+ return { kind: "error", text: "/guard approve 未开启:请在 rule-engine.json 设 approveEnabled=true(设置页开关随后续版本)后使用" };
1347
+ }
1348
+ if (!APPROVE_TYPES().includes(command.type)) {
1349
+ return { kind: "error", text: `不支持的类型 ${command.type}(可用:${APPROVE_TYPES().join("/")};any=全局通配被禁止——物理确认必须最小范围)` };
1350
+ }
1351
+ if (!command.path) return { kind: "error", text: "路径缺失:/guard approve <type> <路径> [min](建议路径用双引号包裹,如 /guard approve write \"D:\\...\\file.json\" 10)" };
1352
+ const minutes = Math.min(Math.max(1, command.minutes), 720);
1353
+ const pfx = normalizePath(command.path);
1354
+ const sid = sessionIdOfInvocation(invocation);
1355
+ recordAuthorization(state, sid, {
1356
+ type: command.type,
1357
+ pathPrefix: pfx,
1358
+ source: "physical-confirm",
1359
+ expiresAt: Date.now() + minutes * 60000
1360
+ });
1361
+ audit({ kind: "guard-command", rule: "__physical-confirm", name: "物理确认授权", event: "command", reason: `/guard approve ${command.type} ${pfx} ${minutes}m`, session: sid });
1362
+ return {
1363
+ kind: "success",
1364
+ text: `已物理确认授权:${command.type}|${pfx}|${minutes} 分钟。仅该类型+该路径(含子路径)生效;/guard revoke 可立即撤销;到期自动失效。`
1365
+ };
1366
+ }
1281
1367
  case "lock": {
1282
1368
  state.unlockUntil = 0;
1283
1369
  state.bypassUntil = 0;
@@ -1551,7 +1637,10 @@ export function apply(ctx) {
1551
1637
  //(调整/补充/评估/建议且无落盘词)→ approval-gap 审计 + 注入提醒(规则 22 自证③:方案性指令 ≠ 落盘授权)
1552
1638
  {
1553
1639
  const cmd = exec?.arguments?.command || exec?.arguments?.code || "";
1554
- if ((exec?.name === "pwsh" || exec?.name === "bash") && /\bdsh-manual-write\.mjs/.test(cmd)) {
1640
+ const entryScript = state.localIntegrations?.entryScript;
1641
+ // A3-2(0.6.0):入口脚本名从配置读取并转义;无配置不走本分支。
1642
+ // 边界裁决(v1.3 关键裁决 4):正则保持无尾部 \b(保守匹配,xxx.mjs.bak 等变体同样命中)。
1643
+ if ((exec?.name === "pwsh" || exec?.name === "bash") && entryScript && new RegExp(`\\b${entryScript.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`).test(cmd)) {
1555
1644
  const s7 = getSessionState(state, sessionIdOfExec(exec));
1556
1645
  if (needsApprovalReminder(s7.turn.userText || "")) {
1557
1646
  audit({
package/lib/service.js CHANGED
@@ -204,7 +204,8 @@ class RuleEngineService extends TypertRemoteService {
204
204
  askEnabled: conf.askEnabled === true,
205
205
  taskContractMode: conf.taskContractMode === "armed" ? "armed" : "observe",
206
206
  taskContractDefaults: conf.taskContractDefaults || {},
207
- turnCard: { enabled: conf.turnCard?.enabled === true }
207
+ turnCard: { enabled: conf.turnCard?.enabled === true },
208
+ approveEnabled: conf.approveEnabled === true
208
209
  }
209
210
  };
210
211
  } catch (error) {
@@ -215,6 +216,7 @@ class RuleEngineService extends TypertRemoteService {
215
216
  /** 保存任务契约配置(设置页;写入 rule-engine.json 并热同步 state) */
216
217
  async setTaskContractConfig(partial) {
217
218
  try {
219
+ audit({ kind: "guard-command", rule: "__settings-save", name: "设置页保存", event: "command", reason: `setTaskContractConfig keys=${JSON.stringify(Object.keys(partial || {}))}`, session: "global" });
218
220
  const conf = savePluginConfig(partial || {});
219
221
  applyTaskContractConfig(state, conf);
220
222
  return {
@@ -224,7 +226,8 @@ class RuleEngineService extends TypertRemoteService {
224
226
  askEnabled: conf.askEnabled === true,
225
227
  taskContractMode: conf.taskContractMode === "armed" ? "armed" : "observe",
226
228
  taskContractDefaults: conf.taskContractDefaults || {},
227
- turnCard: { enabled: conf.turnCard?.enabled === true }
229
+ turnCard: { enabled: conf.turnCard?.enabled === true },
230
+ approveEnabled: conf.approveEnabled === true
228
231
  }
229
232
  };
230
233
  } catch (error) {
package/package.json CHANGED
@@ -1,58 +1,60 @@
1
- {
2
- "name": "dsh-rule-engine",
3
- "version": "0.5.16",
4
- "description": "DSH 规则执行引擎 v3:容器解析 AGENTS.md + 理解器 + 匹配机 + 执行框架",
5
- "type": "module",
6
- "main": "lib/index.js",
7
- "exports": {
8
- ".": "./lib/index.js",
9
- "./lib/service.js": "./lib/service.js",
10
- "./package.json": "./package.json"
11
- },
12
- "files": [
13
- "lib",
14
- "scripts",
15
- "upgrade-impact.json",
16
- "cordis.patch.yml",
17
- "README.md",
18
- "LICENSE"
19
- ],
20
- "keywords": [
21
- "deepseek-harness",
22
- "dsh",
23
- "dsh-plugin",
24
- "cordis",
25
- "rules",
26
- "guard",
27
- "security",
28
- "agent"
29
- ],
30
- "license": "MIT",
31
- "repository": {
32
- "type": "git",
33
- "url": "https://github.com/jilian-dsh/dsh-rule-engine.git"
34
- },
35
- "homepage": "https://github.com/jilian-dsh/dsh-rule-engine",
36
- "engines": {
37
- "node": ">=22"
38
- },
39
- "dshCompat": {
40
- "min": "0.1.0-rc.3",
41
- "max": "0.2.0"
42
- },
43
- "scripts": {
44
- "test": "node test/run-all.mjs",
45
- "check": "node --check lib/index.js",
46
- "verify": "node scripts/verify-all.mjs",
47
- "audit:mount": "node scripts/audit-mount-consistency.mjs --profile web"
48
- },
49
- "peerDependencies": {
50
- "@deepseek-ai/dsh-home-paths": ">=0.1.0-rc.3 <0.2.0 || >=0.1.1-rc.0 <0.2.0",
51
- "@deepseek-ai/dsh-typert-protocol": ">=0.1.0-rc.3 <0.2.0 || >=0.1.1-rc.0 <0.2.0"
52
- },
53
- "dsh": {
54
- "bundle": {
55
- "patch": "./cordis.patch.yml"
56
- }
57
- }
58
- }
1
+ {
2
+ "name": "dsh-rule-engine",
3
+ "version": "0.6.0",
4
+ "description": "DSH 规则执行引擎 v3:容器解析 AGENTS.md + 理解器 + 匹配机 + 执行框架",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "exports": {
8
+ ".": "./lib/index.js",
9
+ "./lib/service.js": "./lib/service.js",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "files": [
13
+ "lib",
14
+ "scripts",
15
+ "!scripts/local-residue-markers.txt",
16
+ "upgrade-impact.json",
17
+ "cordis.patch.yml",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "keywords": [
22
+ "deepseek-harness",
23
+ "dsh",
24
+ "dsh-plugin",
25
+ "cordis",
26
+ "rules",
27
+ "guard",
28
+ "security",
29
+ "agent"
30
+ ],
31
+ "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/jilian-dsh/dsh-rule-engine.git"
35
+ },
36
+ "homepage": "https://github.com/jilian-dsh/dsh-rule-engine",
37
+ "engines": {
38
+ "node": ">=22"
39
+ },
40
+ "dshCompat": {
41
+ "min": "0.1.0-rc.3",
42
+ "max": "0.2.0"
43
+ },
44
+ "scripts": {
45
+ "test": "node test/run-all.mjs",
46
+ "check": "node --check lib/index.js",
47
+ "verify": "node scripts/verify-all.mjs",
48
+ "audit:mount": "node scripts/audit-mount-consistency.mjs --profile web",
49
+ "check:meta": "node scripts/readme-version-check.mjs && node scripts/local-residue-scan.mjs"
50
+ },
51
+ "peerDependencies": {
52
+ "@deepseek-ai/dsh-home-paths": ">=0.1.0-rc.3 <0.2.0 || >=0.1.1-rc.0 <0.2.0",
53
+ "@deepseek-ai/dsh-typert-protocol": ">=0.1.0-rc.3 <0.2.0 || >=0.1.1-rc.0 <0.2.0"
54
+ },
55
+ "dsh": {
56
+ "bundle": {
57
+ "patch": "./cordis.patch.yml"
58
+ }
59
+ }
60
+ }