@nylorun/agents 0.9.0-beta → 0.10.0-beta

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 (52) hide show
  1. package/CHANGELOG.md +131 -0
  2. package/README.md +287 -69
  3. package/dist/a2a/handler.d.ts +59 -0
  4. package/dist/a2a/handler.js +178 -0
  5. package/dist/a2a/index.d.ts +8 -0
  6. package/dist/a2a/index.js +7 -0
  7. package/dist/access.d.ts +101 -0
  8. package/dist/access.js +111 -0
  9. package/dist/action-handler.d.ts +64 -0
  10. package/dist/action-handler.js +283 -0
  11. package/dist/ag-ui/handler.d.ts +12 -4
  12. package/dist/ag-ui/handler.js +87 -281
  13. package/dist/ag-ui/index.d.ts +2 -1
  14. package/dist/ag-ui/index.js +2 -1
  15. package/dist/browser.d.ts +97 -0
  16. package/dist/browser.js +277 -0
  17. package/dist/client.d.ts +3 -230
  18. package/dist/client.js +2 -357
  19. package/dist/connection.d.ts +0 -1
  20. package/dist/connection.js +3 -9
  21. package/dist/delivery-token.d.ts +57 -0
  22. package/dist/delivery-token.js +150 -0
  23. package/dist/execute-action.d.ts +6 -0
  24. package/dist/execute-action.js +24 -0
  25. package/dist/http.d.ts +31 -1
  26. package/dist/http.js +74 -14
  27. package/dist/index.d.ts +9 -7
  28. package/dist/index.js +3 -3
  29. package/dist/sandbox/client.d.ts +4 -5
  30. package/dist/sandbox/client.js +4 -8
  31. package/dist/sandbox/index.d.ts +0 -2
  32. package/dist/sandbox/index.js +0 -1
  33. package/dist/served-definitions.d.ts +20 -0
  34. package/dist/served-definitions.js +100 -0
  35. package/dist/session-client.d.ts +282 -0
  36. package/dist/session-client.js +375 -0
  37. package/dist/sse.js +12 -0
  38. package/dist/studio-embed.d.ts +9 -0
  39. package/dist/studio-embed.js +8 -0
  40. package/dist/token-endpoint.d.ts +23 -0
  41. package/dist/token-endpoint.js +30 -0
  42. package/package.json +15 -8
  43. package/dist/ag-ui/history.d.ts +0 -7
  44. package/dist/ag-ui/history.js +0 -60
  45. package/dist/ag-ui/sse.d.ts +0 -9
  46. package/dist/ag-ui/sse.js +0 -7
  47. package/dist/ag-ui/translate.d.ts +0 -38
  48. package/dist/ag-ui/translate.js +0 -222
  49. package/dist/derived-credentials.d.ts +0 -5
  50. package/dist/derived-credentials.js +0 -17
  51. package/dist/executor.d.ts +0 -20
  52. package/dist/executor.js +0 -417
package/dist/client.js CHANGED
@@ -1,360 +1,6 @@
1
- import { SANDBOX_TOOL_NAMES, delegateOf, isBuiltWorkflow, isSandboxToolName, } from "@nylorun/core/define";
2
- import { SCOPES_HEADER, SUBJECT_HEADER } from "@nylorun/core/compatibility";
3
- import { LiveEventSchema, SessionItemsResponseSchema, parseSubjectHeaders, } from "@nylorun/core/contracts";
1
+ export * from "./session-client.js";
2
+ import { AgentsClient } from "./session-client.js";
4
3
  import { resolveConnection } from "./connection.js";
