@nylorun/runtime 0.14.0-beta → 0.15.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 (198) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/README.md +75 -33
  3. package/dist/adapters/execution/restate.js +2 -2
  4. package/dist/adapters/replication/pgoutput.d.ts +5 -0
  5. package/dist/adapters/replication/pgoutput.js +4 -3
  6. package/dist/admin-openapi.json +184 -685
  7. package/dist/api/components.d.ts +308 -68
  8. package/dist/api/components.js +4 -5
  9. package/dist/api/gate/keys.d.ts +14 -0
  10. package/dist/api/gate/keys.js +59 -0
  11. package/dist/api/gate/routes.d.ts +34 -0
  12. package/dist/api/gate/routes.js +235 -0
  13. package/dist/api/http/define.js +0 -8
  14. package/dist/api/http/routes/access.js +25 -18
  15. package/dist/api/http/routes/tenant.d.ts +2 -1
  16. package/dist/api/http/routes/tenant.js +77 -5
  17. package/dist/api/http/routes/vaults.js +2 -2
  18. package/dist/api/openapi.js +4 -5
  19. package/dist/gates/contract.d.ts +51 -0
  20. package/dist/gates/contract.js +37 -0
  21. package/dist/gates/handler.d.ts +21 -0
  22. package/dist/gates/handler.js +55 -0
  23. package/dist/gates/http-client.d.ts +12 -0
  24. package/dist/gates/http-client.js +160 -0
  25. package/dist/gates/in-process.d.ts +32 -0
  26. package/dist/gates/in-process.js +42 -0
  27. package/dist/gates/inflight.d.ts +39 -0
  28. package/dist/gates/inflight.js +70 -0
  29. package/dist/gates/mcp-handler.d.ts +35 -0
  30. package/dist/gates/mcp-handler.js +142 -0
  31. package/dist/gates/meter.d.ts +24 -0
  32. package/dist/gates/meter.js +165 -0
  33. package/dist/gates/model-gate.d.ts +45 -0
  34. package/dist/gates/model-gate.js +1 -0
  35. package/dist/gates/tenant-vaults.d.ts +64 -0
  36. package/dist/gates/tenant-vaults.js +94 -0
  37. package/dist/gates/tool-calls.d.ts +51 -0
  38. package/dist/gates/tool-calls.js +64 -0
  39. package/dist/gates/tool-client.d.ts +12 -0
  40. package/dist/gates/tool-client.js +201 -0
  41. package/dist/gates/tool-contract.d.ts +116 -0
  42. package/dist/gates/tool-contract.js +64 -0
  43. package/dist/gates/tool-gate.d.ts +45 -0
  44. package/dist/gates/tool-gate.js +10 -0
  45. package/dist/host/admin-api.d.ts +7 -6
  46. package/dist/host/admin-api.js +9 -113
  47. package/dist/host/app.d.ts +1 -1
  48. package/dist/host/app.js +20 -24
  49. package/dist/host/config-for.d.ts +1 -1
  50. package/dist/host/config-for.js +2 -2
  51. package/dist/host/create-host.d.ts +5 -5
  52. package/dist/host/create-host.js +5 -15
  53. package/dist/host/execution.d.ts +20 -18
  54. package/dist/host/execution.js +2 -2
  55. package/dist/host/gates.d.ts +58 -0
  56. package/dist/host/gates.js +127 -0
  57. package/dist/host/http.d.ts +6 -1
  58. package/dist/host/http.js +17 -0
  59. package/dist/host/index.d.ts +1 -1
  60. package/dist/host/main.js +151 -71
  61. package/dist/host/stack-config.d.ts +71 -19
  62. package/dist/host/stack-config.js +192 -18
  63. package/dist/infra/database.d.ts +2 -2
  64. package/dist/infra/execution.d.ts +5 -5
  65. package/dist/infra/execution.js +8 -6
  66. package/dist/infra/index.d.ts +1 -1
  67. package/dist/infra/workers.d.ts +17 -17
  68. package/dist/infra/workers.js +9 -9
  69. package/dist/keys/client.d.ts +9 -0
  70. package/dist/keys/client.js +70 -0
  71. package/dist/keys/contract.d.ts +24 -0
  72. package/dist/keys/contract.js +12 -0
  73. package/dist/keys/keys.d.ts +41 -0
  74. package/dist/keys/keys.js +50 -0
  75. package/dist/mcp/connect.d.ts +43 -3
  76. package/dist/mcp/connect.js +12 -5
  77. package/dist/mcp/pool.d.ts +21 -3
  78. package/dist/mcp/pool.js +15 -2
  79. package/dist/openapi.json +887 -694
  80. package/dist/record/append.d.ts +51 -0
  81. package/dist/record/append.js +20 -0
  82. package/dist/{store/event.js → record/envelope.js} +1 -1
  83. package/dist/record/index.d.ts +11 -0
  84. package/dist/record/index.js +11 -0
  85. package/dist/store/postgres/connect.d.ts +15 -12
  86. package/dist/store/postgres/connect.js +24 -12
  87. package/dist/store/postgres/db.d.ts +16 -0
  88. package/dist/store/postgres/db.js +28 -0
  89. package/dist/store/postgres/drizzle/0000_foundation.sql +22 -0
  90. package/dist/store/postgres/drizzle/0001_baseline.sql +255 -0
  91. package/dist/store/postgres/drizzle/0002_stream_relay.sql +4 -0
  92. package/dist/store/postgres/drizzle/0003_tool_crossings.sql +9 -0
  93. package/dist/store/postgres/drizzle/meta/_journal.json +34 -0
  94. package/dist/store/postgres/migrate.d.ts +68 -0
  95. package/dist/store/postgres/migrate.js +183 -0
  96. package/dist/store/postgres/record-writer.d.ts +4 -0
  97. package/dist/store/postgres/record-writer.js +34 -0
  98. package/dist/store/postgres/record.d.ts +2 -8
  99. package/dist/store/postgres/record.js +50 -54
  100. package/dist/store/postgres/schema.d.ts +3096 -0
  101. package/dist/store/postgres/schema.js +363 -0
  102. package/dist/store/postgres/store.d.ts +2 -4
  103. package/dist/store/postgres/store.js +481 -567
  104. package/dist/store/postgres/tenant.d.ts +56 -0
  105. package/dist/store/postgres/tenant.js +154 -0
  106. package/dist/store/types.d.ts +43 -87
  107. package/dist/store/types.js +0 -1
  108. package/dist/streams/control.d.ts +1 -1
  109. package/dist/streams/control.js +6 -2
  110. package/dist/streams/relay/types.d.ts +2 -1
  111. package/dist/streams/relay/types.js +2 -1
  112. package/dist/streams/types.d.ts +5 -0
  113. package/dist/tenant/advance.d.ts +7 -2
  114. package/dist/tenant/advance.js +95 -16
  115. package/dist/tenant/cause.d.ts +25 -0
  116. package/dist/tenant/cause.js +45 -0
  117. package/dist/tenant/commands.js +4 -2
  118. package/dist/tenant/context.d.ts +26 -3
  119. package/dist/tenant/delivery-token.js +17 -18
  120. package/dist/tenant/delivery.d.ts +1 -1
  121. package/dist/tenant/delivery.js +18 -13
  122. package/dist/tenant/effects.d.ts +15 -1
  123. package/dist/tenant/effects.js +93 -17
  124. package/dist/tenant/ephemeral.d.ts +16 -7
  125. package/dist/tenant/ephemeral.js +45 -111
  126. package/dist/tenant/history.d.ts +64 -0
  127. package/dist/tenant/history.js +120 -0
  128. package/dist/tenant/model-setting.d.ts +2 -2
  129. package/dist/tenant/module.d.ts +14 -28
  130. package/dist/tenant/module.js +131 -271
  131. package/dist/tenant/paths.d.ts +11 -16
  132. package/dist/tenant/paths.js +24 -68
  133. package/dist/tenant/pool.d.ts +1 -1
  134. package/dist/tenant/pool.js +1 -1
  135. package/dist/tenant/principals.d.ts +23 -11
  136. package/dist/tenant/principals.js +61 -49
  137. package/dist/tenant/runtime.d.ts +19 -1
  138. package/dist/tenant/runtime.js +40 -26
  139. package/dist/tenant/scheduler.d.ts +4 -2
  140. package/dist/tenant/scheduler.js +4 -4
  141. package/dist/tenant/session-streams.d.ts +2 -0
  142. package/dist/tenant/session-streams.js +8 -3
  143. package/dist/tenant/status.d.ts +3 -0
  144. package/dist/tenant/status.js +1 -1
  145. package/dist/tenant/store-pg.d.ts +9 -6
  146. package/dist/tenant/store-pg.js +60 -125
  147. package/dist/tenant/streams.d.ts +9 -25
  148. package/dist/tenant/streams.js +7 -22
  149. package/dist/tenant/tokens.js +16 -14
  150. package/dist/tenant/types.d.ts +41 -92
  151. package/dist/tenant/types.js +4 -15
  152. package/dist/tenant/worker.js +4 -4
  153. package/dist/vault/host-model.d.ts +31 -0
  154. package/dist/vault/host-model.js +57 -0
  155. package/dist/vault/service.d.ts +26 -15
  156. package/dist/vault/service.js +70 -109
  157. package/dist/version.d.ts +1 -1
  158. package/dist/version.js +1 -1
  159. package/package.json +9 -5
  160. package/dist/store/memory.d.ts +0 -69
  161. package/dist/store/memory.js +0 -810
  162. package/dist/store/postgres/migrations/001_initial.d.ts +0 -27
  163. package/dist/store/postgres/migrations/001_initial.js +0 -196
  164. package/dist/store/postgres/migrations/002_session_owner.d.ts +0 -6
  165. package/dist/store/postgres/migrations/002_session_owner.js +0 -13
  166. package/dist/store/postgres/migrations/003_subject_tokens.d.ts +0 -8
  167. package/dist/store/postgres/migrations/003_subject_tokens.js +0 -42
  168. package/dist/store/postgres/migrations/004_publishable_keys.d.ts +0 -6
  169. package/dist/store/postgres/migrations/004_publishable_keys.js +0 -18
  170. package/dist/store/postgres/migrations/005_action_endpoints.d.ts +0 -6
  171. package/dist/store/postgres/migrations/005_action_endpoints.js +0 -32
  172. package/dist/store/postgres/migrations/006_drop_executors.d.ts +0 -11
  173. package/dist/store/postgres/migrations/006_drop_executors.js +0 -32
  174. package/dist/store/postgres/migrations/007_event_time.d.ts +0 -7
  175. package/dist/store/postgres/migrations/007_event_time.js +0 -14
  176. package/dist/store/postgres/migrations/008_durable_streams.d.ts +0 -13
  177. package/dist/store/postgres/migrations/008_durable_streams.js +0 -35
  178. package/dist/store/postgres/migrations/index.d.ts +0 -53
  179. package/dist/store/postgres/migrations/index.js +0 -96
  180. package/dist/store/postgres/migrations/shared/index.d.ts +0 -31
  181. package/dist/store/postgres/migrations/shared/index.js +0 -92
  182. package/dist/store/postgres/names.d.ts +0 -11
  183. package/dist/store/postgres/names.js +0 -42
  184. package/dist/store/postgres/tenants.d.ts +0 -90
  185. package/dist/store/postgres/tenants.js +0 -228
  186. package/dist/streams/relay/memory.d.ts +0 -69
  187. package/dist/streams/relay/memory.js +0 -176
  188. package/dist/tenant/envelope.d.ts +0 -7
  189. package/dist/tenant/envelope.js +0 -22
  190. package/dist/tenant/quarantine-error.d.ts +0 -6
  191. package/dist/tenant/quarantine-error.js +0 -9
  192. package/dist/tenant/quarantine.d.ts +0 -27
  193. package/dist/tenant/quarantine.js +0 -72
  194. package/dist/tenant/store-memory.d.ts +0 -16
  195. package/dist/tenant/store-memory.js +0 -111
  196. /package/dist/{store → record}/cursor.d.ts +0 -0
  197. /package/dist/{store → record}/cursor.js +0 -0
  198. /package/dist/{store/event.d.ts → record/envelope.d.ts} +0 -0
