@opengeni/api-router 0.30.3 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (434) hide show
  1. package/dist/access-grant-rls.d.ts +3 -0
  2. package/dist/admission-trace.d.ts +8 -0
  3. package/dist/api-websocket.d.ts +18 -0
  4. package/dist/app.d.ts +50 -2
  5. package/dist/app.js +12 -1
  6. package/dist/artifact-catalog-cursor.d.ts +27 -0
  7. package/dist/auth/managed-auth-attempt-context.d.ts +10 -0
  8. package/dist/auth/managed-auth-rate-limits.d.ts +167 -0
  9. package/dist/auth/managed-auth-session-adapter.d.ts +6 -0
  10. package/dist/auth/managed-auth.d.ts +102 -3
  11. package/dist/auth/managed-email.d.ts +29 -0
  12. package/dist/auth/managed-sign-in-notifications.d.ts +4 -0
  13. package/dist/auth/new-signups-gate.d.ts +23 -0
  14. package/dist/auth/organization-user-setup.d.ts +80 -0
  15. package/dist/auth/signup-funnel-metrics.d.ts +44 -0
  16. package/dist/browser-controller-authority.d.ts +1 -61
  17. package/dist/browser-ephemeral.d.ts +5 -0
  18. package/dist/browser-state-authority.d.ts +1 -27
  19. package/dist/chunk-6TZ5LG3O.js +123948 -0
  20. package/dist/chunk-6TZ5LG3O.js.map +1 -0
  21. package/dist/chunk-OGQDXVXY.js +18 -0
  22. package/dist/chunk-OGQDXVXY.js.map +1 -0
  23. package/dist/claude-subscription-account-conflicts.d.ts +1 -0
  24. package/dist/claude-subscription-account-usage.d.ts +39 -0
  25. package/dist/claude-subscription-oauth.d.ts +25 -0
  26. package/dist/claude-subscription-profile.d.ts +9 -0
  27. package/dist/claude-subscription-usage.d.ts +44 -0
  28. package/dist/claude-workspace-connection.d.ts +15 -0
  29. package/dist/codemode.d.ts +32 -1
  30. package/dist/company-brain-okf.d.ts +24 -0
  31. package/dist/connected-machine-computer-access.d.ts +11 -0
  32. package/dist/connection-ownership.d.ts +54 -0
  33. package/dist/controller-data-plane.d.ts +32 -0
  34. package/dist/editable-artifact-source-session.d.ts +3 -0
  35. package/dist/editable-artifact-websocket.d.ts +10 -18
  36. package/dist/editable-artifact-workspace-files.d.ts +1 -1
  37. package/dist/fatal-process-boundary.d.ts +25 -0
  38. package/dist/github-access.d.ts +31 -2
  39. package/dist/http/acting-person.d.ts +24 -0
  40. package/dist/http/api-contract-compat.d.ts +19 -0
  41. package/dist/http/api-error.d.ts +27 -1
  42. package/dist/http/cors.d.ts +17 -0
  43. package/dist/http/deprecation.d.ts +42 -0
  44. package/dist/http/interaction-control-error.d.ts +13 -0
  45. package/dist/http/keyed-admission.d.ts +12 -0
  46. package/dist/http/local-browser-boundary.d.ts +72 -0
  47. package/dist/http/metrics-listener.d.ts +22 -0
  48. package/dist/http/path-identifier.d.ts +17 -0
  49. package/dist/http/registered-route-label.d.ts +16 -0
  50. package/dist/http/request-body.d.ts +27 -0
  51. package/dist/http/request-source.d.ts +52 -0
  52. package/dist/http/sse.d.ts +17 -6
  53. package/dist/http/unmatched-route.d.ts +18 -0
  54. package/dist/http/user-content.d.ts +36 -0
  55. package/dist/http/websocket-upgrade-dispatch.d.ts +18 -0
  56. package/dist/index.d.ts +10 -3
  57. package/dist/index.js +1091 -162
  58. package/dist/index.js.map +1 -1
  59. package/dist/integration-endpoint-test.d.ts +20 -0
  60. package/dist/integrations/api-integrations.d.ts +2 -2
  61. package/dist/integrations/atlassian.d.ts +29 -3
  62. package/dist/integrations/connect-authority.d.ts +1 -0
  63. package/dist/integrations/connect-callback-return.d.ts +6 -0
  64. package/dist/integrations/fiken.d.ts +16 -2
  65. package/dist/integrations/github-app-connect.d.ts +47 -0
  66. package/dist/integrations/github-installation-proof.d.ts +3 -0
  67. package/dist/integrations/github-lens-connect.d.ts +116 -0
  68. package/dist/integrations/github-skill-source.d.ts +1 -5
  69. package/dist/integrations/google-drive.d.ts +24 -0
  70. package/dist/integrations/integration-commit-authority.d.ts +13 -0
  71. package/dist/integrations/marketplace-plugin.d.ts +2691 -0
  72. package/dist/integrations/oauth-client.d.ts +178 -18
  73. package/dist/integrations/oauth-profiles.d.ts +222 -0
  74. package/dist/integrations/oauth-return-path.d.ts +1 -0
  75. package/dist/integrations/personal-github-repositories.d.ts +84 -0
  76. package/dist/integrations/personal-github.d.ts +42 -0
  77. package/dist/integrations/pr-review-provider.d.ts +22 -0
  78. package/dist/integrations/provider-oauth.d.ts +19 -1
  79. package/dist/integrations/slack-app-home.d.ts +24 -0
  80. package/dist/integrations/slack-bot.d.ts +367 -12
  81. package/dist/integrations/slack-file-upload-flow.d.ts +62 -0
  82. package/dist/integrations/slack-install.d.ts +20 -0
  83. package/dist/integrations/slack-interactions.d.ts +107 -9
  84. package/dist/integrations/slack-mrkdwn.d.ts +41 -0
  85. package/dist/integrations/slack-routing.d.ts +120 -0
  86. package/dist/integrations/slack-session-defaults.d.ts +58 -0
  87. package/dist/integrations/slack-task-file-upload.d.ts +36 -0
  88. package/dist/integrations/social-oauth.d.ts +7 -0
  89. package/dist/interaction-agent-access.d.ts +15 -0
  90. package/dist/interaction-frame-proxy.d.ts +68 -0
  91. package/dist/interaction-metrics.d.ts +2 -0
  92. package/dist/mcp/action-catalog-types.d.ts +9 -0
  93. package/dist/mcp/action-catalog.gen.d.ts +2 -0
  94. package/dist/mcp/capability-account-readiness.d.ts +7 -0
  95. package/dist/mcp/child-read-evidence.d.ts +9 -0
  96. package/dist/mcp/command-read.d.ts +24 -0
  97. package/dist/mcp/company-profile-agent-admin.d.ts +44 -0
  98. package/dist/mcp/contract-input.d.ts +11 -0
  99. package/dist/mcp/documents.d.ts +7 -5
  100. package/dist/mcp/files.d.ts +2 -2
  101. package/dist/mcp/first-party-tool-permissions.d.ts +650 -0
  102. package/dist/mcp/knowledge-entries.d.ts +5 -0
  103. package/dist/mcp/orchestration-failure-diagnostic.d.ts +43 -0
  104. package/dist/mcp/receipts.d.ts +9 -0
  105. package/dist/mcp/request-abort.d.ts +19 -0
  106. package/dist/mcp/scheduled-task-input.d.ts +1189 -0
  107. package/dist/mcp/scheduled-task-view.d.ts +22 -4
  108. package/dist/mcp/server.d.ts +255 -3
  109. package/dist/mcp/session-event-view.d.ts +94 -0
  110. package/dist/mcp/session-view.d.ts +66 -2
  111. package/dist/mcp/session-wait.d.ts +174 -0
  112. package/dist/mcp-oauth.d.ts +52 -0
  113. package/dist/model-catalog.d.ts +11 -31
  114. package/dist/observability.d.ts +3 -1
  115. package/dist/organization-mcp.d.ts +52 -0
  116. package/dist/organization-recovery-notifications.d.ts +41 -0
  117. package/dist/routes/analytics-consent.d.ts +26 -0
  118. package/dist/routes/api-integrations.d.ts +25 -0
  119. package/dist/routes/api-keys.d.ts +17 -0
  120. package/dist/routes/artifact-catalog.d.ts +6 -0
  121. package/dist/routes/automations.d.ts +19 -0
  122. package/dist/routes/billing.d.ts +49 -0
  123. package/dist/routes/browser-sessions.d.ts +59 -2
  124. package/dist/routes/claude-subscription-accounts.d.ts +3 -0
  125. package/dist/routes/claude-subscription-oauth.d.ts +3 -0
  126. package/dist/routes/client-errors.d.ts +146 -0
  127. package/dist/routes/codex.d.ts +76 -2
  128. package/dist/routes/company-brain.d.ts +3 -0
  129. package/dist/routes/company-profile.d.ts +2 -1
  130. package/dist/routes/computer-sessions.d.ts +14 -0
  131. package/dist/routes/connect.d.ts +5 -0
  132. package/dist/routes/editable-artifacts.d.ts +2 -0
  133. package/dist/routes/external-identity-links.d.ts +6 -0
  134. package/dist/routes/feedback.d.ts +3 -0
  135. package/dist/routes/github-browser-pages.d.ts +12 -0
  136. package/dist/routes/github.d.ts +2 -0
  137. package/dist/routes/insights-response-cache.d.ts +10 -0
  138. package/dist/routes/insights-usage.d.ts +12 -0
  139. package/dist/routes/insights.d.ts +30 -1
  140. package/dist/routes/knowledge.d.ts +3 -0
  141. package/dist/routes/managed-auth-session-sets.d.ts +26 -0
  142. package/dist/routes/managed-onboarding.d.ts +29 -0
  143. package/dist/routes/managed-sign-in-methods.d.ts +6 -0
  144. package/dist/routes/model-connection-access.d.ts +3 -0
  145. package/dist/routes/organization-integration-policy.d.ts +3 -0
  146. package/dist/routes/organization-integrations.d.ts +6 -0
  147. package/dist/routes/organization-mcp-connections.d.ts +3 -0
  148. package/dist/routes/organization-model-providers.d.ts +3 -0
  149. package/dist/routes/organization-recovery.d.ts +19 -0
  150. package/dist/routes/organization-service-accounts.d.ts +3 -0
  151. package/dist/routes/organization-sessions.d.ts +26 -0
  152. package/dist/routes/personal-github-git-broker.d.ts +29 -0
  153. package/dist/routes/personal-github.d.ts +3 -0
  154. package/dist/routes/pr-review-github.d.ts +3 -0
  155. package/dist/routes/pr-review.d.ts +3 -0
  156. package/dist/routes/session-artifact-associations.d.ts +4 -0
  157. package/dist/routes/session-history-imports.d.ts +9 -0
  158. package/dist/routes/sessions.d.ts +44 -6
  159. package/dist/routes/skill-content.d.ts +4 -0
  160. package/dist/routes/skill-install-authority.d.ts +6 -0
  161. package/dist/routes/subscription-account-pools.d.ts +34 -0
  162. package/dist/routes/subscription-pool-access.d.ts +16 -0
  163. package/dist/routes/supergrok.d.ts +6 -1
  164. package/dist/routes/usage-allowances.d.ts +12 -0
  165. package/dist/routes/user-resource-authorities.d.ts +3 -0
  166. package/dist/routes/workspace-artifacts.d.ts +2 -1
  167. package/dist/routes/workspace-capture.d.ts +10 -2
  168. package/dist/routes/workspace-integrations.d.ts +35 -0
  169. package/dist/routes/workspace-learning.d.ts +3 -0
  170. package/dist/routes/workspace-model-providers.d.ts +4 -0
  171. package/dist/routes/workspaces.d.ts +29 -0
  172. package/dist/sandbox/auth-callout.d.ts +8 -0
  173. package/dist/sandbox/channel-a.d.ts +19 -1
  174. package/dist/sandbox/connection-authority.d.ts +3 -0
  175. package/dist/sandbox/enrollment.d.ts +9 -1
  176. package/dist/sandbox/machines.d.ts +5 -2
  177. package/dist/sandbox/metrics-ingestion.d.ts +28 -20
  178. package/dist/sandbox/rematerialize.d.ts +2 -0
  179. package/dist/sandbox/session-attach-variable-sets.d.ts +30 -0
  180. package/dist/sandbox/viewer.d.ts +26 -1
  181. package/dist/sandbox-file-artifacts.d.ts +50 -0
  182. package/dist/scheduled-task-deletion.d.ts +13 -0
  183. package/dist/session-create-observability.d.ts +5 -0
  184. package/dist/session-event-view-MPS2OZD3.js +292 -0
  185. package/dist/session-event-view-MPS2OZD3.js.map +1 -0
  186. package/dist/site-session-origin.d.ts +13 -0
  187. package/dist/site-uploads.d.ts +41 -0
  188. package/dist/slack-reaction-files.d.ts +1 -1
  189. package/dist/temporal-schedule-cleanup.d.ts +1 -0
  190. package/dist/temporal-schedule-sync.d.ts +26 -0
  191. package/dist/transcription/providers/xai-subscription.d.ts +1 -0
  192. package/dist/transcription/service.d.ts +2 -1
  193. package/dist/work-discovery-observability.d.ts +33 -0
  194. package/dist/workspace-artifact-content.d.ts +28 -0
  195. package/dist/workspace-artifact-provenance.d.ts +7 -0
  196. package/dist/workspace-delete-observability.d.ts +10 -0
  197. package/dist/workspace-deletion.d.ts +6 -0
  198. package/dist/workspace-state-projection.d.ts +3 -5
  199. package/dist/workspace-tool-gateway-observability.d.ts +17 -0
  200. package/dist/workspace-tool-gateway.d.ts +123 -0
  201. package/dist/workspace-webhook-dispatch.d.ts +26 -0
  202. package/package.json +23 -19
  203. package/src/access-grant-rls.ts +75 -0
  204. package/src/admission-trace.ts +14 -0
  205. package/src/api-websocket.ts +23 -0
  206. package/src/app.ts +1967 -252
  207. package/src/artifact-catalog-cursor.ts +60 -0
  208. package/src/auth/managed-auth-attempt-context.ts +70 -0
  209. package/src/auth/managed-auth-rate-limits.ts +252 -0
  210. package/src/auth/managed-auth-session-adapter.ts +222 -0
  211. package/src/auth/managed-auth.ts +536 -60
  212. package/src/auth/managed-email.ts +174 -0
  213. package/src/auth/managed-sign-in-notifications.ts +81 -0
  214. package/src/auth/new-signups-gate.ts +49 -0
  215. package/src/auth/organization-user-setup.ts +307 -0
  216. package/src/auth/signup-funnel-metrics.ts +164 -0
  217. package/src/browser-controller-authority.ts +1 -186
  218. package/src/browser-ephemeral.ts +39 -0
  219. package/src/browser-state-authority.ts +1 -236
  220. package/src/claude-subscription-account-conflicts.ts +12 -0
  221. package/src/claude-subscription-account-usage.ts +48 -0
  222. package/src/claude-subscription-oauth.ts +362 -0
  223. package/src/claude-subscription-profile.ts +73 -0
  224. package/src/claude-subscription-usage.ts +97 -0
  225. package/src/claude-workspace-connection.ts +82 -0
  226. package/src/codemode.ts +194 -30
  227. package/src/codex-realtime.ts +8 -2
  228. package/src/company-brain-okf.ts +340 -0
  229. package/src/connected-machine-computer-access.ts +33 -0
  230. package/src/connection-ownership.ts +199 -0
  231. package/src/controller-data-plane.ts +81 -0
  232. package/src/editable-artifact-native-kernel.ts +26 -15
  233. package/src/editable-artifact-office-import.ts +28 -3
  234. package/src/editable-artifact-production.ts +23 -7
  235. package/src/editable-artifact-source-session.ts +123 -0
  236. package/src/editable-artifact-websocket.ts +27 -21
  237. package/src/editable-artifact-workspace-files.ts +31 -6
  238. package/src/fatal-process-boundary.ts +246 -0
  239. package/src/gateway-realtime.ts +1 -1
  240. package/src/github-access.ts +147 -5
  241. package/src/http/acting-person.ts +71 -0
  242. package/src/http/api-contract-compat.ts +49 -0
  243. package/src/http/api-error.ts +115 -1
  244. package/src/http/auth.ts +48 -0
  245. package/src/http/cors.ts +67 -0
  246. package/src/http/deprecation.ts +118 -0
  247. package/src/http/interaction-control-error.ts +164 -0
  248. package/src/http/keyed-admission.ts +29 -0
  249. package/src/http/local-browser-boundary.ts +343 -0
  250. package/src/http/metrics-listener.ts +57 -0
  251. package/src/http/path-identifier.ts +54 -0
  252. package/src/http/registered-route-label.ts +34 -0
  253. package/src/http/request-body.ts +127 -0
  254. package/src/http/request-source.ts +178 -0
  255. package/src/http/sse.ts +486 -132
  256. package/src/http/unmatched-route.ts +60 -0
  257. package/src/http/user-content.ts +158 -0
  258. package/src/http/websocket-upgrade-dispatch.ts +29 -0
  259. package/src/index.ts +157 -52
  260. package/src/integration-endpoint-test.ts +291 -0
  261. package/src/integrations/api-integrations.ts +98 -11
  262. package/src/integrations/atlassian.ts +474 -136
  263. package/src/integrations/connect-authority.ts +2 -0
  264. package/src/integrations/connect-callback-return.ts +86 -0
  265. package/src/integrations/fiken.ts +354 -64
  266. package/src/integrations/github-app-connect.ts +435 -0
  267. package/src/integrations/github-installation-proof.ts +60 -0
  268. package/src/integrations/github-lens-connect.ts +76 -0
  269. package/src/integrations/github-skill-source.ts +2 -142
  270. package/src/integrations/google-drive.ts +717 -225
  271. package/src/integrations/integration-commit-authority.ts +69 -0
  272. package/src/integrations/marketplace-plugin.ts +48 -0
  273. package/src/integrations/oauth-client.ts +1356 -459
  274. package/src/integrations/oauth-profiles.ts +537 -0
  275. package/src/integrations/oauth-return-path.ts +18 -0
  276. package/src/integrations/personal-github-repositories.ts +674 -0
  277. package/src/integrations/personal-github.ts +825 -0
  278. package/src/integrations/pr-review-provider.ts +246 -0
  279. package/src/integrations/provider-oauth.ts +250 -22
  280. package/src/integrations/slack-app-home.ts +300 -0
  281. package/src/integrations/slack-bot.ts +963 -239
  282. package/src/integrations/slack-file-upload-flow.ts +147 -0
  283. package/src/integrations/slack-install.ts +83 -0
  284. package/src/integrations/slack-interactions.ts +3167 -474
  285. package/src/integrations/slack-mrkdwn.ts +193 -0
  286. package/src/integrations/slack-routing.ts +337 -0
  287. package/src/integrations/slack-session-defaults.ts +224 -0
  288. package/src/integrations/slack-task-file-upload.ts +323 -0
  289. package/src/integrations/social-oauth.ts +213 -15
  290. package/src/interaction-agent-access.ts +48 -0
  291. package/src/interaction-frame-proxy.ts +409 -0
  292. package/src/interaction-metrics.ts +30 -0
  293. package/src/mcp/action-catalog-types.ts +9 -0
  294. package/src/mcp/action-catalog.gen.ts +6696 -0
  295. package/src/mcp/capability-account-readiness.ts +17 -0
  296. package/src/mcp/child-read-evidence.ts +19 -0
  297. package/src/mcp/command-read.ts +109 -0
  298. package/src/mcp/company-profile-agent-admin.ts +198 -0
  299. package/src/mcp/contract-input.ts +141 -0
  300. package/src/mcp/documents.ts +65 -377
  301. package/src/mcp/editable-artifacts.ts +12 -6
  302. package/src/mcp/files.ts +80 -43
  303. package/src/mcp/first-party-tool-permissions.ts +305 -0
  304. package/src/mcp/knowledge-entries.ts +322 -0
  305. package/src/mcp/orchestration-failure-diagnostic.ts +43 -0
  306. package/src/mcp/receipts.ts +34 -0
  307. package/src/mcp/request-abort.ts +44 -0
  308. package/src/mcp/scheduled-task-input.ts +76 -0
  309. package/src/mcp/scheduled-task-view.ts +44 -3
  310. package/src/mcp/server.ts +3030 -1267
  311. package/src/mcp/session-event-view.ts +383 -0
  312. package/src/mcp/session-view.ts +84 -3
  313. package/src/mcp/session-wait.ts +665 -0
  314. package/src/mcp-oauth.ts +1123 -0
  315. package/src/model-catalog.ts +91 -333
  316. package/src/observability.ts +15 -1
  317. package/src/organization-mcp.ts +397 -0
  318. package/src/organization-recovery-notifications.ts +103 -0
  319. package/src/routes/analytics-consent.ts +123 -0
  320. package/src/routes/api-integrations.ts +364 -74
  321. package/src/routes/api-keys.ts +336 -10
  322. package/src/routes/artifact-catalog.ts +279 -0
  323. package/src/routes/automations.ts +768 -0
  324. package/src/routes/billing.ts +461 -46
  325. package/src/routes/browser-sessions.ts +780 -112
  326. package/src/routes/canonical-human-identities.ts +90 -14
  327. package/src/routes/capabilities.ts +107 -6
  328. package/src/routes/channels.ts +20 -3
  329. package/src/routes/claude-subscription-accounts.ts +334 -0
  330. package/src/routes/claude-subscription-oauth.ts +142 -0
  331. package/src/routes/client-errors.ts +362 -0
  332. package/src/routes/codex.ts +833 -116
  333. package/src/routes/company-brain.ts +322 -0
  334. package/src/routes/company-profile.ts +65 -1
  335. package/src/routes/computer-sessions.ts +557 -106
  336. package/src/routes/connect.ts +1938 -0
  337. package/src/routes/connections.ts +1039 -349
  338. package/src/routes/documents.ts +488 -287
  339. package/src/routes/editable-artifacts.ts +79 -9
  340. package/src/routes/enrollments.ts +135 -39
  341. package/src/routes/environments.ts +154 -58
  342. package/src/routes/external-identity-links.ts +201 -0
  343. package/src/routes/feedback.ts +113 -0
  344. package/src/routes/files.ts +267 -33
  345. package/src/routes/github-browser-pages.ts +162 -0
  346. package/src/routes/github.ts +446 -144
  347. package/src/routes/insights-response-cache.ts +83 -0
  348. package/src/routes/insights-usage.ts +345 -0
  349. package/src/routes/insights.ts +153 -19
  350. package/src/routes/install.ts +133 -38
  351. package/src/routes/integration-facets.ts +107 -33
  352. package/src/routes/interaction-resources.ts +30 -3
  353. package/src/routes/knowledge.ts +488 -0
  354. package/src/routes/machines.ts +313 -15
  355. package/src/routes/managed-auth-session-sets.ts +1218 -0
  356. package/src/routes/managed-onboarding.ts +337 -0
  357. package/src/routes/managed-sign-in-methods.ts +433 -0
  358. package/src/routes/memory-slack-publications.ts +3 -2
  359. package/src/routes/model-connection-access.ts +208 -0
  360. package/src/routes/organization-integration-policy.ts +116 -0
  361. package/src/routes/organization-integrations.ts +326 -0
  362. package/src/routes/organization-mcp-connections.ts +178 -0
  363. package/src/routes/organization-memberships.ts +1019 -27
  364. package/src/routes/organization-model-providers.ts +293 -0
  365. package/src/routes/organization-recovery.ts +325 -0
  366. package/src/routes/organization-service-accounts.ts +128 -0
  367. package/src/routes/organization-sessions.ts +350 -0
  368. package/src/routes/personal-github-git-broker.ts +785 -0
  369. package/src/routes/personal-github.ts +370 -0
  370. package/src/routes/plugins.ts +251 -22
  371. package/src/routes/pr-review-github.ts +881 -0
  372. package/src/routes/pr-review.ts +622 -0
  373. package/src/routes/preference-registry.ts +12 -93
  374. package/src/routes/rigs.ts +167 -50
  375. package/src/routes/scheduled-tasks.ts +346 -41
  376. package/src/routes/session-artifact-associations.ts +87 -0
  377. package/src/routes/session-history-imports.ts +280 -0
  378. package/src/routes/sessions.ts +2306 -264
  379. package/src/routes/skill-content.ts +297 -0
  380. package/src/routes/skill-install-authority.ts +64 -0
  381. package/src/routes/skills.ts +62 -9
  382. package/src/routes/social.ts +46 -18
  383. package/src/routes/subscription-account-pools.ts +436 -0
  384. package/src/routes/subscription-pool-access.ts +126 -0
  385. package/src/routes/supergrok.ts +219 -303
  386. package/src/routes/transcription-recordings.ts +38 -3
  387. package/src/routes/transcriptions.ts +4 -0
  388. package/src/routes/usage-allowances.ts +378 -0
  389. package/src/routes/user-resource-authorities.ts +138 -0
  390. package/src/routes/video-generation.ts +14 -10
  391. package/src/routes/workspace-artifacts.ts +275 -69
  392. package/src/routes/workspace-capture.ts +21 -0
  393. package/src/routes/workspace-integrations.ts +576 -0
  394. package/src/routes/workspace-learning.ts +159 -0
  395. package/src/routes/workspace-model-providers.ts +148 -0
  396. package/src/routes/workspace-state.ts +38 -25
  397. package/src/routes/workspaces.ts +1004 -127
  398. package/src/sandbox/auth-callout.ts +96 -19
  399. package/src/sandbox/channel-a.ts +323 -24
  400. package/src/sandbox/connection-authority.ts +3 -0
  401. package/src/sandbox/enrollment.ts +81 -6
  402. package/src/sandbox/machines.ts +218 -76
  403. package/src/sandbox/metrics-ingestion.ts +411 -62
  404. package/src/sandbox/rematerialize.ts +121 -19
  405. package/src/sandbox/session-attach-variable-sets.ts +131 -0
  406. package/src/sandbox/viewer.ts +280 -25
  407. package/src/sandbox-file-artifacts.ts +413 -0
  408. package/src/scheduled-task-deletion.ts +138 -0
  409. package/src/session-create-observability.ts +46 -0
  410. package/src/session-realtime-context.ts +6 -3
  411. package/src/site-session-origin.ts +48 -0
  412. package/src/site-uploads.ts +224 -0
  413. package/src/slack-reaction-files.ts +16 -5
  414. package/src/temporal-schedule-cleanup.ts +13 -0
  415. package/src/temporal-schedule-sync.ts +74 -0
  416. package/src/transcription/providers/codex-subscription.ts +2 -0
  417. package/src/transcription/providers/openai.ts +1 -0
  418. package/src/transcription/providers/xai-subscription.ts +49 -3
  419. package/src/transcription/segmenter.ts +8 -2
  420. package/src/transcription/service.ts +43 -6
  421. package/src/work-discovery-observability.ts +121 -0
  422. package/src/workspace-artifact-content.ts +144 -0
  423. package/src/workspace-artifact-provenance.ts +104 -0
  424. package/src/workspace-delete-observability.ts +75 -0
  425. package/src/workspace-deletion.ts +73 -0
  426. package/src/workspace-state-projection.ts +18 -148
  427. package/src/workspace-tool-gateway-observability.ts +100 -0
  428. package/src/workspace-tool-gateway.ts +870 -0
  429. package/src/workspace-webhook-dispatch.ts +190 -0
  430. package/src/xai-subscription-auth.ts +9 -2
  431. package/dist/chunk-WCXHI3FF.js +0 -49920
  432. package/dist/chunk-WCXHI3FF.js.map +0 -1
  433. package/dist/routes/packs.d.ts +0 -3
  434. package/src/routes/packs.ts +0 -729
