nylorun 0.9.0-beta → 0.11.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 +87 -0
- package/README.md +54 -40
- package/dist/doctor.js +2 -2
- package/dist/project/link.d.ts +12 -4
- package/dist/project/link.js +19 -6
- package/dist/project/seed.d.ts +2 -1
- package/dist/project/seed.js +5 -5
- package/dist/stack/commands.d.ts +29 -20
- package/dist/stack/commands.js +133 -134
- package/dist/stack/compose-file.d.ts +10 -12
- package/dist/stack/compose-file.js +21 -35
- package/dist/stack/env-file.d.ts +0 -3
- package/dist/stack/env-file.js +0 -5
- package/dist/stack/host-files.d.ts +0 -3
- package/dist/stack/host-files.js +2 -3
- package/dist/stack/keys.d.ts +1 -1
- package/dist/stack/keys.js +17 -15
- package/dist/stack/mcp.js +6 -6
- package/dist/stack/operator-keys.d.ts +20 -20
- package/dist/stack/operator-keys.js +61 -69
- package/dist/stack/paths.d.ts +2 -13
- package/dist/stack/paths.js +1 -7
- package/dist/stack/ports.d.ts +0 -2
- package/dist/stack/ports.js +0 -2
- package/dist/stack/prepare.js +0 -3
- package/package.json +4 -4
|
@@ -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
|
|
27
|
-
*
|
|
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)
|
|
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
|
|
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
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
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
|
-
#
|
|
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
|
|
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
|
|
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}
|
|
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
|
-
#
|
|
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" #
|
|
272
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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))"]
|
package/dist/stack/env-file.d.ts
CHANGED
|
@@ -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`). */
|
package/dist/stack/env-file.js
CHANGED
|
@@ -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>;
|
package/dist/stack/host-files.js
CHANGED
|
@@ -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
|
-
|
|
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`);
|
package/dist/stack/keys.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { type StackDeps } from "./commands.js";
|
|
2
|
-
export declare const keyUsage = " key put <id> [--tenant <name>]
|
|
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>;
|
package/dist/stack/keys.js
CHANGED
|
@@ -1,27 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `nylorun key put | list | rm`: the running local Tenant's
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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,
|
|
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>]
|
|
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
|
|
21
|
-
const key = await putOperatorKey(
|
|
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 ${
|
|
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
|
|
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
|
|
55
|
-
if (!(await deleteOperatorKey(
|
|
56
|
-
deps.err(`No key ${id} on Tenant ${
|
|
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
|
|
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.
|
|
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
|
-
/**
|
|
5
|
-
export interface
|
|
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
|
-
|
|
8
|
-
|
|
8
|
+
/** Runs `nylorun-operate <args>` in the runtime container. */
|
|
9
|
+
operate(args: readonly string[]): Promise<DockerResult>;
|
|
9
10
|
}
|
|
10
|
-
/** The
|
|
11
|
+
/** The keys `nylorun start` gives the projects it links (and `-management`). */
|
|
11
12
|
export declare const PROJECT_KEY_ID = "project";
|
|
12
|
-
/** The
|
|
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(
|
|
16
|
-
export declare function listOperatorKeys(
|
|
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(
|
|
19
|
+
export declare function deleteOperatorKey(endpoint: OperateEndpoint, id: string): Promise<boolean>;
|
|
19
20
|
/**
|
|
20
|
-
* Whether `key` reaches the Tenant
|
|
21
|
-
*
|
|
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
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* concurrent commands share one key instead of
|
|
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
|
-
|
|
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
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* `@nylorun/core` only, so
|
|
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
|
|
11
|
+
/** The keys `nylorun start` gives the projects it links (and `-management`). */
|
|
13
12
|
export const PROJECT_KEY_ID = "project";
|
|
14
|
-
/** The
|
|
13
|
+
/** The keys nylorun commands use outside a linked project (and `-management`). */
|
|
15
14
|
export const CLI_KEY_ID = "cli";
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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(
|
|
59
|
-
|
|
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
|
|
68
|
-
*
|
|
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/
|
|
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/
|
|
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
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
* concurrent commands share one key instead of
|
|
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 {
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
}
|
package/dist/stack/paths.d.ts
CHANGED
|
@@ -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:
|
|
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`. */
|