@opengeni/api-router 0.26.1 → 0.30.1

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 (87) hide show
  1. package/dist/app.d.ts +1 -0
  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 +19 -1
  6. package/dist/browser-network-route.d.ts +8 -0
  7. package/dist/{chunk-JIKNR5YL.js → chunk-2PQMSRCB.js} +15518 -6863
  8. package/dist/chunk-2PQMSRCB.js.map +1 -0
  9. package/dist/controller-data-plane.d.ts +12 -0
  10. package/dist/http/auth.d.ts +1 -1
  11. package/dist/http/sse.d.ts +18 -1
  12. package/dist/index.js +1 -1
  13. package/dist/integrations/api-integrations.d.ts +2 -2
  14. package/dist/integrations/fiken.d.ts +233 -0
  15. package/dist/integrations/google-drive.d.ts +6 -6
  16. package/dist/integrations/provider-oauth.d.ts +0 -2
  17. package/dist/integrations/slack-bot.d.ts +171 -1
  18. package/dist/integrations/slack-interactions.d.ts +17 -5
  19. package/dist/interaction-holder-heartbeat.d.ts +17 -0
  20. package/dist/interaction-metrics.d.ts +11 -0
  21. package/dist/mcp/server.d.ts +3 -2
  22. package/dist/model-catalog.d.ts +1 -0
  23. package/dist/routes/canonical-human-identities.d.ts +3 -0
  24. package/dist/routes/channels.d.ts +3 -0
  25. package/dist/routes/integration-facets.d.ts +3 -0
  26. package/dist/routes/interaction-resources.d.ts +5 -0
  27. package/dist/routes/organization-memberships.d.ts +3 -0
  28. package/dist/routes/sessions.d.ts +2 -1
  29. package/dist/routes/slack-task-policy.d.ts +3 -0
  30. package/dist/routes/supergrok.d.ts +3 -0
  31. package/dist/sandbox/channel-a.d.ts +8 -1
  32. package/dist/sandbox/machines.d.ts +2 -2
  33. package/dist/sandbox/viewer.d.ts +3 -1
  34. package/dist/slack-reaction-files.d.ts +27 -0
  35. package/dist/transcription/providers/xai-subscription.d.ts +8 -0
  36. package/dist/xai-realtime.d.ts +26 -0
  37. package/dist/xai-subscription-auth.d.ts +23 -0
  38. package/package.json +17 -16
  39. package/src/app.ts +352 -49
  40. package/src/auth/canonical-human-session-admission.ts +116 -0
  41. package/src/auth/managed-auth.ts +101 -0
  42. package/src/browser-auth-broker.ts +155 -0
  43. package/src/browser-controller-authority.ts +50 -1
  44. package/src/browser-network-route.ts +34 -0
  45. package/src/controller-data-plane.ts +35 -0
  46. package/src/http/auth.ts +27 -9
  47. package/src/http/sse.ts +229 -5
  48. package/src/integrations/api-integrations.ts +57 -42
  49. package/src/integrations/fiken.ts +1230 -0
  50. package/src/integrations/google-drive.ts +31 -26
  51. package/src/integrations/provider-oauth.ts +70 -91
  52. package/src/integrations/slack-bot.ts +638 -9
  53. package/src/integrations/slack-interactions.ts +1583 -73
  54. package/src/interaction-holder-heartbeat.ts +95 -0
  55. package/src/interaction-metrics.ts +159 -0
  56. package/src/mcp/documents.ts +115 -6
  57. package/src/mcp/server.ts +865 -242
  58. package/src/model-catalog.ts +19 -6
  59. package/src/routes/api-integrations.ts +32 -29
  60. package/src/routes/browser-identities.ts +24 -0
  61. package/src/routes/browser-sessions.ts +2122 -183
  62. package/src/routes/canonical-human-identities.ts +156 -0
  63. package/src/routes/channels.ts +90 -0
  64. package/src/routes/computer-sessions.ts +441 -86
  65. package/src/routes/connections.ts +141 -4
  66. package/src/routes/{integration-features.ts → integration-facets.ts} +63 -63
  67. package/src/routes/interaction-resources.ts +595 -0
  68. package/src/routes/organization-memberships.ts +53 -0
  69. package/src/routes/plugins.ts +2 -1
  70. package/src/routes/sessions.ts +419 -43
  71. package/src/routes/skills.ts +92 -10
  72. package/src/routes/slack-task-policy.ts +115 -0
  73. package/src/routes/supergrok.ts +717 -0
  74. package/src/routes/transcription-recordings.ts +9 -2
  75. package/src/routes/transcriptions.ts +2 -1
  76. package/src/routes/video-generation.ts +34 -4
  77. package/src/routes/workspaces.ts +22 -4
  78. package/src/sandbox/channel-a.ts +29 -2
  79. package/src/sandbox/machines.ts +5 -5
  80. package/src/sandbox/viewer.ts +10 -3
  81. package/src/slack-reaction-files.ts +181 -0
  82. package/src/transcription/providers/xai-subscription.ts +110 -0
  83. package/src/transcription/service.ts +17 -2
  84. package/src/xai-realtime.ts +216 -0
  85. package/src/xai-subscription-auth.ts +132 -0
  86. package/dist/chunk-JIKNR5YL.js.map +0 -1
  87. package/dist/routes/integration-features.d.ts +0 -3
