@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
@@ -59,6 +59,7 @@ import {
59
59
  type SiteAuthAuthority,
60
60
  } from "@opengeni/contracts";
61
61
  import {
62
+ getSessionAuthorityEpoch,
62
63
  acquireLease,
63
64
  activateBrowserSession,
64
65
  ATTACHED_BROWSER_SESSION_CAPABILITIES,
@@ -95,10 +96,11 @@ import {
95
96
  findBrowserDownloadSave,
96
97
  getBrowserSessionControlRecord,
97
98
  getComputerSessionControlRecord,
99
+ getBrowserIdentity,
98
100
  getBrowserPrivateCheckpointAuthority,
99
101
  getAttachedBrowserDevice,
100
102
  getBrowserRevisionArtifactAuthority,
101
- getEnrollment,
103
+ getLiveEnrollmentConnection,
102
104
  getExternalAuthInteractiveContext,
103
105
  getFilesForSubject,
104
106
  getFileUpload,
@@ -131,6 +133,7 @@ import {
131
133
  touchBrowserSessionController,
132
134
  verifyAuthRun,
133
135
  settleBrowserDownloadSaveFailure,
136
+ terminalizeStaleConnectedInteractionPlacement,
134
137
  type BrowserSessionControlRecord,
135
138
  type BrowserPrivateCheckpointAuthority,
136
139
  type BrowserRevisionArtifactAuthority,
@@ -142,6 +145,7 @@ import {
142
145
  recordWorkspaceUsage,
143
146
  requireLimit,
144
147
  relayConfigFromSettings,
148
+ resolveSessionSandboxRuntime,
145
149
  SessionAuthorizationDeniedError,
146
150
  SessionAuthorizationUnavailableError,
147
151
  type ApiRouteDeps,
@@ -158,16 +162,19 @@ import {
158
162
  buildStreamUrl,
159
163
  exposedPortEndpointFromUrl,
160
164
  buildSelfhostedBackendSession,
165
+ ensureDisplayStack,
161
166
  mintStreamToken,
162
167
  NatsControlRpc,
163
168
  NatsOpStreamTransport,
164
169
  provisionBrowserControlClient,
170
+ renewSandboxProviderExpiration,
165
171
  type BrowserControlPlacementSession,
166
172
  type PlacementBrowserStateCaptureReceipt,
167
173
  type PlacementBrowserNetworkRoute,
168
174
  type PlacementBrowserTransport,
169
175
  type RestorePlacementBrowserStateInput,
170
176
  } from "@opengeni/runtime/sandbox";
177
+ import { retryWhileMissing } from "@opengeni/storage";
171
178
  import type { Context, Hono } from "hono";
172
179
  import { HTTPException } from "hono/http-exception";
173
180
  import type { ContentfulStatusCode } from "hono/utils/http-status";
@@ -176,8 +183,14 @@ import {
176
183
  deriveBrowserNetworkRouteAuthorityDigest,
177
184
  deriveBrowserSessionControllerTokens,
178
185
  deriveBrowserViewGrantToken,
186
+ deriveComputerSessionControllerTokens,
179
187
  } from "../browser-controller-authority";
180
- import { withCachedController } from "../controller-data-plane";
188
+ import {
189
+ controllerCacheAllowsHostFetch,
190
+ controllerCachedUrlIsUsable,
191
+ shouldPersistControllerDataPlaneUrl,
192
+ withCachedController,
193
+ } from "../controller-data-plane";
181
194
  import { withInteractionHolderHeartbeat } from "../interaction-holder-heartbeat";
182
195
  import {
183
196
  browserStateArtifactAad,
@@ -192,7 +205,13 @@ import {
192
205
  resolveProtectedAuthFieldValues,
193
206
  } from "../browser-auth-broker";
194
207
  import { managedNetworkRouteForPlacement } from "../browser-network-route";
195
- import { allowedCorsOrigin } from "../http/cors";
208
+ import { validateInteractionRequestOrigin } from "../http/cors";
209
+ import { ApiHttpError } from "../http/api-error";
210
+ import { interactionControlApiError } from "../http/interaction-control-error";
211
+ import {
212
+ createInteractionFrameProxyAttachment,
213
+ placementUsesInteractionFrameProxy,
214
+ } from "../interaction-frame-proxy";
196
215
  import {
197
216
  observeAuthMutation,
198
217
  observeBrowserActionResult,
@@ -298,7 +317,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
298
317
  const request = await parseJsonBody(context, CreateBrowserSessionRequest);
299
318
  const startedAtMs = performance.now();
300
319
  await authorizeSourceSession(deps, grant, request.sessionId, "session.control");
301
- const origin = requestOrigin(context, deps.settings.corsAllowOriginRegex);
320
+ const origin = requestOrigin(context, deps.settings);
302
321
  const authority = browserAuthorityRoot(deps);
303
322
 
304
323
  try {
@@ -325,6 +344,20 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
325
344
  return context.json(parsed, 200);
326
345
  }
327
346
 
347
+ // Reject an unavailable explicit identity before acquiring or waking the
348
+ // browser placement. prepareBrowserSessionCreate repeats this check in its
349
+ // transaction, so an archive racing this preflight is still fenced.
350
+ if (!existing && request.identityId) {
351
+ const identity = await getBrowserIdentity(deps.db, {
352
+ accountId: grant.accountId,
353
+ workspaceId,
354
+ identityId: request.identityId,
355
+ });
356
+ if (identity.status !== "active") {
357
+ throw new BrowserIdentityStateError("BrowserIdentity is archived");
358
+ }
359
+ }
360
+
328
361
  const sourceSession = await requireSourceSession(deps, workspaceId, request.sessionId);
329
362
  const response = await withBrowserPlacement(
330
363
  sourceSession,
@@ -401,12 +434,6 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
401
434
  });
402
435
  const preparedSession = prepared.session;
403
436
  const controllerGeneration = requireOperationGeneration(record);
404
- const linkedComputer = await requireLinkedComputerBinding(
405
- deps,
406
- grant,
407
- prepared.session,
408
- placement,
409
- );
410
437
  const adminToken = deriveBrowserControllerAdminToken({
411
438
  rootSecret: authority,
412
439
  accountId: grant.accountId,
@@ -432,8 +459,15 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
432
459
  placement,
433
460
  adminToken,
434
461
  origin,
435
- async (client) =>
436
- await withInteractionHolderHeartbeat(
462
+ async (client) => {
463
+ const linkedComputer = await ensureLinkedComputerController(
464
+ deps,
465
+ grant,
466
+ preparedSession,
467
+ placement,
468
+ client,
469
+ );
470
+ return await withInteractionHolderHeartbeat(
437
471
  deps,
438
472
  {
439
473
  grant,
@@ -444,8 +478,13 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
444
478
  resourceId: preparedSession.id,
445
479
  controllerGeneration,
446
480
  },
447
- async () =>
448
- await client.createSession({
481
+ async () => {
482
+ await ensureHeadedBrowserDisplayStack(
483
+ placement.session,
484
+ preparedSession.headless,
485
+ linkedComputer !== null,
486
+ );
487
+ return await client.createSession({
449
488
  browserSessionId: preparedSession.id,
450
489
  controllerGeneration,
451
490
  tokenGeneration: record.tokenGeneration,
@@ -456,8 +495,10 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
456
495
  ...(networkRoute ? { networkRoute } : {}),
457
496
  ...(request.initialUrl ? { initialUrl: request.initialUrl } : {}),
458
497
  ...(restore ? { restore } : {}),
459
- }),
460
- ),
498
+ });
499
+ },
500
+ );
501
+ },
461
502
  );
462
503
  await cacheBrowserControllerPlacement(grant, workspaceId, placement).catch(
463
504
  () => placement,
@@ -1663,7 +1704,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
1663
1704
  context,
1664
1705
  "stream:view",
1665
1706
  );
1666
- const origin = requestOrigin(context, deps.settings.corsAllowOriginRegex);
1707
+ const origin = requestOrigin(context, deps.settings);
1667
1708
  const request = await parseJsonBody(context, BrowserSessionAttachmentRequest);
1668
1709
  const result = await withActiveBrowserController(
1669
1710
  context,
@@ -1719,6 +1760,18 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
1719
1760
  });
1720
1761
  const stream = relayed
1721
1762
  ? await (async () => {
1763
+ // 0281: stamp the authenticated viewer subject and the live
1764
+ // session authority epoch into the relay stream token.
1765
+ const relayAuthorityEpoch = await getSessionAuthorityEpoch(deps.db, {
1766
+ accountId: grant.accountId,
1767
+ workspaceId,
1768
+ sessionId: record.sourceSessionId,
1769
+ });
1770
+ if (relayAuthorityEpoch === null) {
1771
+ throw new BrowserControlUnsupportedError(
1772
+ "stream authority is unavailable for this session",
1773
+ );
1774
+ }
1722
1775
  const relayToken = await mintStreamToken(relaySecret!, {
1723
1776
  workspaceId,
1724
1777
  sessionId: record.sourceSessionId,
@@ -1726,6 +1779,8 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
1726
1779
  leaseEpoch: record.tokenGeneration,
1727
1780
  port: relayed.channel.port,
1728
1781
  ttlSeconds: request.expiresInSeconds,
1782
+ subjectId: grant.subjectId,
1783
+ authorityEpoch: relayAuthorityEpoch,
1729
1784
  });
1730
1785
  return {
1731
1786
  kind: "relay" as const,
@@ -1740,14 +1795,44 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
1740
1795
  },
1741
1796
  };
1742
1797
  })()
1743
- : {
1744
- kind: "direct_websocket" as const,
1745
- url: await client.frameStreamUrl(reference, request.targetId, request.stream),
1746
- protocols: [
1798
+ : await (async () => {
1799
+ const protocols = [
1747
1800
  BROWSER_CONTROL_WEBSOCKET_PROTOCOL,
1748
1801
  `${BROWSER_CONTROL_WEBSOCKET_BEARER_PREFIX}${token}`,
1749
- ],
1750
- };
1802
+ ] as const;
1803
+ const upstreamUrl = await client.frameStreamUrl(
1804
+ reference,
1805
+ request.targetId,
1806
+ request.stream,
1807
+ );
1808
+ const attachment = placementUsesInteractionFrameProxy(placement.lease?.backend, {
1809
+ openSandboxSignedEndpoints: deps.settings.openSandboxSignedEndpoints,
1810
+ ...(typeof deps.settings.openSandboxInteractionFrameProxy === "boolean"
1811
+ ? {
1812
+ openSandboxInteractionFrameProxy:
1813
+ deps.settings.openSandboxInteractionFrameProxy,
1814
+ }
1815
+ : {}),
1816
+ })
1817
+ ? createInteractionFrameProxyAttachment({
1818
+ requestUrl: context.req.url,
1819
+ publicBaseUrl: deps.settings.publicBaseUrl,
1820
+ webBaseUrl: deps.settings.webBaseUrl,
1821
+ forwardedProto: context.req.header("x-forwarded-proto"),
1822
+ forwardedHost:
1823
+ context.req.header("x-forwarded-host") ?? context.req.header("host"),
1824
+ rootSecret,
1825
+ upstreamUrl,
1826
+ upstreamProtocols: protocols,
1827
+ origin,
1828
+ expiresAt,
1829
+ })
1830
+ : { url: upstreamUrl, protocols };
1831
+ return {
1832
+ kind: "direct_websocket" as const,
1833
+ ...attachment,
1834
+ };
1835
+ })();
1751
1836
  return BrowserSessionAttachment.parse({
1752
1837
  browserSessionId,
1753
1838
  controllerGeneration: binding.controllerGeneration,
@@ -1958,7 +2043,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
1958
2043
  const browserSessionId = requireUuidParam(context, "browserSessionId");
1959
2044
  const request = await parseJsonBody(context, BrowserSessionLifecycleRequest);
1960
2045
  const startedAtMs = performance.now();
1961
- const origin = requestOrigin(context, deps.settings.corsAllowOriginRegex);
2046
+ const origin = requestOrigin(context, deps.settings);
1962
2047
  try {
1963
2048
  const before = await getBrowserSessionControlRecord(deps.db, {
1964
2049
  accountId: grant.accountId,
@@ -2184,7 +2269,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2184
2269
  const browserSessionId = requireUuidParam(context, "browserSessionId");
2185
2270
  const request = await parseJsonBody(context, BrowserSessionLifecycleRequest);
2186
2271
  const startedAtMs = performance.now();
2187
- const origin = requestOrigin(context, deps.settings.corsAllowOriginRegex);
2272
+ const origin = requestOrigin(context, deps.settings);
2188
2273
  let restore: RestorePlacementBrowserStateInput | null = null;
2189
2274
  try {
2190
2275
  let before = await getBrowserSessionControlRecord(deps.db, {
@@ -2303,12 +2388,6 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2303
2388
  throw error;
2304
2389
  });
2305
2390
  const controllerGeneration = requireOperationGeneration(record);
2306
- const linkedComputer = await requireLinkedComputerBinding(
2307
- deps,
2308
- grant,
2309
- prepared.session,
2310
- placement,
2311
- );
2312
2391
  const adminToken = deriveBrowserControllerAdminToken({
2313
2392
  rootSecret: authority,
2314
2393
  accountId: grant.accountId,
@@ -2332,6 +2411,13 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2332
2411
  ...(origin ? { allowedOrigins: [origin] } : {}),
2333
2412
  });
2334
2413
  try {
2414
+ const linkedComputer = await ensureLinkedComputerController(
2415
+ deps,
2416
+ grant,
2417
+ prepared.session,
2418
+ placement,
2419
+ client,
2420
+ );
2335
2421
  await withInteractionHolderHeartbeat(
2336
2422
  deps,
2337
2423
  {
@@ -2343,8 +2429,13 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2343
2429
  resourceId: browserSessionId,
2344
2430
  controllerGeneration,
2345
2431
  },
2346
- async () =>
2347
- await client.createSession({
2432
+ async () => {
2433
+ await ensureHeadedBrowserDisplayStack(
2434
+ placement.session,
2435
+ prepared.session.headless,
2436
+ linkedComputer !== null,
2437
+ );
2438
+ return await client.createSession({
2348
2439
  browserSessionId,
2349
2440
  controllerGeneration,
2350
2441
  tokenGeneration: record.tokenGeneration,
@@ -2354,7 +2445,8 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2354
2445
  ...(linkedComputer ? { linkedComputer } : {}),
2355
2446
  ...(networkRoute ? { networkRoute } : {}),
2356
2447
  restore: restore!,
2357
- }),
2448
+ });
2449
+ },
2358
2450
  );
2359
2451
  } catch (error) {
2360
2452
  if (!isDefiniteBrowserControllerFailure(error)) throw error;
@@ -2426,7 +2518,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2426
2518
  const browserSessionId = requireUuidParam(context, "browserSessionId");
2427
2519
  const request = await parseJsonBody(context, BrowserSessionLifecycleRequest);
2428
2520
  const startedAtMs = performance.now();
2429
- const origin = requestOrigin(context, deps.settings.corsAllowOriginRegex);
2521
+ const origin = requestOrigin(context, deps.settings);
2430
2522
  try {
2431
2523
  const before = await getBrowserSessionControlRecord(deps.db, {
2432
2524
  accountId: grant.accountId,
@@ -2573,18 +2665,29 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2573
2665
  if (device.state !== "connected") {
2574
2666
  throw new BrowserSessionStateError("Attached browser is disconnected");
2575
2667
  }
2576
- const enrollment = await getEnrollment(
2668
+ const enrollment = await getLiveEnrollmentConnection(
2577
2669
  deps.db,
2578
2670
  sourceSession.workspaceId,
2579
2671
  device.enrollmentId,
2580
2672
  );
2581
- if (!enrollment || enrollment.status !== "active") {
2673
+ if (!enrollment || enrollment.status !== "active" || !enrollment.connectionInstanceId) {
2582
2674
  throw new BrowserSessionStateError("Attached browser machine is unavailable");
2583
2675
  }
2584
- assertPlacementInstance(expectedPlacementInstanceId, device.connectionGeneration);
2676
+ if (!enrollment.workspaceRoot) {
2677
+ throw new BrowserSessionStateError(
2678
+ "Attached browser machine has not reported an absolute workspace root",
2679
+ );
2680
+ }
2681
+ const placementInstanceId = attachedEndPlacementInstanceId(
2682
+ operation,
2683
+ expectedPlacementInstanceId,
2684
+ device.connectionGeneration,
2685
+ );
2585
2686
  const built = await buildSelfhostedBackendSession({
2586
2687
  workspaceId: sourceSession.workspaceId,
2587
2688
  agentId: device.enrollmentId,
2689
+ connectionInstanceId: enrollment.connectionInstanceId,
2690
+ workspaceRoot: enrollment.workspaceRoot,
2588
2691
  relay: relayConfigFromSettings(deps.settings),
2589
2692
  controlRpcFactory: () => new NatsControlRpc(async () => deps.bus.getRequestConnection()),
2590
2693
  // Browser-profile generation is the physical controller fence. Epoch 0
@@ -2593,6 +2696,10 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2593
2696
  epoch: 0,
2594
2697
  timeoutMs: deps.settings.sandboxSelfhostedControlTimeoutMs,
2595
2698
  execTimeoutMs: deps.settings.sandboxSelfhostedExecTimeoutMs,
2699
+ operationResourcePolicy: enrollment.operationPolicy,
2700
+ operationResourcePolicySupported:
2701
+ enrollment.agentCapabilities.operationResourcePolicy === true,
2702
+ operationCpuQuotaSupported: enrollment.agentCapabilities.operationCpuQuota === true,
2596
2703
  ...(deps.settings.agentOpStreamEnabled === true &&
2597
2704
  enrollment.opStream === true &&
2598
2705
  deps.bus.getOpStreamConnection
@@ -2609,13 +2716,13 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2609
2716
  return await callback({
2610
2717
  placement: expectedPlacement,
2611
2718
  controllerHostSandboxGroupId: null,
2612
- placementInstanceId: device.connectionGeneration,
2719
+ placementInstanceId,
2613
2720
  session: built.session as unknown as BrowserControlPlacementSession,
2614
2721
  lease: null,
2615
2722
  transport: {
2616
2723
  kind: "attached_chrome",
2617
2724
  deviceId: device.id,
2618
- connectionGeneration: device.connectionGeneration,
2725
+ connectionGeneration: placementInstanceId,
2619
2726
  browserName: device.browserName,
2620
2727
  browserVersion: device.browserVersion,
2621
2728
  },
@@ -2681,8 +2788,10 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2681
2788
  resolved.kind !== "selfhosted" ||
2682
2789
  resolved.sandboxId !== expectedPlacement.sandboxId
2683
2790
  ) {
2684
- throw new BrowserSessionStateError(
2685
- "BrowserSession Connected Machine is not the active placement",
2791
+ return await throwBrowserSourcePlacementChanged(
2792
+ grant,
2793
+ sourceSession.id,
2794
+ expectedPlacement.sandboxId,
2686
2795
  );
2687
2796
  }
2688
2797
  const placementInstanceId = resolved.providerInstanceId ?? expectedPlacement.sandboxId;
@@ -2779,7 +2888,9 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2779
2888
  !lease ||
2780
2889
  (lease.liveness !== "warm" && lease.liveness !== "draining") ||
2781
2890
  lease.instanceId !== binding.placementInstanceId ||
2782
- !lease.controllerDataPlaneUrl
2891
+ !lease.controllerDataPlaneUrl ||
2892
+ (lease.backend === "opensandbox" && deps.settings.openSandboxSignedEndpoints) ||
2893
+ !controllerCachedUrlIsUsable(lease.controllerDataPlaneUrl)
2783
2894
  ) {
2784
2895
  return null;
2785
2896
  }
@@ -2805,8 +2916,29 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2805
2916
  if (!sandboxGroupId || !placement.lease?.instanceId || !placement.session.resolveExposedPort) {
2806
2917
  return placement;
2807
2918
  }
2919
+ if (placement.lease.backend === "opensandbox" && deps.settings.openSandboxSignedEndpoints) {
2920
+ if (!placement.lease.controllerDataPlaneUrl) return placement;
2921
+ const lease = await recordLeaseControllerDataPlaneUrl(deps.db, {
2922
+ accountId: grant.accountId,
2923
+ workspaceId,
2924
+ sandboxGroupId,
2925
+ expectedEpoch: placement.lease.leaseEpoch,
2926
+ expectedInstanceId: placement.lease.instanceId,
2927
+ controllerDataPlaneUrl: null,
2928
+ });
2929
+ return lease ? { ...placement, lease } : placement;
2930
+ }
2808
2931
  const endpoint = await placement.session.resolveExposedPort(BROWSER_CONTROL_PORT);
2809
2932
  const url = buildStreamUrl(endpoint);
2933
+ if (
2934
+ !shouldPersistControllerDataPlaneUrl({
2935
+ backend: placement.lease.backend,
2936
+ signedEndpoints: deps.settings.openSandboxSignedEndpoints,
2937
+ url,
2938
+ })
2939
+ ) {
2940
+ return placement;
2941
+ }
2810
2942
  const lease = await recordLeaseControllerDataPlaneUrl(deps.db, {
2811
2943
  accountId: grant.accountId,
2812
2944
  workspaceId,
@@ -2816,6 +2948,9 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2816
2948
  controllerDataPlaneUrl: url,
2817
2949
  });
2818
2950
  if (!lease) return placement;
2951
+ if (!controllerCacheAllowsHostFetch(url)) {
2952
+ return { ...placement, lease };
2953
+ }
2819
2954
  return { ...placement, session: controllerOnlySession(url), lease };
2820
2955
  }
2821
2956
 
@@ -2831,7 +2966,16 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2831
2966
  const sandboxGroupId = placement.controllerHostSandboxGroupId;
2832
2967
  const cachedUrl = placement.lease?.controllerDataPlaneUrl;
2833
2968
  return await withCachedController({
2834
- cachedUrl: sandboxGroupId ? (cachedUrl ?? null) : null,
2969
+ cachedUrl:
2970
+ sandboxGroupId &&
2971
+ cachedUrl &&
2972
+ !(
2973
+ placement.lease?.backend === "opensandbox" &&
2974
+ routeDeps.settings.openSandboxSignedEndpoints
2975
+ ) &&
2976
+ controllerCachedUrlIsUsable(cachedUrl)
2977
+ ? cachedUrl
2978
+ : null,
2835
2979
  createCachedClient: (url) =>
2836
2980
  new BrowserControlClient(controllerOnlySession(url), { adminToken }),
2837
2981
  prepareCachedClient: async (client) => {
@@ -2889,6 +3033,17 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2889
3033
  record.sourceSessionId,
2890
3034
  authorizationOperation,
2891
3035
  );
3036
+ let sourceSession: Session | null = null;
3037
+ if (record.session.placement?.kind === "connected_machine") {
3038
+ sourceSession = await requireSourceSession(deps, workspaceId, record.sourceSessionId);
3039
+ if (sourceSession.activeSandboxId !== record.session.placement.sandboxId) {
3040
+ return await throwBrowserSourcePlacementChanged(
3041
+ grant,
3042
+ record.sourceSessionId,
3043
+ record.session.placement.sandboxId,
3044
+ );
3045
+ }
3046
+ }
2892
3047
  if (record.session.lifecycle !== "active" || !record.session.controller) {
2893
3048
  throw new BrowserSessionStateError("BrowserSession is not active");
2894
3049
  }
@@ -2973,7 +3128,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2973
3128
  }
2974
3129
  }
2975
3130
 
2976
- const sourceSession = await requireSourceSession(deps, workspaceId, record.sourceSessionId);
3131
+ sourceSession ??= await requireSourceSession(deps, workspaceId, record.sourceSessionId);
2977
3132
  return await withBrowserPlacement(
2978
3133
  sourceSession,
2979
3134
  grant,
@@ -2993,6 +3148,25 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2993
3148
  }
2994
3149
  }
2995
3150
 
3151
+ async function throwBrowserSourcePlacementChanged(
3152
+ grant: AccessGrant,
3153
+ sourceSessionId: string,
3154
+ connectedSandboxId: string,
3155
+ ): Promise<never> {
3156
+ const reconciliation = await terminalizeStaleConnectedInteractionPlacement(deps.db, {
3157
+ accountId: grant.accountId,
3158
+ workspaceId: grant.workspaceId,
3159
+ sourceSessionId,
3160
+ connectedSandboxId,
3161
+ });
3162
+ if (reconciliation.sourcePlacementChanged) {
3163
+ throw sourcePlacementChangedApiError("browser_session");
3164
+ }
3165
+ throw new BrowserSessionStateError(
3166
+ "BrowserSession source placement changed during the request; retry",
3167
+ );
3168
+ }
3169
+
2996
3170
  async function recoverActiveBrowserController(
2997
3171
  routeDeps: ApiRouteDeps,
2998
3172
  grant: AccessGrant,
@@ -3002,11 +3176,12 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
3002
3176
  client: BrowserControlClient,
3003
3177
  tokens: ReturnType<typeof deriveBrowserSessionControllerTokens>,
3004
3178
  ): Promise<void> {
3005
- const linkedComputer = await requireLinkedComputerBinding(
3179
+ const linkedComputer = await ensureLinkedComputerController(
3006
3180
  routeDeps,
3007
3181
  grant,
3008
3182
  record.session,
3009
3183
  placement,
3184
+ client,
3010
3185
  );
3011
3186
  const networkRoute = await resolveActiveBrowserNetworkRouteLaunch({
3012
3187
  deps: routeDeps,
@@ -3015,6 +3190,11 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
3015
3190
  placement: placement.placement,
3016
3191
  });
3017
3192
  try {
3193
+ await ensureHeadedBrowserDisplayStack(
3194
+ placement.session,
3195
+ record.session.headless,
3196
+ linkedComputer !== null,
3197
+ );
3018
3198
  await client.createSession({
3019
3199
  browserSessionId: record.session.id,
3020
3200
  controllerGeneration: binding.controllerGeneration,
@@ -3065,7 +3245,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
3065
3245
  grant,
3066
3246
  record,
3067
3247
  placement,
3068
- requestOrigin(context, deps.settings.corsAllowOriginRegex),
3248
+ requestOrigin(context, deps.settings),
3069
3249
  );
3070
3250
  await endCapturedBrowserController(client, browserSessionId, binding);
3071
3251
  await clearSuspendedBrowserSessionController(deps.db, {
@@ -3087,6 +3267,21 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
3087
3267
  }
3088
3268
  }
3089
3269
 
3270
+ function sourcePlacementChangedApiError(interactionResource: "browser_session"): ApiHttpError {
3271
+ return new ApiHttpError(409, {
3272
+ code: "conflict",
3273
+ message:
3274
+ "This browser belonged to a previous task placement and was retired. Open a new browser.",
3275
+ retryable: false,
3276
+ outcomeUnknown: false,
3277
+ details: {
3278
+ interactionResource,
3279
+ interactionFailureCode: "source_placement_changed",
3280
+ interactionLifecycle: "lost",
3281
+ },
3282
+ });
3283
+ }
3284
+
3090
3285
  function browserCreateInput(
3091
3286
  grant: AccessGrant,
3092
3287
  workspaceId: string,
@@ -3235,11 +3430,31 @@ function assertCreateReplay(
3235
3430
  }
3236
3431
  }
3237
3432
 
3238
- async function requireLinkedComputerBinding(
3433
+ async function ensureHeadedBrowserDisplayStack(
3434
+ session: BrowserControlPlacementSession,
3435
+ headless: boolean,
3436
+ linkedComputer: boolean,
3437
+ ): Promise<void> {
3438
+ if (!browserNeedsStandaloneDisplayStack({ headless, linkedComputer })) return;
3439
+ if (typeof session.exec !== "function" && typeof session.execCommand !== "function") return;
3440
+ await ensureDisplayStack(session, {
3441
+ telemetryContext: { callerKind: "viewer" },
3442
+ });
3443
+ }
3444
+
3445
+ export function browserNeedsStandaloneDisplayStack(input: {
3446
+ headless: boolean;
3447
+ linkedComputer: boolean;
3448
+ }): boolean {
3449
+ return !input.headless && !input.linkedComputer;
3450
+ }
3451
+
3452
+ async function ensureLinkedComputerController(
3239
3453
  deps: ApiRouteDeps,
3240
3454
  grant: AccessGrant,
3241
3455
  browser: BrowserSessionValue,
3242
3456
  placement: BrowserPlacement,
3457
+ client: BrowserControlClient,
3243
3458
  ): Promise<{ computerSessionId: string; controllerGeneration: string } | null> {
3244
3459
  if (!browser.linkedComputerSessionId) return null;
3245
3460
  const record = await getComputerSessionControlRecord(deps.db, {
@@ -3257,10 +3472,42 @@ async function requireLinkedComputerBinding(
3257
3472
  "Linked ComputerSession is not active on the browser placement",
3258
3473
  );
3259
3474
  }
3260
- return {
3475
+ const reference = {
3261
3476
  computerSessionId: record.session.id,
3262
3477
  controllerGeneration: record.session.controller.controllerGeneration,
3263
3478
  };
3479
+ const tokens = deriveComputerSessionControllerTokens({
3480
+ rootSecret: browserAuthorityRoot(deps),
3481
+ accountId: grant.accountId,
3482
+ workspaceId: browser.workspaceId,
3483
+ placement: record.session.placement,
3484
+ placementInstanceId: placement.placementInstanceId,
3485
+ computerSessionId: record.session.id,
3486
+ controllerGeneration: record.session.controller.controllerGeneration,
3487
+ tokenGeneration: record.tokenGeneration,
3488
+ });
3489
+ const sessionClient = client.computerSessionClient({ reference, ...tokens });
3490
+ try {
3491
+ await sessionClient.heartbeat();
3492
+ } catch (error) {
3493
+ if (!isMissingLinkedComputerControllerSession(error)) throw error;
3494
+ await client.createComputerSession({
3495
+ ...reference,
3496
+ tokenGeneration: record.tokenGeneration,
3497
+ ...tokens,
3498
+ });
3499
+ }
3500
+ return reference;
3501
+ }
3502
+
3503
+ function isMissingLinkedComputerControllerSession(error: unknown): boolean {
3504
+ if (!(error instanceof BrowserControlRequestError)) return false;
3505
+ if (error.status === 401 && error.error.code === "permission_denied") return true;
3506
+ return (
3507
+ error.status === 404 &&
3508
+ error.error.code === "resource_not_found" &&
3509
+ ["computer session not found", "computer session is not active"].includes(error.error.message)
3510
+ );
3264
3511
  }
3265
3512
 
3266
3513
  async function requireBrowserRevisionRestoreAuthority(
@@ -3480,6 +3727,7 @@ async function ensureInteractionHolder(
3480
3727
  if (!placement.lease?.instanceId) {
3481
3728
  throw new BrowserSessionStateError("BrowserSession lease placement is unavailable");
3482
3729
  }
3730
+ const sandboxRuntime = await resolveSessionSandboxRuntime(deps.db, deps.settings, sourceSession);
3483
3731
  const acquired = await acquireLease(deps.db, {
3484
3732
  accountId: grant.accountId,
3485
3733
  workspaceId: sourceSession.workspaceId,
@@ -3489,8 +3737,8 @@ async function ensureInteractionHolder(
3489
3737
  subjectId: sourceSession.id,
3490
3738
  backend: placement.lease.backend,
3491
3739
  os: placement.lease.os,
3492
- image: placement.lease.image,
3493
- rigVersionId: placement.lease.rigVersionId,
3740
+ image: sandboxRuntime.image,
3741
+ rigVersionId: sourceSession.rigVersionId,
3494
3742
  leaseTtlMs: deps.settings.sandboxLeaseTtlMs,
3495
3743
  expectedEpoch: placement.lease.leaseEpoch,
3496
3744
  waitSignal,
@@ -3512,6 +3760,11 @@ async function ensureInteractionHolder(
3512
3760
  ).catch(() => undefined);
3513
3761
  throw new BrowserSessionStateError("BrowserSession placement fence changed; retry");
3514
3762
  }
3763
+ await renewSandboxProviderExpiration({
3764
+ backend: acquired.lease.backend as ApiRouteDeps["settings"]["sandboxBackend"],
3765
+ settings: deps.settings,
3766
+ instanceId: acquired.lease.instanceId,
3767
+ }).catch(() => false);
3515
3768
  return true;
3516
3769
  }
3517
3770
 
@@ -3902,32 +4155,8 @@ function isUuid(value: unknown): value is string {
3902
4155
  );
3903
4156
  }
3904
4157
 
3905
- function requestOrigin(context: Context, allowedPattern: string): string | null {
3906
- return validateBrowserRequestOrigin(context.req.header("origin"), allowedPattern);
3907
- }
3908
-
3909
- export function validateBrowserRequestOrigin(
3910
- value: string | undefined,
3911
- allowedPattern: string,
3912
- ): string | null {
3913
- if (!value) return null;
3914
- let url: URL;
3915
- try {
3916
- url = new URL(value);
3917
- } catch {
3918
- throw new HTTPException(400, { message: "invalid request origin" });
3919
- }
3920
- if (
3921
- url.origin === "null" ||
3922
- (url.protocol !== "http:" && url.protocol !== "https:") ||
3923
- url.origin !== value
3924
- ) {
3925
- throw new HTTPException(400, { message: "invalid request origin" });
3926
- }
3927
- if (!allowedCorsOrigin(allowedPattern, url.origin)) {
3928
- throw new HTTPException(403, { message: "request origin is not allowed" });
3929
- }
3930
- return url.origin;
4158
+ function requestOrigin(context: Context, settings: ApiRouteDeps["settings"]): string | null {
4159
+ return validateInteractionRequestOrigin(context.req.header("origin"), settings);
3931
4160
  }
3932
4161
 
3933
4162
  export function interactionActorForGrant(
@@ -3955,6 +4184,20 @@ function assertPlacementInstance(expected: string | null, actual: string): void
3955
4184
  }
3956
4185
  }
3957
4186
 
4187
+ /** End keeps the original controller token fence so a later Chrome generation
4188
+ * can still dispose the stale browserd session. */
4189
+ function attachedEndPlacementInstanceId(
4190
+ operation: ChannelAOperation,
4191
+ expectedPlacementInstanceId: string | null,
4192
+ liveGeneration: string,
4193
+ ): string {
4194
+ if (operation === "browser.end" && expectedPlacementInstanceId) {
4195
+ return expectedPlacementInstanceId;
4196
+ }
4197
+ assertPlacementInstance(expectedPlacementInstanceId, liveGeneration);
4198
+ return liveGeneration;
4199
+ }
4200
+
3958
4201
  function isTerminalOperation(state: string): boolean {
3959
4202
  return state === "completed" || state === "failed" || state === "outcome_unknown";
3960
4203
  }
@@ -4410,9 +4653,14 @@ async function finalizeBrowserDownloadFile(
4410
4653
  if (upload.expiresAt.getTime() <= Date.now()) {
4411
4654
  throw new InteractionResourceStateError("Browser download file upload authority expired");
4412
4655
  }
4413
- const head = await objectStorage.headFile(upload.file).catch(() => {
4414
- throw new HTTPException(503, { message: "browser download object is not available" });
4656
+ const pendingFile = upload.file;
4657
+ const head = await retryWhileMissing(async () => {
4658
+ if (!(await objectStorage.fileExists(pendingFile))) return null;
4659
+ return await objectStorage.headFile(pendingFile);
4415
4660
  });
4661
+ if (!head) {
4662
+ throw new HTTPException(503, { message: "browser download object is not available" });
4663
+ }
4416
4664
  if (
4417
4665
  Number(head.ContentLength ?? -1) !== upload.file.sizeBytes ||
4418
4666
  (head.ContentType !== undefined && head.ContentType !== upload.file.contentType) ||
@@ -4453,6 +4701,8 @@ async function recordBrowserDownloadFileUsage(
4453
4701
  }
4454
4702
 
4455
4703
  function browserRouteError(error: unknown): HTTPException {
4704
+ const connectedMachineError = interactionControlApiError(error, "browser");
4705
+ if (connectedMachineError) return connectedMachineError;
4456
4706
  if (error instanceof HTTPException) return error;
4457
4707
  if (error instanceof BrowserSessionNotFoundError) {
4458
4708
  return new HTTPException(404, { message: error.message, cause: error });