@opengeni/api-router 0.30.3 → 1.0.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 (434) hide show
  1. package/dist/access-grant-rls.d.ts +3 -0
  2. package/dist/admission-trace.d.ts +8 -0
  3. package/dist/api-websocket.d.ts +18 -0
  4. package/dist/app.d.ts +50 -2
  5. package/dist/app.js +12 -1
  6. package/dist/artifact-catalog-cursor.d.ts +27 -0
  7. package/dist/auth/managed-auth-attempt-context.d.ts +10 -0
  8. package/dist/auth/managed-auth-rate-limits.d.ts +167 -0
  9. package/dist/auth/managed-auth-session-adapter.d.ts +6 -0
  10. package/dist/auth/managed-auth.d.ts +102 -3
  11. package/dist/auth/managed-email.d.ts +29 -0
  12. package/dist/auth/managed-sign-in-notifications.d.ts +4 -0
  13. package/dist/auth/new-signups-gate.d.ts +23 -0
  14. package/dist/auth/organization-user-setup.d.ts +80 -0
  15. package/dist/auth/signup-funnel-metrics.d.ts +44 -0
  16. package/dist/browser-controller-authority.d.ts +1 -61
  17. package/dist/browser-ephemeral.d.ts +5 -0
  18. package/dist/browser-state-authority.d.ts +1 -27
  19. package/dist/chunk-6TZ5LG3O.js +123948 -0
  20. package/dist/chunk-6TZ5LG3O.js.map +1 -0
  21. package/dist/chunk-OGQDXVXY.js +18 -0
  22. package/dist/chunk-OGQDXVXY.js.map +1 -0
  23. package/dist/claude-subscription-account-conflicts.d.ts +1 -0
  24. package/dist/claude-subscription-account-usage.d.ts +39 -0
  25. package/dist/claude-subscription-oauth.d.ts +25 -0
  26. package/dist/claude-subscription-profile.d.ts +9 -0
  27. package/dist/claude-subscription-usage.d.ts +44 -0
  28. package/dist/claude-workspace-connection.d.ts +15 -0
  29. package/dist/codemode.d.ts +32 -1
  30. package/dist/company-brain-okf.d.ts +24 -0
  31. package/dist/connected-machine-computer-access.d.ts +11 -0
  32. package/dist/connection-ownership.d.ts +54 -0
  33. package/dist/controller-data-plane.d.ts +32 -0
  34. package/dist/editable-artifact-source-session.d.ts +3 -0
  35. package/dist/editable-artifact-websocket.d.ts +10 -18
  36. package/dist/editable-artifact-workspace-files.d.ts +1 -1
  37. package/dist/fatal-process-boundary.d.ts +25 -0
  38. package/dist/github-access.d.ts +31 -2
  39. package/dist/http/acting-person.d.ts +24 -0
  40. package/dist/http/api-contract-compat.d.ts +19 -0
  41. package/dist/http/api-error.d.ts +27 -1
  42. package/dist/http/cors.d.ts +17 -0
  43. package/dist/http/deprecation.d.ts +42 -0
  44. package/dist/http/interaction-control-error.d.ts +13 -0
  45. package/dist/http/keyed-admission.d.ts +12 -0
  46. package/dist/http/local-browser-boundary.d.ts +72 -0
  47. package/dist/http/metrics-listener.d.ts +22 -0
  48. package/dist/http/path-identifier.d.ts +17 -0
  49. package/dist/http/registered-route-label.d.ts +16 -0
  50. package/dist/http/request-body.d.ts +27 -0
  51. package/dist/http/request-source.d.ts +52 -0
  52. package/dist/http/sse.d.ts +17 -6
  53. package/dist/http/unmatched-route.d.ts +18 -0
  54. package/dist/http/user-content.d.ts +36 -0
  55. package/dist/http/websocket-upgrade-dispatch.d.ts +18 -0
  56. package/dist/index.d.ts +10 -3
  57. package/dist/index.js +1091 -162
  58. package/dist/index.js.map +1 -1
  59. package/dist/integration-endpoint-test.d.ts +20 -0
  60. package/dist/integrations/api-integrations.d.ts +2 -2
  61. package/dist/integrations/atlassian.d.ts +29 -3
  62. package/dist/integrations/connect-authority.d.ts +1 -0
  63. package/dist/integrations/connect-callback-return.d.ts +6 -0
  64. package/dist/integrations/fiken.d.ts +16 -2
  65. package/dist/integrations/github-app-connect.d.ts +47 -0
  66. package/dist/integrations/github-installation-proof.d.ts +3 -0
  67. package/dist/integrations/github-lens-connect.d.ts +116 -0
  68. package/dist/integrations/github-skill-source.d.ts +1 -5
  69. package/dist/integrations/google-drive.d.ts +24 -0
  70. package/dist/integrations/integration-commit-authority.d.ts +13 -0
  71. package/dist/integrations/marketplace-plugin.d.ts +2691 -0
  72. package/dist/integrations/oauth-client.d.ts +178 -18
  73. package/dist/integrations/oauth-profiles.d.ts +222 -0
  74. package/dist/integrations/oauth-return-path.d.ts +1 -0
  75. package/dist/integrations/personal-github-repositories.d.ts +84 -0
  76. package/dist/integrations/personal-github.d.ts +42 -0
  77. package/dist/integrations/pr-review-provider.d.ts +22 -0
  78. package/dist/integrations/provider-oauth.d.ts +19 -1
  79. package/dist/integrations/slack-app-home.d.ts +24 -0
  80. package/dist/integrations/slack-bot.d.ts +367 -12
  81. package/dist/integrations/slack-file-upload-flow.d.ts +62 -0
  82. package/dist/integrations/slack-install.d.ts +20 -0
  83. package/dist/integrations/slack-interactions.d.ts +107 -9
  84. package/dist/integrations/slack-mrkdwn.d.ts +41 -0
  85. package/dist/integrations/slack-routing.d.ts +120 -0
  86. package/dist/integrations/slack-session-defaults.d.ts +58 -0
  87. package/dist/integrations/slack-task-file-upload.d.ts +36 -0
  88. package/dist/integrations/social-oauth.d.ts +7 -0
  89. package/dist/interaction-agent-access.d.ts +15 -0
  90. package/dist/interaction-frame-proxy.d.ts +68 -0
  91. package/dist/interaction-metrics.d.ts +2 -0
  92. package/dist/mcp/action-catalog-types.d.ts +9 -0
  93. package/dist/mcp/action-catalog.gen.d.ts +2 -0
  94. package/dist/mcp/capability-account-readiness.d.ts +7 -0
  95. package/dist/mcp/child-read-evidence.d.ts +9 -0
  96. package/dist/mcp/command-read.d.ts +24 -0
  97. package/dist/mcp/company-profile-agent-admin.d.ts +44 -0
  98. package/dist/mcp/contract-input.d.ts +11 -0
  99. package/dist/mcp/documents.d.ts +7 -5
  100. package/dist/mcp/files.d.ts +2 -2
  101. package/dist/mcp/first-party-tool-permissions.d.ts +650 -0
  102. package/dist/mcp/knowledge-entries.d.ts +5 -0
  103. package/dist/mcp/orchestration-failure-diagnostic.d.ts +43 -0
  104. package/dist/mcp/receipts.d.ts +9 -0
  105. package/dist/mcp/request-abort.d.ts +19 -0
  106. package/dist/mcp/scheduled-task-input.d.ts +1189 -0
  107. package/dist/mcp/scheduled-task-view.d.ts +22 -4
  108. package/dist/mcp/server.d.ts +255 -3
  109. package/dist/mcp/session-event-view.d.ts +94 -0
  110. package/dist/mcp/session-view.d.ts +66 -2
  111. package/dist/mcp/session-wait.d.ts +174 -0
  112. package/dist/mcp-oauth.d.ts +52 -0
  113. package/dist/model-catalog.d.ts +11 -31
  114. package/dist/observability.d.ts +3 -1
  115. package/dist/organization-mcp.d.ts +52 -0
  116. package/dist/organization-recovery-notifications.d.ts +41 -0
  117. package/dist/routes/analytics-consent.d.ts +26 -0
  118. package/dist/routes/api-integrations.d.ts +25 -0
  119. package/dist/routes/api-keys.d.ts +17 -0
  120. package/dist/routes/artifact-catalog.d.ts +6 -0
  121. package/dist/routes/automations.d.ts +19 -0
  122. package/dist/routes/billing.d.ts +49 -0
  123. package/dist/routes/browser-sessions.d.ts +59 -2
  124. package/dist/routes/claude-subscription-accounts.d.ts +3 -0
  125. package/dist/routes/claude-subscription-oauth.d.ts +3 -0
  126. package/dist/routes/client-errors.d.ts +146 -0
  127. package/dist/routes/codex.d.ts +76 -2
  128. package/dist/routes/company-brain.d.ts +3 -0
  129. package/dist/routes/company-profile.d.ts +2 -1
  130. package/dist/routes/computer-sessions.d.ts +14 -0
  131. package/dist/routes/connect.d.ts +5 -0
  132. package/dist/routes/editable-artifacts.d.ts +2 -0
  133. package/dist/routes/external-identity-links.d.ts +6 -0
  134. package/dist/routes/feedback.d.ts +3 -0
  135. package/dist/routes/github-browser-pages.d.ts +12 -0
  136. package/dist/routes/github.d.ts +2 -0
  137. package/dist/routes/insights-response-cache.d.ts +10 -0
  138. package/dist/routes/insights-usage.d.ts +12 -0
  139. package/dist/routes/insights.d.ts +30 -1
  140. package/dist/routes/knowledge.d.ts +3 -0
  141. package/dist/routes/managed-auth-session-sets.d.ts +26 -0
  142. package/dist/routes/managed-onboarding.d.ts +29 -0
  143. package/dist/routes/managed-sign-in-methods.d.ts +6 -0
  144. package/dist/routes/model-connection-access.d.ts +3 -0
  145. package/dist/routes/organization-integration-policy.d.ts +3 -0
  146. package/dist/routes/organization-integrations.d.ts +6 -0
  147. package/dist/routes/organization-mcp-connections.d.ts +3 -0
  148. package/dist/routes/organization-model-providers.d.ts +3 -0
  149. package/dist/routes/organization-recovery.d.ts +19 -0
  150. package/dist/routes/organization-service-accounts.d.ts +3 -0
  151. package/dist/routes/organization-sessions.d.ts +26 -0
  152. package/dist/routes/personal-github-git-broker.d.ts +29 -0
  153. package/dist/routes/personal-github.d.ts +3 -0
  154. package/dist/routes/pr-review-github.d.ts +3 -0
  155. package/dist/routes/pr-review.d.ts +3 -0
  156. package/dist/routes/session-artifact-associations.d.ts +4 -0
  157. package/dist/routes/session-history-imports.d.ts +9 -0
  158. package/dist/routes/sessions.d.ts +44 -6
  159. package/dist/routes/skill-content.d.ts +4 -0
  160. package/dist/routes/skill-install-authority.d.ts +6 -0
  161. package/dist/routes/subscription-account-pools.d.ts +34 -0
  162. package/dist/routes/subscription-pool-access.d.ts +16 -0
  163. package/dist/routes/supergrok.d.ts +6 -1
  164. package/dist/routes/usage-allowances.d.ts +12 -0
  165. package/dist/routes/user-resource-authorities.d.ts +3 -0
  166. package/dist/routes/workspace-artifacts.d.ts +2 -1
  167. package/dist/routes/workspace-capture.d.ts +10 -2
  168. package/dist/routes/workspace-integrations.d.ts +35 -0
  169. package/dist/routes/workspace-learning.d.ts +3 -0
  170. package/dist/routes/workspace-model-providers.d.ts +4 -0
  171. package/dist/routes/workspaces.d.ts +29 -0
  172. package/dist/sandbox/auth-callout.d.ts +8 -0
  173. package/dist/sandbox/channel-a.d.ts +19 -1
  174. package/dist/sandbox/connection-authority.d.ts +3 -0
  175. package/dist/sandbox/enrollment.d.ts +9 -1
  176. package/dist/sandbox/machines.d.ts +5 -2
  177. package/dist/sandbox/metrics-ingestion.d.ts +28 -20
  178. package/dist/sandbox/rematerialize.d.ts +2 -0
  179. package/dist/sandbox/session-attach-variable-sets.d.ts +30 -0
  180. package/dist/sandbox/viewer.d.ts +26 -1
  181. package/dist/sandbox-file-artifacts.d.ts +50 -0
  182. package/dist/scheduled-task-deletion.d.ts +13 -0
  183. package/dist/session-create-observability.d.ts +5 -0
  184. package/dist/session-event-view-MPS2OZD3.js +292 -0
  185. package/dist/session-event-view-MPS2OZD3.js.map +1 -0
  186. package/dist/site-session-origin.d.ts +13 -0
  187. package/dist/site-uploads.d.ts +41 -0
  188. package/dist/slack-reaction-files.d.ts +1 -1
  189. package/dist/temporal-schedule-cleanup.d.ts +1 -0
  190. package/dist/temporal-schedule-sync.d.ts +26 -0
  191. package/dist/transcription/providers/xai-subscription.d.ts +1 -0
  192. package/dist/transcription/service.d.ts +2 -1
  193. package/dist/work-discovery-observability.d.ts +33 -0
  194. package/dist/workspace-artifact-content.d.ts +28 -0
  195. package/dist/workspace-artifact-provenance.d.ts +7 -0
  196. package/dist/workspace-delete-observability.d.ts +10 -0
  197. package/dist/workspace-deletion.d.ts +6 -0
  198. package/dist/workspace-state-projection.d.ts +3 -5
  199. package/dist/workspace-tool-gateway-observability.d.ts +17 -0
  200. package/dist/workspace-tool-gateway.d.ts +123 -0
  201. package/dist/workspace-webhook-dispatch.d.ts +26 -0
  202. package/package.json +23 -19
  203. package/src/access-grant-rls.ts +75 -0
  204. package/src/admission-trace.ts +14 -0
  205. package/src/api-websocket.ts +23 -0
  206. package/src/app.ts +1967 -252
  207. package/src/artifact-catalog-cursor.ts +60 -0
  208. package/src/auth/managed-auth-attempt-context.ts +70 -0
  209. package/src/auth/managed-auth-rate-limits.ts +252 -0
  210. package/src/auth/managed-auth-session-adapter.ts +222 -0
  211. package/src/auth/managed-auth.ts +536 -60
  212. package/src/auth/managed-email.ts +174 -0
  213. package/src/auth/managed-sign-in-notifications.ts +81 -0
  214. package/src/auth/new-signups-gate.ts +49 -0
  215. package/src/auth/organization-user-setup.ts +307 -0
  216. package/src/auth/signup-funnel-metrics.ts +164 -0
  217. package/src/browser-controller-authority.ts +1 -186
  218. package/src/browser-ephemeral.ts +39 -0
  219. package/src/browser-state-authority.ts +1 -236
  220. package/src/claude-subscription-account-conflicts.ts +12 -0
  221. package/src/claude-subscription-account-usage.ts +48 -0
  222. package/src/claude-subscription-oauth.ts +362 -0
  223. package/src/claude-subscription-profile.ts +73 -0
  224. package/src/claude-subscription-usage.ts +97 -0
  225. package/src/claude-workspace-connection.ts +82 -0
  226. package/src/codemode.ts +194 -30
  227. package/src/codex-realtime.ts +8 -2
  228. package/src/company-brain-okf.ts +340 -0
  229. package/src/connected-machine-computer-access.ts +33 -0
  230. package/src/connection-ownership.ts +199 -0
  231. package/src/controller-data-plane.ts +81 -0
  232. package/src/editable-artifact-native-kernel.ts +26 -15
  233. package/src/editable-artifact-office-import.ts +28 -3
  234. package/src/editable-artifact-production.ts +23 -7
  235. package/src/editable-artifact-source-session.ts +123 -0
  236. package/src/editable-artifact-websocket.ts +27 -21
  237. package/src/editable-artifact-workspace-files.ts +31 -6
  238. package/src/fatal-process-boundary.ts +246 -0
  239. package/src/gateway-realtime.ts +1 -1
  240. package/src/github-access.ts +147 -5
  241. package/src/http/acting-person.ts +71 -0
  242. package/src/http/api-contract-compat.ts +49 -0
  243. package/src/http/api-error.ts +115 -1
  244. package/src/http/auth.ts +48 -0
  245. package/src/http/cors.ts +67 -0
  246. package/src/http/deprecation.ts +118 -0
  247. package/src/http/interaction-control-error.ts +164 -0
  248. package/src/http/keyed-admission.ts +29 -0
  249. package/src/http/local-browser-boundary.ts +343 -0
  250. package/src/http/metrics-listener.ts +57 -0
  251. package/src/http/path-identifier.ts +54 -0
  252. package/src/http/registered-route-label.ts +34 -0
  253. package/src/http/request-body.ts +127 -0
  254. package/src/http/request-source.ts +178 -0
  255. package/src/http/sse.ts +486 -132
  256. package/src/http/unmatched-route.ts +60 -0
  257. package/src/http/user-content.ts +158 -0
  258. package/src/http/websocket-upgrade-dispatch.ts +29 -0
  259. package/src/index.ts +157 -52
  260. package/src/integration-endpoint-test.ts +291 -0
  261. package/src/integrations/api-integrations.ts +98 -11
  262. package/src/integrations/atlassian.ts +474 -136
  263. package/src/integrations/connect-authority.ts +2 -0
  264. package/src/integrations/connect-callback-return.ts +86 -0
  265. package/src/integrations/fiken.ts +354 -64
  266. package/src/integrations/github-app-connect.ts +435 -0
  267. package/src/integrations/github-installation-proof.ts +60 -0
  268. package/src/integrations/github-lens-connect.ts +76 -0
  269. package/src/integrations/github-skill-source.ts +2 -142
  270. package/src/integrations/google-drive.ts +717 -225
  271. package/src/integrations/integration-commit-authority.ts +69 -0
  272. package/src/integrations/marketplace-plugin.ts +48 -0
  273. package/src/integrations/oauth-client.ts +1356 -459
  274. package/src/integrations/oauth-profiles.ts +537 -0
  275. package/src/integrations/oauth-return-path.ts +18 -0
  276. package/src/integrations/personal-github-repositories.ts +674 -0
  277. package/src/integrations/personal-github.ts +825 -0
  278. package/src/integrations/pr-review-provider.ts +246 -0
  279. package/src/integrations/provider-oauth.ts +250 -22
  280. package/src/integrations/slack-app-home.ts +300 -0
  281. package/src/integrations/slack-bot.ts +963 -239
  282. package/src/integrations/slack-file-upload-flow.ts +147 -0
  283. package/src/integrations/slack-install.ts +83 -0
  284. package/src/integrations/slack-interactions.ts +3167 -474
  285. package/src/integrations/slack-mrkdwn.ts +193 -0
  286. package/src/integrations/slack-routing.ts +337 -0
  287. package/src/integrations/slack-session-defaults.ts +224 -0
  288. package/src/integrations/slack-task-file-upload.ts +323 -0
  289. package/src/integrations/social-oauth.ts +213 -15
  290. package/src/interaction-agent-access.ts +48 -0
  291. package/src/interaction-frame-proxy.ts +409 -0
  292. package/src/interaction-metrics.ts +30 -0
  293. package/src/mcp/action-catalog-types.ts +9 -0
  294. package/src/mcp/action-catalog.gen.ts +6696 -0
  295. package/src/mcp/capability-account-readiness.ts +17 -0
  296. package/src/mcp/child-read-evidence.ts +19 -0
  297. package/src/mcp/command-read.ts +109 -0
  298. package/src/mcp/company-profile-agent-admin.ts +198 -0
  299. package/src/mcp/contract-input.ts +141 -0
  300. package/src/mcp/documents.ts +65 -377
  301. package/src/mcp/editable-artifacts.ts +12 -6
  302. package/src/mcp/files.ts +80 -43
  303. package/src/mcp/first-party-tool-permissions.ts +305 -0
  304. package/src/mcp/knowledge-entries.ts +322 -0
  305. package/src/mcp/orchestration-failure-diagnostic.ts +43 -0
  306. package/src/mcp/receipts.ts +34 -0
  307. package/src/mcp/request-abort.ts +44 -0
  308. package/src/mcp/scheduled-task-input.ts +76 -0
  309. package/src/mcp/scheduled-task-view.ts +44 -3
  310. package/src/mcp/server.ts +3030 -1267
  311. package/src/mcp/session-event-view.ts +383 -0
  312. package/src/mcp/session-view.ts +84 -3
  313. package/src/mcp/session-wait.ts +665 -0
  314. package/src/mcp-oauth.ts +1123 -0
  315. package/src/model-catalog.ts +91 -333
  316. package/src/observability.ts +15 -1
  317. package/src/organization-mcp.ts +397 -0
  318. package/src/organization-recovery-notifications.ts +103 -0
  319. package/src/routes/analytics-consent.ts +123 -0
  320. package/src/routes/api-integrations.ts +364 -74
  321. package/src/routes/api-keys.ts +336 -10
  322. package/src/routes/artifact-catalog.ts +279 -0
  323. package/src/routes/automations.ts +768 -0
  324. package/src/routes/billing.ts +461 -46
  325. package/src/routes/browser-sessions.ts +780 -112
  326. package/src/routes/canonical-human-identities.ts +90 -14
  327. package/src/routes/capabilities.ts +107 -6
  328. package/src/routes/channels.ts +20 -3
  329. package/src/routes/claude-subscription-accounts.ts +334 -0
  330. package/src/routes/claude-subscription-oauth.ts +142 -0
  331. package/src/routes/client-errors.ts +362 -0
  332. package/src/routes/codex.ts +833 -116
  333. package/src/routes/company-brain.ts +322 -0
  334. package/src/routes/company-profile.ts +65 -1
  335. package/src/routes/computer-sessions.ts +557 -106
  336. package/src/routes/connect.ts +1938 -0
  337. package/src/routes/connections.ts +1039 -349
  338. package/src/routes/documents.ts +488 -287
  339. package/src/routes/editable-artifacts.ts +79 -9
  340. package/src/routes/enrollments.ts +135 -39
  341. package/src/routes/environments.ts +154 -58
  342. package/src/routes/external-identity-links.ts +201 -0
  343. package/src/routes/feedback.ts +113 -0
  344. package/src/routes/files.ts +267 -33
  345. package/src/routes/github-browser-pages.ts +162 -0
  346. package/src/routes/github.ts +446 -144
  347. package/src/routes/insights-response-cache.ts +83 -0
  348. package/src/routes/insights-usage.ts +345 -0
  349. package/src/routes/insights.ts +153 -19
  350. package/src/routes/install.ts +133 -38
  351. package/src/routes/integration-facets.ts +107 -33
  352. package/src/routes/interaction-resources.ts +30 -3
  353. package/src/routes/knowledge.ts +488 -0
  354. package/src/routes/machines.ts +313 -15
  355. package/src/routes/managed-auth-session-sets.ts +1218 -0
  356. package/src/routes/managed-onboarding.ts +337 -0
  357. package/src/routes/managed-sign-in-methods.ts +433 -0
  358. package/src/routes/memory-slack-publications.ts +3 -2
  359. package/src/routes/model-connection-access.ts +208 -0
  360. package/src/routes/organization-integration-policy.ts +116 -0
  361. package/src/routes/organization-integrations.ts +326 -0
  362. package/src/routes/organization-mcp-connections.ts +178 -0
  363. package/src/routes/organization-memberships.ts +1019 -27
  364. package/src/routes/organization-model-providers.ts +293 -0
  365. package/src/routes/organization-recovery.ts +325 -0
  366. package/src/routes/organization-service-accounts.ts +128 -0
  367. package/src/routes/organization-sessions.ts +350 -0
  368. package/src/routes/personal-github-git-broker.ts +785 -0
  369. package/src/routes/personal-github.ts +370 -0
  370. package/src/routes/plugins.ts +251 -22
  371. package/src/routes/pr-review-github.ts +881 -0
  372. package/src/routes/pr-review.ts +622 -0
  373. package/src/routes/preference-registry.ts +12 -93
  374. package/src/routes/rigs.ts +167 -50
  375. package/src/routes/scheduled-tasks.ts +346 -41
  376. package/src/routes/session-artifact-associations.ts +87 -0
  377. package/src/routes/session-history-imports.ts +280 -0
  378. package/src/routes/sessions.ts +2306 -264
  379. package/src/routes/skill-content.ts +297 -0
  380. package/src/routes/skill-install-authority.ts +64 -0
  381. package/src/routes/skills.ts +62 -9
  382. package/src/routes/social.ts +46 -18
  383. package/src/routes/subscription-account-pools.ts +436 -0
  384. package/src/routes/subscription-pool-access.ts +126 -0
  385. package/src/routes/supergrok.ts +219 -303
  386. package/src/routes/transcription-recordings.ts +38 -3
  387. package/src/routes/transcriptions.ts +4 -0
  388. package/src/routes/usage-allowances.ts +378 -0
  389. package/src/routes/user-resource-authorities.ts +138 -0
  390. package/src/routes/video-generation.ts +14 -10
  391. package/src/routes/workspace-artifacts.ts +275 -69
  392. package/src/routes/workspace-capture.ts +21 -0
  393. package/src/routes/workspace-integrations.ts +576 -0
  394. package/src/routes/workspace-learning.ts +159 -0
  395. package/src/routes/workspace-model-providers.ts +148 -0
  396. package/src/routes/workspace-state.ts +38 -25
  397. package/src/routes/workspaces.ts +1004 -127
  398. package/src/sandbox/auth-callout.ts +96 -19
  399. package/src/sandbox/channel-a.ts +323 -24
  400. package/src/sandbox/connection-authority.ts +3 -0
  401. package/src/sandbox/enrollment.ts +81 -6
  402. package/src/sandbox/machines.ts +218 -76
  403. package/src/sandbox/metrics-ingestion.ts +411 -62
  404. package/src/sandbox/rematerialize.ts +121 -19
  405. package/src/sandbox/session-attach-variable-sets.ts +131 -0
  406. package/src/sandbox/viewer.ts +280 -25
  407. package/src/sandbox-file-artifacts.ts +413 -0
  408. package/src/scheduled-task-deletion.ts +138 -0
  409. package/src/session-create-observability.ts +46 -0
  410. package/src/session-realtime-context.ts +6 -3
  411. package/src/site-session-origin.ts +48 -0
  412. package/src/site-uploads.ts +224 -0
  413. package/src/slack-reaction-files.ts +16 -5
  414. package/src/temporal-schedule-cleanup.ts +13 -0
  415. package/src/temporal-schedule-sync.ts +74 -0
  416. package/src/transcription/providers/codex-subscription.ts +2 -0
  417. package/src/transcription/providers/openai.ts +1 -0
  418. package/src/transcription/providers/xai-subscription.ts +49 -3
  419. package/src/transcription/segmenter.ts +8 -2
  420. package/src/transcription/service.ts +43 -6
  421. package/src/work-discovery-observability.ts +121 -0
  422. package/src/workspace-artifact-content.ts +144 -0
  423. package/src/workspace-artifact-provenance.ts +104 -0
  424. package/src/workspace-delete-observability.ts +75 -0
  425. package/src/workspace-deletion.ts +73 -0
  426. package/src/workspace-state-projection.ts +18 -148
  427. package/src/workspace-tool-gateway-observability.ts +100 -0
  428. package/src/workspace-tool-gateway.ts +870 -0
  429. package/src/workspace-webhook-dispatch.ts +190 -0
  430. package/src/xai-subscription-auth.ts +9 -2
  431. package/dist/chunk-WCXHI3FF.js +0 -49920
  432. package/dist/chunk-WCXHI3FF.js.map +0 -1
  433. package/dist/routes/packs.d.ts +0 -3
  434. package/src/routes/packs.ts +0 -729
