dsh-rule-engine 0.6.1 → 0.6.3
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 +164 -4
- package/lib/core/audit.js +21 -4
- package/lib/core/authorization.js +18 -29
- package/lib/core/config.js +4 -1
- package/lib/core/dualtrack-markers.js +61 -0
- package/lib/core/guard-core.js +96 -16
- package/lib/core/intent.js +15 -22
- package/lib/core/lang.js +61 -0
- package/lib/core/lexicon.js +177 -72
- package/lib/core/llm-intent.js +2 -2
- package/lib/core/matcher.js +18 -9
- package/lib/core/patterns.js +346 -48
- package/lib/core/quality-ledger.js +124 -0
- package/lib/core/state.js +39 -3
- package/lib/core/text-detect.js +126 -68
- package/lib/core/understander.js +9 -9
- package/lib/index.js +119 -6
- package/lib/messages.js +134 -0
- package/lib/service.js +7 -2
- package/package.json +5 -4
- package/scripts/check-tool-coverage.mjs +3 -1
- package/scripts/dualtrack-check.mjs +335 -0
- package/scripts/dualtrack-whitelist.json +7 -0
- package/scripts/local-residue-scan.mjs +10 -4
- package/scripts/plugins.json +10 -0
- package/scripts/release-plugin.mjs +35 -5
- package/scripts/verify-all.mjs +4 -0
- package/scripts/verify-v474.mjs +8 -8
package/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# dsh-rule-engine
|
|
2
2
|
|
|
3
3
|

|
|
4
|
-

