nylorun 0.9.0-beta → 0.11.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.
@@ -23,17 +23,17 @@ import { PINNED_IMAGES } from "./images.js";
23
23
  * The combined packing (blueprint D12): the `runtime` container runs the core
24
24
  * and loop services, and the `gateway` container runs gates: the Model Gate,
25
25
  * which alone reads model credentials, and the Tool Gate (F4.1), which alone
26
- * holds remote MCP connections and their credentials and POSTs every Action
27
- * delivery. Every model call, remote MCP call and delivery of the loop crosses it
26
+ * holds remote MCP connections and their credentials and makes every HTTP tool
27
+ * request. Every model call, remote MCP call and HTTP tool call of the loop crosses it
28
28
  * (NYLORUN_GATES_URL, with NYLORUN_GATES_TOKEN from `.env`, or a run's token from the
29
- * harness); MCP stdio servers run in the harness container. The gateway also runs keys (F4.2): the only
29
+ * harness). The gateway also runs keys (F4.2): the only
30
30
  * holder of the vault key, it runs every vault write that touches a secret and signs
31
31
  * every token (NYLORUN_KEYS_URL). The gateway mounts only the Host's Tenant directory
32
32
  * (`tenant/`) and its keys directory (`keys/`, the vault key), read-only: never
33
33
  * host-credentials.json. The runtime mounts the Host root with `keys/` and `docker/`
34
34
  * covered by empty read-only mounts, so it can read neither the vault key nor
35
35
  * Restate's private key and `.env`. The runtime does not wait for the gateway: while
36
- * it is down, model and MCP calls fail, deliveries are retried, vault writes and
36
+ * it is down, model, MCP and HTTP tool calls fail, vault writes and
37
37
  * token minting answer 503, and the session takes the next message.
38
38
  *
39
39
  * The Object store (blueprint D35) is RustFS, single node and single drive, pinned by digest,
@@ -54,14 +54,12 @@ import { PINNED_IMAGES } from "./images.js";
54
54
  * CONNECT with the egress token, to the hosts the sandbox spec allows). The two HTTP listeners
55
55
  * accept that address as a Host.
56
56
  *
57
- * The harness (F6.2, `harness: "remote"`, the default) runs every agent turn, MCP stdio server
58
- * and workspace in its own container: the runtime image with `--service harness`, connected to
59
- * core's Harness API (`ws://runtime:4200/nylorun/harness/v1`) with NYLORUN_HARNESS_TOKEN, its
60
- * only credential, and calling models and remote MCP servers through the gateway with each
61
- * run's token. It mounts only the Tenant's `sandboxes/`, `plugin-data/`, `home/` and `tmp/`
62
- * under `/harness`, and the Host root's `plugins/` read-only at its own path (plugin roots name
63
- * paths on this machine; the runtime mounts it there too). NYLORUN_HARNESS=in-process in `.env`
64
- * rolls back to turns in the runtime container, without the harness service.
57
+ * The harness (F6.2, `harness: "remote"`, the default) runs every agent turn and workspace in
58
+ * its own container: the runtime image with `--service harness`, connected to core's Harness
59
+ * API (`ws://runtime:4200/nylorun/harness/v1`) with NYLORUN_HARNESS_TOKEN, its only
60
+ * credential, and calling models and remote MCP servers through the gateway with each run's
61
+ * token. It mounts only the Tenant's `sandboxes/` under `/harness`. NYLORUN_HARNESS=in-process
62
+ * in `.env` rolls back to turns in the runtime container, without the harness service.
65
63
  *
66
64
  * Three networks: `store` (internal: no egress) joins Postgres, s2-lite, Restate and RustFS to
67
65
  * the runtime and the gateway; `harness` joins the harness to the runtime and the gateway only;
@@ -176,14 +174,14 @@ ${restateUi ? RESTATE_UI : RESTATE_CLOSED} healthcheck:
176
174
  NYLORUN_OBJECT_STORE_ENDPOINT: http://rustfs:9000
177
175
  NYLORUN_OBJECT_STORE_ACCESS_KEY: nylorun
178
176
  NYLORUN_OBJECT_STORE_SECRET_KEY: \${NYLORUN_OBJECT_STORE_SECRET_KEY:?run nylorun start}
179
- # Action endpoints on this machine: \`localhost\` in a registered URL means the Docker host.
177
+ # MCP servers and HTTP tools on this machine: \`localhost\` in their URLs means the Docker host.
180
178
  NYLORUN_ENDPOINT_LOOPBACK: docker-host
181
179
  # Your credential resolver, for people's own MCP credentials (DEPLOYMENT.md, Credentials);
182
180
  # unset by default. Set both in the shell that runs \`nylorun start\`.
183
181
  NYLORUN_RESOLVER_URL: \${NYLORUN_RESOLVER_URL:-}
184
182
  NYLORUN_RESOLVER_TOKEN: \${NYLORUN_RESOLVER_TOKEN:-}${sandboxes ? SANDBOXES_GATEWAY_ENV : ""}
185
183
  extra_hosts:
186
- host.docker.internal: host-gateway # model servers, MCP servers and Action endpoints on this machine
184
+ host.docker.internal: host-gateway # model servers, MCP servers and HTTP tools on this machine
187
185
  volumes:
188
186
  # The Tenant's homes and its vault key only, read-only.
