codexLB-up 1.25.0b10__py3-none-any.whl

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 (827) hide show
  1. app/__init__.py +10 -0
  2. app/admin_cli.py +494 -0
  3. app/cli.py +339 -0
  4. app/codex_sessions_retag.py +487 -0
  5. app/core/__init__.py +0 -0
  6. app/core/audit/__init__.py +0 -0
  7. app/core/audit/service.py +216 -0
  8. app/core/audit/types.py +94 -0
  9. app/core/auth/__init__.py +236 -0
  10. app/core/auth/api_key_cache.py +65 -0
  11. app/core/auth/dashboard_access.py +429 -0
  12. app/core/auth/dashboard_mode.py +171 -0
  13. app/core/auth/dashboard_session_ttl.py +74 -0
  14. app/core/auth/dashboard_users_cache.py +120 -0
  15. app/core/auth/dependencies.py +710 -0
  16. app/core/auth/external_identity.py +78 -0
  17. app/core/auth/guardian.py +374 -0
  18. app/core/auth/models.py +49 -0
  19. app/core/auth/providers/__init__.py +129 -0
  20. app/core/auth/providers/oidc.py +838 -0
  21. app/core/auth/providers/registry.py +118 -0
  22. app/core/auth/refresh.py +394 -0
  23. app/core/auth/step_up.py +117 -0
  24. app/core/auth/totp.py +81 -0
  25. app/core/balancer/__init__.py +71 -0
  26. app/core/balancer/logic.py +1612 -0
  27. app/core/balancer/rendezvous_hash.py +21 -0
  28. app/core/balancer/types.py +21 -0
  29. app/core/bootstrap.py +172 -0
  30. app/core/cache/invalidation.py +426 -0
  31. app/core/clients/__init__.py +0 -0
  32. app/core/clients/codex.py +996 -0
  33. app/core/clients/codex_version.py +187 -0
  34. app/core/clients/codex_version_snapshot.py +2 -0
  35. app/core/clients/files.py +534 -0
  36. app/core/clients/headers.py +29 -0
  37. app/core/clients/http.py +511 -0
  38. app/core/clients/model_fetcher.py +276 -0
  39. app/core/clients/native_egress.py +1304 -0
  40. app/core/clients/oauth.py +477 -0
  41. app/core/clients/proxy.py +6089 -0
  42. app/core/clients/proxy_websocket.py +1411 -0
  43. app/core/clients/rate_limit_reset_credits.py +412 -0
  44. app/core/clients/stream_errors.py +12 -0
  45. app/core/clients/thread_cache_identity.py +347 -0
  46. app/core/clients/usage.py +467 -0
  47. app/core/clock.py +170 -0
  48. app/core/config/__init__.py +0 -0
  49. app/core/config/background_jobs.py +73 -0
  50. app/core/config/context_window_overrides.py +166 -0
  51. app/core/config/dashboard_overrides.py +145 -0
  52. app/core/config/inheritable.py +47 -0
  53. app/core/config/key_fingerprint.py +115 -0
  54. app/core/config/settings.py +872 -0
  55. app/core/config/settings_cache.py +100 -0
  56. app/core/config/spool_retention.py +217 -0
  57. app/core/config/tiers.py +174 -0
  58. app/core/conversation_archive.py +568 -0
  59. app/core/crypto.py +37 -0
  60. app/core/draining.py +11 -0
  61. app/core/errors.py +365 -0
  62. app/core/exceptions.py +155 -0
  63. app/core/handlers/__init__.py +3 -0
  64. app/core/handlers/exceptions.py +478 -0
  65. app/core/http_protocol.py +176 -0
  66. app/core/http_protocol_httptools.py +204 -0
  67. app/core/ingress_limits.py +17 -0
  68. app/core/metadata_store.py +18 -0
  69. app/core/metrics/__init__.py +0 -0
  70. app/core/metrics/middleware.py +74 -0
  71. app/core/metrics/prometheus.py +604 -0
  72. app/core/middleware/__init__.py +25 -0
  73. app/core/middleware/api_firewall.py +111 -0
  74. app/core/middleware/app_version.py +37 -0
  75. app/core/middleware/dashboard_auth_proxy.py +55 -0
  76. app/core/middleware/dashboard_csrf.py +177 -0
  77. app/core/middleware/dashboard_gzip.py +41 -0
  78. app/core/middleware/dashboard_overrides.py +59 -0
  79. app/core/middleware/firewall_cache.py +91 -0
  80. app/core/middleware/inflight.py +126 -0
  81. app/core/middleware/multipart_content_encoding.py +129 -0
  82. app/core/middleware/path_rewrite.py +148 -0
  83. app/core/middleware/request_body_limit.py +196 -0
  84. app/core/middleware/request_decompression.py +209 -0
  85. app/core/middleware/request_id.py +72 -0
  86. app/core/middleware/required_capability_http.py +116 -0
  87. app/core/middleware/trusted_proxy_headers.py +36 -0
  88. app/core/multipart.py +237 -0
  89. app/core/multipart_fields.py +91 -0
  90. app/core/openai/__init__.py +0 -0
  91. app/core/openai/chat_requests.py +493 -0
  92. app/core/openai/chat_responses.py +931 -0
  93. app/core/openai/contracts.py +46 -0
  94. app/core/openai/exceptions.py +24 -0
  95. app/core/openai/host_models.py +14 -0
  96. app/core/openai/images.py +413 -0
  97. app/core/openai/message_coercion.py +376 -0
  98. app/core/openai/model_refresh_scheduler.py +423 -0
  99. app/core/openai/model_registry.py +997 -0
  100. app/core/openai/model_registry_store.py +548 -0
  101. app/core/openai/models.py +172 -0
  102. app/core/openai/parsing.py +97 -0
  103. app/core/openai/requests.py +1980 -0
  104. app/core/openai/strict_schema.py +199 -0
  105. app/core/openai/tool_call_safety.py +99 -0
  106. app/core/openai/v1_requests.py +187 -0
  107. app/core/plan_types.py +90 -0
  108. app/core/prestop.py +202 -0
  109. app/core/rate_limiter/__init__.py +0 -0
  110. app/core/rate_limiter/db_rate_limiter.py +215 -0
  111. app/core/request_locality.py +473 -0
  112. app/core/resilience/__init__.py +0 -0
  113. app/core/resilience/backpressure.py +57 -0
  114. app/core/resilience/bulkhead.py +202 -0
  115. app/core/resilience/circuit_breaker.py +150 -0
  116. app/core/resilience/degradation.py +55 -0
  117. app/core/resilience/loop_lag_monitor.py +60 -0
  118. app/core/resilience/memory_monitor.py +137 -0
  119. app/core/resilience/network_recovery.py +256 -0
  120. app/core/resilience/overload.py +98 -0
  121. app/core/resilience/toggles.py +114 -0
  122. app/core/retention/__init__.py +0 -0
  123. app/core/retention/job.py +240 -0
  124. app/core/retention/scheduler.py +69 -0
  125. app/core/runtime_logging.py +601 -0
  126. app/core/scheduling/__init__.py +0 -0
  127. app/core/scheduling/leader_election.py +993 -0
  128. app/core/scheduling/leader_election_handle.py +25 -0
  129. app/core/server.py +140 -0
  130. app/core/shutdown.py +326 -0
  131. app/core/socket_peer.py +22 -0
  132. app/core/startup.py +41 -0
  133. app/core/timeout_invariants.py +347 -0
  134. app/core/tracing/__init__.py +0 -0
  135. app/core/tracing/otel.py +207 -0
  136. app/core/types.py +6 -0
  137. app/core/upstream_proxy/__init__.py +13 -0
  138. app/core/upstream_proxy/cache.py +130 -0
  139. app/core/upstream_proxy/resolver.py +189 -0
  140. app/core/upstream_proxy/types.py +104 -0
  141. app/core/usage/__init__.py +407 -0
  142. app/core/usage/depletion.py +109 -0
  143. app/core/usage/live_hub.py +41 -0
  144. app/core/usage/live_snapshots.py +174 -0
  145. app/core/usage/logs.py +212 -0
  146. app/core/usage/metadata_scheduler.py +118 -0
  147. app/core/usage/models.py +54 -0
  148. app/core/usage/pricing.py +576 -0
  149. app/core/usage/pricing_catalog.py +241 -0
  150. app/core/usage/pricing_snapshot.json +821 -0
  151. app/core/usage/quota.py +133 -0
  152. app/core/usage/refresh_policy.py +21 -0
  153. app/core/usage/refresh_scheduler.py +713 -0
  154. app/core/usage/reset_credits_refresh_scheduler.py +435 -0
  155. app/core/usage/types.py +168 -0
  156. app/core/utils/__init__.py +0 -0
  157. app/core/utils/json_guards.py +18 -0
  158. app/core/utils/locks.py +33 -0
  159. app/core/utils/masking.py +20 -0
  160. app/core/utils/proxy_env.py +104 -0
  161. app/core/utils/request_id.py +65 -0
  162. app/core/utils/retry.py +77 -0
  163. app/core/utils/shared_future.py +172 -0
  164. app/core/utils/sse.py +286 -0
  165. app/core/utils/time.py +24 -0
  166. app/db/__init__.py +0 -0
  167. app/db/account_identity_lock.py +56 -0
  168. app/db/alembic/__init__.py +1 -0
  169. app/db/alembic/env.py +70 -0
  170. app/db/alembic/revision_ids.py +43 -0
  171. app/db/alembic/versions/20260213_000000_base_schema.py +248 -0
  172. app/db/alembic/versions/20260213_000100_normalize_account_plan_types.py +45 -0
  173. app/db/alembic/versions/20260213_000200_add_request_logs_reasoning_effort.py +45 -0
  174. app/db/alembic/versions/20260213_000300_add_accounts_reset_at.py +45 -0
  175. app/db/alembic/versions/20260213_000400_add_accounts_chatgpt_account_id.py +45 -0
  176. app/db/alembic/versions/20260213_000500_add_dashboard_settings.py +70 -0
  177. app/db/alembic/versions/20260213_000600_add_dashboard_settings_totp.py +64 -0
  178. app/db/alembic/versions/20260213_000700_add_dashboard_settings_password.py +59 -0
  179. app/db/alembic/versions/20260213_000800_add_api_keys.py +87 -0
  180. app/db/alembic/versions/20260214_000000_add_api_key_limits.py +143 -0
  181. app/db/alembic/versions/20260215_000000_add_idx_logs_requested_at.py +38 -0
  182. app/db/alembic/versions/20260218_000000_add_api_key_usage_reservations.py +117 -0
  183. app/db/alembic/versions/20260218_000100_add_import_without_overwrite_and_drop_accounts_email_unique.py +198 -0
  184. app/db/alembic/versions/20260225_000000_add_dashboard_settings_routing_strategy.py +53 -0
  185. app/db/alembic/versions/20260228_020000_align_api_key_limit_enum_types.py +104 -0
  186. app/db/alembic/versions/20260228_030000_add_api_firewall_allowlist.py +42 -0
  187. app/db/alembic/versions/20260306_000000_add_request_logs_service_tier.py +45 -0
  188. app/db/alembic/versions/20260307_000000_add_api_key_enforcement_fields.py +56 -0
  189. app/db/alembic/versions/20260308_000000_add_sqlite_performance_indexes.py +41 -0
  190. app/db/alembic/versions/20260309_000000_add_additional_usage_history.py +71 -0
  191. app/db/alembic/versions/20260309_000000_request_logs_nullable_account_id.py +28 -0
  192. app/db/alembic/versions/20260310_000000_add_request_logs_transport.py +45 -0
  193. app/db/alembic/versions/20260310_000000_fix_postgresql_enum_value_casing.py +79 -0
  194. app/db/alembic/versions/20260310_120000_add_sticky_session_kinds_and_affinity_ttl.py +110 -0
  195. app/db/alembic/versions/20260312_000000_add_additional_usage_quota_key.py +193 -0
  196. app/db/alembic/versions/20260312_000000_split_sticky_sessions_primary_key_by_kind.py +96 -0
  197. app/db/alembic/versions/20260312_010000_merge_additional_usage_and_sticky_session_heads.py +25 -0
  198. app/db/alembic/versions/20260312_020000_merge_request_logs_transport_and_feature_heads.py +25 -0
  199. app/db/alembic/versions/20260312_020000_merge_request_logs_transport_and_sticky_heads.py +25 -0
  200. app/db/alembic/versions/20260312_120000_add_dashboard_upstream_stream_transport.py +62 -0
  201. app/db/alembic/versions/20260313_024500_merge_request_log_transport_heads.py +27 -0
  202. app/db/alembic/versions/20260319_100937_increase_prompt_cache_ttl_to_1800s.py +73 -0
  203. app/db/alembic/versions/20260319_111500_merge_prompt_cache_ttl_and_upstream_transport_heads.py +25 -0
  204. app/db/alembic/versions/20260319_183000_normalize_sqlite_account_status_casing.py +48 -0
  205. app/db/alembic/versions/20260320_000000_add_request_log_requested_actual_tiers.py +51 -0
  206. app/db/alembic/versions/20260321_190000_tighten_dashboard_db_indexes.py +64 -0
  207. app/db/alembic/versions/20260321_210000_merge_request_log_tiers_and_dashboard_index_heads.py +25 -0
  208. app/db/alembic/versions/20260325_000000_add_request_log_cost.py +193 -0
  209. app/db/alembic/versions/20260328_000000_add_rate_limit_attempts.py +48 -0
  210. app/db/alembic/versions/20260328_120000_add_request_log_ttft.py +37 -0
  211. app/db/alembic/versions/20260328_130000_add_audit_logs_table.py +45 -0
  212. app/db/alembic/versions/20260328_140000_add_scheduler_leader_table.py +39 -0
  213. app/db/alembic/versions/20260330_000000_add_cache_locality_settings.py +69 -0
  214. app/db/alembic/versions/20260330_010000_merge_scheduler_leader_and_cache_locality_heads.py +25 -0
  215. app/db/alembic/versions/20260330_020000_add_bridge_ring_members.py +46 -0
  216. app/db/alembic/versions/20260401_000000_add_api_key_enforced_service_tier.py +52 -0
  217. app/db/alembic/versions/20260401_000000_add_cache_invalidation.py +41 -0
  218. app/db/alembic/versions/20260401_020000_merge_cache_invalidation_and_api_key_service_tier_heads.py +22 -0
  219. app/db/alembic/versions/20260402_000000_switch_dashboard_routing_default_to_capacity_weighted.py +66 -0
  220. app/db/alembic/versions/20260403_000000_add_credit_api_key_limit_values.py +44 -0
  221. app/db/alembic/versions/20260406_000000_add_api_key_account_assignments.py +57 -0
  222. app/db/alembic/versions/20260406_010000_add_api_key_assignment_scope_flag.py +75 -0
  223. app/db/alembic/versions/20260407_000000_add_bridge_gateway_safe_mode.py +51 -0
  224. app/db/alembic/versions/20260407_010000_merge_api_key_assignment_and_bridge_gateway_heads.py +25 -0
  225. app/db/alembic/versions/20260408_000000_switch_import_without_overwrite_default_to_true.py +73 -0
  226. app/db/alembic/versions/20260408_010000_merge_import_without_overwrite_and_assignment_heads.py +25 -0
  227. app/db/alembic/versions/20260409_000000_add_http_bridge_sessions.py +130 -0
  228. app/db/alembic/versions/20260409_000000_switch_sticky_threads_and_prefer_earlier_reset_defaults_to_true.py +89 -0
  229. app/db/alembic/versions/20260409_010000_add_dashboard_settings_bootstrap_token.py +57 -0
  230. app/db/alembic/versions/20260409_010000_merge_http_bridge_and_import_without_overwrite_heads.py +25 -0
  231. app/db/alembic/versions/20260409_020000_fix_http_bridge_last_seen_index.py +46 -0
  232. app/db/alembic/versions/20260410_000000_backfill_pristine_dashboard_settings_defaults.py +77 -0
  233. app/db/alembic/versions/20260410_030000_restore_import_without_overwrite_default_true.py +73 -0
  234. app/db/alembic/versions/20260410_040000_merge_dashboard_defaults_and_import_default_heads.py +25 -0
  235. app/db/alembic/versions/20260413_000000_add_accounts_blocked_at.py +44 -0
  236. app/db/alembic/versions/20260415_160000_add_automations_tables.py +189 -0
  237. app/db/alembic/versions/20260415_160000_add_request_logs_response_lookup_index.py +67 -0
  238. app/db/alembic/versions/20260417_000000_add_request_log_plan_type.py +44 -0
  239. app/db/alembic/versions/20260418_190000_add_automation_runs_cycle_key.py +87 -0
  240. app/db/alembic/versions/20260419_000000_add_automation_run_cycle_metadata.py +56 -0
  241. app/db/alembic/versions/20260419_020000_add_automation_run_cycles_snapshot_tables.py +342 -0
  242. app/db/alembic/versions/20260421_000000_add_dashboard_session_ttl_setting.py +51 -0
  243. app/db/alembic/versions/20260421_120000_merge_request_log_lookup_and_plan_type_heads.py +25 -0
  244. app/db/alembic/versions/20260421_130000_merge_automation_and_request_log_heads.py +25 -0
  245. app/db/alembic/versions/20260422_103000_normalize_legacy_automation_run_cycle_keys.py +648 -0
  246. app/db/alembic/versions/20260423_120000_add_api_key_limit_reset_at_index.py +43 -0
  247. app/db/alembic/versions/20260424_000000_merge_dashboard_session_ttl_and_request_log_heads.py +25 -0
  248. app/db/alembic/versions/20260426_000000_add_dashboard_relative_availability_settings.py +68 -0
  249. app/db/alembic/versions/20260427_130000_add_warmup_model_and_request_kind.py +75 -0
  250. app/db/alembic/versions/20260506_000000_add_account_routing_policy.py +37 -0
  251. app/db/alembic/versions/20260509_000000_add_api_key_traffic_class.py +37 -0
  252. app/db/alembic/versions/20260509_010000_add_additional_quota_routing_policies.py +37 -0
  253. app/db/alembic/versions/20260513_000000_add_accounts_alias.py +44 -0
  254. app/db/alembic/versions/20260513_000000_add_api_key_apply_to_codex_model.py +59 -0
  255. app/db/alembic/versions/20260514_000000_add_request_logs_api_key_time_index.py +43 -0
  256. app/db/alembic/versions/20260515_000000_add_prefer_earlier_reset_window.py +47 -0
  257. app/db/alembic/versions/20260515_000000_soft_delete_request_logs_on_account_delete.py +83 -0
  258. app/db/alembic/versions/20260515_020000_add_split_sticky_budget_thresholds.py +62 -0
  259. app/db/alembic/versions/20260516_000000_add_sqlite_hot_path_indexes.py +75 -0
  260. app/db/alembic/versions/20260517_000000_merge_sqlite_hot_path_and_request_log_delete_heads.py +24 -0
  261. app/db/alembic/versions/20260518_000000_add_http_bridge_durable_input_prefix.py +44 -0
  262. app/db/alembic/versions/20260518_010000_merge_http_bridge_and_sqlite_recovery_heads.py +25 -0
  263. app/db/alembic/versions/20260518_120000_add_dashboard_guest_access.py +57 -0
  264. app/db/alembic/versions/20260520_000000_merge_api_key_and_http_bridge_heads.py +26 -0
  265. app/db/alembic/versions/20260520_010000_add_request_logs_api_key_account_index.py +45 -0
  266. app/db/alembic/versions/20260520_010000_merge_automation_and_api_key_heads.py +26 -0
  267. app/db/alembic/versions/20260520_030000_add_quota_planner.py +194 -0
  268. app/db/alembic/versions/20260520_040000_merge_quota_planner_and_request_log_index_heads.py +24 -0
  269. app/db/alembic/versions/20260521_000000_add_account_security_work_authorized.py +46 -0
  270. app/db/alembic/versions/20260521_000000_merge_dashboard_guest_and_api_key_heads.py +26 -0
  271. app/db/alembic/versions/20260521_000000_merge_prefer_reset_window_head.py +26 -0
  272. app/db/alembic/versions/20260521_000000_merge_warmup_and_request_log_index_heads.py +25 -0
  273. app/db/alembic/versions/20260522_000000_add_limit_warmup_trigger.py +195 -0
  274. app/db/alembic/versions/20260524_000000_merge_accounts_alias_and_reset_window_heads.py +24 -0
  275. app/db/alembic/versions/20260525_000000_add_usage_raw_window_latest_index.py +31 -0
  276. app/db/alembic/versions/20260526_000000_add_request_log_failure_metadata.py +100 -0
  277. app/db/alembic/versions/20260531_000000_add_account_workspace_identity.py +39 -0
  278. app/db/alembic/versions/20260531_000000_add_single_account_routing.py +42 -0
  279. app/db/alembic/versions/20260601_000000_merge_relative_availability_and_usage_raw_heads.py +24 -0
  280. app/db/alembic/versions/20260601_010000_add_api_key_usage_sections.py +58 -0
  281. app/db/alembic/versions/20260601_010000_add_routing_policy_persistence.py +67 -0
  282. app/db/alembic/versions/20260601_010000_merge_security_work_and_relative_availability_heads.py +25 -0
  283. app/db/alembic/versions/20260601_010000_merge_split_sticky_and_relative_usage_heads.py +25 -0
  284. app/db/alembic/versions/20260601_010000_merge_warmup_and_relative_availability_heads.py +25 -0
  285. app/db/alembic/versions/20260601_020000_merge_additional_quota_routing_and_relative_availability_heads.py +25 -0
  286. app/db/alembic/versions/20260601_020000_merge_quota_planner_and_relative_availability_heads.py +25 -0
  287. app/db/alembic/versions/20260601_020000_merge_reset_window_and_relative_availability_heads.py +25 -0
  288. app/db/alembic/versions/20260601_020000_merge_routing_policy_and_split_threshold_heads.py +24 -0
  289. app/db/alembic/versions/20260601_020000_merge_warmup_and_request_log_failure_heads.py +25 -0
  290. app/db/alembic/versions/20260601_030000_merge_single_account_and_routing_quota_heads.py +29 -0
  291. app/db/alembic/versions/20260601_040000_merge_security_work_and_routing_consolidation_heads.py +25 -0
  292. app/db/alembic/versions/20260601_050000_merge_quota_planner_and_routing_consolidation_heads.py +25 -0
  293. app/db/alembic/versions/20260602_050000_add_upstream_proxy_routing.py +164 -0
  294. app/db/alembic/versions/20260602_060000_merge_account_workspace_and_failure_heads.py +25 -0
  295. app/db/alembic/versions/20260602_070000_add_request_log_useragent_fields.py +86 -0
  296. app/db/alembic/versions/20260602_070000_merge_quota_planner_and_account_workspace_heads.py +25 -0
  297. app/db/alembic/versions/20260602_080000_merge_upstream_proxy_and_quota_planner_heads.py +25 -0
  298. app/db/alembic/versions/20260602_080000_merge_useragent_and_upstream_proxy_heads.py +24 -0
  299. app/db/alembic/versions/20260603_000000_add_weekly_pace_working_days.py +52 -0
  300. app/db/alembic/versions/20260603_000000_free_account_monthly_window.py +60 -0
  301. app/db/alembic/versions/20260604_000000_add_reauth_required_account_status.py +159 -0
  302. app/db/alembic/versions/20260604_010000_merge_automations_and_reauth_status_heads.py +25 -0
  303. app/db/alembic/versions/20260604_010000_merge_dashboard_guest_and_reauth_status_heads.py +25 -0
  304. app/db/alembic/versions/20260604_010000_merge_useragent_and_reauth_heads.py +24 -0
  305. app/db/alembic/versions/20260607_000000_merge_weekly_monthly_useragent_heads.py +28 -0
  306. app/db/alembic/versions/20260608_000000_add_hide_upstream_quota_from_api_keys.py +50 -0
  307. app/db/alembic/versions/20260611_000000_add_request_log_archive_request_id.py +47 -0
  308. app/db/alembic/versions/20260611_000000_merge_automations_and_weekly_monthly_useragent_heads.py +26 -0
  309. app/db/alembic/versions/20260611_000000_merge_dashboard_guest_and_weekly_useragent_heads.py +26 -0
  310. app/db/alembic/versions/20260611_010000_add_request_log_client_ip.py +57 -0
  311. app/db/alembic/versions/20260611_020000_merge_request_log_archive_and_client_ip_heads.py +26 -0
  312. app/db/alembic/versions/20260611_030000_merge_dashboard_guest_and_request_log_client_ip_heads.py +26 -0
  313. app/db/alembic/versions/20260613_000000_add_accounts_codex_installation_id.py +57 -0
  314. app/db/alembic/versions/20260613_010000_merge_installation_id_and_dashboard_weekly_heads.py +26 -0
  315. app/db/alembic/versions/20260615_000000_merge_automations_and_dashboard_guest_heads.py +26 -0
  316. app/db/alembic/versions/20260623_000000_add_limit_warmup_exhausted_threshold.py +51 -0
  317. app/db/alembic/versions/20260626_000000_add_http_downstream_transport_policy.py +46 -0
  318. app/db/alembic/versions/20260626_010000_add_request_logs_upstream_transport.py +31 -0
  319. app/db/alembic/versions/20260629_000000_add_dashboard_query_hot_path_indexes.py +53 -0
  320. app/db/alembic/versions/20260630_000000_add_limit_warmup_staggered_idle.py +47 -0
  321. app/db/alembic/versions/20260630_000000_merge_request_log_client_ip_and_dashboard_hot_path_heads.py +26 -0
  322. app/db/alembic/versions/20260630_010000_merge_warmup_and_request_log_dashboard_heads.py +26 -0
  323. app/db/alembic/versions/20260630_020000_merge_automations_and_main_heads.py +26 -0
  324. app/db/alembic/versions/20260630_020000_merge_warmup_threshold_and_main_heads.py +26 -0
  325. app/db/alembic/versions/20260630_030000_add_automation_run_model_snapshot.py +59 -0
  326. app/db/alembic/versions/20260630_040000_merge_automation_snapshot_and_warmup_threshold_heads.py +26 -0
  327. app/db/alembic/versions/20260630_050000_add_automation_run_prompt_snapshot.py +52 -0
  328. app/db/alembic/versions/20260701_000000_add_weekly_pace_smoothing_minutes.py +51 -0
  329. app/db/alembic/versions/20260702_000000_add_model_sources.py +176 -0
  330. app/db/alembic/versions/20260705_000000_add_model_source_audio_transcriptions.py +50 -0
  331. app/db/alembic/versions/20260705_000000_harden_dashboard_session_ttl.py +62 -0
  332. app/db/alembic/versions/20260706_000000_add_model_source_audio_per_minute.py +43 -0
  333. app/db/alembic/versions/20260707_000000_merge_model_sources_and_session_ttl.py +24 -0
  334. app/db/alembic/versions/20260707_010000_merge_automations_and_model_sources_heads.py +24 -0
  335. app/db/alembic/versions/20260707_020000_add_automation_job_account_scope.py +125 -0
  336. app/db/alembic/versions/20260709_000000_add_ttft_phase_observability.py +66 -0
  337. app/db/alembic/versions/20260711_000000_add_dashboard_prohibit_fast_mode.py +42 -0
  338. app/db/alembic/versions/20260711_010000_add_account_chatgpt_user_id.py +40 -0
  339. app/db/alembic/versions/20260711_020000_add_dashboard_account_concurrency_caps.py +71 -0
  340. app/db/alembic/versions/20260711_030000_add_limit_warmup_idle_threshold.py +51 -0
  341. app/db/alembic/versions/20260712_010000_add_account_usage_rollups.py +56 -0
  342. app/db/alembic/versions/20260712_020000_add_api_key_usage_rollups.py +53 -0
  343. app/db/alembic/versions/20260713_020000_add_model_registry_snapshot.py +42 -0
  344. app/db/alembic/versions/20260713_040000_add_account_refresh_claims.py +42 -0
  345. app/db/alembic/versions/20260713_040000_add_replica_guardrails.py +64 -0
  346. app/db/alembic/versions/20260713_070000_add_reset_credit_redeem_tables.py +58 -0
  347. app/db/alembic/versions/20260714_000000_add_oauth_flow_states.py +63 -0
  348. app/db/alembic/versions/20260715_000000_add_request_log_queue_latency.py +44 -0
  349. app/db/alembic/versions/20260716_000000_add_oauth_device_flow_slots.py +41 -0
  350. app/db/alembic/versions/20260716_010000_add_dashboard_retention_settings.py +48 -0
  351. app/db/alembic/versions/20260716_010000_add_reset_credit_display_settings.py +73 -0
  352. app/db/alembic/versions/20260717_000000_add_http_bridge_retry_circuits.py +47 -0
  353. app/db/alembic/versions/20260717_000000_merge_retention_and_reset_credit_display_heads.py +26 -0
  354. app/db/alembic/versions/20260717_000000_optimize_dashboard_hot_path_indexes.py +162 -0
  355. app/db/alembic/versions/20260717_000001_merge_retry_circuits_and_dashboard_indexes.py +22 -0
  356. app/db/alembic/versions/20260720_000000_add_request_log_conversation_id.py +57 -0
  357. app/db/alembic/versions/20260722_000000_backfill_request_log_useragent_families.py +40 -0
  358. app/db/alembic/versions/20260724_000000_add_request_usage_time_rollups.py +176 -0
  359. app/db/alembic/versions/20260724_000000_merge_request_log_schema_heads.py +33 -0
  360. app/db/alembic/versions/20260725_000000_add_http_bridge_pending_tool_calls.py +43 -0
  361. app/db/alembic/versions/20260726_000000_add_account_plan_downgrade_observations.py +44 -0
  362. app/db/alembic/versions/20260726_000000_repair_request_usage_rollups_after_merge.py +37 -0
  363. app/db/alembic/versions/20260727_000000_add_sticky_session_continuity_abandoned_at.py +45 -0
  364. app/db/alembic/versions/20260728_000000_merge_pending_tool_calls_and_rollup_repair_heads.py +31 -0
  365. app/db/alembic/versions/20260730_000000_add_api_key_fair_share_threshold.py +44 -0
  366. app/db/alembic/versions/20260730_000000_add_http_bridge_recovery_attempts.py +60 -0
  367. app/db/alembic/versions/20260731_000000_add_capability_lineage_markers.py +38 -0
  368. app/db/alembic/versions/20260803_000000_merge_http_bridge_recovery_and_capability_lineage_heads.py +30 -0
  369. app/db/alembic/versions/20260804_000000_add_http_bridge_operations.py +63 -0
  370. app/db/alembic/versions/20260804_000001_add_global_http_bridge_operation_fingerprint.py +53 -0
  371. app/db/alembic/versions/20260804_230000_add_request_log_connection_request_kind.py +43 -0
  372. app/db/alembic/versions/20260805_000000_add_http_bridge_operation_spool.py +75 -0
  373. app/db/alembic/versions/20260805_000001_finalize_http_bridge_operation_spool.py +65 -0
  374. app/db/alembic/versions/20260806_000000_add_additional_usage_alias_probe_indexes.py +84 -0
  375. app/db/alembic/versions/20260806_000000_add_anonymous_telemetry.py +49 -0
  376. app/db/alembic/versions/20260806_010000_add_conversation_presence_rollup.py +72 -0
  377. app/db/alembic/versions/20260806_020000_add_usage_history_bulk_covering_indexes.py +99 -0
  378. app/db/alembic/versions/20260806_030000_add_api_key_allowed_reasoning_efforts.py +65 -0
  379. app/db/alembic/versions/20260806_120000_add_http_bridge_owner_process_epoch.py +57 -0
  380. app/db/alembic/versions/20260807_000000_merge_http_bridge_operation_spool_and_latest_main.py +24 -0
  381. app/db/alembic/versions/20260808_000000_tune_usage_history_autovacuum.py +54 -0
  382. app/db/alembic/versions/20260810_000000_add_http_bridge_recovery_dispatch_count.py +44 -0
  383. app/db/alembic/versions/20260811_000000_add_hourly_rollup_cancelled_count.py +92 -0
  384. app/db/alembic/versions/20260812_000000_merge_recovery_dispatch_and_hourly_cancelled_heads.py +25 -0
  385. app/db/alembic/versions/20260812_120000_add_sticky_abandonment_scope.py +43 -0
  386. app/db/alembic/versions/20260813_000000_add_file_account_pins.py +40 -0
  387. app/db/alembic/versions/20260816_000000_add_account_pending_deletion.py +81 -0
  388. app/db/alembic/versions/20260816_000000_add_model_source_embeddings.py +50 -0
  389. app/db/alembic/versions/20260821_000000_add_retry_circuit_admission_generation.py +49 -0
  390. app/db/alembic/versions/20260826_000000_add_http_bridge_event_chunks.py +107 -0
  391. app/db/alembic/versions/20260828_000000_add_accounts_chatgpt_identity_index.py +55 -0
  392. app/db/alembic/versions/20260830_000000_add_quota_warmup_claim_expiry.py +83 -0
  393. app/db/alembic/versions/20260908_000000_add_guest_session_generation.py +37 -0
  394. app/db/alembic/versions/20260908_000000_add_subscription_overflow.py +140 -0
  395. app/db/alembic/versions/20260908_000000_replace_upstream_stream_transport_default_sentinel.py +50 -0
  396. app/db/alembic/versions/20260908_020000_merge_overflow_transport_heads.py +27 -0
  397. app/db/alembic/versions/20260909_000000_add_dashboard_roles.py +72 -0
  398. app/db/alembic/versions/20260909_010000_add_dashboard_users.py +183 -0
  399. app/db/alembic/versions/20260909_020000_reproject_compat_admin_credentials.py +105 -0
  400. app/db/alembic/versions/20260909_030000_add_audit_actor_columns.py +103 -0
  401. app/db/alembic/versions/20260909_030000_dashboard_resilience_toggle_settings.py +63 -0
  402. app/db/alembic/versions/20260909_040000_add_dashboard_user_invites.py +54 -0
  403. app/db/alembic/versions/20260909_040000_dashboard_timeout_settings.py +68 -0
  404. app/db/alembic/versions/20260909_050000_dashboard_routing_overload_settings.py +65 -0
  405. app/db/alembic/versions/20260909_060000_add_report_rollup.py +74 -0
  406. app/db/alembic/versions/20260909_070000_automation_run_claim_budget.py +50 -0
  407. app/db/alembic/versions/20260909_080000_dashboard_stream_bridge_budgets.py +62 -0
  408. app/db/alembic/versions/20260909_090000_dashboard_background_job_toggles.py +63 -0
  409. app/db/alembic/versions/20260909_100000_dashboard_codex_prewarm.py +49 -0
  410. app/db/alembic/versions/20260909_110000_model_context_window_overrides.py +45 -0
  411. app/db/alembic/versions/20260909_120000_dashboard_conversation_archive.py +48 -0
  412. app/db/alembic/versions/20260909_130000_add_request_logs_live_facet_indexes.py +96 -0
  413. app/db/alembic/versions/20260910_000000_add_totp_required_for_admin_role.py +44 -0
  414. app/db/alembic/versions/20260910_000000_request_logs_missing_cost_index.py +52 -0
  415. app/db/alembic/versions/20260910_010000_add_dashboard_auth_providers.py +134 -0
  416. app/db/alembic/versions/20260910_010000_dashboard_spool_retention.py +52 -0
  417. app/db/alembic/versions/20260910_020000_add_dashboard_role_mappings.py +74 -0
  418. app/db/alembic/versions/20260910_143000_request_log_affinity.py +28 -0
  419. app/db/alembic/versions/20260910_180000_merge_affinity_guest_heads.py +17 -0
  420. app/db/alembic/versions/20260910_200000_merge_affinity_identity_heads.py +17 -0
  421. app/db/alembic/versions/20260910_220000_merge_affinity_invite_heads.py +17 -0
  422. app/db/alembic/versions/20260911_000000_model_source_pins_kind_expires_index.py +43 -0
  423. app/db/alembic/versions/20260911_010000_merge_pin_index_and_affinity_heads.py +17 -0
  424. app/db/alembic/versions/20260911_020000_add_http_bridge_terminal_append_phase.py +54 -0
  425. app/db/alembic/versions/20260911_030000_add_local_login_policy.py +48 -0
  426. app/db/alembic/versions/20260911_040000_add_thread_cache_identity_mode.py +48 -0
  427. app/db/alembic/versions/20260911_060000_add_bridge_session_continuity_abandonment.py +40 -0
  428. app/db/alembic/versions/20260912_000000_merge_thread_cache_and_bridge_retirement_heads.py +22 -0
  429. app/db/alembic/versions/20260912_010000_drop_legacy_dashboard_credentials.py +140 -0
  430. app/db/alembic/versions/20260913_000000_add_oidc_provider_flow.py +134 -0
  431. app/db/alembic/versions/20260914_000000_add_scim_tokens.py +78 -0
  432. app/db/alembic/versions/20260914_000000_drop_subscription_overflow_schema.py +132 -0
  433. app/db/alembic/versions/20260918_000000_merge_scim_and_overflow_heads.py +17 -0
  434. app/db/alembic/versions/__init__.py +1 -0
  435. app/db/backup.py +64 -0
  436. app/db/migrate.py +1249 -0
  437. app/db/migration_lock.py +160 -0
  438. app/db/migration_url.py +18 -0
  439. app/db/models.py +3100 -0
  440. app/db/recover.py +244 -0
  441. app/db/session.py +1295 -0
  442. app/db/snapshot.py +46 -0
  443. app/db/sqlite_lock_retry.py +190 -0
  444. app/db/sqlite_utils.py +549 -0
  445. app/dependencies.py +462 -0
  446. app/main.py +1240 -0
  447. app/modules/__init__.py +0 -0
  448. app/modules/accounts/__init__.py +0 -0
  449. app/modules/accounts/api.py +426 -0
  450. app/modules/accounts/auth_manager.py +1439 -0
  451. app/modules/accounts/background_repository.py +160 -0
  452. app/modules/accounts/deletion.py +540 -0
  453. app/modules/accounts/mappers.py +654 -0
  454. app/modules/accounts/refresh_claims.py +352 -0
  455. app/modules/accounts/repository.py +1679 -0
  456. app/modules/accounts/schemas.py +308 -0
  457. app/modules/accounts/service.py +809 -0
  458. app/modules/accounts/usage_rollup.py +486 -0
  459. app/modules/accounts/usage_rollup_scheduler.py +75 -0
  460. app/modules/accounts/usage_time_rollup.py +1037 -0
  461. app/modules/accounts/usage_time_rollup_read.py +522 -0
  462. app/modules/api_keys/__init__.py +1 -0
  463. app/modules/api_keys/api.py +344 -0
  464. app/modules/api_keys/last_used_coalescer.py +216 -0
  465. app/modules/api_keys/limit_windows.py +41 -0
  466. app/modules/api_keys/repository.py +1200 -0
  467. app/modules/api_keys/reset_scheduler.py +85 -0
  468. app/modules/api_keys/schemas.py +135 -0
  469. app/modules/api_keys/service.py +2123 -0
  470. app/modules/audit/__init__.py +0 -0
  471. app/modules/audit/api.py +88 -0
  472. app/modules/audit/repository.py +74 -0
  473. app/modules/audit/schemas.py +33 -0
  474. app/modules/audit/service.py +71 -0
  475. app/modules/auth_providers/__init__.py +0 -0
  476. app/modules/auth_providers/api.py +127 -0
  477. app/modules/auth_providers/config.py +343 -0
  478. app/modules/auth_providers/repository.py +101 -0
  479. app/modules/auth_providers/schemas.py +88 -0
  480. app/modules/auth_providers/seed.py +105 -0
  481. app/modules/auth_providers/service.py +289 -0
  482. app/modules/automations/__init__.py +1 -0
  483. app/modules/automations/api.py +369 -0
  484. app/modules/automations/repository.py +1896 -0
  485. app/modules/automations/scheduler.py +107 -0
  486. app/modules/automations/schemas.py +158 -0
  487. app/modules/automations/service.py +2548 -0
  488. app/modules/cache_isolation_probe/__init__.py +0 -0
  489. app/modules/cache_isolation_probe/api.py +188 -0
  490. app/modules/cache_isolation_probe/prefix.py +144 -0
  491. app/modules/cache_isolation_probe/schemas.py +94 -0
  492. app/modules/cache_isolation_probe/sender.py +229 -0
  493. app/modules/cache_isolation_probe/service.py +525 -0
  494. app/modules/conversation_archive/__init__.py +1 -0
  495. app/modules/conversation_archive/api.py +101 -0
  496. app/modules/conversation_archive/schemas.py +28 -0
  497. app/modules/conversation_archive/service.py +165 -0
  498. app/modules/dashboard/__init__.py +1 -0
  499. app/modules/dashboard/api.py +106 -0
  500. app/modules/dashboard/builders.py +107 -0
  501. app/modules/dashboard/repository.py +304 -0
  502. app/modules/dashboard/schemas.py +148 -0
  503. app/modules/dashboard/service.py +534 -0
  504. app/modules/dashboard/timeframes.py +19 -0
  505. app/modules/dashboard/weekly_pace.py +617 -0
  506. app/modules/dashboard_auth/__init__.py +1 -0
  507. app/modules/dashboard_auth/api.py +1344 -0
  508. app/modules/dashboard_auth/oidc_api.py +753 -0
  509. app/modules/dashboard_auth/oidc_flows.py +330 -0
  510. app/modules/dashboard_auth/repository.py +452 -0
  511. app/modules/dashboard_auth/schemas.py +232 -0
  512. app/modules/dashboard_auth/service.py +1384 -0
  513. app/modules/dashboard_roles/__init__.py +0 -0
  514. app/modules/dashboard_roles/api.py +64 -0
  515. app/modules/dashboard_roles/repository.py +54 -0
  516. app/modules/dashboard_roles/schemas.py +29 -0
  517. app/modules/dashboard_roles/seed.py +80 -0
  518. app/modules/dashboard_roles/service.py +142 -0
  519. app/modules/dashboard_users/__init__.py +0 -0
  520. app/modules/dashboard_users/api.py +278 -0
  521. app/modules/dashboard_users/break_glass.py +169 -0
  522. app/modules/dashboard_users/credentials.py +18 -0
  523. app/modules/dashboard_users/identity_resolver.py +665 -0
  524. app/modules/dashboard_users/repository.py +725 -0
  525. app/modules/dashboard_users/schemas.py +117 -0
  526. app/modules/dashboard_users/service.py +1054 -0
  527. app/modules/firewall/__init__.py +1 -0
  528. app/modules/firewall/api.py +78 -0
  529. app/modules/firewall/repository.py +51 -0
  530. app/modules/firewall/schemas.py +28 -0
  531. app/modules/firewall/service.py +93 -0
  532. app/modules/fleet/__init__.py +1 -0
  533. app/modules/fleet/api.py +177 -0
  534. app/modules/fleet/mappers.py +56 -0
  535. app/modules/fleet/observability.py +372 -0
  536. app/modules/fleet/schemas.py +119 -0
  537. app/modules/health/__init__.py +0 -0
  538. app/modules/health/api.py +246 -0
  539. app/modules/health/schemas.py +27 -0
  540. app/modules/limit_warmup/__init__.py +1 -0
  541. app/modules/limit_warmup/repository.py +159 -0
  542. app/modules/limit_warmup/service.py +1001 -0
  543. app/modules/model_sources/__init__.py +1 -0
  544. app/modules/model_sources/api.py +99 -0
  545. app/modules/model_sources/catalog.py +307 -0
  546. app/modules/model_sources/forwarding.py +1435 -0
  547. app/modules/model_sources/projection.py +55 -0
  548. app/modules/model_sources/repository.py +201 -0
  549. app/modules/model_sources/schemas.py +79 -0
  550. app/modules/model_sources/selection.py +166 -0
  551. app/modules/model_sources/service.py +236 -0
  552. app/modules/oauth/__init__.py +0 -0
  553. app/modules/oauth/api.py +101 -0
  554. app/modules/oauth/repository.py +238 -0
  555. app/modules/oauth/schemas.py +49 -0
  556. app/modules/oauth/service.py +1196 -0
  557. app/modules/oauth/templates/oauth_success.html +122 -0
  558. app/modules/proxy/__init__.py +0 -0
  559. app/modules/proxy/_load_balancer/__init__.py +1 -0
  560. app/modules/proxy/_load_balancer/error_rate.py +116 -0
  561. app/modules/proxy/_load_balancer/latency_cohort.py +154 -0
  562. app/modules/proxy/_load_balancer/model_eligibility.py +250 -0
  563. app/modules/proxy/_load_balancer/opportunistic_admission.py +350 -0
  564. app/modules/proxy/_load_balancer/overload_backoff.py +394 -0
  565. app/modules/proxy/_load_balancer/sticky_selection.py +2417 -0
  566. app/modules/proxy/_load_balancer/throughput_cohort.py +233 -0
  567. app/modules/proxy/_load_balancer/ttft_cohort.py +214 -0
  568. app/modules/proxy/_load_balancer/tunables.py +97 -0
  569. app/modules/proxy/_load_balancer/types.py +96 -0
  570. app/modules/proxy/_load_balancer/unbound_selection.py +534 -0
  571. app/modules/proxy/_service/__init__.py +6 -0
  572. app/modules/proxy/_service/api_key_usage.py +745 -0
  573. app/modules/proxy/_service/clock_budget.py +38 -0
  574. app/modules/proxy/_service/codex_control.py +611 -0
  575. app/modules/proxy/_service/compact.py +2211 -0
  576. app/modules/proxy/_service/file_ops.py +749 -0
  577. app/modules/proxy/_service/http_bridge/__init__.py +3 -0
  578. app/modules/proxy/_service/http_bridge/accepted_replay.py +331 -0
  579. app/modules/proxy/_service/http_bridge/account_sessions.py +57 -0
  580. app/modules/proxy/_service/http_bridge/activity.py +178 -0
  581. app/modules/proxy/_service/http_bridge/helpers.py +3959 -0
  582. app/modules/proxy/_service/http_bridge/mixin.py +2435 -0
  583. app/modules/proxy/_service/http_bridge/owner_forwarding.py +609 -0
  584. app/modules/proxy/_service/http_bridge/protocol.py +167 -0
  585. app/modules/proxy/_service/http_bridge/proxy_failover.py +58 -0
  586. app/modules/proxy/_service/http_bridge/quarantine.py +443 -0
  587. app/modules/proxy/_service/http_bridge/request_submit.py +4582 -0
  588. app/modules/proxy/_service/http_bridge/retry_circuit.py +2442 -0
  589. app/modules/proxy/_service/http_bridge/service_stubs.py +492 -0
  590. app/modules/proxy/_service/http_bridge/session_registry.py +778 -0
  591. app/modules/proxy/_service/http_bridge/streaming.py +5328 -0
  592. app/modules/proxy/_service/http_bridge/upstream_events.py +4538 -0
  593. app/modules/proxy/_service/observability.py +410 -0
  594. app/modules/proxy/_service/rate_limit.py +361 -0
  595. app/modules/proxy/_service/realtime_live.py +655 -0
  596. app/modules/proxy/_service/refresh.py +154 -0
  597. app/modules/proxy/_service/request_log.py +585 -0
  598. app/modules/proxy/_service/response_create.py +930 -0
  599. app/modules/proxy/_service/streaming/__init__.py +3 -0
  600. app/modules/proxy/_service/streaming/helpers.py +1317 -0
  601. app/modules/proxy/_service/streaming/mixin.py +1100 -0
  602. app/modules/proxy/_service/streaming/protocol.py +32 -0
  603. app/modules/proxy/_service/streaming/retry.py +3762 -0
  604. app/modules/proxy/_service/support.py +2122 -0
  605. app/modules/proxy/_service/transcribe.py +434 -0
  606. app/modules/proxy/_service/warmup.py +483 -0
  607. app/modules/proxy/_service/websocket/__init__.py +3 -0
  608. app/modules/proxy/_service/websocket/helpers.py +2360 -0
  609. app/modules/proxy/_service/websocket/mixin.py +7285 -0
  610. app/modules/proxy/_service/websocket/protocol.py +74 -0
  611. app/modules/proxy/account_cache.py +229 -0
  612. app/modules/proxy/account_eligibility.py +87 -0
  613. app/modules/proxy/additional_model_limits.py +40 -0
  614. app/modules/proxy/affinity.py +830 -0
  615. app/modules/proxy/affinity_observation.py +101 -0
  616. app/modules/proxy/api.py +10254 -0
  617. app/modules/proxy/api_key_usage.py +113 -0
  618. app/modules/proxy/cap_partitioning.py +280 -0
  619. app/modules/proxy/capability_lineage.py +65 -0
  620. app/modules/proxy/capability_lineage_repository.py +82 -0
  621. app/modules/proxy/capability_routing.py +253 -0
  622. app/modules/proxy/continuity.py +112 -0
  623. app/modules/proxy/downstream_delivery.py +277 -0
  624. app/modules/proxy/durable_bridge_coordinator.py +1217 -0
  625. app/modules/proxy/durable_bridge_repository.py +4516 -0
  626. app/modules/proxy/durable_bridge_runtime.py +9 -0
  627. app/modules/proxy/durable_bridge_transcript_codec.py +122 -0
  628. app/modules/proxy/fair_share.py +131 -0
  629. app/modules/proxy/file_pin_repository.py +210 -0
  630. app/modules/proxy/helpers.py +439 -0
  631. app/modules/proxy/http_bridge_event_batcher.py +613 -0
  632. app/modules/proxy/http_bridge_forwarding.py +797 -0
  633. app/modules/proxy/http_continuation.py +76 -0
  634. app/modules/proxy/images_observability.py +62 -0
  635. app/modules/proxy/images_service.py +1054 -0
  636. app/modules/proxy/load_balancer.py +3016 -0
  637. app/modules/proxy/rate_limit_cache.py +46 -0
  638. app/modules/proxy/replay_safety.py +1063 -0
  639. app/modules/proxy/repo_bundle.py +31 -0
  640. app/modules/proxy/request_policy.py +1054 -0
  641. app/modules/proxy/ring_membership.py +251 -0
  642. app/modules/proxy/schemas.py +345 -0
  643. app/modules/proxy/selection_errors.py +41 -0
  644. app/modules/proxy/service.py +2600 -0
  645. app/modules/proxy/source_admission.py +125 -0
  646. app/modules/proxy/source_dispatch.py +999 -0
  647. app/modules/proxy/sticky_repository.py +831 -0
  648. app/modules/proxy/thread_anchors.py +491 -0
  649. app/modules/proxy/tool_call_dedupe.py +802 -0
  650. app/modules/proxy/types.py +54 -0
  651. app/modules/proxy/work_admission.py +121 -0
  652. app/modules/quota_planner/__init__.py +1 -0
  653. app/modules/quota_planner/api.py +276 -0
  654. app/modules/quota_planner/logic.py +743 -0
  655. app/modules/quota_planner/repository.py +722 -0
  656. app/modules/quota_planner/scheduler.py +212 -0
  657. app/modules/quota_planner/schemas.py +102 -0
  658. app/modules/quota_planner/warmup.py +693 -0
  659. app/modules/rate_limit_reset_credits/__init__.py +0 -0
  660. app/modules/rate_limit_reset_credits/api.py +601 -0
  661. app/modules/rate_limit_reset_credits/redeem_coordination.py +274 -0
  662. app/modules/rate_limit_reset_credits/store.py +114 -0
  663. app/modules/reports/__init__.py +1 -0
  664. app/modules/reports/api.py +134 -0
  665. app/modules/reports/cache.py +74 -0
  666. app/modules/reports/filters.py +27 -0
  667. app/modules/reports/repository.py +525 -0
  668. app/modules/reports/rollup.py +150 -0
  669. app/modules/reports/rollup_read.py +148 -0
  670. app/modules/reports/schemas.py +123 -0
  671. app/modules/reports/service.py +287 -0
  672. app/modules/reports/thread_identity.py +256 -0
  673. app/modules/request_logs/__init__.py +0 -0
  674. app/modules/request_logs/api.py +224 -0
  675. app/modules/request_logs/cost_backfill.py +154 -0
  676. app/modules/request_logs/mappers.py +97 -0
  677. app/modules/request_logs/repository.py +1765 -0
  678. app/modules/request_logs/schemas.py +145 -0
  679. app/modules/request_logs/service.py +348 -0
  680. app/modules/role_mappings/__init__.py +0 -0
  681. app/modules/role_mappings/api.py +206 -0
  682. app/modules/role_mappings/matching.py +55 -0
  683. app/modules/role_mappings/repository.py +106 -0
  684. app/modules/role_mappings/schemas.py +67 -0
  685. app/modules/role_mappings/service.py +334 -0
  686. app/modules/runtime/__init__.py +1 -0
  687. app/modules/runtime/api.py +18 -0
  688. app/modules/runtime/schemas.py +14 -0
  689. app/modules/runtime/service.py +218 -0
  690. app/modules/scim/__init__.py +1 -0
  691. app/modules/scim/api.py +194 -0
  692. app/modules/scim/dependencies.py +112 -0
  693. app/modules/scim/errors.py +77 -0
  694. app/modules/scim/management_api.py +156 -0
  695. app/modules/scim/repository.py +212 -0
  696. app/modules/scim/schemas.py +145 -0
  697. app/modules/scim/service.py +786 -0
  698. app/modules/scim/tokens.py +43 -0
  699. app/modules/settings/__init__.py +1 -0
  700. app/modules/settings/api.py +1677 -0
  701. app/modules/settings/repository.py +606 -0
  702. app/modules/settings/schemas.py +496 -0
  703. app/modules/settings/service.py +769 -0
  704. app/modules/shared/__init__.py +0 -0
  705. app/modules/shared/schemas.py +22 -0
  706. app/modules/sticky_sessions/__init__.py +1 -0
  707. app/modules/sticky_sessions/api.py +118 -0
  708. app/modules/sticky_sessions/cleanup_scheduler.py +487 -0
  709. app/modules/sticky_sessions/schemas.py +78 -0
  710. app/modules/sticky_sessions/service.py +196 -0
  711. app/modules/telemetry/__init__.py +5 -0
  712. app/modules/telemetry/api.py +115 -0
  713. app/modules/telemetry/clients.py +62 -0
  714. app/modules/telemetry/consent.py +115 -0
  715. app/modules/telemetry/scheduler.py +85 -0
  716. app/modules/telemetry/schemas.py +163 -0
  717. app/modules/telemetry/sender.py +221 -0
  718. app/modules/telemetry/snapshot.py +490 -0
  719. app/modules/usage/__init__.py +0 -0
  720. app/modules/usage/additional_quota_keys.py +379 -0
  721. app/modules/usage/api.py +45 -0
  722. app/modules/usage/background_repository.py +147 -0
  723. app/modules/usage/builders.py +559 -0
  724. app/modules/usage/depletion_service.py +277 -0
  725. app/modules/usage/live_ingest.py +356 -0
  726. app/modules/usage/mappers.py +29 -0
  727. app/modules/usage/plan_downgrade_observations.py +488 -0
  728. app/modules/usage/repository.py +1890 -0
  729. app/modules/usage/schemas.py +68 -0
  730. app/modules/usage/service.py +102 -0
  731. app/modules/usage/updater.py +1488 -0
  732. app/static/assets/Trans-Cj-J9mVA.js +1 -0
  733. app/static/assets/access-page-DHzZiXZi.js +1 -0
  734. app/static/assets/account-cost-donut-DGhfA4U8.js +1 -0
  735. app/static/assets/account-identifiers-CxbdLshZ.js +1 -0
  736. app/static/assets/account-status-DIsU5sUS.js +1 -0
  737. app/static/assets/account-trend-chart-BptlfwwQ.js +1 -0
  738. app/static/assets/accounts-page-Bbai8ift.js +3 -0
  739. app/static/assets/api-Dwa7XxPe.js +1 -0
  740. app/static/assets/api-client-dugX1Pti.js +55 -0
  741. app/static/assets/api-key-create-dialog-DnRm0oYN.js +1 -0
  742. app/static/assets/api-key-created-dialog-B6jkDG9f.js +1 -0
  743. app/static/assets/api-key-edit-dialog-Ck3Bnc3V.js +1 -0
  744. app/static/assets/api-trend-chart-BchCw7Hj.js +1 -0
  745. app/static/assets/apis-page-BvXz9c6i.js +2 -0
  746. app/static/assets/arrow-up-Dqh_KzJo.js +1 -0
  747. app/static/assets/arrow-up-down-LBRgVDGY.js +1 -0
  748. app/static/assets/automations-page-DQgNAJOR.js +3 -0
  749. app/static/assets/badge-DqMb9es0.js +1 -0
  750. app/static/assets/blur-email-HEvKagG2.js +1 -0
  751. app/static/assets/button-BjVmmiGW.js +1 -0
  752. app/static/assets/checkbox-apsl9vzk.js +1 -0
  753. app/static/assets/chevron-left-1d_pHVa5.js +1 -0
  754. app/static/assets/circle-alert-BS0xd_o1.js +1 -0
  755. app/static/assets/clsx-DB0hHKMi.js +1 -0
  756. app/static/assets/colors-BBy8GEtr.js +1 -0
  757. app/static/assets/copy-button-D7nAvOKR.js +1 -0
  758. app/static/assets/cost-per-day-chart-YdqlkZVM.js +1 -0
  759. app/static/assets/createLucideIcon-DBLzLNh2.js +1 -0
  760. app/static/assets/dashboard-page-CSdhBy0f.js +2 -0
  761. app/static/assets/distribution-metric-format-yZTy1ZEu.js +1 -0
  762. app/static/assets/donut-chart-Bxc69cra.js +1 -0
  763. app/static/assets/download-kQteIcsB.js +1 -0
  764. app/static/assets/ellipsis-B_5OgIre.js +1 -0
  765. app/static/assets/empty-state-JcA7tc8m.js +1 -0
  766. app/static/assets/es2015-bMH8YKZT.js +41 -0
  767. app/static/assets/es6-B264ni6W.js +69 -0
  768. app/static/assets/external-link-UVAOgMQ0.js +1 -0
  769. app/static/assets/i18n-BWqZ9Ua2.js +1 -0
  770. app/static/assets/inbox-B_RM2s1J.js +1 -0
  771. app/static/assets/index-DIpd264Z.js +23 -0
  772. app/static/assets/index-DMeLKRJx.css +2 -0
  773. app/static/assets/inherit-badge-QhTYVaU6.js +1 -0
  774. app/static/assets/invite-dialog-0CYh0GiQ.js +1 -0
  775. app/static/assets/iso-Bpe5Ihbb.js +1 -0
  776. app/static/assets/label-CGaLmLNf.js +1 -0
  777. app/static/assets/lazy-recharts-Pv8p9ArM.js +2 -0
  778. app/static/assets/loading-overlay-CzKQfHLI.js +1 -0
  779. app/static/assets/mini-quota-bar-cGT_t8Ae.js +1 -0
  780. app/static/assets/model-distribution-donut-BKjAfT48.js +1 -0
  781. app/static/assets/model-source-multi-select-D2d06Vc7.js +1 -0
  782. app/static/assets/multi-select-filter-KIA5qU6Z.js +1 -0
  783. app/static/assets/oauth-dialog-XeaGGDjS.js +1 -0
  784. app/static/assets/pagination-controls-D1Ypd0RU.js +1 -0
  785. app/static/assets/payload-CIYnGZz9.js +1 -0
  786. app/static/assets/queue-wait-chart-VoV7Yg9L.js +1 -0
  787. app/static/assets/report-chart-card-BXDliwcc.js +1 -0
  788. app/static/assets/reports-page-CGCJeMHG.js +3 -0
  789. app/static/assets/reset-credit-confirm-dialog-QOifTnVB.js +1 -0
  790. app/static/assets/rolldown-runtime-hePW80VL.js +1 -0
  791. app/static/assets/search-HfhqwERt.js +1 -0
  792. app/static/assets/select-Cl33bz-g.js +1 -0
  793. app/static/assets/settings-page-mZj01HYl.js +2 -0
  794. app/static/assets/shim-CZwyM4Q7.js +1 -0
  795. app/static/assets/skeleton-C_EUc4Rp.js +1 -0
  796. app/static/assets/sparkline-chart-DdNNsddM.js +1 -0
  797. app/static/assets/status-badge-CHHA5X6c.js +1 -0
  798. app/static/assets/switch-CzxgokcM.js +1 -0
  799. app/static/assets/table-DXaNSQ0G.js +1 -0
  800. app/static/assets/time-to-first-token-chart--68L0u_v.js +1 -0
  801. app/static/assets/tokens-per-day-chart-Cs82BFuE.js +1 -0
  802. app/static/assets/tokens-per-second-chart-AD2dfOn6.js +1 -0
  803. app/static/assets/totp-settings-CRHtxQ7j.js +1 -0
  804. app/static/assets/trash-D3KyWr1h.js +1 -0
  805. app/static/assets/use-account-quota-display-BPmrTBTC.js +1 -0
  806. app/static/assets/use-accounts-PWlVGw3z.js +1 -0
  807. app/static/assets/use-auth-CuDlW3iC.js +1 -0
  808. app/static/assets/use-chart-colors-DIu6tClI.js +1 -0
  809. app/static/assets/use-model-sources-Bul24JBu.js +1 -0
  810. app/static/assets/use-models-DUCPUuZg.js +1 -0
  811. app/static/assets/use-reduced-motion-hAm5r6_d.js +1 -0
  812. app/static/assets/useTranslation--56nHhlH.js +1 -0
  813. app/static/assets/useragent-distribution-donut-Dup6sf_n.js +1 -0
  814. app/static/assets/users-uIC3TIlE.js +1 -0
  815. app/static/assets/vendor-query-_bHZuxEx.js +1 -0
  816. app/static/assets/vendor-react-BVL0YZPW.js +9 -0
  817. app/static/favicon.svg +10 -0
  818. app/static/fonts/GeistSans-Variable.woff2 +0 -0
  819. app/static/fonts/jetbrains-mono-latin-ext.woff2 +0 -0
  820. app/static/fonts/jetbrains-mono-latin.woff2 +0 -0
  821. app/static/index.html +29 -0
  822. codexlb_up-1.25.0b10.dist-info/METADATA +124 -0
  823. codexlb_up-1.25.0b10.dist-info/RECORD +827 -0
  824. codexlb_up-1.25.0b10.dist-info/WHEEL +4 -0
  825. codexlb_up-1.25.0b10.dist-info/entry_points.txt +3 -0
  826. codexlb_up-1.25.0b10.dist-info/licenses/LICENSE +21 -0
  827. config/additional_quota_registry.json +11 -0
