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
|
@@ -51,12 +51,63 @@ const TYPE_HINTS = [
|
|
|
51
51
|
// 只读/审查类(置末位,2026-08-25 RB-05):不覆盖在前类别的变更语义(“修改并审查”仍按 write/delete),
|
|
52
52
|
// 仅当无其它类型命中时给只读类型——配合 ACTION_RE 只读词(审查/阅读/验证等),使“审查文件”类执行分点
|
|
53
53
|
// 授权类型为 analysis(只读),不再落入 any 宽泛授权。
|
|
54
|
-
{ re: /审查|阅读|查看|核对|检查|审阅|复核|查阅|验证|对照|评估|分析|研究|排查|核实|梳理|调查|诊断|读取|读(?:出|一下|一遍|完)?|展示|打开|提取/i, type: "analysis" }
|
|
54
|
+
{ re: /审查|阅读|查看|核对|检查|审阅|复核|查阅|验证|对照|评估|分析|研究|排查|核实|梳理|调查|诊断|读取|读(?:出|一下|一遍|完)?|展示|打开|提取/i, type: "analysis" },
|
|
55
|
+
// C4(2026-09-04):archive 独立类型(解压/归档高频用例;物理确认最小范围)
|
|
56
|
+
{ re: /解压|解包|解压缩|归档|unzip|expand-archive|extract-archive|tar\s+-x|7z\s+x/i, type: "archive" }
|
|
55
57
|
];
|
|
56
58
|
|
|
59
|
+
// ── C4(2026-09-04):类型提示配置化 + 物理确认类型枚举 ──
|
|
60
|
+
// typeHintsOverride = null → 内置表;非空 → 完全替换(clear 语义);空 → 还原内置。
|
|
61
|
+
let typeHintsOverride = null;
|
|
62
|
+
|
|
63
|
+
function isValidHint(h) {
|
|
64
|
+
if (!h || typeof h.type !== "string" || h.type.length === 0 || h.type === "any") return false;
|
|
65
|
+
if (typeof h.re !== "string") return false;
|
|
66
|
+
try {
|
|
67
|
+
new RegExp(h.re, "i");
|
|
68
|
+
return true;
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function normalizeHints(list) {
|
|
75
|
+
const out = [];
|
|
76
|
+
for (const h of list) {
|
|
77
|
+
if (!isValidHint(h)) continue;
|
|
78
|
+
if (!out.some((x) => x.type === h.type)) out.push({ type: h.type, re: new RegExp(h.re, "i") });
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** C4:配置层扩展/替换授权类型提示;空列表 = 还原内置;{ clear: true } = 完全替换(不含内置) */
|
|
84
|
+
export function setTypeHints(hints, opts = {}) {
|
|
85
|
+
const list = Array.isArray(hints) ? hints : [];
|
|
86
|
+
if (list.length === 0) {
|
|
87
|
+
typeHintsOverride = null;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const normalized = normalizeHints(list);
|
|
91
|
+
typeHintsOverride = opts?.clear ? normalized : [...TYPE_HINTS, ...normalized];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** C4:当前生效的类型枚举(内置 9 类 + 配置扩展;any=全局通配被禁止,不枚举) */
|
|
95
|
+
export function APPROVE_TYPES() {
|
|
96
|
+
const hints = typeHintsOverride ?? TYPE_HINTS;
|
|
97
|
+
const types = [];
|
|
98
|
+
for (const h of hints) {
|
|
99
|
+
if (h && typeof h.type === "string" && h.type !== "any" && !types.includes(h.type)) types.push(h.type);
|
|
100
|
+
}
|
|
101
|
+
return types;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function currentHints() {
|
|
105
|
+
return typeHintsOverride ?? TYPE_HINTS;
|
|
106
|
+
}
|
|
107
|
+
|
|
57
108
|
// C2(2026-08-28 阶段二):`、`(中文顿号)从非路径字符黑名单移除——Windows 中文目录名
|
|
58
|
-
// 常见顿号("D:\1
|
|
59
|
-
// 真实操作路径匹配失败(ERR-LU50QQ:已授权范围 [write d:/1] 拦 d:/1
|
|
109
|
+
// 常见顿号("D:\1、示例\..."——用户工作目录),原正则在此截断 → 授权记录成 "d:/1",
|
|
110
|
+
// 真实操作路径匹配失败(ERR-LU50QQ:已授权范围 [write d:/1] 拦 d:/1、示例/练习/...)。
|
|
60
111
|
// 保守边界:顿号后跟空格+新词时可能合并(fail-closed——提取过长 → 授权匹配失败 → 拦而非放),
|
|
61
112
|
// 不放大授权;引号包裹分支本就不受此影响。
|
|
62
113
|
const PATH_TOKEN_RE = /["'][A-Za-z]:[\\/](?!\/)[^"']+["']|[A-Za-z]:[\\/](?!\/)[^\s'"`,。;:!?()【】《》]+|(?:^|[\s"'`])\/(?:[^\s"'`,。;:!?()【】《》、]+)/g;
|
|
@@ -69,7 +120,7 @@ export function normalizePath(p) {
|
|
|
69
120
|
/** 从文本推断操作类型(取第一个命中) */
|
|
70
121
|
export function inferTypeFromText(text) {
|
|
71
122
|
const s = String(text || "");
|
|
72
|
-
for (const hint of
|
|
123
|
+
for (const hint of currentHints()) {
|
|
73
124
|
if (hint.re.test(s)) return hint.type;
|
|
74
125
|
}
|
|
75
126
|
return "any";
|
|
@@ -78,21 +129,25 @@ export function inferTypeFromText(text) {
|
|
|
78
129
|
/** 0.5.10 建议1①(用户审查采纳):ask 答复文本 → 结构化操作类型(修复"弹窗授权宽泛 any 接不住真操作")。
|
|
79
130
|
* 返回 write | command | any(analysis/artifact 类本就不需授权,不落入授权记录)。
|
|
80
131
|
* 明确操作词 → 具体类型;含糊/纯查看 → any(保守:TTL 短兜底)。 */
|
|
132
|
+
/** ask 授权类型推导(E3/簇 A 配置化):接入 TYPE_HINTS(内置表含 git 类)+ 配置层 typeHints 扩展共同生效。
|
|
133
|
+
* 多类型命中按具体类优先序取(delete/backup/git/network/archive/skill > write > command > analysis);无命中 → any(保守)。
|
|
134
|
+
* 原硬编码 write/command 两正则废弃——与 hints 表漂移("提交/覆盖"等中文词误映射)的风险源。 */
|
|
135
|
+
const ASK_SCOPE_PRIORITY = ["delete", "backup", "git", "archive", "skill", "write", "command", "network", "analysis"];
|
|
81
136
|
export function classifyAskScopeType(text) {
|
|
82
137
|
const s = String(text || "");
|
|
83
|
-
const
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
if (
|
|
87
|
-
|
|
88
|
-
return
|
|
138
|
+
const hints = currentHints();
|
|
139
|
+
const hits = [];
|
|
140
|
+
for (const h of hints) if (h.re.test(s)) hits.push(h.type);
|
|
141
|
+
if (hits.length === 0) return "any";
|
|
142
|
+
for (const t of ASK_SCOPE_PRIORITY) if (hits.includes(t)) return t;
|
|
143
|
+
return hits[0];
|
|
89
144
|
}
|
|
90
145
|
|
|
91
146
|
/** 从文本推断全部命中的操作类型(用于一条子句含多个操作,如“修改 A 并删除 B”) */
|
|
92
147
|
export function inferTypesFromText(text) {
|
|
93
148
|
const s = String(text || "");
|
|
94
149
|
const types = [];
|
|
95
|
-
for (const hint of
|
|
150
|
+
for (const hint of currentHints()) {
|
|
96
151
|
if (hint.re.test(s)) types.push(hint.type);
|
|
97
152
|
}
|
|
98
153
|
return types.length ? [...new Set(types)] : ["any"];
|
|
@@ -101,8 +156,8 @@ export function inferTypesFromText(text) {
|
|
|
101
156
|
/**
|
|
102
157
|
* 从文本提取路径候选(批次 4 统一入口:pairActionScopes 与 inferPathPrefixesFromText 共用)。
|
|
103
158
|
* 处理两类噪声:
|
|
104
|
-
* 1. 前缀冗余:引号路径同时命中两分支 → 截断前缀(如 "d:/
|
|
105
|
-
* 2. 截断可疑:裸含空格路径(无引号)只能提取到空格前的半截("D
|
|
159
|
+
* 1. 前缀冗余:引号路径同时命中两分支 → 截断前缀(如 "d:/example")被更长匹配覆盖 → 丢弃;
|
|
160
|
+
* 2. 截断可疑:裸含空格路径(无引号)只能提取到空格前的半截("D:\\workspace\docs\..." → "d:/workspace")
|
|
106
161
|
* ——匹配后紧跟空白且后随 token 是路径字符开头且非盘符/引号 → 判定为截断 → 丢弃(fail-closed:
|
|
107
162
|
* 宁缺授权,拦截后请用户给完整/引号路径,也不把半截前缀当成授权)。
|
|
108
163
|
* @returns {Array<{index:number,path:string}>} 去噪后的候选(保留原文索引供就近配对)
|
package/lib/core/contract.js
CHANGED
|
@@ -38,7 +38,7 @@ export function categoryOfCommand(command) {
|
|
|
38
38
|
if (/remove-item|\brm(?:dir)?\b|del(?:ete)?\b|move-item|\bmv\b|rename-item|\bren\b|clear-content|clean|purge|rm\s+-/.test(s)) return "delete";
|
|
39
39
|
if (/install|add\s+--save|pnpm\s+add|npm\s+install|yarn\s+add/.test(s)) return "install";
|
|
40
40
|
if (/build|tsc|tsdown|vite\s+build|webpack|rollup|compile|bundle/.test(s)) return "build";
|
|
41
|
-
if (/test|jest|vitest|playwright|npm\s+test|pnpm\s+test|
|
|
41
|
+
if (/test|jest|vitest|playwright|npm\s+test|pnpm\s+test|verify/.test(s)) return "test";
|
|
42
42
|
if (/audit|mount|consistency|dump-config|inspect/.test(s)) return "audit";
|
|
43
43
|
if (/analyze|analysis|review|read|grep|search/.test(s)) return "analyze";
|
|
44
44
|
return null;
|
package/lib/core/guard-core.js
CHANGED
|
@@ -8,9 +8,7 @@ import {
|
|
|
8
8
|
DESTRUCTIVE_CMD,
|
|
9
9
|
DSH_KEYWORDS_RE,
|
|
10
10
|
INLINE_CMD,
|
|
11
|
-
MANUAL_PATH_RE,
|
|
12
11
|
PROTECTED_FILENAME_RE,
|
|
13
|
-
SKILL_EXEMPT,
|
|
14
12
|
commandText,
|
|
15
13
|
isAssemblyMutationTool,
|
|
16
14
|
isBackupTool,
|
|
@@ -27,6 +25,7 @@ import {
|
|
|
27
25
|
isAnalysisOp,
|
|
28
26
|
isAnalysisScratchPath,
|
|
29
27
|
extractAnalysisScratchPaths,
|
|
28
|
+
matchManualPath,
|
|
30
29
|
pathTarget
|
|
31
30
|
} from "./patterns.js";
|
|
32
31
|
import { authMatches, describeAuth, describeOp, describeScopes, findMatchingAuth, operationOf, askQuestionText, inferPathPrefixFromText, inferTypeFromText, scopesFromIntents } from "./authorization.js";
|
|
@@ -75,11 +74,11 @@ const RULE_HINTS = {
|
|
|
75
74
|
"9": "改用脚本文件或显式 UTF-8 BOM 流程",
|
|
76
75
|
"12A": "先 ask_user_question 获取匹配授权",
|
|
77
76
|
"13A": "先对目标路径执行备份(复制到 .bak/.backups/trash-)",
|
|
78
|
-
"18": "
|
|
77
|
+
"18": "先读取本机配置的手册(路径经 localIntegrations 配置;未配置则该规则无对象)",
|
|
79
78
|
"21": "按规则 21 分级确认后再落盘",
|
|
80
79
|
"22": "先回答/展示方案,或补充明确执行分点(工具类别+路径范围);已授权变更必须落在本回合执行分点/ask 授权范围内",
|
|
81
80
|
"24": "确认插件 dsh.bundle 类型或改用正确挂载",
|
|
82
|
-
"27": "先运行 node scripts/audit-mount-consistency.mjs --profile
|
|
81
|
+
"27": "先运行 node scripts/audit-mount-consistency.mjs --profile <p>"
|
|
83
82
|
};
|
|
84
83
|
|
|
85
84
|
// 0.5.12(F3):拒绝来源前缀——机器可解析 token 化(/guard log 可按来源 grep 过滤)
|
|
@@ -128,9 +127,30 @@ export function isProfilePackageJson(p) {
|
|
|
128
127
|
return typeof p === "string" && /profiles[\\/][^\\/]+[\\/]package\.json$/i.test(p);
|
|
129
128
|
}
|
|
130
129
|
|
|
131
|
-
/** 判定命令是否"
|
|
132
|
-
|
|
133
|
-
|
|
130
|
+
/** 判定命令是否"整条命令仅调用统一入口脚本"(无 ; | & 链式/换行/重定向)
|
|
131
|
+
* N1(2026-09-04 P2):排除向文件的重定向(> / >> / &> / fd>file)——`入口 status x > AGENTS.md` 不再被豁免;
|
|
132
|
+
* 仅保留 fd→fd 复制(2>&1 / 1>&2)。
|
|
133
|
+
* G1(2026-09-04 P2):引号感知——引号内的换行/;|& 视为合法参数内容(上会话曾误拦合法多行参数)。
|
|
134
|
+
* entryMarker:统一入口脚本名(来自 localIntegrations.entryScript;无配置 = 不存在"统一入口"概念)。 */
|
|
135
|
+
function isEntryChannelCommand(cmd, entryMarker) {
|
|
136
|
+
if (typeof cmd !== "string") return false;
|
|
137
|
+
if (!entryMarker) return false; // 无配置 = 无对象,自然静默
|
|
138
|
+
const c = cmd.trim();
|
|
139
|
+
if (!c.includes(entryMarker)) return false;
|
|
140
|
+
// ① 链式分隔(引号感知):引号外的 ; | & 换行 → 非法
|
|
141
|
+
let inQ = null;
|
|
142
|
+
for (let i = 0; i < c.length; i++) {
|
|
143
|
+
const ch = c[i];
|
|
144
|
+
if (inQ) { if (ch === inQ) inQ = null; continue; }
|
|
145
|
+
if (ch === '"' || ch === "'") { inQ = ch; continue; }
|
|
146
|
+
if (ch === ";" || ch === "|" || ch === "&" || ch === "\n") return false;
|
|
147
|
+
}
|
|
148
|
+
// ② 重定向:摘除 fd 复制(N>&M / N<&M)后,任何 > / >> / &> 均非法(N1——含数字前置 2>/1> 盲区)
|
|
149
|
+
// 2026-09-06 0.6.x 修复:引号感知——参数值内的 >(如 markdown 引用符/⟨⟩ 占位)不再误判重定向
|
|
150
|
+
const noFd = c.replace(/\d*\s*[<>]&\s*\d+/g, "");
|
|
151
|
+
const noQuoted = noFd.replace(/"[^"]*"|'[^']*'/g, "");
|
|
152
|
+
if (/>>|&>|(?:^|[^<])>/.test(noQuoted)) return false;
|
|
153
|
+
return true;
|
|
134
154
|
}
|
|
135
155
|
|
|
136
156
|
/** 计算 edit/write/str_replace 后的目标文件内容;无法可靠计算时返回 null */
|
|
@@ -312,17 +332,21 @@ export function guardDecision(state, exec, now = Date.now(), opts = {}) {
|
|
|
312
332
|
}
|
|
313
333
|
|
|
314
334
|
// 阶段 C:硬拦 pwsh/bash 绕过统一入口直接写受保护文件(规则 19⑧/21⑨ 的机器执行层)
|
|
315
|
-
// 合法通道 =
|
|
316
|
-
// 0.
|
|
335
|
+
// 合法通道 = 整条命令仅调用统一入口脚本(无 ; | & 链式/换行,防注释文本伪造放行)
|
|
336
|
+
// A2-2(0.6.0):无 localIntegrations.entryScript 配置 = 守卫无对象(不激活);protectedFiles 为通用基线之上的本机追加清单
|
|
317
337
|
if (!unlock && (name === "pwsh" || name === "bash")) {
|
|
318
338
|
const cmd = commandText(args) || "";
|
|
319
|
-
|
|
320
|
-
|
|
339
|
+
const entryMarker = state.localIntegrations?.entryScript;
|
|
340
|
+
if (entryMarker && cmd && isMutationCommand(cmd) && (PROTECTED_FILENAME_RE.test(cmd) || matchManualPath(cmd, state.localIntegrations?.protectedFiles || []))) {
|
|
341
|
+
if (!isEntryChannelCommand(cmd, entryMarker)) {
|
|
321
342
|
return makeHit(
|
|
322
343
|
{ ruleId: "__self-protect", title: "受保护文件禁止绕过统一入口直写", action: "deny" },
|
|
323
|
-
`【硬拦截】受保护文件禁止通过 pwsh/bash 绕过统一入口直写;请使用
|
|
344
|
+
`【硬拦截】受保护文件禁止通过 pwsh/bash 绕过统一入口直写;请使用 ${entryMarker}(整个命令只能调用该脚本,不得链式拼接其他写命令/重定向;或 /guard unlock 临时放行)。`
|
|
324
345
|
);
|
|
325
346
|
}
|
|
347
|
+
} else if (!entryMarker && cmd && isMutationCommand(cmd) && (PROTECTED_FILENAME_RE.test(cmd) || matchManualPath(cmd, state.localIntegrations?.protectedFiles || []))) {
|
|
348
|
+
// A2-2(0.6.0):无 entryScript 配置 = 守卫无对象——登记 skipped 审计(验证"默认无"的留痕;不拦截)
|
|
349
|
+
opts?.audit?.({ kind: "li-skipped", rule: "19", name: "本地集成未配置(守卫无对象)", event: "tool/guard", reason: "skipped: no localIntegrations.entryScript——该守卫在此环境不存在(0.6.0 默认无)", session: sessionIdOf(exec) });
|
|
326
350
|
}
|
|
327
351
|
}
|
|
328
352
|
|
|
@@ -514,17 +538,17 @@ function matchRule(cfg, ctx) {
|
|
|
514
538
|
if (cfg.handler === "rule18-manual-first") {
|
|
515
539
|
const userText = session.turn.userText || session.lastUserText || "";
|
|
516
540
|
const firstTool = session.turn.toolCount === 0;
|
|
517
|
-
if (firstTool && !session.manualReadSeen && DSH_KEYWORDS_RE.test(userText) && !isManualReadTool(name, args)) {
|
|
518
|
-
return makeHit(cfg, "【硬拦截】任务涉及 DSH,首次工具调用前需先 grep/read
|
|
541
|
+
if (firstTool && !session.manualReadSeen && DSH_KEYWORDS_RE.test(userText) && !isManualReadTool(name, args, state.localIntegrations?.manualExempt?.paths || [])) {
|
|
542
|
+
return makeHit(cfg, "【硬拦截】任务涉及 DSH,首次工具调用前需先 grep/read 本机手册(路径经 localIntegrations 配置)");
|
|
519
543
|
}
|
|
520
544
|
return null;
|
|
521
545
|
}
|
|
522
546
|
|
|
523
547
|
// 规则 13A:删除/覆盖/高风险写前需有“目标路径对应备份”证据
|
|
524
548
|
if (cfg.handler === "rule13a-backup") {
|
|
525
|
-
//
|
|
549
|
+
// 统一入口命令豁免:入口脚本每次写入前自身执行备份(backup() 保留 5 份),
|
|
526
550
|
// 引擎静态扫描看不到脚本内部动作(已知盲区);入口命令也已被 __self-protect 限定为唯一写通道。
|
|
527
|
-
if ((name === "pwsh" || name === "bash") && isEntryChannelCommand(cmd)) return null;
|
|
551
|
+
if ((name === "pwsh" || name === "bash") && isEntryChannelCommand(cmd, state.localIntegrations?.entryScript)) return null;
|
|
528
552
|
const destructive = (name === "pwsh" || name === "bash") && cmd && (DESTRUCTIVE_CMD.test(cmd) || (isSensitiveToolCall(name, args, sessionIdOf(exec)) && !/git\s+(push|commit)/i.test(cmd)));
|
|
529
553
|
const highRiskWrite = (name === "edit" || name === "write" || (name === "str_replace_editor" && args?.command !== "view")) && isProtectedConfigPath(p);
|
|
530
554
|
if (highRiskWrite && unlock && !isHighRiskEntryFile(p)) return null;
|
|
@@ -567,7 +591,7 @@ function matchRule(cfg, ctx) {
|
|
|
567
591
|
if (cfg.handler === "rule12b-skill") {
|
|
568
592
|
if (name === "skill") {
|
|
569
593
|
const skillName = typeof args?.name === "string" ? args.name : "";
|
|
570
|
-
if (
|
|
594
|
+
if ((state.localIntegrations?.manualExempt?.skills || []).includes(skillName)) return null;
|
|
571
595
|
// 技能目录实时联动:已加载目录且该技能不存在/被禁用时,规则不激活
|
|
572
596
|
if (state.skillNames && state.skillNames.size > 0 && !state.skillNames.has(skillName)) return null;
|
|
573
597
|
if (denyMutation) {
|
|
@@ -587,7 +611,7 @@ function matchRule(cfg, ctx) {
|
|
|
587
611
|
if ((cfg.hints || []).includes("skill") && name === "skill") {
|
|
588
612
|
// hints 兜底:理解器未分配 handler 但 hints 含 skill 的 cfg——仅 skill 工具时参与,不截胡其它规则
|
|
589
613
|
const skillName = typeof args?.name === "string" ? args.name : "";
|
|
590
|
-
if (
|
|
614
|
+
if ((state.localIntegrations?.manualExempt?.skills || []).includes(skillName)) return null;
|
|
591
615
|
if (state.skillNames && state.skillNames.size > 0 && !state.skillNames.has(skillName)) return null;
|
|
592
616
|
const op = { type: "skill", pathPrefix: "" };
|
|
593
617
|
const auth = findMatchingAuth([...(session.authorizations || []), ...(state.globalAuthorizations || [])], op);
|
|
@@ -616,8 +640,8 @@ function matchRule(cfg, ctx) {
|
|
|
616
640
|
audit?.({ kind: "allow", rule: cfg.ruleId, name: "低风险新建豁免(12A 判据同源)", tool: name, reason: `工作区内低风险新建(12A 判据同源):${describeOp(operationOf(name, args))}`, session: sessionIdOf(exec) });
|
|
617
641
|
return null;
|
|
618
642
|
}
|
|
619
|
-
// 规则 19
|
|
620
|
-
if (p &&
|
|
643
|
+
// 规则 19:本机手册正文更新免逐次确认(仅手册本身;路径经配置)
|
|
644
|
+
if (p && (matchManualPath(p, state.localIntegrations?.manualExempt?.paths))) return null;
|
|
621
645
|
// /guard unlock 本身即用户对受保护配置的授权
|
|
622
646
|
if (unlock && isProtectedConfigPath(p)) return null;
|
|
623
647
|
const op = operationOf(name, args);
|
|
@@ -704,11 +728,11 @@ function matchRule(cfg, ctx) {
|
|
|
704
728
|
if (hints.includes("bom-write") && (name === "pwsh" || name === "bash") && cmd && BOM_WRITE.test(cmd)) {
|
|
705
729
|
return makeHit(cfg, `【硬拦截】${cfg.title}`);
|
|
706
730
|
}
|
|
707
|
-
if (hints.includes("manual") && session.turn.toolCount === 0 && !session.manualReadSeen && !isManualReadTool(name, args)) {
|
|
731
|
+
if (hints.includes("manual") && session.turn.toolCount === 0 && !session.manualReadSeen && !isManualReadTool(name, args, state.localIntegrations?.manualExempt?.paths || [])) {
|
|
708
732
|
return makeHit(cfg, `【硬拦截】${cfg.title}`);
|
|
709
733
|
}
|
|
710
734
|
if (hints.includes("sensitive") && isSensitiveToolCall(name, args, sessionIdOf(exec))) {
|
|
711
|
-
if (p &&
|
|
735
|
+
if (p && (matchManualPath(p, state.localIntegrations?.manualExempt?.paths))) return null;
|
|
712
736
|
if (unlock && isProtectedConfigPath(p)) return null;
|
|
713
737
|
const op = operationOf(name, args);
|
|
714
738
|
if (denyMutation) {
|