dsh-rule-engine 0.5.11 → 0.5.12

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.
@@ -44,7 +44,7 @@ const NUMBERED_ITEM_RE = /^\s*(?:\(?(\d+)[\)、..)]\s*)/;
44
44
  // A1(2026-08-28 阶段一):具体动作词(区别于"执行/开始/进行"等宽泛词)。
45
45
  // 用途:hasAction+hasPlan 时区分"给出执行方案"(方案请求)与"将建议书转化到业务通告"(执行指令)。
46
46
  const SPECIFIC_ACTION_RE =
47
- /写入|写(?:下|好|完|成|作|文件)?|保存|另存为|删除|修改|改(?:为|成|动|一下|进|善)?|补(?:上|齐|全|充|写)?|修(?:改|复|一下)?|做(?:好|完|一下)?|创建|安装|发布|下载|提交|运行|修复|替换|重建|启动|重启|停止|卸载|改进|优化|增强|完善|升级|迁移|整理|调整|实现|实施|添加|增加|补充|重写|改造|调试|排查|处理|解决|更新|部署|核实|梳理|诊断|对齐|跟进|落实|设计并实现|调查|产出|编写|撰写|生成|审查|阅读|查看|核对|检查|审阅|复核|查阅|验证|对照|调研|研究|转化|转换|读取|读(?:完|出|一下|一遍|出来)?|展示|打开|提取|还原|导入|导出|采纳|选择|选定|落盘|落地|清理|移除|清空/i;
47
+ /写入|写(?:下|好|完|成|作|文件)?|保存|另存为|删除|修改|改(?:为|成|动|一下|进|善)?|补(?:上|齐|全|充|写)?|修(?:改|复|一下)?|做(?:好|完|一下)?|创建|安装|发布|下载|提交|运行|修复|替换|重建|启动|重启|停止|卸载|改进|优化|增强|完善|升级|迁移|整理|调整|实现|实施|添加|增加|补充|重写|改造|调试|排查|处理|解决|更新|部署|核实|梳理|诊断|对齐|跟进|落实|设计并实现|调查|产出|编写|撰写|生成|审查|阅读|查看|核对|检查|审阅|复核|查阅|验证|对照|调研|研究|转化|转换|读取|读(?:完|出|一下|一遍|出来)?|展示|打开|提取|还原|导入|导出|采纳|选择|选定|落盘|落地|清理|移除|清空|合入|合并|并入|融合|整合|起草|开展|搭建|重构|兼容|投产|校验|核验|过一遍|跑一遍|复验/i;
48
48
 
49
49
  // A3(2026-08-28,阶段一):@文件引用信号——用户消息中 @ 引用路径/文件(DSH 官方
50
50
  // file-reference 语法,@path 或 @"path with spaces"/@file.docx),且含处理动词 → 视为执行分点。
