@opengeni/api-router 0.30.1 → 2.1.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 (167) 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-2PQMSRCB.js → chunk-QKDFBBUE.js} +29110 -18137
  5. package/dist/chunk-QKDFBBUE.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-office-import.d.ts +7 -4
  13. package/dist/editable-artifact-websocket.d.ts +5 -15
  14. package/dist/editable-artifact-workspace-files.d.ts +1 -1
  15. package/dist/http/api-error.d.ts +10 -0
  16. package/dist/http/cors.d.ts +5 -0
  17. package/dist/http/interaction-control-error.d.ts +13 -0
  18. package/dist/http/sse.d.ts +2 -3
  19. package/dist/index.d.ts +5 -2
  20. package/dist/index.js +364 -86
  21. package/dist/index.js.map +1 -1
  22. package/dist/integrations/atlassian.d.ts +17 -0
  23. package/dist/integrations/fiken.d.ts +7 -2
  24. package/dist/integrations/google-drive.d.ts +19 -0
  25. package/dist/integrations/oauth-client.d.ts +22 -3
  26. package/dist/integrations/oauth-profiles.d.ts +195 -0
  27. package/dist/integrations/personal-github-repositories.d.ts +45 -0
  28. package/dist/integrations/personal-github.d.ts +40 -0
  29. package/dist/integrations/pr-review-provider.d.ts +22 -0
  30. package/dist/integrations/provider-oauth.d.ts +8 -0
  31. package/dist/integrations/slack-app-home.d.ts +24 -0
  32. package/dist/integrations/slack-bot.d.ts +149 -9
  33. package/dist/integrations/slack-interactions.d.ts +10 -4
  34. package/dist/integrations/slack-routing.d.ts +120 -0
  35. package/dist/integrations/social-oauth.d.ts +5 -0
  36. package/dist/interaction-frame-proxy.d.ts +68 -0
  37. package/dist/mcp/company-brain-governed-writes.d.ts +26 -0
  38. package/dist/mcp/company-profile-agent-admin.d.ts +44 -0
  39. package/dist/mcp/documents.d.ts +1 -0
  40. package/dist/mcp/remember.d.ts +28 -0
  41. package/dist/mcp/request-abort.d.ts +19 -0
  42. package/dist/mcp/scheduled-task-view.d.ts +4 -4
  43. package/dist/mcp/server.d.ts +15 -2
  44. package/dist/mcp/session-view.d.ts +4 -0
  45. package/dist/mcp/session-wait.d.ts +137 -0
  46. package/dist/routes/automations.d.ts +4 -0
  47. package/dist/routes/billing.d.ts +5 -0
  48. package/dist/routes/browser-sessions.d.ts +17 -3
  49. package/dist/routes/company-brain.d.ts +3 -0
  50. package/dist/routes/company-profile.d.ts +2 -1
  51. package/dist/routes/connection-authorities.d.ts +3 -0
  52. package/dist/routes/files.d.ts +4 -0
  53. package/dist/routes/personal-github-git-broker.d.ts +29 -0
  54. package/dist/routes/personal-github.d.ts +3 -0
  55. package/dist/routes/pr-review.d.ts +3 -0
  56. package/dist/routes/sessions.d.ts +1 -0
  57. package/dist/routes/user-resource-authorities.d.ts +3 -0
  58. package/dist/routes/workspace-capture.d.ts +10 -2
  59. package/dist/routes/workspace-learning.d.ts +3 -0
  60. package/dist/routes/workspaces.d.ts +19 -0
  61. package/dist/sandbox/channel-a.d.ts +11 -1
  62. package/dist/sandbox/connection-authority.d.ts +3 -0
  63. package/dist/sandbox/enrollment.d.ts +2 -0
  64. package/dist/sandbox/machines.d.ts +2 -2
  65. package/dist/sandbox/metrics-ingestion.d.ts +10 -20
  66. package/dist/sandbox/viewer.d.ts +24 -1
  67. package/dist/sandbox-file-artifacts.d.ts +11 -0
  68. package/dist/scheduled-task-deletion.d.ts +14 -0
  69. package/dist/slack-reaction-files.d.ts +1 -1
  70. package/dist/temporal-schedule-cleanup.d.ts +1 -0
  71. package/package.json +20 -18
  72. package/src/api-websocket.ts +23 -0
  73. package/src/app.ts +160 -27
  74. package/src/auth/managed-auth.ts +25 -3
  75. package/src/codemode.ts +53 -23
  76. package/src/codex-realtime.ts +8 -2
  77. package/src/company-brain-okf.ts +340 -0
  78. package/src/connected-machine-computer-access.ts +33 -0
  79. package/src/connection-authority-owner.ts +61 -0
  80. package/src/connection-ownership.ts +180 -0
  81. package/src/controller-data-plane.ts +47 -0
  82. package/src/editable-artifact-native-kernel.ts +26 -15
  83. package/src/editable-artifact-office-import.ts +77 -9
  84. package/src/editable-artifact-production.ts +13 -7
  85. package/src/editable-artifact-websocket.ts +11 -18
  86. package/src/editable-artifact-workspace-files.ts +31 -6
  87. package/src/http/api-error.ts +28 -0
  88. package/src/http/auth.ts +4 -0
  89. package/src/http/cors.ts +35 -0
  90. package/src/http/interaction-control-error.ts +164 -0
  91. package/src/http/sse.ts +112 -66
  92. package/src/index.ts +46 -6
  93. package/src/integrations/atlassian.ts +146 -35
  94. package/src/integrations/fiken.ts +102 -22
  95. package/src/integrations/google-drive.ts +416 -101
  96. package/src/integrations/oauth-client.ts +272 -143
  97. package/src/integrations/oauth-profiles.ts +477 -0
  98. package/src/integrations/personal-github-repositories.ts +445 -0
  99. package/src/integrations/personal-github.ts +705 -0
  100. package/src/integrations/pr-review-provider.ts +246 -0
  101. package/src/integrations/provider-oauth.ts +121 -5
  102. package/src/integrations/slack-app-home.ts +300 -0
  103. package/src/integrations/slack-bot.ts +337 -57
  104. package/src/integrations/slack-interactions.ts +1274 -214
  105. package/src/integrations/slack-routing.ts +324 -0
  106. package/src/integrations/social-oauth.ts +25 -0
  107. package/src/interaction-frame-proxy.ts +409 -0
  108. package/src/mcp/company-brain-governed-writes.ts +262 -0
  109. package/src/mcp/company-profile-agent-admin.ts +205 -0
  110. package/src/mcp/documents.ts +37 -6
  111. package/src/mcp/files.ts +22 -2
  112. package/src/mcp/remember.ts +181 -0
  113. package/src/mcp/request-abort.ts +44 -0
  114. package/src/mcp/scheduled-task-view.ts +1 -0
  115. package/src/mcp/server.ts +719 -369
  116. package/src/mcp/session-view.ts +54 -0
  117. package/src/mcp/session-wait.ts +554 -0
  118. package/src/model-catalog.ts +20 -12
  119. package/src/routes/api-integrations.ts +4 -0
  120. package/src/routes/api-keys.ts +1 -0
  121. package/src/routes/automations.ts +534 -0
  122. package/src/routes/billing.ts +57 -1
  123. package/src/routes/browser-sessions.ts +388 -98
  124. package/src/routes/channels.ts +17 -1
  125. package/src/routes/codex.ts +50 -0
  126. package/src/routes/company-brain.ts +367 -0
  127. package/src/routes/company-profile.ts +1 -1
  128. package/src/routes/computer-sessions.ts +320 -93
  129. package/src/routes/connection-authorities.ts +139 -0
  130. package/src/routes/connections.ts +145 -34
  131. package/src/routes/documents.ts +221 -3
  132. package/src/routes/editable-artifacts.ts +11 -3
  133. package/src/routes/enrollments.ts +116 -26
  134. package/src/routes/environments.ts +127 -55
  135. package/src/routes/files.ts +152 -27
  136. package/src/routes/install.ts +82 -28
  137. package/src/routes/integration-facets.ts +29 -0
  138. package/src/routes/interaction-resources.ts +20 -2
  139. package/src/routes/machines.ts +275 -13
  140. package/src/routes/organization-memberships.ts +717 -28
  141. package/src/routes/packs.ts +4 -3
  142. package/src/routes/personal-github-git-broker.ts +785 -0
  143. package/src/routes/personal-github.ts +319 -0
  144. package/src/routes/pr-review.ts +531 -0
  145. package/src/routes/rigs.ts +104 -39
  146. package/src/routes/scheduled-tasks.ts +19 -21
  147. package/src/routes/sessions.ts +777 -40
  148. package/src/routes/social.ts +14 -2
  149. package/src/routes/supergrok.ts +23 -4
  150. package/src/routes/transcription-recordings.ts +8 -2
  151. package/src/routes/user-resource-authorities.ts +138 -0
  152. package/src/routes/workspace-artifacts.ts +4 -1
  153. package/src/routes/workspace-capture.ts +21 -0
  154. package/src/routes/workspace-learning.ts +228 -0
  155. package/src/routes/workspaces.ts +59 -6
  156. package/src/sandbox/auth-callout.ts +38 -12
  157. package/src/sandbox/channel-a.ts +242 -19
  158. package/src/sandbox/connection-authority.ts +3 -0
  159. package/src/sandbox/enrollment.ts +15 -3
  160. package/src/sandbox/machines.ts +186 -64
  161. package/src/sandbox/metrics-ingestion.ts +220 -58
  162. package/src/sandbox/viewer.ts +243 -19
  163. package/src/sandbox-file-artifacts.ts +329 -0
  164. package/src/scheduled-task-deletion.ts +127 -0
  165. package/src/slack-reaction-files.ts +16 -5
  166. package/src/temporal-schedule-cleanup.ts +13 -0
  167. package/dist/chunk-2PQMSRCB.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,12 +96,13 @@ 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
