nylorun 0.3.0-beta → 0.4.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 +42 -0
- package/README.md +136 -60
- package/dist/cli.js +13 -11
- package/dist/doctor.d.ts +3 -1
- package/dist/doctor.js +42 -13
- package/dist/project/derived-key.d.ts +8 -0
- package/dist/project/derived-key.js +18 -0
- package/dist/project/link.d.ts +32 -4
- package/dist/project/link.js +83 -14
- package/dist/project/seed.d.ts +27 -0
- package/dist/project/seed.js +111 -0
- package/dist/stack/commands.d.ts +38 -15
- package/dist/stack/commands.js +517 -144
- package/dist/stack/compose-file.d.ts +30 -11
- package/dist/stack/compose-file.js +84 -16
- package/dist/stack/env-file.d.ts +19 -3
- package/dist/stack/env-file.js +44 -4
- package/dist/stack/host-files.d.ts +9 -0
- package/dist/stack/host-files.js +24 -1
- package/dist/stack/index.d.ts +3 -3
- package/dist/stack/index.js +8 -3
- package/dist/stack/paths.d.ts +11 -3
- package/dist/stack/paths.js +9 -6
- package/dist/stack/prepare.d.ts +10 -3
- package/dist/stack/prepare.js +23 -12
- package/dist/stack/restate-identity.d.ts +1 -1
- package/dist/stack/restate-identity.js +1 -1
- package/dist/stack/stacks.d.ts +47 -0
- package/dist/stack/stacks.js +116 -0
- package/package.json +4 -4
|
@@ -1,17 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* `<Host root>/
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* A local stack's Compose file (Runtime Architecture §14.3), written to
|
|
3
|
+
* `<Host root>/docker/compose.yaml` by `nylorun start`. Each stack is one installation with
|
|
4
|
+
* one Tenant (tenancy.md §6): its own Compose project (`nylorun-<name>`, the file's `name`),
|
|
5
|
+
* volumes, network and ports. Everything else that varies (ports, the Postgres password,
|
|
6
|
+
* UID/GID, the Host root, the images, the stack's name) comes from `docker/.env`.
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
-
* s2-lite; the Runtime's /ready checks all three. Postgres
|
|
9
|
-
*
|
|
10
|
-
* replication (Durable Streams), and
|
|
11
|
-
* relay can hold.
|
|
8
|
+
* The Tenant's state is the stack's Postgres database, executed through Restate, with its
|
|
9
|
+
* history in s2-lite; the Runtime's /ready checks all three. Postgres initialises the
|
|
10
|
+
* database with C collation (`--locale=C`) and runs with `wal_level=logical`: the stream
|
|
11
|
+
* relay feeds s2-lite from the record over logical replication (Durable Streams), and
|
|
12
|
+
* `max_slot_wal_keep_size` caps the WAL a stuck relay can hold. The Runtime creates the
|
|
13
|
+
* Tenant on its first start, named after the stack (`NYLORUN_TENANT_NAME`), with the derived
|
|
14
|
+
* principals of `NYLORUN_DERIVED_PRINCIPALS` (`project` for the Project link).
|
|
12
15
|
*
|
|
13
16
|
* Restate signs requests to the Worker endpoint with the private key in
|
|
14
|
-
* `
|
|
17
|
+
* `docker/restate-identity.pem`, mounted read-only; the Runtime gets the public
|
|
15
18
|
* key as NYLORUN_RESTATE_IDENTITY_KEY.
|
|
19
|
+
*
|
|
20
|
+
* The combined packing (blueprint D12): the `runtime` container runs the core
|
|
21
|
+
* and loop services, and the `gateway` container runs gates: the Model Gate,
|
|
22
|
+
* which alone reads model credentials, and the Tool Gate (F4.1), which alone
|
|
23
|
+
* holds remote MCP connections and their credentials and POSTs every Action
|
|
24
|
+
* delivery. Every model call, remote MCP call and delivery of the loop crosses it
|
|
25
|
+
* (NYLORUN_GATES_URL, with NYLORUN_GATES_TOKEN from `.env`); stdio MCP servers
|
|
26
|
+
* still run in the runtime container. The gateway also runs keys (F4.2): the only
|
|
27
|
+
* holder of the vault key, it runs every vault write that touches a secret and signs
|
|
28
|
+
* every token (NYLORUN_KEYS_URL). The gateway mounts only the Host's Tenant directory
|
|
29
|
+
* (`tenant/`) and its keys directory (`keys/`, the vault key), read-only: never
|
|
30
|
+
* host-credentials.json. The runtime mounts the Host root with `keys/` and `docker/`
|
|
31
|
+
* covered by empty read-only mounts, so it can read neither the vault key nor
|
|
32
|
+
* Restate's private key and `.env`. The runtime does not wait for the gateway: while
|
|
33
|
+
* it is down, model and MCP calls fail, deliveries are retried, vault writes and
|
|
34
|
+
* token minting answer 503, and the session takes the next message.
|
|
16
35
|
*/
|
|
17
|
-
export declare function renderComposeFile(): string;
|
|
36
|
+
export declare function renderComposeFile(project: string): string;
|
|
@@ -1,23 +1,42 @@
|
|
|
1
1
|
import { PINNED_IMAGES } from "./images.js";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
* `<Host root>/
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* A local stack's Compose file (Runtime Architecture §14.3), written to
|
|
4
|
+
* `<Host root>/docker/compose.yaml` by `nylorun start`. Each stack is one installation with
|
|
5
|
+
* one Tenant (tenancy.md §6): its own Compose project (`nylorun-<name>`, the file's `name`),
|
|
6
|
+
* volumes, network and ports. Everything else that varies (ports, the Postgres password,
|
|
7
|
+
* UID/GID, the Host root, the images, the stack's name) comes from `docker/.env`.
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
* s2-lite; the Runtime's /ready checks all three. Postgres
|
|
10
|
-
*
|
|
11
|
-
* replication (Durable Streams), and
|
|
12
|
-
* relay can hold.
|
|
9
|
+
* The Tenant's state is the stack's Postgres database, executed through Restate, with its
|
|
10
|
+
* history in s2-lite; the Runtime's /ready checks all three. Postgres initialises the
|
|
11
|
+
* database with C collation (`--locale=C`) and runs with `wal_level=logical`: the stream
|
|
12
|
+
* relay feeds s2-lite from the record over logical replication (Durable Streams), and
|
|
13
|
+
* `max_slot_wal_keep_size` caps the WAL a stuck relay can hold. The Runtime creates the
|
|
14
|
+
* Tenant on its first start, named after the stack (`NYLORUN_TENANT_NAME`), with the derived
|
|
15
|
+
* principals of `NYLORUN_DERIVED_PRINCIPALS` (`project` for the Project link).
|
|
13
16
|
*
|
|
14
17
|
* Restate signs requests to the Worker endpoint with the private key in
|
|
15
|
-
* `
|
|
18
|
+
* `docker/restate-identity.pem`, mounted read-only; the Runtime gets the public
|
|
16
19
|
* key as NYLORUN_RESTATE_IDENTITY_KEY.
|
|
20
|
+
*
|
|
21
|
+
* The combined packing (blueprint D12): the `runtime` container runs the core
|
|
22
|
+
* and loop services, and the `gateway` container runs gates: the Model Gate,
|
|
23
|
+
* which alone reads model credentials, and the Tool Gate (F4.1), which alone
|
|
24
|
+
* holds remote MCP connections and their credentials and POSTs every Action
|
|
25
|
+
* delivery. Every model call, remote MCP call and delivery of the loop crosses it
|
|
26
|
+
* (NYLORUN_GATES_URL, with NYLORUN_GATES_TOKEN from `.env`); stdio MCP servers
|
|
27
|
+
* still run in the runtime container. The gateway also runs keys (F4.2): the only
|
|
28
|
+
* holder of the vault key, it runs every vault write that touches a secret and signs
|
|
29
|
+
* every token (NYLORUN_KEYS_URL). The gateway mounts only the Host's Tenant directory
|
|
30
|
+
* (`tenant/`) and its keys directory (`keys/`, the vault key), read-only: never
|
|
31
|
+
* host-credentials.json. The runtime mounts the Host root with `keys/` and `docker/`
|
|
32
|
+
* covered by empty read-only mounts, so it can read neither the vault key nor
|
|
33
|
+
* Restate's private key and `.env`. The runtime does not wait for the gateway: while
|
|
34
|
+
* it is down, model and MCP calls fail, deliveries are retried, vault writes and
|
|
35
|
+
* token minting answer 503, and the session takes the next message.
|
|
17
36
|
*/
|
|
18
|
-
export function renderComposeFile() {
|
|
37
|
+
export function renderComposeFile(project) {
|
|
19
38
|
return `# Written by \`nylorun start\`; rewritten on every start. Settings live in .env.
|
|
20
|
-
name:
|
|
39
|
+
name: ${project}
|
|
21
40
|
|
|
22
41
|
services:
|
|
23
42
|
postgres: # Session Store
|
|
@@ -28,6 +47,7 @@ services:
|
|
|
28
47
|
POSTGRES_USER: nylorun
|
|
29
48
|
POSTGRES_PASSWORD: \${NYLORUN_POSTGRES_PASSWORD:?run nylorun start}
|
|
30
49
|
POSTGRES_DB: nylorun
|
|
50
|
+
POSTGRES_INITDB_ARGS: "--locale=C" # C collation for the whole database
|
|
31
51
|
volumes:
|
|
32
52
|
- postgres:/var/lib/postgresql/data
|
|
33
53
|
healthcheck:
|
|
@@ -44,7 +64,7 @@ services:
|
|
|
44
64
|
RESTATE_WORKER__INVOKER__REQUEST_IDENTITY_PRIVATE_KEY_PEM_FILE: /run/nylorun/restate-identity.pem
|
|
45
65
|
volumes:
|
|
46
66
|
- restate:/restate-data
|
|
47
|
-
- \${NYLORUN_HOST_ROOT:?run nylorun start}/
|
|
67
|
+
- \${NYLORUN_HOST_ROOT:?run nylorun start}/docker/restate-identity.pem:/run/nylorun/restate-identity.pem:ro
|
|
48
68
|
ports:
|
|
49
69
|
- "127.0.0.1:\${NYLORUN_RESTATE_PORT:?run nylorun start}:9070" # Restate UI and admin, for debugging
|
|
50
70
|
healthcheck:
|
|
@@ -66,9 +86,37 @@ services:
|
|
|
66
86
|
# the Runtime's /ready covers it.
|
|
67
87
|
restart: unless-stopped
|
|
68
88
|
|
|
89
|
+
gateway: # the Model and Tool Gates and keys: the vault key, credentials, signing, outbound calls; not published
|
|
90
|
+
image: \${NYLORUN_RUNTIME_IMAGE:?run nylorun start}
|
|
91
|
+
command: ["--service", "gates,keys"]
|
|
92
|
+
user: "\${NYLORUN_UID:?run nylorun start}:\${NYLORUN_GID:?run nylorun start}"
|
|
93
|
+
depends_on:
|
|
94
|
+
postgres: { condition: service_healthy }
|
|
95
|
+
environment:
|
|
96
|
+
NYLORUN_HOME: /nylorun
|
|
97
|
+
NYLORUN_PACKING: combined
|
|
98
|
+
NYLORUN_GATES_LISTEN_PORT: "4100"
|
|
99
|
+
NYLORUN_GATES_ALLOWED_HOSTS: gateway:4100
|
|
100
|
+
NYLORUN_GATES_TOKEN: \${NYLORUN_GATES_TOKEN:?run nylorun start}
|
|
101
|
+
NYLORUN_DATABASE_URL: postgres://nylorun:\${NYLORUN_POSTGRES_PASSWORD}@postgres:5432/nylorun
|
|
102
|
+
# Action endpoints on this machine: \`localhost\` in a registered URL means the Docker host.
|
|
103
|
+
NYLORUN_ENDPOINT_LOOPBACK: docker-host
|
|
104
|
+
extra_hosts:
|
|
105
|
+
host.docker.internal: host-gateway # model servers, MCP servers and Action endpoints on this machine
|
|
106
|
+
volumes:
|
|
107
|
+
# The Tenant's homes and its vault key only, read-only.
|
|
108
|
+
- \${NYLORUN_HOST_ROOT:?run nylorun start}/tenant:/nylorun/tenant:ro
|
|
109
|
+
- \${NYLORUN_HOST_ROOT:?run nylorun start}/keys:/nylorun/keys:ro
|
|
110
|
+
healthcheck:
|
|
111
|
+
test: ["CMD", "node", "-e", "fetch('http://localhost:4100/ready').then(r=>process.exit(r.ok?0:1),()=>process.exit(1))"]
|
|
112
|
+
interval: 2s
|
|
113
|
+
timeout: 5s
|
|
114
|
+
retries: 60
|
|
115
|
+
restart: unless-stopped
|
|
116
|
+
|
|
69
117
|
runtime:
|
|
70
118
|
image: \${NYLORUN_RUNTIME_IMAGE:?run nylorun start}
|
|
71
|
-
command: ["--
|
|
119
|
+
command: ["--service", "core,loop"]
|
|
72
120
|
user: "\${NYLORUN_UID:?run nylorun start}:\${NYLORUN_GID:?run nylorun start}"
|
|
73
121
|
depends_on:
|
|
74
122
|
postgres: { condition: service_healthy }
|
|
@@ -76,6 +124,16 @@ services:
|
|
|
76
124
|
s2: { condition: service_started }
|
|
77
125
|
environment:
|
|
78
126
|
NYLORUN_HOME: /nylorun
|
|
127
|
+
NYLORUN_PACKING: combined
|
|
128
|
+
# The Tenant the Runtime creates on its first start (later starts open it).
|
|
129
|
+
NYLORUN_TENANT_NAME: \${NYLORUN_STACK_NAME:?run nylorun start}
|
|
130
|
+
NYLORUN_DERIVED_PRINCIPALS: \${NYLORUN_DERIVED_PRINCIPALS:-project}
|
|
131
|
+
# Model calls, remote MCP calls and deliveries go through the gateway, and vault writes
|
|
132
|
+
# and token signing through its keys service: this container never reads a credential
|
|
133
|
+
# or the vault key.
|
|
134
|
+
NYLORUN_GATES_URL: http://gateway:4100
|
|
135
|
+
NYLORUN_KEYS_URL: http://gateway:4100
|
|
136
|
+
NYLORUN_GATES_TOKEN: \${NYLORUN_GATES_TOKEN:?run nylorun start}
|
|
79
137
|
NYLORUN_LISTEN_HOST: 0.0.0.0
|
|
80
138
|
NYLORUN_LISTEN_PORT: "4000"
|
|
81
139
|
# Host headers the Runtime accepts: the stack network name, and the
|
|
@@ -100,12 +158,22 @@ services:
|
|
|
100
158
|
host.docker.internal: host-gateway # the Docker host, also on Linux Docker Engine
|
|
101
159
|
volumes:
|
|
102
160
|
- \${NYLORUN_HOST_ROOT:?run nylorun start}:/nylorun # Host root
|
|
161
|
+
# Empty and read-only over the vault key (keys/) and the stack's secrets (docker/):
|
|
162
|
+
# only the gateway reads the key, and only Restate its private key.
|
|
163
|
+
- type: tmpfs
|
|
164
|
+
target: /nylorun/keys
|
|
165
|
+
read_only: true
|
|
166
|
+
tmpfs: { size: 4096, mode: 0755 } # empty, and listable by the runtime user
|
|
167
|
+
- type: tmpfs
|
|
168
|
+
target: /nylorun/docker
|
|
169
|
+
read_only: true
|
|
170
|
+
tmpfs: { size: 4096, mode: 0755 } # empty, and listable by the runtime user
|
|
103
171
|
- workspaces:/workspaces
|
|
104
172
|
ports:
|
|
105
173
|
- "127.0.0.1:\${NYLORUN_PORT:?run nylorun start}:4000" # Tenant API, SSE, browsers
|
|
106
174
|
- "127.0.0.1:\${NYLORUN_ADMIN_PORT:?run nylorun start}:4001" # Admin API (operators only)
|
|
107
|
-
healthcheck:
|
|
108
|
-
test: ["CMD", "node", "-e", "fetch('http://localhost:4000/
|
|
175
|
+
healthcheck: # liveness: a Tenant that cannot open is reported by nylorun start from the Admin status, not by a 300 s wait
|
|
176
|
+
test: ["CMD", "node", "-e", "fetch('http://localhost:4000/health').then(r=>process.exit(r.ok?0:1),()=>process.exit(1))"]
|
|
109
177
|
interval: 2s
|
|
110
178
|
timeout: 5s
|
|
111
179
|
retries: 60
|
package/dist/stack/env-file.d.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export declare const DEFAULT_STUDIO_FRAME_ANCESTORS: readonly ["nylorun://localhost", "http://nylorun.localhost"];
|
|
6
6
|
/**
|
|
7
|
-
* Settings in `
|
|
8
|
-
* Restate identity public key is derived from `restate-identity.pem`.
|
|
7
|
+
* Settings in `docker/.env` (mode 0600). Ports, the password and the gates token
|
|
8
|
+
* persist; the Restate identity public key is derived from `restate-identity.pem`.
|
|
9
9
|
*/
|
|
10
10
|
export interface StackEnv {
|
|
11
11
|
/** Published Runtime port (loopback): the Tenant API. */
|
|
@@ -17,7 +17,9 @@ export interface StackEnv {
|
|
|
17
17
|
/** Published Restate UI and admin port (loopback). */
|
|
18
18
|
restatePort: number;
|
|
19
19
|
postgresPassword: string;
|
|
20
|
-
/** `
|
|
20
|
+
/** The token the runtime presents to the gateway (`NYLORUN_GATES_TOKEN`), 32 bytes as hex. */
|
|
21
|
+
gatesToken: string;
|
|
22
|
+
/** `publickeyv1_...` of `docker/restate-identity.pem`. */
|
|
21
23
|
restateIdentityKey: string;
|
|
22
24
|
uid: number;
|
|
23
25
|
gid: number;
|
|
@@ -27,8 +29,20 @@ export interface StackEnv {
|
|
|
27
29
|
studioImage: string;
|
|
28
30
|
/** Exact origins that may frame Studio, separated by spaces (persists). */
|
|
29
31
|
studioFrameAncestors: string;
|
|
32
|
+
/** The stack's name: the name of the Tenant its Runtime creates. */
|
|
33
|
+
stackName: string;
|
|
34
|
+
/**
|
|
35
|
+
* Derived principals the Runtime registers on its Tenant, comma-separated (persists);
|
|
36
|
+
* always includes `project`, the Project link's principal.
|
|
37
|
+
*/
|
|
38
|
+
derivedPrincipals: string;
|
|
30
39
|
}
|
|
31
40
|
export declare function renderEnvFile(env: StackEnv): string;
|
|
41
|
+
/**
|
|
42
|
+
* `NYLORUN_DERIVED_PRINCIPALS`: comma-separated principal ids, as the Runtime accepts them
|
|
43
|
+
* (not `studio`), with `project` first so the Project link's key always works.
|
|
44
|
+
*/
|
|
45
|
+
export declare function parseDerivedPrincipals(raw: string, source: string): string[];
|
|
32
46
|
/** Parse KEY=value lines; comments and blank lines are skipped. */
|
|
33
47
|
export declare function parseEnvLines(text: string): Map<string, string>;
|
|
34
48
|
/** The settings that persist across starts, from an existing .env. */
|
|
@@ -38,7 +52,9 @@ export interface PersistedStackEnv {
|
|
|
38
52
|
studioPort?: number;
|
|
39
53
|
restatePort?: number;
|
|
40
54
|
postgresPassword?: string;
|
|
55
|
+
gatesToken?: string;
|
|
41
56
|
/** Validated origins; absent when the line is missing (an older .env). */
|
|
42
57
|
studioFrameAncestors?: string[];
|
|
58
|
+
derivedPrincipals?: string[];
|
|
43
59
|
}
|
|
44
60
|
export declare function parsePersisted(text: string): PersistedStackEnv;
|
package/dist/stack/env-file.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DERIVED_PRINCIPAL_ID_PATTERN } from "@nylorun/core/compatibility";
|
|
1
2
|
import { parseFrameAncestors } from "@nylorun/core/contracts";
|
|
2
3
|
import { CliError } from "../errors.js";
|
|
3
4
|
/**
|
|
@@ -14,6 +15,7 @@ const KEYS = {
|
|
|
14
15
|
studioPort: "NYLORUN_STUDIO_PORT",
|
|
15
16
|
restatePort: "NYLORUN_RESTATE_PORT",
|
|
16
17
|
postgresPassword: "NYLORUN_POSTGRES_PASSWORD",
|
|
18
|
+
gatesToken: "NYLORUN_GATES_TOKEN",
|
|
17
19
|
restateIdentityKey: "NYLORUN_RESTATE_IDENTITY_KEY",
|
|
18
20
|
uid: "NYLORUN_UID",
|
|
19
21
|
gid: "NYLORUN_GID",
|
|
@@ -21,6 +23,8 @@ const KEYS = {
|
|
|
21
23
|
runtimeImage: "NYLORUN_RUNTIME_IMAGE",
|
|
22
24
|
studioImage: "NYLORUN_STUDIO_IMAGE",
|
|
23
25
|
studioFrameAncestors: "NYLORUN_STUDIO_FRAME_ANCESTORS",
|
|
26
|
+
stackName: "NYLORUN_STACK_NAME",
|
|
27
|
+
derivedPrincipals: "NYLORUN_DERIVED_PRINCIPALS",
|
|
24
28
|
};
|
|
25
29
|
/** Compose .env values: single quotes keep a value literal (no interpolation). */
|
|
26
30
|
function quote(key, value) {
|
|
@@ -39,9 +43,9 @@ function unquote(value) {
|
|
|
39
43
|
export function renderEnvFile(env) {
|
|
40
44
|
const line = (field) => `${KEYS[field]}=${quote(KEYS[field], String(env[field]))}`;
|
|
41
45
|
return [
|
|
42
|
-
"# Written by `nylorun start`. Mode 0600: holds the Postgres password
|
|
43
|
-
"# Ports and the
|
|
44
|
-
"# Host root are refreshed on every start.",
|
|
46
|
+
"# Written by `nylorun start`. Mode 0600: holds the Postgres password and the",
|
|
47
|
+
"# gates token. Ports, the password and the token are kept across starts;",
|
|
48
|
+
"# images, UID/GID and the Host root are refreshed on every start.",
|
|
45
49
|
"",
|
|
46
50
|
"# Published on 127.0.0.1; clients use http://localhost:<port>.",
|
|
47
51
|
line("runtimePort"),
|
|
@@ -51,6 +55,10 @@ export function renderEnvFile(env) {
|
|
|
51
55
|
"",
|
|
52
56
|
line("postgresPassword"),
|
|
53
57
|
"",
|
|
58
|
+
"# The runtime presents this token to the gateway (the Model Gate) with every",
|
|
59
|
+
"# model call.",
|
|
60
|
+
line("gatesToken"),
|
|
61
|
+
"",
|
|
54
62
|
"# Restate signs requests to the Runtime's Worker endpoint with the private",
|
|
55
63
|
"# key in restate-identity.pem; the Runtime accepts only this public key.",
|
|
56
64
|
line("restateIdentityKey"),
|
|
@@ -68,8 +76,28 @@ export function renderEnvFile(env) {
|
|
|
68
76
|
"# starts; change with nylorun start --studio-embed-origin <origin>.",
|
|
69
77
|
line("studioFrameAncestors"),
|
|
70
78
|
"",
|
|
79
|
+
"# The stack's name, which its Runtime gives the Tenant it creates on the first",
|
|
80
|
+
"# start, and the derived principals it registers on that Tenant (keys derived",
|
|
81
|
+
"# from the admin key; `project` is the Project link's).",
|
|
82
|
+
line("stackName"),
|
|
83
|
+
line("derivedPrincipals"),
|
|
84
|
+
"",
|
|
71
85
|
].join("\n");
|
|
72
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* `NYLORUN_DERIVED_PRINCIPALS`: comma-separated principal ids, as the Runtime accepts them
|
|
89
|
+
* (not `studio`), with `project` first so the Project link's key always works.
|
|
90
|
+
*/
|
|
91
|
+
export function parseDerivedPrincipals(raw, source) {
|
|
92
|
+
const ids = raw
|
|
93
|
+
.split(",")
|
|
94
|
+
.map((entry) => entry.trim())
|
|
95
|
+
.filter((entry) => entry !== "");
|
|
96
|
+
for (const id of ids)
|
|
97
|
+
if (!DERIVED_PRINCIPAL_ID_PATTERN.test(id) || id === "studio")
|
|
98
|
+
throw new CliError(`${source} has '${id}': each entry must match ${DERIVED_PRINCIPAL_ID_PATTERN} and not be studio.`, 2);
|
|
99
|
+
return [...new Set(["project", ...ids])];
|
|
100
|
+
}
|
|
73
101
|
/** Parse KEY=value lines; comments and blank lines are skipped. */
|
|
74
102
|
export function parseEnvLines(text) {
|
|
75
103
|
const values = new Map();
|
|
@@ -108,13 +136,25 @@ export function parsePersisted(text) {
|
|
|
108
136
|
const password = values.get(KEYS.postgresPassword);
|
|
109
137
|
if (password && /^[A-Za-z0-9]{16,}$/.test(password))
|
|
110
138
|
out.postgresPassword = password;
|
|
139
|
+
const gatesToken = values.get(KEYS.gatesToken);
|
|
140
|
+
if (gatesToken && /^[0-9a-f]{64,}$/i.test(gatesToken))
|
|
141
|
+
out.gatesToken = gatesToken;
|
|
142
|
+
const principals = values.get(KEYS.derivedPrincipals);
|
|
143
|
+
if (principals !== undefined) {
|
|
144
|
+
try {
|
|
145
|
+
out.derivedPrincipals = parseDerivedPrincipals(principals, `${KEYS.derivedPrincipals} in docker/.env`);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
/* rewritten from the default on the next start */
|
|
149
|
+
}
|
|
150
|
+
}
|
|
111
151
|
const ancestors = values.get(KEYS.studioFrameAncestors);
|
|
112
152
|
if (ancestors !== undefined) {
|
|
113
153
|
try {
|
|
114
154
|
out.studioFrameAncestors = parseFrameAncestors(ancestors);
|
|
115
155
|
}
|
|
116
156
|
catch (error) {
|
|
117
|
-
throw new CliError(`${KEYS.studioFrameAncestors} in
|
|
157
|
+
throw new CliError(`${KEYS.studioFrameAncestors} in docker/.env: ${error instanceof Error ? error.message : String(error)}`, 1);
|
|
118
158
|
}
|
|
119
159
|
}
|
|
120
160
|
return out;
|
|
@@ -33,6 +33,15 @@ export declare function writeStackHostConfig(paths: StackPaths, input: {
|
|
|
33
33
|
runtimeVersion: string | undefined;
|
|
34
34
|
}): Promise<HostConfigFile>;
|
|
35
35
|
export declare function readAdminKey(paths: StackPaths): Promise<string | undefined>;
|
|
36
|
+
/**
|
|
37
|
+
* The Tenant's vault key in `keys/vault-kek` (32 random bytes, base64, mode 0600), which only the
|
|
38
|
+
* gateway container mounts (F4.2): created if missing, or moved from `tenant/vault-kek`, where a
|
|
39
|
+
* stack before F4.2 kept it. The gateway never creates it.
|
|
40
|
+
*/
|
|
41
|
+
export declare function ensureVaultKey(paths: StackPaths): Promise<{
|
|
42
|
+
created: boolean;
|
|
43
|
+
moved: boolean;
|
|
44
|
+
}>;
|
|
36
45
|
/** Create host-credentials.json (mode 0600) if missing; always leave it 0600. */
|
|
37
46
|
export declare function ensureHostCredentials(paths: StackPaths): Promise<{
|
|
38
47
|
adminKey: string;
|
package/dist/stack/host-files.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
2
3
|
import { chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
3
5
|
import { newTenantId } from "@nylorun/core/compatibility";
|
|
4
6
|
import { CliError } from "../errors.js";
|
|
5
7
|
/**
|
|
@@ -24,7 +26,7 @@ async function writeAtomic(path, text) {
|
|
|
24
26
|
}
|
|
25
27
|
/** The Host root layout, mode 0700. */
|
|
26
28
|
export async function ensureHostLayout(paths) {
|
|
27
|
-
for (const dir of [paths.root, paths.home, paths.tmp, paths.
|
|
29
|
+
for (const dir of [paths.root, paths.home, paths.tmp, paths.tenant, paths.keys, paths.docker])
|
|
28
30
|
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
29
31
|
}
|
|
30
32
|
export async function readHostConfig(paths) {
|
|
@@ -82,6 +84,27 @@ export async function readAdminKey(paths) {
|
|
|
82
84
|
return undefined;
|
|
83
85
|
}
|
|
84
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* The Tenant's vault key in `keys/vault-kek` (32 random bytes, base64, mode 0600), which only the
|
|
89
|
+
* gateway container mounts (F4.2): created if missing, or moved from `tenant/vault-kek`, where a
|
|
90
|
+
* stack before F4.2 kept it. The gateway never creates it.
|
|
91
|
+
*/
|
|
92
|
+
export async function ensureVaultKey(paths) {
|
|
93
|
+
await mkdir(paths.keys, { recursive: true, mode: 0o700 });
|
|
94
|
+
await chmod(paths.keys, 0o700);
|
|
95
|
+
if (existsSync(paths.vaultKey)) {
|
|
96
|
+
await chmod(paths.vaultKey, 0o600);
|
|
97
|
+
return { created: false, moved: false };
|
|
98
|
+
}
|
|
99
|
+
const legacy = join(paths.tenant, "vault-kek");
|
|
100
|
+
if (existsSync(legacy)) {
|
|
101
|
+
await rename(legacy, paths.vaultKey);
|
|
102
|
+
await chmod(paths.vaultKey, 0o600);
|
|
103
|
+
return { created: false, moved: true };
|
|
104
|
+
}
|
|
105
|
+
await writeAtomic(paths.vaultKey, `${randomBytes(32).toString("base64")}\n`);
|
|
106
|
+
return { created: true, moved: false };
|
|
107
|
+
}
|
|
85
108
|
/** Create host-credentials.json (mode 0600) if missing; always leave it 0600. */
|
|
86
109
|
export async function ensureHostCredentials(paths) {
|
|
87
110
|
const existing = await readAdminKey(paths);
|
package/dist/stack/index.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { type StackDeps } from "./commands.js";
|
|
2
2
|
export { ensureStack, isStackCommand, readStackStatus, stackUsage, studioLoginUrl, tenantStudioPath, STACK_SERVICES, } from "./commands.js";
|
|
3
|
-
export type { StackDeps, StackEndpoints, StackStatus } from "./commands.js";
|
|
3
|
+
export type { StackDeps, StackEndpoints, StackStatus, StackTenant } from "./commands.js";
|
|
4
4
|
export { checkDocker } from "./docker.js";
|
|
5
5
|
export type { Check, DockerChecks } from "./docker.js";
|
|
6
6
|
/** Real dependencies: docker on PATH, global fetch, the terminal. */
|
|
7
7
|
export declare function defaultStackDeps(env: Readonly<Record<string, string | undefined>>): StackDeps;
|
|
8
|
-
/** Entry for `nylorun start|stop|status|logs|reset|studio`. */
|
|
8
|
+
/** Entry for `nylorun start|stop|status|logs|reset|studio|ls|delete|legacy`. */
|
|
9
9
|
export declare function stackCommand(name: string, args: readonly string[], env: Readonly<Record<string, string | undefined>>): Promise<number>;
|
|
10
|
-
/** `nylorun studio`, landing on `next`
|
|
10
|
+
/** `nylorun studio`, landing on `next` when given, else on the stack's Tenant. */
|
|
11
11
|
export declare function studioCommand(args: readonly string[], env: Readonly<Record<string, string | undefined>>, options?: {
|
|
12
12
|
next?: string;
|
|
13
13
|
}): Promise<number>;
|
package/dist/stack/index.js
CHANGED
|
@@ -13,12 +13,17 @@ function browserCommand(env) {
|
|
|
13
13
|
return "wslview";
|
|
14
14
|
return "xdg-open";
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Variables `docker/.env` sets for Compose: `nylorun start` writes them there, so the
|
|
18
|
+
* environment must not override them when Compose interpolates the file.
|
|
19
|
+
*/
|
|
20
|
+
const STACK_ENV_OWNED = ["NYLORUN_STACK_NAME", "NYLORUN_DERIVED_PRINCIPALS"];
|
|
16
21
|
/** Real dependencies: docker on PATH, global fetch, the terminal. */
|
|
17
22
|
export function defaultStackDeps(env) {
|
|
18
23
|
const interactive = Boolean(process.stdin.isTTY && process.stderr.isTTY);
|
|
19
24
|
return {
|
|
20
25
|
env,
|
|
21
|
-
docker: spawnDocker(env),
|
|
26
|
+
docker: spawnDocker(Object.fromEntries(Object.entries(env).filter(([key]) => !STACK_ENV_OWNED.includes(key)))),
|
|
22
27
|
fetch: (input, init) => fetch(input, init),
|
|
23
28
|
ports: loopbackPorts,
|
|
24
29
|
uid: typeof process.getuid === "function" ? process.getuid() : 1000,
|
|
@@ -66,11 +71,11 @@ export function defaultStackDeps(env) {
|
|
|
66
71
|
},
|
|
67
72
|
};
|
|
68
73
|
}
|
|
69
|
-
/** Entry for `nylorun start|stop|status|logs|reset|studio`. */
|
|
74
|
+
/** Entry for `nylorun start|stop|status|logs|reset|studio|ls|delete|legacy`. */
|
|
70
75
|
export async function stackCommand(name, args, env) {
|
|
71
76
|
return await runStackCommand(name, args, defaultStackDeps(env));
|
|
72
77
|
}
|
|
73
|
-
/** `nylorun studio`, landing on `next`
|
|
78
|
+
/** `nylorun studio`, landing on `next` when given, else on the stack's Tenant. */
|
|
74
79
|
export async function studioCommand(args, env, options = {}) {
|
|
75
80
|
return await runStudioCommand(args, defaultStackDeps(env), options);
|
|
76
81
|
}
|
package/dist/stack/paths.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Files under the Host root that the stack commands read and write. */
|
|
2
2
|
export interface StackPaths {
|
|
3
|
-
/** The Host root (`~/.nylorun
|
|
3
|
+
/** The Host root (`~/.nylorun/stacks/<name>` or `NYLORUN_HOME`), bind-mounted at /nylorun. */
|
|
4
4
|
root: string;
|
|
5
5
|
/** host.json: client-facing host and published port, no secrets. */
|
|
6
6
|
config: string;
|
|
@@ -8,10 +8,18 @@ export interface StackPaths {
|
|
|
8
8
|
credentials: string;
|
|
9
9
|
/** host-state.json: written only by a launcher-managed Runtime. */
|
|
10
10
|
state: string;
|
|
11
|
-
|
|
11
|
+
/** stack.json: the stack's name and the project it was created for. */
|
|
12
|
+
record: string;
|
|
13
|
+
/** The Tenant directory: homes, sandboxes, plugin data, logs. */
|
|
14
|
+
tenant: string;
|
|
15
|
+
/** The keys directory, mounted only into the gateway (F4.2). */
|
|
16
|
+
keys: string;
|
|
17
|
+
/** The Tenant's vault key (KEK), `keys/vault-kek`. */
|
|
18
|
+
vaultKey: string;
|
|
12
19
|
home: string;
|
|
13
20
|
tmp: string;
|
|
14
|
-
|
|
21
|
+
/** The Docker Compose files: compose.yaml, .env and the Restate identity key. */
|
|
22
|
+
docker: string;
|
|
15
23
|
compose: string;
|
|
16
24
|
env: string;
|
|
17
25
|
/** Restate's request-identity private key (Ed25519 PKCS#8 PEM), mode 0600. */
|
package/dist/stack/paths.js
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
import { join, resolve } from "node:path";
|
|
2
2
|
export function stackPaths(hostRoot) {
|
|
3
3
|
const root = resolve(hostRoot);
|
|
4
|
-
const
|
|
4
|
+
const docker = join(root, "docker");
|
|
5
5
|
return {
|
|
6
6
|
root,
|
|
7
7
|
config: join(root, "host.json"),
|
|
8
8
|
credentials: join(root, "host-credentials.json"),
|
|
9
9
|
state: join(root, "host-state.json"),
|
|
10
|
-
|
|
10
|
+
record: join(root, "stack.json"),
|
|
11
|
+
tenant: join(root, "tenant"),
|
|
12
|
+
keys: join(root, "keys"),
|
|
13
|
+
vaultKey: join(root, "keys", "vault-kek"),
|
|
11
14
|
home: join(root, "home"),
|
|
12
15
|
tmp: join(root, "tmp"),
|
|
13
|
-
|
|
14
|
-
compose: join(
|
|
15
|
-
env: join(
|
|
16
|
-
restateIdentity: join(
|
|
16
|
+
docker,
|
|
17
|
+
compose: join(docker, "compose.yaml"),
|
|
18
|
+
env: join(docker, ".env"),
|
|
19
|
+
restateIdentity: join(docker, "restate-identity.pem"),
|
|
17
20
|
};
|
|
18
21
|
}
|
package/dist/stack/prepare.d.ts
CHANGED
|
@@ -11,21 +11,28 @@ export interface PreparedStack {
|
|
|
11
11
|
firstRun: boolean;
|
|
12
12
|
}
|
|
13
13
|
/** Read the persisted stack settings without writing anything. */
|
|
14
|
-
export declare function readStackEnv(paths: StackPaths): Promise<import("./env-file.js").PersistedStackEnv | undefined>;
|
|
14
|
+
export declare function readStackEnv(paths: Pick<StackPaths, "env">): Promise<import("./env-file.js").PersistedStackEnv | undefined>;
|
|
15
15
|
/**
|
|
16
16
|
* Write everything `docker compose up` needs under the Host root:
|
|
17
|
-
* host.json, host-credentials.json (0600),
|
|
18
|
-
*
|
|
17
|
+
* host.json, host-credentials.json (0600), docker/compose.yaml,
|
|
18
|
+
* docker/.env (0600) and docker/restate-identity.pem (0600). Ports and the
|
|
19
19
|
* Postgres password persist in .env; the identity key persists in its PEM.
|
|
20
20
|
*/
|
|
21
21
|
export declare function prepareStack(input: {
|
|
22
22
|
paths: StackPaths;
|
|
23
|
+
/** The stack's name (its Tenant's name) and its Compose project. */
|
|
24
|
+
name: string;
|
|
25
|
+
project: string;
|
|
23
26
|
images: StackImages;
|
|
24
27
|
uid: number;
|
|
25
28
|
gid: number;
|
|
26
29
|
/** Recorded in host.json; undefined keeps the recorded version. */
|
|
27
30
|
runtimeVersion: string | undefined;
|
|
28
31
|
ports: PortProbe;
|
|
32
|
+
/** Ports other stacks keep in their `.env`: never chosen for a port not chosen yet. */
|
|
33
|
+
reserved?: ReadonlySet<number>;
|
|
34
|
+
/** `NYLORUN_DERIVED_PRINCIPALS` from the environment of `nylorun start`; else kept. */
|
|
35
|
+
derivedPrincipals?: string;
|
|
29
36
|
/**
|
|
30
37
|
* Changes to the origins that may embed Studio: `reset` goes back to the
|
|
31
38
|
* defaults, `add` appends (for example a desktop app's dev server).
|
package/dist/stack/prepare.js
CHANGED
|
@@ -3,8 +3,8 @@ import { chmod, readFile, rename, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { renderComposeFile } from "./compose-file.js";
|
|
4
4
|
import { parseFrameAncestors } from "@nylorun/core/contracts";
|
|
5
5
|
import { CliError } from "../errors.js";
|
|
6
|
-
import { DEFAULT_STUDIO_FRAME_ANCESTORS, parsePersisted, renderEnvFile, } from "./env-file.js";
|
|
7
|
-
import { ensureHostCredentials, ensureHostLayout, writeStackHostConfig, } from "./host-files.js";
|
|
6
|
+
import { DEFAULT_STUDIO_FRAME_ANCESTORS, parseDerivedPrincipals, parsePersisted, renderEnvFile, } from "./env-file.js";
|
|
7
|
+
import { ensureHostCredentials, ensureVaultKey, ensureHostLayout, writeStackHostConfig, } from "./host-files.js";
|
|
8
8
|
import { choosePort, DEFAULT_PORTS } from "./ports.js";
|
|
9
9
|
import { ensureIdentityKey } from "./restate-identity.js";
|
|
10
10
|
async function writeFileMode(path, text, mode) {
|
|
@@ -28,8 +28,8 @@ export async function readStackEnv(paths) {
|
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
30
30
|
* Write everything `docker compose up` needs under the Host root:
|
|
31
|
-
* host.json, host-credentials.json (0600),
|
|
32
|
-
*
|
|
31
|
+
* host.json, host-credentials.json (0600), docker/compose.yaml,
|
|
32
|
+
* docker/.env (0600) and docker/restate-identity.pem (0600). Ports and the
|
|
33
33
|
* Postgres password persist in .env; the identity key persists in its PEM.
|
|
34
34
|
*/
|
|
35
35
|
export async function prepareStack(input) {
|
|
@@ -38,13 +38,17 @@ export async function prepareStack(input) {
|
|
|
38
38
|
const persisted = (await readStackEnv(paths)) ?? {};
|
|
39
39
|
const firstRun = persisted.runtimePort === undefined;
|
|
40
40
|
const taken = new Set();
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
41
|
+
const reserved = input.reserved ?? new Set();
|
|
42
|
+
const pick = async (preferred, persistedPort) => {
|
|
43
|
+
const avoid = persistedPort === undefined ? new Set([...taken, ...reserved]) : taken;
|
|
44
|
+
const port = await choosePort(input.ports, preferred, persistedPort, avoid);
|
|
45
|
+
taken.add(port);
|
|
46
|
+
return port;
|
|
47
|
+
};
|
|
48
|
+
const runtimePort = await pick(DEFAULT_PORTS.runtime, persisted.runtimePort);
|
|
49
|
+
const adminPort = await pick(DEFAULT_PORTS.admin, persisted.adminPort);
|
|
50
|
+
const studioPort = await pick(DEFAULT_PORTS.studio, persisted.studioPort);
|
|
51
|
+
const restatePort = await pick(DEFAULT_PORTS.restate, persisted.restatePort);
|
|
48
52
|
const identity = await ensureIdentityKey(paths.restateIdentity, writeFileMode);
|
|
49
53
|
let added;
|
|
50
54
|
try {
|
|
@@ -63,6 +67,8 @@ export async function prepareStack(input) {
|
|
|
63
67
|
studioPort,
|
|
64
68
|
restatePort,
|
|
65
69
|
postgresPassword: persisted.postgresPassword ?? randomBytes(24).toString("hex"),
|
|
70
|
+
// Kept across starts: a new token would recreate the runtime and gateway containers.
|
|
71
|
+
gatesToken: persisted.gatesToken ?? randomBytes(32).toString("hex"),
|
|
66
72
|
restateIdentityKey: identity.publicKey,
|
|
67
73
|
uid: input.uid,
|
|
68
74
|
gid: input.gid,
|
|
@@ -70,14 +76,19 @@ export async function prepareStack(input) {
|
|
|
70
76
|
runtimeImage: input.images.runtime,
|
|
71
77
|
studioImage: input.images.studio,
|
|
72
78
|
studioFrameAncestors,
|
|
79
|
+
stackName: input.name,
|
|
80
|
+
derivedPrincipals: (input.derivedPrincipals?.trim()
|
|
81
|
+
? parseDerivedPrincipals(input.derivedPrincipals, "NYLORUN_DERIVED_PRINCIPALS")
|
|
82
|
+
: (persisted.derivedPrincipals ?? ["project"])).join(","),
|
|
73
83
|
};
|
|
74
84
|
const { adminKey } = await ensureHostCredentials(paths);
|
|
85
|
+
await ensureVaultKey(paths);
|
|
75
86
|
const host = await writeStackHostConfig(paths, {
|
|
76
87
|
port: runtimePort,
|
|
77
88
|
adminPort,
|
|
78
89
|
runtimeVersion: input.runtimeVersion,
|
|
79
90
|
});
|
|
80
91
|
await writeFileMode(paths.env, renderEnvFile(env), 0o600);
|
|
81
|
-
await writeFileMode(paths.compose, renderComposeFile(), 0o644);
|
|
92
|
+
await writeFileMode(paths.compose, renderComposeFile(input.project), 0o644);
|
|
82
93
|
return { env, host, adminKey, firstRun };
|
|
83
94
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* every request to the Worker endpoint with an Ed25519 private key; the
|
|
4
4
|
* Runtime accepts only requests that verify against the public key.
|
|
5
5
|
*
|
|
6
|
-
* - The private key is a PKCS#8 PEM at `
|
|
6
|
+
* - The private key is a PKCS#8 PEM at `docker/restate-identity.pem` (mode
|
|
7
7
|
* 0600), mounted read-only into the restate service as
|
|
8
8
|
* `RESTATE_WORKER__INVOKER__REQUEST_IDENTITY_PRIVATE_KEY_PEM_FILE`.
|
|
9
9
|
* - The public key goes to the Runtime as `NYLORUN_RESTATE_IDENTITY_KEY`, in
|