nylorun 0.4.0-beta → 0.6.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +207 -105
  3. package/dist/cli.js +24 -22
  4. package/dist/doctor.d.ts +10 -8
  5. package/dist/doctor.js +54 -37
  6. package/dist/home.d.ts +1 -1
  7. package/dist/home.js +1 -1
  8. package/dist/project/derived-key.d.ts +2 -2
  9. package/dist/project/derived-key.js +2 -2
  10. package/dist/project/link.d.ts +7 -7
  11. package/dist/project/link.js +3 -3
  12. package/dist/stack/commands.d.ts +23 -15
  13. package/dist/stack/commands.js +332 -214
  14. package/dist/stack/compose-file.d.ts +11 -8
  15. package/dist/stack/compose-file.js +54 -21
  16. package/dist/stack/docker.d.ts +10 -0
  17. package/dist/stack/docker.js +64 -1
  18. package/dist/stack/env-file.d.ts +12 -2
  19. package/dist/stack/env-file.js +13 -3
  20. package/dist/stack/host-files.d.ts +4 -4
  21. package/dist/stack/host-files.js +3 -3
  22. package/dist/stack/images.d.ts +4 -2
  23. package/dist/stack/images.js +3 -2
  24. package/dist/stack/index.d.ts +4 -3
  25. package/dist/stack/index.js +10 -4
  26. package/dist/stack/paths.d.ts +3 -3
  27. package/dist/stack/paths.js +1 -1
  28. package/dist/stack/ports.d.ts +4 -2
  29. package/dist/stack/ports.js +3 -1
  30. package/dist/stack/prepare.d.ts +7 -3
  31. package/dist/stack/prepare.js +5 -3
  32. package/dist/stack/proxy.d.ts +78 -0
  33. package/dist/stack/proxy.js +224 -0
  34. package/dist/stack/stacks.d.ts +37 -34
  35. package/dist/stack/stacks.js +91 -57
  36. package/dist/stack/studio-login.d.ts +4 -3
  37. package/dist/stack/studio-login.js +5 -4
  38. package/dist/stack/versions.d.ts +1 -1
  39. package/dist/stack/versions.js +1 -1
  40. package/dist/telemetry.d.ts +31 -0
  41. package/dist/telemetry.js +79 -0
  42. package/package.json +5 -5
@@ -1,16 +1,19 @@
1
1
  /**
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`.
2
+ * A local Tenant's Compose file (Runtime Architecture §14.3), written to
3
+ * `<Host root>/docker/compose.yaml` by `nylorun start`. Each local Tenant is one installation
4
+ * (tenancy.md §6): its own Compose project (`nylorun-<name>`, the file's `name`), volumes,
5
+ * network and ports. Container, network and volume names are global on the Docker engine, so
6
+ * every one is `<project>-<role>` (`nylorun-shop-studio`, network `nylorun-shop`, volume
7
+ * `nylorun-shop-postgres`), labelled `dev.nylorun.tenant: <name>`; they are written here, from
8
+ * the project and the Tenant's name. Everything else that varies (ports, the Postgres password,
9
+ * UID/GID, the Host root, the images, Studio's public origins) comes from `docker/.env`.
7
10
  *
8
- * The Tenant's state is the stack's Postgres database, executed through Restate, with its
11
+ * The Tenant's state is its Postgres database, executed through Restate, with its
9
12
  * history in s2-lite; the Runtime's /ready checks all three. Postgres initialises the
10
13
  * database with C collation (`--locale=C`) and runs with `wal_level=logical`: the stream
11
14
  * relay feeds s2-lite from the record over logical replication (Durable Streams), and
12
15
  * `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
16
+ * Tenant on its first start, with its name (`NYLORUN_TENANT_NAME`) and the derived
14
17
  * principals of `NYLORUN_DERIVED_PRINCIPALS` (`project` for the Project link).
15
18
  *
16
19
  * Restate signs requests to the Worker endpoint with the private key in
@@ -33,4 +36,4 @@
33
36
  * it is down, model and MCP calls fail, deliveries are retried, vault writes and
34
37
  * token minting answer 503, and the session takes the next message.
35
38
  */
36
- export declare function renderComposeFile(project: string): string;
39
+ export declare function renderComposeFile(project: string, name: string): string;
@@ -1,17 +1,20 @@
1
1
  import { PINNED_IMAGES } from "./images.js";
