libp2p-mesh 2026.5.15 → 2026.5.17

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 CHANGED
@@ -42,7 +42,9 @@ openclaw plugins registry --refresh
42
42
 
43
43
  The published npm package includes compiled JavaScript under `dist/`, so OpenClaw and acpx can load it directly.
44
44
 
45
- Then add to your `~/.openclaw/openclaw.json`:
45
+ After installing the plugin, you must:
46
+
47
+ 1. **Enable the plugin** in `~/.openclaw/openclaw.json`:
46
48
 
47
49
  ```json
48
50
  {
@@ -62,6 +64,78 @@ Then add to your `~/.openclaw/openclaw.json`:
62
64
  }
63
65
  ```
64
66
 
67
+ 2. **Set `tools.profile` to `full`** — OpenClaw defaults to `"coding"` profile, which **filters out** P2P tools. Even if you add them to `tools.allow`, the coding profile will still block them. You must change to `"full"` first:
68
+
69
+ ```json
70
+ {
71
+ "tools": {
72
+ "profile": "full",
73
+ "allow": [
74
+ "p2p_list_peers",
75
+ "p2p_send_message",
76
+ "p2p_broadcast"
77
+ ]
78
+ }
79
+ }
80
+ ```
81
+
82
+ > **Why `profile: "full"`?** The `"coding"` profile hardcodes a deny list that includes `p2p_list_peers`, `p2p_send_message`, and `p2p_broadcast`. With `profile: "coding"`, adding tools to `tools.allow` has no effect for these three — the profile filter runs first and removes them.
83
+
84
+ 3. **Restart the gateway:**
85
+
86
+ ```bash
87
+ openclaw gateway restart
88
+ ```
89
+
90
+ 4. **Verify tool registration:**
91
+
92
+ ```bash
93
+ openclaw plugins inspect libp2p-mesh --runtime --json | jq '.plugin.toolNames, .tools'
94
+ ```
95
+
96
+ Expected output:
97
+ ```json
98
+ [
99
+ "p2p_send_message",
100
+ "p2p_broadcast",
101
+ "p2p_list_peers",
102
+ "p2p_get_instance_identity",
103
+ "p2p_get_network_info"
104
+ ]
105
+ ```
106
+
107
+ ### One-command setup (optional)
108
+
109
+ Instead of manually editing `openclaw.json`, run:
110
+
111
+ ```bash
112
+ npx openclaw-libp2p-mesh-configure-tools
113
+ # or, from the plugin directory:
114
+ npm run configure-tools
115
+ ```
116
+
117
+ This patches `~/.openclaw/openclaw.json` with the correct `tools.profile` and `tools.allow` values. Use `--check` to preview without modifying:
118
+
119
+ ```bash
120
+ npm run configure-tools:check
121
+ ```
122
+
123
+ ## Troubleshooting
124
+
125
+ ### "plugin must declare contracts.tools before registering agent tools"
126
+
127
+ Make sure you are running **OpenClaw >= 2026.6.5** and the plugin is installed via `openclaw install` or `npm install` into `~/.openclaw/npm`. The manifest (`openclaw.plugin.json`) ships with `contracts.tools` pre-declared, so no manual manifest edits are needed.
128
+
129
+ ### Tools still not available after setup
130
+
131
+ 1. Confirm `tools.profile` is `"full"` (not `"coding"`):
132
+ ```bash
133
+ cat ~/.openclaw/openclaw.json | jq '.tools'
134
+ ```
135
+ 2. Confirm all three P2P tools are in `tools.allow`.
136
+ 3. Restart the gateway (not just reload).
137
+ 4. Check `openclaw plugins inspect libp2p-mesh --runtime --json` for diagnostics.
138
+
65
139
  ## Configuration
66
140
 
67
141
  Add a `libp2p-mesh` block to your `openclaw.json` under `plugins`:
package/dist/index.d.ts CHANGED
@@ -1,9 +1,3 @@
1
- import type { OpenClawPluginConfigSchema } from "openclaw/plugin-sdk/core";
2
- declare const _default: {
3
- id: string;
4
- name: string;
5
- description: string;
6
- configSchema: OpenClawPluginConfigSchema;
7
- register: NonNullable<import("openclaw/plugin-sdk/core").OpenClawPluginDefinition["register"]>;
8
- } & Pick<import("openclaw/plugin-sdk/core").OpenClawPluginDefinition, "reload" | "kind" | "nodeHostCommands" | "securityAuditCollectors">;
1
+ import { definePluginEntry } from "openclaw/plugin-sdk/core";
2
+ declare const _default: ReturnType<typeof definePluginEntry>;
9
3
  export default _default;
@@ -214,4 +214,31 @@ export declare function buildP2PTools(mesh: MeshNetwork): ({
214
214
  };
215
215
  isError: boolean;
216
216
  }>;
217
+ } | {
218
+ name: string;
219
+ label: string;
220
+ description: string;
221
+ parameters: {
222
+ type: "object";
223
+ properties: {
224
+ peerId?: undefined;
225
+ message?: undefined;
226
+ topic?: undefined;
227
+ };
228
+ required?: undefined;
229
+ };
230
+ execute(_toolCallId: string, _params: {}, _ctx: any): Promise<{
231
+ content: {
232
+ type: "text";
233
+ text: string;
234
+ }[];
235
+ details: {
236
+ localPeerId: string;
237
+ connectedPeers: string[];
238
+ knownIdentities: {
239
+ peerId: string;
240
+ identity: any;
241
+ }[];
242
+ };
243
+ }>;
217
244
  })[];
