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,625 @@
1
+ """
2
+ Patchi Orchestrator — The central brain that turns p chat into an orchestra.
3
+
4
+ The chat IS the brain. It uses LLM-powered reasoning to:
5
+ 1. Understand natural language requests (no keyword matching)
6
+ 2. Decide what tools/agents to use
7
+ 3. Execute them with full control
8
+ 4. Report results with context
9
+
10
+ The orchestrator gives the chat direct access to:
11
+ - Read/write any file in the project
12
+ - Run any CLI command
13
+ - Spawn any agent (security, testing, fixing, etc.)
14
+ - Access the full brain/memory state
15
+ - Make decisions based on context
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import asyncio
21
+ import json
22
+ import logging
23
+ import subprocess
24
+ import time
25
+ from collections.abc import Callable
26
+ from datetime import UTC, datetime
27
+ from enum import StrEnum
28
+ from pathlib import Path
29
+ from typing import Any
30
+
31
+ from pydantic import BaseModel, Field
32
+
33
+ _log = logging.getLogger("patchi.ai.orchestrator")
34
+
35
+
36
+ # ── Pydantic Models ───────────────────────────────────────────────────
37
+
38
+ class ToolCall(BaseModel):
39
+ """A single tool invocation with validated parameters."""
40
+ tool: str
41
+ params: dict[str, Any] = Field(default_factory=dict)
42
+ reason: str = ""
43
+ timeout_seconds: float = 120.0
44
+
45
+
46
+ class StepResult(BaseModel):
47
+ """Result of executing a single step."""
48
+ tool: str
49
+ success: bool
50
+ result: Any = None
51
+ error: str | None = None
52
+ duration_ms: int = 0
53
+ output_summary: str = ""
54
+
55
+
56
+ class Plan(BaseModel):
57
+ """A multi-step execution plan."""
58
+ goal: str
59
+ steps: list[ToolCall] = Field(default_factory=list)
60
+ reasoning: str = "" # LLM's reasoning for why these steps
61
+ created_at: str = Field(default_factory=lambda: datetime.now(UTC).isoformat())
62
+
63
+
64
+ class PlanResult(BaseModel):
65
+ """Complete result of executing a plan."""
66
+ plan: Plan
67
+ steps: list[StepResult] = Field(default_factory=list)
68
+ success: bool = False
69
+ total_duration_ms: int = 0
70
+ findings_count: int = 0
71
+ summary: str = ""
72
+
73
+
74
+ class IntentType(StrEnum):
75
+ """Types of intent — but this is just for categorization, not routing."""
76
+ CHAT = "chat" # General conversation
77
+ ACTION = "action" # User wants to DO something
78
+ QUESTION = "question" # User wants to KNOW something
79
+ COMPLEX = "complex" # Multi-step task
80
+
81
+
82
+ class Intent(BaseModel):
83
+ """Parsed user intent — determined by LLM, not keywords."""
84
+ type: IntentType
85
+ goal: str # What the user wants to achieve
86
+ needs_tools: bool = False # Whether tools are needed
87
+ needs_files: bool = False # Whether file access is needed
88
+ needs_agents: bool = False # Whether agent spawning is needed
89
+ context: dict[str, Any] = Field(default_factory=dict) # Additional context
90
+
91
+
92
+ # ── Brain: The Core Intelligence ──────────────────────────────────────
93
+
94
+ class Brain:
95
+ """
96
+ The Brain — direct access to everything.
97
+
98
+ This is what makes the chat the central orchestrator.
99
+ It can read files, run commands, spawn agents, and make decisions.
100
+ """
101
+
102
+ def __init__(self, root: Path, on_progress: Callable[[str], None] | None = None):
103
+ self.root = root
104
+ self.on_progress = on_progress or (lambda _: None)
105
+
106
+ # ── File Operations ───────────────────────────────────────────────
107
+
108
+ def read_file(self, path: str) -> str:
109
+ """Read a file's contents."""
110
+ target = Path(path)
111
+ if not target.is_absolute():
112
+ target = self.root / target
113
+ try:
114
+ return target.read_text(encoding="utf-8")
115
+ except Exception as e:
116
+ return f"ERROR: Could not read {path}: {e}"
117
+
118
+ def write_file(self, path: str, content: str) -> dict:
119
+ """Write content to a file."""
120
+ target = Path(path)
121
+ if not target.is_absolute():
122
+ target = self.root / target
123
+ try:
124
+ target.parent.mkdir(parents=True, exist_ok=True)
125
+ target.write_text(content, encoding="utf-8")
126
+ return {"success": True, "path": str(target), "size": len(content)}
127
+ except Exception as e:
128
+ return {"success": False, "error": str(e)}
129
+
130
+ def list_files(self, pattern: str = "**/*.py", max_files: int = 100) -> list[str]:
131
+ """List files matching a pattern."""
132
+ files = []
133
+ for f in self.root.glob(pattern):
134
+ if f.is_file() and ".patchi" not in str(f) and "node_modules" not in str(f):
135
+ files.append(str(f.relative_to(self.root)))
136
+ if len(files) >= max_files:
137
+ break
138
+ return files
139
+
140
+ # ── Command Execution ─────────────────────────────────────────────
141
+
142
+ def run_command(self, command: str, timeout: int = 60) -> dict:
143
+ """Run a shell command and return the result."""
144
+ try:
145
+ result = subprocess.run(
146
+ command,
147
+ shell=True,
148
+ cwd=str(self.root),
149
+ capture_output=True,
150
+ text=True,
151
+ timeout=timeout,
152
+ )
153
+ return {
154
+ "success": result.returncode == 0,
155
+ "stdout": result.stdout,
156
+ "stderr": result.stderr,
157
+ "returncode": result.returncode,
158
+ }
159
+ except subprocess.TimeoutExpired:
160
+ return {"success": False, "error": f"Command timed out after {timeout}s"}
161
+ except Exception as e:
162
+ return {"success": False, "error": str(e)}
163
+
164
+ # ── Agent Spawning ────────────────────────────────────────────────
165
+
166
+ def spawn_agent(self, agent_type: str, **kwargs) -> dict:
167
+ """Spawn a specialized agent for a specific task."""
168
+ self.on_progress(f"Spawning {agent_type} agent...")
169
+
170
+ try:
171
+ if agent_type == "security":
172
+ from patchi.core.security.orchestrator import SecurityOrchestrator
173
+ orchestrator = SecurityOrchestrator(self.root)
174
+ result = orchestrator.run()
175
+ return {"success": True, "result": result}
176
+
177
+ elif agent_type == "scanner":
178
+ from patchi.core.scanner import scan_project
179
+ result = scan_project(self.root)
180
+ return {"success": True, "result": result}
181
+
182
+ elif agent_type == "proactive":
183
+ from patchi.core.brain.proactive import run_proactive
184
+ files = kwargs.get("files", [])
185
+ apply = kwargs.get("apply", False)
186
+ result = run_proactive(self.root, files, apply=apply)
187
+ return {"success": True, "result": result}
188
+
189
+ elif agent_type == "council":
190
+ from patchi.core.brain.council import Council
191
+
192
+ async def _deliberate():
193
+ council = Council(self.root, on_progress=self.on_progress)
194
+ session = await council.deliberate(kwargs.get("goal", ""), {})
195
+ return {
196
+ "synthesis": session.synthesis,
197
+ "action_plan": [
198
+ {"tool": s.get("tool", ""), "params": s.get("parameters", {})}
199
+ for s in (session.action_plan or [])
200
+ ],
201
+ "consensus": session.consensus_reached,
202
+ "personas": [d.persona_name for d in (session.persona_decisions or [])],
203
+ }
204
+
205
+ return asyncio.run(_deliberate())
206
+
207
+ elif agent_type == "smart":
208
+ from patchi.core.ai.smart import run_smart_agent
209
+ result = run_smart_agent(
210
+ self.root,
211
+ kwargs.get("goal", ""),
212
+ max_steps=kwargs.get("max_steps", 6),
213
+ )
214
+ return {"success": True, "result": result}
215
+
216
+ else:
217
+ return {"success": False, "error": f"Unknown agent type: {agent_type}"}
218
+
219
+ except Exception as e:
220
+ return {"success": False, "error": str(e)}
221
+
222
+ # ── Brain Access ──────────────────────────────────────────────────
223
+
224
+ def get_brain_state(self) -> dict:
225
+ """Get the current brain/memory state."""
226
+ from patchi.core import memory as mem
227
+ return mem.get_brain(self.root)
228
+
229
+ def get_findings(self, severity: str | None = None) -> list[dict]:
230
+ """Get security findings."""
231
+ from patchi.core import memory as mem
232
+ results = mem.get_scan_results(self.root)
233
+ findings = []
234
+ for agent_name, data in results.items():
235
+ for f in data.get("findings", []):
236
+ f["agent"] = agent_name
237
+ if severity and f.get("severity") != severity:
238
+ continue
239
+ findings.append(f)
240
+ return findings
241
+
242
+ def get_layers(self) -> dict:
243
+ """Get the layered brain data."""
244
+ from patchi.core import memory as mem
245
+ return mem.get_layers(self.root)
246
+
247
+ # ── Tool Execution ────────────────────────────────────────────────
248
+
249
+ def execute_tool(self, tool_name: str, params: dict) -> StepResult:
250
+ """Execute a tool via the ToolExecutor."""
251
+ start = time.monotonic()
252
+ self.on_progress(f"Executing {tool_name}...")
253
+
254
+ try:
255
+ from patchi.core.ai.tools.executor import CLIConfirmationProvider, ToolExecutor
256
+
257
+ async def _run():
258
+ executor = ToolExecutor(
259
+ self.root,
260
+ confirmation_provider=CLIConfirmationProvider(auto_confirm=True),
261
+ on_progress=self.on_progress,
262
+ )
263
+ return await executor.execute(tool_name, params, invoked_by="brain", skip_confirmation=True)
264
+
265
+ result = asyncio.run(_run())
266
+ duration = int((time.monotonic() - start) * 1000)
267
+
268
+ return StepResult(
269
+ tool=tool_name,
270
+ success=getattr(result, "success", False),
271
+ result=getattr(result, "result", None),
272
+ error=getattr(result, "error", None),
273
+ duration_ms=duration,
274
+ output_summary=self._summarize(tool_name, getattr(result, "result", None)),
275
+ )
276
+ except Exception as e:
277
+ duration = int((time.monotonic() - start) * 1000)
278
+ return StepResult(
279
+ tool=tool_name,
280
+ success=False,
281
+ error=str(e),
282
+ duration_ms=duration,
283
+ output_summary=f"ERROR: {e}",
284
+ )
285
+
286
+ def _summarize(self, tool: str, data: Any) -> str:
287
+ """Summarize tool output for display."""
288
+ if not isinstance(data, dict):
289
+ return str(data)[:200] if data else "Done"
290
+
291
+ if tool == "scan_vulnerabilities":
292
+ sev = data.get("by_severity", {})
293
+ return f"{data.get('agent_count', 0)} agents, {data.get('total_findings', 0)} findings (C={sev.get('critical', 0)} H={sev.get('high', 0)} M={sev.get('medium', 0)})"
294
+ elif tool == "run_tests":
295
+ return f"passed={data.get('passed', 0)} failed={data.get('failed', 0)}"
296
+ elif tool == "attack_simulate":
297
+ return f"{data.get('total_findings', 0)} offensive findings"
298
+ elif tool == "stress_test":
299
+ return f"{data.get('requests', 0)} reqs @ {data.get('rps', 0)} rps"
300
+ elif tool == "analyze_project":
301
+ return f"{data.get('file_count', 0)} files, {data.get('route_count', 0)} routes"
302
+ elif tool == "write_file":
303
+ return f"Wrote {data.get('size', 0)} bytes to {data.get('path', '?')}"
304
+ return (data.get("message") or data.get("error") or str(data))[:200]
305
+
306
+
307
+ # ── LLM-Powered Intent Parser ─────────────────────────────────────────
308
+
309
+ def parse_intent_llm(message: str, brain: Brain, config: dict) -> Intent:
310
+ """
311
+ Use the LLM to parse natural language into structured intent.
312
+
313
+ This is NOT keyword matching — the LLM understands context and nuance.
314
+ """
315
+ from patchi.core.ai.client import call_ai
316
+
317
+ # Get brain context for the LLM
318
+ brain.get_brain_state()
319
+ findings = brain.get_findings()[:10] # Top 10 findings
320
+ files = brain.list_files(max_files=20)
321
+
322
+ # Build the parsing prompt
323
+ system_prompt = """You are Patchi's intent parser. Analyze the user's message and determine:
324
+
325
+ 1. intent_type: "chat" (general conversation), "action" (user wants to DO something), "question" (user wants to KNOW something), or "complex" (multi-step task)
326
+ 2. goal: What the user wants to achieve (one sentence)
327
+ 3. needs_tools: Whether tool execution is needed (scan, test, fix, etc.)
328
+ 4. needs_files: Whether file reading/writing is needed
329
+ 5. needs_agents: Whether agent spawning is needed (security, council, etc.)
330
+ 6. context: Any relevant context (file paths, severity, etc.)
331
+
332
+ Respond with ONLY a JSON object, no other text."""
333
+
334
+ user_prompt = f"""User message: "{message}"
335
+
336
+ Project context:
337
+ - {len(files)} files in project
338
+ - {len(findings)} security findings
339
+ - Brain state available
340
+
341
+ Respond with JSON:
342
+ {{
343
+ "intent_type": "action|question|chat|complex",
344
+ "goal": "what the user wants",
345
+ "needs_tools": true/false,
346
+ "needs_files": true/false,
347
+ "needs_agents": true/false,
348
+ "context": {{}}
349
+ }}"""
350
+
351
+ response = call_ai(config, system_prompt, user_prompt)
352
+
353
+ if not response:
354
+ # Fallback to keyword-based parsing if LLM unavailable
355
+ return _parse_intent_fallback(message)
356
+
357
+ try:
358
+ # Parse LLM response
359
+ # Handle markdown code blocks
360
+ cleaned = response.strip()
361
+ if cleaned.startswith("```"):
362
+ lines = cleaned.split("\n")
363
+ cleaned = "\n".join(lines[1:-1])
364
+ if cleaned.startswith("```json"):
365
+ cleaned = cleaned[7:]
366
+
367
+ data = json.loads(cleaned)
368
+ return Intent(
369
+ type=IntentType(data.get("intent_type", "chat")),
370
+ goal=data.get("goal", message),
371
+ needs_tools=data.get("needs_tools", False),
372
+ needs_files=data.get("needs_files", False),
373
+ needs_agents=data.get("needs_agents", False),
374
+ context=data.get("context", {}),
375
+ )
376
+ except Exception as e:
377
+ _log.info("LLM intent parsing failed: %s, using fallback", e)
378
+ return _parse_intent_fallback(message)
379
+
380
+
381
+ def _parse_intent_fallback(message: str) -> Intent:
382
+ """Simple fallback intent parsing when LLM is unavailable."""
383
+ msg = message.lower().strip()
384
+
385
+ # Action words
386
+ action_words = ["scan", "test", "fix", "run", "execute", "build", "create", "write", "generate", "attack", "stress"]
387
+ is_action = any(w in msg for w in action_words)
388
+
389
+ # Question words
390
+ question_words = ["what", "how", "why", "where", "which", "who", "when", "is", "are", "does", "do"]
391
+ is_question = any(msg.startswith(w) for w in question_words)
392
+
393
+ # Multi-step indicators
394
+ multi_step_words = ["and then", "also", "plus", "after that", "first.*then"]
395
+ is_complex = any(w in msg for w in multi_step_words)
396
+
397
+ if is_complex:
398
+ return Intent(type=IntentType.COMPLEX, goal=message, needs_tools=True)
399
+ elif is_action:
400
+ return Intent(type=IntentType.ACTION, goal=message, needs_tools=True)
401
+ elif is_question:
402
+ return Intent(type=IntentType.QUESTION, goal=message, needs_files=True)
403
+ else:
404
+ return Intent(type=IntentType.CHAT, goal=message)
405
+
406
+
407
+ # ── LLM-Powered Planner ───────────────────────────────────────────────
408
+
409
+ def plan_with_llm(intent: Intent, brain: Brain, config: dict) -> Plan:
410
+ """
411
+ Use the LLM to create an execution plan.
412
+
413
+ The LLM decides which tools to use based on the intent and context.
414
+ """
415
+ from patchi.core.ai.client import call_ai
416
+
417
+ # Get available tools
418
+ try:
419
+ from patchi.core.ai.tools.registry import get_tool_registry
420
+ registry = get_tool_registry()
421
+ tools = [t.name for t in registry.list_tools()]
422
+ except Exception:
423
+ tools = []
424
+
425
+ # Get brain context
426
+ brain_state = brain.get_brain_state()
427
+ findings = brain.get_findings()[:5]
428
+
429
+ system_prompt = f"""You are Patchi's planner. Given a user goal, create an execution plan.
430
+
431
+ Available tools: {', '.join(tools)}
432
+
433
+ For each step, specify:
434
+ - tool: tool name from the available list
435
+ - params: parameters for the tool
436
+ - reason: why this step is needed
437
+
438
+ Respond with ONLY a JSON object:
439
+ {{
440
+ "reasoning": "why these steps achieve the goal",
441
+ "steps": [
442
+ {{"tool": "tool_name", "params": {{}}, "reason": "why"}}
443
+ ]
444
+ }}"""
445
+
446
+ user_prompt = f"""Goal: {intent.goal}
447
+
448
+ Context:
449
+ - {len(findings)} security findings
450
+ - Brain state: {json.dumps(brain_state, default=str)[:500]}
451
+
452
+ Create a plan to achieve this goal."""
453
+
454
+ response = call_ai(config, system_prompt, user_prompt)
455
+
456
+ if not response:
457
+ # Fallback to default plan
458
+ return _plan_fallback(intent)
459
+
460
+ try:
461
+ cleaned = response.strip()
462
+ if cleaned.startswith("```"):
463
+ lines = cleaned.split("\n")
464
+ cleaned = "\n".join(lines[1:-1])
465
+ if cleaned.startswith("```json"):
466
+ cleaned = cleaned[7:]
467
+
468
+ data = json.loads(cleaned)
469
+ steps = [
470
+ ToolCall(
471
+ tool=s["tool"],
472
+ params=s.get("params", {}),
473
+ reason=s.get("reason", ""),
474
+ )
475
+ for s in data.get("steps", [])
476
+ if s.get("tool") in tools # Only valid tools
477
+ ]
478
+ return Plan(
479
+ goal=intent.goal,
480
+ steps=steps,
481
+ reasoning=data.get("reasoning", ""),
482
+ )
483
+ except Exception as e:
484
+ _log.info("LLM planning failed: %s, using fallback", e)
485
+ return _plan_fallback(intent)
486
+
487
+
488
+ def _plan_fallback(intent: Intent) -> Plan:
489
+ """Simple fallback plan when LLM is unavailable."""
490
+ steps = []
491
+
492
+ if intent.needs_tools:
493
+ # Default to scan + test
494
+ steps.append(ToolCall(tool="scan_vulnerabilities", params={"domains": None}, reason="Security scan"))
495
+ steps.append(ToolCall(tool="run_tests", params={"test_types": ["unit"]}, reason="Run tests"))
496
+
497
+ if intent.needs_files:
498
+ # Just analyze the project
499
+ steps.append(ToolCall(tool="analyze_project", params={}, reason="Analyze project"))
500
+
501
+ if not steps:
502
+ steps.append(ToolCall(tool="analyze_project", params={}, reason="Analyze project"))
503
+
504
+ return Plan(goal=intent.goal, steps=steps, reasoning="Fallback plan")
505
+
506
+
507
+ # ── Orchestrator ───────────────────────────────────────────────────────
508
+
509
+ class Orchestrator:
510
+ """
511
+ The central brain of Patchi.
512
+
513
+ The chat IS the orchestrator. It uses LLM reasoning to understand
514
+ requests and has direct control over everything.
515
+ """
516
+
517
+ def __init__(
518
+ self,
519
+ root: Path,
520
+ on_progress: Callable[[str], None] | None = None,
521
+ on_event: Callable[[dict], None] | None = None,
522
+ ):
523
+ self.root = root
524
+ self.on_progress = on_progress or (lambda _: None)
525
+ self.on_event = on_event or (lambda _: None)
526
+ self.brain = Brain(root, on_progress=on_progress)
527
+ self._config: dict | None = None
528
+
529
+ @property
530
+ def config(self) -> dict:
531
+ """Lazy-load config."""
532
+ if self._config is None:
533
+ from patchi.core import config as cfg
534
+ self._config = cfg.load(self.root)
535
+ return self._config
536
+
537
+ async def run(self, message: str, max_steps: int = 10) -> PlanResult:
538
+ """
539
+ Process a natural language message using LLM-powered reasoning.
540
+
541
+ This is the main entry point. The LLM decides:
542
+ 1. What the user wants (intent parsing)
543
+ 2. What tools to use (planning)
544
+ 3. How to execute them (orchestration)
545
+ """
546
+ start = time.monotonic()
547
+
548
+ # Step 1: Parse intent with LLM
549
+ self.on_progress("Understanding your request...")
550
+ intent = parse_intent_llm(message, self.brain, self.config)
551
+
552
+ # Step 2: Plan with LLM
553
+ self.on_progress("Planning execution...")
554
+ plan = plan_with_llm(intent, self.brain, self.config)
555
+ plan.steps = plan.steps[:max_steps]
556
+
557
+ self.on_event({
558
+ "event": "orchestrator.planned",
559
+ "data": {
560
+ "goal": intent.goal,
561
+ "steps": len(plan.steps),
562
+ "reasoning": plan.reasoning,
563
+ },
564
+ })
565
+
566
+ # Step 3: Execute plan
567
+ plan_result = PlanResult(plan=plan)
568
+ for i, step in enumerate(plan.steps):
569
+ self.on_progress(f"Step {i+1}/{len(plan.steps)}: {step.tool}")
570
+ result = self.brain.execute_tool(step.tool, step.params)
571
+ plan_result.steps.append(result)
572
+
573
+ if result.tool == "scan_vulnerabilities" and result.result:
574
+ plan_result.findings_count += int(result.result.get("total_findings", 0) or 0)
575
+
576
+ # Step 4: Summarize
577
+ plan_result.total_duration_ms = int((time.monotonic() - start) * 1000)
578
+ successes = sum(1 for s in plan_result.steps if s.success)
579
+ plan_result.success = successes == len(plan_result.steps)
580
+ plan_result.summary = f"{successes}/{len(plan_result.steps)} steps succeeded"
581
+
582
+ self.on_event({
583
+ "event": "orchestrator.completed",
584
+ "data": {
585
+ "goal": intent.goal,
586
+ "steps": len(plan_result.steps),
587
+ "success": plan_result.success,
588
+ "findings": plan_result.findings_count,
589
+ },
590
+ })
591
+
592
+ return plan_result
593
+
594
+ def read_file(self, path: str) -> str:
595
+ """Direct file access from chat."""
596
+ return self.brain.read_file(path)
597
+
598
+ def write_file(self, path: str, content: str) -> dict:
599
+ """Direct file writing from chat."""
600
+ return self.brain.write_file(path, content)
601
+
602
+ def run_command(self, command: str, timeout: int = 60) -> dict:
603
+ """Direct command execution from chat."""
604
+ return self.brain.run_command(command, timeout)
605
+
606
+ def spawn_agent(self, agent_type: str, **kwargs) -> dict:
607
+ """Direct agent spawning from chat."""
608
+ return self.brain.spawn_agent(agent_type, **kwargs)
609
+
610
+ def get_findings(self, severity: str | None = None) -> list[dict]:
611
+ """Get security findings."""
612
+ return self.brain.get_findings(severity)
613
+
614
+ def get_brain_state(self) -> dict:
615
+ """Get brain state."""
616
+ return self.brain.get_brain_state()
617
+
618
+ def get_tools(self) -> list[dict]:
619
+ """Get all available tools."""
620
+ try:
621
+ from patchi.core.ai.tools.registry import get_tool_registry
622
+ registry = get_tool_registry()
623
+ return [t.to_schema() for t in registry.list_tools()]
624
+ except Exception:
625
+ return []