patchi 0.7.5__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 (496) hide show
  1. patchi/__init__.py +1 -0
  2. patchi/__main__.py +13 -0
  3. patchi/cli/__init__.py +0 -0
  4. patchi/cli/command_families.py +235 -0
  5. patchi/cli/commands/__init__.py +0 -0
  6. patchi/cli/commands/access_cmd.py +65 -0
  7. patchi/cli/commands/agent_stats_cmd.py +273 -0
  8. patchi/cli/commands/agents_cmd.py +187 -0
  9. patchi/cli/commands/ai_cmd.py +308 -0
  10. patchi/cli/commands/assure_cmd.py +538 -0
  11. patchi/cli/commands/audit_cmd.py +286 -0
  12. patchi/cli/commands/auto_cmd.py +110 -0
  13. patchi/cli/commands/blame_cmd.py +53 -0
  14. patchi/cli/commands/chains_cmd.py +327 -0
  15. patchi/cli/commands/charter_cmd.py +173 -0
  16. patchi/cli/commands/chat_cmd.py +597 -0
  17. patchi/cli/commands/check_cmd.py +232 -0
  18. patchi/cli/commands/cleanup_cmd.py +328 -0
  19. patchi/cli/commands/cockpit_cmd.py +208 -0
  20. patchi/cli/commands/commands_cmd.py +160 -0
  21. patchi/cli/commands/cross_repo_cmd.py +113 -0
  22. patchi/cli/commands/deps_cmd.py +110 -0
  23. patchi/cli/commands/dev_check_cmd.py +342 -0
  24. patchi/cli/commands/dev_cmd.py +445 -0
  25. patchi/cli/commands/doctor_cmd.py +457 -0
  26. patchi/cli/commands/findings_cmd.py +187 -0
  27. patchi/cli/commands/fix_cmd.py +648 -0
  28. patchi/cli/commands/fix_review_cmd.py +273 -0
  29. patchi/cli/commands/heatmap_cmd.py +62 -0
  30. patchi/cli/commands/help_cmd.py +159 -0
  31. patchi/cli/commands/hosted_cmd.py +980 -0
  32. patchi/cli/commands/init.py +456 -0
  33. patchi/cli/commands/key_cmd.py +571 -0
  34. patchi/cli/commands/learn_cmd.py +140 -0
  35. patchi/cli/commands/link_cmd.py +104 -0
  36. patchi/cli/commands/log_cmd.py +57 -0
  37. patchi/cli/commands/memory_cmd.py +215 -0
  38. patchi/cli/commands/model_cmd.py +207 -0
  39. patchi/cli/commands/notify_cmd.py +213 -0
  40. patchi/cli/commands/notify_prompts.py +29 -0
  41. patchi/cli/commands/patch_cmd.py +231 -0
  42. patchi/cli/commands/plan_cmd.py +128 -0
  43. patchi/cli/commands/plugins_cmd.py +307 -0
  44. patchi/cli/commands/queue_cmd.py +152 -0
  45. patchi/cli/commands/quick_cmd.py +159 -0
  46. patchi/cli/commands/ready_cmd.py +292 -0
  47. patchi/cli/commands/report_cmd.py +416 -0
  48. patchi/cli/commands/restrict_cmd.py +127 -0
  49. patchi/cli/commands/review_cmd.py +225 -0
  50. patchi/cli/commands/rules_cmd.py +233 -0
  51. patchi/cli/commands/scan_cmd.py +1908 -0
  52. patchi/cli/commands/settings_cmd.py +45 -0
  53. patchi/cli/commands/status_cmd.py +679 -0
  54. patchi/cli/commands/test_cmd.py +1052 -0
  55. patchi/cli/commands/trend_cmd.py +296 -0
  56. patchi/cli/commands/undo_cmd.py +235 -0
  57. patchi/cli/commands/update_cmd.py +345 -0
  58. patchi/cli/commands/verify_cmd.py +56 -0
  59. patchi/cli/commands/vr_cmd.py +440 -0
  60. patchi/cli/commands/watch_cmd.py +304 -0
  61. patchi/cli/commands/web_cmd.py +114 -0
  62. patchi/cli/console.py +101 -0
  63. patchi/cli/display/__init__.py +1 -0
  64. patchi/cli/display/live_audit.py +269 -0
  65. patchi/cli/display/live_progress.py +169 -0
  66. patchi/cli/framework.py +241 -0
  67. patchi/cli/logo.py +128 -0
  68. patchi/cli/main.py +222 -0
  69. patchi/cli/registry.py +1171 -0
  70. patchi/cli/style.py +68 -0
  71. patchi/cli/themes.py +187 -0
  72. patchi/cli/ux.py +444 -0
  73. patchi/core/__init__.py +0 -0
  74. patchi/core/agents/__init__.py +0 -0
  75. patchi/core/agents/api_fuzzer_agent.py +141 -0
  76. patchi/core/agents/attack_agent.py +242 -0
  77. patchi/core/agents/base.py +1012 -0
  78. patchi/core/agents/bug_predictor_agent.py +58 -0
  79. patchi/core/agents/build_tool_validator.py +150 -0
  80. patchi/core/agents/cache.py +192 -0
  81. patchi/core/agents/chaos_agent.py +133 -0
  82. patchi/core/agents/cicd_generator.py +240 -0
  83. patchi/core/agents/comment_scanner.py +280 -0
  84. patchi/core/agents/console_logging_agent.py +156 -0
  85. patchi/core/agents/coordinator.py +858 -0
  86. patchi/core/agents/core_scanner.py +269 -0
  87. patchi/core/agents/coverage_prioritizer.py +181 -0
  88. patchi/core/agents/dead_code_hygiene.py +296 -0
  89. patchi/core/agents/dead_code_scanner.py +692 -0
  90. patchi/core/agents/dependency_scanner.py +725 -0
  91. patchi/core/agents/doc_claim_agent.py +573 -0
  92. patchi/core/agents/duplicate_scanner.py +568 -0
  93. patchi/core/agents/env_scanner.py +410 -0
  94. patchi/core/agents/feature_flag_agent.py +89 -0
  95. patchi/core/agents/frontend_framework_agent.py +133 -0
  96. patchi/core/agents/goroutine_agent.py +82 -0
  97. patchi/core/agents/governor.py +1972 -0
  98. patchi/core/agents/i18n_agent.py +95 -0
  99. patchi/core/agents/license_compliance.py +247 -0
  100. patchi/core/agents/linking_agent.py +251 -0
  101. patchi/core/agents/memory_profiler_agent.py +102 -0
  102. patchi/core/agents/modernization_agent.py +84 -0
  103. patchi/core/agents/mutation_agent.py +174 -0
  104. patchi/core/agents/nl_query_agent.py +74 -0
  105. patchi/core/agents/promise_rejection_agent.py +100 -0
  106. patchi/core/agents/refactoring_agent.py +286 -0
  107. patchi/core/agents/resource_leak_agent.py +91 -0
  108. patchi/core/agents/route_graph_scanner.py +951 -0
  109. patchi/core/agents/run_agent.py +106 -0
  110. patchi/core/agents/rust_unwrap_agent.py +100 -0
  111. patchi/core/agents/sbom_generator.py +284 -0
  112. patchi/core/agents/scanners.py +67 -0
  113. patchi/core/agents/side/__init__.py +1 -0
  114. patchi/core/agents/side/build_agent.py +65 -0
  115. patchi/core/agents/side/format_agent.py +50 -0
  116. patchi/core/agents/side/install_agent.py +63 -0
  117. patchi/core/agents/side_file_scanner.py +549 -0
  118. patchi/core/agents/snapshot_drift_detector.py +146 -0
  119. patchi/core/agents/spa_route_inventory.py +203 -0
  120. patchi/core/agents/test_scanner.py +442 -0
  121. patchi/core/agents/tool_health.py +109 -0
  122. patchi/core/agents/tool_runner.py +162 -0
  123. patchi/core/agents/type_scanner.py +168 -0
  124. patchi/core/agents/ui_scanner.py +558 -0
  125. patchi/core/ai/__init__.py +20 -0
  126. patchi/core/ai/agent_audit.py +153 -0
  127. patchi/core/ai/agent_profiler.py +277 -0
  128. patchi/core/ai/client.py +499 -0
  129. patchi/core/ai/cost_tracker.py +107 -0
  130. patchi/core/ai/model_router.py +318 -0
  131. patchi/core/ai/orchestrator.py +625 -0
  132. patchi/core/ai/prompts.py +681 -0
  133. patchi/core/ai/smart.py +302 -0
  134. patchi/core/ai/tool_executor.py +24 -0
  135. patchi/core/ai/tools/__init__.py +10 -0
  136. patchi/core/ai/tools/executor.py +621 -0
  137. patchi/core/ai/tools/realize.py +1160 -0
  138. patchi/core/ai/tools/registry.py +1577 -0
  139. patchi/core/assurance/__init__.py +33 -0
  140. patchi/core/assurance/builder.py +211 -0
  141. patchi/core/assurance/graph.py +342 -0
  142. patchi/core/assurance/invariants.py +168 -0
  143. patchi/core/atomic.py +61 -0
  144. patchi/core/attackers/__init__.py +17 -0
  145. patchi/core/attackers/api_abuse.py +89 -0
  146. patchi/core/attackers/auth_bypass.py +73 -0
  147. patchi/core/attackers/base.py +91 -0
  148. patchi/core/attackers/business_logic.py +88 -0
  149. patchi/core/attackers/planner.py +108 -0
  150. patchi/core/attackers/priv_escalation.py +88 -0
  151. patchi/core/attackers/recon.py +87 -0
  152. patchi/core/brain/__init__.py +0 -0
  153. patchi/core/brain/ast_utils/__init__.py +81 -0
  154. patchi/core/brain/ast_utils/assignments.py +219 -0
  155. patchi/core/brain/ast_utils/calls.py +139 -0
  156. patchi/core/brain/ast_utils/config.py +132 -0
  157. patchi/core/brain/ast_utils/control_flow.py +74 -0
  158. patchi/core/brain/ast_utils/dead_symbols.py +205 -0
  159. patchi/core/brain/ast_utils/decorators.py +108 -0
  160. patchi/core/brain/ast_utils/helpers.py +144 -0
  161. patchi/core/brain/ast_utils/imports.py +169 -0
  162. patchi/core/brain/ast_utils/scan.py +143 -0
  163. patchi/core/brain/ast_utils/taint.py +310 -0
  164. patchi/core/brain/audit.py +379 -0
  165. patchi/core/brain/baseline.py +195 -0
  166. patchi/core/brain/blast_radius.py +312 -0
  167. patchi/core/brain/body_tags.py +144 -0
  168. patchi/core/brain/brain.py +1161 -0
  169. patchi/core/brain/brain_context.py +393 -0
  170. patchi/core/brain/brain_watcher.py +340 -0
  171. patchi/core/brain/charter.py +473 -0
  172. patchi/core/brain/classifier.py +214 -0
  173. patchi/core/brain/cockpit.py +350 -0
  174. patchi/core/brain/code_query.py +710 -0
  175. patchi/core/brain/contract.py +889 -0
  176. patchi/core/brain/contract_diff.py +155 -0
  177. patchi/core/brain/council.py +800 -0
  178. patchi/core/brain/cross_repo.py +104 -0
  179. patchi/core/brain/dead_code_tools.py +157 -0
  180. patchi/core/brain/doc_validator.py +197 -0
  181. patchi/core/brain/domain_activator.py +2183 -0
  182. patchi/core/brain/enriched_context.py +220 -0
  183. patchi/core/brain/file_corpus.py +220 -0
  184. patchi/core/brain/framework.py +953 -0
  185. patchi/core/brain/freshness.py +273 -0
  186. patchi/core/brain/git_aware.py +206 -0
  187. patchi/core/brain/heatmap.py +104 -0
  188. patchi/core/brain/ignore_parser.py +153 -0
  189. patchi/core/brain/import_graph.py +486 -0
  190. patchi/core/brain/languages.py +397 -0
  191. patchi/core/brain/layered_brain.py +419 -0
  192. patchi/core/brain/learning.py +229 -0
  193. patchi/core/brain/orphaned_endpoints.py +313 -0
  194. patchi/core/brain/personas/__init__.py +648 -0
  195. patchi/core/brain/personas/bad_user.py +165 -0
  196. patchi/core/brain/personas/base.py +349 -0
  197. patchi/core/brain/proactive.py +807 -0
  198. patchi/core/brain/project_context.py +121 -0
  199. patchi/core/brain/project_reader.py +521 -0
  200. patchi/core/brain/reasoning.py +269 -0
  201. patchi/core/brain/route_detector/__init__.py +15 -0
  202. patchi/core/brain/route_detector/base.py +31 -0
  203. patchi/core/brain/route_detector/csharp.py +209 -0
  204. patchi/core/brain/route_detector/file_based.py +81 -0
  205. patchi/core/brain/route_detector/go.py +152 -0
  206. patchi/core/brain/route_detector/java.py +195 -0
  207. patchi/core/brain/route_detector/javascript.py +197 -0
  208. patchi/core/brain/route_detector/php.py +158 -0
  209. patchi/core/brain/route_detector/python.py +193 -0
  210. patchi/core/brain/route_detector/registry.py +25 -0
  211. patchi/core/brain/route_detector/ruby.py +134 -0
  212. patchi/core/brain/route_detector/rust.py +216 -0
  213. patchi/core/brain/route_detector/swift.py +118 -0
  214. patchi/core/brain/route_mapper.py +265 -0
  215. patchi/core/brain/scanner.py +2136 -0
  216. patchi/core/brain/secrets.py +280 -0
  217. patchi/core/brain/symbol_graph.py +1955 -0
  218. patchi/core/brain/team.py +28 -0
  219. patchi/core/brain/trace_log.py +132 -0
  220. patchi/core/brain/type_checker/__init__.py +65 -0
  221. patchi/core/brain/type_checker/base.py +60 -0
  222. patchi/core/brain/type_checker/csharp.py +59 -0
  223. patchi/core/brain/type_checker/dart.py +59 -0
  224. patchi/core/brain/type_checker/go.py +81 -0
  225. patchi/core/brain/type_checker/java.py +75 -0
  226. patchi/core/brain/type_checker/kotlin.py +75 -0
  227. patchi/core/brain/type_checker/php.py +88 -0
  228. patchi/core/brain/type_checker/python.py +87 -0
  229. patchi/core/brain/type_checker/rust.py +81 -0
  230. patchi/core/brain/type_checker/swift.py +59 -0
  231. patchi/core/brain/type_checker/typescript.py +298 -0
  232. patchi/core/brain/understander.py +130 -0
  233. patchi/core/brain/verify.py +379 -0
  234. patchi/core/campaigns/__init__.py +14 -0
  235. patchi/core/campaigns/base.py +62 -0
  236. patchi/core/campaigns/data_flow_abuse.py +153 -0
  237. patchi/core/campaigns/orchestrator.py +140 -0
  238. patchi/core/campaigns/state_transitions.py +178 -0
  239. patchi/core/ci_bundle.py +104 -0
  240. patchi/core/config.py +311 -0
  241. patchi/core/constants.py +245 -0
  242. patchi/core/debug/__init__.py +25 -0
  243. patchi/core/debug/adapters/__init__.py +11 -0
  244. patchi/core/debug/adapters/codelldb.py +379 -0
  245. patchi/core/debug/adapters/node.py +222 -0
  246. patchi/core/debug/adapters/powershell.py +210 -0
  247. patchi/core/debug/adapters/python.py +244 -0
  248. patchi/core/debug/capture.py +210 -0
  249. patchi/core/debug/dap_client.py +268 -0
  250. patchi/core/detector/__init__.py +0 -0
  251. patchi/core/detector/audit.py +194 -0
  252. patchi/core/detector/bus.py +226 -0
  253. patchi/core/detector/dispatcher.py +275 -0
  254. patchi/core/detector/event.py +254 -0
  255. patchi/core/detector/sigma_engine.py +378 -0
  256. patchi/core/detector/triage.py +289 -0
  257. patchi/core/export/sarif.py +113 -0
  258. patchi/core/fix/__init__.py +17 -0
  259. patchi/core/fix/applier.py +574 -0
  260. patchi/core/fix/base.py +510 -0
  261. patchi/core/fix/code_fixer.py +169 -0
  262. patchi/core/fix/dead_code_remover.py +131 -0
  263. patchi/core/fix/fix_agents.py +775 -0
  264. patchi/core/fix/patch.py +444 -0
  265. patchi/core/fix/risk_gate.py +383 -0
  266. patchi/core/fix/security_fixer.py +93 -0
  267. patchi/core/fix/verify_loop.py +204 -0
  268. patchi/core/fuzz/__init__.py +8 -0
  269. patchi/core/fuzz/corpus.py +137 -0
  270. patchi/core/fuzz/input_fuzzer.py +256 -0
  271. patchi/core/fuzz/sequence_fuzzer.py +162 -0
  272. patchi/core/fuzz/state_fuzzer.py +236 -0
  273. patchi/core/health.py +407 -0
  274. patchi/core/hosted/__init__.py +1 -0
  275. patchi/core/hosted/anomaly.py +298 -0
  276. patchi/core/hosted/audit_log.py +102 -0
  277. patchi/core/hosted/compliance_report.py +148 -0
  278. patchi/core/hosted/ip_reputation.py +169 -0
  279. patchi/core/hosted/log_parsers.py +251 -0
  280. patchi/core/hosted/tokens.py +127 -0
  281. patchi/core/hosted/watchlist.py +164 -0
  282. patchi/core/hosted/webhooks.py +156 -0
  283. patchi/core/memory.py +437 -0
  284. patchi/core/notifications/__init__.py +18 -0
  285. patchi/core/notifications/channels.py +192 -0
  286. patchi/core/notifications/digest.py +141 -0
  287. patchi/core/notifications/escalation.py +158 -0
  288. patchi/core/notifications/notifier.py +182 -0
  289. patchi/core/notifications/quiet_hours.py +69 -0
  290. patchi/core/plugins/__init__.py +48 -0
  291. patchi/core/plugins/analyzer.py +326 -0
  292. patchi/core/plugins/analyzers/__init__.py +6 -0
  293. patchi/core/plugins/analyzers/dead_code_analyzer.py +120 -0
  294. patchi/core/plugins/analyzers/secret_scanner.py +158 -0
  295. patchi/core/plugins/analyzers/todo_scanner.py +115 -0
  296. patchi/core/plugins/registry.py +335 -0
  297. patchi/core/queue.py +375 -0
  298. patchi/core/reliability/__init__.py +13 -0
  299. patchi/core/reliability/chaos.py +163 -0
  300. patchi/core/reliability/fault_injector.py +172 -0
  301. patchi/core/reliability/idempotency.py +128 -0
  302. patchi/core/reliability/recovery.py +147 -0
  303. patchi/core/runtime/__init__.py +5 -0
  304. patchi/core/runtime/tracer.py +230 -0
  305. patchi/core/scan_bus.py +156 -0
  306. patchi/core/security/__init__.py +1 -0
  307. patchi/core/security/ai_validator.py +462 -0
  308. patchi/core/security/app_mapper.py +216 -0
  309. patchi/core/security/app_profile.py +464 -0
  310. patchi/core/security/attack_feedback.py +141 -0
  311. patchi/core/security/attack_scenarios/__init__.py +1 -0
  312. patchi/core/security/attack_tree.py +176 -0
  313. patchi/core/security/auth_audit_agent.py +344 -0
  314. patchi/core/security/authz_agent.py +602 -0
  315. patchi/core/security/auto_fix_proactive.py +620 -0
  316. patchi/core/security/auto_fixer.py +660 -0
  317. patchi/core/security/auto_ticket.py +53 -0
  318. patchi/core/security/bandit_agent.py +152 -0
  319. patchi/core/security/blast_radius.py +226 -0
  320. patchi/core/security/browser_tester.py +333 -0
  321. patchi/core/security/business_logic_agent.py +263 -0
  322. patchi/core/security/catch_block_auditor.py +395 -0
  323. patchi/core/security/cdn_cache_agent.py +357 -0
  324. patchi/core/security/chain_analyzer.py +321 -0
  325. patchi/core/security/chain_to_assurance.py +168 -0
  326. patchi/core/security/charter.py +510 -0
  327. patchi/core/security/cloud_waf_detector.py +295 -0
  328. patchi/core/security/codeql_agent.py +233 -0
  329. patchi/core/security/compliance_agent.py +348 -0
  330. patchi/core/security/confidence_gate.py +423 -0
  331. patchi/core/security/container_scanner.py +282 -0
  332. patchi/core/security/crypto_agent.py +378 -0
  333. patchi/core/security/cve_monitor.py +262 -0
  334. patchi/core/security/dast_agent.py +865 -0
  335. patchi/core/security/defectdojo.py +238 -0
  336. patchi/core/security/defenders/__init__.py +75 -0
  337. patchi/core/security/defenders/auth_middleware.py +115 -0
  338. patchi/core/security/defenders/base.py +97 -0
  339. patchi/core/security/defenders/block_ip.py +71 -0
  340. patchi/core/security/defenders/block_ws_origin.py +36 -0
  341. patchi/core/security/defenders/crypto_fix.py +118 -0
  342. patchi/core/security/defenders/enforce_rate_limit.py +108 -0
  343. patchi/core/security/defenders/fix_code.py +93 -0
  344. patchi/core/security/defenders/invalidate_session.py +40 -0
  345. patchi/core/security/defenders/patch_config.py +70 -0
  346. patchi/core/security/defenders/rotate_secret.py +54 -0
  347. patchi/core/security/defenders/suspend_account.py +41 -0
  348. patchi/core/security/defenders/update_dependency.py +160 -0
  349. patchi/core/security/defense_layer.py +217 -0
  350. patchi/core/security/dependency_vulnerability_agent.py +616 -0
  351. patchi/core/security/detection_pipeline.py +289 -0
  352. patchi/core/security/dns_security_agent.py +497 -0
  353. patchi/core/security/domain_activator_v2.py +913 -0
  354. patchi/core/security/domain_loader.py +1004 -0
  355. patchi/core/security/domains/__init__.py +0 -0
  356. patchi/core/security/email_authentication_agent.py +417 -0
  357. patchi/core/security/env_var_validator.py +304 -0
  358. patchi/core/security/evidence.py +100 -0
  359. patchi/core/security/falco_runtime_agent.py +387 -0
  360. patchi/core/security/fix-playbooks/__init__.py +0 -0
  361. patchi/core/security/gated_finding.py +85 -0
  362. patchi/core/security/git_diff_activator.py +367 -0
  363. patchi/core/security/governance.py +208 -0
  364. patchi/core/security/gradual_gate.py +34 -0
  365. patchi/core/security/history.py +315 -0
  366. patchi/core/security/iac_scanner.py +250 -0
  367. patchi/core/security/ignore_expiry.py +111 -0
  368. patchi/core/security/ignore_learner.py +416 -0
  369. patchi/core/security/injection_agent.py +319 -0
  370. patchi/core/security/insecure_randomness_agent.py +288 -0
  371. patchi/core/security/intent_analyzer.py +326 -0
  372. patchi/core/security/jwt_agent.py +453 -0
  373. patchi/core/security/kubernetes_agent.py +638 -0
  374. patchi/core/security/layer2_orchestrator.py +341 -0
  375. patchi/core/security/llm_security_agent.py +252 -0
  376. patchi/core/security/misconfig_agent.py +600 -0
  377. patchi/core/security/mobile_security_agent.py +474 -0
  378. patchi/core/security/network_agent.py +376 -0
  379. patchi/core/security/noise_filter.py +274 -0
  380. patchi/core/security/orchestrator.py +313 -0
  381. patchi/core/security/pattern_context.py +125 -0
  382. patchi/core/security/pattern_loader.py +101 -0
  383. patchi/core/security/pentest/__init__.py +4 -0
  384. patchi/core/security/pentest/base.py +58 -0
  385. patchi/core/security/pentest/dalfox_adapter.py +40 -0
  386. patchi/core/security/pentest/ffuf_adapter.py +48 -0
  387. patchi/core/security/pentest/nuclei_adapter.py +50 -0
  388. patchi/core/security/pentest/registry.py +80 -0
  389. patchi/core/security/pentest/shannon_adapter.py +107 -0
  390. patchi/core/security/pentest/sqlmap_adapter.py +56 -0
  391. patchi/core/security/pentest/zap_adapter.py +52 -0
  392. patchi/core/security/plan_auditor.py +276 -0
  393. patchi/core/security/policy_engine.py +272 -0
  394. patchi/core/security/prechecks.py +172 -0
  395. patchi/core/security/precommit_hook.py +247 -0
  396. patchi/core/security/privacy_agent.py +361 -0
  397. patchi/core/security/push_notification_agent.py +417 -0
  398. patchi/core/security/pysa_agent.py +143 -0
  399. patchi/core/security/reasoning.py +442 -0
  400. patchi/core/security/red_team_agent.py +184 -0
  401. patchi/core/security/red_team_engine.py +1142 -0
  402. patchi/core/security/remediation.py +360 -0
  403. patchi/core/security/request_interceptor.py +294 -0
  404. patchi/core/security/runtime_validator.py +245 -0
  405. patchi/core/security/saml_sso_agent.py +401 -0
  406. patchi/core/security/sast_agent.py +213 -0
  407. patchi/core/security/sast_gate.py +95 -0
  408. patchi/core/security/scheduler.py +287 -0
  409. patchi/core/security/secrets_runtime_agent.py +476 -0
  410. patchi/core/security/security_agents.py +175 -0
  411. patchi/core/security/security_config.py +551 -0
  412. patchi/core/security/security_probe.py +762 -0
  413. patchi/core/security/security_taint.py +1005 -0
  414. patchi/core/security/sensitive_data_agent.py +271 -0
  415. patchi/core/security/service_mesh_agent.py +412 -0
  416. patchi/core/security/session_management_agent.py +245 -0
  417. patchi/core/security/ssrf_agent.py +422 -0
  418. patchi/core/security/supply_chain.py +449 -0
  419. patchi/core/security/threat_model_generator.py +378 -0
  420. patchi/core/security/threat_model_updater.py +196 -0
  421. patchi/core/security/tool_adapters.py +135 -0
  422. patchi/core/security/tool_verify.py +173 -0
  423. patchi/core/security/websocket_security_agent.py +188 -0
  424. patchi/core/snapshot.py +242 -0
  425. patchi/core/tenant.py +386 -0
  426. patchi/core/testing/__init__.py +17 -0
  427. patchi/core/testing/_browser.py +147 -0
  428. patchi/core/testing/ads_agent.py +141 -0
  429. patchi/core/testing/api_contract_agent.py +450 -0
  430. patchi/core/testing/app_discovery_agent.py +641 -0
  431. patchi/core/testing/app_launcher.py +184 -0
  432. patchi/core/testing/browser_test_agent.py +516 -0
  433. patchi/core/testing/e2e_flow_agent.py +354 -0
  434. patchi/core/testing/flake_detector_agent.py +292 -0
  435. patchi/core/testing/gate.py +31 -0
  436. patchi/core/testing/live_test_runner.py +304 -0
  437. patchi/core/testing/live_v2/__init__.py +1 -0
  438. patchi/core/testing/live_v2/browser_pool.py +349 -0
  439. patchi/core/testing/live_v2/browser_test_runner.py +313 -0
  440. patchi/core/testing/live_v2/runner.py +626 -0
  441. patchi/core/testing/live_v2/screenshot_manager.py +474 -0
  442. patchi/core/testing/live_v2/stress_orchestrator.py +523 -0
  443. patchi/core/testing/live_v2/video_recorder.py +303 -0
  444. patchi/core/testing/regression_agent.py +302 -0
  445. patchi/core/testing/security_test_agent.py +253 -0
  446. patchi/core/testing/stress_test_agent.py +203 -0
  447. patchi/core/testing/test_agents.py +50 -0
  448. patchi/core/testing/test_config.py +194 -0
  449. patchi/core/testing/ui_accessibility_agent.py +324 -0
  450. patchi/core/testing/ui_button_agent.py +294 -0
  451. patchi/core/testing/ui_layout_agent.py +330 -0
  452. patchi/core/testing/unit_test_agent.py +1149 -0
  453. patchi/core/testing/visual_regression_agent.py +260 -0
  454. patchi/web/__init__.py +1 -0
  455. patchi/web/api/__init__.py +1 -0
  456. patchi/web/api/brain_map.py +114 -0
  457. patchi/web/api/charts.py +47 -0
  458. patchi/web/api/chat.py +224 -0
  459. patchi/web/api/cicd.py +576 -0
  460. patchi/web/api/dast_evidence.py +49 -0
  461. patchi/web/api/dev_check.py +320 -0
  462. patchi/web/api/fix.py +134 -0
  463. patchi/web/api/guard.py +65 -0
  464. patchi/web/api/hosted.py +128 -0
  465. patchi/web/api/hosted_v2.py +234 -0
  466. patchi/web/api/live_testing.py +699 -0
  467. patchi/web/api/scan.py +582 -0
  468. patchi/web/api/smart.py +111 -0
  469. patchi/web/api/tenant.py +117 -0
  470. patchi/web/api/tools.py +85 -0
  471. patchi/web/api_legacy.py +2401 -0
  472. patchi/web/app.py +310 -0
  473. patchi/web/events.py +483 -0
  474. patchi/web/routes/__init__.py +1 -0
  475. patchi/web/routes/assurance.py +541 -0
  476. patchi/web/routes/brain.py +36 -0
  477. patchi/web/routes/charter.py +227 -0
  478. patchi/web/routes/chat.py +23 -0
  479. patchi/web/routes/dashboard.py +558 -0
  480. patchi/web/routes/dashboard_v2.py +402 -0
  481. patchi/web/routes/findings.py +330 -0
  482. patchi/web/routes/guard.py +35 -0
  483. patchi/web/routes/history.py +126 -0
  484. patchi/web/routes/landing.py +52 -0
  485. patchi/web/routes/live_testing.py +24 -0
  486. patchi/web/routes/review.py +57 -0
  487. patchi/web/routes/self_improvement.py +110 -0
  488. patchi/web/routes/settings.py +49 -0
  489. patchi/web/routes/tokens.py +29 -0
  490. patchi/web/spawn.py +139 -0
  491. patchi/web/ws.py +141 -0
  492. patchi-0.7.5.dist-info/METADATA +315 -0
  493. patchi-0.7.5.dist-info/RECORD +496 -0
  494. patchi-0.7.5.dist-info/WHEEL +5 -0
  495. patchi-0.7.5.dist-info/licenses/LICENSE +60 -0
  496. patchi-0.7.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,621 @@