@@ -3,6 +3,18 @@
3
3
  * snapshot and argv. Pure: `host/main.ts` passes the process environment and
4
4
  * arguments; nothing here reads ambient state.
5
5
  *
6
+ * `--service core,loop` names the Runtime services the process runs (blueprint
7
+ * §19, D12): one image runs every service, and a container may pack several.
8
+ * Without the flag a process runs core and loop. `--role api|worker|all` is
9
+ * the deprecated name of the same choice (core, loop, or both). `gates` (the
10
+ * Model Gate) never shares a process with core or loop: it holds the
11
+ * credentials they must not. Only a process that runs core or loop parses the
12
+ * API listener (`NYLORUN_LISTEN_*`); only one that runs gates parses its
13
+ * listener (`NYLORUN_GATES_LISTEN_*`), and only one that runs loop parses
14
+ * where to reach the gate (`NYLORUN_GATES_URL`). Both read
15
+ * `NYLORUN_GATES_TOKEN`. In a container, loop requires the gate: the loop
16
+ * process must never hold a model credential.
17
+ *
6
18
  * Two listen modes:
7
19
  * - **local** (no `NYLORUN_LISTEN_*` / `NYLORUN_ALLOWED_HOSTS`): the Host binds
8
20
  * what `host.json` names, loopback only unless `allowNonLoopback`.
