@nylorun/runtime 0.14.0-beta → 0.15.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.
Files changed (198) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/README.md +82 -40
  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 +185 -686
  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 +6 -7
  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 +888 -695
  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
package/dist/host/app.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Hono } from "hono";
2
2
  import { RESPONSE_ALREADY_SENT } from "@hono/node-server/utils/response";
3
- import { checkCompatibility, HOST_PROTOCOL, isTenantId, PROTOCOL_HEADER, PUBLISHABLE_KEY_HEADER, TENANT_HEADER, tenantOfPublishableKey, } from "@nylorun/core/compatibility";
3
+ import { checkCompatibility, HOST_PROTOCOL, PROTOCOL_HEADER, PUBLISHABLE_KEY_HEADER, TENANT_HEADER, tenantOfPublishableKey, } from "@nylorun/core/compatibility";
4
4
  import { RUNTIME_VERSION } from "../version.js";
5
5
  import { findTenantRoute } from "../api/http/app.js";
6
6
  import { tenantDocument } from "../api/openapi.js";
@@ -69,13 +69,13 @@ export function createHostApp(options) {
69
69
  });
70
70
  if (pathname === "/ready") {
71
71
  const listener = options.listening();
72
- const discovery = module.started;
72
+ const tenant = module.ready;
73
73
  const infra = await options.readiness?.();
74
- const ready = listener && discovery && !options.closing() && (infra?.ok ?? true);
74
+ const ready = listener && tenant && !options.closing() && (infra?.ok ?? true);
75
75
  return jsonResponse(ready ? 200 : 503, {
76
76
  status: ready ? "ready" : "not_ready",
77
77
  service: "nylorun-runtime",
78
- checks: { listener, discovery, ...infra?.checks },
78
+ checks: { listener, tenant, ...infra?.checks },
79
79
  });
80
80
  }
81
81
  // This API's own description: public, as the npm package that ships it.
@@ -93,36 +93,32 @@ export function createHostApp(options) {
93
93
  return opaqueNotFoundResponse();
94
94
  return await options.admin(c.req.raw, { incoming, outgoing });
95
95
  }
96
- // Tenant routes: header pattern → protocol → resolve → the Tenant. The Tenant is named by
97
- // `Nylorun-Tenant`, by the publishable key in `Nylorun-Key`, or by both when they agree.
98
- const invalid = (message) => rejectedResponse(400, "invalid_request", message);
96
+ // Tenant routes: key pattern → protocol → the Tenant → selection → the Tenant's routes.
99
97
  const keyHeader = headerValue(incoming, PUBLISHABLE_KEY_HEADER);
100
98
  const keyTenant = keyHeader === undefined ? undefined : tenantOfPublishableKey(keyHeader);
101
99
  if (keyHeader !== undefined && keyTenant === undefined)
102
- return invalid(`${PUBLISHABLE_KEY_HEADER} header is malformed`);
100
+ return rejectedResponse(400, "invalid_request", `${PUBLISHABLE_KEY_HEADER} header is malformed`);
103
101
  const tenantHeader = headerValue(incoming, TENANT_HEADER);
104
- const named = tenantHeader === undefined || tenantHeader.trim() === "" ? undefined : tenantHeader;
105
- if (named === undefined && keyTenant === undefined)
106
- return invalid(`${TENANT_HEADER} header is required`);
107
- if (named !== undefined && !isTenantId(named))
108
- return invalid(`${TENANT_HEADER} header is malformed`);
109
- if (named !== undefined && keyTenant !== undefined && named !== keyTenant)
110
- return invalid(`${PUBLISHABLE_KEY_HEADER} and ${TENANT_HEADER} name different Tenants`);
111
- const tenantId = (named ?? keyTenant);
112
- c.set("tenantId", tenantId);
102
+ const named = tenantHeader === undefined || tenantHeader.trim() === "" ? undefined : tenantHeader.trim();
113
103
  if (!protocolAccepted(headerValue(incoming, PROTOCOL_HEADER)))
114
104
  return protocolRejectedResponse();