|
|
5
5
|
|
|
6
6
|
DSH 规则执行引擎 v3 的插件实现。它把 `~/.dsh/AGENTS.md` 当作唯一真相源,自动解析规则四要素与执行等级,再通过「工具守卫 + 文本检测 + 时序检查 + 审计台账」执行用户规则,而不是内置一套与用户无关的安全清单。
|
|
7
7
|
|
|
8
|
-
> 当前版本 **0.6.0
|
|
8
|
+
> 当前版本 **0.6.3**(0.1.2-rc.1 适配:Remote 新合同(typert-protocol `Remote(undefined, …)` 形态,typeof-null 陷阱修复)+ peer 锚扩 `>=0.1.2-rc.0`;白名单 3 轮扩充(gh 只读态 / `gh api` 只读 / `git ls-remote` / `cmdkey /list` + Do- 结构词回归修复);E7 豁免预插(release-plugin bump 后自动追加 pnpm minimumReleaseAgeExclude——⑬ 口径防发布后红灯窗口,单源 `scripts/lib/pnpm-exempt.mjs`))。本插件面向"规则机器化执行":规则写在 AGENTS.md 里,引擎负责让它们真的被遵守;所有规则动态解析,规则增删改后无需重写插件。
|
|
9
9
|
|
|
10
10
|
## 项目背景
|
|
11
11
|
|
|
@@ -70,6 +70,151 @@ DSH 规则执行引擎 v3 的插件实现。它把 `~/.dsh/AGENTS.md` 当作唯
|
|
|
70
70
|
|
|
71
71
|
**迁移(0.5.x → 0.6.0)**:0.6.0 前默认生效的守卫在升级后**不再默认激活**——需要本机行为(统一入口保护 / 手册豁免 / M8 记忆链 / 追加受保护文件)时,按上表把配置段并入本机 `rule-engine.json`。**通用用户无需任何配置**;未配置时相关守卫路径不存在,直写任意文件不受引擎限制(这是 0.6.0 的设计决定:发布物无权限制其他用户的写入方式;本机约定属于本机配置,不属于通用引擎)。
|
|
72
72
|
|
|
73
|
+
## 词表配置(lexicons / patterns / criticismPersonal / dualtrack)
|
|
74
|
+
|
|
75
|
+
引擎的行为词表与检测正则**全部可配置**:代码里只留机制与语言无关的内置默认,中文/本机词表放在 `rule-engine.json`。
|
|
76
|
+
|
|
77
|
+
### 分界:通用层 vs 个人层
|
|
78
|
+
|
|
79
|
+
| 层 | 在哪 | 内容 | 谁维护 |
|
|
80
|
+
|---|---|---|---|
|
|
81
|
+
| **通用层** | `lib/` 代码(随包发布) | 机制 + 语言无关最小集(英文核心词) | 插件作者 |
|
|
82
|
+
| **个人层** | `rule-engine.json` | 你的语言/习惯词表(如中文) | 你 |
|
|
83
|
+
|
|
84
|
+
**Override 语义**(三个键一致):
|
|
85
|
+
|
|
86
|
+
- 键**缺省**(不写)→ 用内置默认(通用最小集);
|
|
87
|
+
- 键**存在** → 按键**完全替换**(不做合并,配置即真相);
|
|
88
|
+
- 写 `{}` → 回退内置默认;**删键** → 同样回退。
|
|
89
|
+
|
|
90
|
+
> 配置在插件启动时读取。改完保存后**重载插件**(或重启 DSH)生效。
|
|
91
|
+
|
|
92
|
+
### 三个配置键
|
|
93
|
+
|
|
94
|
+
#### 1. `lexicons` —— 行为词表(11 键)
|
|
95
|
+
|
|
96
|
+
判定“用户这句话是什么意思”用的词表:
|
|
97
|
+
|
|
98
|
+
| 键 | 作用 |
|
|
99
|
+
|---|---|
|
|
100
|
+
| `approval` | 执行许可词(确认/同意/可以…) |
|
|
101
|
+
| `exec_follow` | 执行语(即可/现在/马上…) |
|
|
102
|
+
| `approval_exec` | 派生:许可词 + 16 字内 + 执行语(可显式覆盖) |
|
|
103
|
+
| `plan_only` | 纯方案/理解确认(不算授权) |
|
|
104
|
+
| `action_words` | 执行动作词(意图判定与授权判定共用) |
|
|
105
|
+
| `strong_exec` | 强执行语(直接执行指令) |
|
|
106
|
+
| `directive` | 指令式动作词 |
|
|
107
|
+
| `rejection` | 拒绝词 |
|
|
108
|
+
| `question` | 疑问词 |
|
|
109
|
+
| `status_signal` | 状态信号词(“我已重启”) |
|
|
110
|
+
| `dangerous_action` | 危险动作词(命中则不算状态信号) |
|
|
111
|
+
|
|
112
|
+
```json
|
|
113
|
+
{
|
|
114
|
+
"lexicons": {
|
|
115
|
+
"action_words": "执行|跑|落盘|重构|部署",
|
|
116
|
+
"question": "[??]|吗|为什么|怎么"
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
#### 2. `patterns` —— 检测正则(正则键 + 映射键 + 数值键)
|
|
122
|
+
|
|
123
|
+
文本健康检测(规则 2 时间 / 5 来源 / 7 承诺 / 11 术语 / 16 建议 / 19 / 21 / 22 / 23 / 27 / 31)与规则激活词。
|
|
124
|
+
|
|
125
|
+
- **正则键**:值是正则 source 字符串,如 `"time_words": "今天|昨天|刚才"`;
|
|
126
|
+
- **映射键** `self_cert_hints`:值是对象 `{ "14": "总结|汇报", "31": "撞墙|盲试" }`(键=规则编号);
|
|
127
|
+
- **数值键** `criticism_caps_ratio`:0–1 的阈值(英文全大写比率,默认 0.6)。
|
|
128
|
+
|
|
129
|
+
```json
|
|
130
|
+
{
|
|
131
|
+
"patterns": {
|
|
132
|
+
"time_words": "今天|昨天|刚才",
|
|
133
|
+
"promise_words": "包在我身上|肯定能|万无一失",
|
|
134
|
+
"self_cert_hints": { "14": "总结|汇报|完成" },
|
|
135
|
+
"criticism_caps_ratio": 0.6
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
#### 3. `criticismPersonal` —— 本机辱骂词枚举(数组)
|
|
141
|
+
|
|
142
|
+
批评检测的**个人词表**(通用层只留语言无关形态:连续问号/叹号、全大写比率)。
|
|
143
|
+
|
|
144
|
+
```json
|
|
145
|
+
{ "criticismPersonal": ["示例词一", "示例词二"] }
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
#### 4. `dualtrack` —— 分层残留词表(**发布者私有**)
|
|
149
|
+
|
|
150
|
+
它不是行为词表,而是**发布门禁**用的「我这台机器长什么样」清单:`dualtrack-check`(B3 层)与 `local-residue-scan`(B2 层)用它扫 `lib/`,看有没有夹带本机信息(用户名、本机路径、私人概念等)。
|
|
151
|
+
|
|
152
|
+
| 键 | 作用 |
|
|
153
|
+
|---|---|
|
|
154
|
+
| `markers` | 本机标识词表(字符串数组,每项一个待扫串) |
|
|
155
|
+
| `whitelist` | 本机豁免(`files` 整文件 / `strings` 精确串)——与包内 `scripts/dualtrack-whitelist.json` 合并生效 |
|
|
156
|
+
|
|
157
|
+
```json
|
|
158
|
+
{
|
|
159
|
+
"dualtrack": {
|
|
160
|
+
"markers": ["<你的用户名>", "<你的本机路径片段>"],
|
|
161
|
+
"whitelist": { "files": [], "strings": [] }
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
**加载优先级**(取先命中者):`rule-engine.json` 的 `dualtrack.markers` → 环境变量 `DUALTRACK_MARKERS` 指向的文件(每行一个,`#` 开头为注释)→ 包内 `scripts/local-residue-markers.txt`(仅示例)。
|
|
167
|
+
|
|
168
|
+
> **三者皆空 = REFUSED**(fail-closed):空词表不会被当成「没有残留」,而是直接拒绝执行——**空 txt 文件同样不作为词表源**。闸本身怎么跑,见「开发与测试」章节的「分层残留闸(dualtrack)」。
|
|
169
|
+
|
|
170
|
+
### 「加一个词」操作路径
|
|
171
|
+
|
|
172
|
+
1. 打开 `~/.dsh/rule-engine.json`(或你 DSH_HOME 下的同名文件);
|
|
173
|
+
2. 在 `lexicons` / `patterns` 里找到对应键,用 `|` 追加词(正则元字符需转义,如 `\\.`);
|
|
174
|
+
3. 保存 → 重载插件(或重启 DSH);
|
|
175
|
+
4. 想回滚:删掉该键(或整个键值)→ 恢复内置默认。
|
|
176
|
+
|
|
177
|
+
> **验证**:`/guard status` 查看配置加载是否正常;非法正则/未知键会在启动时记审计(`/guard log` 可查),**不会静默半套生效**。
|
|
178
|
+
|
|
179
|
+
## 质量账本(可选,默认关)
|
|
180
|
+
|
|
181
|
+
给**每个安装者**的私有质量统计:同类任务做得多了,能看出返工率是升是降。
|
|
182
|
+
|
|
183
|
+
### 是什么
|
|
184
|
+
|
|
185
|
+
| 层 | 内容 | 归属 |
|
|
186
|
+
|---|---|---|
|
|
187
|
+
| 机制 | `lib/core/quality-ledger.js`(`taskSignature` / `recordQuality` / `qualityTrend`) | 随包发布 |
|
|
188
|
+
| 数据 | `~/.dsh/quality-ledger.jsonl`(每单一行) | **本机数据,永不发布** |
|
|
189
|
+
| 配置 | `rule-engine.json` 的 `qualityLedger` 键 | 你的配置 |
|
|
190
|
+
|
|
191
|
+
### 怎么开
|
|
192
|
+
|
|
193
|
+
```json
|
|
194
|
+
{ "qualityLedger": { "enabled": true, "window": 5 } }
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
- `enabled` **默认 false**——不开**不会产生任何文件**;
|
|
198
|
+
- `window`:趋势比较窗口(最近 N 单 vs 之前 N 单,默认 5)。
|
|
199
|
+
|
|
200
|
+
### 怎么看
|
|
201
|
+
|
|
202
|
+
按签名查询趋势(同签名 = 同类任务):
|
|
203
|
+
|
|
204
|
+
```js
|
|
205
|
+
import { taskSignature, qualityTrend } from "dsh-rule-engine/lib/core/quality-ledger.js";
|
|
206
|
+
const sig = taskSignature("给插件加一个设置项", ["npm test 全绿"]);
|
|
207
|
+
console.log(qualityTrend(sig).summary); // 方向:rework 改善 / 持平 / 恶化
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
每单记录的指标:`rework`(返工数)、`interventions`(介入数)、`frictions`(摩擦数=引擎拦截次数)、`tokens`。
|
|
211
|
+
|
|
212
|
+
### 隐私说明
|
|
213
|
+
|
|
214
|
+
**落盘的是单向指纹,不是原文。** 签名 = sha256(归一化内容) 取前 12 位;归一化规则(去引号 → 绝对路径替换为 `<path>` → 数字替换为 `<n>` → 折叠空白 → 小写)在机制层公开可审计。即使 `quality-ledger.jsonl` 被人看到,也只能知道「有个 12 位指纹的同类任务做过 N 次、返工趋势如何」,**看不出任务内容**。
|
|
215
|
+
|
|
216
|
+
> 本机制**不拦截、不评分、不上传**——纯旁路统计。
|
|
217
|
+
|
|
73
218
|
## 注入噪音治理(0.5.6 / 0.5.7)
|
|
74
219
|
|
|
75
220
|
只提醒真正值得提醒的事——这条原则贯穿 0.5.6 与 0.5.7:
|
|
@@ -96,7 +241,11 @@ DSH 规则执行引擎 v3 的插件实现。它把 `~/.dsh/AGENTS.md` 当作唯
|
|
|
96
241
|
> 更早版本(0.1.0-0.5.5)与本机历史要点见 git 历史;各版本内部"用户定稿"等决策细节不再随发布物携带。
|
|
97
242
|
|
|
98
243
|
| 版本 | 日期 | 要点 |
|
|
244
|
+
| **0.6.3** | 2026-09-09 | docs(readme): 词表配置章节补齐 dualtrack(markers/whitelist/加载优先级/空表 REFUSED) |
|
|
245
|
+
| **0.6.2** | 2026-09-08 | fix(0.6.2): whitelist 3-round + 0.1.2 adaptation + peer-anchor 0.1.2 + E7 pre-plug wiring |
|
|
99
246
|
|---|---|---|
|
|
247
|
+
| **0.6.1+(本地增强,未发版)** | 2026-09-07 | 只读豁免清单扩充(用户定调"纯只读顺畅"):`gh api` 只读态(无 -X/--method/graphql/-F/-f 的段)、`gh release/issue/pr/run view`、`cmdkey /list`、`git ls-remote` 显式;写形态(gh api -F/graphql、git fetch、curl 下载、重定向落盘)保持拦截;26 用例全绿 + 语法体检 + 热重载生效(发版需 bump 0.6.2) |
|
|
248
|
+
| **0.6.1** | 2026-09-07 | **B 档发布**:豁免预插(release-plugin bump 后自动追加 minimumReleaseAgeExclude——⑬ 口径防发布后红灯窗口);豁免判定单源化(scripts/lib/pnpm-exempt.mjs 与 verify-all ⑬ 共享 + 4 单测);viewFails 发布语境 STRICT 计 ❌;⑬ 头注释绝对口径 |
|
|
100
249
|
| **0.6.0** | 2026-09-04 | **行为变更**:通用与本机分离——`localIntegrations` 本机集成层(entryScript/protectedFiles/m8/manualExempt 四键);此前默认强制的守卫(统一入口阶段 C / 手册/技能豁免 / M8 双通道)改为"配置存在=守卫存在、无配置=代码路径上不存在";`m8` 语义反转(默认开启→显式开启);消号本机痕迹(lib/ 零命中,词表唯一源 `scripts/local-residue-markers.txt`) |
|
|
101
250
|
| **0.5.14** | 2026-09-01 | 分点三柱(条件句零授权/显式命名对象锚定/clauseId 隔离)+ skill 词收紧 + 规则 5 引证检测扩展(内部引用无依据→审计注入)+ 规则 31 查证纪律(B+D)+ README 版本四性对齐 |
|
|
102
251
|
| **0.5.17** | 2026-09-03 | A1 规则 2 时间词拆组(当下词=Get-Date① / 历史日期=证据锚②,消除"引用历史日期必判未核对"误报)+ EVIDENCE_MARK_RE 增证据锚(commit hash/版本行/踩坑 N/版本记录) |
|
|
@@ -111,9 +260,14 @@ DSH 规则执行引擎 v3 的插件实现。它把 `~/.dsh/AGENTS.md` 当作唯
|
|
|
111
260
|
| **0.5.7** | 2026-08-26 | 注入噪音治理(词表只产嫌疑 + LLM 裁决 + fail-closed + 投递资格闸 + 审计完整性)、注入通道重入修复(宏任务投递)、语义层(awaitingJudge/judge-pass/false/unavailable) |
|
|
112
261
|
| **0.5.6** | 2026-08-26 | 同回复聚合注入、已自证规则不重复触发、C2 规则统计(detected/suppressed/injected) |
|
|
113
262
|
|
|
114
|
-
## 发行固定源
|
|
263
|
+
## 发行固定源
|
|
264
|
+
|
|
265
|
+
<!-- fixed-source: 待发布回填 -->
|
|
115
266
|
|
|
116
|
-
- **0.6.
|
|
267
|
+
- **0.6.3(当前)** 固定于 main Commit `68c1e7d`(`git checkout 68c1e7d` + 发布 bump 可复现 npm `dsh-rule-engine@0.6.3` 与 GitHub Release v0.6.3 同源代码——0.6.3 = 分层残留闸 `dualtrack-check`(判据 A:中文≠个人化)+ 词表全量配置化(lexicons / patterns / criticismPersonal / dualtrack 走 `rule-engine.json`)+ 第三批第 1 批文案层 + 三项门禁修复(`--init` 覆盖保护 / loader-smoke 中文夹具 / 中文目录顿号兼容)。**注:发布后回填真实 release 提交 hash。**
|
|
268
|
+
- **0.6.2** 固定于 main Commit `92da194538fce2f56fa7c6712c70711865772686`(`git checkout 92da194538fce2f56fa7c6712c70711865772686` 可复现 npm `dsh-rule-engine@0.6.2` 与 GitHub Release v0.6.2 同源代码——0.6.2 = 0.1.2-rc.1 适配(Remote 新合同 `Remote(undefined, …)` / peer 锚扩 `>=0.1.2-rc.0`)+ 白名单 3 轮扩充 + Do- 结构词回归修复 + E7 豁免预插机制上线)。
|
|
269
|
+
- **0.6.1** 固定于 main Commit `051e2da`(`git checkout 051e2da` 可复现 npm `dsh-rule-engine@0.6.1` 与 GitHub Release v0.6.1 同源代码——0.6.1 = 豁免预插(release-plugin bump 后自动追加 pnpm minimumReleaseAgeExclude,⑬ 绝对口径防发布后红灯窗口——踩坑 18 镜像)+ 豁免判定单源化(scripts/lib/pnpm-exempt.mjs 与 verify-all ⑬ 共享,4 单测锁定)+ viewFails 发布语境 STRICT 计 ❌ + ⑬ 块头注释绝对口径(E1/E2/E3 收尾批)。
|
|
270
|
+
- **0.6.0** 固定于 main Commit `be5b8c93`(可复现 `dsh-rule-engine@0.6.0` 与 Release v0.6.0——0.6.0 = 本机集成层(localIntegrations 四键)+ 本机痕迹消号 + li-skipped/entry-script-missing 启动审计 + 发布门禁 B1/B2(readme-version-check / local-residue-scan,挂 verify-all/release-plugin/check:meta)+ check-tool-coverage 素材 fail-closed;词表文件 `scripts/local-residue-markers.txt` 为本机门禁工具,不入库、不进发布物(见 .gitignore / package.json files 排除)。固定源之后的提交仅限 README 指针文本)。
|
|
117
271
|
|
|
118
272
|
## 任务契约与反过度工程(可选)
|
|
119
273
|
|
|
@@ -261,6 +415,12 @@ bash scripts/build.sh
|
|
|
261
415
|
|
|
262
416
|
交付前体检(0.5.7 起):`node scripts/verify-all.mjs`(七层:语法/单元/组合冒烟/工具箱覆盖/守卫链覆盖/关联一致性/真实判例)与 `node scripts/health-audit.mjs`(找茬)——详见「质量与验证」。
|
|
263
417
|
|
|
418
|
+
**分层残留闸(dualtrack)**:`node scripts/dualtrack-check.mjs` 扫描 `lib/` 是否混入发布者私有内容(本机标识 / 个人规则描述),棘轮式**只许降不许升**;已挂 `npm run check:meta` 与 `verify-all.mjs` 第 ⑩′ 层自动调用。基线 `scripts/dualtrack-baseline.json` **随 git 仓库提供、不进 npm 包**:
|
|
419
|
+
|
|
420
|
+
- **从 git clone 的贡献者无需任何操作**(基线已在仓库里)——**勿在有基线时跑 `--init`**,它会把当前计数覆盖为基线、棘轮当场失效;
|
|
421
|
+
- **仅当基线缺失时**(如从 npm 包解压后跑门禁、或基线被删)首次运行 `node scripts/dualtrack-check.mjs --init` 生成;
|
|
422
|
+
- 词表(哪些字符串算「本机残留」)来自 `rule-engine.json` 的 `dualtrack.markers` → `DUALTRACK_MARKERS` 环境变量 → 包内示例;三者皆空时 **REFUSED**(fail-closed,不会「以为配好了其实没配」)。
|
|
423
|
+
|
|
264
424
|
发布:`node scripts/release-plugin.mjs <插件名> <版本号>`(一键 npm + git + GitHub Release;发布脚本随插件仓库管理——`scripts/release-plugin.mjs`;改发布脚本后须 `--dry-run` + 代码审查,注意 dry-run 不覆盖 git 段)。
|
|
265
425
|
|
|
266
426
|
## License
|
package/lib/core/audit.js
CHANGED
|
@@ -5,11 +5,14 @@
|
|
|
5
5
|
// B2(2026-09-03):判定来源可溯源字段约定(判例回灌前置)——
|
|
6
6
|
// verdictSource: "guard"(工具守卫 intercept,机器判定)| "lexicon"(词表直判/降级)
|
|
7
7
|
// | "judge"(LLM 裁决分流)| "llm-intent"(意图 LLM 兜底);缺失 = 历史记录(无此字段)。
|
|
8
|
-
import { appendFileSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { appendFileSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
9
10
|
import { randomUUID } from "node:crypto";
|
|
10
11
|
import { auditFilePath } from "./paths.js";
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
// 审计归档(2026-09-08 第二批 §1.1-②):上限 512KB→2MB;超限时被裁头部先写入
|
|
14
|
+
// <dshHome>/logs/rule-engine-archive/log-YYYYMM.jsonl(按月文件)再保留尾部——历史可回溯。
|
|
15
|
+
const LOG_MAX_BYTES = 2 * 1024 * 1024;
|
|
13
16
|
const LOG_KEEP_LINES = 400;
|
|
14
17
|
const APPEND_TRIM_INTERVAL = 32;
|
|
15
18
|
|
|
@@ -29,13 +32,27 @@ export function audit(entry) {
|
|
|
29
32
|
}
|
|
30
33
|
}
|
|
31
34
|
|
|
35
|
+
/** 被裁头部写入月归档(失败静默——审计裁剪本身容错) */
|
|
36
|
+
function archiveTrimmed(headLines) {
|
|
37
|
+
try {
|
|
38
|
+
const dir = join(dirname(auditFilePath()), "logs", "rule-engine-archive");
|
|
39
|
+
mkdirSync(dir, { recursive: true });
|
|
40
|
+
const ym = new Date().toISOString().slice(0, 7).replace("-", "");
|
|
41
|
+
appendFileSync(join(dir, `log-${ym}.jsonl`), headLines.join("\n") + "\n", "utf8");
|
|
42
|
+
} catch {
|
|
43
|
+
// 归档失败忽略
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
32
47
|
function trimLog() {
|
|
33
48
|
try {
|
|
34
49
|
const p = auditFilePath();
|
|
35
50
|
if (statSync(p).size <= LOG_MAX_BYTES) return;
|
|
36
51
|
const lines = readFileSync(p, "utf8").split("\n");
|
|
37
|
-
const
|
|
38
|
-
|
|
52
|
+
const keptLines = lines.slice(-LOG_KEEP_LINES);
|
|
53
|
+
const head = lines.slice(0, Math.max(0, lines.length - LOG_KEEP_LINES)).filter(Boolean);
|
|
54
|
+
if (head.length) archiveTrimmed(head);
|
|
55
|
+
writeFileSync(p, keptLines.join("\n"), "utf8");
|
|
39
56
|
} catch {
|
|
40
57
|
// 裁剪失败忽略
|
|
41
58
|
}
|
|
@@ -1,36 +1,25 @@
|
|
|
1
1
|
// authorization.js - 授权证据结构化匹配(P0:授权关联具体操作,区分询问与授权)
|
|
2
2
|
// 纯函数,可独立测试。
|
|
3
3
|
import { GENERIC_EXEC_TOOLS, pathTarget, commandText, writeTargetPathsFromCommand, extractCopyPaths } from "./patterns.js";
|
|
4
|
-
import {
|
|
4
|
+
import { questionWordsRe } from "./lexicon.js";
|
|
5
5
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
ACTION_WORDS_RE
|
|
6
|
+
approvalExecRe,
|
|
7
|
+
directiveWordsRe,
|
|
8
|
+
rejectionWordsRe,
|
|
9
|
+
planOnlyRe,
|
|
10
|
+
actionWordsRe
|
|
12
11
|
} from "./lexicon.js";
|
|
13
12
|
|
|
14
13
|
export const AUTH_TTL_MS = 10 * 60 * 1000;
|
|
15
14
|
|
|
16
15
|
// 0.5.11(用户拍板):词表唯一源 = lexicon.js——授权判定所用的许可词/指令词/拒绝词/动作词
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
//
|
|
23
|
-
const DIRECTIVE_RE = DIRECTIVE_WORDS_RE;
|
|
24
|
-
// 纯批准词(选项/自由文本判定用)
|
|
16
|
+
// 一律从那里取;本文件不再自持定义。
|
|
17
|
+
// P8 小批 A(2026-09-08):词表改经函数取(approvalExecRe() 等)——配置层注入后立即生效;
|
|
18
|
+
// 顶层 `const X = SOME_RE` 缓存会锁死内置值,故本文件不再保留任何正则别名。
|
|
19
|
+
// 遗留(本批范围外,如实记账):下面这份 APPROVAL_RE 是本文件自持的第二份许可词定义,
|
|
20
|
+
// 与 lexicon.approval 存在漂移(多"是/执行/去吧/开始"、少"行")——本批保持行为等价不动,
|
|
21
|
+
// 是否合并/迁入配置待第三方裁定(迁入会改变判定结果,不属"等价迁移")。
|
|
25
22
|
const APPROVAL_RE = /(?:允许|同意|可以|是|好|确认|授权|执行|批准|去吧|开始|ok|yes)/i;
|
|
26
|
-
const REJECTION_RE = REJECTION_WORDS_RE;
|
|
27
|
-
|
|
28
|
-
// 0.5.11:执行许可确认词唯一源(lexicon.js APPROVAL_EXEC_RE,组合=许可词[0,16]执行语);
|
|
29
|
-
// 纯"确认方案/理解/方向"不构成(PLAN_ONLY_RE 亦唯一源)。
|
|
30
|
-
|
|
31
|
-
// 选项/自由文本里的明确动作词(用于 ask 结果判定:选择"执行/调整/修复…"即视为批准)
|
|
32
|
-
// 0.5.11:唯一源 = lexicon.js ACTION_WORDS_RE(与 intent 意图判定同表,不再二份)
|
|
33
|
-
const EXEC_ACTION_RE = ACTION_WORDS_RE;
|
|
34
23
|
|
|
35
24
|
const TYPE_HINTS = [
|
|
36
25
|
{ re: /删除|移除|remove|delete/i, type: "delete" },
|
|
@@ -425,31 +414,31 @@ export function describeOp(op) {
|
|
|
425
414
|
|
|
426
415
|
/** 用户消息是否为“询问影响”而非授权(词表与 intent.js 统一,2026-08-24) */
|
|
427
416
|
export function isQuestionMessage(text) {
|
|
428
|
-
return
|
|
417
|
+
return questionWordsRe().test(String(text || ""));
|
|
429
418
|
}
|
|
430
419
|
|
|
431
420
|
/** 用户消息是否包含明确执行许可(确认/同意/授权 + 执行语;且不是询问句) */
|
|
432
421
|
export function isAuthMessage(text) {
|
|
433
422
|
const s = String(text || "");
|
|
434
|
-
return
|
|
423
|
+
return approvalExecRe().test(s) && !isQuestionMessage(s);
|
|
435
424
|
}
|
|
436
425
|
|
|
437
426
|
/** 用户消息是否为直接命令式指令(非询问,视为授权执行) */
|
|
438
427
|
export function isDirectiveMessage(text) {
|
|
439
428
|
const s = String(text || "");
|
|
440
|
-
return
|
|
429
|
+
return directiveWordsRe().test(s) && !isQuestionMessage(s);
|
|
441
430
|
}
|
|
442
431
|
|
|
443
432
|
/** 选项/自由文本是否表示批准(纯"确认方案/理解/方向"不算批准) */
|
|
444
433
|
export function isApprovalText(text) {
|
|
445
434
|
const s = String(text || "");
|
|
446
|
-
if (
|
|
447
|
-
return APPROVAL_RE.test(s) ||
|
|
435
|
+
if (planOnlyRe().test(s) && !actionWordsRe().test(s) && !approvalExecRe().test(s)) return false;
|
|
436
|
+
return APPROVAL_RE.test(s) || actionWordsRe().test(s);
|
|
448
437
|
}
|
|
449
438
|
|
|
450
439
|
/** 选项/自由文本是否表示拒绝 */
|
|
451
440
|
export function isRejectionText(text) {
|
|
452
|
-
return
|
|
441
|
+
return rejectionWordsRe().test(String(text || ""));
|
|
453
442
|
}
|
|
454
443
|
|
|
455
444
|
/** 从 ask_user_question 的 arguments 中提取问题文本(用于推断授权范围) */
|
package/lib/core/config.js
CHANGED
|
@@ -32,7 +32,10 @@ export const DEFAULT_CONFIG = {
|
|
|
32
32
|
handlerOverrides: {},
|
|
33
33
|
// 残余1 剥离(2026-08-31):本机默认偏好表下沉配置——{ ruleId: handlerName }
|
|
34
34
|
// 通用发布=空(代码零本机编号);本机在此写入 17 条偏好,引擎升级不丢。
|
|
35
|
-
handlerDefaultMap: {}
|
|
35
|
+
handlerDefaultMap: {},
|
|
36
|
+
// §1.3(2026-09-08 第二批):本机辱骂词表(个人话术,发布面不含)——空=不启用;
|
|
37
|
+
// 通用层只保留语言无关形态检测(连续问叹号/全大写比率),语义判定归 LLM。
|
|
38
|
+
criticismPersonal: []
|
|
36
39
|
};
|
|
37
40
|
|
|
38
41
|
export function loadPluginConfig() {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// dualtrack-markers.js - 本机标识词表的**唯一加载器**(B2 扫描器与 dualtrack-check 共用)
|
|
2
|
+
//
|
|
3
|
+
// 设计(第三批第 1 批 · 词表配置化,用户拍板 2026-09-09):
|
|
4
|
+
// 词表是**发布者私有数据**——「我这台机器长什么样」对别人毫无意义,因此:
|
|
5
|
+
// ① 优先读本机 rule-engine.json 的 `dualtrack.markers`(字符串数组);
|
|
6
|
+
// ② 其次读环境变量 DUALTRACK_MARKERS 指向的文件(每行一个,`#` 注释);
|
|
7
|
+
// ③ 最后回退包内 scripts/local-residue-markers.txt(**示例**,发布者应换成本机配置)。
|
|
8
|
+
// 包内那份在公开仓库里只保留示例行,避免把发布者的用户名/路径公开出去。
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import os from "node:os";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
|
|
13
|
+
/** 解析 DSH_HOME(与 dsh-home-paths 口径一致) */
|
|
14
|
+
export function resolveDshHome() {
|
|
15
|
+
return process.env.DSH_HOME || path.join(os.homedir(), ".dsh");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function readMarkerFile(file) {
|
|
19
|
+
if (!file || !fs.existsSync(file)) return null;
|
|
20
|
+
const lines = fs.readFileSync(file, "utf8").split(/\r?\n/)
|
|
21
|
+
.map((s) => s.trim())
|
|
22
|
+
.filter((s) => s && !s.startsWith("#"));
|
|
23
|
+
return lines.length > 0 ? lines : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 加载本机标识词表。
|
|
28
|
+
* @param {object} opts
|
|
29
|
+
* @param {string} opts.root 仓库根(回退示例文件所在)
|
|
30
|
+
* @param {object} [opts.config] 已加载的 rule-engine.json 内容(可选;缺省自行读取)
|
|
31
|
+
* @returns {{markers: string[], source: string}}
|
|
32
|
+
*/
|
|
33
|
+
export function loadMarkers(opts = {}) {
|
|
34
|
+
const root = opts.root || process.cwd();
|
|
35
|
+
|
|
36
|
+
// ① 本机配置:rule-engine.json 的 dualtrack.markers
|
|
37
|
+
let cfg = opts.config;
|
|
38
|
+
if (cfg === undefined) {
|
|
39
|
+
try {
|
|
40
|
+
const p = path.join(resolveDshHome(), "rule-engine.json");
|
|
41
|
+
cfg = fs.existsSync(p) ? JSON.parse(fs.readFileSync(p, "utf8")) : null;
|
|
42
|
+
} catch {
|
|
43
|
+
cfg = null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const fromCfg = cfg?.dualtrack?.markers;
|
|
47
|
+
if (Array.isArray(fromCfg)) {
|
|
48
|
+
const markers = fromCfg.filter((x) => typeof x === "string" && x.trim().length > 0).map((x) => x.trim());
|
|
49
|
+
if (markers.length > 0) return { markers, source: "rule-engine.json:dualtrack.markers" };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ② 环境变量指向的本机文件
|
|
53
|
+
const fromEnv = readMarkerFile(process.env.DUALTRACK_MARKERS);
|
|
54
|
+
if (fromEnv) return { markers: fromEnv, source: `env DUALTRACK_MARKERS=${process.env.DUALTRACK_MARKERS}` };
|
|
55
|
+
|
|
56
|
+
// ③ 包内示例文件(发布者应换成本机配置;此处可能只有注释示例)
|
|
57
|
+
const fallback = readMarkerFile(path.join(root, "scripts", "local-residue-markers.txt"));
|
|
58
|
+
if (fallback) return { markers: fallback, source: "scripts/local-residue-markers.txt (in-package example)" };
|
|
59
|
+
|
|
60
|
+
return { markers: [], source: "empty" };
|
|
61
|
+
}
|
package/lib/core/guard-core.js
CHANGED
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
import { existsSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
6
|
+
import { getMessage, hasMessage } from "../messages.js";
|
|
6
7
|
import {
|
|
7
8
|
BOM_WRITE,
|
|
8
9
|
DESTRUCTIVE_CMD,
|
|
9
|
-
|
|
10
|
+
dshKeywordsRe,
|
|
10
11
|
INLINE_CMD,
|
|
11
12
|
PROTECTED_FILENAME_RE,
|
|
12
13
|
commandText,
|
|
@@ -69,17 +70,8 @@ function isDelegatedSession(exec) {
|
|
|
69
70
|
return false;
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
"9": "改用脚本文件或显式 UTF-8 BOM 流程",
|
|
75
|
-
"12A": "先 ask_user_question 获取匹配授权",
|
|
76
|
-
"13A": "先对目标路径执行备份(复制到 .bak/.backups/trash-)",
|
|
77
|
-
"18": "先读取本机配置的手册(路径经 localIntegrations 配置;未配置则该规则无对象)",
|
|
78
|
-
"21": "按规则 21 分级确认后再落盘",
|
|
79
|
-
"22": "先回答/展示方案,或补充明确执行分点(工具类别+路径范围);已授权变更必须落在本回合执行分点/ask 授权范围内",
|
|
80
|
-
"24": "确认插件 dsh.bundle 类型或改用正确挂载",
|
|
81
|
-
"27": "先运行 node scripts/audit-mount-consistency.mjs --profile <p>"
|
|
82
|
-
};
|
|
73
|
+
// 判据 A(2026-09-09):规则号清单**不再硬编码**——放行提示 key 由约定推导
|
|
74
|
+
//(rule-hint.<ruleId>),是否存在由 messages 层决定;规则号变化时改本机配置即可。
|
|
83
75
|
|
|
84
76
|
// 0.5.12(F3):拒绝来源前缀——机器可解析 token 化(/guard log 可按来源 grep 过滤)
|
|
85
77
|
// 约定:[guardian:rule22](规则层)/ [guardian:contract](任务契约/机制类)/ [guardian:guard](守卫层)
|
|
@@ -90,9 +82,97 @@ export function prefixSource(ruleId) {
|
|
|
90
82
|
return `[guardian:rule${ruleId}]`;
|
|
91
83
|
}
|
|
92
84
|
|
|
85
|
+
// ═══════════ 自由面 / 盲区(AGI 清单 P1-2,2026-09-09 第三批)═══════════
|
|
86
|
+
// 显式声明本守卫「不拦什么」(自由面)与「看不见/判不准什么」(盲区)——
|
|
87
|
+
// 目的:被拦的人能一眼看到哪些动作本来就不需要授权(少走弯路),
|
|
88
|
+
// 以及哪些风险本引擎兜不住(别把守卫当安全边界)。
|
|
89
|
+
// 查询:/guard freedom
|
|
90
|
+
/** 自由面:无需授权即可执行的操作类别(守卫不拦)。
|
|
91
|
+
* 第 1 批 1b:内置默认 = **通用骨架**(语言/环境/规则集无关);本机完整版经
|
|
92
|
+
* rule-engine.json 的 guardFreedom 键覆盖(原为硬编码的本机规则体系自描述)。 */
|
|
93
|
+
export const FREEDOM_SURFACE_DEFAULT = [
|
|
94
|
+
"Read-only tools: file reads, searches, status queries, web fetch/search",
|
|
95
|
+
"Scratch-area writes: the analysis scratch and backup directories (scripts, downloads, temp artifacts)",
|
|
96
|
+
"Verification commands: tests, audit scripts, cold-load probes, --dry-run, read-only GETs",
|
|
97
|
+
"Low-risk new files inside the workspace: a plain file that does not exist yet (not a protected name, not an assembly file, not a high-risk entry point)",
|
|
98
|
+
"Asking and presenting: ask_user_question / todo_write / visualize / report",
|
|
99
|
+
"Changes within an authorized scope: this turn has an execution clause and the path/type falls inside it",
|
|
100
|
+
"Escape hatches: /guard unlock (config write protection) and /guard bypass (all guards suspended, audited)"
|
|
101
|
+
];
|
|
102
|
+
/** 盲区:守卫看不见或判不准的(本引擎不是安全边界)。默认 = 通用骨架,本机可覆盖。 */
|
|
103
|
+
export const BLIND_SPOTS_DEFAULT = [
|
|
104
|
+
"Out-of-band writes: editing files in a terminal or another editor bypasses the tool chain entirely — the guard cannot see them",
|
|
105
|
+
"Semantic misjudgment: authorization/intent decisions rely on word lists and the LLM; uncovered phrasing may be misread (use /guard label to flag)",
|
|
106
|
+
"Unclassified tools: tools absent from the classification table are treated as unknown (first call is blocked until allowed)",
|
|
107
|
+
"External programs writing files: a program invoked by the shell can write files whose paths never appear in the command text",
|
|
108
|
+
"Self-certification rules: the engine only reminds; whether they are followed depends on the model",
|
|
109
|
+
"Weak criticism signals: deliberately broad shapes are logged but not blocked, so real criticism can be missed",
|
|
110
|
+
"Cross-session state: authorization/unlock live in process memory and do not survive a restart or carry across sessions"
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
// 本机覆盖(rule-engine.json 的 guardFreedom 键;null = 用通用骨架)
|
|
114
|
+
let freedomOverride = null;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 注入本机自由面/盲区(Override 语义:undefined/null = 不干预;对象 = 配置即真相;{} = 回退骨架)。
|
|
118
|
+
* @param {{freedom?: string[], blindSpots?: string[]}} cfg
|
|
119
|
+
*/
|
|
120
|
+
export function setGuardFreedom(cfg) {
|
|
121
|
+
const applied = [];
|
|
122
|
+
const rejected = [];
|
|
123
|
+
if (cfg === undefined || cfg === null) return { applied, rejected, noop: true };
|
|
124
|
+
if (typeof cfg !== "object" || Array.isArray(cfg)) {
|
|
125
|
+
freedomOverride = null;
|
|
126
|
+
return { applied, rejected };
|
|
127
|
+
}
|
|
128
|
+
const next = {};
|
|
129
|
+
for (const field of ["freedom", "blindSpots"]) {
|
|
130
|
+
const list = cfg[field];
|
|
131
|
+
if (list === undefined) continue;
|
|
132
|
+
if (!Array.isArray(list)) {
|
|
133
|
+
rejected.push({ key: field, reason: "not-array" });
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const clean = list.filter((x) => typeof x === "string" && x.trim().length > 0);
|
|
137
|
+
if (clean.length !== list.length) rejected.push({ key: field, reason: "non-string-items-dropped" });
|
|
138
|
+
if (clean.length > 0) {
|
|
139
|
+
next[field] = clean;
|
|
140
|
+
applied.push(field);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
freedomOverride = Object.keys(next).length > 0 ? next : null;
|
|
144
|
+
return { applied, rejected };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** 当前生效的自由面(本机覆盖优先 → 通用骨架) */
|
|
148
|
+
export function getFreedomSurface() {
|
|
149
|
+
return freedomOverride?.freedom ?? FREEDOM_SURFACE_DEFAULT;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** 当前生效的盲区(本机覆盖优先 → 通用骨架) */
|
|
153
|
+
export function getBlindSpots() {
|
|
154
|
+
return freedomOverride?.blindSpots ?? BLIND_SPOTS_DEFAULT;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** 是否处于本机覆盖状态 */
|
|
158
|
+
export function hasFreedomOverride() {
|
|
159
|
+
return freedomOverride !== null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** 回退通用骨架(测试用) */
|
|
163
|
+
export function resetGuardFreedom() {
|
|
164
|
+
freedomOverride = null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** 兼容别名(旧引用;等价于 getFreedomSurface) */
|
|
168
|
+
export const FREEDOM_SURFACE = FREEDOM_SURFACE_DEFAULT;
|
|
169
|
+
/** 兼容别名(旧引用;等价于 getBlindSpots) */
|
|
170
|
+
export const BLIND_SPOTS = BLIND_SPOTS_DEFAULT;
|
|
171
|
+
|
|
93
172
|
function makeHit(cfg, reason) {
|
|
94
173
|
const errId = Math.random().toString(36).slice(2, 8).toUpperCase();
|
|
95
|
-
const
|
|
174
|
+
const hintKey = `rule-hint.${cfg.ruleId}`;
|
|
175
|
+
const hint = hasMessage(hintKey) ? getMessage(hintKey) : getMessage("guard.rule-hint-fallback");
|
|
96
176
|
return {
|
|
97
177
|
ruleId: cfg.ruleId,
|
|
98
178
|
title: cfg.title,
|
|
@@ -528,7 +608,7 @@ function matchRule(cfg, ctx) {
|
|
|
528
608
|
return makeHit(cfg, "【硬拦截】禁止内联命令(node -e / pwsh -c / node -p 等),请先写脚本文件再执行");
|
|
529
609
|
}
|
|
530
610
|
if (BOM_WRITE.test(cmd)) {
|
|
531
|
-
return makeHit(cfg, "【硬拦截】禁止用 Set-Content/Out-File -Encoding utf8BOM 写 .json/.yaml(PS7 显式带 BOM
|
|
611
|
+
return makeHit(cfg, "【硬拦截】禁止用 Set-Content/Out-File -Encoding utf8BOM 写 .json/.yaml(PS7 显式带 BOM)(放行:去掉 -Encoding utf8BOM——PS7 默认就是 UTF-8 无 BOM;或改用本机配置的统一入口脚本)");
|
|
532
612
|
}
|
|
533
613
|
}
|
|
534
614
|
return null;
|
|
@@ -538,7 +618,7 @@ function matchRule(cfg, ctx) {
|
|
|
538
618
|
if (cfg.handler === "rule18-manual-first") {
|
|
539
619
|
const userText = session.turn.userText || session.lastUserText || "";
|
|
540
620
|
const firstTool = session.turn.toolCount === 0;
|
|
541
|
-
if (firstTool && !session.manualReadSeen &&
|
|
621
|
+
if (firstTool && !session.manualReadSeen && dshKeywordsRe().test(userText) && !isManualReadTool(name, args, state.localIntegrations?.manualExempt?.paths || [])) {
|
|
542
622
|
return makeHit(cfg, "【硬拦截】任务涉及 DSH,首次工具调用前需先 grep/read 本机手册(路径经 localIntegrations 配置)");
|
|
543
623
|
}
|
|
544
624
|
return null;
|
|
@@ -714,7 +794,7 @@ function matchRule(cfg, ctx) {
|
|
|
714
794
|
const why = auditedSig
|
|
715
795
|
? `装配内容已变化(装配状态哈希 ${currentSig.slice(0, 8)} ≠ 审计通过时 ${auditedSig.slice(0, 8)})`
|
|
716
796
|
: `插件装配已变更(mountRevision=${state.mountRevision})且本会话未通过全量审计`;
|
|
717
|
-
return makeHit(cfg,
|
|
797
|
+
return makeHit(cfg, `[BLOCKED] ${why} — ${getMessage("pitfall.mount-audit")}`);
|
|
718
798
|
}
|
|
719
799
|
}
|
|
720
800
|
return null;
|