libp2p-mesh 2026.5.13 → 2026.5.15

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 (68) hide show
  1. package/README.md +61 -23
  2. package/api.ts +1 -1
  3. package/dist/api.d.ts +1 -1
  4. package/dist/index.d.ts +0 -1
  5. package/dist/index.js +60 -24
  6. package/dist/runtime-setter-api.d.ts +4 -0
  7. package/dist/runtime-setter-api.js +19 -0
  8. package/dist/src/agent-tools.d.ts +63 -24
  9. package/dist/src/agent-tools.js +69 -34
  10. package/dist/src/channel.d.ts +1 -0
  11. package/dist/src/channel.js +20 -4
  12. package/dist/src/dht-registry.d.ts +38 -0
  13. package/dist/src/dht-registry.js +80 -0
  14. package/dist/src/inbound.d.ts +0 -3
  15. package/dist/src/inbound.js +29 -14
  16. package/dist/src/instance-id.d.ts +53 -0
  17. package/dist/src/instance-id.js +156 -0
  18. package/dist/src/mesh.js +310 -23
  19. package/dist/src/plugin.d.ts +1 -2
  20. package/dist/src/plugin.js +18 -30
  21. package/dist/src/types.d.ts +87 -0
  22. package/index.ts +60 -24
  23. package/openclaw.plugin.json +72 -33
  24. package/package.json +20 -8
  25. package/src/agent-tools.ts +69 -35
  26. package/src/channel.ts +25 -4
  27. package/src/dht-registry.ts +105 -0
  28. package/src/inbound.ts +35 -18
  29. package/src/instance-id.ts +221 -0
  30. package/src/mesh.ts +368 -27
  31. package/src/plugin.ts +25 -36
  32. package/src/types.ts +95 -0
  33. package/dist/src/agent-tools-feishu.test.d.ts +0 -1
  34. package/dist/src/agent-tools-feishu.test.js +0 -57
  35. package/dist/src/config-schema.test.d.ts +0 -1
  36. package/dist/src/config-schema.test.js +0 -55
  37. package/dist/src/feishu-channel.d.ts +0 -19
  38. package/dist/src/feishu-channel.js +0 -202
  39. package/dist/src/feishu-channel.test.d.ts +0 -1
  40. package/dist/src/feishu-channel.test.js +0 -166
  41. package/dist/src/feishu-client.d.ts +0 -27
  42. package/dist/src/feishu-client.js +0 -141
  43. package/dist/src/feishu-client.test.d.ts +0 -1
  44. package/dist/src/feishu-client.test.js +0 -271
  45. package/dist/src/feishu-e2e.test.d.ts +0 -1
  46. package/dist/src/feishu-e2e.test.js +0 -69
  47. package/dist/src/feishu-types.d.ts +0 -53
  48. package/dist/src/feishu-types.js +0 -1
  49. package/dist/src/feishu-types.test.d.ts +0 -1
  50. package/dist/src/feishu-types.test.js +0 -108
  51. package/dist/src/inbound-feishu.test.d.ts +0 -1
  52. package/dist/src/inbound-feishu.test.js +0 -70
  53. package/dist/src/index.d.ts +0 -1
  54. package/dist/src/index.js +0 -1
  55. package/dist/src/plugin-registration.test.d.ts +0 -1
  56. package/dist/src/plugin-registration.test.js +0 -42
  57. package/src/agent-tools-feishu.test.ts +0 -68
  58. package/src/config-schema.test.ts +0 -63
  59. package/src/feishu-channel.test.ts +0 -191
  60. package/src/feishu-channel.ts +0 -253
  61. package/src/feishu-client.test.ts +0 -303
  62. package/src/feishu-client.ts +0 -178
  63. package/src/feishu-e2e.test.ts +0 -90
  64. package/src/feishu-types.test.ts +0 -125
  65. package/src/feishu-types.ts +0 -51
  66. package/src/inbound-feishu.test.ts +0 -91
  67. package/src/index.ts +0 -1
  68. package/src/plugin-registration.test.ts +0 -60
package/dist/src/mesh.js CHANGED
@@ -16,14 +16,22 @@ import path from "node:path";
16
16
  import { mdns } from "@libp2p/mdns";
