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,429 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
from types import MappingProxyType
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from app.core.auth.dashboard_mode import DashboardAuthMode
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from app.db.models import DashboardUser
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DashboardRole(StrEnum):
|
|
17
|
+
ADMIN = "admin"
|
|
18
|
+
GUEST = "guest"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DashboardPermission(StrEnum):
|
|
22
|
+
"""Coarse wire-level permission aliases exposed to dashboard clients.
|
|
23
|
+
|
|
24
|
+
``read`` / ``write`` are derived from the fine-grained grants below and stay
|
|
25
|
+
the only values the session response emits, so existing clients keep
|
|
26
|
+
parsing the contract unchanged.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
READ = "read"
|
|
30
|
+
WRITE = "write"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Permission(StrEnum):
|
|
34
|
+
"""Fine-grained dashboard permissions (``<resource>:<action>``)."""
|
|
35
|
+
|
|
36
|
+
DASHBOARD_READ = "dashboard:read"
|
|
37
|
+
ACCOUNTS_READ = "accounts:read"
|
|
38
|
+
ACCOUNTS_WRITE = "accounts:write"
|
|
39
|
+
ACCOUNTS_EXPORT = "accounts:export"
|
|
40
|
+
API_KEYS_READ = "api_keys:read"
|
|
41
|
+
API_KEYS_WRITE = "api_keys:write"
|
|
42
|
+
API_KEYS_ASSIGN = "api_keys:assign"
|
|
43
|
+
OPS_WRITE = "ops:write"
|
|
44
|
+
SECURITY_WRITE = "security:write"
|
|
45
|
+
USERS_MANAGE = "users:manage"
|
|
46
|
+
ROLES_MANAGE = "roles:manage"
|
|
47
|
+
CONVERSATIONS_READ = "conversations:read"
|
|
48
|
+
AUDIT_READ = "audit:read"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Scope(StrEnum):
|
|
52
|
+
"""How far a granted permission reaches.
|
|
53
|
+
|
|
54
|
+
``all`` covers every resource; ``own`` covers only resources owned by the
|
|
55
|
+
principal. ``all`` satisfies any requirement, ``own`` satisfies only an
|
|
56
|
+
``own`` requirement.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
ALL = "all"
|
|
60
|
+
OWN = "own"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
_SCOPE_RANK: Mapping[Scope, int] = MappingProxyType({Scope.OWN: 1, Scope.ALL: 2})
|
|
64
|
+
|
|
65
|
+
Grants = Mapping[Permission, Scope]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _grants(mapping: dict[Permission, Scope]) -> Grants:
|
|
69
|
+
return MappingProxyType(dict(mapping))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
#: Permissions that may be granted with ``own`` scope. Every other permission is
|
|
73
|
+
#: all-or-nothing.
|
|
74
|
+
OWN_SCOPED_PERMISSIONS: frozenset[Permission] = frozenset(
|
|
75
|
+
{
|
|
76
|
+
Permission.DASHBOARD_READ,
|
|
77
|
+
Permission.API_KEYS_READ,
|
|
78
|
+
Permission.API_KEYS_WRITE,
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
#: Dependency rules: granting the key requires every listed permission at
|
|
83
|
+
#: ``all`` scope. Enforced on the built-in role table at import time and on any
|
|
84
|
+
#: future role editor.
|
|
85
|
+
PERMISSION_IMPLIES: Mapping[Permission, frozenset[Permission]] = MappingProxyType(
|
|
86
|
+
{
|
|
87
|
+
Permission.API_KEYS_ASSIGN: frozenset({Permission.API_KEYS_WRITE}),
|
|
88
|
+
Permission.ACCOUNTS_EXPORT: frozenset({Permission.ACCOUNTS_READ}),
|
|
89
|
+
Permission.SECURITY_WRITE: frozenset({Permission.OPS_WRITE}),
|
|
90
|
+
}
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
#: Permissions whose holders count as "admin-grade" for security policy
|
|
94
|
+
#: (for example a future "TOTP required for admins" option).
|
|
95
|
+
PRIVILEGED_PERMISSIONS: frozenset[Permission] = frozenset(
|
|
96
|
+
{
|
|
97
|
+
Permission.SECURITY_WRITE,
|
|
98
|
+
Permission.USERS_MANAGE,
|
|
99
|
+
Permission.ROLES_MANAGE,
|
|
100
|
+
Permission.ACCOUNTS_EXPORT,
|
|
101
|
+
Permission.CONVERSATIONS_READ,
|
|
102
|
+
Permission.AUDIT_READ,
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
#: Permissions whose mutations additionally require a recent re-verification
|
|
107
|
+
#: of the caller ("step-up", PLAN §5 H5): a stolen cookie alone must not be
|
|
108
|
+
#: enough to change who may sign in or to export account credentials.
|
|
109
|
+
STEP_UP_PERMISSIONS: frozenset[Permission] = frozenset(
|
|
110
|
+
{
|
|
111
|
+
Permission.SECURITY_WRITE,
|
|
112
|
+
Permission.USERS_MANAGE,
|
|
113
|
+
Permission.ROLES_MANAGE,
|
|
114
|
+
Permission.ACCOUNTS_EXPORT,
|
|
115
|
+
}
|
|
116
|
+
)
|
|
117
|
+
assert STEP_UP_PERMISSIONS <= PRIVILEGED_PERMISSIONS
|
|
118
|
+
|
|
119
|
+
#: The legacy ``write`` alias means "may perform every mutation the generic
|
|
120
|
+
#: write gate protects", which today spans accounts, API keys, and operations.
|
|
121
|
+
_WRITE_ALIAS_PERMISSIONS: frozenset[Permission] = frozenset(
|
|
122
|
+
{
|
|
123
|
+
Permission.ACCOUNTS_WRITE,
|
|
124
|
+
Permission.API_KEYS_WRITE,
|
|
125
|
+
Permission.OPS_WRITE,
|
|
126
|
+
}
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class RoleKind(StrEnum):
|
|
131
|
+
"""``dashboard_roles.kind``: presets resolve from code, custom roles from grant rows."""
|
|
132
|
+
|
|
133
|
+
PRESET = "preset"
|
|
134
|
+
CUSTOM = "custom"
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class PresetRoleSlug(StrEnum):
|
|
138
|
+
"""The built-in roles. Their grants live in code, never in the database."""
|
|
139
|
+
|
|
140
|
+
ADMIN = "admin"
|
|
141
|
+
OPERATOR = "operator"
|
|
142
|
+
MEMBER = "member"
|
|
143
|
+
VIEWER = "viewer"
|
|
144
|
+
GUEST = "guest"
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
#: Stable identifiers for the preset role rows (``dashboard_roles``), derived
|
|
148
|
+
#: from the slug so every install and every replica agrees without coordination.
|
|
149
|
+
_PRESET_ROLE_ID_NAMESPACE = uuid.UUID("6f1c0e4e-2b4a-4c1e-9c3b-7a5d2e8f0a11")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def preset_role_id(slug: PresetRoleSlug) -> str:
|
|
153
|
+
"""Deterministic id of a preset role row (UUIDv5 of the slug)."""
|
|
154
|
+
|
|
155
|
+
return str(uuid.uuid5(_PRESET_ROLE_ID_NAMESPACE, f"codex-lb:dashboard-role:{slug.value}"))
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
PRESET_ROLE_IDS: Mapping[PresetRoleSlug, str] = MappingProxyType(
|
|
159
|
+
{slug: preset_role_id(slug) for slug in PresetRoleSlug}
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
PRESET_ROLE_NAMES: Mapping[PresetRoleSlug, str] = MappingProxyType(
|
|
163
|
+
{
|
|
164
|
+
PresetRoleSlug.ADMIN: "Admin",
|
|
165
|
+
PresetRoleSlug.OPERATOR: "Operator",
|
|
166
|
+
PresetRoleSlug.MEMBER: "Member",
|
|
167
|
+
PresetRoleSlug.VIEWER: "Viewer",
|
|
168
|
+
PresetRoleSlug.GUEST: "Guest",
|
|
169
|
+
}
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
ADMIN_GRANTS: Grants = _grants({permission: Scope.ALL for permission in Permission})
|
|
173
|
+
OPERATOR_GRANTS: Grants = _grants(
|
|
174
|
+
{
|
|
175
|
+
Permission.DASHBOARD_READ: Scope.ALL,
|
|
176
|
+
Permission.ACCOUNTS_READ: Scope.ALL,
|
|
177
|
+
Permission.ACCOUNTS_WRITE: Scope.ALL,
|
|
178
|
+
Permission.API_KEYS_READ: Scope.ALL,
|
|
179
|
+
Permission.API_KEYS_WRITE: Scope.ALL,
|
|
180
|
+
Permission.API_KEYS_ASSIGN: Scope.ALL,
|
|
181
|
+
Permission.OPS_WRITE: Scope.ALL,
|
|
182
|
+
}
|
|
183
|
+
)
|
|
184
|
+
MEMBER_GRANTS: Grants = _grants(
|
|
185
|
+
{
|
|
186
|
+
Permission.DASHBOARD_READ: Scope.OWN,
|
|
187
|
+
Permission.API_KEYS_READ: Scope.OWN,
|
|
188
|
+
Permission.API_KEYS_WRITE: Scope.OWN,
|
|
189
|
+
}
|
|
190
|
+
)
|
|
191
|
+
VIEWER_GRANTS: Grants = _grants(
|
|
192
|
+
{
|
|
193
|
+
Permission.DASHBOARD_READ: Scope.ALL,
|
|
194
|
+
Permission.ACCOUNTS_READ: Scope.ALL,
|
|
195
|
+
}
|
|
196
|
+
)
|
|
197
|
+
GUEST_GRANTS: Grants = VIEWER_GRANTS
|
|
198
|
+
|
|
199
|
+
#: Grants of every preset role. The database stores preset *rows* (for foreign
|
|
200
|
+
#: keys and listings) but never their grants: this table is the single truth,
|
|
201
|
+
#: so an upgrade that adds a permission updates every preset by definition and
|
|
202
|
+
#: replicas of different versions can never disagree about what ``admin`` means.
|
|
203
|
+
PRESET_ROLE_GRANTS: Mapping[PresetRoleSlug, Grants] = MappingProxyType(
|
|
204
|
+
{
|
|
205
|
+
PresetRoleSlug.ADMIN: ADMIN_GRANTS,
|
|
206
|
+
PresetRoleSlug.OPERATOR: OPERATOR_GRANTS,
|
|
207
|
+
PresetRoleSlug.MEMBER: MEMBER_GRANTS,
|
|
208
|
+
PresetRoleSlug.VIEWER: VIEWER_GRANTS,
|
|
209
|
+
PresetRoleSlug.GUEST: GUEST_GRANTS,
|
|
210
|
+
}
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
#: Presets that may be assigned to a user account. Guest is the anonymous
|
|
214
|
+
#: shared read-only session and is never a user's role. Member (own-scoped
|
|
215
|
+
#: grants only) becomes assignable once own-scoped views ship; until then the
|
|
216
|
+
#: session dependency refuses roles without ``dashboard:read`` at ``all``.
|
|
217
|
+
ASSIGNABLE_PRESET_ROLES: frozenset[PresetRoleSlug] = frozenset(PresetRoleSlug) - {
|
|
218
|
+
PresetRoleSlug.GUEST,
|
|
219
|
+
PresetRoleSlug.MEMBER,
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
ROLE_GRANTS: Mapping[DashboardRole, Grants] = MappingProxyType(
|
|
223
|
+
{
|
|
224
|
+
DashboardRole.ADMIN: ADMIN_GRANTS,
|
|
225
|
+
DashboardRole.GUEST: GUEST_GRANTS,
|
|
226
|
+
}
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def scope_satisfies(granted: Scope | None, required: Scope) -> bool:
|
|
231
|
+
if granted is None:
|
|
232
|
+
return False
|
|
233
|
+
return _SCOPE_RANK[granted] >= _SCOPE_RANK[required]
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class InsufficientDelegationError(Exception):
|
|
237
|
+
"""The caller's grants do not cover the grants of the role it wants to assign or act on."""
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _grants_cover(caller: Grants, target: Grants) -> bool:
|
|
241
|
+
return all(scope_satisfies(caller.get(permission), scope) for permission, scope in target.items())
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def assert_can_delegate(caller: Grants, target: Grants) -> None:
|
|
245
|
+
"""Assigning a role: every (permission, scope) of ``target`` must be at or
|
|
246
|
+
below the caller's own grant for that permission (``own`` < ``all``; a
|
|
247
|
+
permission the caller lacks is below both). It follows that only an admin
|
|
248
|
+
preset holder can grant the admin preset.
|
|
249
|
+
"""
|
|
250
|
+
|
|
251
|
+
if not _grants_cover(caller, target):
|
|
252
|
+
raise InsufficientDelegationError("The role holds permissions the caller does not have")
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def assert_can_act_on(caller: Grants, target_user_grants: Grants) -> None:
|
|
256
|
+
"""Acting on an account (disable, delete, reset, revoke, re-invite): the
|
|
257
|
+
same subset check applied to the grants of the account's current role."""
|
|
258
|
+
|
|
259
|
+
if not _grants_cover(caller, target_user_grants):
|
|
260
|
+
raise InsufficientDelegationError("The account holds permissions the caller does not have")
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def is_admin_level(grants: Grants) -> bool:
|
|
264
|
+
"""Whether a grant table counts as "admin-level" for security policy.
|
|
265
|
+
|
|
266
|
+
The admin preset qualifies by definition; a custom role qualifies as soon as
|
|
267
|
+
it holds one :data:`PRIVILEGED_PERMISSIONS` entry. Operator, member, viewer
|
|
268
|
+
and guest never do.
|
|
269
|
+
"""
|
|
270
|
+
|
|
271
|
+
return not PRIVILEGED_PERMISSIONS.isdisjoint(grants)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def totp_policy_applies(*, required_on_login: bool, required_for_admin_role: bool, grants: Grants) -> bool:
|
|
275
|
+
"""Whether the install's TOTP policy binds an account holding ``grants``.
|
|
276
|
+
|
|
277
|
+
The global toggle binds every password account; the admin-role toggle binds
|
|
278
|
+
admin-level accounts only. One predicate serves the session dependency and
|
|
279
|
+
the session response so the two can never disagree.
|
|
280
|
+
"""
|
|
281
|
+
|
|
282
|
+
return required_on_login or (required_for_admin_role and is_admin_level(grants))
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def legacy_permissions(grants: Grants) -> frozenset[DashboardPermission]:
|
|
286
|
+
"""Derive the coarse ``read`` / ``write`` aliases from fine-grained grants."""
|
|
287
|
+
|
|
288
|
+
aliases: set[DashboardPermission] = set()
|
|
289
|
+
if Permission.DASHBOARD_READ in grants:
|
|
290
|
+
aliases.add(DashboardPermission.READ)
|
|
291
|
+
if all(grants.get(permission) is Scope.ALL for permission in _WRITE_ALIAS_PERMISSIONS):
|
|
292
|
+
aliases.add(DashboardPermission.WRITE)
|
|
293
|
+
return frozenset(aliases)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def validate_grants(grants: Grants) -> None:
|
|
297
|
+
"""Fail fast when a grant table violates the vocabulary rules."""
|
|
298
|
+
|
|
299
|
+
for permission, scope in grants.items():
|
|
300
|
+
if scope is Scope.OWN and permission not in OWN_SCOPED_PERMISSIONS:
|
|
301
|
+
raise ValueError(f"{permission.value} does not support the 'own' scope")
|
|
302
|
+
for permission, required in PERMISSION_IMPLIES.items():
|
|
303
|
+
if permission not in grants:
|
|
304
|
+
continue
|
|
305
|
+
missing = [dep.value for dep in sorted(required) if grants.get(dep) is not Scope.ALL]
|
|
306
|
+
if missing:
|
|
307
|
+
raise ValueError(f"{permission.value} requires {', '.join(missing)} at 'all' scope")
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
for _role_grants in PRESET_ROLE_GRANTS.values():
|
|
311
|
+
validate_grants(_role_grants)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
@dataclass(frozen=True, slots=True)
|
|
315
|
+
class DashboardPrincipal:
|
|
316
|
+
"""An authenticated dashboard caller.
|
|
317
|
+
|
|
318
|
+
``grants`` is the source of truth for authorization; ``permissions`` holds
|
|
319
|
+
the coarse aliases derived from it and is validated for consistency so the
|
|
320
|
+
two can never drift. ``grants`` is excluded from hashing because mappings
|
|
321
|
+
are unhashable; equality still compares it.
|
|
322
|
+
|
|
323
|
+
``role`` stays the coarse wire value (``admin`` for every user account and
|
|
324
|
+
for the implicit/trusted-header/disabled admin, ``guest`` for guests);
|
|
325
|
+
``role_slug`` carries the account's actual role when the principal is a
|
|
326
|
+
user. ``user_id`` is ``None`` for principals without a user row.
|
|
327
|
+
``totp_enrollment_required`` marks a user whose install requires TOTP but
|
|
328
|
+
who has not enrolled yet: only the dashboard-auth self-service routes may
|
|
329
|
+
serve such a principal. ``step_up_verified_at`` is the unix time the
|
|
330
|
+
session cookie last recorded a step-up re-verification (``su``), if any.
|
|
331
|
+
"""
|
|
332
|
+
|
|
333
|
+
role: DashboardRole
|
|
334
|
+
permissions: frozenset[DashboardPermission]
|
|
335
|
+
auth_mode: DashboardAuthMode
|
|
336
|
+
actor: str | None = None
|
|
337
|
+
grants: Grants = field(kw_only=True, hash=False)
|
|
338
|
+
user_id: str | None = field(default=None, kw_only=True)
|
|
339
|
+
username: str | None = field(default=None, kw_only=True)
|
|
340
|
+
role_slug: str | None = field(default=None, kw_only=True)
|
|
341
|
+
auth_method: str | None = field(default=None, kw_only=True)
|
|
342
|
+
totp_enrollment_required: bool = field(default=False, kw_only=True)
|
|
343
|
+
step_up_verified_at: int | None = field(default=None, kw_only=True)
|
|
344
|
+
|
|
345
|
+
def __post_init__(self) -> None:
|
|
346
|
+
validate_grants(self.grants)
|
|
347
|
+
expected = legacy_permissions(self.grants)
|
|
348
|
+
if self.permissions != expected:
|
|
349
|
+
raise ValueError(
|
|
350
|
+
f"permissions {sorted(self.permissions)} do not match grants-derived aliases {sorted(expected)}"
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
def can(self, permission: DashboardPermission) -> bool:
|
|
354
|
+
return permission in self.permissions
|
|
355
|
+
|
|
356
|
+
def scope(self, permission: Permission) -> Scope | None:
|
|
357
|
+
return self.grants.get(permission)
|
|
358
|
+
|
|
359
|
+
def has(self, permission: Permission, *, minimum_scope: Scope = Scope.ALL) -> bool:
|
|
360
|
+
return scope_satisfies(self.grants.get(permission), minimum_scope)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
ADMIN_PERMISSIONS = legacy_permissions(ADMIN_GRANTS)
|
|
364
|
+
GUEST_PERMISSIONS = legacy_permissions(GUEST_GRANTS)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def permission_strings(grants: Grants) -> list[str]:
|
|
368
|
+
"""Wire form of a grant table: the legacy aliases first, then ``<permission>:<scope>``.
|
|
369
|
+
|
|
370
|
+
Clients that predate fine-grained permissions only look for ``write``; newer
|
|
371
|
+
clients read the scoped entries.
|
|
372
|
+
"""
|
|
373
|
+
|
|
374
|
+
aliases = sorted(alias.value for alias in legacy_permissions(grants))
|
|
375
|
+
scoped = sorted(f"{permission.value}:{scope.value}" for permission, scope in grants.items())
|
|
376
|
+
return aliases + scoped
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def admin_principal(
|
|
380
|
+
*,
|
|
381
|
+
auth_mode: DashboardAuthMode,
|
|
382
|
+
actor: str | None = None,
|
|
383
|
+
auth_method: str | None = None,
|
|
384
|
+
) -> DashboardPrincipal:
|
|
385
|
+
"""The implicit admin: no user row (local passwordless install, trusted header, disabled auth)."""
|
|
386
|
+
|
|
387
|
+
return DashboardPrincipal(
|
|
388
|
+
role=DashboardRole.ADMIN,
|
|
389
|
+
permissions=ADMIN_PERMISSIONS,
|
|
390
|
+
auth_mode=auth_mode,
|
|
391
|
+
actor=actor,
|
|
392
|
+
grants=ADMIN_GRANTS,
|
|
393
|
+
auth_method=auth_method,
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def user_principal(
|
|
398
|
+
user: DashboardUser,
|
|
399
|
+
grants: Grants,
|
|
400
|
+
*,
|
|
401
|
+
auth_method: str | None,
|
|
402
|
+
totp_enrollment_required: bool = False,
|
|
403
|
+
auth_mode: DashboardAuthMode = DashboardAuthMode.STANDARD,
|
|
404
|
+
step_up_verified_at: int | None = None,
|
|
405
|
+
) -> DashboardPrincipal:
|
|
406
|
+
"""A signed-in user account. ``grants`` is the resolved grant table of ``user.role``."""
|
|
407
|
+
|
|
408
|
+
return DashboardPrincipal(
|
|
409
|
+
role=DashboardRole.ADMIN,
|
|
410
|
+
permissions=legacy_permissions(grants),
|
|
411
|
+
auth_mode=auth_mode,
|
|
412
|
+
actor=user.username,
|
|
413
|
+
grants=grants,
|
|
414
|
+
user_id=user.id,
|
|
415
|
+
username=user.username,
|
|
416
|
+
role_slug=user.role.slug,
|
|
417
|
+
auth_method=auth_method,
|
|
418
|
+
totp_enrollment_required=totp_enrollment_required,
|
|
419
|
+
step_up_verified_at=step_up_verified_at,
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def guest_principal() -> DashboardPrincipal:
|
|
424
|
+
return DashboardPrincipal(
|
|
425
|
+
role=DashboardRole.GUEST,
|
|
426
|
+
permissions=GUEST_PERMISSIONS,
|
|
427
|
+
auth_mode=DashboardAuthMode.STANDARD,
|
|
428
|
+
grants=GUEST_GRANTS,
|
|
429
|
+
)
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
from functools import lru_cache
|
|
8
|
+
from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network, ip_address, ip_network
|
|
9
|
+
|
|
10
|
+
from fastapi import Request
|
|
11
|
+
|
|
12
|
+
from app.core.socket_peer import raw_socket_peer_host
|
|
13
|
+
|
|
14
|
+
_HEADER_NAME_PATTERN = re.compile(r"^[!#$%&'*+.^_`|~0-9A-Za-z-]+$")
|
|
15
|
+
_FORBIDDEN_PROXY_AUTH_HEADERS = frozenset(
|
|
16
|
+
{
|
|
17
|
+
"authorization",
|
|
18
|
+
"connection",
|
|
19
|
+
"content-length",
|
|
20
|
+
"cookie",
|
|
21
|
+
"forwarded",
|
|
22
|
+
"host",
|
|
23
|
+
"keep-alive",
|
|
24
|
+
"proxy-authenticate",
|
|
25
|
+
"proxy-authorization",
|
|
26
|
+
"te",
|
|
27
|
+
"trailer",
|
|
28
|
+
"transfer-encoding",
|
|
29
|
+
"upgrade",
|
|
30
|
+
"x-forwarded-for",
|
|
31
|
+
"x-forwarded-host",
|
|
32
|
+
"x-forwarded-proto",
|
|
33
|
+
}
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class DashboardAuthMode(StrEnum):
|
|
38
|
+
STANDARD = "standard"
|
|
39
|
+
TRUSTED_HEADER = "trusted_header"
|
|
40
|
+
DISABLED = "disabled"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(slots=True, frozen=True)
|
|
44
|
+
class DashboardRequestAuth:
|
|
45
|
+
mode: DashboardAuthMode
|
|
46
|
+
actor: str | None = None
|
|
47
|
+
#: Groups the proxy asserted for ``actor`` (empty unless the groups header arrived exactly once).
|
|
48
|
+
groups: tuple[str, ...] = ()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def normalize_dashboard_auth_proxy_header(value: str, field_name: str = "dashboard_auth_proxy_header") -> str:
|
|
52
|
+
"""Validate a proxy-asserted header name; ``field_name`` names the setting in the error."""
|
|
53
|
+
|
|
54
|
+
header = value.strip()
|
|
55
|
+
if not header:
|
|
56
|
+
raise ValueError(f"{field_name} must not be empty")
|
|
57
|
+
if not _HEADER_NAME_PATTERN.fullmatch(header):
|
|
58
|
+
raise ValueError(f"{field_name} must be a valid HTTP header name")
|
|
59
|
+
if header.lower() in _FORBIDDEN_PROXY_AUTH_HEADERS:
|
|
60
|
+
raise ValueError(f"{field_name} must not use reserved header '{header}'")
|
|
61
|
+
return header
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
#: Bounds on the group set a proxy may assert: a rule list is evaluated per
|
|
65
|
+
#: sign-in, so an unbounded header must not turn into unbounded work.
|
|
66
|
+
MAX_PROXY_GROUPS = 100
|
|
67
|
+
MAX_PROXY_GROUP_LENGTH = 200
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def parse_proxy_groups(raw_values: Sequence[str]) -> tuple[str, ...]:
|
|
71
|
+
"""The group set of a trusted-header request: comma-separated, case-folded, bounded.
|
|
72
|
+
|
|
73
|
+
Honoured only when the header appears exactly once. A second occurrence
|
|
74
|
+
means something upstream is not stripping a client-supplied copy, so the
|
|
75
|
+
set fails closed to empty (which demotes a person rather than admitting
|
|
76
|
+
one). Items are trimmed, empties dropped, over-long items dropped, and the
|
|
77
|
+
result deduplicated case-insensitively and capped.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
if len(raw_values) != 1:
|
|
81
|
+
return ()
|
|
82
|
+
groups: list[str] = []
|
|
83
|
+
seen: set[str] = set()
|
|
84
|
+
for item in raw_values[0].split(","):
|
|
85
|
+
group = item.strip().casefold()
|
|
86
|
+
if not group or len(group) > MAX_PROXY_GROUP_LENGTH or group in seen:
|
|
87
|
+
continue
|
|
88
|
+
seen.add(group)
|
|
89
|
+
groups.append(group)
|
|
90
|
+
if len(groups) == MAX_PROXY_GROUPS:
|
|
91
|
+
break
|
|
92
|
+
return tuple(groups)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _trusted_proxy_networks() -> tuple[IPv4Network | IPv6Network, ...]:
|
|
96
|
+
from app.core.config.settings import get_settings
|
|
97
|
+
|
|
98
|
+
settings = get_settings()
|
|
99
|
+
return _parse_trusted_proxy_networks(tuple(settings.firewall_trusted_proxy_cidrs))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@lru_cache(maxsize=16)
|
|
103
|
+
def _parse_trusted_proxy_networks(
|
|
104
|
+
cidrs: tuple[str, ...],
|
|
105
|
+
) -> tuple[IPv4Network | IPv6Network, ...]:
|
|
106
|
+
return tuple(ip_network(cidr, strict=False) for cidr in cidrs)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def get_dashboard_request_auth(request: Request) -> DashboardRequestAuth | None:
|
|
110
|
+
cached = getattr(request.state, "dashboard_request_auth", None)
|
|
111
|
+
if isinstance(cached, DashboardRequestAuth):
|
|
112
|
+
return cached
|
|
113
|
+
|
|
114
|
+
from app.core.config.settings import get_settings
|
|
115
|
+
|
|
116
|
+
settings = get_settings()
|
|
117
|
+
auth: DashboardRequestAuth | None = None
|
|
118
|
+
if settings.dashboard_auth_mode == DashboardAuthMode.DISABLED:
|
|
119
|
+
auth = DashboardRequestAuth(mode=DashboardAuthMode.DISABLED)
|
|
120
|
+
elif settings.dashboard_auth_mode == DashboardAuthMode.TRUSTED_HEADER:
|
|
121
|
+
auth = _get_trusted_header_auth(request)
|
|
122
|
+
|
|
123
|
+
if auth is not None:
|
|
124
|
+
request.state.dashboard_request_auth = auth
|
|
125
|
+
return auth
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def password_management_enabled(mode: DashboardAuthMode) -> bool:
|
|
129
|
+
return mode != DashboardAuthMode.DISABLED
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _get_trusted_header_auth(request: Request) -> DashboardRequestAuth | None:
|
|
133
|
+
from app.core.config.settings import get_settings
|
|
134
|
+
|
|
135
|
+
settings = get_settings()
|
|
136
|
+
client_host = raw_socket_peer_host(request)
|
|
137
|
+
if not client_host or not settings.firewall_trust_proxy_headers:
|
|
138
|
+
return None
|
|
139
|
+
if not _is_trusted_proxy_source(client_host, _trusted_proxy_networks()):
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
raw_actors = request.headers.getlist(settings.dashboard_auth_proxy_header)
|
|
143
|
+
if len(raw_actors) != 1:
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
actor = raw_actors[0].strip()
|
|
147
|
+
if not actor:
|
|
148
|
+
return None
|
|
149
|
+
groups = parse_proxy_groups(request.headers.getlist(settings.dashboard_auth_proxy_groups_header))
|
|
150
|
+
return DashboardRequestAuth(mode=DashboardAuthMode.TRUSTED_HEADER, actor=actor, groups=groups)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _is_trusted_proxy_source(
|
|
154
|
+
source_ip: str,
|
|
155
|
+
trusted_proxy_networks: tuple[IPv4Network | IPv6Network, ...],
|
|
156
|
+
) -> bool:
|
|
157
|
+
try:
|
|
158
|
+
candidate = ip_address(source_ip)
|
|
159
|
+
except ValueError:
|
|
160
|
+
return False
|
|
161
|
+
|
|
162
|
+
if not trusted_proxy_networks:
|
|
163
|
+
return False
|
|
164
|
+
return any(_network_contains(network, candidate) for network in trusted_proxy_networks)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _network_contains(
|
|
168
|
+
network: IPv4Network | IPv6Network,
|
|
169
|
+
candidate: IPv4Address | IPv6Address,
|
|
170
|
+
) -> bool:
|
|
171
|
+
return candidate.version == network.version and candidate in network
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from starlette.requests import HTTPConnection
|
|
7
|
+
|
|
8
|
+
DEFAULT_DASHBOARD_SESSION_TTL_SECONDS = 365 * 24 * 60 * 60
|
|
9
|
+
REMOTE_DASHBOARD_SESSION_TTL_SECONDS = 12 * 60 * 60
|
|
10
|
+
LONG_DASHBOARD_SESSION_TTL_THRESHOLD_SECONDS = 30 * 24 * 60 * 60
|
|
11
|
+
_FORWARDED_CLIENT_IP_HEADERS = (
|
|
12
|
+
"x-forwarded-for",
|
|
13
|
+
"forwarded",
|
|
14
|
+
"x-real-ip",
|
|
15
|
+
"true-client-ip",
|
|
16
|
+
"cf-connecting-ip",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def resolve_dashboard_session_ttl_seconds(request: HTTPConnection, configured_ttl_seconds: int) -> int:
|
|
21
|
+
if configured_ttl_seconds <= LONG_DASHBOARD_SESSION_TTL_THRESHOLD_SECONDS:
|
|
22
|
+
return configured_ttl_seconds
|
|
23
|
+
if _allows_long_local_dashboard_session(request):
|
|
24
|
+
return configured_ttl_seconds
|
|
25
|
+
return REMOTE_DASHBOARD_SESSION_TTL_SECONDS
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def resolve_admin_dashboard_session_ttl_seconds(request: HTTPConnection, configured_ttl_seconds: int) -> int:
|
|
29
|
+
"""Admin-role sessions are capped at 12 hours unless the request is a trusted local one.
|
|
30
|
+
|
|
31
|
+
Applies on top of :func:`resolve_dashboard_session_ttl_seconds`: shorter
|
|
32
|
+
configured lifetimes still win, and the same locality test that allows a
|
|
33
|
+
long session for everyone decides whether an admin may keep the full value.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
ttl_seconds = resolve_dashboard_session_ttl_seconds(request, configured_ttl_seconds)
|
|
37
|
+
if ttl_seconds <= REMOTE_DASHBOARD_SESSION_TTL_SECONDS or _allows_long_local_dashboard_session(request):
|
|
38
|
+
return ttl_seconds
|
|
39
|
+
return REMOTE_DASHBOARD_SESSION_TTL_SECONDS
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _allows_long_local_dashboard_session(request: HTTPConnection) -> bool:
|
|
43
|
+
from app.core.auth.dashboard_mode import DashboardAuthMode
|
|
44
|
+
|
|
45
|
+
settings = _get_settings()
|
|
46
|
+
if settings.dashboard_auth_mode != DashboardAuthMode.STANDARD:
|
|
47
|
+
return False
|
|
48
|
+
if settings.firewall_trust_proxy_headers:
|
|
49
|
+
return False
|
|
50
|
+
if any(value for header in _FORWARDED_CLIENT_IP_HEADERS for value in request.headers.getlist(header)):
|
|
51
|
+
return False
|
|
52
|
+
if _is_local_request(request):
|
|
53
|
+
return True
|
|
54
|
+
if not settings.dashboard_trust_loopback_host_header_for_long_sessions:
|
|
55
|
+
return False
|
|
56
|
+
return _uses_loopback_dashboard_url(request)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _get_settings() -> Any:
|
|
60
|
+
from app.core.config.settings import get_settings
|
|
61
|
+
|
|
62
|
+
return get_settings()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _is_local_request(request: HTTPConnection) -> bool:
|
|
66
|
+
from app.core.request_locality import is_local_request
|
|
67
|
+
|
|
68
|
+
return is_local_request(request)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _uses_loopback_dashboard_url(request: HTTPConnection) -> bool:
|
|
72
|
+
from app.core.request_locality import is_local_host
|
|
73
|
+
|
|
74
|
+
return is_local_host(request.url.hostname)
|