@nylorun/agents 0.14.0-beta → 0.16.1-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.
@@ -1,7 +1,15 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { existsSync, readdirSync, readFileSync, realpathSync, statSync, } from "node:fs";
3
+ import { readFile } from "node:fs/promises";
2
4
  import { isAbsolute, join, relative, resolve, sep } from "node:path";
5
+ import { DEFINITION_FILE_MAX_BYTES, SKILL_ENTRY, SKILL_FILES_MAX, skillFilePathIssue, } from "@nylorun/core/define";
3
6
  import matter from "gray-matter";
4
7
  const SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
8
+ /** Version control and operating system files, never part of a skill. */
9
+ const SKIPPED_DIRECTORIES = new Set([".git", ".hg", ".svn", "node_modules"]);
10
+ const SKIPPED_FILES = new Set([".DS_Store", "Thumbs.db", "desktop.ini"]);
11
+ /** Environment files hold secrets: never uploaded (`.env`, `.env.local`, …). */
12
+ const isEnvFile = (name) => name === ".env" || name.startsWith(".env.");
5
13
  export class SkillsError extends Error {
6
14
  code;
7
15
  diagnostics;
@@ -24,7 +32,9 @@ export function resolveSkillsRoot(directory) {
24
32
  }
25
33
  /**
26
34
  * Load Agent Skills (agentskills.io) from a catalog directory.
27
- * Each immediate subdirectory with a valid SKILL.md becomes one skill.
35
+ * Each immediate subdirectory with a valid SKILL.md becomes one skill, with every file of its
36
+ * folder (text or binary) hashed. Throws `SkillsError` for a skill the Runtime cannot hold: a
37
+ * file over 10 MiB, more than 500 files, or a path a manifest cannot name.
28
38
  */
29
39
  export function loadSkillsFromDirectory(directory, diagnostics, options = {}) {
30
40
  const root = resolveSkillsRoot(directory);
@@ -34,7 +44,7 @@ export function loadSkillsFromDirectory(directory, diagnostics, options = {}) {
34
44
  for (const entry of readdirSync(root, { withFileTypes: true })) {
35
45
  if (!entry.isDirectory() || entry.name.startsWith("."))
36
46
  continue;
37
- if (entry.name === "node_modules" || entry.name === ".git")
47
+ if (SKIPPED_DIRECTORIES.has(entry.name))
38
48
  continue;
39
49
  const skillDir = join(root, entry.name);
40
50
  const directoryReal = realInside(boundary, skillDir);
@@ -47,7 +57,7 @@ export function loadSkillsFromDirectory(directory, diagnostics, options = {}) {
47
57
  });
48
58
  continue;
49
59
  }
50
- const skillFile = join(directoryReal, "SKILL.md");
60
+ const skillFile = join(directoryReal, SKILL_ENTRY);
51
61
  if (!existsSync(skillFile))
52
62
  continue;
53
63
  const skillReal = realInside(boundary, skillFile);
@@ -79,12 +89,10 @@ export function loadSkillsFromDirectory(directory, diagnostics, options = {}) {
79
89
  });
80
90
  continue;
81
91
  }
82
- const resources = readResources(boundary, directoryReal, diagnostics, prefix);
83
92
  skills[parsed.name] = {
84
93
  name: parsed.name,
85
94
  description: parsed.description,
86
- instructions: parsed.instructions,
87
- ...(Object.keys(resources).length === 0 ? {} : { resources }),
95
+ ...readFiles(parsed.name, boundary, directoryReal, diagnostics, prefix),
88
96
  };
89
97
  }
90
98
  return skills;
