dsh-rule-engine 0.6.2 → 0.6.4
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 +178 -7
- package/lib/core/audit.js +21 -4
- package/lib/core/authorization.js +30 -33
- package/lib/core/config.js +4 -1
- package/lib/core/dualtrack-markers.js +61 -0
- package/lib/core/guard-core.js +151 -34
- 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 +29 -17
- package/lib/core/measure-kinds.js +127 -0
- package/lib/core/patterns.js +382 -51
- package/lib/core/quality-ledger.js +124 -0
- package/lib/core/state.js +47 -4
- package/lib/core/text-detect.js +126 -68
- package/lib/core/tool-catalog.js +11 -3
- package/lib/core/understander.js +32 -43
- package/lib/index.js +133 -11
- package/lib/messages.js +134 -0
- package/package.json +3 -2
- package/scripts/check-tool-coverage.mjs +3 -1
- package/scripts/dualtrack-check.mjs +481 -0
- package/scripts/dualtrack-whitelist.json +7 -0
- package/scripts/local-residue-scan.mjs +10 -4
- package/scripts/plugins.json +10 -0
- package/scripts/publish-aptitude-check.mjs +5 -2
- package/scripts/release-plugin.mjs +54 -5
- package/scripts/verify-all.mjs +4 -0
- package/scripts/verify-v474.mjs +8 -8
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,
|
|
@@ -36,6 +37,7 @@ import { computeMountSignature, profileNameFromArgs } from "./mount-signature.js
|
|
|
36
37
|
import { findBackupForPath, getSessionState, maybeReloadIfChanged } from "./state.js";
|
|
37
38
|
import { isVersionedFile, validateEditedFile } from "./version-guard.js";
|
|
38
39
|
import { decideContractAction, defaultContract, isArmed } from "./contract.js";
|
|
40
|
+
import { kindOf } from "./measure-kinds.js";
|
|
39
41
|
import { classifyAction } from "./overengineering.js";
|
|
40
42
|
import { labelFingerprint, labelsAllowFingerprint } from "./label-fingerprint.js";
|
|
41
43
|
|
|
@@ -69,17 +71,8 @@ function isDelegatedSession(exec) {
|
|
|
69
71
|
return false;
|
|
70
72
|
}
|
|
71
73
|
|
|
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
|
-
};
|
|
74
|
+
// 判据 A(2026-09-09):规则号清单**不再硬编码**——放行提示 key 由约定推导
|
|
75
|
+
//(rule-hint.<ruleId>),是否存在由 messages 层决定;规则号变化时改本机配置即可。
|
|
83
76
|
|
|
84
77
|
// 0.5.12(F3):拒绝来源前缀——机器可解析 token 化(/guard log 可按来源 grep 过滤)
|
|
85
78
|
// 约定:[guardian:rule22](规则层)/ [guardian:contract](任务契约/机制类)/ [guardian:guard](守卫层)
|
|
@@ -90,9 +83,97 @@ export function prefixSource(ruleId) {
|
|
|
90
83
|
return `[guardian:rule${ruleId}]`;
|
|
91
84
|
}
|
|
92
85
|
|
|
86
|
+
// ═══════════ 自由面 / 盲区(AGI 清单 P1-2,2026-09-09 第三批)═══════════
|
|
87
|
+
// 显式声明本守卫「不拦什么」(自由面)与「看不见/判不准什么」(盲区)——
|
|
88
|
+
// 目的:被拦的人能一眼看到哪些动作本来就不需要授权(少走弯路),
|
|
89
|
+
// 以及哪些风险本引擎兜不住(别把守卫当安全边界)。
|
|
90
|
+
// 查询:/guard freedom
|
|
91
|
+
/** 自由面:无需授权即可执行的操作类别(守卫不拦)。
|
|
92
|
+
* 第 1 批 1b:内置默认 = **通用骨架**(语言/环境/规则集无关);本机完整版经
|
|
93
|
+
* rule-engine.json 的 guardFreedom 键覆盖(原为硬编码的本机规则体系自描述)。 */
|
|
94
|
+
export const FREEDOM_SURFACE_DEFAULT = [
|
|
95
|
+
"Read-only tools: file reads, searches, status queries, web fetch/search",
|
|
96
|
+
"Scratch-area writes: the analysis scratch and backup directories (scripts, downloads, temp artifacts)",
|
|
97
|
+
"Verification commands: tests, audit scripts, cold-load probes, --dry-run, read-only GETs",
|
|
98
|
+
"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)",
|
|
99
|
+
"Asking and presenting: ask_user_question / todo_write / visualize / report",
|
|
100
|
+
"Changes within an authorized scope: this turn has an execution clause and the path/type falls inside it",
|
|
101
|
+
"Escape hatches: /guard unlock (config write protection) and /guard bypass (all guards suspended, audited)"
|
|
102
|
+
];
|
|
103
|
+
/** 盲区:守卫看不见或判不准的(本引擎不是安全边界)。默认 = 通用骨架,本机可覆盖。 */
|
|
104
|
+
export const BLIND_SPOTS_DEFAULT = [
|
|
105
|
+
"Out-of-band writes: editing files in a terminal or another editor bypasses the tool chain entirely — the guard cannot see them",
|
|
106
|
+
"Semantic misjudgment: authorization/intent decisions rely on word lists and the LLM; uncovered phrasing may be misread (use /guard label to flag)",
|
|
107
|
+
"Unclassified tools: tools absent from the classification table are treated as unknown (first call is blocked until allowed)",
|
|
108
|
+
"External programs writing files: a program invoked by the shell can write files whose paths never appear in the command text",
|
|
109
|
+
"Self-certification rules: the engine only reminds; whether they are followed depends on the model",
|
|
110
|
+
"Weak criticism signals: deliberately broad shapes are logged but not blocked, so real criticism can be missed",
|
|
111
|
+
"Cross-session state: authorization/unlock live in process memory and do not survive a restart or carry across sessions"
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
// 本机覆盖(rule-engine.json 的 guardFreedom 键;null = 用通用骨架)
|
|
115
|
+
let freedomOverride = null;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 注入本机自由面/盲区(Override 语义:undefined/null = 不干预;对象 = 配置即真相;{} = 回退骨架)。
|
|
119
|
+
* @param {{freedom?: string[], blindSpots?: string[]}} cfg
|
|
120
|
+
*/
|
|
121
|
+
export function setGuardFreedom(cfg) {
|
|
122
|
+
const applied = [];
|
|
123
|
+
const rejected = [];
|
|
124
|
+
if (cfg === undefined || cfg === null) return { applied, rejected, noop: true };
|
|
125
|
+
if (typeof cfg !== "object" || Array.isArray(cfg)) {
|
|
126
|
+
freedomOverride = null;
|
|
127
|
+
return { applied, rejected };
|
|
128
|
+
}
|
|
129
|
+
const next = {};
|
|
130
|
+
for (const field of ["freedom", "blindSpots"]) {
|
|
131
|
+
const list = cfg[field];
|
|
132
|
+
if (list === undefined) continue;
|
|
133
|
+
if (!Array.isArray(list)) {
|
|
134
|
+
rejected.push({ key: field, reason: "not-array" });
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const clean = list.filter((x) => typeof x === "string" && x.trim().length > 0);
|
|
138
|
+
if (clean.length !== list.length) rejected.push({ key: field, reason: "non-string-items-dropped" });
|
|
139
|
+
if (clean.length > 0) {
|
|
140
|
+
next[field] = clean;
|
|
141
|
+
applied.push(field);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
freedomOverride = Object.keys(next).length > 0 ? next : null;
|
|
145
|
+
return { applied, rejected };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** 当前生效的自由面(本机覆盖优先 → 通用骨架) */
|
|
149
|
+
export function getFreedomSurface() {
|
|
150
|
+
return freedomOverride?.freedom ?? FREEDOM_SURFACE_DEFAULT;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** 当前生效的盲区(本机覆盖优先 → 通用骨架) */
|
|
154
|
+
export function getBlindSpots() {
|
|
155
|
+
return freedomOverride?.blindSpots ?? BLIND_SPOTS_DEFAULT;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** 是否处于本机覆盖状态 */
|
|
159
|
+
export function hasFreedomOverride() {
|
|
160
|
+
return freedomOverride !== null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** 回退通用骨架(测试用) */
|
|
164
|
+
export function resetGuardFreedom() {
|
|
165
|
+
freedomOverride = null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** 兼容别名(旧引用;等价于 getFreedomSurface) */
|
|
169
|
+
export const FREEDOM_SURFACE = FREEDOM_SURFACE_DEFAULT;
|
|
170
|
+
/** 兼容别名(旧引用;等价于 getBlindSpots) */
|
|
171
|
+
export const BLIND_SPOTS = BLIND_SPOTS_DEFAULT;
|
|
172
|
+
|
|
93
173
|
function makeHit(cfg, reason) {
|
|
94
174
|
const errId = Math.random().toString(36).slice(2, 8).toUpperCase();
|
|
95
|
-
const
|
|
175
|
+
const hintKey = `rule-hint.${cfg.ruleId}`;
|
|
176
|
+
const hint = hasMessage(hintKey) ? getMessage(hintKey) : getMessage("guard.rule-hint-fallback");
|
|
96
177
|
return {
|
|
97
178
|
ruleId: cfg.ruleId,
|
|
98
179
|
title: cfg.title,
|
|
@@ -206,23 +287,26 @@ function resolveLocalDependencyPkgPath(bundleName, profilePkgPath, parsed) {
|
|
|
206
287
|
return existsSync(pkgPath) ? pkgPath : null;
|
|
207
288
|
}
|
|
208
289
|
|
|
209
|
-
/**
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
290
|
+
/** 官方 bundle 白名单(B1 修复,2026-09-10):规则 24 自己的豁免条款原文即「官方 bundle 由官方机制管理,
|
|
291
|
+
* 模型不主动修改」——引擎此前未在代码里兑现。这两个包**永远**位于 DSH 本体或 profiles 共享层,
|
|
292
|
+
* 独立 DSH_HOME 的新 profile 两者都可能尚未建立 → 校验必然失败并**卡死 profile 声明编辑**(试点主要阻塞点)。 */
|
|
293
|
+
const OFFICIAL_BUNDLES = new Set(["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app"]);
|
|
294
|
+
|
|
295
|
+
/** 解析给定 profile package.json **内容**中非 bundle / 无法确认的项(内容级纯函数,供 B1 与 C1 复用) */
|
|
296
|
+
export function nonBundleInContent(content, profilePkgPath) {
|
|
215
297
|
let parsed;
|
|
216
298
|
try { parsed = JSON.parse(content); } catch { return null; }
|
|
217
299
|
const bundles = parsed?.dsh?.profile?.bundles;
|
|
218
300
|
if (!Array.isArray(bundles)) return null;
|
|
219
301
|
const bad = [];
|
|
220
302
|
for (const b of bundles) {
|
|
221
|
-
|
|
303
|
+
if (OFFICIAL_BUNDLES.has(b)) continue; // B1:官方 bundle 直接放行(规则 24 豁免条款)
|
|
304
|
+
const pkgPath = resolveBundlePkgPath(b, profilePkgPath) || resolveLocalDependencyPkgPath(b, profilePkgPath, parsed);
|
|
222
305
|
if (!pkgPath) {
|
|
223
306
|
const dep = parsed?.dependencies?.[b] ?? parsed?.devDependencies?.[b] ?? parsed?.optionalDependencies?.[b];
|
|
224
307
|
const depDesc = typeof dep === "string" ? `dependencies 为 ${dep}` : "dependencies 中无此包";
|
|
225
|
-
|
|
308
|
+
// B1 建议 3:给出可执行的排查方向(独立 DSH_HOME 场景最常见的成因)
|
|
309
|
+
bad.push(`${b}(找不到 package.json,无法确认类型;${depDesc}。请先用 dev_install_package 或先安装依赖再写 bundles;若为独立 DSH_HOME,请确认 profiles/node_modules 共享层存在——该层由 DSH 真实启动时的 healProfilesModuleFallback 自动建立)`);
|
|
226
310
|
continue;
|
|
227
311
|
}
|
|
228
312
|
try {
|
|
@@ -235,6 +319,31 @@ export function nonBundleInProfileBundles(name, args) {
|
|
|
235
319
|
return bad.length ? bad : null;
|
|
236
320
|
}
|
|
237
321
|
|
|
322
|
+
/** 若本次文件变更会写入 profile package.json 的 dsh.profile.bundles,返回其中非 bundle/无法确认的项 */
|
|
323
|
+
export function nonBundleInProfileBundles(name, args) {
|
|
324
|
+
const p = pathTarget(args);
|
|
325
|
+
if (!isProfilePackageJson(p)) return null;
|
|
326
|
+
const content = resultingFileContent(name, args);
|
|
327
|
+
if (!content) return null;
|
|
328
|
+
return nonBundleInContent(content, p);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** C1 修复(2026-09-10):判定本次变更是否为「使装配声明趋于自洽」的修复动作。
|
|
332
|
+
* 判据:变更**前** profile package.json 的 bundles 不自洽(存在解析不到的项),变更**后**自洽。
|
|
333
|
+
* 用途:mount-audit 守卫的**收敛豁免**——解除「不一致态只能靠被拦动作修复」的死循环。
|
|
334
|
+
* 保守边界:读不到原文件、或变更后仍不自洽 → 不豁免。 */
|
|
335
|
+
export function isConvergingBundleFix(name, args) {
|
|
336
|
+
const p = pathTarget(args);
|
|
337
|
+
if (!isProfilePackageJson(p)) return false;
|
|
338
|
+
const after = resultingFileContent(name, args);
|
|
339
|
+
if (!after) return false;
|
|
340
|
+
let before = null;
|
|
341
|
+
try { before = readFileSync(p, "utf8"); } catch { return false; }
|
|
342
|
+
const beforeBad = nonBundleInContent(before, p);
|
|
343
|
+
const afterBad = nonBundleInContent(after, p);
|
|
344
|
+
return Array.isArray(beforeBad) && beforeBad.length > 0 && (!afterBad || afterBad.length === 0);
|
|
345
|
+
}
|
|
346
|
+
|
|
238
347
|
/** 任务契约守卫:仅在总开关开启且会话 armed 时硬拦;ask 场景交给 tools/pre-execute */
|
|
239
348
|
function taskContractGuardDecision(state, exec, session) {
|
|
240
349
|
if (!state.taskContract?.taskContractEnabled) return null;
|
|
@@ -419,7 +528,7 @@ function matchRule(cfg, ctx) {
|
|
|
419
528
|
|
|
420
529
|
// 规则 22 机器化:无执行分点/存在歧义 → 本回合禁止变更类工具调用
|
|
421
530
|
//("你的执行方案难道没问题吗?"虽含"执行"仍是疑问句——同形词不构成指令,不豁免)
|
|
422
|
-
if (cfg.handler === "
|
|
531
|
+
if (kindOf(cfg.handler) === "intent-direct") {
|
|
423
532
|
// B2(2026-08-28 阶段二):委派/子代理回合不做"无执行分点"判定(规则 22④ 的识别层落地)——
|
|
424
533
|
// 子代理任务书以 source.kind="user" 注入,会被当用户消息判"无执行分点"而误拦(ERR-L8QAXS 实弹)。
|
|
425
534
|
if (isDelegatedSession(exec)) {
|
|
@@ -508,7 +617,7 @@ function matchRule(cfg, ctx) {
|
|
|
508
617
|
}
|
|
509
618
|
|
|
510
619
|
// 规则 1:同工具同参数连续失败 ≥2 次后拦第 3 次(失败计数由 tool/result 更新)
|
|
511
|
-
if (cfg.handler === "
|
|
620
|
+
if (kindOf(cfg.handler) === "retry") {
|
|
512
621
|
const key = `${name}:${JSON.stringify(args || {})}`;
|
|
513
622
|
const count = state.retryCounts.get(key) || 0;
|
|
514
623
|
const userText = session.turn.userText || session.lastUserText || "";
|
|
@@ -522,30 +631,30 @@ function matchRule(cfg, ctx) {
|
|
|
522
631
|
}
|
|
523
632
|
|
|
524
633
|
// 规则 9:内联命令 / BOM 写配置(PS7 语义:仅拦显式 utf8BOM)
|
|
525
|
-
if (cfg.handler === "
|
|
634
|
+
if (kindOf(cfg.handler) === "inline-command") {
|
|
526
635
|
if ((name === "pwsh" || name === "bash") && cmd) {
|
|
527
636
|
if (INLINE_CMD.test(cmd)) {
|
|
528
637
|
return makeHit(cfg, "【硬拦截】禁止内联命令(node -e / pwsh -c / node -p 等),请先写脚本文件再执行");
|
|
529
638
|
}
|
|
530
639
|
if (BOM_WRITE.test(cmd)) {
|
|
531
|
-
return makeHit(cfg, "【硬拦截】禁止用 Set-Content/Out-File -Encoding utf8BOM 写 .json/.yaml(PS7 显式带 BOM
|
|
640
|
+
return makeHit(cfg, "【硬拦截】禁止用 Set-Content/Out-File -Encoding utf8BOM 写 .json/.yaml(PS7 显式带 BOM)(放行:去掉 -Encoding utf8BOM——PS7 默认就是 UTF-8 无 BOM;或改用本机配置的统一入口脚本)");
|
|
532
641
|
}
|
|
533
642
|
}
|
|
534
643
|
return null;
|
|
535
644
|
}
|
|
536
645
|
|
|
537
646
|
// 规则 18:DSH 任务首次工具调用前必须已读手册
|
|
538
|
-
if (cfg.handler === "
|
|
647
|
+
if (kindOf(cfg.handler) === "manual-first") {
|
|
539
648
|
const userText = session.turn.userText || session.lastUserText || "";
|
|
540
649
|
const firstTool = session.turn.toolCount === 0;
|
|
541
|
-
if (firstTool && !session.manualReadSeen &&
|
|
650
|
+
if (firstTool && !session.manualReadSeen && dshKeywordsRe().test(userText) && !isManualReadTool(name, args, state.localIntegrations?.manualExempt?.paths || [])) {
|
|
542
651
|
return makeHit(cfg, "【硬拦截】任务涉及 DSH,首次工具调用前需先 grep/read 本机手册(路径经 localIntegrations 配置)");
|
|
543
652
|
}
|
|
544
653
|
return null;
|
|
545
654
|
}
|
|
546
655
|
|
|
547
656
|
// 规则 13A:删除/覆盖/高风险写前需有“目标路径对应备份”证据
|
|
548
|
-
if (cfg.handler === "
|
|
657
|
+
if (kindOf(cfg.handler) === "backup") {
|
|
549
658
|
// 统一入口命令豁免:入口脚本每次写入前自身执行备份(backup() 保留 5 份),
|
|
550
659
|
// 引擎静态扫描看不到脚本内部动作(已知盲区);入口命令也已被 __self-protect 限定为唯一写通道。
|
|
551
660
|
if ((name === "pwsh" || name === "bash") && isEntryChannelCommand(cmd, state.localIntegrations?.entryScript)) return null;
|
|
@@ -588,7 +697,7 @@ function matchRule(cfg, ctx) {
|
|
|
588
697
|
// hints 含 "skill" 的其它规则 cfg(如 12A:hints=["ask","skill","sensitive",...])截胡——
|
|
589
698
|
// 非 skill 工具时 return null,导致 12A 敏感授权检查静默失效(C-2 实测 Move-Item 放行)。
|
|
590
699
|
// 现在:hints 兜底仅在「name === 'skill'」时进入;handler 明确为 rule12b-skill 时维持原语义。
|
|
591
|
-
if (cfg.handler === "
|
|
700
|
+
if (kindOf(cfg.handler) === "skill-auth") {
|
|
592
701
|
if (name === "skill") {
|
|
593
702
|
const skillName = typeof args?.name === "string" ? args.name : "";
|
|
594
703
|
if ((state.localIntegrations?.manualExempt?.skills || []).includes(skillName)) return null;
|
|
@@ -623,7 +732,7 @@ function matchRule(cfg, ctx) {
|
|
|
623
732
|
}
|
|
624
733
|
|
|
625
734
|
// 规则 12A:敏感操作需要匹配授权证据
|
|
626
|
-
if (cfg.handler === "
|
|
735
|
+
if (kindOf(cfg.handler) === "approval") {
|
|
627
736
|
if (isSensitiveToolCall(name, args, sessionIdOf(exec))) {
|
|
628
737
|
// 0.5.11(用户定稿):12A 与 22-7 判据同源——分析通道/低风险新建豁免在两条分支
|
|
629
738
|
// 使用同一组判据(isAnalysisOp/isLowRiskWorkspaceNew),不再各写一套(此前 22-7 放行、
|
|
@@ -662,7 +771,7 @@ function matchRule(cfg, ctx) {
|
|
|
662
771
|
}
|
|
663
772
|
|
|
664
773
|
// 规则 21:规则/配置文件变更需 unlock(元规则)
|
|
665
|
-
if (cfg.handler === "
|
|
774
|
+
if (kindOf(cfg.handler) === "meta") {
|
|
666
775
|
if ((name === "edit" || name === "write" || (name === "str_replace_editor" && args?.command !== "view")) && isProtectedConfigPath(p) && !unlock) {
|
|
667
776
|
return makeHit(cfg, "【硬拦截】规则/配置文件受保护:修改需用户先执行 /guard unlock");
|
|
668
777
|
}
|
|
@@ -670,7 +779,7 @@ function matchRule(cfg, ctx) {
|
|
|
670
779
|
}
|
|
671
780
|
|
|
672
781
|
// 规则 24:插件装配类型确认(A 硬拦)+ 变更类工具统一覆盖(原规则 25 语义,检查④)
|
|
673
|
-
if (cfg.handler === "
|
|
782
|
+
if (kindOf(cfg.handler) === "assembly") {
|
|
674
783
|
if (name === "dev_install_package") {
|
|
675
784
|
const dir = args?.dir;
|
|
676
785
|
if (dir) {
|
|
@@ -702,7 +811,7 @@ function matchRule(cfg, ctx) {
|
|
|
702
811
|
}
|
|
703
812
|
|
|
704
813
|
// 规则 27:装配变更后必须先通过全量审计,才能继续装配(C 时序;全局变更 + 本会话审计证据)
|
|
705
|
-
if (cfg.handler === "
|
|
814
|
+
if (kindOf(cfg.handler) === "mount-audit") {
|
|
706
815
|
if (isAssemblyMutationTool(name, args)) {
|
|
707
816
|
const currentSig = computeMountSignature(profileNameFromArgs(args));
|
|
708
817
|
state.mountSignature = currentSig;
|
|
@@ -711,10 +820,18 @@ function matchRule(cfg, ctx) {
|
|
|
711
820
|
? currentSig !== auditedSig
|
|
712
821
|
: (state.mountRevision > (session.mountAuditRevision || 0));
|
|
713
822
|
if (needsAudit) {
|
|
823
|
+
// C1 修复(2026-09-10):**收敛豁免**——允许"使装配趋于自洽"的变更,解除不一致态的死循环。
|
|
824
|
+
// 背景(独立 DSH_HOME 试点):改 dependencies → 通过;改 bundles 补齐 → 被拦;
|
|
825
|
+
// 试图回滚 dependencies → **同样被拦** → 审计失败项(bundles-missing)恰恰只能靠被拦的动作修复
|
|
826
|
+
// → 无法收敛(试点靠用户手工写文件才解开)。
|
|
827
|
+
// ① 回滚豁免:变更后签名回到"审计通过时的签名"
|
|
828
|
+
// ② 修复豁免:变更前声明不自洽(bundles 有解析不到的项)而变更后自洽
|
|
829
|
+
if (auditedSig && currentSig === auditedSig) return null; // ① 回到已审计状态
|
|
830
|
+
if (isConvergingBundleFix(name, args)) return null; // ② 修复审计点名的缺失/非法项
|
|
714
831
|
const why = auditedSig
|
|
715
832
|
? `装配内容已变化(装配状态哈希 ${currentSig.slice(0, 8)} ≠ 审计通过时 ${auditedSig.slice(0, 8)})`
|
|
716
833
|
: `插件装配已变更(mountRevision=${state.mountRevision})且本会话未通过全量审计`;
|
|
717
|
-
return makeHit(cfg,
|
|
834
|
+
return makeHit(cfg, `[BLOCKED] ${why} — ${getMessage("pitfall.mount-audit")}`);
|
|
718
835
|
}
|
|
719
836
|
}
|
|
720
837
|
return null;
|
package/lib/core/intent.js
CHANGED
|
@@ -18,33 +18,30 @@
|
|
|
18
18
|
// 消灭“两套疑问词判定不一致”(如 authorization 的“影响吗”与 intent 的“怎么”)。
|
|
19
19
|
// - 新增 STATUS_SIGNAL_RE / isStatusSignal():状态信号分类(宽松版,危险词排除,
|
|
20
20
|
// 12A/13A 兜底),与“无用户消息回合”同等待遇。
|
|
21
|
-
// 0.5.11(用户拍板):词表唯一源 = lib/core/lexicon.js——下面 5
|
|
22
|
-
//
|
|
23
|
-
//
|
|
21
|
+
// 0.5.11(用户拍板):词表唯一源 = lib/core/lexicon.js——下面 5 张表全部从那里取,
|
|
22
|
+
// 本文件不再自持定义。
|
|
23
|
+
// P8 小批 A(2026-09-08):改为函数取词(questionWordsRe() 等)——配置层注入后立即生效;
|
|
24
|
+
// 旧导出名 QUESTION_RE / STATUS_SIGNAL_RE 已删除(正则常量会锁死内置值,是本次改造的根因)。
|
|
25
|
+
// 对外仍以函数形态提供:questionWordsRe / statusSignalRe(lexicon.js)。
|
|
24
26
|
|
|
25
27
|
import {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
questionWordsRe,
|
|
29
|
+
actionWordsRe,
|
|
30
|
+
strongExecRe,
|
|
31
|
+
statusSignalRe,
|
|
32
|
+
dangerousActionRe
|
|
31
33
|
} from "./lexicon.js";
|
|
32
34
|
import { hasHotwordAction } from "./hotwords.js";
|
|
33
35
|
|
|
34
36
|
export const TAG_RE = /【(执行|方案|问询|信息)】/;
|
|
35
37
|
|
|
36
|
-
// 统一疑问词表(唯一源:lexicon.js;此处再导出保持外部引用兼容)
|
|
37
|
-
export const QUESTION_RE = QUESTION_WORDS_RE;
|
|
38
|
-
|
|
39
38
|
const DEFER_RE = /待确认|确认后再|等我确认|先方案|先别|不要执行|之后再说|确认后|再确认|先确认|不要直接/;
|
|
40
39
|
// 2026-08-31(柱子 C:条件句识别——"推送完成后…/等 X 了…/待…后…"型分点):
|
|
41
40
|
// 条件句分点 = 依赖前序分点完成后的后续动作,**本条不产生任何授权/scope**(安全方向:宁拦不放)。
|
|
42
41
|
// 词面拒绝"推送/上传"等动作词被当作本条独立动作(3 号分点"推送完成后发地址"实证:引擎曾把
|
|
43
42
|
// "推送"当本条动作致 git 全局授权弥漫——分点失效根因之一)。
|
|
44
43
|
const CONDITIONAL_RE = /(?:完成|结束|做完|弄完|搞定|好了|恢复|重启?)\s*(?:了|之后|后|以后|再)|(?:等|待|等\s*[\u4e00-\u9fff]{0,6}?)\s*[\u4e00-\u9fff]{0,8}?(?:后|之后|了)\s*|(?:推送|上传|提交|发布|执行)\s*(?:完成|结束|完|好)\s*(?:了|之?后)?/;
|
|
45
|
-
const ACTION_RE = ACTION_WORDS_RE;
|
|
46
44
|
const PLAN_RE = /方案|建议|评估|分析|设计|规划|计划|给出|提供/;
|
|
47
|
-
const STRONG_EXEC_RE = STRONG_EXEC_LEX;
|
|
48
45
|
const NUMBERED_ITEM_RE = /^\s*(?:\(?(\d+)[\)、..)]\s*)/;
|
|
49
46
|
// A1(2026-08-28 阶段一):具体动作词(区别于"执行/开始/进行"等宽泛词)。
|
|
50
47
|
// 用途:hasAction+hasPlan 时区分"给出执行方案"(方案请求)与"将建议书转化到业务通告"(执行指令)。
|
|
@@ -67,12 +64,8 @@ const AT_ACTION_RE = /看|读|改|写|转|查|处理|提取|打开|展示|审阅
|
|
|
67
64
|
/**
|
|
68
65
|
* 状态信号:对先前指令的就绪/完成确认(“我已重启 / 已输入 / 重启完成 / 好了 / done”)。
|
|
69
66
|
* 语义 = 与“无用户消息回合”同等待遇:不触发规则 22 的“无执行分点”判定(规则 22⑩)。
|
|
70
|
-
* 唯一源:lexicon.js
|
|
67
|
+
* 唯一源:lexicon.js(P8 小批 A 起经 statusSignalRe() 取词,配置注入后生效)。
|
|
71
68
|
*/
|
|
72
|
-
export const STATUS_SIGNAL_RE = STATUS_SIGNAL_WORDS_RE;
|
|
73
|
-
|
|
74
|
-
// 危险动作词:命中则不当状态信号(防“我已删除了某文件”被当成就绪确认)
|
|
75
|
-
const DANGEROUS_ACTION_RE = DANGEROUS_ACTION_WORDS_RE;
|
|
76
69
|
|
|
77
70
|
/** DSH 在会话上下文里注入的系统文本(工作区指令/技能目录等),不是用户的真实消息 */
|
|
78
71
|
export function isInjectedContextMessage(text) {
|
|
@@ -128,7 +121,7 @@ export function isStatusSignal(text) {
|
|
|
128
121
|
//(“完成 A 和 C 两项”/“重启服务”),不是对先前指令的就绪确认;状态信号须带“已/我已完成”前缀
|
|
129
122
|
// 或“了/完成/完毕/好”后缀(根因:STATUS_SIGNAL_RE 的“裸词 + 可选后缀”分支把命令式误判为状态)。
|
|
130
123
|
if (/^(?:请|帮我)?\s*(?:先|接着|然后)?\s*(?:完成|重启|执行|搞定|做完|弄好)\s+\S/.test(t.trim())) return false;
|
|
131
|
-
return
|
|
124
|
+
return statusSignalRe().test(t) && !dangerousActionRe().test(t);
|
|
132
125
|
}
|
|
133
126
|
|
|
134
127
|
function normalizeTag(tag) {
|
|
@@ -214,12 +207,12 @@ function classifyClause(clause) {
|
|
|
214
207
|
const text = String(clause.raw || "").trim();
|
|
215
208
|
const tag = extractTag(text);
|
|
216
209
|
const tagType = normalizeTag(tag);
|
|
217
|
-
const hasQuestion =
|
|
210
|
+
const hasQuestion = questionWordsRe().test(text);
|
|
218
211
|
const hasDefer = DEFER_RE.test(text);
|
|
219
212
|
// B1(2026-08-29):词表命中 ∪ 热词命中(热词=LLM 兜底已确认过的动作词,见 hotwords.js)
|
|
220
|
-
const hasAction =
|
|
213
|
+
const hasAction = actionWordsRe().test(text) || hasHotwordAction(text);
|
|
221
214
|
const hasPlan = PLAN_RE.test(text);
|
|
222
|
-
const hasStrongExec =
|
|
215
|
+
const hasStrongExec = strongExecRe().test(text);
|
|
223
216
|
// A3(阶段一):@文件引用信号 + 处理动词 → 执行分点(用户用 @ 引用文件即"处理它")
|
|
224
217
|
const hasAtRef = AT_REF_RE.test(text) && AT_ACTION_RE.test(text);
|
|
225
218
|
|
package/lib/core/lang.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// lang.js - 首启语言探测(第三批第 1 波 §4)
|
|
2
|
+
//
|
|
3
|
+
// 设计(DSH 方案 §五③ 选项 (b)):引擎启动时探测规则集语言——
|
|
4
|
+
// 读 AGENTS.md(经调用方传入其解析结果或原文),含 CJK → zh-CN;否则 en。
|
|
5
|
+
// 结果只缓存在**内存**,每次启动重探,**不落盘**(无配置文件、无副作用)。
|
|
6
|
+
// 用途:报错/提示/自由面/盲区等面向用户的展示语言。
|
|
7
|
+
//
|
|
8
|
+
// 注意:本模块是**通用层**——它只判断"用什么语言展示",不携带任何语言内容。
|
|
9
|
+
// 文案本身由 messages 层/个人层提供(第 1 批清淤时落地)。
|
|
10
|
+
export const LANGS = ["zh-CN", "en"];
|
|
11
|
+
const DEFAULT_LANG = "en";
|
|
12
|
+
const CJK = /[\u4e00-\u9fff]/;
|
|
13
|
+
|
|
14
|
+
let current = DEFAULT_LANG;
|
|
15
|
+
let detectedFrom = "default";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 探测语言:传规则集文本(AGENTS.md 原文或解析结果拼接),含 CJK → zh-CN。
|
|
19
|
+
* @param {string} text 规则集文本
|
|
20
|
+
* @returns {string} 生效语言
|
|
21
|
+
*/
|
|
22
|
+
export function detectLang(text) {
|
|
23
|
+
const t = String(text || "");
|
|
24
|
+
if (!t.trim()) {
|
|
25
|
+
current = DEFAULT_LANG;
|
|
26
|
+
detectedFrom = "empty";
|
|
27
|
+
return current;
|
|
28
|
+
}
|
|
29
|
+
current = CJK.test(t) ? "zh-CN" : "en";
|
|
30
|
+
detectedFrom = "agents-md";
|
|
31
|
+
return current;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 当前生效语言 */
|
|
35
|
+
export function getLang() {
|
|
36
|
+
return current;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 探测来源(诊断用:default | empty | agents-md | manual) */
|
|
40
|
+
export function getLangSource() {
|
|
41
|
+
return detectedFrom;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 显式设置(测试/用户覆盖用;不落盘) */
|
|
45
|
+
export function setLang(lang) {
|
|
46
|
+
if (!LANGS.includes(lang)) throw new Error(`Unknown language: ${lang} (supported: ${LANGS.join(" / ")})`);
|
|
47
|
+
current = lang;
|
|
48
|
+
detectedFrom = "manual";
|
|
49
|
+
return current;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 双语选择:pickLang(zh, en) → 按当前语言取 */
|
|
53
|
+
export function pickLang(zh, en) {
|
|
54
|
+
return current === "zh-CN" ? zh : en;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 重置为默认(测试用) */
|
|
58
|
+
export function resetLang() {
|
|
59
|
+
current = DEFAULT_LANG;
|
|
60
|
+
detectedFrom = "default";
|
|
61
|
+
}
|