@@ -12,9 +24,41 @@
12
24
  * probes from inside the container.
13
25
  *
14
26
  * The Postgres, Restate and S2 endpoints are parsed and validated here;
15
- * `infra/*` builds the clients from them.
27
+ * `infra/*` builds the clients from them. So is who the Host's Tenant is when its database
28
+ * holds none yet (`NYLORUN_TENANT_ID`, `NYLORUN_TENANT_NAME`, `NYLORUN_DERIVED_PRINCIPALS`).
29
+ */
30
+ import { DERIVED_PRINCIPAL_ID_PATTERN, isTenantId } from "@nylorun/core/compatibility";
31
+ export const RUNTIME_SERVICES = ["core", "loop", "gates", "keys"];
32
+ /**
33
+ * Services that may share a process (D12): they hold the same secrets and parse the same
34
+ * trust class of input. keys joins gates (F4.2); egress joins them in a later release.
16
35
  */
17
- export const RUNTIME_ROLES = ["api", "worker", "all"];
36
+ const SERVICE_GROUPS = [
37
+ ["core", "loop"],
38
+ ["gates", "keys"],
39
+ ];
40
+ /** Where the gates service listens by default. */
41
+ export const DEFAULT_GATES_LISTEN_PORT = 4100;
42
+ /** What a process runs without `--service`: core and loop, as `--role all` did. */
43
+ export const DEFAULT_SERVICES = new Set([
44
+ "core",
45
+ "loop",
46
+ ]);
47
+ /** Services of the blueprint this release doesn't have yet. */
48
+ const LATER_SERVICES = [
49
+ "egress",
50
+ "harness",
51
+ "sandboxd",
52
+ ];
53
+ const ROLE_SERVICES = {
54
+ api: ["core"],
55
+ worker: ["loop"],
56
+ all: ["core", "loop"],
57
+ };
58
+ /** `services` as the `--service` value that selects them, e.g. `core,loop`. */
59
+ export function describeServices(services) {
60
+ return RUNTIME_SERVICES.filter((service) => services.has(service)).join(",");
61
+ }
18
62
  export const DEFAULT_CONTAINER_LISTEN_HOST = "0.0.0.0";
19
63
  export const DEFAULT_CONTAINER_LISTEN_PORT = 4000;