@@ -1,15 +1,19 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { resolveFirstPartyDelegationSecret, resolveStreamTokenSecret } from "@opengeni/config";
2
+ import {
3
+ resolveFirstPartyDelegationSecret,
4
+ resolveStreamTokenSecret,
5
+ sandboxWarmRateMicrosPerSecond,
6
+ } from "@opengeni/config";
3
7
  import {
4
8
  BROWSER_CONTROL_WEBSOCKET_BEARER_PREFIX,
5
9
  BROWSER_CONTROL_PORT,
6
10
  COMPUTER_CONTROL_WEBSOCKET_PROTOCOL,
7
- COMPUTER_RFB_WEBSOCKET_PROTOCOL,
8
11
  ComputerActionCommand,
9
12
  ComputerActionRequest,
10
13
  ComputerSessionAttachment,
11
14
  ComputerSessionAttachmentRequest,
12
15
  ComputerSessionHeartbeatResponse,
16
+ ComputerSessionInputPosture,
13
17
  ComputerSessionLifecycleRequest,
14
18
  ComputerSessionListResponse,
15
19
  ComputerSessionMutationResponse,
@@ -25,18 +29,21 @@ import {
25
29
  type SessionAuthorizationOperation,
26
30
  } from "@opengeni/contracts";
27
31
  import {
32
+ getSessionAuthorityEpoch,
28
33
  acquireLease,
29
34
  activateComputerSession,
30
35
  completeComputerSessionEnd,
31
36
  ComputerSessionNotFoundError,
32
37
  ComputerSessionOperationConflictError,
33
38
  ComputerSessionStateError,
39
+ SandboxPaidComputeAdmissionError,
34
40
  dispatchComputerSessionOperation,
35
41
  failComputerSessionOperation,
36
42
  findComputerSessionControlRecordByOperation,
37
43
  getAttachedBrowserDevice,
38
44
  getComputerSessionControlRecord,
39
- getEnrollment,
45
+ getLiveEnrollmentConnection,
46
+ getSandbox,
40
47
  getSession,
41
48
  listComputerSessions,
42
49
  prepareComputerSessionCreate,
@@ -44,14 +51,17 @@ import {
44
51
  releaseLeaseHolder,
45
52
  readLease,
46
53
  recordLeaseControllerDataPlaneUrl,
54
+ terminalizeStaleConnectedInteractionPlacement,
47
55
  touchComputerSessionController,
48
56
  type ComputerSessionControlRecord,
49
57
  type LeaseSnapshot,
50
58
  } from "@opengeni/db";
51
59
  import {
60
+ hasPermission,
52
61
  requireAccessGrant,
53
62
  requireSessionAuthorization,
54
63
  relayConfigFromSettings,
64
+ resolveSessionSandboxRuntime,
55
65
  SessionAuthorizationDeniedError,
56
66
  SessionAuthorizationUnavailableError,
57
67
  type ApiRouteDeps,
@@ -64,6 +74,7 @@ import {
64
74
  BrowserControlServerUnsupportedError,
65
75
  BrowserControlTransportError,
66
76
  BrowserControlUnsupportedError,
77
+ ComputerFrameEvidenceMismatchError,
67
78
  buildSelfhostedBackendSession,
68
79
  buildStreamUrl,
69
80
  exposedPortEndpointFromUrl,
@@ -71,7 +82,11 @@ import {
71
82
  NatsControlRpc,
72
83
  NatsOpStreamTransport,
73
84
  provisionBrowserControlClient,
85
+ validateComputerControlFrameEvidence,
86
+ renewSandboxProviderExpiration,
74
87
  type BrowserControlPlacementSession,
88
+ type ComputerControlFrame,
89
+ type ExpectedComputerFrameEvidence,
75
90
  } from "@opengeni/runtime/sandbox";
76
91
  import type { Context, Hono } from "hono";
77
92
  import { HTTPException } from "hono/http-exception";
@@ -81,19 +96,109 @@ import {
81
96
  deriveComputerSessionControllerTokens,
82
97
  deriveComputerViewGrantToken,
83
98
  } from "../browser-controller-authority";
84
- import { withCachedController } from "../controller-data-plane";
99
+ import { connectedMachineComputerAccessError } from "../connected-machine-computer-access";
100
+ import {
101
+ controllerCacheAllowsHostFetch,
102
+ controllerCachedUrlIsUsable,
103
+ shouldPersistControllerDataPlaneUrl,
104
+ withCachedController,
105
+ } from "../controller-data-plane";
106
+ import { filterInteractionSessionsForGrant } from "../interaction-agent-access";
85
107
  import { withInteractionHolderHeartbeat } from "../interaction-holder-heartbeat";
86
- import { allowedCorsOrigin } from "../http/cors";
87
- import { observeComputerActionResult, observeLifecycleResult } from "../interaction-metrics";
108
+ import { validateInteractionRequestOrigin } from "../http/cors";
109
+ import { ApiHttpError } from "../http/api-error";
110
+ import { interactionControlApiError } from "../http/interaction-control-error";
111
+ import {
112
+ createInteractionFrameProxyAttachment,
113
+ placementUsesInteractionFrameProxy,
114
+ } from "../interaction-frame-proxy";
115
+ import {
116
+ observeComputerActionResult,
117
+ observeComputerFrameEvidenceMismatch,
118
+ observeLifecycleResult,
119
+ } from "../interaction-metrics";
88
120
  import { withChannelA, withChannelARead, type ChannelAOperation } from "../sandbox/channel-a";
121
+ import { USER_CONTENT_SECURITY_HEADERS } from "../http/user-content";
89
122
 
90
123
  type ComputerPlacement = {
91
124
  placement: InteractionPlacement;
92
125
  placementInstanceId: string;
93
126
  session: BrowserControlPlacementSession;
94
127
  lease: LeaseSnapshot | null;
128
+ /** Current placement consent, separate from passive viewing authority. */
129
+ screenControlAllowed?: boolean;
95
130
  };
96
131
 
132
+ const MODEL_COMPUTER_FRAME_MAX_BYTES = 256 * 1024;
133
+
134
+ type ComputerFrameCaptureClient = {
135
+ capture(
136
+ targetId: string,
137
+ options: {
138
+ format?: "jpeg" | "png";
139
+ quality?: number;
140
+ maxWidth?: number;
141
+ maxHeight?: number;
142
+ },
143
+ ): Promise<ComputerControlFrame>;
144
+ };
145
+
146
+ export function validateComputerFrameForApi(
147
+ frame: Pick<ComputerControlFrame, "data" | "mediaType" | "metadataHeader">,
148
+ expected: ExpectedComputerFrameEvidence,
149
+ ): ComputerControlFrame {
150
+ return validateComputerControlFrameEvidence(frame, expected);
151
+ }
152
+
153
+ export async function captureModelComputerFrame(
154
+ sessionClient: ComputerFrameCaptureClient,
155
+ expected: ExpectedComputerFrameEvidence,
156
+ ): Promise<ComputerControlFrame> {
157
+ let captured = validateComputerFrameForApi(
158
+ await sessionClient.capture(expected.targetId, {
159
+ format: "jpeg",
160
+ quality: 55,
161
+ maxWidth: 1_024,
162
+ maxHeight: 768,
163
+ }),
164
+ expected,
165
+ );
166
+ if (captured.data.byteLength > MODEL_COMPUTER_FRAME_MAX_BYTES) {
167
+ captured = validateComputerFrameForApi(
168
+ await sessionClient.capture(expected.targetId, {
169
+ format: "jpeg",
170
+ quality: 30,
171
+ maxWidth: 640,
172
+ maxHeight: 480,
173
+ }),
174
+ expected,
175
+ );
176
+ }
177
+ if (captured.data.byteLength > MODEL_COMPUTER_FRAME_MAX_BYTES) {
178
+ throw new BrowserControlProtocolError(
179
+ "computer screenshot could not honor the model image byte bound",
180
+ );
181
+ }
182
+ return captured;
183
+ }
184
+
185
+ /** One validated screen frame, served as sandboxed, non-embeddable user content. */
186
+ export function computerScreenshotResponse(
187
+ frame: Pick<ComputerControlFrame, "data" | "mediaType" | "metadataHeader">,
188
+ ): Response {
189
+ // Copy exactly the image bytes: a Node Buffer view may share a larger
190
+ // backing allocation, which `.slice().buffer` would expose whole.
191
+ return new Response(Uint8Array.from(frame.data).buffer, {
192
+ status: 200,
193
+ headers: {
194
+ "cache-control": "no-store",
195
+ "content-type": frame.mediaType,
196
+ "x-opengeni-computer-frame": frame.metadataHeader,
197
+ ...USER_CONTENT_SECURITY_HEADERS,
198
+ },
199
+ });
200
+ }
201
+
97
202
  /** Public ComputerSession resource surface. Physical app/window authority stays
98
203
  * in the same placement controller used by BrowserSession; this route owns only
99
204
  * durable authorization, placement fencing, lifecycle receipts, and routing. */
@@ -108,13 +213,15 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
108
213
  app.get("/v1/workspaces/:workspaceId/computer-sessions", async (context) => {
109
214
  const workspaceId = context.req.param("workspaceId") ?? "";
110
215
  const grant = await requireAccessGrant(context, deps, workspaceId, "sessions:read");
216
+ const listed = await listComputerSessions(deps.db, {
217
+ accountId: grant.accountId,
218
+ workspaceId,
219
+ });
111
220
  return context.json(
112
- ComputerSessionListResponse.parse(
113
- await listComputerSessions(deps.db, {
114
- accountId: grant.accountId,
115
- workspaceId,
116
- }),
117
- ),
221
+ ComputerSessionListResponse.parse({
222
+ ...listed,
223
+ sessions: await filterInteractionSessionsForGrant(deps, grant, listed.sessions),
224
+ }),
118
225
  );
119
226
  });
120
227
 
@@ -141,8 +248,9 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
141
248
  const request = await parseJsonBody(context, CreateComputerSessionRequest);
142
249
  const startedAtMs = performance.now();
143
250
  await authorizeSourceSession(deps, grant, request.sessionId, "session.control");
144
- const origin = requestOrigin(context, deps.settings.corsAllowOriginRegex);
251
+ const origin = requestOrigin(context, deps.settings);
145
252
  const authority = controllerAuthorityRoot(deps);
253
+ let prepared: Awaited<ReturnType<typeof prepareComputerSessionCreate>> | null = null;
146
254
 
147
255
  try {
148
256
  const existing = await findComputerSessionControlRecordByOperation(deps.db, {
@@ -156,7 +264,7 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
156
264
  );
157
265
  }
158
266
  if (existing) assertCreateReplay(request, existing.session);
159
- let prepared = existing
267
+ prepared = existing
160
268
  ? await prepareComputerSessionCreate(
161
269
  deps.db,
162
270
  computerCreateInput(grant, workspaceId, request, existing.session.placement),
@@ -264,26 +372,27 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
264
372
  () => placement,
265
373
  );
266
374
  } catch (error) {
267
- if (
375
+ const retryableRequestFailure =
376
+ error instanceof BrowserControlRequestError &&
377
+ error.retryable &&
378
+ error.error.code !== "machine_locked";
379
+ const outcomeUnknown =
380
+ error instanceof BrowserControlProtocolError ||
268
381
  error instanceof BrowserControlTransportError ||
269
- (error instanceof BrowserControlRequestError &&
270
- error.retryable &&
271
- error.error.code !== "machine_locked") ||
272
- isAbort(error)
273
- ) {
274
- throw error;
275
- }
382
+ isAbort(error);
383
+ const rethrowAfterFailure =
384
+ error instanceof BrowserControlTransportError ||
385
+ retryableRequestFailure ||
386
+ isAbort(error);
276
387
  const failed = await failComputerSessionOperation(deps.db, {
277
388
  accountId: grant.accountId,
278
389
  workspaceId,
279
390
  operationId: request.operationId,
280
391
  computerSessionId: preparedSession.id,
281
- ...(error instanceof BrowserControlProtocolError
282
- ? { state: "outcome_unknown" as const }
283
- : {}),
392
+ ...(outcomeUnknown ? { state: "outcome_unknown" as const } : {}),
284
393
  error: interactionFailure(error),
285
394
  });
286
- if (interactionHeld && !(error instanceof BrowserControlProtocolError)) {
395
+ if (interactionHeld && !outcomeUnknown) {
287
396
  await releaseInteractionHolder(
288
397
  grant,
289
398
  workspaceId,
@@ -291,6 +400,7 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
291
400
  placement.placement,
292
401
  ).catch(() => undefined);
293
402
  }
403
+ if (rethrowAfterFailure) throw error;
294
404
  return failed;
295
405
  }
296
406
  // These facts come from the physical adapter after its native helper
@@ -320,10 +430,49 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
320
430
  parsed.operation.state === "completed" && !parsed.operation.replayed ? 201 : 200,
321
431
  );
322
432
  } catch (error) {
433
+ if (prepared) {
434
+ // A concurrent dispatch owns its exact binding, even if this request's
435
+ // placement failed. Settle only the still-undispatched create receipt.
436
+ await failComputerSessionOperation(deps.db, {
437
+ accountId: grant.accountId,
438
+ workspaceId,
439
+ operationId: request.operationId,
440
+ computerSessionId: prepared.session.id,
441
+ onlyIfPreparedCreate: true,
442
+ error: interactionFailure(error),
443
+ }).catch(() => undefined);
444
+ }
323
445
  throw computerRouteError(error);
324
446
  }
325
447
  });
326
448
 
449
+ app.get(
450
+ "/v1/workspaces/:workspaceId/computer-sessions/:computerSessionId/input-posture",
451
+ async (context) => {
452
+ const { workspaceId, grant, computerSessionId } = await routePreamble(
453
+ context,
454
+ "sessions:read",
455
+ );
456
+ const result = await withActiveComputerController(
457
+ context,
458
+ grant,
459
+ workspaceId,
460
+ computerSessionId,
461
+ "session.read",
462
+ "computer.read",
463
+ async ({ record, binding, placement }) =>
464
+ ComputerSessionInputPosture.parse({
465
+ computerSessionId,
466
+ controllerGeneration: binding.controllerGeneration,
467
+ inputAllowed: await authorizeHumanFrameInput(grant, record, placement),
468
+ }),
469
+ false,
470
+ );
471
+ context.header("cache-control", "no-store");
472
+ return context.json(result);
473
+ },
474
+ );
475
+
327
476
  app.get(
328
477
  "/v1/workspaces/:workspaceId/computer-sessions/:computerSessionId/targets",
329
478
  async (context) => {
@@ -370,6 +519,40 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
370
519
  },
371
520
  );
372
521
 
522
+ app.get(
523
+ "/v1/workspaces/:workspaceId/computer-sessions/:computerSessionId/targets/:targetId/screenshot",
524
+ async (context) => {
525
+ const { workspaceId, grant, computerSessionId } = await routePreamble(
526
+ context,
527
+ "sessions:read",
528
+ );
529
+ const targetId = requireOpaqueParam(context, "targetId");
530
+ const frame = await withActiveComputerController(
531
+ context,
532
+ grant,
533
+ workspaceId,
534
+ computerSessionId,
535
+ "session.read",
536
+ "computer.read",
537
+ async ({ sessionClient, binding }) => {
538
+ try {
539
+ return await captureModelComputerFrame(sessionClient, {
540
+ computerSessionId,
541
+ controllerGeneration: binding.controllerGeneration,
542
+ targetId,
543
+ });
544
+ } catch (error) {
545
+ if (error instanceof ComputerFrameEvidenceMismatchError) {
546
+ observeComputerFrameEvidenceMismatch(deps.observability, error.reason);
547
+ }
548
+ throw error;
549
+ }
550
+ },
551
+ );
552
+ return computerScreenshotResponse(frame);
553
+ },
554
+ );
555
+
373
556
  app.get(
374
557
  "/v1/workspaces/:workspaceId/computer-sessions/:computerSessionId/clipboard",
375
558
  async (context) => {
@@ -406,8 +589,15 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
406
589
  computerSessionId,
407
590
  "session.control",
408
591
  "computer.action",
409
- async ({ sessionClient, binding }) =>
410
- await sessionClient.action(
592
+ async ({ sessionClient, binding, record }) => {
593
+ if (
594
+ record.session.placement.kind === "sandbox_group" &&
595
+ deps.settings.sandboxDesktopInteractive === false &&
596
+ grant.principalKind !== "agent_attempt"
597
+ ) {
598
+ throw new HTTPException(403, { message: "Human desktop input is disabled" });
599
+ }
600
+ return await sessionClient.action(
411
601
  ComputerActionCommand.parse({
412
602
  protocolVersion: INTERACTION_PROTOCOL_VERSION,
413
603
  operationId: request.operationId,
@@ -420,7 +610,8 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
420
610
  actor: interactionActorForGrant(grant),
421
611
  action: request.action,
422
612
  }),
423
- ),
613
+ );
614
+ },
424
615
  );
425
616
  observeComputerActionResult(deps.observability, startedAtMs, request, result);
426
617
  return context.json(result);
@@ -452,7 +643,7 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
452
643
  "/v1/workspaces/:workspaceId/computer-sessions/:computerSessionId/attachments",
453
644
  async (context) => {
454
645
  const { workspaceId, grant, computerSessionId } = await routePreamble(context, "stream:view");
455
- const origin = requestOrigin(context, deps.settings.corsAllowOriginRegex);
646
+ const origin = requestOrigin(context, deps.settings);
456
647
  const request = await parseJsonBody(context, ComputerSessionAttachmentRequest);
457
648
  const result = await withActiveComputerController(
458
649
  context,
@@ -463,25 +654,10 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
463
654
  "computer.attach",
464
655
  async ({ client, sessionClient, record, binding, placement }) => {
465
656
  if (origin) await client.addAllowedOrigins([origin]);
466
- const grantId = randomUUID();
467
- const expiresAt = new Date(Date.now() + request.expiresInSeconds * 1_000).toISOString();
468
- const token = deriveComputerViewGrantToken({
469
- rootSecret: controllerAuthorityRoot(deps),
470
- accountId: grant.accountId,
471
- workspaceId,
472
- placement: record.session.placement,
473
- placementInstanceId: placement.placementInstanceId,
474
- computerSessionId,
475
- controllerGeneration: binding.controllerGeneration,
476
- tokenGeneration: record.tokenGeneration,
477
- grantId,
478
- expiresAt,
479
- });
480
657
  const reference = {
481
658
  computerSessionId,
482
659
  controllerGeneration: binding.controllerGeneration,
483
660
  };
484
- await client.createComputerViewGrant(reference, { grantId, token, expiresAt });
485
661
  const target = (await sessionClient.listTargets()).find(
486
662
  (candidate) => candidate.id === request.targetId,
487
663
  );
@@ -492,6 +668,40 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
492
668
  retryable: false,
493
669
  });
494
670
  }
671
+ if (target.kind === "app") {
672
+ throw new BrowserControlRequestError(409, {
673
+ code: "unsupported",
674
+ message:
675
+ "application targets support semantic control but do not provide a visual stream; select a window or screen",
676
+ retryable: false,
677
+ });
678
+ }
679
+ const grantId = randomUUID();
680
+ const expiresAt = new Date(Date.now() + request.expiresInSeconds * 1_000).toISOString();
681
+ const token = deriveComputerViewGrantToken({
682
+ rootSecret: controllerAuthorityRoot(deps),
683
+ accountId: grant.accountId,
684
+ workspaceId,
685
+ placement: record.session.placement,
686
+ placementInstanceId: placement.placementInstanceId,
687
+ computerSessionId,
688
+ controllerGeneration: binding.controllerGeneration,
689
+ tokenGeneration: record.tokenGeneration,
690
+ grantId,
691
+ expiresAt,
692
+ });
693
+ // Keep the old strict-key request. Human input uses canonical
694
+ // actions; this grant authorizes only the frame producer. An older
695
+ // controller's reusable RFB bearer must stay behind the proxy.
696
+ const viewGrant = await client.createComputerViewGrant(reference, {
697
+ grantId,
698
+ token,
699
+ expiresAt,
700
+ });
701
+ const managedLinuxPlacement =
702
+ record.session.placement.kind === "sandbox_group" &&
703
+ record.session.platform === "linux";
704
+ const inputAllowed = await authorizeHumanFrameInput(grant, record, placement);
495
705
  const relaySecret = placement.session.openComputerFrames
496
706
  ? resolveStreamTokenSecret(deps.settings)
497
707
  : null;
@@ -510,15 +720,31 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
510
720
  ...(request.stream ? { stream: request.stream } : {}),
511
721
  });