@@ -191,5 +191,24 @@ export function buildP2PTools(mesh) {
191
191
  }
192
192
  },
193
193
  },
194
+ {
195
+ name: "p2p_relay_status",
196
+ label: "P2P Relay Status",
197
+ description: "Get current P2P mesh connection status, known peers, and identity mapping.",
198
+ parameters: { type: "object", properties: {} },
199
+ async execute(_toolCallId, _params, _ctx) {
200
+ const peerIdentityMap = this.peerIdentityMap;
201
+ const localPeerId = mesh.getLocalPeerId();
202
+ const connectedPeers = mesh.getConnectedPeers();
203
+ const knownIdentities = connectedPeers
204
+ .map((p) => ({ peerId: p, identity: peerIdentityMap?.resolve(p) }))
205
+ .filter((entry) => entry.identity !== undefined);
206
+ const text = `P2P Mesh Status:\nLocal Peer ID: ${localPeerId}\nConnected Peers: ${connectedPeers.length}\nKnown Identities: ${knownIdentities.length}`;
207
+ return {
208
+ content: [{ type: "text", text }],
209
+ details: { localPeerId, connectedPeers, knownIdentities },
210
+ };
211
+ },
212
+ },
194
213
  ];
195
214
  }
@@ -0,0 +1,12 @@
1
+ import type { P2PMessage } from "./types.js";
2
+ import type { PeerIdentityMap } from "./peer-identity.ts";
3
+ export interface IdentityExchangeDeps {
4
+ peerIdentityMap: PeerIdentityMap;
5
+ localPeerId: string;
6
+ localAgentId: string;
7
+ localChannel: string;
8
+ localAccountId: string;
9
+ send: (peerId: string, message: P2PMessage) => Promise<void>;
10
+ }
11
+ export declare function buildIdentityMessage(peerId: string, agentId: string, channel: string, accountId: string): P2PMessage;
12
+ export declare function handleIdentityMessage(msg: P2PMessage, deps: IdentityExchangeDeps): Promise<void>;
@@ -0,0 +1,35 @@
1
+ export function buildIdentityMessage(peerId, agentId, channel, accountId) {
2
+ return {
3
+ id: crypto.randomUUID(),
4
+ type: "identity",
5
+ from: peerId,
6
+ payload: JSON.stringify({ agentId, channel, accountId }),
7
+ timestamp: Date.now(),
8
+ };
9
+ }
10
+ export async function handleIdentityMessage(msg, deps) {
11
+ // Parse remote identity payload
12
+ let parsed = {};
13
+ try {
14
+ parsed = JSON.parse(msg.payload);
15
+ }
16
+ catch {
17
+ // Malformed payload — skip silently
18
+ return;
19
+ }
20
+ const { agentId, channel, accountId } = parsed;
21
+ // Only register if all required fields are present
22
+ if (agentId && channel && accountId) {
23
+ const { buildAgentSessionKey } = await import("openclaw/plugin-sdk/core");
24
+ const sessionKey = buildAgentSessionKey({ agentId, channel, accountId });
25
+ deps.peerIdentityMap.register(msg.from, {
26
+ agentId,
27
+ channel,
28
+ accountId,
29
+ sessionKey,
30
+ });
31
+ }
32
+ // Send local identity back to the remote peer
33
+ const localIdentity = buildIdentityMessage(deps.localPeerId, deps.localAgentId, deps.localChannel, deps.localAccountId);
34
+ await deps.send(msg.from, localIdentity);
35
+ }
@@ -1,10 +1,17 @@
1
1
  import type { P2PMessage } from "./types.js";
2
- export type InboundHandlerDeps = {
2
+ import type { PeerIdentityMap } from "./peer-identity.ts";
3
+ import type { PluginNextTurnInjection } from "openclaw/plugin-sdk/core";
4
+ export interface InboundHandlerDeps {
5
+ peerIdentityMap?: PeerIdentityMap;
6
+ enqueueNextTurnInjection?: (injection: PluginNextTurnInjection) => Promise<{
7
+ enqueued: boolean;
8
+ id: string;
9
+ }>;
3
10
  logger?: {
4
11
  info?: (msg: string) => void;
5
12
  debug?: (msg: string) => void;
6
13
  warn?: (msg: string) => void;
7
14
  error?: (msg: string) => void;
8
15
  };
9
- };
10
- export declare function handleP2PInbound(msg: P2PMessage, deps: InboundHandlerDeps): void;
16
+ }
17
+ export declare function handleP2PInbound(msg: P2PMessage, deps?: InboundHandlerDeps): void;
@@ -1,6 +1,7 @@
1
1
  import { verifyInstanceSignature } from "./instance-id.js";
2
+ import { buildAgentSessionKey } from "openclaw/plugin-sdk/core";
2
3
  export function handleP2PInbound(msg, deps) {
3
- const { logger } = deps;
4
+ const logger = deps?.logger;
4
5
  const instanceTag = msg.instanceId ? ` [instance: ${msg.instanceId}]` : "";
5
6
  const signedTag = msg.signature ? " [signed]" : "";
6
7
  // Verify signature if present
@@ -15,7 +16,14 @@ export function handleP2PInbound(msg, deps) {
15
16
  timestamp: msg.timestamp,
16
17
  instanceId: msg.instanceId,
17
18
  });
