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,487 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import tomllib
|
|
5
|
+
import warnings
|
|
6
|
+
from pathlib import Path, PurePosixPath, PureWindowsPath
|
|
7
|
+
from typing import Annotated, Any, Literal
|
|
8
|
+
|
|
9
|
+
from packaging.specifiers import InvalidSpecifier, SpecifierSet
|
|
10
|
+
from packaging.version import InvalidVersion, Version
|
|
11
|
+
from pydantic import (
|
|
12
|
+
AliasChoices,
|
|
13
|
+
BaseModel,
|
|
14
|
+
ConfigDict,
|
|
15
|
+
Field,
|
|
16
|
+
HttpUrl,
|
|
17
|
+
TypeAdapter,
|
|
18
|
+
ValidationError,
|
|
19
|
+
field_validator,
|
|
20
|
+
model_validator,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
MANIFEST_FILENAME = "alysis-plugin.toml"
|
|
24
|
+
# Third-party plugins published before the rename ship this filename; they are
|
|
25
|
+
# out of our control, so both spellings stay loadable.
|
|
26
|
+
LEGACY_MANIFEST_FILENAME = "sylliptor-plugin.toml"
|
|
27
|
+
MAX_MANIFEST_BYTES = 64 * 1024
|
|
28
|
+
|
|
29
|
+
_PLUGIN_ID_PATTERN = r"^[a-z][a-z0-9_]*\.[a-z][a-z0-9_-]*$"
|
|
30
|
+
_ENV_NAME_PATTERN = r"^[A-Z][A-Z0-9_]*$"
|
|
31
|
+
_KEYWORD_PATTERN = r"^[a-z0-9_-]+$"
|
|
32
|
+
_EMAIL_PATTERN = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
|
|
33
|
+
_COMMON_SPDX_LICENSE_IDS = frozenset(
|
|
34
|
+
{
|
|
35
|
+
"Apache-2.0",
|
|
36
|
+
"BSD-2-Clause",
|
|
37
|
+
"BSD-3-Clause",
|
|
38
|
+
"GPL-3.0-only",
|
|
39
|
+
"GPL-3.0-or-later",
|
|
40
|
+
"ISC",
|
|
41
|
+
"LGPL-3.0-only",
|
|
42
|
+
"LGPL-3.0-or-later",
|
|
43
|
+
"MIT",
|
|
44
|
+
"MPL-2.0",
|
|
45
|
+
"Proprietary",
|
|
46
|
+
"Unlicense",
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
_CONTACT_URL_ADAPTER = TypeAdapter(HttpUrl)
|
|
50
|
+
|
|
51
|
+
PluginId = Annotated[str, Field(max_length=64, pattern=_PLUGIN_ID_PATTERN)]
|
|
52
|
+
PluginName = Annotated[str, Field(min_length=1, max_length=60)]
|
|
53
|
+
Text200 = Annotated[str, Field(min_length=1, max_length=200)]
|
|
54
|
+
NonEmptyString = Annotated[str, Field(min_length=1)]
|
|
55
|
+
Keyword = Annotated[str, Field(min_length=1, max_length=32, pattern=_KEYWORD_PATTERN)]
|
|
56
|
+
ComponentPath = Annotated[str, Field(min_length=1)]
|
|
57
|
+
OptionalComponentId = Annotated[str, Field(min_length=1)] | None
|
|
58
|
+
EnvName = Annotated[str, Field(pattern=_ENV_NAME_PATTERN)]
|
|
59
|
+
ScopeName = Annotated[str, Field(min_length=1)]
|
|
60
|
+
CommandArg = Annotated[str, Field(min_length=1)]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class PluginManifestError(Exception):
|
|
64
|
+
def __init__(self, errors: list[str] | tuple[str, ...]) -> None:
|
|
65
|
+
self.errors = tuple(str(error) for error in errors)
|
|
66
|
+
if self.errors:
|
|
67
|
+
message = "Invalid plugin manifest:\n" + "\n".join(
|
|
68
|
+
f" - {error}" for error in self.errors
|
|
69
|
+
)
|
|
70
|
+
else:
|
|
71
|
+
message = "Invalid plugin manifest."
|
|
72
|
+
super().__init__(message)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class PluginMeta(BaseModel):
|
|
76
|
+
model_config = ConfigDict(extra="forbid")
|
|
77
|
+
|
|
78
|
+
id: PluginId
|
|
79
|
+
name: PluginName
|
|
80
|
+
version: NonEmptyString
|
|
81
|
+
description: Text200
|
|
82
|
+
author: Text200
|
|
83
|
+
license: NonEmptyString
|
|
84
|
+
homepage: HttpUrl | None = None
|
|
85
|
+
repository: HttpUrl | None = None
|
|
86
|
+
keywords: list[Keyword] = Field(default_factory=list, max_length=8)
|
|
87
|
+
|
|
88
|
+
@field_validator("version")
|
|
89
|
+
@classmethod
|
|
90
|
+
def _validate_version(cls, value: str) -> str:
|
|
91
|
+
try:
|
|
92
|
+
Version(value)
|
|
93
|
+
except InvalidVersion as exc:
|
|
94
|
+
raise ValueError("must be a valid PEP 440 / SemVer version") from exc
|
|
95
|
+
return value
|
|
96
|
+
|
|
97
|
+
@field_validator("license")
|
|
98
|
+
@classmethod
|
|
99
|
+
def _validate_license(cls, value: str) -> str:
|
|
100
|
+
if not value.strip():
|
|
101
|
+
raise ValueError("must not be empty")
|
|
102
|
+
return value
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class Compatibility(BaseModel):
|
|
106
|
+
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
|
107
|
+
|
|
108
|
+
# Manifests written before the rename declare this key as ``sylliptor``.
|
|
109
|
+
# With extra="forbid" they would fail validation outright, so the old
|
|
110
|
+
# spelling stays accepted as an alias.
|
|
111
|
+
alysis: NonEmptyString = Field(
|
|
112
|
+
validation_alias=AliasChoices("alysis", "sylliptor"),
|
|
113
|
+
serialization_alias="alysis",
|
|
114
|
+
)
|
|
115
|
+
platforms: list[Literal["linux", "darwin", "windows"]] = Field(
|
|
116
|
+
default_factory=lambda: ["linux", "darwin", "windows"]
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
@field_validator("alysis")
|
|
120
|
+
@classmethod
|
|
121
|
+
def _validate_alysis(cls, value: str) -> str:
|
|
122
|
+
try:
|
|
123
|
+
SpecifierSet(value)
|
|
124
|
+
except InvalidSpecifier as exc:
|
|
125
|
+
raise ValueError("must be a valid PEP 440 specifier set") from exc
|
|
126
|
+
return value
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class SkillComponent(BaseModel):
|
|
130
|
+
model_config = ConfigDict(extra="forbid")
|
|
131
|
+
|
|
132
|
+
path: ComponentPath
|
|
133
|
+
id: OptionalComponentId = None
|
|
134
|
+
enabled: bool = True
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class ToolComponent(BaseModel):
|
|
138
|
+
model_config = ConfigDict(extra="forbid")
|
|
139
|
+
|
|
140
|
+
path: ComponentPath
|
|
141
|
+
id: OptionalComponentId = None
|
|
142
|
+
enabled: bool = True
|
|
143
|
+
description: Text200
|
|
144
|
+
required_env: list[EnvName] = Field(default_factory=list)
|
|
145
|
+
network: bool
|
|
146
|
+
filesystem: Literal["none", "read", "write"]
|
|
147
|
+
timeout_sec: Annotated[int, Field(ge=1, le=600)] = 60
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class McpServerComponent(BaseModel):
|
|
151
|
+
model_config = ConfigDict(extra="forbid")
|
|
152
|
+
|
|
153
|
+
id: Annotated[str, Field(min_length=1)]
|
|
154
|
+
enabled: bool = True
|
|
155
|
+
transport: Literal["stdio", "http"]
|
|
156
|
+
command: list[CommandArg] | None = Field(default=None, min_length=1)
|
|
157
|
+
url: HttpUrl | None = None
|
|
158
|
+
env: list[EnvName] = Field(default_factory=list)
|
|
159
|
+
scopes: list[ScopeName] = Field(min_length=1, max_length=32)
|
|
160
|
+
oauth: dict[str, Any] | None = None
|
|
161
|
+
|
|
162
|
+
@model_validator(mode="after")
|
|
163
|
+
def _validate_transport(self) -> McpServerComponent:
|
|
164
|
+
if self.transport == "stdio":
|
|
165
|
+
if self.command is None:
|
|
166
|
+
raise ValueError("command is required when transport is 'stdio'")
|
|
167
|
+
if self.url is not None:
|
|
168
|
+
raise ValueError("url is not allowed when transport is 'stdio'")
|
|
169
|
+
return self
|
|
170
|
+
|
|
171
|
+
if self.url is None:
|
|
172
|
+
raise ValueError("url is required when transport is 'http'")
|
|
173
|
+
if self.command is not None:
|
|
174
|
+
raise ValueError("command is not allowed when transport is 'http'")
|
|
175
|
+
|
|
176
|
+
scheme = str(self.url.scheme).lower()
|
|
177
|
+
host = str(self.url.host or "").lower()
|
|
178
|
+
if scheme == "http" and host not in {"localhost", "127.0.0.1"}:
|
|
179
|
+
raise ValueError("url must use https unless host is localhost or 127.0.0.1")
|
|
180
|
+
return self
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class HookComponent(BaseModel):
|
|
184
|
+
model_config = ConfigDict(extra="forbid")
|
|
185
|
+
|
|
186
|
+
event: Literal["PreToolUse", "PostToolUse", "PostWrite", "SessionStart", "SessionStop"]
|
|
187
|
+
path: ComponentPath
|
|
188
|
+
id: OptionalComponentId = None
|
|
189
|
+
enabled: bool = True
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class Components(BaseModel):
|
|
193
|
+
model_config = ConfigDict(extra="forbid")
|
|
194
|
+
|
|
195
|
+
skill: list[SkillComponent] = Field(default_factory=list)
|
|
196
|
+
tool: list[ToolComponent] = Field(default_factory=list)
|
|
197
|
+
mcp_server: list[McpServerComponent] = Field(default_factory=list)
|
|
198
|
+
hook: list[HookComponent] = Field(default_factory=list)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class Security(BaseModel):
|
|
202
|
+
model_config = ConfigDict(extra="forbid")
|
|
203
|
+
|
|
204
|
+
contact: NonEmptyString
|
|
205
|
+
policy_url: HttpUrl | None = None
|
|
206
|
+
disclosure_days: Annotated[int, Field(ge=1, le=365)] | None = None
|
|
207
|
+
|
|
208
|
+
@field_validator("contact")
|
|
209
|
+
@classmethod
|
|
210
|
+
def _validate_contact(cls, value: str) -> str:
|
|
211
|
+
if _EMAIL_PATTERN.fullmatch(value):
|
|
212
|
+
return value
|
|
213
|
+
try:
|
|
214
|
+
parsed = _CONTACT_URL_ADAPTER.validate_python(value)
|
|
215
|
+
except ValidationError as exc:
|
|
216
|
+
raise ValueError("must be an email address or https URL") from exc
|
|
217
|
+
if str(parsed.scheme).lower() != "https":
|
|
218
|
+
raise ValueError("must be an email address or https URL")
|
|
219
|
+
return value
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class PluginManifest(BaseModel):
|
|
223
|
+
model_config = ConfigDict(extra="forbid")
|
|
224
|
+
|
|
225
|
+
schema_version: Literal[1]
|
|
226
|
+
plugin: PluginMeta
|
|
227
|
+
compatibility: Compatibility
|
|
228
|
+
components: Components = Field(default_factory=Components)
|
|
229
|
+
security: Security | None = None
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def resolve_manifest_path(plugin_root: Path) -> Path:
|
|
233
|
+
"""Locate a plugin manifest, preferring the current filename.
|
|
234
|
+
|
|
235
|
+
Falls back to the pre-rebrand name so an already-published plugin keeps
|
|
236
|
+
installing. The current name is returned when neither exists, so the
|
|
237
|
+
"missing manifest" error names the file authors should be writing.
|
|
238
|
+
"""
|
|
239
|
+
current = plugin_root / MANIFEST_FILENAME
|
|
240
|
+
if current.is_file():
|
|
241
|
+
return current
|
|
242
|
+
legacy = plugin_root / LEGACY_MANIFEST_FILENAME
|
|
243
|
+
if legacy.is_file():
|
|
244
|
+
return legacy
|
|
245
|
+
return current
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def load_manifest(plugin_root: Path) -> PluginManifest:
|
|
249
|
+
manifest_path = resolve_manifest_path(plugin_root)
|
|
250
|
+
text = _read_manifest_text(manifest_path)
|
|
251
|
+
|
|
252
|
+
try:
|
|
253
|
+
raw = tomllib.loads(text)
|
|
254
|
+
except tomllib.TOMLDecodeError as exc:
|
|
255
|
+
raise PluginManifestError([_format_toml_error(exc)]) from exc
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
manifest = PluginManifest.model_validate(raw)
|
|
259
|
+
except ValidationError as exc:
|
|
260
|
+
raise PluginManifestError(_format_validation_errors(exc)) from exc
|
|
261
|
+
|
|
262
|
+
invariant_messages = _validate_manifest_invariants(manifest, plugin_root)
|
|
263
|
+
warning_messages = [
|
|
264
|
+
message.removeprefix("WARN: ").strip()
|
|
265
|
+
for message in invariant_messages
|
|
266
|
+
if message.startswith("WARN: ")
|
|
267
|
+
]
|
|
268
|
+
error_messages = [message for message in invariant_messages if not message.startswith("WARN: ")]
|
|
269
|
+
|
|
270
|
+
license_warning = _license_warning(manifest)
|
|
271
|
+
if license_warning is not None:
|
|
272
|
+
warning_messages.append(license_warning)
|
|
273
|
+
|
|
274
|
+
if error_messages:
|
|
275
|
+
raise PluginManifestError(
|
|
276
|
+
[*error_messages, *(f"WARN: {message}" for message in warning_messages)]
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
for message in warning_messages:
|
|
280
|
+
warnings.warn(message, stacklevel=2)
|
|
281
|
+
|
|
282
|
+
return manifest
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _validate_manifest_invariants(manifest: PluginManifest, plugin_root: Path) -> list[str]:
|
|
286
|
+
messages: list[str] = []
|
|
287
|
+
total_components = (
|
|
288
|
+
len(manifest.components.skill)
|
|
289
|
+
+ len(manifest.components.tool)
|
|
290
|
+
+ len(manifest.components.mcp_server)
|
|
291
|
+
+ len(manifest.components.hook)
|
|
292
|
+
)
|
|
293
|
+
if total_components > 32:
|
|
294
|
+
messages.append(
|
|
295
|
+
f"components: total component count {total_components} exceeds the cap of 32"
|
|
296
|
+
)
|
|
297
|
+
if total_components == 0:
|
|
298
|
+
messages.append(
|
|
299
|
+
"WARN: components: manifest declares no components; metadata-only plugins are allowed "
|
|
300
|
+
"but contribute nothing."
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
plugin_root_resolved = plugin_root.resolve()
|
|
304
|
+
for collection_name, index, raw_path in _iter_component_paths(manifest):
|
|
305
|
+
path_label = f"components.{collection_name}[{index}].path"
|
|
306
|
+
if _is_path_absolute(raw_path):
|
|
307
|
+
messages.append(f"{path_label}: absolute paths are not allowed")
|
|
308
|
+
continue
|
|
309
|
+
if _contains_parent_reference(raw_path):
|
|
310
|
+
messages.append(f"{path_label}: path escapes or could resolve outside plugin root")
|
|
311
|
+
continue
|
|
312
|
+
|
|
313
|
+
component_path = _resolve_component_path(plugin_root_resolved, raw_path)
|
|
314
|
+
try:
|
|
315
|
+
component_path.relative_to(plugin_root_resolved)
|
|
316
|
+
except ValueError:
|
|
317
|
+
messages.append(f"{path_label}: resolved path is outside plugin root: {raw_path}")
|
|
318
|
+
continue
|
|
319
|
+
|
|
320
|
+
if not component_path.exists():
|
|
321
|
+
messages.append(f"{path_label}: path does not exist: {raw_path}")
|
|
322
|
+
|
|
323
|
+
for collection_name, ids in _effective_ids_by_type(manifest).items():
|
|
324
|
+
seen: dict[str, int] = {}
|
|
325
|
+
for index, effective_id in enumerate(ids):
|
|
326
|
+
if effective_id in seen:
|
|
327
|
+
messages.append(
|
|
328
|
+
"components."
|
|
329
|
+
f"{collection_name}[{index}]: duplicate effective id {effective_id!r} "
|
|
330
|
+
f"within {collection_name}"
|
|
331
|
+
)
|
|
332
|
+
continue
|
|
333
|
+
seen[effective_id] = index
|
|
334
|
+
|
|
335
|
+
return messages
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _read_manifest_text(manifest_path: Path) -> str:
|
|
339
|
+
try:
|
|
340
|
+
size = manifest_path.stat().st_size
|
|
341
|
+
except FileNotFoundError as exc:
|
|
342
|
+
raise PluginManifestError([f"manifest file not found: {manifest_path}"]) from exc
|
|
343
|
+
except OSError as exc:
|
|
344
|
+
raise PluginManifestError([f"failed to stat manifest file: {manifest_path}"]) from exc
|
|
345
|
+
|
|
346
|
+
if size > MAX_MANIFEST_BYTES:
|
|
347
|
+
raise PluginManifestError(
|
|
348
|
+
[f"manifest file exceeds the {MAX_MANIFEST_BYTES}-byte limit: {size} bytes"]
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
try:
|
|
352
|
+
raw_bytes = manifest_path.read_bytes()
|
|
353
|
+
except OSError as exc:
|
|
354
|
+
raise PluginManifestError([f"failed to read manifest file: {manifest_path}"]) from exc
|
|
355
|
+
|
|
356
|
+
try:
|
|
357
|
+
return raw_bytes.decode("utf-8")
|
|
358
|
+
except UnicodeDecodeError as exc:
|
|
359
|
+
raise PluginManifestError(["manifest file is not valid UTF-8"]) from exc
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _format_toml_error(exc: tomllib.TOMLDecodeError) -> str:
|
|
363
|
+
line = getattr(exc, "lineno", None)
|
|
364
|
+
column = getattr(exc, "colno", None)
|
|
365
|
+
if line is None or column is None:
|
|
366
|
+
position = getattr(exc, "pos", None)
|
|
367
|
+
document = getattr(exc, "doc", None)
|
|
368
|
+
if isinstance(position, int) and isinstance(document, str):
|
|
369
|
+
prefix = document[:position]
|
|
370
|
+
line = prefix.count("\n") + 1
|
|
371
|
+
column = position - prefix.rfind("\n")
|
|
372
|
+
location = ""
|
|
373
|
+
if isinstance(line, int) and isinstance(column, int):
|
|
374
|
+
location = f" at line {line}, column {column}"
|
|
375
|
+
reason = getattr(exc, "msg", None) or str(exc)
|
|
376
|
+
return f"toml parse error{location}: {reason}"
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _format_validation_errors(exc: ValidationError) -> list[str]:
|
|
380
|
+
formatted: list[str] = []
|
|
381
|
+
for error in exc.errors():
|
|
382
|
+
location = _format_error_location(error.get("loc", ()))
|
|
383
|
+
message = _normalize_validation_message(str(error.get("msg", "Invalid value")))
|
|
384
|
+
if location:
|
|
385
|
+
formatted.append(f"{location}: {message}")
|
|
386
|
+
else:
|
|
387
|
+
formatted.append(message)
|
|
388
|
+
return formatted
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _format_error_location(location: tuple[Any, ...] | list[Any]) -> str:
|
|
392
|
+
parts: list[str] = []
|
|
393
|
+
for item in location:
|
|
394
|
+
if isinstance(item, int):
|
|
395
|
+
if parts:
|
|
396
|
+
parts[-1] = f"{parts[-1]}[{item}]"
|
|
397
|
+
else:
|
|
398
|
+
parts.append(f"[{item}]")
|
|
399
|
+
continue
|
|
400
|
+
parts.append(str(item))
|
|
401
|
+
return ".".join(parts)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _normalize_validation_message(message: str) -> str:
|
|
405
|
+
return message.removeprefix("Value error, ")
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _iter_component_paths(manifest: PluginManifest) -> list[tuple[str, int, str]]:
|
|
409
|
+
paths: list[tuple[str, int, str]] = []
|
|
410
|
+
for collection_name, components in (
|
|
411
|
+
("skill", manifest.components.skill),
|
|
412
|
+
("tool", manifest.components.tool),
|
|
413
|
+
("hook", manifest.components.hook),
|
|
414
|
+
):
|
|
415
|
+
for index, component in enumerate(components):
|
|
416
|
+
paths.append((collection_name, index, component.path))
|
|
417
|
+
return paths
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _effective_ids_by_type(manifest: PluginManifest) -> dict[str, list[str]]:
|
|
421
|
+
return {
|
|
422
|
+
"skill": [
|
|
423
|
+
component.id or _default_component_id(component.path)
|
|
424
|
+
for component in manifest.components.skill
|
|
425
|
+
],
|
|
426
|
+
"tool": [
|
|
427
|
+
component.id or _default_component_id(component.path)
|
|
428
|
+
for component in manifest.components.tool
|
|
429
|
+
],
|
|
430
|
+
"mcp_server": [component.id for component in manifest.components.mcp_server],
|
|
431
|
+
"hook": [
|
|
432
|
+
component.id or _default_component_id(component.path)
|
|
433
|
+
for component in manifest.components.hook
|
|
434
|
+
],
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _default_component_id(path_value: str) -> str:
|
|
439
|
+
return PureWindowsPath(path_value).name or PurePosixPath(path_value).name
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _is_path_absolute(path_value: str) -> bool:
|
|
443
|
+
posix_path = PurePosixPath(path_value)
|
|
444
|
+
windows_path = PureWindowsPath(path_value)
|
|
445
|
+
return bool(posix_path.is_absolute() or windows_path.is_absolute() or windows_path.drive)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _contains_parent_reference(path_value: str) -> bool:
|
|
449
|
+
posix_parts = PurePosixPath(path_value).parts
|
|
450
|
+
windows_parts = PureWindowsPath(path_value).parts
|
|
451
|
+
return ".." in posix_parts or ".." in windows_parts
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def _resolve_component_path(plugin_root: Path, path_value: str) -> Path:
|
|
455
|
+
windows_path = PureWindowsPath(path_value)
|
|
456
|
+
parts = [part for part in windows_path.parts if part not in {"", "."}]
|
|
457
|
+
return plugin_root.joinpath(*parts).resolve()
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _license_warning(manifest: PluginManifest) -> str | None:
|
|
461
|
+
license_id = manifest.plugin.license.strip()
|
|
462
|
+
if license_id in _COMMON_SPDX_LICENSE_IDS:
|
|
463
|
+
return None
|
|
464
|
+
return (
|
|
465
|
+
f"plugin.license: {license_id!r} is not in the common SPDX allow-list; "
|
|
466
|
+
"double-check the identifier."
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
__all__ = [
|
|
471
|
+
"Compatibility",
|
|
472
|
+
"Components",
|
|
473
|
+
"HookComponent",
|
|
474
|
+
"LEGACY_MANIFEST_FILENAME",
|
|
475
|
+
"MANIFEST_FILENAME",
|
|
476
|
+
"resolve_manifest_path",
|
|
477
|
+
"MAX_MANIFEST_BYTES",
|
|
478
|
+
"McpServerComponent",
|
|
479
|
+
"PluginManifest",
|
|
480
|
+
"PluginManifestError",
|
|
481
|
+
"PluginMeta",
|
|
482
|
+
"Security",
|
|
483
|
+
"SkillComponent",
|
|
484
|
+
"ToolComponent",
|
|
485
|
+
"_validate_manifest_invariants",
|
|
486
|
+
"load_manifest",
|
|
487
|
+
]
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
7
|
+
|
|
8
|
+
_NON_ALNUM_RE = re.compile(r"[^a-z0-9]+")
|
|
9
|
+
_REPEATED_DASH_RE = re.compile(r"-+")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def normalize_extension_id(value: str) -> str:
|
|
13
|
+
return value.strip().lower()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def plugin_slug_from_id(ext_id: str) -> str:
|
|
17
|
+
slug = _NON_ALNUM_RE.sub("-", normalize_extension_id(ext_id))
|
|
18
|
+
slug = _REPEATED_DASH_RE.sub("-", slug).strip("-")
|
|
19
|
+
return slug or "ext"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RegistryEntry(BaseModel):
|
|
23
|
+
model_config = ConfigDict(extra="allow")
|
|
24
|
+
|
|
25
|
+
id: str
|
|
26
|
+
name: str
|
|
27
|
+
description: str
|
|
28
|
+
repo: str
|
|
29
|
+
commit: str
|
|
30
|
+
version: str | None = None
|
|
31
|
+
tags: list[str] = Field(default_factory=list)
|
|
32
|
+
permissions: list[str] = Field(default_factory=list)
|
|
33
|
+
manifest_path: str | None = None
|
|
34
|
+
sha256: str | None = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class RegistryFile(BaseModel):
|
|
38
|
+
model_config = ConfigDict(extra="allow")
|
|
39
|
+
|
|
40
|
+
schema_version: int = 1
|
|
41
|
+
extensions: list[RegistryEntry] = Field(default_factory=list)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class InstalledExtensionState(BaseModel):
|
|
45
|
+
model_config = ConfigDict(extra="allow")
|
|
46
|
+
|
|
47
|
+
id: str | None = None
|
|
48
|
+
version: str | None = None
|
|
49
|
+
commit: str | None = None
|
|
50
|
+
source: str | None = None
|
|
51
|
+
trust: str | None = None
|
|
52
|
+
enabled: bool = False
|
|
53
|
+
manifest_sha256: str | None = None
|
|
54
|
+
installed_at: str | None = None
|
|
55
|
+
source_url: str | None = None
|
|
56
|
+
scope: Literal["user", "project"] | None = None
|
|
57
|
+
component_ids: dict[str, list[str]] = Field(default_factory=dict)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ExtensionState(BaseModel):
|
|
61
|
+
model_config = ConfigDict(extra="allow")
|
|
62
|
+
|
|
63
|
+
schema_version: int = 1
|
|
64
|
+
installed: dict[str, InstalledExtensionState] = Field(default_factory=dict)
|
|
65
|
+
enabled: list[str] = Field(default_factory=list)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ProjectExtensionOverrides(BaseModel):
|
|
69
|
+
model_config = ConfigDict(extra="allow")
|
|
70
|
+
|
|
71
|
+
schema_version: int = 1
|
|
72
|
+
enabled: list[str] = Field(default_factory=list)
|
|
73
|
+
disabled: list[str] = Field(default_factory=list)
|
|
74
|
+
allow_overrides: bool = False
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from ..branding import canonical_user_config_dir, canonical_user_data_dir, env_get
|
|
6
|
+
from .models import plugin_slug_from_id
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _config_dir() -> Path:
|
|
10
|
+
override = env_get("ALYSIS_CONFIG_DIR")
|
|
11
|
+
if override:
|
|
12
|
+
return Path(override)
|
|
13
|
+
return canonical_user_config_dir()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _data_dir() -> Path:
|
|
17
|
+
override = env_get("ALYSIS_DATA_DIR")
|
|
18
|
+
if override:
|
|
19
|
+
return Path(override)
|
|
20
|
+
return canonical_user_data_dir()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def extensions_data_dir() -> Path:
|
|
24
|
+
return _data_dir() / "extensions"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def extensions_state_path() -> Path:
|
|
28
|
+
return extensions_data_dir() / "state.json"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def extensions_cache_dir() -> Path:
|
|
32
|
+
return extensions_data_dir() / "cache"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def project_extensions_path(repo_root: Path) -> Path:
|
|
36
|
+
return repo_root.resolve() / ".alysis" / "extensions.json"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def workspace_trust_path(*, user_config_dir: Path | None = None) -> Path:
|
|
40
|
+
if user_config_dir is not None:
|
|
41
|
+
return user_config_dir.expanduser().resolve() / "extensions" / "workspace_trust.json"
|
|
42
|
+
return extensions_data_dir() / "workspace_trust.json"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def installed_plugin_root(
|
|
46
|
+
plugin_id: str,
|
|
47
|
+
*,
|
|
48
|
+
project: bool,
|
|
49
|
+
repo_root: Path | None,
|
|
50
|
+
) -> Path:
|
|
51
|
+
slug = plugin_slug_from_id(plugin_id)
|
|
52
|
+
if project:
|
|
53
|
+
if repo_root is None:
|
|
54
|
+
raise ValueError("repo_root is required for project plugin installs.")
|
|
55
|
+
return repo_root.resolve() / ".alysis" / "plugins" / slug
|
|
56
|
+
return extensions_data_dir() / "installed" / slug
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from importlib import resources
|
|
5
|
+
|
|
6
|
+
from .models import RegistryEntry, RegistryFile, normalize_extension_id
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def load_registry() -> RegistryFile:
|
|
10
|
+
try:
|
|
11
|
+
text = (
|
|
12
|
+
resources.files("alysis_code.extensions")
|
|
13
|
+
.joinpath("registry.json")
|
|
14
|
+
.read_text(encoding="utf-8")
|
|
15
|
+
)
|
|
16
|
+
except FileNotFoundError as e:
|
|
17
|
+
raise RuntimeError("Bundled extensions registry is missing.") from e
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
raw = json.loads(text)
|
|
21
|
+
except json.JSONDecodeError as e:
|
|
22
|
+
raise RuntimeError("Bundled extensions registry is not valid JSON.") from e
|
|
23
|
+
|
|
24
|
+
if not isinstance(raw, dict):
|
|
25
|
+
raise RuntimeError("Bundled extensions registry must be a JSON object.")
|
|
26
|
+
return RegistryFile.model_validate(raw)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def find_by_id(registry: RegistryFile, ext_id: str) -> RegistryEntry | None:
|
|
30
|
+
target = normalize_extension_id(ext_id)
|
|
31
|
+
for entry in registry.extensions:
|
|
32
|
+
if normalize_extension_id(entry.id) == target:
|
|
33
|
+
return entry
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def search(registry: RegistryFile, query: str) -> list[RegistryEntry]:
|
|
38
|
+
needle = query.strip().casefold()
|
|
39
|
+
if not needle:
|
|
40
|
+
return list(registry.extensions)
|
|
41
|
+
|
|
42
|
+
matches: list[RegistryEntry] = []
|
|
43
|
+
for entry in registry.extensions:
|
|
44
|
+
haystack = [
|
|
45
|
+
entry.id,
|
|
46
|
+
entry.name,
|
|
47
|
+
entry.description,
|
|
48
|
+
*entry.tags,
|
|
49
|
+
]
|
|
50
|
+
if any(needle in str(field).casefold() for field in haystack):
|
|
51
|
+
matches.append(entry)
|
|
52
|
+
return matches
|