512
722
  } catch (error) {
723
+ const publicFailure = interactionControlApiError(error, "computer");
513
724
  console.error("computer frame relay open failed", {
514
725
  computerSessionId,
515
726
  targetId: request.targetId,
516
- failure: error instanceof Error ? error.message : String(error),
727
+ failureCode: publicFailure?.details?.controlFailureCode ?? "unclassified",
728
+ retryable: publicFailure?.retryable ?? false,
729
+ outcomeUnknown: publicFailure?.outcomeUnknown ?? false,
730
+ controlRequestId: publicFailure?.details?.controlRequestId ?? null,
517
731
  });
518
732
  throw error;
519
733
  }
520
734
  const stream = relayed
521
735
  ? await (async () => {
736
+ // 0281: stamp the authenticated viewer subject and the live
737
+ // session authority epoch into the relay stream token.
738
+ const relayAuthorityEpoch = await getSessionAuthorityEpoch(deps.db, {
739
+ accountId: grant.accountId,
740
+ workspaceId,
741
+ sessionId: record.sourceSessionId,
742
+ });
743
+ if (relayAuthorityEpoch === null) {
744
+ throw new BrowserControlUnsupportedError(
745
+ "stream authority is unavailable for this session",
746
+ );
747
+ }
522
748
  const relayToken = await mintStreamToken(relaySecret!, {
523
749
  workspaceId,
524
750
  sessionId: record.sourceSessionId,
@@ -526,6 +752,8 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
526
752
  leaseEpoch: record.tokenGeneration,
527
753
  port: relayed.channel.port,
528
754
  ttlSeconds: request.expiresInSeconds,
755
+ subjectId: grant.subjectId,
756
+ authorityEpoch: relayAuthorityEpoch,
529
757
  });
530
758
  return {
531
759
  kind: "relay" as const,
@@ -540,34 +768,48 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
540
768
  },
541
769
  };
542
770
  })()
