dsh-rule-engine 0.4.0 → 0.4.2

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 CHANGED
@@ -1,105 +1,107 @@
1
- # dsh-rule-engine
2
-
3
- DSH 规则执行引擎 v3 的插件实现。它把 `~/.dsh/AGENTS.md` 当作唯一真相源,自动解析规则四要素与执行等级,再通过「工具守卫 + 文本检测 + 时序检查 + 审计台账」执行用户规则,而不是内置一套与用户无关的安全清单。
4
-
5
- ## 项目背景
6
-
7
- 这个项目来自一个非常具体的个人需求:
8
-
9
- - 作者是**零编程基础**用户,但极其重视规则的制定、执行、遵守与复盘。
10
- - 作者发现:规则如果只写在文本里、靠模型“自觉”执行,会反复失效(例如时间词写错、内联命令违规、交付前漏验证等)。
11
- - 因此核心思路是:**规则的执行不能只靠自觉,要尽量靠插件在机制层强制**。
12
- - 本插件所有规则均从 `AGENTS.md` 动态解析,规则增删改后无需重写插件。
13
-
14
- 当前实现基于已有的 `AGENTS.md` 规则体系拓展,社区暂无类似插件供参考(大概率为该等约束可能限制开发自由性,不适用于专业编程人员),**可能存在大量不完备、误判或边界问题**。欢迎任何使用者提出调整建议、提交 issue 或 PR。项目仍处于“可运行但需要持续打磨”的阶段。
15
-
16
- ## 功能分层
17
-
18
- - 阶段 1 容器:解析 AGENTS.md 全部规则 → 理解产物(`rule-understanding.json` 可生成)
19
- - 阶段 2 匹配机 + 工具守卫 + 文本检测
20
- - 阶段 3 时序检查 + 授权询问集成
21
- - 阶段 4 D 级自证调度 + `/guard` 命令完善
22
-
23
- 当前实现以「模式库兜底」为主,LLM 理解器预留扩展点;所有规则均从 AGENTS.md 实时解析。
24
-
25
- ## 命令
26
-
27
- | 命令 | 作用 |
28
- |---|---|
29
- | `/guard status` | 引擎状态(规则数/置信度/放行/解锁) |
30
- | `/guard rules` | 规则清单 + 理解产物 |
31
- | `/guard active` | 最近激活了哪些规则、为什么 |
32
- | `/guard log [N]` | 最近 N 条审计 |
33
- | `/guard unlock [N]` | 解锁配置写保护 N 分钟(仅用户) |
34
- | `/guard bypass [N]` | 临时整体放行 N 分钟(仅用户) |
35
- | `/guard lock` | 立即恢复全部守卫(取消解锁/放行) |
36
- | `/guard revoke` | 撤销全部授权记录 |
37
- | `/guard reload` | 强制重解析 AGENTS.md |
38
-
39
- ## 装配方式
40
-
41
- 本插件已按官方 **bundle** 规范打包,包内自带 `cordis.patch.yml`。
42
-
43
- 推荐安装方式:
44
-
45
- ```bash
46
- dsh plugin --profile web add dsh-rule-engine
47
- ```
48
-
49
- 或手动将 `dsh-rule-engine` 加入 profile 的 `dsh.profile.bundles` 数组。包内的 `cordis.patch.yml` 会自动挂载插件行:
50
-
51
- ```yaml
52
- - insert:
53
- - id: dsh-rule-engine
54
- name: 'dsh-rule-engine'
55
- ```
56
-
57
- 如果你是从源码手动调试,也可以沿用 insert 方式挂载,但正式安装建议走 bundle。
58
-
59
- ## 安全设计
60
-
61
- - 只读操作(read/grep/glob/read_image/str_replace_editor view)无条件放行,拦截只针对变更类操作
62
- - 插件自身配置/理解产物对模型只读:直接 `edit/write` 会被守卫拒绝,需 `/guard unlock`
63
- - AGENTS.md mtime 变化后自动重解析(`fs.watch` + stat 兜底),规则增删改无需重启
64
- - 低置信规则不参与硬拦,避免误伤;在 `/guard rules` 中标记人工复核
65
- - 授权证据按“操作类型 + 目标路径前缀”结构化匹配,区分“询问”与“授权”
66
- - 备份证据按“目标路径 → 备份路径”记录,删除/覆盖前必须存在对应路径且备份文件真实存在
67
- - 版本/手册类文件写后自检:版本号连续、append 不覆盖上一行,失败自动回滚并审计
68
- - 跨工具一致性:同一敏感操作经 `edit` / `write` / `str_replace_editor` / `pwsh` 必须得到相同拦截/放行结论
69
- - 命令输出静默错误检测:全 false/0/null 或与上一条完全一致时审计 + 注入提醒,不阻断
70
- - 技能目录实时联动:`ctx.skills` 目录变化后自动刷新,已禁用/不存在的技能不触发 12B
71
- - LLM 增量理解:对非 high 置信规则调用 `ctx.llm` 补全结构化理解,失败自动回退模式库
72
- - D 级自证泛化:按规则特征触发自证提示,每规则每会话限 3 次
73
- - 授权记录默认 10 分钟 TTL,无路径的全局授权 TTL 缩短为 2 分钟;可用 `/guard revoke` 撤销
74
- - 用户直接命令式指令(如“删除这个文件”)也视为授权
75
- - 规则 1 支持“用户明确要求重试”豁免
76
- - 会话状态有容量上限并自动清理,防止长跑内存膨胀
77
- - LLM 理解按“规则 + AGENTS.md mtime”去重,避免重复烧 token
78
- - 审计日志:`~/.dsh/rule-engine.log.jsonl`
79
- - 守卫使用 `ctx.tools.guard()` 单调拒绝,模型无法自行绕过
80
-
81
- ## 当前局限与后续优化路线
82
-
83
- 当前版本已经具备完整四层骨架,但距离“成熟”仍有距离。以下是一些**难度较高、尚未完全实现**的优化方向,欢迎社区共同推进:
84
-
85
- 1. **LLM 理解器深化**
86
- 当前只对非 high 置信规则做一次 LLM 增量理解;未来应支持“规则变更窗口期”、增量重理解、低置信人工复核队列。
87
-
88
- 2. **授权语义精确化**
89
- 当前 ask 授权记录为宽泛 `any` + 路径前缀;未来可要求 ask 面板显式声明操作类型,或支持“一次授权仅针对单个 callId”。
90
-
91
- 3. **备份证据完整化**
92
- 当前校验备份文件存在;未来可增加哈希/大小一致性校验、备份链管理与自动清理。
93
-
94
- 4. **规则 12C / 13B / 10 / 15 / 19 等流程类规则深度执行**
95
- 这些规则需要更多业务语义(下载校验、会话三层验证、版本判断、知识沉淀),目前偏“自证提示”,尚未做到机器可判定。
96
-
97
- 5. **跨会话持久化**
98
- 授权/备份目前为内存态,重启失效。持久化涉及写入保护、并发与恢复,风险较高,暂未实现。
99
-
100
- 6. **输出文本实时拦截**
101
- 受 DSH 官方架构限制,`assistant/message` 无法“拦下不发”,只能事后审计 + 纠正注入;这是平台边界,不是插件能单独突破的。
102
-
1
+ # dsh-rule-engine
2
+
3
+ DSH 规则执行引擎 v3 的插件实现。它把 `~/.dsh/AGENTS.md` 当作唯一真相源,自动解析规则四要素与执行等级,再通过「工具守卫 + 文本检测 + 时序检查 + 审计台账」执行用户规则,而不是内置一套与用户无关的安全清单。
4
+
5
+ ## 项目背景
6
+
7
+ 这个项目来自一个非常具体的个人需求:
8
+
9
+ - 作者是**零编程基础**用户,但极其重视规则的制定、执行、遵守与复盘。
10
+ - 作者发现:规则如果只写在文本里、靠模型“自觉”执行,会反复失效(例如时间词写错、内联命令违规、交付前漏验证等)。
11
+ - 因此核心思路是:**规则的执行不能只靠自觉,要尽量靠插件在机制层强制**。
12
+ - 本插件所有规则均从 `AGENTS.md` 动态解析,规则增删改后无需重写插件。
13
+
14
+ 当前实现基于已有的 `AGENTS.md` 规则体系拓展,社区暂无类似插件供参考(大概率为该等约束可能限制开发自由性,不适用于专业编程人员),**可能存在大量不完备、误判或边界问题**。欢迎任何使用者提出调整建议、提交 issue 或 PR。项目仍处于“可运行但需要持续打磨”的阶段。
15
+
16
+ ## 功能分层
17
+
18
+ - 阶段 1 容器:解析 AGENTS.md 全部规则 → 理解产物(`rule-understanding.json` 可生成)
19
+ - 阶段 2 匹配机 + 工具守卫 + 文本检测
20
+ - 阶段 3 时序检查 + 授权询问集成
21
+ - 阶段 4 D 级自证调度 + `/guard` 命令完善
22
+
23
+ 当前实现以「模式库兜底」为主,LLM 理解器预留扩展点;所有规则均从 AGENTS.md 实时解析。
24
+
25
+ ## 命令
26
+
27
+ | 命令 | 作用 |
28
+ |---|---|
29
+ | `/guard status` | 引擎状态(规则数/置信度/放行/解锁) |
30
+ | `/guard rules` | 规则清单 + 理解产物 |
31
+ | `/guard active` | 最近激活了哪些规则、为什么 |
32
+ | `/guard log [N]` | 最近 N 条审计 |
33
+ | `/guard unlock [N]` | 解锁配置写保护 N 分钟(仅用户) |
34
+ | `/guard bypass [N]` | 临时整体放行 N 分钟(仅用户) |
35
+ | `/guard lock` | 立即恢复全部守卫(取消解锁/放行) |
36
+ | `/guard revoke` | 撤销全部授权记录 |
37
+ | `/guard reload` | 强制重解析 AGENTS.md |
38
+
39
+ ## 装配方式
40
+
41
+ 本插件已按官方 **bundle** 规范打包,包内自带 `cordis.patch.yml`。
42
+
43
+ 推荐安装方式:
44
+
45
+ ```bash
46
+ dsh plugin --profile web add dsh-rule-engine
47
+ ```
48
+
49
+ 或手动将 `dsh-rule-engine` 加入 profile 的 `dsh.profile.bundles` 数组。包内的 `cordis.patch.yml` 会自动挂载插件行:
50
+
51
+ ```yaml
52
+ - insert:
53
+ - id: dsh-rule-engine
54
+ name: 'dsh-rule-engine'
55
+ ```
56
+
57
+ 如果你是从源码手动调试,也可以沿用 insert 方式挂载,但正式安装建议走 bundle。
58
+
59
+ ## 安全设计
60
+
61
+ - 只读操作(read/grep/glob/read_image/str_replace_editor view)无条件放行,拦截只针对变更类操作
62
+ - 插件自身配置/理解产物对模型只读:直接 `edit/write` 会被守卫拒绝,需 `/guard unlock`
63
+ - AGENTS.md mtime 变化后自动重解析(`fs.watch` + stat 兜底),规则增删改无需重启
64
+ - 低置信规则不参与硬拦,避免误伤;在 `/guard rules` 中标记人工复核
65
+ - 授权证据按“操作类型 + 目标路径前缀”结构化匹配,区分“询问”与“授权”
66
+ - 备份证据按“目标路径 → 备份路径”记录,删除/覆盖前必须存在对应路径且备份文件真实存在
67
+ - 版本/手册类文件写后自检:版本号连续、append 不覆盖上一行,失败自动回滚并审计
68
+ - 跨工具一致性:同一敏感操作经 `edit` / `write` / `str_replace_editor` / `pwsh` 必须得到相同拦截/放行结论
69
+ - 命令输出静默错误检测:全 false/0/null 或与上一条完全一致时审计 + 注入提醒,不阻断
70
+ - 技能目录实时联动:`ctx.skills` 目录变化后自动刷新,已禁用/不存在的技能不触发 12B
71
+ - LLM 增量理解:对非 high 置信规则调用 `ctx.llm` 补全结构化理解,失败自动回退模式库;AGENTS.md 变化触发重载后会自动补一次增量理解(按规则+版本去重,不重复烧 token)
72
+ - D 级自证泛化:按规则特征触发自证提示,每规则每会话限 3 次
73
+ - 授权记录默认 10 分钟 TTL,无路径的全局授权 TTL 缩短为 2 分钟;可用 `/guard revoke` 撤销
74
+ - 用户直接命令式指令(如“删除这个文件”)也视为授权
75
+ - 规则 1 支持“用户明确要求重试”豁免
76
+ - 会话状态有容量上限并自动清理,防止长跑内存膨胀
77
+ - LLM 理解按“规则 + AGENTS.md mtime”去重,避免重复烧 token
78
+ - 审计日志:`~/.dsh/rule-engine.log.jsonl`
79
+ - 守卫使用 `ctx.tools.guard()` 单调拒绝,模型无法自行绕过
80
+ - **自由区域(Free Zone)**:AGENTS.md 中 `<!-- free-zone:start -->` / `<!-- free-zone:end -->` 标记框住的区段**整区跳过**(不解析、不产生规则、不硬拦、不审计)——适合放“想生效但不想被机器强制”的软约束(如法律守则)。区内的 `### [规则 F<n>]` 条目由配套插件 dsh-rules-manager 在设置页/`/rules` 中可见可管理。新增自由规则请手动在标记内编写(`/rules add` 只会插入到 free-zone 之前),详见 dsh-rules-manager 的 README「📝 新增一条自由规则(零基础三步)」
81
+ - **禁用规则联动**:dsh-rules-manager 的「禁用规则」存储(`~/.dsh/disabled-rules.json`)会被引擎读取,被禁用的规则标记为 disabled,不参与硬拦/纠察;恢复启用后自动重新生效
82
+
83
+ ## 当前局限与后续优化路线
84
+
85
+ 当前版本已经具备完整四层骨架,但距离“成熟”仍有距离。以下是一些**难度较高、尚未完全实现**的优化方向,欢迎社区共同推进:
86
+
87
+ 1. **LLM 理解器深化**
88
+ 当前只对非 high 置信规则做一次 LLM 增量理解;未来应支持“规则变更窗口期”、增量重理解、低置信人工复核队列。
89
+
90
+ 2. **授权语义精确化**
91
+ 当前 ask 授权记录为宽泛 `any` + 路径前缀;未来可要求 ask 面板显式声明操作类型,或支持“一次授权仅针对单个 callId”。
92
+
93
+ 3. **备份证据完整化**
94
+ 当前校验备份文件存在;未来可增加哈希/大小一致性校验、备份链管理与自动清理。
95
+
96
+ 4. **规则 12C / 13B / 10 / 15 / 19 等流程类规则深度执行**
97
+ 这些规则需要更多业务语义(下载校验、会话三层验证、版本判断、知识沉淀),目前偏“自证提示”,尚未做到机器可判定。
98
+
99
+ 5. **跨会话持久化**
100
+ 授权/备份目前为内存态,重启失效。持久化涉及写入保护、并发与恢复,风险较高,暂未实现。
101
+
102
+ 6. **输出文本实时拦截**
103
+ 受 DSH 官方架构限制,`assistant/message` 无法“拦下不发”,只能事后审计 + 纠正注入;这是平台边界,不是插件能单独突破的。
104
+
103
105
  ## 致谢
