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,304 @@
1
+ """
2
+ LiveTestRunner — Orchestrates all test agents and streams results live.
3
+
4
+ Manages the full test lifecycle:
5
+ 1. Discovers which agents to run based on test type
6
+ 2. Executes agents sequentially (or parallel for independent ones)
7
+ 3. Streams real-time results via WebSocket events
8
+ 4. Generates summary report
9
+ 5. Saves results to memory for historical tracking
10
+
11
+ This is the bridge between the CLI test command, web UI, and test agents.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import asyncio
17
+ import logging
18
+ import time
19
+ from collections.abc import Callable
20
+ from dataclasses import dataclass, field
21
+ from pathlib import Path
22
+
23
+ from ..agents.base import AgentGroup, AgentInput, AgentResult, AgentStatus
24
+
25
+ _log = logging.getLogger("patchi.testing.live_test_runner")
26
+
27
+
28
+ @dataclass
29
+ class TestRunConfig:
30
+ """Configuration for a test run."""
31
+
32
+ test_types: list[str] = field(default_factory=lambda: ["unit", "regression"])
33
+ area: str | None = None
34
+ base_url: str | None = None
35
+ parallel: bool = False
36
+ timeout_per_agent: int = 120
37
+ on_event: Callable | None = None # async callback for live streaming
38
+
39
+
40
+ @dataclass
41
+ class TestRunResult:
42
+ """Aggregated result of a full test run."""
43
+
44
+ started_at: float = 0
45
+ completed_at: float = 0
46
+ total_duration_ms: int = 0
47
+ agents_run: list[str] = field(default_factory=list)
48
+ results: list[AgentResult] = field(default_factory=list)
49
+ total_findings: int = 0
50
+ total_passed: int = 0
51
+ total_failed: int = 0
52
+ summary: dict = field(default_factory=dict)
53
+
54
+ def to_dict(self) -> dict:
55
+ return {
56
+ "started_at": self.started_at,
57
+ "completed_at": self.completed_at,
58
+ "total_duration_ms": self.total_duration_ms,
59
+ "agents_run": self.agents_run,
60
+ "total_findings": self.total_findings,
61
+ "total_passed": self.total_passed,
62
+ "total_failed": self.total_failed,
63
+ "summary": self.summary,
64
+ "results": [r.to_dict() for r in self.results],
65
+ }
66
+
67
+
68
+ # Map of test type names to agent class names
69
+ TEST_TYPE_MAP = {
70
+ "unit": ["UnitTestAgent"],
71
+ "regression": ["RegressionAgent"],
72
+ "browser": ["BrowserTestAgent"],
73
+ "stress": ["StressTestAgent"],
74
+ "accessibility": ["UIAccessibilityAgent"],
75
+ "api": ["APIContractAgent"],
76
+ "buttons": ["UIButtonAgent"],
77
+ "layout": ["UILayoutAgent"],
78
+ "e2e": ["E2EFlowAgent"],
79
+ "visual": ["VisualRegressionAgent"],
80
+ "smoke": ["UIButtonAgent", "UILayoutAgent", "UIAccessibilityAgent"],
81
+ "full": [
82
+ "UnitTestAgent",
83
+ "RegressionAgent",
84
+ "UIButtonAgent",
85
+ "UIAccessibilityAgent",
86
+ "UILayoutAgent",
87
+ "E2EFlowAgent",
88
+ "VisualRegressionAgent",
89
+ ],
90
+ }
91
+
92
+
93
+ class LiveTestRunner:
94
+ """Orchestrates test execution and streams live results."""
95
+
96
+ def __init__(self, root: Path):
97
+ self.root = root
98
+ self._event_queue: asyncio.Queue | None = None
99
+ self._event_callback: Callable | None = None
100
+
101
+ async def run(self, config: TestRunConfig) -> TestRunResult:
102
+ """Execute a full test run with live event streaming."""
103
+ # Import test agents to trigger registration
104
+ import patchi.core.testing.test_agents # noqa
105
+ import patchi.core.testing.ui_button_agent # noqa
106
+ import patchi.core.testing.ui_accessibility_agent # noqa
107
+ import patchi.core.testing.ui_layout_agent # noqa
108
+ import patchi.core.testing.e2e_flow_agent # noqa
109
+ import patchi.core.testing.visual_regression_agent # noqa
110
+
111
+ from ..agents.base import get_agent
112
+
113
+ result = TestRunResult(started_at=time.time())
114
+
115
+ # Resolve which agents to run
116
+ agent_names = self._resolve_agents(config.test_types)
117
+ result.agents_run = agent_names
118
+
119
+ # Load brain and config
120
+ from .. import config as cfg
121
+ from .. import memory as mem
122
+
123
+ brain = mem.get_brain(self.root)
124
+ try:
125
+ app_config = cfg.load(self.root)
126
+ except Exception as e:
127
+ _log.warning("LiveTestRunner.run failed: %s", e)
128
+ app_config = {}
129
+
130
+ # Determine scope
131
+ scope = []
132
+ if config.area:
133
+ area_path = self.root / config.area
134
+ if area_path.is_dir():
135
+ scope = [str(p.relative_to(self.root)) for p in area_path.rglob("*.py")]
136
+
137
+ # Fire start event
138
+ await self._emit(
139
+ "test_run_started",
140
+ {
141
+ "agents": agent_names,
142
+ "test_types": config.test_types,
143
+ "area": config.area,
144
+ },
145
+ )
146
+
147
+ # Run agents
148
+ for agent_name in agent_names:
149
+ agent_cls = get_agent(agent_name)
150
+ if not agent_cls:
151
+ continue
152
+
153
+ await self._emit(
154
+ "agent_started",
155
+ {
156
+ "agent": agent_name,
157
+ "description": agent_cls.description
158
+ if hasattr(agent_cls, "description")
159
+ else "",
160
+ },
161
+ )
162
+
163
+ t0 = time.monotonic()
164
+ try:
165
+ inp = AgentInput(
166
+ root=self.root,
167
+ scope=scope,
168
+ brain=brain,
169
+ config=app_config,
170
+ extra={"base_url": config.base_url} if config.base_url else {},
171
+ )
172
+ agent = agent_cls()
173
+ agent_result = await asyncio.to_thread(agent.run, inp)
174
+ except Exception as e:
175
+ agent_result = AgentResult(
176
+ agent_name=agent_name,
177
+ agent_group=AgentGroup.TEST,
178
+ status=AgentStatus.FAILED,
179
+ )
180
+ agent_result.add_error(str(e))
181
+
182
+ agent_result.duration_ms = int((time.monotonic() - t0) * 1000)
183
+ result.results.append(agent_result)
184
+
185
+ # Count findings
186
+ result.total_findings += agent_result.finding_count
187
+
188
+ # Count passed/failed from suite data
189
+ suite = agent_result.data.get("suite", {})
190
+ result.total_passed += suite.get("passed", 0)
191
+ result.total_failed += suite.get("failed", 0)
192
+
193
+ # Fire agent done event
194
+ await self._emit(
195
+ "agent_done",
196
+ {
197
+ "agent": agent_name,
198
+ "status": agent_result.status.value,
199
+ "finding_count": agent_result.finding_count,
200
+ "duration_ms": agent_result.duration_ms,
201
+ "passed": suite.get("passed", 0),
202
+ "failed": suite.get("failed", 0),
203
+ },
204
+ )
205
+
206
+ # Fire individual findings
207
+ for finding in agent_result.findings:
208
+ await self._emit(
209
+ "agent_finding",
210
+ {
211
+ "agent": agent_name,
212
+ "severity": finding.severity.value,
213
+ "type": finding.type,
214
+ "message": finding.message,
215
+ "file": finding.file,
216
+ },
217
+ )
218
+
219
+ result.completed_at = time.time()
220
+ result.total_duration_ms = int((result.completed_at - result.started_at) * 1000)
221
+
222
+ # Build summary
223
+ result.summary = self._build_summary(result)
224
+
225
+ # Fire completion event
226
+ await self._emit("test_run_completed", result.summary)
227
+
228
+ # Save to memory
229
+ self._save_results(result)
230
+
231
+ return result
232
+
233
+ def _resolve_agents(self, test_types: list[str]) -> list[str]:
234
+ """Resolve test type names to agent class names."""
235
+ agents = []
236
+ for t in test_types:
237
+ t_lower = t.lower()
238
+ if t_lower in TEST_TYPE_MAP:
239
+ agents.extend(TEST_TYPE_MAP[t_lower])
240
+ else:
241
+ # Try direct agent name
242
+ from ..agents.base import get_agent
243
+
244
+ if get_agent(t):
245
+ agents.append(t)
246
+ return list(dict.fromkeys(agents)) # dedupe preserving order
247
+
248
+ def _build_summary(self, result: TestRunResult) -> dict:
249
+ """Build a summary of the test run."""
250
+ by_status = {}
251
+ by_severity = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0}
252
+
253
+ for r in result.results:
254
+ status = r.status.value
255
+ by_status[status] = by_status.get(status, 0) + 1
256
+ for f in r.findings:
257
+ sev = f.severity.value
258
+ by_severity[sev] = by_severity.get(sev, 0) + 1
259
+
260
+ all_passed = by_status.get("failed", 0) == 0 and by_status.get("skipped", 0) == 0
261
+
262
+ return {
263
+ "total_agents": len(result.results),
264
+ "by_status": by_status,
265
+ "by_severity": by_severity,
266
+ "total_findings": result.total_findings,
267
+ "total_passed": result.total_passed,
268
+ "total_failed": result.total_failed,
269
+ "all_passed": all_passed,
270
+ "duration_ms": result.total_duration_ms,
271
+ }
272
+
273
+ def _save_results(self, result: TestRunResult) -> None:
274
+ """Save test results to memory for historical tracking."""
275
+ try:
276
+ from .. import memory as mem
277
+
278
+ scan_data = {
279
+ "status": "done" if result.summary.get("all_passed") else "failed",
280
+ "duration_ms": result.total_duration_ms,
281
+ "finding_count": result.total_findings,
282
+ "passed": result.total_passed,
283
+ "failed": result.total_failed,
284
+ "agents": result.agents_run,
285
+ "summary": result.summary,
286
+ }
287
+ mem.save_scan_result("TestRunner", scan_data, self.root)
288
+ except Exception as e:
289
+ _log.warning("LiveTestRunner._save_results failed: %s", e)
290
+
291
+ async def _emit(self, event: str, data: dict) -> None:
292
+ """Emit a live event for WebSocket streaming."""
293
+ if self._event_queue:
294
+ await self._event_queue.put({"event": event, "data": data, "ts": time.time()})
295
+
296
+ if self._event_callback:
297
+ try:
298
+ await self._event_callback(event, data)
299
+ except Exception as e:
300
+ _log.debug("LiveTestRunner._emit failed: %s", e)
301
+
302
+ def set_event_callback(self, callback: Callable) -> None:
303
+ """Set an async callback for live event streaming."""
304
+ self._event_callback = callback
@@ -0,0 +1 @@
1
+ """Live testing v2: browser pool, stress orchestration, screenshots, video recording."""
@@ -0,0 +1,349 @@
1
+ """
2
+ Browser Pool — Manages browser instances for parallel testing.
3
+
4
+ Features:
5
+ - Pool of reusable browser contexts
6
+ - Automatic lifecycle management
7
+ - Resource limits (max browsers, max pages per browser)
8
+ - Health checks and auto-recovery
9
+ - Support for Chrome, Firefox, WebKit
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import asyncio
15
+ import logging
16
+ import time
17
+ from dataclasses import dataclass, field
18
+ from typing import Any
19
+
20
+ _log = logging.getLogger("patchi.testing.browser_pool")
21
+
22
+
23
+ @dataclass
24
+ class BrowserInstance:
25
+ """A single browser instance in the pool."""
26
+
27
+ id: str
28
+ browser_type: str # "chromium", "firefox", "webkit"
29
+ created_at: float
30
+ last_used: float
31
+ page_count: int = 0
32
+ healthy: bool = True
33
+ _browser: Any = None # Playwright browser
34
+ _context: Any = None # Browser context
35
+
36
+
37
+ @dataclass
38
+ class BrowserConfig:
39
+ """Configuration for browser pool."""
40
+
41
+ max_browsers: int = 5
42
+ max_pages_per_browser: int = 10
43
+ browser_type: str = "chromium" # chromium, firefox, webkit
44
+ headless: bool = True
45
+ viewport: dict = field(default_factory=lambda: {"width": 1280, "height": 720})
46
+ default_timeout: int = 30000 # ms
47
+ launch_args: list[str] = field(
48
+ default_factory=lambda: [
49
+ "--no-sandbox",
50
+ "--disable-setuid-sandbox",
51
+ "--disable-dev-shm-usage",
52
+ "--disable-gpu",
53
+ ]
54
+ )
55
+ # Stealth settings
56
+ stealth: bool = False
57
+ user_agent: str | None = None
58
+
59
+
60
+ class BrowserPool:
61
+ """
62
+ Manages a pool of browser instances for parallel test execution.
63
+
64
+ Usage:
65
+ pool = BrowserPool(config)
66
+ await pool.initialize()
67
+
68
+ # Get a page for testing
69
+ page = await pool.get_page()
70
+ try:
71
+ await page.goto("https://example.com")
72
+ # ... test actions
73
+ finally:
74
+ await pool.release_page(page)
75
+
76
+ await pool.shutdown()
77
+ """
78
+
79
+ def __init__(self, config: BrowserConfig = None):
80
+ self.config = config or BrowserConfig()
81
+ self._browsers: dict[str, BrowserInstance] = {}
82
+ self._playwright = None
83
+ self._initialized = False
84
+ self._lock = asyncio.Lock()
85
+ self._stats = {
86
+ "total_created": 0,
87
+ "total_reused": 0,
88
+ "total_failed": 0,
89
+ "current_active": 0,
90
+ }
91
+
92
+ async def initialize(self):
93
+ """Initialize the browser pool."""
94
+ if self._initialized:
95
+ return
96
+
97
+ try:
98
+ from playwright.async_api import async_playwright
99
+
100
+ self._playwright = await async_playwright().start()
101
+ self._initialized = True
102
+ _log.info(f"Browser pool initialized ({self.config.browser_type})")
103
+ except ImportError:
104
+ _log.error(
105
+ "Playwright not installed. "
106
+ "Install with: pip install playwright && playwright install"
107
+ )
108
+ raise
109
+ except Exception as e:
110
+ _log.error(f"Failed to initialize browser pool: {e}")
111
+ raise
112
+
113
+ async def get_page(self) -> Any:
114
+ """Get a browser page from the pool."""
115
+ async with self._lock:
116
+ # Try to reuse existing browser
117
+ for browser in self._browsers.values():
118
+ if browser.healthy and browser.page_count < self.config.max_pages_per_browser:
119
+ browser.page_count += 1
120
+ browser.last_used = time.time()
121
+ self._stats["total_reused"] += 1
122
+ self._stats["current_active"] += 1
123
+ return await browser._context.new_page()
124
+
125
+ # Create new browser if under limit
126
+ if len(self._browsers) < self.config.max_browsers:
127
+ return await self._create_browser_and_page()
128
+
129
+ # Wait for available browser
130
+ while True:
131
+ for browser in self._browsers.values():
132
+ if browser.healthy and browser.page_count < self.config.max_pages_per_browser:
133
+ browser.page_count += 1
134
+ browser.last_used = time.time()
135
+ self._stats["total_reused"] += 1
136
+ self._stats["current_active"] += 1
137
+ return await browser._context.new_page()
138
+ await asyncio.sleep(0.1)
139
+
140
+ async def _create_browser_and_page(self) -> Any:
141
+ """Create a new browser instance and return a page."""
142
+ browser_type = getattr(self._playwright, self.config.browser_type)
143
+
144
+ # Launch browser
145
+ browser = await browser_type.launch(
146
+ headless=self.config.headless,
147
+ args=self.config.launch_args,
148
+ )
149
+
150
+ # Create context
151
+ context = await browser.new_context(
152
+ viewport=self.config.viewport,
153
+ user_agent=self.config.user_agent,
154
+ )
155
+
156
+ # Apply stealth if enabled
157
+ if self.config.stealth:
158
+ await self._apply_stealth(context)
159
+
160
+ # Create instance record
161
+ instance_id = f"{self.config.browser_type}-{len(self._browsers)}"
162
+ instance = BrowserInstance(
163
+ id=instance_id,
164
+ browser_type=self.config.browser_type,
165
+ created_at=time.time(),
166
+ last_used=time.time(),
167
+ page_count=1,
168
+ _browser=browser,
169
+ _context=context,
170
+ )
171
+
172
+ self._browsers[instance_id] = instance
173
+ self._stats["total_created"] += 1
174
+ self._stats["current_active"] += 1
175
+
176
+ _log.debug(f"Created new browser: {instance_id}")
177
+
178
+ # Return first page
179
+ page = await context.new_page()
180
+ page.set_default_timeout(self.config.default_timeout)
181
+ return page
182
+
183
+ async def _apply_stealth(self, context: Any):
184
+ """Apply stealth settings to avoid detection."""
185
+ await context.add_init_script("""
186
+ // Remove webdriver property
187
+ Object.defineProperty(navigator, 'webdriver', {get: () => undefined});
188
+
189
+ // Mock permissions
190
+ const originalQuery = window.navigator.permissions.query;
191
+ window.navigator.permissions.query = (parameters) => (
192
+ parameters.name === 'notifications' ?
193
+ Promise.resolve({ state: Notification.permission }) :
194
+ originalQuery(parameters)
195
+ );
196
+ """)
197
+
198
+ async def release_page(self, page: Any):
199
+ """Release a page back to the pool."""
200
+ async with self._lock:
201
+ self._stats["current_active"] = max(0, self._stats["current_active"] - 1)
202
+
203
+ # Find which browser this page belongs to
204
+ for browser in self._browsers.values():
205
+ # This is a simplification - in reality we'd track page->browser mapping
206
+ if browser.page_count > 0:
207
+ browser.page_count -= 1
208
+ break
209
+
210
+ try:
211
+ await page.close()
212
+ except Exception as e:
213
+ _log.debug(f"Error closing page: {e}")
214
+
215
+ async def get_browser_for_recording(self, video_dir: str | None = None) -> tuple[Any, BrowserInstance]:
216
+ """Get a dedicated browser for video recording.
217
+
218
+ Args:
219
+ video_dir: Directory to save video recordings.
220
+ Defaults to .patchi/evidence/video/ if not specified.
221
+ """
222
+ import os
223
+ from pathlib import Path
224
+
225
+ async with self._lock:
226
+ # Resolve video directory
227
+ if video_dir is None:
228
+ video_dir = str(Path.cwd() / ".patchi" / "evidence" / "video")
229
+ os.makedirs(video_dir, exist_ok=True)
230
+
231
+ # Create a fresh browser for recording
232
+ browser_type = getattr(self._playwright, self.config.browser_type)
233
+ browser = await browser_type.launch(
234
+ headless=self.config.headless,
235
+ args=self.config.launch_args,
236
+ )
237
+ context = await browser.new_context(
238
+ viewport=self.config.viewport,
239
+ record_video_dir=video_dir,
240
+ record_video_size=self.config.viewport,
241
+ )
242
+
243
+ instance_id = f"recording-{int(time.time())}"
244
+ instance = BrowserInstance(
245
+ id=instance_id,
246
+ browser_type=self.config.browser_type,
247
+ created_at=time.time(),
248
+ last_used=time.time(),
249
+ page_count=1,
250
+ _browser=browser,
251
+ _context=context,
252
+ )
253
+
254
+ self._browsers[instance_id] = instance
255
+ return context, instance
256
+
257
+ async def health_check(self) -> dict:
258
+ """Check health of all browsers in pool."""
259
+ healthy = 0
260
+ unhealthy = 0
261
+
262
+ for instance in list(self._browsers.values()):
263
+ try:
264
+ # Try to create a test page
265
+ page = await instance._context.new_page()
266
+ await page.close()
267
+ instance.healthy = True
268
+ healthy += 1
269
+ except Exception:
270
+ instance.healthy = False
271
+ unhealthy += 1
272
+
273
+ return {
274
+ "total": len(self._browsers),
275
+ "healthy": healthy,
276
+ "unhealthy": unhealthy,
277
+ "stats": self._stats,
278
+ }
279
+
280
+ async def cleanup_idle(self, max_idle_seconds: int = 300):
281
+ """Clean up idle browsers."""
282
+ async with self._lock:
283
+ now = time.time()
284
+ to_remove = []
285
+
286
+ for instance_id, instance in self._browsers.items():
287
+ if instance.page_count == 0 and (now - instance.last_used) > max_idle_seconds:
288
+ to_remove.append(instance_id)
289
+
290
+ for instance_id in to_remove:
291
+ instance = self._browsers.pop(instance_id)
292
+ try:
293
+ await instance._context.close()
294
+ await instance._browser.close()
295
+ except Exception as _exc:
296
+ _log.warning('cleanup_idle failed: %s', _exc)
297
+ _log.debug(f"Cleaned up idle browser: {instance_id}")
298
+
299
+ async def shutdown(self):
300
+ """Shutdown all browsers and playwright."""
301
+ async with self._lock:
302
+ for instance in self._browsers.values():
303
+ try:
304
+ await instance._context.close()
305
+ await instance._browser.close()
306
+ except Exception as _exc:
307
+ _log.warning('shutdown failed: %s', _exc)
308
+
309
+ self._browsers.clear()
310
+
311
+ if self._playwright:
312
+ await self._playwright.stop()
313
+ self._playwright = None
314
+
315
+ self._initialized = False
316
+ _log.info("Browser pool shut down")
317
+
318
+ def get_stats(self) -> dict:
319
+ """Get pool statistics."""
320
+ return {
321
+ **self._stats,
322
+ "browser_count": len(self._browsers),
323
+ "config": {
324
+ "max_browsers": self.config.max_browsers,
325
+ "max_pages_per_browser": self.config.max_pages_per_browser,
326
+ "browser_type": self.config.browser_type,
327
+ },
328
+ }
329
+
330
+
331
+ # Global pool instance
332
+ _browser_pool: BrowserPool | None = None
333
+
334
+
335
+ async def get_browser_pool(config: BrowserConfig = None) -> BrowserPool:
336
+ """Get or create the global browser pool."""
337
+ global _browser_pool
338
+ if _browser_pool is None or not _browser_pool._initialized:
339
+ _browser_pool = BrowserPool(config)
340
+ await _browser_pool.initialize()
341
+ return _browser_pool
342
+
343
+
344
+ async def shutdown_browser_pool():
345
+ """Shutdown the global browser pool."""
346
+ global _browser_pool
347
+ if _browser_pool:
348
+ await _browser_pool.shutdown()
349
+ _browser_pool = None