@opengeni/api-router 0.30.1 → 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 (436) 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-office-import.d.ts +7 -4
  35. package/dist/editable-artifact-source-session.d.ts +3 -0
  36. package/dist/editable-artifact-websocket.d.ts +10 -18
  37. package/dist/editable-artifact-workspace-files.d.ts +1 -1
  38. package/dist/fatal-process-boundary.d.ts +25 -0
  39. package/dist/github-access.d.ts +31 -2
  40. package/dist/http/acting-person.d.ts +24 -0
  41. package/dist/http/api-contract-compat.d.ts +19 -0
  42. package/dist/http/api-error.d.ts +27 -1
  43. package/dist/http/cors.d.ts +17 -0
  44. package/dist/http/deprecation.d.ts +42 -0
  45. package/dist/http/interaction-control-error.d.ts +13 -0
  46. package/dist/http/keyed-admission.d.ts +12 -0
  47. package/dist/http/local-browser-boundary.d.ts +72 -0
  48. package/dist/http/metrics-listener.d.ts +22 -0
  49. package/dist/http/path-identifier.d.ts +17 -0
  50. package/dist/http/registered-route-label.d.ts +16 -0
  51. package/dist/http/request-body.d.ts +27 -0
  52. package/dist/http/request-source.d.ts +52 -0
  53. package/dist/http/sse.d.ts +17 -6
  54. package/dist/http/unmatched-route.d.ts +18 -0
  55. package/dist/http/user-content.d.ts +36 -0
  56. package/dist/http/websocket-upgrade-dispatch.d.ts +18 -0
  57. package/dist/index.d.ts +10 -3
  58. package/dist/index.js +1129 -166
  59. package/dist/index.js.map +1 -1
  60. package/dist/integration-endpoint-test.d.ts +20 -0
  61. package/dist/integrations/api-integrations.d.ts +2 -2
  62. package/dist/integrations/atlassian.d.ts +29 -3
  63. package/dist/integrations/connect-authority.d.ts +1 -0
  64. package/dist/integrations/connect-callback-return.d.ts +6 -0
  65. package/dist/integrations/fiken.d.ts +16 -2
  66. package/dist/integrations/github-app-connect.d.ts +47 -0
  67. package/dist/integrations/github-installation-proof.d.ts +3 -0
  68. package/dist/integrations/github-lens-connect.d.ts +116 -0
  69. package/dist/integrations/github-skill-source.d.ts +1 -5
  70. package/dist/integrations/google-drive.d.ts +25 -0
  71. package/dist/integrations/integration-commit-authority.d.ts +13 -0
  72. package/dist/integrations/marketplace-plugin.d.ts +2691 -0
  73. package/dist/integrations/oauth-client.d.ts +178 -18
  74. package/dist/integrations/oauth-profiles.d.ts +222 -0
  75. package/dist/integrations/oauth-return-path.d.ts +1 -0
  76. package/dist/integrations/personal-github-repositories.d.ts +84 -0
  77. package/dist/integrations/personal-github.d.ts +42 -0
  78. package/dist/integrations/pr-review-provider.d.ts +22 -0
  79. package/dist/integrations/provider-oauth.d.ts +19 -1
  80. package/dist/integrations/slack-app-home.d.ts +24 -0
  81. package/dist/integrations/slack-bot.d.ts +367 -12
  82. package/dist/integrations/slack-file-upload-flow.d.ts +62 -0
  83. package/dist/integrations/slack-install.d.ts +20 -0
  84. package/dist/integrations/slack-interactions.d.ts +113 -10
  85. package/dist/integrations/slack-mrkdwn.d.ts +41 -0
  86. package/dist/integrations/slack-routing.d.ts +120 -0
  87. package/dist/integrations/slack-session-defaults.d.ts +58 -0
  88. package/dist/integrations/slack-task-file-upload.d.ts +36 -0
  89. package/dist/integrations/social-oauth.d.ts +7 -0
  90. package/dist/interaction-agent-access.d.ts +15 -0
  91. package/dist/interaction-frame-proxy.d.ts +68 -0
  92. package/dist/interaction-metrics.d.ts +2 -0
  93. package/dist/mcp/action-catalog-types.d.ts +9 -0
  94. package/dist/mcp/action-catalog.gen.d.ts +2 -0
  95. package/dist/mcp/capability-account-readiness.d.ts +7 -0
  96. package/dist/mcp/child-read-evidence.d.ts +9 -0
  97. package/dist/mcp/command-read.d.ts +24 -0
  98. package/dist/mcp/company-profile-agent-admin.d.ts +44 -0
  99. package/dist/mcp/contract-input.d.ts +11 -0
  100. package/dist/mcp/documents.d.ts +7 -5
  101. package/dist/mcp/files.d.ts +2 -2
  102. package/dist/mcp/first-party-tool-permissions.d.ts +650 -0
  103. package/dist/mcp/knowledge-entries.d.ts +5 -0
  104. package/dist/mcp/orchestration-failure-diagnostic.d.ts +43 -0
  105. package/dist/mcp/receipts.d.ts +9 -0
  106. package/dist/mcp/request-abort.d.ts +19 -0
  107. package/dist/mcp/scheduled-task-input.d.ts +1189 -0
  108. package/dist/mcp/scheduled-task-view.d.ts +22 -4
  109. package/dist/mcp/server.d.ts +255 -3
  110. package/dist/mcp/session-event-view.d.ts +94 -0
  111. package/dist/mcp/session-view.d.ts +66 -2
  112. package/dist/mcp/session-wait.d.ts +174 -0
  113. package/dist/mcp-oauth.d.ts +52 -0
  114. package/dist/model-catalog.d.ts +11 -31
  115. package/dist/observability.d.ts +3 -1
  116. package/dist/organization-mcp.d.ts +52 -0
  117. package/dist/organization-recovery-notifications.d.ts +41 -0
  118. package/dist/routes/analytics-consent.d.ts +26 -0
  119. package/dist/routes/api-integrations.d.ts +25 -0
  120. package/dist/routes/api-keys.d.ts +17 -0
  121. package/dist/routes/artifact-catalog.d.ts +6 -0
  122. package/dist/routes/automations.d.ts +19 -0
  123. package/dist/routes/billing.d.ts +49 -0
  124. package/dist/routes/browser-sessions.d.ts +71 -3
  125. package/dist/routes/claude-subscription-accounts.d.ts +3 -0
  126. package/dist/routes/claude-subscription-oauth.d.ts +3 -0
  127. package/dist/routes/client-errors.d.ts +146 -0
  128. package/dist/routes/codex.d.ts +76 -2
  129. package/dist/routes/company-brain.d.ts +3 -0
  130. package/dist/routes/company-profile.d.ts +2 -1
  131. package/dist/routes/computer-sessions.d.ts +14 -0
  132. package/dist/routes/connect.d.ts +5 -0
  133. package/dist/routes/editable-artifacts.d.ts +2 -0
  134. package/dist/routes/external-identity-links.d.ts +6 -0
  135. package/dist/routes/feedback.d.ts +3 -0
  136. package/dist/routes/files.d.ts +4 -0
  137. package/dist/routes/github-browser-pages.d.ts +12 -0
  138. package/dist/routes/github.d.ts +2 -0
  139. package/dist/routes/insights-response-cache.d.ts +10 -0
  140. package/dist/routes/insights-usage.d.ts +12 -0
  141. package/dist/routes/insights.d.ts +30 -1
  142. package/dist/routes/knowledge.d.ts +3 -0
  143. package/dist/routes/managed-auth-session-sets.d.ts +26 -0
  144. package/dist/routes/managed-onboarding.d.ts +29 -0
  145. package/dist/routes/managed-sign-in-methods.d.ts +6 -0
  146. package/dist/routes/model-connection-access.d.ts +3 -0
  147. package/dist/routes/organization-integration-policy.d.ts +3 -0
  148. package/dist/routes/organization-integrations.d.ts +6 -0
  149. package/dist/routes/organization-mcp-connections.d.ts +3 -0
  150. package/dist/routes/organization-model-providers.d.ts +3 -0
  151. package/dist/routes/organization-recovery.d.ts +19 -0
  152. package/dist/routes/organization-service-accounts.d.ts +3 -0
  153. package/dist/routes/organization-sessions.d.ts +26 -0
  154. package/dist/routes/personal-github-git-broker.d.ts +29 -0
  155. package/dist/routes/personal-github.d.ts +3 -0
  156. package/dist/routes/pr-review-github.d.ts +3 -0
  157. package/dist/routes/pr-review.d.ts +3 -0
  158. package/dist/routes/session-artifact-associations.d.ts +4 -0
  159. package/dist/routes/session-history-imports.d.ts +9 -0
  160. package/dist/routes/sessions.d.ts +44 -6
  161. package/dist/routes/skill-content.d.ts +4 -0
  162. package/dist/routes/skill-install-authority.d.ts +6 -0
  163. package/dist/routes/subscription-account-pools.d.ts +34 -0
  164. package/dist/routes/subscription-pool-access.d.ts +16 -0
  165. package/dist/routes/supergrok.d.ts +6 -1
  166. package/dist/routes/usage-allowances.d.ts +12 -0
  167. package/dist/routes/user-resource-authorities.d.ts +3 -0
  168. package/dist/routes/workspace-artifacts.d.ts +2 -1
  169. package/dist/routes/workspace-capture.d.ts +10 -2
  170. package/dist/routes/workspace-integrations.d.ts +35 -0
  171. package/dist/routes/workspace-learning.d.ts +3 -0
  172. package/dist/routes/workspace-model-providers.d.ts +4 -0
  173. package/dist/routes/workspaces.d.ts +29 -0
  174. package/dist/sandbox/auth-callout.d.ts +8 -0
  175. package/dist/sandbox/channel-a.d.ts +19 -1
  176. package/dist/sandbox/connection-authority.d.ts +3 -0
  177. package/dist/sandbox/enrollment.d.ts +9 -1
  178. package/dist/sandbox/machines.d.ts +5 -2
  179. package/dist/sandbox/metrics-ingestion.d.ts +28 -20
  180. package/dist/sandbox/rematerialize.d.ts +2 -0
  181. package/dist/sandbox/session-attach-variable-sets.d.ts +30 -0
  182. package/dist/sandbox/viewer.d.ts +26 -1
  183. package/dist/sandbox-file-artifacts.d.ts +50 -0
  184. package/dist/scheduled-task-deletion.d.ts +13 -0
  185. package/dist/session-create-observability.d.ts +5 -0
  186. package/dist/session-event-view-MPS2OZD3.js +292 -0
  187. package/dist/session-event-view-MPS2OZD3.js.map +1 -0
  188. package/dist/site-session-origin.d.ts +13 -0
  189. package/dist/site-uploads.d.ts +41 -0
  190. package/dist/slack-reaction-files.d.ts +1 -1
  191. package/dist/temporal-schedule-cleanup.d.ts +1 -0
  192. package/dist/temporal-schedule-sync.d.ts +26 -0
  193. package/dist/transcription/providers/xai-subscription.d.ts +1 -0
  194. package/dist/transcription/service.d.ts +2 -1
  195. package/dist/work-discovery-observability.d.ts +33 -0
  196. package/dist/workspace-artifact-content.d.ts +28 -0
  197. package/dist/workspace-artifact-provenance.d.ts +7 -0
  198. package/dist/workspace-delete-observability.d.ts +10 -0
  199. package/dist/workspace-deletion.d.ts +6 -0
  200. package/dist/workspace-state-projection.d.ts +3 -5
  201. package/dist/workspace-tool-gateway-observability.d.ts +17 -0
  202. package/dist/workspace-tool-gateway.d.ts +123 -0
  203. package/dist/workspace-webhook-dispatch.d.ts +26 -0
  204. package/package.json +23 -19
  205. package/src/access-grant-rls.ts +75 -0
  206. package/src/admission-trace.ts +14 -0
  207. package/src/api-websocket.ts +23 -0
  208. package/src/app.ts +1974 -256
  209. package/src/artifact-catalog-cursor.ts +60 -0
  210. package/src/auth/managed-auth-attempt-context.ts +70 -0
  211. package/src/auth/managed-auth-rate-limits.ts +252 -0
  212. package/src/auth/managed-auth-session-adapter.ts +222 -0
  213. package/src/auth/managed-auth.ts +536 -60
  214. package/src/auth/managed-email.ts +174 -0
  215. package/src/auth/managed-sign-in-notifications.ts +81 -0
  216. package/src/auth/new-signups-gate.ts +49 -0
  217. package/src/auth/organization-user-setup.ts +307 -0
  218. package/src/auth/signup-funnel-metrics.ts +164 -0
  219. package/src/browser-controller-authority.ts +1 -186
  220. package/src/browser-ephemeral.ts +39 -0
  221. package/src/browser-state-authority.ts +1 -236
  222. package/src/claude-subscription-account-conflicts.ts +12 -0
  223. package/src/claude-subscription-account-usage.ts +48 -0
  224. package/src/claude-subscription-oauth.ts +362 -0
  225. package/src/claude-subscription-profile.ts +73 -0
  226. package/src/claude-subscription-usage.ts +97 -0
  227. package/src/claude-workspace-connection.ts +82 -0
  228. package/src/codemode.ts +194 -30
  229. package/src/codex-realtime.ts +8 -2
  230. package/src/company-brain-okf.ts +340 -0
  231. package/src/connected-machine-computer-access.ts +33 -0
  232. package/src/connection-ownership.ts +199 -0
  233. package/src/controller-data-plane.ts +81 -0
  234. package/src/editable-artifact-native-kernel.ts +26 -15
  235. package/src/editable-artifact-office-import.ts +77 -9
  236. package/src/editable-artifact-production.ts +23 -7
  237. package/src/editable-artifact-source-session.ts +123 -0
  238. package/src/editable-artifact-websocket.ts +27 -21
  239. package/src/editable-artifact-workspace-files.ts +31 -6
  240. package/src/fatal-process-boundary.ts +246 -0
  241. package/src/gateway-realtime.ts +1 -1
  242. package/src/github-access.ts +147 -5
  243. package/src/http/acting-person.ts +71 -0
  244. package/src/http/api-contract-compat.ts +49 -0
  245. package/src/http/api-error.ts +115 -1
  246. package/src/http/auth.ts +48 -0
  247. package/src/http/cors.ts +67 -0
  248. package/src/http/deprecation.ts +118 -0
  249. package/src/http/interaction-control-error.ts +164 -0
  250. package/src/http/keyed-admission.ts +29 -0
  251. package/src/http/local-browser-boundary.ts +343 -0
  252. package/src/http/metrics-listener.ts +57 -0
  253. package/src/http/path-identifier.ts +54 -0
  254. package/src/http/registered-route-label.ts +34 -0
  255. package/src/http/request-body.ts +127 -0
  256. package/src/http/request-source.ts +178 -0
  257. package/src/http/sse.ts +486 -132
  258. package/src/http/unmatched-route.ts +60 -0
  259. package/src/http/user-content.ts +158 -0
  260. package/src/http/websocket-upgrade-dispatch.ts +29 -0
  261. package/src/index.ts +157 -52
  262. package/src/integration-endpoint-test.ts +291 -0
  263. package/src/integrations/api-integrations.ts +98 -11
  264. package/src/integrations/atlassian.ts +474 -136
  265. package/src/integrations/connect-authority.ts +2 -0
  266. package/src/integrations/connect-callback-return.ts +86 -0
  267. package/src/integrations/fiken.ts +354 -64
  268. package/src/integrations/github-app-connect.ts +435 -0
  269. package/src/integrations/github-installation-proof.ts +60 -0
  270. package/src/integrations/github-lens-connect.ts +76 -0
  271. package/src/integrations/github-skill-source.ts +2 -142
  272. package/src/integrations/google-drive.ts +820 -222
  273. package/src/integrations/integration-commit-authority.ts +69 -0
  274. package/src/integrations/marketplace-plugin.ts +48 -0
  275. package/src/integrations/oauth-client.ts +1356 -459
  276. package/src/integrations/oauth-profiles.ts +537 -0
  277. package/src/integrations/oauth-return-path.ts +18 -0
  278. package/src/integrations/personal-github-repositories.ts +674 -0
  279. package/src/integrations/personal-github.ts +825 -0
  280. package/src/integrations/pr-review-provider.ts +246 -0
  281. package/src/integrations/provider-oauth.ts +250 -22
  282. package/src/integrations/slack-app-home.ts +300 -0
  283. package/src/integrations/slack-bot.ts +963 -239
  284. package/src/integrations/slack-file-upload-flow.ts +147 -0
  285. package/src/integrations/slack-install.ts +83 -0
  286. package/src/integrations/slack-interactions.ts +3170 -474
  287. package/src/integrations/slack-mrkdwn.ts +193 -0
  288. package/src/integrations/slack-routing.ts +337 -0
  289. package/src/integrations/slack-session-defaults.ts +224 -0
  290. package/src/integrations/slack-task-file-upload.ts +323 -0
  291. package/src/integrations/social-oauth.ts +213 -15
  292. package/src/interaction-agent-access.ts +48 -0
  293. package/src/interaction-frame-proxy.ts +409 -0
  294. package/src/interaction-metrics.ts +30 -0
  295. package/src/mcp/action-catalog-types.ts +9 -0
  296. package/src/mcp/action-catalog.gen.ts +6696 -0
  297. package/src/mcp/capability-account-readiness.ts +17 -0
  298. package/src/mcp/child-read-evidence.ts +19 -0
  299. package/src/mcp/command-read.ts +109 -0
  300. package/src/mcp/company-profile-agent-admin.ts +198 -0
  301. package/src/mcp/contract-input.ts +141 -0
  302. package/src/mcp/documents.ts +65 -377
  303. package/src/mcp/editable-artifacts.ts +12 -6
  304. package/src/mcp/files.ts +80 -38
  305. package/src/mcp/first-party-tool-permissions.ts +305 -0
  306. package/src/mcp/knowledge-entries.ts +322 -0
  307. package/src/mcp/orchestration-failure-diagnostic.ts +43 -0
  308. package/src/mcp/receipts.ts +34 -0
  309. package/src/mcp/request-abort.ts +44 -0
  310. package/src/mcp/scheduled-task-input.ts +76 -0
  311. package/src/mcp/scheduled-task-view.ts +44 -3
  312. package/src/mcp/server.ts +3030 -1267
  313. package/src/mcp/session-event-view.ts +383 -0
  314. package/src/mcp/session-view.ts +84 -3
  315. package/src/mcp/session-wait.ts +665 -0
  316. package/src/mcp-oauth.ts +1123 -0
  317. package/src/model-catalog.ts +91 -333
  318. package/src/observability.ts +15 -1
  319. package/src/organization-mcp.ts +397 -0
  320. package/src/organization-recovery-notifications.ts +103 -0
  321. package/src/routes/analytics-consent.ts +123 -0
  322. package/src/routes/api-integrations.ts +364 -74
  323. package/src/routes/api-keys.ts +336 -10
  324. package/src/routes/artifact-catalog.ts +279 -0
  325. package/src/routes/automations.ts +768 -0
  326. package/src/routes/billing.ts +461 -46
  327. package/src/routes/browser-sessions.ts +839 -131
  328. package/src/routes/canonical-human-identities.ts +90 -14
  329. package/src/routes/capabilities.ts +107 -6
  330. package/src/routes/channels.ts +20 -3
  331. package/src/routes/claude-subscription-accounts.ts +334 -0
  332. package/src/routes/claude-subscription-oauth.ts +142 -0
  333. package/src/routes/client-errors.ts +362 -0
  334. package/src/routes/codex.ts +833 -116
  335. package/src/routes/company-brain.ts +322 -0
  336. package/src/routes/company-profile.ts +65 -1
  337. package/src/routes/computer-sessions.ts +557 -106
  338. package/src/routes/connect.ts +1938 -0
  339. package/src/routes/connections.ts +1040 -349
  340. package/src/routes/documents.ts +488 -287
  341. package/src/routes/editable-artifacts.ts +79 -9
  342. package/src/routes/enrollments.ts +135 -39
  343. package/src/routes/environments.ts +154 -58
  344. package/src/routes/external-identity-links.ts +201 -0
  345. package/src/routes/feedback.ts +113 -0
  346. package/src/routes/files.ts +337 -39
  347. package/src/routes/github-browser-pages.ts +162 -0
  348. package/src/routes/github.ts +446 -144
  349. package/src/routes/insights-response-cache.ts +83 -0
  350. package/src/routes/insights-usage.ts +345 -0
  351. package/src/routes/insights.ts +153 -19
  352. package/src/routes/install.ts +133 -38
  353. package/src/routes/integration-facets.ts +107 -33
  354. package/src/routes/interaction-resources.ts +30 -3
  355. package/src/routes/knowledge.ts +488 -0
  356. package/src/routes/machines.ts +313 -15
  357. package/src/routes/managed-auth-session-sets.ts +1218 -0
  358. package/src/routes/managed-onboarding.ts +337 -0
  359. package/src/routes/managed-sign-in-methods.ts +433 -0
  360. package/src/routes/memory-slack-publications.ts +3 -2
  361. package/src/routes/model-connection-access.ts +208 -0
  362. package/src/routes/organization-integration-policy.ts +116 -0
  363. package/src/routes/organization-integrations.ts +326 -0
  364. package/src/routes/organization-mcp-connections.ts +178 -0
  365. package/src/routes/organization-memberships.ts +1019 -27
  366. package/src/routes/organization-model-providers.ts +293 -0
  367. package/src/routes/organization-recovery.ts +325 -0
  368. package/src/routes/organization-service-accounts.ts +128 -0
  369. package/src/routes/organization-sessions.ts +350 -0
  370. package/src/routes/personal-github-git-broker.ts +785 -0
  371. package/src/routes/personal-github.ts +370 -0
  372. package/src/routes/plugins.ts +251 -22
  373. package/src/routes/pr-review-github.ts +881 -0
  374. package/src/routes/pr-review.ts +622 -0
  375. package/src/routes/preference-registry.ts +12 -93
  376. package/src/routes/rigs.ts +167 -50
  377. package/src/routes/scheduled-tasks.ts +346 -41
  378. package/src/routes/session-artifact-associations.ts +87 -0
  379. package/src/routes/session-history-imports.ts +280 -0
  380. package/src/routes/sessions.ts +2306 -264
  381. package/src/routes/skill-content.ts +297 -0
  382. package/src/routes/skill-install-authority.ts +64 -0
  383. package/src/routes/skills.ts +62 -9
  384. package/src/routes/social.ts +46 -18
  385. package/src/routes/subscription-account-pools.ts +436 -0
  386. package/src/routes/subscription-pool-access.ts +126 -0
  387. package/src/routes/supergrok.ts +219 -303
  388. package/src/routes/transcription-recordings.ts +38 -3
  389. package/src/routes/transcriptions.ts +4 -0
  390. package/src/routes/usage-allowances.ts +378 -0
  391. package/src/routes/user-resource-authorities.ts +138 -0
  392. package/src/routes/video-generation.ts +14 -10
  393. package/src/routes/workspace-artifacts.ts +275 -69
  394. package/src/routes/workspace-capture.ts +21 -0
  395. package/src/routes/workspace-integrations.ts +576 -0
  396. package/src/routes/workspace-learning.ts +159 -0
  397. package/src/routes/workspace-model-providers.ts +148 -0
  398. package/src/routes/workspace-state.ts +38 -25
  399. package/src/routes/workspaces.ts +1004 -127
  400. package/src/sandbox/auth-callout.ts +96 -19
  401. package/src/sandbox/channel-a.ts +323 -24
  402. package/src/sandbox/connection-authority.ts +3 -0
  403. package/src/sandbox/enrollment.ts +81 -6
  404. package/src/sandbox/machines.ts +218 -76
  405. package/src/sandbox/metrics-ingestion.ts +411 -62
  406. package/src/sandbox/rematerialize.ts +121 -19
  407. package/src/sandbox/session-attach-variable-sets.ts +131 -0
  408. package/src/sandbox/viewer.ts +280 -25
  409. package/src/sandbox-file-artifacts.ts +413 -0
  410. package/src/scheduled-task-deletion.ts +138 -0
  411. package/src/session-create-observability.ts +46 -0
  412. package/src/session-realtime-context.ts +6 -3
  413. package/src/site-session-origin.ts +48 -0
  414. package/src/site-uploads.ts +224 -0
  415. package/src/slack-reaction-files.ts +16 -5
  416. package/src/temporal-schedule-cleanup.ts +13 -0
  417. package/src/temporal-schedule-sync.ts +74 -0
  418. package/src/transcription/providers/codex-subscription.ts +2 -0
  419. package/src/transcription/providers/openai.ts +1 -0
  420. package/src/transcription/providers/xai-subscription.ts +49 -3
  421. package/src/transcription/segmenter.ts +8 -2
  422. package/src/transcription/service.ts +43 -6
  423. package/src/work-discovery-observability.ts +121 -0
  424. package/src/workspace-artifact-content.ts +144 -0
  425. package/src/workspace-artifact-provenance.ts +104 -0
  426. package/src/workspace-delete-observability.ts +75 -0
  427. package/src/workspace-deletion.ts +73 -0
  428. package/src/workspace-state-projection.ts +18 -148
  429. package/src/workspace-tool-gateway-observability.ts +100 -0
  430. package/src/workspace-tool-gateway.ts +870 -0
  431. package/src/workspace-webhook-dispatch.ts +190 -0
  432. package/src/xai-subscription-auth.ts +9 -2
  433. package/dist/chunk-2PQMSRCB.js +0 -49759
  434. package/dist/chunk-2PQMSRCB.js.map +0 -1
  435. package/dist/routes/packs.d.ts +0 -3
  436. 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,12 +109,13 @@ 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