@@ -2,7 +2,9 @@ import { randomUUID } from "node:crypto";
2
2
  import { resolveFirstPartyDelegationSecret, resolveStreamTokenSecret } from "@opengeni/config";
3
3
  import {
4
4
  BROWSER_CONTROL_WEBSOCKET_BEARER_PREFIX,
5
+ BROWSER_CONTROL_PORT,
5
6
  COMPUTER_CONTROL_WEBSOCKET_PROTOCOL,
7
+ COMPUTER_RFB_WEBSOCKET_PROTOCOL,
6
8
  ComputerActionCommand,
7
9
  ComputerActionRequest,
8
10
  ComputerSessionAttachment,
@@ -32,12 +34,16 @@ import {
32
34
  dispatchComputerSessionOperation,
33
35
  failComputerSessionOperation,
34
36
  findComputerSessionControlRecordByOperation,
37
+ getAttachedBrowserDevice,
35
38
  getComputerSessionControlRecord,
39
+ getEnrollment,
36
40
  getSession,
37
41
  listComputerSessions,
38
42
  prepareComputerSessionCreate,
39
43
  prepareComputerSessionEnd,
40
44
  releaseLeaseHolder,
45
+ readLease,
46
+ recordLeaseControllerDataPlaneUrl,
41
47
  touchComputerSessionController,
42
48
  type ComputerSessionControlRecord,
43
49
  type LeaseSnapshot,
@@ -45,21 +51,26 @@ import {
45
51
  import {
46
52
  requireAccessGrant,
47
53
  requireSessionAuthorization,
54
+ relayConfigFromSettings,
48
55
  SessionAuthorizationDeniedError,
49
56
  SessionAuthorizationUnavailableError,
50
57
  type ApiRouteDeps,
51
58
  } from "@opengeni/core";
52
59
  import {
60
+ BrowserControlClient,
53
61
  BrowserControlProtocolError,
54
62
  BrowserControlRequestError,
55
63
  BrowserControlServerError,
56
64
  BrowserControlServerUnsupportedError,
57
65
  BrowserControlTransportError,
58
66
  BrowserControlUnsupportedError,
67
+ buildSelfhostedBackendSession,
59
68
  buildStreamUrl,
69
+ exposedPortEndpointFromUrl,
60
70
  mintStreamToken,
71
+ NatsControlRpc,
72
+ NatsOpStreamTransport,
61
73
  provisionBrowserControlClient,
62
- type BrowserControlClient,
63
74
  type BrowserControlPlacementSession,
64
75
  } from "@opengeni/runtime/sandbox";
65
76
  import type { Context, Hono } from "hono";
@@ -70,8 +81,11 @@ import {
70
81
  deriveComputerSessionControllerTokens,
71
82
  deriveComputerViewGrantToken,
72
83
  } from "../browser-controller-authority";
84
+ import { withCachedController } from "../controller-data-plane";
85
+ import { withInteractionHolderHeartbeat } from "../interaction-holder-heartbeat";
73
86
  import { allowedCorsOrigin } from "../http/cors";
74
- import { withChannelA, type ChannelAOperation } from "../sandbox/channel-a";
87
+ import { observeComputerActionResult, observeLifecycleResult } from "../interaction-metrics";
88
+ import { withChannelA, withChannelARead, type ChannelAOperation } from "../sandbox/channel-a";
75
89
 
76
90
  type ComputerPlacement = {
77
91
  placement: InteractionPlacement;
@@ -125,6 +139,7 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
125
139
  const workspaceId = context.req.param("workspaceId") ?? "";
126
140
  const grant = await requireAccessGrant(context, deps, workspaceId, "sessions:control");
127
141
  const request = await parseJsonBody(context, CreateComputerSessionRequest);
142
+ const startedAtMs = performance.now();
128
143
  await authorizeSourceSession(deps, grant, request.sessionId, "session.control");
129
144
  const origin = requestOrigin(context, deps.settings.corsAllowOriginRegex);
130
145
  const authority = controllerAuthorityRoot(deps);
@@ -148,7 +163,9 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
148
163
  )
149
164
  : null;
150
165
  if (prepared && isTerminalOperation(prepared.operation.state)) {
151
- return context.json(ComputerSessionMutationResponse.parse(prepared), 200);
166
+ const parsed = ComputerSessionMutationResponse.parse(prepared);
167
+ observeLifecycleResult(deps.observability, startedAtMs, parsed);
168
+ return context.json(parsed, 200);
152
169
  }
153
170
 
154
171
  const sourceSession = await requireSourceSession(deps, workspaceId, request.sessionId);
@@ -181,7 +198,18 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
181
198
  prepared.session.id,
182
199
  request.operationId,
183
200
  placement.placementInstanceId,
184
- );
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;
185
213
  const controllerGeneration = requireOperationGeneration(record);
186
214
  const adminToken = deriveBrowserControllerAdminToken({
187
215
  rootSecret: authority,
@@ -202,21 +230,45 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
202
230
  });
203
231
  let physical;
204
232
  try {
205
- const { client } = await provisionBrowserControlClient(placement.session, {
233
+ physical = await withComputerCreationController(
234
+ grant,
235
+ workspaceId,
236
+ placement,
206
237
  adminToken,
207
- nativeAuthority: nativeControllerAuthority(workspaceId, placement),
208
- ...(origin ? { allowedOrigins: [origin] } : {}),
209
- });
210
- physical = await client.createComputerSession({
211
- computerSessionId: prepared.session.id,
212
- controllerGeneration,
213
- tokenGeneration: record.tokenGeneration,
214
- ...tokens,
215
- });
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
+ );
216
266
  } catch (error) {
217
267
  if (
218
268
  error instanceof BrowserControlTransportError ||
219
- (error instanceof BrowserControlRequestError && error.retryable) ||
269
+ (error instanceof BrowserControlRequestError &&
270
+ error.retryable &&
271
+ error.error.code !== "machine_locked") ||
220
272
  isAbort(error)
221
273
  ) {
222
274
  throw error;
@@ -225,7 +277,7 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
225
277
  accountId: grant.accountId,
226
278
  workspaceId,
227
279
  operationId: request.operationId,
228
- computerSessionId: prepared.session.id,
280
+ computerSessionId: preparedSession.id,
229
281
  ...(error instanceof BrowserControlProtocolError
230
282
  ? { state: "outcome_unknown" as const }
231
283
  : {}),
@@ -247,7 +299,7 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
247
299
  accountId: grant.accountId,
248
300
  workspaceId,
249
301
  operationId: request.operationId,
250
- computerSessionId: prepared.session.id,
302
+ computerSessionId: preparedSession.id,
251
303
  controller: {
252
304
  controllerId: "opengeni-browserd",
253
305
  controllerGeneration,
@@ -262,6 +314,7 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
262
314
  },
263
315
  );
264
316
  const parsed = ComputerSessionMutationResponse.parse(response);
317
+ observeLifecycleResult(deps.observability, startedAtMs, parsed);
265
318
  return context.json(
266
319
  parsed,
267
320
  parsed.operation.state === "completed" && !parsed.operation.replayed ? 201 : 200,
@@ -317,6 +370,26 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
317
370
  },
318
371
  );
319
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
+
320
393
  app.post(
321
394
  "/v1/workspaces/:workspaceId/computer-sessions/:computerSessionId/actions",
322
395
  async (context) => {
@@ -325,13 +398,14 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
325
398
  "sessions:control",
326
399
  );
327
400
  const request = await parseJsonBody(context, ComputerActionRequest);
401
+ const startedAtMs = performance.now();
328
402
  const result = await withActiveComputerController(
329
403
  context,
330
404
  grant,
331
405
  workspaceId,
332
406
  computerSessionId,
333
407
  "session.control",
334
- "computer.control",
408
+ "computer.action",
335
409
  async ({ sessionClient, binding }) =>
336
410
  await sessionClient.action(
337
411
  ComputerActionCommand.parse({
@@ -348,6 +422,7 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
348
422
  }),
349
423
  ),
350
424
  );
425
+ observeComputerActionResult(deps.observability, startedAtMs, request, result);
351
426
  return context.json(result);
352
427
  },
353
428
  );
@@ -377,6 +452,7 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
377
452
  "/v1/workspaces/:workspaceId/computer-sessions/:computerSessionId/attachments",
378
453
  async (context) => {
379
454
  const { workspaceId, grant, computerSessionId } = await routePreamble(context, "stream:view");
455
+ const origin = requestOrigin(context, deps.settings.corsAllowOriginRegex);
380
456
  const request = await parseJsonBody(context, ComputerSessionAttachmentRequest);
381
457
  const result = await withActiveComputerController(
382
458
  context,
@@ -386,7 +462,7 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
386
462
  "session.viewer.read",
387
463
  "computer.attach",
388
464
  async ({ client, sessionClient, record, binding, placement }) => {
389
- await sessionClient.observe(request.targetId);
465
+ if (origin) await client.addAllowedOrigins([origin]);
390
466
  const grantId = randomUUID();
391
467
  const expiresAt = new Date(Date.now() + request.expiresInSeconds * 1_000).toISOString();
392
468
  const token = deriveComputerViewGrantToken({
@@ -406,6 +482,16 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
406
482
  controllerGeneration: binding.controllerGeneration,
407
483
  };
408
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
+ }
409
495
  const relaySecret = placement.session.openComputerFrames
410
496
  ? resolveStreamTokenSecret(deps.settings)
411
497
  : null;
@@ -414,13 +500,23 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
414
500
  "computer frame relay authority is unavailable",
415
501
  );
416
502
  }
417
- const relayed = await client.openRelayedComputerFrameStream({
418
- reference,
419
- targetId: request.targetId,
420
- viewToken: token,
421
- expiresAt,
422
- ...(request.stream ? { stream: request.stream } : {}),
423
- });
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
+ }
424
520
  const stream = relayed
425
521
  ? await (async () => {
426
522
  const relayToken = await mintStreamToken(relaySecret!, {
@@ -444,14 +540,30 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
444
540
  },
445
541
  };
446
542
  })()
447
- : {
448
- kind: "direct_websocket" as const,
449
- url: await client.computerFrameStreamUrl(reference, request.targetId),
450
- protocols: [
451
- COMPUTER_CONTROL_WEBSOCKET_PROTOCOL,
452
- `${BROWSER_CONTROL_WEBSOCKET_BEARER_PREFIX}${token}`,
453
- ],
454
- };
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
+ };
455
567
  return ComputerSessionAttachment.parse({
456
568
  computerSessionId,
457
569
  controllerGeneration: binding.controllerGeneration,
@@ -488,6 +600,7 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
488
600
  alive: true,
489
601
  });