5
- import { Transport, id, segment } from "./http.js";
6
- import { observeSSE } from "./sse.js";
7
- function sessionSandboxOf(transport, sessionId) {
8
- const call = (tool, args) => transport.json(`/v1/sessions/${segment(sessionId)}/sandbox/${segment(tool)}`, "POST", args);
9
- const sandbox = {};
10
- for (const tool of SANDBOX_TOOL_NAMES) {
11
- if (!isSandboxToolName(tool))
12
- continue;
13
- sandbox[tool] = (args) => call(tool, args);
14
- }
15
- return sandbox;
16
- }
17
- function linkedSessionId(event) {
18
- if (event.type !== "node.agent")
19
- return undefined;
20
- const payload = event.payload;
21
- if (!payload || typeof payload !== "object" || Array.isArray(payload))
22
- return undefined;
23
- const sessionId = payload.sessionId;
24
- return typeof sessionId === "string" && sessionId.length > 0
25
- ? sessionId
26
- : undefined;
27
- }
28
- const middlewareClosure = "Hosted definitions support declarative capabilities and before/after hooks, not middleware closures";
29
- /** The agent's declarations, then those of each agent it uses as a tool, keyed `<child>/<capability>`. */
30
- function declarationsOf(agent) {
31
- if (isBuiltWorkflow(agent) || agent.manifest.kind === "workflow")
32
- return [];
33
- const built = agent.build?.() ?? agent;
34
- if (isBuiltWorkflow(built))
35
- return [];
36
- const binding = built.getBinding?.();
37
- if (!binding || !("declarations" in binding))
38
- return [];
39
- const found = (binding.declarations ?? []).map((item) => ({ key: item.id, item }));
40
- for (const tool of binding.tools ?? []) {
41
- const delegate = delegateOf(tool);
42
- const child = delegate?.agent;
43
- for (const item of child?.getBinding().declarations ?? [])
44
- found.push({ key: `${child.id}/${item.id}`, item });
45
- // A flow agent used as a tool: its agents' declarations, keyed `<flow>/<agent>/<capability>`.
46
- const flow = delegate?.workflow;
47
- for (const leaf of Object.values(flow?.getBinding().agents ?? {}))
48
- for (const item of leaf.declarations ?? [])
49
- found.push({ key: `${flow.id}/${leaf.manifest.id}/${item.id}`, item });
50
- }
51
- return found;
52
- }
53
- function pluginRootsOf(agent) {
54
- const roots = {};
55
- for (const { key, item } of declarationsOf(agent))
56
- if (item.pluginRoot)
57
- roots[key] = item.pluginRoot;
58
- return Object.keys(roots).length === 0 ? undefined : roots;
59
- }
60
- /** Closures stay on the live binding. The published manifest cannot name them. */
61
- export function assertNoMiddlewareClosures(agent) {
62
- if (declarationsOf(agent).some(({ item }) => item.hasMiddleware))
63
- throw new Error(middlewareClosure);
64
- }
65
- export class AgentsClient {
66
- transport;
67
- /** The subject this client acts for (`as`), or `undefined` for the principal itself. */
68
- subject;
69
- constructor(destination = {}) {
70
- this.transport = new Transport(destination);
71
- this.subject = undefined;
72
- }
73
- /**
74
- * A client that acts for `subject`: every call sends `Nylorun-Subject` and
75
- * `Nylorun-Scopes`, and the Runtime limits it to the scopes and to the subject's own sessions
76
- * and vaults (Host feature `subject-headers`). For app servers, which authenticate the person
77
- * themselves and hold the key; nothing is minted, cached or refreshed.
78
- */
79
- as(subject, options = {}) {
80
- if (this.subject !== undefined)
81
- throw new Error(`This client already acts for ${this.subject}`);
82
- const scopes = [...(options.scopes ?? ["sessions:own"])].join(" ");
83
- const parsed = parseSubjectHeaders(subject, scopes);
84
- if (!parsed.ok)
85
- throw new TypeError(parsed.message);
86
- const client = Object.create(AgentsClient.prototype);
87
- return Object.assign(client, {
88
- transport: this.transport.withHeaders({
89
- [SUBJECT_HEADER]: subject,
90
- [SCOPES_HEADER]: [...parsed.scopes].join(" "),
91
- }),
92
- subject,
93
- });
94
- }
95
- /** The Runtime's protocol features, including optional ones such as `transcript-events`. */
96
- hostFeatures(options = {}) {
97
- return this.transport.hostFeatures(options.signal);
98
- }
99
- listAgents(options = {}) {
100
- return this.transport.json("/v1/agents", "GET", undefined, options.signal);
101
- }
102
- listSessions(options = {}) {
103
- const query = options.agentId
104
- ? `?agentId=${encodeURIComponent(options.agentId)}`
105
- : "";
106
- return this.transport.json(`/v1/sessions${query}`, "GET", undefined, options.signal);
107
- }
108
- async saveAgent(agent, options) {
109
- if (isBuiltWorkflow(agent) && agent.manifest.workflowSchemaVersion === 2) {
110
- // A v2 workflow embeds its agents: one document, carrying their plugin roots.
111
- const pluginRoots = {};
112
- for (const binding of Object.values(agent.getBinding().agents)) {
113
- const leaf = { id: binding.manifest.id, manifest: binding.manifest, getBinding: () => binding };
114
- assertNoMiddlewareClosures(leaf);
115
- for (const [key, root] of Object.entries(pluginRootsOf(leaf) ?? {}))
116
- pluginRoots[`${leaf.id}/${key}`] = root;
117
- }
118
- return this.transport.json(`/v1/agents/${segment(agent.id)}`, "PUT", {
119
- requestId: options.requestId ?? id(),
120
- manifest: agent.manifest,
121
- implementationVersion: options.implementationVersion,
122
- ...(Object.keys(pluginRoots).length === 0 ? {} : { pluginRoots }),
123
- });
124
- }
125
- if (isBuiltWorkflow(agent)) {
126
- for (const binding of Object.values(agent.getBinding().agents)) {
127
- await this.saveAgent({
128
- id: binding.manifest.id,
129
- manifest: binding.manifest,
130
- getBinding: () => binding,
131
- }, options);
132
- }
133
- return this.transport.json(`/v1/agents/${segment(agent.id)}`, "PUT", {
134
- requestId: options.requestId ?? id(),
135
- manifest: agent.manifest,
136
- implementationVersion: options.implementationVersion,
137
- });
138
- }
139
- assertNoMiddlewareClosures(agent);
140
- const pluginRoots = pluginRootsOf(agent);
141
- return this.transport.json(`/v1/agents/${segment(agent.id)}`, "PUT", {
142
- requestId: options.requestId ?? id(),
143
- manifest: agent.manifest,
144
- implementationVersion: options.implementationVersion,
145
- ...(pluginRoots === undefined ? {} : { pluginRoots }),
146
- });
147
- }
148
- async createSession(options) {
149
- const sessionId = options.id ?? id();
150
- await this.transport.json(`/v1/sessions/${segment(sessionId)}`, "PUT", {
151
- requestId: options.requestId ?? id(),
152
- agentId: options.agentId,
153
- ownerUserId: options.ownerUserId,
154
- ...(options.info ? { info: options.info } : {}),
155
- ...(options.vaultIds ? { vaultIds: options.vaultIds } : {}),
156
- ...(options.credentialSelections
157
- ? { credentialSelections: options.credentialSelections }
158
- : {}),
159
- ...(options.sandbox ? { sandbox: options.sandbox } : {}),
160
- });
161
- return this.session(sessionId);
162
- }
163
- createVault(options) {
164
- return this.transport.json("/v1/vaults", "POST", {
165
- requestId: options.requestId ?? id(),
166
- idempotencyKey: options.idempotencyKey,
167
- name: options.name,
168
- ownerUserId: options.ownerUserId,
169
- ...(options.metadata ? { metadata: options.metadata } : {}),
170
- });
171
- }
172
- listVaults(ownerUserId, signal) {
173
- return this.transport.json(`/v1/vaults?ownerUserId=${encodeURIComponent(ownerUserId)}`, "GET", undefined, signal);
174
- }
175
- getVault(vaultId, signal) {
176
- return this.transport.json(`/v1/vaults/${segment(vaultId)}`, "GET", undefined, signal);
177
- }
178
- deleteVault(vaultId) {
179
- return this.transport.json(`/v1/vaults/${segment(vaultId)}`, "DELETE");
180
- }
181
- createCredential(vaultId, options) {
182
- return this.transport.json(`/v1/vaults/${segment(vaultId)}/credentials`, "POST", {
183
- requestId: options.requestId ?? id(),
184
- idempotencyKey: options.idempotencyKey,
185
- name: options.name,
186
- auth: options.auth,
187
- });
188
- }
189
- listCredentials(vaultId, signal) {
190
- return this.transport.json(`/v1/vaults/${segment(vaultId)}/credentials`, "GET", undefined, signal);
191
- }
192
- getCredential(vaultId, credentialId, signal) {
193
- return this.transport.json(`/v1/vaults/${segment(vaultId)}/credentials/${segment(credentialId)}`, "GET", undefined, signal);
194
- }
195
- rotateCredential(vaultId, credentialId, options) {
196
- return this.transport.json(`/v1/vaults/${segment(vaultId)}/credentials/${segment(credentialId)}`, "POST", {
197
- requestId: options.requestId ?? id(),
198
- idempotencyKey: options.idempotencyKey,
199
- auth: options.auth,
200
- });
201
- }
202
- deleteCredential(vaultId, credentialId) {
203
- return this.transport.json(`/v1/vaults/${segment(vaultId)}/credentials/${segment(credentialId)}`, "DELETE");
204
- }
205
- session(sessionId) {
206
- return new SessionClient(this.transport, sessionId);
207
- }
208
- }
209
- export class SessionClient {
210
- transport;
211
- id;
212
- sandbox;
213
- constructor(transport, id) {
214
- this.transport = transport;
215
- this.id = id;
216
- this.sandbox = sessionSandboxOf(transport, id);
217
- }
218
- get path() {
219
- return `/v1/sessions/${segment(this.id)}`;
220
- }
221
- inspect(signal) {
222
- return this.transport.json(this.path, "GET", undefined, signal);
223
- }
224
- /** Pending waits / interactions from the session inspect view. */
225
- async pending(signal) {
226
- const view = await this.inspect(signal);
227
- return view.waits ?? [];
228
- }
229
- command(command, signal) {
230
- return this.transport.json(`${this.path}/commands`, "POST", command, signal);
231
- }
232
- /**
233
- * Start a turn. Strings become `message.content`; any other JSON value becomes
234
- * `message.data` (workflows.md §5).
235
- */
236
- input(value, options) {
237
- const base = {
238
- type: "message",
239
- requestId: options.requestId ?? id(),
240
- idempotencyKey: options.idempotencyKey,
241
- };
242
- return this.command(typeof value === "string"
243
- ? { ...base, content: value }
244
- : { ...base, data: value }, options.signal);
245
- }
246
- approve(interactionId, approved, options) {
247
- return this.command({
248
- type: "approve",
249
- interactionId,
250
- approved,
251
- requestId: options.requestId ?? id(),
252
- idempotencyKey: options.idempotencyKey,
253
- }, options.signal);
254
- }
255
- respond(interactionId, value, options) {
256
- return this.command({
257
- type: "respond",
258
- interactionId,
259
- value,
260
- requestId: options.requestId ?? id(),
261
- idempotencyKey: options.idempotencyKey,
262
- }, options.signal);
263
- }
264
- cancel(options) {
265
- return this.command({
266
- type: "cancel",
267
- requestId: options.requestId ?? id(),
268
- idempotencyKey: options.idempotencyKey,
269
- ...(options.reason ? { reason: options.reason } : {}),
270
- }, options.signal);
271
- }
272
- /** `agent` narrows to one agent used as a tool: its delegationId or its path. */
273
- async history(options = {}) {
274
- const query = new URLSearchParams();
275
- if (options.cursor)
276
- query.set("cursor", options.cursor);
277
- if (options.agent)
278
- query.set("agent", options.agent);
279
- const search = query.toString();
280
- return SessionItemsResponseSchema.parse(await this.transport.json(`${this.path}/items${search ? `?${search}` : ""}`, "GET", undefined, options.signal));
281
- }
282
- async *observe(options = {}) {
283
- if (!options.follow) {
284
- yield* this.observeOne(this.id, options);
285
- return;
286
- }
287
- yield* this.observeFollowing(options);
288
- }
289
- async *observeOne(sessionId, options) {
290
- for await (const frame of observeSSE(this.transport, `/v1/sessions/${segment(sessionId)}/events`, options)) {
291
- if (frame.event === "heartbeat" || frame.event === "ready")
292
- continue;
293
- yield LiveEventSchema.parse(JSON.parse(frame.data));
294
- }
295
- }
296
- /**
297
- * Own session stream plus linked agent sessions announced in `node.agent`
298
- * events, merged by `createdAt` then arrival order (workflows.md §11).
299
- */
300
- async *observeFollowing(options) {
301
- const signal = options.signal;
302
- const linked = new Set();
303
- const queue = [];
304
- let wait;
305
- let open = 1;
306
- let failed;
307
- const wake = () => {
308
- wait?.();
309
- wait = undefined;
310
- };
311
- const push = (event) => {
312
- queue.push(event);
313
- queue.sort((a, b) => {
314
- const byTime = a.createdAt.localeCompare(b.createdAt);
315
- return byTime !== 0 ? byTime : a.cursor.localeCompare(b.cursor);
316
- });
317
- wake();
318
- };
319
- const pump = async (sessionId, cursor) => {
320
- try {
321
- for await (const event of this.observeOne(sessionId, { cursor, signal })) {
322
- push(event);
323
- const link = linkedSessionId(event);
324
- if (link && link !== this.id && !linked.has(link)) {
325
- linked.add(link);
326
- open += 1;
327
- void pump(link).finally(() => {
328
- open -= 1;
329
- wake();
330
- });
331
- }
332
- }
333
- }
334
- catch (error) {
335
- if (!signal?.aborted)
336
- failed = error;
337
- }
338
- };
339
- void pump(this.id, options.cursor).finally(() => {
340
- open -= 1;
341
- wake();
342
- });
343
- while (!signal?.aborted) {
344
- if (failed)
345
- throw failed;
346
- if (queue.length) {
347
- yield queue.shift();
348
- continue;
349
- }
350
- if (open <= 0)
351
- return;
352
- await new Promise((resolve) => {
353
- wait = resolve;
354
- });
355
- }
356
- }
357
- }
358
4
  export function createClient(destination = {}) {
359
5
  if (destination.url !== undefined ||
360
6
  destination.key !== undefined ||
@@ -368,4 +14,3 @@ export function createClient(destination = {}) {
368
14
  ...(destination.fetch ? { fetch: destination.fetch } : {}),
369
15
  }));
