nylorun 0.6.0-beta → 0.8.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 (41) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +77 -73
  3. package/dist/cli.js +7 -2
  4. package/dist/doctor.d.ts +2 -4
  5. package/dist/doctor.js +12 -19
  6. package/dist/sandbox/cluster-file.d.ts +47 -0
  7. package/dist/sandbox/cluster-file.js +55 -0
  8. package/dist/sandbox/commands.d.ts +41 -0
  9. package/dist/sandbox/commands.js +281 -0
  10. package/dist/sandbox/kubectl.d.ts +39 -0
  11. package/dist/sandbox/kubectl.js +70 -0
  12. package/dist/sandbox/manifests.d.ts +41 -0
  13. package/dist/sandbox/manifests.js +138 -0
  14. package/dist/sandbox/probe.d.ts +44 -0
  15. package/dist/sandbox/probe.js +137 -0
  16. package/dist/stack/commands.d.ts +56 -9
  17. package/dist/stack/commands.js +167 -123
  18. package/dist/stack/compose-file.d.ts +42 -5
  19. package/dist/stack/compose-file.js +208 -21
  20. package/dist/stack/env-file.d.ts +48 -6
  21. package/dist/stack/env-file.js +81 -9
  22. package/dist/stack/host-files.js +11 -1
  23. package/dist/stack/images.d.ts +8 -4
  24. package/dist/stack/images.js +8 -3
  25. package/dist/stack/index.d.ts +4 -2
  26. package/dist/stack/index.js +7 -3
  27. package/dist/stack/paths.d.ts +15 -0
  28. package/dist/stack/paths.js +7 -0
  29. package/dist/stack/ports.d.ts +0 -2
  30. package/dist/stack/ports.js +0 -2
  31. package/dist/stack/prepare.d.ts +10 -4
  32. package/dist/stack/prepare.js +20 -3
  33. package/dist/stack/sandbox.d.ts +4 -0
  34. package/dist/stack/sandbox.js +106 -0
  35. package/dist/stack/stacks.d.ts +8 -0
  36. package/dist/stack/stacks.js +19 -0
  37. package/dist/stack/studio-login.d.ts +2 -2
  38. package/dist/stack/studio-login.js +2 -2
  39. package/package.json +5 -5
  40. package/dist/stack/proxy.d.ts +0 -78
  41. package/dist/stack/proxy.js +0 -224
@@ -6,8 +6,8 @@ import { PINNED_IMAGES } from "./images.js";
6
6
  * network and ports. Container, network and volume names are global on the Docker engine, so
7
7
  * every one is `<project>-<role>` (`nylorun-shop-studio`, network `nylorun-shop`, volume
8
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`.
9
+ * the project and the Tenant's name. Everything else that varies (ports, the Postgres password and the other
10
+ * secrets, UID/GID, the Host root, the images) comes from `docker/.env`.
11
11
  *
12
12
  * The Tenant's state is its Postgres database, executed through Restate, with its
13
13
  * history in s2-lite; the Runtime's /ready checks all three. Postgres initialises the
@@ -26,8 +26,8 @@ import { PINNED_IMAGES } from "./images.js";
26
26
  * which alone reads model credentials, and the Tool Gate (F4.1), which alone
27
27
  * holds remote MCP connections and their credentials and POSTs every Action
28
28
  * delivery. Every model call, remote MCP call and delivery of the loop crosses it
29
- * (NYLORUN_GATES_URL, with NYLORUN_GATES_TOKEN from `.env`); stdio MCP servers
30
- * still run in the runtime container. The gateway also runs keys (F4.2): the only
29
+ * (NYLORUN_GATES_URL, with NYLORUN_GATES_TOKEN from `.env`, or a run's token from the
30
+ * harness); MCP stdio servers run in the harness container. The gateway also runs keys (F4.2): the only
31
31
  * holder of the vault key, it runs every vault write that touches a secret and signs
32
32
  * every token (NYLORUN_KEYS_URL). The gateway mounts only the Host's Tenant directory
33
33
  * (`tenant/`) and its keys directory (`keys/`, the vault key), read-only: never
@@ -36,8 +36,44 @@ import { PINNED_IMAGES } from "./images.js";
36
36
  * Restate's private key and `.env`. The runtime does not wait for the gateway: while
37
37
  * it is down, model and MCP calls fail, deliveries are retried, vault writes and
38
38
  * token minting answer 503, and the session takes the next message.
39
+ *
40
+ * The Object store (blueprint D35) is RustFS, single node and single drive, pinned by digest,
41
+ * on the `rustfs` volume; it is not published. Its credential is the access key `nylorun` and
42
+ * the secret key NYLORUN_OBJECT_STORE_SECRET_KEY from `.env`, and only the runtime and the
43
+ * gateway receive it (NYLORUN_OBJECT_STORE_*). The runtime creates the bucket at boot and
44
+ * reaches the store through its `s3` BlobStore.
45
+ *
46
+ * With `sandboxes` (after `nylorun sandbox enable`, F7.2) the `sandboxes` service runs the
47
+ * agent-sandbox driver: the only container holding the cluster credentials
48
+ * (`<Host root>/sandboxes`, read-only), not published, reached by the runtime alone with
49
+ * NYLORUN_SANDBOXES_TOKEN. The runtime gets an empty read-only mount over that directory.
50
+ * Sandbox pods reach the stack only on the Docker host's address (NYLORUN_SANDBOX_HOST_ADDRESS),
51
+ * where three ports are published on NYLORUN_SANDBOX_BIND: the runtime's Harness API listener
52
+ * (NYLORUN_SANDBOX_HARNESS_PORT → 4200: pods join and connect with host tokens), the
53
+ * gateway's gates (NYLORUN_SANDBOX_GATES_PORT → 4100: model and MCP calls with run tokens) and
54
+ * egress-gate (NYLORUN_SANDBOX_EGRESS_PORT → 4200 in the gateway, `--service gates,keys,egress`:
55
+ * CONNECT with the egress token, to the hosts the sandbox spec allows). The two HTTP listeners
56
+ * accept that address as a Host.
57
+ *
58
+ * The harness (F6.2, `harness: "remote"`, the default) runs every agent turn, MCP stdio server
59
+ * and workspace in its own container: the runtime image with `--service harness`, connected to
60
+ * core's Harness API (`ws://runtime:4200/nylorun/harness/v1`) with NYLORUN_HARNESS_TOKEN, its
61
+ * only credential, and calling models and remote MCP servers through the gateway with each
62
+ * run's token. It mounts only the Tenant's `sandboxes/`, `plugin-data/`, `home/` and `tmp/`
63
+ * under `/harness`, and the Host root's `plugins/` read-only at its own path (plugin roots name
64
+ * paths on this machine; the runtime mounts it there too). NYLORUN_HARNESS=in-process in `.env`
65
+ * rolls back to turns in the runtime container, without the harness service.
66
+ *
67
+ * Three networks: `store` (internal: no egress) joins Postgres, s2-lite, Restate and RustFS to
68
+ * the runtime and the gateway; `harness` joins the harness to the runtime and the gateway only;
69
+ * `default` carries egress and the published ports (runtime, gateway, Studio, sandboxes).
70
+ * Restate's admin port (its UI, unauthenticated) is published, and Restate joins `default`,
71
+ * only with `restateUi` (`nylorun start --restate-ui`).
39
72
  */
