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.
- app/__init__.py +10 -0
- app/admin_cli.py +494 -0
- app/cli.py +339 -0
- app/codex_sessions_retag.py +487 -0
- app/core/__init__.py +0 -0
- app/core/audit/__init__.py +0 -0
- app/core/audit/service.py +216 -0
- app/core/audit/types.py +94 -0
- app/core/auth/__init__.py +236 -0
- app/core/auth/api_key_cache.py +65 -0
- app/core/auth/dashboard_access.py +429 -0
- app/core/auth/dashboard_mode.py +171 -0
- app/core/auth/dashboard_session_ttl.py +74 -0
- app/core/auth/dashboard_users_cache.py +120 -0
- app/core/auth/dependencies.py +710 -0
- app/core/auth/external_identity.py +78 -0
- app/core/auth/guardian.py +374 -0
- app/core/auth/models.py +49 -0
- app/core/auth/providers/__init__.py +129 -0
- app/core/auth/providers/oidc.py +838 -0
- app/core/auth/providers/registry.py +118 -0
- app/core/auth/refresh.py +394 -0
- app/core/auth/step_up.py +117 -0
- app/core/auth/totp.py +81 -0
- app/core/balancer/__init__.py +71 -0
- app/core/balancer/logic.py +1612 -0
- app/core/balancer/rendezvous_hash.py +21 -0
- app/core/balancer/types.py +21 -0
- app/core/bootstrap.py +172 -0
- app/core/cache/invalidation.py +426 -0
- app/core/clients/__init__.py +0 -0
- app/core/clients/codex.py +996 -0
- app/core/clients/codex_version.py +187 -0
- app/core/clients/codex_version_snapshot.py +2 -0
- app/core/clients/files.py +534 -0
- app/core/clients/headers.py +29 -0
- app/core/clients/http.py +511 -0
- app/core/clients/model_fetcher.py +276 -0
- app/core/clients/native_egress.py +1304 -0
- app/core/clients/oauth.py +477 -0
- app/core/clients/proxy.py +6089 -0
- app/core/clients/proxy_websocket.py +1411 -0
- app/core/clients/rate_limit_reset_credits.py +412 -0
- app/core/clients/stream_errors.py +12 -0
- app/core/clients/thread_cache_identity.py +347 -0
- app/core/clients/usage.py +467 -0
- app/core/clock.py +170 -0
- app/core/config/__init__.py +0 -0
- app/core/config/background_jobs.py +73 -0
- app/core/config/context_window_overrides.py +166 -0
- app/core/config/dashboard_overrides.py +145 -0
- app/core/config/inheritable.py +47 -0
- app/core/config/key_fingerprint.py +115 -0
- app/core/config/settings.py +872 -0
- app/core/config/settings_cache.py +100 -0
- app/core/config/spool_retention.py +217 -0
- app/core/config/tiers.py +174 -0
- app/core/conversation_archive.py +568 -0
- app/core/crypto.py +37 -0
- app/core/draining.py +11 -0
- app/core/errors.py +365 -0
- app/core/exceptions.py +155 -0
- app/core/handlers/__init__.py +3 -0
- app/core/handlers/exceptions.py +478 -0
- app/core/http_protocol.py +176 -0
- app/core/http_protocol_httptools.py +204 -0
- app/core/ingress_limits.py +17 -0
- app/core/metadata_store.py +18 -0
- app/core/metrics/__init__.py +0 -0
- app/core/metrics/middleware.py +74 -0
- app/core/metrics/prometheus.py +604 -0
- app/core/middleware/__init__.py +25 -0
- app/core/middleware/api_firewall.py +111 -0
- app/core/middleware/app_version.py +37 -0
- app/core/middleware/dashboard_auth_proxy.py +55 -0
- app/core/middleware/dashboard_csrf.py +177 -0
- app/core/middleware/dashboard_gzip.py +41 -0
- app/core/middleware/dashboard_overrides.py +59 -0
- app/core/middleware/firewall_cache.py +91 -0
- app/core/middleware/inflight.py +126 -0
- app/core/middleware/multipart_content_encoding.py +129 -0
- app/core/middleware/path_rewrite.py +148 -0
- app/core/middleware/request_body_limit.py +196 -0
- app/core/middleware/request_decompression.py +209 -0
- app/core/middleware/request_id.py +72 -0
- app/core/middleware/required_capability_http.py +116 -0
- app/core/middleware/trusted_proxy_headers.py +36 -0
- app/core/multipart.py +237 -0
- app/core/multipart_fields.py +91 -0
- app/core/openai/__init__.py +0 -0
- app/core/openai/chat_requests.py +493 -0
- app/core/openai/chat_responses.py +931 -0
- app/core/openai/contracts.py +46 -0
- app/core/openai/exceptions.py +24 -0
- app/core/openai/host_models.py +14 -0
- app/core/openai/images.py +413 -0
- app/core/openai/message_coercion.py +376 -0
- app/core/openai/model_refresh_scheduler.py +423 -0
- app/core/openai/model_registry.py +997 -0
- app/core/openai/model_registry_store.py +548 -0
- app/core/openai/models.py +172 -0
- app/core/openai/parsing.py +97 -0
- app/core/openai/requests.py +1980 -0
- app/core/openai/strict_schema.py +199 -0
- app/core/openai/tool_call_safety.py +99 -0
- app/core/openai/v1_requests.py +187 -0
- app/core/plan_types.py +90 -0
- app/core/prestop.py +202 -0
- app/core/rate_limiter/__init__.py +0 -0
- app/core/rate_limiter/db_rate_limiter.py +215 -0
- app/core/request_locality.py +473 -0
- app/core/resilience/__init__.py +0 -0
- app/core/resilience/backpressure.py +57 -0
- app/core/resilience/bulkhead.py +202 -0
- app/core/resilience/circuit_breaker.py +150 -0
- app/core/resilience/degradation.py +55 -0
- app/core/resilience/loop_lag_monitor.py +60 -0
- app/core/resilience/memory_monitor.py +137 -0
- app/core/resilience/network_recovery.py +256 -0
- app/core/resilience/overload.py +98 -0
- app/core/resilience/toggles.py +114 -0
- app/core/retention/__init__.py +0 -0
- app/core/retention/job.py +240 -0
- app/core/retention/scheduler.py +69 -0
- app/core/runtime_logging.py +601 -0
- app/core/scheduling/__init__.py +0 -0
- app/core/scheduling/leader_election.py +993 -0
- app/core/scheduling/leader_election_handle.py +25 -0
- app/core/server.py +140 -0
- app/core/shutdown.py +326 -0
- app/core/socket_peer.py +22 -0
- app/core/startup.py +41 -0
- app/core/timeout_invariants.py +347 -0
- app/core/tracing/__init__.py +0 -0
- app/core/tracing/otel.py +207 -0
- app/core/types.py +6 -0
- app/core/upstream_proxy/__init__.py +13 -0
- app/core/upstream_proxy/cache.py +130 -0
- app/core/upstream_proxy/resolver.py +189 -0
- app/core/upstream_proxy/types.py +104 -0
- app/core/usage/__init__.py +407 -0
- app/core/usage/depletion.py +109 -0
- app/core/usage/live_hub.py +41 -0
- app/core/usage/live_snapshots.py +174 -0
- app/core/usage/logs.py +212 -0
- app/core/usage/metadata_scheduler.py +118 -0
- app/core/usage/models.py +54 -0
- app/core/usage/pricing.py +576 -0
- app/core/usage/pricing_catalog.py +241 -0
- app/core/usage/pricing_snapshot.json +821 -0
- app/core/usage/quota.py +133 -0
- app/core/usage/refresh_policy.py +21 -0
- app/core/usage/refresh_scheduler.py +713 -0
- app/core/usage/reset_credits_refresh_scheduler.py +435 -0
- app/core/usage/types.py +168 -0
- app/core/utils/__init__.py +0 -0
- app/core/utils/json_guards.py +18 -0
- app/core/utils/locks.py +33 -0
- app/core/utils/masking.py +20 -0
- app/core/utils/proxy_env.py +104 -0
- app/core/utils/request_id.py +65 -0
- app/core/utils/retry.py +77 -0
- app/core/utils/shared_future.py +172 -0
- app/core/utils/sse.py +286 -0
- app/core/utils/time.py +24 -0
- app/db/__init__.py +0 -0
- app/db/account_identity_lock.py +56 -0
- app/db/alembic/__init__.py +1 -0
- app/db/alembic/env.py +70 -0
- app/db/alembic/revision_ids.py +43 -0
- app/db/alembic/versions/20260213_000000_base_schema.py +248 -0
- app/db/alembic/versions/20260213_000100_normalize_account_plan_types.py +45 -0
- app/db/alembic/versions/20260213_000200_add_request_logs_reasoning_effort.py +45 -0
- app/db/alembic/versions/20260213_000300_add_accounts_reset_at.py +45 -0
- app/db/alembic/versions/20260213_000400_add_accounts_chatgpt_account_id.py +45 -0
- app/db/alembic/versions/20260213_000500_add_dashboard_settings.py +70 -0
- app/db/alembic/versions/20260213_000600_add_dashboard_settings_totp.py +64 -0
- app/db/alembic/versions/20260213_000700_add_dashboard_settings_password.py +59 -0
- app/db/alembic/versions/20260213_000800_add_api_keys.py +87 -0
- app/db/alembic/versions/20260214_000000_add_api_key_limits.py +143 -0
- app/db/alembic/versions/20260215_000000_add_idx_logs_requested_at.py +38 -0
- app/db/alembic/versions/20260218_000000_add_api_key_usage_reservations.py +117 -0
- app/db/alembic/versions/20260218_000100_add_import_without_overwrite_and_drop_accounts_email_unique.py +198 -0
- app/db/alembic/versions/20260225_000000_add_dashboard_settings_routing_strategy.py +53 -0
- app/db/alembic/versions/20260228_020000_align_api_key_limit_enum_types.py +104 -0
- app/db/alembic/versions/20260228_030000_add_api_firewall_allowlist.py +42 -0
- app/db/alembic/versions/20260306_000000_add_request_logs_service_tier.py +45 -0
- app/db/alembic/versions/20260307_000000_add_api_key_enforcement_fields.py +56 -0
- app/db/alembic/versions/20260308_000000_add_sqlite_performance_indexes.py +41 -0
- app/db/alembic/versions/20260309_000000_add_additional_usage_history.py +71 -0
- app/db/alembic/versions/20260309_000000_request_logs_nullable_account_id.py +28 -0
- app/db/alembic/versions/20260310_000000_add_request_logs_transport.py +45 -0
- app/db/alembic/versions/20260310_000000_fix_postgresql_enum_value_casing.py +79 -0
- app/db/alembic/versions/20260310_120000_add_sticky_session_kinds_and_affinity_ttl.py +110 -0
- app/db/alembic/versions/20260312_000000_add_additional_usage_quota_key.py +193 -0
- app/db/alembic/versions/20260312_000000_split_sticky_sessions_primary_key_by_kind.py +96 -0
- app/db/alembic/versions/20260312_010000_merge_additional_usage_and_sticky_session_heads.py +25 -0
- app/db/alembic/versions/20260312_020000_merge_request_logs_transport_and_feature_heads.py +25 -0
- app/db/alembic/versions/20260312_020000_merge_request_logs_transport_and_sticky_heads.py +25 -0
- app/db/alembic/versions/20260312_120000_add_dashboard_upstream_stream_transport.py +62 -0
- app/db/alembic/versions/20260313_024500_merge_request_log_transport_heads.py +27 -0
- app/db/alembic/versions/20260319_100937_increase_prompt_cache_ttl_to_1800s.py +73 -0
- app/db/alembic/versions/20260319_111500_merge_prompt_cache_ttl_and_upstream_transport_heads.py +25 -0
- app/db/alembic/versions/20260319_183000_normalize_sqlite_account_status_casing.py +48 -0
- app/db/alembic/versions/20260320_000000_add_request_log_requested_actual_tiers.py +51 -0
- app/db/alembic/versions/20260321_190000_tighten_dashboard_db_indexes.py +64 -0
- app/db/alembic/versions/20260321_210000_merge_request_log_tiers_and_dashboard_index_heads.py +25 -0
- app/db/alembic/versions/20260325_000000_add_request_log_cost.py +193 -0
- app/db/alembic/versions/20260328_000000_add_rate_limit_attempts.py +48 -0
- app/db/alembic/versions/20260328_120000_add_request_log_ttft.py +37 -0
- app/db/alembic/versions/20260328_130000_add_audit_logs_table.py +45 -0
- app/db/alembic/versions/20260328_140000_add_scheduler_leader_table.py +39 -0
- app/db/alembic/versions/20260330_000000_add_cache_locality_settings.py +69 -0
- app/db/alembic/versions/20260330_010000_merge_scheduler_leader_and_cache_locality_heads.py +25 -0
- app/db/alembic/versions/20260330_020000_add_bridge_ring_members.py +46 -0
- app/db/alembic/versions/20260401_000000_add_api_key_enforced_service_tier.py +52 -0
- app/db/alembic/versions/20260401_000000_add_cache_invalidation.py +41 -0
- app/db/alembic/versions/20260401_020000_merge_cache_invalidation_and_api_key_service_tier_heads.py +22 -0
- app/db/alembic/versions/20260402_000000_switch_dashboard_routing_default_to_capacity_weighted.py +66 -0
- app/db/alembic/versions/20260403_000000_add_credit_api_key_limit_values.py +44 -0
- app/db/alembic/versions/20260406_000000_add_api_key_account_assignments.py +57 -0
- app/db/alembic/versions/20260406_010000_add_api_key_assignment_scope_flag.py +75 -0
- app/db/alembic/versions/20260407_000000_add_bridge_gateway_safe_mode.py +51 -0
- app/db/alembic/versions/20260407_010000_merge_api_key_assignment_and_bridge_gateway_heads.py +25 -0
- app/db/alembic/versions/20260408_000000_switch_import_without_overwrite_default_to_true.py +73 -0
- app/db/alembic/versions/20260408_010000_merge_import_without_overwrite_and_assignment_heads.py +25 -0
- app/db/alembic/versions/20260409_000000_add_http_bridge_sessions.py +130 -0
- app/db/alembic/versions/20260409_000000_switch_sticky_threads_and_prefer_earlier_reset_defaults_to_true.py +89 -0
- app/db/alembic/versions/20260409_010000_add_dashboard_settings_bootstrap_token.py +57 -0
- app/db/alembic/versions/20260409_010000_merge_http_bridge_and_import_without_overwrite_heads.py +25 -0
- app/db/alembic/versions/20260409_020000_fix_http_bridge_last_seen_index.py +46 -0
- app/db/alembic/versions/20260410_000000_backfill_pristine_dashboard_settings_defaults.py +77 -0
- app/db/alembic/versions/20260410_030000_restore_import_without_overwrite_default_true.py +73 -0
- app/db/alembic/versions/20260410_040000_merge_dashboard_defaults_and_import_default_heads.py +25 -0
- app/db/alembic/versions/20260413_000000_add_accounts_blocked_at.py +44 -0
- app/db/alembic/versions/20260415_160000_add_automations_tables.py +189 -0
- app/db/alembic/versions/20260415_160000_add_request_logs_response_lookup_index.py +67 -0
- app/db/alembic/versions/20260417_000000_add_request_log_plan_type.py +44 -0
- app/db/alembic/versions/20260418_190000_add_automation_runs_cycle_key.py +87 -0
- app/db/alembic/versions/20260419_000000_add_automation_run_cycle_metadata.py +56 -0
- app/db/alembic/versions/20260419_020000_add_automation_run_cycles_snapshot_tables.py +342 -0
- app/db/alembic/versions/20260421_000000_add_dashboard_session_ttl_setting.py +51 -0
- app/db/alembic/versions/20260421_120000_merge_request_log_lookup_and_plan_type_heads.py +25 -0
- app/db/alembic/versions/20260421_130000_merge_automation_and_request_log_heads.py +25 -0
- app/db/alembic/versions/20260422_103000_normalize_legacy_automation_run_cycle_keys.py +648 -0
- app/db/alembic/versions/20260423_120000_add_api_key_limit_reset_at_index.py +43 -0
- app/db/alembic/versions/20260424_000000_merge_dashboard_session_ttl_and_request_log_heads.py +25 -0
- app/db/alembic/versions/20260426_000000_add_dashboard_relative_availability_settings.py +68 -0
- app/db/alembic/versions/20260427_130000_add_warmup_model_and_request_kind.py +75 -0
- app/db/alembic/versions/20260506_000000_add_account_routing_policy.py +37 -0
- app/db/alembic/versions/20260509_000000_add_api_key_traffic_class.py +37 -0
- app/db/alembic/versions/20260509_010000_add_additional_quota_routing_policies.py +37 -0
- app/db/alembic/versions/20260513_000000_add_accounts_alias.py +44 -0
- app/db/alembic/versions/20260513_000000_add_api_key_apply_to_codex_model.py +59 -0
- app/db/alembic/versions/20260514_000000_add_request_logs_api_key_time_index.py +43 -0
- app/db/alembic/versions/20260515_000000_add_prefer_earlier_reset_window.py +47 -0
- app/db/alembic/versions/20260515_000000_soft_delete_request_logs_on_account_delete.py +83 -0
- app/db/alembic/versions/20260515_020000_add_split_sticky_budget_thresholds.py +62 -0
- app/db/alembic/versions/20260516_000000_add_sqlite_hot_path_indexes.py +75 -0
- app/db/alembic/versions/20260517_000000_merge_sqlite_hot_path_and_request_log_delete_heads.py +24 -0
- app/db/alembic/versions/20260518_000000_add_http_bridge_durable_input_prefix.py +44 -0
- app/db/alembic/versions/20260518_010000_merge_http_bridge_and_sqlite_recovery_heads.py +25 -0
- app/db/alembic/versions/20260518_120000_add_dashboard_guest_access.py +57 -0
- app/db/alembic/versions/20260520_000000_merge_api_key_and_http_bridge_heads.py +26 -0
- app/db/alembic/versions/20260520_010000_add_request_logs_api_key_account_index.py +45 -0
- app/db/alembic/versions/20260520_010000_merge_automation_and_api_key_heads.py +26 -0
- app/db/alembic/versions/20260520_030000_add_quota_planner.py +194 -0
- app/db/alembic/versions/20260520_040000_merge_quota_planner_and_request_log_index_heads.py +24 -0
- app/db/alembic/versions/20260521_000000_add_account_security_work_authorized.py +46 -0
- app/db/alembic/versions/20260521_000000_merge_dashboard_guest_and_api_key_heads.py +26 -0
- app/db/alembic/versions/20260521_000000_merge_prefer_reset_window_head.py +26 -0
- app/db/alembic/versions/20260521_000000_merge_warmup_and_request_log_index_heads.py +25 -0
- app/db/alembic/versions/20260522_000000_add_limit_warmup_trigger.py +195 -0
- app/db/alembic/versions/20260524_000000_merge_accounts_alias_and_reset_window_heads.py +24 -0
- app/db/alembic/versions/20260525_000000_add_usage_raw_window_latest_index.py +31 -0
- app/db/alembic/versions/20260526_000000_add_request_log_failure_metadata.py +100 -0
- app/db/alembic/versions/20260531_000000_add_account_workspace_identity.py +39 -0
- app/db/alembic/versions/20260531_000000_add_single_account_routing.py +42 -0
- app/db/alembic/versions/20260601_000000_merge_relative_availability_and_usage_raw_heads.py +24 -0
- app/db/alembic/versions/20260601_010000_add_api_key_usage_sections.py +58 -0
- app/db/alembic/versions/20260601_010000_add_routing_policy_persistence.py +67 -0
- app/db/alembic/versions/20260601_010000_merge_security_work_and_relative_availability_heads.py +25 -0
- app/db/alembic/versions/20260601_010000_merge_split_sticky_and_relative_usage_heads.py +25 -0
- app/db/alembic/versions/20260601_010000_merge_warmup_and_relative_availability_heads.py +25 -0
- app/db/alembic/versions/20260601_020000_merge_additional_quota_routing_and_relative_availability_heads.py +25 -0
- app/db/alembic/versions/20260601_020000_merge_quota_planner_and_relative_availability_heads.py +25 -0
- app/db/alembic/versions/20260601_020000_merge_reset_window_and_relative_availability_heads.py +25 -0
- app/db/alembic/versions/20260601_020000_merge_routing_policy_and_split_threshold_heads.py +24 -0
- app/db/alembic/versions/20260601_020000_merge_warmup_and_request_log_failure_heads.py +25 -0
- app/db/alembic/versions/20260601_030000_merge_single_account_and_routing_quota_heads.py +29 -0
- app/db/alembic/versions/20260601_040000_merge_security_work_and_routing_consolidation_heads.py +25 -0
- app/db/alembic/versions/20260601_050000_merge_quota_planner_and_routing_consolidation_heads.py +25 -0
- app/db/alembic/versions/20260602_050000_add_upstream_proxy_routing.py +164 -0
- app/db/alembic/versions/20260602_060000_merge_account_workspace_and_failure_heads.py +25 -0
- app/db/alembic/versions/20260602_070000_add_request_log_useragent_fields.py +86 -0
- app/db/alembic/versions/20260602_070000_merge_quota_planner_and_account_workspace_heads.py +25 -0
- app/db/alembic/versions/20260602_080000_merge_upstream_proxy_and_quota_planner_heads.py +25 -0
- app/db/alembic/versions/20260602_080000_merge_useragent_and_upstream_proxy_heads.py +24 -0
- app/db/alembic/versions/20260603_000000_add_weekly_pace_working_days.py +52 -0
- app/db/alembic/versions/20260603_000000_free_account_monthly_window.py +60 -0
- app/db/alembic/versions/20260604_000000_add_reauth_required_account_status.py +159 -0
- app/db/alembic/versions/20260604_010000_merge_automations_and_reauth_status_heads.py +25 -0
- app/db/alembic/versions/20260604_010000_merge_dashboard_guest_and_reauth_status_heads.py +25 -0
- app/db/alembic/versions/20260604_010000_merge_useragent_and_reauth_heads.py +24 -0
- app/db/alembic/versions/20260607_000000_merge_weekly_monthly_useragent_heads.py +28 -0
- app/db/alembic/versions/20260608_000000_add_hide_upstream_quota_from_api_keys.py +50 -0
- app/db/alembic/versions/20260611_000000_add_request_log_archive_request_id.py +47 -0
- app/db/alembic/versions/20260611_000000_merge_automations_and_weekly_monthly_useragent_heads.py +26 -0
- app/db/alembic/versions/20260611_000000_merge_dashboard_guest_and_weekly_useragent_heads.py +26 -0
- app/db/alembic/versions/20260611_010000_add_request_log_client_ip.py +57 -0
- app/db/alembic/versions/20260611_020000_merge_request_log_archive_and_client_ip_heads.py +26 -0
- app/db/alembic/versions/20260611_030000_merge_dashboard_guest_and_request_log_client_ip_heads.py +26 -0
- app/db/alembic/versions/20260613_000000_add_accounts_codex_installation_id.py +57 -0
- app/db/alembic/versions/20260613_010000_merge_installation_id_and_dashboard_weekly_heads.py +26 -0
- app/db/alembic/versions/20260615_000000_merge_automations_and_dashboard_guest_heads.py +26 -0
- app/db/alembic/versions/20260623_000000_add_limit_warmup_exhausted_threshold.py +51 -0
- app/db/alembic/versions/20260626_000000_add_http_downstream_transport_policy.py +46 -0
- app/db/alembic/versions/20260626_010000_add_request_logs_upstream_transport.py +31 -0
- app/db/alembic/versions/20260629_000000_add_dashboard_query_hot_path_indexes.py +53 -0
- app/db/alembic/versions/20260630_000000_add_limit_warmup_staggered_idle.py +47 -0
- app/db/alembic/versions/20260630_000000_merge_request_log_client_ip_and_dashboard_hot_path_heads.py +26 -0
- app/db/alembic/versions/20260630_010000_merge_warmup_and_request_log_dashboard_heads.py +26 -0
- app/db/alembic/versions/20260630_020000_merge_automations_and_main_heads.py +26 -0
- app/db/alembic/versions/20260630_020000_merge_warmup_threshold_and_main_heads.py +26 -0
- app/db/alembic/versions/20260630_030000_add_automation_run_model_snapshot.py +59 -0
- app/db/alembic/versions/20260630_040000_merge_automation_snapshot_and_warmup_threshold_heads.py +26 -0
- app/db/alembic/versions/20260630_050000_add_automation_run_prompt_snapshot.py +52 -0
- app/db/alembic/versions/20260701_000000_add_weekly_pace_smoothing_minutes.py +51 -0
- app/db/alembic/versions/20260702_000000_add_model_sources.py +176 -0
- app/db/alembic/versions/20260705_000000_add_model_source_audio_transcriptions.py +50 -0
- app/db/alembic/versions/20260705_000000_harden_dashboard_session_ttl.py +62 -0
- app/db/alembic/versions/20260706_000000_add_model_source_audio_per_minute.py +43 -0
- app/db/alembic/versions/20260707_000000_merge_model_sources_and_session_ttl.py +24 -0
- app/db/alembic/versions/20260707_010000_merge_automations_and_model_sources_heads.py +24 -0
- app/db/alembic/versions/20260707_020000_add_automation_job_account_scope.py +125 -0
- app/db/alembic/versions/20260709_000000_add_ttft_phase_observability.py +66 -0
- app/db/alembic/versions/20260711_000000_add_dashboard_prohibit_fast_mode.py +42 -0
- app/db/alembic/versions/20260711_010000_add_account_chatgpt_user_id.py +40 -0
- app/db/alembic/versions/20260711_020000_add_dashboard_account_concurrency_caps.py +71 -0
- app/db/alembic/versions/20260711_030000_add_limit_warmup_idle_threshold.py +51 -0
- app/db/alembic/versions/20260712_010000_add_account_usage_rollups.py +56 -0
- app/db/alembic/versions/20260712_020000_add_api_key_usage_rollups.py +53 -0
- app/db/alembic/versions/20260713_020000_add_model_registry_snapshot.py +42 -0
- app/db/alembic/versions/20260713_040000_add_account_refresh_claims.py +42 -0
- app/db/alembic/versions/20260713_040000_add_replica_guardrails.py +64 -0
- app/db/alembic/versions/20260713_070000_add_reset_credit_redeem_tables.py +58 -0
- app/db/alembic/versions/20260714_000000_add_oauth_flow_states.py +63 -0
- app/db/alembic/versions/20260715_000000_add_request_log_queue_latency.py +44 -0
- app/db/alembic/versions/20260716_000000_add_oauth_device_flow_slots.py +41 -0
- app/db/alembic/versions/20260716_010000_add_dashboard_retention_settings.py +48 -0
- app/db/alembic/versions/20260716_010000_add_reset_credit_display_settings.py +73 -0
- app/db/alembic/versions/20260717_000000_add_http_bridge_retry_circuits.py +47 -0
- app/db/alembic/versions/20260717_000000_merge_retention_and_reset_credit_display_heads.py +26 -0
- app/db/alembic/versions/20260717_000000_optimize_dashboard_hot_path_indexes.py +162 -0
- app/db/alembic/versions/20260717_000001_merge_retry_circuits_and_dashboard_indexes.py +22 -0
- app/db/alembic/versions/20260720_000000_add_request_log_conversation_id.py +57 -0
- app/db/alembic/versions/20260722_000000_backfill_request_log_useragent_families.py +40 -0
- app/db/alembic/versions/20260724_000000_add_request_usage_time_rollups.py +176 -0
- app/db/alembic/versions/20260724_000000_merge_request_log_schema_heads.py +33 -0
- app/db/alembic/versions/20260725_000000_add_http_bridge_pending_tool_calls.py +43 -0
- app/db/alembic/versions/20260726_000000_add_account_plan_downgrade_observations.py +44 -0
- app/db/alembic/versions/20260726_000000_repair_request_usage_rollups_after_merge.py +37 -0
- app/db/alembic/versions/20260727_000000_add_sticky_session_continuity_abandoned_at.py +45 -0
- app/db/alembic/versions/20260728_000000_merge_pending_tool_calls_and_rollup_repair_heads.py +31 -0
- app/db/alembic/versions/20260730_000000_add_api_key_fair_share_threshold.py +44 -0
- app/db/alembic/versions/20260730_000000_add_http_bridge_recovery_attempts.py +60 -0
- app/db/alembic/versions/20260731_000000_add_capability_lineage_markers.py +38 -0
- app/db/alembic/versions/20260803_000000_merge_http_bridge_recovery_and_capability_lineage_heads.py +30 -0
- app/db/alembic/versions/20260804_000000_add_http_bridge_operations.py +63 -0
- app/db/alembic/versions/20260804_000001_add_global_http_bridge_operation_fingerprint.py +53 -0
- app/db/alembic/versions/20260804_230000_add_request_log_connection_request_kind.py +43 -0
- app/db/alembic/versions/20260805_000000_add_http_bridge_operation_spool.py +75 -0
- app/db/alembic/versions/20260805_000001_finalize_http_bridge_operation_spool.py +65 -0
- app/db/alembic/versions/20260806_000000_add_additional_usage_alias_probe_indexes.py +84 -0
- app/db/alembic/versions/20260806_000000_add_anonymous_telemetry.py +49 -0
- app/db/alembic/versions/20260806_010000_add_conversation_presence_rollup.py +72 -0
- app/db/alembic/versions/20260806_020000_add_usage_history_bulk_covering_indexes.py +99 -0
- app/db/alembic/versions/20260806_030000_add_api_key_allowed_reasoning_efforts.py +65 -0
- app/db/alembic/versions/20260806_120000_add_http_bridge_owner_process_epoch.py +57 -0
- app/db/alembic/versions/20260807_000000_merge_http_bridge_operation_spool_and_latest_main.py +24 -0
- app/db/alembic/versions/20260808_000000_tune_usage_history_autovacuum.py +54 -0
- app/db/alembic/versions/20260810_000000_add_http_bridge_recovery_dispatch_count.py +44 -0
- app/db/alembic/versions/20260811_000000_add_hourly_rollup_cancelled_count.py +92 -0
- app/db/alembic/versions/20260812_000000_merge_recovery_dispatch_and_hourly_cancelled_heads.py +25 -0
- app/db/alembic/versions/20260812_120000_add_sticky_abandonment_scope.py +43 -0
- app/db/alembic/versions/20260813_000000_add_file_account_pins.py +40 -0
- app/db/alembic/versions/20260816_000000_add_account_pending_deletion.py +81 -0
- app/db/alembic/versions/20260816_000000_add_model_source_embeddings.py +50 -0
- app/db/alembic/versions/20260821_000000_add_retry_circuit_admission_generation.py +49 -0
- app/db/alembic/versions/20260826_000000_add_http_bridge_event_chunks.py +107 -0
- app/db/alembic/versions/20260828_000000_add_accounts_chatgpt_identity_index.py +55 -0
- app/db/alembic/versions/20260830_000000_add_quota_warmup_claim_expiry.py +83 -0
- app/db/alembic/versions/20260908_000000_add_guest_session_generation.py +37 -0
- app/db/alembic/versions/20260908_000000_add_subscription_overflow.py +140 -0
- app/db/alembic/versions/20260908_000000_replace_upstream_stream_transport_default_sentinel.py +50 -0
- app/db/alembic/versions/20260908_020000_merge_overflow_transport_heads.py +27 -0
- app/db/alembic/versions/20260909_000000_add_dashboard_roles.py +72 -0
- app/db/alembic/versions/20260909_010000_add_dashboard_users.py +183 -0
- app/db/alembic/versions/20260909_020000_reproject_compat_admin_credentials.py +105 -0
- app/db/alembic/versions/20260909_030000_add_audit_actor_columns.py +103 -0
- app/db/alembic/versions/20260909_030000_dashboard_resilience_toggle_settings.py +63 -0
- app/db/alembic/versions/20260909_040000_add_dashboard_user_invites.py +54 -0
- app/db/alembic/versions/20260909_040000_dashboard_timeout_settings.py +68 -0
- app/db/alembic/versions/20260909_050000_dashboard_routing_overload_settings.py +65 -0
- app/db/alembic/versions/20260909_060000_add_report_rollup.py +74 -0
- app/db/alembic/versions/20260909_070000_automation_run_claim_budget.py +50 -0
- app/db/alembic/versions/20260909_080000_dashboard_stream_bridge_budgets.py +62 -0
- app/db/alembic/versions/20260909_090000_dashboard_background_job_toggles.py +63 -0
- app/db/alembic/versions/20260909_100000_dashboard_codex_prewarm.py +49 -0
- app/db/alembic/versions/20260909_110000_model_context_window_overrides.py +45 -0
- app/db/alembic/versions/20260909_120000_dashboard_conversation_archive.py +48 -0
- app/db/alembic/versions/20260909_130000_add_request_logs_live_facet_indexes.py +96 -0
- app/db/alembic/versions/20260910_000000_add_totp_required_for_admin_role.py +44 -0
- app/db/alembic/versions/20260910_000000_request_logs_missing_cost_index.py +52 -0
- app/db/alembic/versions/20260910_010000_add_dashboard_auth_providers.py +134 -0
- app/db/alembic/versions/20260910_010000_dashboard_spool_retention.py +52 -0
- app/db/alembic/versions/20260910_020000_add_dashboard_role_mappings.py +74 -0
- app/db/alembic/versions/20260910_143000_request_log_affinity.py +28 -0
- app/db/alembic/versions/20260910_180000_merge_affinity_guest_heads.py +17 -0
- app/db/alembic/versions/20260910_200000_merge_affinity_identity_heads.py +17 -0
- app/db/alembic/versions/20260910_220000_merge_affinity_invite_heads.py +17 -0
- app/db/alembic/versions/20260911_000000_model_source_pins_kind_expires_index.py +43 -0
- app/db/alembic/versions/20260911_010000_merge_pin_index_and_affinity_heads.py +17 -0
- app/db/alembic/versions/20260911_020000_add_http_bridge_terminal_append_phase.py +54 -0
- app/db/alembic/versions/20260911_030000_add_local_login_policy.py +48 -0
- app/db/alembic/versions/20260911_040000_add_thread_cache_identity_mode.py +48 -0
- app/db/alembic/versions/20260911_060000_add_bridge_session_continuity_abandonment.py +40 -0
- app/db/alembic/versions/20260912_000000_merge_thread_cache_and_bridge_retirement_heads.py +22 -0
- app/db/alembic/versions/20260912_010000_drop_legacy_dashboard_credentials.py +140 -0
- app/db/alembic/versions/20260913_000000_add_oidc_provider_flow.py +134 -0
- app/db/alembic/versions/20260914_000000_add_scim_tokens.py +78 -0
- app/db/alembic/versions/20260914_000000_drop_subscription_overflow_schema.py +132 -0
- app/db/alembic/versions/20260918_000000_merge_scim_and_overflow_heads.py +17 -0
- app/db/alembic/versions/__init__.py +1 -0
- app/db/backup.py +64 -0
- app/db/migrate.py +1249 -0
- app/db/migration_lock.py +160 -0
- app/db/migration_url.py +18 -0
- app/db/models.py +3100 -0
- app/db/recover.py +244 -0
- app/db/session.py +1295 -0
- app/db/snapshot.py +46 -0
- app/db/sqlite_lock_retry.py +190 -0
- app/db/sqlite_utils.py +549 -0
- app/dependencies.py +462 -0
- app/main.py +1240 -0
- app/modules/__init__.py +0 -0
- app/modules/accounts/__init__.py +0 -0
- app/modules/accounts/api.py +426 -0
- app/modules/accounts/auth_manager.py +1439 -0
- app/modules/accounts/background_repository.py +160 -0
- app/modules/accounts/deletion.py +540 -0
- app/modules/accounts/mappers.py +654 -0
- app/modules/accounts/refresh_claims.py +352 -0
- app/modules/accounts/repository.py +1679 -0
- app/modules/accounts/schemas.py +308 -0
- app/modules/accounts/service.py +809 -0
- app/modules/accounts/usage_rollup.py +486 -0
- app/modules/accounts/usage_rollup_scheduler.py +75 -0
- app/modules/accounts/usage_time_rollup.py +1037 -0
- app/modules/accounts/usage_time_rollup_read.py +522 -0
- app/modules/api_keys/__init__.py +1 -0
- app/modules/api_keys/api.py +344 -0
- app/modules/api_keys/last_used_coalescer.py +216 -0
- app/modules/api_keys/limit_windows.py +41 -0
- app/modules/api_keys/repository.py +1200 -0
- app/modules/api_keys/reset_scheduler.py +85 -0
- app/modules/api_keys/schemas.py +135 -0
- app/modules/api_keys/service.py +2123 -0
- app/modules/audit/__init__.py +0 -0
- app/modules/audit/api.py +88 -0
- app/modules/audit/repository.py +74 -0
- app/modules/audit/schemas.py +33 -0
- app/modules/audit/service.py +71 -0
- app/modules/auth_providers/__init__.py +0 -0
- app/modules/auth_providers/api.py +127 -0
- app/modules/auth_providers/config.py +343 -0
- app/modules/auth_providers/repository.py +101 -0
- app/modules/auth_providers/schemas.py +88 -0
- app/modules/auth_providers/seed.py +105 -0
- app/modules/auth_providers/service.py +289 -0
- app/modules/automations/__init__.py +1 -0
- app/modules/automations/api.py +369 -0
- app/modules/automations/repository.py +1896 -0
- app/modules/automations/scheduler.py +107 -0
- app/modules/automations/schemas.py +158 -0
- app/modules/automations/service.py +2548 -0
- app/modules/cache_isolation_probe/__init__.py +0 -0
- app/modules/cache_isolation_probe/api.py +188 -0
- app/modules/cache_isolation_probe/prefix.py +144 -0
- app/modules/cache_isolation_probe/schemas.py +94 -0
- app/modules/cache_isolation_probe/sender.py +229 -0
- app/modules/cache_isolation_probe/service.py +525 -0
- app/modules/conversation_archive/__init__.py +1 -0
- app/modules/conversation_archive/api.py +101 -0
- app/modules/conversation_archive/schemas.py +28 -0
- app/modules/conversation_archive/service.py +165 -0
- app/modules/dashboard/__init__.py +1 -0
- app/modules/dashboard/api.py +106 -0
- app/modules/dashboard/builders.py +107 -0
- app/modules/dashboard/repository.py +304 -0
- app/modules/dashboard/schemas.py +148 -0
- app/modules/dashboard/service.py +534 -0
- app/modules/dashboard/timeframes.py +19 -0
- app/modules/dashboard/weekly_pace.py +617 -0
- app/modules/dashboard_auth/__init__.py +1 -0
- app/modules/dashboard_auth/api.py +1344 -0
- app/modules/dashboard_auth/oidc_api.py +753 -0
- app/modules/dashboard_auth/oidc_flows.py +330 -0
- app/modules/dashboard_auth/repository.py +452 -0
- app/modules/dashboard_auth/schemas.py +232 -0
- app/modules/dashboard_auth/service.py +1384 -0
- app/modules/dashboard_roles/__init__.py +0 -0
- app/modules/dashboard_roles/api.py +64 -0
- app/modules/dashboard_roles/repository.py +54 -0
- app/modules/dashboard_roles/schemas.py +29 -0
- app/modules/dashboard_roles/seed.py +80 -0
- app/modules/dashboard_roles/service.py +142 -0
- app/modules/dashboard_users/__init__.py +0 -0
- app/modules/dashboard_users/api.py +278 -0
- app/modules/dashboard_users/break_glass.py +169 -0
- app/modules/dashboard_users/credentials.py +18 -0
- app/modules/dashboard_users/identity_resolver.py +665 -0
- app/modules/dashboard_users/repository.py +725 -0
- app/modules/dashboard_users/schemas.py +117 -0
- app/modules/dashboard_users/service.py +1054 -0
- app/modules/firewall/__init__.py +1 -0
- app/modules/firewall/api.py +78 -0
- app/modules/firewall/repository.py +51 -0
- app/modules/firewall/schemas.py +28 -0
- app/modules/firewall/service.py +93 -0
- app/modules/fleet/__init__.py +1 -0
- app/modules/fleet/api.py +177 -0
- app/modules/fleet/mappers.py +56 -0
- app/modules/fleet/observability.py +372 -0
- app/modules/fleet/schemas.py +119 -0
- app/modules/health/__init__.py +0 -0
- app/modules/health/api.py +246 -0
- app/modules/health/schemas.py +27 -0
- app/modules/limit_warmup/__init__.py +1 -0
- app/modules/limit_warmup/repository.py +159 -0
- app/modules/limit_warmup/service.py +1001 -0
- app/modules/model_sources/__init__.py +1 -0
- app/modules/model_sources/api.py +99 -0
- app/modules/model_sources/catalog.py +307 -0
- app/modules/model_sources/forwarding.py +1435 -0
- app/modules/model_sources/projection.py +55 -0
- app/modules/model_sources/repository.py +201 -0
- app/modules/model_sources/schemas.py +79 -0
- app/modules/model_sources/selection.py +166 -0
- app/modules/model_sources/service.py +236 -0
- app/modules/oauth/__init__.py +0 -0
- app/modules/oauth/api.py +101 -0
- app/modules/oauth/repository.py +238 -0
- app/modules/oauth/schemas.py +49 -0
- app/modules/oauth/service.py +1196 -0
- app/modules/oauth/templates/oauth_success.html +122 -0
- app/modules/proxy/__init__.py +0 -0
- app/modules/proxy/_load_balancer/__init__.py +1 -0
- app/modules/proxy/_load_balancer/error_rate.py +116 -0
- app/modules/proxy/_load_balancer/latency_cohort.py +154 -0
- app/modules/proxy/_load_balancer/model_eligibility.py +250 -0
- app/modules/proxy/_load_balancer/opportunistic_admission.py +350 -0
- app/modules/proxy/_load_balancer/overload_backoff.py +394 -0
- app/modules/proxy/_load_balancer/sticky_selection.py +2417 -0
- app/modules/proxy/_load_balancer/throughput_cohort.py +233 -0
- app/modules/proxy/_load_balancer/ttft_cohort.py +214 -0
- app/modules/proxy/_load_balancer/tunables.py +97 -0
- app/modules/proxy/_load_balancer/types.py +96 -0
- app/modules/proxy/_load_balancer/unbound_selection.py +534 -0
- app/modules/proxy/_service/__init__.py +6 -0
- app/modules/proxy/_service/api_key_usage.py +745 -0
- app/modules/proxy/_service/clock_budget.py +38 -0
- app/modules/proxy/_service/codex_control.py +611 -0
- app/modules/proxy/_service/compact.py +2211 -0
- app/modules/proxy/_service/file_ops.py +749 -0
- app/modules/proxy/_service/http_bridge/__init__.py +3 -0
- app/modules/proxy/_service/http_bridge/accepted_replay.py +331 -0
- app/modules/proxy/_service/http_bridge/account_sessions.py +57 -0
- app/modules/proxy/_service/http_bridge/activity.py +178 -0
- app/modules/proxy/_service/http_bridge/helpers.py +3959 -0
- app/modules/proxy/_service/http_bridge/mixin.py +2435 -0
- app/modules/proxy/_service/http_bridge/owner_forwarding.py +609 -0
- app/modules/proxy/_service/http_bridge/protocol.py +167 -0
- app/modules/proxy/_service/http_bridge/proxy_failover.py +58 -0
- app/modules/proxy/_service/http_bridge/quarantine.py +443 -0
- app/modules/proxy/_service/http_bridge/request_submit.py +4582 -0
- app/modules/proxy/_service/http_bridge/retry_circuit.py +2442 -0
- app/modules/proxy/_service/http_bridge/service_stubs.py +492 -0
- app/modules/proxy/_service/http_bridge/session_registry.py +778 -0
- app/modules/proxy/_service/http_bridge/streaming.py +5328 -0
- app/modules/proxy/_service/http_bridge/upstream_events.py +4538 -0
- app/modules/proxy/_service/observability.py +410 -0
- app/modules/proxy/_service/rate_limit.py +361 -0
- app/modules/proxy/_service/realtime_live.py +655 -0
- app/modules/proxy/_service/refresh.py +154 -0
- app/modules/proxy/_service/request_log.py +585 -0
- app/modules/proxy/_service/response_create.py +930 -0
- app/modules/proxy/_service/streaming/__init__.py +3 -0
- app/modules/proxy/_service/streaming/helpers.py +1317 -0
- app/modules/proxy/_service/streaming/mixin.py +1100 -0
- app/modules/proxy/_service/streaming/protocol.py +32 -0
- app/modules/proxy/_service/streaming/retry.py +3762 -0
- app/modules/proxy/_service/support.py +2122 -0
- app/modules/proxy/_service/transcribe.py +434 -0
- app/modules/proxy/_service/warmup.py +483 -0
- app/modules/proxy/_service/websocket/__init__.py +3 -0
- app/modules/proxy/_service/websocket/helpers.py +2360 -0
- app/modules/proxy/_service/websocket/mixin.py +7285 -0
- app/modules/proxy/_service/websocket/protocol.py +74 -0
- app/modules/proxy/account_cache.py +229 -0
- app/modules/proxy/account_eligibility.py +87 -0
- app/modules/proxy/additional_model_limits.py +40 -0
- app/modules/proxy/affinity.py +830 -0
- app/modules/proxy/affinity_observation.py +101 -0
- app/modules/proxy/api.py +10254 -0
- app/modules/proxy/api_key_usage.py +113 -0
- app/modules/proxy/cap_partitioning.py +280 -0
- app/modules/proxy/capability_lineage.py +65 -0
- app/modules/proxy/capability_lineage_repository.py +82 -0
- app/modules/proxy/capability_routing.py +253 -0
- app/modules/proxy/continuity.py +112 -0
- app/modules/proxy/downstream_delivery.py +277 -0
- app/modules/proxy/durable_bridge_coordinator.py +1217 -0
- app/modules/proxy/durable_bridge_repository.py +4516 -0
- app/modules/proxy/durable_bridge_runtime.py +9 -0
- app/modules/proxy/durable_bridge_transcript_codec.py +122 -0
- app/modules/proxy/fair_share.py +131 -0
- app/modules/proxy/file_pin_repository.py +210 -0
- app/modules/proxy/helpers.py +439 -0
- app/modules/proxy/http_bridge_event_batcher.py +613 -0
- app/modules/proxy/http_bridge_forwarding.py +797 -0
- app/modules/proxy/http_continuation.py +76 -0
- app/modules/proxy/images_observability.py +62 -0
- app/modules/proxy/images_service.py +1054 -0
- app/modules/proxy/load_balancer.py +3016 -0
- app/modules/proxy/rate_limit_cache.py +46 -0
- app/modules/proxy/replay_safety.py +1063 -0
- app/modules/proxy/repo_bundle.py +31 -0
- app/modules/proxy/request_policy.py +1054 -0
- app/modules/proxy/ring_membership.py +251 -0
- app/modules/proxy/schemas.py +345 -0
- app/modules/proxy/selection_errors.py +41 -0
- app/modules/proxy/service.py +2600 -0
- app/modules/proxy/source_admission.py +125 -0
- app/modules/proxy/source_dispatch.py +999 -0
- app/modules/proxy/sticky_repository.py +831 -0
- app/modules/proxy/thread_anchors.py +491 -0
- app/modules/proxy/tool_call_dedupe.py +802 -0
- app/modules/proxy/types.py +54 -0
- app/modules/proxy/work_admission.py +121 -0
- app/modules/quota_planner/__init__.py +1 -0
- app/modules/quota_planner/api.py +276 -0
- app/modules/quota_planner/logic.py +743 -0
- app/modules/quota_planner/repository.py +722 -0
- app/modules/quota_planner/scheduler.py +212 -0
- app/modules/quota_planner/schemas.py +102 -0
- app/modules/quota_planner/warmup.py +693 -0
- app/modules/rate_limit_reset_credits/__init__.py +0 -0
- app/modules/rate_limit_reset_credits/api.py +601 -0
- app/modules/rate_limit_reset_credits/redeem_coordination.py +274 -0
- app/modules/rate_limit_reset_credits/store.py +114 -0
- app/modules/reports/__init__.py +1 -0
- app/modules/reports/api.py +134 -0
- app/modules/reports/cache.py +74 -0
- app/modules/reports/filters.py +27 -0
- app/modules/reports/repository.py +525 -0
- app/modules/reports/rollup.py +150 -0
- app/modules/reports/rollup_read.py +148 -0
- app/modules/reports/schemas.py +123 -0
- app/modules/reports/service.py +287 -0
- app/modules/reports/thread_identity.py +256 -0
- app/modules/request_logs/__init__.py +0 -0
- app/modules/request_logs/api.py +224 -0
- app/modules/request_logs/cost_backfill.py +154 -0
- app/modules/request_logs/mappers.py +97 -0
- app/modules/request_logs/repository.py +1765 -0
- app/modules/request_logs/schemas.py +145 -0
- app/modules/request_logs/service.py +348 -0
- app/modules/role_mappings/__init__.py +0 -0
- app/modules/role_mappings/api.py +206 -0
- app/modules/role_mappings/matching.py +55 -0
- app/modules/role_mappings/repository.py +106 -0
- app/modules/role_mappings/schemas.py +67 -0
- app/modules/role_mappings/service.py +334 -0
- app/modules/runtime/__init__.py +1 -0
- app/modules/runtime/api.py +18 -0
- app/modules/runtime/schemas.py +14 -0
- app/modules/runtime/service.py +218 -0
- app/modules/scim/__init__.py +1 -0
- app/modules/scim/api.py +194 -0
- app/modules/scim/dependencies.py +112 -0
- app/modules/scim/errors.py +77 -0
- app/modules/scim/management_api.py +156 -0
- app/modules/scim/repository.py +212 -0
- app/modules/scim/schemas.py +145 -0
- app/modules/scim/service.py +786 -0
- app/modules/scim/tokens.py +43 -0
- app/modules/settings/__init__.py +1 -0
- app/modules/settings/api.py +1677 -0
- app/modules/settings/repository.py +606 -0
- app/modules/settings/schemas.py +496 -0
- app/modules/settings/service.py +769 -0
- app/modules/shared/__init__.py +0 -0
- app/modules/shared/schemas.py +22 -0
- app/modules/sticky_sessions/__init__.py +1 -0
- app/modules/sticky_sessions/api.py +118 -0
- app/modules/sticky_sessions/cleanup_scheduler.py +487 -0
- app/modules/sticky_sessions/schemas.py +78 -0
- app/modules/sticky_sessions/service.py +196 -0
- app/modules/telemetry/__init__.py +5 -0
- app/modules/telemetry/api.py +115 -0
- app/modules/telemetry/clients.py +62 -0
- app/modules/telemetry/consent.py +115 -0
- app/modules/telemetry/scheduler.py +85 -0
- app/modules/telemetry/schemas.py +163 -0
- app/modules/telemetry/sender.py +221 -0
- app/modules/telemetry/snapshot.py +490 -0
- app/modules/usage/__init__.py +0 -0
- app/modules/usage/additional_quota_keys.py +379 -0
- app/modules/usage/api.py +45 -0
- app/modules/usage/background_repository.py +147 -0
- app/modules/usage/builders.py +559 -0
- app/modules/usage/depletion_service.py +277 -0
- app/modules/usage/live_ingest.py +356 -0
- app/modules/usage/mappers.py +29 -0
- app/modules/usage/plan_downgrade_observations.py +488 -0
- app/modules/usage/repository.py +1890 -0
- app/modules/usage/schemas.py +68 -0
- app/modules/usage/service.py +102 -0
- app/modules/usage/updater.py +1488 -0
- app/static/assets/Trans-Cj-J9mVA.js +1 -0
- app/static/assets/access-page-DHzZiXZi.js +1 -0
- app/static/assets/account-cost-donut-DGhfA4U8.js +1 -0
- app/static/assets/account-identifiers-CxbdLshZ.js +1 -0
- app/static/assets/account-status-DIsU5sUS.js +1 -0
- app/static/assets/account-trend-chart-BptlfwwQ.js +1 -0
- app/static/assets/accounts-page-Bbai8ift.js +3 -0
- app/static/assets/api-Dwa7XxPe.js +1 -0
- app/static/assets/api-client-dugX1Pti.js +55 -0
- app/static/assets/api-key-create-dialog-DnRm0oYN.js +1 -0
- app/static/assets/api-key-created-dialog-B6jkDG9f.js +1 -0
- app/static/assets/api-key-edit-dialog-Ck3Bnc3V.js +1 -0
- app/static/assets/api-trend-chart-BchCw7Hj.js +1 -0
- app/static/assets/apis-page-BvXz9c6i.js +2 -0
- app/static/assets/arrow-up-Dqh_KzJo.js +1 -0
- app/static/assets/arrow-up-down-LBRgVDGY.js +1 -0
- app/static/assets/automations-page-DQgNAJOR.js +3 -0
- app/static/assets/badge-DqMb9es0.js +1 -0
- app/static/assets/blur-email-HEvKagG2.js +1 -0
- app/static/assets/button-BjVmmiGW.js +1 -0
- app/static/assets/checkbox-apsl9vzk.js +1 -0
- app/static/assets/chevron-left-1d_pHVa5.js +1 -0
- app/static/assets/circle-alert-BS0xd_o1.js +1 -0
- app/static/assets/clsx-DB0hHKMi.js +1 -0
- app/static/assets/colors-BBy8GEtr.js +1 -0
- app/static/assets/copy-button-D7nAvOKR.js +1 -0
- app/static/assets/cost-per-day-chart-YdqlkZVM.js +1 -0
- app/static/assets/createLucideIcon-DBLzLNh2.js +1 -0
- app/static/assets/dashboard-page-CSdhBy0f.js +2 -0
- app/static/assets/distribution-metric-format-yZTy1ZEu.js +1 -0
- app/static/assets/donut-chart-Bxc69cra.js +1 -0
- app/static/assets/download-kQteIcsB.js +1 -0
- app/static/assets/ellipsis-B_5OgIre.js +1 -0
- app/static/assets/empty-state-JcA7tc8m.js +1 -0
- app/static/assets/es2015-bMH8YKZT.js +41 -0
- app/static/assets/es6-B264ni6W.js +69 -0
- app/static/assets/external-link-UVAOgMQ0.js +1 -0
- app/static/assets/i18n-BWqZ9Ua2.js +1 -0
- app/static/assets/inbox-B_RM2s1J.js +1 -0
- app/static/assets/index-DIpd264Z.js +23 -0
- app/static/assets/index-DMeLKRJx.css +2 -0
- app/static/assets/inherit-badge-QhTYVaU6.js +1 -0
- app/static/assets/invite-dialog-0CYh0GiQ.js +1 -0
- app/static/assets/iso-Bpe5Ihbb.js +1 -0
- app/static/assets/label-CGaLmLNf.js +1 -0
- app/static/assets/lazy-recharts-Pv8p9ArM.js +2 -0
- app/static/assets/loading-overlay-CzKQfHLI.js +1 -0
- app/static/assets/mini-quota-bar-cGT_t8Ae.js +1 -0
- app/static/assets/model-distribution-donut-BKjAfT48.js +1 -0
- app/static/assets/model-source-multi-select-D2d06Vc7.js +1 -0
- app/static/assets/multi-select-filter-KIA5qU6Z.js +1 -0
- app/static/assets/oauth-dialog-XeaGGDjS.js +1 -0
- app/static/assets/pagination-controls-D1Ypd0RU.js +1 -0
- app/static/assets/payload-CIYnGZz9.js +1 -0
- app/static/assets/queue-wait-chart-VoV7Yg9L.js +1 -0
- app/static/assets/report-chart-card-BXDliwcc.js +1 -0
- app/static/assets/reports-page-CGCJeMHG.js +3 -0
- app/static/assets/reset-credit-confirm-dialog-QOifTnVB.js +1 -0
- app/static/assets/rolldown-runtime-hePW80VL.js +1 -0
- app/static/assets/search-HfhqwERt.js +1 -0
- app/static/assets/select-Cl33bz-g.js +1 -0
- app/static/assets/settings-page-mZj01HYl.js +2 -0
- app/static/assets/shim-CZwyM4Q7.js +1 -0
- app/static/assets/skeleton-C_EUc4Rp.js +1 -0
- app/static/assets/sparkline-chart-DdNNsddM.js +1 -0
- app/static/assets/status-badge-CHHA5X6c.js +1 -0
- app/static/assets/switch-CzxgokcM.js +1 -0
- app/static/assets/table-DXaNSQ0G.js +1 -0
- app/static/assets/time-to-first-token-chart--68L0u_v.js +1 -0
- app/static/assets/tokens-per-day-chart-Cs82BFuE.js +1 -0
- app/static/assets/tokens-per-second-chart-AD2dfOn6.js +1 -0
- app/static/assets/totp-settings-CRHtxQ7j.js +1 -0
- app/static/assets/trash-D3KyWr1h.js +1 -0
- app/static/assets/use-account-quota-display-BPmrTBTC.js +1 -0
- app/static/assets/use-accounts-PWlVGw3z.js +1 -0
- app/static/assets/use-auth-CuDlW3iC.js +1 -0
- app/static/assets/use-chart-colors-DIu6tClI.js +1 -0
- app/static/assets/use-model-sources-Bul24JBu.js +1 -0
- app/static/assets/use-models-DUCPUuZg.js +1 -0
- app/static/assets/use-reduced-motion-hAm5r6_d.js +1 -0
- app/static/assets/useTranslation--56nHhlH.js +1 -0
- app/static/assets/useragent-distribution-donut-Dup6sf_n.js +1 -0
- app/static/assets/users-uIC3TIlE.js +1 -0
- app/static/assets/vendor-query-_bHZuxEx.js +1 -0
- app/static/assets/vendor-react-BVL0YZPW.js +9 -0
- app/static/favicon.svg +10 -0
- app/static/fonts/GeistSans-Variable.woff2 +0 -0
- app/static/fonts/jetbrains-mono-latin-ext.woff2 +0 -0
- app/static/fonts/jetbrains-mono-latin.woff2 +0 -0
- app/static/index.html +29 -0
- codexlb_up-1.25.0b10.dist-info/METADATA +124 -0
- codexlb_up-1.25.0b10.dist-info/RECORD +827 -0
- codexlb_up-1.25.0b10.dist-info/WHEEL +4 -0
- codexlb_up-1.25.0b10.dist-info/entry_points.txt +3 -0
- codexlb_up-1.25.0b10.dist-info/licenses/LICENSE +21 -0
- config/additional_quota_registry.json +11 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Which rule wins for an identity (pure; no database, no session).
|
|
2
|
+
|
|
3
|
+
Rules are ordered by descending priority and the FIRST match wins. There is no
|
|
4
|
+
"highest role" rule — custom roles have no total order — and no tie is
|
|
5
|
+
possible: ``UNIQUE(provider, provider_key, priority)`` makes the order total.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Iterable, Sequence
|
|
11
|
+
|
|
12
|
+
from app.db.models import DashboardRoleMapping, DashboardRoleMappingClaim
|
|
13
|
+
|
|
14
|
+
#: Claim names a rule may match on (a plain string column, validated here).
|
|
15
|
+
SUPPORTED_CLAIMS: frozenset[str] = frozenset(claim.value for claim in DashboardRoleMappingClaim)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def normalize_claim_value(claim_name: str, value: str) -> str:
|
|
19
|
+
"""Trim and case-fold; an ``email_domain`` value is stored without its leading ``@``."""
|
|
20
|
+
|
|
21
|
+
normalized = value.strip().casefold()
|
|
22
|
+
if claim_name == DashboardRoleMappingClaim.EMAIL_DOMAIN.value:
|
|
23
|
+
normalized = normalized.removeprefix("@")
|
|
24
|
+
return normalized
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def email_domain(email: str | None) -> str | None:
|
|
28
|
+
"""The domain part of an e-mail, case-folded; ``None`` when there is none to compare."""
|
|
29
|
+
|
|
30
|
+
if email is None:
|
|
31
|
+
return None
|
|
32
|
+
_, _, domain = email.strip().casefold().rpartition("@")
|
|
33
|
+
return domain or None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _matches(row: DashboardRoleMapping, groups: Sequence[str], domain: str | None) -> bool:
|
|
37
|
+
if row.claim_name == DashboardRoleMappingClaim.GROUPS.value:
|
|
38
|
+
return normalize_claim_value(row.claim_name, row.claim_value) in {group.casefold() for group in groups}
|
|
39
|
+
if row.claim_name == DashboardRoleMappingClaim.EMAIL_DOMAIN.value:
|
|
40
|
+
return domain is not None and normalize_claim_value(row.claim_name, row.claim_value) == domain
|
|
41
|
+
# A claim this replica's vocabulary does not know (a newer peer wrote it):
|
|
42
|
+
# it matches nothing rather than breaking every sign-in.
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def match_role_id(
|
|
47
|
+
rows: Iterable[DashboardRoleMapping], *, groups: Sequence[str] = (), email: str | None = None
|
|
48
|
+
) -> str | None:
|
|
49
|
+
"""The role of the highest-priority matching rule, or ``None`` when none matches."""
|
|
50
|
+
|
|
51
|
+
domain = email_domain(email)
|
|
52
|
+
for row in sorted(rows, key=lambda row: row.priority, reverse=True):
|
|
53
|
+
if _matches(row, groups, domain):
|
|
54
|
+
return row.role_id
|
|
55
|
+
return None
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Queries and writes over ``dashboard_role_mappings``.
|
|
2
|
+
|
|
3
|
+
Priorities are server-owned: the rows of one ``(provider, provider_key)`` are
|
|
4
|
+
always the contiguous integers ``N..1`` with ``N`` the winner. Every write that
|
|
5
|
+
changes the order goes through :meth:`RoleMappingsRepository.renumber`, which
|
|
6
|
+
parks the affected rows on disjoint negative values before writing their final
|
|
7
|
+
ones, so ``UNIQUE(provider, provider_key, priority)`` never trips mid-update
|
|
8
|
+
(SQLite has no deferred constraints).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from collections.abc import Sequence
|
|
14
|
+
|
|
15
|
+
from sqlalchemy import delete, func, select, update
|
|
16
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
17
|
+
|
|
18
|
+
from app.db.models import DashboardAuthProvider, DashboardRoleMapping
|
|
19
|
+
|
|
20
|
+
#: Rules are evaluated on every sign-in, so the list stays trivially bounded.
|
|
21
|
+
MAX_MAPPINGS_PER_PROVIDER = 100
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class RoleMappingsRepository:
|
|
25
|
+
def __init__(self, session: AsyncSession) -> None:
|
|
26
|
+
self._session = session
|
|
27
|
+
|
|
28
|
+
# --- reads ---
|
|
29
|
+
|
|
30
|
+
async def list_for_provider(self, provider: str, provider_key: str) -> Sequence[DashboardRoleMapping]:
|
|
31
|
+
"""The provider's rules, winner first."""
|
|
32
|
+
|
|
33
|
+
stmt = (
|
|
34
|
+
select(DashboardRoleMapping)
|
|
35
|
+
.where(DashboardRoleMapping.provider == provider)
|
|
36
|
+
.where(DashboardRoleMapping.provider_key == provider_key)
|
|
37
|
+
.order_by(DashboardRoleMapping.priority.desc())
|
|
38
|
+
# A renumber writes priorities behind the ORM's back; always read the row as it is.
|
|
39
|
+
.execution_options(populate_existing=True)
|
|
40
|
+
)
|
|
41
|
+
return (await self._session.execute(stmt)).scalars().all()
|
|
42
|
+
|
|
43
|
+
async def list_mappings(
|
|
44
|
+
self, *, provider: str | None = None, provider_key: str | None = None
|
|
45
|
+
) -> Sequence[DashboardRoleMapping]:
|
|
46
|
+
stmt = select(DashboardRoleMapping)
|
|
47
|
+
if provider is not None:
|
|
48
|
+
stmt = stmt.where(DashboardRoleMapping.provider == provider)
|
|
49
|
+
if provider_key is not None:
|
|
50
|
+
stmt = stmt.where(DashboardRoleMapping.provider_key == provider_key)
|
|
51
|
+
stmt = stmt.order_by(
|
|
52
|
+
DashboardRoleMapping.provider.asc(),
|
|
53
|
+
DashboardRoleMapping.provider_key.asc(),
|
|
54
|
+
DashboardRoleMapping.priority.desc(),
|
|
55
|
+
).execution_options(populate_existing=True)
|
|
56
|
+
return (await self._session.execute(stmt)).scalars().all()
|
|
57
|
+
|
|
58
|
+
async def get_mapping(self, mapping_id: str) -> DashboardRoleMapping | None:
|
|
59
|
+
return await self._session.get(DashboardRoleMapping, mapping_id, populate_existing=True)
|
|
60
|
+
|
|
61
|
+
async def count_mappings(self) -> int:
|
|
62
|
+
return int((await self._session.execute(select(func.count()).select_from(DashboardRoleMapping))).scalar_one())
|
|
63
|
+
|
|
64
|
+
async def provider_exists(self, provider: str, provider_key: str) -> bool:
|
|
65
|
+
stmt = (
|
|
66
|
+
select(DashboardAuthProvider.id)
|
|
67
|
+
.where(DashboardAuthProvider.kind == provider)
|
|
68
|
+
.where(DashboardAuthProvider.provider_key == provider_key)
|
|
69
|
+
.limit(1)
|
|
70
|
+
)
|
|
71
|
+
return (await self._session.execute(stmt)).first() is not None
|
|
72
|
+
|
|
73
|
+
# --- writes (no commit; the service owns the transaction) ---
|
|
74
|
+
|
|
75
|
+
def add(self, mapping: DashboardRoleMapping) -> None:
|
|
76
|
+
self._session.add(mapping)
|
|
77
|
+
|
|
78
|
+
async def delete_mapping(self, mapping_id: str) -> None:
|
|
79
|
+
await self._session.execute(delete(DashboardRoleMapping).where(DashboardRoleMapping.id == mapping_id))
|
|
80
|
+
|
|
81
|
+
async def renumber(self, ordered_ids: Sequence[str]) -> None:
|
|
82
|
+
"""Write ``len..1`` onto ``ordered_ids`` (winner first) without a mid-update collision."""
|
|
83
|
+
|
|
84
|
+
for index, mapping_id in enumerate(ordered_ids):
|
|
85
|
+
await self._set_priority(mapping_id, -(index + 1))
|
|
86
|
+
total = len(ordered_ids)
|
|
87
|
+
for index, mapping_id in enumerate(ordered_ids):
|
|
88
|
+
await self._set_priority(mapping_id, total - index)
|
|
89
|
+
|
|
90
|
+
async def _set_priority(self, mapping_id: str, priority: int) -> None:
|
|
91
|
+
await self._session.execute(
|
|
92
|
+
update(DashboardRoleMapping)
|
|
93
|
+
.where(DashboardRoleMapping.id == mapping_id)
|
|
94
|
+
.values(priority=priority)
|
|
95
|
+
.execution_options(synchronize_session=False)
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
async def commit(self) -> None:
|
|
99
|
+
try:
|
|
100
|
+
await self._session.commit()
|
|
101
|
+
except Exception:
|
|
102
|
+
await self._session.rollback()
|
|
103
|
+
raise
|
|
104
|
+
|
|
105
|
+
async def rollback(self) -> None:
|
|
106
|
+
await self._session.rollback()
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from pydantic import ConfigDict, Field
|
|
6
|
+
|
|
7
|
+
from app.modules.shared.schemas import DashboardModel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RoleMappingResponse(DashboardModel):
|
|
11
|
+
"""One rule; the list is ordered by descending priority (the winner first)."""
|
|
12
|
+
|
|
13
|
+
id: str
|
|
14
|
+
provider: str
|
|
15
|
+
provider_key: str
|
|
16
|
+
claim_name: str
|
|
17
|
+
claim_value: str
|
|
18
|
+
role_id: str
|
|
19
|
+
priority: int
|
|
20
|
+
created_at: datetime
|
|
21
|
+
updated_at: datetime
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AssignableRoleResponse(DashboardModel):
|
|
25
|
+
"""A role the caller may point a rule (or a provider default) at.
|
|
26
|
+
|
|
27
|
+
Deliberately smaller than the ``users:manage`` roles list: a picker needs
|
|
28
|
+
to name a role, not to know how many people hold it.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
id: str
|
|
32
|
+
slug: str
|
|
33
|
+
name: str
|
|
34
|
+
description: str | None = None
|
|
35
|
+
kind: str
|
|
36
|
+
locked: bool
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class RoleMappingCreateRequest(DashboardModel):
|
|
40
|
+
"""A new rule; the server appends it at the lowest priority (clients never send one)."""
|
|
41
|
+
|
|
42
|
+
model_config = ConfigDict(extra="forbid")
|
|
43
|
+
|
|
44
|
+
provider: str = Field(min_length=1, max_length=32)
|
|
45
|
+
provider_key: str = Field(min_length=1, max_length=64)
|
|
46
|
+
claim_name: str = Field(min_length=1, max_length=32)
|
|
47
|
+
claim_value: str = Field(min_length=1, max_length=320)
|
|
48
|
+
role_id: str = Field(min_length=1, max_length=36)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class RoleMappingUpdateRequest(DashboardModel):
|
|
52
|
+
"""Fields left out are untouched. The order is changed with ``PUT /api/role-mappings/order``."""
|
|
53
|
+
|
|
54
|
+
model_config = ConfigDict(extra="forbid")
|
|
55
|
+
|
|
56
|
+
claim_value: str | None = Field(default=None, min_length=1, max_length=320)
|
|
57
|
+
role_id: str | None = Field(default=None, min_length=1, max_length=36)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class RoleMappingOrderRequest(DashboardModel):
|
|
61
|
+
"""The full order of one provider's rules, winner first."""
|
|
62
|
+
|
|
63
|
+
model_config = ConfigDict(extra="forbid")
|
|
64
|
+
|
|
65
|
+
provider: str = Field(min_length=1, max_length=32)
|
|
66
|
+
provider_key: str = Field(min_length=1, max_length=64)
|
|
67
|
+
ids: list[str] = Field(min_length=1)
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"""Group-to-role rules: the list an operator edits, the order the resolver reads.
|
|
2
|
+
|
|
3
|
+
Every write applies the same role rules as inviting a person: the role must be
|
|
4
|
+
assignable and within the caller's own grants — checked on the role the rule
|
|
5
|
+
would hand out next *and* on the one it hands out today, so a caller who may
|
|
6
|
+
not delegate admin can neither write, retarget, delete nor promote an
|
|
7
|
+
admin-granting rule. Every write also renumbers the provider's rules so their
|
|
8
|
+
priorities stay the contiguous ``N..1`` the resolver relies on, audits
|
|
9
|
+
``role_mapping_changed``, and invalidates the provider registry — a new rule
|
|
10
|
+
that only took effect after a cache TTL would look like it did nothing.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import uuid
|
|
16
|
+
from collections.abc import Sequence
|
|
17
|
+
|
|
18
|
+
from sqlalchemy.exc import IntegrityError
|
|
19
|
+
|
|
20
|
+
from app.core.audit.service import AuditActor, AuditDetails, AuditService, AuditTarget
|
|
21
|
+
from app.core.auth.dashboard_access import (
|
|
22
|
+
DashboardPrincipal,
|
|
23
|
+
InsufficientDelegationError,
|
|
24
|
+
assert_can_delegate,
|
|
25
|
+
)
|
|
26
|
+
from app.core.auth.providers.registry import get_auth_provider_registry
|
|
27
|
+
from app.db.models import DashboardRoleMapping, DashboardRoleRecord
|
|
28
|
+
from app.modules.dashboard_roles.repository import DashboardRolesRepository
|
|
29
|
+
from app.modules.dashboard_roles.service import (
|
|
30
|
+
resolve_assignable_role,
|
|
31
|
+
resolve_role_grants,
|
|
32
|
+
role_assignable_to_users,
|
|
33
|
+
)
|
|
34
|
+
from app.modules.role_mappings.matching import SUPPORTED_CLAIMS, normalize_claim_value
|
|
35
|
+
from app.modules.role_mappings.repository import MAX_MAPPINGS_PER_PROVIDER, RoleMappingsRepository
|
|
36
|
+
from app.modules.role_mappings.schemas import (
|
|
37
|
+
RoleMappingCreateRequest,
|
|
38
|
+
RoleMappingOrderRequest,
|
|
39
|
+
RoleMappingUpdateRequest,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class MappingNotFoundError(LookupError):
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class MappingProviderNotFoundError(LookupError):
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class UnknownClaimError(ValueError):
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class MappingExistsError(ValueError):
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class MappingLimitReachedError(ValueError):
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class OrderStaleError(ValueError):
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class RoleMappingsService:
|
|
68
|
+
def __init__(self, repository: RoleMappingsRepository, roles: DashboardRolesRepository) -> None:
|
|
69
|
+
self._repo = repository
|
|
70
|
+
self._roles = roles
|
|
71
|
+
|
|
72
|
+
async def list_mappings(
|
|
73
|
+
self, *, provider: str | None = None, provider_key: str | None = None
|
|
74
|
+
) -> list[DashboardRoleMapping]:
|
|
75
|
+
return list(await self._repo.list_mappings(provider=provider, provider_key=provider_key))
|
|
76
|
+
|
|
77
|
+
async def create_mapping(
|
|
78
|
+
self, principal: DashboardPrincipal, payload: RoleMappingCreateRequest, *, actor_ip: str | None
|
|
79
|
+
) -> DashboardRoleMapping:
|
|
80
|
+
claim_name = payload.claim_name.strip().casefold()
|
|
81
|
+
if claim_name not in SUPPORTED_CLAIMS:
|
|
82
|
+
raise UnknownClaimError(f"Unsupported claim '{payload.claim_name}'")
|
|
83
|
+
claim_value = normalize_claim_value(claim_name, payload.claim_value)
|
|
84
|
+
if not claim_value:
|
|
85
|
+
raise UnknownClaimError("The rule needs a value to match on")
|
|
86
|
+
if not await self._repo.provider_exists(payload.provider, payload.provider_key):
|
|
87
|
+
raise MappingProviderNotFoundError("Provider not found")
|
|
88
|
+
role_slug = await self._delegated_role_slug(principal, payload.role_id)
|
|
89
|
+
existing = list(await self._repo.list_for_provider(payload.provider, payload.provider_key))
|
|
90
|
+
if len(existing) >= MAX_MAPPINGS_PER_PROVIDER:
|
|
91
|
+
raise MappingLimitReachedError(f"A sign-in method takes at most {MAX_MAPPINGS_PER_PROVIDER} rules")
|
|
92
|
+
if any(row.claim_name == claim_name and row.claim_value == claim_value for row in existing):
|
|
93
|
+
raise MappingExistsError("A rule for that value already exists")
|
|
94
|
+
mapping = DashboardRoleMapping(
|
|
95
|
+
id=str(uuid.uuid4()),
|
|
96
|
+
provider=payload.provider,
|
|
97
|
+
provider_key=payload.provider_key,
|
|
98
|
+
claim_name=claim_name,
|
|
99
|
+
claim_value=claim_value,
|
|
100
|
+
role_id=payload.role_id,
|
|
101
|
+
# Parked outside the live range; ``renumber`` writes the real value.
|
|
102
|
+
priority=0,
|
|
103
|
+
)
|
|
104
|
+
mapping_id = mapping.id
|
|
105
|
+
self._repo.add(mapping)
|
|
106
|
+
# Appended at the bottom: a new rule never outranks a rule that is already there.
|
|
107
|
+
await self._renumber_and_commit([row.id for row in existing] + [mapping_id])
|
|
108
|
+
stored = await self._require(mapping_id)
|
|
109
|
+
self._audit(
|
|
110
|
+
principal,
|
|
111
|
+
stored,
|
|
112
|
+
actor_ip,
|
|
113
|
+
operation="created",
|
|
114
|
+
role_slug=role_slug,
|
|
115
|
+
)
|
|
116
|
+
return stored
|
|
117
|
+
|
|
118
|
+
async def update_mapping(
|
|
119
|
+
self,
|
|
120
|
+
principal: DashboardPrincipal,
|
|
121
|
+
mapping_id: str,
|
|
122
|
+
payload: RoleMappingUpdateRequest,
|
|
123
|
+
*,
|
|
124
|
+
actor_ip: str | None,
|
|
125
|
+
) -> DashboardRoleMapping:
|
|
126
|
+
mapping = await self._require(mapping_id)
|
|
127
|
+
current = await self._assert_may_hand_out(principal, mapping.role_id)
|
|
128
|
+
fields = payload.model_fields_set
|
|
129
|
+
changed = False
|
|
130
|
+
# The rule keeps handing out its current role unless the payload moves it,
|
|
131
|
+
# so the audit row can name the role either way.
|
|
132
|
+
role_slug = current.slug if current is not None else None
|
|
133
|
+
if "role_id" in fields and payload.role_id is not None:
|
|
134
|
+
role_slug = await self._delegated_role_slug(principal, payload.role_id)
|
|
135
|
+
if mapping.role_id != payload.role_id:
|
|
136
|
+
mapping.role_id = payload.role_id
|
|
137
|
+
changed = True
|
|
138
|
+
if "claim_value" in fields and payload.claim_value is not None:
|
|
139
|
+
claim_value = normalize_claim_value(mapping.claim_name, payload.claim_value)
|
|
140
|
+
if not claim_value:
|
|
141
|
+
raise UnknownClaimError("The rule needs a value to match on")
|
|
142
|
+
if claim_value != mapping.claim_value:
|
|
143
|
+
siblings = await self._repo.list_for_provider(mapping.provider, mapping.provider_key)
|
|
144
|
+
if any(
|
|
145
|
+
row.id != mapping.id and row.claim_name == mapping.claim_name and row.claim_value == claim_value
|
|
146
|
+
for row in siblings
|
|
147
|
+
):
|
|
148
|
+
raise MappingExistsError("A rule for that value already exists")
|
|
149
|
+
mapping.claim_value = claim_value
|
|
150
|
+
changed = True
|
|
151
|
+
if not changed:
|
|
152
|
+
return mapping
|
|
153
|
+
await self._commit()
|
|
154
|
+
stored = await self._require(mapping_id)
|
|
155
|
+
self._audit(principal, stored, actor_ip, operation="updated", role_slug=role_slug)
|
|
156
|
+
return stored
|
|
157
|
+
|
|
158
|
+
async def delete_mapping(self, principal: DashboardPrincipal, mapping_id: str, *, actor_ip: str | None) -> None:
|
|
159
|
+
mapping = await self._require(mapping_id)
|
|
160
|
+
current = await self._assert_may_hand_out(principal, mapping.role_id)
|
|
161
|
+
provider, provider_key = mapping.provider, mapping.provider_key
|
|
162
|
+
details: AuditDetails = {
|
|
163
|
+
"operation": "deleted",
|
|
164
|
+
"provider": provider,
|
|
165
|
+
"provider_key": provider_key,
|
|
166
|
+
"claim_name": mapping.claim_name,
|
|
167
|
+
"claim_value": mapping.claim_value,
|
|
168
|
+
}
|
|
169
|
+
# Read off the row before it goes: afterwards nothing can say what the rule granted.
|
|
170
|
+
if current is not None:
|
|
171
|
+
details = {**details, "role": current.slug}
|
|
172
|
+
siblings = await self._repo.list_for_provider(provider, provider_key)
|
|
173
|
+
remaining = [row.id for row in siblings if row.id != mapping_id]
|
|
174
|
+
await self._repo.delete_mapping(mapping_id)
|
|
175
|
+
# Closing the gap keeps the priorities contiguous, so the next create appends at 1.
|
|
176
|
+
await self._renumber_and_commit(remaining)
|
|
177
|
+
AuditService.log_async(
|
|
178
|
+
"role_mapping_changed",
|
|
179
|
+
actor_ip=actor_ip,
|
|
180
|
+
details=details,
|
|
181
|
+
actor=AuditActor.from_principal(principal),
|
|
182
|
+
target=AuditTarget("role_mapping", mapping_id),
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
async def reorder_mappings(
|
|
186
|
+
self, principal: DashboardPrincipal, payload: RoleMappingOrderRequest, *, actor_ip: str | None
|
|
187
|
+
) -> list[DashboardRoleMapping]:
|
|
188
|
+
current = await self._repo.list_for_provider(payload.provider, payload.provider_key)
|
|
189
|
+
if not current:
|
|
190
|
+
raise MappingProviderNotFoundError("That sign-in method has no rules to order")
|
|
191
|
+
if sorted(payload.ids) != sorted(row.id for row in current) or len(set(payload.ids)) != len(payload.ids):
|
|
192
|
+
raise OrderStaleError("The order must list every rule of that sign-in method exactly once")
|
|
193
|
+
await self._assert_may_move(principal, current, payload.ids)
|
|
194
|
+
await self._renumber_and_commit(list(payload.ids))
|
|
195
|
+
AuditService.log_async(
|
|
196
|
+
"role_mapping_changed",
|
|
197
|
+
actor_ip=actor_ip,
|
|
198
|
+
details={
|
|
199
|
+
"operation": "reordered",
|
|
200
|
+
"provider": payload.provider,
|
|
201
|
+
"provider_key": payload.provider_key,
|
|
202
|
+
"ids": list(payload.ids),
|
|
203
|
+
},
|
|
204
|
+
actor=AuditActor.from_principal(principal),
|
|
205
|
+
target=AuditTarget("role_mapping", payload.ids[0]),
|
|
206
|
+
)
|
|
207
|
+
return list(await self._repo.list_for_provider(payload.provider, payload.provider_key))
|
|
208
|
+
|
|
209
|
+
async def assignable_roles(self, principal: DashboardPrincipal) -> list[DashboardRoleRecord]:
|
|
210
|
+
"""The roles this caller may point a rule (or a provider default) at.
|
|
211
|
+
|
|
212
|
+
The same two rules every write applies — assignable to people, within
|
|
213
|
+
the caller's own grants — evaluated once for the picker, so a caller
|
|
214
|
+
holding ``security:write`` without ``users:manage`` can still name the
|
|
215
|
+
roles it hands out and is never offered one the server would refuse.
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
offered: list[DashboardRoleRecord] = []
|
|
219
|
+
for role in await self._roles.list_roles():
|
|
220
|
+
if not role_assignable_to_users(role):
|
|
221
|
+
continue
|
|
222
|
+
try:
|
|
223
|
+
assert_can_delegate(principal.grants, resolve_role_grants(role))
|
|
224
|
+
except InsufficientDelegationError:
|
|
225
|
+
continue
|
|
226
|
+
offered.append(role)
|
|
227
|
+
return offered
|
|
228
|
+
|
|
229
|
+
# --- helpers ---
|
|
230
|
+
|
|
231
|
+
async def _delegated_role_slug(self, principal: DashboardPrincipal, role_id: str) -> str:
|
|
232
|
+
role = await resolve_assignable_role(self._roles, role_id)
|
|
233
|
+
assert_can_delegate(principal.grants, resolve_role_grants(role))
|
|
234
|
+
return role.slug
|
|
235
|
+
|
|
236
|
+
async def _assert_may_hand_out(self, principal: DashboardPrincipal, role_id: str) -> DashboardRoleRecord | None:
|
|
237
|
+
"""Gate a write on the role the rule ALREADY hands out, and name it for the audit.
|
|
238
|
+
|
|
239
|
+
Editing the value of an admin-granting rule is the same delegation as
|
|
240
|
+
writing one: without this a ``security:write`` holder could retarget it
|
|
241
|
+
at a group it belongs to, or delete it, without ever naming the role.
|
|
242
|
+
Deliberately ``get_role`` and not :func:`resolve_assignable_role`: a
|
|
243
|
+
rule whose role later stopped being assignable must still refuse the
|
|
244
|
+
caller who may not delegate it (403), not answer 422 as if the rule
|
|
245
|
+
itself were the problem.
|
|
246
|
+
"""
|
|
247
|
+
|
|
248
|
+
role = await self._roles.get_role(role_id)
|
|
249
|
+
if role is not None:
|
|
250
|
+
assert_can_delegate(principal.grants, resolve_role_grants(role))
|
|
251
|
+
return role
|
|
252
|
+
|
|
253
|
+
async def _assert_may_move(
|
|
254
|
+
self, principal: DashboardPrincipal, current: Sequence[DashboardRoleMapping], ids: Sequence[str]
|
|
255
|
+
) -> None:
|
|
256
|
+
"""Every rule whose position this call changes must be one the caller may hand out.
|
|
257
|
+
|
|
258
|
+
Promoting an admin-granting rule above one's own is the same act as
|
|
259
|
+
writing it, and demoting one silently disarms it.
|
|
260
|
+
"""
|
|
261
|
+
|
|
262
|
+
position = {mapping_id: index for index, mapping_id in enumerate(ids)}
|
|
263
|
+
moved = {row.role_id for index, row in enumerate(current) if position[row.id] != index}
|
|
264
|
+
for role_id in sorted(moved):
|
|
265
|
+
await self._assert_may_hand_out(principal, role_id)
|
|
266
|
+
|
|
267
|
+
async def _renumber_and_commit(self, ordered_ids: Sequence[str]) -> None:
|
|
268
|
+
"""Write the dense order and commit, as one refusal-mapped step.
|
|
269
|
+
|
|
270
|
+
Both renumbering paths decide the new order from a snapshot they read
|
|
271
|
+
first, so a second writer landing in between makes that snapshot stale
|
|
272
|
+
and the write trips ``uq_dashboard_role_mappings_priority``. That is a
|
|
273
|
+
reload, not a 500: it surfaces as the same stale-order refusal a
|
|
274
|
+
rejected reorder gets.
|
|
275
|
+
"""
|
|
276
|
+
|
|
277
|
+
try:
|
|
278
|
+
await self._repo.renumber(ordered_ids)
|
|
279
|
+
await self._commit()
|
|
280
|
+
except IntegrityError as exc:
|
|
281
|
+
await self._repo.rollback()
|
|
282
|
+
raise OrderStaleError("The rules changed while you were editing; reload and try again") from exc
|
|
283
|
+
|
|
284
|
+
async def _require(self, mapping_id: str) -> DashboardRoleMapping:
|
|
285
|
+
mapping = await self._repo.get_mapping(mapping_id)
|
|
286
|
+
if mapping is None:
|
|
287
|
+
raise MappingNotFoundError("Rule not found")
|
|
288
|
+
return mapping
|
|
289
|
+
|
|
290
|
+
async def _commit(self) -> None:
|
|
291
|
+
await self._repo.commit()
|
|
292
|
+
await invalidate_identity_resolution()
|
|
293
|
+
|
|
294
|
+
@staticmethod
|
|
295
|
+
def _audit(
|
|
296
|
+
principal: DashboardPrincipal,
|
|
297
|
+
mapping: DashboardRoleMapping,
|
|
298
|
+
actor_ip: str | None,
|
|
299
|
+
*,
|
|
300
|
+
operation: str,
|
|
301
|
+
role_slug: str | None,
|
|
302
|
+
) -> None:
|
|
303
|
+
details: AuditDetails = {
|
|
304
|
+
"operation": operation,
|
|
305
|
+
"provider": mapping.provider,
|
|
306
|
+
"provider_key": mapping.provider_key,
|
|
307
|
+
"claim_name": mapping.claim_name,
|
|
308
|
+
"claim_value": mapping.claim_value,
|
|
309
|
+
"priority": mapping.priority,
|
|
310
|
+
}
|
|
311
|
+
if role_slug is not None:
|
|
312
|
+
details = {**details, "role": role_slug}
|
|
313
|
+
AuditService.log_async(
|
|
314
|
+
"role_mapping_changed",
|
|
315
|
+
actor_ip=actor_ip,
|
|
316
|
+
details=details,
|
|
317
|
+
actor=AuditActor.from_principal(principal),
|
|
318
|
+
target=AuditTarget("role_mapping", mapping.id),
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
async def invalidate_identity_resolution() -> None:
|
|
323
|
+
"""Drop the caches that decide how an identity resolves, here and on peers.
|
|
324
|
+
|
|
325
|
+
The registry bump reaches other replicas through the invalidation
|
|
326
|
+
namespace; this replica's own identity cache is cleared directly so the
|
|
327
|
+
operator's very next request already sees the new rule (a cached refusal
|
|
328
|
+
must not keep refusing someone a rule now admits).
|
|
329
|
+
"""
|
|
330
|
+
|
|
331
|
+
from app.modules.dashboard_users.identity_resolver import get_identity_resolution_cache
|
|
332
|
+
|
|
333
|
+
await get_auth_provider_registry().invalidate()
|
|
334
|
+
get_identity_resolution_cache().clear()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Runtime metadata APIs."""
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, Depends
|
|
4
|
+
|
|
5
|
+
from app.core.auth.dependencies import set_dashboard_error_format, validate_dashboard_session
|
|
6
|
+
from app.modules.runtime.schemas import RuntimeVersionResponse
|
|
7
|
+
from app.modules.runtime.service import get_runtime_version_service
|
|
8
|
+
|
|
9
|
+
router = APIRouter(
|
|
10
|
+
prefix="/api/runtime",
|
|
11
|
+
tags=["runtime"],
|
|
12
|
+
dependencies=[Depends(validate_dashboard_session), Depends(set_dashboard_error_format)],
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@router.get("/version", response_model=RuntimeVersionResponse)
|
|
17
|
+
async def get_runtime_version() -> RuntimeVersionResponse:
|
|
18
|
+
return await get_runtime_version_service().get_version_status()
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from app.modules.shared.schemas import DashboardModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RuntimeVersionResponse(DashboardModel):
|
|
9
|
+
current_version: str
|
|
10
|
+
latest_version: str | None = None
|
|
11
|
+
update_available: bool = False
|
|
12
|
+
checked_at: datetime
|
|
13
|
+
source: str | None = None
|
|
14
|
+
release_url: str = "https://github.com/Soju06/codex-lb/releases/latest"
|