@@ -0,0 +1,145 @@
1
+ // label-fingerprint.js - 打标命令指纹记忆(F2,0.5.12)
2
+ // 目标:`/guard label ERR-xxx incorrect` 后,同指纹命令直接放行并记 label-hits
3
+ // 审计;持久化到 ~/.dsh/rule-engine-labels.json。
4
+ // 安全规格(重证报告 §3.2 补充):
5
+ // - 开关/写参数(-Recurse/-Force 等)**必须保留**在指纹中——只归一化无害部分;
6
+ // - 破坏类命令(rm/move/Remove-Item/irm/iwr 等)**永不进指纹放行**;
7
+ // - 持久化带 TTL(默认 7 天)+ 可撤销(/guard label clear <fingerprint>)。
8
+ // 纯函数,可独立测试;不依赖 Cordis。
9
+ import { readFileSync, writeFileSync } from "node:fs";
10
+ import { normalizePath } from "./authorization.js";
11
+
12
+ /**
13
+ * 命令指纹归一化规则。
14
+ * 保留:命令名(首个 token 的小写规范形式)、开关类参数(-x/--xx)、位置参数中的"参数名前缀"(= 左侧)。
15
+ * 归一化(无害化):路径前缀(盘符/工作区路径)、时间戳、随机串、引号、大小写、空格压缩。
16
+ */
17
+ export function fingerprintCommand(command) {
18
+ const s = String(command || "").trim();
19
+ if (!s) return "";
20
+ // 只取主命令链的第一段(; | & 换行之后的副作用不在本命令指纹范围内——由守卫整体判定)
21
+ const head = s.split(/[;|&\r\n]/)[0].trim();
22
+ // 去除路径前缀(盘符路径/引号包裹路径 → 只保留最后一段 basename 与扩展名;目录变化不影响指纹)
23
+ const depathed = head
24
+ .replace(/(?:\\?['"]?[A-Za-z]:[\\/][^'"\s]*|['"][^'"]+|\\\/?) /g, " ")
25
+ .replace(/\b(path|pathprefix|workdir)\s*[=:]\s*(?:['"]?[^'"\s]+['"]?)/gi, "")
26
+ .replace(/[\\/](?:node_modules|projects|workspace)[\\/][^'"\s]*/gi, " ")
27
+ .replace(/[\\/]+/g, "/");
28
+ // 去除时间戳/随机串(8-16 位十六进制/数字串)
29
+ const denormalized = depathed.replace(/\b[0-9a-f]{8,16}\b/gi, "<TS>").replace(/\b\d{6,}\b/g, "<N>");
30
+ // 规范形式:小写 + 空白压缩
31
+ const canonical = denormalized
32
+ .toLowerCase()
33
+ .replace(/\s+/g, " ")
34
+ .replace(/\s*([=&])\s*/g, "$1")
35
+ .trim();
36
+ return canonical || "";
37
+ }
38
+
39
+ /** 是否破坏类命令(永不进指纹放行)——写/删除/移动/网络下载类 */
40
+ export function isDangerousCommand(command) {
41
+ const s = String(command || "").toLowerCase();
42
+ return /(?:remove-item|rm(?:dir)?\b|del(?:ete)?\b|move-item|move\b|copy-item|cp\b|mv\b|irm|iwr|\bcurl\b|\bwget\b|invoke-exp(?:ression)?|iex\b|new-item|set-content|add-content|out-file|rename-item|clear-content|git\s+(?:push|commit|reset|checkout|clean))/i.test(s);
43
+ }
44
+
45
+ /** 从命令文本提取可保留的"写/开关参数"(保留在指纹里,防止 -Recurse 被归一化放行) */
46
+ export function extractSignificantParams(command) {
47
+ const s = String(command || "");
48
+ // 开关:-x / --xx / -Force / -Recurse 等(含 = 值)
49
+ const flags = [];
50
+ const flagRe = /(?:^|\s)(--?[A-Za-z][\w-]*)(?:(?:=([^\s]+))|(\s+[^\s-][^\s]*\s*))?/g;
51
+ let m;
52
+ while ((m = flagRe.exec(s))) {
53
+ if (m[1]) flags.push(m[1].toLowerCase());
54
+ }
55
+ return flags;
56
+ }
57
+
58
+ /**
59
+ * 计算放行指纹 = 命令骨架 + 保留参数(开关原样 + 位置参数的"名称=值"对)。
60
+ * @returns {string} 指纹字符串(稳定、可比较);空串表示不可指纹化
61
+ */
62
+ export function labelFingerprint(command) {
63
+ const cmd = String(command || "").trim();
64
+ if (!cmd) return "";
65
+ if (isDangerousCommand(cmd)) return ""; // 危险命令永不进指纹(返回空 = 不可放行)
66
+ const canonical = fingerprintCommand(cmd);
67
+ if (!canonical) return "";
68
+ const params = extractSignificantParams(cmd).sort();
69
+ return `${canonical}|flags=${params.join(",")}`;
70
+ }
71
+
72
+ /** 打标台账记录结构 */
73
+ export function labelEntry(fingerprint, label, sid, now = Date.now(), ttlMs = 7 * 24 * 60 * 60 * 1000) {
74
+ return { fingerprint, label, session: sid || "global", at: now, expiresAt: now + ttlMs };
75
+ }
76
+
77
+ /** 从原始 JSON 解析台账(容错:损坏返回空数组 + 旧格式兼容) */
78
+ export function parseLabels(raw) {
79
+ try {
80
+ const arr = JSON.parse(raw || "[]");
81
+ if (!Array.isArray(arr)) return [];
82
+ return arr
83
+ .map((it) => {
84
+ if (typeof it === "string") return { fingerprint: it, label: "incorrect", session: null, at: null, expiresAt: null };
85
+ if (it && typeof it.fingerprint === "string") {
86
+ return {
87
+ fingerprint: it.fingerprint,
88
+ label: typeof it.label === "string" ? it.label : "incorrect",
89
+ session: typeof it.session === "string" ? it.session : null,
90
+ at: typeof it.at === "number" ? it.at : null,
91
+ expiresAt: typeof it.expiresAt === "number" ? it.expiresAt : null
92
+ };
93
+ }
94
+ return null;
95
+ })
96
+ .filter(Boolean);
97
+ } catch {
98
+ return [];
99
+ }
100
+ }
101
+
102
+ /** 持枪裁判:一份台账中某指纹是否命中"incorrect 放行"(且未过期) */
103
+ export function labelsAllowFingerprint(rows, fingerprint, now = Date.now()) {
104
+ if (!fingerprint) return false;
105
+ return rows.some((r) => r && r.fingerprint === fingerprint && r.label === "incorrect" && (!r.expiresAt || r.expiresAt > now));
106
+ }
107
+
108
+ /** 写台账(保留未过期 + 去重:同指纹同标签更新 at/expiresAt) */
109
+ export function serializeLabels(rows, now = Date.now()) {
110
+ const keep = rows.filter((r) => r && r.fingerprint && (!r.expiresAt || r.expiresAt > now));
111
+ // 同指纹同 label 只保留最新(去重)
112
+ const map = new Map();
113
+ for (const r of keep) map.set(`${r.fingerprint}|${r.label}`, r);
114
+ return JSON.stringify([...map.values()], null, 2);
115
+ }
116
+
117
+ /** 持久化路径(与 toolsWhitelistFilePath 同模式) */
118
+ export function labelsFilePath(dshHome) {
119
+ return `${dshHome}/rule-engine-labels.json`;
120
+ }
121
+
122
+ /** 从磁盘加载台账(纯函数输入输出:读取失败 → 空数组) */
123
+ export function loadLabelsFromDisk(filePath) {
124
+ try {
125
+ return parseLabels(readFileSync(filePath, "utf8"));
126
+ } catch {
127
+ return [];
128
+ }
129
+ }
130
+
131
+ /** 写磁盘(失败静默——审计链路不因台账读写失败而阻断) */
132
+ export function saveLabelsToDisk(filePath, rows) {
133
+ try {
134
+ writeFileSync(filePath, serializeLabels(rows), "utf8");
135
+ return true;
136
+ } catch {
137
+ return false;
138
+ }
139
+ }
140
+
141
+ /** 去重复存:同指纹同 label 记录只留最新一条(新增/更新均用) */
142
+ export function upsertLabel(rows, entry) {
143
+ const others = rows.filter((r) => !(r.fingerprint === entry.fingerprint && r.label === entry.label));
144
+ return [...others, entry];
145
+ }
@@ -1,72 +1,72 @@
1
- // lexicon.js - 规则 22 语义判定的唯一词表源(0.5.11,用户拍板"唯一词表源由引擎维护")。
2
- // 背景(2026-08-28 全量审查):正文曾手抄词表(L144/148),引擎内部 intent.js 与 authorization.js
3
- // 又各写一份(许可词在 authorization 出现 3 次:AUTH_WORDS_RE/APPROVAL_RE/APPROVAL_EXEC_RE)——
4
- // 同语义多处定义 = 漂移根源。本文件 = 五表唯一源:
5
- // ① 执行许可词表(许可词 + 执行语,两者缺一才不构成许可)
6
- // ② 执行动作词表(意图/授权判定共用)
7
- // ③ 疑问词表(问询分点判定)
8
- // ④ 状态信号词表("我已重启/好了/done"等,状态信号不触发无执行分点)
9
- // ⑤ 豁免工具清单(规则 22 ③ "只读/展示/ask/todo_write 等豁免"的明细)
10
- // 调用方(intent.js/authorization.js)一律从这里 import,禁止再定义第二份。
11
- // 设计原则:词表可以在,但只能有一份;语义变更改这里,正文措辞改 AGENTS.md,二者成对。
12
-
13
- // ── ① 执行许可词表 ──
14
- // 许可词:用户确认/同意/批准/授权/可以/允许/好/行/ok/yes 等 + 执行语(即可/现在/马上/…)
15
- // 组合规则:许可词 [0,16 字符内] 执行语 → 构成执行许可;纯"确认方案/理解/方向"不构成。
16
- export const APPROVAL_WORDS_RE = /(?:确认|同意|批准|授权|可以|允许|好|行|ok|yes)/i;
17
- export const EXEC_FOLLOW_RE = /(?:即可|现在|马上|继续|开始|执行|落盘|去做|实施|进行|删除|修改|改|补|修|做|写入|创建|安装|发布|下载|提交|运行)/i;
18
- export const APPROVAL_EXEC_RE = new RegExp(
19
- APPROVAL_WORDS_RE.source.replace(/\\\//g, "/") +
20
- "[^\\n]{0,16}" +
21
- EXEC_FOLLOW_RE.source.replace(/\\\//g, "/"),
22
- "i"
23
- );
24
- // 纯方案/理解/方向确认(不构成执行授权)
25
- export const PLAN_ONLY_RE = /确认方案|确认理解|确认方向|确认无误|理解了|明白方向|了解|没问题|收到/i;
26
-
27
- // ── ② 执行动作词表(intent.ACTION_RE / authorization.EXEC_ACTION_RE 的合并唯一源)──
28
- // 注意:单一表同时服务"意图判定"(这句话是不是执行分点)与"ask 结果批准判定"(选择=批准),
29
- // 语义一致,不另分裂;新增动作词只改这里。
30
- export const ACTION_WORDS_RE =
31
- /执行|跑|落|落盘|认可|开始|写入|写(?:下|好|完|成|作|文件)?|保存|另存为|删除|修改|改(?:为|成|动|一下|进|善)?|补(?:上|齐|全|充|写)?|修(?:改|复|一下)?|做(?:好|完|一下)?|创建|安装|发布|下载|提交|运行|修复|替换|重建|启动|重启|停止|卸载|开始改|开始写|改进|优化|增强|完善|升级|迁移|整理|调整|实现|实施|添加|增加|补充|重写|改造|调试|排查|处理|解决|继续|推进|更新|部署|核实|梳理|诊断|对齐|跟进|落实|设计并实现|调查|开工|动手|进行|完成|选择|采纳|选定|产出|编写|撰写|生成|审查|阅读|查看|核对|检查|审阅|复核|查阅|验证|对照|调研|研究|转化|转换|读取|读(?:出|一下|一遍|完|出来)?|展示|打开|提取|还原|导入|导出|清理|移除|清空|残留/i;
32
-
33
- // 强执行语(直接执行指令,不等授权确认——用于意图置信提升)
34
- export const STRONG_EXEC_RE =
35
- /执行吧|开始执行|直接执行|立即执行|现在执行|马上执行|请落盘|开始写|开始改|执行以下|执行第|执行这个|执行它|落盘|开始蒸馏|开始迁移|开始修复|开始改进|开始优化|采纳|选定/i;
36
-
37
- // 指令式动作词(用户命令式指令 = 视为授权执行;与批准词不同源、用途不同)
38
- export const DIRECTIVE_WORDS_RE =
39
- /(?:请\s*)?(?:删除|移除|修改|编辑|替换|写入|写(?:下|好|完|成|作|文件)?|保存|另存为|创建|复制|移动|执行|运行|下载|提交|推送|备份|安装|卸载|清理|启动|停止|重建|修复|改(?:为|成|动|一下)?|补(?:上|齐|全|充|写)?|修(?:改|复|一下)?|做(?:好|完|一下)?|实施|推进|继续|处理|解决)/i;
40
-
41
- // 拒绝词(授权/批准判定用)
42
- export const REJECTION_WORDS_RE = /(?:不允许|拒绝|不要|不同意|取消(?:这次|并停)?|(?:^|[^是])否(?:[^则]|$)|no(?!tes?|thing|pe)\b|\bno\b|No)/i;
43
-
44
- // ── ③ 疑问词表(问询分点判定;authorization.isQuestionMessage 同源)──
45
- export const QUESTION_WORDS_RE =
46
- /[??]|吗|为什么|是否|能不能|行不行|难道|是不是|能否|怎么|为何|是否要|影响吗|可以吗|要不要|需不需要/;
47
-
48
- // ── ④ 状态信号词表("我已重启/已输入/好了/done";触发时不判"无执行分点")──
49
- export const STATUS_SIGNAL_WORDS_RE =
50
- /(?:我已|我已经|我(?:们)?(?:刚)?(?:重启|输入|执行|完成|搞定|做完|弄好|输完|输好))(?:了|完毕|完成|好)?|(?:^|\s)(?:已|已经)?(?:重启|输入|执行|完成|搞定|做完|弄好|输完|输好|unlock)(?:了|完毕|完成|好)?(?:\s|$)|(?:^|\s)已经(?:刚)?(?:重启|输入|执行|完成|搞定|做完|弄好|输完|输好|unlock)[^\s]*(?:\s|$)|(?:^|\s)(?:重启|输入|命令|操作)(?:完成|完毕|好了|已输入)(?:\s|$)|(?:^|\s)好了(?:\s|$)|^(?:done|restarted|finished|entered)$/i;
51
- // 危险动作词:命中则不当作状态信号(防"我已删除了某文件"被当成就绪确认)
52
- export const DANGEROUS_ACTION_WORDS_RE = /删除|覆盖|移除|清空|卸载|remove-item|rm\s+-r|del\s+/i;
53
-
54
- // ── ⑤ 豁免工具清单(规则 22 ③ "只读/展示/ask/todo_write 等豁免"明细)──
55
- // 只读/展示/询问/产物类工具在"无执行分点"回合也可调用(引擎 tool-catalog 的 analysis/artifact 分类
56
- // 为总开关;本清单 = 规则正文"等豁免"所指的可枚举集,与 tool-catalog 一致,此处集中注释成对)。
57
- export const EXEMPT_TOOL_NAMES = new Set([
58
- // 只读/查询
59
- "read", "grep", "glob", "read_image",
60
- // 视觉分析
61
- "vision_describe", "vision_detect", "vision_ground", "vision_colors", "vision_ocr", "vision_bootstrap",
62
- // 状态查询
63
- "job_list", "job_output", "list_agents", "get_goal", "schedule_list",
64
- "engram_recall", "engram_detail", "engram_model",
65
- "esr_status", "esr_ready", "esr_model",
66
- // 网络只读
67
- "web_fetch", "web_search",
68
- // 询问/展示/产物
69
- "ask_user_question", "todo_write", "visualize", "report",
70
- // inspect 只读
71
- "cordis_inspect_list", "cordis_inspect_query", "cordis_inspect_self"
72
- ]);
1
+ // lexicon.js - 规则 22 语义判定的唯一词表源(0.5.11,用户拍板"唯一词表源由引擎维护")。
2
+ // 背景(2026-08-28 全量审查):正文曾手抄词表(L144/148),引擎内部 intent.js 与 authorization.js
3
+ // 又各写一份(许可词在 authorization 出现 3 次:AUTH_WORDS_RE/APPROVAL_RE/APPROVAL_EXEC_RE)——
4
+ // 同语义多处定义 = 漂移根源。本文件 = 五表唯一源:
5
+ // ① 执行许可词表(许可词 + 执行语,两者缺一才不构成许可)
6
+ // ② 执行动作词表(意图/授权判定共用)
7
+ // ③ 疑问词表(问询分点判定)
8
+ // ④ 状态信号词表("我已重启/好了/done"等,状态信号不触发无执行分点)
9
+ // ⑤ 豁免工具清单(规则 22 ③ "只读/展示/ask/todo_write 等豁免"的明细)
10
+ // 调用方(intent.js/authorization.js)一律从这里 import,禁止再定义第二份。
11
+ // 设计原则:词表可以在,但只能有一份;语义变更改这里,正文措辞改 AGENTS.md,二者成对。
12
+
13
+ // ── ① 执行许可词表 ──
14
+ // 许可词:用户确认/同意/批准/授权/可以/允许/好/行/ok/yes 等 + 执行语(即可/现在/马上/…)
15
+ // 组合规则:许可词 [0,16 字符内] 执行语 → 构成执行许可;纯"确认方案/理解/方向"不构成。
16
+ export const APPROVAL_WORDS_RE = /(?:确认|同意|批准|授权|可以|允许|好|行|ok|yes)/i;
17
+ export const EXEC_FOLLOW_RE = /(?:即可|现在|马上|继续|开始|执行|落盘|去做|实施|进行|删除|修改|改|补|修|做|写入|创建|安装|发布|下载|提交|运行)/i;
18
+ export const APPROVAL_EXEC_RE = new RegExp(
19
+ APPROVAL_WORDS_RE.source.replace(/\\\//g, "/") +
20
+ "[^\\n]{0,16}" +
21
+ EXEC_FOLLOW_RE.source.replace(/\\\//g, "/"),
22
+ "i"
23
+ );
24
+ // 纯方案/理解/方向确认(不构成执行授权)
25
+ export const PLAN_ONLY_RE = /确认方案|确认理解|确认方向|确认无误|理解了|明白方向|了解|没问题|收到/i;
26
+
27
+ // ── ② 执行动作词表(intent.ACTION_RE / authorization.EXEC_ACTION_RE 的合并唯一源)──
28
+ // 注意:单一表同时服务"意图判定"(这句话是不是执行分点)与"ask 结果批准判定"(选择=批准),
29
+ // 语义一致,不另分裂;新增动作词只改这里。
30
+ export const ACTION_WORDS_RE =
31
+ /执行|跑|落|落盘|认可|开始|写入|写(?:下|好|完|成|作|文件)?|保存|另存为|删除|修改|改(?:为|成|动|一下|进|善)?|补(?:上|齐|全|充|写)?|修(?:改|复|一下)?|做(?:好|完|一下)?|创建|安装|发布|下载|提交|运行|修复|替换|重建|启动|重启|停止|卸载|开始改|开始写|改进|优化|增强|完善|升级|迁移|整理|调整|实现|实施|添加|增加|补充|重写|改造|调试|排查|处理|解决|继续|推进|更新|部署|核实|梳理|诊断|对齐|跟进|落实|设计并实现|调查|开工|动手|进行|完成|选择|采纳|选定|产出|编写|撰写|生成|审查|阅读|查看|核对|检查|审阅|复核|查阅|验证|对照|调研|研究|转化|转换|读取|读(?:出|一下|一遍|完|出来)?|展示|打开|提取|还原|导入|导出|清理|移除|清空|残留|合入|合并|并入|融合|整合|起草|开展|搭建|重构|兼容|投产|校验|核验|过一遍|跑一遍|复验/i;
32
+
33
+ // 强执行语(直接执行指令,不等授权确认——用于意图置信提升)
34
+ export const STRONG_EXEC_RE =
35
+ /执行吧|开始执行|直接执行|立即执行|现在执行|马上执行|请落盘|开始写|开始改|执行以下|执行第|执行这个|执行它|落盘|开始蒸馏|开始迁移|开始修复|开始改进|开始优化|采纳|选定/i;
36
+
37
+ // 指令式动作词(用户命令式指令 = 视为授权执行;与批准词不同源、用途不同)
38
+ export const DIRECTIVE_WORDS_RE =
39
+ /(?:请\s*)?(?:删除|移除|修改|编辑|替换|写入|写(?:下|好|完|成|作|文件)?|保存|另存为|创建|复制|移动|执行|运行|下载|提交|推送|备份|安装|卸载|清理|启动|停止|重建|修复|改(?:为|成|动|一下)?|补(?:上|齐|全|充|写)?|修(?:改|复|一下)?|做(?:好|完|一下)?|实施|推进|继续|处理|解决)/i;
40
+
41
+ // 拒绝词(授权/批准判定用)
42
+ export const REJECTION_WORDS_RE = /(?:不允许|拒绝|不要|不同意|取消(?:这次|并停)?|(?:^|[^是])否(?:[^则]|$)|no(?!tes?|thing|pe)\b|\bno\b|No)/i;
43
+
44
+ // ── ③ 疑问词表(问询分点判定;authorization.isQuestionMessage 同源)──
45
+ export const QUESTION_WORDS_RE =
46
+ /[??]|吗|为什么|是否|能不能|行不行|难道|是不是|能否|怎么|为何|是否要|影响吗|可以吗|要不要|需不需要/;
47
+
48
+ // ── ④ 状态信号词表("我已重启/已输入/好了/done";触发时不判"无执行分点")──
49
+ export const STATUS_SIGNAL_WORDS_RE =
50
+ /(?:我已|我已经|我(?:们)?(?:刚)?(?:重启|输入|执行|完成|搞定|做完|弄好|输完|输好))(?:了|完毕|完成|好)?|(?:^|\s)(?:已|已经)?(?:重启|输入|执行|完成|搞定|做完|弄好|输完|输好|unlock)(?:了|完毕|完成|好)?(?:\s|$)|(?:^|\s)已经(?:刚)?(?:重启|输入|执行|完成|搞定|做完|弄好|输完|输好|unlock)[^\s]*(?:\s|$)|(?:^|\s)(?:重启|输入|命令|操作)(?:完成|完毕|好了|已输入)(?:\s|$)|(?:^|\s)好了(?:\s|$)|^(?:done|restarted|finished|entered)$/i;
51
+ // 危险动作词:命中则不当作状态信号(防"我已删除了某文件"被当成就绪确认)
52
+ export const DANGEROUS_ACTION_WORDS_RE = /删除|覆盖|移除|清空|卸载|remove-item|rm\s+-r|del\s+/i;
53
+
54
+ // ── ⑤ 豁免工具清单(规则 22 ③ "只读/展示/ask/todo_write 等豁免"明细)──
55
+ // 只读/展示/询问/产物类工具在"无执行分点"回合也可调用(引擎 tool-catalog 的 analysis/artifact 分类
56
+ // 为总开关;本清单 = 规则正文"等豁免"所指的可枚举集,与 tool-catalog 一致,此处集中注释成对)。
57
+ export const EXEMPT_TOOL_NAMES = new Set([
58
+ // 只读/查询
59
+ "read", "grep", "glob", "read_image",
60
+ // 视觉分析
61
+ "vision_describe", "vision_detect", "vision_ground", "vision_colors", "vision_ocr", "vision_bootstrap",
62
+ // 状态查询
63
+ "job_list", "job_output", "list_agents", "get_goal", "schedule_list",
64
+ "engram_recall", "engram_detail", "engram_model",
65
+ "esr_status", "esr_ready", "esr_model",
66
+ // 网络只读
67
+ "web_fetch", "web_search",
68
+ // 询问/展示/产物
69
+ "ask_user_question", "todo_write", "visualize", "report",
70
+ // inspect 只读
71
+ "cordis_inspect_list", "cordis_inspect_query", "cordis_inspect_self"
72
+ ]);
@@ -247,6 +247,25 @@ const READONLY_CMD_RE =
247
247
  /(?:\b(?:Get-Content|Get-ChildItem|Get-Item|Get-Command|Get-Date|Select-String|Find-String|Test-Path|Get-Process|Get-Service|Get-ItemProperty|Get-Variable|Get-FileHash|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|ForEach-Object|Out-Null|ConvertTo-Json|ConvertFrom-Json|Join-Path|Split-Path|Resolve-Path|New-Object|Add-Type|Get-Member|Get-ChildItemProperty|Get-CimInstance|Get-WmiObject)\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;
248
248
  const MUTATING_CMD_RE =
249
249
  /(?:Set-Content|Add-Content|Out-File|Tee-Object|Remove-Item|Move-Item|Copy-Item|Rename-Item|New-Item|Clear-Content|Start-Process|Invoke-Expression|Invoke-Item|Export-Csv|Export-Clixml|Export-ModuleMember|Compress-Archive|Expand-Archive|Add-Type\s+-Output(?:Assembly|Type)|git\s+(?:push|commit)|rm\s+-r|rmdir\s+\/s|del\s+\/s|(?:^|[^0-9])>>|(?:^|[^0-9])>|(?:curl|wget|iwr|Invoke-WebRequest|Invoke-RestMethod)\s+(?=[^\n;|]*\s(?:-o|--output|-OutFile|OutFile)\s))/i;
250
+ // 0.5.12(F4):PowerShell 别名展开表——写/删除/移动/执行别名 → 标准 cmdlet 名
251
+ // 供 normalizeAliases 做命令规范化(MUTATING_CMD_RE 覆盖完整 cmdlet;别名先展开再判)
252
+ const PS_ALIAS_MAP = [
253
+ [/^rm\b(?![a-z])/i, "Remove-Item"], [/^rd\b(?![a-z])/i, "Remove-Item"], [/^ri\b(?![a-z])/i, "Remove-Item"],
254
+ [/^del\b(?![a-z])/i, "Remove-Item"], [/^erase\b(?![a-z])/i, "Remove-Item"],
255
+ [/^cp\b(?![a-z])/i, "Copy-Item"], [/^copy\b(?![a-z])/i, "Copy-Item"],
256
+ [/^mv\b(?![a-z])/i, "Move-Item"], [/^move\b(?![a-z])/i, "Move-Item"],
257
+ [/^ren\b(?![a-z])/i, "Rename-Item"], [/^iex\b(?![a-z])/i, "Invoke-Expression"],
258
+ [/^gc\b(?![a-z])/i, "Get-Content"], [/^gci\b(?![a-z])/i, "Get-ChildItem"], [/^gi\b(?![a-z])/i, "Get-Item"],
259
+ [/^gp\b(?![a-z])/i, "Get-ItemProperty"], [/^gv\b(?![a-z])/i, "Get-Variable"], [/^gl\b(?![a-z])/i, "Get-Location"]
260
+ ];
261
+ /** 规范化命令文本中的 PS 别名 → 标准 cmdlet 大写(只变换命令头,不动参数/字符串) */
262
+ export function normalizeAliases(command) {
263
+ const s = String(command || "");
264
+ return s.replace(/(?:^|[\s;|&{(])\s*(rm|rd|ri|del|erase|cp|copy|mv|move|ren|iex|gc|gci|gi|gp|gv|gl)(?=\s+)/gi, (m, name) => {
265
+ const hit = PS_ALIAS_MAP.find(([re]) => re.test(name));
266
+ return hit ? `${m.slice(0, m.length - name.length)}${hit[1]}` : m;
267
+ });
268
+ }
250
269
  // .NET 静态/实例写方法(MUTATING 补充:Set-Content 等 cmdlet 之外的写路径)
251
270
  const MUTATING_METHOD_RE =
252
271
  /(?:\[[\w.]+\]::\s*(?:Write|Set|Remove|Delete|Move|Copy|Create|Save|Append)\w*\s*\(|\.(?:Write(?:AllText|Line|Bytes|Text)?|SetContent|MoveTo?|CopyTo|Delete|Remove|SaveAs?2?\s*\(|CreateDirectory|CreateFile|WriteLine)\s*\()/i;
@@ -255,7 +274,10 @@ const MUTATING_METHOD_RE =
255
274
  // 保守边界:内容属性赋值($doc.Text=... / $doc.Content.Text=...)是写文档,不豁免。
256
275
  const COM_CONTENT_ASSIGN_RE = /\.(?:Text|Content|Value|Range|Paragraphs|Sections|Documents|Document)\s*=/i;
257
276
  const ALLOW_SEG_RE =
258
- /(?:\$[A-Za-z_][\w$]*(?:(?:\.[\w$]+)|(?:\[[^\]]+\]))*\s*=)|(?:\$[\w$.]+\.(?:Content|Range|Text|Value|Paragraphs|Sections|Length|Count|Name|Selected)\b)|(?:\$[\w$.]+$)|(?:\b(?:Documents|Document)\.(?:Open|Item)\s*\([^)]*,\s*\d+\s*,\s*\$?true\s*\))/;
277
+ /(?:\$[A-Za-z_][\w$]*(?:(?:\.[\w$]+)|(?:\[[^\]]+\]))*\s*=)|(?:\$[\w$.]+\.(?:Content|Range|Text|Value|Paragraphs|Sections|Length|Count|Name|Selected|LinkType|Target|FullName|Extension|Directory|PSPath|Property|Attributes|Mode|LastWriteTime|Exists)\b)|(?:\$[\w$.]+$)|(?:\b(?:Documents|Document)\.(?:Open|Item)\s*\([^)]*,\s*\d+\s*,\s*\$?true\s*\))|(?:\(!?\$[\w$.]+(?:\.\w+)?\)|\([^()\r\n]*\$[\w$.]+\.[\w.]+\s*\))/;
278
+ // 0.5.12(F4):纯字符串/纯输出段(整段是引号包裹字符串或 $() 子表达式)——不是命令,放行。
279
+ // 保守边界:只匹配"整段即字符串"(^...$ 锚定),不让 node -e "..." / curl -o "..." 借道。
280
+ const PURE_STRING_SEG_RE = /^(?:"[^"\r\n]*"|'[^'\r\n]*'|`[^`\r\n]*`|\$\(\s*[^()\r\n]*\s*\))+$/;
259
281
  // 剥离 try/catch/finally 控制块:块内语句与主语句同样接受只读判定(Try-* 不是 cmdlet)。
260
282
  // 注意:不做全量 [{}] 替换——if/else 子表达式($(if(...){...}else{...}))会被切碎误判。
261
283
  function stripControlBlocks(command) {
@@ -289,7 +311,10 @@ function splitCommandSegments(command) {
289
311
  * ③ 其它不认识的段 → 保守判非只读(不放大白名单,宁紧勿松)。 */
290
312
  export function isReadOnlyCommand(command) {
291
313
  if (typeof command !== "string") return false;
292
- const segments = splitCommandSegments(stripControlBlocks(command));
314
+ // 0.5.12(F4):先做别名规范化(rm→Remove-Item、gc→Get-Content…),
315
+ // 保证 MUTATING/READONLY 词表只看标准 cmdlet(别名双向缺口:写漏 rm/del/iex、读漏 gc/gci)
316
+ const normalized = normalizeAliases(command);
317
+ const segments = splitCommandSegments(stripControlBlocks(normalized));
293
318
  if (segments.length === 0) return false;
294
319
  return segments.every((seg) => {
295
320
  const s = String(seg).trim();
@@ -300,6 +325,7 @@ export function isReadOnlyCommand(command) {
300
325
  if (COM_CONTENT_ASSIGN_RE.test(s)) return false; // $doc.Text=... 是写文档
301
326
  if (READONLY_CMD_RE.test(s)) return true;
302
327
  if (ALLOW_SEG_RE.test(s)) return true;
328
+ if (PURE_STRING_SEG_RE.test(s)) return true; // 0.5.12(F4):纯字符串/输出段(非命令)
303
329
  return false; // 看不清的段:保守按非只读处理
304
330
  });
305
331
  }