@agentmesh-p2p/sdk 0.1.0 → 0.2.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/README.md +60 -0
- package/dist/auth.d.ts +2 -1
- package/dist/auth.js +3 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +117 -0
- package/dist/controlplane.d.ts +2 -2
- package/dist/controlplane.js +15 -12
- package/dist/discovery.js +3 -3
- package/dist/gen/agentmesh_pb.d.ts +133 -2
- package/dist/gen/agentmesh_pb.js +111 -72
- package/dist/gen/datalog.d.ts +2 -0
- package/dist/gen/datalog.js +9 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/mcp-server.d.ts +8 -0
- package/dist/mcp-server.js +305 -0
- package/dist/mcp.js +1 -1
- package/dist/mesh.d.ts +7 -3
- package/dist/mesh.js +19 -8
- package/dist/session.d.ts +23 -4
- package/dist/session.js +63 -17
- package/package.json +6 -2
package/dist/session.js
CHANGED
|
@@ -51,6 +51,17 @@ const DEFAULT_RELAY_CHECK_MS = 30 * 1000;
|
|
|
51
51
|
const DEFAULT_ANNOUNCE_MS = 5 * 60 * 1000;
|
|
52
52
|
/** What close() waits for the member's record to be withdrawn. */
|
|
53
53
|
const WITHDRAW_TIMEOUT_MS = 2 * 1000;
|
|
54
|
+
/**
|
|
55
|
+
* Bounds a join whose caller passed no signal: each step has a timeout of
|
|
56
|
+
* its own, but a join tries them in turn over every router it is offered.
|
|
57
|
+
*/
|
|
58
|
+
export const JOIN_TIMEOUT_MS = 60_000;
|
|
59
|
+
/**
|
|
60
|
+
* Bounds the control plane sync a join starts with. The sync is best effort,
|
|
61
|
+
* so a control plane that does not answer in time leaves the join to go on
|
|
62
|
+
* with the stored credential instead of using up the whole join.
|
|
63
|
+
*/
|
|
64
|
+
export const JOIN_SYNC_TIMEOUT_MS = 5_000;
|
|
54
65
|
/** How long a provider's positive egress verdict is kept; agentmesh-node's labelGateTTL. */
|
|
55
66
|
const EGRESS_VERDICT_TTL_MS = 5 * 60 * 1000;
|
|
56
67
|
/**
|
|
@@ -220,7 +231,7 @@ export class MeshSession {
|
|
|
220
231
|
* the mesh's record TTL. A member with no relayed address announces
|
|
221
232
|
* nothing: there is nothing to reach it at.
|
|
222
233
|
*/
|
|
223
|
-
async announce() {
|
|
234
|
+
async announce(signal) {
|
|
224
235
|
if (this.#closed || this.#isTaskView) {
|
|
225
236
|
return;
|
|
226
237
|
}
|
|
@@ -228,7 +239,7 @@ export class MeshSession {
|
|
|
228
239
|
if (addrs.length === 0) {
|
|
229
240
|
return;
|
|
230
241
|
}
|
|
231
|
-
await provide(this.#directoryRouters(), await peerKey(this.peerId), addrs);
|
|
242
|
+
await provide(this.#directoryRouters(), await peerKey(this.peerId), addrs, signal);
|
|
232
243
|
this.#announced = addrs;
|
|
233
244
|
}
|
|
234
245
|
/** Whether peerId is currently one of the routers this member holds. */
|
|
@@ -372,14 +383,15 @@ export class MeshSession {
|
|
|
372
383
|
/**
|
|
373
384
|
* Runs the handshake on a router connection once: the connection:open
|
|
374
385
|
* listener and an explicit admission both call this, and the second waits
|
|
375
|
-
* for the first instead of running a second handshake.
|
|
386
|
+
* for the first instead of running a second handshake. The signal applies
|
|
387
|
+
* to a handshake this call starts; one already running keeps its own.
|
|
376
388
|
*/
|
|
377
|
-
#authenticateRouter(conn) {
|
|
389
|
+
#authenticateRouter(conn, signal) {
|
|
378
390
|
const pending = this.#routerHandshakes.get(conn.id);
|
|
379
391
|
if (pending !== undefined) {
|
|
380
392
|
return pending;
|
|
381
393
|
}
|
|
382
|
-
const run = authenticateWithPeer(conn, this.mesh.authFrame(), this.mesh.credential.controlPlaneKeys).finally(() => {
|
|
394
|
+
const run = authenticateWithPeer(conn, this.mesh.authFrame(), this.mesh.credential.controlPlaneKeys, signal).finally(() => {
|
|
383
395
|
this.#routerHandshakes.delete(conn.id);
|
|
384
396
|
});
|
|
385
397
|
this.#routerHandshakes.set(conn.id, run);
|
|
@@ -496,6 +508,8 @@ export class MeshSession {
|
|
|
496
508
|
*/
|
|
497
509
|
async topUpRouters(signal) {
|
|
498
510
|
const failures = [];
|
|
511
|
+
// A function, not a narrowed value: the signal aborts across the awaits below.
|
|
512
|
+
const aborted = () => signal?.aborted === true;
|
|
499
513
|
const want = this.#wantRouters();
|
|
500
514
|
const candidates = this.#candidatesForHolding();
|
|
501
515
|
const tried = new Set();
|
|
@@ -513,22 +527,37 @@ export class MeshSession {
|
|
|
513
527
|
}
|
|
514
528
|
let progress = false;
|
|
515
529
|
for (const cand of picks) {
|
|
530
|
+
// A router not dialed yet is not tried under a signal that has
|
|
531
|
+
// already aborted: it would fail at once and be shunned for nothing.
|
|
532
|
+
if (aborted()) {
|
|
533
|
+
return failures;
|
|
534
|
+
}
|
|
516
535
|
tried.add(cand.peerId);
|
|
536
|
+
// Each step has a timeout of its own; the failure names the one the
|
|
537
|
+
// router was in, so a join that hangs says where.
|
|
538
|
+
let step = `dial ${cand.addresses.map(String).join(", ")}`;
|
|
517
539
|
try {
|
|
518
540
|
const conn = await this.node.dial(cand.addresses, signal !== undefined ? { signal } : {});
|
|
519
|
-
|
|
541
|
+
step = "auth handshake";
|
|
542
|
+
const credential = await this.#authenticateRouter(conn, signal);
|
|
520
543
|
requireRole(credential, ROLE_ROUTER);
|
|
521
544
|
const addr = connectedAddress(conn, cand.peerId);
|
|
522
545
|
if (this.#reserveRelay) {
|
|
546
|
+
step = "relay reservation";
|
|
523
547
|
await this.#reserveOnRouter(cand.peerId, addr);
|
|
524
548
|
}
|
|
525
549
|
this.#upsertHeldRouter({ peerId: cand.peerId, addr, credential });
|
|
526
550
|
progress = true;
|
|
527
551
|
}
|
|
528
552
|
catch (err) {
|
|
529
|
-
|
|
553
|
+
failures.push(`${cand.peerId}: ${step}: ${errorText(err)}`);
|
|
530
554
|
await this.node.hangUp(peerIdFromString(cand.peerId)).catch(() => { });
|
|
531
|
-
|
|
555
|
+
// A router cut off by the signal did not fail on its own, so it is
|
|
556
|
+
// not shunned.
|
|
557
|
+
if (aborted()) {
|
|
558
|
+
return failures;
|
|
559
|
+
}
|
|
560
|
+
this.#shunRouter(cand.peerId, ROUTER_SHUN_DURATION_MS);
|
|
532
561
|
}
|
|
533
562
|
}
|
|
534
563
|
if (!progress && picks.length < need) {
|
|
@@ -1187,8 +1216,14 @@ export function selectRouters(candidates, k, options = {}) {
|
|
|
1187
1216
|
});
|
|
1188
1217
|
return scored.slice(0, k).map((s) => s.candidate);
|
|
1189
1218
|
}
|
|
1190
|
-
/**
|
|
1191
|
-
|
|
1219
|
+
/**
|
|
1220
|
+
* Implements AgentMesh.join(); lives here to keep mesh.ts free of libp2p.
|
|
1221
|
+
* timeoutMs bounds a join whose caller passed no signal.
|
|
1222
|
+
*/
|
|
1223
|
+
export async function joinMesh(mesh, options = {}, timeoutMs = JOIN_TIMEOUT_MS) {
|
|
1224
|
+
const signal = options.signal ?? AbortSignal.timeout(timeoutMs);
|
|
1225
|
+
// Why the join stopped: the caller's reason, or its own timeout.
|
|
1226
|
+
const stopReason = () => (options.signal !== undefined ? errorText(signal.reason) : `timed out after ${timeoutMs}ms`);
|
|
1192
1227
|
// The pull agentmesh-node makes before it starts: a member resuming from its
|
|
1193
1228
|
// state directory after a key rotation would otherwise verify the routers,
|
|
1194
1229
|
// which already hold credentials under the new key, against the keys it
|
|
@@ -1196,10 +1231,14 @@ export async function joinMesh(mesh, options = {}) {
|
|
|
1196
1231
|
// once joined. Best effort; the stored credential serves when the control
|
|
1197
1232
|
// plane cannot be reached.
|
|
1198
1233
|
try {
|
|
1199
|
-
await mesh.syncControlPlane();
|
|
1234
|
+
await mesh.syncControlPlane(AbortSignal.any([signal, AbortSignal.timeout(JOIN_SYNC_TIMEOUT_MS)]));
|
|
1200
1235
|
}
|
|
1201
1236
|
catch {
|
|
1202
|
-
// Joining goes on with what the credential holds
|
|
1237
|
+
// Joining goes on with what the credential holds, unless the caller
|
|
1238
|
+
// stopped the join while the control plane kept it waiting.
|
|
1239
|
+
if (signal.aborted) {
|
|
1240
|
+
throw new Error(`join stopped while syncing with the control plane: ${stopReason()}`);
|
|
1241
|
+
}
|
|
1203
1242
|
}
|
|
1204
1243
|
const rawAddrs = options.routerAddresses ?? mesh.credential.routerAddresses;
|
|
1205
1244
|
if (rawAddrs.length === 0 && mesh.routers.length === 0) {
|
|
@@ -1225,14 +1264,18 @@ export async function joinMesh(mesh, options = {}) {
|
|
|
1225
1264
|
onGoAway: (peerId, msg, retryAfterMs) => session?.handleGoAway(peerId, msg, retryAfterMs),
|
|
1226
1265
|
}), AUTH_HANDLER_OPTIONS);
|
|
1227
1266
|
session = new MeshSession(mesh, node, [], authenticatedPeers, banned, options);
|
|
1228
|
-
const failures = await session.topUpRouters(
|
|
1229
|
-
|
|
1267
|
+
const failures = await session.topUpRouters(signal);
|
|
1268
|
+
// One admitted router is enough to be on the mesh; only the caller's own
|
|
1269
|
+
// signal turns that into a failure.
|
|
1270
|
+
if (options.signal?.aborted === true || session.routers.length === 0) {
|
|
1230
1271
|
const detail = failures.length > 0 ? `:\n ${failures.join("\n ")}` : "";
|
|
1231
|
-
throw new Error(`no router admitted this member${detail}`);
|
|
1272
|
+
throw new Error(signal.aborted ? `join stopped: ${stopReason()}${detail}` : `no router admitted this member${detail}`);
|
|
1232
1273
|
}
|
|
1233
1274
|
// Best effort: a router that does not take the record is a warning the
|
|
1234
|
-
// next announcement retries; the member is on the mesh regardless.
|
|
1235
|
-
|
|
1275
|
+
// next announcement retries; the member is on the mesh regardless. Under
|
|
1276
|
+
// the caller's signal only: a join that ran out of its own time still
|
|
1277
|
+
// announces the routers it holds.
|
|
1278
|
+
await session.announce(options.signal).catch(() => { });
|
|
1236
1279
|
return session;
|
|
1237
1280
|
}
|
|
1238
1281
|
catch (err) {
|
|
@@ -1245,6 +1288,9 @@ export async function joinMesh(mesh, options = {}) {
|
|
|
1245
1288
|
throw err;
|
|
1246
1289
|
}
|
|
1247
1290
|
}
|
|
1291
|
+
function errorText(err) {
|
|
1292
|
+
return err instanceof Error ? err.message : String(err);
|
|
1293
|
+
}
|
|
1248
1294
|
/** The peer a multiaddr ends at, in canonical form: its trailing `/p2p/<id>`, or undefined for a relay address with no target yet. */
|
|
1249
1295
|
function targetPeerOf(ma) {
|
|
1250
1296
|
const last = ma.getComponents().at(-1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentmesh-p2p/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Native SDK for joining an Agent Mesh from a Node.js agent process or a browser page",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
"directory": "sdk/js"
|
|
10
10
|
},
|
|
11
11
|
"type": "module",
|
|
12
|
+
"bin": {
|
|
13
|
+
"agentmesh": "./dist/cli.js"
|
|
14
|
+
},
|
|
12
15
|
"engines": {
|
|
13
16
|
"node": ">=22.18"
|
|
14
17
|
},
|
|
@@ -72,5 +75,6 @@
|
|
|
72
75
|
"esbuild": "^0.28.2",
|
|
73
76
|
"express": "^5.2.1",
|
|
74
77
|
"typescript": "^7.0.2"
|
|
75
|
-
}
|
|
78
|
+
},
|
|
79
|
+
"mcpName": "io.github.google/agentmesh"
|
|
76
80
|
}
|