@agentmesh-p2p/sdk 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth.d.ts +2 -1
- package/dist/auth.js +3 -2
- package/dist/controlplane.d.ts +2 -2
- package/dist/controlplane.js +15 -12
- package/dist/discovery.js +3 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- 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 +1 -1
package/dist/auth.d.ts
CHANGED
|
@@ -21,8 +21,9 @@ export declare class AuthRejectedError extends Error {
|
|
|
21
21
|
/**
|
|
22
22
|
* Client side: presents frame on a new /mesh/auth/1.0.0 stream and returns
|
|
23
23
|
* the peer's verified credential. trustedKeys are the control plane keys.
|
|
24
|
+
* The handshake has a timeout of its own; outer, when given, can end it sooner.
|
|
24
25
|
*/
|
|
25
|
-
export declare function authenticateWithPeer(conn: Connection, frame: Uint8Array, trustedKeys: Uint8Array[]): Promise<VerifiedBiscuit>;
|
|
26
|
+
export declare function authenticateWithPeer(conn: Connection, frame: Uint8Array, trustedKeys: Uint8Array[], outer?: AbortSignal): Promise<VerifiedBiscuit>;
|
|
26
27
|
export interface AuthServerOptions {
|
|
27
28
|
/** This member's current biscuit, read per handshake so a refresh takes effect. */
|
|
28
29
|
ownBiscuit(): Uint8Array;
|
package/dist/auth.js
CHANGED
|
@@ -45,9 +45,10 @@ function framed(stream) {
|
|
|
45
45
|
/**
|
|
46
46
|
* Client side: presents frame on a new /mesh/auth/1.0.0 stream and returns
|
|
47
47
|
* the peer's verified credential. trustedKeys are the control plane keys.
|
|
48
|
+
* The handshake has a timeout of its own; outer, when given, can end it sooner.
|
|
48
49
|
*/
|
|
49
|
-
export async function authenticateWithPeer(conn, frame, trustedKeys) {
|
|
50
|
-
const signal = AbortSignal.timeout(AUTH_HANDSHAKE_TIMEOUT_MS);
|
|
50
|
+
export async function authenticateWithPeer(conn, frame, trustedKeys, outer) {
|
|
51
|
+
const signal = outer === undefined ? AbortSignal.timeout(AUTH_HANDSHAKE_TIMEOUT_MS) : AbortSignal.any([outer, AbortSignal.timeout(AUTH_HANDSHAKE_TIMEOUT_MS)]);
|
|
51
52
|
// A peer behind a router is reached over a relayed, limited connection.
|
|
52
53
|
const stream = await conn.newStream(AUTH_PROTOCOL, { signal, runOnLimitedConnection: true });
|
|
53
54
|
try {
|
package/dist/controlplane.d.ts
CHANGED
|
@@ -95,12 +95,12 @@ export declare class ControlPlaneClient {
|
|
|
95
95
|
readonly url: URL;
|
|
96
96
|
constructor(options: ControlPlaneClientOptions);
|
|
97
97
|
/** GET /info: OIDC settings, router addresses and the ban list. Unauthenticated. */
|
|
98
|
-
info(): Promise<ControlPlaneInfoResponse>;
|
|
98
|
+
info(signal?: AbortSignal): Promise<ControlPlaneInfoResponse>;
|
|
99
99
|
/**
|
|
100
100
|
* GET /keys: every signing key the control plane currently trusts,
|
|
101
101
|
* verified against a key the caller already trusts (the enrollment key).
|
|
102
102
|
*/
|
|
103
|
-
keys(trusted: Uint8Array[]): Promise<Uint8Array[]>;
|
|
103
|
+
keys(trusted: Uint8Array[], signal?: AbortSignal): Promise<Uint8Array[]>;
|
|
104
104
|
/**
|
|
105
105
|
* POST /enroll with a bootstrap token, then GET /enroll/status until an
|
|
106
106
|
* operator approves the request if the mesh is not on auto-approve.
|
package/dist/controlplane.js
CHANGED
|
@@ -164,16 +164,16 @@ export class ControlPlaneClient {
|
|
|
164
164
|
this.#now = options.now ?? (() => Date.now());
|
|
165
165
|
}
|
|
166
166
|
/** GET /info: OIDC settings, router addresses and the ban list. Unauthenticated. */
|
|
167
|
-
async info() {
|
|
168
|
-
const body = await this.#request("GET", "/info");
|
|
167
|
+
async info(signal) {
|
|
168
|
+
const body = await this.#request("GET", "/info", undefined, {}, signal);
|
|
169
169
|
return fromBinary(ControlPlaneInfoResponseSchema, body);
|
|
170
170
|
}
|
|
171
171
|
/**
|
|
172
172
|
* GET /keys: every signing key the control plane currently trusts,
|
|
173
173
|
* verified against a key the caller already trusts (the enrollment key).
|
|
174
174
|
*/
|
|
175
|
-
async keys(trusted) {
|
|
176
|
-
const body = await this.#request("GET", "/keys");
|
|
175
|
+
async keys(trusted, signal) {
|
|
176
|
+
const body = await this.#request("GET", "/keys", undefined, {}, signal);
|
|
177
177
|
return verifyKeysResponse(fromBinary(KeysResponseSchema, body), trusted, this.#now());
|
|
178
178
|
}
|
|
179
179
|
/**
|
|
@@ -304,14 +304,14 @@ export class ControlPlaneClient {
|
|
|
304
304
|
}
|
|
305
305
|
throw new ControlPlaneError(path, outcome.status, text);
|
|
306
306
|
}
|
|
307
|
-
async #fetchOnce(method, path, body, headers = {}) {
|
|
308
|
-
|
|
309
|
-
const
|
|
307
|
+
async #fetchOnce(method, path, body, headers = {}, signal) {
|
|
308
|
+
// The caller's signal cancels the request itself, not only the wait for it.
|
|
309
|
+
const timeout = AbortSignal.timeout(this.#timeoutMs);
|
|
310
310
|
try {
|
|
311
311
|
const init = {
|
|
312
312
|
method,
|
|
313
313
|
headers: { Accept: PROTOBUF_CONTENT_TYPE, ...headers },
|
|
314
|
-
signal:
|
|
314
|
+
signal: signal === undefined ? timeout : AbortSignal.any([signal, timeout]),
|
|
315
315
|
};
|
|
316
316
|
if (body !== undefined) {
|
|
317
317
|
init.body = new Uint8Array(body);
|
|
@@ -324,12 +324,15 @@ export class ControlPlaneClient {
|
|
|
324
324
|
}
|
|
325
325
|
return { ok: resp.ok, status: resp.status, buf, dateHeader: resp.headers.get("date") };
|
|
326
326
|
}
|
|
327
|
-
|
|
328
|
-
|
|
327
|
+
catch (err) {
|
|
328
|
+
if (timeout.aborted && signal?.aborted !== true) {
|
|
329
|
+
throw new Error(`control plane ${path}: timed out after ${this.#timeoutMs}ms`);
|
|
330
|
+
}
|
|
331
|
+
throw err;
|
|
329
332
|
}
|
|
330
333
|
}
|
|
331
|
-
async #request(method, path, body, headers = {}) {
|
|
332
|
-
const outcome = await this.#fetchOnce(method, path, body, headers);
|
|
334
|
+
async #request(method, path, body, headers = {}, signal) {
|
|
335
|
+
const outcome = await this.#fetchOnce(method, path, body, headers, signal);
|
|
333
336
|
if (!outcome.ok) {
|
|
334
337
|
throw new ControlPlaneError(path, outcome.status, new TextDecoder().decode(outcome.buf));
|
|
335
338
|
}
|
package/dist/discovery.js
CHANGED
|
@@ -84,7 +84,7 @@ async function ask(conn, req, signal) {
|
|
|
84
84
|
*/
|
|
85
85
|
export async function request(routers, req, signal) {
|
|
86
86
|
const lookup = req.request.case === "findProviders" || req.request.case === "findPeer";
|
|
87
|
-
|
|
87
|
+
const failures = [];
|
|
88
88
|
let asked = 0;
|
|
89
89
|
let empty;
|
|
90
90
|
for (const router of routers) {
|
|
@@ -103,7 +103,7 @@ export async function request(routers, req, signal) {
|
|
|
103
103
|
return resp;
|
|
104
104
|
}
|
|
105
105
|
catch (err) {
|
|
106
|
-
|
|
106
|
+
failures.push(`${router.peerId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
107
107
|
if (signal?.aborted === true) {
|
|
108
108
|
break;
|
|
109
109
|
}
|
|
@@ -115,7 +115,7 @@ export async function request(routers, req, signal) {
|
|
|
115
115
|
if (asked === 0) {
|
|
116
116
|
throw new DirectoryError("no held router to ask");
|
|
117
117
|
}
|
|
118
|
-
throw new DirectoryError(`no held router answered: ${
|
|
118
|
+
throw new DirectoryError(`no held router answered: ${failures.join("; ")}`);
|
|
119
119
|
}
|
|
120
120
|
/**
|
|
121
121
|
* Announces this member as a provider of key, reached at addrs, to a held
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { Identity, canonicalPeerId, peerIdFromPublicKey, libp2pPublicKey, verify
|
|
|
3
3
|
export { ControlPlaneClient, ControlPlaneError, EnrollmentRejectedError, InsecureControlPlaneURLError, KeysNotTrustedError, ROLE_NODE, validateControlPlaneURL, verifyKeysResponse, type ControlPlaneClientOptions, type Enrollment, type EnrollBootstrapParams, type PolicyRulesParams, type RegisterParams, type RefreshParams, type RefreshResult, } from "./controlplane.ts";
|
|
4
4
|
export { attenuateCredential, credentialFromJSON, credentialTimeToLiveSeconds, credentialToJSON, decodeAuthResponse, encodeAuthFrame, sealCredential, withCredentialMethods, type MeshCredential, } from "./credential.ts";
|
|
5
5
|
export { egressChallenge, enrollChallenge, enrollStatusChallenge, nodesCatalogChallenge, policiesChallenge, refreshChallenge, registerChallenge, revocationsChallenge, } from "./challenges.ts";
|
|
6
|
-
export { DEFAULT_ROUTERS, MeshSession, ROUTER_REDIAL_BACKOFFS_MS, candidatesFromRouterInfos, selectRouters, type AdmittedRouter, type DiscoveredProvider, type JoinOptions, type Peer, type RouterCandidate, type SelectRoutersOptions, type ToolCallResult, } from "./session.ts";
|
|
6
|
+
export { DEFAULT_ROUTERS, JOIN_TIMEOUT_MS, MeshSession, ROUTER_REDIAL_BACKOFFS_MS, candidatesFromRouterInfos, selectRouters, type AdmittedRouter, type DiscoveredProvider, type JoinOptions, type Peer, type RouterCandidate, type SelectRoutersOptions, type ToolCallResult, } from "./session.ts";
|
|
7
7
|
export { BiscuitVerificationError, ROLE_ROUTER, attenuateBiscuit, requireRole, sealBiscuit, verifyPeerBiscuit, type VerifiedBiscuit, } from "./biscuit.ts";
|
|
8
8
|
export { decodeTARBlockPayload, effectiveTARExpiration, encodeTARBlockFact, encodeTARBlockPayload, evaluateTaskRules, matchHTTPPath, matchServicePattern, matchTaskRule, parseTARBlockSource, validateHTTPGrantPath, validateServicePattern, validateTaskAuthorizationRule, validateTaskRule, type TaskRequestContext, } from "./tar.ts";
|
|
9
9
|
export { AUTH_HANDLER_OPTIONS, AUTH_PROTOCOL, GOAWAY_MAX_RETRY_AFTER_MS, GOAWAY_PROTOCOL, GOAWAY_STREAM_TIMEOUT_MS, MAX_GOAWAY_MESSAGE_BYTES, MCP_PROTOCOL, ROUTER_SHUN_DURATION_MS, AuthRejectedError, authenticateWithPeer, authStreamHandler, clampGoAwayRetryAfterMs, goAwayReasonName, goAwayStreamHandler, type GoAwayServerOptions, } from "./auth.ts";
|
package/dist/index.js
CHANGED
|
@@ -16,7 +16,7 @@ export { Identity, canonicalPeerId, peerIdFromPublicKey, libp2pPublicKey, verify
|
|
|
16
16
|
export { ControlPlaneClient, ControlPlaneError, EnrollmentRejectedError, InsecureControlPlaneURLError, KeysNotTrustedError, ROLE_NODE, validateControlPlaneURL, verifyKeysResponse, } from "./controlplane.js";
|
|
17
17
|
export { attenuateCredential, credentialFromJSON, credentialTimeToLiveSeconds, credentialToJSON, decodeAuthResponse, encodeAuthFrame, sealCredential, withCredentialMethods, } from "./credential.js";
|
|
18
18
|
export { egressChallenge, enrollChallenge, enrollStatusChallenge, nodesCatalogChallenge, policiesChallenge, refreshChallenge, registerChallenge, revocationsChallenge, } from "./challenges.js";
|
|
19
|
-
export { DEFAULT_ROUTERS, MeshSession, ROUTER_REDIAL_BACKOFFS_MS, candidatesFromRouterInfos, selectRouters, } from "./session.js";
|
|
19
|
+
export { DEFAULT_ROUTERS, JOIN_TIMEOUT_MS, MeshSession, ROUTER_REDIAL_BACKOFFS_MS, candidatesFromRouterInfos, selectRouters, } from "./session.js";
|
|
20
20
|
export { BiscuitVerificationError, ROLE_ROUTER, attenuateBiscuit, requireRole, sealBiscuit, verifyPeerBiscuit, } from "./biscuit.js";
|
|
21
21
|
export { decodeTARBlockPayload, effectiveTARExpiration, encodeTARBlockFact, encodeTARBlockPayload, evaluateTaskRules, matchHTTPPath, matchServicePattern, matchTaskRule, parseTARBlockSource, validateHTTPGrantPath, validateServicePattern, validateTaskAuthorizationRule, validateTaskRule, } from "./tar.js";
|
|
22
22
|
export { AUTH_HANDLER_OPTIONS, AUTH_PROTOCOL, GOAWAY_MAX_RETRY_AFTER_MS, GOAWAY_PROTOCOL, GOAWAY_STREAM_TIMEOUT_MS, MAX_GOAWAY_MESSAGE_BYTES, MCP_PROTOCOL, ROUTER_SHUN_DURATION_MS, AuthRejectedError, authenticateWithPeer, authStreamHandler, clampGoAwayRetryAfterMs, goAwayReasonName, goAwayStreamHandler, } from "./auth.js";
|
package/dist/mcp.js
CHANGED
|
@@ -21,7 +21,7 @@ import { decodeAuthFrame, decodeAuthResponse } from "./credential.js";
|
|
|
21
21
|
/** go-msgio's default message cap, which agentmesh-node's StreamTransport uses. */
|
|
22
22
|
export const MAX_MCP_MESSAGE_BYTES = 8 * 1024 * 1024;
|
|
23
23
|
/** The MCP client the SDK identifies itself as. */
|
|
24
|
-
export const MCP_CLIENT_INFO = { name: "agent-mesh-sdk", version: "0.1.
|
|
24
|
+
export const MCP_CLIENT_INFO = { name: "agent-mesh-sdk", version: "0.1.1" };
|
|
25
25
|
/**
|
|
26
26
|
* A Transport for @modelcontextprotocol/sdk over a libp2p stream that has
|
|
27
27
|
* already passed the auth handshake.
|
package/dist/mesh.d.ts
CHANGED
|
@@ -117,9 +117,10 @@ export declare class AgentMesh {
|
|
|
117
117
|
* Trades the current biscuit for a fresh one and persists it. The control
|
|
118
118
|
* plane redeems only the last biscuit it issued, so a lost refresh result
|
|
119
119
|
* means re-enrolling; persisting before returning keeps that rare.
|
|
120
|
-
* Concurrent calls wait for each other.
|
|
120
|
+
* Concurrent calls wait for each other. A refresh whose signal has
|
|
121
|
+
* aborted by the time its turn comes does not start.
|
|
121
122
|
*/
|
|
122
|
-
refresh(): Promise<MeshCredential>;
|
|
123
|
+
refresh(signal?: AbortSignal): Promise<MeshCredential>;
|
|
123
124
|
/**
|
|
124
125
|
* Adopts a signing key announced by a KEY_ROTATION event, so peers whose
|
|
125
126
|
* credentials the new key signs verify before the next pull confirms it.
|
|
@@ -133,8 +134,11 @@ export declare class AgentMesh {
|
|
|
133
134
|
* addresses and the ban set. Each part is attempted even when another
|
|
134
135
|
* fails; the errors are reported together. Gossip events only bring this
|
|
135
136
|
* forward; they are never the only way state arrives.
|
|
137
|
+
*
|
|
138
|
+
* An aborted signal cancels the request in flight and throws its reason:
|
|
139
|
+
* nothing more is sent and nothing is written to the state directory.
|
|
136
140
|
*/
|
|
137
|
-
syncControlPlane(): Promise<ControlPlaneSync>;
|
|
141
|
+
syncControlPlane(signal?: AbortSignal): Promise<ControlPlaneSync>;
|
|
138
142
|
/**
|
|
139
143
|
* The frame that opens every stream to a peer: this member's biscuit plus
|
|
140
144
|
* the service it wants (e.g. "mcp://calculator").
|
package/dist/mesh.js
CHANGED
|
@@ -194,14 +194,16 @@ export class AgentMesh {
|
|
|
194
194
|
* Trades the current biscuit for a fresh one and persists it. The control
|
|
195
195
|
* plane redeems only the last biscuit it issued, so a lost refresh result
|
|
196
196
|
* means re-enrolling; persisting before returning keeps that rare.
|
|
197
|
-
* Concurrent calls wait for each other.
|
|
197
|
+
* Concurrent calls wait for each other. A refresh whose signal has
|
|
198
|
+
* aborted by the time its turn comes does not start.
|
|
198
199
|
*/
|
|
199
|
-
async refresh() {
|
|
200
|
-
const run = this.#refreshChain.then(() => this.#refreshOnce(), () => this.#refreshOnce());
|
|
200
|
+
async refresh(signal) {
|
|
201
|
+
const run = this.#refreshChain.then(() => this.#refreshOnce(signal), () => this.#refreshOnce(signal));
|
|
201
202
|
this.#refreshChain = run.then(() => undefined, () => undefined);
|
|
202
203
|
return run;
|
|
203
204
|
}
|
|
204
|
-
async #refreshOnce() {
|
|
205
|
+
async #refreshOnce(signal) {
|
|
206
|
+
signal?.throwIfAborted();
|
|
205
207
|
let jwt;
|
|
206
208
|
if (this.#jwtSource !== undefined) {
|
|
207
209
|
try {
|
|
@@ -214,6 +216,10 @@ export class AgentMesh {
|
|
|
214
216
|
// Best effort: fall back to session-only refresh if the token source is temporarily unavailable.
|
|
215
217
|
}
|
|
216
218
|
}
|
|
219
|
+
// The last point to stop. The /refresh request itself is not cut short:
|
|
220
|
+
// the control plane spends the old biscuit when it answers, so only a
|
|
221
|
+
// refresh that runs to the end can save the new one.
|
|
222
|
+
signal?.throwIfAborted();
|
|
217
223
|
const result = await this.controlPlane.refresh({
|
|
218
224
|
identity: this.identity,
|
|
219
225
|
biscuit: this.#credential.biscuit,
|
|
@@ -256,33 +262,37 @@ export class AgentMesh {
|
|
|
256
262
|
* addresses and the ban set. Each part is attempted even when another
|
|
257
263
|
* fails; the errors are reported together. Gossip events only bring this
|
|
258
264
|
* forward; they are never the only way state arrives.
|
|
265
|
+
*
|
|
266
|
+
* An aborted signal cancels the request in flight and throws its reason:
|
|
267
|
+
* nothing more is sent and nothing is written to the state directory.
|
|
259
268
|
*/
|
|
260
|
-
async syncControlPlane() {
|
|
269
|
+
async syncControlPlane(signal) {
|
|
261
270
|
const errors = [];
|
|
262
271
|
let keysChanged = false;
|
|
263
272
|
let refreshed = false;
|
|
264
273
|
try {
|
|
265
|
-
const keys = await this.controlPlane.keys(this.#credential.controlPlaneKeys);
|
|
274
|
+
const keys = await this.controlPlane.keys(this.#credential.controlPlaneKeys, signal);
|
|
266
275
|
if (keys.length === 0) {
|
|
267
276
|
throw new Error("/keys returned no keys");
|
|
268
277
|
}
|
|
269
278
|
keysChanged = !sameKeySet(keys, this.#credential.controlPlaneKeys);
|
|
270
279
|
this.#credential = withCredentialMethods({ ...this.#credential, controlPlaneKeys: keys });
|
|
271
280
|
if (credentialPredatesRotation(this.#credential)) {
|
|
272
|
-
await this.refresh();
|
|
281
|
+
await this.refresh(signal);
|
|
273
282
|
refreshed = true;
|
|
274
283
|
}
|
|
275
284
|
}
|
|
276
285
|
catch (err) {
|
|
277
286
|
errors.push(`keys: ${err instanceof Error ? err.message : String(err)}`);
|
|
278
287
|
}
|
|
288
|
+
signal?.throwIfAborted();
|
|
279
289
|
let bannedPeerIds;
|
|
280
290
|
let routers;
|
|
281
291
|
// Taken before the request: a ban recorded after this instant cannot be
|
|
282
292
|
// in the answer, so its absence must not be read as an unban.
|
|
283
293
|
const fetchedAt = new Date();
|
|
284
294
|
try {
|
|
285
|
-
const info = await this.controlPlane.info();
|
|
295
|
+
const info = await this.controlPlane.info(signal);
|
|
286
296
|
routers = info.routers;
|
|
287
297
|
this.#routers = info.routers;
|
|
288
298
|
const discovered = info.routerAddresses.length > 0
|
|
@@ -296,6 +306,7 @@ export class AgentMesh {
|
|
|
296
306
|
catch (err) {
|
|
297
307
|
errors.push(`info: ${err instanceof Error ? err.message : String(err)}`);
|
|
298
308
|
}
|
|
309
|
+
signal?.throwIfAborted();
|
|
299
310
|
if (keysChanged || bannedPeerIds !== undefined) {
|
|
300
311
|
await this.save();
|
|
301
312
|
}
|
package/dist/session.d.ts
CHANGED
|
@@ -78,7 +78,12 @@ export interface JoinOptions extends MeshHostOptions {
|
|
|
78
78
|
* requiredLabels. Held on every call, MCP and HTTP alike; no call waives it.
|
|
79
79
|
*/
|
|
80
80
|
egressRequireLabels?: Record<string, string>;
|
|
81
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* Bounds the whole join: the control plane sync, and the dial, auth
|
|
83
|
+
* handshake and relay reservation with every router tried. Without one the
|
|
84
|
+
* join is bounded by JOIN_TIMEOUT_MS (60s). A join that stops throws an
|
|
85
|
+
* error naming each router tried and the step it was in.
|
|
86
|
+
*/
|
|
82
87
|
signal?: AbortSignal;
|
|
83
88
|
}
|
|
84
89
|
export interface AdmittedRouter {
|
|
@@ -124,6 +129,17 @@ export interface ToolCallResult {
|
|
|
124
129
|
/** The raw MCP result. */
|
|
125
130
|
raw: unknown;
|
|
126
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Bounds a join whose caller passed no signal: each step has a timeout of
|
|
134
|
+
* its own, but a join tries them in turn over every router it is offered.
|
|
135
|
+
*/
|
|
136
|
+
export declare const JOIN_TIMEOUT_MS = 60000;
|
|
137
|
+
/**
|
|
138
|
+
* Bounds the control plane sync a join starts with. The sync is best effort,
|
|
139
|
+
* so a control plane that does not answer in time leaves the join to go on
|
|
140
|
+
* with the stored credential instead of using up the whole join.
|
|
141
|
+
*/
|
|
142
|
+
export declare const JOIN_SYNC_TIMEOUT_MS = 5000;
|
|
127
143
|
/**
|
|
128
144
|
* A member that is on the mesh: a libp2p host authenticated with at least
|
|
129
145
|
* one router, answering the auth handshake for peers that dial it, and
|
|
@@ -184,7 +200,7 @@ export declare class MeshSession {
|
|
|
184
200
|
* the mesh's record TTL. A member with no relayed address announces
|
|
185
201
|
* nothing: there is nothing to reach it at.
|
|
186
202
|
*/
|
|
187
|
-
announce(): Promise<void>;
|
|
203
|
+
announce(signal?: AbortSignal): Promise<void>;
|
|
188
204
|
/** Whether peerId is currently one of the routers this member holds. */
|
|
189
205
|
isHeldRouter(peerId: string): boolean;
|
|
190
206
|
/** Handles a /mesh/goaway/1.0.0 message from a held router: shuns it, detaches it, and tops up from the catalog. */
|
|
@@ -312,5 +328,8 @@ export declare function candidatesFromRouterInfos(routers: readonly RouterInfo[]
|
|
|
312
328
|
* and orders by matching prefer labels (descending) then load + jitter (ascending).
|
|
313
329
|
*/
|
|
314
330
|
export declare function selectRouters(candidates: readonly RouterCandidate[], k: number, options?: SelectRoutersOptions): RouterCandidate[];
|
|
315
|
-
/**
|
|
316
|
-
|
|
331
|
+
/**
|
|
332
|
+
* Implements AgentMesh.join(); lives here to keep mesh.ts free of libp2p.
|
|
333
|
+
* timeoutMs bounds a join whose caller passed no signal.
|
|
334
|
+
*/
|
|
335
|
+
export declare function joinMesh(mesh: AgentMesh, options?: JoinOptions, timeoutMs?: number): Promise<MeshSession>;
|
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);
|