nylorun 0.15.1-beta → 0.17.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/README.md +136 -74
  3. package/dist/cli.js +31 -12
  4. package/dist/client/agent-manifests.d.ts +4 -0
  5. package/dist/client/agent-manifests.js +100 -0
  6. package/dist/client/agents.d.ts +1 -3
  7. package/dist/client/agents.js +459 -117
  8. package/dist/client/apply.d.ts +7 -0
  9. package/dist/client/apply.js +99 -0
  10. package/dist/client/artifacts.js +3 -3
  11. package/dist/client/doctor.d.ts +1 -1
  12. package/dist/client/doctor.js +2 -2
  13. package/dist/client/io.d.ts +2 -0
  14. package/dist/client/io.js +11 -0
  15. package/dist/client/mcp.d.ts +5 -3
  16. package/dist/client/mcp.js +28 -25
  17. package/dist/client/migrate.d.ts +4 -0
  18. package/dist/client/migrate.js +140 -0
  19. package/dist/client/model/pi-ai.d.ts +1 -1
  20. package/dist/client/model/pi-ai.js +1 -1
  21. package/dist/client/models.d.ts +5 -2
  22. package/dist/client/models.js +17 -8
  23. package/dist/client/runtime.js +3 -3
  24. package/dist/client/sessions.js +29 -18
  25. package/dist/client/settings.d.ts +5 -5
  26. package/dist/client/settings.js +38 -18
  27. package/dist/client/tenant.d.ts +1 -1
  28. package/dist/client/tenant.js +10 -7
  29. package/dist/project/seed.js +2 -1
  30. package/dist/sandbox/commands.js +10 -8
  31. package/dist/sandbox/kubectl.js +2 -1
  32. package/dist/sandbox/manifests.js +3 -2
  33. package/dist/sandbox/probe.js +9 -7
  34. package/dist/stack/commands.js +12 -11
  35. package/dist/stack/compose-file.d.ts +3 -0
  36. package/dist/stack/compose-file.js +32 -36
  37. package/dist/stack/operator-keys.js +7 -6
  38. package/dist/stack/ports.js +2 -1
  39. package/dist/stack/studio-login.js +2 -1
  40. package/dist/tuning.d.ts +90 -0
  41. package/dist/tuning.js +143 -0
  42. package/package.json +9 -9
  43. package/dist/client/environment-manifests.d.ts +0 -10
  44. package/dist/client/environment-manifests.js +0 -173
  45. package/dist/client/environments.d.ts +0 -4
  46. package/dist/client/environments.js +0 -295
