nylorun 0.28.0-beta → 0.29.1-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # nylorun
2
2
 
3
+ ## 0.29.1-beta
4
+
5
+ ### Patch Changes
6
+
7
+ - Pin runtime to the tested release.
8
+
9
+ ## 0.29.0-beta
10
+
11
+ ### Minor Changes
12
+
13
+ - f87ba81: Pods on Agent Substrate (preview, ADR-138 Phase 1a). `nylorun local sandbox enable --context <name> --backend substrate` runs the Tenant's sandboxes as Agent Substrate v1.0 actors instead of agent-sandbox Sandboxes, on a Kubernetes 1.37 cluster with Substrate installed (Docker Desktop's kind cluster, or Substrate's own kind script). It creates the Tenant's namespace and the sandboxes service's RBAC, makes the Tenant's atespace with the service as its editor, gives actors a name for the edge in CoreDNS, and publishes the edge's two ports from the `sandboxes` service: a TLS front, in that container, through which actors reach the Harness API and the gates. The default backend stays agent-sandbox, and nothing changes for a Tenant that does not pass `--backend substrate`.
14
+
15
+ The sandboxes service drives Substrate's API: a template per pod incarnation, pinned by digest; an egress policy from the Agent's network (its preset, hosts and suffixes, which the core now sends with each pod); suspend and resume for `mode`; and the same `/v1/pods` contract, so the core is unchanged. Not yet on Substrate: keys injected at egress, builds and tags; those come in Phase 1b.
16
+
17
+ ### Patch Changes
18
+
19
+ - 0c6634b: `GET /v1/tenant/sandbox`'s `cluster` names its `backend` (`agent-sandbox` or `substrate`) and, on Agent Substrate, its `atespace`. `nylorun doctor sandbox` prints the Tenant's pods line (backend, context, namespace, ready), and Studio's Sandbox section says Agent Substrate with the atespace.
20
+ - Pin core to the tested release.
21
+ - Pin agents to the tested release.
22
+ - Pin admin to the tested release.
23
+ - Pin runtime to the tested release.
24
+ - Pin studio to the tested release.
25
+ - Updated dependencies [0c6634b]
26
+ - Updated dependencies
27
+ - Updated dependencies
28
+ - @nylorun/core@0.39.0-beta
29
+ - @nylorun/agents@0.36.2-beta
30
+ - @nylorun/admin@0.19.15-beta
31
+
3
32
  ## 0.28.0-beta
4
33
 
5
34
  ### Minor Changes
package/README.md CHANGED
@@ -119,7 +119,7 @@ nylorun local ls [--json] # the Tenants on this machine, with their sta
119
119
  nylorun local delete <tenant> --yes # remove a Tenant: containers, volumes, Host root and vault key
120
120
  nylorun local logs [service] [--tenant <name>] [-f] [--tail <n>] # postgres, restate, s2-lite, rustfs, gateway, runtime, harness, studio, sandboxes
121
121
  nylorun local reset [--tenant <name>] [--yes] # delete the Tenant's volumes, Tenant directory and vault key; the next start creates it anew
122
- nylorun local sandbox enable --context <name> [--tenant <name>] [--host-address <ip>] [--bind-address <ip>] [--probe-ports <allowed>,<denied>] [--no-pull]
122
+ nylorun local sandbox enable --context <name> [--tenant <name>] [--backend agent-sandbox|substrate] [--host-address <ip>] [--bind-address <ip>] [--probe-ports <allowed>,<denied>] [--no-pull]
123
123
  nylorun local sandbox disable [--tenant <name>] [--delete-namespace]
124
124
  nylorun local sandbox status [--tenant <name>] [--json] # pods on a Kubernetes context (see Sandboxes)
125
125
  nylorun local telemetry [status|enable|disable] # Studio's anonymous usage analytics
@@ -446,6 +446,32 @@ then run `enable` again.
446
446
  the namespace and every sandbox in it). Sessions run in a pod when their
447
447
  Agent's manifest has `runtime.kind` `pod`.
448
448
 
449
+ ### Pods on Agent Substrate (preview)
450
+
451
+ `nylorun local sandbox enable --context <name> --backend substrate` runs the Tenant's pods as
452
+ [Agent Substrate](https://github.com/agent-substrate/substrate) v1.0 actors instead
453
+ ([ADR-138](https://github.com/nylorun/factory/blob/main/decisions/engine/2026-11-09-substrate-replaces-agent-sandbox.md),
454
+ Phase 1). Actors suspend to a snapshot of their memory and resume where they stopped, which suits
455
+ sessions that are short and mostly asleep. The cluster needs Kubernetes 1.37 with Substrate v1.0
456
+ installed (its `ate-system` namespace and `api` Service): a kind cluster on Docker Desktop, made
457
+ with Substrate's own kind script, works. `enable` refuses a context without Substrate.
458
+
459
+ It only touches the context you name. It creates the namespace `nylorun-sbx-<tenant>` and a
460
+ ServiceAccount allowed only what the sandboxes service uses (its state ConfigMaps, a token for
461
+ itself, a port-forward to Substrate's API and Substrate's trust bundles), and adds a `hosts` entry
462
+ to CoreDNS so that actors resolve the Tenant's edge, `nylorun-sbx-<tenant>.edge.nylorun.internal`,
463
+ to the Docker host. The edge is a TLS front in the `sandboxes` container, published on two host
464
+ ports (8793 and 8794 by default), through which actors reach the Harness API and the gates; its
465
+ certificate is made for that name and its CA is given to each actor. Then, as one of Substrate's
466
+ owners (a 10-minute token for `ate-system/ate-client`, deleted after use), it makes the Tenant's
467
+ atespace with the sandboxes service as its editor.
468
+
469
+ Each Agent's network (preset, hosts, suffixes) becomes its actors' egress policy: only the edge
470
+ and the allowed hosts resolve and connect. Keys injected at egress, builds and tags are not on
471
+ Substrate yet. `nylorun local sandbox status` names the backend, the atespace and the edge.
472
+ `disable` works as for agent-sandbox; the atespace stays until you delete it with Substrate's
473
+ tools.
474
+
449
475
  ## Host root
450
476
 
451
477
  The **Host root** is `~/.nylorun/tenants/<name>/`, or `NYLORUN_HOME`. It is
@@ -20,6 +20,15 @@ export type SandboxReport = {
20
20
  environments?: number;
21
21
  };
22
22
  };
23
+ /** The Tenant's cluster for pod Agents (`nylorun local sandbox enable`), when it has one. */
24
+ cluster?: {
25
+ namespace: string;
26
+ context: string;
27
+ ready: boolean;
28
+ backend?: "agent-sandbox" | "substrate";
29
+ atespace?: string;
30
+ controllerVersion?: string;
31
+ } | null;
23
32
  };
