@opengeni/api-router 3.0.1 → 3.0.2-canary.38894290271001

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (546) hide show
  1. package/dist/access-grant-rls.d.ts +8 -1
  2. package/dist/admission-trace.d.ts +8 -0
  3. package/dist/app.d.ts +50 -3
  4. package/dist/app.js +10 -1
  5. package/dist/artifact-catalog-cursor.d.ts +1 -0
  6. package/dist/attached-browser-viewer.d.ts +11 -0
  7. package/dist/auth/managed-auth-attempt-context.d.ts +2 -0
  8. package/dist/auth/managed-auth-rate-limits.d.ts +167 -0
  9. package/dist/auth/managed-auth-session-adapter.d.ts +9 -1
  10. package/dist/auth/managed-auth.d.ts +72 -5
  11. package/dist/auth/managed-sign-in-notifications.d.ts +4 -0
  12. package/dist/auth/new-signups-gate.d.ts +23 -0
  13. package/dist/auth/signup-funnel-metrics.d.ts +44 -0
  14. package/dist/azure-live.d.ts +28 -0
  15. package/dist/browser-action-fences.d.ts +13 -0
  16. package/dist/browser-controller-authority.d.ts +1 -61
  17. package/dist/browser-ephemeral.d.ts +5 -0
  18. package/dist/browser-lost-runtime-end.d.ts +24 -0
  19. package/dist/browser-state-authority.d.ts +1 -27
  20. package/dist/{chunk-AJ62T5QN.js → chunk-GBUJ2DYZ.js} +80507 -39878
  21. package/dist/chunk-GBUJ2DYZ.js.map +1 -0
  22. package/dist/chunk-OGQDXVXY.js +18 -0
  23. package/dist/chunk-OGQDXVXY.js.map +1 -0
  24. package/dist/claude-default-models.d.ts +19 -0
  25. package/dist/claude-subscription-core.d.ts +50 -0
  26. package/dist/claude-subscription-oauth.d.ts +36 -0
  27. package/dist/claude-subscription-profile.d.ts +9 -0
  28. package/dist/claude-subscription-usage.d.ts +6 -0
  29. package/dist/claude-workspace-connection.d.ts +15 -0
  30. package/dist/codemode.d.ts +40 -5
  31. package/dist/codex-realtime.d.ts +29 -3
  32. package/dist/codex-session-pointers.d.ts +19 -0
  33. package/dist/connection-ownership.d.ts +6 -3
  34. package/dist/controller-data-plane.d.ts +16 -0
  35. package/dist/credit-promotion-snapshot.d.ts +6 -0
  36. package/dist/editable-artifact-source-session.d.ts +3 -0
  37. package/dist/editable-artifact-websocket.d.ts +5 -3
  38. package/dist/fatal-process-boundary.d.ts +9 -1
  39. package/dist/github-access.d.ts +7 -1
  40. package/dist/github-browser-flow.d.ts +1 -1
  41. package/dist/http/acting-person.d.ts +36 -0
  42. package/dist/http/api-contract-compat.d.ts +19 -0
  43. package/dist/http/api-error.d.ts +50 -1
  44. package/dist/http/auth.d.ts +8 -1
  45. package/dist/http/cors.d.ts +12 -0
  46. package/dist/http/deprecation.d.ts +42 -0
  47. package/dist/http/keyed-admission.d.ts +12 -0
  48. package/dist/http/local-browser-boundary.d.ts +72 -0
  49. package/dist/http/metrics-listener.d.ts +22 -0
  50. package/dist/http/path-identifier.d.ts +17 -0
  51. package/dist/http/registered-route-label.d.ts +16 -0
  52. package/dist/http/rejection-telemetry.d.ts +34 -0
  53. package/dist/http/request-body.d.ts +27 -0
  54. package/dist/http/request-source.d.ts +41 -3
  55. package/dist/http/sse.d.ts +12 -1
  56. package/dist/http/unmatched-route.d.ts +18 -0
  57. package/dist/http/user-content.d.ts +36 -0
  58. package/dist/http/variable-sets-removed.d.ts +41 -0
  59. package/dist/http/websocket-upgrade-dispatch.d.ts +18 -0
  60. package/dist/index.js +1279 -108
  61. package/dist/index.js.map +1 -1
  62. package/dist/integration-connect-metrics.d.ts +35 -0
  63. package/dist/integration-endpoint-test.d.ts +20 -0
  64. package/dist/integrations/api-integrations.d.ts +2 -2
  65. package/dist/integrations/atlassian.d.ts +8 -3
  66. package/dist/integrations/fiken.d.ts +3 -1
  67. package/dist/integrations/github-lens-connect.d.ts +37 -66
  68. package/dist/integrations/google-drive.d.ts +2 -0
  69. package/dist/integrations/integration-commit-authority.d.ts +13 -0
  70. package/dist/integrations/marketplace-plugin.d.ts +43 -43
  71. package/dist/integrations/oauth-client.d.ts +171 -12
  72. package/dist/integrations/oauth-profiles.d.ts +42 -13
  73. package/dist/integrations/onepassword-vault-provider.d.ts +9 -0
  74. package/dist/integrations/slack-bot.d.ts +285 -7
  75. package/dist/integrations/slack-file-upload-flow.d.ts +62 -0
  76. package/dist/integrations/slack-interactions.d.ts +89 -10
  77. package/dist/integrations/slack-mrkdwn.d.ts +41 -0
  78. package/dist/integrations/slack-routing.d.ts +1 -1
  79. package/dist/integrations/slack-session-defaults.d.ts +58 -0
  80. package/dist/integrations/slack-task-file-upload.d.ts +36 -0
  81. package/dist/integrations/slack-tool-review.d.ts +5 -0
  82. package/dist/interaction-agent-access.d.ts +13 -8
  83. package/dist/interaction-agent-version.d.ts +21 -0
  84. package/dist/interaction-holder-heartbeat.d.ts +5 -0
  85. package/dist/interaction-metrics.d.ts +27 -2
  86. package/dist/interaction-route-stages.d.ts +25 -0
  87. package/dist/interaction-secrets-vault.d.ts +20 -0
  88. package/dist/interaction-secrets.d.ts +103 -0
  89. package/dist/mcp/action-catalog-types.d.ts +14 -0
  90. package/dist/mcp/action-catalog.gen.d.ts +2 -0
  91. package/dist/mcp/action-descriptions.d.ts +4 -0
  92. package/dist/mcp/action-schema-imports.gen.d.ts +1 -0
  93. package/dist/mcp/capability-account-readiness.d.ts +7 -0
  94. package/dist/mcp/child-read-evidence.d.ts +9 -0
  95. package/dist/mcp/company-profile-agent-admin.d.ts +12 -0
  96. package/dist/mcp/contract-input.d.ts +13 -0
  97. package/dist/mcp/documents.d.ts +2 -0
  98. package/dist/mcp/first-party-tool-permissions.d.ts +3 -606
  99. package/dist/mcp/notification-tools.d.ts +26 -0
  100. package/dist/mcp/orchestration-failure-diagnostic.d.ts +43 -0
  101. package/dist/mcp/scheduled-task-input.d.ts +1309 -0
  102. package/dist/mcp/scheduled-task-view.d.ts +217 -3
  103. package/dist/mcp/secret-tools.d.ts +14 -0
  104. package/dist/mcp/server.d.ts +108 -5
  105. package/dist/mcp/session-admin-access-tools.d.ts +33 -0
  106. package/dist/mcp/session-event-errors.d.ts +28 -0
  107. package/dist/mcp/session-event-view.d.ts +62 -0
  108. package/dist/mcp/session-view.d.ts +15 -0
  109. package/dist/mcp/session-wait.d.ts +18 -0
  110. package/dist/mcp/site-agent-clamp-tools.d.ts +38 -0
  111. package/dist/mcp/site-data-tools.d.ts +22 -0
  112. package/dist/mcp/site-publication-tools.d.ts +19 -0
  113. package/dist/mcp/site-sql-tools.d.ts +27 -0
  114. package/dist/mcp-oauth.d.ts +37 -4
  115. package/dist/model-calls/chat-completions.d.ts +87 -0
  116. package/dist/model-calls/pending-usage-settlements.d.ts +57 -0
  117. package/dist/model-calls/service.d.ts +36 -0
  118. package/dist/model-catalog.d.ts +9 -3
  119. package/dist/native-push-dispatch.d.ts +69 -0
  120. package/dist/observability.d.ts +3 -1
  121. package/dist/organization-mcp.d.ts +82 -0
  122. package/dist/prepared-mcp-actor.d.ts +77 -0
  123. package/dist/prepared-mcp-permissions.d.ts +8 -0
  124. package/dist/routes/analytics-consent.d.ts +26 -0
  125. package/dist/routes/api-integrations.d.ts +11 -0
  126. package/dist/routes/api-keys.d.ts +7 -7
  127. package/dist/routes/automations.d.ts +2 -0
  128. package/dist/routes/billing.d.ts +59 -0
  129. package/dist/routes/browser-sessions.d.ts +65 -2
  130. package/dist/routes/chat-completions.d.ts +11 -0
  131. package/dist/routes/claude-core-usage.d.ts +20 -0
  132. package/dist/routes/claude-subscription-accounts.d.ts +3 -0
  133. package/dist/routes/claude-subscription-oauth.d.ts +3 -0
  134. package/dist/routes/client-errors.d.ts +182 -0
  135. package/dist/routes/codex-core.d.ts +401 -0
  136. package/dist/routes/codex.d.ts +64 -2
  137. package/dist/routes/computer-sessions.d.ts +6 -1
  138. package/dist/routes/desktop-distribution.d.ts +32 -0
  139. package/dist/routes/github-browser-fonts.gen.d.ts +2 -0
  140. package/dist/routes/github-browser-pages.d.ts +27 -0
  141. package/dist/routes/inbox.d.ts +13 -0
  142. package/dist/routes/insights-response-cache.d.ts +10 -0
  143. package/dist/routes/insights-usage.d.ts +12 -0
  144. package/dist/routes/insights.d.ts +28 -0
  145. package/dist/routes/interaction-placement-message.d.ts +7 -0
  146. package/dist/routes/managed-sign-in-methods.d.ts +6 -0
  147. package/dist/routes/model-account-choice.d.ts +13 -0
  148. package/dist/routes/model-account-settings.d.ts +19 -0
  149. package/dist/routes/native-app-auth.d.ts +12 -0
  150. package/dist/routes/organization-integration-policy.d.ts +8 -0
  151. package/dist/routes/organization-integrations.d.ts +6 -0
  152. package/dist/routes/organization-mcp-connections.d.ts +3 -0
  153. package/dist/routes/organization-model-defaults.d.ts +9 -0
  154. package/dist/routes/organization-service-accounts.d.ts +3 -0
  155. package/dist/routes/personal-api-keys.d.ts +3 -0
  156. package/dist/routes/session-admin-access.d.ts +3 -0
  157. package/dist/routes/session-artifact-associations.d.ts +4 -0
  158. package/dist/routes/session-history-imports.d.ts +20 -0
  159. package/dist/routes/sessions.d.ts +38 -2
  160. package/dist/routes/site-data.d.ts +18 -0
  161. package/dist/routes/site-publishing.d.ts +5 -0
  162. package/dist/routes/site-runtime.d.ts +31 -0
  163. package/dist/routes/site-sql.d.ts +16 -0
  164. package/dist/routes/slack-bot-access.d.ts +3 -0
  165. package/dist/routes/subscription-account-pools.d.ts +21 -0
  166. package/dist/routes/subscription-core-http.d.ts +5 -0
  167. package/dist/routes/subscription-pool-access.d.ts +37 -0
  168. package/dist/routes/subscription-pool-core.d.ts +153 -0
  169. package/dist/routes/supergrok.d.ts +6 -6
  170. package/dist/routes/usage-allowances.d.ts +12 -0
  171. package/dist/routes/variable-sets-removed.d.ts +2 -0
  172. package/dist/routes/vault-providers.d.ts +34 -0
  173. package/dist/routes/vaults.d.ts +3 -0
  174. package/dist/routes/workspace-integrations.d.ts +35 -0
  175. package/dist/routes/workspace-model-providers.d.ts +4 -0
  176. package/dist/routes/workspaces.d.ts +11 -2
  177. package/dist/sandbox/agent-update.d.ts +64 -0
  178. package/dist/sandbox/auth-callout.d.ts +8 -0
  179. package/dist/sandbox/channel-a.d.ts +15 -1
  180. package/dist/sandbox/enrollment.d.ts +26 -4
  181. package/dist/sandbox/machine-audience.d.ts +29 -0
  182. package/dist/sandbox/machines.d.ts +15 -2
  183. package/dist/sandbox/metrics-ingestion.d.ts +6 -2
  184. package/dist/sandbox/screen-control.d.ts +85 -0
  185. package/dist/sandbox/viewer.d.ts +5 -1
  186. package/dist/sandbox-file-artifacts.d.ts +17 -1
  187. package/dist/scheduled-task-blockers.d.ts +39 -0
  188. package/dist/scheduled-task-deletion.d.ts +3 -4
  189. package/dist/scheduled-task-lifecycle.d.ts +14 -0
  190. package/dist/secret-answers.d.ts +40 -0
  191. package/dist/session-create-observability.d.ts +5 -0
  192. package/dist/session-event-view-36GO3IL7.js +528 -0
  193. package/dist/session-event-view-36GO3IL7.js.map +1 -0
  194. package/dist/signed-state-secret.d.ts +18 -0
  195. package/dist/site-agent-guard.d.ts +53 -0
  196. package/dist/site-data/operations.d.ts +103 -0
  197. package/dist/site-data/service.d.ts +203 -0
  198. package/dist/site-frame-guard.d.ts +32 -0
  199. package/dist/site-publishing/agent-data-tools.d.ts +84 -0
  200. package/dist/site-publishing/agent-model.d.ts +58 -0
  201. package/dist/site-publishing/agent-session.d.ts +16 -0
  202. package/dist/site-publishing/content-scanner.d.ts +31 -0
  203. package/dist/site-publishing/edge-sync.d.ts +48 -0
  204. package/dist/site-publishing/gateway-signature.d.ts +56 -0
  205. package/dist/site-publishing/limits.d.ts +63 -0
  206. package/dist/site-publishing/page-tool-authority.d.ts +30 -0
  207. package/dist/site-publishing/platform-usage.d.ts +113 -0
  208. package/dist/site-publishing/platform.d.ts +21 -0
  209. package/dist/site-publishing/readiness.d.ts +62 -0
  210. package/dist/site-publishing/runtime.d.ts +179 -0
  211. package/dist/site-publishing/service.d.ts +264 -0
  212. package/dist/site-publishing/session-events.d.ts +22 -0
  213. package/dist/site-publishing/session-views.d.ts +36 -0
  214. package/dist/site-publishing/viewer.d.ts +40 -0
  215. package/dist/site-session-origin.d.ts +31 -6
  216. package/dist/site-sql/format.d.ts +47 -0
  217. package/dist/site-sql/gateway.d.ts +50 -0
  218. package/dist/site-sql/service.d.ts +213 -0
  219. package/dist/temporal-schedule-sync.d.ts +26 -0
  220. package/dist/transcription/billing-refusal.d.ts +8 -0
  221. package/dist/transcription/normalize.d.ts +23 -0
  222. package/dist/transcription/providers/azure-mai.d.ts +12 -0
  223. package/dist/transcription/providers/azure-openai.d.ts +4 -0
  224. package/dist/transcription/providers/openai.d.ts +16 -0
  225. package/dist/transcription/providers/subscription-core.d.ts +39 -0
  226. package/dist/transcription/service.d.ts +10 -1
  227. package/dist/transcription/wav-duration.d.ts +8 -0
  228. package/dist/workspace-tool-gateway-attestations.d.ts +37 -0
  229. package/dist/workspace-tool-gateway-observability.d.ts +16 -0
  230. package/dist/workspace-tool-gateway-secrets.d.ts +27 -0
  231. package/dist/workspace-tool-gateway.d.ts +102 -6
  232. package/dist/workspace-tool-target-fence.d.ts +116 -0
  233. package/dist/workspace-tool-target.d.ts +225 -0
  234. package/dist/workspace-webhook-dispatch.d.ts +26 -0
  235. package/dist/xai-subscription-core.d.ts +60 -0
  236. package/package.json +25 -20
  237. package/src/access-grant-rls.ts +64 -14
  238. package/src/admission-trace.ts +14 -0
  239. package/src/app.ts +1684 -252
  240. package/src/artifact-catalog-cursor.ts +1 -0
  241. package/src/attached-browser-viewer.ts +26 -0
  242. package/src/auth/managed-auth-attempt-context.ts +9 -0
  243. package/src/auth/managed-auth-rate-limits.ts +252 -0
  244. package/src/auth/managed-auth-session-adapter.ts +64 -29
  245. package/src/auth/managed-auth.ts +303 -35
  246. package/src/auth/managed-email.ts +18 -1
  247. package/src/auth/managed-sign-in-notifications.ts +81 -0
  248. package/src/auth/new-signups-gate.ts +49 -0
  249. package/src/auth/organization-user-setup.ts +3 -3
  250. package/src/auth/signup-funnel-metrics.ts +164 -0
  251. package/src/azure-live.ts +103 -0
  252. package/src/browser-action-fences.ts +45 -0
  253. package/src/browser-controller-authority.ts +1 -186
  254. package/src/browser-ephemeral.ts +39 -0
  255. package/src/browser-lost-runtime-end.ts +71 -0
  256. package/src/browser-state-authority.ts +1 -236
  257. package/src/claude-default-models.ts +37 -0
  258. package/src/claude-subscription-core.ts +154 -0
  259. package/src/claude-subscription-oauth.ts +352 -0
  260. package/src/claude-subscription-profile.ts +73 -0
  261. package/src/claude-subscription-usage.ts +51 -0
  262. package/src/claude-workspace-connection.ts +82 -0
  263. package/src/codemode.ts +236 -39
  264. package/src/codex-realtime.ts +181 -53
  265. package/src/codex-session-pointers.ts +80 -0
  266. package/src/company-brain-okf.ts +1 -1
  267. package/src/connected-machine-computer-access.ts +2 -1
  268. package/src/connection-ownership.ts +19 -13
  269. package/src/controller-data-plane.ts +34 -0
  270. package/src/credit-promotion-snapshot.ts +36 -0
  271. package/src/editable-artifact-production.ts +10 -0
  272. package/src/editable-artifact-source-session.ts +123 -0
  273. package/src/editable-artifact-websocket.ts +16 -3
  274. package/src/fatal-process-boundary.ts +67 -3
  275. package/src/gateway-realtime.ts +12 -8
  276. package/src/github-access.ts +31 -5
  277. package/src/github-browser-flow.ts +1 -1
  278. package/src/http/acting-person.ts +105 -0
  279. package/src/http/api-contract-compat.ts +49 -0
  280. package/src/http/api-error.ts +210 -2
  281. package/src/http/auth.ts +68 -4
  282. package/src/http/cors.ts +37 -5
  283. package/src/http/deprecation.ts +118 -0
  284. package/src/http/keyed-admission.ts +29 -0
  285. package/src/http/local-browser-boundary.ts +343 -0
  286. package/src/http/metrics-listener.ts +57 -0
  287. package/src/http/path-identifier.ts +54 -0
  288. package/src/http/registered-route-label.ts +34 -0
  289. package/src/http/rejection-telemetry.ts +120 -0
  290. package/src/http/request-body.ts +127 -0
  291. package/src/http/request-source.ts +154 -13
  292. package/src/http/sse.ts +88 -28
  293. package/src/http/unmatched-route.ts +60 -0
  294. package/src/http/user-content.ts +158 -0
  295. package/src/http/variable-sets-removed.ts +122 -0
  296. package/src/http/websocket-upgrade-dispatch.ts +29 -0
  297. package/src/index.ts +107 -57
  298. package/src/integration-connect-metrics.ts +228 -0
  299. package/src/integration-endpoint-test.ts +291 -0
  300. package/src/integrations/api-integrations.ts +81 -7
  301. package/src/integrations/atlassian.ts +202 -83
  302. package/src/integrations/fiken.ts +66 -38
  303. package/src/integrations/github-app-connect.ts +64 -18
  304. package/src/integrations/github-lens-connect.ts +8 -29
  305. package/src/integrations/google-drive.ts +448 -152
  306. package/src/integrations/integration-commit-authority.ts +69 -0
  307. package/src/integrations/marketplace-plugin.ts +1 -1
  308. package/src/integrations/oauth-client.ts +733 -193
  309. package/src/integrations/oauth-profiles.ts +116 -45
  310. package/src/integrations/onepassword-vault-provider.ts +16 -0
  311. package/src/integrations/personal-github-repositories.ts +1 -1
  312. package/src/integrations/personal-github.ts +61 -35
  313. package/src/integrations/provider-oauth.ts +67 -85
  314. package/src/integrations/slack-app-home.ts +5 -5
  315. package/src/integrations/slack-bot.ts +893 -201
  316. package/src/integrations/slack-file-upload-flow.ts +147 -0
  317. package/src/integrations/slack-install.ts +7 -7
  318. package/src/integrations/slack-interactions.ts +1589 -299
  319. package/src/integrations/slack-mrkdwn.ts +193 -0
  320. package/src/integrations/slack-routing.ts +2 -2
  321. package/src/integrations/slack-session-defaults.ts +224 -0
  322. package/src/integrations/slack-task-file-upload.ts +323 -0
  323. package/src/integrations/slack-tool-review.ts +65 -0
  324. package/src/integrations/social-oauth.ts +90 -58
  325. package/src/interaction-agent-access.ts +40 -20
  326. package/src/interaction-agent-version.ts +85 -0
  327. package/src/interaction-holder-heartbeat.ts +24 -8
  328. package/src/interaction-metrics.ts +164 -2
  329. package/src/interaction-route-stages.ts +94 -0
  330. package/src/interaction-secrets-vault.ts +213 -0
  331. package/src/interaction-secrets.ts +426 -0
  332. package/src/mcp/action-catalog-types.ts +14 -0
  333. package/src/mcp/action-catalog.gen.ts +8014 -0
  334. package/src/mcp/action-descriptions.ts +299 -0
  335. package/src/mcp/action-schema-imports.gen.ts +304 -0
  336. package/src/mcp/capability-account-readiness.ts +17 -0
  337. package/src/mcp/child-read-evidence.ts +19 -0
  338. package/src/mcp/company-profile-agent-admin.ts +13 -0
  339. package/src/mcp/contract-input.ts +165 -0
  340. package/src/mcp/documents.ts +9 -2
  341. package/src/mcp/editable-artifacts.ts +29 -11
  342. package/src/mcp/files.ts +5 -1
  343. package/src/mcp/first-party-tool-permissions.ts +7 -285
  344. package/src/mcp/knowledge-entries.ts +108 -34
  345. package/src/mcp/notification-tools.ts +480 -0
  346. package/src/mcp/orchestration-failure-diagnostic.ts +43 -0
  347. package/src/mcp/scheduled-task-input.ts +89 -0
  348. package/src/mcp/scheduled-task-view.ts +63 -3
  349. package/src/mcp/secret-tools.ts +185 -0
  350. package/src/mcp/server.ts +2213 -1116
  351. package/src/mcp/session-admin-access-tools.ts +163 -0
  352. package/src/mcp/session-event-errors.ts +123 -0
  353. package/src/mcp/session-event-view.ts +395 -13
  354. package/src/mcp/session-view.ts +3 -0
  355. package/src/mcp/session-wait.ts +67 -7
  356. package/src/mcp/site-agent-clamp-tools.ts +140 -0
  357. package/src/mcp/site-data-tools.ts +339 -0
  358. package/src/mcp/site-publication-tools.ts +201 -0
  359. package/src/mcp/site-sql-tools.ts +427 -0
  360. package/src/mcp-oauth.ts +635 -45
  361. package/src/model-calls/chat-completions.ts +352 -0
  362. package/src/model-calls/pending-usage-settlements.ts +125 -0
  363. package/src/model-calls/service.ts +694 -0
  364. package/src/model-catalog.ts +68 -17
  365. package/src/native-push-dispatch.ts +457 -0
  366. package/src/observability.ts +15 -1
  367. package/src/organization-mcp.ts +485 -0
  368. package/src/prepared-mcp-actor.ts +154 -0
  369. package/src/prepared-mcp-permissions.ts +38 -0
  370. package/src/routes/analytics-consent.ts +123 -0
  371. package/src/routes/api-integrations.ts +268 -9
  372. package/src/routes/api-keys.ts +164 -25
  373. package/src/routes/artifact-catalog.ts +111 -0
  374. package/src/routes/automations.ts +163 -90
  375. package/src/routes/billing.ts +535 -59
  376. package/src/routes/browser-sessions.ts +966 -342
  377. package/src/routes/canonical-human-identities.ts +61 -0
  378. package/src/routes/capabilities.ts +112 -5
  379. package/src/routes/channels.ts +4 -3
  380. package/src/routes/chat-completions.ts +235 -0
  381. package/src/routes/claude-core-usage.ts +147 -0
  382. package/src/routes/claude-subscription-accounts.ts +233 -0
  383. package/src/routes/claude-subscription-oauth.ts +175 -0
  384. package/src/routes/client-errors.ts +394 -0
  385. package/src/routes/codex-core.ts +687 -0
  386. package/src/routes/codex.ts +1277 -1119
  387. package/src/routes/company-brain.ts +4 -0
  388. package/src/routes/computer-sessions.ts +319 -85
  389. package/src/routes/connect.ts +277 -175
  390. package/src/routes/connections.ts +690 -363
  391. package/src/routes/desktop-distribution.ts +217 -0
  392. package/src/routes/documents.ts +13 -7
  393. package/src/routes/editable-artifacts.ts +74 -4
  394. package/src/routes/enrollments.ts +110 -16
  395. package/src/routes/external-identity-links.ts +13 -12
  396. package/src/routes/files.ts +41 -16
  397. package/src/routes/github-browser-fonts.gen.ts +12 -0
  398. package/src/routes/github-browser-pages.ts +258 -0
  399. package/src/routes/github.ts +200 -75
  400. package/src/routes/inbox.ts +353 -0
  401. package/src/routes/insights-response-cache.ts +83 -0
  402. package/src/routes/insights-usage.ts +345 -0
  403. package/src/routes/insights.ts +103 -8
  404. package/src/routes/install.ts +51 -10
  405. package/src/routes/integration-facets.ts +70 -28
  406. package/src/routes/interaction-placement-message.ts +12 -0
  407. package/src/routes/knowledge.ts +136 -27
  408. package/src/routes/machines.ts +189 -135
  409. package/src/routes/managed-auth-session-sets.ts +43 -10
  410. package/src/routes/managed-onboarding.ts +39 -19
  411. package/src/routes/managed-sign-in-methods.ts +433 -0
  412. package/src/routes/memory-slack-publications.ts +3 -2
  413. package/src/routes/model-account-choice.ts +194 -0
  414. package/src/routes/model-account-settings.ts +235 -0
  415. package/src/routes/model-connection-access.ts +210 -48
  416. package/src/routes/native-app-auth.ts +296 -0
  417. package/src/routes/organization-integration-policy.ts +116 -0
  418. package/src/routes/organization-integrations.ts +326 -0
  419. package/src/routes/organization-mcp-connections.ts +183 -0
  420. package/src/routes/organization-memberships.ts +205 -19
  421. package/src/routes/organization-model-defaults.ts +110 -0
  422. package/src/routes/organization-model-providers.ts +74 -3
  423. package/src/routes/organization-service-accounts.ts +128 -0
  424. package/src/routes/organization-sessions.ts +21 -5
  425. package/src/routes/personal-api-keys.ts +184 -0
  426. package/src/routes/personal-github-git-broker.ts +2 -2
  427. package/src/routes/personal-github.ts +9 -4
  428. package/src/routes/plugins.ts +46 -7
  429. package/src/routes/pr-review-github.ts +105 -118
  430. package/src/routes/pr-review.ts +88 -82
  431. package/src/routes/preference-registry.ts +18 -0
  432. package/src/routes/rigs.ts +48 -13
  433. package/src/routes/scheduled-tasks.ts +347 -27
  434. package/src/routes/session-admin-access.ts +271 -0
  435. package/src/routes/session-artifact-associations.ts +164 -0
  436. package/src/routes/session-history-imports.ts +305 -0
  437. package/src/routes/sessions.ts +1682 -363
  438. package/src/routes/site-data.ts +340 -0
  439. package/src/routes/site-publishing.ts +584 -0
  440. package/src/routes/site-runtime.ts +1944 -0
  441. package/src/routes/site-sql.ts +263 -0
  442. package/src/routes/skill-content.ts +72 -7
  443. package/src/routes/skills.ts +5 -4
  444. package/src/routes/slack-bot-access.ts +108 -0
  445. package/src/routes/social.ts +27 -15
  446. package/src/routes/subscription-account-pools.ts +209 -0
  447. package/src/routes/subscription-core-http.ts +18 -0
  448. package/src/routes/subscription-pool-access.ts +180 -0
  449. package/src/routes/subscription-pool-core.ts +552 -0
  450. package/src/routes/supergrok.ts +174 -655
  451. package/src/routes/transcription-recordings.ts +71 -4
  452. package/src/routes/transcriptions.ts +26 -2
  453. package/src/routes/usage-allowances.ts +381 -0
  454. package/src/routes/variable-sets-removed.ts +19 -0
  455. package/src/routes/vault-providers.ts +782 -0
  456. package/src/routes/vaults.ts +421 -0
  457. package/src/routes/video-generation.ts +30 -12
  458. package/src/routes/workspace-artifacts.ts +165 -53
  459. package/src/routes/workspace-integrations.ts +576 -0
  460. package/src/routes/workspace-model-providers.ts +148 -0
  461. package/src/routes/workspace-state.ts +4 -0
  462. package/src/routes/workspaces.ts +625 -117
  463. package/src/sandbox/agent-update.ts +315 -0
  464. package/src/sandbox/auth-callout.ts +104 -21
  465. package/src/sandbox/channel-a.ts +150 -49
  466. package/src/sandbox/enrollment.ts +131 -22
  467. package/src/sandbox/machine-audience.ts +192 -0
  468. package/src/sandbox/machines.ts +75 -31
  469. package/src/sandbox/metrics-ingestion.ts +133 -2
  470. package/src/sandbox/rematerialize.ts +51 -21
  471. package/src/sandbox/screen-control.ts +289 -0
  472. package/src/sandbox/viewer.ts +33 -58
  473. package/src/sandbox-file-artifacts.ts +53 -27
  474. package/src/scheduled-task-blockers.ts +293 -0
  475. package/src/scheduled-task-deletion.ts +13 -3
  476. package/src/scheduled-task-lifecycle.ts +84 -0
  477. package/src/secret-answers.ts +119 -0
  478. package/src/session-create-observability.ts +46 -0
  479. package/src/session-realtime-context.ts +26 -4
  480. package/src/signed-state-secret.ts +58 -0
  481. package/src/site-agent-guard.ts +261 -0
  482. package/src/site-data/operations.ts +777 -0
  483. package/src/site-data/service.ts +1043 -0
  484. package/src/site-frame-guard.ts +107 -0
  485. package/src/site-publishing/agent-data-tools.ts +250 -0
  486. package/src/site-publishing/agent-model.ts +127 -0
  487. package/src/site-publishing/agent-session.ts +148 -0
  488. package/src/site-publishing/content-scanner.ts +93 -0
  489. package/src/site-publishing/edge-sync.ts +148 -0
  490. package/src/site-publishing/gateway-signature.ts +198 -0
  491. package/src/site-publishing/limits.ts +315 -0
  492. package/src/site-publishing/page-tool-authority.ts +111 -0
  493. package/src/site-publishing/platform-usage.ts +479 -0
  494. package/src/site-publishing/platform.ts +98 -0
  495. package/src/site-publishing/readiness.ts +330 -0
  496. package/src/site-publishing/runtime.ts +831 -0
  497. package/src/site-publishing/service.ts +1282 -0
  498. package/src/site-publishing/session-events.ts +262 -0
  499. package/src/site-publishing/session-views.ts +262 -0
  500. package/src/site-publishing/viewer.ts +75 -0
  501. package/src/site-session-origin.ts +91 -6
  502. package/src/site-sql/format.ts +134 -0
  503. package/src/site-sql/gateway.ts +168 -0
  504. package/src/site-sql/service.ts +1636 -0
  505. package/src/site-uploads.ts +27 -5
  506. package/src/temporal-schedule-sync.ts +74 -0
  507. package/src/transcription/billing-refusal.ts +14 -0
  508. package/src/transcription/normalize.ts +77 -0
  509. package/src/transcription/providers/azure-mai.ts +98 -0
  510. package/src/transcription/providers/azure-openai.ts +16 -7
  511. package/src/transcription/providers/codex-subscription.ts +190 -67
  512. package/src/transcription/providers/openai.ts +79 -5
  513. package/src/transcription/providers/subscription-core.ts +125 -0
  514. package/src/transcription/providers/xai-subscription.ts +81 -83
  515. package/src/transcription/segmenter.ts +7 -1
  516. package/src/transcription/service.ts +239 -10
  517. package/src/transcription/wav-duration.ts +39 -0
  518. package/src/workspace-deletion.ts +25 -1
  519. package/src/workspace-tool-gateway-attestations.ts +133 -0
  520. package/src/workspace-tool-gateway-observability.ts +64 -0
  521. package/src/workspace-tool-gateway-secrets.ts +104 -0
  522. package/src/workspace-tool-gateway.ts +921 -103
  523. package/src/workspace-tool-target-fence.ts +380 -0
  524. package/src/workspace-tool-target.ts +554 -0
  525. package/src/workspace-webhook-dispatch.ts +225 -0
  526. package/src/xai-realtime.ts +85 -81
  527. package/src/xai-subscription-core.ts +172 -0
  528. package/dist/browser-auth-broker.d.ts +0 -20
  529. package/dist/chunk-AJ62T5QN.js.map +0 -1
  530. package/dist/connection-authority-owner.d.ts +0 -59
  531. package/dist/routes/connection-authorities.d.ts +0 -3
  532. package/dist/routes/environments.d.ts +0 -5
  533. package/dist/routes/host-mcp-bindings.d.ts +0 -5
  534. package/dist/routes/host-mcp-resolvers.d.ts +0 -3
  535. package/dist/routes/packs.d.ts +0 -3
  536. package/dist/session-event-view-MU3HMNV3.js +0 -264
  537. package/dist/session-event-view-MU3HMNV3.js.map +0 -1
  538. package/dist/xai-subscription-auth.d.ts +0 -23
  539. package/src/browser-auth-broker.ts +0 -155
  540. package/src/connection-authority-owner.ts +0 -61
  541. package/src/routes/connection-authorities.ts +0 -139
  542. package/src/routes/environments.ts +0 -388
  543. package/src/routes/host-mcp-bindings.ts +0 -122
  544. package/src/routes/host-mcp-resolvers.ts +0 -41
  545. package/src/routes/packs.ts +0 -784
  546. package/src/xai-subscription-auth.ts +0 -139
