nylorun 0.29.8-beta → 0.30.0-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # nylorun
2
2
 
3
+ ## 0.30.0-beta
4
+
5
+ ### Minor Changes
6
+
7
+ - c4758c0: Pods on Agent Substrate (preview): keys now reach commands through Substrate's own egress ([ADR-142](https://github.com/nylorun/factory/blob/main/decisions/platform/2026-11-13-substrate-egress-adds-keys.md)). A command sends its stand-in, with no proxy, and Substrate's egress replaces the header on each request to a host the Agent has a key for, so `curl`, `gh` and SDKs work against keyed hosts, over HTTP/2 too. A removed key now fails closed with 403 instead of sending the stand-in.
8
+
9
+ - Each host the Agent has an `environment_secret` or slot for, and each host its owner connected through the outside credential source, becomes an `https` rule in the actor's egress policy that names a reference, never a value.
10
+ - The keys provider (`sandboxes provider`, from the sandboxes image) runs beside Substrate's egress. For every keyed request it asks egress-gate's new decide route (`POST /nylorun/v1/egress/decide`, token `NYLORUN_EGRESS_KEYS_TOKEN`), which checks that the actor is the sandbox's current incarnation and returns the vault's header value. For a person's key it returns only who owns it, and the provider fetches the value from the outside source's new `/v1/fetch`.
11
+ - The outside credential source's `/v1/hosts` may name each host's header (`{ host, header }`); a plain host name still means `Authorization`.
12
+ - The engine on Substrate writes a trust bundle with Substrate's egress CA and names it in `SSL_CERT_FILE` and the other trust variables.
13
+ - `nylorun local sandbox enable --backend substrate` deploys the keys provider in `ate-system`, publishes the edge's keys port (8795 by default), and checks that Substrate's egress was installed asking it (`--credential-provider='[{"name":"keys.nylorun.dev","address":"nylorun-keys-provider.ate-system.svc:50051"}]'`). When it wasn't, keys stay off and `enable` says how to turn them on. `sandbox status` shows whether keys are on.
14
+
15
+ ### Patch Changes
16
+
17
+ - Pin runtime to the tested release.
18
+
3
19
  ## 0.29.8-beta
4
20
 
5
21
  ### Patch Changes
package/README.md CHANGED
@@ -467,8 +467,21 @@ owners (a 10-minute token for `ate-system/ate-client`, deleted after use), it ma
467
467
  atespace with the sandboxes service as its editor.
468
468
 
