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,465 @@
|
|
|
1
|
+
"""Non-fakeable build identity: which commit is actually running.
|
|
2
|
+
|
|
3
|
+
Three behaviourally different builds once all reported themselves as version
|
|
4
|
+
``0.9.8``, and one benchmark campaign turned out to have run against an
|
|
5
|
+
unpinned "latest main". Every score those runs produced is therefore
|
|
6
|
+
unattributable: there is no way, now, to say which source tree earned which
|
|
7
|
+
number. A version string is a promise the developer makes; a commit hash
|
|
8
|
+
recorded at build time is a fact about the artifact.
|
|
9
|
+
|
|
10
|
+
This module owns three things:
|
|
11
|
+
|
|
12
|
+
*The generated fact.* ``scripts/generate_build_info.py`` writes
|
|
13
|
+
``_build_info.py`` next to this file, stamping the commit, an ISO-8601 UTC
|
|
14
|
+
build timestamp, and whether the working tree carried uncommitted changes. The
|
|
15
|
+
copy committed to the repository is a deliberate *dev default* -- no commit,
|
|
16
|
+
marked dirty -- so that an artifact built without running the generator is
|
|
17
|
+
correctly reported as unidentifiable rather than quietly inheriting a stale
|
|
18
|
+
hash.
|
|
19
|
+
|
|
20
|
+
*The reading.* :func:`load_build_info` tolerates the file being absent,
|
|
21
|
+
truncated or hand-edited, because the failure mode that matters is a build
|
|
22
|
+
that cannot prove its identity, and that must surface as a clear refusal
|
|
23
|
+
rather than an import error.
|
|
24
|
+
|
|
25
|
+
*The refusal.* :func:`decide_clean_build` is the policy behind
|
|
26
|
+
``--require-clean-build`` / ``ALYSIS_REQUIRE_CLEAN_BUILD``. Benchmark
|
|
27
|
+
harnesses set it so a campaign cannot start against a build whose provenance
|
|
28
|
+
is unknown -- the precise mistake that cost the earlier campaign its meaning.
|
|
29
|
+
|
|
30
|
+
Stdlib only, no package imports: the generator script runs this in a bare
|
|
31
|
+
interpreter before the package is installable, and the tests load it straight
|
|
32
|
+
from this file path.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import importlib.util
|
|
38
|
+
import json
|
|
39
|
+
import os
|
|
40
|
+
import re
|
|
41
|
+
import subprocess
|
|
42
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
43
|
+
from dataclasses import dataclass
|
|
44
|
+
from datetime import datetime, timezone
|
|
45
|
+
from pathlib import Path
|
|
46
|
+
from typing import Any
|
|
47
|
+
|
|
48
|
+
BUILD_INFO_FILENAME = "_build_info.py"
|
|
49
|
+
BUILD_INFO_SCHEMA_VERSION = 1
|
|
50
|
+
|
|
51
|
+
REQUIRE_CLEAN_BUILD_ENV = "ALYSIS_REQUIRE_CLEAN_BUILD"
|
|
52
|
+
|
|
53
|
+
#: Written into the committed dev default. Any artifact still reporting this
|
|
54
|
+
#: source was built without the release generator.
|
|
55
|
+
DEV_DEFAULT_SOURCE = "dev-default"
|
|
56
|
+
GENERATED_SOURCE = "git"
|
|
57
|
+
|
|
58
|
+
#: The generator rewrites this file immediately before the wheel is built, so
|
|
59
|
+
#: git necessarily sees it as modified. Counting that self-inflicted edit as a
|
|
60
|
+
#: dirty tree would make every release build report itself dirty, so the one
|
|
61
|
+
#: path the generator writes is excluded from the dirty computation.
|
|
62
|
+
DIRTY_CHECK_EXCLUDED_PATHS = ("src/alysis_code/" + BUILD_INFO_FILENAME,)
|
|
63
|
+
|
|
64
|
+
_TRUTHY = frozenset({"1", "true", "yes", "on", "enabled", "require"})
|
|
65
|
+
_COMMIT_RE = re.compile(r"\A[0-9a-f]{7,64}\Z")
|
|
66
|
+
_TIMESTAMP_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
|
|
67
|
+
|
|
68
|
+
_BUILD_INFO_ATTRIBUTES = (
|
|
69
|
+
"BUILD_COMMIT",
|
|
70
|
+
"BUILD_TIMESTAMP",
|
|
71
|
+
"BUILD_DIRTY",
|
|
72
|
+
"BUILD_SOURCE",
|
|
73
|
+
"BUILD_INFO_SCHEMA_VERSION",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
UNKNOWN = "unknown"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
# The record
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass(frozen=True)
|
|
85
|
+
class BuildInfo:
|
|
86
|
+
"""What the artifact can prove about its own provenance."""
|
|
87
|
+
|
|
88
|
+
commit: str = ""
|
|
89
|
+
timestamp: str = ""
|
|
90
|
+
dirty: bool = True
|
|
91
|
+
source: str = DEV_DEFAULT_SOURCE
|
|
92
|
+
schema_version: int = BUILD_INFO_SCHEMA_VERSION
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def commit_short(self) -> str:
|
|
96
|
+
return self.commit[:12] if self.commit else ""
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def is_identifiable(self) -> bool:
|
|
100
|
+
"""True when the recorded commit is a plausible git object name.
|
|
101
|
+
|
|
102
|
+
Shape is checked rather than trusted: a hand-edited ``_build_info.py``
|
|
103
|
+
containing ``BUILD_COMMIT = "probably main"`` must not pass for
|
|
104
|
+
provenance.
|
|
105
|
+
"""
|
|
106
|
+
return bool(_COMMIT_RE.fullmatch(self.commit.strip().casefold()))
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def is_clean(self) -> bool:
|
|
110
|
+
return self.is_identifiable and not self.dirty
|
|
111
|
+
|
|
112
|
+
def describe(self) -> str:
|
|
113
|
+
"""One-line human summary, as printed by ``alysis --version``."""
|
|
114
|
+
return (
|
|
115
|
+
f"commit: {self.commit_short or UNKNOWN}, "
|
|
116
|
+
f"built: {self.timestamp or UNKNOWN}, "
|
|
117
|
+
f"dirty: {'yes' if self.dirty else 'no'}, "
|
|
118
|
+
f"source: {self.source or UNKNOWN}"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
def telemetry_payload(self) -> dict[str, Any]:
|
|
122
|
+
return {
|
|
123
|
+
"schema_version": self.schema_version,
|
|
124
|
+
"commit": self.commit,
|
|
125
|
+
"commit_short": self.commit_short,
|
|
126
|
+
"timestamp": self.timestamp,
|
|
127
|
+
"dirty": self.dirty,
|
|
128
|
+
"source": self.source,
|
|
129
|
+
"identifiable": self.is_identifiable,
|
|
130
|
+
"clean": self.is_clean,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
DEV_DEFAULT_BUILD_INFO = BuildInfo()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def version_line(version: str, info: BuildInfo | None = None) -> str:
|
|
138
|
+
"""Render the ``--version`` line.
|
|
139
|
+
|
|
140
|
+
The bare version stays the leading token and the whole thing stays on one
|
|
141
|
+
line: the VS Code extension, the managed-CLI smoke test and the release
|
|
142
|
+
distribution validator all read this output, and they read it by prefix,
|
|
143
|
+
substring and non-emptiness respectively.
|
|
144
|
+
"""
|
|
145
|
+
resolved = load_build_info() if info is None else info
|
|
146
|
+
return f"{str(version).strip()} ({resolved.describe()})"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# ---------------------------------------------------------------------------
|
|
150
|
+
# Reading the generated file
|
|
151
|
+
# ---------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _coerce_build_info(namespace: Mapping[str, Any]) -> BuildInfo:
|
|
155
|
+
commit = str(namespace.get("BUILD_COMMIT") or "").strip()
|
|
156
|
+
timestamp = str(namespace.get("BUILD_TIMESTAMP") or "").strip()
|
|
157
|
+
source = str(namespace.get("BUILD_SOURCE") or "").strip() or DEV_DEFAULT_SOURCE
|
|
158
|
+
raw_dirty = namespace.get("BUILD_DIRTY", True)
|
|
159
|
+
# Anything unparseable resolves to dirty. Provenance failures must fail
|
|
160
|
+
# closed: a build that cannot say whether it was clean is not clean.
|
|
161
|
+
if isinstance(raw_dirty, bool):
|
|
162
|
+
dirty = raw_dirty
|
|
163
|
+
elif isinstance(raw_dirty, (int, float)):
|
|
164
|
+
dirty = bool(raw_dirty)
|
|
165
|
+
elif isinstance(raw_dirty, str):
|
|
166
|
+
dirty = raw_dirty.strip().casefold() not in {"false", "0", "no", "off"}
|
|
167
|
+
else:
|
|
168
|
+
dirty = True
|
|
169
|
+
try:
|
|
170
|
+
schema_version = int(namespace.get("BUILD_INFO_SCHEMA_VERSION", BUILD_INFO_SCHEMA_VERSION))
|
|
171
|
+
except (TypeError, ValueError):
|
|
172
|
+
schema_version = BUILD_INFO_SCHEMA_VERSION
|
|
173
|
+
return BuildInfo(
|
|
174
|
+
commit=commit,
|
|
175
|
+
timestamp=timestamp,
|
|
176
|
+
dirty=dirty,
|
|
177
|
+
source=source,
|
|
178
|
+
schema_version=schema_version,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _namespace_from_path(path: Path) -> dict[str, Any] | None:
|
|
183
|
+
try:
|
|
184
|
+
if not path.is_file():
|
|
185
|
+
return None
|
|
186
|
+
spec = importlib.util.spec_from_file_location("_alysis_build_info", path)
|
|
187
|
+
if spec is None or spec.loader is None:
|
|
188
|
+
return None
|
|
189
|
+
module = importlib.util.module_from_spec(spec)
|
|
190
|
+
spec.loader.exec_module(module)
|
|
191
|
+
except Exception: # noqa: BLE001 - a corrupt stamp must not break startup
|
|
192
|
+
return None
|
|
193
|
+
return {name: getattr(module, name) for name in _BUILD_INFO_ATTRIBUTES if hasattr(module, name)}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def default_build_info_path() -> Path:
|
|
197
|
+
return Path(__file__).resolve().parent / BUILD_INFO_FILENAME
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def read_build_info(path: str | os.PathLike[str] | None = None) -> BuildInfo:
|
|
201
|
+
"""Read build info from disk, uncached.
|
|
202
|
+
|
|
203
|
+
Prefers the installed package module so a frozen or zipped distribution
|
|
204
|
+
resolves the same way the rest of the package does, and falls back to
|
|
205
|
+
loading the sibling file by path -- which is also what makes this readable
|
|
206
|
+
from a bare interpreter that has never imported the package.
|
|
207
|
+
"""
|
|
208
|
+
if path is None:
|
|
209
|
+
try:
|
|
210
|
+
from . import _build_info as module # noqa: PLC0415
|
|
211
|
+
except ImportError:
|
|
212
|
+
module = None
|
|
213
|
+
except Exception: # noqa: BLE001 - corrupt stamp, fall through to path load
|
|
214
|
+
module = None
|
|
215
|
+
if module is not None:
|
|
216
|
+
namespace = {
|
|
217
|
+
name: getattr(module, name)
|
|
218
|
+
for name in _BUILD_INFO_ATTRIBUTES
|
|
219
|
+
if hasattr(module, name)
|
|
220
|
+
}
|
|
221
|
+
if namespace:
|
|
222
|
+
return _coerce_build_info(namespace)
|
|
223
|
+
path = default_build_info_path()
|
|
224
|
+
namespace = _namespace_from_path(Path(path))
|
|
225
|
+
if namespace is None:
|
|
226
|
+
return DEV_DEFAULT_BUILD_INFO
|
|
227
|
+
return _coerce_build_info(namespace)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
_CACHED_BUILD_INFO: BuildInfo | None = None
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def load_build_info() -> BuildInfo:
|
|
234
|
+
"""Read build info once per process."""
|
|
235
|
+
global _CACHED_BUILD_INFO
|
|
236
|
+
if _CACHED_BUILD_INFO is None:
|
|
237
|
+
_CACHED_BUILD_INFO = read_build_info()
|
|
238
|
+
return _CACHED_BUILD_INFO
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def reset_build_info_cache_for_tests() -> None:
|
|
242
|
+
global _CACHED_BUILD_INFO
|
|
243
|
+
_CACHED_BUILD_INFO = None
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
# ---------------------------------------------------------------------------
|
|
247
|
+
# Generating the file
|
|
248
|
+
# ---------------------------------------------------------------------------
|
|
249
|
+
|
|
250
|
+
GitRunner = Callable[[Sequence[str]], "tuple[int, str]"]
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _run_git(args: Sequence[str], *, cwd: Path) -> tuple[int, str]:
|
|
254
|
+
try:
|
|
255
|
+
completed = subprocess.run( # noqa: S603 - fixed argv, no shell
|
|
256
|
+
["git", *args],
|
|
257
|
+
cwd=str(cwd),
|
|
258
|
+
capture_output=True,
|
|
259
|
+
text=True,
|
|
260
|
+
check=False,
|
|
261
|
+
timeout=30,
|
|
262
|
+
)
|
|
263
|
+
except (OSError, subprocess.SubprocessError):
|
|
264
|
+
return 1, ""
|
|
265
|
+
return completed.returncode, completed.stdout
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def build_timestamp(
|
|
269
|
+
now: datetime | None = None,
|
|
270
|
+
*,
|
|
271
|
+
environ: Mapping[str, str] | None = None,
|
|
272
|
+
) -> str:
|
|
273
|
+
"""ISO-8601 UTC build timestamp, honouring ``SOURCE_DATE_EPOCH``.
|
|
274
|
+
|
|
275
|
+
The reproducible-builds variable is respected so that a deliberately
|
|
276
|
+
reproducible release does not become irreproducible because of this stamp.
|
|
277
|
+
"""
|
|
278
|
+
source: Mapping[str, str] = os.environ if environ is None else environ
|
|
279
|
+
raw_epoch = str(source.get("SOURCE_DATE_EPOCH") or "").strip()
|
|
280
|
+
if raw_epoch:
|
|
281
|
+
try:
|
|
282
|
+
return datetime.fromtimestamp(int(raw_epoch), tz=timezone.utc).strftime(
|
|
283
|
+
_TIMESTAMP_FORMAT
|
|
284
|
+
)
|
|
285
|
+
except (TypeError, ValueError, OSError, OverflowError):
|
|
286
|
+
pass
|
|
287
|
+
moment = datetime.now(timezone.utc) if now is None else now
|
|
288
|
+
if moment.tzinfo is None:
|
|
289
|
+
moment = moment.replace(tzinfo=timezone.utc)
|
|
290
|
+
return moment.astimezone(timezone.utc).strftime(_TIMESTAMP_FORMAT)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def parse_dirty_status(porcelain_output: str) -> bool:
|
|
294
|
+
"""Decide dirtiness from ``git status --porcelain`` output.
|
|
295
|
+
|
|
296
|
+
The generator's own output file is excluded (see
|
|
297
|
+
:data:`DIRTY_CHECK_EXCLUDED_PATHS`); everything else that git reports --
|
|
298
|
+
modified, staged, or untracked-and-not-ignored -- counts, because any of
|
|
299
|
+
them can change what the built artifact actually executes.
|
|
300
|
+
"""
|
|
301
|
+
for line in str(porcelain_output or "").splitlines():
|
|
302
|
+
entry = line.strip()
|
|
303
|
+
if not entry:
|
|
304
|
+
continue
|
|
305
|
+
# Porcelain v1: two status characters, a space, then the path. A rename
|
|
306
|
+
# reads "R old -> new"; the destination is what ships.
|
|
307
|
+
path = entry[2:].strip() if len(entry) > 2 else ""
|
|
308
|
+
if "->" in path:
|
|
309
|
+
path = path.split("->")[-1].strip()
|
|
310
|
+
path = path.strip('"')
|
|
311
|
+
if path and path not in DIRTY_CHECK_EXCLUDED_PATHS:
|
|
312
|
+
return True
|
|
313
|
+
return False
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def generate_build_info(
|
|
317
|
+
*,
|
|
318
|
+
repo_root: str | os.PathLike[str],
|
|
319
|
+
now: datetime | None = None,
|
|
320
|
+
git_runner: GitRunner | None = None,
|
|
321
|
+
environ: Mapping[str, str] | None = None,
|
|
322
|
+
) -> BuildInfo:
|
|
323
|
+
"""Interrogate git and return the build stamp for the current tree.
|
|
324
|
+
|
|
325
|
+
A tree with no git available, or no repository, yields the dev default:
|
|
326
|
+
unidentifiable and dirty. That is the honest answer, and it is the answer
|
|
327
|
+
``--require-clean-build`` is designed to reject.
|
|
328
|
+
"""
|
|
329
|
+
root = Path(repo_root)
|
|
330
|
+
runner: GitRunner = git_runner if git_runner is not None else (lambda a: _run_git(a, cwd=root))
|
|
331
|
+
|
|
332
|
+
code, out = runner(["rev-parse", "HEAD"])
|
|
333
|
+
commit = out.strip().splitlines()[0].strip().casefold() if code == 0 and out.strip() else ""
|
|
334
|
+
if not _COMMIT_RE.fullmatch(commit):
|
|
335
|
+
return BuildInfo(
|
|
336
|
+
commit="",
|
|
337
|
+
timestamp=build_timestamp(now, environ=environ),
|
|
338
|
+
dirty=True,
|
|
339
|
+
source=DEV_DEFAULT_SOURCE,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
status_code, status_out = runner(["status", "--porcelain"])
|
|
343
|
+
# A failed status probe is treated as dirty for the same fail-closed reason
|
|
344
|
+
# as an unparseable stamp: not knowing is not the same as being clean.
|
|
345
|
+
dirty = True if status_code != 0 else parse_dirty_status(status_out)
|
|
346
|
+
return BuildInfo(
|
|
347
|
+
commit=commit,
|
|
348
|
+
timestamp=build_timestamp(now, environ=environ),
|
|
349
|
+
dirty=dirty,
|
|
350
|
+
source=GENERATED_SOURCE,
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def render_build_info_module(info: BuildInfo) -> str:
|
|
355
|
+
"""Render the generated ``_build_info.py`` source text.
|
|
356
|
+
|
|
357
|
+
String literals are emitted via ``json.dumps`` rather than ``repr`` so the
|
|
358
|
+
generated file is double-quoted and survives the repository's
|
|
359
|
+
``ruff format`` check unchanged.
|
|
360
|
+
"""
|
|
361
|
+
return (
|
|
362
|
+
'"""Generated build identity. Do not edit by hand.\n'
|
|
363
|
+
"\n"
|
|
364
|
+
"Regenerate with ``python3 scripts/generate_build_info.py``. The copy\n"
|
|
365
|
+
"committed to the repository is the dev default and deliberately reports an\n"
|
|
366
|
+
"unidentifiable build, so that an artifact built without running the\n"
|
|
367
|
+
"generator is refused by ``--require-clean-build`` instead of inheriting a\n"
|
|
368
|
+
"stale commit hash.\n"
|
|
369
|
+
'"""\n'
|
|
370
|
+
"\n"
|
|
371
|
+
f"BUILD_COMMIT = {json.dumps(info.commit)}\n"
|
|
372
|
+
f"BUILD_TIMESTAMP = {json.dumps(info.timestamp)}\n"
|
|
373
|
+
f"BUILD_DIRTY = {bool(info.dirty)!r}\n"
|
|
374
|
+
f"BUILD_SOURCE = {json.dumps(info.source)}\n"
|
|
375
|
+
f"BUILD_INFO_SCHEMA_VERSION = {int(info.schema_version)!r}\n"
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def write_build_info(info: BuildInfo, path: str | os.PathLike[str]) -> Path:
|
|
380
|
+
target = Path(path)
|
|
381
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
382
|
+
target.write_text(render_build_info_module(info), encoding="utf-8", newline="\n")
|
|
383
|
+
return target
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
# ---------------------------------------------------------------------------
|
|
387
|
+
# The refusal
|
|
388
|
+
# ---------------------------------------------------------------------------
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
@dataclass(frozen=True)
|
|
392
|
+
class CleanBuildDecision:
|
|
393
|
+
"""Whether startup may proceed under ``--require-clean-build``."""
|
|
394
|
+
|
|
395
|
+
required: bool
|
|
396
|
+
allowed: bool
|
|
397
|
+
reason: str
|
|
398
|
+
message: str = ""
|
|
399
|
+
|
|
400
|
+
def payload(self) -> dict[str, Any]:
|
|
401
|
+
return {
|
|
402
|
+
"required": self.required,
|
|
403
|
+
"allowed": self.allowed,
|
|
404
|
+
"reason": self.reason,
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def require_clean_build_requested(
|
|
409
|
+
*,
|
|
410
|
+
environ: Mapping[str, str] | None = None,
|
|
411
|
+
flag: bool | None = None,
|
|
412
|
+
) -> bool:
|
|
413
|
+
"""True when this run must refuse an unidentifiable build.
|
|
414
|
+
|
|
415
|
+
An explicit ``--require-clean-build`` wins over the environment; otherwise
|
|
416
|
+
the harness-set variable decides. An unrecognised value means "not
|
|
417
|
+
requested" rather than an error, so a typo cannot silently *enable* a
|
|
418
|
+
refusal that then looks like a build problem.
|
|
419
|
+
"""
|
|
420
|
+
if flag:
|
|
421
|
+
return True
|
|
422
|
+
source: Mapping[str, str] = os.environ if environ is None else environ
|
|
423
|
+
raw = str(source.get(REQUIRE_CLEAN_BUILD_ENV) or "").strip().casefold()
|
|
424
|
+
return raw in _TRUTHY
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def decide_clean_build(
|
|
428
|
+
*,
|
|
429
|
+
info: BuildInfo | None = None,
|
|
430
|
+
environ: Mapping[str, str] | None = None,
|
|
431
|
+
flag: bool | None = None,
|
|
432
|
+
) -> CleanBuildDecision:
|
|
433
|
+
"""Decide whether a run may start, and say why not in words an operator can act on."""
|
|
434
|
+
resolved = load_build_info() if info is None else info
|
|
435
|
+
required = require_clean_build_requested(environ=environ, flag=flag)
|
|
436
|
+
if not required:
|
|
437
|
+
return CleanBuildDecision(required=False, allowed=True, reason="not_required")
|
|
438
|
+
if not resolved.is_identifiable:
|
|
439
|
+
return CleanBuildDecision(
|
|
440
|
+
required=True,
|
|
441
|
+
allowed=False,
|
|
442
|
+
reason="missing_commit",
|
|
443
|
+
message=(
|
|
444
|
+
"This build cannot identify itself: no commit hash was recorded at "
|
|
445
|
+
f"build time (source: {resolved.source or UNKNOWN}). "
|
|
446
|
+
f"{REQUIRE_CLEAN_BUILD_ENV} is set, so the run is refused rather than "
|
|
447
|
+
"producing results that cannot be attributed to a source tree. "
|
|
448
|
+
"Rebuild after running scripts/generate_build_info.py, or unset "
|
|
449
|
+
f"{REQUIRE_CLEAN_BUILD_ENV} to proceed with an unidentifiable build."
|
|
450
|
+
),
|
|
451
|
+
)
|
|
452
|
+
if resolved.dirty:
|
|
453
|
+
return CleanBuildDecision(
|
|
454
|
+
required=True,
|
|
455
|
+
allowed=False,
|
|
456
|
+
reason="dirty_tree",
|
|
457
|
+
message=(
|
|
458
|
+
f"This build was made from a dirty working tree at commit "
|
|
459
|
+
f"{resolved.commit_short} (built {resolved.timestamp or UNKNOWN}). "
|
|
460
|
+
f"{REQUIRE_CLEAN_BUILD_ENV} is set, so the run is refused: the commit "
|
|
461
|
+
"alone does not describe the code that would run. Commit or stash the "
|
|
462
|
+
f"changes and rebuild, or unset {REQUIRE_CLEAN_BUILD_ENV}."
|
|
463
|
+
),
|
|
464
|
+
)
|
|
465
|
+
return CleanBuildDecision(required=True, allowed=True, reason="clean")
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Windows-native lifecycle notification hook using winsound.
|
|
3
|
+
|
|
4
|
+
Built-in handler shipped with alysis-code; no path discovery needed.
|
|
5
|
+
Handles TurnComplete and Notification events.
|
|
6
|
+
TurnComplete with exit_code 0 plays MB_ICONASTERISK.
|
|
7
|
+
TurnComplete with any other exit_code plays MB_ICONHAND.
|
|
8
|
+
Notification with cause "pre_tool_use_blocked" plays MB_ICONEXCLAMATION.
|
|
9
|
+
Other Notification causes play MB_ICONQUESTION.
|
|
10
|
+
Other hook events exit silently.
|
|
11
|
+
Example .alysis/hooks.json:
|
|
12
|
+
{"hooks": {"TurnComplete": [{"hooks": [{"type": "command",
|
|
13
|
+
"command": "python -m alysis_code.builtin_hooks.notify_done_windows"}]}],
|
|
14
|
+
"Notification": [{"hooks": [{"type": "command",
|
|
15
|
+
"command": "python -m alysis_code.builtin_hooks.notify_done_windows"}]}]}}
|
|
16
|
+
The hook never writes stdout, so dispatcher JSON parsing stays clean.
|
|
17
|
+
On non-Windows or unavailable audio devices it exits 0 silently.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import sys
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
import winsound
|
|
27
|
+
except ImportError:
|
|
28
|
+
winsound = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _read_payload() -> dict:
|
|
32
|
+
try:
|
|
33
|
+
payload = json.loads(sys.stdin.read() or "{}")
|
|
34
|
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
35
|
+
return {}
|
|
36
|
+
return payload if isinstance(payload, dict) else {}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _beep(sound: int) -> None:
|
|
40
|
+
if winsound is None:
|
|
41
|
+
return
|
|
42
|
+
try:
|
|
43
|
+
winsound.MessageBeep(sound)
|
|
44
|
+
except Exception: # noqa: BLE001 - notification hooks must never break a turn.
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def main() -> int:
|
|
49
|
+
if winsound is None:
|
|
50
|
+
return 0
|
|
51
|
+
payload = _read_payload()
|
|
52
|
+
event_name = payload.get("hook_event_name")
|
|
53
|
+
if event_name == "TurnComplete":
|
|
54
|
+
sound = winsound.MB_ICONASTERISK if payload.get("exit_code") == 0 else winsound.MB_ICONHAND
|
|
55
|
+
_beep(sound)
|
|
56
|
+
elif event_name == "Notification":
|
|
57
|
+
if payload.get("cause") == "pre_tool_use_blocked":
|
|
58
|
+
_beep(winsound.MB_ICONEXCLAMATION)
|
|
59
|
+
else:
|
|
60
|
+
_beep(winsound.MB_ICONQUESTION)
|
|
61
|
+
return 0
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
if __name__ == "__main__":
|
|
65
|
+
sys.exit(main())
|
alysis_code/bwrap_etc.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import tempfile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
_VALID_NETWORK = {"off", "on"}
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _write_if_changed(path: Path, content: str) -> None:
|
|
11
|
+
if path.exists():
|
|
12
|
+
try:
|
|
13
|
+
if path.read_text(encoding="utf-8") == content:
|
|
14
|
+
return
|
|
15
|
+
except OSError:
|
|
16
|
+
pass
|
|
17
|
+
path.write_text(content, encoding="utf-8")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _resolve_uid_gid() -> tuple[int, int]:
|
|
21
|
+
uid = 0
|
|
22
|
+
gid = 0
|
|
23
|
+
if hasattr(os, "getuid"):
|
|
24
|
+
try:
|
|
25
|
+
uid = int(os.getuid())
|
|
26
|
+
except OSError:
|
|
27
|
+
uid = 0
|
|
28
|
+
if hasattr(os, "getgid"):
|
|
29
|
+
try:
|
|
30
|
+
gid = int(os.getgid())
|
|
31
|
+
except OSError:
|
|
32
|
+
gid = 0
|
|
33
|
+
return uid, gid
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def ensure_minimal_etc_dir(*, network: str) -> Path:
|
|
37
|
+
mode = str(network).strip().lower()
|
|
38
|
+
if mode not in _VALID_NETWORK:
|
|
39
|
+
raise ValueError(f"Invalid network mode for minimal /etc: {network!r}")
|
|
40
|
+
|
|
41
|
+
root = Path(tempfile.gettempdir()) / "alysis-code" / "bwrap-etc" / mode
|
|
42
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
43
|
+
|
|
44
|
+
uid, gid = _resolve_uid_gid()
|
|
45
|
+
passwd_lines = ["root:x:0:0:root:/tmp/home:/bin/sh"]
|
|
46
|
+
if uid != 0:
|
|
47
|
+
passwd_lines.append(f"user:x:{uid}:{gid}:user:/tmp/home:/bin/sh")
|
|
48
|
+
_write_if_changed(root / "passwd", "\n".join(passwd_lines) + "\n")
|
|
49
|
+
|
|
50
|
+
group_lines = ["root:x:0:"]
|
|
51
|
+
if gid != 0:
|
|
52
|
+
group_lines.append(f"user:x:{gid}:")
|
|
53
|
+
_write_if_changed(root / "group", "\n".join(group_lines) + "\n")
|
|
54
|
+
|
|
55
|
+
_write_if_changed(
|
|
56
|
+
root / "nsswitch.conf",
|
|
57
|
+
"passwd: files\ngroup: files\nhosts: files dns\n",
|
|
58
|
+
)
|
|
59
|
+
_write_if_changed(root / "hosts", "127.0.0.1 localhost\n::1 localhost\n")
|
|
60
|
+
|
|
61
|
+
resolv_path = root / "resolv.conf"
|
|
62
|
+
if mode == "on":
|
|
63
|
+
host_resolv = Path("/etc/resolv.conf")
|
|
64
|
+
try:
|
|
65
|
+
if host_resolv.exists():
|
|
66
|
+
_write_if_changed(resolv_path, host_resolv.read_text(encoding="utf-8"))
|
|
67
|
+
else:
|
|
68
|
+
_write_if_changed(resolv_path, "")
|
|
69
|
+
except OSError:
|
|
70
|
+
_write_if_changed(resolv_path, "")
|
|
71
|
+
else:
|
|
72
|
+
_write_if_changed(resolv_path, "")
|
|
73
|
+
|
|
74
|
+
(root / "ssl").mkdir(parents=True, exist_ok=True)
|
|
75
|
+
(root / "ssl" / "certs").mkdir(parents=True, exist_ok=True)
|
|
76
|
+
return root
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Shared cooperative-cancellation primitives.
|
|
2
|
+
|
|
3
|
+
One token discipline for every long-running execution surface (IDE bridge
|
|
4
|
+
jobs, Forge swarm workers): a ``threading.Event`` is the single cancel
|
|
5
|
+
signal, a token wraps it for duck-typed checkpoints, and cancellation
|
|
6
|
+
surfaces as a dedicated exception type so callers can distinguish a
|
|
7
|
+
cooperative stop from a failure.
|
|
8
|
+
|
|
9
|
+
The agent turn engine (``agent/turn/core.py``) duck-types tokens: it calls
|
|
10
|
+
``throw_if_cancelled()`` when available, otherwise checks ``is_cancelled``.
|
|
11
|
+
Both behaviors are provided here.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import threading
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CooperativeCancellationError(Exception):
|
|
20
|
+
"""Raised at a cooperative checkpoint after cancellation was requested."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, reason: str = "cancelled_by_user") -> None:
|
|
23
|
+
super().__init__(reason)
|
|
24
|
+
self.reason = str(reason or "cancelled_by_user")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class EventCancellationToken:
|
|
28
|
+
"""Duck-typed cancellation token backed by a ``threading.Event``."""
|
|
29
|
+
|
|
30
|
+
error_class: type[CooperativeCancellationError] = CooperativeCancellationError
|
|
31
|
+
|
|
32
|
+
def __init__(self, event: threading.Event) -> None:
|
|
33
|
+
self._event = event
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def is_cancelled(self) -> bool:
|
|
37
|
+
return self._event.is_set()
|
|
38
|
+
|
|
39
|
+
def throw_if_cancelled(self, reason: str = "cancelled_by_user") -> None:
|
|
40
|
+
if self.is_cancelled:
|
|
41
|
+
raise self.error_class(reason)
|