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.
Files changed (354) hide show
  1. langchain_agentx/__init__.py +46 -0
  2. langchain_agentx/command/__init__.py +28 -0
  3. langchain_agentx/command/builtin/__init__.py +25 -0
  4. langchain_agentx/command/builtin/clear.py +33 -0
  5. langchain_agentx/command/builtin/compact.py +33 -0
  6. langchain_agentx/command/builtin/memory.py +37 -0
  7. langchain_agentx/command/builtin/reload_plugins.py +42 -0
  8. langchain_agentx/command/context.py +30 -0
  9. langchain_agentx/command/dispatcher.py +183 -0
  10. langchain_agentx/command/registry.py +110 -0
  11. langchain_agentx/command/result.py +25 -0
  12. langchain_agentx/command/types.py +41 -0
  13. langchain_agentx/config/__init__.py +14 -0
  14. langchain_agentx/loop/__init__.py +47 -0
  15. langchain_agentx/loop/config/__init__.py +20 -0
  16. langchain_agentx/loop/config/agent_config.py +66 -0
  17. langchain_agentx/loop/config/agent_loop_config.py +72 -0
  18. langchain_agentx/loop/config/model_context_resolver.py +105 -0
  19. langchain_agentx/loop/config/runtime_settings.py +50 -0
  20. langchain_agentx/loop/config/token_estimator.py +133 -0
  21. langchain_agentx/loop/context/__init__.py +66 -0
  22. langchain_agentx/loop/context/blocking_guard.py +97 -0
  23. langchain_agentx/loop/context/compaction_service.py +60 -0
  24. langchain_agentx/loop/context/message_utils.py +56 -0
  25. langchain_agentx/loop/context/pipeline.py +127 -0
  26. langchain_agentx/loop/context/settings.py +103 -0
  27. langchain_agentx/loop/context/stages/__init__.py +29 -0
  28. langchain_agentx/loop/context/stages/autocompact.py +140 -0
  29. langchain_agentx/loop/context/stages/base.py +32 -0
  30. langchain_agentx/loop/context/stages/collapse.py +76 -0
  31. langchain_agentx/loop/context/stages/microcompact.py +76 -0
  32. langchain_agentx/loop/context/stages/noop.py +33 -0
  33. langchain_agentx/loop/context/stages/snip.py +71 -0
  34. langchain_agentx/loop/context/stages/tool_result_budget.py +69 -0
  35. langchain_agentx/loop/context/types.py +79 -0
  36. langchain_agentx/loop/exit/__init__.py +1 -0
  37. langchain_agentx/loop/exit/exit_logic.py +320 -0
  38. langchain_agentx/loop/exit/reason_codes.py +39 -0
  39. langchain_agentx/loop/graph/__init__.py +5 -0
  40. langchain_agentx/loop/graph/builtin_loop_control.py +197 -0
  41. langchain_agentx/loop/graph/factory.py +1409 -0
  42. langchain_agentx/loop/graph/graph_edges.py +820 -0
  43. langchain_agentx/loop/hook/__init__.py +48 -0
  44. langchain_agentx/loop/hook/async_hook_runner.py +62 -0
  45. langchain_agentx/loop/hook/config.py +280 -0
  46. langchain_agentx/loop/hook/engine.py +321 -0
  47. langchain_agentx/loop/hook/executors/__init__.py +9 -0
  48. langchain_agentx/loop/hook/executors/agent.py +107 -0
  49. langchain_agentx/loop/hook/executors/command.py +230 -0
  50. langchain_agentx/loop/hook/executors/http.py +114 -0
  51. langchain_agentx/loop/hook/executors/prompt.py +92 -0
  52. langchain_agentx/loop/hook/graph_wiring.py +134 -0
  53. langchain_agentx/loop/hook/registry.py +262 -0
  54. langchain_agentx/loop/hook/trust.py +43 -0
  55. langchain_agentx/loop/hook/types.py +110 -0
  56. langchain_agentx/loop/injection/__init__.py +13 -0
  57. langchain_agentx/loop/injection/dedup.py +74 -0
  58. langchain_agentx/loop/loop_abort.py +36 -0
  59. langchain_agentx/loop/model/__init__.py +1 -0
  60. langchain_agentx/loop/model/model_node.py +648 -0
  61. langchain_agentx/loop/model/model_nodes.py +661 -0
  62. langchain_agentx/loop/model/orphan_tool_results.py +38 -0
  63. langchain_agentx/loop/model/retrier.py +307 -0
  64. langchain_agentx/loop/model/retry_bridge.py +58 -0
  65. langchain_agentx/loop/model/retry_events.py +35 -0
  66. langchain_agentx/loop/model/retry_policy.py +56 -0
  67. langchain_agentx/loop/model/schema_and_format.py +153 -0
  68. langchain_agentx/loop/model/tool_and_model_binding.py +227 -0
  69. langchain_agentx/loop/model/tool_call_degradation_corrector.py +443 -0
  70. langchain_agentx/loop/model/tool_transcript_guard.py +225 -0
  71. langchain_agentx/loop/prompt/__init__.py +95 -0
  72. langchain_agentx/loop/prompt/builder.py +61 -0
  73. langchain_agentx/loop/prompt/builtin.py +218 -0
  74. langchain_agentx/loop/prompt/compact.py +408 -0
  75. langchain_agentx/loop/prompt/sections.py +120 -0
  76. langchain_agentx/loop/runtime/__init__.py +19 -0
  77. langchain_agentx/loop/runtime/context.py +34 -0
  78. langchain_agentx/loop/runtime/context_factory.py +107 -0
  79. langchain_agentx/loop/runtime/subagent_execution_paths.py +68 -0
  80. langchain_agentx/loop/subagent/__init__.py +53 -0
  81. langchain_agentx/loop/subagent/async_runner.py +215 -0
  82. langchain_agentx/loop/subagent/context.py +209 -0
  83. langchain_agentx/loop/subagent/fork_worktree_notice.py +25 -0
  84. langchain_agentx/loop/subagent/graph.py +72 -0
  85. langchain_agentx/loop/subagent/orchestrator.py +391 -0
  86. langchain_agentx/loop/subagent/progress.py +30 -0
  87. langchain_agentx/loop/subagent/prompt.py +52 -0
  88. langchain_agentx/loop/subagent/runner.py +504 -0
  89. langchain_agentx/loop/subagent/transcript.py +172 -0
  90. langchain_agentx/memory/__init__.py +2 -0
  91. langchain_agentx/memory/instruction/__init__.py +12 -0
  92. langchain_agentx/memory/instruction/loader.py +325 -0
  93. langchain_agentx/memory/instruction/resolver.py +24 -0
  94. langchain_agentx/memory/instruction/runtime.py +83 -0
  95. langchain_agentx/memory/instruction/sections.py +83 -0
  96. langchain_agentx/memory/instruction/types.py +59 -0
  97. langchain_agentx/memory/memdir/__init__.py +77 -0
  98. langchain_agentx/memory/memdir/age.py +36 -0
  99. langchain_agentx/memory/memdir/agent_memory.py +380 -0
  100. langchain_agentx/memory/memdir/extractor.py +309 -0
  101. langchain_agentx/memory/memdir/loader.py +187 -0
  102. langchain_agentx/memory/memdir/paths.py +63 -0
  103. langchain_agentx/memory/memdir/recall.py +45 -0
  104. langchain_agentx/memory/memdir/runtime.py +43 -0
  105. langchain_agentx/memory/memdir/scan.py +135 -0
  106. langchain_agentx/memory/memdir/types.py +104 -0
  107. langchain_agentx/memory/session/__init__.py +76 -0
  108. langchain_agentx/memory/session/compact_bridge.py +208 -0
  109. langchain_agentx/memory/session/prompts.py +172 -0
  110. langchain_agentx/memory/session/session_memory.py +282 -0
  111. langchain_agentx/observability/__init__.py +67 -0
  112. langchain_agentx/observability/evaluation/__init__.py +17 -0
  113. langchain_agentx/observability/evaluation/checkers/__init__.py +18 -0
  114. langchain_agentx/observability/evaluation/checkers/base.py +34 -0
  115. langchain_agentx/observability/evaluation/checkers/compaction.py +38 -0
  116. langchain_agentx/observability/evaluation/checkers/degradation.py +50 -0
  117. langchain_agentx/observability/evaluation/checkers/exit_quality.py +42 -0
  118. langchain_agentx/observability/evaluation/checkers/session_memory.py +45 -0
  119. langchain_agentx/observability/evaluation/checkers/tool_behavior.py +53 -0
  120. langchain_agentx/observability/evaluation/retention_scheduler.py +67 -0
  121. langchain_agentx/observability/evaluation/service.py +102 -0
  122. langchain_agentx/observability/evaluation/state.py +32 -0
  123. langchain_agentx/observability/evaluation/store.py +258 -0
  124. langchain_agentx/observability/events/__init__.py +15 -0
  125. langchain_agentx/observability/events/langchain_agentx_event_adapter.py +832 -0
  126. langchain_agentx/observability/logging/__init__.py +15 -0
  127. langchain_agentx/observability/logging/debug_burst.py +95 -0
  128. langchain_agentx/observability/logging/logging_config.py +178 -0
  129. langchain_agentx/observability/logging/logging_contract.py +65 -0
  130. langchain_agentx/observability/replay/__init__.py +35 -0
  131. langchain_agentx/observability/replay/cli.py +91 -0
  132. langchain_agentx/observability/replay/service.py +83 -0
  133. langchain_agentx/observability/replay/store.py +278 -0
  134. langchain_agentx/observability/replay/ui.py +47 -0
  135. langchain_agentx/observability/trace/__init__.py +25 -0
  136. langchain_agentx/observability/trace/collector.py +560 -0
  137. langchain_agentx/observability/trace/event_emitter.py +183 -0
  138. langchain_agentx/observability/trace/hook_event_emitter.py +49 -0
  139. langchain_agentx/observability/trace/models.py +144 -0
  140. langchain_agentx/observability/trace/sqlite_store.py +873 -0
  141. langchain_agentx/observability/trace/trace_callback.py +295 -0
  142. langchain_agentx/observability/trace/trace_lifecycle_collector.py +114 -0
  143. langchain_agentx/plugin/__init__.py +26 -0
  144. langchain_agentx/plugin/builtin.py +53 -0
  145. langchain_agentx/plugin/config.py +113 -0
  146. langchain_agentx/plugin/loader.py +386 -0
  147. langchain_agentx/plugin/manifest.py +154 -0
  148. langchain_agentx/plugin/registries.py +211 -0
  149. langchain_agentx/plugin/types.py +142 -0
  150. langchain_agentx/provider/__init__.py +27 -0
  151. langchain_agentx/provider/anthropic.py +121 -0
  152. langchain_agentx/provider/compatible_chat_openai.py +86 -0
  153. langchain_agentx/provider/env.py +45 -0
  154. langchain_agentx/provider/model_profile.py +156 -0
  155. langchain_agentx/provider/openai.py +89 -0
  156. langchain_agentx/session/__init__.py +17 -0
  157. langchain_agentx/session/agent_session.py +320 -0
  158. langchain_agentx/session/conversation_factory.py +87 -0
  159. langchain_agentx/session/conversation_recovery.py +156 -0
  160. langchain_agentx/session/conversation_session.py +198 -0
  161. langchain_agentx/session/factory.py +143 -0
  162. langchain_agentx/session/protocol.py +25 -0
  163. langchain_agentx/task_runtime/__init__.py +113 -0
  164. langchain_agentx/task_runtime/core/__init__.py +51 -0
  165. langchain_agentx/task_runtime/core/ids.py +33 -0
  166. langchain_agentx/task_runtime/core/interfaces.py +115 -0
  167. langchain_agentx/task_runtime/core/notification_priority.py +19 -0
  168. langchain_agentx/task_runtime/core/types.py +136 -0
  169. langchain_agentx/task_runtime/integrations/__init__.py +33 -0
  170. langchain_agentx/task_runtime/integrations/loop_adapter.py +91 -0
  171. langchain_agentx/task_runtime/integrations/loop_integration.py +61 -0
  172. langchain_agentx/task_runtime/integrations/prefetch_providers.py +108 -0
  173. langchain_agentx/task_runtime/integrations/provider_factory.py +103 -0
  174. langchain_agentx/task_runtime/integrations/queued_command_provider.py +184 -0
  175. langchain_agentx/task_runtime/integrations/sqlite_queued_command_provider.py +338 -0
  176. langchain_agentx/task_runtime/integrations/tool_use_summary_provider.py +254 -0
  177. langchain_agentx/task_runtime/orchestrator/__init__.py +5 -0
  178. langchain_agentx/task_runtime/orchestrator/runtime.py +386 -0
  179. langchain_agentx/task_runtime/output/__init__.py +5 -0
  180. langchain_agentx/task_runtime/output/sink.py +64 -0
  181. langchain_agentx/task_runtime/policy/__init__.py +11 -0
  182. langchain_agentx/task_runtime/policy/withhold_visibility.py +32 -0
  183. langchain_agentx/task_runtime/queue/__init__.py +5 -0
  184. langchain_agentx/task_runtime/queue/in_memory.py +55 -0
  185. langchain_agentx/task_runtime/skill_prefetch/__init__.py +4 -0
  186. langchain_agentx/task_runtime/skill_prefetch/attachments.py +46 -0
  187. langchain_agentx/task_runtime/skill_prefetch/models.py +37 -0
  188. langchain_agentx/task_runtime/skill_prefetch/provider.py +344 -0
  189. langchain_agentx/task_runtime/store/__init__.py +6 -0
  190. langchain_agentx/task_runtime/store/in_memory.py +81 -0
  191. langchain_agentx/task_runtime/store/sqlite_store.py +281 -0
  192. langchain_agentx/task_runtime/tasks/__init__.py +76 -0
  193. langchain_agentx/task_runtime/tasks/ai_analysis/__init__.py +15 -0
  194. langchain_agentx/task_runtime/tasks/ai_analysis/base.py +41 -0
  195. langchain_agentx/task_runtime/tasks/ai_analysis/evaluation.py +67 -0
  196. langchain_agentx/task_runtime/tasks/ai_analysis/registry.py +36 -0
  197. langchain_agentx/task_runtime/tasks/ai_analysis/scheduler.py +70 -0
  198. langchain_agentx/task_runtime/tasks/base/__init__.py +6 -0
  199. langchain_agentx/task_runtime/tasks/base/contracts.py +24 -0
  200. langchain_agentx/task_runtime/tasks/custom/__init__.py +7 -0
  201. langchain_agentx/task_runtime/tasks/custom/executor.py +60 -0
  202. langchain_agentx/task_runtime/tasks/custom/notification.py +7 -0
  203. langchain_agentx/task_runtime/tasks/custom/semantics.py +13 -0
  204. langchain_agentx/task_runtime/tasks/custom/spec.py +33 -0
  205. langchain_agentx/task_runtime/tasks/dream_task/__init__.py +15 -0
  206. langchain_agentx/task_runtime/tasks/dream_task/executor.py +61 -0
  207. langchain_agentx/task_runtime/tasks/dream_task/notification.py +19 -0
  208. langchain_agentx/task_runtime/tasks/dream_task/semantics.py +13 -0
  209. langchain_agentx/task_runtime/tasks/dream_task/spec.py +35 -0
  210. langchain_agentx/task_runtime/tasks/dream_task/state.py +17 -0
  211. langchain_agentx/task_runtime/tasks/in_process_teammate/__init__.py +12 -0
  212. langchain_agentx/task_runtime/tasks/in_process_teammate/executor.py +36 -0
  213. langchain_agentx/task_runtime/tasks/in_process_teammate/notification.py +25 -0
  214. langchain_agentx/task_runtime/tasks/in_process_teammate/semantics.py +13 -0
  215. langchain_agentx/task_runtime/tasks/in_process_teammate/spec.py +63 -0
  216. langchain_agentx/task_runtime/tasks/local_agent/__init__.py +14 -0
  217. langchain_agentx/task_runtime/tasks/local_agent/executor.py +33 -0
  218. langchain_agentx/task_runtime/tasks/local_agent/notification.py +21 -0
  219. langchain_agentx/task_runtime/tasks/local_agent/runner.py +43 -0
  220. langchain_agentx/task_runtime/tasks/local_agent/semantics.py +13 -0
  221. langchain_agentx/task_runtime/tasks/local_agent/spec.py +31 -0
  222. langchain_agentx/task_runtime/tasks/local_bash/__init__.py +13 -0
  223. langchain_agentx/task_runtime/tasks/local_bash/executor.py +95 -0
  224. langchain_agentx/task_runtime/tasks/local_bash/notification.py +22 -0
  225. langchain_agentx/task_runtime/tasks/local_bash/semantics.py +13 -0
  226. langchain_agentx/task_runtime/tasks/local_bash/spec.py +55 -0
  227. langchain_agentx/task_runtime/tasks/remote_agent/__init__.py +19 -0
  228. langchain_agentx/task_runtime/tasks/remote_agent/backend.py +76 -0
  229. langchain_agentx/task_runtime/tasks/remote_agent/executor.py +37 -0
  230. langchain_agentx/task_runtime/tasks/remote_agent/notification.py +22 -0
  231. langchain_agentx/task_runtime/tasks/remote_agent/semantics.py +13 -0
  232. langchain_agentx/task_runtime/tasks/remote_agent/spec.py +34 -0
  233. langchain_agentx/task_runtime/tasks/trace_cleanup/__init__.py +19 -0
  234. langchain_agentx/task_runtime/tasks/trace_cleanup/bootstrap.py +95 -0
  235. langchain_agentx/task_runtime/tasks/trace_cleanup/executor.py +66 -0
  236. langchain_agentx/task_runtime/tasks/trace_cleanup/scheduler.py +169 -0
  237. langchain_agentx/tool_runtime/__init__.py +90 -0
  238. langchain_agentx/tool_runtime/adapter.py +365 -0
  239. langchain_agentx/tool_runtime/base.py +319 -0
  240. langchain_agentx/tool_runtime/errors.py +190 -0
  241. langchain_agentx/tool_runtime/identical_call_cache.py +110 -0
  242. langchain_agentx/tool_runtime/loader.py +195 -0
  243. langchain_agentx/tool_runtime/models.py +260 -0
  244. langchain_agentx/tool_runtime/permission_context.py +78 -0
  245. langchain_agentx/tool_runtime/pipeline.py +621 -0
  246. langchain_agentx/tool_runtime/policy.py +447 -0
  247. langchain_agentx/tool_runtime/registry.py +81 -0
  248. langchain_agentx/tool_runtime/resolvers/__init__.py +27 -0
  249. langchain_agentx/tool_runtime/resolvers/agent_session.py +125 -0
  250. langchain_agentx/tool_runtime/resolvers/background.py +32 -0
  251. langchain_agentx/tool_runtime/resolvers/base.py +20 -0
  252. langchain_agentx/tool_runtime/resolvers/conversation.py +22 -0
  253. langchain_agentx/tool_runtime/resolvers/workflow.py +73 -0
  254. langchain_agentx/tool_runtime/session_store.py +132 -0
  255. langchain_agentx/tool_runtime/smoke_test_runtime.py +294 -0
  256. langchain_agentx/tool_runtime/state_bridge.py +164 -0
  257. langchain_agentx/tools/__init__.py +26 -0
  258. langchain_agentx/tools/agent/__init__.py +9 -0
  259. langchain_agentx/tools/agent/backend.py +53 -0
  260. langchain_agentx/tools/agent/built_in/__init__.py +19 -0
  261. langchain_agentx/tools/agent/built_in/agentx_guide.py +65 -0
  262. langchain_agentx/tools/agent/built_in/explore.py +80 -0
  263. langchain_agentx/tools/agent/built_in/general.py +57 -0
  264. langchain_agentx/tools/agent/built_in/plan.py +89 -0
  265. langchain_agentx/tools/agent/built_in/statusline_setup.py +64 -0
  266. langchain_agentx/tools/agent/built_in/verification.py +120 -0
  267. langchain_agentx/tools/agent/builtin_subagent_loader.py +89 -0
  268. langchain_agentx/tools/agent/cwd_resolution.py +119 -0
  269. langchain_agentx/tools/agent/limits.py +26 -0
  270. langchain_agentx/tools/agent/loader.py +270 -0
  271. langchain_agentx/tools/agent/models.py +85 -0
  272. langchain_agentx/tools/agent/prompt.py +120 -0
  273. langchain_agentx/tools/agent/registry/__init__.py +18 -0
  274. langchain_agentx/tools/agent/registry/config.py +29 -0
  275. langchain_agentx/tools/agent/registry/registry.py +47 -0
  276. langchain_agentx/tools/agent/scope.py +137 -0
  277. langchain_agentx/tools/agent/tool.py +256 -0
  278. langchain_agentx/tools/bash/__init__.py +9 -0
  279. langchain_agentx/tools/bash/ast_security.py +571 -0
  280. langchain_agentx/tools/bash/backend.py +1447 -0
  281. langchain_agentx/tools/bash/bash_hardening.py +734 -0
  282. langchain_agentx/tools/bash/bash_runtime_contract.py +41 -0
  283. langchain_agentx/tools/bash/cwd_reporter.py +95 -0
  284. langchain_agentx/tools/bash/limits.py +71 -0
  285. langchain_agentx/tools/bash/mode_validation.py +282 -0
  286. langchain_agentx/tools/bash/models.py +131 -0
  287. langchain_agentx/tools/bash/observability.py +148 -0
  288. langchain_agentx/tools/bash/output_utils.py +200 -0
  289. langchain_agentx/tools/bash/path_security.py +2429 -0
  290. langchain_agentx/tools/bash/prompt.py +68 -0
  291. langchain_agentx/tools/bash/read_only_validation.py +589 -0
  292. langchain_agentx/tools/bash/result_presenter.py +324 -0
  293. langchain_agentx/tools/bash/sandbox_decision.py +133 -0
  294. langchain_agentx/tools/bash/security.py +311 -0
  295. langchain_agentx/tools/bash/sed_edit_parser.py +243 -0
  296. langchain_agentx/tools/bash/sed_validation.py +163 -0
  297. langchain_agentx/tools/bash/semantics.py +111 -0
  298. langchain_agentx/tools/bash/session_manager.py +205 -0
  299. langchain_agentx/tools/bash/session_runtime.py +290 -0
  300. langchain_agentx/tools/bash/shell_locator.py +191 -0
  301. langchain_agentx/tools/bash/task_runtime.py +91 -0
  302. langchain_agentx/tools/bash/tool.py +939 -0
  303. langchain_agentx/tools/bash/windows_shell_quoting.py +45 -0
  304. langchain_agentx/tools/glob/__init__.py +9 -0
  305. langchain_agentx/tools/glob/models.py +57 -0
  306. langchain_agentx/tools/glob/pagination.py +30 -0
  307. langchain_agentx/tools/glob/prompt.py +24 -0
  308. langchain_agentx/tools/glob/rg_list_backend.py +139 -0
  309. langchain_agentx/tools/glob/rg_pattern.py +44 -0
  310. langchain_agentx/tools/glob/tool.py +327 -0
  311. langchain_agentx/tools/grep/__init__.py +7 -0
  312. langchain_agentx/tools/grep/backend.py +375 -0
  313. langchain_agentx/tools/grep/models.py +127 -0
  314. langchain_agentx/tools/grep/prompt.py +30 -0
  315. langchain_agentx/tools/grep/rg_subprocess_controller.py +114 -0
  316. langchain_agentx/tools/grep/tool.py +475 -0
  317. langchain_agentx/tools/read/__init__.py +9 -0
  318. langchain_agentx/tools/read/backend.py +415 -0
  319. langchain_agentx/tools/read/limits.py +67 -0
  320. langchain_agentx/tools/read/models.py +156 -0
  321. langchain_agentx/tools/read/prompt.py +73 -0
  322. langchain_agentx/tools/read/tool.py +494 -0
  323. langchain_agentx/tools/ripgrep_plugin_exclusions.py +137 -0
  324. langchain_agentx/tools/skill/__init__.py +4 -0
  325. langchain_agentx/tools/skill/argument_substitution.py +80 -0
  326. langchain_agentx/tools/skill/loader.py +196 -0
  327. langchain_agentx/tools/skill/models.py +88 -0
  328. langchain_agentx/tools/skill/policy.py +80 -0
  329. langchain_agentx/tools/skill/prompt.py +35 -0
  330. langchain_agentx/tools/skill/tool.py +222 -0
  331. langchain_agentx/utils/__init__.py +0 -0
  332. langchain_agentx/utils/cwd.py +124 -0
  333. langchain_agentx/utils/host_platform.py +112 -0
  334. langchain_agentx/utils/path_hierarchy.py +48 -0
  335. langchain_agentx/utils/path_user_input.py +66 -0
  336. langchain_agentx/utils/rg_executable.py +18 -0
  337. langchain_agentx/utils/subprocess_text.py +101 -0
  338. langchain_agentx/utils/temp_paths.py +77 -0
  339. langchain_agentx/utils/unc_path.py +25 -0
  340. langchain_agentx/utils/win_reserved_paths.py +51 -0
  341. langchain_agentx/workflow/__init__.py +7 -0
  342. langchain_agentx/workflow/base.py +97 -0
  343. langchain_agentx/workflow/batch.py +55 -0
  344. langchain_agentx/workflow/dag.py +54 -0
  345. langchain_agentx/workspace/__init__.py +13 -0
  346. langchain_agentx/workspace/config.py +140 -0
  347. langchain_agentx/workspace/path_key_normalizer.py +30 -0
  348. langchain_agentx/workspace/resolver.py +74 -0
  349. langchain_agentx/workspace/validators.py +41 -0
  350. langchain_agentx_python-0.1.dist-info/LICENSE +201 -0
  351. langchain_agentx_python-0.1.dist-info/METADATA +513 -0
  352. langchain_agentx_python-0.1.dist-info/RECORD +354 -0
  353. langchain_agentx_python-0.1.dist-info/WHEEL +5 -0
  354. 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"