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,447 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Any, Literal, cast
|
|
6
|
+
|
|
7
|
+
from pydantic import (
|
|
8
|
+
BaseModel,
|
|
9
|
+
ConfigDict,
|
|
10
|
+
Field,
|
|
11
|
+
StrictBool,
|
|
12
|
+
StrictInt,
|
|
13
|
+
StrictStr,
|
|
14
|
+
field_validator,
|
|
15
|
+
model_validator,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from ..runtime_kind import normalize_runtime_kind
|
|
19
|
+
|
|
20
|
+
CanonicalHookEventName = Literal[
|
|
21
|
+
"PreToolUse",
|
|
22
|
+
"PostToolUse",
|
|
23
|
+
"PostWrite",
|
|
24
|
+
"SessionEnd",
|
|
25
|
+
"SessionStart",
|
|
26
|
+
"TurnComplete",
|
|
27
|
+
"UserPromptSubmit",
|
|
28
|
+
"Notification",
|
|
29
|
+
"PreCompact",
|
|
30
|
+
"SubagentStop",
|
|
31
|
+
]
|
|
32
|
+
HookEventName = Literal[
|
|
33
|
+
"PreToolUse",
|
|
34
|
+
"PostToolUse",
|
|
35
|
+
"PostWrite",
|
|
36
|
+
"SessionEnd",
|
|
37
|
+
"SessionStart",
|
|
38
|
+
"TurnComplete",
|
|
39
|
+
"Stop",
|
|
40
|
+
"UserPromptSubmit",
|
|
41
|
+
"Notification",
|
|
42
|
+
"PreCompact",
|
|
43
|
+
"SubagentStop",
|
|
44
|
+
]
|
|
45
|
+
HookFailurePolicy = Literal["warn", "continue", "block"]
|
|
46
|
+
HookSessionSource = Literal["startup", "resume", "fork"]
|
|
47
|
+
|
|
48
|
+
_VALID_HOOK_EVENTS: set[str] = {
|
|
49
|
+
"PreToolUse",
|
|
50
|
+
"PostToolUse",
|
|
51
|
+
"PostWrite",
|
|
52
|
+
"SessionEnd",
|
|
53
|
+
"SessionStart",
|
|
54
|
+
"TurnComplete",
|
|
55
|
+
"Stop",
|
|
56
|
+
"UserPromptSubmit",
|
|
57
|
+
"Notification",
|
|
58
|
+
"PreCompact",
|
|
59
|
+
"SubagentStop",
|
|
60
|
+
}
|
|
61
|
+
_CANONICAL_EVENT_NAME_BY_ALIAS: dict[str, CanonicalHookEventName] = {
|
|
62
|
+
"PreToolUse": "PreToolUse",
|
|
63
|
+
"PostToolUse": "PostToolUse",
|
|
64
|
+
"PostWrite": "PostWrite",
|
|
65
|
+
"SessionEnd": "SessionEnd",
|
|
66
|
+
"SessionStart": "SessionStart",
|
|
67
|
+
"TurnComplete": "TurnComplete",
|
|
68
|
+
"Stop": "TurnComplete",
|
|
69
|
+
"UserPromptSubmit": "UserPromptSubmit",
|
|
70
|
+
"Notification": "Notification",
|
|
71
|
+
"PreCompact": "PreCompact",
|
|
72
|
+
"SubagentStop": "SubagentStop",
|
|
73
|
+
}
|
|
74
|
+
_HOOK_ID_RE = re.compile(r"^[a-z0-9][a-z0-9._-]*$")
|
|
75
|
+
_VALID_FAILURE_POLICIES = {"warn", "continue", "block"}
|
|
76
|
+
_VALID_SESSION_SOURCES: frozenset[str] = frozenset({"startup", "resume", "fork"})
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def canonicalize_hook_event_name(event_name: str) -> CanonicalHookEventName:
|
|
80
|
+
normalized = str(event_name or "").strip()
|
|
81
|
+
if normalized not in _VALID_HOOK_EVENTS:
|
|
82
|
+
raise ValueError(f"unsupported hook event: {event_name}")
|
|
83
|
+
return _CANONICAL_EVENT_NAME_BY_ALIAS[normalized]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _normalize_optional_text(value: object, *, field_name: str) -> str:
|
|
87
|
+
if not isinstance(value, str):
|
|
88
|
+
raise TypeError(f"{field_name} must be a string.")
|
|
89
|
+
return value.strip()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _normalize_timeout(value: object, *, field_name: str) -> float:
|
|
93
|
+
if isinstance(value, bool):
|
|
94
|
+
raise TypeError(f"{field_name} must be a positive number.")
|
|
95
|
+
if not isinstance(value, int | float):
|
|
96
|
+
raise TypeError(f"{field_name} must be a positive number.")
|
|
97
|
+
timeout = float(value)
|
|
98
|
+
if timeout <= 0:
|
|
99
|
+
raise ValueError(f"{field_name} must be > 0.")
|
|
100
|
+
return timeout
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _normalize_optional_metadata_text(value: object, *, field_name: str) -> str | None:
|
|
104
|
+
if value is None:
|
|
105
|
+
return None
|
|
106
|
+
text = _normalize_optional_text(value, field_name=field_name)
|
|
107
|
+
return text or None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _normalize_priority(value: object, *, field_name: str) -> int:
|
|
111
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
112
|
+
raise TypeError(f"{field_name} must be an integer.")
|
|
113
|
+
return value
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _normalize_failure_policy(value: object, *, field_name: str) -> HookFailurePolicy:
|
|
117
|
+
text = _normalize_optional_text(value, field_name=field_name).lower()
|
|
118
|
+
if text not in _VALID_FAILURE_POLICIES:
|
|
119
|
+
raise ValueError(f"{field_name} must be one of: warn, continue, block.")
|
|
120
|
+
return cast(HookFailurePolicy, text)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _normalize_runtime_kinds(value: object) -> list[str] | None:
|
|
124
|
+
if value is None:
|
|
125
|
+
return None
|
|
126
|
+
items = [value] if isinstance(value, str) else value
|
|
127
|
+
if not isinstance(items, list | tuple):
|
|
128
|
+
raise TypeError("runtimeKinds must be a string or list of runtime kinds.")
|
|
129
|
+
normalized: list[str] = []
|
|
130
|
+
seen: set[str] = set()
|
|
131
|
+
for item in items:
|
|
132
|
+
kind = normalize_runtime_kind(item).value
|
|
133
|
+
if kind in seen:
|
|
134
|
+
continue
|
|
135
|
+
seen.add(kind)
|
|
136
|
+
normalized.append(kind)
|
|
137
|
+
return normalized or None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _normalize_session_sources(value: object) -> list[HookSessionSource] | None:
|
|
141
|
+
if value is None:
|
|
142
|
+
return None
|
|
143
|
+
items = [value] if isinstance(value, str) else value
|
|
144
|
+
if not isinstance(items, list | tuple):
|
|
145
|
+
raise TypeError("sessionSource must be a string or list of strings.")
|
|
146
|
+
normalized: list[HookSessionSource] = []
|
|
147
|
+
seen: set[str] = set()
|
|
148
|
+
for item in items:
|
|
149
|
+
if not isinstance(item, str):
|
|
150
|
+
raise TypeError("sessionSource entries must be strings.")
|
|
151
|
+
source = item.strip().lower()
|
|
152
|
+
if source not in _VALID_SESSION_SOURCES:
|
|
153
|
+
raise ValueError("sessionSource must contain only: startup, resume, fork.")
|
|
154
|
+
if source in seen:
|
|
155
|
+
continue
|
|
156
|
+
seen.add(source)
|
|
157
|
+
normalized.append(cast(HookSessionSource, source))
|
|
158
|
+
return normalized or None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class _StrictModel(BaseModel):
|
|
162
|
+
model_config = ConfigDict(extra="forbid", populate_by_name=True)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
_ENV_KEY_RE = re.compile(r"^[A-Z][A-Z0-9_]*$")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class CommandHookSpec(_StrictModel):
|
|
169
|
+
type: Literal["command"] = "command"
|
|
170
|
+
command: StrictStr
|
|
171
|
+
timeout: float = 10.0
|
|
172
|
+
enabled: StrictBool = True
|
|
173
|
+
id: StrictStr | None = None
|
|
174
|
+
description: StrictStr | None = None
|
|
175
|
+
priority: StrictInt = 0
|
|
176
|
+
failure_policy: HookFailurePolicy = Field(default="warn", alias="failurePolicy")
|
|
177
|
+
runtime_kinds: list[StrictStr] | None = Field(default=None, alias="runtimeKinds")
|
|
178
|
+
session_source: list[HookSessionSource] | None = Field(default=None, alias="sessionSource")
|
|
179
|
+
env_passthrough: Literal["all", "safe", "explicit"] = Field(
|
|
180
|
+
default="safe", alias="envPassthrough"
|
|
181
|
+
)
|
|
182
|
+
env_allow: list[StrictStr] | None = Field(default=None, alias="envAllow")
|
|
183
|
+
env: dict[StrictStr, StrictStr] | None = None
|
|
184
|
+
receives_full_payload: StrictBool = Field(default=False, alias="receivesFullPayload")
|
|
185
|
+
|
|
186
|
+
@model_validator(mode="before")
|
|
187
|
+
@classmethod
|
|
188
|
+
def _apply_timeout_ms_alias(cls, value: object) -> object:
|
|
189
|
+
if not isinstance(value, dict):
|
|
190
|
+
return value
|
|
191
|
+
raw = dict(value)
|
|
192
|
+
timeout_ms = raw.pop("timeoutMs", None)
|
|
193
|
+
if timeout_ms is not None:
|
|
194
|
+
if "timeout" in raw:
|
|
195
|
+
raise ValueError("Specify only one of timeout or timeoutMs.")
|
|
196
|
+
raw["timeout"] = _normalize_timeout(timeout_ms, field_name="timeoutMs") / 1000.0
|
|
197
|
+
return raw
|
|
198
|
+
|
|
199
|
+
@field_validator("command", mode="before")
|
|
200
|
+
@classmethod
|
|
201
|
+
def _validate_command(cls, value: object) -> str:
|
|
202
|
+
command = _normalize_optional_text(value, field_name="command")
|
|
203
|
+
if not command:
|
|
204
|
+
raise ValueError("command cannot be empty.")
|
|
205
|
+
return command
|
|
206
|
+
|
|
207
|
+
@field_validator("timeout", mode="before")
|
|
208
|
+
@classmethod
|
|
209
|
+
def _validate_timeout(cls, value: object) -> float:
|
|
210
|
+
return _normalize_timeout(value, field_name="timeout")
|
|
211
|
+
|
|
212
|
+
@field_validator("id", mode="before")
|
|
213
|
+
@classmethod
|
|
214
|
+
def _validate_id(cls, value: object) -> str | None:
|
|
215
|
+
hook_id = _normalize_optional_metadata_text(value, field_name="id")
|
|
216
|
+
if hook_id is None:
|
|
217
|
+
return None
|
|
218
|
+
if not _HOOK_ID_RE.fullmatch(hook_id):
|
|
219
|
+
raise ValueError("id must match ^[a-z0-9][a-z0-9._-]*$.")
|
|
220
|
+
return hook_id
|
|
221
|
+
|
|
222
|
+
@field_validator("description", mode="before")
|
|
223
|
+
@classmethod
|
|
224
|
+
def _validate_description(cls, value: object) -> str | None:
|
|
225
|
+
return _normalize_optional_metadata_text(value, field_name="description")
|
|
226
|
+
|
|
227
|
+
@field_validator("priority", mode="before")
|
|
228
|
+
@classmethod
|
|
229
|
+
def _validate_priority(cls, value: object) -> int:
|
|
230
|
+
return _normalize_priority(value, field_name="priority")
|
|
231
|
+
|
|
232
|
+
@field_validator("failure_policy", mode="before")
|
|
233
|
+
@classmethod
|
|
234
|
+
def _validate_failure_policy(cls, value: object) -> HookFailurePolicy:
|
|
235
|
+
return _normalize_failure_policy(value, field_name="failurePolicy")
|
|
236
|
+
|
|
237
|
+
@field_validator("runtime_kinds", mode="before")
|
|
238
|
+
@classmethod
|
|
239
|
+
def _validate_runtime_kinds(cls, value: object) -> list[str] | None:
|
|
240
|
+
return _normalize_runtime_kinds(value)
|
|
241
|
+
|
|
242
|
+
@field_validator("session_source", mode="before")
|
|
243
|
+
@classmethod
|
|
244
|
+
def _validate_session_source(cls, value: object) -> list[HookSessionSource] | None:
|
|
245
|
+
return _normalize_session_sources(value)
|
|
246
|
+
|
|
247
|
+
@field_validator("env_passthrough", mode="before")
|
|
248
|
+
@classmethod
|
|
249
|
+
def _validate_env_passthrough(cls, value: object) -> str:
|
|
250
|
+
if value is None:
|
|
251
|
+
return "safe"
|
|
252
|
+
if not isinstance(value, str):
|
|
253
|
+
raise TypeError("envPassthrough must be a string.")
|
|
254
|
+
lowered = value.strip().lower()
|
|
255
|
+
if lowered not in {"all", "safe", "explicit"}:
|
|
256
|
+
raise ValueError("envPassthrough must be one of: all, safe, explicit.")
|
|
257
|
+
return lowered
|
|
258
|
+
|
|
259
|
+
@field_validator("env_allow", mode="before")
|
|
260
|
+
@classmethod
|
|
261
|
+
def _validate_env_allow(cls, value: object) -> list[str] | None:
|
|
262
|
+
if value is None:
|
|
263
|
+
return None
|
|
264
|
+
if not isinstance(value, list):
|
|
265
|
+
raise TypeError("envAllow must be a list of environment variable names.")
|
|
266
|
+
normalized: list[str] = []
|
|
267
|
+
seen: set[str] = set()
|
|
268
|
+
for item in value:
|
|
269
|
+
if not isinstance(item, str):
|
|
270
|
+
raise TypeError("envAllow entries must be strings.")
|
|
271
|
+
name = item.strip()
|
|
272
|
+
if not _ENV_KEY_RE.fullmatch(name):
|
|
273
|
+
raise ValueError("envAllow entries must match ^[A-Z][A-Z0-9_]*$.")
|
|
274
|
+
if name in seen:
|
|
275
|
+
continue
|
|
276
|
+
seen.add(name)
|
|
277
|
+
normalized.append(name)
|
|
278
|
+
return normalized or None
|
|
279
|
+
|
|
280
|
+
@field_validator("env", mode="before")
|
|
281
|
+
@classmethod
|
|
282
|
+
def _validate_env_dict(cls, value: object) -> dict[str, str] | None:
|
|
283
|
+
if value is None:
|
|
284
|
+
return None
|
|
285
|
+
if not isinstance(value, dict):
|
|
286
|
+
raise TypeError("env must be a mapping of environment variable names to strings.")
|
|
287
|
+
normalized: dict[str, str] = {}
|
|
288
|
+
for key, raw_value in value.items():
|
|
289
|
+
if not isinstance(key, str):
|
|
290
|
+
raise TypeError("env keys must be strings.")
|
|
291
|
+
if not _ENV_KEY_RE.fullmatch(key):
|
|
292
|
+
raise ValueError("env keys must match ^[A-Z][A-Z0-9_]*$.")
|
|
293
|
+
if not isinstance(raw_value, str):
|
|
294
|
+
raise TypeError("env values must be strings.")
|
|
295
|
+
normalized[key] = raw_value
|
|
296
|
+
return normalized or None
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class HookMatcherGroup(_StrictModel):
|
|
300
|
+
matcher: StrictStr = ""
|
|
301
|
+
hooks: list[CommandHookSpec] = Field(default_factory=list)
|
|
302
|
+
enabled: StrictBool = True
|
|
303
|
+
|
|
304
|
+
@field_validator("matcher", mode="before")
|
|
305
|
+
@classmethod
|
|
306
|
+
def _validate_matcher(cls, value: object) -> str:
|
|
307
|
+
if value is None:
|
|
308
|
+
return ""
|
|
309
|
+
return _normalize_optional_text(value, field_name="matcher")
|
|
310
|
+
|
|
311
|
+
@model_validator(mode="after")
|
|
312
|
+
def _validate_unique_hook_ids(self) -> HookMatcherGroup:
|
|
313
|
+
seen: set[str] = set()
|
|
314
|
+
for hook in self.hooks:
|
|
315
|
+
if hook.id is None:
|
|
316
|
+
continue
|
|
317
|
+
if hook.id in seen:
|
|
318
|
+
raise ValueError(f"duplicate hook id in matcher group: {hook.id}")
|
|
319
|
+
seen.add(hook.id)
|
|
320
|
+
return self
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class HookConfigFile(_StrictModel):
|
|
324
|
+
schema_version: int = 1
|
|
325
|
+
hooks: dict[str, list[HookMatcherGroup]] = Field(default_factory=dict)
|
|
326
|
+
|
|
327
|
+
@field_validator("schema_version")
|
|
328
|
+
@classmethod
|
|
329
|
+
def _validate_schema_version(cls, value: int) -> int:
|
|
330
|
+
if value != 1:
|
|
331
|
+
raise ValueError("schema_version must be 1.")
|
|
332
|
+
return value
|
|
333
|
+
|
|
334
|
+
@field_validator("hooks")
|
|
335
|
+
@classmethod
|
|
336
|
+
def _validate_hooks(cls, value: dict[str, list[HookMatcherGroup]]) -> dict[str, Any]:
|
|
337
|
+
normalized: dict[str, list[HookMatcherGroup]] = {}
|
|
338
|
+
for event_name, groups in value.items():
|
|
339
|
+
canonical = canonicalize_hook_event_name(event_name)
|
|
340
|
+
normalized.setdefault(canonical, []).extend(groups)
|
|
341
|
+
return normalized
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
@dataclass(frozen=True)
|
|
345
|
+
class HookDispatchResult:
|
|
346
|
+
blocked: bool = False
|
|
347
|
+
reason: str = ""
|
|
348
|
+
modified_input: dict[str, Any] | None = None
|
|
349
|
+
modified_prompt: str | None = None
|
|
350
|
+
additional_system_messages: tuple[str, ...] = ()
|
|
351
|
+
additional_user_messages: tuple[str, ...] = ()
|
|
352
|
+
system_notices: tuple[str, ...] = ()
|
|
353
|
+
stop_reason: str = ""
|
|
354
|
+
permission_decision: str = ""
|
|
355
|
+
permission_decision_reason: str = ""
|
|
356
|
+
ask_requested: bool = False
|
|
357
|
+
ask_reason: str = ""
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
@dataclass(frozen=True)
|
|
361
|
+
class ParsedHookOutput:
|
|
362
|
+
blocked: bool = False
|
|
363
|
+
reason: str = ""
|
|
364
|
+
modified_input: dict[str, Any] | None = None
|
|
365
|
+
modified_prompt: str | None = None
|
|
366
|
+
additional_system_messages: tuple[str, ...] = ()
|
|
367
|
+
additional_user_messages: tuple[str, ...] = ()
|
|
368
|
+
system_notices: tuple[str, ...] = ()
|
|
369
|
+
halt: bool = False
|
|
370
|
+
stop_reason: str = ""
|
|
371
|
+
allow_short_circuit: bool = False
|
|
372
|
+
suppress_output: bool = False
|
|
373
|
+
permission_decision: str = ""
|
|
374
|
+
permission_decision_reason: str = ""
|
|
375
|
+
stdout_chars: int = 0
|
|
376
|
+
stderr_chars: int = 0
|
|
377
|
+
status: str = "ok"
|
|
378
|
+
warnings: tuple[str, ...] = ()
|
|
379
|
+
ask_requested: bool = False
|
|
380
|
+
ask_reason: str = ""
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
@dataclass(frozen=True)
|
|
384
|
+
class HookInvocationContext:
|
|
385
|
+
event_name: CanonicalHookEventName
|
|
386
|
+
source_path: str
|
|
387
|
+
source_scope: str
|
|
388
|
+
matcher: str
|
|
389
|
+
hook_id: str
|
|
390
|
+
priority: int
|
|
391
|
+
failure_policy: HookFailurePolicy
|
|
392
|
+
command: str
|
|
393
|
+
timeout_s: float
|
|
394
|
+
trusted: bool = True
|
|
395
|
+
returncode: int | None = None
|
|
396
|
+
blocked: bool = False
|
|
397
|
+
modified_input: bool = False
|
|
398
|
+
modified_input_fields: tuple[str, ...] = ()
|
|
399
|
+
modified_prompt: bool = False
|
|
400
|
+
modified_prompt_chars: int = 0
|
|
401
|
+
additional_system_message_count: int = 0
|
|
402
|
+
additional_user_message_count: int = 0
|
|
403
|
+
system_notices_count: int = 0
|
|
404
|
+
halt_requested: bool = False
|
|
405
|
+
allow_short_circuited: bool = False
|
|
406
|
+
suppress_output: bool = False
|
|
407
|
+
permission_decision: str = ""
|
|
408
|
+
stop_reason: str = ""
|
|
409
|
+
stdout_chars: int = 0
|
|
410
|
+
stderr_chars: int = 0
|
|
411
|
+
duration_ms: int = 0
|
|
412
|
+
status: str = "ok"
|
|
413
|
+
warnings: tuple[str, ...] = ()
|
|
414
|
+
payload_truncated: bool = False
|
|
415
|
+
payload_bytes: int = 0
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
@dataclass
|
|
419
|
+
class MutableHookDispatchState:
|
|
420
|
+
blocked: bool = False
|
|
421
|
+
reason: str = ""
|
|
422
|
+
modified_input: dict[str, Any] | None = None
|
|
423
|
+
modified_prompt: str | None = None
|
|
424
|
+
additional_system_messages: list[str] = field(default_factory=list)
|
|
425
|
+
additional_user_messages: list[str] = field(default_factory=list)
|
|
426
|
+
system_notices: list[str] = field(default_factory=list)
|
|
427
|
+
stop_reason: str = ""
|
|
428
|
+
permission_decision: str = ""
|
|
429
|
+
permission_decision_reason: str = ""
|
|
430
|
+
ask_requested: bool = False
|
|
431
|
+
ask_reason: str = ""
|
|
432
|
+
|
|
433
|
+
def freeze(self) -> HookDispatchResult:
|
|
434
|
+
return HookDispatchResult(
|
|
435
|
+
blocked=self.blocked,
|
|
436
|
+
reason=self.reason,
|
|
437
|
+
modified_input=self.modified_input,
|
|
438
|
+
modified_prompt=self.modified_prompt,
|
|
439
|
+
additional_system_messages=tuple(self.additional_system_messages),
|
|
440
|
+
additional_user_messages=tuple(self.additional_user_messages),
|
|
441
|
+
system_notices=tuple(self.system_notices),
|
|
442
|
+
stop_reason=self.stop_reason,
|
|
443
|
+
permission_decision=self.permission_decision,
|
|
444
|
+
permission_decision_reason=self.permission_decision_reason,
|
|
445
|
+
ask_requested=self.ask_requested,
|
|
446
|
+
ask_reason=self.ask_reason,
|
|
447
|
+
)
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from ..atomic_io import atomic_write_json
|
|
10
|
+
from ..branding import canonical_user_config_dir, env_get
|
|
11
|
+
|
|
12
|
+
_TRUST_SCHEMA_VERSION = 1
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class ProjectHooksTrustKey:
|
|
17
|
+
workspace_root: str
|
|
18
|
+
relative_config_path: str
|
|
19
|
+
file_hash: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class ProjectHooksTrustState:
|
|
24
|
+
trusted_configs: tuple[ProjectHooksTrustKey, ...] = ()
|
|
25
|
+
|
|
26
|
+
def contains(self, key: ProjectHooksTrustKey) -> bool:
|
|
27
|
+
return key in set(self.trusted_configs)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _config_dir() -> Path:
|
|
31
|
+
override = env_get("ALYSIS_CONFIG_DIR")
|
|
32
|
+
if override:
|
|
33
|
+
return Path(override)
|
|
34
|
+
return canonical_user_config_dir()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def trust_state_path(*, user_config_dir: Path | None = None) -> Path:
|
|
38
|
+
base = user_config_dir.expanduser().resolve() if user_config_dir is not None else _config_dir()
|
|
39
|
+
return base / "project_hooks_trust.json"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _hash_file(path: Path) -> str:
|
|
43
|
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def project_hooks_trust_key(*, workspace_root: Path, config_path: Path) -> ProjectHooksTrustKey:
|
|
47
|
+
resolved_workspace = workspace_root.expanduser().resolve()
|
|
48
|
+
resolved_path = config_path.expanduser().resolve()
|
|
49
|
+
if not resolved_path.is_file():
|
|
50
|
+
raise ValueError(f"Project hooks config not found: {resolved_path}")
|
|
51
|
+
try:
|
|
52
|
+
relative_config_path = os.fspath(resolved_path.relative_to(resolved_workspace))
|
|
53
|
+
except ValueError as exc:
|
|
54
|
+
raise ValueError(
|
|
55
|
+
f"Project hooks config must live under the workspace root: {resolved_path}"
|
|
56
|
+
) from exc
|
|
57
|
+
return ProjectHooksTrustKey(
|
|
58
|
+
workspace_root=os.fspath(resolved_workspace),
|
|
59
|
+
relative_config_path=relative_config_path,
|
|
60
|
+
file_hash=_hash_file(resolved_path),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def load_trust_state(*, user_config_dir: Path | None = None) -> ProjectHooksTrustState:
|
|
65
|
+
path = trust_state_path(user_config_dir=user_config_dir)
|
|
66
|
+
if not path.exists():
|
|
67
|
+
return ProjectHooksTrustState()
|
|
68
|
+
try:
|
|
69
|
+
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
70
|
+
except (OSError, json.JSONDecodeError, UnicodeDecodeError) as exc:
|
|
71
|
+
raise RuntimeError(f"Failed to read project hooks trust state: {path}") from exc
|
|
72
|
+
if not isinstance(raw, dict):
|
|
73
|
+
raise RuntimeError("Invalid project hooks trust state format.")
|
|
74
|
+
version = raw.get("schema_version", _TRUST_SCHEMA_VERSION)
|
|
75
|
+
if version != _TRUST_SCHEMA_VERSION:
|
|
76
|
+
raise RuntimeError(f"Unsupported project hooks trust schema version: {version}")
|
|
77
|
+
trusted_raw = raw.get("trusted_configs", [])
|
|
78
|
+
if not isinstance(trusted_raw, list):
|
|
79
|
+
raise RuntimeError("Invalid project hooks trust state: trusted_configs must be an array.")
|
|
80
|
+
trusted: list[ProjectHooksTrustKey] = []
|
|
81
|
+
for item in trusted_raw:
|
|
82
|
+
if not isinstance(item, dict):
|
|
83
|
+
continue
|
|
84
|
+
workspace_root = str(item.get("workspace_root") or "").strip()
|
|
85
|
+
relative_config_path = str(item.get("relative_config_path") or "").strip()
|
|
86
|
+
file_hash = str(item.get("file_hash") or "").strip()
|
|
87
|
+
if workspace_root and relative_config_path and file_hash:
|
|
88
|
+
trusted.append(
|
|
89
|
+
ProjectHooksTrustKey(
|
|
90
|
+
workspace_root=workspace_root,
|
|
91
|
+
relative_config_path=relative_config_path,
|
|
92
|
+
file_hash=file_hash,
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
trusted_sorted = tuple(
|
|
96
|
+
sorted(
|
|
97
|
+
set(trusted),
|
|
98
|
+
key=lambda key: (key.workspace_root, key.relative_config_path, key.file_hash),
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
return ProjectHooksTrustState(trusted_configs=trusted_sorted)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def save_trust_state(
|
|
105
|
+
state: ProjectHooksTrustState,
|
|
106
|
+
*,
|
|
107
|
+
user_config_dir: Path | None = None,
|
|
108
|
+
) -> None:
|
|
109
|
+
path = trust_state_path(user_config_dir=user_config_dir)
|
|
110
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
111
|
+
payload = {
|
|
112
|
+
"schema_version": _TRUST_SCHEMA_VERSION,
|
|
113
|
+
"trusted_configs": [
|
|
114
|
+
{
|
|
115
|
+
"workspace_root": key.workspace_root,
|
|
116
|
+
"relative_config_path": key.relative_config_path,
|
|
117
|
+
"file_hash": key.file_hash,
|
|
118
|
+
}
|
|
119
|
+
for key in state.trusted_configs
|
|
120
|
+
],
|
|
121
|
+
}
|
|
122
|
+
atomic_write_json(path, payload, ensure_ascii=True)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def trust_project_hooks_config(
|
|
126
|
+
*,
|
|
127
|
+
workspace_root: Path,
|
|
128
|
+
config_path: Path,
|
|
129
|
+
user_config_dir: Path | None = None,
|
|
130
|
+
) -> ProjectHooksTrustState:
|
|
131
|
+
key = project_hooks_trust_key(workspace_root=workspace_root, config_path=config_path)
|
|
132
|
+
current = load_trust_state(user_config_dir=user_config_dir)
|
|
133
|
+
trusted = set(current.trusted_configs)
|
|
134
|
+
trusted.add(key)
|
|
135
|
+
updated = ProjectHooksTrustState(
|
|
136
|
+
trusted_configs=tuple(
|
|
137
|
+
sorted(
|
|
138
|
+
trusted,
|
|
139
|
+
key=lambda item: (item.workspace_root, item.relative_config_path, item.file_hash),
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
save_trust_state(updated, user_config_dir=user_config_dir)
|
|
144
|
+
return updated
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def untrust_project_hooks_config(
|
|
148
|
+
*,
|
|
149
|
+
workspace_root: Path,
|
|
150
|
+
config_path: Path,
|
|
151
|
+
user_config_dir: Path | None = None,
|
|
152
|
+
) -> ProjectHooksTrustState:
|
|
153
|
+
resolved_workspace = workspace_root.expanduser().resolve()
|
|
154
|
+
resolved_path = config_path.expanduser().resolve()
|
|
155
|
+
try:
|
|
156
|
+
relative_config_path = os.fspath(resolved_path.relative_to(resolved_workspace))
|
|
157
|
+
except ValueError as exc:
|
|
158
|
+
raise ValueError(
|
|
159
|
+
f"Project hooks config must live under the workspace root: {resolved_path}"
|
|
160
|
+
) from exc
|
|
161
|
+
current = load_trust_state(user_config_dir=user_config_dir)
|
|
162
|
+
remaining = tuple(
|
|
163
|
+
key
|
|
164
|
+
for key in current.trusted_configs
|
|
165
|
+
if not (
|
|
166
|
+
key.workspace_root == os.fspath(resolved_workspace)
|
|
167
|
+
and key.relative_config_path == relative_config_path
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
updated = ProjectHooksTrustState(trusted_configs=remaining)
|
|
171
|
+
save_trust_state(updated, user_config_dir=user_config_dir)
|
|
172
|
+
return updated
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def is_project_hooks_config_trusted(
|
|
176
|
+
*,
|
|
177
|
+
workspace_root: Path,
|
|
178
|
+
config_path: Path,
|
|
179
|
+
state: ProjectHooksTrustState | None = None,
|
|
180
|
+
user_config_dir: Path | None = None,
|
|
181
|
+
) -> bool:
|
|
182
|
+
resolved_path = config_path.expanduser().resolve()
|
|
183
|
+
if not resolved_path.is_file():
|
|
184
|
+
return False
|
|
185
|
+
active_state = state if state is not None else load_trust_state(user_config_dir=user_config_dir)
|
|
186
|
+
return project_hooks_trust_key(
|
|
187
|
+
workspace_root=workspace_root,
|
|
188
|
+
config_path=config_path,
|
|
189
|
+
) in set(active_state.trusted_configs)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
__all__ = [
|
|
193
|
+
"ProjectHooksTrustKey",
|
|
194
|
+
"ProjectHooksTrustState",
|
|
195
|
+
"is_project_hooks_config_trusted",
|
|
196
|
+
"load_trust_state",
|
|
197
|
+
"project_hooks_trust_key",
|
|
198
|
+
"save_trust_state",
|
|
199
|
+
"trust_project_hooks_config",
|
|
200
|
+
"trust_state_path",
|
|
201
|
+
"untrust_project_hooks_config",
|
|
202
|
+
]
|