nylorun 0.2.1-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.
@@ -1,14 +1,36 @@
1
1
  /**
2
- * The local stack's Compose file (Runtime Architecture §14.3), written to
3
- * `<Host root>/stack/compose.yaml` by `nylorun start`. It is the same for every
4
- * machine: everything that varies (ports, the Postgres password, UID/GID, the
5
- * Host root, the Runtime and Studio images) comes from `stack/.env`.
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
- * Tenants are Postgres schemas, executed through Restate, with their history in
8
- * s2-lite; the Runtime's /ready checks all three.
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).
9
15
  *
10
16
  * Restate signs requests to the Worker endpoint with the private key in
11
- * `stack/restate-identity.pem`, mounted read-only; the Runtime gets the public
17
+ * `docker/restate-identity.pem`, mounted read-only; the Runtime gets the public
12
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.
13
35
  */
14
- export declare function renderComposeFile(): string;
36
+ export declare function renderComposeFile(project: string): string;
@@ -1,28 +1,53 @@
1
1
  import { PINNED_IMAGES } from "./images.js";
2
2
  /**
3
- * The local stack's Compose file (Runtime Architecture §14.3), written to
4
- * `<Host root>/stack/compose.yaml` by `nylorun start`. It is the same for every
5
- * machine: everything that varies (ports, the Postgres password, UID/GID, the
6
- * Host root, the Runtime and Studio images) comes from `stack/.env`.
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
- * Tenants are Postgres schemas, executed through Restate, with their history in
9
- * s2-lite; the Runtime's /ready checks all three.
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).
10
16
  *
11
17
  * Restate signs requests to the Worker endpoint with the private key in
12
- * `stack/restate-identity.pem`, mounted read-only; the Runtime gets the public
18
+ * `docker/restate-identity.pem`, mounted read-only; the Runtime gets the public
13
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.
14
36
  */
