@nylorun/runtime 0.12.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 +361 -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 +19 -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 +121 -129
  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 +138 -48
  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,9 @@
1
+ /**
2
+ * The Tenant's typed rejections: what a route or the code it calls throws for the client to
3
+ * see (`api/http/respond.ts` answers it), and the opaque 404 for unknown credentials (D5).
4
+ *
5
+ * Later waves: stable; the streams seam (Wave 2 / Y) keeps using these helpers.
6
+ */
1
7
  /** D5 opaque failure for unknown/rejected credentials on Tenant routes. */
2
8
  export const OPAQUE_NOT_FOUND = {
3
9
  status: "rejected",
@@ -7,14 +13,18 @@ export const OPAQUE_NOT_FOUND = {
7
13
  export class HttpError extends Error {
8
14
  status;
9
15
  rejection;
10
- constructor(status, message, rejection = {}) {
16
+ headers;
17
+ constructor(status, message, rejection = {},
18
+ /** Response headers the rejection carries, such as `retry-after` or `www-authenticate`. */
19
+ headers = {}) {
11
20
  super(message);
12
21
  this.status = status;
13
22
  this.rejection = rejection;
23
+ this.headers = headers;
14
24
  }
15
25
  }
16
- export const fail = (status, message, rejection) => {
17
- throw new HttpError(status, message, rejection);
26
+ export const fail = (status, message, rejection, headers) => {
27
+ throw new HttpError(status, message, rejection, headers);
18
28
  };
19
29
  export class OpaqueAuthError extends Error {
20
30
  status = 404;
@@ -27,27 +37,3 @@ export class OpaqueAuthError extends Error {
27
37
  export const failOpaque = () => {
28
38
  throw new OpaqueAuthError();
29
39
  };
30
- /** AbortSignal tied to the HTTP request being closed by the client. */
31
- export function requestAborted(request) {
32
- const controller = new AbortController();
33
- request.on("close", () => {
34
- if (!request.complete)
35
- controller.abort();
36
- });
37
- return controller.signal;
38
- }
39
- /** Read a JSON request body, capped at 1 MiB. */
40
- export async function readBody(request) {
41
- let data = "";
42
- for await (const chunk of request) {
43
- data += chunk;
44
- if (Buffer.byteLength(data) > 1024 * 1024)
45
- fail(413, "Request too large");
46
- }
47
- try {
48
- return JSON.parse(data);
49
- }
50
- catch {
51
- return fail(400, "Invalid JSON");
52
- }
53
- }
@@ -0,0 +1,16 @@
1
+ export declare const MAX_TOKEN_BYTES = 4096;
2
+ export declare const CLOCK_TOLERANCE_SECONDS = 30;
3
+ /** True when `bearer` has the shape of a JWT; application keys never do. */
4
+ export declare function looksLikeToken(bearer: string): boolean;
5
+ /** The `typ` of a JWT-shaped bearer, or `undefined` when its header cannot be read. */
6
+ export declare function tokenType(raw: string): string | undefined;
7
+ /**
8
+ * Why the header of a token of type `typ` is refused before any key is read, or its key id
9
+ * when it is not: `malformed` (size, shape, undecodable) or `header` (algorithm, type, key id,
10
+ * or a header that could name another key).
11
+ */
12
+ export declare function tokenKeyId(raw: string, typ: string): {
13
+ kid: string;
14
+ } | {
15
+ refused: "malformed" | "header";
16
+ };
@@ -0,0 +1,50 @@
1
+ /**
2
+ * What subject tokens and delivery tokens share: the Tenant's ES256 JWTs, recognised by shape,
3
+ * told apart by their `typ`, and refused before any key is read when the header is not one the
4
+ * Runtime issues.
5
+ */
6
+ import { decodeProtectedHeader } from "jose";
7
+ import { SIGNING_KEY_ID_PATTERN } from "@nylorun/core/compatibility";
8
+ export const MAX_TOKEN_BYTES = 4096;
9
+ export const CLOCK_TOLERANCE_SECONDS = 30;
10
+ const JWT_SHAPE = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
11
+ const FORBIDDEN_HEADERS = ["jku", "jwk", "x5u", "x5c", "x5t", "crit"];
12
+ /** True when `bearer` has the shape of a JWT; application keys never do. */
13
+ export function looksLikeToken(bearer) {
14
+ return JWT_SHAPE.test(bearer);
15
+ }
16
+ /** The `typ` of a JWT-shaped bearer, or `undefined` when its header cannot be read. */
17
+ export function tokenType(raw) {
18
+ if (Buffer.byteLength(raw) > MAX_TOKEN_BYTES || !JWT_SHAPE.test(raw))
19
+ return undefined;
20
+ try {
21
+ const typ = decodeProtectedHeader(raw).typ;
22
+ return typeof typ === "string" ? typ : undefined;
23
+ }
24
+ catch {
25
+ return undefined;
26
+ }
27
+ }
28
+ /**
29
+ * Why the header of a token of type `typ` is refused before any key is read, or its key id
30
+ * when it is not: `malformed` (size, shape, undecodable) or `header` (algorithm, type, key id,
31
+ * or a header that could name another key).
32
+ */
33
+ export function tokenKeyId(raw, typ) {
34
+ if (Buffer.byteLength(raw) > MAX_TOKEN_BYTES || !JWT_SHAPE.test(raw))
35
+ return { refused: "malformed" };
36
+ let header;
37
+ try {
38
+ header = decodeProtectedHeader(raw);
39
+ }
40
+ catch {
41
+ return { refused: "malformed" };
42
+ }
43
+ if (header.alg !== "ES256" ||
44
+ header.typ !== typ ||
45
+ typeof header.kid !== "string" ||
46
+ !SIGNING_KEY_ID_PATTERN.test(header.kid) ||
47
+ FORBIDDEN_HEADERS.some((name) => name in header))
48
+ return { refused: "header" };
49
+ return { kid: header.kid };
50
+ }
@@ -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. */