@opengeni/api-router 0.30.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (436) hide show
  1. package/dist/access-grant-rls.d.ts +3 -0
  2. package/dist/admission-trace.d.ts +8 -0
  3. package/dist/api-websocket.d.ts +18 -0
  4. package/dist/app.d.ts +50 -2
  5. package/dist/app.js +12 -1
  6. package/dist/artifact-catalog-cursor.d.ts +27 -0
  7. package/dist/auth/managed-auth-attempt-context.d.ts +10 -0
  8. package/dist/auth/managed-auth-rate-limits.d.ts +167 -0
  9. package/dist/auth/managed-auth-session-adapter.d.ts +6 -0
  10. package/dist/auth/managed-auth.d.ts +102 -3
  11. package/dist/auth/managed-email.d.ts +29 -0
  12. package/dist/auth/managed-sign-in-notifications.d.ts +4 -0
  13. package/dist/auth/new-signups-gate.d.ts +23 -0
  14. package/dist/auth/organization-user-setup.d.ts +80 -0
  15. package/dist/auth/signup-funnel-metrics.d.ts +44 -0
  16. package/dist/browser-controller-authority.d.ts +1 -61
  17. package/dist/browser-ephemeral.d.ts +5 -0
  18. package/dist/browser-state-authority.d.ts +1 -27
  19. package/dist/chunk-6TZ5LG3O.js +123948 -0
  20. package/dist/chunk-6TZ5LG3O.js.map +1 -0
  21. package/dist/chunk-OGQDXVXY.js +18 -0
  22. package/dist/chunk-OGQDXVXY.js.map +1 -0
  23. package/dist/claude-subscription-account-conflicts.d.ts +1 -0
  24. package/dist/claude-subscription-account-usage.d.ts +39 -0
  25. package/dist/claude-subscription-oauth.d.ts +25 -0
  26. package/dist/claude-subscription-profile.d.ts +9 -0
  27. package/dist/claude-subscription-usage.d.ts +44 -0
  28. package/dist/claude-workspace-connection.d.ts +15 -0
  29. package/dist/codemode.d.ts +32 -1
  30. package/dist/company-brain-okf.d.ts +24 -0
  31. package/dist/connected-machine-computer-access.d.ts +11 -0
  32. package/dist/connection-ownership.d.ts +54 -0
  33. package/dist/controller-data-plane.d.ts +32 -0
  34. package/dist/editable-artifact-office-import.d.ts +7 -4
  35. package/dist/editable-artifact-source-session.d.ts +3 -0
  36. package/dist/editable-artifact-websocket.d.ts +10 -18
  37. package/dist/editable-artifact-workspace-files.d.ts +1 -1
  38. package/dist/fatal-process-boundary.d.ts +25 -0
  39. package/dist/github-access.d.ts +31 -2
  40. package/dist/http/acting-person.d.ts +24 -0
  41. package/dist/http/api-contract-compat.d.ts +19 -0
  42. package/dist/http/api-error.d.ts +27 -1
  43. package/dist/http/cors.d.ts +17 -0
  44. package/dist/http/deprecation.d.ts +42 -0
  45. package/dist/http/interaction-control-error.d.ts +13 -0
  46. package/dist/http/keyed-admission.d.ts +12 -0
  47. package/dist/http/local-browser-boundary.d.ts +72 -0
  48. package/dist/http/metrics-listener.d.ts +22 -0
  49. package/dist/http/path-identifier.d.ts +17 -0
  50. package/dist/http/registered-route-label.d.ts +16 -0
  51. package/dist/http/request-body.d.ts +27 -0
  52. package/dist/http/request-source.d.ts +52 -0
  53. package/dist/http/sse.d.ts +17 -6
  54. package/dist/http/unmatched-route.d.ts +18 -0
  55. package/dist/http/user-content.d.ts +36 -0
  56. package/dist/http/websocket-upgrade-dispatch.d.ts +18 -0
  57. package/dist/index.d.ts +10 -3
  58. package/dist/index.js +1129 -166
  59. package/dist/index.js.map +1 -1
  60. package/dist/integration-endpoint-test.d.ts +20 -0
  61. package/dist/integrations/api-integrations.d.ts +2 -2
  62. package/dist/integrations/atlassian.d.ts +29 -3
  63. package/dist/integrations/connect-authority.d.ts +1 -0
  64. package/dist/integrations/connect-callback-return.d.ts +6 -0
  65. package/dist/integrations/fiken.d.ts +16 -2
  66. package/dist/integrations/github-app-connect.d.ts +47 -0
  67. package/dist/integrations/github-installation-proof.d.ts +3 -0
  68. package/dist/integrations/github-lens-connect.d.ts +116 -0
  69. package/dist/integrations/github-skill-source.d.ts +1 -5
  70. package/dist/integrations/google-drive.d.ts +25 -0
  71. package/dist/integrations/integration-commit-authority.d.ts +13 -0
  72. package/dist/integrations/marketplace-plugin.d.ts +2691 -0
  73. package/dist/integrations/oauth-client.d.ts +178 -18
  74. package/dist/integrations/oauth-profiles.d.ts +222 -0
  75. package/dist/integrations/oauth-return-path.d.ts +1 -0
  76. package/dist/integrations/personal-github-repositories.d.ts +84 -0
  77. package/dist/integrations/personal-github.d.ts +42 -0
  78. package/dist/integrations/pr-review-provider.d.ts +22 -0
  79. package/dist/integrations/provider-oauth.d.ts +19 -1
  80. package/dist/integrations/slack-app-home.d.ts +24 -0
  81. package/dist/integrations/slack-bot.d.ts +367 -12
  82. package/dist/integrations/slack-file-upload-flow.d.ts +62 -0
  83. package/dist/integrations/slack-install.d.ts +20 -0
  84. package/dist/integrations/slack-interactions.d.ts +113 -10
  85. package/dist/integrations/slack-mrkdwn.d.ts +41 -0
  86. package/dist/integrations/slack-routing.d.ts +120 -0
  87. package/dist/integrations/slack-session-defaults.d.ts +58 -0
  88. package/dist/integrations/slack-task-file-upload.d.ts +36 -0
  89. package/dist/integrations/social-oauth.d.ts +7 -0
  90. package/dist/interaction-agent-access.d.ts +15 -0
  91. package/dist/interaction-frame-proxy.d.ts +68 -0
  92. package/dist/interaction-metrics.d.ts +2 -0
  93. package/dist/mcp/action-catalog-types.d.ts +9 -0
  94. package/dist/mcp/action-catalog.gen.d.ts +2 -0
  95. package/dist/mcp/capability-account-readiness.d.ts +7 -0
  96. package/dist/mcp/child-read-evidence.d.ts +9 -0
  97. package/dist/mcp/command-read.d.ts +24 -0
  98. package/dist/mcp/company-profile-agent-admin.d.ts +44 -0
  99. package/dist/mcp/contract-input.d.ts +11 -0
  100. package/dist/mcp/documents.d.ts +7 -5
  101. package/dist/mcp/files.d.ts +2 -2
  102. package/dist/mcp/first-party-tool-permissions.d.ts +650 -0
  103. package/dist/mcp/knowledge-entries.d.ts +5 -0
  104. package/dist/mcp/orchestration-failure-diagnostic.d.ts +43 -0
  105. package/dist/mcp/receipts.d.ts +9 -0
  106. package/dist/mcp/request-abort.d.ts +19 -0
  107. package/dist/mcp/scheduled-task-input.d.ts +1189 -0
  108. package/dist/mcp/scheduled-task-view.d.ts +22 -4
  109. package/dist/mcp/server.d.ts +255 -3
  110. package/dist/mcp/session-event-view.d.ts +94 -0
  111. package/dist/mcp/session-view.d.ts +66 -2
  112. package/dist/mcp/session-wait.d.ts +174 -0
  113. package/dist/mcp-oauth.d.ts +52 -0
  114. package/dist/model-catalog.d.ts +11 -31
  115. package/dist/observability.d.ts +3 -1
  116. package/dist/organization-mcp.d.ts +52 -0
  117. package/dist/organization-recovery-notifications.d.ts +41 -0
  118. package/dist/routes/analytics-consent.d.ts +26 -0
  119. package/dist/routes/api-integrations.d.ts +25 -0
  120. package/dist/routes/api-keys.d.ts +17 -0
  121. package/dist/routes/artifact-catalog.d.ts +6 -0
  122. package/dist/routes/automations.d.ts +19 -0
  123. package/dist/routes/billing.d.ts +49 -0
  124. package/dist/routes/browser-sessions.d.ts +71 -3
  125. package/dist/routes/claude-subscription-accounts.d.ts +3 -0
  126. package/dist/routes/claude-subscription-oauth.d.ts +3 -0
  127. package/dist/routes/client-errors.d.ts +146 -0
  128. package/dist/routes/codex.d.ts +76 -2
  129. package/dist/routes/company-brain.d.ts +3 -0
  130. package/dist/routes/company-profile.d.ts +2 -1
  131. package/dist/routes/computer-sessions.d.ts +14 -0
  132. package/dist/routes/connect.d.ts +5 -0
  133. package/dist/routes/editable-artifacts.d.ts +2 -0
  134. package/dist/routes/external-identity-links.d.ts +6 -0
  135. package/dist/routes/feedback.d.ts +3 -0
  136. package/dist/routes/files.d.ts +4 -0
  137. package/dist/routes/github-browser-pages.d.ts +12 -0
  138. package/dist/routes/github.d.ts +2 -0
  139. package/dist/routes/insights-response-cache.d.ts +10 -0
  140. package/dist/routes/insights-usage.d.ts +12 -0
  141. package/dist/routes/insights.d.ts +30 -1
  142. package/dist/routes/knowledge.d.ts +3 -0
  143. package/dist/routes/managed-auth-session-sets.d.ts +26 -0
  144. package/dist/routes/managed-onboarding.d.ts +29 -0
  145. package/dist/routes/managed-sign-in-methods.d.ts +6 -0
  146. package/dist/routes/model-connection-access.d.ts +3 -0
  147. package/dist/routes/organization-integration-policy.d.ts +3 -0
  148. package/dist/routes/organization-integrations.d.ts +6 -0
  149. package/dist/routes/organization-mcp-connections.d.ts +3 -0
  150. package/dist/routes/organization-model-providers.d.ts +3 -0
  151. package/dist/routes/organization-recovery.d.ts +19 -0
  152. package/dist/routes/organization-service-accounts.d.ts +3 -0
  153. package/dist/routes/organization-sessions.d.ts +26 -0
  154. package/dist/routes/personal-github-git-broker.d.ts +29 -0
  155. package/dist/routes/personal-github.d.ts +3 -0
  156. package/dist/routes/pr-review-github.d.ts +3 -0
  157. package/dist/routes/pr-review.d.ts +3 -0
  158. package/dist/routes/session-artifact-associations.d.ts +4 -0
  159. package/dist/routes/session-history-imports.d.ts +9 -0
  160. package/dist/routes/sessions.d.ts +44 -6
  161. package/dist/routes/skill-content.d.ts +4 -0
  162. package/dist/routes/skill-install-authority.d.ts +6 -0
  163. package/dist/routes/subscription-account-pools.d.ts +34 -0
  164. package/dist/routes/subscription-pool-access.d.ts +16 -0
  165. package/dist/routes/supergrok.d.ts +6 -1
  166. package/dist/routes/usage-allowances.d.ts +12 -0
  167. package/dist/routes/user-resource-authorities.d.ts +3 -0
  168. package/dist/routes/workspace-artifacts.d.ts +2 -1
  169. package/dist/routes/workspace-capture.d.ts +10 -2
  170. package/dist/routes/workspace-integrations.d.ts +35 -0
  171. package/dist/routes/workspace-learning.d.ts +3 -0
  172. package/dist/routes/workspace-model-providers.d.ts +4 -0
  173. package/dist/routes/workspaces.d.ts +29 -0
  174. package/dist/sandbox/auth-callout.d.ts +8 -0
  175. package/dist/sandbox/channel-a.d.ts +19 -1
  176. package/dist/sandbox/connection-authority.d.ts +3 -0
  177. package/dist/sandbox/enrollment.d.ts +9 -1
  178. package/dist/sandbox/machines.d.ts +5 -2
  179. package/dist/sandbox/metrics-ingestion.d.ts +28 -20
  180. package/dist/sandbox/rematerialize.d.ts +2 -0
  181. package/dist/sandbox/session-attach-variable-sets.d.ts +30 -0
  182. package/dist/sandbox/viewer.d.ts +26 -1
  183. package/dist/sandbox-file-artifacts.d.ts +50 -0
  184. package/dist/scheduled-task-deletion.d.ts +13 -0
  185. package/dist/session-create-observability.d.ts +5 -0
  186. package/dist/session-event-view-MPS2OZD3.js +292 -0
  187. package/dist/session-event-view-MPS2OZD3.js.map +1 -0
  188. package/dist/site-session-origin.d.ts +13 -0
  189. package/dist/site-uploads.d.ts +41 -0
  190. package/dist/slack-reaction-files.d.ts +1 -1
  191. package/dist/temporal-schedule-cleanup.d.ts +1 -0
  192. package/dist/temporal-schedule-sync.d.ts +26 -0
  193. package/dist/transcription/providers/xai-subscription.d.ts +1 -0
  194. package/dist/transcription/service.d.ts +2 -1
  195. package/dist/work-discovery-observability.d.ts +33 -0
  196. package/dist/workspace-artifact-content.d.ts +28 -0
  197. package/dist/workspace-artifact-provenance.d.ts +7 -0
  198. package/dist/workspace-delete-observability.d.ts +10 -0
  199. package/dist/workspace-deletion.d.ts +6 -0
  200. package/dist/workspace-state-projection.d.ts +3 -5
  201. package/dist/workspace-tool-gateway-observability.d.ts +17 -0
  202. package/dist/workspace-tool-gateway.d.ts +123 -0
  203. package/dist/workspace-webhook-dispatch.d.ts +26 -0
  204. package/package.json +23 -19
  205. package/src/access-grant-rls.ts +75 -0
  206. package/src/admission-trace.ts +14 -0
  207. package/src/api-websocket.ts +23 -0
  208. package/src/app.ts +1974 -256
  209. package/src/artifact-catalog-cursor.ts +60 -0
  210. package/src/auth/managed-auth-attempt-context.ts +70 -0
  211. package/src/auth/managed-auth-rate-limits.ts +252 -0
  212. package/src/auth/managed-auth-session-adapter.ts +222 -0
  213. package/src/auth/managed-auth.ts +536 -60
  214. package/src/auth/managed-email.ts +174 -0
  215. package/src/auth/managed-sign-in-notifications.ts +81 -0
  216. package/src/auth/new-signups-gate.ts +49 -0
  217. package/src/auth/organization-user-setup.ts +307 -0
  218. package/src/auth/signup-funnel-metrics.ts +164 -0
  219. package/src/browser-controller-authority.ts +1 -186
  220. package/src/browser-ephemeral.ts +39 -0
  221. package/src/browser-state-authority.ts +1 -236
  222. package/src/claude-subscription-account-conflicts.ts +12 -0
  223. package/src/claude-subscription-account-usage.ts +48 -0
  224. package/src/claude-subscription-oauth.ts +362 -0
  225. package/src/claude-subscription-profile.ts +73 -0
  226. package/src/claude-subscription-usage.ts +97 -0
  227. package/src/claude-workspace-connection.ts +82 -0
  228. package/src/codemode.ts +194 -30
  229. package/src/codex-realtime.ts +8 -2
  230. package/src/company-brain-okf.ts +340 -0
  231. package/src/connected-machine-computer-access.ts +33 -0
  232. package/src/connection-ownership.ts +199 -0
  233. package/src/controller-data-plane.ts +81 -0
  234. package/src/editable-artifact-native-kernel.ts +26 -15
  235. package/src/editable-artifact-office-import.ts +77 -9
  236. package/src/editable-artifact-production.ts +23 -7
  237. package/src/editable-artifact-source-session.ts +123 -0
  238. package/src/editable-artifact-websocket.ts +27 -21
  239. package/src/editable-artifact-workspace-files.ts +31 -6
  240. package/src/fatal-process-boundary.ts +246 -0
  241. package/src/gateway-realtime.ts +1 -1
  242. package/src/github-access.ts +147 -5
  243. package/src/http/acting-person.ts +71 -0
  244. package/src/http/api-contract-compat.ts +49 -0
  245. package/src/http/api-error.ts +115 -1
  246. package/src/http/auth.ts +48 -0
  247. package/src/http/cors.ts +67 -0
  248. package/src/http/deprecation.ts +118 -0
  249. package/src/http/interaction-control-error.ts +164 -0
  250. package/src/http/keyed-admission.ts +29 -0
  251. package/src/http/local-browser-boundary.ts +343 -0
  252. package/src/http/metrics-listener.ts +57 -0
  253. package/src/http/path-identifier.ts +54 -0
  254. package/src/http/registered-route-label.ts +34 -0
  255. package/src/http/request-body.ts +127 -0
  256. package/src/http/request-source.ts +178 -0
  257. package/src/http/sse.ts +486 -132
  258. package/src/http/unmatched-route.ts +60 -0
  259. package/src/http/user-content.ts +158 -0
  260. package/src/http/websocket-upgrade-dispatch.ts +29 -0
  261. package/src/index.ts +157 -52
  262. package/src/integration-endpoint-test.ts +291 -0
  263. package/src/integrations/api-integrations.ts +98 -11
  264. package/src/integrations/atlassian.ts +474 -136
  265. package/src/integrations/connect-authority.ts +2 -0
  266. package/src/integrations/connect-callback-return.ts +86 -0
  267. package/src/integrations/fiken.ts +354 -64
  268. package/src/integrations/github-app-connect.ts +435 -0
  269. package/src/integrations/github-installation-proof.ts +60 -0
  270. package/src/integrations/github-lens-connect.ts +76 -0
  271. package/src/integrations/github-skill-source.ts +2 -142
  272. package/src/integrations/google-drive.ts +820 -222
  273. package/src/integrations/integration-commit-authority.ts +69 -0
  274. package/src/integrations/marketplace-plugin.ts +48 -0
  275. package/src/integrations/oauth-client.ts +1356 -459
  276. package/src/integrations/oauth-profiles.ts +537 -0
  277. package/src/integrations/oauth-return-path.ts +18 -0
  278. package/src/integrations/personal-github-repositories.ts +674 -0
  279. package/src/integrations/personal-github.ts +825 -0
  280. package/src/integrations/pr-review-provider.ts +246 -0
  281. package/src/integrations/provider-oauth.ts +250 -22
  282. package/src/integrations/slack-app-home.ts +300 -0
  283. package/src/integrations/slack-bot.ts +963 -239
  284. package/src/integrations/slack-file-upload-flow.ts +147 -0
  285. package/src/integrations/slack-install.ts +83 -0
  286. package/src/integrations/slack-interactions.ts +3170 -474
  287. package/src/integrations/slack-mrkdwn.ts +193 -0
  288. package/src/integrations/slack-routing.ts +337 -0
  289. package/src/integrations/slack-session-defaults.ts +224 -0
  290. package/src/integrations/slack-task-file-upload.ts +323 -0
  291. package/src/integrations/social-oauth.ts +213 -15
  292. package/src/interaction-agent-access.ts +48 -0
  293. package/src/interaction-frame-proxy.ts +409 -0
  294. package/src/interaction-metrics.ts +30 -0
  295. package/src/mcp/action-catalog-types.ts +9 -0
  296. package/src/mcp/action-catalog.gen.ts +6696 -0
  297. package/src/mcp/capability-account-readiness.ts +17 -0
  298. package/src/mcp/child-read-evidence.ts +19 -0
  299. package/src/mcp/command-read.ts +109 -0
  300. package/src/mcp/company-profile-agent-admin.ts +198 -0
  301. package/src/mcp/contract-input.ts +141 -0
  302. package/src/mcp/documents.ts +65 -377
  303. package/src/mcp/editable-artifacts.ts +12 -6
  304. package/src/mcp/files.ts +80 -38
  305. package/src/mcp/first-party-tool-permissions.ts +305 -0
  306. package/src/mcp/knowledge-entries.ts +322 -0
  307. package/src/mcp/orchestration-failure-diagnostic.ts +43 -0
  308. package/src/mcp/receipts.ts +34 -0
  309. package/src/mcp/request-abort.ts +44 -0
  310. package/src/mcp/scheduled-task-input.ts +76 -0
  311. package/src/mcp/scheduled-task-view.ts +44 -3
  312. package/src/mcp/server.ts +3030 -1267
  313. package/src/mcp/session-event-view.ts +383 -0
  314. package/src/mcp/session-view.ts +84 -3
  315. package/src/mcp/session-wait.ts +665 -0
  316. package/src/mcp-oauth.ts +1123 -0
  317. package/src/model-catalog.ts +91 -333
  318. package/src/observability.ts +15 -1
  319. package/src/organization-mcp.ts +397 -0
  320. package/src/organization-recovery-notifications.ts +103 -0
  321. package/src/routes/analytics-consent.ts +123 -0
  322. package/src/routes/api-integrations.ts +364 -74
  323. package/src/routes/api-keys.ts +336 -10
  324. package/src/routes/artifact-catalog.ts +279 -0
  325. package/src/routes/automations.ts +768 -0
  326. package/src/routes/billing.ts +461 -46
  327. package/src/routes/browser-sessions.ts +839 -131
  328. package/src/routes/canonical-human-identities.ts +90 -14
  329. package/src/routes/capabilities.ts +107 -6
  330. package/src/routes/channels.ts +20 -3
  331. package/src/routes/claude-subscription-accounts.ts +334 -0
  332. package/src/routes/claude-subscription-oauth.ts +142 -0
  333. package/src/routes/client-errors.ts +362 -0
  334. package/src/routes/codex.ts +833 -116
  335. package/src/routes/company-brain.ts +322 -0
  336. package/src/routes/company-profile.ts +65 -1
  337. package/src/routes/computer-sessions.ts +557 -106
  338. package/src/routes/connect.ts +1938 -0
  339. package/src/routes/connections.ts +1040 -349
  340. package/src/routes/documents.ts +488 -287
  341. package/src/routes/editable-artifacts.ts +79 -9
  342. package/src/routes/enrollments.ts +135 -39
  343. package/src/routes/environments.ts +154 -58
  344. package/src/routes/external-identity-links.ts +201 -0
  345. package/src/routes/feedback.ts +113 -0
  346. package/src/routes/files.ts +337 -39
  347. package/src/routes/github-browser-pages.ts +162 -0
  348. package/src/routes/github.ts +446 -144
  349. package/src/routes/insights-response-cache.ts +83 -0
  350. package/src/routes/insights-usage.ts +345 -0
  351. package/src/routes/insights.ts +153 -19
  352. package/src/routes/install.ts +133 -38
  353. package/src/routes/integration-facets.ts +107 -33
  354. package/src/routes/interaction-resources.ts +30 -3
  355. package/src/routes/knowledge.ts +488 -0
  356. package/src/routes/machines.ts +313 -15
  357. package/src/routes/managed-auth-session-sets.ts +1218 -0
  358. package/src/routes/managed-onboarding.ts +337 -0
  359. package/src/routes/managed-sign-in-methods.ts +433 -0
  360. package/src/routes/memory-slack-publications.ts +3 -2
  361. package/src/routes/model-connection-access.ts +208 -0
  362. package/src/routes/organization-integration-policy.ts +116 -0
  363. package/src/routes/organization-integrations.ts +326 -0
  364. package/src/routes/organization-mcp-connections.ts +178 -0
  365. package/src/routes/organization-memberships.ts +1019 -27
  366. package/src/routes/organization-model-providers.ts +293 -0
  367. package/src/routes/organization-recovery.ts +325 -0
  368. package/src/routes/organization-service-accounts.ts +128 -0
  369. package/src/routes/organization-sessions.ts +350 -0
  370. package/src/routes/personal-github-git-broker.ts +785 -0
  371. package/src/routes/personal-github.ts +370 -0
  372. package/src/routes/plugins.ts +251 -22
  373. package/src/routes/pr-review-github.ts +881 -0
  374. package/src/routes/pr-review.ts +622 -0
  375. package/src/routes/preference-registry.ts +12 -93
  376. package/src/routes/rigs.ts +167 -50
  377. package/src/routes/scheduled-tasks.ts +346 -41
  378. package/src/routes/session-artifact-associations.ts +87 -0
  379. package/src/routes/session-history-imports.ts +280 -0
  380. package/src/routes/sessions.ts +2306 -264
  381. package/src/routes/skill-content.ts +297 -0
  382. package/src/routes/skill-install-authority.ts +64 -0
  383. package/src/routes/skills.ts +62 -9
  384. package/src/routes/social.ts +46 -18
  385. package/src/routes/subscription-account-pools.ts +436 -0
  386. package/src/routes/subscription-pool-access.ts +126 -0
  387. package/src/routes/supergrok.ts +219 -303
  388. package/src/routes/transcription-recordings.ts +38 -3
  389. package/src/routes/transcriptions.ts +4 -0
  390. package/src/routes/usage-allowances.ts +378 -0
  391. package/src/routes/user-resource-authorities.ts +138 -0
  392. package/src/routes/video-generation.ts +14 -10
  393. package/src/routes/workspace-artifacts.ts +275 -69
  394. package/src/routes/workspace-capture.ts +21 -0
  395. package/src/routes/workspace-integrations.ts +576 -0
  396. package/src/routes/workspace-learning.ts +159 -0
  397. package/src/routes/workspace-model-providers.ts +148 -0
  398. package/src/routes/workspace-state.ts +38 -25
  399. package/src/routes/workspaces.ts +1004 -127
  400. package/src/sandbox/auth-callout.ts +96 -19
  401. package/src/sandbox/channel-a.ts +323 -24
  402. package/src/sandbox/connection-authority.ts +3 -0
  403. package/src/sandbox/enrollment.ts +81 -6
  404. package/src/sandbox/machines.ts +218 -76
  405. package/src/sandbox/metrics-ingestion.ts +411 -62
  406. package/src/sandbox/rematerialize.ts +121 -19
  407. package/src/sandbox/session-attach-variable-sets.ts +131 -0
  408. package/src/sandbox/viewer.ts +280 -25
  409. package/src/sandbox-file-artifacts.ts +413 -0
  410. package/src/scheduled-task-deletion.ts +138 -0
  411. package/src/session-create-observability.ts +46 -0
  412. package/src/session-realtime-context.ts +6 -3
  413. package/src/site-session-origin.ts +48 -0
  414. package/src/site-uploads.ts +224 -0
  415. package/src/slack-reaction-files.ts +16 -5
  416. package/src/temporal-schedule-cleanup.ts +13 -0
  417. package/src/temporal-schedule-sync.ts +74 -0
  418. package/src/transcription/providers/codex-subscription.ts +2 -0
  419. package/src/transcription/providers/openai.ts +1 -0
  420. package/src/transcription/providers/xai-subscription.ts +49 -3
  421. package/src/transcription/segmenter.ts +8 -2
  422. package/src/transcription/service.ts +43 -6
  423. package/src/work-discovery-observability.ts +121 -0
  424. package/src/workspace-artifact-content.ts +144 -0
  425. package/src/workspace-artifact-provenance.ts +104 -0
  426. package/src/workspace-delete-observability.ts +75 -0
  427. package/src/workspace-deletion.ts +73 -0
  428. package/src/workspace-state-projection.ts +18 -148
  429. package/src/workspace-tool-gateway-observability.ts +100 -0
  430. package/src/workspace-tool-gateway.ts +870 -0
  431. package/src/workspace-webhook-dispatch.ts +190 -0
  432. package/src/xai-subscription-auth.ts +9 -2
  433. package/dist/chunk-2PQMSRCB.js +0 -49759
  434. package/dist/chunk-2PQMSRCB.js.map +0 -1
  435. package/dist/routes/packs.d.ts +0 -3
  436. package/src/routes/packs.ts +0 -729