490
602
  },
603
+ false,
491
604
  );
492
605
  return context.json(result);
493
606
  },
@@ -500,6 +613,7 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
500
613
  const grant = await requireAccessGrant(context, deps, workspaceId, "sessions:control");
501
614
  const computerSessionId = requireUuidParam(context, "computerSessionId");
502
615
  const request = await parseJsonBody(context, ComputerSessionLifecycleRequest);
616
+ const startedAtMs = performance.now();
503
617
  const origin = requestOrigin(context, deps.settings.corsAllowOriginRegex);
504
618
  try {
505
619
  const before = await getComputerSessionControlRecord(deps.db, {
@@ -525,7 +639,9 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
525
639
  before.session.placement,
526
640
  ).catch(() => undefined);
527
641
  }
528
- return context.json(ComputerSessionMutationResponse.parse(prepared), 200);
642
+ const parsed = ComputerSessionMutationResponse.parse(prepared);
643
+ observeLifecycleResult(deps.observability, startedAtMs, parsed);
644
+ return context.json(parsed, 200);
529
645
  }
530
646
 
531
647
  const record = await getComputerSessionControlRecord(deps.db, {
@@ -549,7 +665,9 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
549
665
  computerSessionId,
550
666
  record.session.placement,
551
667
  ).catch(() => undefined);
552
- return context.json(ComputerSessionMutationResponse.parse(completed), 200);
668
+ const parsed = ComputerSessionMutationResponse.parse(completed);
669
+ observeLifecycleResult(deps.observability, startedAtMs, parsed);
670
+ return context.json(parsed, 200);
553
671
  }