@@ -112,47 +120,57 @@ export function parseSkill(text, directoryName) {
112
120
  function asTrimmedString(value) {
113
121
  return typeof value === "string" ? value.trim() : "";
114
122
  }
115
- function readResources(boundary, directory, diagnostics, prefix) {
116
- const resources = {};
117
- const walk = (current, depth) => {
118
- if (depth > 6)
123
+ /** Every file of the skill's folder, hashed, and where to read each one's bytes. */
124
+ function readFiles(name, boundary, directory, diagnostics, prefix) {
125
+ const files = {};
126
+ const sources = {};
127
+ const visited = new Set();
128
+ // `at` is the folder's path in the skill: symbolic links inside the package are followed.
129
+ const walk = (current, at) => {
130
+ if (visited.has(current))
119
131
  return;
132
+ visited.add(current);
120
133
  for (const entry of readdirSync(current, { withFileTypes: true })) {
121
- if (entry.name.startsWith(".") || entry.name === "node_modules")
122
- continue;
123
134
  const full = join(current, entry.name);
124
- if (entry.isDirectory()) {
125
- const real = realInside(boundary, full);
126
- if (!real)
127
- continue;
128
- walk(real, depth + 1);
129
- continue;
130
- }
131
- if (!entry.isFile() && !entry.isSymbolicLink())
132
- continue;
133
- if (depth === 0 && entry.name === "SKILL.md")
134
- continue;
135
135
  const real = realInside(boundary, full);
136
- if (!real || !statSync(real).isFile()) {
136
+ if (!real) {
137
137
  diagnostics.push({
138
138
  severity: "warning",
139
139
  code: `${prefix}.resource-denied`,
140
- message: `Denied resource outside the package: ${entry.name}`,
140
+ message: `Denied a file of skill '${name}' outside the package: ${entry.name}`,
141
141
  path: full,
142
142
  });
143
143
  continue;
144
144
  }
145
- const bytes = readFileSync(real);
146
- if (bytes.includes(0))
145
+ const stats = statSync(real);
146
+ const path = at === "" ? entry.name : `${at}/${entry.name}`;
147
+ if (stats.isDirectory()) {
148
+ if (!SKIPPED_DIRECTORIES.has(entry.name))
149
+ walk(real, path);
147
150
  continue;
148
- const rel = relative(directory, real).split(sep).join("/");
149
- if (!rel || rel.startsWith(".."))
151
+ }
152
+ if (!stats.isFile() || SKIPPED_FILES.has(entry.name) || isEnvFile(entry.name))
150
153
  continue;
151
- resources[rel] = bytes.toString("utf8");
154
+ const issue = skillFilePathIssue(path);
155
+ if (issue)
156
+ throw new SkillsError(`${prefix}.invalid-path`, `Skill '${name}' has a file whose path ${issue}: ${path}`);
157
+ if (stats.size > DEFINITION_FILE_MAX_BYTES)
158
+ throw new SkillsError(`${prefix}.file-too-large`, `Skill '${name}' file ${path} is ${stats.size} bytes; a skill file may be at most ${DEFINITION_FILE_MAX_BYTES} (10 MiB)`);
159
+ const hash = `sha256:${createHash("sha256").update(readFileSync(real)).digest("hex")}`;
160
+ files[path] = hash;
161
+ sources[hash] = {
162
+ size: stats.size,
163
+ read: async () => new Uint8Array(await readFile(real)),
164
+ };
165
+ if (Object.keys(files).length > SKILL_FILES_MAX)
166
+ throw new SkillsError(`${prefix}.too-many-files`, `Skill '${name}' has more than ${SKILL_FILES_MAX} files; a skill may have at most ${SKILL_FILES_MAX}`);
152
167
  }
153
168
  };
154
- walk(directory, 0);
155
- return resources;
169
+ walk(directory, "");
170
+ return { files: sorted(files), sources };
171
+ }
172
+ function sorted(files) {
173
+ return Object.fromEntries(Object.entries(files).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)));
156
174
  }
157
175
  function realInside(root, candidate) {
158
176
  if (!existsSync(candidate))
@@ -1,5 +1,5 @@
1
1
  import type { CapabilityDeclaration } from "@nylorun/core/define";
2
- import { type SkillDiagnostic } from "./load.js";
2
+ import { type LoadedSkill, type SkillDiagnostic } from "./load.js";
3
3
  export interface SkillsOptions {
4
4
  /** Capability id. Defaults to the catalog directory basename. */
5
5
  readonly id?: string;
@@ -21,8 +21,10 @@ export interface SkillsCapability extends CapabilityDeclaration {
21
21
  * references/policy.md
22
22
  * ```
23
23
  *
24
- * Sets both `skills` (manifest names/descriptions) and `skillRecords` (bodies +
25
- * resources) so callers do not duplicate content. The harness injects
26
- * `load_skill` / `read_skill_resource` from the records.
24
+ * Sets `skills`: each skill's name, description and files (every file of its folder, binary
25
+ * included, by content hash). The client uploads the files the Runtime lacks when it registers
26
+ * the agent, and the Runtime serves `load_skill` / `read_skill_resource` from them.
27
27
  */
28
28
  export declare function skills(directory: string, options?: SkillsOptions): SkillsCapability;
29
+ /** The manifest's skills and the bytes behind their files, for a capability declaration. */
30
+ export declare function skillsDeclaration(listed: readonly LoadedSkill[]): Required<Pick<CapabilityDeclaration, "skills" | "skillFiles">>;
@@ -13,23 +13,20 @@ import { loadSkillsFromDirectory, resolveSkillsRoot, } from "./load.js";
13
13
  * references/policy.md
14
14
  * ```
15
15
  *
16
- * Sets both `skills` (manifest names/descriptions) and `skillRecords` (bodies +
17
- * resources) so callers do not duplicate content. The harness injects
18
- * `load_skill` / `read_skill_resource` from the records.
16
+ * Sets `skills`: each skill's name, description and files (every file of its folder, binary
17
+ * included, by content hash). The client uploads the files the Runtime lacks when it registers
18
+ * the agent, and the Runtime serves `load_skill` / `read_skill_resource` from them.
19
19
  */
20
20
  export function skills(directory, options = {}) {
21
21
  const root = resolveSkillsRoot(directory);
22
22
  const diagnostics = [];
23
- const records = loadSkillsFromDirectory(root, diagnostics);
24
- const listed = Object.values(records);
23
+ const loaded = loadSkillsFromDirectory(root, diagnostics);
24
+ const listed = Object.values(loaded);
25
25
  const id = options.id ?? basename(root);
26
26
  if (listed.length === 0) {
27
27
  return { id, root, diagnostics };
28
28
  }
29
- const skillManifest = Object.fromEntries(listed.map((skill) => [
30
- skill.name,
31
- { name: skill.name, description: skill.description },
32
- ]));
29
+ const { skills: skillManifest, skillFiles } = skillsDeclaration(listed);
33
30
  return {
34
31
  id,
35
32
  root,
@@ -41,7 +38,17 @@ export function skills(directory, options = {}) {
41
38
  }))),
42
39
  ],
43
40
  skills: skillManifest,
44
- skillRecords: records,
41
+ skillFiles,
45
42
  diagnostics,
46
43
  };
47
44
  }
