libp2p-mesh 2026.6.3 → 2026.6.4
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 +81 -19
- package/api.ts +2 -0
- package/dist/api.d.ts +1 -1
- package/dist/index.js +31 -1
- package/dist/src/agent-tools.d.ts +6 -2
- package/dist/src/agent-tools.js +29 -0
- package/dist/src/cli.d.ts +15 -0
- package/dist/src/cli.js +196 -0
- package/dist/src/config-io.d.ts +9 -0
- package/dist/src/config-io.js +173 -0
- package/dist/src/inbound.d.ts +1 -0
- package/dist/src/inbound.js +10 -3
- package/dist/src/instance-router.js +313 -37
- package/dist/src/plugin.js +28 -2
- package/dist/src/types.d.ts +18 -1
- package/dist/src/wizard.d.ts +22 -0
- package/dist/src/wizard.js +276 -0
- package/index.ts +31 -1
- package/openclaw.plugin.json +71 -1
- package/package.json +6 -5
- package/src/agent-tools.ts +36 -1
- package/src/cli.ts +226 -0
- package/src/config-io.ts +204 -0
- package/src/inbound.ts +17 -5
- package/src/instance-router.ts +370 -39
- package/src/plugin.ts +34 -3
- package/src/types.ts +20 -1
- package/src/wizard.ts +332 -0
package/dist/src/inbound.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { verifyInstanceSignature } from "./instance-id.js";
|
|
2
2
|
export function handleP2PInbound(msg, deps) {
|
|
3
|
-
const { logger } = deps;
|
|
3
|
+
const { logger, sendToChannel } = deps;
|
|
4
4
|
const instanceTag = msg.instanceId ? ` [instance: ${msg.instanceId}]` : "";
|
|
5
5
|
const signedTag = msg.signature ? " [signed]" : "";
|
|
6
6
|
// Verify signature if present
|
|
@@ -28,8 +28,15 @@ export function handleP2PInbound(msg, deps) {
|
|
|
28
28
|
}
|
|
29
29
|
if (msg.type === "broadcast") {
|
|
30
30
|
logger?.info?.(`[libp2p-mesh] Broadcast from ${msg.from}${instanceTag}${signedTag} on topic ${msg.topic ?? "(none)"}: ${msg.payload}`);
|
|
31
|
+
return;
|
|
31
32
|
}
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
// Direct message — log and forward to local channel
|
|
34
|
+
logger?.info?.(`[libp2p-mesh] Direct message from ${msg.from}${instanceTag}${signedTag}: ${msg.payload}`);
|
|
35
|
+
if (!sendToChannel || !msg.payload) {
|
|
36
|
+
return;
|
|
34
37
|
}
|
|
38
|
+
const text = `[来自 ${msg.from}]\n${msg.payload}`;
|
|
39
|
+
sendToChannel("libp2p-mesh", msg.from, text).catch((err) => {
|
|
40
|
+
logger?.error?.(`[libp2p-mesh] Failed to forward direct message from ${msg.from}: ${err}`);
|
|
41
|
+
});
|
|
35
42
|
}
|
|
@@ -13,6 +13,67 @@ function isNonEmptyString(value) {
|
|
|
13
13
|
function summarizeError(error) {
|
|
14
14
|
return error instanceof Error ? error.message : String(error);
|
|
15
15
|
}
|
|
16
|
+
const INVALID_INBOUND_TARGET_ERROR = "inbound target channel and target are required";
|
|
17
|
+
function invalidInboundTarget() {
|
|
18
|
+
return {
|
|
19
|
+
channel: "",
|
|
20
|
+
target: "",
|
|
21
|
+
valid: false,
|
|
22
|
+
error: INVALID_INBOUND_TARGET_ERROR,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function displayTargetId(target) {
|
|
26
|
+
return typeof target.id === "string" ? target.id.trim() || undefined : undefined;
|
|
27
|
+
}
|
|
28
|
+
function normalizeConfiguredTarget(target) {
|
|
29
|
+
if (!target || typeof target !== "object" || Array.isArray(target)) {
|
|
30
|
+
return invalidInboundTarget();
|
|
31
|
+
}
|
|
32
|
+
const configuredTarget = target;
|
|
33
|
+
const channel = typeof configuredTarget.channel === "string" ? configuredTarget.channel.trim() : "";
|
|
34
|
+
const destination = typeof configuredTarget.target === "string" ? configuredTarget.target.trim() : "";
|
|
35
|
+
const normalized = {
|
|
36
|
+
id: displayTargetId(configuredTarget),
|
|
37
|
+
channel,
|
|
38
|
+
target: destination,
|
|
39
|
+
valid: Boolean(channel && destination),
|
|
40
|
+
};
|
|
41
|
+
if (!normalized.valid) {
|
|
42
|
+
normalized.error = INVALID_INBOUND_TARGET_ERROR;
|
|
43
|
+
}
|
|
44
|
+
return normalized;
|
|
45
|
+
}
|
|
46
|
+
function effectiveInboundTargets(config) {
|
|
47
|
+
if (Array.isArray(config.inboundTargets)) {
|
|
48
|
+
const seen = new Set();
|
|
49
|
+
const targets = [];
|
|
50
|
+
for (const target of config.inboundTargets) {
|
|
51
|
+
const normalized = normalizeConfiguredTarget(target);
|
|
52
|
+
const key = `${normalized.channel}\0${normalized.target}`;
|
|
53
|
+
if (normalized.valid && seen.has(key)) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (normalized.valid) {
|
|
57
|
+
seen.add(key);
|
|
58
|
+
}
|
|
59
|
+
targets.push(normalized);
|
|
60
|
+
}
|
|
61
|
+
return targets;
|
|
62
|
+
}
|
|
63
|
+
if (!config.inboundChannel || !config.inboundTarget) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
return [
|
|
67
|
+
{
|
|
68
|
+
channel: config.inboundChannel,
|
|
69
|
+
target: config.inboundTarget,
|
|
70
|
+
valid: true,
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
function firstAttemptedResult(results) {
|
|
75
|
+
return results.find((result) => result.ok) ?? results[0];
|
|
76
|
+
}
|
|
16
77
|
export function createInstanceRouter(options) {
|
|
17
78
|
const { mesh, store, delivery } = options;
|
|
18
79
|
const config = options.config ?? {};
|
|
@@ -21,7 +82,10 @@ export function createInstanceRouter(options) {
|
|
|
21
82
|
const announcedPeers = new Set();
|
|
22
83
|
const pendingAcks = new Map();
|
|
23
84
|
const deliveryCache = new Map();
|
|
85
|
+
const inboundTimeoutControllers = new Set();
|
|
86
|
+
const activeAckSends = new Set();
|
|
24
87
|
const unsubs = [];
|
|
88
|
+
let stopped = false;
|
|
25
89
|
function localInstanceId() {
|
|
26
90
|
const identity = mesh.getInstanceIdentity();
|
|
27
91
|
if (!identity) {
|
|
@@ -95,14 +159,191 @@ export function createInstanceRouter(options) {
|
|
|
95
159
|
}
|
|
96
160
|
}
|
|
97
161
|
async function sendAck(peerId, ack) {
|
|
98
|
-
|
|
162
|
+
if (stopped) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const send = mesh.sendStructuredMessage(peerId, {
|
|
99
166
|
id: crypto.randomUUID(),
|
|
100
167
|
type: "delivery-ack",
|
|
101
168
|
to: peerId,
|
|
102
169
|
payload: JSON.stringify(ack),
|
|
103
170
|
});
|
|
171
|
+
activeAckSends.add(send);
|
|
172
|
+
try {
|
|
173
|
+
await send;
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
activeAckSends.delete(send);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function deliveryCacheKey(payload, msg) {
|
|
180
|
+
return [
|
|
181
|
+
payload.fromInstanceId,
|
|
182
|
+
msg.from,
|
|
183
|
+
payload.toInstanceId,
|
|
184
|
+
payload.messageId,
|
|
185
|
+
].join("\0");
|
|
186
|
+
}
|
|
187
|
+
function buildAckFromDeliveryState(messageId, state) {
|
|
188
|
+
const selected = firstAttemptedResult(state.results);
|
|
189
|
+
return {
|
|
190
|
+
ackFor: messageId,
|
|
191
|
+
ok: state.results.some((result) => result.ok),
|
|
192
|
+
inboundChannel: selected?.channel,
|
|
193
|
+
inboundTarget: selected?.target,
|
|
194
|
+
deliveredAt: Date.now(),
|
|
195
|
+
error: state.results.every((result) => !result.ok)
|
|
196
|
+
? state.results.map((result) => result.error).filter(Boolean).join("; ") ||
|
|
197
|
+
"inbound delivery failed"
|
|
198
|
+
: undefined,
|
|
199
|
+
results: state.results,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
async function deliverAndBuildAck(payload, msg, state) {
|
|
203
|
+
const targets = effectiveInboundTargets(config);
|
|
204
|
+
if (targets.length === 0) {
|
|
205
|
+
return {
|
|
206
|
+
ackFor: payload.messageId,
|
|
207
|
+
ok: false,
|
|
208
|
+
deliveredAt: Date.now(),
|
|
209
|
+
error: "inbound delivery is not configured",
|
|
210
|
+
results: [],
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
const metadata = payload.metadata;
|
|
214
|
+
for (const target of targets) {
|
|
215
|
+
if (state.timedOut) {
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
if (!target.valid) {
|
|
219
|
+
state.results.push({
|
|
220
|
+
id: target.id,
|
|
221
|
+
channel: target.channel,
|
|
222
|
+
target: target.target,
|
|
223
|
+
ok: false,
|
|
224
|
+
error: target.error ?? "inbound target channel and target are required",
|
|
225
|
+
});
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
state.currentTarget = target;
|
|
229
|
+
try {
|
|
230
|
+
const result = await delivery.deliver({
|
|
231
|
+
channel: target.channel,
|
|
232
|
+
target: target.target,
|
|
233
|
+
text: payload.text,
|
|
234
|
+
metadata: {
|
|
235
|
+
fromInstanceId: payload.fromInstanceId,
|
|
236
|
+
fromPeerId: msg.from,
|
|
237
|
+
p2pMessageId: payload.messageId,
|
|
238
|
+
allowAgentAutoReply: metadata?.allowAgentAutoReply === true,
|
|
239
|
+
replyToInstanceId: payload.fromInstanceId,
|
|
240
|
+
replyTool: "p2p_send_instance_message",
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
if (state.timedOut) {
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
state.results.push({
|
|
247
|
+
id: target.id,
|
|
248
|
+
channel: result.channel,
|
|
249
|
+
target: result.target,
|
|
250
|
+
ok: result.ok,
|
|
251
|
+
error: result.error,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
if (state.timedOut) {
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
state.results.push({
|
|
259
|
+
id: target.id,
|
|
260
|
+
channel: target.channel,
|
|
261
|
+
target: target.target,
|
|
262
|
+
ok: false,
|
|
263
|
+
error: summarizeError(error),
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
finally {
|
|
267
|
+
if (state.currentTarget === target) {
|
|
268
|
+
state.currentTarget = undefined;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return buildAckFromDeliveryState(payload.messageId, state);
|
|
273
|
+
}
|
|
274
|
+
function buildInboundTimeoutAck(messageId, state) {
|
|
275
|
+
const error = `inbound delivery timeout after ${ackTimeoutMs}ms`;
|
|
276
|
+
const results = state.results.slice();
|
|
277
|
+
const currentTarget = state.currentTarget;
|
|
278
|
+
if (currentTarget) {
|
|
279
|
+
results.push({
|
|
280
|
+
id: currentTarget.id,
|
|
281
|
+
channel: currentTarget.channel,
|
|
282
|
+
target: currentTarget.target,
|
|
283
|
+
ok: false,
|
|
284
|
+
error: currentTarget.valid
|
|
285
|
+
? error
|
|
286
|
+
: currentTarget.error ?? "inbound target channel and target are required",
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
const selected = firstAttemptedResult(results);
|
|
290
|
+
return {
|
|
291
|
+
ackFor: messageId,
|
|
292
|
+
ok: results.some((result) => result.ok),
|
|
293
|
+
inboundChannel: selected?.channel,
|
|
294
|
+
inboundTarget: selected?.target,
|
|
295
|
+
deliveredAt: Date.now(),
|
|
296
|
+
error,
|
|
297
|
+
results,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
function withInboundDeliveryTimeout(promise, messageId, state) {
|
|
301
|
+
return new Promise((resolve) => {
|
|
302
|
+
let settled = false;
|
|
303
|
+
const settle = (ack) => {
|
|
304
|
+
if (settled)
|
|
305
|
+
return;
|
|
306
|
+
settled = true;
|
|
307
|
+
clearTimeout(timer);
|
|
308
|
+
inboundTimeoutControllers.delete(controller);
|
|
309
|
+
resolve(ack);
|
|
310
|
+
};
|
|
311
|
+
const timer = setTimeout(() => {
|
|
312
|
+
state.timedOut = true;
|
|
313
|
+
settle(buildInboundTimeoutAck(messageId, state));
|
|
314
|
+
}, ackTimeoutMs);
|
|
315
|
+
const controller = {
|
|
316
|
+
stop() {
|
|
317
|
+
state.timedOut = true;
|
|
318
|
+
settle({
|
|
319
|
+
ackFor: messageId,
|
|
320
|
+
ok: false,
|
|
321
|
+
deliveredAt: Date.now(),
|
|
322
|
+
error: "instance router stopped",
|
|
323
|
+
results: state.results,
|
|
324
|
+
});
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
inboundTimeoutControllers.add(controller);
|
|
328
|
+
promise
|
|
329
|
+
.then((ack) => {
|
|
330
|
+
settle(ack);
|
|
331
|
+
})
|
|
332
|
+
.catch((error) => {
|
|
333
|
+
settle({
|
|
334
|
+
ackFor: messageId,
|
|
335
|
+
ok: false,
|
|
336
|
+
deliveredAt: Date.now(),
|
|
337
|
+
error: summarizeError(error),
|
|
338
|
+
results: [],
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
});
|
|
104
342
|
}
|
|
105
343
|
async function handleUserMessage(msg) {
|
|
344
|
+
if (stopped) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
106
347
|
const payload = parsePayload(msg);
|
|
107
348
|
if (!payload ||
|
|
108
349
|
!isNonEmptyString(payload.messageId) ||
|
|
@@ -117,6 +358,9 @@ export function createInstanceRouter(options) {
|
|
|
117
358
|
return;
|
|
118
359
|
}
|
|
119
360
|
const senderRoute = await store.resolve(payload.fromInstanceId);
|
|
361
|
+
if (stopped) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
120
364
|
if (!senderRoute || senderRoute.peerId !== msg.from) {
|
|
121
365
|
logger?.warn?.(`[libp2p-mesh] Ignoring user-message from ${msg.from}; instance ${payload.fromInstanceId} is not routed to that peer`);
|
|
122
366
|
return;
|
|
@@ -126,56 +370,78 @@ export function createInstanceRouter(options) {
|
|
|
126
370
|
logger?.warn?.(`[libp2p-mesh] Ignoring user-message for ${payload.toInstanceId}; local instance is ${localId}`);
|
|
127
371
|
return;
|
|
128
372
|
}
|
|
129
|
-
const
|
|
373
|
+
const cacheKey = deliveryCacheKey(payload, msg);
|
|
374
|
+
const cached = deliveryCache.get(cacheKey);
|
|
130
375
|
if (cached) {
|
|
131
|
-
|
|
376
|
+
const ack = cached.payload ?? (await cached.promise);
|
|
377
|
+
if (!ack) {
|
|
378
|
+
throw new Error(`delivery cache entry for ${payload.messageId} has no ACK payload`);
|
|
379
|
+
}
|
|
380
|
+
if (stopped) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
await sendAck(msg.from, ack);
|
|
132
384
|
return;
|
|
133
385
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
ack = {
|
|
386
|
+
if (pendingDeliveryCount() >= MAX_DELIVERY_CACHE_ENTRIES) {
|
|
387
|
+
const ack = {
|
|
137
388
|
ackFor: payload.messageId,
|
|
138
389
|
ok: false,
|
|
139
|
-
inboundChannel: config.inboundChannel,
|
|
140
|
-
inboundTarget: config.inboundTarget,
|
|
141
390
|
deliveredAt: Date.now(),
|
|
142
|
-
error:
|
|
391
|
+
error: `too many pending inbound deliveries (${MAX_DELIVERY_CACHE_ENTRIES})`,
|
|
392
|
+
results: [],
|
|
143
393
|
};
|
|
394
|
+
deliveryCache.set(cacheKey, { payload: ack });
|
|
395
|
+
trimDeliveryCache(cacheKey);
|
|
396
|
+
if (stopped) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
await sendAck(msg.from, ack);
|
|
400
|
+
return;
|
|
144
401
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
inboundTarget: result.target,
|
|
165
|
-
deliveredAt: Date.now(),
|
|
166
|
-
error: result.error,
|
|
167
|
-
};
|
|
402
|
+
const deliveryState = {
|
|
403
|
+
timedOut: false,
|
|
404
|
+
results: [],
|
|
405
|
+
};
|
|
406
|
+
const deliveryPromise = Promise.resolve()
|
|
407
|
+
.then(() => deliverAndBuildAck(payload, msg, deliveryState))
|
|
408
|
+
.finally(() => {
|
|
409
|
+
const entry = deliveryCache.get(cacheKey);
|
|
410
|
+
if (entry) {
|
|
411
|
+
entry.inFlight = false;
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
const ackPromise = withInboundDeliveryTimeout(deliveryPromise, payload.messageId, deliveryState);
|
|
415
|
+
const cacheEntry = { inFlight: true, promise: ackPromise };
|
|
416
|
+
deliveryCache.set(cacheKey, cacheEntry);
|
|
417
|
+
trimDeliveryCache();
|
|
418
|
+
const ack = await ackPromise;
|
|
419
|
+
if (stopped) {
|
|
420
|
+
return;
|
|
168
421
|
}
|
|
169
|
-
|
|
422
|
+
cacheEntry.payload = ack;
|
|
170
423
|
trimDeliveryCache();
|
|
424
|
+
if (stopped) {
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
171
427
|
await sendAck(msg.from, ack);
|
|
172
428
|
}
|
|
173
|
-
function
|
|
429
|
+
function pendingDeliveryCount() {
|
|
430
|
+
let count = 0;
|
|
431
|
+
for (const entry of deliveryCache.values()) {
|
|
432
|
+
if (entry.inFlight)
|
|
433
|
+
count += 1;
|
|
434
|
+
}
|
|
435
|
+
return count;
|
|
436
|
+
}
|
|
437
|
+
function trimDeliveryCache(protectedKey) {
|
|
174
438
|
while (deliveryCache.size > MAX_DELIVERY_CACHE_ENTRIES) {
|
|
175
|
-
const
|
|
176
|
-
|
|
439
|
+
const settledKeys = Array.from(deliveryCache)
|
|
440
|
+
.filter(([key, entry]) => key !== protectedKey && entry.payload && !entry.inFlight)
|
|
441
|
+
.map(([key]) => key);
|
|
442
|
+
if (settledKeys.length === 0)
|
|
177
443
|
return;
|
|
178
|
-
deliveryCache.delete(
|
|
444
|
+
deliveryCache.delete(settledKeys[0]);
|
|
179
445
|
}
|
|
180
446
|
}
|
|
181
447
|
function handleAck(msg) {
|
|
@@ -211,6 +477,7 @@ export function createInstanceRouter(options) {
|
|
|
211
477
|
}
|
|
212
478
|
}
|
|
213
479
|
async function start() {
|
|
480
|
+
stopped = false;
|
|
214
481
|
unsubs.push(mesh.onMessage((msg) => {
|
|
215
482
|
handleMessage(msg).catch((error) => {
|
|
216
483
|
logger?.error?.(`[libp2p-mesh] Instance router message error: ${summarizeError(error)}`);
|
|
@@ -224,9 +491,14 @@ export function createInstanceRouter(options) {
|
|
|
224
491
|
await announceToConnectedPeers();
|
|
225
492
|
}
|
|
226
493
|
async function stop() {
|
|
494
|
+
stopped = true;
|
|
227
495
|
for (const unsub of unsubs.splice(0)) {
|
|
228
496
|
unsub();
|
|
229
497
|
}
|
|
498
|
+
for (const controller of Array.from(inboundTimeoutControllers)) {
|
|
499
|
+
controller.stop();
|
|
500
|
+
}
|
|
501
|
+
inboundTimeoutControllers.clear();
|
|
230
502
|
for (const [messageId, pending] of pendingAcks) {
|
|
231
503
|
clearTimeout(pending.timer);
|
|
232
504
|
pending.resolve({
|
|
@@ -238,6 +510,8 @@ export function createInstanceRouter(options) {
|
|
|
238
510
|
}
|
|
239
511
|
pendingAcks.clear();
|
|
240
512
|
deliveryCache.clear();
|
|
513
|
+
await Promise.allSettled([...activeAckSends]);
|
|
514
|
+
activeAckSends.clear();
|
|
241
515
|
}
|
|
242
516
|
async function listInstances() {
|
|
243
517
|
return store.list();
|
|
@@ -311,6 +585,8 @@ export function createInstanceRouter(options) {
|
|
|
311
585
|
toPeerId: route.peerId,
|
|
312
586
|
ackMessageId: ack.ackFor,
|
|
313
587
|
inboundChannel: ack.inboundChannel,
|
|
588
|
+
inboundTarget: ack.inboundTarget,
|
|
589
|
+
deliveryResults: ack.results,
|
|
314
590
|
error: ack.error,
|
|
315
591
|
};
|
|
316
592
|
}
|
package/dist/src/plugin.js
CHANGED
|
@@ -26,6 +26,7 @@ export function registerLibp2pMesh(api) {
|
|
|
26
26
|
config,
|
|
27
27
|
logger: api.logger,
|
|
28
28
|
});
|
|
29
|
+
const channel = createLibp2pMeshChannel(mesh);
|
|
29
30
|
// 1. Register Service (manages libp2p node lifecycle)
|
|
30
31
|
api.registerService({
|
|
31
32
|
id: "libp2p-mesh",
|
|
@@ -37,7 +38,32 @@ export function registerLibp2pMesh(api) {
|
|
|
37
38
|
await mesh.start();
|
|
38
39
|
await router.start();
|
|
39
40
|
unsubscribeInbound = mesh.onMessage((msg) => {
|
|
40
|
-
if (msg.type === "direct" || msg.type === "broadcast"
|
|
41
|
+
if (msg.type === "direct" || msg.type === "broadcast") {
|
|
42
|
+
const sendToChannel = async (_channelId, _target, text) => {
|
|
43
|
+
if (!config?.inboundChannel || !config?.inboundTarget) {
|
|
44
|
+
api.logger.warn?.("[libp2p-mesh] inboundChannel/inboundTarget not configured; direct message logged only.");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const result = await delivery.deliver({
|
|
48
|
+
channel: config.inboundChannel,
|
|
49
|
+
target: config.inboundTarget,
|
|
50
|
+
text,
|
|
51
|
+
metadata: {
|
|
52
|
+
fromInstanceId: msg.instanceId ?? msg.from,
|
|
53
|
+
fromPeerId: msg.from,
|
|
54
|
+
p2pMessageId: msg.id,
|
|
55
|
+
allowAgentAutoReply: false,
|
|
56
|
+
replyToInstanceId: msg.instanceId ?? msg.from,
|
|
57
|
+
replyTool: "p2p_send_instance_message",
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
if (!result.ok) {
|
|
61
|
+
api.logger.error?.(`[libp2p-mesh] Failed to forward direct message from ${msg.from}: ${result.error}`);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
handleP2PInbound(msg, { logger: api.logger, sendToChannel });
|
|
65
|
+
}
|
|
66
|
+
else if (msg.type === "agent-sync") {
|
|
41
67
|
handleP2PInbound(msg, { logger: api.logger });
|
|
42
68
|
}
|
|
43
69
|
});
|
|
@@ -69,7 +95,7 @@ export function registerLibp2pMesh(api) {
|
|
|
69
95
|
});
|
|
70
96
|
// 2. Register Channel (lightweight debugging surface)
|
|
71
97
|
api.registerChannel({
|
|
72
|
-
plugin:
|
|
98
|
+
plugin: channel,
|
|
73
99
|
});
|
|
74
100
|
// 3. Register Agent Tools
|
|
75
101
|
const tools = buildP2PTools(mesh, router);
|
package/dist/src/types.d.ts
CHANGED
|
@@ -51,6 +51,18 @@ export interface UserMessagePayload {
|
|
|
51
51
|
replyTool: "p2p_send_instance_message";
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
|
+
export interface InboundTargetConfig {
|
|
55
|
+
id?: string;
|
|
56
|
+
channel: string;
|
|
57
|
+
target: string;
|
|
58
|
+
}
|
|
59
|
+
export interface DeliveryTargetResult {
|
|
60
|
+
id?: string;
|
|
61
|
+
channel: string;
|
|
62
|
+
target: string;
|
|
63
|
+
ok: boolean;
|
|
64
|
+
error?: string;
|
|
65
|
+
}
|
|
54
66
|
export interface DeliveryAckPayload {
|
|
55
67
|
ackFor: string;
|
|
56
68
|
ok: boolean;
|
|
@@ -58,6 +70,7 @@ export interface DeliveryAckPayload {
|
|
|
58
70
|
inboundTarget?: string;
|
|
59
71
|
deliveredAt: number;
|
|
60
72
|
error?: string;
|
|
73
|
+
results?: DeliveryTargetResult[];
|
|
61
74
|
}
|
|
62
75
|
export interface InstancePeerRecord {
|
|
63
76
|
instanceId: string;
|
|
@@ -120,6 +133,8 @@ export interface InstanceRouter {
|
|
|
120
133
|
toPeerId: string;
|
|
121
134
|
ackMessageId?: string;
|
|
122
135
|
inboundChannel?: string;
|
|
136
|
+
inboundTarget?: string;
|
|
137
|
+
deliveryResults?: DeliveryTargetResult[];
|
|
123
138
|
error?: string;
|
|
124
139
|
}>;
|
|
125
140
|
}
|
|
@@ -165,7 +180,8 @@ export interface MeshConfig {
|
|
|
165
180
|
/**
|
|
166
181
|
* Deprecated pre-2026.6 config keys kept so existing OpenClaw configs keep
|
|
167
182
|
* validating after upgrade. Relay selection is now configured with
|
|
168
|
-
* `relayList`; inbound display uses `inboundChannel`/`inboundTarget
|
|
183
|
+
* `relayList`; inbound display uses `inboundChannel`/`inboundTarget`, or
|
|
184
|
+
* `inboundTargets` when multi-target inbound delivery is enabled.
|
|
169
185
|
*/
|
|
170
186
|
relayChannel?: string;
|
|
171
187
|
relayAccountId?: string;
|
|
@@ -182,6 +198,7 @@ export interface MeshConfig {
|
|
|
182
198
|
announceAddrs?: string[];
|
|
183
199
|
inboundChannel?: string;
|
|
184
200
|
inboundTarget?: string;
|
|
201
|
+
inboundTargets?: InboundTargetConfig[];
|
|
185
202
|
deliveryAckTimeoutMs?: number;
|
|
186
203
|
}
|
|
187
204
|
export interface NATTraversalStatus {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface PromptChoice {
|
|
2
|
+
label: string;
|
|
3
|
+
value: string;
|
|
4
|
+
hint?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface WizardPrompter {
|
|
7
|
+
question(prompt: string, defaultValue?: string): Promise<string>;
|
|
8
|
+
confirm(prompt: string, defaultValue?: boolean): Promise<boolean>;
|
|
9
|
+
select(prompt: string, choices: PromptChoice[]): Promise<string>;
|
|
10
|
+
multiline(prompt: string, helpText?: string): Promise<string[]>;
|
|
11
|
+
displayBox(title: string, lines: string[]): void;
|
|
12
|
+
displaySuccess(message: string): void;
|
|
13
|
+
displayError(message: string): void;
|
|
14
|
+
displayWarning(message: string): void;
|
|
15
|
+
close(): void;
|
|
16
|
+
}
|
|
17
|
+
export declare function validateMultiaddr(raw: string): string | null;
|
|
18
|
+
export declare function createReadlinePrompter(): WizardPrompter;
|
|
19
|
+
export declare function runSetupWizard(prompter: WizardPrompter, currentConfig: Record<string, unknown>, availableChannels: string[]): Promise<Record<string, unknown>>;
|
|
20
|
+
export declare class WizardCancelledError extends Error {
|
|
21
|
+
constructor();
|
|
22
|
+
}
|