package/src/app.ts CHANGED
@@ -1,33 +1,92 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+
3
+ import { registerClaudeSubscriptionAccountRoutes } from "./routes/claude-subscription-accounts";
4
+ import { isPreparedMcpConnectPath } from "./prepared-mcp-permissions";
5
+ import { registerWorkspaceModelProviderRoutes } from "./routes/workspace-model-providers";
6
+ import { registerClaudeSubscriptionOAuthRoutes } from "./routes/claude-subscription-oauth";
1
7
  import { registerConnectCallbackReturns } from "./integrations/connect-callback-return";
2
8
  import { registerFeedbackRoutes } from "./routes/feedback";
9
+ import { registerInboxRoutes } from "./routes/inbox";
10
+ import { registerWorkspaceIntegrationRoutes } from "./routes/workspace-integrations";
11
+ import { registerOrganizationIntegrationRoutes } from "./routes/organization-integrations";
12
+ import {
13
+ resolveWorkspaceToolTarget,
14
+ invokeWorkspaceToolTarget,
15
+ approveWorkspaceToolTarget,
16
+ resolveWorkspaceToolManifest,
17
+ } from "./workspace-tool-target";
18
+ import type { TargetAuthorityFence } from "./workspace-tool-target-fence";
19
+ import { recordWorkspaceToolTargetOperation } from "./workspace-tool-gateway-observability";
20
+ import {
21
+ CLIENT_ERRORS_PATH,
22
+ isClientErrorReportRequest,
23
+ registerClientErrorRoutes,
24
+ } from "./routes/client-errors";
25
+ import {
26
+ ANALYTICS_CONSENT_PATH,
27
+ isAnalyticsConsentReportRequest,
28
+ registerAnalyticsConsentRoutes,
29
+ } from "./routes/analytics-consent";
3
30
  import { codemodeSessionRequest } from "./codemode";
4
- import { SiteSessionPathError } from "@opengeni/contracts";
31
+ import { siteClampedAgentGuard } from "./site-agent-guard";
32
+ import { siteFrameGuard } from "./site-frame-guard";
33
+ import { SiteDataUnsupportedError } from "@opengeni/contracts/site-session-http";
34
+ import {
35
+ SiteSessionPathError,
36
+ OrganizationIntegrationDeniedError,
37
+ normalizeOrganizationAccessPolicy,
38
+ } from "@opengeni/contracts";
39
+ import {
40
+ httpRejectionFactsFromEnvelope,
41
+ httpRejectionFactsFromError,
42
+ readRejectionEnvelope,
43
+ type HttpRejectionFacts,
44
+ } from "./http/rejection-telemetry";
45
+ import { observeInteractionRouteOutcome } from "./interaction-metrics";
46
+ import {
47
+ isOAuthCallbackRoute,
48
+ observeConnectAttemptTransition,
49
+ observeOAuthCallback,
50
+ } from "./integration-connect-metrics";
5
51
  import { registerModelConnectionAccessRoutes } from "./routes/model-connection-access";
6
52
  import {
7
- canonicalizeConfiguredModelId,
8
- configuredAllowedModels,
53
+ codeSearchDeploymentPolicy,
54
+ agentConfigDeploymentPolicy,
55
+ canonicalPublicOrigin,
9
56
  configuredAllowedReasoningEfforts,
10
- configuredModels,
57
+ environmentsEncryptionKeyBytes,
11
58
  resolveFirstPartyMcpToolPolicy,
12
- resolveVoiceInputProviderRegistry,
13
- withCodexCatalogProvider,
14
- withXaiSubscriptionCatalogProvider,
59
+ UnsupportedLatencyModeError,
60
+ voiceInputPricingIssues,
61
+ type Settings,
62
+ type FirstPartyMcpToolPolicySettings,
15
63
  } from "@opengeni/config";
16
64
  import {
65
+ AGENT_CAPABILITY_IDS,
17
66
  ClientConfig,
18
67
  CodemodeCallRequest,
19
68
  ErrorEnvelope,
20
69
  OPENGENI_API_CONTRACT_HEADER,
21
70
  OPENGENI_API_CONTRACT_REVISION,
22
71
  OPENGENI_CORRELATION_HEADER,
72
+ resolveAgentToolFamilies,
23
73
  resolveWorkspaceMemoryEnabled,
24
74
  resolveWorkspaceMemoryPromptMode,
25
75
  VOICE_INPUT_ACCEPTED_MIME_TYPES,
76
+ WorkspaceVoiceInputSettings,
26
77
  TRANSCRIPTION_RECORDING_PROVIDER_SEGMENT_SECONDS,
27
78
  ToolGatewayApprovalRequest,
28
79
  ToolGatewayCallRequest,
80
+ ToolGatewayResolveRequest,
81
+ ToolGatewayInvokeRequest,
82
+ ToolGatewayTargetApprovalRequest,
83
+ ToolGatewayManifestRequest,
29
84
  type AccessGrant,
85
+ type ClientAgentConfig,
86
+ type ClientSitesConfig,
30
87
  type ErrorCode,
88
+ type FirstPartyMcpToolName,
89
+ type Session,
31
90
  } from "@opengeni/contracts";
