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,260 @@
1
+ """
2
+ VisualRegressionAgent — Screenshot comparison testing via Playwright.
3
+
4
+ Takes screenshots at multiple viewports, waits for full render, and compares
5
+ them against stored baselines:
6
+
7
+ - Baselines stored in ``.patchi/visual_baselines/{route}/``
8
+ - Current shots saved to ``.patchi/evidence/screenshots/visual_regression/``
9
+ - On change, a **pixel-diff overlay** (red boxes around changed regions) is
10
+ generated so reviewers can see *what* moved, not just that it did.
11
+ - HTTP status (>=400) and console/page errors are surfaced as findings.
12
+ - Every finding carries the screenshot path so the UI can display evidence.
13
+
14
+ Requires: playwright, Pillow
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import logging
20
+ from pathlib import Path
21
+
22
+ from ..agents.base import (
23
+ AgentDomain,
24
+ AgentGroup,
25
+ AgentInput,
26
+ AgentResult,
27
+ BaseAgent,
28
+ Severity,
29
+ make_finding,
30
+ register,
31
+ )
32
+ from ._browser import (
33
+ discover_routes,
34
+ find_server,
35
+ open_page,
36
+ save_screenshot,
37
+ )
38
+
39
+ VIEWPORTS = [
40
+ {"width": 1440, "height": 900, "label": "desktop"},
41
+ {"width": 768, "height": 1024, "label": "tablet"},
42
+ {"width": 375, "height": 812, "label": "mobile"},
43
+ ]
44
+
45
+ _log = logging.getLogger("patchi.testing.visual_regression_agent")
46
+
47
+
48
+ @register
49
+ class VisualRegressionAgent(BaseAgent):
50
+ """Screenshot diff testing: baseline comparison, layout shifts, visual regressions."""
51
+
52
+ group = AgentGroup.TEST
53
+ domain = AgentDomain.VISUAL
54
+ name = "VisualRegressionAgent"
55
+ description = "Screenshot diff testing: baseline comparison, layout shifts, visual regressions"
56
+ timeout = 240
57
+
58
+ def _run(self, inp: AgentInput, result: AgentResult) -> None:
59
+ try:
60
+ from playwright.sync_api import sync_playwright # noqa: F401
61
+ except ImportError:
62
+ self.skip(result, "playwright not installed — pip install playwright && playwright install chromium")
63
+ return
64
+
65
+ base_url = find_server(inp.root, inp.config, inp.extra)
66
+ if not base_url:
67
+ self.skip(result, "no running server found — start `p web` (default :1612)")
68
+ return
69
+
70
+ routes = discover_routes(inp.config, inp.extra)
71
+ if not routes:
72
+ self.skip(result, "no routes discovered")
73
+ return
74
+
75
+ baseline_root = inp.root / ".patchi" / "visual_baselines"
76
+ evidence_dir = inp.root / ".patchi" / "evidence" / "screenshots" / "visual_regression"
77
+ evidence_dir.mkdir(parents=True, exist_ok=True)
78
+
79
+ shots = 0
80
+ regressions = 0
81
+ new_baselines = 0
82
+ error_pages = 0
83
+
84
+ with sync_playwright() as pw:
85
+ browser = pw.chromium.launch(headless=True)
86
+ for route in routes[:15]:
87
+ for vp in VIEWPORTS:
88
+ browser.new_context(viewport={"width": vp["width"], "height": vp["height"]})
89
+ page_session = open_page(browser, f"{base_url}{route}")
90
+ page = page_session.page
91
+ try:
92
+ slug = route.strip("/").replace("/", "_") or "root"
93
+ shot_name = f"{slug}_{vp['label']}"
94
+ shot_path = save_screenshot(page, evidence_dir, shot_name)
95
+ if shot_path is None:
96
+ continue
97
+ rel = str(shot_path.relative_to(inp.root))
98
+ shots += 1
99
+
100
+ # Surface server / runtime errors as findings + annotation
101
+ if page_session.status and page_session.status >= 400:
102
+ error_pages += 1
103
+ result.add_finding(
104
+ make_finding(
105
+ agent=self.name,
106
+ finding_type="page_error",
107
+ severity=Severity.HIGH,
108
+ file=route,
109
+ message=f"HTTP {page_session.status} on {route} ({vp['label']})",
110
+ extra={"viewport": vp["label"], "status": page_session.status,
111
+ "screenshot": rel},
112
+ )
113
+ )
114
+ for ce in page_session.console_errors[:5]:
115
+ result.add_finding(
116
+ make_finding(
117
+ agent=self.name,
118
+ finding_type="console_error",
119
+ severity=Severity.MEDIUM,
120
+ file=route,
121
+ message=f"Console error on {route} ({vp['label']}): {ce[:160]}",
122
+ extra={"viewport": vp["label"], "screenshot": rel},
123
+ )
124
+ )
125
+
126
+ # Baseline compare
127
+ base_shot = baseline_root / slug / f"{vp['label']}.png"
128
+ if not base_shot.exists():
129
+ base_shot.parent.mkdir(parents=True, exist_ok=True)
130
+ page.screenshot(path=str(base_shot), full_page=True)
131
+ new_baselines += 1
132
+ result.add_finding(
133
+ make_finding(
134
+ agent=self.name,
135
+ finding_type="new_baseline",
136
+ severity=Severity.INFO,
137
+ file=route,
138
+ message=f"New baseline created: {slug} ({vp['label']})",
139
+ extra={"viewport": vp["label"], "screenshot": rel,
140
+ "baseline": str(base_shot.relative_to(inp.root))},
141
+ )
142
+ )
143
+ else:
144
+ diff_path, changed = self._diff(base_shot, shot_path, evidence_dir, slug, vp["label"])
145
+ if changed:
146
+ regressions += 1
147
+ result.add_finding(
148
+ make_finding(
149
+ agent=self.name,
150
+ finding_type="visual_regression",
151
+ severity=Severity.MEDIUM,
152
+ file=route,
153
+ message=f"Visual change on {route} ({vp['label']}): {len(changed)} region(s) changed",
154
+ detail=f"Baseline: {base_shot.name}\nCurrent: {shot_path.name}",
155
+ extra={"viewport": vp["label"], "screenshot": rel,
156
+ "baseline": str(base_shot.relative_to(inp.root)),
157
+ "diff": str(diff_path.relative_to(inp.root)) if diff_path else None,
158
+ "regions": len(changed)},
159
+ )
160
+ )
161
+ # refresh baseline so next run compares against now-current
162
+ page.screenshot(path=str(base_shot), full_page=True)
163
+ finally:
164
+ page.close()
165
+ browser.close()
166
+
167
+ if regressions == 0 and error_pages == 0 and shots > 0:
168
+ result.add_finding(
169
+ make_finding(
170
+ agent=self.name,
171
+ finding_type="visual_summary",
172
+ severity=Severity.INFO,
173
+ file="(all pages)",
174
+ message=f"Visual regression PASSED: {shots} screenshots, 0 regressions, {new_baselines} new baselines",
175
+ )
176
+ )
177
+ elif shots > 0:
178
+ result.add_finding(
179
+ make_finding(
180
+ agent=self.name,
181
+ finding_type="visual_summary",
182
+ severity=Severity.MEDIUM,
183
+ file="(all pages)",
184
+ message=f"Visual regression: {regressions} changes, {error_pages} error pages across {shots} screenshots",
185
+ )
186
+ )
187
+
188
+ result.data["suite"] = {
189
+ "runner": "visual_regression",
190
+ "screenshots": shots,
191
+ "regressions": regressions,
192
+ "new_baselines": new_baselines,
193
+ "error_pages": error_pages,
194
+ }
195
+ result.files_scanned = shots
196
+
197
+ # ── pixel diff ──────────────────────────────────────────────────────────
198
+
199
+ def _diff(self, base_path: Path, curr_path: Path, out_dir: Path, slug: str, label: str):
200
+ """Return (diff_image_path, changed_regions) using a downscaled pixel diff.
201
+
202
+ Requires Pillow. If Pillow is unavailable, falls back to a file-size /
203
+ hash comparison and reports diffing as unavailable (no crash).
204
+ """
205
+ try:
206
+ from PIL import Image, ImageDraw
207
+ except Exception:
208
+ _log.warning("VisualRegressionAgent: Pillow not installed — pixel diff disabled")
209
+ return None, []
210
+
211
+ try:
212
+ base = Image.open(base_path).convert("RGB")
213
+ curr = Image.open(curr_path).convert("RGB")
214
+ except Exception as e:
215
+ _log.warning("VisualRegressionAgent._diff open failed: %s", e)
216
+ return None, []
217
+
218
+ # Downscale for speed + stable diffing (the overlay is illustrative).
219
+ max_w = 900
220
+ for im in (base, curr):
221
+ if im.width > max_w:
222
+ h = int(im.height * max_w / im.width)
223
+ im.thumbnail((max_w, h))
224
+
225
+ if base.size != curr.size:
226
+ # Layout changed dramatically — treat whole image as changed.
227
+ out = curr.copy()
228
+ d = ImageDraw.Draw(out)
229
+ d.rectangle([0, 0, out.width - 1, out.height - 1], outline=(255, 0, 0), width=3)
230
+ diff_path = out_dir / f"{slug}_{label}_diff.png"
231
+ out.save(str(diff_path))
232
+ return diff_path, [list(out.getbbox())]
233
+
234
+ bpx, cpx = base.load(), curr.load()
235
+ bw, bh = base.size
236
+ cell = 16
237
+ changed = []
238
+ for y in range(0, bh, cell):
239
+ for x in range(0, bw, cell):
240
+ diff = 0
241
+ n = 0
242
+ for yy in range(y, min(y + cell, bh)):
243
+ for xx in range(x, min(x + cell, bw)):
244
+ br, bg, bb = bpx[xx, yy]
245
+ cr, cg, cb = cpx[xx, yy]
246
+ diff += abs(br - cr) + abs(bg - cg) + abs(bb - cb)
247
+ n += 1
248
+ if n and diff / n > 55: # perceptible change in this cell
249
+ changed.append((x, y, min(cell, bw - x), min(cell, bh - y)))
250
+
251
+ if not changed:
252
+ return None, []
253
+
254
+ out = curr.copy()
255
+ d = ImageDraw.Draw(out)
256
+ for (x, y, w, h) in changed:
257
+ d.rectangle([x, y, x + w, y + h], outline=(255, 0, 0), width=2)
258
+ diff_path = out_dir / f"{slug}_{label}_diff.png"
259
+ out.save(str(diff_path))
260
+ return diff_path, [list(c) for c in changed]
patchi/web/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """Patchi Web UI — FastAPI + htmx security dashboard."""
@@ -0,0 +1 @@
1
+ """Patchi web API endpoints."""
@@ -0,0 +1,114 @@
1
+ """Brain Map API — node and edge data for the canvas."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+
7
+ from fastapi import APIRouter, Request
8
+ from fastapi.responses import JSONResponse
9
+
10
+ router = APIRouter(prefix="/api/brain-map")
11
+
12
+
13
+ @router.get("/nodes")
14
+ async def get_nodes(request: Request) -> JSONResponse:
15
+ root = request.app.state.root
16
+ from collections import Counter
17
+
18
+ from patchi.core import memory as mem
19
+
20
+ brain = mem.get_brain(root)
21
+ import_graph = brain.get("import_graph", {})
22
+ nodes_raw = import_graph.get("nodes", [])
23
+ health = brain.get("health_score", {})
24
+ dead_files = set(brain.get("dead_files", []))
25
+
26
+ # Compute finding counts per file from scan results
27
+ scan_results = mem.get_scan_results(root)
28
+ finding_counts: dict[str, int] = Counter()
29
+ finding_severities: dict[str, str] = {}
30
+ for _agent_name, data in scan_results.items():
31
+ for f in data.get("findings", []):
32
+ file_path = f.get("file", "")
33
+ if file_path:
34
+ finding_counts[file_path] += 1
35
+ sev = f.get("severity", "info")
36
+ existing = finding_severities.get(file_path)
37
+ sev_order = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
38
+ if existing is None or sev_order.get(sev, 4) < sev_order.get(existing, 4):
39
+ finding_severities[file_path] = sev
40
+
41
+ # Build node list with metadata
42
+ nodes = []
43
+ for _i, node_id in enumerate(nodes_raw):
44
+ # Determine type (maps to BrainMap node shape/color)
45
+ if any(x in node_id for x in ["main.py", "app.py", "server.py", "__main__.py"]):
46
+ node_type = "entry_point"
47
+ elif any(x in node_id for x in ["base.py", "coordinator.py", "brain.py", "config.py"]):
48
+ node_type = "component"
49
+ elif any(x in node_id for x in ["test_", "tests/"]):
50
+ node_type = "test"
51
+ elif any(x in node_id for x in [".json", ".yaml", ".yml", ".toml", ".env"]):
52
+ node_type = "config"
53
+ else:
54
+ node_type = "default"
55
+
56
+ fcount = finding_counts.get(node_id, 0)
57
+ fsev = finding_severities.get(node_id, "info")
58
+
59
+ # Determine status
60
+ if node_id in dead_files:
61
+ status = "dead"
62
+ elif fcount > 0 and fsev == "critical":
63
+ status = "critical"
64
+ elif fcount > 5:
65
+ status = "warning"
66
+ elif health.get("total", 0) < 30:
67
+ status = "critical"
68
+ elif health.get("total", 0) < 70:
69
+ status = "warning"
70
+ else:
71
+ status = "healthy"
72
+
73
+ # Position hint based on type
74
+ x = 0
75
+ y = 0
76
+ if node_type == "entry_point":
77
+ y = -100 # float to top
78
+ elif node_type == "dead":
79
+ y = 100 # sink to bottom
80
+
81
+ nodes.append(
82
+ {
83
+ "id": node_id,
84
+ "label": os.path.basename(node_id),
85
+ "file": node_id,
86
+ "type": node_type,
87
+ "status": status,
88
+ "finding_count": fcount,
89
+ "severity": fsev,
90
+ "x": x,
91
+ "y": y,
92
+ }
93
+ )
94
+
95
+ return JSONResponse({"nodes": nodes})
96
+
97
+
98
+ @router.get("/edges")
99
+ async def get_edges(request: Request) -> JSONResponse:
100
+ root = request.app.state.root
101
+ from patchi.core import memory as mem
102
+
103
+ brain = mem.get_brain(root)
104
+ import_graph = brain.get("import_graph", {})
105
+ edges_raw = import_graph.get("edges", {})
106
+
107
+ # Convert {source: [targets]} to [{from, to}] format
108
+ edges = []
109
+ for source, targets in edges_raw.items():
110
+ if isinstance(targets, list):
111
+ for target in targets:
112
+ edges.append({"from": source, "to": target, "source": source, "target": target})
113
+
114
+ return JSONResponse({"edges": edges})
@@ -0,0 +1,47 @@
1
+ """Charts API — data for timeline and trends."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ import sqlite3
8
+
9
+ from fastapi import APIRouter, Request
10
+ from fastapi.responses import JSONResponse
11
+
12
+ logger = logging.getLogger("patchi.web.api.charts")
13
+
14
+ router = APIRouter(prefix="/api/charts")
15
+
16
+
17
+ @router.get("/timeline")
18
+ async def get_timeline(request: Request) -> JSONResponse:
19
+ root = request.app.state.root
20
+ db_path = root / ".patchi" / "patchi_history.db"
21
+
22
+ history = []
23
+ if db_path.exists():
24
+ conn = None
25
+ try:
26
+ conn = sqlite3.connect(str(db_path))
27
+ cursor = conn.execute(
28
+ "SELECT severity_breakdown, findings_count FROM scan_history ORDER BY rowid DESC LIMIT 20"
29
+ )
30
+ for row in cursor.fetchall():
31
+ breakdown = json.loads(row[0] or "{}")
32
+ history.append(
33
+ {
34
+ "total": row[1],
35
+ "critical": breakdown.get("critical", 0),
36
+ "high": breakdown.get("high", 0),
37
+ "medium": breakdown.get("medium", 0),
38
+ "low": breakdown.get("low", 0),
39
+ }
40
+ )
41
+ except Exception as e:
42
+ logger.warning("Failed to fetch chart history: %s", e)
43
+ finally:
44
+ if conn:
45
+ conn.close()
46
+
47
+ return JSONResponse({"history": list(reversed(history))})
patchi/web/api/chat.py ADDED
@@ -0,0 +1,224 @@
1
+ """Chat API — AI chat with smart brain context injection."""
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+
6
+ from fastapi import APIRouter, Request
7
+ from fastapi.responses import JSONResponse
8
+
9
+ _log = logging.getLogger("patchi.web.api.chat")
10
+
11
+ router = APIRouter(prefix="/api/chat")
12
+
13
+ BASE_PROMPT = """You are Patchi, a security assistant. You help developers understand and fix security issues.
14
+
15
+ Answer concisely. If asked about specific findings, explain what the issue is and how to fix it.
16
+ Use markdown for formatting."""
17
+
18
+
19
+ def _build_context(brain: dict, message: str) -> str:
20
+ """Build relevant context from brain data based on the user's question."""
21
+ m_lower = message.lower()
22
+ parts = []
23
+
24
+ file_count = brain.get("file_count", 0)
25
+ route_count = brain.get("route_count", 0)
26
+ framework = brain.get("framework", "Unknown")
27
+ health = brain.get("health_score", {})
28
+ health_total = health.get("total", 0) if isinstance(health, dict) else health
29
+ parts.append(
30
+ f"Project: {file_count} files, {route_count} routes, framework: {framework}, health: {health_total}/100"
31
+ )
32
+
33
+ if any(
34
+ w in m_lower
35
+ for w in ["finding", "issue", "vulnerability", "bug", "critical", "high", "error"]
36
+ ):
37
+ issues = brain.get("issues", []) or brain.get("findings", [])
38
+ if issues and isinstance(issues, list):
39
+ by_sev: dict[str, int] = {}
40
+ for f in issues:
41
+ sev = f.get("severity", "info") if isinstance(f, dict) else "info"
42
+ by_sev[sev] = by_sev.get(sev, 0) + 1
43
+ if by_sev:
44
+ parts.append(
45
+ f"Issues ({len(issues)} total): {', '.join(f'{c} {s}' for s, c in sorted(by_sev.items()))}"
46
+ )
47
+ top = [
48
+ f
49
+ for f in issues
50
+ if isinstance(f, dict) and f.get("severity") in ("critical", "high")
51
+ ][:5]
52
+ for f in top:
53
+ parts.append(
54
+ f" [{f['severity']}] {f.get('file', '')}:{f.get('line', 0)} — {f.get('message', '')[:80]}"
55
+ )
56
+
57
+ if any(w in m_lower for w in ["route", "endpoint", "api", "path"]):
58
+ routes = brain.get("routes", [])
59
+ if routes and isinstance(routes, list):
60
+ samples = []
61
+ for r in routes[:15]:
62
+ if isinstance(r, dict):
63
+ samples.append(f" {r.get('method', '?')} {r.get('path', '?')}")
64
+ else:
65
+ samples.append(f" {r}")
66
+ if samples:
67
+ parts.append(f"Routes ({len(routes)} total):\n" + "\n".join(samples[:15]))
68
+
69
+ if any(w in m_lower for w in ["security", "cve", "secret", "injection", "xss", "sqli"]):
70
+ sec = brain.get("security_report", brain.get("security_str", {}))
71
+ if isinstance(sec, dict):
72
+ parts.append(
73
+ f"Security: {sec.get('critical', 0)}c, {sec.get('high', 0)}h, {sec.get('medium', 0)}m"
74
+ )
75
+
76
+ if any(w in m_lower for w in ["patch", "fix", "change"]):
77
+ patches = brain.get("patches", [])
78
+ if patches and isinstance(patches, list):
79
+ applied = sum(
80
+ 1
81
+ for p in patches
82
+ if isinstance(p, dict) and p.get("state") in ("applied", "auto_applied")
83
+ )
84
+ pending = sum(1 for p in patches if isinstance(p, dict) and p.get("state") == "pending")
85
+ parts.append(f"Patches: {applied} applied, {pending} pending")
86
+
87
+ if any(w in m_lower for w in ["test", "coverage"]):
88
+ tests = brain.get("test_results", {})
89
+ if tests:
90
+ parts.append(f"Tests: {tests.get('passed', 0)} passed, {tests.get('failed', 0)} failed")
91
+
92
+ return "\n".join(parts) if parts else ""
93
+
94
+
95
+ def _local_fallback(message: str, brain: dict) -> str:
96
+ """Rule-based fallback when no AI provider is available."""
97
+ m = message.lower()
98
+ file_count = brain.get("file_count", 0)
99
+ framework = brain.get("framework", "Unknown")
100
+ health = brain.get("health_score", {})
101
+ health_total = health.get("total", 0) if isinstance(health, dict) else health
102
+
103
+ # Greeting
104
+ if any(w in m for w in ["hello", "hi", "hey", "greetings"]):
105
+ return (
106
+ f"Hi! I'm Patchi. This project has {file_count} files, "
107
+ f"uses {framework}, and has a health score of {health_total}/100. "
108
+ f"No AI provider is configured — run `p key add` for full AI chat."
109
+ )
110
+
111
+ # Project overview
112
+ if any(w in m for w in ["project", "about", "what is", "overview"]):
113
+ purpose = brain.get("project_purpose", "")
114
+ parts = [f"This is a {framework} project with {file_count} files."]
115
+ if purpose:
116
+ parts.append(f"Purpose: {purpose}")
117
+ parts.append(f"Health score: {health_total}/100.")
118
+ routes = brain.get("routes", [])
119
+ if routes:
120
+ parts.append(f"{len(routes)} routes detected.")
121
+ return " ".join(parts)
122
+
123
+ # Findings / security
124
+ if any(w in m for w in ["finding", "issue", "vulnerability", "security", "critical", "bug"]):
125
+ scans = brain.get("scan_results", {})
126
+ if not scans:
127
+ from pathlib import Path
128
+
129
+ from patchi.core import memory as mem
130
+ try:
131
+ scans = mem.get_scan_results(Path(brain.get("_root", ".")))
132
+ except Exception:
133
+ scans = {}
134
+ total = sum(len(v.get("findings", [])) for v in scans.values()) if scans else 0
135
+ if total:
136
+ return (
137
+ f"Found {total} findings across {len(scans)} scanners. "
138
+ f"Run `p findings` in the CLI for details, or configure an AI provider "
139
+ f"with `p key add` for AI-powered analysis."
140
+ )
141
+ return "No scan results yet. Run `p scan` first to detect findings."
142
+
143
+ # Health
144
+ if any(w in m for w in ["health", "score", "grade"]):
145
+ grade = health.get("grade", "?") if isinstance(health, dict) else "?"
146
+ return (
147
+ f"Health score: {health_total}/100 (grade {grade}). "
148
+ f"{file_count} files, {framework} framework. "
149
+ f"Run `p doctor` for a full health check."
150
+ )
151
+
152
+ # Routes
153
+ if any(w in m for w in ["route", "endpoint", "api", "path"]):
154
+ routes = brain.get("routes", [])
155
+ if routes:
156
+ samples = [f" {r.get('method', '?')} {r.get('path', '?')}" for r in routes[:10] if isinstance(r, dict)]
157
+ return f"{len(routes)} routes detected:\n" + "\n".join(samples)
158
+ return "No routes detected yet. Run `p scan` to discover routes."
159
+
160
+ # Help
161
+ if any(w in m for w in ["help", "how", "what can"]):
162
+ return (
163
+ "I can help with:\n"
164
+ "• **Findings** — ask about security issues and vulnerabilities\n"
165
+ "• **Project** — overview of your codebase\n"
166
+ "• **Health** — project health score and grade\n"
167
+ "• **Routes** — API endpoints and paths\n"
168
+ "\nFor AI-powered analysis, run `p key add` to configure an AI provider."
169
+ )
170
+
171
+ return (
172
+ f"I'm Patchi (local mode — no AI provider configured). "
173
+ f"This {framework} project has {file_count} files with health score {health_total}/100. "
174
+ f"Ask about findings, project, health, or routes. "
175
+ f"Run `p key add` for full AI chat."
176
+ )
177
+
178
+
179
+ @router.post("")
180
+ async def chat_message(request: Request) -> JSONResponse:
181
+ body = await request.json()
182
+ message = body.get("message", "")
183
+
184
+ if not message:
185
+ return JSONResponse({"ok": False, "error": "No message"}, status_code=400)
186
+
187
+ root = request.app.state.root
188
+ from patchi.core import config as cfg
189
+ from patchi.core import memory as mem
190
+
191
+ try:
192
+ config = cfg.load(root)
193
+ except Exception:
194
+ config = {}
195
+
196
+ brain = mem.get_brain(root)
197
+
198
+ context = _build_context(brain, message)
199
+ system = f"{BASE_PROMPT}\n\n{context}" if context else BASE_PROMPT
200
+
201
+ # Skip AI call if no real providers configured — use instant local fallback.
202
+ # AI Horde is too slow for chat; only use it with a custom key, not the anon fallback.
203
+ ai_cfg = config.get("ai", {})
204
+ ai_keys = ai_cfg.get("keys", [])
205
+ has_provider = (
206
+ ai_keys
207
+ or ai_cfg.get("local_model_name")
208
+ )
209
+ if has_provider:
210
+ try:
211
+ from patchi.core.ai.client import call_ai
212
+
213
+ response = call_ai(
214
+ config, system, message,
215
+ max_tokens=1000, timeout=25,
216
+ )
217
+ if response:
218
+ return JSONResponse({"ok": True, "response": response})
219
+ except Exception as e:
220
+ _log.debug("AI call failed: %s", e)
221
+
222
+ # Local fallback — rule-based responses when no AI provider is available
223
+ fallback = _local_fallback(message, brain)
224
+ return JSONResponse({"ok": True, "response": fallback})