@nylorun/runtime 0.13.0-beta → 0.14.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 (223) hide show
  1. package/CHANGELOG.md +330 -0
  2. package/README.md +69 -15
  3. package/dist/adapters/execution/restate.d.ts +7 -2
  4. package/dist/adapters/execution/restate.js +38 -8
  5. package/dist/adapters/replication/pgoutput.d.ts +16 -0
  6. package/dist/adapters/replication/pgoutput.js +169 -0
  7. package/dist/adapters/streams/s2.js +37 -20
  8. package/dist/admin-openapi.json +1071 -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 +7825 -0
  32. package/dist/api/components.js +83 -0
  33. package/dist/api/http/app.d.ts +18 -0
  34. package/dist/api/http/app.js +58 -0
  35. package/dist/api/http/body.d.ts +4 -0
  36. package/dist/api/http/body.js +37 -0
  37. package/dist/api/http/define.d.ts +39 -0
  38. package/dist/api/http/define.js +130 -0
  39. package/dist/api/http/respond.d.ts +9 -0
  40. package/dist/api/http/respond.js +44 -0
  41. package/dist/api/http/routes/access.d.ts +24 -0
  42. package/dist/api/http/routes/access.js +259 -0
  43. package/dist/api/http/routes/actions.d.ts +8 -0
  44. package/dist/api/http/routes/actions.js +76 -0
  45. package/dist/api/http/routes/endpoints.d.ts +7 -0
  46. package/dist/api/http/routes/endpoints.js +100 -0
  47. package/dist/api/http/routes/sessions.d.ts +3 -0
  48. package/dist/api/http/routes/sessions.js +205 -0
  49. package/dist/api/http/routes/tenant.d.ts +10 -0
  50. package/dist/api/http/routes/tenant.js +172 -0
  51. package/dist/api/http/routes/vaults.d.ts +8 -0
  52. package/dist/api/http/routes/vaults.js +167 -0
  53. package/dist/api/openapi.d.ts +19 -0
  54. package/dist/api/openapi.js +138 -0
  55. package/dist/api/route.d.ts +12 -0
  56. package/dist/api/route.js +4 -0
  57. package/dist/core/bearer.d.ts +8 -0
  58. package/dist/core/bearer.js +11 -0
  59. package/dist/core/flow-host.d.ts +7 -15
  60. package/dist/core/flow-host.js +16 -41
  61. package/dist/core/sandbox-routes.d.ts +12 -7
  62. package/dist/core/sandbox-routes.js +24 -25
  63. package/dist/execution/memory.d.ts +8 -5
  64. package/dist/execution/memory.js +33 -13
  65. package/dist/execution/types.d.ts +38 -6
  66. package/dist/execution/types.js +12 -3
  67. package/dist/host/admin-api.d.ts +22 -0
  68. package/dist/host/admin-api.js +178 -0
  69. package/dist/host/app.d.ts +34 -0
  70. package/dist/host/app.js +185 -0
  71. package/dist/host/config-for.d.ts +2 -0
  72. package/dist/host/config-for.js +1 -0
  73. package/dist/host/config.d.ts +11 -0
  74. package/dist/host/cors.d.ts +27 -0
  75. package/dist/host/cors.js +64 -0
  76. package/dist/host/create-host.d.ts +31 -3
  77. package/dist/host/create-host.js +168 -327
  78. package/dist/host/http.d.ts +14 -3
  79. package/dist/host/http.js +64 -23
  80. package/dist/host/main.js +55 -4
  81. package/dist/host/stack-config.d.ts +20 -0
  82. package/dist/host/stack-config.js +55 -0
  83. package/dist/mcp/pool.d.ts +19 -1
  84. package/dist/mcp/pool.js +60 -10
  85. package/dist/model/catalog.d.ts +2 -10
  86. package/dist/model/classify.d.ts +13 -0
  87. package/dist/model/classify.js +61 -0
  88. package/dist/model/models.d.ts +10 -1
  89. package/dist/model/models.js +14 -6
  90. package/dist/model/pi-model.d.ts +17 -2
  91. package/dist/model/pi-model.js +366 -222
  92. package/dist/openapi.json +25696 -0
  93. package/dist/sandbox/manager.d.ts +13 -1
  94. package/dist/sandbox/manager.js +8 -11
  95. package/dist/sandbox/resolve.d.ts +33 -0
  96. package/dist/sandbox/resolve.js +81 -0
  97. package/dist/sandbox/session-sandbox.d.ts +11 -0
  98. package/dist/sandbox/session-sandbox.js +51 -0
  99. package/dist/sandbox/share.d.ts +14 -2
  100. package/dist/sandbox/share.js +33 -3
  101. package/dist/sandbox/tenant-config.d.ts +37 -0
  102. package/dist/sandbox/tenant-config.js +51 -0
  103. package/dist/sandbox/tools.d.ts +7 -9
  104. package/dist/sandbox/tools.js +11 -3
  105. package/dist/sandbox/types.d.ts +8 -2
  106. package/dist/store/event.d.ts +19 -0
  107. package/dist/store/event.js +49 -0
  108. package/dist/store/memory.d.ts +20 -8
  109. package/dist/store/memory.js +265 -100
  110. package/dist/store/postgres/connect.d.ts +13 -0
  111. package/dist/store/postgres/connect.js +5 -0
  112. package/dist/store/postgres/migrations/003_subject_tokens.d.ts +8 -0
  113. package/dist/store/postgres/migrations/003_subject_tokens.js +42 -0
  114. package/dist/store/postgres/migrations/004_publishable_keys.d.ts +6 -0
  115. package/dist/store/postgres/migrations/004_publishable_keys.js +18 -0
  116. package/dist/store/postgres/migrations/005_action_endpoints.d.ts +6 -0
  117. package/dist/store/postgres/migrations/005_action_endpoints.js +32 -0
  118. package/dist/store/postgres/migrations/006_drop_executors.d.ts +11 -0
  119. package/dist/store/postgres/migrations/006_drop_executors.js +32 -0
  120. package/dist/store/postgres/migrations/007_event_time.d.ts +7 -0
  121. package/dist/store/postgres/migrations/007_event_time.js +14 -0
  122. package/dist/store/postgres/migrations/008_durable_streams.d.ts +13 -0
  123. package/dist/store/postgres/migrations/008_durable_streams.js +35 -0
  124. package/dist/store/postgres/migrations/index.d.ts +5 -1
  125. package/dist/store/postgres/migrations/index.js +22 -2
  126. package/dist/store/postgres/migrations/shared/index.d.ts +31 -0
  127. package/dist/store/postgres/migrations/shared/index.js +92 -0
  128. package/dist/store/postgres/record.d.ts +11 -0
  129. package/dist/store/postgres/record.js +64 -0
  130. package/dist/store/postgres/store.js +337 -95
  131. package/dist/store/postgres/tenants.js +14 -0
  132. package/dist/store/types.d.ts +176 -81
  133. package/dist/streams/basin.d.ts +18 -1
  134. package/dist/streams/basin.js +34 -2
  135. package/dist/streams/control.d.ts +18 -0
  136. package/dist/streams/control.js +29 -0
  137. package/dist/streams/memory.d.ts +3 -1
  138. package/dist/streams/memory.js +14 -8
  139. package/dist/streams/relay/core.d.ts +35 -0
  140. package/dist/streams/relay/core.js +366 -0
  141. package/dist/streams/relay/memory.d.ts +69 -0
  142. package/dist/streams/relay/memory.js +176 -0
  143. package/dist/streams/relay/types.d.ts +77 -0
  144. package/dist/streams/relay/types.js +7 -0
  145. package/dist/streams/types.d.ts +66 -36
  146. package/dist/streams/types.js +18 -53
  147. package/dist/tenant/access-policy.d.ts +30 -0
  148. package/dist/tenant/access-policy.js +52 -0
  149. package/dist/tenant/actions.d.ts +10 -149
  150. package/dist/tenant/actions.js +5 -167
  151. package/dist/tenant/advance.js +32 -0
  152. package/dist/tenant/auth.d.ts +19 -17
  153. package/dist/tenant/auth.js +90 -93
  154. package/dist/tenant/browser.d.ts +15 -0
  155. package/dist/tenant/browser.js +39 -0
  156. package/dist/tenant/commands.d.ts +18 -2
  157. package/dist/tenant/commands.js +107 -113
  158. package/dist/tenant/context.d.ts +75 -18
  159. package/dist/tenant/context.js +14 -10
  160. package/dist/tenant/delivery-token.d.ts +41 -0
  161. package/dist/tenant/delivery-token.js +105 -0
  162. package/dist/tenant/delivery.d.ts +62 -0
  163. package/dist/tenant/delivery.js +476 -0
  164. package/dist/tenant/effects.d.ts +5 -0
  165. package/dist/tenant/effects.js +78 -37
  166. package/dist/tenant/endpoints.d.ts +67 -0
  167. package/dist/tenant/endpoints.js +90 -0
  168. package/dist/tenant/ephemeral.d.ts +6 -0
  169. package/dist/tenant/ephemeral.js +60 -36
  170. package/dist/tenant/http.d.ts +8 -9
  171. package/dist/tenant/http.js +13 -27
  172. package/dist/tenant/jwt.d.ts +16 -0
  173. package/dist/tenant/jwt.js +50 -0
  174. package/dist/tenant/module.d.ts +3 -0
  175. package/dist/tenant/module.js +6 -12
  176. package/dist/tenant/outbound.d.ts +44 -0
  177. package/dist/tenant/outbound.js +127 -0
  178. package/dist/tenant/pool.d.ts +0 -2
  179. package/dist/tenant/pool.js +0 -18
  180. package/dist/tenant/reset.d.ts +0 -4
  181. package/dist/tenant/reset.js +0 -5
  182. package/dist/tenant/runtime.d.ts +10 -20
  183. package/dist/tenant/runtime.js +59 -42
  184. package/dist/tenant/scheduler.d.ts +7 -2
  185. package/dist/tenant/scheduler.js +7 -3
  186. package/dist/tenant/session-streams.d.ts +125 -0
  187. package/dist/tenant/session-streams.js +399 -0
  188. package/dist/tenant/sessions.d.ts +28 -4
  189. package/dist/tenant/sessions.js +118 -24
  190. package/dist/tenant/signing-keys.d.ts +55 -0
  191. package/dist/tenant/signing-keys.js +172 -0
  192. package/dist/tenant/slim.d.ts +8 -0
  193. package/dist/tenant/slim.js +21 -0
  194. package/dist/tenant/status.d.ts +0 -3
  195. package/dist/tenant/status.js +19 -12
  196. package/dist/tenant/streams.d.ts +60 -67
  197. package/dist/tenant/streams.js +140 -208
  198. package/dist/tenant/subject-limits.d.ts +16 -0
  199. package/dist/tenant/subject-limits.js +45 -0
  200. package/dist/tenant/sweep.d.ts +1 -10
  201. package/dist/tenant/sweep.js +29 -69
  202. package/dist/tenant/tokens.d.ts +14 -0
  203. package/dist/tenant/tokens.js +178 -0
  204. package/dist/tenant/transcript.d.ts +48 -5
  205. package/dist/tenant/transcript.js +58 -1
  206. package/dist/tenant/types.d.ts +29 -7
  207. package/dist/tenant/worker.d.ts +3 -1
  208. package/dist/tenant/worker.js +9 -3
  209. package/dist/vault/service.d.ts +18 -2
  210. package/dist/vault/service.js +57 -13
  211. package/dist/version.d.ts +1 -1
  212. package/dist/version.js +1 -1
  213. package/package.json +20 -8
  214. package/dist/core/executors.d.ts +0 -44
  215. package/dist/core/executors.js +0 -95
  216. package/dist/streams/relay.d.ts +0 -37
  217. package/dist/streams/relay.js +0 -254
  218. package/dist/tenant/live.d.ts +0 -92
  219. package/dist/tenant/live.js +0 -290
  220. package/dist/tenant/routes-tenant.d.ts +0 -12
  221. package/dist/tenant/routes-tenant.js +0 -137
  222. package/dist/tenant/routes.d.ts +0 -9
  223. package/dist/tenant/routes.js +0 -151
