@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,21 +1,7 @@
1
- /**
2
- * The Action service and the executor registry: discover pending Actions, claim, heartbeat,
3
- * claim-scoped sandbox tool calls, and executor registration, listing and removal.
4
- *
5
- * A claim or heartbeat locks the Action's session and commits in one transaction; the
6
- * registry is updated only after the executors table commits.
7
- *
8
- * Later waves: Wave 2 / Y reads the executor `connected` flag from the work stream instead
9
- * of `executorStreams`.
10
- */
11
- import { randomUUID } from "node:crypto";
12
- import { ActionClaimRequestSchema, ActionHeartbeatRequestSchema, } from "@nylorun/core/contracts";
13
- import { assertExecutorCredential, hashToken, } from "../core/executors.js";
14
1
  import { SandboxRouteError, handleActionSandboxTool, } from "../core/sandbox-routes.js";
15
- import { lockedSession, sandboxLookup, sessionOf, } from "./context.js";
16
- import { fail, readBody, requestAborted } from "./http.js";
17
2
  import { scoped } from "./auth.js";
18
- import { endExecutorStreams, executorConnected } from "./live.js";
3
+ import { sandboxLookup, sessionOf } from "./context.js";
4
+ import { fail } from "./http.js";
19
5
  /** Session and Action lookups for the sandbox route handlers. */
