@nylorun/runtime 0.13.0-beta → 0.15.0-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (309) hide show
  1. package/CHANGELOG.md +434 -0
  2. package/README.md +137 -41
  3. package/dist/adapters/execution/restate.d.ts +7 -2
  4. package/dist/adapters/execution/restate.js +40 -10
  5. package/dist/adapters/replication/pgoutput.d.ts +21 -0
  6. package/dist/adapters/replication/pgoutput.js +170 -0
  7. package/dist/adapters/streams/s2.js +37 -20
  8. package/dist/admin-openapi.json +570 -0
  9. package/dist/api/a2a/card.d.ts +15 -0
  10. package/dist/api/a2a/card.js +24 -0
  11. package/dist/api/a2a/endpoint.d.ts +8 -0
  12. package/dist/api/a2a/endpoint.js +81 -0
  13. package/dist/api/a2a/protocol.d.ts +144 -0
  14. package/dist/api/a2a/protocol.js +188 -0
  15. package/dist/api/a2a/routes.d.ts +19 -0
  16. package/dist/api/a2a/routes.js +289 -0
  17. package/dist/api/a2a/tasks.d.ts +50 -0
  18. package/dist/api/a2a/tasks.js +186 -0
  19. package/dist/api/ag-ui/endpoint.d.ts +8 -0
  20. package/dist/api/ag-ui/endpoint.js +126 -0
  21. package/dist/api/ag-ui/history.d.ts +7 -0
  22. package/dist/api/ag-ui/history.js +60 -0
  23. package/dist/api/ag-ui/routes.d.ts +32 -0
  24. package/dist/api/ag-ui/routes.js +305 -0
  25. package/dist/api/ag-ui/session-id.d.ts +1 -0
  26. package/dist/api/ag-ui/session-id.js +12 -0
  27. package/dist/api/ag-ui/sse.d.ts +9 -0
  28. package/dist/api/ag-ui/sse.js +7 -0
  29. package/dist/api/ag-ui/translate.d.ts +38 -0
  30. package/dist/api/ag-ui/translate.js +222 -0
  31. package/dist/api/components.d.ts +8065 -0
  32. package/dist/api/components.js +82 -0
  33. package/dist/api/gate/keys.d.ts +14 -0
  34. package/dist/api/gate/keys.js +59 -0
  35. package/dist/api/gate/routes.d.ts +34 -0
  36. package/dist/api/gate/routes.js +235 -0
  37. package/dist/api/http/app.d.ts +18 -0
  38. package/dist/api/http/app.js +58 -0
  39. package/dist/api/http/body.d.ts +4 -0
  40. package/dist/api/http/body.js +37 -0
  41. package/dist/api/http/define.d.ts +39 -0
  42. package/dist/api/http/define.js +122 -0
  43. package/dist/api/http/respond.d.ts +9 -0
  44. package/dist/api/http/respond.js +44 -0
  45. package/dist/api/http/routes/access.d.ts +24 -0
  46. package/dist/api/http/routes/access.js +266 -0
  47. package/dist/api/http/routes/actions.d.ts +8 -0
  48. package/dist/api/http/routes/actions.js +76 -0
  49. package/dist/api/http/routes/endpoints.d.ts +7 -0
  50. package/dist/api/http/routes/endpoints.js +100 -0
  51. package/dist/api/http/routes/sessions.d.ts +3 -0
  52. package/dist/api/http/routes/sessions.js +205 -0
  53. package/dist/api/http/routes/tenant.d.ts +11 -0
  54. package/dist/api/http/routes/tenant.js +244 -0
  55. package/dist/api/http/routes/vaults.d.ts +8 -0
  56. package/dist/api/http/routes/vaults.js +167 -0
  57. package/dist/api/openapi.d.ts +19 -0
  58. package/dist/api/openapi.js +137 -0
  59. package/dist/api/route.d.ts +12 -0
  60. package/dist/api/route.js +4 -0
  61. package/dist/core/bearer.d.ts +8 -0
  62. package/dist/core/bearer.js +11 -0
  63. package/dist/core/flow-host.d.ts +7 -15
  64. package/dist/core/flow-host.js +16 -41
  65. package/dist/core/sandbox-routes.d.ts +12 -7
  66. package/dist/core/sandbox-routes.js +24 -25
  67. package/dist/execution/memory.d.ts +8 -5
  68. package/dist/execution/memory.js +33 -13
  69. package/dist/execution/types.d.ts +38 -6
  70. package/dist/execution/types.js +12 -3
  71. package/dist/gates/contract.d.ts +51 -0
  72. package/dist/gates/contract.js +37 -0
  73. package/dist/gates/handler.d.ts +21 -0
  74. package/dist/gates/handler.js +55 -0
  75. package/dist/gates/http-client.d.ts +12 -0
  76. package/dist/gates/http-client.js +160 -0
  77. package/dist/gates/in-process.d.ts +32 -0
  78. package/dist/gates/in-process.js +42 -0
  79. package/dist/gates/inflight.d.ts +39 -0
  80. package/dist/gates/inflight.js +70 -0
  81. package/dist/gates/mcp-handler.d.ts +35 -0
  82. package/dist/gates/mcp-handler.js +142 -0
  83. package/dist/gates/meter.d.ts +24 -0
  84. package/dist/gates/meter.js +165 -0
  85. package/dist/gates/model-gate.d.ts +45 -0
  86. package/dist/gates/model-gate.js +1 -0
  87. package/dist/gates/tenant-vaults.d.ts +64 -0
  88. package/dist/gates/tenant-vaults.js +94 -0
  89. package/dist/gates/tool-calls.d.ts +51 -0
  90. package/dist/gates/tool-calls.js +64 -0
  91. package/dist/gates/tool-client.d.ts +12 -0
  92. package/dist/gates/tool-client.js +201 -0
  93. package/dist/gates/tool-contract.d.ts +116 -0
  94. package/dist/gates/tool-contract.js +64 -0
  95. package/dist/gates/tool-gate.d.ts +45 -0
  96. package/dist/gates/tool-gate.js +10 -0
  97. package/dist/host/admin-api.d.ts +23 -0
  98. package/dist/host/admin-api.js +74 -0
  99. package/dist/host/app.d.ts +34 -0
  100. package/dist/host/app.js +181 -0
  101. package/dist/host/config-for.d.ts +3 -1
  102. package/dist/host/config-for.js +3 -2
  103. package/dist/host/config.d.ts +11 -0
  104. package/dist/host/cors.d.ts +27 -0
  105. package/dist/host/cors.js +64 -0
  106. package/dist/host/create-host.d.ts +36 -8
  107. package/dist/host/create-host.js +161 -330
  108. package/dist/host/execution.d.ts +20 -18
  109. package/dist/host/execution.js +2 -2
  110. package/dist/host/gates.d.ts +58 -0
  111. package/dist/host/gates.js +127 -0
  112. package/dist/host/http.d.ts +20 -4
  113. package/dist/host/http.js +81 -23
  114. package/dist/host/index.d.ts +1 -1
  115. package/dist/host/main.js +189 -58
  116. package/dist/host/stack-config.d.ts +91 -19
  117. package/dist/host/stack-config.js +246 -17
  118. package/dist/infra/database.d.ts +2 -2
  119. package/dist/infra/execution.d.ts +5 -5
  120. package/dist/infra/execution.js +8 -6
  121. package/dist/infra/index.d.ts +1 -1
  122. package/dist/infra/workers.d.ts +17 -17
  123. package/dist/infra/workers.js +9 -9
  124. package/dist/keys/client.d.ts +9 -0
  125. package/dist/keys/client.js +70 -0
  126. package/dist/keys/contract.d.ts +24 -0
  127. package/dist/keys/contract.js +12 -0
  128. package/dist/keys/keys.d.ts +41 -0
  129. package/dist/keys/keys.js +50 -0
  130. package/dist/mcp/connect.d.ts +43 -3
  131. package/dist/mcp/connect.js +12 -5
  132. package/dist/mcp/pool.d.ts +39 -3
  133. package/dist/mcp/pool.js +74 -11
  134. package/dist/model/catalog.d.ts +2 -10
  135. package/dist/model/classify.d.ts +13 -0
  136. package/dist/model/classify.js +61 -0
  137. package/dist/model/models.d.ts +10 -1
  138. package/dist/model/models.js +14 -6
  139. package/dist/model/pi-model.d.ts +17 -2
  140. package/dist/model/pi-model.js +366 -222
  141. package/dist/openapi.json +25889 -0
  142. package/dist/record/append.d.ts +51 -0
  143. package/dist/record/append.js +20 -0
  144. package/dist/record/envelope.d.ts +19 -0
  145. package/dist/record/envelope.js +49 -0
  146. package/dist/record/index.d.ts +11 -0
  147. package/dist/record/index.js +11 -0
  148. package/dist/sandbox/manager.d.ts +13 -1
  149. package/dist/sandbox/manager.js +8 -11
  150. package/dist/sandbox/resolve.d.ts +33 -0
  151. package/dist/sandbox/resolve.js +81 -0
  152. package/dist/sandbox/session-sandbox.d.ts +11 -0
  153. package/dist/sandbox/session-sandbox.js +51 -0
  154. package/dist/sandbox/share.d.ts +14 -2
  155. package/dist/sandbox/share.js +33 -3
  156. package/dist/sandbox/tenant-config.d.ts +37 -0
  157. package/dist/sandbox/tenant-config.js +51 -0
  158. package/dist/sandbox/tools.d.ts +7 -9
  159. package/dist/sandbox/tools.js +11 -3
  160. package/dist/sandbox/types.d.ts +8 -2
  161. package/dist/store/postgres/connect.d.ts +25 -9
  162. package/dist/store/postgres/connect.js +27 -10
  163. package/dist/store/postgres/db.d.ts +16 -0
  164. package/dist/store/postgres/db.js +28 -0
  165. package/dist/store/postgres/drizzle/0000_foundation.sql +22 -0
  166. package/dist/store/postgres/drizzle/0001_baseline.sql +255 -0
  167. package/dist/store/postgres/drizzle/0002_stream_relay.sql +4 -0
  168. package/dist/store/postgres/drizzle/0003_tool_crossings.sql +9 -0
  169. package/dist/store/postgres/drizzle/meta/_journal.json +34 -0
  170. package/dist/store/postgres/migrate.d.ts +68 -0
  171. package/dist/store/postgres/migrate.js +183 -0
  172. package/dist/store/postgres/record-writer.d.ts +4 -0
  173. package/dist/store/postgres/record-writer.js +34 -0
  174. package/dist/store/postgres/record.d.ts +5 -0
  175. package/dist/store/postgres/record.js +60 -0
  176. package/dist/store/postgres/schema.d.ts +3096 -0
  177. package/dist/store/postgres/schema.js +363 -0
  178. package/dist/store/postgres/store.d.ts +2 -4
  179. package/dist/store/postgres/store.js +590 -434
  180. package/dist/store/postgres/tenant.d.ts +56 -0
  181. package/dist/store/postgres/tenant.js +154 -0
  182. package/dist/store/types.d.ts +183 -132
  183. package/dist/store/types.js +0 -1
  184. package/dist/streams/basin.d.ts +18 -1
  185. package/dist/streams/basin.js +34 -2
  186. package/dist/streams/control.d.ts +18 -0
  187. package/dist/streams/control.js +33 -0
  188. package/dist/streams/memory.d.ts +3 -1
  189. package/dist/streams/memory.js +14 -8
  190. package/dist/streams/relay/core.d.ts +35 -0
  191. package/dist/streams/relay/core.js +366 -0
  192. package/dist/streams/relay/types.d.ts +78 -0
  193. package/dist/streams/relay/types.js +8 -0
  194. package/dist/streams/types.d.ts +71 -36
  195. package/dist/streams/types.js +18 -53
  196. package/dist/tenant/access-policy.d.ts +30 -0
  197. package/dist/tenant/access-policy.js +52 -0
  198. package/dist/tenant/actions.d.ts +10 -149
  199. package/dist/tenant/actions.js +5 -167
  200. package/dist/tenant/advance.d.ts +7 -2
  201. package/dist/tenant/advance.js +125 -14
  202. package/dist/tenant/auth.d.ts +19 -17
  203. package/dist/tenant/auth.js +90 -93
  204. package/dist/tenant/browser.d.ts +15 -0
  205. package/dist/tenant/browser.js +39 -0
  206. package/dist/tenant/cause.d.ts +25 -0
  207. package/dist/tenant/cause.js +45 -0
  208. package/dist/tenant/commands.d.ts +18 -2
  209. package/dist/tenant/commands.js +111 -115
  210. package/dist/tenant/context.d.ts +101 -21
  211. package/dist/tenant/context.js +14 -10
  212. package/dist/tenant/delivery-token.d.ts +41 -0
  213. package/dist/tenant/delivery-token.js +104 -0
  214. package/dist/tenant/delivery.d.ts +62 -0
  215. package/dist/tenant/delivery.js +481 -0
  216. package/dist/tenant/effects.d.ts +20 -1
  217. package/dist/tenant/effects.js +159 -42
  218. package/dist/tenant/endpoints.d.ts +67 -0
  219. package/dist/tenant/endpoints.js +90 -0
  220. package/dist/tenant/ephemeral.d.ts +22 -7
  221. package/dist/tenant/ephemeral.js +83 -125
  222. package/dist/tenant/history.d.ts +64 -0
  223. package/dist/tenant/history.js +120 -0
  224. package/dist/tenant/http.d.ts +8 -9
  225. package/dist/tenant/http.js +13 -27
  226. package/dist/tenant/jwt.d.ts +16 -0
  227. package/dist/tenant/jwt.js +50 -0
  228. package/dist/tenant/model-setting.d.ts +2 -2
  229. package/dist/tenant/module.d.ts +17 -28
  230. package/dist/tenant/module.js +133 -279
  231. package/dist/tenant/outbound.d.ts +44 -0
  232. package/dist/tenant/outbound.js +127 -0
  233. package/dist/tenant/paths.d.ts +11 -16
  234. package/dist/tenant/paths.js +24 -68
  235. package/dist/tenant/pool.d.ts +1 -3
  236. package/dist/tenant/pool.js +1 -19
  237. package/dist/tenant/principals.d.ts +23 -11
  238. package/dist/tenant/principals.js +61 -49
  239. package/dist/tenant/reset.d.ts +0 -4
  240. package/dist/tenant/reset.js +0 -5
  241. package/dist/tenant/runtime.d.ts +28 -20
  242. package/dist/tenant/runtime.js +95 -64
  243. package/dist/tenant/scheduler.d.ts +10 -3
  244. package/dist/tenant/scheduler.js +9 -5
  245. package/dist/tenant/session-streams.d.ts +127 -0
  246. package/dist/tenant/session-streams.js +404 -0
  247. package/dist/tenant/sessions.d.ts +28 -4
  248. package/dist/tenant/sessions.js +118 -24
  249. package/dist/tenant/signing-keys.d.ts +55 -0
  250. package/dist/tenant/signing-keys.js +172 -0
  251. package/dist/tenant/slim.d.ts +8 -0
  252. package/dist/tenant/slim.js +21 -0
  253. package/dist/tenant/status.d.ts +3 -3
  254. package/dist/tenant/status.js +20 -13
  255. package/dist/tenant/store-pg.d.ts +9 -6
  256. package/dist/tenant/store-pg.js +60 -125
  257. package/dist/tenant/streams.d.ts +62 -85
  258. package/dist/tenant/streams.js +143 -226
  259. package/dist/tenant/subject-limits.d.ts +16 -0
  260. package/dist/tenant/subject-limits.js +45 -0
  261. package/dist/tenant/sweep.d.ts +1 -10
  262. package/dist/tenant/sweep.js +29 -69
  263. package/dist/tenant/tokens.d.ts +14 -0
  264. package/dist/tenant/tokens.js +180 -0
  265. package/dist/tenant/transcript.d.ts +48 -5
  266. package/dist/tenant/transcript.js +58 -1
  267. package/dist/tenant/types.d.ts +69 -98
  268. package/dist/tenant/types.js +4 -15
  269. package/dist/tenant/worker.d.ts +3 -1
  270. package/dist/tenant/worker.js +13 -7
  271. package/dist/vault/host-model.d.ts +31 -0
  272. package/dist/vault/host-model.js +57 -0
  273. package/dist/vault/service.d.ts +44 -17
  274. package/dist/vault/service.js +126 -121
  275. package/dist/version.d.ts +1 -1
  276. package/dist/version.js +1 -1
  277. package/package.json +25 -9
  278. package/dist/core/executors.d.ts +0 -44
  279. package/dist/core/executors.js +0 -95
  280. package/dist/store/memory.d.ts +0 -57
  281. package/dist/store/memory.js +0 -645
  282. package/dist/store/postgres/migrations/001_initial.d.ts +0 -27
  283. package/dist/store/postgres/migrations/001_initial.js +0 -196
  284. package/dist/store/postgres/migrations/002_session_owner.d.ts +0 -6
  285. package/dist/store/postgres/migrations/002_session_owner.js +0 -13
  286. package/dist/store/postgres/migrations/index.d.ts +0 -49
  287. package/dist/store/postgres/migrations/index.js +0 -76
  288. package/dist/store/postgres/names.d.ts +0 -11
  289. package/dist/store/postgres/names.js +0 -42
  290. package/dist/store/postgres/tenants.d.ts +0 -90
  291. package/dist/store/postgres/tenants.js +0 -214
  292. package/dist/streams/relay.d.ts +0 -37
  293. package/dist/streams/relay.js +0 -254
  294. package/dist/tenant/envelope.d.ts +0 -7
  295. package/dist/tenant/envelope.js +0 -22
  296. package/dist/tenant/live.d.ts +0 -92
  297. package/dist/tenant/live.js +0 -290
  298. package/dist/tenant/quarantine-error.d.ts +0 -6
  299. package/dist/tenant/quarantine-error.js +0 -9
  300. package/dist/tenant/quarantine.d.ts +0 -27
  301. package/dist/tenant/quarantine.js +0 -72
  302. package/dist/tenant/routes-tenant.d.ts +0 -12
  303. package/dist/tenant/routes-tenant.js +0 -137
  304. package/dist/tenant/routes.d.ts +0 -9
  305. package/dist/tenant/routes.js +0 -151
  306. package/dist/tenant/store-memory.d.ts +0 -16
  307. package/dist/tenant/store-memory.js +0 -111
  308. /package/dist/{store → record}/cursor.d.ts +0 -0
  309. /package/dist/{store → record}/cursor.js +0 -0
