@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,29 +1,28 @@
1
1
  /**
2
- * `startEphemeralRuntime`: a private, in-process Host on port 0 with one Tenant, for tests and
3
- * embeds that need the Runtime's HTTP API without the Docker stack. Nothing in it is durable:
4
- * its Tenants live in memory (the memory Session Store and memory Durable Streams) and are gone
5
- * after `close()`, or in the Postgres database a test passes. Scheduling is the in-process
6
- * execution each Tenant starts for itself.
2
+ * `startEphemeralRuntime`: a private, in-process Host on port 0 serving one Tenant, for tests
3
+ * and embeds that need the Runtime's HTTP API without Restate and S2. Its Tenant is the one
4
+ * the Postgres database the caller passes holds (`store/postgres/tenant.ts`), created there on
5
+ * first start through the same path as a Host's; there is no in-memory Session Store. The rest
6
+ * is not durable: Durable Streams are in memory and gone after `close()`, and scheduling is
7
+ * the in-process execution the Tenant starts for itself.
7
8
  *
8
- * It is not the temporary Tenant the smoke checks use (scripts/lib/temporary-tenant.mjs):
9
- * that one is created on the running stack, with the Tenant-level fixture model
10
- * (`model-setting.ts`), and deleted afterwards.
9
+ * The smoke checks do not use it: they reset the Tenant of a temporary Docker stack and seed
10
+ * the Tenant-level fixture model (`model-setting.ts`, scripts/lib/stack-tenant.mjs).
11
11
  */
12
12
  import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
13
13
  import { randomBytes } from "node:crypto";
14
14
  import { createRequire } from "node:module";
15
15
  import { newTenantId } from "@nylorun/core/compatibility";
16
- import { hashToken, mintBearerToken } from "../core/executors.js";
16
+ import { mintBearerToken } from "../core/bearer.js";
17
17
  import { createHost } from "../host/create-host.js";
18
18
  import { createKekFile } from "../vault/kek.js";
19
- import { MemorySessionStore } from "../store/memory.js";
20
19
  import { createTenantModule } from "./module.js";
21
- import { createMemoryTenantStore } from "./store-memory.js";
22
- import { createPostgresTenantStore } from "./store-pg.js";
20
+ import { createPostgresTenantOpener } from "./store-pg.js";
21
+ import { createPostgresClient, } from "../store/postgres/connect.js";
23
22
  import { MemoryStreams } from "../streams/memory.js";
24
23
  import { createTenantLogger } from "./logger.js";
25
24
  import { hostPaths, tenantPaths } from "./paths.js";
26
- import { bootstrapPrincipal } from "./principals.js";
25
+ import { hostPrincipals } from "./principals.js";
27
26
  import { openTenantRuntime } from "./runtime.js";
28
27
  const nodeRequire = createRequire(import.meta.url);
