@opengeni/api-router 0.30.3 → 2.2.0-canary.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 (165) hide show
  1. package/dist/api-websocket.d.ts +18 -0
  2. package/dist/app.js +1 -1
  3. package/dist/auth/managed-auth.d.ts +6 -0
  4. package/dist/{chunk-WCXHI3FF.js → chunk-3TP54PPX.js} +26692 -15389
  5. package/dist/chunk-3TP54PPX.js.map +1 -0
  6. package/dist/codemode.d.ts +8 -1
  7. package/dist/company-brain-okf.d.ts +24 -0
  8. package/dist/connected-machine-computer-access.d.ts +11 -0
  9. package/dist/connection-authority-owner.d.ts +59 -0
  10. package/dist/connection-ownership.d.ts +47 -0
  11. package/dist/controller-data-plane.d.ts +16 -0
  12. package/dist/editable-artifact-websocket.d.ts +5 -15
  13. package/dist/editable-artifact-workspace-files.d.ts +1 -1
  14. package/dist/http/api-error.d.ts +10 -0
  15. package/dist/http/cors.d.ts +5 -0
  16. package/dist/http/interaction-control-error.d.ts +13 -0
  17. package/dist/http/sse.d.ts +2 -3
  18. package/dist/index.d.ts +5 -2
  19. package/dist/index.js +326 -82
  20. package/dist/index.js.map +1 -1
  21. package/dist/integrations/atlassian.d.ts +17 -0
  22. package/dist/integrations/fiken.d.ts +7 -2
  23. package/dist/integrations/google-drive.d.ts +18 -0
  24. package/dist/integrations/oauth-client.d.ts +22 -3
  25. package/dist/integrations/oauth-profiles.d.ts +195 -0
  26. package/dist/integrations/personal-github-repositories.d.ts +45 -0
  27. package/dist/integrations/personal-github.d.ts +40 -0
  28. package/dist/integrations/pr-review-provider.d.ts +22 -0
  29. package/dist/integrations/provider-oauth.d.ts +8 -0
  30. package/dist/integrations/slack-app-home.d.ts +24 -0
  31. package/dist/integrations/slack-bot.d.ts +157 -9
  32. package/dist/integrations/slack-interactions.d.ts +39 -5
  33. package/dist/integrations/slack-routing.d.ts +120 -0
  34. package/dist/integrations/social-oauth.d.ts +5 -0
  35. package/dist/interaction-frame-proxy.d.ts +68 -0
  36. package/dist/mcp/company-brain-governed-writes.d.ts +26 -0
  37. package/dist/mcp/company-profile-agent-admin.d.ts +44 -0
  38. package/dist/mcp/documents.d.ts +1 -0
  39. package/dist/mcp/remember.d.ts +28 -0
  40. package/dist/mcp/request-abort.d.ts +19 -0
  41. package/dist/mcp/scheduled-task-view.d.ts +4 -4
  42. package/dist/mcp/server.d.ts +15 -2
  43. package/dist/mcp/session-view.d.ts +4 -0
  44. package/dist/mcp/session-wait.d.ts +137 -0
  45. package/dist/routes/automations.d.ts +4 -0
  46. package/dist/routes/billing.d.ts +5 -0
  47. package/dist/routes/browser-sessions.d.ts +4 -1
  48. package/dist/routes/company-brain.d.ts +3 -0
  49. package/dist/routes/company-profile.d.ts +2 -1
  50. package/dist/routes/connection-authorities.d.ts +3 -0
  51. package/dist/routes/personal-github-git-broker.d.ts +29 -0
  52. package/dist/routes/personal-github.d.ts +3 -0
  53. package/dist/routes/pr-review.d.ts +3 -0
  54. package/dist/routes/sessions.d.ts +1 -0
  55. package/dist/routes/user-resource-authorities.d.ts +3 -0
  56. package/dist/routes/workspace-capture.d.ts +10 -2
  57. package/dist/routes/workspace-learning.d.ts +3 -0
  58. package/dist/routes/workspaces.d.ts +19 -0
  59. package/dist/sandbox/channel-a.d.ts +11 -1
  60. package/dist/sandbox/connection-authority.d.ts +3 -0
  61. package/dist/sandbox/enrollment.d.ts +2 -0
  62. package/dist/sandbox/machines.d.ts +2 -2
  63. package/dist/sandbox/metrics-ingestion.d.ts +10 -20
  64. package/dist/sandbox/viewer.d.ts +24 -1
  65. package/dist/sandbox-file-artifacts.d.ts +11 -0
  66. package/dist/scheduled-task-deletion.d.ts +14 -0
  67. package/dist/slack-reaction-files.d.ts +1 -1
  68. package/dist/temporal-schedule-cleanup.d.ts +1 -0
  69. package/package.json +20 -18
  70. package/src/api-websocket.ts +23 -0
  71. package/src/app.ts +161 -23
  72. package/src/auth/managed-auth.ts +25 -3
  73. package/src/codemode.ts +53 -23
  74. package/src/codex-realtime.ts +8 -2
  75. package/src/company-brain-okf.ts +340 -0
  76. package/src/connected-machine-computer-access.ts +33 -0
  77. package/src/connection-authority-owner.ts +61 -0
  78. package/src/connection-ownership.ts +180 -0
  79. package/src/controller-data-plane.ts +47 -0
  80. package/src/editable-artifact-native-kernel.ts +26 -15
  81. package/src/editable-artifact-office-import.ts +28 -3
  82. package/src/editable-artifact-production.ts +13 -7
  83. package/src/editable-artifact-websocket.ts +11 -18
  84. package/src/editable-artifact-workspace-files.ts +31 -6
  85. package/src/http/api-error.ts +28 -0
  86. package/src/http/auth.ts +4 -0
  87. package/src/http/cors.ts +35 -0
  88. package/src/http/interaction-control-error.ts +164 -0
  89. package/src/http/sse.ts +112 -66
  90. package/src/index.ts +46 -6
  91. package/src/integrations/atlassian.ts +146 -35
  92. package/src/integrations/fiken.ts +102 -22
  93. package/src/integrations/google-drive.ts +301 -92
  94. package/src/integrations/oauth-client.ts +272 -143
  95. package/src/integrations/oauth-profiles.ts +477 -0
  96. package/src/integrations/personal-github-repositories.ts +445 -0
  97. package/src/integrations/personal-github.ts +705 -0
  98. package/src/integrations/pr-review-provider.ts +246 -0
  99. package/src/integrations/provider-oauth.ts +121 -5
  100. package/src/integrations/slack-app-home.ts +300 -0
  101. package/src/integrations/slack-bot.ts +342 -57
  102. package/src/integrations/slack-interactions.ts +1898 -259
  103. package/src/integrations/slack-routing.ts +337 -0
  104. package/src/integrations/social-oauth.ts +25 -0
  105. package/src/interaction-frame-proxy.ts +409 -0
  106. package/src/mcp/company-brain-governed-writes.ts +262 -0
  107. package/src/mcp/company-profile-agent-admin.ts +205 -0
  108. package/src/mcp/documents.ts +37 -6
  109. package/src/mcp/files.ts +16 -1
  110. package/src/mcp/remember.ts +181 -0
  111. package/src/mcp/request-abort.ts +44 -0
  112. package/src/mcp/scheduled-task-view.ts +1 -0
  113. package/src/mcp/server.ts +719 -369
  114. package/src/mcp/session-view.ts +54 -0
  115. package/src/mcp/session-wait.ts +554 -0
  116. package/src/model-catalog.ts +20 -12
  117. package/src/routes/api-integrations.ts +4 -0
  118. package/src/routes/api-keys.ts +1 -0
  119. package/src/routes/automations.ts +534 -0
  120. package/src/routes/billing.ts +57 -1
  121. package/src/routes/browser-sessions.ts +329 -79
  122. package/src/routes/channels.ts +17 -1
  123. package/src/routes/codex.ts +50 -0
  124. package/src/routes/company-brain.ts +367 -0
  125. package/src/routes/company-profile.ts +1 -1
  126. package/src/routes/computer-sessions.ts +320 -93
  127. package/src/routes/connection-authorities.ts +139 -0
  128. package/src/routes/connections.ts +144 -34
  129. package/src/routes/documents.ts +353 -4
  130. package/src/routes/editable-artifacts.ts +11 -3
  131. package/src/routes/enrollments.ts +116 -26
  132. package/src/routes/environments.ts +127 -55
  133. package/src/routes/files.ts +72 -11
  134. package/src/routes/install.ts +82 -28
  135. package/src/routes/integration-facets.ts +29 -0
  136. package/src/routes/interaction-resources.ts +20 -2
  137. package/src/routes/machines.ts +275 -13
  138. package/src/routes/organization-memberships.ts +717 -28
  139. package/src/routes/packs.ts +4 -3
  140. package/src/routes/personal-github-git-broker.ts +785 -0
  141. package/src/routes/personal-github.ts +319 -0
  142. package/src/routes/pr-review.ts +531 -0
  143. package/src/routes/rigs.ts +104 -39
  144. package/src/routes/scheduled-tasks.ts +19 -21
  145. package/src/routes/sessions.ts +775 -38
  146. package/src/routes/social.ts +14 -2
  147. package/src/routes/supergrok.ts +23 -4
  148. package/src/routes/transcription-recordings.ts +8 -2
  149. package/src/routes/user-resource-authorities.ts +138 -0
  150. package/src/routes/workspace-artifacts.ts +4 -1
  151. package/src/routes/workspace-capture.ts +21 -0
  152. package/src/routes/workspace-learning.ts +228 -0
  153. package/src/routes/workspaces.ts +59 -6
  154. package/src/sandbox/auth-callout.ts +38 -12
  155. package/src/sandbox/channel-a.ts +242 -19
  156. package/src/sandbox/connection-authority.ts +3 -0
  157. package/src/sandbox/enrollment.ts +15 -3
  158. package/src/sandbox/machines.ts +186 -64
  159. package/src/sandbox/metrics-ingestion.ts +220 -58
  160. package/src/sandbox/viewer.ts +243 -19
  161. package/src/sandbox-file-artifacts.ts +329 -0
  162. package/src/scheduled-task-deletion.ts +127 -0
  163. package/src/slack-reaction-files.ts +16 -5
  164. package/src/temporal-schedule-cleanup.ts +13 -0
  165. package/dist/chunk-WCXHI3FF.js.map +0 -1