554
672
 
555
673
  const sourceSession = await requireSourceSession(deps, workspaceId, record.sourceSessionId);
@@ -570,12 +688,28 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
570
688
  });
571
689
  const client = await provisionController(grant, record, placement, origin);
572
690
  try {
573
- await client.endComputerSession(
691
+ await withInteractionHolderHeartbeat(
692
+ deps,
574
693
  {
575
- computerSessionId,
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,
576
703
  controllerGeneration: binding.controllerGeneration,
577
704
  },
578
- { removeState: true },
705
+ async () =>
706
+ await client.endComputerSession(
707
+ {
708
+ computerSessionId,
709
+ controllerGeneration: binding.controllerGeneration,
710
+ },
711
+ { removeState: true },
712
+ ),
579
713
  );
580
714
  } catch (error) {
581
715
  if (!(error instanceof BrowserControlRequestError && error.status === 404)) {
@@ -598,7 +732,9 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
598
732
  return completed;
599
733
  },
600
734
  );
601
- return context.json(ComputerSessionMutationResponse.parse(response), 200);
735
+ const parsed = ComputerSessionMutationResponse.parse(response);
736
+ observeLifecycleResult(deps.observability, startedAtMs, parsed);
737
+ return context.json(parsed, 200);
602
738
  } catch (error) {
603
739
  throw computerRouteError(error);
604
740
  }