18
- const valid = verifyInstanceSignature({ id: msg.instanceId, name: "", pubkey: msg.pubkey, binding: "", bindingComponents: { username: "", hostname: "", platform: "" }, createdAt: 0 }, signedPayload, msg.signature);
19
+ const valid = verifyInstanceSignature({
20
+ id: msg.instanceId,
21
+ name: "",
22
+ pubkey: msg.pubkey,
23
+ binding: "",
24
+ bindingComponents: { username: "", hostname: "", platform: "" },
25
+ createdAt: 0,
26
+ }, signedPayload, msg.signature);
19
27
  if (valid) {
20
28
  logger?.info?.(`[libp2p-mesh] Verified signature from instance ${msg.instanceId}`);
21
29
  }
@@ -26,6 +34,66 @@ export function handleP2PInbound(msg, deps) {
26
34
  else if (msg.signature) {
27
35
  logger?.warn?.(`[libp2p-mesh] Message has signature but no pubkey; cannot verify`);
28
36
  }
37
+ // Relay handling when deps are provided with required fields
38
+ if (deps?.peerIdentityMap && deps?.enqueueNextTurnInjection) {
39
+ if (msg.type === "identity") {
40
+ // Parse remote identity payload and register synchronously
41
+ let parsed = {};
42
+ try {
43
+ parsed = JSON.parse(msg.payload);
44
+ }
45
+ catch {
46
+ // Malformed payload — skip silently
47
+ return;
48
+ }
49
+ const { agentId, channel, accountId } = parsed;
50
+ if (agentId && channel && accountId) {
51
+ const sessionKey = buildAgentSessionKey({ agentId, channel, accountId });
52
+ deps.peerIdentityMap.register(msg.from, {
53
+ agentId,
54
+ channel,
55
+ accountId,
56
+ sessionKey,
57
+ });
58
+ }
59
+ }
60
+ else if (msg.type === "direct") {
61
+ if (!msg.to) {
62
+ logger?.warn?.("[libp2p-mesh] Direct message missing 'to' field");
63
+ }
64
+ else {
65
+ const receiver = deps.peerIdentityMap.resolve(msg.to);
66
+ if (!receiver) {
67
+ logger?.warn?.(`[libp2p-mesh] Unknown recipient peer: ${msg.to}`);
68
+ }
69
+ else {
70
+ const sessionKey = buildAgentSessionKey({
71
+ agentId: receiver.agentId,
72
+ channel: receiver.channel,
73
+ accountId: receiver.accountId,
74
+ });
75
+ const text = `【来自P2P网络的消息】\n来源:${msg.from}\n内容:${msg.payload}\n\n` +
76
+ `请你通过你当前的 channel 将此消息转发给你所服务的用户。转发完成后,请务必调用 p2p_send_message 工具向 ${msg.from} 回复送达确认。`;
77
+ deps.enqueueNextTurnInjection({ sessionKey, text, placement: "prepend_context" });
78
+ }
79
+ }
80
+ }
81
+ else if (msg.type === "broadcast") {
82
+ const topic = msg.topic || "(none)";
83
+ const text = `【来自P2P网络的广播消息】\n话题:${topic}\n来源:${msg.from}\n内容:${msg.payload}\n\n` +
84
+ `请你通过你当前的 channel 将此广播消息转发给你所服务的用户。`;
85
+ // Inject broadcast into each known peer's session
86
+ for (const [, identity] of deps.peerIdentityMap.entries()) {
87
+ const sessionKey = buildAgentSessionKey({
88
+ agentId: identity.agentId,
89
+ channel: identity.channel,
90
+ accountId: identity.accountId,
91
+ });
92
+ deps.enqueueNextTurnInjection({ sessionKey, text, placement: "prepend_context" });
93
+ }
94
+ }
95
+ }
96
+ // Original logging (unchanged)
29
97
  if (msg.type === "broadcast") {
30
98
  logger?.info?.(`[libp2p-mesh] Broadcast from ${msg.from}${instanceTag}${signedTag} on topic ${msg.topic ?? "(none)"}: ${msg.payload}`);
31
99
  }
package/dist/src/mesh.js CHANGED
@@ -214,11 +214,11 @@ export function createMeshNetwork(options) {
214
214
  });
215
215
  state.node.addEventListener("peer:connect", (evt) => {
216
216
  const peerIdStr = evt.detail.toString();
217
- logger?.debug?.(`[libp2p-mesh] Peer connected: ${peerIdStr}`);
217
+ logger?.info?.(`[libp2p-mesh] Peer connected: ${peerIdStr}`);
218
218
  });
219
219
  state.node.addEventListener("peer:disconnect", (evt) => {
220
220
  const peerIdStr = evt.detail.toString();
221
- logger?.debug?.(`[libp2p-mesh] Peer disconnected: ${peerIdStr}`);
221
+ logger?.info?.(`[libp2p-mesh] Peer disconnected: ${peerIdStr}`);
222
222
  });
