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,597 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from pydantic import ValidationError
|
|
11
|
+
|
|
12
|
+
from ..branding import canonical_user_config_dir, env_get, env_get_from
|
|
13
|
+
from ..runtime_kind import RuntimeKind
|
|
14
|
+
from .errors import McpConfigError as ConfigError
|
|
15
|
+
from .models import (
|
|
16
|
+
ProjectMcpConfigFile,
|
|
17
|
+
PromptsMode,
|
|
18
|
+
ResolvedMcpConfig,
|
|
19
|
+
ResolvedMcpHttpOAuthConfig,
|
|
20
|
+
ResolvedMcpServer,
|
|
21
|
+
ResourcesMode,
|
|
22
|
+
RootsMode,
|
|
23
|
+
UserMcpConfigFile,
|
|
24
|
+
UserMcpHttpServer,
|
|
25
|
+
UserMcpStdioServer,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
_FORBIDDEN_PROJECT_SERVER_FIELDS = {
|
|
29
|
+
"args",
|
|
30
|
+
"command",
|
|
31
|
+
"env",
|
|
32
|
+
"headers",
|
|
33
|
+
"oauth",
|
|
34
|
+
"tool_prefix",
|
|
35
|
+
"transport",
|
|
36
|
+
"trust",
|
|
37
|
+
"url",
|
|
38
|
+
}
|
|
39
|
+
_ENV_PATTERN = re.compile(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _mcp_config_dir() -> Path:
|
|
43
|
+
override = env_get("ALYSIS_CONFIG_DIR")
|
|
44
|
+
if override:
|
|
45
|
+
return Path(override)
|
|
46
|
+
return canonical_user_config_dir()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def user_mcp_config_path() -> Path:
|
|
50
|
+
return _mcp_config_dir() / "mcp.json"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def project_mcp_config_path(workspace_root: Path) -> Path:
|
|
54
|
+
return workspace_root.resolve() / ".alysis" / "mcp.json"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def redact_sensitive_mapping(values: Mapping[str, str] | None) -> dict[str, str]:
|
|
58
|
+
if not values:
|
|
59
|
+
return {}
|
|
60
|
+
return {str(key): "[redacted]" for key in values}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def expand_env_placeholders(
|
|
64
|
+
value: str,
|
|
65
|
+
*,
|
|
66
|
+
env: Mapping[str, str],
|
|
67
|
+
source_path: Path,
|
|
68
|
+
field_path: str,
|
|
69
|
+
) -> str:
|
|
70
|
+
text = str(value)
|
|
71
|
+
|
|
72
|
+
def _replace(match: re.Match[str]) -> str:
|
|
73
|
+
var_name = match.group(1)
|
|
74
|
+
resolved = env_get_from(env, var_name)
|
|
75
|
+
if resolved is None:
|
|
76
|
+
raise ConfigError(
|
|
77
|
+
f"Invalid MCP config: {source_path}: {field_path}: missing environment variable "
|
|
78
|
+
f"${{{var_name}}}"
|
|
79
|
+
)
|
|
80
|
+
return resolved
|
|
81
|
+
|
|
82
|
+
return _ENV_PATTERN.sub(_replace, text)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _read_json_object(path: Path) -> dict[str, Any]:
|
|
86
|
+
try:
|
|
87
|
+
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
88
|
+
except Exception as e: # noqa: BLE001
|
|
89
|
+
raise ConfigError(f"Failed to read MCP config: {path}") from e
|
|
90
|
+
if not isinstance(raw, dict):
|
|
91
|
+
raise ConfigError(f"Invalid MCP config format (expected JSON object): {path}")
|
|
92
|
+
return raw
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _normalize_server_keys(path: Path, raw: dict[str, Any]) -> dict[str, Any]:
|
|
96
|
+
servers = raw.get("servers")
|
|
97
|
+
if servers is None:
|
|
98
|
+
raw["servers"] = {}
|
|
99
|
+
return raw
|
|
100
|
+
if not isinstance(servers, dict):
|
|
101
|
+
raise ConfigError(f"Invalid MCP config: {path}: servers must be an object.")
|
|
102
|
+
normalized_servers: dict[str, Any] = {}
|
|
103
|
+
for raw_id, payload in servers.items():
|
|
104
|
+
from .models import normalize_server_id
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
server_id = normalize_server_id(raw_id)
|
|
108
|
+
except ConfigError as exc:
|
|
109
|
+
raise ConfigError(f"Invalid MCP config: {path}: {exc}") from exc
|
|
110
|
+
if server_id in normalized_servers:
|
|
111
|
+
raise ConfigError(
|
|
112
|
+
f"Invalid MCP config: {path}: duplicate server id after normalization: {server_id}"
|
|
113
|
+
)
|
|
114
|
+
normalized_servers[server_id] = payload
|
|
115
|
+
raw["servers"] = normalized_servers
|
|
116
|
+
return raw
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _raise_project_override_error(path: Path, *, server_id: str, field_name: str) -> None:
|
|
120
|
+
raise ConfigError(
|
|
121
|
+
f"Invalid MCP config: {path}: server '{server_id}': field '{field_name}': "
|
|
122
|
+
"project config cannot override this field."
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _raise_project_narrowing_error(
|
|
127
|
+
path: Path,
|
|
128
|
+
*,
|
|
129
|
+
server_id: str,
|
|
130
|
+
field_name: str,
|
|
131
|
+
reason: str,
|
|
132
|
+
) -> None:
|
|
133
|
+
raise ConfigError(
|
|
134
|
+
f"Invalid MCP config: {path}: server '{server_id}': field '{field_name}': {reason}"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _validate_project_override_boundary(path: Path, raw: dict[str, Any]) -> None:
|
|
139
|
+
servers = raw.get("servers")
|
|
140
|
+
if not isinstance(servers, dict):
|
|
141
|
+
return
|
|
142
|
+
for server_id, payload in servers.items():
|
|
143
|
+
if not isinstance(payload, dict):
|
|
144
|
+
continue
|
|
145
|
+
for field_name in sorted(_FORBIDDEN_PROJECT_SERVER_FIELDS):
|
|
146
|
+
if field_name in payload:
|
|
147
|
+
_raise_project_override_error(
|
|
148
|
+
path,
|
|
149
|
+
server_id=str(server_id),
|
|
150
|
+
field_name=field_name,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _format_validation_error(path: Path, exc: ValidationError) -> ConfigError:
|
|
155
|
+
details: list[str] = []
|
|
156
|
+
for err in exc.errors(include_url=False):
|
|
157
|
+
loc = ".".join(str(part) for part in err.get("loc", ()))
|
|
158
|
+
msg = str(err.get("msg") or "invalid value")
|
|
159
|
+
detail = f"{path}: {msg}" if not loc else f"{path}: {loc}: {msg}"
|
|
160
|
+
details.append(detail)
|
|
161
|
+
joined = "\n".join(details) if details else f"{path}: invalid MCP config"
|
|
162
|
+
return ConfigError(f"Invalid MCP config:\n{joined}")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _load_user_mcp_config(path: Path) -> UserMcpConfigFile:
|
|
166
|
+
raw = _normalize_server_keys(path, _read_json_object(path))
|
|
167
|
+
try:
|
|
168
|
+
return UserMcpConfigFile.model_validate(raw)
|
|
169
|
+
except ValidationError as exc:
|
|
170
|
+
raise _format_validation_error(path, exc) from exc
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _load_project_mcp_config(path: Path) -> ProjectMcpConfigFile:
|
|
174
|
+
raw = _normalize_server_keys(path, _read_json_object(path))
|
|
175
|
+
_validate_project_override_boundary(path, raw)
|
|
176
|
+
try:
|
|
177
|
+
return ProjectMcpConfigFile.model_validate(raw)
|
|
178
|
+
except ValidationError as exc:
|
|
179
|
+
raise _format_validation_error(path, exc) from exc
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _expand_user_secret_map(
|
|
183
|
+
values: Mapping[str, str] | None,
|
|
184
|
+
*,
|
|
185
|
+
env: Mapping[str, str],
|
|
186
|
+
source_path: Path,
|
|
187
|
+
field_name: str,
|
|
188
|
+
server_id: str,
|
|
189
|
+
) -> dict[str, str]:
|
|
190
|
+
if not values:
|
|
191
|
+
return {}
|
|
192
|
+
expanded: dict[str, str] = {}
|
|
193
|
+
for key, raw_value in values.items():
|
|
194
|
+
expanded[key] = expand_env_placeholders(
|
|
195
|
+
raw_value,
|
|
196
|
+
env=env,
|
|
197
|
+
source_path=source_path,
|
|
198
|
+
field_path=f"servers.{server_id}.{field_name}.{key}",
|
|
199
|
+
)
|
|
200
|
+
return expanded
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _normalize_name_key(value: str) -> str:
|
|
204
|
+
return value.casefold()
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _merge_enabled(
|
|
208
|
+
*,
|
|
209
|
+
user_value: bool,
|
|
210
|
+
project_value: bool | None,
|
|
211
|
+
project_path: Path,
|
|
212
|
+
server_id: str,
|
|
213
|
+
) -> bool:
|
|
214
|
+
if project_value is None:
|
|
215
|
+
return user_value
|
|
216
|
+
if not user_value and project_value:
|
|
217
|
+
_raise_project_narrowing_error(
|
|
218
|
+
project_path,
|
|
219
|
+
server_id=server_id,
|
|
220
|
+
field_name="enabled",
|
|
221
|
+
reason="project config cannot re-enable a server disabled in user config.",
|
|
222
|
+
)
|
|
223
|
+
return bool(project_value)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _merge_enabled_in(
|
|
227
|
+
*,
|
|
228
|
+
user_value: tuple[RuntimeKind, ...] | None,
|
|
229
|
+
project_value: tuple[RuntimeKind, ...] | None,
|
|
230
|
+
project_path: Path,
|
|
231
|
+
server_id: str,
|
|
232
|
+
) -> tuple[RuntimeKind, ...] | None:
|
|
233
|
+
if project_value is None:
|
|
234
|
+
return user_value
|
|
235
|
+
if user_value is None:
|
|
236
|
+
return project_value
|
|
237
|
+
user_set = set(user_value)
|
|
238
|
+
if any(kind not in user_set for kind in project_value):
|
|
239
|
+
_raise_project_narrowing_error(
|
|
240
|
+
project_path,
|
|
241
|
+
server_id=server_id,
|
|
242
|
+
field_name="enabled_in",
|
|
243
|
+
reason="project config cannot broaden enabled_in beyond user config.",
|
|
244
|
+
)
|
|
245
|
+
return project_value
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _merge_allowed_tools(
|
|
249
|
+
*,
|
|
250
|
+
user_value: tuple[str, ...],
|
|
251
|
+
project_value: tuple[str, ...] | None,
|
|
252
|
+
project_path: Path,
|
|
253
|
+
server_id: str,
|
|
254
|
+
) -> tuple[str, ...]:
|
|
255
|
+
if project_value is None:
|
|
256
|
+
return user_value
|
|
257
|
+
if not user_value:
|
|
258
|
+
return project_value
|
|
259
|
+
user_set = {_normalize_name_key(name) for name in user_value}
|
|
260
|
+
if any(_normalize_name_key(name) not in user_set for name in project_value):
|
|
261
|
+
_raise_project_narrowing_error(
|
|
262
|
+
project_path,
|
|
263
|
+
server_id=server_id,
|
|
264
|
+
field_name="allowed_tools",
|
|
265
|
+
reason="project config cannot broaden allowed_tools beyond user config.",
|
|
266
|
+
)
|
|
267
|
+
return project_value
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _stable_union_names(
|
|
271
|
+
base: tuple[str, ...],
|
|
272
|
+
extra: tuple[str, ...],
|
|
273
|
+
) -> tuple[str, ...]:
|
|
274
|
+
merged = list(base)
|
|
275
|
+
seen = {_normalize_name_key(name) for name in base}
|
|
276
|
+
for name in extra:
|
|
277
|
+
key = _normalize_name_key(name)
|
|
278
|
+
if key in seen:
|
|
279
|
+
continue
|
|
280
|
+
seen.add(key)
|
|
281
|
+
merged.append(name)
|
|
282
|
+
return tuple(merged)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _merge_denied_tools(
|
|
286
|
+
*,
|
|
287
|
+
user_value: tuple[str, ...],
|
|
288
|
+
project_value: tuple[str, ...] | None,
|
|
289
|
+
) -> tuple[str, ...]:
|
|
290
|
+
if project_value is None:
|
|
291
|
+
return user_value
|
|
292
|
+
return _stable_union_names(user_value, project_value)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _merge_timeout(
|
|
296
|
+
*,
|
|
297
|
+
field_name: str,
|
|
298
|
+
user_value: float,
|
|
299
|
+
project_value: float | None,
|
|
300
|
+
project_path: Path,
|
|
301
|
+
server_id: str,
|
|
302
|
+
) -> float:
|
|
303
|
+
if project_value is None:
|
|
304
|
+
return float(user_value)
|
|
305
|
+
if float(project_value) > float(user_value):
|
|
306
|
+
_raise_project_narrowing_error(
|
|
307
|
+
project_path,
|
|
308
|
+
server_id=server_id,
|
|
309
|
+
field_name=field_name,
|
|
310
|
+
reason=f"project config cannot increase {field_name} beyond user config.",
|
|
311
|
+
)
|
|
312
|
+
return float(project_value)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _merge_roots_mode(
|
|
316
|
+
*,
|
|
317
|
+
user_value: RootsMode,
|
|
318
|
+
project_value: RootsMode | None,
|
|
319
|
+
project_path: Path,
|
|
320
|
+
server_id: str,
|
|
321
|
+
) -> RootsMode:
|
|
322
|
+
if project_value is None:
|
|
323
|
+
return user_value
|
|
324
|
+
if user_value == "disabled" and project_value == "workspace":
|
|
325
|
+
_raise_project_narrowing_error(
|
|
326
|
+
project_path,
|
|
327
|
+
server_id=server_id,
|
|
328
|
+
field_name="roots_mode",
|
|
329
|
+
reason="project config cannot broaden roots exposure beyond user config.",
|
|
330
|
+
)
|
|
331
|
+
return project_value
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _merge_resources_mode(
|
|
335
|
+
*,
|
|
336
|
+
user_value: ResourcesMode,
|
|
337
|
+
project_value: ResourcesMode | None,
|
|
338
|
+
project_path: Path,
|
|
339
|
+
server_id: str,
|
|
340
|
+
) -> ResourcesMode:
|
|
341
|
+
if project_value is None:
|
|
342
|
+
return user_value
|
|
343
|
+
if user_value == "disabled" and project_value == "listed_read_only":
|
|
344
|
+
_raise_project_narrowing_error(
|
|
345
|
+
project_path,
|
|
346
|
+
server_id=server_id,
|
|
347
|
+
field_name="resources_mode",
|
|
348
|
+
reason="project config cannot broaden resources exposure beyond user config.",
|
|
349
|
+
)
|
|
350
|
+
return project_value
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _merge_prompts_mode(
|
|
354
|
+
*,
|
|
355
|
+
user_value: PromptsMode,
|
|
356
|
+
project_value: PromptsMode | None,
|
|
357
|
+
project_path: Path,
|
|
358
|
+
server_id: str,
|
|
359
|
+
) -> PromptsMode:
|
|
360
|
+
if project_value is None:
|
|
361
|
+
return user_value
|
|
362
|
+
if user_value == "disabled" and project_value == "listed_get_only":
|
|
363
|
+
_raise_project_narrowing_error(
|
|
364
|
+
project_path,
|
|
365
|
+
server_id=server_id,
|
|
366
|
+
field_name="prompts_mode",
|
|
367
|
+
reason="project config cannot broaden prompts exposure beyond user config.",
|
|
368
|
+
)
|
|
369
|
+
return project_value
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _validate_merged_tool_policy_overlap(
|
|
373
|
+
*,
|
|
374
|
+
source_path: Path,
|
|
375
|
+
server_id: str,
|
|
376
|
+
allowed_tools: tuple[str, ...],
|
|
377
|
+
denied_tools: tuple[str, ...],
|
|
378
|
+
) -> None:
|
|
379
|
+
overlap = sorted(
|
|
380
|
+
name
|
|
381
|
+
for name in allowed_tools
|
|
382
|
+
if _normalize_name_key(name) in {_normalize_name_key(denied) for denied in denied_tools}
|
|
383
|
+
)
|
|
384
|
+
if not overlap:
|
|
385
|
+
return
|
|
386
|
+
joined = ", ".join(overlap)
|
|
387
|
+
_raise_project_narrowing_error(
|
|
388
|
+
source_path,
|
|
389
|
+
server_id=server_id,
|
|
390
|
+
field_name="allowed_tools/denied_tools",
|
|
391
|
+
reason=f"allowed_tools and denied_tools cannot overlap after merge: {joined}",
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _merge_server(
|
|
396
|
+
*,
|
|
397
|
+
server_id: str,
|
|
398
|
+
user_server: UserMcpStdioServer | UserMcpHttpServer,
|
|
399
|
+
project_override: Any | None,
|
|
400
|
+
env: Mapping[str, str],
|
|
401
|
+
user_path: Path,
|
|
402
|
+
project_path: Path,
|
|
403
|
+
) -> ResolvedMcpServer:
|
|
404
|
+
enabled = _merge_enabled(
|
|
405
|
+
user_value=user_server.enabled,
|
|
406
|
+
project_value=getattr(project_override, "enabled", None),
|
|
407
|
+
project_path=project_path,
|
|
408
|
+
server_id=server_id,
|
|
409
|
+
)
|
|
410
|
+
enabled_in = _merge_enabled_in(
|
|
411
|
+
user_value=tuple(user_server.enabled_in) if user_server.enabled_in is not None else None,
|
|
412
|
+
project_value=(
|
|
413
|
+
tuple(project_override.enabled_in)
|
|
414
|
+
if getattr(project_override, "enabled_in", None) is not None
|
|
415
|
+
else None
|
|
416
|
+
),
|
|
417
|
+
project_path=project_path,
|
|
418
|
+
server_id=server_id,
|
|
419
|
+
)
|
|
420
|
+
allowed_tools = _merge_allowed_tools(
|
|
421
|
+
user_value=tuple(user_server.allowed_tools),
|
|
422
|
+
project_value=(
|
|
423
|
+
tuple(project_override.allowed_tools)
|
|
424
|
+
if getattr(project_override, "allowed_tools", None) is not None
|
|
425
|
+
else None
|
|
426
|
+
),
|
|
427
|
+
project_path=project_path,
|
|
428
|
+
server_id=server_id,
|
|
429
|
+
)
|
|
430
|
+
denied_tools = _merge_denied_tools(
|
|
431
|
+
user_value=tuple(user_server.denied_tools),
|
|
432
|
+
project_value=(
|
|
433
|
+
tuple(project_override.denied_tools)
|
|
434
|
+
if getattr(project_override, "denied_tools", None) is not None
|
|
435
|
+
else None
|
|
436
|
+
),
|
|
437
|
+
)
|
|
438
|
+
overlap_source_path = user_path if project_override is None else project_path
|
|
439
|
+
_validate_merged_tool_policy_overlap(
|
|
440
|
+
source_path=overlap_source_path,
|
|
441
|
+
server_id=server_id,
|
|
442
|
+
allowed_tools=allowed_tools,
|
|
443
|
+
denied_tools=denied_tools,
|
|
444
|
+
)
|
|
445
|
+
startup_timeout_s = _merge_timeout(
|
|
446
|
+
field_name="startup_timeout_s",
|
|
447
|
+
user_value=float(user_server.startup_timeout_s),
|
|
448
|
+
project_value=getattr(project_override, "startup_timeout_s", None),
|
|
449
|
+
project_path=project_path,
|
|
450
|
+
server_id=server_id,
|
|
451
|
+
)
|
|
452
|
+
call_timeout_s = _merge_timeout(
|
|
453
|
+
field_name="call_timeout_s",
|
|
454
|
+
user_value=float(user_server.call_timeout_s),
|
|
455
|
+
project_value=getattr(project_override, "call_timeout_s", None),
|
|
456
|
+
project_path=project_path,
|
|
457
|
+
server_id=server_id,
|
|
458
|
+
)
|
|
459
|
+
roots_mode = _merge_roots_mode(
|
|
460
|
+
user_value=user_server.roots_mode,
|
|
461
|
+
project_value=getattr(project_override, "roots_mode", None),
|
|
462
|
+
project_path=project_path,
|
|
463
|
+
server_id=server_id,
|
|
464
|
+
)
|
|
465
|
+
resources_mode = _merge_resources_mode(
|
|
466
|
+
user_value=user_server.resources_mode,
|
|
467
|
+
project_value=getattr(project_override, "resources_mode", None),
|
|
468
|
+
project_path=project_path,
|
|
469
|
+
server_id=server_id,
|
|
470
|
+
)
|
|
471
|
+
prompts_mode = _merge_prompts_mode(
|
|
472
|
+
user_value=user_server.prompts_mode,
|
|
473
|
+
project_value=getattr(project_override, "prompts_mode", None),
|
|
474
|
+
project_path=project_path,
|
|
475
|
+
server_id=server_id,
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
if isinstance(user_server, UserMcpStdioServer):
|
|
479
|
+
return ResolvedMcpServer(
|
|
480
|
+
id=server_id,
|
|
481
|
+
transport="stdio",
|
|
482
|
+
enabled=enabled,
|
|
483
|
+
enabled_in=enabled_in,
|
|
484
|
+
trust=user_server.trust,
|
|
485
|
+
allowed_tools=allowed_tools,
|
|
486
|
+
denied_tools=denied_tools,
|
|
487
|
+
startup_timeout_s=startup_timeout_s,
|
|
488
|
+
call_timeout_s=call_timeout_s,
|
|
489
|
+
tool_prefix=user_server.tool_prefix,
|
|
490
|
+
roots_mode=roots_mode,
|
|
491
|
+
resources_mode=resources_mode,
|
|
492
|
+
prompts_mode=prompts_mode,
|
|
493
|
+
command=user_server.command,
|
|
494
|
+
args=tuple(user_server.args),
|
|
495
|
+
env=_expand_user_secret_map(
|
|
496
|
+
user_server.env,
|
|
497
|
+
env=env,
|
|
498
|
+
source_path=user_path,
|
|
499
|
+
field_name="env",
|
|
500
|
+
server_id=server_id,
|
|
501
|
+
),
|
|
502
|
+
)
|
|
503
|
+
oauth = None
|
|
504
|
+
if user_server.oauth is not None:
|
|
505
|
+
oauth = ResolvedMcpHttpOAuthConfig(
|
|
506
|
+
client_id=user_server.oauth.client_id,
|
|
507
|
+
redirect_host=user_server.oauth.redirect_host,
|
|
508
|
+
redirect_port=user_server.oauth.redirect_port,
|
|
509
|
+
scopes=tuple(user_server.oauth.scopes or []),
|
|
510
|
+
authorization_server_url=user_server.oauth.authorization_server_url,
|
|
511
|
+
)
|
|
512
|
+
return ResolvedMcpServer(
|
|
513
|
+
id=server_id,
|
|
514
|
+
transport="http",
|
|
515
|
+
enabled=enabled,
|
|
516
|
+
enabled_in=enabled_in,
|
|
517
|
+
trust=user_server.trust,
|
|
518
|
+
allowed_tools=allowed_tools,
|
|
519
|
+
denied_tools=denied_tools,
|
|
520
|
+
startup_timeout_s=startup_timeout_s,
|
|
521
|
+
call_timeout_s=call_timeout_s,
|
|
522
|
+
tool_prefix=user_server.tool_prefix,
|
|
523
|
+
roots_mode=roots_mode,
|
|
524
|
+
resources_mode=resources_mode,
|
|
525
|
+
prompts_mode=prompts_mode,
|
|
526
|
+
url=user_server.url,
|
|
527
|
+
headers=_expand_user_secret_map(
|
|
528
|
+
user_server.headers,
|
|
529
|
+
env=env,
|
|
530
|
+
source_path=user_path,
|
|
531
|
+
field_name="headers",
|
|
532
|
+
server_id=server_id,
|
|
533
|
+
),
|
|
534
|
+
oauth=oauth,
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def load_resolved_mcp_config(
|
|
539
|
+
*,
|
|
540
|
+
workspace_root: Path,
|
|
541
|
+
env: Mapping[str, str] | None = None,
|
|
542
|
+
) -> ResolvedMcpConfig:
|
|
543
|
+
resolved_workspace_root = workspace_root.resolve()
|
|
544
|
+
user_path = user_mcp_config_path()
|
|
545
|
+
project_path = project_mcp_config_path(resolved_workspace_root)
|
|
546
|
+
effective_env = os.environ if env is None else env
|
|
547
|
+
|
|
548
|
+
user_config = UserMcpConfigFile()
|
|
549
|
+
project_config = ProjectMcpConfigFile()
|
|
550
|
+
user_present = user_path.exists()
|
|
551
|
+
project_present = project_path.exists()
|
|
552
|
+
|
|
553
|
+
if user_present:
|
|
554
|
+
user_config = _load_user_mcp_config(user_path)
|
|
555
|
+
if project_present:
|
|
556
|
+
project_config = _load_project_mcp_config(project_path)
|
|
557
|
+
|
|
558
|
+
if not user_present and not project_present:
|
|
559
|
+
return ResolvedMcpConfig(
|
|
560
|
+
workspace_root=resolved_workspace_root,
|
|
561
|
+
user_config_path=user_path,
|
|
562
|
+
project_config_path=project_path,
|
|
563
|
+
user_config_present=False,
|
|
564
|
+
project_config_present=False,
|
|
565
|
+
servers=(),
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
user_servers = dict(user_config.servers)
|
|
569
|
+
project_servers = dict(project_config.servers)
|
|
570
|
+
|
|
571
|
+
for server_id in project_servers:
|
|
572
|
+
if server_id not in user_servers:
|
|
573
|
+
raise ConfigError(
|
|
574
|
+
f"Invalid MCP config: {project_path}: unknown server id '{server_id}'. "
|
|
575
|
+
"Project MCP config can only override user-defined servers."
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
resolved_servers = tuple(
|
|
579
|
+
_merge_server(
|
|
580
|
+
server_id=server_id,
|
|
581
|
+
user_server=user_server,
|
|
582
|
+
project_override=project_servers.get(server_id),
|
|
583
|
+
env=effective_env,
|
|
584
|
+
user_path=user_path,
|
|
585
|
+
project_path=project_path,
|
|
586
|
+
)
|
|
587
|
+
for server_id, user_server in user_servers.items()
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
return ResolvedMcpConfig(
|
|
591
|
+
workspace_root=resolved_workspace_root,
|
|
592
|
+
user_config_path=user_path,
|
|
593
|
+
project_config_path=project_path,
|
|
594
|
+
user_config_present=user_present,
|
|
595
|
+
project_config_present=project_present,
|
|
596
|
+
servers=resolved_servers,
|
|
597
|
+
)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ..config import ConfigError as AppConfigError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class McpError(RuntimeError):
|
|
7
|
+
error_code = "mcp_error"
|
|
8
|
+
retryable = False
|
|
9
|
+
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
message: str = "",
|
|
13
|
+
*,
|
|
14
|
+
server_id: str | None = None,
|
|
15
|
+
method: str | None = None,
|
|
16
|
+
cause: BaseException | None = None,
|
|
17
|
+
) -> None:
|
|
18
|
+
super().__init__(message)
|
|
19
|
+
self.server_id = server_id
|
|
20
|
+
self.method = method
|
|
21
|
+
self.cause = cause
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class McpConfigError(McpError, AppConfigError):
|
|
25
|
+
error_code = "mcp_config_error"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class McpPolicyError(McpError):
|
|
29
|
+
error_code = "mcp_policy_error"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class McpTransportError(McpError):
|
|
33
|
+
error_code = "mcp_transport_error"
|
|
34
|
+
retryable = True
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class McpTimeoutError(McpTransportError):
|
|
38
|
+
error_code = "mcp_timeout_error"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class McpProcessError(McpTransportError):
|
|
42
|
+
error_code = "mcp_process_error"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class McpProtocolError(McpError):
|
|
46
|
+
error_code = "mcp_protocol_error"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class McpSerializationError(McpProtocolError):
|
|
50
|
+
error_code = "mcp_serialization_error"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class McpRemoteError(McpError):
|
|
54
|
+
error_code = "mcp_remote_error"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class McpAuthError(McpError):
|
|
58
|
+
error_code = "mcp_auth_error"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class McpTokenStoreError(McpAuthError):
|
|
62
|
+
error_code = "mcp_token_store_error"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class McpOAuthTokenStoreError(McpTokenStoreError):
|
|
66
|
+
error_code = "mcp_oauth_token_store_error"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class McpTokenStoreUnavailableError(McpOAuthTokenStoreError):
|
|
70
|
+
error_code = "mcp_token_store_unavailable"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class McpTokenStoreCorruptError(McpOAuthTokenStoreError):
|
|
74
|
+
error_code = "mcp_token_store_corrupt"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class McpTokenStoreMigrationError(McpOAuthTokenStoreError):
|
|
78
|
+
error_code = "mcp_token_store_migration"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class McpTokenStoreVersionError(McpOAuthTokenStoreError):
|
|
82
|
+
error_code = "mcp_token_store_version"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class McpCancellationError(McpError):
|
|
86
|
+
error_code = "mcp_cancellation_error"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class McpResourceLimitError(McpError):
|
|
90
|
+
error_code = "mcp_resource_limit_error"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class McpNotSupportedError(McpError):
|
|
94
|
+
error_code = "mcp_not_supported_error"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def is_retryable_mcp_error(error: BaseException, *, side_effect_free: bool) -> bool:
|
|
98
|
+
if not side_effect_free or not isinstance(error, McpError):
|
|
99
|
+
return False
|
|
100
|
+
if isinstance(
|
|
101
|
+
error,
|
|
102
|
+
(
|
|
103
|
+
McpConfigError,
|
|
104
|
+
McpPolicyError,
|
|
105
|
+
McpProtocolError,
|
|
106
|
+
McpAuthError,
|
|
107
|
+
McpCancellationError,
|
|
108
|
+
McpResourceLimitError,
|
|
109
|
+
McpNotSupportedError,
|
|
110
|
+
),
|
|
111
|
+
):
|
|
112
|
+
return False
|
|
113
|
+
return bool(getattr(error, "retryable", False))
|