@@ -1,11 +1,18 @@
1
+ import {
2
+ assertEphemeralBrowserCreateEnabled,
3
+ ephemeralBrowserPartition,
4
+ } from "../browser-ephemeral";
1
5
  import { randomUUID } from "node:crypto";
2
6
  import {
3
7
  environmentsEncryptionKeyBytes,
4
8
  resolveFirstPartyDelegationSecret,
5
9
  resolveStreamTokenSecret,
10
+ sandboxWarmRateMicrosPerSecond,
6
11
  } from "@opengeni/config";
7
12
  import {
8
13
  BROWSER_CONTROL_WEBSOCKET_BEARER_PREFIX,
14
+ EPHEMERAL_CHROMIUM_DRIVER_ID,
15
+ browserSessionStorageMode,
9
16
  BROWSER_CONTROL_WEBSOCKET_PROTOCOL,
10
17
  BROWSER_CONTROL_PORT,
11
18
  BROWSER_CONTROL_PROTOCOL_VERSION,
@@ -20,6 +27,8 @@ import {
20
27
  BrowserDownloadSaveRequest,
21
28
  BrowserDownloadSaveResponse,
22
29
  BrowserDownloadListResponse,
30
+ BrowserDomReadRequest,
31
+ BrowserDomReadResponse,
23
32
  BrowserExternalAuthCommand,
24
33
  BrowserOpenTargetRequest,
25
34
  BrowserObservation,
@@ -31,6 +40,7 @@ import {
31
40
  BrowserSessionListResponse,
32
41
  BrowserSessionMutationResponse,
33
42
  BrowserTargetListResponse,
43
+ BrowserTargetState,
34
44
  CreateBrowserSessionRequest,
35
45
  ExternalAuthInteractiveRequest,
36
46
  ExternalAuthInteractiveResponse,
@@ -59,6 +69,7 @@ import {
59
69
  type SiteAuthAuthority,
60
70
  } from "@opengeni/contracts";
61
71
  import {
72
+ getSessionAuthorityEpoch,
62
73
  acquireLease,
63
74
  activateBrowserSession,
64
75
  ATTACHED_BROWSER_SESSION_CAPABILITIES,
@@ -74,6 +85,7 @@ import {
74
85
  BrowserSessionNotFoundError,
75
86
  BrowserSessionOperationConflictError,
76
87
  BrowserSessionStateError,
88
+ SandboxPaidComputeAdmissionError,
77
89
  completeBrowserSessionEnd,
78
90
  completeExternalAuth,
79
91
  completeBrowserDownloadSave,
@@ -87,6 +99,8 @@ import {
87
99
  dispatchExternalAuth,
88
100
  dispatchProtectedAuthFill,
89
101
  failBrowserSessionOperation,
102
+ failPreparedBrowserSessionEnd,
103
+ failPreparedBrowserSessionSuspend,
90
104
  failBrowserSessionResume,
91
105
  failBrowserSessionResumePreparation,
92
106
  failBrowserSessionSuspension,
@@ -95,10 +109,11 @@ import {
95
109
  findBrowserDownloadSave,
96
110
  getBrowserSessionControlRecord,
97
111
  getComputerSessionControlRecord,
112
+ getBrowserIdentity,
98
113
  getBrowserPrivateCheckpointAuthority,
99
114
  getAttachedBrowserDevice,
100
115
  getBrowserRevisionArtifactAuthority,
101
- getEnrollment,
116
+ getLiveEnrollmentConnection,
102
117
  getExternalAuthInteractiveContext,
103
118
  getFilesForSubject,
104
119
  getFileUpload,
@@ -114,6 +129,7 @@ import {
114
129
  listBrowserSessions,
115
130
  listAttachedBrowserDevices,
116
131
  prepareBrowserSessionCreate,
132
+ markEphemeralBrowserSessionLost,
117
133
  prepareExternalAuth,
118
134
  prepareBrowserDownloadSave,
119
135
  prepareBrowserSessionEnd,
@@ -131,6 +147,7 @@ import {
131
147
  touchBrowserSessionController,
132
148
  verifyAuthRun,
133
149
  settleBrowserDownloadSaveFailure,
150
+ terminalizeStaleConnectedInteractionPlacement,
134
151
  type BrowserSessionControlRecord,
135
152
  type BrowserPrivateCheckpointAuthority,
136
153
  type BrowserRevisionArtifactAuthority,
@@ -142,6 +159,7 @@ import {
142
159
  recordWorkspaceUsage,
143
160
  requireLimit,
144
161
  relayConfigFromSettings,
162
+ resolveSessionSandboxRuntime,
145
163
  SessionAuthorizationDeniedError,
146
164
  SessionAuthorizationUnavailableError,
147
165
  type ApiRouteDeps,
@@ -158,16 +176,21 @@ import {
158
176
  buildStreamUrl,
159
177
  exposedPortEndpointFromUrl,
160
178
  buildSelfhostedBackendSession,
179
+ ensureDisplayStack,
161
180
  mintStreamToken,
162
181
  NatsControlRpc,
163
182
  NatsOpStreamTransport,
164
183
  provisionBrowserControlClient,
184
+ renewSandboxProviderExpiration,
165
185
  type BrowserControlPlacementSession,
186
+ type BrowserControlScreenshotOptions,
187
+ type BrowserControlFrame,
166
188
  type PlacementBrowserStateCaptureReceipt,
167
189
  type PlacementBrowserNetworkRoute,
168
190
  type PlacementBrowserTransport,
169
191
  type RestorePlacementBrowserStateInput,
170
192
  } from "@opengeni/runtime/sandbox";
193
+ import { retryWhileMissing } from "@opengeni/storage";
171
194
  import type { Context, Hono } from "hono";
172
195
  import { HTTPException } from "hono/http-exception";
173
196
  import type { ContentfulStatusCode } from "hono/utils/http-status";
@@ -176,8 +199,17 @@ import {
176
199
  deriveBrowserNetworkRouteAuthorityDigest,
177
200
  deriveBrowserSessionControllerTokens,
178
201
  deriveBrowserViewGrantToken,
202
+ deriveComputerSessionControllerTokens,
179
203
  } from "../browser-controller-authority";
180
- import { withCachedController } from "../controller-data-plane";
204
+ import {
205
+ controllerCacheAllowsHostFetch,
206
+ controllerCachedUrlIsUsable,
207
+ shouldPersistControllerDataPlaneUrl,
208
+ isRetryableControllerTransport,
209
+ withCachedController,
210
+ withControllerTransportRecovery,
211
+ } from "../controller-data-plane";
212
+ import { filterInteractionSessionsForGrant } from "../interaction-agent-access";
181
213
  import { withInteractionHolderHeartbeat } from "../interaction-holder-heartbeat";
182
214
  import {
183
215
  browserStateArtifactAad,
@@ -192,7 +224,13 @@ import {
192
224
  resolveProtectedAuthFieldValues,
193
225
  } from "../browser-auth-broker";
194
226
  import { managedNetworkRouteForPlacement } from "../browser-network-route";
195
- import { allowedCorsOrigin } from "../http/cors";
227
+ import { validateInteractionRequestOrigin } from "../http/cors";
228
+ import { ApiHttpError } from "../http/api-error";
229
+ import { interactionControlApiError } from "../http/interaction-control-error";
230
+ import {
231
+ createInteractionFrameProxyAttachment,
232
+ placementUsesInteractionFrameProxy,
233
+ } from "../interaction-frame-proxy";
196
234
  import {
197
235
  observeAuthMutation,
198
236
  observeBrowserActionResult,
@@ -201,6 +239,7 @@ import {
201
239
  } from "../interaction-metrics";
202
240
  import { withChannelA, withChannelARead, type ChannelAOperation } from "../sandbox/channel-a";
203
241
  import { sanitizeFilename } from "./files";
242
+ import { USER_CONTENT_SECURITY_HEADERS } from "../http/user-content";
204
243
 
205
244
  const BROWSER_DRIVER_ID = "opengeni.cdp.v1";
206
245
  const LIGHTPANDA_DRIVER_ID = "opengeni.lightpanda.cdp.v1";
@@ -265,13 +304,15 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
265
304
  app.get("/v1/workspaces/:workspaceId/browser-sessions", async (context) => {
266
305
  const workspaceId = context.req.param("workspaceId") ?? "";
267
306
  const grant = await requireAccessGrant(context, deps, workspaceId, "sessions:read");
307
+ const listed = await listBrowserSessions(deps.db, {
308
+ accountId: grant.accountId,
309
+ workspaceId,
310
+ });
268
311
  return context.json(
269
- BrowserSessionListResponse.parse(
270
- await listBrowserSessions(deps.db, {
271
- accountId: grant.accountId,
272
- workspaceId,
273
- }),
274
- ),
312
+ BrowserSessionListResponse.parse({
313
+ ...listed,
314
+ sessions: await filterInteractionSessionsForGrant(deps, grant, listed.sessions),
315
+ }),
275
316
  );
276
317
  });
277
318
 
@@ -298,10 +339,15 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
298
339
  const request = await parseJsonBody(context, CreateBrowserSessionRequest);
299
340
  const startedAtMs = performance.now();
300
341
  await authorizeSourceSession(deps, grant, request.sessionId, "session.control");
301
- const origin = requestOrigin(context, deps.settings.corsAllowOriginRegex);
342
+ const origin = requestOrigin(context, deps.settings);
302
343
  const authority = browserAuthorityRoot(deps);
344
+ let prepared: Awaited<ReturnType<typeof prepareBrowserSessionCreate>> | null = null;
303
345
 
304
346
  try {
347
+ assertEphemeralBrowserCreateEnabled(
348
+ request,
349
+ deps.settings.experimentalBrowserContextPoolEnabled,
350
+ );
305
351
  const existing = await findBrowserSessionControlRecordByOperation(deps.db, {
306
352
  accountId: grant.accountId,
307
353
  workspaceId,
@@ -313,7 +359,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
313
359
  );
314
360
  }
315
361
  if (existing) assertCreateReplay(request, existing.session);
316
- let prepared = existing
362
+ prepared = existing
317
363
  ? await prepareBrowserSessionCreate(
318
364
  deps.db,
319
365
  browserCreateInput(grant, workspaceId, request, existing.session.placement),
@@ -325,6 +371,20 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
325
371
  return context.json(parsed, 200);
326
372
  }
327
373
 
374
+ // Reject an unavailable explicit identity before acquiring or waking the
375
+ // browser placement. prepareBrowserSessionCreate repeats this check in its
376
+ // transaction, so an archive racing this preflight is still fenced.
377
+ if (!existing && request.identityId) {
378
+ const identity = await getBrowserIdentity(deps.db, {
379
+ accountId: grant.accountId,
380
+ workspaceId,
381
+ identityId: request.identityId,
382
+ });
383
+ if (identity.status !== "active") {
384
+ throw new BrowserIdentityStateError("BrowserIdentity is archived");
385
+ }
386
+ }
387
+
328
388
  const sourceSession = await requireSourceSession(deps, workspaceId, request.sessionId);
329
389
  const response = await withBrowserPlacement(
330
390
  sourceSession,
@@ -401,12 +461,6 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
401
461
  });
402
462
  const preparedSession = prepared.session;
403
463
  const controllerGeneration = requireOperationGeneration(record);
404
- const linkedComputer = await requireLinkedComputerBinding(
405
- deps,
406
- grant,
407
- prepared.session,
408
- placement,
409
- );
410
464
  const adminToken = deriveBrowserControllerAdminToken({
411
465
  rootSecret: authority,
412
466
  accountId: grant.accountId,
@@ -432,8 +486,15 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
432
486
  placement,
433
487
  adminToken,
434
488
  origin,
435
- async (client) =>
436
- await withInteractionHolderHeartbeat(
489
+ async (client) => {
490
+ const linkedComputer = await ensureLinkedComputerController(
491
+ deps,
492
+ grant,
493
+ preparedSession,
494
+ placement,
495
+ client,
496
+ );
497
+ return await withInteractionHolderHeartbeat(
437
498
  deps,
438
499
  {
439
500
  grant,
@@ -444,20 +505,32 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
444
505
  resourceId: preparedSession.id,
445
506
  controllerGeneration,
446
507
  },
447
- async () =>
448
- await client.createSession({
508
+ async () => {
509
+ await ensureHeadedBrowserDisplayStack(
510
+ placement.session,
511
+ preparedSession.headless,
512
+ linkedComputer !== null,
513
+ );
514
+ return await client.createSession({
449
515
  browserSessionId: preparedSession.id,
450
516
  controllerGeneration,
451
517
  tokenGeneration: record.tokenGeneration,
452
518
  ...tokens,
453
519
  headed: !preparedSession.headless,
454
- transport: browserRuntimeTransport(preparedSession, placement.transport),
520
+ transport: browserRuntimeTransport(
521
+ preparedSession,
522
+ placement.transport,
523
+ placement.placementInstanceId,
524
+ authority,
525
+ ),
455
526
  ...(linkedComputer ? { linkedComputer } : {}),
456
527
  ...(networkRoute ? { networkRoute } : {}),
457
528
  ...(request.initialUrl ? { initialUrl: request.initialUrl } : {}),
458
529
  ...(restore ? { restore } : {}),
459
- }),
460
- ),
530
+ });
531
+ },
532
+ );
533
+ },
461
534
  );
462
535
  await cacheBrowserControllerPlacement(grant, workspaceId, placement).catch(
463
536
  () => placement,
@@ -519,6 +592,18 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
519
592
  parsed.operation.state === "completed" && !parsed.operation.replayed ? 201 : 200,
520
593
  );
521
594
  } catch (error) {
595
+ if (prepared) {
596
+ // The operation lock fences a concurrent dispatch. Once a controller
597
+ // binding exists, retain its exact receipt for physical reconciliation.
598
+ await failBrowserSessionOperation(deps.db, {
599
+ accountId: grant.accountId,
600
+ workspaceId,
601
+ operationId: request.operationId,
602
+ browserSessionId: prepared.session.id,
603
+ onlyIfPreparedCreate: true,
604
+ error: interactionFailure(error),
605
+ }).catch(() => undefined);
606
+ }
522
607
  throw browserRouteError(error);
523
608
  }
524
609
  });
@@ -908,6 +993,82 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
908
993
  },
909
994
  );
910
995
 
996
+ app.get(
997
+ "/v1/workspaces/:workspaceId/browser-sessions/:browserSessionId/targets/:targetId/state",
998
+ async (context) => {
999
+ const { workspaceId, grant, browserSessionId } = await browserRoutePreamble(
1000
+ context,
1001
+ "sessions:read",
1002
+ );
1003
+ const targetId = requireOpaqueParam(context, "targetId");
1004
+ const result = await withActiveBrowserController(
1005
+ context,
1006
+ grant,
1007
+ workspaceId,
1008
+ browserSessionId,
1009
+ "session.read",
1010
+ "browser.read",
1011
+ async ({ sessionClient }) => await sessionClient.targetState(targetId),
1012
+ );
1013
+ return context.json(BrowserTargetState.parse(result));
1014
+ },
1015
+ );
1016
+
1017
+ app.post(
1018
+ "/v1/workspaces/:workspaceId/browser-sessions/:browserSessionId/targets/:targetId/dom-read",
1019
+ async (context) => {
1020
+ const { workspaceId, grant, browserSessionId } = await browserRoutePreamble(
1021
+ context,
1022
+ "sessions:read",
1023
+ );
1024
+ const targetId = requireOpaqueParam(context, "targetId");
1025
+ const request = await parseJsonBody(context, BrowserDomReadRequest);
1026
+ const result = await withActiveBrowserController(
1027
+ context,
1028
+ grant,
1029
+ workspaceId,
1030
+ browserSessionId,
1031
+ "session.read",
1032
+ "browser.read",
1033
+ async ({ sessionClient }) => await sessionClient.readDom(targetId, request),
1034
+ );
1035
+ return context.json(BrowserDomReadResponse.parse(result));
1036
+ },
1037
+ );
1038
+
1039
+ app.get(
1040
+ "/v1/workspaces/:workspaceId/browser-sessions/:browserSessionId/targets/:targetId/screenshot",
1041
+ async (context) => {
1042
+ const { workspaceId, grant, browserSessionId } = await browserRoutePreamble(
1043
+ context,
1044
+ "sessions:read",
1045
+ );
1046
+ const targetId = requireOpaqueParam(context, "targetId");
1047
+ const captureOptions = parseBrowserScreenshotOptions(new URL(context.req.url).searchParams);
1048
+ const frame = await withActiveBrowserController(
1049
+ context,
1050
+ grant,
1051
+ workspaceId,
1052
+ browserSessionId,
1053
+ "session.read",
1054
+ "browser.read",
1055
+ async ({ sessionClient, record }) => {
1056
+ if (!record.session.capabilities.screenshots) {
1057
+ throw new BrowserControlUnsupportedError(
1058
+ "This browser engine does not render page screenshots; use semantic observation",
1059
+ );
1060
+ }
1061
+ try {
1062
+ return await sessionClient.capture(targetId, captureOptions);
1063
+ } catch (error) {
1064
+ throw browserScreenshotError(error);
1065
+ }
1066
+ },
1067
+ );
1068
+ return browserScreenshotResponse(frame);
1069
+ },
1070
+ );
1071
+
911
1072
  app.post(
912
1073
  "/v1/workspaces/:workspaceId/browser-sessions/:browserSessionId/actions",
913
1074
  async (context) => {
@@ -1663,7 +1824,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
1663
1824
  context,
1664
1825
  "stream:view",
1665
1826
  );
1666
- const origin = requestOrigin(context, deps.settings.corsAllowOriginRegex);
1827
+ const origin = requestOrigin(context, deps.settings);
1667
1828
  const request = await parseJsonBody(context, BrowserSessionAttachmentRequest);
1668
1829
  const result = await withActiveBrowserController(
1669
1830
  context,
@@ -1705,7 +1866,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
1705
1866
  browserSessionId,
1706
1867
  controllerGeneration: binding.controllerGeneration,
1707
1868
  };
1708
- await client.createViewGrant(reference, {
1869
+ const viewGrant = await client.createViewGrant(reference, {
1709
1870
  grantId,
1710
1871
  token,
1711
1872
  expiresAt,
@@ -1719,6 +1880,18 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
1719
1880
  });
1720
1881
  const stream = relayed
1721
1882
  ? await (async () => {
1883
+ // 0281: stamp the authenticated viewer subject and the live
1884
+ // session authority epoch into the relay stream token.
1885
+ const relayAuthorityEpoch = await getSessionAuthorityEpoch(deps.db, {
1886
+ accountId: grant.accountId,
1887
+ workspaceId,
1888
+ sessionId: record.sourceSessionId,
1889
+ });
1890
+ if (relayAuthorityEpoch === null) {
1891
+ throw new BrowserControlUnsupportedError(
1892
+ "stream authority is unavailable for this session",
1893
+ );
1894
+ }
1722
1895
  const relayToken = await mintStreamToken(relaySecret!, {
1723
1896
  workspaceId,
1724
1897
  sessionId: record.sourceSessionId,
@@ -1726,6 +1899,8 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
1726
1899
  leaseEpoch: record.tokenGeneration,
1727
1900
  port: relayed.channel.port,
1728
1901
  ttlSeconds: request.expiresInSeconds,
1902
+ subjectId: grant.subjectId,
1903
+ authorityEpoch: relayAuthorityEpoch,
1729
1904
  });
1730
1905
  return {
1731
1906
  kind: "relay" as const,
@@ -1740,19 +1915,53 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
1740
1915
  },
1741
1916
  };
1742
1917
  })()
1743
- : {
1744
- kind: "direct_websocket" as const,
1745
- url: await client.frameStreamUrl(reference, request.targetId, request.stream),
1746
- protocols: [
1918
+ : await (async () => {
1919
+ const protocols = [
1747
1920
  BROWSER_CONTROL_WEBSOCKET_PROTOCOL,
1748
1921
  `${BROWSER_CONTROL_WEBSOCKET_BEARER_PREFIX}${token}`,
1749
- ],
1750
- };
1922
+ ] as const;
1923
+ const upstreamUrl = await client.frameStreamUrl(
1924
+ reference,
1925
+ request.targetId,
1926
+ request.stream,
1927
+ );
1928
+ const attachment = placementUsesInteractionFrameProxy(placement.lease?.backend, {
1929
+ openSandboxSignedEndpoints: deps.settings.openSandboxSignedEndpoints,
1930
+ ...(typeof deps.settings.openSandboxInteractionFrameProxy === "boolean"
1931
+ ? {
1932
+ openSandboxInteractionFrameProxy:
1933
+ deps.settings.openSandboxInteractionFrameProxy,
1934
+ }
1935
+ : {}),
1936
+ })
1937
+ ? createInteractionFrameProxyAttachment({
1938
+ requestUrl: context.req.url,
1939
+ publicBaseUrl: deps.settings.publicBaseUrl,
1940
+ webBaseUrl: deps.settings.webBaseUrl,
1941
+ forwardedProto: context.req.header("x-forwarded-proto"),
1942
+ forwardedHost:
1943
+ context.req.header("x-forwarded-host") ?? context.req.header("host"),
1944
+ rootSecret,
1945
+ upstreamUrl,
1946
+ upstreamProtocols: protocols,
1947
+ origin,
1948
+ expiresAt,
1949
+ })
1950
+ : { url: upstreamUrl, protocols };
1951
+ return {
1952
+ kind: "direct_websocket" as const,
1953
+ ...attachment,
1954
+ };
1955
+ })();
1751
1956
  return BrowserSessionAttachment.parse({
1752
1957
  browserSessionId,
1753
1958
  controllerGeneration: binding.controllerGeneration,
1754
1959
  targetId: request.targetId,
1755
1960
  stream,
1961
+ ...(viewGrant.fencedInputBatches === true ? { fencedInputBatches: true } : {}),
1962
+ ...(viewGrant.focusedInputObservations === true
1963
+ ? { focusedInputObservations: true }
1964
+ : {}),
1756
1965
  expiresAt,
1757
1966
  });
1758
1967
  },
@@ -1958,7 +2167,8 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
1958
2167
  const browserSessionId = requireUuidParam(context, "browserSessionId");
1959
2168
  const request = await parseJsonBody(context, BrowserSessionLifecycleRequest);
1960
2169
  const startedAtMs = performance.now();
1961
- const origin = requestOrigin(context, deps.settings.corsAllowOriginRegex);
2170
+ const origin = requestOrigin(context, deps.settings);
2171
+ let preparedForDispatch = false;
1962
2172
  try {
1963
2173
  const before = await getBrowserSessionControlRecord(deps.db, {
1964
2174
  accountId: grant.accountId,
@@ -2010,6 +2220,8 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2010
2220
  return context.json(parsed, 200);
2011
2221
  }
2012
2222
 
2223
+ preparedForDispatch = true;
2224
+
2013
2225
  const record = await getBrowserSessionControlRecord(deps.db, {
2014
2226
  accountId: grant.accountId,
2015
2227
  workspaceId,
@@ -2171,6 +2383,15 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2171
2383
  observeLifecycleResult(deps.observability, startedAtMs, parsed);
2172
2384
  return context.json(parsed, 200);
2173
2385
  } catch (error) {
2386
+ if (preparedForDispatch) {
2387
+ await failPreparedBrowserSessionSuspend(deps.db, {
2388
+ accountId: grant.accountId,
2389
+ workspaceId,
2390
+ browserSessionId,
2391
+ operationId: request.operationId,
2392
+ error: interactionFailure(error),
2393
+ });
2394
+ }
2174
2395
  throw browserRouteError(error);
2175
2396
  }
2176
2397
  },
@@ -2184,7 +2405,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2184
2405
  const browserSessionId = requireUuidParam(context, "browserSessionId");
2185
2406
  const request = await parseJsonBody(context, BrowserSessionLifecycleRequest);
2186
2407
  const startedAtMs = performance.now();
2187
- const origin = requestOrigin(context, deps.settings.corsAllowOriginRegex);
2408
+ const origin = requestOrigin(context, deps.settings);
2188
2409
  let restore: RestorePlacementBrowserStateInput | null = null;
2189
2410
  try {
2190
2411
  let before = await getBrowserSessionControlRecord(deps.db, {
@@ -2303,12 +2524,6 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2303
2524
  throw error;
2304
2525
  });
2305
2526
  const controllerGeneration = requireOperationGeneration(record);
2306
- const linkedComputer = await requireLinkedComputerBinding(
2307
- deps,
2308
- grant,
2309
- prepared.session,
2310
- placement,
2311
- );
2312
2527
  const adminToken = deriveBrowserControllerAdminToken({
2313
2528
  rootSecret: authority,
2314
2529
  accountId: grant.accountId,
@@ -2332,6 +2547,13 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2332
2547
  ...(origin ? { allowedOrigins: [origin] } : {}),
2333
2548
  });
2334
2549
  try {
2550
+ const linkedComputer = await ensureLinkedComputerController(
2551
+ deps,
2552
+ grant,
2553
+ prepared.session,
2554
+ placement,
2555
+ client,
2556
+ );
2335
2557
  await withInteractionHolderHeartbeat(
2336
2558
  deps,
2337
2559
  {
@@ -2343,8 +2565,13 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2343
2565
  resourceId: browserSessionId,
2344
2566
  controllerGeneration,
2345
2567
  },
2346
- async () =>
2347
- await client.createSession({
2568
+ async () => {
2569
+ await ensureHeadedBrowserDisplayStack(
2570
+ placement.session,
2571
+ prepared.session.headless,
2572
+ linkedComputer !== null,
2573
+ );
2574
+ return await client.createSession({
2348
2575
  browserSessionId,
2349
2576
  controllerGeneration,
2350
2577
  tokenGeneration: record.tokenGeneration,
@@ -2354,7 +2581,8 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2354
2581
  ...(linkedComputer ? { linkedComputer } : {}),
2355
2582
  ...(networkRoute ? { networkRoute } : {}),
2356
2583
  restore: restore!,
2357
- }),
2584
+ });
2585
+ },
2358
2586
  );
2359
2587
  } catch (error) {
2360
2588
  if (!isDefiniteBrowserControllerFailure(error)) throw error;
@@ -2426,7 +2654,12 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2426
2654
  const browserSessionId = requireUuidParam(context, "browserSessionId");
2427
2655
  const request = await parseJsonBody(context, BrowserSessionLifecycleRequest);
2428
2656
  const startedAtMs = performance.now();
2429
- const origin = requestOrigin(context, deps.settings.corsAllowOriginRegex);
2657
+ const origin = requestOrigin(context, deps.settings);
2658
+ let endPreparation: {
2659
+ restoreLifecycle: BrowserSessionValue["lifecycle"];
2660
+ restoreFailureCode: string | null;
2661
+ expectedControllerGeneration: string | null;
2662
+ } | null = null;
2430
2663
  try {
2431
2664
  const before = await getBrowserSessionControlRecord(deps.db, {
2432
2665
  accountId: grant.accountId,
@@ -2441,6 +2674,9 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2441
2674
  browserSessionId,
2442
2675
  operationId: request.operationId,
2443
2676
  actorSubjectId: grant.subjectId,
2677
+ expectedLifecycle: before.session.lifecycle,
2678
+ expectedFailureCode: before.session.failureCode,
2679
+ expectedControllerGeneration: before.session.controller?.controllerGeneration ?? null,
2444
2680
  });
2445
2681
  if (isTerminalOperation(prepared.operation.state)) {
2446
2682
  if (prepared.operation.state === "completed") {
@@ -2456,6 +2692,13 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2456
2692
  observeLifecycleResult(deps.observability, startedAtMs, parsed);
2457
2693
  return context.json(parsed, 200);
2458
2694
  }
2695
+ if (before.session.lifecycle !== "ending") {
2696
+ endPreparation = {
2697
+ restoreLifecycle: before.session.lifecycle,
2698
+ restoreFailureCode: before.session.failureCode,
2699
+ expectedControllerGeneration: before.session.controller?.controllerGeneration ?? null,
2700
+ };
2701
+ }
2459
2702
 
2460
2703
  const record = await getBrowserSessionControlRecord(deps.db, {
2461
2704
  accountId: grant.accountId,
@@ -2549,6 +2792,16 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2549
2792
  observeLifecycleResult(deps.observability, startedAtMs, parsed);
2550
2793
  return context.json(parsed, 200);
2551
2794
  } catch (error) {
2795
+ if (endPreparation) {
2796
+ await failPreparedBrowserSessionEnd(deps.db, {
2797
+ accountId: grant.accountId,
2798
+ workspaceId,
2799
+ browserSessionId,
2800
+ operationId: request.operationId,
2801
+ ...endPreparation,
2802
+ error: interactionFailure(error),
2803
+ });
2804
+ }
2552
2805
  throw browserRouteError(error);
2553
2806
  }
2554
2807
  },
@@ -2562,6 +2815,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2562
2815
  operation: ChannelAOperation,
2563
2816
  waitSignal: AbortSignal,
2564
2817
  callback: (placement: BrowserPlacement) => Promise<T>,
2818
+ allowOperationReplay = true,
2565
2819
  ): Promise<T> {
2566
2820
  if (expectedPlacement?.kind === "attached_device") {
2567
2821
  waitSignal.throwIfAborted();
@@ -2573,18 +2827,29 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2573
2827
  if (device.state !== "connected") {
2574
2828
  throw new BrowserSessionStateError("Attached browser is disconnected");
2575
2829
  }
2576
- const enrollment = await getEnrollment(
2830
+ const enrollment = await getLiveEnrollmentConnection(
2577
2831
  deps.db,
2578
2832
  sourceSession.workspaceId,
2579
2833
  device.enrollmentId,
2580
2834
  );
2581
- if (!enrollment || enrollment.status !== "active") {
2835
+ if (!enrollment || enrollment.status !== "active" || !enrollment.connectionInstanceId) {
2582
2836
  throw new BrowserSessionStateError("Attached browser machine is unavailable");
2583
2837
  }
2584
- assertPlacementInstance(expectedPlacementInstanceId, device.connectionGeneration);
2838
+ if (!enrollment.workspaceRoot) {
2839
+ throw new BrowserSessionStateError(
2840
+ "Attached browser machine has not reported an absolute workspace root",
2841
+ );
2842
+ }
2843
+ const placementInstanceId = attachedEndPlacementInstanceId(
2844
+ operation,
2845
+ expectedPlacementInstanceId,
2846
+ device.connectionGeneration,
2847
+ );
2585
2848
  const built = await buildSelfhostedBackendSession({
2586
2849
  workspaceId: sourceSession.workspaceId,
2587
2850
  agentId: device.enrollmentId,
2851
+ connectionInstanceId: enrollment.connectionInstanceId,
2852
+ workspaceRoot: enrollment.workspaceRoot,
2588
2853
  relay: relayConfigFromSettings(deps.settings),
2589
2854
  controlRpcFactory: () => new NatsControlRpc(async () => deps.bus.getRequestConnection()),
2590
2855
  // Browser-profile generation is the physical controller fence. Epoch 0
@@ -2593,6 +2858,11 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2593
2858
  epoch: 0,
2594
2859
  timeoutMs: deps.settings.sandboxSelfhostedControlTimeoutMs,
2595
2860
  execTimeoutMs: deps.settings.sandboxSelfhostedExecTimeoutMs,
2861
+ operationResourcePolicy: enrollment.operationPolicy,
2862
+ operationResourcePolicySupported:
2863
+ enrollment.agentCapabilities.operationResourcePolicy === true,
2864
+ operationCpuQuotaSupported: enrollment.agentCapabilities.operationCpuQuota === true,
2865
+ transactionalFsWriteSupported: enrollment.agentCapabilities.transactionalFsWrite === true,
2596
2866
  ...(deps.settings.agentOpStreamEnabled === true &&
2597
2867
  enrollment.opStream === true &&
2598
2868
  deps.bus.getOpStreamConnection
@@ -2609,13 +2879,13 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2609
2879
  return await callback({
2610
2880
  placement: expectedPlacement,
2611
2881
  controllerHostSandboxGroupId: null,
2612
- placementInstanceId: device.connectionGeneration,
2882
+ placementInstanceId,
2613
2883
  session: built.session as unknown as BrowserControlPlacementSession,
2614
2884
  lease: null,
2615
2885
  transport: {
2616
2886
  kind: "attached_chrome",
2617
2887
  deviceId: device.id,
2618
- connectionGeneration: device.connectionGeneration,
2888
+ connectionGeneration: placementInstanceId,
2619
2889
  browserName: device.browserName,
2620
2890
  browserVersion: device.browserVersion,
2621
2891
  },
@@ -2638,6 +2908,12 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2638
2908
  waitSignal,
2639
2909
  operation,
2640
2910
  retryControllerTransport: operation === "browser.read" || operation === "browser.action",
2911
+ allowOperationReplay,
2912
+ ...(expectedPlacementInstanceId &&
2913
+ (expectedPlacement?.kind === "sandbox_group" ||
2914
+ expectedPlacement?.kind === "external_provider")
2915
+ ? { retainedInstanceId: expectedPlacementInstanceId }
2916
+ : {}),
2641
2917
  },
2642
2918
  async (handle) => {
2643
2919
  if (expectedPlacement?.kind === "sandbox_group") {
@@ -2681,8 +2957,16 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2681
2957
  resolved.kind !== "selfhosted" ||
2682
2958
  resolved.sandboxId !== expectedPlacement.sandboxId
2683
2959
  ) {
2684
- throw new BrowserSessionStateError(
2685
- "BrowserSession Connected Machine is not the active placement",
2960
+ if (operation === "browser.create") {
2961
+ throw new HTTPException(422, {
2962
+ message:
2963
+ "The requested Connected Machine is not this session's current placement. Move the session to that machine before creating an interaction resource.",
2964
+ });
2965
+ }
2966
+ return await throwBrowserSourcePlacementChanged(
2967
+ grant,
2968
+ sourceSession.id,
2969
+ expectedPlacement.sandboxId,
2686
2970
  );
2687
2971
  }
2688
2972
  const placementInstanceId = resolved.providerInstanceId ?? expectedPlacement.sandboxId;
@@ -2779,7 +3063,9 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2779
3063
  !lease ||
2780
3064
  (lease.liveness !== "warm" && lease.liveness !== "draining") ||
2781
3065
  lease.instanceId !== binding.placementInstanceId ||
2782
- !lease.controllerDataPlaneUrl
3066
+ !lease.controllerDataPlaneUrl ||
3067
+ (lease.backend === "opensandbox" && deps.settings.openSandboxSignedEndpoints) ||
3068
+ !controllerCachedUrlIsUsable(lease.controllerDataPlaneUrl)
2783
3069
  ) {
2784
3070
  return null;
2785
3071
  }
@@ -2805,8 +3091,29 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2805
3091
  if (!sandboxGroupId || !placement.lease?.instanceId || !placement.session.resolveExposedPort) {
2806
3092
  return placement;
2807
3093
  }
3094
+ if (placement.lease.backend === "opensandbox" && deps.settings.openSandboxSignedEndpoints) {
3095
+ if (!placement.lease.controllerDataPlaneUrl) return placement;
3096
+ const lease = await recordLeaseControllerDataPlaneUrl(deps.db, {
3097
+ accountId: grant.accountId,
3098
+ workspaceId,
3099
+ sandboxGroupId,
3100
+ expectedEpoch: placement.lease.leaseEpoch,
3101
+ expectedInstanceId: placement.lease.instanceId,
3102
+ controllerDataPlaneUrl: null,
3103
+ });
3104
+ return lease ? { ...placement, lease } : placement;
3105
+ }
2808
3106
  const endpoint = await placement.session.resolveExposedPort(BROWSER_CONTROL_PORT);
2809
3107
  const url = buildStreamUrl(endpoint);
3108
+ if (
3109
+ !shouldPersistControllerDataPlaneUrl({
3110
+ backend: placement.lease.backend,
3111
+ signedEndpoints: deps.settings.openSandboxSignedEndpoints,
3112
+ url,
3113
+ })
3114
+ ) {
3115
+ return placement;
3116
+ }
2810
3117
  const lease = await recordLeaseControllerDataPlaneUrl(deps.db, {
2811
3118
  accountId: grant.accountId,
2812
3119
  workspaceId,
@@ -2816,6 +3123,9 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2816
3123
  controllerDataPlaneUrl: url,
2817
3124
  });
2818
3125
  if (!lease) return placement;
3126
+ if (!controllerCacheAllowsHostFetch(url)) {
3127
+ return { ...placement, lease };
3128
+ }
2819
3129
  return { ...placement, session: controllerOnlySession(url), lease };
2820
3130
  }
2821
3131
 
@@ -2831,7 +3141,16 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2831
3141
  const sandboxGroupId = placement.controllerHostSandboxGroupId;
2832
3142
  const cachedUrl = placement.lease?.controllerDataPlaneUrl;
2833
3143
  return await withCachedController({
2834
- cachedUrl: sandboxGroupId ? (cachedUrl ?? null) : null,
3144
+ cachedUrl:
3145
+ sandboxGroupId &&
3146
+ cachedUrl &&
3147
+ !(
3148
+ placement.lease?.backend === "opensandbox" &&
3149
+ routeDeps.settings.openSandboxSignedEndpoints
3150
+ ) &&
3151
+ controllerCachedUrlIsUsable(cachedUrl)
3152
+ ? cachedUrl
3153
+ : null,
2835
3154
  createCachedClient: (url) =>
2836
3155
  new BrowserControlClient(controllerOnlySession(url), { adminToken }),
2837
3156
  prepareCachedClient: async (client) => {
@@ -2889,6 +3208,17 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2889
3208
  record.sourceSessionId,
2890
3209
  authorizationOperation,
2891
3210
  );
3211
+ let sourceSession: Session | null = null;
3212
+ if (record.session.placement?.kind === "connected_machine") {
3213
+ sourceSession = await requireSourceSession(deps, workspaceId, record.sourceSessionId);
3214
+ if (sourceSession.activeSandboxId !== record.session.placement.sandboxId) {
3215
+ return await throwBrowserSourcePlacementChanged(
3216
+ grant,
3217
+ record.sourceSessionId,
3218
+ record.session.placement.sandboxId,
3219
+ );
3220
+ }
3221
+ }
2892
3222
  if (record.session.lifecycle !== "active" || !record.session.controller) {
2893
3223
  throw new BrowserSessionStateError("BrowserSession is not active");
2894
3224
  }
@@ -2902,10 +3232,18 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2902
3232
  if (!admitted) {
2903
3233
  throw new BrowserSessionStateError("BrowserSession controller authority changed");
2904
3234
  }
2905
- const run = async (placement: BrowserPlacement): Promise<T> => {
3235
+ const safelyReplayable =
3236
+ browserSessionStorageMode(record.session) !== "ephemeral_context" &&
3237
+ (channelOperation === "browser.read" || channelOperation === "browser.action");
3238
+ let controllerTransportFailed = false;
3239
+ const controllerRecoveryState = { attempted: false };
3240
+ const run = async (
3241
+ placement: BrowserPlacement,
3242
+ provisionedClient?: BrowserControlClient,
3243
+ ): Promise<T> => {
2906
3244
  // An active binding already proves browserd was provisioned. Reusing
2907
3245
  // it avoids restarting/checking the sidecar on every live input.
2908
- const client = connectController(deps, grant, record, placement);
3246
+ const client = provisionedClient ?? connectController(deps, grant, record, placement);
2909
3247
  const tokens = deriveBrowserSessionControllerTokens({
2910
3248
  rootSecret: browserAuthorityRoot(deps),
2911
3249
  accountId: grant.accountId,
@@ -2934,6 +3272,30 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2934
3272
  try {
2935
3273
  result = await callback(controller);
2936
3274
  } catch (error) {
3275
+ if (
3276
+ browserSessionStorageMode(record.session) === "ephemeral_context" &&
3277
+ (isMissingBrowserControllerSession(error) ||
3278
+ (error instanceof BrowserControlRequestError &&
3279
+ error.error.code === "controller_lost"))
3280
+ ) {
3281
+ const retired = await markEphemeralBrowserSessionLost(deps.db, {
3282
+ accountId: grant.accountId,
3283
+ workspaceId,
3284
+ browserSessionId,
3285
+ controllerGeneration: binding.controllerGeneration,
3286
+ });
3287
+ if (retired)
3288
+ await releaseInteractionHolder(
3289
+ deps,
3290
+ grant,
3291
+ workspaceId,
3292
+ browserSessionId,
3293
+ record.controllerHostSandboxGroupId,
3294
+ );
3295
+ throw new BrowserSessionStateError(
3296
+ "Ephemeral browser context was lost; create a new BrowserSession. No identity or operation was replayed.",
3297
+ );
3298
+ }
2937
3299
  if (!recoverMissing || !isMissingBrowserControllerSession(error)) throw error;
2938
3300
  await recoverActiveBrowserController(
2939
3301
  deps,
@@ -2943,6 +3305,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2943
3305
  placement,
2944
3306
  client,
2945
3307
  tokens,
3308
+ authorizationOperation,
2946
3309
  );
2947
3310
  result = await callback(controller);
2948
3311
  }
@@ -2953,15 +3316,10 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2953
3316
  try {
2954
3317
  return await run(cached);
2955
3318
  } catch (error) {
2956
- const safelyReplayable =
2957
- channelOperation === "browser.read" || channelOperation === "browser.action";
2958
- if (
2959
- !safelyReplayable ||
2960
- (!(error instanceof BrowserControlTransportError) &&
2961
- !(error instanceof BrowserControlRequestError && error.retryable))
2962
- ) {
3319
+ if (!safelyReplayable || !isRetryableControllerTransport(error)) {
2963
3320
  throw error;
2964
3321
  }
3322
+ controllerTransportFailed = true;
2965
3323
  await recordLeaseControllerDataPlaneUrl(deps.db, {
2966
3324
  accountId: grant.accountId,
2967
3325
  workspaceId,
@@ -2973,7 +3331,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2973
3331
  }
2974
3332
  }
2975
3333
 
2976
- const sourceSession = await requireSourceSession(deps, workspaceId, record.sourceSessionId);
3334
+ sourceSession ??= await requireSourceSession(deps, workspaceId, record.sourceSessionId);
2977
3335
  return await withBrowserPlacement(
2978
3336
  sourceSession,
2979
3337
  grant,
@@ -2981,18 +3339,75 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2981
3339
  binding.placementInstanceId,
2982
3340
  channelOperation,
2983
3341
  context.req.raw.signal,
2984
- async (placement) =>
2985
- await run(
2986
- await cacheBrowserControllerPlacement(grant, workspaceId, placement).catch(
2987
- () => placement,
2988
- ),
2989
- ),
3342
+ async (placement) => {
3343
+ const use = async () =>
3344
+ await run(
3345
+ await cacheBrowserControllerPlacement(grant, workspaceId, placement).catch(
3346
+ () => placement,
3347
+ ),
3348
+ );
3349
+ if (browserSessionStorageMode(record.session) === "ephemeral_context") return await use();
3350
+ return await withControllerTransportRecovery({
3351
+ channelOperation,
3352
+ placementKind: placement.placement.kind,
3353
+ recoveryState: controllerRecoveryState,
3354
+ transportAlreadyFailed: controllerTransportFailed,
3355
+ use,
3356
+ admitRecovery: async () => {
3357
+ if (
3358
+ !(await touchBrowserSessionController(deps.db, {
3359
+ accountId: grant.accountId,
3360
+ workspaceId,
3361
+ browserSessionId,
3362
+ controllerGeneration: binding.controllerGeneration,
3363
+ }))
3364
+ ) {
3365
+ throw new BrowserSessionStateError("BrowserSession controller authority changed");
3366
+ }
3367
+ },
3368
+ recover: async () => {
3369
+ // Provision and restore through the original exec-capable session;
3370
+ // the cached tunnel alone cannot start browserd or its display.
3371
+ const client = await provisionController(
3372
+ deps,
3373
+ grant,
3374
+ record,
3375
+ placement,
3376
+ requestOrigin(context, deps.settings),
3377
+ );
3378
+ await cacheBrowserControllerPlacement(grant, workspaceId, placement).catch(
3379
+ () => placement,
3380
+ );
3381
+ return await run(placement, client);
3382
+ },
3383
+ });
3384
+ },
3385
+ browserSessionStorageMode(record.session) !== "ephemeral_context",
2990
3386
  );
2991
3387
  } catch (error) {
2992
3388
  throw browserRouteError(error);
2993
3389
  }
2994
3390
  }
2995
3391
 
3392
+ async function throwBrowserSourcePlacementChanged(
3393
+ grant: AccessGrant,
3394
+ sourceSessionId: string,
3395
+ connectedSandboxId: string,
3396
+ ): Promise<never> {
3397
+ const reconciliation = await terminalizeStaleConnectedInteractionPlacement(deps.db, {
3398
+ accountId: grant.accountId,
3399
+ workspaceId: grant.workspaceId,
3400
+ sourceSessionId,
3401
+ connectedSandboxId,
3402
+ });
3403
+ if (reconciliation.sourcePlacementChanged) {
3404
+ throw sourcePlacementChangedApiError("browser_session");
3405
+ }
3406
+ throw new BrowserSessionStateError(
3407
+ "BrowserSession source placement changed during the request; retry",
3408
+ );
3409
+ }
3410
+
2996
3411
  async function recoverActiveBrowserController(
2997
3412
  routeDeps: ApiRouteDeps,
2998
3413
  grant: AccessGrant,
@@ -3001,12 +3416,72 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
3001
3416
  placement: BrowserPlacement,
3002
3417
  client: BrowserControlClient,
3003
3418
  tokens: ReturnType<typeof deriveBrowserSessionControllerTokens>,
3419
+ authorizationOperation: SessionAuthorizationOperation,
3004
3420
  ): Promise<void> {
3005
- const linkedComputer = await requireLinkedComputerBinding(
3421
+ if (browserSessionStorageMode(record.session) === "ephemeral_context") {
3422
+ throw new BrowserSessionStateError(
3423
+ "Ephemeral browser context was lost; create a new BrowserSession. Its identity and operations cannot be restored or replayed.",
3424
+ );
3425
+ }
3426
+ const transport = browserRuntimeTransport(record.session, placement.transport);
3427
+ const recoverWorkingDirectory =
3428
+ transport.kind === "managed" &&
3429
+ transport.engine === "chromium" &&
3430
+ !transport.ephemeralPartition &&
3431
+ browserSessionStorageMode(record.session) === "private_profile";
3432
+ if (recoverWorkingDirectory) {
3433
+ // Missing controller memory cannot admit a stale source, token, placement
3434
+ // or route snapshot. Reuse the existing durable authority before recovery.
3435
+ await authorizeSourceSession(
3436
+ routeDeps,
3437
+ grant,
3438
+ record.sourceSessionId,
3439
+ authorizationOperation,
3440
+ );
3441
+ const current = await getBrowserSessionControlRecord(routeDeps.db, {
3442
+ accountId: grant.accountId,
3443
+ workspaceId: record.session.workspaceId,
3444
+ browserSessionId: record.session.id,
3445
+ });
3446
+ if (
3447
+ current.sourceSessionId !== record.sourceSessionId ||
3448
+ current.session.lifecycle !== "active" ||
3449
+ current.session.controller?.controllerGeneration !== binding.controllerGeneration ||
3450
+ current.session.controller.placementInstanceId !== placement.placementInstanceId ||
3451
+ current.session.controller.controllerId !== binding.controllerId ||
3452
+ current.tokenGeneration !== record.tokenGeneration ||
3453
+ !sameInteractionPlacement(current.session.placement, record.session.placement) ||
3454
+ current.session.driverId !== record.session.driverId ||
3455
+ current.session.engine !== record.session.engine ||
3456
+ current.session.headless !== record.session.headless ||
3457
+ current.session.linkedComputerSessionId !== record.session.linkedComputerSessionId ||
3458
+ current.session.networkRouteId !== record.session.networkRouteId ||
3459
+ JSON.stringify(current.networkRouteAuthority) !==
3460
+ JSON.stringify(record.networkRouteAuthority)
3461
+ ) {
3462
+ throw new BrowserSessionStateError("BrowserSession controller authority changed");
3463
+ }
3464
+ if (record.session.placement.kind === "connected_machine") {
3465
+ const source = await requireSourceSession(
3466
+ routeDeps,
3467
+ record.session.workspaceId,
3468
+ record.sourceSessionId,
3469
+ );
3470
+ if (source.activeSandboxId !== record.session.placement.sandboxId) {
3471
+ return await throwBrowserSourcePlacementChanged(
3472
+ grant,
3473
+ record.sourceSessionId,
3474
+ record.session.placement.sandboxId,
3475
+ );
3476
+ }
3477
+ }
3478
+ }
3479
+ const linkedComputer = await ensureLinkedComputerController(
3006
3480
  routeDeps,
3007
3481
  grant,
3008
3482
  record.session,
3009
3483
  placement,
3484
+ client,
3010
3485
  );
3011
3486
  const networkRoute = await resolveActiveBrowserNetworkRouteLaunch({
3012
3487
  deps: routeDeps,
@@ -3015,13 +3490,30 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
3015
3490
  placement: placement.placement,
3016
3491
  });
3017
3492
  try {
3493
+ await ensureHeadedBrowserDisplayStack(
3494
+ placement.session,
3495
+ record.session.headless,
3496
+ linkedComputer !== null,
3497
+ );
3498
+ if (
3499
+ recoverWorkingDirectory &&
3500
+ !(await touchBrowserSessionController(routeDeps.db, {
3501
+ accountId: grant.accountId,
3502
+ workspaceId: record.session.workspaceId,
3503
+ browserSessionId: record.session.id,
3504
+ controllerGeneration: binding.controllerGeneration,
3505
+ }))
3506
+ ) {
3507
+ throw new BrowserSessionStateError("BrowserSession controller authority changed");
3508
+ }
3018
3509
  await client.createSession({
3019
3510
  browserSessionId: record.session.id,
3020
3511
  controllerGeneration: binding.controllerGeneration,
3021
3512
  tokenGeneration: record.tokenGeneration,
3022
3513
  ...tokens,
3023
3514
  headed: !record.session.headless,
3024
- transport: browserRuntimeTransport(record.session, placement.transport),
3515
+ transport,
3516
+ ...(recoverWorkingDirectory ? { recoverExistingWorkingDirectory: true } : {}),
3025
3517
  ...(linkedComputer ? { linkedComputer } : {}),
3026
3518
  ...(networkRoute ? { networkRoute } : {}),
3027
3519
  });
@@ -3065,7 +3557,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
3065
3557
  grant,
3066
3558
  record,
3067
3559
  placement,
3068
- requestOrigin(context, deps.settings.corsAllowOriginRegex),
3560
+ requestOrigin(context, deps.settings),
3069
3561
  );
3070
3562
  await endCapturedBrowserController(client, browserSessionId, binding);
3071
3563
  await clearSuspendedBrowserSessionController(deps.db, {
@@ -3087,18 +3579,42 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
3087
3579
  }
3088
3580
  }
3089
3581
 
3090
- function browserCreateInput(
3582
+ function sourcePlacementChangedApiError(interactionResource: "browser_session"): ApiHttpError {
3583
+ return new ApiHttpError(409, {
3584
+ code: "conflict",
3585
+ message:
3586
+ "This browser belonged to a previous task placement and was retired. Open a new browser.",
3587
+ retryable: false,
3588
+ outcomeUnknown: false,
3589
+ details: {
3590
+ interactionResource,
3591
+ interactionFailureCode: "source_placement_changed",
3592
+ interactionLifecycle: "lost",
3593
+ },
3594
+ });
3595
+ }
3596
+
3597
+ export function browserCreateInput(
3091
3598
  grant: AccessGrant,
3092
3599
  workspaceId: string,
3093
3600
  request: CreateBrowserSessionRequestValue,
3094
3601
  placement: InteractionPlacement,
3095
3602
  ) {
3603
+ if (request.storageMode === "ephemeral_context" && placement.kind !== "sandbox_group") {
3604
+ throw new BrowserControlUnsupportedError(
3605
+ "Ephemeral contexts require a managed sandbox placement",
3606
+ );
3607
+ }
3096
3608
  const attached = placement.kind === "attached_device";
3097
3609
  const external = placement.kind === "external_provider";
3098
3610
  const engine = attached ? ("chrome" as const) : external ? ("external" as const) : request.engine;
3099
- if (engine === "lightpanda" && placement.kind !== "sandbox_group") {
3611
+ if (
3612
+ request.engine === "lightpanda" &&
3613
+ placement.kind !== "sandbox_group" &&
3614
+ placement.kind !== "connected_machine"
3615
+ ) {
3100
3616
  throw new BrowserControlUnsupportedError(
3101
- "Lightpanda is currently available only in managed agent sandboxes",
3617
+ "Lightpanda requires a managed sandbox or Connected Machine browser placement",
3102
3618
  );
3103
3619
  }
3104
3620
  return {
@@ -3111,11 +3627,13 @@ function browserCreateInput(
3111
3627
  initialUrl: request.initialUrl ?? null,
3112
3628
  placement,
3113
3629
  driverId:
3114
- engine === "lightpanda"
3115
- ? LIGHTPANDA_DRIVER_ID
3116
- : external
3117
- ? EXTERNAL_BROWSER_DRIVER_ID
3118
- : BROWSER_DRIVER_ID,
3630
+ request.storageMode === "ephemeral_context"
3631
+ ? EPHEMERAL_CHROMIUM_DRIVER_ID
3632
+ : engine === "lightpanda"
3633
+ ? LIGHTPANDA_DRIVER_ID
3634
+ : external
3635
+ ? EXTERNAL_BROWSER_DRIVER_ID
3636
+ : BROWSER_DRIVER_ID,
3119
3637
  engine,
3120
3638
  headless: attached ? false : request.headless,
3121
3639
  identityId: request.identityId ?? null,
@@ -3140,7 +3658,21 @@ function browserCreateInput(
3140
3658
  function browserRuntimeTransport(
3141
3659
  session: BrowserSessionValue,
3142
3660
  transport: PlacementBrowserTransport,
3661
+ placementInstanceId?: string,
3662
+ rootSecret?: string,
3143
3663
  ): PlacementBrowserTransport {
3664
+ if (browserSessionStorageMode(session) === "ephemeral_context") {
3665
+ if (transport.kind !== "managed" || !placementInstanceId || !rootSecret) {
3666
+ throw new BrowserControlUnsupportedError(
3667
+ "Ephemeral contexts cannot recover or change their placement",
3668
+ );
3669
+ }
3670
+ return {
3671
+ kind: "managed",
3672
+ engine: "chromium",
3673
+ ephemeralPartition: ephemeralBrowserPartition(session, placementInstanceId, rootSecret),
3674
+ };
3675
+ }
3144
3676
  if (transport.kind !== "managed") return transport;
3145
3677
  return {
3146
3678
  kind: "managed",
@@ -3202,6 +3734,11 @@ function assertCreateReplay(
3202
3734
  "BrowserSession create operation is bound to another placement",
3203
3735
  );
3204
3736
  }
3737
+ if (request.storageMode !== browserSessionStorageMode(session)) {
3738
+ throw new BrowserSessionOperationConflictError(
3739
+ "BrowserSession create operation is bound to another storage mode",
3740
+ );
3741
+ }
3205
3742
  const expectedEngine =
3206
3743
  session.placement.kind === "attached_device"
3207
3744
  ? "chrome"
@@ -3235,11 +3772,39 @@ function assertCreateReplay(
3235
3772
  }
3236
3773
  }
3237
3774
 
3238
- async function requireLinkedComputerBinding(
3775
+ async function ensureHeadedBrowserDisplayStack(
3776
+ session: BrowserControlPlacementSession,
3777
+ headless: boolean,
3778
+ linkedComputer: boolean,
3779
+ ): Promise<void> {
3780
+ if (
3781
+ !browserNeedsStandaloneDisplayStack({
3782
+ headless,
3783
+ linkedComputer,
3784
+ nativeBrowserControl: typeof session.ensureBrowserControl === "function",
3785
+ })
3786
+ )
3787
+ return;
3788
+ if (typeof session.exec !== "function" && typeof session.execCommand !== "function") return;
3789
+ await ensureDisplayStack(session, {
3790
+ telemetryContext: { callerKind: "viewer" },
3791
+ });
3792
+ }
3793
+
3794
+ export function browserNeedsStandaloneDisplayStack(input: {
3795
+ headless: boolean;
3796
+ linkedComputer: boolean;
3797
+ nativeBrowserControl: boolean;
3798
+ }): boolean {
3799
+ return !input.headless && !input.linkedComputer && !input.nativeBrowserControl;
3800
+ }
3801
+
3802
+ async function ensureLinkedComputerController(
3239
3803
  deps: ApiRouteDeps,
3240
3804
  grant: AccessGrant,
3241
3805
  browser: BrowserSessionValue,
3242
3806
  placement: BrowserPlacement,
3807
+ client: BrowserControlClient,
3243
3808
  ): Promise<{ computerSessionId: string; controllerGeneration: string } | null> {
3244
3809
  if (!browser.linkedComputerSessionId) return null;
3245
3810
  const record = await getComputerSessionControlRecord(deps.db, {
@@ -3257,10 +3822,42 @@ async function requireLinkedComputerBinding(
3257
3822
  "Linked ComputerSession is not active on the browser placement",
3258
3823
  );
3259
3824
  }
3260
- return {
3825
+ const reference = {
3261
3826
  computerSessionId: record.session.id,
3262
3827
  controllerGeneration: record.session.controller.controllerGeneration,
3263
3828
  };
3829
+ const tokens = deriveComputerSessionControllerTokens({
3830
+ rootSecret: browserAuthorityRoot(deps),
3831
+ accountId: grant.accountId,
3832
+ workspaceId: browser.workspaceId,
3833
+ placement: record.session.placement,
3834
+ placementInstanceId: placement.placementInstanceId,
3835
+ computerSessionId: record.session.id,
3836
+ controllerGeneration: record.session.controller.controllerGeneration,
3837
+ tokenGeneration: record.tokenGeneration,
3838
+ });
3839
+ const sessionClient = client.computerSessionClient({ reference, ...tokens });
3840
+ try {
3841
+ await sessionClient.heartbeat();
3842
+ } catch (error) {
3843
+ if (!isMissingLinkedComputerControllerSession(error)) throw error;
3844
+ await client.createComputerSession({
3845
+ ...reference,
3846
+ tokenGeneration: record.tokenGeneration,
3847
+ ...tokens,
3848
+ });
3849
+ }
3850
+ return reference;
3851
+ }
3852
+
3853
+ function isMissingLinkedComputerControllerSession(error: unknown): boolean {
3854
+ if (!(error instanceof BrowserControlRequestError)) return false;
3855
+ if (error.status === 401 && error.error.code === "permission_denied") return true;
3856
+ return (
3857
+ error.status === 404 &&
3858
+ error.error.code === "resource_not_found" &&
3859
+ ["computer session not found", "computer session is not active"].includes(error.error.message)
3860
+ );
3264
3861
  }
3265
3862
 
3266
3863
  async function requireBrowserRevisionRestoreAuthority(
@@ -3480,6 +4077,7 @@ async function ensureInteractionHolder(
3480
4077
  if (!placement.lease?.instanceId) {
3481
4078
  throw new BrowserSessionStateError("BrowserSession lease placement is unavailable");
3482
4079
  }
4080
+ const sandboxRuntime = await resolveSessionSandboxRuntime(deps.db, deps.settings, sourceSession);
3483
4081
  const acquired = await acquireLease(deps.db, {
3484
4082
  accountId: grant.accountId,
3485
4083
  workspaceId: sourceSession.workspaceId,
@@ -3488,9 +4086,14 @@ async function ensureInteractionHolder(
3488
4086
  holderId: interactionHolderId(browserSessionId),
3489
4087
  subjectId: sourceSession.id,
3490
4088
  backend: placement.lease.backend,
4089
+ warmBilling: {
4090
+ mode: deps.settings.sandboxWarmBillingMode,
4091
+ rateMicrosPerSecond: sandboxWarmRateMicrosPerSecond(deps.settings, placement.lease.backend),
4092
+ },
3491
4093
  os: placement.lease.os,
3492
- image: placement.lease.image,
3493
- rigVersionId: placement.lease.rigVersionId,
4094
+ image: sandboxRuntime.image,
4095
+ imagePolicy: "new_creates_only",
4096
+ rigVersionId: sourceSession.rigVersionId,
3494
4097
  leaseTtlMs: deps.settings.sandboxLeaseTtlMs,
3495
4098
  expectedEpoch: placement.lease.leaseEpoch,
3496
4099
  waitSignal,
@@ -3512,6 +4115,11 @@ async function ensureInteractionHolder(
3512
4115
  ).catch(() => undefined);
3513
4116
  throw new BrowserSessionStateError("BrowserSession placement fence changed; retry");
3514
4117
  }
4118
+ await renewSandboxProviderExpiration({
4119
+ backend: acquired.lease.backend as ApiRouteDeps["settings"]["sandboxBackend"],
4120
+ settings: deps.settings,
4121
+ instanceId: acquired.lease.instanceId,
4122
+ }).catch(() => false);
3515
4123
  return true;
3516
4124
  }
3517
4125
 
@@ -3758,6 +4366,7 @@ async function settleBrowserSessionSuspensionCaptureFailure(
3758
4366
  return await failBrowserSessionSuspension(deps.db, {
3759
4367
  ...input,
3760
4368
  ...(outcomeUnknown ? { state: "outcome_unknown" as const } : {}),
4369
+ missingControllerSession: error.status === 404 && isMissingBrowserControllerSession(error),
3761
4370
  error: error.error,
3762
4371
  });
3763
4372
  }
@@ -3895,6 +4504,66 @@ function optionalBoundedInteger(
3895
4504
  return parsed;
3896
4505
  }
3897
4506
 
4507
+ export function parseBrowserScreenshotOptions(
4508
+ query: URLSearchParams,
4509
+ ): BrowserControlScreenshotOptions {
4510
+ for (const key of ["fullPage", "format", "quality"]) {
4511
+ if (query.getAll(key).length > 1) {
4512
+ throw new HTTPException(400, { message: `duplicate browser screenshot ${key}` });
4513
+ }
4514
+ }
4515
+ const fullPage = query.get("fullPage");
4516
+ if (fullPage !== null && fullPage !== "true" && fullPage !== "false") {
4517
+ throw new HTTPException(400, { message: "invalid browser screenshot fullPage" });
4518
+ }
4519
+ const format = query.get("format");
4520
+ if (format !== null && format !== "jpeg" && format !== "png") {
4521
+ throw new HTTPException(400, { message: "invalid browser screenshot format" });
4522
+ }
4523
+ const quality = query.get("quality");
4524
+ const parsedQuality = quality === null ? null : optionalBoundedInteger(quality, 1, 100);
4525
+ if (quality !== null && parsedQuality === null) {
4526
+ throw new HTTPException(400, { message: "invalid browser screenshot quality" });
4527
+ }
4528
+ return {
4529
+ ...(fullPage === null ? {} : { fullPage: fullPage === "true" }),
4530
+ ...(format === null ? {} : { format }),
4531
+ ...(parsedQuality === null ? {} : { quality: parsedQuality }),
4532
+ };
4533
+ }
4534
+
4535
+ /** Keep a definite read timeout useful without publishing driver diagnostics. */
4536
+ export function browserScreenshotError(error: unknown): unknown {
4537
+ if (error instanceof BrowserControlRequestError && error.error.code === "timeout") {
4538
+ const failure = new ApiHttpError(504, {
4539
+ code: "upstream_unavailable",
4540
+ message:
4541
+ "This tab did not produce a screenshot in time. Other browser operations may still work.",
4542
+ retryable: error.retryable,
4543
+ outcomeUnknown: false,
4544
+ });
4545
+ failure.cause = error;
4546
+ return failure;
4547
+ }
4548
+ return error;
4549
+ }
4550
+
4551
+ export function browserScreenshotResponse(
4552
+ frame: Pick<BrowserControlFrame, "data" | "mediaType" | "metadataHeader">,
4553
+ ): Response {
4554
+ // Node Buffers can be views into a larger backing allocation. Copy exactly
4555
+ // the image bytes before constructing the Response.
4556
+ return new Response(Uint8Array.from(frame.data).buffer, {
4557
+ status: 200,
4558
+ headers: {
4559
+ "cache-control": "no-store",
4560
+ "content-type": frame.mediaType,
4561
+ "x-opengeni-browser-frame": frame.metadataHeader,
4562
+ ...USER_CONTENT_SECURITY_HEADERS,
4563
+ },
4564
+ });
4565
+ }
4566
+
3898
4567
  function isUuid(value: unknown): value is string {
3899
4568
  return (
3900
4569
  typeof value === "string" &&
@@ -3902,32 +4571,8 @@ function isUuid(value: unknown): value is string {
3902
4571
  );
3903
4572
  }
3904
4573
 
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;
4574
+ function requestOrigin(context: Context, settings: ApiRouteDeps["settings"]): string | null {
4575
+ return validateInteractionRequestOrigin(context.req.header("origin"), settings);
3931
4576
  }
3932
4577
 
3933
4578
  export function interactionActorForGrant(
@@ -3955,6 +4600,20 @@ function assertPlacementInstance(expected: string | null, actual: string): void
3955
4600
  }
3956
4601
  }
3957
4602
 
4603
+ /** End keeps the original controller token fence so a later Chrome generation
4604
+ * can still dispose the stale browserd session. */
4605
+ function attachedEndPlacementInstanceId(
4606
+ operation: ChannelAOperation,
4607
+ expectedPlacementInstanceId: string | null,
4608
+ liveGeneration: string,
4609
+ ): string {
4610
+ if (operation === "browser.end" && expectedPlacementInstanceId) {
4611
+ return expectedPlacementInstanceId;
4612
+ }
4613
+ assertPlacementInstance(expectedPlacementInstanceId, liveGeneration);
4614
+ return liveGeneration;
4615
+ }
4616
+
3958
4617
  function isTerminalOperation(state: string): boolean {
3959
4618
  return state === "completed" || state === "failed" || state === "outcome_unknown";
3960
4619
  }
@@ -4410,9 +5069,14 @@ async function finalizeBrowserDownloadFile(
4410
5069
  if (upload.expiresAt.getTime() <= Date.now()) {
4411
5070
  throw new InteractionResourceStateError("Browser download file upload authority expired");
4412
5071
  }
4413
- const head = await objectStorage.headFile(upload.file).catch(() => {
4414
- throw new HTTPException(503, { message: "browser download object is not available" });
5072
+ const pendingFile = upload.file;
5073
+ const head = await retryWhileMissing(async () => {
5074
+ if (!(await objectStorage.fileExists(pendingFile))) return null;
5075
+ return await objectStorage.headFile(pendingFile);
4415
5076
  });
5077
+ if (!head) {
5078
+ throw new HTTPException(503, { message: "browser download object is not available" });
5079
+ }
4416
5080
  if (
4417
5081
  Number(head.ContentLength ?? -1) !== upload.file.sizeBytes ||
4418
5082
  (head.ContentType !== undefined && head.ContentType !== upload.file.contentType) ||
@@ -4453,7 +5117,11 @@ async function recordBrowserDownloadFileUsage(
4453
5117
  }
4454
5118
 
4455
5119
  function browserRouteError(error: unknown): HTTPException {
5120
+ const connectedMachineError = interactionControlApiError(error, "browser");
5121
+ if (connectedMachineError) return connectedMachineError;
4456
5122
  if (error instanceof HTTPException) return error;
5123
+ if (error instanceof SandboxPaidComputeAdmissionError)
5124
+ return new HTTPException(402, { message: error.message, cause: error });
4457
5125
  if (error instanceof BrowserSessionNotFoundError) {
4458
5126
  return new HTTPException(404, { message: error.message, cause: error });
4459
5127
  }