223
223
  await state.node.handle(PROTOCOL, async ({ stream, connection }) => {
224
224
  try {
@@ -0,0 +1,17 @@
1
+ export interface PeerIdentity {
2
+ agentId: string;
3
+ channel: string;
4
+ accountId: string;
5
+ sessionKey: string;
6
+ }
7
+ export interface PeerIdentityMap {
8
+ register(peerId: string, identity: PeerIdentity): void;
9
+ resolve(peerId: string): PeerIdentity | undefined;
10
+ resolveSessionKey(peerId: string): string | undefined;
11
+ unregister(peerId: string): void;
12
+ setLocalIdentity(peerId: string, identity: PeerIdentity): void;
13
+ getLocalIdentity(): PeerIdentity | undefined;
14
+ hasIdentity(peerId: string): boolean;
15
+ entries(): IterableIterator<[string, PeerIdentity]>;
16
+ }
17
+ export declare function createPeerIdentityMap(): PeerIdentityMap;
@@ -0,0 +1,33 @@
1
+ export function createPeerIdentityMap() {
2
+ const peers = new Map();
3
+ let localPeerId;
4
+ let localIdentity;
5
+ return {
6
+ register(peerId, identity) {
7
+ peers.set(peerId, identity);
8
+ },
9
+ resolve(peerId) {
10
+ return peers.get(peerId);
11
+ },
12
+ resolveSessionKey(peerId) {
13
+ return peers.get(peerId)?.sessionKey;
14
+ },
15
+ unregister(peerId) {
16
+ peers.delete(peerId);
17
+ },
18
+ setLocalIdentity(peerId, identity) {
19
+ localPeerId = peerId;
20
+ localIdentity = identity;
21
+ peers.set(peerId, identity);
22
+ },
23
+ getLocalIdentity() {
24
+ return localIdentity;
25
+ },
26
+ hasIdentity(peerId) {
27
+ return peers.has(peerId);
28
+ },
29
+ entries() {
30
+ return peers.entries();
31
+ },
32
+ };
33
+ }
@@ -2,18 +2,62 @@ import { createLibp2pMeshChannel } from "./channel.js";
2
2
  import { handleP2PInbound } from "./inbound.js";
3
3
  import { createMeshNetwork } from "./mesh.js";
4
4
  import { buildP2PTools } from "./agent-tools.js";
5
+ import { createPeerIdentityMap } from "./peer-identity.js";
6
+ import { buildIdentityMessage } from "./identity-exchange.js";
5
7
  export function registerLibp2pMesh(api) {
6
8
  const mesh = createMeshNetwork({
7
9
  config: api.pluginConfig,
8
10
  logger: api.logger,
9
11
  });
12
+ // Singleton: maps peerId -> { agentId, channel, accountId, sessionKey }
13
+ const peerIdentityMap = createPeerIdentityMap();
14
+ // Helper: build the deps object for the relay-aware inbound handler
15
+ function buildInboundDeps() {
16
+ return {
17
+ peerIdentityMap,
18
+ enqueueNextTurnInjection: (injection) => api.session.workflow.enqueueNextTurnInjection(injection),
19
+ logger: api.logger,
20
+ };
21
+ }
10
22
  // 1. Register Service (manages libp2p node lifecycle)
11
23
  api.registerService({
12
24
  id: "libp2p-mesh",
13
25
  start: async () => {
14
26
  await mesh.start();
27
+ // Register local identity so remote peers can route messages back to us
28
+ const config = api.pluginConfig;
29
+ const relayChannel = config?.relayChannel;
30
+ const relayAccountId = config?.relayAccountId;
31
+ if (relayChannel && relayAccountId) {
32
+ const localPeerId = mesh.getLocalPeerId();
33
+ const { buildAgentSessionKey } = await import("openclaw/plugin-sdk/core");
34
+ const sessionKey = buildAgentSessionKey({
35
+ agentId: api.name,
36
+ channel: relayChannel,
37
+ accountId: relayAccountId,
38
+ });
39
+ peerIdentityMap.setLocalIdentity(localPeerId, {
40
+ agentId: api.name,
41
+ channel: relayChannel,
42
+ accountId: relayAccountId,
43
+ sessionKey,
44
+ });
45
+ api.logger.info?.(`[libp2p-mesh] Local identity registered: agent=${api.name}, channel=${relayChannel}, account=${relayAccountId}`);
46
+ // Announce identity to all currently-connected peers
47
+ const identityMsg = buildIdentityMessage(localPeerId, api.name, relayChannel, relayAccountId);
48
+ const connectedPeers = mesh.getConnectedPeers();
49
+ for (const peerId of connectedPeers) {
50
+ try {
51
+ await mesh.sendToPeer(peerId, JSON.stringify(identityMsg));
52
+ }
53
+ catch {
54
+ // Best-effort; peer may be stale in the connection list
55
+ }
56
+ }
57
+ }
58
+ // Wire up relay-aware message handler
15
59
  mesh.onMessage((msg) => {
16
- handleP2PInbound(msg, { logger: api.logger });
60
+ handleP2PInbound(msg, buildInboundDeps());
17
61
  });
18
62
  const identity = mesh.getInstanceIdentity();
19
63
  api.logger.info?.(`[libp2p-mesh] Service started. Peer ID: ${mesh.getLocalPeerId()}`);
@@ -43,6 +87,7 @@ export function registerLibp2pMesh(api) {
43
87
  // 3. Register Agent Tools
44
88
  const tools = buildP2PTools(mesh);
45
89
  for (const tool of tools) {
90
+ tool.peerIdentityMap = peerIdentityMap;
46
91
  api.registerTool(tool);
47
92
  }
48
93
  // 4. Register Hook (log received messages for observability)
@@ -18,7 +18,7 @@ export interface InstanceIdentity {
18
18
  }
19
19
  export interface P2PMessage {
20
20
  id: string;
21
- type: "direct" | "broadcast" | "agent-sync";
21
+ type: "direct" | "broadcast" | "agent-sync" | "identity";
22
22
  from: string;
23
23
  to?: string;
24
24
  topic?: string;
@@ -81,6 +81,10 @@ export interface MeshConfig {
81
81
  * forward where AutoNAT cannot probe (e.g. behind a cloud LB).
82
82
  */
83
83
  announceAddrs?: string[];
84
+ /** Relay channel this agent is bound to (used for peer identity registration) */
85
+ relayChannel?: string;
86
+ /** Relay account this agent is bound to (used for peer identity registration) */
87
+ relayAccountId?: string;
84
88
  }
85
89
  export interface NATTraversalStatus {
86
90
  /** Which NAT-traversal services were wired in at start() */
package/index.ts CHANGED
@@ -122,4 +122,4 @@ export default definePluginEntry({
122
122
  description: "P2P network for cross-instance agent communication via libp2p.",
123
123
  configSchema: createLibp2pMeshConfigSchema(),
124
124
  register: registerLibp2pMesh,
125
- });
125
+ }) as ReturnType<typeof definePluginEntry>;
@@ -100,6 +100,16 @@
100
100
  "type": "array",
101
101
  "items": { "type": "string" },
102
102
  "description": "Extra multiaddrs to announce to the network on top of auto-detected ones."
103
+ },
104
+ "relayChannel": {
105
+ "type": "string",
106
+ "default": "",
107
+ "description": "P2P message relay channel (e.g. feishu). Required for user-to-user message relay."
108
+ },
109
+ "relayAccountId": {
110
+ "type": "string",
111
+ "default": "default",
112
+ "description": "Account ID for relay channel."
103
113
  }
104
114
  }
105
115
  },
@@ -109,7 +119,8 @@
109
119
  "p2p_broadcast",
110
120
  "p2p_list_peers",
111
121
  "p2p_get_instance_identity",
112
- "p2p_get_network_info"
122
+ "p2p_get_network_info",
123
+ "p2p_relay_status"
113
124
  ]
114
125
  },
115
126
  "uiHints": {
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "libp2p-mesh",
3
- "version": "2026.5.15",
3
+ "version": "2026.5.17",
4
4
  "description": "OpenClaw libp2p P2P mesh network plugin for cross-instance agent communication",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
8
  "scripts": {
9
9
  "build": "tsc -p tsconfig.json",
10
- "prepack": "npm run build"
10
+ "prepack": "npm run build",
11
+ "configure-tools": "node scripts/configure-tools.js",
12
+ "configure-tools:check": "node scripts/configure-tools.js --check"
11
13
  },
12
14
  "files": [
13
15
  "dist",
@@ -63,7 +65,7 @@
63
65
  "systemImage": "network"
64
66
  },
65
67
  "install": {
66
- "npmSpec": "libp2p-mesh",
68
+ "npmSpec": "openclaw-libp2p-mesh",
67
69
  "defaultChoice": "npm",
68
70
  "minHostVersion": ">=2026.3.24"
69
71
  },
@@ -191,5 +191,24 @@ export function buildP2PTools(mesh: MeshNetwork) {
191
191
  }
192
192
  },
193
193
  },
194
+ {
195
+ name: "p2p_relay_status",
196
+ label: "P2P Relay Status",
197
+ description: "Get current P2P mesh connection status, known peers, and identity mapping.",
198
+ parameters: { type: "object" as const, properties: {} },
199
+ async execute(_toolCallId: string, _params: {}, _ctx: any) {
200
+ const peerIdentityMap = (this as any).peerIdentityMap;
201
+ const localPeerId = mesh.getLocalPeerId();
202
+ const connectedPeers = mesh.getConnectedPeers();
203
+ const knownIdentities = connectedPeers
204
+ .map((p) => ({ peerId: p, identity: peerIdentityMap?.resolve(p) }))
205
+ .filter((entry) => entry.identity !== undefined);
206
+ const text = `P2P Mesh Status:\nLocal Peer ID: ${localPeerId}\nConnected Peers: ${connectedPeers.length}\nKnown Identities: ${knownIdentities.length}`;
207
+ return {
208
+ content: [{ type: "text" as const, text }],
209
+ details: { localPeerId, connectedPeers, knownIdentities },
210
+ };
211
+ },
212
+ },
194
213
  ];
195
214
  }
