@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
package/src/app.ts CHANGED
@@ -1,71 +1,200 @@
1
+ import { registerClaudeSubscriptionAccountRoutes } from "./routes/claude-subscription-accounts";
2
+ import { registerWorkspaceModelProviderRoutes } from "./routes/workspace-model-providers";
3
+ import { registerClaudeSubscriptionOAuthRoutes } from "./routes/claude-subscription-oauth";
4
+ import { registerConnectCallbackReturns } from "./integrations/connect-callback-return";
5
+ import { registerFeedbackRoutes } from "./routes/feedback";
6
+ import { registerWorkspaceIntegrationRoutes } from "./routes/workspace-integrations";
7
+ import { registerOrganizationIntegrationRoutes } from "./routes/organization-integrations";
1
8
  import {
2
- canonicalizeConfiguredModelId,
3
- configuredAllowedModels,
9
+ CLIENT_ERRORS_PATH,
10
+ isClientErrorReportRequest,
11
+ registerClientErrorRoutes,
12
+ } from "./routes/client-errors";
13
+ import {
14
+ ANALYTICS_CONSENT_PATH,
15
+ isAnalyticsConsentReportRequest,
16
+ registerAnalyticsConsentRoutes,
17
+ } from "./routes/analytics-consent";
18
+ import { codemodeSessionRequest } from "./codemode";
19
+ import { SiteSessionPathError, OrganizationIntegrationDeniedError } from "@opengeni/contracts";
20
+ import { registerModelConnectionAccessRoutes } from "./routes/model-connection-access";
21
+ import {
22
+ codeSearchDeploymentPolicy,
23
+ agentConfigDeploymentPolicy,
4
24
  configuredAllowedReasoningEfforts,
5
- configuredModels,
6
25
  resolveFirstPartyMcpToolPolicy,
7
- withCodexCatalogProvider,
8
- withXaiSubscriptionCatalogProvider,
26
+ resolveVoiceInputProviderRegistry,
27
+ UnsupportedLatencyModeError,
28
+ type Settings,
9
29
  } from "@opengeni/config";
10
30
  import {
31
+ AGENT_CAPABILITY_IDS,
11
32
  ClientConfig,
12
33
  CodemodeCallRequest,
13
34
  ErrorEnvelope,
14
35
  OPENGENI_API_CONTRACT_HEADER,
15
36
  OPENGENI_API_CONTRACT_REVISION,
16
37
  OPENGENI_CORRELATION_HEADER,
38
+ resolveAgentToolFamilies,
17
39
  resolveWorkspaceMemoryEnabled,
18
40
  resolveWorkspaceMemoryPromptMode,
19
41
  VOICE_INPUT_ACCEPTED_MIME_TYPES,
20
42
  TRANSCRIPTION_RECORDING_PROVIDER_SEGMENT_SECONDS,
43
+ ToolGatewayApprovalRequest,
44
+ ToolGatewayCallRequest,
21
45
  type AccessGrant,
46
+ type ClientAgentConfig,
22
47
  type ErrorCode,
48
+ type FirstPartyMcpToolName,
49
+ type Session,
23
50
  } from "@opengeni/contracts";
24
51
  import {
25
52
  createDocumentServices,
26
- getDocument,
53
+ getDocumentForIndexing,
27
54
  indexDocumentNow,
28
55
  type DocumentServices,
29
56
  } from "@opengeni/documents";
30
57
  import {
31
- CodemodeCallBudgetExceededError,
32
58
  CodemodeOperationConflictError,
33
59
  CodemodeOperationNotExecutableError,
34
60
  CodemodePayloadTooLargeError,
35
61
  CodemodeToolApprovalRequiredError,
36
62
  CodemodeToolNotInCatalogError,
63
+ ConnectAttemptConflictError,
64
+ ConnectAttemptNotFoundError,
65
+ configureChildLifecycleNotices,
66
+ configureCodeSearchDeploymentPolicy,
67
+ configureWorkspaceControlRequestLockTimeoutMs,
37
68
  dbSql,
69
+ getManagedAuthSessionSetSnapshot,
38
70
  getWorkspace,
71
+ reapManagedAuthIsolatedSessions,
72
+ reapExpiredManagedAuthSessionSets,
73
+ resolveSessionMemoryAgentScope,
39
74
  rlsContextForWorkspace,
40
- withSessionRlsActorContext,
41
75
  } from "@opengeni/db";
42
- import { createObservability } from "@opengeni/observability";
76
+ import { requireSessionEventDurableFanoutCapability } from "@opengeni/events";
77
+ import { githubAppBotIdentityWarnings } from "@opengeni/github";
78
+ import {
79
+ createObservability,
80
+ failureDiagnostic,
81
+ withTraceContext,
82
+ withMcpTelemetry,
83
+ } from "@opengeni/observability";
43
84
  import { createObjectStorage } from "@opengeni/storage";
44
85
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
45
- import { Hono } from "hono";
86
+ import { SUPPORTED_PROTOCOL_VERSIONS } from "@modelcontextprotocol/sdk/types.js";
87
+ import { handleMcpRequestWithClientAbort } from "./mcp/request-abort";
88
+ import { Hono, type Context } from "hono";
46
89
  import { bodyLimit } from "hono/body-limit";
47
90
  import { compress } from "hono/compress";
48
91
  import { cors } from "hono/cors";
92
+ import { getCookie } from "hono/cookie";
49
93
  import { HTTPException } from "hono/http-exception";
50
- import { ApiHttpError } from "./http/api-error";
94
+ import {
95
+ ApiHttpError,
96
+ agentConfigHttpError,
97
+ allowanceExhaustedHttpError,
98
+ modelUnavailableHttpError,
99
+ scheduledTaskTargetAccessHttpError,
100
+ workspaceControlBusyHttpError,
101
+ } from "./http/api-error";
102
+ import {
103
+ isRequestBodyValidationError,
104
+ requestBodyValidationHttpError,
105
+ tagRequestJsonParseErrors,
106
+ } from "./http/request-body";
107
+ import { invalidPathIdentifierHttpError } from "./http/path-identifier";
108
+ import { replaceTrustedClientAddressHeader } from "./http/request-source";
109
+ import { unmatchedRoute } from "./http/unmatched-route";
51
110
  import type { ContentfulStatusCode } from "hono/utils/http-status";
111
+ import { bearerApiContractHeaderCompatibility } from "./http/api-contract-compat";
112
+ import { deprecationHeadersMiddleware } from "./http/deprecation";
113
+ import {
114
+ boundedRegisteredRouteLabel,
115
+ registeredHandlerRoutePath,
116
+ } from "./http/registered-route-label";
52
117
  import type { ApiRouteDeps, AppDependencies } from "@opengeni/core";
53
118
  import {
54
119
  CodexCompactionV2ProviderLockedError,
120
+ ManagedAuthActorLeaseOutcomeUnknownError,
55
121
  hasPermission,
56
122
  requireAccessGrant,
57
- requireLiveAgentAttemptAuthorization,
123
+ requireAccessGrantAuthorization,
124
+ accountScopedApiKeyWorkspaceAuthority,
125
+ requireAccessContext,
58
126
  requirePermission,
127
+ releaseManagedAuthRequestActorLease,
128
+ resolveCatalogSettings,
129
+ resolveWorkspaceCatalogSettings,
130
+ resolveCallerWorkspaceModelSelections,
131
+ resolveDefaultSessionModelForSelections,
132
+ selectDefaultSessionModel,
133
+ isWorkspaceModelAdmissible,
134
+ resolveWorkspaceModelSelection,
135
+ validateManagedAuthRequestActorLease,
59
136
  requireSessionAuthorization,
60
137
  SessionAuthorizationDeniedError,
138
+ withSessionAuthorizationReadReuse,
61
139
  SessionAuthorizationUnavailableError,
140
+ createUserPresenceRecorder,
141
+ registerProductUsageMetricBaselines,
62
142
  } from "@opengeni/core";
63
- import { createManagedAuth } from "./auth/managed-auth";
143
+ import { createManagedAuthNewSignupsGate } from "./auth/new-signups-gate";
144
+ import {
145
+ createManagedAuth,
146
+ isolatedManagedAuthOAuthCallbackRequest,
147
+ managedAuthNewSignupsPausedResponse,
148
+ managedAuthOAuthReturnMatches,
149
+ resolveManagedAuthOAuthAttempt,
150
+ } from "./auth/managed-auth";
151
+ import {
152
+ adoptManagedAuthSession,
153
+ MANAGED_AUTH_SESSION_SET_COOKIE,
154
+ ManagedAuthActorChangeError,
155
+ managedAuthCsrfHash,
156
+ managedAuthDerivedUuid,
157
+ managedAuthSecretRequestDigest,
158
+ managedAuthSha256,
159
+ } from "@opengeni/core/managed-auth-session-sets";
160
+ import { createBetterAuthSessionAdapter } from "./auth/managed-auth-session-adapter";
161
+ import {
162
+ currentManagedAuthCreatedSessionId,
163
+ runManagedAuthAttempt,
164
+ runManagedAuthDiscardedProviderSession,
165
+ runManagedAuthProvider,
166
+ } from "./auth/managed-auth-attempt-context";
167
+ import { createManagedEmailTransport } from "./auth/managed-email";
168
+ import { startManagedSignInNotificationDelivery } from "./auth/managed-sign-in-notifications";
169
+ import {
170
+ assertManagedEmailTransportMetadata,
171
+ assertOrganizationUserSetupQueryTransportConfigured,
172
+ } from "./auth/organization-user-setup";
64
173
  import { createApiSandboxClient, makeResumeBoxById } from "./sandbox/access";
65
- import { requireLimit } from "@opengeni/core";
66
174
  import { buildOpenGeniMcpServer } from "./mcp/server";