@@ -0,0 +1,99 @@
1
+ /**
2
+ * `nylorun apply [dir…]`: saves the agent manifest each folder describes (protocol 16, track T2),
3
+ * through the Runtime API. A folder's `agent.manifest.json` names its harness and runtime files;
4
+ * `composeManifest` (`@nylorun/agents`) resolves them, hashes the skills, plugins and runtime
5
+ * files they name and checks the harness file's `requires`. Each manifest is planned first
6
+ * (a dry run: from → to, each change, warnings, errors), and saved under `If-Match` set to the
7
+ * version the plan was made against, so a save that raced another fails (`412 manifest_changed`)
8
+ * instead of overwriting it. A folder without an `agent.manifest.json` applies each subfolder
9
+ * that has one (`nylorun apply agents/`).
10
+ */
11
+ import { resolve } from "node:path";
12
+ import { ComposeError, PluginError, SkillsError, composeManifest, manifestFolders, } from "@nylorun/agents";
13
+ import { CliError } from "../errors.js";
14
+ import { integerFlag, parseFlags, runtimeCall, runtimeClient, runtimeFlagsUsage, usageError } from "./runtime.js";
15
+ export const applyUsage = `nylorun apply [<dir>...] [--dry-run] [--if-match <n>]
16
+
17
+ Saves the agent manifest each folder describes (default .), through the Runtime API with an
18
+ application key (or NYLORUN_TOKEN). A folder holds agent.manifest.json, which names its
19
+ harness.manifest.json and runtime.manifest.json (paths relative to the file that names them);
20
+ a folder without one applies each subfolder that has one.
21
+
22
+ Each manifest is planned first: the version it moves from and to, each change (section, path,
23
+ op), warnings and errors. With no errors it is saved, its skills', plugins' and runtime files
24
+ uploaded first, under If-Match set to the version planned against (0 for a new manifest).
25
+ Errors, the gaps between its sections included, save nothing and exit 1.
26
+
27
+ --dry-run print the plans and save nothing
28
+ --if-match <n> save only over version n (one folder)
29
+
30
+ ${runtimeFlagsUsage}`;
31
+ /** What a folder could not be composed for, as a person reads it. */
32
+ function composeFailure(folder, error) {
33
+ if (error instanceof ComposeError || error instanceof SkillsError || error instanceof PluginError || error instanceof TypeError)
34
+ throw new CliError(`${folder}: ${error.message}`, 1);
35
+ throw error;
36
+ }
37
+ /** A plan as lines: the move, then each change, warning and error. */
38
+ export function planLines(plan) {
39
+ const move = plan.unchanged
40
+ ? `unchanged at version ${plan.to}`
41
+ : plan.from === null
42
+ ? `new, version ${plan.to}`
43
+ : `version ${plan.from} → ${plan.to}`;
44
+ return [
45
+ `${plan.name}: ${move}${plan.behind > 0 ? ` (${plan.behind} ${plan.behind === 1 ? "Agent" : "Agents"} on older versions)` : ""}`,
46
+ ...plan.changes.map((change) => ` ${change.section} ${change.path} ${change.op}`),
47
+ ...plan.warnings.map((warning) => ` warning: ${warning.message}`),
48
+ ...plan.errors.map((error) => ` error: ${error}`),
49
+ ];
50
+ }
51
+ /** `nylorun apply [dir…] [--dry-run] [--if-match <n>]` */
52
+ export async function applyCommand(deps, args) {
53
+ if (args[0] === "--help" || args[0] === "-h") {
54
+ deps.out(applyUsage);
55
+ return 0;
56
+ }
57
+ const usage = "nylorun apply [<dir>...] [--dry-run] [--if-match <n>]";
58
+ const flags = parseFlags(args, { booleans: ["--dry-run"], values: ["--if-match"] }, usage);
59
+ const ifMatch = integerFlag(flags, "--if-match");
60
+ const cwd = deps.cwd ?? process.cwd();
61
+ const folders = [];
62
+ for (const dir of flags.rest.length > 0 ? flags.rest : ["."]) {
63
+ const found = await manifestFolders(resolve(cwd, dir)).catch((error) => composeFailure(dir, error));
64
+ if (found.length === 0)
65
+ throw usageError(`${dir} holds no agent.manifest.json, and no subfolder does.`);
66
+ folders.push(...found);
67
+ }
68
+ if (ifMatch !== undefined && folders.length > 1)
69
+ throw usageError("--if-match takes one manifest: name one folder.");
70
+ // Every folder composes before anything is sent: a broken file stops the whole apply.
71
+ const composed = [];
72
+ for (const folder of folders)
73
+ composed.push({ folder, manifest: await composeManifest(folder).catch((error) => composeFailure(folder, error)) });
74
+ const { client, connection } = await runtimeClient(deps, flags);
75
+ let blocked = false;
76
+ for (const { manifest } of composed) {
77
+ for (const warning of manifest.warnings)
78
+ deps.err(`${manifest.manifest.name}: warning: ${warning}`);
79
+ // The plan's errors include the gaps the composer found (its sections, the harness's requires).
80
+ const plan = await runtimeCall(() => client.agentManifests.plan(manifest, ifMatch === undefined ? {} : { ifMatch }));
81
+ for (const line of planLines(plan))
82
+ deps.out(line);
83
+ if (plan.errors.length > 0) {
84
+ blocked = true;
85
+ continue;
86
+ }
87
+ if (flags.booleans.has("--dry-run") || plan.unchanged)
88
+ continue;
89
+ const saved = await runtimeCall(() => client.agentManifests.save(manifest, { ifMatch: ifMatch ?? plan.from ?? 0 }));
90
+ deps.err(saved.unchanged
91
+ ? `Agent manifest ${saved.manifest.name} is unchanged at version ${saved.manifest.version} on ${connection.label}.`
92
+ : `Saved agent manifest ${saved.manifest.name} version ${saved.manifest.version} on ${connection.label}.`);
93
+ for (const warning of saved.warnings)
94
+ deps.err(` warning: ${warning.message}`);
95
+ }
96
+ if (blocked)
97
+ deps.err("Nothing was saved for a manifest with errors.");
98
+ return blocked ? 1 : 0;
99
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * `nylorun artifacts`: the Tenant's file artifacts (`/v1/artifacts`, Runtime API): upload a file
3
3
  * (to name in a message with `sessions send --file`), list, download, browse a folder artifact
4
- * (an Environment's disk), share a version with a capability link, and delete.
4
+ * (an Agent's disk versions), share a version with a capability link, and delete.
5
5
  */
6
6
  import { createWriteStream } from "node:fs";
7
7
  import { readFile } from "node:fs/promises";
@@ -62,8 +62,8 @@ async function list(deps, args) {
62
62
  String(item.latestVersion),
63
63
  item.sessionId
64
64
  ? `session ${item.sessionId}`
65
- : item.environmentId
66
- ? `environment ${item.environmentId}`
65
+ : item.agentId
66
+ ? `agent ${item.agentId}`
67
67
  : "tenant",
68
68
  ]),
69
69
  ]);
@@ -12,7 +12,7 @@ export type SandboxReport = {
12
12
  version?: string;
13
13
  }[];
14
14
  defaultImage?: string;
15
- /** The Tenant's Environment settings in force (`environments.config`, protocol 11). */
15
+ /** The Tenant's settings for Agents' environments in force (`environments.config`). */
16
16
  config?: {
17
17
  limits?: {
18
18
  network?: readonly string[];
@@ -45,7 +45,7 @@ export async function doctorSandbox(deps, options) {
45
45
  ]);
46
46
  rows.push([
47
47
  "sessions",
48
- "get a sandbox only on an Environment (createSession({ environment }) names one)",
48
+ "run on an Agent, with the sandbox its manifest's runtime.kind gives (createSession({ agent }))",
49
49
  ]);
50
50
  const limits = report.config?.limits;
51
51
  if (limits)
@@ -53,7 +53,7 @@ export async function doctorSandbox(deps, options) {
53
53
  "limits",
54
54
  [
55
55
  limits.idle ? `idle ${limits.idle}` : undefined,
56
- limits.environments === undefined ? undefined : `${limits.environments} environments`,
56
+ limits.environments === undefined ? undefined : `${limits.environments} Agents`,
57
57
  limits.network ? `${limits.network.length} allowed hosts` : undefined,
58
58
  ]
59
59
  .filter((item) => item !== undefined)
@@ -14,6 +14,8 @@ export interface BodySchema<T> {
14
14
  };
15
15
  };
16
16
  }
