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
app/__init__.py
ADDED
app/admin_cli.py
ADDED
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
"""Host recovery commands: the way back in when the dashboard cannot let anyone in.
|
|
2
|
+
|
|
3
|
+
PLAN §4.6 gives the identity work one non-negotiable escape hatch. Every gate
|
|
4
|
+
this release adds — a tightened ``local_login_policy``, a sign-in provider that
|
|
5
|
+
owns the login screen, the second factor a break-glass account must always
|
|
6
|
+
present — protects the install right up to the moment it is the thing that
|
|
7
|
+
broke. When the identity provider is unreachable, or the second-factor secret
|
|
8
|
+
went missing with the phone that held it, the only actor left to trust is
|
|
9
|
+
whoever can reach the host and its database.
|
|
10
|
+
|
|
11
|
+
So these commands take the shortest path that exists: the configured database
|
|
12
|
+
URL, a synchronous engine of their own, one transaction. No FastAPI app, no
|
|
13
|
+
``init_db()`` (it takes the SQLite lifetime lock and would fight a running
|
|
14
|
+
server), no environment variable of their own, no network call — and
|
|
15
|
+
deliberately no policy or provider gate, because they are the recovery path for
|
|
16
|
+
the very lockout those gates exist to prevent. Every run leaves one
|
|
17
|
+
``break_glass_cli_used`` row at critical severity naming the command and its
|
|
18
|
+
target: an operator on the host is unauthenticated by construction, so the
|
|
19
|
+
audit trail is the whole of the accountability.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import argparse
|
|
25
|
+
import getpass
|
|
26
|
+
import json
|
|
27
|
+
import sys
|
|
28
|
+
from collections.abc import Iterator
|
|
29
|
+
from contextlib import contextmanager
|
|
30
|
+
from dataclasses import dataclass
|
|
31
|
+
from datetime import UTC, datetime
|
|
32
|
+
|
|
33
|
+
from sqlalchemy import create_engine, select
|
|
34
|
+
from sqlalchemy.exc import DatabaseError
|
|
35
|
+
from sqlalchemy.orm import Session
|
|
36
|
+
from sqlalchemy.orm.exc import StaleDataError
|
|
37
|
+
|
|
38
|
+
from app.core.audit.types import AuditAuthMethod, AuditSeverity
|
|
39
|
+
from app.core.auth.dashboard_access import PRESET_ROLE_IDS, PresetRoleSlug
|
|
40
|
+
from app.core.config.settings import get_settings
|
|
41
|
+
from app.db.migration_url import to_sync_database_url
|
|
42
|
+
from app.db.models import (
|
|
43
|
+
AuditLog,
|
|
44
|
+
AuthProviderKind,
|
|
45
|
+
DashboardAuthProvider,
|
|
46
|
+
DashboardSettings,
|
|
47
|
+
DashboardUser,
|
|
48
|
+
DashboardUserStatus,
|
|
49
|
+
LocalLoginPolicy,
|
|
50
|
+
)
|
|
51
|
+
from app.modules.dashboard_users.break_glass import local_login_admits
|
|
52
|
+
from app.modules.dashboard_users.repository import normalize_username
|
|
53
|
+
|
|
54
|
+
__all__ = ["BREAK_GLASS_CLI_ACTION", "run_admin_command"]
|
|
55
|
+
|
|
56
|
+
#: The one audit action every host recovery command writes.
|
|
57
|
+
BREAK_GLASS_CLI_ACTION = "break_glass_cli_used"
|
|
58
|
+
|
|
59
|
+
# Mirrors ``app/db/session.py``: sqlite3's own 5 s default would let a recovery
|
|
60
|
+
# command lose the writer race to the running server during exactly the
|
|
61
|
+
# incident it exists for.
|
|
62
|
+
_SQLITE_BUSY_TIMEOUT_SECONDS = 30
|
|
63
|
+
|
|
64
|
+
# bcrypt hashes at most 72 bytes and raises past that instead of truncating.
|
|
65
|
+
_MAX_PASSWORD_BYTES = 72
|
|
66
|
+
|
|
67
|
+
_SCHEMA_MISSING_HINT = (
|
|
68
|
+
"This database has no codex-lb schema yet. Run `codex-lb-db upgrade` (or start the server once) and try again."
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
_CACHE_CONVERGENCE = "a running server picks this up within about five seconds; no restart needed"
|
|
72
|
+
|
|
73
|
+
#: The policy values that can refuse an account the local password form. Both
|
|
74
|
+
#: require the admin preset, which is what lets the reachability check narrow
|
|
75
|
+
#: its query; a value added later must be added here deliberately.
|
|
76
|
+
_RESTRICTED_POLICIES = frozenset({LocalLoginPolicy.ADMINS_ONLY.value, LocalLoginPolicy.BREAK_GLASS_ONLY.value})
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True, slots=True)
|
|
80
|
+
class _Report:
|
|
81
|
+
"""What a command did, in the operator's words."""
|
|
82
|
+
|
|
83
|
+
title: str
|
|
84
|
+
facts: tuple[tuple[str, str], ...]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def run_admin_command(args: argparse.Namespace) -> None:
|
|
88
|
+
"""Dispatch one ``codex-lb admin`` sub-command and print what it did."""
|
|
89
|
+
|
|
90
|
+
command = getattr(args, "admin_command", None)
|
|
91
|
+
if command == "reset-password":
|
|
92
|
+
report = _reset_password(args.username, clear_two_factor=bool(args.clear_two_factor))
|
|
93
|
+
elif command == "local-login":
|
|
94
|
+
if getattr(args, "admin_local_login_command", None) != "enable":
|
|
95
|
+
raise SystemExit("admin local-login requires a subcommand: enable")
|
|
96
|
+
report = _enable_local_login()
|
|
97
|
+
elif command == "disable-provider":
|
|
98
|
+
report = _disable_provider(args.provider_id)
|
|
99
|
+
elif command == "reset-login-policy":
|
|
100
|
+
_confirm_reset_login_policy(bool(args.yes))
|
|
101
|
+
report = _reset_login_policy()
|
|
102
|
+
else:
|
|
103
|
+
raise SystemExit(
|
|
104
|
+
"admin requires a subcommand: reset-password, local-login enable, disable-provider, reset-login-policy"
|
|
105
|
+
)
|
|
106
|
+
_print_report(report)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# --- the commands ---
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _reset_password(username: str, *, clear_two_factor: bool = False) -> _Report:
|
|
113
|
+
"""Set a new password for one account, revoke its sessions, optionally drop its second factor.
|
|
114
|
+
|
|
115
|
+
The password is read from the terminal and never from ``argv``: a recovery
|
|
116
|
+
password on the command line survives in the shell history, in ``ps`` and
|
|
117
|
+
in whatever ships the host's logs.
|
|
118
|
+
|
|
119
|
+
``--clear-two-factor`` exists because a break-glass account that holds a
|
|
120
|
+
secret must always present it, whatever the toggles say (PLAN §4.2). That
|
|
121
|
+
rule is what makes the emergency account trustworthy — and it is also the
|
|
122
|
+
one way a lost authenticator turns into a permanent lockout when the
|
|
123
|
+
designated admin is the only one left. Clearing the secret from the host
|
|
124
|
+
is the documented way out; the install-wide requirement is left alone, so
|
|
125
|
+
an install that requires two-factor sends the account straight back to
|
|
126
|
+
enrolment on its next sign-in.
|
|
127
|
+
|
|
128
|
+
Clearing the secret is also what can take the account's *qualification*
|
|
129
|
+
away, and under ``break_glass_only`` a designation that no longer qualifies
|
|
130
|
+
is not admitted: the command would hand the operator a password for a form
|
|
131
|
+
that refuses it. So the write is followed by the one question that matters
|
|
132
|
+
— can anybody still use the local password form? — and when the answer is
|
|
133
|
+
no, local sign-in is re-opened in the same transaction. A recovery command
|
|
134
|
+
that can leave the install unreachable is not a recovery command; a policy
|
|
135
|
+
that still has a working door is never touched.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
normalized = normalize_username(username)
|
|
139
|
+
password_hash = _hash_password(_prompt_new_password())
|
|
140
|
+
|
|
141
|
+
with _transaction() as session:
|
|
142
|
+
user = session.execute(select(DashboardUser).where(DashboardUser.username == normalized)).scalar_one_or_none()
|
|
143
|
+
if user is None:
|
|
144
|
+
raise SystemExit(f"No account named {normalized!r} exists in this database.")
|
|
145
|
+
was_enrolled = user.totp_secret_encrypted is not None
|
|
146
|
+
user.password_hash = password_hash
|
|
147
|
+
# Every cookie the account holds was minted against the old
|
|
148
|
+
# credential; whoever locked it out may be holding one.
|
|
149
|
+
user.session_generation += 1
|
|
150
|
+
if clear_two_factor:
|
|
151
|
+
user.totp_secret_encrypted = None
|
|
152
|
+
user.totp_last_verified_step = None
|
|
153
|
+
settings_row = _settings_row(session)
|
|
154
|
+
if settings_row is not None:
|
|
155
|
+
# An account holds a password again, so the remote bootstrap token
|
|
156
|
+
# must not: it grants first-run admin access and would otherwise
|
|
157
|
+
# outlive the credential the operator just recovered. Decided by
|
|
158
|
+
# the operation, never by which account was reset.
|
|
159
|
+
settings_row.bootstrap_token_encrypted = None
|
|
160
|
+
settings_row.bootstrap_token_hash = None
|
|
161
|
+
reopened = _reopen_local_login_if_unreachable(session, settings_row)
|
|
162
|
+
_audit(
|
|
163
|
+
session,
|
|
164
|
+
"reset-password",
|
|
165
|
+
target_type="user",
|
|
166
|
+
target_id=user.id,
|
|
167
|
+
username=user.username,
|
|
168
|
+
two_factor="cleared" if clear_two_factor else "kept",
|
|
169
|
+
**({"local_login_reopened_from": reopened} if reopened is not None else {}),
|
|
170
|
+
)
|
|
171
|
+
facts = (
|
|
172
|
+
("Account", user.username),
|
|
173
|
+
("Status", _status_fact(user.status)),
|
|
174
|
+
("Two-factor", _two_factor_fact(was_enrolled=was_enrolled, cleared=clear_two_factor)),
|
|
175
|
+
("Existing sessions", "revoked"),
|
|
176
|
+
)
|
|
177
|
+
if reopened is not None:
|
|
178
|
+
facts += (
|
|
179
|
+
(
|
|
180
|
+
"Local sign-in policy",
|
|
181
|
+
f"{reopened} -> {LocalLoginPolicy.ENABLED.value} "
|
|
182
|
+
"(re-opened: no account was left that the policy would let in)",
|
|
183
|
+
),
|
|
184
|
+
("Running servers", _CACHE_CONVERGENCE),
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
return _Report("Password reset", facts)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _reopen_local_login_if_unreachable(session: Session, settings_row: DashboardSettings | None) -> str | None:
|
|
191
|
+
"""Put the policy back to ``enabled`` when no account can use the local form; returns the old value.
|
|
192
|
+
|
|
193
|
+
The question is asked of the state this transaction is about to commit, so
|
|
194
|
+
the flush the ``SELECT`` triggers is what makes the answer current. Only a
|
|
195
|
+
restricted policy can produce a zero, and only a zero re-opens: an install
|
|
196
|
+
that still has one admitted account keeps the door it chose to close.
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
if settings_row is None:
|
|
200
|
+
return None
|
|
201
|
+
policy = settings_row.local_login_policy
|
|
202
|
+
if not _policy_admits_nobody(session, policy):
|
|
203
|
+
return None
|
|
204
|
+
settings_row.local_login_policy = LocalLoginPolicy.ENABLED.value
|
|
205
|
+
return policy
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _policy_admits_nobody(session: Session, policy: str) -> bool:
|
|
209
|
+
"""Whether ``policy`` leaves no account that could sign in through the local form.
|
|
210
|
+
|
|
211
|
+
``enabled`` admits every active password account, so it can never be the
|
|
212
|
+
thing standing in the way. The two restricted values both require the admin
|
|
213
|
+
preset, which narrows the rows this has to look at to a handful; the
|
|
214
|
+
admission rule itself is the same :func:`local_login_admits` the login path
|
|
215
|
+
reads, so the two can never drift. A value this build does not know is
|
|
216
|
+
never re-opened from here — silently relaxing a policy nobody can reason
|
|
217
|
+
about is worse than the lockout it might be.
|
|
218
|
+
"""
|
|
219
|
+
|
|
220
|
+
if policy not in _RESTRICTED_POLICIES:
|
|
221
|
+
return False
|
|
222
|
+
candidates = (
|
|
223
|
+
session.execute(
|
|
224
|
+
select(DashboardUser).where(
|
|
225
|
+
DashboardUser.status == DashboardUserStatus.ACTIVE.value,
|
|
226
|
+
DashboardUser.password_hash.is_not(None),
|
|
227
|
+
DashboardUser.role_id == PRESET_ROLE_IDS[PresetRoleSlug.ADMIN],
|
|
228
|
+
)
|
|
229
|
+
)
|
|
230
|
+
.scalars()
|
|
231
|
+
.all()
|
|
232
|
+
)
|
|
233
|
+
return not any(local_login_admits(candidate, policy) for candidate in candidates)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _status_fact(status: str) -> str:
|
|
237
|
+
"""The account's status, and what it means for the password just handed out.
|
|
238
|
+
|
|
239
|
+
A password is only a way in for an *active* account: the login form refuses
|
|
240
|
+
every other status before it ever looks at a hash. The command still does
|
|
241
|
+
what it was asked -- re-enabling somebody an administrator turned off is a
|
|
242
|
+
decision for an administrator, not for a recovery command -- but it must
|
|
243
|
+
not let the operator walk away believing the door is open.
|
|
244
|
+
"""
|
|
245
|
+
|
|
246
|
+
if status == DashboardUserStatus.ACTIVE.value:
|
|
247
|
+
return status
|
|
248
|
+
return f"{status} — this account cannot sign in until an administrator re-enables it"
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _two_factor_fact(*, was_enrolled: bool, cleared: bool) -> str:
|
|
252
|
+
if cleared:
|
|
253
|
+
return "cleared — enrol again from Settings -> Access -> My sign-in" if was_enrolled else "not enrolled"
|
|
254
|
+
return "enrolled (unchanged)" if was_enrolled else "not enrolled"
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _enable_local_login() -> _Report:
|
|
258
|
+
"""Put ``local_login_policy`` back to ``enabled`` with no qualifying check."""
|
|
259
|
+
|
|
260
|
+
with _transaction() as session:
|
|
261
|
+
settings_row = _settings_row(session)
|
|
262
|
+
previous = LocalLoginPolicy.ENABLED.value if settings_row is None else settings_row.local_login_policy
|
|
263
|
+
if settings_row is not None:
|
|
264
|
+
settings_row.local_login_policy = LocalLoginPolicy.ENABLED.value
|
|
265
|
+
_audit(
|
|
266
|
+
session,
|
|
267
|
+
"local-login enable",
|
|
268
|
+
target_type="settings",
|
|
269
|
+
target_id="local_login_policy",
|
|
270
|
+
**{"from": previous, "to": LocalLoginPolicy.ENABLED.value},
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
return _Report(
|
|
274
|
+
"Local sign-in re-opened",
|
|
275
|
+
(
|
|
276
|
+
("Local sign-in policy", f"{previous} -> {LocalLoginPolicy.ENABLED.value}"),
|
|
277
|
+
("Effect", "every active account that holds a password may sign in again"),
|
|
278
|
+
("Running servers", _CACHE_CONVERGENCE),
|
|
279
|
+
),
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _disable_provider(provider_id: str) -> _Report:
|
|
284
|
+
"""Turn one sign-in provider off so it stops claiming the login screen."""
|
|
285
|
+
|
|
286
|
+
with _transaction() as session:
|
|
287
|
+
provider = session.get(DashboardAuthProvider, provider_id)
|
|
288
|
+
if provider is None:
|
|
289
|
+
raise SystemExit(f"No sign-in provider {provider_id!r} exists in this database.{_known_providers(session)}")
|
|
290
|
+
if provider.kind == AuthProviderKind.PASSWORD.value:
|
|
291
|
+
raise SystemExit(
|
|
292
|
+
"That is the local password provider. Disabling it would remove the recovery path itself; "
|
|
293
|
+
"use `codex-lb admin local-login enable` to re-open local sign-in instead."
|
|
294
|
+
)
|
|
295
|
+
was_enabled = provider.enabled
|
|
296
|
+
provider.enabled = False
|
|
297
|
+
_audit(
|
|
298
|
+
session,
|
|
299
|
+
"disable-provider",
|
|
300
|
+
target_type="auth_provider",
|
|
301
|
+
target_id=provider.id,
|
|
302
|
+
kind=provider.kind,
|
|
303
|
+
provider_key=provider.provider_key,
|
|
304
|
+
)
|
|
305
|
+
facts = (
|
|
306
|
+
("Provider", f"{provider.label} ({provider.kind})"),
|
|
307
|
+
("Id", provider.id),
|
|
308
|
+
("Enabled", "true -> false" if was_enabled else "already false"),
|
|
309
|
+
("Running servers", _CACHE_CONVERGENCE),
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
return _Report("Sign-in provider disabled", facts)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _reset_login_policy() -> _Report:
|
|
316
|
+
"""The big hammer: local sign-in back to open, every company provider off.
|
|
317
|
+
|
|
318
|
+
``local-login enable`` alone is not enough when the provider is the thing
|
|
319
|
+
that is broken — an enabled trusted-header provider still resolves (and
|
|
320
|
+
refuses) the identity on every request, so the local session it pre-empts
|
|
321
|
+
never gets a chance. This command undoes the whole tightening in one step
|
|
322
|
+
and leaves the password provider, which is the way back in.
|
|
323
|
+
"""
|
|
324
|
+
|
|
325
|
+
with _transaction() as session:
|
|
326
|
+
settings_row = _settings_row(session)
|
|
327
|
+
previous = LocalLoginPolicy.ENABLED.value if settings_row is None else settings_row.local_login_policy
|
|
328
|
+
if settings_row is not None:
|
|
329
|
+
settings_row.local_login_policy = LocalLoginPolicy.ENABLED.value
|
|
330
|
+
disabled: list[str] = []
|
|
331
|
+
providers = session.execute(select(DashboardAuthProvider)).scalars().all()
|
|
332
|
+
for provider in providers:
|
|
333
|
+
if provider.kind == AuthProviderKind.PASSWORD.value or not provider.enabled:
|
|
334
|
+
continue
|
|
335
|
+
provider.enabled = False
|
|
336
|
+
disabled.append(f"{provider.label} ({provider.kind})")
|
|
337
|
+
_audit(
|
|
338
|
+
session,
|
|
339
|
+
"reset-login-policy",
|
|
340
|
+
target_type="settings",
|
|
341
|
+
target_id="local_login_policy",
|
|
342
|
+
providers_disabled=len(disabled),
|
|
343
|
+
**{"from": previous, "to": LocalLoginPolicy.ENABLED.value},
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
return _Report(
|
|
347
|
+
"Sign-in policy reset",
|
|
348
|
+
(
|
|
349
|
+
("Local sign-in policy", f"{previous} -> {LocalLoginPolicy.ENABLED.value}"),
|
|
350
|
+
("Providers disabled", ", ".join(disabled) if disabled else "none"),
|
|
351
|
+
("Kept", "the local password provider"),
|
|
352
|
+
("Running servers", _CACHE_CONVERGENCE),
|
|
353
|
+
),
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
# --- plumbing ---
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
@contextmanager
|
|
361
|
+
def _transaction() -> Iterator[Session]:
|
|
362
|
+
"""One engine, one transaction, disposed on the way out (as ``codex-lb-db`` does)."""
|
|
363
|
+
|
|
364
|
+
url = to_sync_database_url(get_settings().database_url)
|
|
365
|
+
connect_args: dict[str, object] = {"timeout": _SQLITE_BUSY_TIMEOUT_SECONDS} if url.startswith("sqlite") else {}
|
|
366
|
+
engine = create_engine(url, future=True, connect_args=connect_args)
|
|
367
|
+
try:
|
|
368
|
+
with Session(engine) as session, session.begin():
|
|
369
|
+
yield session
|
|
370
|
+
except StaleDataError as exc:
|
|
371
|
+
# The settings row carries an optimistic version and the running
|
|
372
|
+
# server writes it too. Losing that race costs nothing: nothing was
|
|
373
|
+
# applied, and these commands are safe to repeat.
|
|
374
|
+
raise SystemExit(
|
|
375
|
+
"The dashboard changed the same row while this ran. Nothing was written; run it again."
|
|
376
|
+
) from exc
|
|
377
|
+
except DatabaseError as exc:
|
|
378
|
+
raise SystemExit(_database_error_message(exc)) from exc
|
|
379
|
+
finally:
|
|
380
|
+
engine.dispose()
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _database_error_message(exc: DatabaseError) -> str:
|
|
384
|
+
"""A never-migrated database is the likely case; say so instead of a traceback."""
|
|
385
|
+
|
|
386
|
+
message = str(exc)
|
|
387
|
+
lowered = message.casefold()
|
|
388
|
+
if "no such table" in lowered or "does not exist" in lowered or "undefinedtable" in lowered:
|
|
389
|
+
return _SCHEMA_MISSING_HINT
|
|
390
|
+
if "locked" in lowered:
|
|
391
|
+
return (
|
|
392
|
+
f"{message}\nThe database stayed locked for {_SQLITE_BUSY_TIMEOUT_SECONDS}s. "
|
|
393
|
+
"Something is holding a long write; stop the server and run this again."
|
|
394
|
+
)
|
|
395
|
+
return f"Database error: {message}"
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _settings_row(session: Session) -> DashboardSettings | None:
|
|
399
|
+
"""The settings singleton, or ``None`` on an install that never wrote one.
|
|
400
|
+
|
|
401
|
+
A missing row is not an error here: the column default is ``enabled``, so
|
|
402
|
+
"local sign-in is open afterwards" already holds.
|
|
403
|
+
"""
|
|
404
|
+
|
|
405
|
+
return session.execute(select(DashboardSettings).limit(1)).scalar_one_or_none()
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _known_providers(session: Session) -> str:
|
|
409
|
+
providers = session.execute(select(DashboardAuthProvider)).scalars().all()
|
|
410
|
+
if not providers:
|
|
411
|
+
return ""
|
|
412
|
+
known = "\n".join(
|
|
413
|
+
f" {provider.id} {provider.kind} {'enabled' if provider.enabled else 'disabled'}" for provider in providers
|
|
414
|
+
)
|
|
415
|
+
return f"\nSign-in providers in this database:\n{known}"
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _audit(
|
|
419
|
+
session: Session,
|
|
420
|
+
command: str,
|
|
421
|
+
*,
|
|
422
|
+
target_type: str,
|
|
423
|
+
target_id: str,
|
|
424
|
+
**details: str | int,
|
|
425
|
+
) -> None:
|
|
426
|
+
"""One critical row per run, in the same transaction as the change it describes.
|
|
427
|
+
|
|
428
|
+
``AuditService.log_async`` wants a running loop and drops events once
|
|
429
|
+
shutdown admission closes, so the row is inserted directly. The trade-off
|
|
430
|
+
is the sink fan-out Phase 4 adds — which a host command could not reach in
|
|
431
|
+
the first place — against an append-only record whose whole point is the
|
|
432
|
+
database it is written to. The actor columns stay NULL: nobody
|
|
433
|
+
authenticated, and inventing an actor would be a lie in the one place that
|
|
434
|
+
must not hold any.
|
|
435
|
+
"""
|
|
436
|
+
|
|
437
|
+
session.add(
|
|
438
|
+
AuditLog(
|
|
439
|
+
timestamp=datetime.now(UTC),
|
|
440
|
+
action=BREAK_GLASS_CLI_ACTION,
|
|
441
|
+
details=json.dumps({"command": command, **details}),
|
|
442
|
+
auth_method=AuditAuthMethod.CLI.value,
|
|
443
|
+
target_type=target_type,
|
|
444
|
+
target_id=target_id,
|
|
445
|
+
severity=AuditSeverity.CRITICAL.value,
|
|
446
|
+
)
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def _prompt_new_password() -> str:
|
|
451
|
+
if not sys.stdin.isatty():
|
|
452
|
+
raise SystemExit(
|
|
453
|
+
"Run `codex-lb admin reset-password` from a terminal (docker exec -it ...): "
|
|
454
|
+
"the new password is prompted for, never taken from the command line."
|
|
455
|
+
)
|
|
456
|
+
password = getpass.getpass("New password: ")
|
|
457
|
+
if password == "":
|
|
458
|
+
raise SystemExit("Aborted: the new password is empty.")
|
|
459
|
+
if len(password.encode("utf-8")) > _MAX_PASSWORD_BYTES:
|
|
460
|
+
raise SystemExit(f"Aborted: the new password is longer than {_MAX_PASSWORD_BYTES} bytes.")
|
|
461
|
+
if getpass.getpass("Repeat new password: ") != password:
|
|
462
|
+
raise SystemExit("Aborted: the two passwords do not match.")
|
|
463
|
+
return password
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _confirm_reset_login_policy(yes: bool) -> None:
|
|
467
|
+
warning = (
|
|
468
|
+
"This re-opens local password sign-in and disables every company sign-in provider.\n"
|
|
469
|
+
"People who sign in through the identity provider will have to use a local password until you turn it "
|
|
470
|
+
"back on in the dashboard."
|
|
471
|
+
)
|
|
472
|
+
print(warning, file=sys.stderr)
|
|
473
|
+
if yes:
|
|
474
|
+
return
|
|
475
|
+
if not sys.stdin.isatty():
|
|
476
|
+
raise SystemExit("Refusing to write without --yes in a non-interactive shell.")
|
|
477
|
+
if input("Continue? [y/N] ").strip().casefold() not in {"y", "yes"}:
|
|
478
|
+
raise SystemExit("Aborted.")
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def _hash_password(password: str) -> str:
|
|
482
|
+
"""The one hashing function the sign-in path uses; imported late to keep the CLI light."""
|
|
483
|
+
|
|
484
|
+
from app.modules.dashboard_auth.service import hash_password
|
|
485
|
+
|
|
486
|
+
return hash_password(password)
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _print_report(report: _Report) -> None:
|
|
490
|
+
print("")
|
|
491
|
+
print(report.title)
|
|
492
|
+
for label, value in report.facts:
|
|
493
|
+
print(f"- {label}: {value}")
|
|
494
|
+
print(f"- Audited: {BREAK_GLASS_CLI_ACTION} (critical)")
|