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/tools/catalog.js
CHANGED
|
@@ -1,159 +1,283 @@
|
|
|
1
|
-
// src/tools/catalog.js - 工具注册表 (参考 openhanako tool-catalog + deepseek Capability Seam)
|
|
2
|
-
// 升级: 能力缝三分法(Definition元数据/Provider实现/Consumer策略) + 热挂载(enable/disable/unregister) + 元数据枚举
|
|
3
|
-
// P0 (2026-09-15): 策略订阅者链 + deny-wins 合并 (吸收 Aegis/HookBus 治理语义) ——
|
|
4
|
-
// 安全策略 (命令守卫/免疫闸门/防注入) 挂到工具执行唯一收口, 成为架构不变量而非可选行为
|
|
5
|
-
import { info } from "../utils/logger.js";
|
|
6
|
-
import { normalizeMeta, runWithPolicy, toDescriptor, TOOL_ERROR_PREFIX } from "./seam.js";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
if (!
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
1
|
+
// src/tools/catalog.js - 工具注册表 (参考 openhanako tool-catalog + deepseek Capability Seam)
|
|
2
|
+
// 升级: 能力缝三分法(Definition元数据/Provider实现/Consumer策略) + 热挂载(enable/disable/unregister) + 元数据枚举
|
|
3
|
+
// P0 (2026-09-15): 策略订阅者链 + deny-wins 合并 (吸收 Aegis/HookBus 治理语义) ——
|
|
4
|
+
// 安全策略 (命令守卫/免疫闸门/防注入) 挂到工具执行唯一收口, 成为架构不变量而非可选行为
|
|
5
|
+
import { info } from "../utils/logger.js";
|
|
6
|
+
import { normalizeMeta, runWithPolicy, toDescriptor, TOOL_ERROR_PREFIX } from "./seam.js";
|
|
7
|
+
// 熔断器 (src/bus/): 保护策略链不被故障订阅者反复拖累 —— 这正是该模块注释声明的设计意图。
|
|
8
|
+
// 接线前它是"完整实现但零消费者"的预留件 (2026-09-17 接入)。
|
|
9
|
+
import { CircuitBreaker } from "../bus/circuit-breaker.js";
|
|
10
|
+
|
|
11
|
+
export { TOOL_ERROR_PREFIX };
|
|
12
|
+
|
|
13
|
+
// 策略订阅者默认熔断参数: 60s 窗口内 3 次异常 → 熔断, 冷却 10s 后放行单个探测
|
|
14
|
+
const DEFAULT_SUBSCRIBER_BREAKER = { threshold: 3, windowMs: 60000, cooldownMs: 10000 };
|
|
15
|
+
|
|
16
|
+
// ---- Deny-Wins 决策合并 (HookBus consolidate 语义) ----
|
|
17
|
+
// 多个策略订阅者对同一工具调用给出冲突决策时:
|
|
18
|
+
// 任一 deny 一票否决 (取最高优先级 reason); 否则任一 ask → ask; 否则 allow
|
|
19
|
+
// 保证安全策略不能被低优先级 allow 投票覆盖 (纵深防御: 治理/合规/预算可叠加互不干扰)
|
|
20
|
+
export function consolidateDecisions(decisions) {
|
|
21
|
+
const denies = decisions.filter((d) => d && d.decision === "deny");
|
|
22
|
+
if (denies.length > 0) {
|
|
23
|
+
const top = denies.reduce((a, b) => ((a.priority || 0) >= (b.priority || 0) ? a : b));
|
|
24
|
+
return { decision: "deny", reason: top.reason || "策略拒绝", priority: top.priority || 0 };
|
|
25
|
+
}
|
|
26
|
+
const asks = decisions.filter((d) => d && d.decision === "ask");
|
|
27
|
+
if (asks.length > 0) {
|
|
28
|
+
const top = asks.reduce((a, b) => ((a.priority || 0) >= (b.priority || 0) ? a : b));
|
|
29
|
+
return { decision: "ask", reason: top.reason || "需要审批", priority: top.priority || 0 };
|
|
30
|
+
}
|
|
31
|
+
return { decision: "allow", reason: null, priority: 0 };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ---- 参数校验 (2026-10-03, "想记做学评"框架第 3 条: 参数要校验) ----
|
|
35
|
+
// 工具声明了 JSON Schema 但此前运行时零校验 — 参数错误浪费一整轮 LLM 交互。
|
|
36
|
+
// 轻量子集: required / type / enum (顶层), 未知键放行 (LLM 常带冗余键, 不因苛刻而误杀)。
|
|
37
|
+
// 返回 null = 通过; 字符串 = 可行动错误信息。
|
|
38
|
+
export function validateArgs(meta, args) {
|
|
39
|
+
const schema = meta?.parameters;
|
|
40
|
+
if (!schema || schema.type !== "object") return null;
|
|
41
|
+
const a = (args && typeof args === "object" && !Array.isArray(args)) ? args : {};
|
|
42
|
+
const problems = [];
|
|
43
|
+
for (const key of schema.required || []) {
|
|
44
|
+
const v = a[key];
|
|
45
|
+
if (v === undefined || v === null || (typeof v === "string" && !v.trim())) {
|
|
46
|
+
problems.push(`缺少必填参数 "${key}"`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
for (const [key, ps] of Object.entries(schema.properties || {})) {
|
|
50
|
+
const v = a[key];
|
|
51
|
+
if (v === undefined) continue;
|
|
52
|
+
const t = Array.isArray(v) ? "array" : typeof v;
|
|
53
|
+
if (ps.type && t !== ps.type) {
|
|
54
|
+
// 数字宽容: "42" 这类字符串数字自动转换语义提示, 不直接判死
|
|
55
|
+
if (ps.type === "number" && t === "string" && v.trim() !== "" && !isNaN(Number(v))) {
|
|
56
|
+
a[key] = Number(v);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
problems.push(`参数 "${key}" 应为 ${ps.type}, 实际 ${t}`);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (ps.enum && !ps.enum.includes(v)) {
|
|
63
|
+
problems.push(`参数 "${key}" 应为: ${ps.enum.join(" / ")}, 实际 "${v}"`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return problems.length ? problems.join("; ") : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export class ToolCatalog {
|
|
70
|
+
constructor() {
|
|
71
|
+
this.tools = new Map(); // name -> meta (Definition + Provider)
|
|
72
|
+
this.policySubscribers = []; // 策略订阅者: { fn(name,args,ctx)->Decision|null, priority, name }
|
|
73
|
+
// 工具披露策略 (2026-10-03, 上下文工程): 与 enabled **正交** —— 控制"给 LLM 看哪些",
|
|
74
|
+
// 不影响"能调用哪些"。动机: 59 个工具的 JSON schema 实测约占 6725 tok/请求,
|
|
75
|
+
// 占空会话固定开销的 82%, 而单个任务通常只用 3–5 个工具。
|
|
76
|
+
// 未披露的工具仍可被 catalog.call 调用 (内部链路与既有测试完全不受影响),
|
|
77
|
+
// 只是不出现在 toOpenAI() 的 tools 参数里; agent 可通过 enable_capability 动态披露。
|
|
78
|
+
this.exposeSet = null; // null = 全部披露 (向后兼容默认)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ---- Definition + Provider 注册 ----
|
|
82
|
+
register(def) {
|
|
83
|
+
if (!def || typeof def.execute !== "function") {
|
|
84
|
+
throw new Error(`工具注册失败: 需 name + execute (got ${def && def.name})`);
|
|
85
|
+
}
|
|
86
|
+
const meta = normalizeMeta(def); // 关键: execute 缺失由 normalizeMeta 的 name 校验兜底
|
|
87
|
+
this.tools.set(meta.name, meta);
|
|
88
|
+
info(`能力已注册: ${meta.name} [${meta.category}/${meta.power}]`);
|
|
89
|
+
return this;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ---- 热挂载: 卸载 ----
|
|
93
|
+
unregister(name) {
|
|
94
|
+
const had = this.tools.delete(name);
|
|
95
|
+
if (had) info(`能力已卸载: ${name}`);
|
|
96
|
+
return had;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---- 能力声明查询 (ZCode PermissionToolCapability 语义) ----
|
|
100
|
+
// 未声明时按 category/power 推断保守默认: system 域一律视为高风险
|
|
101
|
+
getCapability(name) {
|
|
102
|
+
const t = this.tools.get(name);
|
|
103
|
+
if (!t) return null;
|
|
104
|
+
if (t.capability) return t.capability;
|
|
105
|
+
if (t.category === "system") return { riskLevel: "high", destructive: true, sideEffect: "system" };
|
|
106
|
+
if (t.category === "net") return { riskLevel: "high", sideEffect: "network" };
|
|
107
|
+
return { riskLevel: "low", readOnly: true, sideEffect: "none" };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ---- 热挂载: 启用/禁用 ----
|
|
111
|
+
enable(name) {
|
|
112
|
+
const t = this.tools.get(name);
|
|
113
|
+
if (!t) return false;
|
|
114
|
+
t.enabled = true;
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
disable(name) {
|
|
119
|
+
const t = this.tools.get(name);
|
|
120
|
+
if (!t) return false;
|
|
121
|
+
t.enabled = false;
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---- OpenAI 兼容的 tools 格式 (给 LLM 用, 只含 启用且已披露 的项) ----
|
|
126
|
+
toOpenAI() {
|
|
127
|
+
return [...this.tools.values()]
|
|
128
|
+
.filter((t) => t.enabled && this.isExposed(t.name))
|
|
129
|
+
.map((t) => ({
|
|
130
|
+
type: "function",
|
|
131
|
+
function: { name: t.name, description: t.description, parameters: t.parameters },
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ---- 披露策略 (与 enabled 正交) ----
|
|
136
|
+
// setExposure(["read_file", ...]) → 只把列出的工具给 LLM; setExposure(null) → 恢复全量
|
|
137
|
+
setExposure(names) {
|
|
138
|
+
if (names === null || names === undefined) {
|
|
139
|
+
this.exposeSet = null;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
this.exposeSet = new Set(names);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
expose(name) {
|
|
146
|
+
if (this.exposeSet) this.exposeSet.add(name);
|
|
147
|
+
return this;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
isExposed(name) {
|
|
151
|
+
return this.exposeSet === null || this.exposeSet.has(name);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 已注册但未披露给 LLM 的工具名 (供 _context 生成"按需启用"提示)。只算 enabled 的 ——
|
|
155
|
+
// 被 tools.disabled 显式关掉的工具既不可调用也不该提示。
|
|
156
|
+
hiddenFromLLM() {
|
|
157
|
+
if (!this.exposeSet) return [];
|
|
158
|
+
return [...this.tools.values()]
|
|
159
|
+
.filter((t) => t.enabled && !this.exposeSet.has(t.name))
|
|
160
|
+
.map((t) => t.name);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ---- 审计: 可选注入审计哈希链 (未注入时零开销, 保持向后兼容) ----
|
|
164
|
+
// 吸收自 ppx-v2: 每次工具调用落一条 append-only + SHA-256 链式记录, 防审计日志被悄悄改写
|
|
165
|
+
setAudit(auditLog) {
|
|
166
|
+
this.audit = auditLog || null;
|
|
167
|
+
return this;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ---- 策略订阅者 (P0): 工具执行唯一收口上的安全策略链 ----
|
|
171
|
+
// fn(name, args, ctx) -> Promise<{decision:'allow'|'deny'|'ask', reason?, priority?}> | null (null/undefined = 弃权)
|
|
172
|
+
// priority: 高者优先 (合并冲突决策时取高优先级 reason); 默认 0
|
|
173
|
+
// 订阅者异常不拖垮工具执行: 记日志并视同弃权 (fail-open), 且由 per-subscriber 熔断器兜底 ——
|
|
174
|
+
// 连续异常达阈值后进入熔断期, 期间该订阅者直接跳过错开 (不再反复调用 + 不再刷日志), 冷却后半开探测。
|
|
175
|
+
addPolicySubscriber(fn, { priority = 0, name = "", breaker = null } = {}) {
|
|
176
|
+
if (typeof fn !== "function") throw new Error("策略订阅者需为函数");
|
|
177
|
+
const sub = {
|
|
178
|
+
fn,
|
|
179
|
+
priority: Number(priority) || 0,
|
|
180
|
+
name: name || `policy-${this.policySubscribers.length + 1}`,
|
|
181
|
+
// fail-closed: 熔断期 before() 返回 {allowed:false}, 由策略链跳过该订阅者 (弃权) 而非放行
|
|
182
|
+
breaker: new CircuitBreaker({
|
|
183
|
+
...DEFAULT_SUBSCRIBER_BREAKER,
|
|
184
|
+
...(breaker || {}),
|
|
185
|
+
failPolicy: "fail-closed",
|
|
186
|
+
}),
|
|
187
|
+
};
|
|
188
|
+
this.policySubscribers.push(sub);
|
|
189
|
+
return () => {
|
|
190
|
+
const i = this.policySubscribers.indexOf(sub);
|
|
191
|
+
if (i >= 0) this.policySubscribers.splice(i, 1);
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 策略订阅者熔断状态 (可观测): 谁在闭合/熔断/半开, 调用数/熔断次数/窗口内失败数
|
|
196
|
+
policyStatus() {
|
|
197
|
+
return this.policySubscribers.map((s) => ({
|
|
198
|
+
name: s.name,
|
|
199
|
+
priority: s.priority,
|
|
200
|
+
...(s.breaker && typeof s.breaker.stats === "function" ? s.breaker.stats() : {}),
|
|
201
|
+
}));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 未注入策略订阅者时零开销 (空数组循环天然跳过)
|
|
205
|
+
async _runPolicyChain(name, args, ctx) {
|
|
206
|
+
if (!this.policySubscribers.length) return { decision: "allow", reason: null, priority: 0 };
|
|
207
|
+
const results = await Promise.all(this.policySubscribers.map(async (sub) => {
|
|
208
|
+
const breaker = sub.breaker;
|
|
209
|
+
// 熔断期: 跳过故障订阅者 (视同弃权), 避免反复调用 + 日志刷屏
|
|
210
|
+
const verdict = breaker && typeof breaker.before === "function" ? breaker.before() : { allowed: true };
|
|
211
|
+
if (!verdict.allowed) {
|
|
212
|
+
info(`[policy] 订阅者 ${sub.name} 熔断中 (${verdict.reason}), 本轮弃权`);
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
const d = await sub.fn(name, args, ctx);
|
|
217
|
+
breaker?.after?.(true);
|
|
218
|
+
if (!d || !d.decision) return null;
|
|
219
|
+
return { decision: d.decision, reason: d.reason || null, priority: d.priority ?? sub.priority };
|
|
220
|
+
} catch (e) {
|
|
221
|
+
breaker?.after?.(false);
|
|
222
|
+
const st = breaker && typeof breaker.state === "string" ? breaker.state : "closed";
|
|
223
|
+
info(`[policy] 订阅者 ${sub.name} 异常, 视同弃权: ${e?.message || e}${st === "open" ? " (已熔断)" : ""}`);
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
}));
|
|
227
|
+
return consolidateDecisions(results.filter(Boolean));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ---- Consumer: 统一策略执行 ----
|
|
231
|
+
async call(name, args, ctx = {}) {
|
|
232
|
+
const meta = this.tools.get(name);
|
|
233
|
+
if (!meta) {
|
|
234
|
+
return `${TOOL_ERROR_PREFIX} 未知工具: ${name}`;
|
|
235
|
+
}
|
|
236
|
+
// 参数校验先行 (在权限/策略之前: 参数都错了就别问权限)
|
|
237
|
+
const argProblem = validateArgs(meta, args);
|
|
238
|
+
if (argProblem) {
|
|
239
|
+
const hint = Object.keys(meta.parameters?.properties || {}).length
|
|
240
|
+
? ` 可用参数: ${Object.keys(meta.parameters.properties).join(", ")}`
|
|
241
|
+
: "";
|
|
242
|
+
return `${TOOL_ERROR_PREFIX} ${name}: 参数错误 — ${argProblem}.${hint}`;
|
|
243
|
+
}
|
|
244
|
+
info(`tool: ${name}(${JSON.stringify(args)})`);
|
|
245
|
+
// P0: 策略链先行 (deny-wins) —— 免疫闸门/命令守卫/防注入在此拦截, 不可被旁路
|
|
246
|
+
const policy = await this._runPolicyChain(name, args, ctx);
|
|
247
|
+
if (policy.decision === "deny") {
|
|
248
|
+
return `${TOOL_ERROR_PREFIX} ${name}: 策略拦截: ${policy.reason || "未授权"}`;
|
|
249
|
+
}
|
|
250
|
+
if (policy.decision === "ask") {
|
|
251
|
+
return `${TOOL_ERROR_PREFIX} ${name}: 需要人工审批: ${policy.reason || "敏感操作"}`;
|
|
252
|
+
}
|
|
253
|
+
if (!this.audit) return runWithPolicy(meta, args, ctx);
|
|
254
|
+
const t0 = Date.now();
|
|
255
|
+
try {
|
|
256
|
+
const r = await runWithPolicy(meta, args, ctx);
|
|
257
|
+
const failed = typeof r === "string" && r.startsWith(TOOL_ERROR_PREFIX);
|
|
258
|
+
this.audit.append({ tool: name, args, ok: !failed, error: failed ? String(r).slice(0, 200) : null, ms: Date.now() - t0 });
|
|
259
|
+
return r;
|
|
260
|
+
} catch (e) {
|
|
261
|
+
this.audit.append({ tool: name, args, ok: false, error: e?.message || String(e), ms: Date.now() - t0 });
|
|
262
|
+
throw e;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
has(name) {
|
|
267
|
+
return this.tools.has(name);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// 元数据查询 (v1.6.0 第四刀: 超时重试需要知道工具是否幂等/超时预算)
|
|
271
|
+
metaOf(name) {
|
|
272
|
+
return this.tools.get(name) || null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
list() {
|
|
276
|
+
return [...this.tools.keys()];
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ---- 元数据枚举 (供 selfmod / 追踪) ----
|
|
280
|
+
listDetailed() {
|
|
281
|
+
return [...this.tools.values()].map(toDescriptor);
|
|
282
|
+
}
|
|
283
|
+
}
|