@@ -1,3 +1,4 @@
1
+ import type { StreamRelayStatus } from "@nylorun/core/contracts";
1
2
  import { isQuarantineError, quarantine, TenantBusyError, TenantConflictError } from "./quarantine.js";
2
3
  import { TenantNotFoundError, TenantUnavailableError, type Logger, type OpenTenantRuntime, type TenantConfig, type TenantModule, type TenantStore } from "./types.js";
3
4
  export interface CreateTenantModuleOptions {
@@ -21,6 +22,8 @@ export interface CreateTenantModuleOptions {
21
22
  * Durable Streams basin, its armed sweep). A failure is logged; the Tenant stays deleted.
22
23
  */
23
24
  onDeleted?: (tenantId: string) => Promise<void>;
25
+ /** This process's stream relay, for the Host aggregate (a Host with S2 runs one). */
26
+ relayStatus?: () => Promise<StreamRelayStatus>;
24
27
  }
25
28
  /**
26
29
  * Deep Tenant module (§8): discovery, create, resolve, list, delete and summarize over an
@@ -7,7 +7,7 @@ import { TenantNotFoundError, TenantUnavailableError, } from "./types.js";
7
7
  const OPEN_TIMEOUT_MS = 30_000;
8
8
  function hasLiveWork(summary) {
9
9
  return (summary.runningSessions > 0 ||
10
- summary.connectedExecutors > 0 ||
10
+ summary.inFlightDeliveries > 0 ||
11
11
  summary.pendingActions > 0);
12
12
  }
13
13
  function toAdmin(id, entry, envelope) {
@@ -284,28 +284,22 @@ export function createTenantModule(options) {
284
284
  },
285
285
  async summarize() {
286
286
  let runningSessions = 0;
287
- let connectedExecutors = 0;
287
+ let inFlightDeliveries = 0;
288
288
  let pendingActions = 0;
289
289
  let uncertainEffects = 0;
290
- let outboxDepth;
291
- let relayLagMs;
292
290
  for (const s of await Promise.all(openEntries().map((e) => e.handle.summary()))) {
293
291
  runningSessions += s.runningSessions;
294
- connectedExecutors += s.connectedExecutors;
292
+ inFlightDeliveries += s.inFlightDeliveries;
295
293
  pendingActions += s.pendingActions;
296
294
  uncertainEffects += s.uncertainEffects;
297
- if (s.outboxDepth !== undefined)
298
- outboxDepth = (outboxDepth ?? 0) + s.outboxDepth;
299
- if (s.relayLagMs !== undefined)
300
- relayLagMs = Math.max(relayLagMs ?? 0, s.relayLagMs);
301
295
  }
296
+ const relay = await options.relayStatus?.().catch(() => undefined);
302
297
  return {
303
298
  runningSessions,
304
- connectedExecutors,
299
+ inFlightDeliveries,
305
300
  pendingActions,
306
301
  uncertainEffects,
307
- ...(outboxDepth !== undefined ? { outboxDepth } : {}),
308
- ...(relayLagMs !== undefined ? { relayLagMs } : {}),
302
+ ...(relay ? { relay } : {}),
309
303
  };
310
304
  },
311
305
  async close() {
@@ -0,0 +1,44 @@
1
+ import { type IncomingHttpHeaders } from "node:http";
2
+ /** How the Host lets the Runtime call developer URLs (Host settings, `TenantConfig.delivery`). */
3
+ export interface OutboundPolicy {
4
+ /** `http` URLs as well as `https`. Default true (OSS). */
5
+ allowHttp?: boolean;
6
+ /** Private, loopback and link-local addresses. Default `allow` (OSS); Cloud refuses. */
7
+ privateAddresses?: "allow" | "refuse";
8
+ /**
9
+ * `docker-host`: the Runtime runs in the local stack's container, so `localhost`,
10
+ * `127.0.0.1` and `[::1]` mean the machine that runs Docker (`host.docker.internal`).
11
+ */
12
+ loopback?: "docker-host";
13
+ }
14
+ export type OutboundResult = {
15
+ kind: "response";
16
+ status: number;
17
+ headers: IncomingHttpHeaders;
18
+ body: Buffer;
19
+ }
20
+ /** The answer was larger than `maxResponseBytes`. */
21
+ | {
22
+ kind: "too_large";
23
+ status: number;
24
+ } | {
25
+ kind: "not_sent";
26
+ code: string;
27
+ message: string;
28
+ } | {
29
+ kind: "lost";
30
+ code: string;
31
+ message: string;
32
+ };
33
+ /** The largest answer read from an endpoint. */
34
+ export declare const MAX_RESPONSE_BYTES: number;
35
+ /** True when `address` is private, loopback, link-local or unspecified (IPv4-mapped included). */
36
+ export declare function isPrivateAddress(address: string): boolean;
37
+ /** Why the Host refuses `url`, or `undefined` when it may be called. */
38
+ export declare function refusal(url: URL, policy: OutboundPolicy): string | undefined;
39
+ /** POSTs `body` to `url` under `policy`. Never throws; `signal` aborts the request. */
40
+ export declare function post(target: string, body: string, headers: Record<string, string>, options: {
41
+ policy: OutboundPolicy;
42
+ signal: AbortSignal;
43
+ maxResponseBytes?: number;
44
+ }): Promise<OutboundResult>;
@@ -0,0 +1,127 @@
1
+ /**
2
+ * The Runtime's requests to Action endpoints (design: Action endpoints §8.3): one POST, no
3
+ * redirects, a bounded answer, and the Host's address policy checked on the address actually
4
+ * connected to, so a DNS answer cannot steer a delivery somewhere the Host forbids.
5
+ *
6
+ * The result says whether anything reached the endpoint. A request that failed before its body
7
+ * was flushed (refused connection, unknown host, TLS failure, a refused address) was `not_sent`
8
+ * and is safe to send again, even for a tool; one that failed after (reset, timeout) is `lost`.
9
+ */
10
+ import { lookup as dnsLookup } from "node:dns";
11
+ import { request as httpRequest } from "node:http";
12
+ import { request as httpsRequest } from "node:https";
13
+ import { BlockList, isIP } from "node:net";
14
+ /** The largest answer read from an endpoint. */
15
+ export const MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
16
+ const PRIVATE = new BlockList();
17
+ for (const [network, prefix] of [
18
+ ["0.0.0.0", 8],
19
+ ["10.0.0.0", 8],
20
+ ["100.64.0.0", 10],
21
+ ["127.0.0.0", 8],
22
+ ["169.254.0.0", 16],
23
+ ["172.16.0.0", 12],
24
+ ["192.168.0.0", 16],
25
+ ])
26
+ PRIVATE.addSubnet(network, prefix, "ipv4");
27
+ for (const [network, prefix] of [
28
+ ["::", 128],
29
+ ["::1", 128],
30
+ ["fc00::", 7],
31
+ ["fe80::", 10],
32
+ ])
33
+ PRIVATE.addSubnet(network, prefix, "ipv6");
34
+ /** True when `address` is private, loopback, link-local or unspecified (IPv4-mapped included). */
35
+ export function isPrivateAddress(address) {
36
+ const mapped = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/i.exec(address);
37
+ if (mapped)
38
+ return PRIVATE.check(mapped[1], "ipv4");
39
+ const family = isIP(address);
40
+ if (family === 0)
41
+ return false;
42
+ return PRIVATE.check(address, family === 4 ? "ipv4" : "ipv6");
43
+ }
44
+ class RefusedAddress extends Error {
45
+ code = "ENDPOINT_ADDRESS_REFUSED";
46
+ }
47
+ /** Why the Host refuses `url`, or `undefined` when it may be called. */
48
+ export function refusal(url, policy) {
49
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && policy.allowHttp !== false))
50
+ return `This Runtime calls only ${policy.allowHttp === false ? "https" : "http and https"} endpoints`;
51
+ const host = url.hostname.replace(/^\[|\]$/g, "");
52
+ if (policy.privateAddresses === "refuse" && isIP(host) && isPrivateAddress(host))
53
+ return "This Runtime does not call private addresses; expose the endpoint through a public URL (a tunnel, for a local app)";
54
+ return undefined;
55
+ }
56
+ /** POSTs `body` to `url` under `policy`. Never throws; `signal` aborts the request. */
57
+ export async function post(target, body, headers, options) {
58
+ const { policy, signal } = options;
59
+ const limit = options.maxResponseBytes ?? MAX_RESPONSE_BYTES;
60
+ const url = new URL(target);
61
+ const refused = refusal(url, policy);
62
+ if (refused)
63
+ return { kind: "not_sent", code: "ENDPOINT_ADDRESS_REFUSED", message: refused };
64
+ const originalHost = url.host;
65
+ if (policy.loopback === "docker-host" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname))
66
+ url.hostname = "host.docker.internal";
67
+ const lookup = (hostname, lookupOptions, callback) => dnsLookup(hostname, { ...lookupOptions, all: true }, (error, addresses) => {
68
+ if (error)
69
+ return callback(error, []);
70
+ const allowed = policy.privateAddresses === "refuse"
71
+ ? addresses.filter((a) => !isPrivateAddress(a.address))
72
+ : addresses;
73
+ if (allowed.length === 0)
74
+ return callback(new RefusedAddress(`${hostname} resolves only to private addresses, which this Runtime does not call`), []);
75
+ if (lookupOptions.all)
76
+ return callback(null, allowed);
77
+ return callback(null, allowed[0].address, allowed[0].family);
78
+ });
79
+ return new Promise((resolve) => {
80
+ let sent = false;
81
+ let settled = false;
82
+ const finish = (result) => {
83
+ if (settled)
84
+ return;
85
+ settled = true;
86
+ resolve(result);
87
+ };
88
+ const failed = (error) => finish({
89
+ kind: sent ? "lost" : "not_sent",
90
+ code: signal.aborted ? "ABORTED" : (error.code ?? error.name),
91
+ message: signal.aborted ? "The delivery was aborted" : error.message,
92
+ });
93
+ const request = (url.protocol === "https:" ? httpsRequest : httpRequest)(url, {
94
+ method: "POST",
95
+ headers: {
96
+ ...headers,
97
+ host: originalHost,
98
+ "content-type": "application/json",
99
+ "content-length": String(Buffer.byteLength(body)),
100
+ },
101
+ lookup: lookup,
102
+ signal,
103
+ }, (response) => {
104
+ const status = response.statusCode ?? 0;
105
+ const chunks = [];
106
+ let size = 0;
107
+ response.on("data", (chunk) => {
108
+ size += chunk.length;
109
+ if (size > limit) {
110
+ // Settle first: destroying the answer reports an error of its own.
111
+ finish({ kind: "too_large", status });
112
+ response.destroy();
113
+ return;
114
+ }
115
+ chunks.push(chunk);
116
+ });
117
+ response.on("end", () => finish({ kind: "response", status, headers: response.headers, body: Buffer.concat(chunks) }));
118
+ response.on("error", failed);
119
+ response.on("aborted", () => failed(Object.assign(new Error("The answer was cut off"), { code: "ECONNRESET" })));
120
+ });
121
+ request.on("finish", () => {
122
+ sent = true;
123
+ });
124
+ request.on("error", failed);
125
+ request.end(body);
126
+ });
127
+ }
@@ -1,5 +1,3 @@
1
- /** Run async work over items with a fixed concurrency limit. */
2
- export declare function mapPool<T, R>(items: readonly T[], concurrency: number, worker: (item: T, index: number) => Promise<R>): Promise<R[]>;
3
1
  export declare class TimeoutError extends Error {
4
2
  constructor(message?: string);
5
3
  }
