langchain-agentx-python 0.1__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.
- langchain_agentx/__init__.py +46 -0
- langchain_agentx/command/__init__.py +28 -0
- langchain_agentx/command/builtin/__init__.py +25 -0
- langchain_agentx/command/builtin/clear.py +33 -0
- langchain_agentx/command/builtin/compact.py +33 -0
- langchain_agentx/command/builtin/memory.py +37 -0
- langchain_agentx/command/builtin/reload_plugins.py +42 -0
- langchain_agentx/command/context.py +30 -0
- langchain_agentx/command/dispatcher.py +183 -0
- langchain_agentx/command/registry.py +110 -0
- langchain_agentx/command/result.py +25 -0
- langchain_agentx/command/types.py +41 -0
- langchain_agentx/config/__init__.py +14 -0
- langchain_agentx/loop/__init__.py +47 -0
- langchain_agentx/loop/config/__init__.py +20 -0
- langchain_agentx/loop/config/agent_config.py +66 -0
- langchain_agentx/loop/config/agent_loop_config.py +72 -0
- langchain_agentx/loop/config/model_context_resolver.py +105 -0
- langchain_agentx/loop/config/runtime_settings.py +50 -0
- langchain_agentx/loop/config/token_estimator.py +133 -0
- langchain_agentx/loop/context/__init__.py +66 -0
- langchain_agentx/loop/context/blocking_guard.py +97 -0
- langchain_agentx/loop/context/compaction_service.py +60 -0
- langchain_agentx/loop/context/message_utils.py +56 -0
- langchain_agentx/loop/context/pipeline.py +127 -0
- langchain_agentx/loop/context/settings.py +103 -0
- langchain_agentx/loop/context/stages/__init__.py +29 -0
- langchain_agentx/loop/context/stages/autocompact.py +140 -0
- langchain_agentx/loop/context/stages/base.py +32 -0
- langchain_agentx/loop/context/stages/collapse.py +76 -0
- langchain_agentx/loop/context/stages/microcompact.py +76 -0
- langchain_agentx/loop/context/stages/noop.py +33 -0
- langchain_agentx/loop/context/stages/snip.py +71 -0
- langchain_agentx/loop/context/stages/tool_result_budget.py +69 -0
- langchain_agentx/loop/context/types.py +79 -0
- langchain_agentx/loop/exit/__init__.py +1 -0
- langchain_agentx/loop/exit/exit_logic.py +320 -0
- langchain_agentx/loop/exit/reason_codes.py +39 -0
- langchain_agentx/loop/graph/__init__.py +5 -0
- langchain_agentx/loop/graph/builtin_loop_control.py +197 -0
- langchain_agentx/loop/graph/factory.py +1409 -0
- langchain_agentx/loop/graph/graph_edges.py +820 -0
- langchain_agentx/loop/hook/__init__.py +48 -0
- langchain_agentx/loop/hook/async_hook_runner.py +62 -0
- langchain_agentx/loop/hook/config.py +280 -0
- langchain_agentx/loop/hook/engine.py +321 -0
- langchain_agentx/loop/hook/executors/__init__.py +9 -0
- langchain_agentx/loop/hook/executors/agent.py +107 -0
- langchain_agentx/loop/hook/executors/command.py +230 -0
- langchain_agentx/loop/hook/executors/http.py +114 -0
- langchain_agentx/loop/hook/executors/prompt.py +92 -0
- langchain_agentx/loop/hook/graph_wiring.py +134 -0
- langchain_agentx/loop/hook/registry.py +262 -0
- langchain_agentx/loop/hook/trust.py +43 -0
- langchain_agentx/loop/hook/types.py +110 -0
- langchain_agentx/loop/injection/__init__.py +13 -0
- langchain_agentx/loop/injection/dedup.py +74 -0
- langchain_agentx/loop/loop_abort.py +36 -0
- langchain_agentx/loop/model/__init__.py +1 -0
- langchain_agentx/loop/model/model_node.py +648 -0
- langchain_agentx/loop/model/model_nodes.py +661 -0
- langchain_agentx/loop/model/orphan_tool_results.py +38 -0
- langchain_agentx/loop/model/retrier.py +307 -0
- langchain_agentx/loop/model/retry_bridge.py +58 -0
- langchain_agentx/loop/model/retry_events.py +35 -0
- langchain_agentx/loop/model/retry_policy.py +56 -0
- langchain_agentx/loop/model/schema_and_format.py +153 -0
- langchain_agentx/loop/model/tool_and_model_binding.py +227 -0
- langchain_agentx/loop/model/tool_call_degradation_corrector.py +443 -0
- langchain_agentx/loop/model/tool_transcript_guard.py +225 -0
- langchain_agentx/loop/prompt/__init__.py +95 -0
- langchain_agentx/loop/prompt/builder.py +61 -0
- langchain_agentx/loop/prompt/builtin.py +218 -0
- langchain_agentx/loop/prompt/compact.py +408 -0
- langchain_agentx/loop/prompt/sections.py +120 -0
- langchain_agentx/loop/runtime/__init__.py +19 -0
- langchain_agentx/loop/runtime/context.py +34 -0
- langchain_agentx/loop/runtime/context_factory.py +107 -0
- langchain_agentx/loop/runtime/subagent_execution_paths.py +68 -0
- langchain_agentx/loop/subagent/__init__.py +53 -0
- langchain_agentx/loop/subagent/async_runner.py +215 -0
- langchain_agentx/loop/subagent/context.py +209 -0
- langchain_agentx/loop/subagent/fork_worktree_notice.py +25 -0
- langchain_agentx/loop/subagent/graph.py +72 -0
- langchain_agentx/loop/subagent/orchestrator.py +391 -0
- langchain_agentx/loop/subagent/progress.py +30 -0
- langchain_agentx/loop/subagent/prompt.py +52 -0
- langchain_agentx/loop/subagent/runner.py +504 -0
- langchain_agentx/loop/subagent/transcript.py +172 -0
- langchain_agentx/memory/__init__.py +2 -0
- langchain_agentx/memory/instruction/__init__.py +12 -0
- langchain_agentx/memory/instruction/loader.py +325 -0
- langchain_agentx/memory/instruction/resolver.py +24 -0
- langchain_agentx/memory/instruction/runtime.py +83 -0
- langchain_agentx/memory/instruction/sections.py +83 -0
- langchain_agentx/memory/instruction/types.py +59 -0
- langchain_agentx/memory/memdir/__init__.py +77 -0
- langchain_agentx/memory/memdir/age.py +36 -0
- langchain_agentx/memory/memdir/agent_memory.py +380 -0
- langchain_agentx/memory/memdir/extractor.py +309 -0
- langchain_agentx/memory/memdir/loader.py +187 -0
- langchain_agentx/memory/memdir/paths.py +63 -0
- langchain_agentx/memory/memdir/recall.py +45 -0
- langchain_agentx/memory/memdir/runtime.py +43 -0
- langchain_agentx/memory/memdir/scan.py +135 -0
- langchain_agentx/memory/memdir/types.py +104 -0
- langchain_agentx/memory/session/__init__.py +76 -0
- langchain_agentx/memory/session/compact_bridge.py +208 -0
- langchain_agentx/memory/session/prompts.py +172 -0
- langchain_agentx/memory/session/session_memory.py +282 -0
- langchain_agentx/observability/__init__.py +67 -0
- langchain_agentx/observability/evaluation/__init__.py +17 -0
- langchain_agentx/observability/evaluation/checkers/__init__.py +18 -0
- langchain_agentx/observability/evaluation/checkers/base.py +34 -0
- langchain_agentx/observability/evaluation/checkers/compaction.py +38 -0
- langchain_agentx/observability/evaluation/checkers/degradation.py +50 -0
- langchain_agentx/observability/evaluation/checkers/exit_quality.py +42 -0
- langchain_agentx/observability/evaluation/checkers/session_memory.py +45 -0
- langchain_agentx/observability/evaluation/checkers/tool_behavior.py +53 -0
- langchain_agentx/observability/evaluation/retention_scheduler.py +67 -0
- langchain_agentx/observability/evaluation/service.py +102 -0
- langchain_agentx/observability/evaluation/state.py +32 -0
- langchain_agentx/observability/evaluation/store.py +258 -0
- langchain_agentx/observability/events/__init__.py +15 -0
- langchain_agentx/observability/events/langchain_agentx_event_adapter.py +832 -0
- langchain_agentx/observability/logging/__init__.py +15 -0
- langchain_agentx/observability/logging/debug_burst.py +95 -0
- langchain_agentx/observability/logging/logging_config.py +178 -0
- langchain_agentx/observability/logging/logging_contract.py +65 -0
- langchain_agentx/observability/replay/__init__.py +35 -0
- langchain_agentx/observability/replay/cli.py +91 -0
- langchain_agentx/observability/replay/service.py +83 -0
- langchain_agentx/observability/replay/store.py +278 -0
- langchain_agentx/observability/replay/ui.py +47 -0
- langchain_agentx/observability/trace/__init__.py +25 -0
- langchain_agentx/observability/trace/collector.py +560 -0
- langchain_agentx/observability/trace/event_emitter.py +183 -0
- langchain_agentx/observability/trace/hook_event_emitter.py +49 -0
- langchain_agentx/observability/trace/models.py +144 -0
- langchain_agentx/observability/trace/sqlite_store.py +873 -0
- langchain_agentx/observability/trace/trace_callback.py +295 -0
- langchain_agentx/observability/trace/trace_lifecycle_collector.py +114 -0
- langchain_agentx/plugin/__init__.py +26 -0
- langchain_agentx/plugin/builtin.py +53 -0
- langchain_agentx/plugin/config.py +113 -0
- langchain_agentx/plugin/loader.py +386 -0
- langchain_agentx/plugin/manifest.py +154 -0
- langchain_agentx/plugin/registries.py +211 -0
- langchain_agentx/plugin/types.py +142 -0
- langchain_agentx/provider/__init__.py +27 -0
- langchain_agentx/provider/anthropic.py +121 -0
- langchain_agentx/provider/compatible_chat_openai.py +86 -0
- langchain_agentx/provider/env.py +45 -0
- langchain_agentx/provider/model_profile.py +156 -0
- langchain_agentx/provider/openai.py +89 -0
- langchain_agentx/session/__init__.py +17 -0
- langchain_agentx/session/agent_session.py +320 -0
- langchain_agentx/session/conversation_factory.py +87 -0
- langchain_agentx/session/conversation_recovery.py +156 -0
- langchain_agentx/session/conversation_session.py +198 -0
- langchain_agentx/session/factory.py +143 -0
- langchain_agentx/session/protocol.py +25 -0
- langchain_agentx/task_runtime/__init__.py +113 -0
- langchain_agentx/task_runtime/core/__init__.py +51 -0
- langchain_agentx/task_runtime/core/ids.py +33 -0
- langchain_agentx/task_runtime/core/interfaces.py +115 -0
- langchain_agentx/task_runtime/core/notification_priority.py +19 -0
- langchain_agentx/task_runtime/core/types.py +136 -0
- langchain_agentx/task_runtime/integrations/__init__.py +33 -0
- langchain_agentx/task_runtime/integrations/loop_adapter.py +91 -0
- langchain_agentx/task_runtime/integrations/loop_integration.py +61 -0
- langchain_agentx/task_runtime/integrations/prefetch_providers.py +108 -0
- langchain_agentx/task_runtime/integrations/provider_factory.py +103 -0
- langchain_agentx/task_runtime/integrations/queued_command_provider.py +184 -0
- langchain_agentx/task_runtime/integrations/sqlite_queued_command_provider.py +338 -0
- langchain_agentx/task_runtime/integrations/tool_use_summary_provider.py +254 -0
- langchain_agentx/task_runtime/orchestrator/__init__.py +5 -0
- langchain_agentx/task_runtime/orchestrator/runtime.py +386 -0
- langchain_agentx/task_runtime/output/__init__.py +5 -0
- langchain_agentx/task_runtime/output/sink.py +64 -0
- langchain_agentx/task_runtime/policy/__init__.py +11 -0
- langchain_agentx/task_runtime/policy/withhold_visibility.py +32 -0
- langchain_agentx/task_runtime/queue/__init__.py +5 -0
- langchain_agentx/task_runtime/queue/in_memory.py +55 -0
- langchain_agentx/task_runtime/skill_prefetch/__init__.py +4 -0
- langchain_agentx/task_runtime/skill_prefetch/attachments.py +46 -0
- langchain_agentx/task_runtime/skill_prefetch/models.py +37 -0
- langchain_agentx/task_runtime/skill_prefetch/provider.py +344 -0
- langchain_agentx/task_runtime/store/__init__.py +6 -0
- langchain_agentx/task_runtime/store/in_memory.py +81 -0
- langchain_agentx/task_runtime/store/sqlite_store.py +281 -0
- langchain_agentx/task_runtime/tasks/__init__.py +76 -0
- langchain_agentx/task_runtime/tasks/ai_analysis/__init__.py +15 -0
- langchain_agentx/task_runtime/tasks/ai_analysis/base.py +41 -0
- langchain_agentx/task_runtime/tasks/ai_analysis/evaluation.py +67 -0
- langchain_agentx/task_runtime/tasks/ai_analysis/registry.py +36 -0
- langchain_agentx/task_runtime/tasks/ai_analysis/scheduler.py +70 -0
- langchain_agentx/task_runtime/tasks/base/__init__.py +6 -0
- langchain_agentx/task_runtime/tasks/base/contracts.py +24 -0
- langchain_agentx/task_runtime/tasks/custom/__init__.py +7 -0
- langchain_agentx/task_runtime/tasks/custom/executor.py +60 -0
- langchain_agentx/task_runtime/tasks/custom/notification.py +7 -0
- langchain_agentx/task_runtime/tasks/custom/semantics.py +13 -0
- langchain_agentx/task_runtime/tasks/custom/spec.py +33 -0
- langchain_agentx/task_runtime/tasks/dream_task/__init__.py +15 -0
- langchain_agentx/task_runtime/tasks/dream_task/executor.py +61 -0
- langchain_agentx/task_runtime/tasks/dream_task/notification.py +19 -0
- langchain_agentx/task_runtime/tasks/dream_task/semantics.py +13 -0
- langchain_agentx/task_runtime/tasks/dream_task/spec.py +35 -0
- langchain_agentx/task_runtime/tasks/dream_task/state.py +17 -0
- langchain_agentx/task_runtime/tasks/in_process_teammate/__init__.py +12 -0
- langchain_agentx/task_runtime/tasks/in_process_teammate/executor.py +36 -0
- langchain_agentx/task_runtime/tasks/in_process_teammate/notification.py +25 -0
- langchain_agentx/task_runtime/tasks/in_process_teammate/semantics.py +13 -0
- langchain_agentx/task_runtime/tasks/in_process_teammate/spec.py +63 -0
- langchain_agentx/task_runtime/tasks/local_agent/__init__.py +14 -0
- langchain_agentx/task_runtime/tasks/local_agent/executor.py +33 -0
- langchain_agentx/task_runtime/tasks/local_agent/notification.py +21 -0
- langchain_agentx/task_runtime/tasks/local_agent/runner.py +43 -0
- langchain_agentx/task_runtime/tasks/local_agent/semantics.py +13 -0
- langchain_agentx/task_runtime/tasks/local_agent/spec.py +31 -0
- langchain_agentx/task_runtime/tasks/local_bash/__init__.py +13 -0
- langchain_agentx/task_runtime/tasks/local_bash/executor.py +95 -0
- langchain_agentx/task_runtime/tasks/local_bash/notification.py +22 -0
- langchain_agentx/task_runtime/tasks/local_bash/semantics.py +13 -0
- langchain_agentx/task_runtime/tasks/local_bash/spec.py +55 -0
- langchain_agentx/task_runtime/tasks/remote_agent/__init__.py +19 -0
- langchain_agentx/task_runtime/tasks/remote_agent/backend.py +76 -0
- langchain_agentx/task_runtime/tasks/remote_agent/executor.py +37 -0
- langchain_agentx/task_runtime/tasks/remote_agent/notification.py +22 -0
- langchain_agentx/task_runtime/tasks/remote_agent/semantics.py +13 -0
- langchain_agentx/task_runtime/tasks/remote_agent/spec.py +34 -0
- langchain_agentx/task_runtime/tasks/trace_cleanup/__init__.py +19 -0
- langchain_agentx/task_runtime/tasks/trace_cleanup/bootstrap.py +95 -0
- langchain_agentx/task_runtime/tasks/trace_cleanup/executor.py +66 -0
- langchain_agentx/task_runtime/tasks/trace_cleanup/scheduler.py +169 -0
- langchain_agentx/tool_runtime/__init__.py +90 -0
- langchain_agentx/tool_runtime/adapter.py +365 -0
- langchain_agentx/tool_runtime/base.py +319 -0
- langchain_agentx/tool_runtime/errors.py +190 -0
- langchain_agentx/tool_runtime/identical_call_cache.py +110 -0
- langchain_agentx/tool_runtime/loader.py +195 -0
- langchain_agentx/tool_runtime/models.py +260 -0
- langchain_agentx/tool_runtime/permission_context.py +78 -0
- langchain_agentx/tool_runtime/pipeline.py +621 -0
- langchain_agentx/tool_runtime/policy.py +447 -0
- langchain_agentx/tool_runtime/registry.py +81 -0
- langchain_agentx/tool_runtime/resolvers/__init__.py +27 -0
- langchain_agentx/tool_runtime/resolvers/agent_session.py +125 -0
- langchain_agentx/tool_runtime/resolvers/background.py +32 -0
- langchain_agentx/tool_runtime/resolvers/base.py +20 -0
- langchain_agentx/tool_runtime/resolvers/conversation.py +22 -0
- langchain_agentx/tool_runtime/resolvers/workflow.py +73 -0
- langchain_agentx/tool_runtime/session_store.py +132 -0
- langchain_agentx/tool_runtime/smoke_test_runtime.py +294 -0
- langchain_agentx/tool_runtime/state_bridge.py +164 -0
- langchain_agentx/tools/__init__.py +26 -0
- langchain_agentx/tools/agent/__init__.py +9 -0
- langchain_agentx/tools/agent/backend.py +53 -0
- langchain_agentx/tools/agent/built_in/__init__.py +19 -0
- langchain_agentx/tools/agent/built_in/agentx_guide.py +65 -0
- langchain_agentx/tools/agent/built_in/explore.py +80 -0
- langchain_agentx/tools/agent/built_in/general.py +57 -0
- langchain_agentx/tools/agent/built_in/plan.py +89 -0
- langchain_agentx/tools/agent/built_in/statusline_setup.py +64 -0
- langchain_agentx/tools/agent/built_in/verification.py +120 -0
- langchain_agentx/tools/agent/builtin_subagent_loader.py +89 -0
- langchain_agentx/tools/agent/cwd_resolution.py +119 -0
- langchain_agentx/tools/agent/limits.py +26 -0
- langchain_agentx/tools/agent/loader.py +270 -0
- langchain_agentx/tools/agent/models.py +85 -0
- langchain_agentx/tools/agent/prompt.py +120 -0
- langchain_agentx/tools/agent/registry/__init__.py +18 -0
- langchain_agentx/tools/agent/registry/config.py +29 -0
- langchain_agentx/tools/agent/registry/registry.py +47 -0
- langchain_agentx/tools/agent/scope.py +137 -0
- langchain_agentx/tools/agent/tool.py +256 -0
- langchain_agentx/tools/bash/__init__.py +9 -0
- langchain_agentx/tools/bash/ast_security.py +571 -0
- langchain_agentx/tools/bash/backend.py +1447 -0
- langchain_agentx/tools/bash/bash_hardening.py +734 -0
- langchain_agentx/tools/bash/bash_runtime_contract.py +41 -0
- langchain_agentx/tools/bash/cwd_reporter.py +95 -0
- langchain_agentx/tools/bash/limits.py +71 -0
- langchain_agentx/tools/bash/mode_validation.py +282 -0
- langchain_agentx/tools/bash/models.py +131 -0
- langchain_agentx/tools/bash/observability.py +148 -0
- langchain_agentx/tools/bash/output_utils.py +200 -0
- langchain_agentx/tools/bash/path_security.py +2429 -0
- langchain_agentx/tools/bash/prompt.py +68 -0
- langchain_agentx/tools/bash/read_only_validation.py +589 -0
- langchain_agentx/tools/bash/result_presenter.py +324 -0
- langchain_agentx/tools/bash/sandbox_decision.py +133 -0
- langchain_agentx/tools/bash/security.py +311 -0
- langchain_agentx/tools/bash/sed_edit_parser.py +243 -0
- langchain_agentx/tools/bash/sed_validation.py +163 -0
- langchain_agentx/tools/bash/semantics.py +111 -0
- langchain_agentx/tools/bash/session_manager.py +205 -0
- langchain_agentx/tools/bash/session_runtime.py +290 -0
- langchain_agentx/tools/bash/shell_locator.py +191 -0
- langchain_agentx/tools/bash/task_runtime.py +91 -0
- langchain_agentx/tools/bash/tool.py +939 -0
- langchain_agentx/tools/bash/windows_shell_quoting.py +45 -0
- langchain_agentx/tools/glob/__init__.py +9 -0
- langchain_agentx/tools/glob/models.py +57 -0
- langchain_agentx/tools/glob/pagination.py +30 -0
- langchain_agentx/tools/glob/prompt.py +24 -0
- langchain_agentx/tools/glob/rg_list_backend.py +139 -0
- langchain_agentx/tools/glob/rg_pattern.py +44 -0
- langchain_agentx/tools/glob/tool.py +327 -0
- langchain_agentx/tools/grep/__init__.py +7 -0
- langchain_agentx/tools/grep/backend.py +375 -0
- langchain_agentx/tools/grep/models.py +127 -0
- langchain_agentx/tools/grep/prompt.py +30 -0
- langchain_agentx/tools/grep/rg_subprocess_controller.py +114 -0
- langchain_agentx/tools/grep/tool.py +475 -0
- langchain_agentx/tools/read/__init__.py +9 -0
- langchain_agentx/tools/read/backend.py +415 -0
- langchain_agentx/tools/read/limits.py +67 -0
- langchain_agentx/tools/read/models.py +156 -0
- langchain_agentx/tools/read/prompt.py +73 -0
- langchain_agentx/tools/read/tool.py +494 -0
- langchain_agentx/tools/ripgrep_plugin_exclusions.py +137 -0
- langchain_agentx/tools/skill/__init__.py +4 -0
- langchain_agentx/tools/skill/argument_substitution.py +80 -0
- langchain_agentx/tools/skill/loader.py +196 -0
- langchain_agentx/tools/skill/models.py +88 -0
- langchain_agentx/tools/skill/policy.py +80 -0
- langchain_agentx/tools/skill/prompt.py +35 -0
- langchain_agentx/tools/skill/tool.py +222 -0
- langchain_agentx/utils/__init__.py +0 -0
- langchain_agentx/utils/cwd.py +124 -0
- langchain_agentx/utils/host_platform.py +112 -0
- langchain_agentx/utils/path_hierarchy.py +48 -0
- langchain_agentx/utils/path_user_input.py +66 -0
- langchain_agentx/utils/rg_executable.py +18 -0
- langchain_agentx/utils/subprocess_text.py +101 -0
- langchain_agentx/utils/temp_paths.py +77 -0
- langchain_agentx/utils/unc_path.py +25 -0
- langchain_agentx/utils/win_reserved_paths.py +51 -0
- langchain_agentx/workflow/__init__.py +7 -0
- langchain_agentx/workflow/base.py +97 -0
- langchain_agentx/workflow/batch.py +55 -0
- langchain_agentx/workflow/dag.py +54 -0
- langchain_agentx/workspace/__init__.py +13 -0
- langchain_agentx/workspace/config.py +140 -0
- langchain_agentx/workspace/path_key_normalizer.py +30 -0
- langchain_agentx/workspace/resolver.py +74 -0
- langchain_agentx/workspace/validators.py +41 -0
- langchain_agentx_python-0.1.dist-info/LICENSE +201 -0
- langchain_agentx_python-0.1.dist-info/METADATA +513 -0
- langchain_agentx_python-0.1.dist-info/RECORD +354 -0
- langchain_agentx_python-0.1.dist-info/WHEEL +5 -0
- langchain_agentx_python-0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"""
|
|
2
|
+
engine.py — Hook 执行引擎(P1/P2)。
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
负责按顺序执行 Hook 候选并聚合结果,输出 AggregatedHookResult。
|
|
6
|
+
|
|
7
|
+
链路位置:
|
|
8
|
+
graph wiring 节点调用 HookEngine.execute(event, ctx) 获取可消费结果。
|
|
9
|
+
|
|
10
|
+
当前裁剪范围:
|
|
11
|
+
已覆盖 function/callback + command/http/prompt/agent 执行器;
|
|
12
|
+
当前执行策略为 callback/function 顺序、其余执行器并行。
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import asyncio
|
|
18
|
+
import inspect
|
|
19
|
+
import logging
|
|
20
|
+
import time
|
|
21
|
+
from typing import Any, TYPE_CHECKING
|
|
22
|
+
|
|
23
|
+
from .config import (
|
|
24
|
+
HookCandidate,
|
|
25
|
+
HookCandidateDecision,
|
|
26
|
+
HooksConfigSnapshot,
|
|
27
|
+
resolve_executor_callable,
|
|
28
|
+
)
|
|
29
|
+
from .executors import CommandExecutor
|
|
30
|
+
from .executors import HttpExecutor
|
|
31
|
+
from .executors import PromptExecutor
|
|
32
|
+
from .executors import AgentExecutor
|
|
33
|
+
from .types import (
|
|
34
|
+
AggregatedHookResult,
|
|
35
|
+
HookContext,
|
|
36
|
+
HookExecutionRecord,
|
|
37
|
+
HookResult,
|
|
38
|
+
PermissionBehavior,
|
|
39
|
+
)
|
|
40
|
+
from .trust import WorkspaceTrustChecker
|
|
41
|
+
|
|
42
|
+
if TYPE_CHECKING:
|
|
43
|
+
from ...observability.trace.hook_event_emitter import HookEventEmitter
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
logger = logging.getLogger(__name__)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class FunctionExecutor:
|
|
50
|
+
"""执行 function 类型 hook。"""
|
|
51
|
+
|
|
52
|
+
async def run(self, candidate: HookCandidate, ctx: HookContext) -> HookResult:
|
|
53
|
+
fn = resolve_executor_callable(candidate.spec)
|
|
54
|
+
out = fn(ctx)
|
|
55
|
+
if inspect.isawaitable(out):
|
|
56
|
+
out = await out
|
|
57
|
+
return normalize_hook_result(out)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class CallbackExecutor:
|
|
61
|
+
"""执行 callback 类型 hook,支持 timeout。"""
|
|
62
|
+
|
|
63
|
+
async def run(self, candidate: HookCandidate, ctx: HookContext) -> HookResult:
|
|
64
|
+
cb = resolve_executor_callable(candidate.spec)
|
|
65
|
+
coro = cb(ctx)
|
|
66
|
+
if not inspect.isawaitable(coro):
|
|
67
|
+
return normalize_hook_result(coro)
|
|
68
|
+
timeout = candidate.spec.timeout
|
|
69
|
+
try:
|
|
70
|
+
out = await asyncio.wait_for(coro, timeout=timeout)
|
|
71
|
+
except asyncio.TimeoutError:
|
|
72
|
+
return HookResult(
|
|
73
|
+
outcome="non_blocking_error",
|
|
74
|
+
blocking_errors=[f"hook timeout after {timeout:.1f}s"],
|
|
75
|
+
)
|
|
76
|
+
return normalize_hook_result(out)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class HookEngine:
|
|
80
|
+
"""统一调度器(P1 顺序执行)。"""
|
|
81
|
+
|
|
82
|
+
def __init__(
|
|
83
|
+
self,
|
|
84
|
+
snapshot: HooksConfigSnapshot,
|
|
85
|
+
*,
|
|
86
|
+
event_emitter: HookEventEmitter | None = None,
|
|
87
|
+
trust_checker: WorkspaceTrustChecker | None = None,
|
|
88
|
+
) -> None:
|
|
89
|
+
self._snapshot = snapshot
|
|
90
|
+
self._function_executor = FunctionExecutor()
|
|
91
|
+
self._callback_executor = CallbackExecutor()
|
|
92
|
+
self._command_executor = CommandExecutor()
|
|
93
|
+
self._http_executor = HttpExecutor()
|
|
94
|
+
self._prompt_executor = PromptExecutor()
|
|
95
|
+
self._agent_executor = AgentExecutor()
|
|
96
|
+
self._event_emitter = event_emitter
|
|
97
|
+
self._trust_checker = trust_checker
|
|
98
|
+
|
|
99
|
+
async def execute(self, ctx: HookContext) -> AggregatedHookResult:
|
|
100
|
+
candidates, skipped = self._snapshot.get_candidates_with_decisions(ctx)
|
|
101
|
+
self._emit_skipped_records(ctx, skipped)
|
|
102
|
+
if not candidates:
|
|
103
|
+
return AggregatedHookResult()
|
|
104
|
+
if self._trust_checker is not None:
|
|
105
|
+
candidates = self._filter_trusted_candidates(ctx, candidates)
|
|
106
|
+
if not candidates:
|
|
107
|
+
return AggregatedHookResult()
|
|
108
|
+
|
|
109
|
+
result = AggregatedHookResult()
|
|
110
|
+
sequential_candidates = [
|
|
111
|
+
candidate
|
|
112
|
+
for candidate in candidates
|
|
113
|
+
if candidate.spec.executor_type in {"callback", "function"}
|
|
114
|
+
]
|
|
115
|
+
parallel_candidates = [
|
|
116
|
+
candidate
|
|
117
|
+
for candidate in candidates
|
|
118
|
+
if candidate.spec.executor_type in {"command", "http", "prompt", "agent"}
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
for candidate in sequential_candidates:
|
|
122
|
+
one = await self._run_with_record(candidate, ctx)
|
|
123
|
+
self._merge(result, one)
|
|
124
|
+
|
|
125
|
+
if parallel_candidates:
|
|
126
|
+
parallel_results = await asyncio.gather(
|
|
127
|
+
*(self._run_with_record(candidate, ctx) for candidate in parallel_candidates)
|
|
128
|
+
)
|
|
129
|
+
for one in parallel_results:
|
|
130
|
+
self._merge(result, one)
|
|
131
|
+
return result
|
|
132
|
+
|
|
133
|
+
def _filter_trusted_candidates(
|
|
134
|
+
self,
|
|
135
|
+
ctx: HookContext,
|
|
136
|
+
candidates: list[HookCandidate],
|
|
137
|
+
) -> list[HookCandidate]:
|
|
138
|
+
trusted: list[HookCandidate] = []
|
|
139
|
+
for candidate in candidates:
|
|
140
|
+
executor_type = candidate.spec.executor_type
|
|
141
|
+
if self._trust_checker is not None and self._trust_checker.should_skip(executor_type):
|
|
142
|
+
self._emit_trust_skipped_record(ctx, candidate)
|
|
143
|
+
continue
|
|
144
|
+
trusted.append(candidate)
|
|
145
|
+
return trusted
|
|
146
|
+
|
|
147
|
+
async def _run_with_record(self, candidate: HookCandidate, ctx: HookContext) -> HookResult:
|
|
148
|
+
started_at = time.perf_counter()
|
|
149
|
+
one = await self._run_one(candidate, ctx)
|
|
150
|
+
duration_ms = (time.perf_counter() - started_at) * 1000
|
|
151
|
+
outcome = self._emit_execution_record(ctx, candidate, one, duration_ms)
|
|
152
|
+
if outcome == "success" and candidate.spec.on_success is not None:
|
|
153
|
+
candidate.spec.on_success()
|
|
154
|
+
return one
|
|
155
|
+
|
|
156
|
+
async def _run_one(self, candidate: HookCandidate, ctx: HookContext) -> HookResult:
|
|
157
|
+
et = candidate.spec.executor_type
|
|
158
|
+
if et == "function":
|
|
159
|
+
return await self._function_executor.run(candidate, ctx)
|
|
160
|
+
if et == "callback":
|
|
161
|
+
return await self._callback_executor.run(candidate, ctx)
|
|
162
|
+
if et == "command":
|
|
163
|
+
return await self._command_executor.run(candidate, ctx)
|
|
164
|
+
if et == "http":
|
|
165
|
+
return await self._http_executor.run(candidate, ctx)
|
|
166
|
+
if et == "prompt":
|
|
167
|
+
return await self._prompt_executor.run(candidate, ctx)
|
|
168
|
+
if et == "agent":
|
|
169
|
+
return await self._agent_executor.run(candidate, ctx)
|
|
170
|
+
return HookResult(
|
|
171
|
+
outcome="non_blocking_error",
|
|
172
|
+
blocking_errors=[f"unsupported executor in P1: {et}"],
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
@staticmethod
|
|
176
|
+
def _merge(agg: AggregatedHookResult, one: HookResult) -> None:
|
|
177
|
+
if one.prevent_continuation:
|
|
178
|
+
agg.prevent_continuation = True
|
|
179
|
+
if one.blocking_errors:
|
|
180
|
+
agg.blocking_errors.extend(one.blocking_errors)
|
|
181
|
+
if one.additional_context:
|
|
182
|
+
agg.additional_contexts.append(one.additional_context)
|
|
183
|
+
if one.updated_input is not None:
|
|
184
|
+
agg.updated_input = dict(one.updated_input)
|
|
185
|
+
if one.state_patch:
|
|
186
|
+
agg.state_patch.update(one.state_patch)
|
|
187
|
+
|
|
188
|
+
agg.permission_behavior = merge_permission_behavior(
|
|
189
|
+
agg.permission_behavior,
|
|
190
|
+
one.permission_behavior,
|
|
191
|
+
)
|
|
192
|
+
if one.hook_permission_decision_reason:
|
|
193
|
+
agg.hook_permission_decision_reason = one.hook_permission_decision_reason
|
|
194
|
+
|
|
195
|
+
def _emit_skipped_records(
|
|
196
|
+
self,
|
|
197
|
+
ctx: HookContext,
|
|
198
|
+
skipped: list[HookCandidateDecision],
|
|
199
|
+
) -> None:
|
|
200
|
+
for decision in skipped:
|
|
201
|
+
record = HookExecutionRecord(
|
|
202
|
+
hook_id=decision.spec.hook_id,
|
|
203
|
+
hook_event=ctx.event.value,
|
|
204
|
+
hook_source=decision.spec.source,
|
|
205
|
+
matcher=decision.spec.matcher,
|
|
206
|
+
matched=decision.matched,
|
|
207
|
+
skip_reason=decision.skip_reason,
|
|
208
|
+
snapshot_id=self._snapshot.snapshot_id,
|
|
209
|
+
policy_source=self._snapshot.policy_source,
|
|
210
|
+
outcome="cancelled",
|
|
211
|
+
prevent_continuation=False,
|
|
212
|
+
duration_ms=0.0,
|
|
213
|
+
)
|
|
214
|
+
self._try_emit_record(ctx.session_id, record)
|
|
215
|
+
|
|
216
|
+
def _emit_execution_record(
|
|
217
|
+
self,
|
|
218
|
+
ctx: HookContext,
|
|
219
|
+
candidate: HookCandidate,
|
|
220
|
+
result: HookResult,
|
|
221
|
+
duration_ms: float,
|
|
222
|
+
) -> str:
|
|
223
|
+
outcome = result.outcome
|
|
224
|
+
if result.prevent_continuation or result.blocking_errors:
|
|
225
|
+
outcome = "blocking"
|
|
226
|
+
record = HookExecutionRecord(
|
|
227
|
+
hook_id=candidate.spec.hook_id,
|
|
228
|
+
hook_event=ctx.event.value,
|
|
229
|
+
hook_source=candidate.spec.source,
|
|
230
|
+
matcher=candidate.spec.matcher,
|
|
231
|
+
matched=True,
|
|
232
|
+
skip_reason=None,
|
|
233
|
+
snapshot_id=self._snapshot.snapshot_id,
|
|
234
|
+
policy_source=self._snapshot.policy_source,
|
|
235
|
+
outcome=outcome,
|
|
236
|
+
prevent_continuation=result.prevent_continuation,
|
|
237
|
+
duration_ms=duration_ms,
|
|
238
|
+
)
|
|
239
|
+
self._try_emit_record(ctx.session_id, record)
|
|
240
|
+
return outcome
|
|
241
|
+
|
|
242
|
+
def _emit_trust_skipped_record(
|
|
243
|
+
self,
|
|
244
|
+
ctx: HookContext,
|
|
245
|
+
candidate: HookCandidate,
|
|
246
|
+
) -> None:
|
|
247
|
+
reason = (
|
|
248
|
+
self._trust_checker.skip_reason(candidate.spec.executor_type)
|
|
249
|
+
if self._trust_checker is not None
|
|
250
|
+
else "trust_check_skipped"
|
|
251
|
+
)
|
|
252
|
+
record = HookExecutionRecord(
|
|
253
|
+
hook_id=candidate.spec.hook_id,
|
|
254
|
+
hook_event=ctx.event.value,
|
|
255
|
+
hook_source=candidate.spec.source,
|
|
256
|
+
matcher=candidate.spec.matcher,
|
|
257
|
+
matched=True,
|
|
258
|
+
skip_reason=reason,
|
|
259
|
+
snapshot_id=self._snapshot.snapshot_id,
|
|
260
|
+
policy_source=self._snapshot.policy_source,
|
|
261
|
+
outcome="trust_skipped",
|
|
262
|
+
prevent_continuation=False,
|
|
263
|
+
duration_ms=0.0,
|
|
264
|
+
)
|
|
265
|
+
self._try_emit_record(ctx.session_id, record)
|
|
266
|
+
|
|
267
|
+
def _try_emit_record(self, session_id: str | None, record: HookExecutionRecord) -> None:
|
|
268
|
+
if self._event_emitter is None or not session_id:
|
|
269
|
+
return
|
|
270
|
+
if not self._event_emitter.collector.has_session(session_id):
|
|
271
|
+
return
|
|
272
|
+
try:
|
|
273
|
+
self._event_emitter.emit_record(session_id, record)
|
|
274
|
+
except Exception as exc:
|
|
275
|
+
# 发射失败不应阻断主链路
|
|
276
|
+
logger.debug("hook event emit failed (non-blocking): %s", exc)
|
|
277
|
+
return
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def normalize_hook_result(value: Any) -> HookResult:
|
|
281
|
+
"""兼容 dict/HookResult 返回值。"""
|
|
282
|
+
if value is None:
|
|
283
|
+
return HookResult()
|
|
284
|
+
if isinstance(value, HookResult):
|
|
285
|
+
return value
|
|
286
|
+
if isinstance(value, dict):
|
|
287
|
+
kwargs = dict(value)
|
|
288
|
+
# 文档约定别名
|
|
289
|
+
if "preventContinuation" in kwargs and "prevent_continuation" not in kwargs:
|
|
290
|
+
kwargs["prevent_continuation"] = kwargs.pop("preventContinuation")
|
|
291
|
+
if "updatedInput" in kwargs and "updated_input" not in kwargs:
|
|
292
|
+
kwargs["updated_input"] = kwargs.pop("updatedInput")
|
|
293
|
+
if "permissionBehavior" in kwargs and "permission_behavior" not in kwargs:
|
|
294
|
+
kwargs["permission_behavior"] = kwargs.pop("permissionBehavior")
|
|
295
|
+
return HookResult(**kwargs)
|
|
296
|
+
raise TypeError(f"Unsupported hook result type: {type(value).__name__}")
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def merge_permission_behavior(
|
|
300
|
+
current: PermissionBehavior | None,
|
|
301
|
+
incoming: PermissionBehavior | None,
|
|
302
|
+
) -> PermissionBehavior | None:
|
|
303
|
+
"""deny > ask > allow > passthrough(None effect)."""
|
|
304
|
+
if incoming is None or incoming == "passthrough":
|
|
305
|
+
return current
|
|
306
|
+
if incoming == "deny":
|
|
307
|
+
return "deny"
|
|
308
|
+
if incoming == "ask":
|
|
309
|
+
return "ask" if current != "deny" else "deny"
|
|
310
|
+
if incoming == "allow":
|
|
311
|
+
return current or "allow"
|
|
312
|
+
return current
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
__all__ = [
|
|
316
|
+
"CallbackExecutor",
|
|
317
|
+
"FunctionExecutor",
|
|
318
|
+
"HookEngine",
|
|
319
|
+
"merge_permission_behavior",
|
|
320
|
+
"normalize_hook_result",
|
|
321
|
+
]
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agent.py — AgentExecutor(P2)。
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
执行 agent 类型 hook,构建单轮验证器输入并解析 decision。
|
|
6
|
+
|
|
7
|
+
链路位置:
|
|
8
|
+
HookEngine._run_one() 在 executor_type=="agent" 时调用本执行器。
|
|
9
|
+
|
|
10
|
+
当前裁剪范围:
|
|
11
|
+
通过配置注入可调用对象执行验证器;prompt 字段仅在运行时读取,不做预处理。
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import inspect
|
|
17
|
+
import json
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from typing import Any, Callable
|
|
20
|
+
|
|
21
|
+
from ..config import HookCandidate
|
|
22
|
+
from ..types import HookContext, HookResult
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class AgentExecutor:
|
|
27
|
+
"""agent 类型 hook 执行器。"""
|
|
28
|
+
|
|
29
|
+
async def run(self, candidate: HookCandidate, ctx: HookContext) -> HookResult:
|
|
30
|
+
config = candidate.spec.config
|
|
31
|
+
# 对齐 gh-24920 / CC-79:prompt 在运行期按原值读取,不在构建阶段做任何 transform。
|
|
32
|
+
system_prompt = config.get("prompt")
|
|
33
|
+
if not isinstance(system_prompt, str) or not system_prompt.strip():
|
|
34
|
+
return HookResult(
|
|
35
|
+
outcome="non_blocking_error",
|
|
36
|
+
blocking_errors=["agent hook missing 'prompt'"],
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
runner = self._resolve_runner(config)
|
|
40
|
+
if runner is None:
|
|
41
|
+
return HookResult(
|
|
42
|
+
outcome="non_blocking_error",
|
|
43
|
+
blocking_errors=["agent hook missing callable runner"],
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
user_payload = json.dumps(
|
|
47
|
+
{
|
|
48
|
+
"event": ctx.event.value,
|
|
49
|
+
"tool_name": ctx.tool_name,
|
|
50
|
+
"tool_input": ctx.tool_input,
|
|
51
|
+
},
|
|
52
|
+
ensure_ascii=False,
|
|
53
|
+
)
|
|
54
|
+
output_text = await self._call_runner(
|
|
55
|
+
runner=runner,
|
|
56
|
+
system_prompt=system_prompt,
|
|
57
|
+
user_payload=user_payload,
|
|
58
|
+
model=config.get("model"),
|
|
59
|
+
ctx=ctx,
|
|
60
|
+
)
|
|
61
|
+
return self._parse_decision(output_text)
|
|
62
|
+
|
|
63
|
+
@staticmethod
|
|
64
|
+
def _resolve_runner(config: dict[str, Any]) -> Callable[..., Any] | None:
|
|
65
|
+
runner = config.get("runner") or config.get("agent_runner")
|
|
66
|
+
return runner if callable(runner) else None
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
async def _call_runner(
|
|
70
|
+
*,
|
|
71
|
+
runner: Callable[..., Any],
|
|
72
|
+
system_prompt: str,
|
|
73
|
+
user_payload: str,
|
|
74
|
+
model: Any,
|
|
75
|
+
ctx: HookContext,
|
|
76
|
+
) -> str:
|
|
77
|
+
output = runner(system_prompt, user_payload, model, ctx)
|
|
78
|
+
if inspect.isawaitable(output):
|
|
79
|
+
output = await output
|
|
80
|
+
return str(output or "")
|
|
81
|
+
|
|
82
|
+
@staticmethod
|
|
83
|
+
def _parse_decision(text: str) -> HookResult:
|
|
84
|
+
lowered = text.lower()
|
|
85
|
+
if "deny" in lowered:
|
|
86
|
+
return HookResult(
|
|
87
|
+
outcome="success",
|
|
88
|
+
permission_behavior="deny",
|
|
89
|
+
hook_permission_decision_reason=text.strip() or None,
|
|
90
|
+
)
|
|
91
|
+
if "ask" in lowered:
|
|
92
|
+
return HookResult(
|
|
93
|
+
outcome="success",
|
|
94
|
+
permission_behavior="ask",
|
|
95
|
+
hook_permission_decision_reason=text.strip() or None,
|
|
96
|
+
)
|
|
97
|
+
if "allow" in lowered:
|
|
98
|
+
return HookResult(
|
|
99
|
+
outcome="success",
|
|
100
|
+
permission_behavior="allow",
|
|
101
|
+
hook_permission_decision_reason=text.strip() or None,
|
|
102
|
+
)
|
|
103
|
+
return HookResult(
|
|
104
|
+
outcome="non_blocking_error",
|
|
105
|
+
blocking_errors=["agent hook cannot parse decision"],
|
|
106
|
+
)
|
|
107
|
+
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""
|
|
2
|
+
command.py — CommandExecutor(P2)。
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
执行 command 类型 hook,负责子进程调用、退出码语义与 JSON 响应解析。
|
|
6
|
+
|
|
7
|
+
链路位置:
|
|
8
|
+
HookEngine._run_one() 在 executor_type=="command" 时调用本执行器。
|
|
9
|
+
|
|
10
|
+
当前裁剪范围:
|
|
11
|
+
实现 command 主路径、async/async_rewake、SESSION_START 的 CLAUDE_ENV_FILE 解析;
|
|
12
|
+
并行调度语义由 HookEngine 在 F6 阶段统一处理。
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import asyncio
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from langchain_core.messages import HumanMessage
|
|
24
|
+
|
|
25
|
+
from ..config import HookCandidate
|
|
26
|
+
from ..types import HookContext, HookEvent, HookResult
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class CommandExecutor:
|
|
31
|
+
"""command 类型 hook 执行器。"""
|
|
32
|
+
|
|
33
|
+
async def run(self, candidate: HookCandidate, ctx: HookContext) -> HookResult:
|
|
34
|
+
config = candidate.spec.config
|
|
35
|
+
command = str(config.get("command") or "").strip()
|
|
36
|
+
if not command:
|
|
37
|
+
return HookResult(
|
|
38
|
+
outcome="non_blocking_error",
|
|
39
|
+
blocking_errors=["command hook missing 'command'"],
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
shell = str(config.get("shell") or "bash")
|
|
43
|
+
async_mode = bool(config.get("async", False))
|
|
44
|
+
async_rewake = bool(config.get("asyncRewake", False) or config.get("async_rewake", False))
|
|
45
|
+
timeout = candidate.spec.timeout
|
|
46
|
+
env = self._build_hook_env(candidate, ctx)
|
|
47
|
+
|
|
48
|
+
if async_mode:
|
|
49
|
+
asyncio.create_task(
|
|
50
|
+
self._run_and_maybe_rewake(
|
|
51
|
+
command=command,
|
|
52
|
+
shell=shell,
|
|
53
|
+
timeout=timeout,
|
|
54
|
+
env=env,
|
|
55
|
+
ctx=ctx,
|
|
56
|
+
async_rewake=async_rewake,
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
return HookResult(outcome="success")
|
|
60
|
+
|
|
61
|
+
proc = await self._spawn_process(command=command, shell=shell, env=env)
|
|
62
|
+
return await self._finalize_process(proc=proc, ctx=ctx, timeout=timeout, env=env)
|
|
63
|
+
|
|
64
|
+
async def _run_and_maybe_rewake(
|
|
65
|
+
self,
|
|
66
|
+
*,
|
|
67
|
+
command: str,
|
|
68
|
+
shell: str,
|
|
69
|
+
timeout: float,
|
|
70
|
+
env: dict[str, str],
|
|
71
|
+
ctx: HookContext,
|
|
72
|
+
async_rewake: bool,
|
|
73
|
+
) -> None:
|
|
74
|
+
proc = await self._spawn_process(command=command, shell=shell, env=env)
|
|
75
|
+
result = await self._finalize_process(proc=proc, ctx=ctx, timeout=timeout, env=env)
|
|
76
|
+
if not async_rewake:
|
|
77
|
+
return
|
|
78
|
+
if result.outcome != "success":
|
|
79
|
+
return
|
|
80
|
+
rewake_text = (result.additional_context or "").strip()
|
|
81
|
+
if rewake_text:
|
|
82
|
+
messages = ctx.state.get("messages")
|
|
83
|
+
if isinstance(messages, list):
|
|
84
|
+
messages.append(HumanMessage(content=rewake_text))
|
|
85
|
+
|
|
86
|
+
async def _spawn_process(
|
|
87
|
+
self,
|
|
88
|
+
*,
|
|
89
|
+
command: str,
|
|
90
|
+
shell: str,
|
|
91
|
+
env: dict[str, str],
|
|
92
|
+
) -> asyncio.subprocess.Process:
|
|
93
|
+
if shell == "bash":
|
|
94
|
+
return await asyncio.create_subprocess_exec(
|
|
95
|
+
"bash",
|
|
96
|
+
"-lc",
|
|
97
|
+
command,
|
|
98
|
+
stdout=asyncio.subprocess.PIPE,
|
|
99
|
+
stderr=asyncio.subprocess.PIPE,
|
|
100
|
+
env=env,
|
|
101
|
+
)
|
|
102
|
+
if shell == "sh":
|
|
103
|
+
return await asyncio.create_subprocess_exec(
|
|
104
|
+
"sh",
|
|
105
|
+
"-c",
|
|
106
|
+
command,
|
|
107
|
+
stdout=asyncio.subprocess.PIPE,
|
|
108
|
+
stderr=asyncio.subprocess.PIPE,
|
|
109
|
+
env=env,
|
|
110
|
+
)
|
|
111
|
+
return await asyncio.create_subprocess_shell(
|
|
112
|
+
command,
|
|
113
|
+
stdout=asyncio.subprocess.PIPE,
|
|
114
|
+
stderr=asyncio.subprocess.PIPE,
|
|
115
|
+
env=env,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
async def _finalize_process(
|
|
119
|
+
self,
|
|
120
|
+
*,
|
|
121
|
+
proc: asyncio.subprocess.Process,
|
|
122
|
+
ctx: HookContext,
|
|
123
|
+
timeout: float,
|
|
124
|
+
env: dict[str, str],
|
|
125
|
+
) -> HookResult:
|
|
126
|
+
try:
|
|
127
|
+
stdout_raw, stderr_raw = await asyncio.wait_for(proc.communicate(), timeout=timeout)
|
|
128
|
+
except asyncio.TimeoutError:
|
|
129
|
+
proc.kill()
|
|
130
|
+
return HookResult(
|
|
131
|
+
outcome="non_blocking_error",
|
|
132
|
+
blocking_errors=[f"command hook timeout after {timeout:.1f}s"],
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
stdout = stdout_raw.decode("utf-8", errors="replace")
|
|
136
|
+
stderr = stderr_raw.decode("utf-8", errors="replace")
|
|
137
|
+
returncode = int(proc.returncode or 0)
|
|
138
|
+
|
|
139
|
+
result = HookResult(outcome="success")
|
|
140
|
+
parsed_json = self._parse_json(stdout)
|
|
141
|
+
if isinstance(parsed_json, dict):
|
|
142
|
+
self._apply_json_result(result, parsed_json)
|
|
143
|
+
|
|
144
|
+
if ctx.event == HookEvent.SESSION_START:
|
|
145
|
+
session_env_patch = self._read_claude_env_file(env)
|
|
146
|
+
if session_env_patch:
|
|
147
|
+
result.state_patch["_session_env"] = session_env_patch
|
|
148
|
+
|
|
149
|
+
if returncode == 0:
|
|
150
|
+
if not result.additional_context and stdout.strip():
|
|
151
|
+
result.additional_context = stdout.strip()
|
|
152
|
+
return result
|
|
153
|
+
if returncode == 2:
|
|
154
|
+
result.outcome = "blocking"
|
|
155
|
+
result.permission_behavior = result.permission_behavior or "deny"
|
|
156
|
+
if not result.blocking_errors:
|
|
157
|
+
result.blocking_errors = [stderr.strip() or "command hook blocked request"]
|
|
158
|
+
return result
|
|
159
|
+
result.outcome = "non_blocking_error"
|
|
160
|
+
if not result.blocking_errors:
|
|
161
|
+
result.blocking_errors = [stderr.strip() or f"command hook exited with {returncode}"]
|
|
162
|
+
return result
|
|
163
|
+
|
|
164
|
+
@staticmethod
|
|
165
|
+
def _parse_json(stdout: str) -> dict[str, Any] | None:
|
|
166
|
+
text = stdout.strip()
|
|
167
|
+
if not text:
|
|
168
|
+
return None
|
|
169
|
+
try:
|
|
170
|
+
data = json.loads(text)
|
|
171
|
+
except json.JSONDecodeError:
|
|
172
|
+
return None
|
|
173
|
+
return data if isinstance(data, dict) else None
|
|
174
|
+
|
|
175
|
+
@staticmethod
|
|
176
|
+
def _apply_json_result(result: HookResult, payload: dict[str, Any]) -> None:
|
|
177
|
+
if payload.get("continue") is False:
|
|
178
|
+
result.prevent_continuation = True
|
|
179
|
+
stop_reason = payload.get("stopReason")
|
|
180
|
+
if isinstance(stop_reason, str) and stop_reason.strip():
|
|
181
|
+
result.blocking_errors.append(stop_reason.strip())
|
|
182
|
+
decision = payload.get("decision")
|
|
183
|
+
if decision in {"allow", "ask", "deny"}:
|
|
184
|
+
result.permission_behavior = decision
|
|
185
|
+
reason = payload.get("reason")
|
|
186
|
+
if isinstance(reason, str) and reason.strip():
|
|
187
|
+
result.hook_permission_decision_reason = reason.strip()
|
|
188
|
+
updated_input = payload.get("updatedInput")
|
|
189
|
+
if isinstance(updated_input, dict):
|
|
190
|
+
result.updated_input = dict(updated_input)
|
|
191
|
+
if payload.get("suppressOutput") is True:
|
|
192
|
+
result.additional_context = ""
|
|
193
|
+
|
|
194
|
+
@staticmethod
|
|
195
|
+
def _build_hook_env(candidate: HookCandidate, ctx: HookContext) -> dict[str, str]:
|
|
196
|
+
env = dict(os.environ)
|
|
197
|
+
session_env = ctx.state.get("_session_env") if isinstance(ctx.state, dict) else None
|
|
198
|
+
if isinstance(session_env, dict):
|
|
199
|
+
for key, value in session_env.items():
|
|
200
|
+
if isinstance(key, str) and isinstance(value, str):
|
|
201
|
+
env[key] = value
|
|
202
|
+
env["HOOK_EVENT_NAME"] = ctx.event.value
|
|
203
|
+
env["HOOK_TOOL_NAME"] = ctx.tool_name or ""
|
|
204
|
+
env["HOOK_TOOL_INPUT"] = json.dumps(ctx.tool_input or {}, ensure_ascii=False)
|
|
205
|
+
env["HOOK_SESSION_ID"] = ctx.session_id or ""
|
|
206
|
+
env["HOOK_SNAPSHOT_ID"] = candidate.stable_key
|
|
207
|
+
return env
|
|
208
|
+
|
|
209
|
+
@staticmethod
|
|
210
|
+
def _read_claude_env_file(env: dict[str, str]) -> dict[str, str]:
|
|
211
|
+
path = env.get("CLAUDE_ENV_FILE")
|
|
212
|
+
if not path:
|
|
213
|
+
return {}
|
|
214
|
+
try:
|
|
215
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
216
|
+
lines = f.readlines()
|
|
217
|
+
except OSError:
|
|
218
|
+
return {}
|
|
219
|
+
|
|
220
|
+
parsed: dict[str, str] = {}
|
|
221
|
+
for raw in lines:
|
|
222
|
+
line = raw.strip()
|
|
223
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
224
|
+
continue
|
|
225
|
+
key, value = line.split("=", 1)
|
|
226
|
+
key = key.strip()
|
|
227
|
+
if key:
|
|
228
|
+
parsed[key] = value.strip()
|
|
229
|
+
return parsed
|
|
230
|
+
|