@@ -0,0 +1,404 @@
1
+ import { decodeCursor, encodeCursor } from "../record/index.js";
2
+ import { basinOf } from "../streams/basin.js";
3
+ import { sessionStream, } from "../streams/types.js";
4
+ import { fail } from "./http.js";
5
+ export function createSessionStreams() {
6
+ return { wiring: undefined, generation: 0, sessions: new Map() };
7
+ }
8
+ const RETRY_MIN_MS = 100;
9
+ const RETRY_MAX_MS = 2000;
10
+ function frame(event) {
11
+ return `id: ${event.cursor}\nevent: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`;
12
+ }
13
+ export function send(response, data) {
14
+ if (!response.write(data))
15
+ response.destroy();
16
+ }
17
+ /** The frame that tells a client why the Runtime ended its stream (no `id:`: not an event). */
18
+ function closedFrame(reason) {
19
+ return `event: nylorun.closed\ndata: ${JSON.stringify({ reason })}\n\n`;
20
+ }
21
+ /** An observer sink writing SSE frames to `response`. */
22
+ function sseSink(response) {
23
+ return {
24
+ write: (event) => send(response, frame(event)),
25
+ end: (reason) => {
26
+ if (reason && !response.writableEnded)
27
+ send(response, closedFrame(reason));
28
+ response.end();
29
+ },
30
+ };
31
+ }
32
+ /** Start an SSE response with keepalives; `onClose` runs once the client goes away. */
33
+ export function openSse(request, response, onClose) {
34
+ response.writeHead(200, {
35
+ "content-type": "text/event-stream",
36
+ "cache-control": "no-cache",
37
+ connection: "keep-alive",
38
+ "x-accel-buffering": "no",
39
+ });
40
+ response.flushHeaders();
41
+ const timer = setInterval(() => send(response, ": keepalive\n\n"), 15000);
42
+ timer.unref();
43
+ request.on("close", () => {
44
+ clearInterval(timer);
45
+ onClose();
46
+ });
47
+ }
48
+ /** The resume cursor: `?cursor=` wins over `Last-Event-ID`. */
49
+ export function requestCursor(request, url) {
50
+ return (url.searchParams.get("cursor") ??
51
+ (typeof request.headers["last-event-id"] === "string"
52
+ ? request.headers["last-event-id"]
53
+ : undefined));
54
+ }
55
+ /** The first sequence to read: after the cursor's, or the session's first event. */
56
+ function startSeq(sessionId, cursor) {
57
+ return cursor ? decodeCursor(sessionId, cursor) + 1 : 0;
58
+ }
59
+ /** `agent` keeps only events of one agent used as a tool, by delegationId or path. */
60
+ function belongsTo(event, agent) {
61
+ const ref = event.payload?.agent;
62
+ return ref?.delegationId === agent || ref?.path === agent;
63
+ }
64
+ /** The basin the Tenant's session streams are in now. */
65
+ export function currentBasin(ctx) {
66
+ return basinOf(ctx.config.tenantId, ctx.sessionStreams.generation);
67
+ }
68
+ /** The session's stream now, or undefined when the session is gone. */
69
+ async function currentStream(ctx, sessionId) {
70
+ const session = await ctx.store.tx((t) => t.get("sessions", sessionId));
71
+ return session && { basin: currentBasin(ctx), stream: sessionStream(sessionId) };
72
+ }
73
+ const sameStream = (feed, ref) => ref !== undefined && ref.basin === feed.basin && ref.stream === feed.stream;
74
+ function streamsOf(ctx) {
75
+ return (ctx.sessionStreams.wiring?.streams ?? fail(503, "Session streams are unavailable"));
76
+ }
77
+ /**
78
+ * `GET /v1/sessions/:id/items`: the session stream after the cursor, up to its tail when the
79
+ * read starts. The cursor returned is the last record read (the request's cursor when none
80
+ * was), whatever the `agent` filter kept.
81
+ */
82
+ export async function readHistory(ctx, sessionId, cursor, agent) {
83
+ const from = startSeq(sessionId, cursor);
84
+ const streams = streamsOf(ctx);
85
+ const ref = (await currentStream(ctx, sessionId)) ?? fail(404, "Session not found");
86
+ const items = [];
87
+ let last;
88
+ try {
89
+ for await (const record of streams.read(ref.basin, ref.stream, from, { follow: false })) {
90
+ last = record.seq;
91
+ if (served(record.body) && (agent === undefined || belongsTo(record.body, agent)))
92
+ items.push(record.body);
93
+ }
94
+ }
95
+ catch (error) {
96
+ ctx.config.logger.warn("session history read failed", {
97
+ sessionId,
98
+ message: error instanceof Error ? error.message : String(error),
99
+ });
100
+ fail(503, "Session history is unavailable");
101
+ }
102
+ return {
103
+ items,
104
+ cursor: last !== undefined ? encodeCursor(sessionId, last) : cursor ?? null,
105
+ };
106
+ }
107
+ /**
108
+ * `GET /v1/sessions/:id/events`: joins the session's shared read at the client's cursor and
109
+ * follows it until the client leaves, the session is reset or deleted, or the Tenant closes.
110
+ */
111
+ export async function streamSessionEvents(ctx, request, response, sessionId, cursor, holder) {
112
+ const observer = {
113
+ sink: sseSink(response),
114
+ next: startSeq(sessionId, cursor),
115
+ ...(holder ? { holder } : {}),
116
+ };
117
+ const joined = await join(ctx, sessionId, observer);
118
+ openSse(request, response, () => leave(ctx.sessionStreams, joined, observer));
119
+ armDeadline(ctx.sessionStreams, joined, observer);
120
+ }
121
+ /** Thrown by `observeSession` when the Runtime ended the stream for its token. */
122
+ export class StreamClosed extends Error {
123
+ reason;
124
+ cursor;
125
+ constructor(reason,
126
+ /** The cursor of the last event delivered, if any. */
127
+ cursor) {
128
+ super(`The stream ended: ${reason}`);
129
+ this.reason = reason;
130
+ this.cursor = cursor;
131
+ this.name = "StreamClosed";
132
+ }
133
+ }
134
+ /**
135
+ * A session's events from `cursor`, in process: the same shared read and deadlines as an SSE
136
+ * client (`streamSessionEvents`). Ends when `signal` aborts or the feed ends; throws
137
+ * `StreamClosed` when the Runtime ends it for its token (expiry or revocation).
138
+ */
139
+ export async function* observeSession(ctx, sessionId, cursor, options) {
140
+ const queue = [];
141
+ let ended;
142
+ let wake;
143
+ const notify = () => {
144
+ const resolve = wake;
145
+ wake = undefined;
146
+ resolve?.();
147
+ };
148
+ const observer = {
149
+ sink: {
150
+ write: (event) => {
151
+ queue.push(event);
152
+ notify();
153
+ },
154
+ end: (reason) => {
155
+ ended = reason ? { reason } : {};
156
+ notify();
157
+ },
158
+ },
159
+ next: startSeq(sessionId, cursor),
160
+ ...(options.holder ? { holder: options.holder } : {}),
161
+ };
162
+ const joined = await join(ctx, sessionId, observer);
163
+ armDeadline(ctx.sessionStreams, joined, observer);
164
+ const stop = () => {
165
+ leave(ctx.sessionStreams, joined, observer);
166
+ notify();
167
+ };
168
+ options.signal.addEventListener("abort", stop, { once: true });
169
+ let last = cursor;
170
+ try {
171
+ for (;;) {
172
+ while (queue.length > 0) {
173
+ const event = queue.shift();
174
+ last = event.cursor;
175
+ yield event;
176
+ }
177
+ if (options.signal.aborted)
178
+ return;
179
+ if (ended) {
180
+ if (ended.reason)
181
+ throw new StreamClosed(ended.reason, last);
182
+ return;
183
+ }
184
+ await new Promise((resolve) => (wake = resolve));
185
+ }
186
+ }
187
+ finally {
188
+ options.signal.removeEventListener("abort", stop);
189
+ leave(ctx.sessionStreams, joined, observer);
190
+ }
191
+ }
192
+ /** Ends the observer when its token expires. */
193
+ function armDeadline(hub, feed, observer) {
194
+ if (!observer.holder)
195
+ return;
196
+ observer.deadline = setTimeout(() => endObserver(hub, feed, observer, "token_expired"), Math.max(0, observer.holder.expiresAt - Date.now()));
197
+ observer.deadline.unref();
198
+ }
199
+ /**
200
+ * Adds `observer` to the session's shared read, starting or restarting it from the
201
+ * observer's position when needed. 404 when the session has no stream.
202
+ */
203
+ async function join(ctx, sessionId, observer) {
204
+ const from = observer.next;
205
+ streamsOf(ctx);
206
+ const ref = (await currentStream(ctx, sessionId)) ?? fail(404, "Session not found");
207
+ const hub = ctx.sessionStreams;
208
+ let feed = hub.sessions.get(sessionId);
209
+ if (feed && !sameStream(feed, ref)) {
210
+ // The Tenant moved to a new basin since that feed started: it follows an abandoned stream.
211
+ endFeed(hub, feed);
212
+ feed = undefined;
213
+ }
214
+ if (!feed) {
215
+ feed = {
216
+ sessionId,
217
+ basin: ref.basin,
218
+ stream: ref.stream,
219
+ next: from,
220
+ observers: new Set(),
221
+ read: new AbortController(),
222
+ };
223
+ hub.sessions.set(sessionId, feed);
224
+ runFeed(ctx, feed);
225
+ }
226
+ else if (from < feed.next) {
227
+ // Behind the shared read: restart it here; observers ahead skip what they have.
228
+ feed.read.abort();
229
+ feed.read = new AbortController();
230
+ feed.next = from;
231
+ runFeed(ctx, feed);
232
+ }
233
+ feed.observers.add(observer);
234
+ return feed;
235
+ }
236
+ /** Ends one observer early (its token expired or was revoked). */
237
+ function endObserver(hub, feed, observer, reason) {
238
+ if (!feed.observers.has(observer))
239
+ return;
240
+ leave(hub, feed, observer);
241
+ observer.sink.end(reason);
242
+ }
243
+ function leave(hub, feed, observer) {
244
+ if (observer.deadline)
245
+ clearTimeout(observer.deadline);
246
+ if (!feed.observers.delete(observer))
247
+ return;
248
+ if (feed.observers.size > 0)
249
+ return;
250
+ feed.read.abort();
251
+ if (hub.sessions.get(feed.sessionId) === feed)
252
+ hub.sessions.delete(feed.sessionId);
253
+ }
254
+ function deliver(observer, record) {
255
+ if (record.seq < observer.next)
256
+ return;
257
+ observer.next = record.seq + 1;
258
+ if (served(record.body))
259
+ observer.sink.write(record.body);
260
+ }
261
+ /** Internal events (`transcript.updated`) are recorded and streamed but never served. */
262
+ export function served(event) {
263
+ return event.visibility !== "internal";
264
+ }
265
+ /**
266
+ * Follows the feed's stream from `feed.next` for the current `feed.read`, retrying failed
267
+ * reads from where it stopped while the session still has that stream. A read that ends by
268
+ * itself (the stream's Tenant is gone or the streams closed), or a session that is gone or
269
+ * whose Tenant moved to a new basin, ends the observers.
270
+ */
271
+ function runFeed(ctx, feed) {
272
+ const read = feed.read;
273
+ const signal = read.signal;
274
+ void (async () => {
275
+ let delay = RETRY_MIN_MS;
276
+ while (!signal.aborted) {
277
+ const streams = ctx.sessionStreams.wiring?.streams;
278
+ if (!streams)
279
+ break;
280
+ try {
281
+ for await (const record of streams.read(feed.basin, feed.stream, feed.next, { signal })) {
282
+ // A restart replaced this read: the new one owns `feed.next`.
283
+ if (signal.aborted || feed.read !== read)
284
+ return;
285
+ if (record.seq < feed.next)
286
+ continue;
287
+ feed.next = record.seq + 1;
288
+ delay = RETRY_MIN_MS;
289
+ for (const observer of feed.observers)
290
+ deliver(observer, record);
291
+ }
292
+ if (signal.aborted)
293
+ return;
294
+ break;
295
+ }
296
+ catch (error) {
297
+ if (signal.aborted)
298
+ return;
299
+ ctx.config.logger.warn("session stream read failed; retrying", {
300
+ sessionId: feed.sessionId,
301
+ message: error instanceof Error ? error.message : String(error),
302
+ });
303
+ }
304
+ if (!(await stillCurrent(ctx, feed)))
305
+ break;
306
+ await sleep(delay, signal);
307
+ delay = Math.min(delay * 2, RETRY_MAX_MS);
308
+ }
309
+ if (signal.aborted || feed.read !== read)
310
+ return;
311
+ endFeed(ctx.sessionStreams, feed);
312
+ })();
313
+ }
314
+ /** False when the feed's session is gone or the Tenant moved to a new basin. True when unsure. */
315
+ async function stillCurrent(ctx, feed) {
316
+ try {
317
+ return sameStream(feed, await currentStream(ctx, feed.sessionId));
318
+ }
319
+ catch {
320
+ return true;
321
+ }
322
+ }
323
+ /**
324
+ * Ends every feed in this process whose session is gone or whose Tenant moved to a new basin
325
+ * generation, and moves this process to the Tenant's current generation. Run on a
326
+ * `sessions.reset` signal and periodically, so feeds on nodes other than the one that reset
327
+ * the Tenant end too.
328
+ */
329
+ export async function checkSessionStreams(ctx) {
330
+ const feeds = [...ctx.sessionStreams.sessions.values()];
331
+ // Also a backstop for lost `subject.revoked` signals: the holders' current epochs.
332
+ const subjects = new Set();
333
+ for (const feed of feeds)
334
+ for (const observer of feed.observers)
335
+ if (observer.holder)
336
+ subjects.add(observer.holder.subject);
337
+ const { exists, epochs, generation } = await ctx.store.tx(async (t) => {
338
+ const exists = new Set();
339
+ for (const feed of feeds)
340
+ if (await t.get("sessions", feed.sessionId))
341
+ exists.add(feed.sessionId);
342
+ return {
343
+ exists,
344
+ generation: (await t.basinGenerations()).current,
345
+ epochs: subjects.size === 0
346
+ ? new Map()
347
+ : await t.subjectEpochs([...subjects]),
348
+ };
349
+ });
350
+ if (generation !== ctx.sessionStreams.generation)
351
+ ctx.sessionStreams.wiring?.moveTo(generation);
352
+ for (const feed of feeds)
353
+ if (ctx.sessionStreams.sessions.get(feed.sessionId) === feed &&
354
+ !sameStream(feed, exists.has(feed.sessionId)
355
+ ? { basin: currentBasin(ctx), stream: sessionStream(feed.sessionId) }
356
+ : undefined))
357
+ endFeed(ctx.sessionStreams, feed);
358
+ for (const [subject, epoch] of epochs)
359
+ endSubjectStreams(ctx.sessionStreams, subject, epoch);
360
+ }
361
+ /**
362
+ * Ends the streams of `subject` opened with a token older than `epoch` (a revocation), on
363
+ * this process. Other processes do the same on the `subject.revoked` signal.
364
+ */
365
+ export function endSubjectStreams(hub, subject, epoch) {
366
+ for (const feed of [...hub.sessions.values()])
367
+ for (const observer of [...feed.observers])
368
+ if (observer.holder?.subject === subject && observer.holder.epoch < epoch)
369
+ endObserver(hub, feed, observer, "revoked");
370
+ }
371
+ function endFeed(hub, feed) {
372
+ feed.read.abort();
373
+ if (hub.sessions.get(feed.sessionId) === feed)
374
+ hub.sessions.delete(feed.sessionId);
375
+ for (const observer of feed.observers) {
376
+ if (observer.deadline)
377
+ clearTimeout(observer.deadline);
378
+ observer.sink.end();
379
+ }
380
+ feed.observers.clear();
381
+ }
382
+ /** Reset or close: end every session observer and stop every shared read. */
383
+ export function clearObservers(hub) {
384
+ for (const feed of [...hub.sessions.values()])
385
+ endFeed(hub, feed);
386
+ hub.sessions.clear();
387
+ }
388
+ /** Close: end every session observer. */
389
+ export function endAllStreams(hub) {
390
+ clearObservers(hub);
391
+ }
392
+ export function sleep(ms, signal) {
393
+ return new Promise((resolve) => {
394
+ if (signal.aborted)
395
+ return resolve();
396
+ const done = () => {
397
+ clearTimeout(timer);
398
+ signal.removeEventListener("abort", done);
399
+ resolve();
400
+ };
401
+ const timer = setTimeout(done, ms);
402
+ signal.addEventListener("abort", done, { once: true });
403
+ });
404
+ }
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import { type PutAgentRequest, type PutSessionRequest } from "@nylorun/core/contracts";
7
7
  import type { Tx } from "../store/types.js";
