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.
- package/README.md +61 -23
- package/api.ts +1 -1
- package/dist/api.d.ts +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +60 -24
- package/dist/runtime-setter-api.d.ts +4 -0
- package/dist/runtime-setter-api.js +19 -0
- package/dist/src/agent-tools.d.ts +63 -24
- package/dist/src/agent-tools.js +69 -34
- package/dist/src/channel.d.ts +1 -0
- package/dist/src/channel.js +20 -4
- package/dist/src/dht-registry.d.ts +38 -0
- package/dist/src/dht-registry.js +80 -0
- package/dist/src/inbound.d.ts +0 -3
- package/dist/src/inbound.js +29 -14
- package/dist/src/instance-id.d.ts +53 -0
- package/dist/src/instance-id.js +156 -0
- package/dist/src/mesh.js +310 -23
- package/dist/src/plugin.d.ts +1 -2
- package/dist/src/plugin.js +18 -30
- package/dist/src/types.d.ts +87 -0
- package/index.ts +60 -24
- package/openclaw.plugin.json +72 -33
- package/package.json +20 -8
- package/src/agent-tools.ts +69 -35
- package/src/channel.ts +25 -4
- package/src/dht-registry.ts +105 -0
- package/src/inbound.ts +35 -18
- package/src/instance-id.ts +221 -0
- package/src/mesh.ts +368 -27
- package/src/plugin.ts +25 -36
- package/src/types.ts +95 -0
- package/dist/src/agent-tools-feishu.test.d.ts +0 -1
- package/dist/src/agent-tools-feishu.test.js +0 -57
- package/dist/src/config-schema.test.d.ts +0 -1
- package/dist/src/config-schema.test.js +0 -55
- package/dist/src/feishu-channel.d.ts +0 -19
- package/dist/src/feishu-channel.js +0 -202
- package/dist/src/feishu-channel.test.d.ts +0 -1
- package/dist/src/feishu-channel.test.js +0 -166
- package/dist/src/feishu-client.d.ts +0 -27
- package/dist/src/feishu-client.js +0 -141
- package/dist/src/feishu-client.test.d.ts +0 -1
- package/dist/src/feishu-client.test.js +0 -271
- package/dist/src/feishu-e2e.test.d.ts +0 -1
- package/dist/src/feishu-e2e.test.js +0 -69
- package/dist/src/feishu-types.d.ts +0 -53
- package/dist/src/feishu-types.js +0 -1
- package/dist/src/feishu-types.test.d.ts +0 -1
- package/dist/src/feishu-types.test.js +0 -108
- package/dist/src/inbound-feishu.test.d.ts +0 -1
- package/dist/src/inbound-feishu.test.js +0 -70
- package/dist/src/index.d.ts +0 -1
- package/dist/src/index.js +0 -1
- package/dist/src/plugin-registration.test.d.ts +0 -1
- package/dist/src/plugin-registration.test.js +0 -42
- package/src/agent-tools-feishu.test.ts +0 -68
- package/src/config-schema.test.ts +0 -63
- package/src/feishu-channel.test.ts +0 -191
- package/src/feishu-channel.ts +0 -253
- package/src/feishu-client.test.ts +0 -303
- package/src/feishu-client.ts +0 -178
- package/src/feishu-e2e.test.ts +0 -90
- package/src/feishu-types.test.ts +0 -125
- package/src/feishu-types.ts +0 -51
- package/src/inbound-feishu.test.ts +0 -91
- package/src/index.ts +0 -1
- 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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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:
|
|
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
|
-
|
|
137
|
-
|
|
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(
|
|
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, {
|
|
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, {
|
|
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
|
}
|
package/dist/src/plugin.d.ts
CHANGED
|
@@ -1,3 +1,2 @@
|
|
|
1
1
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
|
2
|
-
|
|
3
|
-
export declare function registerLibp2pMesh(api: OpenClawPluginApi, feishuConfig?: FeishuChannelConfig): void;
|
|
2
|
+
export declare function registerLibp2pMesh(api: OpenClawPluginApi): void;
|
package/dist/src/plugin.js
CHANGED
|
@@ -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
|
|
5
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
}
|