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,383 @@
1
+ """
2
+ Risk Gate for Patchi.
3
+
4
+ Every patch passes through the risk gate before anything touches disk.
5
+ The gate decides: auto-apply | ask user | block
6
+
7
+ Three modes (from config):
8
+ CONFIRM — every patch requires explicit user approval (Y/N)
9
+ AUTO — patches with risk_score ≤ threshold auto-apply; riskier ones ask
10
+ AUTOPILOT — all patches auto-apply; gate only blocks if contract is broken
11
+
12
+ Risk threshold for AUTO mode: config["risk_threshold"] (default 30).
13
+
14
+ Additional hard blocks (fire regardless of mode):
15
+ 1. App contract not locked — no fix runs until user confirms the contract
16
+ 2. No-touch restriction violated — patch targets a restricted path
17
+ 3. Blast radius report required — config["require_blast_radius_on_high_risk"] = True
18
+ and risk_score ≥ 61 — blast radius must be computed and shown before applying
19
+
20
+ Gate decisions:
21
+ ALLOW_AUTO — apply without asking (low risk + AUTO/AUTOPILOT mode)
22
+ REQUIRE_REVIEW — surface to user for approval (confirm mode OR high risk)
23
+ BLOCK — refuse entirely (contract not locked, restricted path)
24
+
25
+ The gate NEVER raises exceptions. It always returns a GateDecision.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import datetime
31
+ import logging
32
+ from dataclasses import dataclass, field
33
+ from enum import StrEnum
34
+ from pathlib import Path
35
+
36
+ from patchi.core import config as cfg
37
+ from patchi.core import memory as mem
38
+ from patchi.core.constants import Mode, RiskLevel
39
+ from patchi.core.fix.patch import Patch
40
+
41
+ _log = logging.getLogger("patchi.fix.risk_gate")
42
+
43
+ # ── Gate decision ──────────────────────────────────────────────────────────────
44
+
45
+
46
+ class GateDecision(StrEnum):
47
+ ALLOW_AUTO = "allow_auto" # apply immediately
48
+ REQUIRE_REVIEW = "require_review" # surface to user
49
+ BLOCK = "block" # hard stop
50
+
51
+
52
+ @dataclass
53
+ class GateResult:
54
+ decision: GateDecision
55
+ reason: str
56
+ patch_id: str
57
+ risk_score: int
58
+ risk_level: str
59
+ confidence: int
60
+ blast_radius: int
61
+ blocks: list[str] = field(default_factory=list)
62
+ warnings: list[str] = field(default_factory=list)
63
+ requires_blast_report: bool = False
64
+
65
+ @property
66
+ def is_blocked(self) -> bool:
67
+ return self.decision == GateDecision.BLOCK
68
+
69
+ @property
70
+ def is_auto(self) -> bool:
71
+ return self.decision == GateDecision.ALLOW_AUTO
72
+
73
+ @property
74
+ def needs_review(self) -> bool:
75
+ return self.decision == GateDecision.REQUIRE_REVIEW
76
+
77
+ def to_dict(self) -> dict:
78
+ return {
79
+ "decision": self.decision.value,
80
+ "reason": self.reason,
81
+ "patch_id": self.patch_id,
82
+ "risk_score": self.risk_score,
83
+ "risk_level": self.risk_level,
84
+ "confidence": self.confidence,
85
+ "blast_radius": self.blast_radius,
86
+ "blocks": self.blocks,
87
+ "warnings": self.warnings,
88
+ "requires_blast_report": self.requires_blast_report,
89
+ }
90
+
91
+
92
+ # ── Risk gate ──────────────────────────────────────────────────────────────────
93
+
94
+
95
+ class RiskGate:
96
+ """
97
+ Evaluates every patch before it is applied.
98
+
99
+ Usage:
100
+ gate = RiskGate(project_root)
101
+ result = gate.evaluate(patch)
102
+ if result.is_blocked:
103
+ # tell user why
104
+ elif result.is_auto:
105
+ # apply now
106
+ else:
107
+ # surface for review
108
+ """
109
+
110
+ def __init__(self, root: Path):
111
+ self.root = root
112
+ try:
113
+ self._config = cfg.load(root)
114
+ except Exception as e:
115
+ _log.warning(
116
+ "Failed to load config for %s — falling back to defaults (restricted-path "
117
+ "protections may be affected): %s",
118
+ root,
119
+ e,
120
+ )
121
+ self._config = {}
122
+ try:
123
+ self._brain = mem.get_brain(root)
124
+ except Exception as e:
125
+ _log.warning("Failed to load brain data for %s — falling back to empty: %s", root, e)
126
+ self._brain = {}
127
+
128
+ def evaluate(self, patch: Patch) -> GateResult:
129
+ """Run all gate checks and return a decision."""
130
+ blocks: list[str] = []
131
+ warnings: list[str] = []
132
+
133
+ mode = self._get_mode()
134
+ risk_threshold = self._config.get("risk_threshold", 30)
135
+ risk_level = RiskLevel.from_score(patch.risk_score).value
136
+ requires_blast = False
137
+
138
+ # ── Hard blocks ────────────────────────────────────────────────────────
139
+
140
+ # 1. Contract must be locked before any fix runs
141
+ if not self._brain.get("contract_locked"):
142
+ blocks.append(
143
+ "App contract not confirmed. Run 'p scan' and confirm your critical flows first. "
144
+ "No fixes will apply until the contract is locked."
145
+ )
146
+
147
+ # 2. Restricted path check
148
+ restricted = self._restricted_paths()
149
+ for change in patch.changes:
150
+ for path, rtype in restricted:
151
+ if change.path.startswith(path.rstrip("/")):
152
+ if rtype == "sensitive":
153
+ warnings.append(
154
+ f"'{change.path}' is in a sensitive zone ({path}). "
155
+ "Proceed with caution."
156
+ )
157
+ else:
158
+ blocks.append(
159
+ f"'{change.path}' is in a restricted {rtype} zone ({path}). "
160
+ "Modify restrictions with 'p restrict'."
161
+ )
162
+
163
+ # 3. Charter boundary check — agent fixes must not violate guard rails
164
+ try:
165
+ from patchi.core.security.charter import (
166
+ RuleType,
167
+ check_boundary_violations,
168
+ load_charter,
169
+ )
170
+
171
+ charter = load_charter(self.root)
172
+ if charter.rules:
173
+ # Build import edges from proposed changes
174
+ import_edges: list[tuple[str, str]] = []
175
+ for change in patch.changes:
176
+ if change.proposed:
177
+ # Extract import statements from proposed code
178
+ import re as _re
179
+ for m in _re.finditer(
180
+ r'from\s+(\S+)\s+import\s+\w+', change.proposed
181
+ ):
182
+ import_edges.append((change.path, m.group(1)))
183
+ for m in _re.finditer(
184
+ r'import\s+(\S+)', change.proposed
185
+ ):
186
+ import_edges.append((change.path, m.group(1)))
187
+
188
+ # Check boundary rules
189
+ boundary_violations = check_boundary_violations(charter, import_edges)
190
+ for v in boundary_violations:
191
+ blocks.append(
192
+ f"Charter violation [{v.rule_id}]: {v.message}. "
193
+ f"{v.suggestion}"
194
+ )
195
+
196
+ # Check convention rules (file size limits)
197
+ max_lines = 0
198
+ for rule in charter.rules:
199
+ if rule.type == RuleType.CONVENTION and rule.enabled:
200
+ if rule.max_value > 0 and rule.metric == "lines":
201
+ max_lines = rule.max_value
202
+ if max_lines > 0:
203
+ for change in patch.changes:
204
+ if change.path.endswith((".py", ".ts", ".js")):
205
+ new_lines = (
206
+ (change.proposed or "").count("\n") + 1
207
+ )
208
+ if new_lines > max_lines:
209
+ blocks.append(
210
+ f"Charter violation [{rule.id}]: "
211
+ f"{change.path} would be {new_lines} lines "
212
+ f"(max {max_lines}). {rule.description}"
213
+ )
214
+ except Exception as e:
215
+ _log.debug("Charter check skipped: %s", e)
216
+
217
+ # 4. Blast radius report required for high-risk patches
218
+ if (
219
+ self._config.get("require_blast_radius_on_high_risk", True)
220
+ and patch.risk_score >= 61
221
+ and patch.blast_radius == 0
222
+ ):
223
+ blocks.append(
224
+ f"High-risk patch (score {patch.risk_score}) requires a blast radius report. "
225
+ "Patchi is computing it — this patch will be re-evaluated once ready."
226
+ )
227
+ requires_blast = True
228
+
229
+ # ── Warnings ───────────────────────────────────────────────────────────
230
+
231
+ if patch.confidence < 70:
232
+ warnings.append(
233
+ f"Low confidence ({patch.confidence}%). "
234
+ "Patchi is less certain than usual about this fix."
235
+ )
236
+
237
+ if patch.blast_radius > 10:
238
+ warnings.append(
239
+ f"High blast radius: {patch.blast_radius} files depend on the changed file(s). "
240
+ "Thorough testing is recommended."
241
+ )
242
+
243
+ if patch.total_lines_changed > 100:
244
+ warnings.append(
245
+ f"Large patch: {patch.total_lines_changed} lines changed across "
246
+ f"{patch.file_count} file(s)."
247
+ )
248
+
249
+ # Secrets gate: check if proposed code introduces new secrets
250
+ try:
251
+ from patchi.core.security.secrets_guard import gate_check_proposed_code
252
+
253
+ for change in patch.changes:
254
+ if change.proposed:
255
+ safe, secret_findings = gate_check_proposed_code(change.proposed, change.path)
256
+ if not safe:
257
+ blocks.append(
258
+ f"Secrets guard: proposed change to '{change.path}' introduces "
259
+ f"{len(secret_findings)} secret(s). Fix blocked."
260
+ )
261
+ except ImportError:
262
+ pass
263
+
264
+ # ── Determine decision ─────────────────────────────────────────────────
265
+
266
+ if blocks:
267
+ return GateResult(
268
+ decision=GateDecision.BLOCK,
269
+ reason=blocks[0],
270
+ patch_id=patch.id,
271
+ risk_score=patch.risk_score,
272
+ risk_level=risk_level,
273
+ confidence=patch.confidence,
274
+ blast_radius=patch.blast_radius,
275
+ blocks=blocks,
276
+ warnings=warnings,
277
+ requires_blast_report=requires_blast,
278
+ )
279
+
280
+ # Test-weakening guard: a patch that only edits test files (set by the
281
+ # verify loop / fix command) must NEVER auto-apply — it goes to the
282
+ # human review queue regardless of mode, even AUTOPILOT.
283
+ if patch.requires_review:
284
+ return GateResult(
285
+ decision=GateDecision.REQUIRE_REVIEW,
286
+ reason=(
287
+ "Patch only changes test file(s) — flagged for human review "
288
+ "(test-weakening edits are never auto-applied)."
289
+ ),
290
+ patch_id=patch.id,
291
+ risk_score=patch.risk_score,
292
+ risk_level=risk_level,
293
+ confidence=patch.confidence,
294
+ blast_radius=patch.blast_radius,
295
+ blocks=[],
296
+ warnings=warnings + ["Test-weakening patch: only test files changed."],
297
+ requires_blast_report=requires_blast,
298
+ )
299
+
300
+ # Mode-based routing
301
+ if mode == Mode.CONFIRM:
302
+ # Always ask in CONFIRM mode
303
+ decision = GateDecision.REQUIRE_REVIEW
304
+ reason = "CONFIRM mode — every fix requires approval."
305
+
306
+ elif mode == Mode.AUTO:
307
+ if patch.risk_score <= risk_threshold:
308
+ decision = GateDecision.ALLOW_AUTO
309
+ reason = f"AUTO mode — risk score {patch.risk_score} ≤ threshold {risk_threshold}."
310
+ else:
311
+ decision = GateDecision.REQUIRE_REVIEW
312
+ reason = (
313
+ f"AUTO mode — risk score {patch.risk_score} exceeds threshold {risk_threshold}. "
314
+ "Surfaced for review."
315
+ )
316
+
317
+ else: # AUTOPILOT
318
+ decision = GateDecision.ALLOW_AUTO
319
+ reason = "AUTOPILOT mode — all fixes apply automatically."
320
+ if patch.risk_score >= 61:
321
+ warnings.append(
322
+ f"High-risk fix applied automatically (AUTOPILOT). "
323
+ f"Risk score: {patch.risk_score}. Switch to AUTO mode for manual review."
324
+ )
325
+
326
+ # ── Quiet hours check ─────────────────────────────────────────────────
327
+ # In quiet hours, auto-apply is blocked even in AUTO/AUTOPILOT modes
328
+ if decision == GateDecision.ALLOW_AUTO and self._is_quiet_hours():
329
+ decision = GateDecision.REQUIRE_REVIEW
330
+ reason = f"Quiet hours active — auto-apply deferred to {self._config.get('quiet_hours_end', 'end')}."
331
+ warnings.append("Quiet hours: auto-apply blocked. Patch queued for review.")
332
+
333
+ return GateResult(
334
+ decision=decision,
335
+ reason=reason,
336
+ patch_id=patch.id,
337
+ risk_score=patch.risk_score,
338
+ risk_level=risk_level,
339
+ confidence=patch.confidence,
340
+ blast_radius=patch.blast_radius,
341
+ blocks=blocks,
342
+ warnings=warnings,
343
+ requires_blast_report=requires_blast,
344
+ )
345
+
346
+ def _is_quiet_hours(self) -> bool:
347
+ """Check if current local time falls within configured quiet hours."""
348
+ quiet_hours = self._config.get("quiet_hours", {})
349
+ if not quiet_hours.get("enabled", False):
350
+ return False
351
+ start = quiet_hours.get("start", "")
352
+ end = quiet_hours.get("end", "")
353
+ if not start or not end:
354
+ return False
355
+ try:
356
+ now = datetime.datetime.now()
357
+ now_minutes = now.hour * 60 + now.minute
358
+ start_parts = start.split(":")
359
+ end_parts = end.split(":")
360
+ start_minutes = int(start_parts[0]) * 60 + int(start_parts[1])
361
+ end_minutes = int(end_parts[0]) * 60 + int(end_parts[1])
362
+ if start_minutes <= end_minutes:
363
+ return start_minutes <= now_minutes < end_minutes
364
+ return now_minutes >= start_minutes or now_minutes < end_minutes
365
+ except (ValueError, IndexError, AttributeError):
366
+ return False
367
+
368
+ def _get_mode(self) -> Mode:
369
+ try:
370
+ return cfg.get_mode(self.root)
371
+ except Exception as e:
372
+ _log.warning(
373
+ "Failed to read mode from config — defaulting to CONFIRM (safest option): %s", e
374
+ )
375
+ return Mode.CONFIRM
376
+
377
+ def _restricted_paths(self) -> list[tuple[str, str]]:
378
+ restrictions = self._config.get("restrictions", [])
379
+ return [
380
+ (r["path"], r.get("type", "no_touch"))
381
+ for r in restrictions
382
+ if r.get("enabled", True) and r.get("type") in ("no_touch", "scan_only", "sensitive")
383
+ ]
@@ -0,0 +1,93 @@
1
+ """
2
+ SecurityFixer - Patches confirmed vulnerabilities from security scanner agents.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from patchi.core.agents.base import (
8
+ AgentGroup,
9
+ AgentInput,
10
+ AgentResult,
11
+ BaseAgent,
12
+ register,
13
+ )
14
+ from patchi.core.fix.patch import FileChange, Patch, PatchType
15
+
16
+ # Import common functionality from base module
17
+ from .base import _make_patch, _read_file, compute_blast_radius
18
+
19
+
20
+ @register
21
+ class SecurityFixer(BaseAgent):
22
+ """Patches confirmed vulnerabilities from security scanner agents."""
23
+
24
+ name = "SecurityFixer"
25
+ group = AgentGroup.FIX
26
+ timeout = 120
27
+
28
+ def _run(self, inp: AgentInput, result: AgentResult) -> None:
29
+ from patchi.core.ai.prompts import Skill
30
+ from patchi.core.fix.fix_agents import _ai_fix
31
+
32
+ findings = [
33
+ f
34
+ for f in inp.extra.get("findings", [])
35
+ if (f.get("fix_agent") == self.name or f.get("severity") in ("critical", "high"))
36
+ and f.get("type") not in ("hardcoded_secret",)
37
+ ]
38
+
39
+ patches: list[Patch] = []
40
+
41
+ for finding in findings[:5]:
42
+ fpath = finding.get("file", "")
43
+ path = inp.root / fpath
44
+ original = _read_file(path)
45
+ if not original:
46
+ continue
47
+
48
+ framework = finding.get("framework", "")
49
+ blast_radius = 0
50
+ if hasattr(inp, "extra") and "brain_report" in inp.extra:
51
+ brain_report = inp.extra["brain_report"]
52
+ if (
53
+ hasattr(brain_report, "blast_radius_map")
54
+ and fpath in brain_report.blast_radius_map
55
+ ):
56
+ blast_radius = len(brain_report.blast_radius_map[fpath].all_dependents)
57
+ else:
58
+ blast_radius = compute_blast_radius(fpath, inp.root)
59
+
60
+ proposed = _ai_fix(
61
+ inp.config,
62
+ Skill.SECURITY_FIX,
63
+ fpath,
64
+ original,
65
+ finding,
66
+ blast_radius,
67
+ root=inp.root,
68
+ framework=framework,
69
+ )
70
+ if not proposed:
71
+ continue
72
+
73
+ change = FileChange(path=fpath, original=original, proposed=proposed)
74
+ if not change.diff.strip():
75
+ continue
76
+
77
+ cwe = finding.get("cwe", "")
78
+ issue_desc = finding.get("message", "Security vulnerability")
79
+ patch = _make_patch(
80
+ agent_name=self.name,
81
+ patch_type=PatchType.SECURITY,
82
+ changes=[change],
83
+ description=f"Security fix: {issue_desc[:60]}",
84
+ ai_explanation=f"CWE-{cwe}: {issue_desc}",
85
+ finding_id=finding.get("type", ""),
86
+ blast_radius=blast_radius,
87
+ )
88
+ patches.append(patch)
89
+ result.data.setdefault("patches", []).append(patch.to_dict())
90
+
91
+ result.files_scanned = len(findings)
92
+ result.data["patch_count"] = len(patches)
93
+ result.ai_calls_made = len(patches)
@@ -0,0 +1,204 @@
1
+ """
2
+ Verify Loop — apply → re-run the failing test → retry → never weaken tests.
3
+
4
+ Reconstructed module (the original was lost before any commit existed;
5
+ this build follows the exact call contract in cli/commands/fix_cmd.py):
6
+
7
+ outcome = run_verify_loop(patch, root=r, config=config, brain=brain,
8
+ applier=applier, log=callable)
9
+ outcome.review_required # patch only touches tests -> human must review
10
+ outcome.applied # final state applied
11
+ outcome.verified # the originally-failing test now passes
12
+ outcome.retries_used # number of RETRIES after the first attempt
13
+ outcome.reason # why it failed / could not be verified
14
+
15
+ Honesty rules carried over from the fix_cmd contract:
16
+ - A patch whose changes are exclusively in test files is NEVER applied
17
+ automatically (test-weakening guard) — flagged for human review.
18
+ - Verification means the specific failing test passes again, not just
19
+ "the applier said ok".
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import fnmatch
25
+ import logging
26
+ import subprocess
27
+ import sys
28
+ from dataclasses import dataclass
29
+ from pathlib import Path
30
+
31
+ _log = logging.getLogger("patchi.fix.verify_loop")
32
+
33
+ MAX_RETRIES = 2 # attempts after the first one ("up to 2 retries")
34
+ _VERIFY_TIMEOUT_S = 120 # per-attempt cap for the targeted test re-run
35
+
36
+ _TEST_FILE_PATTERNS = (
37
+ "test_*.py",
38
+ "*_test.py",
39
+ "conftest.py",
40
+ "*.spec.ts",
41
+ "*.spec.tsx",
42
+ "*.spec.js",
43
+ "*.spec.jsx",
44
+ "*.test.ts",
45
+ "*.test.tsx",
46
+ "*.test.js",
47
+ "*.test.jsx",
48
+ "*_test.go",
49
+ "*_test.rb",
50
+ "*Test.java",
51
+ "*Test.kt",
52
+ "*Tests.swift",
53
+ )
54
+
55
+ _TEST_DIR_MARKERS = ("test", "tests", "spec", "specs", "__tests__", "testing")
56
+
57
+
58
+ def is_test_path(path: str) -> bool:
59
+ """True if a relative path points into test code."""
60
+ import posixpath
61
+
62
+ p = path.replace("\\", "/")
63
+ name = posixpath.basename(p)
64
+ if any(fnmatch.fnmatch(name, pat) for pat in _TEST_FILE_PATTERNS):
65
+ return True
66
+ dirs = {seg.lower() for seg in posixpath.dirname(p).split("/") if seg}
67
+ return bool(dirs & set(_TEST_DIR_MARKERS))
68
+
69
+
70
+ def should_flag_for_review(patch) -> bool:
71
+ """Test-weakening guard: only-test-file patches always need a human."""
72
+ changes = getattr(patch, "changes", None) or []
73
+ if not changes:
74
+ return False
75
+ return all(is_test_path(getattr(c, "path", "")) for c in changes)
76
+
77
+
78
+ @dataclass
79
+ class VerifyOutcome:
80
+ """Result of one fix → verify → retry campaign."""
81
+
82
+ applied: bool = False
83
+ verified: bool = False
84
+ review_required: bool = False
85
+ patch: object | None = None
86
+ reason: str = ""
87
+ retries_used: int = 0
88
+
89
+
90
+ def _rerun_failing_test(
91
+ root: Path,
92
+ test_file: str,
93
+ log=None,
94
+ ) -> tuple[bool, str]:
95
+ """Independently re-run one test file. Returns (passed, tail_of_output)."""
96
+ tf = Path(test_file)
97
+ target = tf if tf.is_absolute() else root / tf
98
+ if not target.is_file():
99
+ return False, f"cannot re-run: {test_file} not found"
100
+ try:
101
+ proc = subprocess.run( # noqa: S603 — fixed argv, no shell
102
+ [sys.executable, "-m", "pytest", "-x", "-q", str(target)],
103
+ capture_output=True,
104
+ text=True,
105
+ timeout=_VERIFY_TIMEOUT_S,
106
+ cwd=str(root),
107
+ )
108
+ tail = (proc.stdout or "").strip().splitlines()[-6:]
109
+ output = " | ".join(tail)[:300]
110
+ passed = proc.returncode == 0
111
+ if log:
112
+ log(f"re-ran {test_file}: {'PASS' if passed else 'FAIL'}")
113
+ return passed, output
114
+ except subprocess.TimeoutExpired:
115
+ return False, f"test re-run timed out after {_VERIFY_TIMEOUT_S}s"
116
+ except OSError as exc:
117
+ return False, f"could not launch test runner: {exc}"
118
+
119
+
120
+ def recheck_test_file(root: Path, test_file: str) -> dict:
121
+ """Re-run one previously-verified test file (REVERIFY phase helper).
122
+
123
+ Contract used by governor._recheck_applied_patches:
124
+ {"passed": True} — test ran and passed
125
+ {"passed": False} — test ran and failed (regression)
126
+ {"passed": None} — could not run (file missing, runner error, timeout)
127
+ Always includes "output" (tail of runner output) and "error" (message).
128
+ """
129
+ if not test_file or not is_test_path(test_file):
130
+ return {"passed": None, "output": "", "error": f"not a runnable test path: {test_file}"}
131
+
132
+ passed, output = _rerun_failing_test(Path(root), test_file)
133
+ if "cannot re-run" in output or "could not launch" in output or "timed out" in output:
134
+ return {"passed": None, "output": output, "error": output}
135
+ return {"passed": bool(passed), "output": output, "error": "" if passed else output}
136
+
137
+
138
+ def run_verify_loop(
139
+ patch,
140
+ root: Path,
141
+ config: dict | None = None,
142
+ brain=None,
143
+ applier=None,
144
+ log=None,
145
+ ) -> VerifyOutcome:
146
+ """Apply *patch*, re-verify the failing test, retry up to MAX_RETRIES.
147
+
148
+ Never raises: every failure mode becomes an honest VerifyOutcome.
149
+ """
150
+ say = log or (lambda _msg: None)
151
+
152
+ # Guard first: test-only patches go to a human, full stop.
153
+ if should_flag_for_review(patch):
154
+ return VerifyOutcome(review_required=True, patch=patch)
155
+
156
+ if applier is None:
157
+ from patchi.core.fix.applier import PatchApplier
158
+
159
+ applier = PatchApplier(root)
160
+
161
+ source_finding = getattr(patch, "source_finding", {}) or {}
162
+ test_file = source_finding.get("file", "")
163
+
164
+ attempts = 1 + MAX_RETRIES
165
+ last_reason = ""
166
+
167
+ for attempt in range(1, attempts + 1):
168
+ result = applier.apply(patch) # never raises; rolls back on its own
169
+
170
+ if not getattr(result, "success", False):
171
+ last_reason = getattr(result, "error", "") or "apply failed"
172
+ say(f"attempt {attempt}/{attempts} failed to apply: {last_reason}")
173
+ patch.verify_retries = attempt - 1
174
+ continue
175
+
176
+ # Applied — now prove it: the original failing test must pass.
177
+ if test_file and is_test_path(test_file):
178
+ passed, detail = _rerun_failing_test(Path(root), test_file, log=say)
179
+ verified = passed
180
+ reason = "" if passed else f"verification failed: {detail}"
181
+ else:
182
+ verified = bool(getattr(result, "test_passed", False))
183
+ reason = "" if verified else "applied but applier-level tests did not pass"
184
+
185
+ if verified:
186
+ patch.verify_retries = attempt - 1
187
+ return VerifyOutcome(
188
+ applied=True,
189
+ verified=True,
190
+ patch=patch,
191
+ retries_used=attempt - 1,
192
+ )
193
+
194
+ last_reason = reason
195
+ say(f"attempt {attempt}/{attempts} applied but unverified: {reason}")
196
+ patch.verify_retries = attempt - 1
197
+
198
+ return VerifyOutcome(
199
+ applied=False,
200
+ verified=False,
201
+ patch=patch,
202
+ reason=last_reason or "exhausted retries",
203
+ retries_used=min(MAX_RETRIES, attempts - 1),
204
+ )
@@ -0,0 +1,8 @@
1
+ """Fuzzing module — input mutation, sequence reordering, state-space exploration."""
2
+
3
+ from .corpus import FuzzCorpus
4
+ from .input_fuzzer import InputFuzzer
5
+ from .sequence_fuzzer import SequenceFuzzer
6
+ from .state_fuzzer import StateFuzzer
7
+
8
+ __all__ = ["InputFuzzer", "SequenceFuzzer", "StateFuzzer", "FuzzCorpus"]