@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,65 +1,148 @@
1
+ import { withDirectModelProviders } from "@opengeni/config";
2
+ import { listConnectionsMetadata } from "@opengeni/db";
3
+ import {
4
+ workspaceProviderApiKeyConnectionMetadataFromConnections,
5
+ listWorkspaceProviderCustomModelsByKind,
6
+ getOrganizationModelProviderCatalogForWorkspace,
7
+ type WorkspaceCustomModelProviderKind,
8
+ } from "@opengeni/db";
9
+ import { CLAUDE_CONNECTION_KINDS, type ClaudeConnectionCatalog } from "@opengeni/config";
10
+ import { SessionControlConflictError, WorkspacePauseTimerInputError } from "@opengeni/db";
11
+ import { updateWorkspaceSettingsWithToolDefaults } from "@opengeni/db/workspace-tool-defaults";
12
+ import { WorkspacePauseTimerRequest } from "@opengeni/contracts";
13
+ import { getWorkspaceConnectionModelRestrictions } from "@opengeni/db";
14
+ import { createHash } from "node:crypto";
1
15
  import {
2
16
  AddWorkspaceMemberRequest,
3
17
  CreateWorkspaceRequest,
18
+ EnsureWorkspaceRequest,
19
+ EnsureWorkspaceResponse,
20
+ ListWorkspaceMemberCandidatesResponse,
4
21
  ListWorkspaceMembersResponse,
5
22
  SetWorkspaceDefaultRigRequest,
6
23
  UpdateWorkspaceMemberRequest,
24
+ CreateWorkspaceGatewayCustomModelRequest,
25
+ CreateWorkspaceOpenRouterCustomModelRequest,
26
+ DeleteWorkspaceGatewayCustomModelRequest,
27
+ DeleteWorkspaceOpenRouterCustomModelRequest,
7
28
  UpdateWorkspaceModelPolicyRequest,
8
29
  UpdateWorkspaceRequest,
9
30
  UpdateWorkspaceSettingsRequest,
10
31
  WORKSPACE_CONTROL_ACTOR_MAX_BYTES,
11
32
  WorkspaceModelCatalogResponse,
33
+ WorkspaceGatewayCustomModel,
34
+ WorkspaceGatewayCustomModelsResponse,
35
+ WorkspaceOpenRouterCustomModel,
36
+ WorkspaceOpenRouterCustomModelsResponse,
12
37
  WorkspaceRealtimeModelCatalogResponse,
13
38
  WorkspaceInferenceControlRequest,
14
39
  Workspace,
15
40
  WorkspaceMember,
16
41
  workspaceControlUtf8Bytes,
42
+ stableJson,
17
43
  type AccessContext,
18
44
  type Permission,
45
+ type WorkspaceMemberCandidate,
46
+ type WorkspaceMember as WorkspaceMemberValue,
19
47
  } from "@opengeni/contracts";
48
+ import { loadWorkspaceCodexModelAvailability } from "@opengeni/core";
20
49
  import {
21
50
  allWorkspacePermissions,
22
51
  createWorkspace,
23
- deleteWorkspaceIfQuiescent,
24
- getManagedUserByEmail,
52
+ ensureWorkspaceByExternalIdentity,
53
+ findWorkspaceByExternalIdentity,
54
+ getManagedUserProfilesByIds,
55
+ getWorkspace,
25
56
  getWorkspaceModelPolicy,
26
57
  grantWorkspaceAccess,
27
58
  listWorkspaceMembers,
59
+ listWorkspaceMemberManagementCandidates,
60
+ normalizeWorkspaceMembershipPermissions,
28
61
  listWorkspaceControlEvents,
62
+ listSharedWorkspacesForAccount,
63
+ listWorkspaceGatewayCustomModels,
64
+ listWorkspaceOpenRouterCustomModels,
65
+ createWorkspaceGatewayCustomModel,
66
+ createWorkspaceOpenRouterCustomModel,
67
+ deleteWorkspaceGatewayCustomModel,
68
+ deleteWorkspaceOpenRouterCustomModel,
69
+ replayWorkspaceGatewayCustomModelCreate,
70
+ replayWorkspaceOpenRouterCustomModelCreate,
29
71
  listWorkspacesForSubject,
72
+ nestedPostgresSqlState,
30
73
  removeWorkspaceMember,
31
74
  requireWorkspace,
75
+ updateWorkspaceSettings,
32
76
  getRig,
33
77
  setWorkspaceDefaultRig,
34
78
  updateWorkspace,
35
- updateWorkspaceSettings,
79
+ upsertWorkspaceMemberAsWorkspaceManager,
36
80
  upsertWorkspaceModelPolicy,
37
81
  workspaceCodexSubscriptionActive,
82
+ workspaceControlRequestLockTimeoutMs,
38
83
  workspaceXaiSubscriptionActive,
39
84
  workspaceVercelAiGatewayConnectionActive,
85
+ WorkspaceGatewayCustomModelHistoryLimitError,
86
+ WorkspaceOpenRouterCustomModelHistoryLimitError,
87
+ WorkspaceExternalIdentityConflictError,
88
+ WorkspaceGatewayCustomModelLimitError,
89
+ WorkspaceOpenRouterCustomModelLimitError,
90
+ WorkspaceLimitExceededError,
40
91
  } from "@opengeni/db";
41
92
  import { boundWorkspaceControlHttpPage } from "@opengeni/events";
42
93
  import type { Hono } from "hono";
43
94
  import { HTTPException } from "hono/http-exception";
44
- import { hasPermission, requireAccessContext, requireAccessGrant } from "@opengeni/core";
95
+ import {
96
+ getManagedAuthRequestActorEpoch,
97
+ accountScopedApiKeyWorkspaceAuthority,
98
+ organizationWorkspaceInScope,
99
+ hasPermission,
100
+ requireAccessContext,
101
+ requireApiKeyDelegationContext,
102
+ requireExplicitPermissionDelegation,
103
+ isDeveloperSetupApiKeyContext,
104
+ listExternalActorWorkspaces,
105
+ addExternalWorkspaceMemberForRequest,
106
+ requireAccessGrant,
107
+ requireWorkspaceSettingsGrant,
108
+ requireFreshAccessGrant,
109
+ resolveWorkspaceCatalogSettings,
110
+ creditsDefaultSessionModel,
111
+ loadWorkspaceClaudeSubscriptionReadiness,
112
+ resolveDefaultSessionModelForSelections,
113
+ resolveWorkspaceModelSelection,
114
+ } from "@opengeni/core";
45
115
  import { requireLimit } from "@opengeni/core";
46
116
  import type { ApiRouteDeps } from "@opengeni/core";
47
117
  import {
48
118
  assertWorkspaceMemberRemovable,
119
+ assertWorkspaceMemberUpdateAllowed,
49
120
  controlHumanWorkspace,
50
- resolveMemberSubjectId,
121
+ controlHumanWorkspaceTimer,
51
122
  } from "@opengeni/core";
52
123
  import { boundedLimit } from "../http/common";
53
- import { sseWorkspaceControlStream } from "../http/sse";
54
- import { buildWorkspaceModelCatalog } from "../model-catalog";
55
- import { processTemporalScheduleCleanupClaims } from "../temporal-schedule-cleanup";
124
+ import { ApiHttpError } from "../http/api-error";
125
+ import { browserSseDeliveryOptions, sseWorkspaceControlStream } from "../http/sse";
126
+ import { projectWorkspaceModelCatalog } from "../model-catalog";
127
+ import { deleteWorkspaceForRequest } from "../workspace-deletion";
56
128
  import {
57
129
  AI_GATEWAY_REALTIME_MODELS,
58
130
  CODEX_REALTIME_MODEL_ID,
59
131
  SUPERGROK_REALTIME_MODEL_ID,
60
132
  canonicalizeConfiguredModelId,
133
+ configuredStaticUsageLimits,
134
+ configuredGatewayUpstreamModelIds,
135
+ configuredGatewayWorkspaceProductModelIds,
136
+ configuredModelInputIdentities,
137
+ configuredOpenRouterUpstreamModelIds,
138
+ configuredOpenRouterWorkspaceProductModelIds,
139
+ WORKSPACE_GATEWAY_MODEL_ID_PREFIX,
140
+ WORKSPACE_OPENROUTER_MODEL_ID_PREFIX,
141
+ sandboxImageAllowlist,
61
142
  type Settings,
62
143
  } from "@opengeni/config";
