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,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
bash_runtime_contract.py — Bash 跨平台运行时冻结契约(只读常量)
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
集中记录 Shell 环境变量解析顺序、cwd 双轨策略、Windows 持久会话默认值等
|
|
6
|
+
**与 CC/exec-plan 对齐的冻结项**,供 `shell_locator`、`BashBackend` 与 CR 对齐;
|
|
7
|
+
避免散落在多文件中的口头约定。
|
|
8
|
+
|
|
9
|
+
链路位置:
|
|
10
|
+
不参与 BashBackend 热路径;`shell_locator` 导入 **SHELL_ENV_PRECEDENCE**;
|
|
11
|
+
`BashBackend.__init__` 的 win32 持久会话策略与本模块 **WIN32_DEFAULT_USE_PERSISTENT_SESSION**
|
|
12
|
+
注释一致。
|
|
13
|
+
|
|
14
|
+
当前裁剪范围:
|
|
15
|
+
不包含 Shell 解析、Popen、cwd 文件或 fd3 实现;不包含对 CC 源码的导入。
|
|
16
|
+
|
|
17
|
+
设计 SSOT:
|
|
18
|
+
docs/design-docs/tool-design/bash-tool-cross-platform.md(§1.1–§1.4、§3.0)
|
|
19
|
+
|
|
20
|
+
CC 对照(实施前阅读,路径相对 CC 检出 src/):
|
|
21
|
+
utils/Shell.ts — findSuitableShell、spawn 与 SHELL 环境
|
|
22
|
+
utils/shell/bashProvider.ts — shellCwdFilePath / cwdFilePath 双轨
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
#: 显式覆盖 POSIX shell 可执行文件时,环境变量尝试顺序(先声明者优先;逐项校验见 SSOT §1.2)。
|
|
28
|
+
SHELL_ENV_PRECEDENCE: tuple[str, ...] = (
|
|
29
|
+
"LANGCHAIN_AGENTX_SHELL",
|
|
30
|
+
"CLAUDE_CODE_SHELL",
|
|
31
|
+
"SHELL",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
#: Linux/Unix 上 cwd 回传冻结策略(须保持 fd3 + pass_fds,不得未经 RFC 改为仅文件)。
|
|
35
|
+
CWD_STRATEGY_POSIX = "fd3_pass_fds"
|
|
36
|
+
|
|
37
|
+
#: Windows 上 cwd 回传目标策略(临时文件 + pwd -P;见 `cwd_reporter.py` / exec-plan)。
|
|
38
|
+
CWD_STRATEGY_WIN32 = "temp_file_pwd_p"
|
|
39
|
+
|
|
40
|
+
#: Windows 上 **默认** 关闭真持久 bash 会话(exec-plan:直至持久会话冒烟通过后再评估默认值)。
|
|
41
|
+
WIN32_DEFAULT_USE_PERSISTENT_SESSION = False
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cwd_reporter.py — Bash 执行后 cwd 回传(Unix fd3 / Windows 临时文件)
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
为 `BashBackend._execute_raw` / `_aexecute_raw` 生成包装脚本:Unix 用 **fd3 + pass_fds**;
|
|
6
|
+
Windows 用 **临时文件 + `pwd -P`**(禁止 pass_fds),与 CC `bashProvider` 双轨语义对齐。
|
|
7
|
+
|
|
8
|
+
链路位置:
|
|
9
|
+
仅被 `backend.py` 调用;不读工具入参、不碰权限链。
|
|
10
|
+
|
|
11
|
+
当前裁剪范围:
|
|
12
|
+
不处理流式 `_stream_execute_raw_iter`;不处理沙箱 backend 内部;UNC 临时目录尽力而为
|
|
13
|
+
(`native_path_for_bash_redirect` 覆盖常见 `//server/share` 形态)。
|
|
14
|
+
|
|
15
|
+
设计 SSOT:
|
|
16
|
+
docs/design-docs/tool-design/bash-tool-cross-platform.md(Phase 2)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def bash_single_quoted(s: str) -> str:
|
|
26
|
+
"""Bash 单引号字面量(可嵌入 `pwd -P >| ...` 等)。"""
|
|
27
|
+
return "'" + s.replace("'", "'\\''") + "'"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def native_path_for_bash_redirect(win_native: str) -> str:
|
|
31
|
+
"""
|
|
32
|
+
将 **本机绝对路径** 转为 Git Bash / MSYS 下用于重定向的 POSIX 形态。
|
|
33
|
+
|
|
34
|
+
- `C:\\Users\\x` → `/c/Users/x`
|
|
35
|
+
- `\\\\server\\share\\a` → `//server/share/a`
|
|
36
|
+
- 长路径前缀 `\\\\?\\` 会剥离后再转换(尽力而为)。
|
|
37
|
+
"""
|
|
38
|
+
p = Path(win_native).resolve()
|
|
39
|
+
s = os.fspath(p)
|
|
40
|
+
norm = s.replace("/", "\\")
|
|
41
|
+
if norm.startswith("\\\\?\\"):
|
|
42
|
+
norm = norm[4:]
|
|
43
|
+
if norm.upper().startswith("UNC\\"):
|
|
44
|
+
norm = "\\" + norm[3:]
|
|
45
|
+
if norm.startswith("\\\\"):
|
|
46
|
+
body = norm[2:].replace("\\", "/").strip("/")
|
|
47
|
+
return "//" + body if body else "//"
|
|
48
|
+
if len(norm) >= 2 and norm[1] == ":":
|
|
49
|
+
letter = norm[0].lower()
|
|
50
|
+
tail = norm[2:].replace("\\", "/")
|
|
51
|
+
if not tail.startswith("/"):
|
|
52
|
+
tail = "/" + tail.lstrip("/")
|
|
53
|
+
return f"/{letter}{tail}"
|
|
54
|
+
return norm.replace("\\", "/")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def read_cwd_file(native_path: str) -> str | None:
|
|
58
|
+
"""读取 `pwd -P` 写入的临时文件,返回最后一行非空 cwd,失败为 None。"""
|
|
59
|
+
try:
|
|
60
|
+
raw = Path(native_path).read_text(encoding="utf-8", errors="replace").strip()
|
|
61
|
+
except OSError:
|
|
62
|
+
return None
|
|
63
|
+
if not raw:
|
|
64
|
+
return None
|
|
65
|
+
lines = [ln.strip() for ln in raw.splitlines() if ln.strip()]
|
|
66
|
+
return lines[-1] if lines else None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class BashCwdReporter:
|
|
70
|
+
"""构造「执行用户命令 + 回写 cwd」的包装脚本(双平台)。"""
|
|
71
|
+
|
|
72
|
+
__slots__ = ()
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
def wrap_with_fd3(command: str, write_fd: int) -> str:
|
|
76
|
+
"""Unix:通过 fd3 写 `$PWD`(与既有 `backend._execute_raw` 一致)。"""
|
|
77
|
+
return (
|
|
78
|
+
f"exec 3>&{write_fd}\n"
|
|
79
|
+
f"{command}\n"
|
|
80
|
+
f'__EC=$?; echo "$PWD" >&3; exit $__EC'
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def wrap_with_cwd_file(command: str, posix_target_single_quoted: str) -> str:
|
|
85
|
+
"""
|
|
86
|
+
Windows:用户命令在同一 shell 内执行后 `pwd -P` 覆写到目标文件。
|
|
87
|
+
|
|
88
|
+
`posix_target_single_quoted` 须已用 `bash_single_quoted(...)` 包一层。
|
|
89
|
+
"""
|
|
90
|
+
return (
|
|
91
|
+
f"{command}\n"
|
|
92
|
+
f"__EC=$?\n"
|
|
93
|
+
f"pwd -P >| {posix_target_single_quoted}\n"
|
|
94
|
+
f"exit $__EC\n"
|
|
95
|
+
)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tools/bash/limits.py — BashRuntimeTool 上限配置
|
|
3
|
+
|
|
4
|
+
对应 CC prompt.ts 中的 getDefaultBashTimeoutMs() / getMaxBashTimeoutMs()
|
|
5
|
+
以及 BashTool.tsx 中的 maxResultSizeChars=30_000。
|
|
6
|
+
|
|
7
|
+
全部支持环境变量覆盖,便于测试 mock 和生产调优。
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
# 超时配置
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
DEFAULT_TIMEOUT_SEC = int(os.getenv("BASH_TOOL_DEFAULT_TIMEOUT", "120"))
|
|
19
|
+
"""默认超时秒数(对应 CC getDefaultBashTimeoutMs() = 120000ms)。"""
|
|
20
|
+
|
|
21
|
+
MAX_TIMEOUT_SEC = int(os.getenv("BASH_TOOL_MAX_TIMEOUT", "600"))
|
|
22
|
+
"""最大超时秒数(对应 CC getMaxBashTimeoutMs() = 600000ms / 10min)。"""
|
|
23
|
+
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
# 输出限制
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
MAX_OUTPUT_CHARS = int(os.getenv("BASH_TOOL_MAX_OUTPUT_CHARS", str(30_000)))
|
|
29
|
+
"""单次输出字符上限(对应 CC maxResultSizeChars=30_000)。"""
|
|
30
|
+
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
# 安全阈值
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
SLEEP_BLOCK_THRESHOLD_SEC = int(os.getenv("BASH_TOOL_SLEEP_BLOCK_SEC", "30"))
|
|
36
|
+
"""sleep 命令阻断阈值(秒)。超过此值提示使用 run_in_background(对应 CC detectBlockedSleepPattern)。"""
|
|
37
|
+
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
# 后台任务
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
BACKGROUND_TASK_OUTPUT_DIR = os.path.expanduser(
|
|
43
|
+
os.getenv(
|
|
44
|
+
"BASH_TOOL_BACKGROUND_OUTPUT_DIR",
|
|
45
|
+
"~/.cache/langchain_agentx/tasks",
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
"""后台任务输出目录。"""
|
|
49
|
+
|
|
50
|
+
AUTO_BACKGROUND_ENABLED = os.getenv("BASH_TOOL_AUTO_BACKGROUND_ENABLED", "1") == "1"
|
|
51
|
+
"""是否开启自动后台化策略。"""
|
|
52
|
+
|
|
53
|
+
AUTO_BACKGROUND_TIMEOUT_SEC = int(
|
|
54
|
+
os.getenv("BASH_TOOL_AUTO_BACKGROUND_TIMEOUT_SEC", "180")
|
|
55
|
+
)
|
|
56
|
+
"""触发自动后台化的 timeout 阈值(秒)。"""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def get_bash_limits() -> dict:
|
|
60
|
+
"""
|
|
61
|
+
返回当前有效配置(动态读取,支持运行时环境变量覆盖)。
|
|
62
|
+
"""
|
|
63
|
+
return {
|
|
64
|
+
"default_timeout_sec": DEFAULT_TIMEOUT_SEC,
|
|
65
|
+
"max_timeout_sec": MAX_TIMEOUT_SEC,
|
|
66
|
+
"max_output_chars": MAX_OUTPUT_CHARS,
|
|
67
|
+
"sleep_block_threshold_sec": SLEEP_BLOCK_THRESHOLD_SEC,
|
|
68
|
+
"background_task_output_dir": BACKGROUND_TASK_OUTPUT_DIR,
|
|
69
|
+
"auto_background_enabled": AUTO_BACKGROUND_ENABLED,
|
|
70
|
+
"auto_background_timeout_sec": AUTO_BACKGROUND_TIMEOUT_SEC,
|
|
71
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tools/bash/mode_validation.py — BashRuntimeTool:mode-aware 权限分流(P2 细粒度版)
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
与 CC `modeValidation.ts` 对齐方向,在命令级基础上增加:
|
|
6
|
+
- 子模式 / 参数语义(acceptEdits 何时可自动放行)
|
|
7
|
+
- 可解释结论(reason_code + message,便于归因与降误判)
|
|
8
|
+
|
|
9
|
+
当前实现:
|
|
10
|
+
- `acceptEdits`:按命令 + flag + 参数形态分流;显式拒绝高危 rm/chmod/chown 递归等
|
|
11
|
+
- `dontAsk`:将 ask 降级为 deny
|
|
12
|
+
- `bypassPermissions`:直接 allow(仅跳过权限层,不影响 validate_input)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
import shlex
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from typing import Literal
|
|
21
|
+
|
|
22
|
+
from langchain_agentx.tool_runtime.models import AuthorizationDecision, ToolExecutionContext
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
BashPermissionMode = Literal["default", "acceptEdits", "dontAsk", "bypassPermissions"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class BashModeDecision:
|
|
30
|
+
behavior: Literal["passthrough", "allow", "deny"]
|
|
31
|
+
message: str | None = None
|
|
32
|
+
policy_id: str | None = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class AcceptEditsEvaluation:
|
|
37
|
+
"""acceptEdits 细粒度评估结果(可对接日志 / 策略归因)。"""
|
|
38
|
+
|
|
39
|
+
allowed: bool
|
|
40
|
+
reason_code: str
|
|
41
|
+
message: str
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class BashPermissionModeValidator:
|
|
45
|
+
"""模式级权限协作者(P2:参数语义 + 场景约束)。"""
|
|
46
|
+
|
|
47
|
+
_ACCEPT_EDITS_ALLOWED_COMMANDS = frozenset({
|
|
48
|
+
"mkdir", "touch", "rm", "rmdir", "mv", "cp", "sed", "ln", "install",
|
|
49
|
+
"chmod", "chown",
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
_CHMOD_DENY_RECURSIVE = frozenset({"-R", "-r", "--recursive"})
|
|
53
|
+
_CHOWN_DENY_RECURSIVE = frozenset({"-R", "-r", "--recursive"})
|
|
54
|
+
_CP_DENY_FLAGS = frozenset({
|
|
55
|
+
"--reflink=always", "--parents", "--link", "-l", "--symbolic-link", "-s",
|
|
56
|
+
"--backup", "--remove-destination",
|
|
57
|
+
})
|
|
58
|
+
_MV_DENY_FLAGS = frozenset({"-f", "--force"})
|
|
59
|
+
_MKDIR_DENY_FLAGS = frozenset({"-Z", "--context"})
|
|
60
|
+
_INSTALL_DENY_FLAGS = frozenset({"-D", "--strip", "-s", "-S", "--strip-program"})
|
|
61
|
+
_CHOWN_DENY_FLAGS = frozenset({"--reference", "--from"})
|
|
62
|
+
|
|
63
|
+
def get_mode(self, ctx: ToolExecutionContext) -> BashPermissionMode:
|
|
64
|
+
tool_flags = ctx.tool_flags or {}
|
|
65
|
+
raw_mode = tool_flags.get("permission_mode", "default")
|
|
66
|
+
if raw_mode in {"acceptEdits", "dontAsk", "bypassPermissions"}:
|
|
67
|
+
return raw_mode
|
|
68
|
+
return "default"
|
|
69
|
+
|
|
70
|
+
def check_preflight(
|
|
71
|
+
self,
|
|
72
|
+
command: str,
|
|
73
|
+
ctx: ToolExecutionContext,
|
|
74
|
+
) -> BashModeDecision:
|
|
75
|
+
mode = self.get_mode(ctx)
|
|
76
|
+
if mode == "bypassPermissions":
|
|
77
|
+
return BashModeDecision(
|
|
78
|
+
behavior="allow",
|
|
79
|
+
message="Permission checks bypassed by current permission mode.",
|
|
80
|
+
policy_id="permission_mode",
|
|
81
|
+
)
|
|
82
|
+
return BashModeDecision(behavior="passthrough")
|
|
83
|
+
|
|
84
|
+
def evaluate_accept_edits(self, command: str) -> AcceptEditsEvaluation:
|
|
85
|
+
"""显式评估 acceptEdits 是否允许自动放行(供调试与扩展)。"""
|
|
86
|
+
base = self._base_command(command)
|
|
87
|
+
if base not in self._ACCEPT_EDITS_ALLOWED_COMMANDS:
|
|
88
|
+
return AcceptEditsEvaluation(
|
|
89
|
+
allowed=False,
|
|
90
|
+
reason_code="command_not_in_accept_edits_set",
|
|
91
|
+
message=f"Command `{base}` is not eligible for acceptEdits auto-allow.",
|
|
92
|
+
)
|
|
93
|
+
ok, code, msg = self._accept_edits_semantics(command, base)
|
|
94
|
+
return AcceptEditsEvaluation(allowed=ok, reason_code=code, message=msg)
|
|
95
|
+
|
|
96
|
+
def should_auto_allow_after_policy(
|
|
97
|
+
self,
|
|
98
|
+
command: str,
|
|
99
|
+
ctx: ToolExecutionContext,
|
|
100
|
+
) -> bool:
|
|
101
|
+
if self.get_mode(ctx) != "acceptEdits":
|
|
102
|
+
return False
|
|
103
|
+
return self.evaluate_accept_edits(command).allowed
|
|
104
|
+
|
|
105
|
+
def finalize_decision(
|
|
106
|
+
self,
|
|
107
|
+
*,
|
|
108
|
+
command: str,
|
|
109
|
+
decision: AuthorizationDecision,
|
|
110
|
+
ctx: ToolExecutionContext,
|
|
111
|
+
) -> AuthorizationDecision:
|
|
112
|
+
mode = self.get_mode(ctx)
|
|
113
|
+
if mode == "dontAsk" and decision.behavior == "ask":
|
|
114
|
+
detail = self.evaluate_accept_edits(command)
|
|
115
|
+
suffix = f" [acceptEdits_context: {detail.reason_code}]" if detail.reason_code else ""
|
|
116
|
+
message = (decision.message or f"Command requires approval in dontAsk mode: {command}") + suffix
|
|
117
|
+
return AuthorizationDecision(
|
|
118
|
+
behavior="deny",
|
|
119
|
+
message=message,
|
|
120
|
+
policy_id=decision.policy_id or "permission_mode",
|
|
121
|
+
)
|
|
122
|
+
return decision
|
|
123
|
+
|
|
124
|
+
def _accept_edits_semantics(self, command: str, base: str) -> tuple[bool, str, str]:
|
|
125
|
+
"""返回 (allowed, reason_code, message)。"""
|
|
126
|
+
try:
|
|
127
|
+
tokens = shlex.split(command)
|
|
128
|
+
except ValueError:
|
|
129
|
+
tokens = command.split()
|
|
130
|
+
args = tokens[1:] if len(tokens) > 1 else []
|
|
131
|
+
|
|
132
|
+
if base == "rm":
|
|
133
|
+
risky = {"-r", "-R", "-rf", "-fr", "--recursive", "--force"}
|
|
134
|
+
if any(a in risky for a in args):
|
|
135
|
+
return (False, "rm_recursive_or_force", "Recursive or force rm is not auto-allowed under acceptEdits.")
|
|
136
|
+
return (True, "rm_safe", "Single-file or small-arg rm eligible for acceptEdits.")
|
|
137
|
+
|
|
138
|
+
if base == "sed":
|
|
139
|
+
if any(arg.startswith("-i") or arg == "--in-place" for arg in args):
|
|
140
|
+
if self._sed_inplace_shell_chain(command):
|
|
141
|
+
return (False, "sed_inplace_shell_chain", "sed -i in compound shell chain not auto-allowed.")
|
|
142
|
+
return (True, "sed_inplace", "In-place sed eligible for acceptEdits.")
|
|
143
|
+
return (False, "sed_not_inplace", "Only in-place sed is auto-allowed under acceptEdits.")
|
|
144
|
+
|
|
145
|
+
if base == "cp":
|
|
146
|
+
if self._args_contain_token_or_flag(args, self._CP_DENY_FLAGS):
|
|
147
|
+
return (False, "cp_submode_denied", "cp with link/backup/parents/reflink semantics not auto-allowed.")
|
|
148
|
+
if self._cp_short_bundle_risky(args):
|
|
149
|
+
return (False, "cp_short_link_or_symlink", "cp -l/-s (含合并短选项) not auto-allowed.")
|
|
150
|
+
if self._cp_sparse_or_preserve_root(args):
|
|
151
|
+
return (False, "cp_sparse_or_preserve_root", "cp --sparse=always or --preserve=root not auto-allowed.")
|
|
152
|
+
return (True, "cp_default", "cp eligible for acceptEdits.")
|
|
153
|
+
|
|
154
|
+
if base == "mv":
|
|
155
|
+
if self._args_contain_token_or_flag(args, self._MV_DENY_FLAGS):
|
|
156
|
+
return (False, "mv_force", "mv -f/--force not auto-allowed under acceptEdits.")
|
|
157
|
+
if "--strip-trailing-slashes" in args:
|
|
158
|
+
return (False, "mv_strip_slashes", "mv --strip-trailing-slashes not auto-allowed.")
|
|
159
|
+
return (True, "mv_default", "mv eligible for acceptEdits.")
|
|
160
|
+
|
|
161
|
+
if base == "mkdir":
|
|
162
|
+
if self._args_contain_token_or_flag(args, self._MKDIR_DENY_FLAGS):
|
|
163
|
+
return (False, "mkdir_selinux_context", "mkdir -Z/--context not auto-allowed.")
|
|
164
|
+
if "-m" in args or "--mode" in args:
|
|
165
|
+
return (True, "mkdir_with_mode", "mkdir with explicit mode still eligible.")
|
|
166
|
+
return (True, "mkdir_default", "mkdir eligible for acceptEdits.")
|
|
167
|
+
|
|
168
|
+
if base == "touch":
|
|
169
|
+
return (True, "touch_default", "touch eligible for acceptEdits.")
|
|
170
|
+
|
|
171
|
+
if base == "rmdir":
|
|
172
|
+
if "--ignore-fail-on-non-empty" in args:
|
|
173
|
+
return (False, "rmdir_ignore_nonempty", "rmdir --ignore-fail-on-non-empty not auto-allowed.")
|
|
174
|
+
return (True, "rmdir_default", "rmdir eligible for acceptEdits.")
|
|
175
|
+
|
|
176
|
+
if base == "ln":
|
|
177
|
+
if "-r" in args or "--relative" in args:
|
|
178
|
+
return (False, "ln_relative", "ln --relative not auto-allowed.")
|
|
179
|
+
return (True, "ln_default", "ln eligible for acceptEdits.")
|
|
180
|
+
|
|
181
|
+
if base == "install":
|
|
182
|
+
if self._args_contain_token_or_flag(args, self._INSTALL_DENY_FLAGS):
|
|
183
|
+
return (False, "install_submode_denied", "install -D/strip or similar not auto-allowed.")
|
|
184
|
+
if "-d" in args or "--directory" in args:
|
|
185
|
+
return (True, "install_dirs", "install -d eligible.")
|
|
186
|
+
return (True, "install_default", "install file copy eligible for acceptEdits.")
|
|
187
|
+
|
|
188
|
+
if base == "chmod":
|
|
189
|
+
if any(a in self._CHMOD_DENY_RECURSIVE for a in args):
|
|
190
|
+
return (False, "chmod_recursive", "Recursive chmod not auto-allowed under acceptEdits.")
|
|
191
|
+
mode, paths = self._chmod_mode_and_paths(args)
|
|
192
|
+
if mode and self._chmod_mode_caps_setuid(mode):
|
|
193
|
+
return (False, "chmod_setuid_or_special", "chmod with setuid/setgid/sticky not auto-allowed.")
|
|
194
|
+
return (True, "chmod_non_recursive", "Non-recursive chmod eligible for acceptEdits.")
|
|
195
|
+
|
|
196
|
+
if base == "chown":
|
|
197
|
+
if any(a in self._CHOWN_DENY_RECURSIVE for a in args):
|
|
198
|
+
return (False, "chown_recursive", "Recursive chown not auto-allowed under acceptEdits.")
|
|
199
|
+
if self._args_contain_token_or_flag(args, self._CHOWN_DENY_FLAGS):
|
|
200
|
+
return (False, "chown_reference_or_from", "chown --reference/--from not auto-allowed.")
|
|
201
|
+
return (True, "chown_non_recursive", "Non-recursive chown eligible for acceptEdits.")
|
|
202
|
+
|
|
203
|
+
return (True, "generic_ok", f"{base} eligible for acceptEdits.")
|
|
204
|
+
|
|
205
|
+
@staticmethod
|
|
206
|
+
def _sed_inplace_shell_chain(command: str) -> bool:
|
|
207
|
+
"""与 `&&`、`|` 组合的 in-place sed 不自动放行(子模式边界)。"""
|
|
208
|
+
return "&&" in command or "|" in command
|
|
209
|
+
|
|
210
|
+
@staticmethod
|
|
211
|
+
def _args_contain_token_or_flag(args: list[str], needles: frozenset[str]) -> bool:
|
|
212
|
+
for a in args:
|
|
213
|
+
if a in needles:
|
|
214
|
+
return True
|
|
215
|
+
if a.startswith("--") and "=" in a:
|
|
216
|
+
opt = a.split("=", 1)[0]
|
|
217
|
+
if opt in needles or a in needles:
|
|
218
|
+
return True
|
|
219
|
+
return False
|
|
220
|
+
|
|
221
|
+
@staticmethod
|
|
222
|
+
def _cp_sparse_or_preserve_root(args: list[str]) -> bool:
|
|
223
|
+
for a in args:
|
|
224
|
+
if a.startswith("--sparse=") and a != "--sparse=never":
|
|
225
|
+
return True
|
|
226
|
+
if a == "--preserve=root":
|
|
227
|
+
return True
|
|
228
|
+
return False
|
|
229
|
+
|
|
230
|
+
@staticmethod
|
|
231
|
+
def _cp_short_bundle_risky(args: list[str]) -> bool:
|
|
232
|
+
for a in args:
|
|
233
|
+
if not a.startswith("-") or a.startswith("--") or len(a) < 2:
|
|
234
|
+
continue
|
|
235
|
+
body = a.lstrip("-")
|
|
236
|
+
if "l" in body or "s" in body:
|
|
237
|
+
return True
|
|
238
|
+
return False
|
|
239
|
+
|
|
240
|
+
@staticmethod
|
|
241
|
+
def _chmod_mode_and_paths(args: list[str]) -> tuple[str | None, list[str]]:
|
|
242
|
+
"""从 chmod 参数中取出 mode 字串(若存在)与路径候选(启发式)。"""
|
|
243
|
+
mode: str | None = None
|
|
244
|
+
paths: list[str] = []
|
|
245
|
+
i = 0
|
|
246
|
+
while i < len(args):
|
|
247
|
+
a = args[i]
|
|
248
|
+
if a in ("-f", "--silent", "--quiet", "-v", "--verbose", "-c", "--changes", "-R", "-r", "--recursive"):
|
|
249
|
+
i += 1
|
|
250
|
+
continue
|
|
251
|
+
if a.startswith("-"):
|
|
252
|
+
i += 1
|
|
253
|
+
continue
|
|
254
|
+
if mode is None and re.match(r"^[ugoa]*[-+=]?[rwxXst,0-7]+$", a):
|
|
255
|
+
mode = a
|
|
256
|
+
i += 1
|
|
257
|
+
continue
|
|
258
|
+
paths.append(a)
|
|
259
|
+
i += 1
|
|
260
|
+
return mode, paths
|
|
261
|
+
|
|
262
|
+
@staticmethod
|
|
263
|
+
def _chmod_mode_caps_setuid(mode: str) -> bool:
|
|
264
|
+
if re.fullmatch(r"[0-7]{3,4}", mode):
|
|
265
|
+
try:
|
|
266
|
+
val = int(mode, 8)
|
|
267
|
+
except ValueError:
|
|
268
|
+
return False
|
|
269
|
+
return (val & 0o6000) != 0
|
|
270
|
+
if any(x in mode for x in ("s", "t", "T")) and ("+" in mode or "-" in mode or "=" in mode):
|
|
271
|
+
return True
|
|
272
|
+
return False
|
|
273
|
+
|
|
274
|
+
@staticmethod
|
|
275
|
+
def _base_command(command: str) -> str:
|
|
276
|
+
try:
|
|
277
|
+
tokens = shlex.split(command)
|
|
278
|
+
except ValueError:
|
|
279
|
+
tokens = command.split()
|
|
280
|
+
if not tokens:
|
|
281
|
+
return ""
|
|
282
|
+
return tokens[0].rsplit("/", 1)[-1]
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tools/bash/models.py — BashRuntimeTool 输入输出数据模型
|
|
3
|
+
|
|
4
|
+
对应 CC BashTool.tsx 的 inputSchema / outputSchema。
|
|
5
|
+
纯 Pydantic 数据模型,无 I/O,无框架依赖。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, Field
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BashToolInput(BaseModel):
|
|
16
|
+
"""
|
|
17
|
+
BashRuntimeTool 输入参数。
|
|
18
|
+
|
|
19
|
+
对应 CC inputSchema。
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
command: str = Field(
|
|
23
|
+
...,
|
|
24
|
+
description="The bash command to execute",
|
|
25
|
+
)
|
|
26
|
+
timeout: int | None = Field(
|
|
27
|
+
None,
|
|
28
|
+
gt=0,
|
|
29
|
+
description=(
|
|
30
|
+
"Optional timeout in seconds (max 600). "
|
|
31
|
+
"Defaults to 120 seconds if not specified."
|
|
32
|
+
),
|
|
33
|
+
)
|
|
34
|
+
description: str | None = Field(
|
|
35
|
+
None,
|
|
36
|
+
description=(
|
|
37
|
+
"Clear, concise description of what this command does in active voice. "
|
|
38
|
+
"For simple commands keep it brief (5-10 words). "
|
|
39
|
+
"For complex or piped commands add enough context to clarify intent."
|
|
40
|
+
),
|
|
41
|
+
)
|
|
42
|
+
run_in_background: bool = Field(
|
|
43
|
+
False,
|
|
44
|
+
description=(
|
|
45
|
+
"Set to true to run this command in the background. "
|
|
46
|
+
"Use this when you don't need the result immediately. "
|
|
47
|
+
"A task_id is returned; output is written to a file you can read later."
|
|
48
|
+
),
|
|
49
|
+
)
|
|
50
|
+
dangerously_disable_sandbox: bool = Field(
|
|
51
|
+
False,
|
|
52
|
+
description=(
|
|
53
|
+
"Request to bypass sandbox execution for this command. "
|
|
54
|
+
"This only takes effect when the runtime explicitly allows unsandboxed execution."
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class BashToolOutput(BaseModel):
|
|
60
|
+
"""
|
|
61
|
+
BashRuntimeTool 内部执行结果(对应 CC outputSchema)。
|
|
62
|
+
|
|
63
|
+
由 invoke() 返回,传递给 after_invoke() 和 present()。
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
stdout: str
|
|
67
|
+
"""命令输出(stderr 已合并进 stdout)。"""
|
|
68
|
+
|
|
69
|
+
interrupted: bool
|
|
70
|
+
"""是否因超时或外部中断而提前终止。"""
|
|
71
|
+
|
|
72
|
+
exit_code: int
|
|
73
|
+
"""命令退出码。"""
|
|
74
|
+
|
|
75
|
+
background_task_id: str | None = None
|
|
76
|
+
"""后台任务 ID(仅 run_in_background=True 时有值)。"""
|
|
77
|
+
|
|
78
|
+
background_output_path: str | None = None
|
|
79
|
+
"""后台任务输出文件路径(模型可用 Read 工具读取)。"""
|
|
80
|
+
|
|
81
|
+
task_mode: str = "foreground"
|
|
82
|
+
"""任务模式:foreground / background。"""
|
|
83
|
+
|
|
84
|
+
task_status: str = "completed"
|
|
85
|
+
"""任务状态:running / completed / failed / interrupted。"""
|
|
86
|
+
|
|
87
|
+
task_exit_code: int | None = None
|
|
88
|
+
"""统一任务协议退出码(前后台共用)。"""
|
|
89
|
+
|
|
90
|
+
auto_backgrounded: bool = False
|
|
91
|
+
"""是否由自动后台化策略触发。"""
|
|
92
|
+
|
|
93
|
+
return_code_interpretation: str | None = None
|
|
94
|
+
"""
|
|
95
|
+
特殊退出码的语义解释。
|
|
96
|
+
例如 grep 返回 1 = 无匹配(非错误),会在此字段给出说明。
|
|
97
|
+
对应 CC returnCodeInterpretation。
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
no_output_expected: bool = False
|
|
101
|
+
"""
|
|
102
|
+
命令预期不产生输出(mv/cp/rm 等),成功时 present() 显示 "Done"。
|
|
103
|
+
对应 CC noOutputExpected。
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
destructive_warning: str | None = None
|
|
107
|
+
"""
|
|
108
|
+
危险命令警告文本(非阻断)。
|
|
109
|
+
detect_destructive_patterns() 检测到危险模式时设置,在 present() 中注入到 hints。
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
sandboxed: bool = False
|
|
113
|
+
"""本次命令是否在沙箱模式下执行。"""
|
|
114
|
+
|
|
115
|
+
sandbox_bypass_reason: str | None = None
|
|
116
|
+
"""未走沙箱时的原因说明(如 excluded command / explicit override)。"""
|
|
117
|
+
|
|
118
|
+
sandbox_temp_dir: str | None = None
|
|
119
|
+
"""沙箱注入的 TMPDIR 路径(基础版沙箱使用独立临时目录)。"""
|
|
120
|
+
|
|
121
|
+
sandbox_violation_message: str | None = None
|
|
122
|
+
"""沙箱执行返回的结构化 violation 信息。"""
|
|
123
|
+
|
|
124
|
+
overflow_file: str | None = None
|
|
125
|
+
"""超大 stdout 落盘路径(完整内容),与 output_truncated 配合使用。"""
|
|
126
|
+
|
|
127
|
+
output_truncated: bool = False
|
|
128
|
+
"""stdout 是否已截断,完整内容见 overflow_file。"""
|
|
129
|
+
|
|
130
|
+
observability: dict[str, Any] | None = None
|
|
131
|
+
"""结构化可观测数据(事件摘要/决策归因),供 UI/CLI/日志多端消费。"""
|