17
17
  import { mplex } from "@libp2p/mplex";
18
18
  import { noise } from "@libp2p/noise";
19
+ import { kadDHT } from "@libp2p/kad-dht";
19
20
  import { createEd25519PeerId, createFromProtobuf, exportToProtobuf, } from "@libp2p/peer-id-factory";
20
21
  import { tcp } from "@libp2p/tcp";
21
22
  import { webSockets } from "@libp2p/websockets";
22
23
  import { bootstrap } from "@libp2p/bootstrap";
24
+ import { identifyService } from "libp2p/identify";
25
+ import { autoNATService } from "libp2p/autonat";
26
+ import { circuitRelayServer, circuitRelayTransport } from "libp2p/circuit-relay";
27
+ import { dcutrService } from "libp2p/dcutr";
28
+ import { uPnPNATService } from "libp2p/upnp-nat";
23
29
  import { encode, decode } from "it-length-prefixed";
24
30
  import { pipe } from "it-pipe";
25
31
  import { createLibp2p } from "libp2p";
26
32
  import { Uint8ArrayList } from "uint8arraylist";
33
+ import { loadOrCreateInstanceIdentity, verifyInstanceSignature, } from "./instance-id.js";
34
+ import { registerPubkey, lookupPubkey } from "./dht-registry.js";
27
35
  const PROTOCOL = "/openclaw-msg/1.0.0";
28
36
  const MAX_SEEN_MESSAGES = 1000;