104
106
 
105
107
  感谢以下项目与作者的无私开源付出,本项目在开发过程中直接受益:
@@ -116,17 +118,17 @@ dsh plugin --profile web add dsh-rule-engine
116
118
  - SandBase deepseek-harness-handbook(sandbaseai)
117
119
  - 以及 DSH 官方文档镜像与源码维护者
118
120
 
119
- ## 免责声明
120
-
121
- 本项目是**个人/社区项目**,**不属于 DeepSeek Harness 官方项目**,与官方无隶属关系。使用风险自负,请在生产环境前充分测试。
122
-
123
- ## 开发与测试
124
-
125
- ```bash
126
- npm test
127
- bash scripts/build.sh
128
- ```
129
-
130
- ## License
131
-
132
- MIT
121
+ ## 免责声明
122
+
123
+ 本项目是**个人/社区项目**,**不属于 DeepSeek Harness 官方项目**,与官方无隶属关系。使用风险自负,请在生产环境前充分测试。
124
+
125
+ ## 开发与测试
126
+
127
+ ```bash
128
+ npm test
129
+ bash scripts/build.sh
130
+ ```
131
+
132
+ ## License
133
+
134
+ MIT
package/lib/core/audit.js CHANGED
@@ -1,16 +1,25 @@
1
1
  // audit.js - 审计台账(JSONL)。
