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,3414 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import platform
|
|
6
|
+
import re
|
|
7
|
+
import shutil
|
|
8
|
+
import sys
|
|
9
|
+
from collections.abc import Callable, Mapping
|
|
10
|
+
from dataclasses import asdict, is_dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
from urllib.parse import urlparse
|
|
14
|
+
|
|
15
|
+
from .. import __version__
|
|
16
|
+
from ..cli_impl.commands.hooks import (
|
|
17
|
+
_HOOK_SESSION_SOURCES,
|
|
18
|
+
_HOOKS_INIT_TEMPLATE,
|
|
19
|
+
_collect_hook_source_statuses,
|
|
20
|
+
_find_hook_in_layer,
|
|
21
|
+
_hook_test_match_result,
|
|
22
|
+
_hooks_config_path_for_layer,
|
|
23
|
+
)
|
|
24
|
+
from ..cli_impl.commands.mcp import (
|
|
25
|
+
_build_mcp_auth_status_rows,
|
|
26
|
+
_manual_mcp_manager_for_path,
|
|
27
|
+
_manual_mcp_resolved_config_for_path,
|
|
28
|
+
_mcp_status_payload,
|
|
29
|
+
_parse_manual_mcp_prompt_runtime,
|
|
30
|
+
_require_http_oauth_server_for_auth,
|
|
31
|
+
_resolve_mcp_server_by_id,
|
|
32
|
+
)
|
|
33
|
+
from ..config import (
|
|
34
|
+
AppConfig,
|
|
35
|
+
ConfigError,
|
|
36
|
+
clear_persisted_profile_key,
|
|
37
|
+
config_path,
|
|
38
|
+
credentials_path,
|
|
39
|
+
load_config,
|
|
40
|
+
rename_persisted_profile_key,
|
|
41
|
+
resolve_api_key,
|
|
42
|
+
resolve_profile_api_key,
|
|
43
|
+
save_config,
|
|
44
|
+
set_config_value,
|
|
45
|
+
)
|
|
46
|
+
from ..custom_tools import (
|
|
47
|
+
CustomToolCatalogEntry,
|
|
48
|
+
global_custom_tools_root,
|
|
49
|
+
project_custom_tools_root,
|
|
50
|
+
trust_project_tool,
|
|
51
|
+
untrust_project_tool,
|
|
52
|
+
)
|
|
53
|
+
from ..extensions.install import (
|
|
54
|
+
PluginInstallError,
|
|
55
|
+
disable_plugin,
|
|
56
|
+
enable_plugin,
|
|
57
|
+
install_plugin,
|
|
58
|
+
uninstall_plugin,
|
|
59
|
+
)
|
|
60
|
+
from ..extensions.models import normalize_extension_id
|
|
61
|
+
from ..extensions.paths import project_extensions_path
|
|
62
|
+
from ..extensions.registry import find_by_id, load_registry
|
|
63
|
+
from ..extensions.registry import search as search_extensions
|
|
64
|
+
from ..extensions.state import (
|
|
65
|
+
compute_effective_enabled,
|
|
66
|
+
load_global_state,
|
|
67
|
+
load_project_overrides,
|
|
68
|
+
load_project_state,
|
|
69
|
+
)
|
|
70
|
+
from ..extensions.workspace_trust import is_workspace_trusted as extension_workspace_trusted
|
|
71
|
+
from ..feedback_report import (
|
|
72
|
+
FeedbackReportError,
|
|
73
|
+
create_feedback_bundle,
|
|
74
|
+
create_feedback_github_issue_draft,
|
|
75
|
+
feedback_github_issue_status_lines,
|
|
76
|
+
resolve_feedback_workspace_root,
|
|
77
|
+
)
|
|
78
|
+
from ..hooks import (
|
|
79
|
+
canonicalize_hook_event_name,
|
|
80
|
+
hook_audit_artifact_path,
|
|
81
|
+
load_hook_config_file,
|
|
82
|
+
load_resolved_hooks_config,
|
|
83
|
+
project_hooks_config_path,
|
|
84
|
+
project_local_hooks_config_path,
|
|
85
|
+
read_hook_audit_events,
|
|
86
|
+
trust_project_hooks_config,
|
|
87
|
+
untrust_project_hooks_config,
|
|
88
|
+
)
|
|
89
|
+
from ..mcp.oauth_store import delete_oauth_token_record
|
|
90
|
+
from ..profile_presets import (
|
|
91
|
+
PROFILE_PRESETS,
|
|
92
|
+
convert_profile_to_preset,
|
|
93
|
+
get_preset,
|
|
94
|
+
make_profile_from_preset,
|
|
95
|
+
normalize_conversion_target,
|
|
96
|
+
preset_protocol_kind,
|
|
97
|
+
preset_selection_label,
|
|
98
|
+
target_preset_for_profile_conversion,
|
|
99
|
+
)
|
|
100
|
+
from ..profiles import (
|
|
101
|
+
SUBSCRIPTION_SELECTION_REQUIRED_KEY,
|
|
102
|
+
ProfileSpec,
|
|
103
|
+
active_subscription_selection_ready,
|
|
104
|
+
add_profile,
|
|
105
|
+
get_active_profile,
|
|
106
|
+
get_profile,
|
|
107
|
+
list_profiles,
|
|
108
|
+
remove_profile,
|
|
109
|
+
rename_profile,
|
|
110
|
+
set_active_profile,
|
|
111
|
+
subscription_selection_supported,
|
|
112
|
+
update_active_profile_defaults,
|
|
113
|
+
update_profile,
|
|
114
|
+
)
|
|
115
|
+
from ..provider_auth import create_provider_auth
|
|
116
|
+
from ..provider_diagnostics import build_provider_diagnostics, validate_active_provider_live
|
|
117
|
+
from ..provider_telemetry import (
|
|
118
|
+
diagnostic_bundle_payload,
|
|
119
|
+
last_provider_call_summary,
|
|
120
|
+
last_web_search_summary,
|
|
121
|
+
)
|
|
122
|
+
from ..runtime_kind import RuntimeKind
|
|
123
|
+
from ..sandbox_doctor import (
|
|
124
|
+
configured_sandbox_images,
|
|
125
|
+
diagnose_sandbox,
|
|
126
|
+
format_sandbox_problem_message,
|
|
127
|
+
pull_sandbox_images,
|
|
128
|
+
sandbox_env_summary,
|
|
129
|
+
)
|
|
130
|
+
from ..session_metrics import score_session_log
|
|
131
|
+
from ..session_store import (
|
|
132
|
+
list_sessions,
|
|
133
|
+
read_session_events,
|
|
134
|
+
resolve_sessions_dir,
|
|
135
|
+
sanitize_session_id,
|
|
136
|
+
)
|
|
137
|
+
from ..skills import (
|
|
138
|
+
SkillLifecycleError,
|
|
139
|
+
discover_skills,
|
|
140
|
+
install_skill_bundle,
|
|
141
|
+
load_global_skill_state,
|
|
142
|
+
load_project_skill_state,
|
|
143
|
+
load_repo_conventions,
|
|
144
|
+
remove_managed_skill,
|
|
145
|
+
render_repo_conventions_context,
|
|
146
|
+
render_skill_info_text,
|
|
147
|
+
resolve_skill_catalog,
|
|
148
|
+
resolve_skill_catalog_entry,
|
|
149
|
+
save_global_skill_state,
|
|
150
|
+
save_project_skill_state,
|
|
151
|
+
scaffold_skill_bundle,
|
|
152
|
+
set_global_skill_disabled,
|
|
153
|
+
set_project_skill_override,
|
|
154
|
+
validate_skill_bundle,
|
|
155
|
+
)
|
|
156
|
+
from ..tools.availability import get_tool_availability
|
|
157
|
+
from ..tools.registry import iter_builtin_tool_metadata
|
|
158
|
+
from ..tools.web_search import resolve_web_search_runtime_status
|
|
159
|
+
from ..updates import check_for_updates, status_from_cache
|
|
160
|
+
from ..usage_tracker import aggregate_usage_from_session_logs
|
|
161
|
+
from ..workspace_binding import WorkspaceAction, WorkspaceBindingError
|
|
162
|
+
from ..workspace_binding_ui import resolve_startup_workspace_binding
|
|
163
|
+
from ..workspace_context import WorkspaceContextError
|
|
164
|
+
from .mcp_oauth_coordinator import McpOAuthLoginRequest
|
|
165
|
+
from .protocol import ProtocolError, RequestId, redact_secrets
|
|
166
|
+
|
|
167
|
+
_SESSION_SHOW_DEFAULT_MAX_EVENTS = 200
|
|
168
|
+
_SESSION_SHOW_MAX_EVENTS = 1_000
|
|
169
|
+
_SESSION_SHOW_DEFAULT_MAX_TOTAL_BYTES = 256 * 1024
|
|
170
|
+
_SESSION_SHOW_MAX_TOTAL_BYTES = 1024 * 1024
|
|
171
|
+
_CONVENTIONS_RENDER_DEFAULT_MAX_CHARS = 4_000
|
|
172
|
+
_CONVENTIONS_RENDER_MAX_CHARS = 100_000
|
|
173
|
+
_CONVENTIONS_RENDER_DEFAULT_MAX_BYTES = 64 * 1024
|
|
174
|
+
_CONVENTIONS_RENDER_MAX_BYTES = 256 * 1024
|
|
175
|
+
_SECRET_ASSIGNMENT_PATTERN = re.compile(
|
|
176
|
+
r"(?i)\b([A-Z0-9_.-]*(?:api[_-]?key|token|secret|password|credential)[A-Z0-9_.-]*\s*[:=]\s*)([^\s,;&]+)"
|
|
177
|
+
)
|
|
178
|
+
_URL_USERINFO_PATTERN = re.compile(
|
|
179
|
+
r"(?i)\b((?:git\+)?https?://)([^/@\s]+)@([A-Za-z0-9.-]+(?::\d+)?)"
|
|
180
|
+
)
|
|
181
|
+
_EXTENSION_REGISTRY_ID_RE = re.compile(r"^[a-z][a-z0-9_]*\.[a-z][a-z0-9_-]*$")
|
|
182
|
+
_SHA40_RE = re.compile(r"^[0-9a-fA-F]{40}$")
|
|
183
|
+
|
|
184
|
+
MANAGEMENT_METHODS = (
|
|
185
|
+
"config.get",
|
|
186
|
+
"config.set",
|
|
187
|
+
"config.schema",
|
|
188
|
+
"config.validate",
|
|
189
|
+
"profile.list",
|
|
190
|
+
"profile.show",
|
|
191
|
+
"profile.add",
|
|
192
|
+
"profile.remove",
|
|
193
|
+
"profile.use",
|
|
194
|
+
"profile.rename",
|
|
195
|
+
"profile.presets",
|
|
196
|
+
"profile.preset",
|
|
197
|
+
"profile.convert",
|
|
198
|
+
"session.show",
|
|
199
|
+
"session.usage",
|
|
200
|
+
"session.score",
|
|
201
|
+
"tools.catalog",
|
|
202
|
+
"tool.list",
|
|
203
|
+
"tool.info",
|
|
204
|
+
"tool.trust",
|
|
205
|
+
"tool.untrust",
|
|
206
|
+
"skill.list",
|
|
207
|
+
"skill.info",
|
|
208
|
+
"skill.init",
|
|
209
|
+
"skill.validate",
|
|
210
|
+
"skill.install",
|
|
211
|
+
"skill.enable",
|
|
212
|
+
"skill.disable",
|
|
213
|
+
"skill.remove",
|
|
214
|
+
"doctor.summary",
|
|
215
|
+
"doctor.providers",
|
|
216
|
+
"doctor.providers.live",
|
|
217
|
+
"doctor.bundle",
|
|
218
|
+
"sandbox.doctor",
|
|
219
|
+
"sandbox.setup",
|
|
220
|
+
"sandbox.pull",
|
|
221
|
+
"update.check",
|
|
222
|
+
"report.create",
|
|
223
|
+
"mcp.status",
|
|
224
|
+
"mcp.prompts.list",
|
|
225
|
+
"mcp.prompts.get",
|
|
226
|
+
"mcp.auth.status",
|
|
227
|
+
"mcp.auth.login.start",
|
|
228
|
+
"mcp.auth.login.status",
|
|
229
|
+
"mcp.auth.login.cancel",
|
|
230
|
+
"mcp.auth.logout",
|
|
231
|
+
"hooks.list",
|
|
232
|
+
"hooks.doctor",
|
|
233
|
+
"hooks.trace",
|
|
234
|
+
"hooks.test",
|
|
235
|
+
"hooks.trust",
|
|
236
|
+
"hooks.untrust",
|
|
237
|
+
"hooks.init",
|
|
238
|
+
"hooks.effective",
|
|
239
|
+
"hooks.enable",
|
|
240
|
+
"hooks.disable",
|
|
241
|
+
"conventions.list",
|
|
242
|
+
"conventions.render",
|
|
243
|
+
"ext.search",
|
|
244
|
+
"ext.list",
|
|
245
|
+
"ext.info",
|
|
246
|
+
"ext.install",
|
|
247
|
+
"ext.uninstall",
|
|
248
|
+
"ext.enable",
|
|
249
|
+
"ext.disable",
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
MUTATING_MANAGEMENT_METHODS = frozenset(
|
|
253
|
+
{
|
|
254
|
+
"config.set",
|
|
255
|
+
"profile.add",
|
|
256
|
+
"profile.remove",
|
|
257
|
+
"profile.use",
|
|
258
|
+
"profile.rename",
|
|
259
|
+
"profile.preset",
|
|
260
|
+
"profile.convert",
|
|
261
|
+
"tool.trust",
|
|
262
|
+
"tool.untrust",
|
|
263
|
+
"skill.init",
|
|
264
|
+
"skill.install",
|
|
265
|
+
"skill.enable",
|
|
266
|
+
"skill.disable",
|
|
267
|
+
"skill.remove",
|
|
268
|
+
"sandbox.setup",
|
|
269
|
+
"sandbox.pull",
|
|
270
|
+
"report.create",
|
|
271
|
+
"mcp.auth.login.start",
|
|
272
|
+
"mcp.auth.login.cancel",
|
|
273
|
+
"mcp.auth.logout",
|
|
274
|
+
"hooks.trust",
|
|
275
|
+
"hooks.untrust",
|
|
276
|
+
"hooks.init",
|
|
277
|
+
"hooks.enable",
|
|
278
|
+
"hooks.disable",
|
|
279
|
+
"ext.install",
|
|
280
|
+
"ext.uninstall",
|
|
281
|
+
"ext.enable",
|
|
282
|
+
"ext.disable",
|
|
283
|
+
}
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
WORKSPACE_REQUIRED_MANAGEMENT_METHODS = frozenset(
|
|
287
|
+
{
|
|
288
|
+
"tool.list",
|
|
289
|
+
"tool.info",
|
|
290
|
+
"tool.trust",
|
|
291
|
+
"tool.untrust",
|
|
292
|
+
"skill.list",
|
|
293
|
+
"skill.info",
|
|
294
|
+
"skill.init",
|
|
295
|
+
"skill.validate",
|
|
296
|
+
"skill.install",
|
|
297
|
+
"skill.enable",
|
|
298
|
+
"skill.disable",
|
|
299
|
+
"skill.remove",
|
|
300
|
+
"report.create",
|
|
301
|
+
"mcp.status",
|
|
302
|
+
"mcp.prompts.list",
|
|
303
|
+
"mcp.prompts.get",
|
|
304
|
+
"mcp.auth.status",
|
|
305
|
+
"mcp.auth.login.start",
|
|
306
|
+
"mcp.auth.login.status",
|
|
307
|
+
"mcp.auth.login.cancel",
|
|
308
|
+
"mcp.auth.logout",
|
|
309
|
+
"hooks.list",
|
|
310
|
+
"hooks.doctor",
|
|
311
|
+
"hooks.test",
|
|
312
|
+
"hooks.trust",
|
|
313
|
+
"hooks.untrust",
|
|
314
|
+
"hooks.init",
|
|
315
|
+
"hooks.effective",
|
|
316
|
+
"hooks.enable",
|
|
317
|
+
"hooks.disable",
|
|
318
|
+
"conventions.list",
|
|
319
|
+
"conventions.render",
|
|
320
|
+
"ext.info",
|
|
321
|
+
"ext.list",
|
|
322
|
+
"ext.install",
|
|
323
|
+
"ext.uninstall",
|
|
324
|
+
"ext.enable",
|
|
325
|
+
"ext.disable",
|
|
326
|
+
}
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
MANAGEMENT_METHOD_CAPABILITIES: dict[str, dict[str, Any]] = {
|
|
330
|
+
method: {
|
|
331
|
+
"supported": True,
|
|
332
|
+
"mutates": method in MUTATING_MANAGEMENT_METHODS,
|
|
333
|
+
"trust_required": method in MUTATING_MANAGEMENT_METHODS,
|
|
334
|
+
"workspace_required": method in WORKSPACE_REQUIRED_MANAGEMENT_METHODS,
|
|
335
|
+
"terminal_output_scraping": False,
|
|
336
|
+
"secret_values_in_params": False,
|
|
337
|
+
}
|
|
338
|
+
for method in MANAGEMENT_METHODS
|
|
339
|
+
}
|
|
340
|
+
MANAGEMENT_METHOD_CAPABILITIES["mcp.auth.login.start"].update(
|
|
341
|
+
{
|
|
342
|
+
"supported": True,
|
|
343
|
+
"callable": True,
|
|
344
|
+
"behavior": "returns_authorization_url_and_completes_via_loopback_callback",
|
|
345
|
+
"mutates": True,
|
|
346
|
+
"trust_required": True,
|
|
347
|
+
"passive_safe": False,
|
|
348
|
+
"browser_opened_by_bridge": False,
|
|
349
|
+
"tokens_in_protocol_params": False,
|
|
350
|
+
"secret_store_required": True,
|
|
351
|
+
"lifecycle_methods": [
|
|
352
|
+
"mcp.auth.login.start",
|
|
353
|
+
"mcp.auth.login.status",
|
|
354
|
+
"mcp.auth.login.cancel",
|
|
355
|
+
],
|
|
356
|
+
"flow_state_persistent": True,
|
|
357
|
+
"loopback_callback_owned": True,
|
|
358
|
+
"callback_host": "127.0.0.1",
|
|
359
|
+
"authorization_code_in_protocol": False,
|
|
360
|
+
}
|
|
361
|
+
)
|
|
362
|
+
MANAGEMENT_METHOD_CAPABILITIES["ext.install"].update(
|
|
363
|
+
{
|
|
364
|
+
"trust_review_required": True,
|
|
365
|
+
"approval_payload": "extension_install_trust_approval_v1",
|
|
366
|
+
"yes_alone_auto_trusts_package": False,
|
|
367
|
+
"source_policy": "registry_id_or_pinned_https_git_source",
|
|
368
|
+
"url_userinfo_rejected": True,
|
|
369
|
+
}
|
|
370
|
+
)
|
|
371
|
+
MANAGEMENT_METHOD_CAPABILITIES["session.show"].update(
|
|
372
|
+
{
|
|
373
|
+
"redacted": True,
|
|
374
|
+
"bounded": True,
|
|
375
|
+
"default_max_events": _SESSION_SHOW_DEFAULT_MAX_EVENTS,
|
|
376
|
+
"max_events": _SESSION_SHOW_MAX_EVENTS,
|
|
377
|
+
"default_max_total_bytes": _SESSION_SHOW_DEFAULT_MAX_TOTAL_BYTES,
|
|
378
|
+
"max_total_bytes": _SESSION_SHOW_MAX_TOTAL_BYTES,
|
|
379
|
+
}
|
|
380
|
+
)
|
|
381
|
+
MANAGEMENT_METHOD_CAPABILITIES["conventions.render"].update(
|
|
382
|
+
{
|
|
383
|
+
"redacted": True,
|
|
384
|
+
"bounded": True,
|
|
385
|
+
"default_max_chars": _CONVENTIONS_RENDER_DEFAULT_MAX_CHARS,
|
|
386
|
+
"max_chars": _CONVENTIONS_RENDER_MAX_CHARS,
|
|
387
|
+
"default_max_bytes": _CONVENTIONS_RENDER_DEFAULT_MAX_BYTES,
|
|
388
|
+
"max_bytes": _CONVENTIONS_RENDER_MAX_BYTES,
|
|
389
|
+
}
|
|
390
|
+
)
|
|
391
|
+
MANAGEMENT_METHOD_CAPABILITIES["update.check"].update(
|
|
392
|
+
{
|
|
393
|
+
"network_default": False,
|
|
394
|
+
"network_requires_explicit_user_intent": True,
|
|
395
|
+
"network_params": ["allow_network", "force"],
|
|
396
|
+
"passive_safe": True,
|
|
397
|
+
}
|
|
398
|
+
)
|
|
399
|
+
MANAGEMENT_METHOD_CAPABILITIES["doctor.providers.live"].update(
|
|
400
|
+
{
|
|
401
|
+
"network_default": False,
|
|
402
|
+
"network_requires_explicit_user_intent": True,
|
|
403
|
+
"network_params": ["allow_live"],
|
|
404
|
+
"live_provider_request": True,
|
|
405
|
+
"passive_safe": False,
|
|
406
|
+
}
|
|
407
|
+
)
|
|
408
|
+
MANAGEMENT_METHOD_CAPABILITIES["skill.install"].update(
|
|
409
|
+
{
|
|
410
|
+
"local_path_policy": "workspace_scoped_regular_dir_or_zip",
|
|
411
|
+
"remote_source_policy": "https_remote_requires_allow_remote_and_confirmation",
|
|
412
|
+
"url_userinfo_rejected": True,
|
|
413
|
+
"rejects_symlinks": True,
|
|
414
|
+
}
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _feature_group(*prefixes: str) -> dict[str, Any]:
|
|
419
|
+
methods = [
|
|
420
|
+
method
|
|
421
|
+
for method in MANAGEMENT_METHODS
|
|
422
|
+
if any(method.startswith(f"{prefix}.") for prefix in prefixes)
|
|
423
|
+
]
|
|
424
|
+
return {
|
|
425
|
+
"supported": True,
|
|
426
|
+
"methods": methods,
|
|
427
|
+
"mutating_methods": [method for method in methods if method in MUTATING_MANAGEMENT_METHODS],
|
|
428
|
+
"trust_required_methods": [
|
|
429
|
+
method for method in methods if method in MUTATING_MANAGEMENT_METHODS
|
|
430
|
+
],
|
|
431
|
+
"terminal_output_scraping": False,
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
MANAGEMENT_FEATURES: dict[str, Any] = {
|
|
436
|
+
"methods": MANAGEMENT_METHOD_CAPABILITIES,
|
|
437
|
+
"secret_policy": {
|
|
438
|
+
"inline_secrets_rejected": True,
|
|
439
|
+
"profile_key_values": "status_only",
|
|
440
|
+
"config_secret_values": "redacted_or_status_only",
|
|
441
|
+
"extension_secret_storage": "VS Code SecretStorage remains extension-side",
|
|
442
|
+
"management_responses_redacted": True,
|
|
443
|
+
"management_errors_redacted": True,
|
|
444
|
+
"urls_with_userinfo_redacted": True,
|
|
445
|
+
"authorization_headers_redacted": True,
|
|
446
|
+
},
|
|
447
|
+
"config": _feature_group("config"),
|
|
448
|
+
"profile": _feature_group("profile"),
|
|
449
|
+
"sessions": _feature_group("session"),
|
|
450
|
+
"tools": _feature_group("tools", "tool"),
|
|
451
|
+
"skills": _feature_group("skill"),
|
|
452
|
+
"doctor": _feature_group("doctor"),
|
|
453
|
+
"sandbox": _feature_group("sandbox"),
|
|
454
|
+
"update": _feature_group("update"),
|
|
455
|
+
"report": _feature_group("report"),
|
|
456
|
+
"mcp": {
|
|
457
|
+
**_feature_group("mcp"),
|
|
458
|
+
"auth_login": {
|
|
459
|
+
"supported": True,
|
|
460
|
+
"callable_probe_method": "mcp.auth.login.start",
|
|
461
|
+
"advertised_lifecycle_methods": True,
|
|
462
|
+
"methods": [
|
|
463
|
+
"mcp.auth.login.start",
|
|
464
|
+
"mcp.auth.login.status",
|
|
465
|
+
"mcp.auth.login.cancel",
|
|
466
|
+
],
|
|
467
|
+
"flow_state_persistent": True,
|
|
468
|
+
"loopback_callback_owned": True,
|
|
469
|
+
"pkce_s256": True,
|
|
470
|
+
"state_validation": True,
|
|
471
|
+
"host_header_validation": True,
|
|
472
|
+
"timeout_expiry": True,
|
|
473
|
+
"cancel_cleanup": True,
|
|
474
|
+
"logout_fences_late_token_writes": True,
|
|
475
|
+
"encrypted_token_store": True,
|
|
476
|
+
"browser_opened_by_bridge": False,
|
|
477
|
+
"tokens_in_protocol_params": False,
|
|
478
|
+
"secret_store_required": True,
|
|
479
|
+
"authorization_code_in_protocol": False,
|
|
480
|
+
"remote_extension_host_support": False,
|
|
481
|
+
},
|
|
482
|
+
},
|
|
483
|
+
"hooks": {
|
|
484
|
+
**_feature_group("hooks"),
|
|
485
|
+
"watch": {
|
|
486
|
+
"supported": False,
|
|
487
|
+
"advertised_method": False,
|
|
488
|
+
"advertised_lifecycle_methods": False,
|
|
489
|
+
"proposed_methods": [
|
|
490
|
+
"hooks.watch.start",
|
|
491
|
+
"hooks.watch.poll",
|
|
492
|
+
"hooks.watch.stop",
|
|
493
|
+
"hooks.watch.status",
|
|
494
|
+
],
|
|
495
|
+
"missing_lifecycle_primitives": [
|
|
496
|
+
"subscription_registry",
|
|
497
|
+
"bounded_event_buffer",
|
|
498
|
+
"dropped_event_accounting",
|
|
499
|
+
"watcher_cancellation",
|
|
500
|
+
"stop_cleanup",
|
|
501
|
+
"backend_redaction_for_live_output",
|
|
502
|
+
],
|
|
503
|
+
"explicit_user_action_required": True,
|
|
504
|
+
"workspace_trust_required": True,
|
|
505
|
+
"passive_safe": True,
|
|
506
|
+
"reason": "hooks.watch requires a managed subscription lifecycle, cancellation, bounded redacted events, dropped-event accounting, and disposable cleanup; it is not a request/response IDE method.",
|
|
507
|
+
},
|
|
508
|
+
},
|
|
509
|
+
"conventions": _feature_group("conventions"),
|
|
510
|
+
"ext": _feature_group("ext"),
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def handle_management_method(
|
|
515
|
+
method: str,
|
|
516
|
+
params: dict[str, Any],
|
|
517
|
+
*,
|
|
518
|
+
request_id: RequestId,
|
|
519
|
+
stateful_handlers: Mapping[str, Callable[[dict[str, Any], RequestId], dict[str, Any]]]
|
|
520
|
+
| None = None,
|
|
521
|
+
) -> dict[str, Any]:
|
|
522
|
+
handler = (stateful_handlers or {}).get(method) or _HANDLERS.get(method)
|
|
523
|
+
if handler is None:
|
|
524
|
+
raise ProtocolError(
|
|
525
|
+
"method_not_found",
|
|
526
|
+
f"Unsupported method: {method}",
|
|
527
|
+
request_id=request_id,
|
|
528
|
+
)
|
|
529
|
+
if method in MUTATING_MANAGEMENT_METHODS:
|
|
530
|
+
_require_workspace_trust(params, method=method, request_id=request_id)
|
|
531
|
+
if method in WORKSPACE_REQUIRED_MANAGEMENT_METHODS:
|
|
532
|
+
_require_workspace_binding_param(params, method=method, request_id=request_id)
|
|
533
|
+
try:
|
|
534
|
+
return _redact_payload(handler(params, request_id))
|
|
535
|
+
except ProtocolError as exc:
|
|
536
|
+
raise ProtocolError(
|
|
537
|
+
exc.code,
|
|
538
|
+
_redact_management_error_message(exc.message),
|
|
539
|
+
request_id=exc.request_id if exc.request_id is not None else request_id,
|
|
540
|
+
) from exc
|
|
541
|
+
except ConfigError as exc:
|
|
542
|
+
raise ProtocolError(
|
|
543
|
+
"config_error", _redact_management_error_message(str(exc)), request_id=request_id
|
|
544
|
+
) from exc
|
|
545
|
+
except (
|
|
546
|
+
WorkspaceContextError,
|
|
547
|
+
FeedbackReportError,
|
|
548
|
+
SkillLifecycleError,
|
|
549
|
+
PluginInstallError,
|
|
550
|
+
) as exc:
|
|
551
|
+
raise ProtocolError(
|
|
552
|
+
"management_error", _redact_management_error_message(str(exc)), request_id=request_id
|
|
553
|
+
) from exc
|
|
554
|
+
except (OSError, RuntimeError, ValueError) as exc:
|
|
555
|
+
raise ProtocolError(
|
|
556
|
+
"management_error", _redact_management_error_message(str(exc)), request_id=request_id
|
|
557
|
+
) from exc
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def retained_session_usage(
|
|
561
|
+
params: dict[str, Any],
|
|
562
|
+
*,
|
|
563
|
+
request_id: RequestId,
|
|
564
|
+
) -> dict[str, Any]:
|
|
565
|
+
return _session_usage(params, request_id)
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def _config_get(_params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
569
|
+
cfg = load_config()
|
|
570
|
+
return {
|
|
571
|
+
"config": _safe_config_payload(cfg),
|
|
572
|
+
"active_profile": str((cfg.extra_fields or {}).get("active_profile") or ""),
|
|
573
|
+
"api_key": _api_key_status(resolve_api_key(cfg)),
|
|
574
|
+
"config_path": str(config_path()),
|
|
575
|
+
"credentials_path": str(credentials_path()),
|
|
576
|
+
"secret_values_included": False,
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def _config_schema(_params: dict[str, Any], _request_id: RequestId) -> dict[str, Any]:
|
|
581
|
+
return {
|
|
582
|
+
"schema": AppConfig.model_json_schema(),
|
|
583
|
+
"secret_values_included": False,
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def _config_validate(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
588
|
+
raw_values = params.get("values")
|
|
589
|
+
if raw_values is None:
|
|
590
|
+
cfg = load_config()
|
|
591
|
+
else:
|
|
592
|
+
if not isinstance(raw_values, dict):
|
|
593
|
+
raise ProtocolError(
|
|
594
|
+
"invalid_field",
|
|
595
|
+
"config.validate values must be an object when provided.",
|
|
596
|
+
request_id=request_id,
|
|
597
|
+
)
|
|
598
|
+
cfg = AppConfig.model_validate(raw_values)
|
|
599
|
+
return {
|
|
600
|
+
"valid": True,
|
|
601
|
+
"errors": [],
|
|
602
|
+
"config": _safe_config_payload(cfg),
|
|
603
|
+
"secret_values_included": False,
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def _config_set(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
608
|
+
key = _required_str(params, "key", request_id=request_id)
|
|
609
|
+
if _unsafe_config_set_key(key):
|
|
610
|
+
raise ProtocolError(
|
|
611
|
+
"inline_secret_rejected",
|
|
612
|
+
"Secret-bearing config keys cannot be set through IDE protocol params.",
|
|
613
|
+
request_id=request_id,
|
|
614
|
+
)
|
|
615
|
+
if "value" not in params:
|
|
616
|
+
raise ProtocolError("missing_param", "config.set requires value.", request_id=request_id)
|
|
617
|
+
raw_value = params.get("value")
|
|
618
|
+
_reject_url_userinfo_value(raw_value, field=f"config.{key}", request_id=request_id)
|
|
619
|
+
_reject_secret_bearing_value(raw_value, field=f"config.{key}", request_id=request_id)
|
|
620
|
+
value = _config_value_to_cli_string(raw_value)
|
|
621
|
+
cfg = load_config()
|
|
622
|
+
subscription_effort: str | None = None
|
|
623
|
+
if key.strip().lower() == "model" and get_active_profile(cfg).auth_provider:
|
|
624
|
+
subscription_effort = _select_subscription_model_default(
|
|
625
|
+
cfg,
|
|
626
|
+
value,
|
|
627
|
+
request_id=request_id,
|
|
628
|
+
)
|
|
629
|
+
else:
|
|
630
|
+
cfg = set_config_value(cfg, key, value)
|
|
631
|
+
save_config(cfg)
|
|
632
|
+
return {
|
|
633
|
+
"key": key,
|
|
634
|
+
"changed": True,
|
|
635
|
+
"config_path": str(config_path()),
|
|
636
|
+
**(
|
|
637
|
+
{
|
|
638
|
+
"reasoning_effort": subscription_effort,
|
|
639
|
+
"subscription_selection_ready": active_subscription_selection_ready(cfg),
|
|
640
|
+
}
|
|
641
|
+
if subscription_effort is not None
|
|
642
|
+
else {}
|
|
643
|
+
),
|
|
644
|
+
"secret_values_included": False,
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def _select_subscription_model_default(
|
|
649
|
+
cfg: AppConfig,
|
|
650
|
+
model: str,
|
|
651
|
+
*,
|
|
652
|
+
request_id: RequestId,
|
|
653
|
+
) -> str:
|
|
654
|
+
"""Atomically select an account model and its provider-advertised default effort.
|
|
655
|
+
|
|
656
|
+
The VS Code model picker is intentionally a one-step control. Subscription profiles cannot use
|
|
657
|
+
the generic single-field config mutation because that could leave an unsupported model/effort
|
|
658
|
+
pair, so the bridge validates the live account catalog and commits both fields together.
|
|
659
|
+
"""
|
|
660
|
+
|
|
661
|
+
profile = get_active_profile(cfg)
|
|
662
|
+
provider_id = str(profile.auth_provider or "").strip()
|
|
663
|
+
if not provider_id:
|
|
664
|
+
raise ProtocolError(
|
|
665
|
+
"config_error",
|
|
666
|
+
"The active profile is not an AI subscription connection.",
|
|
667
|
+
request_id=request_id,
|
|
668
|
+
)
|
|
669
|
+
adapter = create_provider_auth(provider_id)
|
|
670
|
+
status = adapter.account_status()
|
|
671
|
+
if not status.connected:
|
|
672
|
+
raise ProtocolError(
|
|
673
|
+
"provider_login_required",
|
|
674
|
+
"Connect the active AI subscription account before choosing its model.",
|
|
675
|
+
request_id=request_id,
|
|
676
|
+
)
|
|
677
|
+
models = tuple(adapter.list_models(refresh=True))
|
|
678
|
+
selected = next((item for item in models if str(getattr(item, "id", "")) == model), None)
|
|
679
|
+
if selected is None:
|
|
680
|
+
raise ProtocolError(
|
|
681
|
+
"unsupported_subscription_model",
|
|
682
|
+
"The selected model is not available to the connected subscription account.",
|
|
683
|
+
request_id=request_id,
|
|
684
|
+
)
|
|
685
|
+
efforts = tuple(
|
|
686
|
+
str(getattr(item, "id", "") or "").strip()
|
|
687
|
+
for item in tuple(getattr(selected, "reasoning_efforts", ()) or ())
|
|
688
|
+
)
|
|
689
|
+
efforts = tuple(item for item in efforts if item)
|
|
690
|
+
advertised_default = str(getattr(selected, "default_reasoning_effort", "") or "").strip()
|
|
691
|
+
reasoning_effort = (
|
|
692
|
+
advertised_default
|
|
693
|
+
if advertised_default and (not efforts or advertised_default in efforts)
|
|
694
|
+
else ("medium" if "medium" in efforts else (efforts[0] if efforts else "auto"))
|
|
695
|
+
)
|
|
696
|
+
update_active_profile_defaults(
|
|
697
|
+
cfg,
|
|
698
|
+
default_model=model,
|
|
699
|
+
reasoning_effort=reasoning_effort,
|
|
700
|
+
allow_subscription_selection=True,
|
|
701
|
+
)
|
|
702
|
+
updated = get_active_profile(cfg)
|
|
703
|
+
if not subscription_selection_supported(updated, models):
|
|
704
|
+
raise ProtocolError(
|
|
705
|
+
"unsupported_subscription_selection",
|
|
706
|
+
"The selected model and reasoning effort are not supported by the connected account.",
|
|
707
|
+
request_id=request_id,
|
|
708
|
+
)
|
|
709
|
+
cfg.extra_fields.pop(SUBSCRIPTION_SELECTION_REQUIRED_KEY, None)
|
|
710
|
+
cfg.extra_fields.pop("subscription_reconnect_required", None)
|
|
711
|
+
cfg.extra_fields["onboarded"] = True
|
|
712
|
+
return reasoning_effort
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
def _profile_list(_params: dict[str, Any], _request_id: RequestId) -> dict[str, Any]:
|
|
716
|
+
cfg = load_config()
|
|
717
|
+
active = str((cfg.extra_fields or {}).get("active_profile") or "")
|
|
718
|
+
return {
|
|
719
|
+
"active_profile": active,
|
|
720
|
+
"profiles": [_profile_payload(cfg, profile) for profile in list_profiles(cfg)],
|
|
721
|
+
"secret_values_included": False,
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
def _profile_show(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
726
|
+
cfg = load_config()
|
|
727
|
+
name = _required_str(params, "name", request_id=request_id)
|
|
728
|
+
profile = get_profile(cfg, name)
|
|
729
|
+
if profile is None:
|
|
730
|
+
raise ProtocolError(
|
|
731
|
+
"profile_not_found", f"Profile not found: {name}", request_id=request_id
|
|
732
|
+
)
|
|
733
|
+
return {
|
|
734
|
+
"profile": _profile_payload(cfg, profile, include_details=True),
|
|
735
|
+
"secret_values_included": False,
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
def _profile_add(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
740
|
+
cfg = load_config()
|
|
741
|
+
extra_headers_action = _extra_headers_secret_storage_action(
|
|
742
|
+
params,
|
|
743
|
+
request_id=request_id,
|
|
744
|
+
)
|
|
745
|
+
if extra_headers_action is not None:
|
|
746
|
+
return {
|
|
747
|
+
"changed": False,
|
|
748
|
+
"profile": None,
|
|
749
|
+
"action": extra_headers_action,
|
|
750
|
+
"secret_values_included": False,
|
|
751
|
+
}
|
|
752
|
+
base_url = _required_str(params, "base_url", request_id=request_id)
|
|
753
|
+
_reject_url_userinfo_value(base_url, field="profile.base_url", request_id=request_id)
|
|
754
|
+
profile = ProfileSpec(
|
|
755
|
+
name=_required_str(params, "name", request_id=request_id),
|
|
756
|
+
protocol=_optional_str(params, "protocol", default="openai_compat"),
|
|
757
|
+
base_url=base_url,
|
|
758
|
+
api_key_env=_optional_str_or_none(params, "api_key_env"),
|
|
759
|
+
extra_headers=_extra_headers_param(params, request_id=request_id),
|
|
760
|
+
default_model=_optional_str(params, "default_model", default=""),
|
|
761
|
+
web_search_adapter=_optional_str(params, "web_search_adapter", default="auto"),
|
|
762
|
+
web_search_model=_optional_str(params, "web_search_model", default=""),
|
|
763
|
+
notes=_optional_str(params, "notes", default=""),
|
|
764
|
+
)
|
|
765
|
+
add_profile(cfg, profile)
|
|
766
|
+
save_config(cfg)
|
|
767
|
+
return {
|
|
768
|
+
"profile": _profile_payload(cfg, profile, include_details=True),
|
|
769
|
+
"changed": True,
|
|
770
|
+
"secret_action": _secret_action_for_profile(profile),
|
|
771
|
+
"secret_values_included": False,
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
def _profile_remove(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
776
|
+
if not _confirmed(params):
|
|
777
|
+
return _confirmation_required("profile.remove", "profile removal")
|
|
778
|
+
cfg = load_config()
|
|
779
|
+
name = _required_str(params, "name", request_id=request_id)
|
|
780
|
+
remove_profile(cfg, name)
|
|
781
|
+
clear_persisted_profile_key(name)
|
|
782
|
+
save_config(cfg)
|
|
783
|
+
return {"name": name, "removed": True, "secret_values_included": False}
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
def _profile_use(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
787
|
+
cfg = load_config()
|
|
788
|
+
name = _required_str(params, "name", request_id=request_id)
|
|
789
|
+
set_active_profile(cfg, name)
|
|
790
|
+
save_config(cfg)
|
|
791
|
+
profile = get_profile(cfg, name)
|
|
792
|
+
return {
|
|
793
|
+
"active_profile": name,
|
|
794
|
+
"profile": _profile_payload(cfg, profile, include_details=True) if profile else None,
|
|
795
|
+
"changed": True,
|
|
796
|
+
"secret_values_included": False,
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
def _profile_rename(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
801
|
+
cfg = load_config()
|
|
802
|
+
old = _required_str(params, "old", request_id=request_id)
|
|
803
|
+
new = _required_str(params, "new", request_id=request_id)
|
|
804
|
+
rename_profile(cfg, old, new)
|
|
805
|
+
rename_persisted_profile_key(old, new)
|
|
806
|
+
save_config(cfg)
|
|
807
|
+
profile = get_profile(cfg, new)
|
|
808
|
+
return {
|
|
809
|
+
"old": old,
|
|
810
|
+
"new": new,
|
|
811
|
+
"profile": _profile_payload(cfg, profile, include_details=True) if profile else None,
|
|
812
|
+
"changed": True,
|
|
813
|
+
"secret_values_included": False,
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
def _profile_presets(_params: dict[str, Any], _request_id: RequestId) -> dict[str, Any]:
|
|
818
|
+
return {
|
|
819
|
+
"presets": [_preset_payload(preset) for preset in PROFILE_PRESETS],
|
|
820
|
+
"secret_values_included": False,
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
def _profile_preset(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
825
|
+
preset_key = _required_str_any(params, ("preset_key", "preset"), request_id=request_id)
|
|
826
|
+
preset = get_preset(preset_key)
|
|
827
|
+
if preset is None:
|
|
828
|
+
raise ProtocolError(
|
|
829
|
+
"unknown_profile_preset",
|
|
830
|
+
f"Unknown profile preset: {preset_key}",
|
|
831
|
+
request_id=request_id,
|
|
832
|
+
)
|
|
833
|
+
cfg = load_config()
|
|
834
|
+
name = _optional_str(params, "name", default=str(preset.key)).strip().lower()
|
|
835
|
+
if get_profile(cfg, name) is not None and not _confirmed(params):
|
|
836
|
+
return _confirmation_required("profile.preset", f"overwrite profile {name}")
|
|
837
|
+
if not preset.base_url and not _optional_str(params, "base_url", default="").strip():
|
|
838
|
+
raise ProtocolError(
|
|
839
|
+
"missing_param",
|
|
840
|
+
"profile.preset requires base_url for the custom preset.",
|
|
841
|
+
request_id=request_id,
|
|
842
|
+
)
|
|
843
|
+
profile = make_profile_from_preset(preset, name=name)
|
|
844
|
+
add_profile(cfg, profile)
|
|
845
|
+
base_url = _optional_str(params, "base_url", default="").strip()
|
|
846
|
+
if base_url and base_url != profile.base_url:
|
|
847
|
+
_reject_url_userinfo_value(base_url, field="profile.base_url", request_id=request_id)
|
|
848
|
+
update_profile(cfg, profile.name, base_url=base_url)
|
|
849
|
+
profile = get_profile(cfg, profile.name) or profile
|
|
850
|
+
save_config(cfg)
|
|
851
|
+
return {
|
|
852
|
+
"profile": _profile_payload(cfg, profile, include_details=True),
|
|
853
|
+
"preset": _preset_payload(preset),
|
|
854
|
+
"changed": True,
|
|
855
|
+
"secret_action": _secret_action_for_profile(profile),
|
|
856
|
+
"secret_values_included": False,
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
|
|
860
|
+
def _profile_convert(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
861
|
+
cfg = load_config()
|
|
862
|
+
profile_name = _optional_str(
|
|
863
|
+
params,
|
|
864
|
+
"name",
|
|
865
|
+
default=str((cfg.extra_fields or {}).get("active_profile") or ""),
|
|
866
|
+
).strip()
|
|
867
|
+
if not profile_name:
|
|
868
|
+
raise ProtocolError(
|
|
869
|
+
"missing_param",
|
|
870
|
+
"profile.convert requires name when no active profile is configured.",
|
|
871
|
+
request_id=request_id,
|
|
872
|
+
)
|
|
873
|
+
profile = get_profile(cfg, profile_name)
|
|
874
|
+
if profile is None:
|
|
875
|
+
raise ProtocolError(
|
|
876
|
+
"profile_not_found", f"Profile not found: {profile_name}", request_id=request_id
|
|
877
|
+
)
|
|
878
|
+
target = normalize_conversion_target(_required_str(params, "target", request_id=request_id))
|
|
879
|
+
preset = target_preset_for_profile_conversion(profile, target=target)
|
|
880
|
+
if preset is None:
|
|
881
|
+
raise ProtocolError(
|
|
882
|
+
"unsupported_profile_conversion",
|
|
883
|
+
"Only OpenAI, Anthropic, and Gemini profiles can be converted between native and compatibility protocols.",
|
|
884
|
+
request_id=request_id,
|
|
885
|
+
)
|
|
886
|
+
converted = convert_profile_to_preset(profile, preset)
|
|
887
|
+
preview = {
|
|
888
|
+
"before": _profile_payload(cfg, profile, include_details=True),
|
|
889
|
+
"after": _profile_payload(cfg, converted, include_details=True),
|
|
890
|
+
"target": target,
|
|
891
|
+
"preset": _preset_payload(preset),
|
|
892
|
+
}
|
|
893
|
+
if converted.to_dict() == profile.to_dict():
|
|
894
|
+
return {**preview, "changed": False, "secret_values_included": False}
|
|
895
|
+
if not _confirmed(params):
|
|
896
|
+
return {
|
|
897
|
+
**preview,
|
|
898
|
+
"changed": False,
|
|
899
|
+
"action": {
|
|
900
|
+
"kind": "requires_confirmation",
|
|
901
|
+
"method": "profile.convert",
|
|
902
|
+
"reason": f"Convert profile {profile.name} to {target}.",
|
|
903
|
+
},
|
|
904
|
+
"secret_values_included": False,
|
|
905
|
+
}
|
|
906
|
+
add_profile(cfg, converted)
|
|
907
|
+
if str((cfg.extra_fields or {}).get("active_profile") or "") == profile.name:
|
|
908
|
+
set_active_profile(cfg, profile.name)
|
|
909
|
+
save_config(cfg)
|
|
910
|
+
return {**preview, "changed": True, "secret_values_included": False}
|
|
911
|
+
|
|
912
|
+
|
|
913
|
+
def _session_show(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
914
|
+
path = _session_log_path(params, request_id=request_id)
|
|
915
|
+
max_events = _positive_int(
|
|
916
|
+
params,
|
|
917
|
+
"max_events",
|
|
918
|
+
default=_SESSION_SHOW_DEFAULT_MAX_EVENTS,
|
|
919
|
+
upper=_SESSION_SHOW_MAX_EVENTS,
|
|
920
|
+
)
|
|
921
|
+
max_total_bytes = _positive_int(
|
|
922
|
+
params,
|
|
923
|
+
"max_total_bytes",
|
|
924
|
+
default=_SESSION_SHOW_DEFAULT_MAX_TOTAL_BYTES,
|
|
925
|
+
upper=_SESSION_SHOW_MAX_TOTAL_BYTES,
|
|
926
|
+
)
|
|
927
|
+
events: list[dict[str, Any]] = []
|
|
928
|
+
event_count = 0
|
|
929
|
+
response_bytes = 0
|
|
930
|
+
truncated_by_events = False
|
|
931
|
+
truncated_by_bytes = False
|
|
932
|
+
for event in read_session_events(path):
|
|
933
|
+
event_count += 1
|
|
934
|
+
if len(events) >= max_events:
|
|
935
|
+
truncated_by_events = True
|
|
936
|
+
continue
|
|
937
|
+
redacted_event = _redact_payload(event)
|
|
938
|
+
event_bytes = _json_size_bytes(redacted_event)
|
|
939
|
+
if response_bytes + event_bytes > max_total_bytes:
|
|
940
|
+
truncated_by_bytes = True
|
|
941
|
+
continue
|
|
942
|
+
events.append(redacted_event)
|
|
943
|
+
response_bytes += event_bytes
|
|
944
|
+
return {
|
|
945
|
+
"session_id": path.stem,
|
|
946
|
+
"path": str(path),
|
|
947
|
+
"events": events,
|
|
948
|
+
"event_count": event_count,
|
|
949
|
+
"returned_event_count": len(events),
|
|
950
|
+
"truncated": truncated_by_events or truncated_by_bytes,
|
|
951
|
+
"truncated_by_events": truncated_by_events,
|
|
952
|
+
"truncated_by_bytes": truncated_by_bytes,
|
|
953
|
+
"max_events": max_events,
|
|
954
|
+
"max_total_bytes": max_total_bytes,
|
|
955
|
+
"response_bytes": response_bytes,
|
|
956
|
+
"secret_values_included": False,
|
|
957
|
+
"redacted": True,
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
|
|
961
|
+
def _session_usage(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
962
|
+
path = _session_log_path(params, request_id=request_id)
|
|
963
|
+
summary = aggregate_usage_from_session_logs([path])
|
|
964
|
+
totals = summary.totals()
|
|
965
|
+
records = summary.records()
|
|
966
|
+
return {
|
|
967
|
+
"session_id": path.stem,
|
|
968
|
+
"path": str(path),
|
|
969
|
+
"by_model": summary.by_model_rows(),
|
|
970
|
+
"totals": totals,
|
|
971
|
+
"call_count": int(totals.get("calls", len(records)) if isinstance(totals, dict) else 0),
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
|
|
975
|
+
def _session_score(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
976
|
+
cfg = load_config()
|
|
977
|
+
sessions_dir = resolve_sessions_dir(cfg)
|
|
978
|
+
session_id = str(params.get("session_id") or "").strip()
|
|
979
|
+
latest = _positive_int(params, "latest", default=0, upper=200)
|
|
980
|
+
if session_id and latest > 0:
|
|
981
|
+
raise ProtocolError(
|
|
982
|
+
"invalid_field",
|
|
983
|
+
"Use either session_id or latest, not both.",
|
|
984
|
+
request_id=request_id,
|
|
985
|
+
)
|
|
986
|
+
if session_id:
|
|
987
|
+
paths = [_session_log_path(params, request_id=request_id)]
|
|
988
|
+
else:
|
|
989
|
+
infos = list_sessions(sessions_dir)
|
|
990
|
+
count = latest if latest > 0 else 1
|
|
991
|
+
paths = [info.path for info in infos[:count]]
|
|
992
|
+
scores = [score_session_log(path) for path in paths]
|
|
993
|
+
return {
|
|
994
|
+
"sessions_dir": str(sessions_dir),
|
|
995
|
+
"scores": scores,
|
|
996
|
+
"score": scores[0] if len(scores) == 1 else None,
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
|
|
1000
|
+
def _tools_catalog(_params: dict[str, Any], _request_id: RequestId) -> dict[str, Any]:
|
|
1001
|
+
cfg = load_config()
|
|
1002
|
+
tools = []
|
|
1003
|
+
for spec in iter_builtin_tool_metadata():
|
|
1004
|
+
status = "available"
|
|
1005
|
+
notes: list[str] = []
|
|
1006
|
+
availability = get_tool_availability(spec.name)
|
|
1007
|
+
if spec.optional and availability is not None and availability.unavailable_reason:
|
|
1008
|
+
status = "optional-unavailable"
|
|
1009
|
+
notes.append(str(availability.unavailable_reason))
|
|
1010
|
+
if spec.name == "web_search":
|
|
1011
|
+
api_key = resolve_api_key(cfg)
|
|
1012
|
+
runtime = resolve_web_search_runtime_status(cfg=cfg, api_key=api_key.key)
|
|
1013
|
+
status = runtime.availability_label
|
|
1014
|
+
notes.extend(runtime.notes)
|
|
1015
|
+
if not runtime.registration_ready:
|
|
1016
|
+
notes.append(runtime.setup_hint)
|
|
1017
|
+
tools.append(
|
|
1018
|
+
{
|
|
1019
|
+
"name": spec.name,
|
|
1020
|
+
"description": spec.description,
|
|
1021
|
+
"categories": list(spec.categories),
|
|
1022
|
+
"parameters": spec.copied_parameters(),
|
|
1023
|
+
"display_name": spec.rich.display_name,
|
|
1024
|
+
"optional": spec.optional,
|
|
1025
|
+
"status": status,
|
|
1026
|
+
"notes": [note for note in notes if note],
|
|
1027
|
+
"built_in_subagent_exposure": spec.built_in_subagent_exposure,
|
|
1028
|
+
}
|
|
1029
|
+
)
|
|
1030
|
+
return {"tools": tools, "count": len(tools), "secret_values_included": False}
|
|
1031
|
+
|
|
1032
|
+
|
|
1033
|
+
def _tool_list(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
1034
|
+
workspace_root, entries = _custom_tool_entries(params, request_id=request_id)
|
|
1035
|
+
return {
|
|
1036
|
+
"workspace_root": str(workspace_root),
|
|
1037
|
+
"project_root": str(project_custom_tools_root(workspace_root)),
|
|
1038
|
+
"global_root": str(global_custom_tools_root()),
|
|
1039
|
+
"tools": [_custom_tool_payload(entry, workspace_root=workspace_root) for entry in entries],
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
|
|
1043
|
+
def _tool_info(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
1044
|
+
name = _required_str(params, "name", request_id=request_id)
|
|
1045
|
+
workspace_root, entries = _custom_tool_entries(params, request_id=request_id)
|
|
1046
|
+
matches = [
|
|
1047
|
+
entry for entry in entries if str(getattr(entry, "name", "")).casefold() == name.casefold()
|
|
1048
|
+
]
|
|
1049
|
+
if not matches:
|
|
1050
|
+
raise ProtocolError(
|
|
1051
|
+
"tool_not_found", f"Custom tool not found: {name}", request_id=request_id
|
|
1052
|
+
)
|
|
1053
|
+
return {
|
|
1054
|
+
"workspace_root": str(workspace_root),
|
|
1055
|
+
"tools": [_custom_tool_payload(entry, workspace_root=workspace_root) for entry in matches],
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
|
|
1059
|
+
def _tool_trust(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
1060
|
+
name = _required_str(params, "name", request_id=request_id)
|
|
1061
|
+
workspace_root, entries = _custom_tool_entries(params, request_id=request_id)
|
|
1062
|
+
entry = _select_project_tool_entry(entries, name=name, request_id=request_id)
|
|
1063
|
+
assert entry.spec is not None
|
|
1064
|
+
trust_project_tool(entry.spec)
|
|
1065
|
+
return {
|
|
1066
|
+
"workspace_root": str(workspace_root),
|
|
1067
|
+
"name": entry.spec.name,
|
|
1068
|
+
"trusted": True,
|
|
1069
|
+
"changed": True,
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
|
|
1073
|
+
def _tool_untrust(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
1074
|
+
name = _required_str(params, "name", request_id=request_id)
|
|
1075
|
+
workspace_root, entries = _custom_tool_entries(params, request_id=request_id)
|
|
1076
|
+
entry = _select_project_tool_entry(entries, name=name, request_id=request_id)
|
|
1077
|
+
assert entry.spec is not None
|
|
1078
|
+
untrust_project_tool(entry.spec)
|
|
1079
|
+
return {
|
|
1080
|
+
"workspace_root": str(workspace_root),
|
|
1081
|
+
"name": entry.spec.name,
|
|
1082
|
+
"trusted": False,
|
|
1083
|
+
"changed": True,
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
|
|
1087
|
+
def _skill_list(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
1088
|
+
workspace_root, catalog = _skill_catalog(params, request_id=request_id)
|
|
1089
|
+
return {
|
|
1090
|
+
"workspace_root": str(workspace_root),
|
|
1091
|
+
"skills": [_skill_entry_payload(entry) for entry in catalog.entries],
|
|
1092
|
+
"issues": _skill_issues_payload(catalog),
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
|
|
1096
|
+
def _skill_info(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
1097
|
+
name = _required_str(params, "name", request_id=request_id)
|
|
1098
|
+
workspace_root, catalog = _skill_catalog(params, request_id=request_id)
|
|
1099
|
+
entry = resolve_skill_catalog_entry(entries=catalog.entries, raw_name=name)
|
|
1100
|
+
if entry is None:
|
|
1101
|
+
raise ProtocolError("skill_not_found", f"Skill not found: {name}", request_id=request_id)
|
|
1102
|
+
return {
|
|
1103
|
+
"workspace_root": str(workspace_root),
|
|
1104
|
+
"skill": _skill_entry_payload(entry),
|
|
1105
|
+
"info_text": render_skill_info_text(entry.skill, catalog_entry=entry),
|
|
1106
|
+
"issues": _skill_issues_payload(catalog),
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
|
|
1110
|
+
def _skill_init(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
1111
|
+
workspace_root = _workspace_root_from_params(params, request_id=request_id, required=True)
|
|
1112
|
+
result = scaffold_skill_bundle(
|
|
1113
|
+
name=_required_str(params, "name", request_id=request_id),
|
|
1114
|
+
description=_optional_str(params, "description", default=""),
|
|
1115
|
+
workspace_root=workspace_root,
|
|
1116
|
+
project=_optional_bool(params, "project", default=True),
|
|
1117
|
+
family=_optional_str(params, "family", default="native"),
|
|
1118
|
+
force=_optional_bool(params, "force", default=False),
|
|
1119
|
+
)
|
|
1120
|
+
return {"result": _dataclass_payload(result), "changed": True}
|
|
1121
|
+
|
|
1122
|
+
|
|
1123
|
+
def _skill_validate(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
1124
|
+
workspace_root, catalog = _skill_catalog(params, request_id=request_id)
|
|
1125
|
+
bundle = _optional_str(params, "bundle", default="").strip()
|
|
1126
|
+
name = _optional_str(params, "name", default="").strip()
|
|
1127
|
+
validate_all = _optional_bool(params, "all", default=False)
|
|
1128
|
+
choices = sum(1 for value in (bool(bundle), bool(name), validate_all) if value)
|
|
1129
|
+
if choices != 1:
|
|
1130
|
+
raise ProtocolError(
|
|
1131
|
+
"invalid_field",
|
|
1132
|
+
"skill.validate requires exactly one of bundle, name, or all.",
|
|
1133
|
+
request_id=request_id,
|
|
1134
|
+
)
|
|
1135
|
+
results = []
|
|
1136
|
+
if bundle:
|
|
1137
|
+
path = _workspace_child_path(workspace_root, bundle, request_id=request_id)
|
|
1138
|
+
results.append(validate_skill_bundle(path))
|
|
1139
|
+
elif name:
|
|
1140
|
+
entry = resolve_skill_catalog_entry(entries=catalog.entries, raw_name=name)
|
|
1141
|
+
if entry is None:
|
|
1142
|
+
raise ProtocolError(
|
|
1143
|
+
"skill_not_found", f"Skill not found: {name}", request_id=request_id
|
|
1144
|
+
)
|
|
1145
|
+
results.append(validate_skill_bundle(entry.skill.bundle_path))
|
|
1146
|
+
else:
|
|
1147
|
+
seen_paths: set[Path] = set()
|
|
1148
|
+
for entry in catalog.entries:
|
|
1149
|
+
if entry.skill.bundle_path in seen_paths:
|
|
1150
|
+
continue
|
|
1151
|
+
seen_paths.add(entry.skill.bundle_path)
|
|
1152
|
+
results.append(validate_skill_bundle(entry.skill.bundle_path))
|
|
1153
|
+
return {
|
|
1154
|
+
"workspace_root": str(workspace_root),
|
|
1155
|
+
"results": [_validation_payload(result) for result in results],
|
|
1156
|
+
"valid": all(bool(getattr(result, "valid", False)) for result in results),
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
|
|
1160
|
+
def _skill_install(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
1161
|
+
workspace_root = _workspace_root_from_params(params, request_id=request_id, required=True)
|
|
1162
|
+
source = _validate_skill_install_source(
|
|
1163
|
+
_required_str(params, "source", request_id=request_id),
|
|
1164
|
+
workspace_root=workspace_root,
|
|
1165
|
+
params=params,
|
|
1166
|
+
request_id=request_id,
|
|
1167
|
+
)
|
|
1168
|
+
result = install_skill_bundle(
|
|
1169
|
+
source=source,
|
|
1170
|
+
workspace_root=workspace_root,
|
|
1171
|
+
project=_optional_bool(params, "project", default=False),
|
|
1172
|
+
subdir=_optional_str_or_none(params, "subdir"),
|
|
1173
|
+
force=_optional_bool(params, "force", default=False),
|
|
1174
|
+
)
|
|
1175
|
+
return {"result": _dataclass_payload(result), "changed": True}
|
|
1176
|
+
|
|
1177
|
+
|
|
1178
|
+
def _skill_enable(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
1179
|
+
return _set_skill_enabled(params, request_id=request_id, enabled=True)
|
|
1180
|
+
|
|
1181
|
+
|
|
1182
|
+
def _skill_disable(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
1183
|
+
return _set_skill_enabled(params, request_id=request_id, enabled=False)
|
|
1184
|
+
|
|
1185
|
+
|
|
1186
|
+
def _skill_remove(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
1187
|
+
workspace_root = _workspace_root_from_params(params, request_id=request_id, required=True)
|
|
1188
|
+
result = remove_managed_skill(
|
|
1189
|
+
name=_required_str(params, "name", request_id=request_id),
|
|
1190
|
+
workspace_root=workspace_root,
|
|
1191
|
+
project=_optional_bool(params, "project", default=False),
|
|
1192
|
+
)
|
|
1193
|
+
return {"result": _dataclass_payload(result), "changed": True}
|
|
1194
|
+
|
|
1195
|
+
|
|
1196
|
+
def _doctor_summary(_params: dict[str, Any], _request_id: RequestId) -> dict[str, Any]:
|
|
1197
|
+
cfg = load_config()
|
|
1198
|
+
api_key = resolve_api_key(cfg)
|
|
1199
|
+
web_search = resolve_web_search_runtime_status(cfg=cfg, api_key=api_key.key)
|
|
1200
|
+
try:
|
|
1201
|
+
sandbox = diagnose_sandbox(cfg, include_smoke=False, include_server_image=False)
|
|
1202
|
+
sandbox_payload: dict[str, Any] = {
|
|
1203
|
+
"ready": sandbox.ready,
|
|
1204
|
+
"status": sandbox.status,
|
|
1205
|
+
"selected_backend": sandbox.selected_backend,
|
|
1206
|
+
}
|
|
1207
|
+
except ConfigError as exc:
|
|
1208
|
+
sandbox_payload = {"ready": False, "status": "config_error", "error": str(exc)}
|
|
1209
|
+
try:
|
|
1210
|
+
update_status = status_from_cache(current_version=__version__, cfg=cfg).to_json()
|
|
1211
|
+
except Exception as exc: # noqa: BLE001 - summary should remain best effort.
|
|
1212
|
+
update_status = {"state": "unknown", "error": str(redact_secrets(str(exc)))}
|
|
1213
|
+
return {
|
|
1214
|
+
"python": sys.version.split()[0],
|
|
1215
|
+
"platform": platform.platform(),
|
|
1216
|
+
"git": shutil.which("git") is not None,
|
|
1217
|
+
"rg": shutil.which("rg") is not None,
|
|
1218
|
+
"model_set": bool(cfg.model),
|
|
1219
|
+
"model": cfg.model,
|
|
1220
|
+
"base_url_host": _url_host(cfg.base_url),
|
|
1221
|
+
"api_key": _api_key_status(api_key),
|
|
1222
|
+
"config_path": str(config_path()),
|
|
1223
|
+
"update": update_status,
|
|
1224
|
+
"web_search": {
|
|
1225
|
+
"availability": web_search.availability_label,
|
|
1226
|
+
"provider": web_search.provider,
|
|
1227
|
+
"setup_hint": web_search.setup_hint,
|
|
1228
|
+
},
|
|
1229
|
+
"sandbox": sandbox_payload,
|
|
1230
|
+
"custom_tools_enabled": bool(cfg.custom_tools_enabled),
|
|
1231
|
+
"skills_enabled": bool(getattr(cfg, "skills_enabled", True)),
|
|
1232
|
+
"stream": bool(cfg.stream),
|
|
1233
|
+
"secret_values_included": False,
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
|
|
1237
|
+
def _doctor_providers(_params: dict[str, Any], _request_id: RequestId) -> dict[str, Any]:
|
|
1238
|
+
cfg = load_config()
|
|
1239
|
+
diagnostics = build_provider_diagnostics(cfg)
|
|
1240
|
+
return {
|
|
1241
|
+
"diagnostics": _rows_to_dict(diagnostics.rows()),
|
|
1242
|
+
"last_provider_call": last_provider_call_summary(),
|
|
1243
|
+
"last_web_search": last_web_search_summary(),
|
|
1244
|
+
"secret_values_included": False,
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
|
|
1248
|
+
def _doctor_providers_live(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
1249
|
+
# One minimal live text request against the active profile. Never passive: the caller must
|
|
1250
|
+
# send allow_live=true, which the extension only does on an explicit user action (Connect /
|
|
1251
|
+
# Test connection). Mirrors `alysis doctor providers --live` and stays fully redacted.
|
|
1252
|
+
if not _optional_bool(params, "allow_live", default=False):
|
|
1253
|
+
raise ProtocolError(
|
|
1254
|
+
"missing_param",
|
|
1255
|
+
"doctor.providers.live requires allow_live=true; it performs one minimal live "
|
|
1256
|
+
"provider request after explicit user intent.",
|
|
1257
|
+
request_id=request_id,
|
|
1258
|
+
)
|
|
1259
|
+
cfg = load_config()
|
|
1260
|
+
validation = validate_active_provider_live(
|
|
1261
|
+
cfg,
|
|
1262
|
+
timeout_s=float(_positive_int(params, "timeout_s", default=15, upper=60)),
|
|
1263
|
+
)
|
|
1264
|
+
return {
|
|
1265
|
+
"validation": {
|
|
1266
|
+
"profile": validation.profile_name,
|
|
1267
|
+
"provider_key": validation.provider_key,
|
|
1268
|
+
"protocol": validation.protocol,
|
|
1269
|
+
"model": validation.model,
|
|
1270
|
+
"status": validation.status,
|
|
1271
|
+
"message": redact_secrets(validation.message),
|
|
1272
|
+
},
|
|
1273
|
+
"ok": validation.ok,
|
|
1274
|
+
"network_used": True,
|
|
1275
|
+
"secret_values_included": False,
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
|
|
1279
|
+
def _doctor_bundle(_params: dict[str, Any], _request_id: RequestId) -> dict[str, Any]:
|
|
1280
|
+
cfg = load_config()
|
|
1281
|
+
diagnostics = build_provider_diagnostics(cfg)
|
|
1282
|
+
return {
|
|
1283
|
+
"bundle": diagnostic_bundle_payload(provider_diagnostics=_rows_to_dict(diagnostics.rows())),
|
|
1284
|
+
"secret_values_included": False,
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
|
|
1288
|
+
def _sandbox_doctor(params: dict[str, Any], _request_id: RequestId) -> dict[str, Any]:
|
|
1289
|
+
cfg = load_config()
|
|
1290
|
+
diagnostic = diagnose_sandbox(
|
|
1291
|
+
cfg,
|
|
1292
|
+
include_smoke=_optional_bool(params, "smoke", default=False),
|
|
1293
|
+
include_server_image=_optional_bool(params, "include_server", default=True),
|
|
1294
|
+
)
|
|
1295
|
+
result = {
|
|
1296
|
+
"diagnostic": _dataclass_payload(diagnostic),
|
|
1297
|
+
"message": format_sandbox_problem_message(diagnostic),
|
|
1298
|
+
}
|
|
1299
|
+
if _optional_bool(params, "env", default=False):
|
|
1300
|
+
result["env"] = sandbox_env_summary()
|
|
1301
|
+
return result
|
|
1302
|
+
|
|
1303
|
+
|
|
1304
|
+
def _sandbox_pull(params: dict[str, Any], _request_id: RequestId) -> dict[str, Any]:
|
|
1305
|
+
cfg = load_config()
|
|
1306
|
+
images = _string_list_param(params.get("images"))
|
|
1307
|
+
if not images:
|
|
1308
|
+
images = list(
|
|
1309
|
+
configured_sandbox_images(
|
|
1310
|
+
cfg,
|
|
1311
|
+
include_server=_optional_bool(params, "include_server", default=True),
|
|
1312
|
+
)
|
|
1313
|
+
)
|
|
1314
|
+
result = pull_sandbox_images(
|
|
1315
|
+
images,
|
|
1316
|
+
timeout_s=_positive_int(params, "timeout_s", default=900, upper=3600),
|
|
1317
|
+
)
|
|
1318
|
+
return {"result": _dataclass_payload(result), "changed": True}
|
|
1319
|
+
|
|
1320
|
+
|
|
1321
|
+
def _sandbox_setup(params: dict[str, Any], _request_id: RequestId) -> dict[str, Any]:
|
|
1322
|
+
cfg = load_config()
|
|
1323
|
+
diagnostic = diagnose_sandbox(cfg, include_smoke=False, include_server_image=True)
|
|
1324
|
+
pull_result = None
|
|
1325
|
+
if (
|
|
1326
|
+
not diagnostic.ready
|
|
1327
|
+
and diagnostic.can_pull
|
|
1328
|
+
and _optional_bool(params, "pull", default=True)
|
|
1329
|
+
):
|
|
1330
|
+
pull_result = pull_sandbox_images(configured_sandbox_images(cfg, include_server=True))
|
|
1331
|
+
diagnostic = diagnose_sandbox(cfg, include_smoke=True, include_server_image=True)
|
|
1332
|
+
return {
|
|
1333
|
+
"diagnostic": _dataclass_payload(diagnostic),
|
|
1334
|
+
"pull_result": _dataclass_payload(pull_result) if pull_result is not None else None,
|
|
1335
|
+
"message": format_sandbox_problem_message(diagnostic),
|
|
1336
|
+
"changed": pull_result is not None,
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
|
|
1340
|
+
def _update_check(params: dict[str, Any], _request_id: RequestId) -> dict[str, Any]:
|
|
1341
|
+
cfg = load_config()
|
|
1342
|
+
force = _optional_bool(params, "force", default=False)
|
|
1343
|
+
allow_network = _optional_bool(params, "allow_network", default=False)
|
|
1344
|
+
cached = _optional_bool(params, "cached", default=not (force or allow_network))
|
|
1345
|
+
network_used = bool((force or allow_network) and not cached)
|
|
1346
|
+
status = (
|
|
1347
|
+
check_for_updates(current_version=__version__, cfg=cfg, force=True)
|
|
1348
|
+
if network_used
|
|
1349
|
+
else status_from_cache(current_version=__version__, cfg=cfg)
|
|
1350
|
+
)
|
|
1351
|
+
return {
|
|
1352
|
+
"status": status.to_json(),
|
|
1353
|
+
"network_used": network_used,
|
|
1354
|
+
"cached": not network_used,
|
|
1355
|
+
"allow_network": allow_network,
|
|
1356
|
+
"force": force,
|
|
1357
|
+
"secret_values_included": False,
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
|
|
1361
|
+
def _report_create(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
1362
|
+
workspace_root = resolve_feedback_workspace_root(
|
|
1363
|
+
_workspace_root_from_params(params, request_id=request_id, required=True)
|
|
1364
|
+
)
|
|
1365
|
+
cfg = load_config()
|
|
1366
|
+
result = create_feedback_bundle(
|
|
1367
|
+
workspace_root=workspace_root,
|
|
1368
|
+
feedback_text=_optional_str_or_none(params, "feedback"),
|
|
1369
|
+
cfg=cfg,
|
|
1370
|
+
session_id=_optional_str_or_none(params, "session_id"),
|
|
1371
|
+
run_id=_optional_str_or_none(params, "run_id"),
|
|
1372
|
+
latest=_optional_bool(params, "latest", default=False),
|
|
1373
|
+
)
|
|
1374
|
+
github_enabled = params.get("github") if "github" in params else False
|
|
1375
|
+
issue_result = None
|
|
1376
|
+
issue_status_lines: list[str] = []
|
|
1377
|
+
if github_enabled is not False and not _optional_bool(params, "local_only", default=True):
|
|
1378
|
+
issue_result = create_feedback_github_issue_draft(
|
|
1379
|
+
bundle_result=result,
|
|
1380
|
+
feedback_text=_optional_str_or_none(params, "feedback"),
|
|
1381
|
+
cfg=cfg,
|
|
1382
|
+
github_enabled=bool(github_enabled),
|
|
1383
|
+
open_browser=False,
|
|
1384
|
+
)
|
|
1385
|
+
issue_status_lines = list(feedback_github_issue_status_lines(issue_result))
|
|
1386
|
+
return {
|
|
1387
|
+
"bundle": _dataclass_payload(result),
|
|
1388
|
+
"github_issue": _dataclass_payload(issue_result) if issue_result is not None else None,
|
|
1389
|
+
"github_status_lines": issue_status_lines,
|
|
1390
|
+
"changed": True,
|
|
1391
|
+
"secret_values_included": False,
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
|
|
1395
|
+
def _mcp_status(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
1396
|
+
manager = _mcp_manager(params, request_id=request_id)
|
|
1397
|
+
bootstrap_errors: list[str] = []
|
|
1398
|
+
prompt_errors: dict[str, str] = {}
|
|
1399
|
+
try:
|
|
1400
|
+
try:
|
|
1401
|
+
_ = manager.tool_bindings
|
|
1402
|
+
except (ConfigError, RuntimeError) as exc:
|
|
1403
|
+
bootstrap_errors.append(str(redact_secrets(str(exc))))
|
|
1404
|
+
prompt_enabled_ids = [
|
|
1405
|
+
str(server_id)
|
|
1406
|
+
for server_id in manager.catalog_snapshot_metadata().get("prompt_enabled_server_ids")
|
|
1407
|
+
or []
|
|
1408
|
+
]
|
|
1409
|
+
for prompt_server_id in prompt_enabled_ids:
|
|
1410
|
+
try:
|
|
1411
|
+
manager.list_prompts(server_id=prompt_server_id, limit=1)
|
|
1412
|
+
except (ConfigError, RuntimeError) as exc:
|
|
1413
|
+
prompt_errors[prompt_server_id] = str(redact_secrets(str(exc)))
|
|
1414
|
+
payload = _mcp_status_payload(
|
|
1415
|
+
manager=manager,
|
|
1416
|
+
bootstrap_errors=bootstrap_errors,
|
|
1417
|
+
prompt_errors=prompt_errors,
|
|
1418
|
+
)
|
|
1419
|
+
payload.pop("table_rows", None)
|
|
1420
|
+
payload["secret_values_included"] = False
|
|
1421
|
+
return _redact_payload(payload)
|
|
1422
|
+
finally:
|
|
1423
|
+
manager.close()
|
|
1424
|
+
|
|
1425
|
+
|
|
1426
|
+
def _mcp_prompts_list(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
1427
|
+
manager = _mcp_manager(params, request_id=request_id)
|
|
1428
|
+
try:
|
|
1429
|
+
payload = manager.list_prompts(
|
|
1430
|
+
server_id=_optional_str_or_none(params, "server"),
|
|
1431
|
+
query=_optional_str_or_none(params, "query"),
|
|
1432
|
+
limit=_positive_int(params, "limit", default=20, upper=50),
|
|
1433
|
+
refresh=_optional_bool(params, "refresh", default=False),
|
|
1434
|
+
)
|
|
1435
|
+
payload["secret_values_included"] = False
|
|
1436
|
+
return _redact_payload(payload)
|
|
1437
|
+
finally:
|
|
1438
|
+
manager.close()
|
|
1439
|
+
|
|
1440
|
+
|
|
1441
|
+
def _mcp_prompts_get(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
1442
|
+
manager = _mcp_manager(params, request_id=request_id)
|
|
1443
|
+
try:
|
|
1444
|
+
payload = manager.get_prompt(
|
|
1445
|
+
server_id=_required_str(params, "server_id", request_id=request_id),
|
|
1446
|
+
prompt_name=_required_str_any(params, ("prompt_name", "name"), request_id=request_id),
|
|
1447
|
+
arguments=_string_dict_param(params.get("arguments"), request_id=request_id),
|
|
1448
|
+
refresh=_optional_bool(params, "refresh", default=False),
|
|
1449
|
+
)
|
|
1450
|
+
payload["secret_values_included"] = False
|
|
1451
|
+
return _redact_payload(payload)
|
|
1452
|
+
finally:
|
|
1453
|
+
manager.close()
|
|
1454
|
+
|
|
1455
|
+
|
|
1456
|
+
def _mcp_auth_status(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
1457
|
+
resolved_config = _manual_mcp_resolved_config_for_path(
|
|
1458
|
+
path=_workspace_root_from_params(params, request_id=request_id, required=True)
|
|
1459
|
+
)
|
|
1460
|
+
rows = _build_mcp_auth_status_rows(
|
|
1461
|
+
resolved_config=resolved_config,
|
|
1462
|
+
server_id=_optional_str_or_none(params, "server"),
|
|
1463
|
+
)
|
|
1464
|
+
return {"rows": _redact_payload(rows), "secret_values_included": False}
|
|
1465
|
+
|
|
1466
|
+
|
|
1467
|
+
def prepare_mcp_oauth_login_request(
|
|
1468
|
+
params: dict[str, Any], *, request_id: RequestId
|
|
1469
|
+
) -> McpOAuthLoginRequest:
|
|
1470
|
+
"""Resolve a configured OAuth server into a secret-safe coordinator request."""
|
|
1471
|
+
|
|
1472
|
+
server_id = _required_str(params, "server_id", request_id=request_id)
|
|
1473
|
+
resolved_config = _manual_mcp_resolved_config_for_path(
|
|
1474
|
+
path=_workspace_root_from_params(params, request_id=request_id, required=True)
|
|
1475
|
+
)
|
|
1476
|
+
server = _require_http_oauth_server_for_auth(
|
|
1477
|
+
server=_resolve_mcp_server_by_id(resolved_config=resolved_config, server_id=server_id),
|
|
1478
|
+
command_name="login",
|
|
1479
|
+
)
|
|
1480
|
+
oauth = server.oauth
|
|
1481
|
+
if oauth is None or not server.url:
|
|
1482
|
+
raise ProtocolError(
|
|
1483
|
+
"mcp_oauth_config_error",
|
|
1484
|
+
"MCP server OAuth configuration is incomplete.",
|
|
1485
|
+
request_id=request_id,
|
|
1486
|
+
)
|
|
1487
|
+
return McpOAuthLoginRequest(
|
|
1488
|
+
server_id=server.id,
|
|
1489
|
+
resource_server_url=str(server.url),
|
|
1490
|
+
client_id=oauth.client_id,
|
|
1491
|
+
scopes=tuple(oauth.scopes or ()),
|
|
1492
|
+
authorization_server_url=oauth.authorization_server_url,
|
|
1493
|
+
)
|
|
1494
|
+
|
|
1495
|
+
|
|
1496
|
+
def _mcp_auth_login_start(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
1497
|
+
prepare_mcp_oauth_login_request(params, request_id=request_id)
|
|
1498
|
+
raise ProtocolError(
|
|
1499
|
+
"stateful_bridge_required",
|
|
1500
|
+
"MCP OAuth login requires the stateful IDE bridge coordinator.",
|
|
1501
|
+
request_id=request_id,
|
|
1502
|
+
)
|
|
1503
|
+
|
|
1504
|
+
|
|
1505
|
+
def _mcp_auth_login_status(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
1506
|
+
prepare_mcp_oauth_login_request(params, request_id=request_id)
|
|
1507
|
+
_required_str(params, "flow_id", request_id=request_id)
|
|
1508
|
+
raise ProtocolError(
|
|
1509
|
+
"stateful_bridge_required",
|
|
1510
|
+
"MCP OAuth flow status requires the stateful IDE bridge coordinator.",
|
|
1511
|
+
request_id=request_id,
|
|
1512
|
+
)
|
|
1513
|
+
|
|
1514
|
+
|
|
1515
|
+
def _mcp_auth_login_cancel(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
1516
|
+
prepare_mcp_oauth_login_request(params, request_id=request_id)
|
|
1517
|
+
_required_str(params, "flow_id", request_id=request_id)
|
|
1518
|
+
raise ProtocolError(
|
|
1519
|
+
"stateful_bridge_required",
|
|
1520
|
+
"MCP OAuth flow cancellation requires the stateful IDE bridge coordinator.",
|
|
1521
|
+
request_id=request_id,
|
|
1522
|
+
)
|
|
1523
|
+
|
|
1524
|
+
|
|
1525
|
+
def _mcp_auth_logout(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
1526
|
+
if not _confirmed(params):
|
|
1527
|
+
return _confirmation_required("mcp.auth.logout", "clear stored MCP OAuth tokens")
|
|
1528
|
+
server_id = _required_str(params, "server_id", request_id=request_id)
|
|
1529
|
+
resolved_config = _manual_mcp_resolved_config_for_path(
|
|
1530
|
+
path=_workspace_root_from_params(params, request_id=request_id, required=True)
|
|
1531
|
+
)
|
|
1532
|
+
server = _require_http_oauth_server_for_auth(
|
|
1533
|
+
server=_resolve_mcp_server_by_id(resolved_config=resolved_config, server_id=server_id),
|
|
1534
|
+
command_name="logout",
|
|
1535
|
+
)
|
|
1536
|
+
removed = delete_oauth_token_record(server.id)
|
|
1537
|
+
return {
|
|
1538
|
+
"server_id": server.id,
|
|
1539
|
+
"removed": bool(removed),
|
|
1540
|
+
"changed": bool(removed),
|
|
1541
|
+
"secret_values_included": False,
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
|
|
1545
|
+
def _hooks_list(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
1546
|
+
workspace_root = _workspace_root_from_params(params, request_id=request_id, required=True)
|
|
1547
|
+
entries: list[dict[str, Any]] = []
|
|
1548
|
+
statuses = _collect_hook_source_statuses(workspace_root=workspace_root)
|
|
1549
|
+
for status in statuses:
|
|
1550
|
+
if not status.exists or status.status == "invalid":
|
|
1551
|
+
continue
|
|
1552
|
+
try:
|
|
1553
|
+
config_file = load_hook_config_file(status.path)
|
|
1554
|
+
except ConfigError:
|
|
1555
|
+
continue
|
|
1556
|
+
for event_name, groups in config_file.hooks.items():
|
|
1557
|
+
for group in groups:
|
|
1558
|
+
if not group.enabled:
|
|
1559
|
+
continue
|
|
1560
|
+
for hook in sorted(
|
|
1561
|
+
(item for item in group.hooks if item.enabled),
|
|
1562
|
+
key=lambda item: (-item.priority, item.id or "", item.command),
|
|
1563
|
+
):
|
|
1564
|
+
entries.append(
|
|
1565
|
+
{
|
|
1566
|
+
"source_scope": status.source_scope,
|
|
1567
|
+
"status": status.status,
|
|
1568
|
+
"trusted": bool(status.trusted),
|
|
1569
|
+
"event": event_name,
|
|
1570
|
+
"matcher": group.matcher or "*",
|
|
1571
|
+
"id": hook.id or "",
|
|
1572
|
+
"priority": hook.priority,
|
|
1573
|
+
"failure_policy": hook.failure_policy,
|
|
1574
|
+
"runtime_kinds": list(hook.runtime_kinds or ()),
|
|
1575
|
+
"session_source": list(hook.session_source or ()),
|
|
1576
|
+
"command": str(redact_secrets(hook.command)),
|
|
1577
|
+
}
|
|
1578
|
+
)
|
|
1579
|
+
return {
|
|
1580
|
+
"workspace_root": str(workspace_root),
|
|
1581
|
+
"sources": [_hook_source_status_payload(status) for status in statuses],
|
|
1582
|
+
"hooks": entries,
|
|
1583
|
+
"count": len(entries),
|
|
1584
|
+
"secret_values_included": False,
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
|
|
1588
|
+
def _hooks_doctor(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
1589
|
+
workspace_root = _workspace_root_from_params(params, request_id=request_id, required=True)
|
|
1590
|
+
statuses = _collect_hook_source_statuses(workspace_root=workspace_root)
|
|
1591
|
+
result: dict[str, Any] = {
|
|
1592
|
+
"workspace_root": str(workspace_root),
|
|
1593
|
+
"sources": [_hook_source_status_payload(status) for status in statuses],
|
|
1594
|
+
"effective": {},
|
|
1595
|
+
"matcher_errors": [],
|
|
1596
|
+
"untrusted_project_paths": [],
|
|
1597
|
+
"secret_values_included": False,
|
|
1598
|
+
}
|
|
1599
|
+
try:
|
|
1600
|
+
resolved = load_resolved_hooks_config(workspace_root)
|
|
1601
|
+
result["effective"] = {
|
|
1602
|
+
event: len(groups) for event, groups in resolved.groups_by_event.items()
|
|
1603
|
+
}
|
|
1604
|
+
matcher_errors: list[dict[str, str]] = []
|
|
1605
|
+
for event_name, groups in resolved.groups_by_event.items():
|
|
1606
|
+
for group in groups:
|
|
1607
|
+
if not group.matcher:
|
|
1608
|
+
continue
|
|
1609
|
+
try:
|
|
1610
|
+
re.compile(group.matcher)
|
|
1611
|
+
except re.error as exc:
|
|
1612
|
+
matcher_errors.append(
|
|
1613
|
+
{
|
|
1614
|
+
"event": event_name,
|
|
1615
|
+
"source_path": str(group.source_path),
|
|
1616
|
+
"matcher": group.matcher,
|
|
1617
|
+
"error": str(redact_secrets(str(exc))),
|
|
1618
|
+
}
|
|
1619
|
+
)
|
|
1620
|
+
result["matcher_errors"] = matcher_errors
|
|
1621
|
+
result["untrusted_project_paths"] = [str(path) for path in resolved.untrusted_project_paths]
|
|
1622
|
+
except ConfigError as exc:
|
|
1623
|
+
result["effective_error"] = str(redact_secrets(str(exc)))
|
|
1624
|
+
return result
|
|
1625
|
+
|
|
1626
|
+
|
|
1627
|
+
def _hooks_trace(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
1628
|
+
cfg = load_config()
|
|
1629
|
+
sessions_dir = resolve_sessions_dir(cfg)
|
|
1630
|
+
session_id = _optional_str_or_none(params, "session_id")
|
|
1631
|
+
if not session_id:
|
|
1632
|
+
infos = list_sessions(sessions_dir)
|
|
1633
|
+
if not infos:
|
|
1634
|
+
return {"session_id": None, "events": [], "count": 0, "secret_values_included": False}
|
|
1635
|
+
session_id = infos[0].session_id
|
|
1636
|
+
artifact_path = hook_audit_artifact_path(sessions_dir=sessions_dir, session_id=session_id)
|
|
1637
|
+
events = list(read_hook_audit_events(artifact_path))
|
|
1638
|
+
limit = _positive_int(params, "limit", default=200, upper=2_000)
|
|
1639
|
+
limited = events[-limit:]
|
|
1640
|
+
return {
|
|
1641
|
+
"session_id": session_id,
|
|
1642
|
+
"artifact_path": str(artifact_path),
|
|
1643
|
+
"events": _redact_payload(limited),
|
|
1644
|
+
"count": len(limited),
|
|
1645
|
+
"total_count": len(events),
|
|
1646
|
+
"secret_values_included": False,
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
|
|
1650
|
+
def _hooks_test(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
1651
|
+
workspace_root = _workspace_root_from_params(params, request_id=request_id, required=True)
|
|
1652
|
+
canonical_event = _canonical_hook_event(params, request_id=request_id)
|
|
1653
|
+
runtime_kind = _runtime_kind_value(
|
|
1654
|
+
params,
|
|
1655
|
+
key="runtime_kind",
|
|
1656
|
+
default="interactive_chat",
|
|
1657
|
+
request_id=request_id,
|
|
1658
|
+
)
|
|
1659
|
+
session_source = _optional_str(params, "session_source", default="startup").strip().lower()
|
|
1660
|
+
if session_source not in _HOOK_SESSION_SOURCES:
|
|
1661
|
+
raise ProtocolError(
|
|
1662
|
+
"invalid_field",
|
|
1663
|
+
"session_source must be one of: startup, resume, fork.",
|
|
1664
|
+
request_id=request_id,
|
|
1665
|
+
)
|
|
1666
|
+
tool = _optional_str(params, "tool", default="").strip()
|
|
1667
|
+
resolved = load_resolved_hooks_config(workspace_root)
|
|
1668
|
+
rows: list[dict[str, Any]] = []
|
|
1669
|
+
for group in resolved.groups_for_event(canonical_event):
|
|
1670
|
+
matcher = str(group.matcher or "")
|
|
1671
|
+
for hook in group.hooks:
|
|
1672
|
+
matched, reason = _hook_test_match_result(
|
|
1673
|
+
event_name=canonical_event,
|
|
1674
|
+
matcher_target=tool,
|
|
1675
|
+
runtime_kind=runtime_kind,
|
|
1676
|
+
session_source=session_source,
|
|
1677
|
+
matcher=matcher,
|
|
1678
|
+
hook=hook,
|
|
1679
|
+
)
|
|
1680
|
+
rows.append(
|
|
1681
|
+
{
|
|
1682
|
+
"source_scope": group.source_scope,
|
|
1683
|
+
"trusted": bool(group.trusted),
|
|
1684
|
+
"matcher": matcher or "*",
|
|
1685
|
+
"id": hook.id or "",
|
|
1686
|
+
"runtime_kinds": list(hook.runtime_kinds or ()),
|
|
1687
|
+
"session_source": list(hook.session_source or ()),
|
|
1688
|
+
"matched": bool(matched),
|
|
1689
|
+
"reason": reason,
|
|
1690
|
+
"command": str(redact_secrets(hook.command)),
|
|
1691
|
+
}
|
|
1692
|
+
)
|
|
1693
|
+
return {
|
|
1694
|
+
"workspace_root": str(workspace_root),
|
|
1695
|
+
"event": canonical_event,
|
|
1696
|
+
"matches": rows,
|
|
1697
|
+
"ignored_untrusted_project_paths": [str(path) for path in resolved.untrusted_project_paths],
|
|
1698
|
+
"secret_values_included": False,
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
|
|
1702
|
+
def _hooks_trust(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
1703
|
+
target = _required_str(params, "target", request_id=request_id)
|
|
1704
|
+
if target != "project_config":
|
|
1705
|
+
raise ProtocolError(
|
|
1706
|
+
"invalid_field",
|
|
1707
|
+
"hooks.trust currently supports only target='project_config'.",
|
|
1708
|
+
request_id=request_id,
|
|
1709
|
+
)
|
|
1710
|
+
workspace_root, config_path = _project_hooks_config(
|
|
1711
|
+
params, request_id=request_id, validate=True
|
|
1712
|
+
)
|
|
1713
|
+
trust_project_hooks_config(workspace_root=workspace_root, config_path=config_path)
|
|
1714
|
+
return {
|
|
1715
|
+
"target": target,
|
|
1716
|
+
"workspace_root": str(workspace_root),
|
|
1717
|
+
"config_path": str(config_path),
|
|
1718
|
+
"trusted": True,
|
|
1719
|
+
"changed": True,
|
|
1720
|
+
"secret_values_included": False,
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
|
|
1724
|
+
def _hooks_untrust(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
1725
|
+
target = _required_str(params, "target", request_id=request_id)
|
|
1726
|
+
if target != "project_config":
|
|
1727
|
+
raise ProtocolError(
|
|
1728
|
+
"invalid_field",
|
|
1729
|
+
"hooks.untrust currently supports only target='project_config'.",
|
|
1730
|
+
request_id=request_id,
|
|
1731
|
+
)
|
|
1732
|
+
workspace_root, config_path = _project_hooks_config(
|
|
1733
|
+
params, request_id=request_id, validate=False
|
|
1734
|
+
)
|
|
1735
|
+
untrust_project_hooks_config(workspace_root=workspace_root, config_path=config_path)
|
|
1736
|
+
return {
|
|
1737
|
+
"target": target,
|
|
1738
|
+
"workspace_root": str(workspace_root),
|
|
1739
|
+
"config_path": str(config_path),
|
|
1740
|
+
"trusted": False,
|
|
1741
|
+
"changed": True,
|
|
1742
|
+
"secret_values_included": False,
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
|
|
1746
|
+
def _hooks_init(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
1747
|
+
workspace_root = _workspace_root_from_params(params, request_id=request_id, required=True)
|
|
1748
|
+
local_config_path = project_local_hooks_config_path(workspace_root)
|
|
1749
|
+
force = _optional_bool(params, "force", default=False)
|
|
1750
|
+
if local_config_path.exists() and not force:
|
|
1751
|
+
return _confirmation_required("hooks.init", "overwrite existing local hooks config")
|
|
1752
|
+
local_config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1753
|
+
local_config_path.write_text(
|
|
1754
|
+
json.dumps(_HOOKS_INIT_TEMPLATE, indent=2, sort_keys=True) + "\n",
|
|
1755
|
+
encoding="utf-8",
|
|
1756
|
+
)
|
|
1757
|
+
gitignore_changed = False
|
|
1758
|
+
gitignore_path = workspace_root / ".gitignore"
|
|
1759
|
+
gitignore_entry = ".alysis/hooks.local.json"
|
|
1760
|
+
if gitignore_path.exists():
|
|
1761
|
+
existing = gitignore_path.read_text(encoding="utf-8")
|
|
1762
|
+
already_ignored = any(
|
|
1763
|
+
line.strip() == gitignore_entry or line.strip() == ".alysis/"
|
|
1764
|
+
for line in existing.splitlines()
|
|
1765
|
+
)
|
|
1766
|
+
if not already_ignored:
|
|
1767
|
+
with gitignore_path.open("a", encoding="utf-8") as handle:
|
|
1768
|
+
if not existing.endswith("\n"):
|
|
1769
|
+
handle.write("\n")
|
|
1770
|
+
handle.write(f"{gitignore_entry}\n")
|
|
1771
|
+
gitignore_changed = True
|
|
1772
|
+
return {
|
|
1773
|
+
"workspace_root": str(workspace_root),
|
|
1774
|
+
"config_path": str(local_config_path),
|
|
1775
|
+
"changed": True,
|
|
1776
|
+
"gitignore_changed": gitignore_changed,
|
|
1777
|
+
"secret_values_included": False,
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
|
|
1781
|
+
def _hooks_effective(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
1782
|
+
workspace_root = _workspace_root_from_params(params, request_id=request_id, required=True)
|
|
1783
|
+
canonical_event = _canonical_hook_event(params, request_id=request_id)
|
|
1784
|
+
tool = _optional_str_or_none(params, "tool")
|
|
1785
|
+
runtime = _runtime_kind_value(
|
|
1786
|
+
params,
|
|
1787
|
+
key="runtime",
|
|
1788
|
+
default="interactive_chat",
|
|
1789
|
+
request_id=request_id,
|
|
1790
|
+
)
|
|
1791
|
+
session_source = _optional_str_or_none(params, "session_source")
|
|
1792
|
+
resolved = load_resolved_hooks_config(workspace_root)
|
|
1793
|
+
rows: list[dict[str, Any]] = []
|
|
1794
|
+
order = 0
|
|
1795
|
+
for group in resolved.groups_for_event(canonical_event):
|
|
1796
|
+
matcher_pass = True
|
|
1797
|
+
matcher_reason = ""
|
|
1798
|
+
if tool is not None and canonical_event in {"PreToolUse", "PostToolUse", "SubagentStop"}:
|
|
1799
|
+
if group.matcher:
|
|
1800
|
+
try:
|
|
1801
|
+
matcher_pass = re.compile(group.matcher).search(tool) is not None
|
|
1802
|
+
if not matcher_pass:
|
|
1803
|
+
matcher_reason = f"matcher {group.matcher!r} does not match {tool!r}"
|
|
1804
|
+
except re.error as exc:
|
|
1805
|
+
matcher_pass = False
|
|
1806
|
+
matcher_reason = f"matcher regex error: {exc}"
|
|
1807
|
+
for hook in group.hooks:
|
|
1808
|
+
order += 1
|
|
1809
|
+
fires = matcher_pass
|
|
1810
|
+
reason = matcher_reason
|
|
1811
|
+
if fires and hook.runtime_kinds and runtime not in set(hook.runtime_kinds):
|
|
1812
|
+
fires = False
|
|
1813
|
+
reason = f"runtime_kind {runtime!r} not in {list(hook.runtime_kinds)}"
|
|
1814
|
+
if fires and hook.session_source:
|
|
1815
|
+
if session_source is None:
|
|
1816
|
+
fires = False
|
|
1817
|
+
reason = "sessionSource filter set but session_source not provided"
|
|
1818
|
+
elif session_source not in set(hook.session_source):
|
|
1819
|
+
fires = False
|
|
1820
|
+
reason = f"session_source {session_source!r} not in {list(hook.session_source)}"
|
|
1821
|
+
rows.append(
|
|
1822
|
+
{
|
|
1823
|
+
"order": order,
|
|
1824
|
+
"id": hook.id or "",
|
|
1825
|
+
"fires": bool(fires),
|
|
1826
|
+
"reason": reason or "",
|
|
1827
|
+
"priority": hook.priority,
|
|
1828
|
+
"source_scope": group.source_scope,
|
|
1829
|
+
"source_path": str(group.source_path),
|
|
1830
|
+
"command": str(redact_secrets(hook.command)),
|
|
1831
|
+
}
|
|
1832
|
+
)
|
|
1833
|
+
return {
|
|
1834
|
+
"workspace_root": str(workspace_root),
|
|
1835
|
+
"event": canonical_event,
|
|
1836
|
+
"hooks": rows,
|
|
1837
|
+
"count": len(rows),
|
|
1838
|
+
"secret_values_included": False,
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
|
|
1842
|
+
def _hooks_enable(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
1843
|
+
return _set_hook_enabled_for_protocol(params, request_id=request_id, enabled=True)
|
|
1844
|
+
|
|
1845
|
+
|
|
1846
|
+
def _hooks_disable(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
1847
|
+
return _set_hook_enabled_for_protocol(params, request_id=request_id, enabled=False)
|
|
1848
|
+
|
|
1849
|
+
|
|
1850
|
+
def _conventions_list(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
1851
|
+
workspace_root, focus_path = _conventions_paths(params, request_id=request_id)
|
|
1852
|
+
documents = load_repo_conventions(focus_path=focus_path, workspace_root=workspace_root)
|
|
1853
|
+
return {
|
|
1854
|
+
"workspace_root": str(workspace_root),
|
|
1855
|
+
"focus_path": str(focus_path),
|
|
1856
|
+
"documents": [_conventions_document_payload(doc, workspace_root) for doc in documents],
|
|
1857
|
+
"count": len(documents),
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
|
|
1861
|
+
def _conventions_render(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
1862
|
+
workspace_root, focus_path = _conventions_paths(params, request_id=request_id)
|
|
1863
|
+
documents = load_repo_conventions(focus_path=focus_path, workspace_root=workspace_root)
|
|
1864
|
+
max_chars = _positive_int(
|
|
1865
|
+
params,
|
|
1866
|
+
"max_chars",
|
|
1867
|
+
default=_CONVENTIONS_RENDER_DEFAULT_MAX_CHARS,
|
|
1868
|
+
upper=_CONVENTIONS_RENDER_MAX_CHARS,
|
|
1869
|
+
)
|
|
1870
|
+
max_bytes = _positive_int(
|
|
1871
|
+
params,
|
|
1872
|
+
"max_bytes",
|
|
1873
|
+
default=_CONVENTIONS_RENDER_DEFAULT_MAX_BYTES,
|
|
1874
|
+
upper=_CONVENTIONS_RENDER_MAX_BYTES,
|
|
1875
|
+
)
|
|
1876
|
+
rendered = render_repo_conventions_context(
|
|
1877
|
+
documents=documents,
|
|
1878
|
+
max_chars=max_chars,
|
|
1879
|
+
)
|
|
1880
|
+
redacted_rendered = _redact_payload(rendered) if rendered is not None else None
|
|
1881
|
+
if isinstance(redacted_rendered, str):
|
|
1882
|
+
limited_rendered, truncated_by_bytes = _truncate_text_utf8(redacted_rendered, max_bytes)
|
|
1883
|
+
else:
|
|
1884
|
+
limited_rendered = None
|
|
1885
|
+
truncated_by_bytes = False
|
|
1886
|
+
rendered_bytes = len((limited_rendered or "").encode("utf-8"))
|
|
1887
|
+
truncated_by_chars = rendered is not None and len(rendered) >= max_chars
|
|
1888
|
+
return {
|
|
1889
|
+
"workspace_root": str(workspace_root),
|
|
1890
|
+
"focus_path": str(focus_path),
|
|
1891
|
+
"rendered": limited_rendered,
|
|
1892
|
+
"document_count": len(documents),
|
|
1893
|
+
"truncated_or_limited": truncated_by_chars or truncated_by_bytes,
|
|
1894
|
+
"truncated": truncated_by_chars or truncated_by_bytes,
|
|
1895
|
+
"truncated_by_chars": truncated_by_chars,
|
|
1896
|
+
"truncated_by_bytes": truncated_by_bytes,
|
|
1897
|
+
"max_chars": max_chars,
|
|
1898
|
+
"max_bytes": max_bytes,
|
|
1899
|
+
"rendered_chars": len(limited_rendered or ""),
|
|
1900
|
+
"rendered_bytes": rendered_bytes,
|
|
1901
|
+
"secret_values_included": False,
|
|
1902
|
+
"redacted": True,
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
|
|
1906
|
+
def _ext_search(params: dict[str, Any], _request_id: RequestId) -> dict[str, Any]:
|
|
1907
|
+
query = _required_str(params, "query", request_id=_request_id)
|
|
1908
|
+
registry = load_registry()
|
|
1909
|
+
matches = search_extensions(registry, query)
|
|
1910
|
+
return {
|
|
1911
|
+
"extensions": [_registry_entry_payload(entry) for entry in matches],
|
|
1912
|
+
"count": len(matches),
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
|
|
1916
|
+
def _ext_list(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
1917
|
+
repo_root = _workspace_root_from_params(params, request_id=request_id, required=False)
|
|
1918
|
+
global_state = load_global_state()
|
|
1919
|
+
project_overrides = load_project_overrides(repo_root)
|
|
1920
|
+
project_state = load_project_state(repo_root)
|
|
1921
|
+
effective_enabled = compute_effective_enabled(global_state, project_overrides)
|
|
1922
|
+
installed: list[dict[str, Any]] = []
|
|
1923
|
+
for ext_id in sorted(set(global_state.installed) | set(project_state.installed)):
|
|
1924
|
+
record = global_state.installed.get(ext_id) or project_state.installed.get(ext_id)
|
|
1925
|
+
if record is None:
|
|
1926
|
+
continue
|
|
1927
|
+
installed.append(_installed_plugin_payload(ext_id, record, effective_enabled))
|
|
1928
|
+
return {
|
|
1929
|
+
"workspace_root": str(repo_root),
|
|
1930
|
+
"extensions": installed,
|
|
1931
|
+
"count": len(installed),
|
|
1932
|
+
"project_overrides": {
|
|
1933
|
+
"enabled": sorted(project_overrides.enabled),
|
|
1934
|
+
"disabled": sorted(project_overrides.disabled),
|
|
1935
|
+
},
|
|
1936
|
+
"secret_values_included": False,
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
|
|
1940
|
+
def _ext_info(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
1941
|
+
ext_id = _required_str_any(params, ("ext_id", "id", "plugin_id"), request_id=request_id)
|
|
1942
|
+
repo_root = _workspace_root_from_params(params, request_id=request_id, required=False)
|
|
1943
|
+
registry = load_registry()
|
|
1944
|
+
global_state = load_global_state()
|
|
1945
|
+
project_overrides = load_project_overrides(repo_root)
|
|
1946
|
+
project_state = load_project_state(repo_root)
|
|
1947
|
+
entry = find_by_id(registry, ext_id)
|
|
1948
|
+
normalized = normalize_extension_id(entry.id if entry is not None else ext_id)
|
|
1949
|
+
global_record = _installed_record(global_state, normalized)
|
|
1950
|
+
project_record = _installed_record(project_state, normalized)
|
|
1951
|
+
installed_record = global_record or project_record
|
|
1952
|
+
if entry is None and installed_record is None:
|
|
1953
|
+
raise ProtocolError(
|
|
1954
|
+
"extension_not_found", f"Extension not found: {ext_id}", request_id=request_id
|
|
1955
|
+
)
|
|
1956
|
+
effective_enabled = compute_effective_enabled(global_state, project_overrides)
|
|
1957
|
+
return {
|
|
1958
|
+
"workspace_root": str(repo_root),
|
|
1959
|
+
"extension": _registry_entry_payload(entry) if entry is not None else {"id": ext_id},
|
|
1960
|
+
"installed": _dataclass_payload(installed_record),
|
|
1961
|
+
"installed_scopes": [
|
|
1962
|
+
scope
|
|
1963
|
+
for scope, record in (("user", global_record), ("project", project_record))
|
|
1964
|
+
if record is not None
|
|
1965
|
+
],
|
|
1966
|
+
"enabled_effective": normalized in effective_enabled,
|
|
1967
|
+
"project_override_state": _project_extension_override_state(project_overrides, normalized),
|
|
1968
|
+
"workspace_trust": _extension_workspace_trust_payload(repo_root),
|
|
1969
|
+
"secret_values_included": False,
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
|
|
1973
|
+
def _ext_install(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
1974
|
+
source = _required_str(params, "source", request_id=request_id)
|
|
1975
|
+
_validate_ext_install_source(source, request_id=request_id)
|
|
1976
|
+
repo_root = _workspace_root_from_params(params, request_id=request_id, required=False)
|
|
1977
|
+
project = _optional_bool(params, "project", default=False)
|
|
1978
|
+
approval = _ext_install_trust_approval_param(params, request_id=request_id)
|
|
1979
|
+
if approval is not None and not _confirmed(params):
|
|
1980
|
+
return _confirmation_required(
|
|
1981
|
+
"ext.install",
|
|
1982
|
+
"install extension package after explicit package trust review",
|
|
1983
|
+
)
|
|
1984
|
+
trust_request: Any | None = None
|
|
1985
|
+
|
|
1986
|
+
def _trust_prompt(request: Any) -> bool:
|
|
1987
|
+
nonlocal trust_request
|
|
1988
|
+
trust_request = request
|
|
1989
|
+
if approval is None:
|
|
1990
|
+
return False
|
|
1991
|
+
_validate_ext_install_trust_approval(
|
|
1992
|
+
request,
|
|
1993
|
+
approval,
|
|
1994
|
+
source=source,
|
|
1995
|
+
project=project,
|
|
1996
|
+
request_id=request_id,
|
|
1997
|
+
)
|
|
1998
|
+
return True
|
|
1999
|
+
|
|
2000
|
+
try:
|
|
2001
|
+
result = install_plugin(
|
|
2002
|
+
source=source,
|
|
2003
|
+
repo_root=repo_root,
|
|
2004
|
+
project=project,
|
|
2005
|
+
trust_prompt=_trust_prompt,
|
|
2006
|
+
)
|
|
2007
|
+
except PluginInstallError as exc:
|
|
2008
|
+
if (
|
|
2009
|
+
trust_request is not None
|
|
2010
|
+
and approval is None
|
|
2011
|
+
and str(exc) == "install rejected by user"
|
|
2012
|
+
):
|
|
2013
|
+
return _ext_install_trust_review_result(
|
|
2014
|
+
trust_request,
|
|
2015
|
+
source=source,
|
|
2016
|
+
project=project,
|
|
2017
|
+
)
|
|
2018
|
+
raise
|
|
2019
|
+
return {
|
|
2020
|
+
"result": _dataclass_payload(result),
|
|
2021
|
+
"changed": bool(getattr(result, "trust_was_prompted", True)),
|
|
2022
|
+
"secret_values_included": False,
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
|
|
2026
|
+
def _ext_uninstall(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
2027
|
+
if not _confirmed(params):
|
|
2028
|
+
return _confirmation_required("ext.uninstall", "uninstall extension package")
|
|
2029
|
+
result = uninstall_plugin(
|
|
2030
|
+
plugin_id=_required_str_any(params, ("plugin_id", "ext_id", "id"), request_id=request_id),
|
|
2031
|
+
repo_root=_workspace_root_from_params(params, request_id=request_id, required=False),
|
|
2032
|
+
project=_optional_bool(params, "project", default=False),
|
|
2033
|
+
)
|
|
2034
|
+
return {"result": _dataclass_payload(result), "changed": True, "secret_values_included": False}
|
|
2035
|
+
|
|
2036
|
+
|
|
2037
|
+
def _ext_enable(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
2038
|
+
return _set_ext_enabled(params, request_id=request_id, enabled=True)
|
|
2039
|
+
|
|
2040
|
+
|
|
2041
|
+
def _ext_disable(params: dict[str, Any], request_id: RequestId) -> dict[str, Any]:
|
|
2042
|
+
return _set_ext_enabled(params, request_id=request_id, enabled=False)
|
|
2043
|
+
|
|
2044
|
+
|
|
2045
|
+
def _set_skill_enabled(
|
|
2046
|
+
params: dict[str, Any],
|
|
2047
|
+
*,
|
|
2048
|
+
request_id: RequestId,
|
|
2049
|
+
enabled: bool,
|
|
2050
|
+
) -> dict[str, Any]:
|
|
2051
|
+
name = _required_str(params, "name", request_id=request_id)
|
|
2052
|
+
project = _optional_bool(params, "project", default=False)
|
|
2053
|
+
workspace_root, catalog = _skill_catalog(params, request_id=request_id)
|
|
2054
|
+
key = name.strip().casefold()
|
|
2055
|
+
if project:
|
|
2056
|
+
project_state = load_project_skill_state(workspace_root)
|
|
2057
|
+
if resolve_skill_catalog_entry(entries=catalog.entries, raw_name=name) is None and (
|
|
2058
|
+
key not in project_state.managed_installs
|
|
2059
|
+
):
|
|
2060
|
+
raise ProtocolError(
|
|
2061
|
+
"skill_not_found",
|
|
2062
|
+
f"Skill not found for project override: {name}",
|
|
2063
|
+
request_id=request_id,
|
|
2064
|
+
)
|
|
2065
|
+
state = set_project_skill_override(project_state, name=name, enabled=enabled)
|
|
2066
|
+
save_project_skill_state(workspace_root, state)
|
|
2067
|
+
scope = "project"
|
|
2068
|
+
else:
|
|
2069
|
+
global_state = load_global_skill_state()
|
|
2070
|
+
visible_user_skill = next(
|
|
2071
|
+
(
|
|
2072
|
+
entry
|
|
2073
|
+
for entry in catalog.entries
|
|
2074
|
+
if str(getattr(entry.skill, "source_scope", "")) == "user"
|
|
2075
|
+
and key in entry.skill.lookup_keys()
|
|
2076
|
+
),
|
|
2077
|
+
None,
|
|
2078
|
+
)
|
|
2079
|
+
if visible_user_skill is None and key not in global_state.managed_installs:
|
|
2080
|
+
raise ProtocolError(
|
|
2081
|
+
"skill_not_found", f"Global skill not found: {name}", request_id=request_id
|
|
2082
|
+
)
|
|
2083
|
+
state = set_global_skill_disabled(global_state, name=name, disabled=not enabled)
|
|
2084
|
+
save_global_skill_state(state)
|
|
2085
|
+
scope = "user"
|
|
2086
|
+
return {
|
|
2087
|
+
"workspace_root": str(workspace_root),
|
|
2088
|
+
"name": name,
|
|
2089
|
+
"enabled": enabled,
|
|
2090
|
+
"scope": scope,
|
|
2091
|
+
"changed": True,
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
|
|
2095
|
+
def _mcp_manager(params: dict[str, Any], *, request_id: RequestId) -> Any:
|
|
2096
|
+
return _manual_mcp_manager_for_path(
|
|
2097
|
+
path=_workspace_root_from_params(params, request_id=request_id, required=True),
|
|
2098
|
+
runtime_kind=_runtime_kind_value(
|
|
2099
|
+
params,
|
|
2100
|
+
key="runtime",
|
|
2101
|
+
default="interactive_chat",
|
|
2102
|
+
request_id=request_id,
|
|
2103
|
+
mcp_prompt_runtime=True,
|
|
2104
|
+
),
|
|
2105
|
+
)
|
|
2106
|
+
|
|
2107
|
+
|
|
2108
|
+
def _runtime_kind_value(
|
|
2109
|
+
params: dict[str, Any],
|
|
2110
|
+
*,
|
|
2111
|
+
key: str,
|
|
2112
|
+
default: str,
|
|
2113
|
+
request_id: RequestId,
|
|
2114
|
+
mcp_prompt_runtime: bool = False,
|
|
2115
|
+
) -> str:
|
|
2116
|
+
raw = _optional_str(params, key, default=default)
|
|
2117
|
+
try:
|
|
2118
|
+
runtime = _parse_manual_mcp_prompt_runtime(raw) if mcp_prompt_runtime else RuntimeKind(raw)
|
|
2119
|
+
except (ConfigError, ValueError) as exc:
|
|
2120
|
+
raise ProtocolError("invalid_field", str(exc), request_id=request_id) from exc
|
|
2121
|
+
return runtime.value
|
|
2122
|
+
|
|
2123
|
+
|
|
2124
|
+
def _string_dict_param(value: Any, *, request_id: RequestId) -> dict[str, str]:
|
|
2125
|
+
if value is None:
|
|
2126
|
+
return {}
|
|
2127
|
+
if not isinstance(value, dict):
|
|
2128
|
+
raise ProtocolError(
|
|
2129
|
+
"invalid_field",
|
|
2130
|
+
"arguments must be an object with string keys and values.",
|
|
2131
|
+
request_id=request_id,
|
|
2132
|
+
)
|
|
2133
|
+
parsed: dict[str, str] = {}
|
|
2134
|
+
for key, child in value.items():
|
|
2135
|
+
key_text = str(key or "").strip()
|
|
2136
|
+
if not key_text:
|
|
2137
|
+
raise ProtocolError(
|
|
2138
|
+
"invalid_field", "argument keys cannot be empty.", request_id=request_id
|
|
2139
|
+
)
|
|
2140
|
+
parsed[key_text] = str(child)
|
|
2141
|
+
return parsed
|
|
2142
|
+
|
|
2143
|
+
|
|
2144
|
+
def _hook_source_status_payload(status: Any) -> dict[str, Any]:
|
|
2145
|
+
return {
|
|
2146
|
+
"source_scope": status.source_scope,
|
|
2147
|
+
"path": str(status.path),
|
|
2148
|
+
"exists": bool(status.exists),
|
|
2149
|
+
"trusted": bool(status.trusted),
|
|
2150
|
+
"status": status.status,
|
|
2151
|
+
"event_count": int(status.event_count),
|
|
2152
|
+
"hook_count": int(status.hook_count),
|
|
2153
|
+
"issue": str(redact_secrets(status.issue)),
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
|
|
2157
|
+
def _project_hooks_config(
|
|
2158
|
+
params: dict[str, Any],
|
|
2159
|
+
*,
|
|
2160
|
+
request_id: RequestId,
|
|
2161
|
+
validate: bool,
|
|
2162
|
+
) -> tuple[Path, Path]:
|
|
2163
|
+
workspace_root = _workspace_root_from_params(params, request_id=request_id, required=True)
|
|
2164
|
+
config_path = project_hooks_config_path(workspace_root)
|
|
2165
|
+
if not config_path.exists():
|
|
2166
|
+
raise ProtocolError(
|
|
2167
|
+
"hooks_config_not_found",
|
|
2168
|
+
"Project hooks config was not found.",
|
|
2169
|
+
request_id=request_id,
|
|
2170
|
+
)
|
|
2171
|
+
if validate:
|
|
2172
|
+
load_hook_config_file(config_path)
|
|
2173
|
+
return workspace_root, config_path
|
|
2174
|
+
|
|
2175
|
+
|
|
2176
|
+
def _canonical_hook_event(params: dict[str, Any], *, request_id: RequestId) -> str:
|
|
2177
|
+
try:
|
|
2178
|
+
return canonicalize_hook_event_name(
|
|
2179
|
+
_required_str_any(params, ("event", "event_name"), request_id=request_id)
|
|
2180
|
+
)
|
|
2181
|
+
except ValueError as exc:
|
|
2182
|
+
raise ProtocolError("invalid_field", str(exc), request_id=request_id) from exc
|
|
2183
|
+
|
|
2184
|
+
|
|
2185
|
+
def _set_hook_enabled_for_protocol(
|
|
2186
|
+
params: dict[str, Any],
|
|
2187
|
+
*,
|
|
2188
|
+
request_id: RequestId,
|
|
2189
|
+
enabled: bool,
|
|
2190
|
+
) -> dict[str, Any]:
|
|
2191
|
+
workspace_root = _workspace_root_from_params(params, request_id=request_id, required=True)
|
|
2192
|
+
hook_id = _required_str(params, "hook_id", request_id=request_id)
|
|
2193
|
+
layer = _optional_str(params, "layer", default="local")
|
|
2194
|
+
config_path = _hooks_config_path_for_layer(workspace_root=workspace_root, layer=layer)
|
|
2195
|
+
if not config_path.exists():
|
|
2196
|
+
raise ProtocolError(
|
|
2197
|
+
"hooks_config_not_found", "Hooks config was not found.", request_id=request_id
|
|
2198
|
+
)
|
|
2199
|
+
result = _find_hook_in_layer(path=config_path, hook_id=hook_id)
|
|
2200
|
+
if result is None:
|
|
2201
|
+
raise ProtocolError(
|
|
2202
|
+
"hook_not_found", f"Hook id not found: {hook_id}", request_id=request_id
|
|
2203
|
+
)
|
|
2204
|
+
raw, entry = result
|
|
2205
|
+
previous = bool(entry.get("enabled", True))
|
|
2206
|
+
entry["enabled"] = enabled
|
|
2207
|
+
config_path.write_text(json.dumps(raw, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
2208
|
+
return {
|
|
2209
|
+
"workspace_root": str(workspace_root),
|
|
2210
|
+
"config_path": str(config_path),
|
|
2211
|
+
"hook_id": hook_id,
|
|
2212
|
+
"layer": layer,
|
|
2213
|
+
"enabled": enabled,
|
|
2214
|
+
"previous_enabled": previous,
|
|
2215
|
+
"changed": previous != enabled,
|
|
2216
|
+
"secret_values_included": False,
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
|
|
2220
|
+
def _conventions_paths(params: dict[str, Any], *, request_id: RequestId) -> tuple[Path, Path]:
|
|
2221
|
+
raw = params.get("focus_path", params.get("path", params.get("workspace")))
|
|
2222
|
+
if raw is None:
|
|
2223
|
+
raise ProtocolError(
|
|
2224
|
+
"missing_param",
|
|
2225
|
+
"conventions methods require workspace, path, or focus_path.",
|
|
2226
|
+
request_id=request_id,
|
|
2227
|
+
)
|
|
2228
|
+
focus_path = Path(str(raw)).expanduser().resolve()
|
|
2229
|
+
workspace_root = _workspace_root_from_params(
|
|
2230
|
+
{"workspace": str(focus_path if focus_path.is_dir() else focus_path.parent)},
|
|
2231
|
+
request_id=request_id,
|
|
2232
|
+
required=True,
|
|
2233
|
+
)
|
|
2234
|
+
try:
|
|
2235
|
+
focus_path.relative_to(workspace_root.resolve())
|
|
2236
|
+
except ValueError as exc:
|
|
2237
|
+
raise ProtocolError(
|
|
2238
|
+
"path_outside_workspace",
|
|
2239
|
+
"Conventions focus_path must stay inside the resolved workspace.",
|
|
2240
|
+
request_id=request_id,
|
|
2241
|
+
) from exc
|
|
2242
|
+
return workspace_root, focus_path
|
|
2243
|
+
|
|
2244
|
+
|
|
2245
|
+
def _conventions_document_payload(document: Any, workspace_root: Path) -> dict[str, Any]:
|
|
2246
|
+
try:
|
|
2247
|
+
display_path = document.path.relative_to(workspace_root).as_posix()
|
|
2248
|
+
except ValueError:
|
|
2249
|
+
display_path = str(document.path)
|
|
2250
|
+
return {
|
|
2251
|
+
"name": document.name,
|
|
2252
|
+
"trust_level": document.trust_level,
|
|
2253
|
+
"chars": len(document.content),
|
|
2254
|
+
"path": str(document.path),
|
|
2255
|
+
"workspace_relative_path": display_path,
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
|
|
2259
|
+
def _registry_entry_payload(entry: Any) -> dict[str, Any]:
|
|
2260
|
+
if entry is None:
|
|
2261
|
+
return {}
|
|
2262
|
+
return {
|
|
2263
|
+
"id": entry.id,
|
|
2264
|
+
"name": entry.name,
|
|
2265
|
+
"description": entry.description,
|
|
2266
|
+
"repo": entry.repo,
|
|
2267
|
+
"commit": entry.commit,
|
|
2268
|
+
"version": entry.version,
|
|
2269
|
+
"tags": list(entry.tags),
|
|
2270
|
+
"permissions": list(entry.permissions),
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
|
|
2274
|
+
def _installed_record(state: Any, ext_id: str) -> Any | None:
|
|
2275
|
+
normalized = normalize_extension_id(ext_id)
|
|
2276
|
+
for installed_id, installed in state.installed.items():
|
|
2277
|
+
if normalize_extension_id(installed_id) == normalized:
|
|
2278
|
+
return installed
|
|
2279
|
+
return None
|
|
2280
|
+
|
|
2281
|
+
|
|
2282
|
+
def _installed_plugin_payload(
|
|
2283
|
+
ext_id: str,
|
|
2284
|
+
record: Any,
|
|
2285
|
+
effective_enabled: set[str],
|
|
2286
|
+
) -> dict[str, Any]:
|
|
2287
|
+
normalized = normalize_extension_id(ext_id)
|
|
2288
|
+
return {
|
|
2289
|
+
"id": ext_id,
|
|
2290
|
+
"enabled_effective": normalized in effective_enabled,
|
|
2291
|
+
"version": getattr(record, "version", "") or "",
|
|
2292
|
+
"trust": getattr(record, "trust", "") or "",
|
|
2293
|
+
"commit": getattr(record, "commit", "") or "",
|
|
2294
|
+
"scope": getattr(record, "scope", "") or "",
|
|
2295
|
+
"installed_at": getattr(record, "installed_at", "") or "",
|
|
2296
|
+
"component_ids": _redact_payload(getattr(record, "component_ids", {}) or {}),
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
|
|
2300
|
+
def _project_extension_override_state(project_overrides: Any, normalized: str) -> str:
|
|
2301
|
+
enabled = {normalize_extension_id(item) for item in project_overrides.enabled}
|
|
2302
|
+
disabled = {normalize_extension_id(item) for item in project_overrides.disabled}
|
|
2303
|
+
if normalized in enabled:
|
|
2304
|
+
return "enabled"
|
|
2305
|
+
if normalized in disabled:
|
|
2306
|
+
return "disabled"
|
|
2307
|
+
return "absent"
|
|
2308
|
+
|
|
2309
|
+
|
|
2310
|
+
def _extension_workspace_trust_payload(repo_root: Path) -> dict[str, Any]:
|
|
2311
|
+
overrides_path = project_extensions_path(repo_root)
|
|
2312
|
+
if not overrides_path.exists():
|
|
2313
|
+
return {"state": "no_overrides_file", "path": str(overrides_path)}
|
|
2314
|
+
raw_bytes = overrides_path.read_bytes()
|
|
2315
|
+
overrides_sha256 = hashlib.sha256(raw_bytes).hexdigest()
|
|
2316
|
+
return {
|
|
2317
|
+
"state": "trusted"
|
|
2318
|
+
if extension_workspace_trusted(repo_root=repo_root, overrides_sha256=overrides_sha256)
|
|
2319
|
+
else "untrusted",
|
|
2320
|
+
"path": str(overrides_path),
|
|
2321
|
+
"sha256": overrides_sha256,
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
|
|
2325
|
+
def _set_ext_enabled(
|
|
2326
|
+
params: dict[str, Any],
|
|
2327
|
+
*,
|
|
2328
|
+
request_id: RequestId,
|
|
2329
|
+
enabled: bool,
|
|
2330
|
+
) -> dict[str, Any]:
|
|
2331
|
+
method = "ext.enable" if enabled else "ext.disable"
|
|
2332
|
+
if not _confirmed(params):
|
|
2333
|
+
return _confirmation_required(
|
|
2334
|
+
method, f"{'enable' if enabled else 'disable'} extension package"
|
|
2335
|
+
)
|
|
2336
|
+
fn = enable_plugin if enabled else disable_plugin
|
|
2337
|
+
result = fn(
|
|
2338
|
+
plugin_id=_required_str_any(params, ("plugin_id", "ext_id", "id"), request_id=request_id),
|
|
2339
|
+
repo_root=_workspace_root_from_params(params, request_id=request_id, required=False),
|
|
2340
|
+
project=_optional_bool(params, "project", default=False),
|
|
2341
|
+
)
|
|
2342
|
+
return {"result": _dataclass_payload(result), "changed": True, "secret_values_included": False}
|
|
2343
|
+
|
|
2344
|
+
|
|
2345
|
+
def _custom_tool_entries(
|
|
2346
|
+
params: dict[str, Any],
|
|
2347
|
+
*,
|
|
2348
|
+
request_id: RequestId,
|
|
2349
|
+
) -> tuple[Path, tuple[CustomToolCatalogEntry, ...]]:
|
|
2350
|
+
from ..cli_impl.commands.tools import _discover_custom_tools_for_path
|
|
2351
|
+
|
|
2352
|
+
workspace_root = _workspace_root_from_params(params, request_id=request_id, required=True)
|
|
2353
|
+
cfg = load_config()
|
|
2354
|
+
resolved_root, state = _discover_custom_tools_for_path(path=workspace_root, cfg=cfg)
|
|
2355
|
+
return resolved_root, tuple(getattr(state, "catalog_entries", ()) or ())
|
|
2356
|
+
|
|
2357
|
+
|
|
2358
|
+
def _select_project_tool_entry(
|
|
2359
|
+
entries: tuple[CustomToolCatalogEntry, ...],
|
|
2360
|
+
*,
|
|
2361
|
+
name: str,
|
|
2362
|
+
request_id: RequestId,
|
|
2363
|
+
) -> CustomToolCatalogEntry:
|
|
2364
|
+
matches = [entry for entry in entries if entry.name.casefold() == name.casefold()]
|
|
2365
|
+
if not matches:
|
|
2366
|
+
raise ProtocolError(
|
|
2367
|
+
"tool_not_found", f"Custom tool not found: {name}", request_id=request_id
|
|
2368
|
+
)
|
|
2369
|
+
for entry in matches:
|
|
2370
|
+
if (
|
|
2371
|
+
entry.source_scope == "project"
|
|
2372
|
+
and entry.spec is not None
|
|
2373
|
+
and entry.status != "shadowed"
|
|
2374
|
+
):
|
|
2375
|
+
return entry
|
|
2376
|
+
raise ProtocolError(
|
|
2377
|
+
"unsupported_tool_trust_target",
|
|
2378
|
+
"Trust commands only apply to valid project custom tools.",
|
|
2379
|
+
request_id=request_id,
|
|
2380
|
+
)
|
|
2381
|
+
|
|
2382
|
+
|
|
2383
|
+
def _skill_catalog(params: dict[str, Any], *, request_id: RequestId) -> tuple[Path, Any]:
|
|
2384
|
+
workspace_root = _workspace_root_from_params(params, request_id=request_id, required=True)
|
|
2385
|
+
discovered = discover_skills(focus_path=workspace_root, workspace_root=workspace_root)
|
|
2386
|
+
catalog = resolve_skill_catalog(discovered=discovered, workspace_root=workspace_root)
|
|
2387
|
+
return workspace_root, catalog
|
|
2388
|
+
|
|
2389
|
+
|
|
2390
|
+
def _workspace_root_from_params(
|
|
2391
|
+
params: dict[str, Any],
|
|
2392
|
+
*,
|
|
2393
|
+
request_id: RequestId,
|
|
2394
|
+
required: bool,
|
|
2395
|
+
) -> Path:
|
|
2396
|
+
raw = params.get("workspace", params.get("path"))
|
|
2397
|
+
if raw is None:
|
|
2398
|
+
if required:
|
|
2399
|
+
raise ProtocolError(
|
|
2400
|
+
"missing_param",
|
|
2401
|
+
"This method requires workspace or path.",
|
|
2402
|
+
request_id=request_id,
|
|
2403
|
+
)
|
|
2404
|
+
raw = "."
|
|
2405
|
+
path = Path(str(raw))
|
|
2406
|
+
try:
|
|
2407
|
+
binding = resolve_startup_workspace_binding(
|
|
2408
|
+
requested_path=path,
|
|
2409
|
+
interactive=False,
|
|
2410
|
+
create_if_missing=False,
|
|
2411
|
+
allow_broad_workspace=_optional_bool(params, "allow_broad_workspace", default=False),
|
|
2412
|
+
source="ide_management_param",
|
|
2413
|
+
action=WorkspaceAction.CHAT,
|
|
2414
|
+
)
|
|
2415
|
+
except WorkspaceBindingError as exc:
|
|
2416
|
+
raise ProtocolError(
|
|
2417
|
+
"workspace_binding_error",
|
|
2418
|
+
_redact_management_error_message(str(exc)),
|
|
2419
|
+
request_id=request_id,
|
|
2420
|
+
) from exc
|
|
2421
|
+
return binding.workspace_context.workspace_root
|
|
2422
|
+
|
|
2423
|
+
|
|
2424
|
+
def _workspace_child_path(workspace_root: Path, raw: str, *, request_id: RequestId) -> Path:
|
|
2425
|
+
candidate = Path(raw).expanduser()
|
|
2426
|
+
if not candidate.is_absolute():
|
|
2427
|
+
candidate = workspace_root / candidate
|
|
2428
|
+
resolved = candidate.resolve()
|
|
2429
|
+
try:
|
|
2430
|
+
resolved.relative_to(workspace_root.resolve())
|
|
2431
|
+
except ValueError as exc:
|
|
2432
|
+
raise ProtocolError(
|
|
2433
|
+
"path_outside_workspace",
|
|
2434
|
+
"IDE management file paths must stay inside the workspace.",
|
|
2435
|
+
request_id=request_id,
|
|
2436
|
+
) from exc
|
|
2437
|
+
return resolved
|
|
2438
|
+
|
|
2439
|
+
|
|
2440
|
+
def _validate_skill_install_source(
|
|
2441
|
+
source: str,
|
|
2442
|
+
*,
|
|
2443
|
+
workspace_root: Path,
|
|
2444
|
+
params: dict[str, Any],
|
|
2445
|
+
request_id: RequestId,
|
|
2446
|
+
) -> str:
|
|
2447
|
+
raw = source.strip()
|
|
2448
|
+
if not raw:
|
|
2449
|
+
raise ProtocolError(
|
|
2450
|
+
"missing_param",
|
|
2451
|
+
"skill.install requires source.",
|
|
2452
|
+
request_id=request_id,
|
|
2453
|
+
)
|
|
2454
|
+
_reject_url_userinfo_value(raw, field="skill.install.source", request_id=request_id)
|
|
2455
|
+
if _is_remote_skill_source(raw):
|
|
2456
|
+
if _unsupported_remote_source(raw):
|
|
2457
|
+
raise ProtocolError(
|
|
2458
|
+
"unsupported_remote_source",
|
|
2459
|
+
"IDE skill.install only allows HTTPS remote sources; use the CLI for other remote transports.",
|
|
2460
|
+
request_id=request_id,
|
|
2461
|
+
)
|
|
2462
|
+
if not (_optional_bool(params, "allow_remote", default=False) and _confirmed(params)):
|
|
2463
|
+
raise ProtocolError(
|
|
2464
|
+
"remote_source_requires_confirmation",
|
|
2465
|
+
"Remote skill installs require allow_remote=true and explicit confirmation.",
|
|
2466
|
+
request_id=request_id,
|
|
2467
|
+
)
|
|
2468
|
+
return raw
|
|
2469
|
+
|
|
2470
|
+
candidate = Path(raw).expanduser()
|
|
2471
|
+
if not candidate.is_absolute():
|
|
2472
|
+
candidate = workspace_root / candidate
|
|
2473
|
+
if candidate.is_symlink():
|
|
2474
|
+
raise ProtocolError(
|
|
2475
|
+
"skill_install_symlink_rejected",
|
|
2476
|
+
"skill.install source paths must not be symlinks.",
|
|
2477
|
+
request_id=request_id,
|
|
2478
|
+
)
|
|
2479
|
+
resolved = candidate.resolve(strict=False)
|
|
2480
|
+
try:
|
|
2481
|
+
resolved.relative_to(workspace_root.resolve())
|
|
2482
|
+
except ValueError as exc:
|
|
2483
|
+
raise ProtocolError(
|
|
2484
|
+
"path_outside_workspace",
|
|
2485
|
+
"skill.install local source paths must stay inside the workspace.",
|
|
2486
|
+
request_id=request_id,
|
|
2487
|
+
) from exc
|
|
2488
|
+
if not resolved.exists():
|
|
2489
|
+
raise ProtocolError(
|
|
2490
|
+
"skill_install_source_not_found",
|
|
2491
|
+
"skill.install local source path was not found.",
|
|
2492
|
+
request_id=request_id,
|
|
2493
|
+
)
|
|
2494
|
+
if resolved.is_symlink():
|
|
2495
|
+
raise ProtocolError(
|
|
2496
|
+
"skill_install_symlink_rejected",
|
|
2497
|
+
"skill.install source paths must not be symlinks.",
|
|
2498
|
+
request_id=request_id,
|
|
2499
|
+
)
|
|
2500
|
+
if resolved.is_dir():
|
|
2501
|
+
return str(resolved)
|
|
2502
|
+
if resolved.is_file() and resolved.suffix.casefold() == ".zip":
|
|
2503
|
+
return str(resolved)
|
|
2504
|
+
raise ProtocolError(
|
|
2505
|
+
"unsupported_skill_install_source",
|
|
2506
|
+
"skill.install local source must be a directory or .zip archive inside the workspace.",
|
|
2507
|
+
request_id=request_id,
|
|
2508
|
+
)
|
|
2509
|
+
|
|
2510
|
+
|
|
2511
|
+
def _validate_ext_install_source(source: str, *, request_id: RequestId) -> None:
|
|
2512
|
+
raw = source.strip()
|
|
2513
|
+
if not raw:
|
|
2514
|
+
raise ProtocolError("missing_param", "ext.install requires source.", request_id=request_id)
|
|
2515
|
+
_reject_url_userinfo_value(raw, field="ext.install.source", request_id=request_id)
|
|
2516
|
+
if _EXTENSION_REGISTRY_ID_RE.fullmatch(raw):
|
|
2517
|
+
return
|
|
2518
|
+
if raw.startswith("git+https://"):
|
|
2519
|
+
base, marker, commit = raw.rpartition("@")
|
|
2520
|
+
if marker and base and _SHA40_RE.fullmatch(commit):
|
|
2521
|
+
return
|
|
2522
|
+
raise ProtocolError(
|
|
2523
|
+
"invalid_extension_source",
|
|
2524
|
+
"ext.install git+https sources must be pinned with @<40-char-commit>.",
|
|
2525
|
+
request_id=request_id,
|
|
2526
|
+
)
|
|
2527
|
+
parsed = urlparse(raw)
|
|
2528
|
+
if parsed.scheme == "https" and parsed.netloc:
|
|
2529
|
+
if parsed.fragment and _SHA40_RE.fullmatch(parsed.fragment):
|
|
2530
|
+
return
|
|
2531
|
+
raise ProtocolError(
|
|
2532
|
+
"invalid_extension_source",
|
|
2533
|
+
"ext.install HTTPS sources must be pinned with #<40-char-commit>.",
|
|
2534
|
+
request_id=request_id,
|
|
2535
|
+
)
|
|
2536
|
+
if _looks_like_local_path(raw) or parsed.scheme or "://" in raw:
|
|
2537
|
+
raise ProtocolError(
|
|
2538
|
+
"invalid_extension_source",
|
|
2539
|
+
"ext.install source must be a registry id or pinned HTTPS git source.",
|
|
2540
|
+
request_id=request_id,
|
|
2541
|
+
)
|
|
2542
|
+
raise ProtocolError(
|
|
2543
|
+
"invalid_extension_source",
|
|
2544
|
+
"ext.install source must be a registry id or pinned HTTPS git source.",
|
|
2545
|
+
request_id=request_id,
|
|
2546
|
+
)
|
|
2547
|
+
|
|
2548
|
+
|
|
2549
|
+
def _is_remote_skill_source(source: str) -> bool:
|
|
2550
|
+
parsed = urlparse(source)
|
|
2551
|
+
return (
|
|
2552
|
+
bool(parsed.scheme)
|
|
2553
|
+
or source.startswith("git@")
|
|
2554
|
+
or source.startswith("ssh://")
|
|
2555
|
+
or source.startswith("git://")
|
|
2556
|
+
)
|
|
2557
|
+
|
|
2558
|
+
|
|
2559
|
+
def _unsupported_remote_source(source: str) -> bool:
|
|
2560
|
+
parsed = urlparse(source)
|
|
2561
|
+
if source.startswith("git+https://"):
|
|
2562
|
+
return False
|
|
2563
|
+
if parsed.scheme == "https":
|
|
2564
|
+
return False
|
|
2565
|
+
return True
|
|
2566
|
+
|
|
2567
|
+
|
|
2568
|
+
def _looks_like_local_path(source: str) -> bool:
|
|
2569
|
+
return (
|
|
2570
|
+
"/" in source
|
|
2571
|
+
or "\\" in source
|
|
2572
|
+
or source.startswith(".")
|
|
2573
|
+
or source.startswith("~")
|
|
2574
|
+
or Path(source).is_absolute()
|
|
2575
|
+
)
|
|
2576
|
+
|
|
2577
|
+
|
|
2578
|
+
def _session_log_path(params: dict[str, Any], *, request_id: RequestId) -> Path:
|
|
2579
|
+
session_id = _required_str(params, "session_id", request_id=request_id)
|
|
2580
|
+
sanitized = sanitize_session_id(session_id)
|
|
2581
|
+
if sanitized != session_id:
|
|
2582
|
+
raise ProtocolError(
|
|
2583
|
+
"invalid_session_id",
|
|
2584
|
+
"Session id contains unsupported characters.",
|
|
2585
|
+
request_id=request_id,
|
|
2586
|
+
)
|
|
2587
|
+
cfg = load_config()
|
|
2588
|
+
path = resolve_sessions_dir(cfg) / f"{session_id}.jsonl"
|
|
2589
|
+
if not path.exists() or not path.is_file():
|
|
2590
|
+
raise ProtocolError(
|
|
2591
|
+
"session_log_not_found", f"Session log not found: {session_id}", request_id=request_id
|
|
2592
|
+
)
|
|
2593
|
+
return path
|
|
2594
|
+
|
|
2595
|
+
|
|
2596
|
+
def _require_workspace_trust(
|
|
2597
|
+
params: dict[str, Any],
|
|
2598
|
+
*,
|
|
2599
|
+
method: str,
|
|
2600
|
+
request_id: RequestId,
|
|
2601
|
+
) -> None:
|
|
2602
|
+
if params.get("workspace_trusted") is True:
|
|
2603
|
+
return
|
|
2604
|
+
raise ProtocolError(
|
|
2605
|
+
"workspace_trust_required",
|
|
2606
|
+
f"{method} mutates local Alysis Code state and requires Workspace Trust.",
|
|
2607
|
+
request_id=request_id,
|
|
2608
|
+
)
|
|
2609
|
+
|
|
2610
|
+
|
|
2611
|
+
def _require_workspace_binding_param(
|
|
2612
|
+
params: dict[str, Any],
|
|
2613
|
+
*,
|
|
2614
|
+
method: str,
|
|
2615
|
+
request_id: RequestId,
|
|
2616
|
+
) -> None:
|
|
2617
|
+
for key in ("workspace", "path"):
|
|
2618
|
+
value = params.get(key)
|
|
2619
|
+
if isinstance(value, str) and value.strip():
|
|
2620
|
+
return
|
|
2621
|
+
raise ProtocolError(
|
|
2622
|
+
"missing_param",
|
|
2623
|
+
f"{method} requires an explicit workspace or path.",
|
|
2624
|
+
request_id=request_id,
|
|
2625
|
+
)
|
|
2626
|
+
|
|
2627
|
+
|
|
2628
|
+
def _safe_config_payload(cfg: AppConfig) -> dict[str, Any]:
|
|
2629
|
+
payload = cfg.model_dump()
|
|
2630
|
+
for key in list(payload):
|
|
2631
|
+
if _unsafe_config_response_key(key):
|
|
2632
|
+
payload[key] = "<redacted>" if payload[key] else payload[key]
|
|
2633
|
+
extra = dict(cfg.extra_fields or {})
|
|
2634
|
+
profiles_raw = extra.get("profiles")
|
|
2635
|
+
if isinstance(profiles_raw, dict):
|
|
2636
|
+
extra["profiles"] = {
|
|
2637
|
+
name: _safe_profile_config_dict(name, value)
|
|
2638
|
+
for name, value in sorted(profiles_raw.items(), key=lambda item: str(item[0]))
|
|
2639
|
+
if isinstance(value, dict)
|
|
2640
|
+
}
|
|
2641
|
+
payload["extra_fields"] = _redact_mapping_by_key(extra)
|
|
2642
|
+
return payload
|
|
2643
|
+
|
|
2644
|
+
|
|
2645
|
+
def _safe_profile_config_dict(name: object, value: dict[str, Any]) -> dict[str, Any]:
|
|
2646
|
+
safe = dict(value)
|
|
2647
|
+
env_var = safe.pop("api_key_env", None)
|
|
2648
|
+
if env_var:
|
|
2649
|
+
safe["env_var"] = str(env_var)
|
|
2650
|
+
headers = safe.get("extra_headers")
|
|
2651
|
+
if isinstance(headers, dict):
|
|
2652
|
+
safe["extra_headers"] = {
|
|
2653
|
+
"names": sorted(str(key) for key in headers),
|
|
2654
|
+
"count": len(headers),
|
|
2655
|
+
"values_redacted": True,
|
|
2656
|
+
}
|
|
2657
|
+
safe["name"] = str(name)
|
|
2658
|
+
return _redact_mapping_by_key(safe)
|
|
2659
|
+
|
|
2660
|
+
|
|
2661
|
+
def _profile_payload(
|
|
2662
|
+
cfg: AppConfig,
|
|
2663
|
+
profile: ProfileSpec | None,
|
|
2664
|
+
*,
|
|
2665
|
+
include_details: bool = False,
|
|
2666
|
+
) -> dict[str, Any]:
|
|
2667
|
+
if profile is None:
|
|
2668
|
+
return {}
|
|
2669
|
+
resolved = resolve_profile_api_key(cfg, profile.name)
|
|
2670
|
+
active = profile.name == str((cfg.extra_fields or {}).get("active_profile") or "")
|
|
2671
|
+
auth_provider = str(profile.auth_provider or "").strip() or None
|
|
2672
|
+
payload: dict[str, Any] = {
|
|
2673
|
+
"name": profile.name,
|
|
2674
|
+
"active": active,
|
|
2675
|
+
"protocol": profile.protocol,
|
|
2676
|
+
"base_url": profile.base_url,
|
|
2677
|
+
"base_url_host": _url_host(profile.base_url),
|
|
2678
|
+
"default_model": profile.default_model,
|
|
2679
|
+
# These are selection metadata, not credentials. The IDE needs the
|
|
2680
|
+
# authoritative active-subscription verdict so it does not enable a
|
|
2681
|
+
# composer merely because an account profile exists while its required
|
|
2682
|
+
# model/reasoning selection is still incomplete.
|
|
2683
|
+
"auth_provider": auth_provider,
|
|
2684
|
+
"reasoning_effort": profile.reasoning_effort,
|
|
2685
|
+
"subscription_selection_ready": (
|
|
2686
|
+
active_subscription_selection_ready(cfg)
|
|
2687
|
+
if active and auth_provider is not None
|
|
2688
|
+
else None
|
|
2689
|
+
),
|
|
2690
|
+
"web_search_adapter": profile.web_search_adapter,
|
|
2691
|
+
"web_search_model": profile.web_search_model,
|
|
2692
|
+
"notes": profile.notes,
|
|
2693
|
+
"key_env_var": profile.api_key_env,
|
|
2694
|
+
"api_key": _api_key_status(resolved),
|
|
2695
|
+
"extra_headers": {
|
|
2696
|
+
"names": sorted(profile.extra_headers),
|
|
2697
|
+
"count": len(profile.extra_headers),
|
|
2698
|
+
"values_redacted": True,
|
|
2699
|
+
},
|
|
2700
|
+
}
|
|
2701
|
+
if include_details:
|
|
2702
|
+
payload["protocol_kind"] = (
|
|
2703
|
+
"native" if profile.protocol != "openai_compat" else "compatibility"
|
|
2704
|
+
)
|
|
2705
|
+
return payload
|
|
2706
|
+
|
|
2707
|
+
|
|
2708
|
+
def _preset_payload(preset: Any) -> dict[str, Any]:
|
|
2709
|
+
return {
|
|
2710
|
+
"key": preset.key,
|
|
2711
|
+
"label": preset.label,
|
|
2712
|
+
"selection_label": preset_selection_label(preset),
|
|
2713
|
+
"protocol": preset.protocol,
|
|
2714
|
+
"protocol_kind": preset_protocol_kind(preset),
|
|
2715
|
+
"base_url": preset.base_url,
|
|
2716
|
+
"base_url_host": _url_host(preset.base_url),
|
|
2717
|
+
"key_env_var": preset.api_key_env,
|
|
2718
|
+
"suggested_models": list(preset.suggested_models),
|
|
2719
|
+
# Forwarded so IDE pickers can label a model with what it is for instead of showing a
|
|
2720
|
+
# bare slug. Both maps are static catalog prose - never credential-bearing.
|
|
2721
|
+
"suggested_model_descriptions": dict(preset.suggested_model_descriptions or {}),
|
|
2722
|
+
"model_aliases": dict(preset.model_aliases or {}),
|
|
2723
|
+
"validation_model": preset.validation_model,
|
|
2724
|
+
"web_search_adapter": preset.web_search_adapter,
|
|
2725
|
+
"web_search_model": preset.web_search_model,
|
|
2726
|
+
"setup_warning": preset.setup_warning,
|
|
2727
|
+
"notes": preset.notes,
|
|
2728
|
+
}
|
|
2729
|
+
|
|
2730
|
+
|
|
2731
|
+
def _secret_action_for_profile(profile: ProfileSpec) -> dict[str, Any] | None:
|
|
2732
|
+
if not profile.api_key_env:
|
|
2733
|
+
return None
|
|
2734
|
+
return {
|
|
2735
|
+
"kind": "open_configure_provider",
|
|
2736
|
+
"reason": "Provider credentials must be supplied through VS Code SecretStorage or existing CLI credential sources.",
|
|
2737
|
+
"profile": profile.name,
|
|
2738
|
+
"env_var": profile.api_key_env,
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
|
|
2742
|
+
def _api_key_status(value: Any) -> dict[str, Any]:
|
|
2743
|
+
return {
|
|
2744
|
+
"present": bool(getattr(value, "key", None)),
|
|
2745
|
+
"source": str(getattr(value, "source", "missing") or "missing"),
|
|
2746
|
+
}
|
|
2747
|
+
|
|
2748
|
+
|
|
2749
|
+
def _extra_headers_secret_storage_action(
|
|
2750
|
+
params: dict[str, Any], *, request_id: RequestId
|
|
2751
|
+
) -> dict[str, Any] | None:
|
|
2752
|
+
raw = params.get("extra_headers")
|
|
2753
|
+
if raw is None:
|
|
2754
|
+
return None
|
|
2755
|
+
if not isinstance(raw, dict):
|
|
2756
|
+
raise ProtocolError(
|
|
2757
|
+
"invalid_field",
|
|
2758
|
+
"extra_headers must be an object.",
|
|
2759
|
+
request_id=request_id,
|
|
2760
|
+
)
|
|
2761
|
+
if not raw:
|
|
2762
|
+
return None
|
|
2763
|
+
header_names: list[str] = []
|
|
2764
|
+
for key in raw:
|
|
2765
|
+
key_text = str(key or "").strip()
|
|
2766
|
+
if not key_text:
|
|
2767
|
+
raise ProtocolError(
|
|
2768
|
+
"invalid_field",
|
|
2769
|
+
"extra_headers keys must be non-empty strings.",
|
|
2770
|
+
request_id=request_id,
|
|
2771
|
+
)
|
|
2772
|
+
header_names.append(key_text)
|
|
2773
|
+
return {
|
|
2774
|
+
"kind": "requires_secret_storage",
|
|
2775
|
+
"method": "profile.add",
|
|
2776
|
+
"reason": "Provider extra header values cannot be passed through IDE protocol params.",
|
|
2777
|
+
"next_step": "open_configure_provider",
|
|
2778
|
+
"header_names": sorted(dict.fromkeys(header_names)),
|
|
2779
|
+
"values_redacted": True,
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2782
|
+
|
|
2783
|
+
def _extra_headers_param(params: dict[str, Any], *, request_id: RequestId) -> dict[str, str]:
|
|
2784
|
+
raw = params.get("extra_headers")
|
|
2785
|
+
if raw is None:
|
|
2786
|
+
return {}
|
|
2787
|
+
if not isinstance(raw, dict):
|
|
2788
|
+
raise ProtocolError(
|
|
2789
|
+
"invalid_field",
|
|
2790
|
+
"extra_headers must be an object.",
|
|
2791
|
+
request_id=request_id,
|
|
2792
|
+
)
|
|
2793
|
+
headers: dict[str, str] = {}
|
|
2794
|
+
for key, value in raw.items():
|
|
2795
|
+
key_text = str(key or "").strip()
|
|
2796
|
+
value_text = str(value or "").strip()
|
|
2797
|
+
if not key_text or not value_text:
|
|
2798
|
+
raise ProtocolError(
|
|
2799
|
+
"invalid_field",
|
|
2800
|
+
"extra_headers keys and values must be non-empty strings.",
|
|
2801
|
+
request_id=request_id,
|
|
2802
|
+
)
|
|
2803
|
+
headers[key_text] = value_text
|
|
2804
|
+
return headers
|
|
2805
|
+
|
|
2806
|
+
|
|
2807
|
+
def _reject_secret_bearing_value(
|
|
2808
|
+
value: Any,
|
|
2809
|
+
*,
|
|
2810
|
+
field: str,
|
|
2811
|
+
request_id: RequestId,
|
|
2812
|
+
) -> None:
|
|
2813
|
+
if _redacted_value_differs(value):
|
|
2814
|
+
raise ProtocolError(
|
|
2815
|
+
"inline_secret_rejected",
|
|
2816
|
+
f"{field} contains a secret-looking value; use configured credential storage instead.",
|
|
2817
|
+
request_id=request_id,
|
|
2818
|
+
)
|
|
2819
|
+
|
|
2820
|
+
|
|
2821
|
+
def _redacted_value_differs(value: Any) -> bool:
|
|
2822
|
+
return _json_safe(redact_secrets(value)) != _json_safe(value)
|
|
2823
|
+
|
|
2824
|
+
|
|
2825
|
+
def _reject_url_userinfo_value(
|
|
2826
|
+
value: Any,
|
|
2827
|
+
*,
|
|
2828
|
+
field: str,
|
|
2829
|
+
request_id: RequestId,
|
|
2830
|
+
) -> None:
|
|
2831
|
+
if not isinstance(value, str):
|
|
2832
|
+
return
|
|
2833
|
+
parsed = urlparse(value.strip())
|
|
2834
|
+
if parsed.username is None and parsed.password is None:
|
|
2835
|
+
return
|
|
2836
|
+
raise ProtocolError(
|
|
2837
|
+
"invalid_base_url",
|
|
2838
|
+
f"{field} must not include URL userinfo.",
|
|
2839
|
+
request_id=request_id,
|
|
2840
|
+
)
|
|
2841
|
+
|
|
2842
|
+
|
|
2843
|
+
def _custom_tool_payload(
|
|
2844
|
+
entry: CustomToolCatalogEntry,
|
|
2845
|
+
*,
|
|
2846
|
+
workspace_root: Path,
|
|
2847
|
+
) -> dict[str, Any]:
|
|
2848
|
+
payload: dict[str, Any] = {
|
|
2849
|
+
"name": entry.name,
|
|
2850
|
+
"source_scope": entry.source_scope,
|
|
2851
|
+
"trust": entry.trust,
|
|
2852
|
+
"status": entry.status,
|
|
2853
|
+
"source_path": str(entry.source_path),
|
|
2854
|
+
"detail": entry.detail,
|
|
2855
|
+
"manifest_version": entry.manifest_version,
|
|
2856
|
+
"exposed": entry.exposed,
|
|
2857
|
+
}
|
|
2858
|
+
if entry.spec is not None:
|
|
2859
|
+
payload.update(
|
|
2860
|
+
{
|
|
2861
|
+
"description": entry.spec.description,
|
|
2862
|
+
"relative_tool_path": entry.spec.relative_tool_path,
|
|
2863
|
+
"capabilities": entry.spec.capabilities.to_dict(),
|
|
2864
|
+
"timeout_s": entry.spec.timeout_s,
|
|
2865
|
+
"required_env": list(entry.spec.required_env),
|
|
2866
|
+
"missing_env": list(entry.spec.missing_env),
|
|
2867
|
+
"enabled_in": list(entry.spec.enabled_in),
|
|
2868
|
+
"isolation": entry.spec.isolation,
|
|
2869
|
+
"input_schema": entry.spec.input_schema,
|
|
2870
|
+
"output_schema": entry.spec.output_schema,
|
|
2871
|
+
}
|
|
2872
|
+
)
|
|
2873
|
+
if entry.issue is not None:
|
|
2874
|
+
payload["issue"] = {
|
|
2875
|
+
"code": entry.issue.code,
|
|
2876
|
+
"message": entry.issue.message,
|
|
2877
|
+
"relative_tool_path": entry.issue.relative_tool_path,
|
|
2878
|
+
}
|
|
2879
|
+
try:
|
|
2880
|
+
payload["workspace_relative_path"] = (
|
|
2881
|
+
entry.source_path.resolve().relative_to(workspace_root.resolve()).as_posix()
|
|
2882
|
+
)
|
|
2883
|
+
except ValueError:
|
|
2884
|
+
payload["workspace_relative_path"] = None
|
|
2885
|
+
return payload
|
|
2886
|
+
|
|
2887
|
+
|
|
2888
|
+
def _skill_entry_payload(entry: Any) -> dict[str, Any]:
|
|
2889
|
+
skill = entry.skill
|
|
2890
|
+
payload = _skill_payload(skill)
|
|
2891
|
+
payload.update(
|
|
2892
|
+
{
|
|
2893
|
+
"enabled": bool(entry.enabled),
|
|
2894
|
+
"managed": bool(entry.managed),
|
|
2895
|
+
"disabled_by": entry.disabled_by,
|
|
2896
|
+
"install_record": _dataclass_payload(entry.install_record)
|
|
2897
|
+
if entry.install_record is not None
|
|
2898
|
+
else None,
|
|
2899
|
+
}
|
|
2900
|
+
)
|
|
2901
|
+
return payload
|
|
2902
|
+
|
|
2903
|
+
|
|
2904
|
+
def _skill_payload(skill: Any) -> dict[str, Any]:
|
|
2905
|
+
return {
|
|
2906
|
+
"name": skill.name,
|
|
2907
|
+
"description": skill.description,
|
|
2908
|
+
"bundle_name": skill.bundle_name,
|
|
2909
|
+
"bundle_path": str(skill.bundle_path),
|
|
2910
|
+
"entry_path": str(skill.entry_path),
|
|
2911
|
+
"source_scope": skill.source_scope,
|
|
2912
|
+
"source_kind": skill.source_kind,
|
|
2913
|
+
"source_family": skill.source_family,
|
|
2914
|
+
"source_path": str(skill.source_path),
|
|
2915
|
+
"trust_level": skill.trust_level,
|
|
2916
|
+
"ancestor_distance": skill.ancestor_distance,
|
|
2917
|
+
"aliases": list(skill.aliases),
|
|
2918
|
+
"metadata": dict(skill.metadata),
|
|
2919
|
+
}
|
|
2920
|
+
|
|
2921
|
+
|
|
2922
|
+
def _skill_issues_payload(catalog: Any) -> dict[str, Any]:
|
|
2923
|
+
return {
|
|
2924
|
+
"discovery": [
|
|
2925
|
+
{"source_path": str(issue.source_path), "message": issue.message}
|
|
2926
|
+
for issue in getattr(catalog.effective, "issues", ())
|
|
2927
|
+
],
|
|
2928
|
+
"lifecycle": [
|
|
2929
|
+
{"source_path": str(issue.source_path), "message": issue.message}
|
|
2930
|
+
for issue in getattr(catalog, "lifecycle_issues", ())
|
|
2931
|
+
],
|
|
2932
|
+
}
|
|
2933
|
+
|
|
2934
|
+
|
|
2935
|
+
def _validation_payload(result: Any) -> dict[str, Any]:
|
|
2936
|
+
return {
|
|
2937
|
+
"bundle_path": str(result.bundle_path),
|
|
2938
|
+
"entry_path": str(result.entry_path),
|
|
2939
|
+
"valid": bool(result.valid),
|
|
2940
|
+
"name": result.name,
|
|
2941
|
+
"description": result.description,
|
|
2942
|
+
"metadata": dict(result.metadata),
|
|
2943
|
+
"issues": [
|
|
2944
|
+
{
|
|
2945
|
+
"severity": issue.severity,
|
|
2946
|
+
"message": issue.message,
|
|
2947
|
+
"path": str(issue.path),
|
|
2948
|
+
}
|
|
2949
|
+
for issue in result.issues
|
|
2950
|
+
],
|
|
2951
|
+
}
|
|
2952
|
+
|
|
2953
|
+
|
|
2954
|
+
def _dataclass_payload(value: Any) -> Any:
|
|
2955
|
+
if value is None:
|
|
2956
|
+
return None
|
|
2957
|
+
if is_dataclass(value):
|
|
2958
|
+
return _json_safe(asdict(value))
|
|
2959
|
+
model_dump = getattr(value, "model_dump", None)
|
|
2960
|
+
if callable(model_dump):
|
|
2961
|
+
return _json_safe(model_dump(mode="json"))
|
|
2962
|
+
return _json_safe(value)
|
|
2963
|
+
|
|
2964
|
+
|
|
2965
|
+
def _json_safe(value: Any) -> Any:
|
|
2966
|
+
if isinstance(value, Path):
|
|
2967
|
+
return str(value)
|
|
2968
|
+
if isinstance(value, dict):
|
|
2969
|
+
return {str(key): _json_safe(child) for key, child in value.items()}
|
|
2970
|
+
if isinstance(value, (list, tuple, set)):
|
|
2971
|
+
return [_json_safe(child) for child in value]
|
|
2972
|
+
return value
|
|
2973
|
+
|
|
2974
|
+
|
|
2975
|
+
def _rows_to_dict(rows: tuple[tuple[str, str], ...]) -> dict[str, str]:
|
|
2976
|
+
return {str(key): str(value) for key, value in rows}
|
|
2977
|
+
|
|
2978
|
+
|
|
2979
|
+
def _url_host(value: str | None) -> str:
|
|
2980
|
+
from urllib.parse import urlsplit
|
|
2981
|
+
|
|
2982
|
+
try:
|
|
2983
|
+
return urlsplit(str(value or "")).netloc
|
|
2984
|
+
except ValueError:
|
|
2985
|
+
return ""
|
|
2986
|
+
|
|
2987
|
+
|
|
2988
|
+
def _redact_mapping_by_key(value: Any) -> Any:
|
|
2989
|
+
if isinstance(value, dict):
|
|
2990
|
+
out: dict[str, Any] = {}
|
|
2991
|
+
for key, child in value.items():
|
|
2992
|
+
key_text = str(key)
|
|
2993
|
+
if _unsafe_config_response_key(key_text):
|
|
2994
|
+
out[key_text] = "<redacted>" if child else child
|
|
2995
|
+
else:
|
|
2996
|
+
out[key_text] = _redact_mapping_by_key(child)
|
|
2997
|
+
return out
|
|
2998
|
+
if isinstance(value, list):
|
|
2999
|
+
return [_redact_mapping_by_key(child) for child in value]
|
|
3000
|
+
return value
|
|
3001
|
+
|
|
3002
|
+
|
|
3003
|
+
def _redact_payload(value: Any) -> Any:
|
|
3004
|
+
if isinstance(value, dict):
|
|
3005
|
+
out: dict[str, Any] = {}
|
|
3006
|
+
for key, child in value.items():
|
|
3007
|
+
key_text = str(key)
|
|
3008
|
+
if _unsafe_payload_response_key(key_text) and isinstance(child, str):
|
|
3009
|
+
out[key_text] = "<redacted>" if child else child
|
|
3010
|
+
else:
|
|
3011
|
+
out[key_text] = _redact_payload(child)
|
|
3012
|
+
return out
|
|
3013
|
+
if isinstance(value, list):
|
|
3014
|
+
return [_redact_payload(child) for child in value]
|
|
3015
|
+
if isinstance(value, tuple):
|
|
3016
|
+
return [_redact_payload(child) for child in value]
|
|
3017
|
+
if isinstance(value, str):
|
|
3018
|
+
return _redact_secret_string(value)
|
|
3019
|
+
return value
|
|
3020
|
+
|
|
3021
|
+
|
|
3022
|
+
def _redact_management_error_message(message: str) -> str:
|
|
3023
|
+
return _redact_secret_string(str(message))
|
|
3024
|
+
|
|
3025
|
+
|
|
3026
|
+
def _redact_secret_string(value: str) -> str:
|
|
3027
|
+
redacted = str(redact_secrets(value))
|
|
3028
|
+
redacted = _SECRET_ASSIGNMENT_PATTERN.sub(r"\1<redacted>", redacted)
|
|
3029
|
+
redacted = _URL_USERINFO_PATTERN.sub(r"\1<redacted>@\3", redacted)
|
|
3030
|
+
return redacted
|
|
3031
|
+
|
|
3032
|
+
|
|
3033
|
+
def _unsafe_payload_response_key(key: str) -> bool:
|
|
3034
|
+
normalized = key.strip().lower()
|
|
3035
|
+
if normalized in {
|
|
3036
|
+
"api_key_env",
|
|
3037
|
+
"key_env_var",
|
|
3038
|
+
"secret_action",
|
|
3039
|
+
"secret_policy",
|
|
3040
|
+
"secret_redaction",
|
|
3041
|
+
"secret_values_included",
|
|
3042
|
+
"secret_values_in_params",
|
|
3043
|
+
"values_redacted",
|
|
3044
|
+
}:
|
|
3045
|
+
return False
|
|
3046
|
+
return _unsafe_config_response_key(key)
|
|
3047
|
+
|
|
3048
|
+
|
|
3049
|
+
def _json_size_bytes(value: Any) -> int:
|
|
3050
|
+
return len(
|
|
3051
|
+
json.dumps(value, ensure_ascii=True, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
3052
|
+
)
|
|
3053
|
+
|
|
3054
|
+
|
|
3055
|
+
def _truncate_text_utf8(value: str, max_bytes: int) -> tuple[str, bool]:
|
|
3056
|
+
encoded = value.encode("utf-8")
|
|
3057
|
+
if len(encoded) <= max_bytes:
|
|
3058
|
+
return value, False
|
|
3059
|
+
return encoded[:max_bytes].decode("utf-8", errors="ignore"), True
|
|
3060
|
+
|
|
3061
|
+
|
|
3062
|
+
def _unsafe_config_response_key(key: str) -> bool:
|
|
3063
|
+
normalized = key.strip().lower()
|
|
3064
|
+
if normalized in {"api_key_env", "key_env_var"}:
|
|
3065
|
+
return False
|
|
3066
|
+
return normalized in {"prompt_cache_key"} or any(
|
|
3067
|
+
marker in normalized
|
|
3068
|
+
for marker in ("api_key", "token", "secret", "password", "credential", "authorization")
|
|
3069
|
+
)
|
|
3070
|
+
|
|
3071
|
+
|
|
3072
|
+
def _unsafe_config_set_key(key: str) -> bool:
|
|
3073
|
+
normalized = key.strip().lower()
|
|
3074
|
+
return normalized in {"prompt_cache_key"} or any(
|
|
3075
|
+
marker in normalized
|
|
3076
|
+
for marker in ("api_key", "token", "secret", "password", "credential", "authorization")
|
|
3077
|
+
)
|
|
3078
|
+
|
|
3079
|
+
|
|
3080
|
+
def _config_value_to_cli_string(value: Any) -> str:
|
|
3081
|
+
if isinstance(value, bool):
|
|
3082
|
+
return "true" if value else "false"
|
|
3083
|
+
if isinstance(value, int | float):
|
|
3084
|
+
return str(value)
|
|
3085
|
+
if isinstance(value, list | dict):
|
|
3086
|
+
return json.dumps(value, ensure_ascii=True, sort_keys=True)
|
|
3087
|
+
return str(value)
|
|
3088
|
+
|
|
3089
|
+
|
|
3090
|
+
def _required_str(
|
|
3091
|
+
params: dict[str, Any],
|
|
3092
|
+
key: str,
|
|
3093
|
+
*,
|
|
3094
|
+
request_id: RequestId,
|
|
3095
|
+
) -> str:
|
|
3096
|
+
value = params.get(key)
|
|
3097
|
+
if not isinstance(value, str) or not value.strip():
|
|
3098
|
+
raise ProtocolError(
|
|
3099
|
+
"missing_param", f"Missing required string param: {key}.", request_id=request_id
|
|
3100
|
+
)
|
|
3101
|
+
return value.strip()
|
|
3102
|
+
|
|
3103
|
+
|
|
3104
|
+
def _required_str_any(
|
|
3105
|
+
params: dict[str, Any],
|
|
3106
|
+
keys: tuple[str, ...],
|
|
3107
|
+
*,
|
|
3108
|
+
request_id: RequestId,
|
|
3109
|
+
) -> str:
|
|
3110
|
+
for key in keys:
|
|
3111
|
+
if key in params:
|
|
3112
|
+
return _required_str(params, key, request_id=request_id)
|
|
3113
|
+
raise ProtocolError(
|
|
3114
|
+
"missing_param",
|
|
3115
|
+
f"Missing required string param: {' or '.join(keys)}.",
|
|
3116
|
+
request_id=request_id,
|
|
3117
|
+
)
|
|
3118
|
+
|
|
3119
|
+
|
|
3120
|
+
def _optional_str(params: dict[str, Any], key: str, *, default: str) -> str:
|
|
3121
|
+
value = params.get(key)
|
|
3122
|
+
if value is None:
|
|
3123
|
+
return default
|
|
3124
|
+
return str(value)
|
|
3125
|
+
|
|
3126
|
+
|
|
3127
|
+
def _optional_str_or_none(params: dict[str, Any], key: str) -> str | None:
|
|
3128
|
+
if key not in params or params.get(key) is None:
|
|
3129
|
+
return None
|
|
3130
|
+
text = str(params.get(key) or "").strip()
|
|
3131
|
+
return text or None
|
|
3132
|
+
|
|
3133
|
+
|
|
3134
|
+
def _optional_bool(params: dict[str, Any], key: str, *, default: bool) -> bool:
|
|
3135
|
+
value = params.get(key)
|
|
3136
|
+
if value is None:
|
|
3137
|
+
return default
|
|
3138
|
+
if isinstance(value, bool):
|
|
3139
|
+
return value
|
|
3140
|
+
if isinstance(value, str):
|
|
3141
|
+
lowered = value.strip().lower()
|
|
3142
|
+
if lowered in {"1", "true", "yes", "on"}:
|
|
3143
|
+
return True
|
|
3144
|
+
if lowered in {"0", "false", "no", "off"}:
|
|
3145
|
+
return False
|
|
3146
|
+
return default
|
|
3147
|
+
|
|
3148
|
+
|
|
3149
|
+
def _positive_int(
|
|
3150
|
+
params: dict[str, Any],
|
|
3151
|
+
key: str,
|
|
3152
|
+
*,
|
|
3153
|
+
default: int,
|
|
3154
|
+
upper: int,
|
|
3155
|
+
) -> int:
|
|
3156
|
+
value = params.get(key)
|
|
3157
|
+
if value is None:
|
|
3158
|
+
return default
|
|
3159
|
+
try:
|
|
3160
|
+
parsed = int(value)
|
|
3161
|
+
except (TypeError, ValueError):
|
|
3162
|
+
return default
|
|
3163
|
+
return max(0, min(parsed, upper))
|
|
3164
|
+
|
|
3165
|
+
|
|
3166
|
+
def _string_list_param(value: Any) -> list[str]:
|
|
3167
|
+
if value is None:
|
|
3168
|
+
return []
|
|
3169
|
+
if isinstance(value, str):
|
|
3170
|
+
return [value.strip()] if value.strip() else []
|
|
3171
|
+
if isinstance(value, list):
|
|
3172
|
+
return [str(item).strip() for item in value if str(item).strip()]
|
|
3173
|
+
return []
|
|
3174
|
+
|
|
3175
|
+
|
|
3176
|
+
def _ext_install_trust_approval_param(
|
|
3177
|
+
params: dict[str, Any], *, request_id: RequestId
|
|
3178
|
+
) -> dict[str, Any] | None:
|
|
3179
|
+
raw = params.get("trust_approval")
|
|
3180
|
+
if raw is None:
|
|
3181
|
+
raw = params.get("trustApproval")
|
|
3182
|
+
if raw is None:
|
|
3183
|
+
return None
|
|
3184
|
+
if not isinstance(raw, dict):
|
|
3185
|
+
raise ProtocolError(
|
|
3186
|
+
"invalid_field",
|
|
3187
|
+
"trust_approval must be an object returned from an ext.install trust review.",
|
|
3188
|
+
request_id=request_id,
|
|
3189
|
+
)
|
|
3190
|
+
if raw.get("approved") is not True:
|
|
3191
|
+
raise ProtocolError(
|
|
3192
|
+
"extension_trust_required",
|
|
3193
|
+
"trust_approval.approved must be true to install extension packages.",
|
|
3194
|
+
request_id=request_id,
|
|
3195
|
+
)
|
|
3196
|
+
required = ("plugin_id", "commit", "manifest_sha256", "approval_fingerprint")
|
|
3197
|
+
approval: dict[str, Any] = {"approved": True}
|
|
3198
|
+
for field in required:
|
|
3199
|
+
value = raw.get(field)
|
|
3200
|
+
if not isinstance(value, str) or not value.strip():
|
|
3201
|
+
raise ProtocolError(
|
|
3202
|
+
"invalid_field",
|
|
3203
|
+
f"trust_approval.{field} must be a non-empty string.",
|
|
3204
|
+
request_id=request_id,
|
|
3205
|
+
)
|
|
3206
|
+
approval[field] = value.strip()
|
|
3207
|
+
if "project" in raw:
|
|
3208
|
+
if not isinstance(raw["project"], bool):
|
|
3209
|
+
raise ProtocolError(
|
|
3210
|
+
"invalid_field",
|
|
3211
|
+
"trust_approval.project must be a boolean when provided.",
|
|
3212
|
+
request_id=request_id,
|
|
3213
|
+
)
|
|
3214
|
+
approval["project"] = raw["project"]
|
|
3215
|
+
for optional in ("source", "source_url"):
|
|
3216
|
+
value = raw.get(optional)
|
|
3217
|
+
if value is not None:
|
|
3218
|
+
if not isinstance(value, str) or not value.strip():
|
|
3219
|
+
raise ProtocolError(
|
|
3220
|
+
"invalid_field",
|
|
3221
|
+
f"trust_approval.{optional} must be a non-empty string when provided.",
|
|
3222
|
+
request_id=request_id,
|
|
3223
|
+
)
|
|
3224
|
+
approval[optional] = value.strip()
|
|
3225
|
+
return approval
|
|
3226
|
+
|
|
3227
|
+
|
|
3228
|
+
def _ext_install_trust_review_result(
|
|
3229
|
+
request: Any,
|
|
3230
|
+
*,
|
|
3231
|
+
source: str,
|
|
3232
|
+
project: bool,
|
|
3233
|
+
) -> dict[str, Any]:
|
|
3234
|
+
trust_request = _redact_payload(_dataclass_payload(request))
|
|
3235
|
+
approval = _ext_install_required_approval(
|
|
3236
|
+
trust_request,
|
|
3237
|
+
source=source,
|
|
3238
|
+
project=project,
|
|
3239
|
+
)
|
|
3240
|
+
return {
|
|
3241
|
+
"changed": False,
|
|
3242
|
+
"action": {
|
|
3243
|
+
"kind": "requires_extension_trust_review",
|
|
3244
|
+
"method": "ext.install",
|
|
3245
|
+
"reason": "extension package install requires explicit review of manifest, source, permissions, and components",
|
|
3246
|
+
"approval_schema": "extension_install_trust_approval_v1",
|
|
3247
|
+
"required_approval": approval,
|
|
3248
|
+
},
|
|
3249
|
+
"trust_request": trust_request,
|
|
3250
|
+
"secret_values_included": False,
|
|
3251
|
+
}
|
|
3252
|
+
|
|
3253
|
+
|
|
3254
|
+
def _validate_ext_install_trust_approval(
|
|
3255
|
+
request: Any,
|
|
3256
|
+
approval: dict[str, Any],
|
|
3257
|
+
*,
|
|
3258
|
+
source: str,
|
|
3259
|
+
project: bool,
|
|
3260
|
+
request_id: RequestId,
|
|
3261
|
+
) -> None:
|
|
3262
|
+
trust_request = _redact_payload(_dataclass_payload(request))
|
|
3263
|
+
expected = _ext_install_required_approval(
|
|
3264
|
+
trust_request,
|
|
3265
|
+
source=source,
|
|
3266
|
+
project=project,
|
|
3267
|
+
)
|
|
3268
|
+
for field in ("plugin_id", "commit", "manifest_sha256", "approval_fingerprint"):
|
|
3269
|
+
if approval.get(field) != expected[field]:
|
|
3270
|
+
raise ProtocolError(
|
|
3271
|
+
"extension_trust_mismatch",
|
|
3272
|
+
f"trust_approval.{field} does not match the extension package being installed.",
|
|
3273
|
+
request_id=request_id,
|
|
3274
|
+
)
|
|
3275
|
+
if "project" in approval and approval["project"] != expected["project"]:
|
|
3276
|
+
raise ProtocolError(
|
|
3277
|
+
"extension_trust_mismatch",
|
|
3278
|
+
"trust_approval.project does not match the requested install scope.",
|
|
3279
|
+
request_id=request_id,
|
|
3280
|
+
)
|
|
3281
|
+
if "source" in approval and approval["source"] != expected["source"]:
|
|
3282
|
+
raise ProtocolError(
|
|
3283
|
+
"extension_trust_mismatch",
|
|
3284
|
+
"trust_approval.source does not match the requested install source.",
|
|
3285
|
+
request_id=request_id,
|
|
3286
|
+
)
|
|
3287
|
+
if "source_url" in approval and approval["source_url"] != expected["source_url"]:
|
|
3288
|
+
raise ProtocolError(
|
|
3289
|
+
"extension_trust_mismatch",
|
|
3290
|
+
"trust_approval.source_url does not match the resolved package source.",
|
|
3291
|
+
request_id=request_id,
|
|
3292
|
+
)
|
|
3293
|
+
|
|
3294
|
+
|
|
3295
|
+
def _ext_install_required_approval(
|
|
3296
|
+
trust_request: dict[str, Any],
|
|
3297
|
+
*,
|
|
3298
|
+
source: str,
|
|
3299
|
+
project: bool,
|
|
3300
|
+
) -> dict[str, Any]:
|
|
3301
|
+
approval = {
|
|
3302
|
+
"approved": True,
|
|
3303
|
+
"plugin_id": str(trust_request.get("plugin_id") or ""),
|
|
3304
|
+
"commit": str(trust_request.get("commit") or ""),
|
|
3305
|
+
"manifest_sha256": str(trust_request.get("manifest_sha256") or ""),
|
|
3306
|
+
"source": str(source or ""),
|
|
3307
|
+
"source_url": str(trust_request.get("source_url") or ""),
|
|
3308
|
+
"project": bool(project),
|
|
3309
|
+
}
|
|
3310
|
+
approval["approval_fingerprint"] = _ext_install_approval_fingerprint(approval)
|
|
3311
|
+
return approval
|
|
3312
|
+
|
|
3313
|
+
|
|
3314
|
+
def _ext_install_approval_fingerprint(approval: dict[str, Any]) -> str:
|
|
3315
|
+
payload = {
|
|
3316
|
+
key: approval[key]
|
|
3317
|
+
for key in (
|
|
3318
|
+
"plugin_id",
|
|
3319
|
+
"commit",
|
|
3320
|
+
"manifest_sha256",
|
|
3321
|
+
"source",
|
|
3322
|
+
"source_url",
|
|
3323
|
+
"project",
|
|
3324
|
+
)
|
|
3325
|
+
}
|
|
3326
|
+
encoded = json.dumps(payload, ensure_ascii=True, sort_keys=True, separators=(",", ":"))
|
|
3327
|
+
return hashlib.sha256(encoded.encode("utf-8")).hexdigest()
|
|
3328
|
+
|
|
3329
|
+
|
|
3330
|
+
def _confirmed(params: dict[str, Any]) -> bool:
|
|
3331
|
+
return _optional_bool(params, "yes", default=False) or _optional_bool(
|
|
3332
|
+
params, "confirm", default=False
|
|
3333
|
+
)
|
|
3334
|
+
|
|
3335
|
+
|
|
3336
|
+
def _confirmation_required(method: str, reason: str) -> dict[str, Any]:
|
|
3337
|
+
return {
|
|
3338
|
+
"changed": False,
|
|
3339
|
+
"action": {
|
|
3340
|
+
"kind": "requires_confirmation",
|
|
3341
|
+
"method": method,
|
|
3342
|
+
"reason": reason,
|
|
3343
|
+
},
|
|
3344
|
+
"secret_values_included": False,
|
|
3345
|
+
}
|
|
3346
|
+
|
|
3347
|
+
|
|
3348
|
+
_HANDLERS: dict[str, Callable[[dict[str, Any], RequestId], dict[str, Any]]] = {
|
|
3349
|
+
"config.get": _config_get,
|
|
3350
|
+
"config.set": _config_set,
|
|
3351
|
+
"config.schema": _config_schema,
|
|
3352
|
+
"config.validate": _config_validate,
|
|
3353
|
+
"profile.list": _profile_list,
|
|
3354
|
+
"profile.show": _profile_show,
|
|
3355
|
+
"profile.add": _profile_add,
|
|
3356
|
+
"profile.remove": _profile_remove,
|
|
3357
|
+
"profile.use": _profile_use,
|
|
3358
|
+
"profile.rename": _profile_rename,
|
|
3359
|
+
"profile.presets": _profile_presets,
|
|
3360
|
+
"profile.preset": _profile_preset,
|
|
3361
|
+
"profile.convert": _profile_convert,
|
|
3362
|
+
"session.show": _session_show,
|
|
3363
|
+
"session.usage": _session_usage,
|
|
3364
|
+
"session.score": _session_score,
|
|
3365
|
+
"tools.catalog": _tools_catalog,
|
|
3366
|
+
"tool.list": _tool_list,
|
|
3367
|
+
"tool.info": _tool_info,
|
|
3368
|
+
"tool.trust": _tool_trust,
|
|
3369
|
+
"tool.untrust": _tool_untrust,
|
|
3370
|
+
"skill.list": _skill_list,
|
|
3371
|
+
"skill.info": _skill_info,
|
|
3372
|
+
"skill.init": _skill_init,
|
|
3373
|
+
"skill.validate": _skill_validate,
|
|
3374
|
+
"skill.install": _skill_install,
|
|
3375
|
+
"skill.enable": _skill_enable,
|
|
3376
|
+
"skill.disable": _skill_disable,
|
|
3377
|
+
"skill.remove": _skill_remove,
|
|
3378
|
+
"doctor.summary": _doctor_summary,
|
|
3379
|
+
"doctor.providers": _doctor_providers,
|
|
3380
|
+
"doctor.providers.live": _doctor_providers_live,
|
|
3381
|
+
"doctor.bundle": _doctor_bundle,
|
|
3382
|
+
"sandbox.doctor": _sandbox_doctor,
|
|
3383
|
+
"sandbox.setup": _sandbox_setup,
|
|
3384
|
+
"sandbox.pull": _sandbox_pull,
|
|
3385
|
+
"update.check": _update_check,
|
|
3386
|
+
"report.create": _report_create,
|
|
3387
|
+
"mcp.status": _mcp_status,
|
|
3388
|
+
"mcp.prompts.list": _mcp_prompts_list,
|
|
3389
|
+
"mcp.prompts.get": _mcp_prompts_get,
|
|
3390
|
+
"mcp.auth.status": _mcp_auth_status,
|
|
3391
|
+
"mcp.auth.login.start": _mcp_auth_login_start,
|
|
3392
|
+
"mcp.auth.login.status": _mcp_auth_login_status,
|
|
3393
|
+
"mcp.auth.login.cancel": _mcp_auth_login_cancel,
|
|
3394
|
+
"mcp.auth.logout": _mcp_auth_logout,
|
|
3395
|
+
"hooks.list": _hooks_list,
|
|
3396
|
+
"hooks.doctor": _hooks_doctor,
|
|
3397
|
+
"hooks.trace": _hooks_trace,
|
|
3398
|
+
"hooks.test": _hooks_test,
|
|
3399
|
+
"hooks.trust": _hooks_trust,
|
|
3400
|
+
"hooks.untrust": _hooks_untrust,
|
|
3401
|
+
"hooks.init": _hooks_init,
|
|
3402
|
+
"hooks.effective": _hooks_effective,
|
|
3403
|
+
"hooks.enable": _hooks_enable,
|
|
3404
|
+
"hooks.disable": _hooks_disable,
|
|
3405
|
+
"conventions.list": _conventions_list,
|
|
3406
|
+
"conventions.render": _conventions_render,
|
|
3407
|
+
"ext.search": _ext_search,
|
|
3408
|
+
"ext.list": _ext_list,
|
|
3409
|
+
"ext.info": _ext_info,
|
|
3410
|
+
"ext.install": _ext_install,
|
|
3411
|
+
"ext.uninstall": _ext_uninstall,
|
|
3412
|
+
"ext.enable": _ext_enable,
|
|
3413
|
+
"ext.disable": _ext_disable,
|
|
3414
|
+
}
|