32
91
  import {
33
92
  createDocumentServices,
@@ -39,53 +98,106 @@ import {
39
98
  CodemodeOperationConflictError,
40
99
  CodemodeOperationNotExecutableError,
41
100
  CodemodePayloadTooLargeError,
101
+ CodemodeOperationLimitError,
42
102
  CodemodeToolApprovalRequiredError,
43
103
  CodemodeToolNotInCatalogError,
44
104
  ConnectAttemptConflictError,
45
105
  ConnectAttemptNotFoundError,
46
106
  configureChildLifecycleNotices,
107
+ configureCodeSearchDeploymentPolicy,
47
108
  configureWorkspaceControlRequestLockTimeoutMs,
109
+ setConnectAttemptTransitionObserver,
48
110
  dbSql,
49
111
  getManagedAuthSessionSetSnapshot,
50
112
  getWorkspace,
51
113
  reapManagedAuthIsolatedSessions,
52
114
  reapExpiredManagedAuthSessionSets,
53
115
  resolveSessionMemoryAgentScope,
116
+ resolveSessionAdminAuthority,
54
117
  rlsContextForWorkspace,
55
118
  } from "@opengeni/db";
56
119
  import { requireSessionEventDurableFanoutCapability } from "@opengeni/events";
57
120
  import { githubAppBotIdentityWarnings } from "@opengeni/github";
58
- import { createObservability } from "@opengeni/observability";
121
+ import {
122
+ createObservability,
123
+ errorCauseAttributes,
124
+ failureDiagnostic,
125
+ withTraceContext,
126
+ withMcpTelemetry,
127
+ } from "@opengeni/observability";
59
128
  import { createObjectStorage } from "@opengeni/storage";
60
129
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
130
+ import { SUPPORTED_PROTOCOL_VERSIONS } from "@modelcontextprotocol/sdk/types.js";
61
131
  import { handleMcpRequestWithClientAbort } from "./mcp/request-abort";
62
- import { Hono } from "hono";
132
+ import { Hono, type Context } from "hono";
63
133
  import { bodyLimit } from "hono/body-limit";
64
134
  import { compress } from "hono/compress";
65
135
  import { cors } from "hono/cors";
66
136
  import { getCookie } from "hono/cookie";
67
137
  import { HTTPException } from "hono/http-exception";
68
- import { ApiHttpError, workspaceControlBusyHttpError } from "./http/api-error";
138
+ import {
139
+ ApiHttpError,
140
+ agentConfigHttpError,
141
+ allowanceExhaustedHttpError,
142
+ DATABASE_CONTENTION_ERROR_DETAIL_CODE,
143
+ DATABASE_UNAVAILABLE_ERROR_DETAIL_CODE,
144
+ databaseContentionHttpError,
145
+ databaseUnavailableHttpError,
146
+ modelUnavailableHttpError,
147
+ scheduledTaskTargetAccessHttpError,
148
+ sessionUsageLimitHttpError,
149
+ workspaceControlBusyHttpError,
150
+ } from "./http/api-error";
151
+ import {
152
+ isRequestBodyValidationError,
153
+ requestBodyValidationHttpError,
154
+ tagRequestJsonParseErrors,
155
+ } from "./http/request-body";
156
+ import { invalidPathIdentifierHttpError } from "./http/path-identifier";
157
+ import { replaceTrustedClientAddressHeader } from "./http/request-source";
158
+ import { unmatchedRoute } from "./http/unmatched-route";
69
159
  import type { ContentfulStatusCode } from "hono/utils/http-status";
160
+ import { bearerApiContractHeaderCompatibility } from "./http/api-contract-compat";
161
+ import { deprecationHeadersMiddleware } from "./http/deprecation";
162
+ import {
163
+ boundedRegisteredRouteLabel,
164
+ registeredHandlerRoutePath,
165
+ } from "./http/registered-route-label";
70
166
  import type { ApiRouteDeps, AppDependencies } from "@opengeni/core";
167
+ import { sessionUsageLimitsProducerEnabled } from "@opengeni/core";
71
168
  import {
72
169
  CodexCompactionV2ProviderLockedError,
73
170
  ManagedAuthActorLeaseOutcomeUnknownError,
74
171
  hasPermission,
75
172
  requireAccessGrant,
76
173
  requireAccessGrantAuthorization,
174
+ requireFreshAccessGrant,
175
+ accountScopedApiKeyWorkspaceAuthority,
176
+ requireAccessContext,
77
177
  requirePermission,
78
178
  releaseManagedAuthRequestActorLease,
79
179
  resolveCatalogSettings,
80
180
  resolveWorkspaceCatalogSettings,
181
+ resolveCallerWorkspaceModelSelections,
182
+ resolveDefaultSessionModelForSelections,
183
+ selectDefaultSessionModel,
184
+ isWorkspaceModelAdmissible,
185
+ resolveWorkspaceModelSelection,
81
186
  validateManagedAuthRequestActorLease,
82
187
  requireSessionAuthorization,
83
188
  SessionAuthorizationDeniedError,
189
+ withSessionAuthorizationReadReuse,
84
190
  SessionAuthorizationUnavailableError,
191
+ createUserPresenceRecorder,
192
+ registerProductUsageMetricBaselines,
193
+ stampPersonalApiKeyRequest,
194
+ verifiedDelegatedHumanAuthorizationForRequest,
85
195
  } from "@opengeni/core";
196
+ import { createManagedAuthNewSignupsGate } from "./auth/new-signups-gate";
86
197
  import {
87
198
  createManagedAuth,
88
199
  isolatedManagedAuthOAuthCallbackRequest,
200
+ managedAuthNewSignupsPausedResponse,
89
201
  managedAuthOAuthReturnMatches,
90
202
  resolveManagedAuthOAuthAttempt,
91
203
  } from "./auth/managed-auth";
@@ -106,6 +218,7 @@ import {
106
218
  runManagedAuthProvider,
107
219
  } from "./auth/managed-auth-attempt-context";
108
220
  import { createManagedEmailTransport } from "./auth/managed-email";
221
+ import { startManagedSignInNotificationDelivery } from "./auth/managed-sign-in-notifications";
109
222
  import {
110
223
  assertManagedEmailTransportMetadata,
111
224
  assertOrganizationUserSetupQueryTransportConfigured,
@@ -115,19 +228,26 @@ import { buildOpenGeniMcpServer } from "./mcp/server";
115
228
  import {
116
229
  buildWorkspaceToolGatewayMcpServer,
117
230
  approveWorkspaceToolGatewayCall,
118
- callWorkspaceToolGateway,
231
+ callWorkspaceToolGatewayForCaller,
119
232
  grantUsesAttemptScopedMcp,
233
+ prepareAttestedWorkspaceToolGateway,
120
234
  prepareMcpOAuthWorkspaceToolGateway,
121
235
  prepareWorkspaceToolGateway,
122
236
  workspaceToolGatewayDeclarations,
237
+ workspaceToolGatewayServerTiming,
123
238
  } from "./workspace-tool-gateway";
239
+ import { createWorkspaceToolGatewayCatalogAttestations } from "./workspace-tool-gateway-attestations";
240
+ import { recordWorkspaceToolGatewayPreparation } from "./workspace-tool-gateway-observability";
124
241
  import {
125
242
  isMcpOAuthResourcePath,
126
243
  mcpOAuthAuthenticateHeader,
127
244
  mcpOAuthBearerToken,
245
+ ORGANIZATION_MCP_PATH,
128
246
  registerMcpOAuthRoutes,
129
247
  resolveMcpOAuthRouteAccess,
248
+ resolveOrganizationMcpOAuthAccess,
130
249
  } from "./mcp-oauth";
250
+ import { buildOrganizationMcpServer, type OrganizationMcpCaller } from "./organization-mcp";
131
251
  import {
132
252
  CodemodeAuthorityError,
133
253
  CodemodeCatalogNotReadyError,
@@ -143,42 +263,65 @@ import {
143
263
  BrowserControlTransportError,
144
264
  } from "@opengeni/runtime/sandbox";
145
265
  import { requireAccessKey } from "./http/auth";
266
+ import {
267
+ publicListenerServesMetrics,
268
+ registerPrometheusMetricsRoute,
269
+ } from "./http/metrics-listener";
146
270
  import { allowedCorsOrigin } from "./http/cors";
271
+ import {
272
+ createLocalBrowserBoundary,
273
+ localBrowserRequestHost,
274
+ markLocalInternalDispatch,
275
+ } from "./http/local-browser-boundary";
147
276
  import { withAccessGrantSessionRlsContext } from "./access-grant-rls";
148
277
  import { registerCapabilityRoutes } from "./routes/capabilities";
149
278
  import { registerCatalogAssetRoutes } from "./routes/catalog-assets";
150
279
  import { registerCodexRoutes } from "./routes/codex";
151
280
  import { registerOrganizationModelProviderRoutes } from "./routes/organization-model-providers";
281
+ import { registerOrganizationModelDefaultsRoutes } from "./routes/organization-model-defaults";
282
+ import {
283
+ registerModelAccountSettingsRoutes,
284
+ subscriptionSettingsLockedHttpError,
285
+ } from "./routes/model-account-settings";
286
+ import { registerModelAccountChoiceRoutes } from "./routes/model-account-choice";
287
+ import { registerSessionAdminAccessRoutes } from "./routes/session-admin-access";
288
+ import { registerOrganizationIntegrationPolicyRoutes } from "./routes/organization-integration-policy";
152
289
  import { registerSuperGrokRoutes } from "./routes/supergrok";
153
290
  import { registerConnectionRoutes } from "./routes/connections";
291
+ import { registerVaultProviderRoutes, vaultProviderRegistryFor } from "./routes/vault-providers";
154
292
  import { registerConnectRoutes } from "./routes/connect";
155
- import { registerHostMcpBindingRoutes } from "./routes/host-mcp-bindings";
156
- import { registerHostMcpResolverRoutes } from "./routes/host-mcp-resolvers";
293
+
157
294
  import { registerExternalIdentityLinkRoutes } from "./routes/external-identity-links";
158
295
  import { registerDocumentRoutes } from "./routes/documents";
159
296
  import { registerKnowledgeRoutes } from "./routes/knowledge";
160
297
  import { registerEnrollmentRoutes } from "./routes/enrollments";
161
298
  import { registerMachineRoutes } from "./routes/machines";
162
299
  import { registerMemorySlackPublicationRoutes } from "./routes/memory-slack-publications";
163
- import { registerEnvironmentRoutes } from "./routes/environments";
300
+ import { registerVariableSetsRemovedRoutes } from "./routes/variable-sets-removed";
301
+ import { variableSetsRemovedErrorResponse } from "./http/variable-sets-removed";
302
+ import { registerVaultRoutes } from "./routes/vaults";
164
303
  import { registerFileRoutes } from "./routes/files";
165
304
  import { registerApiKeyRoutes } from "./routes/api-keys";
305
+ import { registerOrganizationMcpConnectionRoutes } from "./routes/organization-mcp-connections";
306
+ import { registerPersonalApiKeyRoutes } from "./routes/personal-api-keys";
307
+ import { registerOrganizationServiceAccountRoutes } from "./routes/organization-service-accounts";
166
308
  import { registerBillingRoutes } from "./routes/billing";
167
309
  import { registerBrowserIdentityRoutes } from "./routes/browser-identities";
168
310
  import { registerBrowserSessionRoutes } from "./routes/browser-sessions";
169
311
  import { registerComputerSessionRoutes } from "./routes/computer-sessions";
312
+ import { createVaultInteractionSecretsPort } from "./interaction-secrets-vault";
170
313
  import { registerGitHubRoutes } from "./routes/github";
171
314
  import { registerPersonalGitHubRoutes } from "./routes/personal-github";
172
315
  import {
173
316
  isPersonalGitHubGitBrokerRequest,
174
317
  registerPersonalGitHubGitBrokerRoutes,
175
318
  } from "./routes/personal-github-git-broker";
319
+ import { registerDesktopDistributionRoutes } from "./routes/desktop-distribution";
176
320
  import { registerInstallRoutes } from "./routes/install";
177
321
  import { registerApiIntegrationRoutes } from "./routes/api-integrations";
178
322
  import { registerIntegrationFacetRoutes } from "./routes/integration-facets";
179
323
  import { registerInteractionResourceRoutes } from "./routes/interaction-resources";
180
324
  import { registerPrReviewRoutes } from "./routes/pr-review";
181
- import { registerPackRoutes } from "./routes/packs";
182
325
  import { registerAutomationRoutes } from "./routes/automations";
183
326
  import { registerPluginRoutes } from "./routes/plugins";
184
327
  import { registerSkillRoutes } from "./routes/skills";
@@ -186,37 +329,58 @@ import { registerChannelRoutes } from "./routes/channels";
186
329
  import { registerRigRoutes } from "./routes/rigs";
187
330
  import { registerScheduledTaskRoutes } from "./routes/scheduled-tasks";
188
331
  import { registerSessionRoutes } from "./routes/sessions";
332
+ import {
333
+ archivedSessionImportErrorResponse,
334
+ isSessionHistoryImportRequest,
335
+ registerSessionHistoryImportRoutes,
336
+ } from "./routes/session-history-imports";
189
337
  import { registerSocialRoutes } from "./routes/social";
190
338
  import { registerWorkspaceRoutes } from "./routes/workspaces";
339
+ import { registerUsageAllowanceRoutes } from "./routes/usage-allowances";
191
340
  import { registerWorkspaceInstructionPolicyRoutes } from "./routes/workspace-instruction-policies";
192
341
  import { registerWorkspaceLearningRoutes } from "./routes/workspace-learning";
193
342
  import { registerCompanyProfileRoutes } from "./routes/company-profile";
194
343
  import { registerCompanyBrainRoutes } from "./routes/company-brain";
195
344
  import { registerSlackTaskPolicyRoutes } from "./routes/slack-task-policy";
345
+ import { registerSlackBotAccessRoutes } from "./routes/slack-bot-access";
196
346
  import { registerWorkspaceStateRoutes } from "./routes/workspace-state";
197
347
  import { registerWorkspaceArtifactRoutes } from "./routes/workspace-artifacts";
348
+ import { isSiteDataFileUploadRequest, registerSiteDataRoutes } from "./routes/site-data";
349
+ import { registerSitePublishingRoutes } from "./routes/site-publishing";
350
+ import { configuredSitesDomain } from "./site-publishing/service";
351
+ import { registerSiteRuntimeRoutes } from "./routes/site-runtime";
198
352
  import { registerArtifactCatalogRoutes } from "./routes/artifact-catalog";
353
+ import { registerSiteSqlRoutes } from "./routes/site-sql";
199
354
  import { registerPreferenceRegistryRoutes } from "./routes/preference-registry";
200
355
  import { registerInsightsRoutes } from "./routes/insights";
356
+ import { registerInsightsUsageRoutes } from "./routes/insights-usage";
201
357
  import { registerTranscriptionRoutes } from "./routes/transcriptions";
358
+ import { registerChatCompletionRoutes } from "./routes/chat-completions";
359
+ import { createModelCallService } from "./model-calls/service";
202
360
  import { registerEditableArtifactRoutes } from "./routes/editable-artifacts";
361
+ import { registerSessionArtifactAssociationRoutes } from "./routes/session-artifact-associations";
203
362
  import { registerVideoGenerationRoutes } from "./routes/video-generation";
204
363
  import { registerCanonicalHumanIdentityRoutes } from "./routes/canonical-human-identities";
205
364
  import { registerOrganizationMembershipRoutes } from "./routes/organization-memberships";
206
365
  import { registerOrganizationSessionRoutes } from "./routes/organization-sessions";
207
366
  import { registerOrganizationRecoveryRoutes } from "./routes/organization-recovery";
208
367
  import { registerManagedOnboardingRoutes } from "./routes/managed-onboarding";
368
+ import {
369
+ registerManagedSignInMethodRoutes,
370
+ handleManagedSignInConnectCallback,
371
+ } from "./routes/managed-sign-in-methods";
372
+ import { registerNativeAppAuthRoutes } from "./routes/native-app-auth";
209
373
  import {
210
374
  registerManagedAuthSessionSetRoutes,
211
375
  requireManagedAuthProviderRouteAllowed,
212
376
  scrubManagedAuthProviderResponse,
213
377
  } from "./routes/managed-auth-session-sets";
214
378
  import { registerUserResourceAuthorityRoutes } from "./routes/user-resource-authorities";
215
- import { registerConnectionAuthorityRoutes } from "./routes/connection-authorities";
216
379
  import { projectClientModel } from "./model-catalog";
217
380
  import { createTranscriptionService } from "./transcription/service";
218
381
  import { createFfmpegTranscriptionSegmenter } from "./transcription/segmenter";
219
382
  import { registerSlackInteractionRoutes } from "./integrations/slack-interactions";
383
+ import { resolveSignedStateSecret } from "./signed-state-secret";
220
384
 
221
385
  export { allowedCorsOrigin } from "./http/cors";
222
386
 
@@ -278,6 +442,25 @@ export async function resolveWorkspaceMcpRouteDeps(
278
442
  return { ...routeDeps, catalogSourceSettings, settings: resolvedCatalog.settings };
279
443
  }
280
444
 
445
+ /**
446
+ * Signed browser state must verify on every API replica; see
447
+ * {@link resolveSignedStateSecret}. A per-process key is announced once so a
448
+ * multi-replica operator knows why such round trips fail.
449
+ */
450
+ function signedStateSecret(
451
+ settings: AppDependencies["settings"],
452
+ observability: Pick<ReturnType<typeof createObservability>, "warn">,
453
+ ): string {
454
+ const resolved = resolveSignedStateSecret(settings);
455
+ if (resolved.source === "process_random" && !["local", "test"].includes(settings.environment)) {
456
+ observability.warn(
457
+ "Signed browser state uses a per-process key; set OPENGENI_GITHUB_APP_MANIFEST_STATE_SECRET or OPENGENI_INTEGRATIONS_STATE_SECRET before running more than one API replica",
458
+ { source: resolved.source },
459
+ );
460
+ }
461
+ return resolved.secret;
462
+ }
463
+
281
464
  export function createAppComposition(deps: AppDependencies): {
282
465
  app: Hono;
283
466
  routeDeps: ApiRouteDeps;
@@ -291,14 +474,34 @@ export function createAppComposition(deps: AppDependencies): {
291
474
  // Pause) run inside API-originated db commands; install the boot-validated
292
475
  // rollout flag once for this process.
293
476
  configureChildLifecycleNotices({ enabled: deps.settings.childLifecycleNoticesEnabled });
477
+ // Sessions created by this process freeze their code_search decision from
478
+ // the boot-parsed deployment policy.
479
+ configureCodeSearchDeploymentPolicy(codeSearchDeploymentPolicy(deps.settings));
294
480
  const managedEmailTransport =
295
481
  deps.managedEmailTransport ?? createManagedEmailTransport(deps.settings);
296
482
  assertManagedEmailTransportMetadata(managedEmailTransport);
483
+ const observability =
484
+ deps.observability ?? createObservability(deps.settings, { component: "api" });
485
+ // Connect attempt state lives in the db layer; count its committed
486
+ // transitions with bounded provider/state labels (integration-connect-metrics).
487
+ setConnectAttemptTransitionObserver((transition) =>
488
+ observeConnectAttemptTransition(observability, transition),
489
+ );
490
+ const managedAuthNewSignupsGate = createManagedAuthNewSignupsGate({
491
+ db: deps.db,
492
+ settings: deps.settings,
493
+ observability,
494
+ });
297
495
  const managedAuth =
298
- deps.managedAuth ?? createManagedAuth(deps.settings, deps.db, managedEmailTransport);
496
+ deps.managedAuth ??
497
+ createManagedAuth(deps.settings, deps.db, managedEmailTransport, {
498
+ observability,
499
+ newSignupsGate: managedAuthNewSignupsGate,
500
+ });
299
501
  const managedAuthSessionAdapter =
300
502
  deps.managedAuthSessionAdapter ??
301
503
  (managedAuth ? createBetterAuthSessionAdapter(managedAuth, deps.db) : null);
504
+ if (managedAuth) startManagedSignInNotificationDelivery(deps.db, managedEmailTransport);
302
505
  const objectStorage =
303
506
  deps.objectStorage === undefined ? createObjectStorage(deps.settings) : deps.objectStorage;
304
507
  let documentServices: DocumentServices | null = deps.documentServices ?? null;
@@ -369,8 +572,6 @@ export function createAppComposition(deps: AppDependencies): {
369
572
  // concrete for routes; it throws SandboxResumeError when sandboxBackend=none.
370
573
  const sandboxClient = deps.sandboxClient ?? createApiSandboxClient(deps.settings);
371
574
  const resumeBoxById = deps.resumeBoxById ?? makeResumeBoxById(sandboxClient);
372
- const observability =
373
- deps.observability ?? createObservability(deps.settings, { component: "api" });
374
575
  if (
375
576
  managedAuth &&
376
577
  deps.settings.managedAuthSessionSetMode !== "legacy" &&
@@ -389,12 +590,27 @@ export function createAppComposition(deps: AppDependencies): {
389
590
  }, MANAGED_AUTH_REAPER_INTERVAL_MS);
390
591
  (timer as ReturnType<typeof setInterval> & { unref?: () => void }).unref?.();
391
592
  }
593
+ if (deps.transcription === undefined) {
594
+ // Voice pricing problems never fail boot (workers share this config); the
595
+ // affected provider is withheld and the operator sees exactly why, once.
596
+ for (const issue of voiceInputPricingIssues(deps.settings)) {
597
+ observability.error(
598
+ `Voice input provider ${issue.providerId} is unavailable: ${issue.message}`,
599
+ {
600
+ providerId: issue.providerId,
601
+ env: issue.env,
602
+ reason: issue.reason,
603
+ },
604
+ );
605
+ }
606
+ }
392
607
  const transcription =
393
608
  deps.transcription === undefined
394
609
  ? createTranscriptionService({
395
610
  settings: deps.settings,
396
611
  db: deps.db,
397
612
  ...(deps.codexFetch ? { codexFetch: deps.codexFetch } : {}),
613
+ log: (message, attributes) => observability.error(message, attributes),
398
614
  })
399
615
  : deps.transcription;
400
616
  const transcriptionSegmenter =
@@ -403,12 +619,36 @@ export function createAppComposition(deps: AppDependencies): {
403
619
  ffmpegPath: deps.settings.voiceInputFfmpegPath,
404
620
  })
405
621
  : deps.transcriptionSegmenter;
622
+ const modelCalls =
623
+ deps.modelCalls === undefined
624
+ ? createModelCallService({
625
+ db: deps.db,
626
+ settings: deps.settings,
627
+ ...(deps.catalogSourceSettings
628
+ ? { catalogSourceSettings: deps.catalogSourceSettings }
629
+ : {}),
630
+ entitlements: deps.entitlements ?? null,
631
+ ...(deps.codexFetch ? { codexFetch: deps.codexFetch } : {}),
632
+ log: (message, attributes) => observability.error(message, attributes),
633
+ usageLimitMetrics: observability,
634
+ })
635
+ : deps.modelCalls;
636
+ registerProductUsageMetricBaselines(observability);
637
+ // Server-side presence counts managed people, so it exists only where
638
+ // canonical managed browser sessions exist. Writes are batched off the
639
+ // request path; see packages/core/src/user-presence.ts.
640
+ const userPresence =
641
+ deps.userPresence !== undefined
642
+ ? deps.userPresence
643
+ : deps.settings.productAccessMode === "managed"
644
+ ? createUserPresenceRecorder({ db: deps.db, observability })
645
+ : null;
406
646
  const routeDeps: ApiRouteDeps = {
407
647
  ...deps,
408
648
  resolveCatalogSettings: () => resolveCatalogSettings(deps.db, deps.settings),
409
649
  observability,
410
- githubStateSecret:
411
- deps.githubStateSecret ?? deps.settings.githubAppManifestStateSecret ?? crypto.randomUUID(),
650
+ userPresence,
651
+ githubStateSecret: deps.githubStateSecret ?? signedStateSecret(deps.settings, observability),
412
652
  managedAuth,
413
653
  managedAuthSessionAdapter,
414
654
  managedEmailTransport,
@@ -417,20 +657,101 @@ export function createAppComposition(deps: AppDependencies): {
417
657
  getDocumentServices,
418
658
  transcription,
419
659
  transcriptionSegmenter,
660
+ modelCalls,
420
661
  ...(sandboxClient ? { sandboxClient } : {}),
421
662
  resumeBoxById,
422
663
  };
423
664
  const app = new Hono();
424
- const correlationIds = new WeakMap<Request, string>();
665
+ // Body/auth middleware may replace c.req.raw. The Hono context remains the
666
+ // same for the response, diagnostic and completion log of this request.
667
+ const correlationIds = new WeakMap<Context, string>();
668
+ const loggedHttpFailures = new WeakSet<Context>();
669
+ const logHttpFailure = (c: Context, status: number, code: ErrorCode, error?: unknown) => {
670
+ if (loggedHttpFailures.has(c)) return;
671
+ loggedHttpFailures.add(c);
672
+ const correlationId = correlationIds.get(c) ?? crypto.randomUUID();
673
+ const route = routeLabel(new URL(c.req.url).pathname, registeredHandlerRoutePath(c));
674
+ const input =
675
+ error === undefined
676
+ ? null
677
+ : {
678
+ code: "http_request_failed" as const,
679
+ stage: "http.request" as const,
680
+ error,
681
+ };
682
+ const diagnostic = input ? failureDiagnostic(input, deps.settings.deploymentRevision) : null;
683
+ // Diagnostics and logging are observers, never a new response failure.
684
+ try {
685
+ if (input && diagnostic) {
686
+ observability.recordFailureDiagnostic({ ...input, diagnosticId: diagnostic.diagnosticId });
687
+ }
688
+ } catch {
689
+ /* Preserve the original error response if the diagnostic sink fails. */
690
+ }
691
+ try {
692
+ observability.error("HTTP request failed", {
693
+ method: c.req.method,
694
+ route,
695
+ status,
696
+ correlationId,
697
+ errorCode: code,
698
+ errorClass: "HttpOperationError",
699
+ origin: "api",
700
+ reasonKind: diagnostic?.causes[0]?.kind ?? "Response",
701
+ ...(diagnostic ? { diagnosticId: diagnostic.diagnosticId } : {}),
702
+ ...(error === undefined ? {} : errorCauseAttributes(error)),
703
+ });
704
+ observability.incrementCounter({
705
+ name: "opengeni_http_errors_total",
706
+ help: "Total Opengeni HTTP request failures by bounded route, status, and stable code.",
707
+ labels: { route, status: String(status), code },
708
+ });
709
+ } catch {
710
+ /* Preserve the original error response if the logger fails. */
711
+ }
712
+ };
425
713
 
426
714
  app.use("*", async (c, next) => {
427
715
  const correlationId =
428
716
  boundedCorrelationId(c.req.header(OPENGENI_CORRELATION_HEADER)) ?? crypto.randomUUID();
429
- correlationIds.set(c.req.raw, correlationId);
717
+ correlationIds.set(c, correlationId);
430
718
  c.header(OPENGENI_CORRELATION_HEADER, correlationId);
431
719
  await next();
432
720
  });
433
721
 
722
+ // Unauthenticated local mode: admit only requests addressed to this computer
723
+ // and, when a browser sent them, from this stack's web app (see
724
+ // http/local-browser-boundary.ts). Runs before CORS so a refused preflight
725
+ // carries no CORS grant, and logs each distinct refused Host or Origin once
726
+ // because the browser shows only a generic CORS error. Null outside local
727
+ // development.
728
+ const localBrowserBoundary = createLocalBrowserBoundary(deps.settings, {
729
+ warn: (message, attributes) => observability.warn(message, attributes),
730
+ });
731
+ if (localBrowserBoundary) {
732
+ app.use("*", async (c, next) => {
733
+ const rejection = localBrowserBoundary.rejection(c.req.raw);
734
+ if (rejection) {
735
+ throw new ApiHttpError(rejection.status, {
736
+ code: "forbidden",
737
+ message: rejection.message,
738
+ retryable: false,
739
+ details: { code: rejection.code },
740
+ });
741
+ }
742
+ await next();
743
+ });
744
+ }
745
+
746
+ // Better Auth keys its rate limits and session addresses on a request
747
+ // header. Drop any caller-supplied copy everywhere and stamp the trusted
748
+ // source address on managed-auth routes before any route derives a Better
749
+ // Auth request from this one.
750
+ app.use("*", async (c, next) => {
751
+ replaceTrustedClientAddressHeader(c, deps.settings, c.req.path.startsWith("/v1/auth/"));
752
+ await next();
753
+ });
754
+
434
755
  app.use("*", async (c, next) => {
435
756
  const oauthToken = mcpOAuthBearerToken(c.req.raw);
436
757
  if (
@@ -452,15 +773,28 @@ export function createAppComposition(deps: AppDependencies): {
452
773
  "X-OpenGeni-Access-Key",
453
774
  "X-OpenGeni-Api-Contract",
454
775
  "X-OpenGeni-Actor-Epoch",
776
+ // Resumable voice-input chunk integrity/timing (cross-origin embeds).
777
+ "X-OpenGeni-Chunk-Duration-Milliseconds",
778
+ "X-OpenGeni-Chunk-Sha256",
779
+ "X-OpenGeni-Chunk-Start-Milliseconds",
455
780
  "X-OpenGeni-Correlation-Id",
456
781
  "X-OpenGeni-Session-Csrf",
782
+ // Session scope for a session proxy; the API itself ignores it.
783
+ "X-OpenGeni-Session-Id",
784
+ // Set by Site bridges on every bridged request (Site Data frame clamp).
785
+ "X-OpenGeni-Site-Frame",
786
+ "X-OpenGeni-Site-Frame-Id",
457
787
  "X-OpenGeni-Site-Id",
458
788
  "X-OpenGeni-Site-Version",
459
789
  "X-OpenGeni-Subject",
790
+ "X-OpenGeni-User-Activity",
460
791
  ],
461
792
  exposeHeaders: [
462
793
  "Accept-Ranges",
463
794
  "Content-Range",
795
+ "Deprecation",
796
+ "Link",
797
+ "Sunset",
464
798
  "X-OpenGeni-Api-Contract",
465
799
  "X-OpenGeni-Actor-Epoch",
466
800
  "X-OpenGeni-Actor-State",
@@ -471,6 +805,8 @@ export function createAppComposition(deps: AppDependencies): {
471
805
  "X-OpenGeni-Event-Mode",
472
806
  "X-OpenGeni-Event-Result",
473
807
  "X-OpenGeni-Event-Result-Mode",
808
+ "X-OpenGeni-Export-Next-Cursor",
809
+ "X-OpenGeni-Export-Truncated",
474
810
  "X-OpenGeni-Forensic-Exact",
475
811
  "X-OpenGeni-Has-More",
476
812
  "X-OpenGeni-Next-After",
@@ -494,8 +830,25 @@ export function createAppComposition(deps: AppDependencies): {
494
830
  allowedCorsOrigin(deps.settings.corsAllowOriginRegex, origin) ? origin : null,
495
831
  });
496
832
 
833
+ const localCors = localBrowserBoundary
834
+ ? cors({
835
+ ...corsHeaders,
836
+ credentials: true,
837
+ origin: (origin, c) =>
838
+ localBrowserBoundary.originAllowed(origin, localBrowserRequestHost(c.req.raw))
839
+ ? origin
840
+ : null,
841
+ })
842
+ : null;
843
+
497
844
  app.use("*", (c, next) => {
498
845
  const origin = c.req.header("origin");
846
+ if (localCors) {
847
+ // The boundary above already refused every other origin. Local mode
848
+ // never answers with wildcard CORS: a request without credentials acts
849
+ // as the local user, so any site could otherwise read its responses.
850
+ return origin ? localCors(c, next) : next();
851
+ }
499
852
  const middleware =
500
853
  origin && allowedCorsOrigin(deps.settings.corsAllowOriginRegex, origin)
501
854
  ? credentialedCors
@@ -512,7 +865,19 @@ export function createAppComposition(deps: AppDependencies): {
512
865
  // Git packfiles must stay streaming and can legitimately exceed the JSON
513
866
  // request ceiling. The exact closed broker routes apply their own method,
514
867
  // content-type, authority, and idle-deadline checks.
515
- if (isPersonalGitHubGitBrokerRequest(c.req.method, new URL(c.req.url).pathname)) {
868
+ const pathname = new URL(c.req.url).pathname;
869
+ if (isPersonalGitHubGitBrokerRequest(c.req.method, pathname)) {
870
+ await next();
871
+ return;
872
+ }
873
+ // The anonymous web beacons and Site Data uploads enforce their own limits
874
+ // on the streamed body; the generic ceiling would buffer far more first.
875
+ if (
876
+ isClientErrorReportRequest(c.req.method, pathname) ||
877
+ isAnalyticsConsentReportRequest(c.req.method, pathname) ||
878
+ isSessionHistoryImportRequest(c.req.method, pathname) ||
879
+ isSiteDataFileUploadRequest(c.req.method, pathname)
880
+ ) {
516
881
  await next();
517
882
  return;
518
883
  }
@@ -533,75 +898,126 @@ export function createAppComposition(deps: AppDependencies): {
533
898
  }
534
899
  });
535
900
 
901
+ // Public-route deprecations (docs/design/api-compatibility-policy.md) are
902
+ // advertised on every response of the affected route, errors included.
903
+ app.use("/v1/*", deprecationHeadersMiddleware());
904
+ // A pinned SDK must not be told about a revision it is built to reject; see
905
+ // the policy's contract-header rule. Registered before the contract fence so
906
+ // it observes the header that fence sets.
907
+ app.use("/v1/*", bearerApiContractHeaderCompatibility());
908
+
536
909
  app.use("*", async (c, next) => {
537
910
  const url = new URL(c.req.url);
538
- const route = routeLabel(url.pathname);
539
- const correlationId = correlationIds.get(c.req.raw) ?? crypto.randomUUID();
911
+ const route = routeLabel(url.pathname, registeredHandlerRoutePath(c));
912
+ const correlationId = correlationIds.get(c) ?? crypto.randomUUID();
540
913
  const start = performance.now();
541
- const span = observability.startSpan(`HTTP ${c.req.method} ${route}`, {
542
- "http.request.method": c.req.method,
543
- "opengeni.route": route,
544
- });
545
- try {
546
- await next();
547
- const status = c.res.status || 200;
548
- const durationSeconds = (performance.now() - start) / 1000;
549
- observability.recordHttpRequest({
550
- method: c.req.method,
551
- route,
552
- status,
553
- durationSeconds,
554
- });
555
- span.end({
556
- attributes: {
557
- "http.response.status_code": status,
558
- "opengeni.duration_ms": Math.round(durationSeconds * 1000),
559
- },
560
- });
561
- observability.info("HTTP request completed", {
562
- method: c.req.method,
563
- route,
564
- status,
565
- durationMs: Math.round(durationSeconds * 1000),
566
- traceId: span.traceId,
567
- spanId: span.spanId,
568
- correlationId,
569
- });
570
- } catch (error) {
571
- const status = httpStatusForError(error);
572
- const errorCode = errorCodeForStatus(status);
573
- const durationSeconds = (performance.now() - start) / 1000;
574
- observability.recordHttpRequest({
575
- method: c.req.method,
576
- route,
577
- status,
578
- durationSeconds,
579
- });
580
- observability.incrementCounter({
581
- name: "opengeni_http_errors_total",
582
- help: "Total OpenGeni HTTP request failures by bounded route, status, and stable code.",
583
- labels: { route, status: String(status), code: errorCode },
584
- });
585
- span.end({
586
- attributes: {
587
- "http.response.status_code": status,
588
- "opengeni.duration_ms": Math.round(durationSeconds * 1000),
589
- },
590
- error,
591
- });
592
- observability.error("HTTP request failed", {
593
- method: c.req.method,
594
- route,
595
- status,
596
- durationMs: Math.round(durationSeconds * 1000),
597
- traceId: span.traceId,
598
- spanId: span.spanId,
599
- correlationId,
600
- errorCode,
601
- errorClass: "HttpOperationError",
602
- });
603
- throw error;
604
- }
914
+ const span = observability.startSpan(
915
+ `HTTP ${c.req.method} ${route}`,
916
+ {
917
+ "http.request.method": c.req.method,
918
+ "opengeni.route": route,
919
+ },
920
+ { parent: null },
921
+ );
922
+ return await withMcpTelemetry(observability, span.traceId, () =>
923
+ withTraceContext(span, async () => {
924
+ try {
925
+ await next();
926
+ const status = c.res.status || 200;
927
+ // Hono normally handles exceptions in onError before next() resolves.
928
+ // Also cover a handler returning a 5xx without throwing.
929
+ if (status >= 500) logHttpFailure(c, status, errorCodeForStatus(status), c.error);
930
+ const durationSeconds = (performance.now() - start) / 1000;
931
+ observability.recordHttpRequest({
932
+ method: c.req.method,
933
+ route,
934
+ status,
935
+ durationSeconds,
936
+ });
937
+ span.end({
938
+ attributes: {
939
+ "http.response.status_code": status,
940
+ "opengeni.duration_ms": Math.round(durationSeconds * 1000),
941
+ },
942
+ ...(c.error ? { error: c.error } : {}),
943
+ });
944
+ const rejection =
945
+ status >= 400
946
+ ? httpRejectionFactsFromEnvelope(
947
+ await readRejectionEnvelope(c.res),
948
+ errorCodeForStatus(status),
949
+ )
950
+ : null;
951
+ observeInteractionRouteOutcome(observability, {
952
+ method: c.req.method,
953
+ route,
954
+ status,
955
+ durationMs: durationSeconds * 1000,
956
+ rejectionReason: rejection?.reason,
957
+ });
958
+ if (isOAuthCallbackRoute(route)) {
959
+ observeOAuthCallback(observability, {
960
+ route,
961
+ status,
962
+ location: c.res.headers.get("location"),
963
+ });
964
+ }
965
+ if (rejection) {
966
+ observability.recordHttpRejection({
967
+ method: c.req.method,
968
+ route,
969
+ status,
970
+ code: rejection.code,
971
+ reason: rejection.reason,
972
+ });
973
+ }
974
+ observability.info("HTTP request completed", {
975
+ method: c.req.method,
976
+ route,
977
+ status,
978
+ durationMs: Math.round(durationSeconds * 1000),
979
+ traceId: span.traceId,
980
+ spanId: span.spanId,
981
+ correlationId,
982
+ ...(rejection ? rejectionLogAttributes(rejection) : {}),
983
+ });
984
+ } catch (error) {
985
+ const status = httpStatusForError(error);
986
+ const errorCode = errorCodeForStatus(status);
987
+ const durationSeconds = (performance.now() - start) / 1000;
988
+ observability.recordHttpRequest({
989
+ method: c.req.method,
990
+ route,
991
+ status,
992
+ durationSeconds,
993
+ });
994
+ const rejection = httpRejectionFactsFromError(error, errorCode);
995
+ observeInteractionRouteOutcome(observability, {
996
+ method: c.req.method,
997
+ route,
998
+ status,
999
+ durationMs: durationSeconds * 1000,
1000
+ rejectionReason: rejection.reason,
1001
+ });
1002
+ observability.recordHttpRejection({
1003
+ method: c.req.method,
1004
+ route,
1005
+ status,
1006
+ code: rejection.code,
1007
+ reason: rejection.reason,
1008
+ });
1009
+ span.end({
1010
+ attributes: {
1011
+ "http.response.status_code": status,
1012
+ "opengeni.duration_ms": Math.round(durationSeconds * 1000),
1013
+ },
1014
+ error,
1015
+ });
1016
+ logHttpFailure(c, status, errorCode, error);
1017
+ throw error;
1018
+ }
1019
+ }),
1020
+ );
605
1021
  });
606
1022
 
607
1023
  const accessKeyBoundary = requireAccessKey(deps.settings);
@@ -616,17 +1032,66 @@ export function createAppComposition(deps: AppDependencies): {
616
1032
  return await accessKeyBoundary(c, next);
617
1033
  });
618
1034
 
1035
+ // Malformed JSON in a client request body is a 400 wherever a route reads it.
1036
+ app.use("/v1/*", async (c, next) => {
1037
+ tagRequestJsonParseErrors(c);
1038
+ await next();
1039
+ });
1040
+
1041
+ // A request no registered handler answers is a 404 (or a 405 when the path
1042
+ // exists for other methods) before any authentication, authorization, or
1043
+ // contract middleware can turn it into a misleading 401/409/503. This runs
1044
+ // after the deployment perimeter so an unauthenticated caller of a
1045
+ // key-protected deployment learns nothing new.
1046
+ app.use("/v1/*", async (c, next) => {
1047
+ const unmatched = unmatchedRoute(app, c);
1048
+ if (!unmatched) {
1049
+ await next();
1050
+ return;
1051
+ }
1052
+ const requestId = correlationIds.get(c) ?? crypto.randomUUID();
1053
+ c.header(OPENGENI_API_CONTRACT_HEADER, OPENGENI_API_CONTRACT_REVISION);
1054
+ if (unmatched.status === 405) c.header("allow", unmatched.allow.join(", "));
1055
+ return c.json(
1056
+ ErrorEnvelope.parse({
1057
+ error: {
1058
+ status: unmatched.status,
1059
+ code: "not_found",
1060
+ message:
1061
+ unmatched.status === 405
1062
+ ? `Method ${c.req.method} is not supported for this resource. Allowed: ${unmatched.allow.join(", ")}.`
1063
+ : "Resource not found.",
1064
+ retryable: false,
1065
+ requestId,
1066
+ },
1067
+ }),
1068
+ unmatched.status,
1069
+ );
1070
+ });
1071
+
1072
+ // A personal API key acts as its owner. Verify it and stamp this exact raw
1073
+ // Request before any route resolves access; the body limit above may have
1074
+ // rebuilt the Request, so this runs after it.
1075
+ app.use("/v1/*", async (c, next) => {
1076
+ await stampPersonalApiKeyRequest(c.req.raw, deps);
1077
+ await next();
1078
+ });
1079
+
619
1080
  app.use("/v1/*", async (c, next) => {
620
1081
  c.header(OPENGENI_API_CONTRACT_HEADER, OPENGENI_API_CONTRACT_REVISION);
621
1082
  if (
622
1083
  deps.settings.environment !== "test" &&
623
- isApiContractProtectedMutation(c.req.method, new URL(c.req.url).pathname) &&
624
- c.req.header(OPENGENI_API_CONTRACT_HEADER) !== OPENGENI_API_CONTRACT_REVISION
1084
+ apiContractAdmission({
1085
+ method: c.req.method,
1086
+ pathname: new URL(c.req.url).pathname,
1087
+ authorization: c.req.header("authorization"),
1088
+ claimedRevision: c.req.header(OPENGENI_API_CONTRACT_HEADER),
1089
+ }) === "reject"
625
1090
  ) {
626
1091
  return c.json(
627
1092
  {
628
1093
  code: "API_CONTRACT_CHANGED",
629
- message: "OpenGeni updated. Reload this client before changing state.",
1094
+ message: "Opengeni updated. Reload this client before changing state.",
630
1095
  apiContractRevision: OPENGENI_API_CONTRACT_REVISION,
631
1096
  },
632
1097
  409,
@@ -669,10 +1134,72 @@ export function createAppComposition(deps: AppDependencies): {
669
1134
  // wildcard handler or the provider returns its own 404 first.
670
1135
  registerManagedOnboardingRoutes(app, routeDeps);
671
1136
  registerManagedAuthSessionSetRoutes(app, routeDeps);
1137
+ registerManagedSignInMethodRoutes(app, routeDeps);
1138
+ registerNativeAppAuthRoutes(app, routeDeps);
672
1139
  if (managedAuth) {
673
1140
  app.on(["GET", "POST"], "/v1/auth/*", async (c) => {
674
1141
  const pathname = new URL(c.req.url).pathname;
675
1142
  const oauthCallbackProvider = managedAuthOAuthCallbackProvider(pathname);
1143
+ if (
1144
+ pathname === "/v1/auth/sign-up/email" &&
1145
+ c.req.method === "POST" &&
1146
+ !(await managedAuthNewSignupsGate.signupsOpen())
1147
+ ) {
1148
+ // Launch-load safety switch: refuse before Better Auth hashes the
1149
+ // password or sends mail. Better Auth's own disableSignUp flags and
1150
+ // the user-create hook remain the backstop for every other path.
1151
+ return managedAuthNewSignupsPausedResponse();
1152
+ }
1153
+ if (pathname === "/v1/auth/sign-in/social" && c.req.method === "POST") {
1154
+ const body = await c.req.raw
1155
+ .clone()
1156
+ .json()
1157
+ .catch(() => null);
1158
+ if (
1159
+ !body ||
1160
+ !["google", "github"].includes(body.provider) ||
1161
+ Object.prototype.hasOwnProperty.call(body, "idToken")
1162
+ ) {
1163
+ return c.json(
1164
+ {
1165
+ code: "SIGN_IN_METHOD_OAUTH_REDIRECT_REQUIRED",
1166
+ message: "Use the browser OAuth sign-in redirect",
1167
+ },
1168
+ 403,
1169
+ );
1170
+ }
1171
+ }
1172
+ if (oauthCallbackProvider) {
1173
+ const connectResponse = await handleManagedSignInConnectCallback(
1174
+ c,
1175
+ routeDeps,
1176
+ oauthCallbackProvider,
1177
+ );
1178
+ if (connectResponse) return connectResponse;
1179
+ }
1180
+ if (
1181
+ new Set([
1182
+ "link-social",
1183
+ "unlink-account",
1184
+ "list-accounts",
1185
+ "set-password",
1186
+ "change-password",
1187
+ "change-email",
1188
+ "update-user",
1189
+ "delete-user",
1190
+ "get-access-token",
1191
+ "refresh-token",
1192
+ "account-info",
1193
+ ]).has(pathname.slice("/v1/auth/".length))
1194
+ ) {
1195
+ return c.json(
1196
+ {
1197
+ code: "SIGN_IN_METHOD_PRODUCT_ROUTE_REQUIRED",
1198
+ message: "Use personal sign-in method settings",
1199
+ },
1200
+ 403,
1201
+ );
1202
+ }
676
1203
  if (deps.settings.managedAuthSessionSetMode === "legacy") {
677
1204
  return oauthCallbackProvider
678
1205
  ? await runManagedAuthProvider(
@@ -835,37 +1362,151 @@ export function createAppComposition(deps: AppDependencies): {
835
1362
  return c.json(result, result.ok ? 200 : 503);
836
1363
  });
837
1364
 
838
- app.get("/metrics", async (c) =>
839
- c.text(await observability.prometheusMetrics(), 200, {
840
- "content-type": "text/plain; version=0.0.4; charset=utf-8",
841
- }),
842
- );
1365
+ if (publicListenerServesMetrics(deps.settings)) {
1366
+ registerPrometheusMetricsRoute(app, observability);
1367
+ }
843
1368
 
844
1369
  registerMcpOAuthRoutes(app, routeDeps);
845
1370
 
1371
+ registerClientErrorRoutes(app, { observability, settings: deps.settings });
1372
+ registerAnalyticsConsentRoutes(app, { observability, settings: deps.settings });
1373
+
846
1374
  app.get("/v1/config/client", async (c) => {
847
1375
  c.header("cache-control", "no-store");
848
1376
  const resolvedCatalog = await resolveCatalogSettings(deps.db, deps.settings);
849
- const baseCatalogSettings = resolvedCatalog.settings;
850
- const codexCatalogSettings = baseCatalogSettings.codexSubscriptionEnabled
851
- ? withCodexCatalogProvider(baseCatalogSettings)
852
- : baseCatalogSettings;
853
- const catalogSettings = baseCatalogSettings.supergrokSubscriptionEnabled
854
- ? withXaiSubscriptionCatalogProvider(codexCatalogSettings)
855
- : codexCatalogSettings;
1377
+ let catalogSettings = resolvedCatalog.settings;
1378
+ let selections = resolveWorkspaceModelSelection({
1379
+ settings: catalogSettings,
1380
+ policy: null,
1381
+ codexSubscriptionActive: false,
1382
+ });
1383
+ let defaultSelection = selectDefaultSessionModel({
1384
+ settings: catalogSettings,
1385
+ selections,
1386
+ workspaceDefaults: null,
1387
+ creditsAvailable: false,
1388
+ });
1389
+ let modelSelectionForbidden = false;
1390
+ let voiceInputAvailable = false;
1391
+ let voiceInputProviders: string[] = [];
1392
+ const requestedWorkspaceId = c.req.query("workspaceId");
1393
+ if (requestedWorkspaceId !== undefined && requestedWorkspaceId.trim() === "") {
1394
+ throw new HTTPException(422, { message: "workspaceId must not be empty" });
1395
+ }
1396
+ const callerScoped =
1397
+ requestedWorkspaceId !== undefined ||
1398
+ deps.settings.productAccessMode !== "managed" ||
1399
+ c.req.header("authorization") !== undefined ||
1400
+ // Browser bootstrap precedes session-set reconciliation. A cookie alone
1401
+ // cannot scope this public read (broker/changed actors need an epoch).
1402
+ c.req.header("x-opengeni-actor-epoch") !== undefined ||
1403
+ c.req.header("x-opengeni-external-actor") !== undefined;
1404
+ if (callerScoped) {
1405
+ let context: Awaited<ReturnType<typeof requireAccessContext>> | null = null;
1406
+ try {
1407
+ context = await requireAccessContext(c, routeDeps);
1408
+ } catch (error) {
1409
+ // A stale browser cookie must not break the signed-out bootstrap.
1410
+ // Explicit workspace/bearer/external-actor requests fail closed.
1411
+ if (
1412
+ !(error instanceof HTTPException && error.status === 401) ||
1413
+ requestedWorkspaceId !== undefined ||
1414
+ c.req.header("authorization") !== undefined ||
1415
+ c.req.header("x-opengeni-actor-epoch") !== undefined ||
1416
+ c.req.header("x-opengeni-external-actor") !== undefined
1417
+ ) {
1418
+ throw error;
1419
+ }
1420
+ }
1421
+ const workspaceId = requestedWorkspaceId ?? context?.defaultWorkspaceId;
1422
+ if (context && workspaceId) {
1423
+ const grant = await requireAccessGrant(c, routeDeps, workspaceId);
1424
+ await withAccessGrantSessionRlsContext(routeDeps, grant, async () => {
1425
+ catalogSettings = (
1426
+ await resolveWorkspaceCatalogSettings(deps.db, deps.settings, {
1427
+ accountId: grant.accountId,
1428
+ workspaceId,
1429
+ })
1430
+ ).settings;
1431
+ selections = await resolveCallerWorkspaceModelSelections(
1432
+ deps.db,
1433
+ catalogSettings,
1434
+ { accountId: grant.accountId, workspaceId, subjectId: grant.subjectId },
1435
+ { observeAvailability: true },
1436
+ );
1437
+ if (!hasPermission(grant.permissions, "sessions:create")) {
1438
+ modelSelectionForbidden = true;
1439
+ selections = [];
1440
+ }
1441
+ const workspace = await getWorkspace(deps.db, workspaceId);
1442
+ const voicePreferences = WorkspaceVoiceInputSettings.safeParse(
1443
+ workspace?.settings.voiceInput,
1444
+ ).data;
1445
+ const voiceContext = {
1446
+ workspaceId,
1447
+ subjectId: grant.subjectId,
1448
+ preferredProvider: voicePreferences?.preferredProvider,
1449
+ fallbackEnabled: voicePreferences?.fallbackEnabled,
1450
+ };
1451
+ if (hasPermission(grant.permissions, "sessions:create") && transcription) {
1452
+ // `providers` lists every ready provider (the workspace picker);
1453
+ // `available` honours the workspace preference and fallback, so
1454
+ // the composer never offers a mic every request would refuse.
1455
+ voiceInputProviders = (await transcription.availableProviderIds?.(voiceContext)) ?? [];
1456
+ voiceInputAvailable =
1457
+ voiceInputProviders.length > 0 &&
1458
+ (await Promise.resolve(transcription.available(voiceContext)).catch(() => false));
1459
+ }
1460
+
1461
+ defaultSelection = await resolveDefaultSessionModelForSelections(deps.db, {
1462
+ settings: catalogSettings,
1463
+ accountId: grant.accountId,
1464
+ workspaceSettings: workspace?.settings ?? {},
1465
+ selections,
1466
+ });
1467
+ });
1468
+ } else if (context) {
1469
+ // An organization key has no implicit workspace. Do not advertise
1470
+ // another workspace's authority; it can request an exact workspace.
1471
+ selections = [];
1472
+ modelSelectionForbidden = true;
1473
+ }
1474
+ }
1475
+ const models = selections.filter(isWorkspaceModelAdmissible);
1476
+ const defaultModel = defaultSelection.model;
1477
+ const fallbackDefinition = selections.find(
1478
+ ({ model }) => model.id === defaultModel || model.aliases.includes(defaultModel),
1479
+ );
1480
+ const legacyModelFallback =
1481
+ models.length === 0
1482
+ ? {
1483
+ id: defaultModel,
1484
+ availability: {
1485
+ status: "unavailable" as const,
1486
+ selectable: false as const,
1487
+ reason: modelSelectionForbidden
1488
+ ? ("policy_blocked" as const)
1489
+ : (fallbackDefinition?.availability.reason ?? ("unsupported" as const)),
1490
+ checkedAt: fallbackDefinition?.availability.checkedAt ?? null,
1491
+ },
1492
+ }
1493
+ : undefined;
856
1494
  return c.json(
857
1495
  ClientConfig.parse({
858
1496
  deploymentRevision: deps.settings.deploymentRevision,
1497
+ claudeSubscriptionEnabled: deps.settings.claudeSubscriptionEnabled,
1498
+ connectedMachines: deps.settings.sandboxSelfhostedEnabled,
859
1499
  apiContractRevision: OPENGENI_API_CONTRACT_REVISION,
860
1500
  ...(deps.settings.serverVersion ? { serverVersion: deps.settings.serverVersion } : {}),
861
- defaultModel: canonicalizeConfiguredModelId(catalogSettings, catalogSettings.openaiModel),
862
- allowedModels: configuredAllowedModels(catalogSettings),
863
- // Provider-grouped model list for the picker. configuredModels() carries the
864
- // union of the built-in allow-list and every registry provider's models, in
865
- // selection order (default model first); project each to the client-safe
866
- // provider-blind ClientModel shape (execution topology remains server-side).
867
- models: configuredModels(catalogSettings).map(projectClientModel),
868
- defaultReasoningEffort: deps.settings.openaiReasoningEffort,
1501
+ defaultModel,
1502
+ allowedModels: models.length > 0 ? models.map(({ model }) => model.id) : [defaultModel],
1503
+ ...(legacyModelFallback ? { legacyModelFallback } : {}),
1504
+ // Availability remains an observation hint, not the admission predicate.
1505
+ models: models.map(({ model, availability }) => ({
1506
+ ...projectClientModel(model),
1507
+ availability,
1508
+ })),
1509
+ defaultReasoningEffort: defaultSelection.reasoningEffort,
869
1510
  allowedReasoningEfforts: configuredAllowedReasoningEfforts(deps.settings),
870
1511
  defaultSandboxBackend: deps.settings.sandboxBackend,
871
1512
  mcpServers: deps.settings.mcpServers.map((server) => ({
@@ -873,15 +1514,29 @@ export function createAppComposition(deps: AppDependencies): {
873
1514
  name: server.name ?? server.id,
874
1515
  })),
875
1516
  firstPartyMcpTools: resolveFirstPartyMcpToolPolicy(deps.settings),
1517
+ codeSearch: codeSearchDeploymentPolicy(deps.settings),
1518
+ // Archiving needs object storage; without it the worker never archives.
1519
+ ...(deps.settings.sessionArchiveEnabled && objectStorage
1520
+ ? {
1521
+ sessionArchive: {
1522
+ enabled: true as const,
1523
+ idleDays: deps.settings.sessionArchiveIdleDays,
1524
+ },
1525
+ }
1526
+ : {}),
1527
+ agentConfig: clientAgentConfig(deps.settings),
1528
+ // Producer capability for per-task usage limits. Clients only offer a
1529
+ // new limit when enabled; existing limits stay visible and removable.
1530
+ sessionUsageLimits: {
1531
+ enabled: sessionUsageLimitsProducerEnabled(deps.settings),
1532
+ },
876
1533
  fileUploads: {
877
1534
  enabled: objectStorage !== null,
878
1535
  maxSizeBytes: objectStorage?.maxSinglePutSizeBytes ?? 5_000_000_000,
879
1536
  },
880
1537
  voiceInput: {
881
- providers: resolveVoiceInputProviderRegistry(deps.settings).map(
882
- (provider) => provider.id,
883
- ),
884
- available: (await transcription?.available()) ?? false,
1538
+ providers: voiceInputProviders,
1539
+ available: voiceInputAvailable,
885
1540
  maxDurationSeconds: deps.settings.voiceInputMaxDurationSeconds,
886
1541
  maxSizeBytes: deps.settings.voiceInputMaxSizeBytes,
887
1542
  acceptedMimeTypes: [...VOICE_INPUT_ACCEPTED_MIME_TYPES],
@@ -889,7 +1544,7 @@ export function createAppComposition(deps: AppDependencies): {
889
1544
  objectStorage &&
890
1545
  transcription &&
891
1546
  transcriptionSegmenter &&
892
- (await transcription.available()) &&
1547
+ voiceInputAvailable &&
893
1548
  (await transcriptionSegmenter.available())
894
1549
  ? {
895
1550
  resumable: {
@@ -907,16 +1562,126 @@ export function createAppComposition(deps: AppDependencies): {
907
1562
  productAccessMode: deps.settings.productAccessMode,
908
1563
  billingMode: deps.settings.billingMode,
909
1564
  managedAuthSessionSetMode: deps.settings.managedAuthSessionSetMode,
910
- auth: clientAuthConfig(deps.settings),
1565
+ auth: clientAuthConfig(
1566
+ deps.settings,
1567
+ deps.settings.productAccessMode === "managed"
1568
+ ? await managedAuthNewSignupsGate.signupsOpen()
1569
+ : true,
1570
+ ),
1571
+ documentationUrl: deps.settings.documentationUrl,
911
1572
  analytics: clientAnalyticsConfig(deps.settings),
1573
+ legal: clientLegalConfig(deps.settings),
1574
+ ...(deps.settings.supportEmail ? { supportEmail: deps.settings.supportEmail } : {}),
912
1575
  // Channel-A structured services (P4.4) ride exec/readFile/createEditor,
913
1576
  // available on every real backend; `none` has no box so they are all off.
914
1577
  // Per-session availability is still negotiated on /stream-capabilities.
915
1578
  structuredServices: structuredServicesHint(deps.settings.sandboxBackend),
1579
+ sites: clientSitesConfig(deps.settings),
916
1580
  }),
917
1581
  );
918
1582
  });
919
1583
 
1584
+ // The organization MCP server: every public action, as a person who signed
1585
+ // in (capped by their connection's access setting) or as an organization
1586
+ // API key. Each action runs the real route in this process.
1587
+ app.all(ORGANIZATION_MCP_PATH, async (c) => {
1588
+ let boundedRequest: Request;
1589
+ try {
1590
+ boundedRequest = await boundedMcpRequest(c.req.raw);
1591
+ } catch (error) {
1592
+ if (error instanceof McpPayloadTooLargeError) {
1593
+ throw new HTTPException(413, { message: "MCP request body exceeds the safety limit" });
1594
+ }
1595
+ throw error;
1596
+ }
1597
+ const challenge = () => {
1598
+ if (deps.settings.mcpOauthEnabled) {
1599
+ c.header("www-authenticate", mcpOAuthAuthenticateHeader(routeDeps, ORGANIZATION_MCP_PATH));
1600
+ }
1601
+ };
1602
+ let caller: OrganizationMcpCaller;
1603
+ try {
1604
+ const connection = await resolveOrganizationMcpOAuthAccess(routeDeps, c.req.raw);
1605
+ const personalKey = connection
1606
+ ? null
1607
+ : verifiedDelegatedHumanAuthorizationForRequest(c.req.raw);
1608
+ if (connection) {
1609
+ caller = {
1610
+ kind: "person",
1611
+ accountId: connection.accountId,
1612
+ subjectId: connection.subjectId,
1613
+ access: connection.organizationAccess,
1614
+ };
1615
+ } else if (personalKey) {
1616
+ // A personal API key: the person who created it, with its access.
1617
+ // Each action rechecks their live access, exactly as for sign-in.
1618
+ await requireAccessContext(c, routeDeps);
1619
+ caller = {
1620
+ kind: "person",
1621
+ accountId: personalKey.organizationId,
1622
+ subjectId: personalKey.subjectId,
1623
+ access: normalizeOrganizationAccessPolicy({
1624
+ preset: "custom",
1625
+ permissions: [...personalKey.permissions],
1626
+ workspaceScope: personalKey.workspaceScope,
1627
+ }),
1628
+ };
1629
+ } else {
1630
+ const context = await requireAccessContext(c, routeDeps);
1631
+ const authorization = c.req.header("authorization");
1632
+ if (!accountScopedApiKeyWorkspaceAuthority(context) || !authorization) {
1633
+ throw new HTTPException(403, {
1634
+ message:
1635
+ "Connect with Opengeni sign-in, a personal API key or an organization API key.",
1636
+ });
1637
+ }
1638
+ caller = {
1639
+ kind: "key",
1640
+ authorization,
1641
+ accessKey: c.req.header("x-opengeni-access-key") ?? null,
1642
+ };
1643
+ }
1644
+ } catch (error) {
1645
+ if (error instanceof HTTPException && error.status === 401) challenge();
1646
+ throw error;
1647
+ }
1648
+ // Stateless JSON-response transport: there is no server-to-client stream.
1649
+ // Answering GET with an empty 200 made clients (Claude) reconnect every
1650
+ // second; refuse it after authorization, as the workspace endpoint does.
1651
+ if (c.req.method === "GET") {
1652
+ const version = c.req.header("mcp-protocol-version");
1653
+ if (version && !SUPPORTED_PROTOCOL_VERSIONS.includes(version)) {
1654
+ return c.json(
1655
+ {
1656
+ jsonrpc: "2.0",
1657
+ id: null,
1658
+ error: { code: -32000, message: "Unsupported protocol version." },
1659
+ },
1660
+ 400,
1661
+ );
1662
+ }
1663
+ return c.json(
1664
+ { jsonrpc: "2.0", id: null, error: { code: -32000, message: "Method not allowed." } },
1665
+ 405,
1666
+ { allow: "POST" },
1667
+ );
1668
+ }
1669
+ const transport = new WebStandardStreamableHTTPServerTransport({ enableJsonResponse: true });
1670
+ const mcp = buildOrganizationMcpServer({
1671
+ caller,
1672
+ origin: new URL(c.req.url).origin,
1673
+ publicOrigin: canonicalPublicOrigin(deps.settings.publicBaseUrl),
1674
+ dispatch: async (request) => await app.fetch(request, c.env),
1675
+ signal: c.req.raw.signal,
1676
+ });
1677
+ try {
1678
+ await mcp.connect(transport);
1679
+ return await handleMcpRequestWithClientAbort(transport, boundedRequest, c.req.raw.signal);
1680
+ } finally {
1681
+ await mcp.close().catch(() => undefined);
1682
+ }
1683
+ });
1684
+
920
1685
  app.use("/v1/workspaces/:workspaceId/*", async (c, next) => {
921
1686
  const workspaceId = c.req.param("workspaceId");
922
1687
  if (workspaceRequestRequiresCodexAccountPrevalidation(c.req.raw)) {
@@ -929,12 +1694,47 @@ export function createAppComposition(deps: AppDependencies): {
929
1694
  await next();
930
1695
  return;
931
1696
  }
932
- const grant = await requireAccessGrant(c, routeDeps, workspaceId);
1697
+ // Match Hono's decoded routing path before first-use membership and RLS.
1698
+ const permission =
1699
+ c.req.method === "PUT" &&
1700
+ /^\/v1\/workspaces\/[^/]+\/artifact-catalog\/[^/]+\/[^/]+\/pin$/.test(c.req.path)
1701
+ ? "artifacts:publish"
1702
+ : undefined;
1703
+ const grant = await requireAccessGrant(c, routeDeps, workspaceId, permission);
933
1704
  await withAccessGrantSessionRlsContext(routeDeps, grant, next);
934
1705
  });
935
1706
 
1707
+ // Requests a host forwarded from a Site frame: SITE_OPERATIONS only, only
1708
+ // the frame's own sessions, and the validated Site origin as provenance.
1709
+ app.use("/v1/workspaces/:workspaceId/*", siteFrameGuard(routeDeps));
1710
+ // Agents started from a Site page do not learn or widen access: their
1711
+ // workspace writes are held to an allowlist (their tools, conversations,
1712
+ // files and content); everything else is refused.
1713
+ app.use("/v1/workspaces/:workspaceId/*", siteClampedAgentGuard(routeDeps));
1714
+
936
1715
  app.all("/v1/workspaces/:workspaceId/mcp", async (c) => {
937
1716
  const workspaceId = c.req.param("workspaceId");
1717
+ // This endpoint uses a fresh stateless JSON-response transport per POST;
1718
+ // there is no persistent server-to-client stream. Refuse GET only after
1719
+ // the existing OAuth/workspace/session authorization, before tool setup.
1720
+ const unsupportedGet = () => {
1721
+ const version = c.req.header("mcp-protocol-version");
1722
+ if (version && !SUPPORTED_PROTOCOL_VERSIONS.includes(version)) {
1723
+ return c.json(
1724
+ {
1725
+ jsonrpc: "2.0",
1726
+ id: null,
1727
+ error: { code: -32000, message: "Unsupported protocol version." },
1728
+ },
1729
+ 400,
1730
+ );
1731
+ }
1732
+ return c.json(
1733
+ { jsonrpc: "2.0", id: null, error: { code: -32000, message: "Method not allowed." } },
1734
+ 405,
1735
+ { allow: "POST" },
1736
+ );
1737
+ };
938
1738
  let boundedRequest: Request;
939
1739
  try {
940
1740
  boundedRequest = await boundedMcpRequest(c.req.raw);
@@ -960,6 +1760,7 @@ export function createAppComposition(deps: AppDependencies): {
960
1760
  }
961
1761
  if (oauthAccess) {
962
1762
  return await withAccessGrantSessionRlsContext(routeDeps, oauthAccess.grant, async () => {
1763
+ if (c.req.method === "GET") return unsupportedGet();
963
1764
  const prepared = await prepareMcpOAuthWorkspaceToolGateway(
964
1765
  routeDeps,
965
1766
  oauthAccess.grant,
@@ -994,73 +1795,203 @@ export function createAppComposition(deps: AppDependencies): {
994
1795
  throw error;
995
1796
  }
996
1797
  const grant = authorization.grant;
997
- return await withAccessGrantSessionRlsContext(routeDeps, grant, async () => {
998
- const boundSessionId = grant.metadata?.sessionId;
999
- if (typeof boundSessionId === "string") {
1000
- try {
1001
- await requireSessionAuthorization(routeDeps, grant, {
1002
- sessionId: boundSessionId,
1003
- operation: "session.first_party_mcp.call",
1004
- surface: "first_party_mcp",
1005
- });
1006
- } catch (error) {
1007
- if (error instanceof SessionAuthorizationDeniedError) {
1008
- throw new HTTPException(404, { message: "session not found" });
1009
- }
1010
- if (error instanceof SessionAuthorizationUnavailableError) {
1011
- throw new HTTPException(503, {
1012
- message: "session authorization is unavailable",
1798
+ // Admin actions dispatch back into this API as a person. Capture the
1799
+ // context now, before this request takes on the agent's database and
1800
+ // billing scope, so none of it carries into those actions.
1801
+ const outsideAgentContext = AsyncLocalStorage.snapshot();
1802
+ // The agent-attempt context, this route check, and a tool's own entry check
1803
+ // re-read the same caller session and attempt; share those reads.
1804
+ return await withSessionAuthorizationReadReuse((reads) =>
1805
+ withAccessGrantSessionRlsContext(routeDeps, grant, async () => {
1806
+ const boundSessionId = grant.metadata?.sessionId;
1807
+ if (typeof boundSessionId === "string") {
1808
+ try {
1809
+ await requireSessionAuthorization(routeDeps, grant, {
1810
+ sessionId: boundSessionId,
1811
+ operation: "session.first_party_mcp.call",
1812
+ surface: "first_party_mcp",
1013
1813
  });
1814
+ } catch (error) {
1815
+ if (error instanceof SessionAuthorizationDeniedError) {
1816
+ throw new HTTPException(404, { message: "session not found" });
1817
+ }
1818
+ if (error instanceof SessionAuthorizationUnavailableError) {
1819
+ throw new HTTPException(503, {
1820
+ message: "session authorization is unavailable",
1821
+ });
1822
+ }
1823
+ throw error;
1014
1824
  }
1015
- throw error;
1016
1825
  }
1017
- }
1018
- const workspace = await getWorkspace(routeDeps.db, workspaceId);
1019
- const workspaceMemoryEnabled = resolveWorkspaceMemoryEnabled(workspace?.settings);
1020
- const workspaceMemoryPromptMode = resolveWorkspaceMemoryPromptMode();
1021
- const transport = new WebStandardStreamableHTTPServerTransport({
1022
- enableJsonResponse: true,
1023
- });
1024
- if (!grantUsesAttemptScopedMcp(grant)) {
1025
- const prepared = await prepareWorkspaceToolGateway(routeDeps, authorization);
1026
- const mcp = buildWorkspaceToolGatewayMcpServer(prepared, grant, routeDeps.observability);
1826
+ if (c.req.method === "GET") return unsupportedGet();
1827
+ reads.handOffToToolDispatch();
1828
+ const workspace = await getWorkspace(routeDeps.db, workspaceId);
1829
+ const workspaceMemoryEnabled = resolveWorkspaceMemoryEnabled(workspace?.settings);
1830
+ const workspaceMemoryPromptMode = resolveWorkspaceMemoryPromptMode();
1831
+ const transport = new WebStandardStreamableHTTPServerTransport({
1832
+ enableJsonResponse: true,
1833
+ });
1834
+ if (!grantUsesAttemptScopedMcp(grant)) {
1835
+ const prepared = await prepareWorkspaceToolGateway(routeDeps, authorization);
1836
+ const mcp = buildWorkspaceToolGatewayMcpServer(prepared, grant, routeDeps.observability);
1837
+ try {
1838
+ await mcp.connect(transport);
1839
+ return await handleMcpRequestWithClientAbort(
1840
+ transport,
1841
+ boundedRequest,
1842
+ c.req.raw.signal,
1843
+ );
1844
+ } finally {
1845
+ await Promise.allSettled([mcp.close(), prepared.close()]);
1846
+ }
1847
+ }
1848
+ const mcpDeps = await resolveWorkspaceMcpRouteDeps(routeDeps, grant);
1849
+ // The bound session's frozen Memory selector (migration 0427) decides
1850
+ // which Memory tools the attempt receives and which typed layers they
1851
+ // read and write. A missing row resolves to no Memory tools.
1852
+ const sessionMemory =
1853
+ typeof boundSessionId === "string"
1854
+ ? ((await resolveSessionMemoryAgentScope(
1855
+ routeDeps.db,
1856
+ workspaceId,
1857
+ boundSessionId,
1858
+ grant.metadata,
1859
+ )) ?? {
1860
+ mode: "off" as const,
1861
+ userSubjectId: null,
1862
+ rootSessionId: null,
1863
+ })
1864
+ : null;
1865
+ // Checked live: allowed by the organization, given to this session,
1866
+ // and the person who gave it is still an owner or admin.
1867
+ const sessionAdminAccess =
1868
+ typeof boundSessionId === "string" &&
1869
+ (await resolveSessionAdminAuthority(routeDeps.db, {
1870
+ accountId: grant.accountId,
1871
+ workspaceId,
1872
+ sessionId: boundSessionId,
1873
+ }))
1874
+ ? {
1875
+ origin: new URL(c.req.url).origin,
1876
+ dispatch: (request: Request) =>
1877
+ outsideAgentContext(async () => await app.fetch(request, c.env)),
1878
+ signal: c.req.raw.signal,
1879
+ }
1880
+ : null;
1881
+ const mcp = buildOpenGeniMcpServer(mcpDeps, grant, {
1882
+ requestOrigin: new URL(c.req.url).origin,
1883
+ workspaceMemoryEnabled,
1884
+ workspaceMemoryPromptMode,
1885
+ sessionMemory,
1886
+ sessionAdminAccess,
1887
+ });
1888
+ // Bind tool handlers' `extra.signal` to the HTTP client's connection: a
1889
+ // worker that drops the call (Steer/Pause) aborts a blocking tool here.
1890
+ // Each POST builds a fresh server; close it once the JSON response is
1891
+ // ready, like the gateway paths above, so it can't outlive the request.
1027
1892
  try {
1028
1893
  await mcp.connect(transport);
1029
1894
  return await handleMcpRequestWithClientAbort(transport, boundedRequest, c.req.raw.signal);
1030
1895
  } finally {
1031
- await Promise.allSettled([mcp.close(), prepared.close()]);
1896
+ await mcp.close().catch(() => undefined);
1032
1897
  }
1033
- }
1034
- const mcpDeps = await resolveWorkspaceMcpRouteDeps(routeDeps, grant);
1035
- // The bound session's frozen Memory selector (migration 0427) decides
1036
- // which Memory tools the attempt receives and which typed layers they
1037
- // read and write. A missing row resolves to no Memory tools.
1038
- const sessionMemory =
1039
- typeof boundSessionId === "string"
1040
- ? ((await resolveSessionMemoryAgentScope(
1041
- routeDeps.db,
1042
- workspaceId,
1043
- boundSessionId,
1044
- grant.metadata,
1045
- )) ?? {
1046
- mode: "off" as const,
1047
- userSubjectId: null,
1048
- rootSessionId: null,
1049
- })
1050
- : null;
1051
- const mcp = buildOpenGeniMcpServer(mcpDeps, grant, {
1052
- requestOrigin: new URL(c.req.url).origin,
1053
- workspaceMemoryEnabled,
1054
- workspaceMemoryPromptMode,
1055
- sessionMemory,
1056
- });
1057
- await mcp.connect(transport);
1058
- // Bind tool handlers' `extra.signal` to the HTTP client's connection: a
1059
- // worker that drops the call (Steer/Pause) aborts a blocking tool here.
1060
- return await handleMcpRequestWithClientAbort(transport, boundedRequest, c.req.raw.signal);
1061
- });
1898
+ }),
1899
+ );
1062
1900
  });
1063
1901
 
1902
+ // Known calls need no preceding full-catalog discovery or replica-local attestation.
1903
+ for (const operation of ["resolve", "invoke", "target-approvals", "manifest"] as const) {
1904
+ app.post(`/v1/workspaces/:workspaceId/tools/${operation}`, async (c) => {
1905
+ const workspaceId = c.req.param("workspaceId");
1906
+ const authorization = await requireAccessGrantAuthorization(
1907
+ c,
1908
+ routeDeps,
1909
+ workspaceId,
1910
+ "workspace:read",
1911
+ );
1912
+ let fence: TargetAuthorityFence | undefined;
1913
+ const options = {
1914
+ signal: c.req.raw.signal,
1915
+ // Human callers keep their fresh grant. Organization keys and external
1916
+ // actors are re-read by the request fence itself.
1917
+ reauthorize: async () => {
1918
+ const live = await requireFreshAccessGrant(c, routeDeps, workspaceId, "workspace:read");
1919
+ const original = authorization.grant;
1920
+ if (
1921
+ live.accountId !== original.accountId ||
1922
+ live.subjectId !== original.subjectId ||
1923
+ live.principalKind !== original.principalKind ||
1924
+ original.permissions.some((permission) => !hasPermission(live.permissions, permission))
1925
+ )
1926
+ throw new HTTPException(403, { message: "tool_access_changed" });
1927
+ },
1928
+ onFence: (created: TargetAuthorityFence) => {
1929
+ fence = created;
1930
+ },
1931
+ };
1932
+ const body: unknown = await c.req.json().catch(() => null);
1933
+ const schema =
1934
+ operation === "resolve"
1935
+ ? ToolGatewayResolveRequest
1936
+ : operation === "invoke"
1937
+ ? ToolGatewayInvokeRequest
1938
+ : operation === "manifest"
1939
+ ? ToolGatewayManifestRequest
1940
+ : ToolGatewayTargetApprovalRequest;
1941
+ if (!schema.safeParse(body).success)
1942
+ throw new HTTPException(400, { message: "Invalid targeted tool request" });
1943
+ c.header("cache-control", "no-store");
1944
+ const started = performance.now();
1945
+ let outcome: "ok" | "failed" = "failed";
1946
+ try {
1947
+ const response =
1948
+ operation === "resolve"
1949
+ ? await resolveWorkspaceToolTarget(
1950
+ routeDeps,
1951
+ authorization,
1952
+ ToolGatewayResolveRequest.parse(body),
1953
+ options,
1954
+ )
1955
+ : operation === "invoke"
1956
+ ? await invokeWorkspaceToolTarget(
1957
+ routeDeps,
1958
+ authorization,
1959
+ ToolGatewayInvokeRequest.parse(body),
1960
+ options,
1961
+ )
1962
+ : operation === "manifest"
1963
+ ? await resolveWorkspaceToolManifest(
1964
+ routeDeps,
1965
+ authorization,
1966
+ ToolGatewayManifestRequest.parse(body),
1967
+ options,
1968
+ )
1969
+ : await approveWorkspaceToolTarget(
1970
+ routeDeps,
1971
+ authorization,
1972
+ ToolGatewayTargetApprovalRequest.parse(body),
1973
+ options,
1974
+ );
1975
+ c.header(
1976
+ "server-timing",
1977
+ `gw-target;dur=${(performance.now() - started).toFixed(1)};desc="${operation}"` +
1978
+ (fence
1979
+ ? `, gw-fence;dur=${fence.stats.durationMs.toFixed(1)};desc="${fence.stats.evaluations}"`
1980
+ : ""),
1981
+ );
1982
+ outcome = "ok";
1983
+ return c.json(response, operation === "target-approvals" ? 201 : 200);
1984
+ } finally {
1985
+ recordWorkspaceToolTargetOperation(routeDeps.observability, {
1986
+ operation,
1987
+ outcome,
1988
+ durationSeconds: (performance.now() - started) / 1000,
1989
+ });
1990
+ }
1991
+ });
1992
+ }
1993
+ // Legacy calls retain their content-free, per-process catalog attestations.
1994
+ const toolGatewayCatalogAttestations = createWorkspaceToolGatewayCatalogAttestations();
1064
1995
  app.get("/v1/workspaces/:workspaceId/tools/catalog", async (c) => {
1065
1996
  const workspaceId = c.req.param("workspaceId");
1066
1997
  const authorization = await requireAccessGrantAuthorization(
@@ -1069,9 +2000,22 @@ export function createAppComposition(deps: AppDependencies): {
1069
2000
  workspaceId,
1070
2001
  "workspace:read",
1071
2002
  );
1072
- const prepared = await prepareWorkspaceToolGateway(routeDeps, authorization);
2003
+ const startedAt = performance.now();
2004
+ const prepared = await prepareAttestedWorkspaceToolGateway(
2005
+ routeDeps,
2006
+ authorization,
2007
+ toolGatewayCatalogAttestations,
2008
+ );
1073
2009
  try {
2010
+ const prepareMs = performance.now() - startedAt;
2011
+ recordWorkspaceToolGatewayPreparation(routeDeps.observability, {
2012
+ operation: "catalog",
2013
+ scope: "complete",
2014
+ attestation: "record",
2015
+ durationSeconds: prepareMs / 1_000,
2016
+ });
1074
2017
  c.header("cache-control", "no-store");
2018
+ c.header("server-timing", workspaceToolGatewayServerTiming({ prepareMs, scope: "complete" }));
1075
2019
  return c.json(prepared.toolGatewayCatalog);
1076
2020
  } finally {
1077
2021
  await prepared.close();
@@ -1086,26 +2030,28 @@ export function createAppComposition(deps: AppDependencies): {
1086
2030
  workspaceId,
1087
2031
  "workspace:read",
1088
2032
  );
1089
- const grant = authorization.grant;
1090
2033
  const parsed = ToolGatewayCallRequest.safeParse(await c.req.json().catch(() => null));
1091
2034
  if (!parsed.success) {
1092
2035
  throw new HTTPException(400, { message: "Invalid tool gateway call" });
1093
2036
  }
1094
- const prepared = await prepareWorkspaceToolGateway(routeDeps, authorization);
1095
- try {
1096
- return c.json(
1097
- await callWorkspaceToolGateway(
1098
- prepared,
1099
- grant,
1100
- parsed.data,
1101
- routeDeps.db,
1102
- undefined,
1103
- routeDeps.observability,
1104
- ),
1105
- );
1106
- } finally {
1107
- await prepared.close();
1108
- }
2037
+ const { response, timing } = await callWorkspaceToolGatewayForCaller(
2038
+ routeDeps,
2039
+ authorization,
2040
+ parsed.data,
2041
+ {
2042
+ attestations: toolGatewayCatalogAttestations,
2043
+ ...(routeDeps.observability ? { observability: routeDeps.observability } : {}),
2044
+ onPreparation: (preparation) =>
2045
+ recordWorkspaceToolGatewayPreparation(routeDeps.observability, {
2046
+ operation: "call",
2047
+ scope: preparation.scope,
2048
+ attestation: preparation.attestation,
2049
+ durationSeconds: preparation.prepareMs / 1_000,
2050
+ }),
2051
+ },
2052
+ );
2053
+ c.header("server-timing", workspaceToolGatewayServerTiming(timing));
2054
+ return c.json(response);
1109
2055
  });
1110
2056
 
1111
2057
  app.post("/v1/workspaces/:workspaceId/tools/approvals", async (c) => {
@@ -1146,7 +2092,11 @@ export function createAppComposition(deps: AppDependencies): {
1146
2092
  workspaceId,
1147
2093
  "workspace:read",
1148
2094
  );
1149
- const prepared = await prepareWorkspaceToolGateway(routeDeps, authorization);
2095
+ const prepared = await prepareAttestedWorkspaceToolGateway(
2096
+ routeDeps,
2097
+ authorization,
2098
+ toolGatewayCatalogAttestations,
2099
+ );
1150
2100
  try {
1151
2101
  c.header("cache-control", "no-store");
1152
2102
  return c.json(workspaceToolGatewayDeclarations(prepared));
@@ -1162,16 +2112,20 @@ export function createAppComposition(deps: AppDependencies): {
1162
2112
  const prefix = `/v1/workspaces/${workspaceId}/codemode/sdk`;
1163
2113
  let forwarded: Request;
1164
2114
  try {
1165
- forwarded = await codemodeSessionRequest(
1166
- routeDeps,
1167
- grant,
1168
- c.req.raw,
1169
- url.pathname.slice(prefix.length) + url.search,
1170
- );
2115
+ const path = url.pathname.slice(prefix.length) + url.search;
2116
+ forwarded = await codemodeSessionRequest(routeDeps, grant, c.req.raw, path, (session) => {
2117
+ assertConfiguredCodemodeSessionProxyPath(session, path, c.req.method);
2118
+ const proxyTools = configuredCodemodeSessionProxyTools(routeDeps.settings, session);
2119
+ return proxyTools === null
2120
+ ? routeDeps.settings
2121
+ : { ...routeDeps.settings, allowedFirstPartyMcpTools: proxyTools };
2122
+ });
1171
2123
  } catch (error) {
1172
2124
  throw codemodeHttpError(error);
1173
2125
  }
1174
- return app.fetch(forwarded);
2126
+ // The forwarded request drops the caller's Host and names a non-sandbox
2127
+ // path; the request it was built from already passed the local boundary.
2128
+ return app.fetch(markLocalInternalDispatch(forwarded));
1175
2129
  });
1176
2130
 
1177
2131
  app.get("/v1/workspaces/:workspaceId/codemode/catalog", async (c) => {
@@ -1198,7 +2152,12 @@ export function createAppComposition(deps: AppDependencies): {
1198
2152
  throw new HTTPException(400, { message: "Invalid Codemode call" });
1199
2153
  }
1200
2154
  try {
1201
- const submission = await submitAndDispatchCodemodeCall(routeDeps, grant, parsed.data);
2155
+ const submission = await submitAndDispatchCodemodeCall(
2156
+ routeDeps,
2157
+ grant,
2158
+ parsed.data,
2159
+ c.req.raw.signal,
2160
+ );
1202
2161
  const terminal = ["completed", "failed", "outcome_unknown", "cancelled"].includes(
1203
2162
  submission.operation.state,
1204
2163
  );
@@ -1215,7 +2174,9 @@ export function createAppComposition(deps: AppDependencies): {
1215
2174
  throw new HTTPException(403, { message: "Codemode access denied" });
1216
2175
  }
1217
2176
  try {
1218
- const operation = await readCodemodeOperation(routeDeps, grant, c.req.param("operationId"));
2177
+ const operation = await readCodemodeOperation(routeDeps, grant, c.req.param("operationId"), {
2178
+ durableApproval: c.req.header("x-opengeni-codemode-capabilities") === "durable-approval-v1",
2179
+ });
1219
2180
  if (!operation)
1220
2181
  throw new HTTPException(404, {
1221
2182
  message: "Codemode operation not found",
@@ -1229,35 +2190,53 @@ export function createAppComposition(deps: AppDependencies): {
1229
2190
 
1230
2191
  registerConnectCallbackReturns(app, routeDeps);
1231
2192
  registerFileRoutes(app, routeDeps);
2193
+ registerSessionArtifactAssociationRoutes(app, routeDeps);
1232
2194
  registerApiKeyRoutes(app, routeDeps);
2195
+ registerOrganizationMcpConnectionRoutes(app, routeDeps);
2196
+ registerPersonalApiKeyRoutes(app, routeDeps);
2197
+ registerOrganizationServiceAccountRoutes(app, routeDeps);
1233
2198
  registerBillingRoutes(app, routeDeps);
1234
2199
  registerBrowserIdentityRoutes(app, routeDeps);
1235
- registerBrowserSessionRoutes(app, routeDeps);
1236
- registerComputerSessionRoutes(app, routeDeps);
2200
+ const interactionSecrets = createVaultInteractionSecretsPort({
2201
+ db: routeDeps.db,
2202
+ encryptionKey: environmentsEncryptionKeyBytes(routeDeps.settings) ?? null,
2203
+ providersFor: (workspaceId, subjectId) =>
2204
+ vaultProviderRegistryFor(routeDeps, { workspaceId, subjectId }),
2205
+ });
2206
+ registerBrowserSessionRoutes(app, routeDeps, { interactionSecrets });
2207
+ registerComputerSessionRoutes(app, routeDeps, { interactionSecrets });
1237
2208
  registerDocumentRoutes(app, routeDeps);
1238
2209
  registerKnowledgeRoutes(app, routeDeps);
1239
2210
  registerGitHubRoutes(app, routeDeps);
1240
2211
  registerInstallRoutes(app, routeDeps);
2212
+ registerDesktopDistributionRoutes(app, routeDeps);
1241
2213
  registerInteractionResourceRoutes(app, routeDeps);
1242
2214
  registerWorkspaceRoutes(app, routeDeps);
2215
+ registerUsageAllowanceRoutes(app, routeDeps);
1243
2216
  registerInsightsRoutes(app, routeDeps);
2217
+ registerInsightsUsageRoutes(app, routeDeps);
1244
2218
  registerWorkspaceInstructionPolicyRoutes(app, routeDeps);
1245
2219
  registerWorkspaceLearningRoutes(app, routeDeps);
1246
2220
  registerCompanyProfileRoutes(app, routeDeps);
1247
2221
  registerCompanyBrainRoutes(app, routeDeps);
1248
2222
  registerSlackTaskPolicyRoutes(app, routeDeps);
2223
+ registerSlackBotAccessRoutes(app, routeDeps);
1249
2224
  registerWorkspaceStateRoutes(app, routeDeps);
1250
2225
  registerMemorySlackPublicationRoutes(app, routeDeps);
1251
2226
  registerWorkspaceArtifactRoutes(app, routeDeps);
2227
+ registerSiteDataRoutes(app, routeDeps);
2228
+ registerSitePublishingRoutes(app, routeDeps);
2229
+ registerSiteRuntimeRoutes(app, routeDeps);
1252
2230
  registerArtifactCatalogRoutes(app, routeDeps);
2231
+ registerSiteSqlRoutes(app, routeDeps);
1253
2232
  registerPreferenceRegistryRoutes(app, routeDeps);
1254
2233
  registerSocialRoutes(app, routeDeps);
1255
2234
  registerPersonalGitHubRoutes(app, routeDeps);
1256
2235
  registerPersonalGitHubGitBrokerRoutes(app, routeDeps);
1257
2236
  registerConnectionRoutes(app, routeDeps);
2237
+ registerVaultProviderRoutes(app, routeDeps);
1258
2238
  registerConnectRoutes(app, routeDeps);
1259
- registerHostMcpBindingRoutes(app, routeDeps);
1260
- registerHostMcpResolverRoutes(app, routeDeps);
2239
+
1261
2240
  registerExternalIdentityLinkRoutes(app, routeDeps);
1262
2241
  registerCapabilityRoutes(app, routeDeps);
1263
2242
  registerApiIntegrationRoutes(app, routeDeps);
@@ -1265,22 +2244,38 @@ export function createAppComposition(deps: AppDependencies): {
1265
2244
  registerCatalogAssetRoutes(app, routeDeps);
1266
2245
  registerEnrollmentRoutes(app, routeDeps);
1267
2246
  registerMachineRoutes(app, routeDeps);
1268
- registerEnvironmentRoutes(app, routeDeps);
2247
+ registerVariableSetsRemovedRoutes(app);
2248
+ registerVaultRoutes(app, routeDeps);
1269
2249
  registerChannelRoutes(app, routeDeps);
1270
2250
  registerRigRoutes(app, routeDeps);
1271
- registerPackRoutes(app, routeDeps);
1272
2251
  registerAutomationRoutes(app, routeDeps);
1273
2252
  registerPrReviewRoutes(app, routeDeps);
1274
2253
  registerPluginRoutes(app, routeDeps);
1275
2254
  registerSkillRoutes(app, routeDeps);
2255
+ // The inbox's per-session mute routes call the session seam themselves, so
2256
+ // they register before the session module's fail-closed middleware, which
2257
+ // only classifies the session module's own paths.
2258
+ registerInboxRoutes(app, routeDeps);
1276
2259
  registerSessionRoutes(app, routeDeps);
2260
+ registerSessionHistoryImportRoutes(app, routeDeps);
1277
2261
  registerFeedbackRoutes(app, routeDeps);
2262
+ registerWorkspaceIntegrationRoutes(app, routeDeps);
2263
+ registerOrganizationIntegrationRoutes(app, routeDeps);
1278
2264
  registerScheduledTaskRoutes(app, routeDeps);
1279
2265
  registerCodexRoutes(app, routeDeps);
1280
2266
  registerOrganizationModelProviderRoutes(app, routeDeps);
2267
+ registerOrganizationModelDefaultsRoutes(app, routeDeps);
2268
+ registerModelAccountSettingsRoutes(app, routeDeps);
2269
+ registerModelAccountChoiceRoutes(app, routeDeps);
2270
+ registerSessionAdminAccessRoutes(app, routeDeps);
2271
+ registerWorkspaceModelProviderRoutes(app, routeDeps);
2272
+ registerClaudeSubscriptionOAuthRoutes(app, routeDeps);
2273
+ registerOrganizationIntegrationPolicyRoutes(app, routeDeps);
1281
2274
  registerModelConnectionAccessRoutes(app, routeDeps);
1282
2275
  registerSuperGrokRoutes(app, routeDeps);
2276
+ registerClaudeSubscriptionAccountRoutes(app, routeDeps);
1283
2277
  registerTranscriptionRoutes(app, routeDeps);
2278
+ registerChatCompletionRoutes(app, { ...routeDeps, modelCalls });
1284
2279
  registerEditableArtifactRoutes(app, routeDeps);
1285
2280
  registerVideoGenerationRoutes(app, routeDeps);
1286
2281
  registerCanonicalHumanIdentityRoutes(app, routeDeps);
@@ -1288,12 +2283,11 @@ export function createAppComposition(deps: AppDependencies): {
1288
2283
  registerOrganizationSessionRoutes(app, routeDeps);
1289
2284
  registerOrganizationRecoveryRoutes(app, routeDeps);
1290
2285
  registerUserResourceAuthorityRoutes(app, routeDeps);
1291
- registerConnectionAuthorityRoutes(app, routeDeps);
1292
2286
  registerSlackInteractionRoutes(app, routeDeps);
1293
2287
 
1294
2288
  app.notFound((c) => {
1295
2289
  if (!new URL(c.req.url).pathname.startsWith("/v1/")) return c.text("Not Found", 404);
1296
- const requestId = correlationIds.get(c.req.raw) ?? crypto.randomUUID();
2290
+ const requestId = correlationIds.get(c) ?? crypto.randomUUID();
1297
2291
  return c.json(
1298
2292
  ErrorEnvelope.parse({
1299
2293
  error: {
@@ -1309,19 +2303,59 @@ export function createAppComposition(deps: AppDependencies): {
1309
2303
  });
1310
2304
 
1311
2305
  app.onError((rawError, c) => {
2306
+ const requestId = correlationIds.get(c) ?? crypto.randomUUID();
2307
+ c.header(OPENGENI_CORRELATION_HEADER, requestId);
2308
+ if (new URL(c.req.url).pathname.startsWith("/v1/")) {
2309
+ c.header(OPENGENI_API_CONTRACT_HEADER, OPENGENI_API_CONTRACT_REVISION);
2310
+ }
2311
+ // Archives remain read-only through the existing Send/Steer/control paths.
2312
+ const importErrorResponse = archivedSessionImportErrorResponse(c, rawError);
2313
+ if (importErrorResponse) return importErrorResponse;
2314
+ const removedResponse = variableSetsRemovedErrorResponse(c, rawError);
2315
+ if (removedResponse) return removedResponse;
1312
2316
  // One central mapping for every Send/Steer/control route: a bounded
1313
2317
  // control-prefix wait that expired is a known, retryable, not-applied 503.
1314
- const error = workspaceControlBusyHttpError(rawError) ?? rawError;
2318
+ const error =
2319
+ rawError instanceof OrganizationIntegrationDeniedError
2320
+ ? new HTTPException(403, { message: rawError.message })
2321
+ : (sessionUsageLimitHttpError(rawError) ??
2322
+ allowanceExhaustedHttpError(rawError) ??
2323
+ subscriptionSettingsLockedHttpError(rawError) ??
2324
+ workspaceControlBusyHttpError(rawError) ??
2325
+ scheduledTaskTargetAccessHttpError(rawError) ??
2326
+ agentConfigHttpError(rawError) ??
2327
+ modelUnavailableHttpError(rawError) ??
2328
+ (rawError instanceof UnsupportedLatencyModeError
2329
+ ? new ApiHttpError(422, {
2330
+ code: "validation_failed",
2331
+ message: rawError.message,
2332
+ retryable: false,
2333
+ outcomeUnknown: false,
2334
+ details: {
2335
+ code: rawError.code,
2336
+ modelId: rawError.modelId,
2337
+ latencyMode: rawError.latencyMode,
2338
+ allowedLatencyModes: [...rawError.allowedLatencyModes],
2339
+ },
2340
+ })
2341
+ : null) ??
2342
+ requestBodyValidationHttpError(rawError) ??
2343
+ invalidPathIdentifierHttpError(rawError, new URL(c.req.url).pathname) ??
2344
+ databaseUnavailableHttpError(rawError, c.req.method) ??
2345
+ databaseContentionHttpError(rawError, c.req.method) ??
2346
+ rawError);
1315
2347
  const compactionLock = codexCompactionV2ProviderLockedError(error);
1316
2348
  const apiError = error instanceof ApiHttpError ? error : null;
1317
2349
  const status = compactionLock ? 422 : httpStatusForError(error);
1318
2350
  const code: ErrorCode = compactionLock
1319
2351
  ? compactionLock.code
1320
2352
  : (apiError?.code ?? errorCodeForStatus(status));
1321
- const requestId = correlationIds.get(c.req.raw) ?? crypto.randomUUID();
1322
- c.header(OPENGENI_CORRELATION_HEADER, requestId);
1323
- if (new URL(c.req.url).pathname.startsWith("/v1/")) {
1324
- c.header(OPENGENI_API_CONTRACT_HEADER, OPENGENI_API_CONTRACT_REVISION);
2353
+ if (status >= 500) logHttpFailure(c, status, code, rawError);
2354
+ if (
2355
+ apiError?.details?.code === DATABASE_UNAVAILABLE_ERROR_DETAIL_CODE ||
2356
+ apiError?.details?.code === DATABASE_CONTENTION_ERROR_DETAIL_CODE
2357
+ ) {
2358
+ c.header("retry-after", "1");
1325
2359
  }
1326
2360
  const envelope = ErrorEnvelope.parse({
1327
2361
  error: {
@@ -1346,6 +2380,14 @@ export function createAppComposition(deps: AppDependencies): {
1346
2380
  return { app, routeDeps };
1347
2381
  }
1348
2382
 
2383
+ function rejectionLogAttributes(rejection: HttpRejectionFacts) {
2384
+ return {
2385
+ rejectionCode: rejection.code,
2386
+ rejectionReason: rejection.reason,
2387
+ ...(rejection.fingerprint ? { rejectionFingerprint: rejection.fingerprint } : {}),
2388
+ };
2389
+ }
2390
+
1349
2391
  function managedAuthOAuthCallbackProvider(pathname: string): "google" | "github" | null {
1350
2392
  const match = pathname.match(/^\/v1\/auth\/callback\/(google|github)$/u);
1351
2393
  return match?.[1] === "google" || match?.[1] === "github" ? match[1] : null;
@@ -1381,6 +2423,36 @@ export function workspaceActorContextExempt(method: string, pathname: string): b
1381
2423
  // Hono's trailing wildcard also matches it, but "external" is not a workspace UUID;
1382
2424
  // the route performs its own organization API-key authorization.
1383
2425
  if (method === "PUT" && pathname === "/v1/workspaces/external") return true;
2426
+ // Import adapters apply their own exact create/control gates and request-local
2427
+ // RLS in core. Tenant mirrors first resolve the organization-local mapping;
2428
+ // "external" itself is not a native workspace id.
2429
+ if (isSessionHistoryImportRequest(method, pathname)) return true;
2430
+ // Organization budget authority is independent of ordinary workspace
2431
+ // membership; these routes apply their own exact account/workspace gate.
2432
+ if (
2433
+ (["GET", "PUT", "DELETE"].includes(method) &&
2434
+ /^\/v1\/workspaces\/[^/]+\/allowance$/.test(pathname)) ||
2435
+ (method === "GET" && /^\/v1\/workspaces\/[^/]+\/allowance\/state$/.test(pathname)) ||
2436
+ (method === "POST" && /^\/v1\/workspaces\/[^/]+\/allowance\/grants$/.test(pathname))
2437
+ )
2438
+ return true;
2439
+ // Allowance tenant mirrors resolve the workspace by exact organization-local
2440
+ // external identity before applying the ordinary workspace authorization.
2441
+ if (
2442
+ (["GET", "PUT", "DELETE"].includes(method) &&
2443
+ /^\/v1\/workspaces\/external\/[^/]+\/[^/]+\/allowance$/.test(pathname)) ||
2444
+ (method === "GET" &&
2445
+ /^\/v1\/workspaces\/external\/[^/]+\/[^/]+\/allowance\/state$/.test(pathname)) ||
2446
+ (method === "POST" &&
2447
+ /^\/v1\/workspaces\/external\/[^/]+\/[^/]+\/allowance\/grants$/.test(pathname)) ||
2448
+ (method === "GET" &&
2449
+ /^\/v1\/workspaces\/external\/[^/]+\/[^/]+\/usage(?:\/me)?$/.test(pathname)) ||
2450
+ (method === "PUT" &&
2451
+ /^\/v1\/workspaces\/external\/[^/]+\/[^/]+\/members\/(?:[^/]+|external\/[^/]+\/[^/]+)\/allowance$/.test(
2452
+ pathname,
2453
+ ))
2454
+ )
2455
+ return true;
1384
2456
  if (/^\/v1\/workspaces\/[^/]+\/mcp(?:\/(?:docs|files))?$/.test(pathname)) return true;
1385
2457
  if (
1386
2458
  method === "GET" &&
@@ -1434,12 +2506,13 @@ async function requireMcpAccessGrantAuthorization(
1434
2506
  return authorization;
1435
2507
  }
1436
2508
 
1437
- function clientAuthConfig(settings: AppDependencies["settings"]) {
2509
+ function clientAuthConfig(settings: AppDependencies["settings"], newSignupsEnabled: boolean) {
1438
2510
  if (settings.productAccessMode === "managed") {
1439
2511
  return {
1440
2512
  mode: "managedSession" as const,
1441
2513
  session: "cookie" as const,
1442
2514
  emailVerificationRequired: settings.environment !== "local",
2515
+ newSignupsEnabled,
1443
2516
  socialProviders: [
1444
2517
  ...(settings.managedAuthGoogleClientId && settings.managedAuthGoogleClientSecret
1445
2518
  ? (["google"] as const)
@@ -1466,11 +2539,151 @@ function clientAuthConfig(settings: AppDependencies["settings"]) {
1466
2539
  return { mode: "none" as const };
1467
2540
  }
1468
2541
 
2542
+ /** Configured SDK credentials cannot regain tools from widened legacy columns. */
2543
+ export function configuredCodemodeSessionProxyTools(
2544
+ settings: FirstPartyMcpToolPolicySettings,
2545
+ session: Pick<Session, "agent">,
2546
+ ): FirstPartyMcpToolName[] | null {
2547
+ if (!session.agent) return null;
2548
+ const families = resolveAgentToolFamilies(session.agent);
2549
+ return resolveFirstPartyMcpToolPolicy(settings).allowed.filter((tool) =>
2550
+ families.allowsFirstPartyTool(tool),
2551
+ );
2552
+ }
2553
+
2554
+ /**
2555
+ * Permissions span capabilities: project_create grants sessions:create,
2556
+ * and runtime titling grants sessions:control. Fence the endpoint family
2557
+ * too, without changing null-config routing or ordinary REST authorization.
2558
+ */
2559
+ export function assertConfiguredCodemodeSessionProxyPath(
2560
+ session: Pick<Session, "id" | "agent">,
2561
+ path: string,
2562
+ method: string,
2563
+ ): void {
2564
+ if (!session.agent) return;
2565
+ const families = resolveAgentToolFamilies(session.agent);
2566
+ const pathname = new URL(path, "http://codemode.invalid").pathname;
2567
+ const segments = pathname.split("/").filter(Boolean);
2568
+ const verb = method.toUpperCase();
2569
+ const read = verb === "GET" || verb === "HEAD";
2570
+ const allows = (tool: FirstPartyMcpToolName) => families.allowsFirstPartyTool(tool);
2571
+ let allowed = false;
2572
+ if (pathname === "/v1/config/client") {
2573
+ allowed = read;
2574
+ } else if (segments[0] === "v1" && segments[1] === "workspaces" && segments[2] === "site-host") {
2575
+ const [surface, targetSessionId, operation] = segments.slice(3);
2576
+ if (!surface) {
2577
+ allowed = read;
2578
+ } else if (surface === "sessions") {
2579
+ const ownSession = targetSessionId === session.id;
2580
+ const targetAllowed = ownSession || families.subagents;
2581
+ if (!targetSessionId) {
2582
+ allowed = families.subagents;
2583
+ } else if (ownSession && !operation && read) {
2584
+ allowed = true;
2585
+ } else if (ownSession && !operation && verb === "PATCH") {
2586
+ allowed = allows("set_session_title");
2587
+ } else if (ownSession && operation === "background-commands") {
2588
+ allowed = read && allows("command_read");
2589
+ } else if (operation === "goal") {
2590
+ allowed = allows("goal_set") && targetAllowed;
2591
+ } else if (operation === "browser" || operation === "computer") {
2592
+ allowed = allows("browser_open") && targetAllowed;
2593
+ } else if (operation === "human-input-requests") {
2594
+ allowed = families.humanInput && targetAllowed;
2595
+ } else if (operation === "artifacts") {
2596
+ allowed = allows("artifacts_list") && targetAllowed;
2597
+ } else if (ownSession && ["fs", "git", "terminal", "workspace"].includes(operation ?? "")) {
2598
+ // Resource-derived sandbox mechanics are not platform capabilities.
2599
+ allowed = true;
2600
+ } else if (
2601
+ [
2602
+ "agent",
2603
+ "tool-policy",
2604
+ "mcp-servers",
2605
+ "mcp-credentials",
2606
+ "variable-sets",
2607
+ "project",
2608
+ ].includes(operation ?? "")
2609
+ ) {
2610
+ allowed = allows("project_get") && targetAllowed;
2611
+ } else {
2612
+ allowed = families.subagents;
2613
+ }
2614
+ } else if (surface === "connect") {
2615
+ allowed = allows("custom_mcp_setup_request") && isPreparedMcpConnectPath(pathname, verb);
2616
+ } else if (surface === "files") {
2617
+ allowed = families.allowsMcpServer("files");
2618
+ } else if (surface === "skills") {
2619
+ allowed = families.skills === "manage" || (families.skills === "read" && read);
2620
+ } else if (surface === "human-input-requests") {
2621
+ allowed = families.humanInput;
2622
+ } else {
2623
+ const owners: Record<string, FirstPartyMcpToolName> = {
2624
+ "model-catalog": "sessions_list",
2625
+ "realtime-model-catalog": "sessions_list",
2626
+ "model-policy": "sessions_list",
2627
+ "gateway-custom-models": "sessions_list",
2628
+ "openrouter-custom-models": "sessions_list",
2629
+ "opper-custom-models": "sessions_list",
2630
+ "new-session-draft": "session_create",
2631
+ "session-tenancy": "session_create",
2632
+ "session-message-search": "session_events",
2633
+ "agent-topology": "sessions_list",
2634
+ projects: "project_get",
2635
+ sandboxes: "sandboxes_list",
2636
+ machines: "sandboxes_list",
2637
+ rigs: "rig_list",
2638
+ environments: "environment_list",
2639
+ "variable-sets": "variable_set_list",
2640
+ capabilities: "capability_catalog_search",
2641
+ catalog: "capability_catalog_search",
2642
+ members: "project_get",
2643
+ "member-candidates": "project_get",
2644
+ "control-events": "project_get",
2645
+ "browser-identities": "browser_identity",
2646
+ "browser-sessions": "browser_open",
2647
+ "computer-sessions": "computer_open",
2648
+ "attached-browsers": "browser_open",
2649
+ documents: "knowledge_search",
2650
+ knowledge: "knowledge_search",
2651
+ memory: "knowledge_search",
2652
+ "instruction-policy": "instruction_policy_get",
2653
+ preferences: "preference_registry_get",
2654
+ "company-profile": "company_profile_propose",
2655
+ artifacts: "artifacts_list",
2656
+ "scheduled-tasks": "scheduled_tasks_list",
2657
+ automations: "scheduled_tasks_list",
2658
+ connections: "github_connect_link",
2659
+ integrations: "github_connect_link",
2660
+ github: "github_connect_link",
2661
+ social: "social_connections_list",
2662
+ mcp: "github_connect_link",
2663
+ };
2664
+ const tool = owners[surface];
2665
+ allowed = tool !== undefined && allows(tool);
2666
+ }
2667
+ }
2668
+ if (!allowed) {
2669
+ throw new HTTPException(403, {
2670
+ message: "Agent configuration does not allow this Codemode SDK operation",
2671
+ });
2672
+ }
2673
+ }
2674
+
1469
2675
  function codemodeHttpError(error: unknown): HTTPException {
1470
- if (error instanceof SiteSessionPathError) {
1471
- // The proxied Site/SDK surface is an explicit allowlist; a route outside
1472
- // it (tool policy, visibility, forks, Steer, control, ...) does not exist
1473
- // for this caller rather than being a server fault.
2676
+ // The journal admission is one idempotent transaction keyed by the
2677
+ // caller-owned operation id and is already retried in-process; every other
2678
+ // step is a read. An exhausted 40P01/40001 victim therefore committed
2679
+ // nothing and the client may resubmit the same operation id.
2680
+ const contention = databaseContentionHttpError(error, "POST", { outcomeUnknown: false });
2681
+ if (contention) return contention;
2682
+ if (error instanceof SiteSessionPathError || error instanceof SiteDataUnsupportedError) {
2683
+ // Site frame requests are held to the SITE_OPERATIONS allowlist and the
2684
+ // agent's own SDK requests to its workspace; a route outside them (or Site
2685
+ // Data on a host without a published Site) does not exist for this
2686
+ // caller rather than being a server fault.
1474
2687
  return new HTTPException(404, { message: error.message, cause: error });
1475
2688
  }
1476
2689
  if (error instanceof SessionAuthorizationDeniedError) {
@@ -1527,6 +2740,9 @@ function codemodeHttpError(error: unknown): HTTPException {
1527
2740
  if (error instanceof CodemodePayloadTooLargeError) {
1528
2741
  return new HTTPException(413, { message: error.message, cause: error });
1529
2742
  }
2743
+ if (error instanceof CodemodeOperationLimitError) {
2744
+ return new HTTPException(429, { message: error.message, cause: error });
2745
+ }
1530
2746
  return error instanceof HTTPException
1531
2747
  ? error
1532
2748
  : new HTTPException(500, {
@@ -1560,6 +2776,15 @@ function clientAnalyticsConfig(settings: AppDependencies["settings"]) {
1560
2776
  };
1561
2777
  }
1562
2778
 
2779
+ function clientLegalConfig(settings: AppDependencies["settings"]) {
2780
+ return {
2781
+ ...(settings.legalPrivacyPolicyUrl ? { privacyPolicyUrl: settings.legalPrivacyPolicyUrl } : {}),
2782
+ ...(settings.legalTermsOfServiceUrl
2783
+ ? { termsOfServiceUrl: settings.legalTermsOfServiceUrl }
2784
+ : {}),
2785
+ };
2786
+ }
2787
+
1563
2788
  function structuredServicesHint(backend: string): {
1564
2789
  fileSystem: boolean;
1565
2790
  git: boolean;
@@ -1591,6 +2816,9 @@ export function httpStatusForError(error: unknown): number {
1591
2816
  if (workspaceControlBusyHttpError(error)) {
1592
2817
  return 503;
1593
2818
  }
2819
+ if (isRequestBodyValidationError(error)) {
2820
+ return 400;
2821
+ }
1594
2822
  if (error instanceof HTTPException) {
1595
2823
  return error.status;
1596
2824
  }
@@ -1604,7 +2832,7 @@ export function errorCodeForStatus(status: number): ErrorCode {
1604
2832
  if (status === 401) return "unauthenticated";
1605
2833
  if (status === 402) return "payment_required";
1606
2834
  if (status === 403) return "forbidden";
1607
- if (status === 404) return "not_found";
2835
+ if (status === 404 || status === 405) return "not_found";
1608
2836
  if (status === 409) return "conflict";
1609
2837
  if (status === 413 || status === 422 || status === 400) return "validation_failed";
1610
2838
  if (status === 429) return "limit_exceeded";
@@ -1621,10 +2849,10 @@ function publicErrorMessage(error: unknown, status: number): string {
1621
2849
  return "Connection setup changed or is still in progress. Reload its current status before retrying.";
1622
2850
  if (error instanceof ConnectAttemptNotFoundError) return "Connection setup not found.";
1623
2851
  if (status === 502 || status === 503 || status === 504) {
1624
- return "OpenGeni is temporarily unavailable — retry.";
2852
+ return "Opengeni is temporarily unavailable — retry.";
1625
2853
  }
1626
2854
  if (status >= 500) {
1627
- return "OpenGeni could not complete the request.";
2855
+ return "Opengeni could not complete the request.";
1628
2856
  }
1629
2857
  if (error instanceof HTTPException) {
1630
2858
  return boundedPublicMessage(error.message) ?? "Request failed.";
@@ -1739,7 +2967,7 @@ const routeLabelPatterns: Array<{
1739
2967
  label: string | ((match: RegExpMatchArray) => string);
1740
2968
  }> = [
1741
2969
  {
1742
- pattern: /^\/\.well-known\/oauth-authorization-server$/,
2970
+ pattern: /^\/\.well-known\/oauth-authorization-server(?:\/.*)?$/,
1743
2971
  label: "/.well-known/oauth-authorization-server",
1744
2972
  },
1745
2973
  {
@@ -1754,6 +2982,21 @@ const routeLabelPatterns: Array<{
1754
2982
  },
1755
2983
  { pattern: /^\/healthz$/, label: "/healthz" },
1756
2984
  { pattern: /^\/readyz$/, label: "/readyz" },
2985
+ // Better Auth answers these behind one `/v1/auth/*` registration, so the
2986
+ // provider endpoints need an explicit closed set to stay distinguishable.
2987
+ {
2988
+ pattern:
2989
+ /^\/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)$/,
2990
+ label: (match) => `/v1/auth/${match[1]}`,
2991
+ },
2992
+ { pattern: /^\/v1\/auth\/reset-password\/[^/]+$/, label: "/v1/auth/reset-password/:token" },
2993
+ {
2994
+ pattern: /^\/v1\/auth\/callback\/([^/]+)$/,
2995
+ label: (match) =>
2996
+ match[1] === "google" || match[1] === "github"
2997
+ ? `/v1/auth/callback/${match[1]}`
2998
+ : "/v1/auth/callback/:providerId",
2999
+ },
1757
3000
  { pattern: /^\/traffic-readyz$/, label: "/traffic-readyz" },
1758
3001
  {
1759
3002
  pattern: /^\/v1\/workspaces\/[^/]+\/codex\/connect\/start$/,
@@ -1780,7 +3023,8 @@ const routeLabelPatterns: Array<{
1780
3023
  label: (match) => `/v1/organizations/:organizationId/codex/connect/${match[1]}`,
1781
3024
  },
1782
3025
  {
1783
- pattern: /^\/v1\/organizations\/[^/]+\/codex\/accounts\/[^/]+(?:\/activate)?$/,
3026
+ pattern:
3027
+ /^\/v1\/organizations\/[^/]+\/codex\/accounts\/[^/]+(?:\/(activate|allocator|extra-credits))?$/,
1784
3028
  label: "/v1/organizations/:organizationId/codex/accounts/:accountId",
1785
3029
  },
1786
3030
  {
@@ -1809,6 +3053,8 @@ const routeLabelPatterns: Array<{
1809
3053
  },
1810
3054
  { pattern: /^\/metrics$/, label: "/metrics" },
1811
3055
  { pattern: /^\/v1\/config\/client$/, label: "/v1/config/client" },
3056
+ { pattern: /^\/v1\/client-errors$/, label: "/v1/client-errors" },
3057
+ { pattern: /^\/v1\/analytics-consent$/, label: "/v1/analytics-consent" },
1812
3058
  { pattern: /^\/v1\/billing$/, label: "/v1/billing" },
1813
3059
  { pattern: /^\/v1\/billing\/checkout$/, label: "/v1/billing/checkout" },
1814
3060
  { pattern: /^\/v1\/billing\/usage$/, label: "/v1/billing/usage" },
@@ -1816,6 +3062,22 @@ const routeLabelPatterns: Array<{
1816
3062
  pattern: /^\/v1\/workspaces\/[^/]+\/insights$/,
1817
3063
  label: "/v1/workspaces/:workspaceId/insights",
1818
3064
  },
3065
+ {
3066
+ pattern: /^\/v1\/workspaces\/[^/]+\/insights\/usage$/,
3067
+ label: "/v1/workspaces/:workspaceId/insights/usage",
3068
+ },
3069
+ {
3070
+ pattern: /^\/v1\/workspaces\/[^/]+\/insights\/calls$/,
3071
+ label: "/v1/workspaces/:workspaceId/insights/calls",
3072
+ },
3073
+ {
3074
+ pattern: /^\/v1\/organizations\/[^/]+\/insights\/usage$/,
3075
+ label: "/v1/organizations/:accountId/insights/usage",
3076
+ },
3077
+ {
3078
+ pattern: /^\/v1\/organizations\/[^/]+\/insights\/calls$/,
3079
+ label: "/v1/organizations/:accountId/insights/calls",
3080
+ },
1819
3081
  {
1820
3082
  pattern: /^\/v1\/billing\/entitlements$/,
1821
3083
  label: "/v1/billing/entitlements",
@@ -1841,6 +3103,10 @@ const routeLabelPatterns: Array<{
1841
3103
  pattern: /^\/v1\/workspaces\/[^/]+\/tools\/catalog$/,
1842
3104
  label: "/v1/workspaces/:workspaceId/tools/catalog",
1843
3105
  },
3106
+ {
3107
+ pattern: /^\/v1\/workspaces\/[^/]+\/tools\/(resolve|invoke|target-approvals|manifest)$/,
3108
+ label: (match) => `/v1/workspaces/:workspaceId/tools/${match[1]}`,
3109
+ },
1844
3110
  {
1845
3111
  pattern: /^\/v1\/workspaces\/[^/]+\/tools\/calls$/,
1846
3112
  label: "/v1/workspaces/:workspaceId/tools/calls",
@@ -1861,6 +3127,18 @@ const routeLabelPatterns: Array<{
1861
3127
  pattern: /^\/v1\/workspaces\/[^/]+\/sessions$/,
1862
3128
  label: "/v1/workspaces/:workspaceId/sessions",
1863
3129
  },
3130
+ {
3131
+ pattern: /^\/v1\/workspaces\/[^/]+\/session-imports$/,
3132
+ label: "/v1/workspaces/:workspaceId/session-imports",
3133
+ },
3134
+ {
3135
+ pattern: /^\/v1\/workspaces\/[^/]+\/session-imports\/[^/]+\/events$/,
3136
+ label: "/v1/workspaces/:workspaceId/session-imports/:importId/events",
3137
+ },
3138
+ {
3139
+ pattern: /^\/v1\/workspaces\/[^/]+\/session-message-search$/,
3140
+ label: "/v1/workspaces/:workspaceId/session-message-search",
3141
+ },
1864
3142
  {
1865
3143
  pattern: /^\/v1\/workspaces\/[^/]+\/control-events\/stream$/,
1866
3144
  label: "/v1/workspaces/:workspaceId/control-events/stream",
@@ -1914,7 +3192,7 @@ const routeLabelPatterns: Array<{
1914
3192
  },
1915
3193
  {
1916
3194
  pattern:
1917
- /^\/v1\/workspaces\/[^/]+\/browser-sessions\/[^/]+\/auth-runs\/[^/]+\/(external-auth|protected-fill|report|verify)$/,
3195
+ /^\/v1\/workspaces\/[^/]+\/browser-sessions\/[^/]+\/auth-runs\/[^/]+\/(external-auth|report|verify)$/,
1918
3196
  label: (match) =>
1919
3197
  `/v1/workspaces/:workspaceId/browser-sessions/:browserSessionId/auth-runs/:authRunId/${match[1]}`,
1920
3198
  },
@@ -1970,7 +3248,7 @@ const routeLabelPatterns: Array<{
1970
3248
  },
1971
3249
  {
1972
3250
  pattern:
1973
- /^\/v1\/workspaces\/[^/]+\/computer-sessions\/[^/]+\/(actions|attachments|clipboard|end|heartbeat|targets)$/,
3251
+ /^\/v1\/workspaces\/[^/]+\/computer-sessions\/[^/]+\/(actions|native-calls|attachments|clipboard|end|heartbeat|targets)$/,
1974
3252
  label: (match) =>
1975
3253
  `/v1/workspaces/:workspaceId/computer-sessions/:computerSessionId/${match[1]}`,
1976
3254
  },
@@ -2155,6 +3433,72 @@ const routeLabelPatterns: Array<{
2155
3433
  pattern: /^\/v1\/workspaces\/external$/,
2156
3434
  label: "/v1/workspaces/external",
2157
3435
  },
3436
+ {
3437
+ pattern: /^\/v1\/workspaces\/external\/[^/]+\/[^/]+\/session-imports$/,
3438
+ label: "/v1/workspaces/external/:source/:externalId/session-imports",
3439
+ },
3440
+ {
3441
+ pattern: /^\/v1\/workspaces\/external\/[^/]+\/[^/]+\/session-imports\/[^/]+\/events$/,
3442
+ label: "/v1/workspaces/external/:source/:externalId/session-imports/:importId/events",
3443
+ },
3444
+ {
3445
+ pattern: /^\/v1\/workspaces\/external\/[^/]+\/[^/]+\/allowance\/grants$/,
3446
+ label: "/v1/workspaces/external/:source/:externalId/allowance/grants",
3447
+ },
3448
+ {
3449
+ pattern: /^\/v1\/workspaces\/external\/[^/]+\/[^/]+\/allowance$/,
3450
+ label: "/v1/workspaces/external/:source/:externalId/allowance",
3451
+ },
3452
+ {
3453
+ pattern: /^\/v1\/workspaces\/external\/[^/]+\/[^/]+\/allowance\/state$/,
3454
+ label: "/v1/workspaces/external/:source/:externalId/allowance/state",
3455
+ },
3456
+ {
3457
+ pattern: /^\/v1\/workspaces\/external\/[^/]+\/[^/]+\/usage\/me$/,
3458
+ label: "/v1/workspaces/external/:source/:externalId/usage/me",
3459
+ },
3460
+ {
3461
+ pattern: /^\/v1\/workspaces\/external\/[^/]+\/[^/]+\/usage$/,
3462
+ label: "/v1/workspaces/external/:source/:externalId/usage",
3463
+ },
3464
+ {
3465
+ pattern:
3466
+ /^\/v1\/workspaces\/external\/[^/]+\/[^/]+\/members\/external\/[^/]+\/[^/]+\/allowance$/,
3467
+ label:
3468
+ "/v1/workspaces/external/:source/:externalId/members/external/:memberSource/:memberExternalId/allowance",
3469
+ },
3470
+ {
3471
+ pattern: /^\/v1\/workspaces\/external\/[^/]+\/[^/]+\/members\/[^/]+\/allowance$/,
3472
+ label: "/v1/workspaces/external/:source/:externalId/members/:subjectId/allowance",
3473
+ },
3474
+ {
3475
+ pattern: /^\/v1\/workspaces\/[^/]+\/allowance\/grants$/,
3476
+ label: "/v1/workspaces/:workspaceId/allowance/grants",
3477
+ },
3478
+ {
3479
+ pattern: /^\/v1\/workspaces\/[^/]+\/allowance$/,
3480
+ label: "/v1/workspaces/:workspaceId/allowance",
3481
+ },
3482
+ {
3483
+ pattern: /^\/v1\/workspaces\/[^/]+\/allowance\/state$/,
3484
+ label: "/v1/workspaces/:workspaceId/allowance/state",
3485
+ },
3486
+ {
3487
+ pattern: /^\/v1\/workspaces\/[^/]+\/usage\/me$/,
3488
+ label: "/v1/workspaces/:workspaceId/usage/me",
3489
+ },
3490
+ {
3491
+ pattern: /^\/v1\/workspaces\/[^/]+\/usage$/,
3492
+ label: "/v1/workspaces/:workspaceId/usage",
3493
+ },
3494
+ {
3495
+ pattern: /^\/v1\/workspaces\/[^/]+\/members\/external\/[^/]+\/[^/]+\/allowance$/,
3496
+ label: "/v1/workspaces/:workspaceId/members/external/:source/:externalId/allowance",
3497
+ },
3498
+ {
3499
+ pattern: /^\/v1\/workspaces\/[^/]+\/members\/[^/]+\/allowance$/,
3500
+ label: "/v1/workspaces/:workspaceId/members/:subjectId/allowance",
3501
+ },
2158
3502
  {
2159
3503
  pattern: /^\/v1\/workspaces\/[^/]+\/scheduled-tasks$/,
2160
3504
  label: "/v1/workspaces/:workspaceId/scheduled-tasks",
@@ -2373,26 +3717,7 @@ const routeLabelPatterns: Array<{
2373
3717
  pattern: /^\/v1\/workspaces\/[^/]+\/environments\/[^/]+$/,
2374
3718
  label: "/v1/workspaces/:workspaceId/environments/:id",
2375
3719
  },
2376
- {
2377
- pattern: /^\/v1\/workspaces\/[^/]+\/packs$/,
2378
- label: "/v1/workspaces/:workspaceId/packs",
2379
- },
2380
- {
2381
- pattern: /^\/v1\/workspaces\/[^/]+\/packs\/installations$/,
2382
- label: "/v1/workspaces/:workspaceId/packs/installations",
2383
- },
2384
- {
2385
- pattern: /^\/v1\/workspaces\/[^/]+\/packs\/marketing-social-daily-analysis\/scheduled-tasks$/,
2386
- label: "/v1/workspaces/:workspaceId/packs/marketing-social-daily-analysis/scheduled-tasks",
2387
- },
2388
- {
2389
- pattern: /^\/v1\/workspaces\/[^/]+\/packs\/[^/]+\/enable$/,
2390
- label: "/v1/workspaces/:workspaceId/packs/:id/enable",
2391
- },
2392
- {
2393
- pattern: /^\/v1\/workspaces\/[^/]+\/packs\/[^/]+$/,
2394
- label: "/v1/workspaces/:workspaceId/packs/:id",
2395
- },
3720
+
2396
3721
  {
2397
3722
  pattern: /^\/v1\/workspaces\/[^/]+\/plugins\/preview$/,
2398
3723
  label: "/v1/workspaces/:workspaceId/plugins/preview",
@@ -2546,6 +3871,22 @@ const routeLabelPatterns: Array<{
2546
3871
  pattern: /^\/v1\/workspaces\/[^/]+\/machines\/[^/]+\/update$/,
2547
3872
  label: "/v1/workspaces/:workspaceId/machines/:enrollmentId/update",
2548
3873
  },
3874
+ {
3875
+ pattern: /^\/v1\/workspaces\/[^/]+\/machines\/[^/]+\/screen-control$/,
3876
+ label: "/v1/workspaces/:workspaceId/machines/:enrollmentId/screen-control",
3877
+ },
3878
+ {
3879
+ pattern: /^\/v1\/workspaces\/[^/]+\/machines\/[^/]+\/privacy-settings$/,
3880
+ label: "/v1/workspaces/:workspaceId/machines/:enrollmentId/privacy-settings",
3881
+ },
3882
+ {
3883
+ pattern: /^\/v1\/workspaces\/[^/]+\/machines\/[^/]+\/audience\/preview$/,
3884
+ label: "/v1/workspaces/:workspaceId/machines/:enrollmentId/audience/preview",
3885
+ },
3886
+ {
3887
+ pattern: /^\/v1\/workspaces\/[^/]+\/machines\/[^/]+\/audience$/,
3888
+ label: "/v1/workspaces/:workspaceId/machines/:enrollmentId/audience",
3889
+ },
2549
3890
  {
2550
3891
  pattern: /^\/v1\/workspaces\/[^/]+\/machines$/,
2551
3892
  label: "/v1/workspaces/:workspaceId/machines",
@@ -2582,7 +3923,13 @@ const routeLabelPatterns: Array<{
2582
3923
  },
2583
3924
  ];
2584
3925
 
2585
- export function routeLabel(pathname: string): string {
3926
+ /**
3927
+ * Bounded route label for metrics, spans, and request logs. Explicit patterns
3928
+ * keep established label spellings stable; any other request answered by a
3929
+ * registered handler uses that handler's code-owned path template (see
3930
+ * `registeredHandlerRoutePath`). Only unregistered paths fall into `unknown`.
3931
+ */
3932
+ export function routeLabel(pathname: string, registeredRoutePath?: string | null): string {
2586
3933
  if (/^\/v1\/workspaces\/[^/]+\/transcriptions$/.test(pathname))
2587
3934
  return "/v1/workspaces/:workspaceId/transcriptions";
2588
3935
  const transcription = pathname.match(
@@ -2603,11 +3950,61 @@ export function routeLabel(pathname: string): string {
2603
3950
  return typeof candidate.label === "string" ? candidate.label : candidate.label(match);
2604
3951
  }
2605
3952
  }
3953
+ const registered = boundedRegisteredRouteLabel(registeredRoutePath);
3954
+ if (registered) return registered;
2606
3955
  return pathname.startsWith("/v1/") ? "/v1/unknown" : "/unknown";
2607
3956
  }
2608
3957
 
2609
3958
  /**
2610
- * State-changing OpenGeni HTTP calls must never cross an incompatible rollout
3959
+ * API contract revisions no longer admitted from ANY caller, including bearer
3960
+ * integrations that would otherwise be accepted across revisions. Add a
3961
+ * revision here only for a truly breaking wire change (which also requires a
3962
+ * major release-train change); ordinary revision bumps stay additive.
3963
+ */
3964
+ export const REFUSED_API_CONTRACT_REVISIONS: ReadonlySet<string> = new Set<string>([]);
3965
+
3966
+ const BEARER_AUTHORIZATION = /^bearer\s+\S/i;
3967
+
3968
+ /**
3969
+ * The rollout fence for state-changing product calls.
3970
+ *
3971
+ * The exact-revision check exists to stop a stale first-party browser tab
3972
+ * (cookie session, or unauthenticated local mode) from writing with an old
3973
+ * request shape after a deployment; it answers 409 so the page reloads onto
3974
+ * the matching bundle. That tab never sends `Authorization`, so this is not a
3975
+ * header a stale bundle can choose to bypass it with.
3976
+ *
3977
+ * Bearer-authenticated callers (organization/workspace API keys, delegated
3978
+ * tokens, the deployment key sent as a bearer) are integrations pinned to an
3979
+ * SDK version: reloading cannot upgrade them, and the API is additive within a
3980
+ * major release train. They are admitted with an older revision or with no
3981
+ * revision claim at all. Only a revision listed in
3982
+ * `REFUSED_API_CONTRACT_REVISIONS` is refused for them.
3983
+ *
3984
+ * This is a compatibility fence, not an authorization boundary: every route
3985
+ * still authenticates and authorizes the request independently.
3986
+ */
3987
+ export function apiContractAdmission(
3988
+ input: {
3989
+ method: string;
3990
+ pathname: string;
3991
+ authorization: string | undefined;
3992
+ claimedRevision: string | undefined;
3993
+ },
3994
+ refusedRevisions: ReadonlySet<string> = REFUSED_API_CONTRACT_REVISIONS,
3995
+ ): "admit" | "reject" {
3996
+ if (!isApiContractProtectedMutation(input.method, input.pathname)) return "admit";
3997
+ if (input.claimedRevision === OPENGENI_API_CONTRACT_REVISION) return "admit";
3998
+ if (input.claimedRevision !== undefined && refusedRevisions.has(input.claimedRevision)) {
3999
+ return "reject";
4000
+ }
4001
+ return input.authorization !== undefined && BEARER_AUTHORIZATION.test(input.authorization)
4002
+ ? "admit"
4003
+ : "reject";
4004
+ }
4005
+
4006
+ /**
4007
+ * State-changing Opengeni HTTP calls must never cross an incompatible rollout
2611
4008
  * boundary. Standard third-party protocols and externally initiated callbacks
2612
4009
  * are intentionally outside this product API contract.
2613
4010
  */
@@ -2633,12 +4030,47 @@ export function isApiContractProtectedMutation(method: string, pathname: string)
2633
4030
  pathname === "/v1/integrations/slack/commands" ||
2634
4031
  pathname === "/v1/integrations/slack/interactions" ||
2635
4032
  pathname.startsWith("/v1/github/") ||
4033
+ // A stale tab must still report the error that follows a rollout.
4034
+ pathname === CLIENT_ERRORS_PATH ||
4035
+ pathname === ANALYTICS_CONSENT_PATH ||
2636
4036
  pathname === "/v1/enrollments/device/start" ||
2637
4037
  pathname === "/v1/enrollments/device/poll" ||
2638
- pathname === "/v1/enrollments/token/exchange"
4038
+ pathname === "/v1/enrollments/token/exchange" ||
4039
+ // The runner authenticates this protocol with its install-key proof, not
4040
+ // a browser cookie or the product-client revision header.
4041
+ pathname === "/v1/enrollments/renew" ||
4042
+ // The Site gateway's signed protocol carries its own signature version (v1).
4043
+ pathname.startsWith("/v1/site-runtime/")
2639
4044
  ) {
2640
4045
  return false;
2641
4046
  }
2642
4047
  const segments = pathname.split("/");
2643
4048
  return !segments.includes("mcp") && !segments.includes("codemode");
2644
4049
  }
4050
+
4051
+ /** Published Sites deployment facts for the console; null while publishing is off. */
4052
+ function clientSitesConfig(settings: Settings): ClientSitesConfig | null {
4053
+ const domain = configuredSitesDomain(settings);
4054
+ if (!domain) return null;
4055
+ return {
4056
+ domain,
4057
+ publicEnabled: settings.sitesPublicEnabled,
4058
+ publicAgentsEnabled: settings.sitesPublicAgentsEnabled,
4059
+ loginOnly: settings.sitesLoginOnly,
4060
+ forceNoindex: settings.sitesForceNoindex,
4061
+ };
4062
+ }
4063
+
4064
+ /** Client-safe agent-configuration projection. */
4065
+ function clientAgentConfig(settings: Settings): ClientAgentConfig {
4066
+ const policy = agentConfigDeploymentPolicy(settings);
4067
+ return {
4068
+ // Deprecated: agent configuration is always on. Kept for client compatibility.
4069
+ enabled: true,
4070
+ defaultForNewSessions: true,
4071
+ capabilities: AGENT_CAPABILITY_IDS.map((id) => {
4072
+ const reason = policy.unavailable[id];
4073
+ return reason === undefined ? { id, available: true } : { id, available: false, reason };
4074
+ }),
4075
+ };
4076
+ }