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,775 @@
1
+ """
2
+ All 8 fix agents for Patchi.
3
+
4
+ Fix agents use AI to generate surgical patches for findings from scanner agents.
5
+ Each fix agent:
6
+ 1. Receives a Finding + the current file content
7
+ 2. Calls AI with a structured system prompt + skill-specific user prompt
8
+ 3. Parses the AI response into a Patch with FileChange(s)
9
+ 4. Computes risk_score and confidence
10
+ 5. Returns the Patch (never applies it — applier does that)
11
+
12
+ AI call budget: fix agents make 1 AI call per finding. No loops, no re-asks.
13
+ If the AI response can't be parsed into a clean diff, the finding is tagged
14
+ 'fix_failed' and returned to the user with the raw AI explanation.
15
+
16
+ Agents:
17
+ 1. CodeFixer — bug fixes, missing error handling, null checks
18
+ 2. SecurityFixer — patches confirmed vulnerabilities
19
+ 3. DeadCodeRemover — proposes deletion of confirmed dead files/functions
20
+ 4. DependencyFixer — version bumps, lockfile fixes, Dependabot setup
21
+ 5. EnvFixer — .env.example generation, secret removal from source
22
+ 6. TypeFixer — TypeScript type errors, unsafe casts (TS only)
23
+ 7. RefactorAgent — extracts duplicated logic to shared functions
24
+ 8. UnitTestRunner — generates test skeletons for uncovered functions
25
+
26
+ Fix agents are registered in the same registry as scanner agents.
27
+ The coordinator runs them sequentially (not in parallel — spec is explicit).
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import json
33
+ import logging
34
+ import re
35
+ from pathlib import Path
36
+
37
+ from patchi.core.agents.base import (
38
+ AgentDomain,
39
+ AgentGroup,
40
+ AgentInput,
41
+ AgentResult,
42
+ BaseAgent,
43
+ register,
44
+ )
45
+ from patchi.core.ai.client import call_ai
46
+ from patchi.core.ai.prompts import (
47
+ Skill,
48
+ build_prompt,
49
+ get_system_prompt,
50
+ )
51
+ from patchi.core.fix.base import compute_blast_radius
52
+ from patchi.core.fix.patch import (
53
+ FileChange,
54
+ Patch,
55
+ PatchState,
56
+ PatchType,
57
+ compute_confidence,
58
+ compute_risk_score,
59
+ )
60
+
61
+ _log = logging.getLogger("patchi.fix.fix_agents")
62
+
63
+ # ── Shared helpers ─────────────────────────────────────────────────────────────
64
+
65
+
66
+ def _read_file(path: Path) -> str:
67
+ try:
68
+ return path.read_text(encoding="utf-8", errors="replace")
69
+ except OSError:
70
+ return ""
71
+
72
+
73
+ def _extract_code_block(text: str) -> str:
74
+ """Extract the first ```...``` block from an AI response."""
75
+ m = re.search(r"```(?:\w+)?\n(.*?)```", text, re.DOTALL)
76
+ return m.group(1) if m else text.strip()
77
+
78
+
79
+ def _try_apply_partial(ai_response: str, original: str) -> str | None:
80
+ """
81
+ If AI returned partial lines (e.g., '42: fixed_code'), apply them to the original.
82
+ Returns the full file with changes applied, or None if not a partial response.
83
+ """
84
+ lines = original.splitlines()
85
+ partial_pattern = re.compile(r"^(\d+):\s*(.*)$")
86
+ changes = {}
87
+ for line in ai_response.splitlines():
88
+ line = line.strip()
89
+ m = partial_pattern.match(line)
90
+ if m:
91
+ line_num = int(m.group(1))
92
+ new_code = m.group(2)
93
+ if 1 <= line_num <= len(lines):
94
+ changes[line_num - 1] = new_code
95
+
96
+ if not changes:
97
+ return None
98
+
99
+ for idx, new_code in changes.items():
100
+ lines[idx] = new_code
101
+ return "\n".join(lines)
102
+
103
+
104
+ def _detect_language(file_path: str, project_root: str | None = None) -> str:
105
+ """Detect language from file extension for prompt formatting."""
106
+ ext = Path(file_path).suffix.lower()
107
+ # .h files are ambiguous (C vs C++). Default to "c", but if project has .cpp files, assume C++
108
+ if ext == ".h" and project_root:
109
+ root = Path(project_root)
110
+ if root.exists():
111
+ cpp_files = (
112
+ list(root.rglob("*.cpp")) + list(root.rglob("*.cxx")) + list(root.rglob("*.cc"))
113
+ )
114
+ if cpp_files:
115
+ return "cpp"
116
+ return {
117
+ ".py": "python",
118
+ ".pyw": "python",
119
+ ".js": "javascript",
120
+ ".jsx": "javascript",
121
+ ".mjs": "javascript",
122
+ ".cjs": "javascript",
123
+ ".ts": "typescript",
124
+ ".tsx": "typescript",
125
+ ".mts": "typescript",
126
+ ".rs": "rust",
127
+ ".java": "java",
128
+ ".go": "go",
129
+ ".rb": "ruby",
130
+ ".swift": "swift",
131
+ ".kt": "kotlin",
132
+ ".scala": "scala",
133
+ ".cs": "csharp",
134
+ ".dart": "dart",
135
+ ".c": "c",
136
+ ".h": "c",
137
+ ".cpp": "cpp",
138
+ ".cxx": "cpp",
139
+ ".cc": "cpp",
140
+ ".hpp": "cpp",
141
+ ".php": "php",
142
+ ".php3": "php",
143
+ ".php4": "php",
144
+ ".php5": "php",
145
+ ".phtml": "php",
146
+ ".svelte": "svelte",
147
+ ".sh": "bash",
148
+ ".bash": "bash",
149
+ ".zsh": "bash",
150
+ ".yml": "yaml",
151
+ ".yaml": "yaml",
152
+ ".json": "json",
153
+ ".html": "html",
154
+ ".htm": "html",
155
+ ".css": "css",
156
+ ".sql": "sql",
157
+ ".md": "markdown",
158
+ ".dockerfile": "dockerfile",
159
+ }.get(ext, "")
160
+
161
+
162
+ def _make_patch(
163
+ agent_name: str,
164
+ patch_type: PatchType,
165
+ changes: list[FileChange],
166
+ description: str,
167
+ ai_explanation: str,
168
+ finding_id: str = "",
169
+ blast_radius: int = 0,
170
+ agent_certainty: float = 0.9,
171
+ has_test_coverage: bool = True,
172
+ scanner_agent: str = "",
173
+ source_finding: dict | None = None,
174
+ ) -> Patch:
175
+ risk = compute_risk_score(changes, blast_radius)
176
+ conf = compute_confidence(changes, agent_certainty, has_test_coverage=has_test_coverage)
177
+ return Patch(
178
+ agent=agent_name,
179
+ patch_type=patch_type,
180
+ finding_id=finding_id,
181
+ changes=changes,
182
+ description=description,
183
+ ai_explanation=ai_explanation,
184
+ risk_score=risk,
185
+ confidence=conf,
186
+ blast_radius=blast_radius,
187
+ state=PatchState.PROPOSED,
188
+ scanner_agent=scanner_agent,
189
+ source_finding=source_finding or {},
190
+ verify_retries=2,
191
+ )
192
+
193
+
194
+ def _ai_fix(
195
+ config: dict,
196
+ skill: Skill,
197
+ file_path: str,
198
+ file_content: str,
199
+ finding: dict,
200
+ blast_radius: int = 0,
201
+ root=None,
202
+ framework: str = "",
203
+ ) -> str | None:
204
+ """
205
+ Call AI with structured system prompt + skill-specific prompt.
206
+ Returns the corrected file content, or None if AI unavailable/failed.
207
+ Handles both full-file and partial-line responses.
208
+ """
209
+ lang = _detect_language(file_path)
210
+ system = get_system_prompt(skill)
211
+
212
+ fw = framework or finding.get("framework", "")
213
+
214
+ from patchi.core.debug import debug_context_from_finding
215
+
216
+ dbg = debug_context_from_finding(finding, root or Path.cwd())
217
+
218
+ ctx: dict = {
219
+ "debug_context": (json.dumps(dbg, indent=2) if dbg else ""),
220
+ "file_path": file_path,
221
+ "file_content": file_content[:8000],
222
+ "finding_message": finding.get("message", ""),
223
+ "finding_line": finding.get("line", 0),
224
+ "suggestion": finding.get("suggestion", ""),
225
+ "code_snippet": finding.get("code_snippet", ""),
226
+ "cwe": finding.get("cwe", ""),
227
+ "blast_radius": str(blast_radius),
228
+ "language": lang,
229
+ "framework": fw,
230
+ "pattern_type": finding.get("pattern_type", ""),
231
+ "issue_type": finding.get("type", ""),
232
+ }
233
+
234
+ user_prompt = build_prompt(skill, ctx)
235
+
236
+ # Log AI call for audit trail (WIRE-04)
237
+ try:
238
+ from patchi.core.security.governance import patchi_action_log
239
+
240
+ r = root or Path.cwd()
241
+ patchi_action_log(
242
+ r,
243
+ "ai_call",
244
+ file_path,
245
+ agent=skill.value if hasattr(skill, "value") else "fix_agent",
246
+ detail=finding.get("type", ""),
247
+ )
248
+ except Exception as e:
249
+ _log.warning("Failed to write AI-call audit log entry for %s: %s", file_path, e)
250
+
251
+ response = call_ai(config, system, user_prompt)
252
+ if not response:
253
+ return None
254
+
255
+ proposed = _extract_code_block(response)
256
+ if not proposed.strip() or proposed.strip() == file_content.strip():
257
+ return None
258
+
259
+ # Check if AI returned partial lines (e.g., "42: fixed_code")
260
+ # Apply partial changes to the original file
261
+ partial = _try_apply_partial(proposed, file_content)
262
+ if partial is not None:
263
+ return partial
264
+
265
+ return proposed
266
+
267
+
268
+ # CodeFixer, SecurityFixer, and DeadCodeRemover live in their own files
269
+ # (code_fixer.py, security_fixer.py, dead_code_remover.py) imported via fix/__init__.py.
270
+ # They are registered there with @register.
271
+
272
+
273
+ # ── 4. DependencyFixer ────────────────────────────────────────────────────────
274
+
275
+
276
+ @register
277
+ class DependencyFixer(BaseAgent):
278
+ """
279
+ Version bumps for vulnerable dependencies.
280
+ Reads the OSV result and proposes the patched version.
281
+ """
282
+
283
+ name = "DependencyFixer"
284
+ group = AgentGroup.FIX
285
+ domain = AgentDomain.CODE_QUALITY
286
+ timeout = 60
287
+
288
+ def _run(self, inp: AgentInput, result: AgentResult) -> None:
289
+ vuln_findings = [
290
+ f for f in inp.extra.get("findings", []) if f.get("type") == "vulnerable_dependency"
291
+ ]
292
+
293
+ patches: list[Patch] = []
294
+
295
+ for finding in vuln_findings[:10]:
296
+ pkg_name = self._extract_package_name(finding.get("message", ""))
297
+ if not pkg_name:
298
+ continue
299
+
300
+ for dep_file in [
301
+ "requirements.txt",
302
+ "package.json",
303
+ "pyproject.toml",
304
+ "Cargo.toml",
305
+ "go.mod",
306
+ "Gemfile",
307
+ "composer.json",
308
+ "pom.xml",
309
+ "build.gradle",
310
+ "build.gradle.kts",
311
+ "pubspec.yaml",
312
+ "Package.swift",
313
+ "Package.resolved",
314
+ ]:
315
+ dep_path = inp.root / dep_file
316
+ if not dep_path.exists():
317
+ continue
318
+
319
+ original = _read_file(dep_path)
320
+ if not original:
321
+ continue
322
+
323
+ system = get_system_prompt(Skill.DEPENDENCY_FIX)
324
+ user_prompt = build_prompt(
325
+ Skill.DEPENDENCY_FIX,
326
+ {
327
+ "package_name": pkg_name,
328
+ "finding_message": finding.get("message", ""),
329
+ "cwe": finding.get("cwe", finding.get("detail", "")),
330
+ "package_file": dep_file,
331
+ "package_file_content": original[:3000],
332
+ },
333
+ )
334
+
335
+ # Log AI call for audit trail (WIRE-04)
336
+ try:
337
+ from patchi.core.security.governance import patchi_action_log
338
+
339
+ patchi_action_log(
340
+ inp.root, "ai_call", dep_file, agent="DependencyFixer", detail=pkg_name
341
+ )
342
+ except Exception as e:
343
+ _log.warning("Failed to write AI-call audit log entry for %s: %s", dep_file, e)
344
+
345
+ response = call_ai(inp.config, system, user_prompt)
346
+ if not response:
347
+ continue
348
+
349
+ proposed = _extract_code_block(response)
350
+ if proposed == original or not proposed.strip():
351
+ continue
352
+
353
+ change = FileChange(path=dep_file, original=original, proposed=proposed)
354
+ if not change.diff.strip():
355
+ break # AI made no change, skip this file
356
+
357
+ blast_radius = compute_blast_radius(dep_file, inp.root)
358
+
359
+ patch = _make_patch(
360
+ agent_name=self.name,
361
+ patch_type=PatchType.DEPENDENCY,
362
+ changes=[change],
363
+ description=f"Update vulnerable dependency: {pkg_name}",
364
+ ai_explanation=finding.get("detail", finding.get("message", "")),
365
+ finding_id=finding.get("type", ""),
366
+ blast_radius=blast_radius,
367
+ )
368
+ patches.append(patch)
369
+ result.data.setdefault("patches", []).append(patch.to_dict())
370
+ break
371
+
372
+ result.data["patch_count"] = len(patches)
373
+ result.ai_calls_made = len(patches)
374
+
375
+ def _extract_package_name(self, message: str) -> str:
376
+ m = re.match(r"^([\w\-\.\/]+)\s", message)
377
+ return m.group(1) if m else ""
378
+
379
+
380
+ # ── 5. EnvFixer ───────────────────────────────────────────────────────────────
381
+
382
+
383
+ @register
384
+ class EnvFixer(BaseAgent):
385
+ """
386
+ Fixes environment variable issues:
387
+ - Removes hardcoded secrets from source and replaces with os.environ / process.env
388
+ - Generates .env.example from .env variable names
389
+ - Adds missing .gitignore entries
390
+ """
391
+
392
+ name = "EnvFixer"
393
+ group = AgentGroup.FIX
394
+ timeout = 60
395
+
396
+ def _run(self, inp: AgentInput, result: AgentResult) -> None:
397
+ secret_findings = [
398
+ f for f in inp.extra.get("findings", []) if f.get("type") == "hardcoded_secret"
399
+ ]
400
+
401
+ patches: list[Patch] = []
402
+
403
+ for finding in secret_findings[:10]:
404
+ fpath = finding.get("file", "")
405
+ path = inp.root / fpath
406
+ original = _read_file(path)
407
+ if not original:
408
+ continue
409
+
410
+ framework = finding.get("framework", "")
411
+ proposed = _ai_fix(
412
+ inp.config,
413
+ Skill.ENV_FIX,
414
+ fpath,
415
+ original,
416
+ finding,
417
+ root=inp.root,
418
+ framework=framework,
419
+ )
420
+ if not proposed:
421
+ continue
422
+
423
+ change = FileChange(path=fpath, original=original, proposed=proposed)
424
+ if not change.diff.strip():
425
+ continue
426
+
427
+ blast_radius = compute_blast_radius(fpath, inp.root)
428
+
429
+ patch = _make_patch(
430
+ agent_name=self.name,
431
+ patch_type=PatchType.ENV_FIX,
432
+ changes=[change],
433
+ description=f"Replace hardcoded secret in {fpath}",
434
+ ai_explanation="Moved secret to environment variable reference.",
435
+ finding_id=finding.get("type", ""),
436
+ blast_radius=blast_radius,
437
+ agent_certainty=0.9,
438
+ )
439
+ patches.append(patch)
440
+ result.data.setdefault("patches", []).append(patch.to_dict())
441
+
442
+ # Also generate .env.example if not present
443
+ self._generate_env_example(inp, result)
444
+
445
+ result.data["patch_count"] = len(patches)
446
+ result.ai_calls_made = len(patches)
447
+
448
+ def _generate_env_example(self, inp: AgentInput, result: AgentResult) -> None:
449
+ env_example = inp.root / ".env.example"
450
+ if env_example.exists():
451
+ return
452
+
453
+ env_vars = inp.extra.get("env_vars", [])
454
+ if not env_vars:
455
+ return
456
+
457
+ content = "# Environment variables required by this application\n"
458
+ content += "# Copy to .env and fill in the values\n\n"
459
+ for var in env_vars:
460
+ content += f"{var}=\n"
461
+
462
+ change = FileChange(path=".env.example", original="", proposed=content)
463
+ patch = _make_patch(
464
+ agent_name=self.name,
465
+ patch_type=PatchType.ENV_FIX,
466
+ changes=[change],
467
+ description="Generate .env.example from discovered env variables",
468
+ ai_explanation="Created .env.example so contributors know which variables are required.",
469
+ agent_certainty=1.0,
470
+ )
471
+ result.data.setdefault("patches", []).append(patch.to_dict())
472
+
473
+
474
+ # ── 6. TypeFixer ──────────────────────────────────────────────────────────────
475
+
476
+ # Multi-language type-issue finding types produced by the type checker
477
+ # (patchi.core.brain.type_checker). TypeFixer now handles every language the
478
+ # checker supports, not just TypeScript.
479
+ TYPE_ISSUE_TYPES = frozenset(
480
+ {
481
+ "any_type",
482
+ "dynamic_type",
483
+ "empty_interface",
484
+ "explicit_any",
485
+ "explicit_object",
486
+ "missing_param_type",
487
+ "missing_return_type",
488
+ "missing_type",
489
+ "missing_prop_types",
490
+ "non_null_assertion",
491
+ "trait_object",
492
+ "ts_ignore",
493
+ "unsafe_cast",
494
+ "untyped_return",
495
+ "unwrap_call",
496
+ }
497
+ )
498
+
499
+
500
+ @register
501
+ class TypeFixer(BaseAgent):
502
+ """Multi-language type fixer: explicit any, unsafe casts, missing return types, etc.
503
+
504
+ Handles type-issue findings from every language the type checker supports
505
+ (TypeScript, Python, Go, Java, Kotlin, C#, Dart, PHP, Swift, Rust). The AI
506
+ prompt receives the file's language so the corrected code is idiomatic.
507
+ """
508
+
509
+ name = "TypeFixer"
510
+ group = AgentGroup.FIX
511
+ timeout = 120
512
+
513
+ def _run(self, inp: AgentInput, result: AgentResult) -> None:
514
+ # TypeFixer handles any type-issue finding. The type scanner does not
515
+ # always tag fix_agent, so accept both tagged and untagged type issues.
516
+ type_findings = [
517
+ f
518
+ for f in inp.extra.get("findings", [])
519
+ if f.get("type") in TYPE_ISSUE_TYPES
520
+ and (f.get("fix_agent") == self.name or f.get("fix_agent") in (None, ""))
521
+ ]
522
+
523
+ patches: list[Patch] = []
524
+
525
+ for finding in type_findings[:10]:
526
+ fpath = finding.get("file", "")
527
+ path = inp.root / fpath
528
+ original = _read_file(path)
529
+ if not original:
530
+ continue
531
+
532
+ framework = finding.get("framework", "")
533
+ proposed = _ai_fix(
534
+ inp.config,
535
+ Skill.TYPE_FIX,
536
+ fpath,
537
+ original,
538
+ finding,
539
+ root=inp.root,
540
+ framework=framework,
541
+ )
542
+ if not proposed:
543
+ continue
544
+
545
+ change = FileChange(path=fpath, original=original, proposed=proposed)
546
+ if not change.diff.strip():
547
+ continue
548
+
549
+ blast_radius = compute_blast_radius(fpath, inp.root)
550
+
551
+ patch = _make_patch(
552
+ agent_name=self.name,
553
+ patch_type=PatchType.TYPE_FIX,
554
+ changes=[change],
555
+ description=f"Type fix ({finding.get('type', '')}): {fpath}",
556
+ ai_explanation="Fixed type issue using the project's type system.",
557
+ finding_id=finding.get("type", ""),
558
+ blast_radius=blast_radius,
559
+ )
560
+ patches.append(patch)
561
+ result.data.setdefault("patches", []).append(patch.to_dict())
562
+
563
+ result.data["patch_count"] = len(patches)
564
+ result.ai_calls_made = len(patches)
565
+
566
+
567
+ # ── 7. RefactorAgent ──────────────────────────────────────────────────────────
568
+
569
+
570
+ @register
571
+ class RefactorAgent(BaseAgent):
572
+ """
573
+ Extracts duplicated logic into shared functions.
574
+ Only runs after DuplicateScanner confirms high similarity (>= 85%).
575
+ """
576
+
577
+ name = "RefactorAgent"
578
+ group = AgentGroup.FIX
579
+ timeout = 120
580
+
581
+ def _run(self, inp: AgentInput, result: AgentResult) -> None:
582
+ dup_findings = [
583
+ f
584
+ for f in inp.extra.get("findings", [])
585
+ if f.get("type") == "duplicate_function" and f.get("fix_agent") == self.name
586
+ ]
587
+
588
+ patches: list[Patch] = []
589
+
590
+ for finding in dup_findings[:5]:
591
+ file1 = finding.get("file", "")
592
+ file2 = finding.get("similar_to", "").split(":")[0] if finding.get("similar_to") else ""
593
+
594
+ if not file1 or not file2:
595
+ continue
596
+
597
+ orig1 = _read_file(inp.root / file1)
598
+ orig2 = _read_file(inp.root / file2)
599
+ if not orig1 or not orig2:
600
+ continue
601
+
602
+ score = finding.get("similarity_score", 0)
603
+
604
+ system = get_system_prompt(Skill.REFACTOR)
605
+ user_prompt = build_prompt(
606
+ Skill.REFACTOR,
607
+ {
608
+ "file1_path": file1,
609
+ "file1_content": orig1[:2000],
610
+ "file2_path": file2,
611
+ "file2_content": orig2[:2000],
612
+ "similarity_score": str(score),
613
+ "language": _detect_language(file1),
614
+ },
615
+ )
616
+
617
+ # Log AI call for audit trail (WIRE-04)
618
+ try:
619
+ from patchi.core.security.governance import patchi_action_log
620
+
621
+ patchi_action_log(inp.root, "ai_call", f"{file1}, {file2}", agent="RefactorAgent")
622
+ except Exception as e:
623
+ _log.warning(
624
+ "Failed to write AI-call audit log entry for %s, %s: %s", file1, file2, e
625
+ )
626
+
627
+ response = call_ai(inp.config, system, user_prompt, max_tokens=3000)
628
+ if not response:
629
+ continue
630
+
631
+ blocks = re.findall(
632
+ r"(?://|#)?\s*FILE:\s*([^\n]+)\n```(?:\w+)?\n(.*?)```",
633
+ response,
634
+ re.DOTALL,
635
+ )
636
+ if not blocks:
637
+ blocks = re.findall(
638
+ r"FILE:\s*([^\n]+)\n```(?:\w+)?\n(.*?)```",
639
+ response,
640
+ re.DOTALL,
641
+ )
642
+ if len(blocks) < 2:
643
+ continue
644
+
645
+ changes: list[FileChange] = []
646
+ for fname, proposed_code in blocks:
647
+ fname = fname.strip()
648
+ if fname == file1:
649
+ changes.append(FileChange(path=file1, original=orig1, proposed=proposed_code))
650
+ elif fname == file2:
651
+ changes.append(FileChange(path=file2, original=orig2, proposed=proposed_code))
652
+
653
+ if not changes:
654
+ continue
655
+
656
+ br1 = compute_blast_radius(file1, inp.root)
657
+ br2 = compute_blast_radius(file2, inp.root)
658
+ blast_radius = max(br1, br2)
659
+
660
+ patch = _make_patch(
661
+ agent_name=self.name,
662
+ patch_type=PatchType.REFACTOR,
663
+ changes=changes,
664
+ description=f"Extract shared logic from {file1} and {file2}",
665
+ ai_explanation=f"Functions are {int(score * 100)}% similar. Extracted to shared utility.",
666
+ finding_id=finding.get("type", ""),
667
+ blast_radius=blast_radius,
668
+ agent_certainty=0.8,
669
+ has_test_coverage=False,
670
+ )
671
+ patches.append(patch)
672
+ result.data.setdefault("patches", []).append(patch.to_dict())
673
+
674
+ result.data["patch_count"] = len(patches)
675
+ result.ai_calls_made = len(patches)
676
+
677
+
678
+ # ── 8. UnitTestRunner ─────────────────────────────────────────────────────────
679
+
680
+
681
+ @register
682
+ class UnitTestRunner(BaseAgent):
683
+ """
684
+ Generates test skeletons for uncovered source files.
685
+ Writes to tests/ directory. Never modifies source files.
686
+ """
687
+
688
+ name = "UnitTestRunner"
689
+ group = AgentGroup.FIX
690
+ timeout = 120
691
+
692
+ def _run(self, inp: AgentInput, result: AgentResult) -> None:
693
+ uncovered_findings = [
694
+ f
695
+ for f in inp.extra.get("findings", [])
696
+ if f.get("type") == "uncovered_file" and f.get("fix_agent") == self.name
697
+ ]
698
+
699
+ patches: list[Patch] = []
700
+
701
+ for finding in uncovered_findings[:5]:
702
+ source_path = finding.get("file", "")
703
+ path = inp.root / source_path
704
+ original = _read_file(path)
705
+ if not original:
706
+ continue
707
+
708
+ lang = _detect_language(source_path)
709
+ test_path = self._test_path(source_path, lang)
710
+
711
+ if (inp.root / test_path).exists():
712
+ continue
713
+
714
+ system = get_system_prompt(Skill.TEST_GENERATE)
715
+ user_prompt = build_prompt(
716
+ Skill.TEST_GENERATE,
717
+ {
718
+ "file_path": source_path,
719
+ "file_content": original[:3000],
720
+ "existing_tests_section": "Write tests for all exported functions.",
721
+ "language": lang,
722
+ },
723
+ )
724
+
725
+ # Log AI call for audit trail (WIRE-04)
726
+ try:
727
+ from patchi.core.security.governance import patchi_action_log
728
+
729
+ patchi_action_log(inp.root, "ai_call", source_path, agent="UnitTestRunner")
730
+ except Exception as e:
731
+ _log.warning("Failed to write AI-call audit log entry for %s: %s", source_path, e)
732
+
733
+ response = call_ai(inp.config, system, user_prompt)
734
+ if not response:
735
+ continue
736
+
737
+ proposed = _extract_code_block(response)
738
+ if not proposed.strip():
739
+ continue
740
+
741
+ change = FileChange(path=test_path, original="", proposed=proposed)
742
+
743
+ blast_radius = compute_blast_radius(source_path, inp.root)
744
+
745
+ patch = _make_patch(
746
+ agent_name=self.name,
747
+ patch_type=PatchType.TEST_ADD,
748
+ changes=[change],
749
+ description=f"Add tests for {source_path}",
750
+ ai_explanation="Generated unit test skeleton covering exported functions.",
751
+ finding_id=finding.get("type", ""),
752
+ blast_radius=blast_radius,
753
+ agent_certainty=0.85,
754
+ )
755
+ patches.append(patch)
756
+ result.data.setdefault("patches", []).append(patch.to_dict())
757
+
758
+ result.data["patch_count"] = len(patches)
759
+ result.ai_calls_made = len(patches)
760
+
761
+ def _test_path(self, source_path: str, lang: str) -> str:
762
+ from pathlib import PurePosixPath
763
+
764
+ p = PurePosixPath(source_path)
765
+ stem = p.stem
766
+ parent = str(p.parent)
767
+ if lang == "python":
768
+ test_name = f"test_{stem}.py"
769
+ else:
770
+ ext = p.suffix
771
+ test_name = f"{stem}.test{ext}"
772
+ # Mirror source directory structure under tests/
773
+ if parent and parent != ".":
774
+ return f"tests/{parent}/{test_name}"
775
+ return f"tests/{test_name}"