17
+ /** The text in file `source`, or on standard input for `-`, as it is. */
18
+ export declare function readText(deps: StackDeps, source: string): Promise<string>;
17
19
  /**
18
20
  * The JSON body in file `source`, or on standard input for `-`, as `schema` parses it. Fields
19
21
  * named in `readOnly` (a view's, so a `get` can be edited and put back) are dropped first. A
package/dist/client/io.js CHANGED
@@ -13,6 +13,17 @@ async function readStdin(deps, refusal) {
13
13
  throw usageError(refusal);
14
14
  return await deps.stdin.text();
15
15
  }
16
+ /** The text in file `source`, or on standard input for `-`, as it is. */
17
+ export async function readText(deps, source) {
18
+ if (source === "-")
19
+ return readStdin(deps, "Pipe the text in on standard input, or name a file.");
20
+ try {
21
+ return await readFile(resolve(deps.cwd ?? process.cwd(), source), "utf8");
22
+ }
23
+ catch (error) {
24
+ throw usageError(`Cannot read ${source}: ${error.code ?? error}.`);
25
+ }
26
+ }
16
27
  /**
17
28
  * The JSON body in file `source`, or on standard input for `-`, as `schema` parses it. Fields
18
29
  * named in `readOnly` (a view's, so a `get` can be edited and put back) are dropped first. A
@@ -1,11 +1,13 @@
1
1
  import type { McpPreview } from "@nylorun/core/contracts";
2
2
  import { type StackDeps } from "../stack/commands.js";
3
- export declare const mcpUsage = " mcp inspect <url> [--server <name>] [--environment <id>] [--sse] [--tenant <name>] [--json]\n list a remote MCP server's tools, with Environment <id>'s\n credential for <url> (none without it): model names, schema\n sizes, renames, or that it needs a person's sign-in (the\n Runtime calls no tool)";
3
+ export declare const mcpUsage = " mcp inspect <url> [--server <name>] [--agent <id>] [--sse] [--tenant <name>] [--json]\n list a remote MCP server's tools, with Agent <id>'s\n credential for <url> (none without it): model names, schema\n sizes, renames, or that it needs a person's sign-in (the\n Runtime calls no tool)";
4
4
  /** What `--vault` says since protocol 11. */
5
- export declare const MCP_VAULT_REMOVED = "--vault was removed in protocol 11: a credential is a secret of an Environment, in its own vault. Pass --environment <id> to send that Environment's credential for the URL.";
5
+ export declare const MCP_VAULT_REMOVED = "--vault was removed in protocol 11: a credential is a secret of an Agent, in its own vault. Pass --agent <id> to send that Agent's credential for the URL.";
6
+ /** What `--environment` says since protocol 16. */
7
+ export declare const MCP_ENVIRONMENT_REMOVED = "--environment was removed in protocol 16: Environments are Agents. Pass --agent <id> to send that Agent's credential for the URL.";
6
8
  /** What the removed `connect` subcommand says since protocol 10. */
7
9
  export declare const MCP_CONNECT_REMOVED: string;
8
10
  /** A preview as a person reads it: the server, a table of tools, the renames, or the sign-in hint. */
9
- export declare function previewLines(preview: McpPreview, environmentId?: string): string[];
11
+ export declare function previewLines(preview: McpPreview, agentId?: string): string[];
10
12
  /** `nylorun tenant mcp <inspect|connect> ...` */
11
13
  export declare function mcpCommand(deps: StackDeps, args: readonly string[]): Promise<number>;
@@ -1,40 +1,43 @@
1
1
  /**
2
2
  * `nylorun tenant mcp inspect <url>`: a remote MCP server's tools before an agent names it (R2b
3
3
  * C12), through the Management API (`POST /v1/tenant/mcp/preview`) of the installation
4
- * `target.ts` picks. It never starts a local Tenant, and the Runtime never calls a tool. `--environment <id>` sends that Environment's
5
- * credential for the URL (protocol 11; it replaced `--vault`); an Environment's secrets are
6
- * written on the Runtime API (`client.environments.secrets.put` of `@nylorun/agents`). Its
7
- * `connect` subcommand (MCP OAuth) was removed in protocol 10, and says so.
4
+ * `target.ts` picks. It never starts a local Tenant, and the Runtime never calls a tool. `--agent <id>`
5
+ * sends that Agent's credential for the URL (protocol 16; it replaced `--environment`, which
6
+ * replaced `--vault`); an Agent's secrets are written on the Runtime API
7
+ * (`client.agents.secrets.put` of `@nylorun/agents`). Its `connect` subcommand (MCP OAuth) was
8
+ * removed in protocol 10, and says so.
8
9
  */
