@nylorun/runtime 0.13.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 (309) hide show
  1. package/CHANGELOG.md +434 -0
  2. package/README.md +137 -41
  3. package/dist/adapters/execution/restate.d.ts +7 -2
  4. package/dist/adapters/execution/restate.js +40 -10
  5. package/dist/adapters/replication/pgoutput.d.ts +21 -0
  6. package/dist/adapters/replication/pgoutput.js +170 -0
  7. package/dist/adapters/streams/s2.js +37 -20
  8. package/dist/admin-openapi.json +570 -0
  9. package/dist/api/a2a/card.d.ts +15 -0
  10. package/dist/api/a2a/card.js +24 -0
  11. package/dist/api/a2a/endpoint.d.ts +8 -0
  12. package/dist/api/a2a/endpoint.js +81 -0
  13. package/dist/api/a2a/protocol.d.ts +144 -0
  14. package/dist/api/a2a/protocol.js +188 -0
  15. package/dist/api/a2a/routes.d.ts +19 -0
  16. package/dist/api/a2a/routes.js +289 -0
  17. package/dist/api/a2a/tasks.d.ts +50 -0
  18. package/dist/api/a2a/tasks.js +186 -0
  19. package/dist/api/ag-ui/endpoint.d.ts +8 -0
  20. package/dist/api/ag-ui/endpoint.js +126 -0
  21. package/dist/api/ag-ui/history.d.ts +7 -0
  22. package/dist/api/ag-ui/history.js +60 -0
  23. package/dist/api/ag-ui/routes.d.ts +32 -0
  24. package/dist/api/ag-ui/routes.js +305 -0
  25. package/dist/api/ag-ui/session-id.d.ts +1 -0
  26. package/dist/api/ag-ui/session-id.js +12 -0
  27. package/dist/api/ag-ui/sse.d.ts +9 -0
  28. package/dist/api/ag-ui/sse.js +7 -0
  29. package/dist/api/ag-ui/translate.d.ts +38 -0
  30. package/dist/api/ag-ui/translate.js +222 -0
  31. package/dist/api/components.d.ts +8065 -0
  32. package/dist/api/components.js +82 -0
  33. package/dist/api/gate/keys.d.ts +14 -0
  34. package/dist/api/gate/keys.js +59 -0
  35. package/dist/api/gate/routes.d.ts +34 -0
  36. package/dist/api/gate/routes.js +235 -0
  37. package/dist/api/http/app.d.ts +18 -0
  38. package/dist/api/http/app.js +58 -0
  39. package/dist/api/http/body.d.ts +4 -0
  40. package/dist/api/http/body.js +37 -0
  41. package/dist/api/http/define.d.ts +39 -0
  42. package/dist/api/http/define.js +122 -0
  43. package/dist/api/http/respond.d.ts +9 -0
  44. package/dist/api/http/respond.js +44 -0
  45. package/dist/api/http/routes/access.d.ts +24 -0
  46. package/dist/api/http/routes/access.js +266 -0
  47. package/dist/api/http/routes/actions.d.ts +8 -0
  48. package/dist/api/http/routes/actions.js +76 -0
  49. package/dist/api/http/routes/endpoints.d.ts +7 -0
  50. package/dist/api/http/routes/endpoints.js +100 -0
  51. package/dist/api/http/routes/sessions.d.ts +3 -0
  52. package/dist/api/http/routes/sessions.js +205 -0
  53. package/dist/api/http/routes/tenant.d.ts +11 -0
  54. package/dist/api/http/routes/tenant.js +244 -0
  55. package/dist/api/http/routes/vaults.d.ts +8 -0
  56. package/dist/api/http/routes/vaults.js +167 -0
  57. package/dist/api/openapi.d.ts +19 -0
  58. package/dist/api/openapi.js +137 -0
  59. package/dist/api/route.d.ts +12 -0
  60. package/dist/api/route.js +4 -0
  61. package/dist/core/bearer.d.ts +8 -0
  62. package/dist/core/bearer.js +11 -0
  63. package/dist/core/flow-host.d.ts +7 -15
  64. package/dist/core/flow-host.js +16 -41
  65. package/dist/core/sandbox-routes.d.ts +12 -7
  66. package/dist/core/sandbox-routes.js +24 -25
  67. package/dist/execution/memory.d.ts +8 -5
  68. package/dist/execution/memory.js +33 -13
  69. package/dist/execution/types.d.ts +38 -6
  70. package/dist/execution/types.js +12 -3
  71. package/dist/gates/contract.d.ts +51 -0
  72. package/dist/gates/contract.js +37 -0
  73. package/dist/gates/handler.d.ts +21 -0
  74. package/dist/gates/handler.js +55 -0
  75. package/dist/gates/http-client.d.ts +12 -0
  76. package/dist/gates/http-client.js +160 -0
  77. package/dist/gates/in-process.d.ts +32 -0
  78. package/dist/gates/in-process.js +42 -0
  79. package/dist/gates/inflight.d.ts +39 -0
  80. package/dist/gates/inflight.js +70 -0
  81. package/dist/gates/mcp-handler.d.ts +35 -0
  82. package/dist/gates/mcp-handler.js +142 -0
  83. package/dist/gates/meter.d.ts +24 -0
  84. package/dist/gates/meter.js +165 -0
  85. package/dist/gates/model-gate.d.ts +45 -0
  86. package/dist/gates/model-gate.js +1 -0
  87. package/dist/gates/tenant-vaults.d.ts +64 -0
  88. package/dist/gates/tenant-vaults.js +94 -0
  89. package/dist/gates/tool-calls.d.ts +51 -0
  90. package/dist/gates/tool-calls.js +64 -0
  91. package/dist/gates/tool-client.d.ts +12 -0
  92. package/dist/gates/tool-client.js +201 -0
  93. package/dist/gates/tool-contract.d.ts +116 -0
  94. package/dist/gates/tool-contract.js +64 -0
  95. package/dist/gates/tool-gate.d.ts +45 -0
  96. package/dist/gates/tool-gate.js +10 -0
  97. package/dist/host/admin-api.d.ts +23 -0
  98. package/dist/host/admin-api.js +74 -0
  99. package/dist/host/app.d.ts +34 -0
  100. package/dist/host/app.js +181 -0
  101. package/dist/host/config-for.d.ts +3 -1
  102. package/dist/host/config-for.js +3 -2
  103. package/dist/host/config.d.ts +11 -0
  104. package/dist/host/cors.d.ts +27 -0
  105. package/dist/host/cors.js +64 -0
  106. package/dist/host/create-host.d.ts +36 -8
  107. package/dist/host/create-host.js +161 -330
  108. package/dist/host/execution.d.ts +20 -18
  109. package/dist/host/execution.js +2 -2
  110. package/dist/host/gates.d.ts +58 -0
  111. package/dist/host/gates.js +127 -0
  112. package/dist/host/http.d.ts +20 -4
  113. package/dist/host/http.js +81 -23
  114. package/dist/host/index.d.ts +1 -1
  115. package/dist/host/main.js +189 -58
  116. package/dist/host/stack-config.d.ts +91 -19
  117. package/dist/host/stack-config.js +246 -17
  118. package/dist/infra/database.d.ts +2 -2
  119. package/dist/infra/execution.d.ts +5 -5
  120. package/dist/infra/execution.js +8 -6
  121. package/dist/infra/index.d.ts +1 -1
  122. package/dist/infra/workers.d.ts +17 -17
  123. package/dist/infra/workers.js +9 -9
  124. package/dist/keys/client.d.ts +9 -0
  125. package/dist/keys/client.js +70 -0
  126. package/dist/keys/contract.d.ts +24 -0
  127. package/dist/keys/contract.js +12 -0
  128. package/dist/keys/keys.d.ts +41 -0
  129. package/dist/keys/keys.js +50 -0
  130. package/dist/mcp/connect.d.ts +43 -3
  131. package/dist/mcp/connect.js +12 -5
  132. package/dist/mcp/pool.d.ts +39 -3
  133. package/dist/mcp/pool.js +74 -11
  134. package/dist/model/catalog.d.ts +2 -10
  135. package/dist/model/classify.d.ts +13 -0
  136. package/dist/model/classify.js +61 -0
  137. package/dist/model/models.d.ts +10 -1
  138. package/dist/model/models.js +14 -6
  139. package/dist/model/pi-model.d.ts +17 -2
  140. package/dist/model/pi-model.js +366 -222
  141. package/dist/openapi.json +25889 -0
  142. package/dist/record/append.d.ts +51 -0
  143. package/dist/record/append.js +20 -0
  144. package/dist/record/envelope.d.ts +19 -0
  145. package/dist/record/envelope.js +49 -0
  146. package/dist/record/index.d.ts +11 -0
  147. package/dist/record/index.js +11 -0
  148. package/dist/sandbox/manager.d.ts +13 -1
  149. package/dist/sandbox/manager.js +8 -11
  150. package/dist/sandbox/resolve.d.ts +33 -0
  151. package/dist/sandbox/resolve.js +81 -0
  152. package/dist/sandbox/session-sandbox.d.ts +11 -0
  153. package/dist/sandbox/session-sandbox.js +51 -0
  154. package/dist/sandbox/share.d.ts +14 -2
  155. package/dist/sandbox/share.js +33 -3
  156. package/dist/sandbox/tenant-config.d.ts +37 -0
  157. package/dist/sandbox/tenant-config.js +51 -0
  158. package/dist/sandbox/tools.d.ts +7 -9
  159. package/dist/sandbox/tools.js +11 -3
  160. package/dist/sandbox/types.d.ts +8 -2
  161. package/dist/store/postgres/connect.d.ts +25 -9
  162. package/dist/store/postgres/connect.js +27 -10
  163. package/dist/store/postgres/db.d.ts +16 -0
  164. package/dist/store/postgres/db.js +28 -0
  165. package/dist/store/postgres/drizzle/0000_foundation.sql +22 -0
  166. package/dist/store/postgres/drizzle/0001_baseline.sql +255 -0
  167. package/dist/store/postgres/drizzle/0002_stream_relay.sql +4 -0
  168. package/dist/store/postgres/drizzle/0003_tool_crossings.sql +9 -0
  169. package/dist/store/postgres/drizzle/meta/_journal.json +34 -0
  170. package/dist/store/postgres/migrate.d.ts +68 -0
  171. package/dist/store/postgres/migrate.js +183 -0
  172. package/dist/store/postgres/record-writer.d.ts +4 -0
  173. package/dist/store/postgres/record-writer.js +34 -0
  174. package/dist/store/postgres/record.d.ts +5 -0
  175. package/dist/store/postgres/record.js +60 -0
  176. package/dist/store/postgres/schema.d.ts +3096 -0
  177. package/dist/store/postgres/schema.js +363 -0
  178. package/dist/store/postgres/store.d.ts +2 -4
  179. package/dist/store/postgres/store.js +590 -434
  180. package/dist/store/postgres/tenant.d.ts +56 -0
  181. package/dist/store/postgres/tenant.js +154 -0
  182. package/dist/store/types.d.ts +183 -132
  183. package/dist/store/types.js +0 -1
  184. package/dist/streams/basin.d.ts +18 -1
  185. package/dist/streams/basin.js +34 -2
  186. package/dist/streams/control.d.ts +18 -0
  187. package/dist/streams/control.js +33 -0
  188. package/dist/streams/memory.d.ts +3 -1
  189. package/dist/streams/memory.js +14 -8
  190. package/dist/streams/relay/core.d.ts +35 -0
  191. package/dist/streams/relay/core.js +366 -0
  192. package/dist/streams/relay/types.d.ts +78 -0
  193. package/dist/streams/relay/types.js +8 -0
  194. package/dist/streams/types.d.ts +71 -36
  195. package/dist/streams/types.js +18 -53
  196. package/dist/tenant/access-policy.d.ts +30 -0
  197. package/dist/tenant/access-policy.js +52 -0
  198. package/dist/tenant/actions.d.ts +10 -149
  199. package/dist/tenant/actions.js +5 -167
  200. package/dist/tenant/advance.d.ts +7 -2
  201. package/dist/tenant/advance.js +125 -14
  202. package/dist/tenant/auth.d.ts +19 -17
  203. package/dist/tenant/auth.js +90 -93
  204. package/dist/tenant/browser.d.ts +15 -0
  205. package/dist/tenant/browser.js +39 -0
  206. package/dist/tenant/cause.d.ts +25 -0
  207. package/dist/tenant/cause.js +45 -0
  208. package/dist/tenant/commands.d.ts +18 -2
  209. package/dist/tenant/commands.js +111 -115
  210. package/dist/tenant/context.d.ts +101 -21
  211. package/dist/tenant/context.js +14 -10
  212. package/dist/tenant/delivery-token.d.ts +41 -0
  213. package/dist/tenant/delivery-token.js +104 -0
  214. package/dist/tenant/delivery.d.ts +62 -0
  215. package/dist/tenant/delivery.js +481 -0
  216. package/dist/tenant/effects.d.ts +20 -1
  217. package/dist/tenant/effects.js +159 -42
  218. package/dist/tenant/endpoints.d.ts +67 -0
  219. package/dist/tenant/endpoints.js +90 -0
  220. package/dist/tenant/ephemeral.d.ts +22 -7
  221. package/dist/tenant/ephemeral.js +83 -125
  222. package/dist/tenant/history.d.ts +64 -0
  223. package/dist/tenant/history.js +120 -0
  224. package/dist/tenant/http.d.ts +8 -9
  225. package/dist/tenant/http.js +13 -27
  226. package/dist/tenant/jwt.d.ts +16 -0
  227. package/dist/tenant/jwt.js +50 -0
  228. package/dist/tenant/model-setting.d.ts +2 -2
  229. package/dist/tenant/module.d.ts +17 -28
  230. package/dist/tenant/module.js +133 -279
  231. package/dist/tenant/outbound.d.ts +44 -0
  232. package/dist/tenant/outbound.js +127 -0
  233. package/dist/tenant/paths.d.ts +11 -16
  234. package/dist/tenant/paths.js +24 -68
  235. package/dist/tenant/pool.d.ts +1 -3
  236. package/dist/tenant/pool.js +1 -19
  237. package/dist/tenant/principals.d.ts +23 -11
  238. package/dist/tenant/principals.js +61 -49
  239. package/dist/tenant/reset.d.ts +0 -4
  240. package/dist/tenant/reset.js +0 -5
  241. package/dist/tenant/runtime.d.ts +28 -20
  242. package/dist/tenant/runtime.js +95 -64
  243. package/dist/tenant/scheduler.d.ts +10 -3
  244. package/dist/tenant/scheduler.js +9 -5
  245. package/dist/tenant/session-streams.d.ts +127 -0
  246. package/dist/tenant/session-streams.js +404 -0
  247. package/dist/tenant/sessions.d.ts +28 -4
  248. package/dist/tenant/sessions.js +118 -24
  249. package/dist/tenant/signing-keys.d.ts +55 -0
  250. package/dist/tenant/signing-keys.js +172 -0
  251. package/dist/tenant/slim.d.ts +8 -0
  252. package/dist/tenant/slim.js +21 -0
  253. package/dist/tenant/status.d.ts +3 -3
  254. package/dist/tenant/status.js +20 -13
  255. package/dist/tenant/store-pg.d.ts +9 -6
  256. package/dist/tenant/store-pg.js +60 -125
  257. package/dist/tenant/streams.d.ts +62 -85
  258. package/dist/tenant/streams.js +143 -226
  259. package/dist/tenant/subject-limits.d.ts +16 -0
  260. package/dist/tenant/subject-limits.js +45 -0
  261. package/dist/tenant/sweep.d.ts +1 -10
  262. package/dist/tenant/sweep.js +29 -69
  263. package/dist/tenant/tokens.d.ts +14 -0
  264. package/dist/tenant/tokens.js +180 -0
  265. package/dist/tenant/transcript.d.ts +48 -5
  266. package/dist/tenant/transcript.js +58 -1
  267. package/dist/tenant/types.d.ts +69 -98
  268. package/dist/tenant/types.js +4 -15
  269. package/dist/tenant/worker.d.ts +3 -1
  270. package/dist/tenant/worker.js +13 -7
  271. package/dist/vault/host-model.d.ts +31 -0
  272. package/dist/vault/host-model.js +57 -0
  273. package/dist/vault/service.d.ts +44 -17
  274. package/dist/vault/service.js +126 -121
  275. package/dist/version.d.ts +1 -1
  276. package/dist/version.js +1 -1
  277. package/package.json +25 -9
  278. package/dist/core/executors.d.ts +0 -44
  279. package/dist/core/executors.js +0 -95
  280. package/dist/store/memory.d.ts +0 -57
  281. package/dist/store/memory.js +0 -645
  282. package/dist/store/postgres/migrations/001_initial.d.ts +0 -27
  283. package/dist/store/postgres/migrations/001_initial.js +0 -196
  284. package/dist/store/postgres/migrations/002_session_owner.d.ts +0 -6
  285. package/dist/store/postgres/migrations/002_session_owner.js +0 -13
  286. package/dist/store/postgres/migrations/index.d.ts +0 -49
  287. package/dist/store/postgres/migrations/index.js +0 -76
  288. package/dist/store/postgres/names.d.ts +0 -11
  289. package/dist/store/postgres/names.js +0 -42
  290. package/dist/store/postgres/tenants.d.ts +0 -90
  291. package/dist/store/postgres/tenants.js +0 -214
  292. package/dist/streams/relay.d.ts +0 -37
  293. package/dist/streams/relay.js +0 -254
  294. package/dist/tenant/envelope.d.ts +0 -7
  295. package/dist/tenant/envelope.js +0 -22
  296. package/dist/tenant/live.d.ts +0 -92
  297. package/dist/tenant/live.js +0 -290
  298. package/dist/tenant/quarantine-error.d.ts +0 -6
  299. package/dist/tenant/quarantine-error.js +0 -9
  300. package/dist/tenant/quarantine.d.ts +0 -27
  301. package/dist/tenant/quarantine.js +0 -72
  302. package/dist/tenant/routes-tenant.d.ts +0 -12
  303. package/dist/tenant/routes-tenant.js +0 -137
  304. package/dist/tenant/routes.d.ts +0 -9
  305. package/dist/tenant/routes.js +0 -151
  306. package/dist/tenant/store-memory.d.ts +0 -16
  307. package/dist/tenant/store-memory.js +0 -111
  308. /package/dist/{store → record}/cursor.d.ts +0 -0
  309. /package/dist/{store → record}/cursor.js +0 -0