@@ -0,0 +1,63 @@
1
+ import type { P2PMessage } from "./types.js";
2
+ import type { PeerIdentityMap } from "./peer-identity.ts";
3
+
4
+ export interface IdentityExchangeDeps {
5
+ peerIdentityMap: PeerIdentityMap;
6
+ localPeerId: string;
7
+ localAgentId: string;
8
+ localChannel: string;
9
+ localAccountId: string;
10
+ send: (peerId: string, message: P2PMessage) => Promise<void>;
11
+ }
12
+
13
+ export function buildIdentityMessage(
14
+ peerId: string,
15
+ agentId: string,
16
+ channel: string,
17
+ accountId: string,
18
+ ): P2PMessage {
19
+ return {
20
+ id: crypto.randomUUID(),
21
+ type: "identity",
22
+ from: peerId,
23
+ payload: JSON.stringify({ agentId, channel, accountId }),
24
+ timestamp: Date.now(),
25
+ };
26
+ }
27
+
28
+ export async function handleIdentityMessage(
29
+ msg: P2PMessage,
30
+ deps: IdentityExchangeDeps,
31
+ ): Promise<void> {
32
+ // Parse remote identity payload
33
+ let parsed: { agentId?: string; channel?: string; accountId?: string } = {};
34
+ try {
35
+ parsed = JSON.parse(msg.payload);
36
+ } catch {
37
+ // Malformed payload — skip silently
38
+ return;
39
+ }
40
+
41
+ const { agentId, channel, accountId } = parsed;
42
+
43
+ // Only register if all required fields are present
44
+ if (agentId && channel && accountId) {
45
+ const { buildAgentSessionKey } = await import("openclaw/plugin-sdk/core");
46
+ const sessionKey = buildAgentSessionKey({ agentId, channel, accountId });
47
+ deps.peerIdentityMap.register(msg.from, {
48
+ agentId,
49
+ channel,
50
+ accountId,
51
+ sessionKey,
52
+ });
53
+ }
54
+
55
+ // Send local identity back to the remote peer
56
+ const localIdentity = buildIdentityMessage(
57
+ deps.localPeerId,
58
+ deps.localAgentId,
59
+ deps.localChannel,
60
+ deps.localAccountId,
61
+ );
62
+ await deps.send(msg.from, localIdentity);
63
+ }
package/src/inbound.ts CHANGED
@@ -1,17 +1,22 @@
1
1
  import type { P2PMessage } from "./types.js";
