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,571 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tools/bash/ast_security.py — BashRuntimeTool v2 AST 分析
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
使用 tree-sitter 对 bash 命令做结构化分析,提供:
|
|
6
|
+
- 顶层子命令拆分(pipeline / list)
|
|
7
|
+
- 命令替换 / 进程替换 / subshell / command group 检测
|
|
8
|
+
- 包装命令归一化(env / command / builtin / nohup)
|
|
9
|
+
- 嵌套 shell 执行检测(bash -c / sh -c / zsh -c)
|
|
10
|
+
- 输出重定向检测(辅助只读/写入判定)
|
|
11
|
+
|
|
12
|
+
对照 CC:
|
|
13
|
+
- bashSecurity.ts → 结构级危险模式识别
|
|
14
|
+
- bashCommandHelpers.ts → 子命令拆分与分段授权
|
|
15
|
+
- ParsedCommand / treeSitterAnalysis → 结构化命令理解
|
|
16
|
+
|
|
17
|
+
实现约束:
|
|
18
|
+
- 采用 OOP 风格,避免散落函数主导的实现
|
|
19
|
+
- 保持 fail-open:AST 失败时回退到轻量启发式,而不是阻塞工具
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import re
|
|
25
|
+
import shlex
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from functools import lru_cache
|
|
28
|
+
|
|
29
|
+
from tree_sitter import Node
|
|
30
|
+
from tree_sitter_languages import get_parser
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class BashSimpleCommand:
|
|
35
|
+
"""单个 simple command 的结构摘要。"""
|
|
36
|
+
|
|
37
|
+
text: str
|
|
38
|
+
argv: tuple[str, ...]
|
|
39
|
+
base_command: str | None
|
|
40
|
+
normalized_base_command: str | None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class BashAstAnalysis:
|
|
45
|
+
"""
|
|
46
|
+
Bash AST 分析结果。
|
|
47
|
+
|
|
48
|
+
`permission_segments` 是 v2 最关键字段:用于对顶层 pipeline / list
|
|
49
|
+
做逐段授权,对齐 CC `checkCommandOperatorPermissions()`。
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
command: str
|
|
53
|
+
parsed: bool
|
|
54
|
+
has_syntax_error: bool
|
|
55
|
+
permission_segments: tuple[str, ...]
|
|
56
|
+
simple_commands: tuple[BashSimpleCommand, ...]
|
|
57
|
+
has_pipeline: bool
|
|
58
|
+
has_logical_list: bool
|
|
59
|
+
has_subshell: bool
|
|
60
|
+
has_command_group: bool
|
|
61
|
+
has_command_substitution: bool
|
|
62
|
+
has_process_substitution: bool
|
|
63
|
+
has_output_redirection: bool
|
|
64
|
+
output_redirection_paths: tuple[str, ...]
|
|
65
|
+
nested_shell_command: str | None = None
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def normalized_base_commands(self) -> tuple[str, ...]:
|
|
69
|
+
return tuple(
|
|
70
|
+
cmd.normalized_base_command
|
|
71
|
+
for cmd in self.simple_commands
|
|
72
|
+
if cmd.normalized_base_command
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def has_unsafe_compound_structure(self) -> bool:
|
|
77
|
+
return self.has_subshell or self.has_command_group
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class BashCommandNameNormalizer:
|
|
81
|
+
"""
|
|
82
|
+
负责将命令文本归一化到“真实基础命令”。
|
|
83
|
+
|
|
84
|
+
对齐 CC 中 wrapper stripping 的核心思想:
|
|
85
|
+
- 跳过 `FOO=1` 这类环境赋值
|
|
86
|
+
- 跳过 `env` 自身和其 flag/赋值
|
|
87
|
+
- 跳过 `command` / `builtin` / `nohup`
|
|
88
|
+
- 保留真实被执行的主命令
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
_ENV_ASSIGNMENT_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*=.*$")
|
|
92
|
+
_SAFE_WRAPPERS = frozenset({"command", "builtin", "nohup", "time"})
|
|
93
|
+
|
|
94
|
+
def __init__(self, wrapper_stripper: "BashWrapperStripper | None" = None) -> None:
|
|
95
|
+
self._wrapper_stripper = wrapper_stripper or BashWrapperStripper()
|
|
96
|
+
|
|
97
|
+
def normalize(self, command_text: str) -> str | None:
|
|
98
|
+
return self.normalize_argv(self._split(command_text))
|
|
99
|
+
|
|
100
|
+
def normalize_argv(self, tokens: list[str]) -> str | None:
|
|
101
|
+
effective_tokens = list(tokens)
|
|
102
|
+
while effective_tokens and self._is_assignment(effective_tokens[0]):
|
|
103
|
+
effective_tokens = effective_tokens[1:]
|
|
104
|
+
stripped_tokens = self._wrapper_stripper.strip(effective_tokens)
|
|
105
|
+
if not stripped_tokens:
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
return stripped_tokens[0].rsplit("/", 1)[-1]
|
|
109
|
+
|
|
110
|
+
def _skip_env_arguments(self, tokens: list[str], start_idx: int) -> int:
|
|
111
|
+
idx = start_idx
|
|
112
|
+
while idx < len(tokens):
|
|
113
|
+
candidate = tokens[idx]
|
|
114
|
+
if candidate == "--":
|
|
115
|
+
return idx + 1
|
|
116
|
+
if candidate.startswith("-") or self._is_assignment(candidate):
|
|
117
|
+
idx += 1
|
|
118
|
+
continue
|
|
119
|
+
return idx
|
|
120
|
+
return idx
|
|
121
|
+
|
|
122
|
+
def _is_assignment(self, token: str) -> bool:
|
|
123
|
+
return bool(self._ENV_ASSIGNMENT_RE.match(token))
|
|
124
|
+
|
|
125
|
+
@staticmethod
|
|
126
|
+
def _split(command_text: str) -> list[str]:
|
|
127
|
+
try:
|
|
128
|
+
return shlex.split(command_text)
|
|
129
|
+
except ValueError:
|
|
130
|
+
return command_text.split()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class BashAstNodeHelper:
|
|
134
|
+
"""封装 tree-sitter 节点上的常用操作。"""
|
|
135
|
+
|
|
136
|
+
def __init__(self, source: str) -> None:
|
|
137
|
+
self._source = source
|
|
138
|
+
self._source_bytes = source.encode("utf8")
|
|
139
|
+
|
|
140
|
+
def text(self, node: Node) -> str:
|
|
141
|
+
"""
|
|
142
|
+
安全提取 AST 节点文本。
|
|
143
|
+
|
|
144
|
+
重要:tree-sitter 的 `start_byte/end_byte` 是按 UTF-8 字节偏移计算的,
|
|
145
|
+
不能直接对 Python `str` 做切片,否则中文等多字节字符会导致偏移错误。
|
|
146
|
+
"""
|
|
147
|
+
snippet_bytes = self._source_bytes[node.start_byte:node.end_byte]
|
|
148
|
+
return snippet_bytes.decode("utf8")
|
|
149
|
+
|
|
150
|
+
def contains_node_type(self, node: Node, node_types: set[str]) -> bool:
|
|
151
|
+
if node.type in node_types:
|
|
152
|
+
return True
|
|
153
|
+
return any(self.contains_node_type(child, node_types) for child in node.children)
|
|
154
|
+
|
|
155
|
+
def contains_output_redirection(self, node: Node) -> bool:
|
|
156
|
+
if node.type == "file_redirect":
|
|
157
|
+
redirect_text = self.text(node).lstrip()
|
|
158
|
+
# `>` / `>>` / `1>` / `2>` / `&>` / `<>` 都视为写操作;纯 `<` 不算
|
|
159
|
+
if ">" in redirect_text:
|
|
160
|
+
return True
|
|
161
|
+
return any(self.contains_output_redirection(child) for child in node.children)
|
|
162
|
+
|
|
163
|
+
def command_name_text(self, node: Node) -> str | None:
|
|
164
|
+
for child in node.children:
|
|
165
|
+
if child.type == "command_name":
|
|
166
|
+
return self.text(child).strip().rsplit("/", 1)[-1]
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
def argument_like_texts(self, node: Node) -> list[str]:
|
|
170
|
+
values: list[str] = []
|
|
171
|
+
for child in node.children:
|
|
172
|
+
if child.type in {
|
|
173
|
+
"command_name",
|
|
174
|
+
"word",
|
|
175
|
+
"string",
|
|
176
|
+
"raw_string",
|
|
177
|
+
"number",
|
|
178
|
+
"concatenation",
|
|
179
|
+
"expansion",
|
|
180
|
+
"simple_expansion",
|
|
181
|
+
"command_substitution",
|
|
182
|
+
"process_substitution",
|
|
183
|
+
}:
|
|
184
|
+
values.append(self.text(child).strip())
|
|
185
|
+
return [value for value in values if value]
|
|
186
|
+
|
|
187
|
+
def collect_output_redirection_paths(self, node: Node) -> list[str]:
|
|
188
|
+
paths: list[str] = []
|
|
189
|
+
self._walk_output_redirections(node, paths)
|
|
190
|
+
return paths
|
|
191
|
+
|
|
192
|
+
def _walk_output_redirections(self, node: Node, paths: list[str]) -> None:
|
|
193
|
+
if node.type == "file_redirect":
|
|
194
|
+
redirect_text = self.text(node).lstrip()
|
|
195
|
+
if ">" in redirect_text:
|
|
196
|
+
named_children = node.named_children
|
|
197
|
+
if named_children:
|
|
198
|
+
path_text = self.text(named_children[-1]).strip()
|
|
199
|
+
if path_text:
|
|
200
|
+
paths.append(path_text)
|
|
201
|
+
for child in node.children:
|
|
202
|
+
self._walk_output_redirections(child, paths)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class BashPermissionSegmenter:
|
|
206
|
+
"""
|
|
207
|
+
提取用于权限检查的顶层命令段。
|
|
208
|
+
|
|
209
|
+
对齐 CC `checkCommandOperatorPermissions()`:
|
|
210
|
+
- program/list/pipeline 递归展开
|
|
211
|
+
- command/redirected_statement/subshell/compound_statement 保持原段文本
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
def __init__(self, node_helper: BashAstNodeHelper) -> None:
|
|
215
|
+
self._node_helper = node_helper
|
|
216
|
+
|
|
217
|
+
def collect_segments(self, node: Node) -> list[str]:
|
|
218
|
+
if node.type == "program":
|
|
219
|
+
return self._collect_from_container(node)
|
|
220
|
+
|
|
221
|
+
if node.type in {"pipeline", "list"}:
|
|
222
|
+
return self._collect_from_container(node)
|
|
223
|
+
|
|
224
|
+
if node.type in {"command", "redirected_statement", "subshell", "compound_statement"}:
|
|
225
|
+
text = self._node_helper.text(node).strip()
|
|
226
|
+
return [text] if text else []
|
|
227
|
+
|
|
228
|
+
segments = []
|
|
229
|
+
for child in node.named_children:
|
|
230
|
+
segments.extend(self.collect_segments(child))
|
|
231
|
+
if segments:
|
|
232
|
+
return [segment for segment in segments if segment.strip()]
|
|
233
|
+
|
|
234
|
+
text = self._node_helper.text(node).strip()
|
|
235
|
+
return [text] if text else []
|
|
236
|
+
|
|
237
|
+
def _collect_from_container(self, node: Node) -> list[str]:
|
|
238
|
+
segments: list[str] = []
|
|
239
|
+
for child in node.named_children:
|
|
240
|
+
segments.extend(self.collect_segments(child))
|
|
241
|
+
return [segment for segment in segments if segment.strip()]
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class BashSimpleCommandCollector:
|
|
245
|
+
"""提取 AST 中全部 simple command。"""
|
|
246
|
+
|
|
247
|
+
def __init__(
|
|
248
|
+
self,
|
|
249
|
+
node_helper: BashAstNodeHelper,
|
|
250
|
+
normalizer: BashCommandNameNormalizer,
|
|
251
|
+
argv_extractor: "BashArgvExtractor | None" = None,
|
|
252
|
+
) -> None:
|
|
253
|
+
self._node_helper = node_helper
|
|
254
|
+
self._normalizer = normalizer
|
|
255
|
+
self._argv_extractor = argv_extractor or BashArgvExtractor(node_helper)
|
|
256
|
+
|
|
257
|
+
def collect(self, node: Node) -> list[BashSimpleCommand]:
|
|
258
|
+
result: list[BashSimpleCommand] = []
|
|
259
|
+
self._walk(node, result)
|
|
260
|
+
return result
|
|
261
|
+
|
|
262
|
+
def _walk(self, node: Node, result: list[BashSimpleCommand]) -> None:
|
|
263
|
+
if node.type == "command":
|
|
264
|
+
text = self._node_helper.text(node).strip()
|
|
265
|
+
argv = tuple(self._argv_extractor.extract(node))
|
|
266
|
+
result.append(
|
|
267
|
+
BashSimpleCommand(
|
|
268
|
+
text=text,
|
|
269
|
+
argv=argv,
|
|
270
|
+
base_command=self._node_helper.command_name_text(node),
|
|
271
|
+
normalized_base_command=self._normalizer.normalize_argv(list(argv)),
|
|
272
|
+
)
|
|
273
|
+
)
|
|
274
|
+
for child in node.children:
|
|
275
|
+
self._walk(child, result)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class BashArgvExtractor:
|
|
279
|
+
"""
|
|
280
|
+
从 AST command 节点直接提取 argv。
|
|
281
|
+
|
|
282
|
+
对齐 CC validateSinglePathCommandArgv 的思路:优先使用 AST 已解析的参数,
|
|
283
|
+
避免 shell-quote / shlex 在复杂 quoting 下的歧义。
|
|
284
|
+
"""
|
|
285
|
+
|
|
286
|
+
def __init__(self, node_helper: BashAstNodeHelper) -> None:
|
|
287
|
+
self._node_helper = node_helper
|
|
288
|
+
|
|
289
|
+
def extract(self, command_node: Node) -> list[str]:
|
|
290
|
+
return self._node_helper.argument_like_texts(command_node)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class BashWrapperStripper:
|
|
294
|
+
"""
|
|
295
|
+
AST argv 级 wrapper stripping。
|
|
296
|
+
|
|
297
|
+
对照 CC `stripWrappersFromArgv()`,先实现最常见且高价值的 wrappers:
|
|
298
|
+
`time` / `nohup` / `timeout` / `nice` / `stdbuf` / `env` / `command` / `builtin`
|
|
299
|
+
"""
|
|
300
|
+
|
|
301
|
+
def strip(self, argv: list[str]) -> list[str]:
|
|
302
|
+
tokens = list(argv)
|
|
303
|
+
while tokens:
|
|
304
|
+
head = tokens[0]
|
|
305
|
+
if head in {"time", "nohup", "command", "builtin"}:
|
|
306
|
+
tokens = tokens[2:] if len(tokens) > 1 and tokens[1] == "--" else tokens[1:]
|
|
307
|
+
continue
|
|
308
|
+
if head == "timeout":
|
|
309
|
+
next_idx = self._skip_timeout(tokens)
|
|
310
|
+
if next_idx <= 0 or next_idx >= len(tokens):
|
|
311
|
+
return tokens
|
|
312
|
+
tokens = tokens[next_idx + 1:]
|
|
313
|
+
continue
|
|
314
|
+
if head == "nice":
|
|
315
|
+
next_idx = self._skip_nice(tokens)
|
|
316
|
+
if next_idx <= 0 or next_idx >= len(tokens):
|
|
317
|
+
return tokens
|
|
318
|
+
tokens = tokens[next_idx:]
|
|
319
|
+
continue
|
|
320
|
+
if head == "stdbuf":
|
|
321
|
+
next_idx = self._skip_stdbuf(tokens)
|
|
322
|
+
if next_idx <= 0 or next_idx >= len(tokens):
|
|
323
|
+
return tokens
|
|
324
|
+
tokens = tokens[next_idx:]
|
|
325
|
+
continue
|
|
326
|
+
if head == "env":
|
|
327
|
+
next_idx = self._skip_env(tokens)
|
|
328
|
+
if next_idx <= 0 or next_idx >= len(tokens):
|
|
329
|
+
return tokens
|
|
330
|
+
tokens = tokens[next_idx:]
|
|
331
|
+
continue
|
|
332
|
+
break
|
|
333
|
+
return tokens
|
|
334
|
+
|
|
335
|
+
def _skip_timeout(self, argv: list[str]) -> int:
|
|
336
|
+
i = 1
|
|
337
|
+
while i < len(argv):
|
|
338
|
+
arg = argv[i]
|
|
339
|
+
next_arg = argv[i + 1] if i + 1 < len(argv) else None
|
|
340
|
+
if arg in {"--foreground", "--preserve-status", "--verbose"}:
|
|
341
|
+
i += 1
|
|
342
|
+
elif arg.startswith("--kill-after=") or arg.startswith("--signal="):
|
|
343
|
+
i += 1
|
|
344
|
+
elif arg in {"--kill-after", "--signal"} and next_arg:
|
|
345
|
+
i += 2
|
|
346
|
+
elif arg == "--":
|
|
347
|
+
i += 1
|
|
348
|
+
break
|
|
349
|
+
elif arg.startswith("--"):
|
|
350
|
+
return -1
|
|
351
|
+
elif arg == "-v":
|
|
352
|
+
i += 1
|
|
353
|
+
elif arg in {"-k", "-s"} and next_arg:
|
|
354
|
+
i += 2
|
|
355
|
+
elif re.match(r"^-[ks][A-Za-z0-9_.+-]+$", arg):
|
|
356
|
+
i += 1
|
|
357
|
+
elif arg.startswith("-"):
|
|
358
|
+
return -1
|
|
359
|
+
else:
|
|
360
|
+
break
|
|
361
|
+
return i
|
|
362
|
+
|
|
363
|
+
def _skip_nice(self, argv: list[str]) -> int:
|
|
364
|
+
if len(argv) >= 3 and argv[1] == "-n" and re.match(r"^-?\d+$", argv[2]):
|
|
365
|
+
return 4 if len(argv) > 3 and argv[3] == "--" else 3
|
|
366
|
+
if len(argv) >= 2 and re.match(r"^-\d+$", argv[1]):
|
|
367
|
+
return 3 if len(argv) > 2 and argv[2] == "--" else 2
|
|
368
|
+
return 2 if len(argv) > 1 and argv[1] == "--" else 1
|
|
369
|
+
|
|
370
|
+
def _skip_stdbuf(self, argv: list[str]) -> int:
|
|
371
|
+
i = 1
|
|
372
|
+
while i < len(argv):
|
|
373
|
+
arg = argv[i]
|
|
374
|
+
if re.match(r"^-[ioe]$", arg) and i + 1 < len(argv):
|
|
375
|
+
i += 2
|
|
376
|
+
elif re.match(r"^-[ioe].", arg):
|
|
377
|
+
i += 1
|
|
378
|
+
elif re.match(r"^--(input|output|error)=", arg):
|
|
379
|
+
i += 1
|
|
380
|
+
elif arg.startswith("-"):
|
|
381
|
+
return -1
|
|
382
|
+
else:
|
|
383
|
+
break
|
|
384
|
+
return i if i > 1 and i < len(argv) else -1
|
|
385
|
+
|
|
386
|
+
def _skip_env(self, argv: list[str]) -> int:
|
|
387
|
+
i = 1
|
|
388
|
+
while i < len(argv):
|
|
389
|
+
arg = argv[i]
|
|
390
|
+
if "=" in arg and not arg.startswith("-"):
|
|
391
|
+
i += 1
|
|
392
|
+
elif arg in {"-i", "-0", "-v"}:
|
|
393
|
+
i += 1
|
|
394
|
+
elif arg == "-u" and i + 1 < len(argv):
|
|
395
|
+
i += 2
|
|
396
|
+
elif arg.startswith("-"):
|
|
397
|
+
return -1
|
|
398
|
+
else:
|
|
399
|
+
break
|
|
400
|
+
return i if i < len(argv) else -1
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
class BashAstAdvisor:
|
|
404
|
+
"""
|
|
405
|
+
基于 AST 分析结果给出权限建议。
|
|
406
|
+
|
|
407
|
+
对齐 CC 的思路:复杂 shell 结构优先 ask,而不是盲目依赖字符串规则。
|
|
408
|
+
"""
|
|
409
|
+
|
|
410
|
+
def get_approval_reason(self, analysis: BashAstAnalysis) -> str | None:
|
|
411
|
+
if analysis.has_subshell:
|
|
412
|
+
return "This command uses a subshell `(...)`, which requires approval for safety."
|
|
413
|
+
if analysis.has_command_group:
|
|
414
|
+
return "This command uses a command group `{ ...; }`, which requires approval for safety."
|
|
415
|
+
if analysis.has_command_substitution:
|
|
416
|
+
return "This command uses command substitution (`$()` or backticks), which requires approval for safety."
|
|
417
|
+
if analysis.has_process_substitution:
|
|
418
|
+
return "This command uses process substitution (`<()` or `>()`), which requires approval for safety."
|
|
419
|
+
if analysis.nested_shell_command:
|
|
420
|
+
return (
|
|
421
|
+
f"This command launches a nested shell via `{analysis.nested_shell_command} -c`, "
|
|
422
|
+
"which requires approval for safety."
|
|
423
|
+
)
|
|
424
|
+
if analysis.has_syntax_error:
|
|
425
|
+
return "This command has shell syntax errors and requires approval before execution."
|
|
426
|
+
|
|
427
|
+
normalized = analysis.normalized_base_commands
|
|
428
|
+
if sum(1 for command in normalized if command == "cd") > 1:
|
|
429
|
+
return "Multiple directory changes in one command require approval for clarity."
|
|
430
|
+
|
|
431
|
+
if "cd" in normalized and "git" in normalized and len(analysis.permission_segments) > 1:
|
|
432
|
+
return (
|
|
433
|
+
"Compound commands mixing `cd` and `git` across segments require approval "
|
|
434
|
+
"to avoid repository-boundary confusion."
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
return None
|
|
438
|
+
|
|
439
|
+
def requires_write_permissions(self, analysis: BashAstAnalysis) -> bool:
|
|
440
|
+
return analysis.has_output_redirection
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
class BashAstAnalyzer:
|
|
444
|
+
"""
|
|
445
|
+
Bash AST 主分析器。
|
|
446
|
+
|
|
447
|
+
这是 OOP 入口,供 `BashRuntimeTool` 调用。
|
|
448
|
+
"""
|
|
449
|
+
|
|
450
|
+
_SHELL_WRAPPER_COMMANDS = frozenset({"bash", "sh", "zsh", "fish"})
|
|
451
|
+
|
|
452
|
+
def __init__(self) -> None:
|
|
453
|
+
self._normalizer = BashCommandNameNormalizer()
|
|
454
|
+
self._advisor = BashAstAdvisor()
|
|
455
|
+
|
|
456
|
+
def analyze(self, command: str) -> BashAstAnalysis:
|
|
457
|
+
stripped = command.strip()
|
|
458
|
+
if not stripped:
|
|
459
|
+
return BashAstAnalysis(
|
|
460
|
+
command=command,
|
|
461
|
+
parsed=True,
|
|
462
|
+
has_syntax_error=False,
|
|
463
|
+
permission_segments=(),
|
|
464
|
+
simple_commands=(),
|
|
465
|
+
has_pipeline=False,
|
|
466
|
+
has_logical_list=False,
|
|
467
|
+
has_subshell=False,
|
|
468
|
+
has_command_group=False,
|
|
469
|
+
has_command_substitution=False,
|
|
470
|
+
has_process_substitution=False,
|
|
471
|
+
has_output_redirection=False,
|
|
472
|
+
output_redirection_paths=(),
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
try:
|
|
476
|
+
parser = self._get_bash_parser()
|
|
477
|
+
tree = parser.parse(command.encode("utf-8"))
|
|
478
|
+
root = tree.root_node
|
|
479
|
+
except Exception:
|
|
480
|
+
return self._build_fallback_analysis(command)
|
|
481
|
+
|
|
482
|
+
node_helper = BashAstNodeHelper(command)
|
|
483
|
+
segmenter = BashPermissionSegmenter(node_helper)
|
|
484
|
+
collector = BashSimpleCommandCollector(node_helper, self._normalizer)
|
|
485
|
+
|
|
486
|
+
simple_commands = tuple(collector.collect(root))
|
|
487
|
+
permission_segments = tuple(segmenter.collect_segments(root))
|
|
488
|
+
|
|
489
|
+
return BashAstAnalysis(
|
|
490
|
+
command=command,
|
|
491
|
+
parsed=True,
|
|
492
|
+
has_syntax_error=root.has_error,
|
|
493
|
+
permission_segments=permission_segments or ((stripped,) if stripped else ()),
|
|
494
|
+
simple_commands=simple_commands,
|
|
495
|
+
has_pipeline=node_helper.contains_node_type(root, {"pipeline"}),
|
|
496
|
+
has_logical_list=node_helper.contains_node_type(root, {"list"}),
|
|
497
|
+
has_subshell=node_helper.contains_node_type(root, {"subshell"}),
|
|
498
|
+
has_command_group=node_helper.contains_node_type(root, {"compound_statement"}),
|
|
499
|
+
has_command_substitution=node_helper.contains_node_type(root, {"command_substitution"}),
|
|
500
|
+
has_process_substitution=node_helper.contains_node_type(root, {"process_substitution"}),
|
|
501
|
+
has_output_redirection=node_helper.contains_output_redirection(root),
|
|
502
|
+
output_redirection_paths=tuple(node_helper.collect_output_redirection_paths(root)),
|
|
503
|
+
nested_shell_command=self._detect_nested_shell(simple_commands),
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
def get_approval_reason(self, analysis: BashAstAnalysis) -> str | None:
|
|
507
|
+
return self._advisor.get_approval_reason(analysis)
|
|
508
|
+
|
|
509
|
+
def requires_write_permissions(self, analysis: BashAstAnalysis) -> bool:
|
|
510
|
+
return self._advisor.requires_write_permissions(analysis)
|
|
511
|
+
|
|
512
|
+
def _build_fallback_analysis(self, command: str) -> BashAstAnalysis:
|
|
513
|
+
stripped = command.strip()
|
|
514
|
+
return BashAstAnalysis(
|
|
515
|
+
command=command,
|
|
516
|
+
parsed=False,
|
|
517
|
+
has_syntax_error=False,
|
|
518
|
+
permission_segments=(stripped,) if stripped else (),
|
|
519
|
+
simple_commands=(),
|
|
520
|
+
has_pipeline="|" in stripped,
|
|
521
|
+
has_logical_list="&&" in stripped or "||" in stripped or ";" in stripped,
|
|
522
|
+
has_subshell=False,
|
|
523
|
+
has_command_group=False,
|
|
524
|
+
has_command_substitution=False,
|
|
525
|
+
has_process_substitution=False,
|
|
526
|
+
has_output_redirection=">" in stripped,
|
|
527
|
+
output_redirection_paths=(),
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
def _detect_nested_shell(
|
|
531
|
+
self,
|
|
532
|
+
simple_commands: tuple[BashSimpleCommand, ...],
|
|
533
|
+
) -> str | None:
|
|
534
|
+
for command in simple_commands:
|
|
535
|
+
normalized = command.normalized_base_command
|
|
536
|
+
if normalized not in self._SHELL_WRAPPER_COMMANDS:
|
|
537
|
+
continue
|
|
538
|
+
tokens = self._safe_split(command.text)
|
|
539
|
+
if "-c" in tokens:
|
|
540
|
+
return normalized
|
|
541
|
+
return None
|
|
542
|
+
|
|
543
|
+
@staticmethod
|
|
544
|
+
def _safe_split(command_text: str) -> list[str]:
|
|
545
|
+
try:
|
|
546
|
+
return shlex.split(command_text)
|
|
547
|
+
except ValueError:
|
|
548
|
+
return command_text.split()
|
|
549
|
+
|
|
550
|
+
@staticmethod
|
|
551
|
+
@lru_cache(maxsize=1)
|
|
552
|
+
def _get_bash_parser():
|
|
553
|
+
return get_parser("bash")
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
_DEFAULT_ANALYZER = BashAstAnalyzer()
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def analyze_command_ast(command: str) -> BashAstAnalysis:
|
|
560
|
+
"""兼容入口:分析 bash 命令 AST。"""
|
|
561
|
+
return _DEFAULT_ANALYZER.analyze(command)
|
|
562
|
+
|
|
563
|
+
|
|
564
|
+
def get_ast_approval_reason(analysis: BashAstAnalysis) -> str | None:
|
|
565
|
+
"""兼容入口:返回 AST 级审批原因。"""
|
|
566
|
+
return _DEFAULT_ANALYZER.get_approval_reason(analysis)
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def command_requires_write_permissions(command: str) -> bool:
|
|
570
|
+
"""兼容入口:判断命令是否需要写权限。"""
|
|
571
|
+
return _DEFAULT_ANALYZER.requires_write_permissions(analyze_command_ast(command))
|