9
10
  import { PROTOCOL_HEADER, PROTOCOL_VERSION } from "@nylorun/core/compatibility";
10
11
  import { CliError } from "../errors.js";
11
12
  import { parseStackFlags } from "../stack/commands.js";
12
13
  import { connect } from "./target.js";
13
- export const mcpUsage = ` mcp inspect <url> [--server <name>] [--environment <id>] [--sse] [--tenant <name>] [--json]
14
- list a remote MCP server's tools, with Environment <id>'s
14
+ export const mcpUsage = ` mcp inspect <url> [--server <name>] [--agent <id>] [--sse] [--tenant <name>] [--json]
15
+ list a remote MCP server's tools, with Agent <id>'s
15
16
  credential for <url> (none without it): model names, schema
16
17
  sizes, renames, or that it needs a person's sign-in (the
17
18
  Runtime calls no tool)`;
18
- const INSPECT_USAGE = "nylorun tenant mcp inspect <url> [--server <name>] [--environment <id>] [--sse] [--tenant <name>] [--json]";
19
- /** Where a credential for an MCP server goes since protocol 11. */
20
- const SECRET_HINT = "write it as a bearer or headers secret of an Environment (nylorun environments secrets put, client.environments.secrets.put of @nylorun/agents, or Studio)";
19
+ const INSPECT_USAGE = "nylorun tenant mcp inspect <url> [--server <name>] [--agent <id>] [--sse] [--tenant <name>] [--json]";
20
+ /** Where a credential for an MCP server goes since protocol 16. */
21
+ const SECRET_HINT = "write it as a bearer or headers secret of an Agent (nylorun agents secrets put, client.agents.secrets.put of @nylorun/agents, or Studio)";
21
22
  /** What `--vault` says since protocol 11. */
22
- export const MCP_VAULT_REMOVED = "--vault was removed in protocol 11: a credential is a secret of an Environment, in its own vault. Pass --environment <id> to send that Environment's credential for the URL.";
23
+ export const MCP_VAULT_REMOVED = "--vault was removed in protocol 11: a credential is a secret of an Agent, in its own vault. Pass --agent <id> to send that Agent's credential for the URL.";
24
+ /** What `--environment` says since protocol 16. */
25
+ export const MCP_ENVIRONMENT_REMOVED = "--environment was removed in protocol 16: Environments are Agents. Pass --agent <id> to send that Agent's credential for the URL.";
23
26
  /** What the removed `connect` subcommand says since protocol 10. */
24
- export const MCP_CONNECT_REMOVED = `The connect subcommand of nylorun mcp was removed with MCP OAuth (protocol 10): ${SECRET_HINT.replace("write it", "write the server's key")}, or reach a server that needs a person's sign-in through a gateway (a secret with via). See its tools with nylorun tenant mcp inspect <url> --environment <id>.`;
25
- /** The preview takes up to 15 s in the Runtime; the hop gets a margin. */
26
- const PREVIEW_TIMEOUT_MS = 30_000;
27
+ export const MCP_CONNECT_REMOVED = `The connect subcommand of nylorun mcp was removed with MCP OAuth (protocol 10): ${SECRET_HINT.replace("write it", "write the server's key")}, or reach a server that needs a person's sign-in through a gateway (a secret with via). See its tools with nylorun tenant mcp inspect <url> --agent <id>.`;
27
28
  const DESCRIPTION_CHARS = 60;
28
29
  const usageError = (message) => new CliError(message, 2);
29
30
  async function inspect(deps, args) {
30
31
  if (args.some((arg) => arg === "--vault" || arg.startsWith("--vault=")))
31
32
  throw usageError(MCP_VAULT_REMOVED);
32
- const flags = parseStackFlags(args, { booleans: ["--json", "--sse"], values: ["--server", "--environment", "--tenant"] }, INSPECT_USAGE);
33
+ if (args.some((arg) => arg === "--environment" || arg.startsWith("--environment=")))
34
+ throw usageError(MCP_ENVIRONMENT_REMOVED);
35
+ const flags = parseStackFlags(args, { booleans: ["--json", "--sse"], values: ["--server", "--agent", "--tenant"] }, INSPECT_USAGE);
33
36
  if (flags.rest.length !== 1)
34
37
  throw usageError(`Usage: ${INSPECT_USAGE}`);
35
38
  const url = flags.rest[0];
36
39
  const name = flags.values.get("--server");
37
- const environmentId = flags.values.get("--environment");
40
+ const agentId = flags.values.get("--agent");
38
41
  const tenant = flags.values.get("--tenant");
39
42
  const api = await connect(deps, "management", tenant);
40
43
  if (!api.managementKey)
@@ -51,10 +54,10 @@ async function inspect(deps, args) {
51
54
  url,
52
55
  ...(flags.booleans.has("--sse") ? { type: "sse" } : {}),
53
56
  ...(name === undefined ? {} : { name }),
54
- ...(environmentId === undefined ? {} : { environmentId }),
57
+ ...(agentId === undefined ? {} : { agentId }),
55
58
  }),
59
+ // The Runtime bounds the preview (its Runtime limit mcpPreviewMs, each step); the CLI waits.
56
60
  redirect: "error",
57
- signal: AbortSignal.timeout(PREVIEW_TIMEOUT_MS),
58
61
  });