24
33
  /**
25
34
  * `nylorun doctor sandbox`: the Tenant's sandbox report (`GET /v1/tenant/sandbox`, Management API)
@@ -59,6 +59,18 @@ export async function doctorSandbox(deps, options) {
59
59
  .filter((item) => item !== undefined)
60
60
  .join(", ") || "-",
61
61
  ]);
62
+ const cluster = report.cluster;
63
+ if (cluster)
64
+ rows.push([
65
+ "pods",
66
+ [
67
+ cluster.backend === "substrate"
68
+ ? `Agent Substrate (preview), atespace ${cluster.atespace ?? "?"}`
69
+ : `agent-sandbox${cluster.controllerVersion ? ` ${cluster.controllerVersion}` : ""}`,
70
+ `on ${cluster.context}, namespace ${cluster.namespace}`,
71
+ cluster.ready ? "ready" : "not ready",
72
+ ].join(", "),
73
+ ]);
62
74
  rows.push([
63
75
  "selected",
64
76
  report.backend
@@ -28,6 +28,23 @@ export interface ClusterFile {
28
28
  probedAt: string;
29
29
  };
30
30
  enabledAt: string;
31
+ /** agent-sandbox when absent; substrate for Agent Substrate (ADR-138, preview). */
32
+ backend?: "agent-sandbox" | "substrate";
33
+ /** Agent Substrate: where the Tenant's actors live and the edge actors reach the Runtime by. */
34
+ substrate?: {
35
+ atespace: string;
36
+ systemNamespace: string;
37
+ apiService: string;
38
+ serviceAccount: string;
39
+ endpoint?: string;
40
+ version?: string;
41
+ edge: {
42
+ host: string;
43
+ harnessPort: number;
44
+ gatesPort: number;
45
+ };
46
+ workerSelector?: Record<string, string>;
47
+ };
31
48
  }
32
49
  export declare function sandboxesDir(root: string): string;
33
50
  export declare function clusterFilePath(root: string): string;
@@ -3,7 +3,7 @@ import type { FetchLike } from "../stack/studio-login.js";
3
3
  import { type ClusterFile } from "./cluster-file.js";
4
4
  import { type Kube, type KubectlRunner } from "./kubectl.js";
5
5
  import { type ProbePorts } from "./probe.js";
6
- export declare const sandboxUsage = " sandbox enable --context <name> [--tenant <name>] [--host-address <ip>] [--bind-address <ip>] [--probe-ports <allowed>,<denied>] [--no-pull]\n run the Tenant's sandboxes as pods on that kubeconfig context (agent-sandbox v1.0.10)\n sandbox disable [--tenant <name>] [--delete-namespace]\n stop the sandboxes service and forget the cluster\n sandbox status [--tenant <name>] [--json]\n the cluster, the sandboxes service's readiness and what it reports";
6
+ export declare const sandboxUsage = " sandbox enable --context <name> [--tenant <name>] [--backend agent-sandbox|substrate] [--host-address <ip>] [--bind-address <ip>] [--probe-ports <allowed>,<denied>] [--no-pull]\n run the Tenant's sandboxes as pods on that kubeconfig context (agent-sandbox v1.0.10),\n or as Agent Substrate actors (--backend substrate, preview: Kubernetes 1.37 with Substrate v1.0 installed)\n sandbox disable [--tenant <name>] [--delete-namespace]\n stop the sandboxes service and forget the cluster\n sandbox status [--tenant <name>] [--json]\n the cluster, the sandboxes service's readiness and what it reports";
7
7
  /** The default workload image (D36), pre-pulled with the Runtime image. */
8
8
  export declare const DEFAULT_SANDBOX_IMAGE = "python:3.13-slim";