1
+ """
2
+ Tool Executor — Executes AI tool calls with validation, confirmation, and error handling.
3
+
4
+ Handles:
5
+ - Parameter validation against JSON schemas
6
+ - User confirmation for destructive operations
7
+ - Execution with timeout and error recovery
8
+ - Result formatting for AI consumption
9
+ - Audit logging of all tool invocations
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import asyncio
15
+ import inspect
16
+ import json
17
+ import logging
18
+ import time
19
+ import uuid
20
+ from collections.abc import Callable
21
+ from dataclasses import dataclass, field
22
+ from datetime import UTC, datetime
23
+ from pathlib import Path
24
+ from typing import Any
25
+
26
+ from patchi.core.ai.tools.registry import ToolDefinition, ToolParameter, get_tool_registry
27
+
28
+ _log = logging.getLogger("patchi.ai.tool_executor")
29
+
30
+
31
+ @dataclass
32
+ class ToolInvocation:
33
+ """A single tool invocation record for audit logging."""
34
+
35
+ id: str
36
+ tool_name: str
37
+ parameters: dict
38
+ invoked_by: str # "council", "persona", "cli", "user"
39
+ persona_name: str | None = None
40
+ timestamp: str = field(default_factory=lambda: datetime.now(UTC).isoformat())
41
+ started_at: float = 0
42
+ completed_at: float = 0
43
+ success: bool = False
44
+ result: dict | None = None
45
+ error: str | None = None
46
+ confirmation_given: bool = False
47
+
48
+ @property
49
+ def duration_ms(self) -> int:
50
+ """Wall-clock duration of the invocation in milliseconds."""
51
+ if not self.completed_at:
52
+ return 0
53
+ return int((self.completed_at - self.started_at) * 1000)
54
+
55
+
56
+ @dataclass
57
+ class ExecutionResult:
58
+ """Result of a tool execution."""
59
+
60
+ success: bool
61
+ result: Any = None
62
+ error: str | None = None
63
+ invocation_id: str = ""
64
+ duration_ms: int = 0
65
+
66
+
67
+ class ConfirmationProvider:
68
+ """Interface for getting user confirmation."""
69
+
70
+ async def confirm(
71
+ self, tool_name: str, parameters: dict, side_effects: str, description: str
72
+ ) -> bool:
73
+ """Return True if user confirms, False otherwise."""
74
+ raise NotImplementedError
75
+
76
+
77
+ class CLIConfirmationProvider(ConfirmationProvider):
78
+ """CLI-based confirmation using rich prompts."""
79
+
80
+ def __init__(self, auto_confirm: bool = False):
81
+ self.auto_confirm = auto_confirm
82
+
83
+ async def confirm(
84
+ self, tool_name: str, parameters: dict, side_effects: str, description: str
85
+ ) -> bool:
86
+ if self.auto_confirm:
87
+ return True
88
+
89
+ try:
90
+ from rich.console import Console
91
+ from rich.prompt import Confirm
92
+
93
+ console = Console()
94
+ console.print(f"\n[yellow]⚠ Tool requires confirmation:[/yellow] {tool_name}")
95
+ console.print(f" Description: {description}")
96
+ console.print(f" Parameters: {json.dumps(parameters, indent=2)}")
97
+ if side_effects:
98
+ console.print(f" [red]Side effects:[/red] {side_effects}")
99
+
100
+ return Confirm.ask(" Proceed?", default=False)
101
+ except Exception:
102
+ # Fallback to simple input
103
+ if side_effects:
104
+ pass
105
+ response = input(" Proceed? (y/N): ").strip().lower()
106
+ return response in ("y", "yes")
107
+
108
+
109
+ class WebConfirmationProvider(ConfirmationProvider):
110
+ """Web confirmation — auto-confirms since the web UI handles confirmation via its own modal."""
111
+
112
+ async def confirm(
113
+ self, tool_name: str, parameters: dict, side_effects: str, description: str
114
+ ) -> bool:
115
+ # Web UI handles confirmation through its own modal/dialog
116
+ # Auto-confirm here since the web route already validated the request
117
+ return True
118
+
119
+
120
+ @dataclass
121
+ class StateSnapshot:
122
+ """Snapshot of file system state for rollback."""
123
+
124
+ id: str
125
+ files: dict[str, bytes] = field(default_factory=dict) # path -> content
126
+ created_at: str = field(default_factory=lambda: datetime.now(UTC).isoformat())
127
+
128
+
129
+ class ToolExecutor:
130
+ """
131
+ Executes tool calls with full validation, confirmation, and error handling.
132
+
133
+ Features:
134
+ - JSON schema validation for parameters
135
+ - Confirmation gates for destructive operations
136
+ - Timeout enforcement
137
+ - Structured error handling
138
+ - Audit logging
139
+ - Result normalization for AI consumption
140
+ - State snapshots and rollback on failure
141
+ - Tool chain execution with automatic rollback
142
+ """
143
+
144
+ def __init__(
145
+ self,
146
+ root: Path,
147
+ confirmation_provider: ConfirmationProvider | None = None,
148
+ default_timeout: float = 120.0,
149
+ on_progress: Callable[[str], None] | None = None,
150
+ ):
151
+ self.root = root
152
+ self.registry = get_tool_registry()
153
+ self.confirmation_provider = confirmation_provider or CLIConfirmationProvider()
154
+ self.default_timeout = default_timeout
155
+ self.on_progress = on_progress or (lambda _: None)
156
+
157
+ # Audit log
158
+ self.invocation_log: list[ToolInvocation] = []
159
+ self._log_path = root / ".patchi" / "logs" / "tool_invocations.jsonl"
160
+ self._log_path.parent.mkdir(parents=True, exist_ok=True)
161
+
162
+ # Rollback state
163
+ self._snapshots: dict[str, StateSnapshot] = {}
164
+ self._snapshot_dir = root / ".patchi" / "snapshots"
165
+ self._snapshot_dir.mkdir(parents=True, exist_ok=True)
166
+
167
+ async def execute(
168
+ self,
169
+ tool_name: str,
170
+ parameters: dict,
171
+ invoked_by: str = "user",
172
+ persona_name: str | None = None,
173
+ timeout: float | None = None,
174
+ skip_confirmation: bool = False,
175
+ ) -> ExecutionResult:
176
+ """
177
+ Execute a tool call with full validation and safety checks.
178
+
179
+ Args:
180
+ tool_name: Name of the tool to execute
181
+ parameters: Parameters for the tool
182
+ invoked_by: Who is invoking ("council", "persona", "cli", "user")
183
+ persona_name: Name of the persona if invoked by one
184
+ timeout: Custom timeout in seconds
185
+ skip_confirmation: Skip confirmation even if required (for testing)
186
+
187
+ Returns:
188
+ ExecutionResult with success status, result, or error
189
+ """
190
+ invocation_id = str(uuid.uuid4())[:8]
191
+ start_time = time.monotonic()
192
+
193
+ # Create invocation record
194
+ invocation = ToolInvocation(
195
+ id=invocation_id,
196
+ tool_name=tool_name,
197
+ parameters=parameters,
198
+ invoked_by=invoked_by,
199
+ persona_name=persona_name,
200
+ started_at=start_time,
201
+ )
202
+
203
+ self.on_progress(f"[EXEC] Executing tool: {tool_name}")
204
+ _log.info(f"Tool invocation {invocation_id}: {tool_name}({parameters}) by {invoked_by}")
205
+
206
+ try:
207
+ # 1. Validate tool exists
208
+ tool_def = self.registry.get_tool(tool_name)
209
+ if not tool_def:
210
+ raise ValueError(f"Unknown tool: {tool_name}")
211
+
212
+ handler = self.registry.get_handler(tool_name)
213
+ if not handler:
214
+ raise ValueError(f"No handler for tool: {tool_name}")
215
+
216
+ # 2. Validate parameters against schema
217
+ validated_params = self._validate_parameters(tool_def, parameters)
218
+
219
+ # 3. Confirmation gate
220
+ if tool_def.requires_confirmation and not skip_confirmation:
221
+ confirmed = await self.confirmation_provider.confirm(
222
+ tool_name=tool_name,
223
+ parameters=validated_params,
224
+ side_effects=tool_def.side_effects,
225
+ description=tool_def.description,
226
+ )
227
+ invocation.confirmation_given = confirmed
228
+ if not confirmed:
229
+ return ExecutionResult(
230
+ success=False,
231
+ error="User declined confirmation",
232
+ invocation_id=invocation_id,
233
+ duration_ms=int((time.monotonic() - start_time) * 1000),
234
+ )
235
+
236
+ # 4. Execute with timeout
237
+ exec_timeout = timeout or self.default_timeout
238
+ result = await self._execute_with_timeout(handler, validated_params, exec_timeout)
239
+
240
+ # 5. Normalize result
241
+ normalized = self._normalize_result(result)
242
+
243
+ invocation.success = True
244
+ invocation.result = normalized
245
+ invocation.completed_at = time.monotonic()
246
+
247
+ self._log_invocation(invocation)
248
+
249
+ self.on_progress(f"[OK] {tool_name} completed in {invocation.duration_ms}ms")
250
+
251
+ return ExecutionResult(
252
+ success=True,
253
+ result=normalized,
254
+ invocation_id=invocation_id,
255
+ duration_ms=invocation.duration_ms,
256
+ )
257
+
258
+ except Exception as e:
259
+ _log.error(f"Tool {tool_name} failed: {e}", exc_info=True)
260
+ invocation.success = False
261
+ invocation.error = str(e)
262
+ invocation.completed_at = time.monotonic()
263
+ self._log_invocation(invocation)
264
+
265
+ return ExecutionResult(
266
+ success=False,
267
+ error=str(e),
268
+ invocation_id=invocation_id,
269
+ duration_ms=int((time.monotonic() - start_time) * 1000),
270
+ )
271
+
272
+ def _validate_parameters(self, tool_def: ToolDefinition, parameters: dict) -> dict:
273
+ """Validate and coerce parameters against tool schema."""
274
+ validated = {}
275
+
276
+ for param in tool_def.parameters:
277
+ if param.name in parameters:
278
+ value = parameters[param.name]
279
+ validated[param.name] = self._coerce_value(value, param)
280
+ elif param.required:
281
+ raise ValueError(f"Missing required parameter: {param.name}")
282
+ elif param.default is not None:
283
+ validated[param.name] = param.default
284
+
285
+ # Check for unknown parameters
286
+ known_params = {p.name for p in tool_def.parameters}
287
+ unknown = set(parameters.keys()) - known_params
288
+ if unknown:
289
+ _log.warning(f"Tool {tool_def.name} received unknown parameters: {unknown}")
290
+
291
+ return validated
292
+
293
+ def _coerce_value(self, value: Any, param: ToolParameter) -> Any:
294
+ """Coerce a value to the expected type."""
295
+ if param.type == "string":
296
+ return str(value)
297
+ elif param.type == "integer":
298
+ return int(value)
299
+ elif param.type == "number":
300
+ return float(value)
301
+ elif param.type == "boolean":
302
+ if isinstance(value, str):
303
+ return value.lower() in ("true", "1", "yes", "on")
304
+ return bool(value)
305
+ elif param.type == "array":
306
+ if value is None:
307
+ return None
308
+ if isinstance(value, list):
309
+ return value
310
+ return [value]
311
+ elif param.type == "object":
312
+ if isinstance(value, dict):
313
+ return value
314
+ return {}
315
+ return value
316
+
317
+ async def _execute_with_timeout(
318
+ self,
319
+ handler: Callable,
320
+ parameters: dict,
321
+ timeout: float,
322
+ ) -> Any:
323
+ """Execute handler with timeout."""
324
+ # Add root to parameters if handler expects it
325
+ sig = inspect.signature(handler)
326
+ if "root" in sig.parameters:
327
+ parameters["root"] = self.root
328
+
329
+ # Check if handler is async
330
+ if inspect.iscoroutinefunction(handler):
331
+ return await asyncio.wait_for(handler(**parameters), timeout=timeout)
332
+ else:
333
+ # Run sync handlers directly in the event loop's thread.
334
+ #
335
+ # We deliberately do NOT use loop.run_in_executor here: many security
336
+ # agents call signal.signal() / rely on main-thread-only behavior, and
337
+ # tools like scan_vulnerabilities spawn their own thread pools — both
338
+ # break or deadlock when the handler runs in a worker thread (every
339
+ # agent raises -> the scan reports 0 findings). The agent loop is
340
+ # single-purpose, so blocking it briefly is acceptable; per-tool
341
+ # internal timeouts (e.g. realize's PATCHI_AGENT_TIMEOUT) still bound
342
+ # any individual tool that would otherwise hang.
343
+ return handler(**parameters)
344
+
345
+ def _normalize_result(self, result: Any) -> dict:
346
+ """Normalize tool result to a standard dict format."""
347
+ if isinstance(result, dict):
348
+ # Ensure success field
349
+ if "success" not in result:
350
+ result = {"success": True, "data": result}
351
+ return result
352
+ elif isinstance(result, (list, str, int, float, bool)):
353
+ return {"success": True, "data": result}
354
+ else:
355
+ return {"success": True, "data": str(result)}
356
+
357
+ def _log_invocation(self, invocation: ToolInvocation) -> None:
358
+ """Log invocation to audit trail."""
359
+ self.invocation_log.append(invocation)
360
+
361
+ # Write to JSONL file
362
+ try:
363
+ log_entry = {
364
+ "id": invocation.id,
365
+ "tool": invocation.tool_name,
366
+ "parameters": invocation.parameters,
367
+ "invoked_by": invocation.invoked_by,
368
+ "persona": invocation.persona_name,
369
+ "timestamp": invocation.timestamp,
370
+ "duration_ms": int((invocation.completed_at - invocation.started_at) * 1000),
371
+ "success": invocation.success,
372
+ "error": invocation.error,
373
+ "confirmed": invocation.confirmation_given,
374
+ }
375
+ with open(self._log_path, "a", encoding="utf-8") as f:
376
+ f.write(json.dumps(log_entry) + "\n")
377
+ except Exception as e:
378
+ _log.warning(f"Failed to write invocation log: {e}")
379
+
380
+ def get_invocation_history(self, limit: int = 100) -> list[ToolInvocation]:
381
+ """Get recent invocation history."""
382
+ return self.invocation_log[-limit:]
383
+
384
+ def get_tool_schemas(self, category: str = None) -> list[dict]:
385
+ """Get JSON schemas for tools (for AI consumption)."""
386
+ return self.registry.get_schemas(category)
387
+
388
+ def list_available_tools(self, category: str = None) -> list[dict]:
389
+ """List available tools with metadata."""
390
+ tools = self.registry.list_tools(category)
391
+ return [
392
+ {
393
+ "name": t.name,
394
+ "description": t.description,
395
+ "category": t.category,
396
+ "requires_confirmation": t.requires_confirmation,
397
+ "side_effects": t.side_effects,
398
+ }
399
+ for t in tools
400
+ ]
401
+
402
+ # ── Snapshot & Rollback ─────────────────────────────────────────────────
403
+
404
+ def create_snapshot(self, description: str = "") -> str:
405
+ """Create a snapshot of files that might be modified.
406
+
407
+ Snapshots key project files (config, source, etc.) so they can be
408
+ restored if a tool chain fails.
409
+
410
+ Returns:
411
+ Snapshot ID for later rollback.
412
+ """
413
+ snapshot_id = str(uuid.uuid4())[:8]
414
+ files: dict[str, bytes] = {}
415
+
416
+ # Snapshot common tool targets
417
+ patterns = [
418
+ "**/*.py",
419
+ "**/*.yaml",
420
+ "**/*.yml",
421
+ "**/*.json",
422
+ "**/*.toml",
423
+ "**/*.cfg",
424
+ "**/*.ini",
425
+ ]
426
+
427
+ for pattern in patterns:
428
+ for path in self.root.glob(pattern):
429
+ # Skip .patchi, .git, __pycache__, node_modules, venv
430
+ rel = path.relative_to(self.root)
431
+ parts = rel.parts
432
+ if any(
433
+ p.startswith(".") or p in ("__pycache__", "node_modules", ".venv", "venv")
434
+ for p in parts
435
+ ):
436
+ continue
437
+ try:
438
+ content = path.read_bytes()
439
+ # Only snapshot small files (< 100KB)
440
+ if len(content) < 100_000:
441
+ files[str(rel)] = content
442
+ except Exception:
443
+ pass
444
+
445
+ snapshot = StateSnapshot(id=snapshot_id, files=files)
446
+ self._snapshots[snapshot_id] = snapshot
447
+
448
+ # Persist to disk for cross-session rollback
449
+ snapshot_path = self._snapshot_dir / f"{snapshot_id}.json"
450
+ try:
451
+ import base64
452
+
453
+ data = {
454
+ "id": snapshot_id,
455
+ "description": description,
456
+ "created_at": snapshot.created_at,
457
+ "files": {k: base64.b64encode(v).decode() for k, v in files.items()},
458
+ }
459
+ snapshot_path.write_text(json.dumps(data), encoding="utf-8")
460
+ except Exception as e:
461
+ _log.warning(f"Failed to persist snapshot: {e}")
462
+
463
+ self.on_progress(f"[SNAP] Snapshot {snapshot_id} created ({len(files)} files)")
464
+ return snapshot_id
465
+
466
+ def rollback(self, snapshot_id: str) -> bool:
467
+ """Restore files from a snapshot.
468
+
469
+ Returns:
470
+ True if rollback succeeded.
471
+ """
472
+ snapshot = self._snapshots.get(snapshot_id)
473
+ if not snapshot:
474
+ # Try loading from disk
475
+ snapshot = self._load_snapshot(snapshot_id)
476
+
477
+ if not snapshot:
478
+ _log.error(f"Snapshot {snapshot_id} not found")
479
+ return False
480
+
481
+ restored = 0
482
+ for rel_path, content in snapshot.files.items():
483
+ full_path = self.root / rel_path
484
+ try:
485
+ full_path.parent.mkdir(parents=True, exist_ok=True)
486
+ full_path.write_bytes(content)
487
+ restored += 1
488
+ except Exception as e:
489
+ _log.warning(f"Failed to restore {rel_path}: {e}")
490
+
491
+ self.on_progress(f"↩️ Rollback {snapshot_id} restored {restored} files")
492
+ _log.info(f"Rollback {snapshot_id}: restored {restored}/{len(snapshot.files)} files")
493
+ return True
494
+
495
+ def _load_snapshot(self, snapshot_id: str) -> StateSnapshot | None:
496
+ """Load a snapshot from disk."""
497
+ snapshot_path = self._snapshot_dir / f"{snapshot_id}.json"
498
+ if not snapshot_path.exists():
499
+ return None
500
+
501
+ try:
502
+ import base64
503
+
504
+ data = json.loads(snapshot_path.read_text(encoding="utf-8"))
505
+ files = {k: base64.b64decode(v) for k, v in data["files"].items()}
506
+ return StateSnapshot(
507
+ id=data["id"],
508
+ files=files,
509
+ created_at=data["created_at"],
510
+ )
511
+ except Exception as e:
512
+ _log.warning(f"Failed to load snapshot {snapshot_id}: {e}")
513
+ return None
514
+
515
+ async def execute_with_rollback(
516
+ self,
517
+ tool_name: str,
518
+ parameters: dict,
519
+ invoked_by: str = "user",
520
+ persona_name: str | None = None,
521
+ ) -> ExecutionResult:
522
+ """Execute a tool with automatic snapshot and rollback on failure.
523
+
524
+ Creates a state snapshot before execution, and rolls back if the
525
+ tool fails.
526
+ """
527
+ # Create snapshot before execution
528
+ snapshot_id = self.create_snapshot(f"pre-{tool_name}")
529
+
530
+ # Execute the tool
531
+ result = await self.execute(tool_name, parameters, invoked_by, persona_name)
532
+
533
+ # Rollback on failure
534
+ if not result.success:
535
+ self.on_progress(f"⚠️ {tool_name} failed, rolling back...")
536
+ self.rollback(snapshot_id)
537
+ else:
538
+ # Clean up old snapshot on success
539
+ self._cleanup_snapshot(snapshot_id)
540
+
541
+ return result
542
+
543
+ def _cleanup_snapshot(self, snapshot_id: str) -> None:
544
+ """Remove a snapshot after successful use."""
545
+ self._snapshots.pop(snapshot_id, None)
546
+ snapshot_path = self._snapshot_dir / f"{snapshot_id}.json"
547
+ try:
548
+ snapshot_path.unlink(missing_ok=True)
549
+ except Exception:
550
+ pass
551
+
552
+ async def execute_chain(
553
+ self,
554
+ steps: list[dict],
555
+ invoked_by: str = "user",
556
+ stop_on_failure: bool = True,
557
+ ) -> list[ExecutionResult]:
558
+ """Execute a chain of tools with rollback on failure.
559
+
560
+ Args:
561
+ steps: List of {"tool": name, "parameters": {...}} dicts
562
+ invoked_by: Who is invoking the chain
563
+ stop_on_failure: If True, stop and rollback on first failure
564
+
565
+ Returns:
566
+ List of ExecutionResult for each step
567
+ """
568
+ results = []
569
+ snapshot_id = self.create_snapshot("chain-pre-execution")
570
+
571
+ for i, step in enumerate(steps):
572
+ tool_name = step.get("tool")
573
+ parameters = step.get("parameters", {})
574
+
575
+ self.on_progress(f"[CHAIN] Chain step {i + 1}/{len(steps)}: {tool_name}")
576
+
577
+ result = await self.execute(tool_name, parameters, invoked_by)
578
+ results.append(result)
579
+
580
+ if not result.success and stop_on_failure:
581
+ self.on_progress(f"[FAIL] Chain failed at step {i + 1}, rolling back...")
582
+ self.rollback(snapshot_id)
583
+ # Mark remaining steps as skipped
584
+ for _j in range(i + 1, len(steps)):
585
+ results.append(
586
+ ExecutionResult(
587
+ success=False,
588
+ error=f"Skipped: chain stopped at step {i + 1}",
589
+ )
590
+ )
591
+ break
592
+
593
+ # Clean up snapshot if chain succeeded
594
+ all_success = all(r.success for r in results)
595
+ if all_success:
596
+ self._cleanup_snapshot(snapshot_id)
597
+
598
+ return results
599
+
600
+
601
+ # Convenience function for synchronous execution (for non-async contexts)
602
+ def execute_tool_sync(
603
+ root: Path,
604
+ tool_name: str,
605
+ parameters: dict,
606
+ invoked_by: str = "cli",
607
+ timeout: float = 120.0,
608
+ ) -> ExecutionResult:
609
+ """Synchronous tool execution for CLI use."""
610
+ executor = ToolExecutor(root)
611
+
612
+ # Run async executor in event loop
613
+ try:
614
+ loop = asyncio.get_event_loop()
615
+ except RuntimeError:
616
+ loop = asyncio.new_event_loop()
617
+ asyncio.set_event_loop(loop)
618
+
619
+ return loop.run_until_complete(
620
+ executor.execute(tool_name, parameters, invoked_by, timeout=timeout)
621
+ )