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/env-file.js
CHANGED
|
@@ -1,21 +1,30 @@
|
|
|
1
|
-
import { DERIVED_PRINCIPAL_ID_PATTERN } from "@nylorun/core/compatibility";
|
|
2
1
|
import { parseFrameAncestors } from "@nylorun/core/contracts";
|
|
3
2
|
import { CliError } from "../errors.js";
|
|
4
3
|
/**
|
|
5
|
-
* Origins that may embed Studio by default:
|
|
6
|
-
*
|
|
4
|
+
* Origins that may embed Studio by default: none. Embedding is opt-in: the operator lists
|
|
5
|
+
* the exact origins of the app that frames Studio (`--studio-embed-origin`, Studio §8.9).
|
|
7
6
|
*/
|
|
8
|
-
export const DEFAULT_STUDIO_FRAME_ANCESTORS = [
|
|
9
|
-
|
|
10
|
-
"
|
|
11
|
-
|
|
7
|
+
export const DEFAULT_STUDIO_FRAME_ANCESTORS = [];
|
|
8
|
+
const SANDBOX_KEYS = {
|
|
9
|
+
token: "NYLORUN_SANDBOXES_TOKEN",
|
|
10
|
+
image: "NYLORUN_SANDBOXES_IMAGE",
|
|
11
|
+
harnessPort: "NYLORUN_SANDBOX_HARNESS_PORT",
|
|
12
|
+
gatesPort: "NYLORUN_SANDBOX_GATES_PORT",
|
|
13
|
+
egressPort: "NYLORUN_SANDBOX_EGRESS_PORT",
|
|
14
|
+
hostAddress: "NYLORUN_SANDBOX_HOST_ADDRESS",
|
|
15
|
+
bind: "NYLORUN_SANDBOX_BIND",
|
|
16
|
+
};
|
|
12
17
|
const KEYS = {
|
|
13
18
|
runtimePort: "NYLORUN_PORT",
|
|
14
19
|
adminPort: "NYLORUN_ADMIN_PORT",
|
|
15
20
|
studioPort: "NYLORUN_STUDIO_PORT",
|
|
16
21
|
restatePort: "NYLORUN_RESTATE_PORT",
|
|
22
|
+
restateUi: "NYLORUN_RESTATE_UI",
|
|
17
23
|
postgresPassword: "NYLORUN_POSTGRES_PASSWORD",
|
|
18
24
|
gatesToken: "NYLORUN_GATES_TOKEN",
|
|
25
|
+
harnessToken: "NYLORUN_HARNESS_TOKEN",
|
|
26
|
+
harness: "NYLORUN_HARNESS",
|
|
27
|
+
objectStoreSecretKey: "NYLORUN_OBJECT_STORE_SECRET_KEY",
|
|
19
28
|
restateIdentityKey: "NYLORUN_RESTATE_IDENTITY_KEY",
|
|
20
29
|
uid: "NYLORUN_UID",
|
|
21
30
|
gid: "NYLORUN_GID",
|
|
@@ -25,7 +34,6 @@ const KEYS = {
|
|
|
25
34
|
studioFrameAncestors: "NYLORUN_STUDIO_FRAME_ANCESTORS",
|
|
26
35
|
studioAnalyticsId: "NYLORUN_STUDIO_ANALYTICS_ID",
|
|
27
36
|
tenantName: "NYLORUN_TENANT_NAME",
|
|
28
|
-
derivedPrincipals: "NYLORUN_DERIVED_PRINCIPALS",
|
|
29
37
|
};
|
|
30
38
|
/** Compose .env values: single quotes keep a value literal (no interpolation). */
|
|
31
39
|
function quote(key, value) {
|
|
@@ -42,17 +50,25 @@ function unquote(value) {
|
|
|
42
50
|
return trimmed;
|
|
43
51
|
}
|
|
44
52
|
export function renderEnvFile(env) {
|
|
45
|
-
const line = (field) =>
|
|
53
|
+
const line = (field) => {
|
|
54
|
+
const value = env[field];
|
|
55
|
+
return `${KEYS[field]}=${quote(KEYS[field], typeof value === "boolean" ? (value ? "1" : "0") : String(value))}`;
|
|
56
|
+
};
|
|
57
|
+
const sandboxes = env.sandboxes;
|
|
58
|
+
const sandboxLine = (field) => `${SANDBOX_KEYS[field]}=${quote(SANDBOX_KEYS[field], String(sandboxes[field]))}`;
|
|
46
59
|
return [
|
|
47
|
-
"# Written by `nylorun start`. Mode 0600: holds the Postgres password
|
|
48
|
-
"#
|
|
49
|
-
"# images, UID/GID and the Host root are refreshed on every start.",
|
|
60
|
+
"# Written by `nylorun start`. Mode 0600: holds the Postgres password, the gates",
|
|
61
|
+
"# token and the Object store's secret key. Ports and secrets are kept across",
|
|
62
|
+
"# starts; images, UID/GID and the Host root are refreshed on every start.",
|
|
50
63
|
"",
|
|
51
64
|
"# Published on 127.0.0.1; clients use http://localhost:<port>.",
|
|
52
65
|
line("runtimePort"),
|
|
53
66
|
line("adminPort"),
|
|
54
67
|
line("studioPort"),
|
|
68
|
+
"# Restate's UI and admin (unauthenticated) are published only with",
|
|
69
|
+
"# nylorun start --restate-ui (or NYLORUN_RESTATE_UI=1); decided on every start.",
|
|
55
70
|
line("restatePort"),
|
|
71
|
+
line("restateUi"),
|
|
56
72
|
"",
|
|
57
73
|
line("postgresPassword"),
|
|
58
74
|
"",
|
|
@@ -60,6 +76,16 @@ export function renderEnvFile(env) {
|
|
|
60
76
|
"# model call.",
|
|
61
77
|
line("gatesToken"),
|
|
62
78
|
"",
|
|
79
|
+
"# The harness container presents this token to the runtime's Harness API; it",
|
|
80
|
+
"# holds no other credential. NYLORUN_HARNESS=in-process runs agent turns, MCP",
|
|
81
|
+
"# servers and workspaces in the runtime container instead (rollback); kept.",
|
|
82
|
+
line("harnessToken"),
|
|
83
|
+
line("harness"),
|
|
84
|
+
"",
|
|
85
|
+
"# RustFS (the Object store) accepts this secret key; only the runtime and the",
|
|
86
|
+
"# gateway receive it.",
|
|
87
|
+
line("objectStoreSecretKey"),
|
|
88
|
+
"",
|
|
63
89
|
"# Restate signs requests to the Runtime's Worker endpoint with the private",
|
|
64
90
|
"# key in restate-identity.pem; the Runtime accepts only this public key.",
|
|
65
91
|
line("restateIdentityKey"),
|
|
@@ -73,8 +99,8 @@ export function renderEnvFile(env) {
|
|
|
73
99
|
line("runtimeImage"),
|
|
74
100
|
line("studioImage"),
|
|
75
101
|
"",
|
|
76
|
-
"# Exact origins that may show Studio in a frame
|
|
77
|
-
"# starts;
|
|
102
|
+
"# Exact origins that may show Studio in a frame; none by default. Kept across",
|
|
103
|
+
"# starts; add with nylorun start --studio-embed-origin <origin>.",
|
|
78
104
|
line("studioFrameAncestors"),
|
|
79
105
|
"",
|
|
80
106
|
"# Studio's anonymous usage analytics; empty when off. Set on every start: turn",
|
|
@@ -82,27 +108,25 @@ export function renderEnvFile(env) {
|
|
|
82
108
|
line("studioAnalyticsId"),
|
|
83
109
|
"",
|
|
84
110
|
"# The Tenant's name, which the Runtime gives the Tenant it creates on the first",
|
|
85
|
-
"# start
|
|
86
|
-
"# from the admin key; `project` is the Project link's).",
|
|
111
|
+
"# start.",
|
|
87
112
|
line("tenantName"),
|
|
88
|
-
line("derivedPrincipals"),
|
|
89
113
|
"",
|
|
114
|
+
...(sandboxes
|
|
115
|
+
? [
|
|
116
|
+
"# Sandboxes (nylorun sandbox enable): the runtime's token for the sandboxes",
|
|
117
|
+
"# service, its image, and the host ports pods reach on the host address.",
|
|
118
|
+
sandboxLine("token"),
|
|
119
|
+
sandboxLine("image"),
|
|
120
|
+
sandboxLine("harnessPort"),
|
|
121
|
+
sandboxLine("gatesPort"),
|
|
122
|
+
sandboxLine("egressPort"),
|
|
123
|
+
sandboxLine("hostAddress"),
|
|
124
|
+
sandboxLine("bind"),
|
|
125
|
+
"",
|
|
126
|
+
]
|
|
127
|
+
: []),
|
|
90
128
|
].join("\n");
|
|
91
129
|
}
|
|
92
|
-
/**
|
|
93
|
-
* `NYLORUN_DERIVED_PRINCIPALS`: comma-separated principal ids, as the Runtime accepts them
|
|
94
|
-
* (not `studio`), with `project` first so the Project link's key always works.
|
|
95
|
-
*/
|
|
96
|
-
export function parseDerivedPrincipals(raw, source) {
|
|
97
|
-
const ids = raw
|
|
98
|
-
.split(",")
|
|
99
|
-
.map((entry) => entry.trim())
|
|
100
|
-
.filter((entry) => entry !== "");
|
|
101
|
-
for (const id of ids)
|
|
102
|
-
if (!DERIVED_PRINCIPAL_ID_PATTERN.test(id) || id === "studio")
|
|
103
|
-
throw new CliError(`${source} has '${id}': each entry must match ${DERIVED_PRINCIPAL_ID_PATTERN} and not be studio.`, 2);
|
|
104
|
-
return [...new Set(["project", ...ids])];
|
|
105
|
-
}
|
|
106
130
|
/** Parse KEY=value lines; comments and blank lines are skipped. */
|
|
107
131
|
export function parseEnvLines(text) {
|
|
108
132
|
const values = new Map();
|
|
@@ -123,6 +147,24 @@ function port(value) {
|
|
|
123
147
|
const n = Number(value);
|
|
124
148
|
return n >= 1 && n <= 65535 ? n : undefined;
|
|
125
149
|
}
|
|
150
|
+
const IPV4 = /^(?:(?:25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|1?\d?\d)$/;
|
|
151
|
+
/** The sandboxes section, when complete and valid. */
|
|
152
|
+
function parseSandboxes(values) {
|
|
153
|
+
const token = values.get(SANDBOX_KEYS.token);
|
|
154
|
+
const image = values.get(SANDBOX_KEYS.image);
|
|
155
|
+
const harnessPort = port(values.get(SANDBOX_KEYS.harnessPort));
|
|
156
|
+
const gatesPort = port(values.get(SANDBOX_KEYS.gatesPort));
|
|
157
|
+
const egressPort = port(values.get(SANDBOX_KEYS.egressPort));
|
|
158
|
+
const hostAddress = values.get(SANDBOX_KEYS.hostAddress);
|
|
159
|
+
const bind = values.get(SANDBOX_KEYS.bind);
|
|
160
|
+
if (!token || !/^[0-9a-f]{64,}$/i.test(token) || !image)
|
|
161
|
+
return undefined;
|
|
162
|
+
if (!harnessPort || !gatesPort || !egressPort)
|
|
163
|
+
return undefined;
|
|
164
|
+
if (!hostAddress || !IPV4.test(hostAddress) || !bind || !IPV4.test(bind))
|
|
165
|
+
return undefined;
|
|
166
|
+
return { token, image, harnessPort, gatesPort, egressPort, hostAddress, bind };
|
|
167
|
+
}
|
|
126
168
|
export function parsePersisted(text) {
|
|
127
169
|
const values = parseEnvLines(text);
|
|
128
170
|
const out = {};
|
|
@@ -138,21 +180,26 @@ export function parsePersisted(text) {
|
|
|
138
180
|
const restatePort = port(values.get(KEYS.restatePort));
|
|
139
181
|
if (restatePort)
|
|
140
182
|
out.restatePort = restatePort;
|
|
183
|
+
if (values.get(KEYS.restateUi) === "1")
|
|
184
|
+
out.restateUi = true;
|
|
141
185
|
const password = values.get(KEYS.postgresPassword);
|
|
142
186
|
if (password && /^[A-Za-z0-9]{16,}$/.test(password))
|
|
143
187
|
out.postgresPassword = password;
|
|
144
188
|
const gatesToken = values.get(KEYS.gatesToken);
|
|
145
189
|
if (gatesToken && /^[0-9a-f]{64,}$/i.test(gatesToken))
|
|
146
190
|
out.gatesToken = gatesToken;
|
|
147
|
-
const
|
|
148
|
-
if (
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
191
|
+
const harnessToken = values.get(KEYS.harnessToken);
|
|
192
|
+
if (harnessToken && /^[0-9a-f]{64,}$/i.test(harnessToken))
|
|
193
|
+
out.harnessToken = harnessToken;
|
|
194
|
+
const harness = values.get(KEYS.harness);
|
|
195
|
+
if (harness === "remote" || harness === "in-process")
|
|
196
|
+
out.harness = harness;
|
|
197
|
+
const objectStoreSecretKey = values.get(KEYS.objectStoreSecretKey);
|
|
198
|
+
if (objectStoreSecretKey && /^[0-9a-f]{64}$/i.test(objectStoreSecretKey))
|
|
199
|
+
out.objectStoreSecretKey = objectStoreSecretKey;
|
|
200
|
+
const sandboxes = parseSandboxes(values);
|
|
201
|
+
if (sandboxes)
|
|
202
|
+
out.sandboxes = sandboxes;
|
|
156
203
|
const ancestors = values.get(KEYS.studioFrameAncestors);
|
|
157
204
|
if (ancestors !== undefined) {
|
|
158
205
|
try {
|
package/dist/stack/host-files.js
CHANGED
|
@@ -26,7 +26,17 @@ async function writeAtomic(path, text) {
|
|
|
26
26
|
}
|
|
27
27
|
/** The Host root layout, mode 0700. */
|
|
28
28
|
export async function ensureHostLayout(paths) {
|
|
29
|
-
|
|
29
|
+
// The harness container's mounts exist before Compose binds them (else Docker makes them as root).
|
|
30
|
+
for (const dir of [
|
|
31
|
+
paths.root,
|
|
32
|
+
paths.home,
|
|
33
|
+
paths.tmp,
|
|
34
|
+
paths.tenant,
|
|
35
|
+
...Object.values(paths.harness),
|
|
36
|
+
paths.plugins,
|
|
37
|
+
paths.keys,
|
|
38
|
+
paths.docker,
|
|
39
|
+
])
|
|
30
40
|
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
31
41
|
}
|
|
32
42
|
export async function readHostConfig(paths) {
|
package/dist/stack/images.d.ts
CHANGED
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Images a local Tenant runs. The Runtime and Studio tags are pinned by this
|
|
3
3
|
* CLI release; `NYLORUN_RUNTIME_IMAGE` and `NYLORUN_STUDIO_IMAGE` override them
|
|
4
|
-
* (local builds, CI images).
|
|
5
|
-
*
|
|
4
|
+
* (local builds, CI images). The sandboxes service (`nylorun sandbox enable`) is
|
|
5
|
+
* versioned with the Runtime: `ghcr.io/nylorun/sandboxes:<runtime pin>`, or
|
|
6
|
+
* `NYLORUN_SANDBOXES_IMAGE`. Postgres, Restate, s2-lite and RustFS are the
|
|
7
|
+
* official images, pinned here; RustFS (the Object store, D35) by digest as well, so
|
|
8
|
+
* it is upgraded only on purpose.
|
|
6
9
|
*/
|
|
7
10
|
export declare const PINNED_IMAGES: {
|
|
8
11
|
readonly postgres: "postgres:17.11";
|
|
9
12
|
readonly restate: "docker.restate.dev/restatedev/restate:1.7.12";
|
|
10
13
|
readonly s2: "ghcr.io/s2-streamstore/s2:0.43.0";
|
|
14
|
+
readonly rustfs: "rustfs/rustfs:1.0.1@sha256:1803faef57627e2d9c2e7d89d655d712ddded5389040054987163043fecb6a3c";
|
|
11
15
|
};
|
|
12
16
|
export interface StackImages {
|
|
13
17
|
runtime: string;
|
|
14
18
|
studio: string;
|
|
19
|
+
sandboxes: string;
|
|
15
20
|
postgres: string;
|
|
16
21
|
restate: string;
|
|
17
22
|
s2: string;
|
|
23
|
+
rustfs: string;
|
|
18
24
|
}
|
|
19
25
|
/** `NYLORUN_RUNTIME_IMAGE` names the Runtime image, so its version is unknown here. */
|
|
20
26
|
export declare function runtimeImageOverridden(env: Readonly<Record<string, string | undefined>>): boolean;
|
package/dist/stack/images.js
CHANGED
|
@@ -2,13 +2,17 @@ import { pinnedVersion } from "./versions.js";
|
|
|
2
2
|
/**
|
|
3
3
|
* Images a local Tenant runs. The Runtime and Studio tags are pinned by this
|
|
4
4
|
* CLI release; `NYLORUN_RUNTIME_IMAGE` and `NYLORUN_STUDIO_IMAGE` override them
|
|
5
|
-
* (local builds, CI images).
|
|
6
|
-
*
|
|
5
|
+
* (local builds, CI images). The sandboxes service (`nylorun sandbox enable`) is
|
|
6
|
+
* versioned with the Runtime: `ghcr.io/nylorun/sandboxes:<runtime pin>`, or
|
|
7
|
+
* `NYLORUN_SANDBOXES_IMAGE`. Postgres, Restate, s2-lite and RustFS are the
|
|
8
|
+
* official images, pinned here; RustFS (the Object store, D35) by digest as well, so
|
|
9
|
+
* it is upgraded only on purpose.
|
|
7
10
|
*/
|
|
8
11
|
export const PINNED_IMAGES = {
|
|
9
12
|
postgres: "postgres:17.11",
|
|
10
13
|
restate: "docker.restate.dev/restatedev/restate:1.7.12",
|
|
11
14
|
s2: "ghcr.io/s2-streamstore/s2:0.43.0",
|
|
15
|
+
rustfs: "rustfs/rustfs:1.0.1@sha256:1803faef57627e2d9c2e7d89d655d712ddded5389040054987163043fecb6a3c",
|
|
12
16
|
};
|
|
13
17
|
function override(env, name) {
|
|
14
18
|
const value = env[name]?.trim();
|
|
@@ -27,6 +31,8 @@ export function stackImages(env, versions = {
|
|
|
27
31
|
`ghcr.io/nylorun/runtime:${versions.runtime}`,
|
|
28
32
|
studio: override(env, "NYLORUN_STUDIO_IMAGE") ??
|
|
29
33
|
`ghcr.io/nylorun/studio:${versions.studio}`,
|
|
34
|
+
sandboxes: override(env, "NYLORUN_SANDBOXES_IMAGE") ??
|
|
35
|
+
`ghcr.io/nylorun/sandboxes:${versions.runtime}`,
|
|
30
36
|
...PINNED_IMAGES,
|
|
31
37
|
};
|
|
32
38
|
}
|
package/dist/stack/index.d.ts
CHANGED
|
@@ -7,6 +7,15 @@ export type { Check, DockerChecks } from "./docker.js";
|
|
|
7
7
|
export declare function defaultStackDeps(env: Readonly<Record<string, string | undefined>>): StackDeps;
|
|
8
8
|
/** Entry for `nylorun start|stop|status|logs|reset|studio|ls|delete`. */
|
|
9
9
|
export declare function stackCommand(name: string, args: readonly string[], env: Readonly<Record<string, string | undefined>>): Promise<number>;
|
|
10
|
+
/** Entry for `nylorun sandbox ls|rm`. */
|
|
11
|
+
export declare function sandboxCommand(args: readonly string[], env: Readonly<Record<string, string | undefined>>): Promise<number>;
|
|
12
|
+
export { sandboxUsage } from "./sandbox.js";
|
|
13
|
+
/** Entry for `nylorun mcp connect`. */
|
|
14
|
+
export declare function mcpCommand(args: readonly string[], env: Readonly<Record<string, string | undefined>>): Promise<number>;
|
|
15
|
+
export { mcpUsage } from "./mcp.js";
|
|
16
|
+
/** Entry for `nylorun key put|list|rm`. */
|
|
17
|
+
export declare function keyCommand(args: readonly string[], env: Readonly<Record<string, string | undefined>>): Promise<number>;
|
|
18
|
+
export { keyUsage } from "./keys.js";
|
|
10
19
|
/** `nylorun studio`, landing on `next` when given, else on the Tenant's page. */
|
|
11
20
|
export declare function studioCommand(args: readonly string[], env: Readonly<Record<string, string | undefined>>, options?: {
|
|
12
21
|
next?: string;
|
package/dist/stack/index.js
CHANGED
|
@@ -2,6 +2,9 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { createInterface } from "node:readline/promises";
|
|
3
3
|
import { runStackCommand, runStudioCommand } from "./commands.js";
|
|
4
4
|
import { spawnDocker } from "./docker.js";
|
|
5
|
+
import { keyCommand as runKeyCommand } from "./keys.js";
|
|
6
|
+
import { sandboxCommand as runSandboxCommand } from "./sandbox.js";
|
|
7
|
+
import { mcpCommand as runMcpCommand } from "./mcp.js";
|
|
5
8
|
import { pinnedVersion } from "./versions.js";
|
|
6
9
|
import { loopbackPorts } from "./ports.js";
|
|
7
10
|
export { ensureStack, isStackCommand, readStackStatus, stackUsage, studioLoginUrl, tenantStudioPath, STACK_SERVICES, } from "./commands.js";
|
|
@@ -19,7 +22,6 @@ function browserCommand(env) {
|
|
|
19
22
|
*/
|
|
20
23
|
const STACK_ENV_OWNED = [
|
|
21
24
|
"NYLORUN_TENANT_NAME",
|
|
22
|
-
"NYLORUN_DERIVED_PRINCIPALS",
|
|
23
25
|
"NYLORUN_STUDIO_ANALYTICS_ID",
|
|
24
26
|
];
|
|
25
27
|
/** Real dependencies: docker on PATH, global fetch, the terminal. */
|
|
@@ -79,6 +81,21 @@ export function defaultStackDeps(env) {
|
|
|
79
81
|
export async function stackCommand(name, args, env) {
|
|
80
82
|
return await runStackCommand(name, args, defaultStackDeps(env));
|
|
81
83
|
}
|
|
84
|
+
/** Entry for `nylorun sandbox ls|rm`. */
|
|
85
|
+
export async function sandboxCommand(args, env) {
|
|
86
|
+
return await runSandboxCommand(defaultStackDeps(env), args);
|
|
87
|
+
}
|
|
88
|
+
export { sandboxUsage } from "./sandbox.js";
|
|
89
|
+
/** Entry for `nylorun mcp connect`. */
|
|
90
|
+
export async function mcpCommand(args, env) {
|
|
91
|
+
return await runMcpCommand(defaultStackDeps(env), args);
|
|
92
|
+
}
|
|
93
|
+
export { mcpUsage } from "./mcp.js";
|
|
94
|
+
/** Entry for `nylorun key put|list|rm`. */
|
|
95
|
+
export async function keyCommand(args, env) {
|
|
96
|
+
return await runKeyCommand(defaultStackDeps(env), args);
|
|
97
|
+
}
|
|
98
|
+
export { keyUsage } from "./keys.js";
|
|
82
99
|
/** `nylorun studio`, landing on `next` when given, else on the Tenant's page. */
|
|
83
100
|
export async function studioCommand(args, env, options = {}) {
|
|
84
101
|
return await runStudioCommand(args, defaultStackDeps(env), options);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type StackDeps } from "./commands.js";
|
|
2
|
+
export declare const keyUsage = " key put <id> [--tenant <name>] create the Tenant's key <id>, or rotate it (the old key stops working); prints the key once\n key list [--tenant <name>] [--json] the Tenant's keys: id, role, when issued (never the keys)\n key rm <id> [--tenant <name>] delete key <id>: it stops working at once";
|
|
3
|
+
/** `nylorun key <put|list|rm> ...` */
|
|
4
|
+
export declare function keyCommand(deps: StackDeps, args: readonly string[]): Promise<number>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nylorun key put | list | rm`: the running local Tenant's operator keys (F9 I1, Host feature
|
|
3
|
+
* `operator-keys`), through its Admin API with the admin key. Revocable application keys by
|
|
4
|
+
* name, for app servers and other clients; none of these starts the Tenant.
|
|
5
|
+
*/
|
|
6
|
+
import { CliError } from "../errors.js";
|
|
7
|
+
import { parseStackFlags, runningAdmin } from "./commands.js";
|
|
8
|
+
import { deleteOperatorKey, listOperatorKeys, putOperatorKey } from "./operator-keys.js";
|
|
9
|
+
export const keyUsage = ` key put <id> [--tenant <name>] create the Tenant's key <id>, or rotate it (the old key stops working); prints the key once
|
|
10
|
+
key list [--tenant <name>] [--json] the Tenant's keys: id, role, when issued (never the keys)
|
|
11
|
+
key rm <id> [--tenant <name>] delete key <id>: it stops working at once`;
|
|
12
|
+
const usageError = (message) => new CliError(message, 2);
|
|
13
|
+
async function put(deps, args) {
|
|
14
|
+
const usage = "nylorun key put <id> [--tenant <name>]";
|
|
15
|
+
const flags = parseStackFlags(args, { values: ["--tenant"] }, usage);
|
|
16
|
+
if (flags.rest.length !== 1)
|
|
17
|
+
throw usageError(`Usage: ${usage}`);
|
|
18
|
+
const id = flags.rest[0];
|
|
19
|
+
const name = flags.values.get("--tenant");
|
|
20
|
+
const admin = await runningAdmin(deps, name === undefined ? {} : { name });
|
|
21
|
+
const key = await putOperatorKey(admin, id);
|
|
22
|
+
// The key alone on stdout, for scripts; it is not shown again.
|
|
23
|
+
deps.out(key.key);
|
|
24
|
+
deps.err(`${key.rotated ? "Rotated" : "Created"} key ${key.id} on Tenant ${admin.name}${key.rotated ? " (the previous key no longer works)" : ""}. Store it now: it is not shown again.`);
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
async function list(deps, args) {
|
|
28
|
+
const usage = "nylorun key list [--tenant <name>] [--json]";
|
|
29
|
+
const flags = parseStackFlags(args, { booleans: ["--json"], values: ["--tenant"] }, usage);
|
|
30
|
+
if (flags.rest.length)
|
|
31
|
+
throw usageError(`Usage: ${usage}`);
|
|
32
|
+
const name = flags.values.get("--tenant");
|
|
33
|
+
const admin = await runningAdmin(deps, name === undefined ? {} : { name });
|
|
34
|
+
const keys = await listOperatorKeys(admin);
|
|
35
|
+
if (flags.booleans.has("--json")) {
|
|
36
|
+
deps.out(JSON.stringify(keys, null, 2));
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
const rows = [["ID", "ROLE", "CREATED"], ...keys.map((key) => [key.id, key.role, key.createdAt])];
|
|
40
|
+
const widths = rows[0].map((_, column) => Math.max(...rows.map((row) => row[column].length)));
|
|
41
|
+
for (const row of rows)
|
|
42
|
+
deps.out(row
|
|
43
|
+
.map((cell, column) => (column === row.length - 1 ? cell : cell.padEnd(widths[column])))
|
|
44
|
+
.join(" "));
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
async function rm(deps, args) {
|
|
48
|
+
const usage = "nylorun key rm <id> [--tenant <name>]";
|
|
49
|
+
const flags = parseStackFlags(args, { values: ["--tenant"] }, usage);
|
|
50
|
+
if (flags.rest.length !== 1)
|
|
51
|
+
throw usageError(`Usage: ${usage}`);
|
|
52
|
+
const id = flags.rest[0];
|
|
53
|
+
const name = flags.values.get("--tenant");
|
|
54
|
+
const admin = await runningAdmin(deps, name === undefined ? {} : { name });
|
|
55
|
+
if (!(await deleteOperatorKey(admin, id))) {
|
|
56
|
+
deps.err(`No key ${id} on Tenant ${admin.name}.`);
|
|
57
|
+
return 1;
|
|
58
|
+
}
|
|
59
|
+
deps.out(`Deleted key ${id}: it no longer works.`);
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
/** `nylorun key <put|list|rm> ...` */
|
|
63
|
+
export async function keyCommand(deps, args) {
|
|
64
|
+
const [sub, ...rest] = args;
|
|
65
|
+
if (sub === "put")
|
|
66
|
+
return put(deps, rest);
|
|
67
|
+
if (sub === "list" || sub === "ls")
|
|
68
|
+
return list(deps, rest);
|
|
69
|
+
if (sub === "rm" || sub === "delete")
|
|
70
|
+
return rm(deps, rest);
|
|
71
|
+
throw usageError(`Usage:\n${keyUsage}`);
|
|
72
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type StackDeps } from "./commands.js";
|
|
2
|
+
export declare const mcpUsage = " mcp connect <url> --server <name> [--vault <id>] [--client-id <id>] [--tenant <name>] [--no-open]\n sign the Tenant in to a remote MCP server with OAuth; the credential goes to the\n installation vault mcp (or --vault), which sessions attach";
|
|
3
|
+
/** The installation vault `connect` uses unless `--vault` names one. */
|
|
4
|
+
export declare const MCP_VAULT_NAME = "mcp";
|
|
5
|
+
/** How long `connect` waits for the browser sign-in: the Runtime keeps it for ten minutes. */
|
|
6
|
+
export declare const CONNECT_TIMEOUT_MS: number;
|
|
7
|
+
/** `nylorun mcp <connect> ...` */
|
|
8
|
+
export declare function mcpCommand(deps: StackDeps, args: readonly string[]): Promise<number>;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nylorun mcp connect <url> --server <name>`: signs the running local Tenant in to a remote MCP
|
|
3
|
+
* server with OAuth (F9 C2), through its Tenant API with an operator key (`runningTenantApi`).
|
|
4
|
+
* The Runtime's keys module runs discovery, registration and the code exchange and seals the
|
|
5
|
+
* credential in an installation vault (`mcp` unless `--vault` names one); this command only
|
|
6
|
+
* opens the browser and waits for the credential to appear. It never starts the Tenant.
|
|
7
|
+
*/
|
|
8
|
+
import { randomUUID } from "node:crypto";
|
|
9
|
+
import { PROTOCOL_HEADER, PROTOCOL_VERSION } from "@nylorun/core/compatibility";
|
|
10
|
+
import { CliError } from "../errors.js";
|
|
11
|
+
import { parseStackFlags, runningTenantApi } from "./commands.js";
|
|
12
|
+
export const mcpUsage = ` mcp connect <url> --server <name> [--vault <id>] [--client-id <id>] [--tenant <name>] [--no-open]
|
|
13
|
+
sign the Tenant in to a remote MCP server with OAuth; the credential goes to the
|
|
14
|
+
installation vault mcp (or --vault), which sessions attach`;
|
|
15
|
+
/** The installation vault `connect` uses unless `--vault` names one. */
|
|
16
|
+
export const MCP_VAULT_NAME = "mcp";
|
|
17
|
+
/** How long `connect` waits for the browser sign-in: the Runtime keeps it for ten minutes. */
|
|
18
|
+
export const CONNECT_TIMEOUT_MS = 10 * 60_000;
|
|
19
|
+
const usageError = (message) => new CliError(message, 2);
|
|
20
|
+
async function call(deps, api, method, path, body) {
|
|
21
|
+
const response = await deps.fetch(`${api.runtimeUrl}${path}`, {
|
|
22
|
+
method,
|
|
23
|
+
headers: {
|
|
24
|
+
authorization: `Bearer ${api.applicationKey}`,
|
|
25
|
+
[PROTOCOL_HEADER]: String(PROTOCOL_VERSION),
|
|
26
|
+
accept: "application/json",
|
|
27
|
+
...(body === undefined ? {} : { "content-type": "application/json" }),
|
|
28
|
+
},
|
|
29
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
30
|
+
redirect: "error",
|
|
31
|
+
signal: AbortSignal.timeout(150_000),
|
|
32
|
+
});
|
|
33
|
+
const answer = (await response.json().catch(() => ({})));
|
|
34
|
+
if (!response.ok)
|
|
35
|
+
throw new CliError(`${method} ${path} returned ${response.status}${typeof answer.message === "string" ? `: ${answer.message}` : ""}`, 1);
|
|
36
|
+
return answer;
|
|
37
|
+
}
|
|
38
|
+
/** The URL as the vault binds it: origin, path and query, no default port, no fragment. */
|
|
39
|
+
function bindingUrl(input) {
|
|
40
|
+
let url;
|
|
41
|
+
try {
|
|
42
|
+
url = new URL(input);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
throw usageError(`${input} is not a URL.`);
|
|
46
|
+
}
|
|
47
|
+
if (url.protocol !== "https:" && url.protocol !== "http:")
|
|
48
|
+
throw usageError(`${input} is not an http(s) URL.`);
|
|
49
|
+
if (url.username || url.password || url.hash)
|
|
50
|
+
throw usageError(`${input} must not carry userinfo or a fragment.`);
|
|
51
|
+
return `${url.origin}${url.pathname}${url.search}`;
|
|
52
|
+
}
|
|
53
|
+
/** The installation vault named `mcp`, created when there is none. */
|
|
54
|
+
async function mcpVault(deps, api) {
|
|
55
|
+
const { vaults } = await call(deps, api, "GET", "/v1/vaults");
|
|
56
|
+
const found = vaults.find((vault) => vault.ownerUserId === "installation" && vault.name === MCP_VAULT_NAME);
|
|
57
|
+
if (found)
|
|
58
|
+
return found;
|
|
59
|
+
const key = `nylorun-mcp-${randomUUID()}`;
|
|
60
|
+
return await call(deps, api, "POST", "/v1/vaults", {
|
|
61
|
+
requestId: key,
|
|
62
|
+
idempotencyKey: key,
|
|
63
|
+
name: MCP_VAULT_NAME,
|
|
64
|
+
scope: "installation",
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/** Each credential of the vault bound to `url`, by id, with when it last changed. */
|
|
68
|
+
async function credentialsFor(deps, api, vaultId, url) {
|
|
69
|
+
const { credentials } = await call(deps, api, "GET", `/v1/vaults/${encodeURIComponent(vaultId)}/credentials`);
|
|
70
|
+
return new Map(credentials
|
|
71
|
+
.filter((item) => item.type === "oauth" && item.binding.url === url)
|
|
72
|
+
.map((item) => [item.id, item.rotatedAt ?? item.createdAt]));
|
|
73
|
+
}
|
|
74
|
+
async function connect(deps, args) {
|
|
75
|
+
const usage = "nylorun mcp connect <url> --server <name> [--vault <id>] [--client-id <id>] [--tenant <name>] [--no-open]";
|
|
76
|
+
const flags = parseStackFlags(args, { booleans: ["--no-open"], values: ["--server", "--vault", "--client-id", "--tenant"] }, usage);
|
|
77
|
+
if (flags.rest.length !== 1)
|
|
78
|
+
throw usageError(`Usage: ${usage}`);
|
|
79
|
+
const server = flags.values.get("--server");
|
|
80
|
+
if (!server)
|
|
81
|
+
throw usageError(`--server names the MCP server, as the agent declares it. Usage: ${usage}`);
|
|
82
|
+
const url = bindingUrl(flags.rest[0]);
|
|
83
|
+
const clientId = flags.values.get("--client-id");
|
|
84
|
+
const name = flags.values.get("--tenant");
|
|
85
|
+
const api = await runningTenantApi(deps, name === undefined ? {} : { name });
|
|
86
|
+
const vaultId = flags.values.get("--vault") ?? (await mcpVault(deps, api)).id;
|
|
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 }) });
|
|
89
|
+
deps.out(`Sign in to ${server} in your browser:`);
|
|
90
|
+
deps.out(` ${started.authorizeUrl}`);
|
|
91
|
+
if (!flags.booleans.has("--no-open"))
|
|
92
|
+
await deps.openBrowser(started.authorizeUrl);
|
|
93
|
+
deps.out("Waiting for the sign-in to finish…");
|
|
94
|
+
const deadline = Math.min(Date.now() + CONNECT_TIMEOUT_MS, Date.parse(started.expiresAt) || Infinity);
|
|
95
|
+
const pollMs = deps.pollMs ?? 1000;
|
|
96
|
+
while (Date.now() < deadline) {
|
|
97
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
98
|
+
const now = await credentialsFor(deps, api, vaultId, url);
|
|
99
|
+
for (const [id, changed] of now)
|
|
100
|
+
if (before.get(id) !== changed) {
|
|
101
|
+
deps.out(`Connected ${server} (${url}): credential ${id} in installation vault ${vaultId}.`);
|
|
102
|
+
deps.out(`Sessions use it when they attach the vault: vaultIds: ["${vaultId}"].`);
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
throw new CliError(`The sign-in to ${server} did not finish in time. Run "nylorun mcp connect" again; the browser page says why when the server refused it.`, 1);
|
|
107
|
+
}
|
|
108
|
+
/** `nylorun mcp <connect> ...` */
|
|
109
|
+
export async function mcpCommand(deps, args) {
|
|
110
|
+
const [sub, ...rest] = args;
|
|
111
|
+
if (sub === "connect")
|
|
112
|
+
return connect(deps, rest);
|
|
113
|
+
throw usageError(`Usage:\n${mcpUsage}`);
|
|
114
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { OperatorKey, PutOperatorKeyResponse } from "@nylorun/core/contracts";
|
|
2
|
+
import { type KeyCredentials } from "../project/link.js";
|
|
3
|
+
import type { FetchLike } from "./studio-login.js";
|
|
4
|
+
/** Where a local Tenant's Admin API answers, and its admin key. */
|
|
5
|
+
export interface AdminEndpoint {
|
|
6
|
+
fetch: FetchLike;
|
|
7
|
+
adminUrl: string;
|
|
8
|
+
adminKey: string;
|
|
9
|
+
}
|
|
10
|
+
/** The key `nylorun start` gives the projects it links. */
|
|
11
|
+
export declare const PROJECT_KEY_ID = "project";
|
|
12
|
+
/** The key nylorun commands use outside a linked project. */
|
|
13
|
+
export declare const CLI_KEY_ID = "cli";
|
|
14
|
+
/** Creates key `id` or rotates it; the answer holds the key, this once. */
|
|
15
|
+
export declare function putOperatorKey(admin: AdminEndpoint, id: string): Promise<PutOperatorKeyResponse>;
|
|
16
|
+
export declare function listOperatorKeys(admin: AdminEndpoint): Promise<OperatorKey[]>;
|
|
17
|
+
/** Deletes key `id`: false when there is none. */
|
|
18
|
+
export declare function deleteOperatorKey(admin: AdminEndpoint, id: string): Promise<boolean>;
|
|
19
|
+
/**
|
|
20
|
+
* Whether `key` reaches the Tenant API at `runtimeUrl`: one authenticated read
|
|
21
|
+
* (`GET /v1/tenant`). An unknown key is the opaque 404; another answer is an error.
|
|
22
|
+
*/
|
|
23
|
+
export declare function keyAuthenticates(fetch: FetchLike, runtimeUrl: string, key: string): Promise<boolean>;
|
|
24
|
+
/**
|
|
25
|
+
* The operator key `id` kept in the Host root file `file` (0600): the file's key while it
|
|
26
|
+
* still authenticates, else `adopt` when it does (a key a project already holds), else a new
|
|
27
|
+
* key put through the Admin API and written to the file. One process at a time (`lock`), so
|
|
28
|
+
* concurrent commands share one key instead of rotating each other's.
|
|
29
|
+
*/
|
|
30
|
+
export declare function hostKey(options: {
|
|
31
|
+
admin: AdminEndpoint;
|
|
32
|
+
runtimeUrl: string;
|
|
33
|
+
id: string;
|
|
34
|
+
file: string;
|
|
35
|
+
lock: string;
|
|
36
|
+
adopt?: KeyCredentials;
|
|
37
|
+
}): Promise<KeyCredentials & {
|
|
38
|
+
put: boolean;
|
|
39
|
+
}>;
|