@bolloon/bolloon-agent 0.1.23 → 0.1.24

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.
@@ -1,5 +1,6 @@
1
1
  import { Endpoint, Connection } from '@rayhanadev/iroh';
2
2
  import * as crypto from 'crypto';
3
+ import { loadOrCreateIrohSecret } from '../agents/iroh-secret.js';
3
4
 
4
5
  export interface IrohMessage {
5
6
  type: string;
@@ -91,6 +92,19 @@ export class IrohTransport {
91
92
  };
92
93
  }
93
94
 
95
+ // 若调用方未传 secretKey,从 ~/.bolloon/iroh-secret-default.json 落盘/读取
96
+ // 保证 iroh nodeId 在同一台机器上跨重启保持稳定
97
+ if (!secretKey) {
98
+ try {
99
+ const sec = loadOrCreateIrohSecret('default');
100
+ // iroh binding 的 secretKey 字段是 hex 字符串 (32 字节 Ed25519 种子)
101
+ secretKey = Buffer.from(sec.secretKey).toString('hex');
102
+ console.log(`[IrohTransport] ${sec.reused ? '复用' : '新建'} iroh-secret-default.json (createdAt=${sec.createdAt})`);
103
+ } catch (e) {
104
+ console.warn('[IrohTransport] iroh-secret 加载失败, 将使用临时身份:', e);
105
+ }
106
+ }
107
+
94
108
  const options: any = { alpns: [IROH_ALPN] };