@@ -0,0 +1,753 @@
1
+ """The public OIDC routes, mounted on the dashboard-auth router.
2
+
3
+ They live in their own module for reading, not for routing: they are added to
4
+ the very same ``/api/dashboard-auth`` router the password routes use, so they
5
+ inherit its error format, its session exemption and its place in the
6
+ cross-site origin middleware's view of the world without a new prefix or a new
7
+ exemption list.
8
+
9
+ Both routes are ``GET``. That is a security decision, not a style one: the
10
+ cross-site origin middleware runs before routing and refuses any unsafe
11
+ ``/api/`` request that arrives cross-site, which is exactly what the identity
12
+ provider's callback is. A ``form_post`` callback would be answered ``403``
13
+ before the handler existed, and the "fix" -- exempting this prefix -- would
14
+ exempt session-minting routes. The query response mode makes the question
15
+ disappear.
16
+
17
+ Nothing here takes a destination from the caller. Where the browser lands is
18
+ derived from the flow's stored purpose, out of a closed set of in-app paths, so
19
+ there is no ``next``, no ``return_to`` and no way to turn this into an open
20
+ redirect.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import hmac
26
+ import secrets
27
+ from typing import Final
28
+
29
+ from fastapi import Depends, Request, Response
30
+ from fastapi.responses import JSONResponse, RedirectResponse
31
+
32
+ from app.core.audit.service import AuditActor, AuditService, AuditTarget
33
+ from app.core.audit.types import AuditAuthMethod, AuditSeverity
34
+ from app.core.auth.dashboard_access import DashboardPrincipal, Permission
35
+ from app.core.auth.dashboard_session_ttl import REMOTE_DASHBOARD_SESSION_TTL_SECONDS
36
+ from app.core.auth.dependencies import (
37
+ ensure_dashboard_permission,
38
+ require_dashboard_permission,
39
+ validate_dashboard_session,
40
+ )
41
+ from app.core.auth.providers import DEFAULT_PROVIDER_KEY, ExternalIdentity
42
+ from app.core.auth.providers.oidc import (
43
+ OIDC_CLOCK_SKEW_SECONDS,
44
+ OidcCompletion,
45
+ OidcError,
46
+ get_oidc_provider,
47
+ hash_flow_value,
48
+ )
49
+ from app.core.auth.providers.registry import get_auth_provider_registry
50
+ from app.core.auth.step_up import STEP_UP_MAX_AGE_SECONDS, step_up_methods
51
+ from app.core.clients.oauth import generate_pkce_pair
52
+ from app.core.config.settings import get_settings
53
+ from app.core.exceptions import (
54
+ AppError,
55
+ DashboardAuthError,
56
+ DashboardConflictError,
57
+ DashboardNotFoundError,
58
+ DashboardRateLimitError,
59
+ DashboardUpstreamError,
60
+ )
61
+ from app.core.utils.masking import mask_email
62
+ from app.core.utils.time import utcnow
63
+ from app.db.models import AuthProviderKind, DashboardAuthProvider, DashboardUser
64
+ from app.dependencies import DashboardAuthContext, get_dashboard_auth_context
65
+ from app.modules.auth_providers.config import OidcProviderConfig, load_oidc_config, oidc_config_fingerprint
66
+ from app.modules.auth_providers.repository import AuthProvidersRepository
67
+ from app.modules.dashboard_auth.api import (
68
+ _client_host,
69
+ _create_user_session,
70
+ _rate_limit_error,
71
+ _session_client_key,
72
+ _set_session_cookie,
73
+ record_step_up_on,
74
+ router,
75
+ )
76
+ from app.modules.dashboard_auth.oidc_flows import (
77
+ OIDC_FLOW_COOKIE,
78
+ OIDC_FLOW_COOKIE_PATH,
79
+ OIDC_FLOW_TTL_SECONDS,
80
+ OIDC_LOGIN_START_PATH,
81
+ OidcFlowPurpose,
82
+ OidcFlowRecord,
83
+ OidcFlowRepository,
84
+ clear_pending_marker,
85
+ get_oidc_flow_cookie_store,
86
+ set_pending_marker,
87
+ )
88
+ from app.modules.dashboard_auth.schemas import OidcStartResponse
89
+ from app.modules.dashboard_auth.service import (
90
+ get_login_failed_audit_rate_limiter,
91
+ get_oidc_address_rate_limiter,
92
+ get_oidc_state_rate_limiter,
93
+ session_clock,
94
+ )
95
+ from app.modules.dashboard_users.identity_resolver import ResolvedAccount, get_identity_resolution_cache
96
+ from app.modules.dashboard_users.repository import DashboardUsersRepository
97
+
98
+ #: The closed set of places this flow can send a browser. Every one is a
99
+ #: relative in-app path, so no absolute URL can appear even by accident.
100
+ OIDC_DASHBOARD_PATH: Final[str] = "/dashboard"
101
+ OIDC_PENDING_PATH: Final[str] = "/auth/pending"
102
+ #: Where a completed pre-flight or re-authentication lands: the card that
103
+ #: started it, through the settings page's existing deep-link convention.
104
+ OIDC_SETTINGS_PATH: Final[str] = "/settings?org=1#oidc"
105
+ #: One failure destination for every refusal that is not "no account here", and
106
+ #: a constant marker rather than a reason: an unauthenticated caller must not be
107
+ #: able to tell a bad state from a bad nonce from a refused exchange.
108
+ OIDC_FAILURE_PATH: Final[str] = "/login?sso=failed"
109
+
110
+ #: Derived from the advertised path so the route and the login screen's
111
+ #: ``login_url`` cannot drift apart.
112
+ _LOGIN_START_ROUTE: Final[str] = OIDC_LOGIN_START_PATH.removeprefix(router.prefix)
113
+ _REDIRECT_STATUS: Final[int] = 303
114
+
115
+
116
+ def _redirect(path: str) -> RedirectResponse:
117
+ response = RedirectResponse(path, status_code=_REDIRECT_STATUS)
118
+ _clear_flow_cookie(response)
119
+ # Every destination except the pending screen clears the refusal marker:
120
+ # a completed sign-in, a completed pre-flight, a completed
121
+ # re-authentication and every failure are all answers to "who are you"
122
+ # that supersede the last refusal this browser was handed.
123
+ clear_pending_marker(response)
124
+ return response
125
+
126
+
127
+ def _pending_redirect(request: Request, row: DashboardAuthProvider, identity: ExternalIdentity) -> RedirectResponse:
128
+ """The one destination that carries something back, and only to the browser it refused.
129
+
130
+ The person behind this browser has just authenticated at the company
131
+ identity provider and holds no session, so the screen would otherwise have
132
+ nothing to say. The marker names the row (the screen resolves its label,
133
+ which is public: it is what the sign-in button says) and the masked address
134
+ the identity provider asserted, which the administrator's refused sign-ins
135
+ list masks the same way -- so the two strings match by eye. Nothing else
136
+ goes in: no subject, no groups, no claim, no address in clear.
137
+
138
+ An identity provider that asserted no address leaves no marker. There would
139
+ be no reference to quote and the audit row it would be matched against has
140
+ no address either, so the screen keeps its general copy instead of naming a
141
+ provider with nothing to look up.
142
+ """
143
+
144
+ email = (identity.email or "").strip()
145
+ if not email:
146
+ return _redirect(OIDC_PENDING_PATH)
147
+ response = RedirectResponse(OIDC_PENDING_PATH, status_code=_REDIRECT_STATUS)
148
+ _clear_flow_cookie(response)
149
+ set_pending_marker(response, request, provider_id=row.id, reference=mask_email(email))
150
+ return response
151
+
152
+
153
+ def _set_flow_cookie(response: Response, request: Request, state: str) -> None:
154
+ response.set_cookie(
155
+ key=OIDC_FLOW_COOKIE,
156
+ value=get_oidc_flow_cookie_store().create(state),
157
+ httponly=True,
158
+ secure=request.url.scheme == "https",
159
+ # Lax, not Strict: the callback is a top-level cross-site navigation
160
+ # and Strict would drop the cookie on the one request that needs it.
161
+ samesite="lax",
162
+ max_age=OIDC_FLOW_TTL_SECONDS,
163
+ path=OIDC_FLOW_COOKIE_PATH,
164
+ )
165
+
166
+
167
+ def _clear_flow_cookie(response: RedirectResponse) -> None:
168
+ """Cleared with the same attributes it was set with, however the flow ends."""
169
+
170
+ response.delete_cookie(key=OIDC_FLOW_COOKIE, path=OIDC_FLOW_COOKIE_PATH)
171
+
172
+
173
+ async def _active_oidc_row() -> tuple[DashboardAuthProvider, OidcProviderConfig] | None:
174
+ """The enabled row the current auth mode admits, together with a configuration that opens.
175
+
176
+ A row that is disabled, a mode that does not admit it and a blob that does
177
+ not decrypt are one outcome, deliberately: none of them can sign anyone in,
178
+ and an unauthenticated caller learns nothing from the difference.
179
+ """
180
+
181
+ active = await get_auth_provider_registry().get_active(
182
+ AuthProviderKind.OIDC, DEFAULT_PROVIDER_KEY, get_settings().dashboard_auth_mode
183
+ )
184
+ if active is None:
185
+ return None
186
+ config = load_oidc_config(active.row)
187
+ return None if config is None else (active.row, config)
188
+
189
+
190
+ async def _configured_oidc_row(
191
+ context: DashboardAuthContext, *, provider_id: str | None = None
192
+ ) -> tuple[DashboardAuthProvider, OidcProviderConfig] | None:
193
+ """The OIDC row as the **pre-flight** sees it: configured, but not necessarily enabled.
194
+
195
+ A test sign-in exists to prove a connection before an operator turns it on,
196
+ so it is the one path that must work against a disabled row. It still reads
197
+ the row itself (not the registry, which only holds active providers), and
198
+ it still refuses a row nobody has configured.
199
+ """
200
+
201
+ repository = AuthProvidersRepository(context.session)
202
+ row = (
203
+ await repository.get_provider(provider_id)
204
+ if provider_id is not None
205
+ else await repository.get_by_kind(AuthProviderKind.OIDC.value, DEFAULT_PROVIDER_KEY)
206
+ )
207
+ if row is None or row.kind != AuthProviderKind.OIDC.value:
208
+ return None
209
+ config = load_oidc_config(row)
210
+ return None if config is None else (row, config)
211
+
212
+
213
+ async def _audit_failure(request: Request, context: DashboardAuthContext, *, stage: str) -> None:
214
+ """One bounded ``login_failed`` row per refusal, carrying no value from the wire.
215
+
216
+ ``stage`` is a fixed label from this module. The code, the state, the nonce
217
+ and every token are deliberately absent: the audit sanitizer redacts none of
218
+ those key names, so keeping them out is the only thing that keeps them out.
219
+ The public route also cannot be allowed to become an unbounded row source,
220
+ so the same budget the anonymous password refusals spend applies here.
221
+ """
222
+
223
+ try:
224
+ await get_login_failed_audit_rate_limiter().check_and_increment(
225
+ _session_client_key(request, prefix="login_failed_audit"), context.session
226
+ )
227
+ except DashboardRateLimitError:
228
+ return
229
+ AuditService.log_async(
230
+ "login_failed",
231
+ actor_ip=_client_host(request),
232
+ details={
233
+ "method": AuditAuthMethod.OIDC.value,
234
+ "username": None,
235
+ "reason": "oidc_login_failed",
236
+ "stage": stage,
237
+ },
238
+ severity=AuditSeverity.WARNING,
239
+ )
240
+
241
+
242
+ async def _failure(request: Request, context: DashboardAuthContext, *, stage: str) -> RedirectResponse:
243
+ await _audit_failure(request, context, stage=stage)
244
+ return _redirect(OIDC_FAILURE_PATH)
245
+
246
+
247
+ async def _begin_flow(
248
+ context: DashboardAuthContext,
249
+ *,
250
+ row: DashboardAuthProvider,
251
+ config: OidcProviderConfig,
252
+ purpose: OidcFlowPurpose,
253
+ acting_user_id: str | None = None,
254
+ prompt_login: bool = False,
255
+ ) -> tuple[str, str]:
256
+ """Mint one round trip: the authorization URL, and the ``state`` its browser must carry.
257
+
258
+ Every flow — sign-in, pre-flight, re-authentication — is created here, so
259
+ there is one place where the ``state``, the ``nonce`` and the PKCE verifier
260
+ are generated, one place where only their hashes are stored, and one place
261
+ that takes the redirect URI from the row instead of from the request.
262
+ """
263
+
264
+ state, nonce = secrets.token_urlsafe(32), secrets.token_urlsafe(32)
265
+ verifier, challenge = generate_pkce_pair()
266
+ authorization_url = await get_oidc_provider().begin_login(
267
+ provider=row,
268
+ config=config,
269
+ state=state,
270
+ nonce=nonce,
271
+ code_challenge=challenge,
272
+ prompt_login=prompt_login,
273
+ )
274
+ flows = OidcFlowRepository(context.session)
275
+ await flows.purge_expired()
276
+ await flows.create(
277
+ state_hash=hash_flow_value(state),
278
+ provider_id=row.id,
279
+ nonce_hash=hash_flow_value(nonce),
280
+ code_verifier=verifier,
281
+ purpose=purpose,
282
+ acting_user_id=acting_user_id,
283
+ # Sent from the stored configuration and repeated from the row at the
284
+ # exchange: nothing validates or rewrites ``Host``, so a redirect URI
285
+ # built from the request would be attacker-controllable.
286
+ redirect_uri=config.redirect_uri,
287
+ # Which connection document this round trip is about. The callback
288
+ # refuses a flow whose provider has been reconfigured since, and the
289
+ # pre-flight's proof is stamped only against this same document.
290
+ config_fingerprint=oidc_config_fingerprint(config),
291
+ )
292
+ return authorization_url, state
293
+
294
+
295
+ async def _spend_start_budget(request: Request, context: DashboardAuthContext) -> None:
296
+ try:
297
+ await get_oidc_address_rate_limiter().check_and_increment(
298
+ _session_client_key(request, prefix="oidc_start"), context.session
299
+ )
300
+ except DashboardRateLimitError as exc:
301
+ raise _rate_limit_error(exc, code="oidc_rate_limited") from exc
302
+
303
+
304
+ def _start_response(authorization_url: str, request: Request, state: str) -> JSONResponse:
305
+ """The authorization URL as JSON, with the flow cookie that binds it to this browser."""
306
+
307
+ body = OidcStartResponse(authorization_url=authorization_url)
308
+ response = JSONResponse(status_code=200, content=body.model_dump(by_alias=True))
309
+ _set_flow_cookie(response, request, state)
310
+ return response
311
+
312
+
313
+ @router.get(_LOGIN_START_ROUTE, include_in_schema=False)
314
+ async def start_oidc_login(
315
+ request: Request,
316
+ context: DashboardAuthContext = Depends(get_dashboard_auth_context),
317
+ ) -> RedirectResponse:
318
+ """Public: begin a sign-in at the configured identity provider.
319
+
320
+ Login CSRF is not a hole worth a token here: forcing a victim's browser to
321
+ *begin* its own sign-in achieves nothing, and finishing a flow an attacker
322
+ started in the victim's browser is what the flow cookie refuses.
323
+ """
324
+
325
+ await _spend_start_budget(request, context)
326
+ configured = await _active_oidc_row()
327
+ if configured is None:
328
+ raise DashboardNotFoundError("No single sign-on provider is available", code="provider_not_found")
329
+ row, config = configured
330
+ try:
331
+ authorization_url, state = await _begin_flow(context, row=row, config=config, purpose=OidcFlowPurpose.LOGIN)
332
+ except OidcError as exc:
333
+ return await _failure(request, context, stage=exc.stage)
334
+ response = RedirectResponse(authorization_url, status_code=_REDIRECT_STATUS)
335
+ _set_flow_cookie(response, request, state)
336
+ return response
337
+
338
+
339
+ @router.post("/oidc/test-login/start", response_model=OidcStartResponse)
340
+ async def start_oidc_test_login(
341
+ request: Request,
342
+ principal: DashboardPrincipal = Depends(require_dashboard_permission(Permission.SECURITY_WRITE)),
343
+ context: DashboardAuthContext = Depends(get_dashboard_auth_context),
344
+ ) -> JSONResponse:
345
+ """Prove a connection before turning it on: the admin's own browser makes the round trip.
346
+
347
+ Deliberately runs against the row as stored, enabled or not — a pre-flight
348
+ that needed the provider to be on already would prove nothing, since
349
+ turning it on is what it guards. The proof it produces belongs to this
350
+ account (``principal.user_id``) and expires in ten minutes, so a connection
351
+ that stops working cannot be enabled from an old success.
352
+
353
+ An identity provider this server cannot reach answers ``502`` rather than
354
+ the sign-in flow's uniform failure: the caller is the admin who typed the
355
+ URL, telling them it does not resolve is the entire point of a pre-flight,
356
+ and nothing from the identity provider's own response is reflected.
357
+ """
358
+
359
+ if principal.user_id is None:
360
+ raise DashboardConflictError(
361
+ "Set a dashboard password and sign in before connecting single sign-on",
362
+ code="admin_account_required",
363
+ )
364
+ await _spend_start_budget(request, context)
365
+ configured = await _configured_oidc_row(context)
366
+ if configured is None:
367
+ raise DashboardNotFoundError("No single sign-on provider is configured", code="provider_not_found")
368
+ row, config = configured
369
+ try:
370
+ authorization_url, state = await _begin_flow(
371
+ context,
372
+ row=row,
373
+ config=config,
374
+ purpose=OidcFlowPurpose.TEST,
375
+ acting_user_id=principal.user_id,
376
+ )
377
+ except OidcError as exc:
378
+ raise DashboardUpstreamError(
379
+ "The identity provider could not be reached with these settings", code="oidc_provider_unreachable"
380
+ ) from exc
381
+ return _start_response(authorization_url, request, state)
382
+
383
+
384
+ @router.post("/oidc/step-up/start", response_model=OidcStartResponse)
385
+ async def start_oidc_step_up(
386
+ request: Request,
387
+ context: DashboardAuthContext = Depends(get_dashboard_auth_context),
388
+ ) -> JSONResponse:
389
+ """Re-authenticate at the identity provider as a step-up factor (PLAN §4.6, H5).
390
+
391
+ For the account whose only credential *is* its identity provider. It asks
392
+ for ``prompt=login`` and ``max_age=0``, and the callback refuses anything
393
+ the identity provider does not stamp with a fresh ``auth_time`` — otherwise
394
+ "re-authenticate" would be satisfied by a provider session the attacker's
395
+ stolen cookie already rides alongside.
396
+ """
397
+
398
+ principal = await validate_dashboard_session(request)
399
+ if principal.user_id is None:
400
+ raise DashboardAuthError("A user account session is required", code="user_account_required")
401
+ await _spend_start_budget(request, context)
402
+ configured = await _active_oidc_row()
403
+ if configured is None:
404
+ raise DashboardNotFoundError("No single sign-on provider is available", code="provider_not_found")
405
+ row, config = configured
406
+ try:
407
+ authorization_url, state = await _begin_flow(
408
+ context,
409
+ row=row,
410
+ config=config,
411
+ purpose=OidcFlowPurpose.STEP_UP,
412
+ acting_user_id=principal.user_id,
413
+ prompt_login=True,
414
+ )
415
+ except OidcError as exc:
416
+ raise DashboardUpstreamError(
417
+ "The identity provider could not be reached", code="oidc_provider_unreachable"
418
+ ) from exc
419
+ return _start_response(authorization_url, request, state)
420
+
421
+
422
+ @router.get("/oidc/callback", include_in_schema=False)
423
+ async def complete_oidc_login(
424
+ request: Request,
425
+ state: str | None = None,
426
+ code: str | None = None,
427
+ error: str | None = None,
428
+ context: DashboardAuthContext = Depends(get_dashboard_auth_context),
429
+ ) -> RedirectResponse:
430
+ """Public: finish the round trip, or fail the same way for every reason.
431
+
432
+ ``error`` is accepted so a refusal at the identity provider is not a 422,
433
+ and is then ignored: nothing the identity provider says about why is
434
+ reflected to the browser, to a log line or to an audit row.
435
+ """
436
+
437
+ try:
438
+ await get_oidc_address_rate_limiter().check_and_increment(
439
+ _session_client_key(request, prefix="oidc_callback"), context.session
440
+ )
441
+ except DashboardRateLimitError as exc:
442
+ raise _rate_limit_error(exc, code="oidc_rate_limited") from exc
443
+
444
+ cookie_state = get_oidc_flow_cookie_store().get(request.cookies.get(OIDC_FLOW_COOKIE))
445
+ # Compared as hashes rather than as the values: constant time over a fixed
446
+ # length, and ``compare_digest`` refuses non-ASCII text, which a caller can
447
+ # put in a query parameter but never in one of our sealed cookies.
448
+ state_hash = "" if state is None else hash_flow_value(state)
449
+ if cookie_state is None or state is None or not hmac.compare_digest(hash_flow_value(cookie_state), state_hash):
450
+ # The browser that started this flow is the only one that may finish
451
+ # it; a state without its cookie is somebody else's (or nobody's).
452
+ return await _failure(request, context, stage="state")
453
+
454
+ state_key = f"oidc_callback_state:{state_hash}"
455
+ try:
456
+ await get_oidc_state_rate_limiter().check_and_increment(state_key, context.session)
457
+ except DashboardRateLimitError as exc:
458
+ raise _rate_limit_error(exc, code="oidc_rate_limited") from exc
459
+
460
+ flows = OidcFlowRepository(context.session)
461
+ # The point of no return, and it happens BEFORE the exchange: exactly one
462
+ # caller in the fleet wins this row, so a replayed authorization code can
463
+ # never reach the token endpoint twice through this system.
464
+ record = await flows.consume(state_hash)
465
+ if record is None:
466
+ return await _failure(request, context, stage="flow")
467
+ if error is not None or not code:
468
+ return await _failure(request, context, stage="authorization")
469
+ try:
470
+ purpose = OidcFlowPurpose(record.purpose)
471
+ except ValueError: # pragma: no cover - the column is only ever written from the enum
472
+ return await _failure(request, context, stage="purpose")
473
+
474
+ # A pre-flight runs against the row as stored, because proving a connection
475
+ # is what precedes enabling it. A sign-in and a re-authentication need the
476
+ # provider to be active right now, and to still be the same row the flow
477
+ # was started against.
478
+ configured = (
479
+ await _configured_oidc_row(context, provider_id=record.provider_id)
480
+ if purpose is OidcFlowPurpose.TEST
481
+ else await _active_oidc_row()
482
+ )
483
+ if (
484
+ configured is None
485
+ or configured[0].id != record.provider_id
486
+ or oidc_config_fingerprint(configured[1]) != record.config_fingerprint
487
+ ):
488
+ # The provider was disabled, reconfigured or repointed while the person
489
+ # was at the identity provider. A flow completes against the connection
490
+ # document it was started against or not at all: the code was issued by
491
+ # that identity provider, and a pre-flight proves the configuration it
492
+ # actually reached rather than whichever one the row holds when the
493
+ # browser comes back.
494
+ return await _failure(request, context, stage="provider")
495
+ row, config = configured
496
+
497
+ try:
498
+ completion = await get_oidc_provider().complete_login(
499
+ provider=row,
500
+ config=config,
501
+ code=code,
502
+ code_verifier=record.code_verifier,
503
+ # Repeated from the row, not re-derived: RFC 6749 requires the
504
+ # token request to carry the same redirect URI the authorization
505
+ # request did, and a configuration edit mid-flow must not change it
506
+ # underneath the exchange.
507
+ redirect_uri=record.redirect_uri,
508
+ nonce_hash=record.nonce_hash,
509
+ flow_created_at=record.created_at_epoch,
510
+ )
511
+ except OidcError as exc:
512
+ return await _failure(request, context, stage=exc.stage)
513
+
514
+ if purpose is OidcFlowPurpose.LOGIN:
515
+ finished, completed = await _complete_sign_in(request, context, row, completion)
516
+ elif purpose is OidcFlowPurpose.TEST:
517
+ finished, completed = await _complete_test_login(request, context, row, record, completion)
518
+ else:
519
+ finished, completed = await _complete_step_up(request, context, row, record, completion)
520
+ if completed:
521
+ # Only the per-state budget is cleared. The per-address ceiling is the
522
+ # endpoint's only bound on how many flows one client may burn, and
523
+ # handing it to anyone who can complete one round trip would remove it.
524
+ await get_oidc_state_rate_limiter().clear_for_key(state_key, context.session)
525
+ return finished
526
+
527
+
528
+ async def _complete_sign_in(
529
+ request: Request,
530
+ context: DashboardAuthContext,
531
+ row: DashboardAuthProvider,
532
+ completion: OidcCompletion,
533
+ ) -> tuple[RedirectResponse, bool]:
534
+ """The ordinary sign-in: the shared resolver decides who this is, or that it is nobody.
535
+
536
+ The second half of the pair says whether the round trip completed, which is
537
+ the only thing the caller needs it for: whether to hand the per-``state``
538
+ budget back.
539
+ """
540
+
541
+ resolution = await get_identity_resolution_cache().resolve(completion.identity, row, actor_ip=_client_host(request))
542
+ if not isinstance(resolution, ResolvedAccount):
543
+ # The resolver already wrote the ``login_failed reason=unknown_identity``
544
+ # row naming the provider, subject, e-mail and groups; this route adds
545
+ # nothing to it. The pending screen is the one place a failure is
546
+ # allowed to differ, because the person did authenticate.
547
+ return _pending_redirect(request, row, completion.identity), True
548
+
549
+ user = await DashboardUsersRepository(context.session).get_by_id(resolution.user_id)
550
+ if user is None: # pragma: no cover - resolved a moment ago
551
+ return await _failure(request, context, stage="account"), False
552
+ return await _issue_oidc_session(request, user), True
553
+
554
+
555
+ async def _acting_account(
556
+ request: Request,
557
+ context: DashboardAuthContext,
558
+ record: OidcFlowRecord,
559
+ *,
560
+ permission: Permission | None = None,
561
+ ) -> tuple[DashboardPrincipal, DashboardUser] | None:
562
+ """The signed-in account finishing this flow, when it is the one that started it.
563
+
564
+ A pre-flight and a re-authentication are things an account does to itself,
565
+ so the browser coming back must still be that account's, and must still
566
+ hold whatever the start required. Nothing else is accepted: not "some
567
+ admin", not "whoever holds the flow cookie".
568
+
569
+ The row is read fresh rather than from the five-second users cache, like
570
+ the body step-up endpoint does: what follows decides on the account's
571
+ factors and mints a cookie bound to its ``session_generation``, and neither
572
+ may be answered from a copy taken before the request.
573
+ """
574
+
575
+ if record.acting_user_id is None:
576
+ return None
577
+ try:
578
+ principal = await validate_dashboard_session(request)
579
+ if permission is not None:
580
+ ensure_dashboard_permission(principal, permission)
581
+ except AppError:
582
+ return None
583
+ if principal.user_id is None or principal.user_id != record.acting_user_id:
584
+ return None
585
+ user = await DashboardUsersRepository(context.session).get_by_id(principal.user_id)
586
+ return None if user is None else (principal, user)
587
+
588
+
589
+ async def _complete_test_login(
590
+ request: Request,
591
+ context: DashboardAuthContext,
592
+ row: DashboardAuthProvider,
593
+ record: OidcFlowRecord,
594
+ completion: OidcCompletion,
595
+ ) -> tuple[RedirectResponse, bool]:
596
+ """Record that this admin's browser completed a round trip through this configuration.
597
+
598
+ It provisions nothing, links nothing and issues no session: a pre-flight
599
+ that created an account would be a sign-in wearing a pre-flight's name, and
600
+ the identity it just verified may well be one this install ends up
601
+ refusing. All it leaves behind is the stamp the enable gate reads and an
602
+ audit row saying who proved what.
603
+
604
+ The stamp names a configuration, not just a moment: the callback has
605
+ already refused a flow whose provider was reconfigured since it started,
606
+ and the write below is conditional on the row still holding that same
607
+ sealed document, which closes the window between this callback's read and
608
+ its write.
609
+ """
610
+
611
+ acting = await _acting_account(request, context, record, permission=Permission.SECURITY_WRITE)
612
+ if acting is None:
613
+ return await _failure(request, context, stage="acting_account"), False
614
+ principal, user = acting
615
+ # The sealed document as it stood when this callback read the row, taken
616
+ # before the stamping commit expires nothing else. The write is conditional
617
+ # on it: a configuration PATCH that lands while the code is being exchanged
618
+ # clears the proof, and an unconditional stamp would hand it straight back
619
+ # against an issuer nobody has tested.
620
+ verified_config = row.config_encrypted
621
+ stamped = await AuthProvidersRepository(context.session).record_test_login(
622
+ row.id, user_id=user.id, verified_at=utcnow(), verified_config=verified_config
623
+ )
624
+ if not stamped:
625
+ return await _failure(request, context, stage="provider"), False
626
+ AuditService.log_async(
627
+ "oidc_test_login_succeeded",
628
+ actor_ip=_client_host(request),
629
+ details={
630
+ "kind": row.kind,
631
+ "provider_key": row.provider_key,
632
+ "subject": completion.identity.subject,
633
+ },
634
+ # The acting admin as they are signed in right now -- usually a local
635
+ # password session. The round trip proved the *connection*, not a new
636
+ # way for this person to be here.
637
+ actor=AuditActor.from_principal(principal),
638
+ target=AuditTarget("auth_provider", row.id),
639
+ severity=AuditSeverity.WARNING,
640
+ )
641
+ return _redirect(OIDC_SETTINGS_PATH), True
642
+
643
+
644
+ async def _complete_step_up(
645
+ request: Request,
646
+ context: DashboardAuthContext,
647
+ row: DashboardAuthProvider,
648
+ record: OidcFlowRecord,
649
+ completion: OidcCompletion,
650
+ ) -> tuple[RedirectResponse, bool]:
651
+ """A fresh authentication at the identity provider, recorded as this account's step-up.
652
+
653
+ Three things have to hold, and each closes a different hole: the browser
654
+ belongs to the account that started the flow, the identity provider says
655
+ the person authenticated just now (``auth_time``, which is why the request
656
+ carried ``prompt=login`` and ``max_age=0``), and the subject it vouched for
657
+ is already linked to that very account. Without the last one, signing in at
658
+ the identity provider as anybody would step up for whoever's stolen cookie
659
+ started the flow.
660
+ """
661
+
662
+ acting = await _acting_account(request, context, record)
663
+ if acting is None:
664
+ return await _failure(request, context, stage="acting_account"), False
665
+ principal, user = acting
666
+ if step_up_methods(user):
667
+ # The identity provider is an alternative to *nothing*. An account that
668
+ # holds a password or a TOTP secret presents that, here as everywhere
669
+ # else: otherwise a stolen cookie riding alongside a live provider
670
+ # session would step up without presenting anything new. This is the
671
+ # authority; the start route mints no proof and so needs no copy of it.
672
+ return await _failure(request, context, stage="factor"), False
673
+ if not _is_fresh_authentication(completion.auth_time):
674
+ return await _failure(request, context, stage="auth_time"), False
675
+ identity = await DashboardUsersRepository(context.session).get_identity(
676
+ completion.identity.provider, completion.identity.provider_key, completion.identity.subject
677
+ )
678
+ if identity is None or identity.user_id != user.id:
679
+ return await _failure(request, context, stage="identity"), False
680
+
681
+ verified_at = session_clock()
682
+ response = _redirect(OIDC_SETTINGS_PATH)
683
+ # The same two paths the body endpoint mints through -- the session
684
+ # cookie's own ``su`` claim, or the generation-bound step-up cookie -- so
685
+ # this flow adds no third way to hold a step-up.
686
+ await record_step_up_on(response, request, user, verified_at=verified_at)
687
+ AuditService.log_async(
688
+ "step_up_verified",
689
+ actor_ip=_client_host(request),
690
+ details={"username": user.username, "methods": ["oidc"], "provider_key": row.provider_key},
691
+ actor=AuditActor.from_principal(principal),
692
+ target=AuditTarget("user", user.id),
693
+ )
694
+ return response, True
695
+
696
+
697
+ def _is_fresh_authentication(auth_time: int | None) -> bool:
698
+ """Whether the identity provider says the person authenticated inside the step-up window.
699
+
700
+ A missing ``auth_time`` is a refusal, not a pass: ``max_age=0`` was asked
701
+ for, and an identity provider that answers without saying when the person
702
+ authenticated has not told us what we asked. The tolerance is the same
703
+ clock skew the ID token is verified with -- stated, not implied.
704
+ """
705
+
706
+ if auth_time is None:
707
+ return False
708
+ now = session_clock()
709
+ if auth_time > now + OIDC_CLOCK_SKEW_SECONDS:
710
+ return False
711
+ return now - auth_time <= STEP_UP_MAX_AGE_SECONDS + OIDC_CLOCK_SKEW_SECONDS
712
+
713
+
714
+ def _audit_actor(user: DashboardUser) -> AuditActor:
715
+ return AuditActor(
716
+ user_id=user.id,
717
+ username=user.username,
718
+ role_slug=user.role.slug,
719
+ auth_method=AuditAuthMethod.OIDC.value,
720
+ )
721
+
722
+
723
+ async def _issue_oidc_session(request: Request, user: DashboardUser) -> RedirectResponse:
724
+ """The same version-2 account session every other sign-in issues, capped at twelve hours.
725
+
726
+ ``totp_verified`` is false because nothing here verified a local second
727
+ factor: an account that holds a TOTP secret under a policy that requires it
728
+ still has to present it. No step-up is stamped either -- signing in through
729
+ the identity provider is not consent to change security settings, and the
730
+ dedicated step-up flow is what mints that proof.
731
+ """
732
+
733
+ session_id, ttl_seconds = await _create_user_session(
734
+ request,
735
+ user,
736
+ totp_verified=False,
737
+ auth_method=AuditAuthMethod.OIDC.value,
738
+ # The existing "this request is not obviously local" ceiling, reused
739
+ # rather than duplicated: a single sign-on session that outlives the
740
+ # identity provider's own session is a deprovisioning hole. A shorter
741
+ # configured dashboard lifetime still wins.
742
+ max_ttl_seconds=REMOTE_DASHBOARD_SESSION_TTL_SECONDS,
743
+ )
744
+ AuditService.log_async(
745
+ "login_success",
746
+ actor_ip=_client_host(request),
747
+ details={"method": AuditAuthMethod.OIDC.value, "username": user.username},
748
+ actor=_audit_actor(user),
749
+ target=AuditTarget("user", user.id),
750
+ )
751
+ response = _redirect(OIDC_DASHBOARD_PATH)
752
+ _set_session_cookie(response, session_id, request, max_age_seconds=ttl_seconds)
753
+ return response