2
2
  // 每次拦截/纠察/自证都追加一行;超限自动裁剪保留尾部。
3
+ // P2-9:裁剪改为惰性——每 APPEND_TRIM_INTERVAL 次追加才做一次大小检查,
4
+ // 避免每次写入都 statSync(高频 deny/纠察时降低 IO)。
3
5
  import { appendFileSync, readFileSync, statSync, writeFileSync } from "node:fs";
4
6
  import { auditFilePath } from "./paths.js";
5
7
 
6
8
  const LOG_MAX_BYTES = 512 * 1024;
7
9
  const LOG_KEEP_LINES = 400;
10
+ const APPEND_TRIM_INTERVAL = 32;
11
+
12
+ let appendCount = 0;
8
13
 
9
14
  export function audit(entry) {
10
15
  try {
11
16
  const line = JSON.stringify({ ts: new Date().toISOString(), ...entry }) + "\n";
12
17
  appendFileSync(auditFilePath(), line, "utf8");
13
- trimLog();
18
+ appendCount++;
19
+ if (appendCount >= APPEND_TRIM_INTERVAL) {
20
+ appendCount = 0;
21
+ trimLog();
22
+ }
14
23
  } catch {
15
24
  // 审计失败不阻断主流程
16
25
  }
@@ -13,7 +13,6 @@ import {
13
13
  commandText,
14
14
  isAssemblyMutationTool,
15
15
  isBackupTool,
16
- isChinesePs1Violation,
17
16
  isHighRiskEntryFile,
18
17
  isManualReadTool,
19
18
  isProtectedConfigPath,
@@ -255,19 +254,16 @@ function matchRule(cfg, ctx) {
255
254
  return null;
256
255
  }
257
256
 
258
- // 规则 9:内联命令 / BOM 写配置
257
+ // 规则 9:内联命令 / BOM 写配置(PS7 语义:仅拦显式 utf8BOM)
259
258
  if (cfg.handler === "rule9-inline-bom") {
260
259
  if ((name === "pwsh" || name === "bash") && cmd) {
261
260
  if (INLINE_CMD.test(cmd)) {
262
261
  return makeHit(cfg, "【硬拦截】禁止内联命令(node -e / pwsh -c / node -p 等),请先写脚本文件再执行");
263
262
  }
264
263
  if (BOM_WRITE.test(cmd)) {
265
- return makeHit(cfg, "【硬拦截】禁止用 Set-Content/Out-File -Encoding UTF8 写 .json/.yaml(会带 BOM)");
264
+ return makeHit(cfg, "【硬拦截】禁止用 Set-Content/Out-File -Encoding utf8BOM 写 .json/.yaml(PS7 显式带 BOM)");
266
265
  }
267
266
  }
268
- if (isChinesePs1Violation(name, args)) {
269
- return makeHit(cfg, "【硬拦截】含中文的 .ps1 必须 UTF-8 带 BOM;当前写入方式可能无 BOM,请改用纯 ASCII 或显式 BOM 流程");
270
- }
271
267
  return null;
272
268
  }
273
269
 
@@ -3,7 +3,8 @@
3
3
  import { readFileSync, statSync } from "node:fs";
4
4
  import { agentsFilePath } from "./paths.js";
5
5
 
6
- const RULE_HEADER_RE = /^###\s*\[规则\s*([0-9A-Za-z]+)\]\s*(.+?)\s*$/;
6
+ // rules-manager 解析口径一致:标题剥离「(来源:…)」后缀(P1-4),避免 /guard rules 显示带尾巴
7
+ const RULE_HEADER_RE = /^###\s*\[规则\s*([0-9A-Za-z]+)\]\s*(.+?)\s*(?:(来源[^)]*))?\s*$/;
7
8
  const SECTION_RE = /^##\s+(.+?)\s*$/;
8
9
  const FREE_ZONE_START_RE = /^\s*<!--\s*free-zone:start\s*-->\s*$/;
9
10
  const FREE_ZONE_END_RE = /^\s*<!--\s*free-zone:end\s*-->\s*$/;
package/lib/core/paths.js CHANGED
@@ -1,10 +1,11 @@
1
1
  // paths.js - 解析 DSH 用户目录。
2
- // 不依赖 @deepseek-ai/dsh-home-paths,优先使用 DSH_HOME 环境变量,回退到 ~/.dsh。
3
- import { homedir } from "node:os";
2
+ // 统一使用官方 @deepseek-ai/dsh-home-paths resolveDshHome()(P1-7):
3
+ // 优先级 = 显式配置 > $DSH_HOME > ~/.dsh,与 dsh-rules-manager 口径一致。
4
4
  import { join } from "node:path";
5
+ import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
5
6
 
6
7
  export function dshHome() {
7
- return process.env.DSH_HOME || join(homedir(), ".dsh");
8
+ return resolveDshHome();
8
9
  }
9
10
 
10
11
  export function agentsFilePath() {
@@ -22,3 +23,8 @@ export function configFilePath() {
22
23
  export function understandingFilePath() {
23
24
  return join(dshHome(), "rule-understanding.json");
24
25
  }
26
+
27
+ /** dsh-rules-manager 的已禁用规则存储(引擎据此把规则标记 disabled,P0-2) */
28
+ export function disabledRulesFilePath() {
29
+ return join(dshHome(), "disabled-rules.json");
30
+ }
@@ -4,8 +4,10 @@
4
4
  export const INLINE_CMD =
5
5
  /\b(?:node|pwsh|powershell)\s+(?:-[ep]|--eval|--print|-Command|-c)\b/i;
6
6
 
7
+ // PS7 语义(2026-08-19 同步):utf8 = 无 BOM、utf8BOM = 带 BOM、utf8NoBOM = 无 BOM。
8
+ // 只拦显式 -Encoding utf8BOM 写 .json/.yaml;PS7 下 -Encoding utf8 无 BOM 合规放行。
7
9
  export const BOM_WRITE =
8
- /(?:set-content|add-content|out-file|writealltext)[\s\S]{0,300}?(?:-Encoding\s+UTF8|utf8)[\s\S]{0,300}?\.(?:json|ya?ml)\b|(?:set-content|add-content|out-file|writealltext)[\s\S]{0,300}?\.(?:json|ya?ml)\b[\s\S]{0,300}?(?:-Encoding\s+UTF8|utf8)/i;
10
+ /(?:set-content|add-content|out-file|writealltext)[\s\S]{0,300}?(?:-Encoding\s+utf8bom|utf8bom)[\s\S]{0,300}?\.(?:json|ya?ml)\b|(?:set-content|add-content|out-file|writealltext)[\s\S]{0,300}?\.(?:json|ya?ml)\b[\s\S]{0,300}?(?:-Encoding\s+utf8bom|utf8bom)/i;
9
11
 
10
12
  export const DESTRUCTIVE_CMD =
11
13
  /(?:remove-item|rm\s+-r|rmdir\s+\/s|rd\s+\/s|del\s+(?:\/[a-z]+\s+)*\/[a-z]*s[a-z]*|move-item|rename-item|copy-item\s+[^\n]*?(?:-\s*force|overwrite))/i;
@@ -16,6 +18,10 @@ export const SENSITIVE_CMD =
16
18
  export const CONFIG_FILE_RE =
17
19
  /(?:^|[\\/])(?:AGENTS\.md|settings\.yaml|\.credentials\.yaml|workspace\.json|cordis\.patch\.yml|rule-understanding\.json|rule-guard\.json|rule-engine\.json)$/i;
18
20
 
21
+ /** 受保护文件名(命令文本中出现即需警惕;P0-1c 起仅在写类命令中生效) */
22
+ export const PROTECTED_FILENAME_RE =
23
+ /(?:AGENTS\.md|settings\.yaml|\.credentials\.yaml|workspace\.json|cordis\.patch\.yml|rule-understanding\.json|rule-engine\.json)/i;
24
+
19
25
  export const DATA_DIR_RE =
20
26
  /(?:^|[\\/])\.dsh[\\/](?:sessions|storages|\.backups)[\\/]/i;
21
27
 
@@ -145,14 +151,28 @@ function extractCommandWriteTargets(command) {
145
151
  let m;
146
152
  while ((m = flagRe.exec(command))) {
147
153
  const rest = command.slice(m.index + m[0].length);
148
- const toks = absolutePathTokens(rest);
149
- if (toks.length > 0) targets.push(toks[0]);
154
+ // 只取 flag 的“紧邻参数值”(引号包裹或单个 token),不再扫描剩余整条命令——
155
+ // 否则 `-Path $var` 后跟的无关只读路径会被误判为写目标(P0-1 误拦)
156
+ const quoted = rest.match(/^\s*["']([^"']+)["']/);
157
+ let token = null;
158
+ if (quoted) {
159
+ token = quoted[1].trim();
160
+ } else {
161
+ const plain = rest.match(/^\s*([^\s"'`,。;:!?()【】《》、;|&]+)/);
162
+ if (plain) token = plain[1].trim();
163
+ }
164
+ // 只接受绝对路径字面量:变量($x / %x%)、相对路径无法可靠解析 → 跳过(不误判)
165
+ if (token && /^[a-z]:[\\/]/i.test(token)) {
166
+ if (!isExecutableToken(token)) targets.push(token);
167
+ }
150
168
  }
151
169
  const redirRe = /(?:^|[\s>])(?:>>|>)\s*["']?([A-Za-z]:[\\/][^"';\s]+)/g;
152
170
  while ((m = redirRe.exec(command))) {
153
171
  targets.push(m[1].trim());
154
172
  }
155
173
  if (targets.length > 0) return [...new Set(targets)];
174
+ // flag 未解析出目标时:命令含变量则无法可靠推断 → 返回空(宁可不拦,不误拦)
175
+ if (/\$[A-Za-z_][A-Za-z0-9_]*|%\w+%|\$\([^)]*\)/.test(command)) return [];
156
176
  const fallback = absolutePathTokens(command);
157
177
  return fallback.length > 0 ? [fallback[0]] : [];
158
178
  }
@@ -208,30 +228,14 @@ export function isGetDateCommand(command) {
208
228
  return typeof command === "string" && /\bget-date\b/i.test(command);
209
229
  }
210
230
 
211
- const PS1_FILE_RE = /\.(?:ps1|psm1|psd1)(?=[\s'"`]|$)/i;
212
-
213
- /** 判断是否命中规则 9 的「含中文 .ps1 未按 UTF-8 带 BOM」硬拦项 */
214
- export function isChinesePs1Violation(toolName, args) {
215
- const name = String(toolName || "");
216
- const p = pathTarget(args);
217
- const cmd = commandText(args);
218
- const hasCJK = (s) => typeof s === "string" && CJK_RE.test(s);
219
- if ((name === "write" || name === "edit") && p && PS1_FILE_RE.test(p)) {
220
- const content = args?.content ?? args?.new_string ?? "";
221
- if (hasCJK(content)) return true;
222
- }
223
- if ((name === "pwsh" || name === "bash") && cmd && PS1_FILE_RE.test(cmd) && hasCJK(cmd)) {
224
- if (/(?:-Encoding\s+UTF8|utf8)/i.test(cmd)) return false;
225
- return true;
226
- }
227
- return false;
228
- }
231
+ // 注:PS5.1 时代的「含中文 .ps1 必须 UTF-8 带 BOM」硬拦已移除(2026-08-19)——
232
+ // PS7 默认且正确读取 UTF-8 无 BOM 脚本,无需 BOM;规则 9 已同步 PS7 语义。
229
233
 
230
234
  const READ_ONLY_TOOLS = new Set(["read", "grep", "glob", "read_image"]);
231
235
  const READONLY_CMD_RE =
232
236
  /\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)\b/i;
233
237
  const MUTATING_CMD_RE =
234
- /(?: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|>|>>)/i;
238
+ /(?: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;
235
239
 
236
240
  /** 判断命令文本是否只读(读文件/查询类,无写入/删除/提交副作用) */
237
241
  export function isReadOnlyCommand(command) {
@@ -373,7 +377,10 @@ export function isSensitiveToolCall(toolName, args) {
373
377
  if (name === "edit" || name === "write" || name === "pwsh" || name === "bash") return true;
374
378
  }
375
379
  if (name === "pwsh" || name === "bash") {
376
- if (cmd && (SENSITIVE_CMD.test(cmd) || /(?:AGENTS\.md|settings\.yaml|\.credentials\.yaml|workspace\.json|cordis\.patch\.yml|rule-understanding\.json|rule-engine\.json)/i.test(cmd) || commandHasOutsideWrite(cmd))) return true;
380
+ // P0-1c:受保护文件名检查必须配合「写类命令」——纯描述性文本(如 gh release --notes 里的
381
+ // "AGENTS.md" 字样、echo 输出内容)不再触发敏感判定;真写类命令(Set-Content/Remove-Item 等
382
+ // 命中 MUTATING_CMD_RE)提到受保护文件仍判敏感。
383
+ if (cmd && (SENSITIVE_CMD.test(cmd) || (MUTATING_CMD_RE.test(cmd) && PROTECTED_FILENAME_RE.test(cmd)) || commandHasOutsideWrite(cmd))) return true;
377
384
  }
378
385
  if (name === "edit" || name === "write") {
379
386
  if (p && (isProtectedConfigPath(p) || isOutsideWorkspace(p))) return true;
package/lib/core/state.js CHANGED
@@ -1,9 +1,10 @@
1
1
  // state.js - 插件运行时状态(内存态)。
2
2
  // 所有会话级状态以 sessionId 为 key;turn 级状态在 turn/start 重置。
3
- import { statSync } from "node:fs";
3
+ import { readFileSync, statSync } from "node:fs";
4
4
  import { loadRules } from "./parser.js";
5
- import { agentsFilePath } from "./paths.js";
5
+ import { agentsFilePath, disabledRulesFilePath } from "./paths.js";
6
6
  import { understandAll } from "./understander.js";
7
+ import { writeUnderstanding } from "./understanding-store.js";
7
8
  import { AUTH_TTL_MS } from "./authorization.js";
8
9
 
9
10
  const MAX_SESSIONS = 200;
@@ -165,17 +166,35 @@ export function resetTurn(state, sessionId, turnNumber) {
165
166
  return s.turn;
166
167
  }
167
168
 
168
- /** 重新加载并理解 AGENTS.md */
169
+ /** 读取 dsh-rules-manager 的 disabled-rules.json,返回已禁用规则 id 集合(P0-2) */
170
+ export function loadDisabledRuleIds() {
171
+ try {
172
+ const raw = readFileSync(disabledRulesFilePath(), "utf8");
173
+ const list = JSON.parse(raw);
174
+ if (!Array.isArray(list)) return new Set();
175
+ return new Set(list.map((d) => String(d?.index)).filter(Boolean));
176
+ } catch {
177
+ return new Set();
178
+ }
179
+ }
180
+
181
+ /** 重新加载并理解 AGENTS.md;统一在 reload 后刷新理解产物(P0-3) */
169
182
  export function reloadRules(state) {
170
183
  const parsed = loadRules();
171
184
  state.rules = parsed.ok ? parsed.rules : [];
172
185
  state.configs = understandAll(state.rules);
186
+ // 对接 disabled-rules.json:被禁用规则在引擎侧同样标记 disabled(不参与硬拦/纠察)
187
+ const disabledIds = loadDisabledRuleIds();
188
+ for (const cfg of state.configs) {
189
+ if (disabledIds.has(String(cfg.ruleId))) cfg.disabled = true;
190
+ }
173
191
  state.configOk = parsed.ok;
174
192
  state.configError = parsed.error || null;
175
193
  state.mtimeMs = parsed.mtimeMs || 0;
176
194
  state.loadedAt = Date.now();
177
195
  state.lastMtimeCheck = Date.now();
178
196
  state.reloadCount++;
197
+ if (parsed.ok) writeUnderstanding(state.configs);
179
198
  return state;
180
199
  }
181
200
 
@@ -65,7 +65,7 @@ function splitKeywords(text) {
65
65
  function hintPatterns(checkText) {
66
66
  const hints = [];
67
67
  if (/node\s+-e|node\s+-p|pwsh\s+-c|--eval|--print|-Command\b/i.test(checkText)) hints.push("inline-command");
68
- if (/set-content|out-file|add-content|writealltext|utf8/i.test(checkText)) hints.push("bom-write");
68
+ if (/set-content|out-file|add-content|writealltext|utf8bom/i.test(checkText)) hints.push("bom-write");
69
69
  if (/ask_user_question|授权|弹框/i.test(checkText)) hints.push("ask");
70
70
  if (/get-date|时间词|昨天|今天/i.test(checkText)) hints.push("time");
71
71
  if (/skill|技能/i.test(checkText)) hints.push("skill");
@@ -60,6 +60,19 @@ function isVersionRenumber(oldString, newString) {
60
60
  return m1[1] === m2[1] && m1[2] === m2[2];
61
61
  }
62
62
 
63
+ /** 判断是否为“同一行内仅版本号数字变更”(如徽章 version-1.4.3 → version-1.4.7)——
64
+ * 前后缀完全一致、仅中间版本号不同 → 合法更新,不是覆盖上一行(文档审计发现:徽章行更新被误拦) */
65
+ function isInlineVersionChange(oldString, newString) {
66
+ if (typeof oldString !== "string" || typeof newString !== "string") return false;
67
+ if (!isSingleLine(oldString) || !isSingleLine(newString)) return false;
68
+ if (isTableRow(oldString) || isTableRow(newString)) return false;
69
+ const re = /^([\s\S]*?)(\d+\.\d+(?:\.\d+)?)([\s\S]*)$/;
70
+ const m1 = oldString.match(re);
71
+ const m2 = newString.match(re);
72
+ if (!m1 || !m2) return false;
73
+ return m1[1] === m2[1] && m1[3] === m2[3] && m1[2] !== m2[2];
74
+ }
75
+
63
76
  /** 判断是否单行文本(不含换行) */
64
77
  function isSingleLine(s) {
65
78
  return !s.includes("\n") && !s.includes("\r");
@@ -120,6 +133,7 @@ export function validateEditAppend(oldString, newString) {
120
133
  if (typeof oldString !== "string" || typeof newString !== "string") return { ok: true, errors: [] };
121
134
  if (oldString.length === 0) return { ok: true, errors: [] };
122
135
  if (isVersionRenumber(oldString, newString)) return { ok: true, errors: [] };
136
+ if (isInlineVersionChange(oldString, newString)) return { ok: true, errors: [] };
123
137
  if (newString.includes(oldString)) return { ok: true, errors: [] };
124
138
  if (oldString.includes(newString)) return { ok: true, errors: [] };
125
139
  if (isSameLineReplacement(oldString, newString)) return { ok: true, errors: [] };
package/lib/index.js CHANGED
@@ -53,8 +53,8 @@ export const inject = ["tools", "commands", "agents", "workspaceRegistry", "skil
53
53
 
54
54
  const pluginConfig = loadPluginConfig();
55
55
  state.enabled = pluginConfig.enabled;
56
+ // reloadRules 内部已统一刷新理解产物(P0-3),此处不再重复写
56
57
  reloadRules(state);
57
- writeUnderstanding(state.configs);
58
58
 
59
59
  // ── 工具函数 ────────────────────────────────────────────────────────────────
60
60
 
@@ -575,7 +575,8 @@ export function apply(ctx) {
575
575
  clearTimeout(timer);
576
576
  timer = setTimeout(() => {
577
577
  reloadRules(state);
578
- writeUnderstanding(state.configs);
578
+ // 规则变化后补一次 LLM 增量理解(有 ruleId@mtime 去重,不会重复烧 token;P1-6)
579
+ void enrichRulesWithLlm(ctx, state);
579
580
  }, 200);
580
581
  });
581
582
  } catch {
package/lib/service.js CHANGED
@@ -94,7 +94,8 @@ class RuleEngineService extends TypertRemoteService {
94
94
  try {
95
95
  const current = currentVersion();
96
96
  const headers = { "User-Agent": "dsh-rule-engine", Accept: "application/vnd.github+json" };
97
- const releaseRes = await fetch("https://api.github.com/repos/jilian-dsh/dsh-rule-engine/releases/latest", { headers });
97
+ // P2-8:网络异常/慢速时避免面板挂起
98
+ const releaseRes = await fetch("https://api.github.com/repos/jilian-dsh/dsh-rule-engine/releases/latest", { headers, signal: AbortSignal.timeout(8000) });
98
99
  if (!releaseRes.ok) {
99
100
  return { ok: false, error: `GitHub Release 检查失败:HTTP ${releaseRes.status}` };
100
101
  }
@@ -104,7 +105,7 @@ class RuleEngineService extends TypertRemoteService {
104
105
 
105
106
  let impacts = [];
106
107
  try {
107
- const impactRes = await fetch("https://raw.githubusercontent.com/jilian-dsh/dsh-rule-engine/main/upgrade-impact.json", { headers: { "User-Agent": "dsh-rule-engine" } });
108
+ const impactRes = await fetch("https://raw.githubusercontent.com/jilian-dsh/dsh-rule-engine/main/upgrade-impact.json", { headers: { "User-Agent": "dsh-rule-engine" }, signal: AbortSignal.timeout(8000) });
108
109
  if (impactRes.ok) {
109
110
  const data = await impactRes.json();
110
111
  if (Array.isArray(data?.versions)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-rule-engine",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "DSH 规则执行引擎 v3:容器解析 AGENTS.md + 理解器 + 匹配机 + 执行框架",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -38,6 +38,7 @@
38
38
  "audit:mount": "node scripts/audit-mount-consistency.mjs --profile web"
39
39
  },
40
40
  "peerDependencies": {
41
+ "@deepseek-ai/dsh-home-paths": ">=0.1.0-rc.3",
41
42
  "@deepseek-ai/dsh-typert-protocol": ">=0.1.0-rc.3"
42
43
  },
43
44
  "dsh": {