@@ -25,6 +25,7 @@ import {
25
25
  type SessionAuthorizationOperation,
26
26
  } from "@opengeni/contracts";
27
27
  import {
28
+ getSessionAuthorityEpoch,
28
29
  acquireLease,
29
30
  activateComputerSession,
30
31
  completeComputerSessionEnd,
@@ -36,7 +37,8 @@ import {
36
37
  findComputerSessionControlRecordByOperation,
37
38
  getAttachedBrowserDevice,
38
39
  getComputerSessionControlRecord,
39
- getEnrollment,
40
+ getLiveEnrollmentConnection,
41
+ getSandbox,
40
42
  getSession,
41
43
  listComputerSessions,
42
44
  prepareComputerSessionCreate,
@@ -44,6 +46,7 @@ import {
44
46
  releaseLeaseHolder,
45
47
  readLease,
46
48
  recordLeaseControllerDataPlaneUrl,
49
+ terminalizeStaleConnectedInteractionPlacement,
47
50
  touchComputerSessionController,
48
51
  type ComputerSessionControlRecord,
49
52
  type LeaseSnapshot,
@@ -52,6 +55,7 @@ import {
52
55
  requireAccessGrant,
53
56
  requireSessionAuthorization,
54
57
  relayConfigFromSettings,
58
+ resolveSessionSandboxRuntime,
55
59
  SessionAuthorizationDeniedError,
56
60
  SessionAuthorizationUnavailableError,
57
61
  type ApiRouteDeps,
@@ -71,6 +75,7 @@ import {
71
75
  NatsControlRpc,
72
76
  NatsOpStreamTransport,
73
77
  provisionBrowserControlClient,
78
+ renewSandboxProviderExpiration,
74
79
  type BrowserControlPlacementSession,
75
80
  } from "@opengeni/runtime/sandbox";
76
81
  import type { Context, Hono } from "hono";
@@ -81,9 +86,21 @@ import {
81
86
  deriveComputerSessionControllerTokens,
82
87
  deriveComputerViewGrantToken,
83
88
  } from "../browser-controller-authority";
84
- import { withCachedController } from "../controller-data-plane";
89
+ import { connectedMachineComputerAccessError } from "../connected-machine-computer-access";
90
+ import {
91
+ controllerCacheAllowsHostFetch,
92
+ controllerCachedUrlIsUsable,
93
+ shouldPersistControllerDataPlaneUrl,
94
+ withCachedController,
95
+ } from "../controller-data-plane";
85
96
  import { withInteractionHolderHeartbeat } from "../interaction-holder-heartbeat";
86
- import { allowedCorsOrigin } from "../http/cors";
97
+ import { validateInteractionRequestOrigin } from "../http/cors";
98
+ import { ApiHttpError } from "../http/api-error";
99
+ import { interactionControlApiError } from "../http/interaction-control-error";
100
+ import {
101
+ createInteractionFrameProxyAttachment,
102
+ placementUsesInteractionFrameProxy,
103
+ } from "../interaction-frame-proxy";
87
104
  import { observeComputerActionResult, observeLifecycleResult } from "../interaction-metrics";
88
105
  import { withChannelA, withChannelARead, type ChannelAOperation } from "../sandbox/channel-a";
89
106
 
@@ -141,7 +158,7 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
141
158
  const request = await parseJsonBody(context, CreateComputerSessionRequest);
142
159
  const startedAtMs = performance.now();
143
160
  await authorizeSourceSession(deps, grant, request.sessionId, "session.control");
144
- const origin = requestOrigin(context, deps.settings.corsAllowOriginRegex);
161
+ const origin = requestOrigin(context, deps.settings);
145
162
  const authority = controllerAuthorityRoot(deps);
146
163
 
147
164
  try {
@@ -264,26 +281,27 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
264
281
  () => placement,
265
282
  );
266
283
  } catch (error) {
267
- if (
284
+ const retryableRequestFailure =
285
+ error instanceof BrowserControlRequestError &&
286
+ error.retryable &&
287
+ error.error.code !== "machine_locked";
288
+ const outcomeUnknown =
289
+ error instanceof BrowserControlProtocolError ||
268
290
  error instanceof BrowserControlTransportError ||
269
- (error instanceof BrowserControlRequestError &&
270
- error.retryable &&
271
- error.error.code !== "machine_locked") ||
272
- isAbort(error)
273
- ) {
274
- throw error;
275
- }
291
+ isAbort(error);
292
+ const rethrowAfterFailure =
293
+ error instanceof BrowserControlTransportError ||
294
+ retryableRequestFailure ||
295
+ isAbort(error);
276
296
  const failed = await failComputerSessionOperation(deps.db, {
277
297
  accountId: grant.accountId,
278
298
  workspaceId,
279
299
  operationId: request.operationId,
280
300
  computerSessionId: preparedSession.id,
281
- ...(error instanceof BrowserControlProtocolError
282
- ? { state: "outcome_unknown" as const }
283
- : {}),
301
+ ...(outcomeUnknown ? { state: "outcome_unknown" as const } : {}),
284
302
  error: interactionFailure(error),
285
303
  });
286
- if (interactionHeld && !(error instanceof BrowserControlProtocolError)) {
304
+ if (interactionHeld && !outcomeUnknown) {
287
305
  await releaseInteractionHolder(
288
306
  grant,
289
307
  workspaceId,
@@ -291,6 +309,7 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
291
309
  placement.placement,
292
310
  ).catch(() => undefined);
293
311
  }
312
+ if (rethrowAfterFailure) throw error;
294
313
  return failed;
295
314
  }
296
315
  // These facts come from the physical adapter after its native helper
@@ -452,7 +471,7 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
452
471
  "/v1/workspaces/:workspaceId/computer-sessions/:computerSessionId/attachments",
453
472
  async (context) => {
454
473
  const { workspaceId, grant, computerSessionId } = await routePreamble(context, "stream:view");
455
- const origin = requestOrigin(context, deps.settings.corsAllowOriginRegex);
474
+ const origin = requestOrigin(context, deps.settings);
456
475
  const request = await parseJsonBody(context, ComputerSessionAttachmentRequest);
457
476
  const result = await withActiveComputerController(
458
477
  context,
@@ -463,25 +482,10 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
463
482
  "computer.attach",
464
483
  async ({ client, sessionClient, record, binding, placement }) => {
465
484
  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
485
  const reference = {
481
486
  computerSessionId,
482
487
  controllerGeneration: binding.controllerGeneration,
483
488
  };
484
- await client.createComputerViewGrant(reference, { grantId, token, expiresAt });
485
489
  const target = (await sessionClient.listTargets()).find(
486
490
  (candidate) => candidate.id === request.targetId,
487
491
  );
@@ -492,6 +496,33 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
492
496
  retryable: false,
493
497
  });
494
498
  }
499
+ if (target.kind === "app") {
500
+ throw new BrowserControlRequestError(409, {
501
+ code: "unsupported",
502
+ message:
503
+ "application targets support semantic control but do not provide a visual stream; select a window or screen",
504
+ retryable: false,
505
+ });
506
+ }
507
+ const grantId = randomUUID();
508
+ const expiresAt = new Date(Date.now() + request.expiresInSeconds * 1_000).toISOString();
509
+ const token = deriveComputerViewGrantToken({
510
+ rootSecret: controllerAuthorityRoot(deps),
511
+ accountId: grant.accountId,
512
+ workspaceId,
513
+ placement: record.session.placement,
514
+ placementInstanceId: placement.placementInstanceId,
515
+ computerSessionId,
516
+ controllerGeneration: binding.controllerGeneration,
517
+ tokenGeneration: record.tokenGeneration,
518
+ grantId,
519
+ expiresAt,
520
+ });
521
+ await client.createComputerViewGrant(reference, {
522
+ grantId,
523
+ token,
524
+ expiresAt,
525
+ });
495
526
  const relaySecret = placement.session.openComputerFrames
496
527
  ? resolveStreamTokenSecret(deps.settings)
497
528
  : null;
@@ -510,15 +541,31 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
510
541
  ...(request.stream ? { stream: request.stream } : {}),
511
542
  });
512
543
  } catch (error) {
544
+ const publicFailure = interactionControlApiError(error, "computer");
513
545
  console.error("computer frame relay open failed", {
514
546
  computerSessionId,
515
547
  targetId: request.targetId,
516
- failure: error instanceof Error ? error.message : String(error),
548
+ failureCode: publicFailure?.details?.controlFailureCode ?? "unclassified",
549
+ retryable: publicFailure?.retryable ?? false,
550
+ outcomeUnknown: publicFailure?.outcomeUnknown ?? false,
551
+ controlRequestId: publicFailure?.details?.controlRequestId ?? null,
517
552
  });
518
553
  throw error;
519
554
  }
520
555
  const stream = relayed
521
556
  ? await (async () => {
557
+ // 0281: stamp the authenticated viewer subject and the live
558
+ // session authority epoch into the relay stream token.
559
+ const relayAuthorityEpoch = await getSessionAuthorityEpoch(deps.db, {
560
+ accountId: grant.accountId,
561
+ workspaceId,
562
+ sessionId: record.sourceSessionId,
563
+ });
564
+ if (relayAuthorityEpoch === null) {
565
+ throw new BrowserControlUnsupportedError(
566
+ "stream authority is unavailable for this session",
567
+ );
568
+ }
522
569
  const relayToken = await mintStreamToken(relaySecret!, {
523
570
  workspaceId,
524
571
  sessionId: record.sourceSessionId,
@@ -526,6 +573,8 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
526
573
  leaseEpoch: record.tokenGeneration,
527
574
  port: relayed.channel.port,
528
575
  ttlSeconds: request.expiresInSeconds,
576
+ subjectId: grant.subjectId,
577
+ authorityEpoch: relayAuthorityEpoch,
529
578
  });
530
579
  return {
531
580
  kind: "relay" as const,
@@ -540,30 +589,55 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
540
589
  },
541
590
  };
542
591
  })()
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
- };
592
+ : await (async () => {
593
+ const rfb =
594
+ record.session.placement.kind === "sandbox_group" &&
595
+ record.session.platform === "linux" &&
596
+ target.kind === "screen";
597
+ const protocols = rfb
598
+ ? [
599
+ "binary",
600
+ COMPUTER_RFB_WEBSOCKET_PROTOCOL,
601
+ `${BROWSER_CONTROL_WEBSOCKET_BEARER_PREFIX}${token}`,
602
+ ]
603
+ : [
604
+ COMPUTER_CONTROL_WEBSOCKET_PROTOCOL,
605
+ `${BROWSER_CONTROL_WEBSOCKET_BEARER_PREFIX}${token}`,
606
+ ];
607
+ const upstreamUrl = rfb
608
+ ? await client.computerRfbStreamUrl(reference, request.targetId)
609
+ : await client.computerFrameStreamUrl(
610
+ reference,
611
+ request.targetId,
612
+ request.stream,
613
+ );
614
+ const attachment = placementUsesInteractionFrameProxy(placement.lease?.backend, {
615
+ openSandboxSignedEndpoints: deps.settings.openSandboxSignedEndpoints,
616
+ ...(typeof deps.settings.openSandboxInteractionFrameProxy === "boolean"
617
+ ? {
618
+ openSandboxInteractionFrameProxy:
619
+ deps.settings.openSandboxInteractionFrameProxy,
620
+ }
621
+ : {}),
622
+ })
623
+ ? createInteractionFrameProxyAttachment({
624
+ requestUrl: context.req.url,
625
+ publicBaseUrl: deps.settings.publicBaseUrl,
626
+ webBaseUrl: deps.settings.webBaseUrl,
627
+ forwardedProto: context.req.header("x-forwarded-proto"),
628
+ forwardedHost:
629
+ context.req.header("x-forwarded-host") ?? context.req.header("host"),
630
+ rootSecret: controllerAuthorityRoot(deps),
631
+ upstreamUrl,
632
+ upstreamProtocols: protocols,
633
+ origin,
634
+ expiresAt,
635
+ })
636
+ : { url: upstreamUrl, protocols };
637
+ return rfb
638
+ ? { kind: "direct_rfb" as const, ...attachment }
639
+ : { kind: "direct_websocket" as const, ...attachment };
640
+ })();
567
641
  return ComputerSessionAttachment.parse({
568
642
  computerSessionId,
569
643
  controllerGeneration: binding.controllerGeneration,
@@ -614,7 +688,7 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
614
688
  const computerSessionId = requireUuidParam(context, "computerSessionId");
615
689
  const request = await parseJsonBody(context, ComputerSessionLifecycleRequest);
616
690
  const startedAtMs = performance.now();
617
- const origin = requestOrigin(context, deps.settings.corsAllowOriginRegex);
691
+ const origin = requestOrigin(context, deps.settings);
618
692
  try {
619
693
  const before = await getComputerSessionControlRecord(deps.db, {
620
694
  accountId: grant.accountId,
@@ -765,23 +839,41 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
765
839
  if (device.state !== "connected") {
766
840
  throw new ComputerSessionStateError("Attached browser machine is disconnected");
767
841
  }
768
- const enrollment = await getEnrollment(
842
+ const enrollment = await getLiveEnrollmentConnection(
769
843
  deps.db,
770
844
  sourceSession.workspaceId,
771
845
  device.enrollmentId,
772
846
  );
773
- if (!enrollment || enrollment.status !== "active") {
847
+ if (!enrollment || enrollment.status !== "active" || !enrollment.connectionInstanceId) {
774
848
  throw new ComputerSessionStateError("Attached browser machine is unavailable");
775
849
  }
776
- assertPlacementInstance(expectedPlacementInstanceId, device.connectionGeneration);
850
+ if (!enrollment.workspaceRoot) {
851
+ throw new ComputerSessionStateError(
852
+ "Attached browser machine has not reported an absolute workspace root",
853
+ );
854
+ }
855
+ if (operation !== "computer.end") {
856
+ assertConnectedMachineComputerAccess(enrollment, operation);
857
+ }
858
+ const placementInstanceId = attachedEndPlacementInstanceId(
859
+ operation,
860
+ expectedPlacementInstanceId,
861
+ device.connectionGeneration,
862
+ );
777
863
  const built = await buildSelfhostedBackendSession({
778
864
  workspaceId: sourceSession.workspaceId,
779
865
  agentId: device.enrollmentId,
866
+ connectionInstanceId: enrollment.connectionInstanceId,
867
+ workspaceRoot: enrollment.workspaceRoot,
780
868
  relay: relayConfigFromSettings(deps.settings),
781
869
  controlRpcFactory: () => new NatsControlRpc(async () => deps.bus.getRequestConnection()),
782
870
  epoch: 0,
783
871
  timeoutMs: deps.settings.sandboxSelfhostedControlTimeoutMs,
784
872
  execTimeoutMs: deps.settings.sandboxSelfhostedExecTimeoutMs,
873
+ operationResourcePolicy: enrollment.operationPolicy,
874
+ operationResourcePolicySupported:
875
+ enrollment.agentCapabilities.operationResourcePolicy === true,
876
+ operationCpuQuotaSupported: enrollment.agentCapabilities.operationCpuQuota === true,
785
877
  ...(deps.settings.agentOpStreamEnabled === true &&
786
878
  enrollment.opStream === true &&
787
879
  deps.bus.getOpStreamConnection
@@ -797,7 +889,7 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
797
889
  waitSignal.throwIfAborted();
798
890
  return await callback({
799
891
  placement: expectedPlacement,
800
- placementInstanceId: device.connectionGeneration,
892
+ placementInstanceId,
801
893
  session: built.session as unknown as BrowserControlPlacementSession,
802
894
  lease: null,
803
895
  });
@@ -838,13 +930,30 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
838
930
  }
839
931
 
840
932
  const resolved = await handle.routingSession.prime();
933
+ if (operation !== "computer.end" && resolved.kind === "selfhosted" && resolved.sandboxId) {
934
+ const sandbox = await getSandbox(deps.db, grant, resolved.sandboxId);
935
+ if (sandbox?.kind !== "selfhosted" || !sandbox.enrollmentId) {
936
+ throw new ComputerSessionStateError("Connected Machine placement is unavailable");
937
+ }
938
+ const enrollment = await getLiveEnrollmentConnection(
939
+ deps.db,
940
+ grant,
941
+ sandbox.enrollmentId,
942
+ );
943
+ if (!enrollment?.connectionInstanceId) {
944
+ throw new ComputerSessionStateError("Connected Machine is unavailable");
945
+ }
946
+ assertConnectedMachineComputerAccess(enrollment, operation);
947
+ }
841
948
  if (expectedPlacement?.kind === "connected_machine") {
842
949
  if (
843
950
  resolved.kind !== "selfhosted" ||
844
951
  resolved.sandboxId !== expectedPlacement.sandboxId
845
952
  ) {
846
- throw new ComputerSessionStateError(
847
- "ComputerSession Connected Machine is not the active placement",
953
+ return await throwComputerSourcePlacementChanged(
954
+ grant,
955
+ sourceSession.id,
956
+ expectedPlacement.sandboxId,
848
957
  );
849
958
  }
850
959
  const placementInstanceId = resolved.providerInstanceId ?? expectedPlacement.sandboxId;
@@ -878,7 +987,10 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
878
987
  }
879
988
  if (resolved.kind === "selfhosted") {
880
989
  return await callback({
881
- placement: { kind: "connected_machine", sandboxId: resolved.sandboxId },
990
+ placement: {
991
+ kind: "connected_machine",
992
+ sandboxId: resolved.sandboxId,
993
+ },
882
994
  placementInstanceId: resolved.providerInstanceId ?? resolved.sandboxId,
883
995
  session: resolved.session as unknown as BrowserControlPlacementSession,
884
996
  lease: null,
@@ -914,7 +1026,9 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
914
1026
  !lease ||
915
1027
  (lease.liveness !== "warm" && lease.liveness !== "draining") ||
916
1028
  lease.instanceId !== binding.placementInstanceId ||
917
- !lease.controllerDataPlaneUrl
1029
+ !lease.controllerDataPlaneUrl ||
1030
+ (lease.backend === "opensandbox" && deps.settings.openSandboxSignedEndpoints) ||
1031
+ !controllerCachedUrlIsUsable(lease.controllerDataPlaneUrl)
918
1032
  ) {
919
1033
  return null;
920
1034
  }
@@ -938,8 +1052,29 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
938
1052
  ) {
939
1053
  return placement;
940
1054
  }
1055
+ if (placement.lease.backend === "opensandbox" && deps.settings.openSandboxSignedEndpoints) {
1056
+ if (!placement.lease.controllerDataPlaneUrl) return placement;
1057
+ const lease = await recordLeaseControllerDataPlaneUrl(deps.db, {
1058
+ accountId: grant.accountId,
1059
+ workspaceId,
1060
+ sandboxGroupId: placement.placement.sandboxGroupId,
1061
+ expectedEpoch: placement.lease.leaseEpoch,
1062
+ expectedInstanceId: placement.lease.instanceId,
1063
+ controllerDataPlaneUrl: null,
1064
+ });
1065
+ return lease ? { ...placement, lease } : placement;
1066
+ }
941
1067
  const endpoint = await placement.session.resolveExposedPort(BROWSER_CONTROL_PORT);
942
1068
  const url = buildStreamUrl(endpoint);
1069
+ if (
1070
+ !shouldPersistControllerDataPlaneUrl({
1071
+ backend: placement.lease.backend,
1072
+ signedEndpoints: deps.settings.openSandboxSignedEndpoints,
1073
+ url,
1074
+ })
1075
+ ) {
1076
+ return placement;
1077
+ }
943
1078
  const lease = await recordLeaseControllerDataPlaneUrl(deps.db, {
944
1079
  accountId: grant.accountId,
945
1080
  workspaceId,
@@ -949,6 +1084,9 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
949
1084
  controllerDataPlaneUrl: url,
950
1085
  });
951
1086
  if (!lease) return placement;
1087
+ if (!controllerCacheAllowsHostFetch(url)) {
1088
+ return { ...placement, lease };
1089
+ }
952
1090
  return { ...placement, session: controllerOnlySession(url), lease };
953
1091
  }
954
1092
 
@@ -964,7 +1102,13 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
964
1102
  const sandboxGroupId =
965
1103
  placement.placement.kind === "sandbox_group" ? placement.placement.sandboxGroupId : null;
966
1104
  return await withCachedController({
967
- cachedUrl: sandboxGroupId ? (cachedUrl ?? null) : null,
1105
+ cachedUrl:
1106
+ sandboxGroupId &&
1107
+ cachedUrl &&
1108
+ !(placement.lease?.backend === "opensandbox" && deps.settings.openSandboxSignedEndpoints) &&
1109
+ controllerCachedUrlIsUsable(cachedUrl)
1110
+ ? cachedUrl
1111
+ : null,
968
1112
  createCachedClient: (url) =>
969
1113
  new BrowserControlClient(controllerOnlySession(url), { adminToken }),
970
1114
  prepareCachedClient: async (client) => {
@@ -1016,6 +1160,17 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
1016
1160
  computerSessionId,
1017
1161
  });
1018
1162
  await authorizeSourceSession(deps, grant, record.sourceSessionId, authorizationOperation);
1163
+ let sourceSession: Session | null = null;
1164
+ if (record.session.placement?.kind === "connected_machine") {
1165
+ sourceSession = await requireSourceSession(deps, workspaceId, record.sourceSessionId);
1166
+ if (sourceSession.activeSandboxId !== record.session.placement.sandboxId) {
1167
+ return await throwComputerSourcePlacementChanged(
1168
+ grant,
1169
+ record.sourceSessionId,
1170
+ record.session.placement.sandboxId,
1171
+ );
1172
+ }
1173
+ }
1019
1174
  if (record.session.lifecycle !== "active" || !record.session.controller) {
1020
1175
  throw new ComputerSessionStateError("ComputerSession is not active");
1021
1176
  }
@@ -1096,7 +1251,7 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
1096
1251
  }
1097
1252
  }
1098
1253
 
1099
- const sourceSession = await requireSourceSession(deps, workspaceId, record.sourceSessionId);
1254
+ sourceSession ??= await requireSourceSession(deps, workspaceId, record.sourceSessionId);
1100
1255
  return await withComputerPlacement(
1101
1256
  sourceSession,
1102
1257
  grant,
@@ -1116,6 +1271,25 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
1116
1271
  }
1117
1272
  }
1118
1273
 
1274
+ async function throwComputerSourcePlacementChanged(
1275
+ grant: AccessGrant,
1276
+ sourceSessionId: string,
1277
+ connectedSandboxId: string,
1278
+ ): Promise<never> {
1279
+ const reconciliation = await terminalizeStaleConnectedInteractionPlacement(deps.db, {
1280
+ accountId: grant.accountId,
1281
+ workspaceId: grant.workspaceId,
1282
+ sourceSessionId,
1283
+ connectedSandboxId,
1284
+ });
1285
+ if (reconciliation.sourcePlacementChanged) {
1286
+ throw sourcePlacementChangedApiError("computer_session");
1287
+ }
1288
+ throw new ComputerSessionStateError(
1289
+ "ComputerSession source placement changed during the request; retry",
1290
+ );
1291
+ }
1292
+
1119
1293
  async function ensureInteractionHolder(
1120
1294
  grant: AccessGrant,
1121
1295
  sourceSession: Session,
@@ -1127,6 +1301,11 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
1127
1301
  if (!placement.lease?.instanceId) {
1128
1302
  throw new ComputerSessionStateError("ComputerSession lease placement is unavailable");
1129
1303
  }
1304
+ const sandboxRuntime = await resolveSessionSandboxRuntime(
1305
+ deps.db,
1306
+ deps.settings,
1307
+ sourceSession,
1308
+ );
1130
1309
  const acquired = await acquireLease(deps.db, {
1131
1310
  accountId: grant.accountId,
1132
1311
  workspaceId: sourceSession.workspaceId,
@@ -1136,8 +1315,8 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
1136
1315
  subjectId: sourceSession.id,
1137
1316
  backend: placement.lease.backend,
1138
1317
  os: placement.lease.os,
1139
- image: placement.lease.image,
1140
- rigVersionId: placement.lease.rigVersionId,
1318
+ image: sandboxRuntime.image,
1319
+ rigVersionId: sourceSession.rigVersionId,
1141
1320
  leaseTtlMs: deps.settings.sandboxLeaseTtlMs,
1142
1321
  expectedEpoch: placement.lease.leaseEpoch,
1143
1322
  waitSignal,
@@ -1158,6 +1337,11 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
1158
1337
  ).catch(() => undefined);
1159
1338
  throw new ComputerSessionStateError("ComputerSession placement fence changed; retry");
1160
1339
  }
1340
+ await renewSandboxProviderExpiration({
1341
+ backend: acquired.lease.backend as ApiRouteDeps["settings"]["sandboxBackend"],
1342
+ settings: deps.settings,
1343
+ instanceId: acquired.lease.instanceId,
1344
+ }).catch(() => false);
1161
1345
  return true;
1162
1346
  }
1163
1347
 
@@ -1273,7 +1457,11 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
1273
1457
  async function routePreamble(
1274
1458
  context: Context,
1275
1459
  permission: "sessions:read" | "sessions:control" | "stream:view",
1276
- ): Promise<{ workspaceId: string; grant: AccessGrant; computerSessionId: string }> {
1460
+ ): Promise<{
1461
+ workspaceId: string;
1462
+ grant: AccessGrant;
1463
+ computerSessionId: string;
1464
+ }> {
1277
1465
  const workspaceId = context.req.param("workspaceId") ?? "";
1278
1466
  const grant = await requireAccessGrant(context, deps, workspaceId, permission);
1279
1467
  return {
@@ -1284,6 +1472,21 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
1284
1472
  }
1285
1473
  }
1286
1474
 
1475
+ function sourcePlacementChangedApiError(interactionResource: "computer_session"): ApiHttpError {
1476
+ return new ApiHttpError(409, {
1477
+ code: "conflict",
1478
+ message:
1479
+ "This Desktop belonged to a previous task placement and was retired. Open a new Desktop.",
1480
+ retryable: false,
1481
+ outcomeUnknown: false,
1482
+ details: {
1483
+ interactionResource,
1484
+ interactionFailureCode: "source_placement_changed",
1485
+ interactionLifecycle: "lost",
1486
+ },
1487
+ });
1488
+ }
1489
+
1287
1490
  function computerCreateInput(
1288
1491
  grant: AccessGrant,
1289
1492
  workspaceId: string,
@@ -1400,7 +1603,10 @@ async function authorizeSourceSession(
1400
1603
  });
1401
1604
  } catch (error) {
1402
1605
  if (error instanceof SessionAuthorizationDeniedError) {
1403
- throw new HTTPException(404, { message: "session not found", cause: error });
1606
+ throw new HTTPException(404, {
1607
+ message: "session not found",
1608
+ cause: error,
1609
+ });
1404
1610
  }
1405
1611
  if (error instanceof SessionAuthorizationUnavailableError) {
1406
1612
  throw new HTTPException(503, {
@@ -1456,26 +1662,8 @@ function requireOpaqueParam(context: Context, name: string): string {
1456
1662
  return value;
1457
1663
  }
1458
1664
 
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;
1665
+ function requestOrigin(context: Context, settings: ApiRouteDeps["settings"]): string | null {
1666
+ return validateInteractionRequestOrigin(context.req.header("origin"), settings);
1479
1667
  }
1480
1668
 
1481
1669
  function interactionActorForGrant(grant: AccessGrant): ReturnType<typeof InteractionActor.parse> {
@@ -1501,6 +1689,39 @@ function assertPlacementInstance(expected: string | null, actual: string): void
1501
1689
  }
1502
1690
  }
1503
1691
 
1692
+ /** End must still reach the live agent with the session's original token
1693
+ * fence. A later Chrome generation must not block ScreenCaptureKit teardown. */
1694
+ function attachedEndPlacementInstanceId(
1695
+ operation: ChannelAOperation,
1696
+ expectedPlacementInstanceId: string | null,
1697
+ liveGeneration: string,
1698
+ ): string {
1699
+ if (operation === "computer.end" && expectedPlacementInstanceId) {
1700
+ return expectedPlacementInstanceId;
1701
+ }
1702
+ assertPlacementInstance(expectedPlacementInstanceId, liveGeneration);
1703
+ return liveGeneration;
1704
+ }
1705
+
1706
+ function assertConnectedMachineComputerAccess(
1707
+ enrollment: {
1708
+ hasDisplay: boolean;
1709
+ desktopUnavailableReason: string | null;
1710
+ allowScreenControl: boolean;
1711
+ },
1712
+ operation: ChannelAOperation,
1713
+ ): void {
1714
+ const error = connectedMachineComputerAccessError(
1715
+ enrollment,
1716
+ operation === "computer.action" || operation === "computer.control",
1717
+ );
1718
+ if (!error) return;
1719
+ if (error.status === 403) {
1720
+ throw new HTTPException(403, { message: error.message });
1721
+ }
1722
+ throw new ComputerSessionStateError(error.message);
1723
+ }
1724
+
1504
1725
  function isTerminalOperation(state: string): boolean {
1505
1726
  return state === "completed" || state === "failed" || state === "outcome_unknown";
1506
1727
  }
@@ -1521,7 +1742,11 @@ function interactionFailure(error: unknown) {
1521
1742
  error instanceof BrowserControlUnsupportedError ||
1522
1743
  error instanceof BrowserControlServerUnsupportedError
1523
1744
  ) {
1524
- return { code: "unsupported" as const, message: error.message, retryable: false };
1745
+ return {
1746
+ code: "unsupported" as const,
1747
+ message: error.message,
1748
+ retryable: false,
1749
+ };
1525
1750
  }
1526
1751
  if (error instanceof BrowserControlServerError) {
1527
1752
  return {
@@ -1541,6 +1766,8 @@ function interactionFailure(error: unknown) {
1541
1766
  }
1542
1767
 
1543
1768
  function computerRouteError(error: unknown): HTTPException {
1769
+ const connectedMachineError = interactionControlApiError(error, "computer");
1770
+ if (connectedMachineError) return connectedMachineError;
1544
1771
  if (error instanceof HTTPException) return error;
1545
1772
  if (error instanceof ComputerSessionNotFoundError) {
1546
1773
  return new HTTPException(404, { message: error.message, cause: error });