@nylorun/runtime 0.14.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 (198) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/README.md +75 -33
  3. package/dist/adapters/execution/restate.js +2 -2
  4. package/dist/adapters/replication/pgoutput.d.ts +5 -0
  5. package/dist/adapters/replication/pgoutput.js +4 -3
  6. package/dist/admin-openapi.json +184 -685
  7. package/dist/api/components.d.ts +308 -68
  8. package/dist/api/components.js +4 -5
  9. package/dist/api/gate/keys.d.ts +14 -0
  10. package/dist/api/gate/keys.js +59 -0
  11. package/dist/api/gate/routes.d.ts +34 -0
  12. package/dist/api/gate/routes.js +235 -0
  13. package/dist/api/http/define.js +0 -8
  14. package/dist/api/http/routes/access.js +25 -18
  15. package/dist/api/http/routes/tenant.d.ts +2 -1
  16. package/dist/api/http/routes/tenant.js +77 -5
  17. package/dist/api/http/routes/vaults.js +2 -2
  18. package/dist/api/openapi.js +4 -5
  19. package/dist/gates/contract.d.ts +51 -0
  20. package/dist/gates/contract.js +37 -0
  21. package/dist/gates/handler.d.ts +21 -0
  22. package/dist/gates/handler.js +55 -0
  23. package/dist/gates/http-client.d.ts +12 -0
  24. package/dist/gates/http-client.js +160 -0
  25. package/dist/gates/in-process.d.ts +32 -0
  26. package/dist/gates/in-process.js +42 -0
  27. package/dist/gates/inflight.d.ts +39 -0
  28. package/dist/gates/inflight.js +70 -0
  29. package/dist/gates/mcp-handler.d.ts +35 -0
  30. package/dist/gates/mcp-handler.js +142 -0
  31. package/dist/gates/meter.d.ts +24 -0
  32. package/dist/gates/meter.js +165 -0
  33. package/dist/gates/model-gate.d.ts +45 -0
  34. package/dist/gates/model-gate.js +1 -0
  35. package/dist/gates/tenant-vaults.d.ts +64 -0
  36. package/dist/gates/tenant-vaults.js +94 -0
  37. package/dist/gates/tool-calls.d.ts +51 -0
  38. package/dist/gates/tool-calls.js +64 -0
  39. package/dist/gates/tool-client.d.ts +12 -0
  40. package/dist/gates/tool-client.js +201 -0
  41. package/dist/gates/tool-contract.d.ts +116 -0
  42. package/dist/gates/tool-contract.js +64 -0
  43. package/dist/gates/tool-gate.d.ts +45 -0
  44. package/dist/gates/tool-gate.js +10 -0
  45. package/dist/host/admin-api.d.ts +7 -6
  46. package/dist/host/admin-api.js +9 -113
  47. package/dist/host/app.d.ts +1 -1
  48. package/dist/host/app.js +20 -24
  49. package/dist/host/config-for.d.ts +1 -1
  50. package/dist/host/config-for.js +2 -2
  51. package/dist/host/create-host.d.ts +5 -5
  52. package/dist/host/create-host.js +5 -15
  53. package/dist/host/execution.d.ts +20 -18
  54. package/dist/host/execution.js +2 -2
  55. package/dist/host/gates.d.ts +58 -0
  56. package/dist/host/gates.js +127 -0
  57. package/dist/host/http.d.ts +6 -1
  58. package/dist/host/http.js +17 -0
  59. package/dist/host/index.d.ts +1 -1
  60. package/dist/host/main.js +151 -71
  61. package/dist/host/stack-config.d.ts +71 -19
  62. package/dist/host/stack-config.js +192 -18
  63. package/dist/infra/database.d.ts +2 -2
  64. package/dist/infra/execution.d.ts +5 -5
  65. package/dist/infra/execution.js +8 -6
  66. package/dist/infra/index.d.ts +1 -1
  67. package/dist/infra/workers.d.ts +17 -17
  68. package/dist/infra/workers.js +9 -9
  69. package/dist/keys/client.d.ts +9 -0
  70. package/dist/keys/client.js +70 -0
  71. package/dist/keys/contract.d.ts +24 -0
  72. package/dist/keys/contract.js +12 -0
  73. package/dist/keys/keys.d.ts +41 -0
  74. package/dist/keys/keys.js +50 -0
  75. package/dist/mcp/connect.d.ts +43 -3
  76. package/dist/mcp/connect.js +12 -5
  77. package/dist/mcp/pool.d.ts +21 -3
  78. package/dist/mcp/pool.js +15 -2
  79. package/dist/openapi.json +887 -694
  80. package/dist/record/append.d.ts +51 -0
  81. package/dist/record/append.js +20 -0
  82. package/dist/{store/event.js → record/envelope.js} +1 -1
  83. package/dist/record/index.d.ts +11 -0
  84. package/dist/record/index.js +11 -0
  85. package/dist/store/postgres/connect.d.ts +15 -12
  86. package/dist/store/postgres/connect.js +24 -12
  87. package/dist/store/postgres/db.d.ts +16 -0
  88. package/dist/store/postgres/db.js +28 -0
  89. package/dist/store/postgres/drizzle/0000_foundation.sql +22 -0
  90. package/dist/store/postgres/drizzle/0001_baseline.sql +255 -0
  91. package/dist/store/postgres/drizzle/0002_stream_relay.sql +4 -0
  92. package/dist/store/postgres/drizzle/0003_tool_crossings.sql +9 -0
  93. package/dist/store/postgres/drizzle/meta/_journal.json +34 -0
  94. package/dist/store/postgres/migrate.d.ts +68 -0
  95. package/dist/store/postgres/migrate.js +183 -0
  96. package/dist/store/postgres/record-writer.d.ts +4 -0
  97. package/dist/store/postgres/record-writer.js +34 -0
  98. package/dist/store/postgres/record.d.ts +2 -8
  99. package/dist/store/postgres/record.js +50 -54
  100. package/dist/store/postgres/schema.d.ts +3096 -0
  101. package/dist/store/postgres/schema.js +363 -0
  102. package/dist/store/postgres/store.d.ts +2 -4
  103. package/dist/store/postgres/store.js +481 -567
  104. package/dist/store/postgres/tenant.d.ts +56 -0
  105. package/dist/store/postgres/tenant.js +154 -0
  106. package/dist/store/types.d.ts +43 -87
  107. package/dist/store/types.js +0 -1
  108. package/dist/streams/control.d.ts +1 -1
  109. package/dist/streams/control.js +6 -2
  110. package/dist/streams/relay/types.d.ts +2 -1
  111. package/dist/streams/relay/types.js +2 -1
  112. package/dist/streams/types.d.ts +5 -0
  113. package/dist/tenant/advance.d.ts +7 -2
  114. package/dist/tenant/advance.js +95 -16
  115. package/dist/tenant/cause.d.ts +25 -0
  116. package/dist/tenant/cause.js +45 -0
  117. package/dist/tenant/commands.js +4 -2
  118. package/dist/tenant/context.d.ts +26 -3
  119. package/dist/tenant/delivery-token.js +17 -18
  120. package/dist/tenant/delivery.d.ts +1 -1
  121. package/dist/tenant/delivery.js +18 -13
  122. package/dist/tenant/effects.d.ts +15 -1
  123. package/dist/tenant/effects.js +93 -17
  124. package/dist/tenant/ephemeral.d.ts +16 -7
  125. package/dist/tenant/ephemeral.js +45 -111
  126. package/dist/tenant/history.d.ts +64 -0
  127. package/dist/tenant/history.js +120 -0
  128. package/dist/tenant/model-setting.d.ts +2 -2
  129. package/dist/tenant/module.d.ts +14 -28
  130. package/dist/tenant/module.js +131 -271
  131. package/dist/tenant/paths.d.ts +11 -16
  132. package/dist/tenant/paths.js +24 -68
  133. package/dist/tenant/pool.d.ts +1 -1
  134. package/dist/tenant/pool.js +1 -1
  135. package/dist/tenant/principals.d.ts +23 -11
  136. package/dist/tenant/principals.js +61 -49
  137. package/dist/tenant/runtime.d.ts +19 -1
  138. package/dist/tenant/runtime.js +40 -26
  139. package/dist/tenant/scheduler.d.ts +4 -2
  140. package/dist/tenant/scheduler.js +4 -4
  141. package/dist/tenant/session-streams.d.ts +2 -0
  142. package/dist/tenant/session-streams.js +8 -3
  143. package/dist/tenant/status.d.ts +3 -0
  144. package/dist/tenant/status.js +1 -1
  145. package/dist/tenant/store-pg.d.ts +9 -6
  146. package/dist/tenant/store-pg.js +60 -125
  147. package/dist/tenant/streams.d.ts +9 -25
  148. package/dist/tenant/streams.js +7 -22
  149. package/dist/tenant/tokens.js +16 -14
  150. package/dist/tenant/types.d.ts +41 -92
  151. package/dist/tenant/types.js +4 -15
  152. package/dist/tenant/worker.js +4 -4
  153. package/dist/vault/host-model.d.ts +31 -0
  154. package/dist/vault/host-model.js +57 -0
  155. package/dist/vault/service.d.ts +26 -15
  156. package/dist/vault/service.js +70 -109
  157. package/dist/version.d.ts +1 -1
  158. package/dist/version.js +1 -1
  159. package/package.json +9 -5
  160. package/dist/store/memory.d.ts +0 -69
  161. package/dist/store/memory.js +0 -810
  162. package/dist/store/postgres/migrations/001_initial.d.ts +0 -27
  163. package/dist/store/postgres/migrations/001_initial.js +0 -196
  164. package/dist/store/postgres/migrations/002_session_owner.d.ts +0 -6
  165. package/dist/store/postgres/migrations/002_session_owner.js +0 -13
  166. package/dist/store/postgres/migrations/003_subject_tokens.d.ts +0 -8
  167. package/dist/store/postgres/migrations/003_subject_tokens.js +0 -42
  168. package/dist/store/postgres/migrations/004_publishable_keys.d.ts +0 -6
  169. package/dist/store/postgres/migrations/004_publishable_keys.js +0 -18
  170. package/dist/store/postgres/migrations/005_action_endpoints.d.ts +0 -6
  171. package/dist/store/postgres/migrations/005_action_endpoints.js +0 -32
  172. package/dist/store/postgres/migrations/006_drop_executors.d.ts +0 -11
  173. package/dist/store/postgres/migrations/006_drop_executors.js +0 -32
  174. package/dist/store/postgres/migrations/007_event_time.d.ts +0 -7
  175. package/dist/store/postgres/migrations/007_event_time.js +0 -14
  176. package/dist/store/postgres/migrations/008_durable_streams.d.ts +0 -13
  177. package/dist/store/postgres/migrations/008_durable_streams.js +0 -35
  178. package/dist/store/postgres/migrations/index.d.ts +0 -53
  179. package/dist/store/postgres/migrations/index.js +0 -96
  180. package/dist/store/postgres/migrations/shared/index.d.ts +0 -31
  181. package/dist/store/postgres/migrations/shared/index.js +0 -92
  182. package/dist/store/postgres/names.d.ts +0 -11
  183. package/dist/store/postgres/names.js +0 -42
  184. package/dist/store/postgres/tenants.d.ts +0 -90
  185. package/dist/store/postgres/tenants.js +0 -228
  186. package/dist/streams/relay/memory.d.ts +0 -69
  187. package/dist/streams/relay/memory.js +0 -176
  188. package/dist/tenant/envelope.d.ts +0 -7
  189. package/dist/tenant/envelope.js +0 -22
  190. package/dist/tenant/quarantine-error.d.ts +0 -6
  191. package/dist/tenant/quarantine-error.js +0 -9
  192. package/dist/tenant/quarantine.d.ts +0 -27
  193. package/dist/tenant/quarantine.js +0 -72
  194. package/dist/tenant/store-memory.d.ts +0 -16
  195. package/dist/tenant/store-memory.js +0 -111
  196. /package/dist/{store → record}/cursor.d.ts +0 -0
  197. /package/dist/{store → record}/cursor.js +0 -0
  198. /package/dist/{store/event.d.ts → record/envelope.d.ts} +0 -0