543
- : record.session.placement.kind === "sandbox_group" &&
544
- record.session.platform === "linux" &&
545
- target.kind === "screen"
546
- ? {
547
- kind: "direct_rfb" as const,
548
- url: await client.computerRfbStreamUrl(reference, request.targetId),
549
- protocols: [
550
- "binary",
551
- COMPUTER_RFB_WEBSOCKET_PROTOCOL,
552
- `${BROWSER_CONTROL_WEBSOCKET_BEARER_PREFIX}${token}`,
553
- ],
554
- }
555
- : {
556
- kind: "direct_websocket" as const,
557
- url: await client.computerFrameStreamUrl(
558
- reference,
559
- request.targetId,
560
- request.stream,
561
- ),
562
- protocols: [
563
- COMPUTER_CONTROL_WEBSOCKET_PROTOCOL,
564
- `${BROWSER_CONTROL_WEBSOCKET_BEARER_PREFIX}${token}`,
565
- ],
566
- };
771
+ : await (async () => {
772
+ const protocols = [
773
+ COMPUTER_CONTROL_WEBSOCKET_PROTOCOL,
774
+ `${BROWSER_CONTROL_WEBSOCKET_BEARER_PREFIX}${token}`,
775
+ ];
776
+ const upstreamUrl = await client.computerFrameStreamUrl(
777
+ reference,
778
+ request.targetId,
779
+ request.stream,
780
+ );
781
+ const attachment =
782
+ (!viewGrant.scopedRfbInput && managedLinuxPlacement) ||
783
+ placementUsesInteractionFrameProxy(placement.lease?.backend, {
784
+ openSandboxSignedEndpoints: deps.settings.openSandboxSignedEndpoints,
785
+ ...(typeof deps.settings.openSandboxInteractionFrameProxy === "boolean"
786
+ ? {
787
+ openSandboxInteractionFrameProxy:
788
+ deps.settings.openSandboxInteractionFrameProxy,
789
+ }
790
+ : {}),
791
+ })
792
+ ? createInteractionFrameProxyAttachment({
793
+ requestUrl: context.req.url,
794
+ publicBaseUrl: deps.settings.publicBaseUrl,
795
+ webBaseUrl: deps.settings.webBaseUrl,
796
+ forwardedProto: context.req.header("x-forwarded-proto"),
797
+ forwardedHost:
798
+ context.req.header("x-forwarded-host") ?? context.req.header("host"),
799
+ rootSecret: controllerAuthorityRoot(deps),
800
+ upstreamUrl,
801
+ upstreamProtocols: protocols,
802
+ origin,
803
+ expiresAt,
804
+ })
805
+ : { url: upstreamUrl, protocols };
806
+ return { kind: "direct_websocket" as const, ...attachment };
807
+ })();
567
808
  return ComputerSessionAttachment.parse({
568
809
  computerSessionId,
569
810
  controllerGeneration: binding.controllerGeneration,
570
811
  targetId: request.targetId,
812
+ inputAllowed,
571
813
  stream,
572
814
  expiresAt,
573
815
  });