20
6
  export function sandboxRouteDeps(ctx) {
21
7
  return {
@@ -27,19 +13,13 @@ export function sandboxRouteDeps(ctx) {
27
13
  getAction: (id) => ctx.store.tx((t) => t.get("actions", id)),
28
14
  };
29
15
  }
30
- /** `GET /v1/actions`: the pending Actions of the executor's agent. */
31
- export async function listPendingActions(ctx, executor) {
32
- return {
33
- actions: await ctx.store.tx((t) => t.pendingActions(executor.agentId)),
34
- };
35
- }
36
- /** `POST /v1/actions/:id/sandbox/:tool`: a claim-scoped sandbox tool call. */
37
- export async function actionSandboxTool(ctx, scope, actionId, toolName, request) {
16
+ /** `POST /v1/actions/:id/sandbox/:tool`: a sandbox tool call for the Action being delivered. */
17
+ export async function actionSandboxTool(ctx, scope, actionId, toolName, body, signal) {
38
18
  const action = (await ctx.store.tx((t) => t.get("actions", actionId))) ??
39
19
  fail(404, "Action not found");
40
20
  scoped(scope, action);
41
21
  try {
42
- return await handleActionSandboxTool(sandboxRouteDeps(ctx), actionId, toolName, await readBody(request), requestAborted(request));
22
+ return await handleActionSandboxTool(sandboxRouteDeps(ctx), actionId, toolName, await body(), signal, { generation: scope.generation });
43
23
  }
44
24
  catch (error) {
45
25
  if (error instanceof SandboxRouteError)
@@ -47,145 +27,3 @@ export async function actionSandboxTool(ctx, scope, actionId, toolName, request)
47
27
  throw error;
48
28
  }
49
29
  }
50
- /** `POST /v1/actions/:id/claim` and `/heartbeat`; anything else under the Action is a 404. */
51
- export function updateAction(ctx, scope, actionId, method, operation, body) {
52
- return ctx.store.tx(async (t) => {
53
- const found = (await t.get("actions", actionId)) ??
54
- fail(404, "Action not found");
55
- scoped(scope, found);
56
- const s = await lockedSession(t, found.sessionId);
57
- // Read again under the session lock.
58
- const action = (await t.get("actions", actionId)) ??
59
- fail(404, "Action not found");
60
- if (method === "POST" && operation === "claim") {
61
- ActionClaimRequestSchema.parse(body);
62
- if (action.status !== "pending" ||
63
- s.status === "cancelled" ||
64
- s.activeTurnId !== action.turnId)
65
- fail(409, "Action unavailable");
66
- action.status = "claimed";
67
- action.generation++;
68
- action.claimId = randomUUID();
69
- action.leaseExpiresAt = new Date(Date.now() + (ctx.config.leaseMs ?? 30000)).toISOString();
70
- await t.put("actions", actionId, action);
71
- await t.event(action.sessionId, action.turnId, "action.claimed", {
72
- actionId,
73
- generation: action.generation,
74
- ...(action.agent ? { agent: action.agent } : {}),
75
- });
76
- return {
77
- action,
78
- claimId: action.claimId,
79
- generation: action.generation,
80
- leaseExpiresAt: action.leaseExpiresAt,
81
- };
82
- }
83
- if (method === "POST" && operation === "heartbeat") {
84
- const beat = ActionHeartbeatRequestSchema.parse(body);
85
- if (s.activeTurnId !== action.turnId ||
86
- action.status !== "claimed" ||
87
- action.claimId !== beat.claimId ||
88
- action.generation !== beat.generation ||
89
- Date.parse(action.leaseExpiresAt) <= Date.now())
90
- fail(409, "Stale or expired claim");
91
- action.leaseExpiresAt = new Date(Date.now() + (ctx.config.leaseMs ?? 30000)).toISOString();
92
- await t.put("actions", actionId, action);
93
- return { leaseExpiresAt: action.leaseExpiresAt };
94
- }
95
- return fail(404, "Route not found");
96
- });
97
- }
98
- /** `GET /v1/executors`. */
99
- export function listExecutors(ctx) {
100
- return {
101
- executors: ctx.registry.list().map((e) => ({
102
- agentId: e.agentId,
103
- implementationVersion: e.implementationVersion,
104
- ...(e.manifestHash === undefined ? {} : { manifestHash: e.manifestHash }),
105
- connected: executorConnected(ctx.live, e.tokenHash),
106
- updatedAt: e.updatedAt,
107
- })),
108
- };
109
- }
110
- /** `PUT /v1/executors`: persist the batch, then update the registry and end rotated streams. */
111
- export async function registerExecutors(ctx, principalId, body) {
112
- const { store, registry } = ctx;
113
- // Credential rules are shared with startup validation, which throws plainly; on the
114
- // wire a rejected registration is a bad request, not a server fault.
115
- const applicationHashes = await store.tx((t) => t.applicationTokenHashes());
116
- try {
117
- for (const executor of body.executors)
118
- assertExecutorCredential(executor, applicationHashes);
119
- }
120
- catch (error) {
121
- fail(400, error instanceof Error ? error.message : String(error));
122
- }
123
- if (new Set(body.executors.map((e) => e.agentId)).size !==
124
- body.executors.length ||
125
- new Set(body.executors.map((e) => e.token)).size !== body.executors.length)
126
- fail(400, "Executor registrations must be unique per agent and token");
127
- const updatedAt = new Date().toISOString();
128
- const records = body.executors.map((executor) => ({
129
- agentId: executor.agentId,
130
- implementationVersion: executor.implementationVersion,
131
- ...(executor.manifestHash === undefined
132
- ? {}
133
- : { manifestHash: executor.manifestHash }),
134
- tokenHash: hashToken(executor.token),
135
- persisted: true,
136
- updatedAt,
137
- principalId,
138
- }));
139
- for (const record of records) {
140
- const collision = registry.find(record.tokenHash);
141
- if (collision && collision.agentId !== record.agentId)
142
- fail(409, "Executor tokens must be unique");
143
- // A token hash may not appear in both principals and executors (D3).
144
- if (applicationHashes.includes(record.tokenHash))
145
- fail(400, "Executor tokens require independent credentials and an agent id");
146
- }
147
- // Persist the whole batch first; the in-memory registry must never run ahead of the store.
148
- await store.tx(async (t) => {
149
- for (const record of records)
150
- await t.putExecutor({
151
- agentId: record.agentId,
152
- tokenHash: record.tokenHash,
153
- implementationVersion: record.implementationVersion,
154
- ...(record.manifestHash === undefined
155
- ? {}
156
- : { manifestHash: record.manifestHash }),
157
- principalId: record.principalId,
158
- updatedAt,
159
- });
160
- });
161
- const results = records.map((record) => {
162
- const previous = registry.get(record.agentId);
163
- const { rotated, previousHash } = registry.upsert(record);
164
- if (rotated && previousHash)
165
- endExecutorStreams(ctx.live, previousHash);
166
- const replacedByDifferent = previous?.principalId !== undefined &&
167
- previous.principalId !== principalId;
168
- if (replacedByDifferent)
169
- ctx.config.logger.warn("executor registration replaced by different application credential", {
170
- agentId: record.agentId,
171
- previousPrincipalId: previous.principalId,
172
- });
173
- return {
174
- agentId: record.agentId,
175
- implementationVersion: record.implementationVersion,
176
- rotated,
177
- ...(replacedByDifferent
178
- ? { replacedBy: "different-credential" }
179
- : {}),
180
- };
181
- });
182
- return { executors: results };
183
- }
184
- /** `DELETE /v1/executors/:agentId`: forget the executor and end its streams. */
185
- export async function deleteExecutor(ctx, agentId) {
186
- const existing = ctx.registry.get(agentId) ?? fail(404, "Executor not found");
187
- await ctx.store.tx((t) => t.deleteExecutor(agentId));
188
- ctx.registry.remove(agentId);
189
- endExecutorStreams(ctx.live, existing.tokenHash);
190
- return { agentId, deleted: true };
191
- }
@@ -35,6 +35,7 @@ import { aggregateWaits, cancelSiblingWork, isWorkflowManifest, wakeLinkedWorkfl
35
35
  import { isOwnershipLost, ownedTx } from "../store/ownership.js";
36
36
  import { linkedAgentOutput, rebaseSessionState, sessionToolsOf, turnManifestOf, } from "./session.js";
37
37
  import { prepareMcp, resolveEffect } from "./effects.js";
38
+ import { slimModelEffects } from "./slim.js";
38
39
  import { command } from "./commands.js";
39
40
  import { usesFixtureModel } from "./model-setting.js";
40
41
  import { toolFixtureModel } from "../core/provider.js";
@@ -224,6 +225,9 @@ function startHeartbeat(ctx, lease, controller) {
224
225
  },
225
226
  };
226
227
  }
228
+ /** Segment rollover defaults (Model Calls §10), well inside the advance deadline. */
229
+ const ROLLOVER_STEPS = 50;
230
+ const ROLLOVER_MS = 20 * 60_000;
227
231
  /** Runs one segment and settles it. Throws only `ownership.lost` and infrastructure errors. */
228
232
  async function runSegment(ctx, lease, started, signal) {
229
233
  const id = lease.sessionId;
@@ -258,6 +262,10 @@ async function runSegment(ctx, lease, started, signal) {
258
262
  signal,
259
263
  sessionTools: sessionToolsOf(current.mcpSnapshot),
260
264
  host,
265
+ yieldAfter: {
266
+ steps: ctx.config.rollover?.steps ?? ROLLOVER_STEPS,
267
+ ms: ctx.config.rollover?.ms ?? ROLLOVER_MS,
268
+ },
261
269
  });
262
270
  // The engine turns an abort into a `cancelled` (agents) or `failed` (workflows) result;
263
271
  // only a user cancel may settle that, and it already did.
@@ -337,6 +345,28 @@ async function settle(ctx, lease, s, result) {
337
345
  await t.put("sessions", id, current);
338
346
  return siblings;
339
347
  }
348
+ if (result.status === "yielded") {
349
+ // The turn goes on in a new segment: same turn, next checkpoint, woken right away.
350
+ const finished = result.checkpoint;
351
+ current.state = result.result.state;
352
+ current.checkpoint = {
353
+ ...finished,
354
+ segment: finished.segment + 1,
355
+ input: { kind: "continue" },
356
+ state: current.state,
357
+ };
358
+ current.status = "runnable";
359
+ current.waits = undefined;
360
+ await t.put("checkpoints", JSON.stringify([id, s.activeTurnId, finished.segment]), { checkpoint: finished, status: "yielded" });
361
+ await slimModelEffects(t, id, s.activeTurnId);
362
+ await t.put("sessions", id, current);
363
+ const segment = finished.segment + 1;
364
+ t.afterCommit(() => ctx.wake(id, {
365
+ reason: "rollover",
366
+ dedupeKey: `rollover:${s.activeTurnId}:${segment}`,
367
+ }));
368
+ return siblings;
369
+ }
340
370
  const flow = isWorkflowManifest(current.manifest);
341
371
  if (!flow) {
342
372
  current.state =
@@ -370,6 +400,7 @@ async function settle(ctx, lease, s, result) {
370
400
  current.activeTurnId = null;
371
401
  if (s.activeTurnId)
372
402
  current.lastTurnId = s.activeTurnId;
403
+ await slimModelEffects(t, id, s.activeTurnId);
373
404
  }
374
405
  const type = `turn.${result.status}`;
375
406
  const payload = result.status === "completed"
@@ -430,6 +461,7 @@ async function settleFailure(ctx, lease, s, error) {
430
461
  current.error = error instanceof Error ? error.message : String(error);
431
462
  const payload = { message: current.error };
432
463
  await t.event(id, current.activeTurnId, "turn.failed", payload);
464
+ await slimModelEffects(t, id, current.activeTurnId);
433
465
  if (current.activeTurnId)
434
466
  current.lastTurnId = current.activeTurnId;
435
467
  current.activeTurnId = null;
@@ -1,32 +1,34 @@
1
1
  /**
2
2
  * Tenant request authentication and authorization: a bearer token resolves to an application
3
- * principal or a registered executor; anything else is the opaque 404 (D5). An application
4
- * principal may act for a subject (`Nylorun-Subject`, `Nylorun-Scopes`); `authorize` then
5
- * limits the request to the routes its scopes allow, decided from the route alone.
3
+ * principal, a subject token or a delivery token; anything else is the opaque 404 (D5).
4
+ * An application principal may act for a subject (`Nylorun-Subject`, `Nylorun-Scopes`); a
5
+ * subject token names its subject itself. `authorize` limits both to the routes their scopes
6
+ * allow, decided from the route alone.
6
7
  */
7
8
  import type { IncomingMessage } from "node:http";
8
9
  import { type Action, type SubjectScope } from "@nylorun/core/contracts";
9
- import type { AuthScope, TenantContext } from "./context.js";
10
- export declare function authenticate(ctx: TenantContext, request: IncomingMessage): Promise<AuthScope>;
11
- /** What a subject needs for a route: any one of the scopes, or nothing grants it. */
12
- export type RouteAccess = readonly SubjectScope[] | "never";
10
+ import type { AuthScope, SessionAccess, TenantContext } from "./context.js";
11
+ import type { BrowserClient } from "./browser.js";
12
+ export declare function authenticate(ctx: TenantContext, request: IncomingMessage, client?: BrowserClient): Promise<AuthScope>;
13
13
  /**
14
- * The scopes a subject needs for `method path` (`path` starts with `v1`). `undefined` for a
15
- * route that does not exist. Operator and executor routes are `never`: reset, config seed,
16
- * executors, actions and the sandbox tool routes.
14
+ * What a subject needs for a route: any one of the scopes, nothing grants it (`never`), or
15
+ * any caller may (`any`, public data such as the JWKS). Each route declares it
16
+ * (`api/http/define.ts`).
17
17
  */
18
- export declare function routeAccess(method: string | undefined, path: readonly string[]): RouteAccess | undefined;
18
+ export type SubjectAccess = readonly SubjectScope[] | "never" | "any";
19
19
  /**
20
- * Limits a subject to the routes its scopes allow. Decided from the route alone, before any
21
- * lookup, so a refusal reveals nothing about which resources exist. A route the table does not
22
- * know is a 404 for a subject: new routes stay closed until they are added here. Application
23
- * and executor scopes pass through to the checks each route makes today.
20
+ * A subject, subject token or publishable key reaches a route only with one of the scopes it
21
+ * declares: `403 scope_required`, decided from the route alone before anything is read.
24
22
  */
25
- export declare function authorize(scope: AuthScope, method: string | undefined, path: readonly string[]): void;
23
+ export declare function requireScopes(scope: AuthScope, access: SubjectAccess): void;
24
+ export declare function accessOf(scope: AuthScope): SessionAccess | undefined;
26
25
  /** The subject whose sessions and vaults the request is limited to, if it acts for one. */
27
26
  export declare function ownerOf(scope: AuthScope): string | undefined;
28
27
  /** The application principal behind the request, acting for a subject or not. */
29
28
  export declare function requirePrincipal(scope: AuthScope): string;
30
- /** The executor scope must belong to the Action's agent. */
29
+ /**
30
+ * The caller may act on `action`: only the delivery token minted for it. Whether its generation
31
+ * is still the Action's is checked by each callback.
32
+ */
31
33
  export declare function scoped(scope: AuthScope, action: Action): void;
32
34
  export declare function requireApplication(scope: AuthScope): string;
@@ -1,7 +1,11 @@
1
1
  import { SCOPES_HEADER, SUBJECT_HEADER, } from "@nylorun/core/compatibility";
2
- import { parseSubjectHeaders, } from "@nylorun/core/contracts";
3
- import { hashToken } from "../core/executors.js";
2
+ import { DELIVERY_TOKEN_TYPE, parseSubjectHeaders, } from "@nylorun/core/contracts";
3
+ import { hashToken } from "../core/bearer.js";
4
4
  import { fail, failOpaque } from "./http.js";
5
+ import { looksLikeToken, verifySubjectToken } from "./tokens.js";
6
+ import { verifyDeliveryToken } from "./delivery-token.js";
7
+ import { tokenType } from "./jwt.js";
8
+ import { readPolicy } from "./access-policy.js";
5
9
  const SUBJECT_INVALID = { code: "subject_invalid" };
6
10
  /** One header's value, or `undefined`; a header sent twice is refused. */
7
11
  function singleHeader(request, name) {
@@ -14,19 +18,56 @@ function singleHeader(request, name) {
14
18
  return fail(400, `${name} must be sent once`, SUBJECT_INVALID);
15
19
  return values[0];
16
20
  }
17
- export async function authenticate(ctx, request) {
21
+ export async function authenticate(ctx, request, client) {
18
22
  const header = request.headers.authorization;
19
23
  const token = header?.startsWith("Bearer ") ? header.slice(7) : "";
20
24
  if (!token || !header?.startsWith("Bearer ")) {
25
+ // A publishable key alone: what the policy grants `anon` (nothing by default).
26
+ if (client && header === undefined) {
27
+ const { anon } = await ctx.store.tx((t) => readPolicy(t));
28
+ return {
29
+ kind: "publishable",
30
+ keyId: client.keyId,
31
+ scopes: new Set(anon.scopes),
32
+ agents: anon.agents === "*" ? "*" : new Set(anon.agents),
33
+ };
34
+ }
21
35
  ctx.config.logger.warn("credential rejected", {
22
36
  reason: "missing_bearer",
23
37
  });
24
38
  return failOpaque();
25
39
  }
40
+ if (looksLikeToken(token) && tokenType(token) === DELIVERY_TOKEN_TYPE) {
41
+ // Delivery tokens come back from the application's server, never from a browser.
42
+ if (request.headers.origin !== undefined)
43
+ fail(403, "Delivery tokens are not accepted from browsers", { code: "origin_rejected" });
44
+ const scope = await verifyDeliveryToken(ctx, token);
45
+ if (singleHeader(request, SUBJECT_HEADER) !== undefined ||
46
+ singleHeader(request, SCOPES_HEADER) !== undefined)
47
+ fail(403, "A delivery token cannot act for a subject");
48
+ return scope;
49
+ }
50
+ if (looksLikeToken(token)) {
51
+ const scope = await verifySubjectToken(ctx, token);
52
+ // Read only after verification, so an unknown token still sees the opaque 404.
53
+ if (singleHeader(request, SUBJECT_HEADER) !== undefined ||
54
+ singleHeader(request, SCOPES_HEADER) !== undefined)
55
+ fail(403, "A subject token cannot act for another subject");
56
+ return scope;
57
+ }
58
+ // Application keys never come from a browser or a shipped app: refused before they are even
59
+ // looked up.
60
+ if (request.headers.origin !== undefined)
61
+ fail(403, "Application keys are not accepted from browsers", {
62
+ code: "origin_rejected",
63
+ });
64
+ if (client)
65
+ fail(400, "Nylorun-Key is for browser and mobile clients, not with a Tenant key", {
66
+ code: "invalid_request",
67
+ });
26
68
  const tokenHash = hashToken(token);
27
69
  const principal = await ctx.store.tx((t) => t.principalByTokenHash(tokenHash));
28
- const executor = principal ? undefined : ctx.registry.find(tokenHash);
29
- if (!principal && !executor) {
70
+ if (!principal) {
30
71
  ctx.config.logger.warn("credential rejected", {
31
72
  reason: "unknown_token",
32
73
  });
@@ -35,11 +76,6 @@ export async function authenticate(ctx, request) {
35
76
  // Read only after authentication, so an unknown caller sees the opaque 404 either way.
36
77
  const subject = singleHeader(request, SUBJECT_HEADER);
37
78
  const scopes = singleHeader(request, SCOPES_HEADER);
38
- if (executor) {
39
- if (subject !== undefined || scopes !== undefined)
40
- fail(403, "An executor credential cannot act for a subject");
41
- return { kind: "executor", executor };
42
- }
43
79
  if (subject === undefined) {
44
80
  if (scopes !== undefined)
45
81
  fail(400, `${SCOPES_HEADER} requires ${SUBJECT_HEADER}`, SUBJECT_INVALID);
@@ -55,87 +91,17 @@ export async function authenticate(ctx, request) {
55
91
  scopes: parsed.scopes,
56
92
  };
57
93
  }
58
- const SESSIONS = ["sessions:own"];
59
- const VAULTS = ["vaults:own"];
60
- const SETTINGS = ["tenant:settings"];
61
94
  /**
62
- * The scopes a subject needs for `method path` (`path` starts with `v1`). `undefined` for a
63
- * route that does not exist. Operator and executor routes are `never`: reset, config seed,
64
- * executors, actions and the sandbox tool routes.
95
+ * A subject, subject token or publishable key reaches a route only with one of the scopes it
96
+ * declares: `403 scope_required`, decided from the route alone before anything is read.
65
97
  */
66
- export function routeAccess(method, path) {
67
- const [v1, resource, id, sub] = path;
68
- const n = path.length;
69
- if (v1 !== "v1" || !resource)
70
- return undefined;
71
- switch (resource) {
72
- case "executors":
73
- case "actions":
74
- return "never";
75
- case "agents":
76
- if (n === 2 && method === "GET")
77
- return ["agents:read", "agents:write"];
78
- if (n === 3 && method === "PUT")
79
- return ["agents:write"];
80
- return undefined;
81
- case "sessions":
82
- if (n === 2 && method === "GET")
83
- return SESSIONS;
84
- if (n === 3 && (method === "GET" || method === "PUT"))
85
- return SESSIONS;
86
- if (n === 4 && method === "GET" && (sub === "items" || sub === "events"))
87
- return SESSIONS;
88
- if (n === 4 && method === "POST" && sub === "commands")
89
- return SESSIONS;
90
- if (n === 5 && method === "POST" && sub === "sandbox")
91
- return "never";
92
- return undefined;
93
- case "vaults":
94
- if (n === 2)
95
- return method === "GET" || method === "POST" ? VAULTS : undefined;
96
- if (n === 3)
97
- return method === "GET" || method === "DELETE" ? VAULTS : undefined;
98
- if (sub !== "credentials")
99
- return undefined;
100
- if (n === 4)
101
- return method === "GET" || method === "POST" ? VAULTS : undefined;
102
- if (n === 5)
103
- return method === "GET" || method === "POST" || method === "DELETE"
104
- ? VAULTS
105
- : undefined;
106
- return undefined;
107
- case "tenant":
108
- if (n === 2 && method === "GET")
109
- return SETTINGS;
110
- if (n === 3 && id === "reset" && method === "POST")
111
- return "never";
112
- if (n === 4 && id === "config" && sub === "seed" && method === "PUT")
113
- return "never";
114
- if (n === 3 && method === "GET" && (id === "models" || id === "providers"))
115
- return ["tenant:settings", "agents:write"];
116
- if (n === 3 && method === "GET" && id === "sandbox")
117
- return SETTINGS;
118
- if (n === 3 && id === "model" && (method === "GET" || method === "PUT"))
119
- return SETTINGS;
120
- if (n === 4 && id === "model" && sub === "selection" && method === "PUT")
121
- return SETTINGS;
122
- return undefined;
123
- default:
124
- return undefined;
125
- }
126
- }
127
- /**
128
- * Limits a subject to the routes its scopes allow. Decided from the route alone, before any
129
- * lookup, so a refusal reveals nothing about which resources exist. A route the table does not
130
- * know is a 404 for a subject: new routes stay closed until they are added here. Application
131
- * and executor scopes pass through to the checks each route makes today.
132
- */
133
- export function authorize(scope, method, path) {
134
- if (scope.kind !== "subject")
98
+ export function requireScopes(scope, access) {
99
+ if (scope.kind !== "subject" &&
100
+ scope.kind !== "token" &&
101
+ scope.kind !== "publishable")
102
+ return;
103
+ if (access === "any")
135
104
  return;
136
- const access = routeAccess(method, path);
137
- if (access === undefined)
138
- fail(404, "Route not found");
139
105
  if (access === "never")
140
106
  fail(403, "This route is not available when acting for a subject", {
141
107
  code: "scope_required",
@@ -147,20 +113,51 @@ export function authorize(scope, method, path) {
147
113
  details: { scopes: access },
148
114
  });
149
115
  }
116
+ export function accessOf(scope) {
117
+ switch (scope.kind) {
118
+ case "application":
119
+ return undefined;
120
+ case "subject":
121
+ return { owner: scope.subject };
122
+ case "token":
123
+ return {
124
+ owner: scope.subject,
125
+ ...(scope.agents === "*" ? {} : { agents: scope.agents }),
126
+ };
127
+ // A publishable key alone owns nothing: no session or vault is ever reachable.
128
+ case "publishable":
129
+ // Nor does a request with no credential, on a route that serves public data.
130
+ case "anonymous":
131
+ return fail(404, "Not found");
132
+ // A delivery token reaches its Action's callbacks, never a session or vault.
133
+ case "delivery":
134
+ return fail(404, "Not found");
135
+ default: {
136
+ const unknown = scope;
137
+ return fail(404, `Unknown credential ${String(unknown.kind)}`);
138
+ }
139
+ }
140
+ }
150
141
  /** The subject whose sessions and vaults the request is limited to, if it acts for one. */
151
142
  export function ownerOf(scope) {
152
- return scope.kind === "subject" ? scope.subject : undefined;
143
+ return accessOf(scope)?.owner;
153
144
  }
154
145
  /** The application principal behind the request, acting for a subject or not. */
155
146
  export function requirePrincipal(scope) {
156
- if (scope.kind !== "executor")
147
+ if (scope.kind === "application" || scope.kind === "subject")
157
148
  return scope.principalId;
158
149
  return fail(403, "Application credential required");
159
150
  }
160
- /** The executor scope must belong to the Action's agent. */
151
+ /**
152
+ * The caller may act on `action`: only the delivery token minted for it. Whether its generation
153
+ * is still the Action's is checked by each callback.
154
+ */
161
155
  export function scoped(scope, action) {
162
- if (scope.kind !== "executor" || scope.executor.agentId !== action.agentId)
163
- fail(403, "Executor scope does not authorize this action");
156
+ if (scope.kind === "delivery" &&
157
+ scope.actionId === action.actionId &&
158
+ scope.agentId === action.agentId)
159
+ return;
160
+ fail(403, "This delivery token is for another Action");
164
161
  }
165
162
  export function requireApplication(scope) {
166
163
  if (scope.kind === "application")
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Which client app a request comes from (Host feature `browser-access`), decided before the
3
+ * bearer is looked at. `Nylorun-Key` names a publishable key of this Tenant; an `Origin` must
4
+ * be on that key's allowlist, and only then does the response carry CORS headers. The
5
+ * allowlist protects browsers from other sites' pages; it is not authentication, since a
6
+ * non-browser can send any `Origin`: tokens authorize.
7
+ */
8
+ import type { IncomingMessage, ServerResponse } from "node:http";
9
+ import type { TenantContext } from "./context.js";
10
+ /** The client app of a request: its publishable key, and the origin when from a browser. */
11
+ export interface BrowserClient {
12
+ readonly keyId: string;
13
+ readonly origin?: string;
14
+ }
15
+ export declare function identifyClient(ctx: TenantContext, request: IncomingMessage, response: ServerResponse): Promise<BrowserClient | undefined>;
@@ -0,0 +1,39 @@
1
+ import { PUBLISHABLE_KEY_HEADER } from "@nylorun/core/compatibility";
2
+ import { originAllowed } from "@nylorun/core/contracts";
3
+ import { browserOrigin, setCorsHeaders } from "../host/cors.js";
4
+ import { fail, failOpaque } from "./http.js";
5
+ function single(request, name) {
6
+ const raw = request.headers[name.toLowerCase()];
7
+ return Array.isArray(raw) ? raw[0] : raw;
8
+ }
9
+ export async function identifyClient(ctx, request, response) {
10
+ const key = single(request, PUBLISHABLE_KEY_HEADER);
11
+ const hasOrigin = request.headers.origin !== undefined;
12
+ if (key === undefined) {
13
+ if (hasOrigin)
14
+ fail(403, `Browser requests need ${PUBLISHABLE_KEY_HEADER}`, {
15
+ code: "origin_rejected",
16
+ });
17
+ return undefined;
18
+ }
19
+ const row = await ctx.store.tx((t) => t.publishableKeyByKey(key));
20
+ if (!row || row.revokedAt !== null) {
21
+ ctx.config.logger.warn("credential rejected", {
22
+ reason: "publishable_key_rejected",
23
+ });
24
+ return failOpaque();
25
+ }
26
+ if (!hasOrigin)
27
+ return { keyId: row.id };
28
+ const origin = browserOrigin(request);
29
+ const origins = JSON.parse(row.originsJson);
30
+ if (origin === undefined || !originAllowed(origins, origin)) {
31
+ ctx.config.logger.warn("credential rejected", {
32
+ reason: "origin_not_allowed",
33
+ keyId: row.id,
34
+ });
35
+ return failOpaque();
36
+ }
37
+ setCorsHeaders(response, origin);
38
+ return { keyId: row.id, origin };
39
+ }
@@ -1,3 +1,19 @@
1
- import type { SessionCommand } from "@nylorun/core/contracts";
2
- import { type AuthScope, type TenantContext } from "./context.js";
1
+ import type { Action, ActionOutcome, LiveEvent, SessionCommand } from "@nylorun/core/contracts";
2
+ import type { Tx } from "../store/types.js";
3
+ import { type AuthScope, type Session, type TenantContext } from "./context.js";
4
+ /** `outcome`, or a failed one when a tool's output does not match its stored output schema. */
5
+ export declare function acceptedOutcome(action: Action, outcome: ActionOutcome): ActionOutcome;
3
6
  export declare function command(ctx: TenantContext, id: string, input: SessionCommand, scope: AuthScope): Promise<unknown>;
7
+ /**
8
+ * Completes a delivered Action with `outcome`, in the caller's transaction, which
9
+ * holds the lock of the Action's session `s`: the Action and its effect, the
10
+ * `action.completed` event (and `loop.verified` or `loop.decided`), the wake that resumes the
11
+ * turn, and queued workflow effects. The Action deliverer and the background-result callback
12
+ * both record outcomes here. It sets `s.status`; the caller writes `s`.
13
+ */
14
+ export declare function recordActionOutcome(t: Tx, ctx: TenantContext, s: Session, action: Action, received: ActionOutcome, options?: {
15
+ requestId?: string;
16
+ }): Promise<{
17
+ event: LiveEvent;
18
+ receipt: Record<string, unknown>;
19
+ }>;