@@ -34,8 +34,9 @@ import { runDurable, runFlowDurable, } from "@nylorun/harness/run";
34
34
  import { aggregateWaits, cancelSiblingWork, isWorkflowManifest, wakeLinkedWorkflow, } from "../core/flow-host.js";
35
35
  import { isOwnershipLost, ownedTx } from "../store/ownership.js";
36
36
  import { linkedAgentOutput, rebaseSessionState, sessionToolsOf, turnManifestOf, } from "./session.js";
37
- import { prepareMcp, resolveEffect } from "./effects.js";
37
+ import { isRemoteMcpEffect, prepareMcp, recoversMcpCalls, recoversModelCalls, resolveEffect, } from "./effects.js";
38
38
  import { slimModelEffects } from "./slim.js";
39
+ import { checkParity, foldTranscript, leanState, transcriptOf, transcriptUpdates, withTranscript, } from "./history.js";
39
40
  import { command } from "./commands.js";
40
41
  import { usesFixtureModel } from "./model-setting.js";
41
42
  import { toolFixtureModel } from "../core/provider.js";
@@ -86,6 +87,7 @@ export async function advance(ctx, id, signal) {
86
87
  else
87
88
  signal.addEventListener("abort", forward, { once: true });
88
89
  ctx.work.running.set(id, controller);
90
+ ctx.work.runningTurns.set(id, taken.session.activeTurnId ?? null);
89
91
  const heartbeat = startHeartbeat(ctx, lease, controller);
90
92
  let result = DONE;
91
93
  try {
@@ -111,8 +113,10 @@ export async function advance(ctx, id, signal) {
111
113
  finally {
112
114
  heartbeat.stop();
113
115
  signal.removeEventListener("abort", forward);
114
- if (ctx.work.running.get(id) === controller)
116
+ if (ctx.work.running.get(id) === controller) {
115
117
  ctx.work.running.delete(id);
118
+ ctx.work.runningTurns.delete(id);
119
+ }
116
120
  // Best effort: a release that fails leaves a lease that simply expires.
117
121
  await ctx.store
118
122
  .tx((t) => t.releaseOwnership(id, lease.owner, lease.epoch))
@@ -147,7 +151,10 @@ async function takeOwnership(ctx, id) {
147
151
  };
148
152
  const s = await t.assertEpoch(id, taken.epoch);
149
153
  if (taken.takeover)
150
- await takeOver(t, s);
154
+ await takeOver(t, s, {
155
+ recoversModelCalls: recoversModelCalls(ctx),
156
+ recoversMcpCalls: recoversMcpCalls(ctx),
157
+ });
151
158
  if (!s.checkpoint || !["running", "runnable"].includes(s.status)) {
152
159
  await t.releaseOwnership(id, lease.owner, lease.epoch);
153
160
  return { status: "idle" };
@@ -159,13 +166,20 @@ async function takeOwnership(ctx, id) {
159
166
  }
160
167
  /**
161
168
  * Takeover (§10.5 step 2, §11.4), inside the transaction that took ownership: the dead
162
- * owner's `invoking` effects become `uncertain` and are never invoked again. When one belongs
169
+ * owner's `invoking` effects become `uncertain` and are never invoked again, except model
170
+ * calls when the gate recovers them (P1.2) and remote MCP calls when the Tool Gate does (F4.1
171
+ * G3): those stay `invoking`, and the replay re-sends them. When one belongs
163
172
  * to the active turn, the session becomes `uncertain` too, with an `effect.uncertain` event.
164
173
  * Mutates and writes `s`. Returns the effect ids it marked.
165
174
  */
166
- export async function takeOver(t, s) {
175
+ export async function takeOver(t, s, options = {}) {
167
176
  const marked = [];
168
177
  for (const effect of await t.invokingEffects(s.id)) {
178
+ // Still running at the gate, or finished there: the replay re-sends it (P1.2).
179
+ if (options.recoversModelCalls && effect.request.kind === "model")
180
+ continue;
181
+ if (options.recoversMcpCalls && isRemoteMcpEffect(s, effect.request))
182
+ continue;
169
183
  effect.status = "uncertain";
170
184
  effect.error ??= "The Worker running this effect stopped before its outcome was recorded";
171
185
  await t.put("effects", effect.request.effectId, effect);
@@ -238,12 +252,24 @@ async function runSegment(ctx, lease, started, signal) {
238
252
  const { current, fixtureModel } = await ownedTx(ctx.store, id, lease.epoch, async (t, current) => {
239
253
  if (!isWorkflowManifest(current.manifest) && current.checkpoint) {
240
254
  rebaseSessionState(current, current.checkpoint.manifestHash);
255
+ await adoptStoredTranscript(t, current);
241
256
  const cp = current.checkpoint;
242
257
  current.checkpoint = { ...cp, state: current.state };
243
258
  await t.put("sessions", id, current);
244
259
  }
245
260
  return { current, fixtureModel: await usesFixtureModel(t) };
246
261
  });
262
+ // One history (P0.3): the engine resumes from the transcript folded from the record.
263
+ let startTranscript = [];
264
+ let engineCheckpoint = current.checkpoint;
265
+ if (!isWorkflowManifest(current.manifest) && current.checkpoint) {
266
+ const cp = current.checkpoint;
267
+ if (cp.state) {
268
+ startTranscript = await foldSession(ctx, current);
269
+ checkParity(id, startTranscript, transcriptOf(cp.state));
270
+ engineCheckpoint = { ...cp, state: withTranscript(cp.state, startTranscript) };
271
+ }
272
+ }
247
273
  const segment = fixtureModel ? { model: fixture } : {};
248
274
  const host = {
249
275
  resolveEffect: (e) => resolveEffect(ctx, e, signal, lease, segment),
@@ -258,7 +284,7 @@ async function runSegment(ctx, lease, started, signal) {
258
284
  })
259
285
  : await runDurable({
260
286
  manifest: turnManifestOf(current),
261
- checkpoint: current.checkpoint,
287
+ checkpoint: engineCheckpoint,
262
288
  signal,
263
289
  sessionTools: sessionToolsOf(current.mcpSnapshot),
264
290
  host,
@@ -273,8 +299,15 @@ async function runSegment(ctx, lease, started, signal) {
273
299
  if (signal.reason instanceof AdvanceDeadlineError &&
274
300
  (result.status === "cancelled" || result.status === "failed"))
275
301
  await settleFailure(ctx, lease, started, signal.reason);
276
- else
277
- await settle(ctx, lease, started, result);
302
+ else {
303
+ const state = !isWorkflowManifest(current.manifest) &&
304
+ (result.status === "yielded" || result.status === "completed" || result.status === "paused") &&
305
+ "result" in result
306
+ ? result.result?.state
307
+ : undefined;
308
+ const updates = state ? transcriptUpdates(startTranscript, transcriptOf(state)) : [];
309
+ await settle(ctx, lease, started, result, updates);
310
+ }
278
311
  }
279
312
  catch (error) {
280
313
  if (isOwnershipLost(error) || error instanceof SegmentStopped)
@@ -312,7 +345,7 @@ async function wakeWorkflowOf(ctx, t, agent, turnId, type, payload) {
312
345
  });
313
346
  }
314
347
  /** Record a segment result, then cancel failed siblings and wake a linked workflow. */
315
- async function settle(ctx, lease, s, result) {
348
+ async function settle(ctx, lease, s, result, updates = []) {
316
349
  const id = lease.sessionId;
317
350
  const siblingCancelIds = await ownedTx(ctx.store, id, lease.epoch, async (t, current) => {
318
351
  const siblings = [];
@@ -348,7 +381,8 @@ async function settle(ctx, lease, s, result) {
348
381
  if (result.status === "yielded") {
349
382
  // The turn goes on in a new segment: same turn, next checkpoint, woken right away.
350
383
  const finished = result.checkpoint;
351
- current.state = result.result.state;
384
+ await recordTranscript(t, id, s.activeTurnId, current, updates);
385
+ current.state = leanState(result.result.state);
352
386
  current.checkpoint = {
353
387
  ...finished,
354
388
  segment: finished.segment + 1,
@@ -357,7 +391,6 @@ async function settle(ctx, lease, s, result) {
357
391
  };
358
392
  current.status = "runnable";
359
393
  current.waits = undefined;
360
- await t.put("checkpoints", JSON.stringify([id, s.activeTurnId, finished.segment]), { checkpoint: finished, status: "yielded" });
361
394
  await slimModelEffects(t, id, s.activeTurnId);
362
395
  await t.put("sessions", id, current);
363
396
  const segment = finished.segment + 1;
@@ -369,13 +402,14 @@ async function settle(ctx, lease, s, result) {
369
402
  }
370
403
  const flow = isWorkflowManifest(current.manifest);
371
404
  if (!flow) {
405
+ if (result.status !== "failed")
406
+ await recordTranscript(t, id, s.activeTurnId, current, updates);
372
407
  current.state =
373
408
  result.status === "failed"
374
409
  ? current.turnStartState
375
- : result.result.state;
410
+ : leanState(result.result.state);
376
411
  }
377
- current.checkpoint = result.checkpoint;
378
- await t.put("checkpoints", JSON.stringify([id, s.activeTurnId, result.checkpoint.segment]), { checkpoint: result.checkpoint, status: result.status });
412
+ current.checkpoint = leanCheckpoint(result.checkpoint);
379
413
  current.status = result.status;
380
414
  current.waits =
381
415
  result.status === "paused" ? result.result.pending : undefined;
@@ -456,8 +490,6 @@ async function settleFailure(ctx, lease, s, error) {
456
490
  return;
457
491
  current.status = "failed";
458
492
  current.state = current.turnStartState;
459
- if (current.checkpoint)
460
- await t.put("checkpoints", JSON.stringify([id, s.activeTurnId, current.checkpoint.segment]), { checkpoint: current.checkpoint, status: "failed" });
461
493
  current.error = error instanceof Error ? error.message : String(error);
462
494
  const payload = { message: current.error };
463
495
  await t.event(id, current.activeTurnId, "turn.failed", payload);
@@ -469,3 +501,50 @@ async function settleFailure(ctx, lease, s, error) {
469
501
  await wakeWorkflowOf(ctx, t, current, s.activeTurnId, "turn.failed", payload);
470
502
  });
471
503
  }
504
+ /** `checkpoint` with its engine state stored lean (`history.ts`). */
505
+ function leanCheckpoint(checkpoint) {
506
+ const state = checkpoint?.state;
507
+ if (!checkpoint || !state)
508
+ return checkpoint;
509
+ return { ...checkpoint, state: leanState(state) };
510
+ }
511
+ /** Appends a segment's transcript edits; a snapshot moves `history.snapshot`. */
512
+ async function recordTranscript(t, id, turnId, s, updates) {
513
+ for (const update of updates) {
514
+ const event = await t.event(id, turnId, "transcript.updated", update);
515
+ if (update.keep === 0)
516
+ s.history = { from: s.history?.from ?? event.seq, snapshot: event.seq };
517
+ }
518
+ }
519
+ /**
520
+ * A session written before transcripts were recorded keeps its transcript on its row: record
521
+ * it once (the turn's starting transcript, then the turn so far), and store the row lean.
522
+ */
523
+ async function adoptStoredTranscript(t, s) {
524
+ if (s.history)
525
+ return;
526
+ const now = transcriptOf(s.state);
527
+ const start = s.activeTurnId ? transcriptOf(s.turnStartState) : now;
528
+ if (now.length === 0 && start.length === 0)
529
+ return;
530
+ let first;
531
+ const write = async (turnId, updates) => {
532
+ for (const update of updates) {
533
+ const event = await t.event(s.id, turnId, "transcript.updated", update);
534
+ first ??= event.seq;
535
+ }
536
+ };
537
+ await write(null, transcriptUpdates([], start));
538
+ if (s.activeTurnId)
539
+ await write(s.activeTurnId, transcriptUpdates(start, now));
540
+ s.history = { from: first ?? 0, snapshot: first ?? 0 };
541
+ s.state = leanState(s.state);
542
+ s.turnStartState = leanState(s.turnStartState);
543
+ }
544
+ /** The session's transcript, folded from its record from `history.from`. */
545
+ async function foldSession(ctx, s) {
546
+ const rows = await ctx.store
547
+ .record()
548
+ .readRange(ctx.store.tenantId, s.id, s.history?.from ?? 0, Number.MAX_SAFE_INTEGER);
549
+ return foldTranscript(rows.map((row) => row.body));
550
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Why the Host's Tenant could not be opened (tenancy.md §5). A cause is about the Tenant's
3
+ * own data or files (its database is newer than this Runtime, its vault key is missing, the
4
+ * database has an old layout): the Host fails readiness, names the cause in
5
+ * `/v1/admin/status` and the logs, and answers Tenant requests with the opaque 404 until it is
6
+ * repaired and the Host restarted. A failure outside the Tenant (Postgres unreachable) is not
7
+ * a cause: it is `TenantUnavailableError`, and the next request tries again.
8
+ */
9
+ import type { TenantCause, TenantEnvelope } from "@nylorun/core/contracts";
10
+ export type { TenantCause };
11
+ /** Thrown by migrations, the Tenant bootstrap and the Tenant Runtime when the Tenant cannot open. */
12
+ export declare class TenantOpenError extends Error {
13
+ readonly code: TenantCause["code"];
14
+ readonly repair: string;
15
+ /** The Tenant, when it was read before opening failed (`/v1/admin/status` names it). */
16
+ envelope?: TenantEnvelope;
17
+ constructor(fields: TenantCause);
18
+ toCause(): TenantCause;
19
+ }
20
+ /** A `TenantOpenError` with the standard repair for `code`. */
21
+ export declare function openError(code: TenantCause["code"], message: string): TenantOpenError;
22
+ /** The cause an error carries, when it is a `TenantOpenError`. */
23
+ export declare function causeOf(error: unknown): TenantCause | undefined;
24
+ /** What an operator does about each cause. */
25
+ export declare function repairFor(code: TenantCause["code"]): string;
@@ -0,0 +1,45 @@
1
+ /** Thrown by migrations, the Tenant bootstrap and the Tenant Runtime when the Tenant cannot open. */
2
+ export class TenantOpenError extends Error {
3
+ code;
4
+ repair;
5
+ /** The Tenant, when it was read before opening failed (`/v1/admin/status` names it). */
6
+ envelope;
7
+ constructor(fields) {
8
+ super(fields.message);
9
+ this.name = "TenantOpenError";
10
+ this.code = fields.code;
11
+ this.repair = fields.repair;
12
+ }
13
+ toCause() {
14
+ return { code: this.code, message: this.message, repair: this.repair };
15
+ }
16
+ }
17
+ /** A `TenantOpenError` with the standard repair for `code`. */
18
+ export function openError(code, message) {
19
+ return new TenantOpenError({ code, message, repair: repairFor(code) });
20
+ }
21
+ /** The cause an error carries, when it is a `TenantOpenError`. */
22
+ export function causeOf(error) {
23
+ return error instanceof TenantOpenError ? error.toCause() : undefined;
24
+ }
25
+ /** What an operator does about each cause. */
26
+ export function repairFor(code) {
27
+ switch (code) {
28
+ case "kek-missing":
29
+ return "restore the vault key (vault-kek in the Host root's keys/ directory) from a backup, then restart the Runtime and its gateway: the Tenant's ciphertext cannot be opened without it";
30
+ case "corrupt":
31
+ return "restore the database from a backup, then restart the Runtime";
32
+ case "schema-too-new":
33
+ return "the database was migrated by a newer Runtime: run that version or newer";
34
+ case "migration-failed":
35
+ return "inspect the Runtime log for the failed migration, fix it, then restart the Runtime";
36
+ case "envelope-invalid":
37
+ return "the nylorun.tenant row is missing or invalid: restore the database from a backup";
38
+ case "open-timeout":
39
+ return "opening the Tenant timed out: inspect the Runtime log and Postgres, then restart the Runtime";
40
+ case "open-failed":
41
+ return "inspect the Runtime log, repair what it names, then restart the Runtime";
42
+ case "database-layout-old":
43
+ return "this release starts fresh: point the Runtime at a new database (a new stack: `nylorun start`); the old database is left as it is";
44
+ }
45
+ }
@@ -153,7 +153,7 @@ export async function command(ctx, id, input, scope) {
153
153
  });
154
154
  await slimModelEffects(t, id, cancelledTurnId);
155
155
  // The process running the advance aborts it on `session.cancel` (tenant/control).
156
- t.afterCommit(() => signalSessionCancel(ctx, id));
156
+ t.afterCommit(() => signalSessionCancel(ctx, id, cancelledTurnId));
157
157
  s.activeTurnId = null;
158
158
  if (cancelledTurnId !== null)
159
159
  s.lastTurnId = cancelledTurnId;
@@ -182,6 +182,9 @@ export async function command(ctx, id, input, scope) {
182
182
  if (scope.kind === "token" && scope.limits)
183
183
  await chargeTurn(t, scope.subject, scope.limits);
184
184
  s.turnStartState = s.state;
185
+ // A new turn folds from the latest snapshot: a cancel or failure only reverts this turn.
186
+ if (s.history?.snapshot !== undefined)
187
+ s.history = { ...s.history, from: s.history.snapshot };
185
188
  s.activeTurnId = randomUUID();
186
189
  if (isWorkflowManifest(s.manifest)) {
187
190
  const input = "content" in command ? command.content : command.data;
@@ -263,7 +266,6 @@ export async function command(ctx, id, input, scope) {
263
266
  segment: (s.checkpoint?.segment ?? 0) + 1,
264
267
  });
265
268
  }
266
- await t.put("checkpoints", JSON.stringify([id, s.activeTurnId, s.checkpoint.segment]), { checkpoint: s.checkpoint, status: "runnable" });
267
269
  s.status = "runnable";
268
270
  s.waits = undefined;
269
271
  const commandWake = {
@@ -13,6 +13,7 @@
13
13
  * `wake` goes to `DurableExecution.wake`, which calls the Tenant's `advance` (`advance.ts`)
14
14
  * under ownership (§10.6); `abortLocal` aborts an advance running on this process.
15
15
  */
16
+ import type { SessionHistory } from "./history.js";
16
17
  import type { RoleLimits, SubjectScope, TenantEnvelope } from "@nylorun/core/contracts";
17
18
  import type { DurableCheckpoint, FlowCheckpoint } from "@nylorun/harness/run";
18
19
  import type { AgentManifest, JsonValue, SandboxManifest } from "@nylorun/core/define";
@@ -20,6 +21,9 @@ import type { CredentialSelection } from "@nylorun/core/contracts";
20
21
  import type { SessionStore, Tx } from "../store/types.js";
21
22
  import type { FlowLimits } from "../core/limits.js";
22
23
  import type { ModelProvider } from "../core/provider.js";
24
+ import type { ModelGate } from "../gates/model-gate.js";
25
+ import type { ToolGate } from "../gates/tool-gate.js";
26
+ import type { Keys } from "../keys/keys.js";
23
27
  import type { VaultService } from "../vault/service.js";
24
28
  import type { McpPool } from "../mcp/pool.js";
25
29
  import type { McpDiagnostic, McpSnapshot } from "../mcp/snapshot.js";
@@ -43,8 +47,11 @@ export interface Session {
43
47
  status: string;
44
48
  activeTurnId: string | null;
45
49
  checkpoint?: DurableCheckpoint | FlowCheckpoint;
50
+ /** The engine state; its transcript is folded from the record (`history.ts`), not stored. */
46
51
  state?: any;
47
52
  turnStartState?: any;
53
+ /** Where the transcript's fold starts in the session's record (`history.ts`). */
54
+ history?: SessionHistory;
48
55
  waits?: unknown;
49
56
  error?: string;
50
57
  /** Last completed turn output (for linked agent → workflow settle). */
@@ -135,8 +142,21 @@ export interface TenantContext {
135
142
  readonly sandbox: SandboxManager;
136
143
  readonly flowLimits: FlowLimits;
137
144
  readonly modelProvider: ModelProvider;
138
- /** True when the model comes from the Tenant vault selection (pi-ai adapter). */
145
+ /** True when the model comes from the Tenant vault selection, served by `modelGate`. */
139
146
  readonly useVaultModel: boolean;
147
+ /** Serves vault-backed model calls (blueprint §15): in this process, or the gates service. */
148
+ readonly modelGate: ModelGate;
149
+ /**
150
+ * Serves remote MCP servers and Action deliveries (blueprint §12, F4.1): in this process, or
151
+ * the gates service.
152
+ */
153
+ readonly toolGate: ToolGate;
154
+ /**
155
+ * Vault writes that touch a secret, and all token signing (blueprint §14, F4.2): in this
156
+ * process, or the gateway's keys service. Call it instead of `vault`'s sealing methods and
157
+ * `signingKeys`' private key.
158
+ */
159
+ readonly keys: Keys;
140
160
  /** Set by drain/close; reset clears it again. Stops new advances. */
141
161
  closing: boolean;
142
162
  /** Set once close has finished releasing resources. */
@@ -157,8 +177,11 @@ export interface TenantContext {
157
177
  * re-wakes anything left runnable.
158
178
  */
159
179
  wake(sessionId: string, wake: Wake): Promise<void>;
160
- /** Seam: abort the advance of a session running in this process, if any. */
161
- abortLocal(sessionId: string): void;
180
+ /**
181
+ * Seam: abort the advance of a session running in this process, if any. With `turnId`, only
182
+ * an advance of that turn (a cancel signal names the turn it cancelled).
183
+ */
184
+ abortLocal(sessionId: string, turnId?: string): void;
162
185
  /**
163
186
  * Seam: deliver an Action to its agent's Action endpoint (`DurableExecution.deliver`). Call it
164
187
  * from `t.afterCommit`. Dropped while the Tenant is closing; the sweep re-sends it.
@@ -10,7 +10,7 @@
10
10
  * (`signing-keys.ts`), so a rotation never revokes a live token.
11
11
  */
12
12
  import { createHash, randomUUID } from "node:crypto";
13
- import { errors, jwtVerify, SignJWT } from "jose";
13
+ import { errors, jwtVerify } from "jose";
14
14
  import { DELIVERY_TOKEN_MAX_TTL_SECONDS, DELIVERY_TOKEN_TYPE, subjectTokenIssuer, } from "@nylorun/core/contracts";
15
15
  import { failOpaque, HttpError } from "./http.js";
16
16
  import { CLOCK_TOLERANCE_SECONDS, tokenKeyId } from "./jwt.js";
@@ -23,27 +23,26 @@ export function bodyHash(body) {
23
23
  * is capped at `DELIVERY_TOKEN_MAX_TTL_SECONDS`.
24
24
  */
25
25
  export async function mintDeliveryToken(ctx, options) {
26
- const kek = ctx.signingKeys.kek();
27
- const row = await ctx.store.tx((t) => ctx.signingKeys.ensure(t, kek));
28
- const signer = await ctx.signingKeys.privateKey(row, kek);
29
26
  const ttl = Math.max(1, Math.min(Math.floor(options.ttlSeconds), DELIVERY_TOKEN_MAX_TTL_SECONDS));
30
27
  const iat = Math.floor(Date.now() / 1000);
31
28
  const tokenId = randomUUID();
32
29
  const delivery = options.for;
33
- const token = await new SignJWT({
34
- agt: delivery.agentId,
35
- gen: delivery.kind === "action" ? delivery.generation : 0,
36
- bdy: bodyHash(options.body),
37
- })
38
- .setProtectedHeader({ alg: "ES256", typ: DELIVERY_TOKEN_TYPE, kid: signer.id })
39
- .setIssuer(subjectTokenIssuer(ctx.config.tenantId))
40
- .setAudience(options.audience)
41
- .setSubject(delivery.kind === "action" ? delivery.actionId : "ping")
42
- .setIssuedAt(iat)
43
- .setExpirationTime(iat + ttl)
44
- .setJti(tokenId)
45
- .sign(signer.key);
46
- return { token, expiresAt: (iat + ttl) * 1000, tokenId, keyId: signer.id };
30
+ // Signed by the keys service (F4.2): this process never holds the private key.
31
+ const { token, keyId } = await ctx.keys.sign({
32
+ typ: DELIVERY_TOKEN_TYPE,
33
+ claims: {
34
+ agt: delivery.agentId,
35
+ gen: delivery.kind === "action" ? delivery.generation : 0,
36
+ bdy: bodyHash(options.body),
37
+ iss: subjectTokenIssuer(ctx.config.tenantId),
38
+ aud: options.audience,
39
+ sub: delivery.kind === "action" ? delivery.actionId : "ping",
40
+ iat,
41
+ exp: iat + ttl,
42
+ jti: tokenId,
43
+ },
44
+ });
45
+ return { token, expiresAt: (iat + ttl) * 1000, tokenId, keyId };
47
46
  }
48
47
  /**
49
48
  * Verifies a delivery token presented as a bearer. Answers `401 token_expired` for an expired
@@ -3,7 +3,7 @@ import type { DeliverResult } from "../execution/types.js";
3
3
  import type { Tx } from "../store/types.js";
4
4
  import { type Session, type TenantContext } from "./context.js";
5
5
  import { type DeliveryScope } from "./delivery-token.js";
6
- import { type OutboundResult } from "./outbound.js";
6
+ import type { OutboundResult } from "./outbound.js";
7
7
  /**
8
8
  * Offers an Action that just became `pending` to its agent's endpoint, after the transaction
9
9
  * commits. Without an endpoint it waits for the agent's next registration (`PUT /v1/endpoints`).
@@ -27,7 +27,6 @@ import { acceptedOutcome, recordActionOutcome } from "./commands.js";
27
27
  import { sandboxLookup } from "./context.js";
28
28
  import { mintDeliveryToken } from "./delivery-token.js";
29
29
  import { fail } from "./http.js";
30
- import { post } from "./outbound.js";
31
30
  const DONE = { status: "done" };
32
31
  /** Extra time past the endpoint's timeout before an unanswered delivery counts as lost. */
33
32
  const DEADLINE_GRACE_MS = 5_000;
@@ -115,14 +114,17 @@ export async function deliverAction(ctx, actionId, signal) {
115
114
  const inFlight = track(ctx, action.sessionId, cancel);
116
115
  let result;
117
116
  try {
118
- result = await post(endpoint.url, body, {
119
- [SIGNATURE_HEADER]: minted.token,
120
- [PROTOCOL_HEADER]: String(PROTOCOL_VERSION),
121
- "idempotency-key": actionId,
122
- }, {
123
- policy: ctx.config.delivery ?? {},
124
- signal: AbortSignal.any([signal, cancel.signal, AbortSignal.timeout(endpoint.timeoutMs)]),
125
- });
117
+ // Through the Tool Gate (F4.1): the gates service POSTs it under its own address policy.
118
+ result = await ctx.toolGate.post({
119
+ url: endpoint.url,
120
+ body,
121
+ headers: {
122
+ [SIGNATURE_HEADER]: minted.token,
123
+ [PROTOCOL_HEADER]: String(PROTOCOL_VERSION),
124
+ "idempotency-key": actionId,
125
+ },
126
+ timeoutMs: endpoint.timeoutMs,
127
+ }, AbortSignal.any([signal, cancel.signal, AbortSignal.timeout(endpoint.timeoutMs)]));
126
128
  }
127
129
  finally {
128
130
  inFlight();
@@ -370,10 +372,13 @@ export async function pingEndpoint(ctx, agentId) {
370
372
  body,
371
373
  ttlSeconds: TOKEN_SLACK_SECONDS,
372
374
  });
373
- const result = await post(endpoint.url, body, { [SIGNATURE_HEADER]: minted.token, [PROTOCOL_HEADER]: String(PROTOCOL_VERSION) }, {
374
- policy: ctx.config.delivery ?? {},
375
- signal: AbortSignal.timeout(Math.min(endpoint.timeoutMs, 10_000)),
376
- });
375
+ const timeoutMs = Math.min(endpoint.timeoutMs, 10_000);
376
+ const result = await ctx.toolGate.post({
377
+ url: endpoint.url,
378
+ body,
379
+ headers: { [SIGNATURE_HEADER]: minted.token, [PROTOCOL_HEADER]: String(PROTOCOL_VERSION) },
380
+ timeoutMs,
381
+ }, AbortSignal.timeout(timeoutMs));
377
382
  const at = new Date().toISOString();
378
383
  const refuse = async (code, message) => {
379
384
  await ctx.store.tx((t) => t.recordEndpointHealth(agentId, { kind: "failure", at, code, message }));
@@ -1,6 +1,6 @@
1
1
  import type { EffectResolution, HostEffect } from "@nylorun/harness/run";
2
2
  import type { AuthorizeResult } from "../vault/service.js";
3
- import { type Lease, type TenantContext } from "./context.js";
3
+ import { type Lease, type Session, type TenantContext } from "./context.js";
4
4
  import type { ModelProvider } from "../core/provider.js";
5
5
  /** What one advance's segment decides for all its effects. */
6
6
  export interface SegmentOptions {
@@ -13,6 +13,20 @@ export interface SegmentOptions {
13
13
  * advance decides what the abort means.
14
14
  */
15
15
  export declare function invokeModel(ctx: TenantContext, request: HostEffect, signal: AbortSignal, model?: ModelProvider): Promise<unknown>;
16
+ /**
17
+ * True when this Tenant's remote MCP calls outlive the process that sent them (the gates
18
+ * service, F4.1 G3): after a takeover or a shutdown, the journaled call is re-sent and joins
19
+ * the running call or gets its answer, instead of becoming `uncertain`.
20
+ */
21
+ export declare function recoversMcpCalls(ctx: TenantContext): boolean;
22
+ /** True when `request` calls a tool of a remote (`streamable-http` or `sse`) MCP server. */
23
+ export declare function isRemoteMcpEffect(s: Session, request: HostEffect): boolean;
24
+ /**
25
+ * True when this Tenant's vault-backed model calls outlive the process that sent them (the
26
+ * gates service, P1.2): after a takeover or a shutdown, the journaled call is re-sent and joins
27
+ * the running call or gets its outcome, instead of becoming `uncertain`.
28
+ */
29
+ export declare function recoversModelCalls(ctx: TenantContext): boolean;
16
30
  export declare function resolveEffect(ctx: TenantContext, request: HostEffect, signal: AbortSignal, lease: Lease, segment?: SegmentOptions): Promise<EffectResolution>;
17
31
  /** Journal and dispatch a new flow effect (agent / tool node / fn / verify). */
18
32
  export declare function resolveNewFlowEffect(ctx: TenantContext, request: HostEffect, signal: AbortSignal, lease: Lease): Promise<EffectResolution>;