189
187
  - \${NYLORUN_HOST_ROOT:?run nylorun start}/tenant:/nylorun/tenant:ro
@@ -213,7 +211,7 @@ ${sandboxes ? SANDBOXES_GATES_PORT : ""} # Egress and the stores; the harness
213
211
  NYLORUN_PACKING: combined
214
212
  # The Tenant the Runtime creates on its first start (later starts open it).
215
213
  NYLORUN_TENANT_NAME: \${NYLORUN_TENANT_NAME:?run nylorun start}
216
- # Model calls, remote MCP calls and deliveries go through the gateway, and vault writes
214
+ # Model calls, remote MCP calls and HTTP tool calls go through the gateway, and vault writes
217
215
  # and token signing through its keys service: this container never reads a credential
218
216
  # or the vault key.
219
217
  NYLORUN_GATES_URL: http://gateway:4100
@@ -224,11 +222,7 @@ ${sandboxes ? SANDBOXES_GATES_PORT : ""} # Egress and the stores; the harness
224
222
  # Host headers the Runtime accepts: its name on the Compose network, and the
225
223
  # published port as clients on this machine address it.
226
224
  NYLORUN_ALLOWED_HOSTS: runtime:4000,localhost:\${NYLORUN_PORT},127.0.0.1:\${NYLORUN_PORT}
227
- NYLORUN_PUBLIC_URL: http://localhost:\${NYLORUN_PORT} # reported by /v1/admin/status
228
- # The Admin API on its own listener, published on loopback only; Studio reaches it on
229
- # the Compose network. Port 4000 serves the Tenant API alone.
230
- NYLORUN_ADMIN_LISTEN_PORT: "4001"
231
- NYLORUN_ADMIN_ALLOWED_HOSTS: runtime:4001,localhost:\${NYLORUN_ADMIN_PORT},127.0.0.1:\${NYLORUN_ADMIN_PORT}
225
+ NYLORUN_PUBLIC_URL: http://localhost:\${NYLORUN_PORT}
232
226
  NYLORUN_DATABASE_URL: postgres://nylorun:\${NYLORUN_POSTGRES_PASSWORD}@postgres:5432/nylorun
233
227
  NYLORUN_RESTATE_INGRESS_URL: http://restate:8080
234
228
  NYLORUN_RESTATE_ADMIN_URL: http://restate:9070
@@ -241,7 +235,7 @@ ${sandboxes ? SANDBOXES_GATES_PORT : ""} # Egress and the stores; the harness
241
235
  NYLORUN_OBJECT_STORE_ENDPOINT: http://rustfs:9000
242
236
  NYLORUN_OBJECT_STORE_ACCESS_KEY: nylorun
243
237
  NYLORUN_OBJECT_STORE_SECRET_KEY: \${NYLORUN_OBJECT_STORE_SECRET_KEY:?run nylorun start}
244
- # Action endpoints on this machine: \`localhost\` in a registered URL means the Docker host.
238
+ # MCP servers and HTTP tools on this machine: \`localhost\` in their URLs means the Docker host.
245
239
  NYLORUN_ENDPOINT_LOOPBACK: docker-host
246
240
  # Agent turns, MCP servers and workspaces run in the harness container, which connects to
247
241
  # this listener with NYLORUN_HARNESS_TOKEN. NYLORUN_HARNESS=in-process in .env rolls back.
@@ -264,13 +258,10 @@ ${sandboxes ? SANDBOXES_GATES_PORT : ""} # Egress and the stores; the harness
264
258
  read_only: true
265
259
  tmpfs: { size: 4096, mode: 0755 } # empty, and listable by the runtime user${sandboxes ? SANDBOXES_RUNTIME_MOUNT : ""}
266
260
  - workspaces:/workspaces
267
- # Plugin roots on this machine, at their own path (as for the harness), for NYLORUN_HARNESS=in-process.
268
- - \${NYLORUN_HOST_ROOT:?run nylorun start}/plugins:\${NYLORUN_HOST_ROOT:?run nylorun start}/plugins:ro
269
261
  networks: [default, store, harness]
270
262
  ports:
271
- - "127.0.0.1:\${NYLORUN_PORT:?run nylorun start}:4000" # Tenant API, SSE, browsers
272
- - "127.0.0.1:\${NYLORUN_ADMIN_PORT:?run nylorun start}:4001" # Admin API (operators only)${sandboxes ? SANDBOXES_RUNTIME_PORT : ""}
273
- healthcheck: # liveness: a Tenant that cannot open is reported by nylorun start from the Admin status, not by a 300 s wait
263
+ - "127.0.0.1:\${NYLORUN_PORT:?run nylorun start}:4000" # Runtime and Management APIs, SSE, browsers${sandboxes ? SANDBOXES_RUNTIME_PORT : ""}
264
+ healthcheck: # liveness: a Tenant that cannot open is reported by nylorun start (/ready, nylorun-operate status), not by a 300 s wait
274
265
  test: ["CMD", "node", "-e", "fetch('http://localhost:4000/health').then(r=>process.exit(r.ok?0:1),()=>process.exit(1))"]
275
266
  interval: 2s
276
267
  timeout: 5s
@@ -285,7 +276,7 @@ ${sandboxes ? SANDBOXES_GATES_PORT : ""} # Egress and the stores; the harness
285
276
  depends_on:
286
277
  runtime: { condition: service_healthy }
287
278
  environment:
288
- NYLORUN_RUNTIME_URL: http://runtime:4001 # the operator listener: Admin and Tenant API
279
+ NYLORUN_RUNTIME_URL: http://runtime:4000 # the public listener: Runtime and Management API
289
280
  NYLORUN_ADMIN_KEY_FILE: /run/nylorun/host-credentials.json
290
281
  PORT: "3000"
291
282
  # Studio's Host check accepts localhost/127.0.0.1 on the published port.
@@ -341,13 +332,13 @@ const RESTATE_UI = ` networks: [store, default]
341
332
  - "127.0.0.1:\${NYLORUN_RESTATE_PORT:?run nylorun start}:9070" # Restate UI and admin (unauthenticated): nylorun start --restate-ui
342
333
  `;
343
334
  /**
344
- * The harness service (F6.2): agent turns, MCP stdio servers and workspaces, apart from core.
335
+ * The harness service (F6.2): agent turns and workspaces, apart from core.
345
336
  * It holds the harness token only; models and remote MCP servers are reached through the
346
337
  * gateway with each run's token. Healthy once connected to core's Harness API.
347
338
  */
348
339
  function harnessService(project) {
349
340
  return `
350
- harness: # agent turns, MCP servers and workspaces; holds only the harness token; not published
341
+ harness: # agent turns and workspaces; holds only the harness token; not published
351
342
  image: \${NYLORUN_RUNTIME_IMAGE:?run nylorun start}
352
343
  container_name: ${project}-harness
353
344
  labels: *tenant
@@ -362,13 +353,8 @@ function harnessService(project) {
362
353
  NYLORUN_GATES_URL: http://gateway:4100
363
354
  NYLORUN_HARNESS_ROOT: /harness
364
355
  volumes:
365
- # The Tenant's workspaces, plugin data and the homes of MCP stdio servers; nothing else.
356
+ # The Tenant's workspaces; nothing else.
366
357
  - \${NYLORUN_HOST_ROOT:?run nylorun start}/tenant/sandboxes:/harness/sandboxes
367
- - \${NYLORUN_HOST_ROOT:?run nylorun start}/tenant/plugin-data:/harness/plugin-data
368
- - \${NYLORUN_HOST_ROOT:?run nylorun start}/tenant/home:/harness/home
369
- - \${NYLORUN_HOST_ROOT:?run nylorun start}/tenant/tmp:/harness/tmp
370
- # Plugin roots on this machine, read-only, at their own path: agents name them so.
371
- - \${NYLORUN_HOST_ROOT:?run nylorun start}/plugins:\${NYLORUN_HOST_ROOT:?run nylorun start}/plugins:ro
372
358
  networks: [harness]
373
359
  healthcheck: # healthy once connected to core's Harness API
374
360
  test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:4300/health').then(r=>r.json()).then(b=>process.exit(b.connected?0:1),()=>process.exit(1))"]
@@ -10,8 +10,6 @@ export declare const DEFAULT_STUDIO_FRAME_ANCESTORS: readonly string[];
10
10
  export interface StackEnv {
11
11
  /** Published Runtime port (loopback): the Tenant API. */
12
12
  runtimePort: number;
13
- /** Published operator port (loopback): the Admin API. */
14
- adminPort: number;
15
13
  /** Published Studio port (loopback). */
16
14
  studioPort: number;
17
15
  /** Restate UI and admin port (loopback), published only with `restateUi`. */
@@ -84,7 +82,6 @@ export declare function parseEnvLines(text: string): Map<string, string>;
84
82
  /** The settings that persist across starts, from an existing .env. */
85
83
  export interface PersistedStackEnv {
86
84
  runtimePort?: number;
87
- adminPort?: number;
88
85
  studioPort?: number;
89
86
  restatePort?: number;
90
87
  /** Whether the last start published Restate's UI (`NYLORUN_RESTATE_UI=1`). */
@@ -16,7 +16,6 @@ const SANDBOX_KEYS = {
16
16
  };
17
17
  const KEYS = {
18
18
  runtimePort: "NYLORUN_PORT",
19
- adminPort: "NYLORUN_ADMIN_PORT",
20
19
  studioPort: "NYLORUN_STUDIO_PORT",
21
20
  restatePort: "NYLORUN_RESTATE_PORT",
22
21
  restateUi: "NYLORUN_RESTATE_UI",
@@ -63,7 +62,6 @@ export function renderEnvFile(env) {
63
62
  "",
64
63
  "# Published on 127.0.0.1; clients use http://localhost:<port>.",
65
64
  line("runtimePort"),
66
- line("adminPort"),
67
65
  line("studioPort"),
68
66
  "# Restate's UI and admin (unauthenticated) are published only with",
69
67
  "# nylorun start --restate-ui (or NYLORUN_RESTATE_UI=1); decided on every start.",
@@ -171,9 +169,6 @@ export function parsePersisted(text) {
171
169
  const runtimePort = port(values.get(KEYS.runtimePort));
172
170
  if (runtimePort)
173
171
  out.runtimePort = runtimePort;
174
- const adminPort = port(values.get(KEYS.adminPort));
175
- if (adminPort)
176
- out.adminPort = adminPort;
177
172
  const studioPort = port(values.get(KEYS.studioPort));
178
173
  if (studioPort)
179
174
  out.studioPort = studioPort;
@@ -13,8 +13,6 @@ export interface HostConfigFile {
13
13
  hostId: string;
14
14
  host: string;
15
15
  port: number;
16
- /** The operator listener's published port (the Admin API); absent on older Host roots. */
17
- adminPort?: number;
18
16
  runtimeVersion?: string;
19
17
  [field: string]: unknown;
20
18
  }
@@ -29,7 +27,6 @@ export declare function readHostConfig(paths: StackPaths): Promise<Record<string
29
27
  */
30
28
  export declare function writeStackHostConfig(paths: StackPaths, input: {
31
29
  port: number;
32
- adminPort?: number;
33
30
  runtimeVersion: string | undefined;
34
31
  }): Promise<HostConfigFile>;
35
32
  export declare function readAdminKey(paths: StackPaths): Promise<string | undefined>;
@@ -33,7 +33,6 @@ export async function ensureHostLayout(paths) {
33
33
  paths.tmp,
34
34
  paths.tenant,
35
35
  ...Object.values(paths.harness),
36
- paths.plugins,
37
36
  paths.keys,
38
37
  paths.docker,
39
38
  ])
@@ -63,7 +62,8 @@ export async function readHostConfig(paths) {
63
62
  * runtimeVersion when `runtimeVersion` is undefined; refuses a newer format.
64
63
  */
65
64
  export async function writeStackHostConfig(paths, input) {
66
- const existing = (await readHostConfig(paths)) ?? {};
65
+ // An older Host root's operator port (`adminPort`) is dropped: there is no operator listener.
66
+ const { adminPort: _, ...existing } = (await readHostConfig(paths)) ?? {};
67
67
  const format = existing.format;
68
68
  if (format !== undefined &&
69
69
  (typeof format !== "number" || !Number.isInteger(format) || format > KNOWN_FORMAT))
@@ -77,7 +77,6 @@ export async function writeStackHostConfig(paths, input) {
77
77
  hostId,
78
78
  host: STACK_CLIENT_HOST,
79
79
  port: input.port,
80
- ...(input.adminPort === undefined ? {} : { adminPort: input.adminPort }),
81
80
  ...(input.runtimeVersion !== undefined ? { runtimeVersion: input.runtimeVersion } : {}),
82
81
  };
83
82
  await writeAtomic(paths.config, `${JSON.stringify(config, null, 2)}\n`);
@@ -1,4 +1,4 @@
1
1
  import { type StackDeps } from "./commands.js";
2
- export declare const keyUsage = " key put <id> [--tenant <name>] create the Tenant's key <id>, or rotate it (the old key stops working); prints the key once\n key list [--tenant <name>] [--json] the Tenant's keys: id, role, when issued (never the keys)\n key rm <id> [--tenant <name>] delete key <id>: it stops working at once";
2
+ export declare const keyUsage = " key put <id> [--management] [--tenant <name>]\n create the Tenant's application key <id> (--management: a management key), or\n rotate it (the old key stops working); prints the key once\n key list [--tenant <name>] [--json] the Tenant's keys: id, role, when issued (never the keys)\n key rm <id> [--tenant <name>] delete key <id>: it stops working at once";
3
3
  /** `nylorun key <put|list|rm> ...` */
4
4
  export declare function keyCommand(deps: StackDeps, args: readonly string[]): Promise<number>;
@@ -1,27 +1,30 @@
1
1
  /**
2
- * `nylorun key put | list | rm`: the running local Tenant's operator keys (F9 I1, Host feature
3
- * `operator-keys`), through its Admin API with the admin key. Revocable application keys by
4
- * name, for app servers and other clients; none of these starts the Tenant.
2
+ * `nylorun key put | list | rm`: the running local Tenant's keys, through `nylorun-operate` in its
3
+ * runtime container. Revocable application keys by name, for app servers and other clients, and
4
+ * management keys (`--management`), which only this machine issues; none of these starts the
5
+ * Tenant.
5
6
  */
6
7
  import { CliError } from "../errors.js";
7
- import { parseStackFlags, runningAdmin } from "./commands.js";
8
+ import { parseStackFlags, runningOperate } from "./commands.js";
8
9
  import { deleteOperatorKey, listOperatorKeys, putOperatorKey } from "./operator-keys.js";
9
- export const keyUsage = ` key put <id> [--tenant <name>] create the Tenant's key <id>, or rotate it (the old key stops working); prints the key once
10
+ export const keyUsage = ` key put <id> [--management] [--tenant <name>]
11
+ create the Tenant's application key <id> (--management: a management key), or
12
+ rotate it (the old key stops working); prints the key once
10
13
  key list [--tenant <name>] [--json] the Tenant's keys: id, role, when issued (never the keys)
11
14
  key rm <id> [--tenant <name>] delete key <id>: it stops working at once`;
12
15
  const usageError = (message) => new CliError(message, 2);
13
16
  async function put(deps, args) {
14
- const usage = "nylorun key put <id> [--tenant <name>]";
15
- const flags = parseStackFlags(args, { values: ["--tenant"] }, usage);
17
+ const usage = "nylorun key put <id> [--management] [--tenant <name>]";
18
+ const flags = parseStackFlags(args, { booleans: ["--management"], values: ["--tenant"] }, usage);
16
19
  if (flags.rest.length !== 1)
17
20
  throw usageError(`Usage: ${usage}`);
18
21
  const id = flags.rest[0];
19
22
  const name = flags.values.get("--tenant");
20
- const admin = await runningAdmin(deps, name === undefined ? {} : { name });
21
- const key = await putOperatorKey(admin, id);
23
+ const running = await runningOperate(deps, name === undefined ? {} : { name });
24
+ const key = await putOperatorKey(running, id, flags.booleans.has("--management") ? "management" : "application");
22
25
  // The key alone on stdout, for scripts; it is not shown again.
23
26
  deps.out(key.key);
24
- deps.err(`${key.rotated ? "Rotated" : "Created"} key ${key.id} on Tenant ${admin.name}${key.rotated ? " (the previous key no longer works)" : ""}. Store it now: it is not shown again.`);
27
+ deps.err(`${key.rotated ? "Rotated" : "Created"} ${key.role} key ${key.id} on Tenant ${running.name}${key.rotated ? " (the previous key no longer works)" : ""}. Store it now: it is not shown again.`);
25
28
  return 0;
26
29
  }
27
30
  async function list(deps, args) {
@@ -30,8 +33,7 @@ async function list(deps, args) {
30
33
  if (flags.rest.length)
31
34
  throw usageError(`Usage: ${usage}`);
32
35
  const name = flags.values.get("--tenant");
33
- const admin = await runningAdmin(deps, name === undefined ? {} : { name });
34
- const keys = await listOperatorKeys(admin);
36
+ const keys = await listOperatorKeys(await runningOperate(deps, name === undefined ? {} : { name }));
35
37
  if (flags.booleans.has("--json")) {
36
38
  deps.out(JSON.stringify(keys, null, 2));
37
39
  return 0;
@@ -51,9 +53,9 @@ async function rm(deps, args) {
51
53
  throw usageError(`Usage: ${usage}`);
52
54
  const id = flags.rest[0];
53
55
  const name = flags.values.get("--tenant");
54
- const admin = await runningAdmin(deps, name === undefined ? {} : { name });
55
- if (!(await deleteOperatorKey(admin, id))) {
56
- deps.err(`No key ${id} on Tenant ${admin.name}.`);
56
+ const running = await runningOperate(deps, name === undefined ? {} : { name });
57
+ if (!(await deleteOperatorKey(running, id))) {
58
+ deps.err(`No key ${id} on Tenant ${running.name}.`);
57
59
  return 1;
58
60
  }
59
61
  deps.out(`Deleted key ${id}: it no longer works.`);
package/dist/stack/mcp.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * `nylorun mcp connect <url> --server <name>`: signs the running local Tenant in to a remote MCP
3
- * server with OAuth (F9 C2), through its Tenant API with an operator key (`runningTenantApi`).
3
+ * server with OAuth (F9 C2), through its Management API with a management key (`runningTenantApi`).
4
4
  * The Runtime's keys module runs discovery, registration and the code exchange and seals the
5
5
  * credential in an installation vault (`mcp` unless `--vault` names one); this command only
6
6
  * opens the browser and waits for the credential to appear. It never starts the Tenant.
@@ -21,7 +21,7 @@ async function call(deps, api, method, path, body) {
21
21
  const response = await deps.fetch(`${api.runtimeUrl}${path}`, {
22
22
  method,
23
23
  headers: {
24
- authorization: `Bearer ${api.applicationKey}`,
24
+ authorization: `Bearer ${api.managementKey}`,
25
25
  [PROTOCOL_HEADER]: String(PROTOCOL_VERSION),
26
26
  accept: "application/json",
27
27
  ...(body === undefined ? {} : { "content-type": "application/json" }),
@@ -52,12 +52,12 @@ function bindingUrl(input) {
52
52
  }
53
53
  /** The installation vault named `mcp`, created when there is none. */
54
54
  async function mcpVault(deps, api) {
55
- const { vaults } = await call(deps, api, "GET", "/v1/vaults");
55
+ const { vaults } = await call(deps, api, "GET", "/v1/tenant/vaults");
56
56
  const found = vaults.find((vault) => vault.ownerUserId === "installation" && vault.name === MCP_VAULT_NAME);
57
57
  if (found)
58
58
  return found;
59
59
  const key = `nylorun-mcp-${randomUUID()}`;
60
- return await call(deps, api, "POST", "/v1/vaults", {
60
+ return await call(deps, api, "POST", "/v1/tenant/vaults", {
61
61
  requestId: key,
62
62
  idempotencyKey: key,
63
63
  name: MCP_VAULT_NAME,
@@ -66,7 +66,7 @@ async function mcpVault(deps, api) {
66
66
  }
67
67
  /** Each credential of the vault bound to `url`, by id, with when it last changed. */
68
68
  async function credentialsFor(deps, api, vaultId, url) {
69
- const { credentials } = await call(deps, api, "GET", `/v1/vaults/${encodeURIComponent(vaultId)}/credentials`);
69
+ const { credentials } = await call(deps, api, "GET", `/v1/tenant/vaults/${encodeURIComponent(vaultId)}/credentials`);
70
70
  return new Map(credentials
71
71
  .filter((item) => item.type === "oauth" && item.binding.url === url)
72
72
  .map((item) => [item.id, item.rotatedAt ?? item.createdAt]));
@@ -85,7 +85,7 @@ async function connect(deps, args) {
85
85
  const api = await runningTenantApi(deps, name === undefined ? {} : { name });
86
86
  const vaultId = flags.values.get("--vault") ?? (await mcpVault(deps, api)).id;
87
87
  const before = await credentialsFor(deps, api, vaultId, url);
88
- const started = await call(deps, api, "POST", `/v1/vaults/${encodeURIComponent(vaultId)}/oauth/start`, { url, server, ...(clientId === undefined ? {} : { clientId }) });
88
+ const started = await call(deps, api, "POST", `/v1/tenant/vaults/${encodeURIComponent(vaultId)}/oauth/start`, { url, server, ...(clientId === undefined ? {} : { clientId }) });
89
89
  deps.out(`Sign in to ${server} in your browser:`);
90
90
  deps.out(` ${started.authorizeUrl}`);
91
91
  if (!flags.booleans.has("--no-open"))
@@ -1,39 +1,39 @@
1
1
  import type { OperatorKey, PutOperatorKeyResponse } from "@nylorun/core/contracts";
2
2
  import { type KeyCredentials } from "../project/link.js";
3
+ import type { DockerResult } from "./docker.js";
3
4
  import type { FetchLike } from "./studio-login.js";
4
- /** Where a local Tenant's Admin API answers, and its admin key. */
5
- export interface AdminEndpoint {
5
+ /** A local Tenant: `nylorun-operate` in its runtime container, and a fetch to probe keys. */
6
+ export interface OperateEndpoint {
6
7
  fetch: FetchLike;
7
- adminUrl: string;
8
- adminKey: string;
8
+ /** Runs `nylorun-operate <args>` in the runtime container. */
9
+ operate(args: readonly string[]): Promise<DockerResult>;
9
10
  }
10
- /** The key `nylorun start` gives the projects it links. */
11
+ /** The keys `nylorun start` gives the projects it links (and `-management`). */
11
12
  export declare const PROJECT_KEY_ID = "project";
12
- /** The key nylorun commands use outside a linked project. */
13
+ /** The keys nylorun commands use outside a linked project (and `-management`). */
13
14
  export declare const CLI_KEY_ID = "cli";
14
- /** Creates key `id` or rotates it; the answer holds the key, this once. */
15
- export declare function putOperatorKey(admin: AdminEndpoint, id: string): Promise<PutOperatorKeyResponse>;
16
- export declare function listOperatorKeys(admin: AdminEndpoint): Promise<OperatorKey[]>;
15
+ /** Creates key `id` with `role` or rotates it; the answer holds the key, this once. */
16
+ export declare function putOperatorKey(endpoint: OperateEndpoint, id: string, role: "application" | "management"): Promise<PutOperatorKeyResponse>;
17
+ export declare function listOperatorKeys(endpoint: OperateEndpoint): Promise<OperatorKey[]>;
17
18
  /** Deletes key `id`: false when there is none. */
18
- export declare function deleteOperatorKey(admin: AdminEndpoint, id: string): Promise<boolean>;
19
+ export declare function deleteOperatorKey(endpoint: OperateEndpoint, id: string): Promise<boolean>;
19
20
  /**
20
- * Whether `key` reaches the Tenant API at `runtimeUrl`: one authenticated read
21
- * (`GET /v1/tenant`). An unknown key is the opaque 404; another answer is an error.
21
+ * Whether `key` reaches the Tenant at `runtimeUrl`: one authenticated read (`GET /v1/me`, which
22
+ * every key reaches). An unknown key is the opaque 404; another answer is an error.
22
23
  */
23
24
  export declare function keyAuthenticates(fetch: FetchLike, runtimeUrl: string, key: string): Promise<boolean>;
24
25
  /**
25
- * The operator key `id` kept in the Host root file `file` (0600): the file's key while it
26
- * still authenticates, else `adopt` when it does (a key a project already holds), else a new
27
- * key put through the Admin API and written to the file. One process at a time (`lock`), so
28
- * concurrent commands share one key instead of rotating each other's.
26
+ * The keys `id` (application) and `id-management` (management) kept in the Host root file
27
+ * `file` (0600): each of the file's keys while it still authenticates, else `adopt`'s when it
28
+ * does (keys a project already holds), else a new key put through `nylorun-operate`, written to
29
+ * the file. One process at a time (`lock`), so concurrent commands share one key instead of
30
+ * rotating each other's.
29
31
  */
30
32
  export declare function hostKey(options: {
31
- admin: AdminEndpoint;
33
+ endpoint: OperateEndpoint;
32
34
  runtimeUrl: string;
33
35
  id: string;
34
36
  file: string;
35
37
  lock: string;
36
38
  adopt?: KeyCredentials;
37
- }): Promise<KeyCredentials & {
38
- put: boolean;
39
- }>;
39
+ }): Promise<Required<KeyCredentials>>;
@@ -1,76 +1,54 @@
1
1
  /**
2
- * Operator keys (F9 I1, Host feature `operator-keys`) as the nylorun commands use them: the
3
- * Admin API's `/v1/admin/keys` on a local Tenant's operator listener, with its admin key, and
4
- * the keys nylorun keeps in the Host root (`project`, `cli`). nylorun depends on
5
- * `@nylorun/core` only, so these are plain requests (`admin.keys` in `@nylorun/admin` makes
6
- * the same).
2
+ * A local Tenant's keys as the nylorun commands use them: `nylorun-operate keys … --json` in its
3
+ * runtime container (`docker compose exec`), and the keys nylorun keeps in the Host root
4
+ * (`project` and `project-management`, `cli` and `cli-management`). nylorun depends on
5
+ * `@nylorun/core` only, so key probes are plain requests.
7
6
  */
8
7
  import { open, rm, stat } from "node:fs/promises";
9
8
  import { PROTOCOL_HEADER, PROTOCOL_VERSION } from "@nylorun/core/compatibility";
10
9
  import { CliError } from "../errors.js";
11
10
  import { readCredentialsFile, writeCredentialsFile, } from "../project/link.js";
12
- /** The key `nylorun start` gives the projects it links. */
11
+ /** The keys `nylorun start` gives the projects it links (and `-management`). */
13
12
  export const PROJECT_KEY_ID = "project";
14
- /** The key nylorun commands use outside a linked project. */
13
+ /** The keys nylorun commands use outside a linked project (and `-management`). */
15
14
  export const CLI_KEY_ID = "cli";
16
- async function adminRequest(admin, method, path) {
17
- let response;
18
- try {
19
- response = await admin.fetch(`${admin.adminUrl}${path}`, {
20
- method,
21
- headers: {
22
- authorization: `Bearer ${admin.adminKey}`,
23
- [PROTOCOL_HEADER]: String(PROTOCOL_VERSION),
24
- accept: "application/json",
25
- },
26
- redirect: "error",
27
- signal: AbortSignal.timeout(15_000),
28
- });
29
- }
30
- catch (error) {
31
- throw new CliError(`The Admin API at ${admin.adminUrl} did not answer ${method} ${path} (${error instanceof Error ? error.message : String(error)}).`, 7);
15
+ /** `nylorun-operate keys <args> --json`: its JSON answer, or exit 1 refused, 2 Tenant not open. */
16
+ async function operateKeys(endpoint, args) {
17
+ const result = await endpoint.operate(["keys", ...args, "--json"]);
18
+ const message = result.stderr.trim();
19
+ if (result.code === 0) {
20
+ try {
21
+ return JSON.parse(result.stdout);
22
+ }
23
+ catch {
24
+ /* below */
25
+ }
32
26
  }
33
- const body = (await response.json().catch(() => ({})));
34
- return { status: response.status, body };
27
+ if (result.code === 1)
28
+ throw new CliError(message, 1);
29
+ if (result.code === 2)
30
+ throw new CliError(`The Tenant is not open: ${message}`, 7);
31
+ throw new CliError(`nylorun-operate keys ${args.join(" ")} failed (exit ${result.code})${message ? `: ${message}` : ""}`, 1);
35
32
  }
36
- function refused(method, path, answer) {
37
- const message = typeof answer.body.message === "string" ? `: ${answer.body.message}` : "";
38
- // An older Runtime has no key routes: the Admin API's "Route not found".
39
- if (answer.status === 404 && answer.body.message === "Route not found")
40
- return new CliError(`The Runtime does not serve operator keys (Host feature operator-keys). Update it: npx nylorun@latest start.`, 1);
41
- return new CliError(`${method} ${path} returned ${answer.status}${message}`, 1);
33
+ /** Creates key `id` with `role` or rotates it; the answer holds the key, this once. */
34
+ export async function putOperatorKey(endpoint, id, role) {
35
+ return (await operateKeys(endpoint, ["put", id, "--role", role]));
42
36
  }
43
- const keyPath = (id) => `/v1/admin/keys/${encodeURIComponent(id)}`;
44
- /** Creates key `id` or rotates it; the answer holds the key, this once. */
45
- export async function putOperatorKey(admin, id) {
46
- const answer = await adminRequest(admin, "PUT", keyPath(id));
47
- if (answer.status !== 200 || typeof answer.body.key !== "string")
48
- throw refused("PUT", keyPath(id), answer);
49
- return answer.body;
50
- }
51
- export async function listOperatorKeys(admin) {
52
- const answer = await adminRequest(admin, "GET", "/v1/admin/keys");
53
- if (answer.status !== 200 || !Array.isArray(answer.body.keys))
54
- throw refused("GET", "/v1/admin/keys", answer);
55
- return answer.body.keys;
37
+ export async function listOperatorKeys(endpoint) {
38
+ return (await operateKeys(endpoint, ["list"])).keys;
56
39
  }
57
40
  /** Deletes key `id`: false when there is none. */
58
- export async function deleteOperatorKey(admin, id) {
59
- const answer = await adminRequest(admin, "DELETE", keyPath(id));
60
- if (answer.status === 200)
61
- return true;
62
- if (answer.status === 404 && answer.body.message === `No key ${id}`)
63
- return false;
64
- throw refused("DELETE", keyPath(id), answer);
41
+ export async function deleteOperatorKey(endpoint, id) {
42
+ return (await operateKeys(endpoint, ["rm", id])).deleted;
65
43
  }
66
44
  /**
67
- * Whether `key` reaches the Tenant API at `runtimeUrl`: one authenticated read
68
- * (`GET /v1/tenant`). An unknown key is the opaque 404; another answer is an error.
45
+ * Whether `key` reaches the Tenant at `runtimeUrl`: one authenticated read (`GET /v1/me`, which
46
+ * every key reaches). An unknown key is the opaque 404; another answer is an error.
69
47
  */
70
48
  export async function keyAuthenticates(fetch, runtimeUrl, key) {
71
49
  let response;
72
50
  try {
73
- response = await fetch(`${runtimeUrl}/v1/tenant`, {
51
+ response = await fetch(`${runtimeUrl}/v1/me`, {
74
52
  headers: {
75
53
  authorization: `Bearer ${key}`,
76
54
  [PROTOCOL_HEADER]: String(PROTOCOL_VERSION),
@@ -88,7 +66,7 @@ export async function keyAuthenticates(fetch, runtimeUrl, key) {
88
66
  return true;
89
67
  if (response.status === 401 || response.status === 403 || response.status === 404)
90
68
  return false;
91
- throw new CliError(`GET ${runtimeUrl}/v1/tenant returned ${response.status}.`, 7);
69
+ throw new CliError(`GET ${runtimeUrl}/v1/me returned ${response.status}.`, 7);
92
70
  }
93
71
  const LOCK_TIMEOUT_MS = 30_000;
94
72
  /** A lock older than this was left by a process that died. */
@@ -123,24 +101,38 @@ async function withLock(path, fn) {
123
101
  }
124
102
  }
125
103
  /**
126
- * The operator key `id` kept in the Host root file `file` (0600): the file's key while it
127
- * still authenticates, else `adopt` when it does (a key a project already holds), else a new
128
- * key put through the Admin API and written to the file. One process at a time (`lock`), so
129
- * concurrent commands share one key instead of rotating each other's.
104
+ * The keys `id` (application) and `id-management` (management) kept in the Host root file
105
+ * `file` (0600): each of the file's keys while it still authenticates, else `adopt`'s when it
106
+ * does (keys a project already holds), else a new key put through `nylorun-operate`, written to
107
+ * the file. One process at a time (`lock`), so concurrent commands share one key instead of
108
+ * rotating each other's.
130
109
  */
131
110
  export async function hostKey(options) {
132
- const { admin, runtimeUrl, id, file } = options;
111
+ const { endpoint, runtimeUrl, id, file, adopt } = options;
112
+ const reaches = (key) => key === undefined ? Promise.resolve(false) : keyAuthenticates(endpoint.fetch, runtimeUrl, key);
133
113
  return await withLock(options.lock, async () => {
134
114
  const kept = await readCredentialsFile(file);
135
- if (kept && (await keyAuthenticates(admin.fetch, runtimeUrl, kept.applicationKey)))
136
- return { ...kept, put: false };
137
- if (options.adopt && (await keyAuthenticates(admin.fetch, runtimeUrl, options.adopt.applicationKey))) {
138
- await writeCredentialsFile(file, options.adopt);
139
- return { ...options.adopt, put: false };
115
+ let application = kept && (await reaches(kept.applicationKey)) ? kept : undefined;
116
+ if (!application && adopt && (await reaches(adopt.applicationKey)))
117
+ application = adopt;
118
+ if (!application) {
119
+ const put = await putOperatorKey(endpoint, id, "application");
120
+ application = { applicationKey: put.key, principalId: put.id };
121
+ }
122
+ let management = (await reaches(kept?.management?.key)) ? kept.management : undefined;
123
+ if (!management && (await reaches(adopt?.management?.key)))
124
+ management = adopt.management;
125
+ if (!management) {
126
+ const put = await putOperatorKey(endpoint, `${id}-management`, "management");
127
+ management = { key: put.key, principalId: put.id };
140
128
  }
141
- const put = await putOperatorKey(admin, id);
142
- const credentials = { applicationKey: put.key, principalId: put.id };
143
- await writeCredentialsFile(file, credentials);
144
- return { ...credentials, put: true };
129
+ const credentials = {
130
+ applicationKey: application.applicationKey,
131
+ principalId: application.principalId,
132
+ management,
133
+ };
134
+ if (JSON.stringify(kept) !== JSON.stringify(credentials))
135
+ await writeCredentialsFile(file, credentials);
136
+ return credentials;
145
137
  });
146
138
  }
@@ -27,23 +27,12 @@ export interface StackPaths {
27
27
  * runtime reads it at boot (`NYLORUN_IDENTITY_FILE`).
28
28
  */
29
29
  identity: string;
30
- /** The Tenant directory: homes, sandboxes, plugin data, logs. */
30
+ /** The Tenant directory: home, sandboxes, logs. */
31
31
  tenant: string;
32
- /**
33
- * The Tenant directory's parts the harness container mounts (F6.2): workspaces, plugin data,
34
- * and the home and temporary directory of MCP stdio servers.
35
- */
32
+ /** The Tenant directory's parts the harness container mounts (F6.2): workspaces. */
36
33
  harness: {
37
34
  sandboxes: string;
38
- pluginData: string;
39
- home: string;
40
- tmp: string;
41
35
  };
42
- /**
43
- * Plugin roots on this machine (`plugins/`), mounted read-only at the same path into the
44
- * runtime and harness containers, so a stdio MCP server's plugin root resolves there.
45
- */
46
- plugins: string;
47
36
  /** The keys directory, mounted only into the gateway (F4.2). */
48
37
  keys: string;
49
38
  /** The Tenant's vault key (KEK), `keys/vault-kek`. */