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.
- package/CHANGELOG.md +80 -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
package/dist/stack/commands.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { mkdir, readFile, rm } from "node:fs/promises";
|
|
3
3
|
import { basename, resolve } from "node:path";
|
|
4
|
-
import { compareVersions
|
|
4
|
+
import { compareVersions } from "@nylorun/core/compatibility";
|
|
5
5
|
import { CliError } from "../errors.js";
|
|
6
6
|
import { credentialsPath, readCredentialsFile, readProjectCredentials, readProjectLink, writeProjectCredentials, writeProjectLink, } from "../project/link.js";
|
|
7
7
|
import { findProjectRoot } from "../project/root.js";
|
|
@@ -30,10 +30,10 @@ const CORE_SERVICES = ["postgres", "restate", "s2-lite", "rustfs", "gateway", "r
|
|
|
30
30
|
function coreServices(harness) {
|
|
31
31
|
return CORE_SERVICES.filter((service) => service !== "harness" || harness === "remote");
|
|
32
32
|
}
|
|
33
|
-
export const stackUsage = ` up|start [--tenant <name>] [--no-link] [--no-studio] [--
|
|
33
|
+
export const stackUsage = ` up|start [--tenant <name>] [--no-link] [--no-studio] [--allow-downgrade] [--studio-embed-origin <origin>]... [--studio-embed-origin-reset] [--restate-ui]
|
|
34
34
|
start the project's Tenant, creating it and the Project link (.nylorun/link.json,
|
|
35
|
-
credentials.json) on the first run; print the Runtime and Studio URLs
|
|
36
|
-
|
|
35
|
+
credentials.json) on the first run; print the Runtime and Studio URLs (it opens no
|
|
36
|
+
browser). --tenant attaches to (or creates) a named Tenant; --no-link
|
|
37
37
|
starts the default Tenant (or --tenant's) without linking the current directory;
|
|
38
38
|
--restate-ui (or NYLORUN_RESTATE_UI=1) publishes Restate's UI on loopback for debugging
|
|
39
39
|
down|stop [--tenant <name> | --all] stop the Tenant's containers (--all: every Tenant's); keep volumes
|
|
@@ -147,6 +147,13 @@ function composeArgs(ctx, ...args) {
|
|
|
147
147
|
...args,
|
|
148
148
|
];
|
|
149
149
|
}
|
|
150
|
+
/** `nylorun-operate` in the Tenant's runtime container (`docker compose exec`), for its keys. */
|
|
151
|
+
function operateEndpoint(ctx) {
|
|
152
|
+
return {
|
|
153
|
+
fetch: ctx.deps.fetch,
|
|
154
|
+
operate: (args) => ctx.deps.docker.run(composeArgs(ctx, "exec", "-T", "runtime", "nylorun-operate", ...args)),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
150
157
|
function requireStackFiles(ctx) {
|
|
151
158
|
if (!existsSync(ctx.paths.compose) || !existsSync(ctx.paths.env))
|
|
152
159
|
throw new CliError(`No Tenant ${ctx.name} under ${ctx.paths.root}. Run "nylorun start" first.`, 3);
|
|
@@ -221,34 +228,21 @@ async function waitForHealth(ctx, runtimeUrl, hostId) {
|
|
|
221
228
|
await sleep(ctx.deps.pollMs ?? 500);
|
|
222
229
|
}
|
|
223
230
|
}
|
|
224
|
-
/** `/
|
|
225
|
-
async function
|
|
226
|
-
if (!adminKey)
|
|
227
|
-
return undefined;
|
|
231
|
+
/** The Runtime's `/ready` (503 while not ready, same body); undefined when it does not answer. */
|
|
232
|
+
async function fetchReady(deps, runtimeUrl) {
|
|
228
233
|
try {
|
|
229
|
-
const response = await deps.fetch(`${
|
|
230
|
-
headers: {
|
|
231
|
-
authorization: `Bearer ${adminKey}`,
|
|
232
|
-
[PROTOCOL_HEADER]: String(PROTOCOL_VERSION),
|
|
233
|
-
accept: "application/json",
|
|
234
|
-
},
|
|
234
|
+
const response = await deps.fetch(`${runtimeUrl}/ready`, {
|
|
235
|
+
headers: { accept: "application/json" },
|
|
235
236
|
signal: AbortSignal.timeout(3000),
|
|
236
237
|
redirect: "error",
|
|
237
238
|
});
|
|
238
|
-
if (!response.ok)
|
|
239
|
-
return undefined;
|
|
240
239
|
const body = (await response.json());
|
|
241
|
-
|
|
242
|
-
if (!tenant || (tenant.state !== "open" && tenant.state !== "unavailable"))
|
|
240
|
+
if (body.status !== "ready" && body.status !== "not_ready")
|
|
243
241
|
return undefined;
|
|
244
|
-
const harness = body.
|
|
242
|
+
const harness = body.harness;
|
|
245
243
|
return {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
name: typeof tenant.name === "string" ? tenant.name : null,
|
|
249
|
-
state: tenant.state,
|
|
250
|
-
...(tenant.cause ? { cause: tenant.cause } : {}),
|
|
251
|
-
},
|
|
244
|
+
ready: body.status === "ready",
|
|
245
|
+
tenant: body.checks?.tenant === true,
|
|
252
246
|
...((harness?.mode === "remote" || harness?.mode === "in-process") &&
|
|
253
247
|
typeof harness.connected === "number"
|
|
254
248
|
? { harness: { mode: harness.mode, connected: harness.connected } }
|
|
@@ -259,32 +253,59 @@ async function fetchAdminStatus(deps, adminUrl, adminKey) {
|
|
|
259
253
|
return undefined;
|
|
260
254
|
}
|
|
261
255
|
}
|
|
262
|
-
/**
|
|
263
|
-
|
|
264
|
-
|
|
256
|
+
/**
|
|
257
|
+
* `nylorun-operate status --json` in the runtime container (exit 0 open, 2 not open): the
|
|
258
|
+
* Tenant's id and name, and why it cannot open. Undefined when it does not answer.
|
|
259
|
+
*/
|
|
260
|
+
async function operateStatus(ctx) {
|
|
261
|
+
const result = await operateEndpoint(ctx).operate(["status", "--json"]);
|
|
262
|
+
if (result.code !== 0 && result.code !== 2)
|
|
263
|
+
return undefined;
|
|
264
|
+
let tenant;
|
|
265
|
+
try {
|
|
266
|
+
tenant = JSON.parse(result.stdout).tenant;
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
if (!tenant || (tenant.state !== "open" && tenant.state !== "unavailable"))
|
|
272
|
+
return undefined;
|
|
273
|
+
return {
|
|
274
|
+
id: typeof tenant.id === "string" ? tenant.id : null,
|
|
275
|
+
name: typeof tenant.name === "string" ? tenant.name : null,
|
|
276
|
+
state: tenant.state,
|
|
277
|
+
...(typeof tenant.cause === "string" ? { cause: tenant.cause } : {}),
|
|
278
|
+
...(typeof tenant.message === "string" ? { message: tenant.message } : {}),
|
|
279
|
+
};
|
|
265
280
|
}
|
|
266
|
-
/** Why the Tenant is unavailable,
|
|
281
|
+
/** Why the Tenant is unavailable, as `nylorun-operate status` names it. */
|
|
267
282
|
function tenantCauseMessage(ctx, tenant) {
|
|
268
|
-
const
|
|
269
|
-
const hint = cause.code === "schema-too-new"
|
|
283
|
+
const hint = tenant.cause === "schema-too-new"
|
|
270
284
|
? ` This nylorun pins Runtime ${ctx.deps.runtimeVersion}, older than the Tenant's database: update nylorun (npx nylorun@latest start).`
|
|
271
285
|
: "";
|
|
272
|
-
|
|
286
|
+
const message = tenant.message ? `: ${tenant.message.replace(/\.?$/, ".")}` : ".";
|
|
287
|
+
return `Tenant ${ctx.name} is unavailable (${tenant.cause})${message}${hint} See "nylorun logs runtime".`;
|
|
273
288
|
}
|
|
274
289
|
/**
|
|
275
|
-
* Wait until the Tenant is open: the Runtime creates it on the
|
|
276
|
-
* it on later ones.
|
|
290
|
+
* Wait until the Tenant is open (`/ready`'s `tenant` check): the Runtime creates it on the
|
|
291
|
+
* first start and opens it on later ones. Then `nylorun-operate status` gives its id. While it
|
|
292
|
+
* is not open, `nylorun-operate status` is asked now and then: it names a cause only when the
|
|
293
|
+
* Tenant cannot open (`schema-too-new`, `database-layout-old`), which ends the wait.
|
|
277
294
|
*/
|
|
278
|
-
async function waitForTenant(ctx,
|
|
295
|
+
async function waitForTenant(ctx, runtimeUrl) {
|
|
279
296
|
const deadline = Date.now() + (ctx.deps.healthTimeoutMs ?? 60_000);
|
|
280
|
-
for (;;) {
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
297
|
+
for (let poll = 1;; poll += 1) {
|
|
298
|
+
const open = (await fetchReady(ctx.deps, runtimeUrl))?.tenant === true;
|
|
299
|
+
const late = Date.now() > deadline;
|
|
300
|
+
if (open || late || poll % 10 === 0) {
|
|
301
|
+
const tenant = await operateStatus(ctx);
|
|
302
|
+
if (tenant?.state === "open" && tenant.id)
|
|
303
|
+
return { ...tenant, id: tenant.id };
|
|
304
|
+
if (!open && tenant?.cause)
|
|
305
|
+
throw new CliError(tenantCauseMessage(ctx, tenant), 7);
|
|
306
|
+
if (late)
|
|
307
|
+
throw new CliError(`Tenant ${ctx.name} did not open (${runtimeUrl}/ready: ${open ? "open, but nylorun-operate status did not answer" : "not open"})${tenant?.message ? `: ${tenant.message.replace(/\.$/, "")}` : ""}. See "nylorun status" and "nylorun logs runtime".`, 7);
|
|
308
|
+
}
|
|
288
309
|
await sleep(ctx.deps.pollMs ?? 500);
|
|
289
310
|
}
|
|
290
311
|
}
|
|
@@ -353,7 +374,6 @@ async function tenantPorts(base, except) {
|
|
|
353
374
|
const persisted = await readStackEnv(paths);
|
|
354
375
|
for (const port of [
|
|
355
376
|
persisted?.runtimePort,
|
|
356
|
-
persisted?.adminPort,
|
|
357
377
|
persisted?.studioPort,
|
|
358
378
|
persisted?.restatePort,
|
|
359
379
|
])
|
|
@@ -408,14 +428,13 @@ async function bringUp(ctx, options) {
|
|
|
408
428
|
if (prepared.firstRun)
|
|
409
429
|
deps.err(`Created Tenant ${ctx.name} under ${ctx.paths.root} (Runtime port ${prepared.env.runtimePort}, Studio port ${prepared.env.studioPort}).`);
|
|
410
430
|
const runtimeUrl = `http://${STACK_CLIENT_HOST}:${prepared.env.runtimePort}`;
|
|
411
|
-
const adminUrl = `http://${STACK_CLIENT_HOST}:${prepared.env.adminPort}`;
|
|
412
431
|
const up = await deps.docker.stream(
|
|
413
432
|
// --remove-orphans: a service this start's file no longer has (the harness after a rollback
|
|
414
433
|
// to NYLORUN_HARNESS=in-process, sandboxes after disable) is removed, not left running.
|
|
415
434
|
composeArgs(ctx, "up", "--detach", "--wait", "--wait-timeout", "300", "--remove-orphans", ...coreServices(prepared.env.harness)));
|
|
416
435
|
if (up !== 0) {
|
|
417
|
-
// A Runtime whose Tenant cannot open fails readiness;
|
|
418
|
-
const tenant = await
|
|
436
|
+
// A Runtime whose Tenant cannot open fails readiness; nylorun-operate names the cause.
|
|
437
|
+
const tenant = await operateStatus(ctx);
|
|
419
438
|
if (tenant?.cause)
|
|
420
439
|
throw new CliError(tenantCauseMessage(ctx, tenant), 7);
|
|
421
440
|
throw new CliError(`docker compose up failed (exit ${up}). See "nylorun logs runtime", "nylorun logs gateway", "nylorun logs harness" and "nylorun status".`, 7);
|
|
@@ -436,7 +455,6 @@ async function bringUp(ctx, options) {
|
|
|
436
455
|
}
|
|
437
456
|
return {
|
|
438
457
|
runtimeUrl,
|
|
439
|
-
adminUrl,
|
|
440
458
|
hostId: prepared.host.hostId,
|
|
441
459
|
studioPort: prepared.env.studioPort,
|
|
442
460
|
studioUrl: studioOrigin(prepared.env.studioPort),
|
|
@@ -468,12 +486,10 @@ async function tryStudioLogin(ctx, studioPort, adminKey) {
|
|
|
468
486
|
deps.err(`Warning: Studio at ${origin} is not reachable for a login (${lastError}).`);
|
|
469
487
|
return undefined;
|
|
470
488
|
}
|
|
471
|
-
/** Printed by `start
|
|
489
|
+
/** Printed by `start`, which prints Studio's URL and opens no browser. */
|
|
472
490
|
export const STUDIO_SIGN_IN_HINT = 'To sign a browser in to Studio, run "npx nylorun studio".';
|
|
473
|
-
/**
|
|
474
|
-
|
|
475
|
-
return Boolean(deps.interactive) && !deps.env.CI && !flags.booleans.has("--no-open");
|
|
476
|
-
}
|
|
491
|
+
/** Printed last by `start`. */
|
|
492
|
+
export const HELP_HINT = 'Run "npx nylorun --help" to see what else nylorun can do.';
|
|
477
493
|
/**
|
|
478
494
|
* Open a Studio login in the browser, or print it when no browser starts.
|
|
479
495
|
* The login URL carries a single-use token, so it is printed only as a fallback.
|
|
@@ -487,11 +503,12 @@ async function openLogin(ctx, login) {
|
|
|
487
503
|
* `.nylorun/credentials.json`. The link is rewritten only when it is older, or the Tenant's
|
|
488
504
|
* name, URL, Host or id changed; a new link seeds the Tenant from the project's `.env`.
|
|
489
505
|
*
|
|
490
|
-
* The credentials file is kept while its
|
|
491
|
-
* read). Otherwise it gets the
|
|
492
|
-
* the
|
|
493
|
-
*
|
|
494
|
-
* another's
|
|
506
|
+
* The credentials file is kept while both its keys still reach the Tenant (one authenticated
|
|
507
|
+
* read each). Otherwise it gets the keys `project` (application) and `project-management`
|
|
508
|
+
* (management): the ones the Host root keeps for the projects linked to this Tenant
|
|
509
|
+
* (`project-credentials.json`), or new ones put through `nylorun-operate`. Every checkout linked
|
|
510
|
+
* to the Tenant shares them, so linking one never rotates another's keys. A file from before
|
|
511
|
+
* management keys keeps its application key and gains the management key.
|
|
495
512
|
*/
|
|
496
513
|
async function linkProject(ctx, started, tenantId) {
|
|
497
514
|
const { deps, projectDir, link } = ctx;
|
|
@@ -507,35 +524,26 @@ async function linkProject(ctx, started, tenantId) {
|
|
|
507
524
|
hostId: started.hostId,
|
|
508
525
|
tenantId,
|
|
509
526
|
});
|
|
527
|
+
const reaches = (key) => keyAuthenticates(deps.fetch, started.runtimeUrl, key);
|
|
510
528
|
const existing = await readProjectCredentials(projectDir);
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
adminKey: started.adminKey,
|
|
515
|
-
};
|
|
516
|
-
const keyOptions = {
|
|
517
|
-
admin,
|
|
518
|
-
runtimeUrl: started.runtimeUrl,
|
|
519
|
-
id: PROJECT_KEY_ID,
|
|
520
|
-
file: ctx.paths.projectCredentials,
|
|
521
|
-
lock: ctx.paths.keysLock,
|
|
522
|
-
};
|
|
523
|
-
let applicationKey;
|
|
524
|
-
if (existing && (await keyAuthenticates(deps.fetch, started.runtimeUrl, existing.applicationKey))) {
|
|
525
|
-
applicationKey = existing.applicationKey;
|
|
529
|
+
const kept = existing && (await reaches(existing.applicationKey)) ? existing : undefined;
|
|
530
|
+
let credentials = kept;
|
|
531
|
+
if (!kept?.management ||
|
|
526
532
|
// A project key from before operator keys becomes the one the Host root keeps.
|
|
527
|
-
|
|
528
|
-
(await readCredentialsFile(ctx.paths.projectCredentials))?.applicationKey !== applicationKey)
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
principalId:
|
|
533
|
+
(kept.principalId === PROJECT_KEY_ID &&
|
|
534
|
+
(await readCredentialsFile(ctx.paths.projectCredentials))?.applicationKey !== kept.applicationKey) ||
|
|
535
|
+
!(await reaches(kept.management.key))) {
|
|
536
|
+
const keys = await hostKey({
|
|
537
|
+
endpoint: operateEndpoint(ctx),
|
|
538
|
+
runtimeUrl: started.runtimeUrl,
|
|
539
|
+
id: PROJECT_KEY_ID,
|
|
540
|
+
file: ctx.paths.projectCredentials,
|
|
541
|
+
lock: ctx.paths.keysLock,
|
|
542
|
+
...(kept?.principalId === PROJECT_KEY_ID ? { adopt: kept } : {}),
|
|
537
543
|
});
|
|
538
|
-
|
|
544
|
+
credentials = kept ? { ...kept, management: keys.management } : keys;
|
|
545
|
+
await writeProjectCredentials(projectDir, credentials);
|
|
546
|
+
if (existing && !kept && !fresh)
|
|
539
547
|
deps.err(`Replaced the project's key, which no longer reaches Tenant ${ctx.name} (.nylorun/credentials.json).`);
|
|
540
548
|
}
|
|
541
549
|
if (!fresh)
|
|
@@ -545,7 +553,7 @@ async function linkProject(ctx, started, tenantId) {
|
|
|
545
553
|
const seeded = await seedTenant({
|
|
546
554
|
fetch: deps.fetch,
|
|
547
555
|
runtimeUrl: started.runtimeUrl,
|
|
548
|
-
|
|
556
|
+
managementKey: credentials.management.key,
|
|
549
557
|
projectRoot: projectDir,
|
|
550
558
|
});
|
|
551
559
|
const set = [...seeded.applied, ...(seeded.model ? [`model ${seeded.model}`] : [])];
|
|
@@ -556,11 +564,12 @@ async function linkProject(ctx, started, tenantId) {
|
|
|
556
564
|
deps.err(`Warning: could not seed the Tenant from .env (${error instanceof Error ? error.message : String(error)}). Set the model in Studio or with "npx @nylorun/cli configure".`);
|
|
557
565
|
}
|
|
558
566
|
}
|
|
559
|
-
const START_USAGE = "nylorun start [--tenant <name>] [--no-link] [--no-studio] [--
|
|
567
|
+
const START_USAGE = "nylorun start [--tenant <name>] [--no-link] [--no-studio] [--allow-downgrade] [--studio-embed-origin <origin>]... [--studio-embed-origin-reset] [--restate-ui]";
|
|
560
568
|
async function start(deps, args) {
|
|
561
569
|
const flags = parseStackFlags(args, {
|
|
562
570
|
booleans: [
|
|
563
571
|
"--no-studio",
|
|
572
|
+
// Accepted and ignored: start no longer opens a browser.
|
|
564
573
|
"--no-open",
|
|
565
574
|
"--no-link",
|
|
566
575
|
"--allow-downgrade",
|
|
@@ -588,7 +597,7 @@ async function start(deps, args) {
|
|
|
588
597
|
? { studioEmbedOrigins: { add: embedOrigins, reset: resetEmbed } }
|
|
589
598
|
: {}),
|
|
590
599
|
});
|
|
591
|
-
const tenant = await waitForTenant(ctx, started.
|
|
600
|
+
const tenant = await waitForTenant(ctx, started.runtimeUrl);
|
|
592
601
|
if (ctx.projectDir)
|
|
593
602
|
await linkProject({ ...ctx, projectDir: ctx.projectDir }, started, tenant.id);
|
|
594
603
|
deps.out(`Tenant ${ctx.name} (${tenant.id})`);
|
|
@@ -597,15 +606,10 @@ async function start(deps, args) {
|
|
|
597
606
|
deps.out(`Restate ${started.restateUrl} (UI and admin, unauthenticated; for debugging)`);
|
|
598
607
|
if (started.studioStarted) {
|
|
599
608
|
deps.out(`Studio ${started.studioUrl}`);
|
|
600
|
-
|
|
601
|
-
const login = await tryStudioLogin(ctx, started.studioPort, started.adminKey);
|
|
602
|
-
if (login)
|
|
603
|
-
await openLogin(ctx, withNext(login, tenantStudioPath(tenant.id)));
|
|
604
|
-
}
|
|
605
|
-
else
|
|
606
|
-
deps.err(STUDIO_SIGN_IN_HINT);
|
|
609
|
+
deps.err(STUDIO_SIGN_IN_HINT);
|
|
607
610
|
}
|
|
608
611
|
await alsoRunning(ctx);
|
|
612
|
+
deps.err(HELP_HINT);
|
|
609
613
|
return 0;
|
|
610
614
|
}
|
|
611
615
|
/** After `start`: the machine's other running Tenants and their memory, on stderr. */
|
|
@@ -673,7 +677,7 @@ async function stackStatus(ctx) {
|
|
|
673
677
|
project: ctx.project,
|
|
674
678
|
home: ctx.paths.root,
|
|
675
679
|
state: "absent",
|
|
676
|
-
runtime: { healthy: false },
|
|
680
|
+
runtime: { healthy: false, ready: false },
|
|
677
681
|
studio: { state: "absent" },
|
|
678
682
|
restate: { published: false },
|
|
679
683
|
gateway: { state: "absent", healthy: false },
|
|
@@ -691,14 +695,11 @@ async function stackStatus(ctx) {
|
|
|
691
695
|
const health = runtimeUrl ? await fetchHealth(ctx.deps, runtimeUrl) : undefined;
|
|
692
696
|
const host = await readHostConfig(ctx.paths);
|
|
693
697
|
const healthy = health?.status === "ok" && (host === undefined || health.hostId === host.hostId);
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
const answered = healthy && adminUrl
|
|
699
|
-
? await fetchAdminStatus(ctx.deps, adminUrl, await readAdminKey(ctx.paths))
|
|
698
|
+
const ready = healthy && runtimeUrl ? await fetchReady(ctx.deps, runtimeUrl) : undefined;
|
|
699
|
+
// nylorun-operate answers while the container runs, also when the Tenant cannot open.
|
|
700
|
+
const tenant = services.some((s) => s.service === "runtime" && s.state === "running")
|
|
701
|
+
? await operateStatus(ctx)
|
|
700
702
|
: undefined;
|
|
701
|
-
const tenant = answered?.tenant;
|
|
702
703
|
const studio = services.find((s) => s.service === "studio");
|
|
703
704
|
const gateway = services.find((s) => s.service === "gateway");
|
|
704
705
|
const harness = services.find((s) => s.service === "harness");
|
|
@@ -708,8 +709,8 @@ async function stackStatus(ctx) {
|
|
|
708
709
|
state: services.some((s) => s.state === "running") ? "running" : "stopped",
|
|
709
710
|
runtime: {
|
|
710
711
|
...(runtimeUrl ? { url: runtimeUrl } : {}),
|
|
711
|
-
...(persisted.adminPort && adminUrl ? { adminUrl } : {}),
|
|
712
712
|
healthy,
|
|
713
|
+
ready: ready?.ready ?? false,
|
|
713
714
|
...(health?.version ? { version: health.version } : {}),
|
|
714
715
|
...(health?.hostId ? { hostId: health.hostId } : {}),
|
|
715
716
|
},
|
|
@@ -729,10 +730,10 @@ async function stackStatus(ctx) {
|
|
|
729
730
|
healthy: isUp(services, "gateway"),
|
|
730
731
|
},
|
|
731
732
|
harness: {
|
|
732
|
-
mode:
|
|
733
|
+
mode: ready?.harness?.mode ?? harnessMode,
|
|
733
734
|
state: harness ? [harness.state, harness.health].filter(Boolean).join(", ") : "absent",
|
|
734
735
|
healthy: isUp(services, "harness"),
|
|
735
|
-
...(
|
|
736
|
+
...(ready?.harness ? { connected: ready.harness.connected } : {}),
|
|
736
737
|
},
|
|
737
738
|
services,
|
|
738
739
|
};
|
|
@@ -746,9 +747,8 @@ function describeTenant(tenant) {
|
|
|
746
747
|
const who = `${tenant.id ?? "(no id yet)"}${tenant.name ? ` (${tenant.name})` : ""}`;
|
|
747
748
|
if (tenant.state === "open")
|
|
748
749
|
return `${who} open`;
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
: `${who} unavailable (opening)`;
|
|
750
|
+
const why = [tenant.cause, tenant.message].filter(Boolean).join(": ");
|
|
751
|
+
return `${who} unavailable${why ? `: ${why}` : " (opening)"}`;
|
|
752
752
|
}
|
|
753
753
|
async function status(deps, args) {
|
|
754
754
|
const usage = "nylorun status [--tenant <name>] [--json]";
|
|
@@ -769,11 +769,9 @@ async function status(deps, args) {
|
|
|
769
769
|
if (result.tenant)
|
|
770
770
|
out(`Tenant id ${describeTenant(result.tenant)}`);
|
|
771
771
|
const runtimeDetail = result.runtime.healthy
|
|
772
|
-
? `healthy, ${result.runtime.version ?? "?"}, ${result.runtime.hostId ?? "?"}`
|
|
772
|
+
? `healthy, ${result.runtime.ready ? "ready" : "not ready"}, ${result.runtime.version ?? "?"}, ${result.runtime.hostId ?? "?"}`
|
|
773
773
|
: "not answering";
|
|
774
774
|
out(`Runtime ${result.runtime.url ?? "?"} ${runtimeDetail}`);
|
|
775
|
-
if (result.runtime.adminUrl)
|
|
776
|
-
out(`Admin API ${result.runtime.adminUrl} (operators only, never proxied)`);
|
|
777
775
|
out(`Studio ${result.studio.url ?? "?"} ${result.studio.state} (log in with "nylorun studio")`);
|
|
778
776
|
if (result.studio.embedOrigins?.length)
|
|
779
777
|
out(`Embeds ${result.studio.embedOrigins.join(" ")} (may show Studio in a frame)`);
|
|
@@ -884,7 +882,6 @@ async function ls(deps, args) {
|
|
|
884
882
|
...(persisted.studioPort ? { studioUrl: studioOrigin(persisted.studioPort) } : {}),
|
|
885
883
|
ports: {
|
|
886
884
|
...(persisted.runtimePort ? { runtime: persisted.runtimePort } : {}),
|
|
887
|
-
...(persisted.adminPort ? { admin: persisted.adminPort } : {}),
|
|
888
885
|
...(persisted.studioPort ? { studio: persisted.studioPort } : {}),
|
|
889
886
|
...(persisted.restatePort ? { restate: persisted.restatePort } : {}),
|
|
890
887
|
},
|
|
@@ -964,9 +961,6 @@ async function runningStack(ctx, options) {
|
|
|
964
961
|
name: ctx.name,
|
|
965
962
|
home: ctx.paths.root,
|
|
966
963
|
runtimeUrl,
|
|
967
|
-
adminUrl: persisted.adminPort
|
|
968
|
-
? `http://${STACK_CLIENT_HOST}:${persisted.adminPort}`
|
|
969
|
-
: runtimeUrl,
|
|
970
964
|
hostId: host.hostId,
|
|
971
965
|
adminKey,
|
|
972
966
|
studioPort: persisted.studioPort,
|
|
@@ -986,7 +980,6 @@ async function ensureSelected(ctx, options) {
|
|
|
986
980
|
name: ctx.name,
|
|
987
981
|
home: ctx.paths.root,
|
|
988
982
|
runtimeUrl: started.runtimeUrl,
|
|
989
|
-
adminUrl: started.adminUrl,
|
|
990
983
|
hostId: started.hostId,
|
|
991
984
|
adminKey: started.adminKey,
|
|
992
985
|
studioPort: started.studioPort,
|
|
@@ -1002,47 +995,53 @@ async function runningSelected(deps, options) {
|
|
|
1002
995
|
const running = await runningStack(ctx, { studio: false });
|
|
1003
996
|
if (!running)
|
|
1004
997
|
throw new CliError(`Tenant ${ctx.name} is not running. Run "nylorun start" first.`, 3);
|
|
1005
|
-
|
|
1006
|
-
if (!tenant?.id || tenant.state !== "open")
|
|
998
|
+
if ((await fetchReady(deps, running.runtimeUrl))?.tenant !== true)
|
|
1007
999
|
throw new CliError(`Tenant ${ctx.name} is not open. See "nylorun status".`, 7);
|
|
1008
1000
|
return {
|
|
1009
1001
|
ctx,
|
|
1010
|
-
|
|
1002
|
+
running: {
|
|
1011
1003
|
name: ctx.name,
|
|
1012
1004
|
runtimeUrl: running.runtimeUrl,
|
|
1013
1005
|
paths: ctx.paths,
|
|
1014
|
-
|
|
1015
|
-
adminUrl: running.adminUrl,
|
|
1016
|
-
adminKey: running.adminKey,
|
|
1006
|
+
...operateEndpoint(ctx),
|
|
1017
1007
|
},
|
|
1018
1008
|
};
|
|
1019
1009
|
}
|
|
1020
1010
|
/**
|
|
1021
|
-
* The selected Tenant's
|
|
1022
|
-
* not open.
|
|
1011
|
+
* The selected Tenant's `nylorun-operate` while it runs; exit 3 when it is not running, 7 when
|
|
1012
|
+
* it is not open.
|
|
1023
1013
|
*/
|
|
1024
|
-
export async function
|
|
1025
|
-
return (await runningSelected(deps, options)).
|
|
1014
|
+
export async function runningOperate(deps, options = {}) {
|
|
1015
|
+
return (await runningSelected(deps, options)).running;
|
|
1026
1016
|
}
|
|
1027
1017
|
/**
|
|
1028
1018
|
* The selected Tenant's API while it runs: commands that read or change the Tenant's data never
|
|
1029
1019
|
* start it. Exit 3 when it is not running, 7 when it is not open.
|
|
1030
1020
|
*/
|
|
1031
1021
|
export async function runningTenantApi(deps, options = {}) {
|
|
1032
|
-
const { ctx,
|
|
1022
|
+
const { ctx, running } = await runningSelected(deps, options);
|
|
1023
|
+
const api = (applicationKey, managementKey) => ({
|
|
1024
|
+
name: ctx.name,
|
|
1025
|
+
runtimeUrl: running.runtimeUrl,
|
|
1026
|
+
applicationKey,
|
|
1027
|
+
managementKey,
|
|
1028
|
+
});
|
|
1029
|
+
const reaches = (key) => keyAuthenticates(deps.fetch, running.runtimeUrl, key);
|
|
1033
1030
|
if (ctx.projectDir && ctx.link?.format === 3 && ctx.link.tenant === ctx.name) {
|
|
1034
1031
|
const project = await readCredentialsFile(credentialsPath(ctx.projectDir));
|
|
1035
|
-
if (project &&
|
|
1036
|
-
|
|
1032
|
+
if (project?.management &&
|
|
1033
|
+
(await reaches(project.applicationKey)) &&
|
|
1034
|
+
(await reaches(project.management.key)))
|
|
1035
|
+
return api(project.applicationKey, project.management.key);
|
|
1037
1036
|
}
|
|
1038
|
-
const
|
|
1039
|
-
|
|
1040
|
-
runtimeUrl:
|
|
1037
|
+
const keys = await hostKey({
|
|
1038
|
+
endpoint: running,
|
|
1039
|
+
runtimeUrl: running.runtimeUrl,
|
|
1041
1040
|
id: CLI_KEY_ID,
|
|
1042
1041
|
file: ctx.paths.cliCredentials,
|
|
1043
1042
|
lock: ctx.paths.keysLock,
|
|
1044
1043
|
});
|
|
1045
|
-
return
|
|
1044
|
+
return api(keys.applicationKey, keys.management.key);
|
|
1046
1045
|
}
|
|
1047
1046
|
/**
|
|
1048
1047
|
* Start the selected Tenant unless it is already running (`nylorun studio`): the
|
|
@@ -1120,7 +1119,7 @@ async function studio(deps, args, options = {}) {
|
|
|
1120
1119
|
// Studio serves the one Tenant; the link's Tenant id stands in while it is opening.
|
|
1121
1120
|
let next = options.next;
|
|
1122
1121
|
if (next === undefined) {
|
|
1123
|
-
const tenant = await
|
|
1122
|
+
const tenant = await operateStatus(ctx);
|
|
1124
1123
|
const linked = ctx.link?.tenant === ctx.name ? ctx.link.tenantId : undefined;
|
|
1125
1124
|
const tenantId = tenant?.id ?? linked;
|
|
1126
1125
|
if (tenantId)
|
|
@@ -22,17 +22,17 @@
|
|
|
22
22
|
* The combined packing (blueprint D12): the `runtime` container runs the core
|
|
23
23
|
* and loop services, and the `gateway` container runs gates: the Model Gate,
|
|
24
24
|
* which alone reads model credentials, and the Tool Gate (F4.1), which alone
|
|
25
|
-
* holds remote MCP connections and their credentials and
|
|
26
|
-
*
|
|
25
|
+
* holds remote MCP connections and their credentials and makes every HTTP tool
|
|
26
|
+
* request. Every model call, remote MCP call and HTTP tool call of the loop crosses it
|
|
27
27
|
* (NYLORUN_GATES_URL, with NYLORUN_GATES_TOKEN from `.env`, or a run's token from the
|
|
28
|
-
* harness)
|
|
28
|
+
* harness). The gateway also runs keys (F4.2): the only
|
|
29
29
|
* holder of the vault key, it runs every vault write that touches a secret and signs
|
|
30
30
|
* every token (NYLORUN_KEYS_URL). The gateway mounts only the Host's Tenant directory
|
|
31
31
|
* (`tenant/`) and its keys directory (`keys/`, the vault key), read-only: never
|
|
32
32
|
* host-credentials.json. The runtime mounts the Host root with `keys/` and `docker/`
|
|
33
33
|
* covered by empty read-only mounts, so it can read neither the vault key nor
|
|
34
34
|
* Restate's private key and `.env`. The runtime does not wait for the gateway: while
|
|
35
|
-
* it is down, model and
|
|
35
|
+
* it is down, model, MCP and HTTP tool calls fail, vault writes and
|
|
36
36
|
* token minting answer 503, and the session takes the next message.
|
|
37
37
|
*
|
|
38
38
|
* The Object store (blueprint D35) is RustFS, single node and single drive, pinned by digest,
|
|
@@ -53,14 +53,12 @@
|
|
|
53
53
|
* CONNECT with the egress token, to the hosts the sandbox spec allows). The two HTTP listeners
|
|
54
54
|
* accept that address as a Host.
|
|
55
55
|
*
|
|
56
|
-
* The harness (F6.2, `harness: "remote"`, the default) runs every agent turn
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* paths on this machine; the runtime mounts it there too). NYLORUN_HARNESS=in-process in `.env`
|
|
63
|
-
* rolls back to turns in the runtime container, without the harness service.
|
|
56
|
+
* The harness (F6.2, `harness: "remote"`, the default) runs every agent turn and workspace in
|
|
57
|
+
* its own container: the runtime image with `--service harness`, connected to core's Harness
|
|
58
|
+
* API (`ws://runtime:4200/nylorun/harness/v1`) with NYLORUN_HARNESS_TOKEN, its only
|
|
59
|
+
* credential, and calling models and remote MCP servers through the gateway with each run's
|
|
60
|
+
* token. It mounts only the Tenant's `sandboxes/` under `/harness`. NYLORUN_HARNESS=in-process
|
|
61
|
+
* in `.env` rolls back to turns in the runtime container, without the harness service.
|
|
64
62
|
*
|
|
65
63
|
* Three networks: `store` (internal: no egress) joins Postgres, s2-lite, Restate and RustFS to
|
|
66
64
|
* the runtime and the gateway; `harness` joins the harness to the runtime and the gateway only;
|