nylorun 0.7.0-beta → 0.8.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.
@@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
2
2
  import { createInterface } from "node:readline/promises";
3
3
  import { runStackCommand, runStudioCommand } from "./commands.js";
4
4
  import { spawnDocker } from "./docker.js";
5
+ import { sandboxCommand as runSandboxCommand } from "./sandbox.js";
5
6
  import { pinnedVersion } from "./versions.js";
6
7
  import { loopbackPorts } from "./ports.js";
7
8
  export { ensureStack, isStackCommand, readStackStatus, stackUsage, studioLoginUrl, tenantStudioPath, STACK_SERVICES, } from "./commands.js";
@@ -79,6 +80,11 @@ export function defaultStackDeps(env) {
79
80
  export async function stackCommand(name, args, env) {
80
81
  return await runStackCommand(name, args, defaultStackDeps(env));
81
82
  }
83
+ /** Entry for `nylorun sandbox ls|rm`. */
84
+ export async function sandboxCommand(args, env) {
85
+ return await runSandboxCommand(defaultStackDeps(env), args);
86
+ }
87
+ export { sandboxUsage } from "./sandbox.js";
82
88
  /** `nylorun studio`, landing on `next` when given, else on the Tenant's page. */
83
89
  export async function studioCommand(args, env, options = {}) {
84
90
  return await runStudioCommand(args, defaultStackDeps(env), options);
@@ -12,6 +12,21 @@ export interface StackPaths {
12
12
  record: string;
13
13
  /** The Tenant directory: homes, sandboxes, plugin data, logs. */
14
14
  tenant: string;
15
+ /**
16
+ * The Tenant directory's parts the harness container mounts (F6.2): workspaces, plugin data,
17
+ * and the home and temporary directory of MCP stdio servers.
18
+ */
19
+ harness: {
20
+ sandboxes: string;
21
+ pluginData: string;
22
+ home: string;
23
+ tmp: string;
24
+ };
25
+ /**
26
+ * Plugin roots on this machine (`plugins/`), mounted read-only at the same path into the
27
+ * runtime and harness containers, so a stdio MCP server's plugin root resolves there.
28
+ */
29
+ plugins: string;
15
30
  /** The keys directory, mounted only into the gateway (F4.2). */
16
31
  keys: string;
17
32
  /** The Tenant's vault key (KEK), `keys/vault-kek`. */
@@ -9,6 +9,13 @@ export function stackPaths(hostRoot) {
9
9
  state: join(root, "host-state.json"),
10
10
  record: join(root, "tenant.json"),
11
11
  tenant: join(root, "tenant"),
12
+ harness: {
13
+ sandboxes: join(root, "tenant", "sandboxes"),
14
+ pluginData: join(root, "tenant", "plugin-data"),
15
+ home: join(root, "tenant", "home"),
16
+ tmp: join(root, "tenant", "tmp"),
17
+ },
18
+ plugins: join(root, "plugins"),
12
19
  keys: join(root, "keys"),
13
20
  vaultKey: join(root, "keys", "vault-kek"),
14
21
  home: join(root, "home"),
@@ -1,4 +1,4 @@
1
- import { type StackEnv } from "./env-file.js";
1
+ import { type SandboxStackEnv, type StackEnv } from "./env-file.js";
2
2
  import { type HostConfigFile } from "./host-files.js";
3
3
  import type { StackImages } from "./images.js";
4
4
  import type { StackPaths } from "./paths.js";
@@ -16,7 +16,8 @@ export declare function readStackEnv(paths: Pick<StackPaths, "env">): Promise<im
16
16
  * Write everything `docker compose up` needs under the Host root:
17
17
  * host.json, host-credentials.json (0600), docker/compose.yaml,
18
18
  * docker/.env (0600) and docker/restate-identity.pem (0600). Ports and the
19
- * Postgres password persist in .env; the identity key persists in its PEM.
19
+ * Postgres password persist in .env; the identity key persists in its PEM. The harness token
20
+ * and mode persist too; whether Restate's UI is published is decided on every start.
20
21
  */
21
22
  export declare function prepareStack(input: {
22
23
  paths: StackPaths;
@@ -43,4 +44,11 @@ export declare function prepareStack(input: {
43
44
  };
44
45
  /** The measurement id Studio reports page views to; absent when telemetry is off. */
45
46
  studioAnalyticsId?: string;
47
+ /**
48
+ * The sandboxes settings (`nylorun sandbox enable`); null removes them (`disable`).
49
+ * Undefined keeps the persisted ones while `sandboxes/cluster.json` exists.
50
+ */
51
+ sandboxes?: SandboxStackEnv | null;
52
+ /** Publish Restate's UI and admin port (`start --restate-ui`); undefined keeps the last start's choice. */
53
+ restateUi?: boolean;
46
54
  }): Promise<PreparedStack>;
@@ -4,6 +4,7 @@ import { renderComposeFile } from "./compose-file.js";
4
4
  import { parseFrameAncestors } from "@nylorun/core/contracts";
5
5
  import { CliError } from "../errors.js";
6
6
  import { DEFAULT_STUDIO_FRAME_ANCESTORS, parseDerivedPrincipals, parsePersisted, renderEnvFile, } from "./env-file.js";
7
+ import { sandboxesEnabled } from "../sandbox/cluster-file.js";
7
8
  import { ensureHostCredentials, ensureVaultKey, ensureHostLayout, writeStackHostConfig, } from "./host-files.js";
8
9
  import { choosePort, DEFAULT_PORTS } from "./ports.js";
9
10
  import { ensureIdentityKey } from "./restate-identity.js";
@@ -30,7 +31,8 @@ export async function readStackEnv(paths) {
30
31
  * Write everything `docker compose up` needs under the Host root:
31
32
  * host.json, host-credentials.json (0600), docker/compose.yaml,
32
33
  * docker/.env (0600) and docker/restate-identity.pem (0600). Ports and the
33
- * Postgres password persist in .env; the identity key persists in its PEM.
34
+ * Postgres password persist in .env; the identity key persists in its PEM. The harness token
35
+ * and mode persist too; whether Restate's UI is published is decided on every start.
34
36
  */
35
37
  export async function prepareStack(input) {
36
38
  const { paths } = input;
@@ -69,6 +71,12 @@ export async function prepareStack(input) {
69
71
  postgresPassword: persisted.postgresPassword ?? randomBytes(24).toString("hex"),
70
72
  // Kept across starts: a new token would recreate the runtime and gateway containers.
71
73
  gatesToken: persisted.gatesToken ?? randomBytes(32).toString("hex"),
74
+ // Kept across starts: a new token would recreate the runtime and harness containers.
75
+ harnessToken: persisted.harnessToken ?? randomBytes(32).toString("hex"),
76
+ harness: persisted.harness ?? "remote",
77
+ restateUi: input.restateUi ?? persisted.restateUi ?? false,
78
+ // Kept across starts: a new key would recreate the runtime, gateway and rustfs containers.
79
+ objectStoreSecretKey: persisted.objectStoreSecretKey ?? randomBytes(32).toString("hex"),
72
80
  restateIdentityKey: identity.publicKey,
73
81
  uid: input.uid,
74
82
  gid: input.gid,
@@ -82,6 +90,12 @@ export async function prepareStack(input) {
82
90
  ? parseDerivedPrincipals(input.derivedPrincipals, "NYLORUN_DERIVED_PRINCIPALS")
83
91
  : (persisted.derivedPrincipals ?? ["project"])).join(","),
84
92
  };
93
+ const sandboxes = input.sandboxes === null
94
+ ? undefined
95
+ : (input.sandboxes ?? (sandboxesEnabled(paths.root) ? persisted.sandboxes : undefined));
96
+ // The sandboxes image follows the Runtime's, like the runtime image, on every start.
97
+ if (sandboxes)
98
+ env.sandboxes = { ...sandboxes, image: input.images.sandboxes };
85
99
  const { adminKey } = await ensureHostCredentials(paths);
86
100
  await ensureVaultKey(paths);
87
101
  const host = await writeStackHostConfig(paths, {
@@ -90,6 +104,10 @@ export async function prepareStack(input) {
90
104
  runtimeVersion: input.runtimeVersion,
91
105
  });
92
106
  await writeFileMode(paths.env, renderEnvFile(env), 0o600);
93
- await writeFileMode(paths.compose, renderComposeFile(input.project, input.name), 0o644);
107
+ await writeFileMode(paths.compose, renderComposeFile(input.project, input.name, {
108
+ harness: env.harness,
109
+ ...(env.sandboxes ? { sandboxes: true } : {}),
110
+ ...(env.restateUi ? { restateUi: true } : {}),
111
+ }), 0o644);
94
112
  return { env, host, adminKey, firstRun };
95
113
  }
@@ -0,0 +1,4 @@
1
+ import { type StackDeps } from "./commands.js";
2
+ export declare const sandboxUsage = " sandbox ls [--tenant <name>] [--label <key=value>]... [--json]\n the Tenant's sandboxes: id, kind, state, attached sessions and labels\n sandbox rm <id> [--tenant <name>] delete a sandbox and its files (refused while a turn runs in it)\n sandbox enable --context <name> [--tenant <name>] [--host-address <ip>] [--bind-address <ip>] [--no-pull]\n run the Tenant's sandboxes as pods on that kubeconfig context (agent-sandbox v1.0.5)\n sandbox disable [--tenant <name>] [--delete-namespace]\n stop the sandboxes service and forget the cluster\n sandbox status [--tenant <name>] [--json]\n the cluster, the sandboxes service's readiness and what it reports";
3
+ /** `nylorun sandbox <ls|rm|enable|disable|status> ...` */
4
+ export declare function sandboxCommand(deps: StackDeps, args: readonly string[]): Promise<number>;
@@ -0,0 +1,106 @@
1
+ /**
2
+ * `nylorun sandbox ls | rm`: the running local Tenant's sandbox resources (Host feature
3
+ * `sandboxes`), through its Tenant API as the Project's derived principal. Neither starts the
4
+ * Tenant. `nylorun sandbox enable|disable|status` (sandbox pods on a cluster, F7.2) live in
5
+ * `../sandbox/commands.ts`.
6
+ */
7
+ import { PROTOCOL_HEADER, PROTOCOL_VERSION } from "@nylorun/core/compatibility";
8
+ import { CliError } from "../errors.js";
9
+ import { runSandboxCommand, sandboxUsage as clusterUsage } from "../sandbox/commands.js";
10
+ import { spawnKubectl } from "../sandbox/kubectl.js";
11
+ import { parseStackFlags, runningTenantApi } from "./commands.js";
12
+ export const sandboxUsage = ` sandbox ls [--tenant <name>] [--label <key=value>]... [--json]
13
+ the Tenant's sandboxes: id, kind, state, attached sessions and labels
14
+ sandbox rm <id> [--tenant <name>] delete a sandbox and its files (refused while a turn runs in it)
15
+ ${clusterUsage}`;
16
+ const usageError = (message) => new CliError(message, 2);
17
+ async function call(deps, api, method, path) {
18
+ const response = await deps.fetch(`${api.runtimeUrl}${path}`, {
19
+ method,
20
+ headers: {
21
+ authorization: `Bearer ${api.applicationKey}`,
22
+ [PROTOCOL_HEADER]: String(PROTOCOL_VERSION),
23
+ accept: "application/json",
24
+ },
25
+ redirect: "error",
26
+ signal: AbortSignal.timeout(15_000),
27
+ });
28
+ const body = (await response.json().catch(() => ({})));
29
+ if (response.status === 404 && path === "/v1/sandboxes")
30
+ throw new CliError(`Tenant ${api.name} runs a Runtime without sandboxes. Update it: npx nylorun@latest start.`, 1);
31
+ if (!response.ok && response.status !== 404)
32
+ throw new CliError(`${method} ${path} returned ${response.status}${typeof body.message === "string" ? `: ${body.message}` : ""}`, 1);
33
+ return { status: response.status, body };
34
+ }
35
+ function labelsText(labels) {
36
+ const entries = Object.entries(labels);
37
+ return entries.length === 0 ? "-" : entries.map(([key, value]) => `${key}=${value}`).join(",");
38
+ }
39
+ async function ls(deps, args) {
40
+ const usage = "nylorun sandbox ls [--tenant <name>] [--label <key=value>]... [--json]";
41
+ const flags = parseStackFlags(args, { booleans: ["--json"], values: ["--tenant"], lists: ["--label"] }, usage);
42
+ if (flags.rest.length)
43
+ throw usageError(`Usage: ${usage}`);
44
+ const query = new URLSearchParams();
45
+ for (const label of flags.lists.get("--label") ?? []) {
46
+ if (label.indexOf("=") <= 0)
47
+ throw usageError(`--label must be key=value, not ${label}`);
48
+ query.append("label", label);
49
+ }
50
+ const name = flags.values.get("--tenant");
51
+ const api = await runningTenantApi(deps, name === undefined ? {} : { name });
52
+ const search = query.toString();
53
+ const { body } = await call(deps, api, "GET", `/v1/sandboxes${search ? `?${search}` : ""}`);
54
+ const sandboxes = (body.sandboxes ?? []);
55
+ if (flags.booleans.has("--json")) {
56
+ deps.out(JSON.stringify(sandboxes, null, 2));
57
+ return 0;
58
+ }
59
+ if (sandboxes.length === 0) {
60
+ deps.out(`No sandboxes on Tenant ${api.name}.`);
61
+ return 0;
62
+ }
63
+ const rows = [
64
+ ["ID", "KIND", "STATE", "SESSIONS", "LABELS"],
65
+ ...sandboxes.map((sandbox) => [
66
+ sandbox.id,
67
+ sandbox.kind,
68
+ sandbox.state,
69
+ String(sandbox.sessions.length),
70
+ labelsText(sandbox.labels),
71
+ ]),
72
+ ];
73
+ const widths = rows[0].map((_, column) => Math.max(...rows.map((row) => row[column].length)));
74
+ for (const row of rows)
75
+ deps.out(row
76
+ .map((cell, column) => (column === row.length - 1 ? cell : cell.padEnd(widths[column])))
77
+ .join(" "));
78
+ return 0;
79
+ }
80
+ async function rm(deps, args) {
81
+ const usage = "nylorun sandbox rm <id> [--tenant <name>]";
82
+ const flags = parseStackFlags(args, { values: ["--tenant"] }, usage);
83
+ if (flags.rest.length !== 1)
84
+ throw usageError(`Usage: ${usage}`);
85
+ const id = flags.rest[0];
86
+ const name = flags.values.get("--tenant");
87
+ const api = await runningTenantApi(deps, name === undefined ? {} : { name });
88
+ const { status, body } = await call(deps, api, "DELETE", `/v1/sandboxes/${encodeURIComponent(id)}`);
89
+ if (status === 404 || body.deleted !== true) {
90
+ deps.err(`No sandbox ${id} on Tenant ${api.name}.`);
91
+ return 1;
92
+ }
93
+ deps.out(`Deleted sandbox ${id} and its files.`);
94
+ return 0;
95
+ }
96
+ /** `nylorun sandbox <ls|rm|enable|disable|status> ...` */
97
+ export async function sandboxCommand(deps, args) {
98
+ const [sub, ...rest] = args;
99
+ if (sub === "ls" || sub === "list")
100
+ return ls(deps, rest);
101
+ if (sub === "rm" || sub === "delete")
102
+ return rm(deps, rest);
103
+ if (sub === "enable" || sub === "disable" || sub === "status")
104
+ return runSandboxCommand(args, { stack: deps, kubectl: spawnKubectl(deps.env) });
105
+ throw usageError(`Usage:\n${sandboxUsage}`);
106
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nylorun",
3
- "version": "0.7.0-beta",
3
+ "version": "0.8.0-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",
@@ -30,18 +30,18 @@
30
30
  "prepack": "npm run build"
31
31
  },
32
32
  "dependencies": {
33
- "@nylorun/core": "0.11.0-beta"
33
+ "@nylorun/core": "0.12.0-beta"
34
34
  },
35
35
  "devDependencies": {
36
36
  "typescript": "npm:@typescript/typescript6@^6.0.2",
37
37
  "@types/node": "^26.5.0",
38
- "vitest": "^5.0.1"
38
+ "vitest": "^5.0.2"
39
39
  },
40
40
  "bin": {
41
41
  "nylorun": "dist/cli.js"
42
42
  },
43
43
  "nylorun": {
44
- "runtime": "0.15.2-beta",
45
- "studio": "0.18.0-beta"
44
+ "runtime": "0.16.0-beta",
45
+ "studio": "0.19.0-beta"
46
46
  }
47
47
  }