@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,278 @@
1
+ /**
2
+ * gateway-network.ts — Agent 网络加入 (2026-08-14 v2)
3
+ *
4
+ * joinNetwork(link): 通过链接自动加入共享 Agent 网络.
5
+ * 链接形式 (三选一):
6
+ * - orbitdb://<storeAddress> 共享 registry store (主链路, OrbitDB 复制)
7
+ * - ipns://<name> IPNS 标识 (registry 静态快照)
8
+ * - https://.../registry HTTP 端点 (远程 registry JSON)
9
+ *
10
+ * v2 新增:
11
+ * - orbitdb:// 真实可开 (CIDDatabase.openStoreByAddress, replica 只读)
12
+ * - 成员身份持久化 ~/.bolloon/gateway-networks.json (重启后仍是家庭成员)
13
+ * - detectGatewayLink / maybeAutoJoinGateway — 消息里收到链接自动加入 (入口要小)
14
+ * - shareNetworkLink — 生成本机可分享的网络链接 (把 registry 发出去)
15
+ */
16
+ import * as os from 'os';
17
+ import * as path from 'path';
18
+ import { getAgentRegistry, warmAgentRegistry } from './agent-registry.js';
19
+ /** 解析链接字符串 → ParsedLink (剥离 ?name= query) */
20
+ export function parseNetworkLink(link) {
21
+ const l = String(link || '').trim();
22
+ if (!l)
23
+ return null;
24
+ const [base, query] = l.split('?');
25
+ let networkName;
26
+ try {
27
+ networkName = query ? (new URLSearchParams(query).get('name') || undefined) : undefined;
28
+ }
29
+ catch { /* 忽略坏 query */ }
30
+ if (base.startsWith('ipns://'))
31
+ return { kind: 'ipns', name: base.slice('ipns://'.length), networkName };
32
+ if (base.startsWith('orbitdb://')) {
33
+ // orbitdb:///orbitdb/<addr> 或 orbitdb://orbitdb/<addr> → /orbitdb/<addr>
34
+ let address = base.slice('orbitdb://'.length);
35
+ if (!address.startsWith('/'))
36
+ address = `/${address}`;
37
+ return { kind: 'orbitdb', address, networkName };
38
+ }
39
+ if (base.startsWith('http://') || base.startsWith('https://'))
40
+ return { kind: 'http', url: base, networkName };
41
+ return null;
42
+ }
43
+ /** 从消息文本里检测 gateway 链接 (自动加入触发器用) */
44
+ export function detectGatewayLink(text) {
45
+ const t = String(text || '');
46
+ // orbitdb:// 地址含 '/' 必须贪婪匹配到空白/引号; https 允许 /registry 后带 ?query
47
+ const re = /(orbitdb:\/\/\/?orbitdb\/[^\s)'"<>,。;]+|ipns:\/\/[^\s)'"<>,。;]+|https?:\/\/[^\s)'"<>,。;]*\/registry(?:\?[^\s)'"<>,。;]*)?)/;
48
+ const m = re.exec(t);
49
+ return m ? m[1].trim() : null;
50
+ }
51
+ // ============ 远端拉取 ============
52
+ /** 从 HTTP 端点拉取远端 registry */
53
+ async function fetchRemoteRegistry(url) {
54
+ try {
55
+ const r = await fetch(url, { signal: AbortSignal.timeout(15000) });
56
+ if (!r.ok)
57
+ return null;
58
+ const data = await r.json();
59
+ // 兼容: {services:[...]} 或 [...]
60
+ const services = Array.isArray(data) ? data : (data.services ?? null);
61
+ return Array.isArray(services) ? services : null;
62
+ }
63
+ catch {
64
+ return null;
65
+ }
66
+ }
67
+ /** 从 IPNS 拉取 registry (需本地 Kubo + 8080 gateway) */
68
+ async function fetchIpnsRegistry(name) {
69
+ try {
70
+ const r = await fetch(`http://127.0.0.1:8080/ipns/${name}/registry.json`, { signal: AbortSignal.timeout(15000) });
71
+ if (!r.ok)
72
+ return null;
73
+ const data = await r.json();
74
+ return Array.isArray(data) ? data : (data.services ?? null);
75
+ }
76
+ catch {
77
+ return null;
78
+ }
79
+ }
80
+ /** 从 OrbitDB 打开共享 registry store (replica 只读) */
81
+ async function fetchOrbitdbRegistry(address) {
82
+ try {
83
+ const { getCIDDatabase } = await import('../orbitdb/cid-database.js');
84
+ const db = getCIDDatabase();
85
+ const store = await db.openStoreByAddress(address, 'keyvalue');
86
+ if (!store)
87
+ return null;
88
+ // 先立即读 (同节点 store 已有数据); 没有再等复制 (owner 在线时 pubsub 复制通常 <1s)
89
+ const first = await store.get('services').catch(() => null);
90
+ if (Array.isArray(first))
91
+ return first;
92
+ await new Promise((resolve) => {
93
+ let done = false;
94
+ const finish = () => { if (!done) {
95
+ done = true;
96
+ try {
97
+ off();
98
+ }
99
+ catch { }
100
+ resolve();
101
+ } };
102
+ const off = store.onChange(finish);
103
+ setTimeout(finish, 4000);
104
+ });
105
+ // 单键 'services' (registry 的 OrbitDB 布局: 整个列表存单键)
106
+ const v = await store.get('services').catch(() => null);
107
+ if (Array.isArray(v))
108
+ return v;
109
+ // 兼容: 遍历 entries 找数组值
110
+ const all = await store.all().catch(() => null);
111
+ if (Array.isArray(all)) {
112
+ for (const entry of all) {
113
+ if (Array.isArray(entry.value))
114
+ return entry.value;
115
+ const val = entry.value;
116
+ if (val?.services && Array.isArray(val.services))
117
+ return val.services;
118
+ }
119
+ }
120
+ return null;
121
+ }
122
+ catch {
123
+ return null;
124
+ }
125
+ }
126
+ const networksFile = () => path.join(os.homedir() || '/tmp', '.bolloon', 'gateway-networks.json');
127
+ async function loadNetworks() {
128
+ try {
129
+ const { readFile } = await import('fs/promises');
130
+ const parsed = JSON.parse(await readFile(networksFile(), 'utf-8'));
131
+ return Array.isArray(parsed) ? parsed : [];
132
+ }
133
+ catch {
134
+ return [];
135
+ }
136
+ }
137
+ async function saveNetworks(list) {
138
+ try {
139
+ const { mkdir, writeFile } = await import('fs/promises');
140
+ await mkdir(path.dirname(networksFile()), { recursive: true });
141
+ await writeFile(networksFile(), JSON.stringify(list, null, 2), 'utf-8');
142
+ }
143
+ catch { /* 持久化失败静默 */ }
144
+ }
145
+ /** 列出已加入的网络 */
146
+ export async function listJoinedNetworks() {
147
+ return loadNetworks();
148
+ }
149
+ /**
150
+ * 通过链接加入共享网络: 拉取远端服务列表 → 合并到本地 registry (按 agentId+service 去重).
151
+ * 幂等: 已加入的网络直接返回 already=true.
152
+ * deps.registry 可注入 (测试用), 默认单例.
153
+ */
154
+ export async function joinNetwork(link, deps) {
155
+ const parsed = parseNetworkLink(link);
156
+ if (!parsed) {
157
+ return { ok: false, joined: 0, total: 0, error: '无法解析链接 (支持 orbitdb:// / ipns:// / https://)' };
158
+ }
159
+ // 幂等: 已加入 (按解析后的网络身份 key 去重, 忽略 ?name 差异)
160
+ const existing = await loadNetworks();
161
+ const norm = String(link).trim();
162
+ const identityKey = parsed.kind === 'orbitdb' ? `orbitdb:${parsed.address}`
163
+ : parsed.kind === 'ipns' ? `ipns:${parsed.name}`
164
+ : `http:${parsed.url}`;
165
+ if (existing.some((n) => n.linkKey === identityKey || n.link === norm)) {
166
+ return { ok: true, joined: 0, total: 0, already: true, linkKind: parsed.kind, networkName: parsed.networkName };
167
+ }
168
+ // 拉取远端服务
169
+ let remote = null;
170
+ if (parsed.kind === 'http') {
171
+ remote = await fetchRemoteRegistry(parsed.url);
172
+ }
173
+ else if (parsed.kind === 'ipns') {
174
+ remote = await fetchIpnsRegistry(parsed.name);
175
+ }
176
+ else if (parsed.kind === 'orbitdb') {
177
+ remote = await fetchOrbitdbRegistry(parsed.address);
178
+ }
179
+ if (!remote || remote.length === 0) {
180
+ return { ok: false, joined: 0, total: 0, error: '远端网络无服务或不可达 (store owner 需在线)', linkKind: parsed.kind };
181
+ }
182
+ // 合并到本地 registry (按 agentId + service.name 去重)
183
+ const registry = deps?.registry ?? getAgentRegistry();
184
+ const local = await registry.list();
185
+ let joined = 0;
186
+ for (const svc of remote) {
187
+ if (!svc?.agentId || !svc?.service?.name)
188
+ continue;
189
+ const exists = local.some((l) => l.agentId === svc.agentId && l.service?.name === svc.service?.name);
190
+ if (!exists) {
191
+ await registry.register(svc).catch(() => { });
192
+ joined++;
193
+ }
194
+ }
195
+ // 记录成员身份 (持久化 → 重启后自动恢复)
196
+ await saveNetworks([
197
+ ...existing,
198
+ {
199
+ link: norm,
200
+ linkKey: identityKey,
201
+ kind: parsed.kind,
202
+ name: parsed.networkName,
203
+ joinedAt: new Date().toISOString(),
204
+ serviceCount: remote.length,
205
+ lastSyncAt: new Date().toISOString(),
206
+ },
207
+ ]);
208
+ return { ok: true, joined, total: remote.length, linkKind: parsed.kind, networkName: parsed.networkName };
209
+ }
210
+ /** 启动恢复: 重拉所有已加入网络 (失败静默, 保留记录). 返回恢复统计. */
211
+ export async function restoreJoinedNetworks() {
212
+ const nets = await loadNetworks();
213
+ if (nets.length === 0)
214
+ return { restored: 0, failed: 0, total: 0 };
215
+ let restored = 0;
216
+ let failed = 0;
217
+ for (const n of nets) {
218
+ const r = await joinNetwork(n.link).catch(() => null);
219
+ if (r?.ok)
220
+ restored++;
221
+ else
222
+ failed++;
223
+ // 更新 lastSyncAt
224
+ if (r?.ok) {
225
+ const updated = (await loadNetworks()).map((m) => m.link === n.link ? { ...m, lastSyncAt: new Date().toISOString(), serviceCount: r.total || m.serviceCount } : m);
226
+ await saveNetworks(updated);
227
+ }
228
+ }
229
+ return { restored, failed, total: nets.length };
230
+ }
231
+ // ============ 分享链接 ============
232
+ /**
233
+ * 生成本机可分享的网络链接: orbitdb://<registry storeAddress>?name=<网络名>.
234
+ * 对方收到链接 → 自动 joinNetwork → 拉取本机注册的服务.
235
+ * opts.registry 可注入 (测试用), 默认单例.
236
+ */
237
+ export async function shareNetworkLink(opts) {
238
+ try {
239
+ let registry = opts?.registry;
240
+ if (!registry) {
241
+ await warmAgentRegistry();
242
+ registry = getAgentRegistry();
243
+ }
244
+ if (!registry.ready || !registry.storeAddress) {
245
+ return { ok: false, error: 'OrbitDB registry 未就绪 (离线模式). 备选: 把 registry 列表发布成 https://.../registry 端点分享' };
246
+ }
247
+ const name = encodeURIComponent(String(opts?.name || registry.storeName || 'bolloon-network'));
248
+ return { ok: true, link: `orbitdb://${registry.storeAddress}?name=${name}` };
249
+ }
250
+ catch (e) {
251
+ return { ok: false, error: `生成分享链接失败: ${String(e?.message || e).slice(0, 160)}` };
252
+ }
253
+ }
254
+ // ============ 自动加入 (消息触发) ============
255
+ /**
256
+ * 自动加入入口: 文本里检测到 gateway 链接 → 幂等 joinNetwork.
257
+ * 返回给 agent 的通知字符串 (无链接 / 已在网络 → null, 静默).
258
+ * 设计: 加入是自由的 (只拉服务列表, 不花钱), 支付仍走 payment-gate 安全链.
259
+ * deps.registry 可注入 (测试用), 默认单例.
260
+ */
261
+ export async function maybeAutoJoinGateway(text, deps) {
262
+ const link = detectGatewayLink(text);
263
+ if (!link)
264
+ return null;
265
+ try {
266
+ const r = await joinNetwork(link, deps);
267
+ if (r.ok && r.already)
268
+ return null; // 已在网络, 静默
269
+ if (r.ok) {
270
+ const netName = r.networkName ? `「${r.networkName}」` : ''; // URLSearchParams 已 decode
271
+ return `🆕 已自动加入 Agent 网络${netName} (${r.linkKind}): 拉取 ${r.total} 个服务, 新增 ${r.joined} 个。用 gateway_status 查看, gateway_call 调用网络里的服务。`;
272
+ }
273
+ return `⚠️ 检测到 Agent 网络链接 (${r.linkKind || 'unknown'}) 但加入失败: ${r.error}`;
274
+ }
275
+ catch (e) {
276
+ return `⚠️ 自动加入 Agent 网络失败: ${String(e?.message || e).slice(0, 160)}`;
277
+ }
278
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * payment-approval.ts — 人工支付审批 (2026-08-13)
3
+ *
4
+ * YAML 支付验证门 (payment-gate) 判定 confirm 的支付请求 → 进入人工审批流程:
5
+ * - createApproval: 创建 pending 审批请求 (持久化)
6
+ * - approve(id): 人工批准 → 自动重试支付 (executor)
7
+ * - reject(id): 人工拒绝
8
+ * - list/pending: 查询
9
+ *
10
+ * 持久化: ~/.bolloon/payment-approvals.json
11
+ * 审批后执行: 注入 executor (serviceCall 重试 / 链上支付), 由调用方提供.
12
+ */
13
+ import * as fs from 'fs/promises';
14
+ import * as os from 'os';
15
+ import * as path from 'path';
16
+ const home = () => process.env.HOME || os.homedir() || '/tmp';
17
+ let _executor = null;
18
+ /** 注入批准后执行器 (serviceCall 重试 / 链上支付) */
19
+ export function setApprovalExecutor(fn) {
20
+ _executor = fn;
21
+ }
22
+ export class PaymentApprovalStore {
23
+ file;
24
+ approvals = [];
25
+ constructor(file = path.join(home(), '.bolloon', 'payment-approvals.json')) {
26
+ this.file = file;
27
+ }
28
+ async load() {
29
+ try {
30
+ const raw = JSON.parse(await fs.readFile(this.file, 'utf-8'));
31
+ if (Array.isArray(raw))
32
+ this.approvals = raw;
33
+ }
34
+ catch {
35
+ this.approvals = [];
36
+ }
37
+ }
38
+ async persist() {
39
+ try {
40
+ await fs.mkdir(path.dirname(this.file), { recursive: true });
41
+ await fs.writeFile(this.file, JSON.stringify(this.approvals, null, 2), 'utf-8');
42
+ }
43
+ catch { /* 静默 */ }
44
+ }
45
+ /** 创建审批请求 (pending) */
46
+ async create(req) {
47
+ await this.load();
48
+ const approval = {
49
+ id: `pay-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
50
+ service: req.service,
51
+ amount: req.amount,
52
+ recipient: req.recipient,
53
+ reason: req.reason,
54
+ status: 'pending',
55
+ createdAt: Date.now(),
56
+ retryPayload: req.retryPayload,
57
+ };
58
+ this.approvals.push(approval);
59
+ await this.persist();
60
+ return approval;
61
+ }
62
+ /** 待审批列表 */
63
+ async pending() {
64
+ await this.load();
65
+ return this.approvals.filter((a) => a.status === 'pending');
66
+ }
67
+ /** 全部 (含历史) */
68
+ async list() {
69
+ await this.load();
70
+ return [...this.approvals].reverse();
71
+ }
72
+ async get(id) {
73
+ await this.load();
74
+ return this.approvals.find((a) => a.id === id) ?? null;
75
+ }
76
+ /**
77
+ * 人工批准 → 执行支付 (executor) → executed/failed.
78
+ * 未注入 executor 时直接标记 approved (调用方自行处理).
79
+ */
80
+ async approve(id, approver = 'user') {
81
+ await this.load();
82
+ const a = this.approvals.find((x) => x.id === id);
83
+ if (!a)
84
+ return { ok: false, error: `审批 ${id} 不存在` };
85
+ if (a.status !== 'pending')
86
+ return { ok: false, error: `审批 ${id} 状态 ${a.status}, 不可批准` };
87
+ a.status = 'approved';
88
+ a.decidedAt = Date.now();
89
+ await this.persist();
90
+ // 批准后执行支付
91
+ if (_executor) {
92
+ const r = await _executor(a);
93
+ a.status = r.ok ? 'executed' : 'failed';
94
+ a.result = r.ok ? r.result : r.error;
95
+ await this.persist();
96
+ if (!r.ok)
97
+ return { ok: false, approval: a, error: r.error };
98
+ }
99
+ return { ok: true, approval: a };
100
+ }
101
+ /** 人工拒绝 */
102
+ async reject(id, approver = 'user') {
103
+ await this.load();
104
+ const a = this.approvals.find((x) => x.id === id);
105
+ if (!a)
106
+ return { ok: false, error: `审批 ${id} 不存在` };
107
+ if (a.status !== 'pending')
108
+ return { ok: false, error: `审批 ${id} 状态 ${a.status}, 不可拒绝` };
109
+ a.status = 'rejected';
110
+ a.decidedAt = Date.now();
111
+ await this.persist();
112
+ return { ok: true, approval: a };
113
+ }
114
+ /** 清理过期 pending (超时自动标记 rejected) */
115
+ async expireStale(timeoutMs = 60 * 60 * 1000) {
116
+ await this.load();
117
+ const now = Date.now();
118
+ let expired = 0;
119
+ for (const a of this.approvals) {
120
+ if (a.status === 'pending' && now - a.createdAt > timeoutMs) {
121
+ a.status = 'rejected';
122
+ a.decidedAt = now;
123
+ a.result = '审批超时自动拒绝';
124
+ expired++;
125
+ }
126
+ }
127
+ if (expired > 0)
128
+ await this.persist();
129
+ return expired;
130
+ }
131
+ }
132
+ let _store = null;
133
+ /** 单例 */
134
+ export function getApprovalStore() {
135
+ if (!_store)
136
+ _store = new PaymentApprovalStore();
137
+ return _store;
138
+ }
139
+ export function resetApprovalStore() {
140
+ _store = null;
141
+ _executor = null;
142
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * payment-gate.ts — YAML 驱动的支付验证门 (2026-08-13)
3
+ *
4
+ * 用户要求: 智能体支付不能全部交给 AI, 需要 YAML 验证流程.
5
+ * 设计 (参考 Hermes write_approval 配置驱动 + arXiv:2605.30998):
6
+ * - payment-policy.yaml 声明式规则 (allow/confirm/deny)
7
+ * - 支付请求按规则链逐条匹配 → decision
8
+ * - deny 不可覆盖; confirm 返回 pending 等待人工审批 (不自动执行)
9
+ *
10
+ * 加载: ~/.bolloon/payment-policy.yaml (不存在则用内置默认 payment-policy.yaml)
11
+ */
12
+ import * as fs from 'fs';
13
+ import * as os from 'os';
14
+ import * as path from 'path';
15
+ import * as yaml from 'js-yaml';
16
+ const home = () => process.env.HOME || os.homedir() || '/tmp';
17
+ /** 支付验证门: 加载 YAML 并评估 */
18
+ export class PaymentGate {
19
+ policy = null;
20
+ policyPath;
21
+ constructor(policyPath = path.join(home(), '.bolloon', 'payment-policy.yaml')) {
22
+ this.policyPath = policyPath;
23
+ }
24
+ /** 加载 YAML (本地文件优先, 否则内置默认) */
25
+ load() {
26
+ if (this.policy)
27
+ return this.policy;
28
+ const candidates = [
29
+ this.policyPath,
30
+ path.resolve(process.cwd(), 'src/agents/payment-policy.yaml'),
31
+ ];
32
+ for (const f of candidates) {
33
+ try {
34
+ if (fs.existsSync(f)) {
35
+ this.policy = yaml.load(fs.readFileSync(f, 'utf-8'));
36
+ return this.policy;
37
+ }
38
+ }
39
+ catch { /* 下一个 */ }
40
+ }
41
+ this.policy = { default_action: 'confirm', default_reason: '未命中任何支付规则, 需要人工确认' };
42
+ return this.policy;
43
+ }
44
+ /**
45
+ * 评估支付请求 (按规则链, 顺序匹配第一条命中).
46
+ * 流程: 硬限制 (超限即 deny) → 规则链 (allow/confirm/deny) → 默认.
47
+ */
48
+ evaluate(intent) {
49
+ const p = this.load();
50
+ const service = String(intent.service || '').toLowerCase();
51
+ const recipient = String(intent.recipient || '').toLowerCase();
52
+ const amount = Number(intent.amount) || 0;
53
+ const limits = {
54
+ maxPerTransaction: p.limits?.max_per_transaction ?? 1,
55
+ maxDailyTotal: p.limits?.max_daily_total ?? 10,
56
+ };
57
+ // 0. 硬限制: 超单笔/日限 → deny (无论规则)
58
+ if (amount > limits.maxPerTransaction) {
59
+ return { decision: 'deny', reason: `单笔超限: ${amount} > ${limits.maxPerTransaction}`, requiresApproval: false, limits };
60
+ }
61
+ // 1. 规则链 (顺序匹配)
62
+ for (const rule of p.rules ?? []) {
63
+ const matchesService = !rule.services || rule.services.some((s) => s.toLowerCase() === service);
64
+ const matchesRecipient = !rule.recipients || rule.recipients.some((r) => r.toLowerCase() === recipient);
65
+ const matchesAmount = rule.max_amount === undefined || amount <= rule.max_amount;
66
+ if (matchesService && matchesRecipient && matchesAmount) {
67
+ return {
68
+ decision: rule.action,
69
+ reason: rule.reason || rule.description || `规则 ${rule.id} 命中`,
70
+ ruleId: rule.id,
71
+ requiresApproval: rule.action === 'confirm',
72
+ limits,
73
+ };
74
+ }
75
+ }
76
+ // 2. 默认
77
+ return {
78
+ decision: p.default_action ?? 'confirm',
79
+ reason: p.default_reason || '默认: 需人工确认',
80
+ requiresApproval: (p.default_action ?? 'confirm') === 'confirm',
81
+ limits,
82
+ };
83
+ }
84
+ /** 快捷: 是否允许 (allow = true; confirm/deny = false 且不自动) */
85
+ isAllowed(intent) {
86
+ const v = this.evaluate(intent);
87
+ return { allowed: v.decision === 'allow', verdict: v };
88
+ }
89
+ }
90
+ let _gate = null;
91
+ /** 单例 */
92
+ export function getPaymentGate() {
93
+ if (!_gate)
94
+ _gate = new PaymentGate();
95
+ return _gate;
96
+ }
97
+ export function resetPaymentGate() {
98
+ _gate = null;
99
+ }