@nylorun/runtime 0.13.0-beta → 0.14.0-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/CHANGELOG.md +330 -0
  2. package/README.md +69 -15
  3. package/dist/adapters/execution/restate.d.ts +7 -2
  4. package/dist/adapters/execution/restate.js +38 -8
  5. package/dist/adapters/replication/pgoutput.d.ts +16 -0
  6. package/dist/adapters/replication/pgoutput.js +169 -0
  7. package/dist/adapters/streams/s2.js +37 -20
  8. package/dist/admin-openapi.json +1071 -0
  9. package/dist/api/a2a/card.d.ts +15 -0
  10. package/dist/api/a2a/card.js +24 -0
  11. package/dist/api/a2a/endpoint.d.ts +8 -0
  12. package/dist/api/a2a/endpoint.js +81 -0
  13. package/dist/api/a2a/protocol.d.ts +144 -0
  14. package/dist/api/a2a/protocol.js +188 -0
  15. package/dist/api/a2a/routes.d.ts +19 -0
  16. package/dist/api/a2a/routes.js +289 -0
  17. package/dist/api/a2a/tasks.d.ts +50 -0
  18. package/dist/api/a2a/tasks.js +186 -0
  19. package/dist/api/ag-ui/endpoint.d.ts +8 -0
  20. package/dist/api/ag-ui/endpoint.js +126 -0
  21. package/dist/api/ag-ui/history.d.ts +7 -0
  22. package/dist/api/ag-ui/history.js +60 -0
  23. package/dist/api/ag-ui/routes.d.ts +32 -0
  24. package/dist/api/ag-ui/routes.js +305 -0
  25. package/dist/api/ag-ui/session-id.d.ts +1 -0
  26. package/dist/api/ag-ui/session-id.js +12 -0
  27. package/dist/api/ag-ui/sse.d.ts +9 -0
  28. package/dist/api/ag-ui/sse.js +7 -0
  29. package/dist/api/ag-ui/translate.d.ts +38 -0
  30. package/dist/api/ag-ui/translate.js +222 -0
  31. package/dist/api/components.d.ts +7825 -0
  32. package/dist/api/components.js +83 -0
  33. package/dist/api/http/app.d.ts +18 -0
  34. package/dist/api/http/app.js +58 -0
  35. package/dist/api/http/body.d.ts +4 -0
  36. package/dist/api/http/body.js +37 -0
  37. package/dist/api/http/define.d.ts +39 -0
  38. package/dist/api/http/define.js +130 -0
  39. package/dist/api/http/respond.d.ts +9 -0
  40. package/dist/api/http/respond.js +44 -0
  41. package/dist/api/http/routes/access.d.ts +24 -0
  42. package/dist/api/http/routes/access.js +259 -0
  43. package/dist/api/http/routes/actions.d.ts +8 -0
  44. package/dist/api/http/routes/actions.js +76 -0
  45. package/dist/api/http/routes/endpoints.d.ts +7 -0
  46. package/dist/api/http/routes/endpoints.js +100 -0
  47. package/dist/api/http/routes/sessions.d.ts +3 -0
  48. package/dist/api/http/routes/sessions.js +205 -0
  49. package/dist/api/http/routes/tenant.d.ts +10 -0
  50. package/dist/api/http/routes/tenant.js +172 -0
  51. package/dist/api/http/routes/vaults.d.ts +8 -0
  52. package/dist/api/http/routes/vaults.js +167 -0
  53. package/dist/api/openapi.d.ts +19 -0
  54. package/dist/api/openapi.js +138 -0
  55. package/dist/api/route.d.ts +12 -0
  56. package/dist/api/route.js +4 -0
  57. package/dist/core/bearer.d.ts +8 -0
  58. package/dist/core/bearer.js +11 -0
  59. package/dist/core/flow-host.d.ts +7 -15
  60. package/dist/core/flow-host.js +16 -41
  61. package/dist/core/sandbox-routes.d.ts +12 -7
  62. package/dist/core/sandbox-routes.js +24 -25
  63. package/dist/execution/memory.d.ts +8 -5
  64. package/dist/execution/memory.js +33 -13
  65. package/dist/execution/types.d.ts +38 -6
  66. package/dist/execution/types.js +12 -3
  67. package/dist/host/admin-api.d.ts +22 -0
  68. package/dist/host/admin-api.js +178 -0
  69. package/dist/host/app.d.ts +34 -0
  70. package/dist/host/app.js +185 -0
  71. package/dist/host/config-for.d.ts +2 -0
  72. package/dist/host/config-for.js +1 -0
  73. package/dist/host/config.d.ts +11 -0
  74. package/dist/host/cors.d.ts +27 -0
  75. package/dist/host/cors.js +64 -0
  76. package/dist/host/create-host.d.ts +31 -3
  77. package/dist/host/create-host.js +168 -327
  78. package/dist/host/http.d.ts +14 -3
  79. package/dist/host/http.js +64 -23
  80. package/dist/host/main.js +55 -4
  81. package/dist/host/stack-config.d.ts +20 -0
  82. package/dist/host/stack-config.js +55 -0
  83. package/dist/mcp/pool.d.ts +19 -1
  84. package/dist/mcp/pool.js +60 -10
  85. package/dist/model/catalog.d.ts +2 -10
  86. package/dist/model/classify.d.ts +13 -0
  87. package/dist/model/classify.js +61 -0
  88. package/dist/model/models.d.ts +10 -1
  89. package/dist/model/models.js +14 -6
  90. package/dist/model/pi-model.d.ts +17 -2
  91. package/dist/model/pi-model.js +366 -222
  92. package/dist/openapi.json +25696 -0
  93. package/dist/sandbox/manager.d.ts +13 -1
  94. package/dist/sandbox/manager.js +8 -11
  95. package/dist/sandbox/resolve.d.ts +33 -0
  96. package/dist/sandbox/resolve.js +81 -0
  97. package/dist/sandbox/session-sandbox.d.ts +11 -0
  98. package/dist/sandbox/session-sandbox.js +51 -0
  99. package/dist/sandbox/share.d.ts +14 -2
  100. package/dist/sandbox/share.js +33 -3
  101. package/dist/sandbox/tenant-config.d.ts +37 -0
  102. package/dist/sandbox/tenant-config.js +51 -0
  103. package/dist/sandbox/tools.d.ts +7 -9
  104. package/dist/sandbox/tools.js +11 -3
  105. package/dist/sandbox/types.d.ts +8 -2
  106. package/dist/store/event.d.ts +19 -0
  107. package/dist/store/event.js +49 -0
  108. package/dist/store/memory.d.ts +20 -8
  109. package/dist/store/memory.js +265 -100
  110. package/dist/store/postgres/connect.d.ts +13 -0
  111. package/dist/store/postgres/connect.js +5 -0
  112. package/dist/store/postgres/migrations/003_subject_tokens.d.ts +8 -0
  113. package/dist/store/postgres/migrations/003_subject_tokens.js +42 -0
  114. package/dist/store/postgres/migrations/004_publishable_keys.d.ts +6 -0
  115. package/dist/store/postgres/migrations/004_publishable_keys.js +18 -0
  116. package/dist/store/postgres/migrations/005_action_endpoints.d.ts +6 -0
  117. package/dist/store/postgres/migrations/005_action_endpoints.js +32 -0
  118. package/dist/store/postgres/migrations/006_drop_executors.d.ts +11 -0
  119. package/dist/store/postgres/migrations/006_drop_executors.js +32 -0
  120. package/dist/store/postgres/migrations/007_event_time.d.ts +7 -0
  121. package/dist/store/postgres/migrations/007_event_time.js +14 -0
  122. package/dist/store/postgres/migrations/008_durable_streams.d.ts +13 -0
  123. package/dist/store/postgres/migrations/008_durable_streams.js +35 -0
  124. package/dist/store/postgres/migrations/index.d.ts +5 -1
  125. package/dist/store/postgres/migrations/index.js +22 -2
  126. package/dist/store/postgres/migrations/shared/index.d.ts +31 -0
  127. package/dist/store/postgres/migrations/shared/index.js +92 -0
  128. package/dist/store/postgres/record.d.ts +11 -0
  129. package/dist/store/postgres/record.js +64 -0
  130. package/dist/store/postgres/store.js +337 -95
  131. package/dist/store/postgres/tenants.js +14 -0
  132. package/dist/store/types.d.ts +176 -81
  133. package/dist/streams/basin.d.ts +18 -1
  134. package/dist/streams/basin.js +34 -2
  135. package/dist/streams/control.d.ts +18 -0
  136. package/dist/streams/control.js +29 -0
  137. package/dist/streams/memory.d.ts +3 -1
  138. package/dist/streams/memory.js +14 -8
  139. package/dist/streams/relay/core.d.ts +35 -0
  140. package/dist/streams/relay/core.js +366 -0
  141. package/dist/streams/relay/memory.d.ts +69 -0
  142. package/dist/streams/relay/memory.js +176 -0
  143. package/dist/streams/relay/types.d.ts +77 -0
  144. package/dist/streams/relay/types.js +7 -0
  145. package/dist/streams/types.d.ts +66 -36
  146. package/dist/streams/types.js +18 -53
  147. package/dist/tenant/access-policy.d.ts +30 -0
  148. package/dist/tenant/access-policy.js +52 -0
  149. package/dist/tenant/actions.d.ts +10 -149
  150. package/dist/tenant/actions.js +5 -167
  151. package/dist/tenant/advance.js +32 -0
  152. package/dist/tenant/auth.d.ts +19 -17
  153. package/dist/tenant/auth.js +90 -93
  154. package/dist/tenant/browser.d.ts +15 -0
  155. package/dist/tenant/browser.js +39 -0
  156. package/dist/tenant/commands.d.ts +18 -2
  157. package/dist/tenant/commands.js +107 -113
  158. package/dist/tenant/context.d.ts +75 -18
  159. package/dist/tenant/context.js +14 -10
  160. package/dist/tenant/delivery-token.d.ts +41 -0
  161. package/dist/tenant/delivery-token.js +105 -0
  162. package/dist/tenant/delivery.d.ts +62 -0
  163. package/dist/tenant/delivery.js +476 -0
  164. package/dist/tenant/effects.d.ts +5 -0
  165. package/dist/tenant/effects.js +78 -37
  166. package/dist/tenant/endpoints.d.ts +67 -0
  167. package/dist/tenant/endpoints.js +90 -0
  168. package/dist/tenant/ephemeral.d.ts +6 -0
  169. package/dist/tenant/ephemeral.js +60 -36
  170. package/dist/tenant/http.d.ts +8 -9
  171. package/dist/tenant/http.js +13 -27
  172. package/dist/tenant/jwt.d.ts +16 -0
  173. package/dist/tenant/jwt.js +50 -0
  174. package/dist/tenant/module.d.ts +3 -0
  175. package/dist/tenant/module.js +6 -12
  176. package/dist/tenant/outbound.d.ts +44 -0
  177. package/dist/tenant/outbound.js +127 -0
  178. package/dist/tenant/pool.d.ts +0 -2
  179. package/dist/tenant/pool.js +0 -18
  180. package/dist/tenant/reset.d.ts +0 -4
  181. package/dist/tenant/reset.js +0 -5
  182. package/dist/tenant/runtime.d.ts +10 -20
  183. package/dist/tenant/runtime.js +59 -42
  184. package/dist/tenant/scheduler.d.ts +7 -2
  185. package/dist/tenant/scheduler.js +7 -3
  186. package/dist/tenant/session-streams.d.ts +125 -0
  187. package/dist/tenant/session-streams.js +399 -0
  188. package/dist/tenant/sessions.d.ts +28 -4
  189. package/dist/tenant/sessions.js +118 -24
  190. package/dist/tenant/signing-keys.d.ts +55 -0
  191. package/dist/tenant/signing-keys.js +172 -0
  192. package/dist/tenant/slim.d.ts +8 -0
  193. package/dist/tenant/slim.js +21 -0
  194. package/dist/tenant/status.d.ts +0 -3
  195. package/dist/tenant/status.js +19 -12
  196. package/dist/tenant/streams.d.ts +60 -67
  197. package/dist/tenant/streams.js +140 -208
  198. package/dist/tenant/subject-limits.d.ts +16 -0
  199. package/dist/tenant/subject-limits.js +45 -0
  200. package/dist/tenant/sweep.d.ts +1 -10
  201. package/dist/tenant/sweep.js +29 -69
  202. package/dist/tenant/tokens.d.ts +14 -0
  203. package/dist/tenant/tokens.js +178 -0
  204. package/dist/tenant/transcript.d.ts +48 -5
  205. package/dist/tenant/transcript.js +58 -1
  206. package/dist/tenant/types.d.ts +29 -7
  207. package/dist/tenant/worker.d.ts +3 -1
  208. package/dist/tenant/worker.js +9 -3
  209. package/dist/vault/service.d.ts +18 -2
  210. package/dist/vault/service.js +57 -13
  211. package/dist/version.d.ts +1 -1
  212. package/dist/version.js +1 -1
  213. package/package.json +20 -8
  214. package/dist/core/executors.d.ts +0 -44
  215. package/dist/core/executors.js +0 -95
  216. package/dist/streams/relay.d.ts +0 -37
  217. package/dist/streams/relay.js +0 -254
  218. package/dist/tenant/live.d.ts +0 -92
  219. package/dist/tenant/live.js +0 -290
  220. package/dist/tenant/routes-tenant.d.ts +0 -12
  221. package/dist/tenant/routes-tenant.js +0 -137
  222. package/dist/tenant/routes.d.ts +0 -9
  223. package/dist/tenant/routes.js +0 -151
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The session command service: `message`, `approve`/`respond`, `action_result` and `cancel`,
2
+ * The session command service: `message`, `approve`/`respond` and `cancel`,
3
3
  * with per-session idempotency keys. A command locks its session and commits its state
