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,190 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
import subprocess
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .git_ops import (
|
|
9
|
+
GitOpsError,
|
|
10
|
+
branch_exists,
|
|
11
|
+
ensure_git_available,
|
|
12
|
+
ensure_git_repo,
|
|
13
|
+
ensure_runtime_artifact_excludes,
|
|
14
|
+
status_porcelain,
|
|
15
|
+
)
|
|
16
|
+
from .git_safe import build_git_cmd
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class WorktreeInfo:
|
|
21
|
+
path: Path
|
|
22
|
+
branch: str
|
|
23
|
+
base_branch: str
|
|
24
|
+
created_branch: bool
|
|
25
|
+
reused_existing_worktree: bool
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _run_git_checked(
|
|
29
|
+
root: Path,
|
|
30
|
+
args: list[str],
|
|
31
|
+
*,
|
|
32
|
+
error_message: str,
|
|
33
|
+
) -> subprocess.CompletedProcess[str]:
|
|
34
|
+
try:
|
|
35
|
+
cp = subprocess.run(
|
|
36
|
+
build_git_cmd(root, args),
|
|
37
|
+
check=False,
|
|
38
|
+
capture_output=True,
|
|
39
|
+
text=True,
|
|
40
|
+
)
|
|
41
|
+
except OSError as e:
|
|
42
|
+
raise GitOpsError("failed to run git") from e
|
|
43
|
+
if cp.returncode != 0:
|
|
44
|
+
detail = (cp.stderr or cp.stdout).strip()
|
|
45
|
+
raise GitOpsError(f"{error_message}: {detail or 'unknown error'}")
|
|
46
|
+
return cp
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _is_valid_existing_worktree(
|
|
50
|
+
*,
|
|
51
|
+
worktree_repo_path: Path,
|
|
52
|
+
expected_branch: str,
|
|
53
|
+
) -> tuple[bool, str]:
|
|
54
|
+
try:
|
|
55
|
+
inside = subprocess.run(
|
|
56
|
+
build_git_cmd(worktree_repo_path, ["rev-parse", "--is-inside-work-tree"]),
|
|
57
|
+
check=False,
|
|
58
|
+
capture_output=True,
|
|
59
|
+
text=True,
|
|
60
|
+
)
|
|
61
|
+
except OSError as e:
|
|
62
|
+
raise GitOpsError("failed to run git") from e
|
|
63
|
+
if inside.returncode != 0 or inside.stdout.strip().lower() != "true":
|
|
64
|
+
return False, "path exists but is not a valid git worktree"
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
branch_cp = subprocess.run(
|
|
68
|
+
build_git_cmd(worktree_repo_path, ["rev-parse", "--abbrev-ref", "HEAD"]),
|
|
69
|
+
check=False,
|
|
70
|
+
capture_output=True,
|
|
71
|
+
text=True,
|
|
72
|
+
)
|
|
73
|
+
except OSError as e:
|
|
74
|
+
raise GitOpsError("failed to run git") from e
|
|
75
|
+
if branch_cp.returncode != 0:
|
|
76
|
+
return False, "failed to resolve worktree branch"
|
|
77
|
+
current = branch_cp.stdout.strip()
|
|
78
|
+
if current != expected_branch:
|
|
79
|
+
return (
|
|
80
|
+
False,
|
|
81
|
+
f"worktree branch mismatch (expected {expected_branch}, got {current or '(empty)'})",
|
|
82
|
+
)
|
|
83
|
+
ensure_runtime_artifact_excludes(worktree_repo_path)
|
|
84
|
+
status_lines = status_porcelain(worktree_repo_path)
|
|
85
|
+
if status_lines:
|
|
86
|
+
preview = ", ".join(status_lines[:5])
|
|
87
|
+
if len(status_lines) > 5:
|
|
88
|
+
preview += ", ..."
|
|
89
|
+
return False, f"worktree has stale filesystem state ({preview})"
|
|
90
|
+
return True, ""
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _cleanup_existing_worktree_path(*, root: Path, worktree_repo_path: Path) -> None:
|
|
94
|
+
remove_cp = subprocess.run(
|
|
95
|
+
build_git_cmd(root, ["worktree", "remove", str(worktree_repo_path), "--force"]),
|
|
96
|
+
check=False,
|
|
97
|
+
capture_output=True,
|
|
98
|
+
text=True,
|
|
99
|
+
)
|
|
100
|
+
# Best-effort cleanup via git first; path cleanup below handles leftovers.
|
|
101
|
+
_ = remove_cp
|
|
102
|
+
if worktree_repo_path.exists():
|
|
103
|
+
if worktree_repo_path.is_dir():
|
|
104
|
+
shutil.rmtree(worktree_repo_path, ignore_errors=True)
|
|
105
|
+
else:
|
|
106
|
+
try:
|
|
107
|
+
worktree_repo_path.unlink()
|
|
108
|
+
except OSError:
|
|
109
|
+
pass
|
|
110
|
+
if worktree_repo_path.exists():
|
|
111
|
+
raise GitOpsError(f"failed to cleanup existing worktree path {worktree_repo_path}")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def ensure_task_worktree(
|
|
115
|
+
*,
|
|
116
|
+
root: Path,
|
|
117
|
+
worktree_repo_path: Path,
|
|
118
|
+
branch: str,
|
|
119
|
+
base_branch: str,
|
|
120
|
+
) -> WorktreeInfo:
|
|
121
|
+
ensure_git_available()
|
|
122
|
+
ensure_git_repo(root)
|
|
123
|
+
worktree_repo_path = worktree_repo_path.resolve()
|
|
124
|
+
worktree_repo_path.parent.mkdir(parents=True, exist_ok=True)
|
|
125
|
+
|
|
126
|
+
if worktree_repo_path.exists():
|
|
127
|
+
valid, _reason = _is_valid_existing_worktree(
|
|
128
|
+
worktree_repo_path=worktree_repo_path,
|
|
129
|
+
expected_branch=branch,
|
|
130
|
+
)
|
|
131
|
+
if valid:
|
|
132
|
+
return WorktreeInfo(
|
|
133
|
+
path=worktree_repo_path,
|
|
134
|
+
branch=branch,
|
|
135
|
+
base_branch=base_branch,
|
|
136
|
+
created_branch=False,
|
|
137
|
+
reused_existing_worktree=True,
|
|
138
|
+
)
|
|
139
|
+
_cleanup_existing_worktree_path(root=root, worktree_repo_path=worktree_repo_path)
|
|
140
|
+
|
|
141
|
+
if branch_exists(root, branch):
|
|
142
|
+
_run_git_checked(
|
|
143
|
+
root,
|
|
144
|
+
["worktree", "add", str(worktree_repo_path), branch],
|
|
145
|
+
error_message=f"failed to add worktree for branch {branch}",
|
|
146
|
+
)
|
|
147
|
+
return WorktreeInfo(
|
|
148
|
+
path=worktree_repo_path,
|
|
149
|
+
branch=branch,
|
|
150
|
+
base_branch=base_branch,
|
|
151
|
+
created_branch=False,
|
|
152
|
+
reused_existing_worktree=False,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
_run_git_checked(
|
|
156
|
+
root,
|
|
157
|
+
["worktree", "add", "-b", branch, str(worktree_repo_path), base_branch],
|
|
158
|
+
error_message=f"failed to create worktree branch {branch} from {base_branch}",
|
|
159
|
+
)
|
|
160
|
+
return WorktreeInfo(
|
|
161
|
+
path=worktree_repo_path,
|
|
162
|
+
branch=branch,
|
|
163
|
+
base_branch=base_branch,
|
|
164
|
+
created_branch=True,
|
|
165
|
+
reused_existing_worktree=False,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def remove_task_worktree(
|
|
170
|
+
*,
|
|
171
|
+
root: Path,
|
|
172
|
+
worktree_repo_path: Path,
|
|
173
|
+
force: bool = True,
|
|
174
|
+
) -> None:
|
|
175
|
+
args = ["worktree", "remove", str(worktree_repo_path)]
|
|
176
|
+
if force:
|
|
177
|
+
args.append("--force")
|
|
178
|
+
_run_git_checked(
|
|
179
|
+
root,
|
|
180
|
+
args,
|
|
181
|
+
error_message=f"failed to remove worktree {worktree_repo_path}",
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def prune_worktrees(root: Path) -> None:
|
|
186
|
+
_run_git_checked(
|
|
187
|
+
root,
|
|
188
|
+
["worktree", "prune"],
|
|
189
|
+
error_message="failed to prune worktrees",
|
|
190
|
+
)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Lifecycle Hooks
|
|
2
|
+
|
|
3
|
+
This package implements command-based lifecycle hooks around sessions, prompts,
|
|
4
|
+
and tool calls.
|
|
5
|
+
|
|
6
|
+
## Contents
|
|
7
|
+
|
|
8
|
+
- `config.py` loads user, project, and local hook configuration.
|
|
9
|
+
- `models.py` defines hook specs and event payloads.
|
|
10
|
+
- `dispatcher.py` executes hooks and interprets decisions.
|
|
11
|
+
- `trust.py` records trust for project hook configuration.
|
|
12
|
+
- `audit.py` writes hook audit artifacts.
|
|
13
|
+
|
|
14
|
+
## Scope
|
|
15
|
+
|
|
16
|
+
Project hook configuration is executable policy and is not trusted by default.
|
|
17
|
+
Edits to that configuration require a new trust decision.
|
|
18
|
+
|
|
19
|
+
Blocking events can stop prompts or tool calls. Non-blocking events record hook
|
|
20
|
+
output but continue execution. Keep that distinction explicit in code and docs.
|
|
21
|
+
|
|
22
|
+
Hooks receive a filtered environment by default; avoid broadening environment
|
|
23
|
+
inheritance unless the security impact is clear.
|
|
24
|
+
|
|
25
|
+
## Development
|
|
26
|
+
|
|
27
|
+
When changing event payloads or dispatcher behavior, update the hook protocol
|
|
28
|
+
tests and public hook documentation together.
|
|
29
|
+
|
|
30
|
+
## See Also
|
|
31
|
+
|
|
32
|
+
- [Lifecycle hooks](../../../docs/hooks.md)
|
|
33
|
+
- [Security model](../../../docs/security_model.md)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from .audit import (
|
|
2
|
+
HOOK_AUDIT_ARTIFACT_PARTS,
|
|
3
|
+
HookAuditLogger,
|
|
4
|
+
build_hook_audit_event,
|
|
5
|
+
hook_audit_artifact_path,
|
|
6
|
+
hook_audit_log_path,
|
|
7
|
+
read_hook_audit_events,
|
|
8
|
+
)
|
|
9
|
+
from .config import (
|
|
10
|
+
ResolvedHookConfig,
|
|
11
|
+
ResolvedHookMatcherGroup,
|
|
12
|
+
load_hook_config_file,
|
|
13
|
+
load_resolved_hooks_config,
|
|
14
|
+
project_hooks_config_path,
|
|
15
|
+
project_local_hooks_config_path,
|
|
16
|
+
user_hooks_config_path,
|
|
17
|
+
)
|
|
18
|
+
from .dispatcher import HookDispatcher
|
|
19
|
+
from .models import (
|
|
20
|
+
CanonicalHookEventName,
|
|
21
|
+
HookConfigFile,
|
|
22
|
+
HookDispatchResult,
|
|
23
|
+
HookEventName,
|
|
24
|
+
canonicalize_hook_event_name,
|
|
25
|
+
)
|
|
26
|
+
from .trust import (
|
|
27
|
+
ProjectHooksTrustKey,
|
|
28
|
+
ProjectHooksTrustState,
|
|
29
|
+
is_project_hooks_config_trusted,
|
|
30
|
+
load_trust_state,
|
|
31
|
+
project_hooks_trust_key,
|
|
32
|
+
save_trust_state,
|
|
33
|
+
trust_project_hooks_config,
|
|
34
|
+
trust_state_path,
|
|
35
|
+
untrust_project_hooks_config,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"CanonicalHookEventName",
|
|
40
|
+
"HOOK_AUDIT_ARTIFACT_PARTS",
|
|
41
|
+
"HookAuditLogger",
|
|
42
|
+
"HookConfigFile",
|
|
43
|
+
"HookDispatchResult",
|
|
44
|
+
"HookDispatcher",
|
|
45
|
+
"HookEventName",
|
|
46
|
+
"ProjectHooksTrustKey",
|
|
47
|
+
"ProjectHooksTrustState",
|
|
48
|
+
"ResolvedHookConfig",
|
|
49
|
+
"ResolvedHookMatcherGroup",
|
|
50
|
+
"build_hook_audit_event",
|
|
51
|
+
"canonicalize_hook_event_name",
|
|
52
|
+
"hook_audit_artifact_path",
|
|
53
|
+
"hook_audit_log_path",
|
|
54
|
+
"is_project_hooks_config_trusted",
|
|
55
|
+
"load_hook_config_file",
|
|
56
|
+
"load_resolved_hooks_config",
|
|
57
|
+
"load_trust_state",
|
|
58
|
+
"project_hooks_config_path",
|
|
59
|
+
"project_hooks_trust_key",
|
|
60
|
+
"project_local_hooks_config_path",
|
|
61
|
+
"read_hook_audit_events",
|
|
62
|
+
"save_trust_state",
|
|
63
|
+
"trust_project_hooks_config",
|
|
64
|
+
"trust_state_path",
|
|
65
|
+
"untrust_project_hooks_config",
|
|
66
|
+
"user_hooks_config_path",
|
|
67
|
+
]
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from ..error_text import sanitize_error_summary
|
|
10
|
+
from ..session_store import sanitize_session_id
|
|
11
|
+
from .models import HookInvocationContext
|
|
12
|
+
|
|
13
|
+
HOOK_AUDIT_ARTIFACT_PARTS: tuple[str, str] = ("hooks", "hook_runs.jsonl")
|
|
14
|
+
_MAX_WARNING_PREVIEW_ITEMS = 3
|
|
15
|
+
_MAX_WARNING_PREVIEW_CHARS = 160
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _now_ts() -> str:
|
|
19
|
+
return datetime.now(UTC).isoformat()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _truncate_preview(text: str, *, max_chars: int = _MAX_WARNING_PREVIEW_CHARS) -> str:
|
|
23
|
+
return sanitize_error_summary(str(text or ""), max_chars=max_chars)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def hook_audit_log_path(*, sessions_dir: Path, session_id: str) -> Path:
|
|
27
|
+
return (
|
|
28
|
+
sessions_dir
|
|
29
|
+
/ sanitize_session_id(session_id)
|
|
30
|
+
/ HOOK_AUDIT_ARTIFACT_PARTS[0]
|
|
31
|
+
/ HOOK_AUDIT_ARTIFACT_PARTS[1]
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def hook_audit_artifact_path(*, sessions_dir: Path, session_id: str) -> Path:
|
|
36
|
+
return hook_audit_log_path(sessions_dir=sessions_dir, session_id=session_id)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _redacted_hook_payload(payload: dict[str, Any]) -> dict[str, Any]:
|
|
40
|
+
warning_items = payload.get("warnings")
|
|
41
|
+
warning_list = [str(item) for item in warning_items] if isinstance(warning_items, list) else []
|
|
42
|
+
return {
|
|
43
|
+
"event_name": str(payload.get("event_name") or ""),
|
|
44
|
+
"source_path": str(payload.get("source_path") or ""),
|
|
45
|
+
"source_scope": str(payload.get("source_scope") or ""),
|
|
46
|
+
"matcher": str(payload.get("matcher") or ""),
|
|
47
|
+
"hook_id": str(payload.get("hook_id") or ""),
|
|
48
|
+
"priority": int(payload.get("priority") or 0),
|
|
49
|
+
"failure_policy": str(payload.get("failure_policy") or ""),
|
|
50
|
+
"command_preview": _truncate_preview(
|
|
51
|
+
str(payload.get("command") or ""),
|
|
52
|
+
max_chars=180,
|
|
53
|
+
),
|
|
54
|
+
"timeout_s": float(payload.get("timeout_s") or 0.0),
|
|
55
|
+
"trusted": bool(payload.get("trusted")),
|
|
56
|
+
"returncode": payload.get("returncode"),
|
|
57
|
+
"blocked": bool(payload.get("blocked")),
|
|
58
|
+
"modified_input": bool(payload.get("modified_input")),
|
|
59
|
+
"modified_input_fields": [
|
|
60
|
+
str(item)
|
|
61
|
+
for item in (payload.get("modified_input_fields") or [])
|
|
62
|
+
if str(item or "").strip()
|
|
63
|
+
],
|
|
64
|
+
"modified_prompt": bool(payload.get("modified_prompt")),
|
|
65
|
+
"modified_prompt_chars": int(payload.get("modified_prompt_chars") or 0),
|
|
66
|
+
"additional_system_message_count": int(payload.get("additional_system_message_count") or 0),
|
|
67
|
+
"additional_user_message_count": int(payload.get("additional_user_message_count") or 0),
|
|
68
|
+
"system_notices_count": int(payload.get("system_notices_count") or 0),
|
|
69
|
+
"halt_requested": bool(payload.get("halt_requested")),
|
|
70
|
+
"allow_short_circuited": bool(payload.get("allow_short_circuited")),
|
|
71
|
+
"suppress_output": bool(payload.get("suppress_output")),
|
|
72
|
+
"permission_decision": str(payload.get("permission_decision") or ""),
|
|
73
|
+
"stop_reason_preview": _truncate_preview(
|
|
74
|
+
str(payload.get("stop_reason") or ""),
|
|
75
|
+
max_chars=180,
|
|
76
|
+
),
|
|
77
|
+
"stdout_chars": int(payload.get("stdout_chars") or 0),
|
|
78
|
+
"stderr_chars": int(payload.get("stderr_chars") or 0),
|
|
79
|
+
"duration_ms": int(payload.get("duration_ms") or 0),
|
|
80
|
+
"status": str(payload.get("status") or ""),
|
|
81
|
+
"warning_count": len(warning_list),
|
|
82
|
+
"warnings_preview": [
|
|
83
|
+
_truncate_preview(item) for item in warning_list[:_MAX_WARNING_PREVIEW_ITEMS]
|
|
84
|
+
],
|
|
85
|
+
"payload_truncated": bool(payload.get("payload_truncated")),
|
|
86
|
+
"payload_bytes": int(payload.get("payload_bytes") or 0),
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class HookAuditLogger:
|
|
91
|
+
def __init__(self, *, path: Path, session_id: str) -> None:
|
|
92
|
+
self.path = path
|
|
93
|
+
self.session_id = session_id
|
|
94
|
+
|
|
95
|
+
def append(self, event: dict[str, Any]) -> None:
|
|
96
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
97
|
+
with self.path.open("a", encoding="utf-8") as handle:
|
|
98
|
+
handle.write(json.dumps(event, ensure_ascii=True) + "\n")
|
|
99
|
+
|
|
100
|
+
def close(self) -> None:
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def read_hook_audit_events(path: Path) -> Iterable[dict[str, Any]]:
|
|
105
|
+
if not path.exists():
|
|
106
|
+
return
|
|
107
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
108
|
+
line = line.strip()
|
|
109
|
+
if not line:
|
|
110
|
+
continue
|
|
111
|
+
try:
|
|
112
|
+
obj = json.loads(line)
|
|
113
|
+
except json.JSONDecodeError:
|
|
114
|
+
continue
|
|
115
|
+
if isinstance(obj, dict):
|
|
116
|
+
yield obj
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def build_hook_audit_event(
|
|
120
|
+
*,
|
|
121
|
+
session_id: str,
|
|
122
|
+
context: HookInvocationContext,
|
|
123
|
+
) -> dict[str, Any]:
|
|
124
|
+
payload = {
|
|
125
|
+
"event_name": context.event_name,
|
|
126
|
+
"source_path": context.source_path,
|
|
127
|
+
"source_scope": context.source_scope,
|
|
128
|
+
"matcher": context.matcher,
|
|
129
|
+
"hook_id": context.hook_id,
|
|
130
|
+
"priority": context.priority,
|
|
131
|
+
"failure_policy": context.failure_policy,
|
|
132
|
+
"command": context.command,
|
|
133
|
+
"timeout_s": context.timeout_s,
|
|
134
|
+
"trusted": context.trusted,
|
|
135
|
+
"returncode": context.returncode,
|
|
136
|
+
"blocked": context.blocked,
|
|
137
|
+
"modified_input": context.modified_input,
|
|
138
|
+
"modified_input_fields": list(context.modified_input_fields),
|
|
139
|
+
"modified_prompt": context.modified_prompt,
|
|
140
|
+
"modified_prompt_chars": context.modified_prompt_chars,
|
|
141
|
+
"additional_system_message_count": context.additional_system_message_count,
|
|
142
|
+
"additional_user_message_count": context.additional_user_message_count,
|
|
143
|
+
"system_notices_count": context.system_notices_count,
|
|
144
|
+
"halt_requested": context.halt_requested,
|
|
145
|
+
"allow_short_circuited": context.allow_short_circuited,
|
|
146
|
+
"suppress_output": context.suppress_output,
|
|
147
|
+
"permission_decision": context.permission_decision,
|
|
148
|
+
"stop_reason": context.stop_reason,
|
|
149
|
+
"stdout_chars": context.stdout_chars,
|
|
150
|
+
"stderr_chars": context.stderr_chars,
|
|
151
|
+
"duration_ms": context.duration_ms,
|
|
152
|
+
"status": context.status,
|
|
153
|
+
"warnings": list(context.warnings),
|
|
154
|
+
"payload_truncated": context.payload_truncated,
|
|
155
|
+
"payload_bytes": context.payload_bytes,
|
|
156
|
+
}
|
|
157
|
+
redacted = _redacted_hook_payload(payload)
|
|
158
|
+
redacted["type"] = "hook_command"
|
|
159
|
+
redacted["ts"] = _now_ts()
|
|
160
|
+
redacted["session_id"] = session_id
|
|
161
|
+
return redacted
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
__all__ = [
|
|
165
|
+
"HOOK_AUDIT_ARTIFACT_PARTS",
|
|
166
|
+
"HookAuditLogger",
|
|
167
|
+
"build_hook_audit_event",
|
|
168
|
+
"hook_audit_artifact_path",
|
|
169
|
+
"hook_audit_log_path",
|
|
170
|
+
"read_hook_audit_events",
|
|
171
|
+
]
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from pydantic import ValidationError
|
|
9
|
+
|
|
10
|
+
from ..branding import canonical_user_config_dir, env_get
|
|
11
|
+
from ..config import ConfigError
|
|
12
|
+
from .models import (
|
|
13
|
+
CanonicalHookEventName,
|
|
14
|
+
CommandHookSpec,
|
|
15
|
+
HookConfigFile,
|
|
16
|
+
HookEventName,
|
|
17
|
+
canonicalize_hook_event_name,
|
|
18
|
+
)
|
|
19
|
+
from .trust import is_project_hooks_config_trusted, load_trust_state
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _hooks_config_dir() -> Path:
|
|
23
|
+
override = env_get("ALYSIS_CONFIG_DIR")
|
|
24
|
+
if override:
|
|
25
|
+
return Path(override)
|
|
26
|
+
return canonical_user_config_dir()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def user_hooks_config_path() -> Path:
|
|
30
|
+
return _hooks_config_dir() / "hooks.json"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def project_hooks_config_path(workspace_root: Path) -> Path:
|
|
34
|
+
return workspace_root.resolve() / ".alysis" / "hooks.json"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def project_local_hooks_config_path(workspace_root: Path) -> Path:
|
|
38
|
+
return workspace_root.resolve() / ".alysis" / "hooks.local.json"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _read_json_object(path: Path) -> dict[str, Any]:
|
|
42
|
+
try:
|
|
43
|
+
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
44
|
+
except (OSError, json.JSONDecodeError, UnicodeDecodeError) as exc:
|
|
45
|
+
raise ConfigError(f"Failed to read hooks config: {path}") from exc
|
|
46
|
+
if not isinstance(raw, dict):
|
|
47
|
+
raise ConfigError(f"Invalid hooks config format (expected JSON object): {path}")
|
|
48
|
+
return raw
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _format_validation_error(path: Path, exc: ValidationError) -> ConfigError:
|
|
52
|
+
details: list[str] = []
|
|
53
|
+
for err in exc.errors(include_url=False):
|
|
54
|
+
loc = ".".join(str(part) for part in err.get("loc", ()))
|
|
55
|
+
msg = str(err.get("msg") or "invalid value")
|
|
56
|
+
detail = f"{path}: {msg}" if not loc else f"{path}: {loc}: {msg}"
|
|
57
|
+
details.append(detail)
|
|
58
|
+
joined = "\n".join(details) if details else f"{path}: invalid hooks config"
|
|
59
|
+
return ConfigError(f"Invalid hooks config:\n{joined}")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _load_hook_config_file(path: Path) -> HookConfigFile:
|
|
63
|
+
raw = _read_json_object(path)
|
|
64
|
+
try:
|
|
65
|
+
return HookConfigFile.model_validate(raw)
|
|
66
|
+
except ValidationError as exc:
|
|
67
|
+
raise _format_validation_error(path, exc) from exc
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def load_hook_config_file(path: Path) -> HookConfigFile:
|
|
71
|
+
return _load_hook_config_file(path)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(frozen=True)
|
|
75
|
+
class ResolvedHookMatcherGroup:
|
|
76
|
+
event_name: CanonicalHookEventName
|
|
77
|
+
matcher: str
|
|
78
|
+
hooks: tuple[CommandHookSpec, ...]
|
|
79
|
+
source_path: Path
|
|
80
|
+
source_scope: str
|
|
81
|
+
trusted: bool = True
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass(frozen=True)
|
|
85
|
+
class ResolvedHookConfig:
|
|
86
|
+
groups_by_event: dict[str, tuple[ResolvedHookMatcherGroup, ...]] = field(default_factory=dict)
|
|
87
|
+
loaded_paths: tuple[Path, ...] = ()
|
|
88
|
+
untrusted_project_paths: tuple[Path, ...] = ()
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def has_any_hooks(self) -> bool:
|
|
92
|
+
return any(groups for groups in self.groups_by_event.values())
|
|
93
|
+
|
|
94
|
+
def groups_for_event(self, event_name: HookEventName) -> tuple[ResolvedHookMatcherGroup, ...]:
|
|
95
|
+
return self.groups_by_event.get(canonicalize_hook_event_name(event_name), ())
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _resolve_groups_for_file(
|
|
99
|
+
*,
|
|
100
|
+
config_file: HookConfigFile,
|
|
101
|
+
source_path: Path,
|
|
102
|
+
source_scope: str,
|
|
103
|
+
trusted: bool,
|
|
104
|
+
) -> dict[str, list[ResolvedHookMatcherGroup]]:
|
|
105
|
+
resolved: dict[str, list[ResolvedHookMatcherGroup]] = {}
|
|
106
|
+
for event_name, groups in config_file.hooks.items():
|
|
107
|
+
event_groups: list[ResolvedHookMatcherGroup] = []
|
|
108
|
+
for group in groups:
|
|
109
|
+
if not group.enabled:
|
|
110
|
+
continue
|
|
111
|
+
enabled_hooks = tuple(
|
|
112
|
+
sorted(
|
|
113
|
+
(hook for hook in group.hooks if hook.enabled),
|
|
114
|
+
key=lambda hook: (-hook.priority, hook.id or "", hook.command),
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
if not enabled_hooks:
|
|
118
|
+
continue
|
|
119
|
+
event_groups.append(
|
|
120
|
+
ResolvedHookMatcherGroup(
|
|
121
|
+
event_name=event_name,
|
|
122
|
+
matcher=str(group.matcher or ""),
|
|
123
|
+
hooks=enabled_hooks,
|
|
124
|
+
source_path=source_path,
|
|
125
|
+
source_scope=source_scope,
|
|
126
|
+
trusted=trusted,
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
if event_groups:
|
|
130
|
+
resolved[event_name] = event_groups
|
|
131
|
+
return resolved
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _drop_overridden_hooks(
|
|
135
|
+
groups: list[ResolvedHookMatcherGroup],
|
|
136
|
+
*,
|
|
137
|
+
overridden_ids: set[str],
|
|
138
|
+
) -> list[ResolvedHookMatcherGroup]:
|
|
139
|
+
if not overridden_ids:
|
|
140
|
+
return groups
|
|
141
|
+
retained: list[ResolvedHookMatcherGroup] = []
|
|
142
|
+
for group in groups:
|
|
143
|
+
remaining_hooks = tuple(hook for hook in group.hooks if hook.id not in overridden_ids)
|
|
144
|
+
if not remaining_hooks:
|
|
145
|
+
continue
|
|
146
|
+
if remaining_hooks == group.hooks:
|
|
147
|
+
retained.append(group)
|
|
148
|
+
continue
|
|
149
|
+
retained.append(
|
|
150
|
+
ResolvedHookMatcherGroup(
|
|
151
|
+
event_name=group.event_name,
|
|
152
|
+
matcher=group.matcher,
|
|
153
|
+
hooks=remaining_hooks,
|
|
154
|
+
source_path=group.source_path,
|
|
155
|
+
source_scope=group.source_scope,
|
|
156
|
+
trusted=group.trusted,
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
return retained
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _merge_event_groups(
|
|
163
|
+
*,
|
|
164
|
+
existing: list[ResolvedHookMatcherGroup],
|
|
165
|
+
incoming: list[ResolvedHookMatcherGroup],
|
|
166
|
+
) -> list[ResolvedHookMatcherGroup]:
|
|
167
|
+
override_ids = {hook.id for group in incoming for hook in group.hooks if hook.id is not None}
|
|
168
|
+
merged = _drop_overridden_hooks(existing, overridden_ids=override_ids)
|
|
169
|
+
merged.extend(incoming)
|
|
170
|
+
return merged
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def load_resolved_hooks_config(workspace_root: Path) -> ResolvedHookConfig:
|
|
174
|
+
workspace_root = workspace_root.resolve()
|
|
175
|
+
candidate_paths = (
|
|
176
|
+
(user_hooks_config_path(), "user"),
|
|
177
|
+
(project_hooks_config_path(workspace_root), "project"),
|
|
178
|
+
(project_local_hooks_config_path(workspace_root), "project_local"),
|
|
179
|
+
)
|
|
180
|
+
trust_state = load_trust_state()
|
|
181
|
+
merged: dict[str, list[ResolvedHookMatcherGroup]] = {}
|
|
182
|
+
loaded_paths: list[Path] = []
|
|
183
|
+
untrusted_project_paths: list[Path] = []
|
|
184
|
+
for path, source_scope in candidate_paths:
|
|
185
|
+
if not path.exists():
|
|
186
|
+
continue
|
|
187
|
+
trusted = source_scope != "project" or is_project_hooks_config_trusted(
|
|
188
|
+
workspace_root=workspace_root,
|
|
189
|
+
config_path=path,
|
|
190
|
+
state=trust_state,
|
|
191
|
+
)
|
|
192
|
+
loaded_paths.append(path)
|
|
193
|
+
if not trusted:
|
|
194
|
+
untrusted_project_paths.append(path)
|
|
195
|
+
continue
|
|
196
|
+
config_file = _load_hook_config_file(path)
|
|
197
|
+
resolved = _resolve_groups_for_file(
|
|
198
|
+
config_file=config_file,
|
|
199
|
+
source_path=path,
|
|
200
|
+
source_scope=source_scope,
|
|
201
|
+
trusted=trusted,
|
|
202
|
+
)
|
|
203
|
+
if not resolved:
|
|
204
|
+
continue
|
|
205
|
+
for event_name, groups in resolved.items():
|
|
206
|
+
merged[event_name] = _merge_event_groups(
|
|
207
|
+
existing=merged.get(event_name, []),
|
|
208
|
+
incoming=groups,
|
|
209
|
+
)
|
|
210
|
+
return ResolvedHookConfig(
|
|
211
|
+
groups_by_event={event_name: tuple(groups) for event_name, groups in merged.items()},
|
|
212
|
+
loaded_paths=tuple(loaded_paths),
|
|
213
|
+
untrusted_project_paths=tuple(untrusted_project_paths),
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
__all__ = [
|
|
218
|
+
"ResolvedHookConfig",
|
|
219
|
+
"ResolvedHookMatcherGroup",
|
|
220
|
+
"load_hook_config_file",
|
|
221
|
+
"load_resolved_hooks_config",
|
|
222
|
+
"project_hooks_config_path",
|
|
223
|
+
"project_local_hooks_config_path",
|
|
224
|
+
"user_hooks_config_path",
|
|
225
|
+
]
|