@opengeni/api-router 0.23.1 → 0.30.0

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 (152) hide show
  1. package/dist/app.d.ts +3 -2
  2. package/dist/app.js +5 -3
  3. package/dist/auth/canonical-human-session-admission.d.ts +27 -0
  4. package/dist/browser-auth-broker.d.ts +20 -0
  5. package/dist/browser-controller-authority.d.ts +61 -0
  6. package/dist/browser-network-route.d.ts +8 -0
  7. package/dist/browser-state-authority.d.ts +27 -0
  8. package/dist/{chunk-T4T2PGU4.js → chunk-2PQMSRCB.js} +37891 -17463
  9. package/dist/chunk-2PQMSRCB.js.map +1 -0
  10. package/dist/codemode.d.ts +23 -0
  11. package/dist/controller-data-plane.d.ts +12 -0
  12. package/dist/editable-artifact-live-hints.d.ts +11 -0
  13. package/dist/editable-artifact-native-kernel.d.ts +37 -0
  14. package/dist/editable-artifact-office-import.d.ts +22 -0
  15. package/dist/editable-artifact-production.d.ts +29 -0
  16. package/dist/editable-artifact-websocket.d.ts +49 -0
  17. package/dist/editable-artifact-workspace-files.d.ts +23 -0
  18. package/dist/github-browser-flow.d.ts +6 -0
  19. package/dist/http/auth.d.ts +1 -1
  20. package/dist/http/cors.d.ts +1 -0
  21. package/dist/http/sse.d.ts +18 -1
  22. package/dist/index.d.ts +3 -2
  23. package/dist/index.js +1796 -22
  24. package/dist/index.js.map +1 -1
  25. package/dist/integrations/api-integrations.d.ts +24 -0
  26. package/dist/integrations/atlassian.d.ts +176 -0
  27. package/dist/integrations/fiken.d.ts +233 -0
  28. package/dist/integrations/github-skill-source.d.ts +5 -0
  29. package/dist/integrations/google-drive.d.ts +85 -0
  30. package/dist/integrations/oauth-client.d.ts +22 -1
  31. package/dist/integrations/provider-oauth.d.ts +17 -0
  32. package/dist/integrations/slack-bot.d.ts +175 -1
  33. package/dist/integrations/slack-interactions.d.ts +24 -6
  34. package/dist/integrations/social-api.d.ts +2 -1
  35. package/dist/interaction-holder-heartbeat.d.ts +17 -0
  36. package/dist/interaction-metrics.d.ts +11 -0
  37. package/dist/mcp/editable-artifact-query-schema.d.ts +4 -0
  38. package/dist/mcp/editable-artifacts.d.ts +13 -0
  39. package/dist/mcp/scheduled-task-view.d.ts +168 -0
  40. package/dist/mcp/server.d.ts +16 -4
  41. package/dist/memory-slack-delivery.d.ts +9 -0
  42. package/dist/model-catalog.d.ts +1 -0
  43. package/dist/routes/api-integrations.d.ts +8 -0
  44. package/dist/routes/browser-identities.d.ts +5 -0
  45. package/dist/routes/browser-sessions.d.ts +6 -0
  46. package/dist/routes/canonical-human-identities.d.ts +3 -0
  47. package/dist/routes/channels.d.ts +3 -0
  48. package/dist/routes/company-profile.d.ts +3 -0
  49. package/dist/routes/computer-sessions.d.ts +6 -0
  50. package/dist/routes/editable-artifacts.d.ts +44 -0
  51. package/dist/routes/integration-facets.d.ts +3 -0
  52. package/dist/routes/interaction-resources.d.ts +5 -0
  53. package/dist/routes/memory-slack-publications.d.ts +6 -0
  54. package/dist/routes/organization-memberships.d.ts +3 -0
  55. package/dist/routes/plugins.d.ts +8 -0
  56. package/dist/routes/sessions.d.ts +18 -2
  57. package/dist/routes/skills.d.ts +6 -0
  58. package/dist/routes/slack-task-policy.d.ts +3 -0
  59. package/dist/routes/supergrok.d.ts +3 -0
  60. package/dist/routes/video-generation.d.ts +3 -0
  61. package/dist/sandbox/channel-a.d.ts +62 -2
  62. package/dist/sandbox/machines.d.ts +2 -2
  63. package/dist/sandbox/metrics-ingestion.d.ts +6 -1
  64. package/dist/sandbox/viewer.d.ts +7 -3
  65. package/dist/slack-reaction-files.d.ts +27 -0
  66. package/dist/temporal-schedule-cleanup.d.ts +26 -0
  67. package/dist/transcription/providers/xai-subscription.d.ts +8 -0
  68. package/dist/xai-realtime.d.ts +26 -0
  69. package/dist/xai-subscription-auth.d.ts +23 -0
  70. package/package.json +20 -14
  71. package/src/app.ts +604 -64
  72. package/src/auth/canonical-human-session-admission.ts +116 -0
  73. package/src/auth/managed-auth.ts +101 -0
  74. package/src/browser-auth-broker.ts +155 -0
  75. package/src/browser-controller-authority.ts +186 -0
  76. package/src/browser-network-route.ts +34 -0
  77. package/src/browser-state-authority.ts +236 -0
  78. package/src/codemode.ts +186 -0
  79. package/src/controller-data-plane.ts +35 -0
  80. package/src/editable-artifact-live-hints.ts +64 -0
  81. package/src/editable-artifact-native-kernel.ts +659 -0
  82. package/src/editable-artifact-office-import.ts +230 -0
  83. package/src/editable-artifact-production.ts +419 -0
  84. package/src/editable-artifact-websocket.ts +311 -0
  85. package/src/editable-artifact-workspace-files.ts +186 -0
  86. package/src/github-browser-flow.ts +35 -6
  87. package/src/http/auth.ts +29 -9
  88. package/src/http/cors.ts +3 -0
  89. package/src/http/sse.ts +229 -5
  90. package/src/index.ts +119 -20
  91. package/src/integrations/api-integrations.ts +365 -0
  92. package/src/integrations/atlassian.ts +1621 -0
  93. package/src/integrations/fiken.ts +1230 -0
  94. package/src/integrations/github-skill-source.ts +142 -0
  95. package/src/integrations/google-drive.ts +1005 -64
  96. package/src/integrations/oauth-client.ts +123 -33
  97. package/src/integrations/provider-oauth.ts +756 -0
  98. package/src/integrations/slack-bot.ts +669 -11
  99. package/src/integrations/slack-interactions.ts +2063 -93
  100. package/src/integrations/social-api.ts +11 -0
  101. package/src/interaction-holder-heartbeat.ts +95 -0
  102. package/src/interaction-metrics.ts +159 -0
  103. package/src/mcp/documents.ts +146 -6
  104. package/src/mcp/editable-artifact-query-schema.ts +236 -0
  105. package/src/mcp/editable-artifacts.ts +448 -0
  106. package/src/mcp/scheduled-task-view.ts +34 -0
  107. package/src/mcp/server.ts +1756 -355
  108. package/src/memory-slack-delivery.ts +209 -0
  109. package/src/model-catalog.ts +19 -6
  110. package/src/routes/api-integrations.ts +410 -0
  111. package/src/routes/browser-identities.ts +160 -0
  112. package/src/routes/browser-sessions.ts +4482 -0
  113. package/src/routes/canonical-human-identities.ts +156 -0
  114. package/src/routes/channels.ts +90 -0
  115. package/src/routes/company-profile.ts +255 -0
  116. package/src/routes/computer-sessions.ts +1602 -0
  117. package/src/routes/connections.ts +421 -99
  118. package/src/routes/documents.ts +22 -3
  119. package/src/routes/editable-artifacts.ts +1159 -0
  120. package/src/routes/files.ts +106 -4
  121. package/src/routes/github.ts +18 -2
  122. package/src/routes/install.ts +7 -1
  123. package/src/routes/integration-facets.ts +258 -0
  124. package/src/routes/interaction-resources.ts +595 -0
  125. package/src/routes/memory-slack-publications.ts +216 -0
  126. package/src/routes/organization-memberships.ts +53 -0
  127. package/src/routes/packs.ts +437 -7
  128. package/src/routes/plugins.ts +752 -0
  129. package/src/routes/rigs.ts +77 -20
  130. package/src/routes/scheduled-tasks.ts +67 -40
  131. package/src/routes/sessions.ts +830 -278
  132. package/src/routes/skills.ts +256 -0
  133. package/src/routes/slack-task-policy.ts +115 -0
  134. package/src/routes/supergrok.ts +717 -0
  135. package/src/routes/transcription-recordings.ts +9 -2
  136. package/src/routes/transcriptions.ts +2 -1
  137. package/src/routes/video-generation.ts +162 -0
  138. package/src/routes/workspaces.ts +68 -28
  139. package/src/sandbox/channel-a.ts +750 -116
  140. package/src/sandbox/machines.ts +5 -5
  141. package/src/sandbox/metrics-ingestion.ts +121 -3
  142. package/src/sandbox/rematerialize.ts +35 -47
  143. package/src/sandbox/viewer.ts +39 -12
  144. package/src/slack-reaction-files.ts +181 -0
  145. package/src/temporal-schedule-cleanup.ts +135 -0
  146. package/src/transcription/providers/xai-subscription.ts +110 -0
  147. package/src/transcription/service.ts +17 -2
  148. package/src/xai-realtime.ts +216 -0
  149. package/src/xai-subscription-auth.ts +132 -0
  150. package/dist/chunk-T4T2PGU4.js.map +0 -1
  151. package/dist/mcp/toolspace.d.ts +0 -71
  152. package/src/mcp/toolspace.ts +0 -1190
