libp2p-mesh 2026.5.12 → 2026.5.14
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 +67 -0
- package/api.ts +1 -1
- package/dist/api.d.ts +1 -1
- package/dist/index.js +60 -0
- package/dist/runtime-setter-api.d.ts +4 -0
- package/dist/runtime-setter-api.js +19 -0
- package/dist/src/agent-tools-feishu.test.d.ts +1 -0
- package/dist/src/agent-tools-feishu.test.js +57 -0
- package/dist/src/agent-tools.d.ts +87 -0
- package/dist/src/agent-tools.js +79 -0
- package/dist/src/channel.d.ts +1 -0
- package/dist/src/channel.js +20 -4
- package/dist/src/config-schema.test.d.ts +1 -0
- package/dist/src/config-schema.test.js +55 -0
- package/dist/src/dht-registry.d.ts +38 -0
- package/dist/src/dht-registry.js +80 -0
- package/dist/src/feishu-channel.d.ts +19 -0
- package/dist/src/feishu-channel.js +202 -0
- package/dist/src/feishu-channel.test.d.ts +1 -0
- package/dist/src/feishu-channel.test.js +166 -0
- package/dist/src/feishu-client.d.ts +27 -0
- package/dist/src/feishu-client.js +141 -0
- package/dist/src/feishu-client.test.d.ts +1 -0
- package/dist/src/feishu-client.test.js +271 -0
- package/dist/src/feishu-e2e.test.d.ts +1 -0
- package/dist/src/feishu-e2e.test.js +69 -0
- package/dist/src/feishu-types.d.ts +53 -0
- package/dist/src/feishu-types.js +1 -0
- package/dist/src/feishu-types.test.d.ts +1 -0
- package/dist/src/feishu-types.test.js +108 -0
- package/dist/src/inbound-feishu.test.d.ts +1 -0
- package/dist/src/inbound-feishu.test.js +70 -0
- package/dist/src/inbound.d.ts +0 -1
- package/dist/src/inbound.js +30 -11
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/instance-id.d.ts +53 -0
- package/dist/src/instance-id.js +156 -0
- package/dist/src/mesh.js +310 -23
- package/dist/src/plugin-registration.test.d.ts +1 -0
- package/dist/src/plugin-registration.test.js +42 -0
- package/dist/src/plugin.js +17 -48
- package/dist/src/types.d.ts +87 -0
- package/index.ts +60 -0
- package/openclaw.plugin.json +72 -33
- package/package.json +18 -5
- package/src/agent-tools.ts +79 -0
- package/src/channel.ts +25 -4
- package/src/dht-registry.ts +105 -0
- package/src/inbound.ts +37 -15
- package/src/instance-id.ts +221 -0
- package/src/mesh.ts +368 -27
- package/src/plugin.ts +25 -56
- package/src/types.ts +95 -0
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@ P2P mesh network plugin for OpenClaw. Enables direct peer-to-peer communication
|
|
|
9
9
|
- **Broadcast** — Publish messages to a shared topic, flood-fill forwarded across the mesh
|
|
10
10
|
- **Bootstrap Mode** — Optional static bootstrap peer list for non-LAN scenarios
|
|
11
11
|
- **WebSocket Transport** — Optional WebSocket support for NAT/firewall-friendly connections
|
|
12
|
+
- **NAT Traversal** — Built-in AutoNAT + UPnP + Circuit Relay v2 + DCUtR for peers behind home routers / firewalls
|
|
12
13
|
|
|
13
14
|
## Requirements
|
|
14
15
|
|
|
@@ -145,6 +146,72 @@ If peers are on different networks, use a bootstrap node:
|
|
|
145
146
|
| `enableWebSocket` | `boolean` | `false` | Enable WebSocket transport for browser/NAT compatibility |
|
|
146
147
|
| `meshTopic` | `string` | `"openclaw-mesh"` | Default broadcast topic |
|
|
147
148
|
| `enableAgentSync` | `boolean` | `true` | Enable agent state synchronization over the mesh |
|
|
149
|
+
| `enableNATTraversal` | `boolean` | `true` | Master switch for identify + AutoNAT + UPnP + Circuit Relay v2 + DCUtR |
|
|
150
|
+
| `enableIdentify` | `boolean` | `true` | libp2p identify protocol (required by AutoNAT and DCUtR) |
|
|
151
|
+
| `enableAutoNAT` | `boolean` | `true` | AutoNAT — detect whether this node is publicly reachable |
|
|
152
|
+
| `enableUPnP` | `boolean` | `true` | Attempt UPnP/PMP port mapping on the local gateway |
|
|
153
|
+
| `enableCircuitRelay` | `boolean` | `true` | Dial peers via /p2p-circuit relay addresses |
|
|
154
|
+
| `enableCircuitRelayServer` | `boolean` | `false` | Act as a Circuit Relay v2 server (only enable on a public node) |
|
|
155
|
+
| `enableDCUtR` | `boolean` | `true` | Hole-punching: upgrade a relayed connection to a direct one |
|
|
156
|
+
| `relayList` | `string[]` | `[]` | Multiaddrs of relays to reserve a slot on |
|
|
157
|
+
| `discoverRelays` | `number` | `0` | Auto-discover this many relays via content routing |
|
|
158
|
+
| `announceAddrs` | `string[]` | `[]` | Extra multiaddrs to announce on top of auto-detected ones |
|
|
159
|
+
|
|
160
|
+
## NAT Traversal
|
|
161
|
+
|
|
162
|
+
When both peers have a routable address (same LAN, public IPs, or working port-forwarding) no extra setup is needed. The defaults above kick in automatically:
|
|
163
|
+
|
|
164
|
+
- **UPnP** asks your home router to open a port for libp2p TCP.
|
|
165
|
+
- **AutoNAT** asks peers to verify whether you're reachable from the outside.
|
|
166
|
+
- If you're not directly reachable, **Circuit Relay v2** lets another peer (the "relay") forward traffic on your behalf. The relay only sees encrypted bytes — Noise still terminates end-to-end at the original peers.
|
|
167
|
+
- Once a relayed connection is established, **DCUtR** tries to upgrade it to a direct connection via simultaneous TCP open (hole punching). This works for most home NATs (full-cone, restricted-cone, port-restricted) but not symmetric NATs (CGNAT, some carrier networks).
|
|
168
|
+
|
|
169
|
+
### Behind a NAT — minimal config
|
|
170
|
+
|
|
171
|
+
You need at least one relay node with a public IP. Set it in `relayList`:
|
|
172
|
+
|
|
173
|
+
```json
|
|
174
|
+
{
|
|
175
|
+
"plugins": {
|
|
176
|
+
"libp2p-mesh": {
|
|
177
|
+
"enabled": true,
|
|
178
|
+
"config": {
|
|
179
|
+
"discovery": "bootstrap",
|
|
180
|
+
"bootstrapList": [
|
|
181
|
+
"/ip4/<RELAY-IP>/tcp/4001/p2p/<RELAY-PEER-ID>"
|
|
182
|
+
],
|
|
183
|
+
"relayList": [
|
|
184
|
+
"/ip4/<RELAY-IP>/tcp/4001/p2p/<RELAY-PEER-ID>"
|
|
185
|
+
]
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
After start-up you should see your node listening on a `/p2p-circuit` address — that's how remote peers will reach you.
|
|
193
|
+
|
|
194
|
+
### Running your own relay on a public VM
|
|
195
|
+
|
|
196
|
+
Add `enableCircuitRelayServer: true` to your config and announce the public address so other peers can dial you:
|
|
197
|
+
|
|
198
|
+
```json
|
|
199
|
+
{
|
|
200
|
+
"plugins": {
|
|
201
|
+
"libp2p-mesh": {
|
|
202
|
+
"enabled": true,
|
|
203
|
+
"config": {
|
|
204
|
+
"discovery": "bootstrap",
|
|
205
|
+
"listenAddrs": ["/ip4/0.0.0.0/tcp/4001"],
|
|
206
|
+
"announceAddrs": ["/ip4/<PUBLIC-IP>/tcp/4001"],
|
|
207
|
+
"enableCircuitRelayServer": true
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
> Detailed walkthrough including how to rent a cloud VM is in `../TESTING_NAT.md`.
|
|
148
215
|
|
|
149
216
|
## Usage: Two Computers on the Same LAN
|
|
150
217
|
|
package/api.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { createMeshNetwork } from "./src/mesh.js";
|
|
2
|
-
export type { MeshNetwork, P2PMessage, MeshConfig } from "./src/types.js";
|
|
2
|
+
export type { MeshNetwork, P2PMessage, MeshConfig, InstanceIdentity } from "./src/types.js";
|
package/dist/api.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { createMeshNetwork } from "./src/mesh.js";
|
|
2
|
-
export type { MeshNetwork, P2PMessage, MeshConfig } from "./src/types.js";
|
|
2
|
+
export type { MeshNetwork, P2PMessage, MeshConfig, InstanceIdentity } from "./src/types.js";
|
package/dist/index.js
CHANGED
|
@@ -36,6 +36,7 @@ function createLibp2pMeshConfigSchema() {
|
|
|
36
36
|
bootstrapList: {
|
|
37
37
|
type: "array",
|
|
38
38
|
items: { type: "string" },
|
|
39
|
+
description: "List of bootstrap multiaddrs for WAN discovery (required when discovery=dht or bootstrap)",
|
|
39
40
|
},
|
|
40
41
|
meshTopic: {
|
|
41
42
|
type: "string",
|
|
@@ -49,6 +50,65 @@ function createLibp2pMeshConfigSchema() {
|
|
|
49
50
|
type: "boolean",
|
|
50
51
|
default: true,
|
|
51
52
|
},
|
|
53
|
+
enableDHT: {
|
|
54
|
+
type: "boolean",
|
|
55
|
+
default: true,
|
|
56
|
+
description: "Enable DHT for WAN peer discovery and pubkey registry. Default true when discovery=dht, can be explicitly disabled.",
|
|
57
|
+
},
|
|
58
|
+
instanceName: {
|
|
59
|
+
type: "string",
|
|
60
|
+
description: "Custom name for this OpenClaw instance (used in InstanceID). Defaults to \"<username>-<hostname>\".",
|
|
61
|
+
},
|
|
62
|
+
enableNATTraversal: {
|
|
63
|
+
type: "boolean",
|
|
64
|
+
default: true,
|
|
65
|
+
description: "Master switch for the NAT traversal stack (identify + AutoNAT + UPnP + Circuit Relay v2 + DCUtR). Set to false to restore pre-NAT behaviour.",
|
|
66
|
+
},
|
|
67
|
+
enableIdentify: {
|
|
68
|
+
type: "boolean",
|
|
69
|
+
default: true,
|
|
70
|
+
description: "Run the libp2p identify protocol; required by AutoNAT and DCUtR.",
|
|
71
|
+
},
|
|
72
|
+
enableAutoNAT: {
|
|
73
|
+
type: "boolean",
|
|
74
|
+
default: true,
|
|
75
|
+
description: "Use AutoNAT to learn whether this node is publicly reachable.",
|
|
76
|
+
},
|
|
77
|
+
enableUPnP: {
|
|
78
|
+
type: "boolean",
|
|
79
|
+
default: true,
|
|
80
|
+
description: "Attempt UPnP/PMP port mapping against the local gateway so other peers can dial us directly when behind a home router.",
|
|
81
|
+
},
|
|
82
|
+
enableCircuitRelay: {
|
|
83
|
+
type: "boolean",
|
|
84
|
+
default: true,
|
|
85
|
+
description: "Allow this node to dial peers via /p2p-circuit relay addresses and to reserve a slot on the relays in relayList.",
|
|
86
|
+
},
|
|
87
|
+
enableCircuitRelayServer: {
|
|
88
|
+
type: "boolean",
|
|
89
|
+
default: false,
|
|
90
|
+
description: "Act as a Circuit Relay v2 SERVER for other peers. Only enable on a publicly reachable node (e.g. a cloud VM).",
|
|
91
|
+
},
|
|
92
|
+
enableDCUtR: {
|
|
93
|
+
type: "boolean",
|
|
94
|
+
default: true,
|
|
95
|
+
description: "Direct Connection Upgrade through Relay (hole punching). Upgrades a relayed connection to a direct one when possible.",
|
|
96
|
+
},
|
|
97
|
+
relayList: {
|
|
98
|
+
type: "array",
|
|
99
|
+
items: { type: "string" },
|
|
100
|
+
description: "Multiaddrs of relay nodes to reserve a slot on (each entry must end in /p2p/<peer-id>).",
|
|
101
|
+
},
|
|
102
|
+
discoverRelays: {
|
|
103
|
+
type: "number",
|
|
104
|
+
default: 0,
|
|
105
|
+
description: "How many relays to auto-discover via content routing. Requires DHT. 0 disables discovery.",
|
|
106
|
+
},
|
|
107
|
+
announceAddrs: {
|
|
108
|
+
type: "array",
|
|
109
|
+
items: { type: "string" },
|
|
110
|
+
description: "Extra multiaddrs to announce to the network (useful when running behind a known port forward where AutoNAT cannot probe).",
|
|
111
|
+
},
|
|
52
112
|
},
|
|
53
113
|
},
|
|
54
114
|
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Narrow setter for the libp2p-mesh runtime. The bundled channel entry needs
|
|
2
|
+
// a static `libp2pMeshPlugin` export to satisfy openclaw's bundled-channel
|
|
3
|
+
// contract; the channel itself however needs the mesh runtime, which is
|
|
4
|
+
// constructed inside registerFull(). We bridge the two via this module-level
|
|
5
|
+
// holder: registerFull() calls setLibp2pMeshRuntime(mesh) after starting the
|
|
6
|
+
// service, and channel.ts reads from getLibp2pMeshRuntime() when sending.
|
|
7
|
+
let _runtime = null;
|
|
8
|
+
export function setLibp2pMeshRuntime(mesh) {
|
|
9
|
+
_runtime = mesh;
|
|
10
|
+
}
|
|
11
|
+
export function getLibp2pMeshRuntime() {
|
|
12
|
+
if (!_runtime) {
|
|
13
|
+
throw new Error("libp2p-mesh: runtime not initialized — registerFull() must call setLibp2pMeshRuntime() before any channel call");
|
|
14
|
+
}
|
|
15
|
+
return _runtime;
|
|
16
|
+
}
|
|
17
|
+
export function hasLibp2pMeshRuntime() {
|
|
18
|
+
return _runtime !== null;
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { buildFeishuTools } from "./agent-tools.js";
|
|
3
|
+
describe("buildFeishuTools", () => {
|
|
4
|
+
it("should return empty array when feishuClient is null", () => {
|
|
5
|
+
const tools = buildFeishuTools(null);
|
|
6
|
+
expect(tools).toEqual([]);
|
|
7
|
+
});
|
|
8
|
+
it("should return array with one tool when feishuClient is provided", () => {
|
|
9
|
+
const mockClient = {
|
|
10
|
+
sendMessage: vi.fn(),
|
|
11
|
+
};
|
|
12
|
+
const tools = buildFeishuTools(mockClient);
|
|
13
|
+
expect(tools).toHaveLength(1);
|
|
14
|
+
expect(tools[0].name).toBe("feishu_send_message");
|
|
15
|
+
});
|
|
16
|
+
describe("feishu_send_message tool", () => {
|
|
17
|
+
let tools;
|
|
18
|
+
let mockClient;
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
mockClient = {
|
|
21
|
+
sendMessage: vi.fn(),
|
|
22
|
+
};
|
|
23
|
+
tools = buildFeishuTools(mockClient);
|
|
24
|
+
});
|
|
25
|
+
it("should require openId and message parameters", () => {
|
|
26
|
+
const tool = tools[0];
|
|
27
|
+
expect(tool.parameters.required).toEqual(["openId", "message"]);
|
|
28
|
+
});
|
|
29
|
+
it("should return success result on successful send", async () => {
|
|
30
|
+
mockClient.sendMessage.mockResolvedValue({
|
|
31
|
+
success: true,
|
|
32
|
+
messageId: "msg-789",
|
|
33
|
+
});
|
|
34
|
+
const result = await tools[0].execute("call-1", {
|
|
35
|
+
openId: "ou-test",
|
|
36
|
+
message: "hello",
|
|
37
|
+
});
|
|
38
|
+
expect(result.isError).toBeUndefined();
|
|
39
|
+
expect(result.details.sent).toBe(true);
|
|
40
|
+
expect(result.details.openId).toBe("ou-test");
|
|
41
|
+
expect(mockClient.sendMessage).toHaveBeenCalledWith("ou-test", "hello");
|
|
42
|
+
});
|
|
43
|
+
it("should return error result on failed send", async () => {
|
|
44
|
+
mockClient.sendMessage.mockResolvedValue({
|
|
45
|
+
success: false,
|
|
46
|
+
error: "rate limited",
|
|
47
|
+
});
|
|
48
|
+
const result = await tools[0].execute("call-1", {
|
|
49
|
+
openId: "ou-test",
|
|
50
|
+
message: "hello",
|
|
51
|
+
});
|
|
52
|
+
expect(result.isError).toBe(true);
|
|
53
|
+
expect(result.details.sent).toBe(false);
|
|
54
|
+
expect(result.details.error).toBe("rate limited");
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -127,4 +127,91 @@ export declare function buildP2PTools(mesh: MeshNetwork): ({
|
|
|
127
127
|
};
|
|
128
128
|
isError: boolean;
|
|
129
129
|
}>;
|
|
130
|
+
} | {
|
|
131
|
+
name: string;
|
|
132
|
+
label: string;
|
|
133
|
+
description: string;
|
|
134
|
+
parameters: {
|
|
135
|
+
type: "object";
|
|
136
|
+
properties: {
|
|
137
|
+
peerId?: undefined;
|
|
138
|
+
message?: undefined;
|
|
139
|
+
topic?: undefined;
|
|
140
|
+
};
|
|
141
|
+
required?: undefined;
|
|
142
|
+
};
|
|
143
|
+
execute(_toolCallId: string): Promise<{
|
|
144
|
+
content: {
|
|
145
|
+
type: "text";
|
|
146
|
+
text: string;
|
|
147
|
+
}[];
|
|
148
|
+
details: {
|
|
149
|
+
initialized: boolean;
|
|
150
|
+
identity?: undefined;
|
|
151
|
+
error?: undefined;
|
|
152
|
+
};
|
|
153
|
+
isError?: undefined;
|
|
154
|
+
} | {
|
|
155
|
+
content: {
|
|
156
|
+
type: "text";
|
|
157
|
+
text: string;
|
|
158
|
+
}[];
|
|
159
|
+
details: {
|
|
160
|
+
identity: import("./types.js").InstanceIdentity;
|
|
161
|
+
initialized?: undefined;
|
|
162
|
+
error?: undefined;
|
|
163
|
+
};
|
|
164
|
+
isError?: undefined;
|
|
165
|
+
} | {
|
|
166
|
+
content: {
|
|
167
|
+
type: "text";
|
|
168
|
+
text: string;
|
|
169
|
+
}[];
|
|
170
|
+
details: {
|
|
171
|
+
error: string;
|
|
172
|
+
initialized?: undefined;
|
|
173
|
+
identity?: undefined;
|
|
174
|
+
};
|
|
175
|
+
isError: boolean;
|
|
176
|
+
}>;
|
|
177
|
+
} | {
|
|
178
|
+
name: string;
|
|
179
|
+
label: string;
|
|
180
|
+
description: string;
|
|
181
|
+
parameters: {
|
|
182
|
+
type: "object";
|
|
183
|
+
properties: {
|
|
184
|
+
peerId?: undefined;
|
|
185
|
+
message?: undefined;
|
|
186
|
+
topic?: undefined;
|
|
187
|
+
};
|
|
188
|
+
required?: undefined;
|
|
189
|
+
};
|
|
190
|
+
execute(_toolCallId: string): Promise<{
|
|
191
|
+
content: {
|
|
192
|
+
type: "text";
|
|
193
|
+
text: string;
|
|
194
|
+
}[];
|
|
195
|
+
details: {
|
|
196
|
+
peerId: string;
|
|
197
|
+
instanceId: string | undefined;
|
|
198
|
+
listenAddrs: string[];
|
|
199
|
+
connectedPeers: string[];
|
|
200
|
+
error?: undefined;
|
|
201
|
+
};
|
|
202
|
+
isError?: undefined;
|
|
203
|
+
} | {
|
|
204
|
+
content: {
|
|
205
|
+
type: "text";
|
|
206
|
+
text: string;
|
|
207
|
+
}[];
|
|
208
|
+
details: {
|
|
209
|
+
error: string;
|
|
210
|
+
peerId?: undefined;
|
|
211
|
+
instanceId?: undefined;
|
|
212
|
+
listenAddrs?: undefined;
|
|
213
|
+
connectedPeers?: undefined;
|
|
214
|
+
};
|
|
215
|
+
isError: boolean;
|
|
216
|
+
}>;
|
|
130
217
|
})[];
|
package/dist/src/agent-tools.js
CHANGED
|
@@ -112,5 +112,84 @@ export function buildP2PTools(mesh) {
|
|
|
112
112
|
}
|
|
113
113
|
},
|
|
114
114
|
},
|
|
115
|
+
{
|
|
116
|
+
name: "p2p_get_instance_identity",
|
|
117
|
+
label: "P2P Get Instance Identity",
|
|
118
|
+
description: "Get the OpenClaw instance identity (lightweight BAID-inspired ID) of this node.",
|
|
119
|
+
parameters: {
|
|
120
|
+
type: "object",
|
|
121
|
+
properties: {},
|
|
122
|
+
},
|
|
123
|
+
async execute(_toolCallId) {
|
|
124
|
+
try {
|
|
125
|
+
const identity = mesh.getInstanceIdentity();
|
|
126
|
+
if (!identity) {
|
|
127
|
+
return {
|
|
128
|
+
content: [{ type: "text", text: "Instance identity not yet initialized." }],
|
|
129
|
+
details: { initialized: false },
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const lines = [
|
|
133
|
+
`Instance ID: ${identity.id}`,
|
|
134
|
+
`Name: ${identity.name}`,
|
|
135
|
+
`Pubkey: ${identity.pubkey.slice(0, 32)}...`,
|
|
136
|
+
`Binding: ${identity.binding.slice(0, 16)}...`,
|
|
137
|
+
`Bound to: ${identity.bindingComponents.username}@${identity.bindingComponents.hostname} (${identity.bindingComponents.platform})`,
|
|
138
|
+
`Created: ${new Date(identity.createdAt).toLocaleString()}`,
|
|
139
|
+
];
|
|
140
|
+
return {
|
|
141
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
142
|
+
details: { identity },
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
return {
|
|
147
|
+
content: [{ type: "text", text: `Error: ${String(err)}` }],
|
|
148
|
+
details: { error: String(err) },
|
|
149
|
+
isError: true,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: "p2p_get_network_info",
|
|
156
|
+
label: "P2P Get Network Info",
|
|
157
|
+
description: "Get combined network and identity info: Peer ID, Instance ID, listen addresses, and connected peers.",
|
|
158
|
+
parameters: {
|
|
159
|
+
type: "object",
|
|
160
|
+
properties: {},
|
|
161
|
+
},
|
|
162
|
+
async execute(_toolCallId) {
|
|
163
|
+
try {
|
|
164
|
+
const identity = mesh.getInstanceIdentity();
|
|
165
|
+
const peerId = mesh.getLocalPeerId();
|
|
166
|
+
const addrs = mesh.getMultiaddrs();
|
|
167
|
+
const peers = mesh.getConnectedPeers();
|
|
168
|
+
const lines = [
|
|
169
|
+
`Peer ID: ${peerId || "(not started)"}`,
|
|
170
|
+
`Instance ID: ${identity?.id || "(not initialized)"}`,
|
|
171
|
+
`Instance: ${identity?.bindingComponents.username}@${identity?.bindingComponents.hostname}` || "",
|
|
172
|
+
`Listen Addrs: ${addrs.length > 0 ? addrs.join(", ") : "(none)"}`,
|
|
173
|
+
`Connected: ${peers.length} peer(s)${peers.length > 0 ? ": " + peers.join(", ") : ""}`,
|
|
174
|
+
];
|
|
175
|
+
return {
|
|
176
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
177
|
+
details: {
|
|
178
|
+
peerId,
|
|
179
|
+
instanceId: identity?.id,
|
|
180
|
+
listenAddrs: addrs,
|
|
181
|
+
connectedPeers: peers,
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
return {
|
|
187
|
+
content: [{ type: "text", text: `Error: ${String(err)}` }],
|
|
188
|
+
details: { error: String(err) },
|
|
189
|
+
isError: true,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
},
|
|
115
194
|
];
|
|
116
195
|
}
|
package/dist/src/channel.d.ts
CHANGED
package/dist/src/channel.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createChatChannelPlugin } from "openclaw/plugin-sdk/core";
|
|
2
2
|
import { sendViaMesh } from "./send.js";
|
|
3
|
-
|
|
3
|
+
import { getLibp2pMeshRuntime, hasLibp2pMeshRuntime, } from "../runtime-setter-api.js";
|
|
4
|
+
function buildChannel(getMesh) {
|
|
4
5
|
return createChatChannelPlugin({
|
|
5
6
|
base: {
|
|
6
7
|
id: "libp2p-mesh",
|
|
@@ -39,7 +40,9 @@ export function createLibp2pMeshChannel(mesh) {
|
|
|
39
40
|
name: "default",
|
|
40
41
|
configured: true,
|
|
41
42
|
enabled: true,
|
|
42
|
-
connected:
|
|
43
|
+
connected: hasLibp2pMeshRuntime()
|
|
44
|
+
? getMesh().getConnectedPeers().length > 0
|
|
45
|
+
: false,
|
|
43
46
|
}),
|
|
44
47
|
},
|
|
45
48
|
messaging: {
|
|
@@ -54,13 +57,26 @@ export function createLibp2pMeshChannel(mesh) {
|
|
|
54
57
|
deliveryMode: "gateway",
|
|
55
58
|
sendText: async ({ to, text }) => {
|
|
56
59
|
try {
|
|
57
|
-
await sendViaMesh(
|
|
60
|
+
await sendViaMesh(getMesh(), to, text);
|
|
58
61
|
return { channel: "libp2p-mesh", messageId: `p2p-${Date.now()}` };
|
|
59
62
|
}
|
|
60
63
|
catch (err) {
|
|
61
|
-
return {
|
|
64
|
+
return {
|
|
65
|
+
channel: "libp2p-mesh",
|
|
66
|
+
messageId: `p2p-${Date.now()}`,
|
|
67
|
+
meta: { error: String(err) },
|
|
68
|
+
};
|
|
62
69
|
}
|
|
63
70
|
},
|
|
64
71
|
},
|
|
65
72
|
});
|
|
66
73
|
}
|
|
74
|
+
// Static channel plugin export for the bundled-channel-entry contract.
|
|
75
|
+
// The mesh instance is resolved lazily through runtime-setter-api.ts, which
|
|
76
|
+
// plugin.ts populates after starting the mesh service.
|
|
77
|
+
export const libp2pMeshPlugin = buildChannel(getLibp2pMeshRuntime);
|
|
78
|
+
// Backwards-compatible factory: kept so any caller that still passes the mesh
|
|
79
|
+
// instance directly (e.g. the standalone plugin entry) continues to work.
|
|
80
|
+
export function createLibp2pMeshChannel(mesh) {
|
|
81
|
+
return buildChannel(() => mesh);
|
|
82
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { createLibp2pMeshConfigSchema } from "./index.js";
|
|
3
|
+
describe("createLibp2pMeshConfigSchema - Feishu config", () => {
|
|
4
|
+
const schema = createLibp2pMeshConfigSchema();
|
|
5
|
+
it("should include feishu in jsonSchema properties", () => {
|
|
6
|
+
const props = schema.jsonSchema.properties;
|
|
7
|
+
expect(props).toHaveProperty("feishu");
|
|
8
|
+
});
|
|
9
|
+
it("feishu should be an object type with correct properties", () => {
|
|
10
|
+
const props = schema.jsonSchema.properties;
|
|
11
|
+
const feishu = props.feishu;
|
|
12
|
+
expect(feishu.type).toBe("object");
|
|
13
|
+
expect(feishu.properties).toHaveProperty("appId");
|
|
14
|
+
expect(feishu.properties).toHaveProperty("appSecret");
|
|
15
|
+
expect(feishu.properties).toHaveProperty("webhookPort");
|
|
16
|
+
expect(feishu.properties).toHaveProperty("webhookPath");
|
|
17
|
+
});
|
|
18
|
+
it("feishu.webhookPort should default to 9222", () => {
|
|
19
|
+
const props = schema.jsonSchema.properties;
|
|
20
|
+
expect(props.feishu.properties.webhookPort.default).toBe(9222);
|
|
21
|
+
});
|
|
22
|
+
it("feishu.webhookPath should default to /webhook/feishu", () => {
|
|
23
|
+
const props = schema.jsonSchema.properties;
|
|
24
|
+
expect(props.feishu.properties.webhookPath.default).toBe("/webhook/feishu");
|
|
25
|
+
});
|
|
26
|
+
it("feishu.appId and appSecret should be string type without default", () => {
|
|
27
|
+
const props = schema.jsonSchema.properties;
|
|
28
|
+
expect(props.feishu.properties.appId.type).toBe("string");
|
|
29
|
+
expect(props.feishu.properties.appSecret.type).toBe("string");
|
|
30
|
+
expect(props.feishu.properties.appId.default).toBeUndefined();
|
|
31
|
+
expect(props.feishu.properties.appSecret.default).toBeUndefined();
|
|
32
|
+
});
|
|
33
|
+
it("should parse config with feishu fields", () => {
|
|
34
|
+
const result = schema.safeParse({
|
|
35
|
+
listenAddrs: ["/ip4/0.0.0.0/tcp/0"],
|
|
36
|
+
feishu: {
|
|
37
|
+
appId: "cli-123",
|
|
38
|
+
appSecret: "secret-456",
|
|
39
|
+
webhookPort: 9999,
|
|
40
|
+
webhookPath: "/custom/path",
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
expect(result.success).toBe(true);
|
|
44
|
+
if (result.success) {
|
|
45
|
+
expect(result.data.feishu.appId).toBe("cli-123");
|
|
46
|
+
expect(result.data.feishu.webhookPort).toBe(9999);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
it("should parse config without feishu (feishu is optional)", () => {
|
|
50
|
+
const result = schema.safeParse({
|
|
51
|
+
listenAddrs: ["/ip4/0.0.0.0/tcp/0"],
|
|
52
|
+
});
|
|
53
|
+
expect(result.success).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DHT-based public key registry for cross-instance identity verification.
|
|
3
|
+
*
|
|
4
|
+
* Each OpenClaw instance publishes its Ed25519 pubkey to the DHT under the key:
|
|
5
|
+
* openclaw:pubkey:<instanceId>
|
|
6
|
+
*
|
|
7
|
+
* Other instances can look up this pubkey to verify message signatures.
|
|
8
|
+
*/
|
|
9
|
+
import type { KadDHT } from "@libp2p/kad-dht";
|
|
10
|
+
/**
|
|
11
|
+
* Register this instance's pubkey in the DHT.
|
|
12
|
+
* Other nodes can later look it up to verify signatures from this instance.
|
|
13
|
+
*/
|
|
14
|
+
export declare function registerPubkey(dht: KadDHT, instanceId: string, pubkey: string, logger?: {
|
|
15
|
+
info?: (msg: string) => void;
|
|
16
|
+
debug?: (msg: string) => void;
|
|
17
|
+
warn?: (msg: string) => void;
|
|
18
|
+
}): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Look up a pubkey from the DHT for the given instanceId.
|
|
21
|
+
* Results are cached locally to avoid repeated DHT queries.
|
|
22
|
+
*/
|
|
23
|
+
export declare function lookupPubkey(dht: KadDHT, instanceId: string, logger?: {
|
|
24
|
+
info?: (msg: string) => void;
|
|
25
|
+
debug?: (msg: string) => void;
|
|
26
|
+
warn?: (msg: string) => void;
|
|
27
|
+
}): Promise<string | undefined>;
|
|
28
|
+
/**
|
|
29
|
+
* Clear the local pubkey cache.
|
|
30
|
+
*/
|
|
31
|
+
export declare function clearPubkeyCache(): void;
|
|
32
|
+
/**
|
|
33
|
+
* Get cache stats for observability.
|
|
34
|
+
*/
|
|
35
|
+
export declare function getCacheStats(): {
|
|
36
|
+
size: number;
|
|
37
|
+
keys: string[];
|
|
38
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DHT-based public key registry for cross-instance identity verification.
|
|
3
|
+
*
|
|
4
|
+
* Each OpenClaw instance publishes its Ed25519 pubkey to the DHT under the key:
|
|
5
|
+
* openclaw:pubkey:<instanceId>
|
|
6
|
+
*
|
|
7
|
+
* Other instances can look up this pubkey to verify message signatures.
|
|
8
|
+
*/
|
|
9
|
+
const DHT_KEY_PREFIX = "openclaw:pubkey:";
|
|
10
|
+
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
11
|
+
const pubkeyCache = new Map();
|
|
12
|
+
function encodeKey(instanceId) {
|
|
13
|
+
return new TextEncoder().encode(`${DHT_KEY_PREFIX}${instanceId}`);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Register this instance's pubkey in the DHT.
|
|
17
|
+
* Other nodes can later look it up to verify signatures from this instance.
|
|
18
|
+
*/
|
|
19
|
+
export async function registerPubkey(dht, instanceId, pubkey, logger) {
|
|
20
|
+
const key = encodeKey(instanceId);
|
|
21
|
+
const value = new TextEncoder().encode(pubkey);
|
|
22
|
+
try {
|
|
23
|
+
for await (const event of dht.put(key, value)) {
|
|
24
|
+
// Drain the async iterable; put completes when the iterable ends
|
|
25
|
+
logger?.info?.(`[dht-registry] put event: ${event.name}`);
|
|
26
|
+
}
|
|
27
|
+
logger?.info?.(`[dht-registry] Registered pubkey for ${instanceId}`);
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
logger?.warn?.(`[dht-registry] Failed to register pubkey: ${String(err)}`);
|
|
31
|
+
// Non-fatal: identity verification may degrade but mesh continues
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Look up a pubkey from the DHT for the given instanceId.
|
|
36
|
+
* Results are cached locally to avoid repeated DHT queries.
|
|
37
|
+
*/
|
|
38
|
+
export async function lookupPubkey(dht, instanceId, logger) {
|
|
39
|
+
// 1. Check local cache
|
|
40
|
+
const cached = pubkeyCache.get(instanceId);
|
|
41
|
+
if (cached && cached.expiry > Date.now()) {
|
|
42
|
+
logger?.debug?.(`[dht-registry] Cache hit for ${instanceId}`);
|
|
43
|
+
return cached.pubkey;
|
|
44
|
+
}
|
|
45
|
+
// 2. Query DHT
|
|
46
|
+
const key = encodeKey(instanceId);
|
|
47
|
+
try {
|
|
48
|
+
for await (const event of dht.get(key)) {
|
|
49
|
+
if (event.name === "VALUE") {
|
|
50
|
+
const pubkey = new TextDecoder().decode(event.value);
|
|
51
|
+
pubkeyCache.set(instanceId, {
|
|
52
|
+
pubkey,
|
|
53
|
+
expiry: Date.now() + CACHE_TTL_MS,
|
|
54
|
+
});
|
|
55
|
+
logger?.info?.(`[dht-registry] DHT lookup success for ${instanceId}`);
|
|
56
|
+
return pubkey;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
logger?.warn?.(`[dht-registry] DHT lookup failed for ${instanceId}: ${String(err)}`);
|
|
62
|
+
}
|
|
63
|
+
logger?.info?.(`[dht-registry] No pubkey found for ${instanceId}`);
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Clear the local pubkey cache.
|
|
68
|
+
*/
|
|
69
|
+
export function clearPubkeyCache() {
|
|
70
|
+
pubkeyCache.clear();
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Get cache stats for observability.
|
|
74
|
+
*/
|
|
75
|
+
export function getCacheStats() {
|
|
76
|
+
return {
|
|
77
|
+
size: pubkeyCache.size,
|
|
78
|
+
keys: Array.from(pubkeyCache.keys()),
|
|
79
|
+
};
|
|
80
|
+
}
|