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,666 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from ipaddress import ip_address
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Annotated, Any, Literal, cast
|
|
9
|
+
from urllib.parse import urlsplit
|
|
10
|
+
|
|
11
|
+
from pydantic import (
|
|
12
|
+
BaseModel,
|
|
13
|
+
ConfigDict,
|
|
14
|
+
Field,
|
|
15
|
+
StrictBool,
|
|
16
|
+
StrictInt,
|
|
17
|
+
StrictStr,
|
|
18
|
+
field_validator,
|
|
19
|
+
model_validator,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from ..config import ConfigError as AppConfigError
|
|
23
|
+
from ..runtime_kind import RuntimeKind, normalize_runtime_kind
|
|
24
|
+
from .errors import McpConfigError as ConfigError
|
|
25
|
+
|
|
26
|
+
TransportKind = Literal["stdio", "http"]
|
|
27
|
+
RootsMode = Literal["disabled", "workspace"]
|
|
28
|
+
ResourcesMode = Literal["disabled", "listed_read_only"]
|
|
29
|
+
PromptsMode = Literal["disabled", "listed_get_only"]
|
|
30
|
+
|
|
31
|
+
_SERVER_ID_RE = re.compile(r"^[a-z0-9][a-z0-9._-]*(/[a-z0-9][a-z0-9._-]*)?$")
|
|
32
|
+
_ENV_NAME_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
|
33
|
+
_TOOL_PREFIX_RE = re.compile(r"^[a-z0-9][a-z0-9_-]*$")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def normalize_server_id(raw_value: object) -> str:
|
|
37
|
+
value = str(raw_value).strip().lower()
|
|
38
|
+
if not value:
|
|
39
|
+
raise ConfigError("MCP server id cannot be empty.")
|
|
40
|
+
if not _SERVER_ID_RE.fullmatch(value):
|
|
41
|
+
raise ConfigError(
|
|
42
|
+
f"Invalid MCP server id: {raw_value!r}. Expected lowercase [a-z0-9._-] "
|
|
43
|
+
"with at most one plugin scope slash."
|
|
44
|
+
)
|
|
45
|
+
return value
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def normalize_ordered_name_list(
|
|
49
|
+
value: object,
|
|
50
|
+
*,
|
|
51
|
+
field_name: str,
|
|
52
|
+
) -> list[str]:
|
|
53
|
+
if value is None:
|
|
54
|
+
return []
|
|
55
|
+
if not isinstance(value, list):
|
|
56
|
+
raise TypeError(f"{field_name} must be a list of strings.")
|
|
57
|
+
normalized: list[str] = []
|
|
58
|
+
seen: set[str] = set()
|
|
59
|
+
for item in value:
|
|
60
|
+
if not isinstance(item, str):
|
|
61
|
+
raise TypeError(f"{field_name} items must be strings.")
|
|
62
|
+
cleaned = item.strip()
|
|
63
|
+
if not cleaned:
|
|
64
|
+
continue
|
|
65
|
+
lowered = cleaned.lower()
|
|
66
|
+
if lowered in seen:
|
|
67
|
+
continue
|
|
68
|
+
seen.add(lowered)
|
|
69
|
+
normalized.append(cleaned)
|
|
70
|
+
return normalized
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def normalize_ordered_args_list(value: object, *, field_name: str) -> list[str]:
|
|
74
|
+
if value is None:
|
|
75
|
+
return []
|
|
76
|
+
if not isinstance(value, list):
|
|
77
|
+
raise TypeError(f"{field_name} must be a list of strings.")
|
|
78
|
+
normalized: list[str] = []
|
|
79
|
+
for item in value:
|
|
80
|
+
if not isinstance(item, str):
|
|
81
|
+
raise TypeError(f"{field_name} items must be strings.")
|
|
82
|
+
cleaned = item.strip()
|
|
83
|
+
if cleaned:
|
|
84
|
+
normalized.append(cleaned)
|
|
85
|
+
return normalized
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def normalize_ordered_string_list(
|
|
89
|
+
value: object,
|
|
90
|
+
*,
|
|
91
|
+
field_name: str,
|
|
92
|
+
) -> list[str]:
|
|
93
|
+
if value is None:
|
|
94
|
+
return []
|
|
95
|
+
if not isinstance(value, list):
|
|
96
|
+
raise TypeError(f"{field_name} must be a list of strings.")
|
|
97
|
+
normalized: list[str] = []
|
|
98
|
+
seen: set[str] = set()
|
|
99
|
+
for item in value:
|
|
100
|
+
if not isinstance(item, str):
|
|
101
|
+
raise TypeError(f"{field_name} items must be strings.")
|
|
102
|
+
cleaned = item.strip()
|
|
103
|
+
if not cleaned or cleaned in seen:
|
|
104
|
+
continue
|
|
105
|
+
seen.add(cleaned)
|
|
106
|
+
normalized.append(cleaned)
|
|
107
|
+
return normalized
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def normalize_optional_string(
|
|
111
|
+
value: object,
|
|
112
|
+
*,
|
|
113
|
+
field_name: str,
|
|
114
|
+
lowercase: bool = False,
|
|
115
|
+
) -> str | None:
|
|
116
|
+
if value is None:
|
|
117
|
+
return None
|
|
118
|
+
if not isinstance(value, str):
|
|
119
|
+
raise TypeError(f"{field_name} must be a string.")
|
|
120
|
+
cleaned = value.strip()
|
|
121
|
+
if not cleaned:
|
|
122
|
+
return None
|
|
123
|
+
if lowercase:
|
|
124
|
+
cleaned = cleaned.lower()
|
|
125
|
+
return cleaned
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def normalize_timeout(value: object, *, field_name: str) -> float:
|
|
129
|
+
if isinstance(value, bool):
|
|
130
|
+
raise TypeError(f"{field_name} must be a positive number.")
|
|
131
|
+
if not isinstance(value, (int, float)):
|
|
132
|
+
raise TypeError(f"{field_name} must be a positive number.")
|
|
133
|
+
normalized = float(value)
|
|
134
|
+
if normalized <= 0:
|
|
135
|
+
raise ValueError(f"{field_name} must be > 0.")
|
|
136
|
+
return normalized
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def normalize_roots_mode(value: object, *, field_name: str) -> RootsMode:
|
|
140
|
+
normalized = normalize_optional_string(value, field_name=field_name, lowercase=True)
|
|
141
|
+
if normalized is None:
|
|
142
|
+
raise ValueError(f"{field_name} cannot be empty.")
|
|
143
|
+
if normalized not in {"disabled", "workspace"}:
|
|
144
|
+
raise ValueError(f"{field_name} must be 'disabled' or 'workspace'.")
|
|
145
|
+
return cast(RootsMode, normalized)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def normalize_resources_mode(value: object, *, field_name: str) -> ResourcesMode:
|
|
149
|
+
normalized = normalize_optional_string(value, field_name=field_name, lowercase=True)
|
|
150
|
+
if normalized is None:
|
|
151
|
+
raise ValueError(f"{field_name} cannot be empty.")
|
|
152
|
+
if normalized not in {"disabled", "listed_read_only"}:
|
|
153
|
+
raise ValueError(f"{field_name} must be 'disabled' or 'listed_read_only'.")
|
|
154
|
+
return cast(ResourcesMode, normalized)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def normalize_prompts_mode(value: object, *, field_name: str) -> PromptsMode:
|
|
158
|
+
normalized = normalize_optional_string(value, field_name=field_name, lowercase=True)
|
|
159
|
+
if normalized is None:
|
|
160
|
+
raise ValueError(f"{field_name} cannot be empty.")
|
|
161
|
+
if normalized not in {"disabled", "listed_get_only"}:
|
|
162
|
+
raise ValueError(f"{field_name} must be 'disabled' or 'listed_get_only'.")
|
|
163
|
+
return cast(PromptsMode, normalized)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def normalize_runtime_kind_list(value: object) -> list[RuntimeKind] | None:
|
|
167
|
+
if value is None:
|
|
168
|
+
return None
|
|
169
|
+
if not isinstance(value, list):
|
|
170
|
+
raise TypeError("enabled_in must be a list of runtime kinds.")
|
|
171
|
+
normalized: list[RuntimeKind] = []
|
|
172
|
+
seen: set[RuntimeKind] = set()
|
|
173
|
+
for item in value:
|
|
174
|
+
try:
|
|
175
|
+
kind = normalize_runtime_kind(item)
|
|
176
|
+
except AppConfigError as exc:
|
|
177
|
+
raise ValueError(str(exc)) from exc
|
|
178
|
+
if kind in seen:
|
|
179
|
+
continue
|
|
180
|
+
seen.add(kind)
|
|
181
|
+
normalized.append(kind)
|
|
182
|
+
return normalized
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def normalize_string_mapping(
|
|
186
|
+
value: object,
|
|
187
|
+
*,
|
|
188
|
+
field_name: str,
|
|
189
|
+
key_validator: Callable[[str], None] | None = None,
|
|
190
|
+
) -> dict[str, str] | None:
|
|
191
|
+
if value is None:
|
|
192
|
+
return None
|
|
193
|
+
if not isinstance(value, dict):
|
|
194
|
+
raise TypeError(f"{field_name} must be an object mapping strings to strings.")
|
|
195
|
+
normalized: dict[str, str] = {}
|
|
196
|
+
for raw_key, raw_item in value.items():
|
|
197
|
+
if not isinstance(raw_key, str):
|
|
198
|
+
raise TypeError(f"{field_name} keys must be strings.")
|
|
199
|
+
if not isinstance(raw_item, str):
|
|
200
|
+
raise TypeError(f"{field_name} values must be strings.")
|
|
201
|
+
key = raw_key.strip()
|
|
202
|
+
if not key:
|
|
203
|
+
raise ValueError(f"{field_name} keys cannot be empty.")
|
|
204
|
+
if key_validator is not None:
|
|
205
|
+
key_validator(key)
|
|
206
|
+
normalized[key] = raw_item.strip()
|
|
207
|
+
return normalized
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def validate_http_url(value: str, *, field_name: str) -> str:
|
|
211
|
+
split = urlsplit(value)
|
|
212
|
+
if split.scheme not in {"http", "https"} or not split.netloc:
|
|
213
|
+
raise ValueError(f"{field_name} must be an http:// or https:// URL.")
|
|
214
|
+
return value
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _is_loopback_host(hostname: str | None) -> bool:
|
|
218
|
+
cleaned = str(hostname or "").strip()
|
|
219
|
+
if not cleaned:
|
|
220
|
+
return False
|
|
221
|
+
if cleaned.lower() == "localhost":
|
|
222
|
+
return True
|
|
223
|
+
try:
|
|
224
|
+
return ip_address(cleaned).is_loopback
|
|
225
|
+
except ValueError:
|
|
226
|
+
return False
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def validate_https_url(
|
|
230
|
+
value: str,
|
|
231
|
+
*,
|
|
232
|
+
field_name: str,
|
|
233
|
+
allow_loopback_http: bool = False,
|
|
234
|
+
) -> str:
|
|
235
|
+
split = urlsplit(value)
|
|
236
|
+
if not split.netloc:
|
|
237
|
+
raise ValueError(f"{field_name} must be an absolute URL.")
|
|
238
|
+
if split.scheme == "https":
|
|
239
|
+
return value
|
|
240
|
+
if split.scheme == "http" and allow_loopback_http and _is_loopback_host(split.hostname):
|
|
241
|
+
return value
|
|
242
|
+
if allow_loopback_http:
|
|
243
|
+
raise ValueError(
|
|
244
|
+
f"{field_name} must be an https:// URL (http:// is only allowed for loopback hosts)."
|
|
245
|
+
)
|
|
246
|
+
raise ValueError(f"{field_name} must be an https:// URL.")
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def validate_tcp_port(value: object, *, field_name: str) -> int:
|
|
250
|
+
if isinstance(value, bool):
|
|
251
|
+
raise TypeError(f"{field_name} must be a valid TCP port.")
|
|
252
|
+
if not isinstance(value, int):
|
|
253
|
+
raise TypeError(f"{field_name} must be a valid TCP port.")
|
|
254
|
+
if not 1 <= int(value) <= 65535:
|
|
255
|
+
raise ValueError(f"{field_name} must be between 1 and 65535.")
|
|
256
|
+
return int(value)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def validate_redirect_host(value: str, *, field_name: str) -> str:
|
|
260
|
+
if any(marker in value for marker in ("://", "/", "?", "#", "@")):
|
|
261
|
+
raise ValueError(f"{field_name} must be a hostname or IP address without a port.")
|
|
262
|
+
if any(ch.isspace() for ch in value):
|
|
263
|
+
raise ValueError(f"{field_name} must be a hostname or IP address without whitespace.")
|
|
264
|
+
if ":" in value:
|
|
265
|
+
try:
|
|
266
|
+
ip_address(value)
|
|
267
|
+
except ValueError as exc:
|
|
268
|
+
raise ValueError(
|
|
269
|
+
f"{field_name} must be a hostname or IP address without a port."
|
|
270
|
+
) from exc
|
|
271
|
+
return value
|
|
272
|
+
try:
|
|
273
|
+
ip_address(value)
|
|
274
|
+
return value
|
|
275
|
+
except ValueError:
|
|
276
|
+
pass
|
|
277
|
+
labels = value.split(".")
|
|
278
|
+
if any(not label for label in labels):
|
|
279
|
+
raise ValueError(f"{field_name} must be a hostname or IP address without a port.")
|
|
280
|
+
for label in labels:
|
|
281
|
+
if len(label) > 63 or not re.fullmatch(r"[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?", label):
|
|
282
|
+
raise ValueError(f"{field_name} must be a hostname or IP address without a port.")
|
|
283
|
+
return value
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def validate_env_name(name: str) -> None:
|
|
287
|
+
if not _ENV_NAME_RE.fullmatch(name):
|
|
288
|
+
raise ValueError("env keys must be valid environment variable names.")
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def validate_tool_policy_overlap(
|
|
292
|
+
*,
|
|
293
|
+
server_id: str,
|
|
294
|
+
allowed_tools: tuple[str, ...],
|
|
295
|
+
denied_tools: tuple[str, ...],
|
|
296
|
+
) -> None:
|
|
297
|
+
overlap = sorted(set(allowed_tools) & set(denied_tools))
|
|
298
|
+
if overlap:
|
|
299
|
+
joined = ", ".join(overlap)
|
|
300
|
+
raise ConfigError(
|
|
301
|
+
f"MCP server '{server_id}' cannot allow and deny the same tool(s): {joined}"
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
class _StrictModel(BaseModel):
|
|
306
|
+
model_config = ConfigDict(extra="forbid")
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
class _PolicyModel(_StrictModel):
|
|
310
|
+
enabled: StrictBool = True
|
|
311
|
+
enabled_in: list[RuntimeKind] | None = None
|
|
312
|
+
trust: StrictStr = "explicit"
|
|
313
|
+
allowed_tools: list[StrictStr] = Field(default_factory=list)
|
|
314
|
+
denied_tools: list[StrictStr] = Field(default_factory=list)
|
|
315
|
+
startup_timeout_s: float = 10.0
|
|
316
|
+
call_timeout_s: float = 60.0
|
|
317
|
+
tool_prefix: StrictStr | None = None
|
|
318
|
+
roots_mode: RootsMode = "disabled"
|
|
319
|
+
resources_mode: ResourcesMode = "disabled"
|
|
320
|
+
prompts_mode: PromptsMode = "disabled"
|
|
321
|
+
|
|
322
|
+
@field_validator("enabled_in", mode="before")
|
|
323
|
+
@classmethod
|
|
324
|
+
def _normalize_enabled_in(cls, value: object) -> list[RuntimeKind] | None:
|
|
325
|
+
return normalize_runtime_kind_list(value)
|
|
326
|
+
|
|
327
|
+
@field_validator("trust", mode="before")
|
|
328
|
+
@classmethod
|
|
329
|
+
def _normalize_trust(cls, value: object) -> str:
|
|
330
|
+
normalized = normalize_optional_string(value, field_name="trust", lowercase=True)
|
|
331
|
+
if normalized is None:
|
|
332
|
+
raise ValueError("trust cannot be empty.")
|
|
333
|
+
if normalized != "explicit":
|
|
334
|
+
raise ValueError("trust must be 'explicit'; no other MCP trust modes are supported.")
|
|
335
|
+
return normalized
|
|
336
|
+
|
|
337
|
+
@field_validator("allowed_tools", "denied_tools", mode="before")
|
|
338
|
+
@classmethod
|
|
339
|
+
def _normalize_tool_lists(cls, value: object, info: Any) -> list[str]:
|
|
340
|
+
return normalize_ordered_name_list(value, field_name=str(info.field_name))
|
|
341
|
+
|
|
342
|
+
@field_validator("startup_timeout_s", "call_timeout_s", mode="before")
|
|
343
|
+
@classmethod
|
|
344
|
+
def _normalize_timeouts(cls, value: object, info: Any) -> float:
|
|
345
|
+
return normalize_timeout(value, field_name=str(info.field_name))
|
|
346
|
+
|
|
347
|
+
@field_validator("tool_prefix", mode="before")
|
|
348
|
+
@classmethod
|
|
349
|
+
def _normalize_tool_prefix(cls, value: object) -> str | None:
|
|
350
|
+
normalized = normalize_optional_string(value, field_name="tool_prefix", lowercase=True)
|
|
351
|
+
if normalized is None:
|
|
352
|
+
return None
|
|
353
|
+
if not _TOOL_PREFIX_RE.fullmatch(normalized):
|
|
354
|
+
raise ValueError("tool_prefix must match [a-z0-9][a-z0-9_-]*.")
|
|
355
|
+
return normalized
|
|
356
|
+
|
|
357
|
+
@field_validator("roots_mode", mode="before")
|
|
358
|
+
@classmethod
|
|
359
|
+
def _normalize_roots_mode(cls, value: object) -> RootsMode:
|
|
360
|
+
return normalize_roots_mode(value, field_name="roots_mode")
|
|
361
|
+
|
|
362
|
+
@field_validator("resources_mode", mode="before")
|
|
363
|
+
@classmethod
|
|
364
|
+
def _normalize_resources_mode(cls, value: object) -> ResourcesMode:
|
|
365
|
+
return normalize_resources_mode(value, field_name="resources_mode")
|
|
366
|
+
|
|
367
|
+
@field_validator("prompts_mode", mode="before")
|
|
368
|
+
@classmethod
|
|
369
|
+
def _normalize_prompts_mode(cls, value: object) -> PromptsMode:
|
|
370
|
+
return normalize_prompts_mode(value, field_name="prompts_mode")
|
|
371
|
+
|
|
372
|
+
@model_validator(mode="after")
|
|
373
|
+
def _validate_tool_policy_overlap(self) -> _PolicyModel:
|
|
374
|
+
overlap = sorted(set(self.allowed_tools) & set(self.denied_tools))
|
|
375
|
+
if overlap:
|
|
376
|
+
joined = ", ".join(overlap)
|
|
377
|
+
raise ValueError(f"allowed_tools and denied_tools cannot overlap: {joined}")
|
|
378
|
+
return self
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
class UserMcpStdioServer(_PolicyModel):
|
|
382
|
+
transport: Literal["stdio"]
|
|
383
|
+
command: StrictStr
|
|
384
|
+
args: list[StrictStr] = Field(default_factory=list)
|
|
385
|
+
env: dict[StrictStr, StrictStr] | None = None
|
|
386
|
+
|
|
387
|
+
@field_validator("command", mode="before")
|
|
388
|
+
@classmethod
|
|
389
|
+
def _normalize_command(cls, value: object) -> str:
|
|
390
|
+
normalized = normalize_optional_string(value, field_name="command")
|
|
391
|
+
if normalized is None:
|
|
392
|
+
raise ValueError("command cannot be empty.")
|
|
393
|
+
return normalized
|
|
394
|
+
|
|
395
|
+
@field_validator("args", mode="before")
|
|
396
|
+
@classmethod
|
|
397
|
+
def _normalize_args(cls, value: object) -> list[str]:
|
|
398
|
+
return normalize_ordered_args_list(value, field_name="args")
|
|
399
|
+
|
|
400
|
+
@field_validator("env", mode="before")
|
|
401
|
+
@classmethod
|
|
402
|
+
def _normalize_env(cls, value: object) -> dict[str, str] | None:
|
|
403
|
+
return normalize_string_mapping(value, field_name="env", key_validator=validate_env_name)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
class UserMcpHttpOAuthConfig(_StrictModel):
|
|
407
|
+
client_id: StrictStr
|
|
408
|
+
redirect_host: StrictStr | None = None
|
|
409
|
+
redirect_port: StrictInt | None = None
|
|
410
|
+
scopes: list[StrictStr] | None = None
|
|
411
|
+
authorization_server_url: StrictStr | None = None
|
|
412
|
+
|
|
413
|
+
@field_validator("client_id", mode="before")
|
|
414
|
+
@classmethod
|
|
415
|
+
def _normalize_client_id(cls, value: object) -> str:
|
|
416
|
+
normalized = normalize_optional_string(value, field_name="client_id")
|
|
417
|
+
if normalized is None:
|
|
418
|
+
raise ValueError("client_id cannot be empty.")
|
|
419
|
+
return normalized
|
|
420
|
+
|
|
421
|
+
@field_validator("redirect_host", mode="before")
|
|
422
|
+
@classmethod
|
|
423
|
+
def _normalize_redirect_host(cls, value: object) -> str | None:
|
|
424
|
+
normalized = normalize_optional_string(value, field_name="redirect_host")
|
|
425
|
+
if normalized is None:
|
|
426
|
+
return None
|
|
427
|
+
return validate_redirect_host(normalized, field_name="redirect_host")
|
|
428
|
+
|
|
429
|
+
@field_validator("redirect_port", mode="before")
|
|
430
|
+
@classmethod
|
|
431
|
+
def _normalize_redirect_port(cls, value: object) -> int | None:
|
|
432
|
+
if value is None:
|
|
433
|
+
return None
|
|
434
|
+
return validate_tcp_port(value, field_name="redirect_port")
|
|
435
|
+
|
|
436
|
+
@field_validator("scopes", mode="before")
|
|
437
|
+
@classmethod
|
|
438
|
+
def _normalize_scopes(cls, value: object) -> list[str] | None:
|
|
439
|
+
if value is None:
|
|
440
|
+
return None
|
|
441
|
+
return normalize_ordered_string_list(value, field_name="scopes")
|
|
442
|
+
|
|
443
|
+
@field_validator("authorization_server_url", mode="before")
|
|
444
|
+
@classmethod
|
|
445
|
+
def _normalize_authorization_server_url(cls, value: object) -> str | None:
|
|
446
|
+
normalized = normalize_optional_string(value, field_name="authorization_server_url")
|
|
447
|
+
if normalized is None:
|
|
448
|
+
return None
|
|
449
|
+
return validate_https_url(
|
|
450
|
+
normalized,
|
|
451
|
+
field_name="authorization_server_url",
|
|
452
|
+
allow_loopback_http=True,
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
class UserMcpHttpServer(_PolicyModel):
|
|
457
|
+
transport: Literal["http"]
|
|
458
|
+
url: StrictStr
|
|
459
|
+
headers: dict[StrictStr, StrictStr] | None = None
|
|
460
|
+
oauth: UserMcpHttpOAuthConfig | None = None
|
|
461
|
+
|
|
462
|
+
@field_validator("url", mode="before")
|
|
463
|
+
@classmethod
|
|
464
|
+
def _normalize_url(cls, value: object) -> str:
|
|
465
|
+
normalized = normalize_optional_string(value, field_name="url")
|
|
466
|
+
if normalized is None:
|
|
467
|
+
raise ValueError("url cannot be empty.")
|
|
468
|
+
return validate_https_url(normalized, field_name="url", allow_loopback_http=True)
|
|
469
|
+
|
|
470
|
+
@field_validator("headers", mode="before")
|
|
471
|
+
@classmethod
|
|
472
|
+
def _normalize_headers(cls, value: object) -> dict[str, str] | None:
|
|
473
|
+
return normalize_string_mapping(value, field_name="headers")
|
|
474
|
+
|
|
475
|
+
@model_validator(mode="after")
|
|
476
|
+
def _validate_auth_configuration(self) -> UserMcpHttpServer:
|
|
477
|
+
if self.oauth is None or not self.headers:
|
|
478
|
+
return self
|
|
479
|
+
if any(str(key).casefold() == "authorization" for key in self.headers):
|
|
480
|
+
raise ValueError(
|
|
481
|
+
"oauth cannot be combined with headers.Authorization on the same HTTP server."
|
|
482
|
+
)
|
|
483
|
+
return self
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
UserMcpServerModel = Annotated[
|
|
487
|
+
UserMcpStdioServer | UserMcpHttpServer, Field(discriminator="transport")
|
|
488
|
+
]
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
class ProjectMcpServerOverride(_StrictModel):
|
|
492
|
+
enabled: StrictBool | None = None
|
|
493
|
+
enabled_in: list[RuntimeKind] | None = None
|
|
494
|
+
allowed_tools: list[StrictStr] | None = None
|
|
495
|
+
denied_tools: list[StrictStr] | None = None
|
|
496
|
+
startup_timeout_s: float | None = None
|
|
497
|
+
call_timeout_s: float | None = None
|
|
498
|
+
roots_mode: RootsMode | None = None
|
|
499
|
+
resources_mode: ResourcesMode | None = None
|
|
500
|
+
prompts_mode: PromptsMode | None = None
|
|
501
|
+
|
|
502
|
+
@field_validator("enabled_in", mode="before")
|
|
503
|
+
@classmethod
|
|
504
|
+
def _normalize_enabled_in(cls, value: object) -> list[RuntimeKind] | None:
|
|
505
|
+
return normalize_runtime_kind_list(value)
|
|
506
|
+
|
|
507
|
+
@field_validator("allowed_tools", "denied_tools", mode="before")
|
|
508
|
+
@classmethod
|
|
509
|
+
def _normalize_tool_lists(cls, value: object, info: Any) -> list[str] | None:
|
|
510
|
+
if value is None:
|
|
511
|
+
return None
|
|
512
|
+
return normalize_ordered_name_list(value, field_name=str(info.field_name))
|
|
513
|
+
|
|
514
|
+
@field_validator("startup_timeout_s", "call_timeout_s", mode="before")
|
|
515
|
+
@classmethod
|
|
516
|
+
def _normalize_timeouts(cls, value: object, info: Any) -> float | None:
|
|
517
|
+
if value is None:
|
|
518
|
+
return None
|
|
519
|
+
return normalize_timeout(value, field_name=str(info.field_name))
|
|
520
|
+
|
|
521
|
+
@field_validator("roots_mode", mode="before")
|
|
522
|
+
@classmethod
|
|
523
|
+
def _normalize_roots_mode(cls, value: object) -> RootsMode | None:
|
|
524
|
+
if value is None:
|
|
525
|
+
return None
|
|
526
|
+
return normalize_roots_mode(value, field_name="roots_mode")
|
|
527
|
+
|
|
528
|
+
@field_validator("resources_mode", mode="before")
|
|
529
|
+
@classmethod
|
|
530
|
+
def _normalize_resources_mode(cls, value: object) -> ResourcesMode | None:
|
|
531
|
+
if value is None:
|
|
532
|
+
return None
|
|
533
|
+
return normalize_resources_mode(value, field_name="resources_mode")
|
|
534
|
+
|
|
535
|
+
@field_validator("prompts_mode", mode="before")
|
|
536
|
+
@classmethod
|
|
537
|
+
def _normalize_prompts_mode(cls, value: object) -> PromptsMode | None:
|
|
538
|
+
if value is None:
|
|
539
|
+
return None
|
|
540
|
+
return normalize_prompts_mode(value, field_name="prompts_mode")
|
|
541
|
+
|
|
542
|
+
@model_validator(mode="after")
|
|
543
|
+
def _validate_tool_policy_overlap(self) -> ProjectMcpServerOverride:
|
|
544
|
+
allowed_tools = tuple(self.allowed_tools or [])
|
|
545
|
+
denied_tools = tuple(self.denied_tools or [])
|
|
546
|
+
overlap = sorted(set(allowed_tools) & set(denied_tools))
|
|
547
|
+
if overlap:
|
|
548
|
+
joined = ", ".join(overlap)
|
|
549
|
+
raise ValueError(f"allowed_tools and denied_tools cannot overlap: {joined}")
|
|
550
|
+
return self
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
class UserMcpConfigFile(_StrictModel):
|
|
554
|
+
schema_version: StrictInt = 1
|
|
555
|
+
servers: dict[str, UserMcpServerModel] = Field(default_factory=dict)
|
|
556
|
+
|
|
557
|
+
@field_validator("schema_version")
|
|
558
|
+
@classmethod
|
|
559
|
+
def _validate_schema_version(cls, value: int) -> int:
|
|
560
|
+
if value != 1:
|
|
561
|
+
raise ValueError("schema_version must equal 1.")
|
|
562
|
+
return value
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
class ProjectMcpConfigFile(_StrictModel):
|
|
566
|
+
schema_version: StrictInt = 1
|
|
567
|
+
servers: dict[str, ProjectMcpServerOverride] = Field(default_factory=dict)
|
|
568
|
+
|
|
569
|
+
@field_validator("schema_version")
|
|
570
|
+
@classmethod
|
|
571
|
+
def _validate_schema_version(cls, value: int) -> int:
|
|
572
|
+
if value != 1:
|
|
573
|
+
raise ValueError("schema_version must equal 1.")
|
|
574
|
+
return value
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def _redact_sensitive_strings(values: tuple[str, ...]) -> list[str]:
|
|
578
|
+
return ["[redacted]" for _ in values]
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
@dataclass(frozen=True)
|
|
582
|
+
class ResolvedMcpHttpOAuthConfig:
|
|
583
|
+
client_id: str
|
|
584
|
+
redirect_host: str | None = None
|
|
585
|
+
redirect_port: int | None = None
|
|
586
|
+
scopes: tuple[str, ...] = field(default_factory=tuple)
|
|
587
|
+
authorization_server_url: str | None = None
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
@dataclass(frozen=True)
|
|
591
|
+
class ResolvedMcpServer:
|
|
592
|
+
id: str
|
|
593
|
+
transport: TransportKind
|
|
594
|
+
enabled: bool
|
|
595
|
+
enabled_in: tuple[RuntimeKind, ...] | None
|
|
596
|
+
trust: str
|
|
597
|
+
allowed_tools: tuple[str, ...]
|
|
598
|
+
denied_tools: tuple[str, ...]
|
|
599
|
+
startup_timeout_s: float
|
|
600
|
+
call_timeout_s: float
|
|
601
|
+
tool_prefix: str | None
|
|
602
|
+
roots_mode: RootsMode = "disabled"
|
|
603
|
+
resources_mode: ResourcesMode = "disabled"
|
|
604
|
+
prompts_mode: PromptsMode = "disabled"
|
|
605
|
+
command: str | None = None
|
|
606
|
+
args: tuple[str, ...] = field(default_factory=tuple, repr=False)
|
|
607
|
+
env: dict[str, str] = field(default_factory=dict, repr=False)
|
|
608
|
+
url: str | None = field(default=None, repr=False)
|
|
609
|
+
headers: dict[str, str] = field(default_factory=dict, repr=False)
|
|
610
|
+
oauth: ResolvedMcpHttpOAuthConfig | None = field(default=None, repr=False)
|
|
611
|
+
|
|
612
|
+
def enabled_for(self, runtime_kind: RuntimeKind) -> bool:
|
|
613
|
+
if not self.enabled:
|
|
614
|
+
return False
|
|
615
|
+
if self.enabled_in is None:
|
|
616
|
+
return True
|
|
617
|
+
return runtime_kind in self.enabled_in
|
|
618
|
+
|
|
619
|
+
def redacted_connection_payload(self) -> dict[str, Any]:
|
|
620
|
+
payload: dict[str, Any] = {
|
|
621
|
+
"id": self.id,
|
|
622
|
+
"transport": self.transport,
|
|
623
|
+
"enabled": self.enabled,
|
|
624
|
+
"enabled_in": [kind.value for kind in self.enabled_in] if self.enabled_in else None,
|
|
625
|
+
"trust": self.trust,
|
|
626
|
+
"allowed_tools": list(self.allowed_tools),
|
|
627
|
+
"denied_tools": list(self.denied_tools),
|
|
628
|
+
"startup_timeout_s": self.startup_timeout_s,
|
|
629
|
+
"call_timeout_s": self.call_timeout_s,
|
|
630
|
+
"tool_prefix": self.tool_prefix,
|
|
631
|
+
"roots_mode": self.roots_mode,
|
|
632
|
+
"resources_mode": self.resources_mode,
|
|
633
|
+
"prompts_mode": self.prompts_mode,
|
|
634
|
+
}
|
|
635
|
+
if self.transport == "stdio":
|
|
636
|
+
payload["command"] = self.command
|
|
637
|
+
payload["args"] = _redact_sensitive_strings(self.args)
|
|
638
|
+
payload["env"] = {key: "[redacted]" for key in self.env}
|
|
639
|
+
else:
|
|
640
|
+
payload["headers"] = {key: "[redacted]" for key in self.headers}
|
|
641
|
+
if self.oauth is not None:
|
|
642
|
+
payload["oauth"] = {
|
|
643
|
+
"client_id": self.oauth.client_id,
|
|
644
|
+
"redirect_host": self.oauth.redirect_host,
|
|
645
|
+
"redirect_port": self.oauth.redirect_port,
|
|
646
|
+
"scopes": list(self.oauth.scopes),
|
|
647
|
+
"authorization_server_url": self.oauth.authorization_server_url,
|
|
648
|
+
}
|
|
649
|
+
return payload
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
@dataclass(frozen=True)
|
|
653
|
+
class ResolvedMcpConfig:
|
|
654
|
+
workspace_root: Path
|
|
655
|
+
user_config_path: Path
|
|
656
|
+
project_config_path: Path
|
|
657
|
+
user_config_present: bool
|
|
658
|
+
project_config_present: bool
|
|
659
|
+
servers: tuple[ResolvedMcpServer, ...] = ()
|
|
660
|
+
|
|
661
|
+
@property
|
|
662
|
+
def has_any_config(self) -> bool:
|
|
663
|
+
return self.user_config_present or self.project_config_present
|
|
664
|
+
|
|
665
|
+
def active_servers_for(self, runtime_kind: RuntimeKind) -> tuple[ResolvedMcpServer, ...]:
|
|
666
|
+
return tuple(server for server in self.servers if server.enabled_for(runtime_kind))
|