9
9
  export interface SandboxDeps {
@@ -21,6 +21,8 @@ export interface SandboxDeps {
21
21
  * (seen on GKE right after the apply).
22
22
  */
23
23
  export declare function waitForCrd(k: Kube, pollMs: number): Promise<void>;
24
+ /** The ServiceAccount token the token controller writes into the Secret. */
25
+ export declare function serviceAccountToken(k: Kube, namespace: string, pollMs: number): Promise<string>;
24
26
  export interface InstallResult {
25
27
  cluster: ClusterFile;
26
28
  /** The ServiceAccount token. */
@@ -45,5 +47,29 @@ export declare function installCluster(deps: SandboxDeps, input: {
45
47
  egress: number;
46
48
  };
47
49
  }): Promise<InstallResult>;
50
+ /**
51
+ * `enable --backend substrate` against one context (ADR-138, preview): checks Substrate is
52
+ * installed, makes the Tenant's namespace and RBAC, finds the Docker host's address as pods see
53
+ * it, and names the edge for actors in CoreDNS. The atespace is made later, by the sandboxes
54
+ * image's substrate-setup, once cluster.json is written.
55
+ */
56
+ export declare function installSubstrate(deps: SandboxDeps, input: {
57
+ context: string;
58
+ tenant: string;
59
+ hostAddress?: string;
60
+ bindAddress?: string;
61
+ ports: {
62
+ harness: number;
63
+ gates: number;
64
+ egress: number;
65
+ };
66
+ edgePorts: {
67
+ harness: number;
68
+ gates: number;
69
+ };
70
+ }): Promise<{
71
+ cluster: ClusterFile;
72
+ token: string;
73
+ }>;
48
74
  /** `nylorun local sandbox <enable|disable|status>`. */
49
75
  export declare function runSandboxCommand(args: readonly string[], deps: SandboxDeps): Promise<number>;
@@ -9,15 +9,21 @@ import { readStackEnv } from "../stack/prepare.js";
9
9
  import { apiRoute, readClusterFile, removeClusterFiles, writeClusterFiles, } from "./cluster-file.js";
10
10
  import { contexts, kube } from "./kubectl.js";
11
11
  import { CONTROLLER_DEPLOYMENT, CONTROLLER_NAMESPACE, CONTROLLER_SHA256, CONTROLLER_URL, CONTROLLER_VERSION, networkPolicy, SANDBOX_API_VERSION, SANDBOX_CRD, tenantLabel, tenantManifests, tenantNamespace, TOKEN_SECRET, } from "./manifests.js";
12
- import { assertIPv4, parseProbePorts, runProbe } from "./probe.js";
12
+ import { assertIPv4, defaultBind, parseNslookup, parseProbePorts, runProbe } from "./probe.js";
13
+ import { corefileWithEdge, edgeHost, PREFERRED_EDGE_PORTS, SUBSTRATE_API_SERVICE, SUBSTRATE_AUDIENCE, SUBSTRATE_NAMESPACE, SUBSTRATE_OWNER_ACCOUNT, substrateManifests, } from "./substrate.js";
14
+ import { PROBE_IMAGE, SERVICE_ACCOUNT } from "./manifests.js";
15
+ import { join } from "node:path";
16
+ import { rm, writeFile } from "node:fs/promises";
17
+ import { sandboxesDir } from "./cluster-file.js";
13
18
  import { cliTuning } from "../tuning.js";
14
- export const sandboxUsage = ` sandbox enable --context <name> [--tenant <name>] [--host-address <ip>] [--bind-address <ip>] [--probe-ports <allowed>,<denied>] [--no-pull]
15
- run the Tenant's sandboxes as pods on that kubeconfig context (agent-sandbox ${CONTROLLER_VERSION})
19
+ export const sandboxUsage = ` sandbox enable --context <name> [--tenant <name>] [--backend agent-sandbox|substrate] [--host-address <ip>] [--bind-address <ip>] [--probe-ports <allowed>,<denied>] [--no-pull]
20
+ run the Tenant's sandboxes as pods on that kubeconfig context (agent-sandbox ${CONTROLLER_VERSION}),
21
+ or as Agent Substrate actors (--backend substrate, preview: Kubernetes 1.37 with Substrate v1.0 installed)
16
22
  sandbox disable [--tenant <name>] [--delete-namespace]
17
23
  stop the sandboxes service and forget the cluster
18
24
  sandbox status [--tenant <name>] [--json]
19
25
  the cluster, the sandboxes service's readiness and what it reports`;
20
- const ENABLE_USAGE = "nylorun local sandbox enable --context <name> [--tenant <name>] [--host-address <ip>] [--bind-address <ip>] [--probe-ports <allowed>,<denied>] [--no-pull]";
26
+ const ENABLE_USAGE = "nylorun local sandbox enable --context <name> [--tenant <name>] [--backend agent-sandbox|substrate] [--host-address <ip>] [--bind-address <ip>] [--probe-ports <allowed>,<denied>] [--no-pull]";
21
27
  /** The default workload image (D36), pre-pulled with the Runtime image. */
22
28
  export const DEFAULT_SANDBOX_IMAGE = "python:3.13-slim";
23
29
  /** Preferred host ports for the Harness API, gates and egress (pods reach them). */
@@ -82,7 +88,7 @@ export async function waitForCrd(k, pollMs) {
82
88
  }
83
89
  }
84
90
  /** The ServiceAccount token the token controller writes into the Secret. */
85
- async function serviceAccountToken(k, namespace, pollMs) {
91
+ export async function serviceAccountToken(k, namespace, pollMs) {
86
92
  const deadline = Date.now() + cliTuning().sandbox.tokenWaitMs;
87
93
  for (;;) {
88
94
  const secret = await k.getJson(["secret", TOKEN_SECRET, "-n", namespace]);
@@ -153,14 +159,122 @@ export async function installCluster(deps, input) {
153
159
  notPulled: probe.notPulled,
154
160
  };
155
161
  }
162
+ /**
163
+ * `enable --backend substrate` against one context (ADR-138, preview): checks Substrate is
164
+ * installed, makes the Tenant's namespace and RBAC, finds the Docker host's address as pods see
165
+ * it, and names the edge for actors in CoreDNS. The atespace is made later, by the sandboxes
166
+ * image's substrate-setup, once cluster.json is written.
167
+ */
168
+ export async function installSubstrate(deps, input) {
169
+ const k = kube(deps.kubectl, input.context);
170
+ const pollMs = deps.pollMs ?? cliTuning().sandbox.pollMs;
171
+ const { server, caData } = await readKubeconfig(k);
172
+ let route;
173
+ try {
174
+ route = apiRoute(server);
175
+ }
176
+ catch (error) {
177
+ throw new CliError(`Context ${input.context}: ${error instanceof Error ? error.message : String(error)}`, 6);
178
+ }
179
+ const service = await k.getJson([
180
+ "service", SUBSTRATE_API_SERVICE, "-n", SUBSTRATE_NAMESPACE,
181
+ ]);
182
+ if (!service)
183
+ throw new CliError(`Refusing context ${input.context}: Agent Substrate is not installed (no ${SUBSTRATE_NAMESPACE}/${SUBSTRATE_API_SERVICE} Service). Install Substrate v1.0 first; see "Pods on Agent Substrate" in the CLI README.`, 6);
184
+ const namespace = tenantNamespace(input.tenant);
185
+ const existing = await k.getJson(["namespace", namespace]);
186
+ const owner = existing?.metadata?.labels?.["dev.nylorun.tenant"];
187
+ if (existing && owner !== tenantLabel(input.tenant))
188
+ throw new CliError(`Namespace ${namespace} exists in ${input.context} but belongs to ${owner ?? "something else"}.`, 6);
189
+ await k.apply("Creating the Tenant's namespace and RBAC", [...tenantManifests(namespace, input.tenant), ...substrateManifests(namespace)]);
190
+ const token = await serviceAccountToken(k, namespace, pollMs);
191
+ let hostAddress = input.hostAddress;
192
+ if (!hostAddress) {
193
+ const lookup = await k.run(["run", "nylorun-host-lookup", "-n", namespace, "--rm", "-i", "--quiet", "--restart=Never", `--image=${PROBE_IMAGE}`, "--", "nslookup", "host.docker.internal"], { timeoutMs: cliTuning().sandbox.probeExecMs * 3 });
194
+ hostAddress = parseNslookup(lookup.stdout);
195
+ if (!hostAddress)
196
+ throw new CliError(`Refusing context ${input.context}: pods cannot resolve host.docker.internal. Pass --host-address with the Docker host's address as pods reach it (172.17.0.1 for kind on Linux).`, 6);
197
+ }
198
+ const bindAddress = input.bindAddress ?? defaultBind(input.context, hostAddress);
199
+ const host = edgeHost(namespace);
200
+ const coredns = await k.getJson(["configmap", "coredns", "-n", "kube-system"]);
201
+ const corefile = coredns?.data?.Corefile;
202
+ const patched = corefile === undefined ? undefined : corefileWithEdge(corefile, host, hostAddress);
203
+ if (patched === undefined)
204
+ deps.stack.err(`Warning: could not add ${host} -> ${hostAddress} to CoreDNS in ${input.context}; actors resolve the edge only once that name resolves (add it to the cluster's DNS).`);
205
+ else if (patched !== corefile)
206
+ await k.check("Naming the edge in CoreDNS", ["patch", "configmap", "coredns", "-n", "kube-system", "--type", "merge", "-p", JSON.stringify({ data: { Corefile: patched } })]);
207
+ return {
208
+ cluster: {
209
+ context: input.context,
210
+ server,
211
+ dial: route.dial,
212
+ tlsServerName: route.tlsServerName,
213
+ caData,
214
+ namespace,
215
+ controllerVersion: "",
216
+ hostAddress,
217
+ bindAddress,
218
+ ports: input.ports,
219
+ networkPolicy: { enforced: false, probedAt: "" },
220
+ enabledAt: new Date().toISOString(),
221
+ backend: "substrate",
222
+ substrate: {
223
+ atespace: namespace,
224
+ systemNamespace: SUBSTRATE_NAMESPACE,
225
+ apiService: SUBSTRATE_API_SERVICE,
226
+ serviceAccount: SERVICE_ACCOUNT,
227
+ edge: { host, harnessPort: input.edgePorts.harness, gatesPort: input.edgePorts.gates },
228
+ },
229
+ },
230
+ token,
231
+ };
232
+ }
233
+ /**
234
+ * The edge's certificate and the Tenant's atespace, with the sandboxes image: `edge-cert` writes
235
+ * the CA and certificate next to cluster.json, and `substrate-setup` makes the atespace and its
236
+ * access policy as Substrate's owner client, with a 10-minute token minted from the kubeconfig.
237
+ */
238
+ async function prepareSubstrate(deps, root, cluster, image) {
239
+ const { stack } = deps;
240
+ const dir = sandboxesDir(root);
241
+ const user = `${stack.uid}:${stack.gid}`;
242
+ const cert = await stack.docker.run([
243
+ "run", "--rm", "--user", user, "-v", `${dir}:/out`, image, "edge-cert", cluster.substrate.edge.host, "/out",
244
+ ]);
245
+ if (cert.code !== 0)
246
+ throw new CliError(`Making the edge's certificate failed: ${cert.stderr.trim() || cert.stdout.trim()}`, 1);
247
+ const k = kube(deps.kubectl, cluster.context);
248
+ const owner = (await k.check("Minting Substrate's owner token", [
249
+ "create", "token", SUBSTRATE_OWNER_ACCOUNT, "-n", SUBSTRATE_NAMESPACE, "--audience", SUBSTRATE_AUDIENCE, "--duration", "10m",
250
+ ])).trim();
251
+ const tokenFile = join(dir, "owner-token");
252
+ await writeFile(tokenFile, owner, { mode: 0o600 });
253
+ try {
254
+ const setup = await stack.docker.run([
255
+ "run", "--rm", "--user", user, "--add-host", "host.docker.internal:host-gateway",
256
+ "-v", `${dir}:/run/nylorun/sandboxes:ro`, "-e", "NYLORUN_SUBSTRATE_OWNER_TOKEN_FILE=/run/nylorun/sandboxes/owner-token",
257
+ image, "substrate-setup",
258
+ ]);
259
+ if (setup.code !== 0)
260
+ throw new CliError(`Making the Tenant's atespace failed: ${setup.stderr.trim() || setup.stdout.trim()}`, 1);
261
+ stack.err(setup.stdout.trim());
262
+ }
263
+ finally {
264
+ await rm(tokenFile, { force: true });
265
+ }
266
+ }
156
267
  async function enable(deps, args) {
157
268
  const { stack } = deps;
158
- const flags = parseStackFlags(args, { booleans: ["--no-pull"], values: ["--context", "--tenant", "--host-address", "--bind-address", "--probe-ports"] }, ENABLE_USAGE);
269
+ const flags = parseStackFlags(args, { booleans: ["--no-pull"], values: ["--context", "--tenant", "--backend", "--host-address", "--bind-address", "--probe-ports"] }, ENABLE_USAGE);
159
270
  if (flags.rest.length)
160
271
  throw usageError(`Usage: ${ENABLE_USAGE}`);
161
272
  const context = flags.values.get("--context");
162
273
  if (!context)
163
274
  throw usageError(`nylorun local sandbox enable needs --context <name>: the kubeconfig context to run pods in (never the current one implicitly). Usage: ${ENABLE_USAGE}`);
275
+ const backend = flags.values.get("--backend") ?? "agent-sandbox";
276
+ if (backend !== "agent-sandbox" && backend !== "substrate")
277
+ throw usageError(`--backend must be agent-sandbox or substrate, not ${backend}. Usage: ${ENABLE_USAGE}`);
164
278
  const hostAddress = flags.values.get("--host-address");
165
279
  const bindAddress = flags.values.get("--bind-address");
166
280
  if (hostAddress)
@@ -188,6 +302,42 @@ async function enable(deps, args) {
188
302
  gates: await pick(PREFERRED_PORTS.gates, persisted?.gatesPort),
189
303
  egress: await pick(PREFERRED_PORTS.egress, persisted?.egressPort),
190
304
  };
305
+ if (backend === "substrate") {
306
+ const edgePorts = {
307
+ harness: await pick(PREFERRED_EDGE_PORTS.harness, persisted?.edgeHarnessPort),
308
+ gates: await pick(PREFERRED_EDGE_PORTS.gates, persisted?.edgeGatesPort),
309
+ };
310
+ const images = stackImages(stack.env, { runtime: stack.runtimeVersion, studio: stack.studioVersion });
311
+ const installed = await installSubstrate(deps, {
312
+ context,
313
+ tenant: tenant.name,
314
+ ...(hostAddress ? { hostAddress } : {}),
315
+ ...(bindAddress ? { bindAddress } : {}),
316
+ ports,
317
+ edgePorts,
318
+ });
319
+ await writeClusterFiles(tenant.paths.root, installed.cluster, installed.token);
320
+ await prepareSubstrate(deps, tenant.paths.root, installed.cluster, images.sandboxes);
321
+ const settings = {
322
+ token: persisted?.token ?? randomBytes(32).toString("hex"),
323
+ image: images.sandboxes,
324
+ harnessPort: ports.harness,
325
+ gatesPort: ports.gates,
326
+ egressPort: ports.egress,
327
+ hostAddress: installed.cluster.hostAddress,
328
+ bind: installed.cluster.bindAddress,
329
+ edgeHarnessPort: edgePorts.harness,
330
+ edgeGatesPort: edgePorts.gates,
331
+ };
332
+ await restartTenant(stack, { name: tenant.name, sandboxes: settings });
333
+ const ready = await stack.docker.stream(tenant.compose("up", "--detach", "--wait", "--wait-timeout", String(cliTuning().sandbox.composeWaitSeconds), "sandboxes"));
334
+ if (ready !== 0)
335
+ throw new CliError(`The sandboxes service did not become ready (image ${images.sandboxes}). See "nylorun local logs sandboxes" and "nylorun local sandbox status".`, 7);
336
+ const c = installed.cluster;
337
+ stack.out(`Sandboxes ${tenant.name} enabled on ${c.context} (backend substrate, atespace ${c.substrate.atespace}; preview)`);
338
+ stack.out(`Actors reach the edge ${c.substrate.edge.host} (${c.hostAddress}) on ports ${edgePorts.harness} and ${edgePorts.gates}`);
339
+ return 0;
340
+ }
191
341
  if (probePorts && [probePorts.allowed, probePorts.denied].some((port) => taken.has(port)))
192
342
  throw usageError(`--probe-ports ${probePortsFlag} overlaps the sandbox ports ${ports.harness}, ${ports.gates} and ${ports.egress}; pick two others.`);
193
343
  const images = stackImages(stack.env, { runtime: stack.runtimeVersion, studio: stack.studioVersion });
@@ -283,9 +433,17 @@ async function status(deps, args) {
283
433
  }
284
434
  else {
285
435
  stack.out(`Context ${cluster.context} (${cluster.server}, dialled at ${cluster.dial})`);
286
- stack.out(`Namespace ${cluster.namespace} agent-sandbox ${cluster.controllerVersion}`);
287
- stack.out(`Pods reach ${cluster.hostAddress} ports ${cluster.ports.harness}, ${cluster.ports.gates}, ${cluster.ports.egress} (bound on ${cluster.bindAddress})`);
288
- stack.out(`Policy ${cluster.networkPolicy.enforced ? "enforced" : "NOT enforced"} (probed ${cluster.networkPolicy.probedAt})`);
436
+ if (cluster.backend === "substrate" && cluster.substrate) {
437
+ stack.out(`Namespace ${cluster.namespace} backend substrate (preview), atespace ${cluster.substrate.atespace}`);
438
+ stack.out(`Edge ${cluster.substrate.edge.host} ports ${cluster.substrate.edge.harnessPort}, ${cluster.substrate.edge.gatesPort} (bound on ${cluster.bindAddress}, actors reach ${cluster.hostAddress})`);
439
+ }
440
+ else {
441
+ stack.out(`Namespace ${cluster.namespace} agent-sandbox ${cluster.controllerVersion}`);
442
+ }
443
+ if (cluster.backend !== "substrate") {
444
+ stack.out(`Pods reach ${cluster.hostAddress} ports ${cluster.ports.harness}, ${cluster.ports.gates}, ${cluster.ports.egress} (bound on ${cluster.bindAddress})`);
445
+ stack.out(`Policy ${cluster.networkPolicy.enforced ? "enforced" : "NOT enforced"} (probed ${cluster.networkPolicy.probedAt})`);
446
+ }
289
447
  stack.out(`Service ${ready ? "ready" : "not ready"}${info ? "" : " (no /v1/info answer)"}`);
290
448
  }
291
449
  return ready ? 0 : 3;
@@ -0,0 +1,25 @@
1
+ /** Substrate v1.0's install: its control plane's namespace, API Service and owner client. */
2
+ export declare const SUBSTRATE_NAMESPACE = "ate-system";
3
+ export declare const SUBSTRATE_API_SERVICE = "api";
4
+ export declare const SUBSTRATE_OWNER_ACCOUNT = "ate-client";
5
+ /** The audience ate-api-server accepts: its Service's DNS name. */
6
+ export declare const SUBSTRATE_AUDIENCE = "api.ate-system.svc";
7
+ /** Preferred host ports for the edge: the Harness API's and the gates' TLS fronts. */
8
+ export declare const PREFERRED_EDGE_PORTS: {
9
+ readonly harness: 8793;
10
+ readonly gates: 8794;
11
+ };
12
+ /** The name actors dial for the edge, unique to the Tenant's namespace. */
13
+ export declare function edgeHost(namespace: string): string;
14
+ /**
15
+ * RBAC for the sandboxes service on Substrate: in the Tenant's namespace, its state ConfigMaps
16
+ * and a token for its own ServiceAccount (its Substrate identity); in Substrate's namespace, a
17
+ * port-forward to ate-api-server; cluster-wide, reading Substrate's trust bundles.
18
+ */
19
+ export declare function substrateManifests(namespace: string): object[];
20
+ /**
21
+ * CoreDNS's Corefile with `host` resolving to `address` for actors: a `hosts` block in the
22
+ * default server block, marked so a later `enable` replaces it. Undefined when the Corefile has
23
+ * no `.:53` block or already has a `hosts` plugin of its own.
24
+ */
25
+ export declare function corefileWithEdge(corefile: string, host: string, address: string): string | undefined;
@@ -0,0 +1,115 @@
1
+ /**
2
+ * What `nylorun local sandbox enable --backend substrate` adds to a cluster that runs Agent
3
+ * Substrate (ADR-138, Phase 1: preview): the sandboxes service's access to Substrate's API, the
4
+ * Tenant's atespace (made by the sandboxes image's `substrate-setup`), and the name actors reach
5
+ * the edge by, in CoreDNS.
6
+ */
7
+ import { SERVICE_ACCOUNT } from "./manifests.js";
8
+ /** Substrate v1.0's install: its control plane's namespace, API Service and owner client. */
9
+ export const SUBSTRATE_NAMESPACE = "ate-system";
10
+ export const SUBSTRATE_API_SERVICE = "api";
11
+ export const SUBSTRATE_OWNER_ACCOUNT = "ate-client";
12
+ /** The audience ate-api-server accepts: its Service's DNS name. */
13
+ export const SUBSTRATE_AUDIENCE = `${SUBSTRATE_API_SERVICE}.${SUBSTRATE_NAMESPACE}.svc`;
14
+ /** Preferred host ports for the edge: the Harness API's and the gates' TLS fronts. */
15
+ export const PREFERRED_EDGE_PORTS = { harness: 8793, gates: 8794 };
16
+ /** The name actors dial for the edge, unique to the Tenant's namespace. */
17
+ export function edgeHost(namespace) {
18
+ return `${namespace}.edge.nylorun.internal`;
19
+ }
20
+ const MANAGED = { "app.kubernetes.io/managed-by": "nylorun" };
21
+ /**
22
+ * RBAC for the sandboxes service on Substrate: in the Tenant's namespace, its state ConfigMaps
23
+ * and a token for its own ServiceAccount (its Substrate identity); in Substrate's namespace, a
24
+ * port-forward to ate-api-server; cluster-wide, reading Substrate's trust bundles.
25
+ */
26
+ export function substrateManifests(namespace) {
27
+ const subject = [{ kind: "ServiceAccount", name: SERVICE_ACCOUNT, namespace }];
28
+ const name = `nylorun-sandboxes-${namespace}`;
29
+ return [
30
+ {
31
+ apiVersion: "rbac.authorization.k8s.io/v1",
32
+ kind: "Role",
33
+ metadata: { name: `${SERVICE_ACCOUNT}-substrate`, namespace, labels: MANAGED },
34
+ rules: [
35
+ { apiGroups: [""], resources: ["configmaps"], verbs: ["get", "list", "watch", "create", "update", "delete"] },
36
+ { apiGroups: [""], resources: ["serviceaccounts/token"], resourceNames: [SERVICE_ACCOUNT], verbs: ["create"] },
37
+ ],
38
+ },
39
+ {
40
+ apiVersion: "rbac.authorization.k8s.io/v1",
41
+ kind: "RoleBinding",
42
+ metadata: { name: `${SERVICE_ACCOUNT}-substrate`, namespace, labels: MANAGED },
43
+ roleRef: { apiGroup: "rbac.authorization.k8s.io", kind: "Role", name: `${SERVICE_ACCOUNT}-substrate` },
44
+ subjects: subject,
45
+ },
46
+ {
47
+ apiVersion: "rbac.authorization.k8s.io/v1",
48
+ kind: "Role",
49
+ metadata: { name, namespace: SUBSTRATE_NAMESPACE, labels: MANAGED },
50
+ rules: [
51
+ { apiGroups: [""], resources: ["services"], resourceNames: [SUBSTRATE_API_SERVICE], verbs: ["get"] },
52
+ { apiGroups: [""], resources: ["pods"], verbs: ["get", "list"] },
53
+ { apiGroups: [""], resources: ["pods/portforward"], verbs: ["create", "get"] },
54
+ ],
55
+ },
56
+ {
57
+ apiVersion: "rbac.authorization.k8s.io/v1",
58
+ kind: "RoleBinding",
59
+ metadata: { name, namespace: SUBSTRATE_NAMESPACE, labels: MANAGED },
60
+ roleRef: { apiGroup: "rbac.authorization.k8s.io", kind: "Role", name },
61
+ subjects: subject,
62
+ },
63
+ {
64
+ apiVersion: "rbac.authorization.k8s.io/v1",
65
+ kind: "ClusterRole",
66
+ metadata: { name, labels: MANAGED },
67
+ rules: [{ apiGroups: ["certificates.k8s.io"], resources: ["clustertrustbundles"], verbs: ["get", "list", "watch"] }],
68
+ },
69
+ {
70
+ apiVersion: "rbac.authorization.k8s.io/v1",
71
+ kind: "ClusterRoleBinding",
72
+ metadata: { name, labels: MANAGED },
73
+ roleRef: { apiGroup: "rbac.authorization.k8s.io", kind: "ClusterRole", name },
74
+ subjects: subject,
75
+ },
76
+ ];
77
+ }
78
+ const BEGIN = " # nylorun edge (nylorun local sandbox enable --backend substrate)";
79
+ const END = " # end nylorun edge";
80
+ /**
81
+ * CoreDNS's Corefile with `host` resolving to `address` for actors: a `hosts` block in the
82
+ * default server block, marked so a later `enable` replaces it. Undefined when the Corefile has
83
+ * no `.:53` block or already has a `hosts` plugin of its own.
84
+ */
85
+ export function corefileWithEdge(corefile, host, address) {
86
+ const lines = corefile.split("\n");
87
+ const start = lines.indexOf(BEGIN);
88
+ if (start >= 0) {
89
+ const end = lines.indexOf(END, start);
90
+ if (end < 0)
91
+ return undefined;
92
+ const ours = lines.slice(start + 1, end).filter((line) => /^\s+\d+\.\d+\.\d+\.\d+\s+\S/.test(line));
93
+ const entries = new Map(ours.map((line) => [line.trim().split(/\s+/)[1], line.trim().split(/\s+/)[0]]));
94
+ entries.set(host, address);
95
+ lines.splice(start, end - start + 1, ...block(entries));
96
+ return lines.join("\n");
97
+ }
98
+ if (lines.some((line) => /^\s*hosts(\s|$)/.test(line)))
99
+ return undefined;
100
+ const server = lines.findIndex((line) => /^\.:53\s*\{/.test(line.trim()));
101
+ if (server < 0)
102
+ return undefined;
103
+ lines.splice(server + 1, 0, ...block(new Map([[host, address]])));
104
+ return lines.join("\n");
105
+ }
106
+ function block(entries) {
107
+ return [
108
+ BEGIN,
109
+ " hosts {",
110
+ ...[...entries].map(([host, address]) => ` ${address} ${host}`),
111
+ " fallthrough",
112
+ " }",
113
+ END,
114
+ ];
115
+ }
@@ -74,6 +74,8 @@
74
74
  */
75
75
  export declare function renderComposeFile(project: string, name: string, options?: {
76
76
  sandboxes?: true;
77
+ /** The sandboxes container serves the edge (backend substrate, ADR-138). */
78
+ sandboxEdge?: true;
77
79
  harness?: "remote" | "in-process";
78
80
  restateUi?: true;
79
81
  identity?: true;
@@ -327,7 +327,7 @@ ${healthcheck("runtime")} restart: unless-stopped
327
327
  healthcheck: # /healthz needs no session; Host studio:3000 is accepted
328
328
  test: ["CMD", "node", "-e", "require('http').get({port:3000,path:'/healthz',headers:{host:'studio:3000'}},r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))"]
329
329
  ${healthcheck("studio")} restart: unless-stopped
330
- ${remote ? harnessService(project) : ""}${sandboxes ? sandboxesService(project) : ""}
330
+ ${remote ? harnessService(project) : ""}${sandboxes ? sandboxesService(project, options.sandboxEdge === true) : ""}
331
331
  networks:
332
332
  default:
333
333
  name: ${project}
@@ -412,6 +412,15 @@ const SANDBOXES_GATES_PORT = ` ports:
412
412
  - "\${NYLORUN_SANDBOX_BIND:?run nylorun local sandbox enable}:\${NYLORUN_SANDBOX_GATES_PORT:?run nylorun local sandbox enable}:4100" # gates, for sandbox pods (run tokens)
413
413
  - "\${NYLORUN_SANDBOX_BIND:?run nylorun local sandbox enable}:\${NYLORUN_SANDBOX_EGRESS_PORT:?run nylorun local sandbox enable}:4200" # egress-gate, for sandbox pods (egress tokens)
414
414
  `;
415
+ /**
416
+ * The edge (backend substrate, ADR-138): Substrate actors reach the Harness API and the gates
417
+ * over TLS that Substrate passes through; the sandboxes container terminates it and forwards to
418
+ * runtime:4200 and gateway:4100 on \`default\`.
419
+ */
420
+ const SANDBOXES_EDGE_PORTS = `
421
+ ports:
422
+ - "\${NYLORUN_SANDBOX_BIND:?run nylorun local sandbox enable}:\${NYLORUN_SANDBOX_EDGE_HARNESS_PORT:?run nylorun local sandbox enable}:8443" # the edge: the Harness API, for actors
423
+ - "\${NYLORUN_SANDBOX_BIND:?run nylorun local sandbox enable}:\${NYLORUN_SANDBOX_EDGE_GATES_PORT:?run nylorun local sandbox enable}:8444" # the edge: the gates, for actors`;
415
424
  /** egress-gate (F7.2): pods' CONNECT proxy in the gateway. */
416
425
  const SANDBOXES_GATEWAY_ENV = `
417
426
  # egress-gate: the only way out of a sandbox pod, to the hosts its spec allows.
@@ -427,9 +436,9 @@ const SANDBOXES_RUNTIME_MOUNT = `
427
436
  * (NYLORUN_SANDBOX_*_PORT in .env), the Harness API's, the gates' and egress-gate's, are
428
437
  * published by the runtime and the gateway (above).
429
438
  */
430
- function sandboxesService(project) {
439
+ function sandboxesService(project, edge) {
431
440
  return `
432
- sandboxes: # agent-sandbox driver; the only holder of the cluster credentials; not published
441
+ sandboxes: # the sandboxes driver; the only holder of the cluster credentials; not published${edge ? ", but for its edge" : ""}
433
442
  image: \${NYLORUN_SANDBOXES_IMAGE:?run nylorun local sandbox enable}
434
443
  container_name: ${project}-sandboxes
435
444
  labels: *tenant
@@ -443,7 +452,7 @@ function sandboxesService(project) {
443
452
  volumes:
444
453
  # cluster.json (API server, CA, namespace) and the ServiceAccount token, read-only.
445
454
  - \${NYLORUN_HOST_ROOT:?run nylorun start}/sandboxes:/run/nylorun/sandboxes:ro
446
- networks: [default] # the cluster's API server, through host.docker.internal
455
+ networks: [default] # the cluster's API server, through host.docker.internal${edge ? SANDBOXES_EDGE_PORTS : ""}
447
456
  healthcheck: # /ready: informers synced and the API server answers
448
457
  test: ["CMD", "/sandboxes", "healthcheck"]
449
458
  ${healthcheck("sandboxes")} restart: unless-stopped
@@ -74,6 +74,12 @@ export interface SandboxStackEnv {
74
74
  hostAddress: string;
75
75
  /** Where the pod-facing ports are published (127.0.0.1 on Docker Desktop). */
76
76
  bind: string;
77
+ /**
78
+ * With the Substrate backend (ADR-138): the edge's host ports, the TLS front actors reach
79
+ * the Harness API and the gates through, served by the sandboxes container.
80
+ */
81
+ edgeHarnessPort?: number;
82
+ edgeGatesPort?: number;
77
83
  }
78
84
  export type HarnessMode = "remote" | "in-process";
79
85
  export declare function renderEnvFile(env: StackEnv): string;
@@ -13,6 +13,8 @@ const SANDBOX_KEYS = {
13
13
  egressPort: "NYLORUN_SANDBOX_EGRESS_PORT",
14
14
  hostAddress: "NYLORUN_SANDBOX_HOST_ADDRESS",
15
15
  bind: "NYLORUN_SANDBOX_BIND",
16
+ edgeHarnessPort: "NYLORUN_SANDBOX_EDGE_HARNESS_PORT",
17
+ edgeGatesPort: "NYLORUN_SANDBOX_EDGE_GATES_PORT",
16
18
  };
17
19
  const KEYS = {
18
20
  runtimePort: "NYLORUN_PORT",
@@ -120,6 +122,13 @@ export function renderEnvFile(env) {
120
122
  sandboxLine("egressPort"),
121
123
  sandboxLine("hostAddress"),
122
124
  sandboxLine("bind"),
125
+ ...(sandboxes.edgeHarnessPort && sandboxes.edgeGatesPort
126
+ ? [
127
+ "# The edge (backend substrate): the TLS ports actors reach the Runtime through.",
128
+ sandboxLine("edgeHarnessPort"),
129
+ sandboxLine("edgeGatesPort"),
130
+ ]
131
+ : []),
123
132
  "",
124
133
  ]
125
134
  : []),
@@ -161,7 +170,12 @@ function parseSandboxes(values) {
161
170
  return undefined;
162
171
  if (!hostAddress || !IPV4.test(hostAddress) || !bind || !IPV4.test(bind))
163
172
  return undefined;
164
- return { token, image, harnessPort, gatesPort, egressPort, hostAddress, bind };
173
+ const edgeHarnessPort = port(values.get(SANDBOX_KEYS.edgeHarnessPort));
174
+ const edgeGatesPort = port(values.get(SANDBOX_KEYS.edgeGatesPort));
175
+ return {
176
+ token, image, harnessPort, gatesPort, egressPort, hostAddress, bind,
177
+ ...(edgeHarnessPort && edgeGatesPort ? { edgeHarnessPort, edgeGatesPort } : {}),
178
+ };
165
179
  }
166
180
  export function parsePersisted(text) {
167
181
  const values = parseEnvLines(text);
@@ -102,6 +102,7 @@ export async function prepareStack(input) {
102
102
  await writeFileMode(paths.compose, renderComposeFile(input.project, input.name, {
103
103
  harness: env.harness,
104
104
  ...(env.sandboxes ? { sandboxes: true } : {}),
105
+ ...(env.sandboxes?.edgeHarnessPort ? { sandboxEdge: true } : {}),
105
106
  ...(env.restateUi ? { restateUi: true } : {}),
106
107
  // The trusted issuers, when the operator wrote an identity file; a change takes a restart.
107
108
  ...(existsSync(paths.identity) ? { identity: true } : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nylorun",
3
- "version": "0.28.0-beta",
3
+ "version": "0.29.1-beta",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "description": "Set up and run local Nylorun Tenants (Runtime and Studio) with Docker Compose, and act on a Tenant through its Runtime and Management APIs",
@@ -30,9 +30,9 @@
30
30
  "prepack": "npm run build"
31
31
  },
32
32
  "dependencies": {
33
- "@nylorun/admin": "0.19.14-beta",
34
- "@nylorun/agents": "0.36.1-beta",
35
- "@nylorun/core": "0.38.0-beta"
33
+ "@nylorun/admin": "0.19.15-beta",
34
+ "@nylorun/agents": "0.36.2-beta",
35
+ "@nylorun/core": "0.39.0-beta"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@earendil-works/pi-ai": "1.3.0",
@@ -46,7 +46,7 @@
46
46
  "nylo": "dist/nylo.js"
47
47
  },
48
48
  "nylorun": {
49
- "runtime": "0.44.2-beta",
50
- "studio": "0.35.7-beta"
49
+ "runtime": "0.45.1-beta",
50
+ "studio": "0.35.8-beta"
51
51
  }
52
52
  }