- getFiles,
118
+ getFilesForSubject,
104
119
  getFileUpload,
105
120
  getAuthRun,
106
121
  getExternalAuthPreparation,
@@ -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,9 +159,11 @@ import {
142
159
  recordWorkspaceUsage,
143
160
  requireLimit,
144
161
  relayConfigFromSettings,
162
+ resolveSessionSandboxRuntime,
145
163
  SessionAuthorizationDeniedError,
146
164
  SessionAuthorizationUnavailableError,
147
165
  type ApiRouteDeps,
166
+ type ResolvedSessionAuthorization,
148
167
  } from "@opengeni/core";
149
168
  import {
150
169
  BrowserControlProtocolError,
@@ -157,16 +176,21 @@ import {
157
176
  buildStreamUrl,
158
177
  exposedPortEndpointFromUrl,
159
178
  buildSelfhostedBackendSession,
179
+ ensureDisplayStack,
160
180
  mintStreamToken,
161
181
  NatsControlRpc,
162
182
  NatsOpStreamTransport,
163
183
  provisionBrowserControlClient,
184
+ renewSandboxProviderExpiration,
164
185
  type BrowserControlPlacementSession,
186
+ type BrowserControlScreenshotOptions,
187
+ type BrowserControlFrame,
165
188
  type PlacementBrowserStateCaptureReceipt,
166
189
  type PlacementBrowserNetworkRoute,
167
190
  type PlacementBrowserTransport,
168
191
  type RestorePlacementBrowserStateInput,
169
192
  } from "@opengeni/runtime/sandbox";
193
+ import { retryWhileMissing } from "@opengeni/storage";
170
194
  import type { Context, Hono } from "hono";
171
195
  import { HTTPException } from "hono/http-exception";
172
196
  import type { ContentfulStatusCode } from "hono/utils/http-status";
@@ -175,8 +199,17 @@ import {
175
199
  deriveBrowserNetworkRouteAuthorityDigest,
176
200
  deriveBrowserSessionControllerTokens,
177
201
  deriveBrowserViewGrantToken,
202
+ deriveComputerSessionControllerTokens,
178
203
  } from "../browser-controller-authority";
179
- 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";
180
213
  import { withInteractionHolderHeartbeat } from "../interaction-holder-heartbeat";
181
214
  import {
182
215
  browserStateArtifactAad,
@@ -191,7 +224,13 @@ import {
191
224
  resolveProtectedAuthFieldValues,
192
225
  } from "../browser-auth-broker";
193
226
  import { managedNetworkRouteForPlacement } from "../browser-network-route";
194
- 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";
195
234
  import {
196
235
  observeAuthMutation,
197
236
  observeBrowserActionResult,
@@ -200,6 +239,7 @@ import {
200
239
  } from "../interaction-metrics";
201
240
  import { withChannelA, withChannelARead, type ChannelAOperation } from "../sandbox/channel-a";
202
241
  import { sanitizeFilename } from "./files";
242
+ import { USER_CONTENT_SECURITY_HEADERS } from "../http/user-content";
203
243
 
204
244
  const BROWSER_DRIVER_ID = "opengeni.cdp.v1";
205
245
  const LIGHTPANDA_DRIVER_ID = "opengeni.lightpanda.cdp.v1";
@@ -264,13 +304,15 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
264
304
  app.get("/v1/workspaces/:workspaceId/browser-sessions", async (context) => {
265
305
  const workspaceId = context.req.param("workspaceId") ?? "";
266
306
  const grant = await requireAccessGrant(context, deps, workspaceId, "sessions:read");
307
+ const listed = await listBrowserSessions(deps.db, {
308
+ accountId: grant.accountId,
309
+ workspaceId,
310
+ });
267
311
  return context.json(
268
- BrowserSessionListResponse.parse(
269
- await listBrowserSessions(deps.db, {
270
- accountId: grant.accountId,
271
- workspaceId,
272
- }),
273
- ),
312
+ BrowserSessionListResponse.parse({
313
+ ...listed,
314
+ sessions: await filterInteractionSessionsForGrant(deps, grant, listed.sessions),
315
+ }),
274
316
  );
275
317
  });
276
318
 
@@ -297,10 +339,15 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
297
339
  const request = await parseJsonBody(context, CreateBrowserSessionRequest);
298
340
  const startedAtMs = performance.now();
299
341
  await authorizeSourceSession(deps, grant, request.sessionId, "session.control");
300
- const origin = requestOrigin(context, deps.settings.corsAllowOriginRegex);
342
+ const origin = requestOrigin(context, deps.settings);
301
343
  const authority = browserAuthorityRoot(deps);
344
+ let prepared: Awaited<ReturnType<typeof prepareBrowserSessionCreate>> | null = null;
302
345
 
303
346
  try {
347
+ assertEphemeralBrowserCreateEnabled(
348
+ request,
349
+ deps.settings.experimentalBrowserContextPoolEnabled,
350
+ );
304
351
  const existing = await findBrowserSessionControlRecordByOperation(deps.db, {
305
352
  accountId: grant.accountId,
306
353
  workspaceId,
@@ -312,7 +359,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
312
359
  );
313
360
  }
314
361
  if (existing) assertCreateReplay(request, existing.session);
315
- let prepared = existing
362
+ prepared = existing
316
363
  ? await prepareBrowserSessionCreate(
317
364
  deps.db,
318
365
  browserCreateInput(grant, workspaceId, request, existing.session.placement),
@@ -324,6 +371,20 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
324
371
  return context.json(parsed, 200);
325
372
  }
326
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
+
327
388
  const sourceSession = await requireSourceSession(deps, workspaceId, request.sessionId);
328
389
  const response = await withBrowserPlacement(
329
390
  sourceSession,
@@ -400,12 +461,6 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
400
461
  });
401
462
  const preparedSession = prepared.session;
402
463
  const controllerGeneration = requireOperationGeneration(record);
403
- const linkedComputer = await requireLinkedComputerBinding(
404
- deps,
405
- grant,
406
- prepared.session,
407
- placement,
408
- );
409
464
  const adminToken = deriveBrowserControllerAdminToken({
410
465
  rootSecret: authority,
411
466
  accountId: grant.accountId,
@@ -431,8 +486,15 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
431
486
  placement,
432
487
  adminToken,
433
488
  origin,
434
- async (client) =>
435
- 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(
436
498
  deps,
437
499
  {
438
500
  grant,
@@ -443,20 +505,32 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
443
505
  resourceId: preparedSession.id,
444
506
  controllerGeneration,
445
507
  },
446
- async () =>
447
- await client.createSession({
508
+ async () => {
509
+ await ensureHeadedBrowserDisplayStack(
510
+ placement.session,
511
+ preparedSession.headless,
512
+ linkedComputer !== null,
513
+ );
514
+ return await client.createSession({
448
515
  browserSessionId: preparedSession.id,
449
516
  controllerGeneration,
450
517
  tokenGeneration: record.tokenGeneration,
451
518
  ...tokens,
452
519
  headed: !preparedSession.headless,
453
- transport: browserRuntimeTransport(preparedSession, placement.transport),
520
+ transport: browserRuntimeTransport(
521
+ preparedSession,
522
+ placement.transport,
523
+ placement.placementInstanceId,
524
+ authority,
525
+ ),
454
526
  ...(linkedComputer ? { linkedComputer } : {}),
455
527
  ...(networkRoute ? { networkRoute } : {}),
456
528
  ...(request.initialUrl ? { initialUrl: request.initialUrl } : {}),
457
529
  ...(restore ? { restore } : {}),
458
- }),
459
- ),
530
+ });
531
+ },
532
+ );
533
+ },
460
534
  );
461
535
  await cacheBrowserControllerPlacement(grant, workspaceId, placement).catch(
462
536
  () => placement,
@@ -518,6 +592,18 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
518
592
  parsed.operation.state === "completed" && !parsed.operation.replayed ? 201 : 200,
519
593
  );
520
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
+ }
521
607
  throw browserRouteError(error);
522
608
  }
