@bolloon/bolloon-agent 0.4.29 → 0.4.31

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.
Files changed (83) hide show
  1. package/dist/agents/agent-gateway.js +5 -0
  2. package/dist/agents/agent-manifest-protocol.js +29 -0
  3. package/dist/agents/contacts/chain.js +579 -0
  4. package/dist/agents/contacts/cli.js +158 -0
  5. package/dist/agents/contacts/consent.js +124 -0
  6. package/dist/agents/contacts/grant-payload.js +45 -0
  7. package/dist/agents/contacts/grants.js +451 -0
  8. package/dist/agents/contacts/policy.js +264 -0
  9. package/dist/agents/contacts/preview-types.js +6 -0
  10. package/dist/agents/contacts/providers.js +341 -0
  11. package/dist/agents/contacts/store.js +326 -0
  12. package/dist/agents/contacts/tools.js +154 -0
  13. package/dist/agents/contacts/types.js +148 -0
  14. package/dist/agents/economic-policy.js +7 -0
  15. package/dist/agents/gateway-network.js +6 -0
  16. package/dist/agents/local-signer.js +59 -0
  17. package/dist/agents/network-pulse.js +450 -0
  18. package/dist/agents/pi-sdk.js +10 -0
  19. package/dist/agents/task-contract.js +322 -0
  20. package/dist/agents/task-inbox.js +267 -0
  21. package/dist/agents/task-transport.js +280 -0
  22. package/dist/agents/x402/transaction-store.js +8 -1
  23. package/dist/cli/commands/agent.js +230 -0
  24. package/dist/cli/commands/index.js +61 -0
  25. package/dist/cli/commands/mcp.js +98 -0
  26. package/dist/cli/commands/network.js +334 -0
  27. package/dist/cli/commands/payment.js +125 -0
  28. package/dist/cli/commands/tasks.js +1236 -0
  29. package/dist/cli/commands/trade.js +229 -0
  30. package/dist/cli/commands/wallet.js +450 -0
  31. package/dist/cli/mcp/bridge.js +105 -0
  32. package/dist/cli/mcp/server.js +185 -0
  33. package/dist/cli/mcp/tools.js +612 -0
  34. package/dist/cli/protocol-envelope.js +346 -0
  35. package/dist/cli-entry.js +141 -17
  36. package/dist/electron-build/agents/agent-gateway.js +5 -0
  37. package/dist/electron-build/agents/agent-gateway.js.map +1 -1
  38. package/dist/electron-build/agents/agent-manifest-protocol.js +62 -0
  39. package/dist/electron-build/agents/agent-manifest-protocol.js.map +1 -1
  40. package/dist/electron-build/agents/contacts/chain.js +619 -0
  41. package/dist/electron-build/agents/contacts/chain.js.map +1 -0
  42. package/dist/electron-build/agents/contacts/consent.js +163 -0
  43. package/dist/electron-build/agents/contacts/consent.js.map +1 -0
  44. package/dist/electron-build/agents/contacts/grant-payload.js +52 -0
  45. package/dist/electron-build/agents/contacts/grant-payload.js.map +1 -0
  46. package/dist/electron-build/agents/contacts/grants.js +500 -0
  47. package/dist/electron-build/agents/contacts/grants.js.map +1 -0
  48. package/dist/electron-build/agents/contacts/policy.js +271 -0
  49. package/dist/electron-build/agents/contacts/policy.js.map +1 -0
  50. package/dist/electron-build/agents/contacts/preview-types.js +8 -0
  51. package/dist/electron-build/agents/contacts/preview-types.js.map +1 -0
  52. package/dist/electron-build/agents/contacts/providers.js +384 -0
  53. package/dist/electron-build/agents/contacts/providers.js.map +1 -0
  54. package/dist/electron-build/agents/contacts/store.js +370 -0
  55. package/dist/electron-build/agents/contacts/store.js.map +1 -0
  56. package/dist/electron-build/agents/contacts/tools.js +159 -0
  57. package/dist/electron-build/agents/contacts/tools.js.map +1 -0
  58. package/dist/electron-build/agents/contacts/types.js +166 -0
  59. package/dist/electron-build/agents/contacts/types.js.map +1 -0
  60. package/dist/electron-build/agents/economic-policy.js +7 -0
  61. package/dist/electron-build/agents/economic-policy.js.map +1 -1
  62. package/dist/electron-build/agents/external-events.js.map +1 -1
  63. package/dist/electron-build/agents/gateway-network.js +6 -0
  64. package/dist/electron-build/agents/gateway-network.js.map +1 -1
  65. package/dist/electron-build/agents/goal-store.js.map +1 -1
  66. package/dist/electron-build/agents/network-pulse.js +504 -0
  67. package/dist/electron-build/agents/network-pulse.js.map +1 -0
  68. package/dist/electron-build/agents/pi-sdk.js +10 -0
  69. package/dist/electron-build/agents/pi-sdk.js.map +1 -1
  70. package/dist/electron-build/agents/x402/transaction-store.js +8 -1
  71. package/dist/electron-build/agents/x402/transaction-store.js.map +1 -1
  72. package/dist/electron-build/security/tool-gate.js +4 -0
  73. package/dist/electron-build/security/tool-gate.js.map +1 -1
  74. package/dist/index.js +30 -0
  75. package/dist/security/tool-gate.js +4 -0
  76. package/dist/web/mobile-contacts.js +329 -0
  77. package/dist/web/mobile-core.js +481 -15
  78. package/dist/web/mobile.html +32 -0
  79. package/dist/web/mobile.js +139 -0
  80. package/dist/web/routes-contacts.js +420 -0
  81. package/dist/web/server.js +34 -0
  82. package/dist/web/task-frame-server.js +211 -0
  83. package/package.json +1 -1