15
- export function renderComposeFile() {
37
+ export function renderComposeFile(project) {
16
38
  return `# Written by \`nylorun start\`; rewritten on every start. Settings live in .env.
17
- name: nylorun
39
+ name: ${project}
18
40
 
19
41
  services:
20
42
  postgres: # Session Store
21
43
  image: ${PINNED_IMAGES.postgres}
44
+ # Logical replication feeds the stream relay; a stuck slot is capped at 4 GB of WAL.
45
+ command: ["postgres", "-c", "wal_level=logical", "-c", "max_slot_wal_keep_size=4GB"]
22
46
  environment:
23
47
  POSTGRES_USER: nylorun
24
48
  POSTGRES_PASSWORD: \${NYLORUN_POSTGRES_PASSWORD:?run nylorun start}
25
49
  POSTGRES_DB: nylorun
50
+ POSTGRES_INITDB_ARGS: "--locale=C" # C collation for the whole database
26
51
  volumes:
27
52
  - postgres:/var/lib/postgresql/data
28
53
  healthcheck:
@@ -39,7 +64,7 @@ services:
39
64
  RESTATE_WORKER__INVOKER__REQUEST_IDENTITY_PRIVATE_KEY_PEM_FILE: /run/nylorun/restate-identity.pem
40
65
  volumes:
41
66
  - restate:/restate-data
42
- - \${NYLORUN_HOST_ROOT:?run nylorun start}/stack/restate-identity.pem:/run/nylorun/restate-identity.pem:ro
67
+ - \${NYLORUN_HOST_ROOT:?run nylorun start}/docker/restate-identity.pem:/run/nylorun/restate-identity.pem:ro
43
68
  ports:
44
69
  - "127.0.0.1:\${NYLORUN_RESTATE_PORT:?run nylorun start}:9070" # Restate UI and admin, for debugging
45
70
  healthcheck:
@@ -61,9 +86,37 @@ services:
61
86
  # the Runtime's /ready covers it.
62
87
  restart: unless-stopped
63
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
+
64
117
  runtime:
65
118
  image: \${NYLORUN_RUNTIME_IMAGE:?run nylorun start}
66
- command: ["--role", "all"]
119
+ command: ["--service", "core,loop"]
67
120
  user: "\${NYLORUN_UID:?run nylorun start}:\${NYLORUN_GID:?run nylorun start}"
68
121
  depends_on:
69
122
  postgres: { condition: service_healthy }
@@ -71,12 +124,26 @@ services:
71
124
  s2: { condition: service_started }
72
125
  environment:
73
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}
74
137
  NYLORUN_LISTEN_HOST: 0.0.0.0
75
138
  NYLORUN_LISTEN_PORT: "4000"
76
139
  # Host headers the Runtime accepts: the stack network name, and the
77
140
  # published port as clients on this machine address it.
78
141
  NYLORUN_ALLOWED_HOSTS: runtime:4000,localhost:\${NYLORUN_PORT},127.0.0.1:\${NYLORUN_PORT}
79
142
  NYLORUN_PUBLIC_URL: http://localhost:\${NYLORUN_PORT} # reported by /v1/admin/status
143
+ # The Admin API on its own listener, published on loopback only; Studio reaches it on
144
+ # the stack network. Port 4000 serves the Tenant API alone.
145
+ NYLORUN_ADMIN_LISTEN_PORT: "4001"
146
+ NYLORUN_ADMIN_ALLOWED_HOSTS: runtime:4001,localhost:\${NYLORUN_ADMIN_PORT},127.0.0.1:\${NYLORUN_ADMIN_PORT}
80
147
  NYLORUN_DATABASE_URL: postgres://nylorun:\${NYLORUN_POSTGRES_PASSWORD}@postgres:5432/nylorun
81
148
  NYLORUN_RESTATE_INGRESS_URL: http://restate:8080
82
149
  NYLORUN_RESTATE_ADMIN_URL: http://restate:9070
@@ -85,13 +152,28 @@ services:
85
152
  NYLORUN_S2_ENDPOINT: http://s2:80
86
153
  NYLORUN_S2_TOKEN: ignored # s2-lite has no access tokens yet
87
154
  NYLORUN_WORKSPACE_STORE_URL: file:///workspaces
155
+ # Action endpoints on this machine: \`localhost\` in a registered URL means the Docker host.
156
+ NYLORUN_ENDPOINT_LOOPBACK: docker-host
157
+ extra_hosts:
158
+ host.docker.internal: host-gateway # the Docker host, also on Linux Docker Engine
88
159
  volumes:
89
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
90
171
  - workspaces:/workspaces
91
172
  ports:
92
- - "127.0.0.1:\${NYLORUN_PORT:?run nylorun start}:4000" # Tenant API, Admin API, SSE
93
- healthcheck:
94
- test: ["CMD", "node", "-e", "fetch('http://localhost:4000/ready').then(r=>process.exit(r.ok?0:1),()=>process.exit(1))"]
173
+ - "127.0.0.1:\${NYLORUN_PORT:?run nylorun start}:4000" # Tenant API, SSE, browsers
174
+ - "127.0.0.1:\${NYLORUN_ADMIN_PORT:?run nylorun start}:4001" # Admin API (operators only)
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))"]
95
177
  interval: 2s
96
178
  timeout: 5s
97
179
  retries: 60
@@ -103,11 +185,13 @@ services:
103
185
  depends_on:
104
186
  runtime: { condition: service_healthy }
105
187
  environment:
106
- NYLORUN_RUNTIME_URL: http://runtime:4000
188
+ NYLORUN_RUNTIME_URL: http://runtime:4001 # the operator listener: Admin and Tenant API
107
189
  NYLORUN_ADMIN_KEY_FILE: /run/nylorun/host-credentials.json
108
190
  PORT: "3000"
109
191
  # Studio's Host check accepts localhost/127.0.0.1 on the published port.
110
192
  NYLORUN_STUDIO_PUBLIC_PORT: \${NYLORUN_STUDIO_PORT}
193
+ # Exact origins that may frame Studio (Babai Desktop); kept in .env.
194
+ NYLORUN_STUDIO_FRAME_ANCESTORS: \${NYLORUN_STUDIO_FRAME_ANCESTORS:-}
111
195
  volumes:
112
196
  - \${NYLORUN_HOST_ROOT}/host-credentials.json:/run/nylorun/host-credentials.json:ro
113
197
  ports:
@@ -1,16 +1,25 @@
1
1
  /**
2
- * Settings in `stack/.env` (mode 0600). Ports and the password persist; the
3
- * Restate identity public key is derived from `restate-identity.pem`.
2
+ * Origins that may embed Studio by default: Babai Desktop's `nylorun` scheme on
3
+ * macOS and Linux, and its WebView2 origin on Windows (Studio §8.9).
4
+ */
5
+ export declare const DEFAULT_STUDIO_FRAME_ANCESTORS: readonly ["nylorun://localhost", "http://nylorun.localhost"];
6
+ /**
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`.
4
9
  */
5
10
  export interface StackEnv {
6
- /** Published Runtime port (loopback). */
11
+ /** Published Runtime port (loopback): the Tenant API. */
7
12
  runtimePort: number;
13
+ /** Published operator port (loopback): the Admin API. */
14
+ adminPort: number;
8
15
  /** Published Studio port (loopback). */
9
16
  studioPort: number;
10
17
  /** Published Restate UI and admin port (loopback). */
11
18
  restatePort: number;
12
19
  postgresPassword: string;
13
- /** `publickeyv1_...` of `stack/restate-identity.pem`. */
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`. */
14
23
  restateIdentityKey: string;
15
24
  uid: number;
16
25
  gid: number;
@@ -18,15 +27,34 @@ export interface StackEnv {
18
27
  hostRoot: string;
19
28
  runtimeImage: string;
20
29
  studioImage: string;
30
+ /** Exact origins that may frame Studio, separated by spaces (persists). */
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;
21
39
  }
22
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[];
23
46
  /** Parse KEY=value lines; comments and blank lines are skipped. */
24
47
  export declare function parseEnvLines(text: string): Map<string, string>;
25
48
  /** The settings that persist across starts, from an existing .env. */
26
49
  export interface PersistedStackEnv {
27
50
  runtimePort?: number;
51
+ adminPort?: number;
28
52
  studioPort?: number;
29
53
  restatePort?: number;
30
54
  postgresPassword?: string;
55
+ gatesToken?: string;
56
+ /** Validated origins; absent when the line is missing (an older .env). */
57
+ studioFrameAncestors?: string[];
58
+ derivedPrincipals?: string[];
31
59
  }
32
60
  export declare function parsePersisted(text: string): PersistedStackEnv;
@@ -1,15 +1,30 @@
1
+ import { DERIVED_PRINCIPAL_ID_PATTERN } from "@nylorun/core/compatibility";
2
+ import { parseFrameAncestors } from "@nylorun/core/contracts";
1
3
  import { CliError } from "../errors.js";
4
+ /**
5
+ * Origins that may embed Studio by default: Babai Desktop's `nylorun` scheme on
6
+ * macOS and Linux, and its WebView2 origin on Windows (Studio §8.9).
7
+ */
8
+ export const DEFAULT_STUDIO_FRAME_ANCESTORS = [
9
+ "nylorun://localhost",
10
+ "http://nylorun.localhost",
11
+ ];
2
12
  const KEYS = {
3
13
  runtimePort: "NYLORUN_PORT",
14
+ adminPort: "NYLORUN_ADMIN_PORT",
4
15
  studioPort: "NYLORUN_STUDIO_PORT",
5
16
  restatePort: "NYLORUN_RESTATE_PORT",
6
17
  postgresPassword: "NYLORUN_POSTGRES_PASSWORD",
18
+ gatesToken: "NYLORUN_GATES_TOKEN",
7
19
  restateIdentityKey: "NYLORUN_RESTATE_IDENTITY_KEY",
8
20
  uid: "NYLORUN_UID",
9
21
  gid: "NYLORUN_GID",
10
22
  hostRoot: "NYLORUN_HOST_ROOT",
11
23
  runtimeImage: "NYLORUN_RUNTIME_IMAGE",
12
24
  studioImage: "NYLORUN_STUDIO_IMAGE",
25
+ studioFrameAncestors: "NYLORUN_STUDIO_FRAME_ANCESTORS",
26
+ stackName: "NYLORUN_STACK_NAME",
27
+ derivedPrincipals: "NYLORUN_DERIVED_PRINCIPALS",
13
28
  };
14
29
  /** Compose .env values: single quotes keep a value literal (no interpolation). */
15
30
  function quote(key, value) {
@@ -28,17 +43,22 @@ function unquote(value) {
28
43
  export function renderEnvFile(env) {
29
44
  const line = (field) => `${KEYS[field]}=${quote(KEYS[field], String(env[field]))}`;
30
45
  return [
31
- "# Written by `nylorun start`. Mode 0600: holds the Postgres password.",
32
- "# Ports and the password are kept across starts; images, UID/GID and the",
33
- "# 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.",
34
49
  "",
35
50
  "# Published on 127.0.0.1; clients use http://localhost:<port>.",
36
51
  line("runtimePort"),
52
+ line("adminPort"),
37
53
  line("studioPort"),
38
54
  line("restatePort"),
39
55
  "",
40
56
  line("postgresPassword"),
41
57
  "",
58
+ "# The runtime presents this token to the gateway (the Model Gate) with every",
59
+ "# model call.",
60
+ line("gatesToken"),
61
+ "",
42
62
  "# Restate signs requests to the Runtime's Worker endpoint with the private",
43
63
  "# key in restate-identity.pem; the Runtime accepts only this public key.",
44
64
  line("restateIdentityKey"),
@@ -52,8 +72,32 @@ export function renderEnvFile(env) {
52
72
  line("runtimeImage"),
53
73
  line("studioImage"),
54
74
  "",
75
+ "# Exact origins that may show Studio in a frame (Babai Desktop). Kept across",
76
+ "# starts; change with nylorun start --studio-embed-origin <origin>.",
77
+ line("studioFrameAncestors"),
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
+ "",
55
85
  ].join("\n");
56
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
+ }
57
101
  /** Parse KEY=value lines; comments and blank lines are skipped. */
58
102
  export function parseEnvLines(text) {
59
103
  const values = new Map();
@@ -80,6 +124,9 @@ export function parsePersisted(text) {
80
124
  const runtimePort = port(values.get(KEYS.runtimePort));
81
125
  if (runtimePort)
82
126
  out.runtimePort = runtimePort;
127
+ const adminPort = port(values.get(KEYS.adminPort));
128
+ if (adminPort)
129
+ out.adminPort = adminPort;
83
130
  const studioPort = port(values.get(KEYS.studioPort));
84
131
  if (studioPort)
85
132
  out.studioPort = studioPort;
@@ -89,5 +136,26 @@ export function parsePersisted(text) {
89
136
  const password = values.get(KEYS.postgresPassword);
90
137
  if (password && /^[A-Za-z0-9]{16,}$/.test(password))
91
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
+ }
151
+ const ancestors = values.get(KEYS.studioFrameAncestors);
152
+ if (ancestors !== undefined) {
153
+ try {
154
+ out.studioFrameAncestors = parseFrameAncestors(ancestors);
155
+ }
156
+ catch (error) {
157
+ throw new CliError(`${KEYS.studioFrameAncestors} in docker/.env: ${error instanceof Error ? error.message : String(error)}`, 1);
158
+ }
159
+ }
92
160
  return out;
93
161
  }
@@ -13,6 +13,8 @@ export interface HostConfigFile {
13
13
  hostId: string;
14
14
  host: string;
15
15
  port: number;
16
+ /** The operator listener's published port (the Admin API); absent on older stacks. */
17
+ adminPort?: number;
16
18
  runtimeVersion?: string;
17
19
  [field: string]: unknown;
18
20
  }
@@ -21,14 +23,25 @@ export declare function newHostId(): string;
21
23
  export declare function ensureHostLayout(paths: StackPaths): Promise<void>;
22
24
  export declare function readHostConfig(paths: StackPaths): Promise<Record<string, unknown> | undefined>;
23
25
  /**
24
- * Write host.json with the published port. Keeps the hostId and unknown fields
25
- * of an existing file; refuses a newer format.
26
+ * Write host.json with the published port and the Runtime version that runs
27
+ * the Host. Keeps the hostId and unknown fields of an existing file, and its
28
+ * runtimeVersion when `runtimeVersion` is undefined; refuses a newer format.
26
29
  */
27
30
  export declare function writeStackHostConfig(paths: StackPaths, input: {
28
31
  port: number;
29
- runtimeVersion: string;
32
+ adminPort?: number;
33
+ runtimeVersion: string | undefined;
30
34
  }): Promise<HostConfigFile>;
31
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
+ }>;
32
45
  /** Create host-credentials.json (mode 0600) if missing; always leave it 0600. */
33
46
  export declare function ensureHostCredentials(paths: StackPaths): Promise<{
34
47
  adminKey: string;
@@ -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.tenants, paths.stack])
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) {
@@ -46,8 +48,9 @@ export async function readHostConfig(paths) {
46
48
  }
47
49
  }
48
50
  /**
49
- * Write host.json with the published port. Keeps the hostId and unknown fields
50
- * of an existing file; refuses a newer format.
51
+ * Write host.json with the published port and the Runtime version that runs
52
+ * the Host. Keeps the hostId and unknown fields of an existing file, and its
53
+ * runtimeVersion when `runtimeVersion` is undefined; refuses a newer format.
51
54
  */
52
55
  export async function writeStackHostConfig(paths, input) {
53
56
  const existing = (await readHostConfig(paths)) ?? {};
@@ -64,7 +67,8 @@ export async function writeStackHostConfig(paths, input) {
64
67
  hostId,
65
68
  host: STACK_CLIENT_HOST,
66
69
  port: input.port,
67
- runtimeVersion: input.runtimeVersion,
70
+ ...(input.adminPort === undefined ? {} : { adminPort: input.adminPort }),
71
+ ...(input.runtimeVersion !== undefined ? { runtimeVersion: input.runtimeVersion } : {}),
68
72
  };
69
73
  await writeAtomic(paths.config, `${JSON.stringify(config, null, 2)}\n`);
70
74
  return config;
@@ -80,6 +84,27 @@ export async function readAdminKey(paths) {
80
84
  return undefined;
81
85
  }
82
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
+ }
83
108
  /** Create host-credentials.json (mode 0600) if missing; always leave it 0600. */
84
109
  export async function ensureHostCredentials(paths) {
85
110
  const existing = await readAdminKey(paths);
@@ -16,6 +16,8 @@ export interface StackImages {
16
16
  restate: string;
17
17
  s2: string;
18
18
  }
19
+ /** `NYLORUN_RUNTIME_IMAGE` names the Runtime image, so its version is unknown here. */
20
+ export declare function runtimeImageOverridden(env: Readonly<Record<string, string | undefined>>): boolean;
19
21
  export declare function stackImages(env: Readonly<Record<string, string | undefined>>, versions?: {
20
22
  runtime: string;
21
23
  studio: string;
@@ -10,18 +10,22 @@ export const PINNED_IMAGES = {
10
10
  restate: "docker.restate.dev/restatedev/restate:1.7.12",
11
11
  s2: "ghcr.io/s2-streamstore/s2:0.43.0",
12
12
  };
13
+ function override(env, name) {
14
+ const value = env[name]?.trim();
15
+ return value ? value : undefined;
16
+ }
17
+ /** `NYLORUN_RUNTIME_IMAGE` names the Runtime image, so its version is unknown here. */
18
+ export function runtimeImageOverridden(env) {
19
+ return override(env, "NYLORUN_RUNTIME_IMAGE") !== undefined;
20
+ }
13
21
  export function stackImages(env, versions = {
14
22
  runtime: pinnedVersion("runtime"),
15
23
  studio: pinnedVersion("studio"),
16
24
  }) {
17
- const override = (name) => {
18
- const value = env[name]?.trim();
19
- return value ? value : undefined;
20
- };
21
25
  return {
22
- runtime: override("NYLORUN_RUNTIME_IMAGE") ??
26
+ runtime: override(env, "NYLORUN_RUNTIME_IMAGE") ??
23
27
  `ghcr.io/nylorun/runtime:${versions.runtime}`,
24
- studio: override("NYLORUN_STUDIO_IMAGE") ??
28
+ studio: override(env, "NYLORUN_STUDIO_IMAGE") ??
25
29
  `ghcr.io/nylorun/studio:${versions.studio}`,
26
30
  ...PINNED_IMAGES,
27
31
  };
@@ -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` (a Tenant page) when given. */
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>;
@@ -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` (a Tenant page) when given. */
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
  }
@@ -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` or `NYLORUN_HOME`), bind-mounted at /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
- tenants: string;
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
- stack: string;
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. */