@@ -0,0 +1,1602 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { resolveFirstPartyDelegationSecret, resolveStreamTokenSecret } from "@opengeni/config";
3
+ import {
4
+ BROWSER_CONTROL_WEBSOCKET_BEARER_PREFIX,
5
+ BROWSER_CONTROL_PORT,
6
+ COMPUTER_CONTROL_WEBSOCKET_PROTOCOL,
7
+ COMPUTER_RFB_WEBSOCKET_PROTOCOL,
8
+ ComputerActionCommand,
9
+ ComputerActionRequest,
10
+ ComputerSessionAttachment,
11
+ ComputerSessionAttachmentRequest,
12
+ ComputerSessionHeartbeatResponse,
13
+ ComputerSessionLifecycleRequest,
14
+ ComputerSessionListResponse,
15
+ ComputerSessionMutationResponse,
16
+ ComputerTargetListResponse,
17
+ CreateComputerSessionRequest,
18
+ InteractionActor,
19
+ INTERACTION_PROTOCOL_VERSION,
20
+ type AccessGrant,
21
+ type ComputerSession as ComputerSessionValue,
22
+ type CreateComputerSessionRequest as CreateComputerSessionRequestValue,
23
+ type InteractionPlacement,
24
+ type Session,
25
+ type SessionAuthorizationOperation,
26
+ } from "@opengeni/contracts";
27
+ import {
28
+ acquireLease,
29
+ activateComputerSession,
30
+ completeComputerSessionEnd,
31
+ ComputerSessionNotFoundError,
32
+ ComputerSessionOperationConflictError,
33
+ ComputerSessionStateError,
34
+ dispatchComputerSessionOperation,
35
+ failComputerSessionOperation,
36
+ findComputerSessionControlRecordByOperation,
37
+ getAttachedBrowserDevice,
38
+ getComputerSessionControlRecord,
39
+ getEnrollment,
40
+ getSession,
41
+ listComputerSessions,
42
+ prepareComputerSessionCreate,
43
+ prepareComputerSessionEnd,
44
+ releaseLeaseHolder,
45
+ readLease,
46
+ recordLeaseControllerDataPlaneUrl,
47
+ touchComputerSessionController,
48
+ type ComputerSessionControlRecord,
49
+ type LeaseSnapshot,
50
+ } from "@opengeni/db";
51
+ import {
52
+ requireAccessGrant,
53
+ requireSessionAuthorization,
54
+ relayConfigFromSettings,
55
+ SessionAuthorizationDeniedError,
56
+ SessionAuthorizationUnavailableError,
57
+ type ApiRouteDeps,
58
+ } from "@opengeni/core";
59
+ import {
60
+ BrowserControlClient,
61
+ BrowserControlProtocolError,
62
+ BrowserControlRequestError,
63
+ BrowserControlServerError,
64
+ BrowserControlServerUnsupportedError,
65
+ BrowserControlTransportError,
66
+ BrowserControlUnsupportedError,
67
+ buildSelfhostedBackendSession,
68
+ buildStreamUrl,
69
+ exposedPortEndpointFromUrl,
70
+ mintStreamToken,
71
+ NatsControlRpc,
72
+ NatsOpStreamTransport,
73
+ provisionBrowserControlClient,
74
+ type BrowserControlPlacementSession,
75
+ } from "@opengeni/runtime/sandbox";
76
+ import type { Context, Hono } from "hono";
77
+ import { HTTPException } from "hono/http-exception";
78
+ import type { ContentfulStatusCode } from "hono/utils/http-status";
79
+ import {
80
+ deriveBrowserControllerAdminToken,
81
+ deriveComputerSessionControllerTokens,
82
+ deriveComputerViewGrantToken,
83
+ } from "../browser-controller-authority";
84
+ import { withCachedController } from "../controller-data-plane";
85
+ import { withInteractionHolderHeartbeat } from "../interaction-holder-heartbeat";
86
+ import { allowedCorsOrigin } from "../http/cors";
87
+ import { observeComputerActionResult, observeLifecycleResult } from "../interaction-metrics";
88
+ import { withChannelA, withChannelARead, type ChannelAOperation } from "../sandbox/channel-a";
89
+
90
+ type ComputerPlacement = {
91
+ placement: InteractionPlacement;
92
+ placementInstanceId: string;
93
+ session: BrowserControlPlacementSession;
94
+ lease: LeaseSnapshot | null;
95
+ };
96
+
97
+ /** Public ComputerSession resource surface. Physical app/window authority stays
98
+ * in the same placement controller used by BrowserSession; this route owns only
99
+ * durable authorization, placement fencing, lifecycle receipts, and routing. */
100
+ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): void {
101
+ const channelServices = {
102
+ db: deps.db,
103
+ settings: deps.settings,
104
+ bus: deps.bus,
105
+ observability: deps.observability,
106
+ };
107
+
108
+ app.get("/v1/workspaces/:workspaceId/computer-sessions", async (context) => {
109
+ const workspaceId = context.req.param("workspaceId") ?? "";
110
+ const grant = await requireAccessGrant(context, deps, workspaceId, "sessions:read");
111
+ return context.json(
112
+ ComputerSessionListResponse.parse(
113
+ await listComputerSessions(deps.db, {
114
+ accountId: grant.accountId,
115
+ workspaceId,
116
+ }),
117
+ ),
118
+ );
119
+ });
120
+
121
+ app.get("/v1/workspaces/:workspaceId/computer-sessions/:computerSessionId", async (context) => {
122
+ const workspaceId = context.req.param("workspaceId") ?? "";
123
+ const grant = await requireAccessGrant(context, deps, workspaceId, "sessions:read");
124
+ const computerSessionId = requireUuidParam(context, "computerSessionId");
125
+ try {
126
+ const record = await getComputerSessionControlRecord(deps.db, {
127
+ accountId: grant.accountId,
128
+ workspaceId,
129
+ computerSessionId,
130
+ });
131
+ await authorizeSourceSession(deps, grant, record.sourceSessionId, "session.read");
132
+ return context.json(record.session);
133
+ } catch (error) {
134
+ throw computerRouteError(error);
135
+ }
136
+ });
137
+
138
+ app.post("/v1/workspaces/:workspaceId/computer-sessions", async (context) => {
139
+ const workspaceId = context.req.param("workspaceId") ?? "";
140
+ const grant = await requireAccessGrant(context, deps, workspaceId, "sessions:control");
141
+ const request = await parseJsonBody(context, CreateComputerSessionRequest);
142
+ const startedAtMs = performance.now();
143
+ await authorizeSourceSession(deps, grant, request.sessionId, "session.control");
144
+ const origin = requestOrigin(context, deps.settings.corsAllowOriginRegex);
145
+ const authority = controllerAuthorityRoot(deps);
146
+
147
+ try {
148
+ const existing = await findComputerSessionControlRecordByOperation(deps.db, {
149
+ accountId: grant.accountId,
150
+ workspaceId,
151
+ operationId: request.operationId,
152
+ });
153
+ if (existing && existing.sourceSessionId !== request.sessionId) {
154
+ throw new ComputerSessionOperationConflictError(
155
+ "ComputerSession create operation belongs to another source session",
156
+ );
157
+ }
158
+ if (existing) assertCreateReplay(request, existing.session);
159
+ let prepared = existing
160
+ ? await prepareComputerSessionCreate(
161
+ deps.db,
162
+ computerCreateInput(grant, workspaceId, request, existing.session.placement),
163
+ )
164
+ : null;
165
+ if (prepared && isTerminalOperation(prepared.operation.state)) {
166
+ const parsed = ComputerSessionMutationResponse.parse(prepared);
167
+ observeLifecycleResult(deps.observability, startedAtMs, parsed);
168
+ return context.json(parsed, 200);
169
+ }
170
+
171
+ const sourceSession = await requireSourceSession(deps, workspaceId, request.sessionId);
172
+ const response = await withComputerPlacement(
173
+ sourceSession,
174
+ grant,
175
+ existing?.session.placement ?? request.placement ?? null,
176
+ existing?.session.controller?.placementInstanceId ?? null,
177
+ "computer.create",
178
+ context.req.raw.signal,
179
+ async (placement) => {
180
+ if (!prepared) {
181
+ prepared = await prepareComputerSessionCreate(
182
+ deps.db,
183
+ computerCreateInput(grant, workspaceId, request, placement.placement),
184
+ );
185
+ }
186
+ if (isTerminalOperation(prepared.operation.state)) return prepared;
187
+
188
+ const interactionHeld = await ensureInteractionHolder(
189
+ grant,
190
+ sourceSession,
191
+ prepared.session.id,
192
+ placement,
193
+ context.req.raw.signal,
194
+ );
195
+ const record = await ensureDispatchedGeneration(
196
+ grant,
197
+ workspaceId,
198
+ prepared.session.id,
199
+ request.operationId,
200
+ placement.placementInstanceId,
201
+ ).catch(async (error: unknown) => {
202
+ if (interactionHeld) {
203
+ await releaseInteractionHolder(
204
+ grant,
205
+ workspaceId,
206
+ prepared!.session.id,
207
+ placement.placement,
208
+ ).catch(() => undefined);
209
+ }
210
+ throw error;
211
+ });
212
+ const preparedSession = prepared.session;
213
+ const controllerGeneration = requireOperationGeneration(record);
214
+ const adminToken = deriveBrowserControllerAdminToken({
215
+ rootSecret: authority,
216
+ accountId: grant.accountId,
217
+ workspaceId,
218
+ placement: placement.placement,
219
+ placementInstanceId: placement.placementInstanceId,
220
+ });
221
+ const tokens = deriveComputerSessionControllerTokens({
222
+ rootSecret: authority,
223
+ accountId: grant.accountId,
224
+ workspaceId,
225
+ computerSessionId: prepared.session.id,
226
+ placement: placement.placement,
227
+ placementInstanceId: placement.placementInstanceId,
228
+ controllerGeneration,
229
+ tokenGeneration: record.tokenGeneration,
230
+ });
231
+ let physical;
232
+ try {
233
+ physical = await withComputerCreationController(
234
+ grant,
235
+ workspaceId,
236
+ placement,
237
+ adminToken,
238
+ origin,
239
+ async (client) =>
240
+ await withInteractionHolderHeartbeat(
241
+ deps,
242
+ {
243
+ grant,
244
+ workspaceId,
245
+ sandboxGroupId:
246
+ placement.placement.kind === "sandbox_group"
247
+ ? placement.placement.sandboxGroupId
248
+ : null,
249
+ holderId: interactionHolderId(preparedSession.id),
250
+ operationId: request.operationId,
251
+ resourceId: preparedSession.id,
252
+ controllerGeneration,
253
+ },
254
+ async () =>
255
+ await client.createComputerSession({
256
+ computerSessionId: preparedSession.id,
257
+ controllerGeneration,
258
+ tokenGeneration: record.tokenGeneration,
259
+ ...tokens,
260
+ }),
261
+ ),
262
+ );
263
+ await cacheComputerControllerPlacement(grant, workspaceId, placement).catch(
264
+ () => placement,
265
+ );
266
+ } catch (error) {
267
+ if (
268
+ error instanceof BrowserControlTransportError ||
269
+ (error instanceof BrowserControlRequestError &&
270
+ error.retryable &&
271
+ error.error.code !== "machine_locked") ||
272
+ isAbort(error)
273
+ ) {
274
+ throw error;
275
+ }
276
+ const failed = await failComputerSessionOperation(deps.db, {
277
+ accountId: grant.accountId,
278
+ workspaceId,
279
+ operationId: request.operationId,
280
+ computerSessionId: preparedSession.id,
281
+ ...(error instanceof BrowserControlProtocolError
282
+ ? { state: "outcome_unknown" as const }
283
+ : {}),
284
+ error: interactionFailure(error),
285
+ });
286
+ if (interactionHeld && !(error instanceof BrowserControlProtocolError)) {
287
+ await releaseInteractionHolder(
288
+ grant,
289
+ workspaceId,
290
+ prepared.session.id,
291
+ placement.placement,
292
+ ).catch(() => undefined);
293
+ }
294
+ return failed;
295
+ }
296
+ // These facts come from the physical adapter after its native helper
297
+ // and seat are live. The API never guesses a platform or display.
298
+ return await activateComputerSession(deps.db, {
299
+ accountId: grant.accountId,
300
+ workspaceId,
301
+ operationId: request.operationId,
302
+ computerSessionId: preparedSession.id,
303
+ controller: {
304
+ controllerId: "opengeni-browserd",
305
+ controllerGeneration,
306
+ placementInstanceId: placement.placementInstanceId,
307
+ },
308
+ platform: physical.platform,
309
+ adapter: physical.adapter,
310
+ seatId: physical.seatId,
311
+ displayId: physical.displayId,
312
+ capabilities: physical.capabilities,
313
+ });
314
+ },
315
+ );
316
+ const parsed = ComputerSessionMutationResponse.parse(response);
317
+ observeLifecycleResult(deps.observability, startedAtMs, parsed);
318
+ return context.json(
319
+ parsed,
320
+ parsed.operation.state === "completed" && !parsed.operation.replayed ? 201 : 200,
321
+ );
322
+ } catch (error) {
323
+ throw computerRouteError(error);
324
+ }
325
+ });
326
+
327
+ app.get(
328
+ "/v1/workspaces/:workspaceId/computer-sessions/:computerSessionId/targets",
329
+ async (context) => {
330
+ const { workspaceId, grant, computerSessionId } = await routePreamble(
331
+ context,
332
+ "sessions:read",
333
+ );
334
+ const result = await withActiveComputerController(
335
+ context,
336
+ grant,
337
+ workspaceId,
338
+ computerSessionId,
339
+ "session.read",
340
+ "computer.read",
341
+ async ({ sessionClient, binding }) =>
342
+ ComputerTargetListResponse.parse({
343
+ computerSessionId,
344
+ controllerGeneration: binding.controllerGeneration,
345
+ targets: await sessionClient.listTargets(),
346
+ }),
347
+ );
348
+ return context.json(result);
349
+ },
350
+ );
351
+
352
+ app.get(
353
+ "/v1/workspaces/:workspaceId/computer-sessions/:computerSessionId/targets/:targetId/observation",
354
+ async (context) => {
355
+ const { workspaceId, grant, computerSessionId } = await routePreamble(
356
+ context,
357
+ "sessions:read",
358
+ );
359
+ const targetId = requireOpaqueParam(context, "targetId");
360
+ const result = await withActiveComputerController(
361
+ context,
362
+ grant,
363
+ workspaceId,
364
+ computerSessionId,
365
+ "session.read",
366
+ "computer.read",
367
+ async ({ sessionClient }) => await sessionClient.observe(targetId),
368
+ );
369
+ return context.json(result);
370
+ },
371
+ );
372
+
373
+ app.get(
374
+ "/v1/workspaces/:workspaceId/computer-sessions/:computerSessionId/clipboard",
375
+ async (context) => {
376
+ const { workspaceId, grant, computerSessionId } = await routePreamble(
377
+ context,
378
+ "sessions:read",
379
+ );
380
+ const result = await withActiveComputerController(
381
+ context,
382
+ grant,
383
+ workspaceId,
384
+ computerSessionId,
385
+ "session.read",
386
+ "computer.read",
387
+ async ({ sessionClient }) => await sessionClient.readClipboard(),
388
+ );
389
+ return context.json(result);
390
+ },
391
+ );
392
+
393
+ app.post(
394
+ "/v1/workspaces/:workspaceId/computer-sessions/:computerSessionId/actions",
395
+ async (context) => {
396
+ const { workspaceId, grant, computerSessionId } = await routePreamble(
397
+ context,
398
+ "sessions:control",
399
+ );
400
+ const request = await parseJsonBody(context, ComputerActionRequest);
401
+ const startedAtMs = performance.now();
402
+ const result = await withActiveComputerController(
403
+ context,
404
+ grant,
405
+ workspaceId,
406
+ computerSessionId,
407
+ "session.control",
408
+ "computer.action",
409
+ async ({ sessionClient, binding }) =>
410
+ await sessionClient.action(
411
+ ComputerActionCommand.parse({
412
+ protocolVersion: INTERACTION_PROTOCOL_VERSION,
413
+ operationId: request.operationId,
414
+ computerSessionId,
415
+ controllerGeneration: binding.controllerGeneration,
416
+ targetId: request.targetId,
417
+ expectedTargetGeneration: request.expectedTargetGeneration,
418
+ expectedObservationId: request.expectedObservationId,
419
+ expectedFrameId: request.expectedFrameId,
420
+ actor: interactionActorForGrant(grant),
421
+ action: request.action,
422
+ }),
423
+ ),
424
+ );
425
+ observeComputerActionResult(deps.observability, startedAtMs, request, result);
426
+ return context.json(result);
427
+ },
428
+ );
429
+
430
+ app.get(
431
+ "/v1/workspaces/:workspaceId/computer-sessions/:computerSessionId/operations/:operationId",
432
+ async (context) => {
433
+ const { workspaceId, grant, computerSessionId } = await routePreamble(
434
+ context,
435
+ "sessions:read",
436
+ );
437
+ const operationId = requireUuidParam(context, "operationId");
438
+ const result = await withActiveComputerController(
439
+ context,
440
+ grant,
441
+ workspaceId,
442
+ computerSessionId,
443
+ "session.read",
444
+ "computer.read",
445
+ async ({ sessionClient }) => await sessionClient.receipt(operationId),
446
+ );
447
+ return context.json(result);
448
+ },
449
+ );
450
+
451
+ app.post(
452
+ "/v1/workspaces/:workspaceId/computer-sessions/:computerSessionId/attachments",
453
+ async (context) => {
454
+ const { workspaceId, grant, computerSessionId } = await routePreamble(context, "stream:view");
455
+ const origin = requestOrigin(context, deps.settings.corsAllowOriginRegex);
456
+ const request = await parseJsonBody(context, ComputerSessionAttachmentRequest);
457
+ const result = await withActiveComputerController(
458
+ context,
459
+ grant,
460
+ workspaceId,
461
+ computerSessionId,
462
+ "session.viewer.read",
463
+ "computer.attach",
464
+ async ({ client, sessionClient, record, binding, placement }) => {
465
+ if (origin) await client.addAllowedOrigins([origin]);
466
+ const grantId = randomUUID();
467
+ const expiresAt = new Date(Date.now() + request.expiresInSeconds * 1_000).toISOString();
468
+ const token = deriveComputerViewGrantToken({
469
+ rootSecret: controllerAuthorityRoot(deps),
470
+ accountId: grant.accountId,
471
+ workspaceId,
472
+ placement: record.session.placement,
473
+ placementInstanceId: placement.placementInstanceId,
474
+ computerSessionId,
475
+ controllerGeneration: binding.controllerGeneration,
476
+ tokenGeneration: record.tokenGeneration,
477
+ grantId,
478
+ expiresAt,
479
+ });
480
+ const reference = {
481
+ computerSessionId,
482
+ controllerGeneration: binding.controllerGeneration,
483
+ };
484
+ await client.createComputerViewGrant(reference, { grantId, token, expiresAt });
485
+ const target = (await sessionClient.listTargets()).find(
486
+ (candidate) => candidate.id === request.targetId,
487
+ );
488
+ if (!target) {
489
+ throw new BrowserControlRequestError(404, {
490
+ code: "target_not_found",
491
+ message: "computer target does not exist",
492
+ retryable: false,
493
+ });
494
+ }
495
+ const relaySecret = placement.session.openComputerFrames
496
+ ? resolveStreamTokenSecret(deps.settings)
497
+ : null;
498
+ if (placement.session.openComputerFrames && !relaySecret) {
499
+ throw new BrowserControlUnsupportedError(
500
+ "computer frame relay authority is unavailable",
501
+ );
502
+ }
503
+ let relayed = null;
504
+ try {
505
+ relayed = await client.openRelayedComputerFrameStream({
506
+ reference,
507
+ targetId: request.targetId,
508
+ viewToken: token,
509
+ expiresAt,
510
+ ...(request.stream ? { stream: request.stream } : {}),
511
+ });
512
+ } catch (error) {
513
+ console.error("computer frame relay open failed", {
514
+ computerSessionId,
515
+ targetId: request.targetId,
516
+ failure: error instanceof Error ? error.message : String(error),
517
+ });
518
+ throw error;
519
+ }
520
+ const stream = relayed
521
+ ? await (async () => {
522
+ const relayToken = await mintStreamToken(relaySecret!, {
523
+ workspaceId,
524
+ sessionId: record.sourceSessionId,
525
+ viewerId: grantId,
526
+ leaseEpoch: record.tokenGeneration,
527
+ port: relayed.channel.port,
528
+ ttlSeconds: request.expiresInSeconds,
529
+ });
530
+ return {
531
+ kind: "relay" as const,
532
+ url: buildStreamUrl(relayed.endpoint),
533
+ token: relayToken,
534
+ channel: {
535
+ channelId: relayed.channel.channelId,
536
+ workspaceId: relayed.channel.workspaceId,
537
+ agentId: relayed.channel.agentId,
538
+ kind: 4 as const,
539
+ port: relayed.channel.port,
540
+ },
541
+ };
542
+ })()
543
+ : record.session.placement.kind === "sandbox_group" &&
544
+ record.session.platform === "linux" &&
545
+ target.kind === "screen"
546
+ ? {
547
+ kind: "direct_rfb" as const,
548
+ url: await client.computerRfbStreamUrl(reference, request.targetId),
549
+ protocols: [
550
+ "binary",
551
+ COMPUTER_RFB_WEBSOCKET_PROTOCOL,
552
+ `${BROWSER_CONTROL_WEBSOCKET_BEARER_PREFIX}${token}`,
553
+ ],
554
+ }
555
+ : {
556
+ kind: "direct_websocket" as const,
557
+ url: await client.computerFrameStreamUrl(
558
+ reference,
559
+ request.targetId,
560
+ request.stream,
561
+ ),
562
+ protocols: [
563
+ COMPUTER_CONTROL_WEBSOCKET_PROTOCOL,
564
+ `${BROWSER_CONTROL_WEBSOCKET_BEARER_PREFIX}${token}`,
565
+ ],
566
+ };
567
+ return ComputerSessionAttachment.parse({
568
+ computerSessionId,
569
+ controllerGeneration: binding.controllerGeneration,
570
+ targetId: request.targetId,
571
+ stream,
572
+ expiresAt,
573
+ });
574
+ },
575
+ );
576
+ return context.json(result, 201);
577
+ },
578
+ );
579
+
580
+ app.post(
581
+ "/v1/workspaces/:workspaceId/computer-sessions/:computerSessionId/heartbeat",
582
+ async (context) => {
583
+ const { workspaceId, grant, computerSessionId } = await routePreamble(
584
+ context,
585
+ "sessions:read",
586
+ );
587
+ await parseEmptyJsonBody(context);
588
+ const result = await withActiveComputerController(
589
+ context,
590
+ grant,
591
+ workspaceId,
592
+ computerSessionId,
593
+ "session.read",
594
+ "computer.read",
595
+ async ({ sessionClient, binding }) => {
596
+ await sessionClient.heartbeat();
597
+ return ComputerSessionHeartbeatResponse.parse({
598
+ computerSessionId,
599
+ controllerGeneration: binding.controllerGeneration,
600
+ alive: true,
601
+ });
602
+ },
603
+ false,
604
+ );
605
+ return context.json(result);
606
+ },
607
+ );
608
+
609
+ app.post(
610
+ "/v1/workspaces/:workspaceId/computer-sessions/:computerSessionId/end",
611
+ async (context) => {
612
+ const workspaceId = context.req.param("workspaceId") ?? "";
613
+ const grant = await requireAccessGrant(context, deps, workspaceId, "sessions:control");
614
+ const computerSessionId = requireUuidParam(context, "computerSessionId");
615
+ const request = await parseJsonBody(context, ComputerSessionLifecycleRequest);
616
+ const startedAtMs = performance.now();
617
+ const origin = requestOrigin(context, deps.settings.corsAllowOriginRegex);
618
+ try {
619
+ const before = await getComputerSessionControlRecord(deps.db, {
620
+ accountId: grant.accountId,
621
+ workspaceId,
622
+ computerSessionId,
623
+ });
624
+ await authorizeSourceSession(deps, grant, before.sourceSessionId, "session.control");
625
+ if (before.session.controller) controllerAuthorityRoot(deps);
626
+ const prepared = await prepareComputerSessionEnd(deps.db, {
627
+ accountId: grant.accountId,
628
+ workspaceId,
629
+ computerSessionId,
630
+ operationId: request.operationId,
631
+ actorSubjectId: grant.subjectId,
632
+ });
633
+ if (isTerminalOperation(prepared.operation.state)) {
634
+ if (prepared.operation.state === "completed") {
635
+ await releaseInteractionHolder(
636
+ grant,
637
+ workspaceId,
638
+ computerSessionId,
639
+ before.session.placement,
640
+ ).catch(() => undefined);
641
+ }
642
+ const parsed = ComputerSessionMutationResponse.parse(prepared);
643
+ observeLifecycleResult(deps.observability, startedAtMs, parsed);
644
+ return context.json(parsed, 200);
645
+ }
646
+
647
+ const record = await getComputerSessionControlRecord(deps.db, {
648
+ accountId: grant.accountId,
649
+ workspaceId,
650
+ computerSessionId,
651
+ operationId: request.operationId,
652
+ });
653
+ const binding = record.session.controller;
654
+ if (!binding) {
655
+ const completed = await completeComputerSessionEnd(deps.db, {
656
+ accountId: grant.accountId,
657
+ workspaceId,
658
+ operationId: request.operationId,
659
+ computerSessionId,
660
+ expectedControllerGeneration: null,
661
+ });
662
+ await releaseInteractionHolder(
663
+ grant,
664
+ workspaceId,
665
+ computerSessionId,
666
+ record.session.placement,
667
+ ).catch(() => undefined);
668
+ const parsed = ComputerSessionMutationResponse.parse(completed);
669
+ observeLifecycleResult(deps.observability, startedAtMs, parsed);
670
+ return context.json(parsed, 200);
671
+ }
672
+
673
+ const sourceSession = await requireSourceSession(deps, workspaceId, record.sourceSessionId);
674
+ const response = await withComputerPlacement(
675
+ sourceSession,
676
+ grant,
677
+ record.session.placement,
678
+ binding.placementInstanceId,
679
+ "computer.end",
680
+ context.req.raw.signal,
681
+ async (placement) => {
682
+ await dispatchComputerSessionOperation(deps.db, {
683
+ accountId: grant.accountId,
684
+ workspaceId,
685
+ operationId: request.operationId,
686
+ computerSessionId,
687
+ controllerGeneration: binding.controllerGeneration,
688
+ });
689
+ const client = await provisionController(grant, record, placement, origin);
690
+ try {
691
+ await withInteractionHolderHeartbeat(
692
+ deps,
693
+ {
694
+ grant,
695
+ workspaceId,
696
+ sandboxGroupId:
697
+ placement.placement.kind === "sandbox_group"
698
+ ? placement.placement.sandboxGroupId
699
+ : null,
700
+ holderId: interactionHolderId(computerSessionId),
701
+ operationId: request.operationId,
702
+ resourceId: computerSessionId,
703
+ controllerGeneration: binding.controllerGeneration,
704
+ },
705
+ async () =>
706
+ await client.endComputerSession(
707
+ {
708
+ computerSessionId,
709
+ controllerGeneration: binding.controllerGeneration,
710
+ },
711
+ { removeState: true },
712
+ ),
713
+ );
714
+ } catch (error) {
715
+ if (!(error instanceof BrowserControlRequestError && error.status === 404)) {
716
+ throw error;
717
+ }
718
+ }
719
+ const completed = await completeComputerSessionEnd(deps.db, {
720
+ accountId: grant.accountId,
721
+ workspaceId,
722
+ operationId: request.operationId,
723
+ computerSessionId,
724
+ expectedControllerGeneration: binding.controllerGeneration,
725
+ });
726
+ await releaseInteractionHolder(
727
+ grant,
728
+ workspaceId,
729
+ computerSessionId,
730
+ record.session.placement,
731
+ ).catch(() => undefined);
732
+ return completed;
733
+ },
734
+ );
735
+ const parsed = ComputerSessionMutationResponse.parse(response);
736
+ observeLifecycleResult(deps.observability, startedAtMs, parsed);
737
+ return context.json(parsed, 200);
738
+ } catch (error) {
739
+ throw computerRouteError(error);
740
+ }
741
+ },
742
+ );
743
+
744
+ async function withComputerPlacement<T>(
745
+ sourceSession: Session,
746
+ grant: AccessGrant,
747
+ expectedPlacement: InteractionPlacement | null,
748
+ expectedPlacementInstanceId: string | null,
749
+ operation: ChannelAOperation,
750
+ waitSignal: AbortSignal,
751
+ callback: (placement: ComputerPlacement) => Promise<T>,
752
+ ): Promise<T> {
753
+ if (expectedPlacement?.kind === "external_provider") {
754
+ throw new BrowserControlUnsupportedError(
755
+ `computer placement ${expectedPlacement.kind} is not executable`,
756
+ );
757
+ }
758
+ if (expectedPlacement?.kind === "attached_device") {
759
+ waitSignal.throwIfAborted();
760
+ const device = await getAttachedBrowserDevice(deps.db, {
761
+ accountId: grant.accountId,
762
+ workspaceId: sourceSession.workspaceId,
763
+ deviceId: expectedPlacement.deviceId,
764
+ });
765
+ if (device.state !== "connected") {
766
+ throw new ComputerSessionStateError("Attached browser machine is disconnected");
767
+ }
768
+ const enrollment = await getEnrollment(
769
+ deps.db,
770
+ sourceSession.workspaceId,
771
+ device.enrollmentId,
772
+ );
773
+ if (!enrollment || enrollment.status !== "active") {
774
+ throw new ComputerSessionStateError("Attached browser machine is unavailable");
775
+ }
776
+ assertPlacementInstance(expectedPlacementInstanceId, device.connectionGeneration);
777
+ const built = await buildSelfhostedBackendSession({
778
+ workspaceId: sourceSession.workspaceId,
779
+ agentId: device.enrollmentId,
780
+ relay: relayConfigFromSettings(deps.settings),
781
+ controlRpcFactory: () => new NatsControlRpc(async () => deps.bus.getRequestConnection()),
782
+ epoch: 0,
783
+ timeoutMs: deps.settings.sandboxSelfhostedControlTimeoutMs,
784
+ execTimeoutMs: deps.settings.sandboxSelfhostedExecTimeoutMs,
785
+ ...(deps.settings.agentOpStreamEnabled === true &&
786
+ enrollment.opStream === true &&
787
+ deps.bus.getOpStreamConnection
788
+ ? {
789
+ opStream: {
790
+ transport: new NatsOpStreamTransport(
791
+ async () => deps.bus.getOpStreamConnection?.() ?? null,
792
+ ),
793
+ },
794
+ }
795
+ : {}),
796
+ });
797
+ waitSignal.throwIfAborted();
798
+ return await callback({
799
+ placement: expectedPlacement,
800
+ placementInstanceId: device.connectionGeneration,
801
+ session: built.session as unknown as BrowserControlPlacementSession,
802
+ lease: null,
803
+ });
804
+ }
805
+ const runWithChannelA =
806
+ operation === "computer.read" ||
807
+ operation === "computer.action" ||
808
+ operation === "computer.control" ||
809
+ operation === "computer.attach"
810
+ ? withChannelARead
811
+ : withChannelA;
812
+ return await runWithChannelA(
813
+ channelServices,
814
+ {
815
+ accountId: grant.accountId,
816
+ workspaceId: sourceSession.workspaceId,
817
+ session: sourceSession,
818
+ subjectId: grant.subjectId,
819
+ waitSignal,
820
+ operation,
821
+ retryControllerTransport: operation === "computer.read" || operation === "computer.action",
822
+ },
823
+ async (handle) => {
824
+ if (expectedPlacement?.kind === "sandbox_group") {
825
+ if (
826
+ sourceSession.sandboxGroupId !== expectedPlacement.sandboxGroupId ||
827
+ !handle.lease?.instanceId
828
+ ) {
829
+ throw new ComputerSessionStateError("ComputerSession home placement is unavailable");
830
+ }
831
+ assertPlacementInstance(expectedPlacementInstanceId, handle.lease.instanceId);
832
+ return await callback({
833
+ placement: expectedPlacement,
834
+ placementInstanceId: handle.lease.instanceId,
835
+ session: handle.homeSession,
836
+ lease: handle.lease,
837
+ });
838
+ }
839
+
840
+ const resolved = await handle.routingSession.prime();
841
+ if (expectedPlacement?.kind === "connected_machine") {
842
+ if (
843
+ resolved.kind !== "selfhosted" ||
844
+ resolved.sandboxId !== expectedPlacement.sandboxId
845
+ ) {
846
+ throw new ComputerSessionStateError(
847
+ "ComputerSession Connected Machine is not the active placement",
848
+ );
849
+ }
850
+ const placementInstanceId = resolved.providerInstanceId ?? expectedPlacement.sandboxId;
851
+ assertPlacementInstance(expectedPlacementInstanceId, placementInstanceId);
852
+ return await callback({
853
+ placement: expectedPlacement,
854
+ placementInstanceId,
855
+ session: resolved.session as unknown as BrowserControlPlacementSession,
856
+ lease: null,
857
+ });
858
+ }
859
+ if (resolved.sandboxId === null) {
860
+ if (!handle.lease?.instanceId) {
861
+ throw new ComputerSessionStateError("ComputerSession home placement is unavailable");
862
+ }
863
+ if (
864
+ resolved.providerInstanceId &&
865
+ resolved.providerInstanceId !== handle.lease.instanceId
866
+ ) {
867
+ throw new ComputerSessionStateError("ComputerSession home placement fence changed");
868
+ }
869
+ return await callback({
870
+ placement: {
871
+ kind: "sandbox_group",
872
+ sandboxGroupId: sourceSession.sandboxGroupId,
873
+ },
874
+ placementInstanceId: handle.lease.instanceId,
875
+ session: handle.homeSession,
876
+ lease: handle.lease,
877
+ });
878
+ }
879
+ if (resolved.kind === "selfhosted") {
880
+ return await callback({
881
+ placement: { kind: "connected_machine", sandboxId: resolved.sandboxId },
882
+ placementInstanceId: resolved.providerInstanceId ?? resolved.sandboxId,
883
+ session: resolved.session as unknown as BrowserControlPlacementSession,
884
+ lease: null,
885
+ });
886
+ }
887
+ throw new BrowserControlUnsupportedError(
888
+ "computer creation on a non-home provider sandbox is not supported",
889
+ );
890
+ },
891
+ );
892
+ }
893
+
894
+ function controllerOnlySession(url: string): BrowserControlPlacementSession {
895
+ const endpoint = exposedPortEndpointFromUrl(url);
896
+ return {
897
+ resolveExposedPort: async (port: number) => {
898
+ if (port !== BROWSER_CONTROL_PORT) {
899
+ throw new BrowserControlUnsupportedError(`cached controller cannot expose port ${port}`);
900
+ }
901
+ return endpoint;
902
+ },
903
+ };
904
+ }
905
+
906
+ async function cachedComputerPlacement(
907
+ record: ComputerSessionControlRecord,
908
+ ): Promise<ComputerPlacement | null> {
909
+ const binding = record.session.controller;
910
+ if (!binding || record.session.placement.kind !== "sandbox_group") return null;
911
+ const sandboxGroupId = record.session.placement.sandboxGroupId;
912
+ const lease = await readLease(deps.db, record.session.workspaceId, sandboxGroupId);
913
+ if (
914
+ !lease ||
915
+ (lease.liveness !== "warm" && lease.liveness !== "draining") ||
916
+ lease.instanceId !== binding.placementInstanceId ||
917
+ !lease.controllerDataPlaneUrl
918
+ ) {
919
+ return null;
920
+ }
921
+ return {
922
+ placement: record.session.placement,
923
+ placementInstanceId: binding.placementInstanceId,
924
+ session: controllerOnlySession(lease.controllerDataPlaneUrl),
925
+ lease,
926
+ };
927
+ }
928
+
929
+ async function cacheComputerControllerPlacement(
930
+ grant: AccessGrant,
931
+ workspaceId: string,
932
+ placement: ComputerPlacement,
933
+ ): Promise<ComputerPlacement> {
934
+ if (
935
+ placement.placement.kind !== "sandbox_group" ||
936
+ !placement.lease?.instanceId ||
937
+ !placement.session.resolveExposedPort
938
+ ) {
939
+ return placement;
940
+ }
941
+ const endpoint = await placement.session.resolveExposedPort(BROWSER_CONTROL_PORT);
942
+ const url = buildStreamUrl(endpoint);
943
+ const lease = await recordLeaseControllerDataPlaneUrl(deps.db, {
944
+ accountId: grant.accountId,
945
+ workspaceId,
946
+ sandboxGroupId: placement.placement.sandboxGroupId,
947
+ expectedEpoch: placement.lease.leaseEpoch,
948
+ expectedInstanceId: placement.lease.instanceId,
949
+ controllerDataPlaneUrl: url,
950
+ });
951
+ if (!lease) return placement;
952
+ return { ...placement, session: controllerOnlySession(url), lease };
953
+ }
954
+
955
+ async function withComputerCreationController<T>(
956
+ grant: AccessGrant,
957
+ workspaceId: string,
958
+ placement: ComputerPlacement,
959
+ adminToken: string,
960
+ origin: string | null,
961
+ operation: (client: BrowserControlClient) => Promise<T>,
962
+ ): Promise<T> {
963
+ const cachedUrl = placement.lease?.controllerDataPlaneUrl;
964
+ const sandboxGroupId =
965
+ placement.placement.kind === "sandbox_group" ? placement.placement.sandboxGroupId : null;
966
+ return await withCachedController({
967
+ cachedUrl: sandboxGroupId ? (cachedUrl ?? null) : null,
968
+ createCachedClient: (url) =>
969
+ new BrowserControlClient(controllerOnlySession(url), { adminToken }),
970
+ prepareCachedClient: async (client) => {
971
+ if (origin) await client.addAllowedOrigins([origin]);
972
+ },
973
+ invalidateCachedUrl: async () => {
974
+ if (!sandboxGroupId || !placement.lease) return;
975
+ await recordLeaseControllerDataPlaneUrl(deps.db, {
976
+ accountId: grant.accountId,
977
+ workspaceId,
978
+ sandboxGroupId,
979
+ expectedEpoch: placement.lease.leaseEpoch,
980
+ expectedInstanceId: placement.placementInstanceId,
981
+ controllerDataPlaneUrl: null,
982
+ });
983
+ },
984
+ provisionClient: async () => {
985
+ const { client } = await provisionBrowserControlClient(placement.session, {
986
+ adminToken,
987
+ nativeAuthority: nativeControllerAuthority(workspaceId, placement),
988
+ ...(origin ? { allowedOrigins: [origin] } : {}),
989
+ });
990
+ return client;
991
+ },
992
+ use: operation,
993
+ });
994
+ }
995
+
996
+ async function withActiveComputerController<T>(
997
+ context: Context,
998
+ grant: AccessGrant,
999
+ workspaceId: string,
1000
+ computerSessionId: string,
1001
+ authorizationOperation: SessionAuthorizationOperation,
1002
+ channelOperation: ChannelAOperation,
1003
+ callback: (input: {
1004
+ client: BrowserControlClient;
1005
+ sessionClient: ReturnType<BrowserControlClient["computerSessionClient"]>;
1006
+ record: ComputerSessionControlRecord;
1007
+ binding: NonNullable<ComputerSessionControlRecord["session"]["controller"]>;
1008
+ placement: ComputerPlacement;
1009
+ }) => Promise<T>,
1010
+ recoverMissing = true,
1011
+ ): Promise<T> {
1012
+ try {
1013
+ const record = await getComputerSessionControlRecord(deps.db, {
1014
+ accountId: grant.accountId,
1015
+ workspaceId,
1016
+ computerSessionId,
1017
+ });
1018
+ await authorizeSourceSession(deps, grant, record.sourceSessionId, authorizationOperation);
1019
+ if (record.session.lifecycle !== "active" || !record.session.controller) {
1020
+ throw new ComputerSessionStateError("ComputerSession is not active");
1021
+ }
1022
+ const binding = record.session.controller;
1023
+ const admitted = await touchComputerSessionController(deps.db, {
1024
+ accountId: grant.accountId,
1025
+ workspaceId,
1026
+ computerSessionId,
1027
+ controllerGeneration: binding.controllerGeneration,
1028
+ });
1029
+ if (!admitted) {
1030
+ throw new ComputerSessionStateError("ComputerSession controller authority changed");
1031
+ }
1032
+ const run = async (placement: ComputerPlacement): Promise<T> => {
1033
+ // The active controller binding is the provisioning fence. Live
1034
+ // input connects to it directly instead of re-ensuring the sidecar.
1035
+ const client = connectController(grant, record, placement);
1036
+ const tokens = deriveComputerSessionControllerTokens({
1037
+ rootSecret: controllerAuthorityRoot(deps),
1038
+ accountId: grant.accountId,
1039
+ workspaceId,
1040
+ placement: record.session.placement,
1041
+ placementInstanceId: placement.placementInstanceId,
1042
+ computerSessionId,
1043
+ controllerGeneration: binding.controllerGeneration,
1044
+ tokenGeneration: record.tokenGeneration,
1045
+ });
1046
+ const controller = {
1047
+ client,
1048
+ sessionClient: client.computerSessionClient({
1049
+ reference: {
1050
+ computerSessionId,
1051
+ controllerGeneration: binding.controllerGeneration,
1052
+ },
1053
+ ...tokens,
1054
+ }),
1055
+ record,
1056
+ binding,
1057
+ placement,
1058
+ };
1059
+ let result: T;
1060
+ try {
1061
+ result = await callback(controller);
1062
+ } catch (error) {
1063
+ if (!recoverMissing || !isMissingComputerControllerSession(error)) throw error;
1064
+ await client.createComputerSession({
1065
+ computerSessionId,
1066
+ controllerGeneration: binding.controllerGeneration,
1067
+ tokenGeneration: record.tokenGeneration,
1068
+ ...tokens,
1069
+ });
1070
+ result = await callback(controller);
1071
+ }
1072
+ return result;
1073
+ };
1074
+ const cached = await cachedComputerPlacement(record);
1075
+ if (cached) {
1076
+ try {
1077
+ return await run(cached);
1078
+ } catch (error) {
1079
+ const safelyReplayable =
1080
+ channelOperation === "computer.read" || channelOperation === "computer.action";
1081
+ if (
1082
+ !safelyReplayable ||
1083
+ (!(error instanceof BrowserControlTransportError) &&
1084
+ !(error instanceof BrowserControlRequestError && error.retryable))
1085
+ ) {
1086
+ throw error;
1087
+ }
1088
+ await recordLeaseControllerDataPlaneUrl(deps.db, {
1089
+ accountId: grant.accountId,
1090
+ workspaceId,
1091
+ sandboxGroupId: cached.lease!.sandboxGroupId,
1092
+ expectedEpoch: cached.lease!.leaseEpoch,
1093
+ expectedInstanceId: cached.placementInstanceId,
1094
+ controllerDataPlaneUrl: null,
1095
+ }).catch(() => null);
1096
+ }
1097
+ }
1098
+
1099
+ const sourceSession = await requireSourceSession(deps, workspaceId, record.sourceSessionId);
1100
+ return await withComputerPlacement(
1101
+ sourceSession,
1102
+ grant,
1103
+ record.session.placement,
1104
+ binding.placementInstanceId,
1105
+ channelOperation,
1106
+ context.req.raw.signal,
1107
+ async (placement) =>
1108
+ await run(
1109
+ await cacheComputerControllerPlacement(grant, workspaceId, placement).catch(
1110
+ () => placement,
1111
+ ),
1112
+ ),
1113
+ );
1114
+ } catch (error) {
1115
+ throw computerRouteError(error);
1116
+ }
1117
+ }
1118
+
1119
+ async function ensureInteractionHolder(
1120
+ grant: AccessGrant,
1121
+ sourceSession: Session,
1122
+ computerSessionId: string,
1123
+ placement: ComputerPlacement,
1124
+ waitSignal: AbortSignal,
1125
+ ): Promise<boolean> {
1126
+ if (placement.placement.kind !== "sandbox_group") return false;
1127
+ if (!placement.lease?.instanceId) {
1128
+ throw new ComputerSessionStateError("ComputerSession lease placement is unavailable");
1129
+ }
1130
+ const acquired = await acquireLease(deps.db, {
1131
+ accountId: grant.accountId,
1132
+ workspaceId: sourceSession.workspaceId,
1133
+ sandboxGroupId: placement.placement.sandboxGroupId,
1134
+ kind: "interaction",
1135
+ holderId: interactionHolderId(computerSessionId),
1136
+ subjectId: sourceSession.id,
1137
+ backend: placement.lease.backend,
1138
+ os: placement.lease.os,
1139
+ image: placement.lease.image,
1140
+ rigVersionId: placement.lease.rigVersionId,
1141
+ leaseTtlMs: deps.settings.sandboxLeaseTtlMs,
1142
+ expectedEpoch: placement.lease.leaseEpoch,
1143
+ waitSignal,
1144
+ });
1145
+ if (acquired.role === "blocked" || acquired.role === "fenced") {
1146
+ throw new ComputerSessionStateError("ComputerSession placement is transitioning; retry");
1147
+ }
1148
+ if (
1149
+ acquired.role === "spawner" ||
1150
+ acquired.lease.leaseEpoch !== placement.lease.leaseEpoch ||
1151
+ acquired.lease.instanceId !== placement.placementInstanceId
1152
+ ) {
1153
+ await releaseInteractionHolder(
1154
+ grant,
1155
+ sourceSession.workspaceId,
1156
+ computerSessionId,
1157
+ placement.placement,
1158
+ ).catch(() => undefined);
1159
+ throw new ComputerSessionStateError("ComputerSession placement fence changed; retry");
1160
+ }
1161
+ return true;
1162
+ }
1163
+
1164
+ async function ensureDispatchedGeneration(
1165
+ grant: AccessGrant,
1166
+ workspaceId: string,
1167
+ computerSessionId: string,
1168
+ operationId: string,
1169
+ placementInstanceId: string,
1170
+ ): Promise<ComputerSessionControlRecord> {
1171
+ let record = await getComputerSessionControlRecord(deps.db, {
1172
+ accountId: grant.accountId,
1173
+ workspaceId,
1174
+ computerSessionId,
1175
+ operationId,
1176
+ });
1177
+ if (record.operation?.state === "prepared") {
1178
+ const controllerGeneration = randomUUID();
1179
+ try {
1180
+ await dispatchComputerSessionOperation(deps.db, {
1181
+ accountId: grant.accountId,
1182
+ workspaceId,
1183
+ operationId,
1184
+ computerSessionId,
1185
+ controllerGeneration,
1186
+ controller: {
1187
+ controllerId: "opengeni-browserd",
1188
+ controllerGeneration,
1189
+ placementInstanceId,
1190
+ },
1191
+ });
1192
+ } catch (error) {
1193
+ if (!(error instanceof ComputerSessionOperationConflictError)) throw error;
1194
+ }
1195
+ record = await getComputerSessionControlRecord(deps.db, {
1196
+ accountId: grant.accountId,
1197
+ workspaceId,
1198
+ computerSessionId,
1199
+ operationId,
1200
+ });
1201
+ }
1202
+ if (record.operation?.state !== "dispatched") {
1203
+ throw new ComputerSessionStateError("ComputerSession create operation is not dispatchable");
1204
+ }
1205
+ if (
1206
+ !record.session.controller ||
1207
+ record.session.controller.controllerGeneration !== record.operation.controllerGeneration ||
1208
+ record.session.controller.placementInstanceId !== placementInstanceId
1209
+ ) {
1210
+ throw new ComputerSessionOperationConflictError(
1211
+ "ComputerSession dispatch controller binding is inconsistent",
1212
+ );
1213
+ }
1214
+ return record;
1215
+ }
1216
+
1217
+ async function provisionController(
1218
+ grant: AccessGrant,
1219
+ record: ComputerSessionControlRecord,
1220
+ placement: ComputerPlacement,
1221
+ origin: string | null,
1222
+ ): Promise<BrowserControlClient> {
1223
+ const adminToken = deriveBrowserControllerAdminToken({
1224
+ rootSecret: controllerAuthorityRoot(deps),
1225
+ accountId: grant.accountId,
1226
+ workspaceId: record.session.workspaceId,
1227
+ placement: record.session.placement,
1228
+ placementInstanceId: placement.placementInstanceId,
1229
+ });
1230
+ return (
1231
+ await provisionBrowserControlClient(placement.session, {
1232
+ adminToken,
1233
+ nativeAuthority: nativeControllerAuthority(record.session.workspaceId, placement),
1234
+ ...(origin ? { allowedOrigins: [origin] } : {}),
1235
+ })
1236
+ ).client;
1237
+ }
1238
+
1239
+ function connectController(
1240
+ grant: AccessGrant,
1241
+ record: ComputerSessionControlRecord,
1242
+ placement: ComputerPlacement,
1243
+ ): BrowserControlClient {
1244
+ return new BrowserControlClient(placement.session, {
1245
+ adminToken: deriveBrowserControllerAdminToken({
1246
+ rootSecret: controllerAuthorityRoot(deps),
1247
+ accountId: grant.accountId,
1248
+ workspaceId: record.session.workspaceId,
1249
+ placement: record.session.placement,
1250
+ placementInstanceId: placement.placementInstanceId,
1251
+ }),
1252
+ nativeAuthority: nativeControllerAuthority(record.session.workspaceId, placement),
1253
+ });
1254
+ }
1255
+
1256
+ async function releaseInteractionHolder(
1257
+ grant: AccessGrant,
1258
+ workspaceId: string,
1259
+ computerSessionId: string,
1260
+ placement: InteractionPlacement,
1261
+ ): Promise<void> {
1262
+ if (placement.kind !== "sandbox_group") return;
1263
+ await releaseLeaseHolder(deps.db, {
1264
+ accountId: grant.accountId,
1265
+ workspaceId,
1266
+ sandboxGroupId: placement.sandboxGroupId,
1267
+ kind: "interaction",
1268
+ holderId: interactionHolderId(computerSessionId),
1269
+ idleGraceMs: deps.settings.sandboxIdleGraceMs,
1270
+ });
1271
+ }
1272
+
1273
+ async function routePreamble(
1274
+ context: Context,
1275
+ permission: "sessions:read" | "sessions:control" | "stream:view",
1276
+ ): Promise<{ workspaceId: string; grant: AccessGrant; computerSessionId: string }> {
1277
+ const workspaceId = context.req.param("workspaceId") ?? "";
1278
+ const grant = await requireAccessGrant(context, deps, workspaceId, permission);
1279
+ return {
1280
+ workspaceId,
1281
+ grant,
1282
+ computerSessionId: requireUuidParam(context, "computerSessionId"),
1283
+ };
1284
+ }
1285
+ }
1286
+
1287
+ function computerCreateInput(
1288
+ grant: AccessGrant,
1289
+ workspaceId: string,
1290
+ request: CreateComputerSessionRequestValue,
1291
+ placement: InteractionPlacement,
1292
+ ) {
1293
+ return {
1294
+ accountId: grant.accountId,
1295
+ workspaceId,
1296
+ operationId: request.operationId,
1297
+ associatedSessionId: request.sessionId,
1298
+ actorSubjectId: grant.subjectId,
1299
+ name: request.name ?? "Computer",
1300
+ placement,
1301
+ };
1302
+ }
1303
+
1304
+ function assertCreateReplay(
1305
+ request: CreateComputerSessionRequestValue,
1306
+ session: ComputerSessionValue,
1307
+ ): void {
1308
+ if (request.placement && !sameInteractionPlacement(request.placement, session.placement)) {
1309
+ throw new ComputerSessionOperationConflictError(
1310
+ "ComputerSession create operation is bound to another placement",
1311
+ );
1312
+ }
1313
+ }
1314
+
1315
+ function sameInteractionPlacement(
1316
+ left: InteractionPlacement,
1317
+ right: InteractionPlacement,
1318
+ ): boolean {
1319
+ if (left.kind !== right.kind) return false;
1320
+ switch (left.kind) {
1321
+ case "sandbox_group":
1322
+ return right.kind === "sandbox_group" && left.sandboxGroupId === right.sandboxGroupId;
1323
+ case "connected_machine":
1324
+ return right.kind === "connected_machine" && left.sandboxId === right.sandboxId;
1325
+ case "attached_device":
1326
+ return right.kind === "attached_device" && left.deviceId === right.deviceId;
1327
+ case "external_provider":
1328
+ return (
1329
+ right.kind === "external_provider" &&
1330
+ left.providerId === right.providerId &&
1331
+ left.placementId === right.placementId
1332
+ );
1333
+ }
1334
+ }
1335
+
1336
+ function requireOperationGeneration(record: ComputerSessionControlRecord): string {
1337
+ const generation = record.operation?.controllerGeneration;
1338
+ if (!generation) {
1339
+ throw new ComputerSessionStateError("ComputerSession controller fence is absent");
1340
+ }
1341
+ return generation;
1342
+ }
1343
+
1344
+ function nativeControllerAuthority(
1345
+ workspaceId: string,
1346
+ placement: ComputerPlacement,
1347
+ ): { scopeId: string; scopeGeneration: string } {
1348
+ const placementId =
1349
+ placement.placement.kind === "connected_machine"
1350
+ ? placement.placement.sandboxId
1351
+ : placement.placement.kind === "attached_device"
1352
+ ? placement.placement.deviceId
1353
+ : placement.placement.kind === "sandbox_group"
1354
+ ? placement.placement.sandboxGroupId
1355
+ : null;
1356
+ if (!placementId) {
1357
+ throw new BrowserControlUnsupportedError("computer controller placement is unsupported");
1358
+ }
1359
+ return {
1360
+ scopeId: `${workspaceId}:${placement.placement.kind}:${placementId}`,
1361
+ scopeGeneration: placement.placementInstanceId,
1362
+ };
1363
+ }
1364
+
1365
+ function interactionHolderId(computerSessionId: string): string {
1366
+ return `computer-session:${computerSessionId}`;
1367
+ }
1368
+
1369
+ function controllerAuthorityRoot(deps: ApiRouteDeps): string {
1370
+ const root = resolveFirstPartyDelegationSecret(deps.settings);
1371
+ if (!root) {
1372
+ throw new HTTPException(503, {
1373
+ message: "interaction controller authority is not configured",
1374
+ });
1375
+ }
1376
+ return root;
1377
+ }
1378
+
1379
+ async function requireSourceSession(
1380
+ deps: ApiRouteDeps,
1381
+ workspaceId: string,
1382
+ sessionId: string,
1383
+ ): Promise<Session> {
1384
+ const session = await getSession(deps.db, workspaceId, sessionId);
1385
+ if (!session) throw new ComputerSessionNotFoundError("Associated session not found");
1386
+ return session;
1387
+ }
1388
+
1389
+ async function authorizeSourceSession(
1390
+ deps: ApiRouteDeps,
1391
+ grant: AccessGrant,
1392
+ sessionId: string,
1393
+ operation: SessionAuthorizationOperation,
1394
+ ): Promise<void> {
1395
+ try {
1396
+ await requireSessionAuthorization(deps, grant, {
1397
+ sessionId,
1398
+ operation,
1399
+ surface: "http",
1400
+ });
1401
+ } catch (error) {
1402
+ if (error instanceof SessionAuthorizationDeniedError) {
1403
+ throw new HTTPException(404, { message: "session not found", cause: error });
1404
+ }
1405
+ if (error instanceof SessionAuthorizationUnavailableError) {
1406
+ throw new HTTPException(503, {
1407
+ message: "session authorization is unavailable",
1408
+ cause: error,
1409
+ });
1410
+ }
1411
+ throw error;
1412
+ }
1413
+ }
1414
+
1415
+ async function parseJsonBody<T>(
1416
+ context: Context,
1417
+ schema: {
1418
+ safeParse(value: unknown): { success: true; data: T } | { success: false };
1419
+ },
1420
+ ): Promise<T> {
1421
+ const value = await context.req.json().catch(() => undefined);
1422
+ const parsed = schema.safeParse(value);
1423
+ if (!parsed.success) throw new HTTPException(400, { message: "invalid request body" });
1424
+ return parsed.data;
1425
+ }
1426
+
1427
+ async function parseEmptyJsonBody(context: Context): Promise<void> {
1428
+ const text = await context.req.text();
1429
+ if (!text.trim()) return;
1430
+ try {
1431
+ const value = JSON.parse(text) as unknown;
1432
+ if (
1433
+ typeof value !== "object" ||
1434
+ value === null ||
1435
+ Array.isArray(value) ||
1436
+ Object.keys(value).length !== 0
1437
+ ) {
1438
+ throw new Error("not empty");
1439
+ }
1440
+ } catch {
1441
+ throw new HTTPException(400, { message: "invalid request body" });
1442
+ }
1443
+ }
1444
+
1445
+ function requireUuidParam(context: Context, name: string): string {
1446
+ const value = context.req.param(name) ?? "";
1447
+ if (!isUuid(value)) throw new HTTPException(404, { message: "ComputerSession not found" });
1448
+ return value;
1449
+ }
1450
+
1451
+ function requireOpaqueParam(context: Context, name: string): string {
1452
+ const value = context.req.param(name) ?? "";
1453
+ if (value.length < 1 || value.length > 512 || /[\u0000-\u001f\u007f]/u.test(value)) {
1454
+ throw new HTTPException(404, { message: "computer target not found" });
1455
+ }
1456
+ return value;
1457
+ }
1458
+
1459
+ function requestOrigin(context: Context, allowedPattern: string): string | null {
1460
+ const value = context.req.header("origin");
1461
+ if (!value) return null;
1462
+ let url: URL;
1463
+ try {
1464
+ url = new URL(value);
1465
+ } catch {
1466
+ throw new HTTPException(400, { message: "invalid request origin" });
1467
+ }
1468
+ if (
1469
+ url.origin === "null" ||
1470
+ (url.protocol !== "http:" && url.protocol !== "https:") ||
1471
+ url.origin !== value
1472
+ ) {
1473
+ throw new HTTPException(400, { message: "invalid request origin" });
1474
+ }
1475
+ if (!allowedCorsOrigin(allowedPattern, url.origin)) {
1476
+ throw new HTTPException(403, { message: "request origin is not allowed" });
1477
+ }
1478
+ return url.origin;
1479
+ }
1480
+
1481
+ function interactionActorForGrant(grant: AccessGrant): ReturnType<typeof InteractionActor.parse> {
1482
+ if (grant.principalKind !== "agent_attempt") {
1483
+ return InteractionActor.parse({
1484
+ kind: grant.principalKind === "service" ? "system" : "human",
1485
+ subjectId: grant.subjectId,
1486
+ });
1487
+ }
1488
+ return InteractionActor.parse({
1489
+ kind: "agent",
1490
+ subjectId: grant.subjectId,
1491
+ sessionId: grant.metadata?.["sessionId"],
1492
+ turnId: grant.metadata?.["turnId"],
1493
+ attemptId: grant.metadata?.["attemptId"],
1494
+ executionGeneration: grant.metadata?.["executionGeneration"],
1495
+ });
1496
+ }
1497
+
1498
+ function assertPlacementInstance(expected: string | null, actual: string): void {
1499
+ if (expected !== null && expected !== actual) {
1500
+ throw new ComputerSessionStateError("ComputerSession placement instance changed");
1501
+ }
1502
+ }
1503
+
1504
+ function isTerminalOperation(state: string): boolean {
1505
+ return state === "completed" || state === "failed" || state === "outcome_unknown";
1506
+ }
1507
+
1508
+ function isMissingComputerControllerSession(error: unknown): boolean {
1509
+ if (!(error instanceof BrowserControlRequestError)) return false;
1510
+ if (error.status === 401 && error.error.code === "permission_denied") return true;
1511
+ return (
1512
+ error.status === 404 &&
1513
+ error.error.code === "resource_not_found" &&
1514
+ ["computer session not found", "computer session is not active"].includes(error.error.message)
1515
+ );
1516
+ }
1517
+
1518
+ function interactionFailure(error: unknown) {
1519
+ if (error instanceof BrowserControlRequestError) return error.error;
1520
+ if (
1521
+ error instanceof BrowserControlUnsupportedError ||
1522
+ error instanceof BrowserControlServerUnsupportedError
1523
+ ) {
1524
+ return { code: "unsupported" as const, message: error.message, retryable: false };
1525
+ }
1526
+ if (error instanceof BrowserControlServerError) {
1527
+ return {
1528
+ code:
1529
+ error.stage === "engine_unavailable"
1530
+ ? ("unsupported" as const)
1531
+ : ("driver_failed" as const),
1532
+ message: error.message,
1533
+ retryable: error.stage !== "engine_unavailable",
1534
+ };
1535
+ }
1536
+ return {
1537
+ code: "driver_failed" as const,
1538
+ message: error instanceof Error ? error.message : "computer controller failed",
1539
+ retryable: false,
1540
+ };
1541
+ }
1542
+
1543
+ function computerRouteError(error: unknown): HTTPException {
1544
+ if (error instanceof HTTPException) return error;
1545
+ if (error instanceof ComputerSessionNotFoundError) {
1546
+ return new HTTPException(404, { message: error.message, cause: error });
1547
+ }
1548
+ if (
1549
+ error instanceof ComputerSessionOperationConflictError ||
1550
+ error instanceof ComputerSessionStateError
1551
+ ) {
1552
+ return new HTTPException(409, { message: error.message, cause: error });
1553
+ }
1554
+ if (error instanceof BrowserControlRequestError) {
1555
+ return new HTTPException(error.status as ContentfulStatusCode, {
1556
+ message: error.error.message,
1557
+ cause: error,
1558
+ });
1559
+ }
1560
+ if (error instanceof BrowserControlTransportError) {
1561
+ return new HTTPException(503, {
1562
+ message: "computer controller is unavailable",
1563
+ cause: error,
1564
+ });
1565
+ }
1566
+ if (error instanceof BrowserControlProtocolError) {
1567
+ return new HTTPException(502, {
1568
+ message: "computer controller response is invalid",
1569
+ cause: error,
1570
+ });
1571
+ }
1572
+ if (
1573
+ error instanceof BrowserControlUnsupportedError ||
1574
+ error instanceof BrowserControlServerUnsupportedError
1575
+ ) {
1576
+ return new HTTPException(409, { message: error.message, cause: error });
1577
+ }
1578
+ if (error instanceof BrowserControlServerError) {
1579
+ return new HTTPException(error.stage === "engine_unavailable" ? 409 : 503, {
1580
+ message:
1581
+ error.stage === "engine_unavailable"
1582
+ ? "computer adapter is unavailable on this placement"
1583
+ : "computer controller could not start",
1584
+ cause: error,
1585
+ });
1586
+ }
1587
+ return new HTTPException(500, {
1588
+ message: "ComputerSession request failed",
1589
+ cause: error,
1590
+ });
1591
+ }
1592
+
1593
+ function isUuid(value: unknown): value is string {
1594
+ return (
1595
+ typeof value === "string" &&
1596
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(value)
1597
+ );
1598
+ }
1599
+
1600
+ function isAbort(error: unknown): boolean {
1601
+ return error instanceof Error && error.name === "AbortError";
1602
+ }