@@ -614,15 +750,66 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
614
750
  waitSignal: AbortSignal,
615
751
  callback: (placement: ComputerPlacement) => Promise<T>,
616
752
  ): Promise<T> {
617
- if (
618
- expectedPlacement?.kind === "attached_device" ||
619
- expectedPlacement?.kind === "external_provider"
620
- ) {
753
+ if (expectedPlacement?.kind === "external_provider") {
621
754
  throw new BrowserControlUnsupportedError(
622
755
  `computer placement ${expectedPlacement.kind} is not executable`,
623
756
  );
624
757
  }
625
- return await withChannelA(
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(
626
813
  channelServices,
627
814
  {
628
815
  accountId: grant.accountId,
@@ -631,6 +818,7 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
631
818
  subjectId: grant.subjectId,
632
819
  waitSignal,
633
820
  operation,
821
+ retryControllerTransport: operation === "computer.read" || operation === "computer.action",
634
822
  },
635
823
  async (handle) => {
636
824
  if (expectedPlacement?.kind === "sandbox_group") {
@@ -703,6 +891,108 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
703
891
  );
704
892
  }
705
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
+
706
996
  async function withActiveComputerController<T>(
707
997
  context: Context,
708
998
  grant: AccessGrant,
@@ -717,6 +1007,7 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
717
1007
  binding: NonNullable<ComputerSessionControlRecord["session"]["controller"]>;
718
1008
  placement: ComputerPlacement;
719
1009
  }) => Promise<T>,
1010
+ recoverMissing = true,
720
1011
  ): Promise<T> {
721
1012
  try {
722
1013
  const record = await getComputerSessionControlRecord(deps.db, {
@@ -738,6 +1029,73 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
738
1029
  if (!admitted) {
739
1030
  throw new ComputerSessionStateError("ComputerSession controller authority changed");
740
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
+
741
1099
  const sourceSession = await requireSourceSession(deps, workspaceId, record.sourceSessionId);
742
1100
  return await withComputerPlacement(
743
1101
  sourceSession,
@@ -746,44 +1104,12 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
746
1104
  binding.placementInstanceId,
747
1105
  channelOperation,
748
1106
  context.req.raw.signal,
749
- async (placement) => {
750
- const client = await provisionController(
751
- grant,
752
- record,
753
- placement,
754
- requestOrigin(context, deps.settings.corsAllowOriginRegex),
755
- );
756
- const tokens = deriveComputerSessionControllerTokens({
757
- rootSecret: controllerAuthorityRoot(deps),
758
- accountId: grant.accountId,
759
- workspaceId,
760
- placement: record.session.placement,
761
- placementInstanceId: placement.placementInstanceId,
762
- computerSessionId,
763
- controllerGeneration: binding.controllerGeneration,
764
- tokenGeneration: record.tokenGeneration,
765
- });
766
- const result = await callback({
767
- client,
768
- sessionClient: client.computerSessionClient({
769
- reference: {
770
- computerSessionId,
771
- controllerGeneration: binding.controllerGeneration,
772
- },
773
- ...tokens,
774
- }),
775
- record,
776
- binding,
777
- placement,
778
- });
779
- await touchComputerSessionController(deps.db, {
780
- accountId: grant.accountId,
781
- workspaceId,
782
- computerSessionId,
783
- controllerGeneration: binding.controllerGeneration,
784
- }).catch(() => false);
785
- return result;
786
- },
1107
+ async (placement) =>
1108
+ await run(
1109
+ await cacheComputerControllerPlacement(grant, workspaceId, placement).catch(
1110
+ () => placement,
1111
+ ),
1112
+ ),
787
1113
  );
788
1114
  } catch (error) {
789
1115
  throw computerRouteError(error);
@@ -910,6 +1236,23 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
910
1236
  ).client;
911
1237
  }
912
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
+
913
1256
  async function releaseInteractionHolder(
914
1257
  grant: AccessGrant,
915
1258
  workspaceId: string,
@@ -1005,9 +1348,11 @@ function nativeControllerAuthority(
1005
1348
  const placementId =
1006
1349
  placement.placement.kind === "connected_machine"
1007
1350
  ? placement.placement.sandboxId
1008
- : placement.placement.kind === "sandbox_group"
1009
- ? placement.placement.sandboxGroupId
1010
- : null;
1351
+ : placement.placement.kind === "attached_device"
1352
+ ? placement.placement.deviceId
1353
+ : placement.placement.kind === "sandbox_group"
1354
+ ? placement.placement.sandboxGroupId
1355
+ : null;
1011
1356
  if (!placementId) {
1012
1357
  throw new BrowserControlUnsupportedError("computer controller placement is unsupported");
1013
1358
  }
@@ -1160,6 +1505,16 @@ function isTerminalOperation(state: string): boolean {
1160
1505
  return state === "completed" || state === "failed" || state === "outcome_unknown";
1161
1506
  }
1162
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
+
1163
1518
  function interactionFailure(error: unknown) {
1164
1519
  if (error instanceof BrowserControlRequestError) return error.error;
1165
1520
  if (