nylorun 0.24.1-beta → 0.25.1-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,62 @@
1
1
  # nylorun
2
2
 
3
+ ## 0.25.1-beta
4
+
5
+ ### Patch Changes
6
+
7
+ - Pin agents to the tested release.
8
+ - Pin runtime to the tested release.
9
+ - Pin studio to the tested release.
10
+ - Updated dependencies [78787e2]
11
+ - @nylorun/agents@0.33.0-beta
12
+
13
+ ## 0.25.0-beta
14
+
15
+ ### Minor Changes
16
+
17
+ - 67ed2ea: `nylorun check --explain` prints each engine feature the agent and its subagents get, on or off,
18
+ with what declares it and the tools and text blocks it adds (`explain` in `--json`). `nylorun
19
+ migrate folder <dir> --to 2` makes an agent folder `agent-folder/2` in place, writing the
20
+ `engine:` declarations that keep what it gets today, after checking that no agent's system text
21
+ or tool list changes (ADR-100).
22
+ - 67ed2ea: Secret routing: the agent folder names, the installation routes (Host feature `secret-routing`, no
23
+ protocol change). A `bearer` or `headers` secret slot (`runtime.json`, `runtime.secrets`) takes the
24
+ gateway's `toolPrefix`, with or without `via`, and a slot's value write
25
+ (`PUT /v1/agents/{id}/secrets/{name}` with `{ value }` or `{ headers }`) takes `via`, with the same
26
+ URL rules as a slot's. At each request the secret's `via` wins, else the slot's in the version the
27
+ Agent runs, else none (the slot's `url`); the slot's `toolPrefix` wins over the secret's and
28
+ applies only with a `via`; the slot's `url` still picks the credential, for MCP servers and HTTP
29
+ tools alike, and an HTTP tool whose URL has `{field}` parameters still fails behind a `via`. A
30
+ write whose `toolPrefix` differs from its slot's is accepted, with a warning in the log. A `via` a
31
+ write copied from its slot follows the slot when the Agent moves, and a fork keeps a secret's own
32
+ `via`. Secret views show the `via` and `toolPrefix` in force, never a value.
33
+ `client.agents.secrets.put` takes `{ value | headers, via?, toolPrefix? }` (`AgentSecretRouting`),
34
+ and `nylorun agents secrets put` takes `--via` and `--tool-prefix`, and a slot's JSON without a
35
+ `type`. The JSON Schemas that gained the slot's `toolPrefix` are published as
36
+ `agent-manifest/v8.2`, `runtime-manifest/v1.1` and `agent-folder/v1/runtime.1`, which `nylorun init`
37
+ maps; the old `$schema` URLs are still read.
38
+
39
+ ### Patch Changes
40
+
41
+ - Pin core to the tested release.
42
+ - Pin agents to the tested release.
43
+ - Pin admin to the tested release.
44
+ - Pin runtime to the tested release.
45
+ - Pin studio to the tested release.
46
+ - Updated dependencies [67ed2ea]
47
+ - Updated dependencies [67ed2ea]
48
+ - Updated dependencies [67ed2ea]
49
+ - Updated dependencies [67ed2ea]
50
+ - Updated dependencies [67ed2ea]
51
+ - Updated dependencies [67ed2ea]
52
+ - Updated dependencies [67ed2ea]
53
+ - Updated dependencies [67ed2ea]
54
+ - Updated dependencies
55
+ - Updated dependencies
56
+ - @nylorun/core@0.34.0-beta
57
+ - @nylorun/agents@0.32.0-beta
58
+ - @nylorun/admin@0.19.10-beta
59
+
3
60
  ## 0.24.1-beta
4
61
 
5
62
  ### Patch Changes
package/README.md CHANGED
@@ -54,11 +54,12 @@ nylorun doctor sandbox [--tenant <name>] [--json] # the sandbox backend the Te
54
54
  # apply, agent-manifests, agents, sessions, artifacts: the Runtime API (an application key, or NYLORUN_TOKEN)
55
55
  # every one takes --tenant <name>, and --as <subject> [--scope <scope>]... to act for a person
56
56
  nylorun init <path> [--description <text>] [--example] # write <path>/AGENT.md, a new agent folder named after the last segment
57
- nylorun check [<dir>...] [--fix] [--json] # compile each agent folder offline: every problem by file, line and column; --fix makes the fixable ones' edits
57
+ nylorun check [<dir>...] [--fix] [--explain] [--json] # compile each agent folder offline: every problem by file, line and column; --fix makes the fixable ones' edits; --explain prints each engine feature on, and what it adds
58
58
  nylorun apply [<dir>...] [--dry-run] [--if-match <n>] [--agent [<id>]] [--watch [--save]] # plan, then save each folder (AGENT.md or agent.manifest.json), its files uploaded
59
59
  nylorun chat <agent> [--session <id>] [--message <text>] # send each line (or --message), print each reply
60
60
  nylorun migrate v8 <agent.json> [--runtime <environment-manifest.json>] [--name <name>] [--out <dir>] # the three files from a v7 manifest; reads no Runtime
61
61
  nylorun migrate folder <dir> [--out <dir>] # the agent folder of a three-file folder; reads no Runtime
62
+ nylorun migrate folder <dir> --to 2 # make an agent folder agent-folder/2, declaring what it gets today
62
63
  nylorun export --folder <name>[@<n>] [--out <dir>] # the agent folder of a saved manifest (JSON or TypeScript)
63
64
  nylorun pull <name>[@<n>] [--out <dir>] # the folder a version was saved from, byte for byte
64
65
  nylorun agent-manifests list [--json] # name, latest version, runtime kind, harness, Agents
@@ -288,7 +289,11 @@ secret of that Agent's vault, a `bearer` token or a `headers` map, with `via` an
288
289
  an identity header when an MCP gateway holds each person's sign-in. Write it with
289
290
  `nylorun agents secrets put <id> <name>` (a slot's value on standard input) or
290
291
  `nylorun agents secrets put <id> <name> <secret.json|->` (the secret's JSON
291
- comes from a file or standard input, never the command line),
292
+ comes from a file or standard input, never the command line; for a slot, only
293
+ `{ "value": … }` or `{ "headers": { … } }`), with `--via <url>` and
294
+ `--tool-prefix <prefix>` for this installation's routing of a `bearer` or
295
+ `headers` secret (Host feature `secret-routing`: the write's `via` wins over the
296
+ slot's, the slot's tool prefix over the write's),
292
297
  `client.agents.secrets.put(id, name, secret)` of `@nylorun/agents` (the
293
298
  Runtime API, an application key) or in Studio.
294
299
  The whole flow, with gateway recipes, is in
@@ -564,6 +569,40 @@ from `mcp.json`) and saves it, uploading its files by hash; the Runtime keeps th
564
569
  folder as the version's source. `--dry-run` also prints the compiled manifest. A
565
570
  change only to a file outside the agent, such as a README, is `unchanged`.
566
571
 
572
+ **What the engine adds (`agent-folder/2`, ADR-100).** An `agent-folder/1` folder (no `format`,
573
+ the default) gets every engine feature by default: with a computer in `runtime.json`, the six
574
+ sandbox tools, `save_artifact` and the outputs folder; always `read_artifact`; and its MCP tools
575
+ deferred behind `tool_search` past a share of the context window. A folder with
576
+ `format: agent-folder/2` gets only what it declares. Memory, skills, `loop` and
577
+ `runtime.selfWake` declare their parts as before; `engine:` declares the rest:
578
+
579
+ <!-- doc-test: folder declared AGENT.md -->
580
+ ```markdown
581
+ ---
582
+ name: declared
583
+ description: Answers from the docs it is given.
584
+ format: agent-folder/2
585
+ memory:
586
+ index: false
587
+ engine:
588
+ artifacts: { read: true }
589
+ toolSearch: false
590
+ ---
591
+ Answer from the documents in the conversation.
592
+ ```
593
+
594
+ `engine.sandbox` lists any of `bash`, `read`, `write`, `edit`, `grep` and `glob`;
595
+ `artifacts.save`, `outputs` and `sandbox` need a computer; and `engine.instructions.<block>`
596
+ replaces a text block's wording with a Markdown file under `engine/` (the blocks are `skills`,
597
+ `sandbox`, `skill_folder`, `artifacts_save`, `outputs`, `artifacts_read`, `wake`, `memory`,
598
+ `plan`, `connection` and `tool_search`; memory's rule that outside text is data stays). A
599
+ subagent declares its own `engine:`, a subset of its parent's. The compiled manifest lists the
600
+ blocks the agent gets, with their versions and hashes (`harness.engine.blocks`).
601
+ `nylorun check --explain` prints each feature on, and the tools and blocks it adds;
602
+ `previewTurn(manifest)` in `@nylorun/agents` returns the exact system text and tool list.
603
+ `nylorun migrate folder <dir> --to 2` writes the declarations that keep what an `agent-folder/1`
604
+ folder gets, so nothing changes until you remove a line.
605
+
567
606
  The loop from an empty folder to an answer:
568
607
 
569
608
  ```
package/dist/cli.js CHANGED
@@ -17,9 +17,11 @@ Agent manifests:
17
17
  write <path>/AGENT.md, a new agent folder named after the
18
18
  path's last segment; --example adds a tool, mcp.json and a
19
19
  trigger
20
- check [<dir>...] [--fix] [--json] compile each agent folder offline and print every problem
20
+ check [<dir>...] [--fix] [--explain] [--json]
21
+ compile each agent folder offline and print every problem
21
22
  by file, line and column (reads no Runtime); --fix makes
22
- the edits the fixable ones name
23
+ the edits the fixable ones name; --explain prints each
24
+ engine feature on, and the tools and text it adds
23
25
  apply [<dir>...] [--dry-run] [--if-match <n>] [--agent [<id>]] [--watch [--save]]
24
26
  save the agent manifest each folder describes (its AGENT.md,
25
27
  or its agent.manifest.json), with its files (Runtime API);
@@ -32,6 +34,8 @@ Agent manifests:
32
34
  and an environment manifest v1 (reads no Runtime)
33
35
  migrate folder <dir> [--out <dir>] write the agent folder of a three-file folder
34
36
  (agent.manifest.json; reads no Runtime)
37
+ migrate folder <dir> --to 2 make an agent folder agent-folder/2, declaring every
38
+ engine feature it gets today, so nothing it gets changes
35
39
  export --folder <name>[@<version>] [--out <dir>]
36
40
  write the agent folder of a saved manifest (Runtime API)
37
41
  pull <name>[@<version>] [--out <dir>]
@@ -45,8 +45,12 @@ NYLORUN_TOKEN).
45
45
  secrets put <id> <name> the value of a slot its manifest declares, piped in
46
46
  on standard input
47
47
  secrets put <id> <name> <secret.json|->
48
- create or replace a secret: {"type": "bearer", ...}, read
49
- from a file or standard input
48
+ create or replace a secret: {"type": "bearer", ...}, or a
49
+ slot's {"headers": {...}} or {"value": ...}, read from a file
50
+ or standard input
51
+ [--via <url>] [--tool-prefix <prefix>]
52
+ a bearer or headers secret's routing: where its requests
53
+ go (over the slot's via) and the gateway's tool prefix
50
54
  secrets rm <id> <name>
51
55
  triggers list <id> its triggers: status, next and last run, as JSON
52
56
  triggers pause|resume <id> <trigger>
@@ -313,23 +317,28 @@ async function secrets(deps, args) {
313
317
  return 0;
314
318
  }
315
319
  if (verb === "put") {
316
- const usage = "nylorun agents secrets put <id> <name> [<secret.json|->]";
317
- const flags = parseFlags(rest, {}, usage);
320
+ const usage = "nylorun agents secrets put <id> <name> [<secret.json|->] [--via <url>] [--tool-prefix <prefix>]";
321
+ const flags = parseFlags(rest, { values: ["--via", "--tool-prefix"] }, usage);
318
322
  if (flags.rest.length < 2 || flags.rest.length > 3)
319
323
  throw usageError(`Usage: ${usage}`);
320
324
  const [id, name, path] = flags.rest;
325
+ // This installation's routing of a bearer or headers secret (`secret-routing`).
326
+ const via = flags.values.get("--via");
327
+ const toolPrefix = flags.values.get("--tool-prefix");
328
+ const routing = { ...(via === undefined ? {} : { via }), ...(toolPrefix === undefined ? {} : { toolPrefix }) };
321
329
  if (path === undefined) {
322
330
  // A slot its manifest declares (D64): the value only, piped in; the binding is the slot's.
323
331
  const value = await readSecret(deps, `value of ${name}`, `printf %s "$TOKEN" | nylorun agents secrets put ${id} ${name}`);
324
332
  const { client } = await runtimeClient(deps, flags);
325
- printJson(deps, await runtimeCall(() => client.agents.secrets.put(id, name, { value })));
333
+ printJson(deps, await runtimeCall(() => client.agents.secrets.put(id, name, { value, ...routing })));
326
334
  return 0;
327
335
  }
328
336
  const secret = await readJson(path, "The secret");
329
- if (typeof secret.type !== "string")
330
- throw usageError('The secret needs a "type".');
337
+ // Without a type, a slot's value: { "value": … } or { "headers": { … } }, with via and toolPrefix.
338
+ if (typeof secret.type !== "string" && secret.value === undefined && secret.headers === undefined)
339
+ throw usageError('The secret needs a "type", or is a slot\'s "value" or "headers".');
331
340
  const { client } = await runtimeClient(deps, flags);
332
- printJson(deps, await runtimeCall(() => client.agents.secrets.put(id, name, secret)));
341
+ printJson(deps, await runtimeCall(() => client.agents.secrets.put(id, name, { ...secret, ...routing })));
333
342
  return 0;
334
343
  }
335
344
  if (verb === "rm" || verb === "delete") {
@@ -1,13 +1,16 @@
1
1
  import { type PackedFolder } from "@nylorun/agents";
2
2
  import { type AgentFolderDiagnostic } from "@nylorun/core/folder";
3
+ import { type ExplainedAgent } from "@nylorun/core/engine";
3
4
  import { type StackDeps } from "../stack/commands.js";
4
5
  export { initCommand, initUsage } from "./init.js";
5
- export declare const checkUsage = "nylorun check [<dir>...] [--fix] [--json]\n\nCompiles each agent folder (default .) offline, with the compiler the Runtime runs, and prints\nevery problem by file, line and column. A folder holds AGENT.md; a folder without one checks each\nsubfolder that has one. Exits 1 when a folder does not compile. Reads no Runtime.\n\nA problem one edit fixes says (fixable with --fix): an unknown key with one did-you-mean, CRLF\nline ends, and secrets in a SKILL.md, which go in AGENT.md's skills block.\n\n --fix make those edits in the folder's files, print each, then\n check again; nothing is written without it, and a file\n that changed since it was checked is left as it is\n --json print { \"folders\": [{ path, ok, manifest?, diagnostics }] }\n for CI: whether each folder compiles, the manifest it\n compiles to, and every diagnostic, each fixable one with\n its fix; with --fix, also what was fixed and what was not";
6
+ export declare const checkUsage = "nylorun check [<dir>...] [--fix] [--explain] [--json]\n\nCompiles each agent folder (default .) offline, with the compiler the Runtime runs, and prints\nevery problem by file, line and column. A folder holds AGENT.md; a folder without one checks each\nsubfolder that has one. Exits 1 when a folder does not compile. Reads no Runtime.\n\nA problem one edit fixes says (fixable with --fix): an unknown key with one did-you-mean, CRLF\nline ends, and secrets in a SKILL.md, which go in AGENT.md's skills block.\n\n --fix make those edits in the folder's files, print each, then\n check again; nothing is written without it, and a file\n that changed since it was checked is left as it is\n --explain print each engine feature the agent and its subagents get\n (agent-folder/2 declares them; agent-folder/1 gets them all\n by default), with the tools and text blocks each adds\n --json print { \"folders\": [{ path, ok, manifest?, diagnostics }] }\n for CI: whether each folder compiles, the manifest it\n compiles to, and every diagnostic, each fixable one with\n its fix; with --fix, also what was fixed and what was not;\n with --explain, the features as explain";
6
7
  /** The agent folders under `dir`: `dir` when it holds an `AGENT.md`, else each subfolder that does. */
7
8
  export declare function agentFolders(dir: string): Promise<string[]>;
8
9
  /** Packs the folder, or a `CliError` naming why it cannot be. */
9
10
  export declare function packed(folder: string): Promise<PackedFolder>;
10
11
  /** A folder's diagnostics as lines, each path relative to `cwd`; `fixable` marks those with a fix. */
11
12
  export declare function diagnosticLines(folder: string, cwd: string, diagnostics: readonly AgentFolderDiagnostic[], fixable?: boolean): string[];
13
+ /** `check --explain`'s lines for one folder: each agent, then each feature, on or off. */
14
+ export declare function explainLines(agents: readonly ExplainedAgent[]): string[];
12
15
  /** `nylorun check [dir…] [--fix] [--json]` */
13
16
  export declare function checkCommand(deps: StackDeps, args: readonly string[]): Promise<number>;
@@ -10,11 +10,12 @@ import { readdir, readFile, writeFile } from "node:fs/promises";
10
10
  import { basename, join, relative, resolve } from "node:path";
11
11
  import { AGENT_FOLDER_FILE, FolderError, checkFolder, diagnosticLine, isAgentFolder, packFolder, } from "@nylorun/agents";
12
12
  import { applyFolderFixes } from "@nylorun/core/folder";
13
+ import { explainEngine } from "@nylorun/core/engine";
13
14
  import { CliError } from "../errors.js";
14
15
  import { parseStackFlags } from "../stack/commands.js";
15
16
  import { usageError } from "./runtime.js";
16
17
  export { initCommand, initUsage } from "./init.js";
17
- export const checkUsage = `nylorun check [<dir>...] [--fix] [--json]
18
+ export const checkUsage = `nylorun check [<dir>...] [--fix] [--explain] [--json]
18
19
 
19
20
  Compiles each agent folder (default .) offline, with the compiler the Runtime runs, and prints
20
21
  every problem by file, line and column. A folder holds AGENT.md; a folder without one checks each
@@ -26,10 +27,14 @@ line ends, and secrets in a SKILL.md, which go in AGENT.md's skills block.
26
27
  --fix make those edits in the folder's files, print each, then
27
28
  check again; nothing is written without it, and a file
28
29
  that changed since it was checked is left as it is
30
+ --explain print each engine feature the agent and its subagents get
31
+ (agent-folder/2 declares them; agent-folder/1 gets them all
32
+ by default), with the tools and text blocks each adds
29
33
  --json print { "folders": [{ path, ok, manifest?, diagnostics }] }
30
34
  for CI: whether each folder compiles, the manifest it
31
35
  compiles to, and every diagnostic, each fixable one with
32
- its fix; with --fix, also what was fixed and what was not`;
36
+ its fix; with --fix, also what was fixed and what was not;
37
+ with --explain, the features as explain`;
33
38
  /** The agent folders under `dir`: `dir` when it holds an `AGENT.md`, else each subfolder that does. */
34
39
  export async function agentFolders(dir) {
35
40
  const root = resolve(dir);
@@ -107,11 +112,28 @@ async function fixFolder(pack, diagnostics, cwd) {
107
112
  await writeFile(target, after);
108
113
  return { fixed: result.changes, unfixed };
109
114
  }
115
+ /** `check --explain`'s lines for one folder: each agent, then each feature, on or off. */
116
+ export function explainLines(agents) {
117
+ const lines = [];
118
+ for (const agent of agents) {
119
+ lines.push(` ${agent.agent}: ${agent.declared ? "declares its engine features (agent-folder/2)" : "every engine feature by default (agent-folder/1)"}`);
120
+ for (const item of agent.features) {
121
+ const adds = [
122
+ ...(item.tools.length > 0 ? [`tools ${item.tools.join(", ")}`] : []),
123
+ ...(item.blocks.length > 0 ? [`text ${item.blocks.join(", ")}`] : []),
124
+ ...(item.note === undefined ? [] : [item.note]),
125
+ ];
126
+ lines.push(` ${item.on ? "on " : "off"} ${item.feature} (${item.declaredBy})${item.on && adds.length > 0 ? `: ${adds.join("; ")}` : item.note && item.feature === "toolSearch" ? `: ${item.note}` : ""}`);
127
+ }
128
+ }
129
+ return lines;
130
+ }
110
131
  /** `nylorun check [dir…] [--fix] [--json]` */
111
132
  export async function checkCommand(deps, args) {
112
- const flags = parseStackFlags(args, { booleans: ["--json", "--fix"] }, checkUsage);
133
+ const flags = parseStackFlags(args, { booleans: ["--json", "--fix", "--explain"] }, checkUsage);
113
134
  const json = flags.booleans.has("--json");
114
135
  const fix = flags.booleans.has("--fix");
136
+ const explain = flags.booleans.has("--explain");
115
137
  const cwd = deps.cwd ?? process.cwd();
116
138
  const folders = [];
117
139
  for (const dir of flags.rest.length > 0 ? flags.rest : ["."]) {
@@ -146,6 +168,7 @@ export async function checkCommand(deps, args) {
146
168
  ...(compiled.manifest ? { manifest: compiled.manifest } : {}),
147
169
  diagnostics: compiled.diagnostics,
148
170
  ...(pack.skipped.length > 0 ? { skipped: pack.skipped } : {}),
171
+ ...(explain && compiled.manifest ? { explain: explainEngine(compiled.manifest) } : {}),
149
172
  ...(fix ? { fixed: fixed.fixed, unfixed: fixed.unfixed } : {}),
150
173
  });
151
174
  continue;
@@ -164,8 +187,12 @@ export async function checkCommand(deps, args) {
164
187
  deps.err(line);
165
188
  const label = relative(cwd, folder) || basename(folder);
166
189
  const files = `${pack.entries.length} ${pack.entries.length === 1 ? "file" : "files"}`;
167
- if (compiled.manifest)
190
+ if (compiled.manifest) {
168
191
  deps.out(`${label}: ok, agent manifest ${compiled.manifest.name} (${files}, ${compiled.compiler})`);
192
+ if (explain)
193
+ for (const line of explainLines(explainEngine(compiled.manifest)))
194
+ deps.out(line);
195
+ }
169
196
  else {
170
197
  const errors = compiled.diagnostics.filter((item) => item.severity === "error").length;
171
198
  deps.err(`${label}: ${errors} ${errors === 1 ? "error" : "errors"}`);
@@ -49,7 +49,7 @@ export function schemaMappings(root, folder) {
49
49
  // Folders under a parent (agents/) share one pattern; a folder at the root is named alone.
50
50
  const base = parent === "" ? relative(root, folder).split(sep).join("/") : `${parent}/**`;
51
51
  return [
52
- { fileMatch: [`${base}/runtime.json`], url: `${SCHEMA_BASE}/runtime.json` },
52
+ { fileMatch: [`${base}/runtime.json`], url: `${SCHEMA_BASE}/runtime.1.json` },
53
53
  { fileMatch: [`${base}/tools/*.json`], url: `${SCHEMA_BASE}/tool.json` },
54
54
  { fileMatch: [`${base}/flow.json`], url: `${SCHEMA_BASE}/flow.json` },
55
55
  { fileMatch: [`${base}/mcp.json`, `!${base}/plugins/**`], url: `${SCHEMA_BASE}/mcp.json` },
@@ -0,0 +1,12 @@
1
+ import type { StackDeps } from "../stack/commands.js";
2
+ export declare const migrateFormatUsage = "nylorun migrate folder <dir> --to 2\n\nMakes the agent folder <dir> agent-folder/2, in place: it adds format: agent-folder/2 to its\nAGENT.md, and to it and each subagent's AGENT.md the engine: block that declares every engine\nfeature the folder gets today by default (the sandbox tools, save_artifact and the outputs folder\nwhen runtime.json has a computer, read_artifact, tool search). Nothing the model reads or may\ncall changes, which it checks before it writes; then remove the lines the agent does not need.\nReads no Runtime.";
3
+ /** The `engine:` block that keeps what an agent-folder/1 agent gets, as YAML lines. */
4
+ export declare function engineLines(computer: boolean): string[];
5
+ /**
6
+ * `text`, an `AGENT.md`, with `lines` added at the end of its frontmatter and, when `format` is
7
+ * set, its `format:` line replaced by `format: <format>` (first in the frontmatter when it had
8
+ * none). Undefined when it has no frontmatter.
9
+ */
10
+ export declare function withFrontmatter(text: string, lines: readonly string[], format?: string): string | undefined;
11
+ /** `nylorun migrate folder <dir> --to 2` */
12
+ export declare function migrateFormatCommand(deps: StackDeps, dir: string, to: string): Promise<number>;
@@ -0,0 +1,141 @@
1
+ /**
2
+ * `nylorun migrate folder <dir> --to 2` (ADR-100): makes an `agent-folder/1` folder an
3
+ * `agent-folder/2` one, in place, declaring every engine feature it gets today by default, so no
4
+ * prompt and no tool changes until the author removes a line:
5
+ *
6
+ * - `format: agent-folder/2`;
7
+ * - `engine:` in `AGENT.md` and in each `agents/<id>/AGENT.md`: the six sandbox tools,
8
+ * `save_artifact` and the outputs folder when the runtime has a computer, `read_artifact`
9
+ * always, and tool search.
10
+ *
11
+ * It checks the folder before and after, offline, with the compiler the Runtime runs, and that
12
+ * every agent's system text and tool list are the same (`previewTurn`), and writes nothing when
13
+ * they are not. Reads no Runtime.
14
+ */
15
+ import { createHash } from "node:crypto";
16
+ import { readFile, writeFile } from "node:fs/promises";
17
+ import { join, relative } from "node:path";
18
+ import { AGENT_FOLDER_FILE, checkFolder } from "@nylorun/agents";
19
+ import { SANDBOX_TOOL_NAMES } from "@nylorun/core/define";
20
+ import { previewTurn } from "@nylorun/core/engine";
21
+ import { CliError } from "../errors.js";
22
+ import { diagnosticLines, packed } from "./folder.js";
23
+ import { usageError } from "./runtime.js";
24
+ export const migrateFormatUsage = `nylorun migrate folder <dir> --to 2
25
+
26
+ Makes the agent folder <dir> agent-folder/2, in place: it adds format: agent-folder/2 to its
27
+ AGENT.md, and to it and each subagent's AGENT.md the engine: block that declares every engine
28
+ feature the folder gets today by default (the sandbox tools, save_artifact and the outputs folder
29
+ when runtime.json has a computer, read_artifact, tool search). Nothing the model reads or may
30
+ call changes, which it checks before it writes; then remove the lines the agent does not need.
31
+ Reads no Runtime.`;
32
+ /** The `engine:` block that keeps what an agent-folder/1 agent gets, as YAML lines. */
33
+ export function engineLines(computer) {
34
+ return computer
35
+ ? [
36
+ "engine:",
37
+ ` sandbox: [${SANDBOX_TOOL_NAMES.join(", ")}]`,
38
+ " artifacts: { read: true, save: true }",
39
+ " outputs: true",
40
+ " toolSearch: true",
41
+ ]
42
+ : ["engine:", " artifacts: { read: true, save: false }", " toolSearch: true"];
43
+ }
44
+ /**
45
+ * `text`, an `AGENT.md`, with `lines` added at the end of its frontmatter and, when `format` is
46
+ * set, its `format:` line replaced by `format: <format>` (first in the frontmatter when it had
47
+ * none). Undefined when it has no frontmatter.
48
+ */
49
+ export function withFrontmatter(text, lines, format) {
50
+ const eol = text.includes("\r\n") ? "\r\n" : "\n";
51
+ const bom = text.startsWith("") ? "" : "";
52
+ const all = text.slice(bom.length).split(/\r?\n/);
53
+ if (all[0]?.trimEnd() !== "---")
54
+ return undefined;
55
+ const end = all.findIndex((line, index) => index > 0 && line.trimEnd() === "---");
56
+ if (end < 0)
57
+ return undefined;
58
+ let front = all.slice(1, end);
59
+ if (format !== undefined) {
60
+ const at = front.findIndex((line) => /^format\s*:/.test(line));
61
+ if (at >= 0)
62
+ front[at] = `format: ${format}`;
63
+ else
64
+ front = [`format: ${format}`, ...front];
65
+ }
66
+ return bom + ["---", ...front, ...lines, ...all.slice(end)].join(eol);
67
+ }
68
+ /** What each agent of a compiled folder reads and may call, by agent, for a comparison. */
69
+ function seen(manifest) {
70
+ if ("kind" in manifest.harness)
71
+ return "";
72
+ const preview = previewTurn(manifest);
73
+ const one = (item) => ({ system: item.system, tools: item.tools });
74
+ return JSON.stringify({ root: one(preview), pinned: preview.pinned, agents: Object.fromEntries(Object.entries(preview.agents).map(([id, item]) => [id, one(item)])) });
75
+ }
76
+ /** `nylorun migrate folder <dir> --to 2` */
77
+ export async function migrateFormatCommand(deps, dir, to) {
78
+ if (to !== "2" && to !== "agent-folder/2")
79
+ throw usageError(`--to takes 2 (agent-folder/2).\n${migrateFormatUsage}`);
80
+ const cwd = deps.cwd ?? process.cwd();
81
+ const where = relative(cwd, dir) || ".";
82
+ const before = await checkFolder(await packed(dir));
83
+ if (!before.manifest) {
84
+ for (const line of diagnosticLines(dir, cwd, before.diagnostics))
85
+ deps.err(line);
86
+ throw new CliError(`${where} does not compile: fix it first (nylorun check ${where}).`, 1);
87
+ }
88
+ if (before.compiler === "agent-folder/2") {
89
+ deps.out(`${where} is agent-folder/2 already: nothing to do.`);
90
+ return 0;
91
+ }
92
+ const harness = before.manifest.harness;
93
+ if ("kind" in harness)
94
+ throw new CliError(`${where} is a flow: migrate each agent folder it holds instead.`, 1);
95
+ const kind = before.manifest.runtime?.kind ?? "none";
96
+ const computer = kind === "virtual" || kind === "pod";
97
+ // The root, then each subagent (`agents/<id>/`), each with the declarations it gets today.
98
+ const edits = new Map();
99
+ const read = async (path) => readFile(join(dir, ...path.split("/")), "utf8");
100
+ const root = withFrontmatter(await read(AGENT_FOLDER_FILE), engineLines(computer), "agent-folder/2");
101
+ if (root === undefined)
102
+ throw new CliError(`${join(where, AGENT_FOLDER_FILE)} has no frontmatter.`, 1);
103
+ edits.set(AGENT_FOLDER_FILE, root);
104
+ for (const tool of harness.tools ?? []) {
105
+ if (!tool.agent)
106
+ continue;
107
+ const path = `agents/${tool.name}/${AGENT_FOLDER_FILE}`;
108
+ const edited = withFrontmatter(await read(path), engineLines(computer));
109
+ if (edited === undefined)
110
+ throw new CliError(`${join(where, path)} has no frontmatter.`, 1);
111
+ edits.set(path, edited);
112
+ }
113
+ // The folder as it would be, compiled and compared before anything is written.
114
+ const pack = await packed(dir);
115
+ const encoder = new TextEncoder();
116
+ const files = new Map(pack.files);
117
+ const entries = pack.entries.map((entry) => {
118
+ const text = edits.get(entry.path);
119
+ if (text === undefined)
120
+ return entry;
121
+ const bytes = encoder.encode(text);
122
+ const source = files.get(entry.path);
123
+ files.set(entry.path, { ...source, size: bytes.byteLength, read: async () => bytes });
124
+ return { ...entry, size: bytes.byteLength, sha256: sha256Hex(bytes) };
125
+ });
126
+ const after = await checkFolder({ ...pack, entries, files });
127
+ if (!after.manifest) {
128
+ for (const line of diagnosticLines(dir, cwd, after.diagnostics))
129
+ deps.err(line);
130
+ throw new CliError(`${where} would not compile as agent-folder/2: nothing was written.`, 1);
131
+ }
132
+ if (seen(after.manifest) !== seen(before.manifest))
133
+ throw new CliError(`${where} as agent-folder/2 would change what its model reads or may call: nothing was written.`, 1);
134
+ for (const [path, text] of edits)
135
+ await writeFile(join(dir, ...path.split("/")), text);
136
+ for (const path of edits.keys())
137
+ deps.out(`wrote ${join(where, path)}`);
138
+ deps.out(`${where} is agent-folder/2, and its agents get what they got before. Remove the engine features they do not use, and see what each adds with nylorun check --explain ${where}.`);
139
+ return 0;
140
+ }
141
+ const sha256Hex = (bytes) => createHash("sha256").update(bytes).digest("hex");
@@ -26,6 +26,8 @@ import { harnessSectionOf, sectionGaps } from "@nylorun/core/define";
26
26
  import { CliError } from "../errors.js";
27
27
  import { parseStackFlags } from "../stack/commands.js";
28
28
  import { migrateFolderCommand, migrateFolderUsage } from "./convert.js";
29
+ import { migrateFormatCommand, migrateFormatUsage } from "./migrate-format.js";
30
+ import { isHelp } from "../errors.js";
29
31
  import { readJson, usageError } from "./runtime.js";
30
32
  export const migrateUsage = `nylorun migrate v8 <agent.json> [--runtime <environment-manifest.json>] [--name <name>] [--out <dir>]
31
33
 
@@ -69,11 +71,24 @@ function hasSkills(loop) {
69
71
  export async function migrateCommand(deps, args) {
70
72
  const [version, ...rest] = args;
71
73
  if (version === "--help" || version === "-h") {
72
- deps.out(`${migrateUsage}\n\n${migrateFolderUsage}`);
74
+ deps.out(`${migrateUsage}\n\n${migrateFolderUsage}\n\n${migrateFormatUsage}`);
73
75
  return 0;
74
76
  }
75
- if (version === "folder")
76
- return migrateFolderCommand(deps, rest);
77
+ if (version === "folder") {
78
+ // `--to 2`: an agent folder made agent-folder/2 in place (ADR-100).
79
+ const to = rest.indexOf("--to");
80
+ if (to < 0)
81
+ return migrateFolderCommand(deps, rest);
82
+ const value = rest[to + 1];
83
+ const others = rest.filter((_, index) => index !== to && index !== to + 1);
84
+ if (others.some(isHelp)) {
85
+ deps.out(migrateFormatUsage);
86
+ return 0;
87
+ }
88
+ if (value === undefined || value.startsWith("-") || others.length !== 1 || others[0].startsWith("-"))
89
+ throw usageError(`Usage: ${migrateFormatUsage.split("\n")[0]}`);
90
+ return migrateFormatCommand(deps, resolve(deps.cwd ?? process.cwd(), others[0]), value);
91
+ }
77
92
  if (version !== "v8")
78
93
  throw usageError(version === undefined ? migrateUsage : `nylorun migrate knows v8 and folder only.\n${migrateUsage}\n\n${migrateFolderUsage}`);
79
94
  const flags = parseStackFlags(rest, { values: ["--runtime", "--name", "--out"] }, migrateUsage);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nylorun",
3
- "version": "0.24.1-beta",
3
+ "version": "0.25.1-beta",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "description": "Set up and run local Nylorun Tenants (Runtime and Studio) with Docker Compose, and act on a Tenant through its Runtime and Management APIs",
@@ -30,9 +30,9 @@
30
30
  "prepack": "npm run build"
31
31
  },
32
32
  "dependencies": {
33
- "@nylorun/admin": "0.19.9-beta",
34
- "@nylorun/agents": "0.31.1-beta",
35
- "@nylorun/core": "0.33.0-beta"
33
+ "@nylorun/admin": "0.19.10-beta",
34
+ "@nylorun/agents": "0.33.0-beta",
35
+ "@nylorun/core": "0.34.0-beta"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@earendil-works/pi-ai": "1.3.0",
@@ -46,7 +46,7 @@
46
46
  "nylo": "dist/nylo.js"
47
47
  },
48
48
  "nylorun": {
49
- "runtime": "0.38.0-beta",
50
- "studio": "0.35.1-beta"
49
+ "runtime": "0.40.0-beta",
50
+ "studio": "0.35.3-beta"
51
51
  }
52
52
  }