ppxans-harness 2.4.0 → 3.2.0
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 +201 -201
- package/README.md +218 -265
- package/bin/ppx-channels.js +2 -2
- package/bin/ppx-serve.js +5 -5
- package/bin/ppx-setup.js +124 -0
- package/bin/ppx-web.js +140 -0
- package/bin/ppx.js +2 -2
- package/config/identity.md +6 -6
- package/config/ishiki.md +16 -16
- package/config/ppx.json +15 -151
- package/config/ppx.json.example +143 -0
- package/package.json +17 -10
- package/skills/.usage.json +6 -0
- package/skills/agent-professional-training/SKILL.md +94 -0
- package/skills/brainstorm/SKILL.md +24 -0
- package/skills/cupid-lover-comms/SKILL.md +37 -0
- package/skills/debug/SKILL.md +26 -0
- package/skills/plan/SKILL.md +25 -0
- package/skills/ponytail/SKILL.md +25 -0
- package/skills/ppx-memory/SKILL.md +91 -0
- package/skills/ppx-memory/scripts/cli.js +192 -0
- package/skills/ppx-memory/scripts/experience.js +133 -0
- package/skills/ppx-memory/scripts/fact-store.js +842 -0
- package/skills/ppx-memory/scripts/l0.js +52 -0
- package/skills/ppx-memory/scripts/l2.js +146 -0
- package/skills/ppx-memory/scripts/l3.js +112 -0
- package/skills/ppx-memory/scripts/memory-ticker.js +238 -0
- package/skills/ppx-memory/scripts/pii.js +42 -0
- package/skills/ppx-memory/scripts/schema.js +80 -0
- package/skills/ppx-memory/scripts/session.js +398 -0
- package/skills/ppx-memory/scripts/similarity.js +43 -0
- package/skills/ppx-memory/scripts/store.js +116 -0
- package/skills/ppx-memory/scripts/wal.js +38 -0
- package/skills/ppx-selfheal/SKILL.md +24 -0
- package/skills/ppx-selfheal/scripts/cli.js +80 -0
- package/skills/ppx-selfheal/scripts/healer.js +184 -0
- package/skills/ppx-selfheal/scripts/logger.js +17 -0
- package/skills/ppx-selfheal/scripts/store.js +116 -0
- package/skills/prompt-depth-kit/SKILL.md +28 -0
- package/skills/session-naming/SKILL.md +36 -0
- package/skills/verify/SKILL.md +25 -0
- package/src/agent/index.js +1340 -717
- package/src/agent/prompts.js +47 -3
- package/src/aml-server.js +197 -151
- package/src/ans/eviction.js +123 -143
- package/src/ans/guard.js +159 -120
- package/src/ans/lifecycle.js +96 -93
- package/src/ans/proactive.js +112 -129
- package/src/ans/reward.js +95 -111
- package/src/ans/values.js +15 -15
- package/src/audit/audit-chain.js +43 -7
- package/src/audit/verifier.js +157 -120
- package/src/bus/circuit-breaker.js +9 -1
- package/src/bus/runtime-bus.js +107 -93
- package/src/channels/base.js +57 -34
- package/src/channels/feishu.js +118 -126
- package/src/channels/http.js +1090 -592
- package/src/channels/index.js +111 -110
- package/src/channels/log.js +29 -29
- package/src/channels/wechat-crypto.js +73 -74
- package/src/channels/wechat.js +191 -197
- package/src/channels/workspace.js +94 -0
- package/src/channels-cli.js +126 -124
- package/src/cli.js +129 -120
- package/src/commands/index.js +142 -0
- package/src/config/channels.js +137 -170
- package/src/config/index.js +277 -224
- package/src/config/placeholder.js +31 -0
- package/src/config/providers.js +148 -188
- package/src/config/settings.js +156 -182
- package/src/core/policy.js +69 -18
- package/src/core/trace.js +8 -6
- package/src/edit/editblock.js +266 -0
- package/src/edit/snapshot.js +67 -0
- package/src/evidence/index.js +153 -0
- package/src/evolve/playbook.js +9 -10
- package/src/hooks/index.js +113 -0
- package/src/llm/client.js +188 -446
- package/src/llm/dsml.js +74 -74
- package/src/llm/embedder.js +41 -35
- package/src/llm/fence.js +52 -105
- package/src/llm/index.js +4 -4
- package/src/llm/local-embedder.js +94 -0
- package/src/llm/presets.js +113 -0
- package/src/llm/pricing.js +93 -0
- package/src/llm/retry.js +73 -73
- package/src/llm/router.js +92 -97
- package/src/mcp/admin.js +326 -0
- package/src/mcp/client.js +487 -375
- package/src/mcp/http.js +203 -0
- package/src/mcp/index.js +116 -116
- package/src/mcp/server.js +392 -0
- package/src/mcp/tasks.js +133 -0
- package/src/memory/asset-hub.js +6 -11
- package/src/memory/canvas.js +2 -5
- package/src/memory/compaction.js +28 -28
- package/src/memory/experience.js +133 -122
- package/src/memory/fact-store.js +914 -698
- package/src/memory/failure-episode.js +20 -11
- package/src/memory/fork.js +17 -8
- package/src/memory/index.js +8 -6
- package/src/memory/l0.js +53 -52
- package/src/memory/l2.js +145 -130
- package/src/memory/l3.js +111 -111
- package/src/memory/legion-board.js +71 -0
- package/src/memory/memory-ticker.js +239 -240
- package/src/memory/session.js +398 -397
- package/src/memory/sqlite-store.js +581 -0
- package/src/mode/blackboard.js +49 -49
- package/src/mode/graph.js +42 -41
- package/src/mode/index.js +64 -64
- package/src/mode/legion.js +54 -51
- package/src/mode/plan-exec.js +50 -50
- package/src/mode/router.js +28 -40
- package/src/orchestrator/agent-worker.js +69 -69
- package/src/orchestrator/dag.js +90 -83
- package/src/orchestrator/experts.js +76 -0
- package/src/orchestrator/index.js +1 -1
- package/src/orchestrator/legion.js +179 -187
- package/src/orchestrator/supervisor.js +6 -8
- package/src/permissions/index.js +378 -0
- package/src/persona/index.js +28 -29
- package/src/plugin/builtin.js +313 -212
- package/src/plugin/context.js +80 -79
- package/src/plugin/index.js +62 -62
- package/src/plugin/v3.js +73 -0
- package/src/protocol/index.js +148 -0
- package/src/repomap/index.js +309 -0
- package/src/review/index.js +393 -0
- package/src/seam/registry.js +3 -0
- package/src/seam/shell.js +55 -55
- package/src/security/injection.js +79 -0
- package/src/selfheal/evolve.js +67 -67
- package/src/selfheal/healer.js +184 -167
- package/src/selfheal/run.js +9 -9
- package/src/server.js +63 -60
- package/src/services/diagnose.js +180 -0
- package/src/services/learning-service.js +9 -0
- package/src/services/memory-health.js +34 -6
- package/src/services/memory-service.js +42 -9
- package/src/services/triage.js +138 -0
- package/src/session/parts.js +76 -0
- package/src/session/projection.js +73 -0
- package/src/session/rollout.js +54 -0
- package/src/session/turn.js +137 -0
- package/src/skills/lint.js +72 -0
- package/src/skills/loader.js +231 -150
- package/src/skills/search.js +58 -0
- package/src/skills/verify.js +95 -100
- package/src/tools/advanced.js +388 -352
- package/src/tools/builtin.js +384 -297
- package/src/tools/catalog.js +283 -159
- package/src/tools/command-guard.js +112 -112
- package/src/tools/custom.js +47 -47
- package/src/tools/delegate.js +383 -297
- package/src/tools/document.js +254 -253
- package/src/tools/git.js +151 -0
- package/src/tools/governance.js +47 -20
- package/src/tools/index.js +16 -11
- package/src/tools/methods.js +178 -178
- package/src/tools/ocr.js +59 -59
- package/src/tools/sandbox-worker.js +40 -0
- package/src/tools/sandbox.js +92 -0
- package/src/tools/seam.js +162 -125
- package/src/tools/selfmod.js +196 -176
- package/src/tools/v3.js +225 -0
- package/src/tools/vad.js +176 -0
- package/src/tools/voice.js +238 -0
- package/src/utils/async.js +14 -0
- package/src/utils/config-file.js +53 -0
- package/src/utils/crashguard.js +88 -0
- package/src/utils/http.js +53 -0
- package/src/utils/id.js +8 -0
- package/src/utils/json-state.js +33 -0
- package/src/utils/logger.js +17 -17
- package/src/utils/ndjson.js +25 -0
- package/src/utils/pii.js +42 -42
- package/src/utils/rate-limit.js +50 -0
- package/src/utils/schema.js +80 -0
- package/src/utils/similarity.js +43 -0
- package/src/utils/store.js +170 -108
- package/src/utils/text.js +15 -15
- package/src/utils/trace.js +153 -153
- package/src/utils/wal.js +39 -0
- package/src/utils/winutf8.js +16 -15
- package/src/wiki/index.js +170 -0
package/src/ans/eviction.js
CHANGED
|
@@ -1,144 +1,124 @@
|
|
|
1
|
-
// src/ans/eviction.js - 排泄系 (⑤ 遗忘-归档) 自治治理 (ANS)
|
|
2
|
-
//
|
|
3
|
-
// 职责 (RC1 §1 ⑤): 记忆压缩、冷热层、历史会话归档、冗余识别、过期治理、遗忘与删除候选生成。
|
|
4
|
-
// 边界仅限跨会话长期记忆 (L1 FactStore + 会话日志); 功能性缓存/日志不归它管。
|
|
5
|
-
//
|
|
6
|
-
// 本期聚焦三大可落地的自治能力:
|
|
7
|
-
// 1. 冗余识别 - 利用 FactStore.findSimilar 检测高度重复记忆, 标记为"冗余候选"(合并/降权/删除候选)
|
|
8
|
-
// 2. 冷热分层 - 按 低访问天数×低衰减分 识别"冷记忆", 归档为只读, 不占热上下文
|
|
9
|
-
// 3. 归档扫描 - 产出结构化治理报告 (preview 不直接改), 挂 Scheduler 每日自动跑
|
|
10
|
-
//
|
|
11
|
-
// 删除策略 (务实): findSimilar 检测到的高相似对 → 保留高保真(importance/hits 更高)那条, 低的那条标"冗余候选"。
|
|
12
|
-
// 默认只"报告 + 标记", 硬删除交给 FactStore 既有 _prune (超 maxFacts 时)。本模块产出治理信号, 不越权改数据。
|
|
13
|
-
import
|
|
14
|
-
import
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
})
|
|
125
|
-
.map((f) => ({ id: f.id, content: String(f.content || "").slice(0, 120), lastAccess: f.lastAccess, importance: f.importance || 0, hits: f.hits || 0 }))
|
|
126
|
-
.slice(0, REPORT_LIMIT);
|
|
127
|
-
|
|
128
|
-
const run = {
|
|
129
|
-
total: all.length,
|
|
130
|
-
redundant: redundant.slice(0, REPORT_LIMIT),
|
|
131
|
-
redundantCount: redundant.length,
|
|
132
|
-
coldCount: cold.length,
|
|
133
|
-
cold: cold.slice(0, REPORT_LIMIT),
|
|
134
|
-
lastRunAt: Date.now(),
|
|
135
|
-
};
|
|
136
|
-
_recordRun(agent, run);
|
|
137
|
-
info(`[eviction] 扫描 ${all.length} 条: 冗余 ${redundant.length}, 冷记忆 ${cold.length}`);
|
|
138
|
-
return run;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// 人类可读状态 (可观测)
|
|
142
|
-
export function status(agent) {
|
|
143
|
-
return loadState(agent);
|
|
1
|
+
// src/ans/eviction.js - 排泄系 (⑤ 遗忘-归档) 自治治理 (ANS)
|
|
2
|
+
//
|
|
3
|
+
// 职责 (RC1 §1 ⑤): 记忆压缩、冷热层、历史会话归档、冗余识别、过期治理、遗忘与删除候选生成。
|
|
4
|
+
// 边界仅限跨会话长期记忆 (L1 FactStore + 会话日志); 功能性缓存/日志不归它管。
|
|
5
|
+
//
|
|
6
|
+
// 本期聚焦三大可落地的自治能力:
|
|
7
|
+
// 1. 冗余识别 - 利用 FactStore.findSimilar 检测高度重复记忆, 标记为"冗余候选"(合并/降权/删除候选)
|
|
8
|
+
// 2. 冷热分层 - 按 低访问天数×低衰减分 识别"冷记忆", 归档为只读, 不占热上下文
|
|
9
|
+
// 3. 归档扫描 - 产出结构化治理报告 (preview 不直接改), 挂 Scheduler 每日自动跑
|
|
10
|
+
//
|
|
11
|
+
// 删除策略 (务实): findSimilar 检测到的高相似对 → 保留高保真(importance/hits 更高)那条, 低的那条标"冗余候选"。
|
|
12
|
+
// 默认只"报告 + 标记", 硬删除交给 FactStore 既有 _prune (超 maxFacts 时)。本模块产出治理信号, 不越权改数据。
|
|
13
|
+
import { loadAgentState, saveAgentState } from "../utils/json-state.js";
|
|
14
|
+
import { charBigrams, setOverlap } from "../utils/similarity.js";
|
|
15
|
+
import { info, debug } from "../utils/logger.js";
|
|
16
|
+
|
|
17
|
+
// 冗余判定阈值 (bigram overlap 相似度, 0~1; 中文同义变体通常 >0.6)
|
|
18
|
+
const SIM_THRESHOLD = 0.6;
|
|
19
|
+
// bigram overlap 收敛到 utils/similarity (原先此处自带一份 _bigramSet/_overlap 实现)
|
|
20
|
+
// 冷记忆判定: 连续 N 天未被访问 + 分数低于分位 → 冷
|
|
21
|
+
const COLD_DAYS = 14;
|
|
22
|
+
// 治理报告保留条数
|
|
23
|
+
const REPORT_LIMIT = 50;
|
|
24
|
+
|
|
25
|
+
// 治理状态读写 (实现收敛到 utils/json-state, 对外导出签名不变)
|
|
26
|
+
export function loadState(agent) {
|
|
27
|
+
return loadAgentState(agent, "eviction");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function saveState(agent, state) {
|
|
31
|
+
return saveAgentState(agent, "eviction", state);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 幂等记录一次扫描结果 (去重: 同 id 已存在则更新, 不无限追加)
|
|
35
|
+
function _recordRun(agent, run) {
|
|
36
|
+
const st = loadState(agent);
|
|
37
|
+
st.lastRun = run;
|
|
38
|
+
saveState(agent, st);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 扫描一次 L1 记忆: 返回 { redundant, cold, total, scanned } 治理报告 (只读, 不改数据)
|
|
42
|
+
export function scan(agent) {
|
|
43
|
+
const facts = agent.facts;
|
|
44
|
+
if (!facts || typeof facts.list !== "function" || !facts.findSimilar) {
|
|
45
|
+
return { total: 0, redundant: [], cold: [], lastRunAt: Date.now() };
|
|
46
|
+
}
|
|
47
|
+
let all = [];
|
|
48
|
+
try { all = facts.list() || []; } catch { return { total: 0, redundant: [], cold: [], error: "list-failed" }; }
|
|
49
|
+
if (!all.length) return { total: 0, redundant: [], cold: [], lastRunAt: Date.now() };
|
|
50
|
+
|
|
51
|
+
const now = Date.now();
|
|
52
|
+
const DAY = 86400000;
|
|
53
|
+
|
|
54
|
+
// 1) 冗余识别: 两两 findSimilar 检测高相似对, 标低保真那条为冗余候选
|
|
55
|
+
// 2026-09-18 性能修复: 原实现每对都调 _overlap 重算两侧 bigram Set,
|
|
56
|
+
// maxFacts=1000 时最多 100 万次 Set 重建, 全程同步阻塞事件循环。
|
|
57
|
+
// 现预建每条事实的 bigram Set (一次 O(n)), 两两比较只做交集计数。
|
|
58
|
+
const redundant = [];
|
|
59
|
+
const visited = new Set();
|
|
60
|
+
const bigrams = new Map();
|
|
61
|
+
for (const f of all) {
|
|
62
|
+
try { bigrams.set(f.id, charBigrams(f.content)); } catch { bigrams.set(f.id, new Set()); }
|
|
63
|
+
}
|
|
64
|
+
// 两两比较 (自实现 overlap): findSimilar 对自身返回1.0无法用, 逐对算 bigram 相似度
|
|
65
|
+
for (let i = 0; i < all.length; i++) {
|
|
66
|
+
const f = all[i];
|
|
67
|
+
if (visited.has(f.id)) continue;
|
|
68
|
+
let best = null, bestSim = 0;
|
|
69
|
+
const fa = bigrams.get(f.id);
|
|
70
|
+
for (let j = 0; j < all.length; j++) {
|
|
71
|
+
const g = all[j];
|
|
72
|
+
if (g.id === f.id || visited.has(g.id)) continue;
|
|
73
|
+
try {
|
|
74
|
+
const sim = setOverlap(fa, bigrams.get(g.id));
|
|
75
|
+
if (sim > bestSim) { bestSim = sim; best = g; }
|
|
76
|
+
} catch (e) { debug(`[ans/eviction] 已忽略异常: ${e && e.message ? e.message : e}`); }
|
|
77
|
+
}
|
|
78
|
+
if (best && bestSim >= SIM_THRESHOLD) {
|
|
79
|
+
const aKeep = (f.importance || 0) + (f.hits || 0);
|
|
80
|
+
const bKeep = (best.importance || 0) + (best.hits || 0);
|
|
81
|
+
const loser = aKeep >= bKeep ? best : f;
|
|
82
|
+
const winner = aKeep >= bKeep ? f : best;
|
|
83
|
+
redundant.push({
|
|
84
|
+
id: loser.id,
|
|
85
|
+
content: String(loser.content || "").slice(0, 120),
|
|
86
|
+
keepId: winner.id,
|
|
87
|
+
keep: String(winner.content || "").slice(0, 120),
|
|
88
|
+
importance: loser.importance || 0,
|
|
89
|
+
hits: loser.hits || 0,
|
|
90
|
+
sim: Math.round(bestSim * 100) / 100,
|
|
91
|
+
});
|
|
92
|
+
visited.add(loser.id);
|
|
93
|
+
visited.add(f.id);
|
|
94
|
+
} else {
|
|
95
|
+
visited.add(f.id);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 2) 冷热分层: N 天未访问 → 冷 (归档候选, 不参与热上下文)
|
|
100
|
+
const cold = all
|
|
101
|
+
.filter((f) => {
|
|
102
|
+
const last = f.lastAccess ? new Date(f.lastAccess).getTime() : (f.created ? new Date(f.created).getTime() : now);
|
|
103
|
+
return (now - last) > COLD_DAYS * DAY;
|
|
104
|
+
})
|
|
105
|
+
.map((f) => ({ id: f.id, content: String(f.content || "").slice(0, 120), lastAccess: f.lastAccess, importance: f.importance || 0, hits: f.hits || 0 }))
|
|
106
|
+
.slice(0, REPORT_LIMIT);
|
|
107
|
+
|
|
108
|
+
const run = {
|
|
109
|
+
total: all.length,
|
|
110
|
+
redundant: redundant.slice(0, REPORT_LIMIT),
|
|
111
|
+
redundantCount: redundant.length,
|
|
112
|
+
coldCount: cold.length,
|
|
113
|
+
cold: cold.slice(0, REPORT_LIMIT),
|
|
114
|
+
lastRunAt: Date.now(),
|
|
115
|
+
};
|
|
116
|
+
_recordRun(agent, run);
|
|
117
|
+
info(`[eviction] 扫描 ${all.length} 条: 冗余 ${redundant.length}, 冷记忆 ${cold.length}`);
|
|
118
|
+
return run;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 人类可读状态 (可观测)
|
|
122
|
+
export function status(agent) {
|
|
123
|
+
return loadState(agent);
|
|
144
124
|
}
|
package/src/ans/guard.js
CHANGED
|
@@ -1,120 +1,159 @@
|
|
|
1
|
-
// src/ans/guard.js - 免疫系 (⑧安全治理) 全局闸门 (ANS)
|
|
2
|
-
// 在 P0 总线 intercept() 之上搭全局免疫: 任何经总线命令通道的动作执行前先过闸门。
|
|
3
|
-
// 能力 (务实, 不硬造):
|
|
4
|
-
// 1. 全局审计 - 所有放行/拒绝的命令记入 Auditor 账本 (可离线核实)
|
|
5
|
-
// 2. 危险 verb 阻断 - 白名单制: 未显式放行的"危险操作"(删除/清空/覆盖写) 默认拒绝
|
|
6
|
-
// 3. 单次审批授权 - 对越出白名单的关键操作可单次放行 (双模式授权)
|
|
7
|
-
// 说明: 总线 command 通道 (P0 建) + 免疫闸门 (本模块) 形成"执行前统一安全校验"。
|
|
8
|
-
// 现有工具走 catalog/seam (已有局部 runWithPolicy 权限);本闸门覆盖"走总线命令"的敏感动作 (记忆写/删除等未来接线)。
|
|
9
|
-
// P0 (2026-09-15): 新增 installGuardOnCatalog —— 把同一闸门状态挂到 ToolCatalog 策略链,
|
|
10
|
-
// 解决 MERGE-REPORT 遗留 P2 (guard 空转: 工具走 catalog 不走总线)。危险判定作用于工具名。
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
1
|
+
// src/ans/guard.js - 免疫系 (⑧安全治理) 全局闸门 (ANS)
|
|
2
|
+
// 在 P0 总线 intercept() 之上搭全局免疫: 任何经总线命令通道的动作执行前先过闸门。
|
|
3
|
+
// 能力 (务实, 不硬造):
|
|
4
|
+
// 1. 全局审计 - 所有放行/拒绝的命令记入 Auditor 账本 (可离线核实)
|
|
5
|
+
// 2. 危险 verb 阻断 - 白名单制: 未显式放行的"危险操作"(删除/清空/覆盖写) 默认拒绝
|
|
6
|
+
// 3. 单次审批授权 - 对越出白名单的关键操作可单次放行 (双模式授权)
|
|
7
|
+
// 说明: 总线 command 通道 (P0 建) + 免疫闸门 (本模块) 形成"执行前统一安全校验"。
|
|
8
|
+
// 现有工具走 catalog/seam (已有局部 runWithPolicy 权限);本闸门覆盖"走总线命令"的敏感动作 (记忆写/删除等未来接线)。
|
|
9
|
+
// P0 (2026-09-15): 新增 installGuardOnCatalog —— 把同一闸门状态挂到 ToolCatalog 策略链,
|
|
10
|
+
// 解决 MERGE-REPORT 遗留 P2 (guard 空转: 工具走 catalog 不走总线)。危险判定作用于工具名。
|
|
11
|
+
// P0 修复 (2026-09-16): 原 DANGEROUS_RE 前缀匹配对真实工具名 (memory_import/clear_layer 等) 全部落空,
|
|
12
|
+
// blocked 恒为 0 = "假绿灯"。新增: 显式危险工具集 + 参数级判定 (mode/hard/quarantine),
|
|
13
|
+
// 并统一判定函数供总线/catalog 两路径复用。审计写失败不再静默 (warn + writeFailures)。
|
|
14
|
+
import { hasPII } from "../utils/pii.js";
|
|
15
|
+
import { warn, debug } from "../utils/logger.js";
|
|
16
|
+
|
|
17
|
+
// 危险 verb 前缀: 命中且未显式白名单放行 → 需单次审批或拒绝
|
|
18
|
+
const DANGEROUS_RE = /^(delete|remove|clear|wipe|drop|purge|truncate|overwrite)/i;
|
|
19
|
+
|
|
20
|
+
// 显式危险工具集 (名称级): 工具名不匹配前缀正则, 但本身是不可逆/大影响操作。
|
|
21
|
+
// 语义: 未白名单放行即拒绝 (fail-closed), 与 DANGEROUS_RE 前缀判定同权。
|
|
22
|
+
const DANGEROUS_TOOLS = new Set([
|
|
23
|
+
"memory_export", // 导出全量记忆 (含软删/归档, 敏感数据外流), 需审批
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
// 参数级危险判定: 同一工具不同参数风险不同 (可逆 vs 不可逆)。
|
|
27
|
+
// 返回 true = 本次调用属高危, 未白名单放行即拒绝。
|
|
28
|
+
function argsDangerous(name, args) {
|
|
29
|
+
if (!args || typeof args !== "object") return false;
|
|
30
|
+
if (name === "memory_import") return args.mode === "replace"; // 整体替换全部记忆, 不可逆
|
|
31
|
+
if (name === "memory_clear_layer") return args.hard === true; // 物理删除, 不可回滚
|
|
32
|
+
if (name === "audit_verify") return args.quarantine === true; // 隔离并重建审计链 (跳过损坏段)
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 统一危险判定: 返回 { dangerous, reason }
|
|
37
|
+
// 覆盖: 1) 前缀正则 (总线命令/未来工具) 2) 显式危险工具集 3) 参数级判定
|
|
38
|
+
// 供 installGuard (总线) 与 installGuardOnCatalog (工具) 两路径复用, 保证口径一致。
|
|
39
|
+
export function dangerVerdict(verb, args = null) {
|
|
40
|
+
if (DANGEROUS_RE.test(verb)) return { dangerous: true, reason: "dangerous-prefix" };
|
|
41
|
+
if (DANGEROUS_TOOLS.has(verb)) return { dangerous: true, reason: "dangerous-tool" };
|
|
42
|
+
if (argsDangerous(verb, args)) return { dangerous: true, reason: "dangerous-args" };
|
|
43
|
+
return { dangerous: false, reason: null };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 危险判定: verb 命中危险且未白名单放行 (向后兼容, 供外部调用)
|
|
47
|
+
export function isDangerous(verb, allowList = []) {
|
|
48
|
+
return dangerVerdict(verb).dangerous && !allowList.includes(verb);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 默认记审计账本的最大条目
|
|
52
|
+
const AUDIT_LIMIT = 200;
|
|
53
|
+
|
|
54
|
+
// 安装免疫闸门: 挂到 agent.bus.intercept(), 返回可观测状态
|
|
55
|
+
// opts.intercept: 是否实际拦截 (true=默认审计+危险阻断; false=仅审计不阻断, 用于灰度)
|
|
56
|
+
export function installGuard(agent, opts = {}) {
|
|
57
|
+
if (!agent.bus || typeof agent.bus.intercept !== "function") {
|
|
58
|
+
return { enabled: false, reason: "no-bus" };
|
|
59
|
+
}
|
|
60
|
+
const { intercept = true, allowList = [], ledgerLimit = AUDIT_LIMIT } = opts;
|
|
61
|
+
const state = {
|
|
62
|
+
enabled: true,
|
|
63
|
+
intercept,
|
|
64
|
+
allowList: [...allowList],
|
|
65
|
+
checks: 0,
|
|
66
|
+
blocked: 0,
|
|
67
|
+
allowed: 0,
|
|
68
|
+
wouldBlock: 0, // 观测到危险但被白名单放行/灰度放行的次数 (闸门"在工作"的可见性)
|
|
69
|
+
writeFailures: 0, // 审计账本写失败次数 (不静默吞)
|
|
70
|
+
audited: [],
|
|
71
|
+
lastVerdict: null,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const record = (verdict, cmd, note) => {
|
|
75
|
+
state.checks++;
|
|
76
|
+
if (verdict === "block") state.blocked++;
|
|
77
|
+
else if (verdict === "allow" || verdict === "approve") state.allowed++;
|
|
78
|
+
state.audited.push({ verdict, verb: cmd.verb, id: cmd.id, ts: Date.now(), note, payloadHasPII: hasPII(JSON.stringify(cmd.payload || {})) });
|
|
79
|
+
if (state.audited.length > ledgerLimit) state.audited = state.audited.slice(-ledgerLimit);
|
|
80
|
+
// 写 Auditor 账本 (若装了); 写失败不静默吞, 计数 + warn (审计承诺不能被悄悄破坏)
|
|
81
|
+
try {
|
|
82
|
+
if (agent.auditor && typeof agent.auditor.record === "function") {
|
|
83
|
+
agent.auditor.record("guard", { verdict, verb: cmd.verb, note });
|
|
84
|
+
}
|
|
85
|
+
} catch (e) {
|
|
86
|
+
state.writeFailures++;
|
|
87
|
+
warn(`[guard] 审计账本写入失败 (${cmd.verb}): ${e?.message || e}`);
|
|
88
|
+
}
|
|
89
|
+
state.lastVerdict = { verdict, verb: cmd.verb, note };
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// 卸载钩子
|
|
93
|
+
const off = agent.bus.intercept(async (cmd, next) => {
|
|
94
|
+
const verb = String(cmd.verb || "");
|
|
95
|
+
const { dangerous, reason } = dangerVerdict(verb, cmd.payload);
|
|
96
|
+
const allowed = state.allowList.includes(verb);
|
|
97
|
+
if (!dangerous || allowed) {
|
|
98
|
+
if (dangerous && allowed) state.wouldBlock++; // 白名单放行的高危项仍计数, 让"闸门在观察"可见
|
|
99
|
+
record("allow", cmd, dangerous ? "dangerous-whitelisted" : "normal");
|
|
100
|
+
} else if (intercept) {
|
|
101
|
+
record("block", cmd, reason || "dangerous-not-whitelisted");
|
|
102
|
+
throw new Error("免疫闸门: 危险命令未授信而阻断: " + verb);
|
|
103
|
+
} else {
|
|
104
|
+
state.wouldBlock++;
|
|
105
|
+
record("allow", cmd, "dangerous-observed-not-blocked");
|
|
106
|
+
}
|
|
107
|
+
await next();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// 单次审批授权: 放行一次危险 verb (一次用完自动失效)
|
|
111
|
+
const approveOnce = (verb) => { state.allowList.push(verb); return () => removeAllow(verb); };
|
|
112
|
+
const removeAllow = (verb) => { const i = state.allowList.indexOf(verb); if (i >= 0) state.allowList.splice(i, 1); };
|
|
113
|
+
const status = () => ({ enabled: state.enabled, intercept: state.intercept, checks: state.checks, blocked: state.blocked, allowed: state.allowed, wouldBlock: state.wouldBlock, writeFailures: state.writeFailures, allowList: [...state.allowList], recent: state.audited.slice(-10) });
|
|
114
|
+
|
|
115
|
+
return { off, approveOnce, status, _state: state };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ---- P0: 免疫闸门接入工具执行收口 (ToolCatalog 策略链) ----
|
|
119
|
+
// 与 installGuard (总线版) 共享同一 state: allowList / 计数 / 审计记录全部复用,
|
|
120
|
+
// 所以 agent.approveGuard("delete/x") 一次授权同时作用于总线命令和工具调用。
|
|
121
|
+
// catalog.addPolicySubscriber 已保证: 策略订阅者异常不拖垮工具 (fail-open + 日志)。
|
|
122
|
+
export function installGuardOnCatalog(catalog, guardHandle) {
|
|
123
|
+
if (!catalog || typeof catalog.addPolicySubscriber !== "function") {
|
|
124
|
+
return { enabled: false, reason: "no-catalog" };
|
|
125
|
+
}
|
|
126
|
+
const state = guardHandle?._state;
|
|
127
|
+
if (!state) return { enabled: false, reason: "no-guard-state" };
|
|
128
|
+
const off = catalog.addPolicySubscriber(async (name, args, ctx) => {
|
|
129
|
+
const verb = String(name || "");
|
|
130
|
+
const { dangerous, reason } = dangerVerdict(verb, args);
|
|
131
|
+
const allowed = state.allowList.includes(verb);
|
|
132
|
+
if (!dangerous || allowed) {
|
|
133
|
+
if (dangerous && allowed) state.wouldBlock++;
|
|
134
|
+
state.checks++;
|
|
135
|
+
state.allowed++;
|
|
136
|
+
state.audited.push({ verdict: "allow", verb, ts: Date.now(), note: dangerous ? "dangerous-whitelisted" : "normal", payloadHasPII: hasPII(JSON.stringify(args || {})) });
|
|
137
|
+
if (state.audited.length > AUDIT_LIMIT) state.audited = state.audited.slice(-AUDIT_LIMIT);
|
|
138
|
+
return null; // 弃权 = allow (放行)
|
|
139
|
+
}
|
|
140
|
+
state.checks++;
|
|
141
|
+
state.blocked++;
|
|
142
|
+
state.audited.push({ verdict: "block", verb, ts: Date.now(), note: reason || "dangerous-not-whitelisted", payloadHasPII: hasPII(JSON.stringify(args || {})) });
|
|
143
|
+
if (state.audited.length > AUDIT_LIMIT) state.audited = state.audited.slice(-AUDIT_LIMIT);
|
|
144
|
+
if (typeof guardHandle.status === "function") {
|
|
145
|
+
try {
|
|
146
|
+
// 同步状态到 guard 的 lastVerdict (可观测)
|
|
147
|
+
const st = guardHandle._state;
|
|
148
|
+
st.lastVerdict = { verdict: "block", verb, note: reason || "dangerous-not-whitelisted" };
|
|
149
|
+
} catch (e) { debug(`[ans/guard] 已忽略异常: ${e && e.message ? e.message : e}`); }
|
|
150
|
+
}
|
|
151
|
+
return { decision: "deny", reason: "免疫闸门: 危险工具未授信: " + verb, priority: 100 };
|
|
152
|
+
}, { name: "immune-guard", priority: 100 });
|
|
153
|
+
return { enabled: true, off };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 可观测摘要 (读已安装实例)
|
|
157
|
+
export function guardStatus(agent) {
|
|
158
|
+
return agent.__guard ? agent.__guard.status() : { enabled: false };
|
|
159
|
+
}
|