alysis-code 0.13.0__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.
- alysis_code/__init__.py +3 -0
- alysis_code/__main__.py +4 -0
- alysis_code/_build_info.py +14 -0
- alysis_code/account_login.py +468 -0
- alysis_code/agent/README.md +35 -0
- alysis_code/agent/__init__.py +11 -0
- alysis_code/agent/acceptance_contract.py +2217 -0
- alysis_code/agent/blast_radius.py +1403 -0
- alysis_code/agent/cache_keepalive.py +227 -0
- alysis_code/agent/completion_certificate.py +366 -0
- alysis_code/agent/completion_gate.py +306 -0
- alysis_code/agent/empty_response_stall.py +403 -0
- alysis_code/agent/errors.py +28 -0
- alysis_code/agent/llm_calls.py +475 -0
- alysis_code/agent/mutation_classification.py +227 -0
- alysis_code/agent/prompt_context.py +2508 -0
- alysis_code/agent/read_ledger.py +253 -0
- alysis_code/agent/regression_baseline.py +642 -0
- alysis_code/agent/reproduction_first.py +610 -0
- alysis_code/agent/sensitive_output.py +629 -0
- alysis_code/agent/session.py +3218 -0
- alysis_code/agent/steering.py +191 -0
- alysis_code/agent/subagent_execution.py +5177 -0
- alysis_code/agent/subagent_workspace.py +666 -0
- alysis_code/agent/tools_assembly.py +4728 -0
- alysis_code/agent/turn/__init__.py +101 -0
- alysis_code/agent/turn/core.py +8483 -0
- alysis_code/agent/turn/events.py +113 -0
- alysis_code/agent/turn/exploration.py +590 -0
- alysis_code/agent/turn/interventions.py +65 -0
- alysis_code/agent/turn/read_cache.py +420 -0
- alysis_code/agent/turn/snapshot.py +179 -0
- alysis_code/agent/turn_contract.py +661 -0
- alysis_code/agent/turn_path.py +129 -0
- alysis_code/agent/verification.py +2885 -0
- alysis_code/agent/verification_commands.py +512 -0
- alysis_code/agent/verification_evidence.py +738 -0
- alysis_code/agent_loop.py +693 -0
- alysis_code/agent_runtimes/__init__.py +51 -0
- alysis_code/agent_runtimes/base.py +114 -0
- alysis_code/agent_runtimes/builtins.py +129 -0
- alysis_code/agent_runtimes/codex_cli.py +664 -0
- alysis_code/agent_runtimes/host.py +263 -0
- alysis_code/agent_runtimes/registry.py +64 -0
- alysis_code/agent_runtimes/service.py +150 -0
- alysis_code/agentbox_client.py +416 -0
- alysis_code/agentbox_integration.py +310 -0
- alysis_code/alysis_cloud.py +152 -0
- alysis_code/approval_scope.py +276 -0
- alysis_code/assets/README.md +33 -0
- alysis_code/assets/__init__.py +126 -0
- alysis_code/assets/asset_read_core.py +281 -0
- alysis_code/assets/budget_allocator.py +456 -0
- alysis_code/assets/comprehender.py +759 -0
- alysis_code/assets/index.py +654 -0
- alysis_code/assets/ingestion.py +275 -0
- alysis_code/assets/legacy_migration.py +413 -0
- alysis_code/assets/models.py +263 -0
- alysis_code/assets/ocr.py +239 -0
- alysis_code/assets/owl/ascii/f-000.txt +13 -0
- alysis_code/assets/owl/ascii/f-001.txt +13 -0
- alysis_code/assets/owl/ascii/f-002.txt +13 -0
- alysis_code/assets/owl/ascii/f-003.txt +13 -0
- alysis_code/assets/owl/ascii/f-004.txt +13 -0
- alysis_code/assets/owl/ascii/f-005.txt +13 -0
- alysis_code/assets/owl/ascii/f-006.txt +13 -0
- alysis_code/assets/owl/ascii/f-007.txt +13 -0
- alysis_code/assets/owl/ascii/f-008.txt +13 -0
- alysis_code/assets/owl/ascii/f-009.txt +13 -0
- alysis_code/assets/owl/ascii/f-010.txt +13 -0
- alysis_code/assets/owl/ascii/f-011.txt +13 -0
- alysis_code/assets/owl/ascii/f-012.txt +13 -0
- alysis_code/assets/owl/ascii/f-013.txt +13 -0
- alysis_code/assets/owl/ascii/f-014.txt +13 -0
- alysis_code/assets/owl/ascii/f-015.txt +13 -0
- alysis_code/assets/owl/ascii/f-016.txt +13 -0
- alysis_code/assets/owl/ascii/f-017.txt +13 -0
- alysis_code/assets/owl/ascii/f-018.txt +13 -0
- alysis_code/assets/owl/ascii/f-019.txt +13 -0
- alysis_code/assets/owl/ascii/f-020.txt +13 -0
- alysis_code/assets/owl/index.html +98 -0
- alysis_code/assets/owl/show-owl.sh +761 -0
- alysis_code/assets/paths.py +49 -0
- alysis_code/assets/plan_binding.py +326 -0
- alysis_code/assets/planner_context.py +466 -0
- alysis_code/assets/planner_tools.py +184 -0
- alysis_code/assets/prompts.py +101 -0
- alysis_code/assets/replanner_context.py +239 -0
- alysis_code/assets/surface.py +521 -0
- alysis_code/assets/untrusted_content.py +48 -0
- alysis_code/assets/usage_logger.py +94 -0
- alysis_code/assets/worker_mirror.py +428 -0
- alysis_code/assets/worker_section.py +303 -0
- alysis_code/assets/worker_tools.py +468 -0
- alysis_code/atomic_io.py +83 -0
- alysis_code/auth_diagnostics.py +272 -0
- alysis_code/background_runner.py +366 -0
- alysis_code/branding.py +270 -0
- alysis_code/budget_policy.py +390 -0
- alysis_code/build_identity.py +465 -0
- alysis_code/builtin_hooks/__init__.py +7 -0
- alysis_code/builtin_hooks/notify_done_windows.py +65 -0
- alysis_code/bwrap_etc.py +76 -0
- alysis_code/cancellation.py +41 -0
- alysis_code/capabilities.py +137 -0
- alysis_code/chatgpt_codex_static_provider.py +133 -0
- alysis_code/cli.py +51 -0
- alysis_code/cli_impl/__init__.py +1 -0
- alysis_code/cli_impl/assets_cli.py +537 -0
- alysis_code/cli_impl/assets_modal.py +412 -0
- alysis_code/cli_impl/chat/__init__.py +156 -0
- alysis_code/cli_impl/chat/commands.py +2616 -0
- alysis_code/cli_impl/chat/loop.py +4508 -0
- alysis_code/cli_impl/chat/mid_turn_policy.py +125 -0
- alysis_code/cli_impl/chat/rendering.py +444 -0
- alysis_code/cli_impl/chat/state.py +124 -0
- alysis_code/cli_impl/chat_resume.py +830 -0
- alysis_code/cli_impl/chat_slash_completer.py +258 -0
- alysis_code/cli_impl/commands/__init__.py +11 -0
- alysis_code/cli_impl/commands/_shared.py +89 -0
- alysis_code/cli_impl/commands/auth.py +623 -0
- alysis_code/cli_impl/commands/chat_resume_helpers.py +1531 -0
- alysis_code/cli_impl/commands/chat_state.py +158 -0
- alysis_code/cli_impl/commands/chat_status.py +1248 -0
- alysis_code/cli_impl/commands/chat_terminal.py +942 -0
- alysis_code/cli_impl/commands/chat_tui_panels.py +1018 -0
- alysis_code/cli_impl/commands/cli_common.py +1223 -0
- alysis_code/cli_impl/commands/cli_surface.py +77 -0
- alysis_code/cli_impl/commands/config.py +131 -0
- alysis_code/cli_impl/commands/conventions.py +85 -0
- alysis_code/cli_impl/commands/execution_helpers.py +350 -0
- alysis_code/cli_impl/commands/extensions.py +401 -0
- alysis_code/cli_impl/commands/forge.py +1282 -0
- alysis_code/cli_impl/commands/forge_asset_view.py +121 -0
- alysis_code/cli_impl/commands/forge_helpers.py +1215 -0
- alysis_code/cli_impl/commands/hooks.py +737 -0
- alysis_code/cli_impl/commands/ide_bridge.py +31 -0
- alysis_code/cli_impl/commands/mcp.py +700 -0
- alysis_code/cli_impl/commands/profile.py +453 -0
- alysis_code/cli_impl/commands/prompt_helpers.py +307 -0
- alysis_code/cli_impl/commands/report.py +88 -0
- alysis_code/cli_impl/commands/root.py +1118 -0
- alysis_code/cli_impl/commands/sandbox.py +184 -0
- alysis_code/cli_impl/commands/server.py +54 -0
- alysis_code/cli_impl/commands/sessions.py +252 -0
- alysis_code/cli_impl/commands/skills.py +404 -0
- alysis_code/cli_impl/commands/startup.py +946 -0
- alysis_code/cli_impl/commands/tools.py +335 -0
- alysis_code/cli_impl/commands/update.py +364 -0
- alysis_code/cli_impl/commands/welcome.py +972 -0
- alysis_code/cli_impl/config_menu.py +3882 -0
- alysis_code/cli_impl/forge.py +4509 -0
- alysis_code/cli_impl/forge_recovery.py +485 -0
- alysis_code/cli_impl/setup_wizard.py +2409 -0
- alysis_code/cli_impl/tui/__init__.py +58 -0
- alysis_code/cli_impl/tui/app.py +4551 -0
- alysis_code/cli_impl/tui/config.py +32 -0
- alysis_code/cli_impl/tui/config_flow.py +2754 -0
- alysis_code/cli_impl/tui/config_overlay.py +566 -0
- alysis_code/cli_impl/tui/content.py +78 -0
- alysis_code/cli_impl/tui/footer.py +218 -0
- alysis_code/cli_impl/tui/forge_status.py +136 -0
- alysis_code/cli_impl/tui/markdown.py +244 -0
- alysis_code/cli_impl/tui/owl.py +109 -0
- alysis_code/cli_impl/tui/plan_meta.py +477 -0
- alysis_code/cli_impl/tui/setup_app.py +519 -0
- alysis_code/cli_impl/tui/setup_flow.py +1622 -0
- alysis_code/cli_impl/tui/state.py +101 -0
- alysis_code/cli_impl/tui/subagent_identity.py +66 -0
- alysis_code/cli_impl/tui/subagent_panel.py +186 -0
- alysis_code/cli_impl/tui/surface.py +796 -0
- alysis_code/cli_impl/tui/transcript.py +514 -0
- alysis_code/cli_impl/tui/update_prompt.py +79 -0
- alysis_code/cli_impl/tui/workspace_guard.py +384 -0
- alysis_code/clipboard.py +172 -0
- alysis_code/code_review.py +1211 -0
- alysis_code/compaction/__init__.py +28 -0
- alysis_code/compaction/conversation_compactor.py +2932 -0
- alysis_code/compaction/importance.py +177 -0
- alysis_code/compaction/settings.py +297 -0
- alysis_code/compaction/tool_output_offload.py +447 -0
- alysis_code/config.py +3509 -0
- alysis_code/conflict_auto_resolver.py +895 -0
- alysis_code/context/__init__.py +1 -0
- alysis_code/context/tool_schema_budgeter.py +220 -0
- alysis_code/crash_diagnostics.py +282 -0
- alysis_code/custom_tools/README.md +34 -0
- alysis_code/custom_tools/__init__.py +43 -0
- alysis_code/custom_tools/discovery.py +903 -0
- alysis_code/custom_tools/runtime.py +1516 -0
- alysis_code/custom_tools/session.py +227 -0
- alysis_code/custom_tools/trust.py +232 -0
- alysis_code/diff_paths.py +113 -0
- alysis_code/direction_change.py +293 -0
- alysis_code/dispatch_timing.py +306 -0
- alysis_code/durable_service_manager.py +1236 -0
- alysis_code/edit_discipline.py +659 -0
- alysis_code/error_text.py +73 -0
- alysis_code/execution_budget.py +411 -0
- alysis_code/execution_context.py +915 -0
- alysis_code/execution_deadline.py +1065 -0
- alysis_code/execution_shared.py +1904 -0
- alysis_code/extensions/README.md +30 -0
- alysis_code/extensions/__init__.py +93 -0
- alysis_code/extensions/activation.py +138 -0
- alysis_code/extensions/install.py +1436 -0
- alysis_code/extensions/manifest.py +487 -0
- alysis_code/extensions/models.py +74 -0
- alysis_code/extensions/paths.py +56 -0
- alysis_code/extensions/registry.json +4 -0
- alysis_code/extensions/registry.py +52 -0
- alysis_code/extensions/state.py +83 -0
- alysis_code/extensions/workspace_trust.py +101 -0
- alysis_code/failed_task_evidence.py +369 -0
- alysis_code/failure_category.py +315 -0
- alysis_code/feedback_report.py +1647 -0
- alysis_code/file_classification.py +485 -0
- alysis_code/forge.py +2064 -0
- alysis_code/forge_completion.py +362 -0
- alysis_code/forge_events.py +475 -0
- alysis_code/frontmatter_utils.py +95 -0
- alysis_code/git_evidence.py +1181 -0
- alysis_code/git_ops.py +560 -0
- alysis_code/git_safe.py +62 -0
- alysis_code/git_worktrees.py +190 -0
- alysis_code/hooks/README.md +33 -0
- alysis_code/hooks/__init__.py +67 -0
- alysis_code/hooks/audit.py +171 -0
- alysis_code/hooks/config.py +225 -0
- alysis_code/hooks/dispatcher.py +1110 -0
- alysis_code/hooks/models.py +447 -0
- alysis_code/hooks/trust.py +202 -0
- alysis_code/host_actions.py +543 -0
- alysis_code/host_browser.py +103 -0
- alysis_code/ide/__init__.py +5 -0
- alysis_code/ide/activity_events.py +399 -0
- alysis_code/ide/approvals.py +337 -0
- alysis_code/ide/artifacts.py +153 -0
- alysis_code/ide/browser_egress_proxy.py +1076 -0
- alysis_code/ide/cdp_websocket_transport.py +1192 -0
- alysis_code/ide/change_ledger.py +1721 -0
- alysis_code/ide/context_blocks.py +979 -0
- alysis_code/ide/event_stream.py +531 -0
- alysis_code/ide/forge_protocol.py +3112 -0
- alysis_code/ide/forge_request_ledger.py +737 -0
- alysis_code/ide/health.py +965 -0
- alysis_code/ide/managed_browser.py +2251 -0
- alysis_code/ide/management_protocol.py +3414 -0
- alysis_code/ide/mcp_oauth_coordinator.py +744 -0
- alysis_code/ide/mcp_oauth_lifecycle.py +1504 -0
- alysis_code/ide/prompt_queue.py +1070 -0
- alysis_code/ide/protocol.py +191 -0
- alysis_code/ide/resumable_swarm.py +1543 -0
- alysis_code/ide/session_search.py +295 -0
- alysis_code/ide/stdio_bridge.py +9935 -0
- alysis_code/ide/structured_state.py +1579 -0
- alysis_code/ide/swarm_protocol.py +816 -0
- alysis_code/integration_gate.py +506 -0
- alysis_code/interactive_input_guard.py +39 -0
- alysis_code/interactive_plan_mode.py +26 -0
- alysis_code/internal_artifacts.py +179 -0
- alysis_code/knowledge_base.py +1409 -0
- alysis_code/knowledge_capture.py +1190 -0
- alysis_code/knowledge_librarian.py +605 -0
- alysis_code/language_policy.py +34 -0
- alysis_code/litellm_static_provider.py +535 -0
- alysis_code/llm/__init__.py +1 -0
- alysis_code/llm/anthropic_messages.py +2288 -0
- alysis_code/llm/base.py +71 -0
- alysis_code/llm/cache_capabilities.py +985 -0
- alysis_code/llm/cache_control_blocks.py +244 -0
- alysis_code/llm/cache_policy.py +388 -0
- alysis_code/llm/factory.py +373 -0
- alysis_code/llm/gemini_generate_content.py +2652 -0
- alysis_code/llm/gemini_interactions.py +739 -0
- alysis_code/llm/metadata.py +450 -0
- alysis_code/llm/openai_compat.py +2947 -0
- alysis_code/llm/openai_responses.py +2604 -0
- alysis_code/llm/protocols.py +609 -0
- alysis_code/llm/provider_limits.py +525 -0
- alysis_code/llm/request_plan.py +389 -0
- alysis_code/llm/request_shape.py +238 -0
- alysis_code/llm/streaming.py +108 -0
- alysis_code/llm/temperature_compat.py +78 -0
- alysis_code/llm/types.py +195 -0
- alysis_code/llm/usage_normalization.py +222 -0
- alysis_code/llm_error_display.py +315 -0
- alysis_code/logging_redaction.py +326 -0
- alysis_code/managed_host_deadline.py +196 -0
- alysis_code/mcp/README.md +33 -0
- alysis_code/mcp/__init__.py +24 -0
- alysis_code/mcp/client.py +1137 -0
- alysis_code/mcp/config.py +597 -0
- alysis_code/mcp/errors.py +113 -0
- alysis_code/mcp/forge_scope.py +154 -0
- alysis_code/mcp/jsonrpc.py +214 -0
- alysis_code/mcp/manager.py +2308 -0
- alysis_code/mcp/models.py +666 -0
- alysis_code/mcp/oauth.py +972 -0
- alysis_code/mcp/oauth_runtime.py +310 -0
- alysis_code/mcp/oauth_store.py +276 -0
- alysis_code/mcp/prompts.py +329 -0
- alysis_code/mcp/resources.py +295 -0
- alysis_code/mcp/roots.py +106 -0
- alysis_code/mcp/server_requests.py +75 -0
- alysis_code/mcp/token_store.py +859 -0
- alysis_code/mcp/transport_http.py +1338 -0
- alysis_code/mcp/transport_stdio.py +1267 -0
- alysis_code/mcp/untrusted_content.py +119 -0
- alysis_code/merge_conflict_reviewer.py +729 -0
- alysis_code/model_catalog/__init__.py +1 -0
- alysis_code/model_catalog/chatgpt_codex_subscription_snapshot.json +186 -0
- alysis_code/model_catalog/litellm_model_prices_snapshot.json +44715 -0
- alysis_code/model_catalog/litellm_model_prices_snapshot.meta.json +17 -0
- alysis_code/model_metadata_policy.py +223 -0
- alysis_code/model_metadata_utils.py +103 -0
- alysis_code/model_registry.py +1420 -0
- alysis_code/model_router.py +147 -0
- alysis_code/permission_policy.py +1016 -0
- alysis_code/personas.py +451 -0
- alysis_code/pipeline_facts.py +233 -0
- alysis_code/plan_assistant.py +4763 -0
- alysis_code/plan_mode.py +393 -0
- alysis_code/plan_reconciliation.py +1228 -0
- alysis_code/plan_repair.py +652 -0
- alysis_code/plan_validation.py +1099 -0
- alysis_code/planning_constraints.py +904 -0
- alysis_code/policy.py +95 -0
- alysis_code/preview_server.py +457 -0
- alysis_code/process_reaping.py +566 -0
- alysis_code/profile_presets.py +1834 -0
- alysis_code/profiles.py +666 -0
- alysis_code/provider_auth/__init__.py +29 -0
- alysis_code/provider_auth/base.py +99 -0
- alysis_code/provider_auth/openai_codex.py +951 -0
- alysis_code/provider_auth/registry.py +76 -0
- alysis_code/provider_auth/store.py +125 -0
- alysis_code/provider_diagnostics.py +1209 -0
- alysis_code/provider_model_catalog.py +685 -0
- alysis_code/provider_telemetry.py +1699 -0
- alysis_code/provider_url.py +75 -0
- alysis_code/reasoning_contracts.py +911 -0
- alysis_code/remote_sync.py +350 -0
- alysis_code/replanning.py +1195 -0
- alysis_code/repo_scan.py +1152 -0
- alysis_code/request_estimation.py +296 -0
- alysis_code/review_gate.py +617 -0
- alysis_code/run_lock.py +1141 -0
- alysis_code/run_outcome.py +58 -0
- alysis_code/run_provenance.py +774 -0
- alysis_code/run_state.py +445 -0
- alysis_code/runtime_artifacts.py +116 -0
- alysis_code/runtime_context_features.py +78 -0
- alysis_code/runtime_kind.py +52 -0
- alysis_code/safety/__init__.py +11 -0
- alysis_code/safety/mcp_sanitize.py +29 -0
- alysis_code/safety/safe_http.py +297 -0
- alysis_code/safety/subagent_report.py +184 -0
- alysis_code/sandbox_doctor.py +682 -0
- alysis_code/sandbox_runner.py +1025 -0
- alysis_code/sandbox_settings.py +423 -0
- alysis_code/serialized_paths.py +355 -0
- alysis_code/server/__init__.py +3 -0
- alysis_code/server/app.py +367 -0
- alysis_code/server/auth.py +34 -0
- alysis_code/server/job_config.py +30 -0
- alysis_code/server/settings.py +215 -0
- alysis_code/server/store.py +193 -0
- alysis_code/server/worker_runner.py +657 -0
- alysis_code/service_persistence.py +355 -0
- alysis_code/session_artifacts.py +108 -0
- alysis_code/session_metrics.py +331 -0
- alysis_code/session_store.py +624 -0
- alysis_code/skills/README.md +34 -0
- alysis_code/skills/__init__.py +104 -0
- alysis_code/skills/conventions.py +84 -0
- alysis_code/skills/discovery.py +176 -0
- alysis_code/skills/eval_models.py +232 -0
- alysis_code/skills/eval_runner.py +372 -0
- alysis_code/skills/evals.py +1344 -0
- alysis_code/skills/install.py +293 -0
- alysis_code/skills/loader.py +118 -0
- alysis_code/skills/matching.py +103 -0
- alysis_code/skills/models.py +71 -0
- alysis_code/skills/paths.py +56 -0
- alysis_code/skills/prompting.py +500 -0
- alysis_code/skills/scaffold.py +142 -0
- alysis_code/skills/state.py +441 -0
- alysis_code/skills/transactions.py +125 -0
- alysis_code/skills/validation.py +304 -0
- alysis_code/step_budget.py +238 -0
- alysis_code/subagent_labels.py +49 -0
- alysis_code/subagents.py +1072 -0
- alysis_code/surface/__init__.py +80 -0
- alysis_code/surface/base.py +305 -0
- alysis_code/surface/console.py +387 -0
- alysis_code/surface/events.py +372 -0
- alysis_code/surface/hidden_surface.py +529 -0
- alysis_code/surface/noop_surface.py +219 -0
- alysis_code/surface/rich_surface.py +1555 -0
- alysis_code/surface/styles.py +67 -0
- alysis_code/surface/theme.py +455 -0
- alysis_code/surface/types.py +100 -0
- alysis_code/swarm_backend.py +926 -0
- alysis_code/swarm_orchestrator.py +4020 -0
- alysis_code/swarm_scheduler.py +441 -0
- alysis_code/swarm_trace.py +429 -0
- alysis_code/swarm_worker.py +2119 -0
- alysis_code/swarm_write_guard.py +348 -0
- alysis_code/task_dependencies.py +170 -0
- alysis_code/task_readiness.py +992 -0
- alysis_code/task_scope.py +2148 -0
- alysis_code/terminal_manager.py +762 -0
- alysis_code/terminal_ownership.py +460 -0
- alysis_code/text_normalization.py +30 -0
- alysis_code/token_budget.py +97 -0
- alysis_code/tools/README.md +34 -0
- alysis_code/tools/__init__.py +1 -0
- alysis_code/tools/artifacts.py +127 -0
- alysis_code/tools/availability.py +188 -0
- alysis_code/tools/fs.py +1456 -0
- alysis_code/tools/git.py +461 -0
- alysis_code/tools/history.py +229 -0
- alysis_code/tools/http_timeout.py +78 -0
- alysis_code/tools/image_generation.py +552 -0
- alysis_code/tools/registry.py +2936 -0
- alysis_code/tools/repo_map.py +476 -0
- alysis_code/tools/search.py +563 -0
- alysis_code/tools/shell.py +135 -0
- alysis_code/tools/symbols.py +1350 -0
- alysis_code/tools/test_discovery.py +643 -0
- alysis_code/tools/web.py +482 -0
- alysis_code/tools/web_search.py +2012 -0
- alysis_code/tools/web_search_dashscope.py +557 -0
- alysis_code/tools/web_search_ddgs.py +221 -0
- alysis_code/tools/web_search_provider_adapters.py +1429 -0
- alysis_code/tools/web_search_tavily.py +194 -0
- alysis_code/updates.py +933 -0
- alysis_code/usage_tracker.py +1990 -0
- alysis_code/verification_command_analysis.py +1004 -0
- alysis_code/verification_contract.py +574 -0
- alysis_code/verification_failure_summary.py +273 -0
- alysis_code/verification_repair.py +385 -0
- alysis_code/verify_gate.py +3129 -0
- alysis_code/web_research.py +1872 -0
- alysis_code/web_search_adapters.py +66 -0
- alysis_code/web_search_policy.py +27 -0
- alysis_code/workspace_binding.py +389 -0
- alysis_code/workspace_binding_ui.py +408 -0
- alysis_code/workspace_context.py +273 -0
- alysis_code/workspace_isolation.py +138 -0
- alysis_code/workspace_provisioning.py +455 -0
- alysis_code-0.13.0.dist-info/METADATA +507 -0
- alysis_code-0.13.0.dist-info/RECORD +458 -0
- alysis_code-0.13.0.dist-info/WHEEL +4 -0
- alysis_code-0.13.0.dist-info/entry_points.txt +3 -0
- alysis_code-0.13.0.dist-info/licenses/LICENSE +176 -0
- alysis_code-0.13.0.dist-info/licenses/NOTICE +4 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import html
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
import webbrowser
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from http import HTTPStatus
|
|
10
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
11
|
+
from urllib.parse import parse_qs, urlsplit
|
|
12
|
+
|
|
13
|
+
from .oauth import (
|
|
14
|
+
McpAuthorizationServerMetadata,
|
|
15
|
+
McpOAuthCallbackError,
|
|
16
|
+
build_authorization_url,
|
|
17
|
+
build_pkce_challenge,
|
|
18
|
+
exchange_authorization_code,
|
|
19
|
+
generate_oauth_state,
|
|
20
|
+
generate_pkce_verifier,
|
|
21
|
+
)
|
|
22
|
+
from .oauth_store import McpOAuthTokenRecord, save_oauth_token_record
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"McpOAuthCallbackPayload",
|
|
26
|
+
"McpOAuthLoginResult",
|
|
27
|
+
"perform_authorization_code_login",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
_CALLBACK_PATH = "/oauth/callback"
|
|
31
|
+
_LOCALHOST_CALLBACK_TIMEOUT_S = 120.0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class McpOAuthCallbackPayload:
|
|
36
|
+
code: str | None
|
|
37
|
+
state: str | None
|
|
38
|
+
error: str | None = None
|
|
39
|
+
error_description: str | None = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class McpOAuthLoginResult:
|
|
44
|
+
server_id: str
|
|
45
|
+
authorization_url: str
|
|
46
|
+
redirect_uri: str
|
|
47
|
+
browser_opened: bool
|
|
48
|
+
token_record: McpOAuthTokenRecord
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class _CallbackState:
|
|
52
|
+
def __init__(self) -> None:
|
|
53
|
+
self.event = threading.Event()
|
|
54
|
+
self.payload: McpOAuthCallbackPayload | None = None
|
|
55
|
+
self._lock = threading.Lock()
|
|
56
|
+
|
|
57
|
+
def set_payload(self, payload: McpOAuthCallbackPayload) -> None:
|
|
58
|
+
with self._lock:
|
|
59
|
+
if self.payload is None:
|
|
60
|
+
self.payload = payload
|
|
61
|
+
self.event.set()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class _CallbackListenerHttpServer(ThreadingHTTPServer):
|
|
65
|
+
daemon_threads = True
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class _CallbackHandler(BaseHTTPRequestHandler):
|
|
69
|
+
server_version = "AlysisOAuthCallback/1.0"
|
|
70
|
+
|
|
71
|
+
def log_message(self, format: str, *args: object) -> None: # noqa: A003
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def callback_state(self) -> _CallbackState:
|
|
76
|
+
return self.server.callback_state # type: ignore[attr-defined]
|
|
77
|
+
|
|
78
|
+
def do_GET(self) -> None: # noqa: N802
|
|
79
|
+
parsed = urlsplit(self.path)
|
|
80
|
+
if parsed.path != _CALLBACK_PATH:
|
|
81
|
+
self.send_error(HTTPStatus.NOT_FOUND)
|
|
82
|
+
return
|
|
83
|
+
query = parse_qs(parsed.query)
|
|
84
|
+
payload = McpOAuthCallbackPayload(
|
|
85
|
+
code=(query.get("code") or [None])[0],
|
|
86
|
+
state=(query.get("state") or [None])[0],
|
|
87
|
+
error=(query.get("error") or [None])[0],
|
|
88
|
+
error_description=(query.get("error_description") or [None])[0],
|
|
89
|
+
)
|
|
90
|
+
self.callback_state.set_payload(payload)
|
|
91
|
+
message = (
|
|
92
|
+
"OAuth login failed. Return to the terminal for details."
|
|
93
|
+
if payload.error
|
|
94
|
+
else "OAuth login complete. You can close this window and return to the terminal."
|
|
95
|
+
)
|
|
96
|
+
self._send_html(HTTPStatus.OK, message)
|
|
97
|
+
|
|
98
|
+
def _send_html(self, status: int, message: str) -> None:
|
|
99
|
+
safe_message = html.escape(message)
|
|
100
|
+
body = (
|
|
101
|
+
'<!doctype html><html><head><meta charset="utf-8"></head>'
|
|
102
|
+
f"<body><p>{safe_message}</p></body></html>"
|
|
103
|
+
).encode()
|
|
104
|
+
self.send_response(status)
|
|
105
|
+
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
106
|
+
self.send_header("Content-Length", str(len(body)))
|
|
107
|
+
self.end_headers()
|
|
108
|
+
self.wfile.write(body)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class _LocalhostCallbackListener:
|
|
112
|
+
def __init__(
|
|
113
|
+
self,
|
|
114
|
+
*,
|
|
115
|
+
server_id: str,
|
|
116
|
+
authorization_server_url: str,
|
|
117
|
+
host: str,
|
|
118
|
+
port: int,
|
|
119
|
+
) -> None:
|
|
120
|
+
self.server_id = server_id
|
|
121
|
+
self.authorization_server_url = authorization_server_url
|
|
122
|
+
self.callback_state = _CallbackState()
|
|
123
|
+
self._server = _CallbackListenerHttpServer((host, port), _CallbackHandler)
|
|
124
|
+
self._server.callback_state = self.callback_state # type: ignore[attr-defined]
|
|
125
|
+
self._thread = threading.Thread(target=self._server.serve_forever, daemon=True)
|
|
126
|
+
self._thread.start()
|
|
127
|
+
self._closed = False
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def redirect_uri(self) -> str:
|
|
131
|
+
host, port = self._server.server_address
|
|
132
|
+
return f"http://{host}:{port}{_CALLBACK_PATH}"
|
|
133
|
+
|
|
134
|
+
def wait_for_callback(
|
|
135
|
+
self,
|
|
136
|
+
*,
|
|
137
|
+
timeout_s: float,
|
|
138
|
+
interrupt_check: Callable[[], None] | None = None,
|
|
139
|
+
) -> McpOAuthCallbackPayload:
|
|
140
|
+
deadline = time.monotonic() + timeout_s
|
|
141
|
+
try:
|
|
142
|
+
while True:
|
|
143
|
+
remaining = deadline - time.monotonic()
|
|
144
|
+
if remaining <= 0:
|
|
145
|
+
raise McpOAuthCallbackError(
|
|
146
|
+
f"timed out waiting {int(timeout_s)}s for the localhost OAuth callback.",
|
|
147
|
+
server_id=self.server_id,
|
|
148
|
+
authorization_server_url=self.authorization_server_url,
|
|
149
|
+
)
|
|
150
|
+
if interrupt_check is not None:
|
|
151
|
+
interrupt_check()
|
|
152
|
+
if self.callback_state.event.wait(timeout=min(0.1, remaining)):
|
|
153
|
+
if self.callback_state.payload is None:
|
|
154
|
+
raise McpOAuthCallbackError(
|
|
155
|
+
"localhost OAuth callback completed without payload.",
|
|
156
|
+
server_id=self.server_id,
|
|
157
|
+
authorization_server_url=self.authorization_server_url,
|
|
158
|
+
)
|
|
159
|
+
return self.callback_state.payload
|
|
160
|
+
except KeyboardInterrupt as exc:
|
|
161
|
+
raise McpOAuthCallbackError(
|
|
162
|
+
"OAuth login interrupted while waiting for the localhost callback.",
|
|
163
|
+
server_id=self.server_id,
|
|
164
|
+
authorization_server_url=self.authorization_server_url,
|
|
165
|
+
) from exc
|
|
166
|
+
|
|
167
|
+
def close(self) -> None:
|
|
168
|
+
if self._closed:
|
|
169
|
+
return
|
|
170
|
+
self._closed = True
|
|
171
|
+
self._server.shutdown()
|
|
172
|
+
self._server.server_close()
|
|
173
|
+
self._thread.join(timeout=5.0)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _open_localhost_listener(
|
|
177
|
+
*,
|
|
178
|
+
server_id: str,
|
|
179
|
+
authorization_server_url: str,
|
|
180
|
+
redirect_host: str | None,
|
|
181
|
+
redirect_port: int | None,
|
|
182
|
+
) -> _LocalhostCallbackListener:
|
|
183
|
+
host = str(redirect_host or "127.0.0.1").strip() or "127.0.0.1"
|
|
184
|
+
preferred_port = 0 if redirect_port is None else int(redirect_port)
|
|
185
|
+
attempted_ports = [preferred_port, 0]
|
|
186
|
+
last_error: OSError | None = None
|
|
187
|
+
for index, port in enumerate(attempted_ports):
|
|
188
|
+
try:
|
|
189
|
+
return _LocalhostCallbackListener(
|
|
190
|
+
server_id=server_id,
|
|
191
|
+
authorization_server_url=authorization_server_url,
|
|
192
|
+
host=host,
|
|
193
|
+
port=port,
|
|
194
|
+
)
|
|
195
|
+
except OSError as exc:
|
|
196
|
+
last_error = exc
|
|
197
|
+
if index == len(attempted_ports) - 1:
|
|
198
|
+
break
|
|
199
|
+
raise McpOAuthCallbackError(
|
|
200
|
+
f"failed to bind localhost OAuth callback listener on host '{host}' after two attempts.",
|
|
201
|
+
server_id=server_id,
|
|
202
|
+
authorization_server_url=authorization_server_url,
|
|
203
|
+
) from last_error
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _write_manual_login_instructions(
|
|
207
|
+
writer: Callable[[str], None],
|
|
208
|
+
*,
|
|
209
|
+
authorization_url: str,
|
|
210
|
+
redirect_uri: str,
|
|
211
|
+
) -> None:
|
|
212
|
+
writer(
|
|
213
|
+
"Open this URL in a browser to continue MCP OAuth login:\n"
|
|
214
|
+
f"{authorization_url}\n"
|
|
215
|
+
f"Redirect URI: {redirect_uri}\n"
|
|
216
|
+
"This flow still requires localhost callback access and cannot complete without "
|
|
217
|
+
"the redirect reaching this machine."
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def perform_authorization_code_login(
|
|
222
|
+
*,
|
|
223
|
+
server_id: str,
|
|
224
|
+
authorization_server_metadata: McpAuthorizationServerMetadata,
|
|
225
|
+
resource_server_url: str,
|
|
226
|
+
client_id: str,
|
|
227
|
+
scopes: tuple[str, ...] | list[str] | None = None,
|
|
228
|
+
redirect_host: str | None = None,
|
|
229
|
+
redirect_port: int | None = None,
|
|
230
|
+
timeout_s: float = _LOCALHOST_CALLBACK_TIMEOUT_S,
|
|
231
|
+
token_timeout_s: float = 10.0,
|
|
232
|
+
browser_opener: Callable[[str], bool] | None = None,
|
|
233
|
+
output_write: Callable[[str], None] | None = None,
|
|
234
|
+
interrupt_check: Callable[[], None] | None = None,
|
|
235
|
+
) -> McpOAuthLoginResult:
|
|
236
|
+
writer = output_write or print
|
|
237
|
+
listener = _open_localhost_listener(
|
|
238
|
+
server_id=server_id,
|
|
239
|
+
authorization_server_url=authorization_server_metadata.authorization_endpoint,
|
|
240
|
+
redirect_host=redirect_host,
|
|
241
|
+
redirect_port=redirect_port,
|
|
242
|
+
)
|
|
243
|
+
try:
|
|
244
|
+
code_verifier = generate_pkce_verifier()
|
|
245
|
+
code_challenge = build_pkce_challenge(code_verifier)
|
|
246
|
+
state = generate_oauth_state()
|
|
247
|
+
authorization_url = build_authorization_url(
|
|
248
|
+
server_id=server_id,
|
|
249
|
+
authorization_server_metadata=authorization_server_metadata,
|
|
250
|
+
resource_server_url=resource_server_url,
|
|
251
|
+
client_id=client_id,
|
|
252
|
+
redirect_uri=listener.redirect_uri,
|
|
253
|
+
code_challenge=code_challenge,
|
|
254
|
+
state=state,
|
|
255
|
+
scopes=scopes,
|
|
256
|
+
)
|
|
257
|
+
opener = browser_opener or webbrowser.open
|
|
258
|
+
try:
|
|
259
|
+
browser_opened = bool(opener(authorization_url))
|
|
260
|
+
except Exception:
|
|
261
|
+
browser_opened = False
|
|
262
|
+
if not browser_opened:
|
|
263
|
+
_write_manual_login_instructions(
|
|
264
|
+
writer,
|
|
265
|
+
authorization_url=authorization_url,
|
|
266
|
+
redirect_uri=listener.redirect_uri,
|
|
267
|
+
)
|
|
268
|
+
callback = listener.wait_for_callback(timeout_s=timeout_s, interrupt_check=interrupt_check)
|
|
269
|
+
if callback.error:
|
|
270
|
+
details = callback.error
|
|
271
|
+
if callback.error_description:
|
|
272
|
+
details = f"{details}: {callback.error_description}"
|
|
273
|
+
raise McpOAuthCallbackError(
|
|
274
|
+
f"authorization server returned callback error '{details}'.",
|
|
275
|
+
server_id=server_id,
|
|
276
|
+
authorization_server_url=authorization_server_metadata.authorization_endpoint,
|
|
277
|
+
)
|
|
278
|
+
if callback.state != state:
|
|
279
|
+
raise McpOAuthCallbackError(
|
|
280
|
+
"received OAuth callback with mismatched state.",
|
|
281
|
+
server_id=server_id,
|
|
282
|
+
authorization_server_url=authorization_server_metadata.authorization_endpoint,
|
|
283
|
+
)
|
|
284
|
+
if not callback.code:
|
|
285
|
+
raise McpOAuthCallbackError(
|
|
286
|
+
"received OAuth callback without an authorization code.",
|
|
287
|
+
server_id=server_id,
|
|
288
|
+
authorization_server_url=authorization_server_metadata.authorization_endpoint,
|
|
289
|
+
)
|
|
290
|
+
record = exchange_authorization_code(
|
|
291
|
+
server_id=server_id,
|
|
292
|
+
authorization_server_metadata=authorization_server_metadata,
|
|
293
|
+
resource_server_url=resource_server_url,
|
|
294
|
+
client_id=client_id,
|
|
295
|
+
code=callback.code,
|
|
296
|
+
redirect_uri=listener.redirect_uri,
|
|
297
|
+
code_verifier=code_verifier,
|
|
298
|
+
requested_scopes=scopes,
|
|
299
|
+
timeout_s=token_timeout_s,
|
|
300
|
+
)
|
|
301
|
+
save_oauth_token_record(server_id, record)
|
|
302
|
+
return McpOAuthLoginResult(
|
|
303
|
+
server_id=server_id,
|
|
304
|
+
authorization_url=authorization_url,
|
|
305
|
+
redirect_uri=listener.redirect_uri,
|
|
306
|
+
browser_opened=browser_opened,
|
|
307
|
+
token_record=record,
|
|
308
|
+
)
|
|
309
|
+
finally:
|
|
310
|
+
listener.close()
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .config import user_mcp_config_path
|
|
10
|
+
from .errors import (
|
|
11
|
+
McpOAuthTokenStoreError,
|
|
12
|
+
McpTokenStoreCorruptError,
|
|
13
|
+
McpTokenStoreMigrationError,
|
|
14
|
+
McpTokenStoreUnavailableError,
|
|
15
|
+
McpTokenStoreVersionError,
|
|
16
|
+
)
|
|
17
|
+
from .models import normalize_ordered_string_list, normalize_server_id
|
|
18
|
+
from .token_store import load_token_payload_result, migrate_legacy_token_payload, save_token_payload
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"McpOAuthTokenRecord",
|
|
22
|
+
"McpOAuthTokenStoreError",
|
|
23
|
+
"McpTokenStoreCorruptError",
|
|
24
|
+
"McpTokenStoreMigrationError",
|
|
25
|
+
"McpTokenStoreUnavailableError",
|
|
26
|
+
"McpTokenStoreVersionError",
|
|
27
|
+
"delete_oauth_token_record",
|
|
28
|
+
"list_oauth_token_server_ids",
|
|
29
|
+
"load_oauth_token_record",
|
|
30
|
+
"mcp_oauth_token_store_path",
|
|
31
|
+
"save_oauth_token_record",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
_OAUTH_BINDING_ID_RE = re.compile(r"^[A-Za-z0-9_-]{32,128}$")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _store_path() -> Path:
|
|
38
|
+
return user_mcp_config_path().with_name("mcp_oauth_tokens.json")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def mcp_oauth_token_store_path() -> Path:
|
|
42
|
+
return _store_path()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _canonical_server_id(server_id: str) -> str:
|
|
46
|
+
try:
|
|
47
|
+
return normalize_server_id(server_id)
|
|
48
|
+
except Exception as exc: # noqa: BLE001
|
|
49
|
+
raise McpOAuthTokenStoreError(
|
|
50
|
+
f"Invalid OAuth token store server id: {server_id!r}"
|
|
51
|
+
) from exc
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _require_string(value: object, *, field_name: str, server_id: str) -> str:
|
|
55
|
+
if not isinstance(value, str):
|
|
56
|
+
raise McpOAuthTokenStoreError(
|
|
57
|
+
f"OAuth token store entry for server '{server_id}' field '{field_name}' must be a string."
|
|
58
|
+
)
|
|
59
|
+
cleaned = value.strip()
|
|
60
|
+
if not cleaned:
|
|
61
|
+
raise McpOAuthTokenStoreError(
|
|
62
|
+
f"OAuth token store entry for server '{server_id}' field '{field_name}' cannot be empty."
|
|
63
|
+
)
|
|
64
|
+
return cleaned
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _optional_string(value: object, *, field_name: str, server_id: str) -> str | None:
|
|
68
|
+
if value is None:
|
|
69
|
+
return None
|
|
70
|
+
return _require_string(value, field_name=field_name, server_id=server_id)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _optional_binding_id(value: object, *, server_id: str) -> str | None:
|
|
74
|
+
if value is None:
|
|
75
|
+
return None
|
|
76
|
+
binding_id = _require_string(value, field_name="binding_id", server_id=server_id)
|
|
77
|
+
if not _OAUTH_BINDING_ID_RE.fullmatch(binding_id):
|
|
78
|
+
raise McpOAuthTokenStoreError(
|
|
79
|
+
f"OAuth token store entry for server '{server_id}' field 'binding_id' is invalid."
|
|
80
|
+
)
|
|
81
|
+
return binding_id
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _normalize_datetime(value: datetime, *, field_name: str) -> datetime:
|
|
85
|
+
if not isinstance(value, datetime):
|
|
86
|
+
raise McpOAuthTokenStoreError(f"OAuth token field '{field_name}' must be a datetime.")
|
|
87
|
+
if value.tzinfo is None:
|
|
88
|
+
raise McpOAuthTokenStoreError(
|
|
89
|
+
f"OAuth token field '{field_name}' must be timezone-aware UTC."
|
|
90
|
+
)
|
|
91
|
+
return value.astimezone(UTC).replace(microsecond=0)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _parse_datetime(value: object, *, field_name: str, server_id: str) -> datetime:
|
|
95
|
+
raw = _require_string(value, field_name=field_name, server_id=server_id)
|
|
96
|
+
normalized = raw[:-1] + "+00:00" if raw.endswith("Z") else raw
|
|
97
|
+
try:
|
|
98
|
+
parsed = datetime.fromisoformat(normalized)
|
|
99
|
+
except ValueError as exc:
|
|
100
|
+
raise McpOAuthTokenStoreError(
|
|
101
|
+
f"OAuth token store entry for server '{server_id}' field '{field_name}' must be ISO 8601 UTC."
|
|
102
|
+
) from exc
|
|
103
|
+
if parsed.tzinfo is None:
|
|
104
|
+
raise McpOAuthTokenStoreError(
|
|
105
|
+
f"OAuth token store entry for server '{server_id}' field '{field_name}' must be timezone-aware UTC."
|
|
106
|
+
)
|
|
107
|
+
return parsed.astimezone(UTC).replace(microsecond=0)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _format_datetime(value: datetime) -> str:
|
|
111
|
+
return _normalize_datetime(value, field_name="timestamp").isoformat().replace("+00:00", "Z")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass(frozen=True)
|
|
115
|
+
class McpOAuthTokenRecord:
|
|
116
|
+
access_token: str = field(repr=False)
|
|
117
|
+
token_type: str
|
|
118
|
+
expires_at: datetime
|
|
119
|
+
refresh_token: str | None = field(default=None, repr=False)
|
|
120
|
+
granted_scopes: tuple[str, ...] = field(default_factory=tuple)
|
|
121
|
+
obtained_at: datetime = field(default_factory=lambda: datetime.now(UTC).replace(microsecond=0))
|
|
122
|
+
binding_id: str | None = field(default=None, repr=False)
|
|
123
|
+
|
|
124
|
+
def __post_init__(self) -> None:
|
|
125
|
+
access_token = _require_string(
|
|
126
|
+
self.access_token, field_name="access_token", server_id="<record>"
|
|
127
|
+
)
|
|
128
|
+
token_type = _require_string(self.token_type, field_name="token_type", server_id="<record>")
|
|
129
|
+
refresh_token = _optional_string(
|
|
130
|
+
self.refresh_token,
|
|
131
|
+
field_name="refresh_token",
|
|
132
|
+
server_id="<record>",
|
|
133
|
+
)
|
|
134
|
+
scopes = tuple(
|
|
135
|
+
normalize_ordered_string_list(
|
|
136
|
+
list(self.granted_scopes),
|
|
137
|
+
field_name="granted_scopes",
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
expires_at = _normalize_datetime(self.expires_at, field_name="expires_at")
|
|
141
|
+
obtained_at = _normalize_datetime(self.obtained_at, field_name="obtained_at")
|
|
142
|
+
binding_id = _optional_binding_id(self.binding_id, server_id="<record>")
|
|
143
|
+
object.__setattr__(self, "access_token", access_token)
|
|
144
|
+
object.__setattr__(self, "token_type", token_type)
|
|
145
|
+
object.__setattr__(self, "refresh_token", refresh_token)
|
|
146
|
+
object.__setattr__(self, "granted_scopes", scopes)
|
|
147
|
+
object.__setattr__(self, "expires_at", expires_at)
|
|
148
|
+
object.__setattr__(self, "obtained_at", obtained_at)
|
|
149
|
+
object.__setattr__(self, "binding_id", binding_id)
|
|
150
|
+
|
|
151
|
+
def as_payload(self) -> dict[str, Any]:
|
|
152
|
+
payload = {
|
|
153
|
+
"access_token": self.access_token,
|
|
154
|
+
"token_type": self.token_type,
|
|
155
|
+
"expires_at": _format_datetime(self.expires_at),
|
|
156
|
+
"refresh_token": self.refresh_token,
|
|
157
|
+
"granted_scopes": list(self.granted_scopes),
|
|
158
|
+
"obtained_at": _format_datetime(self.obtained_at),
|
|
159
|
+
}
|
|
160
|
+
if self.binding_id is not None:
|
|
161
|
+
payload["binding_id"] = self.binding_id
|
|
162
|
+
return payload
|
|
163
|
+
|
|
164
|
+
@classmethod
|
|
165
|
+
def from_payload(cls, payload: object, *, server_id: str) -> McpOAuthTokenRecord:
|
|
166
|
+
if not isinstance(payload, dict):
|
|
167
|
+
raise McpOAuthTokenStoreError(
|
|
168
|
+
f"OAuth token store entry for server '{server_id}' must be an object."
|
|
169
|
+
)
|
|
170
|
+
scopes_raw = payload.get("granted_scopes")
|
|
171
|
+
try:
|
|
172
|
+
granted_scopes = tuple(
|
|
173
|
+
normalize_ordered_string_list(scopes_raw, field_name="granted_scopes")
|
|
174
|
+
)
|
|
175
|
+
except (TypeError, ValueError) as exc:
|
|
176
|
+
raise McpOAuthTokenStoreError(
|
|
177
|
+
f"OAuth token store entry for server '{server_id}' field 'granted_scopes' is invalid."
|
|
178
|
+
) from exc
|
|
179
|
+
return cls(
|
|
180
|
+
access_token=_require_string(
|
|
181
|
+
payload.get("access_token"),
|
|
182
|
+
field_name="access_token",
|
|
183
|
+
server_id=server_id,
|
|
184
|
+
),
|
|
185
|
+
token_type=_require_string(
|
|
186
|
+
payload.get("token_type"),
|
|
187
|
+
field_name="token_type",
|
|
188
|
+
server_id=server_id,
|
|
189
|
+
),
|
|
190
|
+
expires_at=_parse_datetime(
|
|
191
|
+
payload.get("expires_at"),
|
|
192
|
+
field_name="expires_at",
|
|
193
|
+
server_id=server_id,
|
|
194
|
+
),
|
|
195
|
+
refresh_token=_optional_string(
|
|
196
|
+
payload.get("refresh_token"),
|
|
197
|
+
field_name="refresh_token",
|
|
198
|
+
server_id=server_id,
|
|
199
|
+
),
|
|
200
|
+
granted_scopes=granted_scopes,
|
|
201
|
+
obtained_at=_parse_datetime(
|
|
202
|
+
payload.get("obtained_at"),
|
|
203
|
+
field_name="obtained_at",
|
|
204
|
+
server_id=server_id,
|
|
205
|
+
),
|
|
206
|
+
binding_id=_optional_binding_id(payload.get("binding_id"), server_id=server_id),
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _load_store_payload() -> tuple[dict[str, Any], bool]:
|
|
211
|
+
path = _store_path()
|
|
212
|
+
result = load_token_payload_result(path)
|
|
213
|
+
payload = result.payload
|
|
214
|
+
if not isinstance(payload, dict):
|
|
215
|
+
raise McpOAuthTokenStoreError(f"Invalid OAuth token store format: {path}")
|
|
216
|
+
normalized: dict[str, Any] = {}
|
|
217
|
+
for raw_server_id, record in payload.items():
|
|
218
|
+
if not isinstance(raw_server_id, str):
|
|
219
|
+
raise McpOAuthTokenStoreError(f"Invalid OAuth token store server id in: {path}")
|
|
220
|
+
normalized[_canonical_server_id(raw_server_id)] = record
|
|
221
|
+
return normalized, result.legacy_plaintext
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _migrate_after_successful_legacy_read(
|
|
225
|
+
payload: dict[str, Any], *, legacy_plaintext: bool
|
|
226
|
+
) -> None:
|
|
227
|
+
if legacy_plaintext:
|
|
228
|
+
migrate_legacy_token_payload(_store_path(), payload)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def load_oauth_token_record(server_id: str) -> McpOAuthTokenRecord | None:
|
|
232
|
+
canonical_server_id = _canonical_server_id(server_id)
|
|
233
|
+
payload, legacy_plaintext = _load_store_payload()
|
|
234
|
+
record = payload.get(canonical_server_id)
|
|
235
|
+
if record is None:
|
|
236
|
+
_migrate_after_successful_legacy_read(payload, legacy_plaintext=legacy_plaintext)
|
|
237
|
+
return None
|
|
238
|
+
loaded = McpOAuthTokenRecord.from_payload(record, server_id=canonical_server_id)
|
|
239
|
+
_migrate_after_successful_legacy_read(payload, legacy_plaintext=legacy_plaintext)
|
|
240
|
+
return loaded
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def save_oauth_token_record(server_id: str, record: McpOAuthTokenRecord) -> None:
|
|
244
|
+
canonical_server_id = _canonical_server_id(server_id)
|
|
245
|
+
path = _store_path()
|
|
246
|
+
payload, _legacy_plaintext = _load_store_payload()
|
|
247
|
+
payload[canonical_server_id] = record.as_payload()
|
|
248
|
+
try:
|
|
249
|
+
save_token_payload(path, payload)
|
|
250
|
+
except McpOAuthTokenStoreError as exc:
|
|
251
|
+
raise McpOAuthTokenStoreError(
|
|
252
|
+
f"Failed to write OAuth token store entry for server '{canonical_server_id}' to {path}"
|
|
253
|
+
) from exc
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def delete_oauth_token_record(server_id: str) -> bool:
|
|
257
|
+
canonical_server_id = _canonical_server_id(server_id)
|
|
258
|
+
payload, legacy_plaintext = _load_store_payload()
|
|
259
|
+
if canonical_server_id not in payload:
|
|
260
|
+
_migrate_after_successful_legacy_read(payload, legacy_plaintext=legacy_plaintext)
|
|
261
|
+
return False
|
|
262
|
+
del payload[canonical_server_id]
|
|
263
|
+
path = _store_path()
|
|
264
|
+
try:
|
|
265
|
+
save_token_payload(path, payload)
|
|
266
|
+
except McpOAuthTokenStoreError as exc:
|
|
267
|
+
raise McpOAuthTokenStoreError(
|
|
268
|
+
f"Failed to delete OAuth token store entry for server '{canonical_server_id}' from {path}"
|
|
269
|
+
) from exc
|
|
270
|
+
return True
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def list_oauth_token_server_ids() -> tuple[str, ...]:
|
|
274
|
+
payload, legacy_plaintext = _load_store_payload()
|
|
275
|
+
_migrate_after_successful_legacy_read(payload, legacy_plaintext=legacy_plaintext)
|
|
276
|
+
return tuple(sorted(payload))
|