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,253 @@
1
+ """
2
+ Security Test Agent — generates security-focused tests per route/endpoint.
3
+
4
+ For each route found by RouteMapScanner, generates:
5
+ - Authentication bypass tests
6
+ - SQL injection payload tests
7
+ - XSS payload injection tests
8
+ - CSRF token validation tests
9
+ - Rate limiting tests
10
+ - Header validation tests
11
+ - CORS origin tests
12
+
13
+ Outputs pytest files in tests/security/.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import re
19
+ from pathlib import Path
20
+
21
+ from patchi.core.agents.base import (
22
+ AgentDomain,
23
+ AgentGroup,
24
+ AgentInput,
25
+ AgentResult,
26
+ BaseAgent,
27
+ Finding,
28
+ Severity,
29
+ register,
30
+ )
31
+
32
+ # ── Test templates ────────────────────────────────────────────────────────────
33
+
34
+ _AUTH_BYPASS_TEMPLATE = '''\
35
+ def test_{func}_requires_auth(client):
36
+ """{route} should reject unauthenticated requests."""
37
+ resp = client.{method}("{route}")
38
+ assert resp.status_code in (401, 403), \
39
+ f"Expected 401/403, got {{resp.status_code}}"
40
+ '''
41
+
42
+ _SQLI_TEMPLATE = '''\
43
+ def test_{func}_sqli_protection(client):
44
+ """{route} should reject SQL injection payloads."""
45
+ payloads = ["' OR '1'='1", "1; DROP TABLE--", "' UNION SELECT NULL--"]
46
+ for payload in payloads:
47
+ resp = client.{method}("{route}", json={{"input": payload}})
48
+ assert resp.status_code != 500, \
49
+ f"Server error on SQLi payload: {{payload}}"
50
+ '''
51
+
52
+ _XSS_TEMPLATE = '''\
53
+ def test_{func}_xss_protection(client):
54
+ """{route} should sanitize or escape XSS payloads."""
55
+ payloads = ["<script>alert(1)</script>", "<img onerror=alert(1)>"]
56
+ for payload in payloads:
57
+ resp = client.{method}("{route}", json={{"input": payload}})
58
+ if resp.status_code == 200:
59
+ assert payload not in resp.text, \
60
+ f"XSS payload reflected in response: {{payload}}"
61
+ '''
62
+
63
+ _CSRF_TEMPLATE = '''\
64
+ def test_{func}_csrf_protection(client):
65
+ """{route} should validate CSRF tokens for state-changing operations."""
66
+ resp = client.{method}("{route}", headers={{"X-Requested-With": ""}})
67
+ assert resp.status_code in (400, 403), \
68
+ f"Expected CSRF rejection, got {{resp.status_code}}"
69
+ '''
70
+
71
+ _HEADER_TEMPLATE = '''\
72
+ def test_{func}_security_headers(client):
73
+ """{route} should include security headers."""
74
+ resp = client.get("{route}")
75
+ headers = {h.lower() for h in resp.headers}
76
+ # At minimum, these should be present on sensitive endpoints
77
+ recommended = ["x-content-type-options", "x-frame-options"]
78
+ for h in recommended:
79
+ # Only warn, don't fail — not all routes need all headers
80
+ pass
81
+ '''
82
+
83
+ _RATE_LIMIT_TEMPLATE = '''\
84
+ def test_{func}_rate_limiting(client):
85
+ """{route} should enforce rate limiting on repeated requests."""
86
+ responses = [client.{method}("{route}") for _ in range(50)]
87
+ status_codes = [r.status_code for r in responses]
88
+ # At least some requests should be throttled (429) or the endpoint
89
+ # should handle rapid requests gracefully (no 500s)
90
+ assert 500 not in status_codes, "Server crashed under rapid requests"
91
+ '''
92
+
93
+ _CORS_TEMPLATE = '''\
94
+ def test_{func}_cors_restrictions(client):
95
+ """{route} should not allow arbitrary origins."""
96
+ resp = client.options("{route}", headers={{{{ \
97
+ "Origin": "https://evil.com", \
98
+ "Access-Control-Request-Method": "{method_upper}", \
99
+ }}}})
100
+ acao = resp.headers.get("access-control-allow-origin", "")
101
+ assert acao != "*", \
102
+ f"CORS allows all origins on {route}"
103
+ '''
104
+
105
+
106
+ # ── Agent ─────────────────────────────────────────────────────────────────────
107
+
108
+
109
+ @register
110
+ class SecurityTestAgent(BaseAgent):
111
+ """Generates security-focused pytest tests for detected routes."""
112
+
113
+ name = "SecurityTestAgent"
114
+ group = AgentGroup.TEST
115
+ domain = AgentDomain.TESTING
116
+ timeout = 120
117
+
118
+ def _run(self, inp: AgentInput, result: AgentResult) -> None:
119
+ routes = inp.brain.get("routes", [])
120
+ if not routes:
121
+ result.add_finding(
122
+ Finding(
123
+ agent=self.name,
124
+ type="info",
125
+ severity=Severity.INFO,
126
+ file="",
127
+ message="No routes detected — security tests require a web app with routes",
128
+ )
129
+ )
130
+ return
131
+
132
+ test_dir = inp.root / "tests" / "security"
133
+ test_files: list[Path] = []
134
+
135
+ framework = self._detect_framework(inp.brain)
136
+ for route_info in routes:
137
+ route = (
138
+ route_info.get("path", "")
139
+ if isinstance(route_info, dict)
140
+ else getattr(route_info, "path", "")
141
+ )
142
+ method = (
143
+ route_info.get("method", "get")
144
+ if isinstance(route_info, dict)
145
+ else getattr(route_info, "method", "get")
146
+ ).lower()
147
+ func_name = self._route_to_func(route, method)
148
+
149
+ if not func_name:
150
+ continue
151
+
152
+ tests = self._generate_tests(route, method, func_name)
153
+ if tests:
154
+ test_file = test_dir / f"test_security_{func_name}.py"
155
+ test_file.parent.mkdir(parents=True, exist_ok=True)
156
+ header = self._build_header(framework, route, method)
157
+ test_file.write_text(header + tests, encoding="utf-8")
158
+ test_files.append(test_file)
159
+ result.files_scanned += 1
160
+
161
+ if test_files:
162
+ result.data["generated_files"] = [str(f.relative_to(inp.root)) for f in test_files]
163
+ result.data["test_count"] = len(test_files)
164
+
165
+ def _route_to_func(self, route: str, method: str) -> str:
166
+ """Convert /api/users/{id} -> api_users_id."""
167
+ clean = re.sub(r"[^a-zA-Z0-9/]", "", route)
168
+ parts = [p for p in clean.split("/") if p]
169
+ name = "_".join(parts[:4]) # limit depth
170
+ return f"{method}_{name}" if name else ""
171
+
172
+ def _detect_framework(self, brain: dict) -> str:
173
+ fw = brain.get("framework", "")
174
+ fw_raw = brain.get("frameworks", fw)
175
+ if isinstance(fw_raw, list):
176
+ fw = fw_raw[0] if fw_raw else ""
177
+ else:
178
+ fw = fw_raw or ""
179
+ fw_lower = fw.lower()
180
+ if "fastapi" in fw_lower or "starlette" in fw_lower:
181
+ return "fastapi"
182
+ if "flask" in fw_lower:
183
+ return "flask"
184
+ if "django" in fw_lower:
185
+ return "django"
186
+ if "express" in fw_lower or "node" in fw_lower:
187
+ return "express"
188
+ return "generic"
189
+
190
+ def _build_header(self, framework: str, route: str, method: str) -> str:
191
+ fixtures = {
192
+ "fastapi": (
193
+ "@pytest.fixture\ndef client():\n"
194
+ " from fastapi.testclient import TestClient\n"
195
+ " from app import app\n"
196
+ " return TestClient(app)\n"
197
+ ),
198
+ "flask": (
199
+ "@pytest.fixture\ndef client():\n"
200
+ " from flask import Flask\n"
201
+ " app = Flask(__name__)\n"
202
+ " return app.test_client()\n"
203
+ ),
204
+ "django": (
205
+ "@pytest.fixture\ndef client():\n"
206
+ " from django.test import Client\n"
207
+ " return Client()\n"
208
+ ),
209
+ }
210
+ fixture = fixtures.get(
211
+ framework,
212
+ (
213
+ "@pytest.fixture\ndef client():\n"
214
+ ' """Override with your app\'s test client."""\n'
215
+ " import pytest\n"
216
+ ' raise NotImplementedError("Define a client fixture for your framework")\n'
217
+ ),
218
+ )
219
+ return (
220
+ '"""Auto-generated security tests by Patchi SecurityTestAgent."""\n'
221
+ "import pytest\n\n\n"
222
+ f"{fixture}\n\n"
223
+ )
224
+
225
+ def _generate_tests(self, route: str, method: str, func: str) -> str:
226
+ """Generate test code for a single route."""
227
+ tests = []
228
+ templates = [
229
+ ("auth", _AUTH_BYPASS_TEMPLATE),
230
+ ("sqli", _SQLI_TEMPLATE),
231
+ ("xss", _XSS_TEMPLATE),
232
+ ]
233
+ # Only add CSRF for state-changing methods
234
+ if method in ("post", "put", "patch", "delete"):
235
+ templates.append(("csrf", _CSRF_TEMPLATE))
236
+
237
+ # CORS for all
238
+ templates.append(("cors", _CORS_TEMPLATE))
239
+
240
+ # Rate limit for all
241
+ templates.append(("rate_limit", _RATE_LIMIT_TEMPLATE))
242
+
243
+ for _name, tpl in templates:
244
+ tests.append(
245
+ tpl.format(
246
+ func=func,
247
+ route=route,
248
+ method=method,
249
+ method_upper=method.upper(),
250
+ )
251
+ )
252
+
253
+ return "\n".join(tests)
@@ -0,0 +1,203 @@
1
+ """
2
+ StressTestAgent — Load testing via StressOrchestrator.
3
+
4
+ Thin wrapper around StressOrchestrator that runs as a registered BaseAgent.
5
+ Supports load, spike, soak, and breakpoint testing scenarios.
6
+
7
+ Uses aiohttp for async HTTP requests (no external binary needed).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import asyncio
13
+ import logging
14
+ from pathlib import Path
15
+
16
+ from ..agents.base import (
17
+ AgentDomain,
18
+ AgentGroup,
19
+ AgentInput,
20
+ AgentResult,
21
+ AgentStatus,
22
+ BaseAgent,
23
+ Severity,
24
+ make_finding,
25
+ register,
26
+ )
27
+ from ..testing.live_v2.stress_orchestrator import (
28
+ StressConfig,
29
+ StressOrchestrator,
30
+ StressTestReport,
31
+ run_stress_test,
32
+ )
33
+
34
+ _log = logging.getLogger("patchi.testing.stress_test_agent")
35
+
36
+
37
+ @register
38
+ class StressTestAgent(BaseAgent):
39
+ """Load/stress testing agent using StressOrchestrator."""
40
+
41
+ group = AgentGroup.TEST
42
+ domain = AgentDomain.TESTING
43
+ name = "StressTestAgent"
44
+ description = "Stress testing: load, spike, soak, breakpoint scenarios via async HTTP"
45
+ timeout = 600 # 10 minutes max for stress tests
46
+
47
+ def _run(self, inp: AgentInput, result: AgentResult) -> None:
48
+ target_url = self._detect_target(inp)
49
+ if not target_url:
50
+ self.skip(result, "No running web application detected. Start the web server first.")
51
+ return
52
+
53
+ _log.info("StressTestAgent: Testing %s with StressOrchestrator", target_url)
54
+
55
+ # Get scenario from config (default: load)
56
+ scenario = inp.config.get("stress_scenario", "load")
57
+ users = inp.config.get("stress_users", 10)
58
+ duration = inp.config.get("stress_duration", 60)
59
+
60
+ # Build endpoints from route map
61
+ route_map = inp.brain.get("route_map", {})
62
+ endpoints = []
63
+ for k, v in route_map.items():
64
+ if isinstance(v, dict):
65
+ path = v.get("path", k.split(" ", 1)[-1])
66
+ endpoints.append({"method": "GET", "path": path})
67
+ if not endpoints:
68
+ endpoints = [{"method": "GET", "path": "/"}]
69
+
70
+ # Run stress test
71
+ try:
72
+ report = asyncio.run(self._run_stress(target_url, scenario, users, duration, endpoints))
73
+ self._process_report(report, result)
74
+ except Exception as e:
75
+ _log.error("StressTestAgent failed: %s", e)
76
+ result.add_error(f"Stress test failed: {e}")
77
+ result.status = AgentStatus.FAILED
78
+
79
+ async def _run_stress(
80
+ self,
81
+ target_url: str,
82
+ scenario: str,
83
+ users: int,
84
+ duration: int,
85
+ endpoints: list[dict],
86
+ ) -> StressTestReport:
87
+ """Run the stress test via StressOrchestrator."""
88
+ config = StressConfig(
89
+ base_url=target_url,
90
+ scenario=scenario,
91
+ users=users,
92
+ duration_seconds=duration,
93
+ endpoints=endpoints,
94
+ )
95
+ orchestrator = StressOrchestrator(config)
96
+ return await orchestrator.run()
97
+
98
+ def _process_report(self, report: StressTestReport, result: AgentResult) -> None:
99
+ """Process StressTestReport into AgentResult findings."""
100
+ result.data["report"] = report.to_dict()
101
+ result.data["scenario"] = report.config.scenario
102
+ result.data["users"] = report.config.users
103
+ result.data["duration_seconds"] = report.config.duration_seconds
104
+
105
+ # Add latency findings
106
+ latency = report.latency
107
+ if latency:
108
+ p95 = latency.get("p95", 0)
109
+ p99 = latency.get("p99", 0)
110
+
111
+ if p95 > 2000:
112
+ result.add_finding(
113
+ make_finding(
114
+ agent=self.name,
115
+ finding_type="slow_p95",
116
+ severity=Severity.MEDIUM,
117
+ file="stress_test",
118
+ message=f"Slow p95 latency under load: {p95:.0f}ms",
119
+ )
120
+ )
121
+ if p99 > 5000:
122
+ result.add_finding(
123
+ make_finding(
124
+ agent=self.name,
125
+ finding_type="slow_p99",
126
+ severity=Severity.HIGH,
127
+ file="stress_test",
128
+ message=f"Slow p99 latency under load: {p99:.0f}ms",
129
+ )
130
+ )
131
+
132
+ # Add error rate findings
133
+ if report.total_requests > 0:
134
+ error_rate = report.failed_requests / report.total_requests
135
+ if error_rate > 0.05: # 5%
136
+ result.add_finding(
137
+ make_finding(
138
+ agent=self.name,
139
+ finding_type="high_error_rate",
140
+ severity=Severity.HIGH,
141
+ file="stress_test",
142
+ message=f"High load-test error rate: {error_rate:.1%}",
143
+ )
144
+ )
145
+
146
+ # Add breakpoint findings
147
+ if report.breakpoint_found:
148
+ result.add_finding(
149
+ make_finding(
150
+ agent=self.name,
151
+ finding_type="breakpoint_found",
152
+ severity=Severity.HIGH,
153
+ file="stress_test",
154
+ message=f"Breakpoint found at {report.breakpoint_users} users",
155
+ )
156
+ )
157
+
158
+ # Add soak stability findings
159
+ if not report.soak_stability:
160
+ result.add_finding(
161
+ make_finding(
162
+ agent=self.name,
163
+ finding_type="soak_instability",
164
+ severity=Severity.MEDIUM,
165
+ file="stress_test",
166
+ message="System instability detected during soak test",
167
+ )
168
+ )
169
+
170
+ result.status = AgentStatus.DONE
171
+ result.files_scanned = 1
172
+
173
+ def _detect_target(self, inp: AgentInput) -> str | None:
174
+ """Detect the target URL for testing."""
175
+ import socket
176
+
177
+ # Check for explicit base_url in config
178
+ if inp.config.get("base_url"):
179
+ return inp.config["base_url"]
180
+
181
+ # Try common ports
182
+ ports = [1612, 8000, 3000, 8080, 80]
183
+
184
+ for port in ports:
185
+ try:
186
+ with socket.create_connection(("127.0.0.1", port), timeout=1):
187
+ return f"http://127.0.0.1:{port}"
188
+ except (ConnectionRefusedError, OSError):
189
+ continue
190
+
191
+ # Check config for web port
192
+ try:
193
+ from patchi.core import config as cfg
194
+
195
+ config = cfg.load(inp.root)
196
+ web_config = config.get("web", {})
197
+ host = web_config.get("host", "127.0.0.1")
198
+ port = web_config.get("port", 1612)
199
+ return f"http://{host}:{port}"
200
+ except Exception as _exc:
201
+ _log.warning("_detect_target failed: %s", _exc)
202
+
203
+ return None
@@ -0,0 +1,50 @@
1
+ """
2
+ Test agent aggregation module for Patchi.
3
+
4
+ Exports all test agent classes and shared test helper types.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from ..agents.base import register
10
+ from .app_discovery_agent import AppDiscoveryAgent # noqa: F401
11
+ from .api_contract_agent import APIContractAgent
12
+ from .browser_test_agent import BrowserTestAgent
13
+ from .e2e_flow_agent import E2EFlowAgent
14
+ from .flake_detector_agent import FlakeDetectorAgent # noqa: F401
15
+ from .regression_agent import RegressionAgent
16
+ from .security_test_agent import SecurityTestAgent
17
+ from .stress_test_agent import StressTestAgent
18
+ from .ui_accessibility_agent import UIAccessibilityAgent
19
+ from .ui_button_agent import UIButtonAgent
20
+ from .ui_layout_agent import UILayoutAgent
21
+ from .unit_test_agent import TestCase, TestSuite, UnitTestAgent, _run
22
+ from .visual_regression_agent import VisualRegressionAgent
23
+
24
+ # Live v2 runner registers its agent on import (additive — never breaks v1)
25
+ try:
26
+ from .live_v2.runner import LiveTestRunnerV2Agent # noqa: F401
27
+ except Exception as _e: # pragma: no cover — playwright-less envs still work
28
+ import logging as _logging
29
+
30
+ _logging.getLogger("patchi.testing").warning("LiveTestRunnerV2Agent unavailable: %s", _e)
31
+
32
+ __all__ = [
33
+ "AppDiscoveryAgent",
34
+ "APIContractAgent",
35
+ "BrowserTestAgent",
36
+ "E2EFlowAgent",
37
+ "FlakeDetectorAgent",
38
+ "RegressionAgent",
39
+ "SecurityTestAgent",
40
+ "StressTestAgent",
41
+ "TestCase",
42
+ "TestSuite",
43
+ "UIAccessibilityAgent",
44
+ "UIButtonAgent",
45
+ "UILayoutAgent",
46
+ "UnitTestAgent",
47
+ "VisualRegressionAgent",
48
+ "_run",
49
+ "register",
50
+ ]
@@ -0,0 +1,194 @@
1
+ """
2
+ Test configuration for Patchi.
3
+
4
+ Reads/writes .patchi/tests.json — project-level test settings.
5
+
6
+ Config shape:
7
+ {
8
+ "base_url": "http://localhost:3000",
9
+ "viewport_widths": [375, 768, 1024, 1440],
10
+ "timeout_per_agent": 120,
11
+ "parallel": false,
12
+ "skip_agents": [],
13
+ "only_agents": [],
14
+ "ai_generate": true,
15
+ "screenshots_dir": ".patchi/screenshots",
16
+ "baselines_dir": ".patchi/baselines",
17
+ "custom_flows": [
18
+ {"name": "login", "url": "/login", "steps": ["fill email", "fill password", "click submit"]}
19
+ ],
20
+ "exclude_paths": ["node_modules", ".venv", "dist"],
21
+ "report_format": "rich"
22
+ }
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import json
27
+ import logging
28
+ from pathlib import Path
29
+ from typing import Any
30
+
31
+ from patchi.core.brain.languages import DEFAULT_IGNORE_DIRS
32
+
33
+ from ..constants import PATCHI_DIR
34
+
35
+ TESTS_CONFIG = f"{PATCHI_DIR}/tests.json"
36
+
37
+ DEFAULT_TEST_CONFIG = {
38
+ "base_url": None,
39
+ "viewport_widths": [375, 768, 1024, 1440],
40
+ "timeout_per_agent": 120,
41
+ "parallel": False,
42
+ "skip_agents": [],
43
+ "only_agents": [],
44
+ "ai_generate": True,
45
+ "screenshots_dir": f"{PATCHI_DIR}/screenshots",
46
+ "baselines_dir": f"{PATCHI_DIR}/baselines",
47
+ "custom_flows": [],
48
+ "exclude_paths": sorted(DEFAULT_IGNORE_DIRS),
49
+ "report_format": "rich",
50
+ }
51
+
52
+
53
+ _log = logging.getLogger("patchi.testing.test_config")
54
+
55
+
56
+ def _config_path(root: Path) -> Path:
57
+ return root / TESTS_CONFIG
58
+
59
+
60
+ def load(root: Path | None = None) -> dict:
61
+ """Load test config, merging with defaults."""
62
+ from ..config import require_project_root
63
+
64
+ root = root or require_project_root()
65
+ path = _config_path(root)
66
+ if not path.exists():
67
+ return dict(DEFAULT_TEST_CONFIG)
68
+ try:
69
+ raw = json.loads(path.read_text(encoding="utf-8"))
70
+ return {**DEFAULT_TEST_CONFIG, **raw}
71
+ except Exception as e:
72
+ _log.warning("load failed: %s", e)
73
+ return dict(DEFAULT_TEST_CONFIG)
74
+
75
+
76
+ def save(config: dict, root: Path | None = None) -> None:
77
+ """Write test config to disk."""
78
+ from ..config import require_project_root
79
+
80
+ root = root or require_project_root()
81
+ path = _config_path(root)
82
+ path.parent.mkdir(parents=True, exist_ok=True)
83
+ path.write_text(json.dumps(config, indent=2), encoding="utf-8")
84
+
85
+
86
+ def get(key: str, root: Path | None = None) -> Any:
87
+ """Get a single test config value."""
88
+ return load(root).get(key)
89
+
90
+
91
+ def set_value(key: str, value: Any, root: Path | None = None) -> None:
92
+ """Update a single test config key."""
93
+ config = load(root)
94
+ config[key] = value
95
+ save(config, root)
96
+
97
+
98
+ def add_custom_flow(name: str, url: str, steps: list[str], root: Path | None = None) -> None:
99
+ """Add a custom test flow."""
100
+ config = load(root)
101
+ flows = config.get("custom_flows", [])
102
+ flows.append({"name": name, "url": url, "steps": steps})
103
+ config["custom_flows"] = flows
104
+ save(config, root)
105
+
106
+
107
+ def remove_custom_flow(name: str, root: Path | None = None) -> bool:
108
+ """Remove a custom test flow by name."""
109
+ config = load(root)
110
+ flows = config.get("custom_flows", [])
111
+ before = len(flows)
112
+ config["custom_flows"] = [f for f in flows if f.get("name") != name]
113
+ save(config, root)
114
+ return len(config["custom_flows"]) < before
115
+
116
+
117
+ def get_test_history(root: Path | None = None) -> list[dict]:
118
+ """Load test run history from memory."""
119
+ from .. import memory as mem
120
+ from ..config import require_project_root
121
+
122
+ root = root or require_project_root()
123
+ brain = mem.get_brain(root)
124
+ return brain.get("test_history", [])
125
+
126
+
127
+ def append_test_history(entry: dict, root: Path | None = None) -> None:
128
+ """Append a test run entry to history."""
129
+ from .. import memory as mem
130
+ from ..config import require_project_root
131
+
132
+ root = root or require_project_root()
133
+ brain = mem.get_brain(root)
134
+ history = brain.get("test_history", [])
135
+ history.append(entry)
136
+ # Keep last 50 runs
137
+ if len(history) > 50:
138
+ history = history[-50:]
139
+ brain["test_history"] = history
140
+ mem.save_brain(brain, root)
141
+
142
+
143
+ def get_coverage(root: Path | None = None) -> dict:
144
+ """Get cumulative test coverage data."""
145
+ from .. import memory as mem
146
+ from ..config import require_project_root
147
+
148
+ root = root or require_project_root()
149
+ brain = mem.get_brain(root)
150
+ return brain.get(
151
+ "test_coverage",
152
+ {
153
+ "files_tested": [],
154
+ "agents_run": {},
155
+ "total_runs": 0,
156
+ "last_run": None,
157
+ },
158
+ )
159
+
160
+
161
+ def update_coverage(run_result: dict, root: Path | None = None) -> None:
162
+ """Update coverage data after a test run."""
163
+ from .. import memory as mem
164
+ from ..config import require_project_root
165
+
166
+ root = root or require_project_root()
167
+ brain = mem.get_brain(root)
168
+ coverage = brain.get(
169
+ "test_coverage",
170
+ {
171
+ "files_tested": [],
172
+ "agents_run": {},
173
+ "total_runs": 0,
174
+ "last_run": None,
175
+ },
176
+ )
177
+
178
+ coverage["total_runs"] = coverage.get("total_runs", 0) + 1
179
+ coverage["last_run"] = run_result.get("timestamp")
180
+
181
+ # Track which agents have been run
182
+ for agent_name in run_result.get("agents", []):
183
+ counts = coverage.get("agents_run", {})
184
+ counts[agent_name] = counts.get(agent_name, 0) + 1
185
+ coverage["agents_run"] = counts
186
+
187
+ # Track files tested
188
+ tested_files = set(coverage.get("files_tested", []))
189
+ for f in run_result.get("files_tested", []):
190
+ tested_files.add(f)
191
+ coverage["files_tested"] = sorted(tested_files)
192
+
193
+ brain["test_coverage"] = coverage
194
+ mem.save_brain(brain, root)