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,939 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tools/bash/tool.py — BashRuntimeTool 主类
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
实现 RuntimeTool 的所有生命周期 hook,编排命令执行完整流程。
|
|
6
|
+
核心 I/O 委托给 BashBackend,安全检查来自 security.py,
|
|
7
|
+
语义解析来自 semantics.py,配置来自 limits.py。
|
|
8
|
+
|
|
9
|
+
对应 CC BashTool.tsx 主体逻辑(call() + validateInput() + checkPermissions())。
|
|
10
|
+
present() 对应 CC UI.tsx 的 renderToolResultMessage()。
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import replace
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
import hashlib
|
|
18
|
+
import os
|
|
19
|
+
import tempfile
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from langchain_agentx.tool_runtime.base import RuntimeTool
|
|
23
|
+
from langchain_agentx.tool_runtime.models import (
|
|
24
|
+
AuthorizationDecision,
|
|
25
|
+
ToolExecutionContext,
|
|
26
|
+
ToolResultEnvelope,
|
|
27
|
+
ValidationResult,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
from .ast_security import (
|
|
31
|
+
BashAstAnalyzer,
|
|
32
|
+
)
|
|
33
|
+
from .bash_hardening import BashSecurityHardener
|
|
34
|
+
from .backend import BashBackend, ExecuteResult, strip_empty_lines
|
|
35
|
+
from .limits import get_bash_limits
|
|
36
|
+
from .mode_validation import BashPermissionModeValidator
|
|
37
|
+
from .models import BashToolInput, BashToolOutput
|
|
38
|
+
from .observability import (
|
|
39
|
+
BashEventBuffer,
|
|
40
|
+
TraceContext,
|
|
41
|
+
build_decision_observability,
|
|
42
|
+
finalize_observability_payload,
|
|
43
|
+
)
|
|
44
|
+
from .path_security import BashPathSecurityAnalyzer
|
|
45
|
+
from .prompt import DESCRIPTION, SILENT_COMMANDS, TOOL_NAME
|
|
46
|
+
from .read_only_validation import BashReadOnlyClassifier
|
|
47
|
+
from .result_presenter import BashResultPresenter
|
|
48
|
+
from .sandbox_decision import BashSandboxDecisionEngine
|
|
49
|
+
from .sed_edit_parser import BashSedEditParser
|
|
50
|
+
from .sed_validation import BashSedCommandValidator
|
|
51
|
+
from .security import (
|
|
52
|
+
detect_destructive_patterns,
|
|
53
|
+
detect_infinite_output,
|
|
54
|
+
detect_interactive_command,
|
|
55
|
+
detect_sleep_block,
|
|
56
|
+
)
|
|
57
|
+
from .semantics import interpret_command_result
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _as_bash_input_dict(data: dict[str, Any] | BashToolInput) -> dict[str, Any]:
|
|
61
|
+
"""兼容层:统一将工具入参归一化为 dict。"""
|
|
62
|
+
if isinstance(data, dict):
|
|
63
|
+
return data
|
|
64
|
+
if isinstance(data, BashToolInput):
|
|
65
|
+
return data.model_dump()
|
|
66
|
+
msg = f"Unsupported bash tool input type: {type(data)!r}"
|
|
67
|
+
raise TypeError(msg)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class BashRuntimeTool(RuntimeTool):
|
|
71
|
+
"""
|
|
72
|
+
Shell 命令执行工具。
|
|
73
|
+
|
|
74
|
+
对应 CC BashTool(BashTool.tsx)。
|
|
75
|
+
|
|
76
|
+
特性:
|
|
77
|
+
is_destructive=True 具有写副作用,通过 PolicyEngine 权限检查
|
|
78
|
+
is_read_only=False 保守默认;动态判断只读命令
|
|
79
|
+
is_concurrency_safe=False 同一 shell 有 cwd 状态,不允许并发
|
|
80
|
+
never_truncate=False 走 ToolOutputManager 截断(max 30KB)
|
|
81
|
+
max_result_size_chars 对应 CC maxResultSizeChars=30_000
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
name: str = TOOL_NAME
|
|
85
|
+
description: str = DESCRIPTION
|
|
86
|
+
input_model = BashToolInput
|
|
87
|
+
is_destructive: bool = True
|
|
88
|
+
is_concurrency_safe: bool = False
|
|
89
|
+
never_truncate: bool = False
|
|
90
|
+
max_result_size_chars: int = 30_000
|
|
91
|
+
|
|
92
|
+
def __init__(
|
|
93
|
+
self,
|
|
94
|
+
*,
|
|
95
|
+
policy: Any | None = None,
|
|
96
|
+
state_bridge: Any | None = None,
|
|
97
|
+
backend: BashBackend | None = None,
|
|
98
|
+
ast_analyzer: BashAstAnalyzer | None = None,
|
|
99
|
+
path_security: BashPathSecurityAnalyzer | None = None,
|
|
100
|
+
read_only_classifier: BashReadOnlyClassifier | None = None,
|
|
101
|
+
mode_validator: BashPermissionModeValidator | None = None,
|
|
102
|
+
sed_validator: BashSedCommandValidator | None = None,
|
|
103
|
+
sed_parser: BashSedEditParser | None = None,
|
|
104
|
+
security_hardener: BashSecurityHardener | None = None,
|
|
105
|
+
sandbox_decider: BashSandboxDecisionEngine | None = None,
|
|
106
|
+
result_presenter: BashResultPresenter | None = None,
|
|
107
|
+
) -> None:
|
|
108
|
+
super().__init__(policy=policy, state_bridge=state_bridge)
|
|
109
|
+
self._backend = backend or BashBackend()
|
|
110
|
+
self._ast_analyzer = ast_analyzer or BashAstAnalyzer()
|
|
111
|
+
self._path_security = path_security or BashPathSecurityAnalyzer()
|
|
112
|
+
self._read_only_classifier = read_only_classifier or BashReadOnlyClassifier()
|
|
113
|
+
self._mode_validator = mode_validator or BashPermissionModeValidator()
|
|
114
|
+
self._sed_parser = sed_parser or BashSedEditParser()
|
|
115
|
+
self._sed_validator = sed_validator or BashSedCommandValidator(self._sed_parser)
|
|
116
|
+
self._security_hardener = security_hardener or BashSecurityHardener()
|
|
117
|
+
self._sandbox_decider = sandbox_decider or BashSandboxDecisionEngine()
|
|
118
|
+
self._result_presenter = result_presenter or BashResultPresenter()
|
|
119
|
+
|
|
120
|
+
# ------------------------------------------------------------------
|
|
121
|
+
# Hook 1: normalize_input
|
|
122
|
+
# ------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
def normalize_input(
|
|
125
|
+
self, raw: dict[str, Any], ctx: ToolExecutionContext
|
|
126
|
+
) -> dict[str, Any]:
|
|
127
|
+
"""
|
|
128
|
+
输入预处理:
|
|
129
|
+
- timeout 值 clamp 到最大值
|
|
130
|
+
- command 首尾空白清理
|
|
131
|
+
|
|
132
|
+
对应 CC backfillObservableInput() 的 timeout 处理。
|
|
133
|
+
"""
|
|
134
|
+
raw = dict(raw)
|
|
135
|
+
limits = get_bash_limits()
|
|
136
|
+
|
|
137
|
+
# timeout clamp
|
|
138
|
+
if raw.get("timeout") is not None:
|
|
139
|
+
raw["timeout"] = min(int(raw["timeout"]), limits["max_timeout_sec"])
|
|
140
|
+
|
|
141
|
+
# 清理命令首尾空白(模型有时会多加换行)
|
|
142
|
+
if raw.get("command"):
|
|
143
|
+
raw["command"] = raw["command"].strip()
|
|
144
|
+
|
|
145
|
+
if "dangerouslyDisableSandbox" in raw and "dangerously_disable_sandbox" not in raw:
|
|
146
|
+
raw["dangerously_disable_sandbox"] = bool(raw["dangerouslyDisableSandbox"])
|
|
147
|
+
|
|
148
|
+
return raw
|
|
149
|
+
|
|
150
|
+
# ------------------------------------------------------------------
|
|
151
|
+
# Hook 3: validate_input
|
|
152
|
+
# ------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
def validate_input(
|
|
155
|
+
self, data: dict[str, Any], ctx: ToolExecutionContext
|
|
156
|
+
) -> ValidationResult:
|
|
157
|
+
"""
|
|
158
|
+
语义校验(对应 CC validateInput() + checkPermissionMode() 前置检查)。
|
|
159
|
+
|
|
160
|
+
检查顺序(快速失败原则):
|
|
161
|
+
[1] 空命令检查
|
|
162
|
+
[2] sleep 大值检测(提示使用后台)
|
|
163
|
+
[3] 交互式命令检测(需要 TTY)
|
|
164
|
+
[4] 无限输出命令检测
|
|
165
|
+
"""
|
|
166
|
+
input_data = _as_bash_input_dict(data)
|
|
167
|
+
command = str(input_data.get("command", ""))
|
|
168
|
+
limits = get_bash_limits()
|
|
169
|
+
|
|
170
|
+
# [1] 空命令
|
|
171
|
+
if not command.strip():
|
|
172
|
+
return ValidationResult(ok=False, message="Command cannot be empty.")
|
|
173
|
+
|
|
174
|
+
# [2] sleep 大值检测
|
|
175
|
+
sleep_sec = detect_sleep_block(command, limits["sleep_block_threshold_sec"])
|
|
176
|
+
if sleep_sec is not None:
|
|
177
|
+
threshold = limits["sleep_block_threshold_sec"]
|
|
178
|
+
return ValidationResult(
|
|
179
|
+
ok=False,
|
|
180
|
+
message=(
|
|
181
|
+
f"Blocked: sleep {sleep_sec}s would block for too long "
|
|
182
|
+
f"(threshold: {threshold}s). "
|
|
183
|
+
"Use run_in_background=true for long-running commands, "
|
|
184
|
+
f"or keep sleep under {threshold} seconds."
|
|
185
|
+
),
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# [3] 交互式命令检测
|
|
189
|
+
interactive = detect_interactive_command(command)
|
|
190
|
+
if interactive:
|
|
191
|
+
return ValidationResult(
|
|
192
|
+
ok=False,
|
|
193
|
+
message=(
|
|
194
|
+
f"Cannot run interactive command '{interactive}': "
|
|
195
|
+
"it requires a TTY which is not available in this environment. "
|
|
196
|
+
"Use non-interactive alternatives instead."
|
|
197
|
+
),
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# [4] 无限输出检测
|
|
201
|
+
if detect_infinite_output(command):
|
|
202
|
+
return ValidationResult(
|
|
203
|
+
ok=False,
|
|
204
|
+
message=(
|
|
205
|
+
"This command would produce infinite output. "
|
|
206
|
+
"If you need to capture output, add a size limit (e.g. head -n 100) "
|
|
207
|
+
"or use run_in_background=true with a timeout."
|
|
208
|
+
),
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
return ValidationResult(ok=True)
|
|
212
|
+
|
|
213
|
+
# ------------------------------------------------------------------
|
|
214
|
+
# Hook 4: check_permissions
|
|
215
|
+
# ------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
def check_permissions(
|
|
218
|
+
self, data: dict[str, Any], ctx: ToolExecutionContext
|
|
219
|
+
) -> AuthorizationDecision:
|
|
220
|
+
"""
|
|
221
|
+
Bash 权限检查。
|
|
222
|
+
|
|
223
|
+
v2 对照 CC `checkCommandOperatorPermissions()`:
|
|
224
|
+
- 先用 AST 识别高风险 shell 结构(subshell / command substitution / nested shell)
|
|
225
|
+
- 再把顶层 pipeline / list 拆成 segment,逐段做权限决策
|
|
226
|
+
- 每个 segment 动态计算 `is_read_only` / `is_destructive`,让 read_only_mode 生效
|
|
227
|
+
|
|
228
|
+
对应 CC bashToolHasPermission() 的规则匹配层([8] 规则匹配)。
|
|
229
|
+
"""
|
|
230
|
+
trace: list[dict[str, Any]] = []
|
|
231
|
+
input_data = _as_bash_input_dict(data)
|
|
232
|
+
command = str(input_data.get("command", ""))
|
|
233
|
+
|
|
234
|
+
preflight_mode_decision = self._mode_validator.check_preflight(command, ctx)
|
|
235
|
+
trace.append(
|
|
236
|
+
{
|
|
237
|
+
"layer": "mode_preflight",
|
|
238
|
+
"command": command,
|
|
239
|
+
"behavior": preflight_mode_decision.behavior,
|
|
240
|
+
"policy_id": preflight_mode_decision.policy_id,
|
|
241
|
+
"message": preflight_mode_decision.message,
|
|
242
|
+
}
|
|
243
|
+
)
|
|
244
|
+
if preflight_mode_decision.behavior == "allow":
|
|
245
|
+
return self._with_observability(
|
|
246
|
+
AuthorizationDecision(
|
|
247
|
+
behavior="allow",
|
|
248
|
+
message=preflight_mode_decision.message,
|
|
249
|
+
policy_id=preflight_mode_decision.policy_id,
|
|
250
|
+
),
|
|
251
|
+
trace=trace,
|
|
252
|
+
trace_context=TraceContext.from_ctx(ctx),
|
|
253
|
+
)
|
|
254
|
+
if preflight_mode_decision.behavior == "deny":
|
|
255
|
+
return self._with_observability(
|
|
256
|
+
AuthorizationDecision(
|
|
257
|
+
behavior="deny",
|
|
258
|
+
message=preflight_mode_decision.message,
|
|
259
|
+
policy_id=preflight_mode_decision.policy_id,
|
|
260
|
+
),
|
|
261
|
+
trace=trace,
|
|
262
|
+
trace_context=TraceContext.from_ctx(ctx),
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
current_cwd = self._get_current_cwd()
|
|
266
|
+
analysis = self._ast_analyzer.analyze(command)
|
|
267
|
+
hardening_decision = self._security_hardener.check_command(
|
|
268
|
+
command=command,
|
|
269
|
+
analysis=analysis,
|
|
270
|
+
cwd=current_cwd,
|
|
271
|
+
)
|
|
272
|
+
trace.append(
|
|
273
|
+
{
|
|
274
|
+
"layer": "hardening_command",
|
|
275
|
+
"command": command,
|
|
276
|
+
"behavior": hardening_decision.behavior if hardening_decision else "allow",
|
|
277
|
+
"policy_id": hardening_decision.policy_id if hardening_decision else None,
|
|
278
|
+
}
|
|
279
|
+
)
|
|
280
|
+
if hardening_decision is not None:
|
|
281
|
+
decision = self._mode_validator.finalize_decision(
|
|
282
|
+
command=command,
|
|
283
|
+
ctx=ctx,
|
|
284
|
+
decision=hardening_decision,
|
|
285
|
+
)
|
|
286
|
+
trace.append(
|
|
287
|
+
{
|
|
288
|
+
"layer": "mode_finalize",
|
|
289
|
+
"command": command,
|
|
290
|
+
"behavior": decision.behavior,
|
|
291
|
+
"policy_id": decision.policy_id,
|
|
292
|
+
}
|
|
293
|
+
)
|
|
294
|
+
return self._with_observability(
|
|
295
|
+
decision,
|
|
296
|
+
trace=trace,
|
|
297
|
+
trace_context=TraceContext.from_ctx(ctx),
|
|
298
|
+
)
|
|
299
|
+
approval_reason = self._ast_analyzer.get_approval_reason(analysis)
|
|
300
|
+
if approval_reason:
|
|
301
|
+
decision = self._mode_validator.finalize_decision(
|
|
302
|
+
command=command,
|
|
303
|
+
ctx=ctx,
|
|
304
|
+
decision=AuthorizationDecision(
|
|
305
|
+
behavior="ask",
|
|
306
|
+
message=approval_reason,
|
|
307
|
+
policy_id="bash_ast_v2",
|
|
308
|
+
ask_prompt=approval_reason,
|
|
309
|
+
),
|
|
310
|
+
)
|
|
311
|
+
trace.append(
|
|
312
|
+
{
|
|
313
|
+
"layer": "ast_guard",
|
|
314
|
+
"command": command,
|
|
315
|
+
"behavior": "ask",
|
|
316
|
+
"policy_id": "bash_ast_v2",
|
|
317
|
+
}
|
|
318
|
+
)
|
|
319
|
+
trace.append(
|
|
320
|
+
{
|
|
321
|
+
"layer": "mode_finalize",
|
|
322
|
+
"command": command,
|
|
323
|
+
"behavior": decision.behavior,
|
|
324
|
+
"policy_id": decision.policy_id,
|
|
325
|
+
}
|
|
326
|
+
)
|
|
327
|
+
return self._with_observability(
|
|
328
|
+
decision,
|
|
329
|
+
trace=trace,
|
|
330
|
+
trace_context=TraceContext.from_ctx(ctx),
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
segments = [
|
|
334
|
+
segment.strip()
|
|
335
|
+
for segment in analysis.permission_segments
|
|
336
|
+
if segment and segment.strip()
|
|
337
|
+
]
|
|
338
|
+
if not segments:
|
|
339
|
+
decision = self._mode_validator.finalize_decision(
|
|
340
|
+
command=command,
|
|
341
|
+
ctx=ctx,
|
|
342
|
+
decision=self._authorize_command_segment(
|
|
343
|
+
command,
|
|
344
|
+
analysis,
|
|
345
|
+
ctx,
|
|
346
|
+
current_cwd,
|
|
347
|
+
compound_has_cd=False,
|
|
348
|
+
),
|
|
349
|
+
)
|
|
350
|
+
trace.append(
|
|
351
|
+
{
|
|
352
|
+
"layer": "segment_single",
|
|
353
|
+
"command": command,
|
|
354
|
+
"behavior": decision.behavior,
|
|
355
|
+
"policy_id": decision.policy_id,
|
|
356
|
+
}
|
|
357
|
+
)
|
|
358
|
+
return self._with_observability(
|
|
359
|
+
decision,
|
|
360
|
+
trace=trace,
|
|
361
|
+
trace_context=TraceContext.from_ctx(ctx),
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
compound_has_cd = "cd" in analysis.normalized_base_commands and len(segments) > 1
|
|
365
|
+
|
|
366
|
+
decisions: list[tuple[str, AuthorizationDecision]] = []
|
|
367
|
+
for segment in segments:
|
|
368
|
+
segment_analysis = self._ast_analyzer.analyze(segment)
|
|
369
|
+
seg_decision = self._mode_validator.finalize_decision(
|
|
370
|
+
command=segment,
|
|
371
|
+
ctx=ctx,
|
|
372
|
+
decision=self._authorize_command_segment(
|
|
373
|
+
segment,
|
|
374
|
+
segment_analysis,
|
|
375
|
+
ctx,
|
|
376
|
+
current_cwd,
|
|
377
|
+
compound_has_cd=compound_has_cd,
|
|
378
|
+
),
|
|
379
|
+
)
|
|
380
|
+
trace.append(
|
|
381
|
+
{
|
|
382
|
+
"layer": "segment",
|
|
383
|
+
"command": segment,
|
|
384
|
+
"behavior": seg_decision.behavior,
|
|
385
|
+
"policy_id": seg_decision.policy_id,
|
|
386
|
+
}
|
|
387
|
+
)
|
|
388
|
+
decisions.append(
|
|
389
|
+
(
|
|
390
|
+
segment,
|
|
391
|
+
seg_decision,
|
|
392
|
+
)
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
denied = next((item for item in decisions if item[1].behavior == "deny"), None)
|
|
396
|
+
if denied is not None:
|
|
397
|
+
segment, decision = denied
|
|
398
|
+
return self._with_observability(
|
|
399
|
+
AuthorizationDecision(
|
|
400
|
+
behavior="deny",
|
|
401
|
+
message=decision.message or f"Permission denied for segment: {segment}",
|
|
402
|
+
policy_id=decision.policy_id or "bash_segment_deny",
|
|
403
|
+
),
|
|
404
|
+
trace=trace,
|
|
405
|
+
trace_context=TraceContext.from_ctx(ctx),
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
asked = next((item for item in decisions if item[1].behavior == "ask"), None)
|
|
409
|
+
if asked is not None:
|
|
410
|
+
segment, decision = asked
|
|
411
|
+
message = decision.message or f"This command segment requires approval: {segment}"
|
|
412
|
+
return self._with_observability(
|
|
413
|
+
AuthorizationDecision(
|
|
414
|
+
behavior="ask",
|
|
415
|
+
message=message,
|
|
416
|
+
policy_id=decision.policy_id or "bash_segment_ask",
|
|
417
|
+
ask_prompt=decision.ask_prompt or message,
|
|
418
|
+
updated_input=decision.updated_input,
|
|
419
|
+
),
|
|
420
|
+
trace=trace,
|
|
421
|
+
trace_context=TraceContext.from_ctx(ctx),
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
updated_input = next(
|
|
425
|
+
(item[1].updated_input for item in decisions if item[1].updated_input),
|
|
426
|
+
None,
|
|
427
|
+
)
|
|
428
|
+
return self._with_observability(
|
|
429
|
+
AuthorizationDecision(behavior="allow", updated_input=updated_input),
|
|
430
|
+
trace=trace,
|
|
431
|
+
trace_context=TraceContext.from_ctx(ctx),
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
# ------------------------------------------------------------------
|
|
435
|
+
# Core: invoke
|
|
436
|
+
# ------------------------------------------------------------------
|
|
437
|
+
|
|
438
|
+
def invoke(
|
|
439
|
+
self, data: dict[str, Any], ctx: ToolExecutionContext
|
|
440
|
+
) -> BashToolOutput:
|
|
441
|
+
"""
|
|
442
|
+
核心执行:后台任务路径 or 同步执行路径。
|
|
443
|
+
对应 CC BashTool.tsx::call()。
|
|
444
|
+
"""
|
|
445
|
+
input_data = _as_bash_input_dict(data)
|
|
446
|
+
command = str(input_data.get("command", ""))
|
|
447
|
+
timeout_raw = input_data.get("timeout")
|
|
448
|
+
timeout_opt = int(timeout_raw) if timeout_raw is not None else None
|
|
449
|
+
run_in_background_flag = bool(input_data.get("run_in_background", False))
|
|
450
|
+
disable_sandbox_flag = bool(input_data.get("dangerously_disable_sandbox", False))
|
|
451
|
+
|
|
452
|
+
limits = get_bash_limits()
|
|
453
|
+
timeout_sec = timeout_opt if timeout_opt is not None else limits["default_timeout_sec"]
|
|
454
|
+
|
|
455
|
+
# 从 state_bridge 取上次 cwd(跨调用持久化)
|
|
456
|
+
cwd: str | None = None
|
|
457
|
+
if self._state_bridge is not None:
|
|
458
|
+
cwd = self._state_bridge.get_cwd() if hasattr(self._state_bridge, "get_cwd") else None
|
|
459
|
+
|
|
460
|
+
sandbox_decision = self._sandbox_decider.decide(
|
|
461
|
+
command=command,
|
|
462
|
+
dangerously_disable_sandbox=disable_sandbox_flag,
|
|
463
|
+
ctx=ctx,
|
|
464
|
+
)
|
|
465
|
+
auto_backgrounded = False
|
|
466
|
+
auto_bg_reason = "none"
|
|
467
|
+
run_in_background = run_in_background_flag
|
|
468
|
+
if (
|
|
469
|
+
not run_in_background
|
|
470
|
+
and limits["auto_background_enabled"]
|
|
471
|
+
and timeout_sec >= limits["auto_background_timeout_sec"]
|
|
472
|
+
):
|
|
473
|
+
run_in_background = True
|
|
474
|
+
auto_backgrounded = True
|
|
475
|
+
auto_bg_reason = "long_timeout_policy"
|
|
476
|
+
elif run_in_background:
|
|
477
|
+
auto_bg_reason = "explicit_request"
|
|
478
|
+
|
|
479
|
+
# ------------------------------------------------------------------
|
|
480
|
+
# 后台执行路径
|
|
481
|
+
# ------------------------------------------------------------------
|
|
482
|
+
if run_in_background:
|
|
483
|
+
task = self._backend.submit_background(
|
|
484
|
+
command=command,
|
|
485
|
+
cwd=cwd,
|
|
486
|
+
output_dir=limits["background_task_output_dir"],
|
|
487
|
+
sandbox_decision=sandbox_decision,
|
|
488
|
+
)
|
|
489
|
+
snapshot = self._backend.build_background_snapshot(task)
|
|
490
|
+
ev = BashEventBuffer(trace_context=TraceContext.from_ctx(ctx), scenario="background")
|
|
491
|
+
ev.add(
|
|
492
|
+
"sandbox_decided",
|
|
493
|
+
sandboxed=task.sandboxed,
|
|
494
|
+
reason=sandbox_decision.reason,
|
|
495
|
+
)
|
|
496
|
+
ev.add(
|
|
497
|
+
"auto_background_decision",
|
|
498
|
+
chosen=run_in_background,
|
|
499
|
+
auto_backgrounded=auto_backgrounded,
|
|
500
|
+
reason_code=auto_bg_reason,
|
|
501
|
+
timeout_sec=timeout_sec,
|
|
502
|
+
threshold_sec=limits["auto_background_timeout_sec"],
|
|
503
|
+
)
|
|
504
|
+
ev.add(
|
|
505
|
+
"task_submitted",
|
|
506
|
+
mode="background",
|
|
507
|
+
auto_backgrounded=auto_backgrounded,
|
|
508
|
+
timeout_sec=timeout_sec,
|
|
509
|
+
)
|
|
510
|
+
ev.add("task_state_changed", from_state="created", to_state="running")
|
|
511
|
+
ev.add("task_state_snapshot", task_status=snapshot.task_status, task_mode=snapshot.task_mode)
|
|
512
|
+
return BashToolOutput(
|
|
513
|
+
stdout="",
|
|
514
|
+
interrupted=False,
|
|
515
|
+
exit_code=0,
|
|
516
|
+
background_task_id=task.task_id,
|
|
517
|
+
background_output_path=task.output_path,
|
|
518
|
+
task_mode=snapshot.task_mode,
|
|
519
|
+
task_status=snapshot.task_status,
|
|
520
|
+
task_exit_code=snapshot.exit_code,
|
|
521
|
+
auto_backgrounded=auto_backgrounded,
|
|
522
|
+
sandboxed=task.sandboxed,
|
|
523
|
+
sandbox_temp_dir=task.sandbox_temp_dir,
|
|
524
|
+
sandbox_bypass_reason=(
|
|
525
|
+
None if task.sandboxed else sandbox_decision.reason
|
|
526
|
+
),
|
|
527
|
+
observability=finalize_observability_payload(ev.to_observability()),
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
simulated_edit = self._consume_pending_sed_edit(command, ctx)
|
|
531
|
+
if simulated_edit is not None:
|
|
532
|
+
path = Path(simulated_edit["absolute_path"])
|
|
533
|
+
path.write_text(simulated_edit["new_content"], encoding="utf-8")
|
|
534
|
+
ev = BashEventBuffer(trace_context=TraceContext.from_ctx(ctx), scenario="sed_preview")
|
|
535
|
+
ev.add("sed_preview_apply", mode="foreground")
|
|
536
|
+
return BashToolOutput(
|
|
537
|
+
stdout="",
|
|
538
|
+
interrupted=False,
|
|
539
|
+
exit_code=0,
|
|
540
|
+
no_output_expected=True,
|
|
541
|
+
observability=finalize_observability_payload(ev.to_observability()),
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
# ------------------------------------------------------------------
|
|
545
|
+
# 同步执行路径
|
|
546
|
+
# ------------------------------------------------------------------
|
|
547
|
+
if sandbox_decision.use_sandbox:
|
|
548
|
+
result = self._backend.execute(
|
|
549
|
+
command=command,
|
|
550
|
+
cwd=cwd,
|
|
551
|
+
timeout_sec=timeout_sec,
|
|
552
|
+
sandbox_decision=sandbox_decision,
|
|
553
|
+
session_key=ctx.thread_id,
|
|
554
|
+
)
|
|
555
|
+
else:
|
|
556
|
+
stream_chunks: list[str] = []
|
|
557
|
+
final_exit_code = 0
|
|
558
|
+
final_interrupted = False
|
|
559
|
+
final_cwd_after: str | None = None
|
|
560
|
+
for event in self._backend.stream_execute_iter(
|
|
561
|
+
command=command,
|
|
562
|
+
cwd=cwd,
|
|
563
|
+
timeout_sec=timeout_sec,
|
|
564
|
+
sandbox_decision=sandbox_decision,
|
|
565
|
+
session_key=ctx.thread_id,
|
|
566
|
+
):
|
|
567
|
+
if event.kind == "chunk" and event.chunk:
|
|
568
|
+
stream_chunks.append(event.chunk)
|
|
569
|
+
elif event.kind == "final":
|
|
570
|
+
final_exit_code = event.exit_code or 0
|
|
571
|
+
final_interrupted = bool(event.interrupted)
|
|
572
|
+
final_cwd_after = event.cwd_after
|
|
573
|
+
merged_stdout, parsed_cwd_after = _strip_stream_cwd_marker("".join(stream_chunks))
|
|
574
|
+
result = ExecuteResult(
|
|
575
|
+
stdout=merged_stdout,
|
|
576
|
+
exit_code=final_exit_code,
|
|
577
|
+
interrupted=final_interrupted,
|
|
578
|
+
cwd_after=parsed_cwd_after or final_cwd_after,
|
|
579
|
+
sandboxed=False,
|
|
580
|
+
sandbox_bypass_reason=sandbox_decision.reason,
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
# 更新 cwd(如果 cd 命令改变了目录)
|
|
584
|
+
if (
|
|
585
|
+
self._state_bridge is not None
|
|
586
|
+
and result.cwd_after is not None
|
|
587
|
+
and result.cwd_after != cwd
|
|
588
|
+
and hasattr(self._state_bridge, "set_cwd")
|
|
589
|
+
):
|
|
590
|
+
self._state_bridge.set_cwd(result.cwd_after)
|
|
591
|
+
|
|
592
|
+
# 危险命令警告(非阻断)
|
|
593
|
+
destructive_warning = detect_destructive_patterns(command)
|
|
594
|
+
snapshot = self._backend.build_foreground_snapshot(result)
|
|
595
|
+
|
|
596
|
+
# 退出码语义解析
|
|
597
|
+
cmd_result = interpret_command_result(
|
|
598
|
+
command, result.exit_code, result.stdout
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
# 真正错误时抛异常(pipeline 捕获 → error envelope)
|
|
602
|
+
if cmd_result.is_error and result.exit_code != 0 and not result.sandbox_violation_message:
|
|
603
|
+
raise RuntimeError(
|
|
604
|
+
f"Command failed with exit code {result.exit_code}:\n"
|
|
605
|
+
f"{strip_empty_lines(result.stdout)}"
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
stdout_text = strip_empty_lines(result.stdout)
|
|
609
|
+
overflow_file: str | None = None
|
|
610
|
+
output_truncated = False
|
|
611
|
+
max_chars = limits["max_output_chars"]
|
|
612
|
+
if len(stdout_text) > max_chars:
|
|
613
|
+
spill_root = os.path.join(
|
|
614
|
+
os.path.expanduser("~/.cache/langchain_agentx/bash_spill"),
|
|
615
|
+
)
|
|
616
|
+
os.makedirs(spill_root, exist_ok=True)
|
|
617
|
+
fd, overflow_file = tempfile.mkstemp(
|
|
618
|
+
suffix=".txt", prefix="bash_out_", dir=spill_root, text=True
|
|
619
|
+
)
|
|
620
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
621
|
+
f.write(stdout_text)
|
|
622
|
+
stdout_text = stdout_text[:max_chars]
|
|
623
|
+
output_truncated = True
|
|
624
|
+
|
|
625
|
+
ev = BashEventBuffer(trace_context=TraceContext.from_ctx(ctx), scenario="foreground")
|
|
626
|
+
ev.add("task_state_changed", from_state="created", to_state="running")
|
|
627
|
+
ev.add(
|
|
628
|
+
"sandbox_decided",
|
|
629
|
+
sandboxed=result.sandboxed,
|
|
630
|
+
reason=sandbox_decision.reason,
|
|
631
|
+
)
|
|
632
|
+
ev.add(
|
|
633
|
+
"command_started",
|
|
634
|
+
mode="foreground",
|
|
635
|
+
timeout_sec=timeout_sec,
|
|
636
|
+
)
|
|
637
|
+
if not sandbox_decision.use_sandbox:
|
|
638
|
+
# 为流式路径补充 chunk 粒度观测(安全起见仅预览+长度)
|
|
639
|
+
for idx, c in enumerate(stream_chunks if 'stream_chunks' in locals() else []):
|
|
640
|
+
ev.add(
|
|
641
|
+
"stdout_chunk",
|
|
642
|
+
chunk_index=idx + 1,
|
|
643
|
+
chunk_size=len(c),
|
|
644
|
+
chunk_preview=c[:120],
|
|
645
|
+
)
|
|
646
|
+
ev.add(
|
|
647
|
+
"command_completed",
|
|
648
|
+
mode="foreground",
|
|
649
|
+
exit_code=result.exit_code,
|
|
650
|
+
interrupted=result.interrupted,
|
|
651
|
+
output_truncated=output_truncated,
|
|
652
|
+
auto_backgrounded=False,
|
|
653
|
+
)
|
|
654
|
+
ev.add("task_state_changed", from_state="running", to_state=snapshot.task_status)
|
|
655
|
+
return BashToolOutput(
|
|
656
|
+
stdout=stdout_text,
|
|
657
|
+
interrupted=result.interrupted,
|
|
658
|
+
exit_code=result.exit_code,
|
|
659
|
+
task_mode=snapshot.task_mode,
|
|
660
|
+
task_status=snapshot.task_status,
|
|
661
|
+
task_exit_code=snapshot.exit_code,
|
|
662
|
+
return_code_interpretation=cmd_result.message,
|
|
663
|
+
no_output_expected=_is_silent_command(command),
|
|
664
|
+
destructive_warning=destructive_warning,
|
|
665
|
+
sandboxed=result.sandboxed,
|
|
666
|
+
sandbox_bypass_reason=result.sandbox_bypass_reason,
|
|
667
|
+
sandbox_temp_dir=result.sandbox_temp_dir,
|
|
668
|
+
sandbox_violation_message=result.sandbox_violation_message,
|
|
669
|
+
overflow_file=overflow_file,
|
|
670
|
+
output_truncated=output_truncated,
|
|
671
|
+
observability=finalize_observability_payload(ev.to_observability()),
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
# ------------------------------------------------------------------
|
|
675
|
+
# Hook 9: present
|
|
676
|
+
# ------------------------------------------------------------------
|
|
677
|
+
|
|
678
|
+
def present(
|
|
679
|
+
self, data: dict[str, Any], result: BashToolOutput, ctx: ToolExecutionContext
|
|
680
|
+
) -> ToolResultEnvelope:
|
|
681
|
+
"""
|
|
682
|
+
将执行结果映射为 ToolResultEnvelope。
|
|
683
|
+
对应 CC UI.tsx 的 renderToolResultMessage()。
|
|
684
|
+
"""
|
|
685
|
+
return self._result_presenter.present(
|
|
686
|
+
tool_name=self.name,
|
|
687
|
+
data=_as_bash_input_dict(data),
|
|
688
|
+
result=result,
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
def __repr__(self) -> str:
|
|
692
|
+
return f"<BashRuntimeTool name={self.name!r} [destructive]>"
|
|
693
|
+
|
|
694
|
+
@staticmethod
|
|
695
|
+
def _with_observability(
|
|
696
|
+
decision: AuthorizationDecision,
|
|
697
|
+
*,
|
|
698
|
+
trace: list[dict[str, Any]],
|
|
699
|
+
trace_context: TraceContext,
|
|
700
|
+
) -> AuthorizationDecision:
|
|
701
|
+
metadata = dict(decision.metadata or {})
|
|
702
|
+
metadata.update(
|
|
703
|
+
build_decision_observability(
|
|
704
|
+
trace=trace,
|
|
705
|
+
trace_context=trace_context,
|
|
706
|
+
)
|
|
707
|
+
)
|
|
708
|
+
return AuthorizationDecision(
|
|
709
|
+
behavior=decision.behavior,
|
|
710
|
+
message=decision.message,
|
|
711
|
+
policy_id=decision.policy_id,
|
|
712
|
+
updated_input=decision.updated_input,
|
|
713
|
+
ask_prompt=decision.ask_prompt,
|
|
714
|
+
metadata=metadata,
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
def _authorize_command_segment(
|
|
718
|
+
self,
|
|
719
|
+
command: str,
|
|
720
|
+
analysis: Any,
|
|
721
|
+
ctx: ToolExecutionContext,
|
|
722
|
+
cwd: str,
|
|
723
|
+
compound_has_cd: bool,
|
|
724
|
+
) -> AuthorizationDecision:
|
|
725
|
+
"""
|
|
726
|
+
对单个 command segment 做权限决策。
|
|
727
|
+
|
|
728
|
+
这里动态修正 tool_flags,让 PolicyEngine 的 read_only_mode /
|
|
729
|
+
deny_globs / ask_globs 能基于 segment 语义工作。
|
|
730
|
+
"""
|
|
731
|
+
path_decision = self._path_security.authorize_segment(
|
|
732
|
+
command=command,
|
|
733
|
+
analysis=analysis,
|
|
734
|
+
cwd=cwd,
|
|
735
|
+
policy=self._policy,
|
|
736
|
+
compound_has_cd=compound_has_cd,
|
|
737
|
+
)
|
|
738
|
+
if path_decision.behavior != "allow":
|
|
739
|
+
return path_decision
|
|
740
|
+
|
|
741
|
+
hardening_decision = self._security_hardener.check_segment(
|
|
742
|
+
command=command,
|
|
743
|
+
analysis=analysis,
|
|
744
|
+
cwd=cwd,
|
|
745
|
+
)
|
|
746
|
+
if hardening_decision is not None:
|
|
747
|
+
return hardening_decision
|
|
748
|
+
|
|
749
|
+
if self._policy is None:
|
|
750
|
+
return self._authorize_sed_segment(command=command, ctx=ctx, cwd=cwd) or AuthorizationDecision(behavior="allow")
|
|
751
|
+
|
|
752
|
+
needs_write = self._ast_analyzer.requires_write_permissions(analysis)
|
|
753
|
+
classification = self._read_only_classifier.classify(command)
|
|
754
|
+
read_only = classification.is_read_only and not needs_write
|
|
755
|
+
|
|
756
|
+
seg_ctx = replace(
|
|
757
|
+
ctx,
|
|
758
|
+
tool_flags={
|
|
759
|
+
**(ctx.tool_flags or {}),
|
|
760
|
+
"is_read_only": read_only,
|
|
761
|
+
"is_destructive": not read_only,
|
|
762
|
+
},
|
|
763
|
+
)
|
|
764
|
+
decision = self._policy.authorize(
|
|
765
|
+
tool_name=self.name,
|
|
766
|
+
input_data={
|
|
767
|
+
"command": self._security_hardener.canonicalize_for_policy(command),
|
|
768
|
+
},
|
|
769
|
+
ctx=seg_ctx,
|
|
770
|
+
)
|
|
771
|
+
sed_check = self._sed_validator.evaluate(command)
|
|
772
|
+
if (
|
|
773
|
+
decision.behavior == "ask"
|
|
774
|
+
and self._mode_validator.should_auto_allow_after_policy(command, ctx)
|
|
775
|
+
and not sed_check.is_sed
|
|
776
|
+
):
|
|
777
|
+
return AuthorizationDecision(
|
|
778
|
+
behavior="allow",
|
|
779
|
+
message="Command auto-approved in acceptEdits mode.",
|
|
780
|
+
policy_id="permission_mode",
|
|
781
|
+
)
|
|
782
|
+
if decision.behavior != "allow":
|
|
783
|
+
return decision
|
|
784
|
+
|
|
785
|
+
return self._authorize_sed_segment(command=command, ctx=ctx, cwd=cwd) or decision
|
|
786
|
+
|
|
787
|
+
def _authorize_sed_segment(
|
|
788
|
+
self,
|
|
789
|
+
*,
|
|
790
|
+
command: str,
|
|
791
|
+
ctx: ToolExecutionContext,
|
|
792
|
+
cwd: str,
|
|
793
|
+
) -> AuthorizationDecision | None:
|
|
794
|
+
pending = self._get_pending_sed_edit(command, ctx)
|
|
795
|
+
if pending is not None:
|
|
796
|
+
return AuthorizationDecision(
|
|
797
|
+
behavior="allow",
|
|
798
|
+
message="Applying previously previewed sed edit.",
|
|
799
|
+
policy_id="bash_sed_preview",
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
check = self._sed_validator.evaluate(command)
|
|
803
|
+
if not check.is_sed:
|
|
804
|
+
return None
|
|
805
|
+
if check.requires_manual_approval:
|
|
806
|
+
return AuthorizationDecision(
|
|
807
|
+
behavior="ask",
|
|
808
|
+
message=check.reason,
|
|
809
|
+
policy_id="bash_sed_validation",
|
|
810
|
+
ask_prompt=check.reason,
|
|
811
|
+
)
|
|
812
|
+
if not check.is_in_place or check.edit_info is None:
|
|
813
|
+
return None
|
|
814
|
+
|
|
815
|
+
absolute_path = os.path.abspath(os.path.join(cwd, check.edit_info.file_path))
|
|
816
|
+
try:
|
|
817
|
+
original = Path(absolute_path).read_text(encoding="utf-8")
|
|
818
|
+
except OSError:
|
|
819
|
+
return None
|
|
820
|
+
|
|
821
|
+
updated = self._sed_parser.apply_substitution(original, check.edit_info)
|
|
822
|
+
preview = self._sed_parser.build_preview(
|
|
823
|
+
original=original,
|
|
824
|
+
updated=updated,
|
|
825
|
+
file_path=check.edit_info.file_path,
|
|
826
|
+
)
|
|
827
|
+
self._store_pending_sed_edit(
|
|
828
|
+
command=command,
|
|
829
|
+
ctx=ctx,
|
|
830
|
+
absolute_path=absolute_path,
|
|
831
|
+
new_content=updated,
|
|
832
|
+
)
|
|
833
|
+
|
|
834
|
+
if self._mode_validator.get_mode(ctx) == "acceptEdits":
|
|
835
|
+
return AuthorizationDecision(
|
|
836
|
+
behavior="allow",
|
|
837
|
+
message="sed edit auto-approved in acceptEdits mode.",
|
|
838
|
+
policy_id="bash_sed_preview",
|
|
839
|
+
)
|
|
840
|
+
|
|
841
|
+
prompt = (
|
|
842
|
+
"sed in-place edit requires approval. The command will be applied using the "
|
|
843
|
+
"previewed content shown below.\n\n"
|
|
844
|
+
f"{preview}"
|
|
845
|
+
)
|
|
846
|
+
return AuthorizationDecision(
|
|
847
|
+
behavior="ask",
|
|
848
|
+
message="sed in-place edit requires approval.",
|
|
849
|
+
policy_id="bash_sed_preview",
|
|
850
|
+
ask_prompt=prompt,
|
|
851
|
+
)
|
|
852
|
+
|
|
853
|
+
def _get_current_cwd(self) -> str:
|
|
854
|
+
if self._state_bridge is not None and hasattr(self._state_bridge, "get_cwd"):
|
|
855
|
+
cwd = self._state_bridge.get_cwd()
|
|
856
|
+
if isinstance(cwd, str) and cwd:
|
|
857
|
+
return cwd
|
|
858
|
+
return os.getcwd()
|
|
859
|
+
|
|
860
|
+
@staticmethod
|
|
861
|
+
def _pending_sed_state(ctx: ToolExecutionContext) -> dict[str, dict[str, str]]:
|
|
862
|
+
if ctx.state is None:
|
|
863
|
+
ctx.state = {}
|
|
864
|
+
return ctx.state.setdefault("__bash_sed_pending__", {})
|
|
865
|
+
|
|
866
|
+
def _pending_sed_key(self, command: str, ctx: ToolExecutionContext) -> str:
|
|
867
|
+
if ctx.tool_call_id:
|
|
868
|
+
return ctx.tool_call_id
|
|
869
|
+
return hashlib.sha256(command.encode("utf-8")).hexdigest()
|
|
870
|
+
|
|
871
|
+
def _store_pending_sed_edit(
|
|
872
|
+
self,
|
|
873
|
+
*,
|
|
874
|
+
command: str,
|
|
875
|
+
ctx: ToolExecutionContext,
|
|
876
|
+
absolute_path: str,
|
|
877
|
+
new_content: str,
|
|
878
|
+
) -> None:
|
|
879
|
+
key = self._pending_sed_key(command, ctx)
|
|
880
|
+
self._pending_sed_state(ctx)[key] = {
|
|
881
|
+
"command": command,
|
|
882
|
+
"absolute_path": absolute_path,
|
|
883
|
+
"new_content": new_content,
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
def _get_pending_sed_edit(
|
|
887
|
+
self,
|
|
888
|
+
command: str,
|
|
889
|
+
ctx: ToolExecutionContext,
|
|
890
|
+
) -> dict[str, str] | None:
|
|
891
|
+
key = self._pending_sed_key(command, ctx)
|
|
892
|
+
pending = self._pending_sed_state(ctx).get(key)
|
|
893
|
+
if pending is None or pending.get("command") != command:
|
|
894
|
+
return None
|
|
895
|
+
return pending
|
|
896
|
+
|
|
897
|
+
def _consume_pending_sed_edit(
|
|
898
|
+
self,
|
|
899
|
+
command: str,
|
|
900
|
+
ctx: ToolExecutionContext,
|
|
901
|
+
) -> dict[str, str] | None:
|
|
902
|
+
key = self._pending_sed_key(command, ctx)
|
|
903
|
+
pending = self._pending_sed_state(ctx).get(key)
|
|
904
|
+
if pending is None or pending.get("command") != command:
|
|
905
|
+
return None
|
|
906
|
+
return self._pending_sed_state(ctx).pop(key)
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
# ---------------------------------------------------------------------------
|
|
910
|
+
# 内部辅助
|
|
911
|
+
# ---------------------------------------------------------------------------
|
|
912
|
+
|
|
913
|
+
|
|
914
|
+
def _is_silent_command(command: str) -> bool:
|
|
915
|
+
"""
|
|
916
|
+
检测命令是否预期不产生 stdout(mv/cp/rm 等)。
|
|
917
|
+
对应 CC BASH_SILENT_COMMANDS。
|
|
918
|
+
"""
|
|
919
|
+
stripped = command.strip()
|
|
920
|
+
if not stripped:
|
|
921
|
+
return False
|
|
922
|
+
|
|
923
|
+
# 提取第一个 token
|
|
924
|
+
parts = stripped.split()
|
|
925
|
+
if not parts:
|
|
926
|
+
return False
|
|
927
|
+
|
|
928
|
+
base = parts[0].rsplit("/", 1)[-1]
|
|
929
|
+
return base in SILENT_COMMANDS
|
|
930
|
+
|
|
931
|
+
|
|
932
|
+
def _strip_stream_cwd_marker(stdout: str) -> tuple[str, str | None]:
|
|
933
|
+
marker = "__AGENTX_STREAM_CWD__"
|
|
934
|
+
if marker not in stdout:
|
|
935
|
+
return stdout, None
|
|
936
|
+
before, _, after = stdout.rpartition(marker)
|
|
937
|
+
cwd = after.strip().splitlines()[0] if after.strip() else None
|
|
938
|
+
cleaned = before.rstrip("\n")
|
|
939
|
+
return cleaned, cwd
|