20
64
  export class StackConfigError extends Error {
@@ -30,30 +74,69 @@ function read(env, name) {
30
74
  const trimmed = value.trim();
31
75
  return trimmed === "" ? undefined : trimmed;
32
76
  }
33
- export function parseRole(argv) {
77
+ const USAGE = "Usage: main.js [--service core,loop|gates]";
78
+ /** Parses `--service a,b` (or the deprecated `--role`); throws `StackConfigError`. */
79
+ export function parseServices(argv) {
80
+ let service;
34
81
  let role;
35
82
  for (let index = 0; index < argv.length; index += 1) {
36
83
  const arg = argv[index];
84
+ const flag = arg.startsWith("--service")
85
+ ? "--service"
86
+ : arg.startsWith("--role")
87
+ ? "--role"
88
+ : undefined;
37
89
  let value;
38
- if (arg === "--role") {
90
+ if (flag && arg === flag) {
39
91
  value = argv[index + 1];
40
92
  index += 1;
41
93
  if (value === undefined || value.startsWith("-"))
42
- throw new StackConfigError("--role requires a value: api, worker or all");
94
+ throw new StackConfigError(flag === "--service"
95
+ ? "--service requires a value, e.g. core,loop"
96
+ : "--role requires a value: api, worker or all");
43
97
  }
44
- else if (arg.startsWith("--role=")) {
45
- value = arg.slice("--role=".length);
98
+ else if (flag && arg.startsWith(`${flag}=`)) {
99
+ value = arg.slice(flag.length + 1);
46
100
  }
47
101
  else {
48
- throw new StackConfigError(`Unknown argument ${arg}. Usage: main.js [--role api|worker|all]`);
102
+ throw new StackConfigError(`Unknown argument ${arg}. ${USAGE}`);
49
103
  }
50
- if (role !== undefined)
51
- throw new StackConfigError("--role may only be supplied once");
52
- if (!RUNTIME_ROLES.includes(value))
53
- throw new StackConfigError(`Invalid --role ${value}; expected api, worker or all`);
54
- role = value;
104
+ if ((flag === "--service" ? service : role) !== undefined)
105
+ throw new StackConfigError(`${flag} may only be supplied once`);
106
+ if (flag === "--service")
107
+ service = value;
108
+ else
109
+ role = value;
55
110
  }
56
- return role ?? "all";
111
+ if (service !== undefined && role !== undefined)
112
+ throw new StackConfigError("--role is the deprecated name of --service; supply only --service");
113
+ if (role !== undefined) {
114
+ if (!Object.hasOwn(ROLE_SERVICES, role))
115
+ throw new StackConfigError(`Invalid --role ${role}; expected api, worker or all (deprecated: use --service core, loop or core,loop)`);
116
+ const deprecatedRole = role;
117
+ return { services: new Set(ROLE_SERVICES[deprecatedRole]), deprecatedRole };
118
+ }
119
+ if (service === undefined)
120
+ return { services: DEFAULT_SERVICES };
121
+ const names = service.split(",").map((name) => name.trim());
122
+ const services = new Set();
123
+ for (const name of names) {
124
+ if (name === "")
125
+ throw new StackConfigError(`--service ${service} has an empty entry. ${USAGE}`);
126
+ if (name === "all")
127
+ throw new StackConfigError("--service all is not a service; use --service core,loop");
128
+ if (LATER_SERVICES.includes(name))
129
+ throw new StackConfigError(`The ${name} service is not in this release of the Runtime`);
130
+ if (!RUNTIME_SERVICES.includes(name))
131
+ throw new StackConfigError(`Unknown service ${name}; expected ${RUNTIME_SERVICES.join(" or ")}`);
132
+ if (services.has(name))
133
+ throw new StackConfigError(`--service names ${name} twice`);
134
+ services.add(name);
135
+ }
136
+ const groups = SERVICE_GROUPS.filter((group) => group.some((name) => services.has(name)));
137
+ if (groups.length > 1)
138
+ throw new StackConfigError(`--service ${service}: ${groups.map((group) => group.join(" and ")).join(" may not share a process with ")}; run them as separate processes`);
139
+ return { services };
57
140
  }
58
141
  function parsePort(name, raw) {
59
142
  if (!/^\d+$/.test(raw))
@@ -164,11 +247,23 @@ function parseIdentityKeys(env) {
164
247
  }
165
248
  /** Parse the stack configuration; throws `StackConfigError` naming the variable. */
166
249
  export function parseStackConfig(env, argv) {
167
- const role = parseRole(argv);
168
- const listen = parseListen(env);
169
- const operator = parseAdminListen(env);
250
+ const { services, deprecatedRole } = parseServices(argv);
251
+ // The image sets NYLORUN_LISTEN_*: only the API's processes read them.
252
+ const servesApi = services.has("core") || services.has("loop");
253
+ const listen = servesApi ? parseListen(env) : undefined;
254
+ const operator = servesApi ? parseAdminListen(env) : undefined;
255
+ const gates = services.has("gates") || services.has("keys") ? parseGates(env) : undefined;
256
+ // The gates service's clients: the loop's model and tool calls, and core's endpoint pings.
257
+ const modelGate = servesApi ? parseModelGate(env) : undefined;
258
+ // The keys service is the gateway's unless NYLORUN_KEYS_URL names another listener.
259
+ const keys = servesApi ? (parseKeysEndpoint(env) ?? modelGate) : undefined;
170
260
  if (operator && listen && operator.port === listen.port)
171
261
  throw new StackConfigError("NYLORUN_ADMIN_LISTEN_PORT must differ from NYLORUN_LISTEN_PORT");
262
+ if (services.has("loop") && listen && !modelGate)
263
+ throw new StackConfigError("NYLORUN_GATES_URL is required for the loop service in a container: model calls go through the gates service (the gateway container), so the loop never holds a model credential. Set NYLORUN_GATES_URL and NYLORUN_GATES_TOKEN (`nylorun start` sets both)");
264
+ const rawPacking = read(env, "NYLORUN_PACKING");
265
+ if (rawPacking !== undefined && rawPacking !== "combined" && rawPacking !== "split")
266
+ throw new StackConfigError(`NYLORUN_PACKING must be combined or split, not ${rawPacking}`);
172
267
  const http = ["http:", "https:"];
173
268
  const endpoints = {};
174
269
  const databaseUrl = parseUrl(env, "NYLORUN_DATABASE_URL", [
@@ -208,16 +303,95 @@ export function parseStackConfig(env, argv) {
208
303
  if (rawBrowser !== undefined && rawBrowser !== "on" && rawBrowser !== "off")
209
304
  throw new StackConfigError(`NYLORUN_BROWSER_ACCESS must be on or off, not ${rawBrowser}`);
210
305
  const delivery = parseDelivery(env);
306
+ const tenant = servesApi ? parseTenant(env) : undefined;
211
307
  return {
212
- role,
308
+ services,
309
+ ...(deprecatedRole ? { deprecatedRole } : {}),
310
+ ...(gates ? { gates } : {}),
311
+ ...(modelGate ? { modelGate } : {}),
312
+ ...(keys ? { keys } : {}),
313
+ ...(rawPacking ? { packing: rawPacking } : {}),
213
314
  ...(listen ? { listen } : {}),
214
315
  endpoints,
215
316
  ...(publicUrl ? { publicUrl } : {}),
216
317
  ...(rawBrowser === undefined ? {} : { browserAccess: rawBrowser === "on" }),
217
318
  ...(operator ? { operator } : {}),
218
319
  ...(delivery ? { delivery } : {}),
320
+ ...(tenant ? { tenant } : {}),
321
+ };
322
+ }
323
+ function parseTenant(env) {
324
+ const id = read(env, "NYLORUN_TENANT_ID");
325
+ if (id !== undefined && !isTenantId(id))
326
+ throw new StackConfigError(`NYLORUN_TENANT_ID must be a Tenant id (tn_ and 26 Crockford characters), not ${id}`);
327
+ const raw = read(env, "NYLORUN_DERIVED_PRINCIPALS");
328
+ const derivedPrincipals = raw === undefined ? ["project"] : raw.split(",").map((entry) => entry.trim());
329
+ for (const principal of derivedPrincipals)
330
+ if (!DERIVED_PRINCIPAL_ID_PATTERN.test(principal) || principal === "studio")
331
+ throw new StackConfigError(`NYLORUN_DERIVED_PRINCIPALS has '${principal}': each entry must match ${DERIVED_PRINCIPAL_ID_PATTERN} and not be studio`);
332
+ return {
333
+ ...(id ? { id } : {}),
334
+ name: read(env, "NYLORUN_TENANT_NAME") ?? "default",
335
+ derivedPrincipals: [...new Set(derivedPrincipals)],
336
+ };
337
+ }
338
+ const GATES_TOKEN = /^[0-9a-f]{64,}$/i;
339
+ /** `StackConfig.gates` from `NYLORUN_GATES_*`. */
340
+ function parseGates(env) {
341
+ const host = read(env, "NYLORUN_GATES_LISTEN_HOST") ?? DEFAULT_CONTAINER_LISTEN_HOST;
342
+ if (/\s|\//.test(host))
343
+ throw new StackConfigError(`NYLORUN_GATES_LISTEN_HOST is not an address: ${host}`);
344
+ const rawPort = read(env, "NYLORUN_GATES_LISTEN_PORT");
345
+ const port = rawPort === undefined
346
+ ? DEFAULT_GATES_LISTEN_PORT
347
+ : parsePort("NYLORUN_GATES_LISTEN_PORT", rawPort);
348
+ const rawAllowed = read(env, "NYLORUN_GATES_ALLOWED_HOSTS");
349
+ const explicit = rawAllowed === undefined
350
+ ? []
351
+ : rawAllowed
352
+ .split(",")
353
+ .map((entry) => entry.trim())
354
+ .filter((entry) => entry !== "")
355
+ .map((entry) => normalizeAllowedHost("NYLORUN_GATES_ALLOWED_HOSTS", entry));
356
+ if (explicit.length === 0 && !isLoopbackAddress(host))
357
+ throw new StackConfigError(`NYLORUN_GATES_ALLOWED_HOSTS is required when NYLORUN_GATES_LISTEN_HOST is ${host}: list the Host headers the loop sends, e.g. gateway:${port}`);
358
+ const token = read(env, "NYLORUN_GATES_TOKEN");
359
+ if (token === undefined)
360
+ throw new StackConfigError("NYLORUN_GATES_TOKEN is required for --service gates: the token the loop presents (`nylorun start` sets it)");
361
+ if (!GATES_TOKEN.test(token))
362
+ throw new StackConfigError("NYLORUN_GATES_TOKEN must be at least 32 bytes as hex");
363
+ return {
364
+ listen: { host, port, allowedHosts: [...new Set([...explicit, ...loopbackForms(port)])] },
365
+ token,
219
366
  };
220
367
  }
368
+ /** `StackConfig.modelGate` from `NYLORUN_GATES_URL` and `NYLORUN_GATES_TOKEN`. */
369
+ function parseModelGate(env) {
370
+ const url = parseUrl(env, "NYLORUN_GATES_URL", ["http:", "https:"]);
371
+ const token = read(env, "NYLORUN_GATES_TOKEN");
372
+ if (url === undefined) {
373
+ if (token !== undefined)
374
+ throw new StackConfigError("NYLORUN_GATES_TOKEN is set without NYLORUN_GATES_URL: set the URL of the gates service, e.g. http://gateway:4100");
375
+ return undefined;
376
+ }
377
+ if (token === undefined)
378
+ throw new StackConfigError("NYLORUN_GATES_TOKEN is required with NYLORUN_GATES_URL: the gates service refuses calls without it");
379
+ if (!GATES_TOKEN.test(token))
380
+ throw new StackConfigError("NYLORUN_GATES_TOKEN must be at least 32 bytes as hex");
381
+ return { url: url.replace(/\/+$/, ""), token };
382
+ }
383
+ /** `StackConfig.keys` from `NYLORUN_KEYS_URL` and `NYLORUN_GATES_TOKEN`. */
384
+ function parseKeysEndpoint(env) {
385
+ const url = parseUrl(env, "NYLORUN_KEYS_URL", ["http:", "https:"]);
386
+ if (url === undefined)
387
+ return undefined;
388
+ const token = read(env, "NYLORUN_GATES_TOKEN");
389
+ if (token === undefined)
390
+ throw new StackConfigError("NYLORUN_GATES_TOKEN is required with NYLORUN_KEYS_URL: the keys service refuses calls without it");
391
+ if (!GATES_TOKEN.test(token))
392
+ throw new StackConfigError("NYLORUN_GATES_TOKEN must be at least 32 bytes as hex");
393
+ return { url: url.replace(/\/+$/, ""), token };
394
+ }
221
395
  /** `StackConfig.delivery` from `NYLORUN_ENDPOINT_*`, or `undefined` when none is set. */
222
396
  function parseDelivery(env) {
223
397
  const loopback = read(env, "NYLORUN_ENDPOINT_LOOPBACK");
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Builds the Host's Postgres connection pool from the stack configuration
3
- * (architecture §12.2, §14.5). One pool serves the Tenant catalog and every
4
- * Tenant schema; the Host ends it at shutdown. The driver stays behind
3
+ * (architecture §12.2, §14.5): the pool on the Tenant's database, for its bootstrap and its
4
+ * Session Store; the Host ends it at shutdown. The driver stays behind
5
5
  * `store/postgres/connect.ts`.
6
6
  */
7
7
  import type { StackConfig } from "../host/stack-config.js";
@@ -1,5 +1,5 @@
1
1
  import type { DurableExecution } from "../execution/types.js";
2
- import type { RuntimeRole, StackConfig } from "../host/stack-config.js";
2
+ import { type RuntimeServices, type StackConfig } from "../host/stack-config.js";
3
3
  /** Where a Worker serves the Restate endpoint (architecture §14.3). */
4
4
  export declare const WORKER_LISTEN: {
5
5
  readonly host: "0.0.0.0";
@@ -23,14 +23,14 @@ export interface CreateExecutionOptions {
23
23
  export declare function executionKind(config: Pick<StackConfig, "endpoints">): "restate" | "memory";
24
24
  /**
25
25
  * Throws naming the missing variable when the Restate endpoints are
26
- * incomplete for `role`. The api role never serves the Worker endpoint, so it
27
- * needs no `NYLORUN_WORKER_URL`.
26
+ * incomplete for `services`. Without the loop service a process never serves
27
+ * the Worker endpoint, so it needs no `NYLORUN_WORKER_URL`.
28
28
  */
29
29
  export declare function validateExecutionConfig(config: Pick<StackConfig, "endpoints"> & {
30
- role?: RuntimeRole;
30
+ services?: RuntimeServices;
31
31
  }): void;
32
32
  export declare function createExecution(config: Pick<StackConfig, "endpoints"> & {
33
- role?: RuntimeRole;
33
+ services?: RuntimeServices;
34
34
  }, options?: CreateExecutionOptions): DurableExecution;
35
35
  /**
36
36
  * Readiness probe: resolves when Restate's admin API and ingress answer
@@ -5,9 +5,9 @@
5
5
  * With `NYLORUN_RESTATE_INGRESS_URL` and `NYLORUN_RESTATE_ADMIN_URL` the Host
6
6
  * uses Restate:
7
7
  *
8
- * - wakes, timers and sweep arming go through the ingress, so every role can
8
+ * - wakes, timers and sweep arming go through the ingress, so every process can
9
9
  * call them;
10
- * - a Worker (`--role worker|all`) serves the Restate endpoint on
10
+ * - a Worker (a process running the loop service) serves the Restate endpoint on
11
11
  * `0.0.0.0:9080` and registers `NYLORUN_WORKER_URL` with the admin API when
12
12
  * `infra/workers.ts` starts it;
13
13
  * - with `NYLORUN_RESTATE_IDENTITY_KEY` the endpoint accepts only requests
@@ -18,6 +18,7 @@
18
18
  */
19
19
  import { createRestateExecution } from "../adapters/execution/restate.js";
20
20
  import { MemoryExecution } from "../execution/memory.js";
21
+ import { DEFAULT_SERVICES, describeServices, } from "../host/stack-config.js";
21
22
  /** Where a Worker serves the Restate endpoint (architecture §14.3). */
22
23
  export const WORKER_LISTEN = { host: "0.0.0.0", port: 9080 };
23
24
  /** Which implementation `createExecution` chooses for a configuration. */
@@ -27,8 +28,8 @@ export function executionKind(config) {
27
28
  }
28
29
  /**
29
30
  * Throws naming the missing variable when the Restate endpoints are
30
- * incomplete for `role`. The api role never serves the Worker endpoint, so it
31
- * needs no `NYLORUN_WORKER_URL`.
31
+ * incomplete for `services`. Without the loop service a process never serves
32
+ * the Worker endpoint, so it needs no `NYLORUN_WORKER_URL`.
32
33
  */
33
34
  export function validateExecutionConfig(config) {
34
35
  const { restateIngressUrl, restateAdminUrl, workerUrl, restateIdentityKeys } = config.endpoints;
@@ -41,8 +42,9 @@ export function validateExecutionConfig(config) {
41
42
  throw new Error("NYLORUN_RESTATE_INGRESS_URL is required with NYLORUN_RESTATE_ADMIN_URL");
42
43
  if (!restateAdminUrl)
43
44
  throw new Error("NYLORUN_RESTATE_ADMIN_URL is required with NYLORUN_RESTATE_INGRESS_URL");
44
- if ((config.role ?? "all") !== "api" && !workerUrl)
45
- throw new Error(`NYLORUN_WORKER_URL is required for --role ${config.role ?? "all"}: the Worker endpoint URL Restate calls`);
45
+ const services = config.services ?? DEFAULT_SERVICES;
46
+ if (services.has("loop") && !workerUrl)
47
+ throw new Error(`NYLORUN_WORKER_URL is required for --service ${describeServices(services)}: the Worker endpoint URL Restate calls`);
46
48
  }
47
49
  export function createExecution(config, options = {}) {
48
50
  validateExecutionConfig(config);
@@ -35,7 +35,7 @@ export interface Infra {
35
35
  * Builds the configured clients. Throws naming the variable when endpoints are
36
36
  * incomplete (e.g. a Restate ingress without an admin URL).
37
37
  */
38
- export declare function createInfra(config: Pick<StackConfig, "role" | "endpoints">, options?: {
38
+ export declare function createInfra(config: Pick<StackConfig, "services" | "endpoints">, options?: {
39
39
  logger?: Logger;
40
40
  execution?: CreateExecutionOptions;
41
41
  streams?: CreateStreamsOptions;
@@ -1,39 +1,39 @@
1
1
  /**
2
- * Worker lifecycle by role (architecture §6).
2
+ * Worker lifecycle by service (architecture §6, blueprint §19).
3
3
  *
4
- * | Role | Serves | `execution.start` |
4
+ * | Services | Serves | `execution.start` |
5
5
  * | --- | --- | --- |
6
- * | `api` | Tenant API, Admin API, SSE | **never**: wakes, timers and sweep arming go through Restate's ingress, which needs no Worker endpoint |
7
- * | `worker` | the Restate endpoint (`NylorunSession.advance`, `NylorunTenant.sweep`, timers) | yes |
8
- * | `all` | both (the local stack) | yes |
6
+ * | `core` | Tenant API, Admin API, SSE | **never**: wakes, timers and sweep arming go through Restate's ingress, which needs no Worker endpoint |
7
+ * | `loop` | the Restate endpoint (`NylorunSession.advance`, `NylorunTenant.sweep`, timers) | yes |
8
+ * | `core,loop` | both (the local stack's `runtime` container) | yes |
9
9
  *
10
- * An API node that called `start` would register its own endpoint with
10
+ * A core-only process that called `start` would register its own endpoint with
11
11
  * Restate, and Restate would send new invocations to it (it routes to the
12
12
  * latest registered deployment), so the rule is enforced here rather than left
13
- * to callers: `startWorker` with role `api` returns a handle that never
14
- * started anything.
13
+ * to callers: `startWorker` without the loop service returns a handle that
14
+ * never started anything.
15
15
  */
16
16
  import type { DurableExecution, WorkerHandlers } from "../execution/types.js";
17
- import type { RuntimeRole } from "../host/stack-config.js";
17
+ import type { RuntimeServices } from "../host/stack-config.js";
18
18
  export interface WorkerHandle {
19
- readonly role: RuntimeRole;
19
+ readonly services: RuntimeServices;
20
20
  /** True when this process serves the Worker endpoint. */
21
21
  readonly serving: boolean;
22
22
  }
23
- /** Whether a process in `role` serves the Worker endpoint. */
24
- export declare function servesWorker(role: RuntimeRole): boolean;
23
+ /** Whether a process running `services` serves the Worker endpoint: when it runs loop. */
24
+ export declare function servesWorker(services: RuntimeServices): boolean;
25
25
  /**
26
- * Starts delivering Restate invocations to `handlers` when `role` is `worker`
27
- * or `all` (serving and registering the Worker endpoint, for Restate). For
28
- * `api` it does nothing. Rejects if the endpoint cannot listen or register.
26
+ * Starts delivering Restate invocations to `handlers` when the process runs
27
+ * loop (serving and registering the Worker endpoint, for Restate). Without
28
+ * loop it does nothing. Rejects if the endpoint cannot listen or register.
29
29
  */
30
30
  export declare function startWorker(input: {
31
- role: RuntimeRole;
31
+ services: RuntimeServices;
32
32
  execution: DurableExecution;
33
33
  handlers: WorkerHandlers;
34
34
  }): Promise<WorkerHandle>;
35
35
  /**
36
36
  * Stops a Worker started by `startWorker`: aborts running advances' signals
37
- * and waits for them. Idempotent; a no-op for the api role.
37
+ * and waits for them. Idempotent; a no-op without the loop service.
38
38
  */
39
39
  export declare function stopWorker(handle: WorkerHandle): Promise<void>;
@@ -1,17 +1,17 @@
1
- /** Whether a process in `role` serves the Worker endpoint. */
2
- export function servesWorker(role) {
3
- return role !== "api";
1
+ /** Whether a process running `services` serves the Worker endpoint: when it runs loop. */
2
+ export function servesWorker(services) {
3
+ return services.has("loop");
4
4
  }
5
5
  /** What `stopWorker` needs for a handle that serves the endpoint. */
6
6
  const started = new WeakMap();
7
7
  /**
8
- * Starts delivering Restate invocations to `handlers` when `role` is `worker`
9
- * or `all` (serving and registering the Worker endpoint, for Restate). For
10
- * `api` it does nothing. Rejects if the endpoint cannot listen or register.
8
+ * Starts delivering Restate invocations to `handlers` when the process runs
9
+ * loop (serving and registering the Worker endpoint, for Restate). Without
10
+ * loop it does nothing. Rejects if the endpoint cannot listen or register.
11
11
  */
12
12
  export async function startWorker(input) {
13
- const { role, execution, handlers } = input;
14
- const handle = Object.freeze({ role, serving: servesWorker(role) });
13
+ const { services, execution, handlers } = input;
14
+ const handle = Object.freeze({ services, serving: servesWorker(services) });
15
15
  if (!handle.serving)
16
16
  return handle;
17
17
  await execution.start(handlers);
@@ -20,7 +20,7 @@ export async function startWorker(input) {
20
20
  }
21
21
  /**
22
22
  * Stops a Worker started by `startWorker`: aborts running advances' signals
23
- * and waits for them. Idempotent; a no-op for the api role.
23
+ * and waits for them. Idempotent; a no-op without the loop service.
24
24
  */
25
25
  export async function stopWorker(handle) {
26
26
  const worker = started.get(handle);
@@ -0,0 +1,9 @@
1
+ import { type Keys } from "./keys.js";
2
+ export interface HttpKeysOptions {
3
+ /** The gateway, e.g. `http://gateway:4100` (`NYLORUN_KEYS_URL`). */
4
+ readonly url: string;
5
+ /** `NYLORUN_GATES_TOKEN`. */
6
+ readonly token: string;
7
+ readonly timeoutMs?: number;
8
+ }
9
+ export declare function httpKeys(options: HttpKeysOptions): Keys;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * The runtime container's client of the `keys` service in the gateway (F4.2, `contract.ts`).
3
+ * Over `node:http` rather than `fetch`, which tests and embedders may replace. A refusal comes
4
+ * back as the error the operation threw in the gateway (`VaultError` or `HttpError`, same status,
5
+ * code and details); a hop failure is a 503, so the API route answers that the gateway is down.
6
+ */
7
+ import { request as httpRequest } from "node:http";
8
+ import { request as httpsRequest } from "node:https";
9
+ import { HttpError } from "../tenant/http.js";
10
+ import { VaultError } from "../vault/error.js";
11
+ import { KEYS_PATH } from "./contract.js";
12
+ import { KEYS_OPERATIONS } from "./keys.js";
13
+ /** Signing and vault writes are short; anything slower is a gateway in trouble. */
14
+ const KEYS_TIMEOUT_MS = 30_000;
15
+ export function httpKeys(options) {
16
+ const where = new URL(options.url).origin;
17
+ const timeoutMs = options.timeoutMs ?? KEYS_TIMEOUT_MS;
18
+ function call(operation, args) {
19
+ const url = new URL(`${KEYS_PATH}/${operation}`, options.url);
20
+ const send = url.protocol === "https:" ? httpsRequest : httpRequest;
21
+ const payload = Buffer.from(JSON.stringify({ args }), "utf8");
22
+ return new Promise((resolve, reject) => {
23
+ const unavailable = (detail) => reject(new HttpError(503, `The keys service at ${where} is unavailable (${detail}); check the gateway container`, {
24
+ code: "keys_unavailable",
25
+ }));
26
+ const outgoing = send(url, {
27
+ method: "POST",
28
+ headers: {
29
+ authorization: `Bearer ${options.token}`,
30
+ "content-type": "application/json",
31
+ "content-length": payload.byteLength,
32
+ },
33
+ timeout: timeoutMs,
34
+ }, (response) => {
35
+ const chunks = [];
36
+ response.on("data", (chunk) => chunks.push(chunk));
37
+ response.on("error", (error) => unavailable(error.message));
38
+ response.on("end", () => {
39
+ let answer;
40
+ try {
41
+ answer = JSON.parse(Buffer.concat(chunks).toString("utf8"));
42
+ }
43
+ catch {
44
+ answer = undefined;
45
+ }
46
+ if (response.statusCode !== 200 || !answer)
47
+ return unavailable(response.statusCode === 401
48
+ ? "it refused this Runtime's token: the runtime and gateway containers must share NYLORUN_GATES_TOKEN"
49
+ : `it answered ${response.statusCode}`);
50
+ if ("result" in answer)
51
+ return resolve(answer.result);
52
+ const error = answer.error;
53
+ reject(error.kind === "vault"
54
+ ? new VaultError(error.status, error.message)
55
+ : new HttpError(error.status, error.message, {
56
+ ...(error.code === undefined ? {} : { code: error.code }),
57
+ ...(error.details === undefined ? {} : { details: error.details }),
58
+ }));
59
+ });
60
+ });
61
+ outgoing.on("timeout", () => outgoing.destroy(new Error(`no answer in ${timeoutMs / 1000} s`)));
62
+ outgoing.on("error", (error) => unavailable(error.message));
63
+ outgoing.end(payload);
64
+ });
65
+ }
66
+ const keys = {};
67
+ for (const operation of KEYS_OPERATIONS)
68
+ keys[operation] = (...args) => call(operation, args);
69
+ return keys;
70
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The `keys` service's wire format (F4.2): `POST /nylorun/v1/keys/{operation}` with
3
+ * `{args}`, the operation's arguments in order, on the gateway's listener with the stack's
4
+ * gates token. Internal: only core calls it, so it is not in the published OpenAPI documents.
5
+ *
6
+ * `200 {result}` when the operation succeeded, `200 {error}` when it refused: a vault error or
7
+ * a Tenant API error with its status, code and details, so the route that called it answers
8
+ * exactly what it answered when the operation ran in its own process.
9
+ */
10
+ export declare const KEYS_PATH = "/nylorun/v1/keys";
11
+ /** Largest request body: a credential with its secret, or a token's claims. */
12
+ export declare const MAX_KEYS_BODY_BYTES: number;
13
+ export interface KeysError {
14
+ readonly kind: "vault" | "http";
15
+ readonly status: number;
16
+ readonly message: string;
17
+ readonly code?: string;
18
+ readonly details?: unknown;
19
+ }
20
+ export type KeysAnswer = {
21
+ readonly result: unknown;
22
+ } | {
23
+ readonly error: KeysError;
24
+ };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The `keys` service's wire format (F4.2): `POST /nylorun/v1/keys/{operation}` with
3
+ * `{args}`, the operation's arguments in order, on the gateway's listener with the stack's
4
+ * gates token. Internal: only core calls it, so it is not in the published OpenAPI documents.
5
+ *
6
+ * `200 {result}` when the operation succeeded, `200 {error}` when it refused: a vault error or
7
+ * a Tenant API error with its status, code and details, so the route that called it answers
8
+ * exactly what it answered when the operation ran in its own process.
9
+ */
10
+ export const KEYS_PATH = "/nylorun/v1/keys";
11
+ /** Largest request body: a credential with its secret, or a token's claims. */
12
+ export const MAX_KEYS_BODY_BYTES = 1024 * 1024;
@@ -0,0 +1,41 @@
1
+ import type { CreateCredentialRequest, CredentialInfo, HostModelView, PutHostModelRequest, RotateCredentialRequest, SelectHostModelRequest, SigningKeyView } from "@nylorun/core/contracts";
2
+ import type { SessionStore } from "../store/types.js";
3
+ import { type SigningKeys } from "../tenant/signing-keys.js";
4
+ import type { VaultService } from "../vault/service.js";
5
+ /** One JWT to sign: its `typ` and every claim, `iss`, `aud`, `sub`, `iat`, `exp` and `jti` included. */
6
+ export interface SignRequest {
7
+ readonly typ: string;
8
+ readonly claims: Readonly<Record<string, unknown>>;
9
+ }
10
+ export interface Signed {
11
+ readonly token: string;
12
+ /** The signing key's id, the token's `kid`. */
13
+ readonly keyId: string;
14
+ }
15
+ export interface Keys {
16
+ createCredential(vaultId: string, body: CreateCredentialRequest): Promise<CredentialInfo>;
17
+ rotateCredential(vaultId: string, id: string, body: RotateCredentialRequest): Promise<CredentialInfo>;
18
+ putHostModel(body: PutHostModelRequest): Promise<HostModelView>;
19
+ selectHostModel(body: SelectHostModelRequest): Promise<HostModelView>;
20
+ /** Signs with the current key, creating the Tenant's keys if it has none. */
21
+ sign(request: SignRequest): Promise<Signed>;
22
+ /** Rotates the signing keys (`SigningKeys.rotate`); 409 while the previous key may verify live tokens. */
23
+ rotateSigningKeys(request: {
24
+ maxTtlSeconds: number;
25
+ force: boolean;
26
+ }): Promise<SigningKeyView[]>;
27
+ /** Creates the current and standby keys when the Tenant has none. */
28
+ ensureSigningKeys(): Promise<void>;
29
+ }
30
+ /** The operations, by the name the HTTP route carries. */
31
+ export declare const KEYS_OPERATIONS: readonly ["createCredential", "rotateCredential", "putHostModel", "selectHostModel", "sign", "rotateSigningKeys", "ensureSigningKeys"];
32
+ export type KeysOperation = (typeof KEYS_OPERATIONS)[number];
33
+ export interface InProcessKeysOptions {
34
+ readonly store: SessionStore;
35
+ /** Its vault key is `kek`. */
36
+ readonly vault: VaultService;
37
+ readonly signingKeys: SigningKeys;
38
+ /** The vault key. */
39
+ readonly kek: () => Buffer;
40
+ }
41
+ export declare function inProcessKeys(options: InProcessKeysOptions): Keys;