2
+ import type { PeerIdentityMap } from "./peer-identity.ts";
3
+ import type { PluginNextTurnInjection } from "openclaw/plugin-sdk/core";
2
4
  import { verifyInstanceSignature } from "./instance-id.js";
5
+ import { buildAgentSessionKey } from "openclaw/plugin-sdk/core";
3
6
 
4
- export type InboundHandlerDeps = {
7
+ export interface InboundHandlerDeps {
8
+ peerIdentityMap?: PeerIdentityMap;
9
+ enqueueNextTurnInjection?: (injection: PluginNextTurnInjection) => Promise<{ enqueued: boolean; id: string }>;
5
10
  logger?: {
6
11
  info?: (msg: string) => void;
7
12
  debug?: (msg: string) => void;
8
13
  warn?: (msg: string) => void;
9
14
  error?: (msg: string) => void;
10
15
  };
11
- };
16
+ }
12
17
 
13
- export function handleP2PInbound(msg: P2PMessage, deps: InboundHandlerDeps): void {
14
- const { logger } = deps;
18
+ export function handleP2PInbound(msg: P2PMessage, deps?: InboundHandlerDeps): void {
19
+ const logger = deps?.logger;
15
20
  const instanceTag = msg.instanceId ? ` [instance: ${msg.instanceId}]` : "";
16
21
  const signedTag = msg.signature ? " [signed]" : "";
17
22
 
@@ -28,7 +33,14 @@ export function handleP2PInbound(msg: P2PMessage, deps: InboundHandlerDeps): voi
28
33
  instanceId: msg.instanceId,
29
34
  });
30
35
  const valid = verifyInstanceSignature(
31
- { id: msg.instanceId, name: "", pubkey: msg.pubkey, binding: "", bindingComponents: { username: "", hostname: "", platform: "" }, createdAt: 0 },
36
+ {
37
+ id: msg.instanceId,
38
+ name: "",
39
+ pubkey: msg.pubkey,
40
+ binding: "",
41
+ bindingComponents: { username: "", hostname: "", platform: "" },
42
+ createdAt: 0,
43
+ },
32
44
  signedPayload,
33
45
  msg.signature,
34
46
  );
@@ -41,6 +53,68 @@ export function handleP2PInbound(msg: P2PMessage, deps: InboundHandlerDeps): voi
41
53
  logger?.warn?.(`[libp2p-mesh] Message has signature but no pubkey; cannot verify`);
42
54
  }
43
55
 