2
2
  /**
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`.
3
+ * A local Tenant's Compose file (Runtime Architecture §14.3), written to
4
+ * `<Host root>/docker/compose.yaml` by `nylorun start`. Each local Tenant is one installation
5
+ * (tenancy.md §6): its own Compose project (`nylorun-<name>`, the file's `name`), volumes,
6
+ * network and ports. Container, network and volume names are global on the Docker engine, so
7
+ * every one is `<project>-<role>` (`nylorun-shop-studio`, network `nylorun-shop`, volume
8
+ * `nylorun-shop-postgres`), labelled `dev.nylorun.tenant: <name>`; they are written here, from
9
+ * the project and the Tenant's name. Everything else that varies (ports, the Postgres password,
10
+ * UID/GID, the Host root, the images, Studio's public origins) comes from `docker/.env`.
8
11
  *
9
- * The Tenant's state is the stack's Postgres database, executed through Restate, with its
12
+ * The Tenant's state is its Postgres database, executed through Restate, with its
10
13
  * history in s2-lite; the Runtime's /ready checks all three. Postgres initialises the
11
14
  * database with C collation (`--locale=C`) and runs with `wal_level=logical`: the stream
12
15
  * relay feeds s2-lite from the record over logical replication (Durable Streams), and
13
16
  * `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
17
+ * Tenant on its first start, with its name (`NYLORUN_TENANT_NAME`) and the derived
15
18
  * principals of `NYLORUN_DERIVED_PRINCIPALS` (`project` for the Project link).
16
19
  *
17
20
  * Restate signs requests to the Worker endpoint with the private key in
@@ -34,13 +37,18 @@ import { PINNED_IMAGES } from "./images.js";
34
37
  * it is down, model and MCP calls fail, deliveries are retried, vault writes and
35
38
  * token minting answer 503, and the session takes the next message.
36
39
  */
