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,227 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from ..runtime_kind import RuntimeKind, normalize_runtime_kind
|
|
7
|
+
from .discovery import (
|
|
8
|
+
CustomToolCapabilities,
|
|
9
|
+
CustomToolDiscoveryResult,
|
|
10
|
+
CustomToolIssue,
|
|
11
|
+
CustomToolSpec,
|
|
12
|
+
discover_custom_tools,
|
|
13
|
+
)
|
|
14
|
+
from .trust import ProjectToolTrustState, is_project_tool_trusted, load_trust_state
|
|
15
|
+
|
|
16
|
+
_CUSTOM_TOOL_TOP_LEVEL_RUNTIMES = {
|
|
17
|
+
RuntimeKind.INTERACTIVE_CHAT,
|
|
18
|
+
RuntimeKind.ONE_SHOT,
|
|
19
|
+
RuntimeKind.FORGE_EXEC,
|
|
20
|
+
}
|
|
21
|
+
_CUSTOM_TOOL_TOP_LEVEL_RUNTIME_VALUES = frozenset(
|
|
22
|
+
runtime_kind.value for runtime_kind in _CUSTOM_TOOL_TOP_LEVEL_RUNTIMES
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class CustomToolCatalogEntry:
|
|
28
|
+
name: str
|
|
29
|
+
source_scope: str
|
|
30
|
+
trust: str
|
|
31
|
+
status: str
|
|
32
|
+
source_path: Path
|
|
33
|
+
detail: str = ""
|
|
34
|
+
manifest_version: int | None = None
|
|
35
|
+
capabilities: CustomToolCapabilities | None = None
|
|
36
|
+
spec: CustomToolSpec | None = None
|
|
37
|
+
issue: CustomToolIssue | None = None
|
|
38
|
+
exposed: bool = False
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def normalized_name(self) -> str:
|
|
42
|
+
return self.name.casefold()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class CustomToolSessionState:
|
|
47
|
+
discovery: CustomToolDiscoveryResult
|
|
48
|
+
trust_state: ProjectToolTrustState
|
|
49
|
+
catalog_entries: tuple[CustomToolCatalogEntry, ...]
|
|
50
|
+
effective_tools_by_name: dict[str, CustomToolSpec]
|
|
51
|
+
exposed_tools_by_name: dict[str, CustomToolSpec]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def build_custom_tool_session_state(
|
|
55
|
+
*,
|
|
56
|
+
workspace_root: Path,
|
|
57
|
+
custom_tools_enabled: bool,
|
|
58
|
+
mode: str,
|
|
59
|
+
runtime_kind: RuntimeKind | str,
|
|
60
|
+
built_in_tool_names: set[str],
|
|
61
|
+
catalog_view: bool = False,
|
|
62
|
+
write_scope_restricted: bool = False,
|
|
63
|
+
user_config_dir: Path | None = None,
|
|
64
|
+
discovery: CustomToolDiscoveryResult | None = None,
|
|
65
|
+
trust_state: ProjectToolTrustState | None = None,
|
|
66
|
+
) -> CustomToolSessionState:
|
|
67
|
+
resolved_runtime_kind = normalize_runtime_kind(
|
|
68
|
+
runtime_kind, fallback=RuntimeKind.INTERACTIVE_CHAT
|
|
69
|
+
)
|
|
70
|
+
active_discovery = discovery or discover_custom_tools(
|
|
71
|
+
workspace_root=workspace_root,
|
|
72
|
+
built_in_tool_names=built_in_tool_names,
|
|
73
|
+
user_config_dir=user_config_dir,
|
|
74
|
+
)
|
|
75
|
+
active_trust_state = trust_state
|
|
76
|
+
if active_trust_state is None:
|
|
77
|
+
try:
|
|
78
|
+
active_trust_state = load_trust_state(user_config_dir=user_config_dir)
|
|
79
|
+
except RuntimeError:
|
|
80
|
+
active_trust_state = ProjectToolTrustState()
|
|
81
|
+
|
|
82
|
+
effective = active_discovery.effective_tools_by_name()
|
|
83
|
+
exposed: dict[str, CustomToolSpec] = {}
|
|
84
|
+
entries: list[CustomToolCatalogEntry] = []
|
|
85
|
+
shadowed_names = {tool.normalized_name for tool in active_discovery.shadowed_tools}
|
|
86
|
+
|
|
87
|
+
for tool in active_discovery.effective_tools:
|
|
88
|
+
status, trust_label, detail = _tool_status(
|
|
89
|
+
tool=tool,
|
|
90
|
+
custom_tools_enabled=custom_tools_enabled,
|
|
91
|
+
mode=mode,
|
|
92
|
+
runtime_kind=resolved_runtime_kind,
|
|
93
|
+
catalog_view=catalog_view,
|
|
94
|
+
write_scope_restricted=write_scope_restricted,
|
|
95
|
+
trust_state=active_trust_state,
|
|
96
|
+
)
|
|
97
|
+
exposed_flag = status == "available" and not catalog_view
|
|
98
|
+
if exposed_flag:
|
|
99
|
+
exposed[tool.normalized_name] = tool
|
|
100
|
+
entries.append(
|
|
101
|
+
CustomToolCatalogEntry(
|
|
102
|
+
name=tool.name,
|
|
103
|
+
source_scope=tool.source_scope,
|
|
104
|
+
trust=trust_label,
|
|
105
|
+
status=status,
|
|
106
|
+
source_path=tool.source_path,
|
|
107
|
+
detail=detail,
|
|
108
|
+
manifest_version=tool.manifest_version,
|
|
109
|
+
capabilities=tool.capabilities,
|
|
110
|
+
spec=tool,
|
|
111
|
+
exposed=exposed_flag,
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
for tool in active_discovery.shadowed_tools:
|
|
116
|
+
entries.append(
|
|
117
|
+
CustomToolCatalogEntry(
|
|
118
|
+
name=tool.name,
|
|
119
|
+
source_scope=tool.source_scope,
|
|
120
|
+
trust="global" if tool.source_scope == "global" else "persistent",
|
|
121
|
+
status="shadowed",
|
|
122
|
+
source_path=tool.source_path,
|
|
123
|
+
detail="overridden by a project custom tool with the same name",
|
|
124
|
+
manifest_version=tool.manifest_version,
|
|
125
|
+
capabilities=tool.capabilities,
|
|
126
|
+
spec=tool,
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
for issue in active_discovery.issues:
|
|
131
|
+
issue_name = issue.tool_name.strip() or Path(issue.relative_tool_path).stem
|
|
132
|
+
if issue_name.casefold() in shadowed_names:
|
|
133
|
+
detail = issue.message
|
|
134
|
+
else:
|
|
135
|
+
detail = issue.message
|
|
136
|
+
entries.append(
|
|
137
|
+
CustomToolCatalogEntry(
|
|
138
|
+
name=issue_name,
|
|
139
|
+
source_scope=issue.source_scope,
|
|
140
|
+
trust="-",
|
|
141
|
+
status="invalid",
|
|
142
|
+
source_path=issue.source_path,
|
|
143
|
+
detail=detail,
|
|
144
|
+
issue=issue,
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
entries_sorted = tuple(
|
|
149
|
+
sorted(
|
|
150
|
+
entries,
|
|
151
|
+
key=lambda entry: (
|
|
152
|
+
entry.name.casefold(),
|
|
153
|
+
0 if entry.status == "available" else 1,
|
|
154
|
+
0 if entry.source_scope == "project" else 1,
|
|
155
|
+
entry.source_path.as_posix().casefold(),
|
|
156
|
+
),
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
return CustomToolSessionState(
|
|
160
|
+
discovery=active_discovery,
|
|
161
|
+
trust_state=active_trust_state,
|
|
162
|
+
catalog_entries=entries_sorted,
|
|
163
|
+
effective_tools_by_name=effective,
|
|
164
|
+
exposed_tools_by_name=exposed,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _tool_status(
|
|
169
|
+
*,
|
|
170
|
+
tool: CustomToolSpec,
|
|
171
|
+
custom_tools_enabled: bool,
|
|
172
|
+
mode: str,
|
|
173
|
+
runtime_kind: RuntimeKind,
|
|
174
|
+
catalog_view: bool,
|
|
175
|
+
write_scope_restricted: bool,
|
|
176
|
+
trust_state: ProjectToolTrustState,
|
|
177
|
+
) -> tuple[str, str, str]:
|
|
178
|
+
if tool.source_scope == "global":
|
|
179
|
+
trust_label = "global"
|
|
180
|
+
trusted = True
|
|
181
|
+
else:
|
|
182
|
+
trusted = is_project_tool_trusted(tool, state=trust_state)
|
|
183
|
+
trust_label = "persistent" if trusted else "untrusted"
|
|
184
|
+
|
|
185
|
+
if not custom_tools_enabled:
|
|
186
|
+
return "disabled", trust_label, "custom tools are disabled in config"
|
|
187
|
+
if catalog_view:
|
|
188
|
+
if not _tool_enabled_in_any_supported_top_level_runtime(tool):
|
|
189
|
+
return (
|
|
190
|
+
"disabled",
|
|
191
|
+
trust_label,
|
|
192
|
+
"tool manifest does not enable any supported top-level runtime",
|
|
193
|
+
)
|
|
194
|
+
if tool.missing_env:
|
|
195
|
+
missing = ", ".join(tool.missing_env)
|
|
196
|
+
return "missing-env", trust_label, f"missing required env vars: {missing}"
|
|
197
|
+
if not trusted:
|
|
198
|
+
return "untrusted", trust_label, "project tool requires persistent trust"
|
|
199
|
+
return "available", trust_label, ""
|
|
200
|
+
if write_scope_restricted:
|
|
201
|
+
return (
|
|
202
|
+
"disabled",
|
|
203
|
+
trust_label,
|
|
204
|
+
"custom tools are unavailable when write scope is narrowed",
|
|
205
|
+
)
|
|
206
|
+
if not _custom_tools_supported_runtime(mode=mode, runtime_kind=runtime_kind):
|
|
207
|
+
return "disabled", trust_label, "custom tools are unavailable in this runtime or mode"
|
|
208
|
+
if runtime_kind.value not in set(tool.enabled_in):
|
|
209
|
+
return "disabled", trust_label, "tool manifest disables this runtime"
|
|
210
|
+
if tool.missing_env:
|
|
211
|
+
missing = ", ".join(tool.missing_env)
|
|
212
|
+
return "missing-env", trust_label, f"missing required env vars: {missing}"
|
|
213
|
+
if not trusted:
|
|
214
|
+
return "untrusted", trust_label, "project tool requires persistent trust"
|
|
215
|
+
return "available", trust_label, ""
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _custom_tools_supported_runtime(*, mode: str, runtime_kind: RuntimeKind) -> bool:
|
|
219
|
+
if str(mode or "").strip().lower() == "readonly":
|
|
220
|
+
return False
|
|
221
|
+
return runtime_kind in _CUSTOM_TOOL_TOP_LEVEL_RUNTIMES
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _tool_enabled_in_any_supported_top_level_runtime(tool: CustomToolSpec) -> bool:
|
|
225
|
+
return any(
|
|
226
|
+
runtime_kind in _CUSTOM_TOOL_TOP_LEVEL_RUNTIME_VALUES for runtime_kind in tool.enabled_in
|
|
227
|
+
)
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import threading
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from contextlib import contextmanager
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from io import BufferedRandom
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from ..atomic_io import atomic_write_json
|
|
13
|
+
from ..branding import canonical_user_config_dir, env_get
|
|
14
|
+
from .discovery import CustomToolSpec
|
|
15
|
+
|
|
16
|
+
_TRUST_SCHEMA_VERSION = 1
|
|
17
|
+
_TRUST_STATE_LOCK = threading.RLock()
|
|
18
|
+
|
|
19
|
+
if os.name == "nt": # pragma: no cover - exercised on Windows
|
|
20
|
+
import msvcrt
|
|
21
|
+
else: # pragma: no cover - exercised on POSIX
|
|
22
|
+
import fcntl
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class ProjectToolTrustKey:
|
|
27
|
+
workspace_root: str
|
|
28
|
+
relative_tool_path: str
|
|
29
|
+
file_hash: str
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class ProjectToolTrustState:
|
|
34
|
+
trusted_tools: tuple[ProjectToolTrustKey, ...] = ()
|
|
35
|
+
|
|
36
|
+
def contains(self, key: ProjectToolTrustKey) -> bool:
|
|
37
|
+
return key in set(self.trusted_tools)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _config_dir() -> Path:
|
|
41
|
+
override = env_get("ALYSIS_CONFIG_DIR")
|
|
42
|
+
if override:
|
|
43
|
+
return Path(override)
|
|
44
|
+
return canonical_user_config_dir()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def trust_state_path(*, user_config_dir: Path | None = None) -> Path:
|
|
48
|
+
base = user_config_dir.expanduser().resolve() if user_config_dir is not None else _config_dir()
|
|
49
|
+
return base / "custom_tools_trust.json"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def load_trust_state(*, user_config_dir: Path | None = None) -> ProjectToolTrustState:
|
|
53
|
+
path = trust_state_path(user_config_dir=user_config_dir)
|
|
54
|
+
return _load_trust_state_from_path(path)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _load_trust_state_from_path(path: Path) -> ProjectToolTrustState:
|
|
58
|
+
if not path.exists():
|
|
59
|
+
return ProjectToolTrustState()
|
|
60
|
+
try:
|
|
61
|
+
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
62
|
+
except Exception as exc: # noqa: BLE001
|
|
63
|
+
raise RuntimeError(f"Failed to read custom tool trust state: {path}") from exc
|
|
64
|
+
if not isinstance(raw, dict):
|
|
65
|
+
raise RuntimeError("Invalid custom tool trust state format.")
|
|
66
|
+
version = raw.get("schema_version", _TRUST_SCHEMA_VERSION)
|
|
67
|
+
if version != _TRUST_SCHEMA_VERSION:
|
|
68
|
+
raise RuntimeError(f"Unsupported custom tool trust schema version: {version}")
|
|
69
|
+
trusted_raw = raw.get("trusted_tools", [])
|
|
70
|
+
if not isinstance(trusted_raw, list):
|
|
71
|
+
raise RuntimeError("Invalid custom tool trust state: trusted_tools must be an array.")
|
|
72
|
+
trusted: list[ProjectToolTrustKey] = []
|
|
73
|
+
for item in trusted_raw:
|
|
74
|
+
if not isinstance(item, dict):
|
|
75
|
+
continue
|
|
76
|
+
workspace_root = str(item.get("workspace_root") or "").strip()
|
|
77
|
+
relative_tool_path = str(item.get("relative_tool_path") or "").strip()
|
|
78
|
+
file_hash = str(item.get("file_hash") or "").strip()
|
|
79
|
+
if workspace_root and relative_tool_path and file_hash:
|
|
80
|
+
trusted.append(
|
|
81
|
+
ProjectToolTrustKey(
|
|
82
|
+
workspace_root=workspace_root,
|
|
83
|
+
relative_tool_path=relative_tool_path,
|
|
84
|
+
file_hash=file_hash,
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
trusted_sorted = tuple(
|
|
88
|
+
sorted(
|
|
89
|
+
set(trusted),
|
|
90
|
+
key=lambda key: (key.workspace_root, key.relative_tool_path, key.file_hash),
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
return ProjectToolTrustState(trusted_tools=trusted_sorted)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def save_trust_state(
|
|
97
|
+
state: ProjectToolTrustState,
|
|
98
|
+
*,
|
|
99
|
+
user_config_dir: Path | None = None,
|
|
100
|
+
) -> None:
|
|
101
|
+
path = trust_state_path(user_config_dir=user_config_dir)
|
|
102
|
+
with _locked_trust_state(path):
|
|
103
|
+
_save_trust_state_to_path(state, path)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _save_trust_state_to_path(
|
|
107
|
+
state: ProjectToolTrustState,
|
|
108
|
+
path: Path,
|
|
109
|
+
) -> None:
|
|
110
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
111
|
+
payload = {
|
|
112
|
+
"schema_version": _TRUST_SCHEMA_VERSION,
|
|
113
|
+
"trusted_tools": [
|
|
114
|
+
{
|
|
115
|
+
"workspace_root": key.workspace_root,
|
|
116
|
+
"relative_tool_path": key.relative_tool_path,
|
|
117
|
+
"file_hash": key.file_hash,
|
|
118
|
+
}
|
|
119
|
+
for key in state.trusted_tools
|
|
120
|
+
],
|
|
121
|
+
}
|
|
122
|
+
atomic_write_json(path, payload, ensure_ascii=True)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def trust_project_tool(
|
|
126
|
+
spec: CustomToolSpec,
|
|
127
|
+
*,
|
|
128
|
+
user_config_dir: Path | None = None,
|
|
129
|
+
) -> ProjectToolTrustState:
|
|
130
|
+
key = project_tool_trust_key(spec)
|
|
131
|
+
return _mutate_trust_state(
|
|
132
|
+
user_config_dir=user_config_dir,
|
|
133
|
+
mutation=lambda current: ProjectToolTrustState(
|
|
134
|
+
trusted_tools=tuple(
|
|
135
|
+
sorted(
|
|
136
|
+
{*(current.trusted_tools), key},
|
|
137
|
+
key=lambda item: (item.workspace_root, item.relative_tool_path, item.file_hash),
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def untrust_project_tool(
|
|
145
|
+
spec: CustomToolSpec,
|
|
146
|
+
*,
|
|
147
|
+
user_config_dir: Path | None = None,
|
|
148
|
+
) -> ProjectToolTrustState:
|
|
149
|
+
if spec.source_scope != "project":
|
|
150
|
+
raise ValueError("Only project custom tools can be untrusted.")
|
|
151
|
+
workspace_root = os.fspath(spec.source_path.resolve().parents[2])
|
|
152
|
+
relpath = spec.relative_tool_path
|
|
153
|
+
return _mutate_trust_state(
|
|
154
|
+
user_config_dir=user_config_dir,
|
|
155
|
+
mutation=lambda current: ProjectToolTrustState(
|
|
156
|
+
trusted_tools=tuple(
|
|
157
|
+
key
|
|
158
|
+
for key in current.trusted_tools
|
|
159
|
+
if not (key.workspace_root == workspace_root and key.relative_tool_path == relpath)
|
|
160
|
+
)
|
|
161
|
+
),
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def is_project_tool_trusted(
|
|
166
|
+
spec: CustomToolSpec,
|
|
167
|
+
*,
|
|
168
|
+
state: ProjectToolTrustState | None = None,
|
|
169
|
+
user_config_dir: Path | None = None,
|
|
170
|
+
) -> bool:
|
|
171
|
+
if spec.source_scope != "project":
|
|
172
|
+
return True
|
|
173
|
+
active_state = state if state is not None else load_trust_state(user_config_dir=user_config_dir)
|
|
174
|
+
return project_tool_trust_key(spec) in set(active_state.trusted_tools)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def project_tool_trust_key(spec: CustomToolSpec) -> ProjectToolTrustKey:
|
|
178
|
+
if spec.source_scope != "project":
|
|
179
|
+
raise ValueError("Only project custom tools use explicit trust keys.")
|
|
180
|
+
workspace_root = os.fspath(spec.source_path.resolve().parents[2])
|
|
181
|
+
return ProjectToolTrustKey(
|
|
182
|
+
workspace_root=workspace_root,
|
|
183
|
+
relative_tool_path=spec.relative_tool_path,
|
|
184
|
+
file_hash=spec.file_hash,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _mutate_trust_state(
|
|
189
|
+
*,
|
|
190
|
+
user_config_dir: Path | None,
|
|
191
|
+
mutation: Callable[[ProjectToolTrustState], ProjectToolTrustState],
|
|
192
|
+
) -> ProjectToolTrustState:
|
|
193
|
+
path = trust_state_path(user_config_dir=user_config_dir)
|
|
194
|
+
with _locked_trust_state(path):
|
|
195
|
+
current = _load_trust_state_from_path(path)
|
|
196
|
+
updated = mutation(current)
|
|
197
|
+
if updated != current:
|
|
198
|
+
_save_trust_state_to_path(updated, path)
|
|
199
|
+
return updated
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@contextmanager
|
|
203
|
+
def _locked_trust_state(path: Path):
|
|
204
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
205
|
+
lock_path = path.with_name(f"{path.name}.lock")
|
|
206
|
+
with _TRUST_STATE_LOCK:
|
|
207
|
+
with lock_path.open("a+b") as handle:
|
|
208
|
+
_acquire_file_lock(handle)
|
|
209
|
+
try:
|
|
210
|
+
yield
|
|
211
|
+
finally:
|
|
212
|
+
_release_file_lock(handle)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _acquire_file_lock(handle: BufferedRandom) -> None:
|
|
216
|
+
if os.name == "nt": # pragma: no cover - exercised on Windows
|
|
217
|
+
handle.seek(0, os.SEEK_END)
|
|
218
|
+
if handle.tell() == 0:
|
|
219
|
+
handle.write(b"\0")
|
|
220
|
+
handle.flush()
|
|
221
|
+
handle.seek(0)
|
|
222
|
+
msvcrt.locking(handle.fileno(), msvcrt.LK_LOCK, 1)
|
|
223
|
+
return
|
|
224
|
+
fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _release_file_lock(handle: BufferedRandom) -> None:
|
|
228
|
+
if os.name == "nt": # pragma: no cover - exercised on Windows
|
|
229
|
+
handle.seek(0)
|
|
230
|
+
msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, 1)
|
|
231
|
+
return
|
|
232
|
+
fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shlex
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _unquote_path(value: str) -> str:
|
|
7
|
+
raw = value.strip()
|
|
8
|
+
if not raw:
|
|
9
|
+
return ""
|
|
10
|
+
if raw[0] == raw[-1] == '"' and len(raw) >= 2:
|
|
11
|
+
try:
|
|
12
|
+
parsed = shlex.split(raw)
|
|
13
|
+
if parsed:
|
|
14
|
+
return parsed[0]
|
|
15
|
+
except ValueError:
|
|
16
|
+
return raw[1:-1]
|
|
17
|
+
return raw[1:-1]
|
|
18
|
+
return raw
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _normalize_git_path(value: str) -> str:
|
|
22
|
+
raw = _unquote_path(value.strip())
|
|
23
|
+
if raw.startswith("a/") or raw.startswith("b/"):
|
|
24
|
+
return raw[2:]
|
|
25
|
+
return raw
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def parse_diff_git_line(line: str) -> tuple[str | None, str | None]:
|
|
29
|
+
if not line.startswith("diff --git "):
|
|
30
|
+
return None, None
|
|
31
|
+
payload = line[len("diff --git ") :].strip()
|
|
32
|
+
|
|
33
|
+
if payload.startswith("a/"):
|
|
34
|
+
split = payload.rsplit(" b/", 1)
|
|
35
|
+
if len(split) == 2:
|
|
36
|
+
a_raw = split[0]
|
|
37
|
+
b_raw = "b/" + split[1]
|
|
38
|
+
return _normalize_git_path(a_raw), _normalize_git_path(b_raw)
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
parts = shlex.split(payload)
|
|
42
|
+
except ValueError:
|
|
43
|
+
return None, None
|
|
44
|
+
if len(parts) < 2:
|
|
45
|
+
return None, None
|
|
46
|
+
return _normalize_git_path(parts[0]), _normalize_git_path(parts[1])
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _parse_patch_header_path(line: str, prefix: str) -> str | None:
|
|
50
|
+
if not line.startswith(prefix):
|
|
51
|
+
return None
|
|
52
|
+
raw = line[len(prefix) :].split("\t", 1)[0].strip()
|
|
53
|
+
if not raw:
|
|
54
|
+
return None
|
|
55
|
+
path = _normalize_git_path(raw)
|
|
56
|
+
if not path or path == "/dev/null":
|
|
57
|
+
return None
|
|
58
|
+
return path
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def iter_patch_paths(patch_text: str) -> list[str]:
|
|
62
|
+
paths: list[str] = []
|
|
63
|
+
seen: set[str] = set()
|
|
64
|
+
saw_diff_header = False
|
|
65
|
+
|
|
66
|
+
for line in patch_text.splitlines():
|
|
67
|
+
a_path, b_path = parse_diff_git_line(line)
|
|
68
|
+
if a_path is None and b_path is None:
|
|
69
|
+
continue
|
|
70
|
+
saw_diff_header = True
|
|
71
|
+
for path in (a_path, b_path):
|
|
72
|
+
if not path or path == "/dev/null" or path in seen:
|
|
73
|
+
continue
|
|
74
|
+
seen.add(path)
|
|
75
|
+
paths.append(path)
|
|
76
|
+
|
|
77
|
+
if saw_diff_header:
|
|
78
|
+
return paths
|
|
79
|
+
|
|
80
|
+
for line in patch_text.splitlines():
|
|
81
|
+
for prefix in ("--- ", "+++ "):
|
|
82
|
+
path = _parse_patch_header_path(line, prefix)
|
|
83
|
+
if path is None or path in seen:
|
|
84
|
+
continue
|
|
85
|
+
seen.add(path)
|
|
86
|
+
paths.append(path)
|
|
87
|
+
return paths
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def parse_patch_changed_files(patch_text: str) -> list[str]:
|
|
91
|
+
files: list[str] = []
|
|
92
|
+
seen: set[str] = set()
|
|
93
|
+
saw_diff_header = False
|
|
94
|
+
|
|
95
|
+
for line in patch_text.splitlines():
|
|
96
|
+
a_path, b_path = parse_diff_git_line(line)
|
|
97
|
+
if a_path is None and b_path is None:
|
|
98
|
+
continue
|
|
99
|
+
saw_diff_header = True
|
|
100
|
+
if b_path and b_path != "/dev/null" and b_path not in seen:
|
|
101
|
+
seen.add(b_path)
|
|
102
|
+
files.append(b_path)
|
|
103
|
+
|
|
104
|
+
if saw_diff_header:
|
|
105
|
+
return files
|
|
106
|
+
|
|
107
|
+
for line in patch_text.splitlines():
|
|
108
|
+
path = _parse_patch_header_path(line, "+++ ")
|
|
109
|
+
if path is None or path in seen:
|
|
110
|
+
continue
|
|
111
|
+
seen.add(path)
|
|
112
|
+
files.append(path)
|
|
113
|
+
return files
|