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,184 @@
1
+ """
2
+ AppLauncher — install + start + health + teardown for Playwright/attack.
3
+
4
+ Detect via FrameworkDetector + project_reader, install if plain, start on free port,
5
+ poll GET /health or / 200, log to .patchi/launcher/app.log, atexit teardown.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import atexit
11
+ import logging
12
+ import os
13
+ import shutil
14
+ import socket
15
+ import subprocess
16
+ import time
17
+ from pathlib import Path
18
+
19
+ from patchi.core.brain.file_corpus import FileCorpus
20
+ from patchi.core.brain.framework import FrameworkDetector
21
+
22
+ _log = logging.getLogger("patchi.testing.launcher")
23
+
24
+ _PROC: subprocess.Popen | None = None
25
+ _PORT: int | None = None
26
+
27
+
28
+ def _free_port() -> int:
29
+ s = socket.socket()
30
+ s.bind(("", 0))
31
+ _, port = s.getsockname()
32
+ s.close()
33
+ return port
34
+
35
+
36
+ def _port_free(port: int) -> bool:
37
+ s = socket.socket()
38
+ try:
39
+ s.bind(("127.0.0.1", port))
40
+ return True
41
+ except OSError:
42
+ return False
43
+ finally:
44
+ s.close()
45
+
46
+
47
+ def _preferred_port(root: Path) -> int | None:
48
+ """Configured port chain: .patchi/config.json → env PORT → package.json dev -p."""
49
+ try:
50
+ from patchi.core.testing._browser import read_web_port
51
+
52
+ p = read_web_port(root)
53
+ if p and 1 <= p <= 65535:
54
+ return p
55
+ except Exception as _exc:
56
+ _log.debug('suppressed: %s', _exc)
57
+ try:
58
+ env_p = int(os.environ.get("PORT", ""))
59
+ if 1 <= env_p <= 65535:
60
+ return env_p
61
+ except (TypeError, ValueError):
62
+ pass
63
+ try:
64
+ import json
65
+ import re
66
+
67
+ pkg = json.loads((root / "package.json").read_text(encoding="utf-8"))
68
+ dev = (pkg.get("scripts") or {}).get("dev", "")
69
+ m = re.search(r"(?:-p|--port)[= ](\d{2,5})", dev)
70
+ if m and 1 <= int(m.group(1)) <= 65535:
71
+ return int(m.group(1))
72
+ except Exception as _exc:
73
+ _log.debug('suppressed: %s', _exc)
74
+ return None
75
+
76
+
77
+ def _detect_start_cmd(root: Path) -> list[str] | None:
78
+ corpus = FileCorpus(root)
79
+ stack = FrameworkDetector(root, corpus=corpus).detect()
80
+ fws = {f.name.lower() for f in stack.frameworks}
81
+ # package.json scripts
82
+ if (root / "package.json").exists():
83
+ try:
84
+ import json
85
+
86
+ pkg = json.loads((root / "package.json").read_text(encoding="utf-8"))
87
+ scripts = pkg.get("scripts", {})
88
+ if "dev" in scripts:
89
+ return ["npm", "run", "dev"]
90
+ if "start" in scripts:
91
+ return ["npm", "start"]
92
+ except Exception as _exc:
93
+ _log.debug('suppressed: %s', _exc)
94
+ if "fastapi" in fws:
95
+ # find app module
96
+ for cand in ["app.main:app", "main:app", "server:app"]:
97
+ return ["uvicorn", cand, "--host", "127.0.0.1", "--port", "{port}"]
98
+ if "flask" in fws:
99
+ return ["flask", "run", "--host=127.0.0.1", "--port={port}"]
100
+ if "django" in fws:
101
+ return ["python", "manage.py", "runserver", "127.0.0.1:{port}"]
102
+ if "next.js" in fws:
103
+ return ["npm", "run", "dev", "--", "-p", "{port}"]
104
+ if "go" in fws and shutil.which("go"):
105
+ return ["go", "run", "."]
106
+ if "rust" in fws and shutil.which("cargo"):
107
+ return ["cargo", "run"]
108
+ return None
109
+
110
+
111
+ def ensure_running(root: Path, config: dict | None = None, extra: dict | None = None) -> str | None:
112
+ global _PROC, _PORT
113
+ from patchi.core.testing._browser import find_server
114
+
115
+ # Reuse if already running
116
+ base = find_server(root, config or {}, extra or {})
117
+ if base:
118
+ return base
119
+
120
+ if os.environ.get("PATCHI_LAUNCHER_DISABLE") == "1":
121
+ return None
122
+
123
+ # Side install check: if node_modules missing and package-lock exists, npm ci already via InstallAgent
124
+ cmd = _detect_start_cmd(root)
125
+ if not cmd or not cmd[0] or not shutil.which(cmd[0]):
126
+ return None
127
+
128
+ preferred = _preferred_port(root)
129
+ port = preferred if preferred and _port_free(preferred) else _free_port()
130
+ # replace {port}
131
+ cmd = [str(c).format(port=port) for c in cmd]
132
+ env = os.environ.copy()
133
+ env["PORT"] = str(port)
134
+ env["HOST"] = "127.0.0.1"
135
+
136
+ log_dir = root / ".patchi" / "launcher"
137
+ log_dir.mkdir(parents=True, exist_ok=True)
138
+ log_path = log_dir / "app.log"
139
+ try:
140
+ _PROC = subprocess.Popen(cmd, cwd=str(root), env=env, stdout=log_path.open("w", encoding="utf-8"), stderr=subprocess.STDOUT)
141
+ _PORT = port
142
+ atexit.register(stop)
143
+ # poll health
144
+ import httpx
145
+
146
+ url = f"http://127.0.0.1:{port}"
147
+ for _ in range(15):
148
+ time.sleep(2)
149
+ if _PROC.poll() is not None:
150
+ _log.warning("launcher exited early code %s", _PROC.returncode)
151
+ return None
152
+ try:
153
+ resp = httpx.get(f"{url}/health", timeout=2, follow_redirects=True)
154
+ if resp.status_code < 500:
155
+ return url
156
+ except Exception as _exc:
157
+ _log.debug('suppressed: %s', _exc)
158
+ try:
159
+ resp = httpx.get(url, timeout=2)
160
+ if resp.status_code < 500:
161
+ return url
162
+ except Exception as _exc:
163
+ _log.debug('suppressed: %s', _exc)
164
+ # fallback: try anyway if process still alive
165
+ if _PROC.poll() is None:
166
+ return url
167
+ except Exception as exc: # noqa: BLE001
168
+ _log.warning("launcher failed %s: %s", cmd, exc)
169
+ return None
170
+
171
+
172
+ def stop() -> None:
173
+ global _PROC, _PORT
174
+ if _PROC and _PROC.poll() is None:
175
+ try:
176
+ _PROC.terminate()
177
+ _PROC.wait(timeout=5)
178
+ except Exception:
179
+ try:
180
+ _PROC.kill()
181
+ except Exception as _exc:
182
+ _log.debug('suppressed: %s', _exc)
183
+ _PROC = None
184
+ _PORT = None
@@ -0,0 +1,516 @@
1
+ """
2
+ BrowserTestAgent — Playwright tests for critical flows.
3
+
4
+ Runs Playwright tests for:
5
+ - Login flows
6
+ - Checkout processes
7
+ - Form submissions
8
+ - API calls through the UI
9
+ - Error states
10
+ - Navigation paths
11
+ - Responsive design checks
12
+
13
+ Can use existing Playwright tests or generate new ones based on the app contract.
14
+ Captures screenshots on failures and reports console/network errors.
15
+
16
+ Does NOT write to disk (unless creating new tests).
17
+ May call AI for generating new test scripts.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import logging
23
+ import subprocess
24
+ import sys
25
+ import time
26
+ from pathlib import Path
27
+
28
+ from ..agents.base import (
29
+ AgentDomain,
30
+ AgentGroup,
31
+ AgentInput,
32
+ AgentResult,
33
+ AgentStatus,
34
+ BaseAgent,
35
+ Severity,
36
+ make_finding,
37
+ register,
38
+ )
39
+ from ..ai.client import call_ai
40
+ from ..ai.prompts import Skill, build_prompt, get_system_prompt
41
+
42
+ _log = logging.getLogger("patchi.testing.browser_test_agent")
43
+
44
+
45
+ @register
46
+ class BrowserTestAgent(BaseAgent):
47
+ """Agent for running browser tests with Playwright."""
48
+
49
+ group = AgentGroup.TEST
50
+ domain = AgentDomain.TESTING
51
+ name = "BrowserTestAgent"
52
+ description = "Playwright tests for critical flows: login, checkout, forms, navigation"
53
+
54
+ def _run(self, inp: AgentInput, result: AgentResult) -> None:
55
+ """Run Playwright tests for critical user flows."""
56
+ start_time = time.time()
57
+ findings = []
58
+
59
+ # Check if Playwright is available
60
+ try:
61
+ import playwright # noqa: F401 — used to check availability
62
+ except ImportError:
63
+ findings.append(
64
+ make_finding(
65
+ severity=Severity.INFO,
66
+ file="__playwright__",
67
+ line_start=0,
68
+ title="Playwright not installed",
69
+ description="Playwright is required for browser tests. Install with: pip install playwright",
70
+ evidence="Playwright not found in environment",
71
+ )
72
+ )
73
+ result.status = AgentStatus.SKIPPED
74
+ result.findings = findings
75
+ result.data.update({"playwright_available": False, "needs_ai": False})
76
+ return
77
+
78
+ # ── Live probe against a running app (opt-in) ─────────────────────────
79
+ # Real navigation through the shared browser pool: captures console
80
+ # errors, page crashes, 5xx responses and screenshots of failures.
81
+ # Opt-in via extra["live_probe"]=True (web Live Tests tab sets it) so
82
+ # plain test runs never launch a browser implicitly.
83
+ base_url = (inp.extra or {}).get("base_url")
84
+ wants_probe = bool((inp.extra or {}).get("live_probe"))
85
+ if base_url and wants_probe:
86
+ probe = self._live_probe(inp, base_url)
87
+ if probe:
88
+ result.data["live_probe"] = probe
89
+ for pf in probe.get("findings", []):
90
+ findings.append(pf)
91
+
92
+ # Check for existing Playwright tests
93
+ existing_tests = self._find_existing_playwright_tests(inp.root)
94
+
95
+ if existing_tests:
96
+ # Run existing tests
97
+ test_results = self._run_existing_tests(existing_tests, inp.root)
98
+ else:
99
+ # Check if we have an app contract to generate tests
100
+ app_contract = self._get_app_contract(inp)
101
+ if app_contract:
102
+ # Generate new tests based on app contract
103
+ generated_tests = self._generate_tests_from_contract(app_contract, inp.root)
104
+ test_results = self._run_generated_tests(generated_tests, inp.root)
105
+ else:
106
+ findings.append(
107
+ make_finding(
108
+ severity=Severity.INFO,
109
+ file="__browser_tests__",
110
+ line_start=0,
111
+ title="No browser tests available",
112
+ description="No existing Playwright tests found and no app contract to generate tests",
113
+ evidence="Need either existing tests or confirmed app contract",
114
+ )
115
+ )
116
+ result.status = AgentStatus.SKIPPED
117
+ result.findings = findings
118
+ result.data.update({"tests_run": 0, "needs_ai": True})
119
+ return
120
+
121
+ # Process test results
122
+ if test_results["success"]:
123
+ findings.append(
124
+ make_finding(
125
+ severity=Severity.INFO,
126
+ file="__browser_summary__",
127
+ line_start=0,
128
+ title=f"Browser Tests Passed: {test_results['passed']}/{test_results['total']}",
129
+ description=f"Browser tests passed: {test_results['passed']}/{test_results['total']} tests",
130
+ evidence=f"Passed: {test_results['passed']}, Failed: {test_results['failed']}",
131
+ )
132
+ )
133
+ else:
134
+ findings.append(
135
+ make_finding(
136
+ severity=Severity.HIGH,
137
+ file="__browser_summary__",
138
+ line_start=0,
139
+ title=f"Browser Tests Failed: {test_results['failed']}/{test_results['total']}",
140
+ description=f"Browser tests failed: {test_results['failed']}/{test_results['total']} tests",
141
+ evidence=f"Passed: {test_results['passed']}, Failed: {test_results['failed']}",
142
+ )
143
+ )
144
+
145
+ # Add individual test results
146
+ for test in test_results.get("test_details", []):
147
+ if test.get("status") == "FAILED":
148
+ findings.append(
149
+ make_finding(
150
+ severity=Severity.HIGH,
151
+ file=test.get("flow", "__unknown__"),
152
+ line_start=0,
153
+ title=f"Browser Test Failure: {test['name']}",
154
+ description=test.get("error", "Unknown error"),
155
+ evidence=f"Flow: {test['name']}\nError: {test.get('error', 'N/A')}\nScreenshot: {test.get('screenshot', 'N/A')}",
156
+ )
157
+ )
158
+ elif test.get("status") == "PASSED":
159
+ findings.append(
160
+ make_finding(
161
+ severity=Severity.INFO,
162
+ file=test.get("flow", "__unknown__"),
163
+ line_start=0,
164
+ title=f"Browser Test Passed: {test['name']}",
165
+ description="Browser test passed successfully",
166
+ evidence=f"Flow: {test['name']}",
167
+ )
168
+ )
169
+
170
+ duration = time.time() - start_time
171
+
172
+ result.status = AgentStatus.SUCCEEDED if test_results["success"] else AgentStatus.FAILED
173
+ result.findings = findings
174
+ result.data.update(
175
+ {
176
+ "total_tests": test_results["total"],
177
+ "passed_tests": test_results["passed"],
178
+ "failed_tests": test_results["failed"],
179
+ "duration": round(duration, 2),
180
+ "needs_ai": True, # Generating new tests requires AI
181
+ }
182
+ )
183
+ return
184
+
185
+ # ── Live probe (shared browser pool) ──────────────────────────────────────
186
+
187
+ def _live_probe(self, inp: AgentInput, base_url: str) -> dict | None:
188
+ """Navigate the running app with a pooled browser.
189
+
190
+ Collects per page: console errors, uncaught page errors, HTTP >= 500
191
+ responses, and a screenshot whenever navigation or rendering fails.
192
+ Findings are capped so one noisy app can't flood the report.
193
+ """
194
+ import asyncio
195
+
196
+ try:
197
+ return asyncio.run(self._probe_async(inp, base_url))
198
+ except Exception as e:
199
+ _log.warning("BrowserTestAgent live probe failed: %s", e)
200
+ return None
201
+
202
+ async def _probe_async(self, inp: AgentInput, base_url: str) -> dict:
203
+ from .live_v2.browser_pool import get_browser_pool
204
+
205
+ pool = await get_browser_pool()
206
+ page = await pool.get_page()
207
+
208
+ console_errors: list[dict] = []
209
+ http_errors: list[dict] = []
210
+ nav_failures: list[dict] = []
211
+ screenshots: list[str] = []
212
+
213
+ artifacts = inp.root / ".patchi" / "artifacts" / "browser"
214
+ artifacts.mkdir(parents=True, exist_ok=True)
215
+
216
+ def _on_console(msg):
217
+ if msg.type == "error":
218
+ console_errors.append({"page": page.url, "text": msg.text[:200]})
219
+
220
+ def _on_pageerror(err):
221
+ console_errors.append({"page": page.url, "text": f"pageerror: {err}"[:200]})
222
+
223
+ def _on_response(resp):
224
+ if resp.status >= 500:
225
+ http_errors.append({"page": page.url, "status": resp.status, "url": resp.url[:200]})
226
+
227
+ page.on("console", _on_console)
228
+ page.on("pageerror", _on_pageerror)
229
+ page.on("response", _on_response)
230
+
231
+ urls = [base_url]
232
+ for route in (inp.brain or {}).get("routes", [])[:6]:
233
+ path = route.get("path") if isinstance(route, dict) else getattr(route, "path", "")
234
+ method = (
235
+ route.get("method") if isinstance(route, dict) else getattr(route, "method", "get")
236
+ ) or "get"
237
+ if (
238
+ str(method).lower() in ("get", "")
239
+ and path
240
+ and not path.startswith(("api/", "/api"))
241
+ ):
242
+ urls.append(base_url.rstrip("/") + ("/" + path.lstrip("/")))
243
+ seen = set()
244
+ urls = [u for u in urls if not (u in seen or seen.add(u))][:5]
245
+
246
+ findings_payload: list[dict] = []
247
+ try:
248
+ for url in urls:
249
+ try:
250
+ await page.goto(url, wait_until="networkidle", timeout=15000)
251
+ except Exception as e:
252
+ slug = "nav-failure"
253
+ shot = artifacts / f"{slug}-{len(screenshots)}.png"
254
+ try:
255
+ await page.screenshot(path=str(shot), full_page=False)
256
+ screenshots.append(shot.name)
257
+ except Exception as _exc:
258
+ _log.warning('_probe_async failed: %s', _exc)
259
+ nav_failures.append({"url": url, "error": str(e)[:160]})
260
+ continue
261
+ finally:
262
+ await pool.release_page(page)
263
+
264
+ # Cap noise, convert to finding payloads (agent-level severity mapping)
265
+ for ce in console_errors[:10]:
266
+ findings_payload.append(
267
+ {
268
+ "kind": "console_error",
269
+ "severity": "medium",
270
+ "page": ce["page"],
271
+ "detail": ce["text"],
272
+ }
273
+ )
274
+ for he in http_errors[:10]:
275
+ findings_payload.append(
276
+ {
277
+ "kind": "http_5xx",
278
+ "severity": "high",
279
+ "page": he.get("page", ""),
280
+ "detail": f"{he['status']} on {he['url']}",
281
+ }
282
+ )
283
+ for nf in nav_failures:
284
+ findings_payload.append(
285
+ {
286
+ "kind": "navigation_failure",
287
+ "severity": "high",
288
+ "page": nf["url"],
289
+ "detail": nf["error"],
290
+ }
291
+ )
292
+
293
+ return {
294
+ "base_url": base_url,
295
+ "pages_probed": len(urls),
296
+ "console_errors": len(console_errors),
297
+ "http_errors": len(http_errors),
298
+ "nav_failures": len(nav_failures),
299
+ "screenshots": screenshots,
300
+ "findings": findings_payload,
301
+ }
302
+
303
+ def _find_existing_playwright_tests(self, root: Path) -> list[Path]:
304
+ """Find existing Playwright test files."""
305
+ test_files = []
306
+
307
+ # Look for common Playwright test locations
308
+ playwright_patterns = [
309
+ "tests/e2e/**/*.py",
310
+ "tests/e2e/**/*.js",
311
+ "tests/e2e/**/*.ts",
312
+ "e2e/**/*.py",
313
+ "e2e/**/*.js",
314
+ "e2e/**/*.ts",
315
+ "tests/playwright/**/*.py",
316
+ "tests/playwright/**/*.js",
317
+ "tests/playwright/**/*.ts",
318
+ "**/playwright*.js",
319
+ "**/playwright*.ts",
320
+ "**/playwright*.py",
321
+ ]
322
+
323
+ for pattern in playwright_patterns:
324
+ for test_file in root.rglob(pattern):
325
+ if test_file.is_file():
326
+ test_files.append(test_file)
327
+
328
+ return test_files
329
+
330
+ def _get_app_contract(self, inp: AgentInput) -> list[dict] | None:
331
+ """Get the app contract with critical flows."""
332
+ try:
333
+ from .. import memory as mem
334
+
335
+ brain = mem.get_brain(inp.root)
336
+ if brain and "confirmed_flows" in brain:
337
+ return brain["confirmed_flows"]
338
+ return None
339
+ except Exception as e:
340
+ _log.warning("BrowserTestAgent._get_app_contract failed: %s", e)
341
+ return None
342
+
343
+ def _generate_tests_from_contract(self, app_contract: list[dict], root: Path) -> list[Path]:
344
+ """Generate Playwright tests based on the app contract using AI."""
345
+ generated: list[Path] = []
346
+ from .. import config as cfg
347
+
348
+ config = cfg.load(root)
349
+ if not config:
350
+ return generated
351
+
352
+ flows_text = "\n".join(
353
+ f"- {f.get('name', '?')}: {f.get('description', '')}" for f in app_contract
354
+ )
355
+
356
+ system = get_system_prompt(Skill.TEST_GENERATE)
357
+ user_prompt = build_prompt(
358
+ Skill.TEST_GENERATE,
359
+ {
360
+ "file_path": "browser/critical_flows.py",
361
+ "file_content": f"# App Contract Critical Flows\n{flows_text}",
362
+ "existing_tests_section": "Generate Playwright (Python) end-to-end tests for each critical flow.",
363
+ "language": "python",
364
+ },
365
+ )
366
+
367
+ response = call_ai(config, system, user_prompt, max_tokens=4000)
368
+ if not response:
369
+ return generated
370
+
371
+ import re
372
+
373
+ blocks = re.findall(r"```(?:\w+)?\n(.*?)```", response, re.DOTALL)
374
+ out_dir = root / ".patchi" / "tests" / "browser"
375
+ out_dir.mkdir(parents=True, exist_ok=True)
376
+
377
+ for i, block in enumerate(blocks):
378
+ test_path = out_dir / f"generated_flow_{i}.py"
379
+ test_path.write_text(block, encoding="utf-8")
380
+ generated.append(test_path)
381
+
382
+ return generated
383
+
384
+ def _run_existing_tests(self, test_files: list[Path], root: Path) -> dict:
385
+ """Run existing Playwright tests."""
386
+ if not test_files:
387
+ return {
388
+ "success": True,
389
+ "total": 0,
390
+ "passed": 0,
391
+ "failed": 0,
392
+ "test_details": [],
393
+ }
394
+
395
+ try:
396
+ # Try to run with pytest-playwright
397
+ cmd = (
398
+ [sys.executable, "-m", "pytest"]
399
+ + [str(tf.relative_to(root)) for tf in test_files]
400
+ + ["-v"]
401
+ )
402
+ result = subprocess.run(
403
+ cmd, cwd=root, capture_output=True, text=True, timeout=300
404
+ ) # 5 min timeout
405
+
406
+ if result.returncode in [0, 1]: # 0 = all passed, 1 = some failed
407
+ return self._parse_pytest_playwright_output(result.stdout + result.stderr)
408
+ else:
409
+ return {
410
+ "success": False,
411
+ "error": f"Test execution failed: {result.stderr}",
412
+ "total": 0,
413
+ "passed": 0,
414
+ "failed": 0,
415
+ "test_details": [],
416
+ }
417
+ except subprocess.TimeoutExpired:
418
+ return {
419
+ "success": False,
420
+ "error": "Browser tests timed out after 300 seconds",
421
+ "total": 0,
422
+ "passed": 0,
423
+ "failed": 0,
424
+ "test_details": [],
425
+ }
426
+ except Exception as e:
427
+ return {
428
+ "success": False,
429
+ "error": f"Error running existing tests: {str(e)}",
430
+ "total": 0,
431
+ "passed": 0,
432
+ "failed": 0,
433
+ "test_details": [],
434
+ }
435
+
436
+ def _run_generated_tests(self, test_files: list[Path], root: Path) -> dict:
437
+ """Run generated Playwright tests."""
438
+ if not test_files:
439
+ return {"success": True, "total": 0, "passed": 0, "failed": 0, "test_details": []}
440
+ try:
441
+ import subprocess
442
+ import sys
443
+
444
+ targets = [str(tf.relative_to(root)) for tf in test_files]
445
+ cmd = [sys.executable, "-m", "pytest"] + targets + ["-v", "--tb=short"]
446
+ proc = subprocess.run(cmd, cwd=root, capture_output=True, text=True, timeout=300)
447
+ output = f"{proc.stdout}\n{proc.stderr}"
448
+ passed = failed = 0
449
+ for line in output.splitlines():
450
+ if "PASSED" in line:
451
+ passed += 1
452
+ elif "FAILED" in line:
453
+ failed += 1
454
+ return {
455
+ "success": proc.returncode == 0,
456
+ "total": passed + failed,
457
+ "passed": passed,
458
+ "failed": failed,
459
+ "test_details": [],
460
+ }
461
+ except subprocess.TimeoutExpired:
462
+ return {"success": False, "total": 0, "passed": 0, "failed": 0, "error": "Timeout"}
463
+ except Exception as e:
464
+ return {"success": False, "total": 0, "passed": 0, "failed": 0, "error": str(e)}
465
+
466
+ def _parse_pytest_playwright_output(self, output: str) -> dict:
467
+ """Parse pytest output with Playwright tests."""
468
+ lines = output.split("\n")
469
+ total = 0
470
+ passed = 0
471
+ failed = 0
472
+ test_details = []
473
+
474
+ current_test = None
475
+ in_failure_section = False
476
+
477
+ for line in lines:
478
+ if "PASSED" in line and "::" in line:
479
+ # Example: "test_login.py::test_valid_login PASSED"
480
+ test_name = line.split()[0]
481
+ passed += 1
482
+ test_details.append({"name": test_name, "status": "PASSED", "flow": test_name})
483
+ elif "FAILED" in line and "::" in line:
484
+ # Example: "test_login.py::test_invalid_login FAILED"
485
+ test_name = line.split()[0]
486
+ failed += 1
487
+ in_failure_section = True
488
+ current_test = test_name
489
+ test_details.append(
490
+ {
491
+ "name": test_name,
492
+ "status": "FAILED",
493
+ "flow": test_name,
494
+ "error": "Test failed",
495
+ }
496
+ )
497
+ elif in_failure_section and line.strip().startswith("_"):
498
+ in_failure_section = False
499
+ elif in_failure_section and current_test:
500
+ # Capture error details
501
+ for detail in test_details:
502
+ if detail["name"] == current_test and detail["status"] == "FAILED":
503
+ if "error" in detail:
504
+ detail["error"] += f"\n{line.strip()}"
505
+ else:
506
+ detail["error"] = line.strip()
507
+
508
+ total = passed + failed
509
+
510
+ return {
511
+ "success": failed == 0,
512
+ "total": total or 1, # Default to 1 if parsing failed
513
+ "passed": passed,
514
+ "failed": failed,
515
+ "test_details": test_details,
516
+ }