@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
@@ -3,34 +3,49 @@ import { countActiveFlowWork, deriveAgentEffectSessionId, isFlowEffect, isFlowTo
3
3
  import { mayDispatchMore } from "../core/limits.js";
4
4
  import { canonical } from "../store/canonical.js";
5
5
  import { isOwnershipLost } from "../store/ownership.js";
6
- import { newStreamIncarnation } from "../streams/types.js";
7
6
  import { piModel } from "../model/pi-model.js";
8
7
  import { scrub } from "../redact.js";
9
8
  import { serversOf } from "../mcp/pool.js";
10
9
  import { sandboxCapabilityOf } from "../sandbox/manager.js";
11
- import { owningSandboxSessionId, sandboxSpecOf } from "../sandbox/share.js";
10
+ import { owningSandboxSessionId, sandboxSpecOf, sessionSandboxSpec, } from "../sandbox/share.js";
11
+ import { withSandboxCapability } from "../sandbox/session-sandbox.js";
12
12
  import { loadSession, ownedSession, sandboxLookup, sessionOf, } from "./context.js";
13
13
  import { fail } from "./http.js";
14
14
  import { actionTarget, linkedAgentOutput, manifestFor, mcpToolOf, pinnedTool, turnManifestOf, } from "./session.js";
15
15
  import { command } from "./commands.js";
16
- import { assistantMessage, toolCompleted, toolIds } from "./transcript.js";
16
+ import { offerAction } from "./delivery.js";
17
+ import { assistantMessage, contextCompacted, modelFailed, toolCompleted, toolIds, } from "./transcript.js";
18
+ import { classifyThrown } from "../model/classify.js";
17
19
  import { abortKind } from "./worker.js";
18
- export function invokeModel(ctx, request, signal, model) {
19
- if (model)
20
- return model(request, signal);
21
- if (!ctx.useVaultModel)
22
- return ctx.modelProvider(request, signal);
23
- const adapter = piModel({
24
- root: ctx.config.paths.home,
25
- readHostModel: () => ctx.vault.readHostModel(),
26
- writeHostCredential: (credential) => ctx.vault.updateHostCredential(credential),
27
- });
28
- return adapter(request.input, {
29
- request: request.context.request,
30
- invocationId: String(request.context.invocationId),
31
- signal,
32
- reportPreparedCall() { },
33
- });
20
+ /**
21
+ * Call the model for one effect. A provider failure comes back as a failure outcome
22
+ * (Model Calls §6), whichever provider serves the call; only an abort throws, and the
23
+ * advance decides what the abort means.
24
+ */
25
+ export async function invokeModel(ctx, request, signal, model) {
26
+ try {
27
+ if (model)
28
+ return await model(request, signal);
29
+ if (!ctx.useVaultModel)
30
+ return await ctx.modelProvider(request, signal);
31
+ const adapter = piModel({
32
+ root: ctx.config.paths.home,
33
+ readHostModel: () => ctx.vault.readHostModel(),
34
+ writeHostCredential: (credential) => ctx.vault.updateHostCredential(credential),
35
+ ...(ctx.config.modelCall ? { settings: ctx.config.modelCall } : {}),
36
+ });
37
+ return await adapter(request.input, {
38
+ request: request.context.request,
39
+ invocationId: String(request.context.invocationId),
40
+ signal,
41
+ reportPreparedCall() { },
42
+ });
43
+ }
44
+ catch (error) {
45
+ if (signal.aborted)
46
+ throw error;
47
+ return classifyThrown(error);
48
+ }
34
49
  }
35
50
  /**
36
51
  * The outcome of a flow `agent` effect once the linked turn it started ended, or undefined
@@ -159,8 +174,6 @@ export async function resolveEffect(ctx, request, signal, lease, segment = {}) {
159
174
  context: request.context,
160
175
  status: "pending",
161
176
  generation: 0,
162
- claimId: null,
163
- leaseExpiresAt: null,
164
177
  ...(request.agent ? { agent: request.agent } : {}),
165
178
  };
166
179
  const action = request.kind === "hook"
@@ -189,7 +202,7 @@ export async function resolveEffect(ctx, request, signal, lease, segment = {}) {
189
202
  ...(action.kind === "tool" ? toolIds(request.context) : {}),
190
203
  input: action.input,
191
204
  });
192
- t.signalWork();
205
+ await offerAction(t, ctx, action);
193
206
  return resolved({ status: "pending" });
194
207
  });
195
208
  if (journaled.kind === "resolved")
@@ -219,11 +232,23 @@ export async function resolveEffect(ctx, request, signal, lease, segment = {}) {
219
232
  effect.status = "completed";
220
233
  effect.outcome = { value };
221
234
  await t.put("effects", request.effectId, effect);
222
- const transcript = invoke === "model"
223
- ? assistantMessage(request, value)
224
- : toolCompleted(request, value);
225
- if (transcript)
226
- await t.event(s.id, request.turnId, invoke === "model" ? "message.assistant" : "tool.completed", transcript);
235
+ const failed = invoke === "model" ? modelFailed(request, value) : undefined;
236
+ // A summary call is never an assistant message; the last one publishes context.compacted.
237
+ const summarizing = invoke === "model" && Boolean(request.context.compaction);
238
+ const compacted = summarizing && !failed ? contextCompacted(request, value) : undefined;
239
+ const transcript = failed || summarizing
240
+ ? undefined
241
+ : invoke === "model"
242
+ ? assistantMessage(request, value)
243
+ : toolCompleted(request, value);
244
+ if (failed)
245
+ await t.event(s.id, request.turnId, "model.failed", failed);
246
+ if (compacted)
247
+ await t.event(s.id, request.turnId, "context.compacted", compacted);
248
+ if (transcript && invoke === "model")
249
+ await t.event(s.id, request.turnId, "message.assistant", transcript);
250
+ else if (transcript)
251
+ await t.event(s.id, request.turnId, "tool.completed", transcript);
227
252
  return { status: "completed", outcome: effect.outcome };
228
253
  });
229
254
  }
@@ -312,8 +337,6 @@ export async function resolveNewFlowEffect(ctx, request, signal, lease) {
312
337
  context: request.context,
313
338
  status: "pending",
314
339
  generation: 0,
315
- claimId: null,
316
- leaseExpiresAt: null,
317
340
  kind: request.kind,
318
341
  path: request.path,
319
342
  key: request.key,
@@ -329,8 +352,6 @@ export async function resolveNewFlowEffect(ctx, request, signal, lease) {
329
352
  context: request.context,
330
353
  status: "pending",
331
354
  generation: 0,
332
- claimId: null,
333
- leaseExpiresAt: null,
334
355
  kind: "tool",
335
356
  path: request.path,
336
357
  key: request.key,
@@ -353,7 +374,7 @@ export async function resolveNewFlowEffect(ctx, request, signal, lease) {
353
374
  key: action.key,
354
375
  input: action.input,
355
376
  });
356
- t.signalWork();
377
+ await offerAction(t, ctx, action);
357
378
  return resolved({ status: "pending" });
358
379
  }
359
380
  return { kind: "agent", workflow };
@@ -388,15 +409,18 @@ export async function resolveNewFlowEffect(ctx, request, signal, lease) {
388
409
  if (exists)
389
410
  return;
390
411
  const definition = await leafDefinition(t, workflow, body, request);
391
- const sandboxOwnerId = sandboxSpecOf(workflow.manifest) || workflow.sandboxOwnerId
392
- ? owningSandboxSessionId(workflow, await sandboxLookup(t, workflow.sandboxOwnerId))
412
+ const lookup = await sandboxLookup(t, workflow.sandboxOwnerId);
413
+ const sandboxOwnerId = sessionSandboxSpec(workflow) || workflow.sandboxOwnerId
414
+ ? owningSandboxSessionId(workflow, lookup)
393
415
  : undefined;
416
+ // A sandbox chosen when the tree was opened reaches every agent in it, with its tools.
417
+ const inherited = inheritedSandbox(sandboxOwnerId === undefined ? undefined : lookup(sandboxOwnerId) ?? workflow, definition);
394
418
  const created = {
395
419
  id: agentSessionId,
396
420
  agentId: body.agentId,
397
421
  ownerUserId: workflow.ownerUserId,
398
- manifest: definition.manifest,
399
- manifestHash: definition.manifestHash,
422
+ manifest: inherited?.manifest ?? definition.manifest,
423
+ manifestHash: inherited?.manifestHash ?? definition.manifestHash,
400
424
  implementationVersion: definition.implementationVersion,
401
425
  status: "idle",
402
426
  activeTurnId: null,
@@ -410,7 +434,9 @@ export async function resolveNewFlowEffect(ctx, request, signal, lease) {
410
434
  credentialSelections: workflow.credentialSelections,
411
435
  pluginRoots: definition.pluginRoots ?? {},
412
436
  ...(sandboxOwnerId ? { sandboxOwnerId } : {}),
413
- streamIncarnation: newStreamIncarnation(),
437
+ ...(inherited
438
+ ? { sandbox: inherited.spec, sandboxSource: "shared" }
439
+ : {}),
414
440
  };
415
441
  await t.put("sessions", agentSessionId, created);
416
442
  });
@@ -534,6 +560,21 @@ async function callMcpTool(ctx, request) {
534
560
  pluginRoots: s.pluginRoots ?? {},
535
561
  });
536
562
  }
563
+ /**
564
+ * The sandbox a linked session inherits from its tree's owner when that sandbox was chosen at
565
+ * open (pinned on the owner). A definition that declares its own sandbox keeps it, as before.
566
+ */
567
+ function inheritedSandbox(owner, definition) {
568
+ const spec = owner?.sandbox;
569
+ if (!spec || sandboxSpecOf(definition.manifest))
570
+ return undefined;
571
+ if (isWorkflowManifest(definition.manifest))
572
+ return { spec };
573
+ const sandboxed = withSandboxCapability(definition.manifest, spec);
574
+ if (!sandboxed.ok)
575
+ throw new Error(sandboxed.message);
576
+ return { spec, manifest: sandboxed.manifest, manifestHash: sandboxed.manifestHash };
577
+ }
537
578
  async function callSandboxTool(ctx, request, signal) {
538
579
  const { s, ownerId } = await ctx.store.tx(async (t) => {
539
580
  const s = await sessionOf(t, request.sessionId);
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Action endpoints (design: Action endpoints §4.2): where the Runtime delivers each agent's
3
+ * Actions, registered with the application key.
4
+ */
5
+ import { type Endpoint, type PutEndpointsRequest } from "@nylorun/core/contracts";
6
+ import type { EndpointRow } from "../store/types.js";
7
+ import type { TenantContext } from "./context.js";
8
+ /** An endpoint as the Tenant API answers it. */
9
+ export declare function endpointView(row: EndpointRow): Endpoint;
10
+ /** `GET /v1/endpoints`. */
11
+ export declare function listEndpoints(ctx: TenantContext): Promise<{
12
+ endpoints: {
13
+ agentId: string;
14
+ url: string;
15
+ implementationVersion: string;
16
+ manifestHash?: string | undefined;
17
+ timeoutMs: number;
18
+ maxConcurrent: number;
19
+ health: {
20
+ lastDeliveryAt?: string | undefined;
21
+ lastSuccessAt?: string | undefined;
22
+ lastError?: {
23
+ code: string;
24
+ message: string;
25
+ } | undefined;
26
+ consecutiveFailures: number;
27
+ served?: {
28
+ implementationVersion: string;
29
+ manifestHash?: string | undefined;
30
+ } | undefined;
31
+ };
32
+ updatedAt: string;
33
+ }[];
34
+ }>;
35
+ /**
36
+ * `PUT /v1/endpoints`: upsert the batch in one transaction, then send the Actions already
37
+ * waiting for these agents. Answers every registered endpoint of the batch.
38
+ */
39
+ export declare function putEndpoints(ctx: TenantContext, principalId: string, body: PutEndpointsRequest): Promise<{
40
+ endpoints: {
41
+ agentId: string;
42
+ url: string;
43
+ implementationVersion: string;
44
+ manifestHash?: string | undefined;
45
+ timeoutMs: number;
46
+ maxConcurrent: number;
47
+ health: {
48
+ lastDeliveryAt?: string | undefined;
49
+ lastSuccessAt?: string | undefined;
50
+ lastError?: {
51
+ code: string;
52
+ message: string;
53
+ } | undefined;
54
+ consecutiveFailures: number;
55
+ served?: {
56
+ implementationVersion: string;
57
+ manifestHash?: string | undefined;
58
+ } | undefined;
59
+ };
60
+ updatedAt: string;
61
+ }[];
62
+ }>;
63
+ /** `DELETE /v1/endpoints/:agentId`. The agent's pending Actions wait for a new endpoint. */
64
+ export declare function deleteEndpoint(ctx: TenantContext, agentId: string): Promise<{
65
+ agentId: string;
66
+ deleted: true;
67
+ }>;
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Action endpoints (design: Action endpoints §4.2): where the Runtime delivers each agent's
3
+ * Actions, registered with the application key.
4
+ */
5
+ import { ENDPOINT_MAX_CONCURRENT_DEFAULT, ENDPOINT_TIMEOUT_DEFAULT_MS, } from "@nylorun/core/contracts";
6
+ import { fail } from "./http.js";
7
+ /** An endpoint as the Tenant API answers it. */
8
+ export function endpointView(row) {
9
+ const served = row.servedImplementationVersion === undefined
10
+ ? undefined
11
+ : {
12
+ implementationVersion: row.servedImplementationVersion,
13
+ ...(row.servedManifestHash === undefined
14
+ ? {}
15
+ : { manifestHash: row.servedManifestHash }),
16
+ };
17
+ return {
18
+ agentId: row.agentId,
19
+ url: row.url,
20
+ implementationVersion: row.implementationVersion,
21
+ ...(row.manifestHash === undefined ? {} : { manifestHash: row.manifestHash }),
22
+ timeoutMs: row.timeoutMs,
23
+ maxConcurrent: row.maxConcurrent,
24
+ health: {
25
+ ...(row.lastDeliveryAt === undefined ? {} : { lastDeliveryAt: row.lastDeliveryAt }),
26
+ ...(row.lastSuccessAt === undefined ? {} : { lastSuccessAt: row.lastSuccessAt }),
27
+ ...(row.lastErrorCode === undefined
28
+ ? {}
29
+ : {
30
+ lastError: {
31
+ code: row.lastErrorCode,
32
+ message: row.lastErrorMessage ?? "",
33
+ },
34
+ }),
35
+ consecutiveFailures: row.consecutiveFailures,
36
+ ...(served ? { served } : {}),
37
+ },
38
+ updatedAt: row.updatedAt,
39
+ };
40
+ }
41
+ /** `GET /v1/endpoints`. */
42
+ export async function listEndpoints(ctx) {
43
+ const rows = await ctx.store.tx((t) => t.listEndpoints());
44
+ return { endpoints: rows.map(endpointView) };
45
+ }
46
+ /**
47
+ * `PUT /v1/endpoints`: upsert the batch in one transaction, then send the Actions already
48
+ * waiting for these agents. Answers every registered endpoint of the batch.
49
+ */
50
+ export async function putEndpoints(ctx, principalId, body) {
51
+ const updatedAt = new Date().toISOString();
52
+ const rows = await ctx.store.tx(async (t) => {
53
+ const saved = [];
54
+ for (const endpoint of body.endpoints) {
55
+ await t.putEndpoint({
56
+ agentId: endpoint.agentId,
57
+ url: endpoint.url,
58
+ implementationVersion: endpoint.implementationVersion,
59
+ ...(endpoint.manifestHash === undefined
60
+ ? {}
61
+ : { manifestHash: endpoint.manifestHash }),
62
+ timeoutMs: endpoint.timeoutMs ?? ENDPOINT_TIMEOUT_DEFAULT_MS,
63
+ maxConcurrent: endpoint.maxConcurrent ?? ENDPOINT_MAX_CONCURRENT_DEFAULT,
64
+ principalId,
65
+ updatedAt,
66
+ });
67
+ saved.push((await t.getEndpoint(endpoint.agentId)));
68
+ }
69
+ return saved;
70
+ });
71
+ // Actions already waiting for these agents go to their endpoint now.
72
+ const waiting = await ctx.store.tx(async (t) => {
73
+ const actions = [];
74
+ for (const endpoint of body.endpoints)
75
+ actions.push(...(await t.pendingActions(endpoint.agentId)));
76
+ return actions;
77
+ });
78
+ for (const action of waiting)
79
+ await ctx.deliver(action.actionId);
80
+ return { endpoints: rows.map(endpointView) };
81
+ }
82
+ /** `DELETE /v1/endpoints/:agentId`. The agent's pending Actions wait for a new endpoint. */
83
+ export async function deleteEndpoint(ctx, agentId) {
84
+ await ctx.store.tx(async (t) => {
85
+ if (!(await t.getEndpoint(agentId)))
86
+ fail(404, "Endpoint not found");
87
+ await t.deleteEndpoint(agentId);
88
+ });
89
+ return { agentId, deleted: true };
90
+ }
@@ -19,6 +19,10 @@ export interface StartEphemeralRuntimeOptions {
19
19
  sandboxBackend?: "auto" | "virtual";
20
20
  /** When true, close() leaves hostRoot on disk. */
21
21
  retainRoot?: boolean;
22
+ /** Allow browser requests (an `Origin` with a publishable key). Default off. */
23
+ browserAccess?: boolean;
24
+ /** Serve the Admin API on its own loopback listener (`adminUrl`). Default off. */
25
+ operatorListener?: boolean;
22
26
  logger?: Logger;
23
27
  /**
24
28
  * A Postgres pool: Tenants become schemas in it (`store-pg.ts`) instead of living in memory.
@@ -28,6 +32,8 @@ export interface StartEphemeralRuntimeOptions {
28
32
  }
29
33
  export interface EphemeralRuntime {
30
34
  url: string;
35
+ /** Where the Admin API answers: `url`, or the operator listener when requested. */
36
+ adminUrl: string;
31
37
  tenantId: string;
32
38
  applicationKey: string;
33
39
  adminKey: string;
@@ -13,7 +13,7 @@ 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 { hashToken, mintBearerToken } from "../core/bearer.js";
17
17
  import { createHost } from "../host/create-host.js";
18
18
  import { createKekFile } from "../vault/kek.js";
19
19
  import { MemorySessionStore } from "../store/memory.js";
@@ -95,6 +95,7 @@ function kept(store) {
95
95
  tenantId: store.tenantId,
96
96
  tx: (fn) => store.tx(fn),
97
97
  onCommit: (listener) => store.onCommit(listener),
98
+ record: () => store.record(),
98
99
  health: () => store.health(),
99
100
  close: async () => { },
100
101
  };
@@ -173,37 +174,65 @@ export async function startEphemeralRuntime(options) {
173
174
  logger,
174
175
  onDeleted: (tenantId) => streams.deleteTenant(tenantId),
175
176
  });
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();
177
+ let host;
178
+ /** Closes what was opened, in reverse order; `close()` and a failed start share it. */
179
+ const release = async () => {
180
+ await host?.close();
181
+ await module.close();
182
+ await memory?.close();
183
+ await streams.close();
184
+ if (!options.retainRoot)
185
+ rmSync(hostRoot, { recursive: true, force: true });
186
+ };
187
+ let tenantId;
188
+ let applicationKey;
189
+ let principalId;
190
+ try {
191
+ await module.start();
192
+ tenantId = options.tenantId ?? newTenantId();
193
+ applicationKey = options.applicationKey ?? mintBearerToken();
194
+ principalId =
195
+ options.principalId ?? `principal_${randomBytes(8).toString("hex")}`;
196
+ const credentialHash = hashToken(applicationKey);
197
+ await module.create({
198
+ tenantId,
199
+ name: options.name ?? "ephemeral",
200
+ principalId,
201
+ credentialHash,
202
+ idempotencyKey: `ephemeral-${tenantId}`,
203
+ ...(options.studioCredentialHash
204
+ ? { studioCredentialHash: options.studioCredentialHash }
205
+ : {}),
206
+ });
207
+ // KEK for first vault write; openTenantRuntime also creates when hooks allow.
208
+ const kekPath = tenantPaths(hostRoot, tenantId).kek;
209
+ if (!existsSync(kekPath))
210
+ createKekFile(kekPath);
211
+ host = createHost({
212
+ hostRoot,
213
+ module,
214
+ config: hostConfig,
215
+ credentials,
216
+ logger,
217
+ coreVersion: coreVersion(),
218
+ browserAccess: options.browserAccess === true,
219
+ ...(options.operatorListener
220
+ ? { operator: { host: "127.0.0.1", port: 0 } }
221
+ : {}),
222
+ });
223
+ await host.listen();
224
+ }
225
+ catch (error) {
226
+ await release().catch((cleanup) => logger.warn("ephemeral runtime cleanup failed", {
227
+ message: cleanup instanceof Error ? cleanup.message : String(cleanup),
228
+ }));
229
+ throw error;
230
+ }
231
+ const started = host;
204
232
  let closed = false;
205
233
  return {
206
- url: host.url,
234
+ url: started.url,
235
+ adminUrl: started.adminUrl,
207
236
  tenantId,
208
237
  applicationKey,
209
238
  adminKey,
@@ -213,12 +242,7 @@ export async function startEphemeralRuntime(options) {
213
242
  if (closed)
214
243
  return;
215
244
  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 });
245
+ await release();
222
246
  },
223
247
  };
224
248
  }
@@ -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
+ }