@nylorun/agents 0.23.0-beta → 0.25.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/dist/agents.js ADDED
@@ -0,0 +1,331 @@
1
+ /**
2
+ * Agents (protocol 16): `client.agents`. An Agent is the live instance of one version of an agent
3
+ * manifest (`client.agentManifests`): one environment of the manifest's runtime kind (`virtual`,
4
+ * `pod` or `none`), one vault of secrets for its shell, MCP servers and HTTP tools, and the
5
+ * sessions that run on it. It is made only from a saved manifest, `name` (tracking) or `name@n`
6
+ * (pinned), and runs one version of it at a time: `put` with another version moves it, and a save
7
+ * moves every Agent that tracks the manifest. Its manifest, kind, harness and storage
8
+ * are fixed (`409 agent_fixed_field`): `fork` makes a new Agent to change them. It has
9
+ * its own id and outlives its sessions; whether it serves one person or a project is the app's
10
+ * choice, made by its id:
11
+ *
12
+ * - `agents.put("users/alice/triage", { manifest: "triage", owner: "alice" })`: get-or-create
13
+ * in one call. Write its secrets with `agents.secrets.put(id, name, { value })` and open
14
+ * sessions on it with `createSession({ agent: id, ... })`. Creating it opens no session.
15
+ *
16
+ * Ids may hold `/`; they are sent percent-encoded as one path segment. Secret values are
17
+ * write-only: no call reads one back.
18
+ *
19
+ * Triggers: what its manifest's `runtime.triggers` declares starts its turns with
20
+ * no client connected. `triggers(id)` lists them with their status and next run, `pauseTrigger`,
21
+ * `resumeTrigger` and `runTrigger` change or run one, `rotateWebhookSecret` makes a webhook
22
+ * trigger's signing secret, and `runs(id)` pages what each fire came to.
23
+ *
24
+ * Memory: an Agent whose manifest sets `harness.memory` keeps Markdown files under
25
+ * `/memories` across sessions. `agents.memories` lists, reads, writes (to seed it), deletes and
26
+ * pages the versions every change leaves.
27
+ *
28
+ * Disk versions (Host feature `environment-disk-versions`): `snapshot(id)` keeps a version of
29
+ * the Agent's disk between turns, `restore(id, version)` puts one back, and
30
+ * `put(id, { manifest, from: { agent, version } })` creates a new Agent from another's version.
31
+ */
32
+ import { AgentPageSchema, isAgentId, } from "@nylorun/core/contracts";
33
+ import { definitionFileHash } from "./files.js";
34
+ import { id, segment } from "./http.js";
35
+ /**
36
+ * A runtime section's `files` entry by content hash: `contents` (each file's path under
37
+ * `path` and its bytes or text) hashed, as `entry` for the manifest's `runtime.files` and `bytes`
38
+ * for `agentManifests.save`'s `files`, which uploads them. Each file holds at most 2 GiB.
39
+ */
40
+ export async function runtimeFiles(path, contents) {
41
+ const files = {};
42
+ const bytes = {};
43
+ for (const [name, content] of Object.entries(contents).sort(([a], [b]) => a.localeCompare(b))) {
44
+ const data = typeof content === "string" ? new TextEncoder().encode(content) : content;
45
+ const sha256 = await definitionFileHash(data);
46
+ files[name] = sha256;
47
+ bytes[sha256] = data;
48
+ }
49
+ return { entry: { path, files }, bytes };
50
+ }
51
+ function filterQuery(filter, query = new URLSearchParams()) {
52
+ for (const [key, value] of Object.entries(filter.labels ?? {}))
53
+ query.append("label", `${key}=${value}`);
54
+ if (filter.state !== undefined)
55
+ query.set("state", filter.state);
56
+ if (filter.manifest !== undefined)
57
+ query.set("manifest", filter.manifest);
58
+ if (filter.outdated !== undefined)
59
+ query.set("outdated", String(filter.outdated));
60
+ return query;
61
+ }
62
+ /** An Agent's secrets: `client.agents.secrets`. */
63
+ export class AgentSecretsClient {
64
+ transport;
65
+ path;
66
+ constructor(transport, path) {
67
+ this.transport = transport;
68
+ this.path = path;
69
+ }
70
+ /**
71
+ * Creates or replaces secret `name` (`PUT /v1/agents/{id}/secrets/{name}`): for
72
+ * `environment_secret` and `environment_variable` the name is the variable's. A second
73
+ * credential for a host or a URL is a 409 `credential_conflict`. When the Agent's manifest
74
+ * declares the secret's slot, send only `{ value }` (or `{ headers }`); a name it does
75
+ * not declare is `409 secret_not_in_manifest`.
76
+ */
77
+ async put(agentId, name, secret, options = {}) {
78
+ return this.transport.json(`${this.path(agentId)}/secrets/${segment(name)}`, "PUT", { requestId: id(), ...secret }, options.signal);
79
+ }
80
+ /** The secrets' names, types and bindings; never a value. */
81
+ async list(agentId, options = {}) {
82
+ const reply = await this.transport.json(`${this.path(agentId)}/secrets`, "GET", undefined, options.signal);
83
+ return reply.secrets;
84
+ }
85
+ async get(agentId, name, options = {}) {
86
+ return this.transport.json(`${this.path(agentId)}/secrets/${segment(name)}`, "GET", undefined, options.signal);
87
+ }
88
+ async delete(agentId, name, options = {}) {
89
+ return this.transport.json(`${this.path(agentId)}/secrets/${segment(name)}`, "DELETE", undefined, options.signal);
90
+ }
91
+ }
92
+ /**
93
+ * An Agent's memories: `client.agents.memories`. A path is a memory's path under
94
+ * `/memories` (`conventions/release.md` or `/memories/conventions/release.md`).
95
+ */
96
+ export class AgentMemoriesClient {
97
+ transport;
98
+ path;
99
+ constructor(transport, path) {
100
+ this.transport = transport;
101
+ this.path = path;
102
+ }
103
+ memoryPath(agentId, path) {
104
+ const relative = path.replace(/^\/+/, "").replace(/^memories\//, "");
105
+ return `${this.path(agentId)}/memories/${segment(relative)}`;
106
+ }
107
+ /** The Agent's memories by path; `pathPrefix` keeps one folder's. */
108
+ async list(agentId, options = {}) {
109
+ const query = new URLSearchParams({ limit: String(options.limit ?? 100) });
110
+ if (options.pathPrefix !== undefined)
111
+ query.set("pathPrefix", options.pathPrefix);
112
+ if (options.cursor !== undefined)
113
+ query.set("cursor", options.cursor);
114
+ return this.transport.json(`${this.path(agentId)}/memories?${query}`, "GET", undefined, options.signal);
115
+ }
116
+ /** One memory with its content. */
117
+ async get(agentId, path, options = {}) {
118
+ return this.transport.json(this.memoryPath(agentId, path), "GET", undefined, options.signal);
119
+ }
120
+ /**
121
+ * Writes the memory whole, creating or replacing it: how an app seeds an Agent's memory. With
122
+ * `contentSha256` the write applies only while the memory still has that content (`""`: only
123
+ * while there is none); otherwise `409 memory_conflict`.
124
+ */
125
+ async put(agentId, path, content, options = {}) {
126
+ return this.transport.json(this.memoryPath(agentId, path), "PUT", { content, ...(options.contentSha256 === undefined ? {} : { contentSha256: options.contentSha256 }) }, options.signal);
127
+ }
128
+ /** Deletes a memory, or every memory in a folder. */
129
+ async delete(agentId, path, options = {}) {
130
+ return this.transport.json(this.memoryPath(agentId, path), "DELETE", undefined, options.signal);
131
+ }
132
+ /** Every change to the Agent's memories, newest first; `path` keeps one memory's. */
133
+ async versions(agentId, options = {}) {
134
+ const query = new URLSearchParams({ limit: String(options.limit ?? 50) });
135
+ if (options.path !== undefined)
136
+ query.set("path", options.path);
137
+ if (options.cursor !== undefined)
138
+ query.set("cursor", options.cursor);
139
+ return this.transport.json(`${this.path(agentId)}/memory_versions?${query}`, "GET", undefined, options.signal);
140
+ }
141
+ /** One version with the content it left (`null` for a delete). */
142
+ async version(agentId, versionId, options = {}) {
143
+ return this.transport.json(`${this.path(agentId)}/memory_versions/${segment(versionId)}`, "GET", undefined, options.signal);
144
+ }
145
+ }
146
+ /** The Tenant's Agents: `client.agents`. */
147
+ export class AgentInstancesClient {
148
+ transport;
149
+ constructor(transport) {
150
+ this.transport = transport;
151
+ }
152
+ path(agentId) {
153
+ if (!isAgentId(agentId))
154
+ throw new TypeError(`${agentId} is not an Agent id: up to 200 characters, /-separated segments of letters, digits, '.', '_' and '-'`);
155
+ return `/v1/agents/${segment(agentId)}`;
156
+ }
157
+ /** The Agent's secrets. */
158
+ get secrets() {
159
+ return new AgentSecretsClient(this.transport, (agentId) => this.path(agentId));
160
+ }
161
+ /** The Agent's memories. */
162
+ get memories() {
163
+ return new AgentMemoriesClient(this.transport, (agentId) => this.path(agentId));
164
+ }
165
+ /**
166
+ * Creates the Agent, its vault and its compute from a saved agent manifest, or finds the one
167
+ * with this id (`PUT /v1/agents/{id}`): `{ manifest: "name" }` tracks the manifest (its latest
168
+ * version when the Agent is made), `"name@n"` pins version n. On an existing Agent, labels
169
+ * replace, `owner` sets or clears the owner guard, and `manifest` moves it to that version of
170
+ * its manifest: at once, at its pod's next start for a runtime change (`targetVersion`,
171
+ * `waitingFor: "start"`), or once a required secret the version adds is set. Another manifest,
172
+ * kind, harness or storage is `409 agent_fixed_field` (fork it instead).
173
+ */
174
+ async put(agentId, input, options = {}) {
175
+ return this.transport.json(this.path(agentId), "PUT", { requestId: id(), ...input }, options.signal);
176
+ }
177
+ /**
178
+ * What `put` would do to the existing Agent (`PUT /v1/agents/{id}?dryRun=true`): the
179
+ * version it runs and the one `input.manifest` names, when that applies (`now`, `next_start`,
180
+ * `secrets`, or `refused` with each fixed field), the changes, and the secrets it would wait
181
+ * for or leave unused. Nothing changes.
182
+ */
183
+ async plan(agentId, input, options = {}) {
184
+ const reply = await this.transport.json(`${this.path(agentId)}?dryRun=true`, "PUT", { requestId: id(), ...input }, options.signal);
185
+ return reply.plan;
186
+ }
187
+ async get(agentId, options = {}) {
188
+ return this.transport.json(this.path(agentId), "GET", undefined, options.signal);
189
+ }
190
+ /** The Agents this client reaches with every label in `labels`, and the `state` or `manifest` given. */
191
+ async list(options = {}) {
192
+ const search = filterQuery(options).toString();
193
+ const reply = await this.transport.json(`/v1/agents${search ? `?${search}` : ""}`, "GET", undefined, options.signal);
194
+ return reply.agents;
195
+ }
196
+ /** One page of `list` (Host feature `session-reads`). */
197
+ async page(options = {}) {
198
+ await this.transport.requireFeature("session-reads", options.signal);
199
+ const query = new URLSearchParams({ limit: String(options.limit ?? 50) });
200
+ if (options.cursor !== undefined)
201
+ query.set("cursor", options.cursor);
202
+ filterQuery(options, query);
203
+ return AgentPageSchema.parse(await this.transport.json(`/v1/agents?${query}`, "GET", undefined, options.signal));
204
+ }
205
+ /** Stops a pod Agent's compute: its volume is kept, and the next turn starts it again. */
206
+ async stop(agentId, options = {}) {
207
+ return this.transport.json(`${this.path(agentId)}/stop`, "POST", {}, options.signal);
208
+ }
209
+ /**
210
+ * Applies a version the Agent waits to apply (`POST /v1/agents/{id}/restart`): a
211
+ * running pod stops, so the next turn starts it on the version; a stopped one starts at once.
212
+ * `retry` first moves it to the version that failed to start again. Refused during a turn.
213
+ */
214
+ async restart(agentId, options = {}) {
215
+ return this.transport.json(`${this.path(agentId)}/restart`, "POST", { requestId: id(), ...(options.retry ? { retry: true } : {}) }, options.signal);
216
+ }
217
+ /** Resets a pod Agent: a new pod on a new, empty volume, with its runtime section applied. */
218
+ async reset(agentId, options = {}) {
219
+ return this.transport.json(`${this.path(agentId)}/reset`, "POST", {}, options.signal);
220
+ }
221
+ /**
222
+ * Keeps a version of the Agent's disk (`POST /v1/agents/{id}/snapshot`), between turns;
223
+ * `unchanged` when the disk is as the latest version has it.
224
+ */
225
+ async snapshot(agentId, options = {}) {
226
+ await this.transport.requireFeature("environment-disk-versions", options.signal);
227
+ return this.transport.json(`${this.path(agentId)}/snapshot`, "POST", {}, options.signal);
228
+ }
229
+ /**
230
+ * Puts disk version `version` back: at once for a virtual Agent (`done`), on a new volume
231
+ * seeded before the next turn for a pod (`pending`).
232
+ */
233
+ async restore(agentId, version, options = {}) {
234
+ await this.transport.requireFeature("environment-disk-versions", options.signal);
235
+ return this.transport.json(`${this.path(agentId)}/restore`, "POST", { requestId: id(), version }, options.signal);
236
+ }
237
+ /**
238
+ * Forks the Agent into a new one, `options.id`: how an Agent changes what is fixed for its
239
+ * life (its manifest, kind, harness or storage). It is made from `options.manifest` (default the source's manifest,
240
+ * tracking), its disk starts from `options.disk` (`latest`, the default, snapshots first; a
241
+ * version; or `empty`) and the secrets the new manifest declares are copied
242
+ * (`options.secrets: "none"` copies none). The source keeps what it runs, its disk and its
243
+ * sessions. Answers the new Agent and the plan it ran.
244
+ */
245
+ async fork(agentId, options) {
246
+ const { signal, ...body } = options;
247
+ return this.transport.json(`${this.path(agentId)}/fork`, "POST", { requestId: id(), ...body }, signal);
248
+ }
249
+ /** What `fork` would do (the disk, the secrets copied and left behind, the changes), making nothing. */
250
+ async planFork(agentId, options) {
251
+ const { signal, ...body } = options;
252
+ const answer = await this.transport.json(`${this.path(agentId)}/fork`, "POST", { ...body, dryRun: true }, signal);
253
+ return answer.plan;
254
+ }
255
+ /** The Agent's disk versions, newest first. */
256
+ async versions(agentId, options = {}) {
257
+ await this.transport.requireFeature("environment-disk-versions", options.signal);
258
+ return this.transport.json(`${this.path(agentId)}/versions`, "GET", undefined, options.signal);
259
+ }
260
+ /** Deletes disk version `version`; `deleted` is false when there was none. */
261
+ async deleteVersion(agentId, version, options = {}) {
262
+ return this.transport.json(`${this.path(agentId)}/versions/${version}`, "DELETE", undefined, options.signal);
263
+ }
264
+ /** Deletes the Agent, its vault and its files; `deleted` is false when there was none. */
265
+ async delete(agentId, options = {}) {
266
+ return this.transport.json(this.path(agentId), "DELETE", undefined, options.signal);
267
+ }
268
+ /**
269
+ * The Agent's events, from `from` on: its lifecycle, and `agent.egress` for every connection
270
+ * its sandbox opens (with the turns that were running). `types` keeps only those types;
271
+ * `since` and `until` keep events recorded in that window (ISO times or `Date`s).
272
+ */
273
+ async events(agentId, options = {}) {
274
+ const query = new URLSearchParams();
275
+ if (options.from !== undefined)
276
+ query.set("from", String(options.from));
277
+ if (options.types !== undefined && options.types.length > 0)
278
+ query.set("types", options.types.join(","));
279
+ const iso = (at) => (at instanceof Date ? at.toISOString() : at);
280
+ if (options.since !== undefined)
281
+ query.set("since", iso(options.since));
282
+ if (options.until !== undefined)
283
+ query.set("until", iso(options.until));
284
+ const search = query.toString();
285
+ const reply = await this.transport.json(`${this.path(agentId)}/events${search === "" ? "" : `?${search}`}`, "GET", undefined, options.signal);
286
+ return reply.events;
287
+ }
288
+ /** The Agent's triggers: each one's status (`active`, `paused`, `completed`), next and last run. */
289
+ async triggers(agentId, options = {}) {
290
+ const reply = await this.transport.json(`${this.path(agentId)}/triggers`, "GET", undefined, options.signal);
291
+ return reply.triggers;
292
+ }
293
+ /** Pauses trigger `triggerId`: it stops firing until it is resumed. Answers the Agent's triggers. */
294
+ async pauseTrigger(agentId, triggerId, options = {}) {
295
+ const reply = await this.transport.json(`${this.path(agentId)}/triggers/${segment(triggerId)}/pause`, "POST", {}, options.signal);
296
+ return reply.triggers;
297
+ }
298
+ /** Resumes trigger `triggerId` from its next time after now. Answers the Agent's triggers. */
299
+ async resumeTrigger(agentId, triggerId, options = {}) {
300
+ const reply = await this.transport.json(`${this.path(agentId)}/triggers/${segment(triggerId)}/resume`, "POST", {}, options.signal);
301
+ return reply.triggers;
302
+ }
303
+ /**
304
+ * Runs trigger `triggerId` once now, as if it fired: `idempotencyKey` (default a new one) makes a
305
+ * retried call answer the same run.
306
+ */
307
+ async runTrigger(agentId, triggerId, options = {}) {
308
+ const response = await this.transport.request(`${this.path(agentId)}/triggers/${segment(triggerId)}/run`, {
309
+ method: "POST",
310
+ headers: { "idempotency-key": options.idempotencyKey ?? id() },
311
+ ...(options.signal ? { signal: options.signal } : {}),
312
+ });
313
+ return (await response.json()).run;
314
+ }
315
+ /**
316
+ * A new Standard Webhooks signing secret for webhook trigger `triggerId` (`whsec_…`), shown this
317
+ * once with the trigger's `webhookUrl`; the one before stops working at once.
318
+ */
319
+ async rotateWebhookSecret(agentId, triggerId, options = {}) {
320
+ return this.transport.json(`${this.path(agentId)}/triggers/${segment(triggerId)}/secret`, "POST", {}, options.signal);
321
+ }
322
+ /** The Agent's runs, newest first; `triggerId` keeps one trigger's. */
323
+ async runs(agentId, options = {}) {
324
+ const query = new URLSearchParams({ limit: String(options.limit ?? 50) });
325
+ if (options.triggerId !== undefined)
326
+ query.set("triggerId", options.triggerId);
327
+ if (options.cursor !== undefined)
328
+ query.set("cursor", options.cursor);
329
+ return this.transport.json(`${this.path(agentId)}/runs?${query}`, "GET", undefined, options.signal);
330
+ }
331
+ }
@@ -5,7 +5,7 @@
5
5
  * short-lived URL that needs no credential, for `<img>` tags and sharing. A message names an
6
6
  * artifact in a file part (`session.inputParts`), and the model reads it.
7
7
  *
8
- * Folders (F8.2): at the end of each turn the Runtime exports `/workspace/outputs` of the
8
+ * Folders: at the end of each turn the Runtime exports `/workspace/outputs` of the
9
9
  * session's sandbox as a version of its `outputs` folder. Read one as a tree, one file by path,
10
10
  * a diff between versions or a zip.
11
11
  *
package/dist/artifacts.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * short-lived URL that needs no credential, for `<img>` tags and sharing. A message names an
6
6
  * artifact in a file part (`session.inputParts`), and the model reads it.
7
7
  *
8
- * Folders (F8.2): at the end of each turn the Runtime exports `/workspace/outputs` of the
8
+ * Folders: at the end of each turn the Runtime exports `/workspace/outputs` of the
9
9
  * session's sandbox as a version of its `outputs` folder. Read one as a tree, one file by path,
10
10
  * a diff between versions or a zip.
11
11
  *
@@ -0,0 +1,23 @@
1
+ import { type ComposedManifest } from "./agent-manifests.js";
2
+ /** The file `composeManifest` reads in a folder. */
3
+ export declare const AGENT_MANIFEST_FILE = "agent.manifest.json";
4
+ /** An authoring file that cannot be read or does not validate; `path` names it. */
5
+ export declare class ComposeError extends Error {
6
+ readonly path: string;
7
+ constructor(path: string, message: string);
8
+ }
9
+ /**
10
+ * Composes the agent manifest in `dir` (its `agent.manifest.json`, or `options.file`): the v8
11
+ * document to save, the bytes of every file it names by hash (skills, plugins' skills, runtime
12
+ * files) for `client.agentManifests.save` to upload, its gaps (the harness file's `requires`
13
+ * the runtime misses, and `sectionGaps`, which the Runtime would refuse it for), and the skills
14
+ * and servers skipped. Throws `ComposeError` for a file that cannot be read or does not validate.
15
+ */
16
+ export declare function composeManifest(dir: string, options?: {
17
+ file?: string;
18
+ }): Promise<ComposedManifest>;
19
+ /**
20
+ * The folders `nylorun apply` composes for `dir`: `dir` itself when it holds an
21
+ * `agent.manifest.json`, else each of its subfolders that does, by name.
22
+ */
23
+ export declare function manifestFolders(dir: string): Promise<string[]>;
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Three-file authoring (protocol 16): `composeManifest(dir)` reads `agent.manifest.json`
3
+ * and the files it names, and makes the one agent manifest v8 document the Runtime saves, with
4
+ * the bytes of every file it names by hash. `nylorun apply` runs it for each folder.
5
+ *
6
+ * - `agent.manifest.json` (`AgentManifestFileSchema`): the manifest's name, description and
7
+ * metadata, with `harness` and `runtime` inline or as paths to the two files below.
8
+ * - `harness.manifest.json` (`HarnessManifestFileSchema`): the harness section, whose `skills`
9
+ * may be a folder of skill folders (each skill's `secrets` from its SKILL.md's frontmatter),
10
+ * `mcpServers` an `mcp.json` (the Agent Plugins format), and
11
+ * `plugins` Agent Plugin folders, folded in as `.plugin()` folds them; `credentials` names the
12
+ * `runtime.secrets` slot each MCP server or HTTP tool authenticates with; `requires` is checked
13
+ * against the runtime section here, since the API never sees it.
14
+ * - `runtime.manifest.json` (`RuntimeManifestFileSchema`): the runtime section, often shared by
15
+ * many agents; a `files` entry `{ path, from }` is a folder beside the file, hashed into
16
+ * `{ path, files }`.
17
+ *
18
+ * Each path is relative to the file that names it. Node only: it reads the files.
19
+ */
20
+ import { existsSync, statSync } from "node:fs";
21
+ import { readdir, readFile } from "node:fs/promises";
22
+ import { dirname, join, relative, resolve } from "node:path";
23
+ import { AGENT_MANIFEST_V8_SCHEMA_VERSION, AgentManifestFileSchema, AgentManifestV8Schema, HarnessManifestFileSchema, RuntimeManifestFileSchema, WorkflowHarnessSectionSchema, } from "@nylorun/core/contracts";
24
+ import { sectionGaps } from "@nylorun/core/define";
25
+ import { requirementGaps } from "./agent-manifests.js";
26
+ import { loadMcpServers, loadPlugin } from "./plugins/load.js";
27
+ import { hashFolder, loadSkillsFromDirectory } from "./skills/load.js";
28
+ /** The file `composeManifest` reads in a folder. */
29
+ export const AGENT_MANIFEST_FILE = "agent.manifest.json";
30
+ /** An authoring file that cannot be read or does not validate; `path` names it. */
31
+ export class ComposeError extends Error {
32
+ path;
33
+ constructor(path, message) {
34
+ super(message);
35
+ this.path = path;
36
+ this.name = "ComposeError";
37
+ }
38
+ }
39
+ /** The JSON file at `path`, as `schema` parses it, or a `ComposeError` naming every issue. */
40
+ async function readFileAs(path, schema, where) {
41
+ let text;
42
+ try {
43
+ text = await readFile(path, "utf8");
44
+ }
45
+ catch (error) {
46
+ throw new ComposeError(path, `Cannot read ${where}: ${error.code ?? error}`);
47
+ }
48
+ let value;
49
+ try {
50
+ value = JSON.parse(text);
51
+ }
52
+ catch (error) {
53
+ throw new ComposeError(path, `${where} is not JSON: ${error.message}`);
54
+ }
55
+ return parsedAs(value, schema, path, where);
56
+ }
57
+ function parsedAs(value, schema, path, where) {
58
+ const parsed = schema.safeParse(value);
59
+ if (parsed.success)
60
+ return parsed.data;
61
+ const issues = parsed.error.issues.map((issue) => ` ${issue.path.length ? `${issue.path.map(String).join(".")}: ` : ""}${issue.message}`);
62
+ throw new ComposeError(path, `${where} is not valid:\n${issues.join("\n")}`);
63
+ }
64
+ const isWorkflow = (value) => !!value && typeof value === "object" && value.kind === "workflow";
65
+ /** `$schema` dropped: it is for editors. */
66
+ function withoutSchema(value) {
67
+ const { $schema: _, ...rest } = value;
68
+ return rest;
69
+ }
70
+ /** Reads the runtime section, its `{ path, from }` files hashed into `{ path, files }`. */
71
+ async function runtimeSection(runtime, base, agentFile, files, warnings) {
72
+ const file = typeof runtime === "string" ? resolve(base, runtime) : agentFile;
73
+ const parsed = typeof runtime === "string"
74
+ ? await readFileAs(file, RuntimeManifestFileSchema, relativeTo(file))
75
+ : parsedAs(runtime, RuntimeManifestFileSchema, file, `${relativeTo(file)} runtime`);
76
+ const at = dirname(file);
77
+ const entries = [];
78
+ for (const entry of parsed.files ?? []) {
79
+ if (!("from" in entry)) {
80
+ entries.push(entry);
81
+ continue;
82
+ }
83
+ const folder = resolve(at, entry.from);
84
+ if (!existsSync(folder) || !statSync(folder).isDirectory())
85
+ throw new ComposeError(file, `runtime.files: ${entry.from} (for ${entry.path}) is not a folder beside ${relativeTo(file)}`);
86
+ const diagnostics = [];
87
+ const hashed = hashFolder(folder, diagnostics);
88
+ warnings.push(...warningsOf(diagnostics));
89
+ if (Object.keys(hashed.files).length === 0)
90
+ throw new ComposeError(file, `runtime.files: ${entry.from} (for ${entry.path}) holds no files`);
91
+ for (const [sha256, source] of Object.entries(hashed.sources))
92
+ files.set(sha256, source);
93
+ entries.push({ path: entry.path, files: { ...hashed.files } });
94
+ }
95
+ const section = withoutSchema(parsed);
96
+ return { ...section, ...(parsed.files === undefined ? {} : { files: entries }) };
97
+ }
98
+ function warningsOf(diagnostics) {
99
+ return diagnostics.filter((item) => item.severity === "warning").map((item) => item.message);
100
+ }
101
+ /** `path` relative to the working directory, as a person reads it. */
102
+ function relativeTo(path) {
103
+ return relative(process.cwd(), path) || path;
104
+ }
105
+ /** A skill read from its folder as the harness section declares it, with its SKILL.md's `secrets`. */
106
+ function harnessSkill(skill) {
107
+ return {
108
+ name: skill.name,
109
+ description: skill.description,
110
+ files: { ...skill.files },
111
+ ...(skill.secrets === undefined ? {} : { secrets: [...skill.secrets] }),
112
+ };
113
+ }
114
+ /** The agent harness section a `harness.manifest.json` describes, its paths resolved. */
115
+ function agentHarness(file, base, where, files, warnings) {
116
+ const skills = {};
117
+ const servers = {};
118
+ const plugins = [];
119
+ const addSkill = (skill, from) => {
120
+ if (skills[skill.name])
121
+ throw new ComposeError(where, `Skill ${skill.name} of ${from} is declared twice`);
122
+ skills[skill.name] = skill;
123
+ };
124
+ const addServer = (key, server, from) => {
125
+ if (servers[key])
126
+ throw new ComposeError(where, `MCP server ${key} of ${from} is declared twice`);
127
+ servers[key] = server;
128
+ };
129
+ // Skills: a folder of skill folders, each hashed as `.skills()` hashes it.
130
+ if (typeof file.skills === "string") {
131
+ const diagnostics = [];
132
+ for (const skill of Object.values(loadSkillsFromDirectory(resolve(base, file.skills), diagnostics))) {
133
+ addSkill(harnessSkill(skill), file.skills);
134
+ for (const [sha256, source] of Object.entries(skill.sources))
135
+ files.set(sha256, source);
136
+ }
137
+ warnings.push(...warningsOf(diagnostics));
138
+ }
139
+ else
140
+ for (const skill of Object.values(file.skills ?? {}))
141
+ addSkill(skill, where);
142
+ // MCP servers: an `mcp.json` in the Agent Plugins format.
143
+ if (typeof file.mcpServers === "string") {
144
+ const path = resolve(base, file.mcpServers);
145
+ if (!existsSync(path))
146
+ throw new ComposeError(where, `mcpServers: ${file.mcpServers} does not exist`);
147
+ const diagnostics = [];
148
+ const loaded = loadMcpServers(path, diagnostics, { codePrefix: "harness" });
149
+ const invalid = diagnostics.find((item) => item.code === "harness.mcp-invalid");
150
+ if (invalid)
151
+ throw new ComposeError(path, `mcpServers: ${invalid.message}`);
152
+ warnings.push(...warningsOf(diagnostics));
153
+ for (const [key, server] of Object.entries(loaded))
154
+ addServer(key, server, file.mcpServers);
155
+ }
156
+ else
157
+ for (const [key, server] of Object.entries(file.mcpServers ?? {}))
158
+ addServer(key, server, where);
159
+ // Plugins: a folder is folded in as `.plugin()` folds it, each part labelled with the plugin.
160
+ for (const entry of file.plugins ?? []) {
161
+ if (typeof entry !== "string") {
162
+ plugins.push(entry);
163
+ continue;
164
+ }
165
+ const loaded = loadPlugin(resolve(base, entry));
166
+ warnings.push(...warningsOf(loaded.diagnostics).map((message) => `Plugin '${loaded.name}': ${message}`));
167
+ const label = { name: loaded.name, ...(loaded.version === undefined ? {} : { version: loaded.version }) };
168
+ for (const skill of Object.values(loaded.skills)) {
169
+ addSkill({ ...harnessSkill(skill), plugin: label }, entry);
170
+ for (const [sha256, source] of Object.entries(skill.sources))
171
+ files.set(sha256, source);
172
+ }
173
+ for (const [key, server] of Object.entries(loaded.mcpServers))
174
+ addServer(key, { ...server, plugin: label }, entry);
175
+ plugins.push(label);
176
+ }
177
+ // Credentials: the slot each server or HTTP tool authenticates with.
178
+ let tools = file.tools;
179
+ for (const [name, slot] of Object.entries(file.credentials ?? {})) {
180
+ const server = servers[name];
181
+ const tool = tools?.find((item) => item.name === name && item.http !== undefined);
182
+ if (!server && !tool)
183
+ throw new ComposeError(where, `credentials names ${name}, which is no MCP server or HTTP tool of the harness`);
184
+ if (server)
185
+ servers[name] = { ...server, credential: slot };
186
+ if (tool)
187
+ tools = tools.map((item) => (item === tool ? { ...item, http: { ...item.http, credential: slot } } : item));
188
+ }
189
+ return {
190
+ ...(file.id === undefined ? {} : { id: file.id }),
191
+ ...(file.instructions === undefined ? {} : { instructions: file.instructions }),
192
+ ...(tools === undefined ? {} : { tools }),
193
+ ...(Object.keys(skills).length === 0 ? {} : { skills }),
194
+ ...(Object.keys(servers).length === 0 ? {} : { mcpServers: servers }),
195
+ ...(plugins.length === 0 ? {} : { plugins }),
196
+ ...(file.outputSchema === undefined ? {} : { outputSchema: file.outputSchema }),
197
+ ...(file.loop === undefined ? {} : { loop: file.loop }),
198
+ ...(file.memory === undefined ? {} : { memory: file.memory }),
199
+ };
200
+ }
201
+ /**
202
+ * Composes the agent manifest in `dir` (its `agent.manifest.json`, or `options.file`): the v8
203
+ * document to save, the bytes of every file it names by hash (skills, plugins' skills, runtime
204
+ * files) for `client.agentManifests.save` to upload, its gaps (the harness file's `requires`
205
+ * the runtime misses, and `sectionGaps`, which the Runtime would refuse it for), and the skills
206
+ * and servers skipped. Throws `ComposeError` for a file that cannot be read or does not validate.
207
+ */
208
+ export async function composeManifest(dir, options = {}) {
209
+ const agentFile = resolve(dir, options.file ?? AGENT_MANIFEST_FILE);
210
+ const base = dirname(agentFile);
211
+ const authored = await readFileAs(agentFile, AgentManifestFileSchema, relativeTo(agentFile));
212
+ const files = new Map();
213
+ const warnings = [];
214
+ const runtime = authored.runtime === undefined ? undefined : await runtimeSection(authored.runtime, base, agentFile, files, warnings);
215
+ let harness;
216
+ let requires;
217
+ const harnessFile = typeof authored.harness === "string" ? resolve(base, authored.harness) : agentFile;
218
+ const where = relativeTo(harnessFile);
219
+ const raw = typeof authored.harness === "string"
220
+ ? await readFileAs(harnessFile, { safeParse: (value) => ({ success: true, data: value }) }, where)
221
+ : authored.harness;
222
+ if (isWorkflow(raw)) {
223
+ // A flow: its agents are embedded, with their skills by hash; nothing to resolve.
224
+ harness = parsedAs(withoutSchema(raw), WorkflowHarnessSectionSchema, harnessFile, where);
225
+ }
226
+ else {
227
+ const file = parsedAs(raw, HarnessManifestFileSchema, harnessFile, where);
228
+ requires = file.requires;
229
+ harness = agentHarness(file, dirname(harnessFile), where, files, warnings);
230
+ }
231
+ const manifest = parsedAs({
232
+ manifestSchemaVersion: AGENT_MANIFEST_V8_SCHEMA_VERSION,
233
+ name: authored.name,
234
+ ...(authored.description === undefined ? {} : { description: authored.description }),
235
+ ...(authored.metadata === undefined ? {} : { metadata: authored.metadata }),
236
+ harness,
237
+ ...(runtime === undefined ? {} : { runtime }),
238
+ }, AgentManifestV8Schema, agentFile, `The agent manifest ${authored.name}`);
239
+ const gaps = [...requirementGaps(requires, manifest.runtime).map((gap) => `harness.${gap}`), ...sectionGaps(manifest)];
240
+ return { manifest, files, gaps, warnings };
241
+ }
242
+ /**
243
+ * The folders `nylorun apply` composes for `dir`: `dir` itself when it holds an
244
+ * `agent.manifest.json`, else each of its subfolders that does, by name.
245
+ */
246
+ export async function manifestFolders(dir) {
247
+ const root = resolve(dir);
248
+ if (existsSync(join(root, AGENT_MANIFEST_FILE)))
249
+ return [root];
250
+ const entries = await readdir(root, { withFileTypes: true }).catch((error) => {
251
+ throw new ComposeError(root, `Cannot read ${dir}: ${error.code ?? error}`);
252
+ });
253
+ return entries
254
+ .filter((entry) => entry.isDirectory() && existsSync(join(root, entry.name, AGENT_MANIFEST_FILE)))
255
+ .map((entry) => join(root, entry.name))
256
+ .sort();
257
+ }
package/dist/files.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Definition files (track R2 M4): `client.files`. A definition names the files it needs by the
2
+ * Definition files: `client.files`. A definition names the files it needs by the
3
3
  * SHA-256 of their bytes (a skill's folder, `SkillManifest.files`); the Runtime holds each one
4
- * once, uploaded with `PUT /v1/files/sha256:<hex>`, and refuses a definition naming a file it
5
- * does not hold (`400 definition_files_missing`). `saveAgent` uploads an agent's files itself.
4
+ * once, uploaded with `PUT /v1/files/sha256:<hex>`, and refuses a manifest naming a file it
5
+ * does not hold (`400 definition_files_missing`). `agentManifests.save` uploads them itself.
6
6
  */
7
7
  import { type DefinitionFileView } from "@nylorun/core/contracts";
8
8
  import type { SkillFileSource } from "@nylorun/core/define";
package/dist/files.js CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Definition files (track R2 M4): `client.files`. A definition names the files it needs by the
2
+ * Definition files: `client.files`. A definition names the files it needs by the
3
3
  * SHA-256 of their bytes (a skill's folder, `SkillManifest.files`); the Runtime holds each one
4
- * once, uploaded with `PUT /v1/files/sha256:<hex>`, and refuses a definition naming a file it
5
- * does not hold (`400 definition_files_missing`). `saveAgent` uploads an agent's files itself.
4
+ * once, uploaded with `PUT /v1/files/sha256:<hex>`, and refuses a manifest naming a file it
5
+ * does not hold (`400 definition_files_missing`). `agentManifests.save` uploads them itself.
6
6
  */
7
7
  import { isDefinitionFileHash, } from "@nylorun/core/contracts";
8
8
  import { RuntimeError } from "./http.js";