@opengeni/api-router 3.0.1 → 3.1.2

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 (546) hide show
  1. package/dist/access-grant-rls.d.ts +8 -1
  2. package/dist/admission-trace.d.ts +8 -0
  3. package/dist/app.d.ts +50 -3
  4. package/dist/app.js +10 -1
  5. package/dist/artifact-catalog-cursor.d.ts +1 -0
  6. package/dist/attached-browser-viewer.d.ts +11 -0
  7. package/dist/auth/managed-auth-attempt-context.d.ts +2 -0
  8. package/dist/auth/managed-auth-rate-limits.d.ts +167 -0
  9. package/dist/auth/managed-auth-session-adapter.d.ts +9 -1
  10. package/dist/auth/managed-auth.d.ts +72 -5
  11. package/dist/auth/managed-sign-in-notifications.d.ts +4 -0
  12. package/dist/auth/new-signups-gate.d.ts +23 -0
  13. package/dist/auth/signup-funnel-metrics.d.ts +44 -0
  14. package/dist/azure-live.d.ts +28 -0
  15. package/dist/browser-action-fences.d.ts +13 -0
  16. package/dist/browser-controller-authority.d.ts +1 -61
  17. package/dist/browser-ephemeral.d.ts +5 -0
  18. package/dist/browser-lost-runtime-end.d.ts +24 -0
  19. package/dist/browser-state-authority.d.ts +1 -27
  20. package/dist/chunk-OGQDXVXY.js +18 -0
  21. package/dist/chunk-OGQDXVXY.js.map +1 -0
  22. package/dist/{chunk-AJ62T5QN.js → chunk-OT56OY4V.js} +80503 -39869
  23. package/dist/chunk-OT56OY4V.js.map +1 -0
  24. package/dist/claude-default-models.d.ts +19 -0
  25. package/dist/claude-subscription-core.d.ts +50 -0
  26. package/dist/claude-subscription-oauth.d.ts +36 -0
  27. package/dist/claude-subscription-profile.d.ts +9 -0
  28. package/dist/claude-subscription-usage.d.ts +6 -0
  29. package/dist/claude-workspace-connection.d.ts +15 -0
  30. package/dist/codemode.d.ts +40 -5
  31. package/dist/codex-realtime.d.ts +29 -3
  32. package/dist/codex-session-pointers.d.ts +19 -0
  33. package/dist/connection-ownership.d.ts +6 -3
  34. package/dist/controller-data-plane.d.ts +16 -0
  35. package/dist/credit-promotion-snapshot.d.ts +6 -0
  36. package/dist/editable-artifact-source-session.d.ts +3 -0
  37. package/dist/editable-artifact-websocket.d.ts +5 -3
  38. package/dist/fatal-process-boundary.d.ts +9 -1
  39. package/dist/github-access.d.ts +7 -1
  40. package/dist/github-browser-flow.d.ts +1 -1
  41. package/dist/http/acting-person.d.ts +36 -0
  42. package/dist/http/api-contract-compat.d.ts +19 -0
  43. package/dist/http/api-error.d.ts +50 -1
  44. package/dist/http/auth.d.ts +8 -1
  45. package/dist/http/cors.d.ts +12 -0
  46. package/dist/http/deprecation.d.ts +42 -0
  47. package/dist/http/keyed-admission.d.ts +12 -0
  48. package/dist/http/local-browser-boundary.d.ts +72 -0
  49. package/dist/http/metrics-listener.d.ts +22 -0
  50. package/dist/http/path-identifier.d.ts +17 -0
  51. package/dist/http/registered-route-label.d.ts +16 -0
  52. package/dist/http/rejection-telemetry.d.ts +34 -0
  53. package/dist/http/request-body.d.ts +27 -0
  54. package/dist/http/request-source.d.ts +41 -3
  55. package/dist/http/sse.d.ts +12 -1
  56. package/dist/http/unmatched-route.d.ts +18 -0
  57. package/dist/http/user-content.d.ts +36 -0
  58. package/dist/http/variable-sets-removed.d.ts +41 -0
  59. package/dist/http/websocket-upgrade-dispatch.d.ts +18 -0
  60. package/dist/index.js +1279 -108
  61. package/dist/index.js.map +1 -1
  62. package/dist/integration-connect-metrics.d.ts +35 -0
  63. package/dist/integration-endpoint-test.d.ts +20 -0
  64. package/dist/integrations/api-integrations.d.ts +2 -2
  65. package/dist/integrations/atlassian.d.ts +8 -3
  66. package/dist/integrations/fiken.d.ts +3 -1
  67. package/dist/integrations/github-lens-connect.d.ts +37 -66
  68. package/dist/integrations/google-drive.d.ts +2 -0
  69. package/dist/integrations/integration-commit-authority.d.ts +13 -0
  70. package/dist/integrations/marketplace-plugin.d.ts +43 -43
  71. package/dist/integrations/oauth-client.d.ts +171 -12
  72. package/dist/integrations/oauth-profiles.d.ts +42 -13
  73. package/dist/integrations/onepassword-vault-provider.d.ts +9 -0
  74. package/dist/integrations/slack-bot.d.ts +285 -7
  75. package/dist/integrations/slack-file-upload-flow.d.ts +62 -0
  76. package/dist/integrations/slack-interactions.d.ts +89 -10
  77. package/dist/integrations/slack-mrkdwn.d.ts +41 -0
  78. package/dist/integrations/slack-routing.d.ts +1 -1
  79. package/dist/integrations/slack-session-defaults.d.ts +58 -0
  80. package/dist/integrations/slack-task-file-upload.d.ts +36 -0
  81. package/dist/integrations/slack-tool-review.d.ts +5 -0
  82. package/dist/interaction-agent-access.d.ts +13 -8
  83. package/dist/interaction-agent-version.d.ts +21 -0
  84. package/dist/interaction-holder-heartbeat.d.ts +5 -0
  85. package/dist/interaction-metrics.d.ts +27 -2
  86. package/dist/interaction-route-stages.d.ts +25 -0
  87. package/dist/interaction-secrets-vault.d.ts +20 -0
  88. package/dist/interaction-secrets.d.ts +103 -0
  89. package/dist/mcp/action-catalog-types.d.ts +14 -0
  90. package/dist/mcp/action-catalog.gen.d.ts +2 -0
  91. package/dist/mcp/action-descriptions.d.ts +4 -0
  92. package/dist/mcp/action-schema-imports.gen.d.ts +1 -0
  93. package/dist/mcp/capability-account-readiness.d.ts +7 -0
  94. package/dist/mcp/child-read-evidence.d.ts +9 -0
  95. package/dist/mcp/company-profile-agent-admin.d.ts +12 -0
  96. package/dist/mcp/contract-input.d.ts +13 -0
  97. package/dist/mcp/documents.d.ts +2 -0
  98. package/dist/mcp/first-party-tool-permissions.d.ts +3 -606
  99. package/dist/mcp/notification-tools.d.ts +26 -0
  100. package/dist/mcp/orchestration-failure-diagnostic.d.ts +43 -0
  101. package/dist/mcp/scheduled-task-input.d.ts +1309 -0
  102. package/dist/mcp/scheduled-task-view.d.ts +217 -3
  103. package/dist/mcp/secret-tools.d.ts +14 -0
  104. package/dist/mcp/server.d.ts +108 -5
  105. package/dist/mcp/session-admin-access-tools.d.ts +33 -0
  106. package/dist/mcp/session-event-errors.d.ts +28 -0
  107. package/dist/mcp/session-event-view.d.ts +62 -0
  108. package/dist/mcp/session-view.d.ts +15 -0
  109. package/dist/mcp/session-wait.d.ts +18 -0
  110. package/dist/mcp/site-agent-clamp-tools.d.ts +38 -0
  111. package/dist/mcp/site-data-tools.d.ts +22 -0
  112. package/dist/mcp/site-publication-tools.d.ts +19 -0
  113. package/dist/mcp/site-sql-tools.d.ts +27 -0
  114. package/dist/mcp-oauth.d.ts +37 -4
  115. package/dist/model-calls/chat-completions.d.ts +87 -0
  116. package/dist/model-calls/pending-usage-settlements.d.ts +57 -0
  117. package/dist/model-calls/service.d.ts +36 -0
  118. package/dist/model-catalog.d.ts +9 -3
  119. package/dist/native-push-dispatch.d.ts +69 -0
  120. package/dist/observability.d.ts +3 -1
  121. package/dist/organization-mcp.d.ts +82 -0
  122. package/dist/prepared-mcp-actor.d.ts +77 -0
  123. package/dist/prepared-mcp-permissions.d.ts +8 -0
  124. package/dist/routes/analytics-consent.d.ts +26 -0
  125. package/dist/routes/api-integrations.d.ts +11 -0
  126. package/dist/routes/api-keys.d.ts +7 -7
  127. package/dist/routes/automations.d.ts +2 -0
  128. package/dist/routes/billing.d.ts +59 -0
  129. package/dist/routes/browser-sessions.d.ts +65 -2
  130. package/dist/routes/chat-completions.d.ts +11 -0
  131. package/dist/routes/claude-core-usage.d.ts +20 -0
  132. package/dist/routes/claude-subscription-accounts.d.ts +3 -0
  133. package/dist/routes/claude-subscription-oauth.d.ts +3 -0
  134. package/dist/routes/client-errors.d.ts +182 -0
  135. package/dist/routes/codex-core.d.ts +401 -0
  136. package/dist/routes/codex.d.ts +64 -2
  137. package/dist/routes/computer-sessions.d.ts +6 -1
  138. package/dist/routes/desktop-distribution.d.ts +32 -0
  139. package/dist/routes/github-browser-fonts.gen.d.ts +2 -0
  140. package/dist/routes/github-browser-pages.d.ts +27 -0
  141. package/dist/routes/inbox.d.ts +13 -0
  142. package/dist/routes/insights-response-cache.d.ts +10 -0
  143. package/dist/routes/insights-usage.d.ts +12 -0
  144. package/dist/routes/insights.d.ts +28 -0
  145. package/dist/routes/interaction-placement-message.d.ts +7 -0
  146. package/dist/routes/managed-sign-in-methods.d.ts +6 -0
  147. package/dist/routes/model-account-choice.d.ts +13 -0
  148. package/dist/routes/model-account-settings.d.ts +19 -0
  149. package/dist/routes/native-app-auth.d.ts +12 -0
  150. package/dist/routes/organization-integration-policy.d.ts +8 -0
  151. package/dist/routes/organization-integrations.d.ts +6 -0
  152. package/dist/routes/organization-mcp-connections.d.ts +3 -0
  153. package/dist/routes/organization-model-defaults.d.ts +9 -0
  154. package/dist/routes/organization-service-accounts.d.ts +3 -0
  155. package/dist/routes/personal-api-keys.d.ts +3 -0
  156. package/dist/routes/session-admin-access.d.ts +3 -0
  157. package/dist/routes/session-artifact-associations.d.ts +4 -0
  158. package/dist/routes/session-history-imports.d.ts +20 -0
  159. package/dist/routes/sessions.d.ts +38 -2
  160. package/dist/routes/site-data.d.ts +18 -0
  161. package/dist/routes/site-publishing.d.ts +5 -0
  162. package/dist/routes/site-runtime.d.ts +31 -0
  163. package/dist/routes/site-sql.d.ts +16 -0
  164. package/dist/routes/slack-bot-access.d.ts +3 -0
  165. package/dist/routes/subscription-account-pools.d.ts +21 -0
  166. package/dist/routes/subscription-core-http.d.ts +5 -0
  167. package/dist/routes/subscription-pool-access.d.ts +37 -0
  168. package/dist/routes/subscription-pool-core.d.ts +153 -0
  169. package/dist/routes/supergrok.d.ts +6 -6
  170. package/dist/routes/usage-allowances.d.ts +12 -0
  171. package/dist/routes/variable-sets-removed.d.ts +2 -0
  172. package/dist/routes/vault-providers.d.ts +34 -0
  173. package/dist/routes/vaults.d.ts +3 -0
  174. package/dist/routes/workspace-integrations.d.ts +35 -0
  175. package/dist/routes/workspace-model-providers.d.ts +4 -0
  176. package/dist/routes/workspaces.d.ts +11 -2
  177. package/dist/sandbox/agent-update.d.ts +64 -0
  178. package/dist/sandbox/auth-callout.d.ts +8 -0
  179. package/dist/sandbox/channel-a.d.ts +15 -1
  180. package/dist/sandbox/enrollment.d.ts +26 -4
  181. package/dist/sandbox/machine-audience.d.ts +29 -0
  182. package/dist/sandbox/machines.d.ts +15 -2
  183. package/dist/sandbox/metrics-ingestion.d.ts +6 -2
  184. package/dist/sandbox/screen-control.d.ts +85 -0
  185. package/dist/sandbox/viewer.d.ts +5 -1
  186. package/dist/sandbox-file-artifacts.d.ts +17 -1
  187. package/dist/scheduled-task-blockers.d.ts +39 -0
  188. package/dist/scheduled-task-deletion.d.ts +3 -4
  189. package/dist/scheduled-task-lifecycle.d.ts +14 -0
  190. package/dist/secret-answers.d.ts +40 -0
  191. package/dist/session-create-observability.d.ts +5 -0
  192. package/dist/session-event-view-36GO3IL7.js +528 -0
  193. package/dist/session-event-view-36GO3IL7.js.map +1 -0
  194. package/dist/signed-state-secret.d.ts +18 -0
  195. package/dist/site-agent-guard.d.ts +53 -0
  196. package/dist/site-data/operations.d.ts +103 -0
  197. package/dist/site-data/service.d.ts +203 -0
  198. package/dist/site-frame-guard.d.ts +32 -0
  199. package/dist/site-publishing/agent-data-tools.d.ts +84 -0
  200. package/dist/site-publishing/agent-model.d.ts +58 -0
  201. package/dist/site-publishing/agent-session.d.ts +16 -0
  202. package/dist/site-publishing/content-scanner.d.ts +31 -0
  203. package/dist/site-publishing/edge-sync.d.ts +48 -0
  204. package/dist/site-publishing/gateway-signature.d.ts +56 -0
  205. package/dist/site-publishing/limits.d.ts +63 -0
  206. package/dist/site-publishing/page-tool-authority.d.ts +30 -0
  207. package/dist/site-publishing/platform-usage.d.ts +113 -0
  208. package/dist/site-publishing/platform.d.ts +21 -0
  209. package/dist/site-publishing/readiness.d.ts +62 -0
  210. package/dist/site-publishing/runtime.d.ts +179 -0
  211. package/dist/site-publishing/service.d.ts +264 -0
  212. package/dist/site-publishing/session-events.d.ts +22 -0
  213. package/dist/site-publishing/session-views.d.ts +36 -0
  214. package/dist/site-publishing/viewer.d.ts +40 -0
  215. package/dist/site-session-origin.d.ts +31 -6
  216. package/dist/site-sql/format.d.ts +47 -0
  217. package/dist/site-sql/gateway.d.ts +50 -0
  218. package/dist/site-sql/service.d.ts +213 -0
  219. package/dist/temporal-schedule-sync.d.ts +26 -0
  220. package/dist/transcription/billing-refusal.d.ts +8 -0
  221. package/dist/transcription/normalize.d.ts +23 -0
  222. package/dist/transcription/providers/azure-mai.d.ts +12 -0
  223. package/dist/transcription/providers/azure-openai.d.ts +4 -0
  224. package/dist/transcription/providers/openai.d.ts +16 -0
  225. package/dist/transcription/providers/subscription-core.d.ts +39 -0
  226. package/dist/transcription/service.d.ts +10 -1
  227. package/dist/transcription/wav-duration.d.ts +8 -0
  228. package/dist/workspace-tool-gateway-attestations.d.ts +37 -0
  229. package/dist/workspace-tool-gateway-observability.d.ts +16 -0
  230. package/dist/workspace-tool-gateway-secrets.d.ts +27 -0
  231. package/dist/workspace-tool-gateway.d.ts +102 -6
  232. package/dist/workspace-tool-target-fence.d.ts +116 -0
  233. package/dist/workspace-tool-target.d.ts +225 -0
  234. package/dist/workspace-webhook-dispatch.d.ts +26 -0
  235. package/dist/xai-subscription-core.d.ts +60 -0
  236. package/package.json +25 -20
  237. package/src/access-grant-rls.ts +64 -14
  238. package/src/admission-trace.ts +14 -0
  239. package/src/app.ts +1684 -252
  240. package/src/artifact-catalog-cursor.ts +1 -0
  241. package/src/attached-browser-viewer.ts +26 -0
  242. package/src/auth/managed-auth-attempt-context.ts +9 -0
  243. package/src/auth/managed-auth-rate-limits.ts +252 -0
  244. package/src/auth/managed-auth-session-adapter.ts +64 -29
  245. package/src/auth/managed-auth.ts +303 -35
  246. package/src/auth/managed-email.ts +18 -1
  247. package/src/auth/managed-sign-in-notifications.ts +81 -0
  248. package/src/auth/new-signups-gate.ts +49 -0
  249. package/src/auth/organization-user-setup.ts +3 -3
  250. package/src/auth/signup-funnel-metrics.ts +164 -0
  251. package/src/azure-live.ts +103 -0
  252. package/src/browser-action-fences.ts +45 -0
  253. package/src/browser-controller-authority.ts +1 -186
  254. package/src/browser-ephemeral.ts +39 -0
  255. package/src/browser-lost-runtime-end.ts +71 -0
  256. package/src/browser-state-authority.ts +1 -236
  257. package/src/claude-default-models.ts +37 -0
  258. package/src/claude-subscription-core.ts +154 -0
  259. package/src/claude-subscription-oauth.ts +352 -0
  260. package/src/claude-subscription-profile.ts +73 -0
  261. package/src/claude-subscription-usage.ts +51 -0
  262. package/src/claude-workspace-connection.ts +82 -0
  263. package/src/codemode.ts +236 -39
  264. package/src/codex-realtime.ts +181 -53
  265. package/src/codex-session-pointers.ts +80 -0
  266. package/src/company-brain-okf.ts +1 -1
  267. package/src/connected-machine-computer-access.ts +2 -1
  268. package/src/connection-ownership.ts +19 -13
  269. package/src/controller-data-plane.ts +34 -0
  270. package/src/credit-promotion-snapshot.ts +36 -0
  271. package/src/editable-artifact-production.ts +10 -0
  272. package/src/editable-artifact-source-session.ts +123 -0
  273. package/src/editable-artifact-websocket.ts +16 -3
  274. package/src/fatal-process-boundary.ts +67 -3
  275. package/src/gateway-realtime.ts +12 -8
  276. package/src/github-access.ts +31 -5
  277. package/src/github-browser-flow.ts +1 -1
  278. package/src/http/acting-person.ts +105 -0
  279. package/src/http/api-contract-compat.ts +49 -0
  280. package/src/http/api-error.ts +210 -2
  281. package/src/http/auth.ts +68 -4
  282. package/src/http/cors.ts +37 -5
  283. package/src/http/deprecation.ts +118 -0
  284. package/src/http/keyed-admission.ts +29 -0
  285. package/src/http/local-browser-boundary.ts +343 -0
  286. package/src/http/metrics-listener.ts +57 -0
  287. package/src/http/path-identifier.ts +54 -0
  288. package/src/http/registered-route-label.ts +34 -0
  289. package/src/http/rejection-telemetry.ts +120 -0
  290. package/src/http/request-body.ts +127 -0
  291. package/src/http/request-source.ts +154 -13
  292. package/src/http/sse.ts +88 -28
  293. package/src/http/unmatched-route.ts +60 -0
  294. package/src/http/user-content.ts +158 -0
  295. package/src/http/variable-sets-removed.ts +122 -0
  296. package/src/http/websocket-upgrade-dispatch.ts +29 -0
  297. package/src/index.ts +107 -57
  298. package/src/integration-connect-metrics.ts +228 -0
  299. package/src/integration-endpoint-test.ts +291 -0
  300. package/src/integrations/api-integrations.ts +81 -7
  301. package/src/integrations/atlassian.ts +202 -83
  302. package/src/integrations/fiken.ts +66 -38
  303. package/src/integrations/github-app-connect.ts +64 -18
  304. package/src/integrations/github-lens-connect.ts +8 -29
  305. package/src/integrations/google-drive.ts +448 -152
  306. package/src/integrations/integration-commit-authority.ts +69 -0
  307. package/src/integrations/marketplace-plugin.ts +1 -1
  308. package/src/integrations/oauth-client.ts +733 -193
  309. package/src/integrations/oauth-profiles.ts +116 -45
  310. package/src/integrations/onepassword-vault-provider.ts +16 -0
  311. package/src/integrations/personal-github-repositories.ts +1 -1
  312. package/src/integrations/personal-github.ts +61 -35
  313. package/src/integrations/provider-oauth.ts +67 -85
  314. package/src/integrations/slack-app-home.ts +5 -5
  315. package/src/integrations/slack-bot.ts +893 -201
  316. package/src/integrations/slack-file-upload-flow.ts +147 -0
  317. package/src/integrations/slack-install.ts +7 -7
  318. package/src/integrations/slack-interactions.ts +1589 -299
  319. package/src/integrations/slack-mrkdwn.ts +193 -0
  320. package/src/integrations/slack-routing.ts +2 -2
  321. package/src/integrations/slack-session-defaults.ts +224 -0
  322. package/src/integrations/slack-task-file-upload.ts +323 -0
  323. package/src/integrations/slack-tool-review.ts +65 -0
  324. package/src/integrations/social-oauth.ts +90 -58
  325. package/src/interaction-agent-access.ts +40 -20
  326. package/src/interaction-agent-version.ts +85 -0
  327. package/src/interaction-holder-heartbeat.ts +24 -8
  328. package/src/interaction-metrics.ts +164 -2
  329. package/src/interaction-route-stages.ts +94 -0
  330. package/src/interaction-secrets-vault.ts +213 -0
  331. package/src/interaction-secrets.ts +426 -0
  332. package/src/mcp/action-catalog-types.ts +14 -0
  333. package/src/mcp/action-catalog.gen.ts +8014 -0
  334. package/src/mcp/action-descriptions.ts +299 -0
  335. package/src/mcp/action-schema-imports.gen.ts +304 -0
  336. package/src/mcp/capability-account-readiness.ts +17 -0
  337. package/src/mcp/child-read-evidence.ts +19 -0
  338. package/src/mcp/company-profile-agent-admin.ts +13 -0
  339. package/src/mcp/contract-input.ts +165 -0
  340. package/src/mcp/documents.ts +9 -2
  341. package/src/mcp/editable-artifacts.ts +29 -11
  342. package/src/mcp/files.ts +5 -1
  343. package/src/mcp/first-party-tool-permissions.ts +7 -285
  344. package/src/mcp/knowledge-entries.ts +108 -34
  345. package/src/mcp/notification-tools.ts +480 -0
  346. package/src/mcp/orchestration-failure-diagnostic.ts +43 -0
  347. package/src/mcp/scheduled-task-input.ts +89 -0
  348. package/src/mcp/scheduled-task-view.ts +63 -3
  349. package/src/mcp/secret-tools.ts +185 -0
  350. package/src/mcp/server.ts +2213 -1116
  351. package/src/mcp/session-admin-access-tools.ts +163 -0
  352. package/src/mcp/session-event-errors.ts +123 -0
  353. package/src/mcp/session-event-view.ts +395 -13
  354. package/src/mcp/session-view.ts +3 -0
  355. package/src/mcp/session-wait.ts +67 -7
  356. package/src/mcp/site-agent-clamp-tools.ts +140 -0
  357. package/src/mcp/site-data-tools.ts +339 -0
  358. package/src/mcp/site-publication-tools.ts +201 -0
  359. package/src/mcp/site-sql-tools.ts +427 -0
  360. package/src/mcp-oauth.ts +635 -45
  361. package/src/model-calls/chat-completions.ts +352 -0
  362. package/src/model-calls/pending-usage-settlements.ts +125 -0
  363. package/src/model-calls/service.ts +694 -0
  364. package/src/model-catalog.ts +68 -17
  365. package/src/native-push-dispatch.ts +457 -0
  366. package/src/observability.ts +15 -1
  367. package/src/organization-mcp.ts +485 -0
  368. package/src/prepared-mcp-actor.ts +154 -0
  369. package/src/prepared-mcp-permissions.ts +38 -0
  370. package/src/routes/analytics-consent.ts +123 -0
  371. package/src/routes/api-integrations.ts +268 -9
  372. package/src/routes/api-keys.ts +164 -25
  373. package/src/routes/artifact-catalog.ts +111 -0
  374. package/src/routes/automations.ts +163 -90
  375. package/src/routes/billing.ts +535 -59
  376. package/src/routes/browser-sessions.ts +966 -342
  377. package/src/routes/canonical-human-identities.ts +61 -0
  378. package/src/routes/capabilities.ts +112 -5
  379. package/src/routes/channels.ts +4 -3
  380. package/src/routes/chat-completions.ts +235 -0
  381. package/src/routes/claude-core-usage.ts +147 -0
  382. package/src/routes/claude-subscription-accounts.ts +233 -0
  383. package/src/routes/claude-subscription-oauth.ts +175 -0
  384. package/src/routes/client-errors.ts +394 -0
  385. package/src/routes/codex-core.ts +687 -0
  386. package/src/routes/codex.ts +1277 -1119
  387. package/src/routes/company-brain.ts +4 -0
  388. package/src/routes/computer-sessions.ts +319 -85
  389. package/src/routes/connect.ts +277 -175
  390. package/src/routes/connections.ts +690 -363
  391. package/src/routes/desktop-distribution.ts +217 -0
  392. package/src/routes/documents.ts +13 -7
  393. package/src/routes/editable-artifacts.ts +74 -4
  394. package/src/routes/enrollments.ts +110 -16
  395. package/src/routes/external-identity-links.ts +13 -12
  396. package/src/routes/files.ts +41 -16
  397. package/src/routes/github-browser-fonts.gen.ts +12 -0
  398. package/src/routes/github-browser-pages.ts +258 -0
  399. package/src/routes/github.ts +200 -75
  400. package/src/routes/inbox.ts +353 -0
  401. package/src/routes/insights-response-cache.ts +83 -0
  402. package/src/routes/insights-usage.ts +345 -0
  403. package/src/routes/insights.ts +103 -8
  404. package/src/routes/install.ts +51 -10
  405. package/src/routes/integration-facets.ts +70 -28
  406. package/src/routes/interaction-placement-message.ts +12 -0
  407. package/src/routes/knowledge.ts +136 -27
  408. package/src/routes/machines.ts +189 -135
  409. package/src/routes/managed-auth-session-sets.ts +43 -10
  410. package/src/routes/managed-onboarding.ts +39 -19
  411. package/src/routes/managed-sign-in-methods.ts +433 -0
  412. package/src/routes/memory-slack-publications.ts +3 -2
  413. package/src/routes/model-account-choice.ts +194 -0
  414. package/src/routes/model-account-settings.ts +235 -0
  415. package/src/routes/model-connection-access.ts +210 -48
  416. package/src/routes/native-app-auth.ts +296 -0
  417. package/src/routes/organization-integration-policy.ts +116 -0
  418. package/src/routes/organization-integrations.ts +326 -0
  419. package/src/routes/organization-mcp-connections.ts +183 -0
  420. package/src/routes/organization-memberships.ts +205 -19
  421. package/src/routes/organization-model-defaults.ts +110 -0
  422. package/src/routes/organization-model-providers.ts +74 -3
  423. package/src/routes/organization-service-accounts.ts +128 -0
  424. package/src/routes/organization-sessions.ts +21 -5
  425. package/src/routes/personal-api-keys.ts +184 -0
  426. package/src/routes/personal-github-git-broker.ts +2 -2
  427. package/src/routes/personal-github.ts +9 -4
  428. package/src/routes/plugins.ts +46 -7
  429. package/src/routes/pr-review-github.ts +105 -118
  430. package/src/routes/pr-review.ts +88 -82
  431. package/src/routes/preference-registry.ts +18 -0
  432. package/src/routes/rigs.ts +48 -13
  433. package/src/routes/scheduled-tasks.ts +347 -27
  434. package/src/routes/session-admin-access.ts +271 -0
  435. package/src/routes/session-artifact-associations.ts +164 -0
  436. package/src/routes/session-history-imports.ts +305 -0
  437. package/src/routes/sessions.ts +1682 -363
  438. package/src/routes/site-data.ts +340 -0
  439. package/src/routes/site-publishing.ts +584 -0
  440. package/src/routes/site-runtime.ts +1944 -0
  441. package/src/routes/site-sql.ts +263 -0
  442. package/src/routes/skill-content.ts +72 -7
  443. package/src/routes/skills.ts +5 -4
  444. package/src/routes/slack-bot-access.ts +108 -0
  445. package/src/routes/social.ts +27 -15
  446. package/src/routes/subscription-account-pools.ts +209 -0
  447. package/src/routes/subscription-core-http.ts +18 -0
  448. package/src/routes/subscription-pool-access.ts +180 -0
  449. package/src/routes/subscription-pool-core.ts +552 -0
  450. package/src/routes/supergrok.ts +174 -655
  451. package/src/routes/transcription-recordings.ts +71 -4
  452. package/src/routes/transcriptions.ts +26 -2
  453. package/src/routes/usage-allowances.ts +381 -0
  454. package/src/routes/variable-sets-removed.ts +19 -0
  455. package/src/routes/vault-providers.ts +782 -0
  456. package/src/routes/vaults.ts +421 -0
  457. package/src/routes/video-generation.ts +30 -12
  458. package/src/routes/workspace-artifacts.ts +165 -53
  459. package/src/routes/workspace-integrations.ts +576 -0
  460. package/src/routes/workspace-model-providers.ts +148 -0
  461. package/src/routes/workspace-state.ts +4 -0
  462. package/src/routes/workspaces.ts +625 -117
  463. package/src/sandbox/agent-update.ts +315 -0
  464. package/src/sandbox/auth-callout.ts +104 -21
  465. package/src/sandbox/channel-a.ts +165 -55
  466. package/src/sandbox/enrollment.ts +131 -22
  467. package/src/sandbox/machine-audience.ts +192 -0
  468. package/src/sandbox/machines.ts +75 -31
  469. package/src/sandbox/metrics-ingestion.ts +133 -2
  470. package/src/sandbox/rematerialize.ts +51 -21
  471. package/src/sandbox/screen-control.ts +289 -0
  472. package/src/sandbox/viewer.ts +33 -58
  473. package/src/sandbox-file-artifacts.ts +53 -27
  474. package/src/scheduled-task-blockers.ts +293 -0
  475. package/src/scheduled-task-deletion.ts +13 -3
  476. package/src/scheduled-task-lifecycle.ts +84 -0
  477. package/src/secret-answers.ts +119 -0
  478. package/src/session-create-observability.ts +46 -0
  479. package/src/session-realtime-context.ts +26 -4
  480. package/src/signed-state-secret.ts +58 -0
  481. package/src/site-agent-guard.ts +261 -0
  482. package/src/site-data/operations.ts +777 -0
  483. package/src/site-data/service.ts +1043 -0
  484. package/src/site-frame-guard.ts +107 -0
  485. package/src/site-publishing/agent-data-tools.ts +250 -0
  486. package/src/site-publishing/agent-model.ts +127 -0
  487. package/src/site-publishing/agent-session.ts +148 -0
  488. package/src/site-publishing/content-scanner.ts +93 -0
  489. package/src/site-publishing/edge-sync.ts +148 -0
  490. package/src/site-publishing/gateway-signature.ts +198 -0
  491. package/src/site-publishing/limits.ts +315 -0
  492. package/src/site-publishing/page-tool-authority.ts +111 -0
  493. package/src/site-publishing/platform-usage.ts +479 -0
  494. package/src/site-publishing/platform.ts +98 -0
  495. package/src/site-publishing/readiness.ts +330 -0
  496. package/src/site-publishing/runtime.ts +831 -0
  497. package/src/site-publishing/service.ts +1282 -0
  498. package/src/site-publishing/session-events.ts +262 -0
  499. package/src/site-publishing/session-views.ts +262 -0
  500. package/src/site-publishing/viewer.ts +75 -0
  501. package/src/site-session-origin.ts +91 -6
  502. package/src/site-sql/format.ts +134 -0
  503. package/src/site-sql/gateway.ts +168 -0
  504. package/src/site-sql/service.ts +1636 -0
  505. package/src/site-uploads.ts +27 -5
  506. package/src/temporal-schedule-sync.ts +74 -0
  507. package/src/transcription/billing-refusal.ts +14 -0
  508. package/src/transcription/normalize.ts +77 -0
  509. package/src/transcription/providers/azure-mai.ts +98 -0
  510. package/src/transcription/providers/azure-openai.ts +16 -7
  511. package/src/transcription/providers/codex-subscription.ts +190 -67
  512. package/src/transcription/providers/openai.ts +79 -5
  513. package/src/transcription/providers/subscription-core.ts +125 -0
  514. package/src/transcription/providers/xai-subscription.ts +81 -83
  515. package/src/transcription/segmenter.ts +7 -1
  516. package/src/transcription/service.ts +239 -10
  517. package/src/transcription/wav-duration.ts +39 -0
  518. package/src/workspace-deletion.ts +25 -1
  519. package/src/workspace-tool-gateway-attestations.ts +133 -0
  520. package/src/workspace-tool-gateway-observability.ts +64 -0
  521. package/src/workspace-tool-gateway-secrets.ts +104 -0
  522. package/src/workspace-tool-gateway.ts +921 -103
  523. package/src/workspace-tool-target-fence.ts +380 -0
  524. package/src/workspace-tool-target.ts +554 -0
  525. package/src/workspace-webhook-dispatch.ts +225 -0
  526. package/src/xai-realtime.ts +85 -81
  527. package/src/xai-subscription-core.ts +172 -0
  528. package/dist/browser-auth-broker.d.ts +0 -20
  529. package/dist/chunk-AJ62T5QN.js.map +0 -1
  530. package/dist/connection-authority-owner.d.ts +0 -59
  531. package/dist/routes/connection-authorities.d.ts +0 -3
  532. package/dist/routes/environments.d.ts +0 -5
  533. package/dist/routes/host-mcp-bindings.d.ts +0 -5
  534. package/dist/routes/host-mcp-resolvers.d.ts +0 -3
  535. package/dist/routes/packs.d.ts +0 -3
  536. package/dist/session-event-view-MU3HMNV3.js +0 -264
  537. package/dist/session-event-view-MU3HMNV3.js.map +0 -1
  538. package/dist/xai-subscription-auth.d.ts +0 -23
  539. package/src/browser-auth-broker.ts +0 -155
  540. package/src/connection-authority-owner.ts +0 -61
  541. package/src/routes/connection-authorities.ts +0 -139
  542. package/src/routes/environments.ts +0 -388
  543. package/src/routes/host-mcp-bindings.ts +0 -122
  544. package/src/routes/host-mcp-resolvers.ts +0 -41
  545. package/src/routes/packs.ts +0 -784
  546. package/src/xai-subscription-auth.ts +0 -139