8
- import { type Session, type TenantContext } from "./context.js";
8
+ import { type Session, type SessionAccess, type TenantContext } from "./context.js";
9
9
  export declare function listDefinitions(ctx: TenantContext): Promise<{
10
10
  agents: {
11
11
  agentId: any;
@@ -14,13 +14,27 @@ export declare function listDefinitions(ctx: TenantContext): Promise<{
14
14
  implementationVersion: string;
15
15
  }[];
16
16
  }>;
17
+ /**
18
+ * The agents a subject token or publishable key may see: id, name and description only, never
19
+ * instructions, tools or MCP servers, and only the agents allowed.
20
+ */
21
+ export declare function listAgentsPublic(ctx: TenantContext, agents: ReadonlySet<string> | "*"): Promise<{
22
+ agents: {
23
+ agentId: string;
24
+ name?: any;
25
+ description?: any;
26
+ }[];
27
+ }>;
17
28
  export declare function putDefinition(ctx: TenantContext, agentId: string, body: PutAgentRequest): Promise<{
18
29
  agentId: string;
19
30
  manifestHash: string;
20
31
  implementationVersion: string;
21
32
  }>;
22
- /** `owner`, when the request acts for a subject, limits the list to that subject's sessions. */
23
- export declare function listSessions(ctx: TenantContext, agentId: string | null, owner?: string): Promise<{
33
+ /**
34
+ * `access`, when the request acts for a person, limits the list to that person's sessions of
35
+ * the agents they may use.
36
+ */
37
+ export declare function listSessions(ctx: TenantContext, agentId: string | null, access?: SessionAccess): Promise<{
24
38
  sessions: {
25
39
  id: string;
26
40
  agentId: string;
@@ -34,7 +48,17 @@ export declare function listSessions(ctx: TenantContext, agentId: string | null,
34
48
  * the request acts for a subject, must be the session's owner: another owner's session id is
35
49
  * the 404 of a missing one (not the 409 of a mismatch), and so are another owner's vaults and
36
50
  * sandboxes.
51
+ *
52
+ * The session's sandbox is decided here and fixed for its life (§5 of Sandboxes v3): shared
53
+ * with `{ session }`, none with `false`, inline, or the Tenant default when omitted. A definition
54
+ * that declares its own sandbox (`.sandbox()`) keeps it and takes no sandbox request.
37
55
  */
38
- export declare function putSession(ctx: TenantContext, id: string, body: PutSessionRequest, owner?: string): Promise<Session>;
56
+ export declare function putSession(ctx: TenantContext, id: string, body: PutSessionRequest, access?: SessionAccess, options?: {
57
+ /**
58
+ * Create the session if it is missing and otherwise leave it as it is (an AG-UI thread's
59
+ * session): an existing one must have the same owner and agent, or it is a 404.
60
+ */
61
+ createOnly?: boolean;
62
+ }): Promise<Session>;
39
63
  /** The session resource returned by `GET`/`PUT /v1/sessions/:id`. */
40
64
  export declare function sessionView(t: Tx, s: Session): Promise<unknown>;
@@ -7,9 +7,12 @@ import { DefinitionDocumentSchema, } from "@nylorun/core/contracts";
7
7
  import { hashManifest } from "@nylorun/core/compatibility";
8
8
  import { aggregateWaits, isWorkflowManifest } from "../core/flow-host.js";
9
9
  import { canonical } from "../store/canonical.js";
10
- import { newStreamIncarnation } from "../streams/types.js";
11
10
  import { validateSandboxAttach } from "../core/sandbox-routes.js";
12
- import { sandboxLookup } from "./context.js";
11
+ import { resolveSandbox } from "../sandbox/resolve.js";
12
+ import { withSandboxCapability } from "../sandbox/session-sandbox.js";
13
+ import { declaredSandboxes, sandboxSpecOf, sessionSandboxSpec, } from "../sandbox/share.js";
14
+ import { effectiveSandboxConfig, readSandboxConfig } from "../sandbox/tenant-config.js";
15
+ import { sandboxLookup, } from "./context.js";
13
16
  import { fail } from "./http.js";
14
17
  /** A session's creation identity: everything but the request id and vault attachments. */
15
18
  const sessionIdentity = (value) => {
@@ -27,10 +30,31 @@ export async function listDefinitions(ctx) {
27
30
  })),
28
31
  };
29
32
  }
33
+ /**
34
+ * The agents a subject token or publishable key may see: id, name and description only, never
35
+ * instructions, tools or MCP servers, and only the agents allowed.
36
+ */
37
+ export async function listAgentsPublic(ctx, agents) {
38
+ const definitions = await ctx.store.tx((t) => t.listDefinitions());
39
+ return {
40
+ agents: definitions
41
+ .filter((d) => agents === "*" || agents.has(d.manifest.id))
42
+ .map((d) => ({
43
+ agentId: d.manifest.id,
44
+ ...(typeof d.manifest.name === "string" ? { name: d.manifest.name } : {}),
45
+ ...(typeof d.manifest.description === "string"
46
+ ? { description: d.manifest.description }
47
+ : {}),
48
+ })),
49
+ };
50
+ }
30
51
  export async function putDefinition(ctx, agentId, body) {
31
52
  if (body.manifest.id !== agentId)
32
53
  fail(400, "Agent id mismatch");
33
54
  DefinitionDocumentSchema.parse(body.manifest);
55
+ const declared = declaredSandboxes(body.manifest);
56
+ if (declared.length > 0)
57
+ fail(400, `${declared.join(", ")} declares a sandbox. Agents no longer declare one: remove .sandbox() and open the session with it, createSession({ sandbox: { image, network: { allow }, resources } }), or set the Tenant's default sandbox. See MIGRATION.md.`);
34
58
  const definition = {
35
59
  ...body,
36
60
  manifestHash: hashManifest(body.manifest),
@@ -42,14 +66,20 @@ export async function putDefinition(ctx, agentId, body) {
42
66
  implementationVersion: body.implementationVersion,
43
67
  };
44
68
  }
45
- /** `owner`, when the request acts for a subject, limits the list to that subject's sessions. */
46
- export async function listSessions(ctx, agentId, owner) {
69
+ /**
70
+ * `access`, when the request acts for a person, limits the list to that person's sessions of
71
+ * the agents they may use.
72
+ */
73
+ export async function listSessions(ctx, agentId, access) {
47
74
  const sessions = await ctx.store.tx((t) => t.listSessions({
48
75
  ...(agentId === null ? {} : { agentId }),
49
- ...(owner === undefined ? {} : { ownerUserId: owner }),
76
+ ...(access === undefined ? {} : { ownerUserId: access.owner }),
50
77
  }));
78
+ const allowed = access?.agents;
51
79
  return {
52
- sessions: sessions.map((s) => ({
80
+ sessions: sessions
81
+ .filter((s) => allowed === undefined || allowed.has(s.agentId))
82
+ .map((s) => ({
53
83
  id: s.id,
54
84
  agentId: s.agentId,
55
85
  ownerUserId: s.ownerUserId,
@@ -63,24 +93,33 @@ export async function listSessions(ctx, agentId, owner) {
63
93
  * the request acts for a subject, must be the session's owner: another owner's session id is
64
94
  * the 404 of a missing one (not the 409 of a mismatch), and so are another owner's vaults and
65
95
  * sandboxes.
96
+ *
97
+ * The session's sandbox is decided here and fixed for its life (§5 of Sandboxes v3): shared
98
+ * with `{ session }`, none with `false`, inline, or the Tenant default when omitted. A definition
99
+ * that declares its own sandbox (`.sandbox()`) keeps it and takes no sandbox request.
66
100
  */
67
- export function putSession(ctx, id, body, owner) {
101
+ export async function putSession(ctx, id, body, access, options = {}) {
68
102
  const vaultIds = body.vaultIds ?? [];
69
103
  const credentialSelections = body.credentialSelections ?? [];
70
- const opaque = owner !== undefined;
71
- if (opaque && body.ownerUserId !== owner)
104
+ const opaque = access !== undefined;
105
+ if (opaque && body.ownerUserId !== access.owner)
72
106
  fail(403, "ownerUserId must be the subject");
107
+ // An agent the subject may not use is the 404 of a missing definition.
108
+ if (access?.agents !== undefined && !access.agents.has(body.agentId))
109
+ fail(404, "Definition not found");
73
110
  return ctx.store.tx(async (t) => {
74
111
  const prior = await t.lockSession(id);
75
- if (opaque && prior && prior.ownerUserId !== owner)
112
+ if (opaque &&
113
+ prior &&
114
+ (prior.ownerUserId !== access.owner ||
115
+ (access.agents !== undefined && !access.agents.has(prior.agentId))))
76
116
  fail(404, "Session not found");
77
- const definition = prior === undefined || body.sandbox
78
- ? (await t.get("definitions", body.agentId)) ??
79
- fail(404, "Definition not found")
80
- : undefined;
81
- const sandboxOwnerId = body.sandbox
82
- ? validateSandboxAttach(body, (definition ?? prior).manifest, await sandboxLookup(t, body.sandbox.session), { opaque })
83
- : undefined;
117
+ if (prior && options.createOnly) {
118
+ if (prior.ownerUserId !== body.ownerUserId ||
119
+ prior.agentId !== body.agentId)
120
+ fail(404, "Session not found");
121
+ return prior;
122
+ }
84
123
  await ctx.vault.assertAttachment(t, body.ownerUserId, vaultIds, credentialSelections, { opaque });
85
124
  if (prior) {
86
125
  if (sessionIdentity(prior.creation) !== sessionIdentity(body))
@@ -88,18 +127,19 @@ export function putSession(ctx, id, body, owner) {
88
127
  prior.vaultIds = vaultIds;
89
128
  prior.credentialSelections = credentialSelections;
90
129
  prior.creation = body;
91
- if (sandboxOwnerId !== undefined)
92
- prior.sandboxOwnerId = sandboxOwnerId;
93
130
  await t.put("sessions", id, prior);
94
131
  await ctx.vault.recordAttachment(t, id, vaultIds);
95
132
  return prior;
96
133
  }
134
+ const definition = (await t.get("definitions", body.agentId)) ??
135
+ fail(404, "Definition not found");
136
+ const sandbox = await sessionSandbox(t, body, definition, { opaque });
97
137
  const created = {
98
138
  id,
99
139
  agentId: body.agentId,
100
140
  ownerUserId: body.ownerUserId,
101
- manifest: definition.manifest,
102
- manifestHash: definition.manifestHash,
141
+ manifest: sandbox.manifest ?? definition.manifest,
142
+ manifestHash: sandbox.manifestHash ?? definition.manifestHash,
103
143
  implementationVersion: definition.implementationVersion,
104
144
  info: body.info,
105
145
  status: "idle",
@@ -108,14 +148,66 @@ export function putSession(ctx, id, body, owner) {
108
148
  vaultIds,
109
149
  credentialSelections,
110
150
  pluginRoots: definition.pluginRoots ?? {},
111
- ...(sandboxOwnerId !== undefined ? { sandboxOwnerId } : {}),
112
- streamIncarnation: newStreamIncarnation(),
151
+ ...(sandbox.sandboxOwnerId !== undefined
152
+ ? { sandboxOwnerId: sandbox.sandboxOwnerId }
153
+ : {}),
154
+ ...(sandbox.spec !== undefined
155
+ ? { sandbox: sandbox.spec, sandboxSource: sandbox.source }
156
+ : {}),
113
157
  };
114
158
  await t.put("sessions", id, created);
115
159
  await ctx.vault.recordAttachment(t, id, vaultIds);
116
160
  return created;
117
161
  });
118
162
  }
163
+ function isShare(value) {
164
+ return typeof value === "object" && value !== null && "session" in value;
165
+ }
166
+ /** Decide a new session's sandbox and, for an agent, the manifest that carries its tools. */
167
+ async function sessionSandbox(t, body, definition, options) {
168
+ const request = body.sandbox;
169
+ const declared = sandboxSpecOf(definition.manifest) !== undefined;
170
+ if (isShare(request)) {
171
+ const lookup = await sandboxLookup(t, request.session);
172
+ const sandboxOwnerId = validateSandboxAttach(body, definition.manifest, lookup, {
173
+ opaque: options.opaque,
174
+ inherit: !declared,
175
+ });
176
+ if (declared || sandboxOwnerId === undefined)
177
+ return { sandboxOwnerId };
178
+ const spec = sessionSandboxSpec(lookup(request.session));
179
+ return { ...(await pin(definition, spec, "shared")), sandboxOwnerId };
180
+ }
181
+ if (declared) {
182
+ if (request !== undefined)
183
+ fail(400, `'${body.agentId}' declares its own sandbox with .sandbox(). Remove it from the agent to choose the sandbox when opening the session, or omit sandbox here.`);
184
+ return {};
185
+ }
186
+ const resolved = resolveSandbox({
187
+ request,
188
+ config: effectiveSandboxConfig(await readSandboxConfig(t)),
189
+ actingForSubject: options.opaque,
190
+ });
191
+ if (resolved.kind === "none")
192
+ return {};
193
+ if (resolved.kind === "error")
194
+ return fail(resolved.status, resolved.errors.join(" "));
195
+ return pin(definition, resolved.spec, resolved.source);
196
+ }
197
+ async function pin(definition, spec, source) {
198
+ // A workflow's manifest stays as registered: Action endpoints match workflow actions by its hash.
199
+ if (isWorkflowManifest(definition.manifest))
200
+ return { spec, source };
201
+ const sandboxed = withSandboxCapability(definition.manifest, spec);
202
+ if (!sandboxed.ok)
203
+ return fail(400, sandboxed.message);
204
+ return {
205
+ spec,
206
+ source,
207
+ manifest: sandboxed.manifest,
208
+ manifestHash: sandboxed.manifestHash,
209
+ };
210
+ }
119
211
  /** The session resource returned by `GET`/`PUT /v1/sessions/:id`. */
120
212
  export async function sessionView(t, s) {
121
213
  let waits = Array.isArray(s.waits)
@@ -132,7 +224,7 @@ export async function sessionView(t, s) {
132
224
  waits = aggregated;
133
225
  }
134
226
  const actions = await t.actionsForSession(s.id, {
135
- statuses: ["pending", "claimed", "uncertain"],
227
+ statuses: ["pending", "delivering", "uncertain"],
136
228
  });
137
229
  const uncertain = await t.effectsForSession(s.id, {
138
230
  statuses: ["uncertain"],
@@ -148,6 +240,8 @@ export async function sessionView(t, s) {
148
240
  vaultIds: s.vaultIds ?? [],
149
241
  credentialSelections: s.credentialSelections ?? [],
150
242
  sandboxOwnerId: s.sandboxOwnerId ?? null,
243
+ sandbox: s.sandbox ?? null,
244
+ ...(s.sandboxSource ? { sandboxSource: s.sandboxSource } : {}),
151
245
  mcpSnapshot: s.mcpSnapshot ?? null,
152
246
  mcpDiagnostics: s.mcpDiagnostics ?? [],
153
247
  waits,