@@ -577,6 +819,40 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
577
819
  },
578
820
  );
579
821
 
822
+ async function authorizeHumanFrameInput(
823
+ grant: AccessGrant,
824
+ record: ComputerSessionControlRecord,
825
+ placement: ComputerPlacement,
826
+ ): Promise<boolean> {
827
+ if (
828
+ ((record.session.placement.kind === "connected_machine" ||
829
+ record.session.placement.kind === "attached_device") &&
830
+ placement.screenControlAllowed !== true) ||
831
+ (record.session.placement.kind === "sandbox_group" &&
832
+ deps.settings.sandboxDesktopInteractive === false) ||
833
+ grant.principalKind === "agent_attempt" ||
834
+ !hasPermission(grant.permissions, "sessions:control")
835
+ )
836
+ return false;
837
+ try {
838
+ await requireSessionAuthorization(deps, grant, {
839
+ sessionId: record.sourceSessionId,
840
+ operation: "session.control",
841
+ surface: "http",
842
+ });
843
+ return true;
844
+ } catch (error) {
845
+ // Input availability cannot invalidate an independently authorized view.
846
+ // Every action reauthorizes the live source instead of trusting this hint.
847
+ if (
848
+ error instanceof SessionAuthorizationDeniedError ||
849
+ error instanceof SessionAuthorizationUnavailableError
850
+ )
851
+ return false;
852
+ throw error;
853
+ }
854
+ }
855
+
580
856
  app.post(
581
857
  "/v1/workspaces/:workspaceId/computer-sessions/:computerSessionId/heartbeat",
582
858
  async (context) => {
@@ -614,7 +890,7 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
614
890
  const computerSessionId = requireUuidParam(context, "computerSessionId");
615
891
  const request = await parseJsonBody(context, ComputerSessionLifecycleRequest);
616
892
  const startedAtMs = performance.now();
617
- const origin = requestOrigin(context, deps.settings.corsAllowOriginRegex);
893
+ const origin = requestOrigin(context, deps.settings);
618
894
  try {
619
895
  const before = await getComputerSessionControlRecord(deps.db, {
620
896
  accountId: grant.accountId,
@@ -765,23 +1041,42 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
765
1041
  if (device.state !== "connected") {
766
1042
  throw new ComputerSessionStateError("Attached browser machine is disconnected");
767
1043
  }
768
- const enrollment = await getEnrollment(
1044
+ const enrollment = await getLiveEnrollmentConnection(
769
1045
  deps.db,
770
1046
  sourceSession.workspaceId,
771
1047
  device.enrollmentId,
772
1048
  );
773
- if (!enrollment || enrollment.status !== "active") {
1049
+ if (!enrollment || enrollment.status !== "active" || !enrollment.connectionInstanceId) {
774
1050
  throw new ComputerSessionStateError("Attached browser machine is unavailable");
775
1051
  }
776
- assertPlacementInstance(expectedPlacementInstanceId, device.connectionGeneration);
1052
+ if (!enrollment.workspaceRoot) {
1053
+ throw new ComputerSessionStateError(
1054
+ "Attached browser machine has not reported an absolute workspace root",
1055
+ );
1056
+ }
1057
+ if (operation !== "computer.end") {
1058
+ assertConnectedMachineComputerAccess(enrollment, operation);
1059
+ }
1060
+ const placementInstanceId = attachedEndPlacementInstanceId(
1061
+ operation,
1062
+ expectedPlacementInstanceId,
1063
+ device.connectionGeneration,
1064
+ );
777
1065
  const built = await buildSelfhostedBackendSession({
778
1066
  workspaceId: sourceSession.workspaceId,
779
1067
  agentId: device.enrollmentId,
1068
+ connectionInstanceId: enrollment.connectionInstanceId,
1069
+ workspaceRoot: enrollment.workspaceRoot,
780
1070
  relay: relayConfigFromSettings(deps.settings),
781
1071
  controlRpcFactory: () => new NatsControlRpc(async () => deps.bus.getRequestConnection()),
782
1072
  epoch: 0,
783
1073
  timeoutMs: deps.settings.sandboxSelfhostedControlTimeoutMs,
784
1074
  execTimeoutMs: deps.settings.sandboxSelfhostedExecTimeoutMs,
1075
+ operationResourcePolicy: enrollment.operationPolicy,
1076
+ operationResourcePolicySupported:
1077
+ enrollment.agentCapabilities.operationResourcePolicy === true,
1078
+ operationCpuQuotaSupported: enrollment.agentCapabilities.operationCpuQuota === true,
1079
+ transactionalFsWriteSupported: enrollment.agentCapabilities.transactionalFsWrite === true,
785
1080
  ...(deps.settings.agentOpStreamEnabled === true &&
786
1081
  enrollment.opStream === true &&
787
1082
  deps.bus.getOpStreamConnection
@@ -797,9 +1092,10 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
797
1092
  waitSignal.throwIfAborted();
798
1093
  return await callback({
799
1094
  placement: expectedPlacement,
800
- placementInstanceId: device.connectionGeneration,
1095
+ placementInstanceId,
801
1096
  session: built.session as unknown as BrowserControlPlacementSession,
802
1097
  lease: null,
1098
+ screenControlAllowed: enrollment.allowScreenControl === true,
803
1099
  });
804
1100
  }
805
1101
  const runWithChannelA =
@@ -819,6 +1115,9 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
819
1115
  waitSignal,
820
1116
  operation,
821
1117
  retryControllerTransport: operation === "computer.read" || operation === "computer.action",
1118
+ ...(expectedPlacementInstanceId && expectedPlacement?.kind === "sandbox_group"
1119
+ ? { retainedInstanceId: expectedPlacementInstanceId }
1120
+ : {}),
822
1121
  },
823
1122
  async (handle) => {
824
1123
  if (expectedPlacement?.kind === "sandbox_group") {
@@ -838,13 +1137,38 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
838
1137
  }
839
1138
 
840
1139
  const resolved = await handle.routingSession.prime();
1140
+ let screenControlAllowed = false;
1141
+ if (operation !== "computer.end" && resolved.kind === "selfhosted" && resolved.sandboxId) {
1142
+ const sandbox = await getSandbox(deps.db, grant, resolved.sandboxId);
1143
+ if (sandbox?.kind !== "selfhosted" || !sandbox.enrollmentId) {
1144
+ throw new ComputerSessionStateError("Connected Machine placement is unavailable");
1145
+ }
1146
+ const enrollment = await getLiveEnrollmentConnection(
1147
+ deps.db,
1148
+ grant,
1149
+ sandbox.enrollmentId,
1150
+ );
1151
+ if (!enrollment?.connectionInstanceId) {
1152
+ throw new ComputerSessionStateError("Connected Machine is unavailable");
1153
+ }
1154
+ assertConnectedMachineComputerAccess(enrollment, operation);
1155
+ screenControlAllowed = enrollment.allowScreenControl === true;
1156
+ }
841
1157
  if (expectedPlacement?.kind === "connected_machine") {
842
1158
  if (
843
1159
  resolved.kind !== "selfhosted" ||
844
1160
  resolved.sandboxId !== expectedPlacement.sandboxId
845
1161
  ) {
846
- throw new ComputerSessionStateError(
847
- "ComputerSession Connected Machine is not the active placement",
1162
+ if (operation === "computer.create") {
1163
+ throw new HTTPException(422, {
1164
+ message:
1165
+ "The requested Connected Machine is not this session's current placement. Move the session to that machine before creating an interaction resource.",
1166
+ });
1167
+ }
1168
+ return await throwComputerSourcePlacementChanged(
1169
+ grant,
1170
+ sourceSession.id,
1171
+ expectedPlacement.sandboxId,
848
1172
  );
849
1173
  }
850
1174
  const placementInstanceId = resolved.providerInstanceId ?? expectedPlacement.sandboxId;
@@ -854,6 +1178,7 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
854
1178
  placementInstanceId,
855
1179
  session: resolved.session as unknown as BrowserControlPlacementSession,
856
1180
  lease: null,
1181
+ screenControlAllowed,
857
1182
  });
858
1183
  }
859
1184
  if (resolved.sandboxId === null) {
@@ -878,10 +1203,14 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
878
1203
  }
879
1204
  if (resolved.kind === "selfhosted") {
880
1205
  return await callback({
881
- placement: { kind: "connected_machine", sandboxId: resolved.sandboxId },
1206
+ placement: {
1207
+ kind: "connected_machine",
1208
+ sandboxId: resolved.sandboxId,
1209
+ },
882
1210
  placementInstanceId: resolved.providerInstanceId ?? resolved.sandboxId,
883
1211
  session: resolved.session as unknown as BrowserControlPlacementSession,
884
1212
  lease: null,
1213
+ screenControlAllowed,
885
1214
  });
886
1215
  }
887
1216
  throw new BrowserControlUnsupportedError(
@@ -914,7 +1243,9 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
914
1243
  !lease ||
915
1244
  (lease.liveness !== "warm" && lease.liveness !== "draining") ||
916
1245
  lease.instanceId !== binding.placementInstanceId ||
917
- !lease.controllerDataPlaneUrl
1246
+ !lease.controllerDataPlaneUrl ||
1247
+ (lease.backend === "opensandbox" && deps.settings.openSandboxSignedEndpoints) ||
1248
+ !controllerCachedUrlIsUsable(lease.controllerDataPlaneUrl)
918
1249
  ) {
919
1250
  return null;
920
1251
  }
@@ -938,8 +1269,29 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
938
1269
  ) {
939
1270
  return placement;
940
1271
  }
1272
+ if (placement.lease.backend === "opensandbox" && deps.settings.openSandboxSignedEndpoints) {
1273
+ if (!placement.lease.controllerDataPlaneUrl) return placement;
1274
+ const lease = await recordLeaseControllerDataPlaneUrl(deps.db, {
1275
+ accountId: grant.accountId,
1276
+ workspaceId,
1277
+ sandboxGroupId: placement.placement.sandboxGroupId,
1278
+ expectedEpoch: placement.lease.leaseEpoch,
1279
+ expectedInstanceId: placement.lease.instanceId,
1280
+ controllerDataPlaneUrl: null,
1281
+ });
1282
+ return lease ? { ...placement, lease } : placement;
1283
+ }
941
1284
  const endpoint = await placement.session.resolveExposedPort(BROWSER_CONTROL_PORT);
942
1285
  const url = buildStreamUrl(endpoint);
1286
+ if (
1287
+ !shouldPersistControllerDataPlaneUrl({
1288
+ backend: placement.lease.backend,
1289
+ signedEndpoints: deps.settings.openSandboxSignedEndpoints,
1290
+ url,
1291
+ })
1292
+ ) {
1293
+ return placement;
1294
+ }
943
1295
  const lease = await recordLeaseControllerDataPlaneUrl(deps.db, {
944
1296
  accountId: grant.accountId,
945
1297
  workspaceId,
@@ -949,6 +1301,9 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
949
1301
  controllerDataPlaneUrl: url,
950
1302
  });
951
1303
  if (!lease) return placement;
1304
+ if (!controllerCacheAllowsHostFetch(url)) {
1305
+ return { ...placement, lease };
1306
+ }
952
1307
  return { ...placement, session: controllerOnlySession(url), lease };
953
1308
  }
954
1309
 
@@ -964,7 +1319,13 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
964
1319
  const sandboxGroupId =
965
1320
  placement.placement.kind === "sandbox_group" ? placement.placement.sandboxGroupId : null;
966
1321
  return await withCachedController({
967
- cachedUrl: sandboxGroupId ? (cachedUrl ?? null) : null,
1322
+ cachedUrl:
1323
+ sandboxGroupId &&
1324
+ cachedUrl &&
1325
+ !(placement.lease?.backend === "opensandbox" && deps.settings.openSandboxSignedEndpoints) &&
1326
+ controllerCachedUrlIsUsable(cachedUrl)
1327
+ ? cachedUrl
1328
+ : null,
968
1329
  createCachedClient: (url) =>
969
1330
  new BrowserControlClient(controllerOnlySession(url), { adminToken }),
970
1331
  prepareCachedClient: async (client) => {
@@ -1016,6 +1377,17 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
1016
1377
  computerSessionId,
1017
1378
  });
1018
1379
  await authorizeSourceSession(deps, grant, record.sourceSessionId, authorizationOperation);
1380
+ let sourceSession: Session | null = null;
1381
+ if (record.session.placement?.kind === "connected_machine") {
1382
+ sourceSession = await requireSourceSession(deps, workspaceId, record.sourceSessionId);
1383
+ if (sourceSession.activeSandboxId !== record.session.placement.sandboxId) {
1384
+ return await throwComputerSourcePlacementChanged(
1385
+ grant,
1386
+ record.sourceSessionId,
1387
+ record.session.placement.sandboxId,
1388
+ );
1389
+ }
1390
+ }
1019
1391
  if (record.session.lifecycle !== "active" || !record.session.controller) {
1020
1392
  throw new ComputerSessionStateError("ComputerSession is not active");
1021
1393
  }
@@ -1096,7 +1468,7 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
1096
1468
  }
1097
1469
  }
1098
1470
 
1099
- const sourceSession = await requireSourceSession(deps, workspaceId, record.sourceSessionId);
1471
+ sourceSession ??= await requireSourceSession(deps, workspaceId, record.sourceSessionId);
1100
1472
  return await withComputerPlacement(
1101
1473
  sourceSession,
1102
1474
  grant,
@@ -1116,6 +1488,25 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
1116
1488
  }
1117
1489
  }
1118
1490
 
1491
+ async function throwComputerSourcePlacementChanged(
1492
+ grant: AccessGrant,
1493
+ sourceSessionId: string,
1494
+ connectedSandboxId: string,
1495
+ ): Promise<never> {
1496
+ const reconciliation = await terminalizeStaleConnectedInteractionPlacement(deps.db, {
1497
+ accountId: grant.accountId,
1498
+ workspaceId: grant.workspaceId,
1499
+ sourceSessionId,
1500
+ connectedSandboxId,
1501
+ });
1502
+ if (reconciliation.sourcePlacementChanged) {
1503
+ throw sourcePlacementChangedApiError("computer_session");
1504
+ }
1505
+ throw new ComputerSessionStateError(
1506
+ "ComputerSession source placement changed during the request; retry",
1507
+ );
1508
+ }
1509
+
1119
1510
  async function ensureInteractionHolder(
1120
1511
  grant: AccessGrant,
1121
1512
  sourceSession: Session,
@@ -1127,6 +1518,11 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
1127
1518
  if (!placement.lease?.instanceId) {
1128
1519
  throw new ComputerSessionStateError("ComputerSession lease placement is unavailable");
1129
1520
  }
1521
+ const sandboxRuntime = await resolveSessionSandboxRuntime(
1522
+ deps.db,
1523
+ deps.settings,
1524
+ sourceSession,
1525
+ );
1130
1526
  const acquired = await acquireLease(deps.db, {
1131
1527
  accountId: grant.accountId,
1132
1528
  workspaceId: sourceSession.workspaceId,
@@ -1135,9 +1531,14 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
1135
1531
  holderId: interactionHolderId(computerSessionId),
1136
1532
  subjectId: sourceSession.id,
1137
1533
  backend: placement.lease.backend,
1534
+ warmBilling: {
1535
+ mode: deps.settings.sandboxWarmBillingMode,
1536
+ rateMicrosPerSecond: sandboxWarmRateMicrosPerSecond(deps.settings, placement.lease.backend),
1537
+ },
1138
1538
  os: placement.lease.os,
1139
- image: placement.lease.image,
1140
- rigVersionId: placement.lease.rigVersionId,
1539
+ image: sandboxRuntime.image,
1540
+ imagePolicy: "new_creates_only",
1541
+ rigVersionId: sourceSession.rigVersionId,
1141
1542
  leaseTtlMs: deps.settings.sandboxLeaseTtlMs,
1142
1543
  expectedEpoch: placement.lease.leaseEpoch,
1143
1544
  waitSignal,
@@ -1158,6 +1559,11 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
1158
1559
  ).catch(() => undefined);
1159
1560
  throw new ComputerSessionStateError("ComputerSession placement fence changed; retry");
1160
1561
  }
1562
+ await renewSandboxProviderExpiration({
1563
+ backend: acquired.lease.backend as ApiRouteDeps["settings"]["sandboxBackend"],
1564
+ settings: deps.settings,
1565
+ instanceId: acquired.lease.instanceId,
1566
+ }).catch(() => false);
1161
1567
  return true;
1162
1568
  }
1163
1569
 
@@ -1273,7 +1679,11 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
1273
1679
  async function routePreamble(
1274
1680
  context: Context,
1275
1681
  permission: "sessions:read" | "sessions:control" | "stream:view",
1276
- ): Promise<{ workspaceId: string; grant: AccessGrant; computerSessionId: string }> {
1682
+ ): Promise<{
1683
+ workspaceId: string;
1684
+ grant: AccessGrant;
1685
+ computerSessionId: string;
1686
+ }> {
1277
1687
  const workspaceId = context.req.param("workspaceId") ?? "";
1278
1688
  const grant = await requireAccessGrant(context, deps, workspaceId, permission);
1279
1689
  return {
@@ -1284,6 +1694,21 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
1284
1694
  }
1285
1695
  }
1286
1696
 
1697
+ function sourcePlacementChangedApiError(interactionResource: "computer_session"): ApiHttpError {
1698
+ return new ApiHttpError(409, {
1699
+ code: "conflict",
1700
+ message:
1701
+ "This Desktop belonged to a previous task placement and was retired. Open a new Desktop.",
1702
+ retryable: false,
1703
+ outcomeUnknown: false,
1704
+ details: {
1705
+ interactionResource,
1706
+ interactionFailureCode: "source_placement_changed",
1707
+ interactionLifecycle: "lost",
1708
+ },
1709
+ });
1710
+ }
1711
+
1287
1712
  function computerCreateInput(
1288
1713
  grant: AccessGrant,
1289
1714
  workspaceId: string,
@@ -1400,7 +1825,10 @@ async function authorizeSourceSession(
1400
1825
  });
1401
1826
  } catch (error) {
1402
1827
  if (error instanceof SessionAuthorizationDeniedError) {
1403
- throw new HTTPException(404, { message: "session not found", cause: error });
1828
+ throw new HTTPException(404, {
1829
+ message: "session not found",
1830
+ cause: error,
1831
+ });
1404
1832
  }
1405
1833
  if (error instanceof SessionAuthorizationUnavailableError) {
1406
1834
  throw new HTTPException(503, {
@@ -1456,26 +1884,8 @@ function requireOpaqueParam(context: Context, name: string): string {
1456
1884
  return value;
1457
1885
  }
1458
1886
 
1459
- function requestOrigin(context: Context, allowedPattern: string): string | null {
1460
- const value = context.req.header("origin");
1461
- if (!value) return null;
1462
- let url: URL;
1463
- try {
1464
- url = new URL(value);
1465
- } catch {
1466
- throw new HTTPException(400, { message: "invalid request origin" });
1467
- }
1468
- if (
1469
- url.origin === "null" ||
1470
- (url.protocol !== "http:" && url.protocol !== "https:") ||
1471
- url.origin !== value
1472
- ) {
1473
- throw new HTTPException(400, { message: "invalid request origin" });
1474
- }
1475
- if (!allowedCorsOrigin(allowedPattern, url.origin)) {
1476
- throw new HTTPException(403, { message: "request origin is not allowed" });
1477
- }
1478
- return url.origin;
1887
+ function requestOrigin(context: Context, settings: ApiRouteDeps["settings"]): string | null {
1888
+ return validateInteractionRequestOrigin(context.req.header("origin"), settings);
1479
1889
  }
1480
1890
 
1481
1891
  function interactionActorForGrant(grant: AccessGrant): ReturnType<typeof InteractionActor.parse> {
@@ -1501,6 +1911,39 @@ function assertPlacementInstance(expected: string | null, actual: string): void
1501
1911
  }
1502
1912
  }
1503
1913
 
1914
+ /** End must still reach the live agent with the session's original token
1915
+ * fence. A later Chrome generation must not block ScreenCaptureKit teardown. */
1916
+ function attachedEndPlacementInstanceId(
1917
+ operation: ChannelAOperation,
1918
+ expectedPlacementInstanceId: string | null,
1919
+ liveGeneration: string,
1920
+ ): string {
1921
+ if (operation === "computer.end" && expectedPlacementInstanceId) {
1922
+ return expectedPlacementInstanceId;
1923
+ }
1924
+ assertPlacementInstance(expectedPlacementInstanceId, liveGeneration);
1925
+ return liveGeneration;
1926
+ }
1927
+
1928
+ function assertConnectedMachineComputerAccess(
1929
+ enrollment: {
1930
+ hasDisplay: boolean;
1931
+ desktopUnavailableReason: string | null;
1932
+ allowScreenControl: boolean;
1933
+ },
1934
+ operation: ChannelAOperation,
1935
+ ): void {
1936
+ const error = connectedMachineComputerAccessError(
1937
+ enrollment,
1938
+ operation === "computer.action" || operation === "computer.control",
1939
+ );
1940
+ if (!error) return;
1941
+ if (error.status === 403) {
1942
+ throw new HTTPException(403, { message: error.message });
1943
+ }
1944
+ throw new ComputerSessionStateError(error.message);
1945
+ }
1946
+
1504
1947
  function isTerminalOperation(state: string): boolean {
1505
1948
  return state === "completed" || state === "failed" || state === "outcome_unknown";
1506
1949
  }
@@ -1521,7 +1964,11 @@ function interactionFailure(error: unknown) {
1521
1964
  error instanceof BrowserControlUnsupportedError ||
1522
1965
  error instanceof BrowserControlServerUnsupportedError
1523
1966
  ) {
1524
- return { code: "unsupported" as const, message: error.message, retryable: false };
1967
+ return {
1968
+ code: "unsupported" as const,
1969
+ message: error.message,
1970
+ retryable: false,
1971
+ };
1525
1972
  }
1526
1973
  if (error instanceof BrowserControlServerError) {
1527
1974
  return {
@@ -1541,7 +1988,11 @@ function interactionFailure(error: unknown) {
1541
1988
  }
1542
1989
 
1543
1990
  function computerRouteError(error: unknown): HTTPException {
1991
+ const connectedMachineError = interactionControlApiError(error, "computer");
1992
+ if (connectedMachineError) return connectedMachineError;
1544
1993
  if (error instanceof HTTPException) return error;
1994
+ if (error instanceof SandboxPaidComputeAdmissionError)
1995
+ return new HTTPException(402, { message: error.message, cause: error });
1545
1996
  if (error instanceof ComputerSessionNotFoundError) {
1546
1997
  return new HTTPException(404, { message: error.message, cause: error });
1547
1998
  }