@@ -0,0 +1,59 @@
1
+ /**
2
+ * local-signer.ts — 本机 DID 身份签名器 (P3, 2026-09-21)
3
+ *
4
+ * `bolloon-task/1` 的每一个信封 (request / accept / reject / result) 都必须带签名。
5
+ * 签名用的是本机 DIAP 身份 (`~/.bolloon/identity.json`, ed25519) —— **私钥只在本机进程内存里**,
6
+ * 本模块**只返回可用于签名的 keypair 对象**, 绝不返回私钥字符串/hex, 也不打印任何密钥材料。
7
+ *
8
+ * 红线 (P1 §5.5):
9
+ * · 私钥永不进 stdout/日志/审计账本;
10
+ * · 公钥 (hex) 可以随帧一起走 P2P —— 对端需要它来验签, 它是公开材料;
11
+ * · 没有本机身份 → 返回 null, 调用方必须**如实失败**, 不许用假签名糊过去。
12
+ */
13
+ import * as os from 'os';
14
+ import * as path from 'path';
15
+ import * as crypto from 'crypto';
16
+ /** 本机身份文件 (与 network-pulse 快照签名同源: ~/.bolloon/identity.json) */
17
+ export function identityFile(home) {
18
+ return path.join(home || process.env.HOME || os.homedir(), '.bolloon', 'identity.json');
19
+ }
20
+ /**
21
+ * 加载本机签名器。文件缺失/损坏 → null (调用方如实报"没有可签名的身份")。
22
+ */
23
+ export async function loadLocalSigner(home) {
24
+ try {
25
+ const file = identityFile(home);
26
+ const { KeyManager } = (await import('@diap/sdk'));
27
+ const kp = await KeyManager.fromFile(file);
28
+ if (!kp?.privateKey || !kp?.publicKey)
29
+ return null;
30
+ return { did: String(kp.did || ''), publicKeyHex: Buffer.from(kp.publicKey).toString('hex'), keypair: kp };
31
+ }
32
+ catch {
33
+ return null;
34
+ }
35
+ }
36
+ /** 本机公钥 hex (给帧的信封用; 没有身份 → null) */
37
+ export async function localPublicKeyHex(home) {
38
+ const s = await loadLocalSigner(home);
39
+ return s?.publicKeyHex ?? null;
40
+ }
41
+ /**
42
+ * 从公钥 hex 造一个 `KeyManager.verify` 能吃的 verifier。
43
+ * (@diap/sdk 的 verify 只用到 `publicKey` 字段 —— 它改不了密钥的用途, 这里不涉及任何私钥。)
44
+ */
45
+ export function verifierFor(publicKeyHex) {
46
+ if (!/^[0-9a-f]{64}$/i.test(String(publicKeyHex || '')))
47
+ return null;
48
+ return { publicKey: new Uint8Array(Buffer.from(publicKeyHex, 'hex')) };
49
+ }
50
+ /**
51
+ * 指纹 (sha256 前 12 位 hex) —— 只用于日志/审计的"是谁签的", **不可逆, 不含密钥材料**。
52
+ */
53
+ export function fingerprintOf(input) {
54
+ return `sha256:${crypto.createHash('sha256').update(String(input || '')).digest('hex').slice(0, 12)}`;
55
+ }
56
+ /** payload 摘要 (审计账本只记摘要, 不记正文) */
57
+ export function payloadDigest(payload) {
58
+ return crypto.createHash('sha256').update(String(payload ?? '')).digest('hex');
59
+ }
@@ -0,0 +1,450 @@
1
+ /**
2
+ * network-pulse.ts — 网络脉冲 (Network Pulse): 把已有 P2P 生命周期投影成
3
+ * **匿名、可验证、可降级**的公开统计, 供 bolloon-UI 网关页动态展示。
4
+ *
5
+ * 设计约束 (leo 2026-09-18 计划):
6
+ * · 只对**公开统计**有用的事实留档: 不存任务正文、不存私有 payload
7
+ * · 浏览器**永远不接触原始事件** —— 对外只有聚合快照
8
+ * · 不暴露 DID / peerId / IP / 钱包地址 / Agent 私有内容
9
+ * · 单节点看到的数据**不许说成全网精确总量**: scope=observed / verified 明确写在快照里
10
+ * · 快照过期 → `stale` (不许伪装实时); 观察层不可用 → `unavailable`
11
+ * · 类别小于隐私阈值 → 合并进 other
12
+ * · 事件数 / 时间窗 / capability 数都有上限
13
+ *
14
+ * 借鉴 (只借产品与工程思想, 不借 Go/Postgres/API 项目):
15
+ * EigenFlux 的 "注册总量 / 当前活跃 / 最近出现" 时间窗统计、服务端生成匿名活动文本、
16
+ * live/stale/unavailable 状态、短缓存与时间边界、隐私阈值、白名单投影。
17
+ */
18
+ import * as fs from 'fs';
19
+ import * as os from 'os';
20
+ import * as path from 'path';
21
+ import * as crypto from 'crypto';
22
+ // ── 常量 (上限 + 时间边界) ─────────────────────────────────────────────────
23
+ export const PULSE_LIMITS = {
24
+ /** 最多留多少条事件 (超出丢最旧) */
25
+ maxEvents: 5000,
26
+ /** 统计窗口 */
27
+ windowMs: 24 * 60 * 60 * 1000,
28
+ /** "活跃"的定义: 最近 5 分钟出现过 */
29
+ activeWindowMs: 5 * 60 * 1000,
30
+ /** 时间桶 (小时) */
31
+ bucketMs: 60 * 60 * 1000,
32
+ /** 对外最多列多少个 capability 类别 */
33
+ maxCapabilities: 12,
34
+ /** 隐私阈值: 少于这个数的类别合并进 other */
35
+ privacyThreshold: 3,
36
+ /** 快照新鲜期 (秒) → 之后 stale */
37
+ snapshotTtlMs: 30 * 1000,
38
+ /** 活动流最多几条 */
39
+ maxActivity: 8,
40
+ };
41
+ /** 事件类型白名单 —— 只允许这些进统计 (其余一律丢弃) */
42
+ export const NETWORK_EVENT_TYPES = [
43
+ 'node_joined',
44
+ 'manifest_published',
45
+ 'capability_announced',
46
+ 'peer_connected',
47
+ 'delegation_completed',
48
+ // 经济事件 (P6 扩展; 原五类保持兼容, 老节点发来的事件仍被接受)
49
+ 'task_posted', 'task_accepted', 'task_completed', 'trade_settled', 'trade_verified',
50
+ 'wallet_signed',
51
+ ];
52
+ // ── 存储 ────────────────────────────────────────────────────────────────────
53
+ const home = (h) => h || process.env.HOME || os.homedir() || '/tmp';
54
+ export const pulseDir = (h) => path.join(home(h), '.bolloon', 'network-pulse');
55
+ const eventsFile = (h) => path.join(pulseDir(h), 'events.json');
56
+ const snapshotFile = (h) => path.join(pulseDir(h), 'snapshot.json');
57
+ /** 是否是一条结构合法的事件 (坏数据一律丢弃, 不让快照崩) */
58
+ function isValidEvent(e) {
59
+ return !!e && typeof e === 'object'
60
+ && NETWORK_EVENT_TYPES.includes(e.type)
61
+ && typeof e.sourceProof === 'string' && e.sourceProof.length > 0
62
+ && Number.isFinite(e.occurredAt);
63
+ }
64
+ function readEvents(h) {
65
+ try {
66
+ const raw = JSON.parse(fs.readFileSync(eventsFile(h), 'utf-8'));
67
+ return Array.isArray(raw) ? raw.filter(isValidEvent) : [];
68
+ }
69
+ catch {
70
+ return [];
71
+ }
72
+ }
73
+ function writeEvents(list, h) {
74
+ try {
75
+ fs.mkdirSync(pulseDir(h), { recursive: true });
76
+ fs.writeFileSync(eventsFile(h), JSON.stringify(list.slice(-PULSE_LIMITS.maxEvents)), 'utf8');
77
+ }
78
+ catch {
79
+ /* 统计失败绝不影响主路径 */
80
+ }
81
+ }
82
+ // ── 匿名化 ──────────────────────────────────────────────────────────────────
83
+ /** 节点摘要: sha256(值) 前 16 位。不可逆, 不暴露原值。 */
84
+ export function nodeDigest(value) {
85
+ return crypto.createHash('sha256').update(`bolloon-pulse|${String(value)}`).digest('hex').slice(0, 16);
86
+ }
87
+ const CAPABILITY_GROUPS = [
88
+ [/research|market|调研|分析|survey|search/i, 'research'],
89
+ [/code|review|refactor|debug|编程|代码/i, 'coding'],
90
+ [/data|dataset|etl|metric|数据/i, 'data'],
91
+ [/write|content|doc|translat|写作|文档|翻译/i, 'writing'],
92
+ [/automation|workflow|deploy|ops|自动化/i, 'automation'],
93
+ [/vision|image|audio|speech|视觉|图像|语音/i, 'multimodal'],
94
+ ];
95
+ /** 把原始能力名归并成粗类别 (不外泄原始能力名) */
96
+ export function capabilityGroup(raw) {
97
+ const s = String(raw || '').trim();
98
+ if (!s)
99
+ return 'other';
100
+ for (const [re, g] of CAPABILITY_GROUPS)
101
+ if (re.test(s))
102
+ return g;
103
+ return 'other';
104
+ }
105
+ export function bucketOf(at) {
106
+ return String(Math.floor(at / PULSE_LIMITS.bucketMs));
107
+ }
108
+ /** 完整性标记 (本地校验用; 不含任何可逆信息) */
109
+ function integrityOf(ev) {
110
+ return nodeDigest(`${ev.type}|${ev.bucket}|${ev.sourceProof}|${ev.occurredAt}`);
111
+ }
112
+ /**
113
+ * 记一条网络事件。**永不影响主路径**: 非法类型/写失败/异常一律静默丢弃。
114
+ * 幂等: 同一节点同一桶内的 `node_joined` 只记一次 (重复 join 不虚增节点数)。
115
+ */
116
+ export async function recordNetworkEvent(input, h) {
117
+ try {
118
+ const type = String(input?.type || '');
119
+ if (!NETWORK_EVENT_TYPES.includes(type))
120
+ return { ok: false, reason: `非白名单事件类型: ${type}` };
121
+ const at = Number(input.at ?? Date.now());
122
+ if (!Number.isFinite(at) || at <= 0)
123
+ return { ok: false, reason: 'occurredAt 非法' };
124
+ const sourceProof = nodeDigest(String(input.did || 'unknown-node'));
125
+ const bucket = bucketOf(at);
126
+ const agentProof = input.agentId ? nodeDigest(`${String(input.did || 'unknown-node')}:${String(input.agentId)}`) : undefined;
127
+ const taskProof = input.taskId ? nodeDigest(`task:${String(input.taskId)}`) : undefined;
128
+ const list = readEvents(h);
129
+ if (type === 'node_joined' && list.some((e) => e.type === 'node_joined' && e.sourceProof === sourceProof && e.bucket === bucket)) {
130
+ return { ok: true, reason: 'already' }; // 同桶重复入网不虚增
131
+ }
132
+ const ev = {
133
+ type,
134
+ bucket,
135
+ occurredAt: at,
136
+ sourceProof,
137
+ ...(input.capability ? { capabilityGroup: capabilityGroup(input.capability) } : {}),
138
+ ...(agentProof ? { agentProof } : {}),
139
+ ...(taskProof ? { taskProof } : {}),
140
+ ...(input.signed ? { signed: true } : {}),
141
+ };
142
+ ev.integrity = integrityOf(ev);
143
+ list.push(ev);
144
+ // 只保留窗口内 + 上限
145
+ const cutoff = at - PULSE_LIMITS.windowMs;
146
+ writeEvents(list.filter((e) => e.occurredAt >= cutoff), h);
147
+ return { ok: true };
148
+ }
149
+ catch (e) {
150
+ return { ok: false, reason: String(e?.message || e) };
151
+ }
152
+ }
153
+ // ── 快照 ────────────────────────────────────────────────────────────────────
154
+ /** 服务端固定模板生成匿名活动文本 (不让前端拼字符串) */
155
+ export function renderActivityText(kind) {
156
+ switch (kind) {
157
+ case 'node_joined': return { zh: '有新节点加入网络', en: 'A node joined the network' };
158
+ case 'manifest_published': return { zh: '有节点发布/更新了能力声明', en: 'A node published or updated its manifest' };
159
+ case 'capability_announced': return { zh: '有节点公开声明了新能力', en: 'A node announced a capability' };
160
+ case 'peer_connected': return { zh: '两个节点建立了连接', en: 'Two nodes connected' };
161
+ case 'delegation_completed': return { zh: '一次能力委派完成', en: 'A delegation completed' };
162
+ default: return { zh: '网络有活动', en: 'Network activity' };
163
+ }
164
+ }
165
+ export const MAX_AGENT_SITES = 5;
166
+ /** 归一化 IPNS 标识: 只接受裸 k51…/12D3… · ipns://… · /ipns/… —— 其它一律拒绝 (不猜、不放行) */
167
+ export function normalizeIpns(raw) {
168
+ const s = String(raw || '').trim();
169
+ if (!s)
170
+ return null;
171
+ let v = s;
172
+ if (/^ipns:\/\//i.test(v))
173
+ v = v.replace(/^ipns:\/\//i, '');
174
+ else if (/^\/ipns\//i.test(v))
175
+ v = v.replace(/^\/ipns\//i, '');
176
+ else if (/https?:\/\//i.test(v))
177
+ return null; // 不接受 http(s) 直链
178
+ v = v.split(/[/?#]/)[0].trim();
179
+ if (/^(k51|12D3)[a-zA-Z0-9]{20,}$/.test(v) || /^[a-zA-Z0-9]{46,}$/.test(v))
180
+ return v;
181
+ return null;
182
+ }
183
+ /** 读本节点显式发布的私有站清单 (~/.bolloon/agent-sites.json); 不存在/坏数据 → 空数组 */
184
+ export function readAgentSites(h) {
185
+ try {
186
+ const file = path.join(home(h), '.bolloon', 'agent-sites.json');
187
+ const raw = JSON.parse(fs.readFileSync(file, 'utf-8'));
188
+ if (!Array.isArray(raw))
189
+ return [];
190
+ const out = [];
191
+ const seen = new Set();
192
+ for (const item of raw) {
193
+ const ipns = normalizeIpns(item?.ipns);
194
+ if (!ipns || seen.has(ipns))
195
+ continue;
196
+ seen.add(ipns);
197
+ out.push({
198
+ label: String(item?.label || 'agent').slice(0, 40),
199
+ ipns,
200
+ added_at: Number(item?.added_at) || 0,
201
+ });
202
+ if (out.length >= MAX_AGENT_SITES)
203
+ break;
204
+ }
205
+ return out;
206
+ }
207
+ catch {
208
+ return [];
209
+ }
210
+ }
211
+ /**
212
+ * 把**真实交易活动**投影成脉冲事件 (由交易写路径调用, fire-and-forget)。
213
+ * 只按事实发: 交付 → task_completed · 真验真 → trade_verified · **链上口径**结算 → trade_settled。
214
+ * local-dev 上限是 payment_submitted, 永远进不了 trade_settled 那一支 —— 不冒充链上。
215
+ */
216
+ export async function emitTradePulse(before, after, h) {
217
+ try {
218
+ if (!after)
219
+ return;
220
+ const taskId = String(after.requestId || after.transactionId || '');
221
+ if (!taskId)
222
+ return;
223
+ const did = String(after.buyerDid || after.providerDid || after.payTo || after.paymentMode || 'unknown-node');
224
+ const agentId = String(after.agentId || after.runId || after.goalId || '') || undefined;
225
+ const base = { did, agentId, taskId, signed: true };
226
+ if (after.status === 'delivered' && before?.status !== 'delivered') {
227
+ await recordNetworkEvent({ type: 'task_completed', ...base }, h);
228
+ }
229
+ if (after.status === 'verified' && before?.status !== 'verified') {
230
+ await recordNetworkEvent({ type: 'trade_verified', ...base }, h);
231
+ }
232
+ if (after.settlementFact === 'fully_settled' && before?.settlementFact !== 'fully_settled') {
233
+ await recordNetworkEvent({ type: 'trade_settled', ...base }, h);
234
+ }
235
+ }
236
+ catch {
237
+ /* 统计失败绝不影响交易主路径 */
238
+ }
239
+ }
240
+ export function computeSnapshot(events, opts) {
241
+ const now = opts.now;
242
+ const fresh_until = now + PULSE_LIMITS.snapshotTtlMs;
243
+ if (opts.unavailable) {
244
+ return {
245
+ status: 'unavailable',
246
+ generated_at: now,
247
+ fresh_until,
248
+ scope: 'observed',
249
+ scope_label: { zh: '当前节点观察到', en: 'Observed by this node' },
250
+ totals: { nodes: 0, agents: 0, active_agents: 0, seen_last_24h: 0, tasks: 0, tasks_completed: 0, tasks_verified: 0, signatures: 0 },
251
+ capabilities: [],
252
+ recent_activity: [],
253
+ notes: ['观察层暂不可用 — 这不是"网络为空"'],
254
+ };
255
+ }
256
+ const window = (Array.isArray(events) ? events : []).filter((e) => isValidEvent(e) && e.occurredAt >= now - PULSE_LIMITS.windowMs);
257
+ const nodes = new Set(window.map((e) => e.sourceProof).filter(Boolean));
258
+ const agents = new Set(window.map((e) => e.agentProof).filter(Boolean));
259
+ const active = new Set(window.filter((e) => e.occurredAt >= now - PULSE_LIMITS.activeWindowMs).map((e) => e.agentProof).filter(Boolean));
260
+ // capability 分布 (按粗类别计数, 每个类别按**不同的 agent**去重, 避免单节点刷高)
261
+ const capMap = new Map();
262
+ for (const e of window) {
263
+ if (e.type !== 'capability_announced' || !e.capabilityGroup)
264
+ continue;
265
+ const key = e.capabilityGroup;
266
+ if (!capMap.has(key))
267
+ capMap.set(key, new Set());
268
+ capMap.get(key).add(e.agentProof || e.sourceProof);
269
+ }
270
+ let capabilities = Array.from(capMap.entries())
271
+ .map(([key, set]) => ({ key, count: set.size }))
272
+ .sort((a, b) => b.count - a.count || (a.key < b.key ? -1 : 1))
273
+ .slice(0, PULSE_LIMITS.maxCapabilities);
274
+ // 隐私阈值: 少于阈值的类别合并进 other (other 本身也按阈值判断是否保留)
275
+ const small = capabilities.filter((c) => c.count < PULSE_LIMITS.privacyThreshold && c.key !== 'other');
276
+ if (small.length > 0) {
277
+ capabilities = capabilities.filter((c) => !small.includes(c));
278
+ const otherCount = new Set();
279
+ for (const c of small)
280
+ otherCount.add(c.key);
281
+ capabilities.push({ key: 'other', count: otherCount.size });
282
+ capabilities.sort((a, b) => b.count - a.count || (a.key < b.key ? -1 : 1));
283
+ }
284
+ // 经济计数: 按**不同任务**去重 (同一任务重复事件不虚增), 且只给聚合数不给内容
285
+ const tasks = new Set();
286
+ const tasksCompleted = new Set();
287
+ const tasksVerified = new Set();
288
+ for (const e of window) {
289
+ const t = e.taskProof;
290
+ if (!t)
291
+ continue;
292
+ if (e.type === 'task_posted' || e.type === 'task_accepted')
293
+ tasks.add(t);
294
+ if (e.type === 'task_completed' || e.type === 'trade_settled')
295
+ tasksCompleted.add(t);
296
+ if (e.type === 'trade_verified')
297
+ tasksVerified.add(t);
298
+ }
299
+ // 钱包签名计数: 按 (来源, 时刻) 去重 —— 同一次签名重复上报不虚增
300
+ const signatureKeys = new Set();
301
+ for (const e of window)
302
+ if (e.type === 'wallet_signed')
303
+ signatureKeys.add(`${e.sourceProof}|${e.occurredAt}`);
304
+ const recent = [...window]
305
+ .sort((a, b) => b.occurredAt - a.occurredAt)
306
+ .slice(0, PULSE_LIMITS.maxActivity)
307
+ .map((e) => ({ kind: e.type, at: e.occurredAt, text: renderActivityText(e.type) }));
308
+ const signedNodes = opts.signedNodes ?? new Set(window.filter((e) => e.signed).map((e) => e.sourceProof)).size;
309
+ const scope = signedNodes >= 2 ? 'verified' : 'observed';
310
+ const notes = [];
311
+ if (scope === 'observed')
312
+ notes.push('单节点观察: 这是本节点能看到的部分网络, 不是全网精确总量');
313
+ else
314
+ notes.push(`多签名来源汇总 (${signedNodes} 个签名节点)`);
315
+ return {
316
+ status: 'live',
317
+ generated_at: now,
318
+ fresh_until,
319
+ scope,
320
+ scope_label: scope === 'verified'
321
+ ? { zh: '网络观察快照 (多签名来源)', en: 'Verified network snapshot' }
322
+ : { zh: '当前节点观察到', en: 'Observed by this node' },
323
+ totals: {
324
+ nodes: nodes.size,
325
+ agents: agents.size,
326
+ active_agents: active.size,
327
+ seen_last_24h: agents.size, // 24h 窗口内的不同 Agent
328
+ tasks: tasks.size, // 观察到发起的任务数 (聚合, 无内容)
329
+ tasks_completed: tasksCompleted.size,
330
+ tasks_verified: tasksVerified.size,
331
+ signatures: signatureKeys.size, // 真实签名次数 (只计数)
332
+ },
333
+ capabilities,
334
+ recent_activity: recent,
335
+ notes,
336
+ };
337
+ }
338
+ /** 读快照 (带 30s 短缓存; 过期 → 重算; 重算失败 → unavailable) */
339
+ export async function getNetworkPulse(opts = {}) {
340
+ const now = opts.now ?? Date.now();
341
+ if (opts.unavailable)
342
+ return computeSnapshot([], { now, unavailable: true });
343
+ if (!opts.force) {
344
+ try {
345
+ const cached = JSON.parse(fs.readFileSync(snapshotFile(opts.home), 'utf-8'));
346
+ if (cached && Number.isFinite(cached.generated_at) && cached.generated_at + PULSE_LIMITS.snapshotTtlMs > now)
347
+ return cached;
348
+ }
349
+ catch { /* 无缓存 */ }
350
+ }
351
+ let events = [];
352
+ try {
353
+ events = readEvents(opts.home);
354
+ }
355
+ catch (e) {
356
+ return computeSnapshot([], { now, unavailable: true });
357
+ }
358
+ const snap = computeSnapshot(events, { now });
359
+ try {
360
+ fs.mkdirSync(pulseDir(opts.home), { recursive: true });
361
+ fs.writeFileSync(snapshotFile(opts.home), JSON.stringify(snap), 'utf8');
362
+ }
363
+ catch { /* 写不进就算了, 返回值仍然有效 */ }
364
+ return snap;
365
+ }
366
+ /** 快照是否仍是 live (给前端/观察入口判断; 过期即 stale) */
367
+ export function snapshotStatus(snap, now = Date.now()) {
368
+ if (snap.status === 'unavailable')
369
+ return 'unavailable';
370
+ return now <= snap.fresh_until ? 'live' : 'stale';
371
+ }
372
+ // ── 签名 (可验证快照) ───────────────────────────────────────────────────────
373
+ /** 规范化 JSON (键排序), 用于签名覆盖 */
374
+ export function canonicalize(value) {
375
+ const walk = (v) => {
376
+ if (Array.isArray(v))
377
+ return v.map(walk);
378
+ if (v && typeof v === 'object') {
379
+ const out = {};
380
+ for (const k of Object.keys(v).sort())
381
+ out[k] = walk(v[k]);
382
+ return out;
383
+ }
384
+ return v;
385
+ };
386
+ return JSON.stringify(walk(value));
387
+ }
388
+ /** 用本机 DID 身份给快照签名 (公开入口可校验; 只暴露签名与指纹, 不暴露 DID) */
389
+ /** 上一次快照签名失败的原因 (给调用方诊断用; 绝不出现在公开输出里) */
390
+ let lastSignErr = null;
391
+ export function lastSnapshotSignError() { return lastSignErr; }
392
+ /** 快照签名输入是 Uint8Array (ed25519), 存 base64 —— 不能直接把字符串喂给 KeyManager.sign */
393
+ function snapBytes(snap) {
394
+ const payload = { ...snap, signature: undefined, signer_fingerprint: undefined };
395
+ return new TextEncoder().encode(canonicalize(payload));
396
+ }
397
+ function decodeSnapSig(sig) {
398
+ if (/^[0-9a-f]{128}$/i.test(sig))
399
+ return new Uint8Array(Buffer.from(sig, 'hex'));
400
+ return new Uint8Array(Buffer.from(sig, 'base64'));
401
+ }
402
+ export async function signSnapshot(snap, h) {
403
+ lastSignErr = null;
404
+ try {
405
+ const { KeyManager } = await import('@diap/sdk');
406
+ const file = path.join(home(h), '.bolloon', 'identity.json');
407
+ const kp = await KeyManager.fromFile(file);
408
+ if (!kp?.privateKey) {
409
+ lastSignErr = `没有可用身份 (${file}) → 未签名`;
410
+ return snap;
411
+ }
412
+ const sig = await KeyManager.sign(kp, snapBytes(snap));
413
+ const sigStr = typeof sig === 'string' ? sig : Buffer.from(sig).toString('base64');
414
+ return { ...snap, signature: sigStr, signer_fingerprint: nodeDigest(String(kp.did || '')) };
415
+ }
416
+ catch (e) {
417
+ lastSignErr = String(e?.message || e);
418
+ return snap; // 没身份/签名失败 → 就返回未签名快照 (绝不假装签过)
419
+ }
420
+ }
421
+ /** 校验快照签名 (需要显式传入公钥方; 内部/测试用) */
422
+ export async function verifySnapshotSignature(snap, publicKey) {
423
+ try {
424
+ if (!snap.signature)
425
+ return false;
426
+ const { KeyManager } = await import('@diap/sdk');
427
+ return await KeyManager.verify(publicKey, snapBytes(snap), decodeSnapSig(snap.signature));
428
+ }
429
+ catch {
430
+ return false;
431
+ }
432
+ }
433
+ /** 公开投影的"禁止出现"字段检查 (测试与调用方共用) */
434
+ export const FORBIDDEN_PUBLIC_KEYS = ['did', 'peerId', 'peer_id', 'multiaddrs', 'ip', 'wallet', 'address', 'privateKey', 'content', 'instruction', 'payload'];
435
+ export function assertNoPrivateFields(value, at = '$') {
436
+ const issues = [];
437
+ const walk = (v, p) => {
438
+ if (Array.isArray(v))
439
+ return v.forEach((x, i) => walk(x, `${p}[${i}]`));
440
+ if (v && typeof v === 'object') {
441
+ for (const [k, val] of Object.entries(v)) {
442
+ if (FORBIDDEN_PUBLIC_KEYS.includes(k))
443
+ issues.push(`${p}.${k} 不该出现在公开投影里`);
444
+ walk(val, `${p}.${k}`);
445
+ }
446
+ }
447
+ };
448
+ walk(value, at);
449
+ return issues;
450
+ }
@@ -45,6 +45,8 @@ export { TOOL_DEFINITIONS, } from './pi-sdk-types.js';
45
45
  export { PiSessionManager } from './pi-sdk-session-manager.js';
46
46
  import { PiSessionManager } from './pi-sdk-session-manager.js';
47
47
  import { registerBuiltinTools, registerWalletTools, setupInboxListener, IdempotencyCache, } from './pi-sdk-tools.js';
48
+ import { registerContactTools } from './contacts/tools.js';
49
+ import { ContactChain } from './contacts/chain.js';
48
50
  export { createAgentSession, getAgentSession, resetAgentSession, runSelfImproveLoop, } from './pi-sdk-session-factory.js';
49
51
  // Judgment 注入门 (P0): 在主对话 LLM 调起前自动拼入 Top 3 判断力
50
52
  // 失败静默, 不阻塞主对话
@@ -575,6 +577,14 @@ export class PiAgentSession {
575
577
  },
576
578
  };
577
579
  registerBuiltinTools(toolCtx);
580
+ // 2026-09-19: 联系方式工具 (手机/邮箱) —— Agent 拿不到明文, 只能给 contactId;
581
+ // 所有调用都要过 contacts/policy (首次联系/敏感内容需人工批准, 批量永远禁止)。
582
+ try {
583
+ registerContactTools(toolCtx, new ContactChain({ ownerDid: this.identity?.did || 'did:bolln:local' }));
584
+ }
585
+ catch (err) {
586
+ console.warn('[contacts] 注册联系工具失败 (非致命, 该能力不可用):', err?.message || err);
587
+ }
578
588
  registerWalletTools(toolCtx);
579
589
  setupInboxListener(toolCtx);
580
590
  // 镜像到 ToolRegistry (alias resolve 用)