nylorun 0.7.0-beta → 0.9.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 +109 -0
- package/README.md +95 -25
- package/dist/cli.js +17 -2
- package/dist/doctor.d.ts +1 -1
- package/dist/doctor.js +10 -0
- package/dist/project/link.d.ts +13 -7
- package/dist/project/link.js +13 -2
- package/dist/sandbox/cluster-file.d.ts +47 -0
- package/dist/sandbox/cluster-file.js +55 -0
- package/dist/sandbox/commands.d.ts +41 -0
- package/dist/sandbox/commands.js +281 -0
- package/dist/sandbox/kubectl.d.ts +39 -0
- package/dist/sandbox/kubectl.js +70 -0
- package/dist/sandbox/manifests.d.ts +41 -0
- package/dist/sandbox/manifests.js +138 -0
- package/dist/sandbox/probe.d.ts +44 -0
- package/dist/sandbox/probe.js +137 -0
- package/dist/stack/commands.d.ts +71 -1
- package/dist/stack/commands.js +209 -38
- package/dist/stack/compose-file.d.ts +47 -7
- package/dist/stack/compose-file.js +223 -18
- package/dist/stack/env-file.d.ts +50 -14
- package/dist/stack/env-file.js +87 -40
- package/dist/stack/host-files.js +11 -1
- package/dist/stack/images.d.ts +8 -2
- package/dist/stack/images.js +8 -2
- package/dist/stack/index.d.ts +9 -0
- package/dist/stack/index.js +18 -1
- package/dist/stack/keys.d.ts +4 -0
- package/dist/stack/keys.js +72 -0
- package/dist/stack/mcp.d.ts +8 -0
- package/dist/stack/mcp.js +114 -0
- package/dist/stack/operator-keys.d.ts +39 -0
- package/dist/stack/operator-keys.js +146 -0
- package/dist/stack/paths.d.ts +32 -0
- package/dist/stack/paths.js +11 -0
- package/dist/stack/prepare.d.ts +12 -6
- package/dist/stack/prepare.js +24 -6
- package/dist/stack/sandbox.d.ts +4 -0
- package/dist/stack/sandbox.js +106 -0
- package/package.json +5 -5
- package/dist/project/derived-key.d.ts +0 -8
- package/dist/project/derived-key.js +0 -18
package/dist/stack/commands.js
CHANGED
|
@@ -3,8 +3,7 @@ import { mkdir, readFile, rm } from "node:fs/promises";
|
|
|
3
3
|
import { basename, resolve } from "node:path";
|
|
4
4
|
import { compareVersions, PROTOCOL_HEADER, PROTOCOL_VERSION, } from "@nylorun/core/compatibility";
|
|
5
5
|
import { CliError } from "../errors.js";
|
|
6
|
-
import {
|
|
7
|
-
import { readProjectCredentials, readProjectLink, writeProjectCredentials, writeProjectLink, } from "../project/link.js";
|
|
6
|
+
import { credentialsPath, readCredentialsFile, readProjectCredentials, readProjectLink, writeProjectCredentials, writeProjectLink, } from "../project/link.js";
|
|
8
7
|
import { findProjectRoot } from "../project/root.js";
|
|
9
8
|
import { seedTenant } from "../project/seed.js";
|
|
10
9
|
import { readTelemetry, STUDIO_ANALYTICS_ID, TELEMETRY_NOTICE, telemetryDecision, writeTelemetry, } from "../telemetry.js";
|
|
@@ -15,13 +14,28 @@ import { stackPaths } from "./paths.js";
|
|
|
15
14
|
import { prepareStack, readStackEnv } from "./prepare.js";
|
|
16
15
|
import { assertTenantName, chooseTenantName, DEFAULT_TENANT, defaultNylorunRoot, listTenants, moveStackRoots, readTenantRecord, removeOldProxy, sanitizeTenantName, tenantRoot, tenantsDir, writeTenantRecord, } from "./stacks.js";
|
|
17
16
|
import { mintStudioLogin, studioOrigin } from "./studio-login.js";
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
17
|
+
import { CLI_KEY_ID, hostKey, keyAuthenticates, PROJECT_KEY_ID, } from "./operator-keys.js";
|
|
18
|
+
export const STACK_SERVICES = [
|
|
19
|
+
"postgres",
|
|
20
|
+
"restate",
|
|
21
|
+
"s2-lite",
|
|
22
|
+
"rustfs",
|
|
23
|
+
"gateway",
|
|
24
|
+
"runtime",
|
|
25
|
+
"harness",
|
|
26
|
+
"studio",
|
|
27
|
+
];
|
|
28
|
+
const CORE_SERVICES = ["postgres", "restate", "s2-lite", "rustfs", "gateway", "runtime", "harness"];
|
|
29
|
+
/** The services `start` waits for: the harness only while it runs turns (`NYLORUN_HARNESS=remote`). */
|
|
30
|
+
function coreServices(harness) {
|
|
31
|
+
return CORE_SERVICES.filter((service) => service !== "harness" || harness === "remote");
|
|
32
|
+
}
|
|
33
|
+
export const stackUsage = ` up|start [--tenant <name>] [--no-link] [--no-studio] [--no-open] [--allow-downgrade] [--studio-embed-origin <origin>]... [--studio-embed-origin-reset] [--restate-ui]
|
|
21
34
|
start the project's Tenant, creating it and the Project link (.nylorun/link.json,
|
|
22
35
|
credentials.json) on the first run; print the Runtime and Studio URLs and open Studio
|
|
23
36
|
signed in (in a terminal). --tenant attaches to (or creates) a named Tenant; --no-link
|
|
24
|
-
starts the default Tenant (or --tenant's) without linking the current directory
|
|
37
|
+
starts the default Tenant (or --tenant's) without linking the current directory;
|
|
38
|
+
--restate-ui (or NYLORUN_RESTATE_UI=1) publishes Restate's UI on loopback for debugging
|
|
25
39
|
down|stop [--tenant <name> | --all] stop the Tenant's containers (--all: every Tenant's); keep volumes
|
|
26
40
|
status [--tenant <name>] [--json] the Tenant, its id, services, endpoints and Runtime health
|
|
27
41
|
logs [service] [--tenant <name>] [-f] [--tail <n>]
|
|
@@ -207,8 +221,8 @@ async function waitForHealth(ctx, runtimeUrl, hostId) {
|
|
|
207
221
|
await sleep(ctx.deps.pollMs ?? 500);
|
|
208
222
|
}
|
|
209
223
|
}
|
|
210
|
-
/** `/v1/admin/status`
|
|
211
|
-
async function
|
|
224
|
+
/** `/v1/admin/status` on the operator listener; undefined when it does not answer. */
|
|
225
|
+
async function fetchAdminStatus(deps, adminUrl, adminKey) {
|
|
212
226
|
if (!adminKey)
|
|
213
227
|
return undefined;
|
|
214
228
|
try {
|
|
@@ -223,20 +237,32 @@ async function fetchTenant(deps, adminUrl, adminKey) {
|
|
|
223
237
|
});
|
|
224
238
|
if (!response.ok)
|
|
225
239
|
return undefined;
|
|
226
|
-
const
|
|
240
|
+
const body = (await response.json());
|
|
241
|
+
const tenant = body.tenant;
|
|
227
242
|
if (!tenant || (tenant.state !== "open" && tenant.state !== "unavailable"))
|
|
228
243
|
return undefined;
|
|
244
|
+
const harness = body.aggregate?.harness;
|
|
229
245
|
return {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
246
|
+
tenant: {
|
|
247
|
+
id: typeof tenant.id === "string" ? tenant.id : null,
|
|
248
|
+
name: typeof tenant.name === "string" ? tenant.name : null,
|
|
249
|
+
state: tenant.state,
|
|
250
|
+
...(tenant.cause ? { cause: tenant.cause } : {}),
|
|
251
|
+
},
|
|
252
|
+
...((harness?.mode === "remote" || harness?.mode === "in-process") &&
|
|
253
|
+
typeof harness.connected === "number"
|
|
254
|
+
? { harness: { mode: harness.mode, connected: harness.connected } }
|
|
255
|
+
: {}),
|
|
234
256
|
};
|
|
235
257
|
}
|
|
236
258
|
catch {
|
|
237
259
|
return undefined;
|
|
238
260
|
}
|
|
239
261
|
}
|
|
262
|
+
/** `/v1/admin/status`'s Tenant on the operator listener; undefined when it does not answer. */
|
|
263
|
+
async function fetchTenant(deps, adminUrl, adminKey) {
|
|
264
|
+
return (await fetchAdminStatus(deps, adminUrl, adminKey))?.tenant;
|
|
265
|
+
}
|
|
240
266
|
/** Why the Tenant is unavailable, with the repair the Runtime names. */
|
|
241
267
|
function tenantCauseMessage(ctx, tenant) {
|
|
242
268
|
const cause = tenant.cause;
|
|
@@ -366,11 +392,10 @@ async function bringUp(ctx, options) {
|
|
|
366
392
|
runtimeVersion: overridden ? undefined : deps.runtimeVersion,
|
|
367
393
|
ports: deps.ports,
|
|
368
394
|
reserved: await reservedPorts(ctx),
|
|
369
|
-
...(deps.env.NYLORUN_DERIVED_PRINCIPALS?.trim()
|
|
370
|
-
? { derivedPrincipals: deps.env.NYLORUN_DERIVED_PRINCIPALS }
|
|
371
|
-
: {}),
|
|
372
395
|
...(options.studioEmbedOrigins ? { studioEmbedOrigins: options.studioEmbedOrigins } : {}),
|
|
373
396
|
...(analytics ? { studioAnalyticsId: STUDIO_ANALYTICS_ID } : {}),
|
|
397
|
+
...(options.sandboxes !== undefined ? { sandboxes: options.sandboxes } : {}),
|
|
398
|
+
...(options.restateUi !== undefined ? { restateUi: options.restateUi } : {}),
|
|
374
399
|
});
|
|
375
400
|
if (analytics && options.studio && telemetry.noticeShown === undefined) {
|
|
376
401
|
deps.err(TELEMETRY_NOTICE);
|
|
@@ -384,15 +409,24 @@ async function bringUp(ctx, options) {
|
|
|
384
409
|
deps.err(`Created Tenant ${ctx.name} under ${ctx.paths.root} (Runtime port ${prepared.env.runtimePort}, Studio port ${prepared.env.studioPort}).`);
|
|
385
410
|
const runtimeUrl = `http://${STACK_CLIENT_HOST}:${prepared.env.runtimePort}`;
|
|
386
411
|
const adminUrl = `http://${STACK_CLIENT_HOST}:${prepared.env.adminPort}`;
|
|
387
|
-
const up = await deps.docker.stream(
|
|
412
|
+
const up = await deps.docker.stream(
|
|
413
|
+
// --remove-orphans: a service this start's file no longer has (the harness after a rollback
|
|
414
|
+
// to NYLORUN_HARNESS=in-process, sandboxes after disable) is removed, not left running.
|
|
415
|
+
composeArgs(ctx, "up", "--detach", "--wait", "--wait-timeout", "300", "--remove-orphans", ...coreServices(prepared.env.harness)));
|
|
388
416
|
if (up !== 0) {
|
|
389
417
|
// A Runtime whose Tenant cannot open fails readiness; its status names the cause.
|
|
390
418
|
const tenant = await fetchTenant(deps, adminUrl, prepared.adminKey);
|
|
391
419
|
if (tenant?.cause)
|
|
392
420
|
throw new CliError(tenantCauseMessage(ctx, tenant), 7);
|
|
393
|
-
throw new CliError(`docker compose up failed (exit ${up}). See "nylorun logs runtime", "nylorun logs gateway" and "nylorun status".`, 7);
|
|
421
|
+
throw new CliError(`docker compose up failed (exit ${up}). See "nylorun logs runtime", "nylorun logs gateway", "nylorun logs harness" and "nylorun status".`, 7);
|
|
394
422
|
}
|
|
395
423
|
await waitForHealth(ctx, runtimeUrl, prepared.host.hostId);
|
|
424
|
+
// Without --wait: a cluster that is down must not fail the start; sessions refuse pods.
|
|
425
|
+
if (prepared.env.sandboxes) {
|
|
426
|
+
const sandboxes = await deps.docker.stream(composeArgs(ctx, "up", "--detach", "sandboxes"));
|
|
427
|
+
if (sandboxes !== 0)
|
|
428
|
+
deps.err(`Warning: the sandboxes service did not start. See "nylorun logs sandboxes".`);
|
|
429
|
+
}
|
|
396
430
|
let studioStarted = false;
|
|
397
431
|
if (options.studio) {
|
|
398
432
|
const studio = await deps.docker.stream(composeArgs(ctx, "up", "--detach", "--wait", "--wait-timeout", "120", "studio"));
|
|
@@ -408,6 +442,9 @@ async function bringUp(ctx, options) {
|
|
|
408
442
|
studioUrl: studioOrigin(prepared.env.studioPort),
|
|
409
443
|
studioStarted,
|
|
410
444
|
adminKey: prepared.adminKey,
|
|
445
|
+
...(prepared.env.restateUi
|
|
446
|
+
? { restateUrl: `http://${STACK_CLIENT_HOST}:${prepared.env.restatePort}` }
|
|
447
|
+
: {}),
|
|
411
448
|
};
|
|
412
449
|
}
|
|
413
450
|
const errorText = (error) => (error instanceof Error ? error.message : String(error));
|
|
@@ -447,13 +484,17 @@ async function openLogin(ctx, login) {
|
|
|
447
484
|
}
|
|
448
485
|
/**
|
|
449
486
|
* Link the project to the Tenant: `.nylorun/link.json` (format 3) and
|
|
450
|
-
* `.nylorun/credentials.json
|
|
451
|
-
*
|
|
452
|
-
*
|
|
487
|
+
* `.nylorun/credentials.json`. The link is rewritten only when it is older, or the Tenant's
|
|
488
|
+
* name, URL, Host or id changed; a new link seeds the Tenant from the project's `.env`.
|
|
489
|
+
*
|
|
490
|
+
* The credentials file is kept while its key still reaches the Tenant (one authenticated
|
|
491
|
+
* read). Otherwise it gets the operator key `project` (F9 I1): the one the Host root keeps for
|
|
492
|
+
* the projects linked to this Tenant (`project-credentials.json`), or a new one put through
|
|
493
|
+
* the Admin API. Every checkout linked to the Tenant shares it, so linking one never rotates
|
|
494
|
+
* another's key.
|
|
453
495
|
*/
|
|
454
496
|
async function linkProject(ctx, started, tenantId) {
|
|
455
497
|
const { deps, projectDir, link } = ctx;
|
|
456
|
-
const applicationKey = deriveTenantKey(started.adminKey, tenantId, PROJECT_PRINCIPAL_ID);
|
|
457
498
|
const fresh = link?.format !== 3 ||
|
|
458
499
|
link.tenant !== ctx.name ||
|
|
459
500
|
link.hostUrl !== started.runtimeUrl ||
|
|
@@ -466,14 +507,37 @@ async function linkProject(ctx, started, tenantId) {
|
|
|
466
507
|
hostId: started.hostId,
|
|
467
508
|
tenantId,
|
|
468
509
|
});
|
|
469
|
-
const
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
510
|
+
const existing = await readProjectCredentials(projectDir);
|
|
511
|
+
const admin = {
|
|
512
|
+
fetch: deps.fetch,
|
|
513
|
+
adminUrl: started.adminUrl,
|
|
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;
|
|
526
|
+
// A project key from before operator keys becomes the one the Host root keeps.
|
|
527
|
+
if (existing.principalId === PROJECT_KEY_ID &&
|
|
528
|
+
(await readCredentialsFile(ctx.paths.projectCredentials))?.applicationKey !== applicationKey)
|
|
529
|
+
await hostKey({ ...keyOptions, adopt: existing });
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
const key = await hostKey(keyOptions);
|
|
533
|
+
applicationKey = key.applicationKey;
|
|
473
534
|
await writeProjectCredentials(projectDir, {
|
|
474
|
-
applicationKey,
|
|
475
|
-
principalId:
|
|
535
|
+
applicationKey: key.applicationKey,
|
|
536
|
+
principalId: key.principalId,
|
|
476
537
|
});
|
|
538
|
+
if (existing && !fresh)
|
|
539
|
+
deps.err(`Replaced the project's key, which no longer reaches Tenant ${ctx.name} (.nylorun/credentials.json).`);
|
|
540
|
+
}
|
|
477
541
|
if (!fresh)
|
|
478
542
|
return;
|
|
479
543
|
deps.err(`Linked ${projectDir} to Tenant ${ctx.name} (.nylorun/link.json, .nylorun/credentials.json).`);
|
|
@@ -492,7 +556,7 @@ async function linkProject(ctx, started, tenantId) {
|
|
|
492
556
|
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".`);
|
|
493
557
|
}
|
|
494
558
|
}
|
|
495
|
-
const START_USAGE = "nylorun start [--tenant <name>] [--no-link] [--no-studio] [--no-open] [--allow-downgrade] [--studio-embed-origin <origin>]... [--studio-embed-origin-reset]";
|
|
559
|
+
const START_USAGE = "nylorun start [--tenant <name>] [--no-link] [--no-studio] [--no-open] [--allow-downgrade] [--studio-embed-origin <origin>]... [--studio-embed-origin-reset] [--restate-ui]";
|
|
496
560
|
async function start(deps, args) {
|
|
497
561
|
const flags = parseStackFlags(args, {
|
|
498
562
|
booleans: [
|
|
@@ -501,6 +565,7 @@ async function start(deps, args) {
|
|
|
501
565
|
"--no-link",
|
|
502
566
|
"--allow-downgrade",
|
|
503
567
|
"--studio-embed-origin-reset",
|
|
568
|
+
"--restate-ui",
|
|
504
569
|
],
|
|
505
570
|
values: ["--tenant"],
|
|
506
571
|
lists: ["--studio-embed-origin"],
|
|
@@ -518,6 +583,7 @@ async function start(deps, args) {
|
|
|
518
583
|
const started = await bringUp(ctx, {
|
|
519
584
|
studio: !flags.booleans.has("--no-studio"),
|
|
520
585
|
allowDowngrade: flags.booleans.has("--allow-downgrade"),
|
|
586
|
+
restateUi: flags.booleans.has("--restate-ui") || deps.env.NYLORUN_RESTATE_UI?.trim() === "1",
|
|
521
587
|
...(embedOrigins.length || resetEmbed
|
|
522
588
|
? { studioEmbedOrigins: { add: embedOrigins, reset: resetEmbed } }
|
|
523
589
|
: {}),
|
|
@@ -527,6 +593,8 @@ async function start(deps, args) {
|
|
|
527
593
|
await linkProject({ ...ctx, projectDir: ctx.projectDir }, started, tenant.id);
|
|
528
594
|
deps.out(`Tenant ${ctx.name} (${tenant.id})`);
|
|
529
595
|
deps.out(`Runtime ${started.runtimeUrl}`);
|
|
596
|
+
if (started.restateUrl)
|
|
597
|
+
deps.out(`Restate ${started.restateUrl} (UI and admin, unauthenticated; for debugging)`);
|
|
530
598
|
if (started.studioStarted) {
|
|
531
599
|
deps.out(`Studio ${started.studioUrl}`);
|
|
532
600
|
if (opensBrowser(deps, flags)) {
|
|
@@ -607,8 +675,9 @@ async function stackStatus(ctx) {
|
|
|
607
675
|
state: "absent",
|
|
608
676
|
runtime: { healthy: false },
|
|
609
677
|
studio: { state: "absent" },
|
|
610
|
-
restate: {},
|
|
678
|
+
restate: { published: false },
|
|
611
679
|
gateway: { state: "absent", healthy: false },
|
|
680
|
+
harness: { mode: "remote", state: "absent", healthy: false },
|
|
612
681
|
services: [],
|
|
613
682
|
};
|
|
614
683
|
if (!existsSync(ctx.paths.compose) || !existsSync(ctx.paths.env))
|
|
@@ -626,11 +695,14 @@ async function stackStatus(ctx) {
|
|
|
626
695
|
const adminUrl = persisted.adminPort
|
|
627
696
|
? `http://${STACK_CLIENT_HOST}:${persisted.adminPort}`
|
|
628
697
|
: runtimeUrl;
|
|
629
|
-
const
|
|
630
|
-
? await
|
|
698
|
+
const answered = healthy && adminUrl
|
|
699
|
+
? await fetchAdminStatus(ctx.deps, adminUrl, await readAdminKey(ctx.paths))
|
|
631
700
|
: undefined;
|
|
701
|
+
const tenant = answered?.tenant;
|
|
632
702
|
const studio = services.find((s) => s.service === "studio");
|
|
633
703
|
const gateway = services.find((s) => s.service === "gateway");
|
|
704
|
+
const harness = services.find((s) => s.service === "harness");
|
|
705
|
+
const harnessMode = persisted.harness ?? "remote";
|
|
634
706
|
return {
|
|
635
707
|
...base,
|
|
636
708
|
state: services.some((s) => s.state === "running") ? "running" : "stopped",
|
|
@@ -649,13 +721,19 @@ async function stackStatus(ctx) {
|
|
|
649
721
|
? { embedOrigins: persisted.studioFrameAncestors }
|
|
650
722
|
: {}),
|
|
651
723
|
},
|
|
652
|
-
restate: persisted.restatePort
|
|
653
|
-
? { url: `http://${STACK_CLIENT_HOST}:${persisted.restatePort}
|
|
654
|
-
: {},
|
|
724
|
+
restate: persisted.restatePort && persisted.restateUi
|
|
725
|
+
? { url: `http://${STACK_CLIENT_HOST}:${persisted.restatePort}`, published: true }
|
|
726
|
+
: { published: false },
|
|
655
727
|
gateway: {
|
|
656
728
|
state: gateway ? [gateway.state, gateway.health].filter(Boolean).join(", ") : "absent",
|
|
657
729
|
healthy: isUp(services, "gateway"),
|
|
658
730
|
},
|
|
731
|
+
harness: {
|
|
732
|
+
mode: answered?.harness?.mode ?? harnessMode,
|
|
733
|
+
state: harness ? [harness.state, harness.health].filter(Boolean).join(", ") : "absent",
|
|
734
|
+
healthy: isUp(services, "harness"),
|
|
735
|
+
...(answered?.harness ? { connected: answered.harness.connected } : {}),
|
|
736
|
+
},
|
|
659
737
|
services,
|
|
660
738
|
};
|
|
661
739
|
}
|
|
@@ -700,8 +778,15 @@ async function status(deps, args) {
|
|
|
700
778
|
if (result.studio.embedOrigins?.length)
|
|
701
779
|
out(`Embeds ${result.studio.embedOrigins.join(" ")} (may show Studio in a frame)`);
|
|
702
780
|
out(`Gateway ${result.gateway.state} (the Model Gate; model calls fail while it is down)`);
|
|
703
|
-
|
|
704
|
-
|
|
781
|
+
out(result.harness.mode === "remote"
|
|
782
|
+
? `Harness ${result.harness.state}, remote${result.harness.connected === undefined ? "" : `, ${result.harness.connected} connected`} (agent turns, MCP servers and workspaces; "nylorun logs harness")`
|
|
783
|
+
: `Harness in-process (NYLORUN_HARNESS=in-process in .env: turns run in the runtime container)`);
|
|
784
|
+
out(result.restate.url
|
|
785
|
+
? `Restate UI ${result.restate.url} (unauthenticated; published by --restate-ui)`
|
|
786
|
+
: `Restate UI not published (nylorun start --restate-ui)`);
|
|
787
|
+
const sandboxes = result.services.find((s) => s.service === "sandboxes");
|
|
788
|
+
if (sandboxes)
|
|
789
|
+
out(`Sandboxes ${[sandboxes.state, sandboxes.health].filter(Boolean).join(", ")} (see "nylorun sandbox status")`);
|
|
705
790
|
out(`Services ${STACK_SERVICES.map((name) => {
|
|
706
791
|
const service = result.services.find((s) => s.service === name);
|
|
707
792
|
const state = service
|
|
@@ -718,7 +803,7 @@ async function logs(deps, args) {
|
|
|
718
803
|
if (flags.rest.length > 1)
|
|
719
804
|
throw usageError(`Usage: ${usage}`);
|
|
720
805
|
const service = flags.rest[0];
|
|
721
|
-
if (service !== undefined && !STACK_SERVICES.includes(service))
|
|
806
|
+
if (service !== undefined && ![...STACK_SERVICES, "sandboxes"].includes(service))
|
|
722
807
|
throw usageError(`Unknown service ${service}. Usage: ${usage}`);
|
|
723
808
|
const tail = flags.values.get("--tail");
|
|
724
809
|
if (tail !== undefined && !/^\d+$/.test(tail))
|
|
@@ -865,6 +950,9 @@ async function runningStack(ctx, options) {
|
|
|
865
950
|
const services = await composePs(ctx);
|
|
866
951
|
if (!isUp(services, "runtime") || !isUp(services, "gateway"))
|
|
867
952
|
return undefined;
|
|
953
|
+
// An older Tenant without the harness container is brought up again (remote is the default).
|
|
954
|
+
if ((persisted.harness ?? "remote") === "remote" && !isUp(services, "harness"))
|
|
955
|
+
return undefined;
|
|
868
956
|
const studioUp = isUp(services, "studio");
|
|
869
957
|
if (options.studio && !studioUp)
|
|
870
958
|
return undefined;
|
|
@@ -907,6 +995,55 @@ async function ensureSelected(ctx, options) {
|
|
|
907
995
|
started: !runtimeWasUp,
|
|
908
996
|
};
|
|
909
997
|
}
|
|
998
|
+
async function runningSelected(deps, options) {
|
|
999
|
+
const ctx = await selectStack(deps, options);
|
|
1000
|
+
requireStackFiles(ctx);
|
|
1001
|
+
await dockerPreflight(deps.docker);
|
|
1002
|
+
const running = await runningStack(ctx, { studio: false });
|
|
1003
|
+
if (!running)
|
|
1004
|
+
throw new CliError(`Tenant ${ctx.name} is not running. Run "nylorun start" first.`, 3);
|
|
1005
|
+
const tenant = await fetchTenant(deps, running.adminUrl, running.adminKey);
|
|
1006
|
+
if (!tenant?.id || tenant.state !== "open")
|
|
1007
|
+
throw new CliError(`Tenant ${ctx.name} is not open. See "nylorun status".`, 7);
|
|
1008
|
+
return {
|
|
1009
|
+
ctx,
|
|
1010
|
+
admin: {
|
|
1011
|
+
name: ctx.name,
|
|
1012
|
+
runtimeUrl: running.runtimeUrl,
|
|
1013
|
+
paths: ctx.paths,
|
|
1014
|
+
fetch: deps.fetch,
|
|
1015
|
+
adminUrl: running.adminUrl,
|
|
1016
|
+
adminKey: running.adminKey,
|
|
1017
|
+
},
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* The selected Tenant's Admin API while it runs; exit 3 when it is not running, 7 when it is
|
|
1022
|
+
* not open.
|
|
1023
|
+
*/
|
|
1024
|
+
export async function runningAdmin(deps, options = {}) {
|
|
1025
|
+
return (await runningSelected(deps, options)).admin;
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* The selected Tenant's API while it runs: commands that read or change the Tenant's data never
|
|
1029
|
+
* start it. Exit 3 when it is not running, 7 when it is not open.
|
|
1030
|
+
*/
|
|
1031
|
+
export async function runningTenantApi(deps, options = {}) {
|
|
1032
|
+
const { ctx, admin } = await runningSelected(deps, options);
|
|
1033
|
+
if (ctx.projectDir && ctx.link?.format === 3 && ctx.link.tenant === ctx.name) {
|
|
1034
|
+
const project = await readCredentialsFile(credentialsPath(ctx.projectDir));
|
|
1035
|
+
if (project && (await keyAuthenticates(deps.fetch, admin.runtimeUrl, project.applicationKey)))
|
|
1036
|
+
return { name: ctx.name, runtimeUrl: admin.runtimeUrl, applicationKey: project.applicationKey };
|
|
1037
|
+
}
|
|
1038
|
+
const key = await hostKey({
|
|
1039
|
+
admin,
|
|
1040
|
+
runtimeUrl: admin.runtimeUrl,
|
|
1041
|
+
id: CLI_KEY_ID,
|
|
1042
|
+
file: ctx.paths.cliCredentials,
|
|
1043
|
+
lock: ctx.paths.keysLock,
|
|
1044
|
+
});
|
|
1045
|
+
return { name: ctx.name, runtimeUrl: admin.runtimeUrl, applicationKey: key.applicationKey };
|
|
1046
|
+
}
|
|
910
1047
|
/**
|
|
911
1048
|
* Start the selected Tenant unless it is already running (`nylorun studio`): the
|
|
912
1049
|
* `start` code path without its output, its Tenant wait or the Project link. Compose
|
|
@@ -915,6 +1052,40 @@ async function ensureSelected(ctx, options) {
|
|
|
915
1052
|
export async function ensureStack(deps, options) {
|
|
916
1053
|
return await ensureSelected(await selectStack(deps, options.name === undefined ? {} : { name: options.name }), options);
|
|
917
1054
|
}
|
|
1055
|
+
/** The Tenant `nylorun sandbox` acts on; it must have been started once. */
|
|
1056
|
+
export async function selectTenant(deps, options = {}) {
|
|
1057
|
+
await tidyMachine(deps);
|
|
1058
|
+
const ctx = await selectStack(deps, options);
|
|
1059
|
+
requireStackFiles(ctx);
|
|
1060
|
+
return {
|
|
1061
|
+
name: ctx.name,
|
|
1062
|
+
paths: ctx.paths,
|
|
1063
|
+
project: ctx.project,
|
|
1064
|
+
compose: (...args) => composeArgs(ctx, ...args),
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
/**
|
|
1068
|
+
* Rewrite the Tenant's Compose files with `sandboxes` set (an object) or removed (null),
|
|
1069
|
+
* and bring it up again: Compose recreates the containers whose settings changed.
|
|
1070
|
+
*/
|
|
1071
|
+
export async function restartTenant(deps, options) {
|
|
1072
|
+
const ctx = await selectStack(deps, options.name === undefined ? {} : { name: options.name });
|
|
1073
|
+
await dockerPreflight(deps.docker);
|
|
1074
|
+
await bringUp(ctx, { studio: false, sandboxes: options.sandboxes });
|
|
1075
|
+
}
|
|
1076
|
+
/** Ports every Tenant on this machine keeps for sandboxes, but the one under `except`. */
|
|
1077
|
+
export async function reservedSandboxPorts(deps, except) {
|
|
1078
|
+
const reserved = await tenantPorts(nylorunRoot(deps), except);
|
|
1079
|
+
for (const tenant of await listTenants(nylorunRoot(deps))) {
|
|
1080
|
+
if (resolve(tenant.root) === except)
|
|
1081
|
+
continue;
|
|
1082
|
+
const sandboxes = (await readStackEnv(stackPaths(tenant.root)))?.sandboxes;
|
|
1083
|
+
for (const port of [sandboxes?.harnessPort, sandboxes?.gatesPort, sandboxes?.egressPort])
|
|
1084
|
+
if (port !== undefined)
|
|
1085
|
+
reserved.add(port);
|
|
1086
|
+
}
|
|
1087
|
+
return reserved;
|
|
1088
|
+
}
|
|
918
1089
|
/** Add Studio's `next` path (e.g. `/tenants/<id>`) to a login URL. */
|
|
919
1090
|
export function withNext(loginUrl, next) {
|
|
920
1091
|
if (!next)
|
|
@@ -5,16 +5,15 @@
|
|
|
5
5
|
* network and ports. Container, network and volume names are global on the Docker engine, so
|
|
6
6
|
* every one is `<project>-<role>` (`nylorun-shop-studio`, network `nylorun-shop`, volume
|
|
7
7
|
* `nylorun-shop-postgres`), labelled `dev.nylorun.tenant: <name>`; they are written here, from
|
|
8
|
-
* the project and the Tenant's name. Everything else that varies (ports, the Postgres password
|
|
9
|
-
* UID/GID, the Host root, the images) comes from `docker/.env`.
|
|
8
|
+
* the project and the Tenant's name. Everything else that varies (ports, the Postgres password and the other
|
|
9
|
+
* secrets, UID/GID, the Host root, the images) comes from `docker/.env`.
|
|
10
10
|
*
|
|
11
11
|
* The Tenant's state is its Postgres database, executed through Restate, with its
|
|
12
12
|
* history in s2-lite; the Runtime's /ready checks all three. Postgres initialises the
|
|
13
13
|
* database with C collation (`--locale=C`) and runs with `wal_level=logical`: the stream
|
|
14
14
|
* relay feeds s2-lite from the record over logical replication (Durable Streams), and
|
|
15
15
|
* `max_slot_wal_keep_size` caps the WAL a stuck relay can hold. The Runtime creates the
|
|
16
|
-
* Tenant on its first start, with its name (`NYLORUN_TENANT_NAME`)
|
|
17
|
-
* principals of `NYLORUN_DERIVED_PRINCIPALS` (`project` for the Project link).
|
|
16
|
+
* Tenant on its first start, with its name (`NYLORUN_TENANT_NAME`).
|
|
18
17
|
*
|
|
19
18
|
* Restate signs requests to the Worker endpoint with the private key in
|
|
20
19
|
* `docker/restate-identity.pem`, mounted read-only; the Runtime gets the public
|
|
@@ -25,8 +24,8 @@
|
|
|
25
24
|
* which alone reads model credentials, and the Tool Gate (F4.1), which alone
|
|
26
25
|
* holds remote MCP connections and their credentials and POSTs every Action
|
|
27
26
|
* delivery. Every model call, remote MCP call and delivery of the loop crosses it
|
|
28
|
-
* (NYLORUN_GATES_URL, with NYLORUN_GATES_TOKEN from `.env
|
|
29
|
-
*
|
|
27
|
+
* (NYLORUN_GATES_URL, with NYLORUN_GATES_TOKEN from `.env`, or a run's token from the
|
|
28
|
+
* harness); MCP stdio servers run in the harness container. The gateway also runs keys (F4.2): the only
|
|
30
29
|
* holder of the vault key, it runs every vault write that touches a secret and signs
|
|
31
30
|
* every token (NYLORUN_KEYS_URL). The gateway mounts only the Host's Tenant directory
|
|
32
31
|
* (`tenant/`) and its keys directory (`keys/`, the vault key), read-only: never
|
|
@@ -35,5 +34,46 @@
|
|
|
35
34
|
* Restate's private key and `.env`. The runtime does not wait for the gateway: while
|
|
36
35
|
* it is down, model and MCP calls fail, deliveries are retried, vault writes and
|
|
37
36
|
* token minting answer 503, and the session takes the next message.
|
|
37
|
+
*
|
|
38
|
+
* The Object store (blueprint D35) is RustFS, single node and single drive, pinned by digest,
|
|
39
|
+
* on the `rustfs` volume; it is not published. Its credential is the access key `nylorun` and
|
|
40
|
+
* the secret key NYLORUN_OBJECT_STORE_SECRET_KEY from `.env`, and only the runtime and the
|
|
41
|
+
* gateway receive it (NYLORUN_OBJECT_STORE_*). The runtime creates the bucket at boot and
|
|
42
|
+
* reaches the store through its `s3` BlobStore.
|
|
43
|
+
*
|
|
44
|
+
* With `sandboxes` (after `nylorun sandbox enable`, F7.2) the `sandboxes` service runs the
|
|
45
|
+
* agent-sandbox driver: the only container holding the cluster credentials
|
|
46
|
+
* (`<Host root>/sandboxes`, read-only), not published, reached by the runtime alone with
|
|
47
|
+
* NYLORUN_SANDBOXES_TOKEN. The runtime gets an empty read-only mount over that directory.
|
|
48
|
+
* Sandbox pods reach the stack only on the Docker host's address (NYLORUN_SANDBOX_HOST_ADDRESS),
|
|
49
|
+
* where three ports are published on NYLORUN_SANDBOX_BIND: the runtime's Harness API listener
|
|
50
|
+
* (NYLORUN_SANDBOX_HARNESS_PORT → 4200: pods join and connect with host tokens), the
|
|
51
|
+
* gateway's gates (NYLORUN_SANDBOX_GATES_PORT → 4100: model and MCP calls with run tokens) and
|
|
52
|
+
* egress-gate (NYLORUN_SANDBOX_EGRESS_PORT → 4200 in the gateway, `--service gates,keys,egress`:
|
|
53
|
+
* CONNECT with the egress token, to the hosts the sandbox spec allows). The two HTTP listeners
|
|
54
|
+
* accept that address as a Host.
|
|
55
|
+
*
|
|
56
|
+
* The harness (F6.2, `harness: "remote"`, the default) runs every agent turn, MCP stdio server
|
|
57
|
+
* and workspace in its own container: the runtime image with `--service harness`, connected to
|
|
58
|
+
* core's Harness API (`ws://runtime:4200/nylorun/harness/v1`) with NYLORUN_HARNESS_TOKEN, its
|
|
59
|
+
* only credential, and calling models and remote MCP servers through the gateway with each
|
|
60
|
+
* run's token. It mounts only the Tenant's `sandboxes/`, `plugin-data/`, `home/` and `tmp/`
|
|
61
|
+
* under `/harness`, and the Host root's `plugins/` read-only at its own path (plugin roots name
|
|
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.
|
|
64
|
+
*
|
|
65
|
+
* Three networks: `store` (internal: no egress) joins Postgres, s2-lite, Restate and RustFS to
|
|
66
|
+
* the runtime and the gateway; `harness` joins the harness to the runtime and the gateway only;
|
|
67
|
+
* `default` carries egress and the published ports (runtime, gateway, Studio, sandboxes).
|
|
68
|
+
* Restate's admin port (its UI, unauthenticated) is published, and Restate joins `default`,
|
|
69
|
+
* only with `restateUi` (`nylorun start --restate-ui`).
|
|
70
|
+
*
|
|
71
|
+
* With `identity` (when `<Host root>/identity.yaml` exists) the runtime reads its trusted issuers
|
|
72
|
+
* from it, through the Host root mount (NYLORUN_IDENTITY_FILE=/nylorun/identity.yaml).
|
|
38
73
|
*/
|
|
39
|
-
export declare function renderComposeFile(project: string, name: string
|
|
74
|
+
export declare function renderComposeFile(project: string, name: string, options?: {
|
|
75
|
+
sandboxes?: true;
|
|
76
|
+
harness?: "remote" | "in-process";
|
|
77
|
+
restateUi?: true;
|
|
78
|
+
identity?: true;
|
|
79
|
+
}): string;
|