focus-guard 0.0.0-stage → 3.0.7
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
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
// FocusGuard 母版层 · 事前资格审核(六层判定,规格 §三)
|
|
2
|
+
//
|
|
3
|
+
// 逻辑层是法官:自己判简单的(状态 / 红线 / 资格 / 前置),复杂的送模型(鉴定科)出信号,
|
|
4
|
+
// 最终 allow / deny / needApproval 一律由本层裁决。模型只出信号,不出裁决。
|
|
5
|
+
//
|
|
6
|
+
// 依赖注入:redlines / model / audit / grants / profile 全部由调用方传入 —— 母版不反向依赖适配层
|
|
7
|
+
// (《资料与代码分层总规范》二·1)。model 未注入时跳过第 5 层,故第一步零模型即可用。
|
|
8
|
+
// 审计一律走注入的 audit:本文件不直接写 AUDIT.log(测试注入 mock,不污染真实审计)。
|
|
9
|
+
//
|
|
10
|
+
// 3.0.5 增补(画像驱动):
|
|
11
|
+
// · 接收 profile;每层判定前查 layerEnabled(profile, 层键),被画像禁用的层整体跳过,
|
|
12
|
+
// 并在 trace 里记 { decision:"skip", reason:"profile:xxx off" };
|
|
13
|
+
// · L1 按领导批复拆成两个子判据:1-fuse(stalledFuse,管 fused/probation)、
|
|
14
|
+
// 1-budget(budgetGate,管 taskBudget);两者同用层号 "1",全关才整层 skip;
|
|
15
|
+
// · reasoningWatch / emotionFilter 系已登记未实现,只在 trace 追加 skip(附原因),不参与执行;
|
|
16
|
+
// · **不传 profile 时行为与 3.0.4 完全一致**(全部层启用、不追加任何 skip 条目)。
|
|
17
|
+
|
|
18
|
+
import { isMutating } from "./risk.mjs";
|
|
19
|
+
import { createGrantTable } from "./grants.mjs";
|
|
20
|
+
import { layerEnabled, unimplementedSwitches } from "./profileLoader.mjs";
|
|
21
|
+
import { redlineExempt } from "./redlines.mjs";
|
|
22
|
+
|
|
23
|
+
/** 第 5 层语义风险的审批阈值(与 pipeline 的 RISK_ASK_THRESHOLD 同值)。 */
|
|
24
|
+
export const RISK_ASK_THRESHOLD = 0.85;
|
|
25
|
+
|
|
26
|
+
/** 第 4 层·系统路径:命中即 deny(配置与系统目录不属业务改动面)。 */
|
|
27
|
+
export const SYSTEM_PATH_RE =
|
|
28
|
+
/(?:^|[\s"'=:,])(?:[A-Za-z]:[\\/]Windows(?:[\\/]|$)|[A-Za-z]:[\\/]Program Files(?:[\\/]|$)|\/etc(?:\/|$)|\/usr(?:\/|$)|\/bin(?:\/|$)|\/sbin(?:\/|$)|\/System(?:\/|$))/im;
|
|
29
|
+
|
|
30
|
+
/** 第 4 层·特殊目录:命中需人工审批(改之会污染版本库或依赖树)。 */
|
|
31
|
+
export const SPECIAL_DIR_RE = /(?:^|[\s"'=:,]|[\\/])(?:\.git|node_modules)(?:[\\/]|$)/i;
|
|
32
|
+
|
|
33
|
+
/** 第 3 层·高危工具/命令特征库。带 id,便于审计与测试定点。 */
|
|
34
|
+
export const HIGH_RISK_TOOLS = [
|
|
35
|
+
{ id: "rm-rf", re: /\brm\s+(?:-{1,2}[\w-]+\s+)*-\w*(?:r\w*f|f\w*r)\w*/ },
|
|
36
|
+
{ id: "format-volume", re: /\bFormat-Volume\b|\bmkfs\b|\bdiskpart\b|\bformat\s+[a-z]:/i },
|
|
37
|
+
{ id: "git-push-force", re: /\bgit\s+push\b[^\n]*\s(?:-f|--force(?:-with-lease)?)\b/i },
|
|
38
|
+
{ id: "drop-database", re: /\b(?:drop\s+(?:database|schema)|truncate\s+table)\b/i },
|
|
39
|
+
{ id: "publish", re: /\b(?:npm|pnpm|yarn)\s+publish\b|\bdocker\s+push\b/i },
|
|
40
|
+
{ id: "global-install", re: /\b(?:npm|pnpm)\s+(?:i|install|add)\b[^\n]*\s-g(?:\s|$)|\byarn\s+global\s+add\b/i },
|
|
41
|
+
{ id: "chmod-wide", re: /\bchmod\s+[^\n]*\b777\b|\bchmod\s+-R\b/i },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 不可逆高危类别:用于 approvalGate.scope='irreversible' 时的审批范围判定。
|
|
46
|
+
* 执行后无法回滚的才计入;publish / global-install / chmod 属可逆或可撤销,不计入。
|
|
47
|
+
*/
|
|
48
|
+
export const IRREVERSIBLE_IDS = ["rm-rf", "format-volume", "drop-database", "git-push-force", "write-system-path"];
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 取画像的审批范围。缺字段或非法值一律按最严 'mutating'(保守优先)。
|
|
52
|
+
* 注意:本函数只决定"哪些类别需审批",**不决定审批层是否启用**——审批层不可被画像关闭。
|
|
53
|
+
*/
|
|
54
|
+
export function profileScope(profile) {
|
|
55
|
+
const entry = profile && profile.approvalGate;
|
|
56
|
+
const scope = entry && typeof entry === "object" ? entry.scope : undefined;
|
|
57
|
+
return scope === "irreversible" ? "irreversible" : "mutating";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** 命中哪条高危特征(未命中返回 null)。 */
|
|
61
|
+
export function highRiskOf(command) {
|
|
62
|
+
const cmd = String(command || "");
|
|
63
|
+
return HIGH_RISK_TOOLS.find((r) => r.re.test(cmd)) || null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 六层资格审核。async:第 5 层要 await 模型信号。
|
|
68
|
+
*
|
|
69
|
+
* @param {object} req
|
|
70
|
+
* @param {string} req.tool 工具名
|
|
71
|
+
* @param {string} [req.command] 命令或操作描述
|
|
72
|
+
* @param {string} req.purpose 声称的目的(必填,空白即 deny)
|
|
73
|
+
* @param {string} req.scope 声称的影响范围(必填,空白即 deny)
|
|
74
|
+
* @param {"turn"|"task"|"persist"} [req.ttl="turn"]
|
|
75
|
+
* @param {object} [req.state] guard 侧只读状态:fused / probation / taskBudget / readSet
|
|
76
|
+
* @param {string} [req.target] 改动类操作的目标路径(供第 4 层比对 readSet;缺省则跳过该项)
|
|
77
|
+
* @param {Array<{name: string, re: RegExp}>} [req.redlines] 第 2 层绝对红线表(适配层注入)
|
|
78
|
+
* @param {Function} [req.model] 第 5 层探针:({command,purpose,scope}) => {mismatch,risk,reason,actual_effect}
|
|
79
|
+
* @param {Function} [req.audit] 审计写入(注入;本函数不碰真实 AUDIT.log)
|
|
80
|
+
* @param {object} [req.grants] 授权表(默认新建一张)
|
|
81
|
+
* @param {Function} [req.trace] 逐层回调,用于打印/断言每层判定
|
|
82
|
+
* @param {object|null} [req.profile] 画像对象;缺省 null = 全部层启用(与 3.0.4 行为一致)
|
|
83
|
+
* @returns {Promise<{decision: "allow"|"deny"|"needApproval", layer: string, reason: string, modelSignal: object|null, trace: Array}>}
|
|
84
|
+
*/
|
|
85
|
+
export async function checkEligibility({
|
|
86
|
+
tool,
|
|
87
|
+
command = "",
|
|
88
|
+
purpose,
|
|
89
|
+
scope,
|
|
90
|
+
ttl = "turn",
|
|
91
|
+
state = {},
|
|
92
|
+
target,
|
|
93
|
+
redlines = [],
|
|
94
|
+
model,
|
|
95
|
+
audit,
|
|
96
|
+
grants = createGrantTable(),
|
|
97
|
+
trace,
|
|
98
|
+
profile = null,
|
|
99
|
+
} = {}) {
|
|
100
|
+
const steps = [];
|
|
101
|
+
const mark = (layer, decision, reason) => {
|
|
102
|
+
const row = { layer, decision, reason };
|
|
103
|
+
steps.push(row);
|
|
104
|
+
if (typeof trace === "function") trace(row);
|
|
105
|
+
return row;
|
|
106
|
+
};
|
|
107
|
+
const skippedRows = () => steps.filter((s) => s.decision === "skip").map((s) => ({ layer: s.layer, reason: s.reason }));
|
|
108
|
+
const settle = (decision, layer, reason, modelSignal = null) => {
|
|
109
|
+
if (typeof audit === "function") {
|
|
110
|
+
audit({
|
|
111
|
+
action: decision === "allow" ? "grant" : decision,
|
|
112
|
+
tool,
|
|
113
|
+
command: String(command || "").slice(0, 120),
|
|
114
|
+
purpose,
|
|
115
|
+
scope,
|
|
116
|
+
ttl,
|
|
117
|
+
layer,
|
|
118
|
+
decision,
|
|
119
|
+
modelSignal,
|
|
120
|
+
reason,
|
|
121
|
+
evidence: reason,
|
|
122
|
+
skipped: skippedRows(),
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return { decision, layer, reason, modelSignal, trace: steps };
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// 第 0 层:申请完整性(规格 §五:purpose / scope 必填,空白即 deny)。无画像开关,永不跳过。
|
|
129
|
+
if (!String(purpose ?? "").trim()) {
|
|
130
|
+
mark("0", "deny", "purpose 缺失");
|
|
131
|
+
return settle("deny", "0", "purpose 缺失:申请必须写明目的");
|
|
132
|
+
}
|
|
133
|
+
if (!String(scope ?? "").trim()) {
|
|
134
|
+
mark("0", "deny", "scope 缺失");
|
|
135
|
+
return settle("deny", "0", "scope 缺失:申请必须写明影响范围");
|
|
136
|
+
}
|
|
137
|
+
mark("0", "pass", "申请完整");
|
|
138
|
+
|
|
139
|
+
// 第 1 层:状态(拆两个子判据,共用层号 "1";两者皆关才整层跳过)
|
|
140
|
+
const fuseOn = layerEnabled(profile, "1-fuse");
|
|
141
|
+
const budgetOn = layerEnabled(profile, "1-budget");
|
|
142
|
+
if (!fuseOn && !budgetOn) {
|
|
143
|
+
mark("1", "skip", "profile: stalledFuse 与 budgetGate 均关闭");
|
|
144
|
+
} else {
|
|
145
|
+
if (fuseOn && state.fused) {
|
|
146
|
+
mark("1", "deny", "熔断中");
|
|
147
|
+
return settle("deny", "1", "熔断中,仅允许只读");
|
|
148
|
+
}
|
|
149
|
+
if (fuseOn && state.probation) {
|
|
150
|
+
mark("1", "deny", "降权中");
|
|
151
|
+
return settle("deny", "1", "降权中,资格审核暂停");
|
|
152
|
+
}
|
|
153
|
+
const budget =
|
|
154
|
+
typeof state.taskBudget === "number" ? state.taskBudget : typeof state.budget === "number" ? state.budget : undefined;
|
|
155
|
+
if (budgetOn && typeof budget === "number" && budget <= 0) {
|
|
156
|
+
mark("1", "deny", "预算耗尽");
|
|
157
|
+
return settle("deny", "1", "预算耗尽");
|
|
158
|
+
}
|
|
159
|
+
mark("1", "pass", fuseOn && budgetOn ? "状态正常" : `状态正常(${fuseOn ? "budgetGate" : "stalledFuse"} 子判据已关)`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 第 2 层:绝对红线(命中即 deny,不进第 3 层)
|
|
163
|
+
// 3.0.6 补口(HANDOFF §十 缺陷 2):命中后先过上下文豁免,与 pipeline 文本层同口径。
|
|
164
|
+
// 豁免成立则**降级**(不 deny,继续往下走),绝不直接放行。
|
|
165
|
+
if (!layerEnabled(profile, "2")) {
|
|
166
|
+
mark("2", "skip", "profile:redlineGate off");
|
|
167
|
+
} else {
|
|
168
|
+
const hit = (Array.isArray(redlines) ? redlines : []).find(
|
|
169
|
+
(r) => r && r.re instanceof RegExp && r.re.test(String(command || "")),
|
|
170
|
+
);
|
|
171
|
+
if (hit) {
|
|
172
|
+
const exempt = redlineExempt(String(command || ""), hit);
|
|
173
|
+
if (exempt) {
|
|
174
|
+
mark("2", "pass", `红线豁免降级(${exempt.basis}):${hit.name}`);
|
|
175
|
+
} else {
|
|
176
|
+
mark("2", "deny", `绝对红线 ${hit.name}`);
|
|
177
|
+
return settle("deny", "2", `命中绝对红线「${hit.name}」,直接拒绝(不弹审批)`);
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
mark("2", "pass", "未命中绝对红线");
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 第 3 层:资格(高危且本会话无授权 → needApproval;已有授权则继续)
|
|
185
|
+
// 3.0.6 修正(HANDOFF §十 缺陷 4/5 + 安全项):审批层**不可被画像关闭**(否则闸只剩授权表
|
|
186
|
+
// 一道防线),故不再查 layerEnabled(profile,"3");scope 是唯一可调维度。
|
|
187
|
+
// 3.0.7 判定(回应外部接手模型提问「L3 是否也该接 redlineExempt」):**不接,是设计决策而非口径遗漏**。
|
|
188
|
+
// 闸(gateToolCall)与 L3 语义不同——
|
|
189
|
+
// · 闸:「无授权即 deny」,无人类环节,所以它必须自判上下文豁免,否则引号内数据被直接拒绝;
|
|
190
|
+
// · L3:「命中高危 → 转人工审批」,人类环节本身就是裁决,且人看得到完整命令。
|
|
191
|
+
// 若 L3 也接豁免,"数据形态的高危命令"会无人审批直接放行 —— 风险不对称,故保留转审批。
|
|
192
|
+
{
|
|
193
|
+
const scope = profileScope(profile);
|
|
194
|
+
const risky = highRiskOf(command);
|
|
195
|
+
const inScope = Boolean(risky) && (scope === "mutating" || IRREVERSIBLE_IDS.includes(risky.id));
|
|
196
|
+
if (inScope && !grants.has(tool)) {
|
|
197
|
+
mark("3", "needApproval", `高危 ${risky.id} 且无授权(scope=${scope})`);
|
|
198
|
+
return settle("needApproval", "3", `${tool} 命中高危特征「${risky.id}」且本会话无授权,需人工审批`);
|
|
199
|
+
}
|
|
200
|
+
if (!risky) mark("3", "pass", "非高危");
|
|
201
|
+
else if (!inScope) mark("3", "pass", `超出审批范围(scope=${scope}):${risky.id}`);
|
|
202
|
+
else mark("3", "pass", `已有授权(${risky.id})`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 第 4 层:前置条件(整体受 evidenceGate 控制)
|
|
206
|
+
if (!layerEnabled(profile, "4")) {
|
|
207
|
+
mark("4", "skip", "profile:evidenceGate off");
|
|
208
|
+
} else {
|
|
209
|
+
if (target && isMutating(tool, { command }, false)) {
|
|
210
|
+
const readSet = state.readSet || {};
|
|
211
|
+
if (!readSet[target]) {
|
|
212
|
+
mark("4", "deny", "未取证");
|
|
213
|
+
return settle("deny", "4", `改动 ${target} 前未取证(readSet 无此目标)`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (SYSTEM_PATH_RE.test(String(command || ""))) {
|
|
217
|
+
mark("4", "deny", "系统路径");
|
|
218
|
+
return settle("deny", "4", "命令涉及系统路径(C:\\Windows / /etc / /usr 等),禁止");
|
|
219
|
+
}
|
|
220
|
+
if (SPECIAL_DIR_RE.test(String(command || ""))) {
|
|
221
|
+
mark("4", "needApproval", "特殊目录");
|
|
222
|
+
return settle("needApproval", "4", "命令涉及 .git/ 或 node_modules/,需人工审批");
|
|
223
|
+
}
|
|
224
|
+
mark("4", "pass", "前置条件通过");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 第 5 层:语义对齐(仅注入了 model 时生效;未注入则跳过,第一步零模型可跑)
|
|
228
|
+
let modelSignal = null;
|
|
229
|
+
if (typeof model === "function") {
|
|
230
|
+
try {
|
|
231
|
+
modelSignal = await model({ command, purpose, scope });
|
|
232
|
+
} catch (error) {
|
|
233
|
+
modelSignal = { mismatch: false, risk: 0, reason: "model-unavailable", error: String((error && error.message) || error) };
|
|
234
|
+
}
|
|
235
|
+
if (!modelSignal || typeof modelSignal !== "object") {
|
|
236
|
+
modelSignal = { mismatch: false, risk: 0, reason: "model-unavailable" };
|
|
237
|
+
}
|
|
238
|
+
if (modelSignal.reason === "model-unavailable") {
|
|
239
|
+
mark("5", "needApproval", "模型不可用");
|
|
240
|
+
return settle("needApproval", "5", "模型审核失败,保守审批", modelSignal);
|
|
241
|
+
}
|
|
242
|
+
if (modelSignal.mismatch === true) {
|
|
243
|
+
mark("5", "deny", "目的不符");
|
|
244
|
+
return settle("deny", "5", `声称目的与实际效果不符:${String(modelSignal.actual_effect || modelSignal.reason || "")}`, modelSignal);
|
|
245
|
+
}
|
|
246
|
+
const risk = Number(modelSignal.risk);
|
|
247
|
+
if (Number.isFinite(risk) && risk > RISK_ASK_THRESHOLD) {
|
|
248
|
+
mark("5", "needApproval", `语义风险 ${risk}`);
|
|
249
|
+
return settle("needApproval", "5", `语义风险 ${risk} 超过阈值 ${RISK_ASK_THRESHOLD},转人工审批`, modelSignal);
|
|
250
|
+
}
|
|
251
|
+
mark("5", "pass", "语义一致");
|
|
252
|
+
} else {
|
|
253
|
+
mark("5", "skip", "未注入模型探针");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// 已登记未实现的开关:只在传了画像时登记进 trace,不参与执行
|
|
257
|
+
if (profile) {
|
|
258
|
+
for (const s of unimplementedSwitches(profile)) {
|
|
259
|
+
mark(s.name, "skip", s.reason);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// 第 6 层:通过 → 临时开放该工具 + 审计
|
|
264
|
+
const granted = grants.grant(tool, {
|
|
265
|
+
ttl,
|
|
266
|
+
reason: `${String(purpose).trim()} / ${String(scope).trim()}`,
|
|
267
|
+
layer: "6",
|
|
268
|
+
});
|
|
269
|
+
mark("6", "allow", "已临时开放");
|
|
270
|
+
return settle("allow", "6", `资格审核通过,已按 ttl=${granted.ttl} 临时开放 ${tool}`, modelSignal);
|
|
271
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// FocusGuard 母版层 · 常量与模板(v3.0.5;《资料与代码分层总规范》二·1)
|
|
2
|
+
// 阈值、窗口、版本号、固定文本模板与规则类正则。纯数据:无逻辑、无外部依赖。
|
|
3
|
+
|
|
4
|
+
export const OUTPUT_GATE_BYTES = 50 * 1024; // 触发③:体积闸值
|
|
5
|
+
export const RANDOM_AUDIT_EVERY = 5; // 抽查A:每 N 次写操作全量审计 1 次
|
|
6
|
+
export const BUDGET_DEFAULT = 10; // 默认任务预算
|
|
7
|
+
export const BUDGET_CAP = 200; // 硬上限:达到强制熔断
|
|
8
|
+
export const REFILL = 10; // 自动续杯步长
|
|
9
|
+
export const STALL_FUSE = 3; // 连续无效调用 → L3 熔断
|
|
10
|
+
export const MERCY_SHORT = 30; // 特赦短语仅认短指令(trim 后 ≤30 字符),防协议文本误触
|
|
11
|
+
export const ENGINE_VERSION = "3.0.7"; // 42条:部署版本核验基准(须与五处清单及本文件头注释一致,见验收"版本一致性"用例)
|
|
12
|
+
export const INV_POOL_DEFAULT = 15; // 20条:侦查池独立额度(批示可追加)
|
|
13
|
+
export const SHA_LIMIT = 200 * 1024; // 总纲四:SHA-256 校验上限(≤200KB)
|
|
14
|
+
export const CASE_MAX_ROWS = 200; // 卷宗【三】最大行数(超出淘汰最旧)
|
|
15
|
+
export const TTL_FIRST = 4 * 3600e3; // 自适应:首次 4h
|
|
16
|
+
export const TTL_RECENT = 2 * 3600e3; // 自适应:7天内有变 2h
|
|
17
|
+
export const TTL_WEEK = 24 * 3600e3; // 自适应:7-30天未变 24h
|
|
18
|
+
export const TTL_STABLE = 7 * 86400e3; // 自适应:30天未变 7天
|
|
19
|
+
|
|
20
|
+
export const FUSE_PHRASE = "【熔断】无法通过现有资料定位核心问题";
|
|
21
|
+
export const FUSE_HINT = "1【最小复现】步骤/实验 2【联网证据】链接+原文 3【卡点记录】写HANDOFF.md"; // 各一行
|
|
22
|
+
// 3.0.5 资料分层(《资料与代码分层总规范》):.ai/library 为外部原文静态区(只同步不就地改);
|
|
23
|
+
// 派生积木视图由 library-build 生成到 .ai/output/library(R6 记忆更新隔离区的机械化)
|
|
24
|
+
export const LIBRARY_RE = /(^|[\\/])\.ai[\\/]library[\\/]/i;
|
|
25
|
+
export const BLOCKS_RE = /(^|[\\/])\.ai[\\/]output[\\/]library[\\/]/i;
|
|
26
|
+
// 3.0.0 解释器黑名单(R5-3 采纳):解释器 -c/--eval 一类等价任意代码执行,不再判只读侦查(熔断期不放行、不进侦查池)
|
|
27
|
+
export const INTERP_EVAL_RE = /\b(?:python3?|py|node|perl|ruby|php|lua|pwsh|powershell)\b[^&|;]*(?:-c|-e|--eval|--command)\b/i;
|
|
28
|
+
// 2.4.0:标准审批单(一行,禁长篇解释)
|
|
29
|
+
export const HIGH_RISK_FORM =
|
|
30
|
+
"【高危申请】命令:`<真实命令>` | 真实目的:<一句话> | 影响范围:<具体文件/表/系统> | 回滚方案:<可否回滚> | 允许执行?(y/n)";
|
|
31
|
+
export const MERCY_RE = /允许基于有限信息(进行)?猜测|(开启|启动|进入|批准|授予)绝境模式|【特赦】|(^|[\s,。!?,!?])特赦(?=$|[\s,。!?,!?])/;
|
|
32
|
+
export const CREDIT_RE = /继续|放行|延长/; // 信用延期批复(短指令)
|
|
33
|
+
export const STOP_ORDER_RE = /熔断|停止|^停$/; // 停止批复(短指令)
|
|
34
|
+
export const TASK_SCALE_RE = /【任务规模】[^0-9]{0,8}(\d{1,3})/;
|
|
35
|
+
export const KEY50_RE = /审计|红队|重构|全量|批量|探索|遍历|升级|补丁/;
|
|
36
|
+
export const KEY15_RE = /修复|添加|修改|重命名|删除/;
|
|
37
|
+
// 《授权识别与留痕条例》声明核验
|
|
38
|
+
export const PARDON_DECL_RE = /【授权识别】/;
|
|
39
|
+
export const PARDON_PENDING_RE = /【授权待确认】/;
|
|
40
|
+
export const PARDON_QUOTE_RE = /【授权识别】[\s\S]{0,60}?「([^「」\n]{2,120})」/;
|
|
41
|
+
export const PARDON_BASIS_RE = /依据[::]\s*【?([^】\n,。;]{2,50})/;
|
|
42
|
+
export const AUTH_SEMANTICS_RE = /授权|特赦|赦免|批准|允许|豁免|跳过|绕过|无需|不用|猜测/;
|
|
43
|
+
export const DOWNGRADE_MARKERS = /最小复现|复现请求|排查实验|联网证据|外部搜寻|卡点记录|HANDOFF\.md|交接报告/i;
|
|
44
|
+
export const EVIDENCE_ANCHORS = /:\d+|日志原文|报错|HANDOFF\.md|交接报告|【假设】|【熔断】/;
|
|
45
|
+
export const RISKY_FILE_RE = /(^|\/)(package(-lock)?\.json|[^\/]*\.lock|tsconfig\.json|AGENTS\.md|CLAUDE\.md|Dockerfile|[^\/]*\.env[^\/]*|zcode\.json|[^\/]*\.csproj|[^\/]*\.sln)$|\.github\/|\.zcode-plugin\//i;
|
|
46
|
+
// 2.5.2:敏感文件(明文密钥与凭据)——改动前不做明文副本,只留痕(.pub 公钥不在此列)
|
|
47
|
+
export const SECRET_FILE_RE = /(^|\/)(?:\.env(?:\.[^\/]*)?|\.npmrc|\.netrc|\.git-credentials|\.pgpass|\.htpasswd|id_rsa|id_ed25519|id_ecdsa)$|[^\/]*\.(?:pem|key|pfx|p12|jks)$/i;
|
|
48
|
+
// 2.5.2 变更类命令判定:按"命令词"识别,并剥掉 sudo/env/xargs/时间前缀等外壳。
|
|
49
|
+
// 旧写法(`(^|[;&|]\s*)(rm|mv|…)`)要求命令词紧跟段首,于是 `sudo mv`、`xargs mv`、`cp`(当时根本不在表里)
|
|
50
|
+
// 全被判成"只读侦查"——既绕过触发①(未取证就改),又在熔断期拿到放行。
|
|
51
|
+
export const BACKUP_KEEP = 100; // 2.2.0:.ai/backup/ 最大保留份数(超出淘汰最旧)
|
|
52
|
+
export const DELEGATE_DEFAULT = 20; // 2.3.0:委托池默认额度(独立于执行池;批示『追加额度』三池各+10)
|
|
53
|
+
export const DOWNGRADE_MSG =
|
|
54
|
+
"[触发⑤·熔断]改动类已拒(只读放行)。三选一各一行:" + FUSE_HINT +
|
|
55
|
+
"。禁静默禁硬凑。解除后经验按 [环境:x][任务:y] 记入 .ai/PATTERNS.md。";
|
|
56
|
+
|
|
57
|
+
export const SESSION_RULES =
|
|
58
|
+
"<focus-guard AI履职执法模型v3.0:日常零打扰,只看行为>" +
|
|
59
|
+
"【触发】①未取证就改 ②≥5次收尾无[文件:行号]锚点且无【假设】 ③整读>50KB/Grep无head_limit/裸cat ④超预算 ⑤查无实据硬凑。" +
|
|
60
|
+
"【额度】批示关键词定(50/15/10),【任务规模】可上调;侦查/执行/委托三池分立,执行满+10上限200。" +
|
|
61
|
+
"【进度】3次无效→熔断;停滞2次→【信用延期】(继续/放行/延长→+10)。" +
|
|
62
|
+
"【处罚】L1打回→L2取证→L3熔断(只读放行)→L4记档→L5降权→L6上报;人类指令=批示。" +
|
|
63
|
+
"【熔断出口】『" + FUSE_PHRASE + "』+三行降级方案。" +
|
|
64
|
+
"【特赦】仅认短指令(绝境模式/允许猜测/【特赦】);受权须先出【授权识别】(引原文+法条),否则越权。" +
|
|
65
|
+
"【留痕】全程记<工作区>/.focus-guard/AUDIT.log(因果链chain/seq)。细则见focus-thinking技能与docs/RULES.md。";
|
|
66
|
+
|
|
67
|
+
// ============ 2.0 卷宗(总纲二:.ai/CASE_FILE.md 四册) ============
|
|
68
|
+
|
|
69
|
+
export const CASE_TEMPLATE =
|
|
70
|
+
"# FocusGuard 卷宗(CASE_FILE)\n\n" +
|
|
71
|
+
"> 引擎自动维护【一】【三】【四】;【二】由人工填写。请勿手工重排结构。【三】TTL 列留空=自适应,人工填写(如 30天/1小时)=覆盖。\n\n" +
|
|
72
|
+
"### 【一】环境声明(会话启动检测,全程复用)\n\n(SessionStart 自动写入检测结果并全程复用,人类可在此直接查阅)\n\n" +
|
|
73
|
+
"### 【二】项目依赖声明(人工填写,可覆盖自动 TTL)\n\n" +
|
|
74
|
+
"| 依赖名 | 版本 | 安装路径 | 更新频率 | 信任TTL | 备注 |\n|---|---|---|---|---|---|\n\n" +
|
|
75
|
+
"### 【三】侦查取证记录(插件自动追加)\n\n" +
|
|
76
|
+
"| 文件名 | 读取时间 | mtime | size | SHA-256 | 变更历史 | TTL | 验证方式 |\n|---|---|---|---|---|---|---|---|\n\n" +
|
|
77
|
+
"### 【四】工作额度台账\n\n" +
|
|
78
|
+
"| 任务 | 初始额度 | 已用额度 | 剩余额度 | 有效调用 | 无效调用 | 更新时间 | KPI |\n|---|---|---|---|---|---|---|---|\n";
|
|
79
|
+
|
|
80
|
+
export const HIGH_RISK_QUEUE_MAX = 10; // 待批队列上限(防状态无界)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// FocusGuard 母版层 · 模型决策层(3.0.5)
|
|
2
|
+
//
|
|
3
|
+
// 职责:request + state + profile → 调 checkEligibility → 汇总 decision / layer / skipped。
|
|
4
|
+
// 纪律:本层**不做任何判定**,只做编排与汇总;判定权全在 checkEligibility(逻辑层是法官)。
|
|
5
|
+
// 跳过的层由 checkEligibility 写进 trace(decision:"skip"),本层负责提炼成 skipped 清单,
|
|
6
|
+
// 供适配层写进 AUDIT.log 的 trace 字段。
|
|
7
|
+
|
|
8
|
+
import { checkEligibility } from "./checkEligibility.mjs";
|
|
9
|
+
import { loadProfile } from "./profileLoader.mjs";
|
|
10
|
+
|
|
11
|
+
/** 从 trace 里取出被跳过的层 { layer, reason }[]。 */
|
|
12
|
+
export function skippedFromTrace(trace = []) {
|
|
13
|
+
return trace
|
|
14
|
+
.filter((s) => s && s.decision === "skip")
|
|
15
|
+
.map((s) => ({ layer: s.layer, reason: s.reason }));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** 从 trace 里取出真正参与判定的层(decision 非 skip)。 */
|
|
19
|
+
export function activeFromTrace(trace = []) {
|
|
20
|
+
return trace.filter((s) => s && s.decision !== "skip");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 按画像决策一次。
|
|
25
|
+
* @param {object} req
|
|
26
|
+
* @param {string} [req.modelId] 模型标识;未给 profile 时用它加载画像(匹配不到即 default)
|
|
27
|
+
* @param {object} [req.profile] 直接传入画像对象(优先于 modelId)
|
|
28
|
+
* @param {Function} [req.audit] 审计写入(注入;checkEligibility 会带 skipped 一起写)
|
|
29
|
+
* @param {object} [req.state] 会话状态
|
|
30
|
+
* @returns {Promise<{decision, layer, reason, modelSignal, profileId, profileSource, skipped, trace}>}
|
|
31
|
+
*/
|
|
32
|
+
export async function decide({ modelId, profile, ...request } = {}) {
|
|
33
|
+
const resolved = profile ?? loadProfile(modelId);
|
|
34
|
+
const result = await checkEligibility({ ...request, profile: resolved });
|
|
35
|
+
return {
|
|
36
|
+
decision: result.decision,
|
|
37
|
+
layer: result.layer,
|
|
38
|
+
reason: result.reason,
|
|
39
|
+
modelSignal: result.modelSignal,
|
|
40
|
+
profileId: resolved.id,
|
|
41
|
+
profileSource: resolved.profileSource,
|
|
42
|
+
skipped: skippedFromTrace(result.trace),
|
|
43
|
+
trace: result.trace,
|
|
44
|
+
};
|
|
45
|
+
}
|
package/src/core/env.mjs
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// FocusGuard 母版层 · 平台环境(v3.0.5;《资料与代码分层总规范》二·1)
|
|
2
|
+
// shell/编码/大小写检测与平台命令规则;项目根由适配层以参数传入(母版不认 harness)。
|
|
3
|
+
import {
|
|
4
|
+
readFileSync, writeFileSync, rmSync, statSync, appendFileSync,
|
|
5
|
+
mkdirSync, existsSync, renameSync, realpathSync, readdirSync, copyFileSync,
|
|
6
|
+
} from "node:fs";
|
|
7
|
+
import { join, dirname, sep, basename, relative } from "node:path";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
import { execFileSync } from "node:child_process";
|
|
11
|
+
|
|
12
|
+
export function quickShellId() {
|
|
13
|
+
// 2026-10-18 修(REPORT-2026-10-12 P2「三处 shell 判定互相矛盾」):
|
|
14
|
+
// 原实现只看**宿主 Node 进程自身**的 SHELL/ComSpec,于是 DSH 下报成 cmd,
|
|
15
|
+
// 而 DSH 的 shell 工具实际执行体是 pwsh(cordis.patch.yml 的 pwshPath)——
|
|
16
|
+
// 卷宗【一】、env-fingerprint、实际执行三方长期不一致,替代规则与实际 shell 脱节。
|
|
17
|
+
// 修法:① 显式 FG_SHELL 最高优先(可测、可覆盖);② DSH 宿主直接判 powershell。
|
|
18
|
+
const forced = String(process.env.FG_SHELL || "").trim().toLowerCase();
|
|
19
|
+
if (forced) return forced;
|
|
20
|
+
if (process.platform === "win32") {
|
|
21
|
+
const sh = String(process.env.SHELL || "");
|
|
22
|
+
// 2.5.2:分别识别,别把 zsh/sh 一律报成 bash——旧写法 /bash|zsh|sh\b/ 命中后硬返回 "bash",
|
|
23
|
+
// 于是 bash↔zsh 之间的切换在总纲三的"shell 变化才重检"里永远检测不到。
|
|
24
|
+
if (/bash/i.test(sh)) return "bash";
|
|
25
|
+
if (/zsh/i.test(sh)) return "zsh";
|
|
26
|
+
if (/(^|[\\/])sh(\.exe)?$/i.test(sh)) return "sh";
|
|
27
|
+
// 2.0.1 修复:PSModulePath 系统级恒存(Windows PowerShell 5.0 起写入机器环境),
|
|
28
|
+
// 不足以证明当前是 PowerShell 会话;仅认 pwsh7 特征路径 / ComSpec 指向 PowerShell。
|
|
29
|
+
// 其余一律落 cmd/unknown → 不启用平台禁令(误判宁宽勿严,避免堵死 Git Bash 工作流)。
|
|
30
|
+
const psm = String(process.env.PSModulePath || "");
|
|
31
|
+
if (/Program Files[\\/]+PowerShell[\\/]+\d/i.test(psm) || /windowsapps[\\/]+microsoft\.powershell/i.test(psm)) return "powershell";
|
|
32
|
+
const cs = String(process.env.ComSpec || "");
|
|
33
|
+
if (/powershell/i.test(cs)) return "powershell";
|
|
34
|
+
return cs.toLowerCase().includes("cmd") ? "cmd" : "unknown";
|
|
35
|
+
}
|
|
36
|
+
return String(process.env.SHELL || "sh").split(/[\\/]/).pop() || "sh";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @param {string} [projDir]
|
|
41
|
+
* @param {{ shell?: string }} [opts] 显式执行体(适配层传入,母版不猜 harness)。
|
|
42
|
+
* 2026-10-18:DSH 适配层此前靠写 process.env.FG_SHELL 传递,结果宿主设的变量
|
|
43
|
+
* 被子进程(含 npm test)继承,直接污染了 4 条环境检测用例。改为参数传递。
|
|
44
|
+
*/
|
|
45
|
+
export function detectEnv(projDir, opts = {}) {
|
|
46
|
+
const osName = process.platform;
|
|
47
|
+
const forced = String((opts && opts.shell) || "").trim().toLowerCase();
|
|
48
|
+
const shellIdKey = forced || quickShellId();
|
|
49
|
+
let shellVersion = "";
|
|
50
|
+
try {
|
|
51
|
+
if (shellIdKey === "powershell")
|
|
52
|
+
shellVersion = execFileSync("powershell", ["-NoProfile", "-c", "$PSVersionTable.PSVersion.ToString()"], { encoding: "utf8", timeout: 6000 }).trim();
|
|
53
|
+
else if (shellIdKey === "zsh")
|
|
54
|
+
shellVersion = (execFileSync("zsh", ["--version"], { encoding: "utf8", timeout: 6000 }).match(/(\S+)\s*$/) || [])[1] || "";
|
|
55
|
+
else if (shellIdKey === "bash")
|
|
56
|
+
shellVersion = (execFileSync("bash", ["--version"], { encoding: "utf8", timeout: 6000 }).match(/version\s+(\S+)/) || [])[1] || "";
|
|
57
|
+
} catch {}
|
|
58
|
+
let encoding = "UTF-8";
|
|
59
|
+
if (osName === "win32") {
|
|
60
|
+
try {
|
|
61
|
+
const cp = execFileSync("cmd", ["/c", "chcp"], { encoding: "utf8", timeout: 6000 }).match(/(\d+)\s*$/);
|
|
62
|
+
encoding = cp ? (cp[1] === "65001" ? "UTF-8" : cp[1] === "936" ? "GBK" : "CP" + cp[1]) : "unknown";
|
|
63
|
+
} catch {}
|
|
64
|
+
}
|
|
65
|
+
// 大小写敏感:realpath 返回的盘上真实大小写与请求不同(仅大小写差异)→ 不敏感
|
|
66
|
+
let caseSensitive = osName !== "win32";
|
|
67
|
+
try {
|
|
68
|
+
const probeDir = projDir || process.cwd();
|
|
69
|
+
const real = realpathSync.native(probeDir);
|
|
70
|
+
if (real !== String(probeDir) && real.toLowerCase() === String(probeDir).toLowerCase()) caseSensitive = false;
|
|
71
|
+
} catch {}
|
|
72
|
+
const bsd = osName === "darwin"; // darwin 的 sed/grep/awk 为 BSD 版
|
|
73
|
+
const cmds = {};
|
|
74
|
+
const dirs = String(process.env.PATH || "").split(process.platform === "win32" ? ";" : ":").filter(Boolean);
|
|
75
|
+
for (const name of ["grep", "sed", "awk", "gsed", "greadlink"]) {
|
|
76
|
+
cmds[name] = dirs.some((d) => {
|
|
77
|
+
try {
|
|
78
|
+
return statSync(join(d, name + (osName === "win32" ? ".exe" : ""))).isFile();
|
|
79
|
+
} catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
os: osName,
|
|
86
|
+
shellIdKey,
|
|
87
|
+
shell: shellIdKey + (shellVersion ? " " + shellVersion : ""),
|
|
88
|
+
encoding,
|
|
89
|
+
pathSep: sep,
|
|
90
|
+
caseSensitive,
|
|
91
|
+
bsd,
|
|
92
|
+
cmds,
|
|
93
|
+
detectedAt: new Date().toISOString(),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function platformBashViolation(env, cmd) {
|
|
98
|
+
if (!env) return null;
|
|
99
|
+
const c = String(cmd);
|
|
100
|
+
if (env.os === "win32" && env.shellIdKey === "powershell") {
|
|
101
|
+
if (/&&/.test(c)) return "Windows PowerShell 会话禁 &&:用 ; 分隔或分开执行";
|
|
102
|
+
if (/\|\s*(head|grep|wc|sed|awk)\b/.test(c)) return "PowerShell 禁 bash 管道工具:用 Select-String / Measure-Object / Select-Object -First";
|
|
103
|
+
if (/(^|[;&|]\s*)(cat|type|Get-Content)\s/i.test(c) && !/\|\s*Select-/.test(c) && !/-(TotalCount|First|Tail)\b/.test(c))
|
|
104
|
+
return "禁裸 cat/type/Get-Content 刷屏:用 Get-Content -TotalCount N -Encoding UTF8";
|
|
105
|
+
}
|
|
106
|
+
if (env.os === "darwin") {
|
|
107
|
+
if (/\bsed\s+-i(?!\s*['"])/.test(c)) return "macOS BSD sed:-i 必须带后缀参数(sed -i '' …)";
|
|
108
|
+
if (/\bgrep\s+[^|;&]*?-P\b/.test(c)) return "macOS BSD grep 不支持 -P:用 -E";
|
|
109
|
+
if (/\breadlink\s+-f\b/.test(c) && !/\bgreadlink\b/.test(c)) return "macOS 无 readlink -f:用 greadlink -f";
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// FocusGuard 母版层 · 授权表(资格审核的独立状态源)
|
|
2
|
+
//
|
|
3
|
+
// 纪律(领导 2026-10-08 批):
|
|
4
|
+
// grants 是 FG 自己的状态,**不塞进 guard.mjs 的 state** —— 否则两套状态源会在第 3 层
|
|
5
|
+
// 校验时分叉。持久化目标是 FG 自己的状态文件(第五步实现)。
|
|
6
|
+
// 本步(第一步)只提供内存表与查询:ttl 只记录、不生效;计时与自动回收见第五步。
|
|
7
|
+
|
|
8
|
+
/** 规格 §五 认可的三档有效期。 */
|
|
9
|
+
export const TTL_KINDS = ["turn", "task", "persist"];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 建一张授权表。工厂而非单例:测试与多会话互不污染。
|
|
13
|
+
* @returns {{ has: (tool: string) => boolean, get: Function, grant: Function, revoke: Function, list: Function, clear: Function, readonly size: number }}
|
|
14
|
+
*/
|
|
15
|
+
export function createGrantTable() {
|
|
16
|
+
/** @type {Map<string, {tool: string, ttl: string, reason: string, layer: string, grantedAt: string}>} */
|
|
17
|
+
const table = new Map();
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
/** 该工具本会话是否已临时开放。 */
|
|
21
|
+
has(tool) {
|
|
22
|
+
return table.has(String(tool));
|
|
23
|
+
},
|
|
24
|
+
/** 取授权条目(未授权返回 undefined)。 */
|
|
25
|
+
get(tool) {
|
|
26
|
+
return table.get(String(tool));
|
|
27
|
+
},
|
|
28
|
+
/**
|
|
29
|
+
* 临时开放一个工具。ttl 非法即抛错——不静默归一,早暴露。
|
|
30
|
+
* @param {string} tool
|
|
31
|
+
* @param {{ttl?: string, reason?: string, layer?: string|number}} [opts]
|
|
32
|
+
*/
|
|
33
|
+
grant(tool, { ttl = "turn", reason = "", layer = "6" } = {}) {
|
|
34
|
+
if (!TTL_KINDS.includes(ttl)) {
|
|
35
|
+
throw new RangeError(`ttl 必须是 ${TTL_KINDS.join(" | ")} 之一,收到:${String(ttl)}`);
|
|
36
|
+
}
|
|
37
|
+
const entry = {
|
|
38
|
+
tool: String(tool),
|
|
39
|
+
ttl,
|
|
40
|
+
reason: String(reason || ""),
|
|
41
|
+
layer: String(layer),
|
|
42
|
+
grantedAt: new Date().toISOString(),
|
|
43
|
+
};
|
|
44
|
+
table.set(entry.tool, entry);
|
|
45
|
+
return entry;
|
|
46
|
+
},
|
|
47
|
+
/** 回收授权;返回是否确有一条被撤销。 */
|
|
48
|
+
revoke(tool) {
|
|
49
|
+
return table.delete(String(tool));
|
|
50
|
+
},
|
|
51
|
+
/** 全部授权条目(副本)。 */
|
|
52
|
+
list() {
|
|
53
|
+
return [...table.values()];
|
|
54
|
+
},
|
|
55
|
+
/** 清空(会话结束或测试用)。 */
|
|
56
|
+
clear() {
|
|
57
|
+
table.clear();
|
|
58
|
+
},
|
|
59
|
+
get size() {
|
|
60
|
+
return table.size;
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|