370
16
  }
371
- export { RuntimeError, IncompatibleRuntimeError } from "./http.js";
@@ -3,7 +3,6 @@ export interface ResolvedConnection {
3
3
  url: string;
4
4
  tenant: string;
5
5
  key: string;
6
- role: "application" | "executor";
7
6
  source: "options" | "environment" | "project-link";
8
7
  }
9
8
  export declare class ConnectionError extends Error {
@@ -42,7 +42,6 @@ async function readProjectLink(startDir) {
42
42
  url: stripTrailingSlash(link.hostUrl),
43
43
  tenant: link.tenantId,
44
44
  key: credentials.applicationKey,
45
- role: "application",
46
45
  source: "project-link",
47
46
  };
48
47
  }
@@ -63,7 +62,6 @@ export async function resolveConnection(options) {
63
62
  url: stripTrailingSlash(optionUrl),
64
63
  tenant: optionTenant,
65
64
  key: optionKey,
66
- role: "application",
67
65
  source: "options",
68
66
  };
69
67
  }
@@ -72,20 +70,16 @@ export async function resolveConnection(options) {
72
70
  const envUrl = env("NYLORUN_RUNTIME_URL");
73
71
  const envTenant = env("NYLORUN_TENANT");
74
72
  const envServerKey = env("NYLORUN_SERVER_KEY");
75
- const envExecutorKey = env("NYLORUN_EXECUTOR_KEY");
76
73
  const anyEnv = envUrl !== undefined ||
77
74
  envTenant !== undefined ||
78
- envServerKey !== undefined ||
79
- envExecutorKey !== undefined;
75
+ envServerKey !== undefined;
80
76
  tried.push("environment");
81
77
  if (anyEnv) {
82
- const key = envExecutorKey ?? envServerKey;
83
- if (envUrl && envTenant && key) {
78
+ if (envUrl && envTenant && envServerKey) {
84
79
  return {
85
80
  url: stripTrailingSlash(envUrl),
86
81
  tenant: envTenant,
87
- key,
88
- role: envExecutorKey !== undefined ? "executor" : "application",
82
+ key: envServerKey,
89
83
  source: "environment",
90
84
  };
91
85
  }
@@ -0,0 +1,57 @@
1
+ /** The claims of a verified delivery token. */
2
+ export interface DeliveryClaims {
3
+ readonly iss: string;
4
+ readonly aud: string;
5
+ /** The Action id, or `ping`. */
6
+ readonly sub: string;
7
+ /** The agent or workflow the Action belongs to. */
8
+ readonly agt: string;
9
+ /** The delivery's generation; `0` for a ping. */
10
+ readonly gen: number;
11
+ /** base64url SHA-256 of the request body. */
12
+ readonly bdy: string;
13
+ readonly iat: number;
14
+ readonly exp: number;
15
+ readonly jti: string;
16
+ }
17
+ /** Why a delivery was refused; `code` is sent back to the Runtime. */
18
+ export declare class DeliveryVerificationError extends Error {
19
+ readonly code: "signature_missing" | "signature_invalid" | "key_unknown" | "token_expired";
20
+ readonly name = "DeliveryVerificationError";
21
+ constructor(code: "signature_missing" | "signature_invalid" | "key_unknown" | "token_expired", message: string);
22
+ }
23
+ /** Finds the public key for a `kid`, or `undefined` when the Tenant has none. */
24
+ export type KeyLookup = (kid: string) => Promise<CryptoKey | undefined>;
25
+ export declare function verifyDeliveryToken(token: string | null, options: {
26
+ keys: KeyLookup;
27
+ tenantId: string;
28
+ body: Uint8Array;
29
+ /** When set, `aud` must equal it: the URL this endpoint was registered with. */
30
+ audience?: string;
31
+ now?: number;
32
+ }): Promise<DeliveryClaims>;
33
+ /** base64url SHA-256 of `body`: the token's `bdy` claim. */
34
+ export declare function bodyHash(body: Uint8Array): Promise<string>;
35
+ /**
36
+ * The Tenant's public keys, fetched from `GET /v1/access/jwks` and cached by `kid`. An
37
+ * unknown `kid` refetches once, at most every `refetchMs`, so forged tokens cannot make the
38
+ * endpoint call the Runtime on every request. Concurrent lookups share one in-flight fetch.
39
+ */
40
+ export declare class JwksCache {
41
+ private readonly source;
42
+ private readonly refetchMs;
43
+ private readonly keys;
44
+ private lastFetch;
45
+ private pending;
46
+ constructor(source: {
47
+ url: string;
48
+ tenant: string;
49
+ key?: string;
50
+ fetch?: typeof fetch;
51
+ } | {
52
+ keys: readonly JsonWebKey[];
53
+ }, refetchMs?: number);
54
+ readonly lookup: KeyLookup;
55
+ private load;
56
+ private fetchKeys;
57
+ }
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Verifies the delivery token on a request the Runtime sends to an Action endpoint: an ES256
3
+ * JWT signed with the Tenant's current signing key (design: Action endpoints §8.1). WebCrypto
4
+ * only, so the SDK takes no JWT dependency: a JWS ES256 signature is the raw `r || s` pair
5
+ * WebCrypto's ECDSA verifies.
6
+ */
7
+ import { PROTOCOL_HEADER, PROTOCOL_VERSION, TENANT_HEADER, } from "@nylorun/core/compatibility";
8
+ import { DELIVERY_TOKEN_TYPE, subjectTokenIssuer } from "@nylorun/core/contracts";
9
+ /** Why a delivery was refused; `code` is sent back to the Runtime. */
10
+ export class DeliveryVerificationError extends Error {
11
+ code;
12
+ name = "DeliveryVerificationError";
13
+ constructor(code, message) {
14
+ super(message);
15
+ this.code = code;
16
+ }
17
+ }
18
+ /** Clock skew tolerated on `iat` and `exp`, in seconds. */
19
+ const SKEW_SECONDS = 30;
20
+ const FORBIDDEN_HEADERS = ["jku", "jwk", "x5u", "x5c", "x5t", "x5t#S256", "crit"];
21
+ /** The largest token accepted: well above a real one, well below a request limit. */
22
+ const MAX_TOKEN_LENGTH = 4096;
23
+ export async function verifyDeliveryToken(token, options) {
24
+ if (!token)
25
+ throw new DeliveryVerificationError("signature_missing", "No delivery token");
26
+ const parts = token.length <= MAX_TOKEN_LENGTH ? token.split(".") : [];
27
+ if (parts.length !== 3)
28
+ throw invalid("Not a compact JWS");
29
+ const [encodedHeader, encodedPayload, encodedSignature] = parts;
30
+ const header = json(encodedHeader);
31
+ if (header.alg !== "ES256" ||
32
+ header.typ !== DELIVERY_TOKEN_TYPE ||
33
+ typeof header.kid !== "string" ||
34
+ FORBIDDEN_HEADERS.some((name) => name in header))
35
+ throw invalid("Unexpected token header");
36
+ const key = await options.keys(header.kid);
37
+ if (!key)
38
+ throw new DeliveryVerificationError("key_unknown", `Unknown signing key ${header.kid}`);
39
+ const signed = await crypto.subtle.verify({ name: "ECDSA", hash: "SHA-256" }, key, bytes(base64url(encodedSignature)), new TextEncoder().encode(`${encodedHeader}.${encodedPayload}`));
40
+ if (!signed)
41
+ throw invalid("Bad signature");
42
+ const claims = json(encodedPayload);
43
+ if (typeof claims.iss !== "string" ||
44
+ typeof claims.aud !== "string" ||
45
+ typeof claims.sub !== "string" ||
46
+ typeof claims.agt !== "string" ||
47
+ !Number.isInteger(claims.gen) ||
48
+ typeof claims.bdy !== "string" ||
49
+ typeof claims.iat !== "number" ||
50
+ typeof claims.exp !== "number" ||
51
+ typeof claims.jti !== "string")
52
+ throw invalid("Missing claims");
53
+ if (claims.iss !== subjectTokenIssuer(options.tenantId))
54
+ throw invalid("The token is for another Tenant");
55
+ if (options.audience !== undefined && claims.aud !== options.audience)
56
+ throw invalid(`The token is for ${claims.aud}, not ${options.audience}`);
57
+ const now = (options.now ?? Date.now()) / 1000;
58
+ if (claims.exp < now - SKEW_SECONDS || claims.iat > now + SKEW_SECONDS)
59
+ throw new DeliveryVerificationError("token_expired", "The delivery token has expired");
60
+ if (claims.bdy !== (await bodyHash(options.body)))
61
+ throw invalid("The body does not match the token");
62
+ return claims;
63
+ }
64
+ /** base64url SHA-256 of `body`: the token's `bdy` claim. */
65
+ export async function bodyHash(body) {
66
+ const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes(body)));
67
+ return Buffer.from(digest).toString("base64url");
68
+ }
69
+ /**
70
+ * The Tenant's public keys, fetched from `GET /v1/access/jwks` and cached by `kid`. An
71
+ * unknown `kid` refetches once, at most every `refetchMs`, so forged tokens cannot make the
72
+ * endpoint call the Runtime on every request. Concurrent lookups share one in-flight fetch.
73
+ */
74
+ export class JwksCache {
75
+ source;
76
+ refetchMs;
77
+ keys = new Map();
78
+ lastFetch = 0;
79
+ pending;
80
+ constructor(source, refetchMs = 5000) {
81
+ this.source = source;
82
+ this.refetchMs = refetchMs;
83
+ }
84
+ lookup = async (kid) => {
85
+ const known = this.keys.get(kid);
86
+ if (known)
87
+ return known;
88
+ // Wait for an in-flight fetch: stamping the cooldown at the start of `load` used
89
+ // to make a concurrent ping (register + smoke, or tsx watch restart) answer
90
+ // `key_unknown` before the keys arrived.
91
+ if (this.pending) {
92
+ await this.pending;
93
+ return this.keys.get(kid);
94
+ }
95
+ if (Date.now() - this.lastFetch < this.refetchMs && this.lastFetch > 0)
96
+ return undefined;
97
+ await (this.pending ??= this.load().finally(() => (this.pending = undefined)));
98
+ return this.keys.get(kid);
99
+ };
100
+ async load() {
101
+ const keys = "keys" in this.source ? this.source.keys : await this.fetchKeys(this.source);
102
+ this.lastFetch = Date.now();
103
+ for (const jwk of keys) {
104
+ const kid = jwk.kid;
105
+ if (typeof kid !== "string" || jwk.kty !== "EC" || jwk.crv !== "P-256")
106
+ continue;
107
+ if (this.keys.has(kid))
108
+ continue;
109
+ this.keys.set(kid, await crypto.subtle.importKey("jwk", { kty: jwk.kty, crv: jwk.crv, x: jwk.x, y: jwk.y }, { name: "ECDSA", namedCurve: "P-256" }, false, ["verify"]));
110
+ }
111
+ }
112
+ async fetchKeys(source) {
113
+ const headers = {
114
+ [TENANT_HEADER]: source.tenant,
115
+ [PROTOCOL_HEADER]: String(PROTOCOL_VERSION),
116
+ };
117
+ if (source.key)
118
+ headers.Authorization = `Bearer ${source.key}`;
119
+ const response = await (source.fetch ?? globalThis.fetch)(`${source.url.replace(/\/$/, "")}/v1/access/jwks`, { headers, redirect: "error", signal: AbortSignal.timeout(10_000) });
120
+ if (!response.ok) {
121
+ await response.body?.cancel();
122
+ throw new Error(`The Runtime answered ${response.status} for its public keys (GET /v1/access/jwks)`);
123
+ }
124
+ const body = (await response.json());
125
+ return Array.isArray(body.keys) ? body.keys : [];
126
+ }
127
+ }
128
+ function invalid(message) {
129
+ return new DeliveryVerificationError("signature_invalid", message);
130
+ }
131
+ function base64url(value) {
132
+ return new Uint8Array(Buffer.from(value, "base64url"));
133
+ }
134
+ /** A copy backed by a plain `ArrayBuffer`, which WebCrypto's types require. */
135
+ function bytes(value) {
136
+ const copy = new Uint8Array(value.byteLength);
137
+ copy.set(value);
138
+ return copy;
139
+ }
140
+ function json(segment) {
141
+ try {
142
+ const value = JSON.parse(Buffer.from(segment, "base64url").toString("utf8"));
143
+ if (value && typeof value === "object" && !Array.isArray(value))
144
+ return value;
145
+ }
146
+ catch {
147
+ /* fall through */
148
+ }
149
+ throw invalid("Malformed token");
150
+ }
@@ -8,3 +8,9 @@ export type ExecuteActionOptions = {
8
8
  };
9
9
  /** Executes code only after a host-issued claim. No engine dependency. */
10
10
  export declare function executeAction(action: Action, root: ExecutableDefinition, signal: AbortSignal, options?: ExecuteActionOptions): Promise<ActionOutcome>;
11
+ /**
12
+ * True when `action` runs a tool marked `background` (`ToolDefinition.background`): an Action
13
+ * endpoint answers its delivery at once and posts the outcome later. False for anything it
14
+ * cannot find; `executeAction` reports those.
15
+ */
16
+ export declare function runsInBackground(action: Action, root: ExecutableDefinition): boolean;
@@ -42,6 +42,30 @@ export async function executeAction(action, root, signal, options = {}) {
42
42
  const tool = normalizeToolDefinition(raw);
43
43
  return runTool(action, tool, ref, signal, sandbox);
44
44
  }
45
+ /**
46
+ * True when `action` runs a tool marked `background` (`ToolDefinition.background`): an Action
47
+ * endpoint answers its delivery at once and posts the outcome later. False for anything it
48
+ * cannot find; `executeAction` reports those.
49
+ */
50
+ export function runsInBackground(action, root) {
51
+ if (action.kind !== "tool")
52
+ return false;
53
+ try {
54
+ if (isBuiltWorkflow(root)) {
55
+ const key = action.key;
56
+ const impl = typeof key === "string" ? root.getBinding().nodes[key] : undefined;
57
+ return impl?.kind === "tool" && impl.tool.background === true;
58
+ }
59
+ if (!("capabilityId" in action))
60
+ return false;
61
+ const agent = action.agent ? delegatedAgent(root, action.agent.id) : root;
62
+ const raw = implementationsFor(agent)[action.capabilityId]?.tools?.[action.toolName];
63
+ return raw !== undefined && normalizeToolDefinition(raw).background === true;
64
+ }
65
+ catch {
66
+ return false;
67
+ }
68
+ }
45
69
  async function executeWorkflowTool(action, binding, signal, sandbox) {
46
70
  const impl = binding.nodes[action.key];
47
71
  if (!impl || impl.kind !== "tool")