4
4
  * change, events and wakes in one transaction (the store publishes the events and runs the
5
5
  * `afterCommit` wakes), then aborts a cancelled advance and cascades workflow cancels to the
@@ -7,8 +7,9 @@
7
7
  *
8
8
  * Wakes (`ctx.wake`, architecture §12.3) carry the command type as the reason and a dedupe
9
9
  * key naming the cause: `<type>:<turnId>:<segment>` for `message`, `approve` and `respond`
10
- * (every accepted one writes a new checkpoint segment), and
11
- * `action_result:<turnId>:<actionId>:<generation>` for executor results.
10
+ * (every accepted one writes a new checkpoint segment). An Action's outcome
11
+ * (`recordActionOutcome`, from the deliverer) wakes with
12
+ * `action_result:<turnId>:<actionId>:<generation>`.
12
13
  *
13
14
  * Cancel commits `cancelled` first; the engine host sees it before its next effect and before
14
15
  * settlement on any Worker. It then aborts an advance running on this process
@@ -22,19 +23,21 @@ import { resolveMessageManifest } from "../core/turn-manifest.js";
22
23
  import { canonical } from "../store/canonical.js";
23
24
  import { lockedSession, } from "./context.js";
24
25
  import { fail } from "./http.js";
25
- import { ownerOf, scoped } from "./auth.js";
26
+ import { accessOf } from "./auth.js";
27
+ import { chargeTurn } from "./subject-limits.js";
26
28
  import { actionTarget, rebaseSessionState, turnManifestOf, variantStore, } from "./session.js";
