dsh-rule-engine 0.5.17 → 0.6.1
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 +268 -236
- package/lib/core/authorization.js +68 -13
- package/lib/core/contract.js +1 -1
- package/lib/core/guard-core.js +46 -22
- package/lib/core/intent.js +322 -322
- package/lib/core/mount-signature.js +87 -87
- package/lib/core/patterns.js +744 -730
- package/lib/core/state.js +2 -1
- package/lib/core/text-detect.js +24 -4
- package/lib/core/tool-catalog.js +3 -0
- package/lib/index.js +101 -13
- package/lib/service.js +5 -2
- package/package.json +60 -58
- package/scripts/audit-mount-consistency.mjs +198 -198
- package/scripts/check-tool-coverage.mjs +63 -41
- package/scripts/lib/pnpm-exempt.mjs +56 -0
- package/scripts/local-residue-scan.mjs +40 -0
- package/scripts/publish-aptitude-check.mjs +102 -144
- package/scripts/readme-version-check.mjs +41 -0
- package/scripts/release-plugin.mjs +371 -329
- package/scripts/verify-all.mjs +85 -2
package/lib/core/state.js
CHANGED
|
@@ -247,6 +247,7 @@ export function freshTurn() {
|
|
|
247
247
|
realUserSeen: false, // v0.5.7:本回合是否有真实用户消息(注入轮不检测的依据)
|
|
248
248
|
askSeen: false,
|
|
249
249
|
askRejected: false, // 本回合 ask 已被拒(防连环 ask,弹窗消减)
|
|
250
|
+
askApproved: false, // 2026-09-06 M7 修复:本回合已获 ask 授权答复(M7 approval-gap 豁免信号)
|
|
250
251
|
questionOnly: false,
|
|
251
252
|
intents: null,
|
|
252
253
|
intentState: "lexicon", // lexicon | llm-pending | llm-ready(LLM 意图兜底状态)
|
|
@@ -265,7 +266,7 @@ export function freshTurn() {
|
|
|
265
266
|
reasoningText: "",
|
|
266
267
|
// 规则 22 粒度升级(2026-08-24):本回合已获授权范围(execute 子句 + ask 授权)
|
|
267
268
|
scopes: [],
|
|
268
|
-
// M8 双通道机制(2026-08-24
|
|
269
|
+
// M8 双通道机制(2026-08-24):统一入口落盘后同轮 engram_store 校验(entryMarker 经配置)
|
|
269
270
|
manualWriteSeen: false,
|
|
270
271
|
engramStoreSeen: false
|
|
271
272
|
};
|
package/lib/core/text-detect.js
CHANGED
|
@@ -35,6 +35,15 @@ export const CRITICISM_WEAK_RE =
|
|
|
35
35
|
|
|
36
36
|
export const DELIVERY_RE = /(?:已完成|全部(?:[^\s,。;!?]{0,12})完成|已[^\s,。;!?]{0,8}完成|修复完成|落盘完成|验证[^\s,。;!?]{0,6}(?:通过|成功)|全部通过|全部[^\s,。;!?]{0,10}通过|已通过|已修复|搞定)(?:\s*了|!|!|,[^。]*)?(?![^。]*(?:尚未|没有|未|没|还没|未完|待做|待完成))/;
|
|
37
37
|
|
|
38
|
+
// 2026-09-06 0.6.x(tsk_527d222c 第一期):路径缩写 B 级检测——回复含缩写路径(reports\ / ~/.dsh / %USERPROFILE%)
|
|
39
|
+
// 且非完整盘符形态 → 提醒写完整绝对路径(规则 14⑤ 机器化第一层;WEAK 形态:留痕+注入,不拦截)
|
|
40
|
+
const PATH_ABBREV_RE = /(?:^|[\s"'“”((])(?:reports|~\/?\.dsh|%USERPROFILE%)[\\/]/i;
|
|
41
|
+
const ABS_PATH_RE = /[A-Za-z]:[\\/][^\n]*/g;
|
|
42
|
+
export function hasPathAbbrev(text) {
|
|
43
|
+
if (typeof text !== "string") return false;
|
|
44
|
+
return PATH_ABBREV_RE.test(String(text).replace(ABS_PATH_RE, ""));
|
|
45
|
+
}
|
|
46
|
+
|
|
38
47
|
/** 从 assistant message 内容中提取纯文本 */
|
|
39
48
|
export function extractAssistantText(message) {
|
|
40
49
|
if (!message) return "";
|
|
@@ -144,7 +153,7 @@ export function detectTimeRule(session, text, timeCfg) {
|
|
|
144
153
|
* @param {string} options.text assistant 纯文本
|
|
145
154
|
* @returns {Array<{ruleId:string,title:string,kind:string,reason:string}>}
|
|
146
155
|
*/
|
|
147
|
-
export function detectViolations({ configs, session, text, reasoningText = "", mountRevision = 0 }) {
|
|
156
|
+
export function detectViolations({ configs, session, text, reasoningText = "", mountRevision = 0, rule5Window = 3 }) {
|
|
148
157
|
const hits = [];
|
|
149
158
|
const byId = new Map(configs.filter((c) => c.confidence !== "low").map((c) => [String(c.ruleId), c]));
|
|
150
159
|
|
|
@@ -184,6 +193,16 @@ export function detectViolations({ configs, session, text, reasoningText = "", m
|
|
|
184
193
|
});
|
|
185
194
|
}
|
|
186
195
|
|
|
196
|
+
// 2026-09-06 0.6.x(tsk_527d222c 第一期):路径缩写 B 级(规则 14⑤ 机器化——WEAK,留痕+注入不拦)
|
|
197
|
+
if (hasPathAbbrev(text)) {
|
|
198
|
+
hits.push({
|
|
199
|
+
ruleId: "14",
|
|
200
|
+
title: "汇报规范(完整文件位置路径)",
|
|
201
|
+
kind: "correct",
|
|
202
|
+
reason: "回复含缩写路径(reports\\ / ~/.dsh / %USERPROFILE% 等)——给用户应写完整盘符绝对路径(规则 14⑤)"
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
187
206
|
const sourceCfg = byId.get("5");
|
|
188
207
|
if (sourceCfg && !SOURCE_MARK.test(text)) {
|
|
189
208
|
if (URL_RE.test(text)) {
|
|
@@ -195,15 +214,16 @@ export function detectViolations({ configs, session, text, reasoningText = "", m
|
|
|
195
214
|
});
|
|
196
215
|
} else if (INTERNAL_REF_RE.test(text) && !isQuoteOrParaphraseContext(text, INTERNAL_REF_RE)) {
|
|
197
216
|
// 规则 5 扩展(2026-09-01 用户拍板):内部文档引用(手册/踩坑/条款/源码…)须有依据——
|
|
198
|
-
// 近 3
|
|
217
|
+
// 近 rule5Window 回合(默认 3;配置层 rule-engine.json `rule5SourceWindow` 可覆盖,2026-09-03
|
|
218
|
+
// 通用化修正:本机偏好走配置、通用默认保持 3)无对应 read/grep 时提示(B 级:留痕+注入,不拦截)。
|
|
199
219
|
const curTurn = session.turn.number || 0;
|
|
200
220
|
const lastQuery = session.lastQueryTurn ?? -1;
|
|
201
|
-
if (lastQuery < 0 || curTurn - lastQuery >
|
|
221
|
+
if (lastQuery < 0 || curTurn - lastQuery > rule5Window) {
|
|
202
222
|
hits.push({
|
|
203
223
|
ruleId: "5",
|
|
204
224
|
title: sourceCfg.title,
|
|
205
225
|
kind: "correct",
|
|
206
|
-
reason:
|
|
226
|
+
reason: `回答引用内部文档(手册/踩坑/条款/源码等)但近 ${rule5Window} 回合无对应 read/grep——请标注手册位置或删去断言`
|
|
207
227
|
});
|
|
208
228
|
}
|
|
209
229
|
}
|
package/lib/core/tool-catalog.js
CHANGED
|
@@ -26,6 +26,9 @@ const ANALYSIS_TOOLS = new Set([
|
|
|
26
26
|
// 子代理交付工具(官方 dsh-tool-subagent-report,2026-08-24 实测发现):仅子代理环境注册,
|
|
27
27
|
// 参数仅 output 文本 → 直接交付父代理,无文件副作用 → analysis(避免子代理交付被 unknown deny 阻断)
|
|
28
28
|
"report",
|
|
29
|
+
// 官方子代理模型发现(@deepseek-ai/dsh-tool-subagent,2026-09-06 工具目录漂移补录):只读枚举
|
|
30
|
+
// 可用子代理模型(模型发现/所选路由校验),无持久副作用 → analysis
|
|
31
|
+
"list_subagent_models",
|
|
29
32
|
// 官方 0.5.9 补全(tool-catalog 镜像 2026-08-27 交叉)
|
|
30
33
|
"lsp",
|
|
31
34
|
"session_event_read", "session_event_search", "session_event_trace", "session_search", "session_trace",
|
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
|
|
|
@@ -434,15 +475,18 @@ export function handleSessionEvent(ctx, session, event) {
|
|
|
434
475
|
return;
|
|
435
476
|
}
|
|
436
477
|
if (event.type === "turn/end") {
|
|
437
|
-
// M8
|
|
478
|
+
// M8 双通道机制:本回合用统一入口落盘手册/AGENTS 后,必须同轮 engram_store 沉淀记忆;
|
|
438
479
|
// 缺失 → 审计 + 注入纠正(用户明确要求机制化,2026-08-24)
|
|
439
|
-
|
|
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) {
|
|
440
484
|
audit({
|
|
441
485
|
kind: "engram-gap",
|
|
442
486
|
rule: "__engram-gap",
|
|
443
487
|
name: "双通道记忆缺失",
|
|
444
488
|
event: "turn/end",
|
|
445
|
-
reason: "
|
|
489
|
+
reason: "本回合经本机配置的统一入口落盘了手册/AGENTS,但未同轮调用 engram_store;按规则 19/77/M8 应在同一回合完成记忆沉淀",
|
|
446
490
|
session: sid
|
|
447
491
|
});
|
|
448
492
|
maybeInject(ctx, sid, {
|
|
@@ -519,7 +563,7 @@ export function handleSessionEvent(ctx, session, event) {
|
|
|
519
563
|
if (!text) return;
|
|
520
564
|
// 引擎自身注入的 [规则引擎] 提示不是用户消息,跳过——否则其中的“请直接执行”会被记录成授权(自我续授权漏洞)
|
|
521
565
|
if (isEngineInjectedMessage(text)) return;
|
|
522
|
-
// 模板兜底(对 kind=user 同样生效,防"标 user 的注入"——
|
|
566
|
+
// 模板兜底(对 kind=user 同样生效,防"标 user 的注入"——example-injector createUserMessage 同类风险):
|
|
523
567
|
// runtime context 快照 / 子代理完成通知 / vision-router 挂载提醒等已知注入模板 → 整体跳过
|
|
524
568
|
if (isKnownSystemInjection(text)) {
|
|
525
569
|
audit({
|
|
@@ -659,7 +703,7 @@ export function handleSessionEvent(ctx, session, event) {
|
|
|
659
703
|
pendingCall.backupPaths.push(bpTool);
|
|
660
704
|
}
|
|
661
705
|
}
|
|
662
|
-
if (isManualReadTool(toolName, args)) s.manualReadSeen = true;
|
|
706
|
+
if (isManualReadTool(toolName, args, state.localIntegrations?.manualExempt?.paths || [])) s.manualReadSeen = true;
|
|
663
707
|
// 规则 5/31 扩展(2026-09-01):查询类工具调用记下回合号("近 3 回合有据"判定)
|
|
664
708
|
if (isReadOnlyTool(toolName, args)) s.lastQueryTurn = s.turn.number;
|
|
665
709
|
if (toolName === "skill" && args?.name) s.turn.skillNames.push(args.name);
|
|
@@ -772,10 +816,13 @@ export function handleSessionEvent(ctx, session, event) {
|
|
|
772
816
|
}
|
|
773
817
|
}
|
|
774
818
|
|
|
775
|
-
// M8 双通道机制(2026-08-24
|
|
819
|
+
// M8 双通道机制(2026-08-24):统一入口落盘成功 → 标记;同轮 engram_store 成功 → 标记
|
|
820
|
+
// A3-1(0.6.0):默认关闭 → 显式 enabled:true + 配置 entryMarker 才生效(无配置 = M8 机制整体不存在)
|
|
776
821
|
if (!isError && pendingCall) {
|
|
777
822
|
const cmd = pendingCall.args?.command || pendingCall.args?.code || "";
|
|
778
|
-
|
|
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)) {
|
|
779
826
|
s.turn.manualWriteSeen = true;
|
|
780
827
|
}
|
|
781
828
|
if (pendingCall.name === "engram_store") {
|
|
@@ -924,6 +971,7 @@ export function handleSessionEvent(ctx, session, event) {
|
|
|
924
971
|
const result = d.result ?? d.value ?? d;
|
|
925
972
|
if (askResultApproved(result)) {
|
|
926
973
|
s.turn.realUserSeen = true; // v0.5.7:ask 弹窗答复也算"真实用户在场"
|
|
974
|
+
s.turn.askApproved = true; // 2026-09-06 M7 修复:ask 授权答复 = 本回合已授权(M7 approval-gap 豁免)
|
|
927
975
|
const qText = askQuestionText(pending.questions);
|
|
928
976
|
const selectedText = askResultSelectedText(result);
|
|
929
977
|
// 授权范围只取“问题核心 + 用户实际选择”,不再把全部选项描述纳入;
|
|
@@ -1036,7 +1084,7 @@ export function handleSessionEvent(ctx, session, event) {
|
|
|
1036
1084
|
rule: "23",
|
|
1037
1085
|
name: "交付声明缺验证闸门记录",
|
|
1038
1086
|
event: "assistant/message",
|
|
1039
|
-
reason: "交付/完成类声明缺少同会话近期 verify-pass
|
|
1087
|
+
reason: "交付/完成类声明缺少同会话近期 verify-pass(测试全绿或冷加载探针 RESULT: PASS)记录",
|
|
1040
1088
|
session: sid
|
|
1041
1089
|
});
|
|
1042
1090
|
// v0.5.7 后续(用户拍板"暗示型统一裁决"):23④ 词面命中只是嫌疑——"完成"≠交付声明
|
|
@@ -1081,7 +1129,7 @@ export function handleSessionEvent(ctx, session, event) {
|
|
|
1081
1129
|
}
|
|
1082
1130
|
}
|
|
1083
1131
|
// F1(2026-08-28 阶段三):规则 2 违规不在此时投递——标记 pendingRule2,turn/end 复核(Get-Date 定案)①
|
|
1084
|
-
let violations = detectViolations({ configs: state.configs, session: s, text, reasoningText: s.turn.reasoningText, mountRevision: state.mountRevision });
|
|
1132
|
+
let violations = detectViolations({ configs: state.configs, session: s, text, reasoningText: s.turn.reasoningText, mountRevision: state.mountRevision, rule5Window: pluginConfig.rule5SourceWindow ?? 3 });
|
|
1085
1133
|
const rule2s = violations.filter((v) => v.ruleId === "2");
|
|
1086
1134
|
for (const v of rule2s) {
|
|
1087
1135
|
if (!s.turn.pendingRule2) s.turn.pendingRule2 = v.reason;
|
|
@@ -1148,7 +1196,8 @@ const COMMAND_SPECS = [
|
|
|
1148
1196
|
{ name: "budget", args: "...", desc: "设置预算(agents=N files=... deps=allow hash=allow)" },
|
|
1149
1197
|
{ name: "contract", args: "", desc: "查看当前任务契约" },
|
|
1150
1198
|
{ name: "contract categories", args: "...", desc: "设定契约类别白名单(0.5.12)" },
|
|
1151
|
-
{ name: "label", args: "<id> <label>", desc: "给审计记录打标(correct/incorrect/inconclusive)" }
|
|
1199
|
+
{ name: "label", args: "<id> <label>", desc: "给审计记录打标(correct/incorrect/inconclusive)" },
|
|
1200
|
+
{ name: "approve", args: "<type> <路径> [min]", desc: "物理确认:授予指定类型+路径的临时授权(仅用户输入;默认 10 分钟)" }
|
|
1152
1201
|
];
|
|
1153
1202
|
|
|
1154
1203
|
const USAGE = [
|
|
@@ -1204,6 +1253,17 @@ function parseCommand(rawInput) {
|
|
|
1204
1253
|
if (m) return { kind: "label", eventId: m[1], label: m[2].toLowerCase() };
|
|
1205
1254
|
m = text.match(/^label\s+clear\s+(.+)$/i);
|
|
1206
1255
|
if (m) return { kind: "label-clear", fingerprint: m[1].trim() };
|
|
1256
|
+
// C4:/guard approve <type> <路径> [min]——物理确认(类型在 executeGuard 校验枚举)
|
|
1257
|
+
m = text.match(/^approve\s+([A-Za-z_][A-Za-z0-9_-]*)\s*("(?:[^"]*)"|'(?:[^']*)'|\S+)(?:\s+(\d+))?$/i);
|
|
1258
|
+
if (m) {
|
|
1259
|
+
const p = m[2];
|
|
1260
|
+
return {
|
|
1261
|
+
kind: "approve",
|
|
1262
|
+
type: m[1].toLowerCase(),
|
|
1263
|
+
path: p.replace(/^["']|["']$/g, ""),
|
|
1264
|
+
minutes: m[3] ? Number(m[3]) : 10
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1207
1267
|
return { kind: "invalid" };
|
|
1208
1268
|
}
|
|
1209
1269
|
|
|
@@ -1281,6 +1341,30 @@ async function executeGuard(ctx, invocation) {
|
|
|
1281
1341
|
text: `已临时放行全部守卫 ${minutes} 分钟。到期自动恢复,也可 /guard reload 后立即恢复。`
|
|
1282
1342
|
};
|
|
1283
1343
|
}
|
|
1344
|
+
case "approve": {
|
|
1345
|
+
// C4(2026-09-03)物理确认:用户亲手输入=词表无法误读;最小范围(类型+路径+时长);无全局通配
|
|
1346
|
+
if (!loadPluginConfig().approveEnabled) {
|
|
1347
|
+
return { kind: "error", text: "/guard approve 未开启:请在 rule-engine.json 设 approveEnabled=true(设置页开关随后续版本)后使用" };
|
|
1348
|
+
}
|
|
1349
|
+
if (!APPROVE_TYPES().includes(command.type)) {
|
|
1350
|
+
return { kind: "error", text: `不支持的类型 ${command.type}(可用:${APPROVE_TYPES().join("/")};any=全局通配被禁止——物理确认必须最小范围)` };
|
|
1351
|
+
}
|
|
1352
|
+
if (!command.path) return { kind: "error", text: "路径缺失:/guard approve <type> <路径> [min](建议路径用双引号包裹,如 /guard approve write \"D:\\...\\file.json\" 10)" };
|
|
1353
|
+
const minutes = Math.min(Math.max(1, command.minutes), 720);
|
|
1354
|
+
const pfx = normalizePath(command.path);
|
|
1355
|
+
const sid = sessionIdOfInvocation(invocation);
|
|
1356
|
+
recordAuthorization(state, sid, {
|
|
1357
|
+
type: command.type,
|
|
1358
|
+
pathPrefix: pfx,
|
|
1359
|
+
source: "physical-confirm",
|
|
1360
|
+
expiresAt: Date.now() + minutes * 60000
|
|
1361
|
+
});
|
|
1362
|
+
audit({ kind: "guard-command", rule: "__physical-confirm", name: "物理确认授权", event: "command", reason: `/guard approve ${command.type} ${pfx} ${minutes}m`, session: sid });
|
|
1363
|
+
return {
|
|
1364
|
+
kind: "success",
|
|
1365
|
+
text: `已物理确认授权:${command.type}|${pfx}|${minutes} 分钟。仅该类型+该路径(含子路径)生效;/guard revoke 可立即撤销;到期自动失效。`
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1284
1368
|
case "lock": {
|
|
1285
1369
|
state.unlockUntil = 0;
|
|
1286
1370
|
state.bypassUntil = 0;
|
|
@@ -1554,9 +1638,13 @@ export function apply(ctx) {
|
|
|
1554
1638
|
//(调整/补充/评估/建议且无落盘词)→ approval-gap 审计 + 注入提醒(规则 22 自证③:方案性指令 ≠ 落盘授权)
|
|
1555
1639
|
{
|
|
1556
1640
|
const cmd = exec?.arguments?.command || exec?.arguments?.code || "";
|
|
1557
|
-
|
|
1641
|
+
const entryScript = state.localIntegrations?.entryScript;
|
|
1642
|
+
// A3-2(0.6.0):入口脚本名从配置读取并转义;无配置不走本分支。
|
|
1643
|
+
// 边界裁决(v1.3 关键裁决 4):正则保持无尾部 \b(保守匹配,xxx.mjs.bak 等变体同样命中)。
|
|
1644
|
+
if ((exec?.name === "pwsh" || exec?.name === "bash") && entryScript && new RegExp(`\\b${entryScript.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`).test(cmd)) {
|
|
1558
1645
|
const s7 = getSessionState(state, sessionIdOfExec(exec));
|
|
1559
|
-
|
|
1646
|
+
// 2026-09-06 M7 修复:ask 授权答复(turn.askApproved)→ 豁免 approval-gap(12A 授权链已由 ask 建立)
|
|
1647
|
+
if (needsApprovalReminder(s7.turn.userText || "", { askApproved: !!s7.turn.askApproved })) {
|
|
1560
1648
|
audit({
|
|
1561
1649
|
kind: "approval-gap",
|
|
1562
1650
|
rule: "__approval-gap",
|
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.
|
|
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
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"
|
|
23
|
-
"dsh
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
"
|
|
32
|
-
|
|
33
|
-
"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-rule-engine",
|
|
3
|
+
"version": "0.6.1",
|
|
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
|
+
}
|