libp2p-mesh 2026.6.13 → 2026.6.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 +49 -0
- package/dist/src/debug-cli.d.ts +21 -0
- package/dist/src/debug-cli.js +54 -0
- package/dist/src/debug-wizard.d.ts +19 -0
- package/dist/src/debug-wizard.js +41 -0
- package/dist/src/instance-router.js +45 -5
- package/dist/src/plugin.d.ts +7 -0
- package/dist/src/plugin.js +97 -48
- package/dist/src/profile-cli.d.ts +2 -0
- package/dist/src/profile-cli.js +2 -0
- package/dist/src/setup-config.d.ts +4 -1
- package/dist/src/setup-config.js +27 -0
- package/dist/src/types.d.ts +4 -0
- package/dist/src/user-md-attributes.d.ts +1 -0
- package/dist/src/user-md-attributes.js +12 -1
- package/openclaw.plugin.json +12 -0
- package/package.json +1 -1
- package/src/debug-cli.ts +93 -0
- package/src/debug-wizard.ts +64 -0
- package/src/instance-router.ts +67 -6
- package/src/plugin.ts +125 -57
- package/src/profile-cli.ts +3 -0
- package/src/setup-config.ts +35 -1
- package/src/types.ts +5 -0
- package/src/user-md-attributes.ts +15 -1
package/openclaw.plugin.json
CHANGED
|
@@ -35,6 +35,12 @@
|
|
|
35
35
|
"type": "string",
|
|
36
36
|
"default": "openclaw-mesh"
|
|
37
37
|
},
|
|
38
|
+
"announceLogDetail": {
|
|
39
|
+
"type": "string",
|
|
40
|
+
"enum": ["off", "summary", "payload"],
|
|
41
|
+
"default": "summary",
|
|
42
|
+
"description": "Controls instance announce logging detail."
|
|
43
|
+
},
|
|
38
44
|
"inboundTargets": {
|
|
39
45
|
"type": "array",
|
|
40
46
|
"items": {
|
|
@@ -82,6 +88,12 @@
|
|
|
82
88
|
"type": "string",
|
|
83
89
|
"default": "openclaw-mesh"
|
|
84
90
|
},
|
|
91
|
+
"announceLogDetail": {
|
|
92
|
+
"type": "string",
|
|
93
|
+
"enum": ["off", "summary", "payload"],
|
|
94
|
+
"default": "summary",
|
|
95
|
+
"description": "Controls instance announce logging detail."
|
|
96
|
+
},
|
|
85
97
|
"enablePubsub": {
|
|
86
98
|
"type": "boolean",
|
|
87
99
|
"default": true
|
package/package.json
CHANGED
package/src/debug-cli.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { OpenClawConfig, OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
|
2
|
+
import type { OpenClawPluginCliContext } from "openclaw/plugin-sdk/plugin-runtime";
|
|
3
|
+
import {
|
|
4
|
+
createReadlinePrompter,
|
|
5
|
+
LIBP2P_MESH_CLI_REGISTRATION,
|
|
6
|
+
type ClosableSetupPrompter,
|
|
7
|
+
} from "./setup-cli.js";
|
|
8
|
+
import {
|
|
9
|
+
applyAnnounceLogDetail,
|
|
10
|
+
getAnnounceLogDetail,
|
|
11
|
+
type OpenClawConfigLike,
|
|
12
|
+
} from "./setup-config.js";
|
|
13
|
+
import type { SetupPrompter } from "./setup-wizard.js";
|
|
14
|
+
import { runDebugWizard } from "./debug-wizard.js";
|
|
15
|
+
import type { AnnounceLogDetail } from "./types.js";
|
|
16
|
+
|
|
17
|
+
const DEBUG_CLI_AFTER_WRITE = {
|
|
18
|
+
mode: "none",
|
|
19
|
+
reason: "libp2p-mesh debug config updated; restart manually to apply gateway changes.",
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
type CliRootCommand = {
|
|
23
|
+
command(name: string): {
|
|
24
|
+
description(text: string): {
|
|
25
|
+
action(handler: () => Promise<void>): void;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type DebugConfigWriter = {
|
|
31
|
+
saveAnnounceLogDetail(detail: AnnounceLogDetail): Promise<void>;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type DebugCliDeps = {
|
|
35
|
+
createPrompter?: (ctx: OpenClawPluginCliContext) => SetupPrompter;
|
|
36
|
+
createWriter?: (api: OpenClawPluginApi) => DebugConfigWriter;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export function registerLibp2pMeshDebugCli(api: OpenClawPluginApi, deps: DebugCliDeps = {}): void {
|
|
40
|
+
api.registerCli((ctx) => {
|
|
41
|
+
const root = ctx.program
|
|
42
|
+
.command("libp2p-mesh")
|
|
43
|
+
.description("Configure libp2p-mesh plugin.");
|
|
44
|
+
|
|
45
|
+
registerLibp2pMeshDebugCommand(root, api, ctx, deps);
|
|
46
|
+
}, LIBP2P_MESH_CLI_REGISTRATION);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function registerLibp2pMeshDebugCommand(
|
|
50
|
+
root: CliRootCommand,
|
|
51
|
+
api: OpenClawPluginApi,
|
|
52
|
+
ctx: OpenClawPluginCliContext,
|
|
53
|
+
deps: DebugCliDeps = {},
|
|
54
|
+
): void {
|
|
55
|
+
root
|
|
56
|
+
.command("debug")
|
|
57
|
+
.description("Manage libp2p-mesh debug logging config.")
|
|
58
|
+
.action(async () => {
|
|
59
|
+
const prompter = (deps.createPrompter?.(ctx) ?? createReadlinePrompter()) as ClosableSetupPrompter;
|
|
60
|
+
const writer = deps.createWriter?.(api) ?? createOpenClawDebugConfigWriter(api);
|
|
61
|
+
try {
|
|
62
|
+
const result = await runDebugWizard({
|
|
63
|
+
prompter,
|
|
64
|
+
current: getAnnounceLogDetail(ctx.config as OpenClawConfigLike),
|
|
65
|
+
writer,
|
|
66
|
+
});
|
|
67
|
+
prompter.print(result.message);
|
|
68
|
+
} finally {
|
|
69
|
+
prompter.close?.();
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function createOpenClawDebugConfigWriter(api: OpenClawPluginApi): DebugConfigWriter {
|
|
75
|
+
return {
|
|
76
|
+
async saveAnnounceLogDetail(detail) {
|
|
77
|
+
await api.runtime.config.mutateConfigFile({
|
|
78
|
+
afterWrite: DEBUG_CLI_AFTER_WRITE,
|
|
79
|
+
mutate(draft) {
|
|
80
|
+
const nextConfig = applyAnnounceLogDetail(draft as OpenClawConfigLike, detail);
|
|
81
|
+
replaceConfig(draft as OpenClawConfig, nextConfig as OpenClawConfig);
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function replaceConfig(draft: OpenClawConfig, nextConfig: OpenClawConfig): void {
|
|
89
|
+
for (const key of Object.keys(draft) as Array<keyof OpenClawConfig>) {
|
|
90
|
+
delete draft[key];
|
|
91
|
+
}
|
|
92
|
+
Object.assign(draft, structuredClone(nextConfig));
|
|
93
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { SetupCancelledError, type SetupPrompter } from "./setup-wizard.js";
|
|
2
|
+
import type { AnnounceLogDetail } from "./types.js";
|
|
3
|
+
|
|
4
|
+
const CANCELLED_MESSAGE = "Debug configuration cancelled. No changes were written.";
|
|
5
|
+
const SAVED_MESSAGE = "Debug config updated.\n\nRestart the gateway to apply changes:\nopenclaw gateway restart";
|
|
6
|
+
|
|
7
|
+
export type DebugPromptChoice = AnnounceLogDetail | "cancel";
|
|
8
|
+
|
|
9
|
+
export type RunDebugWizardOptions = {
|
|
10
|
+
prompter: SetupPrompter;
|
|
11
|
+
current: AnnounceLogDetail;
|
|
12
|
+
writer: {
|
|
13
|
+
saveAnnounceLogDetail(detail: AnnounceLogDetail): Promise<void>;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type DebugWizardResult =
|
|
18
|
+
| { status: "saved"; announceLogDetail: AnnounceLogDetail; message: string }
|
|
19
|
+
| { status: "cancelled"; message: string };
|
|
20
|
+
|
|
21
|
+
export async function runDebugWizard(options: RunDebugWizardOptions): Promise<DebugWizardResult> {
|
|
22
|
+
try {
|
|
23
|
+
options.prompter.print(`Current announceLogDetail: ${options.current}`);
|
|
24
|
+
const selected = await options.prompter.select<DebugPromptChoice>("Set announceLogDetail:", [
|
|
25
|
+
{ label: "summary: log peer, instance, address and attribute counts", value: "summary" },
|
|
26
|
+
{ label: "off: disable announce summary/payload logs", value: "off" },
|
|
27
|
+
{ label: "payload: log full announce JSON", value: "payload" },
|
|
28
|
+
{ label: "Cancel", value: "cancel" },
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
if (selected === "cancel") {
|
|
32
|
+
return cancelledResult();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (selected === "payload") {
|
|
36
|
+
const confirmed = await options.prompter.confirm(
|
|
37
|
+
"Full announce payload logs may include userPublicAttributes, multiaddrs, pubkey and instance identity. Enable payload logs?",
|
|
38
|
+
false,
|
|
39
|
+
);
|
|
40
|
+
if (!confirmed) {
|
|
41
|
+
return cancelledResult();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
await options.writer.saveAnnounceLogDetail(selected);
|
|
46
|
+
return {
|
|
47
|
+
status: "saved",
|
|
48
|
+
announceLogDetail: selected,
|
|
49
|
+
message: SAVED_MESSAGE,
|
|
50
|
+
};
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (error instanceof SetupCancelledError) {
|
|
53
|
+
return cancelledResult();
|
|
54
|
+
}
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function cancelledResult(): DebugWizardResult {
|
|
60
|
+
return {
|
|
61
|
+
status: "cancelled",
|
|
62
|
+
message: CANCELLED_MESSAGE,
|
|
63
|
+
};
|
|
64
|
+
}
|
package/src/instance-router.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
AnnounceLogDetail,
|
|
2
3
|
DeliveryAckPayload,
|
|
3
4
|
DeliveryTargetResult,
|
|
4
5
|
InboundTargetConfig,
|
|
@@ -50,6 +51,20 @@ function summarizeError(error: unknown): string {
|
|
|
50
51
|
return error instanceof Error ? error.message : String(error);
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
function effectiveAnnounceLogDetail(value: unknown): AnnounceLogDetail {
|
|
55
|
+
if (value === "off" || value === "payload") {
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return "summary";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function countAnnounceAttributes(payload: InstanceAnnouncePayload): number {
|
|
63
|
+
return Array.isArray(payload.userPublicAttributes)
|
|
64
|
+
? payload.userPublicAttributes.length
|
|
65
|
+
: 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
53
68
|
function describeAttributeMatch(match: UserAttributeMatch): string {
|
|
54
69
|
if (match.kind === "tag") {
|
|
55
70
|
return `tag ${match.value}`;
|
|
@@ -149,6 +164,7 @@ export function createInstanceRouter(options: InstanceRouterOptions): InstanceRo
|
|
|
149
164
|
const { mesh, store, delivery } = options;
|
|
150
165
|
const config = options.config ?? {};
|
|
151
166
|
const logger = options.logger;
|
|
167
|
+
const announceLogDetail = effectiveAnnounceLogDetail(config.announceLogDetail);
|
|
152
168
|
const ackTimeoutMs = config.deliveryAckTimeoutMs ?? 15000;
|
|
153
169
|
const announcedPeers = new Set<string>();
|
|
154
170
|
const pendingAcks = new Map<string, PendingAck>();
|
|
@@ -212,9 +228,46 @@ export function createInstanceRouter(options: InstanceRouterOptions): InstanceRo
|
|
|
212
228
|
payload: JSON.stringify(payload),
|
|
213
229
|
});
|
|
214
230
|
announcedPeers.add(peerId);
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
231
|
+
logAnnounce("Sent", peerId, payload);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function announceSummary(
|
|
235
|
+
direction: "Sent" | "Received",
|
|
236
|
+
peerId: string,
|
|
237
|
+
payload: InstanceAnnouncePayload,
|
|
238
|
+
changed?: boolean,
|
|
239
|
+
): string {
|
|
240
|
+
const changedDetail = changed === undefined ? "" : ` changed=${changed}`;
|
|
241
|
+
return `[libp2p-mesh] ${direction} instance announce peer=${peerId} instance=${payload.instanceId} addrs=${payload.multiaddrs.length} attrs=${countAnnounceAttributes(payload)}${changedDetail}`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function logAnnounce(
|
|
245
|
+
direction: "Sent" | "Received",
|
|
246
|
+
peerId: string,
|
|
247
|
+
payload: InstanceAnnouncePayload,
|
|
248
|
+
changed?: boolean,
|
|
249
|
+
): void {
|
|
250
|
+
if (announceLogDetail === "off") {
|
|
251
|
+
if (direction === "Sent") {
|
|
252
|
+
logger?.info?.(
|
|
253
|
+
`[libp2p-mesh] Sent instance announce to ${peerId} (${payload.instanceId})`,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
logger?.info?.(announceSummary(direction, peerId, payload, changed));
|
|
260
|
+
if (announceLogDetail !== "payload") {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
logger?.debug?.(
|
|
266
|
+
`[libp2p-mesh] ${direction} instance announce payload=${JSON.stringify(payload)}`,
|
|
267
|
+
);
|
|
268
|
+
} catch {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
218
271
|
}
|
|
219
272
|
|
|
220
273
|
async function announceToConnectedPeers(): Promise<void> {
|
|
@@ -255,9 +308,8 @@ export function createInstanceRouter(options: InstanceRouterOptions): InstanceRo
|
|
|
255
308
|
logger?.info?.(
|
|
256
309
|
`[libp2p-mesh] Instance mapping updated: ${payload.instanceId} -> ${payload.peerId}`,
|
|
257
310
|
);
|
|
258
|
-
} else {
|
|
259
|
-
logger?.debug?.(`[libp2p-mesh] Instance mapping unchanged: ${payload.instanceId}`);
|
|
260
311
|
}
|
|
312
|
+
logAnnounce("Received", msg.from, payload, result.changed);
|
|
261
313
|
|
|
262
314
|
if (!announcedPeers.has(msg.from)) {
|
|
263
315
|
await announceToPeer(msg.from).catch((error) => {
|
|
@@ -449,7 +501,11 @@ export function createInstanceRouter(options: InstanceRouterOptions): InstanceRo
|
|
|
449
501
|
}
|
|
450
502
|
}
|
|
451
503
|
|
|
452
|
-
|
|
504
|
+
function attachHandlers(): void {
|
|
505
|
+
if (unsubs.length > 0) {
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
|
|
453
509
|
unsubs.push(
|
|
454
510
|
mesh.onMessage((msg) => {
|
|
455
511
|
handleMessage(msg).catch((error) => {
|
|
@@ -468,7 +524,10 @@ export function createInstanceRouter(options: InstanceRouterOptions): InstanceRo
|
|
|
468
524
|
});
|
|
469
525
|
}),
|
|
470
526
|
);
|
|
527
|
+
}
|
|
471
528
|
|
|
529
|
+
async function start(): Promise<void> {
|
|
530
|
+
attachHandlers();
|
|
472
531
|
await announceToConnectedPeers();
|
|
473
532
|
}
|
|
474
533
|
|
|
@@ -658,6 +717,8 @@ export function createInstanceRouter(options: InstanceRouterOptions): InstanceRo
|
|
|
658
717
|
}
|
|
659
718
|
|
|
660
719
|
return {
|
|
720
|
+
attachHandlers,
|
|
721
|
+
announceToConnectedPeers,
|
|
661
722
|
start,
|
|
662
723
|
stop,
|
|
663
724
|
handleMessage,
|
package/src/plugin.ts
CHANGED
|
@@ -12,13 +12,26 @@ import { registerLibp2pMeshCli } from "./profile-cli.js";
|
|
|
12
12
|
import type { ChannelPlugin } from "openclaw/plugin-sdk/core";
|
|
13
13
|
import type { MeshConfig } from "./types.js";
|
|
14
14
|
|
|
15
|
+
export type Libp2pMeshPluginDeps = {
|
|
16
|
+
createMeshNetwork?: typeof createMeshNetwork;
|
|
17
|
+
createInstanceRouter?: typeof createInstanceRouter;
|
|
18
|
+
};
|
|
19
|
+
|
|
15
20
|
export function registerLibp2pMesh(api: OpenClawPluginApi) {
|
|
21
|
+
registerLibp2pMeshWithDeps(api);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function registerLibp2pMeshWithDeps(
|
|
25
|
+
api: OpenClawPluginApi,
|
|
26
|
+
deps: Libp2pMeshPluginDeps = {},
|
|
27
|
+
) {
|
|
16
28
|
registerLibp2pMeshCli(api);
|
|
17
29
|
|
|
18
30
|
const config = api.pluginConfig as MeshConfig | undefined;
|
|
19
31
|
let unsubscribeInbound: (() => void) | undefined;
|
|
20
32
|
let serviceStarted = false;
|
|
21
|
-
|
|
33
|
+
let startPromise: Promise<void> | undefined;
|
|
34
|
+
const mesh = (deps.createMeshNetwork ?? createMeshNetwork)({
|
|
22
35
|
config,
|
|
23
36
|
logger: api.logger,
|
|
24
37
|
});
|
|
@@ -39,7 +52,7 @@ export function registerLibp2pMesh(api: OpenClawPluginApi) {
|
|
|
39
52
|
},
|
|
40
53
|
logger: api.logger,
|
|
41
54
|
});
|
|
42
|
-
const router = createInstanceRouter({
|
|
55
|
+
const router = (deps.createInstanceRouter ?? createInstanceRouter)({
|
|
43
56
|
mesh,
|
|
44
57
|
store,
|
|
45
58
|
delivery,
|
|
@@ -51,6 +64,73 @@ export function registerLibp2pMesh(api: OpenClawPluginApi) {
|
|
|
51
64
|
|
|
52
65
|
const channel = createLibp2pMeshChannel(mesh);
|
|
53
66
|
|
|
67
|
+
function attachInboundHandlers(): () => void {
|
|
68
|
+
return mesh.onMessage((msg) => {
|
|
69
|
+
if (msg.type === "direct" || msg.type === "broadcast") {
|
|
70
|
+
const sendToChannel: InboundHandlerDeps["sendToChannel"] = async (_channelId, _target, text) => {
|
|
71
|
+
if (!config?.inboundChannel || !config?.inboundTarget) {
|
|
72
|
+
api.logger.warn?.(
|
|
73
|
+
"[libp2p-mesh] inboundChannel/inboundTarget not configured; direct message logged only.",
|
|
74
|
+
);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const result = await delivery.deliver({
|
|
79
|
+
channel: config.inboundChannel,
|
|
80
|
+
target: config.inboundTarget,
|
|
81
|
+
text,
|
|
82
|
+
metadata: {
|
|
83
|
+
fromInstanceId: msg.instanceId ?? msg.from,
|
|
84
|
+
fromPeerId: msg.from,
|
|
85
|
+
p2pMessageId: msg.id,
|
|
86
|
+
allowAgentAutoReply: false,
|
|
87
|
+
replyToInstanceId: msg.instanceId ?? msg.from,
|
|
88
|
+
replyTool: "p2p_send_instance_message",
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
if (!result.ok) {
|
|
92
|
+
api.logger.error?.(
|
|
93
|
+
`[libp2p-mesh] Failed to forward direct message from ${msg.from}: ${result.error}`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
handleP2PInbound(msg, { logger: api.logger, sendToChannel });
|
|
98
|
+
} else if (msg.type === "agent-sync") {
|
|
99
|
+
handleP2PInbound(msg, { logger: api.logger });
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function cleanupStartupFailure(): Promise<void> {
|
|
105
|
+
try {
|
|
106
|
+
unsubscribeInbound?.();
|
|
107
|
+
} catch (error) {
|
|
108
|
+
api.logger.warn?.(
|
|
109
|
+
`[libp2p-mesh] Failed to clean inbound handler after startup failure: ${String(error)}`,
|
|
110
|
+
);
|
|
111
|
+
} finally {
|
|
112
|
+
unsubscribeInbound = undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
await router.stop();
|
|
117
|
+
} catch (error) {
|
|
118
|
+
api.logger.warn?.(
|
|
119
|
+
`[libp2p-mesh] Failed to stop router after startup failure: ${String(error)}`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
await mesh.stop();
|
|
125
|
+
} catch (error) {
|
|
126
|
+
api.logger.warn?.(
|
|
127
|
+
`[libp2p-mesh] Failed to stop mesh after startup failure: ${String(error)}`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
serviceStarted = false;
|
|
132
|
+
}
|
|
133
|
+
|
|
54
134
|
// 1. Register Service (manages libp2p node lifecycle)
|
|
55
135
|
api.registerService({
|
|
56
136
|
id: "libp2p-mesh",
|
|
@@ -59,64 +139,52 @@ export function registerLibp2pMesh(api: OpenClawPluginApi) {
|
|
|
59
139
|
api.logger.debug?.("[libp2p-mesh] Service already started; ignoring duplicate start.");
|
|
60
140
|
return;
|
|
61
141
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const sendToChannel: InboundHandlerDeps["sendToChannel"] = async (_channelId, _target, text) => {
|
|
67
|
-
if (!config?.inboundChannel || !config?.inboundTarget) {
|
|
68
|
-
api.logger.warn?.(
|
|
69
|
-
"[libp2p-mesh] inboundChannel/inboundTarget not configured; direct message logged only.",
|
|
70
|
-
);
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const result = await delivery.deliver({
|
|
75
|
-
channel: config.inboundChannel,
|
|
76
|
-
target: config.inboundTarget,
|
|
77
|
-
text,
|
|
78
|
-
metadata: {
|
|
79
|
-
fromInstanceId: msg.instanceId ?? msg.from,
|
|
80
|
-
fromPeerId: msg.from,
|
|
81
|
-
p2pMessageId: msg.id,
|
|
82
|
-
allowAgentAutoReply: false,
|
|
83
|
-
replyToInstanceId: msg.instanceId ?? msg.from,
|
|
84
|
-
replyTool: "p2p_send_instance_message",
|
|
85
|
-
},
|
|
86
|
-
});
|
|
87
|
-
if (!result.ok) {
|
|
88
|
-
api.logger.error?.(
|
|
89
|
-
`[libp2p-mesh] Failed to forward direct message from ${msg.from}: ${result.error}`,
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
};
|
|
93
|
-
handleP2PInbound(msg, { logger: api.logger, sendToChannel });
|
|
94
|
-
} else if (msg.type === "agent-sync") {
|
|
95
|
-
handleP2PInbound(msg, { logger: api.logger });
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
const identity = mesh.getInstanceIdentity();
|
|
99
|
-
api.logger.info?.(`[libp2p-mesh] Service started. Peer ID: ${mesh.getLocalPeerId()}`);
|
|
100
|
-
if (identity) {
|
|
101
|
-
api.logger.info?.(`[libp2p-mesh] Instance Identity: ${identity.id}`);
|
|
102
|
-
}
|
|
103
|
-
const nat = mesh.getNATStatus();
|
|
104
|
-
const enabledNames = Object.entries(nat.enabled)
|
|
105
|
-
.filter(([, on]) => on)
|
|
106
|
-
.map(([k]) => k);
|
|
107
|
-
if (enabledNames.length > 0) {
|
|
108
|
-
api.logger.info?.(
|
|
109
|
-
`[libp2p-mesh] NAT traversal services: ${enabledNames.join(", ")}`,
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
if (nat.reservedRelays.length > 0) {
|
|
113
|
-
api.logger.info?.(
|
|
114
|
-
`[libp2p-mesh] Active relay reservations: ${nat.reservedRelays.join(", ")}`,
|
|
115
|
-
);
|
|
142
|
+
|
|
143
|
+
if (startPromise) {
|
|
144
|
+
api.logger.debug?.("[libp2p-mesh] Service start already in progress; waiting for it.");
|
|
145
|
+
return startPromise;
|
|
116
146
|
}
|
|
117
|
-
|
|
147
|
+
|
|
148
|
+
startPromise = (async () => {
|
|
149
|
+
try {
|
|
150
|
+
router.attachHandlers();
|
|
151
|
+
unsubscribeInbound = attachInboundHandlers();
|
|
152
|
+
await mesh.start();
|
|
153
|
+
await router.announceToConnectedPeers();
|
|
154
|
+
const identity = mesh.getInstanceIdentity();
|
|
155
|
+
api.logger.info?.(
|
|
156
|
+
`[libp2p-mesh] Service started. Peer ID: ${mesh.getLocalPeerId()}`,
|
|
157
|
+
);
|
|
158
|
+
if (identity) {
|
|
159
|
+
api.logger.info?.(`[libp2p-mesh] Instance Identity: ${identity.id}`);
|
|
160
|
+
}
|
|
161
|
+
const nat = mesh.getNATStatus();
|
|
162
|
+
const enabledNames = Object.entries(nat.enabled)
|
|
163
|
+
.filter(([, on]) => on)
|
|
164
|
+
.map(([k]) => k);
|
|
165
|
+
if (enabledNames.length > 0) {
|
|
166
|
+
api.logger.info?.(
|
|
167
|
+
`[libp2p-mesh] NAT traversal services: ${enabledNames.join(", ")}`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
if (nat.reservedRelays.length > 0) {
|
|
171
|
+
api.logger.info?.(
|
|
172
|
+
`[libp2p-mesh] Active relay reservations: ${nat.reservedRelays.join(", ")}`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
serviceStarted = true;
|
|
176
|
+
} catch (error) {
|
|
177
|
+
await cleanupStartupFailure();
|
|
178
|
+
throw error;
|
|
179
|
+
} finally {
|
|
180
|
+
startPromise = undefined;
|
|
181
|
+
}
|
|
182
|
+
})();
|
|
183
|
+
|
|
184
|
+
return startPromise;
|
|
118
185
|
},
|
|
119
186
|
stop: async () => {
|
|
187
|
+
await startPromise?.catch(() => undefined);
|
|
120
188
|
unsubscribeInbound?.();
|
|
121
189
|
unsubscribeInbound = undefined;
|
|
122
190
|
await router.stop();
|
package/src/profile-cli.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
type ClosableSetupPrompter,
|
|
8
8
|
type SetupCliDeps,
|
|
9
9
|
} from "./setup-cli.js";
|
|
10
|
+
import { registerLibp2pMeshDebugCommand, type DebugCliDeps } from "./debug-cli.js";
|
|
10
11
|
import { runProfileWizard } from "./profile-wizard.js";
|
|
11
12
|
import { createUserMdAttributeSource } from "./user-md-attributes.js";
|
|
12
13
|
import { createUserProfileStore, type UserProfileStore } from "./user-profile-store.js";
|
|
@@ -29,6 +30,7 @@ export type ProfileCliDeps = {
|
|
|
29
30
|
export type Libp2pMeshCliDeps = {
|
|
30
31
|
setup?: SetupCliDeps;
|
|
31
32
|
profile?: ProfileCliDeps;
|
|
33
|
+
debug?: DebugCliDeps;
|
|
32
34
|
};
|
|
33
35
|
|
|
34
36
|
export function registerLibp2pMeshCli(api: OpenClawPluginApi, deps: Libp2pMeshCliDeps = {}): void {
|
|
@@ -39,6 +41,7 @@ export function registerLibp2pMeshCli(api: OpenClawPluginApi, deps: Libp2pMeshCl
|
|
|
39
41
|
|
|
40
42
|
registerLibp2pMeshSetupCommand(root, api, ctx, deps.setup);
|
|
41
43
|
registerLibp2pMeshProfileCommand(root, api, ctx, deps.profile);
|
|
44
|
+
registerLibp2pMeshDebugCommand(root, api, ctx, deps.debug);
|
|
42
45
|
}, LIBP2P_MESH_CLI_REGISTRATION);
|
|
43
46
|
}
|
|
44
47
|
|
package/src/setup-config.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { InboundTargetConfig, MeshConfig } from "./types.js";
|
|
1
|
+
import type { AnnounceLogDetail, InboundTargetConfig, MeshConfig } from "./types.js";
|
|
2
2
|
|
|
3
3
|
export const LIBP2P_MESH_PLUGIN_ID = "libp2p-mesh";
|
|
4
4
|
export const DEFAULT_DELIVERY_ACK_TIMEOUT_MS = 15000;
|
|
@@ -38,6 +38,14 @@ export function getLibp2pMeshConfig(config: OpenClawConfigLike): MeshConfig | un
|
|
|
38
38
|
return config.plugins?.entries?.[LIBP2P_MESH_PLUGIN_ID]?.config as MeshConfig | undefined;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
export function getAnnounceLogDetail(config: OpenClawConfigLike): AnnounceLogDetail {
|
|
42
|
+
return normalizeAnnounceLogDetail(getLibp2pMeshConfig(config)?.announceLogDetail);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function normalizeAnnounceLogDetail(value: unknown): AnnounceLogDetail {
|
|
46
|
+
return value === "off" || value === "payload" || value === "summary" ? value : "summary";
|
|
47
|
+
}
|
|
48
|
+
|
|
41
49
|
export function buildNetworkConfig(
|
|
42
50
|
mode: SetupMode,
|
|
43
51
|
options?: {
|
|
@@ -99,6 +107,32 @@ export function applyPluginConfig(config: OpenClawConfigLike, pluginConfig: Mesh
|
|
|
99
107
|
};
|
|
100
108
|
}
|
|
101
109
|
|
|
110
|
+
export function applyAnnounceLogDetail(
|
|
111
|
+
config: OpenClawConfigLike,
|
|
112
|
+
announceLogDetail: AnnounceLogDetail,
|
|
113
|
+
): OpenClawConfigLike {
|
|
114
|
+
const existingEntry = config.plugins?.entries?.[LIBP2P_MESH_PLUGIN_ID];
|
|
115
|
+
const existingPluginConfig = existingEntry?.config ?? {};
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
...config,
|
|
119
|
+
plugins: {
|
|
120
|
+
...config.plugins,
|
|
121
|
+
entries: {
|
|
122
|
+
...config.plugins?.entries,
|
|
123
|
+
[LIBP2P_MESH_PLUGIN_ID]: {
|
|
124
|
+
...existingEntry,
|
|
125
|
+
enabled: existingEntry?.enabled ?? true,
|
|
126
|
+
config: {
|
|
127
|
+
...existingPluginConfig,
|
|
128
|
+
announceLogDetail,
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
102
136
|
export function mergeNetworkConfig(existing: MeshConfig | undefined, networkConfig: MeshConfig): MeshConfig {
|
|
103
137
|
if (!existing) {
|
|
104
138
|
return { ...networkConfig };
|
package/src/types.ts
CHANGED
|
@@ -203,6 +203,8 @@ export type InstanceRouterOptions = {
|
|
|
203
203
|
};
|
|
204
204
|
|
|
205
205
|
export interface InstanceRouter {
|
|
206
|
+
attachHandlers(): void;
|
|
207
|
+
announceToConnectedPeers(): Promise<void>;
|
|
206
208
|
start(): Promise<void>;
|
|
207
209
|
stop(): Promise<void>;
|
|
208
210
|
handleMessage(msg: P2PMessage): Promise<void>;
|
|
@@ -232,6 +234,7 @@ export interface MeshConfig {
|
|
|
232
234
|
discovery?: "mdns" | "bootstrap" | "dht";
|
|
233
235
|
bootstrapList?: string[];
|
|
234
236
|
meshTopic?: string;
|
|
237
|
+
announceLogDetail?: AnnounceLogDetail;
|
|
235
238
|
enableAgentSync?: boolean;
|
|
236
239
|
enableWebSocket?: boolean;
|
|
237
240
|
peerIdPath?: string;
|
|
@@ -335,6 +338,8 @@ export interface MeshNetwork {
|
|
|
335
338
|
getNATStatus(): NATTraversalStatus;
|
|
336
339
|
}
|
|
337
340
|
|
|
341
|
+
export type AnnounceLogDetail = "off" | "summary" | "payload";
|
|
342
|
+
|
|
338
343
|
export type MeshAccount = {
|
|
339
344
|
accountId: string;
|
|
340
345
|
configured: boolean;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
|
|
4
5
|
import type { UserPublicAttribute } from "./types.js";
|
|
@@ -59,6 +60,19 @@ export type UserMdAttributeSource = {
|
|
|
59
60
|
};
|
|
60
61
|
};
|
|
61
62
|
|
|
63
|
+
export function resolveUserMdPath(customPath?: string): string {
|
|
64
|
+
if (customPath) {
|
|
65
|
+
return customPath;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const stateDir = process.env.OPENCLAW_STATE_DIR;
|
|
69
|
+
if (stateDir) {
|
|
70
|
+
return path.join(stateDir, "workspace", "USER.md");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return path.join(homedir(), ".openclaw", "workspace", "USER.md");
|
|
74
|
+
}
|
|
75
|
+
|
|
62
76
|
function isTemplateText(value: string): boolean {
|
|
63
77
|
const trimmed = value.trim();
|
|
64
78
|
return trimmed.length === 0 || TEMPLATE_PATTERN.test(trimmed) || /^\[[^\]]+\]$/.test(trimmed);
|
|
@@ -236,7 +250,7 @@ export function extractUserMdTags(markdown: string): UserPublicAttribute[] {
|
|
|
236
250
|
export function createUserMdAttributeSource(options?: UserMdAttributeSource): {
|
|
237
251
|
loadTags(): Promise<UserPublicAttribute[]>;
|
|
238
252
|
} {
|
|
239
|
-
const filePath = options?.path
|
|
253
|
+
const filePath = resolveUserMdPath(options?.path);
|
|
240
254
|
const logger = options?.logger;
|
|
241
255
|
|
|
242
256
|
return {
|