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,68 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tools/bash/prompt.py — BashRuntimeTool 工具名与 prompt 模板
|
|
3
|
+
|
|
4
|
+
对应 CC prompt.ts 的 getSimplePrompt()(简化版,去掉 sandbox/git 详细指令)。
|
|
5
|
+
纯字符串/常量,无 I/O,无框架依赖,可单独测试。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
TOOL_NAME = "bash"
|
|
11
|
+
"""工具名,面向模型暴露。"""
|
|
12
|
+
|
|
13
|
+
DESCRIPTION = "Execute shell commands in a persistent bash session."
|
|
14
|
+
"""简短描述(对应 CC BashTool.description)。"""
|
|
15
|
+
|
|
16
|
+
# 预期不产生 stdout 的命令(对应 CC BASH_SILENT_COMMANDS)
|
|
17
|
+
SILENT_COMMANDS: frozenset[str] = frozenset({
|
|
18
|
+
"mv", "cp", "rm", "mkdir", "rmdir", "chmod",
|
|
19
|
+
"chown", "chgrp", "touch", "ln", "cd",
|
|
20
|
+
"export", "unset",
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_prompt() -> str:
|
|
25
|
+
"""
|
|
26
|
+
返回面向模型的完整使用说明(对应 CC getSimplePrompt())。
|
|
27
|
+
动态引用 limits 配置,确保 prompt 与实际限制一致。
|
|
28
|
+
"""
|
|
29
|
+
from .limits import get_bash_limits
|
|
30
|
+
|
|
31
|
+
limits = get_bash_limits()
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
"Executes a given bash command in a persistent shell session and returns its output.\n"
|
|
35
|
+
"\n"
|
|
36
|
+
"The working directory persists between commands. "
|
|
37
|
+
"Shell variable state does NOT persist between separate calls.\n"
|
|
38
|
+
"\n"
|
|
39
|
+
"Usage:\n"
|
|
40
|
+
f"- By default commands timeout after {limits['default_timeout_sec']} seconds "
|
|
41
|
+
f"(max {limits['max_timeout_sec']} seconds)\n"
|
|
42
|
+
"- Use the timeout parameter (in seconds) to override the default timeout\n"
|
|
43
|
+
"- Use run_in_background=true for commands you don't need to wait for "
|
|
44
|
+
"(dev servers, long builds)\n"
|
|
45
|
+
"- Commands run in sandbox mode by default; use dangerously_disable_sandbox=true "
|
|
46
|
+
"only when the runtime explicitly allows bypassing sandbox execution\n"
|
|
47
|
+
"- Stderr is merged into stdout for a unified terminal-like view\n"
|
|
48
|
+
"\n"
|
|
49
|
+
"IMPORTANT: Avoid using this tool to run find/grep/cat/head/tail/sed/awk/echo commands, "
|
|
50
|
+
"unless the user explicitly instructs it or you have verified that a dedicated tool cannot "
|
|
51
|
+
"accomplish the task.\n"
|
|
52
|
+
"\n"
|
|
53
|
+
"Prefer dedicated tools over this tool when possible:\n"
|
|
54
|
+
"- Read files: Use the read_file tool (NOT cat/head/tail)\n"
|
|
55
|
+
"- Search content: Use the grep tool (NOT grep/rg)\n"
|
|
56
|
+
"- File search: Use the glob tool (NOT find/ls)\n"
|
|
57
|
+
"- Communication: Output text directly (NOT echo/printf)\n"
|
|
58
|
+
"\n"
|
|
59
|
+
"Important restrictions:\n"
|
|
60
|
+
"- Avoid interactive commands (vim, ssh, python REPL) — they require a TTY\n"
|
|
61
|
+
f"- Avoid sleep > {limits['sleep_block_threshold_sec']}s — "
|
|
62
|
+
"use run_in_background=true instead\n"
|
|
63
|
+
"- Do not retry failing commands in a sleep loop — diagnose the root cause\n"
|
|
64
|
+
"- When issuing multiple commands:\n"
|
|
65
|
+
" - If independent, run multiple bash calls in parallel when appropriate\n"
|
|
66
|
+
" - If dependent, use a single bash call and chain with &&\n"
|
|
67
|
+
" - Use ';' only when earlier commands may fail\n"
|
|
68
|
+
)
|
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tools/bash/read_only_validation.py — BashRuntimeTool 模块1:只读分类器
|
|
3
|
+
|
|
4
|
+
职责:
|
|
5
|
+
将“命令是否可视为只读”从 `security.py` 中解耦出来,形成独立 OOP 协作者。
|
|
6
|
+
|
|
7
|
+
设计目标:
|
|
8
|
+
- 对照 CC `readOnlyValidation.ts`,提供 command/flag 级别的只读判断
|
|
9
|
+
- 大 allowlist + 危险 flag 黑名单 + 组合语义(git stash/remote/config 等)
|
|
10
|
+
- 轻量平台差异(POSIX / Windows / darwin 常见只读探测命令)
|
|
11
|
+
- 与 AST / path security 解耦,不直接做 ask / deny,只回答只读语义
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
import shlex
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
|
|
20
|
+
from langchain_agentx.utils.host_platform import (
|
|
21
|
+
HostPlatform,
|
|
22
|
+
HostPlatformDetector,
|
|
23
|
+
default_host_platform_detector,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class BashReadOnlyClassification:
|
|
29
|
+
is_read_only: bool
|
|
30
|
+
reason: str | None = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class BashReadOnlyClassifier:
|
|
34
|
+
"""命令级只读语义分类器(P2:规模 + 危险 flag + 组合语义 + 平台差异)。"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, *, host_detector: HostPlatformDetector | None = None) -> None:
|
|
37
|
+
self._host = host_detector or default_host_platform_detector()
|
|
38
|
+
|
|
39
|
+
# ------------------------------------------------------------------
|
|
40
|
+
# 大 allowlist:常见只读/观测类命令(不含必然写盘的工具如 tee/dd of=)
|
|
41
|
+
# ------------------------------------------------------------------
|
|
42
|
+
_READ_ONLY_COMMANDS = frozenset({
|
|
43
|
+
"ls", "dir", "cat", "head", "tail", "wc", "stat", "file",
|
|
44
|
+
"echo", "printf", "pwd", "env", "which", "whereis", "type", "where",
|
|
45
|
+
"grep", "egrep", "fgrep", "rg", "ag", "ack", "find",
|
|
46
|
+
"ps", "top", "htop", "df", "du", "free", "uptime", "date", "uname", "hostname",
|
|
47
|
+
"id", "whoami", "groups", "who", "w", "last", "finger",
|
|
48
|
+
"diff", "cmp", "comm", "join", "paste", "column",
|
|
49
|
+
"md5sum", "sha256sum", "sha512sum", "sha1sum", "shasum", "cksum", "sum",
|
|
50
|
+
"xxd", "hexdump", "od", "strings", "nm", "objdump", "readelf",
|
|
51
|
+
"lsof", "netstat", "ss", "ip", "ifconfig", "route",
|
|
52
|
+
"zipinfo",
|
|
53
|
+
"nl", "cut", "tr", "uniq", "expand", "unexpand", "fold", "fmt", "pr", "rev", "tac",
|
|
54
|
+
"look", "basename", "dirname", "readlink", "realpath",
|
|
55
|
+
"true", "false", "test", "expr", "seq",
|
|
56
|
+
"iconv", "locale", "locales", "getconf",
|
|
57
|
+
"sw_vers", "sysctl", "system_profiler", "pbpaste",
|
|
58
|
+
"ver",
|
|
59
|
+
"less", "more", "most", "bat", "jq", "yq", "awk", "mawk", "gawk",
|
|
60
|
+
"bc", "factor", "units", "getent",
|
|
61
|
+
"vmstat", "iostat", "mpstat", "sar", "dmesg", "tload",
|
|
62
|
+
"apropos", "whatis", "man",
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
_READ_ONLY_GIT = frozenset({
|
|
66
|
+
"log", "status", "diff", "show", "describe", "blame", "shortlog",
|
|
67
|
+
"whatchanged", "name-rev", "verify-tag", "rev-list", "ls-files", "ls-tree",
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
_FIND_WRITE_FLAGS = frozenset({
|
|
71
|
+
"-delete", "-exec", "-execdir", "-ok", "-okdir", "-quit",
|
|
72
|
+
"-fprint", "-fprintf", "-fls", "-ls", "-print0",
|
|
73
|
+
})
|
|
74
|
+
_UNZIP_READ_ONLY_FLAGS = frozenset({"l", "t", "p", "v", "Z"})
|
|
75
|
+
_RAR_READ_ONLY_ACTIONS = frozenset({"l", "lb", "lt", "t", "p", "v"})
|
|
76
|
+
_SEVEN_Z_READ_ONLY_ACTIONS = frozenset({"l", "t", "i"})
|
|
77
|
+
_STDOUT_FLAGS = frozenset({"-c", "--stdout", "--to-stdout"})
|
|
78
|
+
_ZIPNOTE_WRITE_FLAGS = frozenset({"-w", "--write"})
|
|
79
|
+
|
|
80
|
+
_SORT_WRITE_FLAGS = frozenset({"-o", "--output"})
|
|
81
|
+
|
|
82
|
+
_GIT_MUTATING_SUBCOMMANDS = frozenset({
|
|
83
|
+
"add", "apply", "am", "archive", "bisect", "checkout", "cherry-pick",
|
|
84
|
+
"clean", "clone", "commit", "fetch", "gc", "init", "merge", "mv",
|
|
85
|
+
"pull", "push", "rebase", "reset", "restore", "revert", "rm", "switch",
|
|
86
|
+
"worktree", "update-ref", "notes", "sparse-checkout", "submodule",
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
_GIT_READONLY_BUT_MUTATING_FLAGS = {
|
|
90
|
+
"tag": frozenset({"-d", "--delete", "-f", "--force"}),
|
|
91
|
+
"branch": frozenset({"-d", "-D", "-m", "-M", "-c", "-C", "--delete", "--move", "--copy"}),
|
|
92
|
+
"stash": frozenset({"push", "pop", "apply", "drop", "clear", "store", "branch", "save"}),
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
_STASH_READ_ONLY_VERBS = frozenset({"show", "list", "diff"})
|
|
96
|
+
|
|
97
|
+
_DOCKER_READ_ONLY = frozenset({
|
|
98
|
+
"ps", "images", "inspect", "version", "info", "top", "stats", "events",
|
|
99
|
+
"history", "logs",
|
|
100
|
+
})
|
|
101
|
+
_DOCKER_MUTATING = frozenset({
|
|
102
|
+
"run", "exec", "cp", "build", "pull", "push", "rm", "rmi", "create", "start",
|
|
103
|
+
"stop", "kill", "restart", "commit", "import", "load", "save", "tag", "update",
|
|
104
|
+
"volume", "network", "system",
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
_KUBECTL_READ_ONLY = frozenset({
|
|
108
|
+
"get", "describe", "explain", "version", "api-resources", "api-versions",
|
|
109
|
+
"cluster-info", "logs", "top", "auth", "config",
|
|
110
|
+
})
|
|
111
|
+
_KUBECTL_MUTATING = frozenset({
|
|
112
|
+
"apply", "create", "delete", "patch", "replace", "run", "expose", "scale",
|
|
113
|
+
"rollout", "label", "annotate", "cordon", "uncordon", "drain", "taint",
|
|
114
|
+
"port-forward", "attach", "exec", "cp",
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
def _platform_readonly_extras(self) -> frozenset[str]:
|
|
118
|
+
pf = self._platform_family()
|
|
119
|
+
if pf == "win32":
|
|
120
|
+
return frozenset({"findstr", "fc", "where"})
|
|
121
|
+
if pf == "darwin":
|
|
122
|
+
return frozenset({"vm_stat"})
|
|
123
|
+
return frozenset()
|
|
124
|
+
|
|
125
|
+
def classify(self, command: str) -> BashReadOnlyClassification:
|
|
126
|
+
if self._shell_compound_or_pipeline(command):
|
|
127
|
+
return BashReadOnlyClassification(False, "shell_compound_or_pipeline")
|
|
128
|
+
|
|
129
|
+
tokens = self._split(command)
|
|
130
|
+
if not tokens:
|
|
131
|
+
return BashReadOnlyClassification(False, "empty_command")
|
|
132
|
+
|
|
133
|
+
base = self._basename(tokens[0])
|
|
134
|
+
|
|
135
|
+
if base in {"cmd", "cmd.exe", "powershell", "powershell.exe", "pwsh", "pwsh.exe"}:
|
|
136
|
+
return BashReadOnlyClassification(False, f"{base}_shell_not_classified")
|
|
137
|
+
|
|
138
|
+
if base == "git":
|
|
139
|
+
return self._classify_git(tokens)
|
|
140
|
+
|
|
141
|
+
if base == "sort":
|
|
142
|
+
if self._tokens_contain_any_flag(tokens, self._SORT_WRITE_FLAGS):
|
|
143
|
+
return BashReadOnlyClassification(False, "sort_output_file_flag")
|
|
144
|
+
return BashReadOnlyClassification(True, "sort_stdout")
|
|
145
|
+
|
|
146
|
+
if base == "tee":
|
|
147
|
+
return BashReadOnlyClassification(False, "tee_writes")
|
|
148
|
+
|
|
149
|
+
if base == "dd":
|
|
150
|
+
if self._dd_has_output_file(command):
|
|
151
|
+
return BashReadOnlyClassification(False, "dd_output_file")
|
|
152
|
+
return BashReadOnlyClassification(True, "dd_stdout_only")
|
|
153
|
+
|
|
154
|
+
if base in {"tar", "bsdtar"}:
|
|
155
|
+
return BashReadOnlyClassification(self._has_tar_list_mode(tokens), "tar_mode")
|
|
156
|
+
|
|
157
|
+
if base == "unzip":
|
|
158
|
+
return BashReadOnlyClassification(self._has_unzip_read_only_mode(tokens), "unzip_mode")
|
|
159
|
+
|
|
160
|
+
if base in {"7z", "7za", "7zr"}:
|
|
161
|
+
return BashReadOnlyClassification(
|
|
162
|
+
self._7z_action(tokens) in self._SEVEN_Z_READ_ONLY_ACTIONS,
|
|
163
|
+
"7z_action",
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
if base == "jar":
|
|
167
|
+
return BashReadOnlyClassification(self._jar_mode_is_read_only(tokens), "jar_mode")
|
|
168
|
+
|
|
169
|
+
if base in {"rar", "unrar"}:
|
|
170
|
+
action = tokens[1].lower() if len(tokens) > 1 else ""
|
|
171
|
+
return BashReadOnlyClassification(action in self._RAR_READ_ONLY_ACTIONS, "rar_action")
|
|
172
|
+
|
|
173
|
+
if base == "zipnote":
|
|
174
|
+
has_write = any(token in self._ZIPNOTE_WRITE_FLAGS for token in tokens[1:])
|
|
175
|
+
return BashReadOnlyClassification(not has_write, "zipnote_mode")
|
|
176
|
+
|
|
177
|
+
if base in {"gzip", "gunzip", "xz", "unxz", "bzip2", "bunzip2", "zstd", "unzstd", "lz4"}:
|
|
178
|
+
return BashReadOnlyClassification(self._has_stdout_mode(tokens), f"{base}_stdout")
|
|
179
|
+
|
|
180
|
+
if base in {"curl", "wget"}:
|
|
181
|
+
return self._classify_http_fetch(tokens, base)
|
|
182
|
+
|
|
183
|
+
if base == "cpio":
|
|
184
|
+
return BashReadOnlyClassification(self._cpio_list_mode(tokens), "cpio_mode")
|
|
185
|
+
|
|
186
|
+
if base == "docker":
|
|
187
|
+
return self._classify_docker(tokens)
|
|
188
|
+
|
|
189
|
+
if base == "kustomize":
|
|
190
|
+
return self._classify_kustomize(tokens)
|
|
191
|
+
if base in {"kubectl", "oc"}:
|
|
192
|
+
return self._classify_kubectl(tokens, base)
|
|
193
|
+
|
|
194
|
+
if base == "sed":
|
|
195
|
+
return self._classify_sed(tokens)
|
|
196
|
+
|
|
197
|
+
if base in {"awk", "mawk", "gawk"}:
|
|
198
|
+
return self._classify_awk(tokens)
|
|
199
|
+
|
|
200
|
+
if base in {"perl", "perl5"}:
|
|
201
|
+
return self._classify_perl(tokens)
|
|
202
|
+
|
|
203
|
+
if base in self._READ_ONLY_COMMANDS or base in self._platform_readonly_extras():
|
|
204
|
+
if base == "find" and any(token in self._FIND_WRITE_FLAGS for token in tokens[1:]):
|
|
205
|
+
return BashReadOnlyClassification(False, "find_write_flag")
|
|
206
|
+
return BashReadOnlyClassification(True, f"{base}_readonly")
|
|
207
|
+
|
|
208
|
+
return BashReadOnlyClassification(False, f"{base}_unknown")
|
|
209
|
+
|
|
210
|
+
@staticmethod
|
|
211
|
+
def _shell_compound_or_pipeline(command: str) -> bool:
|
|
212
|
+
"""单 segment 内的 `|` / `&&` / `||` 保守视为非只读(不含 `;`,避免 awk/sed 脚本误判)。"""
|
|
213
|
+
if "|" in command:
|
|
214
|
+
return True
|
|
215
|
+
if "&&" in command or "||" in command:
|
|
216
|
+
return True
|
|
217
|
+
return False
|
|
218
|
+
|
|
219
|
+
def _classify_kustomize(self, tokens: list[str]) -> BashReadOnlyClassification:
|
|
220
|
+
if len(tokens) > 1 and tokens[1].lower() == "build":
|
|
221
|
+
return BashReadOnlyClassification(True, "kustomize_build")
|
|
222
|
+
return BashReadOnlyClassification(False, "kustomize_not_classified")
|
|
223
|
+
|
|
224
|
+
def _classify_sed(self, tokens: list[str]) -> BashReadOnlyClassification:
|
|
225
|
+
if self._sed_has_inplace(tokens):
|
|
226
|
+
return BashReadOnlyClassification(False, "sed_inplace")
|
|
227
|
+
return BashReadOnlyClassification(True, "sed_stdout")
|
|
228
|
+
|
|
229
|
+
def _sed_has_inplace(self, tokens: list[str]) -> bool:
|
|
230
|
+
for i, t in enumerate(tokens[1:], start=1):
|
|
231
|
+
if t == "--in-place":
|
|
232
|
+
return True
|
|
233
|
+
if t.startswith("-i") and t != "-i":
|
|
234
|
+
return True
|
|
235
|
+
if t == "-i":
|
|
236
|
+
return True
|
|
237
|
+
if t.startswith("-i") and len(t) > 2:
|
|
238
|
+
return True
|
|
239
|
+
return False
|
|
240
|
+
|
|
241
|
+
def _classify_awk(self, tokens: list[str]) -> BashReadOnlyClassification:
|
|
242
|
+
args = tokens[1:]
|
|
243
|
+
if self._awk_inplace_gnu(args):
|
|
244
|
+
return BashReadOnlyClassification(False, "awk_inplace")
|
|
245
|
+
return BashReadOnlyClassification(True, "awk_stdout")
|
|
246
|
+
|
|
247
|
+
@staticmethod
|
|
248
|
+
def _awk_inplace_gnu(args: list[str]) -> bool:
|
|
249
|
+
for i, a in enumerate(args):
|
|
250
|
+
if a == "-i" and i + 1 < len(args) and args[i + 1] == "inplace":
|
|
251
|
+
return True
|
|
252
|
+
return False
|
|
253
|
+
|
|
254
|
+
def _classify_perl(self, tokens: list[str]) -> BashReadOnlyClassification:
|
|
255
|
+
for t in tokens[1:]:
|
|
256
|
+
if t == "-i" or (t.startswith("-i") and len(t) > 2):
|
|
257
|
+
return BashReadOnlyClassification(False, "perl_inplace")
|
|
258
|
+
return BashReadOnlyClassification(True, "perl_stdout")
|
|
259
|
+
|
|
260
|
+
def _cpio_list_mode(self, tokens: list[str]) -> bool:
|
|
261
|
+
"""仅列出归档内容(-it / -t + -F archive)视为只读;extract/create/pass 非只读。"""
|
|
262
|
+
has_t = False
|
|
263
|
+
has_i = False
|
|
264
|
+
has_o = False
|
|
265
|
+
has_p = False
|
|
266
|
+
for t in tokens[1:]:
|
|
267
|
+
if t in ("-t", "--list"):
|
|
268
|
+
has_t = True
|
|
269
|
+
continue
|
|
270
|
+
if t.startswith("-") and not t.startswith("--"):
|
|
271
|
+
body = t[1:]
|
|
272
|
+
if "t" in body:
|
|
273
|
+
has_t = True
|
|
274
|
+
if "i" in body:
|
|
275
|
+
has_i = True
|
|
276
|
+
if "o" in body:
|
|
277
|
+
has_o = True
|
|
278
|
+
if "p" in body:
|
|
279
|
+
has_p = True
|
|
280
|
+
if has_o or has_p:
|
|
281
|
+
return False
|
|
282
|
+
if has_i and has_t:
|
|
283
|
+
return True
|
|
284
|
+
return False
|
|
285
|
+
|
|
286
|
+
def _classify_docker(self, tokens: list[str]) -> BashReadOnlyClassification:
|
|
287
|
+
sub, rest = self._docker_primary_and_rest(tokens)
|
|
288
|
+
if not sub:
|
|
289
|
+
return BashReadOnlyClassification(False, "docker_empty")
|
|
290
|
+
if sub in {"compose", "buildx"}:
|
|
291
|
+
return BashReadOnlyClassification(False, f"docker_{sub}_not_classified")
|
|
292
|
+
if sub in self._DOCKER_NETWORK_VOLUME_SYSTEM:
|
|
293
|
+
return self._classify_docker_plugin(sub, rest)
|
|
294
|
+
if sub in self._DOCKER_READ_ONLY:
|
|
295
|
+
return BashReadOnlyClassification(True, f"docker_{sub}_readonly")
|
|
296
|
+
if sub in self._DOCKER_MUTATING:
|
|
297
|
+
return BashReadOnlyClassification(False, f"docker_{sub}_mutating")
|
|
298
|
+
return BashReadOnlyClassification(False, "docker_unknown")
|
|
299
|
+
|
|
300
|
+
_DOCKER_NETWORK_VOLUME_SYSTEM = frozenset({"network", "volume", "system"})
|
|
301
|
+
|
|
302
|
+
def _classify_docker_plugin(self, plugin: str, rest: list[str]) -> BashReadOnlyClassification:
|
|
303
|
+
verb = self._git_first_non_flag(rest).lower()
|
|
304
|
+
if plugin == "network" and verb in {"ls", "list", "inspect"}:
|
|
305
|
+
return BashReadOnlyClassification(True, "docker_network_readonly")
|
|
306
|
+
if plugin == "volume" and verb in {"ls", "list", "inspect"}:
|
|
307
|
+
return BashReadOnlyClassification(True, "docker_volume_readonly")
|
|
308
|
+
if plugin == "system" and verb in {"df", "info", "events", "prune"}:
|
|
309
|
+
if verb == "prune":
|
|
310
|
+
return BashReadOnlyClassification(False, "docker_system_prune")
|
|
311
|
+
return BashReadOnlyClassification(True, f"docker_system_{verb}")
|
|
312
|
+
return BashReadOnlyClassification(False, f"docker_{plugin}_unknown")
|
|
313
|
+
|
|
314
|
+
@staticmethod
|
|
315
|
+
def _docker_primary_and_rest(tokens: list[str]) -> tuple[str, list[str]]:
|
|
316
|
+
i = 1
|
|
317
|
+
n = len(tokens)
|
|
318
|
+
while i < n:
|
|
319
|
+
t = tokens[i]
|
|
320
|
+
if t in ("-H", "--host", "-c", "--context", "--config") and i + 1 < n:
|
|
321
|
+
i += 2
|
|
322
|
+
continue
|
|
323
|
+
if t.startswith("-"):
|
|
324
|
+
i += 1
|
|
325
|
+
continue
|
|
326
|
+
return t.lower(), tokens[i + 1 :]
|
|
327
|
+
return "", []
|
|
328
|
+
|
|
329
|
+
def _classify_kubectl(self, tokens: list[str], base: str) -> BashReadOnlyClassification:
|
|
330
|
+
verb, rest = self._kubectl_verb_and_rest(tokens)
|
|
331
|
+
if not verb:
|
|
332
|
+
return BashReadOnlyClassification(False, f"{base}_empty")
|
|
333
|
+
if verb == "config":
|
|
334
|
+
return self._classify_kubectl_config(rest)
|
|
335
|
+
if verb in self._KUBECTL_READ_ONLY:
|
|
336
|
+
return BashReadOnlyClassification(True, f"{base}_{verb}_readonly")
|
|
337
|
+
if verb in self._KUBECTL_MUTATING:
|
|
338
|
+
return BashReadOnlyClassification(False, f"{base}_{verb}_mutating")
|
|
339
|
+
return BashReadOnlyClassification(False, f"{base}_{verb}_unknown")
|
|
340
|
+
|
|
341
|
+
def _classify_kubectl_config(self, rest: list[str]) -> BashReadOnlyClassification:
|
|
342
|
+
"""kubectl config view|get-contexts 只读;set-credentials/set-context 等变更。"""
|
|
343
|
+
sub = self._git_first_non_flag(rest).lower()
|
|
344
|
+
if sub in {"view", "get-contexts", "get-clusters", "current-context"}:
|
|
345
|
+
return BashReadOnlyClassification(True, "kubectl_config_readonly")
|
|
346
|
+
if sub in {
|
|
347
|
+
"set", "use-context", "rename-context", "delete-context",
|
|
348
|
+
"set-cluster", "set-credentials", "unset",
|
|
349
|
+
}:
|
|
350
|
+
return BashReadOnlyClassification(False, "kubectl_config_mutating")
|
|
351
|
+
if not sub:
|
|
352
|
+
return BashReadOnlyClassification(False, "kubectl_config_empty")
|
|
353
|
+
return BashReadOnlyClassification(False, "kubectl_config_unknown")
|
|
354
|
+
|
|
355
|
+
@staticmethod
|
|
356
|
+
def _kubectl_verb_and_rest(tokens: list[str]) -> tuple[str, list[str]]:
|
|
357
|
+
i = 1
|
|
358
|
+
n = len(tokens)
|
|
359
|
+
while i < n:
|
|
360
|
+
t = tokens[i]
|
|
361
|
+
if not t.startswith("-"):
|
|
362
|
+
return t.lower(), tokens[i + 1 :]
|
|
363
|
+
if t in (
|
|
364
|
+
"-n", "--namespace", "-o", "--output", "--kubeconfig", "--context",
|
|
365
|
+
"--server", "-f", "--filename", "-l", "--selector",
|
|
366
|
+
) and i + 1 < n:
|
|
367
|
+
i += 2
|
|
368
|
+
continue
|
|
369
|
+
if t == "--":
|
|
370
|
+
i += 1
|
|
371
|
+
continue
|
|
372
|
+
i += 1
|
|
373
|
+
return "", []
|
|
374
|
+
|
|
375
|
+
def _git_skip_global_options(self, tokens: list[str], start: int) -> int:
|
|
376
|
+
i = start
|
|
377
|
+
while i < len(tokens):
|
|
378
|
+
t = tokens[i]
|
|
379
|
+
if t in ("-C", "--git-dir", "--work-tree") and i + 1 < len(tokens):
|
|
380
|
+
i += 2
|
|
381
|
+
continue
|
|
382
|
+
if t.startswith("-"):
|
|
383
|
+
i += 1
|
|
384
|
+
continue
|
|
385
|
+
break
|
|
386
|
+
return i
|
|
387
|
+
|
|
388
|
+
def _git_primary_and_rest(self, tokens: list[str]) -> tuple[str, list[str]]:
|
|
389
|
+
i = self._git_skip_global_options(tokens, 1)
|
|
390
|
+
if i >= len(tokens):
|
|
391
|
+
return "", []
|
|
392
|
+
return tokens[i], tokens[i + 1 :]
|
|
393
|
+
|
|
394
|
+
@staticmethod
|
|
395
|
+
def _git_first_non_flag(rest: list[str]) -> str:
|
|
396
|
+
for t in rest:
|
|
397
|
+
if not t.startswith("-"):
|
|
398
|
+
return t
|
|
399
|
+
return ""
|
|
400
|
+
|
|
401
|
+
def _classify_git(self, tokens: list[str]) -> BashReadOnlyClassification:
|
|
402
|
+
subcommand, rest = self._git_primary_and_rest(tokens)
|
|
403
|
+
if not subcommand:
|
|
404
|
+
return BashReadOnlyClassification(False, "git_empty")
|
|
405
|
+
|
|
406
|
+
if subcommand == "config":
|
|
407
|
+
return self._classify_git_config(rest)
|
|
408
|
+
|
|
409
|
+
if subcommand in self._GIT_MUTATING_SUBCOMMANDS:
|
|
410
|
+
return BashReadOnlyClassification(False, f"git_{subcommand}_mutating")
|
|
411
|
+
|
|
412
|
+
if subcommand == "stash":
|
|
413
|
+
if any(
|
|
414
|
+
x in rest
|
|
415
|
+
for x in ("-p", "--patch", "-u", "--include-untracked", "-a", "--all", "-m", "--message")
|
|
416
|
+
):
|
|
417
|
+
return BashReadOnlyClassification(False, "git_stash_create_flags")
|
|
418
|
+
verb = self._git_first_non_flag(rest)
|
|
419
|
+
if verb in self._GIT_READONLY_BUT_MUTATING_FLAGS["stash"]:
|
|
420
|
+
return BashReadOnlyClassification(False, "git_stash_mutating")
|
|
421
|
+
if verb in self._STASH_READ_ONLY_VERBS:
|
|
422
|
+
return BashReadOnlyClassification(True, "git_stash_readonly")
|
|
423
|
+
if not verb:
|
|
424
|
+
return BashReadOnlyClassification(True, "git_stash_list_default")
|
|
425
|
+
return BashReadOnlyClassification(False, "git_stash_unknown")
|
|
426
|
+
|
|
427
|
+
if subcommand == "remote":
|
|
428
|
+
verb = self._git_first_non_flag(rest)
|
|
429
|
+
mutating = {"add", "remove", "rename", "set-url", "set-branches", "prune", "update"}
|
|
430
|
+
if verb in mutating:
|
|
431
|
+
return BashReadOnlyClassification(False, "git_remote_mutating")
|
|
432
|
+
return BashReadOnlyClassification(True, "git_remote_readonly")
|
|
433
|
+
|
|
434
|
+
if subcommand == "branch":
|
|
435
|
+
if self._git_scan_mutating_flags("branch", rest):
|
|
436
|
+
return BashReadOnlyClassification(False, "git_branch_mutating_flag")
|
|
437
|
+
return BashReadOnlyClassification(True, "git_branch_list")
|
|
438
|
+
|
|
439
|
+
if subcommand == "tag":
|
|
440
|
+
if self._git_scan_mutating_flags("tag", rest):
|
|
441
|
+
return BashReadOnlyClassification(False, "git_tag_mutating_flag")
|
|
442
|
+
return BashReadOnlyClassification(True, "git_tag_list")
|
|
443
|
+
|
|
444
|
+
if self._git_scan_mutating_flags(subcommand, rest):
|
|
445
|
+
return BashReadOnlyClassification(False, f"git_{subcommand}_mutating_flag")
|
|
446
|
+
|
|
447
|
+
is_ro = subcommand in self._READ_ONLY_GIT
|
|
448
|
+
return BashReadOnlyClassification(is_ro, f"git_{subcommand}")
|
|
449
|
+
|
|
450
|
+
def _classify_git_config(self, rest: list[str]) -> BashReadOnlyClassification:
|
|
451
|
+
if any(
|
|
452
|
+
x in rest
|
|
453
|
+
for x in (
|
|
454
|
+
"--add", "--unset", "--unset-all", "--replace-all",
|
|
455
|
+
"--remove-section", "--rename-section",
|
|
456
|
+
)
|
|
457
|
+
):
|
|
458
|
+
return BashReadOnlyClassification(False, "git_config_mutating_flag")
|
|
459
|
+
if "--list" in rest or "-l" in rest:
|
|
460
|
+
return BashReadOnlyClassification(True, "git_config_list")
|
|
461
|
+
if any(x in rest for x in ("--get", "--get-all", "--get-regexp", "--get-urlmatch")):
|
|
462
|
+
return BashReadOnlyClassification(True, "git_config_get")
|
|
463
|
+
# 位置参数:仅 section.key → 查询;存在赋值(两段子键值或含 =)→ 变更
|
|
464
|
+
pos: list[str] = []
|
|
465
|
+
i = 0
|
|
466
|
+
while i < len(rest):
|
|
467
|
+
t = rest[i]
|
|
468
|
+
if t.startswith("-"):
|
|
469
|
+
if t in ("--file", "-f") and i + 1 < len(rest):
|
|
470
|
+
i += 2
|
|
471
|
+
continue
|
|
472
|
+
i += 1
|
|
473
|
+
continue
|
|
474
|
+
pos.append(t)
|
|
475
|
+
i += 1
|
|
476
|
+
if len(pos) >= 2:
|
|
477
|
+
return BashReadOnlyClassification(False, "git_config_set_value")
|
|
478
|
+
if len(pos) == 1 and "=" in pos[0]:
|
|
479
|
+
return BashReadOnlyClassification(False, "git_config_set_inline")
|
|
480
|
+
if len(pos) == 1:
|
|
481
|
+
return BashReadOnlyClassification(True, "git_config_get_key")
|
|
482
|
+
return BashReadOnlyClassification(False, "git_config_ambiguous")
|
|
483
|
+
|
|
484
|
+
def _git_scan_mutating_flags(self, subcommand: str, rest: list[str]) -> bool:
|
|
485
|
+
rules = self._GIT_READONLY_BUT_MUTATING_FLAGS.get(subcommand)
|
|
486
|
+
if not rules:
|
|
487
|
+
return False
|
|
488
|
+
return any(tok in rules for tok in rest)
|
|
489
|
+
|
|
490
|
+
def _classify_http_fetch(self, tokens: list[str], base: str) -> BashReadOnlyClassification:
|
|
491
|
+
if base == "curl":
|
|
492
|
+
if self._tokens_contain_any_flag(
|
|
493
|
+
tokens,
|
|
494
|
+
{"-o", "--output", "-O", "--remote-name", "--remote-name-all"},
|
|
495
|
+
):
|
|
496
|
+
return BashReadOnlyClassification(False, "curl_local_write")
|
|
497
|
+
return BashReadOnlyClassification(True, "curl_stdout")
|
|
498
|
+
if base == "wget":
|
|
499
|
+
if any(t in tokens for t in ("-O", "--output-document")):
|
|
500
|
+
doc = self._wget_output_document(tokens)
|
|
501
|
+
if doc and doc != "-":
|
|
502
|
+
return BashReadOnlyClassification(False, "wget_local_write")
|
|
503
|
+
if "-P" in tokens or "--directory-prefix" in tokens:
|
|
504
|
+
return BashReadOnlyClassification(False, "wget_local_write")
|
|
505
|
+
joined = " ".join(tokens)
|
|
506
|
+
if "-O-" in joined or any(
|
|
507
|
+
i < len(tokens) - 1 and tokens[i] == "-O" and tokens[i + 1] == "-"
|
|
508
|
+
for i in range(len(tokens))
|
|
509
|
+
):
|
|
510
|
+
return BashReadOnlyClassification(True, "wget_stdout")
|
|
511
|
+
return BashReadOnlyClassification(False, "wget_default_file")
|
|
512
|
+
|
|
513
|
+
return BashReadOnlyClassification(False, "http_fetch_unknown")
|
|
514
|
+
|
|
515
|
+
def _wget_output_document(self, tokens: list[str]) -> str | None:
|
|
516
|
+
for i, t in enumerate(tokens):
|
|
517
|
+
if t in ("-O", "--output-document") and i + 1 < len(tokens):
|
|
518
|
+
return tokens[i + 1]
|
|
519
|
+
return None
|
|
520
|
+
|
|
521
|
+
@staticmethod
|
|
522
|
+
def _dd_has_output_file(command: str) -> bool:
|
|
523
|
+
return bool(re.search(r"(?:^|\s)of=", command))
|
|
524
|
+
|
|
525
|
+
@staticmethod
|
|
526
|
+
def _tokens_contain_any_flag(tokens: list[str], flags: set[str]) -> bool:
|
|
527
|
+
for t in tokens[1:]:
|
|
528
|
+
if t in flags:
|
|
529
|
+
return True
|
|
530
|
+
if t.startswith("-") and not t.startswith("--"):
|
|
531
|
+
for fl in flags:
|
|
532
|
+
if fl.startswith("-") and len(fl) == 2 and fl[1] in t[1:]:
|
|
533
|
+
return True
|
|
534
|
+
return False
|
|
535
|
+
|
|
536
|
+
@staticmethod
|
|
537
|
+
def _split(command: str) -> list[str]:
|
|
538
|
+
try:
|
|
539
|
+
return shlex.split(command)
|
|
540
|
+
except ValueError:
|
|
541
|
+
return command.split()
|
|
542
|
+
|
|
543
|
+
@staticmethod
|
|
544
|
+
def _basename(token: str) -> str:
|
|
545
|
+
return token.rsplit("/", 1)[-1]
|
|
546
|
+
|
|
547
|
+
@staticmethod
|
|
548
|
+
def _has_tar_list_mode(tokens: list[str]) -> bool:
|
|
549
|
+
for token in tokens[1:]:
|
|
550
|
+
if token == "--list":
|
|
551
|
+
return True
|
|
552
|
+
if token.startswith("-") and not token.startswith("--") and "t" in token and "x" not in token:
|
|
553
|
+
return True
|
|
554
|
+
return False
|
|
555
|
+
|
|
556
|
+
def _has_unzip_read_only_mode(self, tokens: list[str]) -> bool:
|
|
557
|
+
for token in tokens[1:]:
|
|
558
|
+
if token.startswith("--"):
|
|
559
|
+
continue
|
|
560
|
+
if token.startswith("-") and any(flag in token for flag in self._UNZIP_READ_ONLY_FLAGS):
|
|
561
|
+
return True
|
|
562
|
+
return False
|
|
563
|
+
|
|
564
|
+
@staticmethod
|
|
565
|
+
def _7z_action(tokens: list[str]) -> str:
|
|
566
|
+
for token in tokens[1:]:
|
|
567
|
+
if token.startswith("-"):
|
|
568
|
+
continue
|
|
569
|
+
return token.lower()
|
|
570
|
+
return ""
|
|
571
|
+
|
|
572
|
+
@staticmethod
|
|
573
|
+
def _jar_mode_is_read_only(tokens: list[str]) -> bool:
|
|
574
|
+
if len(tokens) < 2:
|
|
575
|
+
return False
|
|
576
|
+
mode_token = tokens[1].lstrip("-")
|
|
577
|
+
return "t" in mode_token and "x" not in mode_token and "c" not in mode_token and "u" not in mode_token
|
|
578
|
+
|
|
579
|
+
def _has_stdout_mode(self, tokens: list[str]) -> bool:
|
|
580
|
+
return any(token in self._STDOUT_FLAGS for token in tokens[1:])
|
|
581
|
+
|
|
582
|
+
def _platform_family(self) -> str:
|
|
583
|
+
"""与历史 ``sys.platform`` 三分支对齐;WSL/Linux 归入 posix extras。"""
|
|
584
|
+
kind = self._host.current()
|
|
585
|
+
if kind == HostPlatform.WINDOWS:
|
|
586
|
+
return "win32"
|
|
587
|
+
if kind == HostPlatform.MACOS:
|
|
588
|
+
return "darwin"
|
|
589
|
+
return "posix"
|