37
- export function renderComposeFile(project) {
40
+ export function renderComposeFile(project, name) {
38
41
  return `# Written by \`nylorun start\`; rewritten on every start. Settings live in .env.
39
42
  name: ${project}
40
43
 
44
+ x-tenant: &tenant
45
+ dev.nylorun.tenant: "${name}"
46
+
41
47
  services:
42
48
  postgres: # Session Store
43
49
  image: ${PINNED_IMAGES.postgres}
50
+ container_name: ${project}-postgres
51
+ labels: *tenant
44
52
  # Logical replication feeds the stream relay; a stuck slot is capped at 4 GB of WAL.
45
53
  command: ["postgres", "-c", "wal_level=logical", "-c", "max_slot_wal_keep_size=4GB"]
46
54
  environment:
@@ -59,9 +67,14 @@ services:
59
67
 
60
68
  restate: # Durable Session Execution
61
69
  image: ${PINNED_IMAGES.restate}
70
+ container_name: ${project}-restate
71
+ labels: *tenant
62
72
  command: ["--node-name=restate-1"] # stable name, so data is found on restart
63
73
  environment:
64
74
  RESTATE_WORKER__INVOKER__REQUEST_IDENTITY_PRIVATE_KEY_PEM_FILE: /run/nylorun/restate-identity.pem
75
+ # RocksDB's memory (block cache and memtables) defaults to 2 GiB; a local Tenant needs a
76
+ # fraction of it, and several Tenants share a laptop.
77
+ RESTATE_ROCKSDB_TOTAL_MEMORY_SIZE: 256MiB
65
78
  volumes:
66
79
  - restate:/restate-data
67
80
  - \${NYLORUN_HOST_ROOT:?run nylorun start}/docker/restate-identity.pem:/run/nylorun/restate-identity.pem:ro
@@ -74,20 +87,24 @@ services:
74
87
  retries: 30
75
88
  restart: unless-stopped
76
89
 
77
- s2: # Durable Streams; not published, only the Runtime reaches it
90
+ s2-lite: # Durable Streams; not published, only the Runtime reaches it
78
91
  image: ${PINNED_IMAGES.s2}
92
+ container_name: ${project}-s2-lite
93
+ labels: *tenant
79
94
  command: ["lite", "--local-root", "/home/nonroot/data"] # local disk; listens on port 80
80
95
  # The image runs as uid 65532. Docker fills a new volume with the image's
81
96
  # /home/nonroot, owned by that user; a volume at a path the image lacks
82
97
  # (e.g. /data) is root-owned and s2-lite cannot write it.
83
98
  volumes:
84
- - s2:/home/nonroot
99
+ - s2-lite:/home/nonroot
85
100
  # The s2 image has no shell or HTTP client, so it has no health check;
86
101
  # the Runtime's /ready covers it.
87
102
  restart: unless-stopped
88
103
 
89
104
  gateway: # the Model and Tool Gates and keys: the vault key, credentials, signing, outbound calls; not published
90
105
  image: \${NYLORUN_RUNTIME_IMAGE:?run nylorun start}
106
+ container_name: ${project}-gateway
107
+ labels: *tenant
91
108
  command: ["--service", "gates,keys"]
92
109
  user: "\${NYLORUN_UID:?run nylorun start}:\${NYLORUN_GID:?run nylorun start}"
93
110
  depends_on:
@@ -116,17 +133,19 @@ services:
116
133
 
117
134
  runtime:
118
135
  image: \${NYLORUN_RUNTIME_IMAGE:?run nylorun start}
136
+ container_name: ${project}-runtime
137
+ labels: *tenant
119
138
  command: ["--service", "core,loop"]
120
139
  user: "\${NYLORUN_UID:?run nylorun start}:\${NYLORUN_GID:?run nylorun start}"
121
140
  depends_on:
122
141
  postgres: { condition: service_healthy }
123
142
  restate: { condition: service_healthy }
124
- s2: { condition: service_started }
143
+ s2-lite: { condition: service_started }
125
144
  environment:
126
145
  NYLORUN_HOME: /nylorun
127
146
  NYLORUN_PACKING: combined
128
147
  # The Tenant the Runtime creates on its first start (later starts open it).
129
- NYLORUN_TENANT_NAME: \${NYLORUN_STACK_NAME:?run nylorun start}
148
+ NYLORUN_TENANT_NAME: \${NYLORUN_TENANT_NAME:?run nylorun start}
130
149
  NYLORUN_DERIVED_PRINCIPALS: \${NYLORUN_DERIVED_PRINCIPALS:-project}
131
150
  # Model calls, remote MCP calls and deliveries go through the gateway, and vault writes
132
151
  # and token signing through its keys service: this container never reads a credential
@@ -136,12 +155,12 @@ services:
136
155
  NYLORUN_GATES_TOKEN: \${NYLORUN_GATES_TOKEN:?run nylorun start}
137
156
  NYLORUN_LISTEN_HOST: 0.0.0.0
138
157
  NYLORUN_LISTEN_PORT: "4000"
139
- # Host headers the Runtime accepts: the stack network name, and the
158
+ # Host headers the Runtime accepts: its name on the Compose network, and the
140
159
  # published port as clients on this machine address it.
141
160
  NYLORUN_ALLOWED_HOSTS: runtime:4000,localhost:\${NYLORUN_PORT},127.0.0.1:\${NYLORUN_PORT}
142
161
  NYLORUN_PUBLIC_URL: http://localhost:\${NYLORUN_PORT} # reported by /v1/admin/status
143
162
  # 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.
163
+ # the Compose network. Port 4000 serves the Tenant API alone.
145
164
  NYLORUN_ADMIN_LISTEN_PORT: "4001"
146
165
  NYLORUN_ADMIN_ALLOWED_HOSTS: runtime:4001,localhost:\${NYLORUN_ADMIN_PORT},127.0.0.1:\${NYLORUN_ADMIN_PORT}
147
166
  NYLORUN_DATABASE_URL: postgres://nylorun:\${NYLORUN_POSTGRES_PASSWORD}@postgres:5432/nylorun
@@ -149,7 +168,7 @@ services:
149
168
  NYLORUN_RESTATE_ADMIN_URL: http://restate:9070
150
169
  NYLORUN_WORKER_URL: http://runtime:9080 # registered with Restate; not published
151
170
  NYLORUN_RESTATE_IDENTITY_KEY: \${NYLORUN_RESTATE_IDENTITY_KEY:?run nylorun start}
152
- NYLORUN_S2_ENDPOINT: http://s2:80
171
+ NYLORUN_S2_ENDPOINT: http://s2-lite:80
153
172
  NYLORUN_S2_TOKEN: ignored # s2-lite has no access tokens yet
154
173
  NYLORUN_WORKSPACE_STORE_URL: file:///workspaces
155
174
  # Action endpoints on this machine: \`localhost\` in a registered URL means the Docker host.
@@ -158,7 +177,7 @@ services:
158
177
  host.docker.internal: host-gateway # the Docker host, also on Linux Docker Engine
159
178
  volumes:
160
179
  - \${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/):
180
+ # Empty and read-only over the vault key (keys/) and the Compose secrets (docker/):
162
181
  # only the gateway reads the key, and only Restate its private key.
163
182
  - type: tmpfs
164
183
  target: /nylorun/keys
@@ -181,6 +200,8 @@ services:
181
200
 
182
201
  studio: # dashboard + trusted proxy
183
202
  image: \${NYLORUN_STUDIO_IMAGE:?run nylorun start}
203
+ container_name: ${project}-studio
204
+ labels: *tenant
184
205
  user: "\${NYLORUN_UID}:\${NYLORUN_GID}" # reads the 0600 admin key file
185
206
  depends_on:
186
207
  runtime: { condition: service_healthy }
@@ -188,10 +209,17 @@ services:
188
209
  NYLORUN_RUNTIME_URL: http://runtime:4001 # the operator listener: Admin and Tenant API
189
210
  NYLORUN_ADMIN_KEY_FILE: /run/nylorun/host-credentials.json
190
211
  PORT: "3000"
191
- # Studio's Host check accepts localhost/127.0.0.1 on the published port.
212
+ # Studio's Host check accepts localhost/127.0.0.1 on the published port, and the
213
+ # proxy's origin (http://<name>.localhost:<port>), kept in .env.
192
214
  NYLORUN_STUDIO_PUBLIC_PORT: \${NYLORUN_STUDIO_PORT}
215
+ NYLORUN_STUDIO_PUBLIC_ORIGINS: \${NYLORUN_STUDIO_PUBLIC_ORIGINS:-}
216
+ # Browsers share cookies across ports of one host: a cookie per Tenant keeps the
217
+ # sessions of two Studios on localhost apart.
218
+ NYLORUN_STUDIO_SESSION_COOKIE: nylorun_studio_${name}
193
219
  # Exact origins that may frame Studio (Babai Desktop); kept in .env.
194
220
  NYLORUN_STUDIO_FRAME_ANCESTORS: \${NYLORUN_STUDIO_FRAME_ANCESTORS:-}
221
+ # Studio's anonymous usage analytics; empty when telemetry is off.
222
+ NYLORUN_STUDIO_ANALYTICS_ID: \${NYLORUN_STUDIO_ANALYTICS_ID:-}
195
223
  volumes:
196
224
  - \${NYLORUN_HOST_ROOT}/host-credentials.json:/run/nylorun/host-credentials.json:ro
197
225
  ports:
@@ -203,10 +231,15 @@ services:
203
231
  retries: 30
204
232
  restart: unless-stopped
205
233
 
234
+ networks:
235
+ default:
236
+ name: ${project}
237
+ labels: *tenant
238
+
206
239
  volumes:
207
- postgres: {}
208
- restate: {}
209
- s2: {}
210
- workspaces: {}
240
+ postgres: { name: ${project}-postgres, labels: *tenant }
241
+ restate: { name: ${project}-restate, labels: *tenant }
242
+ s2-lite: { name: ${project}-s2-lite, labels: *tenant }
243
+ workspaces: { name: ${project}-workspaces, labels: *tenant }
211
244
  `;
212
245
  }
@@ -36,3 +36,13 @@ export interface ComposeService {
36
36
  }
37
37
  /** Parse `docker compose ps --format json` (a JSON array, or one object per line). */
38
38
  export declare function parseComposePs(stdout: string): ComposeService[];
39
+ /** `123.4MiB` (from `docker stats` MemUsage) in bytes; undefined when unreadable. */
40
+ export declare function parseByteSize(text: string): number | undefined;
41
+ /** `812 MB`, `2.1 GB`. */
42
+ export declare function formatBytes(bytes: number): string;
43
+ /**
44
+ * Memory in use per Tenant: the sum over its running containers, found by the label
45
+ * `dev.nylorun.tenant`, from one `docker stats --no-stream`. Undefined when Docker does not
46
+ * answer; a Tenant with no running container is absent.
47
+ */
48
+ export declare function tenantMemory(docker: DockerRunner): Promise<Map<string, number> | undefined>;
@@ -45,7 +45,7 @@ export async function checkDocker(docker) {
45
45
  return {
46
46
  docker: {
47
47
  ok: false,
48
- problem: "Docker is required for the Nylorun stack, and the docker command was not found. Install Docker Desktop, OrbStack, Colima or another Docker engine with Compose v2, then retry.",
48
+ problem: "Docker is required for local Nylorun Tenants, and the docker command was not found. Install Docker Desktop, OrbStack, Colima or another Docker engine with Compose v2, then retry.",
49
49
  },
50
50
  };
51
51
  if (server.code !== 0)
@@ -99,3 +99,66 @@ export function parseComposePs(stdout) {
99
99
  };
100
100
  });
