libp2p-mesh 2026.5.13 → 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 +61 -23
- package/api.ts +1 -1
- package/dist/api.d.ts +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +60 -24
- package/dist/runtime-setter-api.d.ts +4 -0
- package/dist/runtime-setter-api.js +19 -0
- package/dist/src/agent-tools.d.ts +63 -24
- package/dist/src/agent-tools.js +69 -34
- package/dist/src/channel.d.ts +1 -0
- package/dist/src/channel.js +20 -4
- package/dist/src/dht-registry.d.ts +38 -0
- package/dist/src/dht-registry.js +80 -0
- package/dist/src/inbound.d.ts +0 -3
- package/dist/src/inbound.js +29 -14
- 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.d.ts +1 -2
- package/dist/src/plugin.js +18 -30
- package/dist/src/types.d.ts +87 -0
- package/index.ts +60 -24
- package/openclaw.plugin.json +72 -33
- package/package.json +19 -7
- package/src/agent-tools.ts +69 -35
- package/src/channel.ts +25 -4
- package/src/dht-registry.ts +105 -0
- package/src/inbound.ts +35 -18
- package/src/instance-id.ts +221 -0
- package/src/mesh.ts +368 -27
- package/src/plugin.ts +25 -36
- package/src/types.ts +95 -0
- package/src/agent-tools-feishu.test.ts +0 -68
- package/src/config-schema.test.ts +0 -63
- package/src/feishu-channel.test.ts +0 -191
- package/src/feishu-channel.ts +0 -253
- package/src/feishu-client.test.ts +0 -303
- package/src/feishu-client.ts +0 -178
- package/src/feishu-e2e.test.ts +0 -90
- package/src/feishu-types.test.ts +0 -125
- package/src/feishu-types.ts +0 -51
- package/src/inbound-feishu.test.ts +0 -91
- package/src/index.ts +0 -1
- package/src/plugin-registration.test.ts +0 -60
package/src/inbound.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { P2PMessage } from "./types.js";
|
|
2
|
-
import
|
|
2
|
+
import { verifyInstanceSignature } from "./instance-id.js";
|
|
3
3
|
|
|
4
4
|
export type InboundHandlerDeps = {
|
|
5
5
|
logger?: {
|
|
@@ -8,29 +8,46 @@ export type InboundHandlerDeps = {
|
|
|
8
8
|
warn?: (msg: string) => void;
|
|
9
9
|
error?: (msg: string) => void;
|
|
10
10
|
};
|
|
11
|
-
sendToChannel?: (channelId: string, target: string, text: string) => Promise<void>;
|
|
12
|
-
feishuClient?: FeishuApiClient;
|
|
13
11
|
};
|
|
14
12
|
|
|
15
13
|
export function handleP2PInbound(msg: P2PMessage, deps: InboundHandlerDeps): void {
|
|
16
|
-
const { logger
|
|
14
|
+
const { logger } = deps;
|
|
15
|
+
const instanceTag = msg.instanceId ? ` [instance: ${msg.instanceId}]` : "";
|
|
16
|
+
const signedTag = msg.signature ? " [signed]" : "";
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
18
|
+
// Verify signature if present
|
|
19
|
+
if (msg.signature && msg.instanceId && msg.pubkey) {
|
|
20
|
+
const signedPayload = JSON.stringify({
|
|
21
|
+
id: msg.id,
|
|
22
|
+
type: msg.type,
|
|
23
|
+
from: msg.from,
|
|
24
|
+
to: msg.to,
|
|
25
|
+
topic: msg.topic,
|
|
26
|
+
payload: msg.payload,
|
|
27
|
+
timestamp: msg.timestamp,
|
|
28
|
+
instanceId: msg.instanceId,
|
|
28
29
|
});
|
|
30
|
+
const valid = verifyInstanceSignature(
|
|
31
|
+
{ id: msg.instanceId, name: "", pubkey: msg.pubkey, binding: "", bindingComponents: { username: "", hostname: "", platform: "" }, createdAt: 0 },
|
|
32
|
+
signedPayload,
|
|
33
|
+
msg.signature,
|
|
34
|
+
);
|
|
35
|
+
if (valid) {
|
|
36
|
+
logger?.info?.(`[libp2p-mesh] Verified signature from instance ${msg.instanceId}`);
|
|
37
|
+
} else {
|
|
38
|
+
logger?.warn?.(`[libp2p-mesh] Invalid signature from instance ${msg.instanceId}`);
|
|
39
|
+
}
|
|
40
|
+
} else if (msg.signature) {
|
|
41
|
+
logger?.warn?.(`[libp2p-mesh] Message has signature but no pubkey; cannot verify`);
|
|
29
42
|
}
|
|
30
43
|
|
|
31
|
-
if (
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
44
|
+
if (msg.type === "broadcast") {
|
|
45
|
+
logger?.info?.(
|
|
46
|
+
`[libp2p-mesh] Broadcast from ${msg.from}${instanceTag}${signedTag} on topic ${msg.topic ?? "(none)"}: ${msg.payload}`,
|
|
47
|
+
);
|
|
48
|
+
} else {
|
|
49
|
+
logger?.info?.(
|
|
50
|
+
`[libp2p-mesh] Direct message from ${msg.from}${instanceTag}${signedTag}: ${msg.payload}`,
|
|
51
|
+
);
|
|
35
52
|
}
|
|
36
53
|
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight Instance Identity module inspired by BAID (Binding Agent ID).
|
|
3
|
+
*
|
|
4
|
+
* BAID core idea: bind multiple identity dimensions (name, code, profile, user)
|
|
5
|
+
* into a single cryptographic identity.
|
|
6
|
+
*
|
|
7
|
+
* Our lightweight adaptation:
|
|
8
|
+
* - Ed25519 keypair for self-sovereign identity (provable via signatures)
|
|
9
|
+
* - Multi-dimensional binding hash: username + hostname + platform
|
|
10
|
+
* - InstanceID format: name@<pubkey_b64url[0:12]>.<binding[0:8]>
|
|
11
|
+
* - Persistent storage in ~/.openclaw/libp2p/instance-id.json
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { createHash, generateKeyPairSync, sign, verify } from "node:crypto";
|
|
15
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
16
|
+
import { homedir, hostname, platform, userInfo } from "node:os";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
|
|
19
|
+
export interface InstanceIdentity {
|
|
20
|
+
/** Full InstanceID string, e.g. "alice-mac@AQIDBAUGBweI.7a3f9e2b" */
|
|
21
|
+
id: string;
|
|
22
|
+
/** Human-readable instance name */
|
|
23
|
+
name: string;
|
|
24
|
+
/** Base64url-encoded Ed25519 public key (SPKI/DER) */
|
|
25
|
+
pubkey: string;
|
|
26
|
+
/** Hex SHA-256 binding hash of environment dimensions */
|
|
27
|
+
binding: string;
|
|
28
|
+
/** Components that contributed to the binding hash */
|
|
29
|
+
bindingComponents: {
|
|
30
|
+
username: string;
|
|
31
|
+
hostname: string;
|
|
32
|
+
platform: string;
|
|
33
|
+
};
|
|
34
|
+
/** Timestamp when the identity was created */
|
|
35
|
+
createdAt: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface PersistedIdentity extends InstanceIdentity {
|
|
39
|
+
/** Base64url-encoded Ed25519 private key (PKCS8/DER) — stored for signing */
|
|
40
|
+
privkey: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface InstanceIDOptions {
|
|
44
|
+
/** Custom instance name (defaults to "<username>-<hostname>") */
|
|
45
|
+
name?: string;
|
|
46
|
+
/** Custom storage path for the identity file */
|
|
47
|
+
customPath?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function resolveInstanceIDPath(customPath?: string): string {
|
|
51
|
+
if (customPath) return customPath;
|
|
52
|
+
const stateDir = process.env.OPENCLAW_STATE_DIR;
|
|
53
|
+
if (stateDir) {
|
|
54
|
+
return path.join(stateDir, "libp2p", "instance-id.json");
|
|
55
|
+
}
|
|
56
|
+
return path.join(homedir(), ".openclaw", "libp2p", "instance-id.json");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getBindingComponents(): InstanceIdentity["bindingComponents"] {
|
|
60
|
+
let username: string;
|
|
61
|
+
try {
|
|
62
|
+
username = userInfo().username;
|
|
63
|
+
} catch {
|
|
64
|
+
username = process.env.USER || process.env.USERNAME || "unknown";
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
username,
|
|
68
|
+
hostname: hostname(),
|
|
69
|
+
platform: platform(),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function computeBindingHash(
|
|
74
|
+
components: InstanceIdentity["bindingComponents"],
|
|
75
|
+
): string {
|
|
76
|
+
const data = `${components.username}::${components.hostname}::${components.platform}`;
|
|
77
|
+
return createHash("sha256").update(data).digest("hex");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function getDefaultName(): string {
|
|
81
|
+
const { username, hostname: h } = getBindingComponents();
|
|
82
|
+
const shortHost = h.split(".")[0];
|
|
83
|
+
return `${username}-${shortHost}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function generateEd25519KeyPair(): { publicKey: Buffer; privateKey: Buffer } {
|
|
87
|
+
const { publicKey, privateKey } = generateKeyPairSync("ed25519", {
|
|
88
|
+
publicKeyEncoding: { type: "spki", format: "der" },
|
|
89
|
+
privateKeyEncoding: { type: "pkcs8", format: "der" },
|
|
90
|
+
});
|
|
91
|
+
return { publicKey: Buffer.from(publicKey), privateKey: Buffer.from(privateKey) };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function pubkeyShort(pubkey: Buffer): string {
|
|
95
|
+
return pubkey.toString("base64url").slice(0, 12);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function bindingShort(binding: string): string {
|
|
99
|
+
return binding.slice(0, 8);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function generateInstanceIdentity(options: InstanceIDOptions = {}): PersistedIdentity {
|
|
103
|
+
const name = options.name?.trim() || getDefaultName();
|
|
104
|
+
const { publicKey, privateKey } = generateEd25519KeyPair();
|
|
105
|
+
const bindingComponents = getBindingComponents();
|
|
106
|
+
const binding = computeBindingHash(bindingComponents);
|
|
107
|
+
|
|
108
|
+
const id = `${name}@${pubkeyShort(publicKey)}.${bindingShort(binding)}`;
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
id,
|
|
112
|
+
name,
|
|
113
|
+
pubkey: publicKey.toString("base64url"),
|
|
114
|
+
privkey: privateKey.toString("base64url"),
|
|
115
|
+
binding,
|
|
116
|
+
bindingComponents,
|
|
117
|
+
createdAt: Date.now(),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function loadOrCreateInstanceIdentity(
|
|
122
|
+
options: InstanceIDOptions = {},
|
|
123
|
+
): Promise<{ identity: InstanceIdentity; signMessage: (message: string) => string }> {
|
|
124
|
+
const filePath = resolveInstanceIDPath(options.customPath);
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
const raw = await readFile(filePath, "utf8");
|
|
128
|
+
const persisted = JSON.parse(raw) as PersistedIdentity;
|
|
129
|
+
|
|
130
|
+
// Validate that the stored identity still matches this environment
|
|
131
|
+
const currentComponents = getBindingComponents();
|
|
132
|
+
const currentBinding = computeBindingHash(currentComponents);
|
|
133
|
+
|
|
134
|
+
if (persisted.binding !== currentBinding) {
|
|
135
|
+
// Environment changed (e.g. migrated to new machine) — regenerate
|
|
136
|
+
const fresh = generateInstanceIdentity(options);
|
|
137
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
138
|
+
await writeFile(filePath, JSON.stringify(fresh, null, 2));
|
|
139
|
+
return {
|
|
140
|
+
identity: stripPrivateKey(fresh),
|
|
141
|
+
signMessage: (msg) => signWithKey(Buffer.from(fresh.privkey, "base64url"), msg),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
identity: stripPrivateKey(persisted),
|
|
147
|
+
signMessage: (msg) => signWithKey(Buffer.from(persisted.privkey, "base64url"), msg),
|
|
148
|
+
};
|
|
149
|
+
} catch {
|
|
150
|
+
// File doesn't exist or is corrupt — create new identity
|
|
151
|
+
const fresh = generateInstanceIdentity(options);
|
|
152
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
153
|
+
await writeFile(filePath, JSON.stringify(fresh, null, 2));
|
|
154
|
+
return {
|
|
155
|
+
identity: stripPrivateKey(fresh),
|
|
156
|
+
signMessage: (msg) => signWithKey(Buffer.from(fresh.privkey, "base64url"), msg),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function stripPrivateKey(persisted: PersistedIdentity): InstanceIdentity {
|
|
162
|
+
const { privkey: _, ...identity } = persisted;
|
|
163
|
+
return identity;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function signWithKey(privateKey: Buffer, message: string): string {
|
|
167
|
+
const sig = sign(null, Buffer.from(message, "utf8"), {
|
|
168
|
+
key: privateKey,
|
|
169
|
+
format: "der",
|
|
170
|
+
type: "pkcs8",
|
|
171
|
+
});
|
|
172
|
+
return sig.toString("base64url");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function verifyInstanceSignature(
|
|
176
|
+
identity: InstanceIdentity,
|
|
177
|
+
message: string,
|
|
178
|
+
signature: string,
|
|
179
|
+
): boolean {
|
|
180
|
+
try {
|
|
181
|
+
const pubkeyBuffer = Buffer.from(identity.pubkey, "base64url");
|
|
182
|
+
return verify(
|
|
183
|
+
null,
|
|
184
|
+
Buffer.from(message, "utf8"),
|
|
185
|
+
{ key: pubkeyBuffer, format: "der", type: "spki" },
|
|
186
|
+
Buffer.from(signature, "base64url"),
|
|
187
|
+
);
|
|
188
|
+
} catch {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function verifyInstanceIDBinding(
|
|
194
|
+
identity: InstanceIdentity,
|
|
195
|
+
): { valid: boolean; currentBinding: string; mismatch?: string } {
|
|
196
|
+
const currentComponents = getBindingComponents();
|
|
197
|
+
const currentBinding = computeBindingHash(currentComponents);
|
|
198
|
+
|
|
199
|
+
if (identity.binding !== currentBinding) {
|
|
200
|
+
return {
|
|
201
|
+
valid: false,
|
|
202
|
+
currentBinding,
|
|
203
|
+
mismatch: `Stored binding ${identity.binding.slice(0, 8)} does not match current environment ${currentBinding.slice(0, 8)}`,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return { valid: true, currentBinding };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function formatInstanceIDForDisplay(identity: InstanceIdentity): string {
|
|
211
|
+
const { bindingComponents, createdAt } = identity;
|
|
212
|
+
const date = new Date(createdAt).toLocaleString();
|
|
213
|
+
return [
|
|
214
|
+
`Instance ID: ${identity.id}`,
|
|
215
|
+
`Name: ${identity.name}`,
|
|
216
|
+
`Pubkey: ${identity.pubkey.slice(0, 24)}...`,
|
|
217
|
+
`Binding: ${identity.binding.slice(0, 16)}...`,
|
|
218
|
+
`Bound to: ${bindingComponents.username}@${bindingComponents.hostname} (${bindingComponents.platform})`,
|
|
219
|
+
`Created: ${date}`,
|
|
220
|
+
].join("\n");
|
|
221
|
+
}
|