@nylorun/agents 0.27.0-beta → 0.28.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 CHANGED
@@ -1,5 +1,52 @@
1
1
  # @nylorun/agents
2
2
 
3
+ ## 0.28.0-beta
4
+
5
+ ### Minor Changes
6
+
7
+ - 498d972: `@nylorun/agents` exports `harnessSectionOf` and `sectionGaps` from its main entry, as its README documents: the harness section `agentManifests.save` makes from a built agent or flow, and the gaps the Runtime refuses between a manifest's two sections. Both were only in `@nylorun/agents/define` and `@nylorun/core/define`.
8
+
9
+ `nylo` no longer says nylorun 0.19 removes it, which 0.19 does not: its message ends "A later nylorun release removes nylo."
10
+
11
+ - b3dad06: Agent folders convert both ways (track T7, D80). `folderOfManifest(manifest)` in `@nylorun/core/folder` (also exported by `@nylorun/agents`) writes the agent folder that compiles back to the same manifest: name, description, metadata, harness, memory, loop and server and skill settings in `AGENT.md`, the first instruction as its body and the rest as `instructions/NN-*.md`, HTTP tools as `tools/<name>.json`, subagents as `agents/<name>/`, a flow as `flow.json` (v3's node tree, naming agents by id and tools by file name) with one flat `agents/<id>/` for its agents and its sub-flows' (a sub-flow is an `agents/<id>/` with its own `flow.json`, and a tool stage or HTTP verifier a `tools/<name>.json` beside the `flow.json` that names it), MCP servers in `mcp.json` with `Bearer ${SLOT}` or `${SLOT}` for a credential, triggers as `triggers/<id>.md`, and `runtime.json` without the slots the compiler derives the same way. It checks itself by compiling the folder, says whether the hash is the same (`exact`), and notes what it writes differently. Skills are written from their bytes when given them, else as a placeholder with the files to copy in. An embedded agent's display `name` is left out with a note, since a folder names an agent by its id.
12
+
13
+ `nylorun export --folder <name>[@<version>]` writes the folder of a saved manifest, from JSON or TypeScript; `nylorun migrate folder <dir>` writes the folder of a three-file folder, offline, with its skills; `nylorun pull <name>[@<version>]` writes back the folder a version was saved from, byte for byte, and says to use `export --folder` for a version saved as JSON. None writes over a file with other bytes.
14
+
15
+ Deprecated, still read: the three-file format (`agent.manifest.json`), whose `composeManifest` and `nylorun apply` now warn and name `nylorun migrate folder`, and `secrets` in a `SKILL.md`, which the skills loader warns about (a `DeprecationWarning` from `.skills()`): an agent folder lists a skill's secrets in `AGENT.md`.
16
+
17
+ - b3dad06: Both ends of the agent folder loop (track T7).
18
+
19
+ - `nylorun init <path>` names the agent after the path's last segment (`init agents/hello`). `--example` adds `tools/get_order.json`, `mcp.json` (a `Bearer ${ORDERS_MCP_TOKEN}` header) and `triggers/new-order.md`, explained in the folder's README.md, and the result passes `nylorun check`. The first run in a project (the nearest directory with `.git`) writes `.gitattributes` (`* text=auto eol=lf`), ignores `.nylorun/credentials.json` in `.gitignore`, and maps the folder files to their JSON Schemas in `.vscode/settings.json`, merged into an existing file. Its hint names `apply --agent` and `chat`.
20
+ - `nylorun apply --agent [<id>]` makes the Agent from the manifest after a save (or on an unchanged one), or moves it, as `nylorun agents put` does, and prints what it needs a person for.
21
+ - `nylorun apply --watch [--save]` checks and plans again on each change to the folders (and the files their `runtime.json` extends); it saves only with `--save`.
22
+ - A plan's change to the instructions prints their text diff, `-` and `+` lines.
23
+ - `nylorun chat <agent> [--session <id>] [--message <text>]` opens or reuses a session, sends `--message` or each line of standard input, and prints the reply as it comes.
24
+ - `nylorun check --json` prints `{ folders: [{ path, ok, manifest?, diagnostics }] }` for CI, with the same exit codes. `check` says "1 file", and `nylorun --version` prints the version.
25
+ - `packFolder` (`@nylorun/agents`) leaves out what the folder's `.nyloignore` names (gitignore rules), and resolves `extends` in `runtime.json`: the packed file is the merge, the folder's own keys winning, with `.nylorun/origin.json` naming the files it came from; a missing file or a cycle is a `FolderError`. `ignoreMatcher` is exported.
26
+ - `session.approve(id, approved)` and `session.respond(id, value)` take no options: each makes its own idempotency key.
27
+ - The Runtime's Agent view names each required secret of the version it runs that has no value, as an attention item (`secret_unset: required secret ORDERS_TOKEN unset`, state `input_required`), until it is set.
28
+ - The compiler's refusal of an unresolved `extends` says to resolve it before uploading.
29
+
30
+ - b3dad06: Compare two versions' agent folders, and set what a folder may hold per Tenant (track T7).
31
+
32
+ - **`GET /v1/agent-manifests/{name}/source/diff?from=<v>&to=<v>`** answers the files added, removed and changed (by SHA-256) from one version's source to another's, each list sorted by path, and how many stayed the same (`AgentManifestSourceDiff` in `@nylorun/core/contracts`). A version saved as JSON has no source (`404`); a `from` or `to` that is not a version number is `400`. `client.agentManifests.sourceDiff(name, from, to)` in `@nylorun/agents` calls it.
33
+ - **Folder limits are Tenant settings**: the Runtime limits `folderFiles` (2,000), `folderBytes` (50 MiB) and `folderTextFileBytes` (1 MiB), T6's fixed values, set with `PUT /v1/tenant/limits` like the others. A folder save with more files or bytes is `413 limit_exceeded` (`details.limit` names the limit), and the compiler holds each Markdown or JSON file to `folderTextFileBytes` (`422 folder_invalid`).
34
+ - `compileAgentFolder({ entries, files, name, limits })` takes the limits as an optional input (`AgentFolderLimits`); each left out is `AGENT_FOLDER_LIMITS`'.
35
+ - `@nylorun/agents` also exports the types `AgentManifestSource`, `AgentManifestSourceDiff`, `AgentManifestSourceSummary` and `AgentFolderDiagnostic`, and `AGENT_FOLDER_MEDIA_TYPE`.
36
+
37
+ ### Patch Changes
38
+
39
+ - 7bcdc85: `composeManifest` reads the `mcp.json` a `harness.manifest.json` names in a folder reached through a symlink, such as macOS's temporary directory (`/var` → `/private/var`). The package boundary is now resolved through `realpath` like the file, so it is no longer refused as "not a file inside the package".
40
+ - Pin core to the tested release.
41
+ - Updated dependencies [498d972]
42
+ - Updated dependencies [d23304e]
43
+ - Updated dependencies [b3dad06]
44
+ - Updated dependencies [b3dad06]
45
+ - Updated dependencies [b3dad06]
46
+ - Updated dependencies [498d972]
47
+ - Updated dependencies [b3dad06]
48
+ - @nylorun/core@0.28.0-beta
49
+
3
50
  ## 0.27.0-beta
4
51
 
5
52
  ### Minor Changes
package/README.md CHANGED
@@ -150,6 +150,8 @@ management key (`PUT /v1/tenant/tool-policy`, `admin.toolPolicy.put()` in
150
150
  journals the call, before anything is sent, and its Tool Gate runs only a call
151
151
  the journal holds running, so a harness cannot skip it.
152
152
 
153
+ <!-- doc-test: tool-policy -->
154
+ <!-- doc-test: typescript -->
153
155
  ```ts
154
156
  import { createAdmin } from "@nylorun/admin";
155
157
 
@@ -157,23 +159,27 @@ await createAdmin().toolPolicy.put({
157
159
  rules: [
158
160
  { effect: "ask", tool: "refund_order", reason: "Refunds need a person" },
159
161
  { effect: "deny", tool: "refund_order", when: { amount: { gt: 10_000 } } },
160
- { effect: "ask", server: "github", tool: "github__delete_*" },
162
+ { effect: "ask", server: "github", tool: "delete_*" },
161
163
  ],
162
164
  });
163
165
  ```
164
166
 
165
- A rule matches a call when every selector it sets matches: `tool` (the name the
166
- model calls it by; `*` matches any run of characters), `server` (an MCP
167
+ A rule matches a call when every selector it sets matches: `tool` (the tool's
168
+ own name: an MCP tool's name on its server, such as `delete_file`, or an HTTP
169
+ tool's declared name; `*` matches any run of characters), `server` (an MCP
167
170
  server's name), `kind` (`mcp` or `http`), `agent` (the agent that calls it) and
168
171
  `when` (fields of the call's input by dotted path, each tested with `equals`,
169
172
  `in`, `gt`, `gte`, `lt`, `lte` or `matches`). The strictest matching rule
170
- decides, and a call no rule matches runs:
173
+ decides, and a call no rule matches runs. Name an MCP tool by `server` and
174
+ `tool`, never by the name the model calls it by (`github__delete_file`): a
175
+ harness chooses that name, and a `tool` with `__` in it is refused (`400`).
171
176
 
172
177
  - `deny`: the call is never sent; the model gets `tool.denied` with the rule's
173
178
  `reason`.
174
179
  - `ask`: the turn pauses (`turn.paused`, with the tool, its input and the
175
180
  reason). `session.approve(interactionId, true)` sends that exact call;
176
- `false` fails it to the model as `tool.denied`, unsent. Nothing holds a
181
+ `false` fails it to the model as `tool.denied`, unsent (pass
182
+ `{ idempotencyKey }` as a third argument to retry an answer safely). Nothing holds a
177
183
  process while it waits, so an approval can wait a day, or a Runtime restart.
178
184
  Studio shows an approval card; AG-UI ends the run with a `tool_approval`
179
185
  interrupt.
@@ -252,7 +258,7 @@ const assistant = Agent({ id: "assistant", name: "Order assistant" })
252
258
  .skills("./assistant-skills");
253
259
  ```
254
260
 
255
- Each subdirectory under the catalog must contain a `SKILL.md` with YAML frontmatter (`name`, `description`) per [Agent Skills](https://agentskills.io/home). Its `secrets` (`secrets: [GH_TOKEN]`, environment variable names) are the skill's `secrets` in the harness section: the `runtime.secrets` slots its scripts read, of type `environment_secret` or `environment_variable`; a skill whose `secrets` is not such a list is skipped with a warning. Every file of a skill's folder, binary included (not `.git/`, `node_modules/`, OS files like `.DS_Store`, or `.env` and `.env.*` files, which hold secrets), is part of the definition: the manifest names each one by path and SHA-256 (`skills.<name>.files`). A skill's files may hold together at most the Tenant's skill limit (`PUT /v1/tenant/skills`, default 2 GiB); no file and no count of files has a limit of its own, and a manifest's runtime section may set a lower limit for its Agents' sessions (`runtime.limits.skills.totalBytes`). Skills need a sandbox: the runtime section's kind is `virtual` or `pod`, which the Runtime checks when the manifest is saved. `agentManifests.save` uploads the files the Runtime does not hold (`client.files`, `PUT /v1/files/sha256:<hex>`) before it saves the manifest, which the Runtime refuses while it names a file it lacks (`400 definition_files_missing`) or a skill past the Tenant's limit (`413 limit_exceeded`). `read_skill_resource` reads files of up to 10 MiB into the model's context; a larger one is used in the sandbox.
261
+ Each subdirectory under the catalog must contain a `SKILL.md` with YAML frontmatter (`name`, `description`) per [Agent Skills](https://agentskills.io/home). Its `secrets` (`secrets: [GH_TOKEN]`, environment variable names) are the skill's `secrets` in the harness section: the `runtime.secrets` slots its scripts read, of type `environment_secret` or `environment_variable`; a skill whose `secrets` is not such a list is skipped with a warning. `secrets` is not an Agent Skills key, so it is deprecated in `SKILL.md` (still read, with a `DeprecationWarning`): an agent folder lists a skill's secrets in `AGENT.md`'s skills block (`skills: { <name>: { secrets: [GH_TOKEN] } }`). Every file of a skill's folder, binary included (not `.git/`, `node_modules/`, OS files like `.DS_Store`, or `.env` and `.env.*` files, which hold secrets), is part of the definition: the manifest names each one by path and SHA-256 (`skills.<name>.files`). A skill's files may hold together at most the Tenant's skill limit (`PUT /v1/tenant/skills`, default 2 GiB); no file and no count of files has a limit of its own, and a manifest's runtime section may set a lower limit for its Agents' sessions (`runtime.limits.skills.totalBytes`). Skills need a sandbox: the runtime section's kind is `virtual` or `pod`, which the Runtime checks when the manifest is saved. `agentManifests.save` uploads the files the Runtime does not hold (`client.files`, `PUT /v1/files/sha256:<hex>`) before it saves the manifest, which the Runtime refuses while it names a file it lacks (`400 definition_files_missing`) or a skill past the Tenant's limit (`413 limit_exceeded`). `read_skill_resource` reads files of up to 10 MiB into the model's context; a larger one is used in the sandbox.
256
262
 
257
263
  The Runtime serves the skills itself, with no call to your process. When our harness runs a turn, it adds the skill catalog to the instructions and two tools: `load_skill` returns a skill's `SKILL.md` body and the paths of its other files, and `read_skill_resource` returns a text file. A session with a sandbox also has each skill's files read-only under `/skills/<name>@<hash>/`, so the model can run a skill's scripts with `bash`.
258
264
 
@@ -301,12 +307,13 @@ await client.agents.secrets.put("users/alice/analyst", "GH_TOKEN", { value: proc
301
307
  const session = await client.createSession({ id: "alice-q3", agent: "users/alice/analyst", ownerUserId: "alice" });
302
308
  ```
303
309
 
304
- `agentManifests.save(input, { ifMatch, requestId })` and `plan(input, { ifMatch })` (a dry run that saves nothing) take a v8 document, what `composeManifest` read, or `{ agent, runtime?, harness?, loop?, memory?, name?, description?, metadata? }`: the SDK makes the harness section from the built agent or flow (`harnessSectionOf`, with its skills' `secrets`; `harness` is the harness's id; `memory`, `{ access?, instructions? }`, turns on an agent's memory, not a flow's) and names the manifest after the agent's id. First it uploads every file the manifest names by hash that the Runtime does not hold: the agent's skills, a flow's agents' skills, and the runtime's files (`runtimeFiles(path, contents)` hashes a `files` entry; pass its `bytes` as `files`). `If-Match` is the version you read (`0` for a new manifest). A save that changes nothing answers the latest version, `unchanged`. `get(name, version?)`, `list()`, `versions(name)` (with the Agents made from each) and `delete(name)` (refused while an Agent was made from it: `409 manifest_in_use`) read and remove them.
310
+ `agentManifests.save(input, { ifMatch, requestId })` and `plan(input, { ifMatch })` (a dry run that saves nothing) take a v8 document, what `composeManifest` read, or `{ agent, runtime?, harness?, loop?, memory?, name?, description?, metadata? }`: the SDK makes the harness section from the built agent or flow (`harnessSectionOf`, exported for code that builds a manifest itself, with its skills' `secrets`; `harness` is the harness's id; `memory`, `{ access?, instructions? }`, turns on an agent's memory, not a flow's) and names the manifest after the agent's id. First it uploads every file the manifest names by hash that the Runtime does not hold: the agent's skills, a flow's agents' skills, and the runtime's files (`runtimeFiles(path, contents)` hashes a `files` entry; pass its `bytes` as `files`). `If-Match` is the version you read (`0` for a new manifest). A save that changes nothing answers the latest version, `unchanged`. `get(name, version?)`, `list()`, `versions(name)` (with the Agents made from each) and `delete(name)` (refused while an Agent was made from it: `409 manifest_in_use`) read and remove them.
305
311
 
306
312
  ### Agent folders: `packFolder`, `checkFolder`, `saveFolder`
307
313
 
308
- An agent can be a folder of Markdown and JSON (track T6): `AGENT.md` with `name`, `description` and the instructions, and beside it `mcp.json`, `runtime.json`, `skills/`, `tools/`, `triggers/` and `files/` as it needs them. `packFolder(dir)` (Node only) lists every file with its size and SHA-256, skipping `.git` and `node_modules`; `checkFolder(folder)` compiles it offline with the Runtime's compiler (`@nylorun/core/folder`) and returns `{ manifest?, diagnostics, compiler }`, each diagnostic with its file, line and column (`diagnosticLine` formats one). `agentManifests.saveFolder(folder, { ifMatch })` and `planFolder` compile it first, refuse one that does not compile (`FolderInvalidError`), upload the files the Runtime lacks and send the folder by hash; the Runtime keeps it as the version's source, which `source(name, version)` and `sourceFile(name, version, path)` read back.
314
+ An agent is best written as a folder of Markdown and JSON (tracks T6 and T7): `AGENT.md` with `name`, `description` and the instructions, and beside it `mcp.json`, `runtime.json`, `skills/`, `tools/`, `triggers/` and `files/` as it needs them. `agents/<id>/` holds subagents (each offered to the agent as a tool), `flow.json` makes the folder a flow whose stages name those agents by id, and `plugins/<name>/` vendors an Agent Plugin; [the CLI's README](../../cli/nylorun/README.md#agent-manifests-apply) shows each. `packFolder(dir)` (Node only) lists every file with its size and SHA-256, skipping `.git`, `node_modules` and what the folder's `.nyloignore` names (gitignore rules, `ignoreMatcher`), and resolves `"extends"` in `runtime.json` (the packed file is the merge, the folder's own keys winning); `checkFolder(folder)` compiles it offline with the Runtime's compiler (`@nylorun/core/folder`) and returns `{ manifest?, diagnostics, compiler }`, each diagnostic with its file, line and column (`diagnosticLine` formats one). `agentManifests.saveFolder(folder, { ifMatch })` and `planFolder` compile it first, refuse one that does not compile (`FolderInvalidError`), upload the files the Runtime lacks and send the folder by hash; the Runtime keeps it as the version's source, which `source(name, version)` and `sourceFile(name, version, path)` read back, and `sourceDiff(name, from, to)` compares two versions' sources: the files added, removed and changed, by hash. `folderOfManifest(manifest)` goes the other way: the folder that compiles back to a saved manifest (what `nylorun export --folder` writes).
309
315
 
316
+ <!-- doc-test: typescript -->
310
317
  ```ts
311
318
  import { createClient, packFolder } from "@nylorun/agents";
312
319
 
@@ -316,9 +323,11 @@ console.log(plan.manifest, plan.diagnostics); // the compiled manifest, and e.g.
316
323
  await client.agentManifests.saveFolder(await packFolder("agents/triage"), { ifMatch: plan.from ?? 0 });
317
324
  ```
318
325
 
319
- ### Three files: `composeManifest`
326
+ ### Three files: `composeManifest` (deprecated)
320
327
 
321
- A manifest can also be authored as files, so a runtime is shared by many agents and skills stay folders. `composeManifest(dir)` (Node only) reads `<dir>/agent.manifest.json` and the files it names, each path relative to the file that names it, and returns `{ manifest, files, gaps, warnings }`; `agentManifests.save` takes it as it is and uploads `files`. `npx nylorun apply <dir>` does the same from the command line.
328
+ The three-file form is deprecated in favour of agent folders: `composeManifest` and `nylorun apply` still read it, and warn, and `npx nylorun migrate folder <dir>` writes the agent folder of a three-file folder, with its skills, offline. A folder's `runtime.json` shares a runtime between agents with `"extends"`.
329
+
330
+ A manifest can be authored as three files, so a runtime is shared by many agents and skills stay folders. `composeManifest(dir)` (Node only) reads `<dir>/agent.manifest.json` and the files it names, each path relative to the file that names it, and returns `{ manifest, files, gaps, warnings }`; `agentManifests.save` takes it as it is and uploads `files`. `npx nylorun apply <dir>` does the same from the command line.
322
331
 
323
332
  ```jsonc
324
333
  // agents/triage/agent.manifest.json
@@ -413,7 +422,12 @@ the manifest, then `createSession({ agent })` and `session.input(value)`. `input
413
422
  `content` and other JSON as `data`. On the wire a flow agent is a workflow manifest v3
414
423
  (`kind: "workflow"`, `workflowSchemaVersion: 3`) that embeds the agents it runs, so one
415
424
  document and one manifest hash cover the whole flow; its agents are not listed on their own.
416
- In an agent manifest it is the harness section, `harness.kind: "workflow"`.
425
+ In an agent manifest it is the harness section, `harness.kind: "workflow"`. A flow can also
426
+ be an [agent folder](#agent-folders-packfolder-checkfolder-savefolder): `flow.json` holds the
427
+ same node tree, naming its agents (each an `agents/<id>/` folder) by id, and the compiler
428
+ embeds each agent where the manifest needs it
429
+ ([`examples/agents/repo-brief-folder/`](../../examples/agents/repo-brief-folder/) is the Repo
430
+ brief flow as a folder).
417
431
 
418
432
  A flow passes data between its stages itself: the first stage gets the flow's input, and
419
433
  every later stage gets the previous stage's output. So each agent returns, through its
@@ -542,7 +556,7 @@ default release registry). Host contract:
542
556
  [HOST_CONTRACT.md](../../runtime/harness/HOST_CONTRACT.md). Vocabulary (turn loop vs
543
557
  workflow Loop): [runtime/harness/src/CONTEXT.md](../../runtime/harness/src/CONTEXT.md).
544
558
 
545
- Use `session.observe({ cursor, signal })` for resumable canonical events, `session.inspect()` for waiting/uncertain state, and `approve`, `respond`, or `cancel` with an explicit stable idempotency key. Retry the same semantic command with the same key. `ownerUserId` must come from trusted server authentication. Application credentials are not browser credentials; browser applications need an authorized backend. Definition authoring is browser-bundleable.
559
+ Use `session.observe({ cursor, signal })` for resumable canonical events, `session.inspect()` for waiting/uncertain state, and `approve`, `respond` or `cancel` to answer or stop a turn. `cancel` takes an idempotency key; `approve(interactionId, approved)` and `respond(interactionId, value)` make one of their own when given none, so give them a stable one (`{ idempotencyKey }`) when you may retry. Retry the same semantic command with the same key. `ownerUserId` must come from trusted server authentication. Application credentials are not browser credentials; browser applications need an authorized backend. Definition authoring is browser-bundleable.
546
560
 
547
561
  Connection defaults follow `resolveConnection` (options → environment → Project
548
562
  link). Agents do not hash the manifest; a session pins the manifest version its Agent runs
@@ -19,7 +19,7 @@
19
19
  * upload its files by hash, and send the folder, which the Runtime compiles again and keeps as the
20
20
  * version's source.
21
21
  */
22
- import { AGENT_MANIFEST_V8_SCHEMA_VERSION, type AgentManifestPlan, type AgentManifestSource, type AgentManifestV8, type AgentMemorySettings, type AgentManifestVersions, type AgentManifestView, type DeleteAgentManifestResponse, type RuntimeSection, type SaveAgentManifestResponse } from "@nylorun/core/contracts";
22
+ import { AGENT_MANIFEST_V8_SCHEMA_VERSION, type AgentManifestPlan, type AgentManifestSource, type AgentManifestSourceDiff, type AgentManifestV8, type AgentMemorySettings, type AgentManifestVersions, type AgentManifestView, type DeleteAgentManifestResponse, type RuntimeSection, type SaveAgentManifestResponse } from "@nylorun/core/contracts";
23
23
  import { type AgentManifest, type BuiltAgent, type BuiltWorkflow, type JsonObject, type SkillFileSource, type WorkflowManifest } from "@nylorun/core/define";
24
24
  import { type PackedFolder } from "./folder.js";
25
25
  import { type Transport } from "./http.js";
@@ -136,6 +136,13 @@ export declare class AgentManifestsClient {
136
136
  sourceFile(name: string, version: number, path: string, options?: {
137
137
  signal?: AbortSignal;
138
138
  }): Promise<Uint8Array>;
139
+ /**
140
+ * What changed from version `from`'s source to version `to`'s: the files added, removed and
141
+ * changed by path and hash. A version saved as JSON has no source (`404`).
142
+ */
143
+ sourceDiff(name: string, from: number, to: number, options?: {
144
+ signal?: AbortSignal;
145
+ }): Promise<AgentManifestSourceDiff>;
139
146
  /** The latest version of manifest `name`, or version `version`. */
140
147
  get(name: string, version?: number, options?: {
141
148
  signal?: AbortSignal;
@@ -262,6 +262,14 @@ export class AgentManifestsClient {
262
262
  });
263
263
  return new Uint8Array(await response.arrayBuffer());
264
264
  }
265
+ /**
266
+ * What changed from version `from`'s source to version `to`'s: the files added, removed and
267
+ * changed by path and hash. A version saved as JSON has no source (`404`).
268
+ */
269
+ async sourceDiff(name, from, to, options = {}) {
270
+ const query = new URLSearchParams({ from: String(from), to: String(to) });
271
+ return this.transport.json(`${this.path(name)}/source/diff?${query}`, "GET", undefined, options.signal);
272
+ }
265
273
  /** The latest version of manifest `name`, or version `version`. */
266
274
  async get(name, version, options = {}) {
267
275
  return this.transport.json(`${this.path(name)}${version === undefined ? "" : `/versions/${version}`}`, "GET", undefined, options.signal);
package/dist/compose.d.ts CHANGED
@@ -6,6 +6,11 @@ export declare class ComposeError extends Error {
6
6
  readonly path: string;
7
7
  constructor(path: string, message: string);
8
8
  }
9
+ /**
10
+ * The warning every three-file folder gets (D80): agent folders replace it. It is still read, and
11
+ * removed in a later release.
12
+ */
13
+ export declare function threeFileDeprecation(dir: string): string;
9
14
  /**
10
15
  * Composes the agent manifest in `dir` (its `agent.manifest.json`, or `options.file`): the v8
11
16
  * document to save, the bytes of every file it names by hash (skills, plugins' skills, runtime
package/dist/compose.js CHANGED
@@ -16,10 +16,13 @@
16
16
  * `{ path, files }`.
17
17
  *
18
18
  * Each path is relative to the file that names it. Node only: it reads the files.
19
+ *
20
+ * Deprecated (D80): agent folders (`AGENT.md`) replace the three files, and every composed
21
+ * manifest carries a warning naming `nylorun migrate folder`. It is still read.
19
22
  */
20
23
  import { existsSync, statSync } from "node:fs";
21
24
  import { readdir, readFile } from "node:fs/promises";
22
- import { dirname, join, relative, resolve } from "node:path";
25
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path";
23
26
  import { AGENT_MANIFEST_V8_SCHEMA_VERSION, AgentManifestFileSchema, AgentManifestV8Schema, HarnessManifestFileSchema, RuntimeManifestFileSchema, WorkflowHarnessSectionSchema, } from "@nylorun/core/contracts";
24
27
  import { sectionGaps } from "@nylorun/core/define";
25
28
  import { requirementGaps } from "./agent-manifests.js";
@@ -198,6 +201,15 @@ function agentHarness(file, base, where, files, warnings) {
198
201
  ...(file.memory === undefined ? {} : { memory: file.memory }),
199
202
  };
200
203
  }
204
+ /**
205
+ * The warning every three-file folder gets (D80): agent folders replace it. It is still read, and
206
+ * removed in a later release.
207
+ */
208
+ export function threeFileDeprecation(dir) {
209
+ const inside = relative(process.cwd(), dir);
210
+ const where = inside === "" ? "." : inside.startsWith("..") || isAbsolute(inside) ? dir : inside;
211
+ return `${AGENT_MANIFEST_FILE} (the three-file format) is deprecated: nylorun migrate folder ${where} writes it as an agent folder (AGENT.md), which saves the same manifest. It is still read until a later release.`;
212
+ }
201
213
  /**
202
214
  * Composes the agent manifest in `dir` (its `agent.manifest.json`, or `options.file`): the v8
203
215
  * document to save, the bytes of every file it names by hash (skills, plugins' skills, runtime
@@ -210,7 +222,7 @@ export async function composeManifest(dir, options = {}) {
210
222
  const base = dirname(agentFile);
211
223
  const authored = await readFileAs(agentFile, AgentManifestFileSchema, relativeTo(agentFile));
212
224
  const files = new Map();
213
- const warnings = [];
225
+ const warnings = [threeFileDeprecation(base)];
214
226
  const runtime = authored.runtime === undefined ? undefined : await runtimeSection(authored.runtime, base, agentFile, files, warnings);
215
227
  let harness;
216
228
  let requires;
@@ -5,6 +5,8 @@
5
5
  */
6
6
  import { type CompiledAgentFolder } from "@nylorun/core/folder";
7
7
  import type { PackedFolder } from "./folder.js";
8
+ /** The agent folder of a manifest (track T7): what `nylorun export --folder` writes. */
9
+ export { folderOfManifest, type FolderOfManifest, type FolderOfManifestOptions } from "@nylorun/core/folder";
8
10
  /**
9
11
  * Compiles a packed folder offline, as the Runtime will: the manifest when it compiles, and every
10
12
  * diagnostic either way. With `name`, the name it will be saved as, `AGENT.md`'s must match.
@@ -4,6 +4,8 @@
4
4
  * module only when a folder is sent.
5
5
  */
6
6
  import { AGENT_FOLDER_LIMITS, compileAgentFolder, folderTextPaths, } from "@nylorun/core/folder";
7
+ /** The agent folder of a manifest (track T7): what `nylorun export --folder` writes. */
8
+ export { folderOfManifest } from "@nylorun/core/folder";
7
9
  /**
8
10
  * Compiles a packed folder offline, as the Runtime will: the manifest when it compiles, and every
9
11
  * diagnostic either way. With `name`, the name it will be saved as, `AGENT.md`'s must match.
@@ -0,0 +1,13 @@
1
+ /**
2
+ * `.nyloignore` (track T7): the files `packFolder` leaves out of an agent folder, in gitignore
3
+ * syntax. Blank lines and `#` comments are skipped; `!` re-includes what an earlier line left out;
4
+ * a trailing `/` matches directories only; a pattern with a `/` before its end is anchored at the
5
+ * folder, and one without matches a name at any depth; `*`, `?`, `[…]` and `**` glob as in git.
6
+ * The last line that matches decides, and nothing below a directory left out comes back, as in
7
+ * git. Pure: no Node module, so it also reads a project's `.gitignore`.
8
+ */
9
+ /**
10
+ * A matcher for an ignore file's text: whether `path` (relative, `/` between segments) is left
11
+ * out, a directory when `directory` is true. A path below a directory left out is left out too.
12
+ */
13
+ export declare function ignoreMatcher(text: string): (path: string, directory?: boolean) => boolean;
@@ -0,0 +1,113 @@
1
+ /**
2
+ * `.nyloignore` (track T7): the files `packFolder` leaves out of an agent folder, in gitignore
3
+ * syntax. Blank lines and `#` comments are skipped; `!` re-includes what an earlier line left out;
4
+ * a trailing `/` matches directories only; a pattern with a `/` before its end is anchored at the
5
+ * folder, and one without matches a name at any depth; `*`, `?`, `[…]` and `**` glob as in git.
6
+ * The last line that matches decides, and nothing below a directory left out comes back, as in
7
+ * git. Pure: no Node module, so it also reads a project's `.gitignore`.
8
+ */
9
+ /** A glob's body as a regular expression's source: `*` and `?` stop at `/`, `**` crosses it. */
10
+ function globSource(glob) {
11
+ let source = "";
12
+ for (let index = 0; index < glob.length; index += 1) {
13
+ const char = glob[index];
14
+ if (char === "\\" && index + 1 < glob.length) {
15
+ source += escapeChar(glob[++index]);
16
+ continue;
17
+ }
18
+ if (char === "*") {
19
+ if (glob[index + 1] === "*") {
20
+ const before = index === 0 || glob[index - 1] === "/";
21
+ const after = index + 2 === glob.length || glob[index + 2] === "/";
22
+ if (before && after) {
23
+ if (index + 2 === glob.length)
24
+ source += ".*"; // `a/**`: everything inside
25
+ else {
26
+ source += "(?:.*/)?"; // `**/` and `/**/`: zero or more directories
27
+ index += 1; // the `/` after `**`
28
+ }
29
+ index += 1;
30
+ continue;
31
+ }
32
+ }
33
+ source += "[^/]*";
34
+ continue;
35
+ }
36
+ if (char === "?") {
37
+ source += "[^/]";
38
+ continue;
39
+ }
40
+ if (char === "[") {
41
+ const end = glob.indexOf("]", index + 2);
42
+ if (end > 0) {
43
+ let body = glob.slice(index + 1, end);
44
+ if (body.startsWith("!"))
45
+ body = `^${body.slice(1)}`;
46
+ source += `[${body.replace(/\\/g, "\\\\")}]`;
47
+ index = end;
48
+ continue;
49
+ }
50
+ }
51
+ source += escapeChar(char);
52
+ }
53
+ return source;
54
+ }
55
+ const escapeChar = (char) => char.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
56
+ /** The rules of an ignore file's text. */
57
+ function parseIgnore(text) {
58
+ const rules = [];
59
+ for (const raw of text.split(/\r?\n/)) {
60
+ // Trailing spaces go, unless the last is escaped.
61
+ let line = raw.replace(/(?<!\\)\s+$/, "");
62
+ if (line === "" || line.startsWith("#"))
63
+ continue;
64
+ let negate = false;
65
+ if (line.startsWith("!")) {
66
+ negate = true;
67
+ line = line.slice(1);
68
+ }
69
+ else if (line.startsWith("\\!") || line.startsWith("\\#"))
70
+ line = line.slice(1);
71
+ const directoryOnly = line.endsWith("/");
72
+ if (directoryOnly)
73
+ line = line.slice(0, -1);
74
+ if (line === "")
75
+ continue;
76
+ const anchored = line.includes("/");
77
+ if (line.startsWith("/"))
78
+ line = line.slice(1);
79
+ const body = globSource(line);
80
+ rules.push({
81
+ negate,
82
+ directoryOnly,
83
+ pattern: new RegExp(anchored ? `^${body}$` : `(?:^|/)${body}$`),
84
+ });
85
+ }
86
+ return rules;
87
+ }
88
+ /**
89
+ * A matcher for an ignore file's text: whether `path` (relative, `/` between segments) is left
90
+ * out, a directory when `directory` is true. A path below a directory left out is left out too.
91
+ */
92
+ export function ignoreMatcher(text) {
93
+ const rules = parseIgnore(text);
94
+ const decide = (path, directory) => {
95
+ let ignored = false;
96
+ for (const rule of rules) {
97
+ if (rule.directoryOnly && !directory)
98
+ continue;
99
+ if (rule.negate === ignored && rule.pattern.test(path))
100
+ ignored = !rule.negate;
101
+ }
102
+ return ignored;
103
+ };
104
+ return (path, directory = false) => {
105
+ if (rules.length === 0)
106
+ return false;
107
+ const segments = path.split("/");
108
+ for (let depth = 1; depth < segments.length; depth += 1)
109
+ if (decide(segments.slice(0, depth).join("/"), true))
110
+ return true;
111
+ return decide(path, directory);
112
+ };
113
+ }
@@ -1,4 +1,6 @@
1
1
  import { type PackedFolder } from "./folder.js";
2
+ /** The file, at a folder's top, that names what packing leaves out (gitignore syntax). */
3
+ export declare const FOLDER_IGNORE_FILE = ".nyloignore";
2
4
  /** Whether `dir` holds an `AGENT.md`. */
3
5
  export declare function isAgentFolder(dir: string): Promise<boolean>;
4
6
  /** Lists the agent folder at `dir` with each file's size and SHA-256. */
@@ -1,24 +1,110 @@
1
1
  /**
2
2
  * Packing an agent folder (track T6): `packFolder(dir)` lists every file with its size and SHA-256
3
- * and reads its bytes on demand. It skips `.git`, `.hg`, `.svn` and `node_modules` and follows no
4
- * symbolic link. Node only: it reads the folder.
3
+ * and reads its bytes on demand. It skips `.git`, `.hg`, `.svn` and `node_modules`, what the
4
+ * folder's `.nyloignore` names (gitignore rules, `folder-ignore.ts`; track T7), and follows no
5
+ * symbolic link.
6
+ *
7
+ * `extends` in `runtime.json` (track T7, design §3.9) names a shared JSON file, inside the folder
8
+ * or outside it, by a path relative to the file that names it. The packed `runtime.json` is the
9
+ * merge, the folder's own keys winning (objects merge, arrays replace), so every saved version is
10
+ * self-contained and its source is what was sent; `.nylorun/origin.json` in the pack names the
11
+ * files it was merged from. A chain of `extends` resolves in turn; a cycle, a missing file or
12
+ * one that is not a JSON object is a `FolderError`. Node only: it reads the folder.
5
13
  */
6
14
  import { createHash } from "node:crypto";
7
15
  import { readdir, readFile, stat } from "node:fs/promises";
8
- import { join, relative, resolve, sep } from "node:path";
16
+ import { dirname, join, relative, resolve, sep } from "node:path";
9
17
  import { AGENT_FOLDER_LIMITS } from "@nylorun/core/folder";
10
18
  import { AGENT_FOLDER_FILE, FolderError } from "./folder.js";
19
+ import { ignoreMatcher } from "./folder-ignore.js";
11
20
  const SKIPPED_DIRECTORIES = new Set([".git", ".hg", ".svn", "node_modules"]);
21
+ /** The file, at a folder's top, that names what packing leaves out (gitignore syntax). */
22
+ export const FOLDER_IGNORE_FILE = ".nyloignore";
23
+ const RUNTIME_FILE = "runtime.json";
24
+ const ORIGIN_FILE = ".nylorun/origin.json";
12
25
  /** Whether `dir` holds an `AGENT.md`. */
13
26
  export async function isAgentFolder(dir) {
14
27
  return stat(join(dir, AGENT_FOLDER_FILE)).then((found) => found.isFile(), () => false);
15
28
  }
29
+ const sha256 = (data) => createHash("sha256").update(data).digest("hex");
30
+ const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
31
+ const byPath = (a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
32
+ /** `over` merged onto `under`: `over`'s keys win, objects merge, arrays and the rest replace. */
33
+ function merge(under, over) {
34
+ const merged = { ...under };
35
+ for (const [key, value] of Object.entries(over)) {
36
+ const below = merged[key];
37
+ merged[key] = isObject(below) && isObject(value) ? merge(below, value) : value;
38
+ }
39
+ return merged;
40
+ }
41
+ /**
42
+ * `runtime.json` with its `extends` resolved, and each file it was merged from (absolute paths,
43
+ * nearest first); undefined when it says no `extends`, or is not a JSON object (the compiler
44
+ * reports that with its position).
45
+ */
46
+ async function resolveExtends(root, text) {
47
+ let local;
48
+ try {
49
+ local = JSON.parse(text);
50
+ }
51
+ catch {
52
+ return undefined;
53
+ }
54
+ if (!isObject(local) || !("extends" in local))
55
+ return undefined;
56
+ const shown = (file) => relative(root, file).split(sep).join("/");
57
+ const chain = [join(root, RUNTIME_FILE)];
58
+ const layers = [];
59
+ let current = local;
60
+ for (;;) {
61
+ const { extends: target, ...own } = current;
62
+ // The extended file's `$schema` names that file's kind; the folder's own file has none.
63
+ if (layers.length > 0)
64
+ delete own.$schema;
65
+ layers.push(own);
66
+ if (target === undefined)
67
+ break;
68
+ const from = chain.at(-1);
69
+ if (typeof target !== "string" || target.trim() === "")
70
+ throw new FolderError(root, `${shown(from)}: extends is the path of a JSON file, such as "../../platform/pod.runtime.json"`);
71
+ const file = resolve(dirname(from), target);
72
+ if (chain.includes(file))
73
+ throw new FolderError(root, `${RUNTIME_FILE}: extends makes a cycle: ${[...chain, file].map(shown).join(" → ")}`);
74
+ let body;
75
+ try {
76
+ body = await readFile(file, "utf8");
77
+ }
78
+ catch (error) {
79
+ const code = error.code;
80
+ throw new FolderError(root, code === "ENOENT"
81
+ ? `${shown(from)}: extends ${target}, which does not exist (${file})`
82
+ : `${shown(from)}: cannot read ${target}, which it extends: ${code ?? error}`);
83
+ }
84
+ let parsed;
85
+ try {
86
+ parsed = JSON.parse(body);
87
+ }
88
+ catch (error) {
89
+ throw new FolderError(root, `${shown(file)}, which ${shown(from)} extends, is not JSON: ${error.message}`);
90
+ }
91
+ if (!isObject(parsed))
92
+ throw new FolderError(root, `${shown(file)}, which ${shown(from)} extends, is not a JSON object`);
93
+ chain.push(file);
94
+ current = parsed;
95
+ }
96
+ // The farthest file first, each nearer one over it.
97
+ const value = layers.reduceRight((under, layer) => merge(under, layer), {});
98
+ return { value, from: chain.slice(1) };
99
+ }
16
100
  /** Lists the agent folder at `dir` with each file's size and SHA-256. */
17
101
  export async function packFolder(dir) {
18
102
  const root = resolve(dir);
19
103
  const entries = [];
20
104
  const files = new Map();
21
105
  const skipped = [];
106
+ const ignoreText = await readFile(join(root, FOLDER_IGNORE_FILE), "utf8").catch(() => undefined);
107
+ const ignored = ignoreText === undefined ? () => false : ignoreMatcher(ignoreText);
22
108
  let bytes = 0;
23
109
  const walk = async (at) => {
24
110
  const children = await readdir(at, { withFileTypes: true }).catch((error) => {
@@ -28,10 +114,12 @@ export async function packFolder(dir) {
28
114
  const full = join(at, child.name);
29
115
  const path = relative(root, full).split(sep).join("/");
30
116
  if (child.isDirectory()) {
31
- if (!SKIPPED_DIRECTORIES.has(child.name))
117
+ if (!SKIPPED_DIRECTORIES.has(child.name) && !ignored(path, true))
32
118
  await walk(full);
33
119
  continue;
34
120
  }
121
+ if (ignored(path))
122
+ continue;
35
123
  if (child.isSymbolicLink()) {
36
124
  skipped.push(`${path}: a symbolic link, not followed`);
37
125
  continue;
@@ -44,11 +132,33 @@ export async function packFolder(dir) {
44
132
  bytes += data.byteLength;
45
133
  if (bytes > AGENT_FOLDER_LIMITS.totalBytes)
46
134
  throw new FolderError(root, `${dir} holds more than ${AGENT_FOLDER_LIMITS.totalBytes} bytes, an agent folder's limit`);
47
- entries.push({ path, size: data.byteLength, sha256: createHash("sha256").update(data).digest("hex") });
135
+ entries.push({ path, size: data.byteLength, sha256: sha256(data) });
48
136
  files.set(path, { size: data.byteLength, read: () => readFile(full) });
49
137
  }
50
138
  };
51
139
  await walk(root);
52
- entries.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
53
- return { dir: root, entries, files, skipped };
140
+ // `extends`: the packed runtime.json is the merge, and origin.json names what it came from.
141
+ let extended;
142
+ const runtime = files.get(RUNTIME_FILE);
143
+ if (runtime && runtime.size <= AGENT_FOLDER_LIMITS.textFileBytes) {
144
+ const resolved = await resolveExtends(root, new TextDecoder().decode(await runtime.read()));
145
+ if (resolved) {
146
+ extended = resolved.from;
147
+ const put = (path, text) => {
148
+ const data = new TextEncoder().encode(text);
149
+ const at = entries.findIndex((entry) => entry.path === path);
150
+ const entry = { path, size: data.byteLength, sha256: sha256(data) };
151
+ if (at < 0)
152
+ entries.push(entry);
153
+ else
154
+ entries[at] = entry;
155
+ files.set(path, { size: data.byteLength, read: async () => data });
156
+ };
157
+ put(RUNTIME_FILE, `${JSON.stringify(resolved.value, null, 2)}\n`);
158
+ const relativeTo = (file) => relative(root, file).split(sep).join("/");
159
+ put(ORIGIN_FILE, `${JSON.stringify({ [RUNTIME_FILE]: { extends: resolved.from.map(relativeTo) } }, null, 2)}\n`);
160
+ }
161
+ }
162
+ entries.sort(byPath);
163
+ return { dir: root, entries, files, skipped, ...(extended ? { extended } : {}) };
54
164
  }
package/dist/folder.d.ts CHANGED
@@ -22,8 +22,16 @@ export interface PackedFolder {
22
22
  readonly files: ReadonlyMap<string, SkillFileSource>;
23
23
  /** What packing left out, and why (a symbolic link). */
24
24
  readonly skipped: readonly string[];
25
+ /**
26
+ * The files `runtime.json` was merged from (its `extends`, nearest first), as absolute paths;
27
+ * absent when it extends none.
28
+ */
29
+ readonly extended?: readonly string[];
25
30
  }
26
- /** A folder that cannot be packed: too many files, or more bytes than an agent folder holds. */
31
+ /**
32
+ * A folder that cannot be packed: too many files, more bytes than an agent folder holds, or an
33
+ * `extends` in `runtime.json` that does not resolve.
34
+ */
27
35
  export declare class FolderError extends Error {
28
36
  readonly dir: string;
29
37
  constructor(dir: string, message: string);
package/dist/folder.js CHANGED
@@ -1,6 +1,9 @@
1
1
  /** The file that makes a folder an agent folder. */
2
2
  export const AGENT_FOLDER_FILE = "AGENT.md";
3
- /** A folder that cannot be packed: too many files, or more bytes than an agent folder holds. */
3
+ /**
4
+ * A folder that cannot be packed: too many files, more bytes than an agent folder holds, or an
5
+ * `extends` in `runtime.json` that does not resolve.
6
+ */
4
7
  export class FolderError extends Error {
5
8
  dir;
6
9
  constructor(dir, message) {
package/dist/index.d.ts CHANGED
@@ -3,11 +3,11 @@ export type { ProtocolFeature, ProtocolRange, Compatibility, ErrorCode, } from "
3
3
  export { checkHealth, describeIncompatibility, parseProtocolRange, } from "@nylorun/core/transport";
4
4
  export type { HealthCheck, Incompatibility } from "@nylorun/core/transport";
5
5
  export { Agent, AgentBuilder } from "./builder.js";
6
- export { AgentBuildError, AgentLifecycleError, VerdictSchema, isVerdict, WorkflowBuildError, isBuiltWorkflow, flow, tool, http, defineSchema, ToolError, } from "@nylorun/core/define";
6
+ export { AgentBuildError, AgentLifecycleError, VerdictSchema, isVerdict, WorkflowBuildError, isBuiltWorkflow, flow, tool, http, defineSchema, ToolError, harnessSectionOf, sectionGaps, } from "@nylorun/core/define";
7
7
  export type { FlowAgentBuilder, Flow, Named, LoopOptions, StageOptions, HttpTarget, } from "@nylorun/core/define";
8
8
  export type { AgentOptions, AgentManifest, BuiltAgent, BuiltWorkflow, Verdict, WorkflowBinding, WorkflowManifest, ToolDefinition, ToolExecutionContext, ToolOutcome, JsonValue, JsonObject, } from "@nylorun/core/define";
9
- export type { SessionPage, SessionListItem, SessionManifestView, HistoryPage, SessionUsageTotals, CostSplit, ModelCall, ModelCallsPage, AgentPage, ToolCall, ToolCallsPage, LiveEvent, SessionEvent, SessionEventOf, EventType, SessionCommand, SubjectScope, AgentView, AgentState, AgentAttention, AgentEvent, AgentEventType, EgressRunningTurn, EnvironmentKind, EnvironmentRecipe, EnvironmentSecretSlot, AgentSecretView, AgentSnapshot, AgentRestore, AgentDiskVersions, RestoreAgentRequest, AgentForkPlan, AgentMovePlan, RestartAgentRequest, ForkAgentRequest, ForkAgentResponse, DeleteAgentResponse, PutAgentRequest, PutAgentSecretRequest, AgentManifestV8, AgentManifestFile, HarnessManifestFile, RuntimeManifestFile, RuntimeSection, AgentManifestChange, AgentManifestWarning, AgentManifestPlan, AgentManifestView, AgentManifestVersions, SaveAgentManifestResponse, DeleteAgentManifestResponse, MessagePart, ArtifactView, ArtifactKind, ArtifactListItem, ArtifactPage, ArtifactVersionView, ArtifactLink, UploadArtifactResponse, DefinitionFileView, ArtifactTree, ArtifactDiff, FolderEntry, } from "@nylorun/core/contracts";
10
- export { AGENT_EVENT_TYPES, AGENT_STATES, COMPUTE_STATES, RUNTIME_LIMIT_DEFAULTS, SUBJECT_SCOPES, } from "@nylorun/core/contracts";
9
+ export type { SessionPage, SessionListItem, SessionManifestView, HistoryPage, SessionUsageTotals, CostSplit, ModelCall, ModelCallsPage, AgentPage, ToolCall, ToolCallsPage, LiveEvent, SessionEvent, SessionEventOf, EventType, SessionCommand, SubjectScope, AgentView, AgentState, AgentAttention, AgentEvent, AgentEventType, EgressRunningTurn, EnvironmentKind, EnvironmentRecipe, EnvironmentSecretSlot, AgentSecretView, AgentSnapshot, AgentRestore, AgentDiskVersions, RestoreAgentRequest, AgentForkPlan, AgentMovePlan, RestartAgentRequest, ForkAgentRequest, ForkAgentResponse, DeleteAgentResponse, PutAgentRequest, PutAgentSecretRequest, AgentManifestV8, AgentManifestFile, HarnessManifestFile, RuntimeManifestFile, RuntimeSection, AgentManifestChange, AgentManifestWarning, AgentManifestPlan, AgentManifestSource, AgentManifestSourceDiff, AgentManifestSourceSummary, AgentFolderDiagnostic, AgentManifestView, AgentManifestVersions, SaveAgentManifestResponse, DeleteAgentManifestResponse, MessagePart, ArtifactView, ArtifactKind, ArtifactListItem, ArtifactPage, ArtifactVersionView, ArtifactLink, UploadArtifactResponse, DefinitionFileView, ArtifactTree, ArtifactDiff, FolderEntry, } from "@nylorun/core/contracts";
10
+ export { AGENT_EVENT_TYPES, AGENT_FOLDER_MEDIA_TYPE, AGENT_STATES, COMPUTE_STATES, RUNTIME_LIMIT_DEFAULTS, SUBJECT_SCOPES, } from "@nylorun/core/contracts";
11
11
  export type { AgentHarnessSection, HarnessSection, HarnessSkill, HarnessMcpServer, WorkflowHarnessSection, } from "@nylorun/core";
12
12
  export { AgentsClient, SessionClient, createClient } from "./client.js";
13
13
  export { ArtifactsClient } from "./artifacts.js";
@@ -17,9 +17,10 @@ export type { ActAsOptions, AgentSource, SessionView, CommandOptions, CreateSess
17
17
  export { AgentInstancesClient, AgentManifestsClient, AgentMemoriesClient, AgentSecretsClient, ManifestGapsError, agentManifest, requirementGaps, runtimeFiles, } from "./client.js";
18
18
  export { composeManifest, manifestFolders, ComposeError, AGENT_MANIFEST_FILE } from "./compose.js";
19
19
  export { AGENT_FOLDER_FILE, FolderError, FolderInvalidError, diagnosticLine } from "./folder.js";
20
- export { checkFolder } from "./folder-check.js";
20
+ export { checkFolder, folderOfManifest, type FolderOfManifest, type FolderOfManifestOptions } from "./folder-check.js";
21
21
  export type { PackedFolder } from "./folder.js";
22
- export { packFolder, isAgentFolder } from "./folder-pack.js";
22
+ export { packFolder, isAgentFolder, FOLDER_IGNORE_FILE } from "./folder-pack.js";
23
+ export { ignoreMatcher } from "./folder-ignore.js";
23
24
  export { AccessClient } from "./access.js";
24
25
  export { resolveConnection, ConnectionError } from "./connection.js";
25
26
  export type { ResolvedConnection } from "./connection.js";
package/dist/index.js CHANGED
@@ -3,16 +3,20 @@ export { hashManifest, PROTOCOL_VERSION, PROTOCOL_FEATURES, HOST_PROTOCOL, DEFIN
3
3
  // The `/health` compatibility probe every Runtime client shares (Studio's server uses it).
4
4
  export { checkHealth, describeIncompatibility, parseProtocolRange, } from "@nylorun/core/transport";
5
5
  export { Agent, AgentBuilder } from "./builder.js";
6
- export { AgentBuildError, AgentLifecycleError, VerdictSchema, isVerdict, WorkflowBuildError, isBuiltWorkflow, flow, tool, http, defineSchema, ToolError, } from "@nylorun/core/define";
7
- export { AGENT_EVENT_TYPES, AGENT_STATES, COMPUTE_STATES, RUNTIME_LIMIT_DEFAULTS, SUBJECT_SCOPES, } from "@nylorun/core/contracts";
6
+ export { AgentBuildError, AgentLifecycleError, VerdictSchema, isVerdict, WorkflowBuildError, isBuiltWorkflow, flow, tool, http, defineSchema, ToolError,
7
+ // The harness section `agentManifests.save` makes from a built agent or flow, and the gaps the
8
+ // Runtime refuses between a manifest's two sections, for code that builds a manifest itself.
9
+ harnessSectionOf, sectionGaps, } from "@nylorun/core/define";
10
+ export { AGENT_EVENT_TYPES, AGENT_FOLDER_MEDIA_TYPE, AGENT_STATES, COMPUTE_STATES, RUNTIME_LIMIT_DEFAULTS, SUBJECT_SCOPES, } from "@nylorun/core/contracts";
8
11
  export { AgentsClient, SessionClient, createClient } from "./client.js";
9
12
  export { ArtifactsClient } from "./artifacts.js";
10
13
  export { FilesClient, definitionFileHash } from "./files.js";
11
14
  export { AgentInstancesClient, AgentManifestsClient, AgentMemoriesClient, AgentSecretsClient, ManifestGapsError, agentManifest, requirementGaps, runtimeFiles, } from "./client.js";
12
15
  export { composeManifest, manifestFolders, ComposeError, AGENT_MANIFEST_FILE } from "./compose.js";
13
16
  export { AGENT_FOLDER_FILE, FolderError, FolderInvalidError, diagnosticLine } from "./folder.js";
14
- export { checkFolder } from "./folder-check.js";
15
- export { packFolder, isAgentFolder } from "./folder-pack.js";
17
+ export { checkFolder, folderOfManifest } from "./folder-check.js";
18
+ export { packFolder, isAgentFolder, FOLDER_IGNORE_FILE } from "./folder-pack.js";
19
+ export { ignoreMatcher } from "./folder-ignore.js";
16
20
  export { AccessClient } from "./access.js";
17
21
  export { resolveConnection, ConnectionError } from "./connection.js";
18
22
  export { RuntimeError, IncompatibleRuntimeError } from "./http.js";
@@ -291,7 +291,8 @@ function realInside(root, candidate) {
291
291
  return undefined;
292
292
  try {
293
293
  const real = realpathSync(candidate);
294
- return isInside(root, real) ? real : undefined;
294
+ // Both sides real: a package under a symlinked directory (macOS's /var → /private/var) is inside itself.
295
+ return isInside(realpathSync(root), real) ? real : undefined;
295
296
  }
296
297
  catch {
297
298
  return undefined;
@@ -152,7 +152,8 @@ export declare class SessionClient {
152
152
  cursor: string | null;
153
153
  requestId: string;
154
154
  }>;
155
- approve(interactionId: string, approved: boolean, options: CommandOptions): Promise<{
155
+ /** Approve (or deny) a held tool call. Without an idempotency key, a new one is made. */
156
+ approve(interactionId: string, approved: boolean, options?: Partial<CommandOptions>): Promise<{
156
157
  status: "accepted";
157
158
  turnId: string | null;
158
159
  cursor: string | null;
@@ -164,7 +165,8 @@ export declare class SessionClient {
164
165
  cursor: string | null;
165
166
  requestId: string;
166
167
  }>;
167
- respond(interactionId: string, value: JsonValue, options: CommandOptions): Promise<{
168
+ /** Answer a question the agent asked. Without an idempotency key, a new one is made. */
169
+ respond(interactionId: string, value: JsonValue, options?: Partial<CommandOptions>): Promise<{
168
170
  status: "accepted";
169
171
  turnId: string | null;
170
172
  cursor: string | null;
@@ -168,22 +168,24 @@ export class SessionClient {
168
168
  ...(options.delivery ? { delivery: options.delivery } : {}),
169
169
  }, options.signal);
170
170
  }
171
- approve(interactionId, approved, options) {
171
+ /** Approve (or deny) a held tool call. Without an idempotency key, a new one is made. */
172
+ approve(interactionId, approved, options = {}) {
172
173
  return this.command({
173
174
  type: "approve",
174
175
  interactionId,
175
176
  approved,
176
177
  requestId: options.requestId ?? id(),
177
- idempotencyKey: options.idempotencyKey,
178
+ idempotencyKey: options.idempotencyKey ?? id(),
178
179
  }, options.signal);
179
180
  }
180
- respond(interactionId, value, options) {
181
+ /** Answer a question the agent asked. Without an idempotency key, a new one is made. */
182
+ respond(interactionId, value, options = {}) {
181
183
  return this.command({
182
184
  type: "respond",
183
185
  interactionId,
184
186
  value,
185
187
  requestId: options.requestId ?? id(),
186
- idempotencyKey: options.idempotencyKey,
188
+ idempotencyKey: options.idempotencyKey ?? id(),
187
189
  }, options.signal);
188
190
  }
189
191
  cancel(options) {
@@ -91,6 +91,13 @@ export function loadSkillsFromDirectory(directory, diagnostics, options = {}) {
91
91
  });
92
92
  continue;
93
93
  }
94
+ if (parsed.secrets !== undefined)
95
+ diagnostics.push({
96
+ severity: "warning",
97
+ code: `${prefix}.skill-secrets`,
98
+ message: `Skill '${parsed.name}': secrets in SKILL.md is deprecated (D80), since it is not an Agent Skills key: an agent folder lists a skill's secrets in AGENT.md's skills block, where nylorun migrate folder moves them. It is still read until a later release.`,
99
+ path: skillFile,
100
+ });
94
101
  skills[parsed.name] = {
95
102
  name: parsed.name,
96
103
  description: parsed.description,
@@ -19,9 +19,10 @@ export interface SkillsPart extends PartDeclaration {
19
19
  *
20
20
  * Sets `skills`: each skill's name, description and files (every file of its folder, binary
21
21
  * included, by content hash). A SKILL.md's frontmatter `secrets` (environment variable names)
22
- * become the skill's `secrets` in the harness section when the agent's manifest is saved. The
23
- * client uploads the files the Runtime lacks when it saves the agent's manifest; the Runtime adds
24
- * the skill catalog and serves `load_skill` / `read_skill_resource` from them.
22
+ * become the skill's `secrets` in the harness section when the agent's manifest is saved; that key
23
+ * is deprecated (D80), so it also emits a `DeprecationWarning`. The client uploads the files the
24
+ * Runtime lacks when it saves the agent's manifest; the Runtime adds the skill catalog and serves
25
+ * `load_skill` / `read_skill_resource` from them.
25
26
  */
26
27
  export declare function skills(directory: string): SkillsPart;
27
28
  /**
@@ -14,14 +14,23 @@ import { loadSkillsFromDirectory, resolveSkillsRoot, } from "./load.js";
14
14
  *
15
15
  * Sets `skills`: each skill's name, description and files (every file of its folder, binary
16
16
  * included, by content hash). A SKILL.md's frontmatter `secrets` (environment variable names)
17
- * become the skill's `secrets` in the harness section when the agent's manifest is saved. The
18
- * client uploads the files the Runtime lacks when it saves the agent's manifest; the Runtime adds
19
- * the skill catalog and serves `load_skill` / `read_skill_resource` from them.
17
+ * become the skill's `secrets` in the harness section when the agent's manifest is saved; that key
18
+ * is deprecated (D80), so it also emits a `DeprecationWarning`. The client uploads the files the
19
+ * Runtime lacks when it saves the agent's manifest; the Runtime adds the skill catalog and serves
20
+ * `load_skill` / `read_skill_resource` from them.
20
21
  */
21
22
  export function skills(directory) {
22
23
  const root = resolveSkillsRoot(directory);
23
24
  const diagnostics = [];
24
25
  const loaded = loadSkillsFromDirectory(root, diagnostics);
26
+ // `secrets` in SKILL.md is deprecated (D80): building or saving the agent says so.
27
+ for (const diagnostic of diagnostics)
28
+ if (diagnostic.code === "skills.skill-secrets")
29
+ process.emitWarning(diagnostic.message, {
30
+ type: "DeprecationWarning",
31
+ code: diagnostic.code,
32
+ ...(diagnostic.path === undefined ? {} : { detail: diagnostic.path }),
33
+ });
25
34
  const listed = Object.values(loaded);
26
35
  const id = `skills:${basename(root)}`;
27
36
  if (listed.length === 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nylorun/agents",
3
- "version": "0.27.0-beta",
3
+ "version": "0.28.0-beta",
4
4
  "description": "Nylorun agent definitions and session client",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -43,7 +43,7 @@
43
43
  "test": "vitest run --passWithNoTests"
44
44
  },
45
45
  "dependencies": {
46
- "@nylorun/core": "0.27.0-beta",
46
+ "@nylorun/core": "0.28.0-beta",
47
47
  "gray-matter": "^4.0.3"
48
48
  },
49
49
  "peerDependencies": {