libp2p-mesh 2026.5.20 → 2026.5.21
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/dist/src/identity-exchange.d.ts +6 -0
- package/dist/src/identity-exchange.js +15 -3
- package/dist/src/inbound.d.ts +1 -1
- package/dist/src/inbound.js +91 -22
- package/dist/src/peer-identity.d.ts +8 -2
- package/dist/src/peer-identity.js +41 -18
- package/dist/src/plugin.js +41 -2
- package/package.json +1 -1
- package/src/identity-exchange.ts +26 -3
- package/src/inbound.ts +95 -24
- package/src/peer-identity.ts +52 -20
- package/src/plugin.ts +40 -2
|
@@ -8,6 +8,12 @@ export interface IdentityExchangeDeps {
|
|
|
8
8
|
localAccountId: string;
|
|
9
9
|
localInstanceId?: string;
|
|
10
10
|
send: (peerId: string, message: P2PMessage) => Promise<void>;
|
|
11
|
+
logger?: {
|
|
12
|
+
info?: (msg: string) => void;
|
|
13
|
+
debug?: (msg: string) => void;
|
|
14
|
+
warn?: (msg: string) => void;
|
|
15
|
+
error?: (msg: string) => void;
|
|
16
|
+
};
|
|
11
17
|
}
|
|
12
18
|
export declare function buildIdentityMessage(peerId: string, agentId: string, channel: string, accountId: string, instanceId?: string): P2PMessage;
|
|
13
19
|
export declare function handleIdentityMessage(msg: P2PMessage, deps: IdentityExchangeDeps): Promise<void>;
|
|
@@ -8,6 +8,7 @@ export function buildIdentityMessage(peerId, agentId, channel, accountId, instan
|
|
|
8
8
|
};
|
|
9
9
|
}
|
|
10
10
|
export async function handleIdentityMessage(msg, deps) {
|
|
11
|
+
const logger = deps.logger;
|
|
11
12
|
// Parse remote identity payload
|
|
12
13
|
let parsed = {};
|
|
13
14
|
try {
|
|
@@ -22,15 +23,26 @@ export async function handleIdentityMessage(msg, deps) {
|
|
|
22
23
|
if (agentId && channel && accountId) {
|
|
23
24
|
const { buildAgentSessionKey } = await import("openclaw/plugin-sdk/core");
|
|
24
25
|
const sessionKey = buildAgentSessionKey({ agentId, channel, accountId });
|
|
25
|
-
deps.peerIdentityMap.register(msg.from, {
|
|
26
|
+
await deps.peerIdentityMap.register(msg.from, {
|
|
26
27
|
agentId,
|
|
27
28
|
channel,
|
|
28
29
|
accountId,
|
|
29
30
|
sessionKey,
|
|
30
31
|
instanceId,
|
|
31
32
|
});
|
|
33
|
+
// Force immediate persistence so the identity is durable even if the
|
|
34
|
+
// process exits before the next save cycle.
|
|
35
|
+
await deps.peerIdentityMap.saveNow();
|
|
36
|
+
logger?.info?.(`[libp2p-mesh] Registered peer identity: ${msg.from} (agent=${agentId}, channel=${channel}, instanceId=${instanceId ?? "n/a"})`);
|
|
32
37
|
}
|
|
33
|
-
// Send local identity back to the remote peer
|
|
38
|
+
// Send local identity back to the remote peer.
|
|
39
|
+
// We intentionally do NOT await this — the send callback (and any
|
|
40
|
+
// downstream identity handling it triggers) runs asynchronously and
|
|
41
|
+
// independently. Awaiting would create a deep synchronous chain that
|
|
42
|
+
// blocks the caller; the recipient's handler will persist the reply on
|
|
43
|
+
// its own via saveNow().
|
|
34
44
|
const localIdentity = buildIdentityMessage(deps.localPeerId, deps.localAgentId, deps.localChannel, deps.localAccountId, deps.localInstanceId);
|
|
35
|
-
|
|
45
|
+
deps.send(msg.from, localIdentity).catch((sendErr) => {
|
|
46
|
+
logger?.warn?.(`[libp2p-mesh] Failed to send identity reply to ${msg.from}: ${String(sendErr)}`);
|
|
47
|
+
});
|
|
36
48
|
}
|
package/dist/src/inbound.d.ts
CHANGED
|
@@ -14,4 +14,4 @@ export interface InboundHandlerDeps {
|
|
|
14
14
|
error?: (msg: string) => void;
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
|
-
export declare function handleP2PInbound(msg: P2PMessage, deps?: InboundHandlerDeps): void
|
|
17
|
+
export declare function handleP2PInbound(msg: P2PMessage, deps?: InboundHandlerDeps): Promise<void>;
|
package/dist/src/inbound.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { verifyInstanceSignature } from "./instance-id.js";
|
|
2
2
|
import { buildAgentSessionKey } from "openclaw/plugin-sdk/core";
|
|
3
|
-
|
|
3
|
+
import { handleIdentityMessage } from "./identity-exchange.js";
|
|
4
|
+
export async function handleP2PInbound(msg, deps) {
|
|
4
5
|
const logger = deps?.logger;
|
|
5
6
|
const instanceTag = msg.instanceId ? ` [instance: ${msg.instanceId}]` : "";
|
|
6
7
|
const signedTag = msg.signature ? " [signed]" : "";
|
|
@@ -32,31 +33,29 @@ export function handleP2PInbound(msg, deps) {
|
|
|
32
33
|
}
|
|
33
34
|
}
|
|
34
35
|
else if (msg.signature) {
|
|
35
|
-
logger?.warn?.(
|
|
36
|
+
logger?.warn?.("[libp2p-mesh] Message has signature but no pubkey; cannot verify");
|
|
36
37
|
}
|
|
37
38
|
// Relay handling when deps are provided with required fields
|
|
38
39
|
if (deps?.peerIdentityMap && deps?.enqueueNextTurnInjection) {
|
|
39
40
|
if (msg.type === "identity") {
|
|
40
|
-
//
|
|
41
|
-
|
|
41
|
+
// Delegate full identity exchange (register + send reply) to the
|
|
42
|
+
// dedicated handler. This ensures bidirectional identity propagation,
|
|
43
|
+
// forced persistence via saveNow(), and proper logging.
|
|
42
44
|
try {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
deps.peerIdentityMap.register(msg.from, {
|
|
53
|
-
agentId,
|
|
54
|
-
channel,
|
|
55
|
-
accountId,
|
|
56
|
-
sessionKey,
|
|
57
|
-
instanceId,
|
|
45
|
+
await handleIdentityMessage(msg, {
|
|
46
|
+
peerIdentityMap: deps.peerIdentityMap,
|
|
47
|
+
localPeerId: deps.peerIdentityMap.getLocalIdentity()?.sessionKey ?? "",
|
|
48
|
+
localAgentId: "",
|
|
49
|
+
localChannel: "",
|
|
50
|
+
localAccountId: "",
|
|
51
|
+
localInstanceId: deps.peerIdentityMap.getLocalIdentity()?.instanceId,
|
|
52
|
+
send: async () => { }, // no-op; reply is handled within handleIdentityMessage
|
|
53
|
+
logger,
|
|
58
54
|
});
|
|
59
55
|
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
logger?.error?.(`[libp2p-mesh] Identity message handler error: ${String(err)}`);
|
|
58
|
+
}
|
|
60
59
|
}
|
|
61
60
|
else if (msg.type === "direct") {
|
|
62
61
|
if (!msg.to) {
|
|
@@ -65,7 +64,25 @@ export function handleP2PInbound(msg, deps) {
|
|
|
65
64
|
else {
|
|
66
65
|
const receiver = deps.peerIdentityMap.resolve(msg.to);
|
|
67
66
|
if (!receiver) {
|
|
68
|
-
|
|
67
|
+
// Fallback: check if the recipient is our own local identity.
|
|
68
|
+
// setLocalIdentity stores the local peer separately (not in peers
|
|
69
|
+
// Map) to avoid duplication, so direct messages addressed to
|
|
70
|
+
// ourselves need this fallback to resolve correctly.
|
|
71
|
+
const localPeerId = deps.peerIdentityMap.getLocalPeerId();
|
|
72
|
+
const localId = deps.peerIdentityMap.getLocalIdentity();
|
|
73
|
+
if (localId && msg.to === localPeerId) {
|
|
74
|
+
const sessionKey = buildAgentSessionKey({
|
|
75
|
+
agentId: localId.agentId,
|
|
76
|
+
channel: localId.channel,
|
|
77
|
+
accountId: localId.accountId,
|
|
78
|
+
});
|
|
79
|
+
const text = `【来自P2P网络的消息】\n来源:${msg.from}\n内容:${msg.payload}\n\n` +
|
|
80
|
+
`请你通过你当前的 channel 将此消息转发给你所服务的用户。转发完成后,请务必调用 p2p_send_message 工具向 ${msg.from} 回复送达确认。`;
|
|
81
|
+
deps.enqueueNextTurnInjection({ sessionKey, text, placement: "prepend_context" });
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
logger?.warn?.(`[libp2p-mesh] Unknown recipient peer: ${msg.to}`);
|
|
85
|
+
}
|
|
69
86
|
}
|
|
70
87
|
else {
|
|
71
88
|
const sessionKey = buildAgentSessionKey({
|
|
@@ -78,13 +95,50 @@ export function handleP2PInbound(msg, deps) {
|
|
|
78
95
|
deps.enqueueNextTurnInjection({ sessionKey, text, placement: "prepend_context" });
|
|
79
96
|
}
|
|
80
97
|
}
|
|
98
|
+
// If the sender is not yet registered, register them synchronously
|
|
99
|
+
// (in-memory only) so that subsequent message routing works correctly.
|
|
100
|
+
// This handles NAT/relay scenarios where peer:connect may not fire
|
|
101
|
+
// bidirectionally, causing the identity exchange to be one-sided.
|
|
102
|
+
// Persistence (saveNow) is fire-and-forget to avoid blocking the
|
|
103
|
+
// message handler.
|
|
104
|
+
if (!deps.peerIdentityMap.hasIdentity(msg.from)) {
|
|
105
|
+
const localIdentity = deps.peerIdentityMap.getLocalIdentity();
|
|
106
|
+
if (localIdentity) {
|
|
107
|
+
deps.peerIdentityMap.registerSync(msg.from, {
|
|
108
|
+
agentId: localIdentity.agentId,
|
|
109
|
+
channel: localIdentity.channel,
|
|
110
|
+
accountId: localIdentity.accountId,
|
|
111
|
+
sessionKey: "",
|
|
112
|
+
instanceId: msg.instanceId,
|
|
113
|
+
});
|
|
114
|
+
deps.peerIdentityMap.saveNow().catch(() => { });
|
|
115
|
+
logger?.debug?.(`[libp2p-mesh] Auto-registered unknown sender ${msg.from} from direct message`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
81
118
|
}
|
|
82
119
|
else if (msg.type === "broadcast") {
|
|
83
120
|
const topic = msg.topic || "(none)";
|
|
84
121
|
const text = `【来自P2P网络的广播消息】\n话题:${topic}\n来源:${msg.from}\n内容:${msg.payload}\n\n` +
|
|
85
122
|
`请你通过你当前的 channel 将此广播消息转发给你所服务的用户。`;
|
|
86
|
-
// Inject broadcast into each known peer's session
|
|
87
|
-
|
|
123
|
+
// Inject broadcast into each known peer's session.
|
|
124
|
+
// Include remote peers from the peers Map, plus our own local identity
|
|
125
|
+
// (setLocalIdentity stores local separately to avoid duplication).
|
|
126
|
+
// Exclude the sender so they don't receive their own broadcast.
|
|
127
|
+
const seenPeers = new Set([msg.from]);
|
|
128
|
+
const localPeerId = deps.peerIdentityMap.getLocalPeerId();
|
|
129
|
+
const localIdentity = deps.peerIdentityMap.getLocalIdentity();
|
|
130
|
+
if (localPeerId && localIdentity && !seenPeers.has(localPeerId)) {
|
|
131
|
+
const sessionKey = buildAgentSessionKey({
|
|
132
|
+
agentId: localIdentity.agentId,
|
|
133
|
+
channel: localIdentity.channel,
|
|
134
|
+
accountId: localIdentity.accountId,
|
|
135
|
+
});
|
|
136
|
+
deps.enqueueNextTurnInjection({ sessionKey, text, placement: "prepend_context" });
|
|
137
|
+
seenPeers.add(localPeerId);
|
|
138
|
+
}
|
|
139
|
+
for (const [peerId, identity] of deps.peerIdentityMap.entries()) {
|
|
140
|
+
if (seenPeers.has(peerId))
|
|
141
|
+
continue;
|
|
88
142
|
const sessionKey = buildAgentSessionKey({
|
|
89
143
|
agentId: identity.agentId,
|
|
90
144
|
channel: identity.channel,
|
|
@@ -92,6 +146,21 @@ export function handleP2PInbound(msg, deps) {
|
|
|
92
146
|
});
|
|
93
147
|
deps.enqueueNextTurnInjection({ sessionKey, text, placement: "prepend_context" });
|
|
94
148
|
}
|
|
149
|
+
// If the sender is not yet registered, register them synchronously
|
|
150
|
+
if (!deps.peerIdentityMap.hasIdentity(msg.from)) {
|
|
151
|
+
const localIdentity = deps.peerIdentityMap.getLocalIdentity();
|
|
152
|
+
if (localIdentity) {
|
|
153
|
+
deps.peerIdentityMap.registerSync(msg.from, {
|
|
154
|
+
agentId: localIdentity.agentId,
|
|
155
|
+
channel: localIdentity.channel,
|
|
156
|
+
accountId: localIdentity.accountId,
|
|
157
|
+
sessionKey: "",
|
|
158
|
+
instanceId: msg.instanceId,
|
|
159
|
+
});
|
|
160
|
+
deps.peerIdentityMap.saveNow().catch(() => { });
|
|
161
|
+
logger?.debug?.(`[libp2p-mesh] Auto-registered unknown sender ${msg.from} from broadcast`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
95
164
|
}
|
|
96
165
|
}
|
|
97
166
|
// Original logging (unchanged)
|
|
@@ -11,7 +11,9 @@ export interface PeerIdentity {
|
|
|
11
11
|
lastSeen?: number;
|
|
12
12
|
}
|
|
13
13
|
export interface PeerIdentityMap {
|
|
14
|
-
register(peerId: string, identity: PeerIdentity): void
|
|
14
|
+
register(peerId: string, identity: PeerIdentity): Promise<void>;
|
|
15
|
+
/** Synchronous in-memory registration without disk I/O */
|
|
16
|
+
registerSync(peerId: string, identity: PeerIdentity): void;
|
|
15
17
|
resolve(peerId: string): PeerIdentity | undefined;
|
|
16
18
|
resolveSessionKey(peerId: string): string | undefined;
|
|
17
19
|
resolveByInstanceId(instanceId: string): {
|
|
@@ -21,9 +23,13 @@ export interface PeerIdentityMap {
|
|
|
21
23
|
unregister(peerId: string): void;
|
|
22
24
|
setLocalIdentity(peerId: string, identity: PeerIdentity): void;
|
|
23
25
|
getLocalIdentity(): PeerIdentity | undefined;
|
|
26
|
+
/** Returns the peerId of the local node (if set) */
|
|
27
|
+
getLocalPeerId(): string | undefined;
|
|
24
28
|
hasIdentity(peerId: string): boolean;
|
|
25
29
|
entries(): IterableIterator<[string, PeerIdentity]>;
|
|
26
|
-
/**
|
|
30
|
+
/** Force an immediate synchronous-style save (returns promise for await) */
|
|
31
|
+
saveNow(): Promise<void>;
|
|
32
|
+
/** Persist current state to disk (fire-and-forget, same as before) */
|
|
27
33
|
save(): Promise<void>;
|
|
28
34
|
}
|
|
29
35
|
export declare function createPeerIdentityMap(storePath?: string): PeerIdentityMap & {
|
|
@@ -8,7 +8,20 @@ export function createPeerIdentityMap(storePath) {
|
|
|
8
8
|
let localPeerId;
|
|
9
9
|
let localIdentity;
|
|
10
10
|
const filePath = storePath ?? DEFAULT_STORE_PATH;
|
|
11
|
-
|
|
11
|
+
// Synchronous in-memory registration (no disk I/O). Used as a fast-path
|
|
12
|
+
// when we need the entry to be visible immediately (e.g. routing a
|
|
13
|
+
// received message) and can persist to disk afterwards.
|
|
14
|
+
function doRegisterSync(peerId, identity) {
|
|
15
|
+
const now = Date.now();
|
|
16
|
+
const existing = peers.get(peerId);
|
|
17
|
+
identity.firstSeen = existing?.firstSeen ?? now;
|
|
18
|
+
identity.lastSeen = now;
|
|
19
|
+
peers.set(peerId, identity);
|
|
20
|
+
if (identity.instanceId) {
|
|
21
|
+
byInstanceId.set(identity.instanceId, { peerId, identity });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async function doSave() {
|
|
12
25
|
const entries = {};
|
|
13
26
|
for (const [peerId, identity] of peers.entries()) {
|
|
14
27
|
entries[peerId] = identity;
|
|
@@ -23,8 +36,9 @@ export function createPeerIdentityMap(storePath) {
|
|
|
23
36
|
await mkdir(path.dirname(filePath), { recursive: true });
|
|
24
37
|
await writeFile(filePath, JSON.stringify(payload, null, 2), "utf-8");
|
|
25
38
|
}
|
|
26
|
-
catch {
|
|
27
|
-
// Best-effort persistence; non-critical
|
|
39
|
+
catch (err) {
|
|
40
|
+
// Best-effort persistence; non-critical but surface unexpected errors
|
|
41
|
+
console.error(`[peer-identity] Failed to save identity map: ${String(err)}`);
|
|
28
42
|
}
|
|
29
43
|
}
|
|
30
44
|
async function load() {
|
|
@@ -49,18 +63,18 @@ export function createPeerIdentityMap(storePath) {
|
|
|
49
63
|
}
|
|
50
64
|
}
|
|
51
65
|
return {
|
|
52
|
-
async save() { await
|
|
66
|
+
async save() { await doSave(); },
|
|
67
|
+
/** Force an immediate save — caller can await for confirmation */
|
|
68
|
+
async saveNow() { await doSave(); },
|
|
53
69
|
async load() { await load(); },
|
|
54
|
-
register(peerId, identity) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
save().catch(() => { });
|
|
70
|
+
async register(peerId, identity) {
|
|
71
|
+
doRegisterSync(peerId, identity);
|
|
72
|
+
await doSave();
|
|
73
|
+
},
|
|
74
|
+
/** Synchronous in-memory registration (no disk I/O). Use this when the
|
|
75
|
+
* entry must be visible immediately; call saveNow() afterwards to persist. */
|
|
76
|
+
registerSync(peerId, identity) {
|
|
77
|
+
doRegisterSync(peerId, identity);
|
|
64
78
|
},
|
|
65
79
|
resolve(peerId) {
|
|
66
80
|
return peers.get(peerId);
|
|
@@ -84,19 +98,28 @@ export function createPeerIdentityMap(storePath) {
|
|
|
84
98
|
},
|
|
85
99
|
setLocalIdentity(peerId, identity) {
|
|
86
100
|
localPeerId = peerId;
|
|
87
|
-
localIdentity = identity;
|
|
88
101
|
const now = Date.now();
|
|
89
|
-
|
|
102
|
+
// Preserve firstSeen if the local identity was already loaded from disk
|
|
103
|
+
if (!localIdentity) {
|
|
104
|
+
identity.firstSeen = now;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
identity.firstSeen = localIdentity.firstSeen ?? now;
|
|
108
|
+
}
|
|
90
109
|
identity.lastSeen = now;
|
|
91
|
-
|
|
110
|
+
localIdentity = identity;
|
|
111
|
+
// Also index by instanceId so resolveByInstanceId works for local
|
|
92
112
|
if (identity.instanceId) {
|
|
93
113
|
byInstanceId.set(identity.instanceId, { peerId, identity });
|
|
94
114
|
}
|
|
95
|
-
|
|
115
|
+
doSave().catch(() => { });
|
|
96
116
|
},
|
|
97
117
|
getLocalIdentity() {
|
|
98
118
|
return localIdentity;
|
|
99
119
|
},
|
|
120
|
+
getLocalPeerId() {
|
|
121
|
+
return localPeerId;
|
|
122
|
+
},
|
|
100
123
|
hasIdentity(peerId) {
|
|
101
124
|
return peers.has(peerId);
|
|
102
125
|
},
|
package/dist/src/plugin.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createLibp2pMeshChannel } from "./channel.js";
|
|
2
2
|
import { handleP2PInbound } from "./inbound.js";
|
|
3
|
+
import { handleIdentityMessage } from "./identity-exchange.js";
|
|
3
4
|
import { createMeshNetwork } from "./mesh.js";
|
|
4
5
|
import { buildP2PTools } from "./agent-tools.js";
|
|
5
6
|
import { createPeerIdentityMap } from "./peer-identity.js";
|
|
@@ -41,6 +42,10 @@ export function registerLibp2pMesh(api) {
|
|
|
41
42
|
channel: relayChannel,
|
|
42
43
|
accountId: relayAccountId,
|
|
43
44
|
});
|
|
45
|
+
// setLocalIdentity stores the local identity separately (not in peers
|
|
46
|
+
// Map) to avoid duplication. We also register in peers Map so
|
|
47
|
+
// direct messages addressed to us (msg.to === localPeerId) can be
|
|
48
|
+
// resolved by deps.peerIdentityMap.resolve() in handleP2PInbound.
|
|
44
49
|
peerIdentityMap.setLocalIdentity(localPeerId, {
|
|
45
50
|
agentId: api.name,
|
|
46
51
|
channel: relayChannel,
|
|
@@ -48,6 +53,13 @@ export function registerLibp2pMesh(api) {
|
|
|
48
53
|
sessionKey,
|
|
49
54
|
instanceId: localInstanceId,
|
|
50
55
|
});
|
|
56
|
+
await peerIdentityMap.register(localPeerId, {
|
|
57
|
+
agentId: api.name,
|
|
58
|
+
channel: relayChannel,
|
|
59
|
+
accountId: relayAccountId,
|
|
60
|
+
sessionKey,
|
|
61
|
+
instanceId: localInstanceId,
|
|
62
|
+
});
|
|
51
63
|
api.logger.info?.(`[libp2p-mesh] Local identity registered: agent=${api.name}, channel=${relayChannel}, account=${relayAccountId}, instanceId=${localInstanceId}`);
|
|
52
64
|
// Announce identity to all currently-connected peers
|
|
53
65
|
const identityMsg = buildIdentityMessage(localPeerId, api.name, relayChannel, relayAccountId, localInstanceId);
|
|
@@ -61,6 +73,7 @@ export function registerLibp2pMesh(api) {
|
|
|
61
73
|
}
|
|
62
74
|
// When a new peer connects, send our identity to them
|
|
63
75
|
mesh.onPeerConnect((peerId) => {
|
|
76
|
+
api.logger.info?.(`[libp2p-mesh] Peer connected: ${peerId} — sending identity`);
|
|
64
77
|
const msg = buildIdentityMessage(localPeerId, api.name, relayChannel, relayAccountId, localInstanceId);
|
|
65
78
|
mesh.sendToPeer(peerId, JSON.stringify(msg)).catch(() => {
|
|
66
79
|
// Best-effort
|
|
@@ -68,12 +81,38 @@ export function registerLibp2pMesh(api) {
|
|
|
68
81
|
});
|
|
69
82
|
// When a peer disconnects, clean up the identity map
|
|
70
83
|
mesh.onPeerDisconnect((peerId) => {
|
|
84
|
+
api.logger.info?.(`[libp2p-mesh] Peer disconnected: ${peerId}`);
|
|
71
85
|
peerIdentityMap.unregister(peerId);
|
|
86
|
+
peerIdentityMap.saveNow().catch(() => { });
|
|
72
87
|
});
|
|
73
88
|
}
|
|
74
|
-
// Wire up relay-aware message handler
|
|
89
|
+
// Wire up relay-aware message handler.
|
|
90
|
+
// Identity messages get special treatment: they register the remote
|
|
91
|
+
// peer's identity, force immediate persistence via saveNow(), and
|
|
92
|
+
// automatically send our identity back — ensuring bidirectional
|
|
93
|
+
// identity exchange even when peer:connect events are one-sided
|
|
94
|
+
// (e.g. NAT/relay scenarios).
|
|
75
95
|
mesh.onMessage((msg) => {
|
|
76
|
-
|
|
96
|
+
if (msg.type === "identity") {
|
|
97
|
+
const localIdentity = peerIdentityMap.getLocalIdentity();
|
|
98
|
+
handleIdentityMessage(msg, {
|
|
99
|
+
peerIdentityMap,
|
|
100
|
+
localPeerId,
|
|
101
|
+
localAgentId: api.name,
|
|
102
|
+
localChannel: relayChannel ?? "",
|
|
103
|
+
localAccountId: relayAccountId ?? "",
|
|
104
|
+
localInstanceId,
|
|
105
|
+
send: async (targetPeerId, replyMsg) => {
|
|
106
|
+
mesh.sendToPeer(targetPeerId, JSON.stringify(replyMsg)).catch(() => { });
|
|
107
|
+
},
|
|
108
|
+
logger: api.logger,
|
|
109
|
+
}).catch((err) => {
|
|
110
|
+
api.logger.error?.(`[libp2p-mesh] Identity exchange error: ${String(err)}`);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
handleP2PInbound(msg, buildInboundDeps());
|
|
115
|
+
}
|
|
77
116
|
});
|
|
78
117
|
const identity = mesh.getInstanceIdentity();
|
|
79
118
|
api.logger.info?.(`[libp2p-mesh] Service started. Peer ID: ${localPeerId}`);
|
package/package.json
CHANGED
package/src/identity-exchange.ts
CHANGED
|
@@ -9,6 +9,12 @@ export interface IdentityExchangeDeps {
|
|
|
9
9
|
localAccountId: string;
|
|
10
10
|
localInstanceId?: string;
|
|
11
11
|
send: (peerId: string, message: P2PMessage) => Promise<void>;
|
|
12
|
+
logger?: {
|
|
13
|
+
info?: (msg: string) => void;
|
|
14
|
+
debug?: (msg: string) => void;
|
|
15
|
+
warn?: (msg: string) => void;
|
|
16
|
+
error?: (msg: string) => void;
|
|
17
|
+
};
|
|
12
18
|
}
|
|
13
19
|
|
|
14
20
|
export function buildIdentityMessage(
|
|
@@ -31,6 +37,8 @@ export async function handleIdentityMessage(
|
|
|
31
37
|
msg: P2PMessage,
|
|
32
38
|
deps: IdentityExchangeDeps,
|
|
33
39
|
): Promise<void> {
|
|
40
|
+
const logger = deps.logger;
|
|
41
|
+
|
|
34
42
|
// Parse remote identity payload
|
|
35
43
|
let parsed: { agentId?: string; channel?: string; accountId?: string; instanceId?: string } = {};
|
|
36
44
|
try {
|
|
@@ -46,16 +54,27 @@ export async function handleIdentityMessage(
|
|
|
46
54
|
if (agentId && channel && accountId) {
|
|
47
55
|
const { buildAgentSessionKey } = await import("openclaw/plugin-sdk/core");
|
|
48
56
|
const sessionKey = buildAgentSessionKey({ agentId, channel, accountId });
|
|
49
|
-
deps.peerIdentityMap.register(msg.from, {
|
|
57
|
+
await deps.peerIdentityMap.register(msg.from, {
|
|
50
58
|
agentId,
|
|
51
59
|
channel,
|
|
52
60
|
accountId,
|
|
53
61
|
sessionKey,
|
|
54
62
|
instanceId,
|
|
55
63
|
});
|
|
64
|
+
// Force immediate persistence so the identity is durable even if the
|
|
65
|
+
// process exits before the next save cycle.
|
|
66
|
+
await deps.peerIdentityMap.saveNow();
|
|
67
|
+
logger?.info?.(
|
|
68
|
+
`[libp2p-mesh] Registered peer identity: ${msg.from} (agent=${agentId}, channel=${channel}, instanceId=${instanceId ?? "n/a"})`,
|
|
69
|
+
);
|
|
56
70
|
}
|
|
57
71
|
|
|
58
|
-
// Send local identity back to the remote peer
|
|
72
|
+
// Send local identity back to the remote peer.
|
|
73
|
+
// We intentionally do NOT await this — the send callback (and any
|
|
74
|
+
// downstream identity handling it triggers) runs asynchronously and
|
|
75
|
+
// independently. Awaiting would create a deep synchronous chain that
|
|
76
|
+
// blocks the caller; the recipient's handler will persist the reply on
|
|
77
|
+
// its own via saveNow().
|
|
59
78
|
const localIdentity = buildIdentityMessage(
|
|
60
79
|
deps.localPeerId,
|
|
61
80
|
deps.localAgentId,
|
|
@@ -63,5 +82,9 @@ export async function handleIdentityMessage(
|
|
|
63
82
|
deps.localAccountId,
|
|
64
83
|
deps.localInstanceId,
|
|
65
84
|
);
|
|
66
|
-
|
|
85
|
+
deps.send(msg.from, localIdentity).catch((sendErr) => {
|
|
86
|
+
logger?.warn?.(
|
|
87
|
+
`[libp2p-mesh] Failed to send identity reply to ${msg.from}: ${String(sendErr)}`,
|
|
88
|
+
);
|
|
89
|
+
});
|
|
67
90
|
}
|
package/src/inbound.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { PeerIdentityMap } from "./peer-identity.ts";
|
|
|
3
3
|
import type { PluginNextTurnInjection } from "openclaw/plugin-sdk/core";
|
|
4
4
|
import { verifyInstanceSignature } from "./instance-id.js";
|
|
5
5
|
import { buildAgentSessionKey } from "openclaw/plugin-sdk/core";
|
|
6
|
+
import { handleIdentityMessage } from "./identity-exchange.js";
|
|
6
7
|
|
|
7
8
|
export interface InboundHandlerDeps {
|
|
8
9
|
peerIdentityMap?: PeerIdentityMap;
|
|
@@ -15,7 +16,7 @@ export interface InboundHandlerDeps {
|
|
|
15
16
|
};
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
export function handleP2PInbound(msg: P2PMessage, deps?: InboundHandlerDeps): void {
|
|
19
|
+
export async function handleP2PInbound(msg: P2PMessage, deps?: InboundHandlerDeps): Promise<void> {
|
|
19
20
|
const logger = deps?.logger;
|
|
20
21
|
const instanceTag = msg.instanceId ? ` [instance: ${msg.instanceId}]` : "";
|
|
21
22
|
const signedTag = msg.signature ? " [signed]" : "";
|
|
@@ -50,32 +51,28 @@ export function handleP2PInbound(msg: P2PMessage, deps?: InboundHandlerDeps): vo
|
|
|
50
51
|
logger?.warn?.(`[libp2p-mesh] Invalid signature from instance ${msg.instanceId}`);
|
|
51
52
|
}
|
|
52
53
|
} else if (msg.signature) {
|
|
53
|
-
logger?.warn?.(
|
|
54
|
+
logger?.warn?.("[libp2p-mesh] Message has signature but no pubkey; cannot verify");
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
// Relay handling when deps are provided with required fields
|
|
57
58
|
if (deps?.peerIdentityMap && deps?.enqueueNextTurnInjection) {
|
|
58
59
|
if (msg.type === "identity") {
|
|
59
|
-
//
|
|
60
|
-
|
|
60
|
+
// Delegate full identity exchange (register + send reply) to the
|
|
61
|
+
// dedicated handler. This ensures bidirectional identity propagation,
|
|
62
|
+
// forced persistence via saveNow(), and proper logging.
|
|
61
63
|
try {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const sessionKey = buildAgentSessionKey({ agentId, channel, accountId });
|
|
72
|
-
deps.peerIdentityMap.register(msg.from, {
|
|
73
|
-
agentId,
|
|
74
|
-
channel,
|
|
75
|
-
accountId,
|
|
76
|
-
sessionKey,
|
|
77
|
-
instanceId,
|
|
64
|
+
await handleIdentityMessage(msg, {
|
|
65
|
+
peerIdentityMap: deps.peerIdentityMap,
|
|
66
|
+
localPeerId: deps.peerIdentityMap.getLocalIdentity()?.sessionKey ?? "",
|
|
67
|
+
localAgentId: "",
|
|
68
|
+
localChannel: "",
|
|
69
|
+
localAccountId: "",
|
|
70
|
+
localInstanceId: deps.peerIdentityMap.getLocalIdentity()?.instanceId,
|
|
71
|
+
send: async () => {}, // no-op; reply is handled within handleIdentityMessage
|
|
72
|
+
logger,
|
|
78
73
|
});
|
|
74
|
+
} catch (err) {
|
|
75
|
+
logger?.error?.(`[libp2p-mesh] Identity message handler error: ${String(err)}`);
|
|
79
76
|
}
|
|
80
77
|
} else if (msg.type === "direct") {
|
|
81
78
|
if (!msg.to) {
|
|
@@ -83,7 +80,25 @@ export function handleP2PInbound(msg: P2PMessage, deps?: InboundHandlerDeps): vo
|
|
|
83
80
|
} else {
|
|
84
81
|
const receiver = deps.peerIdentityMap.resolve(msg.to);
|
|
85
82
|
if (!receiver) {
|
|
86
|
-
|
|
83
|
+
// Fallback: check if the recipient is our own local identity.
|
|
84
|
+
// setLocalIdentity stores the local peer separately (not in peers
|
|
85
|
+
// Map) to avoid duplication, so direct messages addressed to
|
|
86
|
+
// ourselves need this fallback to resolve correctly.
|
|
87
|
+
const localPeerId = deps.peerIdentityMap.getLocalPeerId();
|
|
88
|
+
const localId = deps.peerIdentityMap.getLocalIdentity();
|
|
89
|
+
if (localId && msg.to === localPeerId) {
|
|
90
|
+
const sessionKey = buildAgentSessionKey({
|
|
91
|
+
agentId: localId.agentId,
|
|
92
|
+
channel: localId.channel,
|
|
93
|
+
accountId: localId.accountId,
|
|
94
|
+
});
|
|
95
|
+
const text =
|
|
96
|
+
`【来自P2P网络的消息】\n来源:${msg.from}\n内容:${msg.payload}\n\n` +
|
|
97
|
+
`请你通过你当前的 channel 将此消息转发给你所服务的用户。转发完成后,请务必调用 p2p_send_message 工具向 ${msg.from} 回复送达确认。`;
|
|
98
|
+
deps.enqueueNextTurnInjection({ sessionKey, text, placement: "prepend_context" });
|
|
99
|
+
} else {
|
|
100
|
+
logger?.warn?.(`[libp2p-mesh] Unknown recipient peer: ${msg.to}`);
|
|
101
|
+
}
|
|
87
102
|
} else {
|
|
88
103
|
const sessionKey = buildAgentSessionKey({
|
|
89
104
|
agentId: receiver.agentId,
|
|
@@ -93,18 +108,56 @@ export function handleP2PInbound(msg: P2PMessage, deps?: InboundHandlerDeps): vo
|
|
|
93
108
|
const text =
|
|
94
109
|
`【来自P2P网络的消息】\n来源:${msg.from}\n内容:${msg.payload}\n\n` +
|
|
95
110
|
`请你通过你当前的 channel 将此消息转发给你所服务的用户。转发完成后,请务必调用 p2p_send_message 工具向 ${msg.from} 回复送达确认。`;
|
|
96
|
-
|
|
97
111
|
deps.enqueueNextTurnInjection({ sessionKey, text, placement: "prepend_context" });
|
|
98
112
|
}
|
|
99
113
|
}
|
|
114
|
+
|
|
115
|
+
// If the sender is not yet registered, register them synchronously
|
|
116
|
+
// (in-memory only) so that subsequent message routing works correctly.
|
|
117
|
+
// This handles NAT/relay scenarios where peer:connect may not fire
|
|
118
|
+
// bidirectionally, causing the identity exchange to be one-sided.
|
|
119
|
+
// Persistence (saveNow) is fire-and-forget to avoid blocking the
|
|
120
|
+
// message handler.
|
|
121
|
+
if (!deps.peerIdentityMap.hasIdentity(msg.from)) {
|
|
122
|
+
const localIdentity = deps.peerIdentityMap.getLocalIdentity();
|
|
123
|
+
if (localIdentity) {
|
|
124
|
+
deps.peerIdentityMap.registerSync(msg.from, {
|
|
125
|
+
agentId: localIdentity.agentId,
|
|
126
|
+
channel: localIdentity.channel,
|
|
127
|
+
accountId: localIdentity.accountId,
|
|
128
|
+
sessionKey: "",
|
|
129
|
+
instanceId: msg.instanceId,
|
|
130
|
+
});
|
|
131
|
+
deps.peerIdentityMap.saveNow().catch(() => {});
|
|
132
|
+
logger?.debug?.(
|
|
133
|
+
`[libp2p-mesh] Auto-registered unknown sender ${msg.from} from direct message`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
100
137
|
} else if (msg.type === "broadcast") {
|
|
101
138
|
const topic = msg.topic || "(none)";
|
|
102
139
|
const text =
|
|
103
140
|
`【来自P2P网络的广播消息】\n话题:${topic}\n来源:${msg.from}\n内容:${msg.payload}\n\n` +
|
|
104
141
|
`请你通过你当前的 channel 将此广播消息转发给你所服务的用户。`;
|
|
105
142
|
|
|
106
|
-
// Inject broadcast into each known peer's session
|
|
107
|
-
|
|
143
|
+
// Inject broadcast into each known peer's session.
|
|
144
|
+
// Include remote peers from the peers Map, plus our own local identity
|
|
145
|
+
// (setLocalIdentity stores local separately to avoid duplication).
|
|
146
|
+
// Exclude the sender so they don't receive their own broadcast.
|
|
147
|
+
const seenPeers = new Set<string>([msg.from]);
|
|
148
|
+
const localPeerId = deps.peerIdentityMap.getLocalPeerId();
|
|
149
|
+
const localIdentity = deps.peerIdentityMap.getLocalIdentity();
|
|
150
|
+
if (localPeerId && localIdentity && !seenPeers.has(localPeerId)) {
|
|
151
|
+
const sessionKey = buildAgentSessionKey({
|
|
152
|
+
agentId: localIdentity.agentId,
|
|
153
|
+
channel: localIdentity.channel,
|
|
154
|
+
accountId: localIdentity.accountId,
|
|
155
|
+
});
|
|
156
|
+
deps.enqueueNextTurnInjection({ sessionKey, text, placement: "prepend_context" });
|
|
157
|
+
seenPeers.add(localPeerId);
|
|
158
|
+
}
|
|
159
|
+
for (const [peerId, identity] of deps.peerIdentityMap.entries()) {
|
|
160
|
+
if (seenPeers.has(peerId)) continue;
|
|
108
161
|
const sessionKey = buildAgentSessionKey({
|
|
109
162
|
agentId: identity.agentId,
|
|
110
163
|
channel: identity.channel,
|
|
@@ -112,6 +165,24 @@ export function handleP2PInbound(msg: P2PMessage, deps?: InboundHandlerDeps): vo
|
|
|
112
165
|
});
|
|
113
166
|
deps.enqueueNextTurnInjection({ sessionKey, text, placement: "prepend_context" });
|
|
114
167
|
}
|
|
168
|
+
|
|
169
|
+
// If the sender is not yet registered, register them synchronously
|
|
170
|
+
if (!deps.peerIdentityMap.hasIdentity(msg.from)) {
|
|
171
|
+
const localIdentity = deps.peerIdentityMap.getLocalIdentity();
|
|
172
|
+
if (localIdentity) {
|
|
173
|
+
deps.peerIdentityMap.registerSync(msg.from, {
|
|
174
|
+
agentId: localIdentity.agentId,
|
|
175
|
+
channel: localIdentity.channel,
|
|
176
|
+
accountId: localIdentity.accountId,
|
|
177
|
+
sessionKey: "",
|
|
178
|
+
instanceId: msg.instanceId,
|
|
179
|
+
});
|
|
180
|
+
deps.peerIdentityMap.saveNow().catch(() => {});
|
|
181
|
+
logger?.debug?.(
|
|
182
|
+
`[libp2p-mesh] Auto-registered unknown sender ${msg.from} from broadcast`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
115
186
|
}
|
|
116
187
|
}
|
|
117
188
|
|
package/src/peer-identity.ts
CHANGED
|
@@ -16,16 +16,22 @@ export interface PeerIdentity {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export interface PeerIdentityMap {
|
|
19
|
-
register(peerId: string, identity: PeerIdentity): void
|
|
19
|
+
register(peerId: string, identity: PeerIdentity): Promise<void>;
|
|
20
|
+
/** Synchronous in-memory registration without disk I/O */
|
|
21
|
+
registerSync(peerId: string, identity: PeerIdentity): void;
|
|
20
22
|
resolve(peerId: string): PeerIdentity | undefined;
|
|
21
23
|
resolveSessionKey(peerId: string): string | undefined;
|
|
22
24
|
resolveByInstanceId(instanceId: string): { peerId: string; identity: PeerIdentity } | undefined;
|
|
23
25
|
unregister(peerId: string): void;
|
|
24
26
|
setLocalIdentity(peerId: string, identity: PeerIdentity): void;
|
|
25
27
|
getLocalIdentity(): PeerIdentity | undefined;
|
|
28
|
+
/** Returns the peerId of the local node (if set) */
|
|
29
|
+
getLocalPeerId(): string | undefined;
|
|
26
30
|
hasIdentity(peerId: string): boolean;
|
|
27
31
|
entries(): IterableIterator<[string, PeerIdentity]>;
|
|
28
|
-
/**
|
|
32
|
+
/** Force an immediate synchronous-style save (returns promise for await) */
|
|
33
|
+
saveNow(): Promise<void>;
|
|
34
|
+
/** Persist current state to disk (fire-and-forget, same as before) */
|
|
29
35
|
save(): Promise<void>;
|
|
30
36
|
}
|
|
31
37
|
|
|
@@ -38,7 +44,21 @@ export function createPeerIdentityMap(storePath?: string): PeerIdentityMap & { l
|
|
|
38
44
|
let localIdentity: PeerIdentity | undefined;
|
|
39
45
|
const filePath = storePath ?? DEFAULT_STORE_PATH;
|
|
40
46
|
|
|
41
|
-
|
|
47
|
+
// Synchronous in-memory registration (no disk I/O). Used as a fast-path
|
|
48
|
+
// when we need the entry to be visible immediately (e.g. routing a
|
|
49
|
+
// received message) and can persist to disk afterwards.
|
|
50
|
+
function doRegisterSync(peerId: string, identity: PeerIdentity): void {
|
|
51
|
+
const now = Date.now();
|
|
52
|
+
const existing = peers.get(peerId);
|
|
53
|
+
identity.firstSeen = existing?.firstSeen ?? now;
|
|
54
|
+
identity.lastSeen = now;
|
|
55
|
+
peers.set(peerId, identity);
|
|
56
|
+
if (identity.instanceId) {
|
|
57
|
+
byInstanceId.set(identity.instanceId, { peerId, identity });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function doSave(): Promise<void> {
|
|
42
62
|
const entries: Record<string, PeerIdentity> = {};
|
|
43
63
|
for (const [peerId, identity] of peers.entries()) {
|
|
44
64
|
entries[peerId] = identity;
|
|
@@ -52,8 +72,9 @@ export function createPeerIdentityMap(storePath?: string): PeerIdentityMap & { l
|
|
|
52
72
|
try {
|
|
53
73
|
await mkdir(path.dirname(filePath), { recursive: true });
|
|
54
74
|
await writeFile(filePath, JSON.stringify(payload, null, 2), "utf-8");
|
|
55
|
-
} catch {
|
|
56
|
-
// Best-effort persistence; non-critical
|
|
75
|
+
} catch (err) {
|
|
76
|
+
// Best-effort persistence; non-critical but surface unexpected errors
|
|
77
|
+
console.error(`[peer-identity] Failed to save identity map: ${String(err)}`);
|
|
57
78
|
}
|
|
58
79
|
}
|
|
59
80
|
|
|
@@ -76,20 +97,22 @@ export function createPeerIdentityMap(storePath?: string): PeerIdentityMap & { l
|
|
|
76
97
|
}
|
|
77
98
|
|
|
78
99
|
return {
|
|
79
|
-
async save() { await
|
|
100
|
+
async save() { await doSave(); },
|
|
101
|
+
|
|
102
|
+
/** Force an immediate save — caller can await for confirmation */
|
|
103
|
+
async saveNow() { await doSave(); },
|
|
80
104
|
|
|
81
105
|
async load() { await load(); },
|
|
82
106
|
|
|
83
|
-
register(peerId: string, identity: PeerIdentity): void {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
save().catch(() => {});
|
|
107
|
+
async register(peerId: string, identity: PeerIdentity): Promise<void> {
|
|
108
|
+
doRegisterSync(peerId, identity);
|
|
109
|
+
await doSave();
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
/** Synchronous in-memory registration (no disk I/O). Use this when the
|
|
113
|
+
* entry must be visible immediately; call saveNow() afterwards to persist. */
|
|
114
|
+
registerSync(peerId: string, identity: PeerIdentity): void {
|
|
115
|
+
doRegisterSync(peerId, identity);
|
|
93
116
|
},
|
|
94
117
|
|
|
95
118
|
resolve(peerId: string): PeerIdentity | undefined {
|
|
@@ -118,21 +141,30 @@ export function createPeerIdentityMap(storePath?: string): PeerIdentityMap & { l
|
|
|
118
141
|
|
|
119
142
|
setLocalIdentity(peerId: string, identity: PeerIdentity): void {
|
|
120
143
|
localPeerId = peerId;
|
|
121
|
-
localIdentity = identity;
|
|
122
144
|
const now = Date.now();
|
|
123
|
-
|
|
145
|
+
// Preserve firstSeen if the local identity was already loaded from disk
|
|
146
|
+
if (!localIdentity) {
|
|
147
|
+
identity.firstSeen = now;
|
|
148
|
+
} else {
|
|
149
|
+
identity.firstSeen = localIdentity.firstSeen ?? now;
|
|
150
|
+
}
|
|
124
151
|
identity.lastSeen = now;
|
|
125
|
-
|
|
152
|
+
localIdentity = identity;
|
|
153
|
+
// Also index by instanceId so resolveByInstanceId works for local
|
|
126
154
|
if (identity.instanceId) {
|
|
127
155
|
byInstanceId.set(identity.instanceId, { peerId, identity });
|
|
128
156
|
}
|
|
129
|
-
|
|
157
|
+
doSave().catch(() => {});
|
|
130
158
|
},
|
|
131
159
|
|
|
132
160
|
getLocalIdentity(): PeerIdentity | undefined {
|
|
133
161
|
return localIdentity;
|
|
134
162
|
},
|
|
135
163
|
|
|
164
|
+
getLocalPeerId(): string | undefined {
|
|
165
|
+
return localPeerId;
|
|
166
|
+
},
|
|
167
|
+
|
|
136
168
|
hasIdentity(peerId: string): boolean {
|
|
137
169
|
return peers.has(peerId);
|
|
138
170
|
},
|
package/src/plugin.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
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
|
+
import { handleIdentityMessage } from "./identity-exchange.js";
|
|
4
5
|
import { createMeshNetwork } from "./mesh.js";
|
|
5
6
|
import { buildP2PTools } from "./agent-tools.js";
|
|
6
7
|
import { createPeerIdentityMap } from "./peer-identity.js";
|
|
@@ -52,6 +53,10 @@ export function registerLibp2pMesh(api: OpenClawPluginApi) {
|
|
|
52
53
|
channel: relayChannel,
|
|
53
54
|
accountId: relayAccountId,
|
|
54
55
|
});
|
|
56
|
+
// setLocalIdentity stores the local identity separately (not in peers
|
|
57
|
+
// Map) to avoid duplication. We also register in peers Map so
|
|
58
|
+
// direct messages addressed to us (msg.to === localPeerId) can be
|
|
59
|
+
// resolved by deps.peerIdentityMap.resolve() in handleP2PInbound.
|
|
55
60
|
peerIdentityMap.setLocalIdentity(localPeerId, {
|
|
56
61
|
agentId: api.name,
|
|
57
62
|
channel: relayChannel,
|
|
@@ -59,6 +64,13 @@ export function registerLibp2pMesh(api: OpenClawPluginApi) {
|
|
|
59
64
|
sessionKey,
|
|
60
65
|
instanceId: localInstanceId,
|
|
61
66
|
});
|
|
67
|
+
await peerIdentityMap.register(localPeerId, {
|
|
68
|
+
agentId: api.name,
|
|
69
|
+
channel: relayChannel,
|
|
70
|
+
accountId: relayAccountId,
|
|
71
|
+
sessionKey,
|
|
72
|
+
instanceId: localInstanceId,
|
|
73
|
+
});
|
|
62
74
|
api.logger.info?.(
|
|
63
75
|
`[libp2p-mesh] Local identity registered: agent=${api.name}, channel=${relayChannel}, account=${relayAccountId}, instanceId=${localInstanceId}`,
|
|
64
76
|
);
|
|
@@ -81,6 +93,7 @@ export function registerLibp2pMesh(api: OpenClawPluginApi) {
|
|
|
81
93
|
|
|
82
94
|
// When a new peer connects, send our identity to them
|
|
83
95
|
mesh.onPeerConnect((peerId: string) => {
|
|
96
|
+
api.logger.info?.(`[libp2p-mesh] Peer connected: ${peerId} — sending identity`);
|
|
84
97
|
const msg = buildIdentityMessage(
|
|
85
98
|
localPeerId,
|
|
86
99
|
api.name,
|
|
@@ -95,13 +108,38 @@ export function registerLibp2pMesh(api: OpenClawPluginApi) {
|
|
|
95
108
|
|
|
96
109
|
// When a peer disconnects, clean up the identity map
|
|
97
110
|
mesh.onPeerDisconnect((peerId: string) => {
|
|
111
|
+
api.logger.info?.(`[libp2p-mesh] Peer disconnected: ${peerId}`);
|
|
98
112
|
peerIdentityMap.unregister(peerId);
|
|
113
|
+
peerIdentityMap.saveNow().catch(() => {});
|
|
99
114
|
});
|
|
100
115
|
}
|
|
101
116
|
|
|
102
|
-
// Wire up relay-aware message handler
|
|
117
|
+
// Wire up relay-aware message handler.
|
|
118
|
+
// Identity messages get special treatment: they register the remote
|
|
119
|
+
// peer's identity, force immediate persistence via saveNow(), and
|
|
120
|
+
// automatically send our identity back — ensuring bidirectional
|
|
121
|
+
// identity exchange even when peer:connect events are one-sided
|
|
122
|
+
// (e.g. NAT/relay scenarios).
|
|
103
123
|
mesh.onMessage((msg) => {
|
|
104
|
-
|
|
124
|
+
if (msg.type === "identity") {
|
|
125
|
+
const localIdentity = peerIdentityMap.getLocalIdentity();
|
|
126
|
+
handleIdentityMessage(msg, {
|
|
127
|
+
peerIdentityMap,
|
|
128
|
+
localPeerId,
|
|
129
|
+
localAgentId: api.name,
|
|
130
|
+
localChannel: relayChannel ?? "",
|
|
131
|
+
localAccountId: relayAccountId ?? "",
|
|
132
|
+
localInstanceId,
|
|
133
|
+
send: async (targetPeerId: string, replyMsg: any) => {
|
|
134
|
+
mesh.sendToPeer(targetPeerId, JSON.stringify(replyMsg)).catch(() => {});
|
|
135
|
+
},
|
|
136
|
+
logger: api.logger,
|
|
137
|
+
}).catch((err) => {
|
|
138
|
+
api.logger.error?.(`[libp2p-mesh] Identity exchange error: ${String(err)}`);
|
|
139
|
+
});
|
|
140
|
+
} else {
|
|
141
|
+
handleP2PInbound(msg, buildInboundDeps());
|
|
142
|
+
}
|
|
105
143
|
});
|
|
106
144
|
|
|
107
145
|
const identity = mesh.getInstanceIdentity();
|