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