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,1004 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import shlex
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from enum import StrEnum
|
|
9
|
+
from pathlib import Path, PurePosixPath
|
|
10
|
+
|
|
11
|
+
from .config import (
|
|
12
|
+
normalize_verify_module_invocation,
|
|
13
|
+
split_verify_command_parts,
|
|
14
|
+
strip_verify_runner_prefix,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class VerificationCommandEvidentiaryCapability(StrEnum):
|
|
19
|
+
ASSERTIVE = "ASSERTIVE"
|
|
20
|
+
NON_ASSERTIVE = "NON_ASSERTIVE"
|
|
21
|
+
UNKNOWN = "UNKNOWN"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class VerificationCommandStatus(StrEnum):
|
|
25
|
+
PASSED = "passed"
|
|
26
|
+
SKIPPED = "skipped"
|
|
27
|
+
FAILED = "failed"
|
|
28
|
+
INCONCLUSIVE = "inconclusive"
|
|
29
|
+
NOT_EXECUTED = "not_executed"
|
|
30
|
+
STALE = "stale"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class CheckerEntrypointFingerprint:
|
|
35
|
+
display_path: str
|
|
36
|
+
resolved_path: str
|
|
37
|
+
is_regular_file: bool
|
|
38
|
+
size: int | None
|
|
39
|
+
sha256: str | None
|
|
40
|
+
|
|
41
|
+
def as_payload(self) -> dict[str, object]:
|
|
42
|
+
return {
|
|
43
|
+
"display_path": self.display_path,
|
|
44
|
+
"resolved_path": self.resolved_path,
|
|
45
|
+
"is_regular_file": self.is_regular_file,
|
|
46
|
+
"size": self.size,
|
|
47
|
+
"sha256": self.sha256,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class VerificationCommandAnalysis:
|
|
53
|
+
original_command: str
|
|
54
|
+
normalized_command: str
|
|
55
|
+
unwrapped_command: str
|
|
56
|
+
primary_command: str
|
|
57
|
+
canonical_command: str | None
|
|
58
|
+
parts: tuple[str, ...]
|
|
59
|
+
command_family: str | None
|
|
60
|
+
provenance_source: str = ""
|
|
61
|
+
trust_source: str = ""
|
|
62
|
+
evidentiary_capability: VerificationCommandEvidentiaryCapability = (
|
|
63
|
+
VerificationCommandEvidentiaryCapability.UNKNOWN
|
|
64
|
+
)
|
|
65
|
+
capability_reason: str = ""
|
|
66
|
+
shell_control_flow: str = "none"
|
|
67
|
+
pipeline_policy: str = "none"
|
|
68
|
+
checker_entrypoint_paths: tuple[str, ...] = tuple()
|
|
69
|
+
checker_integrity_required: bool = False
|
|
70
|
+
checker_fingerprints: tuple[CheckerEntrypointFingerprint, ...] = tuple()
|
|
71
|
+
real_execution_required: bool = True
|
|
72
|
+
rejection_reason: str = ""
|
|
73
|
+
inconclusive_reason: str = ""
|
|
74
|
+
cd_target: str | None = None
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def is_valid_verifier(self) -> bool:
|
|
78
|
+
return (
|
|
79
|
+
not self.rejection_reason
|
|
80
|
+
and self.evidentiary_capability == VerificationCommandEvidentiaryCapability.ASSERTIVE
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
_ENV_ASSIGNMENT_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*=.*$")
|
|
85
|
+
_SHELL_CONTROL_TOKENS = {"||", "&&", ";", "|", "&"}
|
|
86
|
+
_SHELL_WRAPPER_HEADS = {"bash", "sh", "zsh"}
|
|
87
|
+
_PYTHON_EXECUTABLES = {"python", "python3", "py"}
|
|
88
|
+
_VACUOUS_SUCCESS_HEADS = {"true", ":"}
|
|
89
|
+
_OBSERVATION_HEADS = {
|
|
90
|
+
"cat",
|
|
91
|
+
"echo",
|
|
92
|
+
"find",
|
|
93
|
+
"head",
|
|
94
|
+
"less",
|
|
95
|
+
"ls",
|
|
96
|
+
"more",
|
|
97
|
+
"printf",
|
|
98
|
+
"pwd",
|
|
99
|
+
"tail",
|
|
100
|
+
"tee",
|
|
101
|
+
"type",
|
|
102
|
+
"wc",
|
|
103
|
+
"which",
|
|
104
|
+
}
|
|
105
|
+
_VALIDATION_COMMAND_PATH_MARKERS = {
|
|
106
|
+
"accept",
|
|
107
|
+
"acceptance",
|
|
108
|
+
"check",
|
|
109
|
+
"smoke",
|
|
110
|
+
"test",
|
|
111
|
+
"validate",
|
|
112
|
+
"validation",
|
|
113
|
+
"verify",
|
|
114
|
+
}
|
|
115
|
+
_META_OPTIONS = {"--help", "-h", "--version", "version", "help"}
|
|
116
|
+
_PYTEST_NON_VERIFICATION_OPTIONS = {"--fixtures", "--markers", "--collect-only", "--co"}
|
|
117
|
+
_PYTEST_NON_EXECUTING_OPTIONS = {"--setup-plan"}
|
|
118
|
+
_CARGO_TEST_NON_EXECUTING_OPTIONS = {"--no-run", "--list"}
|
|
119
|
+
_GO_TEST_NON_EXECUTING_OPTIONS = {"-c", "-list"}
|
|
120
|
+
_RUFF_CHECK_NON_EXECUTING_OPTIONS = {"--fix", "--fix-only"}
|
|
121
|
+
_MYPY_NON_EXECUTING_OPTIONS = {"--install-types"}
|
|
122
|
+
_NODE_ASSERTIVE_SCRIPTS = {"test", "build", "lint", "typecheck", "check", "verify"}
|
|
123
|
+
_TEST_SURFACE_PARTS = frozenset(
|
|
124
|
+
{"__tests__", "fixture", "fixtures", "spec", "specs", "test", "tests"}
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
_PYTHON_VERIFICATION_SUFFIXES = frozenset({".py", ".pyi", ".toml", ".ini", ".cfg"})
|
|
128
|
+
_NODE_VERIFICATION_SUFFIXES = frozenset(
|
|
129
|
+
{".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".mts", ".cts", ".json"}
|
|
130
|
+
)
|
|
131
|
+
_GO_VERIFICATION_SUFFIXES = frozenset({".go", ".mod", ".sum"})
|
|
132
|
+
_RUST_VERIFICATION_SUFFIXES = frozenset({".rs", ".toml", ".lock"})
|
|
133
|
+
_JVM_VERIFICATION_SUFFIXES = frozenset(
|
|
134
|
+
{".java", ".kt", ".kts", ".groovy", ".scala", ".xml", ".gradle"}
|
|
135
|
+
)
|
|
136
|
+
_DOTNET_VERIFICATION_SUFFIXES = frozenset({".cs", ".fs", ".vb", ".csproj", ".fsproj", ".sln"})
|
|
137
|
+
_ELIXIR_VERIFICATION_SUFFIXES = frozenset({".ex", ".exs"})
|
|
138
|
+
_ASSERTION_VERIFICATION_SUFFIXES = frozenset(
|
|
139
|
+
{
|
|
140
|
+
*_PYTHON_VERIFICATION_SUFFIXES,
|
|
141
|
+
*_NODE_VERIFICATION_SUFFIXES,
|
|
142
|
+
*_GO_VERIFICATION_SUFFIXES,
|
|
143
|
+
*_RUST_VERIFICATION_SUFFIXES,
|
|
144
|
+
*_JVM_VERIFICATION_SUFFIXES,
|
|
145
|
+
*_DOTNET_VERIFICATION_SUFFIXES,
|
|
146
|
+
*_ELIXIR_VERIFICATION_SUFFIXES,
|
|
147
|
+
}
|
|
148
|
+
)
|
|
149
|
+
_TOOLCHAIN_BASENAMES: dict[str, frozenset[str]] = {
|
|
150
|
+
"python": frozenset({"pyproject.toml", "setup.cfg", "setup.py", "tox.ini"}),
|
|
151
|
+
"node": frozenset({"package.json", "package-lock.json", "pnpm-lock.yaml", "yarn.lock"}),
|
|
152
|
+
"go": frozenset({"go.mod", "go.sum"}),
|
|
153
|
+
"rust": frozenset({"cargo.toml", "cargo.lock"}),
|
|
154
|
+
"jvm": frozenset({"pom.xml", "build.gradle", "build.gradle.kts", "settings.gradle"}),
|
|
155
|
+
"dotnet": frozenset({"global.json", "nuget.config"}),
|
|
156
|
+
"elixir": frozenset({"mix.exs", "mix.lock"}),
|
|
157
|
+
"broad": frozenset({"makefile", "justfile"}),
|
|
158
|
+
}
|
|
159
|
+
_VERIFICATION_FAMILY_TOOLCHAINS: dict[str, str] = {
|
|
160
|
+
"pytest": "python",
|
|
161
|
+
"unittest": "python",
|
|
162
|
+
"mypy": "python",
|
|
163
|
+
"ruff:check": "python",
|
|
164
|
+
"node:test": "node",
|
|
165
|
+
"npm:test": "node",
|
|
166
|
+
"npm:build": "node",
|
|
167
|
+
"npm:lint": "node",
|
|
168
|
+
"npm:typecheck": "node",
|
|
169
|
+
"npm:check": "node",
|
|
170
|
+
"npm:verify": "node",
|
|
171
|
+
"pnpm:test": "node",
|
|
172
|
+
"pnpm:build": "node",
|
|
173
|
+
"pnpm:lint": "node",
|
|
174
|
+
"pnpm:typecheck": "node",
|
|
175
|
+
"pnpm:check": "node",
|
|
176
|
+
"pnpm:verify": "node",
|
|
177
|
+
"yarn:test": "node",
|
|
178
|
+
"yarn:build": "node",
|
|
179
|
+
"yarn:lint": "node",
|
|
180
|
+
"yarn:typecheck": "node",
|
|
181
|
+
"yarn:check": "node",
|
|
182
|
+
"yarn:verify": "node",
|
|
183
|
+
"go:test": "go",
|
|
184
|
+
"cargo:test": "rust",
|
|
185
|
+
"cargo:check": "rust",
|
|
186
|
+
"maven:test": "jvm",
|
|
187
|
+
"maven:verify": "jvm",
|
|
188
|
+
"gradle:test": "jvm",
|
|
189
|
+
"dotnet:test": "dotnet",
|
|
190
|
+
"mix:test": "elixir",
|
|
191
|
+
"make:test": "broad",
|
|
192
|
+
"make:check": "broad",
|
|
193
|
+
"make:verify": "broad",
|
|
194
|
+
"just:test": "broad",
|
|
195
|
+
"just:check": "broad",
|
|
196
|
+
"just:verify": "broad",
|
|
197
|
+
}
|
|
198
|
+
_TOOLCHAIN_SUFFIXES: dict[str, frozenset[str]] = {
|
|
199
|
+
"python": _PYTHON_VERIFICATION_SUFFIXES,
|
|
200
|
+
"node": _NODE_VERIFICATION_SUFFIXES,
|
|
201
|
+
"go": _GO_VERIFICATION_SUFFIXES,
|
|
202
|
+
"rust": _RUST_VERIFICATION_SUFFIXES,
|
|
203
|
+
"jvm": _JVM_VERIFICATION_SUFFIXES,
|
|
204
|
+
"dotnet": _DOTNET_VERIFICATION_SUFFIXES,
|
|
205
|
+
"elixir": _ELIXIR_VERIFICATION_SUFFIXES,
|
|
206
|
+
"broad": _ASSERTION_VERIFICATION_SUFFIXES,
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _normalize_rel_match_path(raw: str, *, strip_trailing_slash: bool = True) -> str:
|
|
211
|
+
cleaned = str(raw).strip().replace("\\", "/")
|
|
212
|
+
while cleaned.startswith("./"):
|
|
213
|
+
cleaned = cleaned[2:]
|
|
214
|
+
if strip_trailing_slash:
|
|
215
|
+
cleaned = cleaned.rstrip("/")
|
|
216
|
+
return cleaned
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def path_matches_verification_toolchain(rel_path: str, toolchain: str) -> bool:
|
|
220
|
+
normalized = _normalize_rel_match_path(rel_path)
|
|
221
|
+
if not normalized:
|
|
222
|
+
return False
|
|
223
|
+
lowered = normalized.casefold()
|
|
224
|
+
path = PurePosixPath(lowered)
|
|
225
|
+
basename = path.name
|
|
226
|
+
if basename in _TOOLCHAIN_BASENAMES.get(toolchain, frozenset()):
|
|
227
|
+
return True
|
|
228
|
+
if toolchain != "broad" and any(part in _TEST_SURFACE_PARTS for part in path.parts[:-1]):
|
|
229
|
+
return True
|
|
230
|
+
return path.suffix in _TOOLCHAIN_SUFFIXES.get(toolchain, frozenset())
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def path_has_known_verification_surface(rel_path: str) -> bool:
|
|
234
|
+
return any(
|
|
235
|
+
path_matches_verification_toolchain(rel_path, toolchain)
|
|
236
|
+
for toolchain in _TOOLCHAIN_SUFFIXES
|
|
237
|
+
if toolchain != "broad"
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def verification_commands_apply_to_paths(
|
|
242
|
+
paths: set[str] | frozenset[str],
|
|
243
|
+
commands: list[str] | tuple[str, ...] | set[str] | None,
|
|
244
|
+
) -> bool:
|
|
245
|
+
if not paths or not commands:
|
|
246
|
+
return False
|
|
247
|
+
toolchains: set[str] = set()
|
|
248
|
+
for command in commands:
|
|
249
|
+
family = analyze_verification_command(str(command), trusted=True).command_family
|
|
250
|
+
if family is None:
|
|
251
|
+
continue
|
|
252
|
+
toolchain = _VERIFICATION_FAMILY_TOOLCHAINS.get(family)
|
|
253
|
+
if toolchain is None and family.startswith("assertion:"):
|
|
254
|
+
toolchain = "broad"
|
|
255
|
+
if toolchain is not None:
|
|
256
|
+
toolchains.add(toolchain)
|
|
257
|
+
return any(
|
|
258
|
+
path_matches_verification_toolchain(path, toolchain)
|
|
259
|
+
for path in paths
|
|
260
|
+
for toolchain in toolchains
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def paths_require_verification(paths: set[str] | frozenset[str]) -> bool:
|
|
265
|
+
if not paths:
|
|
266
|
+
return False
|
|
267
|
+
return any(path_has_known_verification_surface(path) for path in paths)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def analyze_verification_command(
|
|
271
|
+
command: str,
|
|
272
|
+
*,
|
|
273
|
+
source: str = "",
|
|
274
|
+
contract_type: str = "",
|
|
275
|
+
trusted: bool = False,
|
|
276
|
+
workspace_root: Path | None = None,
|
|
277
|
+
capture_checker_fingerprints: bool = False,
|
|
278
|
+
) -> VerificationCommandAnalysis:
|
|
279
|
+
original = str(command or "").strip()
|
|
280
|
+
normalized = _normalize_command(original)
|
|
281
|
+
if not normalized:
|
|
282
|
+
return _analysis(
|
|
283
|
+
original,
|
|
284
|
+
normalized,
|
|
285
|
+
"",
|
|
286
|
+
"",
|
|
287
|
+
rejection_reason="empty_command",
|
|
288
|
+
capability=VerificationCommandEvidentiaryCapability.NON_ASSERTIVE,
|
|
289
|
+
capability_reason="empty_command",
|
|
290
|
+
source=source,
|
|
291
|
+
contract_type=contract_type,
|
|
292
|
+
)
|
|
293
|
+
if "\n" in original or "\r" in original:
|
|
294
|
+
return _analysis(
|
|
295
|
+
original,
|
|
296
|
+
normalized,
|
|
297
|
+
normalized,
|
|
298
|
+
normalized,
|
|
299
|
+
rejection_reason="disallowed_shell_control_flow",
|
|
300
|
+
capability=VerificationCommandEvidentiaryCapability.UNKNOWN,
|
|
301
|
+
capability_reason="multi_line_shell_expression",
|
|
302
|
+
shell_control_flow="unsafe",
|
|
303
|
+
source=source,
|
|
304
|
+
contract_type=contract_type,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
unwrapped = normalized
|
|
308
|
+
while True:
|
|
309
|
+
wrapped = _unwrap_shell_wrapper_command(unwrapped)
|
|
310
|
+
if not wrapped or wrapped == unwrapped:
|
|
311
|
+
break
|
|
312
|
+
unwrapped = wrapped
|
|
313
|
+
|
|
314
|
+
tokens = _shell_tokens(unwrapped)
|
|
315
|
+
if tokens is None:
|
|
316
|
+
return _analysis(
|
|
317
|
+
original,
|
|
318
|
+
normalized,
|
|
319
|
+
unwrapped,
|
|
320
|
+
unwrapped,
|
|
321
|
+
rejection_reason="parse_error",
|
|
322
|
+
capability=VerificationCommandEvidentiaryCapability.UNKNOWN,
|
|
323
|
+
capability_reason="parse_error",
|
|
324
|
+
source=source,
|
|
325
|
+
contract_type=contract_type,
|
|
326
|
+
)
|
|
327
|
+
if not tokens:
|
|
328
|
+
return _analysis(
|
|
329
|
+
original,
|
|
330
|
+
normalized,
|
|
331
|
+
unwrapped,
|
|
332
|
+
unwrapped,
|
|
333
|
+
rejection_reason="empty_command",
|
|
334
|
+
capability=VerificationCommandEvidentiaryCapability.NON_ASSERTIVE,
|
|
335
|
+
capability_reason="empty_command",
|
|
336
|
+
source=source,
|
|
337
|
+
contract_type=contract_type,
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
shell_control_flow = "none"
|
|
341
|
+
pipeline_policy = "none"
|
|
342
|
+
cd_target: str | None = None
|
|
343
|
+
primary_tokens = tokens
|
|
344
|
+
if "|" in tokens:
|
|
345
|
+
pipeline_policy = "rejected_without_pipefail"
|
|
346
|
+
return _analysis(
|
|
347
|
+
original,
|
|
348
|
+
normalized,
|
|
349
|
+
unwrapped,
|
|
350
|
+
_join_tokens(tokens),
|
|
351
|
+
rejection_reason="unsafe_pipeline",
|
|
352
|
+
capability=VerificationCommandEvidentiaryCapability.UNKNOWN,
|
|
353
|
+
capability_reason="pipeline_exit_status_can_mask_upstream_failure",
|
|
354
|
+
shell_control_flow="pipeline",
|
|
355
|
+
pipeline_policy=pipeline_policy,
|
|
356
|
+
source=source,
|
|
357
|
+
contract_type=contract_type,
|
|
358
|
+
)
|
|
359
|
+
if len(tokens) >= 4 and tokens[0] == "cd" and tokens[2] == "&&":
|
|
360
|
+
shell_control_flow = "safe_cd_and"
|
|
361
|
+
cd_target = tokens[1]
|
|
362
|
+
cd_rejection = _cd_target_rejection_reason(cd_target, workspace_root=workspace_root)
|
|
363
|
+
primary_tokens = tokens[3:]
|
|
364
|
+
if cd_rejection:
|
|
365
|
+
return _analysis(
|
|
366
|
+
original,
|
|
367
|
+
normalized,
|
|
368
|
+
unwrapped,
|
|
369
|
+
_join_tokens(primary_tokens),
|
|
370
|
+
rejection_reason=cd_rejection,
|
|
371
|
+
capability=VerificationCommandEvidentiaryCapability.UNKNOWN,
|
|
372
|
+
capability_reason=cd_rejection,
|
|
373
|
+
shell_control_flow=shell_control_flow,
|
|
374
|
+
cd_target=cd_target,
|
|
375
|
+
source=source,
|
|
376
|
+
contract_type=contract_type,
|
|
377
|
+
)
|
|
378
|
+
if any(token in _SHELL_CONTROL_TOKENS for token in primary_tokens):
|
|
379
|
+
reason = "unsafe_pipeline" if "|" in primary_tokens else "disallowed_shell_control_flow"
|
|
380
|
+
return _analysis(
|
|
381
|
+
original,
|
|
382
|
+
normalized,
|
|
383
|
+
unwrapped,
|
|
384
|
+
_join_tokens(primary_tokens),
|
|
385
|
+
rejection_reason=reason,
|
|
386
|
+
capability=VerificationCommandEvidentiaryCapability.UNKNOWN,
|
|
387
|
+
capability_reason=reason,
|
|
388
|
+
shell_control_flow="unsafe",
|
|
389
|
+
pipeline_policy=(
|
|
390
|
+
"rejected_without_pipefail" if reason == "unsafe_pipeline" else "none"
|
|
391
|
+
),
|
|
392
|
+
cd_target=cd_target,
|
|
393
|
+
source=source,
|
|
394
|
+
contract_type=contract_type,
|
|
395
|
+
)
|
|
396
|
+
elif any(token in _SHELL_CONTROL_TOKENS for token in tokens):
|
|
397
|
+
reason = "unsafe_pipeline" if "|" in tokens else "disallowed_shell_control_flow"
|
|
398
|
+
return _analysis(
|
|
399
|
+
original,
|
|
400
|
+
normalized,
|
|
401
|
+
unwrapped,
|
|
402
|
+
_join_tokens(tokens),
|
|
403
|
+
rejection_reason=reason,
|
|
404
|
+
capability=VerificationCommandEvidentiaryCapability.UNKNOWN,
|
|
405
|
+
capability_reason=reason,
|
|
406
|
+
shell_control_flow="unsafe",
|
|
407
|
+
pipeline_policy=(
|
|
408
|
+
"rejected_without_pipefail" if reason == "unsafe_pipeline" else "none"
|
|
409
|
+
),
|
|
410
|
+
source=source,
|
|
411
|
+
contract_type=contract_type,
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
primary = _join_tokens(primary_tokens) if shell_control_flow == "safe_cd_and" else unwrapped
|
|
415
|
+
parts = _canonical_parts(primary)
|
|
416
|
+
if not parts:
|
|
417
|
+
return _analysis(
|
|
418
|
+
original,
|
|
419
|
+
normalized,
|
|
420
|
+
unwrapped,
|
|
421
|
+
primary,
|
|
422
|
+
rejection_reason="unrecognized_command",
|
|
423
|
+
capability=VerificationCommandEvidentiaryCapability.UNKNOWN,
|
|
424
|
+
capability_reason="unrecognized_command",
|
|
425
|
+
shell_control_flow=shell_control_flow,
|
|
426
|
+
cd_target=cd_target,
|
|
427
|
+
source=source,
|
|
428
|
+
contract_type=contract_type,
|
|
429
|
+
)
|
|
430
|
+
canonical = shlex.join(parts)
|
|
431
|
+
rejection = _non_assertive_rejection_reason(parts)
|
|
432
|
+
if rejection:
|
|
433
|
+
return _analysis(
|
|
434
|
+
original,
|
|
435
|
+
normalized,
|
|
436
|
+
unwrapped,
|
|
437
|
+
primary,
|
|
438
|
+
canonical_command=canonical,
|
|
439
|
+
parts=tuple(parts),
|
|
440
|
+
rejection_reason=rejection,
|
|
441
|
+
capability=VerificationCommandEvidentiaryCapability.NON_ASSERTIVE,
|
|
442
|
+
capability_reason=rejection,
|
|
443
|
+
shell_control_flow=shell_control_flow,
|
|
444
|
+
cd_target=cd_target,
|
|
445
|
+
source=source,
|
|
446
|
+
contract_type=contract_type,
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
family = _command_family(parts, trusted=trusted)
|
|
450
|
+
checker_paths = _checker_entrypoint_paths(parts, trusted=trusted)
|
|
451
|
+
fingerprints = (
|
|
452
|
+
tuple(
|
|
453
|
+
_fingerprint_checker_path(path, workspace_root=workspace_root) for path in checker_paths
|
|
454
|
+
)
|
|
455
|
+
if capture_checker_fingerprints
|
|
456
|
+
else tuple()
|
|
457
|
+
)
|
|
458
|
+
if family is not None:
|
|
459
|
+
return _analysis(
|
|
460
|
+
original,
|
|
461
|
+
normalized,
|
|
462
|
+
unwrapped,
|
|
463
|
+
primary,
|
|
464
|
+
canonical_command=canonical,
|
|
465
|
+
parts=tuple(parts),
|
|
466
|
+
family=family,
|
|
467
|
+
capability=VerificationCommandEvidentiaryCapability.ASSERTIVE,
|
|
468
|
+
capability_reason="recognized_assertive_verification_command",
|
|
469
|
+
shell_control_flow=shell_control_flow,
|
|
470
|
+
checker_paths=checker_paths,
|
|
471
|
+
checker_integrity_required=bool(checker_paths),
|
|
472
|
+
checker_fingerprints=fingerprints,
|
|
473
|
+
cd_target=cd_target,
|
|
474
|
+
source=source,
|
|
475
|
+
contract_type=contract_type,
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
inconclusive = _unknown_capability_reason(parts)
|
|
479
|
+
return _analysis(
|
|
480
|
+
original,
|
|
481
|
+
normalized,
|
|
482
|
+
unwrapped,
|
|
483
|
+
primary,
|
|
484
|
+
canonical_command=canonical,
|
|
485
|
+
parts=tuple(parts),
|
|
486
|
+
capability=VerificationCommandEvidentiaryCapability.UNKNOWN,
|
|
487
|
+
capability_reason=inconclusive,
|
|
488
|
+
inconclusive_reason=inconclusive,
|
|
489
|
+
shell_control_flow=shell_control_flow,
|
|
490
|
+
checker_paths=checker_paths,
|
|
491
|
+
checker_integrity_required=bool(checker_paths),
|
|
492
|
+
checker_fingerprints=fingerprints,
|
|
493
|
+
cd_target=cd_target,
|
|
494
|
+
source=source,
|
|
495
|
+
contract_type=contract_type,
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
_BENIGN_NON_EXECUTION_REASONS = frozenset(
|
|
500
|
+
{
|
|
501
|
+
"dotnet_test_zero_tests",
|
|
502
|
+
"go_test_no_test_files",
|
|
503
|
+
"go_test_no_tests_to_run",
|
|
504
|
+
"gradle_test_zero_tests",
|
|
505
|
+
"maven_test_zero_tests",
|
|
506
|
+
"maven_verify_zero_tests",
|
|
507
|
+
"no_applicable_verification",
|
|
508
|
+
"npm_test_zero_tests",
|
|
509
|
+
"pnpm_test_zero_tests",
|
|
510
|
+
"pytest_no_tests_collected",
|
|
511
|
+
"unittest_no_tests_run",
|
|
512
|
+
"verification_nothing_to_do",
|
|
513
|
+
"yarn_test_zero_tests",
|
|
514
|
+
}
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def is_benign_non_execution_reason(reason: str | None) -> bool:
|
|
519
|
+
return str(reason or "").strip() in _BENIGN_NON_EXECUTION_REASONS
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def command_status_from_execution(
|
|
523
|
+
*,
|
|
524
|
+
exit_code: int,
|
|
525
|
+
real_execution: bool | None,
|
|
526
|
+
non_execution_reason: str | None = None,
|
|
527
|
+
) -> VerificationCommandStatus:
|
|
528
|
+
if non_execution_reason == "stale_verification":
|
|
529
|
+
return VerificationCommandStatus.STALE
|
|
530
|
+
if is_benign_non_execution_reason(non_execution_reason):
|
|
531
|
+
return VerificationCommandStatus.SKIPPED
|
|
532
|
+
if exit_code != 0:
|
|
533
|
+
return VerificationCommandStatus.FAILED
|
|
534
|
+
if real_execution is True:
|
|
535
|
+
return VerificationCommandStatus.PASSED
|
|
536
|
+
if real_execution is False:
|
|
537
|
+
return VerificationCommandStatus.NOT_EXECUTED
|
|
538
|
+
return VerificationCommandStatus.INCONCLUSIVE
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def _analysis(
|
|
542
|
+
original: str,
|
|
543
|
+
normalized: str,
|
|
544
|
+
unwrapped: str,
|
|
545
|
+
primary: str,
|
|
546
|
+
*,
|
|
547
|
+
canonical_command: str | None = None,
|
|
548
|
+
parts: tuple[str, ...] = tuple(),
|
|
549
|
+
family: str | None = None,
|
|
550
|
+
capability: VerificationCommandEvidentiaryCapability,
|
|
551
|
+
capability_reason: str,
|
|
552
|
+
shell_control_flow: str = "none",
|
|
553
|
+
pipeline_policy: str = "none",
|
|
554
|
+
checker_paths: tuple[str, ...] = tuple(),
|
|
555
|
+
checker_integrity_required: bool = False,
|
|
556
|
+
checker_fingerprints: tuple[CheckerEntrypointFingerprint, ...] = tuple(),
|
|
557
|
+
rejection_reason: str = "",
|
|
558
|
+
inconclusive_reason: str = "",
|
|
559
|
+
cd_target: str | None = None,
|
|
560
|
+
source: str = "",
|
|
561
|
+
contract_type: str = "",
|
|
562
|
+
) -> VerificationCommandAnalysis:
|
|
563
|
+
return VerificationCommandAnalysis(
|
|
564
|
+
original_command=original,
|
|
565
|
+
normalized_command=normalized,
|
|
566
|
+
unwrapped_command=unwrapped,
|
|
567
|
+
primary_command=primary,
|
|
568
|
+
canonical_command=canonical_command,
|
|
569
|
+
parts=parts,
|
|
570
|
+
command_family=family,
|
|
571
|
+
provenance_source=source,
|
|
572
|
+
trust_source=contract_type,
|
|
573
|
+
evidentiary_capability=capability,
|
|
574
|
+
capability_reason=capability_reason,
|
|
575
|
+
shell_control_flow=shell_control_flow,
|
|
576
|
+
pipeline_policy=pipeline_policy,
|
|
577
|
+
checker_entrypoint_paths=checker_paths,
|
|
578
|
+
checker_integrity_required=checker_integrity_required,
|
|
579
|
+
checker_fingerprints=checker_fingerprints,
|
|
580
|
+
real_execution_required=True,
|
|
581
|
+
rejection_reason=rejection_reason,
|
|
582
|
+
inconclusive_reason=inconclusive_reason,
|
|
583
|
+
cd_target=cd_target,
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def _normalize_command(command: str) -> str:
|
|
588
|
+
return " ".join(str(command or "").strip().split())
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def _shell_tokens(command: str) -> list[str] | None:
|
|
592
|
+
try:
|
|
593
|
+
lexer = shlex.shlex(command, posix=True, punctuation_chars="|&;")
|
|
594
|
+
lexer.whitespace_split = True
|
|
595
|
+
lexer.commenters = ""
|
|
596
|
+
return list(lexer)
|
|
597
|
+
except ValueError:
|
|
598
|
+
return None
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def _join_tokens(tokens: list[str]) -> str:
|
|
602
|
+
return shlex.join(tokens)
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def _unwrap_shell_wrapper_command(command: str) -> str | None:
|
|
606
|
+
parts = split_verify_command_parts(command)
|
|
607
|
+
if not parts:
|
|
608
|
+
return None
|
|
609
|
+
head = _command_head(parts[0])
|
|
610
|
+
if head in _SHELL_WRAPPER_HEADS and len(parts) == 3 and parts[1] in {"-c", "-lc"}:
|
|
611
|
+
return _normalize_command(parts[2])
|
|
612
|
+
if head == "fish" and len(parts) == 3 and parts[1] == "-c":
|
|
613
|
+
return _normalize_command(parts[2])
|
|
614
|
+
if head == "cmd" and len(parts) == 3 and parts[1].casefold() == "/c":
|
|
615
|
+
return _normalize_command(parts[2])
|
|
616
|
+
if head in {"powershell", "pwsh"} and len(parts) == 3 and parts[1].casefold() == "-command":
|
|
617
|
+
return _normalize_command(parts[2])
|
|
618
|
+
return None
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
def _cd_target_rejection_reason(target: str, *, workspace_root: Path | None) -> str:
|
|
622
|
+
if not target or target.startswith("-"):
|
|
623
|
+
return "disallowed_cd_target"
|
|
624
|
+
if workspace_root is None:
|
|
625
|
+
return ""
|
|
626
|
+
if "$" in target or "`" in target:
|
|
627
|
+
return "disallowed_cd_target"
|
|
628
|
+
if target.startswith("/"):
|
|
629
|
+
return ""
|
|
630
|
+
try:
|
|
631
|
+
root = workspace_root.resolve()
|
|
632
|
+
raw = Path(os.path.expanduser(target))
|
|
633
|
+
if raw.is_absolute():
|
|
634
|
+
return ""
|
|
635
|
+
candidate = raw if raw.is_absolute() else root / raw
|
|
636
|
+
candidate.resolve().relative_to(root)
|
|
637
|
+
except (OSError, RuntimeError, ValueError):
|
|
638
|
+
return "disallowed_cd_target"
|
|
639
|
+
return ""
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def _canonical_parts(command: str) -> list[str] | None:
|
|
643
|
+
parts = split_verify_command_parts(command)
|
|
644
|
+
if not parts:
|
|
645
|
+
return None
|
|
646
|
+
while True:
|
|
647
|
+
changed = False
|
|
648
|
+
stripped_env = _strip_env_prefix(parts)
|
|
649
|
+
if stripped_env is None:
|
|
650
|
+
return None
|
|
651
|
+
if stripped_env != parts:
|
|
652
|
+
parts = stripped_env
|
|
653
|
+
changed = True
|
|
654
|
+
stripped_runner = strip_verify_runner_prefix(parts)
|
|
655
|
+
if stripped_runner is None:
|
|
656
|
+
return None
|
|
657
|
+
if stripped_runner != parts:
|
|
658
|
+
parts = stripped_runner
|
|
659
|
+
changed = True
|
|
660
|
+
if parts and _command_head(parts[0]) == "command":
|
|
661
|
+
parts = parts[1:]
|
|
662
|
+
changed = True
|
|
663
|
+
if not parts:
|
|
664
|
+
return None
|
|
665
|
+
if not changed:
|
|
666
|
+
break
|
|
667
|
+
parts = normalize_verify_module_invocation(parts)
|
|
668
|
+
return parts or None
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
def _strip_env_prefix(parts: list[str]) -> list[str] | None:
|
|
672
|
+
out = list(parts)
|
|
673
|
+
if not out:
|
|
674
|
+
return None
|
|
675
|
+
if out[0] == "env":
|
|
676
|
+
out = out[1:]
|
|
677
|
+
if not out or out[0].startswith("-"):
|
|
678
|
+
return None
|
|
679
|
+
while out and _ENV_ASSIGNMENT_RE.match(out[0]):
|
|
680
|
+
out = out[1:]
|
|
681
|
+
return out or None
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def _command_head(token: str) -> str:
|
|
685
|
+
head = Path(str(token).replace("\\", "/")).name.casefold()
|
|
686
|
+
if head.endswith(".exe"):
|
|
687
|
+
head = head[:-4]
|
|
688
|
+
return head
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def _non_assertive_rejection_reason(parts: list[str]) -> str:
|
|
692
|
+
if _is_vacuous_success_command(parts):
|
|
693
|
+
return "vacuous_verifier"
|
|
694
|
+
if _is_non_assertive_observation_command(parts):
|
|
695
|
+
return "non_assertive_observation"
|
|
696
|
+
if _is_non_assertive_python_snippet(parts):
|
|
697
|
+
return "vacuous_verifier"
|
|
698
|
+
if _is_compile_only_python_command(parts):
|
|
699
|
+
return "non_assertive_observation"
|
|
700
|
+
if _is_non_executing_verification_form(parts):
|
|
701
|
+
return "non_assertive_verification_mode"
|
|
702
|
+
return ""
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def _is_vacuous_success_command(parts: list[str]) -> bool:
|
|
706
|
+
if not parts:
|
|
707
|
+
return False
|
|
708
|
+
head = _command_head(parts[0])
|
|
709
|
+
if len(parts) == 1 and (head in _VACUOUS_SUCCESS_HEADS or parts[0].strip() == ":"):
|
|
710
|
+
return True
|
|
711
|
+
if len(parts) == 2 and head in {"exit", "return"} and parts[1] == "0":
|
|
712
|
+
return True
|
|
713
|
+
return _is_non_assertive_python_snippet(parts)
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
def _is_non_assertive_observation_command(parts: list[str]) -> bool:
|
|
717
|
+
if not parts:
|
|
718
|
+
return False
|
|
719
|
+
head = _command_head(parts[0])
|
|
720
|
+
if head in {"grep", "curl"}:
|
|
721
|
+
return False
|
|
722
|
+
return head in _OBSERVATION_HEADS
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
def _is_python_interpreter_snippet(parts: list[str]) -> bool:
|
|
726
|
+
return len(parts) >= 3 and _command_head(parts[0]) in _PYTHON_EXECUTABLES and parts[1] == "-c"
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
def _is_non_assertive_python_snippet(parts: list[str]) -> bool:
|
|
730
|
+
if not _is_python_interpreter_snippet(parts):
|
|
731
|
+
return False
|
|
732
|
+
snippet = " ".join(parts[2:]).strip().casefold()
|
|
733
|
+
compact = "".join(snippet.split())
|
|
734
|
+
if compact in {
|
|
735
|
+
"pass",
|
|
736
|
+
"print('ok')",
|
|
737
|
+
'print("ok")',
|
|
738
|
+
"print(1)",
|
|
739
|
+
"sys.exit(0)",
|
|
740
|
+
"raisesystemexit(0)",
|
|
741
|
+
}:
|
|
742
|
+
return True
|
|
743
|
+
return compact.startswith("importsys;") and compact.endswith("sys.exit(0)")
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
def _is_compile_only_python_command(parts: list[str]) -> bool:
|
|
747
|
+
return (
|
|
748
|
+
len(parts) >= 3
|
|
749
|
+
and _command_head(parts[0]) in _PYTHON_EXECUTABLES
|
|
750
|
+
and parts[1] == "-m"
|
|
751
|
+
and parts[2] in {"compileall", "py_compile"}
|
|
752
|
+
)
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
def _is_non_executing_verification_form(parts: list[str]) -> bool:
|
|
756
|
+
lowered = [part.casefold() for part in parts]
|
|
757
|
+
if not lowered:
|
|
758
|
+
return True
|
|
759
|
+
if any(part in _META_OPTIONS for part in lowered):
|
|
760
|
+
return True
|
|
761
|
+
if lowered[0] in {"pytest", "py.test"}:
|
|
762
|
+
return any(
|
|
763
|
+
part in _PYTEST_NON_VERIFICATION_OPTIONS | _PYTEST_NON_EXECUTING_OPTIONS
|
|
764
|
+
for part in lowered[1:]
|
|
765
|
+
)
|
|
766
|
+
if lowered[:2] == ["go", "test"]:
|
|
767
|
+
if any(
|
|
768
|
+
part in _GO_TEST_NON_EXECUTING_OPTIONS or part.startswith("-list=")
|
|
769
|
+
for part in lowered[2:]
|
|
770
|
+
):
|
|
771
|
+
return True
|
|
772
|
+
for index, part in enumerate(lowered[2:], start=2):
|
|
773
|
+
if part == "-run" and index + 1 < len(lowered) and lowered[index + 1] == "^$":
|
|
774
|
+
return True
|
|
775
|
+
if part == "-run=^$":
|
|
776
|
+
return True
|
|
777
|
+
if lowered[:2] == ["cargo", "test"]:
|
|
778
|
+
return any(part in _CARGO_TEST_NON_EXECUTING_OPTIONS for part in lowered[2:])
|
|
779
|
+
if lowered[:2] == ["ruff", "check"]:
|
|
780
|
+
return any(part in _RUFF_CHECK_NON_EXECUTING_OPTIONS for part in lowered[2:])
|
|
781
|
+
if lowered[0] == "mypy":
|
|
782
|
+
return any(part in _MYPY_NON_EXECUTING_OPTIONS for part in lowered[1:])
|
|
783
|
+
return False
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
def _command_family(parts: list[str], *, trusted: bool) -> str | None:
|
|
787
|
+
if not parts:
|
|
788
|
+
return None
|
|
789
|
+
head = _command_head(parts[0])
|
|
790
|
+
tail = [part.casefold() for part in parts[1:]]
|
|
791
|
+
if head in {"pytest", "py.test"}:
|
|
792
|
+
return "pytest"
|
|
793
|
+
if head == "unittest":
|
|
794
|
+
return "unittest"
|
|
795
|
+
if head == "mypy":
|
|
796
|
+
return "mypy"
|
|
797
|
+
if head == "go" and tail and tail[0] == "test":
|
|
798
|
+
return "go:test"
|
|
799
|
+
if head == "node" and tail and tail[0] == "--test":
|
|
800
|
+
return "node:test"
|
|
801
|
+
if head == "cargo" and tail and tail[0] in {"test", "check"}:
|
|
802
|
+
return f"cargo:{tail[0]}"
|
|
803
|
+
if head in {"mvn", "mvnw", "./mvnw"} and tail and tail[0] in {"test", "verify"}:
|
|
804
|
+
return f"maven:{tail[0]}"
|
|
805
|
+
if head in {"gradle", "gradlew", "./gradlew"} and tail and tail[0] == "test":
|
|
806
|
+
return "gradle:test"
|
|
807
|
+
if head == "dotnet" and tail and tail[0] == "test":
|
|
808
|
+
return "dotnet:test"
|
|
809
|
+
if head == "mix" and tail and tail[0] == "test":
|
|
810
|
+
return "mix:test"
|
|
811
|
+
if head in {"npm", "pnpm", "yarn"}:
|
|
812
|
+
script = _node_assertive_script(parts)
|
|
813
|
+
if script:
|
|
814
|
+
return f"{head}:{script}"
|
|
815
|
+
if head in {"make", "just"} and tail and tail[0] in {"test", "check", "verify"}:
|
|
816
|
+
return f"{head}:{tail[0]}"
|
|
817
|
+
if head == "ruff" and tail and tail[0] == "check":
|
|
818
|
+
return "ruff:check"
|
|
819
|
+
if head in {"test", "["} and len(tail) >= 2:
|
|
820
|
+
return "assertion:test"
|
|
821
|
+
if head in {"cmp", "diff"}:
|
|
822
|
+
operands = [part for part in parts[1:] if part != "--" and not part.startswith("-")]
|
|
823
|
+
if len(operands) >= 2:
|
|
824
|
+
return f"assertion:{head}"
|
|
825
|
+
if head == "grep":
|
|
826
|
+
has_quiet = any(part in {"-q", "--quiet"} for part in tail)
|
|
827
|
+
operands = [part for part in parts[1:] if part != "--" and not part.startswith("-")]
|
|
828
|
+
if has_quiet and len(operands) >= 2:
|
|
829
|
+
return "assertion:grep"
|
|
830
|
+
if head == "curl" and _curl_has_fail_semantics(parts[1:]):
|
|
831
|
+
return "assertion:http_readiness"
|
|
832
|
+
if _is_assertive_python_snippet(parts):
|
|
833
|
+
return "assertion:python_snippet"
|
|
834
|
+
if _repo_local_validation_executable(parts):
|
|
835
|
+
return "assertion:repo_local_executable"
|
|
836
|
+
if _interpreter_validation_script(parts):
|
|
837
|
+
return "assertion:repo_local_script"
|
|
838
|
+
if trusted and _checker_entrypoint_paths(parts, trusted=trusted):
|
|
839
|
+
return "assertion:custom_checker"
|
|
840
|
+
return None
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
def _curl_has_fail_semantics(args: list[str]) -> bool:
|
|
844
|
+
for arg in args:
|
|
845
|
+
if arg == "--fail" or arg == "--fail-with-body":
|
|
846
|
+
return True
|
|
847
|
+
if arg.startswith("--fail-"):
|
|
848
|
+
return True
|
|
849
|
+
if arg.startswith("-") and not arg.startswith("--") and "f" in arg[1:]:
|
|
850
|
+
return True
|
|
851
|
+
return False
|
|
852
|
+
|
|
853
|
+
|
|
854
|
+
def _node_assertive_script(parts: list[str]) -> str | None:
|
|
855
|
+
if not parts:
|
|
856
|
+
return None
|
|
857
|
+
head = _command_head(parts[0])
|
|
858
|
+
idx = 1
|
|
859
|
+
while idx < len(parts):
|
|
860
|
+
token = parts[idx].casefold()
|
|
861
|
+
if token in {"--prefix", "--dir", "--cwd", "-c"} and idx + 1 < len(parts):
|
|
862
|
+
idx += 2
|
|
863
|
+
continue
|
|
864
|
+
if (
|
|
865
|
+
token.startswith("--prefix=")
|
|
866
|
+
or token.startswith("--dir=")
|
|
867
|
+
or token.startswith("--cwd=")
|
|
868
|
+
):
|
|
869
|
+
idx += 1
|
|
870
|
+
continue
|
|
871
|
+
break
|
|
872
|
+
if idx >= len(parts):
|
|
873
|
+
return None
|
|
874
|
+
command = parts[idx].casefold()
|
|
875
|
+
if command == "run" and idx + 1 < len(parts):
|
|
876
|
+
script = parts[idx + 1].casefold()
|
|
877
|
+
elif head == "yarn" and command not in {"run", "exec"}:
|
|
878
|
+
script = command
|
|
879
|
+
else:
|
|
880
|
+
script = command
|
|
881
|
+
if script.startswith("test:"):
|
|
882
|
+
return "test"
|
|
883
|
+
return script if script in _NODE_ASSERTIVE_SCRIPTS else None
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
def _unknown_capability_reason(parts: list[str]) -> str:
|
|
887
|
+
if parts and _command_head(parts[0]) == "curl":
|
|
888
|
+
return "http_probe_requires_curl_fail"
|
|
889
|
+
return "unknown_verification_capability"
|
|
890
|
+
|
|
891
|
+
|
|
892
|
+
def _is_assertive_python_snippet(parts: list[str]) -> bool:
|
|
893
|
+
if not _is_python_interpreter_snippet(parts):
|
|
894
|
+
return False
|
|
895
|
+
snippet = " ".join(parts[2:]).strip()
|
|
896
|
+
compact = snippet.replace(" ", "")
|
|
897
|
+
return "assert " in snippet or ";assert" in compact or compact.startswith("assert")
|
|
898
|
+
|
|
899
|
+
|
|
900
|
+
def _repo_local_validation_executable(parts: list[str]) -> bool:
|
|
901
|
+
if not parts:
|
|
902
|
+
return False
|
|
903
|
+
executable = PurePosixPath(parts[0].replace("\\", "/"))
|
|
904
|
+
if "/" not in parts[0] and not parts[0].startswith("."):
|
|
905
|
+
return False
|
|
906
|
+
markers = {executable.stem.casefold(), *(part.casefold() for part in executable.parts[:-1])}
|
|
907
|
+
return any(marker in value for value in markers for marker in _VALIDATION_COMMAND_PATH_MARKERS)
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
def _interpreter_validation_script(parts: list[str]) -> bool:
|
|
911
|
+
path = _interpreter_script_path(parts)
|
|
912
|
+
if not path:
|
|
913
|
+
return False
|
|
914
|
+
script = PurePosixPath(path.replace("\\", "/"))
|
|
915
|
+
markers = {script.stem.casefold(), *(part.casefold() for part in script.parts[:-1])}
|
|
916
|
+
return any(marker in value for value in markers for marker in _VALIDATION_COMMAND_PATH_MARKERS)
|
|
917
|
+
|
|
918
|
+
|
|
919
|
+
def _checker_entrypoint_paths(parts: list[str], *, trusted: bool) -> tuple[str, ...]:
|
|
920
|
+
if not parts:
|
|
921
|
+
return tuple()
|
|
922
|
+
script = _interpreter_script_path(parts)
|
|
923
|
+
if script:
|
|
924
|
+
return (script,)
|
|
925
|
+
head = parts[0]
|
|
926
|
+
if _is_repo_local_path(head) and ("/" in head.replace("\\", "/") or head.startswith(".")):
|
|
927
|
+
return (head,)
|
|
928
|
+
return tuple()
|
|
929
|
+
|
|
930
|
+
|
|
931
|
+
def _interpreter_script_path(parts: list[str]) -> str | None:
|
|
932
|
+
if not parts:
|
|
933
|
+
return None
|
|
934
|
+
head = _command_head(parts[0])
|
|
935
|
+
if head not in {"python", "python3", "py", "node", "ruby", "rscript", "r", "bash", "sh", "zsh"}:
|
|
936
|
+
return None
|
|
937
|
+
tail = parts[1:]
|
|
938
|
+
if not tail or tail[0].casefold() in _META_OPTIONS:
|
|
939
|
+
return None
|
|
940
|
+
if head in _PYTHON_EXECUTABLES and tail[0] == "-m":
|
|
941
|
+
return None
|
|
942
|
+
for item in tail:
|
|
943
|
+
if item == "--" or item.startswith("-"):
|
|
944
|
+
continue
|
|
945
|
+
if _is_repo_local_path(item):
|
|
946
|
+
return item
|
|
947
|
+
return None
|
|
948
|
+
|
|
949
|
+
|
|
950
|
+
def _is_repo_local_path(path: str) -> bool:
|
|
951
|
+
if not path or path.startswith("-"):
|
|
952
|
+
return False
|
|
953
|
+
pure = PurePosixPath(path.replace("\\", "/"))
|
|
954
|
+
return not pure.is_absolute() and ".." not in pure.parts
|
|
955
|
+
|
|
956
|
+
|
|
957
|
+
def _fingerprint_checker_path(
|
|
958
|
+
display_path: str,
|
|
959
|
+
*,
|
|
960
|
+
workspace_root: Path | None,
|
|
961
|
+
max_bytes: int = 2_000_000,
|
|
962
|
+
) -> CheckerEntrypointFingerprint:
|
|
963
|
+
base = workspace_root.resolve() if workspace_root is not None else Path.cwd().resolve()
|
|
964
|
+
raw = Path(display_path)
|
|
965
|
+
candidate = raw if raw.is_absolute() else base / raw
|
|
966
|
+
try:
|
|
967
|
+
resolved = candidate.resolve(strict=True)
|
|
968
|
+
except OSError:
|
|
969
|
+
return CheckerEntrypointFingerprint(
|
|
970
|
+
display_path=display_path,
|
|
971
|
+
resolved_path=os.fspath(candidate),
|
|
972
|
+
is_regular_file=False,
|
|
973
|
+
size=None,
|
|
974
|
+
sha256=None,
|
|
975
|
+
)
|
|
976
|
+
try:
|
|
977
|
+
stat = resolved.stat()
|
|
978
|
+
except OSError:
|
|
979
|
+
return CheckerEntrypointFingerprint(
|
|
980
|
+
display_path=display_path,
|
|
981
|
+
resolved_path=os.fspath(resolved),
|
|
982
|
+
is_regular_file=False,
|
|
983
|
+
size=None,
|
|
984
|
+
sha256=None,
|
|
985
|
+
)
|
|
986
|
+
if not resolved.is_file() or stat.st_size > max_bytes:
|
|
987
|
+
return CheckerEntrypointFingerprint(
|
|
988
|
+
display_path=display_path,
|
|
989
|
+
resolved_path=os.fspath(resolved),
|
|
990
|
+
is_regular_file=resolved.is_file(),
|
|
991
|
+
size=int(stat.st_size),
|
|
992
|
+
sha256=None,
|
|
993
|
+
)
|
|
994
|
+
digest = hashlib.sha256()
|
|
995
|
+
with resolved.open("rb") as handle:
|
|
996
|
+
for chunk in iter(lambda: handle.read(65536), b""):
|
|
997
|
+
digest.update(chunk)
|
|
998
|
+
return CheckerEntrypointFingerprint(
|
|
999
|
+
display_path=display_path,
|
|
1000
|
+
resolved_path=os.fspath(resolved),
|
|
1001
|
+
is_regular_file=True,
|
|
1002
|
+
size=int(stat.st_size),
|
|
1003
|
+
sha256=digest.hexdigest(),
|
|
1004
|
+
)
|