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,543 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from collections.abc import Callable, Collection
|
|
6
|
+
from typing import Any, TypeAlias
|
|
7
|
+
|
|
8
|
+
HOST_ACTION_PROTOCOL_VERSION = "1"
|
|
9
|
+
HOST_ACTIONS: tuple[str, ...] = (
|
|
10
|
+
"tasks.list",
|
|
11
|
+
"tasks.run",
|
|
12
|
+
"tasks.status",
|
|
13
|
+
"tasks.terminate",
|
|
14
|
+
"debug.list",
|
|
15
|
+
"debug.start",
|
|
16
|
+
"debug.stop",
|
|
17
|
+
"debug.status",
|
|
18
|
+
)
|
|
19
|
+
HOST_ACTION_SET = frozenset(HOST_ACTIONS)
|
|
20
|
+
HOST_ACTION_TOOL_NAMES: dict[str, str] = {
|
|
21
|
+
"tasks.list": "ide_task_list",
|
|
22
|
+
"tasks.run": "ide_task_run",
|
|
23
|
+
"tasks.status": "ide_task_status",
|
|
24
|
+
"tasks.terminate": "ide_task_terminate",
|
|
25
|
+
"debug.list": "ide_debug_list",
|
|
26
|
+
"debug.start": "ide_debug_start",
|
|
27
|
+
"debug.stop": "ide_debug_stop",
|
|
28
|
+
"debug.status": "ide_debug_status",
|
|
29
|
+
}
|
|
30
|
+
HOST_ACTION_MAX_ARGUMENT_BYTES = 8 * 1024
|
|
31
|
+
HOST_ACTION_MAX_RESULT_BYTES = 64 * 1024
|
|
32
|
+
HOST_ACTION_MAX_LIST_ITEMS = 100
|
|
33
|
+
HOST_ACTION_MAX_IDENTIFIER_CHARS = 256
|
|
34
|
+
HOST_ACTION_MAX_LABEL_CHARS = 512
|
|
35
|
+
HOST_ACTION_MAX_ERROR_MESSAGE_CHARS = 512
|
|
36
|
+
HOST_ACTION_ERROR_CODE_PATTERN = re.compile(r"^[a-z][a-z0-9_.-]{0,63}$")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class HostActionError(RuntimeError):
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
code: str,
|
|
43
|
+
message: str,
|
|
44
|
+
*,
|
|
45
|
+
retryable: bool = False,
|
|
46
|
+
host_error_code: str | None = None,
|
|
47
|
+
) -> None:
|
|
48
|
+
self.code = str(code or "host_action_failed")
|
|
49
|
+
self.message = str(message or "The IDE host action failed.")
|
|
50
|
+
self.retryable = bool(retryable)
|
|
51
|
+
self.host_error_code = str(host_error_code or "") or None
|
|
52
|
+
super().__init__(self.message)
|
|
53
|
+
|
|
54
|
+
def to_result_payload(self) -> dict[str, Any]:
|
|
55
|
+
payload: dict[str, Any] = {
|
|
56
|
+
"error": self.message,
|
|
57
|
+
"error_code": self.code,
|
|
58
|
+
"retryable": self.retryable,
|
|
59
|
+
}
|
|
60
|
+
if self.host_error_code:
|
|
61
|
+
payload["host_error_code"] = self.host_error_code
|
|
62
|
+
return payload
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
HostActionHandler: TypeAlias = Callable[[str, dict[str, Any]], dict[str, Any]]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def normalized_host_action_capabilities(actions: Collection[str] | None) -> frozenset[str]:
|
|
69
|
+
if actions is None:
|
|
70
|
+
return frozenset()
|
|
71
|
+
return frozenset(
|
|
72
|
+
action.strip()
|
|
73
|
+
for action in actions
|
|
74
|
+
if isinstance(action, str) and action.strip() in HOST_ACTION_SET
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def normalize_host_action_arguments(action: str, arguments: dict[str, Any]) -> dict[str, Any]:
|
|
79
|
+
normalized_action = str(action or "").strip()
|
|
80
|
+
if normalized_action not in HOST_ACTION_SET:
|
|
81
|
+
raise HostActionError(
|
|
82
|
+
"unsupported_host_action",
|
|
83
|
+
"The requested IDE host action is not supported.",
|
|
84
|
+
)
|
|
85
|
+
if not isinstance(arguments, dict):
|
|
86
|
+
raise HostActionError(
|
|
87
|
+
"invalid_host_action_arguments",
|
|
88
|
+
"IDE host action arguments must be an object.",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
if normalized_action == "tasks.list":
|
|
92
|
+
_reject_extra_keys(arguments, {"task_type"})
|
|
93
|
+
task_type = _optional_identifier(arguments.get("task_type"), "task_type")
|
|
94
|
+
normalized = {"task_type": task_type} if task_type else {}
|
|
95
|
+
elif normalized_action == "tasks.run":
|
|
96
|
+
_reject_extra_keys(arguments, {"task_id"})
|
|
97
|
+
normalized = {"task_id": _required_identifier(arguments.get("task_id"), "task_id")}
|
|
98
|
+
elif normalized_action == "tasks.status":
|
|
99
|
+
_reject_extra_keys(arguments, {"execution_id"})
|
|
100
|
+
execution_id = _optional_identifier(arguments.get("execution_id"), "execution_id")
|
|
101
|
+
normalized = {"execution_id": execution_id} if execution_id else {}
|
|
102
|
+
elif normalized_action == "tasks.terminate":
|
|
103
|
+
_reject_extra_keys(arguments, {"execution_id"})
|
|
104
|
+
normalized = {
|
|
105
|
+
"execution_id": _required_identifier(arguments.get("execution_id"), "execution_id")
|
|
106
|
+
}
|
|
107
|
+
elif normalized_action == "debug.list":
|
|
108
|
+
_reject_extra_keys(arguments, set())
|
|
109
|
+
normalized = {}
|
|
110
|
+
elif normalized_action == "debug.start":
|
|
111
|
+
_reject_extra_keys(arguments, {"configuration_id"})
|
|
112
|
+
normalized = {
|
|
113
|
+
"configuration_id": _required_identifier(
|
|
114
|
+
arguments.get("configuration_id"), "configuration_id"
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
elif normalized_action == "debug.stop":
|
|
118
|
+
_reject_extra_keys(arguments, {"debug_session_id"})
|
|
119
|
+
normalized = {
|
|
120
|
+
"debug_session_id": _required_identifier(
|
|
121
|
+
arguments.get("debug_session_id"), "debug_session_id"
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
else:
|
|
125
|
+
_reject_extra_keys(arguments, {"debug_session_id"})
|
|
126
|
+
debug_session_id = _optional_identifier(
|
|
127
|
+
arguments.get("debug_session_id"), "debug_session_id"
|
|
128
|
+
)
|
|
129
|
+
normalized = {"debug_session_id": debug_session_id} if debug_session_id else {}
|
|
130
|
+
|
|
131
|
+
if json_size_bytes(normalized) > HOST_ACTION_MAX_ARGUMENT_BYTES:
|
|
132
|
+
raise HostActionError(
|
|
133
|
+
"host_action_arguments_too_large",
|
|
134
|
+
"IDE host action arguments exceed the allowed size.",
|
|
135
|
+
)
|
|
136
|
+
return normalized
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def normalize_host_action_result(action: str, result: dict[str, Any]) -> dict[str, Any]:
|
|
140
|
+
normalized_action = str(action or "").strip()
|
|
141
|
+
if normalized_action not in HOST_ACTION_SET:
|
|
142
|
+
raise HostActionError(
|
|
143
|
+
"unsupported_host_action",
|
|
144
|
+
"The requested IDE host action is not supported.",
|
|
145
|
+
)
|
|
146
|
+
if not isinstance(result, dict):
|
|
147
|
+
raise HostActionError(
|
|
148
|
+
"invalid_host_action_result",
|
|
149
|
+
"IDE host action result must be an object.",
|
|
150
|
+
)
|
|
151
|
+
if json_size_bytes(result) > HOST_ACTION_MAX_RESULT_BYTES:
|
|
152
|
+
raise HostActionError(
|
|
153
|
+
"host_action_result_too_large",
|
|
154
|
+
"IDE host action result exceeds the allowed size.",
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
if normalized_action == "tasks.list":
|
|
158
|
+
normalized = _normalize_task_list_result(result)
|
|
159
|
+
elif normalized_action == "tasks.run":
|
|
160
|
+
_reject_extra_keys(
|
|
161
|
+
result,
|
|
162
|
+
{"execution_id", "task_id", "state", "exit_code", "diagnostics_delta"},
|
|
163
|
+
)
|
|
164
|
+
normalized = {
|
|
165
|
+
"execution_id": _required_identifier(result.get("execution_id"), "execution_id"),
|
|
166
|
+
"task_id": _required_identifier(result.get("task_id"), "task_id"),
|
|
167
|
+
"state": _enum_value(result.get("state"), "state", {"started", "completed", "failed"}),
|
|
168
|
+
}
|
|
169
|
+
if result.get("exit_code") is not None:
|
|
170
|
+
normalized["exit_code"] = _bounded_int(result.get("exit_code"), "exit_code")
|
|
171
|
+
if result.get("diagnostics_delta") is not None:
|
|
172
|
+
normalized["diagnostics_delta"] = _normalize_diagnostics_delta(
|
|
173
|
+
result.get("diagnostics_delta")
|
|
174
|
+
)
|
|
175
|
+
elif normalized_action == "tasks.status":
|
|
176
|
+
normalized = _normalize_task_status_result(result)
|
|
177
|
+
elif normalized_action == "tasks.terminate":
|
|
178
|
+
_reject_extra_keys(result, {"execution_id", "terminated", "state"})
|
|
179
|
+
normalized = {
|
|
180
|
+
"execution_id": _required_identifier(result.get("execution_id"), "execution_id"),
|
|
181
|
+
"terminated": _required_bool(result.get("terminated"), "terminated"),
|
|
182
|
+
"state": _enum_value(
|
|
183
|
+
result.get("state"), "state", {"terminated", "not_found", "already_ended"}
|
|
184
|
+
),
|
|
185
|
+
}
|
|
186
|
+
elif normalized_action == "debug.list":
|
|
187
|
+
normalized = _normalize_debug_list_result(result)
|
|
188
|
+
elif normalized_action == "debug.start":
|
|
189
|
+
_reject_extra_keys(result, {"debug_session_id", "configuration_id", "state"})
|
|
190
|
+
normalized = {
|
|
191
|
+
"debug_session_id": _required_identifier(
|
|
192
|
+
result.get("debug_session_id"), "debug_session_id"
|
|
193
|
+
),
|
|
194
|
+
"configuration_id": _required_identifier(
|
|
195
|
+
result.get("configuration_id"), "configuration_id"
|
|
196
|
+
),
|
|
197
|
+
"state": _enum_value(result.get("state"), "state", {"started", "failed"}),
|
|
198
|
+
}
|
|
199
|
+
elif normalized_action == "debug.stop":
|
|
200
|
+
_reject_extra_keys(result, {"debug_session_id", "stopped", "state"})
|
|
201
|
+
normalized = {
|
|
202
|
+
"debug_session_id": _required_identifier(
|
|
203
|
+
result.get("debug_session_id"), "debug_session_id"
|
|
204
|
+
),
|
|
205
|
+
"stopped": _required_bool(result.get("stopped"), "stopped"),
|
|
206
|
+
"state": _enum_value(
|
|
207
|
+
result.get("state"), "state", {"stopped", "not_found", "already_ended"}
|
|
208
|
+
),
|
|
209
|
+
}
|
|
210
|
+
else:
|
|
211
|
+
normalized = _normalize_debug_status_result(result)
|
|
212
|
+
|
|
213
|
+
if json_size_bytes(normalized) > HOST_ACTION_MAX_RESULT_BYTES:
|
|
214
|
+
raise HostActionError(
|
|
215
|
+
"host_action_result_too_large",
|
|
216
|
+
"IDE host action result exceeds the allowed size.",
|
|
217
|
+
)
|
|
218
|
+
return normalized
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def json_size_bytes(value: Any) -> int:
|
|
222
|
+
try:
|
|
223
|
+
encoded = json.dumps(
|
|
224
|
+
value,
|
|
225
|
+
ensure_ascii=False,
|
|
226
|
+
allow_nan=False,
|
|
227
|
+
separators=(",", ":"),
|
|
228
|
+
sort_keys=True,
|
|
229
|
+
).encode("utf-8")
|
|
230
|
+
except (TypeError, ValueError) as exc:
|
|
231
|
+
raise HostActionError(
|
|
232
|
+
"invalid_host_action_payload",
|
|
233
|
+
"IDE host action payload must contain finite JSON values.",
|
|
234
|
+
) from exc
|
|
235
|
+
return len(encoded)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def normalize_host_error(error: dict[str, Any]) -> tuple[str, str, bool]:
|
|
239
|
+
if not isinstance(error, dict):
|
|
240
|
+
raise HostActionError(
|
|
241
|
+
"invalid_host_action_error",
|
|
242
|
+
"IDE host action error must be an object.",
|
|
243
|
+
)
|
|
244
|
+
_reject_extra_keys(error, {"code", "message", "retryable"})
|
|
245
|
+
raw_code = error.get("code")
|
|
246
|
+
code = raw_code.strip() if isinstance(raw_code, str) else ""
|
|
247
|
+
if not HOST_ACTION_ERROR_CODE_PATTERN.fullmatch(code):
|
|
248
|
+
raise HostActionError(
|
|
249
|
+
"invalid_host_action_error",
|
|
250
|
+
"IDE host action error code is invalid.",
|
|
251
|
+
)
|
|
252
|
+
raw_message = error.get("message")
|
|
253
|
+
message = raw_message.strip() if isinstance(raw_message, str) else ""
|
|
254
|
+
if (
|
|
255
|
+
not message
|
|
256
|
+
or len(message) > HOST_ACTION_MAX_ERROR_MESSAGE_CHARS
|
|
257
|
+
or _has_disallowed_text_control(message)
|
|
258
|
+
):
|
|
259
|
+
raise HostActionError(
|
|
260
|
+
"invalid_host_action_error",
|
|
261
|
+
"IDE host action error message is invalid.",
|
|
262
|
+
)
|
|
263
|
+
retryable = error.get("retryable", False)
|
|
264
|
+
if not isinstance(retryable, bool):
|
|
265
|
+
raise HostActionError(
|
|
266
|
+
"invalid_host_action_error",
|
|
267
|
+
"IDE host action retryable must be a boolean.",
|
|
268
|
+
)
|
|
269
|
+
return code, message, retryable
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _normalize_task_list_result(result: dict[str, Any]) -> dict[str, Any]:
|
|
273
|
+
_reject_extra_keys(result, {"tasks", "truncated"})
|
|
274
|
+
items = result.get("tasks")
|
|
275
|
+
if not isinstance(items, list):
|
|
276
|
+
raise HostActionError("invalid_host_action_result", "tasks must be an array.")
|
|
277
|
+
normalized_items: list[dict[str, Any]] = []
|
|
278
|
+
for raw in items[:HOST_ACTION_MAX_LIST_ITEMS]:
|
|
279
|
+
if not isinstance(raw, dict):
|
|
280
|
+
raise HostActionError("invalid_host_action_result", "Each task must be an object.")
|
|
281
|
+
_reject_extra_keys(raw, {"id", "label", "type", "source", "detail"})
|
|
282
|
+
item: dict[str, Any] = {
|
|
283
|
+
"id": _required_identifier(raw.get("id"), "task.id"),
|
|
284
|
+
"label": _required_label(raw.get("label"), "task.label"),
|
|
285
|
+
}
|
|
286
|
+
for key in ("type", "source", "detail"):
|
|
287
|
+
value = _optional_label(raw.get(key), f"task.{key}")
|
|
288
|
+
if value:
|
|
289
|
+
item[key] = value
|
|
290
|
+
normalized_items.append(item)
|
|
291
|
+
return {
|
|
292
|
+
"tasks": normalized_items,
|
|
293
|
+
"truncated": bool(
|
|
294
|
+
_optional_bool(result.get("truncated"), "truncated")
|
|
295
|
+
or len(items) > len(normalized_items)
|
|
296
|
+
),
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _normalize_task_status_result(result: dict[str, Any]) -> dict[str, Any]:
|
|
301
|
+
_reject_extra_keys(result, {"executions", "truncated"})
|
|
302
|
+
items = result.get("executions")
|
|
303
|
+
if not isinstance(items, list):
|
|
304
|
+
raise HostActionError("invalid_host_action_result", "executions must be an array.")
|
|
305
|
+
normalized_items: list[dict[str, Any]] = []
|
|
306
|
+
for raw in items[:HOST_ACTION_MAX_LIST_ITEMS]:
|
|
307
|
+
if not isinstance(raw, dict):
|
|
308
|
+
raise HostActionError(
|
|
309
|
+
"invalid_host_action_result", "Each task execution must be an object."
|
|
310
|
+
)
|
|
311
|
+
_reject_extra_keys(
|
|
312
|
+
raw,
|
|
313
|
+
{"execution_id", "task_id", "state", "exit_code", "diagnostics_delta"},
|
|
314
|
+
)
|
|
315
|
+
item: dict[str, Any] = {
|
|
316
|
+
"execution_id": _required_identifier(raw.get("execution_id"), "execution_id"),
|
|
317
|
+
"task_id": _required_identifier(raw.get("task_id"), "task_id"),
|
|
318
|
+
"state": _enum_value(
|
|
319
|
+
raw.get("state"),
|
|
320
|
+
"task_execution.state",
|
|
321
|
+
{"running", "completed", "failed", "terminated"},
|
|
322
|
+
),
|
|
323
|
+
}
|
|
324
|
+
if raw.get("exit_code") is not None:
|
|
325
|
+
item["exit_code"] = _bounded_int(raw.get("exit_code"), "exit_code")
|
|
326
|
+
if raw.get("diagnostics_delta") is not None:
|
|
327
|
+
item["diagnostics_delta"] = _normalize_diagnostics_delta(raw.get("diagnostics_delta"))
|
|
328
|
+
normalized_items.append(item)
|
|
329
|
+
return {
|
|
330
|
+
"executions": normalized_items,
|
|
331
|
+
"truncated": bool(
|
|
332
|
+
_optional_bool(result.get("truncated"), "truncated")
|
|
333
|
+
or len(items) > len(normalized_items)
|
|
334
|
+
),
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _normalize_debug_list_result(result: dict[str, Any]) -> dict[str, Any]:
|
|
339
|
+
_reject_extra_keys(result, {"configurations", "truncated"})
|
|
340
|
+
items = result.get("configurations")
|
|
341
|
+
if not isinstance(items, list):
|
|
342
|
+
raise HostActionError("invalid_host_action_result", "configurations must be an array.")
|
|
343
|
+
normalized_items: list[dict[str, Any]] = []
|
|
344
|
+
for raw in items[:HOST_ACTION_MAX_LIST_ITEMS]:
|
|
345
|
+
if not isinstance(raw, dict):
|
|
346
|
+
raise HostActionError(
|
|
347
|
+
"invalid_host_action_result", "Each debug configuration must be an object."
|
|
348
|
+
)
|
|
349
|
+
_reject_extra_keys(raw, {"id", "name", "type", "request", "workspace_folder"})
|
|
350
|
+
item: dict[str, Any] = {
|
|
351
|
+
"id": _required_identifier(raw.get("id"), "configuration.id"),
|
|
352
|
+
"name": _required_label(raw.get("name"), "configuration.name"),
|
|
353
|
+
"type": _required_label(raw.get("type"), "configuration.type"),
|
|
354
|
+
}
|
|
355
|
+
request = _optional_label(raw.get("request"), "configuration.request")
|
|
356
|
+
if request:
|
|
357
|
+
item["request"] = request
|
|
358
|
+
workspace_folder = _optional_label(
|
|
359
|
+
raw.get("workspace_folder"), "configuration.workspace_folder"
|
|
360
|
+
)
|
|
361
|
+
if workspace_folder:
|
|
362
|
+
item["workspace_folder"] = workspace_folder
|
|
363
|
+
normalized_items.append(item)
|
|
364
|
+
return {
|
|
365
|
+
"configurations": normalized_items,
|
|
366
|
+
"truncated": bool(
|
|
367
|
+
_optional_bool(result.get("truncated"), "truncated")
|
|
368
|
+
or len(items) > len(normalized_items)
|
|
369
|
+
),
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _normalize_debug_status_result(result: dict[str, Any]) -> dict[str, Any]:
|
|
374
|
+
_reject_extra_keys(result, {"sessions", "truncated"})
|
|
375
|
+
items = result.get("sessions")
|
|
376
|
+
if not isinstance(items, list):
|
|
377
|
+
raise HostActionError("invalid_host_action_result", "sessions must be an array.")
|
|
378
|
+
normalized_items: list[dict[str, Any]] = []
|
|
379
|
+
for raw in items[:HOST_ACTION_MAX_LIST_ITEMS]:
|
|
380
|
+
if not isinstance(raw, dict):
|
|
381
|
+
raise HostActionError(
|
|
382
|
+
"invalid_host_action_result", "Each debug session must be an object."
|
|
383
|
+
)
|
|
384
|
+
_reject_extra_keys(raw, {"id", "name", "type", "state", "workspace_folder"})
|
|
385
|
+
item: dict[str, Any] = {
|
|
386
|
+
"id": _required_identifier(raw.get("id"), "debug_session.id"),
|
|
387
|
+
"name": _required_label(raw.get("name"), "debug_session.name"),
|
|
388
|
+
"type": _required_label(raw.get("type"), "debug_session.type"),
|
|
389
|
+
"state": _enum_value(raw.get("state"), "debug_session.state", {"running", "stopped"}),
|
|
390
|
+
}
|
|
391
|
+
workspace_folder = _optional_label(
|
|
392
|
+
raw.get("workspace_folder"), "debug_session.workspace_folder"
|
|
393
|
+
)
|
|
394
|
+
if workspace_folder:
|
|
395
|
+
item["workspace_folder"] = workspace_folder
|
|
396
|
+
normalized_items.append(item)
|
|
397
|
+
return {
|
|
398
|
+
"sessions": normalized_items,
|
|
399
|
+
"truncated": bool(
|
|
400
|
+
_optional_bool(result.get("truncated"), "truncated")
|
|
401
|
+
or len(items) > len(normalized_items)
|
|
402
|
+
),
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _normalize_diagnostics_delta(value: Any) -> dict[str, Any]:
|
|
407
|
+
if not isinstance(value, dict):
|
|
408
|
+
raise HostActionError("invalid_host_action_result", "diagnostics_delta must be an object.")
|
|
409
|
+
_reject_extra_keys(value, {"added", "removed", "changed", "total", "truncated", "items"})
|
|
410
|
+
items = value.get("items", [])
|
|
411
|
+
if not isinstance(items, list):
|
|
412
|
+
raise HostActionError(
|
|
413
|
+
"invalid_host_action_result", "diagnostics_delta.items must be an array."
|
|
414
|
+
)
|
|
415
|
+
normalized_items: list[dict[str, Any]] = []
|
|
416
|
+
for raw in items[:50]:
|
|
417
|
+
if not isinstance(raw, dict):
|
|
418
|
+
raise HostActionError(
|
|
419
|
+
"invalid_host_action_result", "Each diagnostic delta item must be an object."
|
|
420
|
+
)
|
|
421
|
+
_reject_extra_keys(raw, {"uri", "line", "severity", "message", "source"})
|
|
422
|
+
raw_uri = raw.get("uri")
|
|
423
|
+
uri = raw_uri.strip() if isinstance(raw_uri, str) else ""
|
|
424
|
+
if not uri or len(uri) > 2048 or any(ord(char) < 32 or ord(char) == 127 for char in uri):
|
|
425
|
+
raise HostActionError(
|
|
426
|
+
"invalid_host_action_result", "diagnostic uri must be bounded non-empty text."
|
|
427
|
+
)
|
|
428
|
+
line = raw.get("line")
|
|
429
|
+
if isinstance(line, bool) or not isinstance(line, int) or not 1 <= line < 2**31:
|
|
430
|
+
raise HostActionError(
|
|
431
|
+
"invalid_host_action_result", "diagnostic line must be a positive integer."
|
|
432
|
+
)
|
|
433
|
+
item: dict[str, Any] = {
|
|
434
|
+
"uri": uri,
|
|
435
|
+
"line": line,
|
|
436
|
+
"severity": _enum_value(
|
|
437
|
+
raw.get("severity"),
|
|
438
|
+
"diagnostic.severity",
|
|
439
|
+
{"error", "warning", "information", "hint"},
|
|
440
|
+
),
|
|
441
|
+
"message": _required_label(raw.get("message"), "diagnostic.message"),
|
|
442
|
+
}
|
|
443
|
+
source = _optional_label(raw.get("source"), "diagnostic.source")
|
|
444
|
+
if source:
|
|
445
|
+
item["source"] = source
|
|
446
|
+
normalized_items.append(item)
|
|
447
|
+
return {
|
|
448
|
+
"added": _nonnegative_int(value.get("added"), "diagnostics_delta.added"),
|
|
449
|
+
"removed": _nonnegative_int(value.get("removed"), "diagnostics_delta.removed"),
|
|
450
|
+
"changed": _nonnegative_int(value.get("changed"), "diagnostics_delta.changed"),
|
|
451
|
+
"total": _nonnegative_int(value.get("total"), "diagnostics_delta.total"),
|
|
452
|
+
"truncated": bool(
|
|
453
|
+
_optional_bool(value.get("truncated"), "diagnostics_delta.truncated")
|
|
454
|
+
or len(items) > len(normalized_items)
|
|
455
|
+
),
|
|
456
|
+
"items": normalized_items,
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _reject_extra_keys(value: dict[str, Any], allowed: set[str]) -> None:
|
|
461
|
+
if extra := sorted(set(value) - allowed):
|
|
462
|
+
preview = ", ".join(str(key)[:64] for key in extra[:5])
|
|
463
|
+
if len(extra) > 5:
|
|
464
|
+
preview += ", ..."
|
|
465
|
+
raise HostActionError(
|
|
466
|
+
"invalid_host_action_payload",
|
|
467
|
+
"IDE host action payload contains unsupported field(s): " + preview,
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def _required_identifier(value: Any, field: str) -> str:
|
|
472
|
+
text = value.strip() if isinstance(value, str) else ""
|
|
473
|
+
if (
|
|
474
|
+
not text
|
|
475
|
+
or len(text) > HOST_ACTION_MAX_IDENTIFIER_CHARS
|
|
476
|
+
or any(ord(char) < 32 or ord(char) == 127 for char in text)
|
|
477
|
+
):
|
|
478
|
+
raise HostActionError(
|
|
479
|
+
"invalid_host_action_payload", f"{field} must be a bounded non-empty identifier."
|
|
480
|
+
)
|
|
481
|
+
return text
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _optional_identifier(value: Any, field: str) -> str | None:
|
|
485
|
+
if value is None:
|
|
486
|
+
return None
|
|
487
|
+
return _required_identifier(value, field)
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def _required_label(value: Any, field: str) -> str:
|
|
491
|
+
text = value.strip() if isinstance(value, str) else ""
|
|
492
|
+
if not text or len(text) > HOST_ACTION_MAX_LABEL_CHARS or _has_disallowed_text_control(text):
|
|
493
|
+
raise HostActionError(
|
|
494
|
+
"invalid_host_action_payload", f"{field} must be bounded non-empty text."
|
|
495
|
+
)
|
|
496
|
+
return text
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _optional_label(value: Any, field: str) -> str | None:
|
|
500
|
+
if value is None:
|
|
501
|
+
return None
|
|
502
|
+
return _required_label(value, field)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _enum_value(value: Any, field: str, allowed: set[str]) -> str:
|
|
506
|
+
text = value.strip() if isinstance(value, str) else ""
|
|
507
|
+
if text not in allowed:
|
|
508
|
+
raise HostActionError(
|
|
509
|
+
"invalid_host_action_payload",
|
|
510
|
+
f"{field} must be one of: " + ", ".join(sorted(allowed)),
|
|
511
|
+
)
|
|
512
|
+
return text
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def _has_disallowed_text_control(value: str) -> bool:
|
|
516
|
+
return any(
|
|
517
|
+
(ord(char) < 32 and char not in {"\t", "\n", "\r"}) or ord(char) == 127 for char in value
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def _required_bool(value: Any, field: str) -> bool:
|
|
522
|
+
if not isinstance(value, bool):
|
|
523
|
+
raise HostActionError("invalid_host_action_payload", f"{field} must be a boolean.")
|
|
524
|
+
return value
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def _optional_bool(value: Any, field: str) -> bool:
|
|
528
|
+
if value is None:
|
|
529
|
+
return False
|
|
530
|
+
return _required_bool(value, field)
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def _bounded_int(value: Any, field: str) -> int:
|
|
534
|
+
if isinstance(value, bool) or not isinstance(value, int) or not -(2**31) <= value < 2**31:
|
|
535
|
+
raise HostActionError("invalid_host_action_payload", f"{field} must be a 32-bit integer.")
|
|
536
|
+
return value
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def _nonnegative_int(value: Any, field: str) -> int:
|
|
540
|
+
normalized = _bounded_int(value, field)
|
|
541
|
+
if normalized < 0:
|
|
542
|
+
raise HostActionError("invalid_host_action_payload", f"{field} must be non-negative.")
|
|
543
|
+
return normalized
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Open URLs in the user's graphical browser across native and WSL hosts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import webbrowser
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
_LAUNCH_TIMEOUT_SECONDS = 8.0
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def is_wsl() -> bool:
|
|
15
|
+
"""Return whether the current Python process is running inside WSL."""
|
|
16
|
+
|
|
17
|
+
if os.name == "nt":
|
|
18
|
+
return False
|
|
19
|
+
if os.environ.get("WSL_DISTRO_NAME") or os.environ.get("WSL_INTEROP"):
|
|
20
|
+
return True
|
|
21
|
+
try:
|
|
22
|
+
return "microsoft" in Path("/proc/version").read_text(encoding="utf-8").lower()
|
|
23
|
+
except OSError:
|
|
24
|
+
return False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def open_url(url: str) -> bool:
|
|
28
|
+
"""Open *url* in the host browser, including the Windows browser from WSL."""
|
|
29
|
+
|
|
30
|
+
target = str(url or "").strip()
|
|
31
|
+
if not target:
|
|
32
|
+
return False
|
|
33
|
+
if is_wsl() and _open_url_from_wsl(target):
|
|
34
|
+
return True
|
|
35
|
+
try:
|
|
36
|
+
return bool(webbrowser.open(target))
|
|
37
|
+
except Exception: # noqa: BLE001 - browser launch is best-effort
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _open_url_from_wsl(url: str) -> bool:
|
|
42
|
+
for command in _wsl_browser_commands(url):
|
|
43
|
+
try:
|
|
44
|
+
result = subprocess.run(
|
|
45
|
+
command,
|
|
46
|
+
check=False,
|
|
47
|
+
stdin=subprocess.DEVNULL,
|
|
48
|
+
stdout=subprocess.DEVNULL,
|
|
49
|
+
stderr=subprocess.DEVNULL,
|
|
50
|
+
timeout=_LAUNCH_TIMEOUT_SECONDS,
|
|
51
|
+
)
|
|
52
|
+
except (OSError, subprocess.SubprocessError):
|
|
53
|
+
continue
|
|
54
|
+
if result.returncode == 0:
|
|
55
|
+
return True
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _wsl_browser_commands(url: str) -> tuple[tuple[str, ...], ...]:
|
|
60
|
+
commands: list[tuple[str, ...]] = []
|
|
61
|
+
wslview = shutil.which("wslview")
|
|
62
|
+
if wslview:
|
|
63
|
+
commands.append((wslview, url))
|
|
64
|
+
|
|
65
|
+
powershell = _windows_executable(
|
|
66
|
+
"powershell.exe",
|
|
67
|
+
Path("/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe"),
|
|
68
|
+
)
|
|
69
|
+
if powershell:
|
|
70
|
+
commands.append(
|
|
71
|
+
(
|
|
72
|
+
powershell,
|
|
73
|
+
"-NoLogo",
|
|
74
|
+
"-NoProfile",
|
|
75
|
+
"-NonInteractive",
|
|
76
|
+
"-Command",
|
|
77
|
+
"Start-Process -FilePath $args[0]",
|
|
78
|
+
url,
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
protocol_handler = _windows_executable(
|
|
83
|
+
"rundll32.exe",
|
|
84
|
+
Path("/mnt/c/Windows/System32/rundll32.exe"),
|
|
85
|
+
)
|
|
86
|
+
if protocol_handler:
|
|
87
|
+
commands.append((protocol_handler, "url.dll,FileProtocolHandler", url))
|
|
88
|
+
return tuple(commands)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _windows_executable(name: str, fallback: Path) -> str | None:
|
|
92
|
+
resolved = shutil.which(name)
|
|
93
|
+
if resolved:
|
|
94
|
+
return resolved
|
|
95
|
+
try:
|
|
96
|
+
if fallback.is_file():
|
|
97
|
+
return os.fspath(fallback)
|
|
98
|
+
except OSError:
|
|
99
|
+
pass
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
__all__ = ["is_wsl", "open_url"]
|