@@ -1,42 +1,13 @@
1
1
  import { createServer, } from "node:http";
2
- import { createHash, timingSafeEqual } from "node:crypto";
3
- import { checkCompatibility, HOST_PROTOCOL, isTenantId, PROTOCOL_HEADER, TENANT_HEADER, } from "@nylorun/core/compatibility";
4
- import { AdminStatusSchema, CreateTenantRequestSchema, } from "@nylorun/core/contracts";
5
- import { TenantBusyError, TenantConflictError, } from "../tenant/quarantine.js";
6
- import { TenantNotFoundError, } from "../tenant/types.js";
7
- import { EXIT_NON_LOOPBACK, EXIT_PORT_IN_USE, HostListenError, isAllowedRequestHost, isJsonContentType, isLoopbackHost, readBearer, readJsonBody, redactRoutePath, requestHasBody, sendJson, sendOpaqueNotFound, sendProtocolRejected, sendRejected, } from "./http.js";
2
+ import { getRequestListener, RequestError } from "@hono/node-server";
3
+ import { HOST_PROTOCOL } from "@nylorun/core/compatibility";
4
+ import { AdminStatusSchema } from "@nylorun/core/contracts";
5
+ import { adminDocument } from "../api/openapi.js";
6
+ import { createAdminApi } from "./admin-api.js";
7
+ import { createHostApp } from "./app.js";
8
+ import { bindListener as bind, EXIT_NON_LOOPBACK, headerValue, HostListenError, isAllowedRequestHost, isLoopbackHost, pathnameIsLogged, redactRoutePath, rejectedResponse, sendRejected, } from "./http.js";
8
9
  import { RUNTIME_VERSION } from "../version.js";
