@agentmesh-p2p/sdk 0.0.0-stage → 0.1.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.
Files changed (68) hide show
  1. package/README.md +182 -2
  2. package/dist/auth.d.ts +66 -0
  3. package/dist/auth.js +164 -0
  4. package/dist/authorizer.d.ts +41 -0
  5. package/dist/authorizer.js +201 -0
  6. package/dist/base58.d.ts +2 -0
  7. package/dist/base58.js +57 -0
  8. package/dist/biscuit.d.ts +75 -0
  9. package/dist/biscuit.js +250 -0
  10. package/dist/bytes.d.ts +10 -0
  11. package/dist/bytes.js +51 -0
  12. package/dist/challenges.d.ts +16 -0
  13. package/dist/challenges.js +55 -0
  14. package/dist/conformance-join.d.ts +1 -0
  15. package/dist/conformance-join.js +257 -0
  16. package/dist/conformance.d.ts +1 -0
  17. package/dist/conformance.js +60 -0
  18. package/dist/controlplane.d.ts +123 -0
  19. package/dist/controlplane.js +368 -0
  20. package/dist/credential.d.ts +66 -0
  21. package/dist/credential.js +124 -0
  22. package/dist/discovery.d.ts +50 -0
  23. package/dist/discovery.js +150 -0
  24. package/dist/gen/agentmesh_pb.d.ts +2740 -0
  25. package/dist/gen/agentmesh_pb.js +591 -0
  26. package/dist/gen/datalog.d.ts +39 -0
  27. package/dist/gen/datalog.js +84 -0
  28. package/dist/host.d.ts +47 -0
  29. package/dist/host.js +109 -0
  30. package/dist/http1.d.ts +68 -0
  31. package/dist/http1.js +371 -0
  32. package/dist/identity.d.ts +31 -0
  33. package/dist/identity.js +125 -0
  34. package/dist/index.d.ts +18 -0
  35. package/dist/index.js +30 -0
  36. package/dist/libp2p-http-node.d.ts +17 -0
  37. package/dist/libp2p-http-node.js +116 -0
  38. package/dist/libp2p-http.d.ts +165 -0
  39. package/dist/libp2p-http.js +581 -0
  40. package/dist/mcp.d.ts +67 -0
  41. package/dist/mcp.js +157 -0
  42. package/dist/mesh.d.ts +151 -0
  43. package/dist/mesh.js +370 -0
  44. package/dist/platform/ingress.browser.d.ts +1 -0
  45. package/dist/platform/ingress.browser.js +16 -0
  46. package/dist/platform/ingress.d.ts +1 -0
  47. package/dist/platform/ingress.js +15 -0
  48. package/dist/platform/state.browser.d.ts +5 -0
  49. package/dist/platform/state.browser.js +47 -0
  50. package/dist/platform/state.d.ts +5 -0
  51. package/dist/platform/state.js +44 -0
  52. package/dist/platform/transports.browser.d.ts +3 -0
  53. package/dist/platform/transports.browser.js +27 -0
  54. package/dist/platform/transports.d.ts +3 -0
  55. package/dist/platform/transports.js +27 -0
  56. package/dist/platform/types.d.ts +7 -0
  57. package/dist/platform/types.js +14 -0
  58. package/dist/platform/wasm.browser.d.ts +2 -0
  59. package/dist/platform/wasm.browser.js +16 -0
  60. package/dist/platform/wasm.d.ts +2 -0
  61. package/dist/platform/wasm.js +56 -0
  62. package/dist/session.d.ts +316 -0
  63. package/dist/session.js +1269 -0
  64. package/dist/sync.d.ts +41 -0
  65. package/dist/sync.js +111 -0
  66. package/dist/tar.d.ts +25 -0
  67. package/dist/tar.js +406 -0
  68. package/package.json +74 -4