144
+ import { AddExternalWorkspaceMemberRequest } from "@opengeni/contracts/external-identities";
145
+ import { parseRequestBody, parseRequestJson, readRequestJson } from "../http/request-body";
63
146
 
64
147
  export function canonicalWorkspacePolicyModelIds(
65
148
  settings: Settings,
@@ -71,13 +154,145 @@ export function canonicalWorkspacePolicyModelIds(
71
154
  return [...new Set(modelIds.map((modelId) => canonicalizeConfiguredModelId(settings, modelId)))];
72
155
  }
73
156
 
157
+ function projectWorkspaceGatewayCustomModel(model: {
158
+ id: string;
159
+ upstreamModelId: string;
160
+ label: string | null;
161
+ version: number;
162
+ createdAt: Date;
163
+ updatedAt: Date;
164
+ }) {
165
+ return WorkspaceGatewayCustomModel.parse({
166
+ id: model.id,
167
+ upstreamModelId: model.upstreamModelId,
168
+ label: model.label,
169
+ version: model.version,
170
+ createdAt: model.createdAt.toISOString(),
171
+ updatedAt: model.updatedAt.toISOString(),
172
+ });
173
+ }
174
+
175
+ function projectWorkspaceOpenRouterCustomModel(model: {
176
+ id: string;
177
+ upstreamModelId: string;
178
+ label: string | null;
179
+ version: number;
180
+ createdAt: Date;
181
+ updatedAt: Date;
182
+ }) {
183
+ return WorkspaceOpenRouterCustomModel.parse({
184
+ id: model.id,
185
+ upstreamModelId: model.upstreamModelId,
186
+ label: model.label,
187
+ version: model.version,
188
+ createdAt: model.createdAt.toISOString(),
189
+ updatedAt: model.updatedAt.toISOString(),
190
+ });
191
+ }
192
+
193
+ function workspaceCustomModelRequestHash(value: unknown): string {
194
+ return createHash("sha256").update(stableJson(value), "utf8").digest("hex");
195
+ }
196
+
197
+ type WorkspaceMemberProjectionInput = Omit<WorkspaceMemberValue, "permissions"> & {
198
+ permissions: unknown;
199
+ };
200
+
201
+ /**
202
+ * Keep the roster readable across stored-permission contract changes. Unknown
203
+ * values are never re-authorized: they are omitted from the public projection,
204
+ * while every currently recognized permission is preserved in stored order.
205
+ */
206
+ export function workspaceMembersResponse(members: readonly WorkspaceMemberProjectionInput[]) {
207
+ return ListWorkspaceMembersResponse.parse({
208
+ members: members.map((member) => ({
209
+ ...member,
210
+ permissions: normalizeWorkspaceMembershipPermissions(member.permissions),
211
+ })),
212
+ });
213
+ }
214
+
215
+ export function workspaceUpdateRequestsSettings(value: unknown): boolean {
216
+ return (
217
+ typeof value === "object" &&
218
+ value !== null &&
219
+ Object.prototype.hasOwnProperty.call(value, "settings")
220
+ );
221
+ }
222
+
223
+ export function workspaceUpdateRequestsAccountTransfer(value: unknown): boolean {
224
+ return (
225
+ typeof value === "object" &&
226
+ value !== null &&
227
+ Object.prototype.hasOwnProperty.call(value, "accountId")
228
+ );
229
+ }
230
+
231
+ /**
232
+ * Owning organization for `PUT /v1/workspaces/external`. An organization API
233
+ * key can only ever create workspaces in its own organization, so an omitted
234
+ * `accountId` resolves to that organization. Every other caller - notably a
235
+ * human who may belong to several organizations - must name it explicitly;
236
+ * guessing a default there could create the workspace in the wrong tenant.
237
+ */
238
+ export function externalWorkspaceAccountId(
239
+ context: AccessContext,
240
+ requested: string | undefined,
241
+ ): string {
242
+ if (requested !== undefined) return requested;
243
+ const organizationKey = accountScopedApiKeyWorkspaceAuthority(context);
244
+ if (organizationKey) return organizationKey.accountId;
245
+ throw new ApiHttpError(400, {
246
+ code: "validation_failed",
247
+ message:
248
+ "Invalid request body: accountId: required - pass the id of the organization that will own the workspace (only an organization API key may omit it).",
249
+ retryable: false,
250
+ details: {
251
+ code: "invalid_request_body",
252
+ issues: [{ path: "accountId", message: "Required" }],
253
+ omittedIssueCount: 0,
254
+ },
255
+ });
256
+ }
257
+
74
258
  export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
259
+ app.post("/v1/workspaces/:workspaceId/external-members", async (c) => {
260
+ // Validate at the HTTP boundary so a malformed body is a 400, not a raw
261
+ // schema error from the application layer.
262
+ const body = await readRequestJson(c);
263
+ parseRequestBody(AddExternalWorkspaceMemberRequest, body);
264
+ return c.json(
265
+ await addExternalWorkspaceMemberForRequest(c, deps, c.req.param("workspaceId"), body),
266
+ );
267
+ });
75
268
  app.get("/v1/access/me", async (c) => {
76
269
  return c.json(await requireAccessContext(c, deps));
77
270
  });
78
271
 
272
+ app.get("/v1/workspaces/:workspaceId/access/grant", async (c) => {
273
+ // Inventory may be empty for an external actor. Resolve the selected
274
+ // workspace through the canonical membership/key-ceiling boundary.
275
+ const grant = await requireAccessGrant(c, deps, c.req.param("workspaceId"));
276
+ c.header("cache-control", "private, no-store");
277
+ return c.json(grant);
278
+ });
279
+
79
280
  app.get("/v1/workspaces", async (c) => {
80
281
  const context = await requireAccessContext(c, deps);
282
+ const externalWorkspaces = await listExternalActorWorkspaces(context, deps);
283
+ if (externalWorkspaces !== null)
284
+ return c.json(externalWorkspaces.map((workspace) => Workspace.parse(workspace)));
285
+ const accountScopedAuthority = accountScopedApiKeyWorkspaceAuthority(context);
286
+ if (accountScopedAuthority) {
287
+ if (!hasPermission(accountScopedAuthority.permissions, "workspace:read")) return c.json([]);
288
+ return c.json(
289
+ (await listSharedWorkspacesForAccount(deps.db, accountScopedAuthority.accountId))
290
+ .filter((workspace) =>
291
+ organizationWorkspaceInScope(accountScopedAuthority.workspaceScope, workspace.id),
292
+ )
293
+ .map((workspace) => Workspace.parse(workspace)),
294
+ );
295
+ }
81
296
  const readableWorkspaceIds = [
82
297
  ...new Set(
83
298
  context.workspaceGrants
@@ -98,34 +313,123 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
98
313
  );
99
314
  });
100
315
 
316
+ app.put("/v1/workspaces/external", async (c) => {
317
+ const context = await requireAccessContext(c, deps);
318
+ const request = await parseRequestJson(c, EnsureWorkspaceRequest);
319
+ const accountId = externalWorkspaceAccountId(context, request.accountId);
320
+ const payload = { ...request, accountId };
321
+ requireAccountPermission(context, accountId, "workspace:create");
322
+ try {
323
+ const existing = await findWorkspaceByExternalIdentity(deps.db, {
324
+ accountId: payload.accountId,
325
+ externalSource: payload.externalSource,
326
+ externalId: payload.externalId,
327
+ });
328
+ if (existing) {
329
+ if (existing.accountId !== payload.accountId || existing.kind !== "shared") {
330
+ throw new WorkspaceExternalIdentityConflictError();
331
+ }
332
+ const authority = accountScopedApiKeyWorkspaceAuthority(context);
333
+ if (authority && !organizationWorkspaceInScope(authority.workspaceScope, existing.id))
334
+ throw new HTTPException(403, { message: "workspace is outside organization key scope" });
335
+ return c.json(
336
+ EnsureWorkspaceResponse.parse({
337
+ workspace: existing,
338
+ created: false,
339
+ }),
340
+ );
341
+ }
342
+ await requireLimit(deps, {
343
+ accountId: payload.accountId,
344
+ action: "workspace:create",
345
+ quantity: 1,
346
+ });
347
+ const result = await ensureWorkspaceByExternalIdentity(deps.db, {
348
+ accountId: payload.accountId,
349
+ externalSource: payload.externalSource,
350
+ externalId: payload.externalId,
351
+ name: payload.name,
352
+ slug: payload.slug ?? null,
353
+ ...(payload.agentInstructions !== undefined
354
+ ? {
355
+ agentInstructions: normalizeAgentInstructions(payload.agentInstructions),
356
+ }
357
+ : {}),
358
+ maxWorkspacesPerAccount: workspaceLimit(deps),
359
+ });
360
+ const response = EnsureWorkspaceResponse.parse(result);
361
+ const authority = accountScopedApiKeyWorkspaceAuthority(context);
362
+ if (
363
+ !result.created &&
364
+ authority &&
365
+ !organizationWorkspaceInScope(authority.workspaceScope, result.workspace.id)
366
+ )
367
+ throw new HTTPException(403, { message: "workspace is outside organization key scope" });
368
+ return result.created ? c.json(response, 201) : c.json(response);
369
+ } catch (error) {
370
+ if (error instanceof WorkspaceExternalIdentityConflictError) {
371
+ throw new HTTPException(409, {
372
+ message: "external workspace identity is already in use",
373
+ });
374
+ }
375
+ if (error instanceof WorkspaceLimitExceededError) {
376
+ throw new HTTPException(429, { message: error.message });
377
+ }
378
+ throw error;
379
+ }
380
+ });
381
+
101
382
  app.post("/v1/workspaces", async (c) => {
102
383
  const context = await requireAccessContext(c, deps);
103
- const payload = CreateWorkspaceRequest.parse(await c.req.json());
384
+ const payload = await parseRequestJson(c, CreateWorkspaceRequest);
104
385
  const accountId = payload.accountId ?? context.defaultAccountId;
105
386
  if (!accountId) {
106
- throw new HTTPException(409, { message: "account selection is required" });
387
+ throw new HTTPException(409, {
388
+ message: "account selection is required",
389
+ });
107
390
  }
108
391
  requireAccountPermission(context, accountId, "workspace:create");
109
- await requireLimit(deps, { accountId, action: "workspace:create", quantity: 1 });
110
- const workspace = await createWorkspace(deps.db, {
392
+ await requireLimit(deps, {
111
393
  accountId,
112
- name: payload.name.trim(),
113
- slug: payload.slug?.trim() || null,
114
- externalSource: payload.externalSource ?? null,
115
- externalId: payload.externalId ?? null,
116
- ...(payload.agentInstructions !== undefined
117
- ? { agentInstructions: normalizeAgentInstructions(payload.agentInstructions) }
118
- : {}),
394
+ action: "workspace:create",
395
+ quantity: 1,
119
396
  });
120
- await grantWorkspaceAccess(deps.db, {
121
- accountId,
122
- workspaceId: workspace.id,
123
- subjectId: context.subjectId,
124
- role: "owner",
125
- permissions: allWorkspacePermissions,
126
- ...(context.subjectLabel ? { subjectLabel: context.subjectLabel } : {}),
127
- });
128
- return c.json(Workspace.parse(workspace), 201);
397
+ try {
398
+ const workspace = await createWorkspace(deps.db, {
399
+ accountId,
400
+ name: payload.name.trim(),
401
+ slug: payload.slug?.trim() || null,
402
+ externalSource: payload.externalSource ?? null,
403
+ externalId: payload.externalId ?? null,
404
+ ...(payload.agentInstructions !== undefined
405
+ ? {
406
+ agentInstructions: normalizeAgentInstructions(payload.agentInstructions),
407
+ }
408
+ : {}),
409
+ maxWorkspacesPerAccount: workspaceLimit(deps),
410
+ });
411
+ // Setup keys already have canonical same-organization workspace access.
412
+ // Do not widen that ceiling with an all-permissions creator membership.
413
+ if (
414
+ !isDeveloperSetupApiKeyContext(context) &&
415
+ accountScopedApiKeyWorkspaceAuthority(context)?.permissionMode !== "explicit"
416
+ ) {
417
+ await grantWorkspaceAccess(deps.db, {
418
+ accountId,
419
+ workspaceId: workspace.id,
420
+ subjectId: context.subjectId,
421
+ role: "owner",
422
+ permissions: allWorkspacePermissions,
423
+ ...(context.subjectLabel ? { subjectLabel: context.subjectLabel } : {}),
424
+ });
425
+ }
426
+ return c.json(Workspace.parse(workspace), 201);
427
+ } catch (error) {
428
+ if (error instanceof WorkspaceLimitExceededError) {
429
+ throw new HTTPException(429, { message: error.message });
430
+ }
431
+ throw error;
432
+ }
129
433
  });
130
434
 
131
435
  app.get("/v1/workspaces/:workspaceId", async (c) => {
@@ -137,12 +441,43 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
137
441
  app.patch("/v1/workspaces/:workspaceId", async (c) => {
138
442
  const workspaceId = c.req.param("workspaceId");
139
443
  await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
140
- const payload = UpdateWorkspaceRequest.parse(await c.req.json());
444
+ const body = await readRequestJson(c);
445
+ if (workspaceUpdateRequestsSettings(body)) {
446
+ throw new ApiHttpError(400, {
447
+ code: "validation_failed",
448
+ message:
449
+ "Invalid request body: settings: workspace settings are not accepted here; send them to PATCH /v1/workspaces/:workspaceId/settings (SDK: updateWorkspaceSettings).",
450
+ retryable: false,
451
+ details: {
452
+ code: "invalid_request_body",
453
+ issues: [
454
+ {
455
+ path: "settings",
456
+ message: "Use PATCH /v1/workspaces/:workspaceId/settings",
457
+ },
458
+ ],
459
+ omittedIssueCount: 0,
460
+ },
461
+ });
462
+ }
463
+ if (workspaceUpdateRequestsAccountTransfer(body)) {
464
+ throw new ApiHttpError(409, {
465
+ code: "conflict",
466
+ message:
467
+ "A workspace is permanently owned by one organization; use workspace grants for same-organization access handoff.",
468
+ retryable: false,
469
+ outcomeUnknown: false,
470
+ details: { code: "workspace_transfer_unsupported" },
471
+ });
472
+ }
473
+ const payload = parseRequestBody(UpdateWorkspaceRequest, body);
141
474
  const workspace = await updateWorkspace(deps.db, workspaceId, {
142
475
  ...(payload.name !== undefined ? { name: payload.name.trim() } : {}),
143
476
  ...(payload.slug !== undefined ? { slug: payload.slug?.trim() || null } : {}),
144
477
  ...(payload.agentInstructions !== undefined
145
- ? { agentInstructions: normalizeAgentInstructions(payload.agentInstructions) }
478
+ ? {
479
+ agentInstructions: normalizeAgentInstructions(payload.agentInstructions),
480
+ }
146
481
  : {}),
147
482
  });
148
483
  return c.json(Workspace.parse(workspace));
@@ -152,13 +487,42 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
152
487
  // deep-merges (top-level) a settings patch, preserving unknown/future keys.
153
488
  app.patch("/v1/workspaces/:workspaceId/settings", async (c) => {
154
489
  const workspaceId = c.req.param("workspaceId");
155
- await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
490
+ await requireWorkspaceSettingsGrant(c, deps, workspaceId);
156
491
  const parsed = UpdateWorkspaceSettingsRequest.safeParse(await c.req.json());
157
492
  if (!parsed.success) {
158
- throw new HTTPException(400, { message: "invalid workspace settings patch" });
493
+ throw new HTTPException(400, {
494
+ message: "invalid workspace settings patch",
495
+ });
496
+ }
497
+ const requestedImage = parsed.data.defaultSandboxImage;
498
+ if (requestedImage && !sandboxImageAllowlist(deps.settings).includes(requestedImage)) {
499
+ throw new HTTPException(422, {
500
+ message:
501
+ "defaultSandboxImage must be one of the images in this deployment's OPENGENI_SANDBOX_IMAGE_ALLOWLIST",
502
+ });
503
+ }
504
+ // Request-scoped: bound the exclusive control-prefix wait so a busy
505
+ // workspace yields the retryable 503 instead of parking this request.
506
+ try {
507
+ const workspace = await updateWorkspaceSettingsWithToolDefaults(
508
+ deps.db,
509
+ workspaceId,
510
+ parsed.data,
511
+ { requireWorkspace, updateWorkspaceSettings },
512
+ {
513
+ controlLockTimeoutMs: workspaceControlRequestLockTimeoutMs(),
514
+ },
515
+ );
516
+ return c.json(Workspace.parse(workspace));
517
+ } catch (error) {
518
+ if (nestedPostgresSqlState(error) === "0A000") {
519
+ throw new HTTPException(409, {
520
+ message:
521
+ "Memory settings moved to Settings > Agent learning. Refresh the app to change them.",
522
+ });
523
+ }
524
+ throw error;
159
525
  }
160
- const workspace = await updateWorkspaceSettings(deps.db, workspaceId, parsed.data);
161
- return c.json(Workspace.parse(workspace));
162
526
  });
163
527
 
164
528
  // Per-workspace model/provider availability policy (the HARD blocker over
@@ -168,31 +532,392 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
168
532
  app.get("/v1/workspaces/:workspaceId/model-catalog", async (c) => {
169
533
  const workspaceId = c.req.param("workspaceId");
170
534
  const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
535
+ const resolvedCatalog = await deps.resolveCatalogSettings();
536
+ const providerKinds: WorkspaceCustomModelProviderKind[] = [
537
+ "vercel_gateway",
538
+ "openrouter",
539
+ ...CLAUDE_CONNECTION_KINDS.filter(
540
+ (kind) =>
541
+ kind !== "claude_subscription" || resolvedCatalog.settings.claudeSubscriptionEnabled,
542
+ ),
543
+ ];
171
544
  const [
545
+ connectionModelRestrictions,
172
546
  policy,
173
547
  codexSubscriptionActive,
548
+ codexModelAvailability,
174
549
  xaiSubscriptionActive,
175
- workspaceGatewayConnectionActive,
550
+ claudePool,
551
+ workspaceConnections,
552
+ workspaceCustomModels,
553
+ organizationProviders,
554
+ workspace,
176
555
  ] = await Promise.all([
556
+ getWorkspaceConnectionModelRestrictions(deps.db, workspaceId, grant.subjectId),
177
557
  getWorkspaceModelPolicy(deps.db, workspaceId),
178
558
  workspaceCodexSubscriptionActive(deps.db, deps.settings, workspaceId),
559
+ loadWorkspaceCodexModelAvailability(deps.db, resolvedCatalog.settings, workspaceId),
179
560
  workspaceXaiSubscriptionActive(deps.db, deps.settings, workspaceId, grant.subjectId),
180
- workspaceVercelAiGatewayConnectionActive(deps.db, workspaceId),
561
+ loadWorkspaceClaudeSubscriptionReadiness(deps.db, resolvedCatalog.settings, {
562
+ accountId: grant.accountId,
563
+ workspaceId,
564
+ subjectId: grant.subjectId,
565
+ }),
566
+ listConnectionsMetadata(deps.db, workspaceId, null),
567
+ listWorkspaceProviderCustomModelsByKind(deps.db, {
568
+ accountId: grant.accountId,
569
+ workspaceId,
570
+ providerKinds,
571
+ }),
572
+ getOrganizationModelProviderCatalogForWorkspace(deps.db, {
573
+ accountId: grant.accountId,
574
+ workspaceId,
575
+ providerKinds,
576
+ }),
577
+ getWorkspace(deps.db, workspaceId),
181
578
  ]);
579
+ const claudeConnections: ClaudeConnectionCatalog = {};
580
+ const workspaceClaudeConnections: ClaudeConnectionCatalog = {};
581
+ const workspaceConnectionActive = (kind: WorkspaceCustomModelProviderKind) =>
582
+ workspaceProviderApiKeyConnectionMetadataFromConnections(workspaceConnections, kind) !== null;
583
+ for (const kind of CLAUDE_CONNECTION_KINDS) {
584
+ if (kind === "claude_subscription" && !resolvedCatalog.settings.claudeSubscriptionEnabled)
585
+ continue;
586
+ claudeConnections[kind] = {
587
+ active:
588
+ kind === "claude_subscription"
589
+ ? claudePool.organization
590
+ : organizationProviders[kind].active,
591
+ models: organizationProviders[kind].models,
592
+ };
593
+ workspaceClaudeConnections[kind] = {
594
+ active:
595
+ kind === "claude_subscription" ? claudePool.workspace : workspaceConnectionActive(kind),
596
+ models: workspaceCustomModels[kind],
597
+ };
598
+ }
599
+ const workspaceCatalogSettings = withDirectModelProviders(
600
+ resolvedCatalog.settings,
601
+ workspaceConnections,
602
+ );
603
+ const selections = resolveWorkspaceModelSelection({
604
+ claudeConnections,
605
+ workspaceClaudeConnections,
606
+ connectionModelRestrictions,
607
+ settings: workspaceCatalogSettings,
608
+ policy,
609
+ observations: codexModelAvailability,
610
+ codexSubscriptionActive,
611
+ xaiSubscriptionActive,
612
+ workspaceGatewayConnectionActive: workspaceConnectionActive("vercel_gateway"),
613
+ workspaceGatewayCustomModels: workspaceCustomModels.vercel_gateway,
614
+ workspaceOpenRouterConnectionActive: workspaceConnectionActive("openrouter"),
615
+ workspaceOpenRouterCustomModels: workspaceCustomModels.openrouter,
616
+ organizationGatewayConnectionActive: organizationProviders.vercel_gateway.active,
617
+ organizationOpenRouterConnectionActive: organizationProviders.openrouter.active,
618
+ organizationGatewayCustomModels: organizationProviders.vercel_gateway.models,
619
+ organizationOpenRouterCustomModels: organizationProviders.openrouter.models,
620
+ });
621
+ // The same precedence the server applies when a new chat, API create, or
622
+ // scheduled occurrence names no model; published so pickers show it.
623
+ const workspaceSettings = workspace?.settings ?? {};
624
+ const defaultSelection = await resolveDefaultSessionModelForSelections(deps.db, {
625
+ settings: workspaceCatalogSettings,
626
+ accountId: grant.accountId,
627
+ workspaceSettings,
628
+ selections,
629
+ });
182
630
  c.header("cache-control", "private, no-store");
183
631
  return c.json(
184
632
  WorkspaceModelCatalogResponse.parse(
185
- buildWorkspaceModelCatalog({
186
- settings: deps.settings,
187
- policy,
188
- codexSubscriptionActive,
189
- xaiSubscriptionActive,
190
- workspaceGatewayConnectionActive,
633
+ projectWorkspaceModelCatalog(selections, {
634
+ defaultSelection,
635
+ creditsSelection: creditsDefaultSessionModel({
636
+ settings: workspaceCatalogSettings,
637
+ selections,
638
+ workspaceSettings,
639
+ }),
191
640
  }),
192
641
  ),
193
642
  );
194
643
  });
195
644
 
645
+ app.get("/v1/workspaces/:workspaceId/gateway-custom-models", async (c) => {
646
+ const workspaceId = c.req.param("workspaceId");
647
+ const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
648
+ const models = await listWorkspaceGatewayCustomModels(deps.db, {
649
+ accountId: grant.accountId,
650
+ workspaceId,
651
+ });
652
+ c.header("cache-control", "private, no-store");
653
+ return c.json(
654
+ WorkspaceGatewayCustomModelsResponse.parse({
655
+ models: models.map(projectWorkspaceGatewayCustomModel),
656
+ }),
657
+ );
658
+ });
659
+
660
+ app.post("/v1/workspaces/:workspaceId/gateway-custom-models", async (c) => {
661
+ const workspaceId = c.req.param("workspaceId");
662
+ const grant = await requireWorkspaceSettingsGrant(c, deps, workspaceId);
663
+ const parsed = CreateWorkspaceGatewayCustomModelRequest.safeParse(
664
+ await c.req.json().catch(() => null),
665
+ );
666
+ if (!parsed.success) {
667
+ throw new HTTPException(422, { message: "invalid Gateway custom model" });
668
+ }
669
+ const requestHash = workspaceCustomModelRequestHash({
670
+ action: "create",
671
+ upstreamModelId: parsed.data.upstreamModelId,
672
+ label: parsed.data.label ?? null,
673
+ });
674
+ const replay = await replayWorkspaceGatewayCustomModelCreate(deps.db, {
675
+ accountId: grant.accountId,
676
+ workspaceId,
677
+ operationId: parsed.data.operationId,
678
+ requestHash,
679
+ });
680
+ if (replay.outcome === "conflict") {
681
+ throw new HTTPException(409, {
682
+ message: "Gateway custom model operation conflicts with current state",
683
+ });
684
+ }
685
+ if (replay.outcome === "success") {
686
+ return c.json(projectWorkspaceGatewayCustomModel(replay.model), 201);
687
+ }
688
+ const catalog = await deps.resolveCatalogSettings();
689
+ if (configuredGatewayUpstreamModelIds(catalog.settings).includes(parsed.data.upstreamModelId)) {
690
+ throw new HTTPException(422, {
691
+ message: "Gateway model is already included in the deployment catalog",
692
+ });
693
+ }
694
+ const customProductId = `${WORKSPACE_GATEWAY_MODEL_ID_PREFIX}${parsed.data.upstreamModelId}`;
695
+ const deploymentProductIds = new Set([
696
+ ...configuredModelInputIdentities(catalog.settings),
697
+ ...configuredGatewayWorkspaceProductModelIds(catalog.settings),
698
+ ]);
699
+ if (deploymentProductIds.has(customProductId)) {
700
+ throw new HTTPException(422, {
701
+ message: "Gateway model product id conflicts with the deployment catalog",
702
+ });
703
+ }
704
+ try {
705
+ const model = await createWorkspaceGatewayCustomModel(deps.db, {
706
+ accountId: grant.accountId,
707
+ workspaceId,
708
+ upstreamModelId: parsed.data.upstreamModelId,
709
+ label: parsed.data.label ?? null,
710
+ operationId: parsed.data.operationId,
711
+ requestHash,
712
+ createdBySubjectId: grant.subjectId,
713
+ });
714
+ if (!model || model.retiredAt) {
715
+ throw new HTTPException(409, {
716
+ message: "Gateway custom model operation conflicts with current state",
717
+ });
718
+ }
719
+ return c.json(projectWorkspaceGatewayCustomModel(model), 201);
720
+ } catch (error) {
721
+ if (
722
+ error instanceof WorkspaceGatewayCustomModelLimitError ||
723
+ error instanceof WorkspaceGatewayCustomModelHistoryLimitError
724
+ ) {
725
+ throw new HTTPException(422, { message: error.message });
726
+ }
727
+ if (nestedPostgresSqlState(error) === "23505") {
728
+ throw new HTTPException(422, {
729
+ message: "Gateway custom model already exists",
730
+ });
731
+ }
732
+ throw error;
733
+ }
734
+ });
735
+
736
+ app.delete("/v1/workspaces/:workspaceId/gateway-custom-models/:customModelId", async (c) => {
737
+ const workspaceId = c.req.param("workspaceId");
738
+ const grant = await requireWorkspaceSettingsGrant(c, deps, workspaceId);
739
+ const customModelId = c.req.param("customModelId");
740
+ if (
741
+ !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(
742
+ customModelId,
743
+ )
744
+ ) {
745
+ throw new HTTPException(422, {
746
+ message: "invalid Gateway custom model id",
747
+ });
748
+ }
749
+ const parsed = DeleteWorkspaceGatewayCustomModelRequest.safeParse(
750
+ await c.req.json().catch(() => null),
751
+ );
752
+ if (!parsed.success) {
753
+ throw new HTTPException(422, {
754
+ message: "invalid Gateway custom model deletion",
755
+ });
756
+ }
757
+ const removed = await deleteWorkspaceGatewayCustomModel(deps.db, {
758
+ accountId: grant.accountId,
759
+ workspaceId,
760
+ customModelId,
761
+ expectedVersion: parsed.data.expectedVersion,
762
+ operationId: parsed.data.operationId,
763
+ requestHash: workspaceCustomModelRequestHash({
764
+ action: "delete",
765
+ customModelId,
766
+ expectedVersion: parsed.data.expectedVersion,
767
+ }),
768
+ });
769
+ if (removed.outcome === "not_found") {
770
+ throw new HTTPException(404, {
771
+ message: "Gateway custom model not found",
772
+ });
773
+ }
774
+ if (removed.outcome === "conflict") {
775
+ throw new HTTPException(409, {
776
+ message: "Gateway custom model changed; reload and retry",
777
+ });
778
+ }
779
+ return c.body(null, 204);
780
+ });
781
+
782
+ app.get("/v1/workspaces/:workspaceId/openrouter-custom-models", async (c) => {
783
+ const workspaceId = c.req.param("workspaceId");
784
+ const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
785
+ const models = await listWorkspaceOpenRouterCustomModels(deps.db, {
786
+ accountId: grant.accountId,
787
+ workspaceId,
788
+ });
789
+ c.header("cache-control", "private, no-store");
790
+ return c.json(
791
+ WorkspaceOpenRouterCustomModelsResponse.parse({
792
+ models: models.map(projectWorkspaceOpenRouterCustomModel),
793
+ }),
794
+ );
795
+ });
796
+
797
+ app.post("/v1/workspaces/:workspaceId/openrouter-custom-models", async (c) => {
798
+ const workspaceId = c.req.param("workspaceId");
799
+ const grant = await requireWorkspaceSettingsGrant(c, deps, workspaceId);
800
+ const parsed = CreateWorkspaceOpenRouterCustomModelRequest.safeParse(
801
+ await c.req.json().catch(() => null),
802
+ );
803
+ if (!parsed.success) {
804
+ throw new HTTPException(422, { message: "invalid OpenRouter custom model" });
805
+ }
806
+ const requestHash = workspaceCustomModelRequestHash({
807
+ action: "create",
808
+ upstreamModelId: parsed.data.upstreamModelId,
809
+ label: parsed.data.label ?? null,
810
+ });
811
+ const replay = await replayWorkspaceOpenRouterCustomModelCreate(deps.db, {
812
+ accountId: grant.accountId,
813
+ workspaceId,
814
+ operationId: parsed.data.operationId,
815
+ requestHash,
816
+ });
817
+ if (replay.outcome === "conflict") {
818
+ throw new HTTPException(409, {
819
+ message: "OpenRouter custom model operation conflicts with current state",
820
+ });
821
+ }
822
+ if (replay.outcome === "success") {
823
+ return c.json(projectWorkspaceOpenRouterCustomModel(replay.model), 201);
824
+ }
825
+ const catalog = await deps.resolveCatalogSettings();
826
+ if (
827
+ configuredOpenRouterUpstreamModelIds(catalog.settings).includes(parsed.data.upstreamModelId)
828
+ ) {
829
+ throw new HTTPException(422, {
830
+ message: "OpenRouter model is already included in the deployment catalog",
831
+ });
832
+ }
833
+ const customProductId = `${WORKSPACE_OPENROUTER_MODEL_ID_PREFIX}${parsed.data.upstreamModelId}`;
834
+ const deploymentProductIds = new Set([
835
+ ...configuredModelInputIdentities(catalog.settings),
836
+ ...configuredOpenRouterWorkspaceProductModelIds(catalog.settings),
837
+ ]);
838
+ if (deploymentProductIds.has(customProductId)) {
839
+ throw new HTTPException(422, {
840
+ message: "OpenRouter model product id conflicts with the deployment catalog",
841
+ });
842
+ }
843
+ try {
844
+ const model = await createWorkspaceOpenRouterCustomModel(deps.db, {
845
+ accountId: grant.accountId,
846
+ workspaceId,
847
+ upstreamModelId: parsed.data.upstreamModelId,
848
+ label: parsed.data.label ?? null,
849
+ operationId: parsed.data.operationId,
850
+ requestHash,
851
+ createdBySubjectId: grant.subjectId,
852
+ });
853
+ if (!model || model.retiredAt) {
854
+ throw new HTTPException(409, {
855
+ message: "OpenRouter custom model operation conflicts with current state",
856
+ });
857
+ }
858
+ return c.json(projectWorkspaceOpenRouterCustomModel(model), 201);
859
+ } catch (error) {
860
+ if (
861
+ error instanceof WorkspaceOpenRouterCustomModelLimitError ||
862
+ error instanceof WorkspaceOpenRouterCustomModelHistoryLimitError
863
+ ) {
864
+ throw new HTTPException(422, { message: error.message });
865
+ }
866
+ if (nestedPostgresSqlState(error) === "23505") {
867
+ throw new HTTPException(422, {
868
+ message: "OpenRouter custom model already exists",
869
+ });
870
+ }
871
+ throw error;
872
+ }
873
+ });
874
+
875
+ app.delete("/v1/workspaces/:workspaceId/openrouter-custom-models/:customModelId", async (c) => {
876
+ const workspaceId = c.req.param("workspaceId");
877
+ const grant = await requireWorkspaceSettingsGrant(c, deps, workspaceId);
878
+ const customModelId = c.req.param("customModelId");
879
+ if (
880
+ !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(
881
+ customModelId,
882
+ )
883
+ ) {
884
+ throw new HTTPException(422, {
885
+ message: "invalid OpenRouter custom model id",
886
+ });
887
+ }
888
+ const parsed = DeleteWorkspaceOpenRouterCustomModelRequest.safeParse(
889
+ await c.req.json().catch(() => null),
890
+ );
891
+ if (!parsed.success) {
892
+ throw new HTTPException(422, {
893
+ message: "invalid OpenRouter custom model deletion",
894
+ });
895
+ }
896
+ const removed = await deleteWorkspaceOpenRouterCustomModel(deps.db, {
897
+ accountId: grant.accountId,
898
+ workspaceId,
899
+ customModelId,
900
+ expectedVersion: parsed.data.expectedVersion,
901
+ operationId: parsed.data.operationId,
902
+ requestHash: workspaceCustomModelRequestHash({
903
+ action: "delete",
904
+ customModelId,
905
+ expectedVersion: parsed.data.expectedVersion,
906
+ }),
907
+ });
908
+ if (removed.outcome === "not_found") {
909
+ throw new HTTPException(404, {
910
+ message: "OpenRouter custom model not found",
911
+ });
912
+ }
913
+ if (removed.outcome === "conflict") {
914
+ throw new HTTPException(409, {
915
+ message: "OpenRouter custom model changed; reload and retry",
916
+ });
917
+ }
918
+ return c.body(null, 204);
919
+ });
920
+
196
921
  app.get("/v1/workspaces/:workspaceId/realtime-model-catalog", async (c) => {
197
922
  const workspaceId = c.req.param("workspaceId");
198
923
  const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
@@ -218,7 +943,7 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
218
943
  description: model.description,
219
944
  ...availability(
220
945
  Boolean(deps.settings.vercelAiGatewayApiKey),
221
- "OpenGeni Gateway voice is not configured",
946
+ "Opengeni Gateway voice is not configured",
222
947
  ),
223
948
  recommended: index === 0,
224
949
  })),
@@ -262,35 +987,83 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
262
987
  });
263
988
 
264
989
  // Full replace (PUT, not merge): null/omitted = unrestricted for that
265
- // dimension; an empty array is a valid explicit total block. Admin access —
266
- // this decides whether turns can reach paid providers, so it is the same
267
- // trust level as billing-affecting workspace settings.
990
+ // dimension; an empty array is a valid explicit total block. Settings access
991
+ // admits workspace administrators and the verified Personal owner, without
992
+ // widening membership or API-key delegation authority.
268
993
  app.put("/v1/workspaces/:workspaceId/model-policy", async (c) => {
269
994
  const workspaceId = c.req.param("workspaceId");
270
- const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
271
- const payload = UpdateWorkspaceModelPolicyRequest.parse(await c.req.json());
995
+ const grant = await requireWorkspaceSettingsGrant(c, deps, workspaceId);
996
+ const payload = await parseRequestJson(c, UpdateWorkspaceModelPolicyRequest);
997
+ const catalog = await resolveWorkspaceCatalogSettings(deps.db, deps.settings, {
998
+ accountId: grant.accountId,
999
+ workspaceId,
1000
+ });
272
1001
  const policy = await upsertWorkspaceModelPolicy(deps.db, {
273
1002
  accountId: grant.accountId,
274
1003
  workspaceId,
275
1004
  allowedProviders: payload.allowedProviders ?? null,
276
- allowedModels: canonicalWorkspacePolicyModelIds(deps.settings, payload.allowedModels),
1005
+ allowedModels: canonicalWorkspacePolicyModelIds(catalog.settings, payload.allowedModels),
277
1006
  });
278
1007
  return c.json(policy);
279
1008
  });
280
1009
 
281
- app.post("/v1/workspaces/:workspaceId/inference-control", async (c) => {
1010
+ app.post("/v1/workspaces/:workspaceId/pause-timer", async (c) => {
282
1011
  const workspaceId = c.req.param("workspaceId");
283
- const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
1012
+ const grant = await requireWorkspaceSettingsGrant(c, deps, workspaceId);
284
1013
  if (workspaceControlUtf8Bytes(grant.subjectId) > WORKSPACE_CONTROL_ACTOR_MAX_BYTES) {
285
1014
  throw new HTTPException(400, { message: "workspace-control actor is too large" });
286
1015
  }
1016
+ const parsed = WorkspacePauseTimerRequest.safeParse(await c.req.json().catch(() => null));
1017
+ if (!parsed.success) throw new HTTPException(400, { message: "Invalid pause timer" });
1018
+ try {
1019
+ await controlHumanWorkspaceTimer(
1020
+ {
1021
+ db: deps.db,
1022
+ bus: deps.bus,
1023
+ workflowClient: deps.workflowClient,
1024
+ ...(deps.schedulePromptPostCommit
1025
+ ? { schedulePromptPostCommit: deps.schedulePromptPostCommit }
1026
+ : {}),
1027
+ },
1028
+ { accountId: grant.accountId, workspaceId, subjectId: grant.subjectId },
1029
+ parsed.data,
1030
+ );
1031
+ } catch (error) {
1032
+ if (error instanceof SessionControlConflictError)
1033
+ throw new HTTPException(409, {
1034
+ message: "Workspace changed. Reopen the timer and try again.",
1035
+ });
1036
+ if (error instanceof WorkspacePauseTimerInputError)
1037
+ throw new HTTPException(400, { message: error.message });
1038
+ throw error;
1039
+ }
1040
+ return c.json({ ok: true });
1041
+ });
1042
+
1043
+ app.post("/v1/workspaces/:workspaceId/inference-control", async (c) => {
1044
+ const workspaceId = c.req.param("workspaceId");
1045
+ const grant = await requireWorkspaceSettingsGrant(c, deps, workspaceId);
1046
+ if (workspaceControlUtf8Bytes(grant.subjectId) > WORKSPACE_CONTROL_ACTOR_MAX_BYTES) {
1047
+ throw new HTTPException(400, {
1048
+ message: "workspace-control actor is too large",
1049
+ });
1050
+ }
287
1051
  const parsed = WorkspaceInferenceControlRequest.safeParse(await c.req.json().catch(() => null));
288
1052
  if (!parsed.success) {
289
- throw new HTTPException(400, { message: "invalid workspace inference-control request" });
1053
+ throw new HTTPException(400, {
1054
+ message: "invalid workspace inference-control request",
1055
+ });
290
1056
  }
291
1057
  return c.json(
292
1058
  await controlHumanWorkspace(
293
- { db: deps.db, bus: deps.bus, workflowClient: deps.workflowClient },
1059
+ {
1060
+ db: deps.db,
1061
+ bus: deps.bus,
1062
+ workflowClient: deps.workflowClient,
1063
+ ...(deps.schedulePromptPostCommit
1064
+ ? { schedulePromptPostCommit: deps.schedulePromptPostCommit }
1065
+ : {}),
1066
+ },
294
1067
  { accountId: grant.accountId, workspaceId, subjectId: grant.subjectId },
295
1068
  parsed.data,
296
1069
  ),
@@ -324,18 +1097,27 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
324
1097
  workspaceId,
325
1098
  after,
326
1099
  c.req.raw.signal,
327
- { observability: deps.observability },
1100
+ {
1101
+ ...browserSseDeliveryOptions(c.req.query("transport")),
1102
+ observability: deps.observability,
1103
+ actorEpoch: getManagedAuthRequestActorEpoch(c.req.raw) ?? undefined,
1104
+ reauthorize: async () => {
1105
+ await requireFreshAccessGrant(c, deps, workspaceId, "workspace:read");
1106
+ },
1107
+ },
328
1108
  );
329
1109
  });
330
1110
 
331
1111
  app.put("/v1/workspaces/:workspaceId/default-rig", async (c) => {
332
1112
  const workspaceId = c.req.param("workspaceId");
333
1113
  await requireAccessGrant(c, deps, workspaceId, "rigs:manage");
334
- const payload = SetWorkspaceDefaultRigRequest.parse(await c.req.json());
1114
+ const payload = await parseRequestJson(c, SetWorkspaceDefaultRigRequest);
335
1115
  if (payload.rigId) {
336
1116
  const rig = await getRig(deps.db, workspaceId, payload.rigId);
337
1117
  if (!rig) {
338
- throw new HTTPException(422, { message: `unknown rigId: ${payload.rigId}` });
1118
+ throw new HTTPException(422, {
1119
+ message: `unknown rigId: ${payload.rigId}`,
1120
+ });
339
1121
  }
340
1122
  }
341
1123
  const workspace = await setWorkspaceDefaultRig(deps.db, workspaceId, payload.rigId);
@@ -345,51 +1127,10 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
345
1127
  app.delete("/v1/workspaces/:workspaceId", async (c) => {
346
1128
  const workspaceId = c.req.param("workspaceId");
347
1129
  const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
348
- // The DB transaction locks the account/workspace and every existing
349
- // session/lease before checking runtime quiescence, then returns the exact
350
- // external schedules removed by the cascade. A racing cold->warm transition
351
- // can therefore never erase the only provider/capture ownership receipt.
352
- const deleted = await deleteWorkspaceIfQuiescent(deps.db, {
1130
+ await deleteWorkspaceForRequest(deps, {
353
1131
  accountId: grant.accountId,
354
1132
  workspaceId,
355
1133
  });
356
- if (deleted.status === "not_found") {
357
- throw new HTTPException(404, { message: "workspace not found" });
358
- }
359
- if (deleted.status === "only_workspace") {
360
- throw new HTTPException(409, { message: "cannot delete the account's only workspace" });
361
- }
362
- if (deleted.status === "active_sessions") {
363
- throw new HTTPException(409, {
364
- message: "stop the workspace's running sessions before deleting it",
365
- });
366
- }
367
- if (deleted.status === "active_video_generations") {
368
- throw new HTTPException(409, {
369
- message: "wait for the workspace's active video generations to finish before deleting it",
370
- });
371
- }
372
- if (deleted.status === "live_sandboxes") {
373
- throw new HTTPException(409, {
374
- message: "wait for the workspace's active sandboxes to finish draining before deleting it",
375
- });
376
- }
377
- if (deleted.status !== "deleted") {
378
- throw new Error(`Unhandled workspace deletion outcome: ${deleted.status}`);
379
- }
380
- // The cleanup claims were inserted in the same transaction as the cascade.
381
- // Try them immediately; failures are released to the replica-safe outbox
382
- // pump, so a process crash or Temporal outage cannot orphan the schedules.
383
- await processTemporalScheduleCleanupClaims(
384
- {
385
- db: deps.db,
386
- deleteSchedule: async (temporalScheduleId) => {
387
- await deps.workflowClient.deleteScheduledTaskSchedule({ temporalScheduleId });
388
- },
389
- ...(deps.observability ? { observability: deps.observability } : {}),
390
- },
391
- deleted.temporalScheduleCleanups,
392
- );
393
1134
  return c.body(null, 204);
394
1135
  });
395
1136
 
@@ -398,28 +1139,75 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
398
1139
  app.get("/v1/workspaces/:workspaceId/members", async (c) => {
399
1140
  const workspaceId = c.req.param("workspaceId");
400
1141
  await requireAccessGrant(c, deps, workspaceId, "workspace:read");
401
- const members = await listWorkspaceMembers(deps.db, workspaceId);
402
- return c.json(ListWorkspaceMembersResponse.parse({ members }));
1142
+ const members = await listWorkspacePeople(deps, workspaceId);
1143
+ return c.json(workspaceMembersResponse(members));
1144
+ });
1145
+
1146
+ app.get("/v1/workspaces/:workspaceId/member-candidates", async (c) => {
1147
+ const workspaceId = c.req.param("workspaceId");
1148
+ const grant = await requireAccessGrant(c, deps, workspaceId, "members:manage");
1149
+ try {
1150
+ return c.json(
1151
+ ListWorkspaceMemberCandidatesResponse.parse({
1152
+ members: await listWorkspaceMemberManagementCandidates(deps.db, {
1153
+ accountId: grant.accountId,
1154
+ workspaceId,
1155
+ actorSubjectId: grant.subjectId,
1156
+ }),
1157
+ }),
1158
+ );
1159
+ } catch (error) {
1160
+ rethrowWorkspaceMemberCandidateError(error);
1161
+ }
403
1162
  });
404
1163
 
405
1164
  app.post("/v1/workspaces/:workspaceId/members", async (c) => {
406
1165
  const workspaceId = c.req.param("workspaceId");
407
1166
  const grant = await requireAccessGrant(c, deps, workspaceId, "members:manage");
408
- const payload = AddWorkspaceMemberRequest.parse(await c.req.json());
409
- const email = payload.email.trim();
410
- // Email invites for not-yet-registered users are deferred: an unknown email
411
- // resolves to null, which resolveMemberSubjectId turns into a 404.
412
- const subjectId = resolveMemberSubjectId(await getManagedUserByEmail(deps.db, email));
413
- await grantWorkspaceAccess(deps.db, {
414
- accountId: grant.accountId,
415
- workspaceId,
416
- subjectId,
417
- subjectLabel: email,
418
- ...(payload.role !== undefined ? { role: payload.role } : {}),
419
- permissions: payload.permissions,
420
- });
421
- const members = await listWorkspaceMembers(deps.db, workspaceId);
422
- const member = members.find((candidate) => candidate.subjectId === subjectId);
1167
+ const payload = await parseRequestJson(c, AddWorkspaceMemberRequest);
1168
+ requireApiKeyDelegationContext(await requireAccessContext(c, deps), payload.permissions);
1169
+ requireExplicitPermissionDelegation(grant, payload.permissions);
1170
+ let candidates: WorkspaceMemberCandidate[];
1171
+ try {
1172
+ candidates = await listWorkspaceMemberManagementCandidates(deps.db, {
1173
+ accountId: grant.accountId,
1174
+ workspaceId,
1175
+ actorSubjectId: grant.subjectId,
1176
+ });
1177
+ } catch (error) {
1178
+ rethrowWorkspaceMemberCandidateError(error);
1179
+ }
1180
+ const candidate = candidates.find(
1181
+ (entry) => entry.organizationMembershipId === payload.organizationMembershipId,
1182
+ );
1183
+ if (!candidate) {
1184
+ throw new HTTPException(404, {
1185
+ message: "this organization member is not available to add",
1186
+ });
1187
+ }
1188
+ const subjectId = candidate.subjectId;
1189
+ const existing = await listWorkspaceMembers(deps.db, workspaceId);
1190
+ if (existing.some((member) => member.subjectId === subjectId)) {
1191
+ throw new HTTPException(409, {
1192
+ message: "this person already has access to the workspace",
1193
+ });
1194
+ }
1195
+ try {
1196
+ await upsertWorkspaceMemberAsWorkspaceManager(deps.db, {
1197
+ accountId: grant.accountId,
1198
+ workspaceId,
1199
+ actorSubjectId: grant.subjectId,
1200
+ targetSubjectId: subjectId,
1201
+ mode: "add",
1202
+ subjectLabel: candidate.name?.trim() || candidate.email || subjectId,
1203
+ role: payload.role ?? "member",
1204
+ permissions: payload.permissions,
1205
+ });
1206
+ } catch (error) {
1207
+ rethrowWorkspaceMemberCandidateError(error);
1208
+ }
1209
+ const members = await listWorkspacePeople(deps, workspaceId);
1210
+ const member = members.find((addedMember) => addedMember.subjectId === subjectId);
423
1211
  if (!member) {
424
1212
  throw new HTTPException(500, { message: "failed to add member" });
425
1213
  }
@@ -430,21 +1218,35 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
430
1218
  const workspaceId = c.req.param("workspaceId");
431
1219
  const grant = await requireAccessGrant(c, deps, workspaceId, "members:manage");
432
1220
  const subjectId = decodeURIComponent(c.req.param("subjectId"));
433
- const payload = UpdateWorkspaceMemberRequest.parse(await c.req.json());
434
- const existing = await listWorkspaceMembers(deps.db, workspaceId);
1221
+ const payload = await parseRequestJson(c, UpdateWorkspaceMemberRequest);
1222
+ requireApiKeyDelegationContext(await requireAccessContext(c, deps), payload.permissions);
1223
+ requireExplicitPermissionDelegation(grant, payload.permissions);
1224
+ const existing = await listWorkspacePeople(deps, workspaceId);
435
1225
  const current = existing.find((member) => member.subjectId === subjectId);
436
1226
  if (!current) {
437
1227
  throw new HTTPException(404, { message: "member not found" });
438
1228
  }
439
- await grantWorkspaceAccess(deps.db, {
440
- accountId: grant.accountId,
441
- workspaceId,
1229
+ assertWorkspaceMemberUpdateAllowed({
1230
+ members: existing,
442
1231
  subjectId,
443
- ...(current.subjectLabel ? { subjectLabel: current.subjectLabel } : {}),
444
- role: payload.role ?? current.role,
445
- permissions: payload.permissions,
1232
+ callerSubjectId: grant.subjectId,
1233
+ nextPermissions: payload.permissions,
446
1234
  });
447
- const members = await listWorkspaceMembers(deps.db, workspaceId);
1235
+ try {
1236
+ await upsertWorkspaceMemberAsWorkspaceManager(deps.db, {
1237
+ accountId: grant.accountId,
1238
+ workspaceId,
1239
+ actorSubjectId: grant.subjectId,
1240
+ targetSubjectId: subjectId,
1241
+ mode: "update",
1242
+ ...(current.subjectLabel ? { subjectLabel: current.subjectLabel } : {}),
1243
+ role: payload.role ?? current.role,
1244
+ permissions: payload.permissions,
1245
+ });
1246
+ } catch (error) {
1247
+ rethrowWorkspaceMemberCandidateError(error);
1248
+ }
1249
+ const members = await listWorkspacePeople(deps, workspaceId);
448
1250
  const member = members.find((candidate) => candidate.subjectId === subjectId);
449
1251
  if (!member) {
450
1252
  throw new HTTPException(500, { message: "failed to update member" });
@@ -458,12 +1260,85 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
458
1260
  const subjectId = decodeURIComponent(c.req.param("subjectId"));
459
1261
  const members = await listWorkspaceMembers(deps.db, workspaceId);
460
1262
  // Never remove yourself, and never remove the last administering member.
461
- assertWorkspaceMemberRemovable({ members, subjectId, callerSubjectId: grant.subjectId });
462
- await removeWorkspaceMember(deps.db, workspaceId, subjectId);
1263
+ // The fenced removal command re-enforces both guards fail-closed.
1264
+ assertWorkspaceMemberRemovable({
1265
+ members,
1266
+ subjectId,
1267
+ callerSubjectId: grant.subjectId,
1268
+ });
1269
+ await removeWorkspaceMember(deps.db, {
1270
+ accountId: grant.accountId,
1271
+ workspaceId,
1272
+ actorSubjectId: grant.subjectId,
1273
+ targetSubjectId: subjectId,
1274
+ });
463
1275
  return c.body(null, 204);
464
1276
  });
465
1277
  }
466
1278
 
1279
+ function workspaceLimit(deps: ApiRouteDeps): number | null {
1280
+ if (deps.settings.usageLimitsMode !== "static" && deps.settings.usageLimitsMode !== "managed") {
1281
+ return null;
1282
+ }
1283
+ return configuredStaticUsageLimits(deps.settings).maxWorkspacesPerAccount ?? null;
1284
+ }
1285
+
1286
+ async function listWorkspacePeople(
1287
+ deps: ApiRouteDeps,
1288
+ workspaceId: string,
1289
+ ): Promise<WorkspaceMemberValue[]> {
1290
+ const members = await listWorkspaceMembers(deps.db, workspaceId);
1291
+ const userIds = members.flatMap((member) =>
1292
+ member.subjectId.startsWith("user:") ? [member.subjectId.slice("user:".length)] : [],
1293
+ );
1294
+ const profiles = await getManagedUserProfilesByIds(deps.db, userIds);
1295
+ const profileBySubject = new Map(
1296
+ profiles.map((profile) => [`user:${profile.id}`, profile.name?.trim() || profile.email]),
1297
+ );
1298
+ return members.map((member) => ({
1299
+ ...member,
1300
+ subjectLabel: profileBySubject.get(member.subjectId) ?? member.subjectLabel,
1301
+ }));
1302
+ }
1303
+
1304
+ function rethrowWorkspaceMemberCandidateError(error: unknown): never {
1305
+ const managementCode =
1306
+ error && typeof error === "object" && "code" in error ? error.code : undefined;
1307
+ if (managementCode === "WORKSPACE_MEMBER_ALREADY_EXISTS") {
1308
+ throw new HTTPException(409, {
1309
+ message: "this person already has access to the workspace",
1310
+ });
1311
+ }
1312
+ if (managementCode === "WORKSPACE_MEMBER_NOT_FOUND") {
1313
+ throw new HTTPException(404, { message: "member not found" });
1314
+ }
1315
+ if (managementCode === "WORKSPACE_MEMBER_SELF_UPDATE") {
1316
+ throw new HTTPException(409, {
1317
+ message: "you cannot change your own workspace access",
1318
+ });
1319
+ }
1320
+ if (managementCode === "WORKSPACE_MEMBER_LAST_ADMIN") {
1321
+ throw new HTTPException(409, {
1322
+ message: "the workspace must keep at least one administrator",
1323
+ });
1324
+ }
1325
+ const sqlState = nestedPostgresSqlState(error);
1326
+ if (sqlState === "P0002") {
1327
+ throw new HTTPException(404, { message: "workspace not found" });
1328
+ }
1329
+ if (sqlState === "42501") {
1330
+ throw new HTTPException(403, {
1331
+ message: "workspace member management is not allowed",
1332
+ });
1333
+ }
1334
+ if (sqlState === "54000") {
1335
+ throw new HTTPException(409, {
1336
+ message: "this organization has too many members to show here",
1337
+ });
1338
+ }
1339
+ throw error;
1340
+ }
1341
+
467
1342
  // A persona override that is null or trims to empty collapses to null (use the
468
1343
  // deployment default). Otherwise the template is stored verbatim so the runtime
469
1344
  // can substitute the non-bypassable CORE at its {{core}} marker.
@@ -485,6 +1360,8 @@ function requireAccountPermission(
485
1360
  !grant ||
486
1361
  (!grant.permissions.includes(permission) && !grant.permissions.includes("account:admin"))
487
1362
  ) {
488
- throw new HTTPException(403, { message: `missing permission: ${permission}` });
1363
+ throw new HTTPException(403, {
1364
+ message: `missing permission: ${permission}`,
1365
+ });
489
1366
  }
490
1367
  }