@@ -1,11 +1,19 @@
1
+ import { attachedBrowserViewer } from "../attached-browser-viewer";
2
+ import {
3
+ assertEphemeralBrowserCreateEnabled,
4
+ ephemeralBrowserPartition,
5
+ } from "../browser-ephemeral";
1
6
  import { randomUUID } from "node:crypto";
2
7
  import {
3
8
  environmentsEncryptionKeyBytes,
4
9
  resolveFirstPartyDelegationSecret,
5
10
  resolveStreamTokenSecret,
11
+ sandboxWarmRateMicrosPerSecond,
6
12
  } from "@opengeni/config";
7
13
  import {
8
14
  BROWSER_CONTROL_WEBSOCKET_BEARER_PREFIX,
15
+ EPHEMERAL_CHROMIUM_DRIVER_ID,
16
+ browserSessionStorageMode,
9
17
  BROWSER_CONTROL_WEBSOCKET_PROTOCOL,
10
18
  BROWSER_CONTROL_PORT,
11
19
  BROWSER_CONTROL_PROTOCOL_VERSION,
@@ -14,16 +22,18 @@ import {
14
22
  AttachedBrowserDevice,
15
23
  AttachedBrowserDeviceListResponse,
16
24
  BrowserActionCommand,
25
+ browserActionSecretTexts,
17
26
  BrowserActionRequest,
18
27
  BrowserDiagnosticKind,
19
28
  BrowserDownload,
20
29
  BrowserDownloadSaveRequest,
21
30
  BrowserDownloadSaveResponse,
22
31
  BrowserDownloadListResponse,
32
+ BrowserDomReadRequest,
33
+ BrowserDomReadResponse,
23
34
  BrowserExternalAuthCommand,
24
35
  BrowserOpenTargetRequest,
25
36
  BrowserObservation,
26
- BrowserProtectedAuthFillCommand,
27
37
  BrowserSessionAttachment,
28
38
  BrowserSessionAttachmentRequest,
29
39
  BrowserSessionHeartbeatResponse,
@@ -31,21 +41,19 @@ import {
31
41
  BrowserSessionListResponse,
32
42
  BrowserSessionMutationResponse,
33
43
  BrowserTargetListResponse,
44
+ BrowserTargetState,
34
45
  CreateBrowserSessionRequest,
35
46
  ExternalAuthInteractiveRequest,
36
47
  ExternalAuthInteractiveResponse,
37
48
  ExternalAuthRunRequest,
38
49
  ExternalAuthRunResponse,
39
50
  InteractionActor,
40
- ProtectedAuthFillRequest,
41
- ProtectedAuthFillResponse,
42
51
  ReportAuthRunRequest,
43
52
  StartAuthRunRequest,
44
53
  VerifyAuthRunRequest,
45
54
  PublishBrowserRevisionRequest,
46
55
  PublishBrowserRevisionResponse,
47
56
  type AccessGrant,
48
- type BrowserProtectedAuthFillReceipt as BrowserProtectedAuthFillReceiptValue,
49
57
  type BrowserActionRequest as BrowserActionRequestValue,
50
58
  type BrowserDownload as BrowserDownloadValue,
51
59
  type BrowserSession as BrowserSessionValue,
@@ -56,7 +64,6 @@ import {
56
64
  type BrowserRevisionMaterialization as BrowserRevisionMaterializationValue,
57
65
  type Session,
58
66
  type SessionAuthorizationOperation,
59
- type SiteAuthAuthority,
60
67
  } from "@opengeni/contracts";
61
68
  import {
62
69
  getSessionAuthorityEpoch,
@@ -75,21 +82,23 @@ import {
75
82
  BrowserSessionNotFoundError,
76
83
  BrowserSessionOperationConflictError,
77
84
  BrowserSessionStateError,
85
+ SandboxPaidComputeAdmissionError,
78
86
  completeBrowserSessionEnd,
79
87
  completeExternalAuth,
80
88
  completeBrowserDownloadSave,
81
89
  completeFileUpload,
82
- completeProtectedAuthFill,
83
90
  commitBrowserSessionSuspension,
84
91
  commitBrowserRevisionPublication,
85
92
  dispatchBrowserRevisionPublication,
86
93
  dispatchBrowserDownloadSave,
87
94
  dispatchBrowserSessionOperation,
88
95
  dispatchExternalAuth,
89
- dispatchProtectedAuthFill,
90
96
  failBrowserSessionOperation,
97
+ failPreparedBrowserSessionEnd,
98
+ failPreparedBrowserSessionSuspend,
91
99
  failBrowserSessionResume,
92
100
  failBrowserSessionResumePreparation,
101
+ getExpiredBrowserSessionResume,
93
102
  failBrowserSessionSuspension,
94
103
  failBrowserRevisionPublication,
95
104
  findBrowserSessionControlRecordByOperation,
@@ -106,8 +115,6 @@ import {
106
115
  getFileUpload,
107
116
  getAuthRun,
108
117
  getExternalAuthPreparation,
109
- getProtectedAuthFillPreparation,
110
- getSiteAuthConnection,
111
118
  getSession,
112
119
  InteractionResourceConflictError,
113
120
  InteractionResourceNotFoundError,
@@ -115,20 +122,22 @@ import {
115
122
  listAuthRuns,
116
123
  listBrowserSessions,
117
124
  listAttachedBrowserDevices,
125
+ interactionControllerGone,
118
126
  prepareBrowserSessionCreate,
127
+ markEphemeralBrowserSessionLost,
119
128
  prepareExternalAuth,
120
129
  prepareBrowserDownloadSave,
121
130
  prepareBrowserSessionEnd,
122
131
  prepareBrowserSessionResume,
132
+ settleExpiredBrowserSessionResume,
133
+ continueExpiredBrowserSessionResume,
123
134
  prepareBrowserSessionSuspend,
124
135
  prepareBrowserRevisionPublication,
125
- prepareProtectedAuthFill,
126
136
  reportAuthRun,
127
137
  releaseLeaseHolder,
128
138
  readLease,
129
139
  recordLeaseControllerDataPlaneUrl,
130
140
  loadConnectionCredentialForBroker,
131
- markProtectedAuthFillOutcomeUnknown,
132
141
  startAuthRun,
133
142
  touchBrowserSessionController,
134
143
  verifyAuthRun,
@@ -169,6 +178,8 @@ import {
169
178
  provisionBrowserControlClient,
170
179
  renewSandboxProviderExpiration,
171
180
  type BrowserControlPlacementSession,
181
+ type BrowserControlScreenshotOptions,
182
+ type BrowserControlFrame,
172
183
  type PlacementBrowserStateCaptureReceipt,
173
184
  type PlacementBrowserNetworkRoute,
174
185
  type PlacementBrowserTransport,
@@ -189,9 +200,14 @@ import {
189
200
  controllerCacheAllowsHostFetch,
190
201
  controllerCachedUrlIsUsable,
191
202
  shouldPersistControllerDataPlaneUrl,
203
+ isRetryableControllerTransport,
192
204
  withCachedController,
205
+ withControllerTransportRecovery,
193
206
  } from "../controller-data-plane";
194
- import { filterInteractionSessionsForGrant } from "../interaction-agent-access";
207
+ import {
208
+ filterInteractionSessionsForGrant,
209
+ parseInteractionSessionListQuery,
210
+ } from "../interaction-agent-access";
195
211
  import { withInteractionHolderHeartbeat } from "../interaction-holder-heartbeat";
196
212
  import {
197
213
  browserStateArtifactAad,
@@ -201,11 +217,13 @@ import {
201
217
  unwrapBrowserStateDataKey,
202
218
  wrapBrowserStateDataKey,
203
219
  } from "../browser-state-authority";
204
- import {
205
- BrowserAuthCredentialError,
206
- resolveProtectedAuthFieldValues,
207
- } from "../browser-auth-broker";
208
220
  import { managedNetworkRouteForPlacement } from "../browser-network-route";
221
+ import {
222
+ endBrowserDiscardingLostRuntime,
223
+ endExpiredResumeSettingAsideInterruptedLaunch,
224
+ } from "../browser-lost-runtime-end";
225
+ import { resolveBrowserActionFences, type BrowserActionFences } from "../browser-action-fences";
226
+ import { InteractionRouteStages, reportInteractionRouteStages } from "../interaction-route-stages";
209
227
  import { validateInteractionRequestOrigin } from "../http/cors";
210
228
  import { ApiHttpError } from "../http/api-error";
211
229
  import { interactionControlApiError } from "../http/interaction-control-error";
@@ -221,6 +239,15 @@ import {
221
239
  } from "../interaction-metrics";
222
240
  import { withChannelA, withChannelARead, type ChannelAOperation } from "../sandbox/channel-a";
223
241
  import { sanitizeFilename } from "./files";
242
+ import { USER_CONTENT_SECURITY_HEADERS } from "../http/user-content";
243
+ import { wrongInteractionPlacementMessage } from "./interaction-placement-message";
244
+ import {
245
+ releaseBrowserPasskeys,
246
+ resolveInteractionSecretValues,
247
+ syncBrowserPasskeys,
248
+ type InteractionSecretsPort,
249
+ } from "../interaction-secrets";
250
+ import { requirePlacementAcceptsSecretValues } from "../interaction-agent-version";
224
251
 
225
252
  const BROWSER_DRIVER_ID = "opengeni.cdp.v1";
226
253
  const LIGHTPANDA_DRIVER_ID = "opengeni.lightpanda.cdp.v1";
@@ -237,7 +264,12 @@ type BrowserPlacement = {
237
264
  transport: PlacementBrowserTransport;
238
265
  };
239
266
 
240
- export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): void {
267
+ export function registerBrowserSessionRoutes(
268
+ app: Hono,
269
+ deps: ApiRouteDeps,
270
+ options: { interactionSecrets?: InteractionSecretsPort | null } = {},
271
+ ): void {
272
+ const interactionSecrets = options.interactionSecrets ?? null;
241
273
  const channelServices = {
242
274
  db: deps.db,
243
275
  settings: deps.settings,
@@ -252,8 +284,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
252
284
  return context.json(
253
285
  AttachedBrowserDeviceListResponse.parse(
254
286
  await listAttachedBrowserDevices(deps.db, {
255
- accountId: grant.accountId,
256
- workspaceId,
287
+ ...(await attachedBrowserViewer(deps, grant, workspaceId)),
257
288
  includeDisconnected,
258
289
  }),
259
290
  ),
@@ -268,8 +299,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
268
299
  return context.json(
269
300
  AttachedBrowserDevice.parse(
270
301
  await getAttachedBrowserDevice(deps.db, {
271
- accountId: grant.accountId,
272
- workspaceId,
302
+ ...(await attachedBrowserViewer(deps, grant, workspaceId)),
273
303
  deviceId,
274
304
  }),
275
305
  ),
@@ -288,6 +318,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
288
318
  const listed = await listBrowserSessions(deps.db, {
289
319
  accountId: grant.accountId,
290
320
  workspaceId,
321
+ ...parseInteractionSessionListQuery(context.req.query()),
291
322
  });
292
323
  return context.json(
293
324
  BrowserSessionListResponse.parse({
@@ -319,11 +350,18 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
319
350
  const grant = await requireAccessGrant(context, deps, workspaceId, "sessions:control");
320
351
  const request = await parseJsonBody(context, CreateBrowserSessionRequest);
321
352
  const startedAtMs = performance.now();
353
+ const stages = new InteractionRouteStages("create");
322
354
  await authorizeSourceSession(deps, grant, request.sessionId, "session.control");
355
+ stages.mark("authorize");
323
356
  const origin = requestOrigin(context, deps.settings);
324
357
  const authority = browserAuthorityRoot(deps);
358
+ let prepared: Awaited<ReturnType<typeof prepareBrowserSessionCreate>> | null = null;
325
359
 
326
360
  try {
361
+ assertEphemeralBrowserCreateEnabled(
362
+ request,
363
+ deps.settings.experimentalBrowserContextPoolEnabled,
364
+ );
327
365
  const existing = await findBrowserSessionControlRecordByOperation(deps.db, {
328
366
  accountId: grant.accountId,
329
367
  workspaceId,
@@ -335,7 +373,8 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
335
373
  );
336
374
  }
337
375
  if (existing) assertCreateReplay(request, existing.session);
338
- let prepared = existing
376
+ stages.mark("replay_check");
377
+ prepared = existing
339
378
  ? await prepareBrowserSessionCreate(
340
379
  deps.db,
341
380
  browserCreateInput(grant, workspaceId, request, existing.session.placement),
@@ -362,6 +401,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
362
401
  }
363
402
 
364
403
  const sourceSession = await requireSourceSession(deps, workspaceId, request.sessionId);
404
+ stages.mark("source_session");
365
405
  const response = await withBrowserPlacement(
366
406
  sourceSession,
367
407
  grant,
@@ -370,6 +410,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
370
410
  "browser.create",
371
411
  context.req.raw.signal,
372
412
  async (placement) => {
413
+ stages.mark("placement_resolve");
373
414
  let revisionAuthority: BrowserRevisionArtifactAuthority | null = null;
374
415
  if (!prepared) {
375
416
  prepared = await prepareBrowserSessionCreate(
@@ -377,6 +418,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
377
418
  browserCreateInput(grant, workspaceId, request, placement.placement),
378
419
  );
379
420
  }
421
+ stages.mark("prepare");
380
422
  if (isTerminalOperation(prepared.operation.state)) return prepared;
381
423
 
382
424
  if (prepared.session.baseRevisionId && !revisionAuthority) {
@@ -397,6 +439,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
397
439
  placement.placement,
398
440
  revisionAuthority,
399
441
  );
442
+ stages.mark("restore");
400
443
 
401
444
  try {
402
445
  const networkRoute = await resolveBrowserNetworkRouteLaunch({
@@ -408,6 +451,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
408
451
  rootSecret: authority,
409
452
  placement: placement.placement,
410
453
  });
454
+ stages.mark("network_route");
411
455
  const interactionHeld = await ensureInteractionHolder(
412
456
  deps,
413
457
  grant,
@@ -416,6 +460,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
416
460
  placement,
417
461
  context.req.raw.signal,
418
462
  );
463
+ stages.mark("holder");
419
464
  const record = await ensureDispatchedGeneration(
420
465
  deps,
421
466
  grant,
@@ -435,6 +480,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
435
480
  }
436
481
  throw error;
437
482
  });
483
+ stages.mark("generation");
438
484
  const preparedSession = prepared.session;
439
485
  const controllerGeneration = requireOperationGeneration(record);
440
486
  const adminToken = deriveBrowserControllerAdminToken({
@@ -482,18 +528,25 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
482
528
  controllerGeneration,
483
529
  },
484
530
  async () => {
531
+ stages.mark("provision");
485
532
  await ensureHeadedBrowserDisplayStack(
486
533
  placement.session,
487
534
  preparedSession.headless,
488
535
  linkedComputer !== null,
489
536
  );
537
+ stages.mark("display");
490
538
  return await client.createSession({
491
539
  browserSessionId: preparedSession.id,
492
540
  controllerGeneration,
493
541
  tokenGeneration: record.tokenGeneration,
494
542
  ...tokens,
495
543
  headed: !preparedSession.headless,
496
- transport: browserRuntimeTransport(preparedSession, placement.transport),
544
+ transport: browserRuntimeTransport(
545
+ preparedSession,
546
+ placement.transport,
547
+ placement.placementInstanceId,
548
+ authority,
549
+ ),
497
550
  ...(linkedComputer ? { linkedComputer } : {}),
498
551
  ...(networkRoute ? { networkRoute } : {}),
499
552
  ...(request.initialUrl ? { initialUrl: request.initialUrl } : {}),
@@ -503,9 +556,11 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
503
556
  );
504
557
  },
505
558
  );
559
+ stages.mark("launch");
506
560
  await cacheBrowserControllerPlacement(grant, workspaceId, placement).catch(
507
561
  () => placement,
508
562
  );
563
+ stages.mark("placement_cache_write");
509
564
  } catch (error) {
510
565
  if (
511
566
  error instanceof BrowserControlTransportError ||
@@ -538,7 +593,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
538
593
  // Once the controller has accepted the exact binding, a persistence
539
594
  // failure is retried against that same idempotent physical session.
540
595
  // Never terminalize or release its placement authority here.
541
- return await activateBrowserSession(deps.db, {
596
+ const activated = await activateBrowserSession(deps.db, {
542
597
  accountId: grant.accountId,
543
598
  workspaceId,
544
599
  operationId: request.operationId,
@@ -550,6 +605,8 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
550
605
  },
551
606
  engineVersion: null,
552
607
  });
608
+ stages.mark("activate");
609
+ return activated;
553
610
  } finally {
554
611
  restore?.dataKey.fill(0);
555
612
  restore?.aad.fill(0);
@@ -558,11 +615,24 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
558
615
  );
559
616
  const parsed = BrowserSessionMutationResponse.parse(response);
560
617
  observeLifecycleResult(deps.observability, startedAtMs, parsed);
618
+ reportInteractionRouteStages(context, deps.observability, stages);
561
619
  return context.json(
562
620
  parsed,
563
621
  parsed.operation.state === "completed" && !parsed.operation.replayed ? 201 : 200,
564
622
  );
565
623
  } catch (error) {
624
+ if (prepared) {
625
+ // The operation lock fences a concurrent dispatch. Once a controller
626
+ // binding exists, retain its exact receipt for physical reconciliation.
627
+ await failBrowserSessionOperation(deps.db, {
628
+ accountId: grant.accountId,
629
+ workspaceId,
630
+ operationId: request.operationId,
631
+ browserSessionId: prepared.session.id,
632
+ onlyIfPreparedCreate: true,
633
+ error: interactionFailure(error),
634
+ }).catch(() => undefined);
635
+ }
566
636
  throw browserRouteError(error);
567
637
  }
568
638
  });
@@ -614,6 +684,28 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
614
684
  },
615
685
  );
616
686
 
687
+ app.post(
688
+ "/v1/workspaces/:workspaceId/browser-sessions/:browserSessionId/targets/open-with-inventory",
689
+ async (context) => {
690
+ const { workspaceId, grant, browserSessionId } = await browserRoutePreamble(
691
+ context,
692
+ "sessions:control",
693
+ );
694
+ const request = await parseJsonBody(context, BrowserOpenTargetRequest);
695
+ const result = await withActiveBrowserController(
696
+ context,
697
+ grant,
698
+ workspaceId,
699
+ browserSessionId,
700
+ "session.control",
701
+ "browser.control",
702
+ async ({ sessionClient }) =>
703
+ BrowserTargetListResponse.parse(await sessionClient.openTargetWithInventory(request.url)),
704
+ );
705
+ return context.json(result, 201);
706
+ },
707
+ );
708
+
617
709
  app.post(
618
710
  "/v1/workspaces/:workspaceId/browser-sessions/:browserSessionId/targets/:targetId/select",
619
711
  async (context) => {
@@ -889,6 +981,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
889
981
  workspaceId,
890
982
  session: sourceSession,
891
983
  subjectId: grant.subjectId,
984
+ grant,
892
985
  waitSignal: context.req.raw.signal,
893
986
  operation: "browser.download.save",
894
987
  },
@@ -898,6 +991,9 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
898
991
  destinationPath: save.destinationPath,
899
992
  overwrite: save.overwrite,
900
993
  mayReplaceExisting: save.overwrite && dispatched.dispatchedNow,
994
+ // Missing folders on the relative destination are created; the
995
+ // import still confines every existing ancestor to the workspace.
996
+ createParents: true,
901
997
  sizeBytes: save.download.receivedBytes,
902
998
  sha256: save.download.sha256!,
903
999
  source: {
@@ -934,11 +1030,13 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
934
1030
  app.get(
935
1031
  "/v1/workspaces/:workspaceId/browser-sessions/:browserSessionId/targets/:targetId/observation",
936
1032
  async (context) => {
1033
+ const stages = new InteractionRouteStages("observe");
937
1034
  const { workspaceId, grant, browserSessionId } = await browserRoutePreamble(
938
1035
  context,
939
1036
  "sessions:read",
940
1037
  );
941
1038
  const targetId = requireOpaqueParam(context, "targetId");
1039
+ stages.mark("auth");
942
1040
  const result = await withActiveBrowserController(
943
1041
  context,
944
1042
  grant,
@@ -946,15 +1044,114 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
946
1044
  browserSessionId,
947
1045
  "session.read",
948
1046
  "browser.read",
949
- async ({ sessionClient }) => await sessionClient.observe(targetId),
1047
+ async ({ sessionClient }) => {
1048
+ stages.mark("placement");
1049
+ try {
1050
+ return await sessionClient.observe(targetId);
1051
+ } finally {
1052
+ stages.mark("dispatch");
1053
+ }
1054
+ },
1055
+ true,
1056
+ stages,
950
1057
  );
1058
+ reportInteractionRouteStages(context, deps.observability, stages);
951
1059
  return context.json(result);
952
1060
  },
953
1061
  );
954
1062
 
1063
+ app.get(
1064
+ "/v1/workspaces/:workspaceId/browser-sessions/:browserSessionId/targets/:targetId/state",
1065
+ async (context) => {
1066
+ const stages = new InteractionRouteStages("target_state");
1067
+ const { workspaceId, grant, browserSessionId } = await browserRoutePreamble(
1068
+ context,
1069
+ "sessions:read",
1070
+ );
1071
+ const targetId = requireOpaqueParam(context, "targetId");
1072
+ stages.mark("auth");
1073
+ const result = await withActiveBrowserController(
1074
+ context,
1075
+ grant,
1076
+ workspaceId,
1077
+ browserSessionId,
1078
+ "session.read",
1079
+ "browser.read",
1080
+ async ({ sessionClient }) => {
1081
+ stages.mark("placement");
1082
+ try {
1083
+ return await sessionClient.targetState(targetId);
1084
+ } finally {
1085
+ stages.mark("dispatch");
1086
+ }
1087
+ },
1088
+ true,
1089
+ stages,
1090
+ );
1091
+ reportInteractionRouteStages(context, deps.observability, stages);
1092
+ return context.json(BrowserTargetState.parse(result));
1093
+ },
1094
+ );
1095
+
1096
+ app.post(
1097
+ "/v1/workspaces/:workspaceId/browser-sessions/:browserSessionId/targets/:targetId/dom-read",
1098
+ async (context) => {
1099
+ const { workspaceId, grant, browserSessionId } = await browserRoutePreamble(
1100
+ context,
1101
+ "sessions:read",
1102
+ );
1103
+ const targetId = requireOpaqueParam(context, "targetId");
1104
+ const request = await parseJsonBody(context, BrowserDomReadRequest);
1105
+ const result = await withActiveBrowserController(
1106
+ context,
1107
+ grant,
1108
+ workspaceId,
1109
+ browserSessionId,
1110
+ "session.read",
1111
+ "browser.read",
1112
+ async ({ sessionClient }) => await sessionClient.readDom(targetId, request),
1113
+ );
1114
+ return context.json(BrowserDomReadResponse.parse(result));
1115
+ },
1116
+ );
1117
+
1118
+ app.get(
1119
+ "/v1/workspaces/:workspaceId/browser-sessions/:browserSessionId/targets/:targetId/screenshot",
1120
+ async (context) => {
1121
+ const { workspaceId, grant, browserSessionId } = await browserRoutePreamble(
1122
+ context,
1123
+ "sessions:read",
1124
+ );
1125
+ const targetId = requireOpaqueParam(context, "targetId");
1126
+ const captureOptions = parseBrowserScreenshotOptions(new URL(context.req.url).searchParams);
1127
+ const frame = await withActiveBrowserController(
1128
+ context,
1129
+ grant,
1130
+ workspaceId,
1131
+ browserSessionId,
1132
+ "session.read",
1133
+ "browser.read",
1134
+ async ({ sessionClient, record }) => {
1135
+ if (!record.session.capabilities.screenshots) {
1136
+ throw new BrowserControlUnsupportedError(
1137
+ "This browser engine does not render page screenshots; use semantic observation",
1138
+ );
1139
+ }
1140
+ try {
1141
+ return await sessionClient.capture(targetId, captureOptions);
1142
+ } catch (error) {
1143
+ throw browserScreenshotError(error);
1144
+ }
1145
+ },
1146
+ );
1147
+ return browserScreenshotResponse(frame);
1148
+ },
1149
+ );
1150
+
955
1151
  app.post(
956
1152
  "/v1/workspaces/:workspaceId/browser-sessions/:browserSessionId/actions",
957
1153
  async (context) => {
1154
+ const stages = new InteractionRouteStages("act");
958
1155
  const { workspaceId, grant, browserSessionId } = await browserRoutePreamble(
959
1156
  context,
960
1157
  "sessions:control",
@@ -964,7 +1161,12 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
964
1161
  if (workspaceFileIds.length > 0) {
965
1162
  await requireAccessGrant(context, deps, workspaceId, "files:read");
966
1163
  }
1164
+ stages.mark("auth");
967
1165
  const startedAtMs = performance.now();
1166
+ // Omitted fences mean the target's current state. Read them once from the
1167
+ // live controller in this request (a recovery retry of the callback must
1168
+ // replay the same command digest, so it reuses the first read).
1169
+ let resolvedFences: BrowserActionFences | null = null;
968
1170
  const result = await withActiveBrowserController(
969
1171
  context,
970
1172
  grant,
@@ -973,19 +1175,38 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
973
1175
  "session.control",
974
1176
  "browser.action",
975
1177
  async ({ sessionClient, binding, record, sourceAuthorization }) => {
976
- const command = BrowserActionCommand.parse({
1178
+ stages.mark("placement");
1179
+ const passkeySync = (refresh: boolean) =>
1180
+ syncBrowserPasskeys({
1181
+ port: interactionSecrets,
1182
+ grant,
1183
+ browserSessionId,
1184
+ controllerGeneration: binding.controllerGeneration,
1185
+ client: sessionClient,
1186
+ refresh,
1187
+ log: (event, fields) => console.warn(event, { browserSessionId, ...fields }),
1188
+ });
1189
+ // Before the action this browser takes over the principal's passkeys.
1190
+ // It is independent of the fence read, so both run together; the
1191
+ // action is dispatched only after the takeover settles.
1192
+ const passkeyTakeover = passkeySync(true);
1193
+ resolvedFences ??= await resolveBrowserActionFences(request, () =>
1194
+ sessionClient.targetState(request.targetId),
1195
+ );
1196
+ stages.mark("fences");
1197
+ const commandFields = {
977
1198
  protocolVersion: BROWSER_CONTROL_PROTOCOL_VERSION,
978
1199
  operationId: request.operationId,
979
1200
  browserSessionId,
980
1201
  controllerGeneration: binding.controllerGeneration,
981
1202
  targetId: request.targetId,
982
- expectedTargetGeneration: request.expectedTargetGeneration,
983
- expectedDocumentGeneration: request.expectedDocumentGeneration,
984
- expectedFrameId: request.expectedFrameId,
1203
+ ...resolvedFences,
985
1204
  actor: interactionActorForGrant(grant),
986
1205
  observationMode: request.observationMode,
987
1206
  action: request.action,
988
- });
1207
+ };
1208
+ // Validate the public command before resolving any reference.
1209
+ BrowserActionCommand.parse(commandFields);
989
1210
  if (workspaceFileIds.length > 0) {
990
1211
  let alreadyAdmitted = false;
991
1212
  try {
@@ -1034,10 +1255,46 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
1034
1255
  });
1035
1256
  }
1036
1257
  }
1037
- return await sessionClient.action(command);
1258
+ await passkeyTakeover;
1259
+ stages.mark("passkey_pre");
1260
+ // Resolved last, right before dispatch, so TOTP codes are fresh (Q-33).
1261
+ const secretValues = await resolveInteractionSecretValues({
1262
+ port: interactionSecrets,
1263
+ grant,
1264
+ texts: browserActionSecretTexts(request.action),
1265
+ use: {
1266
+ surface: "browser",
1267
+ toolName: "browser_act",
1268
+ operationId: request.operationId,
1269
+ },
1270
+ beforeResolve: requirePlacementAcceptsSecretValues(
1271
+ deps.db,
1272
+ grant,
1273
+ workspaceId,
1274
+ record.session.placement,
1275
+ ),
1276
+ });
1277
+ stages.mark("secrets");
1278
+ try {
1279
+ return await sessionClient.action(
1280
+ BrowserActionCommand.parse({
1281
+ ...commandFields,
1282
+ ...(secretValues === undefined ? {} : { secretValues }),
1283
+ }),
1284
+ );
1285
+ } finally {
1286
+ stages.mark("dispatch");
1287
+ // Write back what the action did (new registrations, sign counts)
1288
+ // without holding the response: unacknowledged events stay in
1289
+ // browserd, and the session's next sync waits for this one.
1290
+ void passkeySync(false);
1291
+ }
1038
1292
  },
1293
+ true,
1294
+ stages,
1039
1295
  );
1040
1296
  observeBrowserActionResult(deps.observability, startedAtMs, request, result);
1297
+ reportInteractionRouteStages(context, deps.observability, stages);
1041
1298
  return context.json(result);
1042
1299
  },
1043
1300
  );
@@ -1200,209 +1457,6 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
1200
1457
  },
1201
1458
  );
1202
1459
 
1203
- app.post(
1204
- "/v1/workspaces/:workspaceId/browser-sessions/:browserSessionId/auth-runs/:authRunId/protected-fill",
1205
- async (context) => {
1206
- const { workspaceId, grant, browserSessionId } = await browserRoutePreamble(
1207
- context,
1208
- "sessions:control",
1209
- );
1210
- const authRunId = requireUuidParam(context, "authRunId");
1211
- const request = await parseJsonBody(context, ProtectedAuthFillRequest);
1212
- const startedAtMs = performance.now();
1213
- try {
1214
- const replay = await getProtectedAuthFillPreparation(deps.db, {
1215
- accountId: grant.accountId,
1216
- workspaceId,
1217
- actorSubjectId: grant.subjectId,
1218
- authRunId,
1219
- ...request,
1220
- });
1221
- if (replay?.response) {
1222
- const record = await getBrowserSessionControlRecord(deps.db, {
1223
- accountId: grant.accountId,
1224
- workspaceId,
1225
- browserSessionId,
1226
- });
1227
- await authorizeSourceSession(deps, grant, record.sourceSessionId, "session.control");
1228
- assertAuthRunBrowser(replay.run.browserSessionId, browserSessionId);
1229
- const response = ProtectedAuthFillResponse.parse(replay.response);
1230
- observeAuthMutation(deps.observability, startedAtMs, response);
1231
- return context.json(response);
1232
- }
1233
- } catch (error) {
1234
- throw browserRouteError(error);
1235
- }
1236
- const result = await withActiveBrowserController(
1237
- context,
1238
- grant,
1239
- workspaceId,
1240
- browserSessionId,
1241
- "session.control",
1242
- "browser.control",
1243
- async ({ sessionClient, binding, record }) => {
1244
- const actor = interactionActorForGrant(grant);
1245
- const scope = {
1246
- accountId: grant.accountId,
1247
- workspaceId,
1248
- actorSubjectId: grant.subjectId,
1249
- authRunId,
1250
- };
1251
- let preparation = await getProtectedAuthFillPreparation(deps.db, {
1252
- ...scope,
1253
- ...request,
1254
- });
1255
- let credential: Awaited<ReturnType<typeof loadConnectionCredentialForBroker>> = null;
1256
- if (!preparation) {
1257
- const run = await getAuthRun(deps.db, {
1258
- accountId: grant.accountId,
1259
- workspaceId,
1260
- authRunId,
1261
- });
1262
- assertAuthRunBrowser(run.browserSessionId, browserSessionId);
1263
- const connection = await getSiteAuthConnection(deps.db, {
1264
- accountId: grant.accountId,
1265
- workspaceId,
1266
- siteAuthConnectionId: run.siteAuthConnectionId,
1267
- });
1268
- const authority = connection.authorities.find(
1269
- (candidate) => candidate.id === request.authorityId,
1270
- );
1271
- if (!authority) {
1272
- throw new InteractionResourceStateError("Auth authority is not configured");
1273
- }
1274
- if (authority.kind === "external_provider") {
1275
- throw new InteractionResourceStateError(
1276
- "External auth providers cannot use protected field fill",
1277
- );
1278
- }
1279
- if (authority.kind === "connection_fields") {
1280
- credential = await loadBoundBrowserCredential(deps, grant, workspaceId, authority);
1281
- }
1282
- preparation = await prepareProtectedAuthFill(deps.db, {
1283
- ...scope,
1284
- ...request,
1285
- credentialVersion: credential?.version ?? null,
1286
- });
1287
- }
1288
- assertAuthRunBrowser(preparation.run.browserSessionId, browserSessionId);
1289
- if (preparation.run.controllerGeneration !== binding.controllerGeneration) {
1290
- throw new InteractionResourceConflictError(
1291
- "Auth run belongs to a stale browser controller",
1292
- );
1293
- }
1294
- if (preparation.response) return preparation.response;
1295
- if (
1296
- preparation.operationState === "failed" ||
1297
- preparation.operationState === "outcome_unknown"
1298
- ) {
1299
- throw new InteractionResourceStateError(
1300
- `Protected-fill operation is ${preparation.operationState.replace("_", " ")}`,
1301
- );
1302
- }
1303
- if (preparation.authority.kind === "human") {
1304
- if (preparation.operationState !== "prepared") {
1305
- throw new InteractionResourceStateError(
1306
- "Human protected-fill operation entered an invalid dispatch state",
1307
- );
1308
- }
1309
- return await completeProtectedAuthFill(deps.db, {
1310
- ...scope,
1311
- operationId: request.operationId,
1312
- status: "needs_human",
1313
- intervention: {
1314
- originatingSessionId: record.sourceSessionId,
1315
- originatingTurnId: actor.kind === "agent" ? (actor.turnId ?? null) : null,
1316
- originatingAttemptId: actor.kind === "agent" ? (actor.attemptId ?? null) : null,
1317
- originatingToolOperationId:
1318
- actor.kind === "agent" && actor.attemptId ? request.operationId : null,
1319
- kind: preparation.authority.fields.some((field) => field.purpose === "totp")
1320
- ? "mfa"
1321
- : "manual_login",
1322
- reason: "Sign in or complete authentication in this browser tab.",
1323
- expiresInSeconds: 900,
1324
- },
1325
- });
1326
- }
1327
- if (preparation.authority.kind === "external_provider") {
1328
- throw new InteractionResourceStateError(
1329
- "External auth providers cannot use protected field fill",
1330
- );
1331
- }
1332
-
1333
- if (preparation.operationState === "dispatched") {
1334
- try {
1335
- const receipt = await sessionClient.protectedAuthReceipt(request.operationId);
1336
- return await settleProtectedAuthReceipt({
1337
- deps,
1338
- scope,
1339
- receipt,
1340
- });
1341
- } catch (error) {
1342
- if (
1343
- error instanceof BrowserControlRequestError &&
1344
- error.error.code === "resource_not_found"
1345
- ) {
1346
- await markProtectedAuthFillOutcomeUnknown(deps.db, {
1347
- ...scope,
1348
- operationId: request.operationId,
1349
- errorCode: "controller_receipt_missing",
1350
- });
1351
- throw new InteractionResourceStateError(
1352
- "Protected-fill outcome is unknown after controller recovery",
1353
- );
1354
- }
1355
- throw error;
1356
- }
1357
- }
1358
-
1359
- credential ??= await loadBoundBrowserCredential(
1360
- deps,
1361
- grant,
1362
- workspaceId,
1363
- preparation.authority,
1364
- );
1365
- if (credential.version !== preparation.credentialVersion) {
1366
- throw new InteractionResourceConflictError(
1367
- "Credential changed after protected fill was prepared; use a new operation id",
1368
- );
1369
- }
1370
- const fields = resolveProtectedAuthFieldValues({
1371
- authority: preparation.authority,
1372
- requestedFields: request.fields,
1373
- credential: credential.credential,
1374
- });
1375
- await dispatchProtectedAuthFill(deps.db, {
1376
- ...scope,
1377
- operationId: request.operationId,
1378
- });
1379
- const receipt = await sessionClient.protectedAuthFill(
1380
- BrowserProtectedAuthFillCommand.parse({
1381
- protocolVersion: BROWSER_CONTROL_PROTOCOL_VERSION,
1382
- operationId: request.operationId,
1383
- browserSessionId,
1384
- controllerGeneration: binding.controllerGeneration,
1385
- targetId: preparation.run.targetId,
1386
- expectedTargetGeneration: request.expectedTargetGeneration,
1387
- expectedDocumentGeneration: request.expectedDocumentGeneration,
1388
- expectedFrameId: request.expectedFrameId,
1389
- actor,
1390
- authorityId: preparation.authority.id,
1391
- credentialVersion: preparation.credentialVersion,
1392
- allowedOrigins: preparation.origins,
1393
- fields,
1394
- submit: request.submit,
1395
- }),
1396
- );
1397
- return await settleProtectedAuthReceipt({ deps, scope, receipt });
1398
- },
1399
- );
1400
- const response = ProtectedAuthFillResponse.parse(result);
1401
- observeAuthMutation(deps.observability, startedAtMs, response);
1402
- return context.json(response);
1403
- },
1404
- );
1405
-
1406
1460
  app.post(
1407
1461
  "/v1/workspaces/:workspaceId/browser-sessions/:browserSessionId/auth-runs/:authRunId/external-auth",
1408
1462
  async (context) => {
@@ -1749,7 +1803,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
1749
1803
  browserSessionId,
1750
1804
  controllerGeneration: binding.controllerGeneration,
1751
1805
  };
1752
- await client.createViewGrant(reference, {
1806
+ const viewGrant = await client.createViewGrant(reference, {
1753
1807
  grantId,
1754
1808
  token,
1755
1809
  expiresAt,
@@ -1841,6 +1895,10 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
1841
1895
  controllerGeneration: binding.controllerGeneration,
1842
1896
  targetId: request.targetId,
1843
1897
  stream,
1898
+ ...(viewGrant.fencedInputBatches === true ? { fencedInputBatches: true } : {}),
1899
+ ...(viewGrant.focusedInputObservations === true
1900
+ ? { focusedInputObservations: true }
1901
+ : {}),
1844
1902
  expiresAt,
1845
1903
  });
1846
1904
  },
@@ -2047,6 +2105,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2047
2105
  const request = await parseJsonBody(context, BrowserSessionLifecycleRequest);
2048
2106
  const startedAtMs = performance.now();
2049
2107
  const origin = requestOrigin(context, deps.settings);
2108
+ let preparedForDispatch = false;
2050
2109
  try {
2051
2110
  const before = await getBrowserSessionControlRecord(deps.db, {
2052
2111
  accountId: grant.accountId,
@@ -2098,6 +2157,8 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2098
2157
  return context.json(parsed, 200);
2099
2158
  }
2100
2159
 
2160
+ preparedForDispatch = true;
2161
+
2101
2162
  const record = await getBrowserSessionControlRecord(deps.db, {
2102
2163
  accountId: grant.accountId,
2103
2164
  workspaceId,
@@ -2259,10 +2320,19 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2259
2320
  observeLifecycleResult(deps.observability, startedAtMs, parsed);
2260
2321
  return context.json(parsed, 200);
2261
2322
  } catch (error) {
2262
- throw browserRouteError(error);
2263
- }
2264
- },
2265
- );
2323
+ if (preparedForDispatch) {
2324
+ await failPreparedBrowserSessionSuspend(deps.db, {
2325
+ accountId: grant.accountId,
2326
+ workspaceId,
2327
+ browserSessionId,
2328
+ operationId: request.operationId,
2329
+ error: interactionFailure(error),
2330
+ });
2331
+ }
2332
+ throw browserRouteError(error);
2333
+ }
2334
+ },
2335
+ );
2266
2336
 
2267
2337
  app.post(
2268
2338
  "/v1/workspaces/:workspaceId/browser-sessions/:browserSessionId/resume",
@@ -2281,6 +2351,34 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2281
2351
  browserSessionId,
2282
2352
  });
2283
2353
  await authorizeSourceSession(deps, grant, before.sourceSessionId, "session.control");
2354
+ if (
2355
+ before.session.lifecycle === "lost" &&
2356
+ before.session.failureCode === "controller_transition_expired"
2357
+ ) {
2358
+ // A resume whose transition expired never activated. When its
2359
+ // controller still holds that generation's working copy, continue the
2360
+ // same browser from it; nothing is restored over it or discarded.
2361
+ const continued = await continueExpiredBrowserResume(
2362
+ context,
2363
+ grant,
2364
+ workspaceId,
2365
+ browserSessionId,
2366
+ request.operationId,
2367
+ );
2368
+ if (continued) {
2369
+ const parsed = BrowserSessionMutationResponse.parse(continued);
2370
+ observeLifecycleResult(deps.observability, startedAtMs, parsed);
2371
+ return context.json(parsed, 200);
2372
+ }
2373
+ // Otherwise end its retained controller generation, then resume
2374
+ // normally from the same durable state.
2375
+ await settleExpiredBrowserResume(context, grant, workspaceId, browserSessionId);
2376
+ before = await getBrowserSessionControlRecord(deps.db, {
2377
+ accountId: grant.accountId,
2378
+ workspaceId,
2379
+ browserSessionId,
2380
+ });
2381
+ }
2284
2382
  if (before.session.lifecycle === "suspended" && before.session.controller) {
2285
2383
  await finishSuspendedBrowserControllerCleanup(
2286
2384
  context,
@@ -2522,6 +2620,11 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2522
2620
  const request = await parseJsonBody(context, BrowserSessionLifecycleRequest);
2523
2621
  const startedAtMs = performance.now();
2524
2622
  const origin = requestOrigin(context, deps.settings);
2623
+ let endPreparation: {
2624
+ restoreLifecycle: BrowserSessionValue["lifecycle"];
2625
+ restoreFailureCode: string | null;
2626
+ expectedControllerGeneration: string | null;
2627
+ } | null = null;
2525
2628
  try {
2526
2629
  const before = await getBrowserSessionControlRecord(deps.db, {
2527
2630
  accountId: grant.accountId,
@@ -2536,6 +2639,9 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2536
2639
  browserSessionId,
2537
2640
  operationId: request.operationId,
2538
2641
  actorSubjectId: grant.subjectId,
2642
+ expectedLifecycle: before.session.lifecycle,
2643
+ expectedFailureCode: before.session.failureCode,
2644
+ expectedControllerGeneration: before.session.controller?.controllerGeneration ?? null,
2539
2645
  });
2540
2646
  if (isTerminalOperation(prepared.operation.state)) {
2541
2647
  if (prepared.operation.state === "completed") {
@@ -2546,11 +2652,19 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2546
2652
  browserSessionId,
2547
2653
  before.controllerHostSandboxGroupId,
2548
2654
  ).catch(() => undefined);
2655
+ await releaseBrowserPasskeys(interactionSecrets, browserSessionId);
2549
2656
  }
2550
2657
  const parsed = BrowserSessionMutationResponse.parse(prepared);
2551
2658
  observeLifecycleResult(deps.observability, startedAtMs, parsed);
2552
2659
  return context.json(parsed, 200);
2553
2660
  }
2661
+ if (before.session.lifecycle !== "ending") {
2662
+ endPreparation = {
2663
+ restoreLifecycle: before.session.lifecycle,
2664
+ restoreFailureCode: before.session.failureCode,
2665
+ expectedControllerGeneration: before.session.controller?.controllerGeneration ?? null,
2666
+ };
2667
+ }
2554
2668
 
2555
2669
  const record = await getBrowserSessionControlRecord(deps.db, {
2556
2670
  accountId: grant.accountId,
@@ -2559,13 +2673,19 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2559
2673
  operationId: request.operationId,
2560
2674
  });
2561
2675
  const binding = record.session.controller;
2562
- if (!binding) {
2676
+ // A browser with no controller, or whose controller can never be
2677
+ // reached again, ends here: nothing physical remains to clean up, and
2678
+ // completing under the bound generation fences that controller off.
2679
+ if (
2680
+ !binding ||
2681
+ (await browserControllerGone(grant, workspaceId, record.session.placement))
2682
+ ) {
2563
2683
  const completed = await completeBrowserSessionEnd(deps.db, {
2564
2684
  accountId: grant.accountId,
2565
2685
  workspaceId,
2566
2686
  operationId: request.operationId,
2567
2687
  browserSessionId,
2568
- expectedControllerGeneration: null,
2688
+ expectedControllerGeneration: binding?.controllerGeneration ?? null,
2569
2689
  });
2570
2690
  await releaseInteractionHolder(
2571
2691
  deps,
@@ -2574,6 +2694,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2574
2694
  browserSessionId,
2575
2695
  record.controllerHostSandboxGroupId,
2576
2696
  ).catch(() => undefined);
2697
+ await releaseBrowserPasskeys(interactionSecrets, browserSessionId);
2577
2698
  const parsed = BrowserSessionMutationResponse.parse(completed);
2578
2699
  observeLifecycleResult(deps.observability, startedAtMs, parsed);
2579
2700
  return context.json(parsed, 200);
@@ -2608,13 +2729,10 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2608
2729
  controllerGeneration: binding.controllerGeneration,
2609
2730
  },
2610
2731
  async () =>
2611
- await client.endSession(
2612
- {
2613
- browserSessionId,
2614
- controllerGeneration: binding.controllerGeneration,
2615
- },
2616
- { removeState: true },
2617
- ),
2732
+ await endBrowserDiscardingLostRuntime(client, {
2733
+ browserSessionId,
2734
+ controllerGeneration: binding.controllerGeneration,
2735
+ }),
2618
2736
  );
2619
2737
  } catch (error) {
2620
2738
  // Physical absence already satisfies an exact end. A stale live
@@ -2637,6 +2755,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2637
2755
  browserSessionId,
2638
2756
  placement.controllerHostSandboxGroupId,
2639
2757
  ).catch(() => undefined);
2758
+ await releaseBrowserPasskeys(interactionSecrets, browserSessionId);
2640
2759
  return completed;
2641
2760
  },
2642
2761
  );
@@ -2644,6 +2763,16 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2644
2763
  observeLifecycleResult(deps.observability, startedAtMs, parsed);
2645
2764
  return context.json(parsed, 200);
2646
2765
  } catch (error) {
2766
+ if (endPreparation) {
2767
+ await failPreparedBrowserSessionEnd(deps.db, {
2768
+ accountId: grant.accountId,
2769
+ workspaceId,
2770
+ browserSessionId,
2771
+ operationId: request.operationId,
2772
+ ...endPreparation,
2773
+ error: interactionFailure(error),
2774
+ });
2775
+ }
2647
2776
  throw browserRouteError(error);
2648
2777
  }
2649
2778
  },
@@ -2657,26 +2786,27 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2657
2786
  operation: ChannelAOperation,
2658
2787
  waitSignal: AbortSignal,
2659
2788
  callback: (placement: BrowserPlacement) => Promise<T>,
2789
+ allowOperationReplay = true,
2660
2790
  ): Promise<T> {
2661
2791
  if (expectedPlacement?.kind === "attached_device") {
2662
2792
  waitSignal.throwIfAborted();
2793
+ // Personal machines are controlled only for their owner, as everywhere else.
2794
+ const viewer = await attachedBrowserViewer(deps, grant, sourceSession.workspaceId);
2663
2795
  const device = await getAttachedBrowserDevice(deps.db, {
2664
- accountId: grant.accountId,
2665
- workspaceId: sourceSession.workspaceId,
2796
+ ...viewer,
2666
2797
  deviceId: expectedPlacement.deviceId,
2667
2798
  });
2668
2799
  if (device.state !== "connected") {
2800
+ recordAttachedBrowserUnavailable(deps.observability, "disconnected");
2669
2801
  throw new BrowserSessionStateError("Attached browser is disconnected");
2670
2802
  }
2671
- const enrollment = await getLiveEnrollmentConnection(
2672
- deps.db,
2673
- sourceSession.workspaceId,
2674
- device.enrollmentId,
2675
- );
2803
+ const enrollment = await getLiveEnrollmentConnection(deps.db, viewer, device.enrollmentId);
2676
2804
  if (!enrollment || enrollment.status !== "active" || !enrollment.connectionInstanceId) {
2805
+ recordAttachedBrowserUnavailable(deps.observability, "machine_unavailable");
2677
2806
  throw new BrowserSessionStateError("Attached browser machine is unavailable");
2678
2807
  }
2679
2808
  if (!enrollment.workspaceRoot) {
2809
+ recordAttachedBrowserUnavailable(deps.observability, "no_workspace_root");
2680
2810
  throw new BrowserSessionStateError(
2681
2811
  "Attached browser machine has not reported an absolute workspace root",
2682
2812
  );
@@ -2746,9 +2876,16 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2746
2876
  workspaceId: sourceSession.workspaceId,
2747
2877
  session: sourceSession,
2748
2878
  subjectId: grant.subjectId,
2879
+ grant,
2749
2880
  waitSignal,
2750
2881
  operation,
2751
2882
  retryControllerTransport: operation === "browser.read" || operation === "browser.action",
2883
+ allowOperationReplay,
2884
+ ...(expectedPlacementInstanceId &&
2885
+ (expectedPlacement?.kind === "sandbox_group" ||
2886
+ expectedPlacement?.kind === "external_provider")
2887
+ ? { retainedInstanceId: expectedPlacementInstanceId }
2888
+ : {}),
2752
2889
  },
2753
2890
  async (handle) => {
2754
2891
  if (expectedPlacement?.kind === "sandbox_group") {
@@ -2792,6 +2929,11 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
2792
2929
  resolved.kind !== "selfhosted" ||
2793
2930
  resolved.sandboxId !== expectedPlacement.sandboxId
2794
2931
  ) {
2932
+ if (operation === "browser.create") {
2933
+ throw new HTTPException(422, {
2934
+ message: wrongInteractionPlacementMessage(expectedPlacement.sandboxId),
2935
+ });
2936
+ }
2795
2937
  return await throwBrowserSourcePlacementChanged(
2796
2938
  grant,
2797
2939
  sourceSession.id,
@@ -3024,6 +3166,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
3024
3166
  sourceAuthorization: ResolvedSessionAuthorization | null;
3025
3167
  }) => Promise<T>,
3026
3168
  recoverMissing = true,
3169
+ stages?: InteractionRouteStages,
3027
3170
  ): Promise<T> {
3028
3171
  try {
3029
3172
  const record = await getBrowserSessionControlRecord(deps.db, {
@@ -3031,12 +3174,14 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
3031
3174
  workspaceId,
3032
3175
  browserSessionId,
3033
3176
  });
3177
+ stages?.mark("record");
3034
3178
  const sourceAuthorization = await authorizeSourceSession(
3035
3179
  deps,
3036
3180
  grant,
3037
3181
  record.sourceSessionId,
3038
3182
  authorizationOperation,
3039
3183
  );
3184
+ stages?.mark("authorize");
3040
3185
  let sourceSession: Session | null = null;
3041
3186
  if (record.session.placement?.kind === "connected_machine") {
3042
3187
  sourceSession = await requireSourceSession(deps, workspaceId, record.sourceSessionId);
@@ -3048,6 +3193,7 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
3048
3193
  );
3049
3194
  }
3050
3195
  }
3196
+ stages?.mark("source_session");
3051
3197
  if (record.session.lifecycle !== "active" || !record.session.controller) {
3052
3198
  throw new BrowserSessionStateError("BrowserSession is not active");
3053
3199
  }
@@ -3061,10 +3207,19 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
3061
3207
  if (!admitted) {
3062
3208
  throw new BrowserSessionStateError("BrowserSession controller authority changed");
3063
3209
  }
3064
- const run = async (placement: BrowserPlacement): Promise<T> => {
3210
+ stages?.mark("control");
3211
+ const safelyReplayable =
3212
+ browserSessionStorageMode(record.session) !== "ephemeral_context" &&
3213
+ (channelOperation === "browser.read" || channelOperation === "browser.action");
3214
+ let controllerTransportFailed = false;
3215
+ const controllerRecoveryState = { attempted: false };
3216
+ const run = async (
3217
+ placement: BrowserPlacement,
3218
+ provisionedClient?: BrowserControlClient,
3219
+ ): Promise<T> => {
3065
3220
  // An active binding already proves browserd was provisioned. Reusing
3066
3221
  // it avoids restarting/checking the sidecar on every live input.
3067
- const client = connectController(deps, grant, record, placement);
3222
+ const client = provisionedClient ?? connectController(deps, grant, record, placement);
3068
3223
  const tokens = deriveBrowserSessionControllerTokens({
3069
3224
  rootSecret: browserAuthorityRoot(deps),
3070
3225
  accountId: grant.accountId,
@@ -3093,6 +3248,30 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
3093
3248
  try {
3094
3249
  result = await callback(controller);
3095
3250
  } catch (error) {
3251
+ if (
3252
+ browserSessionStorageMode(record.session) === "ephemeral_context" &&
3253
+ (isMissingBrowserControllerSession(error) ||
3254
+ (error instanceof BrowserControlRequestError &&
3255
+ error.error.code === "controller_lost"))
3256
+ ) {
3257
+ const retired = await markEphemeralBrowserSessionLost(deps.db, {
3258
+ accountId: grant.accountId,
3259
+ workspaceId,
3260
+ browserSessionId,
3261
+ controllerGeneration: binding.controllerGeneration,
3262
+ });
3263
+ if (retired)
3264
+ await releaseInteractionHolder(
3265
+ deps,
3266
+ grant,
3267
+ workspaceId,
3268
+ browserSessionId,
3269
+ record.controllerHostSandboxGroupId,
3270
+ );
3271
+ throw new BrowserSessionStateError(
3272
+ "Ephemeral browser context was lost; create a new BrowserSession. No identity or operation was replayed.",
3273
+ );
3274
+ }
3096
3275
  if (!recoverMissing || !isMissingBrowserControllerSession(error)) throw error;
3097
3276
  await recoverActiveBrowserController(
3098
3277
  deps,
@@ -3102,25 +3281,22 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
3102
3281
  placement,
3103
3282
  client,
3104
3283
  tokens,
3284
+ authorizationOperation,
3105
3285
  );
3106
3286
  result = await callback(controller);
3107
3287
  }
3108
3288
  return result;
3109
3289
  };
3110
3290
  const cached = await cachedBrowserPlacement(record);
3291
+ stages?.mark("placement_cache");
3111
3292
  if (cached) {
3112
3293
  try {
3113
3294
  return await run(cached);
3114
3295
  } catch (error) {
3115
- const safelyReplayable =
3116
- channelOperation === "browser.read" || channelOperation === "browser.action";
3117
- if (
3118
- !safelyReplayable ||
3119
- (!(error instanceof BrowserControlTransportError) &&
3120
- !(error instanceof BrowserControlRequestError && error.retryable))
3121
- ) {
3296
+ if (!safelyReplayable || !isRetryableControllerTransport(error)) {
3122
3297
  throw error;
3123
3298
  }
3299
+ controllerTransportFailed = true;
3124
3300
  await recordLeaseControllerDataPlaneUrl(deps.db, {
3125
3301
  accountId: grant.accountId,
3126
3302
  workspaceId,
@@ -3140,12 +3316,54 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
3140
3316
  binding.placementInstanceId,
3141
3317
  channelOperation,
3142
3318
  context.req.raw.signal,
3143
- async (placement) =>
3144
- await run(
3145
- await cacheBrowserControllerPlacement(grant, workspaceId, placement).catch(
3146
- () => placement,
3147
- ),
3148
- ),
3319
+ async (placement) => {
3320
+ stages?.mark("placement_resolve");
3321
+ const use = async () => {
3322
+ const cachedPlacement = await cacheBrowserControllerPlacement(
3323
+ grant,
3324
+ workspaceId,
3325
+ placement,
3326
+ ).catch(() => placement);
3327
+ stages?.mark("placement_cache_write");
3328
+ return await run(cachedPlacement);
3329
+ };
3330
+ if (browserSessionStorageMode(record.session) === "ephemeral_context") return await use();
3331
+ return await withControllerTransportRecovery({
3332
+ channelOperation,
3333
+ placementKind: placement.placement.kind,
3334
+ recoveryState: controllerRecoveryState,
3335
+ transportAlreadyFailed: controllerTransportFailed,
3336
+ use,
3337
+ admitRecovery: async () => {
3338
+ if (
3339
+ !(await touchBrowserSessionController(deps.db, {
3340
+ accountId: grant.accountId,
3341
+ workspaceId,
3342
+ browserSessionId,
3343
+ controllerGeneration: binding.controllerGeneration,
3344
+ }))
3345
+ ) {
3346
+ throw new BrowserSessionStateError("BrowserSession controller authority changed");
3347
+ }
3348
+ },
3349
+ recover: async () => {
3350
+ // Provision and restore through the original exec-capable session;
3351
+ // the cached tunnel alone cannot start browserd or its display.
3352
+ const client = await provisionController(
3353
+ deps,
3354
+ grant,
3355
+ record,
3356
+ placement,
3357
+ requestOrigin(context, deps.settings),
3358
+ );
3359
+ await cacheBrowserControllerPlacement(grant, workspaceId, placement).catch(
3360
+ () => placement,
3361
+ );
3362
+ return await run(placement, client);
3363
+ },
3364
+ });
3365
+ },
3366
+ browserSessionStorageMode(record.session) !== "ephemeral_context",
3149
3367
  );
3150
3368
  } catch (error) {
3151
3369
  throw browserRouteError(error);
@@ -3171,6 +3389,23 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
3171
3389
  );
3172
3390
  }
3173
3391
 
3392
+ /** See `interactionControllerGone`: a removed machine or a disconnected
3393
+ * Chrome device; a managed browser on a machine that is only offline waits. */
3394
+ async function browserControllerGone(
3395
+ grant: AccessGrant,
3396
+ workspaceId: string,
3397
+ placement: InteractionPlacement | null,
3398
+ ): Promise<boolean> {
3399
+ if (placement?.kind !== "connected_machine" && placement?.kind !== "attached_device") {
3400
+ return false;
3401
+ }
3402
+ return await interactionControllerGone(
3403
+ deps.db,
3404
+ await attachedBrowserViewer(deps, grant, workspaceId),
3405
+ placement,
3406
+ );
3407
+ }
3408
+
3174
3409
  async function recoverActiveBrowserController(
3175
3410
  routeDeps: ApiRouteDeps,
3176
3411
  grant: AccessGrant,
@@ -3179,7 +3414,66 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
3179
3414
  placement: BrowserPlacement,
3180
3415
  client: BrowserControlClient,
3181
3416
  tokens: ReturnType<typeof deriveBrowserSessionControllerTokens>,
3417
+ authorizationOperation: SessionAuthorizationOperation,
3182
3418
  ): Promise<void> {
3419
+ if (browserSessionStorageMode(record.session) === "ephemeral_context") {
3420
+ throw new BrowserSessionStateError(
3421
+ "Ephemeral browser context was lost; create a new BrowserSession. Its identity and operations cannot be restored or replayed.",
3422
+ );
3423
+ }
3424
+ const transport = browserRuntimeTransport(record.session, placement.transport);
3425
+ const recoverWorkingDirectory =
3426
+ transport.kind === "managed" &&
3427
+ transport.engine === "chromium" &&
3428
+ !transport.ephemeralPartition &&
3429
+ browserSessionStorageMode(record.session) === "private_profile";
3430
+ if (recoverWorkingDirectory) {
3431
+ // Missing controller memory cannot admit a stale source, token, placement
3432
+ // or route snapshot. Reuse the existing durable authority before recovery.
3433
+ await authorizeSourceSession(
3434
+ routeDeps,
3435
+ grant,
3436
+ record.sourceSessionId,
3437
+ authorizationOperation,
3438
+ );
3439
+ const current = await getBrowserSessionControlRecord(routeDeps.db, {
3440
+ accountId: grant.accountId,
3441
+ workspaceId: record.session.workspaceId,
3442
+ browserSessionId: record.session.id,
3443
+ });
3444
+ if (
3445
+ current.sourceSessionId !== record.sourceSessionId ||
3446
+ current.session.lifecycle !== "active" ||
3447
+ current.session.controller?.controllerGeneration !== binding.controllerGeneration ||
3448
+ current.session.controller.placementInstanceId !== placement.placementInstanceId ||
3449
+ current.session.controller.controllerId !== binding.controllerId ||
3450
+ current.tokenGeneration !== record.tokenGeneration ||
3451
+ !sameInteractionPlacement(current.session.placement, record.session.placement) ||
3452
+ current.session.driverId !== record.session.driverId ||
3453
+ current.session.engine !== record.session.engine ||
3454
+ current.session.headless !== record.session.headless ||
3455
+ current.session.linkedComputerSessionId !== record.session.linkedComputerSessionId ||
3456
+ current.session.networkRouteId !== record.session.networkRouteId ||
3457
+ JSON.stringify(current.networkRouteAuthority) !==
3458
+ JSON.stringify(record.networkRouteAuthority)
3459
+ ) {
3460
+ throw new BrowserSessionStateError("BrowserSession controller authority changed");
3461
+ }
3462
+ if (record.session.placement.kind === "connected_machine") {
3463
+ const source = await requireSourceSession(
3464
+ routeDeps,
3465
+ record.session.workspaceId,
3466
+ record.sourceSessionId,
3467
+ );
3468
+ if (source.activeSandboxId !== record.session.placement.sandboxId) {
3469
+ return await throwBrowserSourcePlacementChanged(
3470
+ grant,
3471
+ record.sourceSessionId,
3472
+ record.session.placement.sandboxId,
3473
+ );
3474
+ }
3475
+ }
3476
+ }
3183
3477
  const linkedComputer = await ensureLinkedComputerController(
3184
3478
  routeDeps,
3185
3479
  grant,
@@ -3199,13 +3493,25 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
3199
3493
  record.session.headless,
3200
3494
  linkedComputer !== null,
3201
3495
  );
3496
+ if (
3497
+ recoverWorkingDirectory &&
3498
+ !(await touchBrowserSessionController(routeDeps.db, {
3499
+ accountId: grant.accountId,
3500
+ workspaceId: record.session.workspaceId,
3501
+ browserSessionId: record.session.id,
3502
+ controllerGeneration: binding.controllerGeneration,
3503
+ }))
3504
+ ) {
3505
+ throw new BrowserSessionStateError("BrowserSession controller authority changed");
3506
+ }
3202
3507
  await client.createSession({
3203
3508
  browserSessionId: record.session.id,
3204
3509
  controllerGeneration: binding.controllerGeneration,
3205
3510
  tokenGeneration: record.tokenGeneration,
3206
3511
  ...tokens,
3207
3512
  headed: !record.session.headless,
3208
- transport: browserRuntimeTransport(record.session, placement.transport),
3513
+ transport,
3514
+ ...(recoverWorkingDirectory ? { recoverExistingWorkingDirectory: true } : {}),
3209
3515
  ...(linkedComputer ? { linkedComputer } : {}),
3210
3516
  ...(networkRoute ? { networkRoute } : {}),
3211
3517
  });
@@ -3219,6 +3525,245 @@ export function registerBrowserSessionRoutes(app: Hono, deps: ApiRouteDeps): voi
3219
3525
  }
3220
3526
  }
3221
3527
 
3528
+ /** Continue an expired resume on its retained controller generation when the
3529
+ * controller still holds that generation's own working copy (a resident
3530
+ * browser, or its preserved profile and launch receipt after the controller
3531
+ * restarted). The controller's existing same-generation recovery adopts or
3532
+ * relaunches it from that directory, so the record, its history and the
3533
+ * possibly newer working profile all survive. Returns null when the browser
3534
+ * cannot be continued this way, leaving the record unchanged for the
3535
+ * settle-and-restore path. */
3536
+ async function continueExpiredBrowserResume(
3537
+ context: Context,
3538
+ grant: AccessGrant,
3539
+ workspaceId: string,
3540
+ browserSessionId: string,
3541
+ operationId: string,
3542
+ ): Promise<BrowserSessionMutationResponse | null> {
3543
+ const expired = await getExpiredBrowserSessionResume(deps.db, {
3544
+ accountId: grant.accountId,
3545
+ workspaceId,
3546
+ browserSessionId,
3547
+ });
3548
+ if (!expired?.controllerGeneration) return null;
3549
+ const record = await getBrowserSessionControlRecord(deps.db, {
3550
+ accountId: grant.accountId,
3551
+ workspaceId,
3552
+ browserSessionId,
3553
+ });
3554
+ const binding = record.session.controller;
3555
+ if (binding?.controllerGeneration !== expired.controllerGeneration) {
3556
+ throw new BrowserSessionStateError("BrowserSession controller authority changed");
3557
+ }
3558
+ // Only a private profile has a working copy the controller can adopt.
3559
+ if (browserSessionStorageMode(record.session) !== "private_profile") return null;
3560
+ const rootSecret = browserAuthorityRoot(deps);
3561
+ const sourceSession = await requireSourceSession(deps, workspaceId, record.sourceSessionId);
3562
+ if (
3563
+ record.session.placement.kind === "connected_machine" &&
3564
+ sourceSession.activeSandboxId !== record.session.placement.sandboxId
3565
+ ) {
3566
+ return null;
3567
+ }
3568
+ return await withBrowserPlacement(
3569
+ sourceSession,
3570
+ grant,
3571
+ record.session.placement,
3572
+ binding.placementInstanceId,
3573
+ "browser.resume",
3574
+ context.req.raw.signal,
3575
+ async (placement) => {
3576
+ const transport = browserRuntimeTransport(record.session, placement.transport);
3577
+ if (
3578
+ transport.kind !== "managed" ||
3579
+ transport.engine !== "chromium" ||
3580
+ transport.ephemeralPartition
3581
+ ) {
3582
+ return null;
3583
+ }
3584
+ const client = await provisionController(
3585
+ deps,
3586
+ grant,
3587
+ record,
3588
+ placement,
3589
+ requestOrigin(context, deps.settings),
3590
+ );
3591
+ let linkedComputer: Awaited<ReturnType<typeof ensureLinkedComputerController>>;
3592
+ try {
3593
+ linkedComputer = await ensureLinkedComputerController(
3594
+ deps,
3595
+ grant,
3596
+ record.session,
3597
+ placement,
3598
+ client,
3599
+ );
3600
+ } catch (error) {
3601
+ // Its working copy was launched on a desktop that is gone.
3602
+ if (error instanceof BrowserSessionStateError) return null;
3603
+ throw error;
3604
+ }
3605
+ const networkRoute = await resolveActiveBrowserNetworkRouteLaunch({
3606
+ deps,
3607
+ grant,
3608
+ record,
3609
+ placement: placement.placement,
3610
+ });
3611
+ const tokens = deriveBrowserSessionControllerTokens({
3612
+ rootSecret,
3613
+ accountId: grant.accountId,
3614
+ workspaceId,
3615
+ browserSessionId,
3616
+ placement: placement.placement,
3617
+ placementInstanceId: placement.placementInstanceId,
3618
+ controllerGeneration: binding.controllerGeneration,
3619
+ tokenGeneration: record.tokenGeneration,
3620
+ });
3621
+ const interactionHeld = await ensureInteractionHolder(
3622
+ deps,
3623
+ grant,
3624
+ sourceSession,
3625
+ browserSessionId,
3626
+ placement,
3627
+ context.req.raw.signal,
3628
+ );
3629
+ const release = async () => {
3630
+ if (!interactionHeld) return;
3631
+ await releaseInteractionHolder(
3632
+ deps,
3633
+ grant,
3634
+ workspaceId,
3635
+ browserSessionId,
3636
+ placement.controllerHostSandboxGroupId,
3637
+ ).catch(() => undefined);
3638
+ };
3639
+ try {
3640
+ await withInteractionHolderHeartbeat(
3641
+ deps,
3642
+ {
3643
+ grant,
3644
+ workspaceId,
3645
+ sandboxGroupId: placement.controllerHostSandboxGroupId,
3646
+ holderId: interactionHolderId(browserSessionId),
3647
+ operationId,
3648
+ // This operation is recorded only after the controller adopts
3649
+ // the working copy; until then the expired resume holds authority.
3650
+ continuesExpiredBrowserResume: true,
3651
+ resourceId: browserSessionId,
3652
+ controllerGeneration: binding.controllerGeneration,
3653
+ },
3654
+ async () => {
3655
+ await ensureHeadedBrowserDisplayStack(
3656
+ placement.session,
3657
+ record.session.headless,
3658
+ linkedComputer !== null,
3659
+ );
3660
+ await client.createSession({
3661
+ browserSessionId,
3662
+ controllerGeneration: binding.controllerGeneration,
3663
+ tokenGeneration: record.tokenGeneration,
3664
+ ...tokens,
3665
+ headed: !record.session.headless,
3666
+ transport,
3667
+ recoverExistingWorkingDirectory: true,
3668
+ ...(linkedComputer ? { linkedComputer } : {}),
3669
+ ...(networkRoute ? { networkRoute } : {}),
3670
+ });
3671
+ },
3672
+ );
3673
+ } catch (error) {
3674
+ // The controller proves no adoptable working copy for this generation
3675
+ // (none was completed, or its authority or profile changed): leave
3676
+ // the record for the settle-and-restore path.
3677
+ if (
3678
+ isDefiniteBrowserControllerFailure(error) &&
3679
+ error instanceof BrowserControlRequestError &&
3680
+ error.error.code === "resource_unavailable"
3681
+ ) {
3682
+ await release();
3683
+ return null;
3684
+ }
3685
+ throw error;
3686
+ }
3687
+ return await continueExpiredBrowserSessionResume(deps.db, {
3688
+ accountId: grant.accountId,
3689
+ workspaceId,
3690
+ browserSessionId,
3691
+ operationId,
3692
+ actorSubjectId: grant.subjectId,
3693
+ controller: binding,
3694
+ });
3695
+ },
3696
+ );
3697
+ }
3698
+
3699
+ /** End the retained controller generation of an expired resume and return
3700
+ * the browser to suspended. Other lost browsers are left unchanged so the
3701
+ * normal resume preparation refuses them. */
3702
+ async function settleExpiredBrowserResume(
3703
+ context: Context,
3704
+ grant: AccessGrant,
3705
+ workspaceId: string,
3706
+ browserSessionId: string,
3707
+ ): Promise<void> {
3708
+ const expired = await getExpiredBrowserSessionResume(deps.db, {
3709
+ accountId: grant.accountId,
3710
+ workspaceId,
3711
+ browserSessionId,
3712
+ });
3713
+ if (!expired) return;
3714
+ const record = await getBrowserSessionControlRecord(deps.db, {
3715
+ accountId: grant.accountId,
3716
+ workspaceId,
3717
+ browserSessionId,
3718
+ });
3719
+ const binding = record.session.controller;
3720
+ if ((binding?.controllerGeneration ?? null) !== expired.controllerGeneration) {
3721
+ throw new BrowserSessionStateError("BrowserSession controller authority changed");
3722
+ }
3723
+ if (binding) {
3724
+ browserAuthorityRoot(deps);
3725
+ const sourceSession = await requireSourceSession(deps, workspaceId, record.sourceSessionId);
3726
+ await withBrowserPlacement(
3727
+ sourceSession,
3728
+ grant,
3729
+ record.session.placement,
3730
+ binding.placementInstanceId,
3731
+ "browser.resume",
3732
+ context.req.raw.signal,
3733
+ async (placement) => {
3734
+ const client = await provisionController(
3735
+ deps,
3736
+ grant,
3737
+ record,
3738
+ placement,
3739
+ requestOrigin(context, deps.settings),
3740
+ );
3741
+ // A resident or provably never-completed launch is ended exactly; an
3742
+ // interrupted launch whose profile no browser runs is set aside
3743
+ // intact; a launch that completed (adoptable) or whose profile may
3744
+ // be in use stays preserved and refuses this resume.
3745
+ await endExpiredResumeSettingAsideInterruptedLaunch(client, {
3746
+ browserSessionId,
3747
+ controllerGeneration: binding.controllerGeneration,
3748
+ });
3749
+ },
3750
+ );
3751
+ }
3752
+ await settleExpiredBrowserSessionResume(deps.db, {
3753
+ accountId: grant.accountId,
3754
+ workspaceId,
3755
+ browserSessionId,
3756
+ expectedControllerGeneration: expired.controllerGeneration,
3757
+ });
3758
+ await releaseInteractionHolder(
3759
+ deps,
3760
+ grant,
3761
+ workspaceId,
3762
+ browserSessionId,
3763
+ record.controllerHostSandboxGroupId,
3764
+ ).catch(() => undefined);
3765
+ }
3766
+
3222
3767
  async function finishSuspendedBrowserControllerCleanup(
3223
3768
  context: Context,
3224
3769
  grant: AccessGrant,
@@ -3286,18 +3831,27 @@ function sourcePlacementChangedApiError(interactionResource: "browser_session"):
3286
3831
  });
3287
3832
  }
3288
3833
 
3289
- function browserCreateInput(
3834
+ export function browserCreateInput(
3290
3835
  grant: AccessGrant,
3291
3836
  workspaceId: string,
3292
3837
  request: CreateBrowserSessionRequestValue,
3293
3838
  placement: InteractionPlacement,
3294
3839
  ) {
3840
+ if (request.storageMode === "ephemeral_context" && placement.kind !== "sandbox_group") {
3841
+ throw new BrowserControlUnsupportedError(
3842
+ "Ephemeral contexts require a managed sandbox placement",
3843
+ );
3844
+ }
3295
3845
  const attached = placement.kind === "attached_device";
3296
3846
  const external = placement.kind === "external_provider";
3297
3847
  const engine = attached ? ("chrome" as const) : external ? ("external" as const) : request.engine;
3298
- if (engine === "lightpanda" && placement.kind !== "sandbox_group") {
3848
+ if (
3849
+ request.engine === "lightpanda" &&
3850
+ placement.kind !== "sandbox_group" &&
3851
+ placement.kind !== "connected_machine"
3852
+ ) {
3299
3853
  throw new BrowserControlUnsupportedError(
3300
- "Lightpanda is currently available only in managed agent sandboxes",
3854
+ "Lightpanda requires a managed sandbox or Connected Machine browser placement",
3301
3855
  );
3302
3856
  }
3303
3857
  return {
@@ -3310,11 +3864,13 @@ function browserCreateInput(
3310
3864
  initialUrl: request.initialUrl ?? null,
3311
3865
  placement,
3312
3866
  driverId:
3313
- engine === "lightpanda"
3314
- ? LIGHTPANDA_DRIVER_ID
3315
- : external
3316
- ? EXTERNAL_BROWSER_DRIVER_ID
3317
- : BROWSER_DRIVER_ID,
3867
+ request.storageMode === "ephemeral_context"
3868
+ ? EPHEMERAL_CHROMIUM_DRIVER_ID
3869
+ : engine === "lightpanda"
3870
+ ? LIGHTPANDA_DRIVER_ID
3871
+ : external
3872
+ ? EXTERNAL_BROWSER_DRIVER_ID
3873
+ : BROWSER_DRIVER_ID,
3318
3874
  engine,
3319
3875
  headless: attached ? false : request.headless,
3320
3876
  identityId: request.identityId ?? null,
@@ -3339,7 +3895,21 @@ function browserCreateInput(
3339
3895
  function browserRuntimeTransport(
3340
3896
  session: BrowserSessionValue,
3341
3897
  transport: PlacementBrowserTransport,
3898
+ placementInstanceId?: string,
3899
+ rootSecret?: string,
3342
3900
  ): PlacementBrowserTransport {
3901
+ if (browserSessionStorageMode(session) === "ephemeral_context") {
3902
+ if (transport.kind !== "managed" || !placementInstanceId || !rootSecret) {
3903
+ throw new BrowserControlUnsupportedError(
3904
+ "Ephemeral contexts cannot recover or change their placement",
3905
+ );
3906
+ }
3907
+ return {
3908
+ kind: "managed",
3909
+ engine: "chromium",
3910
+ ephemeralPartition: ephemeralBrowserPartition(session, placementInstanceId, rootSecret),
3911
+ };
3912
+ }
3343
3913
  if (transport.kind !== "managed") return transport;
3344
3914
  return {
3345
3915
  kind: "managed",
@@ -3401,6 +3971,11 @@ function assertCreateReplay(
3401
3971
  "BrowserSession create operation is bound to another placement",
3402
3972
  );
3403
3973
  }
3974
+ if (request.storageMode !== browserSessionStorageMode(session)) {
3975
+ throw new BrowserSessionOperationConflictError(
3976
+ "BrowserSession create operation is bound to another storage mode",
3977
+ );
3978
+ }
3404
3979
  const expectedEngine =
3405
3980
  session.placement.kind === "attached_device"
3406
3981
  ? "chrome"
@@ -3739,7 +4314,9 @@ async function ensureInteractionHolder(
3739
4314
  if (!placement.lease?.instanceId) {
3740
4315
  throw new BrowserSessionStateError("BrowserSession lease placement is unavailable");
3741
4316
  }
3742
- const sandboxRuntime = await resolveSessionSandboxRuntime(deps.db, deps.settings, sourceSession);
4317
+ const sandboxRuntime = await resolveSessionSandboxRuntime(deps.db, deps.settings, sourceSession, {
4318
+ grant,
4319
+ });
3743
4320
  const acquired = await acquireLease(deps.db, {
3744
4321
  accountId: grant.accountId,
3745
4322
  workspaceId: sourceSession.workspaceId,
@@ -3748,8 +4325,13 @@ async function ensureInteractionHolder(
3748
4325
  holderId: interactionHolderId(browserSessionId),
3749
4326
  subjectId: sourceSession.id,
3750
4327
  backend: placement.lease.backend,
4328
+ warmBilling: {
4329
+ mode: deps.settings.sandboxWarmBillingMode,
4330
+ rateMicrosPerSecond: sandboxWarmRateMicrosPerSecond(deps.settings, placement.lease.backend),
4331
+ },
3751
4332
  os: placement.lease.os,
3752
4333
  image: sandboxRuntime.image,
4334
+ imagePolicy: "new_creates_only",
3753
4335
  rigVersionId: sourceSession.rigVersionId,
3754
4336
  leaseTtlMs: deps.settings.sandboxLeaseTtlMs,
3755
4337
  expectedEpoch: placement.lease.leaseEpoch,
@@ -4023,6 +4605,7 @@ async function settleBrowserSessionSuspensionCaptureFailure(
4023
4605
  return await failBrowserSessionSuspension(deps.db, {
4024
4606
  ...input,
4025
4607
  ...(outcomeUnknown ? { state: "outcome_unknown" as const } : {}),
4608
+ missingControllerSession: error.status === 404 && isMissingBrowserControllerSession(error),
4026
4609
  error: error.error,
4027
4610
  });
4028
4611
  }
@@ -4160,6 +4743,86 @@ function optionalBoundedInteger(
4160
4743
  return parsed;
4161
4744
  }
4162
4745
 
4746
+ export function parseBrowserScreenshotOptions(
4747
+ query: URLSearchParams,
4748
+ ): BrowserControlScreenshotOptions {
4749
+ for (const key of ["fullPage", "format", "quality"]) {
4750
+ if (query.getAll(key).length > 1) {
4751
+ throw new HTTPException(400, { message: `duplicate browser screenshot ${key}` });
4752
+ }
4753
+ }
4754
+ const fullPage = query.get("fullPage");
4755
+ if (fullPage !== null && fullPage !== "true" && fullPage !== "false") {
4756
+ throw new HTTPException(400, { message: "invalid browser screenshot fullPage" });
4757
+ }
4758
+ const format = query.get("format");
4759
+ if (format !== null && format !== "jpeg" && format !== "png") {
4760
+ throw new HTTPException(400, { message: "invalid browser screenshot format" });
4761
+ }
4762
+ const quality = query.get("quality");
4763
+ const parsedQuality = quality === null ? null : optionalBoundedInteger(quality, 1, 100);
4764
+ if (quality !== null && parsedQuality === null) {
4765
+ throw new HTTPException(400, { message: "invalid browser screenshot quality" });
4766
+ }
4767
+ return {
4768
+ ...(fullPage === null ? {} : { fullPage: fullPage === "true" }),
4769
+ ...(format === null ? {} : { format }),
4770
+ ...(parsedQuality === null ? {} : { quality: parsedQuality }),
4771
+ };
4772
+ }
4773
+
4774
+ /** Preserve the controller's definite tab diagnosis (a tab that stopped
4775
+ * answering, or one whose renderer crashed) through the public API with its
4776
+ * own code and guidance. Any other controller failure keeps the ordinary
4777
+ * envelope, which never publishes driver diagnostics. This only handles typed
4778
+ * read/pre-dispatch failures. Dispatched action uncertainty stays in the
4779
+ * action receipt, never relabeled as a safe retry. */
4780
+ export function browserTabDiagnosisError(error: unknown): unknown {
4781
+ if (!(error instanceof BrowserControlRequestError)) return error;
4782
+ const code = error.error.code;
4783
+ if (code !== "tab_not_responding" && code !== "tab_crashed") return error;
4784
+ const failure = new ApiHttpError(409, {
4785
+ code,
4786
+ message: error.error.message,
4787
+ retryable: false,
4788
+ outcomeUnknown: false,
4789
+ });
4790
+ failure.cause = error;
4791
+ return failure;
4792
+ }
4793
+
4794
+ /** Keep a definite read timeout useful without publishing driver diagnostics. */
4795
+ export function browserScreenshotError(error: unknown): unknown {
4796
+ if (error instanceof BrowserControlRequestError && error.error.code === "timeout") {
4797
+ const failure = new ApiHttpError(504, {
4798
+ code: "upstream_unavailable",
4799
+ message:
4800
+ "This tab did not produce a screenshot in time. Other browser operations may still work.",
4801
+ retryable: error.retryable,
4802
+ outcomeUnknown: false,
4803
+ });
4804
+ failure.cause = error;
4805
+ return failure;
4806
+ }
4807
+ return error;
4808
+ }
4809
+
4810
+ export function browserScreenshotResponse(
4811
+ frame: Pick<BrowserControlFrame, "data" | "mediaType" | "metadataHeader">,
4812
+ ): Response {
4813
+ // Node Buffers can be views into a larger backing allocation. Copy exactly
4814
+ // the image bytes before constructing the Response.
4815
+ return new Response(Uint8Array.from(frame.data).buffer, {
4816
+ status: 200,
4817
+ headers: {
4818
+ "cache-control": "no-store",
4819
+ "content-type": frame.mediaType,
4820
+ "x-opengeni-browser-frame": frame.metadataHeader,
4821
+ ...USER_CONTENT_SECURITY_HEADERS,
4822
+ },
4823
+ });
4824
+ }
4825
+
4163
4826
  function isUuid(value: unknown): value is string {
4164
4827
  return (
4165
4828
  typeof value === "string" &&
@@ -4214,13 +4877,9 @@ function isTerminalOperation(state: string): boolean {
4214
4877
  return state === "completed" || state === "failed" || state === "outcome_unknown";
4215
4878
  }
4216
4879
 
4217
- async function loadBoundBrowserCredential(
4218
- deps: ApiRouteDeps,
4219
- grant: AccessGrant,
4220
- workspaceId: string,
4221
- authority: Extract<SiteAuthAuthority, { kind: "connection_fields" }>,
4222
- ) {
4223
- return await loadExactInteractionCredential(deps, grant, workspaceId, authority.credential);
4880
+ /** A configured browser credential (proxy or provider authority) is unusable. */
4881
+ class BrowserAuthCredentialError extends Error {
4882
+ override readonly name = "BrowserAuthCredentialError";
4224
4883
  }
4225
4884
 
4226
4885
  async function loadExactInteractionCredential(
@@ -4478,61 +5137,6 @@ function browserProxyUrl(
4478
5137
  return url.toString();
4479
5138
  }
4480
5139
 
4481
- async function settleProtectedAuthReceipt(input: {
4482
- deps: ApiRouteDeps;
4483
- scope: {
4484
- accountId: string;
4485
- workspaceId: string;
4486
- actorSubjectId: string;
4487
- authRunId: string;
4488
- };
4489
- receipt: BrowserProtectedAuthFillReceiptValue;
4490
- }) {
4491
- const { deps, scope, receipt } = input;
4492
- if (receipt.state === "completed") {
4493
- if (!receipt.observation) {
4494
- throw new BrowserControlProtocolError("protected-fill receipt omitted its observation");
4495
- }
4496
- return await completeProtectedAuthFill(deps.db, {
4497
- ...scope,
4498
- operationId: receipt.operationId,
4499
- status: receipt.observation.status,
4500
- targetGeneration: receipt.observation.target.targetGeneration,
4501
- documentGeneration: receipt.observation.target.documentGeneration,
4502
- });
4503
- }
4504
- if (receipt.state === "failed") {
4505
- if (!receipt.error) {
4506
- throw new BrowserControlProtocolError("protected-fill failure omitted its error");
4507
- }
4508
- const staleCodes = new Set([
4509
- "controller_stale",
4510
- "target_not_found",
4511
- "target_stale",
4512
- "observation_stale",
4513
- "document_stale",
4514
- "frame_stale",
4515
- "locator_not_found",
4516
- "locator_ambiguous",
4517
- ]);
4518
- return await completeProtectedAuthFill(deps.db, {
4519
- ...scope,
4520
- operationId: receipt.operationId,
4521
- status: staleCodes.has(receipt.error.code) ? "stale" : "failed",
4522
- failureCode: receipt.error.code,
4523
- });
4524
- }
4525
- if (receipt.state === "outcome_unknown") {
4526
- await markProtectedAuthFillOutcomeUnknown(deps.db, {
4527
- ...scope,
4528
- operationId: receipt.operationId,
4529
- errorCode: receipt.error?.code ?? "outcome_unknown",
4530
- });
4531
- throw new InteractionResourceStateError("Protected-fill outcome is unknown");
4532
- }
4533
- throw new InteractionResourceStateError(`Protected-fill operation is still ${receipt.state}`);
4534
- }
4535
-
4536
5140
  function assertAuthRunBrowser(actual: string, expected: string): void {
4537
5141
  if (actual !== expected) {
4538
5142
  throw new InteractionResourceConflictError("Auth run belongs to another browser session");
@@ -4712,10 +5316,30 @@ async function recordBrowserDownloadFileUsage(
4712
5316
  });
4713
5317
  }
4714
5318
 
5319
+ /** A user's own attached browser could not be reached for an operation. */
5320
+ function recordAttachedBrowserUnavailable(
5321
+ observability: ApiRouteDeps["observability"],
5322
+ reason: "disconnected" | "machine_unavailable" | "no_workspace_root",
5323
+ ): void {
5324
+ try {
5325
+ observability?.incrementCounter({
5326
+ name: "opengeni_attached_browser_unavailable_total",
5327
+ help: "Browser operations refused because the user's attached browser (or its machine) was unreachable, by closed reason.",
5328
+ labels: { reason },
5329
+ });
5330
+ } catch {
5331
+ // Telemetry never changes the refusal.
5332
+ }
5333
+ }
5334
+
4715
5335
  function browserRouteError(error: unknown): HTTPException {
5336
+ const tabDiagnosis = browserTabDiagnosisError(error);
5337
+ if (tabDiagnosis instanceof ApiHttpError) return tabDiagnosis;
4716
5338
  const connectedMachineError = interactionControlApiError(error, "browser");
4717
5339
  if (connectedMachineError) return connectedMachineError;
4718
5340
  if (error instanceof HTTPException) return error;
5341
+ if (error instanceof SandboxPaidComputeAdmissionError)
5342
+ return new HTTPException(402, { message: error.message, cause: error });
4719
5343
  if (error instanceof BrowserSessionNotFoundError) {
4720
5344
  return new HTTPException(404, { message: error.message, cause: error });
4721
5345
  }