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,441 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from pathlib import Path, PurePosixPath
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
from ..atomic_io import atomic_write_json
|
|
9
|
+
from .models import DiscoveredSkills, SkillBundle
|
|
10
|
+
from .paths import global_skills_state_path, project_skills_state_path
|
|
11
|
+
|
|
12
|
+
SkillInstallSourceKind = Literal["dir", "zip", "git", "scaffold"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SkillLifecycleError(RuntimeError):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class SkillLifecycleIssue:
|
|
21
|
+
source_path: Path
|
|
22
|
+
message: str
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class ManagedSkillRecord:
|
|
27
|
+
name: str
|
|
28
|
+
bundle_dir: str
|
|
29
|
+
scope: Literal["project", "user"]
|
|
30
|
+
source_kind: SkillInstallSourceKind | None = None
|
|
31
|
+
source: str | None = None
|
|
32
|
+
source_subdir: str | None = None
|
|
33
|
+
source_commit: str | None = None
|
|
34
|
+
installed_at: str | None = None
|
|
35
|
+
family: str = "native"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class SkillLifecycleState:
|
|
40
|
+
managed_installs: dict[str, ManagedSkillRecord] = field(default_factory=dict)
|
|
41
|
+
disabled_names: tuple[str, ...] = ()
|
|
42
|
+
enabled_names: tuple[str, ...] = ()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class SkillCatalogEntry:
|
|
47
|
+
skill: SkillBundle
|
|
48
|
+
enabled: bool
|
|
49
|
+
managed: bool
|
|
50
|
+
disabled_by: str | None = None
|
|
51
|
+
install_record: ManagedSkillRecord | None = None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class SkillCatalog:
|
|
56
|
+
effective: DiscoveredSkills
|
|
57
|
+
entries: tuple[SkillCatalogEntry, ...]
|
|
58
|
+
global_state: SkillLifecycleState
|
|
59
|
+
project_state: SkillLifecycleState
|
|
60
|
+
lifecycle_issues: tuple[SkillLifecycleIssue, ...] = ()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def normalize_skill_name(raw: str) -> str:
|
|
64
|
+
candidate = str(raw or "").strip().casefold()
|
|
65
|
+
if not candidate:
|
|
66
|
+
raise SkillLifecycleError("Skill name cannot be empty.")
|
|
67
|
+
return candidate
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def load_global_skill_state(*, user_config_dir: Path | None = None) -> SkillLifecycleState:
|
|
71
|
+
return _load_state(global_skills_state_path(user_config_dir=user_config_dir))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def load_global_skill_state_tolerant(
|
|
75
|
+
*,
|
|
76
|
+
user_config_dir: Path | None = None,
|
|
77
|
+
) -> tuple[SkillLifecycleState, tuple[SkillLifecycleIssue, ...]]:
|
|
78
|
+
return _load_state_tolerant(global_skills_state_path(user_config_dir=user_config_dir))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def save_global_skill_state(
|
|
82
|
+
state: SkillLifecycleState,
|
|
83
|
+
*,
|
|
84
|
+
user_config_dir: Path | None = None,
|
|
85
|
+
) -> None:
|
|
86
|
+
_save_state(global_skills_state_path(user_config_dir=user_config_dir), state)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def load_project_skill_state(workspace_root: Path) -> SkillLifecycleState:
|
|
90
|
+
return _load_state(project_skills_state_path(workspace_root))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def load_project_skill_state_tolerant(
|
|
94
|
+
workspace_root: Path,
|
|
95
|
+
) -> tuple[SkillLifecycleState, tuple[SkillLifecycleIssue, ...]]:
|
|
96
|
+
return _load_state_tolerant(project_skills_state_path(workspace_root))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def save_project_skill_state(workspace_root: Path, state: SkillLifecycleState) -> None:
|
|
100
|
+
_save_state(project_skills_state_path(workspace_root), state)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def with_managed_install(
|
|
104
|
+
state: SkillLifecycleState,
|
|
105
|
+
record: ManagedSkillRecord,
|
|
106
|
+
) -> SkillLifecycleState:
|
|
107
|
+
managed = dict(state.managed_installs)
|
|
108
|
+
managed[normalize_skill_name(record.name)] = record
|
|
109
|
+
return SkillLifecycleState(
|
|
110
|
+
managed_installs=dict(sorted(managed.items(), key=lambda item: item[0])),
|
|
111
|
+
disabled_names=state.disabled_names,
|
|
112
|
+
enabled_names=state.enabled_names,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def without_managed_install(state: SkillLifecycleState, name: str) -> SkillLifecycleState:
|
|
117
|
+
managed = dict(state.managed_installs)
|
|
118
|
+
managed.pop(normalize_skill_name(name), None)
|
|
119
|
+
return SkillLifecycleState(
|
|
120
|
+
managed_installs=dict(sorted(managed.items(), key=lambda item: item[0])),
|
|
121
|
+
disabled_names=tuple(
|
|
122
|
+
item for item in state.disabled_names if item != normalize_skill_name(name)
|
|
123
|
+
),
|
|
124
|
+
enabled_names=tuple(
|
|
125
|
+
item for item in state.enabled_names if item != normalize_skill_name(name)
|
|
126
|
+
),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def set_global_skill_disabled(
|
|
131
|
+
state: SkillLifecycleState, *, name: str, disabled: bool
|
|
132
|
+
) -> SkillLifecycleState:
|
|
133
|
+
key = normalize_skill_name(name)
|
|
134
|
+
disabled_names = set(state.disabled_names)
|
|
135
|
+
if disabled:
|
|
136
|
+
disabled_names.add(key)
|
|
137
|
+
else:
|
|
138
|
+
disabled_names.discard(key)
|
|
139
|
+
return SkillLifecycleState(
|
|
140
|
+
managed_installs=state.managed_installs,
|
|
141
|
+
disabled_names=tuple(sorted(disabled_names)),
|
|
142
|
+
enabled_names=(),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def set_project_skill_override(
|
|
147
|
+
state: SkillLifecycleState,
|
|
148
|
+
*,
|
|
149
|
+
name: str,
|
|
150
|
+
enabled: bool,
|
|
151
|
+
) -> SkillLifecycleState:
|
|
152
|
+
key = normalize_skill_name(name)
|
|
153
|
+
disabled_names = set(state.disabled_names)
|
|
154
|
+
enabled_names = set(state.enabled_names)
|
|
155
|
+
if enabled:
|
|
156
|
+
disabled_names.discard(key)
|
|
157
|
+
enabled_names.add(key)
|
|
158
|
+
else:
|
|
159
|
+
enabled_names.discard(key)
|
|
160
|
+
disabled_names.add(key)
|
|
161
|
+
return SkillLifecycleState(
|
|
162
|
+
managed_installs=state.managed_installs,
|
|
163
|
+
disabled_names=tuple(sorted(disabled_names)),
|
|
164
|
+
enabled_names=tuple(sorted(enabled_names)),
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def resolve_skill_catalog(
|
|
169
|
+
*,
|
|
170
|
+
discovered: DiscoveredSkills,
|
|
171
|
+
workspace_root: Path,
|
|
172
|
+
user_config_dir: Path | None = None,
|
|
173
|
+
) -> SkillCatalog:
|
|
174
|
+
global_state, global_issues = load_global_skill_state_tolerant(user_config_dir=user_config_dir)
|
|
175
|
+
project_state, project_issues = load_project_skill_state_tolerant(workspace_root)
|
|
176
|
+
entries: list[SkillCatalogEntry] = []
|
|
177
|
+
effective_skills: dict[str, SkillBundle] = {}
|
|
178
|
+
effective_ordered: list[SkillBundle] = []
|
|
179
|
+
for skill in discovered.ordered:
|
|
180
|
+
key = normalize_skill_name(skill.name)
|
|
181
|
+
global_record = global_state.managed_installs.get(key)
|
|
182
|
+
project_record = project_state.managed_installs.get(key)
|
|
183
|
+
install_record = project_record if skill.source_scope == "project" else global_record
|
|
184
|
+
enabled = True
|
|
185
|
+
disabled_by: str | None = None
|
|
186
|
+
if skill.source_scope == "user" and key in global_state.disabled_names:
|
|
187
|
+
enabled = False
|
|
188
|
+
disabled_by = "global"
|
|
189
|
+
if key in project_state.enabled_names:
|
|
190
|
+
enabled = True
|
|
191
|
+
disabled_by = None
|
|
192
|
+
if key in project_state.disabled_names:
|
|
193
|
+
enabled = False
|
|
194
|
+
disabled_by = "project"
|
|
195
|
+
entry = SkillCatalogEntry(
|
|
196
|
+
skill=skill,
|
|
197
|
+
enabled=enabled,
|
|
198
|
+
managed=install_record is not None,
|
|
199
|
+
disabled_by=disabled_by,
|
|
200
|
+
install_record=install_record,
|
|
201
|
+
)
|
|
202
|
+
entries.append(entry)
|
|
203
|
+
if enabled:
|
|
204
|
+
effective_skills[key] = skill
|
|
205
|
+
effective_ordered.append(skill)
|
|
206
|
+
return SkillCatalog(
|
|
207
|
+
effective=DiscoveredSkills(
|
|
208
|
+
skills=dict(sorted(effective_skills.items(), key=lambda item: item[0])),
|
|
209
|
+
ordered=tuple(effective_ordered),
|
|
210
|
+
issues=discovered.issues,
|
|
211
|
+
),
|
|
212
|
+
entries=tuple(entries),
|
|
213
|
+
global_state=global_state,
|
|
214
|
+
project_state=project_state,
|
|
215
|
+
lifecycle_issues=(*global_issues, *project_issues),
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def resolve_skill_catalog_entry(
|
|
220
|
+
*,
|
|
221
|
+
entries: tuple[SkillCatalogEntry, ...],
|
|
222
|
+
raw_name: str,
|
|
223
|
+
) -> SkillCatalogEntry | None:
|
|
224
|
+
needle = str(raw_name or "").strip().casefold()
|
|
225
|
+
if not needle:
|
|
226
|
+
return None
|
|
227
|
+
for entry in entries:
|
|
228
|
+
if needle in entry.skill.lookup_keys():
|
|
229
|
+
return entry
|
|
230
|
+
return None
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _load_state(path: Path) -> SkillLifecycleState:
|
|
234
|
+
if not path.exists():
|
|
235
|
+
return SkillLifecycleState()
|
|
236
|
+
try:
|
|
237
|
+
raw_text = path.read_text(encoding="utf-8")
|
|
238
|
+
except OSError as exc:
|
|
239
|
+
raise SkillLifecycleError(f"Failed to read skills state: {path}") from exc
|
|
240
|
+
except UnicodeDecodeError as exc:
|
|
241
|
+
raise SkillLifecycleError(f"Invalid UTF-8 in skills state: {path}") from exc
|
|
242
|
+
try:
|
|
243
|
+
raw = json.loads(raw_text)
|
|
244
|
+
except json.JSONDecodeError as exc:
|
|
245
|
+
raise SkillLifecycleError(f"Invalid JSON in skills state: {path}") from exc
|
|
246
|
+
return _parse_state_payload(raw, path=path)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _load_state_tolerant(path: Path) -> tuple[SkillLifecycleState, tuple[SkillLifecycleIssue, ...]]:
|
|
250
|
+
if not path.exists():
|
|
251
|
+
return SkillLifecycleState(), ()
|
|
252
|
+
try:
|
|
253
|
+
raw_text = path.read_text(encoding="utf-8")
|
|
254
|
+
except OSError:
|
|
255
|
+
return SkillLifecycleState(), (
|
|
256
|
+
SkillLifecycleIssue(
|
|
257
|
+
source_path=path,
|
|
258
|
+
message="Failed to read lifecycle state; using defaults.",
|
|
259
|
+
),
|
|
260
|
+
)
|
|
261
|
+
except UnicodeDecodeError:
|
|
262
|
+
return SkillLifecycleState(), (
|
|
263
|
+
SkillLifecycleIssue(
|
|
264
|
+
source_path=path,
|
|
265
|
+
message="Invalid UTF-8 in lifecycle state; using defaults.",
|
|
266
|
+
),
|
|
267
|
+
)
|
|
268
|
+
try:
|
|
269
|
+
raw = json.loads(raw_text)
|
|
270
|
+
except json.JSONDecodeError:
|
|
271
|
+
return SkillLifecycleState(), (
|
|
272
|
+
SkillLifecycleIssue(
|
|
273
|
+
source_path=path,
|
|
274
|
+
message="Invalid JSON in lifecycle state; using defaults.",
|
|
275
|
+
),
|
|
276
|
+
)
|
|
277
|
+
return _parse_state_payload_tolerant(raw, path=path)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def resolve_managed_bundle_path(*, managed_root: Path, bundle_dir: str) -> Path:
|
|
281
|
+
safe_dir = _validated_bundle_dir_name(bundle_dir)
|
|
282
|
+
resolved_root = managed_root.expanduser().resolve()
|
|
283
|
+
target = (resolved_root / safe_dir).resolve()
|
|
284
|
+
try:
|
|
285
|
+
target.relative_to(resolved_root)
|
|
286
|
+
except ValueError as exc:
|
|
287
|
+
raise SkillLifecycleError(
|
|
288
|
+
f"Managed skill bundle escapes managed root: {bundle_dir!r}"
|
|
289
|
+
) from exc
|
|
290
|
+
return target
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _parse_state_payload(raw: object, *, path: Path) -> SkillLifecycleState:
|
|
294
|
+
if not isinstance(raw, dict):
|
|
295
|
+
raise SkillLifecycleError(f"Invalid skills state payload: {path}")
|
|
296
|
+
installs_raw = raw.get("managed_installs", {})
|
|
297
|
+
if not isinstance(installs_raw, dict):
|
|
298
|
+
raise SkillLifecycleError(f"Invalid managed_installs payload in skills state: {path}")
|
|
299
|
+
installs, _issues = _parse_managed_installs(installs_raw, path=path, tolerant=False)
|
|
300
|
+
return SkillLifecycleState(
|
|
301
|
+
managed_installs=dict(sorted(installs.items(), key=lambda item: item[0])),
|
|
302
|
+
disabled_names=_normalized_name_tuple(raw.get("disabled_names")),
|
|
303
|
+
enabled_names=_normalized_name_tuple(raw.get("enabled_names")),
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _parse_state_payload_tolerant(
|
|
308
|
+
raw: object,
|
|
309
|
+
*,
|
|
310
|
+
path: Path,
|
|
311
|
+
) -> tuple[SkillLifecycleState, tuple[SkillLifecycleIssue, ...]]:
|
|
312
|
+
if not isinstance(raw, dict):
|
|
313
|
+
return SkillLifecycleState(), (
|
|
314
|
+
SkillLifecycleIssue(
|
|
315
|
+
source_path=path,
|
|
316
|
+
message="Invalid lifecycle state payload; using defaults.",
|
|
317
|
+
),
|
|
318
|
+
)
|
|
319
|
+
installs_raw = raw.get("managed_installs", {})
|
|
320
|
+
issues: list[SkillLifecycleIssue] = []
|
|
321
|
+
installs: dict[str, ManagedSkillRecord] = {}
|
|
322
|
+
if not isinstance(installs_raw, dict):
|
|
323
|
+
issues.append(
|
|
324
|
+
SkillLifecycleIssue(
|
|
325
|
+
source_path=path,
|
|
326
|
+
message="Invalid managed_installs payload in lifecycle state; ignoring it.",
|
|
327
|
+
)
|
|
328
|
+
)
|
|
329
|
+
else:
|
|
330
|
+
installs, install_issues = _parse_managed_installs(installs_raw, path=path, tolerant=True)
|
|
331
|
+
issues.extend(install_issues)
|
|
332
|
+
return (
|
|
333
|
+
SkillLifecycleState(
|
|
334
|
+
managed_installs=dict(sorted(installs.items(), key=lambda item: item[0])),
|
|
335
|
+
disabled_names=_normalized_name_tuple(raw.get("disabled_names")),
|
|
336
|
+
enabled_names=_normalized_name_tuple(raw.get("enabled_names")),
|
|
337
|
+
),
|
|
338
|
+
tuple(issues),
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _parse_managed_installs(
|
|
343
|
+
installs_raw: dict[object, object],
|
|
344
|
+
*,
|
|
345
|
+
path: Path,
|
|
346
|
+
tolerant: bool,
|
|
347
|
+
) -> tuple[dict[str, ManagedSkillRecord], tuple[SkillLifecycleIssue, ...]]:
|
|
348
|
+
installs: dict[str, ManagedSkillRecord] = {}
|
|
349
|
+
issues: list[SkillLifecycleIssue] = []
|
|
350
|
+
for key, payload in installs_raw.items():
|
|
351
|
+
if not isinstance(payload, dict):
|
|
352
|
+
if tolerant:
|
|
353
|
+
issues.append(
|
|
354
|
+
SkillLifecycleIssue(
|
|
355
|
+
source_path=path,
|
|
356
|
+
message=f"Skipping invalid managed skill record {key!r}; expected an object.",
|
|
357
|
+
)
|
|
358
|
+
)
|
|
359
|
+
continue
|
|
360
|
+
raise SkillLifecycleError(f"Invalid managed skill record in skills state: {path}")
|
|
361
|
+
try:
|
|
362
|
+
normalized = normalize_skill_name(str(payload.get("name") or key))
|
|
363
|
+
bundle_dir = _validated_bundle_dir_name(payload.get("bundle_dir") or normalized)
|
|
364
|
+
except SkillLifecycleError as exc:
|
|
365
|
+
if tolerant:
|
|
366
|
+
issues.append(
|
|
367
|
+
SkillLifecycleIssue(
|
|
368
|
+
source_path=path,
|
|
369
|
+
message=f"Skipping invalid managed skill record {key!r}: {exc}",
|
|
370
|
+
)
|
|
371
|
+
)
|
|
372
|
+
continue
|
|
373
|
+
raise SkillLifecycleError(f"{exc} ({path})") from exc
|
|
374
|
+
installs[normalized] = ManagedSkillRecord(
|
|
375
|
+
name=str(payload.get("name") or normalized),
|
|
376
|
+
bundle_dir=bundle_dir,
|
|
377
|
+
scope=("project" if str(payload.get("scope") or "") == "project" else "user"),
|
|
378
|
+
source_kind=_optional_source_kind(payload.get("source_kind")),
|
|
379
|
+
source=str(payload.get("source") or "").strip() or None,
|
|
380
|
+
source_subdir=str(payload.get("source_subdir") or "").strip() or None,
|
|
381
|
+
source_commit=str(payload.get("source_commit") or "").strip() or None,
|
|
382
|
+
installed_at=str(payload.get("installed_at") or "").strip() or None,
|
|
383
|
+
family=str(payload.get("family") or "native").strip() or "native",
|
|
384
|
+
)
|
|
385
|
+
return installs, tuple(issues)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _save_state(path: Path, state: SkillLifecycleState) -> None:
|
|
389
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
390
|
+
payload = {
|
|
391
|
+
"version": 1,
|
|
392
|
+
"managed_installs": {
|
|
393
|
+
name: {
|
|
394
|
+
"name": record.name,
|
|
395
|
+
"bundle_dir": record.bundle_dir,
|
|
396
|
+
"scope": record.scope,
|
|
397
|
+
"source_kind": record.source_kind,
|
|
398
|
+
"source": record.source,
|
|
399
|
+
"source_subdir": record.source_subdir,
|
|
400
|
+
"source_commit": record.source_commit,
|
|
401
|
+
"installed_at": record.installed_at,
|
|
402
|
+
"family": record.family,
|
|
403
|
+
}
|
|
404
|
+
for name, record in sorted(state.managed_installs.items(), key=lambda item: item[0])
|
|
405
|
+
},
|
|
406
|
+
"disabled_names": list(state.disabled_names),
|
|
407
|
+
"enabled_names": list(state.enabled_names),
|
|
408
|
+
}
|
|
409
|
+
atomic_write_json(path, payload)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _normalized_name_tuple(raw: object) -> tuple[str, ...]:
|
|
413
|
+
if not isinstance(raw, list):
|
|
414
|
+
return ()
|
|
415
|
+
out: list[str] = []
|
|
416
|
+
seen: set[str] = set()
|
|
417
|
+
for item in raw:
|
|
418
|
+
candidate = str(item or "").strip().casefold()
|
|
419
|
+
if not candidate or candidate in seen:
|
|
420
|
+
continue
|
|
421
|
+
seen.add(candidate)
|
|
422
|
+
out.append(candidate)
|
|
423
|
+
return tuple(sorted(out))
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def _optional_source_kind(raw: object) -> SkillInstallSourceKind | None:
|
|
427
|
+
candidate = str(raw or "").strip().lower()
|
|
428
|
+
if candidate in {"dir", "zip", "git", "scaffold"}:
|
|
429
|
+
return candidate # type: ignore[return-value]
|
|
430
|
+
return None
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _validated_bundle_dir_name(raw: object) -> str:
|
|
434
|
+
candidate = str(raw or "").strip()
|
|
435
|
+
if not candidate:
|
|
436
|
+
raise SkillLifecycleError("Managed skill record is missing bundle_dir.")
|
|
437
|
+
pure = PurePosixPath(candidate.replace("\\", "/"))
|
|
438
|
+
parts = [part for part in pure.parts if part not in {"", "."}]
|
|
439
|
+
if pure.is_absolute() or any(part == ".." for part in parts) or len(parts) != 1:
|
|
440
|
+
raise SkillLifecycleError(f"Managed skill record has invalid bundle_dir: {candidate!r}")
|
|
441
|
+
return parts[0]
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import tempfile
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from contextlib import suppress
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from uuid import uuid4
|
|
10
|
+
|
|
11
|
+
from .state import SkillLifecycleError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def commit_managed_bundle_update(
|
|
15
|
+
*,
|
|
16
|
+
target_path: Path,
|
|
17
|
+
force: bool,
|
|
18
|
+
stage_bundle: Callable[[Path], None],
|
|
19
|
+
persist_state: Callable[[], None],
|
|
20
|
+
) -> None:
|
|
21
|
+
existing_target = target_path.exists() or target_path.is_symlink()
|
|
22
|
+
if existing_target and not force:
|
|
23
|
+
raise SkillLifecycleError(f"Managed skill already exists: {target_path}")
|
|
24
|
+
|
|
25
|
+
target_path.parent.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
backup_path: Path | None = None
|
|
27
|
+
installed_target = False
|
|
28
|
+
with tempfile.TemporaryDirectory(
|
|
29
|
+
prefix=f".{target_path.name}.stage-",
|
|
30
|
+
dir=target_path.parent,
|
|
31
|
+
) as stage_dir:
|
|
32
|
+
staged_path = Path(stage_dir) / target_path.name
|
|
33
|
+
stage_bundle(staged_path)
|
|
34
|
+
if not (staged_path.exists() or staged_path.is_symlink()):
|
|
35
|
+
raise SkillLifecycleError(f"Staged managed skill bundle was not created: {staged_path}")
|
|
36
|
+
try:
|
|
37
|
+
if existing_target:
|
|
38
|
+
backup_path = _backup_path_for(target_path)
|
|
39
|
+
os.replace(target_path, backup_path)
|
|
40
|
+
os.replace(staged_path, target_path)
|
|
41
|
+
installed_target = True
|
|
42
|
+
persist_state()
|
|
43
|
+
except Exception as exc: # noqa: BLE001
|
|
44
|
+
_rollback_managed_bundle_update(
|
|
45
|
+
target_path=target_path,
|
|
46
|
+
backup_path=backup_path,
|
|
47
|
+
installed_target=installed_target,
|
|
48
|
+
cause=exc,
|
|
49
|
+
)
|
|
50
|
+
raise
|
|
51
|
+
else:
|
|
52
|
+
if backup_path is not None:
|
|
53
|
+
_best_effort_remove(backup_path)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def commit_managed_bundle_removal(
|
|
57
|
+
*,
|
|
58
|
+
target_path: Path,
|
|
59
|
+
persist_state: Callable[[], None],
|
|
60
|
+
) -> None:
|
|
61
|
+
if not (target_path.exists() or target_path.is_symlink()):
|
|
62
|
+
persist_state()
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
backup_path = _backup_path_for(target_path)
|
|
66
|
+
try:
|
|
67
|
+
os.replace(target_path, backup_path)
|
|
68
|
+
persist_state()
|
|
69
|
+
except Exception as exc: # noqa: BLE001
|
|
70
|
+
rollback_errors: list[str] = []
|
|
71
|
+
if backup_path.exists() or backup_path.is_symlink():
|
|
72
|
+
try:
|
|
73
|
+
os.replace(backup_path, target_path)
|
|
74
|
+
except Exception as restore_exc: # noqa: BLE001
|
|
75
|
+
rollback_errors.append(f"failed to restore backup: {restore_exc}")
|
|
76
|
+
if rollback_errors:
|
|
77
|
+
raise SkillLifecycleError(
|
|
78
|
+
"Managed skill remove failed and rollback also failed: "
|
|
79
|
+
+ "; ".join(rollback_errors)
|
|
80
|
+
) from exc
|
|
81
|
+
raise
|
|
82
|
+
else:
|
|
83
|
+
_best_effort_remove(backup_path)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _rollback_managed_bundle_update(
|
|
87
|
+
*,
|
|
88
|
+
target_path: Path,
|
|
89
|
+
backup_path: Path | None,
|
|
90
|
+
installed_target: bool,
|
|
91
|
+
cause: Exception,
|
|
92
|
+
) -> None:
|
|
93
|
+
rollback_errors: list[str] = []
|
|
94
|
+
if installed_target and (target_path.exists() or target_path.is_symlink()):
|
|
95
|
+
try:
|
|
96
|
+
_remove_path(target_path)
|
|
97
|
+
except Exception as cleanup_exc: # noqa: BLE001
|
|
98
|
+
rollback_errors.append(f"failed to remove staged bundle: {cleanup_exc}")
|
|
99
|
+
if backup_path is not None and (backup_path.exists() or backup_path.is_symlink()):
|
|
100
|
+
try:
|
|
101
|
+
os.replace(backup_path, target_path)
|
|
102
|
+
except Exception as restore_exc: # noqa: BLE001
|
|
103
|
+
rollback_errors.append(f"failed to restore previous bundle: {restore_exc}")
|
|
104
|
+
if rollback_errors:
|
|
105
|
+
raise SkillLifecycleError(
|
|
106
|
+
"Managed skill update failed and rollback also failed: " + "; ".join(rollback_errors)
|
|
107
|
+
) from cause
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _backup_path_for(target_path: Path) -> Path:
|
|
111
|
+
return target_path.parent / f".{target_path.name}.backup-{uuid4().hex}"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _remove_path(path: Path) -> None:
|
|
115
|
+
if not path.exists() and not path.is_symlink():
|
|
116
|
+
return
|
|
117
|
+
if path.is_dir() and not path.is_symlink():
|
|
118
|
+
shutil.rmtree(path)
|
|
119
|
+
return
|
|
120
|
+
path.unlink()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _best_effort_remove(path: Path) -> None:
|
|
124
|
+
with suppress(Exception): # noqa: BLE001
|
|
125
|
+
_remove_path(path)
|