27
29
  import { signalSessionCancel } from "./streams.js";
28
30
  import { toolIds } from "./transcript.js";
29
- /** A tool result whose output does not match the Action's stored output schema fails the tool. */
30
- function acceptedToolResult(action, command) {
31
+ import { slimModelEffects } from "./slim.js";
32
+ /** `outcome`, or a failed one when a tool's output does not match its stored output schema. */
33
+ export function acceptedOutcome(action, outcome) {
31
34
  if (action.kind !== "tool" || !action.outputSchema)
32
- return command;
33
- const value = command.outcome.value;
35
+ return outcome;
36
+ const value = outcome.value;
34
37
  // Only a result carries output: failures, denials, interactions and deferrals pass as sent.
35
38
  if (!isResultToolValue(value))
36
- return command;
37
- // Executors wrap successful tool output as `{ kind: "completed", output }`.
39
+ return outcome;
40
+ // Endpoints wrap successful tool output as `{ kind: "completed", output }`.
38
41
  // Validate the tool payload, not the outcome envelope.
39
42
  const candidate = completedToolOutput(value);
40
43
  let matches = false;
@@ -45,16 +48,13 @@ function acceptedToolResult(action, command) {
45
48
  matches = false;
46
49
  }
47
50
  if (matches)
48
- return command;
51
+ return outcome;
49
52
  return {
50
- ...command,
51
- outcome: {
52
- ...command.outcome,
53
- value: {
54
- kind: "failed",
55
- code: "tool.invalid-output",
56
- message: "Tool result does not match the output schema stored on the action",
57
- },
53
+ ...outcome,
54
+ value: {
55
+ kind: "failed",
56
+ code: "tool.invalid-output",
57
+ message: "Tool result does not match the output schema stored on the action",
58
58
  },
59
59
  };
60
60
  }
@@ -89,18 +89,15 @@ export async function command(ctx, id, input, scope) {
89
89
  const { store } = ctx;
90
90
  const cascadeCancelIds = [];
91
91
  const response = await store.tx(async (t) => {
92
- const s = await lockedSession(t, id, ownerOf(scope));
93
- let command = input;
94
- if (command.type === "action_result") {
95
- const a = (await t.get("actions", command.actionId)) ??
96
- fail(404, "Action not found");
97
- if (a.sessionId !== id)
98
- fail(403, "Action belongs to another session");
99
- scoped(scope, a);
100
- command = acceptedToolResult(a, command);
101
- }
102
- else if (scope.kind === "executor")
103
- fail(403, "Application credential required");
92
+ const s = await lockedSession(t, id, accessOf(scope));
93
+ const command = input;
94
+ // A person's token may not replace the instructions of a turn.
95
+ if (scope.kind === "token" &&
96
+ command.type === "message" &&
97
+ command.manifest !== undefined)
98
+ fail(403, "A subject token cannot send message.manifest", {
99
+ code: "scope_required",
100
+ });
104
101
  const key = commandKey(id, command.idempotencyKey);
105
102
  const existing = await t.get("commands", key);
106
103
  if (existing) {
@@ -111,85 +108,7 @@ export async function command(ctx, id, input, scope) {
111
108
  let event;
112
109
  // The turn a `cancel` ended, if one was active.
113
110
  let cancelledTurnId = null;
114
- if (command.type === "action_result") {
115
- const action = (await t.get("actions", command.actionId));
116
- const prior = await t.get("effects", command.actionId);
117
- if (action.status === "completed") {
118
- if (action.claimId !== command.claimId ||
119
- action.generation !== command.generation ||
120
- canonical(prior.outcome) !== canonical(command.outcome))
121
- fail(409, "Conflicting action result");
122
- await t.put("commands", key, { command, response: prior.receipt });
123
- return prior.receipt;
124
- }
125
- if (s.status === "cancelled" ||
126
- s.activeTurnId !== action.turnId ||
127
- action.status !== "claimed" ||
128
- action.claimId !== command.claimId ||
129
- action.generation !== command.generation ||
130
- Date.parse(action.leaseExpiresAt) <= Date.now())
131
- fail(409, "Stale, expired, or cancelled claim");
132
- action.status = "completed";
133
- await t.put("actions", action.actionId, action);
134
- prior.status = "completed";
135
- prior.outcome = command.outcome;
136
- s.status = "runnable";
137
- const resultWake = {
138
- reason: "action_result",
139
- dedupeKey: `action_result:${action.turnId}:${action.actionId}:${action.generation}`,
140
- };
141
- t.afterCommit(() => ctx.wake(id, resultWake));
142
- event = await t.event(id, s.activeTurnId, "action.completed", {
143
- actionId: action.actionId,
144
- ...actionTarget(action),
145
- kind: action.kind,
146
- ...(action.kind === "tool" ? toolIds(action.context) : {}),
147
- result: command.outcome.value,
148
- });
149
- if (action.kind === "verify") {
150
- const verdict = command.outcome.value;
151
- if (verdict?.kind !== "failed") {
152
- await t.event(id, s.activeTurnId, "loop.verified", {
153
- path: String(action.context?.loopPath ?? action.path ?? ""),
154
- n: Number(action.context?.n ?? 1),
155
- pass: Boolean(verdict?.pass),
156
- ...(verdict?.feedback !== undefined
157
- ? { feedback: verdict.feedback }
158
- : {}),
159
- ...(verdict?.data !== undefined ? { data: verdict.data } : {}),
160
- });
161
- }
162
- }
163
- else if (action.kind === "fn" && action.context?.role === "decide") {
164
- const decision = command.outcome.value;
165
- await t.event(id, s.activeTurnId, "loop.decided", {
166
- path: String(action.context?.loopPath ?? action.path ?? ""),
167
- n: Number(action.context?.n ?? 1),
168
- next: "output" in decision && !("input" in decision)
169
- ? "output"
170
- : "input",
171
- patched: Boolean(decision.agent),
172
- });
173
- }
174
- const receipt = {
175
- status: "accepted",
176
- turnId: s.activeTurnId,
177
- cursor: event.cursor,
178
- requestId: command.requestId,
179
- };
180
- prior.receipt = receipt;
181
- await t.put("effects", action.actionId, prior);
182
- if (isWorkflowManifest(s.manifest) && s.activeTurnId) {
183
- await wakeForQueuedEffects({
184
- t,
185
- workflowSessionId: id,
186
- turnId: s.activeTurnId,
187
- limits: ctx.flowLimits,
188
- schedule: ctx.wake,
189
- });
190
- }
191
- }
192
- else if (command.type === "cancel") {
111
+ if (command.type === "cancel") {
193
112
  cancelledTurnId = s.activeTurnId;
194
113
  const workflowCancel = isWorkflowManifest(s.manifest);
195
114
  // Linked sessions this one started: a workflow's agents, or flow agents an agent
@@ -210,10 +129,10 @@ export async function command(ctx, id, input, scope) {
210
129
  else if (cancelledTurnId !== null) {
211
130
  for (const a of await t.actionsForSession(id, {
212
131
  turnId: cancelledTurnId,
213
- statuses: ["pending", "claimed"],
132
+ statuses: ["pending", "delivering"],
214
133
  })) {
215
- // Claimed work may already have an external effect. Preserve it for reconciliation.
216
- a.status = a.status === "claimed" ? "uncertain" : "cancelled";
134
+ // Delivered work may already have an external effect. Preserve it for reconciliation.
135
+ a.status = a.status === "pending" ? "cancelled" : "uncertain";
217
136
  await t.put("actions", a.actionId, a);
218
137
  const effect = await t.get("effects", a.actionId);
219
138
  if (effect) {
@@ -232,6 +151,7 @@ export async function command(ctx, id, input, scope) {
232
151
  event = await t.event(id, cancelledTurnId, "turn.cancelled", {
233
152
  reason: command.reason,
234
153
  });
154
+ await slimModelEffects(t, id, cancelledTurnId);
235
155
  // The process running the advance aborts it on `session.cancel` (tenant/control).
236
156
  t.afterCommit(() => signalSessionCancel(ctx, id));
237
157
  s.activeTurnId = null;
@@ -258,6 +178,9 @@ export async function command(ctx, id, input, scope) {
258
178
  if (command.type === "message") {
259
179
  if (!["idle", "completed", "failed", "cancelled"].includes(s.status))
260
180
  fail(409, "Session has active or unresolved work");
181
+ // After the replay check above, so a retried message is never charged twice.
182
+ if (scope.kind === "token" && scope.limits)
183
+ await chargeTurn(t, scope.subject, scope.limits);
261
184
  s.turnStartState = s.state;
262
185
  s.activeTurnId = randomUUID();
263
186
  if (isWorkflowManifest(s.manifest)) {
@@ -389,3 +312,74 @@ export async function command(ctx, id, input, scope) {
389
312
  }
390
313
  return response;
391
314
  }
315
+ /**
316
+ * Completes a delivered Action with `outcome`, in the caller's transaction, which
317
+ * holds the lock of the Action's session `s`: the Action and its effect, the
318
+ * `action.completed` event (and `loop.verified` or `loop.decided`), the wake that resumes the
319
+ * turn, and queued workflow effects. The Action deliverer and the background-result callback
320
+ * both record outcomes here. It sets `s.status`; the caller writes `s`.
321
+ */
322
+ export async function recordActionOutcome(t, ctx, s, action, received, options = {}) {
323
+ const outcome = acceptedOutcome(action, received);
324
+ const prior = await t.get("effects", action.actionId);
325
+ action.status = "completed";
326
+ await t.put("actions", action.actionId, action);
327
+ prior.status = "completed";
328
+ prior.outcome = outcome;
329
+ s.status = "runnable";
330
+ const resultWake = {
331
+ reason: "action_result",
332
+ dedupeKey: `action_result:${action.turnId}:${action.actionId}:${action.generation}`,
333
+ };
334
+ t.afterCommit(() => ctx.wake(s.id, resultWake));
335
+ const event = await t.event(s.id, s.activeTurnId, "action.completed", {
336
+ actionId: action.actionId,
337
+ ...actionTarget(action),
338
+ kind: action.kind,
339
+ ...(action.kind === "tool" ? toolIds(action.context) : {}),
340
+ result: outcome.value,
341
+ });
342
+ if (action.kind === "verify") {
343
+ const verdict = outcome.value;
344
+ if (verdict?.kind !== "failed") {
345
+ await t.event(s.id, s.activeTurnId, "loop.verified", {
346
+ path: String(action.context?.loopPath ?? action.path ?? ""),
347
+ n: Number(action.context?.n ?? 1),
348
+ pass: Boolean(verdict?.pass),
349
+ ...(verdict?.feedback !== undefined
350
+ ? { feedback: verdict.feedback }
351
+ : {}),
352
+ ...(verdict?.data !== undefined ? { data: verdict.data } : {}),
353
+ });
354
+ }
355
+ }
356
+ else if (action.kind === "fn" && action.context?.role === "decide") {
357
+ const decision = outcome.value;
358
+ await t.event(s.id, s.activeTurnId, "loop.decided", {
359
+ path: String(action.context?.loopPath ?? action.path ?? ""),
360
+ n: Number(action.context?.n ?? 1),
361
+ next: "output" in decision && !("input" in decision)
362
+ ? "output"
363
+ : "input",
364
+ patched: Boolean(decision.agent),
365
+ });
366
+ }
367
+ const receipt = {
368
+ status: "accepted",
369
+ turnId: s.activeTurnId,
370
+ cursor: event.cursor,
371
+ ...(options.requestId === undefined ? {} : { requestId: options.requestId }),
372
+ };
373
+ prior.receipt = receipt;
374
+ await t.put("effects", action.actionId, prior);
375
+ if (isWorkflowManifest(s.manifest) && s.activeTurnId) {
376
+ await wakeForQueuedEffects({
377
+ t,
378
+ workflowSessionId: s.id,
379
+ turnId: s.activeTurnId,
380
+ limits: ctx.flowLimits,
381
+ schedule: ctx.wake,
382
+ });
383
+ }
384
+ return { event, receipt };
385
+ }
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * Business code changes state only inside `ctx.store.tx(async (t) => …)` and follows the
7
7
  * seam rules: events through `t.event(...)` (the relay appends them to Durable Streams after
8
- * commit), executor wakes through `t.signalWork()`, and advances through
8
+ * commit), deliveries through `t.afterCommit(() => ctx.deliver(actionId))`, and advances through
9
9
  * `t.afterCommit(() => ctx.wake(id, { reason, dedupeKey }))`. It never publishes or notifies
10
10
  * itself; `runtime.ts` wires the streams with `wireStreams()`. No external I/O runs inside a
11
11
  * tx.
@@ -13,12 +13,11 @@
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 { SubjectScope, TenantEnvelope } from "@nylorun/core/contracts";
16
+ import type { RoleLimits, SubjectScope, TenantEnvelope } from "@nylorun/core/contracts";
17
17
  import type { DurableCheckpoint, FlowCheckpoint } from "@nylorun/harness/run";
18
- import type { AgentManifest, JsonValue } from "@nylorun/core/define";
18
+ import type { AgentManifest, JsonValue, SandboxManifest } from "@nylorun/core/define";
19
19
  import type { CredentialSelection } from "@nylorun/core/contracts";
20
20
  import type { SessionStore, Tx } from "../store/types.js";
21
- import type { ExecutorRecord, ExecutorRegistry } from "../core/executors.js";
22
21
  import type { FlowLimits } from "../core/limits.js";
23
22
  import type { ModelProvider } from "../core/provider.js";
24
23
  import type { VaultService } from "../vault/service.js";
@@ -26,9 +25,10 @@ import type { McpPool } from "../mcp/pool.js";
26
25
  import type { McpDiagnostic, McpSnapshot } from "../mcp/snapshot.js";
27
26
  import type { SandboxManager } from "../sandbox/manager.js";
28
27
  import type { TenantConfig } from "./types.js";
29
- import type { LiveHub } from "./live.js";
28
+ import type { SessionStreams } from "./session-streams.js";
30
29
  import type { StuckInvocation, Wake } from "../execution/types.js";
31
30
  import type { WorkState } from "./scheduler.js";
31
+ import type { SigningKeys } from "./signing-keys.js";
32
32
  export interface Session {
33
33
  id: string;
34
34
  agentId: string;
@@ -64,31 +64,73 @@ export interface Session {
64
64
  /** Session id that keys the shared sandbox; absent means this session owns it. */
65
65
  sandboxOwnerId?: string;
66
66
  /**
67
- * The incarnation naming this session's event stream (`sessions/<id>/<incarnation>`), set at
68
- * creation and never changed. Absent only on sessions created before incarnations.
67
+ * The sandbox chosen when the session was opened, resolved against the Tenant's limits, or
68
+ * inherited from the session it shares with. Absent when the session has no sandbox or its
69
+ * definition declares one (`.sandbox()`).
69
70
  */
70
- streamIncarnation?: string;
71
+ sandbox?: SandboxManifest;
72
+ /** Where `sandbox` came from. */
73
+ sandboxSource?: "default" | "inline" | "shared";
71
74
  }
72
75
  export type AuthScope = {
73
76
  kind: "application";
74
77
  principalId: string;
75
78
  }
79
+ /**
80
+ * A subject token (Host feature `subject-tokens`): one subject, the role's scopes and agents
81
+ * narrowed by the token, until `expiresAt` (ms) or the subject's epoch moves past `epoch`.
82
+ */
83
+ | {
84
+ kind: "token";
85
+ subject: string;
86
+ scopes: ReadonlySet<SubjectScope>;
87
+ agents: ReadonlySet<string> | "*";
88
+ role: string;
89
+ limits?: RoleLimits;
90
+ epoch: number;
91
+ expiresAt: number;
92
+ tokenId: string;
93
+ keyId: string;
94
+ }
76
95
  /** An application principal acting for `subject` (`Nylorun-Subject`), narrowed to `scopes`. */
77
96
  | {
78
97
  kind: "subject";
79
98
  principalId: string;
80
99
  subject: string;
81
100
  scopes: ReadonlySet<SubjectScope>;
82
- } | {
83
- kind: "executor";
84
- executor: ExecutorRecord;
101
+ }
102
+ /** No credential, on a route that serves public data (`RouteAccess.anonymous`). */
103
+ | {
104
+ kind: "anonymous";
105
+ }
106
+ /**
107
+ * A delivery token (Action endpoints): the Runtime's own token for one delivery of one
108
+ * Action, presented back by the Action endpoint on that Action's callbacks only.
109
+ */
110
+ | {
111
+ kind: "delivery";
112
+ actionId: string;
113
+ agentId: string;
114
+ generation: number;
115
+ expiresAt: number;
116
+ tokenId: string;
117
+ keyId: string;
118
+ }
119
+ /**
120
+ * A publishable key with no bearer (Host feature `browser-access`): what the policy grants
121
+ * `anon`, at most `agents:read`. It owns no session or vault.
122
+ */
123
+ | {
124
+ kind: "publishable";
125
+ keyId: string;
126
+ scopes: ReadonlySet<SubjectScope>;
127
+ agents: ReadonlySet<string> | "*";
85
128
  };
86
129
  export interface TenantContext {
87
130
  readonly config: TenantConfig;
88
131
  readonly envelope: TenantEnvelope;
89
132
  readonly store: SessionStore;
90
133
  readonly vault: VaultService;
91
- readonly registry: ExecutorRegistry;
92
134
  readonly mcp: McpPool;
93
135
  readonly sandbox: SandboxManager;
94
136
  readonly flowLimits: FlowLimits;
@@ -101,8 +143,10 @@ export interface TenantContext {
101
143
  closed: boolean;
102
144
  /** Advances running on this process, for `abortLocal`, drain and close. */
103
145
  readonly work: WorkState;
104
- /** Live delivery over Durable Streams: session feeds, executor streams, the streams wiring. */
105
- readonly live: LiveHub;
146
+ /** Live delivery over Durable Streams: one `SessionStream` per observed session, and the streams wiring. */
147
+ readonly sessionStreams: SessionStreams;
148
+ /** The Tenant's signing keys for subject tokens. */
149
+ readonly signingKeys: SigningKeys;
106
150
  /** The Worker id this process writes as session `owner` (§10.6). */
107
151
  readonly workerId: string;
108
152
  /** How long an advance's ownership lease lasts; the heartbeat renews it. */
@@ -115,17 +159,30 @@ export interface TenantContext {
115
159
  wake(sessionId: string, wake: Wake): Promise<void>;
116
160
  /** Seam: abort the advance of a session running in this process, if any. */
117
161
  abortLocal(sessionId: string): void;
162
+ /**
163
+ * Seam: deliver an Action to its agent's Action endpoint (`DurableExecution.deliver`). Call it
164
+ * from `t.afterCommit`. Dropped while the Tenant is closing; the sweep re-sends it.
165
+ */
166
+ deliver(actionId: string): Promise<void>;
118
167
  /** This Tenant's execution invocations that need an operator, for Tenant status. */
119
168
  readonly stuckInvocations?: () => Promise<StuckInvocation[]>;
120
169
  /** Callbacks the Tenant sweep runs after its own steps (`sweep.ts`). */
121
170
  readonly sweepHooks: ReadonlySet<() => Promise<void>>;
122
- /** Adds a sweep callback (the outbox drain, Wave 2 / Y). Returns a function that removes it. */
171
+ /** Adds a sweep callback. Returns a function that removes it. */
123
172
  onSweep(hook: () => Promise<void>): () => void;
124
173
  }
174
+ /**
175
+ * What a request acting for a person may reach: that person's sessions and vaults, and, for a
176
+ * subject token, only sessions of the agents its role allows. Undefined means the whole Tenant.
177
+ */
178
+ export interface SessionAccess {
179
+ readonly owner: string;
180
+ readonly agents?: ReadonlySet<string>;
181
+ }
125
182
  /** The session, or a 404. Reads without locking. */
126
- export declare function sessionOf(t: Tx, id: string, owner?: string): Promise<Session>;
183
+ export declare function sessionOf(t: Tx, id: string, access?: SessionAccess): Promise<Session>;
127
184
  /** Locks the session for the rest of the transaction and returns it, or a 404. */
128
- export declare function lockedSession(t: Tx, id: string, owner?: string): Promise<Session>;
185
+ export declare function lockedSession(t: Tx, id: string, access?: SessionAccess): Promise<Session>;
129
186
  /** An advance's hold on its session (§10.6): every write the advance makes presents `epoch`. */
130
187
  export interface Lease {
131
188
  readonly sessionId: string;
@@ -139,7 +196,7 @@ export interface Lease {
139
196
  */
140
197
  export declare function ownedSession(t: Tx, lease: Lease, id: string): Promise<Session>;
141
198
  /** The session read in its own transaction, or a 404. */
142
- export declare function loadSession(ctx: TenantContext, id: string, owner?: string): Promise<Session>;
199
+ export declare function loadSession(ctx: TenantContext, id: string, access?: SessionAccess): Promise<Session>;
143
200
  /**
144
201
  * A synchronous lookup over `start` and every session its `sandboxOwnerId` chain names, for
145
202
  * the sandbox-sharing helpers, which walk that chain synchronously.
@@ -1,20 +1,24 @@
1
1
  import { fail } from "./http.js";
2
2
  /**
3
- * `owner`, when set, is the subject the request acts for: another owner's session is the same
4
- * 404 as a missing one, so a subject cannot learn which session ids exist.
3
+ * `access`, when set, limits the request to the owner's sessions of the allowed agents:
4
+ * anything else is the same 404 as a missing session, so a subject cannot learn which session
5
+ * ids exist.
5
6
  */
6
- function owned(session, owner) {
7
- if (!session || (owner !== undefined && session.ownerUserId !== owner))
7
+ function owned(session, access) {
8
+ if (!session ||
9
+ (access !== undefined &&
10
+ (session.ownerUserId !== access.owner ||
11
+ (access.agents !== undefined && !access.agents.has(session.agentId)))))
8
12
  return fail(404, "Session not found");
9
13
  return session;
10
14
  }
11
15
  /** The session, or a 404. Reads without locking. */
12
- export async function sessionOf(t, id, owner) {
13
- return owned(await t.get("sessions", id), owner);
16
+ export async function sessionOf(t, id, access) {
17
+ return owned(await t.get("sessions", id), access);
14
18
  }
15
19
  /** Locks the session for the rest of the transaction and returns it, or a 404. */
16
- export async function lockedSession(t, id, owner) {
17
- return owned(await t.lockSession(id), owner);
20
+ export async function lockedSession(t, id, access) {
21
+ return owned(await t.lockSession(id), access);
18
22
  }
19
23
  /**
20
24
  * Locks `id` for the advance holding `lease`. The advance's own session is epoch-checked: a
@@ -27,8 +31,8 @@ export async function ownedSession(t, lease, id) {
27
31
  return lockedSession(t, id);
28
32
  }
29
33
  /** The session read in its own transaction, or a 404. */
30
- export function loadSession(ctx, id, owner) {
31
- return ctx.store.tx((t) => sessionOf(t, id, owner));
34
+ export function loadSession(ctx, id, access) {
35
+ return ctx.store.tx((t) => sessionOf(t, id, access));
32
36
  }
33
37
  /**
34
38
  * A synchronous lookup over `start` and every session its `sandboxOwnerId` chain names, for
@@ -0,0 +1,41 @@
1
+ import type { AuthScope, TenantContext } from "./context.js";
2
+ /** What a delivery token is for: one delivery of an Action, or a ping. */
3
+ export type DeliveryTokenFor = {
4
+ kind: "action";
5
+ actionId: string;
6
+ agentId: string;
7
+ generation: number;
8
+ } | {
9
+ kind: "ping";
10
+ agentId: string;
11
+ };
12
+ export interface MintedDeliveryToken {
13
+ token: string;
14
+ /** Milliseconds since the epoch. */
15
+ expiresAt: number;
16
+ tokenId: string;
17
+ keyId: string;
18
+ }
19
+ /** The request scope of a verified delivery token (`AuthScope` kind `delivery`). */
20
+ export type DeliveryScope = Extract<AuthScope, {
21
+ kind: "delivery";
22
+ }>;
23
+ /** base64url SHA-256 of a request body: the token's `bdy` claim. */
24
+ export declare function bodyHash(body: string | Uint8Array): string;
25
+ /**
26
+ * Signs a token for `delivery`, for the endpoint at `audience`, covering `body`. `ttlSeconds`
27
+ * is capped at `DELIVERY_TOKEN_MAX_TTL_SECONDS`.
28
+ */
29
+ export declare function mintDeliveryToken(ctx: TenantContext, options: {
30
+ for: DeliveryTokenFor;
31
+ audience: string;
32
+ body: string | Uint8Array;
33
+ ttlSeconds: number;
34
+ }): Promise<MintedDeliveryToken>;
35
+ /**
36
+ * Verifies a delivery token presented as a bearer. Answers `401 token_expired` for an expired
37
+ * token or a revoked key, and the opaque 404 for everything else, a ping token included: only
38
+ * an Action's token reaches its callbacks. Whether the Action is still being delivered at the
39
+ * token's generation is checked where the Action is read (`auth.ts` `scoped`).
40
+ */
41
+ export declare function verifyDeliveryToken(ctx: TenantContext, raw: string): Promise<DeliveryScope>;
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Delivery tokens (design: Action endpoints §8.1): the ES256 JWT the Runtime signs, with the
3
+ * Tenant's current signing key, for one delivery of one Action, or for a ping. It travels in
4
+ * `Nylorun-Signature` on the request to the Action endpoint, which verifies it with the
5
+ * Tenant's public keys, and comes back as the bearer of that Action's callbacks (a background
6
+ * result, heartbeats, sandbox tools).
7
+ *
8
+ * A token lives at most `DELIVERY_TOKEN_MAX_TTL_SECONDS`, the subject-token maximum, and key
9
+ * rotation waits at least that long before it revokes the previous key
10
+ * (`signing-keys.ts`), so a rotation never revokes a live token.
11
+ */
12
+ import { createHash, randomUUID } from "node:crypto";
13
+ import { errors, jwtVerify, SignJWT } from "jose";
14
+ import { DELIVERY_TOKEN_MAX_TTL_SECONDS, DELIVERY_TOKEN_TYPE, subjectTokenIssuer, } from "@nylorun/core/contracts";
15
+ import { failOpaque, HttpError } from "./http.js";
16
+ import { CLOCK_TOLERANCE_SECONDS, tokenKeyId } from "./jwt.js";
17
+ /** base64url SHA-256 of a request body: the token's `bdy` claim. */
18
+ export function bodyHash(body) {
19
+ return createHash("sha256").update(body).digest("base64url");
20
+ }
21
+ /**
22
+ * Signs a token for `delivery`, for the endpoint at `audience`, covering `body`. `ttlSeconds`
23
+ * is capped at `DELIVERY_TOKEN_MAX_TTL_SECONDS`.
24
+ */
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
+ const ttl = Math.max(1, Math.min(Math.floor(options.ttlSeconds), DELIVERY_TOKEN_MAX_TTL_SECONDS));
30
+ const iat = Math.floor(Date.now() / 1000);
31
+ const tokenId = randomUUID();
32
+ 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 };
47
+ }
48
+ /**
49
+ * Verifies a delivery token presented as a bearer. Answers `401 token_expired` for an expired
50
+ * token or a revoked key, and the opaque 404 for everything else, a ping token included: only
51
+ * an Action's token reaches its callbacks. Whether the Action is still being delivered at the
52
+ * token's generation is checked where the Action is read (`auth.ts` `scoped`).
53
+ */
54
+ export async function verifyDeliveryToken(ctx, raw) {
55
+ const checked = tokenKeyId(raw, DELIVERY_TOKEN_TYPE);
56
+ if ("refused" in checked)
57
+ return refused(ctx, `delivery_token_${checked.refused}`);
58
+ const { kid } = checked;
59
+ const row = await ctx.store.tx((t) => t.signingKey(kid));
60
+ if (!row)
61
+ return refused(ctx, "delivery_token_key_unknown");
62
+ let claims;
63
+ try {
64
+ const verified = await jwtVerify(raw, await ctx.signingKeys.publicKey(row), {
65
+ algorithms: ["ES256"],
66
+ issuer: subjectTokenIssuer(ctx.config.tenantId),
67
+ typ: DELIVERY_TOKEN_TYPE,
68
+ clockTolerance: CLOCK_TOLERANCE_SECONDS,
69
+ requiredClaims: ["sub", "aud", "iat", "exp", "jti"],
70
+ });
71
+ claims = verified.payload;
72
+ }
73
+ catch (error) {
74
+ if (error instanceof errors.JWTExpired)
75
+ expired(ctx, "delivery_token_expired");
76
+ return refused(ctx, "delivery_token_invalid");
77
+ }
78
+ if (row.state === "revoked")
79
+ expired(ctx, "delivery_token_key_revoked");
80
+ const { sub, agt, gen, exp, iat, jti } = claims;
81
+ if (sub === "ping" ||
82
+ typeof agt !== "string" ||
83
+ typeof gen !== "number" ||
84
+ !Number.isInteger(gen) ||
85
+ gen < 1 ||
86
+ exp - iat > DELIVERY_TOKEN_MAX_TTL_SECONDS)
87
+ return refused(ctx, "delivery_token_claims");
88
+ return {
89
+ kind: "delivery",
90
+ actionId: sub,
91
+ agentId: agt,
92
+ generation: gen,
93
+ expiresAt: exp * 1000,
94
+ tokenId: jti,
95
+ keyId: kid,
96
+ };
97
+ }
98
+ function refused(ctx, reason) {
99
+ ctx.config.logger.warn("credential rejected", { reason });
100
+ return failOpaque();
101
+ }
102
+ function expired(ctx, reason) {
103
+ ctx.config.logger.warn("delivery token refused", { reason });
104
+ throw new HttpError(401, "The delivery token has expired or its key was revoked", { code: "token_expired" }, { "www-authenticate": 'Bearer error="invalid_token"' });
105
+ }