101
101
  }
102
+ const UNITS = {
103
+ b: 1,
104
+ kb: 1e3,
105
+ mb: 1e6,
106
+ gb: 1e9,
107
+ tb: 1e12,
108
+ kib: 2 ** 10,
109
+ mib: 2 ** 20,
110
+ gib: 2 ** 30,
111
+ tib: 2 ** 40,
112
+ };
113
+ /** `123.4MiB` (from `docker stats` MemUsage) in bytes; undefined when unreadable. */
114
+ export function parseByteSize(text) {
115
+ const match = /^([\d.]+)\s*([a-z]+)$/i.exec(text.trim());
116
+ const unit = match ? UNITS[match[2].toLowerCase()] : undefined;
117
+ return match && unit ? Number(match[1]) * unit : undefined;
118
+ }
119
+ /** `812 MB`, `2.1 GB`. */
120
+ export function formatBytes(bytes) {
121
+ return bytes >= 1e9 ? `${(bytes / 1e9).toFixed(1)} GB` : `${Math.round(bytes / 1e6)} MB`;
122
+ }
123
+ /**
124
+ * Memory in use per Tenant: the sum over its running containers, found by the label
125
+ * `dev.nylorun.tenant`, from one `docker stats --no-stream`. Undefined when Docker does not
126
+ * answer; a Tenant with no running container is absent.
127
+ */
128
+ export async function tenantMemory(docker) {
129
+ const ps = await docker.run([
130
+ "ps",
131
+ "--filter",
132
+ "label=dev.nylorun.tenant",
133
+ "--format",
134
+ '{{.Names}}\t{{.Label "dev.nylorun.tenant"}}',
135
+ ]);
136
+ if (ps.code !== 0)
137
+ return undefined;
138
+ const owners = new Map(ps.stdout
139
+ .split(/\r?\n/)
140
+ .map((line) => line.trim().split("\t"))
141
+ .filter((cells) => cells.length === 2 && cells[1] !== ""));
142
+ const memory = new Map();
143
+ if (owners.size === 0)
144
+ return memory;
145
+ const stats = await docker.run([
146
+ "stats",
147
+ "--no-stream",
148
+ "--format",
149
+ "{{.Name}}\t{{.MemUsage}}",
150
+ ...owners.keys(),
151
+ ]);
152
+ if (stats.code !== 0)
153
+ return undefined;
154
+ for (const line of stats.stdout.split(/\r?\n/)) {
155
+ const [name, usage] = line.trim().split("\t");
156
+ const tenant = name ? owners.get(name) : undefined;
157
+ const bytes = usage ? parseByteSize(usage.split("/")[0]) : undefined;
158
+ if (tenant && bytes !== undefined)
159
+ memory.set(tenant, (memory.get(tenant) ?? 0) + bytes);
160
+ }
161
+ for (const [tenant, bytes] of memory)
162
+ memory.set(tenant, Math.round(bytes));
163
+ return memory;
164
+ }
@@ -29,8 +29,18 @@ export interface StackEnv {
29
29
  studioImage: string;
30
30
  /** Exact origins that may frame Studio, separated by spaces (persists). */
31
31
  studioFrameAncestors: string;
32
- /** The stack's name: the name of the Tenant its Runtime creates. */
33
- stackName: string;
32
+ /**
33
+ * The Google Analytics measurement id Studio reports page views to, or empty
34
+ * when telemetry is off. Decided again on every start.
35
+ */
36
+ studioAnalyticsId: string;
37
+ /**
38
+ * Origins Studio also serves besides localhost and 127.0.0.1 on its port, separated by
39
+ * spaces: the proxy's `http://<name>.localhost:<port>`, or empty. Decided on every start.
40
+ */
41
+ studioPublicOrigins: string;
42
+ /** The Tenant's name, which the Runtime gives the Tenant it creates. */
43
+ tenantName: string;
34
44
  /**
35
45
  * Derived principals the Runtime registers on its Tenant, comma-separated (persists);
36
46
  * always includes `project`, the Project link's principal.
@@ -23,7 +23,9 @@ const KEYS = {
23
23
  runtimeImage: "NYLORUN_RUNTIME_IMAGE",
24
24
  studioImage: "NYLORUN_STUDIO_IMAGE",
25
25
  studioFrameAncestors: "NYLORUN_STUDIO_FRAME_ANCESTORS",
26
- stackName: "NYLORUN_STACK_NAME",
26
+ studioAnalyticsId: "NYLORUN_STUDIO_ANALYTICS_ID",
27
+ studioPublicOrigins: "NYLORUN_STUDIO_PUBLIC_ORIGINS",
28
+ tenantName: "NYLORUN_TENANT_NAME",
27
29
  derivedPrincipals: "NYLORUN_DERIVED_PRINCIPALS",
28
30
  };
29
31
  /** Compose .env values: single quotes keep a value literal (no interpolation). */
@@ -76,10 +78,18 @@ export function renderEnvFile(env) {
76
78
  "# starts; change with nylorun start --studio-embed-origin <origin>.",
77
79
  line("studioFrameAncestors"),
78
80
  "",
79
- "# The stack's name, which its Runtime gives the Tenant it creates on the first",
81
+ "# Studio's anonymous usage analytics; empty when off. Set on every start: turn",
82
+ "# it off with nylorun telemetry disable or NYLORUN_TELEMETRY_DISABLED=1.",
83
+ line("studioAnalyticsId"),
84
+ "",
85
+ "# Studio's address through the proxy (http://<name>.localhost:<port>); empty",
86
+ "# without the proxy. Set on every start.",
87
+ line("studioPublicOrigins"),
88
+ "",
89
+ "# The Tenant's name, which the Runtime gives the Tenant it creates on the first",
80
90
  "# start, and the derived principals it registers on that Tenant (keys derived",
81
91
  "# from the admin key; `project` is the Project link's).",
82
- line("stackName"),
92
+ line("tenantName"),
83
93
  line("derivedPrincipals"),
84
94
  "",
85
95
  ].join("\n");
@@ -3,17 +3,17 @@ import type { StackPaths } from "./paths.js";
3
3
  * host.json and host-credentials.json (format 1), which `@nylorun/admin`,
4
4
  * Studio and the Runtime container all read.
5
5
  *
6
- * For the stack, host.json names the client-facing address: `localhost` and
6
+ * For a local Tenant, host.json names the client-facing address: `localhost` and
7
7
  * the published port. The container binds 0.0.0.0:4000 on its own.
8
8
  */
9
- /** Host name clients use for the stack. Studio's cookie is set on this name too. */
9
+ /** Host name clients use for a local Tenant. Studio's cookie is set on this name too. */
10
10
  export declare const STACK_CLIENT_HOST = "localhost";
11
11
  export interface HostConfigFile {
12
12
  format: 1;
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. */
16
+ /** The operator listener's published port (the Admin API); absent on older Host roots. */
17
17
  adminPort?: number;
18
18
  runtimeVersion?: string;
19
19
  [field: string]: unknown;
@@ -36,7 +36,7 @@ export declare function readAdminKey(paths: StackPaths): Promise<string | undefi
36
36
  /**
37
37
  * The Tenant's vault key in `keys/vault-kek` (32 random bytes, base64, mode 0600), which only the
38
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.
39
+ * Host root before F4.2 kept it. The gateway never creates it.
40
40
  */
41
41
  export declare function ensureVaultKey(paths: StackPaths): Promise<{
42
42
  created: boolean;
@@ -8,10 +8,10 @@ import { CliError } from "../errors.js";
8
8
  * host.json and host-credentials.json (format 1), which `@nylorun/admin`,
9
9
  * Studio and the Runtime container all read.
10
10
  *
11
- * For the stack, host.json names the client-facing address: `localhost` and
11
+ * For a local Tenant, host.json names the client-facing address: `localhost` and
12
12
  * the published port. The container binds 0.0.0.0:4000 on its own.
13
13
  */
14
- /** Host name clients use for the stack. Studio's cookie is set on this name too. */
14
+ /** Host name clients use for a local Tenant. Studio's cookie is set on this name too. */
15
15
  export const STACK_CLIENT_HOST = "localhost";
16
16
  const KNOWN_FORMAT = 1;
17
17
  const ADMIN_KEY_PATTERN = /^[0-9a-f]{64}$/;
@@ -87,7 +87,7 @@ export async function readAdminKey(paths) {
87
87
  /**
88
88
  * The Tenant's vault key in `keys/vault-kek` (32 random bytes, base64, mode 0600), which only the
89
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.
90
+ * Host root before F4.2 kept it. The gateway never creates it.
91
91
  */
92
92
  export async function ensureVaultKey(paths) {
93
93
  await mkdir(paths.keys, { recursive: true, mode: 0o700 });
@@ -1,13 +1,14 @@
1
1
  /**
2
- * Images the local stack runs. The Runtime and Studio tags are pinned by this
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
4
  * (local builds, CI images). Postgres, Restate and s2-lite are the official
5
- * images, pinned here.
5
+ * images, pinned here, as is Caddy for the machine's Studio proxy.
6
6
  */
7
7
  export declare const PINNED_IMAGES: {
8
8
  readonly postgres: "postgres:17.11";
9
9
  readonly restate: "docker.restate.dev/restatedev/restate:1.7.12";
10
10
  readonly s2: "ghcr.io/s2-streamstore/s2:0.43.0";
11
+ readonly proxy: "caddy:2.11.6";
11
12
  };
12
13
  export interface StackImages {
13
14
  runtime: string;
@@ -15,6 +16,7 @@ export interface StackImages {
15
16
  postgres: string;
16
17
  restate: string;
17
18
  s2: string;
19
+ proxy: string;
18
20
  }
19
21
  /** `NYLORUN_RUNTIME_IMAGE` names the Runtime image, so its version is unknown here. */
20
22
  export declare function runtimeImageOverridden(env: Readonly<Record<string, string | undefined>>): boolean;
@@ -1,14 +1,15 @@
1
1
  import { pinnedVersion } from "./versions.js";
2
2
  /**
3
- * Images the local stack runs. The Runtime and Studio tags are pinned by this
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
5
  * (local builds, CI images). Postgres, Restate and s2-lite are the official
6
- * images, pinned here.
6
+ * images, pinned here, as is Caddy for the machine's Studio proxy.
7
7
  */
8
8
  export const PINNED_IMAGES = {
9
9
  postgres: "postgres:17.11",
10
10
  restate: "docker.restate.dev/restatedev/restate:1.7.12",
11
11
  s2: "ghcr.io/s2-streamstore/s2:0.43.0",
12
+ proxy: "caddy:2.11.6",
12
13
  };
13
14
  function override(env, name) {
14
15
  const value = env[name]?.trim();
@@ -1,13 +1,14 @@
1
1
  import { type StackDeps } from "./commands.js";
2
- export { ensureStack, isStackCommand, readStackStatus, stackUsage, studioLoginUrl, tenantStudioPath, STACK_SERVICES, } from "./commands.js";
2
+ export { ensureStack, isStackCommand, readProxyReport, readStackStatus, stackUsage, studioLoginUrl, tenantStudioPath, STACK_SERVICES, } from "./commands.js";
3
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
+ export type { ProxyStatus } from "./proxy.js";
6
7
  /** Real dependencies: docker on PATH, global fetch, the terminal. */
7
8
  export declare function defaultStackDeps(env: Readonly<Record<string, string | undefined>>): StackDeps;
8
- /** Entry for `nylorun start|stop|status|logs|reset|studio|ls|delete|legacy`. */
9
+ /** Entry for `nylorun start|stop|status|logs|reset|studio|ls|delete`. */
9
10
  export declare function stackCommand(name: string, args: readonly string[], env: Readonly<Record<string, string | undefined>>): Promise<number>;
10
- /** `nylorun studio`, landing on `next` when given, else on the stack's Tenant. */
11
+ /** `nylorun studio`, landing on `next` when given, else on the Tenant's page. */
11
12
  export declare function studioCommand(args: readonly string[], env: Readonly<Record<string, string | undefined>>, options?: {
12
13
  next?: string;
13
14
  }): Promise<number>;
@@ -4,7 +4,7 @@ import { runStackCommand, runStudioCommand } from "./commands.js";
4
4
  import { spawnDocker } from "./docker.js";
5
5
  import { pinnedVersion } from "./versions.js";
6
6
  import { loopbackPorts } from "./ports.js";
7
- export { ensureStack, isStackCommand, readStackStatus, stackUsage, studioLoginUrl, tenantStudioPath, STACK_SERVICES, } from "./commands.js";
7
+ export { ensureStack, isStackCommand, readProxyReport, readStackStatus, stackUsage, studioLoginUrl, tenantStudioPath, STACK_SERVICES, } from "./commands.js";
8
8
  export { checkDocker } from "./docker.js";
9
9
  function browserCommand(env) {
10
10
  if (process.platform === "darwin")
@@ -17,7 +17,13 @@ function browserCommand(env) {
17
17
  * Variables `docker/.env` sets for Compose: `nylorun start` writes them there, so the
18
18
  * environment must not override them when Compose interpolates the file.
19
19
  */
20
- const STACK_ENV_OWNED = ["NYLORUN_STACK_NAME", "NYLORUN_DERIVED_PRINCIPALS"];
20
+ const STACK_ENV_OWNED = [
21
+ "NYLORUN_TENANT_NAME",
22
+ "NYLORUN_DERIVED_PRINCIPALS",
23
+ "NYLORUN_STUDIO_ANALYTICS_ID",
24
+ "NYLORUN_STUDIO_PUBLIC_ORIGINS",
25
+ "NYLORUN_PROXY_PORT",
26
+ ];
21
27
  /** Real dependencies: docker on PATH, global fetch, the terminal. */
22
28
  export function defaultStackDeps(env) {
23
29
  const interactive = Boolean(process.stdin.isTTY && process.stderr.isTTY);
@@ -71,11 +77,11 @@ export function defaultStackDeps(env) {
71
77
  },
72
78
  };
73
79
  }
74
- /** Entry for `nylorun start|stop|status|logs|reset|studio|ls|delete|legacy`. */
80
+ /** Entry for `nylorun start|stop|status|logs|reset|studio|ls|delete`. */
75
81
  export async function stackCommand(name, args, env) {
76
82
  return await runStackCommand(name, args, defaultStackDeps(env));
77
83
  }
78
- /** `nylorun studio`, landing on `next` when given, else on the stack's Tenant. */
84
+ /** `nylorun studio`, landing on `next` when given, else on the Tenant's page. */
79
85
  export async function studioCommand(args, env, options = {}) {
80
86
  return await runStudioCommand(args, defaultStackDeps(env), options);
81
87
  }
@@ -1,6 +1,6 @@
1
- /** Files under the Host root that the stack commands read and write. */
1
+ /** Files under the Host root that the nylorun commands read and write. */
2
2
  export interface StackPaths {
3
- /** The Host root (`~/.nylorun/stacks/<name>` or `NYLORUN_HOME`), bind-mounted at /nylorun. */
3
+ /** The Host root (`~/.nylorun/tenants/<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,7 +8,7 @@ export interface StackPaths {
8
8
  credentials: string;
9
9
  /** host-state.json: written only by a launcher-managed Runtime. */
10
10
  state: string;
11
- /** stack.json: the stack's name and the project it was created for. */
11
+ /** tenant.json: the Tenant's name and the project it was created for. */
12
12
  record: string;
13
13
  /** The Tenant directory: homes, sandboxes, plugin data, logs. */
14
14
  tenant: string;
@@ -7,7 +7,7 @@ export function stackPaths(hostRoot) {
7
7
  config: join(root, "host.json"),
8
8
  credentials: join(root, "host-credentials.json"),
9
9
  state: join(root, "host-state.json"),
10
- record: join(root, "stack.json"),
10
+ record: join(root, "tenant.json"),
11
11
  tenant: join(root, "tenant"),
12
12
  keys: join(root, "keys"),
13
13
  vaultKey: join(root, "keys", "vault-kek"),
@@ -1,11 +1,13 @@
1
1
  export declare const DEFAULT_PORTS: {
2
+ /** The Studio proxy, one per machine (`http://<tenant>.localhost:<port>`). */
3
+ readonly proxy: 4160;
2
4
  readonly runtime: 8787;
3
5
  /** The Runtime's operator listener (Admin API). */
4
6
  readonly admin: 8788;
5
7
  readonly studio: 4161;
6
8
  readonly restate: 9070;
7
9
  };
8
- /** How the stack finds ports on 127.0.0.1; injectable for tests. */
10
+ /** How `nylorun start` finds ports on 127.0.0.1; injectable for tests. */
9
11
  export interface PortProbe {
10
12
  isFree(port: number): Promise<boolean>;
11
13
  /** A port the OS reports free right now. */
@@ -14,7 +16,7 @@ export interface PortProbe {
14
16
  export declare const loopbackPorts: PortProbe;
15
17
  /**
16
18
  * Choose a port: the persisted one if any (kept even when busy, because the
17
- * running stack may hold it), else the default when free, else a free one.
19
+ * running Tenant may hold it), else the default when free, else a free one.
18
20
  * Never returns a port in `taken`.
19
21
  */
20
22
  export declare function choosePort(probe: PortProbe, preferred: number, persisted: number | undefined, taken: ReadonlySet<number>): Promise<number>;
@@ -1,5 +1,7 @@
1
1
  import { createServer } from "node:net";
2
2
  export const DEFAULT_PORTS = {
3
+ /** The Studio proxy, one per machine (`http://<tenant>.localhost:<port>`). */
4
+ proxy: 4160,
3
5
  runtime: 8787,
4
6
  /** The Runtime's operator listener (Admin API). */
5
7
  admin: 8788,
@@ -30,7 +32,7 @@ export const loopbackPorts = {
30
32
  };
31
33
  /**
32
34
  * Choose a port: the persisted one if any (kept even when busy, because the
33
- * running stack may hold it), else the default when free, else a free one.
35
+ * running Tenant may hold it), else the default when free, else a free one.
34
36
  * Never returns a port in `taken`.
35
37
  */
36
38
  export async function choosePort(probe, preferred, persisted, taken) {
@@ -10,7 +10,7 @@ export interface PreparedStack {
10
10
  /** True when this run chose the ports (first start). */
11
11
  firstRun: boolean;
12
12
  }
13
- /** Read the persisted stack settings without writing anything. */
13
+ /** Read the persisted Compose settings without writing anything. */
14
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:
@@ -20,7 +20,7 @@ export declare function readStackEnv(paths: Pick<StackPaths, "env">): Promise<im
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. */
23
+ /** The Tenant's name and its Compose project. */
24
24
  name: string;
25
25
  project: string;
26
26
  images: StackImages;
@@ -29,7 +29,7 @@ export declare function prepareStack(input: {
29
29
  /** Recorded in host.json; undefined keeps the recorded version. */
30
30
  runtimeVersion: string | undefined;
31
31
  ports: PortProbe;
32
- /** Ports other stacks keep in their `.env`: never chosen for a port not chosen yet. */
32
+ /** Ports other Tenants keep in their `.env`: never chosen for a port not chosen yet. */
33
33
  reserved?: ReadonlySet<number>;
34
34
  /** `NYLORUN_DERIVED_PRINCIPALS` from the environment of `nylorun start`; else kept. */
35
35
  derivedPrincipals?: string;
@@ -41,4 +41,8 @@ export declare function prepareStack(input: {
41
41
  add?: readonly string[];
42
42
  reset?: boolean;
43
43
  };
44
+ /** The measurement id Studio reports page views to; absent when telemetry is off. */
45
+ studioAnalyticsId?: string;
46
+ /** Studio's origin through the proxy; absent without the proxy. */
47
+ studioPublicOrigin?: string;
44
48
  }): Promise<PreparedStack>;