focus-guard 0.0.0-stage → 3.0.6
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/LICENSE +21 -0
- package/README.md +99 -2
- package/README.zh-CN.md +98 -0
- package/docs/LEGISLATION-LAW.md +102 -0
- package/docs/MASTER-PLAN-2.0.0.md +133 -0
- package/docs/MASTER-PLAN-3.0.0.md +76 -0
- package/docs/RULES.md +416 -0
- package/docs/deepseek-pricing-audit.md +63 -0
- package/hooks/guard.mjs +1142 -0
- package/hooks/hooks.json +76 -0
- package/package.json +61 -4
- package/skills/focus-thinking/SKILL.md +299 -0
- package/src/adapters/dsh/eligibility-gate.mjs +302 -0
- package/src/adapters/dsh/fg-appeal-tool.mjs +47 -0
- package/src/adapters/dsh/fg-apply-tool.mjs +68 -0
- package/src/adapters/dsh/protocol.mjs +42 -0
- package/src/core/approval.mjs +64 -0
- package/src/core/audit.mjs +71 -0
- package/src/core/backup.mjs +55 -0
- package/src/core/checkEligibility.mjs +271 -0
- package/src/core/constants.mjs +80 -0
- package/src/core/decisionEngine.mjs +45 -0
- package/src/core/env.mjs +113 -0
- package/src/core/grants.mjs +63 -0
- package/src/core/profileLoader.mjs +122 -0
- package/src/core/redlines.mjs +238 -0
- package/src/core/risk.mjs +75 -0
- package/src/core/session.mjs +9 -0
- package/src/core/state.mjs +265 -0
- package/src/dsh/audit.mjs +192 -0
- package/src/dsh/compactionWatch.mjs +407 -0
- package/src/dsh/cordis.patch.yml +10 -0
- package/src/dsh/heartbeat.mjs +128 -0
- package/src/dsh/index.js +146 -0
- package/src/dsh/pipeline.mjs +908 -0
- package/src/dsh/postProgress.mjs +362 -0
- package/src/dsh/preGuard.mjs +288 -0
- package/src/dsh/seams.mjs +454 -0
- package/src/dsh/selftest.mjs +463 -0
- package/src/dsh/sentinel.py +160 -0
- package/src/dsh/status.mjs +73 -0
- package/src/dsh/stopGuard.mjs +259 -0
- package/src/dsh/toolName.mjs +41 -0
- package/src/dsh/tools.mjs +115 -0
- package/src/dsh/workspace.mjs +145 -0
- package/src/peak-cost.mjs +115 -0
- package/src/profiles/deepseek-flash.mjs +24 -0
- package/src/profiles/deepseek-pro.mjs +25 -0
- package/src/profiles/default.mjs +26 -0
- package/src/profiles/glm.mjs +26 -0
- package/src/profiles/gpt-astra.mjs +30 -0
- package/tools/audit-chain.mjs +105 -0
- package/tools/env-fingerprint.mjs +88 -0
- package/tools/library-build.mjs +222 -0
- package/tools/sentinel.mjs +121 -0
package/hooks/guard.mjs
ADDED
|
@@ -0,0 +1,1142 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// focus-guard 护栏脚本 v3.0.4【已封存·版本独立,不随 FG 主版本更新;见 HANDOFF §九】 — 卷宗体系(总纲 2.0.0)+ 《AI 履职执法模型 v3.0》+ 动态预算 + 协作治理(总纲 3.0.0)
|
|
3
|
+
// v3.0.0 增补:①因果链留痕(audit 带 seq/chain/ref,tools/audit-chain.mjs 渲染因果图)②KPI 兑现闭环
|
|
4
|
+
// (收尾结算等次→委托池奖惩,二十七~三十二条部分机械化)③静态知识图书馆隔离(.ai/library/ 积木区
|
|
5
|
+
// 只读、便签只进 inbox/,读不占卷宗【三】)④本地哨兵(FG_SENTINEL=1 启用 tools/sentinel.mjs 离线预判,
|
|
6
|
+
// 默认仅记档,strict 模式拦截)⑤解释器 eval 类命令不再判只读侦查(R5-3 解释器黑名单)⑥FG-D1~D4 修复。
|
|
7
|
+
// v3.0.1 增补(七十五条(四)):⑦高危批量审批——多条待批合并出示,y 放行全部待批(各一次)、n 全部阻断;
|
|
8
|
+
// ⑧批示词容错——y/同意/批准等批示词后接分隔符与简短补充指令(总长≤30字符)仍构成批示。
|
|
9
|
+
// v3.0.2 增补:⑨批示词头尾皆可("…,y" 尾置亦构成批示)⑩外接三件套桥(viking-bridge /
|
|
10
|
+
// audit-chain --semantica / needle2-sentinel)落于 external-bridges 分支,主分支为无外接精简版(3.0.3)。
|
|
11
|
+
// 一、空气层:不查词、不打扰(违禁词扫描已废除)
|
|
12
|
+
// 二、触发层:行为违规即罚,梯度处罚 L1-L6;触发④=动态预算+进度检测;三预算池(侦查/执行/委托,20条)
|
|
13
|
+
// 三、卷宗层(2.0):.ai/CASE_FILE.md 四册(环境声明/依赖声明/侦查记录/额度台账)
|
|
14
|
+
// 会话级环境检测一次全程复用(仅 shell 变化时重检);跨回合取证指纹
|
|
15
|
+
// (mtime+size+SHA-256≤200KB+git 脏态)防伪;自适应 TTL+依赖声明/人工标注覆盖;
|
|
16
|
+
// 指纹一致且 TTL 未超 → 免重读放行(拦截本次 Read,复用已有取证)
|
|
17
|
+
// 四、平台层(2.0):按检出 shell 适配命令规则(PS 禁 bash 管道 / macOS BSD 限制 / 大小写冲突拦截)
|
|
18
|
+
// 五、抽查层:盲写检测 / 风险文件 100% 留痕 / 每 5 次写操作全量审计 / 上下文污染检测(58条)
|
|
19
|
+
// 六、留痕层:执法 → <工作区>/.focus-guard/AUDIT.log (JSONL);取证 → <工作区>/.ai/CASE_FILE.md
|
|
20
|
+
// v2.0.0 破坏性变更:旧 mtime 逐回合闸(49条 per-turn)废弃,由卷宗指纹+TTL 体系替代(总纲:不写兼容层)
|
|
21
|
+
// 模式:
|
|
22
|
+
// start (SessionStart) 环境检测 + 卷宗载入 + 注入执法模型
|
|
23
|
+
// reset (UserPromptSubmit) 批示 + 重置任务态(保留侦查缓存)+ shell 变化重检
|
|
24
|
+
// pre (PreToolUse) 平台规则 + 双规白名单 + 触发①③ + 盲写 + 卷宗免重读
|
|
25
|
+
// post (PostToolUse) 进度检测引擎 + 卷宗取证记录 + 抽查 + 污染检测
|
|
26
|
+
// postfail (PostToolUseFailure) 失败=无进展,计入停滞
|
|
27
|
+
// stop (Stop) 回合边界 + 锚点核验 + 额度台账落卷
|
|
28
|
+
import {
|
|
29
|
+
readFileSync, writeFileSync, rmSync, statSync, appendFileSync,
|
|
30
|
+
mkdirSync, existsSync, renameSync, realpathSync, readdirSync, copyFileSync,
|
|
31
|
+
} from "node:fs";
|
|
32
|
+
import { join, dirname, sep, basename, relative } from "node:path";
|
|
33
|
+
import { tmpdir } from "node:os";
|
|
34
|
+
import { createHash } from "node:crypto";
|
|
35
|
+
import { execFileSync } from "node:child_process";
|
|
36
|
+
import { OUTPUT_GATE_BYTES, RANDOM_AUDIT_EVERY, BUDGET_DEFAULT, BUDGET_CAP, REFILL, STALL_FUSE, MERCY_SHORT, ENGINE_VERSION, INV_POOL_DEFAULT, SHA_LIMIT, CASE_MAX_ROWS, TTL_FIRST, TTL_RECENT, TTL_WEEK, TTL_STABLE, FUSE_PHRASE, FUSE_HINT, LIBRARY_RE, BLOCKS_RE, INTERP_EVAL_RE, HIGH_RISK_FORM, MERCY_RE, CREDIT_RE, STOP_ORDER_RE, TASK_SCALE_RE, KEY50_RE, KEY15_RE, PARDON_DECL_RE, PARDON_PENDING_RE, PARDON_QUOTE_RE, PARDON_BASIS_RE, AUTH_SEMANTICS_RE, DOWNGRADE_MARKERS, EVIDENCE_ANCHORS, RISKY_FILE_RE, SECRET_FILE_RE, BACKUP_KEEP, DELEGATE_DEFAULT, DOWNGRADE_MSG, SESSION_RULES, CASE_TEMPLATE, HIGH_RISK_QUEUE_MAX } from "../src/core/constants.mjs";
|
|
37
|
+
import { MUTATOR_HEAD_RE, CMD_PREFIX_RE, isMutatingBashCmd, FILE_REDIRECT_RE, DANGEROUS_PATTERNS, SQL_NOWHERE_RE, sqlNowhere, CURL_DATA_RE, SCRIPT_FILE_RE, GIT_OPT_WITH_VALUE, GIT_OPT_VALUELESS, gitHighRisk, cmdKey, isDangerousCmd } from "../src/core/redlines.mjs";
|
|
38
|
+
import { auditFile, auditTarget, auditCtx, auditChainInit, audit, noteFail } from "../src/core/audit.mjs";
|
|
39
|
+
import { isMutating, isInvestigation, stable, callHash, collectStrings, block, penalize } from "../src/core/risk.mjs";
|
|
40
|
+
import { statePath, loadState, saveState, cleanStaleTemp, normalize, casePath, ensureCaseFile, sectionOf, parseDur, loadCaseRecords, saveCaseRecords, saveLedger, fingerprint, gitDirty, resolveTTL } from "../src/core/state.mjs";
|
|
41
|
+
import { readStdinJson, sessionId, projectDir } from "../src/adapters/dsh/protocol.mjs";
|
|
42
|
+
import { quickShellId, detectEnv, platformBashViolation } from "../src/core/env.mjs";
|
|
43
|
+
import { backupSeq, backupBeforeEdit } from "../src/core/backup.mjs";
|
|
44
|
+
import { pushHighRiskPending, queueNote, consumeHighRisk, ladderNote } from "../src/core/approval.mjs";
|
|
45
|
+
import { bindSession } from "../src/core/session.mjs";
|
|
46
|
+
|
|
47
|
+
// 2.0.1 热修:win32 shell 误判(PSModulePath 系统级恒存 → 误判 powershell → 平台禁令堵死 Git Bash 管道)
|
|
48
|
+
// 2.0.2 DSH 版:csproj/sln 列入风险文件备案(C# 项目配置与 package.json 同级)
|
|
49
|
+
// 2.2.0 正面指引版:git push 人类专属闸(二.3/五.3,本地 commit AI 可做、推送人类 UI 执行)
|
|
50
|
+
// (该口径已于 2.4.0 废止:git push 并入高危特征库,走【高危申请】审批单,y 放行本次由 AI 执行、n 彻底阻断)
|
|
51
|
+
// + 污染核实闸(三.1,输出矛盾后首个改动类先拦一次,要求先出【污染核实】声明)
|
|
52
|
+
// + 改动前自动备份 .ai/backup/(一.2,无 .git 工作区的物理回滚依据)+ 熔断出口提示经验固化 PATTERNS.md(六.1)
|
|
53
|
+
// 2.3.0 委派条例:委托池独立核算(默认20次,不占执行池,批示『追加额度』执行/侦查/委托三池各+10)+ 子代理摘要格式校验
|
|
54
|
+
// (【子代理摘要】四字段≤200字,否则拒收)+ 强制委派场景检测(全库搜索/大文档/批量处理,未委派 KPI-5)
|
|
55
|
+
// + 熔断期启动子代理=越权绕行(L4记档+L5降权)+ 委派 KPI(+5/+3/-5/-3)
|
|
56
|
+
// 2.4.0 高危命令闸:完全访问下 rm 类命令仍须【高危命令申请】+人类批示,批准后逐字一致才放行;放松日常+刚性高危
|
|
57
|
+
// 2.5.0 DSH 硬拦截:官方桥 dsh-hooks-claude-code 直接运行本 hooks.json(exit 2 硬拦/ask 审批/stderr 原文透传),
|
|
58
|
+
// 取代 fire-and-forget 监察模式;Stop 载荷无收尾文本 → 锚点/审批单校验按 DSH 签名降级审计(防桥接续跑死循环)
|
|
59
|
+
// (2.4.1 特征库加固与结构优化时引擎号曾漏升:manifest 2.4.1 / 引擎 2.4.0,2.5.0 已对齐)
|
|
60
|
+
// 2.5.1 盘点修复版:①假留痕防线——AUDIT/卷宗【三】【四】/改动前备份写失败一律上 stderr,禁止静默丢记录;
|
|
61
|
+
// ②卷宗【一】环境声明落卷(此前仅占位符,人类无从查阅);③42条源码路径适配"根即插件"布局、
|
|
62
|
+
// 市场源扫描不再写死 default 工作区;④58条路径查重改判"首 token 是路径",避免 git 警告行误报且不放过裸相对路径;
|
|
63
|
+
// ⑤委派 KPI 入额度台账并跨阈值提醒一次;⑥版本一致性自检用例(五处清单+引擎号+本头注释)
|
|
64
|
+
// 2.5.2 对抗审查版(两路独立审计后逐条复现修复):
|
|
65
|
+
// [P0] 熔断期白名单先 exit 0,导致 npm publish/shutil.rmtree/reg add 等"不在变更表里"的高危命令绕过审批——改为白名单前先判高危;
|
|
66
|
+
// [P1] 中文批示词全部失效(正则用 \b 收尾,JS 的 \b 不认 CJK)——改精确整句匹配;「停止/熔断」批示被同一函数写回 false,止停令无效;
|
|
67
|
+
// [P1] 高危命令超 300 字符后 y/n 永远匹配不上——比对键改为全量哈希(cmdKey),展示仍截断;
|
|
68
|
+
// [P1] Stop 的部分打回路径没有一次性保护,可被无限重复触发——加 stopBlocked + stop_hook_active 双重闸,并放宽 DSH 无文本签名判定;
|
|
69
|
+
// [P1] 变更类判定锚定段首,sudo/xargs 前缀与 cp 被当成"只读侦查"——改为按命令词识别并剥离前缀,补 cp/copy/rsync/sed -i 等;
|
|
70
|
+
// [P1] 熔断期「查无实据」二次判定、reset 的 shell 重检此前无任何覆盖;quickShellId 把 zsh/sh 一律报成 bash,bash↔zsh 切换永不重检;
|
|
71
|
+
// [P2] 敏感文件(.env/私钥/凭据)不再复制明文副本进 .ai/backup/;会话状态改原子落盘;损坏状态上 stderr 不再静默;
|
|
72
|
+
// [P2] SQL 的 WHERE 豁免改为按语句判定;curl 的 -d 与 -D 区分大小写;补 find -delete/rimraf/wget post/Invoke-WebRequest POST;
|
|
73
|
+
// [P2] 删除只写不读且无上限的 state.seen;备份/台账临时文件加进程号;会话 ID 消毒撞名以短哈希区分;抽查A 文案去掉"随机"(实现是确定性节奏)
|
|
74
|
+
// 2.5.3 亲历修复版(依 FG-纪审〔2026〕第1号处理意见书,2026-10-02 领导批示"可"):
|
|
75
|
+
// [案一] 卷宗继承指纹只提示不拦——会话启动从卷宗重建的取证记录不再触发免重读拦截
|
|
76
|
+
// (内容不在本会话上下文,拦首读=阻断取证;会话内真读过后自动恢复拦截);
|
|
77
|
+
// [案三] 58条输出对账限单条语句——复合命令(&&/;/换行)里 head 只约束其段,整段对账有六起系统性误报;
|
|
78
|
+
// [案二] 审计/盘点/审查批示下,取证对象整读留痕不罚(体积闸豁免),预算与巨量输出追责仍生效;
|
|
79
|
+
// [防线] 卷宗【一】落卷与 PATTERNS.md 创建失败上 stderr(对齐 2.5.1 假留痕防线标准);
|
|
80
|
+
// [61条] 会话启动清扫临时目录中 30 天未动的 focus-guard 状态/档案文件(实测残留曾达 8376 个)。
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
const mode = process.argv[2] || "";
|
|
85
|
+
const input = readStdinJson();
|
|
86
|
+
const sid = sessionId(input);
|
|
87
|
+
const path = statePath(sid);
|
|
88
|
+
bindSession(sid); // 母版层 state 模块的卷宗写入需要会话 ID(拆层后显式注入)
|
|
89
|
+
|
|
90
|
+
if (mode === "start") {
|
|
91
|
+
rmSync(path, { force: true });
|
|
92
|
+
cleanStaleTemp(sid); // 2.5.3(61条落地):清扫临时目录 30 天未动的残留状态/档案
|
|
93
|
+
// 2.0 环境检测:会话级一次,写入 state.envCache(总纲三)
|
|
94
|
+
const env = detectEnv(projectDir());
|
|
95
|
+
// 2.0 卷宗载入:重建 readSetCache 与 TTL 表(总纲七)
|
|
96
|
+
const st = loadState(path);
|
|
97
|
+
st.envCache = env;
|
|
98
|
+
st.taskChain = "S" + Date.now().toString(36); // 3.0.0 因果链:会话根链
|
|
99
|
+
const projDir = projectDir();
|
|
100
|
+
if (projDir) {
|
|
101
|
+
// FG-D4:卷宗初始化包防御——工作区只读时 SessionStart 整体抛错违反降级哲学,改降级继续
|
|
102
|
+
try {
|
|
103
|
+
st.caseCache = loadCaseRecords(ensureCaseFile(projDir));
|
|
104
|
+
// 2.5.3(案一):会话启动从卷宗重建的取证记录标为"继承"——文件内容并不在本会话上下文中,
|
|
105
|
+
// 据此免重读拦截会挡住合法首读。继承记录只提示不拦;本会话真读过后(post 重录指纹)自动转正。
|
|
106
|
+
for (const k of Object.keys(st.caseCache)) st.caseCache[k].inherited = 1;
|
|
107
|
+
} catch {
|
|
108
|
+
noteFail(sid, "卷宗初始化(工作区可能只读,降级继续)");
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
// 2.5.1:卷宗【一】环境声明落卷(此前仅占位符,人类无法查阅——盘点报告半成品项)
|
|
112
|
+
// FG-D2:改 tmp+rename 原子写(同文件另两处均为原子模式,此前此处崩溃窗可损坏 CASE_FILE.md)
|
|
113
|
+
const cp = casePath(projDir);
|
|
114
|
+
let t = readFileSync(cp, "utf8");
|
|
115
|
+
const envRow = `- OS=${env.os} / Shell=${env.shellIdKey} / 大小写=${env.caseSensitive === false ? "不敏感" : "敏感"} / 编码=${env.encoding || "-"} / 检测于 ${new Date().toISOString()}`;
|
|
116
|
+
t = t.replace(/### 【一】[\s\S]*?(?=\n### |\n## |$)/, () => `### 【一】环境声明(会话级检测,全程复用)\n\n${envRow}\n`);
|
|
117
|
+
const tmp = cp + "." + process.pid + ".tmp";
|
|
118
|
+
writeFileSync(tmp, t);
|
|
119
|
+
renameSync(tmp, cp);
|
|
120
|
+
} catch {
|
|
121
|
+
noteFail(sid, "卷宗【一】环境声明落卷");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
saveState(path, st);
|
|
125
|
+
let ctx = SESSION_RULES;
|
|
126
|
+
if (projDir) {
|
|
127
|
+
// 36条 异地交叉巡视:新会话接手 → 复核前任结论
|
|
128
|
+
try {
|
|
129
|
+
readFileSync(join(projDir, "HANDOFF.md"), "utf8");
|
|
130
|
+
ctx += "\n【异地交叉巡视·36条】发现 HANDOFF.md:先读复核前任结论,未证实标【假设】。";
|
|
131
|
+
audit(sid, "handover-inspect", { level: null, evidence: "36条 交叉巡视:发现 HANDOFF.md" });
|
|
132
|
+
} catch {}
|
|
133
|
+
// 42条 部署版本核验:运行引擎 vs 工作区源码
|
|
134
|
+
for (const rel of ["packages/core/hooks/guard.mjs", "hooks/guard.mjs", "focus-guard/hooks/guard.mjs", "plugins/focus-guard/hooks/guard.mjs"]) {
|
|
135
|
+
try {
|
|
136
|
+
const m = readFileSync(join(projDir, rel), "utf8").slice(0, 400).match(/v(\d+\.\d+\.\d+)/);
|
|
137
|
+
if (m && m[1] !== ENGINE_VERSION) {
|
|
138
|
+
ctx += `\n【部署版本核验·42条】运行引擎 v${ENGINE_VERSION} ≠ 工作区源码 v${m[1]}(${rel}),疑似升级后未同步部署,请核验一致性。`;
|
|
139
|
+
audit(sid, "version-check", { level: null, evidence: `42条 引擎 v${ENGINE_VERSION} vs 源码 v${m[1]} (${rel})` });
|
|
140
|
+
}
|
|
141
|
+
break;
|
|
142
|
+
} catch {}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// 立法法·第七章 规则备案:生效规则集登记(名称/版本/生效时间)
|
|
146
|
+
audit(sid, "rules-registered", {
|
|
147
|
+
level: null,
|
|
148
|
+
evidence: `立法法(试行)v1.0 生效2026-09-29; 监督办法v1.0(docs/RULES.md); 引擎v${ENGINE_VERSION}`,
|
|
149
|
+
});
|
|
150
|
+
// 立法法·第七章 / 监督办法第九章 部署核验:注册表 ↔ 市场源 ↔ 运行引擎
|
|
151
|
+
try {
|
|
152
|
+
const home = process.env.USERPROFILE || process.env.HOME || "";
|
|
153
|
+
const reg = JSON.parse(
|
|
154
|
+
readFileSync(join(home, ".zcode", "cli", "plugins", "installed_plugins.json"), "utf8")
|
|
155
|
+
);
|
|
156
|
+
const entry = (reg.plugins || []).find((p) => String(p.id || "").startsWith("focus-guard"));
|
|
157
|
+
if (entry) {
|
|
158
|
+
const regVer = String(basename(String(entry.installPath || "")));
|
|
159
|
+
let srcVer = "";
|
|
160
|
+
try {
|
|
161
|
+
// 2.5.1:扫描全部工作区取市场源版本(不再写死 "default" 工作区名)
|
|
162
|
+
const wsRoot = join(home, ".zcode", "workspace");
|
|
163
|
+
for (const ws of readdirSync(wsRoot)) {
|
|
164
|
+
try {
|
|
165
|
+
srcVer =
|
|
166
|
+
JSON.parse(
|
|
167
|
+
readFileSync(join(wsRoot, ws, "plugins", "focus-guard", "marketplace.json"), "utf8")
|
|
168
|
+
).version || "";
|
|
169
|
+
if (srcVer) break;
|
|
170
|
+
} catch {}
|
|
171
|
+
}
|
|
172
|
+
} catch {}
|
|
173
|
+
const drift = [];
|
|
174
|
+
if (regVer && regVer !== ENGINE_VERSION) drift.push(`注册表v${regVer}`);
|
|
175
|
+
if (srcVer && srcVer !== ENGINE_VERSION) drift.push(`市场源v${srcVer}`);
|
|
176
|
+
if (drift.length) {
|
|
177
|
+
ctx += `\n【部署版本核验】引擎v${ENGINE_VERSION} ≠ ${drift.join(" / ")},存在部署漂移,请核验。`;
|
|
178
|
+
audit(sid, "deploy-mismatch", { level: null, evidence: `立法法第七章 引擎v${ENGINE_VERSION} vs ${drift.join(" / ")}` });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
} catch {}
|
|
182
|
+
|
|
183
|
+
process.stdout.write(
|
|
184
|
+
JSON.stringify({
|
|
185
|
+
hookSpecificOutput: {
|
|
186
|
+
hookEventName: "SessionStart",
|
|
187
|
+
additionalContext: ctx,
|
|
188
|
+
},
|
|
189
|
+
})
|
|
190
|
+
);
|
|
191
|
+
process.exit(0);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (mode === "reset") {
|
|
195
|
+
// 批示:关键词预算/信用延期批复/特赦识别/追加批示;保留侦查缓存(caseCache/envCache 不清)
|
|
196
|
+
const state = loadState(path);
|
|
197
|
+
const promptText =
|
|
198
|
+
typeof input.prompt === "string"
|
|
199
|
+
? input.prompt
|
|
200
|
+
: typeof input.user_prompt === "string"
|
|
201
|
+
? input.user_prompt
|
|
202
|
+
: "";
|
|
203
|
+
const grant = promptText.match(MERCY_RE);
|
|
204
|
+
const mercy = !!(grant && promptText.trim().length <= MERCY_SHORT);
|
|
205
|
+
if (mercy) audit(sid, "mercy-granted", { level: null, evidence: `批示原文: ${grant[0]}`, pardon: true });
|
|
206
|
+
// 2.4.0 三.1:目标预授权与执行级授权分离——任务指令里的『上传/推送』只记 goal,不解锁任何执行标记
|
|
207
|
+
const goalPush = /上传|推送|push/i.test(promptText) && !/(不|禁|勿|别|暂|缓)[^,。;\n]{0,6}(上传|推送|push)/i.test(promptText);
|
|
208
|
+
if (goalPush) audit(sid, "goal-preauth", { level: null, evidence: "goal=push-at-end(目标预授权,不构成执行级授权)" });
|
|
209
|
+
|
|
210
|
+
// 总纲三:仅当明确探测到 shell 变化时重检环境
|
|
211
|
+
if (state.envCache && state.envCache.shellIdKey && state.envCache.shellIdKey !== quickShellId()) {
|
|
212
|
+
state.envCache = detectEnv(projectDir());
|
|
213
|
+
audit(sid, "env-redetect", { level: null, evidence: `shell 变化 → 重检为 ${state.envCache.shell}` });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
let creditGranted = false;
|
|
217
|
+
const short = promptText.trim();
|
|
218
|
+
if (short.length <= 12 && (state.stalledStreak || 0) >= 2 && CREDIT_RE.test(short)) {
|
|
219
|
+
state.stalledStreak = 0;
|
|
220
|
+
state.taskBudget = Math.min((state.taskBudget || BUDGET_DEFAULT) + REFILL, BUDGET_CAP);
|
|
221
|
+
state.invCap = Math.min((state.invCap || INV_POOL_DEFAULT) + REFILL, BUDGET_CAP); // 20条:侦查池同步追加
|
|
222
|
+
state.invWarned = false;
|
|
223
|
+
creditGranted = true;
|
|
224
|
+
}
|
|
225
|
+
const stopOrdered = short.length <= 12 && STOP_ORDER_RE.test(short);
|
|
226
|
+
if (stopOrdered) {
|
|
227
|
+
state.fused = true;
|
|
228
|
+
state.violations = Math.max(state.violations || 0, 3);
|
|
229
|
+
audit(sid, "stall-fuse", { level: 3, evidence: "人类批示停止" });
|
|
230
|
+
}
|
|
231
|
+
// 24条(三) 追加批示:明示追加 → 执行池/侦查池/委托池各+10
|
|
232
|
+
if (short.length <= 12 && /追加|增加额度|扩大额度/.test(short)) {
|
|
233
|
+
state.taskBudget = Math.min((state.taskBudget || BUDGET_DEFAULT) + REFILL, BUDGET_CAP);
|
|
234
|
+
state.invCap = Math.min((state.invCap || INV_POOL_DEFAULT) + REFILL, BUDGET_CAP);
|
|
235
|
+
state.invWarned = false;
|
|
236
|
+
state.delegateBudget = Math.min((state.delegateBudget ?? DELEGATE_DEFAULT) + REFILL, BUDGET_CAP); // 2.3.0:委托池同步追加
|
|
237
|
+
audit(sid, "budget-extend", { level: null, evidence: `24条(三) 追加批示 budget=${state.taskBudget} invCap=${state.invCap} delegate=${state.delegateBudget}` });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// 2.4.0 二.3/4:实时审批——执行级授权只认人类当回合短指令:y 放行本次,n 彻底阻断。
|
|
241
|
+
// 2.5.2 修:①原正则以 \b 收尾,而 JS 的 \b 只认 ASCII 词字符,导致「同意/批准/不/拒绝」等中文批示全部失效
|
|
242
|
+
// (整个产品的操作界面是中文,按"同意"却一直待批、按"不"却不阻断);改为"整条短指令就是一个批示词
|
|
243
|
+
// +可有尾标点"的精确匹配,顺带避免"是不是应该…"这类句子被误判成 y。
|
|
244
|
+
// ②比对键改用全量哈希(见 cmdKey):超过 300 字符的命令此前永远等不到 y/n 匹配。
|
|
245
|
+
// 3.0.2:批示词头尾皆可——句首(后接分隔符+补充)或句尾(分隔符前导,如"…,y")均构成批示
|
|
246
|
+
const Y_TOKEN = "(?:y|yes|是|好|行|ok|同意|批准|允许|可以|没问题|通过)";
|
|
247
|
+
const N_TOKEN = "(?:n|no|不|不行|否|不要|拒绝|不许)";
|
|
248
|
+
const LEAD = (tok) => new RegExp(`^${tok}(?:[\\s。!!,,]*$|[\\s]*[,,。::!!][\\s]*\\S)`, "i");
|
|
249
|
+
const TAIL = (tok) => new RegExp(`(?:^|[\\s,,。::!!])${tok}[\\s。!!,,]*$`, "i");
|
|
250
|
+
const yReply = short.length <= MERCY_SHORT && (LEAD(Y_TOKEN).test(short) || TAIL(Y_TOKEN).test(short));
|
|
251
|
+
const nReply = short.length <= MERCY_SHORT && (LEAD(N_TOKEN).test(short) || TAIL(N_TOKEN).test(short));
|
|
252
|
+
// 3.0.1(七十五条(四)):y/n 放行全部待批(队列 + 当前),每条各消费一次;n 全部阻断。
|
|
253
|
+
// 批示词容错:批示词 + 分隔符 + 简短补充指令(总长 ≤30 字符)仍构成批示——"y,规则改一下…"不再被吞。
|
|
254
|
+
const pendingCount = (state.highRiskQueue || []).length + (state.highRiskKey ? 1 : 0);
|
|
255
|
+
if (yReply && pendingCount > 0) {
|
|
256
|
+
const keys = (state.highRiskQueue || []).map((x) => x.k);
|
|
257
|
+
if (state.highRiskKey && !keys.includes(state.highRiskKey)) keys.push(state.highRiskKey);
|
|
258
|
+
state.highRiskOk = true;
|
|
259
|
+
state.highRiskBatch = keys;
|
|
260
|
+
state.highRiskQueue = [];
|
|
261
|
+
// 3.0.5 目标绑定:批示即绑定目标键(本会话有效),替代"一次一 attempt"消费制
|
|
262
|
+
state.highRiskApprovedKeys = state.highRiskApprovedKeys || {};
|
|
263
|
+
for (const k of keys) state.highRiskApprovedKeys[k] = true;
|
|
264
|
+
audit(sid, "high-risk-approved", { level: null, evidence: `批示原文: ${short} | 放行 ${keys.length} 条待批(目标绑定)`, pardon: true });
|
|
265
|
+
}
|
|
266
|
+
if (nReply && pendingCount > 0) {
|
|
267
|
+
state.rejectedCmds = state.rejectedCmds || {};
|
|
268
|
+
const keys = (state.highRiskQueue || []).map((x) => x.k);
|
|
269
|
+
if (state.highRiskKey && !keys.includes(state.highRiskKey)) keys.push(state.highRiskKey);
|
|
270
|
+
for (const k of keys) state.rejectedCmds[String(k)] = 1;
|
|
271
|
+
// 3.0.5:n 即彻底阻断——同步解除目标绑定,防止历史 y 豁免压过否决
|
|
272
|
+
if (state.highRiskApprovedKeys) for (const k of keys) delete state.highRiskApprovedKeys[k];
|
|
273
|
+
audit(sid, "high-risk-rejected", { level: null, evidence: `批示原文: ${short} | 已彻底阻断 ${keys.length} 条` });
|
|
274
|
+
state.highRiskCmd = "";
|
|
275
|
+
state.highRiskKey = "";
|
|
276
|
+
state.highRiskQueue = [];
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// 43条 状态重置核验:上一回合残留 → 记档报告后清理(本事件随后统一重置)
|
|
280
|
+
const residues = [];
|
|
281
|
+
if ((state.turnCount || 0) > 0) residues.push(`turnCount=${state.turnCount}`);
|
|
282
|
+
if ((state.stalledStreak || 0) > 0) residues.push(`stall=${state.stalledStreak}`);
|
|
283
|
+
if (state.fused && !stopOrdered) residues.push("fused");
|
|
284
|
+
if (state.forcedInvestigate) residues.push("forcedInvestigate");
|
|
285
|
+
if (state.stopBlocked) residues.push("stopBlocked");
|
|
286
|
+
if (state.readSet && Object.keys(state.readSet).length) residues.push(`readSet=${Object.keys(state.readSet).length}`);
|
|
287
|
+
if (residues.length) audit(sid, "residue-check", { level: null, evidence: `43条 残留(已清理): ${residues.join(" ")}` });
|
|
288
|
+
|
|
289
|
+
const kw = KEY50_RE.test(promptText) ? 50 : KEY15_RE.test(promptText) ? 15 : BUDGET_DEFAULT;
|
|
290
|
+
state.taskBudget = Math.max(kw, state.declaredBudget || 0, state.taskBudget || BUDGET_DEFAULT);
|
|
291
|
+
state.taskInitial = state.taskBudget;
|
|
292
|
+
state.ineffCalls = 0;
|
|
293
|
+
state.taskChain = "T" + Date.now().toString(36); // 3.0.0 因果链:新任务起新链(随 saveState 落盘)
|
|
294
|
+
|
|
295
|
+
saveState(path, {
|
|
296
|
+
...state, // envCache/caseCache(侦查缓存)随 spread 保留;kpi/delegateUsed(考核与委托台账)跨回合保留
|
|
297
|
+
turnCount: 0,
|
|
298
|
+
// 2.5.2:人类批示"停/熔断"必须真的生效。旧代码在此把 fused 硬写回 false、violations 清零,
|
|
299
|
+
// 同一个函数前面刚设的止停令被自己抹掉——只有一行 AUDIT 记录,熔断等于没下。
|
|
300
|
+
fused: stopOrdered,
|
|
301
|
+
stopBlocked: false,
|
|
302
|
+
mercy,
|
|
303
|
+
goalPush, // 2.4.0:目标预授权仅记录,不构成执行级授权
|
|
304
|
+
highRiskOk: yReply && (pendingCount > 0), // 2.4.0:执行级授权只认当回合人类短指令(3.0.1 含待批队列)
|
|
305
|
+
highRiskDeniedThisTurn: false,
|
|
306
|
+
violations: stopOrdered ? Math.max(state.violations || 0, 3) : 0,
|
|
307
|
+
forcedInvestigate: false,
|
|
308
|
+
probation: false,
|
|
309
|
+
readSet: {},
|
|
310
|
+
// 2.3.0:delegateBudget 跨回合保留(用尽须批示追加,不自动回满);pollutionFlagged 有意不清(跨回合存活至消费)
|
|
311
|
+
delegated: false, // 2.3.0:新回合重置委派标记与场景判定
|
|
312
|
+
editedFiles: {},
|
|
313
|
+
kpiScolded: {},
|
|
314
|
+
kpiDelegatedAwarded: false,
|
|
315
|
+
turnPrompt: promptText.slice(0, 500),
|
|
316
|
+
turnPromptFull: promptText.slice(0, 4000), // FG-D1:授权引文核验窗口(批示含授权语义可能在 500 字符之外)
|
|
317
|
+
taskBudget: state.taskBudget,
|
|
318
|
+
taskInitial: state.taskInitial,
|
|
319
|
+
lastSig: "",
|
|
320
|
+
lastInput: "",
|
|
321
|
+
});
|
|
322
|
+
auditCtx.chain = state.taskChain; // 3.0.0:reset-fired 及其后事件挂新任务链(见上方 taskChain 赋值)
|
|
323
|
+
audit(sid, "reset-fired", { level: null, evidence: `prompt:${promptText ? "有" : "无"} kw=${kw} budget=${state.taskBudget} eff=${state.effectiveCalls || 0} inv=${state.invCalls || 0} stall=${state.stalledStreak || 0}${creditGranted ? " 信用延期" : ""}${mercy ? " 特赦" : ""}` });
|
|
324
|
+
process.exit(0);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (mode === "pre") {
|
|
328
|
+
const state = loadState(path);
|
|
329
|
+
auditChainInit(state); // 3.0.0 因果链
|
|
330
|
+
const tool = input.tool_name || "";
|
|
331
|
+
const ti = input.tool_input || {};
|
|
332
|
+
const rawPath = ti.file_path || ti.path || "";
|
|
333
|
+
const filePath = normalize(rawPath);
|
|
334
|
+
const handoff = /^(Write|Edit)$/.test(tool) && /(^|\/)handoff\.md$/i.test(filePath);
|
|
335
|
+
const search = /^(WebSearch|WebFetch)$/.test(tool) || /mcp__.*(web|search)/i.test(tool);
|
|
336
|
+
const env = state.envCache || null;
|
|
337
|
+
|
|
338
|
+
// ===== 3.0.0 本地哨兵(总纲 3.0.0 十五·二):零成本离线预判,opt-in(FG_SENTINEL=1)=====
|
|
339
|
+
// 默认 audit-only:flag 只记档;FG_SENTINEL_MODE=strict 时 block/flag 一律拦截。
|
|
340
|
+
// 引擎内建启发式(tools/sentinel.mjs,零依赖);CLI 侧可用 Needle 2(本地 45M 模型)跑 --model-cmd 外判。
|
|
341
|
+
if (tool === "Bash" && /^(1|true)$/i.test(String(process.env.FG_SENTINEL || ""))) {
|
|
342
|
+
try {
|
|
343
|
+
const { assess } = await import(new URL("../tools/sentinel.mjs", import.meta.url).href);
|
|
344
|
+
const v = assess(String(ti.command || ""));
|
|
345
|
+
if (v.verdict === "block" || (v.verdict === "flag" && String(process.env.FG_SENTINEL_MODE || "") === "strict")) {
|
|
346
|
+
audit(sid, "sentinel-deny", { level: null, evidence: `哨兵拦截 verdict=${v.verdict} score=${v.score} ${v.reasons.join(";")}` });
|
|
347
|
+
process.stderr.write(`[本地哨兵]判定 ${v.verdict}(score ${v.score}):${v.reasons.join(";")}。确需执行请走【高危申请】或批示关闭哨兵。`);
|
|
348
|
+
process.exit(2);
|
|
349
|
+
}
|
|
350
|
+
if (v.verdict === "flag") {
|
|
351
|
+
audit(sid, "sentinel-flag", { level: null, evidence: `哨兵标记 score=${v.score} ${v.reasons.join(";").slice(0, 150)}` });
|
|
352
|
+
}
|
|
353
|
+
} catch {} // 哨兵缺失/损坏不影响护栏主流程(降级哲学)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ===== 3.0.5 资料分层隔离:静态资料区与派生积木区均不得直写(记忆更新隔离区,R6 采纳)=====
|
|
357
|
+
if (/^(Write|Edit)$/.test(tool) && (LIBRARY_RE.test(filePath) || BLOCKS_RE.test(filePath))) {
|
|
358
|
+
audit(sid, "library-write-deny", { level: null, evidence: `静态资料区直写被拒 ${filePath}` });
|
|
359
|
+
process.stderr.write(
|
|
360
|
+
`[图书馆·隔离]${filePath} 属静态资料区/派生积木区,不得直写。外部原文走同步三步(旧版归档 → 覆盖 → 写 frontmatter),派生积木由 library-build 生成到 .ai/output/library(防自我投毒)。`
|
|
361
|
+
);
|
|
362
|
+
process.exit(2);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ===== 2.0 总纲六:环境规则检查(平台命令拦截)=====
|
|
366
|
+
if (tool === "Bash") {
|
|
367
|
+
const v = platformBashViolation(env, String(ti.command || ""));
|
|
368
|
+
if (v) {
|
|
369
|
+
audit(sid, "platform-deny", { level: null, evidence: `${env ? env.os + "/" + env.shellIdKey : "unknown"} ${v.slice(0, 100)}` });
|
|
370
|
+
process.stderr.write(`[平台规则·${env ? env.os + "/" + env.shellIdKey : "?"}]${v}`);
|
|
371
|
+
process.exit(2);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
// 2.0 总纲六:大小写不敏感文件系统 → 禁仅大小写不同的重名文件
|
|
375
|
+
// 注意:不敏感 FS 上大小写变体目标的 existsSync 恒为 true,故不能以 existsSync 豁免
|
|
376
|
+
if (/^(Write|Edit)$/.test(tool) && rawPath && env && env.caseSensitive === false) {
|
|
377
|
+
try {
|
|
378
|
+
const dirp = dirname(rawPath);
|
|
379
|
+
const base = basename(rawPath);
|
|
380
|
+
const clash = readdirSync(dirp).find((e) => e !== base && e.toLowerCase() === base.toLowerCase());
|
|
381
|
+
if (clash) {
|
|
382
|
+
audit(sid, "platform-deny", { level: null, evidence: `2.0平台 大小写冲突 ${base} vs ${clash}` });
|
|
383
|
+
process.stderr.write(`[平台规则·大小写冲突]本文件系统大小写不敏感:${base} 与已有 ${clash} 仅大小写不同,创建后会互相覆盖。改名,或改用现有文件。`);
|
|
384
|
+
process.exit(2);
|
|
385
|
+
}
|
|
386
|
+
} catch {}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (tool === "Agent") {
|
|
390
|
+
// 2.3.0 四:越权绕行边界——仅熔断期启动子代理属违规(L4 记档 + L5 降权);正常委派放行不计违规
|
|
391
|
+
if (state.fused) {
|
|
392
|
+
state.violations = Math.max(state.violations || 0, 4);
|
|
393
|
+
const lvl = penalize(state, sid, "violation-subagent-usurp", `56条 熔断期启动子代理 ${String(ti.description || ti.prompt || "").slice(0, 60)}`);
|
|
394
|
+
saveState(path, state);
|
|
395
|
+
process.stderr.write(
|
|
396
|
+
`[越权绕行·L${lvl}]熔断期启动子代理执行被禁操作:L4 记档+L5 降权。只读可亲自查,或等批示。${ladderNote(lvl)}`
|
|
397
|
+
);
|
|
398
|
+
process.exit(2);
|
|
399
|
+
}
|
|
400
|
+
// 48条 子代理继承留痕:父会话处分状态随派单记录(平台无注入通道,以留痕方式移交)
|
|
401
|
+
const spawnSeq = audit(sid, "subagent-spawn", {
|
|
402
|
+
level: null,
|
|
403
|
+
evidence: `48条 父状态 fused=${!!state.fused} L${state.violations || 0} eff=${state.effectiveCalls || 0} inv=${state.invCalls || 0} | ${String(ti.description || ti.prompt || "").slice(0, 60)}`,
|
|
404
|
+
});
|
|
405
|
+
// 3.0.0 蜂群因果链:每次委派派生子链 /dN,ref 指回派单事件——因果图上"主会话调度→子代理执行"成边
|
|
406
|
+
state.childChainN = (state.childChainN || 0) + 1;
|
|
407
|
+
var childChain = (state.taskChain || sid) + "/d" + state.childChainN;
|
|
408
|
+
// 2.3.0 一:委托池独立核算(不占执行池;用尽须人类批示追加)
|
|
409
|
+
if ((state.delegateBudget ?? DELEGATE_DEFAULT) <= 0) {
|
|
410
|
+
audit(sid, "delegate-exhausted", { level: null, evidence: `2.3.0 委托池用尽 剩余=0 累计=${state.delegateUsed || 0}` });
|
|
411
|
+
process.stderr.write(
|
|
412
|
+
"[委托池用尽]子代理额度已用完(默认20次,独立于执行池)。请批示『追加额度』(执行/侦查/委托三池各+10),或主会话自行收敛。"
|
|
413
|
+
);
|
|
414
|
+
process.exit(2);
|
|
415
|
+
}
|
|
416
|
+
state.delegateBudget = (state.delegateBudget ?? DELEGATE_DEFAULT) - 1;
|
|
417
|
+
state.delegateUsed = (state.delegateUsed || 0) + 1;
|
|
418
|
+
state.delegated = true;
|
|
419
|
+
saveState(path, state);
|
|
420
|
+
audit(sid, "delegate-used", {
|
|
421
|
+
chain: childChain,
|
|
422
|
+
ref: spawnSeq,
|
|
423
|
+
level: null,
|
|
424
|
+
evidence: `2.3.0 委托池消耗 剩余=${state.delegateBudget} 累计=${state.delegateUsed} | ${String(ti.description || "").slice(0, 50)}`,
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// 触发⑤:熔断期白名单——只读调查类 + 降级动作放行,改动类拒绝(L3 放行只读工具)
|
|
429
|
+
if (state.fused) {
|
|
430
|
+
// 2.5.2 [P0] 熔断期不是高危命令的免检通道。旧顺序让白名单先 exit 0,于是
|
|
431
|
+
// npm publish / shutil.rmtree / reg add / diskpart 等"不在 MUTATING 表里"的高危命令
|
|
432
|
+
// 被 isInvestigation 判成只读侦查,直接绕过审批——"即使完全访问也须实时审批"的不变量在熔断期失效。
|
|
433
|
+
if (tool === "Bash" && isDangerousCmd(String(ti.command || ""))) {
|
|
434
|
+
audit(sid, "deny-shuanggui-highrisk", { level: 3, evidence: `熔断期高危命令被拒 ${String(ti.command).slice(0, 120)}` });
|
|
435
|
+
process.stderr.write("[熔断期·高危命令]熔断期只读放行不含高危命令。此类命令即使解熔也须走【高危申请】审批,现在一律拒绝。\n");
|
|
436
|
+
process.exit(2);
|
|
437
|
+
}
|
|
438
|
+
if (search || handoff || isInvestigation(tool, ti)) process.exit(0);
|
|
439
|
+
audit(sid, "deny-shuanggui", { level: 3, evidence: `${tool} 熔断期改动类被拒` });
|
|
440
|
+
process.stderr.write(DOWNGRADE_MSG);
|
|
441
|
+
process.exit(2);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// L2 强制取证 / L5 降权:改动类操作一律拒绝
|
|
445
|
+
if ((state.forcedInvestigate || state.probation) && isMutating(tool, ti, handoff)) {
|
|
446
|
+
const lvl = state.probation ? 5 : 2;
|
|
447
|
+
audit(sid, state.probation ? "deny-L5-probation" : "deny-L2-forced", { level: lvl, evidence: `${tool} ${filePath}` });
|
|
448
|
+
process.stderr.write(
|
|
449
|
+
state.probation
|
|
450
|
+
? "[L5 降权]只读模式,改动类全拒,等人类批示。"
|
|
451
|
+
: "[L2 强制取证]下一调用必须是取证类,取证后自动解除。"
|
|
452
|
+
);
|
|
453
|
+
process.exit(2);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const mutating = isMutating(tool, ti, handoff);
|
|
457
|
+
|
|
458
|
+
// ===== 2.4.0 高危命令闸(统一推送/删除/清盘/发布/包装绕行):完全访问也须实时审批 =====
|
|
459
|
+
if (tool === "Bash") {
|
|
460
|
+
const cmdStr = String(ti.command || "");
|
|
461
|
+
const scriptHits = (cmdStr.match(/[\w.\\\/-]+\.(sh|ps1|bat|cmd|py|pl|rb|mjs|cjs|js)\b/ig) || [])
|
|
462
|
+
.map((t) => normalize(t))
|
|
463
|
+
.filter((t) => (state.scriptFiles || {})[t] === "d");
|
|
464
|
+
if (isDangerousCmd(cmdStr) || scriptHits.length) {
|
|
465
|
+
if ((state.rejectedCmds || {})[cmdKey(cmdStr)]) {
|
|
466
|
+
audit(sid, "high-risk-rejected", { level: null, evidence: `2.4.0 已否决命令再次尝试 ${cmdStr.slice(0, 120)}` });
|
|
467
|
+
process.stderr.write("[高危命令闸·已否决]该命令已被人类批示 n,彻底阻断。如需变体,重新走【高危申请】。");
|
|
468
|
+
process.exit(2);
|
|
469
|
+
}
|
|
470
|
+
if (consumeHighRisk(state, cmdKey(cmdStr))) { // 3.0.1:单条/批量统一消费,放行本次(一次性)
|
|
471
|
+
saveState(path, state);
|
|
472
|
+
audit(sid, "high-risk-executed", { level: null, evidence: `2.4.0 已批高危命令执行 ${cmdStr.slice(0, 120)}` });
|
|
473
|
+
} else if (state.highRiskDeniedThisTurn && scriptHits.length) {
|
|
474
|
+
state.violations = Math.max(state.violations || 0, 3); // 16条:对抗审查从重,至少落在 L4 记档
|
|
475
|
+
const level = penalize(state, sid, "violation-wrap-bypass", `2.4.0 脚本包装绕过 ${scriptHits.join(",").slice(0, 80)}`);
|
|
476
|
+
saveState(path, state);
|
|
477
|
+
process.stderr.write(
|
|
478
|
+
`[对抗审查·L${level}]高危命令被拒后改写脚本执行,按对抗审查记档。要执行请走【高危申请】审批,不得绕行。${ladderNote(level)}`
|
|
479
|
+
);
|
|
480
|
+
process.exit(2);
|
|
481
|
+
} else {
|
|
482
|
+
state.highRiskCmd = cmdStr.slice(0, 300);
|
|
483
|
+
state.highRiskKey = cmdKey(cmdStr);
|
|
484
|
+
state.highRiskDeniedThisTurn = true;
|
|
485
|
+
pushHighRiskPending(state, state.highRiskKey, cmdStr.slice(0, 80));
|
|
486
|
+
saveState(path, state);
|
|
487
|
+
audit(sid, "high-risk-request", { level: null, evidence: `2.4.0 高危命令待批 ${cmdStr.slice(0, 120)}` });
|
|
488
|
+
process.stderr.write(
|
|
489
|
+
"[高危命令闸]命中高危特征库(删除/推送/清盘/发布/全局安装等),即使完全访问也须人类实时审批。" +
|
|
490
|
+
`输出一行审批单:${HIGH_RISK_FORM} 人类回复 y 放行全部待批(各一次)、n 全部阻断;严禁改写脚本绕行(按对抗审查 L4 记档)。` +
|
|
491
|
+
queueNote(state, cmdStr.slice(0, 80))
|
|
492
|
+
);
|
|
493
|
+
process.exit(2);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
// 2.4.0:写脚本文件且内容含高危命令 → 写入本身走审批(源头拦包装);被拒后继续写 → 对抗审查 L4
|
|
498
|
+
if (/^(Write|Edit)$/.test(tool) && !handoff && filePath && SCRIPT_FILE_RE.test(filePath)) {
|
|
499
|
+
const body = String(ti.content ?? "") + String(ti.new_string ?? "");
|
|
500
|
+
if (isDangerousCmd(body)) {
|
|
501
|
+
const key = cmdKey("write:" + filePath); // 2.5.2:脚本写入路径也改用全量哈希比对
|
|
502
|
+
if (consumeHighRisk(state, key)) { // 3.0.1:单条/批量统一消费
|
|
503
|
+
saveState(path, state);
|
|
504
|
+
audit(sid, "high-risk-executed", { level: null, evidence: `2.4.0 已批高危脚本写入 ${filePath}` });
|
|
505
|
+
} else if (
|
|
506
|
+
state.highRiskDeniedThisTurn &&
|
|
507
|
+
// 3.0.1 同源判定:只有"同回合被拒过的同一写入目标"重试才构成对抗审查;
|
|
508
|
+
// 被拒的是 A 文件、随后写 B 文件的,走正常待批(此前不分目标连坐,L4 误伤跨目标写入)
|
|
509
|
+
(state.highRiskKey === key || (state.highRiskQueue || []).some((x) => x.k === key))
|
|
510
|
+
) {
|
|
511
|
+
state.violations = Math.max(state.violations || 0, 3); // 16条:对抗审查从重,至少落在 L4 记档
|
|
512
|
+
const level = penalize(state, sid, "violation-wrap-bypass", `2.4.0 脚本包装绕过(写入·同源) ${filePath}`);
|
|
513
|
+
saveState(path, state);
|
|
514
|
+
process.stderr.write(
|
|
515
|
+
`[对抗审查·L${level}]高危命令被拒后改写脚本继续,按对抗审查记档。要写入请走【高危申请】审批,不得绕行。${ladderNote(level)}`
|
|
516
|
+
);
|
|
517
|
+
process.exit(2);
|
|
518
|
+
} else {
|
|
519
|
+
state.highRiskCmd = ("write:" + filePath).slice(0, 300);
|
|
520
|
+
state.highRiskKey = key;
|
|
521
|
+
state.highRiskDeniedThisTurn = true;
|
|
522
|
+
pushHighRiskPending(state, key, ("写入 " + filePath).slice(0, 80));
|
|
523
|
+
saveState(path, state);
|
|
524
|
+
audit(sid, "high-risk-request", { level: null, evidence: `2.4.0 高危脚本写入待批 ${filePath}` });
|
|
525
|
+
process.stderr.write(
|
|
526
|
+
`[高危命令闸]脚本内容含高危命令,写入同样须审批。${HIGH_RISK_FORM}(命令填:写入 ${filePath}),人类回复 y 放行全部待批(各一次)、n 全部阻断。` +
|
|
527
|
+
queueNote(state, ("写入 " + filePath).slice(0, 80))
|
|
528
|
+
);
|
|
529
|
+
process.exit(2);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// 2.2.0 三.1:污染核实闸——上轮工具输出与指令参数矛盾且未核实前,首个改动类先拦一次(一次性,重试放行)
|
|
535
|
+
if (state.pollutionFlagged && mutating) {
|
|
536
|
+
state.pollutionFlagged = false;
|
|
537
|
+
saveState(path, state);
|
|
538
|
+
audit(sid, "pollution-gate", { level: null, evidence: `三.1 污染核实 ${tool} ${filePath}` });
|
|
539
|
+
process.stderr.write(
|
|
540
|
+
"[污染核实闸]上轮输出曾与参数矛盾(已记档)。先输出:【污染核实】预期X 实际Y,结论:可信/不可信/需重试,再重试本次修改。只读核验不受限。"
|
|
541
|
+
);
|
|
542
|
+
process.exit(2);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// 触发①:未取证就改(turnCount 为本回合调用数,由 stop 清零)
|
|
546
|
+
if ((state.turnCount || 0) === 0 && mutating) {
|
|
547
|
+
const level = penalize(state, sid, "violation-no-investigation", `${tool} ${filePath}`);
|
|
548
|
+
saveState(path, state);
|
|
549
|
+
process.stderr.write(
|
|
550
|
+
`[触发①·L${level}]程序正义:本回合零调查即改文件/执行变更命令,拒绝。先 Read/Grep/只读命令取证再动手。${ladderNote(level)}`
|
|
551
|
+
);
|
|
552
|
+
process.exit(2);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// 抽查B前置:风险文件修改 100% 留痕(不拦,记录)
|
|
556
|
+
if (/^(Write|Edit)$/.test(tool) && !handoff && (RISKY_FILE_RE.test(filePath) || /\.github\/|\.zcode-plugin\//.test(filePath))) {
|
|
557
|
+
audit(sid, "risk-audit", { level: null, evidence: `风险文件修改 ${filePath}` });
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// 抽查B强化(盲写检测)
|
|
561
|
+
if (/^(Write|Edit)$/.test(tool) && !handoff && filePath && rawPath) {
|
|
562
|
+
const read = (state.readSet || {})[filePath];
|
|
563
|
+
if (!read) {
|
|
564
|
+
let exists = false;
|
|
565
|
+
try {
|
|
566
|
+
exists = statSync(rawPath).isFile();
|
|
567
|
+
} catch {}
|
|
568
|
+
if (exists) {
|
|
569
|
+
const level = penalize(state, sid, "violation-blind-write", `盲写未读文件 ${filePath}`);
|
|
570
|
+
saveState(path, state);
|
|
571
|
+
process.stderr.write(
|
|
572
|
+
`[触发②·L${level}]盲写拦截:目标文件已存在但本回合未读过,先 Read 取证再改。${ladderNote(level)}`
|
|
573
|
+
);
|
|
574
|
+
process.exit(2);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// 触发③:体积刺客三闸
|
|
580
|
+
if (tool === "Read" && !ti.limit && !ti.pages && rawPath) {
|
|
581
|
+
try {
|
|
582
|
+
const kb = Math.round(statSync(rawPath).size / 1024);
|
|
583
|
+
if (kb * 1024 > OUTPUT_GATE_BYTES) {
|
|
584
|
+
// 2.5.3(案二):审计/盘点/审查类任务的取证对象整读留痕不罚——法典将审计定为 50 预算
|
|
585
|
+
// 重大专项,体积闸却拦审计最需要的整读,属制度性误伤。预算与巨量输出追责仍生效。
|
|
586
|
+
if (/审计|盘点|审查/.test(String(state.turnPromptFull || state.turnPrompt || ""))) {
|
|
587
|
+
audit(sid, "audit-read-allow", { level: null, evidence: `${filePath} ${kb}KB 整读(审计任务豁免,留痕不罚)` });
|
|
588
|
+
process.stderr.write(`[体积刺客·审计豁免]${filePath} ${kb}KB 整读已留痕(审计任务)。预算仍计费,巨量输出仍追责。`);
|
|
589
|
+
process.exit(0);
|
|
590
|
+
}
|
|
591
|
+
const level = penalize(state, sid, "violation-read-gate", `${filePath} ${kb}KB 整读`);
|
|
592
|
+
saveState(path, state);
|
|
593
|
+
process.stderr.write(
|
|
594
|
+
`[触发③·L${level}]体积刺客:${filePath} ${kb}KB,整读一次灌入、步步重复计费。limit+offset 分段或交子代理。${ladderNote(level)}`
|
|
595
|
+
);
|
|
596
|
+
process.exit(2);
|
|
597
|
+
}
|
|
598
|
+
} catch {}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (tool === "Grep" && ti.output_mode === "content" && !ti.head_limit) {
|
|
602
|
+
const level = penalize(state, sid, "violation-grep-gate", "Grep content 无 head_limit");
|
|
603
|
+
saveState(path, state);
|
|
604
|
+
process.stderr.write(
|
|
605
|
+
`[触发③·L${level}]体积刺客:Grep content 必带 head_limit≤50,或先用 files_with_matches 定位。${ladderNote(level)}`
|
|
606
|
+
);
|
|
607
|
+
process.exit(2);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (tool === "Bash") {
|
|
611
|
+
const cmd = String(ti.command || "");
|
|
612
|
+
if (/(^|[;&|]\s*)(cat|type|Get-Content)\s/i.test(cmd) && !cmd.includes("|") && !cmd.includes(">") && !/-(TotalCount|First|Tail)\b/.test(cmd)) {
|
|
613
|
+
const level = penalize(state, sid, "violation-bash-gate", `裸 cat/type:${cmd.slice(0, 60)}`);
|
|
614
|
+
saveState(path, state);
|
|
615
|
+
process.stderr.write(
|
|
616
|
+
`[触发③·L${level}]体积刺客:禁裸 cat/type 刷屏。cat x | head -100 或先 grep/wc 定位。${ladderNote(level)}`
|
|
617
|
+
);
|
|
618
|
+
process.exit(2);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// ===== 2.0 总纲四:卷宗不重复读校验(跨回合)=====
|
|
623
|
+
// 指纹一致(mtime+size+SHA/git)且 TTL 未超 → 免重读放行:拦截本次 Read,复用已有取证。
|
|
624
|
+
// 指纹不一致 / TTL 超时 → 拦截免读资格,放行真重读(post 更新卷宗指纹)。
|
|
625
|
+
// 熔断/强制取证期豁免:降级重建证据需要真重读。offset 增量读永远放行。
|
|
626
|
+
if (tool === "Read" && rawPath && !ti.offset && !state.fused && !state.forcedInvestigate && !BLOCKS_RE.test(rawPath)) {
|
|
627
|
+
// (3.0.5:派生积木免卷宗闸——积木指纹在 INDEX.md 固化且内容不可变,动态卷宗【三】不为其记账)
|
|
628
|
+
try {
|
|
629
|
+
const rec = (state.caseCache || {})[filePath];
|
|
630
|
+
if (rec) {
|
|
631
|
+
const fp = fingerprint(rawPath);
|
|
632
|
+
const pDir = projectDir();
|
|
633
|
+
const dirty = fp.sha ? null : gitDirty(pDir, rawPath);
|
|
634
|
+
const changed =
|
|
635
|
+
rec.mtime !== fp.mtime ||
|
|
636
|
+
rec.size !== fp.size ||
|
|
637
|
+
(rec.sha && fp.sha && rec.sha !== fp.sha) ||
|
|
638
|
+
(rec.gitDirty !== null && rec.gitDirty !== undefined && dirty !== null && dirty !== rec.gitDirty);
|
|
639
|
+
if (!changed) {
|
|
640
|
+
const ttl = resolveTTL(pDir, rec, filePath);
|
|
641
|
+
if (Date.now() - (rec.readAt || 0) < ttl.ms) {
|
|
642
|
+
if (rec.inherited) {
|
|
643
|
+
// 2.5.3(案一):卷宗继承的指纹只提示不拦——本会话从未读过,内容不在上下文,
|
|
644
|
+
// 拦首读即阻断取证。会话内真读过后(post 重录指纹),恢复免重读拦截。
|
|
645
|
+
audit(sid, "casefile-inherit", { level: null, evidence: `2.0卷宗 继承指纹放行首读 ${filePath} ttl=${ttl.src}` });
|
|
646
|
+
process.stderr.write(`[卷宗·提示]${filePath} 卷宗有近期取证(${ttl.src}),但本会话尚未读过——首读放行。`);
|
|
647
|
+
process.exit(0);
|
|
648
|
+
}
|
|
649
|
+
audit(sid, "casefile-hit", { level: null, evidence: `2.0卷宗 免重读 ${filePath} ttl=${ttl.src}` });
|
|
650
|
+
process.stderr.write(
|
|
651
|
+
`[卷宗·免重读]${filePath} 指纹一致(${rec.via || "mtime+size"})且 TTL 未超(${ttl.src}),勿重复整读;需新内容用 offset 增量读或请批示。`
|
|
652
|
+
);
|
|
653
|
+
process.exit(2);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
} catch {}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// 2.2.0 一.2:所有闸通过、本次调用确定执行 → 备份改动前内容到 .ai/backup/(新文件无内容可备份,跳过)
|
|
661
|
+
if (/^(Write|Edit)$/.test(tool) && !handoff && rawPath) backupBeforeEdit(rawPath, projectDir());
|
|
662
|
+
|
|
663
|
+
process.exit(0);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (mode === "post" || mode === "postfail") {
|
|
667
|
+
const state = loadState(path);
|
|
668
|
+
auditChainInit(state); // 3.0.0 因果链
|
|
669
|
+
let reason = null;
|
|
670
|
+
|
|
671
|
+
state.turnCount = (state.turnCount || 0) + 1;
|
|
672
|
+
// 2.5.2:删除只写不读且无上限的 state.seen(去重信号实际由 lastSig/lastInput 承担,该表只涨不用)
|
|
673
|
+
|
|
674
|
+
const tool = input.tool_name || "";
|
|
675
|
+
const ti = input.tool_input || {};
|
|
676
|
+
// 2.4.1 结构优化:响应内容单次采样,进度/污染/追责/摘要/强制场景五处检查共用(此前重复遍历 5 次)
|
|
677
|
+
const respProbe = { parts: [], total: 0 };
|
|
678
|
+
if (mode === "post") collectStrings(input.tool_response ?? {}, respProbe, 200000);
|
|
679
|
+
const respText = respProbe.parts.join("\n");
|
|
680
|
+
|
|
681
|
+
// 取证销账:L2 强制取证由一次成功调查解除
|
|
682
|
+
if (state.forcedInvestigate && mode === "post" && isInvestigation(tool, ti)) {
|
|
683
|
+
state.forcedInvestigate = false;
|
|
684
|
+
audit(sid, "L2-cleared", { level: 2, evidence: `${tool} 取证完成` });
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// 本回合已读/已写文件集合(供盲写检测):自己刚写过的文件内容已知,不构成盲写
|
|
688
|
+
if (mode === "post") {
|
|
689
|
+
state.readSet = state.readSet || {};
|
|
690
|
+
if (ti.file_path && ["Read", "Write", "Edit"].includes(tool)) state.readSet[normalize(ti.file_path)] = 1;
|
|
691
|
+
// 2.4.0:登记写入过的脚本文件及内容危险性(供脚本包装绕行检测)
|
|
692
|
+
if ((tool === "Write" || tool === "Edit") && ti.file_path && SCRIPT_FILE_RE.test(String(ti.file_path))) {
|
|
693
|
+
state.scriptFiles = state.scriptFiles || {};
|
|
694
|
+
const body = String(ti.content ?? "") + String(ti.old_string ?? "") + String(ti.new_string ?? "");
|
|
695
|
+
state.scriptFiles[normalize(ti.file_path)] = isDangerousCmd(body) ? "d" : "c";
|
|
696
|
+
}
|
|
697
|
+
if (tool === "Grep" && typeof ti.path === "string") state.readSet[normalize(ti.path)] = 1;
|
|
698
|
+
// ===== 2.0 总纲四/七:更新侦查取证记录(指纹 + TTL 依据)=====
|
|
699
|
+
// 3.0.5:派生积木不进卷宗【三】——积木内容不可变(指纹固化在 INDEX.md),动态卷宗只管动态工作区
|
|
700
|
+
if (tool === "Read" && ti.file_path && !BLOCKS_RE.test(String(ti.file_path))) {
|
|
701
|
+
try {
|
|
702
|
+
const fp = fingerprint(ti.file_path);
|
|
703
|
+
const key = normalize(ti.file_path);
|
|
704
|
+
const cc = state.caseCache || {};
|
|
705
|
+
const prev = cc[key];
|
|
706
|
+
const changedFp = !!(prev && (prev.mtime !== fp.mtime || prev.size !== fp.size || (prev.sha && fp.sha && prev.sha !== fp.sha)));
|
|
707
|
+
cc[key] = {
|
|
708
|
+
path: ti.file_path,
|
|
709
|
+
mtime: fp.mtime,
|
|
710
|
+
size: fp.size,
|
|
711
|
+
sha: fp.sha || "",
|
|
712
|
+
gitDirty: fp.sha ? null : gitDirty(projectDir(), ti.file_path),
|
|
713
|
+
readAt: Date.now(),
|
|
714
|
+
changes: (prev ? prev.changes || 0 : 0) + (changedFp ? 1 : 0),
|
|
715
|
+
lastChange: prev ? (changedFp ? Date.now() : prev.lastChange || 0) : 0,
|
|
716
|
+
ttlOverride: prev ? prev.ttlOverride || "" : "",
|
|
717
|
+
via: fp.sha ? "mtime+size+sha" : "mtime+size+git",
|
|
718
|
+
};
|
|
719
|
+
state.caseCache = cc;
|
|
720
|
+
const ccKeys = Object.keys(cc);
|
|
721
|
+
if (ccKeys.length > CASE_MAX_ROWS) {
|
|
722
|
+
// 2.4.1 极限加固:卷宗缓存上限裁剪(按取证时间淘汰最旧),防长会话无界增长
|
|
723
|
+
ccKeys.sort((a, b) => (cc[a].readAt || 0) - (cc[b].readAt || 0));
|
|
724
|
+
for (const k of ccKeys.slice(0, ccKeys.length - CASE_MAX_ROWS)) delete cc[k];
|
|
725
|
+
}
|
|
726
|
+
const pDir = projectDir();
|
|
727
|
+
if (pDir) saveCaseRecords(pDir, { ...loadCaseRecords(casePath(pDir)), ...cc });
|
|
728
|
+
} catch {}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// ===== 动态预算:进度检测引擎 =====
|
|
733
|
+
let progress = false;
|
|
734
|
+
if (mode === "post") {
|
|
735
|
+
const contentSig = respText.slice(0, 1500);
|
|
736
|
+
const inputSig = callHash(input);
|
|
737
|
+
progress =
|
|
738
|
+
(tool === "Write" || tool === "Edit") ||
|
|
739
|
+
contentSig !== (state.lastSig ?? "") ||
|
|
740
|
+
inputSig !== (state.lastInput ?? "");
|
|
741
|
+
state.lastSig = contentSig;
|
|
742
|
+
state.lastInput = inputSig;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const inv = isInvestigation(tool, ti);
|
|
746
|
+
if (progress) {
|
|
747
|
+
// 20条 三预算池:侦查(只读)/执行(改动)/委托(子代理,pre 阶段核算) 三池分列,不互相挤占
|
|
748
|
+
if (tool === "Agent") {
|
|
749
|
+
// 2.3.0:委派不占主会话执行池(委托池已在 pre 核算),仅计入进度
|
|
750
|
+
state.stalledStreak = 0;
|
|
751
|
+
} else if (inv) {
|
|
752
|
+
state.invCalls = (state.invCalls || 0) + 1;
|
|
753
|
+
state.stalledStreak = 0;
|
|
754
|
+
} else {
|
|
755
|
+
state.effectiveCalls = (state.effectiveCalls || 0) + 1;
|
|
756
|
+
state.stalledStreak = 0;
|
|
757
|
+
}
|
|
758
|
+
if ((state.effectiveCalls || 0) >= BUDGET_CAP) {
|
|
759
|
+
state.fused = true;
|
|
760
|
+
state.violations = Math.max(state.violations || 0, 3);
|
|
761
|
+
audit(sid, "stall-fuse", { level: 3, evidence: `硬上限:有效调用达 ${BUDGET_CAP}` });
|
|
762
|
+
reason = `[触发④·L3]硬上限 ${BUDGET_CAP} 次,强制熔断。输出『${FUSE_PHRASE}』+三行降级方案,等批示。`;
|
|
763
|
+
} else if ((state.effectiveCalls || 0) >= (state.taskBudget || BUDGET_DEFAULT)) {
|
|
764
|
+
state.taskBudget = Math.min((state.taskBudget || BUDGET_DEFAULT) + REFILL, BUDGET_CAP);
|
|
765
|
+
audit(sid, "budget-extend", { level: null, evidence: `自动续杯 budget=${state.taskBudget} eff=${state.effectiveCalls} stall=0` });
|
|
766
|
+
reason = `[触发④]续杯:执行池 ${state.effectiveCalls} 次达阈值,预算+${REFILL}→${state.taskBudget}。任务继续,自查是否收敛。`;
|
|
767
|
+
}
|
|
768
|
+
if (inv && !state.invWarned && (state.invCalls || 0) > (state.invCap || INV_POOL_DEFAULT)) {
|
|
769
|
+
state.invWarned = true;
|
|
770
|
+
audit(sid, "inv-pool-exceeded", { level: null, evidence: `20条 侦查池超限 inv=${state.invCalls}/${state.invCap || INV_POOL_DEFAULT}` });
|
|
771
|
+
reason = `[20条]侦查池(${state.invCap || INV_POOL_DEFAULT}次)超限。汇总证据请示追加(『追加额度』+10),或交子代理压缩侦查成本。`;
|
|
772
|
+
}
|
|
773
|
+
} else {
|
|
774
|
+
state.stalledStreak = (state.stalledStreak || 0) + 1;
|
|
775
|
+
state.ineffCalls = (state.ineffCalls || 0) + 1;
|
|
776
|
+
if ((state.stalledStreak || 0) >= STALL_FUSE) {
|
|
777
|
+
state.fused = true;
|
|
778
|
+
state.violations = Math.max(state.violations || 0, 3);
|
|
779
|
+
audit(sid, "stall-fuse", { level: 3, evidence: `连续 ${state.stalledStreak} 次无效调用 eff=${state.effectiveCalls || 0}` });
|
|
780
|
+
reason = `[触发④·L3]真失控:连续 ${state.stalledStreak} 次无效调用。停止探索,输出『${FUSE_PHRASE}』+三行降级方案,或等批示。`;
|
|
781
|
+
} else if ((state.stalledStreak || 0) === STALL_FUSE - 1) {
|
|
782
|
+
audit(sid, "stall-warning", { level: null, evidence: `停滞 ${state.stalledStreak} 次 eff=${state.effectiveCalls || 0}` });
|
|
783
|
+
reason = `[触发④]停滞预警:连续 ${state.stalledStreak} 次无效,再有一次即熔断。换有证据的方法,或结束回合发【信用延期】请批示。`;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// 58条 上下文污染检测:工具输出与指令矛盾 → 停止使用该输出并报告人类
|
|
788
|
+
if (!reason && mode === "post" && tool === "Bash") {
|
|
789
|
+
const cmd = String(ti.command || "");
|
|
790
|
+
const outLines = respText.split("\n").filter((l) => l.trim() !== "");
|
|
791
|
+
// 2.5.3(案三):输出对账只对"单条语句"有意义——复合命令(&& / ; / 换行分段)里 head 只约束
|
|
792
|
+
// 其所在段,其余段的输出无法归因,整段对账曾产生六起系统性误报。复合命令跳过本检查,宁宽勿严;
|
|
793
|
+
// 巨量输出仍有下方"体积刺客"事后追责兜底。管道 | 不分段:一条管道内 head 的承诺对整段输出有效。
|
|
794
|
+
const stmts = cmd.split(/[;&\n]+/).map((s) => s.trim()).filter(Boolean);
|
|
795
|
+
if (stmts.length === 1) {
|
|
796
|
+
const hm = cmd.match(/\bhead\s+(?:-n\s*(\d{1,6})|-(\d{1,6}))\b/);
|
|
797
|
+
if (hm) {
|
|
798
|
+
const n = parseInt(hm[1] || hm[2], 10);
|
|
799
|
+
if (n > 0 && outLines.length > n) {
|
|
800
|
+
state.pollutionFlagged = true; // 2.2.0:标记可疑 → 下次改动前须出【污染核实】
|
|
801
|
+
audit(sid, "ctx-pollution", { level: null, evidence: `58条 行数超限 head ${n} → 实际 ${outLines.length} 行 | ${cmd.slice(0, 60)}` });
|
|
802
|
+
reason = `[38条·上下文污染]输出与指令矛盾:head ${n} 行实得 ${outLines.length}。停用本次输出,echo MARK-X 隔离核实并报告人类。`;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
if (!reason && /\b(find|git\s+(ls-files|ls-tree))\b/.test(cmd)) {
|
|
806
|
+
const seen = new Set();
|
|
807
|
+
let dup = "";
|
|
808
|
+
for (const l of outLines) {
|
|
809
|
+
if (seen.has(l)) { dup = l; break; }
|
|
810
|
+
// 2.5.1:判定标准为"首 token 本身是路径"(./x/a.js、hooks/guard.mjs、C:\x\y 均算):
|
|
811
|
+
// 既排除 git 警告/说明等散文行(首 token 形如 warning:),又不放过 git ls-files 的裸相对路径
|
|
812
|
+
const tok = l.trim().split(/\s+/)[0] || "";
|
|
813
|
+
if (/[\/\\]/.test(tok) && !/[::]$/.test(tok)) seen.add(l);
|
|
814
|
+
}
|
|
815
|
+
if (dup) {
|
|
816
|
+
state.pollutionFlagged = true; // 2.2.0:标记可疑 → 下次改动前须出【污染核实】
|
|
817
|
+
audit(sid, "ctx-pollution", { level: null, evidence: `58条 路径重复 ${dup.slice(0, 80)} | ${cmd.slice(0, 50)}` });
|
|
818
|
+
reason = `[38条·上下文污染]清单出现不可能的重复路径(${dup.slice(0, 60)})。停用本次输出,echo MARK-X 隔离核实并报告人类。`;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// 巡视:Bash/Grep 巨量输出事后追责
|
|
825
|
+
if (!reason && (tool === "Bash" || tool === "Grep")) {
|
|
826
|
+
if (respProbe.total > OUTPUT_GATE_BYTES) {
|
|
827
|
+
state.dumpCount = (state.dumpCount || 0) + 1;
|
|
828
|
+
audit(sid, "scold-dump", { level: null, evidence: `${tool} 输出 ${Math.round(respProbe.total / 1024)}KB` });
|
|
829
|
+
reason =
|
|
830
|
+
`体积刺客:${tool} 输出 ${Math.round(respProbe.total / 1024)}KB 已入上下文。下次先 head/tail/wc/grep 过滤(累计 ${state.dumpCount} 次)。`;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// ===== 2.3.0 二/五:子代理摘要格式校验(只收【子代理摘要】,≤200字)=====
|
|
835
|
+
if (!reason && mode === "post" && tool === "Agent") {
|
|
836
|
+
const sumText = respText;
|
|
837
|
+
const fmtOk =
|
|
838
|
+
/【子代理摘要】/.test(sumText) &&
|
|
839
|
+
/任务[::]/.test(sumText) &&
|
|
840
|
+
/结果[::]/.test(sumText) &&
|
|
841
|
+
/异常[::]/.test(sumText) &&
|
|
842
|
+
/文件线索[::]/.test(sumText);
|
|
843
|
+
if (!fmtOk || sumText.length > 200) {
|
|
844
|
+
state.kpi = (state.kpi || 0) - 3;
|
|
845
|
+
audit(sid, "delegate-summary-pollution", { level: null, evidence: `2.3.0 摘要污染 -3 len=${sumText.length} fmt=${fmtOk ? "有" : "无"}` });
|
|
846
|
+
reason =
|
|
847
|
+
"[委派摘要拒收·KPI-3]超200字或缺字段,拒绝全量采纳。压缩为:【子代理摘要】任务:…|结果:≤5条|异常:…|文件线索:文件:行号,以此继续。";
|
|
848
|
+
} else {
|
|
849
|
+
state.kpi = (state.kpi || 0) + 3;
|
|
850
|
+
audit(sid, "kpi-summary-good", { level: null, evidence: `2.3.0 委派摘要合格 +3 len=${sumText.length}` });
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// ===== 2.3.0 三/五:强制委派场景检测(该委派不委派 → KPI-5 提醒;已委派 → KPI+5 一次)=====
|
|
855
|
+
if (!reason && mode === "post") {
|
|
856
|
+
const scLines = respText.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
857
|
+
const scPaths = scLines.filter((l) => /[\/\\]/.test(l) && l.length <= 200 && !/^(total|\.\.?|d[-rwx]|-[-rwx])/.test(l));
|
|
858
|
+
const scDirs = new Set(scPaths.map((l) => l.replace(/[\/\\][^\/\\]*$/, "")));
|
|
859
|
+
let scenario = "";
|
|
860
|
+
if (tool === "Grep" || (tool === "Bash" && /\b(find|rg|grep)\b/i.test(String(ti.command || "")))) {
|
|
861
|
+
if (scPaths.length >= 10 || scDirs.size >= 3) scenario = "全库搜索";
|
|
862
|
+
} else if (tool === "Read" && ti.file_path) {
|
|
863
|
+
let kb = 0;
|
|
864
|
+
try { kb = statSync(ti.file_path).size / 1024; } catch {}
|
|
865
|
+
if (kb > OUTPUT_GATE_BYTES / 1024 || scLines.length > 2000) scenario = "大文档摘要";
|
|
866
|
+
} else if ((tool === "Write" || tool === "Edit") && ti.file_path) {
|
|
867
|
+
state.editedFiles = state.editedFiles || {};
|
|
868
|
+
state.editedFiles[normalize(ti.file_path)] = 1;
|
|
869
|
+
if (Object.keys(state.editedFiles).length >= 5) scenario = "批量文件处理";
|
|
870
|
+
}
|
|
871
|
+
if (scenario) {
|
|
872
|
+
if (state.delegated) {
|
|
873
|
+
if (!state.kpiDelegatedAwarded) {
|
|
874
|
+
state.kpiDelegatedAwarded = true;
|
|
875
|
+
state.kpi = (state.kpi || 0) + 5;
|
|
876
|
+
audit(sid, "kpi-delegated", { level: null, evidence: `2.3.0 强制场景已委派 +5 ${scenario}` });
|
|
877
|
+
}
|
|
878
|
+
} else {
|
|
879
|
+
state.kpiScolded = state.kpiScolded || {};
|
|
880
|
+
const key = scenario === "全库搜索" ? "search" : scenario === "大文档摘要" ? "bigdoc" : "batch";
|
|
881
|
+
if (!state.kpiScolded[key]) {
|
|
882
|
+
state.kpiScolded[key] = true;
|
|
883
|
+
state.kpi = (state.kpi || 0) - 5;
|
|
884
|
+
audit(sid, "kpi-not-delegated", { level: null, evidence: `2.3.0 强制场景未委派 -5 ${scenario}` });
|
|
885
|
+
reason =
|
|
886
|
+
`[未尽职·KPI-5]${scenario}属强制委派场景(≥3目录/≥10文件、>50KB或>2000行、≥5文件批量、并行任务),本任务未委派过。` +
|
|
887
|
+
`改用 Agent 委派,只回【子代理摘要】;特例需向人类说明。`;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// 抽查A:每 5 次写操作全量审计 1 次(确定性节奏,不是随机抽样——原文案写"随机"与实现不符)
|
|
894
|
+
if (!reason && mode === "post" && (tool === "Write" || tool === "Edit")) {
|
|
895
|
+
state.writeOps = (state.writeOps || 0) + 1;
|
|
896
|
+
if (state.writeOps % RANDOM_AUDIT_EVERY === 0) {
|
|
897
|
+
audit(sid, "random-audit", { level: null, evidence: `第 ${state.writeOps} 次写操作全量审计 ${normalize(ti.file_path)}` });
|
|
898
|
+
reason =
|
|
899
|
+
`抽查A:第 ${state.writeOps} 次写操作已留痕。确认有锚点([文件:行号]),无则补【假设】。`;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
saveState(path, state);
|
|
904
|
+
if (reason) block(reason);
|
|
905
|
+
process.exit(0);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
if (mode === "stop") {
|
|
909
|
+
const state = loadState(path);
|
|
910
|
+
auditChainInit(state); // 3.0.0 因果链
|
|
911
|
+
const respText = ["response", "last_message", "message", "text", "output"]
|
|
912
|
+
.map((k) => input[k])
|
|
913
|
+
.find((v) => typeof v === "string");
|
|
914
|
+
// 2.5.0:DSH 桥接签名(dsh-hooks-claude-code 的 Stop 载荷 transcript_path 恒为空串且无收尾文本)。
|
|
915
|
+
// 该平台 Stop 打回会强制续跑且桥接无连败上限,无文本可校验时锚点/审批单打回降级为仅审计,防死循环。
|
|
916
|
+
// 2.5.2:签名判定放宽到"无收尾文本 且 transcript_path 缺失或为空串"——旧写法要求严格 === "",
|
|
917
|
+
// 载荷若连该键都没有(不同宿主/桥版本)就会被当成 ZCode,打回无限重复。
|
|
918
|
+
const dshBridge = respText === undefined && (input.transcript_path === "" || input.transcript_path === undefined);
|
|
919
|
+
// 2.5.2:宿主在"打回后再次触发 Stop"时会带 stop_hook_active=true。这是协议层的一次性信号:
|
|
920
|
+
// 见到它就不再打回,直接放行收尾,避免任何一条 block 路径变成强制续跑死循环。
|
|
921
|
+
const stopReentry = input.stop_hook_active === true;
|
|
922
|
+
let text;
|
|
923
|
+
if (respText !== undefined) {
|
|
924
|
+
text = respText;
|
|
925
|
+
} else {
|
|
926
|
+
const probe = { parts: [], total: 0 };
|
|
927
|
+
collectStrings(input, probe, 100000);
|
|
928
|
+
text = probe.parts.join("\n");
|
|
929
|
+
}
|
|
930
|
+
const formalFuse = text.includes("【熔断】");
|
|
931
|
+
|
|
932
|
+
// 任务规模声明:预算只升不降,declaredBudget 持久
|
|
933
|
+
const scale = text.match(TASK_SCALE_RE);
|
|
934
|
+
if (scale) {
|
|
935
|
+
const x = Math.min(parseInt(scale[1], 10) || 0, BUDGET_CAP);
|
|
936
|
+
if (x > (state.taskBudget || BUDGET_DEFAULT)) {
|
|
937
|
+
state.taskBudget = x;
|
|
938
|
+
audit(sid, "budget-extend", { level: null, evidence: `任务规模声明上调 budget=${x}` });
|
|
939
|
+
}
|
|
940
|
+
state.declaredBudget = Math.max(state.declaredBudget || 0, x);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// 绝境模式:本回合结论锚点检查豁免
|
|
944
|
+
if (state.mercy) {
|
|
945
|
+
state.turnCount = 0;
|
|
946
|
+
saveState(path, state);
|
|
947
|
+
process.exit(0);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// 《授权识别与留痕条例》+ 信用延期:仅在收尾无锚点(确需豁免/请示)时核验
|
|
951
|
+
if (!EVIDENCE_ANCHORS.test(text)) {
|
|
952
|
+
if (PARDON_PENDING_RE.test(text)) {
|
|
953
|
+
audit(sid, "pardon-pending", { level: null, evidence: "输出【授权待确认】暂停,等待人类明确批示" });
|
|
954
|
+
state.turnCount = 0;
|
|
955
|
+
saveState(path, state);
|
|
956
|
+
process.exit(0);
|
|
957
|
+
}
|
|
958
|
+
if (text.includes("【信用延期】")) {
|
|
959
|
+
audit(sid, "credit-request", { level: null, evidence: `eff=${state.effectiveCalls || 0} budget=${state.taskBudget} stall=${state.stalledStreak || 0}` });
|
|
960
|
+
state.turnCount = 0;
|
|
961
|
+
saveState(path, state);
|
|
962
|
+
process.exit(0);
|
|
963
|
+
}
|
|
964
|
+
if (PARDON_DECL_RE.test(text)) {
|
|
965
|
+
const q = text.match(PARDON_QUOTE_RE);
|
|
966
|
+
const b = text.match(PARDON_BASIS_RE);
|
|
967
|
+
const tp = String(state.turnPromptFull || state.turnPrompt || "").replace(/\s+/g, ""); // FG-D1:核验窗口扩到 4000 字符
|
|
968
|
+
const quote = q ? q[1].replace(/\s+/g, "") : "";
|
|
969
|
+
let invalid = "";
|
|
970
|
+
if (!q) invalid = "声明未引用人类指令原文(条例四-B)";
|
|
971
|
+
else if (!b) invalid = "声明未指明依据法条(条例四-C)";
|
|
972
|
+
else if (!quote || !tp.includes(quote))
|
|
973
|
+
invalid = "引用的人类指令原文与本回合实际指令不符,涉嫌编造或事后补(条例四-D/E)";
|
|
974
|
+
else if (!AUTH_SEMANTICS_RE.test(q[1]))
|
|
975
|
+
invalid = "引用的人类指令原文不含授权语义(条例四-E)";
|
|
976
|
+
if (invalid) {
|
|
977
|
+
state.fused = true;
|
|
978
|
+
state.violations = Math.max(state.violations || 0, 3);
|
|
979
|
+
// 2.5.2:同样加一次性保护——AI 若反复输出不合规的【授权识别】,此前每次 Stop 都会打回。
|
|
980
|
+
const first = !state.stopBlocked && !stopReentry;
|
|
981
|
+
if (first) state.stopBlocked = true;
|
|
982
|
+
audit(sid, "violation-usurp-pardon", { level: 3, evidence: invalid });
|
|
983
|
+
saveState(path, state);
|
|
984
|
+
if (first) {
|
|
985
|
+
block(
|
|
986
|
+
`[越权解释授权·L3]${invalid}。正确:【授权识别】引本回合人类指令原文+法条;未明→【授权待确认】。禁止自行推断。`
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
process.exit(0);
|
|
990
|
+
}
|
|
991
|
+
state.mercy = true;
|
|
992
|
+
state.fused = false;
|
|
993
|
+
audit(sid, "pardon-interpreted", {
|
|
994
|
+
action: "pardon-interpreted",
|
|
995
|
+
trigger: q[1],
|
|
996
|
+
level: "PARDON",
|
|
997
|
+
evidence: b[1],
|
|
998
|
+
pardon: true,
|
|
999
|
+
});
|
|
1000
|
+
saveState(path, state);
|
|
1001
|
+
process.exit(0);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// 触发⑤:熔断声明与未知跟踪
|
|
1006
|
+
if (formalFuse || text.includes("查无依据") || text.includes("查无实据")) {
|
|
1007
|
+
state.unknownStreak = (state.unknownStreak || 0) + 1;
|
|
1008
|
+
if (formalFuse || state.unknownStreak >= 2) {
|
|
1009
|
+
state.fused = true;
|
|
1010
|
+
state.violations = Math.max(state.violations || 0, 3);
|
|
1011
|
+
audit(sid, "shuanggui-declared", { level: 3, evidence: formalFuse ? "明示熔断" : `连续 ${state.unknownStreak} 次未知声明` });
|
|
1012
|
+
// 2.5.1:熔断时确保经验库存在(79条:解除后 AI 追加经验,卡点时 Grep 检索)
|
|
1013
|
+
if (formalFuse) {
|
|
1014
|
+
try {
|
|
1015
|
+
const pd = projectDir();
|
|
1016
|
+
if (pd) {
|
|
1017
|
+
const pat = join(pd, ".ai", "PATTERNS.md");
|
|
1018
|
+
if (!existsSync(pat)) {
|
|
1019
|
+
mkdirSync(dirname(pat), { recursive: true });
|
|
1020
|
+
writeFileSync(pat, "# PATTERNS 经验库\n\n> 格式:[环境:OS] [任务:类型] 以后遇到 X 必须先做 Y。熔断/返工后由 AI 追加;新任务不预读,卡点时 Grep 检索(79条)。\n");
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
} catch {
|
|
1024
|
+
noteFail(sid, "PATTERNS.md 经验库创建");
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
// 2.5.2 一次性打回:声明熔断却缺降级方案时打回一次;二次起仅审计放行,防宿主强制续跑死循环
|
|
1029
|
+
// (DSH 桥接的 Stop 打回会强制续步且无连败上限,本分支此前没有 stopBlocked 保护)。
|
|
1030
|
+
const needDowngradeBlock = formalFuse && !DOWNGRADE_MARKERS.test(text) && !state.stopBlocked && !stopReentry;
|
|
1031
|
+
if (needDowngradeBlock) state.stopBlocked = true;
|
|
1032
|
+
state.turnCount = 0;
|
|
1033
|
+
saveState(path, state);
|
|
1034
|
+
if (needDowngradeBlock) {
|
|
1035
|
+
audit(sid, "reject-no-downgrade", { level: 3, evidence: "声明熔断但缺降级方案(已打回一次,二次起只审计)" });
|
|
1036
|
+
block(DOWNGRADE_MSG);
|
|
1037
|
+
}
|
|
1038
|
+
process.exit(0);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// 双规后必须交代(2.5.2:加 stop_hook_active 一次性信号;否则打回→放行→再打回来回震荡)
|
|
1042
|
+
if (
|
|
1043
|
+
state.fused &&
|
|
1044
|
+
!EVIDENCE_ANCHORS.test(text) &&
|
|
1045
|
+
!DOWNGRADE_MARKERS.test(text) &&
|
|
1046
|
+
!state.stopBlocked &&
|
|
1047
|
+
!stopReentry
|
|
1048
|
+
) {
|
|
1049
|
+
state.stopBlocked = true;
|
|
1050
|
+
state.turnCount = 0;
|
|
1051
|
+
saveState(path, state);
|
|
1052
|
+
audit(sid, "reject-no-account", { level: 3, evidence: "双规后未交代" });
|
|
1053
|
+
block(
|
|
1054
|
+
`[触发⑤]熔断已触发未交代。输出『${FUSE_PHRASE}』+三行降级方案,禁静默结束。`
|
|
1055
|
+
);
|
|
1056
|
+
process.exit(0);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// 2.4.0 二.2:高危拒绝后收尾必须带标准审批单(格式校验,缺字段即打回);DSH 桥接无收尾文本 → 降级审计
|
|
1060
|
+
// 2.5.2:补一次性保护——此前该分支没有任何 stopBlocked 守卫,AI 若始终不出审批单会被无限打回(实测 3/3 重复)。
|
|
1061
|
+
if (state.highRiskDeniedThisTurn && !dshBridge) {
|
|
1062
|
+
const formOk =
|
|
1063
|
+
/【高危申请】/.test(text) &&
|
|
1064
|
+
/命令[::]/.test(text) &&
|
|
1065
|
+
/真实目的[::]/.test(text) &&
|
|
1066
|
+
/影响范围[::]/.test(text) &&
|
|
1067
|
+
/回滚方案[::]/.test(text) &&
|
|
1068
|
+
/允许执行\s*[??]\s*[((]y\/n[))]/.test(text);
|
|
1069
|
+
if (!formOk && !state.stopBlocked && !stopReentry) {
|
|
1070
|
+
state.stopBlocked = true;
|
|
1071
|
+
state.turnCount = 0;
|
|
1072
|
+
saveState(path, state);
|
|
1073
|
+
audit(sid, "reject-no-form", { level: null, evidence: "2.4.0 高危拒绝后未输出标准审批单(已打回一次,二次起只审计)" });
|
|
1074
|
+
block(`[高危申请缺失]本回合拒绝了高危命令,收尾必须输出标准审批单(一行,禁长篇解释):${HIGH_RISK_FORM}`);
|
|
1075
|
+
process.exit(0);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// 2.5.0:DSH 桥接降级审计(有本应校验的事项时留痕,供人类复核)
|
|
1080
|
+
if (dshBridge && ((state.turnCount || 0) >= 5 || state.highRiskDeniedThisTurn)) {
|
|
1081
|
+
audit(sid, "dsh-stop-observe", { level: null, evidence: `2.5.0 DSH 桥接无收尾文本,锚点/审批单校验降级审计 turnCalls=${state.turnCount || 0} formPending=${!!state.highRiskDeniedThisTurn}` });
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
state.unknownStreak = 0;
|
|
1085
|
+
|
|
1086
|
+
// 触发②:本回合调用≥5 且收尾无证据锚点(turnCount 由本事件清零,回合边界不依赖 reset);DSH 桥接降级审计
|
|
1087
|
+
if (
|
|
1088
|
+
(state.turnCount || 0) >= 5 &&
|
|
1089
|
+
!EVIDENCE_ANCHORS.test(text) &&
|
|
1090
|
+
!state.stopBlocked &&
|
|
1091
|
+
!stopReentry &&
|
|
1092
|
+
!dshBridge
|
|
1093
|
+
) {
|
|
1094
|
+
const tc = state.turnCount;
|
|
1095
|
+
const level = penalize(state, sid, "violation-no-anchor", `收尾无锚点,本回合 ${tc} 次调用`);
|
|
1096
|
+
state.stopBlocked = true;
|
|
1097
|
+
state.turnCount = 0;
|
|
1098
|
+
saveState(path, state);
|
|
1099
|
+
block(
|
|
1100
|
+
`[触发②·L${level}]本回合 ${tc} 次调用后无证据锚点收尾。补:结论+[文件:行号]证据链,或写 HANDOFF.md,或标【假设】;` +
|
|
1101
|
+
`确有授权→【授权识别】引原文+法条,未明→【授权待确认】。${ladderNote(level)}`
|
|
1102
|
+
);
|
|
1103
|
+
process.exit(0);
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
state.stopBlocked = false;
|
|
1107
|
+
state.turnCount = 0;
|
|
1108
|
+
// ===== 3.0.0 KPI 兑现闭环(二十七~三十二条部分机械化)=====
|
|
1109
|
+
// 收尾即结算:本任务 KPI → 等次 → 委托池奖惩(执行池增减仍由人类批示,引擎只动委托池),跨任务累计 kpiCarry。
|
|
1110
|
+
// 必须在 saveState 之前判定,否则 kpiLowReported 标记不落盘,会每次收尾重复告警。
|
|
1111
|
+
const kpiNow = state.kpi || 0;
|
|
1112
|
+
let grade = "称职";
|
|
1113
|
+
let poolDelta = 0;
|
|
1114
|
+
if (kpiNow >= 15) { grade = "优秀"; poolDelta = 5; }
|
|
1115
|
+
else if (kpiNow >= 0) { grade = "称职"; poolDelta = 0; }
|
|
1116
|
+
else if (kpiNow >= -9) { grade = "基本称职"; poolDelta = -2; }
|
|
1117
|
+
else { grade = "不称职"; poolDelta = -5; }
|
|
1118
|
+
state.delegateBudget = Math.max(0, Math.min(BUDGET_CAP, (state.delegateBudget ?? DELEGATE_DEFAULT) + poolDelta));
|
|
1119
|
+
state.kpiCarry = (state.kpiCarry || 0) + kpiNow;
|
|
1120
|
+
if (kpiNow <= -10) {
|
|
1121
|
+
if (!state.kpiLowReported) {
|
|
1122
|
+
state.kpiLowReported = true;
|
|
1123
|
+
audit(sid, "kpi-low", { level: null, evidence: `委派 KPI ${kpiNow}:强制委派场景累计失分,下任务请优先 Agent 委派(委托池独立 20 次)` });
|
|
1124
|
+
}
|
|
1125
|
+
} else if (state.kpiLowReported) {
|
|
1126
|
+
state.kpiLowReported = false; // KPI 回升到阈值以上后,再次跌破可重新提醒
|
|
1127
|
+
}
|
|
1128
|
+
saveState(path, state);
|
|
1129
|
+
// 2.0 总纲七:更新工作额度台账到卷宗
|
|
1130
|
+
const pDir = projectDir();
|
|
1131
|
+
if (pDir) saveLedger(pDir, state);
|
|
1132
|
+
// 回合诊断:与 reset-fired 对照,定位 UserPromptSubmit 是否触发
|
|
1133
|
+
const settleSeq = audit(sid, "stop-fired", { level: null, evidence: `turnCalls=${input && input.stop_hook_active !== undefined ? "有" : "?"} eff=${state.effectiveCalls || 0} inv=${state.invCalls || 0} budget=${state.taskBudget} kpi=${kpiNow}` });
|
|
1134
|
+
audit(sid, "kpi-settle", {
|
|
1135
|
+
ref: settleSeq,
|
|
1136
|
+
level: null,
|
|
1137
|
+
evidence: `二十七~三十二条 兑现 KPI=${kpiNow} 等次=${grade} 委托池${poolDelta >= 0 ? "+" : ""}${poolDelta}(现=${state.delegateBudget})跨任务累计=${state.kpiCarry}`,
|
|
1138
|
+
});
|
|
1139
|
+
process.exit(0);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
process.exit(0);
|