59
62
  const body = (await response.json().catch(() => ({})));
60
63
  if (response.status === 404 && body.code === "not_found")
@@ -71,17 +74,17 @@ async function inspect(deps, args) {
71
74
  deps.out(JSON.stringify(preview, null, 2));
72
75
  return 0;
73
76
  }
74
- for (const line of previewLines(preview, environmentId))
77
+ for (const line of previewLines(preview, agentId))
75
78
  deps.out(line);
76
79
  return 0;
77
80
  }
78
81
  /** A preview as a person reads it: the server, a table of tools, the renames, or the sign-in hint. */
79
- export function previewLines(preview, environmentId) {
82
+ export function previewLines(preview, agentId) {
80
83
  const credential = preview.credentialSent
81
- ? `with Environment ${environmentId ?? "(unnamed)"}'s credential`
82
- : environmentId === undefined
83
- ? "with no credential (pass --environment <id> to send an Environment's)"
84
- : `with no credential (Environment ${environmentId} holds none for this URL)`;
84
+ ? `with Agent ${agentId ?? "(unnamed)"}'s credential`
85
+ : agentId === undefined
86
+ ? "with no credential (pass --agent <id> to send an Agent's)"
87
+ : `with no credential (Agent ${agentId} holds none for this URL)`;
85
88
  if (preview.authRequired) {
86
89
  const metadata = preview.authRequired.resourceMetadata;
87
90
  const servers = Array.isArray(metadata?.authorization_servers)
@@ -89,13 +92,13 @@ export function previewLines(preview, environmentId) {
89
92
  : [];
90
93
  return [
91
94
  preview.credentialSent
92
- ? `The MCP server at ${preview.url} answered 401: it rejected Environment ${environmentId ?? "(unnamed)"}'s credential.`
95
+ ? `The MCP server at ${preview.url} answered 401: it rejected Agent ${agentId ?? "(unnamed)"}'s credential.`
93
96
  : `The MCP server at ${preview.url} answered 401: it needs a credential.`,
94
97
  ...(servers.length > 0
95
98
  ? [`It signs people in with ${servers.join(", ")} (${preview.authRequired.resourceMetadataUrl}).`]
96
99
  : []),
97
100
  `Nylorun holds no OAuth client: ${SECRET_HINT.replace("write it", "write the server's API key")},`,
98
- "and pass --environment <id>; or reach it through a gateway that keeps each person's sign-in",
101
+ "and pass --agent <id>; or reach it through a gateway that keeps each person's sign-in",
99
102
  "(a secret with via and an identity header).",
100
103
  ];
101
104
  }
@@ -0,0 +1,4 @@
1
+ import { type StackDeps } from "../stack/commands.js";
2
+ export declare const migrateUsage = "nylorun migrate v8 <agent.json> [--runtime <environment-manifest.json>] [--name <name>] [--out <dir>]\n\nWrites agent.manifest.json, harness.manifest.json and runtime.manifest.json (in --out, default\n.) from an agent manifest v7 or a workflow manifest v3, as nylorun agents save took it, and the\nenvironment manifest v1 its sessions ran in. Then save it with nylorun apply <dir>.\n\n --runtime <file> the environment manifest: its fields become the runtime\n section, its harness the harness file's id\n --name <name> the manifest's name (default: the agent's id)\n --out <dir> where the files are written; none is overwritten";
3
+ /** `nylorun migrate v8 <agent.json> [--runtime <file>] [--name <name>] [--out <dir>]` */
4
+ export declare function migrateCommand(deps: StackDeps, args: readonly string[]): Promise<number>;
@@ -0,0 +1,140 @@
1
+ /**
2
+ * `nylorun migrate v8 <agent.json>`: a one-off for protocol 16 (track T2). It writes the three
3
+ * authoring files `nylorun apply` reads from an agent manifest v7 (or a workflow manifest v3), as
4
+ * `agents save` took it, and the environment manifest v1 its sessions ran in, if any:
5
+ *
6
+ * - `agent.manifest.json`: the name (the v7 id), description and metadata, naming the two files;
7
+ * - `harness.manifest.json`: the loop (`harnessSectionOf`: instructions, tools, skills, MCP
8
+ * servers, output schema, the plugins they came from), the environment manifest's `harness` as
9
+ * `id`, and v7's `requires`, which the composer checks against the runtime;
10
+ * - `runtime.manifest.json`: the environment manifest's fields without its identity; with none,
11
+ * kind `pod` when the agent requires one, `virtual` when it has skills or requires a sandbox,
12
+ * else `none`.
13
+ *
14
+ * It writes nothing over an existing file, reads nothing from a Runtime, and prints the gaps
15
+ * `nylorun apply` would refuse, to fix in the files first.
16
+ */
17
+ import { existsSync } from "node:fs";
18
+ import { mkdir, writeFile } from "node:fs/promises";
19
+ import { join, relative, resolve } from "node:path";
20
+ import { requirementGaps } from "@nylorun/agents";
21
+ import { AgentManifestSchema, RuntimeManifestFileSchema, WorkflowManifestSchema, isAgentManifestName, } from "@nylorun/core/contracts";
22
+ import { harnessSectionOf, sectionGaps } from "@nylorun/core/define";
23
+ import { CliError } from "../errors.js";
24
+ import { parseStackFlags } from "../stack/commands.js";
25
+ import { readJson, usageError } from "./runtime.js";
26
+ export const migrateUsage = `nylorun migrate v8 <agent.json> [--runtime <environment-manifest.json>] [--name <name>] [--out <dir>]
27
+
28
+ Writes agent.manifest.json, harness.manifest.json and runtime.manifest.json (in --out, default
29
+ .) from an agent manifest v7 or a workflow manifest v3, as nylorun agents save took it, and the
30
+ environment manifest v1 its sessions ran in. Then save it with nylorun apply <dir>.
31
+
32
+ --runtime <file> the environment manifest: its fields become the runtime
33
+ section, its harness the harness file's id
34
+ --name <name> the manifest's name (default: the agent's id)
35
+ --out <dir> where the files are written; none is overwritten`;
36
+ const SCHEMAS = {
37
+ agent: "https://docs.nylorun.com/schemas/agent-manifest/v8.json",
38
+ harness: "https://docs.nylorun.com/schemas/harness-manifest/v1.json",
39
+ runtime: "https://docs.nylorun.com/schemas/runtime-manifest/v1.json",
40
+ };
41
+ /** Environment manifest v1's identity: the agent manifest holds it now, or nothing does. */
42
+ const ENVIRONMENT_IDENTITY = ["$schema", "name", "description", "manifestSchemaVersion", "harness"];
43
+ function issuesOf(error) {
44
+ return error.issues
45
+ .map((issue) => ` ${issue.path.length ? `${issue.path.map(String).join(".")}: ` : ""}${issue.message}`)
46
+ .join("\n");
47
+ }
48
+ /** The loop manifest in `path`: an agent manifest v7 or a workflow manifest v3. */
49
+ async function loopManifest(path) {
50
+ const { $schema: _, ...raw } = await readJson(path, "The agent manifest");
51
+ const parsed = raw.kind === "workflow" ? WorkflowManifestSchema.safeParse(raw) : AgentManifestSchema.safeParse(raw);
52
+ if (!parsed.success)
53
+ throw usageError(`${path} is not an agent manifest v7 or a workflow manifest v3:\n${issuesOf(parsed.error)}`);
54
+ return parsed.data;
55
+ }
56
+ /** Whether a loop manifest, or an agent it embeds or delegates to, carries skills. */
57
+ function hasSkills(loop) {
58
+ if ("kind" in loop)
59
+ return Object.values(loop.agents).some(hasSkills);
60
+ if (Object.keys(loop.skills ?? {}).length > 0)
61
+ return true;
62
+ return (loop.tools ?? []).some((tool) => tool.agent !== undefined && hasSkills(tool.agent));
63
+ }
64
+ /** `nylorun migrate v8 <agent.json> [--runtime <file>] [--name <name>] [--out <dir>]` */
65
+ export async function migrateCommand(deps, args) {
66
+ const [version, ...rest] = args;
67
+ if (version === "--help" || version === "-h") {
68
+ deps.out(migrateUsage);
69
+ return 0;
70
+ }
71
+ if (version !== "v8")
72
+ throw usageError(version === undefined ? migrateUsage : `nylorun migrate knows v8 only.\n${migrateUsage}`);
73
+ const flags = parseStackFlags(rest, { values: ["--runtime", "--name", "--out"] }, migrateUsage);
74
+ if (flags.rest.length !== 1)
75
+ throw usageError(`Usage: ${migrateUsage.split("\n")[0]}`);
76
+ const cwd = deps.cwd ?? process.cwd();
77
+ const loop = await loopManifest(resolve(cwd, flags.rest[0]));
78
+ const name = flags.values.get("--name") ?? loop.id;
79
+ if (!isAgentManifestName(name))
80
+ throw usageError(`${name} is not an agent manifest's name (1-64 letters, digits, '.', '_' and '-', starting with a letter or digit): pass --name.`);
81
+ // The environment manifest: its fields are the runtime section, its harness the loop's.
82
+ let harnessId;
83
+ let runtime;
84
+ const runtimePath = flags.values.get("--runtime");
85
+ const requires = "kind" in loop ? undefined : loop.requires;
86
+ if (runtimePath !== undefined) {
87
+ const environment = await readJson(resolve(cwd, runtimePath), "The environment manifest");
88
+ if (typeof environment.harness === "string")
89
+ harnessId = environment.harness;
90
+ const fields = Object.fromEntries(Object.entries(environment).filter(([key]) => !ENVIRONMENT_IDENTITY.includes(key)));
91
+ // Environment manifest v1 made kind `virtual` when it named none.
92
+ const parsed = RuntimeManifestFileSchema.safeParse({ kind: "virtual", ...fields });
93
+ if (!parsed.success)
94
+ throw usageError(`${runtimePath} does not make a runtime section:\n${issuesOf(parsed.error)}`);
95
+ const { $schema: _, ...section } = parsed.data;
96
+ runtime = section;
97
+ }
98
+ else {
99
+ const kind = requires?.sandbox === "pod" ? "pod" : hasSkills(loop) || requires?.sandbox === "any" ? "virtual" : "none";
100
+ runtime = { kind };
101
+ }
102
+ const harness = harnessSectionOf(loop, harnessId === undefined ? {} : { harness: harnessId });
103
+ const out = resolve(cwd, flags.values.get("--out") ?? ".");
104
+ const files = {
105
+ "agent.manifest.json": {
106
+ $schema: SCHEMAS.agent,
107
+ manifestSchemaVersion: 8,
108
+ name,
109
+ ...(loop.description === undefined ? {} : { description: loop.description }),
110
+ ...(loop.metadata === undefined ? {} : { metadata: loop.metadata }),
111
+ harness: "./harness.manifest.json",
112
+ runtime: "./runtime.manifest.json",
113
+ },
114
+ // A flow's harness section is the flow itself; an agent's keeps `requires` for the composer.
115
+ "harness.manifest.json": {
116
+ ...("kind" in harness ? {} : { $schema: SCHEMAS.harness }),
117
+ ...harness,
118
+ ...(requires === undefined ? {} : { requires }),
119
+ },
120
+ "runtime.manifest.json": { $schema: SCHEMAS.runtime, ...runtime },
121
+ };
122
+ const existing = Object.keys(files).filter((file) => existsSync(join(out, file)));
123
+ if (existing.length > 0)
124
+ throw new CliError(`${relative(cwd, out) || "."} already holds ${existing.join(", ")}: nothing was written. Pass another --out.`, 1);
125
+ await mkdir(out, { recursive: true });
126
+ for (const [file, body] of Object.entries(files))
127
+ await writeFile(join(out, file), `${JSON.stringify(body, null, 2)}\n`);
128
+ const where = relative(cwd, out) || ".";
129
+ deps.out(`Wrote ${Object.keys(files).map((file) => join(where, file)).join(", ")}.`);
130
+ const gaps = [
131
+ ...requirementGaps(requires, runtime).map((gap) => `harness.${gap}`),
132
+ ...sectionGaps({ harness, runtime }),
133
+ ];
134
+ for (const gap of gaps)
135
+ deps.err(` fix before apply: ${gap}`);
136
+ if (!("kind" in harness) && harness.skills !== undefined)
137
+ deps.err(" its skills stay inline, by hash: the Runtime must hold their files. To upload them from source, set harness.manifest.json skills to their folder.");
138
+ deps.err(`Save it with nylorun apply ${where}.`);
139
+ return 0;
140
+ }
@@ -2,7 +2,7 @@
2
2
  * The pi-ai `nylorun tenant models set` loads; nylorun's devDependency has the same version
3
3
  * (scripts/check-package.mjs).
4
4
  */
5
- export declare const PI_AI_VERSION = "0.99.1";
5
+ export declare const PI_AI_VERSION = "1.2.0";
6
6
  /** What `nylorun tenant models set` uses of pi-ai: its provider registry and login flows. */
7
7
  export interface PiAi {
8
8
  createProvider: typeof import("@earendil-works/pi-ai").createProvider;
@@ -10,7 +10,7 @@ import { CliError } from "../../errors.js";
10
10
  * The pi-ai `nylorun tenant models set` loads; nylorun's devDependency has the same version
11
11
  * (scripts/check-package.mjs).
12
12
  */
13
- export const PI_AI_VERSION = "0.99.1";
13
+ export const PI_AI_VERSION = "1.2.0";
14
14
  const PI_AI = "@earendil-works/pi-ai";
15
15
  /** The module written beside an on-demand install, which resolves pi-ai from there. */
16
16
  const ENTRY = "pi-ai.js";
@@ -1,9 +1,12 @@
1
1
  import { type StackDeps } from "../stack/commands.js";
2
2
  export declare const modelsUsage = " models catalog [--tenant <name>] [--json]\n every provider and model the Runtime knows\n models providers [--tenant <name>] [--json]\n the providers with a credential, and the one in use\n models get [--tenant <name>] [--json]\n the Tenant's model\n models set [--tenant <name>] sign in to a model provider and set the Tenant's model\n models set --provider <id> --model <id> [--base-url <url>] [--tenant <name>]\n the same with an API key piped in on stdin (CI)\n models select <provider> [--model <id>] [--tenant <name>]\n use a provider that has a credential (default: its model)";
3
- export declare const usageUsage = " usage [--scope tenant|agent|turn] [--id <id>] [--period day|month|total] [--tenant <name>]\n what the model calls used and cost (JSON)\n calls [--after <cursor>] [--limit <n>] [--tenant <name>]\n every recorded model call, one JSON line each, until\n caught up; prints the cursor to resume from on stderr";
3
+ export declare const usageUsage = " usage [--scope tenant|manifest|turn] [--id <id>] [--period day|month|total] [--tenant <name>]\n what the model calls used and cost (JSON); --scope\n manifest: the Agents made from agent manifest <id>\n calls [--after <cursor>] [--limit <n>] [--tenant <name>]\n every recorded model call, one JSON line each, until\n caught up; prints the cursor to resume from on stderr";
4
4
  /** `nylorun tenant models <verb> ...` */
5
5
  export declare function modelsCommand(deps: StackDeps, args: readonly string[]): Promise<number>;
6
- /** `nylorun tenant usage`: one scope's totals for one period, as JSON. */
6
+ /**
7
+ * `nylorun tenant usage`: one scope's totals for one period, as JSON: the Tenant's, the Agents
8
+ * made from one agent manifest (`--scope manifest --id <name>`), or one turn's.
9
+ */
7
10
  export declare function usageCommand(deps: StackDeps, args: readonly string[]): Promise<number>;
8
11
  /**
9
12
  * `nylorun tenant calls`: every recorded model call from `--after`, one JSON object per line
@@ -20,8 +20,9 @@ export const modelsUsage = ` models catalog [--tenant <name>] [--json]
20
20
  the same with an API key piped in on stdin (CI)
21
21
  models select <provider> [--model <id>] [--tenant <name>]
22
22
  use a provider that has a credential (default: its model)`;
23
- export const usageUsage = ` usage [--scope tenant|agent|turn] [--id <id>] [--period day|month|total] [--tenant <name>]
24
- what the model calls used and cost (JSON)
23
+ export const usageUsage = ` usage [--scope tenant|manifest|turn] [--id <id>] [--period day|month|total] [--tenant <name>]
24
+ what the model calls used and cost (JSON); --scope
25
+ manifest: the Agents made from agent manifest <id>
25
26
  calls [--after <cursor>] [--limit <n>] [--tenant <name>]
26
27
  every recorded model call, one JSON line each, until
27
28
  caught up; prints the cursor to resume from on stderr`;
@@ -147,15 +148,20 @@ export async function modelsCommand(deps, args) {
147
148
  return select(deps, rest);
148
149
  throw usageError();
149
150
  }
150
- const SCOPES = ["tenant", "agent", "turn"];
151
+ const SCOPES = ["tenant", "manifest", "turn"];
151
152
  const PERIODS = ["day", "month", "total"];
152
- const USAGE_USAGE = "nylorun tenant usage [--scope tenant|agent|turn] [--id <id>] [--period day|month|total] [--tenant <name>]";
153
- /** `nylorun tenant usage`: one scope's totals for one period, as JSON. */
153
+ const USAGE_USAGE = "nylorun tenant usage [--scope tenant|manifest|turn] [--id <id>] [--period day|month|total] [--tenant <name>]";
154
+ /**
155
+ * `nylorun tenant usage`: one scope's totals for one period, as JSON: the Tenant's, the Agents
156
+ * made from one agent manifest (`--scope manifest --id <name>`), or one turn's.
157
+ */
154
158
  export async function usageCommand(deps, args) {
155
159
  const flags = parseStackFlags(args, { values: ["--tenant", "--scope", "--id", "--period"] }, USAGE_USAGE);
156
160
  if (flags.rest.length)
157
161
  throw usageError(`Usage: ${USAGE_USAGE}`);
158
162
  const scope = flags.values.get("--scope") ?? "tenant";
163
+ if (scope === "agent")
164
+ throw usageError("--scope agent is --scope manifest in protocol 16: --id names an agent manifest.");
159
165
  const period = flags.values.get("--period") ?? "total";
160
166
  const id = flags.values.get("--id");
161
167
  if (!SCOPES.includes(scope))
@@ -163,7 +169,9 @@ export async function usageCommand(deps, args) {
163
169
  if (!PERIODS.includes(period))
164
170
  throw usageError(`--period is one of ${PERIODS.join(", ")}.`);
165
171
  if ((scope === "tenant") !== (id === undefined))
166
- throw usageError(scope === "tenant" ? "--id names an agent or a turn: pass --scope agent or turn." : `--scope ${scope} needs --id.`);
172
+ throw usageError(scope === "tenant"
173
+ ? "--id names an agent manifest or a turn: pass --scope manifest or turn."
174
+ : `--scope ${scope} needs --id.`);
167
175
  const query = {
168
176
  scope: scope,
169
177
  period: period,
@@ -185,8 +193,9 @@ export async function callsCommand(deps, args) {
185
193
  const after = flags.values.get("--after");
186
194
  const limitFlag = flags.values.get("--limit");
187
195
  const limit = limitFlag === undefined ? undefined : Number(limitFlag);
188
- if (limit !== undefined && !(Number.isInteger(limit) && limit >= 1 && limit <= 1000))
189
- throw usageError("--limit is a whole number from 1 to 1000.");
196
+ // A page holds at most the Runtime limit exportPageItems; a larger --limit gets that many.
197
+ if (limit !== undefined && !(Number.isInteger(limit) && limit >= 1))
198
+ throw usageError("--limit is a whole number of at least 1.");
190
199
  const admin = await client(deps, flags.values.get("--tenant"));
191
200
  let next = null;
192
201
  for await (const page of admin.models.exportCalls({
@@ -1,7 +1,7 @@
1
1
  /**
2
- * What the Runtime API groups (`agents`, `sessions`, `environments`, `artifacts`) share: their
3
- * flags, the `@nylorun/agents` client of the installation `target.ts` picks, acting for a subject
4
- * with `--as`, and how they read input and print output.
2
+ * What the Runtime API commands (`apply`, `agent-manifests`, `agents`, `sessions`, `artifacts`)
3
+ * share: their flags, the `@nylorun/agents` client of the installation `target.ts` picks, acting
4
+ * for a subject with `--as`, and how they read input and print output.
5
5
  */
6
6
  import { readFile } from "node:fs/promises";
7
7
  import { AgentsClient, RuntimeError, SUBJECT_SCOPES, } from "@nylorun/agents";