175
+ import {
176
+ buildWorkspaceToolGatewayMcpServer,
177
+ approveWorkspaceToolGatewayCall,
178
+ callWorkspaceToolGateway,
179
+ grantUsesAttemptScopedMcp,
180
+ prepareMcpOAuthWorkspaceToolGateway,
181
+ prepareWorkspaceToolGateway,
182
+ workspaceToolGatewayDeclarations,
183
+ } from "./workspace-tool-gateway";
184
+ import {
185
+ isMcpOAuthResourcePath,
186
+ mcpOAuthAuthenticateHeader,
187
+ mcpOAuthBearerToken,
188
+ ORGANIZATION_MCP_PATH,
189
+ registerMcpOAuthRoutes,
190
+ resolveMcpOAuthRouteAccess,
191
+ resolveOrganizationMcpOAuthAccess,
192
+ } from "./mcp-oauth";
193
+ import { buildOrganizationMcpServer, type OrganizationMcpCaller } from "./organization-mcp";
67
194
  import {
68
195
  CodemodeAuthorityError,
196
+ CodemodeCatalogNotReadyError,
197
+ CodemodeCatalogStaleError,
69
198
  isCodemodeGrant,
70
199
  readCodemodeOperation,
71
200
  requireActiveCodemodeCatalog,
@@ -77,49 +206,97 @@ import {
77
206
  BrowserControlTransportError,
78
207
  } from "@opengeni/runtime/sandbox";
79
208
  import { requireAccessKey } from "./http/auth";
209
+ import {
210
+ publicListenerServesMetrics,
211
+ registerPrometheusMetricsRoute,
212
+ } from "./http/metrics-listener";
80
213
  import { allowedCorsOrigin } from "./http/cors";
214
+ import {
215
+ createLocalBrowserBoundary,
216
+ localBrowserRequestHost,
217
+ markLocalInternalDispatch,
218
+ } from "./http/local-browser-boundary";
219
+ import { withAccessGrantSessionRlsContext } from "./access-grant-rls";
81
220
  import { registerCapabilityRoutes } from "./routes/capabilities";
82
221
  import { registerCatalogAssetRoutes } from "./routes/catalog-assets";
83
222
  import { registerCodexRoutes } from "./routes/codex";
223
+ import { registerOrganizationModelProviderRoutes } from "./routes/organization-model-providers";
224
+ import { registerOrganizationIntegrationPolicyRoutes } from "./routes/organization-integration-policy";
84
225
  import { registerSuperGrokRoutes } from "./routes/supergrok";
85
226
  import { registerConnectionRoutes } from "./routes/connections";
227
+ import { registerConnectRoutes } from "./routes/connect";
228
+
229
+ import { registerExternalIdentityLinkRoutes } from "./routes/external-identity-links";
86
230
  import { registerDocumentRoutes } from "./routes/documents";
231
+ import { registerKnowledgeRoutes } from "./routes/knowledge";
87
232
  import { registerEnrollmentRoutes } from "./routes/enrollments";
88
233
  import { registerMachineRoutes } from "./routes/machines";
89
234
  import { registerMemorySlackPublicationRoutes } from "./routes/memory-slack-publications";
90
235
  import { registerEnvironmentRoutes } from "./routes/environments";
91
236
  import { registerFileRoutes } from "./routes/files";
92
237
  import { registerApiKeyRoutes } from "./routes/api-keys";
238
+ import { registerOrganizationMcpConnectionRoutes } from "./routes/organization-mcp-connections";
239
+ import { registerOrganizationServiceAccountRoutes } from "./routes/organization-service-accounts";
93
240
  import { registerBillingRoutes } from "./routes/billing";
94
241
  import { registerBrowserIdentityRoutes } from "./routes/browser-identities";
95
242
  import { registerBrowserSessionRoutes } from "./routes/browser-sessions";
96
243
  import { registerComputerSessionRoutes } from "./routes/computer-sessions";
97
244
  import { registerGitHubRoutes } from "./routes/github";
245
+ import { registerPersonalGitHubRoutes } from "./routes/personal-github";
246
+ import {
247
+ isPersonalGitHubGitBrokerRequest,
248
+ registerPersonalGitHubGitBrokerRoutes,
249
+ } from "./routes/personal-github-git-broker";
98
250
  import { registerInstallRoutes } from "./routes/install";
99
251
  import { registerApiIntegrationRoutes } from "./routes/api-integrations";
100
252
  import { registerIntegrationFacetRoutes } from "./routes/integration-facets";
101
253
  import { registerInteractionResourceRoutes } from "./routes/interaction-resources";
102
- import { registerPackRoutes } from "./routes/packs";
254
+ import { registerPrReviewRoutes } from "./routes/pr-review";
255
+ import { registerAutomationRoutes } from "./routes/automations";
103
256
  import { registerPluginRoutes } from "./routes/plugins";
104
257
  import { registerSkillRoutes } from "./routes/skills";
105
258
  import { registerChannelRoutes } from "./routes/channels";
106
259
  import { registerRigRoutes } from "./routes/rigs";
107
260
  import { registerScheduledTaskRoutes } from "./routes/scheduled-tasks";
108
261
  import { registerSessionRoutes } from "./routes/sessions";
262
+ import {
263
+ archivedSessionImportErrorResponse,
264
+ isSessionHistoryImportRequest,
265
+ registerSessionHistoryImportRoutes,
266
+ } from "./routes/session-history-imports";
109
267
  import { registerSocialRoutes } from "./routes/social";
110
268
  import { registerWorkspaceRoutes } from "./routes/workspaces";
269
+ import { registerUsageAllowanceRoutes } from "./routes/usage-allowances";
111
270
  import { registerWorkspaceInstructionPolicyRoutes } from "./routes/workspace-instruction-policies";
271
+ import { registerWorkspaceLearningRoutes } from "./routes/workspace-learning";
112
272
  import { registerCompanyProfileRoutes } from "./routes/company-profile";
273
+ import { registerCompanyBrainRoutes } from "./routes/company-brain";
113
274
  import { registerSlackTaskPolicyRoutes } from "./routes/slack-task-policy";
114
275
  import { registerWorkspaceStateRoutes } from "./routes/workspace-state";
115
276
  import { registerWorkspaceArtifactRoutes } from "./routes/workspace-artifacts";
277
+ import { registerArtifactCatalogRoutes } from "./routes/artifact-catalog";
116
278
  import { registerPreferenceRegistryRoutes } from "./routes/preference-registry";
117
279
  import { registerInsightsRoutes } from "./routes/insights";
280
+ import { registerInsightsUsageRoutes } from "./routes/insights-usage";
118
281
  import { registerTranscriptionRoutes } from "./routes/transcriptions";
119
282
  import { registerEditableArtifactRoutes } from "./routes/editable-artifacts";
283
+ import { registerSessionArtifactAssociationRoutes } from "./routes/session-artifact-associations";
120
284
  import { registerVideoGenerationRoutes } from "./routes/video-generation";
121
285
  import { registerCanonicalHumanIdentityRoutes } from "./routes/canonical-human-identities";
122
286
  import { registerOrganizationMembershipRoutes } from "./routes/organization-memberships";
287
+ import { registerOrganizationSessionRoutes } from "./routes/organization-sessions";
288
+ import { registerOrganizationRecoveryRoutes } from "./routes/organization-recovery";
289
+ import { registerManagedOnboardingRoutes } from "./routes/managed-onboarding";
290
+ import {
291
+ registerManagedSignInMethodRoutes,
292
+ handleManagedSignInConnectCallback,
293
+ } from "./routes/managed-sign-in-methods";
294
+ import {
295
+ registerManagedAuthSessionSetRoutes,
296
+ requireManagedAuthProviderRouteAllowed,
297
+ scrubManagedAuthProviderResponse,
298
+ } from "./routes/managed-auth-session-sets";
299
+ import { registerUserResourceAuthorityRoutes } from "./routes/user-resource-authorities";
123
300
  import { projectClientModel } from "./model-catalog";
124
301
  import { createTranscriptionService } from "./transcription/service";
125
302
  import { createFfmpegTranscriptionSegmenter } from "./transcription/segmenter";
@@ -149,6 +326,8 @@ export { workflowIdForSession } from "@opengeni/core";
149
326
  export { replaySessionEvents, sseSessionStream, sseWorkspaceControlStream } from "./http/sse";
150
327
 
151
328
  export const API_MAX_REQUEST_BODY_BYTES = 32 * 1024 * 1024;
329
+ const managedAuthReaperDatabases = new WeakSet<object>();
330
+ const MANAGED_AUTH_REAPER_INTERVAL_MS = 60_000;
152
331
 
153
332
  /** Effective Hono bodyLimit — API JSON ceiling or voice multipart + multipart overhead. */
154
333
  export function apiRequestBodyLimitBytes(settings: {
@@ -167,11 +346,58 @@ export function createApp(deps: AppDependencies): Hono {
167
346
  return createAppComposition(deps).app;
168
347
  }
169
348
 
349
+ export async function resolveWorkspaceMcpRouteDeps(
350
+ routeDeps: ApiRouteDeps,
351
+ grant: AccessGrant,
352
+ ): Promise<ApiRouteDeps> {
353
+ const catalogSourceSettings = routeDeps.catalogSourceSettings ?? routeDeps.settings;
354
+ const resolvedCatalog = await resolveWorkspaceCatalogSettings(
355
+ routeDeps.db,
356
+ catalogSourceSettings,
357
+ {
358
+ accountId: grant.accountId,
359
+ workspaceId: grant.workspaceId,
360
+ },
361
+ );
362
+ return { ...routeDeps, catalogSourceSettings, settings: resolvedCatalog.settings };
363
+ }
364
+
170
365
  export function createAppComposition(deps: AppDependencies): {
171
366
  app: Hono;
172
367
  routeDeps: ApiRouteDeps;
173
368
  } {
174
- const managedAuth = deps.managedAuth ?? createManagedAuth(deps.settings, deps.db);
369
+ assertOrganizationUserSetupQueryTransportConfigured(deps.settings);
370
+ // The request-scoped workspace control-prefix budget is validated once by
371
+ // @opengeni/config at boot; install it for every request-scoped db command
372
+ // (Send/Steer/control/queue/settings/delete) built by this app.
373
+ configureWorkspaceControlRequestLockTimeoutMs(deps.settings.workspaceControlLockTimeoutMs);
374
+ // Child lifecycle notice producers (human-input/approval resolutions, direct
375
+ // Pause) run inside API-originated db commands; install the boot-validated
376
+ // rollout flag once for this process.
377
+ configureChildLifecycleNotices({ enabled: deps.settings.childLifecycleNoticesEnabled });
378
+ // Sessions created by this process freeze their code_search decision from
379
+ // the boot-parsed deployment policy.
380
+ configureCodeSearchDeploymentPolicy(codeSearchDeploymentPolicy(deps.settings));
381
+ const managedEmailTransport =
382
+ deps.managedEmailTransport ?? createManagedEmailTransport(deps.settings);
383
+ assertManagedEmailTransportMetadata(managedEmailTransport);
384
+ const observability =
385
+ deps.observability ?? createObservability(deps.settings, { component: "api" });
386
+ const managedAuthNewSignupsGate = createManagedAuthNewSignupsGate({
387
+ db: deps.db,
388
+ settings: deps.settings,
389
+ observability,
390
+ });
391
+ const managedAuth =
392
+ deps.managedAuth ??
393
+ createManagedAuth(deps.settings, deps.db, managedEmailTransport, {
394
+ observability,
395
+ newSignupsGate: managedAuthNewSignupsGate,
396
+ });
397
+ const managedAuthSessionAdapter =
398
+ deps.managedAuthSessionAdapter ??
399
+ (managedAuth ? createBetterAuthSessionAdapter(managedAuth, deps.db) : null);
400
+ if (managedAuth) startManagedSignInNotificationDelivery(deps.db, managedEmailTransport);
175
401
  const objectStorage =
176
402
  deps.objectStorage === undefined ? createObjectStorage(deps.settings) : deps.objectStorage;
177
403
  let documentServices: DocumentServices | null = deps.documentServices ?? null;
@@ -204,9 +430,12 @@ export function createAppComposition(deps: AppDependencies): {
204
430
  if (context.accountId !== accountId) {
205
431
  throw new Error("document account/workspace authority mismatch");
206
432
  }
207
- const claimedDocument = await getDocument(deps.db, workspaceId, documentId, {
208
- viewerSubjectId: authoritySubjectId,
209
- });
433
+ // This is a metadata-only authority-tuple fence. Workspace and
434
+ // organization documents intentionally have no authority subject, so a
435
+ // viewer-scoped public read cannot supply the immutable human needed for
436
+ // Drive ACL evaluation. The byte boundary below reconstructs that
437
+ // subject from the stored document through indexDocumentNow.
438
+ const claimedDocument = await getDocumentForIndexing(deps.db, workspaceId, documentId);
210
439
  if (
211
440
  !claimedDocument ||
212
441
  claimedDocument.authorityKind !== authorityKind ||
@@ -221,16 +450,6 @@ export function createAppComposition(deps: AppDependencies): {
221
450
  workspaceId,
222
451
  documentId,
223
452
  getDocumentServices(),
224
- {
225
- beforeEmbed: async ({ chunkCount }) => {
226
- await requireLimit(routeDeps, {
227
- accountId,
228
- workspaceId,
229
- action: "document:index",
230
- quantity: chunkCount,
231
- });
232
- },
233
- },
234
453
  { viewerSubjectId: authoritySubjectId },
235
454
  );
236
455
  if (
@@ -249,8 +468,24 @@ export function createAppComposition(deps: AppDependencies): {
249
468
  // concrete for routes; it throws SandboxResumeError when sandboxBackend=none.
250
469
  const sandboxClient = deps.sandboxClient ?? createApiSandboxClient(deps.settings);
251
470
  const resumeBoxById = deps.resumeBoxById ?? makeResumeBoxById(sandboxClient);
252
- const observability =
253
- deps.observability ?? createObservability(deps.settings, { component: "api" });
471
+ if (
472
+ managedAuth &&
473
+ deps.settings.managedAuthSessionSetMode !== "legacy" &&
474
+ !managedAuthReaperDatabases.has(deps.db as object)
475
+ ) {
476
+ managedAuthReaperDatabases.add(deps.db as object);
477
+ const timer = setInterval(() => {
478
+ void Promise.all([
479
+ reapManagedAuthIsolatedSessions(deps.db, 100),
480
+ reapExpiredManagedAuthSessionSets(deps.db, 100),
481
+ ]).catch((error) => {
482
+ observability.error("Managed isolated-auth orphan reap failed", {
483
+ errorClass: error instanceof Error ? error.name : "UnknownError",
484
+ });
485
+ });
486
+ }, MANAGED_AUTH_REAPER_INTERVAL_MS);
487
+ (timer as ReturnType<typeof setInterval> & { unref?: () => void }).unref?.();
488
+ }
254
489
  const transcription =
255
490
  deps.transcription === undefined
256
491
  ? createTranscriptionService({
@@ -265,12 +500,26 @@ export function createAppComposition(deps: AppDependencies): {
265
500
  ffmpegPath: deps.settings.voiceInputFfmpegPath,
266
501
  })
267
502
  : deps.transcriptionSegmenter;
503
+ registerProductUsageMetricBaselines(observability);
504
+ // Server-side presence counts managed people, so it exists only where
505
+ // canonical managed browser sessions exist. Writes are batched off the
506
+ // request path; see packages/core/src/user-presence.ts.
507
+ const userPresence =
508
+ deps.userPresence !== undefined
509
+ ? deps.userPresence
510
+ : deps.settings.productAccessMode === "managed"
511
+ ? createUserPresenceRecorder({ db: deps.db, observability })
512
+ : null;
268
513
  const routeDeps: ApiRouteDeps = {
269
514
  ...deps,
515
+ resolveCatalogSettings: () => resolveCatalogSettings(deps.db, deps.settings),
270
516
  observability,
517
+ userPresence,
271
518
  githubStateSecret:
272
519
  deps.githubStateSecret ?? deps.settings.githubAppManifestStateSecret ?? crypto.randomUUID(),
273
520
  managedAuth,
521
+ managedAuthSessionAdapter,
522
+ managedEmailTransport,
274
523
  objectStorage,
275
524
  documentIndexer,
276
525
  getDocumentServices,
@@ -280,16 +529,107 @@ export function createAppComposition(deps: AppDependencies): {
280
529
  resumeBoxById,
281
530
  };
282
531
  const app = new Hono();
283
- const correlationIds = new WeakMap<Request, string>();
532
+ // Body/auth middleware may replace c.req.raw. The Hono context remains the
533
+ // same for the response, diagnostic and completion log of this request.
534
+ const correlationIds = new WeakMap<Context, string>();
535
+ const loggedHttpFailures = new WeakSet<Context>();
536
+ const logHttpFailure = (c: Context, status: number, code: ErrorCode, error?: unknown) => {
537
+ if (loggedHttpFailures.has(c)) return;
538
+ loggedHttpFailures.add(c);
539
+ const correlationId = correlationIds.get(c) ?? crypto.randomUUID();
540
+ const route = routeLabel(new URL(c.req.url).pathname, registeredHandlerRoutePath(c));
541
+ const input =
542
+ error === undefined
543
+ ? null
544
+ : {
545
+ code: "http_request_failed" as const,
546
+ stage: "http.request" as const,
547
+ error,
548
+ };
549
+ const diagnostic = input ? failureDiagnostic(input, deps.settings.deploymentRevision) : null;
550
+ // Diagnostics and logging are observers, never a new response failure.
551
+ try {
552
+ if (input && diagnostic) {
553
+ observability.recordFailureDiagnostic({ ...input, diagnosticId: diagnostic.diagnosticId });
554
+ }
555
+ } catch {
556
+ /* Preserve the original error response if the diagnostic sink fails. */
557
+ }
558
+ try {
559
+ observability.error("HTTP request failed", {
560
+ method: c.req.method,
561
+ route,
562
+ status,
563
+ correlationId,
564
+ errorCode: code,
565
+ errorClass: "HttpOperationError",
566
+ origin: "api",
567
+ reasonKind: diagnostic?.causes[0]?.kind ?? "Response",
568
+ ...(diagnostic ? { diagnosticId: diagnostic.diagnosticId } : {}),
569
+ });
570
+ observability.incrementCounter({
571
+ name: "opengeni_http_errors_total",
572
+ help: "Total OpenGeni HTTP request failures by bounded route, status, and stable code.",
573
+ labels: { route, status: String(status), code },
574
+ });
575
+ } catch {
576
+ /* Preserve the original error response if the logger fails. */
577
+ }
578
+ };
284
579
 
285
580
  app.use("*", async (c, next) => {
286
581
  const correlationId =
287
582
  boundedCorrelationId(c.req.header(OPENGENI_CORRELATION_HEADER)) ?? crypto.randomUUID();
288
- correlationIds.set(c.req.raw, correlationId);
583
+ correlationIds.set(c, correlationId);
289
584
  c.header(OPENGENI_CORRELATION_HEADER, correlationId);
290
585
  await next();
291
586
  });
292
587
 
588
+ // Unauthenticated local mode: admit only requests addressed to this computer
589
+ // and, when a browser sent them, from this stack's web app (see
590
+ // http/local-browser-boundary.ts). Runs before CORS so a refused preflight
591
+ // carries no CORS grant, and logs each distinct refused Host or Origin once
592
+ // because the browser shows only a generic CORS error. Null outside local
593
+ // development.
594
+ const localBrowserBoundary = createLocalBrowserBoundary(deps.settings, {
595
+ warn: (message, attributes) => observability.warn(message, attributes),
596
+ });
597
+ if (localBrowserBoundary) {
598
+ app.use("*", async (c, next) => {
599
+ const rejection = localBrowserBoundary.rejection(c.req.raw);
600
+ if (rejection) {
601
+ throw new ApiHttpError(rejection.status, {
602
+ code: "forbidden",
603
+ message: rejection.message,
604
+ retryable: false,
605
+ details: { code: rejection.code },
606
+ });
607
+ }
608
+ await next();
609
+ });
610
+ }
611
+
612
+ // Better Auth keys its rate limits and session addresses on a request
613
+ // header. Drop any caller-supplied copy everywhere and stamp the trusted
614
+ // source address on managed-auth routes before any route derives a Better
615
+ // Auth request from this one.
616
+ app.use("*", async (c, next) => {
617
+ replaceTrustedClientAddressHeader(c, deps.settings, c.req.path.startsWith("/v1/auth/"));
618
+ await next();
619
+ });
620
+
621
+ app.use("*", async (c, next) => {
622
+ const oauthToken = mcpOAuthBearerToken(c.req.raw);
623
+ if (
624
+ oauthToken &&
625
+ (!deps.settings.mcpOauthEnabled || !isMcpOAuthResourcePath(new URL(c.req.url).pathname))
626
+ ) {
627
+ c.header("www-authenticate", 'Bearer error="invalid_token"');
628
+ return c.json({ error: "invalid_token" }, 401);
629
+ }
630
+ await next();
631
+ });
632
+
293
633
  const corsHeaders = {
294
634
  allowHeaders: [
295
635
  "Accept",
@@ -298,14 +638,39 @@ export function createAppComposition(deps: AppDependencies): {
298
638
  "Range",
299
639
  "X-OpenGeni-Access-Key",
300
640
  "X-OpenGeni-Api-Contract",
641
+ "X-OpenGeni-Actor-Epoch",
301
642
  "X-OpenGeni-Correlation-Id",
643
+ "X-OpenGeni-Session-Csrf",
644
+ "X-OpenGeni-Site-Id",
645
+ "X-OpenGeni-Site-Version",
302
646
  "X-OpenGeni-Subject",
647
+ "X-OpenGeni-User-Activity",
303
648
  ],
304
649
  exposeHeaders: [
305
650
  "Accept-Ranges",
306
651
  "Content-Range",
652
+ "Deprecation",
653
+ "Link",
654
+ "Sunset",
307
655
  "X-OpenGeni-Api-Contract",
656
+ "X-OpenGeni-Actor-Epoch",
657
+ "X-OpenGeni-Actor-State",
308
658
  "X-OpenGeni-Correlation-Id",
659
+ "X-OpenGeni-Covered-First",
660
+ "X-OpenGeni-Covered-Last",
661
+ "X-OpenGeni-Event-Direction",
662
+ "X-OpenGeni-Event-Mode",
663
+ "X-OpenGeni-Event-Result",
664
+ "X-OpenGeni-Event-Result-Mode",
665
+ "X-OpenGeni-Forensic-Exact",
666
+ "X-OpenGeni-Has-More",
667
+ "X-OpenGeni-Next-After",
668
+ "X-OpenGeni-Next-Before",
669
+ "X-OpenGeni-Page-Bytes",
670
+ "X-OpenGeni-Page-Max-Bytes",
671
+ "X-OpenGeni-Page-Truncated",
672
+ "X-OpenGeni-Payload-Mode",
673
+ "X-OpenGeni-Truncated-By",
309
674
  ],
310
675
  };
311
676
  const publicApiCors = cors({
@@ -320,8 +685,25 @@ export function createAppComposition(deps: AppDependencies): {
320
685
  allowedCorsOrigin(deps.settings.corsAllowOriginRegex, origin) ? origin : null,
321
686
  });
322
687
 
688
+ const localCors = localBrowserBoundary
689
+ ? cors({
690
+ ...corsHeaders,
691
+ credentials: true,
692
+ origin: (origin, c) =>
693
+ localBrowserBoundary.originAllowed(origin, localBrowserRequestHost(c.req.raw))
694
+ ? origin
695
+ : null,
696
+ })
697
+ : null;
698
+
323
699
  app.use("*", (c, next) => {
324
700
  const origin = c.req.header("origin");
701
+ if (localCors) {
702
+ // The boundary above already refused every other origin. Local mode
703
+ // never answers with wildcard CORS: a request without credentials acts
704
+ // as the local user, so any site could otherwise read its responses.
705
+ return origin ? localCors(c, next) : next();
706
+ }
325
707
  const middleware =
326
708
  origin && allowedCorsOrigin(deps.settings.corsAllowOriginRegex, origin)
327
709
  ? credentialedCors
@@ -329,14 +711,32 @@ export function createAppComposition(deps: AppDependencies): {
329
711
  return middleware(c, next);
330
712
  });
331
713
 
332
- app.use(
333
- "*",
334
- bodyLimit({
335
- maxSize: apiRequestBodyLimitBytes(deps.settings),
336
- onError: (c) =>
337
- c.json({ code: "PAYLOAD_TOO_LARGE", message: "Request body is too large." }, 413),
338
- }),
339
- );
714
+ const standardRequestBodyLimit = bodyLimit({
715
+ maxSize: apiRequestBodyLimitBytes(deps.settings),
716
+ onError: (c) =>
717
+ c.json({ code: "PAYLOAD_TOO_LARGE", message: "Request body is too large." }, 413),
718
+ });
719
+ app.use("*", async (c, next) => {
720
+ // Git packfiles must stay streaming and can legitimately exceed the JSON
721
+ // request ceiling. The exact closed broker routes apply their own method,
722
+ // content-type, authority, and idle-deadline checks.
723
+ const pathname = new URL(c.req.url).pathname;
724
+ if (isPersonalGitHubGitBrokerRequest(c.req.method, pathname)) {
725
+ await next();
726
+ return;
727
+ }
728
+ // The anonymous web beacons enforce their own small limits on the
729
+ // streamed body; the generic ceiling would buffer far more first.
730
+ if (
731
+ isClientErrorReportRequest(c.req.method, pathname) ||
732
+ isAnalyticsConsentReportRequest(c.req.method, pathname) ||
733
+ isSessionHistoryImportRequest(c.req.method, pathname)
734
+ ) {
735
+ await next();
736
+ return;
737
+ }
738
+ return await standardRequestBodyLimit(c, next);
739
+ });
340
740
 
341
741
  // Large catalog, capture, and session-list responses are on the browser's
342
742
  // critical path. Compress JSON at the API boundary while leaving SSE and
@@ -352,90 +752,146 @@ export function createAppComposition(deps: AppDependencies): {
352
752
  }
353
753
  });
354
754
 
755
+ // Public-route deprecations (docs/design/api-compatibility-policy.md) are
756
+ // advertised on every response of the affected route, errors included.
757
+ app.use("/v1/*", deprecationHeadersMiddleware());
758
+ // A pinned SDK must not be told about a revision it is built to reject; see
759
+ // the policy's contract-header rule. Registered before the contract fence so
760
+ // it observes the header that fence sets.
761
+ app.use("/v1/*", bearerApiContractHeaderCompatibility());
762
+
355
763
  app.use("*", async (c, next) => {
356
764
  const url = new URL(c.req.url);
357
- const route = routeLabel(url.pathname);
358
- const correlationId = correlationIds.get(c.req.raw) ?? crypto.randomUUID();
765
+ const route = routeLabel(url.pathname, registeredHandlerRoutePath(c));
766
+ const correlationId = correlationIds.get(c) ?? crypto.randomUUID();
359
767
  const start = performance.now();
360
- const span = observability.startSpan(`HTTP ${c.req.method} ${route}`, {
361
- "http.request.method": c.req.method,
362
- "opengeni.route": route,
363
- });
364
- try {
768
+ const span = observability.startSpan(
769
+ `HTTP ${c.req.method} ${route}`,
770
+ {
771
+ "http.request.method": c.req.method,
772
+ "opengeni.route": route,
773
+ },
774
+ { parent: null },
775
+ );
776
+ return await withMcpTelemetry(observability, span.traceId, () =>
777
+ withTraceContext(span, async () => {
778
+ try {
779
+ await next();
780
+ const status = c.res.status || 200;
781
+ // Hono normally handles exceptions in onError before next() resolves.
782
+ // Also cover a handler returning a 5xx without throwing.
783
+ if (status >= 500) logHttpFailure(c, status, errorCodeForStatus(status), c.error);
784
+ const durationSeconds = (performance.now() - start) / 1000;
785
+ observability.recordHttpRequest({
786
+ method: c.req.method,
787
+ route,
788
+ status,
789
+ durationSeconds,
790
+ });
791
+ span.end({
792
+ attributes: {
793
+ "http.response.status_code": status,
794
+ "opengeni.duration_ms": Math.round(durationSeconds * 1000),
795
+ },
796
+ ...(c.error ? { error: c.error } : {}),
797
+ });
798
+ observability.info("HTTP request completed", {
799
+ method: c.req.method,
800
+ route,
801
+ status,
802
+ durationMs: Math.round(durationSeconds * 1000),
803
+ traceId: span.traceId,
804
+ spanId: span.spanId,
805
+ correlationId,
806
+ });
807
+ } catch (error) {
808
+ const status = httpStatusForError(error);
809
+ const errorCode = errorCodeForStatus(status);
810
+ const durationSeconds = (performance.now() - start) / 1000;
811
+ observability.recordHttpRequest({
812
+ method: c.req.method,
813
+ route,
814
+ status,
815
+ durationSeconds,
816
+ });
817
+ span.end({
818
+ attributes: {
819
+ "http.response.status_code": status,
820
+ "opengeni.duration_ms": Math.round(durationSeconds * 1000),
821
+ },
822
+ error,
823
+ });
824
+ logHttpFailure(c, status, errorCode, error);
825
+ throw error;
826
+ }
827
+ }),
828
+ );
829
+ });
830
+
831
+ const accessKeyBoundary = requireAccessKey(deps.settings);
832
+ app.use("*", async (c, next) => {
833
+ // Git's credential helper supplies the exact encrypted broker bearer in
834
+ // Authorization. It cannot also supply the deployment access key, and the
835
+ // route performs its own attempt-bound authorization before every use.
836
+ if (isPersonalGitHubGitBrokerRequest(c.req.method, new URL(c.req.url).pathname)) {
365
837
  await next();
366
- const status = c.res.status || 200;
367
- const durationSeconds = (performance.now() - start) / 1000;
368
- observability.recordHttpRequest({
369
- method: c.req.method,
370
- route,
371
- status,
372
- durationSeconds,
373
- });
374
- span.end({
375
- attributes: {
376
- "http.response.status_code": status,
377
- "opengeni.duration_ms": Math.round(durationSeconds * 1000),
378
- },
379
- });
380
- observability.info("HTTP request completed", {
381
- method: c.req.method,
382
- route,
383
- status,
384
- durationMs: Math.round(durationSeconds * 1000),
385
- traceId: span.traceId,
386
- spanId: span.spanId,
387
- correlationId,
388
- });
389
- } catch (error) {
390
- const status = httpStatusForError(error);
391
- const errorCode = errorCodeForStatus(status);
392
- const durationSeconds = (performance.now() - start) / 1000;
393
- observability.recordHttpRequest({
394
- method: c.req.method,
395
- route,
396
- status,
397
- durationSeconds,
398
- });
399
- observability.incrementCounter({
400
- name: "opengeni_http_errors_total",
401
- help: "Total OpenGeni HTTP request failures by bounded route, status, and stable code.",
402
- labels: { route, status: String(status), code: errorCode },
403
- });
404
- span.end({
405
- attributes: {
406
- "http.response.status_code": status,
407
- "opengeni.duration_ms": Math.round(durationSeconds * 1000),
408
- },
409
- error,
410
- });
411
- observability.error("HTTP request failed", {
412
- method: c.req.method,
413
- route,
414
- status,
415
- durationMs: Math.round(durationSeconds * 1000),
416
- traceId: span.traceId,
417
- spanId: span.spanId,
418
- correlationId,
419
- errorCode,
420
- errorClass: "HttpOperationError",
421
- });
422
- throw error;
838
+ return;
423
839
  }
840
+ return await accessKeyBoundary(c, next);
841
+ });
842
+
843
+ // Malformed JSON in a client request body is a 400 wherever a route reads it.
844
+ app.use("/v1/*", async (c, next) => {
845
+ tagRequestJsonParseErrors(c);
846
+ await next();
424
847
  });
425
848
 
426
- app.use("*", requireAccessKey(deps.settings));
849
+ // A request no registered handler answers is a 404 (or a 405 when the path
850
+ // exists for other methods) before any authentication, authorization, or
851
+ // contract middleware can turn it into a misleading 401/409/503. This runs
852
+ // after the deployment perimeter so an unauthenticated caller of a
853
+ // key-protected deployment learns nothing new.
854
+ app.use("/v1/*", async (c, next) => {
855
+ const unmatched = unmatchedRoute(app, c);
856
+ if (!unmatched) {
857
+ await next();
858
+ return;
859
+ }
860
+ const requestId = correlationIds.get(c) ?? crypto.randomUUID();
861
+ c.header(OPENGENI_API_CONTRACT_HEADER, OPENGENI_API_CONTRACT_REVISION);
862
+ if (unmatched.status === 405) c.header("allow", unmatched.allow.join(", "));
863
+ return c.json(
864
+ ErrorEnvelope.parse({
865
+ error: {
866
+ status: unmatched.status,
867
+ code: "not_found",
868
+ message:
869
+ unmatched.status === 405
870
+ ? `Method ${c.req.method} is not supported for this resource. Allowed: ${unmatched.allow.join(", ")}.`
871
+ : "Resource not found.",
872
+ retryable: false,
873
+ requestId,
874
+ },
875
+ }),
876
+ unmatched.status,
877
+ );
878
+ });
427
879
 
428
880
  app.use("/v1/*", async (c, next) => {
429
881
  c.header(OPENGENI_API_CONTRACT_HEADER, OPENGENI_API_CONTRACT_REVISION);
430
882
  if (
431
883
  deps.settings.environment !== "test" &&
432
- isApiContractProtectedMutation(c.req.method, new URL(c.req.url).pathname) &&
433
- c.req.header(OPENGENI_API_CONTRACT_HEADER) !== OPENGENI_API_CONTRACT_REVISION
884
+ apiContractAdmission({
885
+ method: c.req.method,
886
+ pathname: new URL(c.req.url).pathname,
887
+ authorization: c.req.header("authorization"),
888
+ claimedRevision: c.req.header(OPENGENI_API_CONTRACT_HEADER),
889
+ }) === "reject"
434
890
  ) {
435
891
  return c.json(
436
892
  {
437
893
  code: "API_CONTRACT_CHANGED",
438
- message: "OpenGeni updated. Reload this client before changing state.",
894
+ message: "Opengeni updated. Reload this client before changing state.",
439
895
  apiContractRevision: OPENGENI_API_CONTRACT_REVISION,
440
896
  },
441
897
  409,
@@ -444,19 +900,255 @@ export function createAppComposition(deps: AppDependencies): {
444
900
  await next();
445
901
  });
446
902
 
903
+ app.use("/v1/*", async (c, next) => {
904
+ try {
905
+ await next();
906
+ try {
907
+ await validateManagedAuthRequestActorLease(c.req.raw);
908
+ } catch (error) {
909
+ if (error instanceof ManagedAuthActorChangeError) {
910
+ c.header("x-opengeni-actor-state", "changed");
911
+ throw new HTTPException(409, { message: error.code, cause: error });
912
+ }
913
+ if (error instanceof ManagedAuthActorLeaseOutcomeUnknownError) {
914
+ throw new ApiHttpError(503, {
915
+ code: "upstream_unavailable",
916
+ message: error.code,
917
+ retryable: true,
918
+ outcomeUnknown: true,
919
+ details: { managedAuthCode: error.code },
920
+ });
921
+ }
922
+ throw error;
923
+ }
924
+ } finally {
925
+ await releaseManagedAuthRequestActorLease(c.req.raw).catch((error) => {
926
+ observability.error("Managed actor mutation lease release failed", {
927
+ errorClass: error instanceof Error ? error.name : "UnknownError",
928
+ });
929
+ });
930
+ }
931
+ });
932
+
933
+ // These product-owned auth routes must be registered before Better Auth's
934
+ // wildcard handler or the provider returns its own 404 first.
935
+ registerManagedOnboardingRoutes(app, routeDeps);
936
+ registerManagedAuthSessionSetRoutes(app, routeDeps);
937
+ registerManagedSignInMethodRoutes(app, routeDeps);
447
938
  if (managedAuth) {
448
- app.on(["GET", "POST"], "/v1/auth/*", (c) => managedAuth.handler(c.req.raw));
939
+ app.on(["GET", "POST"], "/v1/auth/*", async (c) => {
940
+ const pathname = new URL(c.req.url).pathname;
941
+ const oauthCallbackProvider = managedAuthOAuthCallbackProvider(pathname);
942
+ if (
943
+ pathname === "/v1/auth/sign-up/email" &&
944
+ c.req.method === "POST" &&
945
+ !(await managedAuthNewSignupsGate.signupsOpen())
946
+ ) {
947
+ // Launch-load safety switch: refuse before Better Auth hashes the
948
+ // password or sends mail. Better Auth's own disableSignUp flags and
949
+ // the user-create hook remain the backstop for every other path.
950
+ return managedAuthNewSignupsPausedResponse();
951
+ }
952
+ if (pathname === "/v1/auth/sign-in/social" && c.req.method === "POST") {
953
+ const body = await c.req.raw
954
+ .clone()
955
+ .json()
956
+ .catch(() => null);
957
+ if (
958
+ !body ||
959
+ !["google", "github"].includes(body.provider) ||
960
+ Object.prototype.hasOwnProperty.call(body, "idToken")
961
+ ) {
962
+ return c.json(
963
+ {
964
+ code: "SIGN_IN_METHOD_OAUTH_REDIRECT_REQUIRED",
965
+ message: "Use the browser OAuth sign-in redirect",
966
+ },
967
+ 403,
968
+ );
969
+ }
970
+ }
971
+ if (oauthCallbackProvider) {
972
+ const connectResponse = await handleManagedSignInConnectCallback(
973
+ c,
974
+ routeDeps,
975
+ oauthCallbackProvider,
976
+ );
977
+ if (connectResponse) return connectResponse;
978
+ }
979
+ if (
980
+ new Set([
981
+ "link-social",
982
+ "unlink-account",
983
+ "list-accounts",
984
+ "set-password",
985
+ "change-password",
986
+ "change-email",
987
+ "update-user",
988
+ "delete-user",
989
+ "get-access-token",
990
+ "refresh-token",
991
+ "account-info",
992
+ ]).has(pathname.slice("/v1/auth/".length))
993
+ ) {
994
+ return c.json(
995
+ {
996
+ code: "SIGN_IN_METHOD_PRODUCT_ROUTE_REQUIRED",
997
+ message: "Use personal sign-in method settings",
998
+ },
999
+ 403,
1000
+ );
1001
+ }
1002
+ if (deps.settings.managedAuthSessionSetMode === "legacy") {
1003
+ return oauthCallbackProvider
1004
+ ? await runManagedAuthProvider(
1005
+ oauthCallbackProvider,
1006
+ async () => await managedAuth.handler(c.req.raw),
1007
+ )
1008
+ : await managedAuth.handler(c.req.raw);
1009
+ }
1010
+ requireManagedAuthProviderRouteAllowed(c.req.method, pathname);
1011
+ // Provider authentication/recovery is isolated from whichever actor the
1012
+ // browser currently renders. Selected-session capabilities are all
1013
+ // product-owned above this wildcard and generation/epoch fenced.
1014
+ const authority = getCookie(c, MANAGED_AUTH_SESSION_SET_COOKIE);
1015
+ let providerResponse: Response;
1016
+ let preserveCookieNames: readonly string[] | undefined;
1017
+ if (oauthCallbackProvider) {
1018
+ const attempt = await resolveManagedAuthOAuthAttempt(
1019
+ managedAuth,
1020
+ c.req.raw,
1021
+ oauthCallbackProvider,
1022
+ deps.settings.publicBaseUrl!,
1023
+ );
1024
+ if (!attempt || !authority || attempt.authorityHash !== managedAuthSha256(authority)) {
1025
+ throw new HTTPException(409, { message: "provider_route_blocked" });
1026
+ }
1027
+ const isolated = await isolatedManagedAuthOAuthCallbackRequest(managedAuth, c.req.raw);
1028
+ preserveCookieNames = [isolated.stateCookieName];
1029
+ const handled = await runManagedAuthAttempt(
1030
+ attempt.transactionId,
1031
+ oauthCallbackProvider,
1032
+ async () => {
1033
+ const response = await managedAuth.handler(isolated.request);
1034
+ return {
1035
+ response,
1036
+ authSessionId: currentManagedAuthCreatedSessionId(),
1037
+ };
1038
+ },
1039
+ );
1040
+ providerResponse = handled.response;
1041
+ if (
1042
+ !handled.authSessionId &&
1043
+ managedAuthOAuthReturnMatches(
1044
+ providerResponse.headers.get("location") ?? "",
1045
+ new URL(deps.settings.publicBaseUrl!).origin,
1046
+ attempt.transactionId,
1047
+ "complete",
1048
+ )
1049
+ ) {
1050
+ const location = new URL("/account-auth", deps.settings.publicBaseUrl!);
1051
+ location.searchParams.set("transaction", attempt.transactionId);
1052
+ location.searchParams.set("social", "error");
1053
+ providerResponse = Response.redirect(location, 302);
1054
+ } else if (handled.authSessionId) {
1055
+ try {
1056
+ await adoptManagedAuthSession({
1057
+ db: deps.db,
1058
+ adapter: managedAuthSessionAdapter!,
1059
+ authority,
1060
+ authorityHash: attempt.authorityHash,
1061
+ csrfHash: managedAuthCsrfHash(authority),
1062
+ operationId: managedAuthDerivedUuid(
1063
+ "opengeni:managed-auth:social-completion",
1064
+ `${attempt.transactionId}:${attempt.provider}`,
1065
+ ),
1066
+ requestDigest: managedAuthSecretRequestDigest(deps.settings.betterAuthSecret!, {
1067
+ operation: "social_completion",
1068
+ transactionId: attempt.transactionId,
1069
+ provider: attempt.provider,
1070
+ expectedGeneration: attempt.expectedGeneration,
1071
+ expectedActorEpoch: attempt.expectedActorEpoch,
1072
+ }),
1073
+ expectedGeneration: attempt.expectedGeneration,
1074
+ expectedActorEpoch: attempt.expectedActorEpoch,
1075
+ transactionId: attempt.transactionId,
1076
+ transactionSecretHash: attempt.transactionSecretHash,
1077
+ authSessionId: handled.authSessionId,
1078
+ mode: deps.settings.managedAuthSessionSetMode,
1079
+ });
1080
+ } catch {
1081
+ // adoptManagedAuthSession revokes only when durable reconciliation
1082
+ // proves that completion did not commit. An uncertain outcome may
1083
+ // already point the selected slot at this session, so the callback
1084
+ // must not independently revoke it.
1085
+ const location = new URL("/account-auth", deps.settings.publicBaseUrl!);
1086
+ location.searchParams.set("transaction", attempt.transactionId);
1087
+ location.searchParams.set("social", "error");
1088
+ providerResponse = Response.redirect(location, 302);
1089
+ }
1090
+ }
1091
+ } else {
1092
+ const headers = new Headers(c.req.raw.headers);
1093
+ headers.delete("cookie");
1094
+ headers.delete("authorization");
1095
+ headers.delete("x-forwarded-user");
1096
+ const providerRequest = new Request(c.req.raw, { headers });
1097
+ const discardProviderSession =
1098
+ deps.settings.managedAuthSessionSetMode === "broker" || authority !== undefined;
1099
+ providerResponse = discardProviderSession
1100
+ ? await runManagedAuthDiscardedProviderSession(
1101
+ async () => await managedAuth.handler(providerRequest),
1102
+ )
1103
+ : await managedAuth.handler(providerRequest);
1104
+ }
1105
+ let replacementCookies: readonly string[] | undefined;
1106
+ if (deps.settings.managedAuthSessionSetMode === "broker") {
1107
+ replacementCookies = await managedAuthSessionAdapter!.createLegacySelectedSessionCookies(
1108
+ null,
1109
+ c.req.header("cookie") ?? null,
1110
+ );
1111
+ } else {
1112
+ if (authority !== undefined) {
1113
+ const snapshot = await getManagedAuthSessionSetSnapshot(deps.db, {
1114
+ authorityHash: managedAuthSha256(authority),
1115
+ mode: "dual",
1116
+ includeInternal: true,
1117
+ readOnly: true,
1118
+ });
1119
+ if (snapshot) {
1120
+ replacementCookies =
1121
+ await managedAuthSessionAdapter!.createLegacySelectedSessionCookies(
1122
+ snapshot.selected,
1123
+ c.req.header("cookie") ?? null,
1124
+ );
1125
+ } else {
1126
+ replacementCookies =
1127
+ await managedAuthSessionAdapter!.createLegacySelectedSessionCookies(
1128
+ null,
1129
+ c.req.header("cookie") ?? null,
1130
+ );
1131
+ }
1132
+ }
1133
+ }
1134
+ return await scrubManagedAuthProviderResponse(providerResponse, {
1135
+ replacementCookies,
1136
+ preserveCookieNames,
1137
+ });
1138
+ });
449
1139
  }
450
1140
 
451
- app.get("/healthz", (c) =>
452
- c.json({
1141
+ app.get("/healthz", (c) => {
1142
+ const warnings = githubAppBotIdentityWarnings(deps.settings);
1143
+ return c.json({
453
1144
  service: deps.settings.serviceName,
454
1145
  environment: deps.settings.environment,
455
1146
  deploymentRevision: deps.settings.deploymentRevision,
456
1147
  ...(deps.settings.serverVersion ? { serverVersion: deps.settings.serverVersion } : {}),
1148
+ ...(warnings.length > 0 ? { warnings } : {}),
457
1149
  ok: true,
458
- }),
459
- );
1150
+ });
1151
+ });
460
1152
 
461
1153
  app.get("/readyz", async (c) => {
462
1154
  const result = await runReadinessChecks(readinessChecks(deps), 2_000);
@@ -469,44 +1161,146 @@ export function createAppComposition(deps: AppDependencies): {
469
1161
  return c.json(result, result.ok ? 200 : 503);
470
1162
  });
471
1163
 
472
- app.get("/metrics", async (c) =>
473
- c.text(await observability.prometheusMetrics(), 200, {
474
- "content-type": "text/plain; version=0.0.4; charset=utf-8",
475
- }),
476
- );
1164
+ if (publicListenerServesMetrics(deps.settings)) {
1165
+ registerPrometheusMetricsRoute(app, observability);
1166
+ }
1167
+
1168
+ registerMcpOAuthRoutes(app, routeDeps);
1169
+
1170
+ registerClientErrorRoutes(app, { observability, settings: deps.settings });
1171
+ registerAnalyticsConsentRoutes(app, { observability, settings: deps.settings });
477
1172
 
478
1173
  app.get("/v1/config/client", async (c) => {
479
1174
  c.header("cache-control", "no-store");
480
- const codexCatalogSettings = deps.settings.codexSubscriptionEnabled
481
- ? withCodexCatalogProvider(deps.settings)
482
- : deps.settings;
483
- const catalogSettings = deps.settings.supergrokSubscriptionEnabled
484
- ? withXaiSubscriptionCatalogProvider(codexCatalogSettings)
485
- : codexCatalogSettings;
1175
+ const resolvedCatalog = await resolveCatalogSettings(deps.db, deps.settings);
1176
+ let catalogSettings = resolvedCatalog.settings;
1177
+ let selections = resolveWorkspaceModelSelection({
1178
+ settings: catalogSettings,
1179
+ policy: null,
1180
+ codexSubscriptionActive: false,
1181
+ });
1182
+ let defaultSelection = selectDefaultSessionModel({
1183
+ settings: catalogSettings,
1184
+ selections,
1185
+ workspaceDefaults: null,
1186
+ creditsAvailable: false,
1187
+ });
1188
+ let modelSelectionForbidden = false;
1189
+ const requestedWorkspaceId = c.req.query("workspaceId");
1190
+ if (requestedWorkspaceId !== undefined && requestedWorkspaceId.trim() === "") {
1191
+ throw new HTTPException(422, { message: "workspaceId must not be empty" });
1192
+ }
1193
+ const callerScoped =
1194
+ requestedWorkspaceId !== undefined ||
1195
+ deps.settings.productAccessMode !== "managed" ||
1196
+ c.req.header("authorization") !== undefined ||
1197
+ // Browser bootstrap precedes session-set reconciliation. A cookie alone
1198
+ // cannot scope this public read (broker/changed actors need an epoch).
1199
+ c.req.header("x-opengeni-actor-epoch") !== undefined ||
1200
+ c.req.header("x-opengeni-external-actor") !== undefined;
1201
+ if (callerScoped) {
1202
+ let context: Awaited<ReturnType<typeof requireAccessContext>> | null = null;
1203
+ try {
1204
+ context = await requireAccessContext(c, routeDeps);
1205
+ } catch (error) {
1206
+ // A stale browser cookie must not break the signed-out bootstrap.
1207
+ // Explicit workspace/bearer/external-actor requests fail closed.
1208
+ if (
1209
+ !(error instanceof HTTPException && error.status === 401) ||
1210
+ requestedWorkspaceId !== undefined ||
1211
+ c.req.header("authorization") !== undefined ||
1212
+ c.req.header("x-opengeni-actor-epoch") !== undefined ||
1213
+ c.req.header("x-opengeni-external-actor") !== undefined
1214
+ ) {
1215
+ throw error;
1216
+ }
1217
+ }
1218
+ const workspaceId = requestedWorkspaceId ?? context?.defaultWorkspaceId;
1219
+ if (context && workspaceId) {
1220
+ const grant = await requireAccessGrant(c, routeDeps, workspaceId);
1221
+ await withAccessGrantSessionRlsContext(routeDeps, grant, async () => {
1222
+ catalogSettings = (
1223
+ await resolveWorkspaceCatalogSettings(deps.db, deps.settings, {
1224
+ accountId: grant.accountId,
1225
+ workspaceId,
1226
+ })
1227
+ ).settings;
1228
+ selections = await resolveCallerWorkspaceModelSelections(
1229
+ deps.db,
1230
+ catalogSettings,
1231
+ { accountId: grant.accountId, workspaceId, subjectId: grant.subjectId },
1232
+ { observeAvailability: true },
1233
+ );
1234
+ if (!hasPermission(grant.permissions, "sessions:create")) {
1235
+ modelSelectionForbidden = true;
1236
+ selections = [];
1237
+ }
1238
+ const workspace = await getWorkspace(deps.db, workspaceId);
1239
+ defaultSelection = await resolveDefaultSessionModelForSelections(deps.db, {
1240
+ settings: catalogSettings,
1241
+ accountId: grant.accountId,
1242
+ workspaceSettings: workspace?.settings ?? {},
1243
+ selections,
1244
+ });
1245
+ });
1246
+ } else if (context) {
1247
+ // An organization key has no implicit workspace. Do not advertise
1248
+ // another workspace's authority; it can request an exact workspace.
1249
+ selections = [];
1250
+ modelSelectionForbidden = true;
1251
+ }
1252
+ }
1253
+ const models = selections.filter(isWorkspaceModelAdmissible);
1254
+ const defaultModel = defaultSelection.model;
1255
+ const fallbackDefinition = selections.find(
1256
+ ({ model }) => model.id === defaultModel || model.aliases.includes(defaultModel),
1257
+ );
1258
+ const legacyModelFallback =
1259
+ models.length === 0
1260
+ ? {
1261
+ id: defaultModel,
1262
+ availability: {
1263
+ status: "unavailable" as const,
1264
+ selectable: false as const,
1265
+ reason: modelSelectionForbidden
1266
+ ? ("policy_blocked" as const)
1267
+ : (fallbackDefinition?.availability.reason ?? ("unsupported" as const)),
1268
+ checkedAt: fallbackDefinition?.availability.checkedAt ?? null,
1269
+ },
1270
+ }
1271
+ : undefined;
486
1272
  return c.json(
487
1273
  ClientConfig.parse({
488
1274
  deploymentRevision: deps.settings.deploymentRevision,
1275
+ claudeSubscriptionEnabled: deps.settings.claudeSubscriptionEnabled,
489
1276
  apiContractRevision: OPENGENI_API_CONTRACT_REVISION,
490
1277
  ...(deps.settings.serverVersion ? { serverVersion: deps.settings.serverVersion } : {}),
491
- defaultModel: canonicalizeConfiguredModelId(catalogSettings, catalogSettings.openaiModel),
492
- allowedModels: configuredAllowedModels(catalogSettings),
493
- // Provider-grouped model list for the picker. configuredModels() carries the
494
- // union of the built-in allow-list and every registry provider's models, in
495
- // selection order (default model first); project each to the client-safe
496
- // provider-blind ClientModel shape (execution topology remains server-side).
497
- models: configuredModels(catalogSettings).map(projectClientModel),
498
- defaultReasoningEffort: deps.settings.openaiReasoningEffort,
1278
+ defaultModel,
1279
+ allowedModels: models.length > 0 ? models.map(({ model }) => model.id) : [defaultModel],
1280
+ ...(legacyModelFallback ? { legacyModelFallback } : {}),
1281
+ // Availability remains an observation hint, not the admission predicate.
1282
+ models: models.map(({ model, availability }) => ({
1283
+ ...projectClientModel(model),
1284
+ availability,
1285
+ })),
1286
+ defaultReasoningEffort: defaultSelection.reasoningEffort,
499
1287
  allowedReasoningEfforts: configuredAllowedReasoningEfforts(deps.settings),
1288
+ defaultSandboxBackend: deps.settings.sandboxBackend,
500
1289
  mcpServers: deps.settings.mcpServers.map((server) => ({
501
1290
  id: server.id,
502
1291
  name: server.name ?? server.id,
503
1292
  })),
504
1293
  firstPartyMcpTools: resolveFirstPartyMcpToolPolicy(deps.settings),
1294
+ codeSearch: codeSearchDeploymentPolicy(deps.settings),
1295
+ agentConfig: clientAgentConfig(deps.settings),
505
1296
  fileUploads: {
506
1297
  enabled: objectStorage !== null,
507
1298
  maxSizeBytes: objectStorage?.maxSinglePutSizeBytes ?? 5_000_000_000,
508
1299
  },
509
1300
  voiceInput: {
1301
+ providers: resolveVoiceInputProviderRegistry(deps.settings).map(
1302
+ (provider) => provider.id,
1303
+ ),
510
1304
  available: (await transcription?.available()) ?? false,
511
1305
  maxDurationSeconds: deps.settings.voiceInputMaxDurationSeconds,
512
1306
  maxSizeBytes: deps.settings.voiceInputMaxSizeBytes,
@@ -531,8 +1325,18 @@ export function createAppComposition(deps: AppDependencies): {
531
1325
  : {}),
532
1326
  },
533
1327
  productAccessMode: deps.settings.productAccessMode,
534
- auth: clientAuthConfig(deps.settings),
1328
+ billingMode: deps.settings.billingMode,
1329
+ managedAuthSessionSetMode: deps.settings.managedAuthSessionSetMode,
1330
+ auth: clientAuthConfig(
1331
+ deps.settings,
1332
+ deps.settings.productAccessMode === "managed"
1333
+ ? await managedAuthNewSignupsGate.signupsOpen()
1334
+ : true,
1335
+ ),
1336
+ documentationUrl: deps.settings.documentationUrl,
535
1337
  analytics: clientAnalyticsConfig(deps.settings),
1338
+ legal: clientLegalConfig(deps.settings),
1339
+ ...(deps.settings.supportEmail ? { supportEmail: deps.settings.supportEmail } : {}),
536
1340
  // Channel-A structured services (P4.4) ride exec/readFile/createEditor,
537
1341
  // available on every real backend; `none` has no box so they are all off.
538
1342
  // Per-session availability is still negotiated on /stream-capabilities.
@@ -541,6 +1345,67 @@ export function createAppComposition(deps: AppDependencies): {
541
1345
  );
542
1346
  });
543
1347
 
1348
+ // The organization MCP server: every public action, as a person who signed
1349
+ // in (capped by their connection's access setting) or as an organization
1350
+ // API key. Each action runs the real route in this process.
1351
+ app.all(ORGANIZATION_MCP_PATH, async (c) => {
1352
+ let boundedRequest: Request;
1353
+ try {
1354
+ boundedRequest = await boundedMcpRequest(c.req.raw);
1355
+ } catch (error) {
1356
+ if (error instanceof McpPayloadTooLargeError) {
1357
+ throw new HTTPException(413, { message: "MCP request body exceeds the safety limit" });
1358
+ }
1359
+ throw error;
1360
+ }
1361
+ const challenge = () => {
1362
+ if (deps.settings.mcpOauthEnabled) {
1363
+ c.header("www-authenticate", mcpOAuthAuthenticateHeader(routeDeps, ORGANIZATION_MCP_PATH));
1364
+ }
1365
+ };
1366
+ let caller: OrganizationMcpCaller;
1367
+ try {
1368
+ const connection = await resolveOrganizationMcpOAuthAccess(routeDeps, c.req.raw);
1369
+ if (connection) {
1370
+ caller = {
1371
+ kind: "person",
1372
+ accountId: connection.accountId,
1373
+ subjectId: connection.subjectId,
1374
+ access: connection.organizationAccess,
1375
+ };
1376
+ } else {
1377
+ const context = await requireAccessContext(c, routeDeps);
1378
+ const authorization = c.req.header("authorization");
1379
+ if (!accountScopedApiKeyWorkspaceAuthority(context) || !authorization) {
1380
+ throw new HTTPException(403, {
1381
+ message: "Connect with Opengeni sign-in or an organization API key.",
1382
+ });
1383
+ }
1384
+ caller = {
1385
+ kind: "key",
1386
+ authorization,
1387
+ accessKey: c.req.header("x-opengeni-access-key") ?? null,
1388
+ };
1389
+ }
1390
+ } catch (error) {
1391
+ if (error instanceof HTTPException && error.status === 401) challenge();
1392
+ throw error;
1393
+ }
1394
+ const transport = new WebStandardStreamableHTTPServerTransport({ enableJsonResponse: true });
1395
+ const mcp = buildOrganizationMcpServer({
1396
+ caller,
1397
+ origin: new URL(c.req.url).origin,
1398
+ dispatch: async (request) => await app.fetch(request, c.env),
1399
+ signal: c.req.raw.signal,
1400
+ });
1401
+ try {
1402
+ await mcp.connect(transport);
1403
+ return await handleMcpRequestWithClientAbort(transport, boundedRequest, c.req.raw.signal);
1404
+ } finally {
1405
+ await mcp.close().catch(() => undefined);
1406
+ }
1407
+ });
1408
+
544
1409
  app.use("/v1/workspaces/:workspaceId/*", async (c, next) => {
545
1410
  const workspaceId = c.req.param("workspaceId");
546
1411
  if (workspaceRequestRequiresCodexAccountPrevalidation(c.req.raw)) {
@@ -559,6 +1424,27 @@ export function createAppComposition(deps: AppDependencies): {
559
1424
 
560
1425
  app.all("/v1/workspaces/:workspaceId/mcp", async (c) => {
561
1426
  const workspaceId = c.req.param("workspaceId");
1427
+ // This endpoint uses a fresh stateless JSON-response transport per POST;
1428
+ // there is no persistent server-to-client stream. Refuse GET only after
1429
+ // the existing OAuth/workspace/session authorization, before tool setup.
1430
+ const unsupportedGet = () => {
1431
+ const version = c.req.header("mcp-protocol-version");
1432
+ if (version && !SUPPORTED_PROTOCOL_VERSIONS.includes(version)) {
1433
+ return c.json(
1434
+ {
1435
+ jsonrpc: "2.0",
1436
+ id: null,
1437
+ error: { code: -32000, message: "Unsupported protocol version." },
1438
+ },
1439
+ 400,
1440
+ );
1441
+ }
1442
+ return c.json(
1443
+ { jsonrpc: "2.0", id: null, error: { code: -32000, message: "Method not allowed." } },
1444
+ 405,
1445
+ { allow: "POST" },
1446
+ );
1447
+ };
562
1448
  let boundedRequest: Request;
563
1449
  try {
564
1450
  boundedRequest = await boundedMcpRequest(c.req.raw);
@@ -570,42 +1456,253 @@ export function createAppComposition(deps: AppDependencies): {
570
1456
  }
571
1457
  throw error;
572
1458
  }
573
- const grant = await requireMcpAccessGrant(c, routeDeps, workspaceId);
574
- return await withAccessGrantSessionRlsContext(routeDeps, grant, async () => {
575
- const boundSessionId = grant.metadata?.sessionId;
576
- if (typeof boundSessionId === "string") {
1459
+ let oauthAccess: Awaited<ReturnType<typeof resolveMcpOAuthRouteAccess>>;
1460
+ try {
1461
+ oauthAccess = await resolveMcpOAuthRouteAccess(routeDeps, c.req.raw, workspaceId);
1462
+ } catch (error) {
1463
+ if (error instanceof HTTPException && error.status === 401) {
1464
+ c.header(
1465
+ "www-authenticate",
1466
+ mcpOAuthAuthenticateHeader(routeDeps, new URL(c.req.url).pathname),
1467
+ );
1468
+ }
1469
+ throw error;
1470
+ }
1471
+ if (oauthAccess) {
1472
+ return await withAccessGrantSessionRlsContext(routeDeps, oauthAccess.grant, async () => {
1473
+ if (c.req.method === "GET") return unsupportedGet();
1474
+ const prepared = await prepareMcpOAuthWorkspaceToolGateway(
1475
+ routeDeps,
1476
+ oauthAccess.grant,
1477
+ oauthAccess.allowedToolIdentities,
1478
+ );
1479
+ const transport = new WebStandardStreamableHTTPServerTransport({
1480
+ enableJsonResponse: true,
1481
+ });
1482
+ const mcp = buildWorkspaceToolGatewayMcpServer(
1483
+ prepared,
1484
+ oauthAccess.grant,
1485
+ routeDeps.observability,
1486
+ );
577
1487
  try {
578
- await requireSessionAuthorization(routeDeps, grant, {
579
- sessionId: boundSessionId,
580
- operation: "session.first_party_mcp.call",
581
- surface: "first_party_mcp",
582
- });
583
- } catch (error) {
584
- if (error instanceof SessionAuthorizationDeniedError) {
585
- throw new HTTPException(404, { message: "session not found" });
586
- }
587
- if (error instanceof SessionAuthorizationUnavailableError) {
588
- throw new HTTPException(503, {
589
- message: "session authorization is unavailable",
1488
+ await mcp.connect(transport);
1489
+ return await handleMcpRequestWithClientAbort(transport, boundedRequest, c.req.raw.signal);
1490
+ } finally {
1491
+ await Promise.allSettled([mcp.close(), prepared.close()]);
1492
+ }
1493
+ });
1494
+ }
1495
+ let authorization: Awaited<ReturnType<typeof requireMcpAccessGrantAuthorization>>;
1496
+ try {
1497
+ authorization = await requireMcpAccessGrantAuthorization(c, routeDeps, workspaceId);
1498
+ } catch (error) {
1499
+ if (deps.settings.mcpOauthEnabled && error instanceof HTTPException && error.status === 401) {
1500
+ c.header(
1501
+ "www-authenticate",
1502
+ mcpOAuthAuthenticateHeader(routeDeps, new URL(c.req.url).pathname),
1503
+ );
1504
+ }
1505
+ throw error;
1506
+ }
1507
+ const grant = authorization.grant;
1508
+ // The agent-attempt context, this route check, and a tool's own entry check
1509
+ // re-read the same caller session and attempt; share those reads.
1510
+ return await withSessionAuthorizationReadReuse((reads) =>
1511
+ withAccessGrantSessionRlsContext(routeDeps, grant, async () => {
1512
+ const boundSessionId = grant.metadata?.sessionId;
1513
+ if (typeof boundSessionId === "string") {
1514
+ try {
1515
+ await requireSessionAuthorization(routeDeps, grant, {
1516
+ sessionId: boundSessionId,
1517
+ operation: "session.first_party_mcp.call",
1518
+ surface: "first_party_mcp",
590
1519
  });
1520
+ } catch (error) {
1521
+ if (error instanceof SessionAuthorizationDeniedError) {
1522
+ throw new HTTPException(404, { message: "session not found" });
1523
+ }
1524
+ if (error instanceof SessionAuthorizationUnavailableError) {
1525
+ throw new HTTPException(503, {
1526
+ message: "session authorization is unavailable",
1527
+ });
1528
+ }
1529
+ throw error;
591
1530
  }
592
- throw error;
593
1531
  }
594
- }
595
- const workspace = await getWorkspace(routeDeps.db, workspaceId);
596
- const workspaceMemoryEnabled = resolveWorkspaceMemoryEnabled(workspace?.settings);
597
- const workspaceMemoryPromptMode = resolveWorkspaceMemoryPromptMode(workspace?.settings);
598
- const transport = new WebStandardStreamableHTTPServerTransport({
599
- enableJsonResponse: true,
600
- });
601
- const mcp = buildOpenGeniMcpServer(routeDeps, grant, {
602
- requestOrigin: new URL(c.req.url).origin,
603
- workspaceMemoryEnabled,
604
- workspaceMemoryPromptMode,
1532
+ if (c.req.method === "GET") return unsupportedGet();
1533
+ reads.handOffToToolDispatch();
1534
+ const workspace = await getWorkspace(routeDeps.db, workspaceId);
1535
+ const workspaceMemoryEnabled = resolveWorkspaceMemoryEnabled(workspace?.settings);
1536
+ const workspaceMemoryPromptMode = resolveWorkspaceMemoryPromptMode();
1537
+ const transport = new WebStandardStreamableHTTPServerTransport({
1538
+ enableJsonResponse: true,
1539
+ });
1540
+ if (!grantUsesAttemptScopedMcp(grant)) {
1541
+ const prepared = await prepareWorkspaceToolGateway(routeDeps, authorization);
1542
+ const mcp = buildWorkspaceToolGatewayMcpServer(prepared, grant, routeDeps.observability);
1543
+ try {
1544
+ await mcp.connect(transport);
1545
+ return await handleMcpRequestWithClientAbort(
1546
+ transport,
1547
+ boundedRequest,
1548
+ c.req.raw.signal,
1549
+ );
1550
+ } finally {
1551
+ await Promise.allSettled([mcp.close(), prepared.close()]);
1552
+ }
1553
+ }
1554
+ const mcpDeps = await resolveWorkspaceMcpRouteDeps(routeDeps, grant);
1555
+ // The bound session's frozen Memory selector (migration 0427) decides
1556
+ // which Memory tools the attempt receives and which typed layers they
1557
+ // read and write. A missing row resolves to no Memory tools.
1558
+ const sessionMemory =
1559
+ typeof boundSessionId === "string"
1560
+ ? ((await resolveSessionMemoryAgentScope(
1561
+ routeDeps.db,
1562
+ workspaceId,
1563
+ boundSessionId,
1564
+ grant.metadata,
1565
+ )) ?? {
1566
+ mode: "off" as const,
1567
+ userSubjectId: null,
1568
+ rootSessionId: null,
1569
+ })
1570
+ : null;
1571
+ const mcp = buildOpenGeniMcpServer(mcpDeps, grant, {
1572
+ requestOrigin: new URL(c.req.url).origin,
1573
+ workspaceMemoryEnabled,
1574
+ workspaceMemoryPromptMode,
1575
+ sessionMemory,
1576
+ });
1577
+ // Bind tool handlers' `extra.signal` to the HTTP client's connection: a
1578
+ // worker that drops the call (Steer/Pause) aborts a blocking tool here.
1579
+ // Each POST builds a fresh server; close it once the JSON response is
1580
+ // ready, like the gateway paths above, so it can't outlive the request.
1581
+ try {
1582
+ await mcp.connect(transport);
1583
+ return await handleMcpRequestWithClientAbort(transport, boundedRequest, c.req.raw.signal);
1584
+ } finally {
1585
+ await mcp.close().catch(() => undefined);
1586
+ }
1587
+ }),
1588
+ );
1589
+ });
1590
+
1591
+ app.get("/v1/workspaces/:workspaceId/tools/catalog", async (c) => {
1592
+ const workspaceId = c.req.param("workspaceId");
1593
+ const authorization = await requireAccessGrantAuthorization(
1594
+ c,
1595
+ routeDeps,
1596
+ workspaceId,
1597
+ "workspace:read",
1598
+ );
1599
+ const prepared = await prepareWorkspaceToolGateway(routeDeps, authorization);
1600
+ try {
1601
+ c.header("cache-control", "no-store");
1602
+ return c.json(prepared.toolGatewayCatalog);
1603
+ } finally {
1604
+ await prepared.close();
1605
+ }
1606
+ });
1607
+
1608
+ app.post("/v1/workspaces/:workspaceId/tools/calls", async (c) => {
1609
+ const workspaceId = c.req.param("workspaceId");
1610
+ const authorization = await requireAccessGrantAuthorization(
1611
+ c,
1612
+ routeDeps,
1613
+ workspaceId,
1614
+ "workspace:read",
1615
+ );
1616
+ const grant = authorization.grant;
1617
+ const parsed = ToolGatewayCallRequest.safeParse(await c.req.json().catch(() => null));
1618
+ if (!parsed.success) {
1619
+ throw new HTTPException(400, { message: "Invalid tool gateway call" });
1620
+ }
1621
+ const prepared = await prepareWorkspaceToolGateway(routeDeps, authorization);
1622
+ try {
1623
+ return c.json(
1624
+ await callWorkspaceToolGateway(
1625
+ prepared,
1626
+ grant,
1627
+ parsed.data,
1628
+ routeDeps.db,
1629
+ undefined,
1630
+ routeDeps.observability,
1631
+ ),
1632
+ );
1633
+ } finally {
1634
+ await prepared.close();
1635
+ }
1636
+ });
1637
+
1638
+ app.post("/v1/workspaces/:workspaceId/tools/approvals", async (c) => {
1639
+ const workspaceId = c.req.param("workspaceId");
1640
+ const authorization = await requireAccessGrantAuthorization(
1641
+ c,
1642
+ routeDeps,
1643
+ workspaceId,
1644
+ "workspace:read",
1645
+ );
1646
+ const parsed = ToolGatewayApprovalRequest.safeParse(await c.req.json().catch(() => null));
1647
+ if (!parsed.success) {
1648
+ throw new HTTPException(400, { message: "Invalid tool gateway approval" });
1649
+ }
1650
+ const prepared = await prepareWorkspaceToolGateway(routeDeps, authorization);
1651
+ try {
1652
+ return c.json(
1653
+ await approveWorkspaceToolGatewayCall(
1654
+ prepared,
1655
+ authorization.grant,
1656
+ routeDeps.db,
1657
+ parsed.data,
1658
+ undefined,
1659
+ routeDeps.observability,
1660
+ ),
1661
+ 201,
1662
+ );
1663
+ } finally {
1664
+ await prepared.close();
1665
+ }
1666
+ });
1667
+
1668
+ app.get("/v1/workspaces/:workspaceId/tools/declarations", async (c) => {
1669
+ const workspaceId = c.req.param("workspaceId");
1670
+ const authorization = await requireAccessGrantAuthorization(
1671
+ c,
1672
+ routeDeps,
1673
+ workspaceId,
1674
+ "workspace:read",
1675
+ );
1676
+ const prepared = await prepareWorkspaceToolGateway(routeDeps, authorization);
1677
+ try {
1678
+ c.header("cache-control", "no-store");
1679
+ return c.json(workspaceToolGatewayDeclarations(prepared));
1680
+ } finally {
1681
+ await prepared.close();
1682
+ }
1683
+ });
1684
+
1685
+ app.all("/v1/workspaces/:workspaceId/codemode/sdk/*", async (c) => {
1686
+ const workspaceId = c.req.param("workspaceId");
1687
+ const grant = await requireAccessGrant(c, routeDeps, workspaceId);
1688
+ const url = new URL(c.req.url);
1689
+ const prefix = `/v1/workspaces/${workspaceId}/codemode/sdk`;
1690
+ let forwarded: Request;
1691
+ try {
1692
+ const path = url.pathname.slice(prefix.length) + url.search;
1693
+ forwarded = await codemodeSessionRequest(routeDeps, grant, c.req.raw, path, (session) => {
1694
+ assertConfiguredCodemodeSessionProxyPath(session, path, c.req.method);
1695
+ const proxyTools = configuredCodemodeSessionProxyTools(routeDeps.settings, session);
1696
+ return proxyTools === null
1697
+ ? routeDeps.settings
1698
+ : { ...routeDeps.settings, allowedFirstPartyMcpTools: proxyTools };
605
1699
  });
606
- await mcp.connect(transport);
607
- return await transport.handleRequest(boundedRequest);
608
- });
1700
+ } catch (error) {
1701
+ throw codemodeHttpError(error);
1702
+ }
1703
+ // The forwarded request drops the caller's Host and names a non-sandbox
1704
+ // path; the request it was built from already passed the local boundary.
1705
+ return app.fetch(markLocalInternalDispatch(forwarded));
609
1706
  });
610
1707
 
611
1708
  app.get("/v1/workspaces/:workspaceId/codemode/catalog", async (c) => {
@@ -661,27 +1758,42 @@ export function createAppComposition(deps: AppDependencies): {
661
1758
  }
662
1759
  });
663
1760
 
1761
+ registerConnectCallbackReturns(app, routeDeps);
664
1762
  registerFileRoutes(app, routeDeps);
1763
+ registerSessionArtifactAssociationRoutes(app, routeDeps);
665
1764
  registerApiKeyRoutes(app, routeDeps);
1765
+ registerOrganizationMcpConnectionRoutes(app, routeDeps);
1766
+ registerOrganizationServiceAccountRoutes(app, routeDeps);
666
1767
  registerBillingRoutes(app, routeDeps);
667
1768
  registerBrowserIdentityRoutes(app, routeDeps);
668
1769
  registerBrowserSessionRoutes(app, routeDeps);
669
1770
  registerComputerSessionRoutes(app, routeDeps);
670
1771
  registerDocumentRoutes(app, routeDeps);
1772
+ registerKnowledgeRoutes(app, routeDeps);
671
1773
  registerGitHubRoutes(app, routeDeps);
672
1774
  registerInstallRoutes(app, routeDeps);
673
1775
  registerInteractionResourceRoutes(app, routeDeps);
674
1776
  registerWorkspaceRoutes(app, routeDeps);
1777
+ registerUsageAllowanceRoutes(app, routeDeps);
675
1778
  registerInsightsRoutes(app, routeDeps);
1779
+ registerInsightsUsageRoutes(app, routeDeps);
676
1780
  registerWorkspaceInstructionPolicyRoutes(app, routeDeps);
1781
+ registerWorkspaceLearningRoutes(app, routeDeps);
677
1782
  registerCompanyProfileRoutes(app, routeDeps);
1783
+ registerCompanyBrainRoutes(app, routeDeps);
678
1784
  registerSlackTaskPolicyRoutes(app, routeDeps);
679
1785
  registerWorkspaceStateRoutes(app, routeDeps);
680
1786
  registerMemorySlackPublicationRoutes(app, routeDeps);
681
1787
  registerWorkspaceArtifactRoutes(app, routeDeps);
1788
+ registerArtifactCatalogRoutes(app, routeDeps);
682
1789
  registerPreferenceRegistryRoutes(app, routeDeps);
683
1790
  registerSocialRoutes(app, routeDeps);
1791
+ registerPersonalGitHubRoutes(app, routeDeps);
1792
+ registerPersonalGitHubGitBrokerRoutes(app, routeDeps);
684
1793
  registerConnectionRoutes(app, routeDeps);
1794
+ registerConnectRoutes(app, routeDeps);
1795
+
1796
+ registerExternalIdentityLinkRoutes(app, routeDeps);
685
1797
  registerCapabilityRoutes(app, routeDeps);
686
1798
  registerApiIntegrationRoutes(app, routeDeps);
687
1799
  registerIntegrationFacetRoutes(app, routeDeps);
@@ -691,23 +1803,37 @@ export function createAppComposition(deps: AppDependencies): {
691
1803
  registerEnvironmentRoutes(app, routeDeps);
692
1804
  registerChannelRoutes(app, routeDeps);
693
1805
  registerRigRoutes(app, routeDeps);
694
- registerPackRoutes(app, routeDeps);
1806
+ registerAutomationRoutes(app, routeDeps);
1807
+ registerPrReviewRoutes(app, routeDeps);
695
1808
  registerPluginRoutes(app, routeDeps);
696
1809
  registerSkillRoutes(app, routeDeps);
697
1810
  registerSessionRoutes(app, routeDeps);
1811
+ registerSessionHistoryImportRoutes(app, routeDeps);
1812
+ registerFeedbackRoutes(app, routeDeps);
1813
+ registerWorkspaceIntegrationRoutes(app, routeDeps);
1814
+ registerOrganizationIntegrationRoutes(app, routeDeps);
698
1815
  registerScheduledTaskRoutes(app, routeDeps);
699
1816
  registerCodexRoutes(app, routeDeps);
1817
+ registerOrganizationModelProviderRoutes(app, routeDeps);
1818
+ registerWorkspaceModelProviderRoutes(app, routeDeps);
1819
+ registerClaudeSubscriptionOAuthRoutes(app, routeDeps);
1820
+ registerOrganizationIntegrationPolicyRoutes(app, routeDeps);
1821
+ registerModelConnectionAccessRoutes(app, routeDeps);
700
1822
  registerSuperGrokRoutes(app, routeDeps);
1823
+ registerClaudeSubscriptionAccountRoutes(app, routeDeps);
701
1824
  registerTranscriptionRoutes(app, routeDeps);
702
1825
  registerEditableArtifactRoutes(app, routeDeps);
703
1826
  registerVideoGenerationRoutes(app, routeDeps);
704
1827
  registerCanonicalHumanIdentityRoutes(app, routeDeps);
705
1828
  registerOrganizationMembershipRoutes(app, routeDeps);
1829
+ registerOrganizationSessionRoutes(app, routeDeps);
1830
+ registerOrganizationRecoveryRoutes(app, routeDeps);
1831
+ registerUserResourceAuthorityRoutes(app, routeDeps);
706
1832
  registerSlackInteractionRoutes(app, routeDeps);
707
1833
 
708
1834
  app.notFound((c) => {
709
1835
  if (!new URL(c.req.url).pathname.startsWith("/v1/")) return c.text("Not Found", 404);
710
- const requestId = correlationIds.get(c.req.raw) ?? crypto.randomUUID();
1836
+ const requestId = correlationIds.get(c) ?? crypto.randomUUID();
711
1837
  return c.json(
712
1838
  ErrorEnvelope.parse({
713
1839
  error: {
@@ -722,18 +1848,49 @@ export function createAppComposition(deps: AppDependencies): {
722
1848
  );
723
1849
  });
724
1850
 
725
- app.onError((error, c) => {
1851
+ app.onError((rawError, c) => {
1852
+ const requestId = correlationIds.get(c) ?? crypto.randomUUID();
1853
+ c.header(OPENGENI_CORRELATION_HEADER, requestId);
1854
+ if (new URL(c.req.url).pathname.startsWith("/v1/")) {
1855
+ c.header(OPENGENI_API_CONTRACT_HEADER, OPENGENI_API_CONTRACT_REVISION);
1856
+ }
1857
+ // Archives remain read-only through the existing Send/Steer/control paths.
1858
+ const importErrorResponse = archivedSessionImportErrorResponse(c, rawError);
1859
+ if (importErrorResponse) return importErrorResponse;
1860
+ // One central mapping for every Send/Steer/control route: a bounded
1861
+ // control-prefix wait that expired is a known, retryable, not-applied 503.
1862
+ const error =
1863
+ rawError instanceof OrganizationIntegrationDeniedError
1864
+ ? new HTTPException(403, { message: rawError.message })
1865
+ : (allowanceExhaustedHttpError(rawError) ??
1866
+ workspaceControlBusyHttpError(rawError) ??
1867
+ scheduledTaskTargetAccessHttpError(rawError) ??
1868
+ agentConfigHttpError(rawError) ??
1869
+ modelUnavailableHttpError(rawError) ??
1870
+ (rawError instanceof UnsupportedLatencyModeError
1871
+ ? new ApiHttpError(422, {
1872
+ code: "validation_failed",
1873
+ message: rawError.message,
1874
+ retryable: false,
1875
+ outcomeUnknown: false,
1876
+ details: {
1877
+ code: rawError.code,
1878
+ modelId: rawError.modelId,
1879
+ latencyMode: rawError.latencyMode,
1880
+ allowedLatencyModes: [...rawError.allowedLatencyModes],
1881
+ },
1882
+ })
1883
+ : null) ??
1884
+ requestBodyValidationHttpError(rawError) ??
1885
+ invalidPathIdentifierHttpError(rawError, new URL(c.req.url).pathname) ??
1886
+ rawError);
726
1887
  const compactionLock = codexCompactionV2ProviderLockedError(error);
727
1888
  const apiError = error instanceof ApiHttpError ? error : null;
728
1889
  const status = compactionLock ? 422 : httpStatusForError(error);
729
1890
  const code: ErrorCode = compactionLock
730
1891
  ? compactionLock.code
731
1892
  : (apiError?.code ?? errorCodeForStatus(status));
732
- const requestId = correlationIds.get(c.req.raw) ?? crypto.randomUUID();
733
- c.header(OPENGENI_CORRELATION_HEADER, requestId);
734
- if (new URL(c.req.url).pathname.startsWith("/v1/")) {
735
- c.header(OPENGENI_API_CONTRACT_HEADER, OPENGENI_API_CONTRACT_REVISION);
736
- }
1893
+ if (status >= 500) logHttpFailure(c, status, code, rawError);
737
1894
  const envelope = ErrorEnvelope.parse({
738
1895
  error: {
739
1896
  status,
@@ -744,7 +1901,9 @@ export function createAppComposition(deps: AppDependencies): {
744
1901
  ? (boundedPublicMessage(apiError.message) ?? "Request failed.")
745
1902
  : publicErrorMessage(error, status),
746
1903
  retryable: apiError?.retryable ?? retryableHttpStatus(status),
747
- ...(mutationOutcomeUnknown(error, c.req.method) ? { outcomeUnknown: true } : {}),
1904
+ ...((apiError?.outcomeUnknown ?? mutationOutcomeUnknown(error, c.req.method))
1905
+ ? { outcomeUnknown: true }
1906
+ : {}),
748
1907
  requestId,
749
1908
  ...(apiError?.details ? { details: apiError.details } : {}),
750
1909
  },
@@ -755,6 +1914,11 @@ export function createAppComposition(deps: AppDependencies): {
755
1914
  return { app, routeDeps };
756
1915
  }
757
1916
 
1917
+ function managedAuthOAuthCallbackProvider(pathname: string): "google" | "github" | null {
1918
+ const match = pathname.match(/^\/v1\/auth\/callback\/(google|github)$/u);
1919
+ return match?.[1] === "google" || match?.[1] === "github" ? match[1] : null;
1920
+ }
1921
+
758
1922
  function mutationOutcomeUnknown(error: unknown, method: string): boolean {
759
1923
  if (method === "GET" || method === "HEAD" || method === "OPTIONS") return false;
760
1924
  const cause = error instanceof HTTPException ? error.cause : error;
@@ -781,7 +1945,41 @@ const publicWorkspaceBrowserRoutePatterns = [
781
1945
  ] as const;
782
1946
 
783
1947
  export function workspaceActorContextExempt(method: string, pathname: string): boolean {
784
- if (/^\/v1\/workspaces\/[^/]+\/mcp$/.test(pathname)) return true;
1948
+ // The account-scoped provisioning route shares the workspace collection prefix.
1949
+ // Hono's trailing wildcard also matches it, but "external" is not a workspace UUID;
1950
+ // the route performs its own organization API-key authorization.
1951
+ if (method === "PUT" && pathname === "/v1/workspaces/external") return true;
1952
+ // Import adapters apply their own exact create/control gates and request-local
1953
+ // RLS in core. Tenant mirrors first resolve the organization-local mapping;
1954
+ // "external" itself is not a native workspace id.
1955
+ if (isSessionHistoryImportRequest(method, pathname)) return true;
1956
+ // Organization budget authority is independent of ordinary workspace
1957
+ // membership; these routes apply their own exact account/workspace gate.
1958
+ if (
1959
+ (["GET", "PUT", "DELETE"].includes(method) &&
1960
+ /^\/v1\/workspaces\/[^/]+\/allowance$/.test(pathname)) ||
1961
+ (method === "GET" && /^\/v1\/workspaces\/[^/]+\/allowance\/state$/.test(pathname)) ||
1962
+ (method === "POST" && /^\/v1\/workspaces\/[^/]+\/allowance\/grants$/.test(pathname))
1963
+ )
1964
+ return true;
1965
+ // Allowance tenant mirrors resolve the workspace by exact organization-local
1966
+ // external identity before applying the ordinary workspace authorization.
1967
+ if (
1968
+ (["GET", "PUT", "DELETE"].includes(method) &&
1969
+ /^\/v1\/workspaces\/external\/[^/]+\/[^/]+\/allowance$/.test(pathname)) ||
1970
+ (method === "GET" &&
1971
+ /^\/v1\/workspaces\/external\/[^/]+\/[^/]+\/allowance\/state$/.test(pathname)) ||
1972
+ (method === "POST" &&
1973
+ /^\/v1\/workspaces\/external\/[^/]+\/[^/]+\/allowance\/grants$/.test(pathname)) ||
1974
+ (method === "GET" &&
1975
+ /^\/v1\/workspaces\/external\/[^/]+\/[^/]+\/usage(?:\/me)?$/.test(pathname)) ||
1976
+ (method === "PUT" &&
1977
+ /^\/v1\/workspaces\/external\/[^/]+\/[^/]+\/members\/(?:[^/]+|external\/[^/]+\/[^/]+)\/allowance$/.test(
1978
+ pathname,
1979
+ ))
1980
+ )
1981
+ return true;
1982
+ if (/^\/v1\/workspaces\/[^/]+\/mcp(?:\/(?:docs|files))?$/.test(pathname)) return true;
785
1983
  if (
786
1984
  method === "GET" &&
787
1985
  publicWorkspaceBrowserRoutePatterns.some((route) => route.test(pathname))
@@ -810,46 +2008,15 @@ async function validateCodexAccountTarget(request: Request): Promise<void> {
810
2008
  }
811
2009
  }
812
2010
 
813
- async function withAccessGrantSessionRlsContext<T>(
814
- deps: ApiRouteDeps,
815
- grant: AccessGrant,
816
- fn: () => Promise<T>,
817
- ): Promise<T> {
818
- if (grant.principalKind !== "agent_attempt") {
819
- return await withSessionRlsActorContext({ subjectId: grant.subjectId }, fn);
820
- }
821
- const callerSessionId = grant.metadata?.sessionId;
822
- if (typeof callerSessionId !== "string") {
823
- throw new HTTPException(403, { message: "agent attempt authority is invalid" });
824
- }
825
- try {
826
- const actor = await requireLiveAgentAttemptAuthorization(deps.db, grant, callerSessionId);
827
- return await withSessionRlsActorContext(
828
- {
829
- subjectId: actor.subjectId,
830
- initiatingHumanSubjectId: actor.initiatingHumanSubjectId,
831
- },
832
- fn,
833
- );
834
- } catch (error) {
835
- if (error instanceof SessionAuthorizationDeniedError) {
836
- throw new HTTPException(403, {
837
- message: "agent attempt authority is invalid",
838
- cause: error,
839
- });
840
- }
841
- throw error;
842
- }
843
- }
844
-
845
- async function requireMcpAccessGrant(
2011
+ async function requireMcpAccessGrantAuthorization(
846
2012
  c: Parameters<typeof requireAccessGrant>[0],
847
2013
  deps: ApiRouteDeps,
848
2014
  workspaceId: string,
849
- ): Promise<AccessGrant> {
850
- const grant = await requireAccessGrant(c, deps, workspaceId);
2015
+ ): Promise<Awaited<ReturnType<typeof requireAccessGrantAuthorization>>> {
2016
+ const authorization = await requireAccessGrantAuthorization(c, deps, workspaceId);
2017
+ const grant = authorization.grant;
851
2018
  if (hasPermission(grant.permissions, "workspace:read")) {
852
- return grant;
2019
+ return authorization;
853
2020
  }
854
2021
  if (isCodemodeGrant(grant)) {
855
2022
  requirePermission(grant, "workspace:read");
@@ -859,15 +2026,28 @@ async function requireMcpAccessGrant(
859
2026
  // authorization seam runs immediately after this gate, and tool registration
860
2027
  // still exposes only capabilities permitted by the delegated grant.
861
2028
  if (grant.metadata?.delegated === true && typeof grant.metadata.sessionId === "string") {
862
- return grant;
2029
+ return authorization;
863
2030
  }
864
2031
  requirePermission(grant, "workspace:read");
865
- return grant;
2032
+ return authorization;
866
2033
  }
867
2034
 
868
- function clientAuthConfig(settings: AppDependencies["settings"]) {
2035
+ function clientAuthConfig(settings: AppDependencies["settings"], newSignupsEnabled: boolean) {
869
2036
  if (settings.productAccessMode === "managed") {
870
- return { mode: "managedSession" as const, session: "cookie" as const };
2037
+ return {
2038
+ mode: "managedSession" as const,
2039
+ session: "cookie" as const,
2040
+ emailVerificationRequired: settings.environment !== "local",
2041
+ newSignupsEnabled,
2042
+ socialProviders: [
2043
+ ...(settings.managedAuthGoogleClientId && settings.managedAuthGoogleClientSecret
2044
+ ? (["google"] as const)
2045
+ : []),
2046
+ ...(settings.managedAuthGithubClientId && settings.managedAuthGithubClientSecret
2047
+ ? (["github"] as const)
2048
+ : []),
2049
+ ],
2050
+ };
871
2051
  }
872
2052
  if (settings.productAccessMode === "configured") {
873
2053
  return {
@@ -885,7 +2065,143 @@ function clientAuthConfig(settings: AppDependencies["settings"]) {
885
2065
  return { mode: "none" as const };
886
2066
  }
887
2067
 
2068
+ /** Configured SDK credentials cannot regain tools from widened legacy columns. */
2069
+ export function configuredCodemodeSessionProxyTools(
2070
+ settings: Pick<Settings, "defaultFirstPartyMcpTools" | "allowedFirstPartyMcpTools">,
2071
+ session: Pick<Session, "agent">,
2072
+ ): FirstPartyMcpToolName[] | null {
2073
+ if (!session.agent) return null;
2074
+ const families = resolveAgentToolFamilies(session.agent);
2075
+ return resolveFirstPartyMcpToolPolicy(settings).allowed.filter((tool) =>
2076
+ families.allowsFirstPartyTool(tool),
2077
+ );
2078
+ }
2079
+
2080
+ /**
2081
+ * Permissions span capabilities: project_create grants sessions:create,
2082
+ * and runtime titling grants sessions:control. Fence the endpoint family
2083
+ * too, without changing null-config routing or ordinary REST authorization.
2084
+ */
2085
+ export function assertConfiguredCodemodeSessionProxyPath(
2086
+ session: Pick<Session, "id" | "agent">,
2087
+ path: string,
2088
+ method: string,
2089
+ ): void {
2090
+ if (!session.agent) return;
2091
+ const families = resolveAgentToolFamilies(session.agent);
2092
+ const pathname = new URL(path, "http://codemode.invalid").pathname;
2093
+ const segments = pathname.split("/").filter(Boolean);
2094
+ const verb = method.toUpperCase();
2095
+ const read = verb === "GET" || verb === "HEAD";
2096
+ const allows = (tool: FirstPartyMcpToolName) => families.allowsFirstPartyTool(tool);
2097
+ let allowed = false;
2098
+ if (pathname === "/v1/config/client") {
2099
+ allowed = read;
2100
+ } else if (segments[0] === "v1" && segments[1] === "workspaces" && segments[2] === "site-host") {
2101
+ const [surface, targetSessionId, operation] = segments.slice(3);
2102
+ if (!surface) {
2103
+ allowed = read;
2104
+ } else if (surface === "sessions") {
2105
+ const ownSession = targetSessionId === session.id;
2106
+ const targetAllowed = ownSession || families.subagents;
2107
+ if (!targetSessionId) {
2108
+ allowed = families.subagents;
2109
+ } else if (ownSession && !operation && read) {
2110
+ allowed = true;
2111
+ } else if (ownSession && !operation && verb === "PATCH") {
2112
+ allowed = allows("set_session_title");
2113
+ } else if (ownSession && operation === "background-commands") {
2114
+ allowed = read && allows("command_read");
2115
+ } else if (operation === "goal") {
2116
+ allowed = allows("goal_set") && targetAllowed;
2117
+ } else if (operation === "browser" || operation === "computer") {
2118
+ allowed = allows("browser_open") && targetAllowed;
2119
+ } else if (operation === "human-input-requests") {
2120
+ allowed = families.humanInput && targetAllowed;
2121
+ } else if (operation === "artifacts") {
2122
+ allowed = allows("artifacts_list") && targetAllowed;
2123
+ } else if (ownSession && ["fs", "git", "terminal", "workspace"].includes(operation ?? "")) {
2124
+ // Resource-derived sandbox mechanics are not platform capabilities.
2125
+ allowed = true;
2126
+ } else if (
2127
+ [
2128
+ "agent",
2129
+ "tool-policy",
2130
+ "mcp-servers",
2131
+ "mcp-credentials",
2132
+ "variable-sets",
2133
+ "project",
2134
+ ].includes(operation ?? "")
2135
+ ) {
2136
+ allowed = allows("project_get") && targetAllowed;
2137
+ } else {
2138
+ allowed = families.subagents;
2139
+ }
2140
+ } else if (surface === "files") {
2141
+ allowed = families.allowsMcpServer("files");
2142
+ } else if (surface === "skills") {
2143
+ allowed = families.skills === "manage" || (families.skills === "read" && read);
2144
+ } else if (surface === "human-input-requests") {
2145
+ allowed = families.humanInput;
2146
+ } else {
2147
+ const owners: Record<string, FirstPartyMcpToolName> = {
2148
+ "model-catalog": "sessions_list",
2149
+ "realtime-model-catalog": "sessions_list",
2150
+ "model-policy": "sessions_list",
2151
+ "gateway-custom-models": "sessions_list",
2152
+ "openrouter-custom-models": "sessions_list",
2153
+ "new-session-draft": "session_create",
2154
+ "session-tenancy": "session_create",
2155
+ "session-message-search": "session_events",
2156
+ "agent-topology": "sessions_list",
2157
+ projects: "project_get",
2158
+ sandboxes: "sandboxes_list",
2159
+ machines: "sandboxes_list",
2160
+ rigs: "rig_list",
2161
+ environments: "environment_list",
2162
+ "variable-sets": "variable_set_list",
2163
+ capabilities: "capability_catalog_search",
2164
+ catalog: "capability_catalog_search",
2165
+ members: "project_get",
2166
+ "member-candidates": "project_get",
2167
+ "control-events": "project_get",
2168
+ "browser-identities": "browser_identity",
2169
+ "browser-sessions": "browser_open",
2170
+ "computer-sessions": "computer_open",
2171
+ "attached-browsers": "browser_open",
2172
+ documents: "knowledge_search",
2173
+ knowledge: "knowledge_search",
2174
+ memory: "knowledge_search",
2175
+ "instruction-policy": "instruction_policy_get",
2176
+ preferences: "preference_registry_get",
2177
+ "company-profile": "company_profile_propose",
2178
+ artifacts: "artifacts_list",
2179
+ "scheduled-tasks": "scheduled_tasks_list",
2180
+ automations: "scheduled_tasks_list",
2181
+ connections: "github_connect_link",
2182
+ integrations: "github_connect_link",
2183
+ github: "github_connect_link",
2184
+ social: "social_connections_list",
2185
+ mcp: "github_connect_link",
2186
+ };
2187
+ const tool = owners[surface];
2188
+ allowed = tool !== undefined && allows(tool);
2189
+ }
2190
+ }
2191
+ if (!allowed) {
2192
+ throw new HTTPException(403, {
2193
+ message: "Agent configuration does not allow this Codemode SDK operation",
2194
+ });
2195
+ }
2196
+ }
2197
+
888
2198
  function codemodeHttpError(error: unknown): HTTPException {
2199
+ if (error instanceof SiteSessionPathError) {
2200
+ // The proxied Site/SDK surface is an explicit allowlist; a route outside
2201
+ // it (tool policy, visibility, forks, Steer, control, ...) does not exist
2202
+ // for this caller rather than being a server fault.
2203
+ return new HTTPException(404, { message: error.message, cause: error });
2204
+ }
889
2205
  if (error instanceof SessionAuthorizationDeniedError) {
890
2206
  return new HTTPException(404, {
891
2207
  message: "session not found",
@@ -898,18 +2214,39 @@ function codemodeHttpError(error: unknown): HTTPException {
898
2214
  cause: error,
899
2215
  });
900
2216
  }
901
- if (
902
- error instanceof CodemodeAuthorityError ||
903
- error instanceof CodemodeOperationNotExecutableError
904
- ) {
2217
+ if (error instanceof CodemodeCatalogNotReadyError) {
2218
+ return new ApiHttpError(409, {
2219
+ code: "conflict",
2220
+ message: error.message,
2221
+ retryable: true,
2222
+ outcomeUnknown: false,
2223
+ details: { code: error.code },
2224
+ });
2225
+ }
2226
+ if (error instanceof CodemodeCatalogStaleError) {
2227
+ return new ApiHttpError(409, {
2228
+ code: "conflict",
2229
+ message: error.message,
2230
+ retryable: true,
2231
+ outcomeUnknown: false,
2232
+ details: { code: error.code },
2233
+ });
2234
+ }
2235
+ if (error instanceof CodemodeAuthorityError) {
2236
+ return new ApiHttpError(error.reason === "invalid_grant" ? 403 : 409, {
2237
+ code: error.reason === "invalid_grant" ? "forbidden" : "conflict",
2238
+ message: error.message,
2239
+ retryable: false,
2240
+ outcomeUnknown: false,
2241
+ details: { code: error.code },
2242
+ });
2243
+ }
2244
+ if (error instanceof CodemodeOperationNotExecutableError) {
905
2245
  return new HTTPException(409, { message: error.message, cause: error });
906
2246
  }
907
2247
  if (error instanceof CodemodeOperationConflictError) {
908
2248
  return new HTTPException(409, { message: error.message, cause: error });
909
2249
  }
910
- if (error instanceof CodemodeCallBudgetExceededError) {
911
- return new HTTPException(429, { message: error.message, cause: error });
912
- }
913
2250
  if (error instanceof CodemodeToolNotInCatalogError) {
914
2251
  return new HTTPException(404, { message: error.message, cause: error });
915
2252
  }
@@ -952,6 +2289,15 @@ function clientAnalyticsConfig(settings: AppDependencies["settings"]) {
952
2289
  };
953
2290
  }
954
2291
 
2292
+ function clientLegalConfig(settings: AppDependencies["settings"]) {
2293
+ return {
2294
+ ...(settings.legalPrivacyPolicyUrl ? { privacyPolicyUrl: settings.legalPrivacyPolicyUrl } : {}),
2295
+ ...(settings.legalTermsOfServiceUrl
2296
+ ? { termsOfServiceUrl: settings.legalTermsOfServiceUrl }
2297
+ : {}),
2298
+ };
2299
+ }
2300
+
955
2301
  function structuredServicesHint(backend: string): {
956
2302
  fileSystem: boolean;
957
2303
  git: boolean;
@@ -975,9 +2321,17 @@ function codexCompactionV2ProviderLockedError(
975
2321
  }
976
2322
 
977
2323
  export function httpStatusForError(error: unknown): number {
2324
+ if (error instanceof ConnectAttemptConflictError) return 409;
2325
+ if (error instanceof ConnectAttemptNotFoundError) return 404;
978
2326
  if (codexCompactionV2ProviderLockedError(error)) {
979
2327
  return 422;
980
2328
  }
2329
+ if (workspaceControlBusyHttpError(error)) {
2330
+ return 503;
2331
+ }
2332
+ if (isRequestBodyValidationError(error)) {
2333
+ return 400;
2334
+ }
981
2335
  if (error instanceof HTTPException) {
982
2336
  return error.status;
983
2337
  }
@@ -991,7 +2345,7 @@ export function errorCodeForStatus(status: number): ErrorCode {
991
2345
  if (status === 401) return "unauthenticated";
992
2346
  if (status === 402) return "payment_required";
993
2347
  if (status === 403) return "forbidden";
994
- if (status === 404) return "not_found";
2348
+ if (status === 404 || status === 405) return "not_found";
995
2349
  if (status === 409) return "conflict";
996
2350
  if (status === 413 || status === 422 || status === 400) return "validation_failed";
997
2351
  if (status === 429) return "limit_exceeded";
@@ -1004,11 +2358,14 @@ function retryableHttpStatus(status: number): boolean {
1004
2358
  }
1005
2359
 
1006
2360
  function publicErrorMessage(error: unknown, status: number): string {
2361
+ if (error instanceof ConnectAttemptConflictError)
2362
+ return "Connection setup changed or is still in progress. Reload its current status before retrying.";
2363
+ if (error instanceof ConnectAttemptNotFoundError) return "Connection setup not found.";
1007
2364
  if (status === 502 || status === 503 || status === 504) {
1008
- return "OpenGeni is temporarily unavailable — retry.";
2365
+ return "Opengeni is temporarily unavailable — retry.";
1009
2366
  }
1010
2367
  if (status >= 500) {
1011
- return "OpenGeni could not complete the request.";
2368
+ return "Opengeni could not complete the request.";
1012
2369
  }
1013
2370
  if (error instanceof HTTPException) {
1014
2371
  return boundedPublicMessage(error.message) ?? "Request failed.";
@@ -1038,19 +2395,26 @@ type ReadinessChecks = Record<ReadinessCheckName, ReadinessCheck>;
1038
2395
  type ReadinessCheckResult = { ok: boolean; error?: string };
1039
2396
 
1040
2397
  function readinessChecks(deps: AppDependencies): ReadinessChecks {
2398
+ const configuredNatsCheck = deps.readinessChecks?.nats;
1041
2399
  return {
1042
- db:
1043
- deps.readinessChecks?.db ??
1044
- (async () => {
2400
+ db: async () => {
2401
+ if (deps.readinessChecks?.db) {
2402
+ await deps.readinessChecks.db();
2403
+ } else {
1045
2404
  await deps.db.execute(dbSql`select 1`);
1046
- }),
1047
- nats:
1048
- deps.readinessChecks?.nats ??
1049
- (() => {
2405
+ }
2406
+ await resolveCatalogSettings(deps.db, deps.settings);
2407
+ },
2408
+ nats: async () => {
2409
+ requireSessionEventDurableFanoutCapability(deps.bus);
2410
+ if (configuredNatsCheck) {
2411
+ await configuredNatsCheck();
2412
+ } else {
1050
2413
  if (deps.bus.isConnected && !deps.bus.isConnected()) {
1051
2414
  throw new Error("NATS is not connected");
1052
2415
  }
1053
- }),
2416
+ }
2417
+ },
1054
2418
  temporal:
1055
2419
  deps.readinessChecks?.temporal ??
1056
2420
  deps.workflowClient.check ??
@@ -1115,8 +2479,37 @@ const routeLabelPatterns: Array<{
1115
2479
  pattern: RegExp;
1116
2480
  label: string | ((match: RegExpMatchArray) => string);
1117
2481
  }> = [
2482
+ {
2483
+ pattern: /^\/\.well-known\/oauth-authorization-server$/,
2484
+ label: "/.well-known/oauth-authorization-server",
2485
+ },
2486
+ {
2487
+ pattern:
2488
+ /^\/\.well-known\/oauth-protected-resource\/v1\/workspaces\/[^/]+\/mcp(?:\/(docs|files))?$/,
2489
+ label: (match) =>
2490
+ `/.well-known/oauth-protected-resource/v1/workspaces/:workspaceId/mcp${match[1] ? `/${match[1]}` : ""}`,
2491
+ },
2492
+ {
2493
+ pattern: /^\/oauth\/(register|authorize|token)$/,
2494
+ label: (match) => `/oauth/${match[1]}`,
2495
+ },
1118
2496
  { pattern: /^\/healthz$/, label: "/healthz" },
1119
2497
  { pattern: /^\/readyz$/, label: "/readyz" },
2498
+ // Better Auth answers these behind one `/v1/auth/*` registration, so the
2499
+ // provider endpoints need an explicit closed set to stay distinguishable.
2500
+ {
2501
+ pattern:
2502
+ /^\/v1\/auth\/(sign-up\/email|sign-in\/email|sign-in\/social|sign-out|send-verification-email|verify-email|request-password-reset|reset-password|error|ok)$/,
2503
+ label: (match) => `/v1/auth/${match[1]}`,
2504
+ },
2505
+ { pattern: /^\/v1\/auth\/reset-password\/[^/]+$/, label: "/v1/auth/reset-password/:token" },
2506
+ {
2507
+ pattern: /^\/v1\/auth\/callback\/([^/]+)$/,
2508
+ label: (match) =>
2509
+ match[1] === "google" || match[1] === "github"
2510
+ ? `/v1/auth/callback/${match[1]}`
2511
+ : "/v1/auth/callback/:providerId",
2512
+ },
1120
2513
  { pattern: /^\/traffic-readyz$/, label: "/traffic-readyz" },
1121
2514
  {
1122
2515
  pattern: /^\/v1\/workspaces\/[^/]+\/codex\/connect\/start$/,
@@ -1130,6 +2523,22 @@ const routeLabelPatterns: Array<{
1130
2523
  pattern: /^\/v1\/workspaces\/[^/]+\/codex\/status$/,
1131
2524
  label: "/v1/workspaces/:workspaceId/codex/status",
1132
2525
  },
2526
+ {
2527
+ pattern: /^\/v1\/workspaces\/[^/]+\/codex\/source$/,
2528
+ label: "/v1/workspaces/:workspaceId/codex/source",
2529
+ },
2530
+ {
2531
+ pattern: /^\/v1\/organizations\/[^/]+\/codex\/(accounts|settings)$/,
2532
+ label: (match) => `/v1/organizations/:organizationId/codex/${match[1]}`,
2533
+ },
2534
+ {
2535
+ pattern: /^\/v1\/organizations\/[^/]+\/codex\/connect\/(start|poll)$/,
2536
+ label: (match) => `/v1/organizations/:organizationId/codex/connect/${match[1]}`,
2537
+ },
2538
+ {
2539
+ pattern: /^\/v1\/organizations\/[^/]+\/codex\/accounts\/[^/]+(?:\/activate)?$/,
2540
+ label: "/v1/organizations/:organizationId/codex/accounts/:accountId",
2541
+ },
1133
2542
  {
1134
2543
  pattern: /^\/v1\/workspaces\/[^/]+\/supergrok\/connect\/(start|poll)$/,
1135
2544
  label: (match) => `/v1/workspaces/:workspaceId/supergrok/connect/${match[1]}`,
@@ -1156,6 +2565,8 @@ const routeLabelPatterns: Array<{
1156
2565
  },
1157
2566
  { pattern: /^\/metrics$/, label: "/metrics" },
1158
2567
  { pattern: /^\/v1\/config\/client$/, label: "/v1/config/client" },
2568
+ { pattern: /^\/v1\/client-errors$/, label: "/v1/client-errors" },
2569
+ { pattern: /^\/v1\/analytics-consent$/, label: "/v1/analytics-consent" },
1159
2570
  { pattern: /^\/v1\/billing$/, label: "/v1/billing" },
1160
2571
  { pattern: /^\/v1\/billing\/checkout$/, label: "/v1/billing/checkout" },
1161
2572
  { pattern: /^\/v1\/billing\/usage$/, label: "/v1/billing/usage" },
@@ -1163,11 +2574,31 @@ const routeLabelPatterns: Array<{
1163
2574
  pattern: /^\/v1\/workspaces\/[^/]+\/insights$/,
1164
2575
  label: "/v1/workspaces/:workspaceId/insights",
1165
2576
  },
2577
+ {
2578
+ pattern: /^\/v1\/workspaces\/[^/]+\/insights\/usage$/,
2579
+ label: "/v1/workspaces/:workspaceId/insights/usage",
2580
+ },
2581
+ {
2582
+ pattern: /^\/v1\/workspaces\/[^/]+\/insights\/calls$/,
2583
+ label: "/v1/workspaces/:workspaceId/insights/calls",
2584
+ },
2585
+ {
2586
+ pattern: /^\/v1\/organizations\/[^/]+\/insights\/usage$/,
2587
+ label: "/v1/organizations/:accountId/insights/usage",
2588
+ },
2589
+ {
2590
+ pattern: /^\/v1\/organizations\/[^/]+\/insights\/calls$/,
2591
+ label: "/v1/organizations/:accountId/insights/calls",
2592
+ },
1166
2593
  {
1167
2594
  pattern: /^\/v1\/billing\/entitlements$/,
1168
2595
  label: "/v1/billing/entitlements",
1169
2596
  },
1170
2597
  { pattern: /^\/v1\/webhooks\/stripe$/, label: "/v1/webhooks/stripe" },
2598
+ {
2599
+ pattern: /^\/v1\/workspaces\/[^/]+\/pr-review\/(registrations|repositories)(?:\/[^/]+)?$/,
2600
+ label: (match) => `/v1/workspaces/:workspaceId/pr-review/${match[1]}`,
2601
+ },
1171
2602
  {
1172
2603
  pattern: /^\/v1\/workspaces\/[^/]+\/mcp$/,
1173
2604
  label: "/v1/workspaces/:workspaceId/mcp",
@@ -1180,6 +2611,22 @@ const routeLabelPatterns: Array<{
1180
2611
  pattern: /^\/v1\/workspaces\/[^/]+\/mcp\/files$/,
1181
2612
  label: "/v1/workspaces/:workspaceId/mcp/files",
1182
2613
  },
2614
+ {
2615
+ pattern: /^\/v1\/workspaces\/[^/]+\/tools\/catalog$/,
2616
+ label: "/v1/workspaces/:workspaceId/tools/catalog",
2617
+ },
2618
+ {
2619
+ pattern: /^\/v1\/workspaces\/[^/]+\/tools\/calls$/,
2620
+ label: "/v1/workspaces/:workspaceId/tools/calls",
2621
+ },
2622
+ {
2623
+ pattern: /^\/v1\/workspaces\/[^/]+\/tools\/approvals$/,
2624
+ label: "/v1/workspaces/:workspaceId/tools/approvals",
2625
+ },
2626
+ {
2627
+ pattern: /^\/v1\/workspaces\/[^/]+\/tools\/declarations$/,
2628
+ label: "/v1/workspaces/:workspaceId/tools/declarations",
2629
+ },
1183
2630
  {
1184
2631
  pattern: /^\/v1\/workspaces\/[^/]+\/default-rig$/,
1185
2632
  label: "/v1/workspaces/:workspaceId/default-rig",
@@ -1188,6 +2635,18 @@ const routeLabelPatterns: Array<{
1188
2635
  pattern: /^\/v1\/workspaces\/[^/]+\/sessions$/,
1189
2636
  label: "/v1/workspaces/:workspaceId/sessions",
1190
2637
  },
2638
+ {
2639
+ pattern: /^\/v1\/workspaces\/[^/]+\/session-imports$/,
2640
+ label: "/v1/workspaces/:workspaceId/session-imports",
2641
+ },
2642
+ {
2643
+ pattern: /^\/v1\/workspaces\/[^/]+\/session-imports\/[^/]+\/events$/,
2644
+ label: "/v1/workspaces/:workspaceId/session-imports/:importId/events",
2645
+ },
2646
+ {
2647
+ pattern: /^\/v1\/workspaces\/[^/]+\/session-message-search$/,
2648
+ label: "/v1/workspaces/:workspaceId/session-message-search",
2649
+ },
1191
2650
  {
1192
2651
  pattern: /^\/v1\/workspaces\/[^/]+\/control-events\/stream$/,
1193
2652
  label: "/v1/workspaces/:workspaceId/control-events/stream",
@@ -1285,6 +2744,11 @@ const routeLabelPatterns: Array<{
1285
2744
  label:
1286
2745
  "/v1/workspaces/:workspaceId/computer-sessions/:computerSessionId/targets/:targetId/observation",
1287
2746
  },
2747
+ {
2748
+ pattern: /^\/v1\/workspaces\/[^/]+\/computer-sessions\/[^/]+\/targets\/[^/]+\/screenshot$/,
2749
+ label:
2750
+ "/v1/workspaces/:workspaceId/computer-sessions/:computerSessionId/targets/:targetId/screenshot",
2751
+ },
1288
2752
  {
1289
2753
  pattern: /^\/v1\/workspaces\/[^/]+\/computer-sessions\/[^/]+\/operations\/[^/]+$/,
1290
2754
  label:
@@ -1348,6 +2812,10 @@ const routeLabelPatterns: Array<{
1348
2812
  pattern: /^\/v1\/workspaces\/[^/]+\/inference-control$/,
1349
2813
  label: "/v1/workspaces/:workspaceId/inference-control",
1350
2814
  },
2815
+ {
2816
+ pattern: /^\/v1\/workspaces\/[^/]+\/pause-timer$/,
2817
+ label: "/v1/workspaces/:workspaceId/pause-timer",
2818
+ },
1351
2819
  {
1352
2820
  pattern:
1353
2821
  /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/fs\/(list|list-batch|read|write|delete|move|mkdir)$/,
@@ -1369,6 +2837,10 @@ const routeLabelPatterns: Array<{
1369
2837
  pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/events\/stream$/,
1370
2838
  label: "/v1/workspaces/:workspaceId/sessions/:id/events/stream",
1371
2839
  },
2840
+ {
2841
+ pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/(visibility|forks)$/,
2842
+ label: (match) => `/v1/workspaces/:workspaceId/sessions/:id/${match[1]}`,
2843
+ },
1372
2844
  {
1373
2845
  pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/lineage$/,
1374
2846
  label: "/v1/workspaces/:workspaceId/sessions/:id/lineage",
@@ -1377,6 +2849,10 @@ const routeLabelPatterns: Array<{
1377
2849
  pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/events$/,
1378
2850
  label: "/v1/workspaces/:workspaceId/sessions/:id/events",
1379
2851
  },
2852
+ {
2853
+ pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/turns$/,
2854
+ label: "/v1/workspaces/:workspaceId/sessions/:id/turns",
2855
+ },
1380
2856
  {
1381
2857
  pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/queue\/[^/]+\/(move|edit|steer|delete)$/,
1382
2858
  label: "/v1/workspaces/:workspaceId/sessions/:id/queue/:turnId/:action",
@@ -1385,6 +2861,10 @@ const routeLabelPatterns: Array<{
1385
2861
  pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/queue$/,
1386
2862
  label: "/v1/workspaces/:workspaceId/sessions/:id/queue",
1387
2863
  },
2864
+ {
2865
+ pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/composer-draft\/submit$/,
2866
+ label: "/v1/workspaces/:workspaceId/sessions/:id/composer-draft/submit",
2867
+ },
1388
2868
  {
1389
2869
  pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/composer-draft$/,
1390
2870
  label: "/v1/workspaces/:workspaceId/sessions/:id/composer-draft",
@@ -1449,6 +2929,84 @@ const routeLabelPatterns: Array<{
1449
2929
  pattern: /^\/v1\/workspaces\/[^/]+\/api-keys\/[^/]+$/,
1450
2930
  label: "/v1/workspaces/:workspaceId/api-keys/:id",
1451
2931
  },
2932
+ {
2933
+ pattern: /^\/v1\/organizations\/[^/]+\/api-keys$/,
2934
+ label: "/v1/organizations/:organizationId/api-keys",
2935
+ },
2936
+ {
2937
+ pattern: /^\/v1\/organizations\/[^/]+\/api-keys\/[^/]+$/,
2938
+ label: "/v1/organizations/:organizationId/api-keys/:id",
2939
+ },
2940
+ {
2941
+ pattern: /^\/v1\/workspaces\/external$/,
2942
+ label: "/v1/workspaces/external",
2943
+ },
2944
+ {
2945
+ pattern: /^\/v1\/workspaces\/external\/[^/]+\/[^/]+\/session-imports$/,
2946
+ label: "/v1/workspaces/external/:source/:externalId/session-imports",
2947
+ },
2948
+ {
2949
+ pattern: /^\/v1\/workspaces\/external\/[^/]+\/[^/]+\/session-imports\/[^/]+\/events$/,
2950
+ label: "/v1/workspaces/external/:source/:externalId/session-imports/:importId/events",
2951
+ },
2952
+ {
2953
+ pattern: /^\/v1\/workspaces\/external\/[^/]+\/[^/]+\/allowance\/grants$/,
2954
+ label: "/v1/workspaces/external/:source/:externalId/allowance/grants",
2955
+ },
2956
+ {
2957
+ pattern: /^\/v1\/workspaces\/external\/[^/]+\/[^/]+\/allowance$/,
2958
+ label: "/v1/workspaces/external/:source/:externalId/allowance",
2959
+ },
2960
+ {
2961
+ pattern: /^\/v1\/workspaces\/external\/[^/]+\/[^/]+\/allowance\/state$/,
2962
+ label: "/v1/workspaces/external/:source/:externalId/allowance/state",
2963
+ },
2964
+ {
2965
+ pattern: /^\/v1\/workspaces\/external\/[^/]+\/[^/]+\/usage\/me$/,
2966
+ label: "/v1/workspaces/external/:source/:externalId/usage/me",
2967
+ },
2968
+ {
2969
+ pattern: /^\/v1\/workspaces\/external\/[^/]+\/[^/]+\/usage$/,
2970
+ label: "/v1/workspaces/external/:source/:externalId/usage",
2971
+ },
2972
+ {
2973
+ pattern:
2974
+ /^\/v1\/workspaces\/external\/[^/]+\/[^/]+\/members\/external\/[^/]+\/[^/]+\/allowance$/,
2975
+ label:
2976
+ "/v1/workspaces/external/:source/:externalId/members/external/:memberSource/:memberExternalId/allowance",
2977
+ },
2978
+ {
2979
+ pattern: /^\/v1\/workspaces\/external\/[^/]+\/[^/]+\/members\/[^/]+\/allowance$/,
2980
+ label: "/v1/workspaces/external/:source/:externalId/members/:subjectId/allowance",
2981
+ },
2982
+ {
2983
+ pattern: /^\/v1\/workspaces\/[^/]+\/allowance\/grants$/,
2984
+ label: "/v1/workspaces/:workspaceId/allowance/grants",
2985
+ },
2986
+ {
2987
+ pattern: /^\/v1\/workspaces\/[^/]+\/allowance$/,
2988
+ label: "/v1/workspaces/:workspaceId/allowance",
2989
+ },
2990
+ {
2991
+ pattern: /^\/v1\/workspaces\/[^/]+\/allowance\/state$/,
2992
+ label: "/v1/workspaces/:workspaceId/allowance/state",
2993
+ },
2994
+ {
2995
+ pattern: /^\/v1\/workspaces\/[^/]+\/usage\/me$/,
2996
+ label: "/v1/workspaces/:workspaceId/usage/me",
2997
+ },
2998
+ {
2999
+ pattern: /^\/v1\/workspaces\/[^/]+\/usage$/,
3000
+ label: "/v1/workspaces/:workspaceId/usage",
3001
+ },
3002
+ {
3003
+ pattern: /^\/v1\/workspaces\/[^/]+\/members\/external\/[^/]+\/[^/]+\/allowance$/,
3004
+ label: "/v1/workspaces/:workspaceId/members/external/:source/:externalId/allowance",
3005
+ },
3006
+ {
3007
+ pattern: /^\/v1\/workspaces\/[^/]+\/members\/[^/]+\/allowance$/,
3008
+ label: "/v1/workspaces/:workspaceId/members/:subjectId/allowance",
3009
+ },
1452
3010
  {
1453
3011
  pattern: /^\/v1\/workspaces\/[^/]+\/scheduled-tasks$/,
1454
3012
  label: "/v1/workspaces/:workspaceId/scheduled-tasks",
@@ -1513,6 +3071,10 @@ const routeLabelPatterns: Array<{
1513
3071
  pattern: /^\/v1\/workspaces\/[^/]+\/github\/app$/,
1514
3072
  label: "/v1/workspaces/:workspaceId/github/app",
1515
3073
  },
3074
+ {
3075
+ pattern: /^\/v1\/workspaces\/[^/]+\/github\/action-policies$/,
3076
+ label: "/v1/workspaces/:workspaceId/github/action-policies",
3077
+ },
1516
3078
  {
1517
3079
  pattern: /^\/v1\/workspaces\/[^/]+\/github\/repositories$/,
1518
3080
  label: "/v1/workspaces/:workspaceId/github/repositories",
@@ -1545,6 +3107,22 @@ const routeLabelPatterns: Array<{
1545
3107
  pattern: /^\/v1\/workspaces\/[^/]+\/github\/app-manifest$/,
1546
3108
  label: "/v1/workspaces/:workspaceId/github/app-manifest",
1547
3109
  },
3110
+ {
3111
+ pattern: /^\/v1\/workspaces\/[^/]+\/pr-review\/github$/,
3112
+ label: "/v1/workspaces/:workspaceId/pr-review/github",
3113
+ },
3114
+ {
3115
+ pattern: /^\/v1\/workspaces\/[^/]+\/pr-review\/github\/connect$/,
3116
+ label: "/v1/workspaces/:workspaceId/pr-review/github/connect",
3117
+ },
3118
+ {
3119
+ pattern: /^\/v1\/workspaces\/[^/]+\/pr-review\/github\/installations\/select$/,
3120
+ label: "/v1/workspaces/:workspaceId/pr-review/github/installations/select",
3121
+ },
3122
+ {
3123
+ pattern: /^\/v1\/workspaces\/[^/]+\/pr-review\/github\/installations\/[^/]+\/configure$/,
3124
+ label: "/v1/workspaces/:workspaceId/pr-review/github/installations/:installationId/configure",
3125
+ },
1548
3126
  {
1549
3127
  pattern: /^\/v1\/workspaces\/[^/]+\/capabilities$/,
1550
3128
  label: "/v1/workspaces/:workspaceId/capabilities",
@@ -1647,26 +3225,7 @@ const routeLabelPatterns: Array<{
1647
3225
  pattern: /^\/v1\/workspaces\/[^/]+\/environments\/[^/]+$/,
1648
3226
  label: "/v1/workspaces/:workspaceId/environments/:id",
1649
3227
  },
1650
- {
1651
- pattern: /^\/v1\/workspaces\/[^/]+\/packs$/,
1652
- label: "/v1/workspaces/:workspaceId/packs",
1653
- },
1654
- {
1655
- pattern: /^\/v1\/workspaces\/[^/]+\/packs\/installations$/,
1656
- label: "/v1/workspaces/:workspaceId/packs/installations",
1657
- },
1658
- {
1659
- pattern: /^\/v1\/workspaces\/[^/]+\/packs\/marketing-social-daily-analysis\/scheduled-tasks$/,
1660
- label: "/v1/workspaces/:workspaceId/packs/marketing-social-daily-analysis/scheduled-tasks",
1661
- },
1662
- {
1663
- pattern: /^\/v1\/workspaces\/[^/]+\/packs\/[^/]+\/enable$/,
1664
- label: "/v1/workspaces/:workspaceId/packs/:id/enable",
1665
- },
1666
- {
1667
- pattern: /^\/v1\/workspaces\/[^/]+\/packs\/[^/]+$/,
1668
- label: "/v1/workspaces/:workspaceId/packs/:id",
1669
- },
3228
+
1670
3229
  {
1671
3230
  pattern: /^\/v1\/workspaces\/[^/]+\/plugins\/preview$/,
1672
3231
  label: "/v1/workspaces/:workspaceId/plugins/preview",
@@ -1703,6 +3262,26 @@ const routeLabelPatterns: Array<{
1703
3262
  pattern: /^\/v1\/workspaces\/[^/]+\/connections\/oauth\/start$/,
1704
3263
  label: "/v1/workspaces/:workspaceId/connections/oauth/start",
1705
3264
  },
3265
+ {
3266
+ pattern: /^\/v1\/workspaces\/[^/]+\/connections\/github$/,
3267
+ label: "/v1/workspaces/:workspaceId/connections/github",
3268
+ },
3269
+ {
3270
+ pattern: /^\/v1\/workspaces\/[^/]+\/connections\/github\/oauth\/start$/,
3271
+ label: "/v1/workspaces/:workspaceId/connections/github/oauth/start",
3272
+ },
3273
+ {
3274
+ pattern: /^\/v1\/workspaces\/[^/]+\/connections\/[^/]+\/github\/reconnect$/,
3275
+ label: "/v1/workspaces/:workspaceId/connections/:connectionId/github/reconnect",
3276
+ },
3277
+ {
3278
+ pattern: /^\/v1\/workspaces\/[^/]+\/connections\/[^/]+\/github\/repositories\/verify$/,
3279
+ label: "/v1/workspaces/:workspaceId/connections/:connectionId/github/repositories/verify",
3280
+ },
3281
+ {
3282
+ pattern: /^\/v1\/workspaces\/[^/]+\/connections\/[^/]+\/github\/repositories$/,
3283
+ label: "/v1/workspaces/:workspaceId/connections/:connectionId/github/repositories",
3284
+ },
1706
3285
  {
1707
3286
  pattern: /^\/v1\/workspaces\/[^/]+\/integrations\/oauth\/start$/,
1708
3287
  label: "/v1/workspaces/:workspaceId/integrations/oauth/start",
@@ -1727,6 +3306,18 @@ const routeLabelPatterns: Array<{
1727
3306
  pattern: /^\/v1\/workspaces\/[^/]+\/connections\/[^/]+$/,
1728
3307
  label: "/v1/workspaces/:workspaceId/connections/:connectionId",
1729
3308
  },
3309
+ {
3310
+ pattern: /^\/v1\/git\/personal\/[^/]+\/info\/refs$/,
3311
+ label: "/v1/git/personal/:routeId/info/refs",
3312
+ },
3313
+ {
3314
+ pattern: /^\/v1\/git\/personal\/[^/]+\/git-upload-pack$/,
3315
+ label: "/v1/git/personal/:routeId/git-upload-pack",
3316
+ },
3317
+ {
3318
+ pattern: /^\/v1\/git\/personal\/[^/]+\/git-receive-pack$/,
3319
+ label: "/v1/git/personal/:routeId/git-receive-pack",
3320
+ },
1730
3321
  { pattern: /^\/v1\/catalog-assets\/.+$/, label: "/v1/catalog-assets/*" },
1731
3322
  {
1732
3323
  pattern: /^\/v1\/integrations\/oauth\/callback$/,
@@ -1736,6 +3327,10 @@ const routeLabelPatterns: Array<{
1736
3327
  pattern: /^\/v1\/integrations\/provider-oauth\/callback$/,
1737
3328
  label: "/v1/integrations/provider-oauth/callback",
1738
3329
  },
3330
+ {
3331
+ pattern: /^\/v1\/integrations\/github-personal\/oauth\/callback$/,
3332
+ label: "/v1/integrations/github-personal/oauth/callback",
3333
+ },
1739
3334
  {
1740
3335
  pattern: /^\/v1\/integrations\/google-drive\/callback$/,
1741
3336
  label: "/v1/integrations/google-drive/callback",
@@ -1776,6 +3371,14 @@ const routeLabelPatterns: Array<{
1776
3371
  pattern: /^\/v1\/workspaces\/[^/]+\/machines\/[^/]+\/metrics\/series$/,
1777
3372
  label: "/v1/workspaces/:workspaceId/machines/:enrollmentId/metrics/series",
1778
3373
  },
3374
+ {
3375
+ pattern: /^\/v1\/workspaces\/[^/]+\/machines\/[^/]+\/operation-policy$/,
3376
+ label: "/v1/workspaces/:workspaceId/machines/:enrollmentId/operation-policy",
3377
+ },
3378
+ {
3379
+ pattern: /^\/v1\/workspaces\/[^/]+\/machines\/[^/]+\/update$/,
3380
+ label: "/v1/workspaces/:workspaceId/machines/:enrollmentId/update",
3381
+ },
1779
3382
  {
1780
3383
  pattern: /^\/v1\/workspaces\/[^/]+\/machines$/,
1781
3384
  label: "/v1/workspaces/:workspaceId/machines",
@@ -1793,18 +3396,105 @@ const routeLabelPatterns: Array<{
1793
3396
  pattern: /^\/v1\/github\/oauth\/callback$/,
1794
3397
  label: "/v1/github/oauth/callback",
1795
3398
  },
3399
+ { pattern: /^\/v1\/pr-review\/github\/setup$/, label: "/v1/pr-review/github/setup" },
3400
+ {
3401
+ pattern: /^\/v1\/pr-review\/github\/install\/callback$/,
3402
+ label: "/v1/pr-review/github/install/callback",
3403
+ },
3404
+ {
3405
+ pattern: /^\/v1\/pr-review\/github\/oauth\/callback$/,
3406
+ label: "/v1/pr-review/github/oauth/callback",
3407
+ },
3408
+ {
3409
+ pattern: /^\/v1\/webhooks\/pr-review\/github$/,
3410
+ label: "/v1/webhooks/pr-review/github",
3411
+ },
3412
+ {
3413
+ pattern: /^\/v1\/workspaces\/[^/]+$/,
3414
+ label: "/v1/workspaces/:workspaceId",
3415
+ },
1796
3416
  ];
1797
3417
 
1798
- export function routeLabel(pathname: string): string {
3418
+ /**
3419
+ * Bounded route label for metrics, spans, and request logs. Explicit patterns
3420
+ * keep established label spellings stable; any other request answered by a
3421
+ * registered handler uses that handler's code-owned path template (see
3422
+ * `registeredHandlerRoutePath`). Only unregistered paths fall into `unknown`.
3423
+ */
3424
+ export function routeLabel(pathname: string, registeredRoutePath?: string | null): string {
3425
+ if (/^\/v1\/workspaces\/[^/]+\/transcriptions$/.test(pathname))
3426
+ return "/v1/workspaces/:workspaceId/transcriptions";
3427
+ const transcription = pathname.match(
3428
+ /^\/v1\/workspaces\/[^/]+\/transcription-recordings(?:\/[^/]+(\/(?:finalize|process-next)|\/chunks\/\d+)?)?$/,
3429
+ );
3430
+ if (transcription) {
3431
+ const base = "/v1/workspaces/:workspaceId/transcription-recordings";
3432
+ if (pathname.split("/").length === 5) return base;
3433
+ return (
3434
+ base +
3435
+ "/:recordingId" +
3436
+ (transcription[1]?.startsWith("/chunks/") ? "/chunks/:chunkNumber" : (transcription[1] ?? ""))
3437
+ );
3438
+ }
1799
3439
  for (const candidate of routeLabelPatterns) {
1800
3440
  const match = pathname.match(candidate.pattern);
1801
3441
  if (match) {
1802
3442
  return typeof candidate.label === "string" ? candidate.label : candidate.label(match);
1803
3443
  }
1804
3444
  }
3445
+ const registered = boundedRegisteredRouteLabel(registeredRoutePath);
3446
+ if (registered) return registered;
1805
3447
  return pathname.startsWith("/v1/") ? "/v1/unknown" : "/unknown";
1806
3448
  }
1807
3449
 
3450
+ /**
3451
+ * API contract revisions no longer admitted from ANY caller, including bearer
3452
+ * integrations that would otherwise be accepted across revisions. Add a
3453
+ * revision here only for a truly breaking wire change (which also requires a
3454
+ * major release-train change); ordinary revision bumps stay additive.
3455
+ */
3456
+ export const REFUSED_API_CONTRACT_REVISIONS: ReadonlySet<string> = new Set<string>([]);
3457
+
3458
+ const BEARER_AUTHORIZATION = /^bearer\s+\S/i;
3459
+
3460
+ /**
3461
+ * The rollout fence for state-changing product calls.
3462
+ *
3463
+ * The exact-revision check exists to stop a stale first-party browser tab
3464
+ * (cookie session, or unauthenticated local mode) from writing with an old
3465
+ * request shape after a deployment; it answers 409 so the page reloads onto
3466
+ * the matching bundle. That tab never sends `Authorization`, so this is not a
3467
+ * header a stale bundle can choose to bypass it with.
3468
+ *
3469
+ * Bearer-authenticated callers (organization/workspace API keys, delegated
3470
+ * tokens, the deployment key sent as a bearer) are integrations pinned to an
3471
+ * SDK version: reloading cannot upgrade them, and the API is additive within a
3472
+ * major release train. They are admitted with an older revision or with no
3473
+ * revision claim at all. Only a revision listed in
3474
+ * `REFUSED_API_CONTRACT_REVISIONS` is refused for them.
3475
+ *
3476
+ * This is a compatibility fence, not an authorization boundary: every route
3477
+ * still authenticates and authorizes the request independently.
3478
+ */
3479
+ export function apiContractAdmission(
3480
+ input: {
3481
+ method: string;
3482
+ pathname: string;
3483
+ authorization: string | undefined;
3484
+ claimedRevision: string | undefined;
3485
+ },
3486
+ refusedRevisions: ReadonlySet<string> = REFUSED_API_CONTRACT_REVISIONS,
3487
+ ): "admit" | "reject" {
3488
+ if (!isApiContractProtectedMutation(input.method, input.pathname)) return "admit";
3489
+ if (input.claimedRevision === OPENGENI_API_CONTRACT_REVISION) return "admit";
3490
+ if (input.claimedRevision !== undefined && refusedRevisions.has(input.claimedRevision)) {
3491
+ return "reject";
3492
+ }
3493
+ return input.authorization !== undefined && BEARER_AUTHORIZATION.test(input.authorization)
3494
+ ? "admit"
3495
+ : "reject";
3496
+ }
3497
+
1808
3498
  /**
1809
3499
  * State-changing OpenGeni HTTP calls must never cross an incompatible rollout
1810
3500
  * boundary. Standard third-party protocols and externally initiated callbacks
@@ -1817,19 +3507,47 @@ export function isApiContractProtectedMutation(method: string, pathname: string)
1817
3507
  if (!pathname.startsWith("/v1/")) {
1818
3508
  return false;
1819
3509
  }
3510
+ if (
3511
+ pathname === "/v1/auth/organization-onboarding" ||
3512
+ pathname === "/v1/auth/organization-setup"
3513
+ ) {
3514
+ return true;
3515
+ }
1820
3516
  if (
1821
3517
  pathname.startsWith("/v1/auth/") ||
1822
3518
  pathname.startsWith("/v1/webhooks/") ||
1823
3519
  pathname.startsWith("/v1/integrations/oauth/") ||
3520
+ isPersonalGitHubGitBrokerRequest(method, pathname) ||
1824
3521
  pathname === "/v1/integrations/slack/events" ||
1825
3522
  pathname === "/v1/integrations/slack/commands" ||
1826
3523
  pathname === "/v1/integrations/slack/interactions" ||
1827
3524
  pathname.startsWith("/v1/github/") ||
3525
+ // A stale tab must still report the error that follows a rollout.
3526
+ pathname === CLIENT_ERRORS_PATH ||
3527
+ pathname === ANALYTICS_CONSENT_PATH ||
1828
3528
  pathname === "/v1/enrollments/device/start" ||
1829
3529
  pathname === "/v1/enrollments/device/poll" ||
1830
- pathname === "/v1/enrollments/token/exchange"
3530
+ pathname === "/v1/enrollments/token/exchange" ||
3531
+ // The runner authenticates this protocol with its install-key proof, not
3532
+ // a browser cookie or the product-client revision header.
3533
+ pathname === "/v1/enrollments/renew"
1831
3534
  ) {
1832
3535
  return false;
1833
3536
  }
1834
- return !pathname.split("/").includes("mcp");
3537
+ const segments = pathname.split("/");
3538
+ return !segments.includes("mcp") && !segments.includes("codemode");
3539
+ }
3540
+
3541
+ /** Client-safe agent-configuration projection. */
3542
+ function clientAgentConfig(settings: Settings): ClientAgentConfig {
3543
+ const policy = agentConfigDeploymentPolicy(settings);
3544
+ return {
3545
+ // Deprecated: agent configuration is always on. Kept for client compatibility.
3546
+ enabled: true,
3547
+ defaultForNewSessions: true,
3548
+ capabilities: AGENT_CAPABILITY_IDS.map((id) => {
3549
+ const reason = policy.unavailable[id];
3550
+ return reason === undefined ? { id, available: true } : { id, available: false, reason };
3551
+ }),
3552
+ };
1835
3553
  }