95
109
  if (secretKey) {
96
110
  options.secretKey = secretKey;
@@ -363,12 +363,22 @@ export async function checkAndUpdate(): Promise<{
363
363
  updated: boolean;
364
364
  message: string;
365
365
  }> {
366
- // 检查是否有 --no-update 标志
366
+ // opt-in: 默认跳过更新检查. 想允许必须显式提供以下任一标志:
367
+ // --update-check, --update-now, --allow-update
368
+ // BOLLOON_AUTO_UPDATE=1
369
+ const allowFlag = process.argv.includes('--update-check')
370
+ || process.argv.includes('--update-now')
371
+ || process.argv.includes('--allow-update');
372
+ const allowEnv = process.env.BOLLOON_AUTO_UPDATE === '1';
373
+ if (!allowFlag && !allowEnv) {
374
+ return { hasUpdate: false, info: null, updated: false, message: '跳过更新检查 (默认关闭, 用 --update-check 显式触发)' };
375
+ }
376
+
377
+ // 显式屏蔽仍然优先
367
378
  if (process.argv.includes('--no-update') || process.argv.includes('--skip-update')) {
368
379
  return { hasUpdate: false, info: null, updated: false, message: '跳过更新检查' };
369
380
  }
370
381
 
371
- // 检查环境变量
372
382
  if (process.env.BOLLOON_SKIP_UPDATE === 'true') {
373
383
  return { hasUpdate: false, info: null, updated: false, message: '跳过更新检查(环境变量)' };
374
384
  }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * agent-delegate-server - 把 manifest + agent_delegate 协议挂到 Web API
3
+ *
4
+ * 启动: PORT=54189 npx tsx src/web/agent-delegate-server.ts
5
+ *
6
+ * 提供:
7
+ * GET /api/agent/local-manifest — 本节点智能体清单
8
+ * POST /api/agent/register — 注册/更新本节点智能体
9
+ * GET /api/agent/remote-manifests — 缓存的远端 manifest 列表
10
+ * POST /api/agent/pick — 按 capability + 可选 ownerPublicKey 选 agent
11
+ * POST /api/agent/delegate — DOC 驱动委派 (转发到对端 agent)
12
+ *
13
+ * 该模块不直接绑 Hyperswarm — 通过注入的 transport 抽象:
14
+ * - sendToNode(nodeId, frame) -> Promise<responseFrame>
15
+ *
16
+ * 主进程接入时把 Hyperswarm 拨号逻辑包成 transport 注入。
17
+ */
18
+
19
+ import express from 'express';
20
+ import {
21
+ buildAgentDelegateRequest, buildAgentResponse, buildManifestPayload, buildManifestRequest,
22
+ parseFrame, setLocalManifest, addLocalAgent, getLocalManifest, getRemoteManifests,
23
+ cacheRemoteManifest, pickAgent, type AgentManifestEntry, type AgentManifest,
24
+ } from '../agents/agent-manifest-protocol.js';
25
+
26
+ export interface DelegateTransport {
27
+ /** 发送 frame 到指定节点公钥, 等待回包. null 表示不实现 (同步返回占位) */
28
+ sendToNode(publicKey: string, frame: string, timeoutMs?: number): Promise<string | null>;
29
+ /** 注册 onMessage: 对方 manifest_request 来了, 我方要回 manifest_payload */
30
+ onIncomingFrame(handler: (fromPublicKey: string, frame: string) => Promise<string | null>): void;
31
+ }
32
+
33
+ export function createAgentDelegateApp(transport: DelegateTransport): express.Express {
34
+ const app = express();
35
+ app.use(express.json({ limit: '2mb' }));
36
+
37
+ // ---- 本地 manifest ----
38
+ app.get('/api/agent/local-manifest', (_req, res) => {
39
+ res.json(getLocalManifest());
40
+ });
41
+
42
+ app.post('/api/agent/register', (req, res) => {
43
+ const body = req.body as { agents: AgentManifestEntry[]; ownerName?: string; ownerPublicKey?: string };
44
+ if (!body.agents || !Array.isArray(body.agents)) {
45
+ return res.status(400).json({ error: 'agents array required' });
46
+ }
47
+ setLocalManifest({
48
+ ownerName: body.ownerName || getLocalManifest().ownerName || 'unknown',
49
+ ownerPublicKey: body.ownerPublicKey || getLocalManifest().ownerPublicKey || '',
50
+ agents: body.agents,
51
+ });
52
+ res.json({ ok: true, manifest: getLocalManifest() });
53
+ });
54
+
55
+ // ---- 远端 manifest 列表 ----
56
+ app.get('/api/agent/remote-manifests', (_req, res) => {
57
+ res.json({ count: getRemoteManifests().length, manifests: getRemoteManifests() });
58
+ });
59
+
60
+ // ---- 按 capability 选 agent ----
61
+ app.post('/api/agent/pick', (req, res) => {
62
+ const { capability, ownerPublicKey } = req.body as { capability: string; ownerPublicKey?: string };
63
+ if (!capability) return res.status(400).json({ error: 'capability required' });
64
+ const picked = pickAgent(capability, ownerPublicKey);
65
+ if (!picked) return res.status(404).json({ error: 'no matching agent', capability });
66
+ res.json({ ok: true, agent: picked.agent, owner: { name: picked.owner.ownerName, publicKey: picked.owner.ownerPublicKey } });
67
+ });
68
+
69
+ // ---- DOC 驱动委派 ----
70
+ app.post('/api/agent/delegate', async (req, res) => {
71
+ try {
72
+ const { toPublicKey, capability, docPath, docContent, instruction, fromAgentId } = req.body as {
73
+ toPublicKey: string;
74
+ capability: string;
75
+ docPath?: string;
76
+ docContent?: string;
77
+ instruction: string;
78
+ fromAgentId?: string;
79
+ };
80
+ if (!toPublicKey || !capability || !instruction) {
81
+ return res.status(400).json({ error: 'toPublicKey, capability, instruction required' });
82
+ }
83
+
84
+ // 1) 优先从已缓存的远端 manifest 里选 agent
85
+ let targetAgent: AgentManifestEntry | null = null;
86
+ const remote = getRemoteManifests().find((m) => m.ownerPublicKey === toPublicKey || m.ownerPublicKey.startsWith(toPublicKey.substring(0, 16)));
87
+ if (remote) {
88
+ targetAgent = remote.agents.find((a) => a.capabilities.includes(capability) && a.status === 'active') || null;
89
+ }
90
+
91
+ // 2) 构造 frame, 通过 transport 发送
92
+ const frame = buildAgentDelegateRequest({
93
+ capability,
94
+ docPath,
95
+ docContent,
96
+ instruction,
97
+ fromAgentId: fromAgentId || 'local-user',
98
+ });
99
+
100
+ const replyFrame = await transport.sendToNode(toPublicKey, frame, 30000);
101
+ if (!replyFrame) {
102
+ return res.status(504).json({ error: 'no response from peer (timeout or transport not wired)' });
103
+ }
104
+ const f = parseFrame(replyFrame);
105
+ if (!f || f.type !== 'agent_response') {
106
+ return res.status(502).json({ error: 'bad response', frame: replyFrame });
107
+ }
108
+ res.json({
109
+ ok: true,
110
+ targetAgent: targetAgent || { id: f.payload.delegatedTo, capabilities: [capability], status: 'active', name: f.payload.delegatedTo },
111
+ response: f.payload,
112
+ });
113
+ } catch (e: any) {
114
+ res.status(500).json({ error: e.message });
115
+ }
116
+ });
117
+
118
+ // ---- 接收对方 manifest_request / agent_delegate 处理 ----
119
+ // 业务端需要把对端来的入站 frame 转给本 handler
120
+ // 简化: 把 handler 挂到 transport.onIncomingFrame
121
+ transport.onIncomingFrame(async (fromPublicKey, frame) => {
122
+ const f = parseFrame(frame);
123
+ if (!f) return null;
124
+ if (f.type === 'manifest_request') {
125
+ return buildManifestPayload(getLocalManifest());
126
+ }
127
+ if (f.type === 'manifest_payload') {
128
+ cacheRemoteManifest(f.payload as AgentManifest);
129
+ return null; // 不需要回包
130
+ }
131
+ if (f.type === 'agent_delegate') {
132
+ // 路由到本地匹配 agent
133
+ const req = f.payload as any;
134
+ const local = getLocalManifest();
135
+ const target = local.agents.find((a) => a.capabilities.includes(req.capability) && a.status === 'active') || local.agents[0];
136
+ if (!target) return buildAgentResponse({ ok: false, delegatedTo: 'none', summary: 'no local agent available' });
137
+ return buildAgentResponse({
138
+ ok: true,
139
+ delegatedTo: target.id,
140
+ resultCid: `mock-${Date.now()}`,
141
+ summary: `[${target.name}] 已处理任务: ${req.instruction?.substring(0, 30)}`,
142
+ });
143
+ }
144
+ return null;
145
+ });
146
+
147
+ return app;
148
+ }