@nylorun/agents 0.8.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.
- package/CHANGELOG.md +167 -0
- package/README.md +371 -166
- package/dist/a2a/handler.d.ts +59 -0
- package/dist/a2a/handler.js +178 -0
- package/dist/a2a/index.d.ts +8 -0
- package/dist/a2a/index.js +7 -0
- package/dist/access.d.ts +101 -0
- package/dist/access.js +111 -0
- package/dist/action-handler.d.ts +64 -0
- package/dist/action-handler.js +283 -0
- package/dist/ag-ui/handler.d.ts +12 -4
- package/dist/ag-ui/handler.js +87 -281
- package/dist/ag-ui/index.d.ts +2 -1
- package/dist/ag-ui/index.js +2 -1
- package/dist/browser.d.ts +97 -0
- package/dist/browser.js +277 -0
- package/dist/builder.d.ts +20 -0
- package/dist/builder.js +26 -0
- package/dist/client.d.ts +3 -230
- package/dist/client.js +2 -335
- package/dist/connection.d.ts +0 -1
- package/dist/connection.js +3 -9
- package/dist/delivery-token.d.ts +57 -0
- package/dist/delivery-token.js +150 -0
- package/dist/execute-action.d.ts +6 -0
- package/dist/execute-action.js +24 -0
- package/dist/http.d.ts +31 -1
- package/dist/http.js +74 -14
- package/dist/index.d.ts +13 -9
- package/dist/index.js +5 -4
- package/dist/mcp/index.d.ts +2 -2
- package/dist/mcp/index.js +1 -1
- package/dist/sandbox/client.d.ts +4 -5
- package/dist/sandbox/client.js +4 -8
- package/dist/sandbox/index.d.ts +0 -2
- package/dist/sandbox/index.js +0 -1
- package/dist/served-definitions.d.ts +20 -0
- package/dist/served-definitions.js +100 -0
- package/dist/session-client.d.ts +282 -0
- package/dist/session-client.js +375 -0
- package/dist/sse.js +12 -0
- package/dist/studio-embed.d.ts +9 -0
- package/dist/studio-embed.js +8 -0
- package/dist/token-endpoint.d.ts +23 -0
- package/dist/token-endpoint.js +30 -0
- package/package.json +15 -8
- package/dist/ag-ui/history.d.ts +0 -7
- package/dist/ag-ui/history.js +0 -60
- package/dist/ag-ui/sse.d.ts +0 -9
- package/dist/ag-ui/sse.js +0 -7
- package/dist/ag-ui/translate.d.ts +0 -38
- package/dist/ag-ui/translate.js +0 -222
- package/dist/derived-credentials.d.ts +0 -5
- package/dist/derived-credentials.js +0 -17
- package/dist/executor.d.ts +0 -20
- package/dist/executor.js +0 -353
- package/dist/mcp/mcp.d.ts +0 -30
- package/dist/mcp/mcp.js +0 -80
- package/dist/sandbox/sandbox.d.ts +0 -27
- package/dist/sandbox/sandbox.js +0 -92
package/dist/client.js
CHANGED
|
@@ -1,338 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
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 child = delegateOf(tool)?.agent;
|
|
42
|
-
for (const item of child?.getBinding().declarations ?? [])
|
|
43
|
-
found.push({ key: `${child.id}/${item.id}`, item });
|
|
44
|
-
}
|
|
45
|
-
return found;
|
|
46
|
-
}
|
|
47
|
-
function pluginRootsOf(agent) {
|
|
48
|
-
const roots = {};
|
|
49
|
-
for (const { key, item } of declarationsOf(agent))
|
|
50
|
-
if (item.pluginRoot)
|
|
51
|
-
roots[key] = item.pluginRoot;
|
|
52
|
-
return Object.keys(roots).length === 0 ? undefined : roots;
|
|
53
|
-
}
|
|
54
|
-
/** Closures stay on the live binding. The published manifest cannot name them. */
|
|
55
|
-
export function assertNoMiddlewareClosures(agent) {
|
|
56
|
-
if (declarationsOf(agent).some(({ item }) => item.hasMiddleware))
|
|
57
|
-
throw new Error(middlewareClosure);
|
|
58
|
-
}
|
|
59
|
-
export class AgentsClient {
|
|
60
|
-
transport;
|
|
61
|
-
/** The subject this client acts for (`as`), or `undefined` for the principal itself. */
|
|
62
|
-
subject;
|
|
63
|
-
constructor(destination = {}) {
|
|
64
|
-
this.transport = new Transport(destination);
|
|
65
|
-
this.subject = undefined;
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* A client that acts for `subject`: every call sends `Nylorun-Subject` and
|
|
69
|
-
* `Nylorun-Scopes`, and the Runtime limits it to the scopes and to the subject's own sessions
|
|
70
|
-
* and vaults (Host feature `subject-headers`). For app servers, which authenticate the person
|
|
71
|
-
* themselves and hold the key; nothing is minted, cached or refreshed.
|
|
72
|
-
*/
|
|
73
|
-
as(subject, options = {}) {
|
|
74
|
-
if (this.subject !== undefined)
|
|
75
|
-
throw new Error(`This client already acts for ${this.subject}`);
|
|
76
|
-
const scopes = [...(options.scopes ?? ["sessions:own"])].join(" ");
|
|
77
|
-
const parsed = parseSubjectHeaders(subject, scopes);
|
|
78
|
-
if (!parsed.ok)
|
|
79
|
-
throw new TypeError(parsed.message);
|
|
80
|
-
const client = Object.create(AgentsClient.prototype);
|
|
81
|
-
return Object.assign(client, {
|
|
82
|
-
transport: this.transport.withHeaders({
|
|
83
|
-
[SUBJECT_HEADER]: subject,
|
|
84
|
-
[SCOPES_HEADER]: [...parsed.scopes].join(" "),
|
|
85
|
-
}),
|
|
86
|
-
subject,
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
/** The Runtime's protocol features, including optional ones such as `transcript-events`. */
|
|
90
|
-
hostFeatures(options = {}) {
|
|
91
|
-
return this.transport.hostFeatures(options.signal);
|
|
92
|
-
}
|
|
93
|
-
listAgents(options = {}) {
|
|
94
|
-
return this.transport.json("/v1/agents", "GET", undefined, options.signal);
|
|
95
|
-
}
|
|
96
|
-
listSessions(options = {}) {
|
|
97
|
-
const query = options.agentId
|
|
98
|
-
? `?agentId=${encodeURIComponent(options.agentId)}`
|
|
99
|
-
: "";
|
|
100
|
-
return this.transport.json(`/v1/sessions${query}`, "GET", undefined, options.signal);
|
|
101
|
-
}
|
|
102
|
-
async saveAgent(agent, options) {
|
|
103
|
-
if (isBuiltWorkflow(agent)) {
|
|
104
|
-
for (const binding of Object.values(agent.getBinding().agents)) {
|
|
105
|
-
await this.saveAgent({
|
|
106
|
-
id: binding.manifest.id,
|
|
107
|
-
manifest: binding.manifest,
|
|
108
|
-
getBinding: () => binding,
|
|
109
|
-
}, options);
|
|
110
|
-
}
|
|
111
|
-
return this.transport.json(`/v1/agents/${segment(agent.id)}`, "PUT", {
|
|
112
|
-
requestId: options.requestId ?? id(),
|
|
113
|
-
manifest: agent.manifest,
|
|
114
|
-
implementationVersion: options.implementationVersion,
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
assertNoMiddlewareClosures(agent);
|
|
118
|
-
const pluginRoots = pluginRootsOf(agent);
|
|
119
|
-
return this.transport.json(`/v1/agents/${segment(agent.id)}`, "PUT", {
|
|
120
|
-
requestId: options.requestId ?? id(),
|
|
121
|
-
manifest: agent.manifest,
|
|
122
|
-
implementationVersion: options.implementationVersion,
|
|
123
|
-
...(pluginRoots === undefined ? {} : { pluginRoots }),
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
async createSession(options) {
|
|
127
|
-
const sessionId = options.id ?? id();
|
|
128
|
-
await this.transport.json(`/v1/sessions/${segment(sessionId)}`, "PUT", {
|
|
129
|
-
requestId: options.requestId ?? id(),
|
|
130
|
-
agentId: options.agentId,
|
|
131
|
-
ownerUserId: options.ownerUserId,
|
|
132
|
-
...(options.info ? { info: options.info } : {}),
|
|
133
|
-
...(options.vaultIds ? { vaultIds: options.vaultIds } : {}),
|
|
134
|
-
...(options.credentialSelections
|
|
135
|
-
? { credentialSelections: options.credentialSelections }
|
|
136
|
-
: {}),
|
|
137
|
-
...(options.sandbox ? { sandbox: options.sandbox } : {}),
|
|
138
|
-
});
|
|
139
|
-
return this.session(sessionId);
|
|
140
|
-
}
|
|
141
|
-
createVault(options) {
|
|
142
|
-
return this.transport.json("/v1/vaults", "POST", {
|
|
143
|
-
requestId: options.requestId ?? id(),
|
|
144
|
-
idempotencyKey: options.idempotencyKey,
|
|
145
|
-
name: options.name,
|
|
146
|
-
ownerUserId: options.ownerUserId,
|
|
147
|
-
...(options.metadata ? { metadata: options.metadata } : {}),
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
listVaults(ownerUserId, signal) {
|
|
151
|
-
return this.transport.json(`/v1/vaults?ownerUserId=${encodeURIComponent(ownerUserId)}`, "GET", undefined, signal);
|
|
152
|
-
}
|
|
153
|
-
getVault(vaultId, signal) {
|
|
154
|
-
return this.transport.json(`/v1/vaults/${segment(vaultId)}`, "GET", undefined, signal);
|
|
155
|
-
}
|
|
156
|
-
deleteVault(vaultId) {
|
|
157
|
-
return this.transport.json(`/v1/vaults/${segment(vaultId)}`, "DELETE");
|
|
158
|
-
}
|
|
159
|
-
createCredential(vaultId, options) {
|
|
160
|
-
return this.transport.json(`/v1/vaults/${segment(vaultId)}/credentials`, "POST", {
|
|
161
|
-
requestId: options.requestId ?? id(),
|
|
162
|
-
idempotencyKey: options.idempotencyKey,
|
|
163
|
-
name: options.name,
|
|
164
|
-
auth: options.auth,
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
listCredentials(vaultId, signal) {
|
|
168
|
-
return this.transport.json(`/v1/vaults/${segment(vaultId)}/credentials`, "GET", undefined, signal);
|
|
169
|
-
}
|
|
170
|
-
getCredential(vaultId, credentialId, signal) {
|
|
171
|
-
return this.transport.json(`/v1/vaults/${segment(vaultId)}/credentials/${segment(credentialId)}`, "GET", undefined, signal);
|
|
172
|
-
}
|
|
173
|
-
rotateCredential(vaultId, credentialId, options) {
|
|
174
|
-
return this.transport.json(`/v1/vaults/${segment(vaultId)}/credentials/${segment(credentialId)}`, "POST", {
|
|
175
|
-
requestId: options.requestId ?? id(),
|
|
176
|
-
idempotencyKey: options.idempotencyKey,
|
|
177
|
-
auth: options.auth,
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
deleteCredential(vaultId, credentialId) {
|
|
181
|
-
return this.transport.json(`/v1/vaults/${segment(vaultId)}/credentials/${segment(credentialId)}`, "DELETE");
|
|
182
|
-
}
|
|
183
|
-
session(sessionId) {
|
|
184
|
-
return new SessionClient(this.transport, sessionId);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
export class SessionClient {
|
|
188
|
-
transport;
|
|
189
|
-
id;
|
|
190
|
-
sandbox;
|
|
191
|
-
constructor(transport, id) {
|
|
192
|
-
this.transport = transport;
|
|
193
|
-
this.id = id;
|
|
194
|
-
this.sandbox = sessionSandboxOf(transport, id);
|
|
195
|
-
}
|
|
196
|
-
get path() {
|
|
197
|
-
return `/v1/sessions/${segment(this.id)}`;
|
|
198
|
-
}
|
|
199
|
-
inspect(signal) {
|
|
200
|
-
return this.transport.json(this.path, "GET", undefined, signal);
|
|
201
|
-
}
|
|
202
|
-
/** Pending waits / interactions from the session inspect view. */
|
|
203
|
-
async pending(signal) {
|
|
204
|
-
const view = await this.inspect(signal);
|
|
205
|
-
return view.waits ?? [];
|
|
206
|
-
}
|
|
207
|
-
command(command, signal) {
|
|
208
|
-
return this.transport.json(`${this.path}/commands`, "POST", command, signal);
|
|
209
|
-
}
|
|
210
|
-
/**
|
|
211
|
-
* Start a turn. Strings become `message.content`; any other JSON value becomes
|
|
212
|
-
* `message.data` (workflows.md §5).
|
|
213
|
-
*/
|
|
214
|
-
input(value, options) {
|
|
215
|
-
const base = {
|
|
216
|
-
type: "message",
|
|
217
|
-
requestId: options.requestId ?? id(),
|
|
218
|
-
idempotencyKey: options.idempotencyKey,
|
|
219
|
-
};
|
|
220
|
-
return this.command(typeof value === "string"
|
|
221
|
-
? { ...base, content: value }
|
|
222
|
-
: { ...base, data: value }, options.signal);
|
|
223
|
-
}
|
|
224
|
-
approve(interactionId, approved, options) {
|
|
225
|
-
return this.command({
|
|
226
|
-
type: "approve",
|
|
227
|
-
interactionId,
|
|
228
|
-
approved,
|
|
229
|
-
requestId: options.requestId ?? id(),
|
|
230
|
-
idempotencyKey: options.idempotencyKey,
|
|
231
|
-
}, options.signal);
|
|
232
|
-
}
|
|
233
|
-
respond(interactionId, value, options) {
|
|
234
|
-
return this.command({
|
|
235
|
-
type: "respond",
|
|
236
|
-
interactionId,
|
|
237
|
-
value,
|
|
238
|
-
requestId: options.requestId ?? id(),
|
|
239
|
-
idempotencyKey: options.idempotencyKey,
|
|
240
|
-
}, options.signal);
|
|
241
|
-
}
|
|
242
|
-
cancel(options) {
|
|
243
|
-
return this.command({
|
|
244
|
-
type: "cancel",
|
|
245
|
-
requestId: options.requestId ?? id(),
|
|
246
|
-
idempotencyKey: options.idempotencyKey,
|
|
247
|
-
...(options.reason ? { reason: options.reason } : {}),
|
|
248
|
-
}, options.signal);
|
|
249
|
-
}
|
|
250
|
-
/** `agent` narrows to one agent used as a tool: its delegationId or its path. */
|
|
251
|
-
async history(options = {}) {
|
|
252
|
-
const query = new URLSearchParams();
|
|
253
|
-
if (options.cursor)
|
|
254
|
-
query.set("cursor", options.cursor);
|
|
255
|
-
if (options.agent)
|
|
256
|
-
query.set("agent", options.agent);
|
|
257
|
-
const search = query.toString();
|
|
258
|
-
return SessionItemsResponseSchema.parse(await this.transport.json(`${this.path}/items${search ? `?${search}` : ""}`, "GET", undefined, options.signal));
|
|
259
|
-
}
|
|
260
|
-
async *observe(options = {}) {
|
|
261
|
-
if (!options.follow) {
|
|
262
|
-
yield* this.observeOne(this.id, options);
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
yield* this.observeFollowing(options);
|
|
266
|
-
}
|
|
267
|
-
async *observeOne(sessionId, options) {
|
|
268
|
-
for await (const frame of observeSSE(this.transport, `/v1/sessions/${segment(sessionId)}/events`, options)) {
|
|
269
|
-
if (frame.event === "heartbeat" || frame.event === "ready")
|
|
270
|
-
continue;
|
|
271
|
-
yield LiveEventSchema.parse(JSON.parse(frame.data));
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
/**
|
|
275
|
-
* Own session stream plus linked agent sessions announced in `node.agent`
|
|
276
|
-
* events, merged by `createdAt` then arrival order (workflows.md §11).
|
|
277
|
-
*/
|
|
278
|
-
async *observeFollowing(options) {
|
|
279
|
-
const signal = options.signal;
|
|
280
|
-
const linked = new Set();
|
|
281
|
-
const queue = [];
|
|
282
|
-
let wait;
|
|
283
|
-
let open = 1;
|
|
284
|
-
let failed;
|
|
285
|
-
const wake = () => {
|
|
286
|
-
wait?.();
|
|
287
|
-
wait = undefined;
|
|
288
|
-
};
|
|
289
|
-
const push = (event) => {
|
|
290
|
-
queue.push(event);
|
|
291
|
-
queue.sort((a, b) => {
|
|
292
|
-
const byTime = a.createdAt.localeCompare(b.createdAt);
|
|
293
|
-
return byTime !== 0 ? byTime : a.cursor.localeCompare(b.cursor);
|
|
294
|
-
});
|
|
295
|
-
wake();
|
|
296
|
-
};
|
|
297
|
-
const pump = async (sessionId, cursor) => {
|
|
298
|
-
try {
|
|
299
|
-
for await (const event of this.observeOne(sessionId, { cursor, signal })) {
|
|
300
|
-
push(event);
|
|
301
|
-
const link = linkedSessionId(event);
|
|
302
|
-
if (link && link !== this.id && !linked.has(link)) {
|
|
303
|
-
linked.add(link);
|
|
304
|
-
open += 1;
|
|
305
|
-
void pump(link).finally(() => {
|
|
306
|
-
open -= 1;
|
|
307
|
-
wake();
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
catch (error) {
|
|
313
|
-
if (!signal?.aborted)
|
|
314
|
-
failed = error;
|
|
315
|
-
}
|
|
316
|
-
};
|
|
317
|
-
void pump(this.id, options.cursor).finally(() => {
|
|
318
|
-
open -= 1;
|
|
319
|
-
wake();
|
|
320
|
-
});
|
|
321
|
-
while (!signal?.aborted) {
|
|
322
|
-
if (failed)
|
|
323
|
-
throw failed;
|
|
324
|
-
if (queue.length) {
|
|
325
|
-
yield queue.shift();
|
|
326
|
-
continue;
|
|
327
|
-
}
|
|
328
|
-
if (open <= 0)
|
|
329
|
-
return;
|
|
330
|
-
await new Promise((resolve) => {
|
|
331
|
-
wait = resolve;
|
|
332
|
-
});
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
4
|
export function createClient(destination = {}) {
|
|
337
5
|
if (destination.url !== undefined ||
|
|
338
6
|
destination.key !== undefined ||
|
|
@@ -346,4 +14,3 @@ export function createClient(destination = {}) {
|
|
|
346
14
|
...(destination.fetch ? { fetch: destination.fetch } : {}),
|
|
347
15
|
}));
|
|
348
16
|
}
|
|
349
|
-
export { RuntimeError, IncompatibleRuntimeError } from "./http.js";
|
package/dist/connection.d.ts
CHANGED
package/dist/connection.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/dist/execute-action.d.ts
CHANGED
|
@@ -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;
|
package/dist/execute-action.js
CHANGED
|
@@ -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")
|