45
+ /** The manifest's skills and the bytes behind their files, for a capability declaration. */
46
+ export function skillsDeclaration(listed) {
47
+ return {
48
+ skills: Object.fromEntries(listed.map((skill) => [
49
+ skill.name,
50
+ { name: skill.name, description: skill.description, files: skill.files },
51
+ ])),
52
+ skillFiles: Object.assign({}, ...listed.map((skill) => skill.sources)),
53
+ };
54
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nylorun/agents",
3
- "version": "0.14.0-beta",
4
- "description": "Nylorun agent definitions, session client and Action endpoint",
3
+ "version": "0.16.1-beta",
4
+ "description": "Nylorun agent definitions and session client",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
7
7
  "files": [
@@ -43,7 +43,7 @@
43
43
  "test": "vitest run --passWithNoTests"
44
44
  },
45
45
  "dependencies": {
46
- "@nylorun/core": "0.13.0-beta",
46
+ "@nylorun/core": "0.15.0-beta",
47
47
  "gray-matter": "^4.0.3"
48
48
  },
49
49
  "peerDependencies": {
@@ -1,63 +0,0 @@
1
- import { type EndpointPingResponse } from "@nylorun/core/contracts";
2
- import type { BuiltWorkflow } from "@nylorun/core/define";
3
- import { toNodeListener } from "./ag-ui/node.js";
4
- import { type AgentSource, type AgentsClient } from "./client.js";
5
- export interface ActionHandlerOptions {
6
- /** The agents and workflows this endpoint serves. Agents they embed are served too. */
7
- agents: readonly (AgentSource | BuiltWorkflow)[];
8
- /**
9
- * Application client, used by `register` (and, when `runtime` is not set, to read the
10
- * Tenant's public keys). Default: `createClient()`, from the environment or the Project link.
11
- */
12
- client?: AgentsClient | Promise<AgentsClient>;
13
- /**
14
- * Where to read the Tenant's public keys without a key. Set it for a process that only
15
- * serves Actions and holds no application key.
16
- */
17
- runtime?: {
18
- url: string;
19
- fetch?: typeof fetch;
20
- };
21
- /** The Tenant's public keys, instead of reading them from the Runtime. */
22
- jwks?: {
23
- keys: readonly JsonWebKey[];
24
- };
25
- /**
26
- * The URL the Runtime calls, as registered. Every delivery token must name it. Default: the
27
- * URL passed to `register` in this process; until then, any URL of the Tenant.
28
- */
29
- url?: string;
30
- /** Registered with each endpoint. Default: `NYLORUN_IMPLEMENTATION_VERSION`, else `dev`. */
31
- implementationVersion?: string;
32
- /** Receives errors that are answered with a status instead of thrown. */
33
- onError?: (error: unknown) => void;
34
- /**
35
- * Keeps the process alive for a background tool's work after its `202` answer, on platforms
36
- * that end a request's work with its response (for example a serverless platform's
37
- * `waitUntil`). A Node server needs nothing.
38
- */
39
- waitUntil?: (work: Promise<unknown>) => void;
40
- }
41
- export interface RegisterOptions {
42
- /** The URL the Runtime calls, e.g. `http://localhost:3000/nylorun/actions`. */
43
- url: string;
44
- /** Save the definitions first. Default true. */
45
- saveDefinitions?: boolean;
46
- /** How long one inline delivery may take, in milliseconds. */
47
- timeoutMs?: number;
48
- /** In-flight deliveries per agent. */
49
- maxConcurrent?: number;
50
- signal?: AbortSignal;
51
- }
52
- export interface ActionHandler {
53
- /** A web-standard handler: Hono, Next.js route handlers, Workers, Bun, Deno. */
54
- fetch(request: Request): Promise<Response>;
55
- /** The same handler for `node:http` and Express. */
56
- readonly node: ReturnType<typeof toNodeListener>;
57
- /**
58
- * Saves the definitions, points the Runtime at `url` for every served agent and pings each
59
- * one through the Runtime, so a wrong URL or a tunnel that is down fails here.
60
- */
61
- register(options: RegisterOptions): Promise<EndpointPingResponse[]>;
62
- }
63
- export declare function createActionHandler(options: ActionHandlerOptions): ActionHandler;
@@ -1,279 +0,0 @@
1
- /**
2
- * An Action endpoint (design: Action endpoints): the application mounts this handler at a URL
3
- * and registers the URL; the Runtime POSTs each Action for the served agents to it and records
4
- * the answer as the Action's outcome. No stream, no claim, no key needed to serve.
5
- *
6
- * Each request is checked before any code runs: the delivery token must be signed by the
7
- * Tenant's signing key, be for this Tenant, this URL (once known), this Action and this
8
- * generation, and cover the exact body. The Action then runs through `executeAction`, with the
9
- * request's signal as `ctx.signal`.
10
- */
11
- import { OUTCOME_HEADER, SIGNATURE_HEADER, } from "@nylorun/core/compatibility";
12
- import { ActionDeliverySchema, DeliveryHeartbeatResponseSchema, EndpointPingResponseSchema, PutEndpointsRequestSchema, } from "@nylorun/core/contracts";
13
- import { toNodeListener } from "./ag-ui/node.js";
14
- import { createClient } from "./client.js";
15
- import { DeliveryVerificationError, JwksCache, verifyDeliveryToken, } from "./delivery-token.js";
16
- import { executeAction, runsInBackground, } from "./execute-action.js";
17
- import { RuntimeError, Transport, delay, segment } from "./http.js";
18
- import { createActionSandbox } from "./sandbox/client.js";
19
- import { buildAgents, embeddedIn, flowManifestHash, implementationVersionOf, } from "./served-definitions.js";
20
- /** The Host feature `register` needs. */
21
- const REQUIRED_FEATURE = "action-endpoints";
22
- export function createActionHandler(options) {
23
- const agents = buildAgents(options.agents, "createActionHandler");
24
- const version = implementationVersionOf(options);
25
- let client;
26
- const clientOf = () => (client ??= Promise.resolve(options.client ?? createClient()));
27
- let audience = options.url;
28
- let verification;
29
- const verificationOf = () => (verification ??= resolveVerification(options, clientOf)).catch((error) => {
30
- verification = undefined;
31
- throw error;
32
- });
33
- let callbacks;
34
- const report = (error) => {
35
- try {
36
- options.onError?.(error);
37
- }
38
- catch { }
39
- };
40
- const handle = async (request) => {
41
- if (request.method !== "POST")
42
- return answer(405, "method_not_allowed", "An Action endpoint accepts POST only");
43
- const body = new Uint8Array(await request.arrayBuffer());
44
- const { url, fetch, keys } = await verificationOf();
45
- const token = request.headers.get(SIGNATURE_HEADER);
46
- let claims;
47
- try {
48
- claims = await verifyDeliveryToken(token, {
49
- keys: keys.lookup,
50
- body,
51
- ...(audience === undefined ? {} : { audience }),
52
- });
53
- }
54
- catch (error) {
55
- if (!(error instanceof DeliveryVerificationError))
56
- throw error;
57
- // A key this endpoint has not seen yet (a rotation within the refetch interval) is not
58
- // a refusal: the Runtime retries a 503, while a 401 would fail the Action.
59
- return answer(error.code === "key_unknown" ? 503 : 401, error.code, error.message);
60
- }
61
- let delivery;
62
- try {
63
- delivery = ActionDeliverySchema.parse(JSON.parse(new TextDecoder().decode(body)));
64
- }
65
- catch {
66
- return answer(400, "invalid_delivery", "The body is not an Action delivery");
67
- }
68
- if (delivery.type === "ping") {
69
- if (claims.sub !== "ping" || claims.agt !== delivery.agentId)
70
- return answer(401, "signature_invalid", "The token is for another delivery");
71
- const agent = agents.get(delivery.agentId);
72
- if (!agent)
73
- return notServed(delivery.agentId);
74
- const manifestHash = flowManifestHash(agent);
75
- return Response.json({
76
- agentId: agent.id,
77
- implementationVersion: version,
78
- ...(manifestHash === undefined ? {} : { manifestHash }),
79
- });
80
- }
81
- const { action } = delivery;
82
- if (claims.sub !== action.actionId ||
83
- claims.gen !== action.generation ||
84
- claims.agt !== action.agentId)
85
- return answer(401, "signature_invalid", "The token is for another delivery");
86
- const agent = agents.get(action.agentId);
87
- if (!agent)
88
- return notServed(action.agentId);
89
- const served = flowManifestHash(agent);
90
- if (served !== undefined && "key" in action && action.manifestHash !== served)
91
- return answer(409, "version_mismatch", `Action ${action.actionId} belongs to manifest ${action.manifestHash} of '${action.agentId}'; this endpoint serves ${served}`);
92
- if (runsInBackground(action, agent)) {
93
- const work = runInBackground({
94
- action,
95
- agent,
96
- token: token,
97
- sandbox: delivery.sandbox,
98
- runtime: { url, ...(fetch ? { fetch } : {}) },
99
- report,
100
- });
101
- options.waitUntil?.(work);
102
- return new Response(null, { status: 202 });
103
- }
104
- const sandbox = delivery.sandbox
105
- ? createActionSandbox({
106
- transport: (callbacks = callbacks
107
- ? callbacks.withKey(token)
108
- : new Transport({ url, key: token, ...(fetch ? { fetch } : {}) })),
109
- actionId: action.actionId,
110
- signal: request.signal,
111
- })
112
- : undefined;
113
- try {
114
- const outcome = await executeAction(action, agent, request.signal, {
115
- ...(sandbox ? { sandbox } : {}),
116
- });
117
- return Response.json(outcome, { headers: { [OUTCOME_HEADER]: "1" } });
118
- }
119
- catch (error) {
120
- if (request.signal.aborted)
121
- return answer(503, "aborted", "The Runtime closed the delivery");
122
- // Tool errors are outcomes; a throw means this endpoint cannot run the Action at all.
123
- report(error);
124
- return answer(404, "action_not_served", error instanceof Error ? error.message : String(error));
125
- }
126
- };
127
- const fetchHandler = async (request) => {
128
- try {
129
- return await handle(request);
130
- }
131
- catch (error) {
132
- report(error);
133
- // Not run: the Runtime retries a 503 whatever the Action's kind.
134
- return answer(503, "endpoint_unavailable", error instanceof Error ? error.message : String(error));
135
- }
136
- };
137
- return {
138
- fetch: fetchHandler,
139
- node: toNodeListener({ fetch: fetchHandler }),
140
- async register(register) {
141
- const application = await clientOf();
142
- const { transport } = application;
143
- await transport.requireFeature(REQUIRED_FEATURE, register.signal);
144
- if (register.saveDefinitions ?? true) {
145
- const embedded = embeddedIn(agents);
146
- for (const agent of agents.values())
147
- if (!embedded.has(agent.id))
148
- await application.saveAgent(agent, { implementationVersion: version });
149
- }
150
- const endpoints = [...agents.values()].map((agent) => {
151
- const manifestHash = flowManifestHash(agent);
152
- return {
153
- agentId: agent.id,
154
- url: register.url,
155
- implementationVersion: version,
156
- ...(manifestHash === undefined ? {} : { manifestHash }),
157
- ...(register.timeoutMs === undefined ? {} : { timeoutMs: register.timeoutMs }),
158
- ...(register.maxConcurrent === undefined
159
- ? {}
160
- : { maxConcurrent: register.maxConcurrent }),
161
- };
162
- });
163
- await transport.json("/v1/endpoints", "PUT", PutEndpointsRequestSchema.parse({ endpoints }), register.signal);
164
- audience = register.url;
165
- const answers = [];
166
- for (const agent of agents.values())
167
- answers.push(EndpointPingResponseSchema.parse(await transport.json(`/v1/endpoints/${segment(agent.id)}/ping`, "POST", {}, register.signal)));
168
- return answers;
169
- },
170
- };
171
- }
172
- /** How long a background tool waits before its first heartbeat, and after a failed one. */
173
- const HEARTBEAT_RETRY_MS = 1_000;
174
- /**
175
- * A background tool (`ToolDefinition.background`): runs after the `202` answer, heartbeats on
176
- * the deadline the Runtime returns, stops when a heartbeat answers `409` (cancelled, lost or
177
- * sent again), and posts its outcome. Every callback uses the newest delivery token.
178
- */
179
- async function runInBackground(input) {
180
- const { action, report } = input;
181
- let current = new Transport({ ...input.runtime, key: input.token });
182
- const live = {
183
- json: (...args) => current.json(...args),
184
- };
185
- const path = `/v1/actions/${segment(action.actionId)}`;
186
- const stop = new AbortController();
187
- const done = new AbortController();
188
- const heartbeats = (async () => {
189
- let wait = HEARTBEAT_RETRY_MS;
190
- while (!done.signal.aborted && !stop.signal.aborted) {
191
- try {
192
- await delay(wait, done.signal);
193
- }
194
- catch {
195
- return;
196
- }
197
- try {
198
- const renewed = DeliveryHeartbeatResponseSchema.parse(await current.json(`${path}/heartbeat`, "POST", undefined, done.signal));
199
- current = current.withKey(renewed.token);
200
- wait = Math.max(HEARTBEAT_RETRY_MS, Math.floor((Date.parse(renewed.deadlineAt) - Date.now()) / 3));
201
- }
202
- catch (error) {
203
- if (done.signal.aborted)
204
- return;
205
- if (error instanceof RuntimeError && error.status === 409) {
206
- stop.abort(error);
207
- return;
208
- }
209
- report(error);
210
- wait = HEARTBEAT_RETRY_MS;
211
- }
212
- }
213
- })();
214
- let outcome;
215
- try {
216
- outcome = await executeAction(action, input.agent, stop.signal, {
217
- ...(input.sandbox
218
- ? { sandbox: createActionSandbox({ transport: live, actionId: action.actionId, signal: stop.signal }) }
219
- : {}),
220
- });
221
- }
222
- catch (error) {
223
- // Not served here: say so rather than leave the Runtime waiting for the deadline.
224
- report(error);
225
- outcome = {
226
- value: {
227
- kind: "failed",
228
- code: "action_not_served",
229
- message: error instanceof Error ? error.message : String(error),
230
- },
231
- };
232
- }
233
- finally {
234
- done.abort();
235
- }
236
- await heartbeats;
237
- if (stop.signal.aborted)
238
- return;
239
- for (let attempt = 0;; attempt++) {
240
- try {
241
- await current.json(`${path}/result`, "POST", outcome);
242
- return;
243
- }
244
- catch (error) {
245
- if (attempt >= 4 ||
246
- (error instanceof RuntimeError && error.status < 500 && error.status !== 429)) {
247
- report(error);
248
- return;
249
- }
250
- await delay(Math.min(4_000, 250 * 2 ** attempt), new AbortController().signal);
251
- }
252
- }
253
- }
254
- async function resolveVerification(options, clientOf) {
255
- if (options.runtime) {
256
- const { url, fetch } = options.runtime;
257
- return {
258
- url,
259
- ...(fetch ? { fetch } : {}),
260
- keys: new JwksCache(options.jwks ?? { url, ...(fetch ? { fetch } : {}) }),
261
- };
262
- }
263
- const { transport } = await clientOf();
264
- return {
265
- url: transport.url,
266
- fetch: transport.fetcher,
267
- keys: new JwksCache(options.jwks ?? {
268
- url: transport.url,
269
- key: transport.key,
270
- fetch: transport.fetcher,
271
- }),
272
- };
273
- }
274
- function notServed(agentId) {
275
- return answer(404, "agent_not_served", `This endpoint does not serve '${agentId}'`);
276
- }
277
- function answer(status, code, message) {
278
- return Response.json({ code, message }, { status });
279
- }
@@ -1,62 +0,0 @@
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
- /**
26
- * Verifies `token` against `body`. The keys are the installation's, so a valid signature already
27
- * binds the issuer to its one Tenant: any `urn:nylorun:tenant:<id>` issuer is accepted (and
28
- * returned in `iss`) unless `tenantId` names the one expected.
29
- */
30
- export declare function verifyDeliveryToken(token: string | null, options: {
31
- keys: KeyLookup;
32
- /** When set, `iss` must name this Tenant. */
33
- tenantId?: string;
34
- body: Uint8Array;
35
- /** When set, `aud` must equal it: the URL this endpoint was registered with. */
36
- audience?: string;
37
- now?: number;
38
- }): Promise<DeliveryClaims>;
39
- /** base64url SHA-256 of `body`: the token's `bdy` claim. */
40
- export declare function bodyHash(body: Uint8Array): Promise<string>;
41
- /**
42
- * The Tenant's public keys, fetched from `GET /v1/access/jwks` and cached by `kid`. An
43
- * unknown `kid` refetches once, at most every `refetchMs`, so forged tokens cannot make the
44
- * endpoint call the Runtime on every request. Concurrent lookups share one in-flight fetch.
45
- */
46
- export declare class JwksCache {
47
- private readonly source;
48
- private readonly refetchMs;
49
- private readonly keys;
50
- private lastFetch;
51
- private pending;
52
- constructor(source: {
53
- url: string;
54
- key?: string;
55
- fetch?: typeof fetch;
56
- } | {
57
- keys: readonly JsonWebKey[];
58
- }, refetchMs?: number);
59
- readonly lookup: KeyLookup;
60
- private load;
61
- private fetchKeys;
62
- }