115
- const resolution = await module.resolve(tenantId);
105
+ const resolution = await module.resolve();
116
106
  if (resolution.kind !== "open") {
117
- if (resolution.kind === "quarantined")
118
- logger.warn("tenant_quarantined", {
119
- tenantId,
120
- code: resolution.quarantine.code,
121
- repair: resolution.quarantine.repair,
107
+ if (resolution.cause)
108
+ logger.warn("tenant_unavailable", {
109
+ code: resolution.cause.code,
110
+ repair: resolution.cause.repair,
122
111
  });
123
112
  return opaqueNotFoundResponse();
124
113
  }
125
- return await resolution.handle.fetch(c.req.raw, { incoming, outgoing });
114
+ const { handle } = resolution;
115
+ const tenantId = handle.envelope.id;
116
+ // A protocol 4 client names the Tenant; a publishable key carries one. Either naming
117
+ // another Tenant (or a malformed header) reached the wrong installation.
118
+ if ((named !== undefined && named !== tenantId) || (keyTenant !== undefined && keyTenant !== tenantId))
119
+ return opaqueNotFoundResponse();
120
+ c.set("tenantId", tenantId);
121
+ return await handle.fetch(c.req.raw, { incoming, outgoing });
126
122
  });
127
123
  app.onError((error, c) => {
128
124
  const status = error.status;
@@ -1,7 +1,7 @@
1
1
  import type { HostConfigFile } from "./config.js";
2
2
  import type { Logger, TenantConfig } from "../tenant/types.js";
3
3
  /**
4
- * Builds a TenantConfig for `openTenantRuntime`.
4
+ * Builds the TenantConfig of the Host's Tenant for `openTenantRuntime`.
5
5
  * The sandbox backend here is the Host default; a Tenant's seeded `sandbox.backend`
6
6
  * setting overrides it when the Tenant opens (I1, A18).
7
7
  */
@@ -1,14 +1,14 @@
1
1
  import { tenantPaths } from "../tenant/paths.js";
2
2
  import { tenantChildEnvironment } from "./environment.js";
3
3
  /**
4
- * Builds a TenantConfig for `openTenantRuntime`.
4
+ * Builds the TenantConfig of the Host's Tenant for `openTenantRuntime`.
5
5
  * The sandbox backend here is the Host default; a Tenant's seeded `sandbox.backend`
6
6
  * setting overrides it when the Tenant opens (I1, A18).
7
7
  */
8
8
  export function configForFactory(options) {
9
9
  const { baseline } = options;
10
10
  return (id) => {
11
- const tenant = tenantPaths(options.hostRoot, id);
11
+ const tenant = tenantPaths(options.hostRoot);
12
12
  const sandboxBackend = "auto";
13
13
  return {
14
14
  tenantId: id,
@@ -39,18 +39,18 @@ export interface CreateHostOptions {
39
39
  operator?: OperatorListen;
40
40
  /**
41
41
  * Infrastructure readiness (`infra/readiness.ts`). `/ready` adds its checks
42
- * and answers 503 while it reports not ok. Default: listener and discovery
43
- * only.
42
+ * and answers 503 while it reports not ok. Default: the listener and the
43
+ * Tenant only.
44
44
  */
45
45
  readiness?: () => Promise<{
46
46
  ok: boolean;
47
47
  checks: Record<string, boolean>;
48
48
  }>;
49
49
  /**
50
- * Shutdown steps around closing the Tenants. `close()` runs them whatever asked for it
50
+ * Shutdown steps around closing the Tenant. `close()` runs them whatever asked for it
51
51
  * (SIGTERM in `host/main.ts`, `POST /v1/admin/host/shutdown`): the listener stops, then
52
- * `beforeTenants` (stop the Worker so no advance starts on a closing Tenant), the Tenants
53
- * close, then `afterTenants` (end the infrastructure clients). A failing step is logged
52
+ * `beforeTenants` (stop the Worker so no advance starts on the closing Tenant), the Tenant
53
+ * closes, then `afterTenants` (end the infrastructure clients). A failing step is logged
54
54
  * and shutdown goes on.
55
55
  */
56
56
  shutdown?: {
@@ -5,7 +5,7 @@ import { AdminStatusSchema } from "@nylorun/core/contracts";
5
5
  import { adminDocument } from "../api/openapi.js";
6
6
  import { createAdminApi } from "./admin-api.js";
7
7
  import { createHostApp } from "./app.js";
8
- import { EXIT_NON_LOOPBACK, EXIT_PORT_IN_USE, headerValue, HostListenError, isAllowedRequestHost, isLoopbackHost, pathnameIsLogged, redactRoutePath, rejectedResponse, sendRejected, } from "./http.js";
8
+ import { bindListener as bind, EXIT_NON_LOOPBACK, headerValue, HostListenError, isAllowedRequestHost, isLoopbackHost, pathnameIsLogged, redactRoutePath, rejectedResponse, sendRejected, } from "./http.js";
9
9
  import { RUNTIME_VERSION } from "../version.js";
10
10
  export { adminKeyMatches } from "./http.js";
11
11
  export function createHost(options) {
@@ -25,7 +25,6 @@ export function createHost(options) {
25
25
  let closing = false;
26
26
  let closePromise;
27
27
  const adminStatusBody = async () => {
28
- const tenants = await module.list();
29
28
  const aggregate = await module.summarize();
30
29
  return AdminStatusSchema.parse({
31
30
  service: "nylorun-runtime",
@@ -35,7 +34,7 @@ export function createHost(options) {
35
34
  max: HOST_PROTOCOL.max,
36
35
  features: [...HOST_PROTOCOL.features],
37
36
  },
38
- tenants,
37
+ tenant: module.tenant(),
39
38
  aggregate,
40
39
  host: {
41
40
  hostId: config.hostId,
@@ -45,7 +44,6 @@ export function createHost(options) {
45
44
  });
46
45
  };
47
46
  const adminApi = createAdminApi({
48
- module,
49
47
  status: adminStatusBody,
50
48
  shutdown: () => void close(),
51
49
  document: adminDocument,
@@ -114,17 +112,7 @@ export function createHost(options) {
114
112
  };
115
113
  };
116
114
  /** Binds `listening`; once bound, a listener error is logged rather than lost. */
117
- const bindListener = (listening, port, host) => new Promise((resolve, reject) => {
118
- const failed = (error) => reject(error.code === "EADDRINUSE"
119
- ? new HostListenError(`Port ${port} on ${host} is already in use`, EXIT_PORT_IN_USE, error)
120
- : error);
121
- listening.once("error", failed);
122
- listening.listen(port, host, () => {
123
- listening.off("error", failed);
124
- listening.on("error", (error) => logger.error("listener_error", { error: error.message }));
125
- resolve();
126
- });
127
- });
115
+ const bindListener = (listening, port, host) => bind(listening, port, host, (error) => logger.error("listener_error", { error: error.message }));
128
116
  async function listen() {
129
117
  if (server)
130
118
  throw new Error("Already listening");
@@ -159,6 +147,8 @@ export function createHost(options) {
159
147
  : operator.port;
160
148
  adminUrl = `http://${operator.host}:${operatorPort}`;
161
149
  }
150
+ // The Tenant opens once the listener is bound: `/ready` and `/v1/admin/status` answer
151
+ // while it opens, and say why when it cannot.
162
152
  await module.start();
163
153
  logger.info("host_listening", {
164
154
  hostId: config.hostId,
@@ -1,37 +1,39 @@
1
1
  /**
2
2
  * The Host's Durable Session Execution (architecture §6, §12.3, §14.8): one per process,
3
- * shared by every Tenant the Host opens.
3
+ * for the Tenant the Host serves.
4
4
  *
5
- * - `tenantExecution` is what the Host passes to each Tenant it opens
5
+ * - `tenantExecution` is what the Host passes to its Tenant when it opens it
6
6
  * (`TenantOpenHooks.execution`): the execution, the `TenantWorkers` registry its handlers
7
- * dispatch through (with `resolve` opening a Tenant on demand when an invocation arrives for
8
- * one that is not open), and the Tenant status hook for stuck invocations.
9
- * - `start` serves the Worker endpoint for `--role worker|all` (`infra/workers.ts`) and does
10
- * nothing for `api`, whose wakes, timers and sweep arming go through Restate's ingress.
11
- * - `armAll` re-arms every Tenant's sweep at startup. Arming is idempotent, so it is safe on
12
- * every start, and it is what recovers from lost Restate state: the sweep re-wakes `runnable`
13
- * sessions without a live owner (§14.8).
14
- * - `disarm` stops a deleted Tenant's sweep.
7
+ * dispatch through (with `resolve` waiting for the Tenant when an invocation arrives while
8
+ * it opens), and the Tenant status hook for stuck invocations.
9
+ * - `start` serves the Worker endpoint when the process runs loop (`infra/workers.ts`), and does
10
+ * nothing for a core-only process, whose wakes, timers and sweep arming go through Restate's
11
+ * ingress.
12
+ * - `armAll` arms the listed Tenants' sweeps (tests, recovery). Arming is idempotent; opening
13
+ * the Tenant arms its own at every start, which is what recovers from lost Restate state:
14
+ * the sweep re-wakes `runnable` sessions without a live owner (§14.8).
15
+ * - `disarm` stops a Tenant's sweep.
15
16
  *
16
- * **Order at startup.** Call `start` before opening any Tenant. Opening a Tenant arms its
17
+ * **Order at startup.** Call `start` before opening the Tenant. Opening the Tenant arms its
17
18
  * sweep through Restate's ingress, which answers 404 until a Worker has registered the
18
- * services, so the open would fail. An api-role process relies on a Worker having registered
19
+ * services, so the open would fail. A core-only process relies on a Worker having registered
19
20
  * them first.
20
21
  *
21
22
  * **One Worker URL.** `start` registers the Worker endpoint (`NYLORUN_WORKER_URL`) with
22
23
  * Restate once. Restate sends new invocations to the latest registered deployment, so
23
24
  * several Workers that each register their own URL do not share load: the last one to start
24
- * takes every new invocation. The V1 stack runs a single `runtime` container with
25
- * `--role all`. A deployment with several Worker processes (Helm, Cloud) must advertise one
25
+ * takes every new invocation. The local stack runs a single `runtime` container with
26
+ * `--service core,loop`. A deployment with several Worker processes (Helm, Cloud) must advertise one
26
27
  * load-balanced `NYLORUN_WORKER_URL` shared by all of them.
27
28
  */
28
29
  import type { DurableExecution } from "../execution/types.js";
29
30
  import type { Logger } from "../tenant/types.js";
30
31
  import { type TenantExecution, type TenantWorker } from "../tenant/worker.js";
31
- import type { RuntimeRole } from "./stack-config.js";
32
+ import type { RuntimeServices } from "./stack-config.js";
32
33
  export interface CreateHostExecutionOptions {
33
34
  execution: DurableExecution;
34
- role: RuntimeRole;
35
+ /** The services this process runs: it serves the Worker endpoint when they include loop. */
36
+ services: RuntimeServices;
35
37
  /** Finds or opens a Tenant an invocation arrived for that is not open on this process. */
36
38
  resolve: (tenantId: string) => Promise<TenantWorker | undefined>;
37
39
  logger?: Logger;
@@ -44,9 +46,9 @@ export interface CreateHostExecutionOptions {
44
46
  advanceGraceMs?: number;
45
47
  }
46
48
  export interface HostExecution {
47
- /** Pass to every Tenant the Host opens (`TenantOpenHooks.execution`). */
49
+ /** Pass to the Tenant the Host opens (`TenantOpenHooks.execution`). */
48
50
  readonly tenantExecution: TenantExecution;
49
- /** Serves the Worker endpoint for the worker and all roles. Idempotent. */
51
+ /** Serves the Worker endpoint when the process runs loop. Idempotent. */
50
52
  start(): Promise<void>;
51
53
  /** Stops serving: aborts running advances and waits for them. Idempotent. */
52
54
  stop(): Promise<void>;
@@ -3,7 +3,7 @@ import { TenantWorkers, } from "../tenant/worker.js";
3
3
  /** How many sweep arms `armAll` sends at once. */
4
4
  const ARM_CONCURRENCY = 16;
5
5
  export function createHostExecution(options) {
6
- const { execution, role, logger } = options;
6
+ const { execution, services, logger } = options;
7
7
  const workers = new TenantWorkers({
8
8
  resolve: options.resolve,
9
9
  ...(options.advanceDeadlineMs !== undefined
@@ -27,7 +27,7 @@ export function createHostExecution(options) {
27
27
  return {
28
28
  tenantExecution,
29
29
  async start() {
30
- started ??= startWorker({ role, execution, handlers: workers.handlers });
30
+ started ??= startWorker({ services, execution, handlers: workers.handlers });
31
31
  try {
32
32
  await started;
33
33
  }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The gates service's process (blueprint §15, §19): one listener serving the Model Gate's and
3
+ * the Tool Gate's routes (`api/gate/routes.ts`), behind the same `Host` check as the Runtime's
4
+ * listeners.
5
+ * It opens no Tenant runtime, no Restate endpoint and no stream relay, and runs no migration;
6
+ * it needs only the Postgres pool and the Host's tenant directory (read-only).
7
+ *
8
+ * Nothing is written while a model call runs, so the server's request timeout sits above the
9
+ * gate's longest call. Closing stops accepting calls, lets running ones finish for up to
10
+ * `drainMs`, then aborts them (their providers' requests with them).
11
+ */
12
+ import { type Server } from "node:http";
13
+ import { type InflightCallsOptions } from "../gates/inflight.js";
14
+ import type { openMcpServer } from "../mcp/connect.js";
15
+ import type { OutboundPolicy } from "../tenant/outbound.js";
16
+ import type { ModelCallSettings } from "../gates/model-gate.js";
17
+ import { type TenantVaults } from "../gates/tenant-vaults.js";
18
+ import type { PostgresClient } from "../store/postgres/connect.js";
19
+ import type { Logger } from "../tenant/types.js";
20
+ import type { GatesConfig } from "./stack-config.js";
21
+ /** Above the gate's 600 s provider request timeout and the loop's 630 s client timeout. */
22
+ export declare const GATES_REQUEST_TIMEOUT_MS = 660000;
23
+ export interface StartGatesOptions {
24
+ readonly gates: GatesConfig;
25
+ readonly logger: Logger;
26
+ /** The Host root, for the Tenant's vault key. Required unless `vaults` is given. */
27
+ readonly hostRoot?: string;
28
+ /** The pool for the Tenant's vault and readiness. Required unless `vaults` is given. */
29
+ readonly database?: PostgresClient;
30
+ /** Replaces the Postgres-backed Tenant vault (tests). */
31
+ readonly vaults?: TenantVaults;
32
+ /** Retries and timeouts of model calls (tests). */
33
+ readonly settings?: ModelCallSettings;
34
+ /** Largest model call body (tests). */
35
+ readonly maxBodyBytes?: number;
36
+ /** How long `close` lets running calls finish. Default 10 s. */
37
+ readonly drainMs?: number;
38
+ /** How long keyed outcomes are kept, and how many (tests). */
39
+ readonly inflight?: InflightCallsOptions;
40
+ /** How the gate may call Action endpoints (`NYLORUN_ENDPOINT_*`). Default: no limits. */
41
+ readonly delivery?: OutboundPolicy;
42
+ /** How long an MCP connection may sit unused. Default `GATE_MCP_IDLE_MS`. */
43
+ readonly mcpIdleMs?: number;
44
+ /** Tests replace how a remote MCP server is opened. */
45
+ readonly openMcp?: typeof openMcpServer;
46
+ /**
47
+ * Serve the keys service too (`--service gates,keys`, F4.2): vault writes and token signing
48
+ * with the Tenant's vault key. The gateway is then not ready until the key file is there.
49
+ */
50
+ readonly keys?: boolean;
51
+ }
52
+ export interface GatesServer {
53
+ /** Where the gate listens, e.g. `http://0.0.0.0:4100`. */
54
+ readonly url: string;
55
+ readonly server: Server;
56
+ close(): Promise<void>;
57
+ }
58
+ export declare function startGates(options: StartGatesOptions): Promise<GatesServer>;
@@ -0,0 +1,127 @@
1
+ /**
2
+ * The gates service's process (blueprint §15, §19): one listener serving the Model Gate's and
3
+ * the Tool Gate's routes (`api/gate/routes.ts`), behind the same `Host` check as the Runtime's
4
+ * listeners.
5
+ * It opens no Tenant runtime, no Restate endpoint and no stream relay, and runs no migration;
6
+ * it needs only the Postgres pool and the Host's tenant directory (read-only).
7
+ *
8
+ * Nothing is written while a model call runs, so the server's request timeout sits above the
9
+ * gate's longest call. Closing stops accepting calls, lets running ones finish for up to
10
+ * `drainMs`, then aborts them (their providers' requests with them).
11
+ */
12
+ import { createServer } from "node:http";
13
+ import { getRequestListener } from "@hono/node-server";
14
+ import { createGatesApp } from "../api/gate/routes.js";
15
+ import { createModelCallHandler } from "../gates/handler.js";
16
+ import { createInflightCalls } from "../gates/inflight.js";
17
+ import { createMcpHandler, GATE_MCP_IDLE_MS } from "../gates/mcp-handler.js";
18
+ import { createToolCalls } from "../gates/tool-calls.js";
19
+ import { existsSync } from "node:fs";
20
+ import { tenantPaths } from "../tenant/paths.js";
21
+ import { createTenantVaults } from "../gates/tenant-vaults.js";
22
+ import { probeDatabase } from "../infra/database.js";
23
+ import { bindListener, headerValue, isAllowedRequestHost, sendRejected } from "./http.js";
24
+ /** Above the gate's 600 s provider request timeout and the loop's 630 s client timeout. */
25
+ export const GATES_REQUEST_TIMEOUT_MS = 660_000;
26
+ export async function startGates(options) {
27
+ const { gates, logger, database } = options;
28
+ let vaults = options.vaults;
29
+ if (!vaults) {
30
+ if (!database || options.hostRoot === undefined)
31
+ throw new Error("startGates needs the Postgres pool and the Host root");
32
+ vaults = createTenantVaults({ sql: database, hostRoot: options.hostRoot });
33
+ }
34
+ const inflight = createInflightCalls(options.inflight);
35
+ const mcpIdleMs = options.mcpIdleMs ?? GATE_MCP_IDLE_MS;
36
+ const mcp = createMcpHandler({
37
+ vaults,
38
+ logger,
39
+ idleMs: mcpIdleMs,
40
+ ...(options.openMcp ? { open: options.openMcp } : {}),
41
+ });
42
+ const toolCalls = createToolCalls({
43
+ vaults,
44
+ mcp,
45
+ logger,
46
+ ...(options.inflight ? { inflight: options.inflight } : {}),
47
+ });
48
+ const sweep = setInterval(() => void mcp.sweep(), Math.max(1_000, Math.min(mcpIdleMs, 60_000)));
49
+ sweep.unref();
50
+ const prune = setInterval(() => void toolCalls.prune(), 60 * 60_000);
51
+ prune.unref();
52
+ const app = createGatesApp({
53
+ token: gates.token,
54
+ inflight,
55
+ modelGate: createModelCallHandler({
56
+ vaults,
57
+ logger,
58
+ ...(options.settings ? { settings: options.settings } : {}),
59
+ }),
60
+ ready: async () => {
61
+ // The keys service needs the vault key, which `nylorun start` writes; it never creates one.
62
+ if (options.keys && options.hostRoot !== undefined && !existsSync(tenantPaths(options.hostRoot).kek))
63
+ return false;
64
+ if (!database)
65
+ return true;
66
+ try {
67
+ await probeDatabase(database, AbortSignal.timeout(2000));
68
+ return true;
69
+ }
70
+ catch {
71
+ return false;
72
+ }
73
+ },
74
+ ...(options.maxBodyBytes !== undefined ? { maxBodyBytes: options.maxBodyBytes } : {}),
75
+ mcp,
76
+ toolCalls,
77
+ delivery: options.delivery ?? {},
78
+ logger,
79
+ ...(options.keys ? { keys: async () => (await vaults.open()).keys() } : {}),
80
+ });
81
+ const listener = getRequestListener(app.fetch);
82
+ const inFlight = new Set();
83
+ // The configured Host values, plus the loopback forms of the port actually bound (probes
84
+ // from inside the container; port 0 in tests).
85
+ let allowedHosts = gates.listen.allowedHosts;
86
+ const server = createServer((req, res) => {
87
+ if (!isAllowedRequestHost(headerValue(req, "host"), {
88
+ port: gates.listen.port,
89
+ host: gates.listen.host,
90
+ allowedHosts,
91
+ }))
92
+ return sendRejected(res, 421, "host_rejected", "Host header is not an allowed address of the gateway");
93
+ inFlight.add(res);
94
+ res.once("close", () => inFlight.delete(res));
95
+ void listener(req, res);
96
+ });
97
+ server.requestTimeout = GATES_REQUEST_TIMEOUT_MS;
98
+ await bindListener(server, gates.listen.port, gates.listen.host, (error) => logger.error("listener_error", { error: error.message }));
99
+ const port = server.address().port;
100
+ allowedHosts = [
101
+ ...new Set([...allowedHosts, `localhost:${port}`, `127.0.0.1:${port}`, `[::1]:${port}`]),
102
+ ];
103
+ const url = `http://${gates.listen.host.includes(":") ? `[${gates.listen.host}]` : gates.listen.host}:${port}`;
104
+ let closing;
105
+ return {
106
+ url,
107
+ server,
108
+ close() {
109
+ closing ??= (async () => {
110
+ const closed = new Promise((resolve) => server.close(() => resolve()));
111
+ server.closeIdleConnections();
112
+ const deadline = Date.now() + (options.drainMs ?? 10_000);
113
+ while (inFlight.size > 0 && Date.now() < deadline)
114
+ await new Promise((resolve) => setTimeout(resolve, 50));
115
+ // Closing a running call's connection aborts an unkeyed one; keyed calls stop here.
116
+ server.closeAllConnections();
117
+ inflight.close();
118
+ toolCalls.close();
119
+ clearInterval(sweep);
120
+ clearInterval(prune);
121
+ await mcp.closeAll();
122
+ await closed;
123
+ })();
124
+ return closing;
125
+ },
126
+ };
127
+ }
@@ -1,4 +1,4 @@
1
- import type { IncomingMessage, ServerResponse } from "node:http";
1
+ import type { IncomingMessage, Server, ServerResponse } from "node:http";
2
2
  import { jsonResponse } from "../api/http/respond.js";
3
3
  import { type ErrorCode, type ProtocolRange } from "@nylorun/core/compatibility";
4
4
  export declare const OPAQUE_NOT_FOUND: {
@@ -13,6 +13,11 @@ export declare class HostListenError extends Error {
13
13
  readonly cause?: unknown;
14
14
  constructor(message: string, exitCode: number, cause?: unknown);
15
15
  }
16
+ /**
17
+ * Binds `listening` on `host:port`. A port in use rejects with `HostListenError` (exit 98);
18
+ * once bound, a listener error goes to `onError` rather than being lost.
19
+ */
20
+ export declare function bindListener(listening: Server, port: number, host: string, onError: (error: Error) => void): Promise<void>;
16
21
  export declare function sendJson(response: ServerResponse, status: number, body: unknown): void;
17
22
  /** `sendJson` as a Response, byte for byte (with any extra headers): the Host's answers. */
18
23
  export { jsonResponse };
package/dist/host/http.js CHANGED
@@ -18,6 +18,23 @@ export class HostListenError extends Error {
18
18
  this.name = "HostListenError";
19
19
  }
20
20
  }
21
+ /**
22
+ * Binds `listening` on `host:port`. A port in use rejects with `HostListenError` (exit 98);
23
+ * once bound, a listener error goes to `onError` rather than being lost.
24
+ */
25
+ export function bindListener(listening, port, host, onError) {
26
+ return new Promise((resolve, reject) => {
27
+ const failed = (error) => reject(error.code === "EADDRINUSE"
28
+ ? new HostListenError(`Port ${port} on ${host} is already in use`, EXIT_PORT_IN_USE, error)
29
+ : error);
30
+ listening.once("error", failed);
31
+ listening.listen(port, host, () => {
32
+ listening.off("error", failed);
33
+ listening.on("error", onError);
34
+ resolve();
35
+ });
36
+ });
37
+ }
21
38
  export function sendJson(response, status, body) {
22
39
  const payload = JSON.stringify(body);
23
40
  // No Access-Control-* headers (D§11).
@@ -4,5 +4,5 @@ export { createHost, adminKeyMatches, type CreateHostOptions, type HostServer }
4
4
  export { createHostLogger } from "./logger.js";
5
5
  export { configForFactory } from "./config-for.js";
6
6
  export { createHostExecution, type CreateHostExecutionOptions, type HostExecution, } from "./execution.js";
7
- export { parseStackConfig, StackConfigError, type ContainerListen, type RuntimeRole, type StackConfig, type StackEndpoints, } from "./stack-config.js";
7
+ export { parseStackConfig, StackConfigError, type ContainerListen, type RuntimeRole, type RuntimeService, type RuntimeServices, type StackConfig, type StackEndpoints, } from "./stack-config.js";
8
8
  export { EXIT_PORT_IN_USE, EXIT_NON_LOOPBACK, HostListenError, OPAQUE_NOT_FOUND, } from "./http.js";