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,158 @@
1
+ """
2
+ Secret Scanner Analyzer — detects hardcoded secrets and credentials.
3
+
4
+ This is a built-in analyzer that scans for:
5
+ - Hardcoded passwords
6
+ - API keys
7
+ - Private keys
8
+ - Connection strings
9
+ - Tokens
10
+
11
+ Example usage:
12
+ from patchi.core.plugins import get_registry, AnalyzerContext
13
+ from pathlib import Path
14
+
15
+ registry = get_registry()
16
+ context = AnalyzerContext(root=Path("."))
17
+ result = registry.run("secret-scanner", context)
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import re
23
+
24
+ from patchi.core.plugins.analyzer import (
25
+ Analyzer,
26
+ AnalyzerContext,
27
+ AnalyzerResult,
28
+ FileNode,
29
+ Finding,
30
+ Severity,
31
+ make_finding,
32
+ )
33
+
34
+ # Patterns that indicate secrets
35
+ SECRET_PATTERNS = [
36
+ # Passwords
37
+ (r'(?i)(password|passwd|pwd)\s*[=:]\s*["\']([^"\']{8,})["\']', "hardcoded-password", Severity.CRITICAL),
38
+ (r'(?i)(password|passwd|pwd)\s*[=:]\s*([^\s"\'#]{8,})', "hardcoded-password", Severity.CRITICAL),
39
+
40
+ # API Keys
41
+ (r'(?i)(api[_-]?key|apikey)\s*[=:]\s*["\']([A-Za-z0-9_\-]{20,})["\']', "hardcoded-api-key", Severity.HIGH),
42
+ (r'(?i)(secret[_-]?key|client[_-]?secret)\s*[=:]\s*["\']([A-Za-z0-9_\-]{20,})["\']', "hardcoded-secret-key", Severity.HIGH),
43
+
44
+ # Private keys
45
+ (r'-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----', "private-key-exposure", Severity.CRITICAL),
46
+
47
+ # Connection strings
48
+ (r'(?i)(mysql|postgres|postgresql|mongodb|redis)://[^\s"\'<>]{20,}', "hardcoded-connection-string", Severity.HIGH),
49
+
50
+ # Tokens
51
+ (r'(?i)(token|access[_-]?token|auth[_-]?token)\s*[=:]\s*["\']([A-Za-z0-9_\-\.]{20,})["\']', "hardcoded-token", Severity.HIGH),
52
+
53
+ # AWS keys
54
+ (r'AKIA[0-9A-Z]{16}', "aws-access-key", Severity.CRITICAL),
55
+
56
+ # GitHub tokens
57
+ (r'ghp_[A-Za-z0-9]{36}', "github-personal-access-token", Severity.CRITICAL),
58
+
59
+ # JWT tokens
60
+ (r'eyJ[A-Za-z0-9_\-]*\.eyJ[A-Za-z0-9_\-]*\.[A-Za-z0-9_\-]*', "jwt-token-exposure", Severity.HIGH),
61
+ ]
62
+
63
+ # Files to skip
64
+ SKIP_PATTERNS = [
65
+ "*.min.js",
66
+ "*.min.css",
67
+ "*.map",
68
+ "*.lock",
69
+ "package-lock.json",
70
+ "yarn.lock",
71
+ "*.pyc",
72
+ "__pycache__/*",
73
+ "node_modules/*",
74
+ ".git/*",
75
+ ]
76
+
77
+
78
+ class SecretScanner(Analyzer):
79
+ """Scans for hardcoded secrets and credentials."""
80
+
81
+ name = "secret-scanner"
82
+ version = "1.0.0"
83
+ description = "Detects hardcoded passwords, API keys, tokens, and other secrets"
84
+ priority = 10 # Run early
85
+ supported_file_patterns = [] # All files
86
+
87
+ def analyze(self, context: AnalyzerContext) -> AnalyzerResult:
88
+ findings: list[Finding] = []
89
+ files_scanned = 0
90
+
91
+ for file in context.files:
92
+ if not self.should_analyze(file):
93
+ continue
94
+
95
+ # Skip binary/minified files
96
+ if any(re.match(p.replace("*", ".*"), file.path) for p in SKIP_PATTERNS):
97
+ continue
98
+
99
+ files_scanned += 1
100
+ file_findings = self._scan_file(file)
101
+ findings.extend(file_findings)
102
+
103
+ return AnalyzerResult(
104
+ findings=findings,
105
+ files_analyzed=files_scanned,
106
+ metrics={
107
+ "files_scanned": files_scanned,
108
+ "findings_count": len(findings),
109
+ },
110
+ )
111
+
112
+ def _scan_file(self, file: FileNode) -> list[Finding]:
113
+ """Scan a single file for secrets."""
114
+ findings: list[Finding] = []
115
+
116
+ if not file.content:
117
+ return findings
118
+
119
+ lines = file.content.splitlines()
120
+
121
+ for line_num, line in enumerate(lines, start=1):
122
+ for pattern, finding_type, severity in SECRET_PATTERNS:
123
+ matches = re.finditer(pattern, line)
124
+ for match in matches:
125
+ # Skip false positives
126
+ if self._is_false_positive(line, match):
127
+ continue
128
+
129
+ findings.append(make_finding(
130
+ file=file.path,
131
+ line=line_num,
132
+ type=finding_type,
133
+ severity=severity,
134
+ message=f"Possible {finding_type.replace('-', ' ')} detected",
135
+ detail=f"Pattern matched: {match.group()[:50]}...",
136
+ code_snippet=line.strip()[:200],
137
+ suggestion="Move secrets to environment variables or a secrets manager",
138
+ confidence=0.8,
139
+ ))
140
+
141
+ return findings
142
+
143
+ def _is_false_positive(self, line: str, match: re.Match) -> bool:
144
+ """Check if a match is likely a false positive."""
145
+ # Skip comments
146
+ stripped = line.strip()
147
+ if stripped.startswith("#") or stripped.startswith("//") or stripped.startswith("*"):
148
+ return True
149
+
150
+ # Skip test files
151
+ if "test" in line.lower() or "example" in line.lower() or "mock" in line.lower():
152
+ return True
153
+
154
+ # Skip placeholders
155
+ if any(p in match.group().lower() for p in ["xxx", "yyy", "placeholder", "your-", "example"]):
156
+ return True
157
+
158
+ return False
@@ -0,0 +1,115 @@
1
+ """
2
+ TODO Scanner Analyzer — detects TODO, FIXME, HACK, and other code comments.
3
+
4
+ This is a built-in analyzer that scans for:
5
+ - TODO comments
6
+ - FIXME comments
7
+ - HACK comments
8
+ - XXX comments
9
+ - NOTE comments
10
+
11
+ Example usage:
12
+ from patchi.core.plugins import get_registry, AnalyzerContext
13
+ from pathlib import Path
14
+
15
+ registry = get_registry()
16
+ context = AnalyzerContext(root=Path("."))
17
+ result = registry.run("todo-scanner", context)
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import re
23
+
24
+ from patchi.core.plugins.analyzer import (
25
+ Analyzer,
26
+ AnalyzerContext,
27
+ AnalyzerResult,
28
+ FileNode,
29
+ Finding,
30
+ Severity,
31
+ make_finding,
32
+ )
33
+
34
+ # TODO-like patterns
35
+ TODO_PATTERNS = [
36
+ (r'(?i)\bTODO\b', "todo-comment", Severity.LOW),
37
+ (r'(?i)\bFIXME\b', "fixme-comment", Severity.MEDIUM),
38
+ (r'(?i)\bHACK\b', "hack-comment", Severity.MEDIUM),
39
+ (r'(?i)\bXXX\b', "xxx-comment", Severity.MEDIUM),
40
+ (r'(?i)\bNOTE\b', "note-comment", Severity.INFO),
41
+ (r'(?i)\bBUG\b', "bug-comment", Severity.MEDIUM),
42
+ (r'(?i)\bOPTIMIZE\b', "optimize-comment", Severity.LOW),
43
+ (r'(?i)\bREFACTOR\b', "refactor-comment", Severity.LOW),
44
+ ]
45
+
46
+
47
+ class TodoScanner(Analyzer):
48
+ """Scans for TODO, FIXME, HACK, and other code comments."""
49
+
50
+ name = "todo-scanner"
51
+ version = "1.0.0"
52
+ description = "Detects TODO, FIXME, HACK, and other code comments"
53
+ priority = 200 # Run last
54
+ supported_file_patterns = [] # All files
55
+
56
+ def analyze(self, context: AnalyzerContext) -> AnalyzerResult:
57
+ findings: list[Finding] = []
58
+ files_scanned = 0
59
+
60
+ for file in context.files:
61
+ if not self.should_analyze(file):
62
+ continue
63
+
64
+ files_scanned += 1
65
+ file_findings = self._scan_file(file)
66
+ findings.extend(file_findings)
67
+
68
+ return AnalyzerResult(
69
+ findings=findings,
70
+ files_analyzed=files_scanned,
71
+ metrics={
72
+ "files_scanned": files_scanned,
73
+ "findings_count": len(findings),
74
+ },
75
+ )
76
+
77
+ def _scan_file(self, file: FileNode) -> list[Finding]:
78
+ """Scan a single file for TODO-like comments."""
79
+ findings: list[Finding] = []
80
+
81
+ if not file.content:
82
+ return findings
83
+
84
+ lines = file.content.splitlines()
85
+
86
+ for line_num, line in enumerate(lines, start=1):
87
+ stripped = line.strip()
88
+
89
+ # Skip non-comment lines
90
+ if not any(stripped.startswith(p) for p in ["#", "//", "/*", "*", "<!--"]):
91
+ # Check if it's inline comment
92
+ if not any(p in line for p in ["#", "//", "/*", "<!--"]):
93
+ continue
94
+
95
+ for pattern, finding_type, severity in TODO_PATTERNS:
96
+ match = re.search(pattern, line)
97
+ if match:
98
+ # Extract the comment text
99
+ comment_text = line.strip()
100
+ if len(comment_text) > 200:
101
+ comment_text = comment_text[:200] + "..."
102
+
103
+ findings.append(make_finding(
104
+ file=file.path,
105
+ line=line_num,
106
+ type=finding_type,
107
+ severity=severity,
108
+ message=f"Code comment: {match.group().upper()}",
109
+ detail=comment_text,
110
+ code_snippet=line.strip()[:200],
111
+ suggestion=f"Address the {match.group().upper()} comment or remove it",
112
+ confidence=1.0,
113
+ ))
114
+
115
+ return findings
@@ -0,0 +1,335 @@
1
+ """
2
+ Plugin registry with auto-discovery for Patchi.
3
+
4
+ Automatically discovers and registers analyzers from:
5
+ 1. patchi/core/plugins/analyzers/ (built-in)
6
+ 2. ~/.patchi/plugins/ (user plugins)
7
+ 3. .patchi/plugins/ (project plugins)
8
+
9
+ Usage:
10
+ from patchi.core.plugins.registry import get_registry
11
+
12
+ registry = get_registry()
13
+ results = registry.run_all(context)
14
+
15
+ # Or run specific analyzer
16
+ result = registry.run("secret-scanner", context)
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import importlib
22
+ import importlib.util
23
+ import logging
24
+ import sys
25
+ import time
26
+ from pathlib import Path
27
+
28
+ from patchi.core.plugins.analyzer import (
29
+ Analyzer,
30
+ AnalyzerContext,
31
+ AnalyzerResult,
32
+ Finding,
33
+ Severity,
34
+ )
35
+
36
+ _log = logging.getLogger("patchi.plugins")
37
+
38
+
39
+ class PluginRegistry:
40
+ """Registry for discovering and running analyzers.
41
+
42
+ Features:
43
+ - Auto-discovery from plugin directories
44
+ - Lazy loading (only import when needed)
45
+ - Priority-based execution order
46
+ - Timeout enforcement
47
+ - Error isolation (one plugin can't crash others)
48
+ """
49
+
50
+ def __init__(self) -> None:
51
+ self._analyzers: dict[str, Analyzer] = {}
52
+ self._loaded: bool = False
53
+ self._load_time_ms: int = 0
54
+
55
+ def discover(self, extra_dirs: list[Path] | None = None) -> list[str]:
56
+ """Discover and register all analyzers.
57
+
58
+ Returns list of errors (empty = success).
59
+ """
60
+ if self._loaded:
61
+ return []
62
+
63
+ t0 = time.monotonic()
64
+ errors: list[str] = []
65
+
66
+ # Plugin search paths
67
+ search_paths = [
68
+ Path(__file__).parent / "analyzers", # built-in
69
+ Path.home() / ".patchi" / "plugins", # user
70
+ Path.cwd() / ".patchi" / "plugins", # project
71
+ ]
72
+ if extra_dirs:
73
+ search_paths.extend(extra_dirs)
74
+
75
+ for plugin_dir in search_paths:
76
+ if not plugin_dir.exists():
77
+ continue
78
+
79
+ _log.debug("Discovering plugins in %s", plugin_dir)
80
+
81
+ # Walk all Python files in the plugin directory
82
+ for py_file in plugin_dir.glob("**/*.py"):
83
+ if py_file.name.startswith("_"):
84
+ continue
85
+
86
+ try:
87
+ self._load_plugin_file(py_file, plugin_dir)
88
+ except Exception as e:
89
+ errors.append(f"{py_file}: {type(e).__name__}: {e}")
90
+ _log.debug("Failed to load plugin %s: %s", py_file, e)
91
+
92
+ # Also discover from AGENT_PACKAGES (existing agents)
93
+ try:
94
+ self._discover_from_agent_packages()
95
+ except Exception as e:
96
+ errors.append(f"Agent package discovery: {type(e).__name__}: {e}")
97
+
98
+ self._loaded = True
99
+ self._load_time_ms = int((time.monotonic() - t0) * 1000)
100
+
101
+ _log.info(
102
+ "Plugin discovery complete: %d analyzers in %dms",
103
+ len(self._analyzers),
104
+ self._load_time_ms,
105
+ )
106
+
107
+ return errors
108
+
109
+ def _load_plugin_file(self, py_file: Path, base_dir: Path) -> None:
110
+ """Load a single plugin file and register any Analyzer subclasses."""
111
+ # Compute module name relative to base_dir
112
+ rel = py_file.relative_to(base_dir)
113
+ module_name = f"patchi.plugins.{rel.stem}"
114
+
115
+ # Skip if already loaded
116
+ if module_name in sys.modules:
117
+ return
118
+
119
+ # Load the module
120
+ spec = importlib.util.spec_from_file_location(module_name, py_file)
121
+ if not spec or not spec.loader:
122
+ return
123
+
124
+ module = importlib.util.module_from_spec(spec)
125
+ sys.modules[module_name] = module
126
+ spec.loader.exec_module(module)
127
+
128
+ # Find and register any Analyzer subclasses
129
+ for attr_name in dir(module):
130
+ attr = getattr(module, attr_name)
131
+ if (
132
+ isinstance(attr, type)
133
+ and issubclass(attr, Analyzer)
134
+ and attr is not Analyzer
135
+ ):
136
+ self.register_analyzer(attr())
137
+
138
+ def _discover_from_agent_packages(self) -> None:
139
+ """Discover analyzers from existing agent packages.
140
+
141
+ This wraps existing BaseAgent subclasses as analyzers for backwards
142
+ compatibility.
143
+ """
144
+ try:
145
+ from patchi.core.agents.base import (
146
+ _REGISTRY,
147
+ discover_agent_modules,
148
+ )
149
+
150
+ # Discover all agents first
151
+ discover_agent_modules()
152
+
153
+ # Wrap each agent as an analyzer
154
+ for name, agent_cls in _REGISTRY.items():
155
+ if name not in self._analyzers:
156
+ wrapper = AgentAnalyzerWrapper(agent_cls)
157
+ self._analyzers[name] = wrapper
158
+
159
+ except ImportError:
160
+ pass
161
+
162
+ def register_analyzer(self, analyzer: Analyzer) -> None:
163
+ """Register an analyzer instance."""
164
+ if analyzer.name in self._analyzers:
165
+ _log.warning(
166
+ "Analyzer '%s' already registered, overriding",
167
+ analyzer.name,
168
+ )
169
+ self._analyzers[analyzer.name] = analyzer
170
+ _log.debug("Registered analyzer: %s v%s", analyzer.name, analyzer.version)
171
+
172
+ def get(self, name: str) -> Analyzer | None:
173
+ """Get an analyzer by name."""
174
+ if not self._loaded:
175
+ self.discover()
176
+ return self._analyzers.get(name)
177
+
178
+ def list_analyzers(self, enabled_only: bool = True) -> list[Analyzer]:
179
+ """List all registered analyzers."""
180
+ if not self._loaded:
181
+ self.discover()
182
+
183
+ analyzers = list(self._analyzers.values())
184
+ if enabled_only:
185
+ analyzers = [a for a in analyzers if a.enabled]
186
+
187
+ return sorted(analyzers, key=lambda a: (a.priority, a.name))
188
+
189
+ def run(self, name: str, context: AnalyzerContext) -> AnalyzerResult:
190
+ """Run a specific analyzer by name."""
191
+ analyzer = self.get(name)
192
+ if not analyzer:
193
+ return AnalyzerResult(
194
+ errors=[f"Analyzer '{name}' not found"],
195
+ )
196
+
197
+ return self._run_with_timeout(analyzer, context)
198
+
199
+ def run_all(
200
+ self,
201
+ context: AnalyzerContext,
202
+ analyzers: list[str] | None = None,
203
+ ) -> dict[str, AnalyzerResult]:
204
+ """Run all (or specified) analyzers and return results.
205
+
206
+ Args:
207
+ context: The analysis context
208
+ analyzers: Optional list of analyzer names to run (None = all)
209
+
210
+ Returns:
211
+ Dict mapping analyzer name to its result
212
+ """
213
+ if not self._loaded:
214
+ self.discover()
215
+
216
+ to_run = self.list_analyzers(enabled_only=True)
217
+ if analyzers:
218
+ to_run = [a for a in to_run if a.name in analyzers]
219
+
220
+ results: dict[str, AnalyzerResult] = {}
221
+ for analyzer in to_run:
222
+ try:
223
+ results[analyzer.name] = self._run_with_timeout(analyzer, context)
224
+ except Exception as e:
225
+ results[analyzer.name] = AnalyzerResult(
226
+ errors=[f"{type(e).__name__}: {e}"],
227
+ )
228
+ _log.debug("Analyzer %s failed: %s", analyzer.name, e)
229
+
230
+ return results
231
+
232
+ def _run_with_timeout(
233
+ self,
234
+ analyzer: Analyzer,
235
+ context: AnalyzerContext,
236
+ ) -> AnalyzerResult:
237
+ """Run an analyzer with timeout enforcement."""
238
+ import concurrent.futures
239
+
240
+ t0 = time.monotonic()
241
+
242
+ try:
243
+ with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
244
+ future = executor.submit(analyzer.analyze, context)
245
+ result = future.result(timeout=analyzer.timeout)
246
+ result.duration_ms = int((time.monotonic() - t0) * 1000)
247
+
248
+ # Set analyzer name on all findings for stable IDs
249
+ for finding in result.findings:
250
+ finding.extra["_analyzer"] = analyzer.name
251
+
252
+ return result
253
+
254
+ except concurrent.futures.TimeoutError:
255
+ return AnalyzerResult(
256
+ errors=[f"Analyzer '{analyzer.name}' timed out after {analyzer.timeout}s"],
257
+ duration_ms=int((time.monotonic() - t0) * 1000),
258
+ )
259
+ except Exception as e:
260
+ return AnalyzerResult(
261
+ errors=[f"{type(e).__name__}: {e}"],
262
+ duration_ms=int((time.monotonic() - t0) * 1000),
263
+ )
264
+
265
+
266
+ class AgentAnalyzerWrapper(Analyzer):
267
+ """Wraps an existing BaseAgent as an Analyzer for backwards compatibility."""
268
+
269
+ def __init__(self, agent_cls: type) -> None:
270
+ self._agent_cls = agent_cls
271
+ self.name = getattr(agent_cls, "name", "unknown")
272
+ self.version = "0.0.0"
273
+ self.description = f"Legacy agent wrapper for {self.name}"
274
+ self.priority = 200 # Run after native analyzers
275
+ self.timeout = getattr(agent_cls, "timeout", 60)
276
+
277
+ def analyze(self, context: AnalyzerContext) -> AnalyzerResult:
278
+ """Wrap agent execution as analyzer."""
279
+ from patchi.core.agents.base import AgentInput
280
+
281
+ # Convert AnalyzerContext to AgentInput
282
+ agent_input = AgentInput(
283
+ root=context.root,
284
+ scope=context.scope,
285
+ brain={},
286
+ config=context.config,
287
+ extra=context.extra,
288
+ )
289
+
290
+ # Run the agent
291
+ agent = self._agent_cls()
292
+ result = agent.run(agent_input)
293
+
294
+ # Convert AgentResult to AnalyzerResult
295
+ findings = []
296
+ for f in result.findings:
297
+ findings.append(Finding(
298
+ file=f.file,
299
+ line=f.line,
300
+ type=f.type,
301
+ severity=Severity(f.severity.value),
302
+ message=f.message,
303
+ detail=f.detail,
304
+ code_snippet=f.code_snippet,
305
+ suggestion=f.suggestion,
306
+ cwe=f.cwe,
307
+ extra={"_legacy_agent": True},
308
+ ))
309
+
310
+ return AnalyzerResult(
311
+ findings=findings,
312
+ metrics=result.data,
313
+ errors=result.errors,
314
+ duration_ms=result.duration_ms,
315
+ files_analyzed=len(context.files),
316
+ )
317
+
318
+
319
+ # ── Singleton ─────────────────────────────────────────────────────────────────
320
+
321
+ _registry: PluginRegistry | None = None
322
+
323
+
324
+ def get_registry() -> PluginRegistry:
325
+ """Get or create the global plugin registry."""
326
+ global _registry
327
+ if _registry is None:
328
+ _registry = PluginRegistry()
329
+ return _registry
330
+
331
+
332
+ def list_plugins() -> list[dict]:
333
+ """List all registered plugins with their metadata."""
334
+ registry = get_registry()
335
+ return [a.describe() for a in registry.list_analyzers()]