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,510 @@
1
+ """
2
+ Base functionality for fix agents.
3
+ """
4
+
5
+ import logging
6
+ import os
7
+ import re
8
+ from pathlib import Path
9
+ from typing import Callable
10
+
11
+ from patchi.core.fix.patch import (
12
+ FileChange,
13
+ Patch,
14
+ PatchState,
15
+ PatchType,
16
+ compute_confidence,
17
+ compute_risk_score,
18
+ )
19
+
20
+ _log = logging.getLogger("patchi.fix")
21
+
22
+
23
+ def _call_ai(
24
+ prompt: str,
25
+ config: dict,
26
+ max_tokens: int = 1500,
27
+ system_prompt: str = "",
28
+ progress_callback: Callable[[str], None] | None = None
29
+ ) -> str:
30
+ if os.environ.get("PATCHI_OFFLINE"):
31
+ return ""
32
+ """
33
+ Call the configured AI model with a prompt.
34
+ Delegates to the canonical client. Uses skill-specific system prompt when provided.
35
+ """
36
+ from patchi.core.ai.client import call_ai
37
+
38
+ sys_prompt = system_prompt or "You are a helpful coding assistant."
39
+ result = call_ai(config, sys_prompt, prompt, max_tokens, progress_callback=progress_callback)
40
+ return result or ""
41
+
42
+
43
+ def _read_file(path: Path) -> str:
44
+ try:
45
+ return path.read_text(encoding="utf-8", errors="replace")
46
+ except OSError:
47
+ return ""
48
+
49
+
50
+ def _extract_code_block(text: str) -> str:
51
+ """Extract the first ```...``` block from an AI response."""
52
+ m = re.search(r"```(?:\w+)?\n(.*?)```", text, re.DOTALL)
53
+ return m.group(1) if m else text.strip()
54
+
55
+
56
+ def _make_patch(
57
+ agent_name: str,
58
+ patch_type: PatchType,
59
+ changes: list[FileChange],
60
+ description: str,
61
+ ai_explanation: str,
62
+ finding_id: str = "",
63
+ blast_radius: int = 0,
64
+ agent_certainty: float = 0.9,
65
+ has_test_coverage: bool = True,
66
+ scanner_agent: str = "",
67
+ source_finding: dict | None = None,
68
+ ) -> Patch:
69
+ risk = compute_risk_score(changes, blast_radius)
70
+ conf = compute_confidence(changes, agent_certainty, has_test_coverage=has_test_coverage)
71
+ return Patch(
72
+ agent=agent_name,
73
+ patch_type=patch_type,
74
+ finding_id=finding_id,
75
+ changes=changes,
76
+ description=description,
77
+ ai_explanation=ai_explanation,
78
+ risk_score=risk,
79
+ confidence=conf,
80
+ blast_radius=blast_radius,
81
+ state=PatchState.PROPOSED,
82
+ scanner_agent=scanner_agent,
83
+ source_finding=source_finding or {},
84
+ verify_retries=2,
85
+ )
86
+
87
+
88
+ _SOURCE_EXTENSIONS = {
89
+ ".py",
90
+ ".js",
91
+ ".jsx",
92
+ ".mjs",
93
+ ".cjs",
94
+ ".ts",
95
+ ".tsx",
96
+ ".mts",
97
+ ".java",
98
+ ".kt",
99
+ ".kts",
100
+ ".go",
101
+ ".rs",
102
+ ".rb",
103
+ ".php",
104
+ ".cs",
105
+ ".swift",
106
+ ".dart",
107
+ ".c",
108
+ ".cpp",
109
+ ".cxx",
110
+ ".cc",
111
+ ".h",
112
+ ".hpp",
113
+ ".scala",
114
+ ".svelte",
115
+ ".vue",
116
+ }
117
+
118
+
119
+ def compute_blast_radius(file_path: str, root: Path) -> int:
120
+ """Count how many files in the project import this file."""
121
+ rel = file_path
122
+ stem = Path(rel).stem
123
+ count = 0
124
+ for src in root.rglob("*"):
125
+ if src.suffix not in _SOURCE_EXTENSIONS or not src.is_file():
126
+ continue
127
+ try:
128
+ content = src.read_text(encoding="utf-8", errors="ignore")
129
+ if rel in content or stem in content:
130
+ count += 1
131
+ except OSError:
132
+ pass
133
+ return count
134
+
135
+
136
+ # Domain-specific fix patterns: finding_type → (search_regex, replacement)
137
+ _FIX_PATTERNS: list[tuple[str, str, str]] = [
138
+ # Secrets
139
+ (
140
+ r'(?:password|secret|api_key|token)\s*=\s*["\'][^"\']+["\']',
141
+ "hardcoded_secret",
142
+ "Use os.environ.get() or a secrets manager instead of hardcoded values",
143
+ ),
144
+ # Debug mode
145
+ (
146
+ r"DEBUG\s*=\s*True",
147
+ "debug_mode",
148
+ "DEBUG = os.environ.get('DEBUG', 'false').lower() == 'true'",
149
+ ),
150
+ # SQL injection
151
+ (
152
+ r"execute\(.*%s.*\%",
153
+ "sqli",
154
+ "Use parameterized queries: cursor.execute('SELECT * FROM users WHERE id = %s', (user_id,))",
155
+ ),
156
+ # Missing HTTPS redirect
157
+ (
158
+ r"@app\.route.*methods.*GET",
159
+ "missing_header",
160
+ "Add @app.before_request to redirect HTTP to HTTPS",
161
+ ),
162
+ # Weak hash
163
+ (r"hashlib\.(md5|sha1)\(", "weak_hash", "Use hashlib.sha256() or hashlib.sha3_256() instead"),
164
+ # Eval/exec
165
+ (
166
+ r"\b(eval|exec)\s*\(",
167
+ "injection",
168
+ "Avoid eval()/exec(); use ast.literal_eval() or safe parsing",
169
+ ),
170
+ # Pickle deserialization
171
+ (
172
+ r"pickle\.loads?\s*\(",
173
+ "injection",
174
+ "Use json.loads() or a safe serialization format instead of pickle",
175
+ ),
176
+ # CORS wildcard
177
+ (
178
+ r"Access-Control-Allow-Origin.*\*",
179
+ "cors_wildcard",
180
+ "Restrict CORS to specific trusted origins instead of wildcard (*)",
181
+ ),
182
+ # Missing rate limiting
183
+ (r"@app\.route", "missing_rate_limit", "Add Flask-Limiter or similar rate limiting middleware"),
184
+ ]
185
+
186
+
187
+ def _find_vulnerable_line(
188
+ content: str, finding_type: str, file_path: str
189
+ ) -> tuple[int, str] | None:
190
+ """Find the specific line that needs fixing based on finding type."""
191
+ import re
192
+
193
+ lines = content.splitlines()
194
+ for search_re, ftype, _ in _FIX_PATTERNS:
195
+ if ftype == finding_type or ftype in finding_type:
196
+ for i, line in enumerate(lines):
197
+ if re.search(search_re, line):
198
+ return (i, line)
199
+ return None
200
+
201
+
202
+ def _apply_fix_pattern(line: str, finding_type: str) -> str | None:
203
+ """Apply a fix pattern to a specific line."""
204
+ import re
205
+
206
+ if "hardcoded_secret" in finding_type or "password" in finding_type:
207
+ # Replace hardcoded value with env var lookup
208
+ m = re.search(r'(\w+)\s*=\s*["\']([^"\']+)["\']', line)
209
+ if m:
210
+ var_name = m.group(1)
211
+ indent = line[: len(line) - len(line.lstrip())]
212
+ return f'{indent}{var_name} = os.environ.get("{var_name.upper()}", "")'
213
+
214
+ elif "debug_mode" in finding_type:
215
+ indent = line[: len(line) - len(line.lstrip())]
216
+ return f"{indent}DEBUG = os.environ.get('DEBUG', 'false').lower() == 'true'"
217
+
218
+ elif "weak_hash" in finding_type:
219
+ return line.replace("hashlib.md5(", "hashlib.sha256(").replace(
220
+ "hashlib.sha1(", "hashlib.sha256("
221
+ )
222
+
223
+ elif "eval" in finding_type or "exec" in finding_type:
224
+ indent = line[: len(line) - len(line.lstrip())]
225
+ return f"{indent}# SECURITY: Replaced eval/exec with safe alternative\n{line}"
226
+
227
+ elif "pickle" in finding_type:
228
+ return line.replace("pickle.loads(", "json.loads(").replace("pickle.load(", "json.load(")
229
+
230
+ elif "cors_wildcard" in finding_type:
231
+ return line.replace("*", "os.environ.get('ALLOWED_ORIGINS', '').split(',')")
232
+
233
+ return None
234
+
235
+
236
+ def _build_fix_prompt(
237
+ finding_type: str,
238
+ file_path: str,
239
+ original: str,
240
+ line_num: int,
241
+ suggestion: str,
242
+ playbook: dict | None,
243
+ message: str = "",
244
+ ) -> str:
245
+ """Build the AI prompt for fix generation."""
246
+ # Extract context around the vulnerable line + caller hint via Understander/body_tags
247
+ lines = original.splitlines()
248
+ start = max(0, line_num - 5) if line_num else 0
249
+ end = min(len(lines), line_num + 10) if line_num else min(20, len(lines))
250
+ context = "\n".join(f"{i + 1:4d} | {line}" for i, line in enumerate(lines[start:end], start))
251
+ caller_hint = ""
252
+ try:
253
+ from pathlib import Path as _Pth
254
+
255
+ _root = _Pth.cwd()
256
+ for cand in [Path.cwd(), Path(".")]:
257
+ if (cand / ".patchi").exists():
258
+ _root = cand.resolve()
259
+ break
260
+ from patchi.core.brain.body_tags import load_body_tags
261
+ from patchi.core.brain.understander import Understander
262
+
263
+ _tags = load_body_tags(_root)
264
+ if _tags and file_path in _tags:
265
+ tag = _tags[file_path]
266
+ caller_hint = f"\nBody: role={tag.get('role')} fan_in={tag.get('fan_in')} score={tag.get('score')} layer={tag.get('layer')}\n"
267
+ # add function block
268
+ _u = Understander(_root, [], _tags, {}, [])
269
+ caller_hint += _u.function_at(file_path, line_num)[:1200]
270
+ except Exception:
271
+ caller_hint = ""
272
+
273
+ prompt = f"""You are a security expert. Fix the following vulnerability.
274
+
275
+ ## Finding
276
+ - Type: {finding_type}
277
+ - File: {file_path}
278
+ - Line: {line_num or "unknown"}
279
+ - Message: {message or suggestion or finding_type}
280
+ """
281
+
282
+ if playbook:
283
+ prompt += f"- Control ID: {playbook.get('control_id', 'unknown')}\n"
284
+ prompt += f"- Fix strategy: {playbook.get('fix_strategy', 'unknown')}\n"
285
+ if playbook.get("llm_template"):
286
+ prompt += f"- Template hint: {playbook['llm_template']}\n"
287
+
288
+ prompt += f"""
289
+ ## Code Context (lines {start + 1}-{end})
290
+ ```python
291
+ {context}
292
+ ```
293
+ {caller_hint}
294
+ ```
295
+
296
+ ## Requirements
297
+ 1. Fix the vulnerability with minimal changes
298
+ 2. Preserve the original code style and indentation
299
+ 3. Do NOT add comments explaining the fix
300
+ 4. Do NOT change unrelated code
301
+ 5. Output ONLY the fixed line(s), no explanation
302
+
303
+ ## Fixed code
304
+ ```python
305
+ """
306
+
307
+ return prompt
308
+
309
+
310
+ def _generate_fix_with_ai(
311
+ root: Path,
312
+ file_path: str,
313
+ original: str,
314
+ finding_type: str,
315
+ line_num: int,
316
+ suggestion: str,
317
+ playbook: dict | None,
318
+ config: dict,
319
+ message: str = "",
320
+ progress_callback: Callable[[str], None] | None = None,
321
+ ) -> FileChange | None:
322
+ """Use AI to generate a fix for the vulnerability."""
323
+ prompt = _build_fix_prompt(
324
+ finding_type, file_path, original, line_num, suggestion, playbook, message
325
+ )
326
+
327
+ system_prompt = (
328
+ "You are a security engineer fixing vulnerabilities in Python/JS/Go code. "
329
+ "Output only the fixed code in a code block. No explanations. "
330
+ "Make minimal changes to fix the security issue."
331
+ )
332
+
333
+ try:
334
+ response = _call_ai(prompt, config, max_tokens=500, system_prompt=system_prompt, progress_callback=progress_callback)
335
+ except Exception as e:
336
+ _log.warning("AI fix generation failed: %s", e)
337
+ return None
338
+
339
+ if not response:
340
+ return None
341
+
342
+ # Extract the fixed code from AI response
343
+ fixed_code = _extract_code_block(response)
344
+ if not fixed_code or fixed_code.strip() == "":
345
+ return None
346
+
347
+ # Find the vulnerable line to replace
348
+ lines = original.splitlines()
349
+ if line_num and 0 < line_num <= len(lines):
350
+ original_line = lines[line_num - 1]
351
+ # Use the first non-empty line from the AI response
352
+ fixed_lines = [line for line in fixed_code.strip().splitlines() if line.strip()]
353
+ if fixed_lines:
354
+ proposed_line = fixed_lines[0]
355
+ # Preserve original indentation
356
+ indent = len(original_line) - len(original_line.lstrip())
357
+ proposed_line = " " * indent + proposed_line.strip()
358
+ if proposed_line != original_line:
359
+ return FileChange(
360
+ path=file_path,
361
+ original=original_line,
362
+ proposed=proposed_line,
363
+ )
364
+
365
+ return None
366
+
367
+
368
+ def generate_fix(
369
+ root: Path,
370
+ finding_dict: dict,
371
+ config: dict | None = None,
372
+ progress_callback: Callable[[str], None] | None = None,
373
+ ) -> Patch | None:
374
+ """Generate a fix patch for a finding.
375
+
376
+ Strategy:
377
+ 1. AI-powered fix generation (calls LLM with file context)
378
+ 2. Pattern-based fix (regex replacements)
379
+ 3. Playbook template fix
380
+ 4. Suggestion-based fallback
381
+
382
+ The generated patch passes through RiskGate for mode-based approval.
383
+ """
384
+ config = config or {}
385
+ suggestion = finding_dict.get("suggestion", "")
386
+ file_path = finding_dict.get("file", "")
387
+ finding_type = finding_dict.get("type", "unknown")
388
+ line_num = finding_dict.get("line", 0)
389
+ playbook = finding_dict.get("playbook")
390
+ message = finding_dict.get("message", "")
391
+
392
+ if not file_path:
393
+ return None
394
+
395
+ target = root / file_path
396
+ if not target.is_file():
397
+ return None
398
+
399
+ try:
400
+ original = target.read_text(encoding="utf-8", errors="ignore")
401
+ except OSError:
402
+ return None
403
+
404
+ lines = original.splitlines()
405
+ changes: list[FileChange] = []
406
+ ai_fix_used = False
407
+
408
+ # Strategy 1: AI-powered fix generation
409
+ ai_change = _generate_fix_with_ai(
410
+ root,
411
+ file_path,
412
+ original,
413
+ finding_type,
414
+ line_num,
415
+ suggestion,
416
+ playbook,
417
+ config,
418
+ message,
419
+ progress_callback,
420
+ )
421
+ if ai_change:
422
+ changes.append(ai_change)
423
+ ai_fix_used = True
424
+
425
+ # Strategy 2: Pattern-based fix (fallback)
426
+ if not changes:
427
+ found = _find_vulnerable_line(original, finding_type, file_path)
428
+ if found:
429
+ line_idx, old_line = found
430
+ fixed_line = _apply_fix_pattern(old_line, finding_type)
431
+ if fixed_line and fixed_line != old_line:
432
+ changes.append(
433
+ FileChange(
434
+ path=file_path,
435
+ original=old_line,
436
+ proposed=fixed_line,
437
+ )
438
+ )
439
+
440
+ # Strategy 3: Playbook template fix (fallback)
441
+ if not changes and playbook and playbook.get("llm_template"):
442
+ control_id = playbook.get("control_id", finding_type)
443
+ found = _find_vulnerable_line(original, control_id, file_path)
444
+ if found:
445
+ line_idx, old_line = found
446
+ fixed_line = _apply_fix_pattern(old_line, control_id)
447
+ if fixed_line and fixed_line != old_line:
448
+ changes.append(
449
+ FileChange(
450
+ path=file_path,
451
+ original=old_line,
452
+ proposed=fixed_line,
453
+ )
454
+ )
455
+
456
+ # Strategy 4: Line-number targeted fix (fallback)
457
+ if not changes and line_num and 0 < line_num <= len(lines):
458
+ old_line = lines[line_num - 1]
459
+ fixed_line = _apply_fix_pattern(old_line, finding_type)
460
+ if fixed_line and fixed_line != old_line:
461
+ changes.append(
462
+ FileChange(
463
+ path=file_path,
464
+ original=old_line,
465
+ proposed=fixed_line,
466
+ )
467
+ )
468
+
469
+ # Strategy 5: Suggestion-based fallback (last resort)
470
+ if not changes and suggestion:
471
+ changes.append(
472
+ FileChange(
473
+ path=file_path,
474
+ original=lines[0] if lines else "",
475
+ proposed=f"# TODO: {suggestion}\n{lines[0] if lines else ''}",
476
+ )
477
+ )
478
+
479
+ if not changes:
480
+ return None
481
+
482
+ # Build the patch
483
+ patch = _make_patch(
484
+ agent_name="auto_fixer",
485
+ patch_type=PatchType.SECURITY,
486
+ changes=changes,
487
+ description=f"{'AI-generated' if ai_fix_used else 'Pattern-based'} fix for {finding_type}",
488
+ ai_explanation=suggestion or message or f"Fixed {finding_type} in {file_path}",
489
+ finding_id=finding_type,
490
+ blast_radius=compute_blast_radius(file_path, root),
491
+ agent_certainty=0.8 if ai_fix_used else 0.6,
492
+ source_finding=finding_dict,
493
+ )
494
+
495
+ # Pass through RiskGate
496
+ try:
497
+ from patchi.core.fix.risk_gate import RiskGate
498
+
499
+ gate = RiskGate(root)
500
+ gate_result = gate.evaluate(patch)
501
+ _log.info(
502
+ "RiskGate for %s: %s (risk=%d)",
503
+ finding_type,
504
+ "auto" if gate_result.is_auto else "blocked" if gate_result.is_blocked else "review",
505
+ patch.risk_score,
506
+ )
507
+ except Exception as e:
508
+ _log.warning("RiskGate evaluation failed: %s", e)
509
+
510
+ return patch
@@ -0,0 +1,169 @@
1
+ """
2
+ CodeFixer - Surgical line-level bug and error patches.
3
+ Handles: null/undefined errors, missing error handling, off-by-one, type mismatch.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import textwrap
10
+ from pathlib import Path
11
+
12
+ from patchi.core.agents.base import (
13
+ AgentGroup,
14
+ AgentInput,
15
+ AgentResult,
16
+ BaseAgent,
17
+ Finding,
18
+ register,
19
+ )
20
+ from patchi.core.debug import debug_context_from_finding
21
+ from patchi.core.fix.patch import FileChange, Patch, PatchType
22
+
23
+ # Import common functionality from base module
24
+ from .base import _call_ai, _extract_code_block, _make_patch, _read_file, compute_blast_radius
25
+ from .fix_agents import _detect_language
26
+
27
+
28
+ @register
29
+ class CodeFixer(BaseAgent):
30
+ """
31
+ Surgical line-level bug and error patches.
32
+ Handles: null/undefined errors, missing error handling, off-by-one, type mismatch.
33
+ """
34
+
35
+ name = "CodeFixer"
36
+ group = AgentGroup.FIX
37
+ timeout = 120
38
+
39
+ def _run(self, inp: AgentInput, result: AgentResult) -> None:
40
+ findings = self._get_findings(
41
+ inp, ["parse_error", "unprotected_sensitive_route", "bug", "test_failure"]
42
+ )
43
+ patches: list[Patch] = []
44
+ debug_cache: dict[tuple[str, str], dict | None] = {}
45
+
46
+ for finding in findings[:10]:
47
+ ftype = finding.get("type", "")
48
+ finding_file = finding.get("file", "")
49
+
50
+ cache_key = (str(inp.root), finding_file)
51
+ if cache_key not in debug_cache:
52
+ debug_cache[cache_key] = debug_context_from_finding(finding, inp.root)
53
+ dbg = debug_cache[cache_key]
54
+
55
+ target_file = finding_file
56
+ if ftype == "test_failure" and dbg:
57
+ frames = dbg.get("frames") or []
58
+ ti = dbg.get("trigger_frame", 0)
59
+ if 0 <= ti < len(frames):
60
+ cand = frames[ti].get("path", "")
61
+ if cand:
62
+ p = Path(cand)
63
+ if not p.is_absolute():
64
+ p = inp.root / p
65
+ p = p.resolve()
66
+ try:
67
+ p.relative_to(inp.root.resolve())
68
+ if p.is_file():
69
+ # Normalise to forward slashes so patch paths stay
70
+ # consistent across platforms (the rest of the
71
+ # pipeline stores relative paths with '/' separators).
72
+ target_file = p.relative_to(inp.root).as_posix()
73
+ except ValueError:
74
+ pass
75
+
76
+ fpath = target_file
77
+ path = inp.root / fpath
78
+ original = _read_file(path)
79
+ if not original:
80
+ continue
81
+
82
+ blast_radius = 0
83
+ if hasattr(inp, "extra") and "brain_report" in inp.extra:
84
+ brain_report = inp.extra["brain_report"]
85
+ if (
86
+ hasattr(brain_report, "blast_radius_map")
87
+ and fpath in brain_report.blast_radius_map
88
+ ):
89
+ blast_radius = len(brain_report.blast_radius_map[fpath].all_dependents)
90
+ else:
91
+ blast_radius = compute_blast_radius(fpath, inp.root)
92
+
93
+ lang = _detect_language(fpath)
94
+ debug_section = ""
95
+ if dbg:
96
+ debug_section = f"\n\nRUNTIME STATE AT FAILURE:\n{json.dumps(dbg, indent=2)}"
97
+
98
+ test_context = ""
99
+ if ftype == "test_failure" and finding_file:
100
+ test_path = inp.root / finding_file
101
+ test_content = _read_file(test_path)
102
+ if test_content:
103
+ test_context = (
104
+ f"\n\nFailing test file ({finding_file}):\n```\n{test_content[:2000]}\n```"
105
+ )
106
+
107
+ prompt = textwrap.dedent(f"""
108
+ Fix this issue in the file below. Return ONLY the corrected file content
109
+ inside a code block. Do not explain. Do not add comments. Make the
110
+ minimal change needed.
111
+
112
+ Language: {lang}
113
+ Issue: {finding.get("message", "")}
114
+ File: {fpath}
115
+ Line: {finding.get("line", 0)}
116
+ Suggestion: {finding.get("suggestion", "")}
117
+ {test_context}
118
+
119
+ File content:
120
+ ```
121
+ {original[:3000]}
122
+ ```
123
+ {debug_section}
124
+
125
+ Return the full corrected file inside triple backticks.
126
+ """).strip()
127
+
128
+ ai_response = _call_ai(prompt, inp.config, progress_callback=inp.on_ai_progress)
129
+ if not ai_response:
130
+ continue
131
+
132
+ proposed = _extract_code_block(ai_response)
133
+ if proposed == original or not proposed.strip():
134
+ continue
135
+
136
+ change = FileChange(path=fpath, original=original, proposed=proposed)
137
+ if not change.diff.strip():
138
+ continue
139
+
140
+ patch = _make_patch(
141
+ agent_name=self.name,
142
+ patch_type=PatchType.BUG_FIX,
143
+ changes=[change],
144
+ description=f"Fix: {finding.get('message', '')[:80]}",
145
+ ai_explanation=finding.get("suggestion", ""),
146
+ finding_id=finding.get("type", ""),
147
+ blast_radius=blast_radius,
148
+ # Only default the scanner to UnitTestAgent for test_failure
149
+ # findings — for other types an empty scanner keeps the
150
+ # applier's M-16 _verify_fix on its generic fallback path
151
+ # instead of re-running the test suite inside apply().
152
+ scanner_agent=(
153
+ "UnitTestAgent" if ftype == "test_failure" else finding.get("agent", "")
154
+ ),
155
+ source_finding=finding,
156
+ )
157
+ patches.append(patch)
158
+ result.data.setdefault("patches", []).append(patch.to_dict())
159
+
160
+ result.files_scanned = len(findings)
161
+ result.data["patch_count"] = len(patches)
162
+ result.ai_calls_made = len(patches)
163
+
164
+ def _get_findings(self, inp: AgentInput, types: list[str]) -> list[Finding]:
165
+ # Fix agents receive findings via extra dict (set by the fix command)
166
+ all_findings = inp.extra.get("findings", [])
167
+ return [
168
+ f for f in all_findings if f.get("type") in types and f.get("fix_agent") == self.name
169
+ ]