40
- export function renderComposeFile(project, name) {
73
+ export function renderComposeFile(project, name, options = {}) {
74
+ const sandboxes = options.sandboxes === true;
75
+ const remote = (options.harness ?? "remote") === "remote";
76
+ const restateUi = options.restateUi === true;
41
77
  return `# Written by \`nylorun start\`; rewritten on every start. Settings live in .env.
42
78
  name: ${project}
43
79
 
@@ -58,6 +94,7 @@ services:
58
94
  POSTGRES_INITDB_ARGS: "--locale=C" # C collation for the whole database
59
95
  volumes:
60
96
  - postgres:/var/lib/postgresql/data
97
+ networks: [store]
61
98
  healthcheck:
62
99
  test: ["CMD-SHELL", "pg_isready -U nylorun -d nylorun"]
63
100
  interval: 2s
@@ -72,15 +109,10 @@ services:
72
109
  command: ["--node-name=restate-1"] # stable name, so data is found on restart
73
110
  environment:
74
111
  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
78
112
  volumes:
79
113
  - restate:/restate-data
80
114
  - \${NYLORUN_HOST_ROOT:?run nylorun start}/docker/restate-identity.pem:/run/nylorun/restate-identity.pem:ro
81
- ports:
82
- - "127.0.0.1:\${NYLORUN_RESTATE_PORT:?run nylorun start}:9070" # Restate UI and admin, for debugging
83
- healthcheck:
115
+ ${restateUi ? RESTATE_UI : RESTATE_CLOSED} healthcheck:
84
116
  test: ["CMD", "curl", "-fsS", "http://localhost:9070/health"]
85
117
  interval: 2s
86
118
  timeout: 5s
@@ -97,15 +129,36 @@ services:
97
129
  # (e.g. /data) is root-owned and s2-lite cannot write it.
98
130
  volumes:
99
131
  - s2-lite:/home/nonroot
132
+ networks: [store]
100
133
  # The s2 image has no shell or HTTP client, so it has no health check;
101
134
  # the Runtime's /ready covers it.
102
135
  restart: unless-stopped
103
136
 
104
- gateway: # the Model and Tool Gates and keys: the vault key, credentials, signing, outbound calls; not published
137
+ rustfs: # the Object store (D35); not published, only the runtime and the gateway hold its credential
138
+ image: ${PINNED_IMAGES.rustfs}
139
+ container_name: ${project}-rustfs
140
+ labels: *tenant
141
+ environment:
142
+ RUSTFS_ACCESS_KEY: nylorun
143
+ RUSTFS_SECRET_KEY: \${NYLORUN_OBJECT_STORE_SECRET_KEY:?run nylorun start}
144
+ RUSTFS_CONSOLE_ENABLE: "false" # no web console: clients go through the Runtime
145
+ RUSTFS_OBS_LOG_DIRECTORY: "" # log to stdout (nylorun logs rustfs)
146
+ # The image runs as uid 10001 and owns /data, which Docker copies into the new volume.
147
+ volumes:
148
+ - rustfs:/data
149
+ networks: [store]
150
+ healthcheck:
151
+ test: ["CMD", "curl", "-fsS", "http://localhost:9000/health"]
152
+ interval: 2s
153
+ timeout: 5s
154
+ retries: 30
155
+ restart: unless-stopped
156
+
157
+ gateway: # the Model and Tool Gates and keys: the vault key, credentials, signing, outbound calls; not published${sandboxes ? ", but for sandbox pods" : ""}
105
158
  image: \${NYLORUN_RUNTIME_IMAGE:?run nylorun start}
106
159
  container_name: ${project}-gateway
107
160
  labels: *tenant
108
- command: ["--service", "gates,keys"]
161
+ command: ["--service", "gates,keys${sandboxes ? ",egress" : ""}"]
109
162
  user: "\${NYLORUN_UID:?run nylorun start}:\${NYLORUN_GID:?run nylorun start}"
110
163
  depends_on:
111
164
  postgres: { condition: service_healthy }
@@ -113,17 +166,23 @@ services:
113
166
  NYLORUN_HOME: /nylorun
114
167
  NYLORUN_PACKING: combined
115
168
  NYLORUN_GATES_LISTEN_PORT: "4100"
116
- NYLORUN_GATES_ALLOWED_HOSTS: gateway:4100
169
+ NYLORUN_GATES_ALLOWED_HOSTS: gateway:4100${sandboxes ? SANDBOXES_GATES_HOST : ""}
117
170
  NYLORUN_GATES_TOKEN: \${NYLORUN_GATES_TOKEN:?run nylorun start}
118
171
  NYLORUN_DATABASE_URL: postgres://nylorun:\${NYLORUN_POSTGRES_PASSWORD}@postgres:5432/nylorun
172
+ # The Object store, where model-gate will read file parts (F8.1).
173
+ NYLORUN_OBJECT_STORE_ENDPOINT: http://rustfs:9000
174
+ NYLORUN_OBJECT_STORE_ACCESS_KEY: nylorun
175
+ NYLORUN_OBJECT_STORE_SECRET_KEY: \${NYLORUN_OBJECT_STORE_SECRET_KEY:?run nylorun start}
119
176
  # Action endpoints on this machine: \`localhost\` in a registered URL means the Docker host.
120
- NYLORUN_ENDPOINT_LOOPBACK: docker-host
177
+ NYLORUN_ENDPOINT_LOOPBACK: docker-host${sandboxes ? SANDBOXES_GATEWAY_ENV : ""}
121
178
  extra_hosts:
122
179
  host.docker.internal: host-gateway # model servers, MCP servers and Action endpoints on this machine
123
180
  volumes:
124
181
  # The Tenant's homes and its vault key only, read-only.
125
182
  - \${NYLORUN_HOST_ROOT:?run nylorun start}/tenant:/nylorun/tenant:ro
126
183
  - \${NYLORUN_HOST_ROOT:?run nylorun start}/keys:/nylorun/keys:ro
184
+ ${sandboxes ? SANDBOXES_GATES_PORT : ""} # Egress and the stores; the harness reaches its gates on \`harness\`.
185
+ networks: [default, store, harness]
127
186
  healthcheck:
128
187
  test: ["CMD", "node", "-e", "fetch('http://localhost:4100/ready').then(r=>process.exit(r.ok?0:1),()=>process.exit(1))"]
129
188
  interval: 2s
@@ -141,6 +200,7 @@ services:
141
200
  postgres: { condition: service_healthy }
142
201
  restate: { condition: service_healthy }
143
202
  s2-lite: { condition: service_started }
203
+ rustfs: { condition: service_healthy }
144
204
  environment:
145
205
  NYLORUN_HOME: /nylorun
146
206
  NYLORUN_PACKING: combined
@@ -171,8 +231,18 @@ services:
171
231
  NYLORUN_S2_ENDPOINT: http://s2-lite:80
172
232
  NYLORUN_S2_TOKEN: ignored # s2-lite has no access tokens yet
173
233
  NYLORUN_WORKSPACE_STORE_URL: file:///workspaces
234
+ # The Object store, through the BlobStore seam's s3 adapter; the runtime creates the bucket.
235
+ NYLORUN_OBJECT_STORE_ENDPOINT: http://rustfs:9000
236
+ NYLORUN_OBJECT_STORE_ACCESS_KEY: nylorun
237
+ NYLORUN_OBJECT_STORE_SECRET_KEY: \${NYLORUN_OBJECT_STORE_SECRET_KEY:?run nylorun start}
174
238
  # Action endpoints on this machine: \`localhost\` in a registered URL means the Docker host.
175
239
  NYLORUN_ENDPOINT_LOOPBACK: docker-host
240
+ # Agent turns, MCP servers and workspaces run in the harness container, which connects to
241
+ # this listener with NYLORUN_HARNESS_TOKEN. NYLORUN_HARNESS=in-process in .env rolls back.
242
+ NYLORUN_HARNESS: \${NYLORUN_HARNESS:-remote}
243
+ NYLORUN_HARNESS_LISTEN_PORT: "4200"
244
+ NYLORUN_HARNESS_ALLOWED_HOSTS: runtime:4200${sandboxes ? SANDBOXES_HARNESS_HOST : ""}
245
+ NYLORUN_HARNESS_TOKEN: \${NYLORUN_HARNESS_TOKEN:?run nylorun start}${sandboxes ? SANDBOXES_RUNTIME_ENV : ""}
176
246
  extra_hosts:
177
247
  host.docker.internal: host-gateway # the Docker host, also on Linux Docker Engine
178
248
  volumes:
@@ -186,11 +256,14 @@ services:
186
256
  - type: tmpfs
187
257
  target: /nylorun/docker
188
258
  read_only: true
189
- tmpfs: { size: 4096, mode: 0755 } # empty, and listable by the runtime user
259
+ tmpfs: { size: 4096, mode: 0755 } # empty, and listable by the runtime user${sandboxes ? SANDBOXES_RUNTIME_MOUNT : ""}
190
260
  - workspaces:/workspaces
261
+ # Plugin roots on this machine, at their own path (as for the harness), for NYLORUN_HARNESS=in-process.
262
+ - \${NYLORUN_HOST_ROOT:?run nylorun start}/plugins:\${NYLORUN_HOST_ROOT:?run nylorun start}/plugins:ro
263
+ networks: [default, store, harness]
191
264
  ports:
192
265
  - "127.0.0.1:\${NYLORUN_PORT:?run nylorun start}:4000" # Tenant API, SSE, browsers
193
- - "127.0.0.1:\${NYLORUN_ADMIN_PORT:?run nylorun start}:4001" # Admin API (operators only)
266
+ - "127.0.0.1:\${NYLORUN_ADMIN_PORT:?run nylorun start}:4001" # Admin API (operators only)${sandboxes ? SANDBOXES_RUNTIME_PORT : ""}
194
267
  healthcheck: # liveness: a Tenant that cannot open is reported by nylorun start from the Admin status, not by a 300 s wait
195
268
  test: ["CMD", "node", "-e", "fetch('http://localhost:4000/health').then(r=>process.exit(r.ok?0:1),()=>process.exit(1))"]
196
269
  interval: 2s
@@ -209,10 +282,8 @@ services:
209
282
  NYLORUN_RUNTIME_URL: http://runtime:4001 # the operator listener: Admin and Tenant API
210
283
  NYLORUN_ADMIN_KEY_FILE: /run/nylorun/host-credentials.json
211
284
  PORT: "3000"
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.
285
+ # Studio's Host check accepts localhost/127.0.0.1 on the published port.
214
286
  NYLORUN_STUDIO_PUBLIC_PORT: \${NYLORUN_STUDIO_PORT}
215
- NYLORUN_STUDIO_PUBLIC_ORIGINS: \${NYLORUN_STUDIO_PUBLIC_ORIGINS:-}
216
287
  # Browsers share cookies across ports of one host: a cookie per Tenant keeps the
217
288
  # sessions of two Studios on localhost apart.
218
289
  NYLORUN_STUDIO_SESSION_COOKIE: nylorun_studio_${name}
@@ -222,6 +293,7 @@ services:
222
293
  NYLORUN_STUDIO_ANALYTICS_ID: \${NYLORUN_STUDIO_ANALYTICS_ID:-}
223
294
  volumes:
224
295
  - \${NYLORUN_HOST_ROOT}/host-credentials.json:/run/nylorun/host-credentials.json:ro
296
+ networks: [default]
225
297
  ports:
226
298
  - "127.0.0.1:\${NYLORUN_STUDIO_PORT:?run nylorun start}:3000" # opened as http://localhost:<port>
227
299
  healthcheck: # /healthz needs no session; Host studio:3000 is accepted
@@ -230,16 +302,131 @@ services:
230
302
  timeout: 5s
231
303
  retries: 30
232
304
  restart: unless-stopped
233
-
305
+ ${remote ? harnessService(project) : ""}${sandboxes ? sandboxesService(project) : ""}
234
306
  networks:
235
307
  default:
236
308
  name: ${project}
237
309
  labels: *tenant
310
+ store: # the stores: no egress, no published port
311
+ name: ${project}-store
312
+ internal: true
313
+ labels: *tenant
314
+ harness: # the harness reaches the runtime's Harness API and the gateway only (egress for MCP servers and bash)
315
+ name: ${project}-harness
316
+ labels: *tenant
238
317
 
239
318
  volumes:
240
319
  postgres: { name: ${project}-postgres, labels: *tenant }
241
320
  restate: { name: ${project}-restate, labels: *tenant }
242
321
  s2-lite: { name: ${project}-s2-lite, labels: *tenant }
322
+ rustfs: { name: ${project}-rustfs, labels: *tenant }
243
323
  workspaces: { name: ${project}-workspaces, labels: *tenant }
244
324
  `;
245
325
  }
326
+ /** Restate on the stores' network only: its admin port (the UI) is not published. */
327
+ const RESTATE_CLOSED = ` networks: [store] # admin and UI not published: nylorun start --restate-ui
328
+ `;
329
+ /** Restate's admin port and UI on loopback (`--restate-ui`): it must join `default` to be published. */
330
+ const RESTATE_UI = ` networks: [store, default]
331
+ ports:
332
+ - "127.0.0.1:\${NYLORUN_RESTATE_PORT:?run nylorun start}:9070" # Restate UI and admin (unauthenticated): nylorun start --restate-ui
333
+ `;
334
+ /**
335
+ * The harness service (F6.2): agent turns, MCP stdio servers and workspaces, apart from core.
336
+ * It holds the harness token only; models and remote MCP servers are reached through the
337
+ * gateway with each run's token. Healthy once connected to core's Harness API.
338
+ */
339
+ function harnessService(project) {
340
+ return `
341
+ harness: # agent turns, MCP servers and workspaces; holds only the harness token; not published
342
+ image: \${NYLORUN_RUNTIME_IMAGE:?run nylorun start}
343
+ container_name: ${project}-harness
344
+ labels: *tenant
345
+ command: ["--service", "harness"]
346
+ user: "\${NYLORUN_UID:?run nylorun start}:\${NYLORUN_GID:?run nylorun start}"
347
+ depends_on:
348
+ runtime: { condition: service_healthy }
349
+ gateway: { condition: service_healthy }
350
+ environment:
351
+ NYLORUN_HARNESS_URL: ws://runtime:4200/nylorun/harness/v1
352
+ NYLORUN_HARNESS_TOKEN: \${NYLORUN_HARNESS_TOKEN:?run nylorun start}
353
+ NYLORUN_GATES_URL: http://gateway:4100
354
+ NYLORUN_HARNESS_ROOT: /harness
355
+ volumes:
356
+ # The Tenant's workspaces, plugin data and the homes of MCP stdio servers; nothing else.
357
+ - \${NYLORUN_HOST_ROOT:?run nylorun start}/tenant/sandboxes:/harness/sandboxes
358
+ - \${NYLORUN_HOST_ROOT:?run nylorun start}/tenant/plugin-data:/harness/plugin-data
359
+ - \${NYLORUN_HOST_ROOT:?run nylorun start}/tenant/home:/harness/home
360
+ - \${NYLORUN_HOST_ROOT:?run nylorun start}/tenant/tmp:/harness/tmp
361
+ # Plugin roots on this machine, read-only, at their own path: agents name them so.
362
+ - \${NYLORUN_HOST_ROOT:?run nylorun start}/plugins:\${NYLORUN_HOST_ROOT:?run nylorun start}/plugins:ro
363
+ networks: [harness]
364
+ healthcheck: # healthy once connected to core's Harness API
365
+ test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:4300/health').then(r=>r.json()).then(b=>process.exit(b.connected?0:1),()=>process.exit(1))"]
366
+ interval: 2s
367
+ timeout: 5s
368
+ retries: 90
369
+ restart: unless-stopped
370
+ `;
371
+ }
372
+ /**
373
+ * The runtime reaches the sandboxes service on the Compose network with its token, and serves
374
+ * the Harness API to sandbox pods (F7.2) on the Docker host's address.
375
+ */
376
+ const SANDBOXES_RUNTIME_ENV = `
377
+ # The sandboxes service (nylorun sandbox enable): pods on the Tenant's cluster.
378
+ NYLORUN_SANDBOXES_URL: http://sandboxes:4300
379
+ NYLORUN_SANDBOXES_TOKEN: \${NYLORUN_SANDBOXES_TOKEN:?run nylorun sandbox enable}
380
+ NYLORUN_SANDBOX_HARNESS_IMAGE: \${NYLORUN_RUNTIME_IMAGE:?run nylorun start} # pods copy the engine from it`;
381
+ /** Pods join and connect to the Harness API at the Docker host's address. */
382
+ const SANDBOXES_HARNESS_HOST = ",\${NYLORUN_SANDBOX_HOST_ADDRESS:?run nylorun sandbox enable}:\${NYLORUN_SANDBOX_HARNESS_PORT:?run nylorun sandbox enable}";
383
+ /** The Harness API, published for sandbox pods. */
384
+ const SANDBOXES_RUNTIME_PORT = `
385
+ - "\${NYLORUN_SANDBOX_BIND:?run nylorun sandbox enable}:\${NYLORUN_SANDBOX_HARNESS_PORT:?run nylorun sandbox enable}:4200" # Harness API, for sandbox pods`;
386
+ /** The gates accept the pods' Host; their calls carry run tokens. */
387
+ const SANDBOXES_GATES_HOST = ",\${NYLORUN_SANDBOX_HOST_ADDRESS:?run nylorun sandbox enable}:\${NYLORUN_SANDBOX_GATES_PORT:?run nylorun sandbox enable}";
388
+ /** The gates and egress-gate, published for sandbox pods. */
389
+ const SANDBOXES_GATES_PORT = ` ports:
390
+ - "\${NYLORUN_SANDBOX_BIND:?run nylorun sandbox enable}:\${NYLORUN_SANDBOX_GATES_PORT:?run nylorun sandbox enable}:4100" # gates, for sandbox pods (run tokens)
391
+ - "\${NYLORUN_SANDBOX_BIND:?run nylorun sandbox enable}:\${NYLORUN_SANDBOX_EGRESS_PORT:?run nylorun sandbox enable}:4200" # egress-gate, for sandbox pods (egress tokens)
392
+ `;
393
+ /** egress-gate (F7.2): pods' CONNECT proxy in the gateway. */
394
+ const SANDBOXES_GATEWAY_ENV = `
395
+ # egress-gate: the only way out of a sandbox pod, to the hosts its spec allows.
396
+ NYLORUN_EGRESS_LISTEN_PORT: "4200"`;
397
+ /** Empty and read-only over the cluster credentials: only the sandboxes service reads them. */
398
+ const SANDBOXES_RUNTIME_MOUNT = `
399
+ - type: tmpfs
400
+ target: /nylorun/sandboxes
401
+ read_only: true
402
+ tmpfs: { size: 4096, mode: 0755 } # empty, and listable by the runtime user`;
403
+ /**
404
+ * The sandboxes service. The pod-facing ports `nylorun sandbox enable` records
405
+ * (NYLORUN_SANDBOX_*_PORT in .env), the Harness API's, the gates' and egress-gate's, are
406
+ * published by the runtime and the gateway (above).
407
+ */
408
+ function sandboxesService(project) {
409
+ return `
410
+ sandboxes: # agent-sandbox driver; the only holder of the cluster credentials; not published
411
+ image: \${NYLORUN_SANDBOXES_IMAGE:?run nylorun sandbox enable}
412
+ container_name: ${project}-sandboxes
413
+ labels: *tenant
414
+ user: "\${NYLORUN_UID:?run nylorun start}:\${NYLORUN_GID:?run nylorun start}" # reads the 0600 token
415
+ environment:
416
+ NYLORUN_SANDBOXES_TOKEN: \${NYLORUN_SANDBOXES_TOKEN:?run nylorun sandbox enable}
417
+ NYLORUN_SANDBOXES_DIR: /run/nylorun/sandboxes
418
+ NYLORUN_SANDBOXES_LISTEN: ":4300"
419
+ extra_hosts:
420
+ host.docker.internal: host-gateway # the API server when the kubeconfig names this machine's loopback
421
+ volumes:
422
+ # cluster.json (API server, CA, namespace) and the ServiceAccount token, read-only.
423
+ - \${NYLORUN_HOST_ROOT:?run nylorun start}/sandboxes:/run/nylorun/sandboxes:ro
424
+ networks: [default] # the cluster's API server, through host.docker.internal
425
+ healthcheck: # /ready: informers synced and the API server answers
426
+ test: ["CMD", "/sandboxes", "healthcheck"]
427
+ interval: 5s
428
+ timeout: 5s
429
+ retries: 24
430
+ restart: unless-stopped
431
+ `;
432
+ }
@@ -14,11 +14,32 @@ export interface StackEnv {
14
14
  adminPort: number;
15
15
  /** Published Studio port (loopback). */
16
16
  studioPort: number;
17
- /** Published Restate UI and admin port (loopback). */
17
+ /** Restate UI and admin port (loopback), published only with `restateUi`. */
18
18
  restatePort: number;
19
+ /**
20
+ * `NYLORUN_RESTATE_UI`: `1` publishes Restate's UI and admin port (unauthenticated) for this
21
+ * start (`nylorun start --restate-ui`); `0` keeps it closed. Decided again on every start.
22
+ */
23
+ restateUi: boolean;
19
24
  postgresPassword: string;
20
25
  /** The token the runtime presents to the gateway (`NYLORUN_GATES_TOKEN`), 32 bytes as hex. */
21
26
  gatesToken: string;
27
+ /**
28
+ * The harness's credential for core's Harness API (`NYLORUN_HARNESS_TOKEN`), 32 bytes as hex:
29
+ * given only to the runtime (which checks it) and the harness.
30
+ */
31
+ harnessToken: string;
32
+ /**
33
+ * `NYLORUN_HARNESS`: `remote` (the harness container runs agent turns, MCP servers and
34
+ * workspaces) or `in-process` (the runtime does, without a harness container). Kept across
35
+ * starts; edit `.env` to roll back.
36
+ */
37
+ harness: HarnessMode;
38
+ /**
39
+ * RustFS's secret key (`NYLORUN_OBJECT_STORE_SECRET_KEY`), 32 bytes as hex: the Object store's
40
+ * credential, given only to the runtime and the gateway.
41
+ */
42
+ objectStoreSecretKey: string;
22
43
  /** `publickeyv1_...` of `docker/restate-identity.pem`. */
23
44
  restateIdentityKey: string;
24
45
  uid: number;
@@ -34,11 +55,6 @@ export interface StackEnv {
34
55
  * when telemetry is off. Decided again on every start.
35
56
  */
36
57
  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
58
  /** The Tenant's name, which the Runtime gives the Tenant it creates. */
43
59
  tenantName: string;
44
60
  /**
@@ -46,7 +62,27 @@ export interface StackEnv {
46
62
  * always includes `project`, the Project link's principal.
47
63
  */
48
64
  derivedPrincipals: string;
65
+ /** Present once `nylorun sandbox enable` ran for the Tenant (F7.2). */
66
+ sandboxes?: SandboxStackEnv;
67
+ }
68
+ /**
69
+ * The sandboxes service's settings (`nylorun sandbox enable`): its bearer token (only the
70
+ * runtime presents it), its image, and the host ports pods reach (Harness API, gates,
71
+ * egress) on the host address, published on the bind address. The ports persist.
72
+ */
73
+ export interface SandboxStackEnv {
74
+ /** NYLORUN_SANDBOXES_TOKEN, 32 bytes as hex. */
75
+ token: string;
76
+ image: string;
77
+ harnessPort: number;
78
+ gatesPort: number;
79
+ egressPort: number;
80
+ /** The Docker host as pods reach it (192.168.65.254 on Docker Desktop). */
81
+ hostAddress: string;
82
+ /** Where the pod-facing ports are published (127.0.0.1 on Docker Desktop). */
83
+ bind: string;
49
84
  }
85
+ export type HarnessMode = "remote" | "in-process";
50
86
  export declare function renderEnvFile(env: StackEnv): string;
51
87
  /**
52
88
  * `NYLORUN_DERIVED_PRINCIPALS`: comma-separated principal ids, as the Runtime accepts them
@@ -61,10 +97,16 @@ export interface PersistedStackEnv {
61
97
  adminPort?: number;
62
98
  studioPort?: number;
63
99
  restatePort?: number;
100
+ /** Whether the last start published Restate's UI (`NYLORUN_RESTATE_UI=1`). */
101
+ restateUi?: boolean;
64
102
  postgresPassword?: string;
65
103
  gatesToken?: string;
104
+ harnessToken?: string;
105
+ harness?: HarnessMode;
106
+ objectStoreSecretKey?: string;
66
107
  /** Validated origins; absent when the line is missing (an older .env). */
67
108
  studioFrameAncestors?: string[];
68
109
  derivedPrincipals?: string[];
110
+ sandboxes?: SandboxStackEnv;
69
111
  }
70
112
  export declare function parsePersisted(text: string): PersistedStackEnv;
@@ -9,13 +9,26 @@ export const DEFAULT_STUDIO_FRAME_ANCESTORS = [
9
9
  "nylorun://localhost",
10
10
  "http://nylorun.localhost",
11
11
  ];
12
+ const SANDBOX_KEYS = {
13
+ token: "NYLORUN_SANDBOXES_TOKEN",
14
+ image: "NYLORUN_SANDBOXES_IMAGE",
15
+ harnessPort: "NYLORUN_SANDBOX_HARNESS_PORT",
16
+ gatesPort: "NYLORUN_SANDBOX_GATES_PORT",
17
+ egressPort: "NYLORUN_SANDBOX_EGRESS_PORT",
18
+ hostAddress: "NYLORUN_SANDBOX_HOST_ADDRESS",
19
+ bind: "NYLORUN_SANDBOX_BIND",
20
+ };
12
21
  const KEYS = {
13
22
  runtimePort: "NYLORUN_PORT",
14
23
  adminPort: "NYLORUN_ADMIN_PORT",
15
24
  studioPort: "NYLORUN_STUDIO_PORT",
16
25
  restatePort: "NYLORUN_RESTATE_PORT",
26
+ restateUi: "NYLORUN_RESTATE_UI",
17
27
  postgresPassword: "NYLORUN_POSTGRES_PASSWORD",
18
28
  gatesToken: "NYLORUN_GATES_TOKEN",
29
+ harnessToken: "NYLORUN_HARNESS_TOKEN",
30
+ harness: "NYLORUN_HARNESS",
31
+ objectStoreSecretKey: "NYLORUN_OBJECT_STORE_SECRET_KEY",
19
32
  restateIdentityKey: "NYLORUN_RESTATE_IDENTITY_KEY",
20
33
  uid: "NYLORUN_UID",
21
34
  gid: "NYLORUN_GID",
@@ -24,7 +37,6 @@ const KEYS = {
24
37
  studioImage: "NYLORUN_STUDIO_IMAGE",
25
38
  studioFrameAncestors: "NYLORUN_STUDIO_FRAME_ANCESTORS",
26
39
  studioAnalyticsId: "NYLORUN_STUDIO_ANALYTICS_ID",
27
- studioPublicOrigins: "NYLORUN_STUDIO_PUBLIC_ORIGINS",
28
40
  tenantName: "NYLORUN_TENANT_NAME",
29
41
  derivedPrincipals: "NYLORUN_DERIVED_PRINCIPALS",
30
42
  };
@@ -43,17 +55,25 @@ function unquote(value) {
43
55
  return trimmed;
44
56
  }
45
57
  export function renderEnvFile(env) {
46
- const line = (field) => `${KEYS[field]}=${quote(KEYS[field], String(env[field]))}`;
58
+ const line = (field) => {
59
+ const value = env[field];
60
+ return `${KEYS[field]}=${quote(KEYS[field], typeof value === "boolean" ? (value ? "1" : "0") : String(value))}`;
61
+ };
62
+ const sandboxes = env.sandboxes;
63
+ const sandboxLine = (field) => `${SANDBOX_KEYS[field]}=${quote(SANDBOX_KEYS[field], String(sandboxes[field]))}`;
47
64
  return [
48
- "# Written by `nylorun start`. Mode 0600: holds the Postgres password and the",
49
- "# gates token. Ports, the password and the token are kept across starts;",
50
- "# images, UID/GID and the Host root are refreshed on every start.",
65
+ "# Written by `nylorun start`. Mode 0600: holds the Postgres password, the gates",
66
+ "# token and the Object store's secret key. Ports and secrets are kept across",
67
+ "# starts; images, UID/GID and the Host root are refreshed on every start.",
51
68
  "",
52
69
  "# Published on 127.0.0.1; clients use http://localhost:<port>.",
53
70
  line("runtimePort"),
54
71
  line("adminPort"),
55
72
  line("studioPort"),
73
+ "# Restate's UI and admin (unauthenticated) are published only with",
74
+ "# nylorun start --restate-ui (or NYLORUN_RESTATE_UI=1); decided on every start.",
56
75
  line("restatePort"),
76
+ line("restateUi"),
57
77
  "",
58
78
  line("postgresPassword"),
59
79
  "",
@@ -61,6 +81,16 @@ export function renderEnvFile(env) {
61
81
  "# model call.",
62
82
  line("gatesToken"),
63
83
  "",
84
+ "# The harness container presents this token to the runtime's Harness API; it",
85
+ "# holds no other credential. NYLORUN_HARNESS=in-process runs agent turns, MCP",
86
+ "# servers and workspaces in the runtime container instead (rollback); kept.",
87
+ line("harnessToken"),
88
+ line("harness"),
89
+ "",
90
+ "# RustFS (the Object store) accepts this secret key; only the runtime and the",
91
+ "# gateway receive it.",
92
+ line("objectStoreSecretKey"),
93
+ "",
64
94
  "# Restate signs requests to the Runtime's Worker endpoint with the private",
65
95
  "# key in restate-identity.pem; the Runtime accepts only this public key.",
66
96
  line("restateIdentityKey"),
@@ -82,16 +112,26 @@ export function renderEnvFile(env) {
82
112
  "# it off with nylorun telemetry disable or NYLORUN_TELEMETRY_DISABLED=1.",
83
113
  line("studioAnalyticsId"),
84
114
  "",
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
115
  "# The Tenant's name, which the Runtime gives the Tenant it creates on the first",
90
116
  "# start, and the derived principals it registers on that Tenant (keys derived",
91
117
  "# from the admin key; `project` is the Project link's).",
92
118
  line("tenantName"),
93
119
  line("derivedPrincipals"),
94
120
  "",
121
+ ...(sandboxes
122
+ ? [
123
+ "# Sandboxes (nylorun sandbox enable): the runtime's token for the sandboxes",
124
+ "# service, its image, and the host ports pods reach on the host address.",
125
+ sandboxLine("token"),
126
+ sandboxLine("image"),
127
+ sandboxLine("harnessPort"),
128
+ sandboxLine("gatesPort"),
129
+ sandboxLine("egressPort"),
130
+ sandboxLine("hostAddress"),
131
+ sandboxLine("bind"),
132
+ "",
133
+ ]
134
+ : []),
95
135
  ].join("\n");
96
136
  }
97
137
  /**
@@ -128,6 +168,24 @@ function port(value) {
128
168
  const n = Number(value);
129
169
  return n >= 1 && n <= 65535 ? n : undefined;
130
170
  }
171
+ const IPV4 = /^(?:(?:25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|1?\d?\d)$/;
172
+ /** The sandboxes section, when complete and valid. */
173
+ function parseSandboxes(values) {
174
+ const token = values.get(SANDBOX_KEYS.token);
175
+ const image = values.get(SANDBOX_KEYS.image);
176
+ const harnessPort = port(values.get(SANDBOX_KEYS.harnessPort));
177
+ const gatesPort = port(values.get(SANDBOX_KEYS.gatesPort));
178
+ const egressPort = port(values.get(SANDBOX_KEYS.egressPort));
179
+ const hostAddress = values.get(SANDBOX_KEYS.hostAddress);
180
+ const bind = values.get(SANDBOX_KEYS.bind);
181
+ if (!token || !/^[0-9a-f]{64,}$/i.test(token) || !image)
182
+ return undefined;
183
+ if (!harnessPort || !gatesPort || !egressPort)
184
+ return undefined;
185
+ if (!hostAddress || !IPV4.test(hostAddress) || !bind || !IPV4.test(bind))
186
+ return undefined;
187
+ return { token, image, harnessPort, gatesPort, egressPort, hostAddress, bind };
188
+ }
131
189
  export function parsePersisted(text) {
132
190
  const values = parseEnvLines(text);
133
191
  const out = {};
@@ -143,12 +201,23 @@ export function parsePersisted(text) {
143
201
  const restatePort = port(values.get(KEYS.restatePort));
144
202
  if (restatePort)
145
203
  out.restatePort = restatePort;
204
+ if (values.get(KEYS.restateUi) === "1")
205
+ out.restateUi = true;
146
206
  const password = values.get(KEYS.postgresPassword);
147
207
  if (password && /^[A-Za-z0-9]{16,}$/.test(password))
148
208
  out.postgresPassword = password;
149
209
  const gatesToken = values.get(KEYS.gatesToken);
150
210
  if (gatesToken && /^[0-9a-f]{64,}$/i.test(gatesToken))
151
211
  out.gatesToken = gatesToken;
212
+ const harnessToken = values.get(KEYS.harnessToken);
213
+ if (harnessToken && /^[0-9a-f]{64,}$/i.test(harnessToken))
214
+ out.harnessToken = harnessToken;
215
+ const harness = values.get(KEYS.harness);
216
+ if (harness === "remote" || harness === "in-process")
217
+ out.harness = harness;
218
+ const objectStoreSecretKey = values.get(KEYS.objectStoreSecretKey);
219
+ if (objectStoreSecretKey && /^[0-9a-f]{64}$/i.test(objectStoreSecretKey))
220
+ out.objectStoreSecretKey = objectStoreSecretKey;
152
221
  const principals = values.get(KEYS.derivedPrincipals);
153
222
  if (principals !== undefined) {
154
223
  try {
@@ -158,6 +227,9 @@ export function parsePersisted(text) {
158
227
  /* rewritten from the default on the next start */
159
228
  }
160
229
  }
230
+ const sandboxes = parseSandboxes(values);
231
+ if (sandboxes)
232
+ out.sandboxes = sandboxes;
161
233
  const ancestors = values.get(KEYS.studioFrameAncestors);
162
234
  if (ancestors !== undefined) {
163
235
  try {
@@ -26,7 +26,17 @@ async function writeAtomic(path, text) {
26
26
  }
27
27
  /** The Host root layout, mode 0700. */
28
28
  export async function ensureHostLayout(paths) {
29
- for (const dir of [paths.root, paths.home, paths.tmp, paths.tenant, paths.keys, paths.docker])
29
+ // The harness container's mounts exist before Compose binds them (else Docker makes them as root).
30
+ for (const dir of [
31
+ paths.root,
32
+ paths.home,
33
+ paths.tmp,
34
+ paths.tenant,
35
+ ...Object.values(paths.harness),
36
+ paths.plugins,
37
+ paths.keys,
38
+ paths.docker,
39
+ ])
30
40
  await mkdir(dir, { recursive: true, mode: 0o700 });
31
41
  }
32
42
  export async function readHostConfig(paths) {
@@ -1,22 +1,26 @@
1
1
  /**
2
2
  * Images a local Tenant runs. The Runtime and Studio tags are pinned by this
3
3
  * CLI release; `NYLORUN_RUNTIME_IMAGE` and `NYLORUN_STUDIO_IMAGE` override them
4
- * (local builds, CI images). Postgres, Restate and s2-lite are the official
5
- * images, pinned here, as is Caddy for the machine's Studio proxy.
4
+ * (local builds, CI images). The sandboxes service (`nylorun sandbox enable`) is
5
+ * versioned with the Runtime: `ghcr.io/nylorun/sandboxes:<runtime pin>`, or
6
+ * `NYLORUN_SANDBOXES_IMAGE`. Postgres, Restate, s2-lite and RustFS are the
7
+ * official images, pinned here; RustFS (the Object store, D35) by digest as well, so
8
+ * it is upgraded only on purpose.
6
9
  */
7
10
  export declare const PINNED_IMAGES: {
8
11
  readonly postgres: "postgres:17.11";
9
12
  readonly restate: "docker.restate.dev/restatedev/restate:1.7.12";
10
13
  readonly s2: "ghcr.io/s2-streamstore/s2:0.43.0";
11
- readonly proxy: "caddy:2.11.6";
14
+ readonly rustfs: "rustfs/rustfs:1.0.1@sha256:1803faef57627e2d9c2e7d89d655d712ddded5389040054987163043fecb6a3c";
12
15
  };
13
16
  export interface StackImages {
14
17
  runtime: string;
15
18
  studio: string;
19
+ sandboxes: string;
16
20
  postgres: string;
17
21
  restate: string;
18
22
  s2: string;
19
- proxy: string;
23
+ rustfs: string;
20
24
  }
21
25
  /** `NYLORUN_RUNTIME_IMAGE` names the Runtime image, so its version is unknown here. */
22
26
  export declare function runtimeImageOverridden(env: Readonly<Record<string, string | undefined>>): boolean;