@nylorun/agents 0.14.0-beta → 0.16.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 +112 -0
- package/README.md +193 -121
- package/dist/access.d.ts +6 -26
- package/dist/access.js +1 -26
- package/dist/files.d.ts +31 -0
- package/dist/files.js +60 -0
- package/dist/http.d.ts +0 -5
- package/dist/http.js +0 -8
- package/dist/index.d.ts +8 -13
- package/dist/index.js +4 -6
- package/dist/plugins/load.d.ts +3 -2
- package/dist/plugins/load.js +40 -100
- package/dist/plugins/plugin.d.ts +6 -1
- package/dist/plugins/plugin.js +20 -7
- package/dist/reads.d.ts +17 -0
- package/dist/reads.js +17 -0
- package/dist/sandboxes.d.ts +29 -0
- package/dist/sandboxes.js +10 -1
- package/dist/session-client.d.ts +91 -114
- package/dist/session-client.js +79 -109
- package/dist/skills/index.d.ts +1 -1
- package/dist/skills/load.d.ts +14 -3
- package/dist/skills/load.js +50 -32
- package/dist/skills/skills.d.ts +6 -4
- package/dist/skills/skills.js +17 -10
- package/package.json +3 -3
- package/dist/action-handler.d.ts +0 -63
- package/dist/action-handler.js +0 -279
- package/dist/delivery-token.d.ts +0 -62
- package/dist/delivery-token.js +0 -159
- package/dist/execute-action.d.ts +0 -16
- package/dist/execute-action.js +0 -288
- package/dist/plugins/launch.d.ts +0 -17
- package/dist/plugins/launch.js +0 -71
- package/dist/sandbox/client.d.ts +0 -63
- package/dist/sandbox/client.js +0 -29
- package/dist/sandbox/index.d.ts +0 -2
- package/dist/sandbox/index.js +0 -1
- package/dist/served-definitions.d.ts +0 -20
- package/dist/served-definitions.js +0 -100
package/dist/skills/load.js
CHANGED
|
@@ -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 (
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
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
|
|
136
|
+
if (!real) {
|
|
137
137
|
diagnostics.push({
|
|
138
138
|
severity: "warning",
|
|
139
139
|
code: `${prefix}.resource-denied`,
|
|
140
|
-
message: `Denied
|
|
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
|
|
146
|
-
|
|
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
|
-
|
|
149
|
-
if (!
|
|
151
|
+
}
|
|
152
|
+
if (!stats.isFile() || SKIPPED_FILES.has(entry.name) || isEnvFile(entry.name))
|
|
150
153
|
continue;
|
|
151
|
-
|
|
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,
|
|
155
|
-
return
|
|
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))
|
package/dist/skills/skills.d.ts
CHANGED
|
@@ -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
|
|
25
|
-
*
|
|
26
|
-
* `load_skill` / `read_skill_resource` from
|
|
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">>;
|
package/dist/skills/skills.js
CHANGED
|
@@ -13,23 +13,20 @@ import { loadSkillsFromDirectory, resolveSkillsRoot, } from "./load.js";
|
|
|
13
13
|
* references/policy.md
|
|
14
14
|
* ```
|
|
15
15
|
*
|
|
16
|
-
* Sets
|
|
17
|
-
*
|
|
18
|
-
* `load_skill` / `read_skill_resource` from
|
|
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
|
|
24
|
-
const listed = Object.values(
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
4
|
-
"description": "Nylorun agent definitions
|
|
3
|
+
"version": "0.16.0-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.
|
|
46
|
+
"@nylorun/core": "0.15.0-beta",
|
|
47
47
|
"gray-matter": "^4.0.3"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
package/dist/action-handler.d.ts
DELETED
|
@@ -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;
|
package/dist/action-handler.js
DELETED
|
@@ -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
|
-
}
|
package/dist/delivery-token.d.ts
DELETED
|
@@ -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
|
-
}
|