- getFiles,
105
+ getFilesForSubject,
104
106
  getFileUpload,
105
107
  getAuthRun,
106
108
  getExternalAuthPreparation,
@@ -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,9 +145,11 @@ import {
142
145
  recordWorkspaceUsage,
143
146
  requireLimit,
144
147
  relayConfigFromSettings,
148
+ resolveSessionSandboxRuntime,
145
149
  SessionAuthorizationDeniedError,
146
150
  SessionAuthorizationUnavailableError,
147
151
  type ApiRouteDeps,
152
+ type ResolvedSessionAuthorization,
148
153
  } from "@opengeni/core";
149
154
  import {
150
155
  BrowserControlProtocolError,
@@ -157,16 +162,19 @@ import {
157
162
  buildStreamUrl,
158
163
  exposedPortEndpointFromUrl,
159
164
  buildSelfhostedBackendSession,
165
+ ensureDisplayStack,
160
166
  mintStreamToken,
161
167
  NatsControlRpc,
162
168
  NatsOpStreamTransport,
163
169
  provisionBrowserControlClient,
170
+ renewSandboxProviderExpiration,
164
171
  type BrowserControlPlacementSession,
165
172
  type PlacementBrowserStateCaptureReceipt,
166
173
  type PlacementBrowserNetworkRoute,
167
174
  type PlacementBrowserTransport,
168
175
  type RestorePlacementBrowserStateInput,
169
176
  } from "@opengeni/runtime/sandbox";
177
+ import { retryWhileMissing } from "@opengeni/storage";
170
178
  import type { Context, Hono } from "hono";
171
179
  import { HTTPException } from "hono/http-exception";
172
180
  import type { ContentfulStatusCode } from "hono/utils/http-status";
@@ -175,8 +183,14 @@ import {
175
183
  deriveBrowserNetworkRouteAuthorityDigest,
176
184
  deriveBrowserSessionControllerTokens,
177
185
  deriveBrowserViewGrantToken,
186
+ deriveComputerSessionControllerTokens,
178
187
  } from "../browser-controller-authority";
179
- import { withCachedController } from "../controller-data-plane";
188
+ import {
189
+ controllerCacheAllowsHostFetch,
190
+ controllerCachedUrlIsUsable,
191
+ shouldPersistControllerDataPlaneUrl,
192
+ withCachedController,
193
+ } from "../controller-data-plane";
180
194
  import { withInteractionHolderHeartbeat } from "../interaction-holder-heartbeat";
181
195
  import {
182
196
  browserStateArtifactAad,
@@ -191,7 +205,13 @@ import {
191
205
  resolveProtectedAuthFieldValues,
192
206
  } from "../browser-auth-broker";
193
207
  import { managedNetworkRouteForPlacement } from "../browser-network-route";
194
- 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";
195
215
  import {
196
216
  observeAuthMutation,
197
217
  observeBrowserActionResult,
@@ -297,7 +317,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
297
317
  const request = await parseJsonBody(context, CreateBrowserSessionRequest);
298
318
  const startedAtMs = performance.now();
299
319
  await authorizeSourceSession(deps, grant, request.sessionId, "session.control");
300
- const origin = requestOrigin(context, deps.settings.corsAllowOriginRegex);
320
+ const origin = requestOrigin(context, deps.settings);
301
321
  const authority = browserAuthorityRoot(deps);
302
322
 
303
323
  try {
@@ -324,6 +344,20 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
324
344
  return context.json(parsed, 200);
325
345
  }
326
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
+
327
361
  const sourceSession = await requireSourceSession(deps, workspaceId, request.sessionId);
328
362
  const response = await withBrowserPlacement(
329
363
  sourceSession,
@@ -400,12 +434,6 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
400
434
  });
401
435
  const preparedSession = prepared.session;
402
436
  const controllerGeneration = requireOperationGeneration(record);
403
- const linkedComputer = await requireLinkedComputerBinding(
404
- deps,
405
- grant,
406
- prepared.session,
407
- placement,
408
- );
409
437
  const adminToken = deriveBrowserControllerAdminToken({
410
438
  rootSecret: authority,
411
439
  accountId: grant.accountId,
@@ -431,8 +459,15 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
431
459
  placement,
432
460
  adminToken,
433
461
  origin,
434
- async (client) =>
435
- 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(
436
471
  deps,
437
472
  {
438
473
  grant,
@@ -443,8 +478,13 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
443
478
  resourceId: preparedSession.id,
444
479
  controllerGeneration,
445
480
  },
446
- async () =>
447
- await client.createSession({
481
+ async () => {
482
+ await ensureHeadedBrowserDisplayStack(
483
+ placement.session,
484
+ preparedSession.headless,
485
+ linkedComputer !== null,
486
+ );
487
+ return await client.createSession({
448
488
  browserSessionId: preparedSession.id,
449
489
  controllerGeneration,
450
490
  tokenGeneration: record.tokenGeneration,
@@ -455,8 +495,10 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
455
495
  ...(networkRoute ? { networkRoute } : {}),
456
496
  ...(request.initialUrl ? { initialUrl: request.initialUrl } : {}),
457
497
  ...(restore ? { restore } : {}),
458
- }),
459
- ),
498
+ });
499
+ },
500
+ );
501
+ },
460
502
  );
461
503
  await cacheBrowserControllerPlacement(grant, workspaceId, placement).catch(
462
504
  () => placement,
@@ -927,7 +969,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
927
969
  browserSessionId,
928
970
  "session.control",
929
971
  "browser.action",
930
- async ({ sessionClient, binding, record }) => {
972
+ async ({ sessionClient, binding, record, sourceAuthorization }) => {
931
973
  const command = BrowserActionCommand.parse({
932
974
  protocolVersion: BROWSER_CONTROL_PROTOCOL_VERSION,
933
975
  operationId: request.operationId,
@@ -957,27 +999,25 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
957
999
  message: "object storage is not configured",
958
1000
  });
959
1001
  }
960
- const files = await getFiles(deps.db, workspaceId, workspaceFileIds);
961
- const byId = new Map(files.map((file) => [file.id, file]));
962
- const ordered = workspaceFileIds.map((fileId) => byId.get(fileId));
963
- if (ordered.some((file) => !file)) {
964
- throw new HTTPException(404, { message: "workspace file not found" });
965
- }
966
- if (ordered.some((file) => file!.status !== "ready")) {
967
- throw new HTTPException(409, { message: "workspace file is not ready" });
968
- }
1002
+ const files = await getFilesForSubject(deps.db, {
1003
+ accountId: grant.accountId,
1004
+ workspaceId,
1005
+ subjectId: browserFileAuthoritySubjectId(grant, sourceAuthorization),
1006
+ fileIds: workspaceFileIds,
1007
+ });
1008
+ const ordered = requireAuthorizedBrowserUploadFiles(workspaceFileIds, files);
969
1009
  const authorities = await Promise.all(
970
1010
  ordered.map(async (file) => {
971
1011
  const signed = await deps.objectStorage!.createGetUrl({
972
- key: file!.objectKey,
1012
+ key: file.objectKey,
973
1013
  expiresInSeconds: BROWSER_WORKSPACE_FILE_AUTHORITY_TTL_SECONDS,
974
1014
  audience: signedUrlAudienceForPlacement(record.session.placement),
975
1015
  });
976
1016
  return {
977
- fileId: file!.id,
978
- safeFilename: browserUploadFilename(file!.safeFilename),
979
- sizeBytes: file!.sizeBytes,
980
- sha256: browserUploadSha256(file!.sha256),
1017
+ fileId: file.id,
1018
+ safeFilename: browserUploadFilename(file.safeFilename),
1019
+ sizeBytes: file.sizeBytes,
1020
+ sha256: browserUploadSha256(file.sha256),
981
1021
  download: {
982
1022
  url: signed.url,
983
1023
  expiresAt: signed.expiresAt.toISOString(),
@@ -1664,7 +1704,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
1664
1704
  context,
1665
1705
  "stream:view",
1666
1706
  );
1667
- const origin = requestOrigin(context, deps.settings.corsAllowOriginRegex);
1707
+ const origin = requestOrigin(context, deps.settings);
1668
1708
  const request = await parseJsonBody(context, BrowserSessionAttachmentRequest);
1669
1709
  const result = await withActiveBrowserController(
1670
1710
  context,
@@ -1720,6 +1760,18 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
1720
1760
  });
1721
1761
  const stream = relayed
1722
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
+ }
1723
1775
  const relayToken = await mintStreamToken(relaySecret!, {
1724
1776
  workspaceId,
1725
1777
  sessionId: record.sourceSessionId,
@@ -1727,6 +1779,8 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
1727
1779
  leaseEpoch: record.tokenGeneration,
1728
1780
  port: relayed.channel.port,
1729
1781
  ttlSeconds: request.expiresInSeconds,
1782
+ subjectId: grant.subjectId,
1783
+ authorityEpoch: relayAuthorityEpoch,
1730
1784
  });
1731
1785
  return {
1732
1786
  kind: "relay" as const,
@@ -1741,14 +1795,44 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
1741
1795
  },
1742
1796
  };
1743
1797
  })()
1744
- : {
1745
- kind: "direct_websocket" as const,
1746
- url: await client.frameStreamUrl(reference, request.targetId, request.stream),
1747
- protocols: [
1798
+ : await (async () => {
1799
+ const protocols = [
1748
1800
  BROWSER_CONTROL_WEBSOCKET_PROTOCOL,
1749
1801
  `${BROWSER_CONTROL_WEBSOCKET_BEARER_PREFIX}${token}`,
1750
- ],
1751
- };
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
+ })();
1752
1836
  return BrowserSessionAttachment.parse({
1753
1837
  browserSessionId,
1754
1838
  controllerGeneration: binding.controllerGeneration,
@@ -1959,7 +2043,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
1959
2043
  const browserSessionId = requireUuidParam(context, "browserSessionId");
1960
2044
  const request = await parseJsonBody(context, BrowserSessionLifecycleRequest);
1961
2045
  const startedAtMs = performance.now();
1962
- const origin = requestOrigin(context, deps.settings.corsAllowOriginRegex);
2046
+ const origin = requestOrigin(context, deps.settings);
1963
2047
  try {
1964
2048
  const before = await getBrowserSessionControlRecord(deps.db, {
1965
2049
  accountId: grant.accountId,
@@ -2185,7 +2269,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2185
2269
  const browserSessionId = requireUuidParam(context, "browserSessionId");
2186
2270
  const request = await parseJsonBody(context, BrowserSessionLifecycleRequest);
2187
2271
  const startedAtMs = performance.now();
2188
- const origin = requestOrigin(context, deps.settings.corsAllowOriginRegex);
2272
+ const origin = requestOrigin(context, deps.settings);
2189
2273
  let restore: RestorePlacementBrowserStateInput | null = null;
2190
2274
  try {
2191
2275
  let before = await getBrowserSessionControlRecord(deps.db, {
@@ -2304,12 +2388,6 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2304
2388
  throw error;
2305
2389
  });
2306
2390
  const controllerGeneration = requireOperationGeneration(record);
2307
- const linkedComputer = await requireLinkedComputerBinding(
2308
- deps,
2309
- grant,
2310
- prepared.session,
2311
- placement,
2312
- );
2313
2391
  const adminToken = deriveBrowserControllerAdminToken({
2314
2392
  rootSecret: authority,
2315
2393
  accountId: grant.accountId,
@@ -2333,6 +2411,13 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2333
2411
  ...(origin ? { allowedOrigins: [origin] } : {}),
2334
2412
  });
2335
2413
  try {
2414
+ const linkedComputer = await ensureLinkedComputerController(
2415
+ deps,
2416
+ grant,
2417
+ prepared.session,
2418
+ placement,
2419
+ client,
2420
+ );
2336
2421
  await withInteractionHolderHeartbeat(
2337
2422
  deps,
2338
2423
  {
@@ -2344,8 +2429,13 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2344
2429
  resourceId: browserSessionId,
2345
2430
  controllerGeneration,
2346
2431
  },
2347
- async () =>
2348
- await client.createSession({
2432
+ async () => {
2433
+ await ensureHeadedBrowserDisplayStack(
2434
+ placement.session,
2435
+ prepared.session.headless,
2436
+ linkedComputer !== null,
2437
+ );
2438
+ return await client.createSession({
2349
2439
  browserSessionId,
2350
2440
  controllerGeneration,
2351
2441
  tokenGeneration: record.tokenGeneration,
@@ -2355,7 +2445,8 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2355
2445
  ...(linkedComputer ? { linkedComputer } : {}),
2356
2446
  ...(networkRoute ? { networkRoute } : {}),
2357
2447
  restore: restore!,
2358
- }),
2448
+ });
2449
+ },
2359
2450
  );
2360
2451
  } catch (error) {
2361
2452
  if (!isDefiniteBrowserControllerFailure(error)) throw error;
@@ -2427,7 +2518,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2427
2518
  const browserSessionId = requireUuidParam(context, "browserSessionId");
2428
2519
  const request = await parseJsonBody(context, BrowserSessionLifecycleRequest);
2429
2520
  const startedAtMs = performance.now();
2430
- const origin = requestOrigin(context, deps.settings.corsAllowOriginRegex);
2521
+ const origin = requestOrigin(context, deps.settings);
2431
2522
  try {
2432
2523
  const before = await getBrowserSessionControlRecord(deps.db, {
2433
2524
  accountId: grant.accountId,
@@ -2574,18 +2665,29 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2574
2665
  if (device.state !== "connected") {
2575
2666
  throw new BrowserSessionStateError("Attached browser is disconnected");
2576
2667
  }
2577
- const enrollment = await getEnrollment(
2668
+ const enrollment = await getLiveEnrollmentConnection(
2578
2669
  deps.db,
2579
2670
  sourceSession.workspaceId,
2580
2671
  device.enrollmentId,
2581
2672
  );
2582
- if (!enrollment || enrollment.status !== "active") {
2673
+ if (!enrollment || enrollment.status !== "active" || !enrollment.connectionInstanceId) {
2583
2674
  throw new BrowserSessionStateError("Attached browser machine is unavailable");
2584
2675
  }
2585
- 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
+ );
2586
2686
  const built = await buildSelfhostedBackendSession({
2587
2687
  workspaceId: sourceSession.workspaceId,
2588
2688
  agentId: device.enrollmentId,
2689
+ connectionInstanceId: enrollment.connectionInstanceId,
2690
+ workspaceRoot: enrollment.workspaceRoot,
2589
2691
  relay: relayConfigFromSettings(deps.settings),
2590
2692
  controlRpcFactory: () => new NatsControlRpc(async () => deps.bus.getRequestConnection()),
2591
2693
  // Browser-profile generation is the physical controller fence. Epoch 0
@@ -2594,6 +2696,10 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2594
2696
  epoch: 0,
2595
2697
  timeoutMs: deps.settings.sandboxSelfhostedControlTimeoutMs,
2596
2698
  execTimeoutMs: deps.settings.sandboxSelfhostedExecTimeoutMs,
2699
+ operationResourcePolicy: enrollment.operationPolicy,
2700
+ operationResourcePolicySupported:
2701
+ enrollment.agentCapabilities.operationResourcePolicy === true,
2702
+ operationCpuQuotaSupported: enrollment.agentCapabilities.operationCpuQuota === true,
2597
2703
  ...(deps.settings.agentOpStreamEnabled === true &&
2598
2704
  enrollment.opStream === true &&
2599
2705
  deps.bus.getOpStreamConnection
@@ -2610,13 +2716,13 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2610
2716
  return await callback({
2611
2717
  placement: expectedPlacement,
2612
2718
  controllerHostSandboxGroupId: null,
2613
- placementInstanceId: device.connectionGeneration,
2719
+ placementInstanceId,
2614
2720
  session: built.session as unknown as BrowserControlPlacementSession,
2615
2721
  lease: null,
2616
2722
  transport: {
2617
2723
  kind: "attached_chrome",
2618
2724
  deviceId: device.id,
2619
- connectionGeneration: device.connectionGeneration,
2725
+ connectionGeneration: placementInstanceId,
2620
2726
  browserName: device.browserName,
2621
2727
  browserVersion: device.browserVersion,
2622
2728
  },
@@ -2682,8 +2788,10 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2682
2788
  resolved.kind !== "selfhosted" ||
2683
2789
  resolved.sandboxId !== expectedPlacement.sandboxId
2684
2790
  ) {
2685
- throw new BrowserSessionStateError(
2686
- "BrowserSession Connected Machine is not the active placement",
2791
+ return await throwBrowserSourcePlacementChanged(
2792
+ grant,
2793
+ sourceSession.id,
2794
+ expectedPlacement.sandboxId,
2687
2795
  );
2688
2796
  }
2689
2797
  const placementInstanceId = resolved.providerInstanceId ?? expectedPlacement.sandboxId;
@@ -2780,7 +2888,9 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2780
2888
  !lease ||
2781
2889
  (lease.liveness !== "warm" && lease.liveness !== "draining") ||
2782
2890
  lease.instanceId !== binding.placementInstanceId ||
2783
- !lease.controllerDataPlaneUrl
2891
+ !lease.controllerDataPlaneUrl ||
2892
+ (lease.backend === "opensandbox" && deps.settings.openSandboxSignedEndpoints) ||
2893
+ !controllerCachedUrlIsUsable(lease.controllerDataPlaneUrl)
2784
2894
  ) {
2785
2895
  return null;
2786
2896
  }
@@ -2806,8 +2916,29 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2806
2916
  if (!sandboxGroupId || !placement.lease?.instanceId || !placement.session.resolveExposedPort) {
2807
2917
  return placement;
2808
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
+ }
2809
2931
  const endpoint = await placement.session.resolveExposedPort(BROWSER_CONTROL_PORT);
2810
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
+ }
2811
2942
  const lease = await recordLeaseControllerDataPlaneUrl(deps.db, {
2812
2943
  accountId: grant.accountId,
2813
2944
  workspaceId,
@@ -2817,6 +2948,9 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2817
2948
  controllerDataPlaneUrl: url,
2818
2949
  });
2819
2950
  if (!lease) return placement;
2951
+ if (!controllerCacheAllowsHostFetch(url)) {
2952
+ return { ...placement, lease };
2953
+ }
2820
2954
  return { ...placement, session: controllerOnlySession(url), lease };
2821
2955
  }
2822
2956
 
@@ -2832,7 +2966,16 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2832
2966
  const sandboxGroupId = placement.controllerHostSandboxGroupId;
2833
2967
  const cachedUrl = placement.lease?.controllerDataPlaneUrl;
2834
2968
  return await withCachedController({
2835
- 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,
2836
2979
  createCachedClient: (url) =>
2837
2980
  new BrowserControlClient(controllerOnlySession(url), { adminToken }),
2838
2981
  prepareCachedClient: async (client) => {
@@ -2874,6 +3017,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2874
3017
  record: BrowserSessionControlRecord;
2875
3018
  binding: NonNullable<BrowserSessionControlRecord["session"]["controller"]>;
2876
3019
  placement: BrowserPlacement;
3020
+ sourceAuthorization: ResolvedSessionAuthorization | null;
2877
3021
  }) => Promise<T>,
2878
3022
  recoverMissing = true,
2879
3023
  ): Promise<T> {
@@ -2883,7 +3027,23 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2883
3027
  workspaceId,
2884
3028
  browserSessionId,
2885
3029
  });
2886
- await authorizeSourceSession(deps, grant, record.sourceSessionId, authorizationOperation);
3030
+ const sourceAuthorization = await authorizeSourceSession(
3031
+ deps,
3032
+ grant,
3033
+ record.sourceSessionId,
3034
+ authorizationOperation,
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
+ }
2887
3047
  if (record.session.lifecycle !== "active" || !record.session.controller) {
2888
3048
  throw new BrowserSessionStateError("BrowserSession is not active");
2889
3049
  }
@@ -2923,6 +3083,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2923
3083
  record,
2924
3084
  binding,
2925
3085
  placement,
3086
+ sourceAuthorization,
2926
3087
  };
2927
3088
  let result: T;
2928
3089
  try {
@@ -2967,7 +3128,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2967
3128
  }
2968
3129
  }
2969
3130
 
2970
- const sourceSession = await requireSourceSession(deps, workspaceId, record.sourceSessionId);
3131
+ sourceSession ??= await requireSourceSession(deps, workspaceId, record.sourceSessionId);
2971
3132
  return await withBrowserPlacement(
2972
3133
  sourceSession,
2973
3134
  grant,
@@ -2987,6 +3148,25 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2987
3148
  }
2988
3149
  }
2989
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
+
2990
3170
  async function recoverActiveBrowserController(
2991
3171
  routeDeps: ApiRouteDeps,
2992
3172
  grant: AccessGrant,
@@ -2996,11 +3176,12 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2996
3176
  client: BrowserControlClient,
2997
3177
  tokens: ReturnType<typeof deriveBrowserSessionControllerTokens>,
2998
3178
  ): Promise<void> {
2999
- const linkedComputer = await requireLinkedComputerBinding(
3179
+ const linkedComputer = await ensureLinkedComputerController(
3000
3180
  routeDeps,
3001
3181
  grant,
3002
3182
  record.session,
3003
3183
  placement,
3184
+ client,
3004
3185
  );
3005
3186
  const networkRoute = await resolveActiveBrowserNetworkRouteLaunch({
3006
3187
  deps: routeDeps,
@@ -3009,6 +3190,11 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
3009
3190
  placement: placement.placement,
3010
3191
  });
3011
3192
  try {
3193
+ await ensureHeadedBrowserDisplayStack(
3194
+ placement.session,
3195
+ record.session.headless,
3196
+ linkedComputer !== null,
3197
+ );
3012
3198
  await client.createSession({
3013
3199
  browserSessionId: record.session.id,
3014
3200
  controllerGeneration: binding.controllerGeneration,
@@ -3059,7 +3245,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
3059
3245
  grant,
3060
3246
  record,
3061
3247
  placement,
3062
- requestOrigin(context, deps.settings.corsAllowOriginRegex),
3248
+ requestOrigin(context, deps.settings),
3063
3249
  );
3064
3250
  await endCapturedBrowserController(client, browserSessionId, binding);
3065
3251
  await clearSuspendedBrowserSessionController(deps.db, {
@@ -3081,6 +3267,21 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
3081
3267
  }
3082
3268
  }
3083
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
+
3084
3285
  function browserCreateInput(
3085
3286
  grant: AccessGrant,
3086
3287
  workspaceId: string,
@@ -3229,11 +3430,31 @@ function assertCreateReplay(
3229
3430
  }
3230
3431
  }
3231
3432
 
3232
- 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(
3233
3453
  deps: ApiRouteDeps,
3234
3454
  grant: AccessGrant,
3235
3455
  browser: BrowserSessionValue,
3236
3456
  placement: BrowserPlacement,
3457
+ client: BrowserControlClient,
3237
3458
  ): Promise<{ computerSessionId: string; controllerGeneration: string } | null> {
3238
3459
  if (!browser.linkedComputerSessionId) return null;
3239
3460
  const record = await getComputerSessionControlRecord(deps.db, {
@@ -3251,10 +3472,42 @@ async function requireLinkedComputerBinding(
3251
3472
  "Linked ComputerSession is not active on the browser placement",
3252
3473
  );
3253
3474
  }
3254
- return {
3475
+ const reference = {
3255
3476
  computerSessionId: record.session.id,
3256
3477
  controllerGeneration: record.session.controller.controllerGeneration,
3257
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
+ );
3258
3511
  }
3259
3512
 
3260
3513
  async function requireBrowserRevisionRestoreAuthority(
@@ -3474,6 +3727,7 @@ async function ensureInteractionHolder(
3474
3727
  if (!placement.lease?.instanceId) {
3475
3728
  throw new BrowserSessionStateError("BrowserSession lease placement is unavailable");
3476
3729
  }
3730
+ const sandboxRuntime = await resolveSessionSandboxRuntime(deps.db, deps.settings, sourceSession);
3477
3731
  const acquired = await acquireLease(deps.db, {
3478
3732
  accountId: grant.accountId,
3479
3733
  workspaceId: sourceSession.workspaceId,
@@ -3483,8 +3737,8 @@ async function ensureInteractionHolder(
3483
3737
  subjectId: sourceSession.id,
3484
3738
  backend: placement.lease.backend,
3485
3739
  os: placement.lease.os,
3486
- image: placement.lease.image,
3487
- rigVersionId: placement.lease.rigVersionId,
3740
+ image: sandboxRuntime.image,
3741
+ rigVersionId: sourceSession.rigVersionId,
3488
3742
  leaseTtlMs: deps.settings.sandboxLeaseTtlMs,
3489
3743
  expectedEpoch: placement.lease.leaseEpoch,
3490
3744
  waitSignal,
@@ -3506,6 +3760,11 @@ async function ensureInteractionHolder(
3506
3760
  ).catch(() => undefined);
3507
3761
  throw new BrowserSessionStateError("BrowserSession placement fence changed; retry");
3508
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);
3509
3768
  return true;
3510
3769
  }
3511
3770
 
@@ -3792,9 +4051,9 @@ async function authorizeSourceSession(
3792
4051
  grant: AccessGrant,
3793
4052
  sessionId: string,
3794
4053
  operation: SessionAuthorizationOperation,
3795
- ): Promise<void> {
4054
+ ): Promise<ResolvedSessionAuthorization | null> {
3796
4055
  try {
3797
- await requireSessionAuthorization(deps, grant, {
4056
+ return await requireSessionAuthorization(deps, grant, {
3798
4057
  sessionId,
3799
4058
  operation,
3800
4059
  surface: "http",
@@ -3896,32 +4155,8 @@ function isUuid(value: unknown): value is string {
3896
4155
  );
3897
4156
  }
3898
4157
 
3899
- function requestOrigin(context: Context, allowedPattern: string): string | null {
3900
- return validateBrowserRequestOrigin(context.req.header("origin"), allowedPattern);
3901
- }
3902
-
3903
- export function validateBrowserRequestOrigin(
3904
- value: string | undefined,
3905
- allowedPattern: string,
3906
- ): string | null {
3907
- if (!value) return null;
3908
- let url: URL;
3909
- try {
3910
- url = new URL(value);
3911
- } catch {
3912
- throw new HTTPException(400, { message: "invalid request origin" });
3913
- }
3914
- if (
3915
- url.origin === "null" ||
3916
- (url.protocol !== "http:" && url.protocol !== "https:") ||
3917
- url.origin !== value
3918
- ) {
3919
- throw new HTTPException(400, { message: "invalid request origin" });
3920
- }
3921
- if (!allowedCorsOrigin(allowedPattern, url.origin)) {
3922
- throw new HTTPException(403, { message: "request origin is not allowed" });
3923
- }
3924
- return url.origin;
4158
+ function requestOrigin(context: Context, settings: ApiRouteDeps["settings"]): string | null {
4159
+ return validateInteractionRequestOrigin(context.req.header("origin"), settings);
3925
4160
  }
3926
4161
 
3927
4162
  export function interactionActorForGrant(
@@ -3949,6 +4184,20 @@ function assertPlacementInstance(expected: string | null, actual: string): void
3949
4184
  }
3950
4185
  }
3951
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
+
3952
4201
  function isTerminalOperation(state: string): boolean {
3953
4202
  return state === "completed" || state === "failed" || state === "outcome_unknown";
3954
4203
  }
@@ -4285,6 +4534,40 @@ function browserUploadFileIds(action: BrowserActionRequestValue["action"]): stri
4285
4534
  ];
4286
4535
  }
4287
4536
 
4537
+ /** Resolve Drive authority from the same immutable session authorization that
4538
+ * admitted the browser action. Agent-attempt subjects are technical worker
4539
+ * identities; their frozen initiating human is the only personal Drive
4540
+ * principal. Pure service attempts retain null and can read only ordinary
4541
+ * workspace files through the database predicate. */
4542
+ export function browserFileAuthoritySubjectId(
4543
+ grant: AccessGrant,
4544
+ authorization: ResolvedSessionAuthorization | null,
4545
+ ): string | null {
4546
+ if (!authorization) return grant.principalKind === "agent_attempt" ? null : grant.subjectId;
4547
+ return authorization.actor.kind === "agent_attempt"
4548
+ ? authorization.actor.initiatingHumanSubjectId
4549
+ : authorization.actor.subjectId;
4550
+ }
4551
+
4552
+ /** The batch authority query intentionally omits every unauthorized file. Any
4553
+ * omission therefore fails the whole upload before an object-storage URL is
4554
+ * minted, including mixed ordinary/Drive mappings and partially authorized
4555
+ * batches. */
4556
+ export function requireAuthorizedBrowserUploadFiles(
4557
+ workspaceFileIds: readonly string[],
4558
+ authorizedFiles: readonly FileAsset[],
4559
+ ): FileAsset[] {
4560
+ const byId = new Map(authorizedFiles.map((file) => [file.id, file]));
4561
+ return workspaceFileIds.map((fileId) => {
4562
+ const file = byId.get(fileId);
4563
+ if (!file) throw new HTTPException(404, { message: "workspace file not found" });
4564
+ if (file.status !== "ready") {
4565
+ throw new HTTPException(409, { message: "workspace file is not ready" });
4566
+ }
4567
+ return file;
4568
+ });
4569
+ }
4570
+
4288
4571
  function browserUploadFilename(value: string): string {
4289
4572
  const normalized = value
4290
4573
  .trim()
@@ -4370,9 +4653,14 @@ async function finalizeBrowserDownloadFile(
4370
4653
  if (upload.expiresAt.getTime() <= Date.now()) {
4371
4654
  throw new InteractionResourceStateError("Browser download file upload authority expired");
4372
4655
  }
4373
- const head = await objectStorage.headFile(upload.file).catch(() => {
4374
- 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);
4375
4660
  });
4661
+ if (!head) {
4662
+ throw new HTTPException(503, { message: "browser download object is not available" });
4663
+ }
4376
4664
  if (
4377
4665
  Number(head.ContentLength ?? -1) !== upload.file.sizeBytes ||
4378
4666
  (head.ContentType !== undefined && head.ContentType !== upload.file.contentType) ||
@@ -4413,6 +4701,8 @@ async function recordBrowserDownloadFileUsage(
4413
4701
  }
4414
4702
 
4415
4703
  function browserRouteError(error: unknown): HTTPException {
4704
+ const connectedMachineError = interactionControlApiError(error, "browser");
4705
+ if (connectedMachineError) return connectedMachineError;
4416
4706
  if (error instanceof HTTPException) return error;
4417
4707
  if (error instanceof BrowserSessionNotFoundError) {
4418
4708
  return new HTTPException(404, { message: error.message, cause: error });