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,734 @@
1
+ """
2
+ tools/bash/bash_hardening.py — BashRuntimeTool 模块3:安全攻防加固
3
+
4
+ 职责:
5
+ 在 AST 结构审批、路径授权、PolicyEngine 之外,补上 command-string 层面的
6
+ 防绕过检查,避免 env / wrapper / quoting / git-internal 等模式绕过主权限链。
7
+
8
+ 协作者(对照 CC 更大攻防规则库方向扩展):
9
+ 1. BashCommandCanonicalizer — env/wrapper 固定点归一化
10
+ 2. BashEnvSecurityChecker — 高危环境变量(含 JVM/SSH/TLS 劫持面)
11
+ 3. BashUnicodeRiskChecker — 零宽 / RTL 等不可见字符混淆
12
+ 4. BashExecutionContextChecker — sudo/chroot/nsenter 等执行上下文切换
13
+ 5. BashCrossPlatformBinaryChecker — wsl/cmd/powershell 等跨边界执行
14
+ 6. BashRemoteShellPipelineChecker — curl|bash、base64|sh 等「下载/解码→解释器」链
15
+ 7. BashShellRiskChecker — quoting / heredoc / UNC / WebDAV 等
16
+ 8. BashGitCliInjectionChecker — git -c hooksPath / sshCommand 等 CLI 注入
17
+ 9. BashGitSecurityChecker — .git 路径、bare、compound cd 等
18
+
19
+ 观测:
20
+ BashSecurityHardener.audit_explain() 返回结构化快照,便于与 CC 式调试输出对齐。
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import os
26
+ import re
27
+ import shlex
28
+ import sys
29
+ from dataclasses import dataclass
30
+ from typing import Any
31
+
32
+ from langchain_agentx.tool_runtime.models import AuthorizationDecision
33
+ from langchain_agentx.utils.host_platform import get_host_platform
34
+
35
+ from .ast_security import BashAstAnalysis, BashWrapperStripper
36
+ from .path_security import BashPathExtractorRegistry
37
+
38
+
39
+ @dataclass(frozen=True)
40
+ class BashHardeningCheckResult:
41
+ behavior: str
42
+ message: str | None = None
43
+ policy_id: str | None = None
44
+ ask_prompt: str | None = None
45
+ rule_code: str | None = None
46
+ category: str | None = None
47
+ extra: dict[str, Any] | None = None
48
+
49
+ def to_decision(self) -> AuthorizationDecision:
50
+ meta: dict[str, Any] | None = None
51
+ if self.rule_code or self.category or self.extra:
52
+ bh: dict[str, Any] = {}
53
+ if self.rule_code:
54
+ bh["rule_code"] = self.rule_code
55
+ if self.category:
56
+ bh["category"] = self.category
57
+ if self.extra:
58
+ bh.update(self.extra)
59
+ meta = {"bash_hardening": bh}
60
+ return AuthorizationDecision(
61
+ behavior=self.behavior,
62
+ message=self.message,
63
+ policy_id=self.policy_id,
64
+ ask_prompt=self.ask_prompt,
65
+ metadata=meta,
66
+ )
67
+
68
+
69
+ class BashCommandCanonicalizer:
70
+ """对命令做 env/wrapper 固定点归一化,供策略匹配使用。"""
71
+
72
+ _ENV_ASSIGNMENT_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*=.*$")
73
+
74
+ def __init__(self, wrapper_stripper: BashWrapperStripper | None = None) -> None:
75
+ self._wrapper_stripper = wrapper_stripper or BashWrapperStripper()
76
+
77
+ def canonicalize(self, command: str) -> str:
78
+ tokens = self._split(command)
79
+ if not tokens:
80
+ return command.strip()
81
+
82
+ previous: list[str] | None = None
83
+ current = list(tokens)
84
+ while current != previous and current:
85
+ previous = list(current)
86
+ current = self._strip_assignments(current)
87
+ current = self._wrapper_stripper.strip(current)
88
+
89
+ return " ".join(current) if current else command.strip()
90
+
91
+ @staticmethod
92
+ def _split(command: str) -> list[str]:
93
+ try:
94
+ return shlex.split(command)
95
+ except ValueError:
96
+ return command.split()
97
+
98
+ def _strip_assignments(self, tokens: list[str]) -> list[str]:
99
+ index = 0
100
+ while index < len(tokens) and self._ENV_ASSIGNMENT_RE.match(tokens[index]):
101
+ index += 1
102
+ return tokens[index:]
103
+
104
+
105
+ class BashEnvSecurityChecker:
106
+ """检测前导环境变量中的高风险执行劫持面。"""
107
+
108
+ SAFE_ENV_VARS = frozenset({
109
+ "TERM", "COLORTERM", "NO_COLOR", "FORCE_COLOR", "CLICOLOR",
110
+ "LANG", "LANGUAGE", "LC_ALL", "LC_CTYPE", "LC_MESSAGES", "TZ",
111
+ "COLUMNS", "LINES", "CI",
112
+ })
113
+ BINARY_HIJACK_VARS = frozenset({
114
+ "PATH", "LD_PRELOAD", "LD_LIBRARY_PATH", "DYLD_INSERT_LIBRARIES",
115
+ "DYLD_LIBRARY_PATH", "PYTHONPATH", "PYTHONHOME", "PYTHONSTARTUP",
116
+ "RUBYLIB", "RUBYOPT", "PERL5LIB", "PERL5OPT", "NODE_PATH",
117
+ "NODE_OPTIONS", "GIT_EXEC_PATH", "GIT_CONFIG", "GIT_CONFIG_GLOBAL",
118
+ "GIT_CONFIG_SYSTEM", "BASH_ENV", "ENV", "SHELLOPTS", "IFS",
119
+ "CDPATH", "PROMPT_COMMAND",
120
+ "JAVA_TOOL_OPTIONS", "_JAVA_OPTIONS", "JDK_JAVA_OPTIONS",
121
+ "GIT_SSH_COMMAND", "GIT_SSH",
122
+ "LD_AUDIT", "OPENSSL_CONF", "SSLKEYLOGFILE",
123
+ })
124
+ _ENV_ASSIGNMENT_RE = re.compile(r"^([A-Za-z_][A-Za-z0-9_]*)=(.*)$")
125
+
126
+ def collect_modified_vars(self, command: str) -> set[str]:
127
+ tokens = self._split(command)
128
+ if not tokens:
129
+ return set()
130
+
131
+ result: set[str] = set()
132
+ index = 0
133
+ while index < len(tokens):
134
+ match = self._ENV_ASSIGNMENT_RE.match(tokens[index])
135
+ if match:
136
+ result.add(match.group(1))
137
+ index += 1
138
+ continue
139
+ break
140
+
141
+ if index < len(tokens) and tokens[index] == "env":
142
+ index += 1
143
+ while index < len(tokens):
144
+ token = tokens[index]
145
+ if token == "--":
146
+ index += 1
147
+ break
148
+ if token == "-u" and index + 1 < len(tokens):
149
+ result.add(tokens[index + 1])
150
+ index += 2
151
+ continue
152
+ if token in {"-i", "-0", "-v"}:
153
+ index += 1
154
+ continue
155
+ match = self._ENV_ASSIGNMENT_RE.match(token)
156
+ if match:
157
+ result.add(match.group(1))
158
+ index += 1
159
+ continue
160
+ break
161
+ return result
162
+
163
+ def evaluate(self, command: str) -> BashHardeningCheckResult | None:
164
+ modified_vars = self.collect_modified_vars(command)
165
+ dangerous = sorted(var for var in modified_vars if var in self.BINARY_HIJACK_VARS)
166
+ if not dangerous:
167
+ return None
168
+ joined = ", ".join(dangerous)
169
+ return BashHardeningCheckResult(
170
+ behavior="ask",
171
+ message=(
172
+ "This command modifies high-risk environment variables that can change "
173
+ f"which binary or config is used: {joined}."
174
+ ),
175
+ policy_id="bash_env_hardening",
176
+ ask_prompt=(
177
+ "High-risk environment variable override detected.\n"
178
+ f"Variables: {joined}\n"
179
+ "These variables can alter binary resolution or runtime configuration, "
180
+ "so explicit approval is required."
181
+ ),
182
+ rule_code="env.binary_hijack",
183
+ category="env",
184
+ extra={"variables": dangerous},
185
+ )
186
+
187
+ @staticmethod
188
+ def _split(command: str) -> list[str]:
189
+ try:
190
+ return shlex.split(command)
191
+ except ValueError:
192
+ return command.split()
193
+
194
+
195
+ class BashUnicodeRiskChecker:
196
+ """零宽字符 / RTL 控制符等不可见混淆(跨平台 Unicode 特例)。"""
197
+
198
+ _INVISIBLE_OR_RTL = re.compile(r"[\u200b\u200c\u200d\ufeff\u202a-\u202e]")
199
+
200
+ def evaluate(self, command: str) -> BashHardeningCheckResult | None:
201
+ if not self._INVISIBLE_OR_RTL.search(command):
202
+ return None
203
+ return BashHardeningCheckResult(
204
+ behavior="ask",
205
+ message="Command contains invisible Unicode characters that can obfuscate intent.",
206
+ policy_id="bash_unicode_obfuscation",
207
+ ask_prompt=(
208
+ "Invisible Unicode characters (zero-width / bidi controls) detected.\n"
209
+ "These can hide flags or path segments; explicit approval is required."
210
+ ),
211
+ rule_code="shell.unicode_invisible",
212
+ category="shell",
213
+ )
214
+
215
+
216
+ class BashExecutionContextChecker:
217
+ """sudo / chroot / namespace 等改变真实执行身份或根目录。"""
218
+
219
+ _CTX = re.compile(
220
+ r"(?:^|[;&|]|\s|&&|\|\|)"
221
+ r"(?:sudo|doas|su\b|runuser|chroot|nsenter|strace|ltrace|"
222
+ r"pkexec|firejail|bwrap|bubblewrap|unshare|systemd-run|machinectl)\s",
223
+ re.IGNORECASE,
224
+ )
225
+
226
+ def evaluate(self, command: str) -> BashHardeningCheckResult | None:
227
+ if not self._CTX.search(command):
228
+ return None
229
+ return BashHardeningCheckResult(
230
+ behavior="ask",
231
+ message="Command uses a privilege or namespace wrapper that can change execution semantics.",
232
+ policy_id="bash_execution_context",
233
+ ask_prompt=(
234
+ "Privilege / chroot / namespace / tracer wrapper detected.\n"
235
+ "These can bypass normal permission and sandbox assumptions; approve explicitly."
236
+ ),
237
+ rule_code="exec.context.privilege_or_namespace",
238
+ category="execution_context",
239
+ )
240
+
241
+
242
+ class BashCrossPlatformBinaryChecker:
243
+ """跨 OS 边界执行(Windows / WSL / PowerShell),与 POSIX 沙盒假设不一致。"""
244
+
245
+ _CROSS = re.compile(
246
+ r"(?:^|\s)(?:wsl(?:\.exe)?|cmd(?:\.exe)?|powershell(?:\.exe)?|pwsh(?:\.exe)?)\s",
247
+ re.IGNORECASE,
248
+ )
249
+
250
+ def evaluate(self, command: str) -> BashHardeningCheckResult | None:
251
+ if not self._CROSS.search(command):
252
+ return None
253
+ return BashHardeningCheckResult(
254
+ behavior="ask",
255
+ message="Command invokes a cross-platform shell binary (WSL/cmd/PowerShell), which requires review.",
256
+ policy_id="bash_cross_platform_shell",
257
+ ask_prompt=(
258
+ "Cross-platform shell invocation detected (wsl/cmd/powershell/pwsh).\n"
259
+ "Paths and permission models differ from POSIX bash; explicit approval is required."
260
+ ),
261
+ rule_code="platform.cross_shell",
262
+ category="platform",
263
+ extra={
264
+ "sys_platform_raw": sys.platform,
265
+ "host_platform": get_host_platform().value,
266
+ },
267
+ )
268
+
269
+
270
+ class BashRemoteShellPipelineChecker:
271
+ """远程拉取 / 解码后直接进入解释器的高风险链。"""
272
+
273
+ _FETCH_PIPE_SHELL = re.compile(
274
+ r"(?:curl|wget|fetch)\b[^\n|]*\|\s*(?:bash|sh|zsh|dash)\b",
275
+ re.IGNORECASE,
276
+ )
277
+ _B64_PIPE_SHELL = re.compile(
278
+ r"base64\b[^\n|]*(?:-d|--decode)[^\n|]*\|\s*(?:bash|sh)\b",
279
+ re.IGNORECASE,
280
+ )
281
+
282
+ def evaluate(self, command: str) -> BashHardeningCheckResult | None:
283
+ if self._FETCH_PIPE_SHELL.search(command):
284
+ return BashHardeningCheckResult(
285
+ behavior="ask",
286
+ message="Pipeline fetches remote content directly into a shell interpreter.",
287
+ policy_id="bash_remote_pipe_shell",
288
+ ask_prompt=(
289
+ "curl/wget/fetch piped into bash/sh detected.\n"
290
+ "This is a common remote-code execution pattern; explicit approval is required."
291
+ ),
292
+ rule_code="network.fetch_pipe_interpreter",
293
+ category="network",
294
+ )
295
+ if self._B64_PIPE_SHELL.search(command):
296
+ return BashHardeningCheckResult(
297
+ behavior="ask",
298
+ message="Pipeline decodes base64 directly into a shell interpreter.",
299
+ policy_id="bash_remote_pipe_shell",
300
+ ask_prompt=(
301
+ "base64 decode piped into bash/sh detected.\n"
302
+ "This can hide payload from static review; explicit approval is required."
303
+ ),
304
+ rule_code="network.base64_pipe_interpreter",
305
+ category="network",
306
+ )
307
+ return None
308
+
309
+
310
+ class BashGitCliInjectionChecker:
311
+ """git -c 覆盖 hooks / SSH 等敏感运行时配置。"""
312
+
313
+ _DANGEROUS_GIT_C = re.compile(
314
+ r"\bgit\s+(?:-\S+\s+)*-c\s+\S*(hooksPath|sshCommand|uploadpack|receivepack)\S*=",
315
+ re.IGNORECASE,
316
+ )
317
+
318
+ def evaluate(self, command: str) -> BashHardeningCheckResult | None:
319
+ if not re.search(r"\bgit\b", command):
320
+ return None
321
+ if not self._DANGEROUS_GIT_C.search(command):
322
+ return None
323
+ return BashHardeningCheckResult(
324
+ behavior="ask",
325
+ message="Git command overrides sensitive runtime settings via `-c` (hooks/ssh/pack).",
326
+ policy_id="bash_git_cli_injection",
327
+ ask_prompt=(
328
+ "git -c override for hooksPath/sshCommand/uploadpack/receivepack detected.\n"
329
+ "These can redirect hooks or transport; explicit approval is required."
330
+ ),
331
+ rule_code="git.cli_runtime_override",
332
+ category="git",
333
+ )
334
+
335
+
336
+ class BashShellRiskChecker:
337
+ """检测 quoting / heredoc / UNC / WebDAV / eval / process substitution 等模式。"""
338
+
339
+ _ANSI_C_QUOTING_RE = re.compile(r"\$'[^']*'")
340
+ _LOCALE_QUOTING_RE = re.compile(r'\$"[^"]*"')
341
+ _EMPTY_QUOTES_BEFORE_DASH_RE = re.compile(r"(?:^|\s)(?:''|\"\")+\s*-")
342
+ _ADJACENT_QUOTED_DASH_RE = re.compile(r'(?:""|\'\')+[\'"]-')
343
+ _CONSECUTIVE_QUOTES_RE = re.compile(r"(?:^|\s)['\"]{3,}")
344
+ _HEREDOC_IN_SUBSTITUTION_RE = re.compile(
345
+ r"(\$\([^)]*<<-?\s*['\"]?[A-Za-z0-9_]+|`[^`]*<<-?\s*['\"]?[A-Za-z0-9_]+)"
346
+ )
347
+ _UNC_BACKSLASH_RE = re.compile(r"\\\\[^\\/\s]+\\[^\\/\s]+")
348
+ _UNC_DEVICE_RE = re.compile(r"//\?/UNC/", re.IGNORECASE)
349
+ _WEBDAV_RE = re.compile(r"\b(?:dav|davs|webdav|smb)://", re.IGNORECASE)
350
+ _EVAL_OR_SOURCE_RE = re.compile(
351
+ r"(?:^|\s)(?:eval|source)\s",
352
+ )
353
+ _PROCESS_SUBST_RE = re.compile(r"[<>]\(")
354
+
355
+ def pattern_rules(
356
+ self,
357
+ ) -> list[tuple[str, re.Pattern[str], str, str]]:
358
+ return [
359
+ (
360
+ "ansi_c_quote",
361
+ self._ANSI_C_QUOTING_RE,
362
+ "Command contains ANSI-C quoting which can hide characters.",
363
+ "bash_shell_obfuscation",
364
+ ),
365
+ (
366
+ "locale_quote",
367
+ self._LOCALE_QUOTING_RE,
368
+ "Command contains locale quoting which can hide characters.",
369
+ "bash_shell_obfuscation",
370
+ ),
371
+ (
372
+ "empty_quotes_dash",
373
+ self._EMPTY_QUOTES_BEFORE_DASH_RE,
374
+ "Command contains empty quotes before a dash, which can obfuscate flags.",
375
+ "bash_shell_obfuscation",
376
+ ),
377
+ (
378
+ "adjacent_quotes_dash",
379
+ self._ADJACENT_QUOTED_DASH_RE,
380
+ "Command contains empty quote pairs adjacent to a quoted dash.",
381
+ "bash_shell_obfuscation",
382
+ ),
383
+ (
384
+ "consecutive_quotes",
385
+ self._CONSECUTIVE_QUOTES_RE,
386
+ "Command contains suspicious consecutive quote characters at word start.",
387
+ "bash_shell_obfuscation",
388
+ ),
389
+ (
390
+ "heredoc_in_subst",
391
+ self._HEREDOC_IN_SUBSTITUTION_RE,
392
+ "Command contains heredoc content inside command substitution, which requires review.",
393
+ "bash_heredoc_substitution",
394
+ ),
395
+ (
396
+ "unc_backslash",
397
+ self._UNC_BACKSLASH_RE,
398
+ "Command references a UNC path, which may trigger remote filesystem access.",
399
+ "bash_remote_path_risk",
400
+ ),
401
+ (
402
+ "unc_device",
403
+ self._UNC_DEVICE_RE,
404
+ "Command references a Windows UNC device path, which requires approval.",
405
+ "bash_remote_path_risk",
406
+ ),
407
+ (
408
+ "webdav",
409
+ self._WEBDAV_RE,
410
+ "Command references a WebDAV/SMB-style remote path, which requires approval.",
411
+ "bash_remote_path_risk",
412
+ ),
413
+ (
414
+ "eval_source",
415
+ self._EVAL_OR_SOURCE_RE,
416
+ "Command uses eval/source/. which can execute arbitrary strings.",
417
+ "bash_dynamic_eval",
418
+ ),
419
+ (
420
+ "process_substitution",
421
+ self._PROCESS_SUBST_RE,
422
+ "Command uses process substitution (<(...) / >(...)), which can hide side effects.",
423
+ "bash_process_substitution",
424
+ ),
425
+ ]
426
+
427
+ def evaluate(self, command: str) -> BashHardeningCheckResult | None:
428
+ for rule_id, pattern, message, policy_id in self.pattern_rules():
429
+ if pattern.search(command):
430
+ return BashHardeningCheckResult(
431
+ behavior="ask",
432
+ message=message,
433
+ policy_id=policy_id,
434
+ ask_prompt=message,
435
+ rule_code=f"shell.{rule_id}",
436
+ category="shell",
437
+ )
438
+ return None
439
+
440
+ def probe(self, command: str) -> list[dict[str, Any]]:
441
+ out: list[dict[str, Any]] = []
442
+ for rule_id, pattern, message, policy_id in self.pattern_rules():
443
+ out.append(
444
+ {
445
+ "rule_id": rule_id,
446
+ "policy_id": policy_id,
447
+ "matched": bool(pattern.search(command)),
448
+ "message": message,
449
+ }
450
+ )
451
+ return out
452
+
453
+
454
+ class BashGitSecurityChecker:
455
+ """检测 git-internal 路径、裸仓库和仓库边界混淆。"""
456
+
457
+ def __init__(self, extractor_registry: BashPathExtractorRegistry | None = None) -> None:
458
+ self._extractor_registry = extractor_registry or BashPathExtractorRegistry()
459
+
460
+ def evaluate_command(
461
+ self,
462
+ *,
463
+ command: str,
464
+ analysis: BashAstAnalysis,
465
+ cwd: str,
466
+ ) -> BashHardeningCheckResult | None:
467
+ normalized = set(analysis.normalized_base_commands)
468
+ if "git" in normalized:
469
+ risky_subcommand = self._git_risky_subcommand(command)
470
+ if risky_subcommand is not None:
471
+ return BashHardeningCheckResult(
472
+ behavior="ask",
473
+ message=(
474
+ "Git command may mutate repository or remote state "
475
+ f"(`git {risky_subcommand}`), explicit approval is required."
476
+ ),
477
+ policy_id="bash_git_high_risk_subcommand",
478
+ ask_prompt=(
479
+ f"High-risk git subcommand detected: git {risky_subcommand}\n"
480
+ "This operation can modify refs/history/remotes, approve explicitly."
481
+ ),
482
+ rule_code="git.high_risk_subcommand",
483
+ category="git",
484
+ extra={"subcommand": risky_subcommand},
485
+ )
486
+ if self._is_git_internal_dir(cwd):
487
+ return BashHardeningCheckResult(
488
+ behavior="ask",
489
+ message="Running git from inside a .git internal directory requires approval.",
490
+ policy_id="bash_git_workspace_boundary",
491
+ ask_prompt=(
492
+ "Git command is running from inside a .git internal directory.\n"
493
+ "This can change repository internals in non-obvious ways, so approval is required."
494
+ ),
495
+ rule_code="git.cwd_inside_dot_git",
496
+ category="git",
497
+ )
498
+ if self._looks_like_bare_repo(cwd):
499
+ return BashHardeningCheckResult(
500
+ behavior="ask",
501
+ message="Running git inside a bare repository requires approval.",
502
+ policy_id="bash_git_workspace_boundary",
503
+ ask_prompt=(
504
+ "Git command appears to target a bare repository.\n"
505
+ "Bare repos expose internal refs/config directly, so approval is required."
506
+ ),
507
+ rule_code="git.bare_repo_cwd",
508
+ category="git",
509
+ )
510
+
511
+ if "git" in normalized and self._has_cd_into_git_internal(analysis, cwd):
512
+ return BashHardeningCheckResult(
513
+ behavior="ask",
514
+ message="Compound command changes into a git-internal directory before running git.",
515
+ policy_id="bash_git_workspace_boundary",
516
+ ask_prompt=(
517
+ "This command changes into a git-internal directory and then runs git.\n"
518
+ "Explicit approval is required to avoid repository-boundary confusion."
519
+ ),
520
+ rule_code="git.cd_into_internal_then_git",
521
+ category="git",
522
+ )
523
+ return None
524
+
525
+ def _git_risky_subcommand(self, command: str) -> str | None:
526
+ tokens = self._split(command)
527
+ if not tokens:
528
+ return None
529
+ if os.path.basename(tokens[0]) != "git":
530
+ return None
531
+ subcommand = ""
532
+ for token in tokens[1:]:
533
+ if token.startswith("-"):
534
+ continue
535
+ subcommand = token
536
+ break
537
+ if not subcommand:
538
+ return None
539
+ risky = {
540
+ "push", "fetch", "pull", "clone", "remote", "submodule", "config",
541
+ "update-ref", "rebase", "reset", "checkout", "switch", "worktree",
542
+ "commit", "cherry-pick", "revert", "merge", "apply", "am",
543
+ "notes", "tag", "branch", "stash",
544
+ }
545
+ if subcommand in risky:
546
+ if subcommand == "remote" and any(t in {"-v", "show"} for t in tokens[2:]):
547
+ return None
548
+ return subcommand
549
+ return None
550
+
551
+ def evaluate_segment(
552
+ self,
553
+ *,
554
+ command: str,
555
+ analysis: BashAstAnalysis,
556
+ cwd: str,
557
+ ) -> BashHardeningCheckResult | None:
558
+ path_info = self._extractor_registry.extract(command, analysis)
559
+ if path_info is None:
560
+ return None
561
+
562
+ for target in path_info.path_targets:
563
+ resolved = self._resolve_path(target.raw_path, cwd)
564
+ if self._is_git_internal_path(resolved):
565
+ if target.operation_type in {"write", "create"}:
566
+ return BashHardeningCheckResult(
567
+ behavior="deny",
568
+ message="Direct writes to git-internal paths are not allowed.",
569
+ policy_id="bash_git_internal_path",
570
+ rule_code="git.internal_path_write",
571
+ category="git",
572
+ extra={"path": resolved},
573
+ )
574
+ return BashHardeningCheckResult(
575
+ behavior="ask",
576
+ message="Direct access to git-internal paths requires approval.",
577
+ policy_id="bash_git_internal_path",
578
+ ask_prompt=(
579
+ f"Direct access to git-internal path detected: {resolved}\n"
580
+ "This requires approval because repository internals may contain hooks or config."
581
+ ),
582
+ rule_code="git.internal_path_access",
583
+ category="git",
584
+ extra={"path": resolved},
585
+ )
586
+ return None
587
+
588
+ def _has_cd_into_git_internal(self, analysis: BashAstAnalysis, cwd: str) -> bool:
589
+ if "git" not in analysis.normalized_base_commands:
590
+ return False
591
+ for simple in analysis.simple_commands:
592
+ if simple.normalized_base_command != "cd":
593
+ continue
594
+ tokens = self._split(simple.text)
595
+ if len(tokens) < 2:
596
+ continue
597
+ resolved = self._resolve_path(tokens[1], cwd)
598
+ if self._is_git_internal_dir(resolved) or self._looks_like_bare_repo(resolved):
599
+ return True
600
+ return False
601
+
602
+ @staticmethod
603
+ def _split(command: str) -> list[str]:
604
+ try:
605
+ return shlex.split(command)
606
+ except ValueError:
607
+ return command.split()
608
+
609
+ @staticmethod
610
+ def _resolve_path(raw_path: str, cwd: str) -> str:
611
+ expanded = os.path.expanduser(raw_path.strip("\"'"))
612
+ absolute = expanded if os.path.isabs(expanded) else os.path.join(cwd, expanded)
613
+ return os.path.realpath(absolute)
614
+
615
+ @staticmethod
616
+ def _is_git_internal_path(path: str) -> bool:
617
+ normalized = os.path.normpath(path)
618
+ parts = normalized.split(os.sep)
619
+ if ".git" in parts:
620
+ return True
621
+ basename = os.path.basename(normalized)
622
+ return basename == ".gitmodules"
623
+
624
+ @staticmethod
625
+ def _is_git_internal_dir(path: str) -> bool:
626
+ normalized = os.path.normpath(path)
627
+ parts = normalized.split(os.sep)
628
+ return ".git" in parts
629
+
630
+ @staticmethod
631
+ def _looks_like_bare_repo(path: str) -> bool:
632
+ return (
633
+ os.path.isdir(path)
634
+ and os.path.isfile(os.path.join(path, "HEAD"))
635
+ and os.path.isfile(os.path.join(path, "config"))
636
+ and os.path.isdir(os.path.join(path, "objects"))
637
+ and os.path.isdir(os.path.join(path, "refs"))
638
+ and not os.path.isdir(os.path.join(path, ".git"))
639
+ )
640
+
641
+
642
+ class BashSecurityHardener:
643
+ """模块 3 总协作者:在 AST/path/policy 链路之外追加防绕过检查。"""
644
+
645
+ def __init__(
646
+ self,
647
+ *,
648
+ canonicalizer: BashCommandCanonicalizer | None = None,
649
+ env_checker: BashEnvSecurityChecker | None = None,
650
+ unicode_checker: BashUnicodeRiskChecker | None = None,
651
+ exec_ctx_checker: BashExecutionContextChecker | None = None,
652
+ cross_platform_checker: BashCrossPlatformBinaryChecker | None = None,
653
+ remote_pipe_checker: BashRemoteShellPipelineChecker | None = None,
654
+ git_cli_checker: BashGitCliInjectionChecker | None = None,
655
+ shell_checker: BashShellRiskChecker | None = None,
656
+ git_checker: BashGitSecurityChecker | None = None,
657
+ ) -> None:
658
+ self._canonicalizer = canonicalizer or BashCommandCanonicalizer()
659
+ self._env_checker = env_checker or BashEnvSecurityChecker()
660
+ self._unicode_checker = unicode_checker or BashUnicodeRiskChecker()
661
+ self._exec_ctx_checker = exec_ctx_checker or BashExecutionContextChecker()
662
+ self._cross_platform_checker = cross_platform_checker or BashCrossPlatformBinaryChecker()
663
+ self._remote_pipe_checker = remote_pipe_checker or BashRemoteShellPipelineChecker()
664
+ self._git_cli_checker = git_cli_checker or BashGitCliInjectionChecker()
665
+ self._shell_checker = shell_checker or BashShellRiskChecker()
666
+ self._git_checker = git_checker or BashGitSecurityChecker()
667
+
668
+ def canonicalize_for_policy(self, command: str) -> str:
669
+ canonical = self._canonicalizer.canonicalize(command)
670
+ return canonical or command
671
+
672
+ def check_command(
673
+ self,
674
+ *,
675
+ command: str,
676
+ analysis: BashAstAnalysis,
677
+ cwd: str,
678
+ ) -> AuthorizationDecision | None:
679
+ for checker in (
680
+ self._env_checker.evaluate(command),
681
+ self._unicode_checker.evaluate(command),
682
+ self._exec_ctx_checker.evaluate(command),
683
+ self._cross_platform_checker.evaluate(command),
684
+ self._remote_pipe_checker.evaluate(command),
685
+ self._shell_checker.evaluate(command),
686
+ self._git_cli_checker.evaluate(command),
687
+ self._git_checker.evaluate_command(command=command, analysis=analysis, cwd=cwd),
688
+ ):
689
+ if checker is not None:
690
+ return checker.to_decision()
691
+ return None
692
+
693
+ def check_segment(
694
+ self,
695
+ *,
696
+ command: str,
697
+ analysis: BashAstAnalysis,
698
+ cwd: str,
699
+ ) -> AuthorizationDecision | None:
700
+ result = self._git_checker.evaluate_segment(command=command, analysis=analysis, cwd=cwd)
701
+ return result.to_decision() if result is not None else None
702
+
703
+ def audit_explain(
704
+ self,
705
+ *,
706
+ command: str,
707
+ analysis: BashAstAnalysis | None = None,
708
+ cwd: str = "",
709
+ ) -> dict[str, Any]:
710
+ """
711
+ 结构化攻防审计快照(不阻断执行):用于日志、调试与 UI 对齐 CC 式可观测性。
712
+ """
713
+ modified = sorted(self._env_checker.collect_modified_vars(command))
714
+ dangerous = sorted(
715
+ v for v in modified if v in self._env_checker.BINARY_HIJACK_VARS
716
+ )
717
+ first_block: dict[str, Any] | None = None
718
+ if analysis is not None:
719
+ d = self.check_command(command=command, analysis=analysis, cwd=cwd or os.getcwd())
720
+ if d is not None:
721
+ first_block = {
722
+ "behavior": d.behavior,
723
+ "policy_id": d.policy_id,
724
+ "metadata": d.metadata,
725
+ }
726
+ return {
727
+ "canonical_command": self.canonicalize_for_policy(command),
728
+ "sys_platform_raw": sys.platform,
729
+ "host_platform": get_host_platform().value,
730
+ "modified_env_vars": modified,
731
+ "dangerous_env_vars": dangerous,
732
+ "would_block_or_ask": first_block,
733
+ "shell_pattern_probe": self._shell_checker.probe(command),
734
+ }