@@ -1,21 +1,3 @@
1
- /** Run async work over items with a fixed concurrency limit. */
2
- export async function mapPool(items, concurrency, worker) {
3
- if (items.length === 0)
4
- return [];
5
- const limit = Math.max(1, Math.min(concurrency, items.length));
6
- const results = new Array(items.length);
7
- let next = 0;
8
- async function run() {
9
- for (;;) {
10
- const index = next++;
11
- if (index >= items.length)
12
- return;
13
- results[index] = await worker(items[index], index);
14
- }
15
- }
16
- await Promise.all(Array.from({ length: limit }, () => run()));
17
- return results;
18
- }
19
1
  export class TimeoutError extends Error {
20
2
  constructor(message = "operation timed out") {
21
3
  super(message);
@@ -1,17 +1,13 @@
1
1
  import type { ResetScope, SessionStore } from "../store/types.js";
2
- import type { ExecutorRegistry } from "../core/executors.js";
3
2
  import type { SandboxManager } from "../sandbox/manager.js";
4
3
  import type { TenantPaths } from "./types.js";
5
4
  export type { ResetScope } from "../store/types.js";
6
5
  export interface ResetTenantContext {
7
6
  store: SessionStore;
8
- registry: ExecutorRegistry;
9
7
  sandbox: SandboxManager;
10
8
  paths: TenantPaths;
11
9
  /** Clear in-memory session observers after the store wipe. */
12
10
  clearSessionState: () => void;
13
- /** Clear executor SSE streams when registrations are removed. */
14
- clearExecutorStreams: () => void;
15
11
  }
16
12
  /**
17
13
  * Reset Tenant durable state for `scope` (A17).
@@ -42,13 +42,8 @@ export async function resetTenant(ctx, scope) {
42
42
  await ctx.sandbox.reconcile(() => false);
43
43
  }
44
44
  await ctx.store.tx((t) => t.reset(scope));
45
- if (clearAll)
46
- for (const row of ctx.registry.list())
47
- ctx.registry.remove(row.agentId);
48
45
  if (clearSessions)
49
46
  ctx.clearSessionState();
50
- if (clearAll)
51
- ctx.clearExecutorStreams();
52
47
  if (clearSandboxes)
53
48
  replaceDirectory(ctx.paths.sandboxes);
54
49
  if (clearAll) {
@@ -1,25 +1,8 @@
1
- /**
2
- * Composition root of an open Tenant: validates the config, opens the Session Store and the
3
- * vault key, builds the services and the `TenantContext`, wires the seams, registers the
4
- * Tenant's Worker handlers and arms its sweep, and implements `TenantHandle` (handle,
5
- * summary, drain, close) by delegating to the Tenant modules.
6
- *
7
- * Seams wired here: `wireStreams()` connects the store's commits to Durable Streams (the
8
- * relay, and the history, SSE, work and control readers); `wake` goes to the
9
- * `DurableExecution`, whose handlers (`worker.ts`) call `advance` and `sweep`; `abortLocal`
10
- * aborts an advance running on this process. The sweep also drains the outbox.
11
- *
12
- * Execution: the Host passes one `TenantExecution` for every Tenant it opens
13
- * (`TenantOpenHooks.execution`); without one, the Tenant runs its own in-process
14
- * `MemoryExecution`. No lock file: several processes may open a Tenant, and ownership of each
15
- * session (§10.6) keeps its advances apart.
16
- */
17
- import type { ServerResponse, IncomingMessage } from "node:http";
18
1
  import type { TenantEnvelope } from "@nylorun/core/contracts";
19
2
  import { type ModelProvider } from "../core/provider.js";
20
3
  import { type AuthorizeResult } from "../vault/service.js";
21
4
  import type { DurableStreams } from "../streams/types.js";
22
- import type { OpenedTenant, TenantConfig, TenantHandle, TenantSummary } from "./types.js";
5
+ import type { NodeBindings, OpenedTenant, TenantConfig, TenantHandle, TenantSummary } from "./types.js";
23
6
  import { type StreamsStatus } from "./streams.js";
24
7
  import { type TenantExecution, type TenantWorker } from "./worker.js";
25
8
  /** TENANTS-CCR: test/injection hooks until TenantConfig gains them. */
@@ -42,6 +25,13 @@ export type TenantOpenHooks = {
42
25
  * survive a restart (tests, and a Host without S2).
43
26
  */
44
27
  streams?: DurableStreams;
28
+ /**
29
+ * The Host's stream relay feeds `streams` from the record for every Tenant. Without it the
30
+ * Tenant relays its own commits.
31
+ */
32
+ hostRelay?: boolean;
33
+ /** How long a retired stream basin is kept after a reset (tests). Default 60 s. */
34
+ retireGraceMs?: number;
45
35
  } & OpenedTenant;
46
36
  export declare class TenantRuntime implements TenantHandle {
47
37
  private readonly ctx;
@@ -53,7 +43,7 @@ export declare class TenantRuntime implements TenantHandle {
53
43
  private readonly closeGraceMs;
54
44
  private constructor();
55
45
  static open(config: TenantConfig, hooks: TenantOpenHooks): Promise<TenantRuntime>;
56
- handle(request: IncomingMessage, response: ServerResponse, url?: URL): Promise<void>;
46
+ fetch(request: Request, node: NodeBindings): Promise<Response>;
57
47
  authorize(sessionId: string, request: {
58
48
  url: string;
59
49
  serverName?: string;
@@ -67,7 +57,7 @@ export declare class TenantRuntime implements TenantHandle {
67
57
  abortLocal(sessionId: string): void;
68
58
  /**
69
59
  * The Durable Streams seam's status for Tenant status and readiness: S2 reachability, the
70
- * basin, outbox depth and relay lag (`streamsStatus` in `tenant/streams.ts`).
60
+ * basin, its generation and the Tenant's relay (`streamsStatus` in `tenant/streams.ts`).
71
61
  */
72
62
  streamsStatus(): Promise<StreamsStatus>;
73
63
  /** Adds a callback to the Tenant sweep. Returns a function that removes it. */
@@ -1,8 +1,8 @@
1
1
  import { mkdirSync } from "node:fs";
2
2
  import { resolveFlowLimits } from "../core/limits.js";
3
- import { loadExecutorRegistry } from "../core/executors.js";
4
3
  import { scriptedModel, gatewayModel, toolFixtureModel, } from "../core/provider.js";
5
4
  import { createKekFile, readVaultKek } from "../vault/kek.js";
5
+ import { SigningKeys } from "./signing-keys.js";
6
6
  import { VaultService } from "../vault/service.js";
7
7
  import { McpPool } from "../mcp/pool.js";
8
8
  import { SandboxManager } from "../sandbox/manager.js";
@@ -10,14 +10,15 @@ import { defaultSandboxBackends } from "../sandbox/select.js";
10
10
  import { MemoryExecution } from "../execution/memory.js";
11
11
  import { QuarantineError } from "./quarantine-error.js";
12
12
  import { MemoryStreams } from "../streams/memory.js";
13
- import { connectedExecutorCount, createLiveHub, endAllStreams, } from "./live.js";
14
- import { closeStreams, drainOutbox, streamsStatus, wireStreams, } from "./streams.js";
13
+ import { createSessionStreams, endAllStreams, } from "./session-streams.js";
14
+ import { closeStreams, streamsStatus, wireStreams, } from "./streams.js";
15
15
  import { abortAll, abortLocal, createWorkState, drain, waitForIdle, } from "./scheduler.js";
16
16
  import { advance } from "./advance.js";
17
17
  import { sweep } from "./sweep.js";
18
+ import { deliverAction } from "./delivery.js";
18
19
  import { TenantWorkers, WORKER_ID, } from "./worker.js";
19
20
  import { authorize } from "./effects.js";
20
- import { handle } from "./routes.js";
21
+ import { tenantApi } from "../api/http/app.js";
21
22
  /** Default ownership lease of an advance; the heartbeat renews it every third. */
22
23
  const DEFAULT_OWNER_LEASE_MS = 30_000;
23
24
  function validateConfig(config) {
@@ -70,8 +71,9 @@ export class TenantRuntime {
70
71
  vaultKek: hooks.vaultKek,
71
72
  vaultKekPath: paths.kek,
72
73
  });
73
- if ((await store.tx((t) => t.countCredentials())) > 0 && !kek) {
74
- throw new QuarantineError("kek-missing", "Vault key-encryption key is missing for ciphertext in this Tenant", "restore the vault-kek file in the Tenant directory");
74
+ const sealed = await store.tx(async (t) => (await t.countCredentials()) + (await t.countSigningKeys()));
75
+ if (sealed > 0 && !kek) {
76
+ throw new QuarantineError("kek-missing", "Vault key-encryption key is missing for ciphertext (vault credentials or signing keys) in this Tenant", "restore the vault-kek file in the Tenant directory");
75
77
  }
76
78
  const createKekIfMissing = hooks.createKekIfMissing !== false;
77
79
  const ensureKek = () => {
@@ -83,7 +85,6 @@ export class TenantRuntime {
83
85
  return kek;
84
86
  };
85
87
  const opened = store;
86
- const registry = await loadExecutorRegistry(opened);
87
88
  // A seeded Tenant setting wins over the Host default (A18).
88
89
  const seededBackend = await opened.tx((t) => t.getSetting("sandbox.backend"));
89
90
  const vault = new VaultService({
@@ -141,7 +142,7 @@ export class TenantRuntime {
141
142
  }
142
143
  else
143
144
  modelProvider = scriptedModel();
144
- const live = createLiveHub();
145
+ const sessionStreams = createSessionStreams();
145
146
  const local = hooks.execution
146
147
  ? undefined
147
148
  : new MemoryExecution({
@@ -160,7 +161,6 @@ export class TenantRuntime {
160
161
  envelope,
161
162
  store: opened,
162
163
  vault,
163
- registry,
164
164
  mcp,
165
165
  sandbox,
166
166
  flowLimits,
@@ -169,7 +169,8 @@ export class TenantRuntime {
169
169
  closing: false,
170
170
  closed: false,
171
171
  work: createWorkState(),
172
- live,
172
+ sessionStreams,
173
+ signingKeys: new SigningKeys({ tenantId: config.tenantId, kek: ensureKek }),
173
174
  workerId: hooks.workerId ?? WORKER_ID,
174
175
  ownerLeaseMs: config.ownerLeaseMs ?? DEFAULT_OWNER_LEASE_MS,
175
176
  wake: async (sessionId, wake) => {
@@ -178,6 +179,11 @@ export class TenantRuntime {
178
179
  await execution.wake(config.tenantId, sessionId, wake);
179
180
  },
180
181
  abortLocal: (sessionId) => abortLocal(ctx, sessionId),
182
+ deliver: async (actionId) => {
183
+ if (ctx.closing || ctx.closed)
184
+ return;
185
+ await execution.deliver(config.tenantId, actionId);
186
+ },
181
187
  ...(hooks.execution?.stuckInvocations
182
188
  ? {
183
189
  stuckInvocations: () => hooks.execution.stuckInvocations(config.tenantId),
@@ -194,21 +200,15 @@ export class TenantRuntime {
194
200
  streams: hooks.streams ?? new MemoryStreams(),
195
201
  ownsStreams: !hooks.streams,
196
202
  tenantId: config.tenantId,
197
- });
198
- // Outbox rows a lost relay step left behind are appended by the sweep.
199
- sweepHooks.add(async () => {
200
- await drainOutbox(ctx);
203
+ ...(hooks.hostRelay ? { hostRelay: true } : {}),
204
+ ...(hooks.retireGraceMs !== undefined ? { retireGraceMs: hooks.retireGraceMs } : {}),
201
205
  });
202
206
  // Register the handlers, then arm the sweep: its first pass runs at once and re-wakes
203
207
  // sessions a previous process left runnable or running (takeover handles the rest).
204
- let afterOpen = true;
205
208
  const worker = {
206
209
  advance: (sessionId, signal) => advance(ctx, sessionId, signal),
207
- sweep: async () => {
208
- const first = afterOpen;
209
- afterOpen = false;
210
- await sweep(ctx, { afterOpen: first });
211
- },
210
+ deliver: (actionId, signal) => deliverAction(ctx, actionId, signal),
211
+ sweep: () => sweep(ctx),
212
212
  };
213
213
  const unregister = workers.register(config.tenantId, worker);
214
214
  detach = async () => {
@@ -227,28 +227,21 @@ export class TenantRuntime {
227
227
  throw error;
228
228
  }
229
229
  }
230
- handle(request, response, url) {
231
- return handle(this.ctx, request, response, url);
230
+ async fetch(request, node) {
231
+ return await tenantApi().fetch(request, { ...node, tenant: this.ctx });
232
232
  }
233
233
  authorize(sessionId, request) {
234
234
  return authorize(this.ctx, sessionId, request);
235
235
  }
236
236
  async summary() {
237
- const { store, live } = this.ctx;
238
- const { counts, outbox } = await store.tx(async (t) => ({
239
- counts: await t.counts(),
240
- outbox: await t.outboxStats(),
241
- }));
237
+ const { store } = this.ctx;
238
+ const counts = await store.tx((t) => t.counts());
242
239
  return {
243
240
  ready: !this.ctx.closing && !this.ctx.closed,
244
241
  runningSessions: counts.runningSessions,
245
- connectedExecutors: connectedExecutorCount(live),
242
+ inFlightDeliveries: [...this.ctx.work.deliveries.values()].reduce((n, set) => n + set.size, 0),
246
243
  pendingActions: counts.pendingActions,
247
244
  uncertainEffects: counts.uncertainEffects,
248
- outboxDepth: outbox.depth,
249
- relayLagMs: outbox.oldestCreatedAt === null
250
- ? 0
251
- : Math.max(0, Date.now() - Date.parse(outbox.oldestCreatedAt)),
252
245
  };
253
246
  }
254
247
  drain(activeWork, timeoutMs = 30_000) {
@@ -263,7 +256,7 @@ export class TenantRuntime {
263
256
  }
264
257
  /**
265
258
  * The Durable Streams seam's status for Tenant status and readiness: S2 reachability, the
266
- * basin, outbox depth and relay lag (`streamsStatus` in `tenant/streams.ts`).
259
+ * basin, its generation and the Tenant's relay (`streamsStatus` in `tenant/streams.ts`).
267
260
  */
268
261
  streamsStatus() {
269
262
  return streamsStatus(this.ctx);
@@ -278,15 +271,39 @@ export class TenantRuntime {
278
271
  // A shutdown abort: the advances leave their sessions to the next advance, unsettled.
279
272
  abortAll(ctx, "shutdown");
280
273
  const idleBy = Date.now() + this.closeGraceMs;
281
- await this.detach();
282
- await ctx.mcp.close();
283
- endAllStreams(ctx.live);
284
- // Bounded: an advance that ignores its abort is abandoned; its lease lapses (§11.4).
285
- await waitForIdle(ctx, Math.max(0, idleBy - Date.now()));
286
- await ctx.sandbox.close();
287
- ctx.closed = true;
288
- await closeStreams(ctx);
289
- await ctx.store.close();
274
+ // Every step runs even when one before it fails, so nothing is left running; the first
275
+ // error is rethrown at the end.
276
+ const steps = [
277
+ ["detach", () => this.detach()],
278
+ ["mcp", () => ctx.mcp.close()],
279
+ ["session streams", () => endAllStreams(ctx.sessionStreams)],
280
+ // Bounded: an advance that ignores its abort is abandoned; its lease lapses (§11.4).
281
+ ["idle", () => waitForIdle(ctx, Math.max(0, idleBy - Date.now()))],
282
+ ["sandbox", () => ctx.sandbox.close()],
283
+ [
284
+ "streams",
285
+ () => {
286
+ ctx.closed = true;
287
+ return closeStreams(ctx);
288
+ },
289
+ ],
290
+ ["store", () => ctx.store.close()],
291
+ ];
292
+ let failure;
293
+ for (const [step, run] of steps) {
294
+ try {
295
+ await run();
296
+ }
297
+ catch (error) {
298
+ ctx.config.logger.warn("tenant close step failed", {
299
+ step,
300
+ message: error instanceof Error ? error.message : String(error),
301
+ });
302
+ failure ??= { error };
303
+ }
304
+ }
305
+ if (failure)
306
+ throw failure.error;
290
307
  }
291
308
  }
292
309
  /**
@@ -3,11 +3,16 @@ import { type AdvanceAbortKind } from "./worker.js";
3
3
  export interface WorkState {
4
4
  /** Advances running on this process, by session id. */
5
5
  readonly running: Map<string, AbortController>;
6
+ /** Deliveries to Action endpoints in flight on this process, by their Action's session id. */
7
+ readonly deliveries: Map<string, Set<AbortController>>;
8
+ /** When each Action last reported a failed delivery (`action.delivery_failed` throttling). */
9
+ readonly deliveryNotices: Map<string, number>;
6
10
  }
7
11
  export declare function createWorkState(): WorkState;
8
12
  /**
9
- * Abort the advance of `id` if it runs on this process. Cancel calls it after committing
10
- * `cancelled`, and so does the control stream for cancels made elsewhere.
13
+ * Abort the advance of `id` if it runs on this process, and its deliveries to Action endpoints.
14
+ * Cancel calls it after committing `cancelled`, and so does the control stream for cancels made
15
+ * elsewhere.
11
16
  */
12
17
  export declare function abortLocal(ctx: TenantContext, id: string, kind?: AdvanceAbortKind): void;
13
18
  /** Reset: abort every advance running on this process; their sessions are being cleared. */
@@ -13,7 +13,7 @@ import { randomUUID } from "node:crypto";
13
13
  import { command } from "./commands.js";
14
14
  import { AdvanceAbort } from "./worker.js";
15
15
  export function createWorkState() {
16
- return { running: new Map() };
16
+ return { running: new Map(), deliveries: new Map(), deliveryNotices: new Map() };
17
17
  }
18
18
  const MESSAGES = {
19
19
  cancel: "Turn cancelled",
@@ -26,13 +26,17 @@ function abortWith(controller, kind) {
26
26
  controller.abort(new AdvanceAbort(kind, MESSAGES[kind]));
27
27
  }
28
28
  /**
29
- * Abort the advance of `id` if it runs on this process. Cancel calls it after committing
30
- * `cancelled`, and so does the control stream for cancels made elsewhere.
29
+ * Abort the advance of `id` if it runs on this process, and its deliveries to Action endpoints.
30
+ * Cancel calls it after committing `cancelled`, and so does the control stream for cancels made
31
+ * elsewhere.
31
32
  */
32
33
  export function abortLocal(ctx, id, kind = "cancel") {
33
34
  const controller = ctx.work.running.get(id);
34
35
  if (controller)
35
36
  abortWith(controller, kind);
37
+ // A cancel also closes the session's deliveries, which aborts the tools' `ctx.signal`.
38
+ for (const delivery of ctx.work.deliveries.get(id) ?? [])
39
+ abortWith(delivery, kind);
36
40
  }
37
41
  /** Reset: abort every advance running on this process; their sessions are being cleared. */
38
42
  export function clearWork(ctx) {