@openvole/volenet 1.0.0
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/LICENSE +21 -0
- package/README.md +75 -0
- package/dist/index.d.ts +1673 -0
- package/dist/index.js +6363 -0
- package/dist/index.js.map +1 -0
- package/package.json +53 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,1673 @@
|
|
|
1
|
+
import * as crypto from 'node:crypto';
|
|
2
|
+
import { KeyObject } from 'node:crypto';
|
|
3
|
+
import * as http from 'node:http';
|
|
4
|
+
|
|
5
|
+
/** What an outbox entry is. Consent traffic waits the same way chat does — a request that
|
|
6
|
+
* vanished because the other side was away was the first thing a phone user noticed. */
|
|
7
|
+
type OutboxKind = 'chat' | 'connect-request' | 'connect-accept' | 'connect-deny';
|
|
8
|
+
/** A message waiting on its recipient. `sentAt` is when it was written, which is what the recipient should see. */
|
|
9
|
+
interface OutboxEntry {
|
|
10
|
+
ref: string;
|
|
11
|
+
to: string;
|
|
12
|
+
toName: string;
|
|
13
|
+
text: string;
|
|
14
|
+
sentAt: number;
|
|
15
|
+
attempts: number;
|
|
16
|
+
lastError?: string;
|
|
17
|
+
/** Absent on entries written before consent traffic was held: chat. */
|
|
18
|
+
kind?: OutboxKind;
|
|
19
|
+
/** connect-request only. */
|
|
20
|
+
note?: string;
|
|
21
|
+
}
|
|
22
|
+
/** What a hub tells a member on reconnect about one sender. */
|
|
23
|
+
interface RelayNotice {
|
|
24
|
+
from: string;
|
|
25
|
+
fromName: string;
|
|
26
|
+
count: number;
|
|
27
|
+
first: number;
|
|
28
|
+
last: number;
|
|
29
|
+
}
|
|
30
|
+
declare const DEFAULT_OUTBOX_TTL_MS: number;
|
|
31
|
+
declare const DEFAULT_NOTICE_TTL_MS: number;
|
|
32
|
+
declare class ChatOutbox {
|
|
33
|
+
private readonly file;
|
|
34
|
+
private readonly ttlMs;
|
|
35
|
+
private entries;
|
|
36
|
+
constructor(file: string, ttlMs?: number);
|
|
37
|
+
load(now?: number): Promise<void>;
|
|
38
|
+
list(): OutboxEntry[];
|
|
39
|
+
forPeer(to: string): OutboxEntry[];
|
|
40
|
+
get size(): number;
|
|
41
|
+
add(entry: Omit<OutboxEntry, 'ref' | 'attempts'> & {
|
|
42
|
+
ref?: string;
|
|
43
|
+
}): Promise<OutboxEntry>;
|
|
44
|
+
remove(ref: string): Promise<boolean>;
|
|
45
|
+
noteAttempt(ref: string, error?: string): Promise<void>;
|
|
46
|
+
/** Drop entries older than the TTL. Returns what was dropped so the owner can say so. */
|
|
47
|
+
sweep(now?: number): Promise<OutboxEntry[]>;
|
|
48
|
+
private persist;
|
|
49
|
+
}
|
|
50
|
+
declare class RelayNotices {
|
|
51
|
+
private readonly file;
|
|
52
|
+
private readonly ttlMs;
|
|
53
|
+
/** to → (from → notice) */
|
|
54
|
+
private byMember;
|
|
55
|
+
constructor(file: string, ttlMs?: number);
|
|
56
|
+
load(now?: number): Promise<void>;
|
|
57
|
+
/** Remember that `from` tried to reach `to` just now. */
|
|
58
|
+
record(to: string, from: string, fromName: string, now?: number): Promise<void>;
|
|
59
|
+
/** Hand over everything waiting for `to`, and forget it. */
|
|
60
|
+
take(to: string, now?: number): Promise<RelayNotice[]>;
|
|
61
|
+
/** Put notices back that could not be handed over after all (the member dropped off again). */
|
|
62
|
+
restore(to: string, notices: RelayNotice[]): Promise<void>;
|
|
63
|
+
peek(to: string): RelayNotice[];
|
|
64
|
+
/** Members with something waiting. */
|
|
65
|
+
get size(): number;
|
|
66
|
+
sweep(now?: number): Promise<void>;
|
|
67
|
+
private persist;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* VoleNet Protocol — message types, serialization, validation.
|
|
72
|
+
* All messages are signed with Ed25519 for integrity and authenticity.
|
|
73
|
+
*/
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The local node's post-quantum (ML-DSA) signing key, set once at VoleNet start.
|
|
77
|
+
* Module-level because there is exactly one signing identity per process — this lets
|
|
78
|
+
*/
|
|
79
|
+
type VoleNetMessageType = 'ping' | 'pong' | 'discover' | 'discover:response' | 'auth:challenge' | 'auth:response' | 'auth:result' | 'task:delegate' | 'task:result' | 'task:status' | 'memory:sync' | 'memory:search' | 'memory:results' | 'session:sync' | 'tool:list' | 'tool:list:response' | 'tool:call' | 'tool:result' | 'leader:heartbeat' | 'leader:claim' | 'leader:ack' | 'chat:message' | 'sealed' | 'sealed:direct' | 'relay:deliver' | 'relay:error' | 'relay:ack' | 'relay:pending' | 'roster' | 'relay:connect-request' | 'relay:connect-accept' | 'relay:connect-deny' | 'file:offer' | 'file:accept' | 'file:reject' | 'file:relay-ready' | 'file:done' | 'file:error' | 'file:cancel' | 'relay:blob:create' | 'relay:blob:grant' | 'relay:blob:deny' | 'relay:blob:fetch' | 'relay:blob:done';
|
|
80
|
+
interface VoleNetMessage {
|
|
81
|
+
version: number;
|
|
82
|
+
id: string;
|
|
83
|
+
type: VoleNetMessageType;
|
|
84
|
+
from: string;
|
|
85
|
+
to: string | '*';
|
|
86
|
+
timestamp: number;
|
|
87
|
+
signature: string;
|
|
88
|
+
sigPq?: string;
|
|
89
|
+
payload: unknown;
|
|
90
|
+
}
|
|
91
|
+
interface VoleNetInstance {
|
|
92
|
+
id: string;
|
|
93
|
+
name: string;
|
|
94
|
+
publicKey: string;
|
|
95
|
+
endpoint: string;
|
|
96
|
+
capabilities: string[];
|
|
97
|
+
role: 'coordinator' | 'worker' | 'peer';
|
|
98
|
+
load: number;
|
|
99
|
+
maxTasks: number;
|
|
100
|
+
lastSeen: number;
|
|
101
|
+
version: string;
|
|
102
|
+
/** X25519 public key (base64 SPKI) for sealed envelopes, when the peer announces one. */
|
|
103
|
+
xPublicKey?: string;
|
|
104
|
+
/** ML-KEM-768 public key (base64 SPKI) — the post-quantum half of the hybrid seal. */
|
|
105
|
+
mlkemPublicKey?: string;
|
|
106
|
+
}
|
|
107
|
+
interface RemoteToolInfo {
|
|
108
|
+
name: string;
|
|
109
|
+
description: string;
|
|
110
|
+
pawName: string;
|
|
111
|
+
instanceId: string;
|
|
112
|
+
instanceName: string;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Create a signed VoleNet message.
|
|
116
|
+
*/
|
|
117
|
+
declare function createMessage(type: VoleNetMessageType, from: string, to: string | '*', payload: unknown, privateKey: KeyObject, pqPrivateKey?: KeyObject): VoleNetMessage;
|
|
118
|
+
/**
|
|
119
|
+
* Verify a received message's signature and freshness.
|
|
120
|
+
*/
|
|
121
|
+
declare function verifyMessage(message: VoleNetMessage, publicKey: KeyObject, pqPublicKey?: KeyObject): {
|
|
122
|
+
valid: boolean;
|
|
123
|
+
error?: string;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* VoleNet Transport — HTTP server + WebSocket for peer communication.
|
|
128
|
+
*
|
|
129
|
+
* Two modes:
|
|
130
|
+
* - HTTP POST: for initial auth and one-shot messages (fallback)
|
|
131
|
+
* - WebSocket: for persistent bidirectional messaging (preferred)
|
|
132
|
+
*
|
|
133
|
+
* WebSocket enables NAT traversal — the peer behind NAT connects out,
|
|
134
|
+
* and the other side pushes through the open connection.
|
|
135
|
+
* Auto-reconnect with exponential backoff on disconnect.
|
|
136
|
+
*/
|
|
137
|
+
|
|
138
|
+
interface TransportConfig {
|
|
139
|
+
port: number;
|
|
140
|
+
tls?: {
|
|
141
|
+
cert: string;
|
|
142
|
+
key: string;
|
|
143
|
+
};
|
|
144
|
+
/** Max inbound messages per minute per source (IP / WS connection). Default 1200. */
|
|
145
|
+
maxMessagesPerMinute?: number;
|
|
146
|
+
/** Max concurrent inbound WebSocket connections (DoS). Default 1000. */
|
|
147
|
+
maxConnections?: number;
|
|
148
|
+
/** Close an inbound WS that doesn't send a verified message within this window, ms (DoS). Default 10000. */
|
|
149
|
+
authTimeoutMs?: number;
|
|
150
|
+
/** Global inbound message ceiling per second across all sources (load shed). Default 5000. */
|
|
151
|
+
maxMessagesPerSecond?: number;
|
|
152
|
+
/** Publish peer display names in /volenet/info (off by default — names are enumeration surface). */
|
|
153
|
+
publishNames?: boolean;
|
|
154
|
+
/** Liveness: ping every socket this often (ms) and drop one that misses a pong. Default 20000; 0 disables. */
|
|
155
|
+
pingIntervalMs?: number;
|
|
156
|
+
}
|
|
157
|
+
type MessageHandler = (message: VoleNetMessage, peerId: string) => void;
|
|
158
|
+
type JoinHandler = (body: unknown, ip: string) => Promise<{
|
|
159
|
+
status: number;
|
|
160
|
+
json: unknown;
|
|
161
|
+
}>;
|
|
162
|
+
/**
|
|
163
|
+
* VoleNet Transport layer.
|
|
164
|
+
* HTTP server for initial connections + WebSocket for persistent messaging.
|
|
165
|
+
*/
|
|
166
|
+
declare class VoleNetTransport {
|
|
167
|
+
private server;
|
|
168
|
+
private wss;
|
|
169
|
+
private peers;
|
|
170
|
+
private messageHandlers;
|
|
171
|
+
/** Optional outbound transform — wraps a message in a sealed:direct envelope before sending. */
|
|
172
|
+
private sealer;
|
|
173
|
+
/** Resolves a full peerId to its announced display name (for /volenet/info when publishNames). */
|
|
174
|
+
private nameResolver;
|
|
175
|
+
private joinHandler;
|
|
176
|
+
private pairHandler;
|
|
177
|
+
private identityProvider;
|
|
178
|
+
/** VoleDrop data plane — handles /volenet/blob/* streamed requests. */
|
|
179
|
+
private blobHandler;
|
|
180
|
+
private msgWindow;
|
|
181
|
+
private wsConnSeq;
|
|
182
|
+
private config;
|
|
183
|
+
private started;
|
|
184
|
+
private wsConnections;
|
|
185
|
+
private globalWindow;
|
|
186
|
+
private verifyFn?;
|
|
187
|
+
private responder?;
|
|
188
|
+
private onConnectCbs;
|
|
189
|
+
private onDisconnectCbs;
|
|
190
|
+
private seenMsgs;
|
|
191
|
+
/** Every socket this transport owns, either direction — so stop() closes orphans too. */
|
|
192
|
+
private sockets;
|
|
193
|
+
private alive;
|
|
194
|
+
private pingTimer;
|
|
195
|
+
constructor(config: TransportConfig);
|
|
196
|
+
/** Register a handler for public self-join requests (HTTP POST /volenet/join). */
|
|
197
|
+
setJoinHandler(handler: JoinHandler): void;
|
|
198
|
+
/** Register a handler for consent-based pairing requests (HTTP POST /volenet/pair). */
|
|
199
|
+
setPairHandler(handler: JoinHandler): void;
|
|
200
|
+
/**
|
|
201
|
+
* Provide this node's own public identity for /volenet/info — the key `vole net pair`
|
|
202
|
+
* fetches (and fingerprints) before asking the operator here for consent. Public keys
|
|
203
|
+
* are announced to every peer anyway; exposing one here is not an enumeration surface.
|
|
204
|
+
*/
|
|
205
|
+
setIdentityProvider(fn: () => {
|
|
206
|
+
publicKey: string;
|
|
207
|
+
name?: string;
|
|
208
|
+
instanceId?: string;
|
|
209
|
+
}): void;
|
|
210
|
+
/**
|
|
211
|
+
* Register the VoleDrop blob handler for /volenet/blob/* — the file-transfer data
|
|
212
|
+
* plane. Returns true when it handled the request. Auth (per-transfer tokens), byte
|
|
213
|
+
* budgets, and concurrency caps are the handler's responsibility: these routes carry
|
|
214
|
+
* large streamed bodies and deliberately bypass the JSON message pipeline.
|
|
215
|
+
*/
|
|
216
|
+
setBlobHandler(fn: (req: http.IncomingMessage, res: http.ServerResponse, pathname: string) => boolean): void;
|
|
217
|
+
/**
|
|
218
|
+
* Inject a signature verifier. An inbound WebSocket is bound to a peer id ONLY after a
|
|
219
|
+
* message from it verifies — so an attacker can't claim a victim's id and hijack its
|
|
220
|
+
* downstream traffic.
|
|
221
|
+
*/
|
|
222
|
+
setVerifier(fn: (message: VoleNetMessage) => boolean): void;
|
|
223
|
+
/**
|
|
224
|
+
* Inject a request responder for request/response messages (e.g. `discover` over HTTP).
|
|
225
|
+
* The returned message is delivered inline in the HTTP response body, so a peer behind NAT
|
|
226
|
+
* learns the responder's identity without the responder having to dial it back.
|
|
227
|
+
*/
|
|
228
|
+
setResponder(fn: (message: VoleNetMessage) => VoleNetMessage | null): void;
|
|
229
|
+
/**
|
|
230
|
+
* Called when an outbound WebSocket to a peer opens. Lets the owner push a signed message
|
|
231
|
+
* immediately so the remote side binds this socket without waiting for the next heartbeat —
|
|
232
|
+
* which is what makes reverse delivery (hub→NAT'd-follower) consistent right after (re)connect.
|
|
233
|
+
*/
|
|
234
|
+
setOnConnect(fn: (peerId: string) => void): void;
|
|
235
|
+
setOnDisconnect(fn: (peerId: string) => void): void;
|
|
236
|
+
/** Global sliding-window message ceiling across all sources (load shed). False when over. */
|
|
237
|
+
private globalRateAllow;
|
|
238
|
+
/**
|
|
239
|
+
* Central inbound gate. Every message must (a) verify — valid signature from an authorized
|
|
240
|
+
* peer — and (b) not be a replay of a recently-accepted (from,id), before it reaches ANY
|
|
241
|
+
* handler. Fails closed if no verifier is wired. This makes verification a single chokepoint
|
|
242
|
+
* so an individual handler can never forget to check.
|
|
243
|
+
*/
|
|
244
|
+
private verifyAndAccept;
|
|
245
|
+
/** Sliding-window rate limit per source (IP or WS connection). Returns false when over. */
|
|
246
|
+
private rateAllow;
|
|
247
|
+
/**
|
|
248
|
+
* Start the transport server (HTTP + WebSocket).
|
|
249
|
+
*/
|
|
250
|
+
start(): Promise<void>;
|
|
251
|
+
/** Bind the listening port, retrying briefly on EADDRINUSE (covers restart races). */
|
|
252
|
+
private listen;
|
|
253
|
+
/**
|
|
254
|
+
* Stop the transport.
|
|
255
|
+
*/
|
|
256
|
+
stop(): Promise<void>;
|
|
257
|
+
onMessage(handler: MessageHandler): void;
|
|
258
|
+
/** Install the outbound seal transform (direct end-to-end encryption). */
|
|
259
|
+
setSealer(fn: (peerId: string, message: VoleNetMessage) => VoleNetMessage): void;
|
|
260
|
+
/** Install the display-name resolver used by /volenet/info when publishNames is enabled. */
|
|
261
|
+
setNameResolver(fn: (peerId: string) => string | undefined): void;
|
|
262
|
+
/**
|
|
263
|
+
* Feed a message into the normal receive pipeline as if it arrived over the wire. Used to
|
|
264
|
+
* re-dispatch the inner message recovered from a sealed:direct envelope, so every handler
|
|
265
|
+
* (tool calls, sync, chat) processes it — with full signature/replay/authorization checks —
|
|
266
|
+
* exactly as an unencrypted direct message.
|
|
267
|
+
*/
|
|
268
|
+
injectMessage(message: VoleNetMessage): boolean;
|
|
269
|
+
/**
|
|
270
|
+
* Send a message to a peer.
|
|
271
|
+
* Prefers WebSocket (instant, bidirectional), falls back to HTTP POST.
|
|
272
|
+
*/
|
|
273
|
+
sendToPeer(peerId: string, message: VoleNetMessage): Promise<boolean>;
|
|
274
|
+
broadcast(message: VoleNetMessage): Promise<number>;
|
|
275
|
+
/**
|
|
276
|
+
* Register a peer and initiate WebSocket connection.
|
|
277
|
+
*/
|
|
278
|
+
addPeer(peerId: string, endpoint: string): void;
|
|
279
|
+
removePeer(peerId: string): void;
|
|
280
|
+
/**
|
|
281
|
+
* One socket per pair.
|
|
282
|
+
*
|
|
283
|
+
* Two nodes that can both dial end up with two sockets between them, and each side binds the
|
|
284
|
+
* OTHER's dial as the peer's socket while its own stays open, unreferenced. A stop() closes
|
|
285
|
+
* what it references and leaves the orphan; the far side then keeps believing the peer is
|
|
286
|
+
* connected, on a socket nothing will ever close, and forwards into it.
|
|
287
|
+
*
|
|
288
|
+
* When a second socket appears for a peer, the node with the smaller id counts as the dialer
|
|
289
|
+
* and both sides keep that one — so they agree — and close the other. Only a genuine double
|
|
290
|
+
* triggers this: a node that cannot be dialed keeps whatever socket it has. Without a known
|
|
291
|
+
* self id, the newest wins.
|
|
292
|
+
*
|
|
293
|
+
* @returns false when the new socket was the one let go.
|
|
294
|
+
*/
|
|
295
|
+
private adopt;
|
|
296
|
+
/** Close a socket we are done with. Its close handler finds no peer referencing it and does nothing. */
|
|
297
|
+
private drop;
|
|
298
|
+
private track;
|
|
299
|
+
/**
|
|
300
|
+
* Liveness. A phone that walks out of Wi-Fi sends no close frame; without this its socket
|
|
301
|
+
* would stay OPEN here for as long as TCP takes to notice, and everything sent to it is lost.
|
|
302
|
+
* The ws library answers pings on its own, so the far side needs nothing.
|
|
303
|
+
*/
|
|
304
|
+
private startPinging;
|
|
305
|
+
getPeers(): Array<{
|
|
306
|
+
peerId: string;
|
|
307
|
+
endpoint: string;
|
|
308
|
+
connected: boolean;
|
|
309
|
+
lastSeen: number;
|
|
310
|
+
transport: 'websocket' | 'http' | 'disconnected';
|
|
311
|
+
}>;
|
|
312
|
+
isPeerConnected(peerId: string): boolean;
|
|
313
|
+
pingPeer(endpoint: string): Promise<boolean>;
|
|
314
|
+
/**
|
|
315
|
+
* Connect to a peer via WebSocket.
|
|
316
|
+
*/
|
|
317
|
+
private connectWebSocket;
|
|
318
|
+
/**
|
|
319
|
+
* Schedule WebSocket reconnect with exponential backoff.
|
|
320
|
+
*/
|
|
321
|
+
private scheduleReconnect;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* VoleNet Discovery — peer registry, capability announcement, health monitoring.
|
|
326
|
+
*/
|
|
327
|
+
|
|
328
|
+
interface DiscoveryConfig {
|
|
329
|
+
netDir: string;
|
|
330
|
+
instanceId: string;
|
|
331
|
+
instanceName: string;
|
|
332
|
+
role: 'coordinator' | 'worker' | 'peer';
|
|
333
|
+
endpoint: string;
|
|
334
|
+
capabilities: string[];
|
|
335
|
+
privateKey: KeyObject;
|
|
336
|
+
/** ML-DSA-65 signing key — hybrid signatures when present. */
|
|
337
|
+
pqPrivateKey?: KeyObject;
|
|
338
|
+
publicKeyString: string;
|
|
339
|
+
/** Peer URLs from vole.config.json — used only to warn when one goes stale (endpoint drift). */
|
|
340
|
+
configuredPeerUrls?: string[];
|
|
341
|
+
/** Our X25519 public key (base64 SPKI), announced so peers can seal envelopes to us. */
|
|
342
|
+
xPublicKeyB64?: string;
|
|
343
|
+
/** Our ML-KEM-768 public key (base64 SPKI), announced for post-quantum hybrid seals. */
|
|
344
|
+
mlkemPublicKeyB64?: string;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* The configured URL that has gone stale for a peer, or null. A peer's advertised endpoint is
|
|
348
|
+
* what we actually reconnect to at runtime; if the config lists a DIFFERENT url on the SAME
|
|
349
|
+
* hostname (e.g. the old ":9710" after a hub moved behind a proxied "/mesh" path), the config
|
|
350
|
+
* entry silently stops working on the next restart — worth a loud hint now. Different-host
|
|
351
|
+
* advertisements (other mesh members, NAT'd guests) are none of the config's business.
|
|
352
|
+
*/
|
|
353
|
+
declare function findEndpointDrift(configuredUrls: string[] | undefined, advertised: string): string | null;
|
|
354
|
+
declare class VoleNetDiscovery {
|
|
355
|
+
private instances;
|
|
356
|
+
private remoteTools;
|
|
357
|
+
private transport;
|
|
358
|
+
private config;
|
|
359
|
+
private healthTimer;
|
|
360
|
+
private authorizedPeers;
|
|
361
|
+
private onPeerChanged?;
|
|
362
|
+
/** Instances already warned about a stale configured URL — hint once, not every health cycle. */
|
|
363
|
+
private driftHinted;
|
|
364
|
+
constructor(transport: VoleNetTransport, config: DiscoveryConfig);
|
|
365
|
+
/**
|
|
366
|
+
* Start discovery — load authorized peers, announce self, begin health checks.
|
|
367
|
+
*/
|
|
368
|
+
/** Set callback for when peers join/leave (triggers leader re-election) */
|
|
369
|
+
setOnPeerChanged(handler: () => void): void;
|
|
370
|
+
start(): Promise<void>;
|
|
371
|
+
/**
|
|
372
|
+
* Stop discovery — cleanup timers.
|
|
373
|
+
*/
|
|
374
|
+
stop(): void;
|
|
375
|
+
/**
|
|
376
|
+
* Connect to a peer and perform authentication.
|
|
377
|
+
*/
|
|
378
|
+
connectToPeer(endpoint: string): Promise<string | null>;
|
|
379
|
+
/**
|
|
380
|
+
* Handle incoming VoleNet messages.
|
|
381
|
+
*/
|
|
382
|
+
private handleMessage;
|
|
383
|
+
/**
|
|
384
|
+
* Verify an inbound message is from an authorized peer (keystore + Ed25519 signature).
|
|
385
|
+
* Used by non-discovery handlers (e.g. peer chat) that receive raw transport messages.
|
|
386
|
+
*/
|
|
387
|
+
verifyMessageFrom(message: VoleNetMessage): boolean;
|
|
388
|
+
/**
|
|
389
|
+
* Handle discovery announcement from a peer.
|
|
390
|
+
*/
|
|
391
|
+
/**
|
|
392
|
+
* Build a signed discover:response for an inbound discover, with NO side effects.
|
|
393
|
+
* Wired as the transport's HTTP responder so a peer behind NAT receives our identity
|
|
394
|
+
* inline in its own request's response — it can't be reached by a dial-back. Returns
|
|
395
|
+
* null unless the discover is from an authorized peer with a valid signature.
|
|
396
|
+
*/
|
|
397
|
+
buildDiscoverResponse(message: VoleNetMessage): VoleNetMessage | null;
|
|
398
|
+
private handleDiscover;
|
|
399
|
+
/**
|
|
400
|
+
* Handle discovery response.
|
|
401
|
+
*/
|
|
402
|
+
private handleDiscoverResponse;
|
|
403
|
+
private handlePing;
|
|
404
|
+
private handlePong;
|
|
405
|
+
private handleToolList;
|
|
406
|
+
/**
|
|
407
|
+
* Health check — ping all peers, remove stale ones.
|
|
408
|
+
*/
|
|
409
|
+
/** Send a signed ping to a peer — fired on WS connect for immediate binding, and by healthCheck. */
|
|
410
|
+
/** Warn (once per instance) when a configured peer URL no longer matches what it advertises. */
|
|
411
|
+
private hintEndpointDrift;
|
|
412
|
+
private sendPing;
|
|
413
|
+
private healthCheck;
|
|
414
|
+
/**
|
|
415
|
+
* Get all connected instances.
|
|
416
|
+
*/
|
|
417
|
+
getInstances(): VoleNetInstance[];
|
|
418
|
+
/**
|
|
419
|
+
* Get all remote tools across all peers.
|
|
420
|
+
*/
|
|
421
|
+
getRemoteTools(): RemoteToolInfo[];
|
|
422
|
+
/**
|
|
423
|
+
* Find which peer has a specific tool.
|
|
424
|
+
*/
|
|
425
|
+
findToolOwner(toolName: string): {
|
|
426
|
+
instanceId: string;
|
|
427
|
+
instance: VoleNetInstance;
|
|
428
|
+
} | null;
|
|
429
|
+
/**
|
|
430
|
+
* Reload authorized peers from disk.
|
|
431
|
+
*/
|
|
432
|
+
reloadAuthorized(): Promise<void>;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* VoleNet Ed25519 key management.
|
|
437
|
+
* Each vole instance has a keypair for identity and message signing.
|
|
438
|
+
*
|
|
439
|
+
* Key format: "vole-ed25519 <base64-ed25519-public-key> <instance-name> [base64-ml-dsa-public-key]"
|
|
440
|
+
* The 4th field (ML-DSA public key) is present when post-quantum support is available
|
|
441
|
+
* — the identity is then a hybrid Ed25519 + ML-DSA keypair.
|
|
442
|
+
* Storage: .openvole/net/vole_key (private), vole_key.pub (public), vole_key.pq (ML-DSA private)
|
|
443
|
+
* .openvole/net/authorized_voles (trusted peer public keys)
|
|
444
|
+
*/
|
|
445
|
+
|
|
446
|
+
interface VoleKeyPair {
|
|
447
|
+
publicKey: crypto.KeyObject;
|
|
448
|
+
privateKey: crypto.KeyObject;
|
|
449
|
+
publicKeyString: string;
|
|
450
|
+
instanceId: string;
|
|
451
|
+
/** Post-quantum (ML-DSA-65) keys — present when the runtime supports it (OpenSSL 3.5+ / Node 24+). */
|
|
452
|
+
pqPublicKey?: crypto.KeyObject;
|
|
453
|
+
pqPrivateKey?: crypto.KeyObject;
|
|
454
|
+
/** X25519 key-agreement keys for sealed (end-to-end encrypted) envelopes. */
|
|
455
|
+
xPublicKey?: crypto.KeyObject;
|
|
456
|
+
xPrivateKey?: crypto.KeyObject;
|
|
457
|
+
/** Base64 SPKI of xPublicKey — announced to peers via discovery. */
|
|
458
|
+
xPublicKeyB64?: string;
|
|
459
|
+
/** ML-KEM-768 keys — the post-quantum half of the hybrid seal (OpenSSL 3.5+ / Node 24+). */
|
|
460
|
+
mlkemPublicKey?: crypto.KeyObject;
|
|
461
|
+
mlkemPrivateKey?: crypto.KeyObject;
|
|
462
|
+
/** Base64 SPKI of mlkemPublicKey — announced to peers via discovery alongside xPublicKeyB64. */
|
|
463
|
+
mlkemPublicKeyB64?: string;
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Generate a new Ed25519 keypair.
|
|
467
|
+
* Saves to .openvole/net/vole_key and vole_key.pub
|
|
468
|
+
*/
|
|
469
|
+
declare function generateKeyPair(netDir: string, instanceName: string): Promise<VoleKeyPair>;
|
|
470
|
+
/**
|
|
471
|
+
* Load existing keypair from disk.
|
|
472
|
+
*/
|
|
473
|
+
declare function loadKeyPair(netDir: string): Promise<VoleKeyPair | null>;
|
|
474
|
+
/**
|
|
475
|
+
* Parse a public key string ("vole-ed25519 <base64> <name>") into a KeyObject.
|
|
476
|
+
*/
|
|
477
|
+
declare function parsePublicKey(keyString: string): {
|
|
478
|
+
publicKey: crypto.KeyObject;
|
|
479
|
+
instanceId: string;
|
|
480
|
+
name: string;
|
|
481
|
+
pqPublicKey?: crypto.KeyObject;
|
|
482
|
+
} | null;
|
|
483
|
+
/**
|
|
484
|
+
* Load trusted peer public keys from authorized_voles file.
|
|
485
|
+
*/
|
|
486
|
+
declare function loadAuthorizedVoles(netDir: string): Promise<Map<string, {
|
|
487
|
+
publicKey: crypto.KeyObject;
|
|
488
|
+
name: string;
|
|
489
|
+
pqPublicKey?: crypto.KeyObject;
|
|
490
|
+
}>>;
|
|
491
|
+
/**
|
|
492
|
+
* Add a peer's public key to authorized_voles.
|
|
493
|
+
*/
|
|
494
|
+
declare function trustPeer(netDir: string, publicKeyString: string, opts?: {
|
|
495
|
+
allowUpgrade?: boolean;
|
|
496
|
+
}): Promise<string>;
|
|
497
|
+
/**
|
|
498
|
+
* Remove a peer's public key from authorized_voles.
|
|
499
|
+
*/
|
|
500
|
+
declare function revokePeer(netDir: string, instanceIdOrKey: string): Promise<boolean>;
|
|
501
|
+
/**
|
|
502
|
+
* Relay consent store: peers whose relayed (hub-forwarded) chat this vole accepts. Distinct from
|
|
503
|
+
* authorized_voles — accepting relay contact from a peer does NOT grant it direct-connect trust
|
|
504
|
+
* (memory sync, tool sharing). Same on-disk line format so keys stay pinned to identities.
|
|
505
|
+
* Storage: .openvole/net/relay_accepts
|
|
506
|
+
*/
|
|
507
|
+
declare function loadRelayAccepts(netDir: string): Promise<Map<string, {
|
|
508
|
+
publicKey: crypto.KeyObject;
|
|
509
|
+
name: string;
|
|
510
|
+
pqPublicKey?: crypto.KeyObject;
|
|
511
|
+
}>>;
|
|
512
|
+
/** Record consent to receive relayed contact from a peer (append its pinned key line). */
|
|
513
|
+
declare function addRelayAccept(netDir: string, publicKeyString: string): Promise<string>;
|
|
514
|
+
/** Withdraw relay consent for a peer (remove its line from relay_accepts). */
|
|
515
|
+
declare function removeRelayAccept(netDir: string, instanceIdOrKey: string): Promise<boolean>;
|
|
516
|
+
|
|
517
|
+
interface VoleNetFilesConfig {
|
|
518
|
+
enabled?: boolean;
|
|
519
|
+
inboxDir?: string;
|
|
520
|
+
/**
|
|
521
|
+
* Largest file this node will accept over a direct transfer. Default 2 GiB; `0` means no
|
|
522
|
+
* limit. Transfers are chunked, resumable and streamed to disk, so size costs nothing but
|
|
523
|
+
* disk — which is exactly why a limit exists at all: it is the only thing standing between a
|
|
524
|
+
* peer (or a buggy sender) and a full disk. Relayed transfers are bounded separately by
|
|
525
|
+
* `relayMaxBytes`, because there the bytes land on somebody else's disk.
|
|
526
|
+
*/
|
|
527
|
+
maxBytes?: number;
|
|
528
|
+
acceptFrom?: '*' | string[];
|
|
529
|
+
maxConcurrent?: number;
|
|
530
|
+
offerTtlMinutes?: number;
|
|
531
|
+
chunkBytes?: number;
|
|
532
|
+
/** Largest single blob this node will host when acting as a relay hub. Default 512 MiB. */
|
|
533
|
+
relayMaxBytes?: number;
|
|
534
|
+
relayQuotaBytes?: number;
|
|
535
|
+
relayTtlHours?: number;
|
|
536
|
+
}
|
|
537
|
+
type TransferState = 'offered' | 'pending' | 'accepted' | 'transferring' | 'verifying' | 'done' | 'rejected' | 'failed' | 'cancelled' | 'expired';
|
|
538
|
+
interface TransferInfo {
|
|
539
|
+
transferId: string;
|
|
540
|
+
dir: 'send' | 'recv';
|
|
541
|
+
peerId: string;
|
|
542
|
+
peerName: string;
|
|
543
|
+
name: string;
|
|
544
|
+
size: number;
|
|
545
|
+
sha256: string;
|
|
546
|
+
state: TransferState;
|
|
547
|
+
mode?: 'pull' | 'push' | 'relay';
|
|
548
|
+
bytesDone: number;
|
|
549
|
+
error?: string;
|
|
550
|
+
note?: string;
|
|
551
|
+
savedPath?: string;
|
|
552
|
+
createdAt: number;
|
|
553
|
+
updatedAt: number;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* VoleNet Leader Election — ensures only one instance runs heartbeat/schedules.
|
|
558
|
+
*
|
|
559
|
+
* Algorithm: lowest instance ID wins (deterministic, no voting).
|
|
560
|
+
* Leader sends periodic heartbeat. If 3 heartbeats missed (30s), next-lowest takes over.
|
|
561
|
+
*/
|
|
562
|
+
|
|
563
|
+
interface LeaderState {
|
|
564
|
+
leaderId: string | null;
|
|
565
|
+
leaderName: string | null;
|
|
566
|
+
isLeader: boolean;
|
|
567
|
+
lastHeartbeat: number;
|
|
568
|
+
missedHeartbeats: number;
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Manages leader election for heartbeat/schedule ownership.
|
|
572
|
+
*/
|
|
573
|
+
declare class VoleNetLeader {
|
|
574
|
+
private transport;
|
|
575
|
+
private discovery;
|
|
576
|
+
private instanceId;
|
|
577
|
+
private instanceName;
|
|
578
|
+
private privateKey;
|
|
579
|
+
private pqPrivateKey?;
|
|
580
|
+
private leaderId;
|
|
581
|
+
private leaderName;
|
|
582
|
+
private lastLeaderHeartbeat;
|
|
583
|
+
private missedHeartbeats;
|
|
584
|
+
private heartbeatTimer;
|
|
585
|
+
private monitorTimer;
|
|
586
|
+
private onBecomeLeader?;
|
|
587
|
+
private onLoseLeader?;
|
|
588
|
+
private forcedLeader?;
|
|
589
|
+
constructor(transport: VoleNetTransport, discovery: VoleNetDiscovery, instanceId: string, instanceName: string, privateKey: KeyObject, pqPrivateKey: KeyObject | undefined, forcedLeader?: string);
|
|
590
|
+
/**
|
|
591
|
+
* Start leader election.
|
|
592
|
+
*/
|
|
593
|
+
start(onBecomeLeader?: () => void, onLoseLeader?: () => void): void;
|
|
594
|
+
/**
|
|
595
|
+
* Stop leader election.
|
|
596
|
+
*/
|
|
597
|
+
stop(): void;
|
|
598
|
+
/**
|
|
599
|
+
* Get current leader state.
|
|
600
|
+
*/
|
|
601
|
+
getState(): LeaderState;
|
|
602
|
+
/**
|
|
603
|
+
* Check if this instance is the leader.
|
|
604
|
+
*/
|
|
605
|
+
isLeader(): boolean;
|
|
606
|
+
/** Trigger re-election (called when peers change) */
|
|
607
|
+
reelect(): void;
|
|
608
|
+
/**
|
|
609
|
+
* Elect leader.
|
|
610
|
+
* If forcedLeader is set, that instance name is always leader.
|
|
611
|
+
* Otherwise, lowest instance ID wins.
|
|
612
|
+
*/
|
|
613
|
+
private electLeader;
|
|
614
|
+
/**
|
|
615
|
+
* Start sending leader heartbeats.
|
|
616
|
+
*/
|
|
617
|
+
private startLeaderHeartbeat;
|
|
618
|
+
/**
|
|
619
|
+
* Stop sending leader heartbeats.
|
|
620
|
+
*/
|
|
621
|
+
private stopLeaderHeartbeat;
|
|
622
|
+
/**
|
|
623
|
+
* Monitor leader liveness.
|
|
624
|
+
*/
|
|
625
|
+
private monitor;
|
|
626
|
+
/**
|
|
627
|
+
* Handle incoming leader-related messages.
|
|
628
|
+
*/
|
|
629
|
+
private handleMessage;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* VoleNet Remote Task — delegate tasks to remote peers, execute remote tools.
|
|
634
|
+
* Transparent to the Brain — remote tools appear in the tool registry like local ones.
|
|
635
|
+
*/
|
|
636
|
+
|
|
637
|
+
interface RemoteTaskRequest {
|
|
638
|
+
taskId: string;
|
|
639
|
+
input: string;
|
|
640
|
+
maxIterations?: number;
|
|
641
|
+
agentProfile?: string;
|
|
642
|
+
context?: string;
|
|
643
|
+
/** Set by net_message — the sender's instance name. Marks this as a chat message
|
|
644
|
+
* (framed as a peer message + run in a per-peer session) rather than a one-shot task. */
|
|
645
|
+
fromName?: string;
|
|
646
|
+
}
|
|
647
|
+
interface RemoteTaskResult {
|
|
648
|
+
taskId: string;
|
|
649
|
+
status: 'queued' | 'running' | 'completed' | 'failed' | 'timeout';
|
|
650
|
+
result?: string;
|
|
651
|
+
error?: string;
|
|
652
|
+
durationMs?: number;
|
|
653
|
+
}
|
|
654
|
+
interface RemoteToolCallRequest {
|
|
655
|
+
callId: string;
|
|
656
|
+
toolName: string;
|
|
657
|
+
params: unknown;
|
|
658
|
+
}
|
|
659
|
+
interface RemoteToolCallResult {
|
|
660
|
+
callId: string;
|
|
661
|
+
success: boolean;
|
|
662
|
+
output?: unknown;
|
|
663
|
+
error?: string;
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Manages remote task delegation and tool execution across VoleNet peers.
|
|
667
|
+
*/
|
|
668
|
+
declare class RemoteTaskManager {
|
|
669
|
+
private transport;
|
|
670
|
+
private discovery;
|
|
671
|
+
private instanceId;
|
|
672
|
+
private privateKey;
|
|
673
|
+
private pqPrivateKey?;
|
|
674
|
+
private pendingTasks;
|
|
675
|
+
private pendingToolCalls;
|
|
676
|
+
private routing;
|
|
677
|
+
constructor(transport: VoleNetTransport, discovery: VoleNetDiscovery, instanceId: string, privateKey: KeyObject, pqPrivateKey: KeyObject | undefined, routing?: Record<string, string>);
|
|
678
|
+
/**
|
|
679
|
+
* Delegate a task to a remote peer.
|
|
680
|
+
* Returns when the remote task completes or times out.
|
|
681
|
+
*/
|
|
682
|
+
delegateTask(targetInstanceId: string, request: RemoteTaskRequest, timeoutMs?: number): Promise<RemoteTaskResult>;
|
|
683
|
+
/**
|
|
684
|
+
* Execute a tool on a remote peer.
|
|
685
|
+
* Used by the tool registry when a tool is remote.
|
|
686
|
+
*/
|
|
687
|
+
executeRemoteTool(targetInstanceId: string, toolName: string, params: unknown, timeoutMs?: number): Promise<RemoteToolCallResult>;
|
|
688
|
+
/**
|
|
689
|
+
* Resolve which peer should handle a tool call.
|
|
690
|
+
* Checks routing config first, then falls back to discovery.
|
|
691
|
+
*/
|
|
692
|
+
resolveToolTarget(toolName: string): string | null;
|
|
693
|
+
/**
|
|
694
|
+
* Handle incoming messages related to tasks and tools.
|
|
695
|
+
*/
|
|
696
|
+
private handleMessage;
|
|
697
|
+
private handleTaskResult;
|
|
698
|
+
private handleTaskStatus;
|
|
699
|
+
private handleToolResult;
|
|
700
|
+
/**
|
|
701
|
+
* Cleanup pending requests.
|
|
702
|
+
*/
|
|
703
|
+
dispose(): void;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* VoleNet Sync — memory and session synchronization across peers.
|
|
708
|
+
*
|
|
709
|
+
* Memory sync: write propagation + remote search + result merging.
|
|
710
|
+
* Session sync: transcript replication for cross-device continuity.
|
|
711
|
+
*
|
|
712
|
+
* Conflict resolution: last-write-wins with instance attribution.
|
|
713
|
+
* Consistency model: eventual (async propagation, no blocking).
|
|
714
|
+
*/
|
|
715
|
+
|
|
716
|
+
/** Memory write event for propagation */
|
|
717
|
+
interface MemorySyncEntry {
|
|
718
|
+
file: string;
|
|
719
|
+
source: string;
|
|
720
|
+
content: string;
|
|
721
|
+
mode: 'overwrite' | 'append';
|
|
722
|
+
timestamp: number;
|
|
723
|
+
instanceId: string;
|
|
724
|
+
version: number;
|
|
725
|
+
}
|
|
726
|
+
/** Remote memory search request */
|
|
727
|
+
interface MemorySearchRequest {
|
|
728
|
+
query: string;
|
|
729
|
+
source?: string;
|
|
730
|
+
limit?: number;
|
|
731
|
+
requestId: string;
|
|
732
|
+
}
|
|
733
|
+
/** Remote memory search result */
|
|
734
|
+
interface MemorySearchResult {
|
|
735
|
+
requestId: string;
|
|
736
|
+
instanceId: string;
|
|
737
|
+
instanceName: string;
|
|
738
|
+
results: Array<{
|
|
739
|
+
file: string;
|
|
740
|
+
source: string;
|
|
741
|
+
score: number;
|
|
742
|
+
snippet: string;
|
|
743
|
+
}>;
|
|
744
|
+
}
|
|
745
|
+
/** Session sync entry */
|
|
746
|
+
interface SessionSyncEntry {
|
|
747
|
+
sessionId: string;
|
|
748
|
+
role: string;
|
|
749
|
+
content: string;
|
|
750
|
+
timestamp: number;
|
|
751
|
+
instanceId: string;
|
|
752
|
+
}
|
|
753
|
+
interface SyncConfig {
|
|
754
|
+
memory: boolean;
|
|
755
|
+
session: boolean;
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Manages memory and session synchronization across VoleNet peers.
|
|
759
|
+
*/
|
|
760
|
+
declare class VoleNetSync {
|
|
761
|
+
private transport;
|
|
762
|
+
private discovery;
|
|
763
|
+
private instanceId;
|
|
764
|
+
private instanceName;
|
|
765
|
+
private privateKey;
|
|
766
|
+
private pqPrivateKey?;
|
|
767
|
+
private config;
|
|
768
|
+
private onMemoryWrite?;
|
|
769
|
+
private onMemorySearch?;
|
|
770
|
+
private onSessionWrite?;
|
|
771
|
+
private pendingSearches;
|
|
772
|
+
private recentSyncs;
|
|
773
|
+
constructor(transport: VoleNetTransport, discovery: VoleNetDiscovery, instanceId: string, instanceName: string, privateKey: KeyObject, pqPrivateKey: KeyObject | undefined, config: SyncConfig);
|
|
774
|
+
/**
|
|
775
|
+
* Register callback for when a remote memory write arrives.
|
|
776
|
+
* The callback should apply the write to the local memory store.
|
|
777
|
+
*/
|
|
778
|
+
setMemoryWriteHandler(handler: (entry: MemorySyncEntry) => Promise<void>): void;
|
|
779
|
+
/**
|
|
780
|
+
* Register callback for handling remote memory search requests.
|
|
781
|
+
* The callback should search the local store and return results.
|
|
782
|
+
*/
|
|
783
|
+
setMemorySearchHandler(handler: (request: MemorySearchRequest) => Promise<MemorySearchResult['results']>): void;
|
|
784
|
+
/**
|
|
785
|
+
* Register callback for when a remote session write arrives.
|
|
786
|
+
*/
|
|
787
|
+
setSessionWriteHandler(handler: (entry: SessionSyncEntry) => Promise<void>): void;
|
|
788
|
+
/**
|
|
789
|
+
* Propagate a local memory write to all peers.
|
|
790
|
+
* Called by paw-memory after a successful local write.
|
|
791
|
+
*/
|
|
792
|
+
propagateMemoryWrite(entry: MemorySyncEntry): Promise<void>;
|
|
793
|
+
/**
|
|
794
|
+
* Search memory across all peers + local.
|
|
795
|
+
* Returns merged results from all sources, re-ranked by score.
|
|
796
|
+
*/
|
|
797
|
+
searchRemoteMemory(query: string, options?: {
|
|
798
|
+
source?: string;
|
|
799
|
+
limit?: number;
|
|
800
|
+
timeoutMs?: number;
|
|
801
|
+
}): Promise<MemorySearchResult[]>;
|
|
802
|
+
/**
|
|
803
|
+
* Propagate a session message to peers.
|
|
804
|
+
*/
|
|
805
|
+
propagateSessionWrite(entry: SessionSyncEntry): Promise<void>;
|
|
806
|
+
/**
|
|
807
|
+
* Handle incoming sync messages.
|
|
808
|
+
*/
|
|
809
|
+
private handleMessage;
|
|
810
|
+
/**
|
|
811
|
+
* Handle incoming memory write from a peer.
|
|
812
|
+
*/
|
|
813
|
+
private handleMemorySync;
|
|
814
|
+
/**
|
|
815
|
+
* Handle remote memory search request — search local store and reply.
|
|
816
|
+
*/
|
|
817
|
+
private handleMemorySearchRequest;
|
|
818
|
+
/**
|
|
819
|
+
* Handle memory search results from a peer.
|
|
820
|
+
*/
|
|
821
|
+
private handleMemorySearchResults;
|
|
822
|
+
/**
|
|
823
|
+
* Handle session sync from a peer.
|
|
824
|
+
*/
|
|
825
|
+
private handleSessionSync;
|
|
826
|
+
/**
|
|
827
|
+
* Cleanup.
|
|
828
|
+
*/
|
|
829
|
+
dispose(): void;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* The only thing this library needs to know about an agent's tools.
|
|
834
|
+
*
|
|
835
|
+
* VoleNet can lend a node's tools to a peer it trusts, which means it has to be able to list them
|
|
836
|
+
* and run one. That is the whole contract — two methods and five fields — so it is stated here
|
|
837
|
+
* structurally rather than imported from the agent framework. Anything that satisfies this shape
|
|
838
|
+
* works, including a host with no notion of paws at all.
|
|
839
|
+
*/
|
|
840
|
+
/** One tool, as the network needs to see it. */
|
|
841
|
+
interface SharedToolEntry {
|
|
842
|
+
name: string;
|
|
843
|
+
description: string;
|
|
844
|
+
/** Which plugin provides it. Used to decide what may be shared, so it is required. */
|
|
845
|
+
pawName: string;
|
|
846
|
+
/**
|
|
847
|
+
* The host's own context type is opaque here, and deliberately `any`: typed as `unknown` this
|
|
848
|
+
* contract would be satisfiable by nothing, since a function taking a specific context is not
|
|
849
|
+
* assignable to one taking anything at all.
|
|
850
|
+
*/
|
|
851
|
+
execute: (params: unknown, ctx?: any) => Promise<unknown>;
|
|
852
|
+
}
|
|
853
|
+
/** A tool as the host defines it, when the network registers a peer's tools locally. */
|
|
854
|
+
interface SharedToolDefinition {
|
|
855
|
+
name: string;
|
|
856
|
+
description: string;
|
|
857
|
+
/** The host's own parameter schema type (a Zod schema in OpenVole). Opaque here. */
|
|
858
|
+
parameters: unknown;
|
|
859
|
+
/**
|
|
860
|
+
* The host's own context type is opaque here, and deliberately `any`: typed as `unknown` this
|
|
861
|
+
* contract would be satisfiable by nothing, since a function taking a specific context is not
|
|
862
|
+
* assignable to one taking anything at all.
|
|
863
|
+
*/
|
|
864
|
+
execute: (params: unknown, ctx?: any) => Promise<unknown>;
|
|
865
|
+
}
|
|
866
|
+
/** Whatever holds this node's tools. `ToolRegistry` in OpenVole; anything with this shape here. */
|
|
867
|
+
interface ToolProvider {
|
|
868
|
+
get(toolName: string): SharedToolEntry | undefined;
|
|
869
|
+
list(): SharedToolEntry[];
|
|
870
|
+
/** Publish a peer's tools locally, so the host can call them as if they were its own. */
|
|
871
|
+
register(pawName: string, tools: SharedToolDefinition[], inProcess: boolean): void;
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* Plugins whose tools are backed by the host's control plane, and must never be shared over the
|
|
875
|
+
* mesh.
|
|
876
|
+
*
|
|
877
|
+
* They read as ordinary tools but execute against the local server — managing siblings, or
|
|
878
|
+
* speaking as this agent to one. A shared tool runs *on its owner*, so lending one of these lends
|
|
879
|
+
* the owner's authority with it: a peer that cannot manage agents itself could drive the owner's
|
|
880
|
+
* `agent_submit` and have every permission check pass, because by then the call really is the
|
|
881
|
+
* owner's. Excluded by **source** rather than by name — a name pattern would have to be kept in
|
|
882
|
+
* step with every tool ever added, and would quietly miss the first one somebody forgets.
|
|
883
|
+
*
|
|
884
|
+
* This lives with the network rather than with the tools because the network is what enforces it.
|
|
885
|
+
*/
|
|
886
|
+
declare const CONTROL_PLANE_PAWS: readonly ["__orchestrate__", "__agent_chat__"];
|
|
887
|
+
declare function isControlPlanePaw(pawName: string): boolean;
|
|
888
|
+
|
|
889
|
+
interface Logger {
|
|
890
|
+
error: (msg: string, ...args: unknown[]) => void;
|
|
891
|
+
warn: (msg: string, ...args: unknown[]) => void;
|
|
892
|
+
info: (msg: string, ...args: unknown[]) => void;
|
|
893
|
+
debug: (msg: string, ...args: unknown[]) => void;
|
|
894
|
+
trace: (msg: string, ...args: unknown[]) => void;
|
|
895
|
+
}
|
|
896
|
+
type LoggerFactory = (tag: string) => Logger;
|
|
897
|
+
/**
|
|
898
|
+
* Hand this library the host's logger. Loggers created before this call are redirected too, so a
|
|
899
|
+
* host can inject at startup without caring which modules have already been imported.
|
|
900
|
+
*/
|
|
901
|
+
declare function setLoggerFactory(make: LoggerFactory): void;
|
|
902
|
+
declare function createLogger(tag: string): Logger;
|
|
903
|
+
/** Close the log file this library opened. A host that injected its own logger owns its stream. */
|
|
904
|
+
declare function closeLogger(): void;
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* Sealed envelopes — end-to-end encryption for VoleNet messages (relay and, opt-in, direct).
|
|
908
|
+
*
|
|
909
|
+
* Hybrid KEM: every seal mixes an X25519 ECDH shared secret with an ML-KEM-768 (post-quantum)
|
|
910
|
+
* shared secret — HKDF-SHA256 over the concatenation — so confidentiality holds unless BOTH the
|
|
911
|
+
* classical and the post-quantum KEM are broken. This closes the harvest-now-decrypt-later gap:
|
|
912
|
+
* an adversary recording ciphertext today cannot read it with a future quantum computer. The
|
|
913
|
+
* X25519 half uses a fresh ephemeral key per envelope (random nonces, no cross-envelope leakage);
|
|
914
|
+
* the ML-KEM half encapsulates to the recipient's static PQ key. The AAD binds the envelope to
|
|
915
|
+
* its routing (`from|to`), so a relay cannot re-address a ciphertext without breaking the tag.
|
|
916
|
+
*
|
|
917
|
+
* Backward-compatible: when the recipient announces no ML-KEM key (an older peer) or the runtime
|
|
918
|
+
* lacks ML-KEM, the seal falls back to X25519-only (the `v1` scheme, byte-identical to before).
|
|
919
|
+
* The scheme is bound into the KDF, so stripping the KEM ciphertext from a hybrid box yields the
|
|
920
|
+
* wrong key and fails the tag — a downgrade cannot silently weaken an envelope, only drop it.
|
|
921
|
+
*
|
|
922
|
+
* The plaintext is a full, signed VoleNet message: sealing wraps the existing protocol, it does
|
|
923
|
+
* not replace any of its checks — the recipient still verifies the inner signature, freshness,
|
|
924
|
+
* and sender identity after unsealing.
|
|
925
|
+
*/
|
|
926
|
+
|
|
927
|
+
interface SealedBox {
|
|
928
|
+
/** Ephemeral X25519 public key, base64 SPKI DER. */
|
|
929
|
+
epk: string;
|
|
930
|
+
/** ML-KEM-768 ciphertext, base64 — present only for hybrid (post-quantum) envelopes. */
|
|
931
|
+
kem?: string;
|
|
932
|
+
/** ChaCha20-Poly1305 nonce, base64 (12 bytes). */
|
|
933
|
+
n: string;
|
|
934
|
+
/** Ciphertext ‖ auth tag, base64. */
|
|
935
|
+
c: string;
|
|
936
|
+
}
|
|
937
|
+
/**
|
|
938
|
+
* Seal plaintext to a recipient. When `recipientMlkemPubB64` is supplied (and ML-KEM is available),
|
|
939
|
+
* produces a post-quantum hybrid envelope; otherwise an X25519-only one. Returns null on a bad key.
|
|
940
|
+
*/
|
|
941
|
+
declare function seal(recipientXPubB64: string, plaintext: Buffer, aad: string, recipientMlkemPubB64?: string): SealedBox | null;
|
|
942
|
+
/**
|
|
943
|
+
* Unseal with our X25519 private key (and ML-KEM private key for hybrid envelopes). The scheme is
|
|
944
|
+
* chosen by whether the box carries a KEM ciphertext, so a stripped-KEM downgrade fails the tag
|
|
945
|
+
* rather than decrypting under a weaker key. Returns null on any tampering or mismatch.
|
|
946
|
+
*/
|
|
947
|
+
declare function unseal(xPrivateKey: crypto.KeyObject, box: SealedBox, aad: string, mlkemPrivateKey?: crypto.KeyObject): Buffer | null;
|
|
948
|
+
|
|
949
|
+
/** One answer waiting on the peer that asked for it. */
|
|
950
|
+
interface PendingResult {
|
|
951
|
+
/** Who asked. */
|
|
952
|
+
peerId: string;
|
|
953
|
+
/** Their id for the task — what the answer must carry to be recognised. */
|
|
954
|
+
taskId: string;
|
|
955
|
+
status: string;
|
|
956
|
+
result?: string;
|
|
957
|
+
error?: string;
|
|
958
|
+
/** When the answer was ready. */
|
|
959
|
+
at: number;
|
|
960
|
+
attempts: number;
|
|
961
|
+
}
|
|
962
|
+
declare const DEFAULT_RESULT_TTL_MS: number;
|
|
963
|
+
/** Ceiling per peer, oldest dropped first, so one absent asker cannot fill the disk. */
|
|
964
|
+
declare const MAX_RESULTS_PER_PEER = 50;
|
|
965
|
+
declare class ResultOutbox {
|
|
966
|
+
private readonly file;
|
|
967
|
+
private readonly ttlMs;
|
|
968
|
+
private entries;
|
|
969
|
+
constructor(file: string, ttlMs?: number);
|
|
970
|
+
load(now?: number): Promise<void>;
|
|
971
|
+
list(): PendingResult[];
|
|
972
|
+
forPeer(peerId: string): PendingResult[];
|
|
973
|
+
has(peerId: string): boolean;
|
|
974
|
+
get size(): number;
|
|
975
|
+
/** Hold an answer. Re-asking the same task replaces the old answer rather than stacking. */
|
|
976
|
+
add(entry: Omit<PendingResult, 'attempts'>): Promise<PendingResult>;
|
|
977
|
+
remove(peerId: string, taskId: string): Promise<boolean>;
|
|
978
|
+
noteAttempt(peerId: string, taskId: string): Promise<void>;
|
|
979
|
+
/** Drop answers older than the TTL. Returns what was dropped so the owner can say so. */
|
|
980
|
+
sweep(now?: number): Promise<PendingResult[]>;
|
|
981
|
+
private persist;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
/**
|
|
985
|
+
* VoleNet Manager — lifecycle management for the distributed networking layer.
|
|
986
|
+
* Initializes transport, discovery, and key management.
|
|
987
|
+
* Starts/stops with the engine.
|
|
988
|
+
*/
|
|
989
|
+
|
|
990
|
+
/**
|
|
991
|
+
* Message types NEVER wrapped in a sealed:direct envelope: the handshake/transport types that must
|
|
992
|
+
* be readable to bootstrap encryption (they carry no secrets — public keys, endpoints, liveness),
|
|
993
|
+
* the seal envelopes themselves, the relay envelopes (already end-to-end sealed to a third party),
|
|
994
|
+
* and high-frequency leader election (not confidential). Everything else is sealed when enabled.
|
|
995
|
+
*/
|
|
996
|
+
/**
|
|
997
|
+
* Anywhere this node's events can be published. Only `emit` is ever called, so any emitter shape
|
|
998
|
+
* satisfies it — OpenVole's mitt bus, an EventEmitter, or a few lines in a host that has neither.
|
|
999
|
+
*/
|
|
1000
|
+
interface EventSink {
|
|
1001
|
+
emit: (type: any, event?: any) => void;
|
|
1002
|
+
}
|
|
1003
|
+
/** An EventSink that can also be listened to, for a host that has no bus of its own. */
|
|
1004
|
+
interface EventBus extends EventSink {
|
|
1005
|
+
on: (type: any, handler: (event?: any) => void) => void;
|
|
1006
|
+
off: (type: any, handler?: (event?: any) => void) => void;
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
1009
|
+
* A minimal event bus, so a host without one can still hear what this node is doing. OpenVole
|
|
1010
|
+
* passes its own typed bus instead; both satisfy EventSink.
|
|
1011
|
+
*/
|
|
1012
|
+
declare function createEventBus(): EventBus;
|
|
1013
|
+
/** Glob-ish tool-name match: exact, '*' wildcard, or 'prefix*'. */
|
|
1014
|
+
/**
|
|
1015
|
+
* Display prefix for a peer's namespaced tools. Peer names are self-announced labels —
|
|
1016
|
+
* identity is the key-derived instanceId — so when two peers share a name, the prefix
|
|
1017
|
+
* is disambiguated with a short id suffix: alice~3f9c/tool.
|
|
1018
|
+
*/
|
|
1019
|
+
declare function peerPrefix(peerName: string, peerId: string, duplicateName: boolean): string;
|
|
1020
|
+
/** Whether a tool passes the share-level allowlist (empty/absent allows all). */
|
|
1021
|
+
declare function isSharedTool(name: string, toolAllow?: string[]): boolean;
|
|
1022
|
+
interface VoleNetConfig {
|
|
1023
|
+
enabled?: boolean;
|
|
1024
|
+
instanceName?: string;
|
|
1025
|
+
role?: 'coordinator' | 'worker' | 'peer';
|
|
1026
|
+
port?: number;
|
|
1027
|
+
/**
|
|
1028
|
+
* Hostname this instance advertises to peers (the host in its discovery endpoint).
|
|
1029
|
+
* Defaults to the first non-internal IPv4 address. Set this to your public domain
|
|
1030
|
+
* (e.g. "hub.example.com") when running with TLS so the advertised endpoint matches
|
|
1031
|
+
* the certificate — otherwise peers connecting over wss/https hit a name mismatch.
|
|
1032
|
+
* Overridable at runtime via the VOLE_NET_HOSTNAME env var.
|
|
1033
|
+
*/
|
|
1034
|
+
hostname?: string;
|
|
1035
|
+
/**
|
|
1036
|
+
* Full endpoint advertised to peers INSTEAD of `<scheme>://<hostname>:<port>` — for running
|
|
1037
|
+
* VoleNet behind a reverse proxy so the raw listen port never has to be exposed. Example:
|
|
1038
|
+
* "https://club.example.com/mesh", with nginx proxying that path (WebSocket upgrade included)
|
|
1039
|
+
* to the local VoleNet port. Peers join with this URL and are told to reconnect to it; the
|
|
1040
|
+
* joining side needs nothing — all peer traffic is endpoint-relative and the WS upgrade is
|
|
1041
|
+
* accepted on any path. Env override: VOLE_NET_PUBLIC_URL.
|
|
1042
|
+
*/
|
|
1043
|
+
publicUrl?: string;
|
|
1044
|
+
keyPath?: string;
|
|
1045
|
+
/**
|
|
1046
|
+
* Remember a peer learned at runtime — accepting a pair request, or joining a hub — so it
|
|
1047
|
+
* survives a restart. The host owns its own config format; without this the entry is
|
|
1048
|
+
* live-only, which is the right default for a library.
|
|
1049
|
+
*/
|
|
1050
|
+
persistPeer?: (url: string) => Promise<void>;
|
|
1051
|
+
peers?: Array<{
|
|
1052
|
+
/**
|
|
1053
|
+
* Where to reach this peer. Also how the entry is matched to a connected peer, by
|
|
1054
|
+
* port or host. Omit it for a peer that has no address of its own — a phone, or
|
|
1055
|
+
* anything behind NAT that can only dial us — and identify it with `id`/`name`.
|
|
1056
|
+
*/
|
|
1057
|
+
url?: string;
|
|
1058
|
+
/**
|
|
1059
|
+
* Match by instance id instead of address (a full id, or a prefix of at least 8
|
|
1060
|
+
* characters). This is the only way to name a peer that advertises no endpoint,
|
|
1061
|
+
* and the only stable one for a peer whose address moves.
|
|
1062
|
+
*/
|
|
1063
|
+
id?: string;
|
|
1064
|
+
/** Match by announced instance name. Weaker than `id` — a name is not proof of identity. */
|
|
1065
|
+
name?: string;
|
|
1066
|
+
/** What this peer can do on OUR instance. Defaults to 'full' — set it for a guest. */
|
|
1067
|
+
trust?: 'full' | 'tool' | 'read';
|
|
1068
|
+
allowTools?: string[];
|
|
1069
|
+
denyTools?: string[];
|
|
1070
|
+
/** Allow this peer to use our Brain for their tasks (LLM cost on us) */
|
|
1071
|
+
allowBrain?: boolean;
|
|
1072
|
+
}>;
|
|
1073
|
+
share?: {
|
|
1074
|
+
tools?: boolean;
|
|
1075
|
+
memory?: boolean;
|
|
1076
|
+
session?: boolean;
|
|
1077
|
+
/**
|
|
1078
|
+
* Patterns limiting WHICH tools are shared (advertised + callable) to peers without
|
|
1079
|
+
* an explicit per-peer allowTools entry — e.g. ["club_*"]. Empty/absent = all tools.
|
|
1080
|
+
* Essential for public hubs: share one curated tool set with strangers.
|
|
1081
|
+
*/
|
|
1082
|
+
toolAllow?: string[];
|
|
1083
|
+
};
|
|
1084
|
+
/** Retention for node-to-node chat sessions (volenet:<peer>). */
|
|
1085
|
+
chatRetention?: {
|
|
1086
|
+
/** Max messages kept per peer transcript (oldest trimmed). Default 1000. */
|
|
1087
|
+
maxMessages?: number;
|
|
1088
|
+
/** Clear chat sessions idle longer than this many days. Default 90; 0 disables. */
|
|
1089
|
+
maxAgeDays?: number;
|
|
1090
|
+
};
|
|
1091
|
+
/**
|
|
1092
|
+
* Brain source for brainless workers:
|
|
1093
|
+
* - "local" (default): use local brain paw
|
|
1094
|
+
* - "remote": delegate thinking to a peer that allows brain sharing
|
|
1095
|
+
* - "<instanceName>": delegate to a specific peer's brain
|
|
1096
|
+
*/
|
|
1097
|
+
brainSource?: 'local' | 'remote' | string;
|
|
1098
|
+
tls?: {
|
|
1099
|
+
cert: string;
|
|
1100
|
+
key: string;
|
|
1101
|
+
};
|
|
1102
|
+
/** Max concurrent inbound VoleNet WebSocket connections (DoS). Default 1000. */
|
|
1103
|
+
maxConnections?: number;
|
|
1104
|
+
/** Close inbound WS that don't send a verified message within this many ms (DoS). Default 10000. */
|
|
1105
|
+
authTimeoutMs?: number;
|
|
1106
|
+
/** Global inbound message ceiling per second across all sources (load shed). Default 5000. */
|
|
1107
|
+
maxMessagesPerSecond?: number;
|
|
1108
|
+
discovery?: 'manual' | 'mdns';
|
|
1109
|
+
routing?: Record<string, string>;
|
|
1110
|
+
/**
|
|
1111
|
+
* Leader selection mode:
|
|
1112
|
+
* - "auto" (default): lowest instance ID wins, automatic failover
|
|
1113
|
+
* - "<instanceName>": force a specific instance as leader
|
|
1114
|
+
*/
|
|
1115
|
+
leader?: 'auto' | string;
|
|
1116
|
+
/**
|
|
1117
|
+
* Heartbeat mode:
|
|
1118
|
+
* - "leader" (default): only the leader runs heartbeat/schedules
|
|
1119
|
+
* - "independent": each instance runs its own heartbeat independently
|
|
1120
|
+
*/
|
|
1121
|
+
heartbeatMode?: 'leader' | 'independent';
|
|
1122
|
+
/**
|
|
1123
|
+
* Brain load balancing:
|
|
1124
|
+
* - "local" (default): each instance handles its own tasks
|
|
1125
|
+
* - "loadbalance": route incoming tasks to the least-loaded brain across peers
|
|
1126
|
+
*/
|
|
1127
|
+
brainMode?: 'local' | 'loadbalance';
|
|
1128
|
+
/**
|
|
1129
|
+
* Task overflow behavior when local queue is full:
|
|
1130
|
+
* - "reject" (default): reject the task
|
|
1131
|
+
* - "forward": forward to the least-loaded peer automatically
|
|
1132
|
+
*/
|
|
1133
|
+
taskOverflow?: 'reject' | 'forward';
|
|
1134
|
+
/** Max queued tasks before overflow triggers (default: 10) */
|
|
1135
|
+
maxQueuedTasks?: number;
|
|
1136
|
+
/**
|
|
1137
|
+
* Public self-join — let unknown peers register over HTTP and join at a restricted
|
|
1138
|
+
* "guest" trust level (for a public mesh hub). Off by default. Guests are NEVER 'full'.
|
|
1139
|
+
*/
|
|
1140
|
+
publicJoin?: {
|
|
1141
|
+
enabled?: boolean;
|
|
1142
|
+
/** Trust granted to self-joined guests. Never 'full'. Default 'tool'. */
|
|
1143
|
+
trustLevel?: 'read' | 'tool';
|
|
1144
|
+
/** Let guests use OUR Brain (LLM cost on us). Default false. */
|
|
1145
|
+
allowBrain?: boolean;
|
|
1146
|
+
/** Max trusted peers before new joins are refused. Default 200. */
|
|
1147
|
+
maxPeers?: number;
|
|
1148
|
+
/** Join requests allowed per minute per IP. Default 5. */
|
|
1149
|
+
ratePerMinute?: number;
|
|
1150
|
+
/** Queue joins to pending_joins.jsonl for manual `vole net trust` instead of auto-trusting. */
|
|
1151
|
+
requireApproval?: boolean;
|
|
1152
|
+
};
|
|
1153
|
+
/**
|
|
1154
|
+
* Blind relay (hub side): forward sealed member↔member envelopes the hub cannot read.
|
|
1155
|
+
* v1 carries end-to-end encrypted chat only — no tool calls or delegation ride the relay.
|
|
1156
|
+
*/
|
|
1157
|
+
relay?: {
|
|
1158
|
+
enabled?: boolean;
|
|
1159
|
+
/** Forwards allowed per minute per (sender, recipient) pair. Default 30. */
|
|
1160
|
+
maxPerMinutePerPair?: number;
|
|
1161
|
+
/** Max sealed envelope size in bytes. Default 65536. */
|
|
1162
|
+
maxBytes?: number;
|
|
1163
|
+
/**
|
|
1164
|
+
* Member side: who may reach ME over a relay. Sharing a hub is not consent — a member's
|
|
1165
|
+
* relayed chat is dropped until I accept it. Default (unset): only peers I've explicitly
|
|
1166
|
+
* approved (a connect-request) or already directly trust. '*' opens me to any hub member
|
|
1167
|
+
* (community-hub behaviour). A list pre-authorises peers by name or instanceId prefix.
|
|
1168
|
+
*/
|
|
1169
|
+
acceptFrom?: '*' | string[];
|
|
1170
|
+
/**
|
|
1171
|
+
* Hub side: how long a "somebody tried to reach you" notice is kept for a member who is
|
|
1172
|
+
* away, in hours. Default 168 (a week). A notice is sender, count and times — the message
|
|
1173
|
+
* itself is never held by the hub.
|
|
1174
|
+
*/
|
|
1175
|
+
noticeTtlHours?: number;
|
|
1176
|
+
/**
|
|
1177
|
+
* Member side: how long an undelivered chat message waits in THIS node's own outbox for
|
|
1178
|
+
* its recipient to reappear, in hours. Default 168 (a week).
|
|
1179
|
+
*/
|
|
1180
|
+
outboxTtlHours?: number;
|
|
1181
|
+
};
|
|
1182
|
+
/**
|
|
1183
|
+
* Direct end-to-end encryption. When true, post-handshake messages to a peer that supports it
|
|
1184
|
+
* (announces an ML-KEM key) are sealed with the hybrid X25519 + ML-KEM-768 KEM before sending —
|
|
1185
|
+
* confidentiality independent of TLS, and post-quantum. Opportunistic: peers that don't support
|
|
1186
|
+
* it (older versions) still receive plaintext, so a mixed-version mesh keeps working. Default off.
|
|
1187
|
+
*/
|
|
1188
|
+
encrypt?: boolean;
|
|
1189
|
+
/**
|
|
1190
|
+
* Publish peer display names (the live announced `instanceName`) in the public /volenet/info
|
|
1191
|
+
* response. Off by default — names are an enumeration surface. Turn on for a public hub whose
|
|
1192
|
+
* members are meant to be seen (e.g. a social wall), so external tooling can read live names
|
|
1193
|
+
* without the authenticated dashboard.
|
|
1194
|
+
*/
|
|
1195
|
+
publishNames?: boolean;
|
|
1196
|
+
/**
|
|
1197
|
+
* VoleDrop — E2E-encrypted file transfer (files stream over /volenet/blob/*; the per-transfer
|
|
1198
|
+
* key is sealed with the PQ-hybrid seal). `acceptFrom` mirrors relay.acceptFrom: unset means
|
|
1199
|
+
* every offer waits for an explicit accept; '*' or a name/id-prefix list auto-accepts (use for
|
|
1200
|
+
* your own fleet). Hubs with relay enabled also store ciphertext blobs for NAT'd member pairs,
|
|
1201
|
+
* bounded by relayQuotaBytes/relayTtlHours.
|
|
1202
|
+
*/
|
|
1203
|
+
files?: VoleNetFilesConfig;
|
|
1204
|
+
}
|
|
1205
|
+
/** A hub-vouched mesh member, learned from a relay hub's roster broadcast. */
|
|
1206
|
+
interface RosterMember {
|
|
1207
|
+
instanceId: string;
|
|
1208
|
+
name: string;
|
|
1209
|
+
publicKey: string;
|
|
1210
|
+
xPublicKey?: string;
|
|
1211
|
+
mlkemPublicKey?: string;
|
|
1212
|
+
connected: boolean;
|
|
1213
|
+
}
|
|
1214
|
+
/** A single human-capable peer-chat message, stored per peer for the dashboard. */
|
|
1215
|
+
interface ChatEntry {
|
|
1216
|
+
dir: 'in' | 'out';
|
|
1217
|
+
text: string;
|
|
1218
|
+
fromName: string;
|
|
1219
|
+
timestamp: number;
|
|
1220
|
+
messageId: string;
|
|
1221
|
+
/** True when this message travelled through a relay hub as a sealed envelope. */
|
|
1222
|
+
relayed?: boolean;
|
|
1223
|
+
}
|
|
1224
|
+
declare class VoleNetManager {
|
|
1225
|
+
private keyPair;
|
|
1226
|
+
private transport;
|
|
1227
|
+
private discovery;
|
|
1228
|
+
private remoteTaskMgr;
|
|
1229
|
+
private sync;
|
|
1230
|
+
private leader;
|
|
1231
|
+
private toolProviders;
|
|
1232
|
+
/** registered remote tool name → owning instanceId (identity-keyed routing; never by peer name) */
|
|
1233
|
+
private remoteToolOwners;
|
|
1234
|
+
private config;
|
|
1235
|
+
private projectRoot;
|
|
1236
|
+
private toolRegistry;
|
|
1237
|
+
private started;
|
|
1238
|
+
/** Per-IP join timestamps for public-join rate limiting. */
|
|
1239
|
+
private joinTimestamps;
|
|
1240
|
+
/** Relay: per-(from,to) forward windows (hub side). */
|
|
1241
|
+
private relayWindows;
|
|
1242
|
+
/** Relay: hub-vouched member rosters, keyed by hub instanceId. */
|
|
1243
|
+
private hubRosters;
|
|
1244
|
+
/** Relay: inner-message replay guard — the transport's outer guard can't see re-wraps. */
|
|
1245
|
+
private seenSealed;
|
|
1246
|
+
private rosterTimer;
|
|
1247
|
+
/** Relay consent gate: instanceIds whose relayed chat I accept (persisted to relay_accepts). */
|
|
1248
|
+
private relayAcceptIds;
|
|
1249
|
+
private messageBus;
|
|
1250
|
+
/** Sender side: chat the hub could not forward, waiting on its recipient. Own disk only. */
|
|
1251
|
+
private chatOutbox;
|
|
1252
|
+
/** Sender side: envelopes awaiting the hub's verdict, keyed by the ref on the outer envelope. */
|
|
1253
|
+
private relayInflight;
|
|
1254
|
+
private outboxFlushing;
|
|
1255
|
+
/** Member side: who tried to reach me while I was away, as a hub told me on reconnect. */
|
|
1256
|
+
private chatPending;
|
|
1257
|
+
/** Member side: when I last received chat from each sender — a notice older than that is stale. */
|
|
1258
|
+
private lastChatFrom;
|
|
1259
|
+
/** Hub side: notices for members who are away. */
|
|
1260
|
+
private relayNotices;
|
|
1261
|
+
/** Brain answers whose asker had gone by the time they were ready — see result-outbox.ts. */
|
|
1262
|
+
private resultOutbox;
|
|
1263
|
+
/** Peers a flush is already running for, so a burst of pings does not send an answer twice. */
|
|
1264
|
+
private flushingResults;
|
|
1265
|
+
/** The polling timers waiting on delegated tasks, so stopping cancels them. */
|
|
1266
|
+
private delegationTimers;
|
|
1267
|
+
/** Relay: inbound connect-requests awaiting my approval (in-memory; keyed by requester id). */
|
|
1268
|
+
private relayRequests;
|
|
1269
|
+
/** Relay: connect-requests I sent that aren't confirmed yet (for the "awaiting" UI hint). */
|
|
1270
|
+
private relayOutgoing;
|
|
1271
|
+
/** Relay: peers that have accepted MY request (in-memory; distinguishes connected vs awaiting). */
|
|
1272
|
+
private relayConfirmed;
|
|
1273
|
+
/** Per-peer human chat logs (in-memory; keyed by peer instanceId). */
|
|
1274
|
+
private chatLog;
|
|
1275
|
+
/** Periodically re-attempts configured peers — self-heals start-order races + drops. */
|
|
1276
|
+
private peerConnectTimer?;
|
|
1277
|
+
/** Periodic chat-session retention prune. */
|
|
1278
|
+
private chatPruneTimer?;
|
|
1279
|
+
/** VoleDrop file transfer engine (net.files). */
|
|
1280
|
+
private files;
|
|
1281
|
+
/** Consent-based pairing: inbound requests awaiting the operator (persisted to pair_requests.json). */
|
|
1282
|
+
private pairRequests;
|
|
1283
|
+
/** Per-IP pair-request timestamps (rate limiting, same shape as publicJoin's). */
|
|
1284
|
+
private pairTimestamps;
|
|
1285
|
+
constructor(config: VoleNetConfig, projectRoot: string);
|
|
1286
|
+
/**
|
|
1287
|
+
* Start VoleNet — load keys, start transport, connect to peers.
|
|
1288
|
+
*/
|
|
1289
|
+
start(toolRegistry?: ToolProvider, bus?: EventSink): Promise<void>;
|
|
1290
|
+
/**
|
|
1291
|
+
* Stop VoleNet — disconnect peers, stop transport.
|
|
1292
|
+
*/
|
|
1293
|
+
/**
|
|
1294
|
+
* Send one brain answer to the peer that asked for it, and keep it if that fails.
|
|
1295
|
+
*
|
|
1296
|
+
* Failure here is ordinary: thinking takes as long as it takes, and a peer that asked from
|
|
1297
|
+
* a phone is very likely closed by the time there is something to say. Nobody else holds a
|
|
1298
|
+
* copy — the asker has the question and we have the only answer — so it waits, and goes out
|
|
1299
|
+
* the next time that peer speaks to us.
|
|
1300
|
+
*/
|
|
1301
|
+
private deliverTaskResult;
|
|
1302
|
+
/**
|
|
1303
|
+
* A peer just spoke to us, so anything we were holding for it can go out now.
|
|
1304
|
+
*
|
|
1305
|
+
* Re-signed rather than replayed: a stored message carries the timestamp it was written
|
|
1306
|
+
* with, and receivers enforce freshness.
|
|
1307
|
+
*/
|
|
1308
|
+
private flushResultsFor;
|
|
1309
|
+
stop(): Promise<void>;
|
|
1310
|
+
/**
|
|
1311
|
+
* Get connected instances.
|
|
1312
|
+
*/
|
|
1313
|
+
getInstances(): VoleNetInstance[];
|
|
1314
|
+
/** Send a file to a peer (direct or relay-rostered). Async — completion via bus events. */
|
|
1315
|
+
sendFile(peerRef: string, filePath: string, note?: string): Promise<{
|
|
1316
|
+
ok: boolean;
|
|
1317
|
+
transferId?: string;
|
|
1318
|
+
error?: string;
|
|
1319
|
+
}>;
|
|
1320
|
+
acceptFile(transferId: string): Promise<{
|
|
1321
|
+
ok: boolean;
|
|
1322
|
+
error?: string;
|
|
1323
|
+
}>;
|
|
1324
|
+
rejectFile(transferId: string, reason?: string): Promise<{
|
|
1325
|
+
ok: boolean;
|
|
1326
|
+
}>;
|
|
1327
|
+
cancelFileTransfer(transferId: string): Promise<{
|
|
1328
|
+
ok: boolean;
|
|
1329
|
+
}>;
|
|
1330
|
+
listFileTransfers(): TransferInfo[];
|
|
1331
|
+
getFileTransfer(transferId: string): TransferInfo | undefined;
|
|
1332
|
+
/** Session ID for a peer's human-chat transcript (persisted via paw-session). */
|
|
1333
|
+
private chatSessionId;
|
|
1334
|
+
/**
|
|
1335
|
+
* Append a chat entry for a peer. Persists via paw-session's session_append tool
|
|
1336
|
+
* when available; otherwise falls back to an in-memory log (capped at 200).
|
|
1337
|
+
*/
|
|
1338
|
+
private appendChat;
|
|
1339
|
+
/**
|
|
1340
|
+
* Get the human-chat history with a peer. Reads from paw-session when available,
|
|
1341
|
+
* otherwise the in-memory fallback.
|
|
1342
|
+
*/
|
|
1343
|
+
getChatHistory(peerId: string): Promise<ChatEntry[]>;
|
|
1344
|
+
/** Clear the human-chat history with a peer (paw-session or in-memory). */
|
|
1345
|
+
clearChat(peerId: string): Promise<void>;
|
|
1346
|
+
/** Clear chat sessions (volenet:*) idle longer than the retention age cap. */
|
|
1347
|
+
private pruneChatSessions;
|
|
1348
|
+
/**
|
|
1349
|
+
* Send a human chat message to a peer. Does NOT invoke any brain.
|
|
1350
|
+
* Resolves the peer by id or name, signs + sends a chat:message, and logs it locally.
|
|
1351
|
+
*/
|
|
1352
|
+
sendChat(peerId: string, text: string): Promise<{
|
|
1353
|
+
ok: boolean;
|
|
1354
|
+
delivered?: boolean;
|
|
1355
|
+
relayed?: boolean;
|
|
1356
|
+
error?: string;
|
|
1357
|
+
}>;
|
|
1358
|
+
/**
|
|
1359
|
+
* Find a hub-rostered member by ref, seal an inner message to it, and forward via its hub.
|
|
1360
|
+
* Shared by relayed chat and the consent handshake — the hub sees only ciphertext.
|
|
1361
|
+
*/
|
|
1362
|
+
private sealToMemberViaRelay;
|
|
1363
|
+
/**
|
|
1364
|
+
* Seal a chat message to a hub-rostered member, send it via that hub, and wait for the hub's
|
|
1365
|
+
* verdict. Forwarded, or held: when the member is away the message goes to this node's own
|
|
1366
|
+
* outbox and leaves when the member reappears in a roster. A hub too old to give a verdict
|
|
1367
|
+
* is treated the way it always was — the write to the hub counts as the delivery.
|
|
1368
|
+
*/
|
|
1369
|
+
private sendChatViaRelay;
|
|
1370
|
+
/**
|
|
1371
|
+
* The hub's verdict on an envelope we sent: forwarded, held because the member is away,
|
|
1372
|
+
* refused — or silence, from a hub too old to give one.
|
|
1373
|
+
*/
|
|
1374
|
+
private awaitRelayVerdict;
|
|
1375
|
+
/**
|
|
1376
|
+
* Route a hub's relay:error to the envelope it is about. Older hubs echo no ref, so a verdict
|
|
1377
|
+
* without one settles everything in flight to that member — in practice the one message.
|
|
1378
|
+
*/
|
|
1379
|
+
private settleRelay;
|
|
1380
|
+
/**
|
|
1381
|
+
* Send what is waiting for anyone who is back. Runs on every roster update, so a member
|
|
1382
|
+
* reappearing on a hub is what triggers delivery — nothing polls. One message at a time and
|
|
1383
|
+
* in order, so a conversation arrives the way it was written.
|
|
1384
|
+
*/
|
|
1385
|
+
private flushChatOutbox;
|
|
1386
|
+
/**
|
|
1387
|
+
* Outbound direct-seal transform. Wraps a message in a sealed:direct envelope when encryption is
|
|
1388
|
+
* enabled and the target peer supports it (announces an ML-KEM key ⟹ 4.12.0+, can unwrap). The
|
|
1389
|
+
* inner message is already signed; the recipient recovers and re-verifies it. Handshake, relay,
|
|
1390
|
+
* and leader types pass through untouched. Never throws — falls back to plaintext on any issue.
|
|
1391
|
+
*/
|
|
1392
|
+
private maybeSealDirect;
|
|
1393
|
+
/** Resolve a relay-member ref (id / name / id-prefix) against every hub roster. */
|
|
1394
|
+
private resolveRelayPeer;
|
|
1395
|
+
/** Persist consent to a peer, pinning its roster-vouched key line in relay_accepts. */
|
|
1396
|
+
private persistRelayAccept;
|
|
1397
|
+
/**
|
|
1398
|
+
* Ask a rostered member to accept relay contact. Initiating implies consent to receive its
|
|
1399
|
+
* reply, so the peer is added to my accept list immediately (persisted).
|
|
1400
|
+
*/
|
|
1401
|
+
requestRelayConnect(peerRef: string, note?: string): Promise<{
|
|
1402
|
+
ok: boolean;
|
|
1403
|
+
queued?: boolean;
|
|
1404
|
+
error?: string;
|
|
1405
|
+
}>;
|
|
1406
|
+
/**
|
|
1407
|
+
* Consent traffic goes through the same verdict-and-hold path as chat. It used to be
|
|
1408
|
+
* fire-and-forget, so a request to a member who had just locked their phone simply vanished —
|
|
1409
|
+
* and, on the older hub, so did the fact that anything had been sent.
|
|
1410
|
+
*/
|
|
1411
|
+
private sendConsentViaRelay;
|
|
1412
|
+
/** Approve an inbound connect-request: consent to the peer, persist it, and notify the peer. */
|
|
1413
|
+
approveRelayConnect(peerRef: string): Promise<{
|
|
1414
|
+
ok: boolean;
|
|
1415
|
+
queued?: boolean;
|
|
1416
|
+
error?: string;
|
|
1417
|
+
}>;
|
|
1418
|
+
/** Deny an inbound connect-request: clear it and (best-effort) tell the peer. */
|
|
1419
|
+
denyRelayConnect(peerRef: string): Promise<{
|
|
1420
|
+
ok: boolean;
|
|
1421
|
+
error?: string;
|
|
1422
|
+
}>;
|
|
1423
|
+
/** Withdraw previously-granted relay consent for a peer (removes it from relay_accepts). */
|
|
1424
|
+
revokeRelayConnect(peerRef: string): Promise<{
|
|
1425
|
+
ok: boolean;
|
|
1426
|
+
}>;
|
|
1427
|
+
/** Inbound relay connect-requests awaiting my approval (newest first). */
|
|
1428
|
+
getRelayRequests(): Array<{
|
|
1429
|
+
id: string;
|
|
1430
|
+
name: string;
|
|
1431
|
+
viaHub: string;
|
|
1432
|
+
viaHubName: string;
|
|
1433
|
+
note?: string;
|
|
1434
|
+
ts: number;
|
|
1435
|
+
}>;
|
|
1436
|
+
/** Chat waiting on this node for recipients who are away. Own disk only, never a hub's. */
|
|
1437
|
+
getChatOutbox(): OutboxEntry[];
|
|
1438
|
+
/** Who tried to reach me while I was away, per the hub that told me. Cleared as their messages arrive. */
|
|
1439
|
+
getChatPending(): Array<RelayNotice & {
|
|
1440
|
+
viaHub: string;
|
|
1441
|
+
}>;
|
|
1442
|
+
/** Unseal, verify, and ingest an envelope addressed to us. Allowlist: chat, consent, files. */
|
|
1443
|
+
private deliverSealed;
|
|
1444
|
+
/** True when a relay sender may reach me: '*' policy, an acceptFrom match, or a prior approval. */
|
|
1445
|
+
private isRelayAccepted;
|
|
1446
|
+
/** Record (or refresh) an inbound relay connect-request awaiting my approval. */
|
|
1447
|
+
private recordRelayRequest;
|
|
1448
|
+
private rosterName;
|
|
1449
|
+
private findRosterMember;
|
|
1450
|
+
/** Hub: push the current member directory to every connected member. */
|
|
1451
|
+
private broadcastRoster;
|
|
1452
|
+
/** Hand a member that just reconnected everything the hub noted for it while it was away. */
|
|
1453
|
+
private sendRelayNotices;
|
|
1454
|
+
/** Rosters received from relay hubs (hub instanceId → member directory). */
|
|
1455
|
+
getRosters(): Map<string, Map<string, RosterMember>>;
|
|
1456
|
+
/**
|
|
1457
|
+
* Relay-reachable members from all hub rosters, flattened for the dashboard.
|
|
1458
|
+
* A member we ALSO connect to directly is omitted here — direct trumps relay, the same
|
|
1459
|
+
* precedence sendChat() uses — so the UI never lists one peer in two places.
|
|
1460
|
+
*/
|
|
1461
|
+
getRelayMembers(): Array<{
|
|
1462
|
+
id: string;
|
|
1463
|
+
name: string;
|
|
1464
|
+
viaHub: string;
|
|
1465
|
+
viaHubName: string;
|
|
1466
|
+
connected: boolean;
|
|
1467
|
+
/** I accept this member's relayed chat ('*'/acceptFrom match or an approval). */
|
|
1468
|
+
accepted: boolean;
|
|
1469
|
+
/** This member has an inbound connect-request awaiting my approval. */
|
|
1470
|
+
incoming: boolean;
|
|
1471
|
+
/** I've requested this member but it hasn't accepted yet. */
|
|
1472
|
+
awaiting: boolean;
|
|
1473
|
+
}>;
|
|
1474
|
+
/**
|
|
1475
|
+
* Get all remote tools.
|
|
1476
|
+
*/
|
|
1477
|
+
getRemoteTools(): RemoteToolInfo[];
|
|
1478
|
+
/**
|
|
1479
|
+
* Find which peer owns a tool.
|
|
1480
|
+
*/
|
|
1481
|
+
findToolOwner(toolName: string): {
|
|
1482
|
+
instanceId: string;
|
|
1483
|
+
instance: VoleNetInstance;
|
|
1484
|
+
} | null;
|
|
1485
|
+
/**
|
|
1486
|
+
* Get the remote task manager.
|
|
1487
|
+
*/
|
|
1488
|
+
getRemoteTaskManager(): RemoteTaskManager | null;
|
|
1489
|
+
/**
|
|
1490
|
+
* Get the sync manager (for memory/session propagation).
|
|
1491
|
+
*/
|
|
1492
|
+
getSync(): VoleNetSync | null;
|
|
1493
|
+
/**
|
|
1494
|
+
* Get the leader election manager.
|
|
1495
|
+
*/
|
|
1496
|
+
getLeader(): VoleNetLeader | null;
|
|
1497
|
+
/**
|
|
1498
|
+
* Check if this instance is the VoleNet leader.
|
|
1499
|
+
*/
|
|
1500
|
+
isLeader(): boolean;
|
|
1501
|
+
/**
|
|
1502
|
+
* Check if this instance should run heartbeat.
|
|
1503
|
+
* In "independent" mode, every instance runs heartbeat.
|
|
1504
|
+
* In "leader" mode (default), only the leader runs it.
|
|
1505
|
+
*/
|
|
1506
|
+
shouldRunHeartbeat(): boolean;
|
|
1507
|
+
/**
|
|
1508
|
+
* Find the best peer for load-balanced task routing.
|
|
1509
|
+
* Returns null if local should handle it (or no peers available).
|
|
1510
|
+
*/
|
|
1511
|
+
findLeastLoadedPeer(): VoleNetInstance | null;
|
|
1512
|
+
/**
|
|
1513
|
+
* Check if a task should be forwarded to a peer (overflow mode).
|
|
1514
|
+
* Returns the target peer or null if local should handle it.
|
|
1515
|
+
*/
|
|
1516
|
+
shouldForwardTask(currentQueueSize: number): VoleNetInstance | null;
|
|
1517
|
+
/**
|
|
1518
|
+
* Check if this instance should delegate thinking to a remote brain.
|
|
1519
|
+
* Returns the target peer instance ID, or null if local brain should be used.
|
|
1520
|
+
*/
|
|
1521
|
+
shouldDelegateBrain(): string | null;
|
|
1522
|
+
/** Handle a public self-join request (HTTP POST /volenet/join). Returns status + JSON body. */
|
|
1523
|
+
handlePublicJoin(body: unknown, ip: string): Promise<{
|
|
1524
|
+
status: number;
|
|
1525
|
+
json: unknown;
|
|
1526
|
+
}>;
|
|
1527
|
+
private pairRequestsPath;
|
|
1528
|
+
private loadPairRequests;
|
|
1529
|
+
private persistPairRequests;
|
|
1530
|
+
/** Handle POST /volenet/pair — queue the introduction; trust NOTHING until acceptPair. */
|
|
1531
|
+
handlePairRequest(body: unknown, ip: string, bus?: EventSink): Promise<{
|
|
1532
|
+
status: number;
|
|
1533
|
+
json: unknown;
|
|
1534
|
+
}>;
|
|
1535
|
+
listPairRequests(): Array<{
|
|
1536
|
+
id: string;
|
|
1537
|
+
name: string;
|
|
1538
|
+
endpoint?: string;
|
|
1539
|
+
note?: string;
|
|
1540
|
+
ts: number;
|
|
1541
|
+
}>;
|
|
1542
|
+
/** Operator consent: trust the requester's pinned key, live-reload, dial back if possible. */
|
|
1543
|
+
acceptPair(ref: string): Promise<{
|
|
1544
|
+
ok: boolean;
|
|
1545
|
+
name?: string;
|
|
1546
|
+
error?: string;
|
|
1547
|
+
}>;
|
|
1548
|
+
denyPair(ref: string): Promise<{
|
|
1549
|
+
ok: boolean;
|
|
1550
|
+
}>;
|
|
1551
|
+
/** Fetch a remote node's identity for the operator to fingerprint before pairing. */
|
|
1552
|
+
probePair(url: string): Promise<{
|
|
1553
|
+
ok: boolean;
|
|
1554
|
+
name?: string;
|
|
1555
|
+
fingerprint?: string;
|
|
1556
|
+
publicKey?: string;
|
|
1557
|
+
alreadyTrusted?: boolean;
|
|
1558
|
+
error?: string;
|
|
1559
|
+
}>;
|
|
1560
|
+
/** Treat a URL as a configured peer and dial it now. The inverse of {@link forgetPeer}. */
|
|
1561
|
+
addPeer(url: string): Promise<void>;
|
|
1562
|
+
/**
|
|
1563
|
+
* Stop treating a URL as a configured peer.
|
|
1564
|
+
*
|
|
1565
|
+
* The reconnect loop re-dials everything in `config.peers` every 15 seconds, so dropping the
|
|
1566
|
+
* socket alone would have it back within a tick. Both halves belong together: forget the entry
|
|
1567
|
+
* and close the connection. Trust is untouched — this says "do not dial", not "do not trust".
|
|
1568
|
+
*/
|
|
1569
|
+
forgetPeer(url: string): boolean;
|
|
1570
|
+
/** Record a peer URL in the live config, ask the host to remember it, then dial it. */
|
|
1571
|
+
private addPeerEntry;
|
|
1572
|
+
/**
|
|
1573
|
+
* Dashboard-initiated pairing: trust the probed key (the operator confirmed the
|
|
1574
|
+
* fingerprint client-side), persist + dial the peer, and file the pair request for
|
|
1575
|
+
* the other operator. Fully live — no restart needed on this side.
|
|
1576
|
+
*/
|
|
1577
|
+
initiatePair(url: string, publicKey: string, note?: string): Promise<{
|
|
1578
|
+
ok: boolean;
|
|
1579
|
+
pending?: boolean;
|
|
1580
|
+
alreadyTrusted?: boolean;
|
|
1581
|
+
error?: string;
|
|
1582
|
+
}>;
|
|
1583
|
+
/** Dashboard-initiated public-hub join (the vole net join flow, in-process and live). */
|
|
1584
|
+
initiateJoin(url: string): Promise<{
|
|
1585
|
+
ok: boolean;
|
|
1586
|
+
pending?: boolean;
|
|
1587
|
+
hubName?: string;
|
|
1588
|
+
error?: string;
|
|
1589
|
+
}>;
|
|
1590
|
+
/**
|
|
1591
|
+
* Check if a specific peer is allowed to use our brain.
|
|
1592
|
+
*/
|
|
1593
|
+
isPeerAllowedBrain(peerId: string): boolean;
|
|
1594
|
+
/**
|
|
1595
|
+
* Whether this peer may call our tools at all — tool/full trust (from net.peers OR a
|
|
1596
|
+
* publicJoin guest's granted trustLevel), or share.tools. Uses getPeerTrust, not
|
|
1597
|
+
* matchPeerConfig, so a publicJoin guest's `trustLevel: "tool"` actually grants tool
|
|
1598
|
+
* access — matching the documented behavior and how isPeerAllowedBrain already works.
|
|
1599
|
+
* (Per-tool curation still applies via share.toolAllow in isPeerAllowedTool.)
|
|
1600
|
+
*/
|
|
1601
|
+
private peerToolsEnabled;
|
|
1602
|
+
/** Whether this peer may call a specific tool (honors per-peer allow/deny lists). */
|
|
1603
|
+
isPeerAllowedTool(peerId: string, toolName: string): boolean;
|
|
1604
|
+
/**
|
|
1605
|
+
* Match a connected peer to its config entry.
|
|
1606
|
+
* Matches by port (handles localhost vs real IP) or by instance name.
|
|
1607
|
+
*/
|
|
1608
|
+
private matchPeerConfig;
|
|
1609
|
+
/**
|
|
1610
|
+
* Get trust level for a peer.
|
|
1611
|
+
*/
|
|
1612
|
+
getPeerTrust(peerId: string): {
|
|
1613
|
+
trust: string;
|
|
1614
|
+
allowTools?: string[];
|
|
1615
|
+
denyTools?: string[];
|
|
1616
|
+
allowBrain?: boolean;
|
|
1617
|
+
} | null;
|
|
1618
|
+
/**
|
|
1619
|
+
* Get the keypair (for CLI display).
|
|
1620
|
+
*/
|
|
1621
|
+
/** Our instance name (for message framing). */
|
|
1622
|
+
getInstanceName(): string;
|
|
1623
|
+
getKeyPair(): VoleKeyPair | null;
|
|
1624
|
+
/**
|
|
1625
|
+
* Get the transport (for sending messages).
|
|
1626
|
+
*/
|
|
1627
|
+
getTransport(): VoleNetTransport | null;
|
|
1628
|
+
/**
|
|
1629
|
+
* Get the discovery manager.
|
|
1630
|
+
*/
|
|
1631
|
+
getDiscovery(): VoleNetDiscovery | null;
|
|
1632
|
+
/**
|
|
1633
|
+
* Check if VoleNet is active.
|
|
1634
|
+
*/
|
|
1635
|
+
isActive(): boolean;
|
|
1636
|
+
private getNetDir;
|
|
1637
|
+
private getHostname;
|
|
1638
|
+
private getCapabilities;
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
/**
|
|
1642
|
+
* Merge the transport-verified caller identity into remote tool params.
|
|
1643
|
+
* Always overwrites any incoming `__caller` — a peer cannot impersonate another instance.
|
|
1644
|
+
* Tools that want attribution declare an optional `__caller` parameter; others ignore it.
|
|
1645
|
+
*/
|
|
1646
|
+
declare function withVerifiedCaller(params: unknown, instanceId: string, name?: string): Record<string, unknown>;
|
|
1647
|
+
/**
|
|
1648
|
+
* The endpoint an instance advertises to peers. An explicit publicUrl wins outright —
|
|
1649
|
+
* set it when VoleNet sits behind a reverse proxy so peers are told the proxy URL
|
|
1650
|
+
* (e.g. "https://club.example.com/mesh") instead of a raw listen port that may be firewalled.
|
|
1651
|
+
*/
|
|
1652
|
+
declare function buildAdvertisedEndpoint(opts: {
|
|
1653
|
+
publicUrl?: string;
|
|
1654
|
+
tls?: boolean;
|
|
1655
|
+
hostname: string;
|
|
1656
|
+
port: number;
|
|
1657
|
+
}): string;
|
|
1658
|
+
type PeerEntry = {
|
|
1659
|
+
url: string;
|
|
1660
|
+
trust?: string;
|
|
1661
|
+
} & Record<string, unknown>;
|
|
1662
|
+
/**
|
|
1663
|
+
* Add a peer URL to a config peers list, REPLACING any existing entry on the same hostname —
|
|
1664
|
+
* re-joining a hub at a new endpoint (a proxied /mesh path instead of a raw :9710 port) must
|
|
1665
|
+
* update the entry, not stack a dead duplicate beside it. The replaced entry's trust and
|
|
1666
|
+
* per-peer settings carry over. Returns the new list plus the URL it replaced, if any.
|
|
1667
|
+
*/
|
|
1668
|
+
declare function upsertPeerUrl(peers: PeerEntry[], url: string): {
|
|
1669
|
+
peers: PeerEntry[];
|
|
1670
|
+
replaced?: string;
|
|
1671
|
+
};
|
|
1672
|
+
|
|
1673
|
+
export { CONTROL_PLANE_PAWS, type ChatEntry, ChatOutbox, DEFAULT_NOTICE_TTL_MS, DEFAULT_OUTBOX_TTL_MS, DEFAULT_RESULT_TTL_MS, type EventBus, type EventSink, type LeaderState, type Logger, type LoggerFactory, MAX_RESULTS_PER_PEER, type MemorySearchRequest, type MemorySearchResult, type MemorySyncEntry, type OutboxEntry, type OutboxKind, type PendingResult, type RelayNotice, RelayNotices, RemoteTaskManager, type RemoteTaskRequest, type RemoteTaskResult, type RemoteToolCallRequest, type RemoteToolCallResult, type RemoteToolInfo, ResultOutbox, type RosterMember, type SealedBox, type SessionSyncEntry, type SharedToolDefinition, type SharedToolEntry, type ToolProvider, type VoleKeyPair, type VoleNetConfig, VoleNetDiscovery, type VoleNetInstance, VoleNetLeader, VoleNetManager, type VoleNetMessage, type VoleNetMessageType, VoleNetSync, VoleNetTransport, addRelayAccept, buildAdvertisedEndpoint, closeLogger, createEventBus, createLogger, createMessage, findEndpointDrift, generateKeyPair, isControlPlanePaw, isSharedTool, loadAuthorizedVoles, loadKeyPair, loadRelayAccepts, parsePublicKey, peerPrefix, removeRelayAccept, revokePeer, seal, setLoggerFactory, trustPeer, unseal, upsertPeerUrl, verifyMessage, withVerifiedCaller };
|