523
609
  });
@@ -907,6 +993,82 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
907
993
  },
908
994
  );
909
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
+
910
1072
  app.post(
911
1073
  "/v1/workspaces/:workspaceId/browser-sessions/:browserSessionId/actions",
912
1074
  async (context) => {
@@ -927,7 +1089,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
927
1089
  browserSessionId,
928
1090
  "session.control",
929
1091
  "browser.action",
930
- async ({ sessionClient, binding, record }) => {
1092
+ async ({ sessionClient, binding, record, sourceAuthorization }) => {
931
1093
  const command = BrowserActionCommand.parse({
932
1094
  protocolVersion: BROWSER_CONTROL_PROTOCOL_VERSION,
933
1095
  operationId: request.operationId,
@@ -957,27 +1119,25 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
957
1119
  message: "object storage is not configured",
958
1120
  });
959
1121
  }
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
- }
1122
+ const files = await getFilesForSubject(deps.db, {
1123
+ accountId: grant.accountId,
1124
+ workspaceId,
1125
+ subjectId: browserFileAuthoritySubjectId(grant, sourceAuthorization),
1126
+ fileIds: workspaceFileIds,
1127
+ });
1128
+ const ordered = requireAuthorizedBrowserUploadFiles(workspaceFileIds, files);
969
1129
  const authorities = await Promise.all(
970
1130
  ordered.map(async (file) => {
971
1131
  const signed = await deps.objectStorage!.createGetUrl({
972
- key: file!.objectKey,
1132
+ key: file.objectKey,
973
1133
  expiresInSeconds: BROWSER_WORKSPACE_FILE_AUTHORITY_TTL_SECONDS,
974
1134
  audience: signedUrlAudienceForPlacement(record.session.placement),
975
1135
  });
976
1136
  return {
977
- fileId: file!.id,
978
- safeFilename: browserUploadFilename(file!.safeFilename),
979
- sizeBytes: file!.sizeBytes,
980
- sha256: browserUploadSha256(file!.sha256),
1137
+ fileId: file.id,
1138
+ safeFilename: browserUploadFilename(file.safeFilename),
1139
+ sizeBytes: file.sizeBytes,
1140
+ sha256: browserUploadSha256(file.sha256),
981
1141
  download: {
982
1142
  url: signed.url,
983
1143
  expiresAt: signed.expiresAt.toISOString(),
@@ -1664,7 +1824,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
1664
1824
  context,
1665
1825
  "stream:view",
1666
1826
  );
1667
- const origin = requestOrigin(context, deps.settings.corsAllowOriginRegex);
1827
+ const origin = requestOrigin(context, deps.settings);
1668
1828
  const request = await parseJsonBody(context, BrowserSessionAttachmentRequest);
1669
1829
  const result = await withActiveBrowserController(
1670
1830
  context,
@@ -1706,7 +1866,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
1706
1866
  browserSessionId,
1707
1867
  controllerGeneration: binding.controllerGeneration,
1708
1868
  };
1709
- await client.createViewGrant(reference, {
1869
+ const viewGrant = await client.createViewGrant(reference, {
1710
1870
  grantId,
1711
1871
  token,
1712
1872
  expiresAt,
@@ -1720,6 +1880,18 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
1720
1880
  });
1721
1881
  const stream = relayed
1722
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
+ }
1723
1895
  const relayToken = await mintStreamToken(relaySecret!, {
1724
1896
  workspaceId,
1725
1897
  sessionId: record.sourceSessionId,
@@ -1727,6 +1899,8 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
1727
1899
  leaseEpoch: record.tokenGeneration,
1728
1900
  port: relayed.channel.port,
1729
1901
  ttlSeconds: request.expiresInSeconds,
1902
+ subjectId: grant.subjectId,
1903
+ authorityEpoch: relayAuthorityEpoch,
1730
1904
  });
1731
1905
  return {
1732
1906
  kind: "relay" as const,
@@ -1741,19 +1915,53 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
1741
1915
  },
1742
1916
  };
1743
1917
  })()
1744
- : {
1745
- kind: "direct_websocket" as const,
1746
- url: await client.frameStreamUrl(reference, request.targetId, request.stream),
1747
- protocols: [
1918
+ : await (async () => {
1919
+ const protocols = [
1748
1920
  BROWSER_CONTROL_WEBSOCKET_PROTOCOL,
1749
1921
  `${BROWSER_CONTROL_WEBSOCKET_BEARER_PREFIX}${token}`,
1750
- ],
1751
- };
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
+ })();
1752
1956
  return BrowserSessionAttachment.parse({
1753
1957
  browserSessionId,
1754
1958
  controllerGeneration: binding.controllerGeneration,
1755
1959
  targetId: request.targetId,
1756
1960
  stream,
1961
+ ...(viewGrant.fencedInputBatches === true ? { fencedInputBatches: true } : {}),
1962
+ ...(viewGrant.focusedInputObservations === true
1963
+ ? { focusedInputObservations: true }
1964
+ : {}),
1757
1965
  expiresAt,
1758
1966
  });
1759
1967
  },
@@ -1959,7 +2167,8 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
1959
2167
  const browserSessionId = requireUuidParam(context, "browserSessionId");
1960
2168
  const request = await parseJsonBody(context, BrowserSessionLifecycleRequest);
1961
2169
  const startedAtMs = performance.now();
1962
- const origin = requestOrigin(context, deps.settings.corsAllowOriginRegex);
2170
+ const origin = requestOrigin(context, deps.settings);
2171
+ let preparedForDispatch = false;
1963
2172
  try {
1964
2173
  const before = await getBrowserSessionControlRecord(deps.db, {
1965
2174
  accountId: grant.accountId,
@@ -2011,6 +2220,8 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2011
2220
  return context.json(parsed, 200);
2012
2221
  }
2013
2222
 
2223
+ preparedForDispatch = true;
2224
+
2014
2225
  const record = await getBrowserSessionControlRecord(deps.db, {
2015
2226
  accountId: grant.accountId,
2016
2227
  workspaceId,
@@ -2172,6 +2383,15 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2172
2383
  observeLifecycleResult(deps.observability, startedAtMs, parsed);
2173
2384
  return context.json(parsed, 200);
2174
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
+ }
2175
2395
  throw browserRouteError(error);
2176
2396
  }
2177
2397
  },
@@ -2185,7 +2405,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2185
2405
  const browserSessionId = requireUuidParam(context, "browserSessionId");
2186
2406
  const request = await parseJsonBody(context, BrowserSessionLifecycleRequest);
2187
2407
  const startedAtMs = performance.now();
2188
- const origin = requestOrigin(context, deps.settings.corsAllowOriginRegex);
2408
+ const origin = requestOrigin(context, deps.settings);
2189
2409
  let restore: RestorePlacementBrowserStateInput | null = null;
2190
2410
  try {
2191
2411
  let before = await getBrowserSessionControlRecord(deps.db, {
@@ -2304,12 +2524,6 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2304
2524
  throw error;
2305
2525
  });
2306
2526
  const controllerGeneration = requireOperationGeneration(record);
2307
- const linkedComputer = await requireLinkedComputerBinding(
2308
- deps,
2309
- grant,
2310
- prepared.session,
2311
- placement,
2312
- );
2313
2527
  const adminToken = deriveBrowserControllerAdminToken({
2314
2528
  rootSecret: authority,
2315
2529
  accountId: grant.accountId,
@@ -2333,6 +2547,13 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2333
2547
  ...(origin ? { allowedOrigins: [origin] } : {}),
2334
2548
  });
2335
2549
  try {
2550
+ const linkedComputer = await ensureLinkedComputerController(
2551
+ deps,
2552
+ grant,
2553
+ prepared.session,
2554
+ placement,
2555
+ client,
2556
+ );
2336
2557
  await withInteractionHolderHeartbeat(
2337
2558
  deps,
2338
2559
  {
@@ -2344,8 +2565,13 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2344
2565
  resourceId: browserSessionId,
2345
2566
  controllerGeneration,
2346
2567
  },
2347
- async () =>
2348
- await client.createSession({
2568
+ async () => {
2569
+ await ensureHeadedBrowserDisplayStack(
2570
+ placement.session,
2571
+ prepared.session.headless,
2572
+ linkedComputer !== null,
2573
+ );
2574
+ return await client.createSession({
2349
2575
  browserSessionId,
2350
2576
  controllerGeneration,
2351
2577
  tokenGeneration: record.tokenGeneration,
@@ -2355,7 +2581,8 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2355
2581
  ...(linkedComputer ? { linkedComputer } : {}),
2356
2582
  ...(networkRoute ? { networkRoute } : {}),
2357
2583
  restore: restore!,
2358
- }),
2584
+ });
2585
+ },
2359
2586
  );
2360
2587
  } catch (error) {
2361
2588
  if (!isDefiniteBrowserControllerFailure(error)) throw error;
@@ -2427,7 +2654,12 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2427
2654
  const browserSessionId = requireUuidParam(context, "browserSessionId");
2428
2655
  const request = await parseJsonBody(context, BrowserSessionLifecycleRequest);
2429
2656
  const startedAtMs = performance.now();
2430
- 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;
2431
2663
  try {
2432
2664
  const before = await getBrowserSessionControlRecord(deps.db, {
2433
2665
  accountId: grant.accountId,
@@ -2442,6 +2674,9 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2442
2674
  browserSessionId,
2443
2675
  operationId: request.operationId,
2444
2676
  actorSubjectId: grant.subjectId,
2677
+ expectedLifecycle: before.session.lifecycle,
2678
+ expectedFailureCode: before.session.failureCode,
2679
+ expectedControllerGeneration: before.session.controller?.controllerGeneration ?? null,
2445
2680
  });
2446
2681
  if (isTerminalOperation(prepared.operation.state)) {
2447
2682
  if (prepared.operation.state === "completed") {
@@ -2457,6 +2692,13 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2457
2692
  observeLifecycleResult(deps.observability, startedAtMs, parsed);
2458
2693
  return context.json(parsed, 200);
2459
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
+ }
2460
2702
 
2461
2703
  const record = await getBrowserSessionControlRecord(deps.db, {
2462
2704
  accountId: grant.accountId,
@@ -2550,6 +2792,16 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2550
2792
  observeLifecycleResult(deps.observability, startedAtMs, parsed);
2551
2793
  return context.json(parsed, 200);
2552
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
+ }
2553
2805
  throw browserRouteError(error);
2554
2806
  }
2555
2807
  },
@@ -2563,6 +2815,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2563
2815
  operation: ChannelAOperation,
2564
2816
  waitSignal: AbortSignal,
2565
2817
  callback: (placement: BrowserPlacement) => Promise<T>,
2818
+ allowOperationReplay = true,
2566
2819
  ): Promise<T> {
2567
2820
  if (expectedPlacement?.kind === "attached_device") {
2568
2821
  waitSignal.throwIfAborted();
@@ -2574,18 +2827,29 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2574
2827
  if (device.state !== "connected") {
2575
2828
  throw new BrowserSessionStateError("Attached browser is disconnected");
2576
2829
  }
2577
- const enrollment = await getEnrollment(
2830
+ const enrollment = await getLiveEnrollmentConnection(
2578
2831
  deps.db,
2579
2832
  sourceSession.workspaceId,
2580
2833
  device.enrollmentId,
2581
2834
  );
2582
- if (!enrollment || enrollment.status !== "active") {
2835
+ if (!enrollment || enrollment.status !== "active" || !enrollment.connectionInstanceId) {
2583
2836
  throw new BrowserSessionStateError("Attached browser machine is unavailable");
2584
2837
  }
2585
- 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
+ );
2586
2848
  const built = await buildSelfhostedBackendSession({
2587
2849
  workspaceId: sourceSession.workspaceId,
2588
2850
  agentId: device.enrollmentId,
2851
+ connectionInstanceId: enrollment.connectionInstanceId,
2852
+ workspaceRoot: enrollment.workspaceRoot,
2589
2853
  relay: relayConfigFromSettings(deps.settings),
2590
2854
  controlRpcFactory: () => new NatsControlRpc(async () => deps.bus.getRequestConnection()),
2591
2855
  // Browser-profile generation is the physical controller fence. Epoch 0
@@ -2594,6 +2858,11 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2594
2858
  epoch: 0,
2595
2859
  timeoutMs: deps.settings.sandboxSelfhostedControlTimeoutMs,
2596
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,
2597
2866
  ...(deps.settings.agentOpStreamEnabled === true &&
2598
2867
  enrollment.opStream === true &&
2599
2868
  deps.bus.getOpStreamConnection
@@ -2610,13 +2879,13 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2610
2879
  return await callback({
2611
2880
  placement: expectedPlacement,
2612
2881
  controllerHostSandboxGroupId: null,
2613
- placementInstanceId: device.connectionGeneration,
2882
+ placementInstanceId,
2614
2883
  session: built.session as unknown as BrowserControlPlacementSession,
2615
2884
  lease: null,
2616
2885
  transport: {
2617
2886
  kind: "attached_chrome",
2618
2887
  deviceId: device.id,
2619
- connectionGeneration: device.connectionGeneration,
2888
+ connectionGeneration: placementInstanceId,
2620
2889
  browserName: device.browserName,
2621
2890
  browserVersion: device.browserVersion,
2622
2891
  },
@@ -2639,6 +2908,12 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2639
2908
  waitSignal,
2640
2909
  operation,
2641
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
+ : {}),
2642
2917
  },
2643
2918
  async (handle) => {
2644
2919
  if (expectedPlacement?.kind === "sandbox_group") {
@@ -2682,8 +2957,16 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2682
2957
  resolved.kind !== "selfhosted" ||
2683
2958
  resolved.sandboxId !== expectedPlacement.sandboxId
2684
2959
  ) {
2685
- throw new BrowserSessionStateError(
2686
- "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,
2687
2970
  );
2688
2971
  }
2689
2972
  const placementInstanceId = resolved.providerInstanceId ?? expectedPlacement.sandboxId;
@@ -2780,7 +3063,9 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2780
3063
  !lease ||
2781
3064
  (lease.liveness !== "warm" && lease.liveness !== "draining") ||
2782
3065
  lease.instanceId !== binding.placementInstanceId ||
2783
- !lease.controllerDataPlaneUrl
3066
+ !lease.controllerDataPlaneUrl ||
3067
+ (lease.backend === "opensandbox" && deps.settings.openSandboxSignedEndpoints) ||
3068
+ !controllerCachedUrlIsUsable(lease.controllerDataPlaneUrl)
2784
3069
  ) {
2785
3070
  return null;
2786
3071
  }
@@ -2806,8 +3091,29 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2806
3091
  if (!sandboxGroupId || !placement.lease?.instanceId || !placement.session.resolveExposedPort) {
2807
3092
  return placement;
2808
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
+ }
2809
3106
  const endpoint = await placement.session.resolveExposedPort(BROWSER_CONTROL_PORT);
2810
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
+ }
2811
3117
  const lease = await recordLeaseControllerDataPlaneUrl(deps.db, {
2812
3118
  accountId: grant.accountId,
2813
3119
  workspaceId,
@@ -2817,6 +3123,9 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2817
3123
  controllerDataPlaneUrl: url,
2818
3124
  });
2819
3125
  if (!lease) return placement;
3126
+ if (!controllerCacheAllowsHostFetch(url)) {
3127
+ return { ...placement, lease };
3128
+ }
2820
3129
  return { ...placement, session: controllerOnlySession(url), lease };
2821
3130
  }
2822
3131
 
@@ -2832,7 +3141,16 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2832
3141
  const sandboxGroupId = placement.controllerHostSandboxGroupId;
2833
3142
  const cachedUrl = placement.lease?.controllerDataPlaneUrl;
2834
3143
  return await withCachedController({
2835
- 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,
2836
3154
  createCachedClient: (url) =>
2837
3155
  new BrowserControlClient(controllerOnlySession(url), { adminToken }),
2838
3156
  prepareCachedClient: async (client) => {
@@ -2874,6 +3192,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2874
3192
  record: BrowserSessionControlRecord;
2875
3193
  binding: NonNullable<BrowserSessionControlRecord["session"]["controller"]>;
2876
3194
  placement: BrowserPlacement;
3195
+ sourceAuthorization: ResolvedSessionAuthorization | null;
2877
3196
  }) => Promise<T>,
2878
3197
  recoverMissing = true,
2879
3198
  ): Promise<T> {
@@ -2883,7 +3202,23 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2883
3202
  workspaceId,
2884
3203
  browserSessionId,
2885
3204
  });
2886
- await authorizeSourceSession(deps, grant, record.sourceSessionId, authorizationOperation);
3205
+ const sourceAuthorization = await authorizeSourceSession(
3206
+ deps,
3207
+ grant,
3208
+ record.sourceSessionId,
3209
+ authorizationOperation,
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
+ }
2887
3222
  if (record.session.lifecycle !== "active" || !record.session.controller) {
2888
3223
  throw new BrowserSessionStateError("BrowserSession is not active");
2889
3224
  }
@@ -2897,10 +3232,18 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2897
3232
  if (!admitted) {
2898
3233
  throw new BrowserSessionStateError("BrowserSession controller authority changed");
2899
3234
  }
2900
- 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> => {
2901
3244
  // An active binding already proves browserd was provisioned. Reusing
2902
3245
  // it avoids restarting/checking the sidecar on every live input.
2903
- const client = connectController(deps, grant, record, placement);
3246
+ const client = provisionedClient ?? connectController(deps, grant, record, placement);
2904
3247
  const tokens = deriveBrowserSessionControllerTokens({
2905
3248
  rootSecret: browserAuthorityRoot(deps),
2906
3249
  accountId: grant.accountId,
@@ -2923,11 +3266,36 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2923
3266
  record,
2924
3267
  binding,
2925
3268
  placement,
3269
+ sourceAuthorization,
2926
3270
  };
2927
3271
  let result: T;
2928
3272
  try {
2929
3273
  result = await callback(controller);
2930
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
+ }
2931
3299
  if (!recoverMissing || !isMissingBrowserControllerSession(error)) throw error;
2932
3300
  await recoverActiveBrowserController(
2933
3301
  deps,
@@ -2937,6 +3305,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2937
3305
  placement,
2938
3306
  client,
2939
3307
  tokens,
3308
+ authorizationOperation,
2940
3309
  );
2941
3310
  result = await callback(controller);
2942
3311
  }
@@ -2947,15 +3316,10 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2947
3316
  try {
2948
3317
  return await run(cached);
2949
3318
  } catch (error) {
2950
- const safelyReplayable =
2951
- channelOperation === "browser.read" || channelOperation === "browser.action";
2952
- if (
2953
- !safelyReplayable ||
2954
- (!(error instanceof BrowserControlTransportError) &&
2955
- !(error instanceof BrowserControlRequestError && error.retryable))
2956
- ) {
3319
+ if (!safelyReplayable || !isRetryableControllerTransport(error)) {
2957
3320
  throw error;
2958
3321
  }
3322
+ controllerTransportFailed = true;
2959
3323
  await recordLeaseControllerDataPlaneUrl(deps.db, {
2960
3324
  accountId: grant.accountId,
2961
3325
  workspaceId,
@@ -2967,7 +3331,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2967
3331
  }
2968
3332
  }
2969
3333
 
2970
- const sourceSession = await requireSourceSession(deps, workspaceId, record.sourceSessionId);
3334
+ sourceSession ??= await requireSourceSession(deps, workspaceId, record.sourceSessionId);
2971
3335
  return await withBrowserPlacement(
2972
3336
  sourceSession,
2973
3337
  grant,
@@ -2975,18 +3339,75 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2975
3339
  binding.placementInstanceId,
2976
3340
  channelOperation,
2977
3341
  context.req.raw.signal,
2978
- async (placement) =>
2979
- await run(
2980
- await cacheBrowserControllerPlacement(grant, workspaceId, placement).catch(
2981
- () => placement,
2982
- ),
2983
- ),
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",
2984
3386
  );
2985
3387
  } catch (error) {
2986
3388
  throw browserRouteError(error);
2987
3389
  }
2988
3390
  }
2989
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
+
2990
3411
  async function recoverActiveBrowserController(
2991
3412
  routeDeps: ApiRouteDeps,
2992
3413
  grant: AccessGrant,
@@ -2995,12 +3416,72 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2995
3416
  placement: BrowserPlacement,
2996
3417
  client: BrowserControlClient,
2997
3418
  tokens: ReturnType<typeof deriveBrowserSessionControllerTokens>,
3419
+ authorizationOperation: SessionAuthorizationOperation,
2998
3420
  ): Promise<void> {
2999
- 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(
3000
3480
  routeDeps,
3001
3481
  grant,
3002
3482
  record.session,
3003
3483
  placement,
3484
+ client,
3004
3485
  );
3005
3486
  const networkRoute = await resolveActiveBrowserNetworkRouteLaunch({
3006
3487
  deps: routeDeps,
@@ -3009,13 +3490,30 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
3009
3490
  placement: placement.placement,
3010
3491
  });
3011
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
+ }
3012
3509
  await client.createSession({
3013
3510
  browserSessionId: record.session.id,
3014
3511
  controllerGeneration: binding.controllerGeneration,
3015
3512
  tokenGeneration: record.tokenGeneration,
3016
3513
  ...tokens,
3017
3514
  headed: !record.session.headless,
3018
- transport: browserRuntimeTransport(record.session, placement.transport),
3515
+ transport,
3516
+ ...(recoverWorkingDirectory ? { recoverExistingWorkingDirectory: true } : {}),
3019
3517
  ...(linkedComputer ? { linkedComputer } : {}),
3020
3518
  ...(networkRoute ? { networkRoute } : {}),
3021
3519
  });
@@ -3059,7 +3557,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
3059
3557
  grant,
3060
3558
  record,
3061
3559
  placement,
3062
- requestOrigin(context, deps.settings.corsAllowOriginRegex),
3560
+ requestOrigin(context, deps.settings),
3063
3561
  );
3064
3562
  await endCapturedBrowserController(client, browserSessionId, binding);
3065
3563
  await clearSuspendedBrowserSessionController(deps.db, {
@@ -3081,18 +3579,42 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
3081
3579
  }
3082
3580
  }
3083
3581
 
3084
- 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(
3085
3598
  grant: AccessGrant,
3086
3599
  workspaceId: string,
3087
3600
  request: CreateBrowserSessionRequestValue,
3088
3601
  placement: InteractionPlacement,
3089
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
+ }
3090
3608
  const attached = placement.kind === "attached_device";
3091
3609
  const external = placement.kind === "external_provider";
3092
3610
  const engine = attached ? ("chrome" as const) : external ? ("external" as const) : request.engine;
3093
- if (engine === "lightpanda" && placement.kind !== "sandbox_group") {
3611
+ if (
3612
+ request.engine === "lightpanda" &&
3613
+ placement.kind !== "sandbox_group" &&
3614
+ placement.kind !== "connected_machine"
3615
+ ) {
3094
3616
  throw new BrowserControlUnsupportedError(
3095
- "Lightpanda is currently available only in managed agent sandboxes",
3617
+ "Lightpanda requires a managed sandbox or Connected Machine browser placement",
3096
3618
  );
3097
3619
  }
3098
3620
  return {
@@ -3105,11 +3627,13 @@ function browserCreateInput(
3105
3627
  initialUrl: request.initialUrl ?? null,
3106
3628
  placement,
3107
3629
  driverId:
3108
- engine === "lightpanda"
3109
- ? LIGHTPANDA_DRIVER_ID
3110
- : external
3111
- ? EXTERNAL_BROWSER_DRIVER_ID
3112
- : 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,
3113
3637
  engine,
3114
3638
  headless: attached ? false : request.headless,
3115
3639
  identityId: request.identityId ?? null,
@@ -3134,7 +3658,21 @@ function browserCreateInput(
3134
3658
  function browserRuntimeTransport(
3135
3659
  session: BrowserSessionValue,
3136
3660
  transport: PlacementBrowserTransport,
3661
+ placementInstanceId?: string,
3662
+ rootSecret?: string,
3137
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
+ }
3138
3676
  if (transport.kind !== "managed") return transport;
3139
3677
  return {
3140
3678
  kind: "managed",
@@ -3196,6 +3734,11 @@ function assertCreateReplay(
3196
3734
  "BrowserSession create operation is bound to another placement",
3197
3735
  );
3198
3736
  }
3737
+ if (request.storageMode !== browserSessionStorageMode(session)) {
3738
+ throw new BrowserSessionOperationConflictError(
3739
+ "BrowserSession create operation is bound to another storage mode",
3740
+ );
3741
+ }
3199
3742
  const expectedEngine =
3200
3743
  session.placement.kind === "attached_device"
3201
3744
  ? "chrome"
@@ -3229,11 +3772,39 @@ function assertCreateReplay(
3229
3772
  }
3230
3773
  }
3231
3774
 
3232
- 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(
3233
3803
  deps: ApiRouteDeps,
3234
3804
  grant: AccessGrant,
3235
3805
  browser: BrowserSessionValue,
3236
3806
  placement: BrowserPlacement,
3807
+ client: BrowserControlClient,
3237
3808
  ): Promise<{ computerSessionId: string; controllerGeneration: string } | null> {
3238
3809
  if (!browser.linkedComputerSessionId) return null;
3239
3810
  const record = await getComputerSessionControlRecord(deps.db, {
@@ -3251,10 +3822,42 @@ async function requireLinkedComputerBinding(
3251
3822
  "Linked ComputerSession is not active on the browser placement",
3252
3823
  );
3253
3824
  }
3254
- return {
3825
+ const reference = {
3255
3826
  computerSessionId: record.session.id,
3256
3827
  controllerGeneration: record.session.controller.controllerGeneration,
3257
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
+ );
3258
3861
  }
3259
3862
 
3260
3863
  async function requireBrowserRevisionRestoreAuthority(
@@ -3474,6 +4077,7 @@ async function ensureInteractionHolder(
3474
4077
  if (!placement.lease?.instanceId) {
3475
4078
  throw new BrowserSessionStateError("BrowserSession lease placement is unavailable");
3476
4079
  }
4080
+ const sandboxRuntime = await resolveSessionSandboxRuntime(deps.db, deps.settings, sourceSession);
3477
4081
  const acquired = await acquireLease(deps.db, {
3478
4082
  accountId: grant.accountId,
3479
4083
  workspaceId: sourceSession.workspaceId,
@@ -3482,9 +4086,14 @@ async function ensureInteractionHolder(
3482
4086
  holderId: interactionHolderId(browserSessionId),
3483
4087
  subjectId: sourceSession.id,
3484
4088
  backend: placement.lease.backend,
4089
+ warmBilling: {
4090
+ mode: deps.settings.sandboxWarmBillingMode,
4091
+ rateMicrosPerSecond: sandboxWarmRateMicrosPerSecond(deps.settings, placement.lease.backend),
4092
+ },
3485
4093
  os: placement.lease.os,
3486
- image: placement.lease.image,
3487
- rigVersionId: placement.lease.rigVersionId,
4094
+ image: sandboxRuntime.image,
4095
+ imagePolicy: "new_creates_only",
4096
+ rigVersionId: sourceSession.rigVersionId,
3488
4097
  leaseTtlMs: deps.settings.sandboxLeaseTtlMs,
3489
4098
  expectedEpoch: placement.lease.leaseEpoch,
3490
4099
  waitSignal,
@@ -3506,6 +4115,11 @@ async function ensureInteractionHolder(
3506
4115
  ).catch(() => undefined);
3507
4116
  throw new BrowserSessionStateError("BrowserSession placement fence changed; retry");
3508
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);
3509
4123
  return true;
3510
4124
  }
3511
4125
 
@@ -3752,6 +4366,7 @@ async function settleBrowserSessionSuspensionCaptureFailure(
3752
4366
  return await failBrowserSessionSuspension(deps.db, {
3753
4367
  ...input,
3754
4368
  ...(outcomeUnknown ? { state: "outcome_unknown" as const } : {}),
4369
+ missingControllerSession: error.status === 404 && isMissingBrowserControllerSession(error),
3755
4370
  error: error.error,
3756
4371
  });
3757
4372
  }
@@ -3792,9 +4407,9 @@ async function authorizeSourceSession(
3792
4407
  grant: AccessGrant,
3793
4408
  sessionId: string,
3794
4409
  operation: SessionAuthorizationOperation,
3795
- ): Promise<void> {
4410
+ ): Promise<ResolvedSessionAuthorization | null> {
3796
4411
  try {
3797
- await requireSessionAuthorization(deps, grant, {
4412
+ return await requireSessionAuthorization(deps, grant, {
3798
4413
  sessionId,
3799
4414
  operation,
3800
4415
  surface: "http",
@@ -3889,6 +4504,66 @@ function optionalBoundedInteger(
3889
4504
  return parsed;
3890
4505
  }
3891
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
+
3892
4567
  function isUuid(value: unknown): value is string {
3893
4568
  return (
3894
4569
  typeof value === "string" &&
@@ -3896,32 +4571,8 @@ function isUuid(value: unknown): value is string {
3896
4571
  );
3897
4572
  }
3898
4573
 
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;
4574
+ function requestOrigin(context: Context, settings: ApiRouteDeps["settings"]): string | null {
4575
+ return validateInteractionRequestOrigin(context.req.header("origin"), settings);
3925
4576
  }
3926
4577
 
3927
4578
  export function interactionActorForGrant(
@@ -3949,6 +4600,20 @@ function assertPlacementInstance(expected: string | null, actual: string): void
3949
4600
  }
3950
4601
  }
3951
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
+
3952
4617
  function isTerminalOperation(state: string): boolean {
3953
4618
  return state === "completed" || state === "failed" || state === "outcome_unknown";
3954
4619
  }
@@ -4285,6 +4950,40 @@ function browserUploadFileIds(action: BrowserActionRequestValue["action"]): stri
4285
4950
  ];
4286
4951
  }
4287
4952
 
4953
+ /** Resolve Drive authority from the same immutable session authorization that
4954
+ * admitted the browser action. Agent-attempt subjects are technical worker
4955
+ * identities; their frozen initiating human is the only personal Drive
4956
+ * principal. Pure service attempts retain null and can read only ordinary
4957
+ * workspace files through the database predicate. */
4958
+ export function browserFileAuthoritySubjectId(
4959
+ grant: AccessGrant,
4960
+ authorization: ResolvedSessionAuthorization | null,
4961
+ ): string | null {
4962
+ if (!authorization) return grant.principalKind === "agent_attempt" ? null : grant.subjectId;
4963
+ return authorization.actor.kind === "agent_attempt"
4964
+ ? authorization.actor.initiatingHumanSubjectId
4965
+ : authorization.actor.subjectId;
4966
+ }
4967
+
4968
+ /** The batch authority query intentionally omits every unauthorized file. Any
4969
+ * omission therefore fails the whole upload before an object-storage URL is
4970
+ * minted, including mixed ordinary/Drive mappings and partially authorized
4971
+ * batches. */
4972
+ export function requireAuthorizedBrowserUploadFiles(
4973
+ workspaceFileIds: readonly string[],
4974
+ authorizedFiles: readonly FileAsset[],
4975
+ ): FileAsset[] {
4976
+ const byId = new Map(authorizedFiles.map((file) => [file.id, file]));
4977
+ return workspaceFileIds.map((fileId) => {
4978
+ const file = byId.get(fileId);
4979
+ if (!file) throw new HTTPException(404, { message: "workspace file not found" });
4980
+ if (file.status !== "ready") {
4981
+ throw new HTTPException(409, { message: "workspace file is not ready" });
4982
+ }
4983
+ return file;
4984
+ });
4985
+ }
4986
+
4288
4987
  function browserUploadFilename(value: string): string {
4289
4988
  const normalized = value
4290
4989
  .trim()
@@ -4370,9 +5069,14 @@ async function finalizeBrowserDownloadFile(
4370
5069
  if (upload.expiresAt.getTime() <= Date.now()) {
4371
5070
  throw new InteractionResourceStateError("Browser download file upload authority expired");
4372
5071
  }
4373
- const head = await objectStorage.headFile(upload.file).catch(() => {
4374
- 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);
4375
5076
  });
5077
+ if (!head) {
5078
+ throw new HTTPException(503, { message: "browser download object is not available" });
5079
+ }
4376
5080
  if (
4377
5081
  Number(head.ContentLength ?? -1) !== upload.file.sizeBytes ||
4378
5082
  (head.ContentType !== undefined && head.ContentType !== upload.file.contentType) ||
@@ -4413,7 +5117,11 @@ async function recordBrowserDownloadFileUsage(
4413
5117
  }
4414
5118
 
4415
5119
  function browserRouteError(error: unknown): HTTPException {
5120
+ const connectedMachineError = interactionControlApiError(error, "browser");
5121
+ if (connectedMachineError) return connectedMachineError;
4416
5122
  if (error instanceof HTTPException) return error;
5123
+ if (error instanceof SandboxPaidComputeAdmissionError)
5124
+ return new HTTPException(402, { message: error.message, cause: error });
4417
5125
  if (error instanceof BrowserSessionNotFoundError) {
4418
5126
  return new HTTPException(404, { message: error.message, cause: error });
4419
5127
  }