package/README.md CHANGED
@@ -1,3 +1,183 @@
1
- # Temporary Holding Version
1
+ # @agentmesh-p2p/sdk
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Native JavaScript SDK for joining an Agent Mesh from inside the agent
4
+ process. It replaces the `agentmesh-node` sidecar for agents written for Node.js
5
+ or running in a browser page:
6
+ the agent enrolls with the control plane, joins the mesh through a router,
7
+ finds services and calls them, answers A2A requests for the agent itself,
8
+ and follows the control plane's keys, bans and policy while it runs. It
9
+ publishes no service; a tool or a model others should find by name runs
10
+ behind a `agentmesh-node`.
11
+
12
+ Guide: [google.github.io/agentmesh/docs/guides/native-sdks](https://google.github.io/agentmesh/docs/guides/native-sdks/),
13
+ from an empty machine to two programs on a mesh.
14
+ Source: [github.com/google/agentmesh/tree/main/sdk/js](https://github.com/google/agentmesh/tree/main/sdk/js).
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ npm install @agentmesh-p2p/sdk @modelcontextprotocol/sdk zod
20
+ ```
21
+
22
+ Requires Node.js 22.18 or later. In a browser, bundle it with the page
23
+ (the package's `browser` field selects the browser files); the guide's
24
+ [In a browser](https://google.github.io/agentmesh/docs/guides/native-sdks/#in-a-browser)
25
+ section has the details and an example page.
26
+
27
+ ## Use
28
+
29
+ Both programs below are in
30
+ [`examples/`](https://github.com/google/agentmesh/tree/main/sdk/js/examples) and
31
+ run against a real mesh in the repository's tests. They read the mesh from
32
+ `AGENTMESH_CONTROL_PLANE_URL` and the enrollment token from
33
+ `AGENTMESH_BOOTSTRAP_TOKEN_PATH`; the guide shows how to get both from `agentmesh-one`
34
+ or from the operator of an existing mesh.
35
+
36
+ Find a service and call it:
37
+
38
+ <!-- embed: sdk/js/examples/call.ts -->
39
+ ```ts
40
+ // Calls something on the mesh: a tool of an MCP service or a path of an
41
+ // inference or A2A service someone published, found by name, or an agent that
42
+ // published nothing, reached by its peer ID.
43
+ //
44
+ // node call.js mcp://everything echo '{"message": "hi"}'
45
+ // node call.js inference://ollama /v1/models
46
+ // node call.js 12D3KooW... a2a://agent /card
47
+ //
48
+ // AGENTMESH_CONTROL_PLANE_URL names the mesh. The first run enrolls with the file
49
+ // AGENTMESH_BOOTSTRAP_TOKEN_PATH (a token the mesh operator gave you) or
50
+ // AGENTMESH_JWT_PATH (a workload identity token your platform issues, such as a
51
+ // Kubernetes projected service account token), and keeps the identity and
52
+ // credential in AGENTMESH_STATE_DIR; later runs resume from there without it.
53
+ import { homedir } from "node:os";
54
+ import { AgentMesh, type DiscoveredProvider } from "@agentmesh-p2p/sdk";
55
+
56
+ let argv = process.argv.slice(2);
57
+ // A first argument that is not a service target is the peer ID of an agent.
58
+ let peerId: string | undefined;
59
+ if (argv[0] !== undefined && !argv[0].includes("://")) {
60
+ [peerId, ...argv] = argv as [string, ...string[]];
61
+ }
62
+ const [service = peerId !== undefined ? "a2a://agent" : "mcp://everything", toolOrPath = peerId !== undefined ? "/card" : "echo", args = '{"message": "hi"}'] = argv;
63
+
64
+ const mesh = await AgentMesh.enroll({
65
+ controlPlaneUrl: process.env.AGENTMESH_CONTROL_PLANE_URL ?? "https://mesh.example.com",
66
+ bootstrapTokenPath: process.env.AGENTMESH_BOOTSTRAP_TOKEN_PATH,
67
+ jwtPath: process.env.AGENTMESH_JWT_PATH,
68
+ stateDir: process.env.AGENTMESH_STATE_DIR ?? `${homedir()}/.config/agentmesh/caller`,
69
+ // A plaintext http:// control plane is otherwise accepted only on loopback.
70
+ allowInsecure: process.env.AGENTMESH_INSECURE_CONTROL_PLANE === "true",
71
+ });
72
+ const session = await mesh.join();
73
+ console.log(`on the mesh as ${session.peerId}`);
74
+
75
+ // The peer to call: an agent named by ID, or a provider of a published service.
76
+ let provider: DiscoveredProvider = { peerId: peerId ?? "", addrs: [] };
77
+ if (peerId !== undefined) {
78
+ // An agent is not in the discovery table; the SDK finds the path to its
79
+ // peer ID through the routers.
80
+ await session.connect(peerId);
81
+ } else {
82
+ const providers = await session.discover(service);
83
+ if (providers.length === 0) {
84
+ throw new Error(`no member of the mesh serves ${service}`);
85
+ }
86
+ // A provider record can outlive its member; the first that answers is used.
87
+ let reached = false;
88
+ for (const candidate of providers) {
89
+ try {
90
+ await session.connect(candidate);
91
+ provider = candidate;
92
+ reached = true;
93
+ break;
94
+ } catch (err) {
95
+ console.error(`${candidate.peerId}: ${(err as Error).message}`);
96
+ }
97
+ }
98
+ if (!reached) {
99
+ throw new Error(`no provider of ${service} is reachable`);
100
+ }
101
+ }
102
+ console.log(`${service} is served by ${provider.peerId}`);
103
+
104
+ if (service.startsWith("mcp://")) {
105
+ const tools = await session.listTools(provider, service);
106
+ console.log(`tools: ${tools.map((t) => t.name).join(", ")}`);
107
+ const result = await session.callTool(provider, service, toolOrPath, JSON.parse(args));
108
+ console.log(result.text.join("\n"));
109
+ } else {
110
+ const response = await session.request(provider, service, toolOrPath);
111
+ console.log(response.status, response.text());
112
+ }
113
+
114
+ await session.close();
115
+ ```
116
+ <!-- /embed -->
117
+
118
+ Be an agent others can call. Nothing is published; a caller reaches the
119
+ agent by its peer ID, the one it prints, through a router:
120
+
121
+ <!-- embed: sdk/js/examples/agent.ts -->
122
+ ```ts
123
+ // An agent on the mesh: joins, then answers A2A requests from other members
124
+ // until stopped. It publishes nothing. There is no service name to look up; a
125
+ // caller reaches the agent by its peer ID, through a router, as a2a://agent.
126
+ // The mesh policy decides which members may call; the SDK turns the others
127
+ // away before anything reaches this code.
128
+ //
129
+ // node agent.js # answered by the handler below
130
+ // node agent.js http://127.0.0.1:9999 # forwarded to an A2A server beside it
131
+ //
132
+ // AGENTMESH_CONTROL_PLANE_URL names the mesh. The first run enrolls with the file
133
+ // AGENTMESH_BOOTSTRAP_TOKEN_PATH (a token the mesh operator gave you) or
134
+ // AGENTMESH_JWT_PATH (a workload identity token your platform issues, such as a
135
+ // Kubernetes projected service account token), and keeps the identity and
136
+ // credential in AGENTMESH_STATE_DIR; later runs resume from there, as the same
137
+ // peer, without it.
138
+ import { homedir } from "node:os";
139
+ import { AgentMesh } from "@agentmesh-p2p/sdk";
140
+
141
+ const [backendURL] = process.argv.slice(2);
142
+
143
+ const mesh = await AgentMesh.enroll({
144
+ controlPlaneUrl: process.env.AGENTMESH_CONTROL_PLANE_URL ?? "https://mesh.example.com",
145
+ bootstrapTokenPath: process.env.AGENTMESH_BOOTSTRAP_TOKEN_PATH,
146
+ jwtPath: process.env.AGENTMESH_JWT_PATH,
147
+ stateDir: process.env.AGENTMESH_STATE_DIR ?? `${homedir()}/.config/agentmesh/agent`,
148
+ // A plaintext http:// control plane is otherwise accepted only on loopback.
149
+ allowInsecure: process.env.AGENTMESH_INSECURE_CONTROL_PLANE === "true",
150
+ });
151
+ const session = await mesh.join();
152
+
153
+ // Answers every path with who was asked and who asked; a real agent runs an
154
+ // A2A server here, or beside this process at backendURL.
155
+ const target = await session.acceptA2A(
156
+ backendURL !== undefined
157
+ ? { url: backendURL }
158
+ : { handler: (request, caller) => Response.json({ name: "agent", path: new URL(request.url).pathname, caller: caller.peerId }) },
159
+ );
160
+ console.log(`accepting ${target} as ${session.peerId}`);
161
+
162
+ const stop = () => void session.close().then(() => process.exit(0));
163
+ process.on("SIGINT", stop);
164
+ process.on("SIGTERM", stop);
165
+ ```
166
+ <!-- /embed -->
167
+
168
+ `enroll` reuses the identity and credential saved in `stateDir` when they
169
+ are still valid for that control plane, and needs exactly one of
170
+ `bootstrapTokenPath`, `bootstrapToken`, `jwtPath` or `jwt` (a token string or
171
+ a `() => string | Promise<string>` callback invoked at enrollment and on every
172
+ refresh) otherwise. Read tokens from a file, a callback or the environment; do
173
+ not put them on a command line. `labels` are attested at enrollment;
174
+ `join({ egressRequireLabels })` is the floor every peer the session calls must
175
+ attest, all of it, held for the session.
176
+
177
+ A plaintext `http://` control plane is accepted only on loopback. Pass
178
+ `allowInsecure: true` for a network you trust.
179
+
180
+ ## License
181
+
182
+ Apache-2.0. Issues and contributions at
183
+ [github.com/google/agentmesh](https://github.com/google/agentmesh).
package/dist/auth.d.ts ADDED
@@ -0,0 +1,66 @@
1
+ import type { Connection, StreamHandler } from "@libp2p/interface";
2
+ import { type VerifiedBiscuit } from "./biscuit.ts";
3
+ import { RouterGoAway_Reason, type RouterGoAway } from "./gen/agentmesh_pb.ts";
4
+ export declare const AUTH_PROTOCOL = "/mesh/auth/1.0.0";
5
+ export declare const MCP_PROTOCOL = "/mesh/mcp/1.0.0";
6
+ export declare const GOAWAY_PROTOCOL = "/mesh/goaway/1.0.0";
7
+ /** The first frame on a stream is capped, as msgio.NewVarintReaderSize(s, 64 KiB). */
8
+ export declare const MAX_AUTH_FRAME_BYTES: number;
9
+ /** How long either side waits for the other's frame. */
10
+ export declare const AUTH_HANDSHAKE_TIMEOUT_MS = 10000;
11
+ /** The go-away message on /mesh/goaway/1.0.0 is capped at 1 KiB, with a 5s read timeout. */
12
+ export declare const MAX_GOAWAY_MESSAGE_BYTES = 1024;
13
+ export declare const GOAWAY_STREAM_TIMEOUT_MS = 5000;
14
+ /** Default duration a router is shunned when retry_after is unset or non-positive (5 minutes). */
15
+ export declare const ROUTER_SHUN_DURATION_MS: number;
16
+ /** Maximum duration a router go-away retry_after can shun a router (1 hour). */
17
+ export declare const GOAWAY_MAX_RETRY_AFTER_MS: number;
18
+ export declare class AuthRejectedError extends Error {
19
+ constructor(peerId: string, reason: string);
20
+ }
21
+ /**
22
+ * Client side: presents frame on a new /mesh/auth/1.0.0 stream and returns
23
+ * the peer's verified credential. trustedKeys are the control plane keys.
24
+ */
25
+ export declare function authenticateWithPeer(conn: Connection, frame: Uint8Array, trustedKeys: Uint8Array[]): Promise<VerifiedBiscuit>;
26
+ export interface AuthServerOptions {
27
+ /** This member's current biscuit, read per handshake so a refresh takes effect. */
28
+ ownBiscuit(): Uint8Array;
29
+ trustedKeys(): Uint8Array[];
30
+ /** Peers the control plane has banned; a banned peer's frame gets no answer. */
31
+ isBanned?(peerId: string): boolean;
32
+ /** Called with every peer that passes; the session keeps the admitted set. */
33
+ onAuthenticated?(peerId: string, verified: VerifiedBiscuit): void;
34
+ }
35
+ /** Options for libp2p.handle() so the handshake also runs over relayed connections. */
36
+ export declare const AUTH_HANDLER_OPTIONS: {
37
+ runOnLimitedConnection: boolean;
38
+ };
39
+ /**
40
+ * Server side of /mesh/auth/1.0.0, mirroring agentmesh-node's HandleAuthHandshake:
41
+ * verify the caller's biscuit against the control plane keys and its
42
+ * connection peer ID, then answer with our own. A failed verification gets
43
+ * no answer, only a closed stream, as on the Go side.
44
+ */
45
+ export declare function authStreamHandler(options: AuthServerOptions): StreamHandler;
46
+ /** Returns the canonical proto enum name for a RouterGoAway_Reason value. */
47
+ export declare function goAwayReasonName(reason: RouterGoAway_Reason): string;
48
+ /**
49
+ * Computes the shun duration in milliseconds from a RouterGoAway message:
50
+ * defaults to ROUTER_SHUN_DURATION_MS (5m) when retry_after <= 0, capped at
51
+ * GOAWAY_MAX_RETRY_AFTER_MS (1h).
52
+ */
53
+ export declare function clampGoAwayRetryAfterMs(msg: RouterGoAway): number;
54
+ export interface GoAwayServerOptions {
55
+ /** Whether peerId is currently a router this member holds. */
56
+ isHeldRouter(peerId: string): boolean;
57
+ /** Called once a held router's RouterGoAway message has been read and the stream closed. */
58
+ onGoAway(peerId: string, msg: RouterGoAway, retryAfterMs: number): void | Promise<void>;
59
+ }
60
+ /**
61
+ * Server side of /mesh/goaway/1.0.0, mirroring agentmesh-node's HandleGoAway:
62
+ * reads one varint-prefixed RouterGoAway message (1024 B cap, 5s timeout) only
63
+ * from a router the member currently holds, closes the stream, and notifies
64
+ * the session to shun the router and top up from the catalog.
65
+ */
66
+ export declare function goAwayStreamHandler(options: GoAwayServerOptions): StreamHandler;
package/dist/auth.js ADDED
@@ -0,0 +1,164 @@
1
+ // Copyright 2026 Google LLC
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+ // The /mesh/auth/1.0.0 handshake, both sides. One varint-length-prefixed
15
+ // AuthFrame, one AuthResponse back; go-msgio framing on the Go side is
16
+ // the same unsigned varint prefix lpStream uses.
17
+ import { create, fromBinary, toBinary } from "@bufbuild/protobuf";
18
+ import { lpStream } from "@libp2p/utils";
19
+ import { BiscuitVerificationError, verifyPeerBiscuit } from "./biscuit.js";
20
+ import { decodeAuthResponse } from "./credential.js";
21
+ import { AuthFrameSchema, AuthResponseSchema, RouterGoAwaySchema, RouterGoAway_Reason, } from "./gen/agentmesh_pb.js";
22
+ export const AUTH_PROTOCOL = "/mesh/auth/1.0.0";
23
+ export const MCP_PROTOCOL = "/mesh/mcp/1.0.0";
24
+ export const GOAWAY_PROTOCOL = "/mesh/goaway/1.0.0";
25
+ /** The first frame on a stream is capped, as msgio.NewVarintReaderSize(s, 64 KiB). */
26
+ export const MAX_AUTH_FRAME_BYTES = 64 * 1024;
27
+ /** How long either side waits for the other's frame. */
28
+ export const AUTH_HANDSHAKE_TIMEOUT_MS = 10_000;
29
+ /** The go-away message on /mesh/goaway/1.0.0 is capped at 1 KiB, with a 5s read timeout. */
30
+ export const MAX_GOAWAY_MESSAGE_BYTES = 1024;
31
+ export const GOAWAY_STREAM_TIMEOUT_MS = 5_000;
32
+ /** Default duration a router is shunned when retry_after is unset or non-positive (5 minutes). */
33
+ export const ROUTER_SHUN_DURATION_MS = 5 * 60_000;
34
+ /** Maximum duration a router go-away retry_after can shun a router (1 hour). */
35
+ export const GOAWAY_MAX_RETRY_AFTER_MS = 60 * 60_000;
36
+ export class AuthRejectedError extends Error {
37
+ constructor(peerId, reason) {
38
+ super(`peer ${peerId} rejected the auth handshake: ${reason}`);
39
+ this.name = "AuthRejectedError";
40
+ }
41
+ }
42
+ function framed(stream) {
43
+ return lpStream(stream, { maxDataLength: MAX_AUTH_FRAME_BYTES });
44
+ }
45
+ /**
46
+ * Client side: presents frame on a new /mesh/auth/1.0.0 stream and returns
47
+ * the peer's verified credential. trustedKeys are the control plane keys.
48
+ */
49
+ export async function authenticateWithPeer(conn, frame, trustedKeys) {
50
+ const signal = AbortSignal.timeout(AUTH_HANDSHAKE_TIMEOUT_MS);
51
+ // A peer behind a router is reached over a relayed, limited connection.
52
+ const stream = await conn.newStream(AUTH_PROTOCOL, { signal, runOnLimitedConnection: true });
53
+ try {
54
+ const lp = framed(stream);
55
+ await lp.write(frame, { signal });
56
+ const resp = decodeAuthResponse((await lp.read({ signal })).subarray());
57
+ if (!resp.success) {
58
+ throw new AuthRejectedError(conn.remotePeer.toString(), resp.error || "no reason given");
59
+ }
60
+ if (resp.biscuit.length === 0) {
61
+ throw new AuthRejectedError(conn.remotePeer.toString(), "empty credential in the mutual response");
62
+ }
63
+ // Discovery names whoever answered; only this says the peer is enrolled.
64
+ return await verifyPeerBiscuit(resp.biscuit, conn.remotePeer.toString(), trustedKeys);
65
+ }
66
+ finally {
67
+ await stream.close().catch(() => stream.abort(new Error("auth stream close failed")));
68
+ }
69
+ }
70
+ /** Options for libp2p.handle() so the handshake also runs over relayed connections. */
71
+ export const AUTH_HANDLER_OPTIONS = { runOnLimitedConnection: true };
72
+ /**
73
+ * Server side of /mesh/auth/1.0.0, mirroring agentmesh-node's HandleAuthHandshake:
74
+ * verify the caller's biscuit against the control plane keys and its
75
+ * connection peer ID, then answer with our own. A failed verification gets
76
+ * no answer, only a closed stream, as on the Go side.
77
+ */
78
+ export function authStreamHandler(options) {
79
+ return async (stream, connection) => {
80
+ const peerId = connection.remotePeer.toString();
81
+ const signal = AbortSignal.timeout(AUTH_HANDSHAKE_TIMEOUT_MS);
82
+ try {
83
+ const lp = framed(stream);
84
+ const frame = fromBinary(AuthFrameSchema, (await lp.read({ signal })).subarray());
85
+ if (options.isBanned?.(peerId) === true) {
86
+ stream.log?.("auth handshake from %s refused: peer is revoked", peerId);
87
+ return;
88
+ }
89
+ let verified;
90
+ try {
91
+ verified = await verifyPeerBiscuit(frame.biscuit, peerId, options.trustedKeys());
92
+ }
93
+ catch (err) {
94
+ if (err instanceof BiscuitVerificationError) {
95
+ stream.log?.("auth handshake from %s refused: %s", peerId, err.message);
96
+ return;
97
+ }
98
+ throw err;
99
+ }
100
+ options.onAuthenticated?.(peerId, verified);
101
+ await lp.write(toBinary(AuthResponseSchema, create(AuthResponseSchema, { success: true, biscuit: options.ownBiscuit() })), { signal });
102
+ }
103
+ finally {
104
+ await stream.close().catch(() => stream.abort(new Error("auth stream close failed")));
105
+ }
106
+ };
107
+ }
108
+ /** Returns the canonical proto enum name for a RouterGoAway_Reason value. */
109
+ export function goAwayReasonName(reason) {
110
+ switch (reason) {
111
+ case RouterGoAway_Reason.DRAINING:
112
+ return "DRAINING";
113
+ case RouterGoAway_Reason.OVERLOADED:
114
+ return "OVERLOADED";
115
+ default:
116
+ return "REASON_UNSPECIFIED";
117
+ }
118
+ }
119
+ /**
120
+ * Computes the shun duration in milliseconds from a RouterGoAway message:
121
+ * defaults to ROUTER_SHUN_DURATION_MS (5m) when retry_after <= 0, capped at
122
+ * GOAWAY_MAX_RETRY_AFTER_MS (1h).
123
+ */
124
+ export function clampGoAwayRetryAfterMs(msg) {
125
+ let ms = 0;
126
+ if (msg.retryAfter !== undefined) {
127
+ ms = Number(msg.retryAfter.seconds) * 1000 + Math.floor(msg.retryAfter.nanos / 1_000_000);
128
+ }
129
+ if (ms <= 0) {
130
+ return ROUTER_SHUN_DURATION_MS;
131
+ }
132
+ if (ms > GOAWAY_MAX_RETRY_AFTER_MS) {
133
+ return GOAWAY_MAX_RETRY_AFTER_MS;
134
+ }
135
+ return ms;
136
+ }
137
+ /**
138
+ * Server side of /mesh/goaway/1.0.0, mirroring agentmesh-node's HandleGoAway:
139
+ * reads one varint-prefixed RouterGoAway message (1024 B cap, 5s timeout) only
140
+ * from a router the member currently holds, closes the stream, and notifies
141
+ * the session to shun the router and top up from the catalog.
142
+ */
143
+ export function goAwayStreamHandler(options) {
144
+ return async (stream, connection) => {
145
+ const peerId = connection.remotePeer.toString();
146
+ if (!options.isHeldRouter(peerId)) {
147
+ stream.abort(new Error(`ignoring go-away from unattached peer ${peerId}`));
148
+ return;
149
+ }
150
+ const signal = AbortSignal.timeout(GOAWAY_STREAM_TIMEOUT_MS);
151
+ let msg;
152
+ try {
153
+ const lp = lpStream(stream, { maxDataLength: MAX_GOAWAY_MESSAGE_BYTES });
154
+ msg = fromBinary(RouterGoAwaySchema, (await lp.read({ signal })).subarray());
155
+ }
156
+ catch (err) {
157
+ stream.abort(err instanceof Error ? err : new Error(String(err)));
158
+ return;
159
+ }
160
+ await stream.close().catch(() => stream.abort(new Error("goaway stream close failed")));
161
+ const retryAfterMs = clampGoAwayRetryAfterMs(msg);
162
+ await options.onGoAway(peerId, msg, retryAfterMs);
163
+ };
164
+ }
@@ -0,0 +1,41 @@
1
+ import { type VerifiedBiscuit } from "./biscuit.ts";
2
+ /** What a caller asks for, as agentmesh-node's RequestContext. */
3
+ export interface AuthorizeRequest {
4
+ /** The caller's biscuit, as it arrived in the AuthFrame or X-Mesh-Biscuit. */
5
+ biscuit: Uint8Array;
6
+ /** The peer at the other end of the authenticated connection. */
7
+ peerId: string;
8
+ /** "mcp://<name>", or "" for the protocol itself (the provider's own catalog). */
9
+ targetService: string;
10
+ /** The stream protocol; names the service when targetService is "". */
11
+ protocol: string;
12
+ /**
13
+ * The HTTP method and the path as the backend sees it, when the request is
14
+ * HTTP. Both are injected together; a request without them (a stream that
15
+ * carries no HTTP request) does not match a grant narrowed by
16
+ * PolicyRole.http.
17
+ */
18
+ method?: string;
19
+ path?: string;
20
+ /** The MCP tool name when evaluating a specific MCP tools/call invocation. */
21
+ mcpTool?: string;
22
+ }
23
+ export interface ProviderAuthorizerOptions {
24
+ /** Control plane keys, read per request so a rotation takes effect. */
25
+ trustedKeys(): Uint8Array[];
26
+ /** This provider's own biscuit; its identity facts are what target grants match. */
27
+ ownBiscuit(): Uint8Array;
28
+ /** The mesh policy as PolicyConfigGetResponse.datalog_rules. */
29
+ policyRules(): string[];
30
+ now?(): Date;
31
+ }
32
+ export declare class AuthorizationError extends Error {
33
+ constructor(peerId: string, reason: string);
34
+ }
35
+ /**
36
+ * Authorizes one request from a caller. Returns the caller's verified
37
+ * credential when every check holds and a policy allows; throws
38
+ * AuthorizationError otherwise. Every trusted key is tried, as
39
+ * VerifyBiscuitToken does.
40
+ */
41
+ export declare function authorizeCaller(req: AuthorizeRequest, options: ProviderAuthorizerOptions): Promise<VerifiedBiscuit>;
@@ -0,0 +1,201 @@
1
+ // Copyright 2026 Google LLC
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+ // The provider-side authorizer, mirroring internal/node.(*AgentMeshNode).Authorize.
15
+ // Every piece of Datalog it evaluates is text: the baseline generated from
16
+ // api/datalog.go (gen/datalog.ts) and the mesh policy rules the control
17
+ // plane renders (GET /policies). Nothing here derives rules from roles.
18
+ import { AUTHORIZER_LIMITS, BiscuitVerificationError, loadBiscuit, verifyPeerBiscuit, withinLimits } from "./biscuit.js";
19
+ import { parseServiceTarget } from "./discovery.js";
20
+ import { BASELINE_DATALOG } from "./gen/datalog.js";
21
+ import { evaluateTaskRules, isSafeRequestHTTPPath } from "./tar.js";
22
+ export class AuthorizationError extends Error {
23
+ constructor(peerId, reason) {
24
+ super(`caller ${peerId} is not authorized: ${reason}`);
25
+ this.name = "AuthorizationError";
26
+ }
27
+ }
28
+ // biscuit-wasm throws plain objects ({ FailedLogic: ... }, { RunLimit: ... }).
29
+ function describe(err) {
30
+ if (err instanceof Error) {
31
+ return err.message;
32
+ }
33
+ try {
34
+ return JSON.stringify(err);
35
+ }
36
+ catch {
37
+ return String(err);
38
+ }
39
+ }
40
+ function timeFact(now) {
41
+ return `${BASELINE_DATALOG.fact_time}(${now.toISOString().replace(/\.\d{3}Z$/, "Z")})`;
42
+ }
43
+ /**
44
+ * Authorizes one request from a caller. Returns the caller's verified
45
+ * credential when every check holds and a policy allows; throws
46
+ * AuthorizationError otherwise. Every trusted key is tried, as
47
+ * VerifyBiscuitToken does.
48
+ */
49
+ export async function authorizeCaller(req, options) {
50
+ const wasm = await loadBiscuit();
51
+ const now = options.now?.() ?? new Date();
52
+ const keys = options.trustedKeys();
53
+ if (keys.length === 0) {
54
+ throw new AuthorizationError(req.peerId, "no trusted control plane key");
55
+ }
56
+ // Signature under a trusted key, authority block + tar_block chain, expiry and binding
57
+ // to the connection peer: RequireAuthorityRequestBinding and EnforceExpiration.
58
+ let caller;
59
+ try {
60
+ caller = await verifyPeerBiscuit(req.biscuit, req.peerId, keys, now, { allowDelegated: true });
61
+ }
62
+ catch (err) {
63
+ throw new AuthorizationError(req.peerId, describe(err));
64
+ }
65
+ const token = wasm.Biscuit.fromBytes(req.biscuit, wasm.PublicKey.fromBytes(caller.verifyingKey, wasm.SignatureAlgorithm.Ed25519));
66
+ const b = new wasm.AuthorizerBuilder();
67
+ const fact = (source, params = {}) => {
68
+ const f = wasm.Fact.fromString(source);
69
+ for (const [k, v] of Object.entries(params)) {
70
+ f.set(k, v);
71
+ }
72
+ b.addFact(f);
73
+ };
74
+ // The action: service(type, name). An empty target is the protocol itself
75
+ // in the system namespace, as agentmesh-node scopes its own catalog.
76
+ let svcType;
77
+ let svcName;
78
+ if (req.targetService === "") {
79
+ svcType = BASELINE_DATALOG.system_namespace;
80
+ svcName = req.protocol;
81
+ }
82
+ else {
83
+ let parsed;
84
+ try {
85
+ parsed = parseServiceTarget(req.targetService);
86
+ }
87
+ catch (err) {
88
+ throw new AuthorizationError(req.peerId, describe(err));
89
+ }
90
+ svcType = parsed.type;
91
+ svcName = parsed.name;
92
+ }
93
+ fact(`${BASELINE_DATALOG.fact_service}({t}, {n})`, { t: svcType, n: svcName });
94
+ fact(`${BASELINE_DATALOG.fact_connection_peer_id}({p})`, { p: req.peerId });
95
+ fact(timeFact(now));
96
+ // The request as the wire carried it, never as the caller describes it.
97
+ if (req.method !== undefined) {
98
+ if (req.method !== "CONNECT" && req.path !== undefined && req.path !== "" && !isSafeRequestHTTPPath(req.path)) {
99
+ throw new AuthorizationError(req.peerId, `invalid HTTP path ${JSON.stringify(req.path)}`);
100
+ }
101
+ fact(`${BASELINE_DATALOG.fact_method}({m})`, { m: req.method });
102
+ fact(`${BASELINE_DATALOG.fact_path}({p})`, { p: req.path ?? "" });
103
+ }
104
+ b.addCheck(wasm.Check.fromString(BASELINE_DATALOG.replay_check));
105
+ b.addCheck(wasm.Check.fromString(BASELINE_DATALOG.time_check));
106
+ // This provider's identity as target_fact facts, so the caller's target
107
+ // grants have something to match (injectIdentityFacts).
108
+ for (const f of await identityTargetFacts(options.ownBiscuit(), keys, now)) {
109
+ b.addFact(f);
110
+ }
111
+ b.addCheck(wasm.Check.fromString(BASELINE_DATALOG.target_check));
112
+ for (const p of BASELINE_DATALOG.policies) {
113
+ b.addPolicy(wasm.Policy.fromString(p));
114
+ }
115
+ for (const r of BASELINE_DATALOG.rules) {
116
+ b.addRule(wasm.Rule.fromString(r));
117
+ }
118
+ for (const r of BASELINE_DATALOG.http_rules) {
119
+ b.addRule(wasm.Rule.fromString(r));
120
+ }
121
+ for (const r of options.policyRules()) {
122
+ b.addRule(wasm.Rule.fromString(r));
123
+ }
124
+ const authorizer = b.buildAuthenticated(token);
125
+ try {
126
+ withinLimits(() => authorizer.authorizeWithLimits(AUTHORIZER_LIMITS));
127
+ }
128
+ catch (err) {
129
+ throw new AuthorizationError(req.peerId, describe(err));
130
+ }
131
+ if (caller.taskRules.length > 0) {
132
+ const mcpTool = req.mcpTool ?? "";
133
+ try {
134
+ evaluateTaskRules(caller.taskRules, {
135
+ serviceType: svcType,
136
+ serviceName: svcName,
137
+ hasHttp: req.method !== undefined,
138
+ method: req.method ?? "",
139
+ path: req.path ?? "",
140
+ mcpTool,
141
+ allowMCPStreamInit: req.method === undefined && mcpTool === "" && req.protocol === "/mesh/mcp/1.0.0",
142
+ }, now);
143
+ }
144
+ catch (err) {
145
+ throw new AuthorizationError(req.peerId, describe(err));
146
+ }
147
+ }
148
+ return caller;
149
+ }
150
+ /**
151
+ * Evaluates this provider's own biscuit and returns its identity as
152
+ * target_fact facts, one per target_fact rule of the baseline.
153
+ */
154
+ async function identityTargetFacts(ownBiscuit, keys, now) {
155
+ const wasm = await loadBiscuit();
156
+ if (ownBiscuit.length === 0) {
157
+ return [];
158
+ }
159
+ let own;
160
+ try {
161
+ own = await verifyPeerBiscuit(ownBiscuit, await boundPeer(ownBiscuit, keys), keys, now);
162
+ }
163
+ catch (err) {
164
+ throw new BiscuitVerificationError(`own credential does not verify: ${describe(err)}`, { cause: err });
165
+ }
166
+ const token = wasm.Biscuit.fromBytes(ownBiscuit, wasm.PublicKey.fromBytes(own.verifyingKey, wasm.SignatureAlgorithm.Ed25519));
167
+ const b = new wasm.AuthorizerBuilder();
168
+ b.addFact(wasm.Fact.fromString(timeFact(now)));
169
+ b.addPolicy(wasm.Policy.fromString(BASELINE_DATALOG.allow_if_true));
170
+ const authorizer = b.buildAuthenticated(token);
171
+ withinLimits(() => authorizer.authorizeWithLimits(AUTHORIZER_LIMITS));
172
+ const facts = [];
173
+ for (const r of BASELINE_DATALOG.target_fact_rules) {
174
+ facts.push(...withinLimits(() => authorizer.queryWithLimits(wasm.Rule.fromString(r), AUTHORIZER_LIMITS)));
175
+ }
176
+ return facts;
177
+ }
178
+ /** The peer a biscuit binds itself to: its node() fact. */
179
+ async function boundPeer(biscuit, keys) {
180
+ const wasm = await loadBiscuit();
181
+ let lastErr;
182
+ for (const key of keys) {
183
+ try {
184
+ const token = wasm.Biscuit.fromBytes(biscuit, wasm.PublicKey.fromBytes(key, wasm.SignatureAlgorithm.Ed25519));
185
+ const b = new wasm.AuthorizerBuilder();
186
+ b.addPolicy(wasm.Policy.fromString(BASELINE_DATALOG.allow_if_true));
187
+ const authorizer = b.buildAuthenticated(token);
188
+ const bound = withinLimits(() => authorizer.queryWithLimits(wasm.Rule.fromString("p($p) <- node($p)"), AUTHORIZER_LIMITS))
189
+ .map((f) => f.terms()[0])
190
+ .find((t) => typeof t === "string");
191
+ if (bound === undefined) {
192
+ throw new Error("biscuit carries no node() fact");
193
+ }
194
+ return bound;
195
+ }
196
+ catch (err) {
197
+ lastErr = err;
198
+ }
199
+ }
200
+ throw new BiscuitVerificationError(`biscuit is not signed by a trusted control plane key: ${describe(lastErr)}`);
201
+ }
@@ -0,0 +1,2 @@
1
+ export declare function encodeBase58(bytes: Uint8Array): string;
2
+ export declare function decodeBase58(text: string): Uint8Array;