9
- function hashUtf8(value) {
10
- return createHash("sha256").update(value, "utf8").digest();
11
- }
12
- /** Constant-time comparison of SHA-256 digests of the presented and stored admin keys. */
13
- export function adminKeyMatches(presented, adminKey) {
14
- if (presented === undefined)
15
- return false;
16
- const a = hashUtf8(presented);
17
- const b = hashUtf8(adminKey);
18
- return a.length === b.length && timingSafeEqual(a, b);
19
- }
20
- function headerValue(headers, name) {
21
- const raw = headers[name.toLowerCase()];
22
- if (Array.isArray(raw))
23
- return raw[0];
24
- return raw;
25
- }
26
- function parseProtocolVersion(raw) {
27
- if (raw === undefined)
28
- return undefined;
29
- const trimmed = raw.trim();
30
- if (!/^\d+$/.test(trimmed))
31
- return undefined;
32
- return Number(trimmed);
33
- }
34
- function protocolAccepted(raw) {
35
- const version = parseProtocolVersion(raw);
36
- if (version === undefined)
37
- return false;
38
- return checkCompatibility({ version, required: [] }, HOST_PROTOCOL).ok;
39
- }
10
+ export { adminKeyMatches } from "./http.js";
40
11
  export function createHost(options) {
41
12
  const { hostRoot, module, config, credentials, logger, coreVersion, } = options;
42
13
  const pid = options.pid ?? process.pid;
@@ -46,10 +17,14 @@ export function createHost(options) {
46
17
  let server;
47
18
  let url = "";
48
19
  let listenPort = bindPort;
20
+ const operator = options.operator;
21
+ let operatorServer;
22
+ let operatorPort = operator?.port ?? 0;
23
+ let adminUrl = "";
24
+ const mainRole = operator ? "public" : "combined";
49
25
  let closing = false;
50
26
  let closePromise;
51
27
  const adminStatusBody = async () => {
52
- const tenants = await module.list();
53
28
  const aggregate = await module.summarize();
54
29
  return AdminStatusSchema.parse({
55
30
  service: "nylorun-runtime",
@@ -59,7 +34,7 @@ export function createHost(options) {
59
34
  max: HOST_PROTOCOL.max,
60
35
  features: [...HOST_PROTOCOL.features],
61
36
  },
62
- tenants,
37
+ tenant: module.tenant(),
63
38
  aggregate,
64
39
  host: {
65
40
  hostId: config.hostId,
@@ -68,276 +43,76 @@ export function createHost(options) {
68
43
  },
69
44
  });
70
45
  };
71
- const requireAdmin = (request, response) => {
72
- const token = readBearer(headerValue(request.headers, "authorization"));
73
- if (!adminKeyMatches(token, credentials.adminKey)) {
74
- sendOpaqueNotFound(response);
75
- return false;
76
- }
77
- return true;
78
- };
79
- const handleAdmin = async (request, response, urlObj, segments) => {
80
- const method = request.method ?? "GET";
81
- // /v1/admin/...
82
- if (segments[2] === "tenants") {
83
- if (segments.length === 3 && method === "GET") {
84
- const tenants = await module.list();
85
- return sendJson(response, 200, tenants);
86
- }
87
- if (segments.length === 3 && method === "POST") {
88
- const body = CreateTenantRequestSchema.parse(await readJsonBody(request));
89
- try {
90
- const result = await module.create({
91
- tenantId: body.tenantId,
92
- name: body.name,
93
- principalId: body.principalId,
94
- credentialHash: body.credentialHash,
95
- idempotencyKey: body.idempotencyKey,
96
- ...(body.studioCredentialHash
97
- ? { studioCredentialHash: body.studioCredentialHash }
98
- : {}),
99
- ...(body.derivedPrincipals?.length
100
- ? { derivedPrincipals: body.derivedPrincipals }
101
- : {}),
102
- });
103
- return sendJson(response, result.created ? 201 : 200, result.envelope);
104
- }
105
- catch (error) {
106
- const code = error.code;
107
- if (error instanceof TenantConflictError ||
108
- error.name === "TenantConflictError" ||
109
- code === "conflict" ||
110
- code === "tenant_conflict") {
111
- return sendRejected(response, 409, "tenant_conflict", error instanceof Error ? error.message : "Tenant conflict");
112
- }
113
- throw error;
114
- }
115
- }
116
- if (segments.length === 4 && segments[3]) {
117
- const id = segments[3];
118
- if (method === "GET") {
119
- const status = await module.status(id);
120
- if (!status)
121
- return sendOpaqueNotFound(response);
122
- return sendJson(response, 200, status);
123
- }
124
- if (method === "DELETE") {
125
- const activeWork = urlObj.searchParams.get("activeWork") ?? "refuse";
126
- if (activeWork !== "refuse" &&
127
- activeWork !== "drain" &&
128
- activeWork !== "cancel") {
129
- return sendJson(response, 400, {
130
- status: "rejected",
131
- code: "invalid_request",
132
- message: "activeWork must be refuse, drain, or cancel",
133
- });
134
- }
135
- try {
136
- await module.delete(id, activeWork);
137
- response.writeHead(204);
138
- response.end();
139
- return;
140
- }
141
- catch (error) {
142
- if (error instanceof TenantNotFoundError)
143
- return sendOpaqueNotFound(response);
144
- const code = error.code;
145
- if (error instanceof TenantBusyError ||
146
- error.name === "TenantBusyError" ||
147
- code === "active_work" ||
148
- code === "conflict") {
149
- return sendRejected(response, 409, "active_work", error instanceof Error ? error.message : "Active work");
150
- }
151
- throw error;
152
- }
153
- }
154
- }
155
- }
156
- // D12: /v1/admin/status and /v1/admin/host share one handler.
157
- if ((segments[2] === "host" || segments[2] === "status") &&
158
- segments.length === 3 &&
159
- method === "GET") {
160
- return sendJson(response, 200, await adminStatusBody());
161
- }
162
- if (segments[2] === "host" &&
163
- segments.length === 4 &&
164
- segments[3] === "shutdown" &&
165
- method === "POST") {
166
- sendJson(response, 200, { status: "shutting_down" });
167
- void close();
168
- return;
169
- }
170
- return sendRejected(response, 404, "not_found", "Route not found");
171
- };
172
- const handle = async (request, response) => {
173
- const started = Date.now();
174
- let tenantId;
175
- let statusCode = 500;
176
- try {
177
- const urlObj = new URL(request.url ?? "/", "http://runtime.local");
178
- const pathname = urlObj.pathname;
179
- // D§11: Host, Origin, then Content-Type — before any other processing.
180
- const hostHeader = headerValue(request.headers, "host");
181
- if (!isAllowedRequestHost(hostHeader, {
182
- port: listenPort,
183
- host: config.host,
184
- allowNonLoopback: config.allowNonLoopback,
185
- ...(containerListen
186
- ? { allowedHosts: containerListen.allowedHosts }
187
- : {}),
188
- })) {
189
- sendRejected(response, 421, "host_rejected", "Host header is not an allowed loopback or configured address");
190
- statusCode = 421;
191
- return;
192
- }
193
- if (headerValue(request.headers, "origin") !== undefined) {
194
- sendRejected(response, 403, "origin_rejected", "Browser Origin headers are not accepted");
195
- statusCode = 403;
196
- return;
197
- }
198
- if (requestHasBody(request) &&
199
- !isJsonContentType(headerValue(request.headers, "content-type"))) {
200
- sendRejected(response, 415, "unsupported_media_type", "Request bodies must use application/json");
201
- statusCode = 415;
202
- return;
203
- }
204
- if (pathname === "/health") {
205
- sendJson(response, 200, {
206
- status: "ok",
207
- service: "nylorun-runtime",
208
- version: RUNTIME_VERSION,
209
- protocol: {
210
- min: HOST_PROTOCOL.min,
211
- max: HOST_PROTOCOL.max,
212
- features: [...HOST_PROTOCOL.features],
213
- },
214
- coreVersion,
215
- hostId: config.hostId,
216
- pid,
217
- });
218
- statusCode = 200;
219
- return;
220
- }
221
- if (pathname === "/ready") {
222
- const listener = Boolean(server?.listening);
223
- const discovery = module.started;
224
- const infra = await options.readiness?.();
225
- const ready = listener && discovery && !closing && (infra?.ok ?? true);
226
- sendJson(response, ready ? 200 : 503, {
227
- status: ready ? "ready" : "not_ready",
228
- service: "nylorun-runtime",
229
- checks: { listener, discovery, ...infra?.checks },
230
- });
231
- statusCode = ready ? 200 : 503;
232
- return;
233
- }
234
- const segments = pathname.split("/").filter(Boolean);
235
- const isAdmin = segments[0] === "v1" && segments[1] === "admin";
236
- if (isAdmin) {
237
- const protocolHeader = headerValue(request.headers, PROTOCOL_HEADER);
238
- if (!protocolAccepted(protocolHeader)) {
239
- sendProtocolRejected(response);
240
- statusCode = 426;
241
- return;
242
- }
243
- if (!requireAdmin(request, response)) {
244
- statusCode = 404;
245
- return;
246
- }
247
- await handleAdmin(request, response, urlObj, segments);
248
- statusCode = response.statusCode || 200;
249
- return;
250
- }
251
- // Tenant-scoped routes: header pattern → protocol → resolve → Tenant handle.
252
- const tenantHeader = headerValue(request.headers, TENANT_HEADER);
253
- if (tenantHeader === undefined || tenantHeader.trim() === "") {
254
- sendJson(response, 400, {
255
- status: "rejected",
256
- code: "invalid_request",
257
- message: `${TENANT_HEADER} header is required`,
258
- });
259
- statusCode = 400;
260
- return;
261
- }
262
- if (!isTenantId(tenantHeader)) {
263
- sendJson(response, 400, {
264
- status: "rejected",
265
- code: "invalid_request",
266
- message: `${TENANT_HEADER} header is malformed`,
267
- });
268
- statusCode = 400;
269
- return;
270
- }
271
- tenantId = tenantHeader;
272
- const protocolHeader = headerValue(request.headers, PROTOCOL_HEADER);
273
- if (!protocolAccepted(protocolHeader)) {
274
- sendProtocolRejected(response);
275
- statusCode = 426;
276
- return;
277
- }
278
- const resolution = await module.resolve(tenantId);
279
- if (resolution.kind !== "open") {
280
- if (resolution.kind === "quarantined") {
281
- logger.warn("tenant_quarantined", {
282
- tenantId,
283
- code: resolution.quarantine.code,
284
- repair: resolution.quarantine.repair,
285
- });
286
- }
287
- sendOpaqueNotFound(response);
288
- statusCode = 404;
289
- return;
290
- }
291
- await resolution.handle.handle(request, response, urlObj);
292
- statusCode = response.statusCode || 200;
293
- }
294
- catch (error) {
295
- const status = error.status;
296
- if (typeof status === "number" && status >= 400 && status < 600) {
297
- sendJson(response, status, {
298
- status: "rejected",
299
- code: status === 400 ? "invalid_request" : "request_rejected",
300
- message: error instanceof Error ? error.message : "Request rejected",
301
- });
302
- statusCode = status;
303
- }
304
- else if (error &&
305
- typeof error === "object" &&
306
- "name" in error &&
307
- error.name === "ZodError") {
308
- sendJson(response, 400, {
309
- status: "rejected",
310
- code: "invalid_request",
311
- message: "Invalid request body",
312
- });
313
- statusCode = 400;
314
- }
315
- else {
316
- logger.error("request_failed", {
317
- error: error instanceof Error ? error.message : String(error),
318
- });
319
- if (!response.headersSent) {
320
- sendJson(response, 500, {
321
- status: "rejected",
322
- code: "internal_error",
323
- message: "Internal error",
324
- });
325
- }
326
- statusCode = 500;
327
- }
328
- }
329
- finally {
330
- if (pathnameIsLogged(request.url)) {
46
+ const adminApi = createAdminApi({
47
+ status: adminStatusBody,
48
+ shutdown: () => void close(),
49
+ document: adminDocument,
50
+ });
51
+ const app = createHostApp({
52
+ module,
53
+ logger,
54
+ hostId: config.hostId,
55
+ adminKey: credentials.adminKey,
56
+ coreVersion,
57
+ pid,
58
+ browserAccess: options.browserAccess === true,
59
+ ...(options.readiness ? { readiness: options.readiness } : {}),
60
+ listening: () => Boolean(server?.listening) && (!operator || Boolean(operatorServer?.listening)),
61
+ closing: () => closing,
62
+ admin: async (request, node) => await adminApi.fetch(request, node),
63
+ });
64
+ /** Where a listener's requests go once their `Host` header checks out. */
65
+ const pipeline = (role) => getRequestListener(async (request, node) => alreadySent(await app.fetch(request, { ...node, role })), {
66
+ // The Runtime runs inside other processes (`startEphemeralRuntime`): leave their
67
+ // `Request` and `Response` alone.
68
+ overrideGlobalObjects: false,
69
+ // A request target `@hono/node-server` cannot make a URL of, such as `*`.
70
+ errorHandler: (error) => {
71
+ if (error instanceof RequestError)
72
+ return rejectedResponse(400, "invalid_request", "Invalid request target");
73
+ logger.error("request_failed", {
74
+ error: error instanceof Error ? error.message : String(error),
75
+ });
76
+ return rejectedResponse(500, "internal_error", "Internal error");
77
+ },
78
+ });
79
+ /** The `Host` values a listener accepts. */
80
+ const hostAllowed = (role, hostHeader) => role === "operator"
81
+ ? isAllowedRequestHost(hostHeader, {
82
+ port: operatorPort,
83
+ host: operator.host,
84
+ allowNonLoopback: false,
85
+ ...(operator.allowedHosts ? { allowedHosts: operator.allowedHosts } : {}),
86
+ })
87
+ : isAllowedRequestHost(hostHeader, {
88
+ port: listenPort,
89
+ host: config.host,
90
+ allowNonLoopback: config.allowNonLoopback,
91
+ ...(containerListen ? { allowedHosts: containerListen.allowedHosts } : {}),
92
+ });
93
+ /** Responses not yet finished; shutdown ends the streams among them. */
94
+ const inFlight = new Set();
95
+ const serve = (role) => {
96
+ const next = pipeline(role);
97
+ return (req, res) => {
98
+ const started = Date.now();
99
+ inFlight.add(res);
100
+ res.once("close", () => inFlight.delete(res));
101
+ // D§11: `Host` first, before anything reads the request.
102
+ if (hostAllowed(role, headerValue(req, "host")))
103
+ return void next(req, res);
104
+ sendRejected(res, 421, "host_rejected", "Host header is not an allowed loopback or configured address");
105
+ if (pathnameIsLogged(req.url))
331
106
  logger.info("request", {
332
- status: statusCode,
333
- tenantId,
107
+ status: 421,
334
108
  durationMs: Date.now() - started,
335
- path: redactRoutePath(new URL(request.url ?? "/", "http://runtime.local").pathname),
336
- method: request.method,
109
+ path: redactRoutePath(new URL(req.url ?? "/", "http://runtime.local").pathname),
110
+ method: req.method,
337
111
  });
338
- }
339
- }
112
+ };
340
113
  };
114
+ /** Binds `listening`; once bound, a listener error is logged rather than lost. */
115
+ const bindListener = (listening, port, host) => bind(listening, port, host, (error) => logger.error("listener_error", { error: error.message }));
341
116
  async function listen() {
342
117
  if (server)
343
118
  throw new Error("Already listening");
@@ -348,27 +123,37 @@ export function createHost(options) {
348
123
  !isLoopbackHost(config.host)) {
349
124
  throw new HostListenError(`Refusing to bind non-loopback host ${config.host} without allowNonLoopback`, EXIT_NON_LOOPBACK);
350
125
  }
351
- server = createServer((req, res) => {
352
- void handle(req, res);
353
- });
354
- await new Promise((resolve, reject) => {
355
- server.once("error", (error) => {
356
- if (error.code === "EADDRINUSE") {
357
- reject(new HostListenError(`Port ${bindPort} on ${bindHost} is already in use`, EXIT_PORT_IN_USE, error));
358
- return;
359
- }
360
- reject(error);
361
- });
362
- server.listen(bindPort, bindHost, resolve);
363
- });
126
+ if (operator && !containerListen && !isLoopbackHost(operator.host))
127
+ throw new HostListenError(`Refusing to bind the operator listener on non-loopback host ${operator.host}`, EXIT_NON_LOOPBACK);
128
+ server = createServer(serve(mainRole));
129
+ await bindListener(server, bindPort, bindHost);
364
130
  const address = server.address();
365
131
  listenPort =
366
132
  typeof address === "object" && address ? address.port : bindPort;
367
133
  url = `http://${bindHost}:${listenPort}`;
134
+ adminUrl = url;
135
+ if (operator) {
136
+ operatorServer = createServer(serve("operator"));
137
+ await bindListener(operatorServer, operator.port, operator.host).catch(async (error) => {
138
+ // Leave nothing half-open: the main listener closes too.
139
+ await new Promise((resolve) => server.close(() => resolve()));
140
+ server = undefined;
141
+ throw error;
142
+ });
143
+ const operatorAddress = operatorServer.address();
144
+ operatorPort =
145
+ typeof operatorAddress === "object" && operatorAddress
146
+ ? operatorAddress.port
147
+ : operator.port;
148
+ adminUrl = `http://${operator.host}:${operatorPort}`;
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.
368
152
  await module.start();
369
153
  logger.info("host_listening", {
370
154
  hostId: config.hostId,
371
155
  url,
156
+ ...(operator ? { adminUrl } : {}),
372
157
  pid,
373
158
  });
374
159
  }
@@ -393,13 +178,40 @@ export function createHost(options) {
393
178
  closePromise = (async () => {
394
179
  closing = true;
395
180
  logger.info("host_shutdown", { hostId: config.hostId });
396
- if (server) {
397
- await new Promise((resolve) => {
398
- server.close(() => resolve());
399
- server.closeIdleConnections?.();
400
- });
401
- server = undefined;
402
- }
181
+ const listeners = [server, operatorServer].filter((listening) => listening !== undefined);
182
+ const stopped = Promise.all(listeners.map((listening) => new Promise((resolve) => {
183
+ listening.close(() => resolve());
184
+ listening.closeIdleConnections();
185
+ })));
186
+ // A stream never finishes by itself, and a listener closes only once every connection
187
+ // has: end the streams, close their connections once idle, let other requests finish,
188
+ // then cut what is left.
189
+ const streams = [...inFlight].filter(isStreaming);
190
+ void Promise.all(streams.map((response) => {
191
+ const done = new Promise((resolve) => response.once("close", resolve));
192
+ response.end();
193
+ return done;
194
+ })).then(() => {
195
+ for (const listening of listeners)
196
+ listening.closeIdleConnections();
197
+ });
198
+ let grace;
199
+ const graceful = await Promise.race([
200
+ stopped.then(() => true),
201
+ new Promise((resolve) => {
202
+ grace = setTimeout(() => resolve(false), SHUTDOWN_GRACE_MS);
203
+ grace.unref();
204
+ }),
205
+ ]);
206
+ clearTimeout(grace);
207
+ if (!graceful) {
208
+ logger.warn("host_shutdown_forced", { openResponses: inFlight.size });
209
+ for (const listening of listeners)
210
+ listening.closeAllConnections();
211
+ await stopped;
212
+ }
213
+ server = undefined;
214
+ operatorServer = undefined;
403
215
  await step("beforeTenants", options.shutdown?.beforeTenants);
404
216
  await step("tenants", () => module.close());
405
217
  await step("afterTenants", options.shutdown?.afterTenants);
@@ -413,11 +225,30 @@ export function createHost(options) {
413
225
  get url() {
414
226
  return url;
415
227
  },
228
+ get adminUrl() {
229
+ return adminUrl;
230
+ },
416
231
  };
417
232
  }
418
- function pathnameIsLogged(rawUrl) {
419
- if (!rawUrl)
420
- return true;
421
- const pathname = new URL(rawUrl, "http://runtime.local").pathname;
422
- return pathname !== "/health" && pathname !== "/ready";
233
+ /**
234
+ * An answer already written to the Node response. `@hono/node-server` skips writing one that
235
+ * says so in its headers, unless it is its own `Response` class, which a process that ran its
236
+ * `serve()` has as the global one: it writes those again. This is never that class.
237
+ */
238
+ const SENT = {
239
+ status: 200,
240
+ headers: new Headers({ "x-hono-already-sent": "true" }),
241
+ body: null,
242
+ };
243
+ function alreadySent(response) {
244
+ return response.headers.has("x-hono-already-sent") ? SENT : response;
245
+ }
246
+ /** How long shutdown waits for requests in progress before closing their connections. */
247
+ const SHUTDOWN_GRACE_MS = 10_000;
248
+ /**
249
+ * A response that has started and not ended is a stream: JSON answers are written and ended
250
+ * at once. (`getHeader` cannot tell, since streams pass their headers to `writeHead`.)
251
+ */
252
+ function isStreaming(response) {
253
+ return response.headersSent && !response.writableEnded;
423
254
  }
@@ -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
  }