@@ -7,14 +7,19 @@
7
7
  // is added to isAuthExempt. Secrets never leave the server: status/usage read the
8
8
  // decrypted token only to call the codex backend; the token is never returned.
9
9
 
10
- import { environmentsEncryptionKeyBytes } from "@opengeni/config";
10
+ import {
11
+ environmentsEncryptionKeyBytes,
12
+ configuredModels,
13
+ getSettings,
14
+ productLabelForModelId,
15
+ withCodexCatalogProvider,
16
+ type Settings,
17
+ } from "@opengeni/config";
11
18
  import {
12
19
  accessTokenExpiry,
13
20
  buildCodexUsageWindowFromCache,
14
21
  CODEX_CLIENT_VERSION,
15
- CODEX_FALLBACK_MODEL_SLUGS,
16
22
  CODEX_FIVE_HOUR_WINDOW_SECONDS,
17
- CODEX_MODEL_ID_PREFIX,
18
23
  CODEX_PROVIDER_ID,
19
24
  CODEX_WEEKLY_WINDOW_SECONDS,
20
25
  CodexDeviceError,
@@ -39,8 +44,10 @@ import {
39
44
  designateCodexAppsCredential,
40
45
  disconnectAllCodexAccounts,
41
46
  disconnectCodexAccount,
47
+ disconnectOrganizationCodexAccount,
42
48
  encryptEnvironmentValue,
43
49
  ensureCodexRotationSettings,
50
+ ensureOrganizationCodexRotationSettings,
44
51
  fetchCodexUsageForAccount,
45
52
  fetchCodexRateLimitResetCreditsForAccount,
46
53
  fenceCodexResetRedemptionSend,
@@ -48,18 +55,30 @@ import {
48
55
  getCodexCredentialStatus,
49
56
  getCodexAppsSettings,
50
57
  getCodexRotationSettings,
58
+ getOrganizationCodexRotationSettings,
59
+ getWorkspaceCodexSubscriptionSource,
51
60
  listPendingCodexCapacityWakeTargets,
52
61
  listCodexAccountStatuses,
62
+ listOrganizationCodexAccountStatuses,
53
63
  listCodexResetRedemptionRecoveries,
64
+ nestedPostgresSqlState,
54
65
  releaseCodexResetRedemptionClaim,
55
66
  updateCodexAllocatorEligibility,
56
67
  loadCodexCredentialForRun,
57
68
  renameCodexAccount,
69
+ renameOrganizationCodexAccount,
58
70
  setActiveCodexCredential,
71
+ setActiveOrganizationCodexCredential,
59
72
  setInitialActiveCodexCredential,
73
+ setWorkspaceCodexSubscriptionMode,
74
+ setWorkspaceCodexSubscriptionModeInTransaction,
75
+ updateOrganizationCodexRotationSettings,
60
76
  updateCodexRotationSettings,
77
+ upsertOrganizationCodexSubscriptionCredential,
61
78
  upsertCodexSubscriptionCredential,
62
79
  withCodexCapacityMutation,
80
+ withSessionCodexCapacityMutation,
81
+ activeCodexPlanExclusions,
63
82
  type CodexAccountStatus,
64
83
  type CodexCapacityWakeTarget,
65
84
  } from "@opengeni/db";
@@ -71,7 +90,7 @@ const CODEX_PROVIDER_LABEL = "Codex subscription · no credits";
71
90
  // The wire shape for one Codex account (metadata only; never the secret column).
72
91
  // P2: fiveHour/weekly ride along, built from the CACHED usage columns (zero
73
92
  // provider calls, zero decrypts) so the bars render instantly off this read.
74
- function codexAccountJson(
93
+ export function codexAccountJson(
75
94
  row: CodexAccountStatus,
76
95
  options: {
77
96
  appsCredentialId?: string | null;
@@ -81,10 +100,25 @@ function codexAccountJson(
81
100
  ) {
82
101
  return {
83
102
  id: row.id,
103
+ source: row.source,
84
104
  chatgptAccountId: row.chatgptAccountId,
85
105
  label: row.label,
86
106
  email: row.accountEmail,
87
107
  plan: row.planType,
108
+ planCheckedAt: row.planCheckedAt ?? null,
109
+ // The most recent observed plan change (for example "pro" before a move to
110
+ // "free"), kept as evidence until the plan changes again.
111
+ planChangedFrom: row.planPreviousType ?? null,
112
+ planChangedAt: row.planChangedAt ?? null,
113
+ // Models the CURRENT plan was proven not to include. Each leaves automatic
114
+ // selection until `retryAfter` (one request then re-checks it) or until a
115
+ // different plan is observed (refresh usage after an upgrade).
116
+ planExcludedModels: activeCodexPlanExclusions(row, new Date()).map((entry) => ({
117
+ model: entry.modelId,
118
+ label: productLabelForModelId(entry.modelId),
119
+ excludedAt: entry.excludedAt,
120
+ retryAfter: entry.expiresAt,
121
+ })),
88
122
  status: row.status,
89
123
  active: row.isActive,
90
124
  expiresAt: row.expiresAt,
@@ -110,6 +144,7 @@ function codexAccountJson(
110
144
  exhaustedUntil: row.exhaustedUntil,
111
145
  appsDesignated: options.appsCredentialId === row.id,
112
146
  canEnableApps:
147
+ row.source === "workspace" &&
113
148
  options.appsCredentialId === null &&
114
149
  options.canManageApps === true &&
115
150
  options.humanSubjectId !== null &&
@@ -119,6 +154,55 @@ function codexAccountJson(
119
154
  };
120
155
  }
121
156
 
157
+ /**
158
+ * Derive the two distinct cached worker-readiness meanings exposed by the
159
+ * status route. `poolReady` means at least one effective-pool account passes
160
+ * the worker's cached admission predicate. `workerRoutable` additionally
161
+ * honors rotation-off's active-pointer-only rule; it says nothing about a
162
+ * session-specific manual pin.
163
+ */
164
+ export function codexWorkerReadiness(input: {
165
+ effectiveSource: "workspace" | "organization" | "disabled";
166
+ rotationEnabled: boolean;
167
+ activeCredentialId: string | null;
168
+ accounts: ReadonlyArray<
169
+ Pick<
170
+ CodexAccountStatus,
171
+ | "id"
172
+ | "status"
173
+ | "allocatorEnabled"
174
+ | "primaryUsedPercent"
175
+ | "primaryResetAt"
176
+ | "secondaryUsedPercent"
177
+ | "secondaryResetAt"
178
+ | "exhaustedUntil"
179
+ >
180
+ >;
181
+ now: Date;
182
+ }): { poolReady: boolean; workerRoutable: boolean } {
183
+ if (input.effectiveSource === "disabled") {
184
+ return { poolReady: false, workerRoutable: false };
185
+ }
186
+ const windowUsed = (used: number | null, resetAt: Date | null): number =>
187
+ resetAt !== null && resetAt.getTime() <= input.now.getTime() ? 0 : (used ?? 0);
188
+ const eligible = (account: (typeof input.accounts)[number]): boolean =>
189
+ account.status === "active" &&
190
+ account.allocatorEnabled &&
191
+ (account.exhaustedUntil === null || account.exhaustedUntil.getTime() <= input.now.getTime()) &&
192
+ Math.max(
193
+ windowUsed(account.primaryUsedPercent, account.primaryResetAt),
194
+ windowUsed(account.secondaryUsedPercent, account.secondaryResetAt),
195
+ ) < 100;
196
+ const poolReady = input.accounts.some(eligible);
197
+ const activePointerReady = input.accounts.some(
198
+ (account) => account.id === input.activeCredentialId && eligible(account),
199
+ );
200
+ return {
201
+ poolReady,
202
+ workerRoutable: input.rotationEnabled ? poolReady : activePointerReady,
203
+ };
204
+ }
205
+
122
206
  // The /codex/usage{,/refresh,/:id} wire wrapper: the rich normalized payload
123
207
  // carries its own `status`, surfaced at the top level for back-compat with the
124
208
  // existing CodexUsage = { status; usage } shape.
@@ -129,35 +213,48 @@ function codexUsageJson(payload: CodexUsagePayload): {
129
213
  return { status: payload.status, usage: payload };
130
214
  }
131
215
 
132
- export function codexModelsForPicker(liveSlugs: readonly string[]): Array<{
216
+ export function codexModelsForPicker(
217
+ settings: Settings = getSettings(),
218
+ supportedSlugs?: readonly string[],
219
+ ): Array<{
133
220
  id: string;
134
221
  label: string;
135
222
  provider: string;
136
223
  providerLabel: string;
137
224
  api: "responses";
138
225
  }> {
139
- const available = new Set(liveSlugs);
140
- const missing = CODEX_FALLBACK_MODEL_SLUGS.filter((slug) => !available.has(slug));
141
- if (missing.length > 0) {
142
- throw new Error(`Codex catalog is missing required models: ${missing.join(", ")}`);
143
- }
144
- return CODEX_FALLBACK_MODEL_SLUGS.map((slug) => ({
145
- id: `${CODEX_MODEL_ID_PREFIX}${slug}`,
146
- label: slug.replace(/^gpt-/, "GPT-"),
147
- provider: CODEX_PROVIDER_ID,
148
- providerLabel: CODEX_PROVIDER_LABEL,
149
- api: "responses" as const,
150
- }));
226
+ return configuredModels(withCodexCatalogProvider(settings))
227
+ .filter(
228
+ (model) =>
229
+ model.providerId === CODEX_PROVIDER_ID &&
230
+ (supportedSlugs === undefined || supportedSlugs.includes(model.upstreamModelId)),
231
+ )
232
+ .map((model) => ({
233
+ id: model.id,
234
+ label: model.label,
235
+ provider: CODEX_PROVIDER_ID,
236
+ providerLabel: CODEX_PROVIDER_LABEL,
237
+ api: "responses" as const,
238
+ }));
151
239
  }
152
240
  import { createSignedState, readSignedState } from "@opengeni/github";
153
241
  import {
154
242
  getManagedSession,
155
243
  hasPermission,
156
244
  requireAccessGrant,
245
+ requireCanonicalLocalAccountAdministrator,
246
+ resolveCatalogSettings,
157
247
  type ApiRouteDeps,
158
248
  } from "@opengeni/core";
159
249
  import type { Context, Hono } from "hono";
160
250
  import { HTTPException } from "hono/http-exception";
251
+ import {
252
+ agentActingAsPerson,
253
+ agentActingAsPersonBeforeGrantCheck,
254
+ isAgentActingAsPerson,
255
+ organizationSettingsPermission,
256
+ requireNotAgent,
257
+ } from "../http/acting-person";
161
258
  import * as z from "zod/v4";
162
259
  import {
163
260
  hashCodexBrowserSession,
@@ -183,7 +280,7 @@ type ManagedCookieHuman = {
183
280
  browserSessionHash: string;
184
281
  };
185
282
 
186
- async function managedCookieHuman(
283
+ export async function managedCookieHuman(
187
284
  c: Context,
188
285
  deps: ApiRouteDeps,
189
286
  ): Promise<ManagedCookieHuman | null> {
@@ -195,7 +292,11 @@ async function managedCookieHuman(
195
292
  ) {
196
293
  return null;
197
294
  }
198
- const session = await getManagedSession(c, deps.managedAuth);
295
+ const session = await getManagedSession(c, deps.managedAuth, {
296
+ db: deps.db,
297
+ sessionAdapter: deps.managedAuthSessionAdapter,
298
+ sessionSetMode: deps.settings.managedAuthSessionSetMode,
299
+ });
199
300
  if (!session?.user?.id || !session.session?.id) return null;
200
301
  return {
201
302
  subjectId: `user:${session.user.id}`,
@@ -203,31 +304,202 @@ async function managedCookieHuman(
203
304
  };
204
305
  }
205
306
 
206
- function requireSameOriginBrowserMutation(c: Context, deps: ApiRouteDeps): void {
307
+ /**
308
+ * The person in this browser, or an agent acting as them. Only for routes that
309
+ * then require the same person's workspace grant; provider sign-in steps keep
310
+ * managedCookieHuman. An agent has no browser session, so its hash never
311
+ * matches a sign-in started in one.
312
+ */
313
+ export async function managedHumanOrAgent(
314
+ c: Context,
315
+ deps: ApiRouteDeps,
316
+ ): Promise<ManagedCookieHuman | null> {
317
+ const human = await managedCookieHuman(c, deps);
318
+ if (human) return human;
319
+ const agent = agentActingAsPersonBeforeGrantCheck(c);
320
+ return agent
321
+ ? {
322
+ subjectId: agent.subjectId,
323
+ browserSessionHash: await hashCodexBrowserSession(`agent:${crypto.randomUUID()}`),
324
+ }
325
+ : null;
326
+ }
327
+
328
+ export async function requireOrganizationCodexHuman(
329
+ c: Context,
330
+ deps: ApiRouteDeps,
331
+ organizationId: string,
332
+ options: {
333
+ /** A provider sign-in step: the person does it in the browser, never an agent. */
334
+ providerConsent?: boolean;
335
+ } = {},
336
+ ): Promise<ManagedCookieHuman> {
337
+ if (options.providerConsent) requireNotAgent(c, "Signing in to a provider");
338
+ const parsed = z.string().uuid().safeParse(organizationId);
339
+ if (!parsed.success) throw new HTTPException(422, { message: "invalid organization id" });
340
+ let human = await managedCookieHuman(c, deps);
341
+ if (!human) {
342
+ const agent = agentActingAsPerson(c, parsed.data, organizationSettingsPermission(c));
343
+ // An agent has no browser session, so it never matches a sign-in started in one.
344
+ if (agent) {
345
+ human = {
346
+ subjectId: agent.subjectId,
347
+ browserSessionHash: await hashCodexBrowserSession(`agent:${crypto.randomUUID()}`),
348
+ };
349
+ }
350
+ }
351
+ if (!human && deps.settings.productAccessMode === "local") {
352
+ const local = await requireCanonicalLocalAccountAdministrator(c, deps, organizationId);
353
+ human = {
354
+ subjectId: local.subjectId,
355
+ browserSessionHash: await hashCodexBrowserSession(`local:${local.subjectId}`),
356
+ };
357
+ }
358
+ if (!human) {
359
+ throw new HTTPException(401, {
360
+ message: "organization administrator session required",
361
+ });
362
+ }
363
+ try {
364
+ await getOrganizationCodexRotationSettings(deps.db, {
365
+ organizationId,
366
+ actorSubjectId: human.subjectId,
367
+ });
368
+ } catch (error) {
369
+ const state = nestedPostgresSqlState(error);
370
+ if (state === "42501") {
371
+ throw new HTTPException(403, {
372
+ message: "organization administration is not authorized",
373
+ });
374
+ }
375
+ if (state === "P0002") {
376
+ throw new HTTPException(404, { message: "organization not found" });
377
+ }
378
+ throw error;
379
+ }
380
+ return human;
381
+ }
382
+
383
+ async function requireWorkspaceCodexManagementSource(
384
+ deps: ApiRouteDeps,
385
+ workspaceId: string,
386
+ ): Promise<void> {
387
+ const source = await getWorkspaceCodexSubscriptionSource(deps.db, workspaceId);
388
+ if (source.effectiveSource === "organization") {
389
+ throw new HTTPException(409, {
390
+ message: "this Codex subscription is managed in Organization settings",
391
+ });
392
+ }
393
+ if (source.effectiveSource === "disabled") {
394
+ throw new HTTPException(409, {
395
+ message: "Codex is disabled for this workspace",
396
+ });
397
+ }
398
+ }
399
+
400
+ export function requireSameOriginBrowserMutation(c: Context, deps: ApiRouteDeps): void {
401
+ // Built in process for an agent acting as a person: no browser credentials
402
+ // ride along, so there is no cross-site request to guard against.
403
+ if (isAgentActingAsPerson(c)) return;
207
404
  const contentType = c.req.header("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
208
405
  if (contentType !== "application/json") {
209
406
  throw new HTTPException(403, {
210
407
  message: "JSON browser request required",
211
408
  });
212
409
  }
213
- if (!deps.settings.publicBaseUrl) {
410
+ if (deps.settings.productAccessMode !== "local" && !deps.settings.publicBaseUrl) {
214
411
  throw new HTTPException(503, {
215
412
  message: "managed browser origin is not configured",
216
413
  });
217
414
  }
218
- const expectedOrigin = new URL(deps.settings.publicBaseUrl).origin;
219
- if (c.req.header("origin") !== expectedOrigin) {
415
+ const origin = c.req.header("origin");
416
+ const localOriginMatches =
417
+ deps.settings.productAccessMode === "local" && localBrowserOriginMatchesRequest(c, origin);
418
+ if (
419
+ deps.settings.productAccessMode === "local"
420
+ ? !localOriginMatches
421
+ : origin !== new URL(deps.settings.publicBaseUrl!).origin
422
+ ) {
220
423
  throw new HTTPException(403, {
221
424
  message: "same-origin browser request required",
222
425
  });
223
426
  }
224
- if (c.req.header("sec-fetch-site")?.toLowerCase() !== "same-origin") {
427
+ const fetchSite = c.req.header("sec-fetch-site")?.toLowerCase();
428
+ const localFetchSiteMatches =
429
+ localOriginMatches &&
430
+ (fetchSite === "same-origin" ||
431
+ fetchSite === "same-site" ||
432
+ (fetchSite === "cross-site" && localLoopbackOriginMatchesRequest(c, origin)));
433
+ if (
434
+ deps.settings.productAccessMode === "local"
435
+ ? !localFetchSiteMatches
436
+ : fetchSite !== "same-origin"
437
+ ) {
225
438
  throw new HTTPException(403, {
226
439
  message: "same-origin fetch metadata required",
227
440
  });
228
441
  }
229
442
  }
230
443
 
444
+ function localBrowserOriginMatchesRequest(c: Context, value: string | undefined): boolean {
445
+ if (!value) return false;
446
+ let origin: URL;
447
+ try {
448
+ origin = new URL(value);
449
+ } catch {
450
+ return false;
451
+ }
452
+ if (
453
+ origin.origin !== value ||
454
+ origin.origin === "null" ||
455
+ (origin.protocol !== "http:" && origin.protocol !== "https:")
456
+ ) {
457
+ return false;
458
+ }
459
+
460
+ const forwardedProtocol = c.req.header("x-forwarded-proto")?.trim().toLowerCase();
461
+ const protocol = forwardedProtocol ? `${forwardedProtocol}:` : new URL(c.req.url).protocol;
462
+ if (protocol !== "http:" && protocol !== "https:") return false;
463
+ const forwardedHost = c.req.header("x-forwarded-host") ?? c.req.header("host");
464
+ if (!forwardedHost || /[\s,/?#@\\]/u.test(forwardedHost)) return false;
465
+
466
+ let request: URL;
467
+ try {
468
+ request = new URL(`${protocol}//${forwardedHost}`);
469
+ } catch {
470
+ return false;
471
+ }
472
+ return (
473
+ origin.protocol === request.protocol &&
474
+ (origin.hostname === request.hostname ||
475
+ (isLoopbackHostname(origin.hostname) && isLoopbackHostname(request.hostname)))
476
+ );
477
+ }
478
+
479
+ function localLoopbackOriginMatchesRequest(c: Context, value: string | undefined): boolean {
480
+ if (!value) return false;
481
+ try {
482
+ const origin = new URL(value);
483
+ const forwardedProtocol = c.req.header("x-forwarded-proto")?.trim().toLowerCase();
484
+ const protocol = forwardedProtocol ? `${forwardedProtocol}:` : new URL(c.req.url).protocol;
485
+ const forwardedHost = c.req.header("x-forwarded-host") ?? c.req.header("host");
486
+ if (!forwardedHost || /[\s,/?#@\\]/u.test(forwardedHost)) return false;
487
+ const request = new URL(`${protocol}//${forwardedHost}`);
488
+ return (
489
+ origin.origin === value &&
490
+ origin.protocol === request.protocol &&
491
+ isLoopbackHostname(origin.hostname) &&
492
+ isLoopbackHostname(request.hostname)
493
+ );
494
+ } catch {
495
+ return false;
496
+ }
497
+ }
498
+
499
+ function isLoopbackHostname(value: string): boolean {
500
+ return value === "localhost" || value === "[::1]" || /^127(?:\.[0-9]{1,3}){3}$/u.test(value);
501
+ }
502
+
231
503
  async function requireRedemptionHuman(
232
504
  c: Context,
233
505
  deps: ApiRouteDeps,
@@ -248,13 +520,13 @@ async function requireRedemptionHuman(
248
520
  });
249
521
  }
250
522
  requireSameOriginBrowserMutation(c, deps);
251
- const human = await managedCookieHuman(c, deps);
523
+ const human = await managedHumanOrAgent(c, deps);
252
524
  if (!human) {
253
525
  throw new HTTPException(401, {
254
526
  message: "managed browser session required",
255
527
  });
256
528
  }
257
- const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
529
+ const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
258
530
  if (grant.subjectId !== human.subjectId) {
259
531
  throw new HTTPException(403, {
260
532
  message: "managed browser identity mismatch",
@@ -274,13 +546,17 @@ async function requireCodexAppsHuman(
274
546
  });
275
547
  }
276
548
  requireSameOriginBrowserMutation(c, deps);
277
- const human = await managedCookieHuman(c, deps);
549
+ const human = await managedHumanOrAgent(c, deps);
278
550
  if (!human) {
279
- throw new HTTPException(401, { message: "managed browser session required" });
551
+ throw new HTTPException(401, {
552
+ message: "managed browser session required",
553
+ });
280
554
  }
281
555
  const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
282
556
  if (grant.subjectId !== human.subjectId) {
283
- throw new HTTPException(403, { message: "managed browser identity mismatch" });
557
+ throw new HTTPException(403, {
558
+ message: "managed browser identity mismatch",
559
+ });
284
560
  }
285
561
  return { human, accountId: grant.accountId };
286
562
  }
@@ -386,10 +662,46 @@ function createProviderCallLimiter(limit: number): CodexProviderCall {
386
662
  };
387
663
  }
388
664
 
665
+ type CodexRedemptionAccess = {
666
+ ownership: "current_human" | "unowned" | "different_human" | "managed_human_unavailable";
667
+ canClaimUnownedViaReconnect: boolean;
668
+ };
669
+
670
+ function codexRedemptionAccess(input: {
671
+ connectedBySubjectId: string | null;
672
+ grantSubjectId: string;
673
+ managedHumanSubjectId: string | null;
674
+ canManage: boolean;
675
+ }): CodexRedemptionAccess {
676
+ if (
677
+ input.managedHumanSubjectId === null ||
678
+ input.managedHumanSubjectId !== input.grantSubjectId
679
+ ) {
680
+ return {
681
+ ownership: "managed_human_unavailable",
682
+ canClaimUnownedViaReconnect: false,
683
+ };
684
+ }
685
+ if (input.connectedBySubjectId === null) {
686
+ return {
687
+ ownership: "unowned",
688
+ canClaimUnownedViaReconnect: input.canManage,
689
+ };
690
+ }
691
+ return {
692
+ ownership:
693
+ input.connectedBySubjectId === input.managedHumanSubjectId
694
+ ? "current_human"
695
+ : "different_human",
696
+ canClaimUnownedViaReconnect: false,
697
+ };
698
+ }
699
+
389
700
  async function fetchCodexAccountOverview(
390
701
  deps: ApiRouteDeps,
391
702
  workspaceId: string,
392
703
  row: CodexAccountStatus,
704
+ redemptionAccess: CodexRedemptionAccess,
393
705
  canRedeem: boolean,
394
706
  canResumeRedemption: boolean,
395
707
  redemptions: Awaited<ReturnType<typeof listCodexResetRedemptionRecoveries>> = [],
@@ -496,6 +808,7 @@ async function fetchCodexAccountOverview(
496
808
  })),
497
809
  },
498
810
  canRedeem,
811
+ redemptionAccess,
499
812
  canResumeRedemption,
500
813
  redemptions: redemptions.map((redemption) => ({
501
814
  attemptId: redemption.attemptId,
@@ -512,6 +825,8 @@ async function fetchCodexAccountOverview(
512
825
 
513
826
  type CodexConnectState = {
514
827
  workspaceId?: string;
828
+ organizationId?: string;
829
+ actorSubjectId?: string;
515
830
  deviceAuthId?: string;
516
831
  userCode?: string;
517
832
  iat?: number;
@@ -556,11 +871,306 @@ const CODEX_DEVICE_EXPIRY_SECONDS = 15 * 60; // the device code expires 15 min a
556
871
  export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
557
872
  const { db, settings, githubStateSecret } = deps;
558
873
 
874
+ app.get("/v1/workspaces/:workspaceId/codex/source", async (c) => {
875
+ const workspaceId = c.req.param("workspaceId");
876
+ await requireAccessGrant(c, deps, workspaceId, "workspace:read");
877
+ return c.json(await getWorkspaceCodexSubscriptionSource(db, workspaceId));
878
+ });
879
+
880
+ app.patch("/v1/workspaces/:workspaceId/codex/source", async (c) => {
881
+ const workspaceId = c.req.param("workspaceId");
882
+ const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
883
+ const parsed = z
884
+ .object({
885
+ mode: z.enum(["automatic", "workspace", "organization", "disabled"]),
886
+ })
887
+ .safeParse(await c.req.json().catch(() => null));
888
+ if (!parsed.success) {
889
+ throw new HTTPException(400, {
890
+ message: "a valid Codex source mode is required",
891
+ });
892
+ }
893
+ try {
894
+ return c.json(
895
+ await setWorkspaceCodexSubscriptionMode(db, {
896
+ accountId: grant.accountId,
897
+ workspaceId,
898
+ subjectId: grant.subjectId,
899
+ mode: parsed.data.mode,
900
+ }),
901
+ );
902
+ } catch (error) {
903
+ if (
904
+ error instanceof Error &&
905
+ (error.message.includes("personal workspaces") ||
906
+ error.message.includes("active turns are using it"))
907
+ ) {
908
+ throw new HTTPException(409, { message: error.message });
909
+ }
910
+ throw error;
911
+ }
912
+ });
913
+
914
+ app.get("/v1/organizations/:organizationId/codex/accounts", async (c) => {
915
+ const organizationId = c.req.param("organizationId");
916
+ const human = await requireOrganizationCodexHuman(c, deps, organizationId);
917
+ const [accounts, rotation] = await Promise.all([
918
+ listOrganizationCodexAccountStatuses(db, {
919
+ organizationId,
920
+ actorSubjectId: human.subjectId,
921
+ }),
922
+ getOrganizationCodexRotationSettings(db, {
923
+ organizationId,
924
+ actorSubjectId: human.subjectId,
925
+ }),
926
+ ]);
927
+ return c.json({
928
+ accounts: accounts.map((account) => codexAccountJson(account)),
929
+ activeAccountId: rotation?.activeCredentialId ?? null,
930
+ settings: {
931
+ rotationEnabled: rotation?.rotationEnabled ?? false,
932
+ rotationStrategy: "sharded",
933
+ activeCredentialId: rotation?.activeCredentialId ?? null,
934
+ },
935
+ });
936
+ });
937
+
938
+ app.post("/v1/organizations/:organizationId/codex/connect/start", async (c) => {
939
+ const organizationId = c.req.param("organizationId");
940
+ requireSameOriginBrowserMutation(c, deps);
941
+ const human = await requireOrganizationCodexHuman(c, deps, organizationId, {
942
+ providerConsent: true,
943
+ });
944
+ let start: Awaited<ReturnType<typeof startDeviceCode>>;
945
+ try {
946
+ start = await startDeviceCode();
947
+ } catch (error) {
948
+ throw new HTTPException(502, {
949
+ message:
950
+ error instanceof CodexDeviceError ? error.message : "failed to start Codex device login",
951
+ });
952
+ }
953
+ return c.json({
954
+ userCode: start.userCode,
955
+ verificationUri: start.verificationUri,
956
+ intervalSeconds: start.intervalSeconds,
957
+ state: createSignedState(githubStateSecret, {
958
+ organizationId,
959
+ actorSubjectId: human.subjectId,
960
+ deviceAuthId: start.deviceAuthId,
961
+ userCode: start.userCode,
962
+ }),
963
+ });
964
+ });
965
+
966
+ app.post("/v1/organizations/:organizationId/codex/connect/poll", async (c) => {
967
+ const organizationId = c.req.param("organizationId");
968
+ requireSameOriginBrowserMutation(c, deps);
969
+ const human = await requireOrganizationCodexHuman(c, deps, organizationId, {
970
+ providerConsent: true,
971
+ });
972
+ const { state } = (await c.req.json().catch(() => null)) as {
973
+ state?: string;
974
+ };
975
+ const payload = (state
976
+ ? readSignedState(state, githubStateSecret)
977
+ : null) as unknown as CodexConnectState | null;
978
+ if (
979
+ !payload ||
980
+ payload.organizationId !== organizationId ||
981
+ payload.actorSubjectId !== human.subjectId ||
982
+ !payload.deviceAuthId ||
983
+ !payload.userCode
984
+ ) {
985
+ throw new HTTPException(400, {
986
+ message: "codex connect state is invalid or expired",
987
+ });
988
+ }
989
+ if (
990
+ typeof payload.iat === "number" &&
991
+ Date.now() / 1000 - payload.iat > CODEX_DEVICE_EXPIRY_SECONDS
992
+ ) {
993
+ return c.json({ status: "expired" });
994
+ }
995
+ let poll: Awaited<ReturnType<typeof pollDeviceCode>>;
996
+ try {
997
+ poll = await pollDeviceCode({
998
+ deviceAuthId: payload.deviceAuthId,
999
+ userCode: payload.userCode,
1000
+ });
1001
+ } catch (error) {
1002
+ throw new HTTPException(502, {
1003
+ message: error instanceof CodexDeviceError ? error.message : "codex device poll failed",
1004
+ });
1005
+ }
1006
+ if (poll.status === "pending") return c.json({ status: "pending" });
1007
+ if (poll.status === "expired") return c.json({ status: "expired" });
1008
+ let tokens: Awaited<ReturnType<typeof exchangeDeviceCode>>;
1009
+ try {
1010
+ tokens = await exchangeDeviceCode({
1011
+ authorizationCode: poll.authorizationCode,
1012
+ codeVerifier: poll.codeVerifier,
1013
+ });
1014
+ } catch (error) {
1015
+ throw new HTTPException(502, {
1016
+ message: error instanceof CodexDeviceError ? error.message : "codex token exchange failed",
1017
+ });
1018
+ }
1019
+ const key = environmentsEncryptionKeyBytes(settings);
1020
+ if (!key) {
1021
+ throw new HTTPException(500, {
1022
+ message: "OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY is not configured",
1023
+ });
1024
+ }
1025
+ const id = parseIdToken(tokens.idToken);
1026
+ await ensureOrganizationCodexRotationSettings(db, {
1027
+ organizationId,
1028
+ actorSubjectId: human.subjectId,
1029
+ });
1030
+ let upserted: Awaited<ReturnType<typeof upsertOrganizationCodexSubscriptionCredential>>;
1031
+ try {
1032
+ upserted = await upsertOrganizationCodexSubscriptionCredential(db, {
1033
+ organizationId,
1034
+ actorSubjectId: human.subjectId,
1035
+ credentialEncrypted: encryptEnvironmentValue(
1036
+ key,
1037
+ JSON.stringify({
1038
+ access_token: tokens.accessToken,
1039
+ refresh_token: tokens.refreshToken,
1040
+ id_token: tokens.idToken,
1041
+ }),
1042
+ ),
1043
+ chatgptAccountId: id.chatgptAccountId,
1044
+ scopes: null,
1045
+ planType: id.planType,
1046
+ isFedramp: id.isFedramp,
1047
+ expiresAt: accessTokenExpiry(tokens.accessToken),
1048
+ lastRefreshAt: new Date(),
1049
+ accountEmail: id.email ?? null,
1050
+ label: id.email ?? id.chatgptAccountId ?? null,
1051
+ });
1052
+ } catch (error) {
1053
+ const cause = (error as { cause?: unknown } | null)?.cause;
1054
+ const message =
1055
+ cause instanceof Error ? cause.message : error instanceof Error ? error.message : "";
1056
+ if (message.includes("active turns are using it")) {
1057
+ throw new HTTPException(409, {
1058
+ message: "Codex subscription source cannot change while active turns are using it",
1059
+ });
1060
+ }
1061
+ throw error;
1062
+ }
1063
+ await signalCodexCapacityTargets(deps, upserted.wakeTargets);
1064
+ const rotation = await getOrganizationCodexRotationSettings(db, {
1065
+ organizationId,
1066
+ actorSubjectId: human.subjectId,
1067
+ });
1068
+ return c.json({
1069
+ status: "connected",
1070
+ plan: id.planType,
1071
+ accountId: upserted.id,
1072
+ isActive: rotation?.activeCredentialId === upserted.id,
1073
+ });
1074
+ });
1075
+
1076
+ app.post("/v1/organizations/:organizationId/codex/accounts/:accountId/activate", async (c) => {
1077
+ const organizationId = c.req.param("organizationId");
1078
+ requireSameOriginBrowserMutation(c, deps);
1079
+ const human = await requireOrganizationCodexHuman(c, deps, organizationId);
1080
+ const credentialId = c.req.param("accountId");
1081
+ const activation = await setActiveOrganizationCodexCredential(db, {
1082
+ organizationId,
1083
+ actorSubjectId: human.subjectId,
1084
+ credentialId,
1085
+ });
1086
+ if (!activation.activated) {
1087
+ throw new HTTPException(404, { message: "codex account not found" });
1088
+ }
1089
+ await signalCodexCapacityTargets(deps, activation.wakeTargets);
1090
+ return c.json({ activated: true, accountId: credentialId });
1091
+ });
1092
+
1093
+ app.patch("/v1/organizations/:organizationId/codex/settings", async (c) => {
1094
+ const organizationId = c.req.param("organizationId");
1095
+ requireSameOriginBrowserMutation(c, deps);
1096
+ const human = await requireOrganizationCodexHuman(c, deps, organizationId);
1097
+ const parsed = z
1098
+ .object({ rotationEnabled: z.boolean() })
1099
+ .safeParse(await c.req.json().catch(() => null));
1100
+ if (!parsed.success) {
1101
+ throw new HTTPException(400, { message: "rotationEnabled is required" });
1102
+ }
1103
+ const updated = await updateOrganizationCodexRotationSettings(db, {
1104
+ organizationId,
1105
+ actorSubjectId: human.subjectId,
1106
+ rotationEnabled: parsed.data.rotationEnabled,
1107
+ });
1108
+ if (!updated) throw new HTTPException(404, { message: "Codex settings not found" });
1109
+ await signalCodexCapacityTargets(deps, updated.wakeTargets);
1110
+ return c.json({
1111
+ rotationEnabled: updated.rotationEnabled,
1112
+ rotationStrategy: "sharded",
1113
+ activeCredentialId: updated.activeCredentialId,
1114
+ });
1115
+ });
1116
+
1117
+ app.patch("/v1/organizations/:organizationId/codex/accounts/:accountId", async (c) => {
1118
+ const organizationId = c.req.param("organizationId");
1119
+ requireSameOriginBrowserMutation(c, deps);
1120
+ const human = await requireOrganizationCodexHuman(c, deps, organizationId);
1121
+ const body = (await c.req.json().catch(() => null)) as {
1122
+ label?: unknown;
1123
+ } | null;
1124
+ const renamed = await renameOrganizationCodexAccount(db, {
1125
+ organizationId,
1126
+ actorSubjectId: human.subjectId,
1127
+ credentialId: c.req.param("accountId"),
1128
+ label: typeof body?.label === "string" ? body.label : null,
1129
+ });
1130
+ if (!renamed) throw new HTTPException(404, { message: "codex account not found" });
1131
+ const accounts = await listOrganizationCodexAccountStatuses(db, {
1132
+ organizationId,
1133
+ actorSubjectId: human.subjectId,
1134
+ });
1135
+ const row = accounts.find((account) => account.id === c.req.param("accountId"));
1136
+ if (!row) throw new HTTPException(404, { message: "codex account not found" });
1137
+ return c.json(codexAccountJson(row));
1138
+ });
1139
+
1140
+ app.delete("/v1/organizations/:organizationId/codex/accounts/:accountId", async (c) => {
1141
+ const organizationId = c.req.param("organizationId");
1142
+ requireSameOriginBrowserMutation(c, deps);
1143
+ const human = await requireOrganizationCodexHuman(c, deps, organizationId);
1144
+ let result: Awaited<ReturnType<typeof disconnectOrganizationCodexAccount>>;
1145
+ try {
1146
+ result = await disconnectOrganizationCodexAccount(db, {
1147
+ organizationId,
1148
+ actorSubjectId: human.subjectId,
1149
+ credentialId: c.req.param("accountId"),
1150
+ });
1151
+ } catch (error) {
1152
+ const cause = (error as { cause?: unknown } | null)?.cause;
1153
+ const message =
1154
+ cause instanceof Error ? cause.message : error instanceof Error ? error.message : "";
1155
+ if (message.includes("active turns are using it")) {
1156
+ throw new HTTPException(409, {
1157
+ message: "Codex subscription cannot disconnect while active turns are using it",
1158
+ });
1159
+ }
1160
+ throw error;
1161
+ }
1162
+ await signalCodexCapacityTargets(deps, result.wakeTargets);
1163
+ return c.json({
1164
+ disconnected: result.removed,
1165
+ newActiveId: result.newActiveCredentialId,
1166
+ });
1167
+ });
1168
+
559
1169
  // Begin device-code login: returns the user code + verification URL and a
560
1170
  // signed state that carries the device_auth_id back to `poll`.
561
1171
  app.post("/v1/workspaces/:workspaceId/codex/connect/start", async (c) => {
562
1172
  const workspaceId = c.req.param("workspaceId");
563
- await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
1173
+ await requireAccessGrant(c, deps, workspaceId, "connections:write");
564
1174
  let start: Awaited<ReturnType<typeof startDeviceCode>>;
565
1175
  try {
566
1176
  start = await startDeviceCode();
@@ -586,7 +1196,7 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
586
1196
  // Poll for authorization: pending | expired | connected (persists on success).
587
1197
  app.post("/v1/workspaces/:workspaceId/codex/connect/poll", async (c) => {
588
1198
  const workspaceId = c.req.param("workspaceId");
589
- const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
1199
+ const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
590
1200
  const { state } = (await c.req.json()) as { state?: string };
591
1201
  const payload = (state
592
1202
  ? readSignedState(state, githubStateSecret)
@@ -647,58 +1257,79 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
647
1257
  message: "OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY is not configured",
648
1258
  });
649
1259
  }
650
- await ensureCodexRotationSettings(db, grant.accountId, workspaceId);
651
- const mutation = await withCodexCapacityMutation(
652
- db,
653
- { workspaceId, reason: "codex_credential_connected" },
654
- async (tx) => {
655
- const upserted = await upsertCodexSubscriptionCredential(tx, {
656
- accountId: grant.accountId,
657
- workspaceId,
658
- credentialEncrypted: encryptEnvironmentValue(
659
- key,
660
- JSON.stringify({
661
- access_token: tokens.accessToken,
662
- refresh_token: tokens.refreshToken,
663
- id_token: tokens.idToken,
664
- }),
665
- ),
666
- chatgptAccountId: id.chatgptAccountId,
667
- scopes: null, // device grant scopes are discovered at runtime, not asserted here
668
- planType: id.planType,
669
- isFedramp: id.isFedramp,
670
- expiresAt: accessTokenExpiry(tokens.accessToken),
671
- lastRefreshAt: new Date(),
672
- accountEmail: id.email ?? null,
673
- label: id.email ?? id.chatgptAccountId ?? null,
674
- connectedBySubjectId:
675
- connectingHuman?.subjectId === grant.subjectId ? connectingHuman.subjectId : null,
676
- });
677
- return { result: upserted, changed: upserted.kind === "upserted" };
678
- },
679
- );
680
- const upserted = mutation.result;
1260
+ const mutation = await withSessionCodexCapacityMutation<{
1261
+ upserted: Awaited<ReturnType<typeof upsertCodexSubscriptionCredential>>;
1262
+ isActive: boolean;
1263
+ }>(db, { workspaceId, reason: "codex_credential_connected" }, async (tx) => {
1264
+ // The capacity mutation holds the source lock before entering this callback.
1265
+ const sourceBeforeConnect = await getWorkspaceCodexSubscriptionSource(tx, workspaceId);
1266
+ const upserted = await upsertCodexSubscriptionCredential(tx, {
1267
+ accountId: grant.accountId,
1268
+ workspaceId,
1269
+ credentialEncrypted: encryptEnvironmentValue(
1270
+ key,
1271
+ JSON.stringify({
1272
+ access_token: tokens.accessToken,
1273
+ refresh_token: tokens.refreshToken,
1274
+ id_token: tokens.idToken,
1275
+ }),
1276
+ ),
1277
+ chatgptAccountId: id.chatgptAccountId,
1278
+ scopes: null, // device grant scopes are discovered at runtime, not asserted here
1279
+ planType: id.planType,
1280
+ isFedramp: id.isFedramp,
1281
+ expiresAt: accessTokenExpiry(tokens.accessToken),
1282
+ lastRefreshAt: new Date(),
1283
+ accountEmail: id.email ?? null,
1284
+ label: id.email ?? id.chatgptAccountId ?? null,
1285
+ connectedBySubjectId:
1286
+ connectingHuman?.subjectId === grant.subjectId ? connectingHuman.subjectId : null,
1287
+ });
1288
+ if (upserted.kind === "unresolved_redemption") {
1289
+ return { result: { upserted, isActive: false }, changed: false };
1290
+ }
1291
+ await ensureCodexRotationSettings(tx, grant.accountId, workspaceId);
1292
+ await setInitialActiveCodexCredential(tx, workspaceId, upserted.id);
1293
+ await setWorkspaceCodexSubscriptionModeInTransaction(tx, {
1294
+ accountId: grant.accountId,
1295
+ workspaceId,
1296
+ subjectId: grant.subjectId,
1297
+ // Connecting local capacity does not override an explicit source choice.
1298
+ // Automatic naturally prefers the newly connected workspace pool.
1299
+ mode: sourceBeforeConnect.mode,
1300
+ effectiveSourceBeforeMutation: sourceBeforeConnect.effectiveSource,
1301
+ });
1302
+ const rotation = await getCodexRotationSettings(tx, workspaceId);
1303
+ return {
1304
+ result: {
1305
+ upserted,
1306
+ isActive: rotation?.activeCredentialId === upserted.id,
1307
+ },
1308
+ changed: true,
1309
+ };
1310
+ }).catch((error: unknown) => {
1311
+ const cause = (error as { cause?: unknown } | null)?.cause;
1312
+ const message =
1313
+ cause instanceof Error ? cause.message : error instanceof Error ? error.message : "";
1314
+ if (message.includes("active turns are using it")) {
1315
+ throw new HTTPException(409, { message });
1316
+ }
1317
+ throw error;
1318
+ });
1319
+ const { upserted, isActive } = mutation.result;
681
1320
  if (upserted.kind === "unresolved_redemption") {
682
1321
  throw new HTTPException(409, {
683
1322
  message:
684
1323
  "this subscription has an unresolved reset redemption; recover it before changing ownership",
685
1324
  });
686
1325
  }
687
- // Ensure the per-workspace rotation-settings row exists, then auto-activate
688
- // the FIRST account only. Additional new accounts do NOT auto-activate — a
689
- // manual switch is required (no auto-rotation in P1). A re-connect of the
690
- // already-active account is a no-op for the pointer.
691
- // Keep both rotation bits false on first connect. The deployment flag makes
692
- // the compatible allocator available, but the workspace-local cutover bit
693
- // is enabled only by an explicit settings write after every worker replica
694
- // understands leasing.
695
- const rotation = await getCodexRotationSettings(db, workspaceId);
696
- let isActive = rotation?.activeCredentialId === upserted.id;
697
- if (!isActive && rotation?.activeCredentialId == null) {
698
- isActive = await setInitialActiveCodexCredential(db, workspaceId, upserted.id);
699
- }
700
1326
  await signalCodexCapacityTargets(deps, mutation.wakeTargets);
701
- return c.json({ status: "connected", plan: id.planType, accountId: upserted.id, isActive });
1327
+ return c.json({
1328
+ status: "connected",
1329
+ plan: id.planType,
1330
+ accountId: upserted.id,
1331
+ isActive,
1332
+ });
702
1333
  });
703
1334
 
704
1335
  // Connection health: the cheapest real call is GET /codex/models (a 200 proves
@@ -706,12 +1337,13 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
706
1337
  app.get("/v1/workspaces/:workspaceId/codex/status", async (c) => {
707
1338
  const workspaceId = c.req.param("workspaceId");
708
1339
  await requireAccessGrant(c, deps, workspaceId, "workspace:read");
709
- const status = await getCodexCredentialStatus(db, workspaceId);
710
- if (!status) {
711
- return c.json({ connected: false });
712
- }
713
- const accounts = await listCodexAccountStatuses(db, workspaceId);
714
- const activeRow = accounts.find((account) => account.id === status.credentialId) ?? null;
1340
+ const [status, accounts, source, rotation] = await Promise.all([
1341
+ getCodexCredentialStatus(db, workspaceId),
1342
+ listCodexAccountStatuses(db, workspaceId),
1343
+ getWorkspaceCodexSubscriptionSource(db, workspaceId),
1344
+ getCodexRotationSettings(db, workspaceId),
1345
+ ]);
1346
+ const activeRow = accounts.find((account) => account.id === status?.credentialId) ?? null;
715
1347
  const activeAccount = activeRow
716
1348
  ? {
717
1349
  id: activeRow.id,
@@ -723,23 +1355,33 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
723
1355
  chatgptAccountId: activeRow.chatgptAccountId,
724
1356
  }
725
1357
  : null;
1358
+ const activeCredentialId = status?.credentialId ?? rotation?.activeCredentialId ?? null;
1359
+ const readiness = codexWorkerReadiness({
1360
+ effectiveSource: source.effectiveSource,
1361
+ rotationEnabled: rotation?.rotationEnabled ?? false,
1362
+ activeCredentialId,
1363
+ accounts,
1364
+ now: new Date(),
1365
+ });
726
1366
  let valid = false;
1367
+ const catalogSettings = (await resolveCatalogSettings(db, settings)).settings;
727
1368
  let models: ReturnType<typeof codexModelsForPicker> = [];
728
1369
  let catalogError: string | null = null;
729
1370
  try {
730
- const cred = status.credentialId
1371
+ const cred = status?.credentialId
731
1372
  ? await loadCodexCredentialForRun(db, settings, workspaceId, status.credentialId)
732
1373
  : null;
733
1374
  if (cred) {
1375
+ const token = await buildCodexTokenResolver(db, settings, workspaceId, cred.id).getToken();
734
1376
  const live = await fetchCodexModels({
735
- accessToken: cred.tokens.accessToken,
736
- chatgptAccountId: cred.chatgptAccountId,
737
- isFedramp: cred.isFedramp,
1377
+ accessToken: token.accessToken,
1378
+ chatgptAccountId: token.chatgptAccountId,
1379
+ isFedramp: token.isFedramp,
738
1380
  clientVersion: CODEX_CLIENT_VERSION,
739
1381
  });
740
1382
  if (live.ok) {
741
- models = codexModelsForPicker(live.slugs);
742
1383
  valid = true;
1384
+ models = codexModelsForPicker(catalogSettings, live.slugs);
743
1385
  } else {
744
1386
  catalogError = `Codex models request failed with status ${live.status}`;
745
1387
  }
@@ -749,14 +1391,18 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
749
1391
  catalogError = error instanceof Error ? error.message : String(error);
750
1392
  }
751
1393
  return c.json({
752
- connected: status.connected,
753
- plan: status.planType,
1394
+ connected: status?.connected ?? false,
1395
+ plan: status?.planType ?? null,
754
1396
  valid,
755
- expiresAt: status.expiresAt,
756
- lastError: catalogError ?? status.lastError,
1397
+ activeAccountValid: valid,
1398
+ poolReady: readiness.poolReady,
1399
+ workerRoutable: readiness.workerRoutable,
1400
+ expiresAt: status?.expiresAt ?? null,
1401
+ lastError: catalogError ?? status?.lastError ?? null,
757
1402
  models, // ClientModel[] the picker surfaces under the "no credits" group
758
1403
  activeAccount, // the account a session runs on when unpinned (label for the indicator)
759
1404
  accountCount: accounts.length,
1405
+ source,
760
1406
  });
761
1407
  });
762
1408
 
@@ -765,11 +1411,12 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
765
1411
  app.get("/v1/workspaces/:workspaceId/codex/accounts", async (c) => {
766
1412
  const workspaceId = c.req.param("workspaceId");
767
1413
  const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
768
- const [accounts, rotation, apps, human] = await Promise.all([
1414
+ const [accounts, rotation, apps, human, source] = await Promise.all([
769
1415
  listCodexAccountStatuses(db, workspaceId),
770
1416
  getCodexRotationSettings(db, workspaceId),
771
1417
  getCodexAppsSettings(db, workspaceId),
772
- managedCookieHuman(c, deps),
1418
+ managedHumanOrAgent(c, deps),
1419
+ getWorkspaceCodexSubscriptionSource(db, workspaceId),
773
1420
  ]);
774
1421
  const activeAccountId = rotation?.activeCredentialId ?? null;
775
1422
  const humanSubjectId = human?.subjectId === grant.subjectId ? human.subjectId : null;
@@ -784,6 +1431,7 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
784
1431
  }),
785
1432
  ),
786
1433
  activeAccountId,
1434
+ source,
787
1435
  apps: {
788
1436
  available: settings.codexConnectedAppsEnabled,
789
1437
  credentialId: apps.credentialId,
@@ -803,8 +1451,11 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
803
1451
 
804
1452
  app.post("/v1/workspaces/:workspaceId/codex/apps", async (c) => {
805
1453
  const workspaceId = c.req.param("workspaceId");
1454
+ await requireWorkspaceCodexManagementSource(deps, workspaceId);
806
1455
  if (!settings.codexConnectedAppsEnabled) {
807
- throw new HTTPException(409, { message: "Codex Apps is disabled for this deployment" });
1456
+ throw new HTTPException(409, {
1457
+ message: "Codex Apps is disabled for this deployment",
1458
+ });
808
1459
  }
809
1460
  const { human, accountId } = await requireCodexAppsHuman(c, deps, workspaceId);
810
1461
  const parsed = z
@@ -814,7 +1465,9 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
814
1465
  })
815
1466
  .safeParse(await c.req.json().catch(() => null));
816
1467
  if (!parsed.success) {
817
- throw new HTTPException(400, { message: "accountId and expectedVersion are required" });
1468
+ throw new HTTPException(400, {
1469
+ message: "accountId and expectedVersion are required",
1470
+ });
818
1471
  }
819
1472
  const result = await designateCodexAppsCredential(db, {
820
1473
  accountId,
@@ -832,10 +1485,14 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
832
1485
  });
833
1486
  }
834
1487
  if (result.kind === "forbidden") {
835
- throw new HTTPException(403, { message: "missing permission: connections:write" });
1488
+ throw new HTTPException(403, {
1489
+ message: "missing permission: connections:write",
1490
+ });
836
1491
  }
837
1492
  if (result.kind === "unavailable") {
838
- throw new HTTPException(409, { message: "codex account requires relogin" });
1493
+ throw new HTTPException(409, {
1494
+ message: "codex account requires relogin",
1495
+ });
839
1496
  }
840
1497
  const response = {
841
1498
  credentialId: result.credentialId,
@@ -848,6 +1505,7 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
848
1505
 
849
1506
  app.delete("/v1/workspaces/:workspaceId/codex/apps", async (c) => {
850
1507
  const workspaceId = c.req.param("workspaceId");
1508
+ await requireWorkspaceCodexManagementSource(deps, workspaceId);
851
1509
  const { human, accountId } = await requireCodexAppsHuman(c, deps, workspaceId);
852
1510
  const parsed = z
853
1511
  .object({ expectedVersion: z.number().int().nonnegative() })
@@ -862,7 +1520,9 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
862
1520
  expectedVersion: parsed.data.expectedVersion,
863
1521
  });
864
1522
  if (result.kind === "forbidden") {
865
- throw new HTTPException(403, { message: "missing permission: connections:write" });
1523
+ throw new HTTPException(403, {
1524
+ message: "missing permission: connections:write",
1525
+ });
866
1526
  }
867
1527
  const response = {
868
1528
  credentialId: result.credentialId,
@@ -877,7 +1537,8 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
877
1537
  // Pure pointer flip; in-flight turns pick it up on their next token fetch.
878
1538
  app.post("/v1/workspaces/:workspaceId/codex/accounts/:accountId/activate", async (c) => {
879
1539
  const workspaceId = c.req.param("workspaceId");
880
- await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
1540
+ await requireAccessGrant(c, deps, workspaceId, "connections:write");
1541
+ await requireWorkspaceCodexManagementSource(deps, workspaceId);
881
1542
  const accountId = c.req.param("accountId");
882
1543
  const mutation = await withCodexCapacityMutation(
883
1544
  db,
@@ -895,14 +1556,15 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
895
1556
  return c.json({ activated: true, accountId });
896
1557
  });
897
1558
 
898
- // Update rotation settings. admin access. sharded-rotation policy: the strategy picker is GONE —
1559
+ // Update rotation settings. Connection-management access. Sharded-rotation policy: the strategy picker is GONE —
899
1560
  // rotation-enabled always behaves as sticky-sharded (worker-side
900
1561
  // effectiveRotationStrategy normalization). `rotationStrategy` in the body is
901
1562
  // ACCEPTED-BUT-IGNORED so no existing SDK/UI caller breaks (deprecation), and
902
1563
  // the stored column is only legacy residue kept for old-binary rollback.
903
1564
  app.patch("/v1/workspaces/:workspaceId/codex/settings", async (c) => {
904
1565
  const workspaceId = c.req.param("workspaceId");
905
- const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
1566
+ const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
1567
+ await requireWorkspaceCodexManagementSource(deps, workspaceId);
906
1568
  const body = (await c.req.json().catch(() => ({}))) as {
907
1569
  rotationEnabled?: unknown;
908
1570
  rotationStrategy?: unknown;
@@ -917,7 +1579,10 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
917
1579
  if (patch.rotationEnabled === undefined) {
918
1580
  // Strategy-only writes are a deprecated no-op (no db touch): report the
919
1581
  // (only) truth. Callers that also flip rotationEnabled fall through.
920
- return c.json({ rotationStrategy: "sharded", rotationStrategyDeprecated: true });
1582
+ return c.json({
1583
+ rotationStrategy: "sharded",
1584
+ rotationStrategyDeprecated: true,
1585
+ });
921
1586
  }
922
1587
  await ensureCodexRotationSettings(db, grant.accountId, workspaceId);
923
1588
  const mutation = await withCodexCapacityMutation(
@@ -946,7 +1611,8 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
946
1611
  // Rename an account (label only in P1).
947
1612
  app.patch("/v1/workspaces/:workspaceId/codex/accounts/:accountId", async (c) => {
948
1613
  const workspaceId = c.req.param("workspaceId");
949
- await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
1614
+ await requireAccessGrant(c, deps, workspaceId, "connections:write");
1615
+ await requireWorkspaceCodexManagementSource(deps, workspaceId);
950
1616
  const accountId = c.req.param("accountId");
951
1617
  const body = (await c.req.json()) as { label?: string | null };
952
1618
  const label = typeof body.label === "string" ? body.label : null;
@@ -967,7 +1633,8 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
967
1633
  // an explicit 409 carrying the current version.
968
1634
  app.patch("/v1/workspaces/:workspaceId/codex/accounts/:accountId/allocator", async (c) => {
969
1635
  const workspaceId = c.req.param("workspaceId");
970
- const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
1636
+ const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
1637
+ await requireWorkspaceCodexManagementSource(deps, workspaceId);
971
1638
  const parsed = z
972
1639
  .object({
973
1640
  enabled: z.boolean(),
@@ -1005,7 +1672,8 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
1005
1672
  // row was active (FK ON DELETE SET NULL + re-pick in the same RLS txn).
1006
1673
  app.delete("/v1/workspaces/:workspaceId/codex/accounts/:accountId", async (c) => {
1007
1674
  const workspaceId = c.req.param("workspaceId");
1008
- const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
1675
+ const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
1676
+ await requireWorkspaceCodexManagementSource(deps, workspaceId);
1009
1677
  const accountId = c.req.param("accountId");
1010
1678
  const mutation = await withCodexCapacityMutation(
1011
1679
  db,
@@ -1023,14 +1691,18 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
1023
1691
  });
1024
1692
  }
1025
1693
  await signalCodexCapacityTargets(deps, mutation.wakeTargets);
1026
- return c.json({ disconnected: result.removed, newActiveId: result.newActiveCredentialId });
1694
+ return c.json({
1695
+ disconnected: result.removed,
1696
+ newActiveId: result.newActiveCredentialId,
1697
+ });
1027
1698
  });
1028
1699
 
1029
1700
  // Legacy "disconnect all" (old workspace-wide behavior), deprecated in favor of
1030
1701
  // the by-id route above.
1031
1702
  app.delete("/v1/workspaces/:workspaceId/codex", async (c) => {
1032
1703
  const workspaceId = c.req.param("workspaceId");
1033
- const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
1704
+ const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
1705
+ await requireWorkspaceCodexManagementSource(deps, workspaceId);
1034
1706
  const mutation = await withCodexCapacityMutation(
1035
1707
  db,
1036
1708
  { workspaceId, reason: "codex_credentials_disconnected" },
@@ -1135,12 +1807,12 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
1135
1807
  app.get("/v1/workspaces/:workspaceId/codex/overview", async (c) => {
1136
1808
  const workspaceId = c.req.param("workspaceId");
1137
1809
  const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
1138
- const human = await managedCookieHuman(c, deps);
1810
+ const human = await managedHumanOrAgent(c, deps);
1139
1811
  const accounts = await listCodexAccountStatuses(db, workspaceId);
1140
1812
  const ownerRecoveries =
1141
1813
  human &&
1142
1814
  human.subjectId === grant.subjectId &&
1143
- hasPermission(grant.permissions, "workspace:admin")
1815
+ hasPermission(grant.permissions, "connections:write")
1144
1816
  ? await listCodexResetRedemptionRecoveries(db, {
1145
1817
  accountId: grant.accountId,
1146
1818
  workspaceId,
@@ -1160,16 +1832,30 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
1160
1832
  const account = queue.shift();
1161
1833
  if (!account) return;
1162
1834
  const canResumeRedemption = Boolean(
1835
+ account.source === "workspace" &&
1163
1836
  human &&
1164
1837
  human.subjectId === grant.subjectId &&
1165
1838
  human.subjectId === account.connectedBySubjectId &&
1166
- hasPermission(grant.permissions, "workspace:admin"),
1839
+ hasPermission(grant.permissions, "connections:write"),
1167
1840
  );
1168
1841
  const canRedeem = canResumeRedemption && account.status === "active";
1842
+ const redemptionAccess: CodexRedemptionAccess =
1843
+ account.source === "organization"
1844
+ ? {
1845
+ ownership: "managed_human_unavailable",
1846
+ canClaimUnownedViaReconnect: false,
1847
+ }
1848
+ : codexRedemptionAccess({
1849
+ connectedBySubjectId: account.connectedBySubjectId,
1850
+ grantSubjectId: grant.subjectId,
1851
+ managedHumanSubjectId: human?.subjectId ?? null,
1852
+ canManage: hasPermission(grant.permissions, "connections:write"),
1853
+ });
1169
1854
  overview[account.id] = await fetchCodexAccountOverview(
1170
1855
  deps,
1171
1856
  workspaceId,
1172
1857
  account,
1858
+ redemptionAccess,
1173
1859
  canRedeem,
1174
1860
  canResumeRedemption,
1175
1861
  canResumeRedemption
@@ -1207,15 +1893,27 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
1207
1893
  .filter((account) => overview[account.id] == null)
1208
1894
  .map(async (account) => {
1209
1895
  const canResumeRedemption = Boolean(
1896
+ account.source === "workspace" &&
1210
1897
  human &&
1211
1898
  human.subjectId === grant.subjectId &&
1212
1899
  human.subjectId === account.connectedBySubjectId &&
1213
- hasPermission(grant.permissions, "workspace:admin"),
1900
+ hasPermission(grant.permissions, "connections:write"),
1214
1901
  );
1215
1902
  const fallback = await fetchCodexAccountOverview(
1216
1903
  deps,
1217
1904
  workspaceId,
1218
1905
  account,
1906
+ account.source === "organization"
1907
+ ? {
1908
+ ownership: "managed_human_unavailable",
1909
+ canClaimUnownedViaReconnect: false,
1910
+ }
1911
+ : codexRedemptionAccess({
1912
+ connectedBySubjectId: account.connectedBySubjectId,
1913
+ grantSubjectId: grant.subjectId,
1914
+ managedHumanSubjectId: human?.subjectId ?? null,
1915
+ canManage: hasPermission(grant.permissions, "connections:write"),
1916
+ }),
1219
1917
  false,
1220
1918
  canResumeRedemption,
1221
1919
  canResumeRedemption
@@ -1257,6 +1955,11 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
1257
1955
  const accounts = await listCodexAccountStatuses(db, workspaceId);
1258
1956
  const account = accounts.find((candidate) => candidate.id === credentialId);
1259
1957
  if (!account) throw new HTTPException(404, { message: "codex account not found" });
1958
+ if (account.source === "organization") {
1959
+ throw new HTTPException(409, {
1960
+ message: "organization Codex subscriptions are managed in Organization settings",
1961
+ });
1962
+ }
1260
1963
  let existing = await getCodexResetRedemptionAttempt(db, workspaceId, parsed.data.attemptId);
1261
1964
  if (account.connectedBySubjectId !== human.subjectId) {
1262
1965
  throw new HTTPException(403, {
@@ -1289,10 +1992,14 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
1289
1992
  });
1290
1993
  }
1291
1994
  if (adoption.kind === "not_found") {
1292
- throw new HTTPException(409, { message: "redemption recovery state changed" });
1995
+ throw new HTTPException(409, {
1996
+ message: "redemption recovery state changed",
1997
+ });
1293
1998
  }
1294
1999
  if (adoption.kind === "forbidden") {
1295
- throw new HTTPException(403, { message: "redemption owner is unavailable" });
2000
+ throw new HTTPException(403, {
2001
+ message: "redemption owner is unavailable",
2002
+ });
1296
2003
  }
1297
2004
  if (adoption.kind === "conflict") {
1298
2005
  throw new HTTPException(409, {
@@ -1306,7 +2013,9 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
1306
2013
  // and a lost HTTP response must remain replayable after a later health
1307
2014
  // transition without another consume call.
1308
2015
  if (account.status !== "active" && existing?.status !== "completed") {
1309
- throw new HTTPException(403, { message: "redemption credential is unavailable" });
2016
+ throw new HTTPException(403, {
2017
+ message: "redemption credential is unavailable",
2018
+ });
1310
2019
  }
1311
2020
  const secret = settings.betterAuthSecret;
1312
2021
  if (!secret) {
@@ -1505,13 +2214,21 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
1505
2214
  if (fenced.kind !== "ready") {
1506
2215
  if (fenced.reason === "confirmation_expired") {
1507
2216
  return c.json(
1508
- { status: "confirmation_expired", attemptId: attempt.id, retryable: true },
2217
+ {
2218
+ status: "confirmation_expired",
2219
+ attemptId: attempt.id,
2220
+ retryable: true,
2221
+ },
1509
2222
  403,
1510
2223
  );
1511
2224
  }
1512
2225
  if (fenced.reason === "credential_unavailable") {
1513
2226
  return c.json(
1514
- { status: "provider_unavailable", attemptId: attempt.id, retryable: true },
2227
+ {
2228
+ status: "provider_unavailable",
2229
+ attemptId: attempt.id,
2230
+ retryable: true,
2231
+ },
1515
2232
  503,
1516
2233
  );
1517
2234
  }