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,447 @@
|
|
|
1
|
+
"""
|
|
2
|
+
runtime/policy.py — 工具权限策略引擎
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
提供可复用的最小策略层,将工具本地校验(validate_input)与
|
|
6
|
+
全局权限治理(check_permissions)分开。PolicyEngine 作为可注入
|
|
7
|
+
的策略对象,由 RuntimeTool.__init__ 接收,在 check_permissions
|
|
8
|
+
默认实现中委托调用。
|
|
9
|
+
|
|
10
|
+
DefaultPolicyEngine 实现 v1.5 范围:
|
|
11
|
+
- read_roots / write_roots 读写分离路径限制
|
|
12
|
+
- deny_globs 用户自定义禁止模式匹配
|
|
13
|
+
- ask_globs 需要人工确认的敏感路径模式(headless 时降级为 deny)
|
|
14
|
+
- enable_builtin_deny 内置危险路径黑名单(.env/.ssh/*.key 等)
|
|
15
|
+
- read_only_mode 全局只读
|
|
16
|
+
- allow / deny / ask 三种决策
|
|
17
|
+
|
|
18
|
+
决策优先级(严格顺序):
|
|
19
|
+
[1] read_only_mode + is_destructive → deny
|
|
20
|
+
[2] enable_builtin_deny → 内置黑名单 → deny
|
|
21
|
+
[3] deny_globs 命中 → deny
|
|
22
|
+
[4] ask_globs 命中 → ask(headless 模式下由 pipeline 降级为 deny)
|
|
23
|
+
[5] 读写分权:is_read_only → read_roots;否则 → write_roots
|
|
24
|
+
[6] 通过 → allow
|
|
25
|
+
|
|
26
|
+
与 CC 对比:
|
|
27
|
+
CC 的权限系统分三层:
|
|
28
|
+
Layer 1: 每个工具的 checkPermissions()
|
|
29
|
+
Layer 2: hasPermissionsToUseToolInner() 全局引擎
|
|
30
|
+
alwaysDenyRules → alwaysAskRules → alwaysAllowRules → PermissionMode
|
|
31
|
+
Layer 3: HITL race(User UI + Hooks + Classifier + Bridge)
|
|
32
|
+
本框架 v1.5 实现 Layer 1 + Layer 2 完整版:
|
|
33
|
+
- deny_globs + BUILTIN_DENY_GLOBS 对应 CC alwaysDenyRules
|
|
34
|
+
- ask_globs 对应 CC alwaysAskRules(需人工确认路径)
|
|
35
|
+
- read_roots / write_roots 对应 CC allWorkingDirectories
|
|
36
|
+
- headless 模式 ask → deny 对应 CC shouldAvoidPermissionPrompts
|
|
37
|
+
- HITL 交互(Layer 3 完整 race)延期到 v2
|
|
38
|
+
|
|
39
|
+
详细设计见 docs/design-docs/tool-runtime/tool-permission-system.md
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
import fnmatch
|
|
45
|
+
import os
|
|
46
|
+
from abc import ABC, abstractmethod
|
|
47
|
+
from dataclasses import dataclass, field
|
|
48
|
+
from typing import Any
|
|
49
|
+
|
|
50
|
+
from .models import AuthorizationDecision, ToolExecutionContext
|
|
51
|
+
from .permission_context import get_active_auto_approved_tools
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
# ToolPolicyConfig
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class ToolPolicyConfig:
|
|
60
|
+
"""
|
|
61
|
+
工具策略配置。
|
|
62
|
+
|
|
63
|
+
read_roots: 允许读操作的路径根列表(read/glob/grep 等只读工具)。
|
|
64
|
+
空列表 = 不限制(开发模式)。
|
|
65
|
+
write_roots: 允许写操作的路径根列表(write/edit/bash 等写工具)。
|
|
66
|
+
通常是 read_roots 的子集。空列表 = 不限制(开发模式)。
|
|
67
|
+
deny_globs: 硬拒绝的 glob 模式列表,优先级高于 roots 检查。
|
|
68
|
+
对应 CC alwaysDenyRules。
|
|
69
|
+
示例:["**/secrets/**", "**/*.prod.env"]
|
|
70
|
+
ask_globs: 需要人工确认的敏感路径 glob 模式列表。
|
|
71
|
+
对应 CC alwaysAskRules。
|
|
72
|
+
headless 模式下自动降级为 deny;交互模式下触发 HITL。
|
|
73
|
+
示例:["**/config/**", "**/.claude/**"]
|
|
74
|
+
read_only_mode: 全局只读模式,is_destructive=True 的工具直接拒绝。
|
|
75
|
+
enable_builtin_deny: 启用内置危险路径保护(.env/.ssh/*.key/credentials 等)。
|
|
76
|
+
生产环境建议保持 True。
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
read_roots: list[str] = field(default_factory=list)
|
|
80
|
+
write_roots: list[str] = field(default_factory=list)
|
|
81
|
+
deny_globs: list[str] = field(default_factory=list)
|
|
82
|
+
ask_globs: list[str] = field(default_factory=list)
|
|
83
|
+
read_only_mode: bool = False
|
|
84
|
+
enable_builtin_deny: bool = True
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
# PolicyEngine 抽象基类
|
|
89
|
+
# ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
class PolicyEngine(ABC):
|
|
92
|
+
"""
|
|
93
|
+
权限策略引擎抽象基类。
|
|
94
|
+
|
|
95
|
+
由 RuntimeTool.check_permissions() 默认实现委托调用。
|
|
96
|
+
子类实现 authorize() / aauthorize() 提供具体策略决策。
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
@abstractmethod
|
|
100
|
+
def authorize(
|
|
101
|
+
self,
|
|
102
|
+
*,
|
|
103
|
+
tool_name: str,
|
|
104
|
+
input_data: dict[str, Any],
|
|
105
|
+
ctx: ToolExecutionContext,
|
|
106
|
+
) -> AuthorizationDecision:
|
|
107
|
+
"""同步授权决策。"""
|
|
108
|
+
...
|
|
109
|
+
|
|
110
|
+
async def aauthorize(
|
|
111
|
+
self,
|
|
112
|
+
*,
|
|
113
|
+
tool_name: str,
|
|
114
|
+
input_data: dict[str, Any],
|
|
115
|
+
ctx: ToolExecutionContext,
|
|
116
|
+
) -> AuthorizationDecision:
|
|
117
|
+
"""
|
|
118
|
+
异步授权决策,默认委托同步 authorize()。
|
|
119
|
+
有真正异步需求(如远程策略服务)的子类可覆盖。
|
|
120
|
+
"""
|
|
121
|
+
return self.authorize(tool_name=tool_name, input_data=input_data, ctx=ctx)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
# DefaultPolicyEngine — v1 实现
|
|
126
|
+
# ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
class DefaultPolicyEngine(PolicyEngine):
|
|
129
|
+
"""
|
|
130
|
+
v1.5 默认策略引擎。
|
|
131
|
+
|
|
132
|
+
决策优先级(严格顺序):
|
|
133
|
+
[1] read_only_mode + is_destructive → deny 最高优先级
|
|
134
|
+
[2] enable_builtin_deny → BUILTIN_DENY_GLOBS → deny
|
|
135
|
+
[3] deny_globs 命中 → deny
|
|
136
|
+
[4] ask_globs 命中 → ask(headless 时 pipeline 降级为 deny)
|
|
137
|
+
[5] 读写分权:
|
|
138
|
+
is_read_only=True → 检查 read_roots
|
|
139
|
+
is_read_only=False → 检查 write_roots
|
|
140
|
+
roots 为空列表 → 跳过,视为不限制
|
|
141
|
+
[6] 通过 → allow
|
|
142
|
+
|
|
143
|
+
对应 CC:
|
|
144
|
+
[1] ~ read_only_mode(全局禁止写操作)
|
|
145
|
+
[2][3] ~ alwaysDenyRules(固定拒绝)
|
|
146
|
+
[4] ~ alwaysAskRules(需确认)
|
|
147
|
+
[5] ~ allWorkingDirectories(访问空间限制)
|
|
148
|
+
|
|
149
|
+
工具读写标志来源:
|
|
150
|
+
从 ctx.tool_flags["is_read_only"] / ctx.tool_flags["is_destructive"] 读取。
|
|
151
|
+
由 LangChainAdapter.build_tool_execution_context() 在构造 ctx 时注入。
|
|
152
|
+
|
|
153
|
+
路径提取规则:
|
|
154
|
+
从 input_data 中按优先级查找路径字段:file_path > path > pattern
|
|
155
|
+
找不到路径字段时跳过路径相关检查(直接 allow)。
|
|
156
|
+
|
|
157
|
+
路径校验:
|
|
158
|
+
使用 realpath 双检(对 path 和 root 均做 realpath),
|
|
159
|
+
防路径遍历(../)、符号链接绕过、前缀误匹配。
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
# 按优先级尝试的路径字段名
|
|
163
|
+
_PATH_FIELDS = ("file_path", "path", "pattern")
|
|
164
|
+
|
|
165
|
+
# 内置危险路径黑名单,对应 CC 的危险目录/文件检查
|
|
166
|
+
BUILTIN_DENY_GLOBS: list[str] = [
|
|
167
|
+
# 环境变量 / 配置
|
|
168
|
+
"**/.env", "**/.env.*", "**/*.env",
|
|
169
|
+
# SSH 密钥
|
|
170
|
+
"**/id_rsa", "**/id_rsa.pub", "**/id_ed25519", "**/id_ed25519.pub",
|
|
171
|
+
"**/.ssh/**",
|
|
172
|
+
# 证书与密钥文件
|
|
173
|
+
"**/*.pem", "**/*.key", "**/*.p12", "**/*.pfx",
|
|
174
|
+
# 云服务凭证
|
|
175
|
+
"**/.aws/credentials", "**/.aws/config",
|
|
176
|
+
"**/.gcp/**", "**/service-account*.json",
|
|
177
|
+
# 密码/密钥目录
|
|
178
|
+
"**/secrets/**", "**/.secrets/**", "**/credentials/**",
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
def __init__(self, config: ToolPolicyConfig) -> None:
|
|
182
|
+
self._config = config
|
|
183
|
+
self._read_roots = [os.path.realpath(r) for r in config.read_roots]
|
|
184
|
+
self._write_roots = [os.path.realpath(r) for r in config.write_roots]
|
|
185
|
+
|
|
186
|
+
def authorize(
|
|
187
|
+
self,
|
|
188
|
+
*,
|
|
189
|
+
tool_name: str,
|
|
190
|
+
input_data: dict[str, Any],
|
|
191
|
+
ctx: ToolExecutionContext,
|
|
192
|
+
) -> AuthorizationDecision:
|
|
193
|
+
tool_flags = ctx.tool_flags if hasattr(ctx, "tool_flags") else {}
|
|
194
|
+
is_destructive = tool_flags.get("is_destructive", False)
|
|
195
|
+
is_read_only_tool = tool_flags.get("is_read_only", True)
|
|
196
|
+
active_auto_approved_tools = get_active_auto_approved_tools(
|
|
197
|
+
getattr(ctx, "session_store", None)
|
|
198
|
+
)
|
|
199
|
+
active_auto_approved_tools_lower = {
|
|
200
|
+
name.lower() for name in active_auto_approved_tools
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
# [1] 全局只读模式:拒绝破坏性工具
|
|
204
|
+
if self._config.read_only_mode and is_destructive:
|
|
205
|
+
return self._with_decision_source(
|
|
206
|
+
AuthorizationDecision(
|
|
207
|
+
behavior="deny",
|
|
208
|
+
message="Operation not permitted: agent is in read-only mode.",
|
|
209
|
+
policy_id="read_only_mode",
|
|
210
|
+
),
|
|
211
|
+
matched_rule="read_only_mode",
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# Bash 工具按“命令字符串”做规则匹配,不参与路径 roots 校验。
|
|
215
|
+
# 对齐 docs/design-docs/tool-design/bash-tool.md 的 v2 方案。
|
|
216
|
+
if tool_name == "bash":
|
|
217
|
+
command = input_data.get("command")
|
|
218
|
+
if not isinstance(command, str) or not command.strip():
|
|
219
|
+
return AuthorizationDecision(behavior="allow")
|
|
220
|
+
|
|
221
|
+
deny_result = self._check_deny_globs(command)
|
|
222
|
+
if deny_result is not None:
|
|
223
|
+
return self._with_decision_source(
|
|
224
|
+
deny_result,
|
|
225
|
+
matched_rule=deny_result.policy_id,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
ask_result = self._check_ask_globs(command)
|
|
229
|
+
if ask_result is not None:
|
|
230
|
+
return self._with_decision_source(
|
|
231
|
+
ask_result,
|
|
232
|
+
matched_rule=ask_result.policy_id,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
return self._with_decision_source(
|
|
236
|
+
AuthorizationDecision(behavior="allow"),
|
|
237
|
+
matched_rule="default_allow",
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
# 提取并规范化路径字段
|
|
241
|
+
path = self._extract_path(input_data)
|
|
242
|
+
if path is None:
|
|
243
|
+
return self._with_decision_source(
|
|
244
|
+
AuthorizationDecision(behavior="allow"),
|
|
245
|
+
matched_rule="no_path_field",
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
real_path = os.path.realpath(os.path.expanduser(path))
|
|
249
|
+
|
|
250
|
+
# [2] 用户自定义 deny_globs
|
|
251
|
+
deny_result = self._check_deny_globs(real_path)
|
|
252
|
+
if deny_result is not None:
|
|
253
|
+
return self._with_decision_source(
|
|
254
|
+
deny_result,
|
|
255
|
+
matched_rule=deny_result.policy_id,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
# [3] 内置危险路径黑名单
|
|
259
|
+
if self._config.enable_builtin_deny:
|
|
260
|
+
for pattern in self.BUILTIN_DENY_GLOBS:
|
|
261
|
+
if fnmatch.fnmatch(real_path, pattern):
|
|
262
|
+
return self._with_decision_source(
|
|
263
|
+
AuthorizationDecision(
|
|
264
|
+
behavior="deny",
|
|
265
|
+
message="Permission denied: access to this path is not allowed.",
|
|
266
|
+
policy_id="builtin_deny",
|
|
267
|
+
),
|
|
268
|
+
matched_rule="builtin_deny",
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
# [4] ask_globs:敏感路径需要人工确认
|
|
272
|
+
# headless 模式下 pipeline 会将 ask 自动降级为 deny;交互模式触发 HITL
|
|
273
|
+
ask_result = self._check_ask_globs(real_path)
|
|
274
|
+
if ask_result is not None:
|
|
275
|
+
if tool_name.lower() in active_auto_approved_tools_lower:
|
|
276
|
+
decision = AuthorizationDecision(
|
|
277
|
+
behavior="allow",
|
|
278
|
+
policy_id="skill_auto_approved_bypass_ask",
|
|
279
|
+
metadata={
|
|
280
|
+
"tool_name": tool_name,
|
|
281
|
+
"source": "skill_allowed_tools",
|
|
282
|
+
"decision_trace": [
|
|
283
|
+
{
|
|
284
|
+
"stage": "ask_globs",
|
|
285
|
+
"matched": True,
|
|
286
|
+
"decision": "allow",
|
|
287
|
+
"reason": "skill_auto_approved_bypass_ask",
|
|
288
|
+
"tool_name": tool_name,
|
|
289
|
+
"active_auto_approved_tools": sorted(active_auto_approved_tools),
|
|
290
|
+
}
|
|
291
|
+
],
|
|
292
|
+
},
|
|
293
|
+
)
|
|
294
|
+
return self._with_decision_source(
|
|
295
|
+
decision,
|
|
296
|
+
matched_rule="skill_auto_approved_bypass_ask",
|
|
297
|
+
)
|
|
298
|
+
return self._with_decision_source(
|
|
299
|
+
ask_result,
|
|
300
|
+
matched_rule=ask_result.policy_id,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
# [5] 读写分权根目录检查
|
|
304
|
+
roots = self._read_roots if is_read_only_tool else self._write_roots
|
|
305
|
+
if roots:
|
|
306
|
+
if not any(self._within_root(real_path, r) for r in roots):
|
|
307
|
+
return self._with_decision_source(
|
|
308
|
+
AuthorizationDecision(
|
|
309
|
+
behavior="deny",
|
|
310
|
+
message="Permission denied: path is outside the allowed workspace.",
|
|
311
|
+
policy_id="read_roots" if is_read_only_tool else "write_roots",
|
|
312
|
+
),
|
|
313
|
+
matched_rule="read_roots" if is_read_only_tool else "write_roots",
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
return self._with_decision_source(
|
|
317
|
+
AuthorizationDecision(behavior="allow"),
|
|
318
|
+
matched_rule="default_allow",
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
def authorize_path(
|
|
322
|
+
self,
|
|
323
|
+
path: str,
|
|
324
|
+
*,
|
|
325
|
+
is_write: bool,
|
|
326
|
+
) -> AuthorizationDecision:
|
|
327
|
+
"""
|
|
328
|
+
对单个文件系统路径做授权。
|
|
329
|
+
|
|
330
|
+
供 BashRuntimeTool v2 的路径安全层调用,避免 bash 因 command-string
|
|
331
|
+
特殊分支而跳过 read_roots / write_roots / builtin deny 检查。
|
|
332
|
+
"""
|
|
333
|
+
real_path = os.path.realpath(os.path.expanduser(path))
|
|
334
|
+
|
|
335
|
+
if self._config.read_only_mode and is_write:
|
|
336
|
+
return self._with_decision_source(
|
|
337
|
+
AuthorizationDecision(
|
|
338
|
+
behavior="deny",
|
|
339
|
+
message="Operation not permitted: agent is in read-only mode.",
|
|
340
|
+
policy_id="read_only_mode",
|
|
341
|
+
),
|
|
342
|
+
matched_rule="read_only_mode",
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
deny_result = self._check_deny_globs(real_path)
|
|
346
|
+
if deny_result is not None:
|
|
347
|
+
return self._with_decision_source(
|
|
348
|
+
deny_result,
|
|
349
|
+
matched_rule=deny_result.policy_id,
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
if self._config.enable_builtin_deny:
|
|
353
|
+
for pattern in self.BUILTIN_DENY_GLOBS:
|
|
354
|
+
if fnmatch.fnmatch(real_path, pattern):
|
|
355
|
+
return self._with_decision_source(
|
|
356
|
+
AuthorizationDecision(
|
|
357
|
+
behavior="deny",
|
|
358
|
+
message="Permission denied: access to this path is not allowed.",
|
|
359
|
+
policy_id="builtin_deny",
|
|
360
|
+
),
|
|
361
|
+
matched_rule="builtin_deny",
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
ask_result = self._check_ask_globs(real_path)
|
|
365
|
+
if ask_result is not None:
|
|
366
|
+
return self._with_decision_source(
|
|
367
|
+
ask_result,
|
|
368
|
+
matched_rule=ask_result.policy_id,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
roots = self._write_roots if is_write else self._read_roots
|
|
372
|
+
if roots:
|
|
373
|
+
if not any(self._within_root(real_path, root) for root in roots):
|
|
374
|
+
return self._with_decision_source(
|
|
375
|
+
AuthorizationDecision(
|
|
376
|
+
behavior="deny",
|
|
377
|
+
message="Permission denied: path is outside the allowed workspace.",
|
|
378
|
+
policy_id="write_roots" if is_write else "read_roots",
|
|
379
|
+
),
|
|
380
|
+
matched_rule="write_roots" if is_write else "read_roots",
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
return self._with_decision_source(
|
|
384
|
+
AuthorizationDecision(behavior="allow"),
|
|
385
|
+
matched_rule="default_allow",
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
# ---- 内部辅助 ----
|
|
389
|
+
|
|
390
|
+
def _extract_path(self, input_data: dict[str, Any]) -> str | None:
|
|
391
|
+
for field_name in self._PATH_FIELDS:
|
|
392
|
+
val = input_data.get(field_name)
|
|
393
|
+
if isinstance(val, str) and val:
|
|
394
|
+
return val
|
|
395
|
+
return None
|
|
396
|
+
|
|
397
|
+
def _check_deny_globs(self, real_path: str) -> AuthorizationDecision | None:
|
|
398
|
+
for glob_pattern in self._config.deny_globs:
|
|
399
|
+
if fnmatch.fnmatch(real_path, glob_pattern):
|
|
400
|
+
return AuthorizationDecision(
|
|
401
|
+
behavior="deny",
|
|
402
|
+
message="Permission denied: path matches a restricted pattern.",
|
|
403
|
+
policy_id=f"deny_glob:{glob_pattern}",
|
|
404
|
+
)
|
|
405
|
+
return None
|
|
406
|
+
|
|
407
|
+
@staticmethod
|
|
408
|
+
def _with_decision_source(
|
|
409
|
+
decision: AuthorizationDecision,
|
|
410
|
+
*,
|
|
411
|
+
matched_rule: str | None,
|
|
412
|
+
) -> AuthorizationDecision:
|
|
413
|
+
meta = dict(decision.metadata or {})
|
|
414
|
+
meta.setdefault("rule_source", "default_policy_engine")
|
|
415
|
+
if matched_rule is not None:
|
|
416
|
+
meta.setdefault("matched_rule", matched_rule)
|
|
417
|
+
decision.metadata = meta
|
|
418
|
+
return decision
|
|
419
|
+
|
|
420
|
+
def _check_ask_globs(self, real_path: str) -> AuthorizationDecision | None:
|
|
421
|
+
"""
|
|
422
|
+
检查路径是否命中 ask_globs(需要人工确认的敏感路径)。
|
|
423
|
+
命中时返回 behavior="ask",由 pipeline 根据 headless 标志决定后续处理:
|
|
424
|
+
headless=True → pipeline 自动降级为 deny
|
|
425
|
+
headless=False → pipeline 抛出 PermissionAskInterrupt
|
|
426
|
+
"""
|
|
427
|
+
for glob_pattern in self._config.ask_globs:
|
|
428
|
+
if fnmatch.fnmatch(real_path, glob_pattern):
|
|
429
|
+
return AuthorizationDecision(
|
|
430
|
+
behavior="ask",
|
|
431
|
+
ask_prompt=(
|
|
432
|
+
f"Tool wants to access a sensitive path: {real_path}\n"
|
|
433
|
+
f"(matched pattern: {glob_pattern})\n"
|
|
434
|
+
f"Allow this operation?"
|
|
435
|
+
),
|
|
436
|
+
policy_id=f"ask_glob:{glob_pattern}",
|
|
437
|
+
)
|
|
438
|
+
return None
|
|
439
|
+
|
|
440
|
+
@staticmethod
|
|
441
|
+
def _within_root(path: str, root: str) -> bool:
|
|
442
|
+
"""
|
|
443
|
+
判断 path 是否在 root 目录内。
|
|
444
|
+
root 已在 __init__ 中 realpath,path 在调用前已 realpath,
|
|
445
|
+
使用 os.sep 边界确保不会前缀误匹配(如 /proj 不会匹配 /proj_evil)。
|
|
446
|
+
"""
|
|
447
|
+
return path == root or path.startswith(root.rstrip(os.sep) + os.sep)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""
|
|
2
|
+
runtime/registry.py — 工具注册表
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
管理所有 RuntimeTool 的注册与查询。
|
|
6
|
+
register() 只存储 RuntimeTool,不构造 StructuredTool。
|
|
7
|
+
to_langchain_tools() 在需要时按需构造 StructuredTool;控制面由 RunnableConfig 注入。
|
|
8
|
+
|
|
9
|
+
与 CC 对比:
|
|
10
|
+
CC 中工具通过静态 TOOLS 数组集中声明,没有显式注册表。
|
|
11
|
+
本框架引入 RuntimeToolRegistry 作为运行时注册中心,支持动态注册。
|
|
12
|
+
StructuredTool 构造时序由 factory 控制(需 state 生命周期对齐),不在注册时提前构造。
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from langchain_core.tools import BaseTool
|
|
20
|
+
|
|
21
|
+
from .adapter import LangChainAdapter
|
|
22
|
+
from .base import RuntimeTool
|
|
23
|
+
from .errors import ToolNotFoundError, ToolRegistrationError
|
|
24
|
+
from .pipeline import ToolExecutorPipeline
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class RuntimeToolRegistry:
|
|
28
|
+
"""
|
|
29
|
+
工具注册表。
|
|
30
|
+
|
|
31
|
+
register(tool) — 注册 RuntimeTool,工具名重复时抛 ToolRegistrationError
|
|
32
|
+
get(name) — 按名查询 RuntimeTool,不存在时抛 ToolNotFoundError
|
|
33
|
+
list() — 返回所有已注册 RuntimeTool 列表
|
|
34
|
+
to_langchain_tools() — 按需构造 StructuredTool 列表
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
*,
|
|
40
|
+
pipeline: ToolExecutorPipeline,
|
|
41
|
+
adapter: LangChainAdapter,
|
|
42
|
+
) -> None:
|
|
43
|
+
self._pipeline = pipeline
|
|
44
|
+
self._adapter = adapter
|
|
45
|
+
self._tools: dict[str, RuntimeTool] = {}
|
|
46
|
+
|
|
47
|
+
def register(self, tool: RuntimeTool) -> None:
|
|
48
|
+
"""注册一个 RuntimeTool。工具名重复时抛 ToolRegistrationError。"""
|
|
49
|
+
if tool.name in self._tools:
|
|
50
|
+
raise ToolRegistrationError(tool.name)
|
|
51
|
+
self._tools[tool.name] = tool
|
|
52
|
+
|
|
53
|
+
def get(self, name: str) -> RuntimeTool:
|
|
54
|
+
"""按名查询 RuntimeTool。不存在时抛 ToolNotFoundError。"""
|
|
55
|
+
if name not in self._tools:
|
|
56
|
+
raise ToolNotFoundError(name)
|
|
57
|
+
return self._tools[name]
|
|
58
|
+
|
|
59
|
+
def list(self) -> list[RuntimeTool]:
|
|
60
|
+
"""返回所有已注册 RuntimeTool 列表(按注册顺序)。"""
|
|
61
|
+
return list(self._tools.values())
|
|
62
|
+
|
|
63
|
+
def to_langchain_tools(self) -> list[BaseTool]:
|
|
64
|
+
"""按需构造并返回所有 StructuredTool。"""
|
|
65
|
+
return [
|
|
66
|
+
self._adapter.build_structured_tool(
|
|
67
|
+
tool=t,
|
|
68
|
+
pipeline=self._pipeline,
|
|
69
|
+
)
|
|
70
|
+
for t in self._tools.values()
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
def __len__(self) -> int:
|
|
74
|
+
return len(self._tools)
|
|
75
|
+
|
|
76
|
+
def __contains__(self, name: str) -> bool:
|
|
77
|
+
return name in self._tools
|
|
78
|
+
|
|
79
|
+
def __repr__(self) -> str:
|
|
80
|
+
names = list(self._tools.keys())
|
|
81
|
+
return f"<RuntimeToolRegistry tools={names}>"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tool_runtime/resolvers/ — ask 分支权限 resolver 集合
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
汇总导出 PermissionResolver 协议与四类场景实现。
|
|
6
|
+
|
|
7
|
+
链路位置:
|
|
8
|
+
由会话/工作流装配层注入 ToolExecutorPipeline。
|
|
9
|
+
|
|
10
|
+
当前裁剪范围:
|
|
11
|
+
仅提供 resolver 定义与基础实现,不负责 UI 事件循环集成。
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from .agent_session import AgentSessionPermissionResolver, PermissionRequest
|
|
15
|
+
from .background import BackgroundPermissionResolver
|
|
16
|
+
from .base import PermissionResolver
|
|
17
|
+
from .conversation import ConversationPermissionResolver
|
|
18
|
+
from .workflow import WorkflowPermissionResolver
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"PermissionResolver",
|
|
22
|
+
"PermissionRequest",
|
|
23
|
+
"AgentSessionPermissionResolver",
|
|
24
|
+
"ConversationPermissionResolver",
|
|
25
|
+
"BackgroundPermissionResolver",
|
|
26
|
+
"WorkflowPermissionResolver",
|
|
27
|
+
]
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tool_runtime/resolvers/agent_session.py — CLI 会话权限确认 resolver
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
在同进程交互场景中通过 asyncio.Queue + Future 等待用户批准/拒绝。
|
|
6
|
+
|
|
7
|
+
链路位置:
|
|
8
|
+
AgentSession 注入 resolver -> ToolExecutorPipeline ask 分支 await。
|
|
9
|
+
|
|
10
|
+
当前裁剪范围:
|
|
11
|
+
仅实现单路 UI 队列确认,不实现 hook/classifier 多路 race。
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
import time
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from typing import Awaitable, Callable
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class PermissionRequest:
|
|
24
|
+
tool_name: str
|
|
25
|
+
ask_prompt: str | None
|
|
26
|
+
future: asyncio.Future[bool]
|
|
27
|
+
created_at: float = field(default_factory=time.monotonic)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class _OnceGuard:
|
|
31
|
+
"""多路 race 原子 claim,防止重复 resolve。"""
|
|
32
|
+
|
|
33
|
+
def __init__(self) -> None:
|
|
34
|
+
self._claimed = False
|
|
35
|
+
self._lock = asyncio.Lock()
|
|
36
|
+
|
|
37
|
+
async def claim(self) -> bool:
|
|
38
|
+
async with self._lock:
|
|
39
|
+
if self._claimed:
|
|
40
|
+
return False
|
|
41
|
+
self._claimed = True
|
|
42
|
+
return True
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class AgentSessionPermissionResolver:
|
|
46
|
+
"""CLI 交互 resolver:支持 UI future 与 hook 决策多路 race。"""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
request_queue: asyncio.Queue[PermissionRequest],
|
|
51
|
+
*,
|
|
52
|
+
timeout: float = 300.0,
|
|
53
|
+
hook_runner: Callable[[str, str | None], Awaitable[bool | None]] | None = None,
|
|
54
|
+
) -> None:
|
|
55
|
+
self._queue = request_queue
|
|
56
|
+
self._timeout = timeout
|
|
57
|
+
self._hook_runner = hook_runner
|
|
58
|
+
|
|
59
|
+
async def __call__(self, tool_name: str, ask_prompt: str | None) -> bool:
|
|
60
|
+
loop = asyncio.get_running_loop()
|
|
61
|
+
future: asyncio.Future[bool] = loop.create_future()
|
|
62
|
+
await self._queue.put(
|
|
63
|
+
PermissionRequest(
|
|
64
|
+
tool_name=tool_name,
|
|
65
|
+
ask_prompt=ask_prompt,
|
|
66
|
+
future=future,
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
guard = _OnceGuard()
|
|
70
|
+
ui_task = asyncio.create_task(self._await_ui_future(future, guard))
|
|
71
|
+
tasks: list[asyncio.Task[bool | None]] = [ui_task]
|
|
72
|
+
if self._hook_runner is not None:
|
|
73
|
+
tasks.append(asyncio.create_task(self._await_hook_decision(tool_name, ask_prompt, guard, future)))
|
|
74
|
+
try:
|
|
75
|
+
done, pending = await asyncio.wait(
|
|
76
|
+
tasks,
|
|
77
|
+
timeout=self._timeout,
|
|
78
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
79
|
+
)
|
|
80
|
+
if not done:
|
|
81
|
+
return False
|
|
82
|
+
result = done.pop().result()
|
|
83
|
+
return bool(result)
|
|
84
|
+
except asyncio.TimeoutError:
|
|
85
|
+
return False
|
|
86
|
+
finally:
|
|
87
|
+
for task in tasks:
|
|
88
|
+
if not task.done():
|
|
89
|
+
task.cancel()
|
|
90
|
+
if not future.done():
|
|
91
|
+
future.cancel()
|
|
92
|
+
|
|
93
|
+
async def _await_ui_future(self, future: asyncio.Future[bool], guard: _OnceGuard) -> bool | None:
|
|
94
|
+
try:
|
|
95
|
+
result = await asyncio.wait_for(future, timeout=self._timeout)
|
|
96
|
+
except asyncio.TimeoutError:
|
|
97
|
+
if await guard.claim():
|
|
98
|
+
return False
|
|
99
|
+
return None
|
|
100
|
+
except asyncio.CancelledError:
|
|
101
|
+
return None
|
|
102
|
+
if await guard.claim():
|
|
103
|
+
return result
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
async def _await_hook_decision(
|
|
107
|
+
self,
|
|
108
|
+
tool_name: str,
|
|
109
|
+
ask_prompt: str | None,
|
|
110
|
+
guard: _OnceGuard,
|
|
111
|
+
ui_future: asyncio.Future[bool],
|
|
112
|
+
) -> bool | None:
|
|
113
|
+
if self._hook_runner is None:
|
|
114
|
+
return None
|
|
115
|
+
hook_result = await self._hook_runner(tool_name, ask_prompt)
|
|
116
|
+
if hook_result is None:
|
|
117
|
+
return None
|
|
118
|
+
if await guard.claim():
|
|
119
|
+
if not ui_future.done():
|
|
120
|
+
ui_future.cancel()
|
|
121
|
+
return bool(hook_result)
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
__all__ = ["PermissionRequest", "AgentSessionPermissionResolver"]
|