29
28
  function coreVersion() {
@@ -38,69 +37,8 @@ function newHostId() {
38
37
  return `host_${newTenantId().slice(3)}`;
39
38
  }
40
39
  /**
41
- * A Tenant store whose Tenants live in memory: one memory Session Store per Tenant, created
42
- * with its bootstrap principals, kept across closes and re-opens, dropped on delete.
43
- */
44
- function memoryTenants(options) {
45
- const stores = new Map();
46
- const inner = createMemoryTenantStore({
47
- configFor: options.configFor,
48
- logger: options.logger,
49
- openRuntime: async (config) => {
50
- const store = stores.get(config.tenantId);
51
- if (!store)
52
- throw new Error(`Tenant ${config.tenantId} has no Session Store`);
53
- return openTenantRuntime(config, {
54
- createKekIfMissing: true,
55
- streams: options.streams,
56
- store: kept(store),
57
- envelope: await inner.readEnvelope(config.tenantId),
58
- });
59
- },
60
- });
61
- const store = {
62
- ...inner,
63
- async create(envelope, bootstrap) {
64
- const created = await inner.create(envelope, bootstrap);
65
- if (created === "created") {
66
- const sessions = new MemorySessionStore({ tenantId: envelope.id });
67
- await sessions.tx((t) => bootstrapPrincipal(t, bootstrap));
68
- stores.set(envelope.id, sessions);
69
- }
70
- return created;
71
- },
72
- async trash(id, now) {
73
- await inner.trash(id, now);
74
- await stores.get(id)?.close();
75
- stores.delete(id);
76
- },
77
- async removePartial(id) {
78
- await inner.removePartial(id);
79
- await stores.get(id)?.close();
80
- stores.delete(id);
81
- },
82
- };
83
- return {
84
- store,
85
- async close() {
86
- for (const sessions of stores.values())
87
- await sessions.close();
88
- stores.clear();
89
- },
90
- };
91
- }
92
- /** The Tenant closes its store on close; a memory store must outlive that for a re-open. */
93
- function kept(store) {
94
- return {
95
- tenantId: store.tenantId,
96
- tx: (fn) => store.tx(fn),
97
- onCommit: (listener) => store.onCommit(listener),
98
- health: () => store.health(),
99
- close: async () => { },
100
- };
101
- }
102
- /**
103
- * Private Host on port 0 with one Tenant (D9 / A19). Not durable (see the module comment).
40
+ * Private Host on port 0 serving one Tenant (D9 / A19). Durable only in its database (see the
41
+ * module comment).
104
42
  * Exported from `@nylorun/runtime` and `@nylorun/runtime/core`.
105
43
  */
106
44
  export async function startEphemeralRuntime(options) {
@@ -108,8 +46,6 @@ export async function startEphemeralRuntime(options) {
108
46
  const paths = hostPaths(hostRoot);
109
47
  mkdirSync(paths.home, { recursive: true });
110
48
  mkdirSync(paths.tmp, { recursive: true });
111
- mkdirSync(paths.tenants, { recursive: true });
112
- mkdirSync(paths.trash, { recursive: true });
113
49
  const hostId = newHostId();
114
50
  const adminKey = options.adminKey ?? randomBytes(32).toString("hex");
115
51
  const hostConfig = {
@@ -131,7 +67,7 @@ export async function startEphemeralRuntime(options) {
131
67
  const baseline = options.baseline ?? {};
132
68
  const model = options.model ?? { kind: "scripted", output: "ok" };
133
69
  const configFor = (tenantId) => {
134
- const tenant = tenantPaths(hostRoot, tenantId);
70
+ const tenant = tenantPaths(hostRoot);
135
71
  // A seeded `sandbox.backend` setting overrides this when the Tenant opens.
136
72
  const sandboxBackend = options.sandboxBackend ?? "virtual";
137
73
  return {
@@ -152,13 +88,26 @@ export async function startEphemeralRuntime(options) {
152
88
  };
153
89
  };
154
90
  const streams = new MemoryStreams();
155
- const memory = options.database
156
- ? undefined
157
- : memoryTenants({ configFor, streams, logger });
158
- const store = options.database
159
- ? createPostgresTenantStore({
91
+ const ownPool = typeof options.database === "string"
92
+ ? createPostgresClient(options.database)
93
+ : undefined;
94
+ const applicationKey = options.applicationKey ?? mintBearerToken();
95
+ const principalId = options.principalId ?? `principal_${randomBytes(8).toString("hex")}`;
96
+ const module = createTenantModule({
97
+ open: createPostgresTenantOpener({
160
98
  hostRoot,
161
- sql: options.database,
99
+ sql: ownPool ?? options.database,
100
+ create: {
101
+ tenantId: options.tenantId ?? newTenantId(),
102
+ name: options.name ?? "ephemeral",
103
+ principals: hostPrincipals({
104
+ adminKey,
105
+ application: { principalId, key: applicationKey },
106
+ ...(options.studioCredentialHash
107
+ ? { studioCredentialHash: options.studioCredentialHash }
108
+ : {}),
109
+ }),
110
+ },
162
111
  configFor,
163
112
  logger,
164
113
  openRuntime: (config, opened) => openTenantRuntime(config, {
@@ -166,44 +115,58 @@ export async function startEphemeralRuntime(options) {
166
115
  streams,
167
116
  ...opened,
168
117
  }),
169
- })
170
- : memory.store;
171
- const module = createTenantModule({
172
- store,
118
+ }),
173
119
  logger,
174
- onDeleted: (tenantId) => streams.deleteTenant(tenantId),
175
120
  });
176
- await module.start();
177
- const tenantId = options.tenantId ?? newTenantId();
178
- const applicationKey = options.applicationKey ?? mintBearerToken();
179
- const principalId = options.principalId ?? `principal_${randomBytes(8).toString("hex")}`;
180
- const credentialHash = hashToken(applicationKey);
181
- await module.create({
182
- tenantId,
183
- name: options.name ?? "ephemeral",
184
- principalId,
185
- credentialHash,
186
- idempotencyKey: `ephemeral-${tenantId}`,
187
- ...(options.studioCredentialHash
188
- ? { studioCredentialHash: options.studioCredentialHash }
189
- : {}),
190
- });
191
- // KEK for first vault write; openTenantRuntime also creates when hooks allow.
192
- const kekPath = tenantPaths(hostRoot, tenantId).kek;
193
- if (!existsSync(kekPath))
194
- createKekFile(kekPath);
195
- const host = createHost({
196
- hostRoot,
197
- module,
198
- config: hostConfig,
199
- credentials,
200
- logger,
201
- coreVersion: coreVersion(),
202
- });
203
- await host.listen();
121
+ let host;
122
+ /** Closes what was opened, in reverse order; `close()` and a failed start share it. */
123
+ const release = async () => {
124
+ await host?.close();
125
+ await module.close();
126
+ await streams.close();
127
+ await ownPool?.end({ timeout: 5 });
128
+ if (!options.retainRoot)
129
+ rmSync(hostRoot, { recursive: true, force: true });
130
+ };
131
+ let tenantId;
132
+ try {
133
+ // KEK for first vault write; openTenantRuntime also creates when hooks allow.
134
+ const kekPath = tenantPaths(hostRoot).kek;
135
+ mkdirSync(tenantPaths(hostRoot).root, { recursive: true, mode: 0o700 });
136
+ if (!existsSync(kekPath))
137
+ createKekFile(kekPath);
138
+ host = createHost({
139
+ hostRoot,
140
+ module,
141
+ config: hostConfig,
142
+ credentials,
143
+ logger,
144
+ coreVersion: coreVersion(),
145
+ browserAccess: options.browserAccess === true,
146
+ ...(options.operatorListener
147
+ ? { operator: { host: "127.0.0.1", port: 0 } }
148
+ : {}),
149
+ });
150
+ // Opens the Tenant: creates it in the database first when it holds none.
151
+ await host.listen();
152
+ const tenant = module.tenant();
153
+ if (tenant.state !== "open" || !tenant.id)
154
+ throw new Error(tenant.cause
155
+ ? `The Tenant could not be opened (${tenant.cause.code}): ${tenant.cause.message}`
156
+ : "The Tenant could not be opened: its database is unavailable");
157
+ tenantId = tenant.id;
158
+ }
159
+ catch (error) {
160
+ await release().catch((cleanup) => logger.warn("ephemeral runtime cleanup failed", {
161
+ message: cleanup instanceof Error ? cleanup.message : String(cleanup),
162
+ }));
163
+ throw error;
164
+ }
165
+ const started = host;
204
166
  let closed = false;
205
167
  return {
206
- url: host.url,
168
+ url: started.url,
169
+ adminUrl: started.adminUrl,
207
170
  tenantId,
208
171
  applicationKey,
209
172
  adminKey,
@@ -213,12 +176,7 @@ export async function startEphemeralRuntime(options) {
213
176
  if (closed)
214
177
  return;
215
178
  closed = true;
216
- await host.close();
217
- await module.close();
218
- await memory?.close();
219
- await streams.close();
220
- if (!options.retainRoot)
221
- rmSync(hostRoot, { recursive: true, force: true });
179
+ await release();
222
180
  },
223
181
  };
224
182
  }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * One history (blueprint P0.3): the own loop's model-facing transcript is a fold of the
3
+ * session's record, not a copy on the session row.
4
+ *
5
+ * - **Write.** After a segment that keeps the engine's state (a rollover, or a settle that is
6
+ * not a failure), the Runtime appends `transcript.updated` events: an edit from the
7
+ * transcript the segment started with to the one the engine returned (`transcriptUpdates`).
8
+ * Normal steps only append; compaction replaces the prefix, so its edit is a snapshot.
9
+ * - **Fold.** `foldTranscript` applies the edits in order. `turn.cancelled` and `turn.failed`
10
+ * restore the transcript from before that turn's first edit, as cancel and failure restore
11
+ * `turnStartState`.
12
+ * - **Lean rows.** The session row stores the engine state with an empty transcript
13
+ * (`leanState`). `runSegment` folds the record from `Session.history.from` and hands the
14
+ * engine the full transcript (`withTranscript`).
15
+ * - **Shadow mode** (on in the runtime's tests, `test/setup/transcript-shadow.ts`) keeps the
16
+ * transcript on the row too and checks the fold against it at every segment start.
17
+ */
18
+ import type { LiveEvent } from "@nylorun/core/contracts";
19
+ /** The payload of one `transcript.updated` event. */
20
+ export interface TranscriptUpdate {
21
+ keep: number;
22
+ entries: unknown[];
23
+ length: number;
24
+ }
25
+ /** Where a session's fold starts. */
26
+ export interface SessionHistory {
27
+ /** The fold reads the record from this seq: a snapshot at or before the turn's start. */
28
+ from: number;
29
+ /** The latest snapshot (`keep: 0`), which `from` moves to when the next turn starts. */
30
+ snapshot?: number;
31
+ }
32
+ /** An edit's entries are split so each event stays well under S2's 1 MiB record limit. */
33
+ export declare const CHUNK_BYTES: number;
34
+ /** Shadow mode: keep the transcript on the session row and check the fold against it. */
35
+ export declare function transcriptShadow(): boolean;
36
+ /** Tests switch shadow mode off to exercise lean rows. Returns the previous setting. */
37
+ export declare function setTranscriptShadow(on: boolean): boolean;
38
+ export declare class TranscriptFoldError extends Error {
39
+ constructor(message: string);
40
+ }
41
+ /** The fold and the transcript on the row differ (shadow mode only). */
42
+ export declare class TranscriptParityError extends Error {
43
+ constructor(sessionId: string, at: number, folded: number, stored: number);
44
+ }
45
+ /** The engine state's transcript, or none. */
46
+ export declare function transcriptOf(state: unknown): unknown[];
47
+ /**
48
+ * The edits from `before` to `after`, split into chunks of at most `CHUNK_BYTES` of entries.
49
+ * Empty when nothing changed.
50
+ */
51
+ export declare function transcriptUpdates(before: readonly unknown[], after: readonly unknown[], chunkBytes?: number): TranscriptUpdate[];
52
+ /** Applies one edit. Throws `TranscriptFoldError` when it does not fit the transcript. */
53
+ export declare function applyUpdate(transcript: readonly unknown[], update: TranscriptUpdate): unknown[];
54
+ /**
55
+ * Folds a session's events, from the start of its record or from a snapshot, into the
56
+ * transcript the engine resumes from.
57
+ */
58
+ export declare function foldTranscript(events: Iterable<Pick<LiveEvent, "type" | "turnId" | "payload">>): unknown[];
59
+ /** `state` with an empty transcript (what the session row stores outside shadow mode). */
60
+ export declare function leanState<T>(state: T): T;
61
+ /** `state` with `transcript` (what the engine runs on). */
62
+ export declare function withTranscript<T>(state: T, transcript: readonly unknown[]): T;
63
+ /** In shadow mode, checks the fold against the transcript the row kept. */
64
+ export declare function checkParity(sessionId: string, folded: readonly unknown[], stored: readonly unknown[]): void;
@@ -0,0 +1,120 @@
1
+ /** An edit's entries are split so each event stays well under S2's 1 MiB record limit. */
2
+ export const CHUNK_BYTES = 256 * 1024;
3
+ let shadow = false;
4
+ /** Shadow mode: keep the transcript on the session row and check the fold against it. */
5
+ export function transcriptShadow() {
6
+ return shadow;
7
+ }
8
+ /** Tests switch shadow mode off to exercise lean rows. Returns the previous setting. */
9
+ export function setTranscriptShadow(on) {
10
+ const previous = shadow;
11
+ shadow = on;
12
+ return previous;
13
+ }
14
+ export class TranscriptFoldError extends Error {
15
+ constructor(message) {
16
+ super(message);
17
+ this.name = "TranscriptFoldError";
18
+ }
19
+ }
20
+ /** The fold and the transcript on the row differ (shadow mode only). */
21
+ export class TranscriptParityError extends Error {
22
+ constructor(sessionId, at, folded, stored) {
23
+ super(`Session ${sessionId}: the folded transcript differs from the stored one at entry ${at}` +
24
+ ` (folded ${folded} entries, stored ${stored})`);
25
+ this.name = "TranscriptParityError";
26
+ }
27
+ }
28
+ const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
29
+ /** The engine state's transcript, or none. */
30
+ export function transcriptOf(state) {
31
+ const transcript = state?.transcript;
32
+ return Array.isArray(transcript) ? transcript : [];
33
+ }
34
+ /**
35
+ * The edits from `before` to `after`, split into chunks of at most `CHUNK_BYTES` of entries.
36
+ * Empty when nothing changed.
37
+ */
38
+ export function transcriptUpdates(before, after, chunkBytes = CHUNK_BYTES) {
39
+ let keep = 0;
40
+ while (keep < before.length && keep < after.length && same(before[keep], after[keep]))
41
+ keep += 1;
42
+ if (keep === before.length && keep === after.length)
43
+ return [];
44
+ const added = after.slice(keep);
45
+ if (added.length === 0)
46
+ return [{ keep, entries: [], length: keep }];
47
+ const updates = [];
48
+ let chunk = [];
49
+ let bytes = 0;
50
+ let base = keep;
51
+ const flush = () => {
52
+ updates.push({ keep: base, entries: chunk, length: base + chunk.length });
53
+ base += chunk.length;
54
+ chunk = [];
55
+ bytes = 0;
56
+ };
57
+ for (const entry of added) {
58
+ const size = JSON.stringify(entry).length;
59
+ if (chunk.length > 0 && bytes + size > chunkBytes)
60
+ flush();
61
+ chunk.push(entry);
62
+ bytes += size;
63
+ }
64
+ flush();
65
+ return updates;
66
+ }
67
+ /** Applies one edit. Throws `TranscriptFoldError` when it does not fit the transcript. */
68
+ export function applyUpdate(transcript, update) {
69
+ if (update.keep > transcript.length)
70
+ throw new TranscriptFoldError(`transcript.updated keeps ${update.keep} entries of ${transcript.length}`);
71
+ const next = [...transcript.slice(0, update.keep), ...update.entries];
72
+ if (next.length !== update.length)
73
+ throw new TranscriptFoldError(`transcript.updated should give ${update.length} entries, not ${next.length}`);
74
+ return next;
75
+ }
76
+ /**
77
+ * Folds a session's events, from the start of its record or from a snapshot, into the
78
+ * transcript the engine resumes from.
79
+ */
80
+ export function foldTranscript(events) {
81
+ let transcript = [];
82
+ // The transcript before the current turn's first edit: what a cancel or failure restores.
83
+ let turn;
84
+ for (const event of events) {
85
+ if (event.type === "transcript.updated") {
86
+ if (!turn || turn.id !== event.turnId)
87
+ turn = { id: event.turnId, before: transcript };
88
+ transcript = applyUpdate(transcript, event.payload);
89
+ }
90
+ else if (event.type === "turn.cancelled" || event.type === "turn.failed") {
91
+ if (turn && turn.id === event.turnId)
92
+ transcript = turn.before;
93
+ turn = undefined;
94
+ }
95
+ else if (event.type === "turn.completed")
96
+ turn = undefined;
97
+ }
98
+ return transcript;
99
+ }
100
+ /** `state` with an empty transcript (what the session row stores outside shadow mode). */
101
+ export function leanState(state) {
102
+ if (shadow || !state || transcriptOf(state).length === 0)
103
+ return state;
104
+ return { ...state, transcript: [] };
105
+ }
106
+ /** `state` with `transcript` (what the engine runs on). */
107
+ export function withTranscript(state, transcript) {
108
+ if (!state)
109
+ return state;
110
+ return { ...state, transcript: [...transcript] };
111
+ }
112
+ /** In shadow mode, checks the fold against the transcript the row kept. */
113
+ export function checkParity(sessionId, folded, stored) {
114
+ if (!shadow)
115
+ return;
116
+ const length = Math.max(folded.length, stored.length);
117
+ for (let i = 0; i < length; i += 1)
118
+ if (!same(folded[i], stored[i]))
119
+ throw new TranscriptParityError(sessionId, i, folded.length, stored.length);
120
+ }
@@ -1,10 +1,9 @@
1
1
  /**
2
- * HTTP plumbing shared by the Tenant route modules: typed rejections, the opaque 404 for
3
- * unknown credentials (D5), request-body parsing and the client-abort signal.
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
4
  *
5
5
  * Later waves: stable; the streams seam (Wave 2 / Y) keeps using these helpers.
6
6
  */
7
- import type { IncomingMessage } from "node:http";
8
7
  /** D5 opaque failure for unknown/rejected credentials on Tenant routes. */
9
8
  export declare const OPAQUE_NOT_FOUND: {
10
9
  status: "rejected";
@@ -19,9 +18,13 @@ export interface Rejection {
19
18
  export declare class HttpError extends Error {
20
19
  readonly status: number;
21
20
  readonly rejection: Rejection;
22
- constructor(status: number, message: string, rejection?: Rejection);
21
+ /** Response headers the rejection carries, such as `retry-after` or `www-authenticate`. */
22
+ readonly headers: Readonly<Record<string, string>>;
23
+ constructor(status: number, message: string, rejection?: Rejection,
24
+ /** Response headers the rejection carries, such as `retry-after` or `www-authenticate`. */
25
+ headers?: Readonly<Record<string, string>>);
23
26
  }
24
- export declare const fail: (status: number, message: string, rejection?: Rejection) => never;
27
+ export declare const fail: (status: number, message: string, rejection?: Rejection, headers?: Readonly<Record<string, string>>) => never;
25
28
  export declare class OpaqueAuthError extends Error {
26
29
  readonly status = 404;
27
30
  readonly body: {
@@ -32,7 +35,3 @@ export declare class OpaqueAuthError extends Error {
32
35
  constructor();
33
36
  }
34
37
  export declare const failOpaque: () => never;
35
- /** AbortSignal tied to the HTTP request being closed by the client. */
36
- export declare function requestAborted(request: IncomingMessage): AbortSignal;
37
- /** Read a JSON request body, capped at 1 MiB. */
38
- export declare function readBody(request: IncomingMessage): Promise<unknown>;
@@ -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
+ }
@@ -2,8 +2,8 @@
2
2
  * The Tenant-level fixture model: Tenant setting `model.fixture`, seeded with
3
3
  * `fixtureModel: true` (`PUT /v1/tenant/config/seed`, Host feature `tenant-fixture-model`).
4
4
  * A Tenant with it answers every model call with the Runtime's deterministic fixture model
5
- * (`core/provider.ts` `toolFixtureModel`) instead of its configured model, whatever the Host
6
- * uses for other Tenants. Temporary test Tenants seed it (scripts/lib/temporary-tenant.mjs).
5
+ * (`core/provider.ts` `toolFixtureModel`) instead of its configured model. Smoke checks seed
6
+ * it (scripts/lib/stack-tenant.mjs).
7
7
  *
8
8
  * The setting is read once per advance, in a transaction the advance makes anyway, so a Tenant
9
9
  * seeded on another node takes it at its next advance.