469
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.
470
+ and the allowed hosts resolve and connect.
471
+
472
+ Keys reach commands through Substrate's own egress
473
+ ([ADR-142](https://github.com/nylorun/factory/blob/main/decisions/platform/2026-11-13-substrate-egress-adds-keys.md)):
474
+ a command sends its stand-in (`Authorization: Bearer nylorun_managed`) with no proxy, and
475
+ Substrate's egress replaces the header on each request to a host the Agent has a key for, asking
476
+ the keys provider, which asks the Tenant. `enable` deploys the keys provider in `ate-system`
477
+ (`nylorun-keys-provider`, from the sandboxes image), publishes a third edge port (8795 by
478
+ default) for it, and keeps its token in `docker/.env` (`NYLORUN_EGRESS_KEYS_TOKEN`). Substrate's
479
+ egress must be installed asking it:
480
+ `ate-setup deploy ate-system` and `deploy atenet` with
481
+ `--credential-provider='[{"name":"keys.nylorun.dev","address":"nylorun-keys-provider.ate-system.svc:50051"}]'`.
482
+ Without that, `enable` says so and keys stay off. A person's connected keys come from the outside
483
+ credential source the shell names (`NYLORUN_CREDENTIAL_PROVIDER_URL` and `_TOKEN`), which must
484
+ answer `/v1/fetch` and be reachable from the cluster. Builds and tags are not on Substrate yet. `nylorun local sandbox status` names the backend, the atespace, the edge and whether keys are on.
472
485
  `disable` works as for agent-sandbox; the atespace stays until you delete it with Substrate's
473
486
  tools.
474
487
 
@@ -38,12 +38,16 @@ export interface ClusterFile {
38
38
  serviceAccount: string;
39
39
  endpoint?: string;
40
40
  version?: string;
41
+ /** keysPort: where the keys provider reaches the egress service's decide route (ADR-142). */
41
42
  edge: {
42
43
  host: string;
43
44
  harnessPort: number;
44
45
  gatesPort: number;
46
+ keysPort?: number;
45
47
  };
46
48
  workerSelector?: Record<string, string>;
49
+ /** The keys provider's name, once it is deployed and registered with Substrate's egress. */
50
+ keysProvider?: string;
47
51
  };
48
52
  }
49
53
  export declare function sandboxesDir(root: string): string;
@@ -71,5 +71,20 @@ export declare function installSubstrate(deps: SandboxDeps, input: {
71
71
  cluster: ClusterFile;
72
72
  token: string;
73
73
  }>;
74
+ /**
75
+ * Keys at Substrate's egress (ADR-142): deploys the keys provider with this Tenant's entry (the
76
+ * edge's keys port, its token and CA, and the Host's outside credential source when the shell
77
+ * names one), keeping other Tenants' entries, and checks Substrate's egress asks it. Returns
78
+ * whether it does: when not, keys stay off and `enable` says how to turn them on.
79
+ */
80
+ export declare function deployKeysProvider(deps: SandboxDeps, root: string, cluster: ClusterFile, input: {
81
+ image: string;
82
+ port: number;
83
+ token: string;
84
+ outside?: {
85
+ url: string;
86
+ token: string;
87
+ };
88
+ }): Promise<boolean>;
74
89
  /** `nylorun local sandbox <enable|disable|status>`. */
75
90
  export declare function runSandboxCommand(args: readonly string[], deps: SandboxDeps): Promise<number>;
@@ -10,7 +10,7 @@ import { apiRoute, readClusterFile, removeClusterFiles, writeClusterFiles, } fro
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
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";
13
+ import { corefileWithEdge, edgeHost, KEYS_PROVIDER, KEYS_PROVIDER_FLAG, KEYS_PROVIDER_NAME, keysProviderManifests, keysProviderRegistered, PREFERRED_EDGE_PORTS, SUBSTRATE_API_SERVICE, SUBSTRATE_AUDIENCE, SUBSTRATE_NAMESPACE, SUBSTRATE_OWNER_ACCOUNT, substrateManifests, } from "./substrate.js";
14
14
  import { PROBE_IMAGE, SERVICE_ACCOUNT } from "./manifests.js";
15
15
  import { join } from "node:path";
16
16
  import { rm, writeFile } from "node:fs/promises";
@@ -268,6 +268,40 @@ async function prepareSubstrate(deps, root, cluster, image) {
268
268
  await rm(tokenFile, { force: true });
269
269
  }
270
270
  }
271
+ /**
272
+ * Keys at Substrate's egress (ADR-142): deploys the keys provider with this Tenant's entry (the
273
+ * edge's keys port, its token and CA, and the Host's outside credential source when the shell
274
+ * names one), keeping other Tenants' entries, and checks Substrate's egress asks it. Returns
275
+ * whether it does: when not, keys stay off and `enable` says how to turn them on.
276
+ */
277
+ export async function deployKeysProvider(deps, root, cluster, input) {
278
+ const k = kube(deps.kubectl, cluster.context);
279
+ const substrate = cluster.substrate;
280
+ const edgeCa = await readFile(join(sandboxesDir(root), "edge-ca.pem"), "utf8");
281
+ const existing = await k.getJson(["secret", KEYS_PROVIDER, "-n", SUBSTRATE_NAMESPACE]);
282
+ let tenants = {};
283
+ const raw = existing?.data?.["config.json"];
284
+ if (raw) {
285
+ try {
286
+ tenants = JSON.parse(Buffer.from(raw, "base64").toString("utf8")).tenants ?? {};
287
+ }
288
+ catch {
289
+ tenants = {};
290
+ }
291
+ }
292
+ tenants[substrate.atespace] = {
293
+ decideUrl: `https://${substrate.edge.host}:${input.port}/nylorun/v1/egress/decide`,
294
+ token: input.token,
295
+ edgeCa,
296
+ ...(input.outside ? { outside: input.outside } : {}),
297
+ };
298
+ await k.apply("Deploying the keys provider", keysProviderManifests(input.image, tenants));
299
+ const egress = await k.getJson(["deployment", "atenet-egress", "-n", SUBSTRATE_NAMESPACE]);
300
+ const registered = keysProviderRegistered(egress);
301
+ if (!registered)
302
+ deps.stack.err(`Keys at Substrate's egress are off: Substrate's egress does not ask ${KEYS_PROVIDER_NAME}. Install Substrate with ${KEYS_PROVIDER_FLAG} (ate-setup deploy ate-system and deploy atenet), then run enable again. Until then keyed hosts are passed through with their stand-ins.`);
303
+ return registered;
304
+ }
271
305
  async function enable(deps, args) {
272
306
  const { stack } = deps;
273
307
  const flags = parseStackFlags(args, { booleans: ["--no-pull"], values: ["--context", "--tenant", "--backend", "--host-address", "--bind-address", "--probe-ports"] }, ENABLE_USAGE);
@@ -310,7 +344,9 @@ async function enable(deps, args) {
310
344
  const edgePorts = {
311
345
  harness: await pick(PREFERRED_EDGE_PORTS.harness, persisted?.edgeHarnessPort),
312
346
  gates: await pick(PREFERRED_EDGE_PORTS.gates, persisted?.edgeGatesPort),
347
+ keys: await pick(PREFERRED_EDGE_PORTS.keys, persisted?.edgeKeysPort),
313
348
  };
349
+ const keysToken = persisted?.keysToken ?? randomBytes(32).toString("hex");
314
350
  const images = stackImages(stack.env, { runtime: stack.runtimeVersion, studio: stack.studioVersion });
315
351
  const installed = await installSubstrate(deps, {
316
352
  context,
@@ -322,6 +358,18 @@ async function enable(deps, args) {
322
358
  });
323
359
  await writeClusterFiles(tenant.paths.root, installed.cluster, installed.token);
324
360
  await prepareSubstrate(deps, tenant.paths.root, installed.cluster, images.sandboxes);
361
+ const outsideUrl = stack.env.NYLORUN_CREDENTIAL_PROVIDER_URL?.trim();
362
+ const outsideToken = stack.env.NYLORUN_CREDENTIAL_PROVIDER_TOKEN?.trim();
363
+ const keysOn = await deployKeysProvider(deps, tenant.paths.root, installed.cluster, {
364
+ image: images.sandboxes,
365
+ port: edgePorts.keys,
366
+ token: keysToken,
367
+ ...(outsideUrl && outsideToken ? { outside: { url: outsideUrl, token: outsideToken } } : {}),
368
+ });
369
+ installed.cluster.substrate.edge.keysPort = edgePorts.keys;
370
+ if (keysOn)
371
+ installed.cluster.substrate.keysProvider = KEYS_PROVIDER_NAME;
372
+ await writeClusterFiles(tenant.paths.root, installed.cluster, installed.token);
325
373
  const settings = {
326
374
  token: persisted?.token ?? randomBytes(32).toString("hex"),
327
375
  image: images.sandboxes,
@@ -332,6 +380,8 @@ async function enable(deps, args) {
332
380
  bind: installed.cluster.bindAddress,
333
381
  edgeHarnessPort: edgePorts.harness,
334
382
  edgeGatesPort: edgePorts.gates,
383
+ edgeKeysPort: edgePorts.keys,
384
+ keysToken,
335
385
  };
336
386
  await restartTenant(stack, { name: tenant.name, sandboxes: settings });
337
387
  const ready = await stack.docker.stream(tenant.compose("up", "--detach", "--wait", "--wait-timeout", String(cliTuning().sandbox.composeWaitSeconds), "sandboxes"));
@@ -340,6 +390,9 @@ async function enable(deps, args) {
340
390
  const c = installed.cluster;
341
391
  stack.out(`Sandboxes ${tenant.name} enabled on ${c.context} (backend substrate, atespace ${c.substrate.atespace}; preview)`);
342
392
  stack.out(`Actors reach the edge ${c.substrate.edge.host} (${c.hostAddress}) on ports ${edgePorts.harness} and ${edgePorts.gates}`);
393
+ stack.out(keysOn
394
+ ? `Keys at Substrate's egress: on (${KEYS_PROVIDER_NAME}, edge port ${edgePorts.keys})`
395
+ : `Keys at Substrate's egress: off (see above)`);
343
396
  return 0;
344
397
  }
345
398
  if (probePorts && [probePorts.allowed, probePorts.denied].some((port) => taken.has(port)))
@@ -440,6 +493,7 @@ async function status(deps, args) {
440
493
  if (cluster.backend === "substrate" && cluster.substrate) {
441
494
  stack.out(`Namespace ${cluster.namespace} backend substrate (preview), atespace ${cluster.substrate.atespace}`);
442
495
  stack.out(`Edge ${cluster.substrate.edge.host} ports ${cluster.substrate.edge.harnessPort}, ${cluster.substrate.edge.gatesPort} (bound on ${cluster.bindAddress}, actors reach ${cluster.hostAddress})`);
496
+ stack.out(`Keys ${cluster.substrate.keysProvider ? `on: ${cluster.substrate.keysProvider}, edge port ${cluster.substrate.edge.keysPort}` : "off: Substrate's egress does not ask the keys provider (see enable's output)"}`);
443
497
  }
444
498
  else {
445
499
  stack.out(`Namespace ${cluster.namespace} agent-sandbox ${cluster.controllerVersion}`);
@@ -8,6 +8,7 @@ export declare const SUBSTRATE_AUDIENCE = "api.ate-system.svc";
8
8
  export declare const PREFERRED_EDGE_PORTS: {
9
9
  readonly harness: 8793;
10
10
  readonly gates: 8794;
11
+ readonly keys: 8795;
11
12
  };
12
13
  /** The name actors dial for the edge, unique to the Tenant's namespace. */
13
14
  export declare function edgeHost(namespace: string): string;
@@ -23,3 +24,35 @@ export declare function substrateManifests(namespace: string): object[];
23
24
  * no `.:53` block or already has a `hosts` plugin of its own.
24
25
  */
25
26
  export declare function corefileWithEdge(corefile: string, host: string, address: string): string | undefined;
27
+ /**
28
+ * Keys at Substrate's egress (ADR-142): the keys provider, `sandboxes provider` from the sandboxes
29
+ * image, runs beside Substrate's egress in its namespace. One per cluster: its config Secret holds
30
+ * an entry per Tenant, by atespace. Substrate learns it at its own install
31
+ * (`--credential-provider`); `enable` deploys it and checks that registration, never changing
32
+ * Substrate's install.
33
+ */
34
+ export declare const KEYS_PROVIDER_NAME = "keys.nylorun.dev";
35
+ export declare const KEYS_PROVIDER = "nylorun-keys-provider";
36
+ export declare const KEYS_PROVIDER_PORT = 50051;
37
+ /** The address Substrate's egress dials, as `ate-setup`'s `--credential-provider` names it. */
38
+ export declare const KEYS_PROVIDER_ADDRESS = "nylorun-keys-provider.ate-system.svc:50051";
39
+ /** The flag to install Substrate with, so its egress asks the keys provider. */
40
+ export declare const KEYS_PROVIDER_FLAG = "--credential-provider='[{\"name\":\"keys.nylorun.dev\",\"address\":\"nylorun-keys-provider.ate-system.svc:50051\"}]'";
41
+ /** One Tenant's entry in the keys provider's config. */
42
+ export interface KeysProviderTenant {
43
+ readonly decideUrl: string;
44
+ readonly token: string;
45
+ readonly edgeCa: string;
46
+ readonly outside?: {
47
+ readonly url: string;
48
+ readonly token: string;
49
+ };
50
+ }
51
+ /** Whether Substrate's egress was installed with the keys provider among its providers. */
52
+ export declare function keysProviderRegistered(deployment: unknown): boolean;
53
+ /**
54
+ * The keys provider: its ServiceAccount, Deployment, Service, a NetworkPolicy that admits only
55
+ * Substrate's egress, and its config Secret. Mutual TLS as Substrate's reference provider serves
56
+ * it: its servicedns pod certificate, callers' podidentity certificates.
57
+ */
58
+ export declare function keysProviderManifests(image: string, tenants: Record<string, KeysProviderTenant>): object[];
@@ -12,7 +12,7 @@ export const SUBSTRATE_OWNER_ACCOUNT = "ate-client";
12
12
  /** The audience ate-api-server accepts: its Service's DNS name. */
13
13
  export const SUBSTRATE_AUDIENCE = `${SUBSTRATE_API_SERVICE}.${SUBSTRATE_NAMESPACE}.svc`;
14
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 };
15
+ export const PREFERRED_EDGE_PORTS = { harness: 8793, gates: 8794, keys: 8795 };
16
16
  /** The name actors dial for the edge, unique to the Tenant's namespace. */
17
17
  export function edgeHost(namespace) {
18
18
  return `${namespace}.edge.nylorun.internal`;
@@ -113,3 +113,112 @@ function block(entries) {
113
113
  END,
114
114
  ];
115
115
  }
116
+ /**
117
+ * Keys at Substrate's egress (ADR-142): the keys provider, `sandboxes provider` from the sandboxes
118
+ * image, runs beside Substrate's egress in its namespace. One per cluster: its config Secret holds
119
+ * an entry per Tenant, by atespace. Substrate learns it at its own install
120
+ * (`--credential-provider`); `enable` deploys it and checks that registration, never changing
121
+ * Substrate's install.
122
+ */
123
+ export const KEYS_PROVIDER_NAME = "keys.nylorun.dev";
124
+ export const KEYS_PROVIDER = "nylorun-keys-provider";
125
+ export const KEYS_PROVIDER_PORT = 50051;
126
+ /** The address Substrate's egress dials, as `ate-setup`'s `--credential-provider` names it. */
127
+ export const KEYS_PROVIDER_ADDRESS = `${KEYS_PROVIDER}.${SUBSTRATE_NAMESPACE}.svc:${KEYS_PROVIDER_PORT}`;
128
+ /** The flag to install Substrate with, so its egress asks the keys provider. */
129
+ export const KEYS_PROVIDER_FLAG = `--credential-provider='[{"name":"${KEYS_PROVIDER_NAME}","address":"${KEYS_PROVIDER_ADDRESS}"}]'`;
130
+ /** Whether Substrate's egress was installed with the keys provider among its providers. */
131
+ export function keysProviderRegistered(deployment) {
132
+ const containers = deployment?.spec?.template?.spec
133
+ ?.containers ?? [];
134
+ return containers.some((c) => Array.isArray(c.args) && c.args.some((arg) => arg === `--credential-provider-name=${KEYS_PROVIDER_NAME}`));
135
+ }
136
+ const PODCERT = (name, signer) => ({
137
+ name,
138
+ projected: {
139
+ sources: [
140
+ { podCertificate: { signerName: `${signer}/identity`, keyType: "ECDSAP256", credentialBundlePath: "credential-bundle.pem" } },
141
+ {
142
+ clusterTrustBundle: {
143
+ signerName: `${signer}/identity`,
144
+ labelSelector: { matchLabels: { "podcert.ate.dev/canarying": "live" } },
145
+ path: "trust-bundle.pem",
146
+ },
147
+ },
148
+ ],
149
+ },
150
+ });
151
+ /**
152
+ * The keys provider: its ServiceAccount, Deployment, Service, a NetworkPolicy that admits only
153
+ * Substrate's egress, and its config Secret. Mutual TLS as Substrate's reference provider serves
154
+ * it: its servicedns pod certificate, callers' podidentity certificates.
155
+ */
156
+ export function keysProviderManifests(image, tenants) {
157
+ const labels = { app: KEYS_PROVIDER, ...MANAGED };
158
+ const ns = SUBSTRATE_NAMESPACE;
159
+ return [
160
+ { apiVersion: "v1", kind: "ServiceAccount", metadata: { name: KEYS_PROVIDER, namespace: ns, labels: MANAGED } },
161
+ {
162
+ apiVersion: "v1",
163
+ kind: "Secret",
164
+ metadata: { name: KEYS_PROVIDER, namespace: ns, labels: MANAGED },
165
+ type: "Opaque",
166
+ stringData: { "config.json": JSON.stringify({ tenants }) },
167
+ },
168
+ {
169
+ apiVersion: "apps/v1",
170
+ kind: "Deployment",
171
+ metadata: { name: KEYS_PROVIDER, namespace: ns, labels },
172
+ spec: {
173
+ replicas: 1,
174
+ selector: { matchLabels: { app: KEYS_PROVIDER } },
175
+ template: {
176
+ metadata: { labels },
177
+ spec: {
178
+ serviceAccountName: KEYS_PROVIDER,
179
+ securityContext: { runAsUser: 65532, runAsGroup: 65532, runAsNonRoot: true },
180
+ containers: [
181
+ {
182
+ name: "provider",
183
+ image,
184
+ args: ["provider", "--name", KEYS_PROVIDER_NAME, "--listen", `:${KEYS_PROVIDER_PORT}`, "--config", "/etc/nylorun-keys/config.json"],
185
+ ports: [
186
+ { name: "grpc", containerPort: KEYS_PROVIDER_PORT },
187
+ { name: "health", containerPort: 9091 },
188
+ ],
189
+ readinessProbe: { httpGet: { path: "/readyz", port: "health" }, periodSeconds: 10 },
190
+ securityContext: { allowPrivilegeEscalation: false, readOnlyRootFilesystem: true, capabilities: { drop: ["ALL"] } },
191
+ volumeMounts: [
192
+ { name: "config", mountPath: "/etc/nylorun-keys", readOnly: true },
193
+ { name: "servicedns", mountPath: "/run/servicedns.podcert.ate.dev", readOnly: true },
194
+ { name: "podidentity", mountPath: "/run/podidentity.podcert.ate.dev", readOnly: true },
195
+ ],
196
+ },
197
+ ],
198
+ volumes: [
199
+ { name: "config", secret: { secretName: KEYS_PROVIDER } },
200
+ PODCERT("servicedns", "servicedns.podcert.ate.dev"),
201
+ PODCERT("podidentity", "podidentity.podcert.ate.dev"),
202
+ ],
203
+ },
204
+ },
205
+ },
206
+ },
207
+ {
208
+ apiVersion: "v1",
209
+ kind: "Service",
210
+ metadata: { name: KEYS_PROVIDER, namespace: ns, labels: MANAGED },
211
+ spec: { type: "ClusterIP", selector: { app: KEYS_PROVIDER }, ports: [{ name: "grpc", port: KEYS_PROVIDER_PORT, targetPort: "grpc", protocol: "TCP" }] },
212
+ },
213
+ {
214
+ apiVersion: "networking.k8s.io/v1",
215
+ kind: "NetworkPolicy",
216
+ metadata: { name: KEYS_PROVIDER, namespace: ns, labels: MANAGED },
217
+ spec: {
218
+ podSelector: { matchLabels: { app: KEYS_PROVIDER } },
219
+ policyTypes: ["Ingress"],
220
+ ingress: [{ from: [{ podSelector: { matchLabels: { app: "atenet-egress" } } }], ports: [{ protocol: "TCP", port: KEYS_PROVIDER_PORT }] }],
221
+ },
222
+ },
223
+ ];
224
+ }
@@ -76,6 +76,8 @@ export declare function renderComposeFile(project: string, name: string, options
76
76
  sandboxes?: true;
77
77
  /** The sandboxes container serves the edge (backend substrate, ADR-138). */
78
78
  sandboxEdge?: true;
79
+ /** The edge also serves the keys provider's port (ADR-142). */
80
+ sandboxKeys?: true;
79
81
  harness?: "remote" | "in-process";
80
82
  restateUi?: true;
81
83
  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, options.sandboxEdge === true) : ""}
330
+ ${remote ? harnessService(project) : ""}${sandboxes ? sandboxesService(project, options.sandboxEdge === true, options.sandboxKeys === true) : ""}
331
331
  networks:
332
332
  default:
333
333
  name: ${project}
@@ -421,10 +421,19 @@ const SANDBOXES_EDGE_PORTS = `
421
421
  ports:
422
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
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`;
424
+ /**
425
+ * The edge's keys port (ADR-142): Substrate's keys provider reaches egress-gate's decide route on
426
+ * gateway:4200 through it. No actor's egress policy allows it.
427
+ */
428
+ const SANDBOXES_EDGE_KEYS_PORT = `
429
+ - "\${NYLORUN_SANDBOX_BIND:?run nylorun local sandbox enable}:\${NYLORUN_SANDBOX_EDGE_KEYS_PORT:?run nylorun local sandbox enable}:8445" # the edge: egress-gate's decide route, for the keys provider`;
424
430
  /** egress-gate (F7.2): pods' CONNECT proxy in the gateway. */
425
431
  const SANDBOXES_GATEWAY_ENV = `
426
432
  # egress-gate: the only way out of a sandbox pod, to the hosts its spec allows.
427
- NYLORUN_EGRESS_LISTEN_PORT: "4200"`;
433
+ NYLORUN_EGRESS_LISTEN_PORT: "4200"
434
+ # On Agent Substrate, the keys provider's token for egress-gate's decide route (ADR-142).
435
+ # Empty: the route is off.
436
+ NYLORUN_EGRESS_KEYS_TOKEN: \${NYLORUN_EGRESS_KEYS_TOKEN:-}`;
428
437
  /** Empty and read-only over the cluster credentials: only the sandboxes service reads them. */
429
438
  const SANDBOXES_RUNTIME_MOUNT = `
430
439
  - type: tmpfs
@@ -436,7 +445,7 @@ const SANDBOXES_RUNTIME_MOUNT = `
436
445
  * (NYLORUN_SANDBOX_*_PORT in .env), the Harness API's, the gates' and egress-gate's, are
437
446
  * published by the runtime and the gateway (above).
438
447
  */
439
- function sandboxesService(project, edge) {
448
+ function sandboxesService(project, edge, keys) {
440
449
  return `
441
450
  sandboxes: # the sandboxes driver; the only holder of the cluster credentials; not published${edge ? ", but for its edge" : ""}
442
451
  image: \${NYLORUN_SANDBOXES_IMAGE:?run nylorun local sandbox enable}
@@ -452,7 +461,7 @@ function sandboxesService(project, edge) {
452
461
  volumes:
453
462
  # cluster.json (API server, CA, namespace) and the ServiceAccount token, read-only.
454
463
  - \${NYLORUN_HOST_ROOT:?run nylorun start}/sandboxes:/run/nylorun/sandboxes:ro
455
- networks: [default] # the cluster's API server, through host.docker.internal${edge ? SANDBOXES_EDGE_PORTS : ""}
464
+ networks: [default] # the cluster's API server, through host.docker.internal${edge ? SANDBOXES_EDGE_PORTS : ""}${edge && keys ? SANDBOXES_EDGE_KEYS_PORT : ""}
456
465
  healthcheck: # /ready: informers synced and the API server answers
457
466
  test: ["CMD", "/sandboxes", "healthcheck"]
458
467
  ${healthcheck("sandboxes")} restart: unless-stopped
@@ -80,6 +80,13 @@ export interface SandboxStackEnv {
80
80
  */
81
81
  edgeHarnessPort?: number;
82
82
  edgeGatesPort?: number;
83
+ /**
84
+ * With the Substrate backend: the edge's keys port, where the keys provider reaches
85
+ * egress-gate's decide route, and the token it presents there (ADR-142).
86
+ */
87
+ edgeKeysPort?: number;
88
+ /** NYLORUN_EGRESS_KEYS_TOKEN, 32 bytes as hex. */
89
+ keysToken?: string;
83
90
  }
84
91
  export type HarnessMode = "remote" | "in-process";
85
92
  export declare function renderEnvFile(env: StackEnv): string;
@@ -15,6 +15,8 @@ const SANDBOX_KEYS = {
15
15
  bind: "NYLORUN_SANDBOX_BIND",
16
16
  edgeHarnessPort: "NYLORUN_SANDBOX_EDGE_HARNESS_PORT",
17
17
  edgeGatesPort: "NYLORUN_SANDBOX_EDGE_GATES_PORT",
18
+ edgeKeysPort: "NYLORUN_SANDBOX_EDGE_KEYS_PORT",
19
+ keysToken: "NYLORUN_EGRESS_KEYS_TOKEN",
18
20
  };
19
21
  const KEYS = {
20
22
  runtimePort: "NYLORUN_PORT",
@@ -129,6 +131,14 @@ export function renderEnvFile(env) {
129
131
  sandboxLine("edgeGatesPort"),
130
132
  ]
131
133
  : []),
134
+ ...(sandboxes.edgeKeysPort && sandboxes.keysToken
135
+ ? [
136
+ "# Keys at Substrate's egress (ADR-142): the edge's port for the keys provider,",
137
+ "# and the token it presents to egress-gate's decide route.",
138
+ sandboxLine("edgeKeysPort"),
139
+ sandboxLine("keysToken"),
140
+ ]
141
+ : []),
132
142
  "",
133
143
  ]
134
144
  : []),
@@ -172,9 +182,12 @@ function parseSandboxes(values) {
172
182
  return undefined;
173
183
  const edgeHarnessPort = port(values.get(SANDBOX_KEYS.edgeHarnessPort));
174
184
  const edgeGatesPort = port(values.get(SANDBOX_KEYS.edgeGatesPort));
185
+ const edgeKeysPort = port(values.get(SANDBOX_KEYS.edgeKeysPort));
186
+ const keysToken = values.get(SANDBOX_KEYS.keysToken);
175
187
  return {
176
188
  token, image, harnessPort, gatesPort, egressPort, hostAddress, bind,
177
189
  ...(edgeHarnessPort && edgeGatesPort ? { edgeHarnessPort, edgeGatesPort } : {}),
190
+ ...(edgeKeysPort && keysToken && /^[0-9a-f]{64,}$/i.test(keysToken) ? { edgeKeysPort, keysToken } : {}),
178
191
  };
179
192
  }
180
193
  export function parsePersisted(text) {
@@ -103,6 +103,7 @@ export async function prepareStack(input) {
103
103
  harness: env.harness,
104
104
  ...(env.sandboxes ? { sandboxes: true } : {}),
105
105
  ...(env.sandboxes?.edgeHarnessPort ? { sandboxEdge: true } : {}),
106
+ ...(env.sandboxes?.edgeHarnessPort && env.sandboxes.edgeKeysPort ? { sandboxKeys: true } : {}),
106
107
  ...(env.restateUi ? { restateUi: true } : {}),
107
108
  // The trusted issuers, when the operator wrote an identity file; a change takes a restart.
108
109
  ...(existsSync(paths.identity) ? { identity: true } : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nylorun",
3
- "version": "0.29.8-beta",
3
+ "version": "0.30.0-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",
@@ -46,7 +46,7 @@
46
46
  "nylo": "dist/nylo.js"
47
47
  },
48
48
  "nylorun": {
49
- "runtime": "0.48.1-beta",
49
+ "runtime": "0.49.0-beta",
50
50
  "studio": "0.35.11-beta"
51
51
  }
52
52
  }