@bolloon/bolloon-agent 0.4.12 → 0.4.14

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.
@@ -0,0 +1,130 @@
1
+ /**
2
+ * agent-gateway.ts — Agent Gateway 协调层 (2026-08-14)
3
+ *
4
+ * Agent Economy 的"经济路由器": 统一入口, 内部编排所有已有层.
5
+ * 定位: 入口层 + 协调层 + 安全边界 (不是新系统, 是把 Registry/DIAP/x402/Policy/Reputation/Treasury 粘起来).
6
+ *
7
+ * 简单用法 (入口要小):
8
+ * gateway.registerAgent({ capability: "research", price: "0.05 USDC" })
9
+ * gateway.callAgent({ task: "analyze paper", budget: 1, capability: "research" })
10
+ * gateway.joinNetwork(link) // 通过链接自动加入共享网络
11
+ *
12
+ * callAgent 自动闭环:
13
+ * Discovery (registry) → Identity (DIAP) → Negotiation (价格/预算匹配)
14
+ * → Policy (预算/白名单) → Payment (x402/审批) → Execution → Reputation
15
+ */
16
+ import { getAgentRegistry, warmAgentRegistry } from './agent-registry.js';
17
+ import { getEconomicPolicy } from './economic-policy.js';
18
+ import { getPaymentGate } from './payment-gate.js';
19
+ import { getApprovalStore } from './payment-approval.js';
20
+ import { recordServiceOutcome } from './agent-reputation.js';
21
+ // ============ 协调层 ============
22
+ /**
23
+ * 注册本 Agent 为服务提供者 (身份 + Registry + 信誉初始化).
24
+ */
25
+ export async function gatewayRegisterAgent(config, identity, deps) {
26
+ // 2026-08-14: 生产路径先 warm OrbitDB — 否则注册只落本地文件, 分享链接指向的 store 是空的.
27
+ // 离线/测试环境 warm 失败不阻塞 (注册仍落本地 fallback).
28
+ let registry = deps?.registry;
29
+ if (!registry) {
30
+ try {
31
+ await warmAgentRegistry();
32
+ }
33
+ catch { /* 离线模式 */ }
34
+ registry = getAgentRegistry();
35
+ }
36
+ const r = await registry.register({
37
+ agentId: identity.did || `did:local:${identity.name}`,
38
+ name: identity.name,
39
+ wallet: config.wallet || identity.wallet,
40
+ service: {
41
+ name: config.capability,
42
+ description: config.description || `${config.capability} 服务`,
43
+ price: {
44
+ amount: config.price,
45
+ currency: (config.currency || 'USDC').toUpperCase(),
46
+ per: config.per || 'query',
47
+ },
48
+ },
49
+ capabilities: [config.capability],
50
+ });
51
+ return { ok: r.ok, error: r.error };
52
+ }
53
+ /**
54
+ * 调用 Agent 服务 (自动闭环).
55
+ * Discovery → Policy → Payment gate → Approval → Execution → Reputation.
56
+ */
57
+ export async function gatewayCallAgent(opts) {
58
+ const { task, budget, capability, url, privateKey } = opts;
59
+ // 1. Discovery: 从 Registry 找 provider
60
+ const registry = getAgentRegistry();
61
+ const providers = capability ? await registry.discover(capability) : await registry.list();
62
+ if (providers.length === 0) {
63
+ return { success: false, error: `未找到能力为 '${capability || 'any'}' 的 Agent (先 joinNetwork 或注册)` };
64
+ }
65
+ const provider = providers[0]; // 简版: 取第一个 (完整版按信誉/价格排序)
66
+ const price = parseFloat(provider.service?.price?.amount || '0');
67
+ // 2. Negotiation: 预算检查
68
+ if (price > budget) {
69
+ return { success: false, error: `价格 ${price} 超预算 ${budget}`, provider: provider.agentId };
70
+ }
71
+ // 3. Payment gate (YAML 验证门): allow/confirm/deny
72
+ const gate = getPaymentGate();
73
+ const verdict = gate.evaluate({ service: provider.service?.name, amount: price, recipient: provider.wallet });
74
+ if (verdict.decision === 'deny') {
75
+ return { success: false, error: `[payment-gate] ${verdict.reason}`, provider: provider.agentId, decision: 'deny' };
76
+ }
77
+ // 4. Policy (预算/白名单)
78
+ const policy = getEconomicPolicy();
79
+ const pol = await policy.check({ payTo: provider.wallet, amount: price, service: provider.service?.name });
80
+ if (!pol.allowed) {
81
+ return { success: false, error: `[policy] ${pol.reason}`, provider: provider.agentId, decision: 'deny' };
82
+ }
83
+ // 5. confirm → 创建人工审批 (不自动执行)
84
+ if (verdict.decision === 'confirm') {
85
+ const store = getApprovalStore();
86
+ const approval = await store.create({
87
+ service: provider.service?.name || capability || 'service',
88
+ amount: price,
89
+ recipient: provider.wallet,
90
+ reason: verdict.reason,
91
+ retryPayload: { task, url, privateKey, serviceName: provider.service?.name, args: { task } },
92
+ });
93
+ return {
94
+ success: false,
95
+ error: `需人工确认: ${verdict.reason} (approval=${approval.id})`,
96
+ provider: provider.agentId,
97
+ decision: 'confirm',
98
+ requiresApproval: true,
99
+ approvalId: approval.id,
100
+ };
101
+ }
102
+ // 6. Execution (x402 支付 + 调用服务)
103
+ try {
104
+ const { serviceCall } = await import('./agent-service-client.js');
105
+ const r = await serviceCall({
106
+ serviceName: provider.service?.name || capability || 'service',
107
+ args: { task },
108
+ privateKey,
109
+ url,
110
+ registry,
111
+ });
112
+ if (!r.success)
113
+ return { success: false, error: r.error, provider: provider.agentId, decision: 'allow' };
114
+ // 7. Reputation: 记录成功
115
+ await recordServiceOutcome(provider.agentId, provider.service?.name || 'service', 'success', registry).catch(() => { });
116
+ return { success: true, provider: provider.agentId, decision: 'allow', output: r.output };
117
+ }
118
+ catch (e) {
119
+ return { success: false, error: `执行失败: ${String(e?.message || e).slice(0, 200)}`, provider: provider.agentId };
120
+ }
121
+ }
122
+ /**
123
+ * 查询网络状态 (注册表 + 信誉概览).
124
+ */
125
+ export async function gatewayStatus() {
126
+ const registry = getAgentRegistry();
127
+ const services = await registry.list();
128
+ const lines = services.slice(0, 20).map((s) => ` [${s.service?.name}] ${s.name} (${s.service?.price?.amount} ${s.service?.price?.currency}/${s.service?.price?.per}) rep=${s.reputation?.score ?? 'n/a'}`);
129
+ return `Agent 网络 (${services.length} 服务):\n${lines.join('\n') || ' (空, 先 joinNetwork 或注册)'}`;
130
+ }
@@ -30,12 +30,38 @@ export async function serviceCall(opts) {
30
30
  // 3. 调服务 (x402 自动支付: 402 → 签名 → 重试)
31
31
  try {
32
32
  const { x402Fetch } = await import('./x402/x402Pay.js');
33
+ // 2026-08-13: YAML 验证门 (不全部交给 AI) — 支付前先过 payment-policy.yaml 规则链
34
+ const { getPaymentGate } = await import('./payment-gate.js');
35
+ const gate = getPaymentGate();
36
+ const amount = parseFloat(service.service?.price?.amount || '0');
37
+ const gateVerdict = gate.evaluate({ service: service.service?.name, amount, recipient: service.wallet });
38
+ if (gateVerdict.decision === 'deny') {
39
+ return { success: false, error: `[payment-gate] ${gateVerdict.reason}`, service };
40
+ }
41
+ if (gateVerdict.decision === 'confirm') {
42
+ // 2026-08-13: 人工审批 — 创建 pending 审批请求 (不自动执行), UI/CLI 批准后重试
43
+ const { getApprovalStore } = await import('./payment-approval.js');
44
+ const store = getApprovalStore();
45
+ const approval = await store.create({
46
+ service: service.service?.name || serviceName,
47
+ amount,
48
+ recipient: service.wallet,
49
+ reason: gateVerdict.reason,
50
+ retryPayload: { serviceName, args, privateKey, maxPaymentAmount: maxPaymentAmount || service.service?.price?.amount },
51
+ });
52
+ return {
53
+ success: false,
54
+ error: `[payment-gate] 需人工确认: ${gateVerdict.reason} (approval=${approval.id}, 批准后自动执行)`,
55
+ service,
56
+ requiresApproval: true,
57
+ approvalId: approval.id,
58
+ };
59
+ }
33
60
  // Phase E3: Policy Engine 授权 (预算/白名单) — 通过才允许自动支付
34
61
  let effectiveKey = privateKey;
35
62
  if (privateKey) {
36
63
  const { getEconomicPolicy } = await import('./economic-policy.js');
37
64
  const policy = getEconomicPolicy();
38
- const amount = parseFloat(service.service?.price?.amount || '0');
39
65
  const decision = await policy.check({
40
66
  payTo: service.wallet,
41
67
  amount,
@@ -0,0 +1,261 @@
1
+ /**
2
+ * gateway-group.ts — Agent Gateway P2P 群组 (2026-08-14)
3
+ *
4
+ * 群组 = OrbitDB events store (accessController write:'*'), 任何成员可广播消息,
5
+ * 全成员通过 OrbitDB pubsub 复制实时同步. 链接: orbitdb://<addr>?type=group&name=<群名>.
6
+ *
7
+ * 符合用户习惯: 群组 = 微信式群聊 — 加入链接即进群, 发消息全网同步.
8
+ * 持久化: ~/.bolloon/gateway-groups.json (重启后仍是群成员, 自动重开 store).
9
+ */
10
+ import * as os from 'os';
11
+ import * as path from 'path';
12
+ import { getCIDDatabase } from '../orbitdb/cid-database.js';
13
+ // ============ 依赖注入 (测试用, 避免单测起真实 OrbitDB 节点) ============
14
+ let _dbOverride = null;
15
+ /** 测试用: 注入 fake CIDDatabase */
16
+ export function setGroupTestDb(db) { _dbOverride = db; }
17
+ function getDb() { return _dbOverride ?? getCIDDatabase(); }
18
+ /** 测试用: 清空 store 缓存 / 订阅 (避免测试间污染) */
19
+ export function resetGroupState() {
20
+ storeCache.clear();
21
+ onChangeCallbacks.clear();
22
+ }
23
+ // ============ 持久化 ============
24
+ const groupsFile = () => path.join(os.homedir() || '/tmp', '.bolloon', 'gateway-groups.json');
25
+ async function loadGroups() {
26
+ try {
27
+ const { readFile } = await import('fs/promises');
28
+ const parsed = JSON.parse(await readFile(groupsFile(), 'utf-8'));
29
+ return Array.isArray(parsed) ? parsed : [];
30
+ }
31
+ catch {
32
+ return [];
33
+ }
34
+ }
35
+ async function saveGroups(list) {
36
+ try {
37
+ const { mkdir, writeFile } = await import('fs/promises');
38
+ await mkdir(path.dirname(groupsFile()), { recursive: true });
39
+ await writeFile(groupsFile(), JSON.stringify(list, null, 2), 'utf-8');
40
+ }
41
+ catch { /* 持久化失败静默 */ }
42
+ }
43
+ // ============ 链接解析 ============
44
+ /** 解析群组链接: orbitdb://<addr>?type=group&name=<群名> (type 非 group 返回 null) */
45
+ export function parseGroupLink(link) {
46
+ const l = String(link || '').trim();
47
+ if (!l.startsWith('orbitdb://'))
48
+ return null;
49
+ const [base, query] = l.split('?');
50
+ let params = null;
51
+ try {
52
+ params = query ? new URLSearchParams(query) : null;
53
+ }
54
+ catch { /* 忽略 */ }
55
+ if (params && params.get('type') === 'group') {
56
+ let address = base.slice('orbitdb://'.length);
57
+ if (!address.startsWith('/'))
58
+ address = `/${address}`;
59
+ return { address, name: params.get('name') || undefined };
60
+ }
61
+ return null;
62
+ }
63
+ /** 从消息文本检测群组链接 */
64
+ export function detectGroupLink(text) {
65
+ const t = String(text || '');
66
+ const re = /(orbitdb:\/\/\/?orbitdb\/[^\s)'"<>,。;]*\?[^\s)'"<>,。;]*type=group[^\s)'"<>,。;]*)/;
67
+ const m = re.exec(t);
68
+ return m ? m[1].trim() : null;
69
+ }
70
+ // ============ store 缓存 (避免重复 open) ============
71
+ const storeCache = new Map();
72
+ /** 订阅回调注册 (server 层挂 SSE 广播) */
73
+ const onChangeCallbacks = new Map();
74
+ function groupIdOf(address) {
75
+ // /orbitdb/zdpu... → zdpu... (地址后 12 位做短 id)
76
+ const m = /\/orbitdb\/(.{8,})/.exec(address);
77
+ return m ? m[1] : address;
78
+ }
79
+ /** 打开群组 store (缓存) — 可写 (replica=false, write:'*') */
80
+ async function openGroupStore(address) {
81
+ const id = groupIdOf(address);
82
+ if (storeCache.has(id))
83
+ return storeCache.get(id);
84
+ const db = getDb();
85
+ const store = await db.openStoreByAddress(address, 'events', {
86
+ replica: false,
87
+ accessController: { write: ['*'] },
88
+ });
89
+ if (!store)
90
+ return null;
91
+ storeCache.set(id, store);
92
+ // 订阅: 新消息 → 通知回调 (server SSE)
93
+ store.onChange(() => {
94
+ groupMessages(id, 5).then((msgs) => {
95
+ if (msgs.length === 0)
96
+ return;
97
+ const cb = onChangeCallbacks.get(id);
98
+ if (cb)
99
+ for (const fn of cb) {
100
+ try {
101
+ fn(msgs[msgs.length - 1]);
102
+ }
103
+ catch { /* 忽略 */ }
104
+ }
105
+ }).catch(() => { });
106
+ });
107
+ return store;
108
+ }
109
+ /** 注册群组消息回调 (返回退订函数) */
110
+ export function onGroupMessage(groupId, fn) {
111
+ if (!onChangeCallbacks.has(groupId))
112
+ onChangeCallbacks.set(groupId, new Set());
113
+ onChangeCallbacks.get(groupId).add(fn);
114
+ return () => { onChangeCallbacks.get(groupId)?.delete(fn); };
115
+ }
116
+ // ============ 群组操作 ============
117
+ /**
118
+ * 创建群组: 新 events store (write:'*') + 持久化 + 生成邀请链接.
119
+ */
120
+ export async function createGroup(name, opts) {
121
+ const groupName = String(name || '').trim() || `group-${Date.now().toString(36).slice(-4)}`;
122
+ try {
123
+ const db = getDb();
124
+ const store = await db.openStore(`bolloon-gw-group-${groupName}`, 'events', {
125
+ accessController: { write: ['*'] },
126
+ });
127
+ const address = store.address;
128
+ const id = groupIdOf(address);
129
+ const link = `orbitdb://${address}?type=group&name=${encodeURIComponent(groupName)}`;
130
+ const info = {
131
+ id, name: groupName, address, link,
132
+ createdAt: new Date().toISOString(),
133
+ lastSyncAt: new Date().toISOString(),
134
+ };
135
+ storeCache.set(id, store);
136
+ // 欢迎消息 (群主自我介绍)
137
+ const from = opts?.from || 'group-owner';
138
+ await store.add({ from, text: opts?.hello || `📢 群主创建了群「${groupName}」, 分享链接邀请成员加入`, ts: Date.now() });
139
+ const groups = await loadGroups();
140
+ await saveGroups([...groups.filter((g) => g.id !== id), info]);
141
+ return { ok: true, group: info };
142
+ }
143
+ catch (e) {
144
+ return { ok: false, error: `创建群组失败: ${String(e?.message || e).slice(0, 160)}` };
145
+ }
146
+ }
147
+ /**
148
+ * 通过链接加入群组: 打开 store (可写) + 持久化 + 幂等.
149
+ * 失败静默返回错误, 不影响本地.
150
+ */
151
+ export async function joinGroup(link) {
152
+ const parsed = parseGroupLink(link);
153
+ if (!parsed) {
154
+ return { ok: false, error: '不是群组链接 (需要 orbitdb://...?type=group&name=...)' };
155
+ }
156
+ // 幂等: 按地址去重
157
+ const existing = await loadGroups();
158
+ const id = groupIdOf(parsed.address);
159
+ if (existing.some((g) => g.id === id)) {
160
+ return { ok: true, already: true, group: existing.find((g) => g.id === id) };
161
+ }
162
+ const store = await openGroupStore(parsed.address);
163
+ if (!store) {
164
+ return { ok: false, error: '群组 store 不可达 (群主节点需在线), 稍后重试或让群主分享最新链接' };
165
+ }
166
+ const name = parsed.name || id.slice(0, 12);
167
+ const info = {
168
+ id, name, address: parsed.address,
169
+ link: `orbitdb://${parsed.address}?type=group&name=${encodeURIComponent(name)}`,
170
+ createdAt: new Date().toISOString(),
171
+ lastSyncAt: new Date().toISOString(),
172
+ };
173
+ await saveGroups([...existing, info]);
174
+ return { ok: true, group: info };
175
+ }
176
+ /** 列出已加入的群组 */
177
+ export async function listGroups() {
178
+ return loadGroups();
179
+ }
180
+ /** 获取群组 store 的最新消息 (ts 升序, 取最后 N 条) */
181
+ export async function groupMessages(groupId, limit = 50) {
182
+ let store = storeCache.get(groupId) ?? null;
183
+ if (!store) {
184
+ const groups = await loadGroups();
185
+ const g = groups.find((x) => x.id === groupId);
186
+ if (!g)
187
+ return [];
188
+ store = await openGroupStore(g.address);
189
+ if (!store)
190
+ return [];
191
+ }
192
+ const all = await store.all().catch(() => []);
193
+ const msgs = [];
194
+ for (const entry of all) {
195
+ const v = entry.value;
196
+ if (v && typeof v.text === 'string' && typeof v.from === 'string') {
197
+ msgs.push({ from: v.from, text: v.text, ts: typeof v.ts === 'number' ? v.ts : 0 });
198
+ }
199
+ }
200
+ msgs.sort((a, b) => a.ts - b.ts);
201
+ return msgs.slice(-limit);
202
+ }
203
+ /** 群成员: 从消息里提取 from 去重 */
204
+ export async function groupMembers(groupId) {
205
+ const msgs = await groupMessages(groupId, 500);
206
+ return Array.from(new Set(msgs.map((m) => m.from)));
207
+ }
208
+ /** 发送群消息 (广播给所有成员) */
209
+ export async function groupSend(groupId, text, from) {
210
+ const msg = String(text || '').trim();
211
+ if (!msg)
212
+ return { ok: false, error: '消息不能为空' };
213
+ let store = storeCache.get(groupId) ?? null;
214
+ if (!store) {
215
+ const groups = await loadGroups();
216
+ const g = groups.find((x) => x.id === groupId);
217
+ if (!g)
218
+ return { ok: false, error: '群组不存在 (先 joinGroup)' };
219
+ store = await openGroupStore(g.address);
220
+ if (!store)
221
+ return { ok: false, error: '群组 store 不可达' };
222
+ }
223
+ try {
224
+ await store.add({ from: String(from || 'anonymous'), text: msg, ts: Date.now() });
225
+ return { ok: true };
226
+ }
227
+ catch (e) {
228
+ return { ok: false, error: `发送失败: ${String(e?.message || e).slice(0, 160)}` };
229
+ }
230
+ }
231
+ /** 群组邀请链接 */
232
+ export async function groupLink(groupId) {
233
+ const groups = await loadGroups();
234
+ return groups.find((g) => g.id === groupId)?.link ?? null;
235
+ }
236
+ /** 群组信息 (含成员数/消息数) */
237
+ export async function groupInfo(groupId) {
238
+ const groups = await loadGroups();
239
+ const g = groups.find((x) => x.id === groupId);
240
+ if (!g)
241
+ return null;
242
+ const msgs = await groupMessages(groupId, 500);
243
+ const members = await groupMembers(groupId);
244
+ return { ...g, messageCount: msgs.length, memberCount: members.length };
245
+ }
246
+ /** 重启恢复: 重开所有已加入群组的 store (失败静默) */
247
+ export async function restoreGroups() {
248
+ const groups = await loadGroups();
249
+ if (groups.length === 0)
250
+ return { restored: 0, failed: 0, total: 0 };
251
+ let restored = 0;
252
+ let failed = 0;
253
+ for (const g of groups) {
254
+ const store = await openGroupStore(g.address).catch(() => null);
255
+ if (store)
256
+ restored++;
257
+ else
258
+ failed++;
259
+ }
260
+ return { restored, failed, total: groups.length };
261
+ }