@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
package/dist/host/main.js CHANGED
@@ -2,30 +2,33 @@
2
2
  * Runtime Host process entry (`@nylorun/runtime/server`).
3
3
  *
4
4
  * Reads `NYLORUN_HOME` (falls back to `~/.nylorun`), and hands an environment
5
- * snapshot and argv to `parseStackConfig` (container listen mode, `--role`,
5
+ * snapshot and argv to `parseStackConfig` (container listen mode, `--service`,
6
6
  * stack endpoints). Absolute Host root is resolved once and passed down.
7
7
  *
8
8
  * Composition: `createInfra` builds the Postgres pool, Durable Session
9
9
  * Execution and Durable Streams from the endpoints; `createHostExecution`
10
- * shares one execution across the Tenants; the Tenant store is Postgres
11
- * (`NYLORUN_DATABASE_URL`, required), with the shared record of session events
12
- * (`nylorun_streams`, migrated here); with S2, an `api` or `all` process runs
13
- * the stream relay, which feeds every Tenant's streams from the record over
10
+ * builds the process's execution; the Host serves the one Tenant its Postgres
11
+ * database holds (`NYLORUN_DATABASE_URL`, required), and creates it there on
12
+ * first start (`NYLORUN_TENANT_ID`, `NYLORUN_TENANT_NAME`,
13
+ * `NYLORUN_DERIVED_PRINCIPALS`; tenancy.md §4); with S2, a process running core
14
+ * runs the stream relay, which feeds the Tenant's streams from the record over
14
15
  * logical replication (one process at a time holds the slot); `/ready` reports
15
- * the infrastructure checks. See the startup order in `main()`. Tests compose a Host without this
16
+ * the Tenant and the infrastructure checks. A process running the gates service (the local
17
+ * stack's `gateway` container) starts only the gate (`runGates`): it needs
18
+ * neither host.json nor host-credentials.json. See the startup order in `main()`. Tests compose a Host without this
16
19
  * entry, with `createHost` and an injected Tenant module.
17
20
  */
18
21
  import { readFileSync, mkdirSync, existsSync } from "node:fs";
19
22
  import { createRequire } from "node:module";
20
23
  import { homedir } from "node:os";
21
24
  import { resolve } from "node:path";
22
- import { hostPaths, trashSqliteTenants } from "../tenant/paths.js";
25
+ import { hostPaths } from "../tenant/paths.js";
23
26
  import { createTenantModule } from "../tenant/module.js";
24
- import { createPostgresTenantStore } from "../tenant/store-pg.js";
27
+ import { hostPrincipals } from "../tenant/principals.js";
28
+ import { createPostgresTenantOpener } from "../tenant/store-pg.js";
25
29
  import { openTenantRuntime } from "../tenant/runtime.js";
26
- import { createTenantStreams, deleteTenantStreams } from "../tenant/streams.js";
27
30
  import { createPgoutputSource } from "../adapters/replication/pgoutput.js";
28
- import { assertLogicalReplication, migrateStreamsSchema, } from "../store/postgres/migrations/shared/index.js";
31
+ import { assertLogicalReplication } from "../store/postgres/connect.js";
29
32
  import { createPostgresRecordReader } from "../store/postgres/record.js";
30
33
  import { createStreamRelay } from "../streams/relay/core.js";
31
34
  import { configForFactory } from "./config-for.js";
@@ -34,8 +37,13 @@ import { createHostExecution } from "./execution.js";
34
37
  import { baselineEnvironment } from "./environment.js";
35
38
  import { HostListenError, EXIT_PORT_IN_USE, EXIT_NON_LOOPBACK, } from "./http.js";
36
39
  import { createHostLogger } from "./logger.js";
37
- import { describeEndpoints, parseStackConfig } from "./stack-config.js";
40
+ import { describeEndpoints, describeServices, parseStackConfig, } from "./stack-config.js";
38
41
  import { createExecution, createInfra } from "../infra/index.js";
42
+ import { createDatabase } from "../infra/database.js";
43
+ import { startGates } from "./gates.js";
44
+ import { httpToolGate } from "../gates/tool-client.js";
45
+ import { httpKeys } from "../keys/client.js";
46
+ import { httpModelGate } from "../gates/http-client.js";
39
47
  const nodeRequire = createRequire(import.meta.url);
40
48
  function coreVersion() {
41
49
  try {
@@ -58,20 +66,69 @@ async function lagOf(source) {
58
66
  const lagBytes = await source.lag?.().catch(() => undefined);
59
67
  return lagBytes === undefined ? {} : { lagBytes };
60
68
  }
69
+ /**
70
+ * The gates service: the Model Gate's listener over the Postgres pool and the Host's tenant
71
+ * directory. Writes nothing to the Host root (the local stack mounts it read-only).
72
+ */
73
+ async function runGates(stack) {
74
+ const gates = stack.gates;
75
+ if (!stack.endpoints.databaseUrl)
76
+ throw new Error("NYLORUN_DATABASE_URL is required for --service gates: the gate reads Tenant vaults from Postgres (`nylorun start` sets it)");
77
+ const logger = createHostLogger();
78
+ logger.info("host_stack_config", {
79
+ services: [...stack.services],
80
+ ...(stack.packing ? { packing: stack.packing } : {}),
81
+ endpoints: describeEndpoints(stack.endpoints),
82
+ });
83
+ const database = createDatabase(stack);
84
+ let server;
85
+ try {
86
+ server = await startGates({
87
+ gates,
88
+ database,
89
+ hostRoot: resolveHostRoot(),
90
+ logger,
91
+ ...(stack.delivery ? { delivery: stack.delivery } : {}),
92
+ ...(stack.services.has("keys") ? { keys: true } : {}),
93
+ });
94
+ }
95
+ catch (error) {
96
+ await database.end({ timeout: 5 });
97
+ if (error instanceof HostListenError) {
98
+ logger.error("listen_failed", { message: error.message, exitCode: error.exitCode });
99
+ process.exitCode = error.exitCode;
100
+ return;
101
+ }
102
+ throw error;
103
+ }
104
+ logger.info("gates_ready", { url: server.url });
105
+ let stopping = false;
106
+ for (const signal of ["SIGINT", "SIGTERM"])
107
+ process.on(signal, () => {
108
+ if (stopping)
109
+ return;
110
+ stopping = true;
111
+ void server
112
+ .close()
113
+ .then(() => database.end({ timeout: 5 }))
114
+ .finally(() => process.exit(0));
115
+ });
116
+ }
61
117
  export async function main() {
62
118
  const stack = parseStackConfig(process.env, process.argv.slice(2));
119
+ if (stack.services.has("gates") || stack.services.has("keys"))
120
+ return runGates(stack);
63
121
  const hostRoot = resolveHostRoot();
64
122
  const paths = hostPaths(hostRoot);
65
123
  mkdirSync(paths.home, { recursive: true });
66
124
  mkdirSync(paths.tmp, { recursive: true });
67
- mkdirSync(paths.tenants, { recursive: true });
68
125
  if (!existsSync(paths.config)) {
69
126
  throw new Error(`Missing host.json at ${paths.config}; run \`nylorun start\` first`);
70
127
  }
71
128
  if (!existsSync(paths.credentials)) {
72
129
  throw new Error(`Missing host-credentials.json at ${paths.credentials}; run \`nylorun start\` first`);
73
130
  }
74
- // Tenants are Postgres schemas. Only a Host outside a container (a local
131
+ // The Tenant is a Postgres database. Only a Host outside a container (a local
75
132
  // development Host) may run without S2, on in-process streams.
76
133
  if (!stack.endpoints.databaseUrl) {
77
134
  throw new Error("NYLORUN_DATABASE_URL is required: the Postgres URL of the Session Store (`nylorun start` sets it)");
@@ -82,13 +139,20 @@ export async function main() {
82
139
  const config = loadJson(paths.config);
83
140
  const credentials = loadJson(paths.credentials);
84
141
  const logger = createHostLogger();
142
+ if (stack.deprecatedRole)
143
+ logger.warn("deprecated_flag", {
144
+ flag: "--role",
145
+ use: `--service ${describeServices(stack.services)}`,
146
+ });
85
147
  logger.info("host_stack_config", {
86
- role: stack.role,
148
+ services: [...stack.services],
149
+ ...(stack.packing ? { packing: stack.packing } : {}),
150
+ ...(stack.services.has("loop")
151
+ ? { modelGate: stack.modelGate ? stack.modelGate.url : "in-process" }
152
+ : {}),
87
153
  mode: stack.listen ? "container" : "local",
88
154
  endpoints: describeEndpoints(stack.endpoints),
89
155
  });
90
- // Tenants from the SQLite Runtime are not migrated: move them out of the way.
91
- trashSqliteTenants(hostRoot, logger);
92
156
  const infra = createInfra(stack, { logger });
93
157
  const database = infra.database;
94
158
  if (!database)
@@ -103,68 +167,84 @@ export async function main() {
103
167
  baseline,
104
168
  ...(stack.delivery ? { delivery: stack.delivery } : {}),
105
169
  });
106
- // One Durable Session Execution for every Tenant this process opens: Restate
107
- // when its endpoints are set, else the in-process memory execution. An
108
- // invocation for a Tenant that is not open here opens it on demand.
170
+ // The process's Durable Session Execution: Restate when its endpoints are set, else the
171
+ // in-process memory execution. An invocation that arrives while the Tenant opens waits for it.
109
172
  const hostExecution = createHostExecution({
110
173
  execution: infra.execution ?? createExecution(stack),
111
- role: stack.role,
174
+ services: stack.services,
112
175
  resolve: (tenantId) => module.worker(tenantId),
113
176
  logger,
114
177
  });
115
178
  // Durable Streams: S2 when configured (always in container mode). Without
116
- // them (a local Host only) each Tenant keeps in-process streams, whose history
179
+ // them (a local Host only) the Tenant keeps in-process streams, whose history
117
180
  // does not survive a restart.
118
181
  const streams = infra.streams;
119
- let relayLag;
120
- // The record of session events is shared by every Tenant; it exists before any opens.
121
- await migrateStreamsSchema(database);
122
- // With S2, the stream relay feeds every Tenant's streams from the record. Every api or all
123
- // process runs one; the replication slot lets exactly one be active. Without S2, each
124
- // Tenant relays its own commits to its in-process streams.
182
+ // With S2, the stream relay feeds the Tenant's streams from the record. Every process
183
+ // running core runs one, once the Tenant is open (the relay reads its id); the replication
184
+ // slot lets exactly one be active. Without S2, the Tenant relays its own commits.
125
185
  let relay;
126
- if (streams && stack.role !== "worker") {
186
+ let relayLag;
187
+ if (streams && stack.services.has("core"))
127
188
  await assertLogicalReplication(database);
189
+ const startRelay = (tenantId) => {
190
+ if (!streams || !stack.services.has("core") || relay)
191
+ return;
128
192
  const source = createPgoutputSource({
129
193
  connectionString: stack.endpoints.databaseUrl,
194
+ tenantId,
130
195
  log: (message, fields) => logger.info(message, fields),
131
196
  });
132
197
  relay = createStreamRelay({
133
198
  source,
134
- record: createPostgresRecordReader(database),
199
+ record: createPostgresRecordReader(database, { tenantId }),
135
200
  streams,
136
201
  log: (message, fields) => logger.info(message, fields),
137
202
  });
138
203
  const status = relay.status;
139
204
  relayLag = async () => ({ ...status(), ...(await lagOf(source)) });
140
- }
141
- const store = createPostgresTenantStore({
142
- hostRoot,
143
- sql: database,
144
- configFor,
145
- logger,
146
- openRuntime: (tenantConfig, opened) => openTenantRuntime(tenantConfig, {
147
- execution: hostExecution.tenantExecution,
148
- ...(streams ? { streams, hostRelay: true } : {}),
149
- ...opened,
150
- }),
151
- });
205
+ relay.start();
206
+ };
207
+ // With the gates service the Tenant's vault-backed model calls, remote MCP calls and Action
208
+ // deliveries cross it, and this process never reads a model or MCP credential.
209
+ const modelGate = stack.modelGate
210
+ ? httpModelGate({ url: stack.modelGate.url, token: stack.modelGate.token })
211
+ : undefined;
212
+ const toolGate = stack.modelGate
213
+ ? httpToolGate({ url: stack.modelGate.url, token: stack.modelGate.token })
214
+ : undefined;
215
+ // With the keys service, vault writes and token signing cross it, and this process never
216
+ // reads the vault key (F4.2).
217
+ const keys = stack.keys ? httpKeys({ url: stack.keys.url, token: stack.keys.token }) : undefined;
218
+ const tenantSettings = stack.tenant ?? { name: "default", derivedPrincipals: ["project"] };
152
219
  const module = createTenantModule({
153
- store,
220
+ open: createPostgresTenantOpener({
221
+ hostRoot,
222
+ sql: database,
223
+ create: {
224
+ ...(tenantSettings.id ? { tenantId: tenantSettings.id } : {}),
225
+ name: tenantSettings.name,
226
+ principals: hostPrincipals({
227
+ adminKey: credentials.adminKey,
228
+ derived: tenantSettings.derivedPrincipals,
229
+ }),
230
+ },
231
+ configFor,
232
+ logger,
233
+ openRuntime: (tenantConfig, opened) => openTenantRuntime(tenantConfig, {
234
+ execution: hostExecution.tenantExecution,
235
+ ...(modelGate ? { modelGate } : {}),
236
+ ...(toolGate ? { toolGate } : {}),
237
+ ...(keys ? { keys } : {}),
238
+ ...(streams ? { streams, hostRelay: true } : {}),
239
+ ...opened,
240
+ }),
241
+ }),
154
242
  logger,
155
- ...(relayLag ? { relayStatus: relayLag } : {}),
156
- // A new Tenant's basin is created with it (opening it repairs a failure);
157
- // a deleted Tenant's sweep stops re-arming and its basin goes.
158
- ...(streams
159
- ? { onCreated: (tenantId) => createTenantStreams(streams, tenantId) }
160
- : {}),
161
- onDeleted: async (tenantId) => {
162
- const failed = (await Promise.allSettled([
163
- hostExecution.disarm(tenantId),
164
- ...(streams ? [deleteTenantStreams(streams, tenantId)] : []),
165
- ])).flatMap((result) => (result.status === "rejected" ? [result.reason] : []));
166
- if (failed.length > 0)
167
- throw new AggregateError(failed, "Deleted Tenant cleanup failed");
243
+ onOpen: (handle) => startRelay(handle.envelope.id),
244
+ relayStatus: async () => {
245
+ if (!relayLag)
246
+ throw new Error("no relay");
247
+ return relayLag();
168
248
  },
169
249
  });
170
250
  const options = {
@@ -188,7 +268,7 @@ export async function main() {
188
268
  : {}),
189
269
  ...(infra.readiness ? { readiness: infra.readiness } : {}),
190
270
  // SIGTERM and POST /v1/admin/host/shutdown both close the Host this way:
191
- // stop the Worker, close the Tenants, then end the infrastructure clients.
271
+ // stop the Worker and the relay, close the Tenant, then end the infrastructure clients.
192
272
  shutdown: {
193
273
  beforeTenants: async () => {
194
274
  await hostExecution.stop();
@@ -198,21 +278,24 @@ export async function main() {
198
278
  },
199
279
  };
200
280
  const host = createHost(options);
201
- // Startup order. The Worker starts first: opening a Tenant arms its sweep
281
+ // Startup order. The Worker starts first: opening the Tenant arms its sweep
202
282
  // through Restate's ingress, which answers 404 until a Worker has registered
203
- // the services, so no Tenant may open before `start` (the listener opens
204
- // Tenants on demand). An api-role process serves no Worker endpoint, so it
205
- // can open Tenants only once some Worker process has registered. Container
206
- // mode runs one `--role all` process, which registers here. Then the
207
- // listener starts (and marks discovery done), and every listed Tenant's
208
- // sweep is re-armed, which recovers wakes lost with Restate's state (§14.8).
283
+ // the services, so the Tenant may not open before `start`. A core-only
284
+ // process serves no Worker endpoint, so it can open the Tenant only once some
285
+ // Worker process has registered (until then the open is retried). Container
286
+ // mode runs one `--service core,loop` process, which registers here. Then the
287
+ // listener starts and opens the Tenant (migrating its database, creating it on
288
+ // first start); opening arms its sweep, which recovers wakes lost with
289
+ // Restate's state (§14.8), and starts the stream relay. A Tenant that cannot
290
+ // be opened leaves the Host listening but not ready, with the cause in
291
+ // `/v1/admin/status`.
209
292
  try {
210
293
  await hostExecution.start();
211
- relay?.start();
212
294
  await host.listen();
213
295
  }
214
296
  catch (error) {
215
297
  await hostExecution.stop().catch(() => undefined);
298
+ await module.close().catch(() => undefined);
216
299
  await relay?.stop().catch(() => undefined);
217
300
  await infra.close();
218
301
  if (error instanceof HostListenError) {
@@ -225,16 +308,13 @@ export async function main() {
225
308
  }
226
309
  throw error;
227
310
  }
228
- const tenants = await module.list();
229
- await hostExecution
230
- .armAll(tenants.filter((t) => t.state === "open").map((t) => t.id))
231
- .catch((error) => logger.warn("tenant_sweeps_not_armed", {
232
- message: error instanceof Error ? error.message : String(error),
233
- }));
311
+ const tenant = module.tenant();
234
312
  logger.info("host_ready", {
235
313
  url: host.url,
236
314
  hostId: config.hostId,
237
- tenants: tenants.length,
315
+ tenantId: tenant.id,
316
+ tenant: tenant.state,
317
+ ...(tenant.cause ? { cause: tenant.cause.code } : {}),
238
318
  });
239
319
  void host.closed.then(() => process.exit(0));
240
320
  for (const signal of ["SIGINT", "SIGTERM"]) {
@@ -1,21 +1,20 @@
1
- /**
2
- * Stack configuration for the Runtime process, parsed from an environment
3
- * snapshot and argv. Pure: `host/main.ts` passes the process environment and
4
- * arguments; nothing here reads ambient state.
5
- *
6
- * Two listen modes:
7
- * - **local** (no `NYLORUN_LISTEN_*` / `NYLORUN_ALLOWED_HOSTS`): the Host binds
8
- * what `host.json` names, loopback only unless `allowNonLoopback`.
9
- * - **container**: the Host binds `NYLORUN_LISTEN_HOST:NYLORUN_LISTEN_PORT`
10
- * (default `0.0.0.0:4000`) and accepts only the `Host` headers in
11
- * `NYLORUN_ALLOWED_HOSTS`, plus the loopback forms of the listen port for
12
- * probes from inside the container.
13
- *
14
- * The Postgres, Restate and S2 endpoints are parsed and validated here;
15
- * `infra/*` builds the clients from them.
16
- */
1
+ /** A Runtime service this release has. */
2
+ export type RuntimeService = "core" | "loop" | "gates" | "keys";
3
+ export type RuntimeServices = ReadonlySet<RuntimeService>;
4
+ export declare const RUNTIME_SERVICES: readonly RuntimeService[];
5
+ /** Where the gates service listens by default. */
6
+ export declare const DEFAULT_GATES_LISTEN_PORT = 4100;
7
+ /** What a process runs without `--service`: core and loop, as `--role all` did. */
8
+ export declare const DEFAULT_SERVICES: RuntimeServices;
9
+ /** The deprecated `--role` values, and the services each stands for. */
17
10
  export type RuntimeRole = "api" | "worker" | "all";
18
- export declare const RUNTIME_ROLES: readonly RuntimeRole[];
11
+ /** `services` as the `--service` value that selects them, e.g. `core,loop`. */
12
+ export declare function describeServices(services: RuntimeServices): string;
13
+ export interface ServiceSelection {
14
+ services: RuntimeServices;
15
+ /** Set when the process was started with the deprecated `--role`. */
16
+ deprecatedRole?: RuntimeRole;
17
+ }
19
18
  export declare const DEFAULT_CONTAINER_LISTEN_HOST = "0.0.0.0";
20
19
  export declare const DEFAULT_CONTAINER_LISTEN_PORT = 4000;
21
20
  export interface ContainerListen {
@@ -43,8 +42,43 @@ export interface StackEndpoints {
43
42
  */
44
43
  restateIdentityKeys?: string[];
45
44
  }
45
+ /** The gates service's listener and the token callers must present (`NYLORUN_GATES_*`). */
46
+ export interface GatesConfig {
47
+ listen: ContainerListen;
48
+ /** `NYLORUN_GATES_TOKEN`: the bearer the loop presents; at least 32 bytes as hex. */
49
+ token: string;
50
+ }
51
+ /** Where the loop reaches the gates service (`NYLORUN_GATES_URL`, `NYLORUN_GATES_TOKEN`). */
52
+ export interface ModelGateEndpoint {
53
+ url: string;
54
+ token: string;
55
+ }
46
56
  export interface StackConfig {
47
- role: RuntimeRole;
57
+ /** The Runtime services this process runs. */
58
+ services: RuntimeServices;
59
+ /** Present when the process runs gates. */
60
+ gates?: GatesConfig;
61
+ /**
62
+ * Present when the process runs core or loop and `NYLORUN_GATES_URL` is set: its model calls,
63
+ * remote MCP calls and Action deliveries cross the gates service. Required for loop in
64
+ * container mode; outside a container (a development Host, tests) the loop may make them in
65
+ * its own process.
66
+ */
67
+ modelGate?: ModelGateEndpoint;
68
+ /**
69
+ * Present when the process runs core or loop and `NYLORUN_KEYS_URL`, or else
70
+ * `NYLORUN_GATES_URL`, is set (F4.2): its vault writes and token signing cross the gateway's
71
+ * keys service, with `NYLORUN_GATES_TOKEN`, and it never reads the vault key. A loop in a
72
+ * container always has it, since it requires `NYLORUN_GATES_URL`.
73
+ */
74
+ keys?: ModelGateEndpoint;
75
+ /**
76
+ * How the stack packs services into containers (`NYLORUN_PACKING`), for the startup log:
77
+ * `combined` (the local stack: runtime and gateway) or `split` (one container per service).
78
+ */
79
+ packing?: "combined" | "split";
80
+ /** Set when the process was started with the deprecated `--role` (logged at startup). */
81
+ deprecatedRole?: RuntimeRole;
48
82
  /** Present in container mode; absent means bind what host.json names. */
49
83
  listen?: ContainerListen;
50
84
  endpoints: StackEndpoints;
@@ -74,12 +108,30 @@ export interface StackConfig {
74
108
  privateAddresses?: "allow" | "refuse";
75
109
  allowHttp?: boolean;
76
110
  };
111
+ /** Present when the process runs core or loop: who its Tenant is on first start. */
112
+ tenant?: TenantSettings;
113
+ }
114
+ /**
115
+ * The Tenant a Host creates when its database holds none (tenancy.md §4). Later starts open the
116
+ * Tenant the database holds; only principals missing from it are added.
117
+ */
118
+ export interface TenantSettings {
119
+ /** `NYLORUN_TENANT_ID`: the new Tenant's id. Default: a new id. */
120
+ id?: string;
121
+ /** `NYLORUN_TENANT_NAME`. Default `default`. */
122
+ name: string;
123
+ /**
124
+ * `NYLORUN_DERIVED_PRINCIPALS`: comma-separated application principals whose keys the admin
125
+ * key derives (`deriveTenantKey`). Default `project`.
126
+ */
127
+ derivedPrincipals: readonly string[];
77
128
  }
78
129
  export declare class StackConfigError extends Error {
79
130
  constructor(message: string);
80
131
  }
81
132
  type EnvSnapshot = Readonly<Record<string, string | undefined>>;
82
- export declare function parseRole(argv: readonly string[]): RuntimeRole;
133
+ /** Parses `--service a,b` (or the deprecated `--role`); throws `StackConfigError`. */
134
+ export declare function parseServices(argv: readonly string[]): ServiceSelection;
83
135
  /**
84
136
  * Normalize one `Host` header allowlist entry to lowercase `name:port`.
85
137
  * IPv6 literals must be bracketed (`[::1]:4000`).