56
+ // Relay handling when deps are provided with required fields
57
+ if (deps?.peerIdentityMap && deps?.enqueueNextTurnInjection) {
58
+ if (msg.type === "identity") {
59
+ // Parse remote identity payload and register synchronously
60
+ let parsed: { agentId?: string; channel?: string; accountId?: string } = {};
61
+ try {
62
+ parsed = JSON.parse(msg.payload);
63
+ } catch {
64
+ // Malformed payload — skip silently
65
+ return;
66
+ }
67
+
68
+ const { agentId, channel, accountId } = parsed;
69
+
70
+ if (agentId && channel && accountId) {
71
+ const sessionKey = buildAgentSessionKey({ agentId, channel, accountId });
72
+ deps.peerIdentityMap.register(msg.from, {
73
+ agentId,
74
+ channel,
75
+ accountId,
76
+ sessionKey,
77
+ });
78
+ }
79
+ } else if (msg.type === "direct") {
80
+ if (!msg.to) {
81
+ logger?.warn?.("[libp2p-mesh] Direct message missing 'to' field");
82
+ } else {
83
+ const receiver = deps.peerIdentityMap.resolve(msg.to);
84
+ if (!receiver) {
85
+ logger?.warn?.(`[libp2p-mesh] Unknown recipient peer: ${msg.to}`);
86
+ } else {
87
+ const sessionKey = buildAgentSessionKey({
88
+ agentId: receiver.agentId,
89
+ channel: receiver.channel,
90
+ accountId: receiver.accountId,
91
+ });
92
+ const text =
93
+ `【来自P2P网络的消息】\n来源:${msg.from}\n内容:${msg.payload}\n\n` +
94
+ `请你通过你当前的 channel 将此消息转发给你所服务的用户。转发完成后,请务必调用 p2p_send_message 工具向 ${msg.from} 回复送达确认。`;
95
+
96
+ deps.enqueueNextTurnInjection({ sessionKey, text, placement: "prepend_context" });
97
+ }
98
+ }
99
+ } else if (msg.type === "broadcast") {
100
+ const topic = msg.topic || "(none)";
101
+ const text =
102
+ `【来自P2P网络的广播消息】\n话题:${topic}\n来源:${msg.from}\n内容:${msg.payload}\n\n` +
103
+ `请你通过你当前的 channel 将此广播消息转发给你所服务的用户。`;
104
+
105
+ // Inject broadcast into each known peer's session
106
+ for (const [, identity] of deps.peerIdentityMap.entries()) {
107
+ const sessionKey = buildAgentSessionKey({
108
+ agentId: identity.agentId,
109
+ channel: identity.channel,
110
+ accountId: identity.accountId,
111
+ });
112
+ deps.enqueueNextTurnInjection({ sessionKey, text, placement: "prepend_context" });
113
+ }
114
+ }
115
+ }
116
+
117
+ // Original logging (unchanged)
44
118
  if (msg.type === "broadcast") {
45
119
  logger?.info?.(
46
120
  `[libp2p-mesh] Broadcast from ${msg.from}${instanceTag}${signedTag} on topic ${msg.topic ?? "(none)"}: ${msg.payload}`,
package/src/mesh.ts CHANGED
@@ -257,12 +257,12 @@ export function createMeshNetwork(options: {
257
257
 
258
258
  state.node.addEventListener("peer:connect", (evt) => {
259
259
  const peerIdStr = evt.detail.toString();
260
- logger?.debug?.(`[libp2p-mesh] Peer connected: ${peerIdStr}`);
260
+ logger?.info?.(`[libp2p-mesh] Peer connected: ${peerIdStr}`);
261
261
  });
262
262
 
263
263
  state.node.addEventListener("peer:disconnect", (evt) => {
264
264
  const peerIdStr = evt.detail.toString();
265
- logger?.debug?.(`[libp2p-mesh] Peer disconnected: ${peerIdStr}`);
265
+ logger?.info?.(`[libp2p-mesh] Peer disconnected: ${peerIdStr}`);
266
266
  });
267
267
 
268
268
  await state.node.handle(
@@ -0,0 +1,59 @@
1
+ export interface PeerIdentity {
2
+ agentId: string;
3
+ channel: string;
4
+ accountId: string;
5
+ sessionKey: string;
6
+ }
7
+
8
+ export interface PeerIdentityMap {
9
+ register(peerId: string, identity: PeerIdentity): void;
10
+ resolve(peerId: string): PeerIdentity | undefined;
11
+ resolveSessionKey(peerId: string): string | undefined;
12
+ unregister(peerId: string): void;
13
+ setLocalIdentity(peerId: string, identity: PeerIdentity): void;
14
+ getLocalIdentity(): PeerIdentity | undefined;
15
+ hasIdentity(peerId: string): boolean;
16
+ entries(): IterableIterator<[string, PeerIdentity]>;
17
+ }
18
+
19
+ export function createPeerIdentityMap(): PeerIdentityMap {
20
+ const peers = new Map<string, PeerIdentity>();
21
+ let localPeerId: string | undefined;
22
+ let localIdentity: PeerIdentity | undefined;
23
+
24
+ return {
25
+ register(peerId: string, identity: PeerIdentity): void {
26
+ peers.set(peerId, identity);
27
+ },
28
+
29
+ resolve(peerId: string): PeerIdentity | undefined {
30
+ return peers.get(peerId);
31
+ },
32
+
33
+ resolveSessionKey(peerId: string): string | undefined {
34
+ return peers.get(peerId)?.sessionKey;
35
+ },
36
+
37
+ unregister(peerId: string): void {
38
+ peers.delete(peerId);
39
+ },
40
+
41
+ setLocalIdentity(peerId: string, identity: PeerIdentity): void {
42
+ localPeerId = peerId;
43
+ localIdentity = identity;
44
+ peers.set(peerId, identity);
45
+ },
46
+
47
+ getLocalIdentity(): PeerIdentity | undefined {
48
+ return localIdentity;
49
+ },
50
+
51
+ hasIdentity(peerId: string): boolean {
52
+ return peers.has(peerId);
53
+ },
54
+
55
+ entries(): IterableIterator<[string, PeerIdentity]> {
56
+ return peers.entries();
57
+ },
58
+ };
59
+ }
package/src/plugin.ts CHANGED
@@ -1,8 +1,10 @@
1
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
1
+ import type { OpenClawPluginApi, PluginNextTurnInjection } from "openclaw/plugin-sdk/core";
2
2
  import { createLibp2pMeshChannel } from "./channel.js";
3
3
  import { handleP2PInbound } from "./inbound.js";
4
4
  import { createMeshNetwork } from "./mesh.js";
5
5
  import { buildP2PTools } from "./agent-tools.js";
6
+ import { createPeerIdentityMap } from "./peer-identity.js";
7
+ import { buildIdentityMessage } from "./identity-exchange.js";
6
8
  import type { ChannelPlugin } from "openclaw/plugin-sdk/core";
7
9
  import type { MeshConfig } from "./types.js";
8
10
 
@@ -12,14 +14,69 @@ export function registerLibp2pMesh(api: OpenClawPluginApi) {
12
14
  logger: api.logger,
13
15
  });
14
16
 
17
+ // Singleton: maps peerId -> { agentId, channel, accountId, sessionKey }
18
+ const peerIdentityMap = createPeerIdentityMap();
19
+
20
+ // Helper: build the deps object for the relay-aware inbound handler
21
+ function buildInboundDeps() {
22
+ return {
23
+ peerIdentityMap,
24
+ enqueueNextTurnInjection: (injection: PluginNextTurnInjection) =>
25
+ api.session.workflow.enqueueNextTurnInjection(injection),
26
+ logger: api.logger,
27
+ };
28
+ }
29
+
15
30
  // 1. Register Service (manages libp2p node lifecycle)
16
31
  api.registerService({
17
32
  id: "libp2p-mesh",
18
33
  start: async () => {
19
34
  await mesh.start();
35
+
36
+ // Register local identity so remote peers can route messages back to us
37
+ const config = api.pluginConfig as MeshConfig | undefined;
38
+ const relayChannel = config?.relayChannel;
39
+ const relayAccountId = config?.relayAccountId;
40
+ if (relayChannel && relayAccountId) {
41
+ const localPeerId = mesh.getLocalPeerId();
42
+ const { buildAgentSessionKey } = await import("openclaw/plugin-sdk/core");
43
+ const sessionKey = buildAgentSessionKey({
44
+ agentId: api.name,
45
+ channel: relayChannel,
46
+ accountId: relayAccountId,
47
+ });
48
+ peerIdentityMap.setLocalIdentity(localPeerId, {
49
+ agentId: api.name,
50
+ channel: relayChannel,
51
+ accountId: relayAccountId,
52
+ sessionKey,
53
+ });
54
+ api.logger.info?.(
55
+ `[libp2p-mesh] Local identity registered: agent=${api.name}, channel=${relayChannel}, account=${relayAccountId}`,
56
+ );
57
+
58
+ // Announce identity to all currently-connected peers
59
+ const identityMsg = buildIdentityMessage(
60
+ localPeerId,
61
+ api.name,
62
+ relayChannel,
63
+ relayAccountId,
64
+ );
65
+ const connectedPeers = mesh.getConnectedPeers();
66
+ for (const peerId of connectedPeers) {
67
+ try {
68
+ await mesh.sendToPeer(peerId, JSON.stringify(identityMsg));
69
+ } catch {
70
+ // Best-effort; peer may be stale in the connection list
71
+ }
72
+ }
73
+ }
74
+
75
+ // Wire up relay-aware message handler
20
76
  mesh.onMessage((msg) => {
21
- handleP2PInbound(msg, { logger: api.logger });
77
+ handleP2PInbound(msg, buildInboundDeps());
22
78
  });
79
+
23
80
  const identity = mesh.getInstanceIdentity();
24
81
  api.logger.info?.(`[libp2p-mesh] Service started. Peer ID: ${mesh.getLocalPeerId()}`);
25
82
  if (identity) {
@@ -54,11 +111,12 @@ export function registerLibp2pMesh(api: OpenClawPluginApi) {
54
111
  // 3. Register Agent Tools
55
112
  const tools = buildP2PTools(mesh);
56
113
  for (const tool of tools) {
114
+ (tool as any).peerIdentityMap = peerIdentityMap;
57
115
  api.registerTool(tool as never);
58
116
  }
59
117
 
60
118
  // 4. Register Hook (log received messages for observability)
61
- api.registerHook("message:received", async (event) => {
119
+ api.registerHook("message:received", async (event: any) => {
62
120
  const ctx = event.context as { channelId?: string } | undefined;
63
121
  api.logger.debug?.(`[libp2p-mesh] message received on channel ${ctx?.channelId ?? "unknown"}`);
64
122
  }, { name: "libp2p-mesh-message-received" });
package/src/types.ts CHANGED
@@ -19,7 +19,7 @@ export interface InstanceIdentity {
19
19
 
20
20
  export interface P2PMessage {
21
21
  id: string;
22
- type: "direct" | "broadcast" | "agent-sync";
22
+ type: "direct" | "broadcast" | "agent-sync" | "identity";
23
23
  from: string;
24
24
  to?: string;
25
25
  topic?: string;
@@ -89,6 +89,10 @@ export interface MeshConfig {
89
89
  * forward where AutoNAT cannot probe (e.g. behind a cloud LB).
90
90
  */
91
91
  announceAddrs?: string[];
92
+ /** Relay channel this agent is bound to (used for peer identity registration) */
93
+ relayChannel?: string;
94
+ /** Relay account this agent is bound to (used for peer identity registration) */
95
+ relayAccountId?: string;
92
96
  }
93
97
 
94
98
  export interface NATTraversalStatus {