29
37
  function resolvePeerIdPath(customPath) {
@@ -53,43 +61,138 @@ async function loadOrCreatePeerId(customPath) {
53
61
  export function createMeshNetwork(options) {
54
62
  const config = options.config ?? {};
55
63
  const logger = options.logger;
56
- // Use an object property instead of a bare `let` so all closures share
57
- // the same mutable reference even if the bundler rewrites scopes.
58
64
  const state = {
59
65
  node: null,
66
+ instanceIdentity: null,
67
+ signMessage: null,
68
+ natFlags: {
69
+ identify: false,
70
+ autoNAT: false,
71
+ upnp: false,
72
+ circuitRelay: false,
73
+ circuitRelayServer: false,
74
+ dcutr: false,
75
+ },
60
76
  };
61
77
  const seenMessages = new Set();
62
78
  const messageHandlers = new Set();
63
79
  const topicHandlers = new Map();
80
+ function getDHTService() {
81
+ return state.node?.services?.dht;
82
+ }
64
83
  async function start() {
84
+ // Load or create lightweight BAID-inspired instance identity
85
+ const instanceResult = await loadOrCreateInstanceIdentity({
86
+ name: config.instanceName,
87
+ });
88
+ state.instanceIdentity = instanceResult.identity;
89
+ state.signMessage = instanceResult.signMessage;
90
+ logger?.info?.(`[libp2p-mesh] Instance Identity: ${instanceResult.identity.id}`);
91
+ logger?.info?.(`[libp2p-mesh] Bound to: ${instanceResult.identity.bindingComponents.username}@${instanceResult.identity.bindingComponents.hostname} (${instanceResult.identity.bindingComponents.platform})`);
65
92
  const peerId = await loadOrCreatePeerId(config.peerIdPath);
66
- // Build transports dynamically
67
93
  const transports = [tcp()];
68
94
  if (config.enableWebSocket) {
69
95
  transports.push(webSockets());
70
96
  }
71
- // Build peer discovery dynamically
97
+ // Peer discovery: mDNS for LAN, bootstrap for WAN entry points
72
98
  const peerDiscovery = [];
73
99
  const discoveryMechanism = config.discovery ?? "mdns";
74
100
  if (discoveryMechanism === "mdns") {
75
101
  peerDiscovery.push(mdns({ interval: 1000 }));
76
102
  logger?.info?.("[libp2p-mesh] Using mDNS discovery (LAN)");
77
103
  }
78
- else if (discoveryMechanism === "bootstrap") {
104
+ if (discoveryMechanism === "bootstrap" || discoveryMechanism === "dht") {
79
105
  const bootstrapList = config.bootstrapList ?? [];
80
106
  if (bootstrapList.length > 0) {
81
107
  peerDiscovery.push(bootstrap({ list: bootstrapList }));
82
108
  logger?.info?.(`[libp2p-mesh] Using bootstrap discovery (${bootstrapList.length} node(s))`);
83
109
  }
84
- else {
110
+ else if (discoveryMechanism === "bootstrap") {
85
111
  logger?.warn?.("[libp2p-mesh] discovery=bootstrap but bootstrapList is empty; falling back to mDNS");
86
112
  peerDiscovery.push(mdns({ interval: 1000 }));
87
113
  }
114
+ else {
115
+ logger?.warn?.("[libp2p-mesh] discovery=dht but bootstrapList is empty; DHT may not find peers");
116
+ }
88
117
  }
89
- else if (discoveryMechanism === "dht") {
90
- logger?.warn?.("[libp2p-mesh] DHT discovery is not yet implemented; falling back to mDNS");
91
- peerDiscovery.push(mdns({ interval: 1000 }));
118
+ // Configure DHT for both WAN peer discovery and pubkey registry
119
+ const enableDHT = discoveryMechanism === "dht" || config.enableDHT !== false;
120
+ const services = {};
121
+ if (enableDHT) {
122
+ services.dht = kadDHT({
123
+ protocolPrefix: "/openclaw",
124
+ clientMode: false,
125
+ });
126
+ logger?.info?.("[libp2p-mesh] DHT enabled (protocol: /openclaw/kad/1.0.0)");
127
+ }
128
+ // -------------------------------------------------------------------
129
+ // NAT traversal stack (identify + autonat + upnp + circuit-relay + dcutr)
130
+ // -------------------------------------------------------------------
131
+ const natOn = config.enableNATTraversal !== false;
132
+ const useIdentify = natOn && config.enableIdentify !== false;
133
+ const useAutoNAT = natOn && config.enableAutoNAT !== false;
134
+ const useUPnP = natOn && config.enableUPnP !== false;
135
+ const useRelay = natOn && config.enableCircuitRelay !== false;
136
+ const useRelayServer = natOn && config.enableCircuitRelayServer === true;
137
+ const useDCUtR = natOn && config.enableDCUtR !== false;
138
+ const relayList = config.relayList ?? [];
139
+ const discoverRelays = Math.max(0, config.discoverRelays ?? 0);
140
+ if (useIdentify) {
141
+ services.identify = identifyService();
142
+ state.natFlags.identify = true;
143
+ logger?.info?.("[libp2p-mesh] identify service enabled");
144
+ }
145
+ else if (useAutoNAT || useDCUtR) {
146
+ logger?.warn?.("[libp2p-mesh] enableIdentify=false but AutoNAT/DCUtR rely on identify; they may not function correctly");
147
+ }
148
+ if (useAutoNAT) {
149
+ services.autoNAT = autoNATService();
150
+ state.natFlags.autoNAT = true;
151
+ logger?.info?.("[libp2p-mesh] AutoNAT enabled (will probe reachability)");
152
+ }
153
+ if (useUPnP) {
154
+ services.upnp = uPnPNATService({
155
+ description: `openclaw-libp2p-mesh/${state.instanceIdentity?.bindingComponents?.hostname ?? "node"}`,
156
+ keepAlive: true,
157
+ });
158
+ state.natFlags.upnp = true;
159
+ logger?.info?.("[libp2p-mesh] UPnP NAT port-mapping enabled");
160
+ }
161
+ if (useRelay) {
162
+ transports.push(circuitRelayTransport({
163
+ discoverRelays,
164
+ }));
165
+ state.natFlags.circuitRelay = true;
166
+ logger?.info?.(`[libp2p-mesh] Circuit Relay v2 transport enabled (discoverRelays=${discoverRelays})`);
167
+ }
168
+ if (useRelayServer) {
169
+ services.circuitRelay = circuitRelayServer({
170
+ // Advertise via content-routing only if DHT is up — otherwise the
171
+ // service still runs but won't auto-publish itself.
172
+ advertise: enableDHT,
173
+ });
174
+ state.natFlags.circuitRelayServer = true;
175
+ logger?.info?.(`[libp2p-mesh] Circuit Relay v2 SERVER enabled (advertise=${enableDHT}) — this node will relay traffic for other peers`);
176
+ }
177
+ if (useDCUtR) {
178
+ services.dcutr = dcutrService();
179
+ state.natFlags.dcutr = true;
180
+ logger?.info?.("[libp2p-mesh] DCUtR (hole-punching) enabled");
181
+ }
182
+ // Build the addresses block. listen always honours user config. The
183
+ // circuit-relay transport reserves a slot on each relay we listen on
184
+ // via /p2p-circuit — auto-derive those entries from relayList when the
185
+ // user hasn't already specified them.
186
+ const listenAddrs = [...(config.listenAddrs ?? ["/ip4/0.0.0.0/tcp/0"])];
187
+ if (useRelay) {
188
+ for (const relay of relayList) {
189
+ const circuitListen = relay.endsWith("/p2p-circuit") ? relay : `${relay}/p2p-circuit`;
190
+ if (!listenAddrs.includes(circuitListen)) {
191
+ listenAddrs.push(circuitListen);
192
+ }
193
+ }
92
194
  }
195
+ const announce = config.announceAddrs ?? [];
93
196
  state.node = await createLibp2p({
94
197
  peerId,
95
198
  start: false,
@@ -97,9 +200,17 @@ export function createMeshNetwork(options) {
97
200
  connectionEncryption: [noise()],
98
201
  streamMuxers: [mplex()],
99
202
  addresses: {
100
- listen: config.listenAddrs ?? ["/ip4/0.0.0.0/tcp/0"],
203
+ listen: listenAddrs,
204
+ announce,
101
205
  },
102
206
  peerDiscovery,
207
+ services,
208
+ // Circuit-relay-v2 transport can't bind to a listen address until a
209
+ // relay reservation is established, so allow per-transport listen
210
+ // failures when it's enabled rather than crashing the whole node.
211
+ transportManager: useRelay
212
+ ? { faultTolerance: 1 /* FaultTolerance.NO_FATAL */ }
213
+ : undefined,
103
214
  });
104
215
  state.node.addEventListener("peer:connect", (evt) => {
105
216
  const peerIdStr = evt.detail.toString();
@@ -129,12 +240,51 @@ export function createMeshNetwork(options) {
129
240
  seenMessages.clear();
130
241
  }
131
242
  seenMessages.add(parsed.id);
132
- // Enrich with local timestamp if missing
133
243
  if (!parsed.timestamp) {
134
244
  parsed.timestamp = Date.now();
135
245
  }
136
- logger?.debug?.(`[libp2p-mesh] Received ${parsed.type} from ${parsed.from}`);
137
- // Notify direct message handlers
246
+ // Verify instance identity signature if present
247
+ if (parsed.instanceId && parsed.signature) {
248
+ const dht = getDHTService();
249
+ if (dht) {
250
+ // Reconstruct the signed payload
251
+ const signedPayload = JSON.stringify({
252
+ id: parsed.id,
253
+ type: parsed.type,
254
+ from: parsed.from,
255
+ to: parsed.to,
256
+ topic: parsed.topic,
257
+ payload: parsed.payload,
258
+ timestamp: parsed.timestamp,
259
+ instanceId: parsed.instanceId,
260
+ });
261
+ // Look up sender's pubkey from DHT
262
+ const senderPubkey = await lookupPubkey(dht, parsed.instanceId, logger);
263
+ if (senderPubkey) {
264
+ const valid = verifyInstanceSignature({
265
+ id: parsed.instanceId,
266
+ name: "",
267
+ pubkey: senderPubkey,
268
+ binding: "",
269
+ bindingComponents: { username: "", hostname: "", platform: "" },
270
+ createdAt: 0,
271
+ }, signedPayload, parsed.signature);
272
+ if (valid) {
273
+ logger?.info?.(`[libp2p-mesh] Verified signature from instance ${parsed.instanceId}`);
274
+ }
275
+ else {
276
+ logger?.warn?.(`[libp2p-mesh] Invalid signature from instance ${parsed.instanceId}`);
277
+ }
278
+ }
279
+ else {
280
+ logger?.warn?.(`[libp2p-mesh] No pubkey in DHT for instance ${parsed.instanceId}; skipping verification`);
281
+ }
282
+ }
283
+ else {
284
+ logger?.debug?.(`[libp2p-mesh] DHT disabled; cannot verify signature from ${parsed.instanceId}`);
285
+ }
286
+ }
287
+ logger?.debug?.(`[libp2p-mesh] Received ${parsed.type} from ${parsed.from}${parsed.instanceId ? ` (instance: ${parsed.instanceId})` : ""}`);
138
288
  for (const handler of messageHandlers) {
139
289
  try {
140
290
  handler(parsed);
@@ -143,7 +293,6 @@ export function createMeshNetwork(options) {
143
293
  logger?.error?.(`[libp2p-mesh] Message handler error: ${String(err)}`);
144
294
  }
145
295
  }
146
- // Handle broadcast / topic subscription
147
296
  if (parsed.type === "broadcast" && parsed.topic) {
148
297
  const handlers = topicHandlers.get(parsed.topic);
149
298
  if (handlers) {
@@ -156,7 +305,6 @@ export function createMeshNetwork(options) {
156
305
  }
157
306
  }
158
307
  }
159
- // Flood-fill forward to other connected peers (with TTL guard)
160
308
  await forwardBroadcast(parsed, connection.remotePeer.toString());
161
309
  }
162
310
  }
@@ -165,8 +313,55 @@ export function createMeshNetwork(options) {
165
313
  catch (err) {
166
314
  logger?.error?.(`[libp2p-mesh] Protocol handler error: ${String(err)}`);
167
315
  }
316
+ }, {
317
+ // Allow the openclaw protocol to be served over relayed (transient)
318
+ // connections; without this, peers behind NAT can't deliver messages.
319
+ runOnTransientConnection: true,
168
320
  });
169
321
  await state.node.start();
322
+ // Wait for DHT routing table to populate before registering pubkey
323
+ if (enableDHT) {
324
+ const dht = getDHTService();
325
+ if (dht) {
326
+ let attempts = 0;
327
+ const maxAttempts = 30;
328
+ while (attempts < maxAttempts) {
329
+ const rtSize = dht.routingTable?.size ?? 0;
330
+ const peerCount = state.node.getPeers().length;
331
+ if (rtSize > 0 || peerCount > 0) {
332
+ logger?.info?.(`[libp2p-mesh] DHT routing table ready (peers: ${peerCount}, rt: ${rtSize})`);
333
+ break;
334
+ }
335
+ await new Promise((r) => setTimeout(r, 1000));
336
+ attempts++;
337
+ }
338
+ if (attempts >= maxAttempts) {
339
+ logger?.warn?.(`[libp2p-mesh] DHT routing table still empty after ${maxAttempts}s; continuing anyway`);
340
+ }
341
+ if (state.instanceIdentity) {
342
+ await registerPubkey(dht, state.instanceIdentity.id, state.instanceIdentity.pubkey, logger).catch(() => {
343
+ // Already logged inside registerPubkey
344
+ });
345
+ }
346
+ }
347
+ }
348
+ // Reserve a slot on each configured relay so other peers can dial us
349
+ // through them via /p2p-circuit. Fire-and-forget; failures are logged.
350
+ if (useRelay && relayList.length > 0) {
351
+ const { multiaddr } = await import("@multiformats/multiaddr");
352
+ for (const addr of relayList) {
353
+ const node = state.node;
354
+ (async () => {
355
+ try {
356
+ await node.dial(multiaddr(addr));
357
+ logger?.info?.(`[libp2p-mesh] Connected to relay ${addr} — reservation in progress`);
358
+ }
359
+ catch (err) {
360
+ logger?.warn?.(`[libp2p-mesh] Failed to connect to relay ${addr}: ${String(err)}`);
361
+ }
362
+ })();
363
+ }
364
+ }
170
365
  logger?.info?.(`[libp2p-mesh] Node started. Peer ID: ${state.node.peerId.toString()}`);
171
366
  logger?.info?.(`[libp2p-mesh] Listening on: ${state.node.getMultiaddrs().map((ma) => ma.toString()).join(", ")}`);
172
367
  }
@@ -177,26 +372,77 @@ export function createMeshNetwork(options) {
177
372
  logger?.info?.("[libp2p-mesh] Node stopped.");
178
373
  }
179
374
  }
375
+ function buildSignedMessage(base) {
376
+ const instanceId = state.instanceIdentity?.id;
377
+ const sign = state.signMessage;
378
+ const msg = { ...base };
379
+ if (instanceId && sign) {
380
+ msg.instanceId = instanceId;
381
+ msg.pubkey = state.instanceIdentity?.pubkey;
382
+ const signedPayload = JSON.stringify({
383
+ id: msg.id,
384
+ type: msg.type,
385
+ from: msg.from,
386
+ to: msg.to,
387
+ topic: msg.topic,
388
+ payload: msg.payload,
389
+ timestamp: msg.timestamp,
390
+ instanceId: msg.instanceId,
391
+ });
392
+ msg.signature = sign(signedPayload);
393
+ }
394
+ return msg;
395
+ }
180
396
  async function sendToPeer(peerId, message) {
181
397
  if (!state.node) {
182
398
  throw new Error("Mesh network is not started");
183
399
  }
184
- const msg = {
400
+ const msg = buildSignedMessage({
185
401
  id: crypto.randomUUID(),
186
402
  type: "direct",
187
403
  from: state.node.peerId.toString(),
188
404
  to: peerId,
189
405
  payload: message,
190
406
  timestamp: Date.now(),
191
- };
407
+ });
192
408
  const data = new TextEncoder().encode(JSON.stringify(msg));
193
409
  const abortController = new AbortController();
194
410
  const timeout = setTimeout(() => abortController.abort(), 8000);
195
411
  try {
196
412
  const { peerIdFromString } = await import("@libp2p/peer-id");
413
+ const targetPid = peerIdFromString(peerId);
414
+ // If we have no open connection to the target and no peer-store
415
+ // address (typical when both ends are NAT-isolated and only share a
416
+ // configured relay), proactively dial via each /p2p-circuit path we
417
+ // know — this is what makes the relay configuration actually deliver
418
+ // messages when peer discovery hasn't propagated yet.
419
+ const alreadyConnected = state.node
420
+ .getConnections()
421
+ .some((c) => c.remotePeer.equals(targetPid));
422
+ if (!alreadyConnected) {
423
+ const relayAddrs = config.relayList ?? [];
424
+ for (const relayAddr of relayAddrs) {
425
+ try {
426
+ const circuit = relayAddr.endsWith("/p2p-circuit")
427
+ ? `${relayAddr}/p2p/${peerId}`
428
+ : `${relayAddr}/p2p-circuit/p2p/${peerId}`;
429
+ const { multiaddr } = await import("@multiformats/multiaddr");
430
+ logger?.debug?.(`[libp2p-mesh] sendToPeer: pre-dialling ${peerId} via relay path ${circuit}`);
431
+ await state.node.dial(multiaddr(circuit), {
432
+ signal: abortController.signal,
433
+ });
434
+ logger?.debug?.(`[libp2p-mesh] sendToPeer: established relayed connection to ${peerId}`);
435
+ break;
436
+ }
437
+ catch (relayErr) {
438
+ logger?.debug?.(`[libp2p-mesh] sendToPeer: relay pre-dial via ${relayAddr} failed: ${String(relayErr)}`);
439
+ }
440
+ }
441
+ }
197
442
  logger?.debug?.(`[libp2p-mesh] dialProtocol to ${peerId}`);
198
- const stream = await state.node.dialProtocol(peerIdFromString(peerId), PROTOCOL, {
443
+ const stream = await state.node.dialProtocol(targetPid, PROTOCOL, {
199
444
  signal: abortController.signal,
445
+ runOnTransientConnection: true,
200
446
  });
201
447
  if (!stream) {
202
448
  throw new Error(`Failed to establish stream to ${peerId}; peer may be unreachable`);
@@ -220,14 +466,14 @@ export function createMeshNetwork(options) {
220
466
  if (!state.node) {
221
467
  throw new Error("Mesh network is not started");
222
468
  }
223
- const msg = {
469
+ const msg = buildSignedMessage({
224
470
  id: crypto.randomUUID(),
225
471
  type: "broadcast",
226
472
  from: state.node.peerId.toString(),
227
473
  topic,
228
474
  payload: message,
229
475
  timestamp: Date.now(),
230
- };
476
+ });
231
477
  const data = new TextEncoder().encode(JSON.stringify(msg));
232
478
  const connections = state.node.getConnections();
233
479
  let sent = 0;
@@ -235,7 +481,10 @@ export function createMeshNetwork(options) {
235
481
  const abortController = new AbortController();
236
482
  const timeout = setTimeout(() => abortController.abort(), 5000);
237
483
  try {
238
- const stream = await conn.newStream(PROTOCOL, { signal: abortController.signal });
484
+ const stream = await conn.newStream(PROTOCOL, {
485
+ signal: abortController.signal,
486
+ runOnTransientConnection: true,
487
+ });
239
488
  await pipe([new Uint8ArrayList(data)], encode, stream.sink);
240
489
  sent++;
241
490
  }
@@ -251,7 +500,6 @@ export function createMeshNetwork(options) {
251
500
  async function forwardBroadcast(msg, fromPeerId) {
252
501
  if (!state.node)
253
502
  return;
254
- // Simple flood-fill: forward to all connected peers except the sender
255
503
  const data = new TextEncoder().encode(JSON.stringify(msg));
256
504
  for (const conn of state.node.getConnections()) {
257
505
  const remotePeerId = conn.remotePeer.toString();
@@ -260,7 +508,10 @@ export function createMeshNetwork(options) {
260
508
  const abortController = new AbortController();
261
509
  const timeout = setTimeout(() => abortController.abort(), 5000);
262
510
  try {
263
- const stream = await conn.newStream(PROTOCOL, { signal: abortController.signal });
511
+ const stream = await conn.newStream(PROTOCOL, {
512
+ signal: abortController.signal,
513
+ runOnTransientConnection: true,
514
+ });
264
515
  await pipe([new Uint8ArrayList(data)], encode, stream.sink);
265
516
  }
266
517
  catch {
@@ -292,6 +543,38 @@ export function createMeshNetwork(options) {
292
543
  const peers = state.node.getConnections().map((c) => c.remotePeer.toString());
293
544
  return [...new Set(peers)];
294
545
  }
546
+ function getMultiaddrs() {
547
+ if (!state.node)
548
+ return [];
549
+ return state.node.getMultiaddrs().map((ma) => ma.toString());
550
+ }
551
+ function getInstanceIdentity() {
552
+ return state.instanceIdentity ?? undefined;
553
+ }
554
+ async function dial(multiaddr) {
555
+ if (!state.node) {
556
+ throw new Error("Mesh network is not started");
557
+ }
558
+ const { multiaddr: ma } = await import("@multiformats/multiaddr");
559
+ await state.node.dial(ma(multiaddr));
560
+ }
561
+ function getNATStatus() {
562
+ const enabled = { ...state.natFlags };
563
+ const reservedRelays = [];
564
+ let hasRelayedListenAddr = false;
565
+ if (state.node) {
566
+ // Listen multiaddrs that include /p2p-circuit are reservations the
567
+ // circuit-relay transport managed to acquire from a relay server.
568
+ for (const ma of state.node.getMultiaddrs()) {
569
+ const s = ma.toString();
570
+ if (s.includes("/p2p-circuit")) {
571
+ hasRelayedListenAddr = true;
572
+ reservedRelays.push(s);
573
+ }
574
+ }
575
+ }
576
+ return { enabled, reservedRelays, hasRelayedListenAddr };
577
+ }
295
578
  return {
296
579
  start,
297
580
  stop,
@@ -301,5 +584,9 @@ export function createMeshNetwork(options) {
301
584
  subscribeToTopic,
302
585
  getLocalPeerId,
303
586
  getConnectedPeers,
587
+ getMultiaddrs,
588
+ dial,
589
+ getInstanceIdentity,
590
+ getNATStatus,
304
591
  };
305
592
  }
@@ -1,3 +1,2 @@
1
1
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
2
- import type { FeishuChannelConfig } from "./feishu-types.js";
3
- export declare function registerLibp2pMesh(api: OpenClawPluginApi, feishuConfig?: FeishuChannelConfig): void;
2
+ export declare function registerLibp2pMesh(api: OpenClawPluginApi): void;
@@ -1,36 +1,35 @@
1
1
  import { createLibp2pMeshChannel } from "./channel.js";
2
2
  import { handleP2PInbound } from "./inbound.js";
3
3
  import { createMeshNetwork } from "./mesh.js";
4
- import { buildP2PTools, buildFeishuTools } from "./agent-tools.js";
5
- import { sendViaMesh } from "./send.js";
6
- import { FeishuApiClient } from "./feishu-client.js";
7
- import { createFeishuChannel } from "./feishu-channel.js";
8
- export function registerLibp2pMesh(api, feishuConfig) {
4
+ import { buildP2PTools } from "./agent-tools.js";
5
+ export function registerLibp2pMesh(api) {
9
6
  const mesh = createMeshNetwork({
10
7
  config: api.pluginConfig,
11
8
  logger: api.logger,
12
9
  });
13
- const channel = createLibp2pMeshChannel(mesh);
14
- const feishuClient = feishuConfig?.appId
15
- ? new FeishuApiClient(feishuConfig, { logger: api.logger })
16
- : undefined;
17
10
  // 1. Register Service (manages libp2p node lifecycle)
18
11
  api.registerService({
19
12
  id: "libp2p-mesh",
20
13
  start: async () => {
21
14
  await mesh.start();
22
15
  mesh.onMessage((msg) => {
23
- const sendToChannel = async (_channelId, target, text) => {
24
- try {
25
- await sendViaMesh(mesh, target, text);
26
- }
27
- catch (err) {
28
- api.logger.error?.(`[libp2p-mesh] sendToChannel error: ${err}`);
29
- }
30
- };
31
- handleP2PInbound(msg, { logger: api.logger, sendToChannel, feishuClient });
16
+ handleP2PInbound(msg, { logger: api.logger });
32
17
  });
18
+ const identity = mesh.getInstanceIdentity();
33
19
  api.logger.info?.(`[libp2p-mesh] Service started. Peer ID: ${mesh.getLocalPeerId()}`);
20
+ if (identity) {
21
+ api.logger.info?.(`[libp2p-mesh] Instance Identity: ${identity.id}`);
22
+ }
23
+ const nat = mesh.getNATStatus();
24
+ const enabledNames = Object.entries(nat.enabled)
25
+ .filter(([, on]) => on)
26
+ .map(([k]) => k);
27
+ if (enabledNames.length > 0) {
28
+ api.logger.info?.(`[libp2p-mesh] NAT traversal services: ${enabledNames.join(", ")}`);
29
+ }
30
+ if (nat.reservedRelays.length > 0) {
31
+ api.logger.info?.(`[libp2p-mesh] Active relay reservations: ${nat.reservedRelays.join(", ")}`);
32
+ }
34
33
  },
35
34
  stop: async () => {
36
35
  await mesh.stop();
@@ -39,27 +38,16 @@ export function registerLibp2pMesh(api, feishuConfig) {
39
38
  });
40
39
  // 2. Register Channel (lightweight debugging surface)
41
40
  api.registerChannel({
42
- plugin: channel,
41
+ plugin: createLibp2pMeshChannel(mesh),
43
42
  });
44
43
  // 3. Register Agent Tools
45
44
  const tools = buildP2PTools(mesh);
46
45
  for (const tool of tools) {
47
46
  api.registerTool(tool);
48
47
  }
49
- const feishuTools = buildFeishuTools(feishuClient ?? null);
50
- for (const tool of feishuTools) {
51
- api.registerTool(tool);
52
- }
53
48
  // 4. Register Hook (log received messages for observability)
54
49
  api.registerHook("message:received", async (event) => {
55
50
  const ctx = event.context;
56
51
  api.logger.debug?.(`[libp2p-mesh] message received on channel ${ctx?.channelId ?? "unknown"}`);
57
52
  }, { name: "libp2p-mesh-message-received" });
58
- // 5. Conditionally register Feishu channel
59
- if (feishuClient && feishuConfig) {
60
- const feishuChannel = createFeishuChannel(feishuConfig, { logger: api.logger }, feishuClient);
61
- api.registerChannel({
62
- plugin: feishuChannel,
63
- });
64
- }
65
53
  }