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,641 @@
1
+ """
2
+ AppDiscoveryAgent — Universal project detection and launch for testing.
3
+
4
+ Detects what type of project it is (web, desktop, CLI, mobile, etc.),
5
+ starts it up with the appropriate command, and exposes a target that
6
+ visual/browser testing agents can use.
7
+
8
+ Returns a Target object with:
9
+ - type: "http" | "desktop" | "cli"
10
+ - url: HTTP URL for web apps (None for desktop/CLI)
11
+ - pid: Process ID for desktop/CLI apps (None for web)
12
+ - screenshot_mode: "playwright" | "os" | "none"
13
+ - health_check: function to verify the app is running
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import asyncio
19
+ import logging
20
+ import os
21
+ import re
22
+ import shutil
23
+ import socket
24
+ import subprocess
25
+ import sys
26
+ import tempfile
27
+ import time
28
+ from dataclasses import dataclass, field
29
+ from pathlib import Path
30
+ from typing import Any, Callable, Optional
31
+
32
+ from ..agents.base import (
33
+ AgentDomain,
34
+ AgentGroup,
35
+ AgentInput,
36
+ AgentResult,
37
+ AgentStatus,
38
+ BaseAgent,
39
+ Severity,
40
+ make_finding,
41
+ register,
42
+ )
43
+ from ..brain.trace_log import trace_agent
44
+
45
+ _log = logging.getLogger("patchi.testing.app_discovery")
46
+
47
+
48
+ @dataclass
49
+ class AppTarget:
50
+ """A launchable app target for testing."""
51
+
52
+ type: str # "http" | "desktop" | "cli"
53
+ url: str | None = None # For HTTP apps
54
+ pid: int | None = None # For desktop/CLI apps
55
+ screenshot_mode: str = "playwright" # "playwright" | "os" | "none"
56
+ health_check: Callable[[], bool] = lambda: True
57
+ process: subprocess.Popen | None = None
58
+ port: int | None = None
59
+ metadata: dict = field(default_factory=dict)
60
+
61
+ def is_running(self) -> bool:
62
+ if self.type == "http":
63
+ return self._check_http()
64
+ elif self.type == "desktop":
65
+ return self._check_process()
66
+ elif self.type == "cli":
67
+ return self._check_process()
68
+ return False
69
+
70
+ def _check_http(self) -> bool:
71
+ if not self.url:
72
+ return False
73
+ try:
74
+ import urllib.request
75
+ req = urllib.request.Request(self.url, method="HEAD")
76
+ with urllib.request.urlopen(req, timeout=5) as resp:
77
+ return resp.status < 500
78
+ except Exception:
79
+ return False
80
+
81
+ def _check_process(self) -> bool:
82
+ if not self.pid:
83
+ return False
84
+ try:
85
+ os.kill(self.pid, 0)
86
+ return True
87
+ except OSError:
88
+ return False
89
+
90
+ def cleanup(self) -> None:
91
+ if self.process:
92
+ try:
93
+ self.process.terminate()
94
+ self.process.wait(timeout=5)
95
+ except subprocess.TimeoutExpired:
96
+ self.process.kill()
97
+ except Exception:
98
+ pass
99
+
100
+
101
+ @dataclass
102
+ class DetectedProject:
103
+ """What we discovered about the project."""
104
+
105
+ project_type: str # "web", "desktop", "cli", "mobile", "library"
106
+ framework: str | None = None
107
+ start_command: str | None = None
108
+ health_method: str = "http" # "http" | "tcp" | "process" | "stdout"
109
+ port: int | None = None
110
+ working_dir: Path | None = None
111
+ env: dict[str, str] = field(default_factory=dict)
112
+ screenshot_capable: bool = False
113
+ metadata: dict = field(default_factory=dict)
114
+
115
+
116
+ class FrameworkDetector:
117
+ """Enhanced framework detection for all project types."""
118
+
119
+ @staticmethod
120
+ def detect(root: Path) -> DetectedProject:
121
+ project = DetectedProject(working_dir=root)
122
+
123
+ # Web: Node.js frameworks
124
+ pkg_json = root / "package.json"
125
+ if pkg_json.exists():
126
+ try:
127
+ import json
128
+ pkg = json.loads(pkg_json.read_text(encoding="utf-8"))
129
+ deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})}
130
+ scripts = pkg.get("scripts", {})
131
+
132
+ # Check for various frameworks
133
+ if "next" in deps:
134
+ project.framework = "nextjs"
135
+ project.project_type = "web"
136
+ project.start_command = scripts.get("dev") or "npm run dev"
137
+ project.health_method = "http"
138
+ project.port = FrameworkDetector._extract_port(scripts.get("dev", "")) or 3000
139
+ elif "vite" in deps:
140
+ project.framework = "vite"
141
+ project.project_type = "web"
142
+ project.start_command = scripts.get("dev") or "npm run dev"
143
+ project.health_method = "http"
144
+ project.port = 5173
145
+ elif "react-scripts" in deps:
146
+ project.framework = "cra"
147
+ project.project_type = "web"
148
+ project.start_command = scripts.get("start") or "npm start"
149
+ project.health_method = "http"
150
+ project.port = 3000
151
+ elif "nuxt" in deps:
152
+ project.framework = "nuxt"
153
+ project.project_type = "web"
154
+ project.start_command = scripts.get("dev") or "npm run dev"
155
+ project.health_method = "http"
156
+ project.port = 3000
157
+ elif "sveltekit" in deps:
158
+ project.framework = "sveltekit"
159
+ project.project_type = "web"
160
+ project.start_command = scripts.get("dev") or "npm run dev"
161
+ project.health_method = "http"
162
+ project.port = 5173
163
+ elif "astro" in deps:
164
+ project.framework = "astro"
165
+ project.project_type = "web"
166
+ project.start_command = scripts.get("dev") or "npm run dev"
167
+ project.health_method = "http"
168
+ project.port = 4321
169
+ elif "electron" in deps:
170
+ project.framework = "electron"
171
+ project.project_type = "desktop"
172
+ project.start_command = "electron ."
173
+ project.health_method = "process"
174
+ project.screenshot_capable = True
175
+ elif "tauri" in deps or (root / "src-tauri").exists():
176
+ project.framework = "tauri"
177
+ project.project_type = "desktop"
178
+ project.start_command = "cargo tauri dev"
179
+ project.health_method = "process"
180
+ project.screenshot_capable = True
181
+ elif "expo" in deps:
182
+ project.framework = "expo"
183
+ project.project_type = "mobile"
184
+ project.start_command = "expo start --web"
185
+ project.health_method = "http"
186
+ project.port = 19006
187
+ elif "ionic" in deps:
188
+ project.framework = "ionic"
189
+ project.project_type = "mobile"
190
+ project.start_command = "ionic serve"
191
+ project.health_method = "http"
192
+ project.port = 8100
193
+ elif "flutter" in deps:
194
+ project.framework = "flutter"
195
+ project.project_type = "mobile"
196
+ project.start_command = "flutter run -d web-server"
197
+ project.health_method = "http"
198
+ project.port = 8080
199
+ elif "express" in deps or "fastify" in deps or "koa" in deps or "hono" in deps:
200
+ project.framework = "node"
201
+ project.project_type = "web"
202
+ project.start_command = scripts.get("dev") or scripts.get("start") or "npm start"
203
+ project.health_method = "http"
204
+ project.port = FrameworkDetector._extract_port(scripts.get("dev", "") or scripts.get("start", "")) or 3000
205
+ else:
206
+ project.framework = "node"
207
+ project.project_type = "web"
208
+ project.start_command = scripts.get("dev") or scripts.get("start") or "npm start"
209
+ project.health_method = "http"
210
+ project.port = 3000
211
+ except Exception:
212
+ pass
213
+
214
+ # Web: Python frameworks
215
+ if not project.framework:
216
+ if (root / "pyproject.toml").exists() or (root / "requirements.txt").exists():
217
+ try:
218
+ content = ""
219
+ for f in ["pyproject.toml", "requirements.txt", "setup.py"]:
220
+ if (root / f).exists():
221
+ content += (root / f).read_text(encoding="utf-8", errors="ignore")
222
+
223
+ if "fastapi" in content:
224
+ project.framework = "fastapi"
225
+ project.project_type = "web"
226
+ project.start_command = "uvicorn app.main:app --host 127.0.0.1 --port {port}"
227
+ project.health_method = "http"
228
+ project.port = 8000
229
+ elif "flask" in content:
230
+ project.framework = "flask"
231
+ project.project_type = "web"
232
+ project.start_command = "flask run --host=127.0.0.1 --port={port}"
233
+ project.health_method = "http"
234
+ project.port = 5000
235
+ elif "django" in content and (root / "manage.py").exists():
236
+ project.framework = "django"
237
+ project.project_type = "web"
238
+ project.start_command = "python manage.py runserver 127.0.0.1:{port}"
239
+ project.health_method = "http"
240
+ project.port = 8000
241
+ elif "streamlit" in content:
242
+ project.framework = "streamlit"
243
+ project.project_type = "web"
244
+ project.start_command = "streamlit run app.py --server.port {port}"
245
+ project.health_method = "http"
246
+ project.port = 8501
247
+ elif "gradio" in content:
248
+ project.framework = "gradio"
249
+ project.project_type = "web"
250
+ project.start_command = "python app.py"
251
+ project.health_method = "http"
252
+ project.port = 7860
253
+ elif "nicegui" in content:
254
+ project.framework = "nicegui"
255
+ project.project_type = "web"
256
+ project.start_command = "python app.py"
257
+ project.health_method = "http"
258
+ project.port = 8080
259
+ elif "toga" in content:
260
+ project.framework = "toga"
261
+ project.project_type = "desktop"
262
+ project.start_command = "python -m toga"
263
+ project.health_method = "process"
264
+ project.screenshot_capable = True
265
+ elif "kivy" in content:
266
+ project.framework = "kivy"
267
+ project.project_type = "desktop"
268
+ project.start_command = "python main.py"
269
+ project.health_method = "process"
270
+ project.screenshot_capable = True
271
+ except Exception:
272
+ pass
273
+
274
+ # Web: Go
275
+ if not project.framework and (root / "go.mod").exists():
276
+ project.framework = "go"
277
+ project.project_type = "web"
278
+ project.start_command = "go run ."
279
+ project.health_method = "http"
280
+ project.port = 8080
281
+
282
+ # Web: Rust
283
+ if not project.framework and (root / "Cargo.toml").exists():
284
+ try:
285
+ cargo = (root / "Cargo.toml").read_text(encoding="utf-8")
286
+ if "actix-web" in cargo or "axum" in cargo or "warp" in cargo or "rocket" in cargo:
287
+ project.framework = "rust-web"
288
+ project.project_type = "web"
289
+ project.start_command = "cargo run"
290
+ project.health_method = "http"
291
+ project.port = 8080
292
+ elif "tauri" in cargo:
293
+ project.framework = "tauri"
294
+ project.project_type = "desktop"
295
+ project.start_command = "cargo tauri dev"
296
+ project.health_method = "process"
297
+ project.screenshot_capable = True
298
+ except Exception:
299
+ pass
300
+
301
+ # Desktop: Python GUI
302
+ if not project.framework:
303
+ for f in ["main.py", "app.py", "gui.py"]:
304
+ if (root / f).exists():
305
+ try:
306
+ content = (root / f).read_text(encoding="utf-8", errors="ignore")
307
+ if any(kw in content for kw in ["PyQt", "PySide", "tkinter", "tkinter.ttk", "wx."]):
308
+ project.framework = "python-gui"
309
+ project.project_type = "desktop"
310
+ project.start_command = f"python {f}"
311
+ project.health_method = "process"
312
+ project.screenshot_capable = True
313
+ break
314
+ except Exception:
315
+ pass
316
+
317
+ # Desktop: C#/.NET
318
+ if not project.framework:
319
+ csproj_files = list(root.glob("*.csproj")) + list(root.glob("**/*.csproj"))
320
+ if csproj_files:
321
+ try:
322
+ content = csproj_files[0].read_text(encoding="utf-8", errors="ignore")
323
+ if "Microsoft.NET.Sdk.Web" in content:
324
+ project.framework = "aspnet"
325
+ project.project_type = "web"
326
+ project.start_command = "dotnet run"
327
+ project.health_method = "http"
328
+ project.port = 5000
329
+ elif any(kw in content for kw in ["WPF", "WindowsBase", "PresentationCore", "Avalonia"]):
330
+ project.framework = "dotnet-gui"
331
+ project.project_type = "desktop"
332
+ project.start_command = "dotnet run"
333
+ project.health_method = "process"
334
+ project.screenshot_capable = True
335
+ except Exception:
336
+ pass
337
+
338
+ # Java: Spring Boot
339
+ if not project.framework:
340
+ if (root / "pom.xml").exists() or (root / "build.gradle").exists() or (root / "build.gradle.kts").exists():
341
+ project.framework = "spring"
342
+ project.project_type = "web"
343
+ if (root / "mvnw").exists():
344
+ project.start_command = "./mvnw spring-boot:run"
345
+ elif (root / "gradlew").exists():
346
+ project.start_command = "./gradlew bootRun"
347
+ else:
348
+ project.start_command = "mvn spring-boot:run"
349
+ project.health_method = "http"
350
+ project.port = 8080
351
+
352
+ # PHP: Laravel/Symfony
353
+ if not project.framework and (root / "artisan").exists():
354
+ project.framework = "laravel"
355
+ project.project_type = "web"
356
+ project.start_command = "php artisan serve --host=127.0.0.1 --port={port}"
357
+ project.health_method = "http"
358
+ project.port = 8000
359
+
360
+ # PHP: Generic
361
+ if not project.framework and (root / "composer.json").exists():
362
+ project.framework = "php"
363
+ project.project_type = "web"
364
+ project.start_command = "php -S 127.0.0.1:{port}"
365
+ project.health_method = "http"
366
+ project.port = 8000
367
+
368
+ # Ruby: Rails
369
+ if not project.framework and (root / "Gemfile").exists():
370
+ try:
371
+ content = (root / "Gemfile").read_text(encoding="utf-8")
372
+ if "rails" in content:
373
+ project.framework = "rails"
374
+ project.project_type = "web"
375
+ project.start_command = "rails s -b 127.0.0.1 -p {port}"
376
+ project.health_method = "http"
377
+ project.port = 3000
378
+ except Exception:
379
+ pass
380
+
381
+ # CLI tools detection
382
+ if not project.framework:
383
+ for f in ["cli.py", "main.py", "run.py"]:
384
+ if (root / f).exists():
385
+ try:
386
+ content = (root / f).read_text(encoding="utf-8", errors="ignore")
387
+ if any(kw in content for kw in ["click.", "argparse", "typer", "fire.Fire"]):
388
+ project.framework = "cli"
389
+ project.project_type = "cli"
390
+ project.start_command = f"python {f}"
391
+ project.health_method = "stdout"
392
+ break
393
+ except Exception:
394
+ pass
395
+
396
+ # Fallback: generic web on common ports
397
+ if not project.framework:
398
+ project.project_type = "web"
399
+ project.framework = "unknown"
400
+ project.start_command = "npm run dev"
401
+ project.health_method = "http"
402
+ project.port = 3000
403
+
404
+ return project
405
+
406
+ @staticmethod
407
+ def _extract_port(cmd: str) -> int | None:
408
+ """Extract port from command like '--port 3000' or '-p 3000'."""
409
+ if not cmd:
410
+ return None
411
+ import re
412
+ match = re.search(r'[-\-]p(?:ort)?\s+(\d+)', cmd)
413
+ if match:
414
+ return int(match.group(1))
415
+ match = re.search(r'--port\s+(\d+)', cmd)
416
+ if match:
417
+ return int(match.group(1))
418
+ return None
419
+
420
+
421
+ class AppLauncher:
422
+ """Launches the detected app and returns an AppTarget."""
423
+
424
+ def __init__(self, root: Path):
425
+ self.root = root
426
+ self.project = FrameworkDetector.detect(root)
427
+
428
+ def launch(self, port: int | None = None) -> AppTarget:
429
+ """Launch the app and return the target."""
430
+ if self.project.health_method == "http":
431
+ return self._launch_http(port)
432
+ elif self.project.health_method == "process":
433
+ return self._launch_process()
434
+ elif self.project.health_method == "stdout":
435
+ return self._launch_cli()
436
+ else:
437
+ return self._launch_fallback(port)
438
+
439
+ def _launch_http(self, port: int | None = None) -> AppTarget:
440
+ """Launch a web app that serves HTTP."""
441
+ target_port = port or self.project.port or self._find_free_port()
442
+
443
+ # Substitute port in command
444
+ cmd = self.project.start_command or "npm run dev"
445
+ cmd = cmd.replace("{port}", str(target_port)).replace("{PORT}", str(target_port))
446
+
447
+ # Set up environment
448
+ env = os.environ.copy()
449
+ env.update(self.project.env)
450
+ env["PORT"] = str(target_port)
451
+
452
+ # Start process
453
+ working_dir = self.project.working_dir or self.root
454
+ process = subprocess.Popen(
455
+ cmd,
456
+ shell=True,
457
+ cwd=working_dir,
458
+ env=env,
459
+ stdout=subprocess.PIPE,
460
+ stderr=subprocess.PIPE,
461
+ )
462
+
463
+ # Wait for health
464
+ target = AppTarget(
465
+ type="http",
466
+ url=f"http://127.0.0.1:{target_port}",
467
+ pid=process.pid,
468
+ screenshot_mode="playwright",
469
+ process=process,
470
+ port=target_port,
471
+ )
472
+
473
+ # Wait for health
474
+ for _ in range(30):
475
+ time.sleep(1)
476
+ if target.is_running():
477
+ break
478
+ else:
479
+ _log.warning("App didn't become healthy in time")
480
+
481
+ return target
482
+
483
+ def _launch_process(self) -> AppTarget:
484
+ """Launch a desktop app that runs as a process."""
485
+ cmd = self.project.start_command or "python main.py"
486
+
487
+ env = os.environ.copy()
488
+ env.update(self.project.env)
489
+
490
+ # For desktop apps, we may need special handling
491
+ if sys.platform == "win32":
492
+ # On Windows, use CREATE_NEW_CONSOLE for GUI apps
493
+ creation_flags = subprocess.CREATE_NEW_CONSOLE
494
+ else:
495
+ creation_flags = 0
496
+
497
+ process = subprocess.Popen(
498
+ cmd,
499
+ shell=True,
500
+ cwd=self.project.working_dir or self.root,
501
+ env=env,
502
+ stdout=subprocess.PIPE,
503
+ stderr=subprocess.PIPE,
504
+ creationflags=creation_flags,
505
+ )
506
+
507
+ target = AppTarget(
508
+ type="desktop",
509
+ pid=process.pid,
510
+ screenshot_mode="os",
511
+ process=process,
512
+ )
513
+
514
+ # Give it a moment to start
515
+ time.sleep(2)
516
+
517
+ return target
518
+
519
+ def _launch_cli(self) -> AppTarget:
520
+ """Launch a CLI tool."""
521
+ cmd = self.project.start_command or "python main.py"
522
+
523
+ process = subprocess.Popen(
524
+ cmd,
525
+ shell=True,
526
+ cwd=self.project.working_dir or self.root,
527
+ stdout=subprocess.PIPE,
528
+ stderr=subprocess.PIPE,
529
+ )
530
+
531
+ target = AppTarget(
532
+ type="cli",
533
+ pid=process.pid,
534
+ screenshot_mode="none",
535
+ process=process,
536
+ )
537
+
538
+ return target
539
+
540
+ def _launch_fallback(self, port: int | None = None) -> AppTarget:
541
+ """Fallback launch."""
542
+ return self._launch_http(port)
543
+
544
+ def _find_free_port(self) -> int:
545
+ """Find a free port."""
546
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
547
+ s.bind(("", 0))
548
+ return s.getsockname()[1]
549
+
550
+
551
+ @register
552
+ class AppDiscoveryAgent(BaseAgent):
553
+ """Universal project detection and launch for testing."""
554
+
555
+ group = AgentGroup.TEST
556
+ domain = AgentDomain.TESTING
557
+ name = "AppDiscoveryAgent"
558
+ description = "Detects project type, launches app, returns target for testing (web/desktop/CLI)"
559
+ timeout = 120
560
+
561
+ def _run(self, inp: AgentInput, result: AgentResult) -> None:
562
+ with trace_agent(self.name, inp.root) as trace:
563
+ _log.info("AppDiscoveryAgent: Detecting project type...")
564
+
565
+ # Detect project
566
+ launcher = AppLauncher(inp.root)
567
+ project = launcher.project
568
+
569
+ # Check if already running
570
+ target = self._check_existing(project)
571
+ if target:
572
+ _log.info("App already running at %s", target.url)
573
+ result.data["target"] = self._target_to_dict(target)
574
+ result.data["project_type"] = project.project_type
575
+ result.data["framework"] = project.framework
576
+ result.status = AgentStatus.DONE
577
+ trace.metadata["project_type"] = project.project_type
578
+ trace.metadata["framework"] = project.framework
579
+ return
580
+
581
+ # Launch the app
582
+ _log.info("AppDiscoveryAgent: Launching %s app (%s)...", project.project_type, project.framework)
583
+ target = launcher.launch()
584
+
585
+ # Store target info
586
+ result.data["target"] = self._target_to_dict(target)
587
+ result.data["project_type"] = project.project_type
588
+ result.data["framework"] = project.framework
589
+ result.data["start_command"] = project.start_command
590
+ result.data["screenshot_capable"] = project.screenshot_capable
591
+ result.data["health_method"] = project.health_method
592
+
593
+ # Store target in brain/extra for downstream agents
594
+ inp.extra["app_target"] = target
595
+
596
+ result.status = AgentStatus.DONE
597
+ trace.metadata["project_type"] = project.project_type
598
+ trace.metadata["framework"] = project.framework
599
+ trace.metadata["target_type"] = target.type
600
+
601
+ def _check_existing(self, project: DetectedProject) -> Optional[AppTarget]:
602
+ """Check if app is already running."""
603
+ import socket
604
+
605
+ # Try configured port first
606
+ if project.port:
607
+ try:
608
+ with socket.create_connection(("127.0.0.1", project.port), timeout=1):
609
+ return AppTarget(
610
+ type="http",
611
+ url=f"http://127.0.0.1:{project.port}",
612
+ port=project.port,
613
+ screenshot_mode="playwright",
614
+ )
615
+ except (ConnectionRefusedError, OSError):
616
+ pass
617
+
618
+ # Try common ports
619
+ ports = [1612, 8000, 3000, 8080, 5000, 4200, 5173, 80]
620
+ for port in ports:
621
+ try:
622
+ with socket.create_connection(("127.0.0.1", port), timeout=1):
623
+ return AppTarget(
624
+ type="http",
625
+ url=f"http://127.0.0.1:{port}",
626
+ port=port,
627
+ screenshot_mode="playwright",
628
+ )
629
+ except (ConnectionRefusedError, OSError):
630
+ continue
631
+
632
+ return None
633
+
634
+ def _target_to_dict(self, target: AppTarget) -> dict:
635
+ return {
636
+ "type": target.type,
637
+ "url": target.url,
638
+ "pid": target.pid,
639
+ "screenshot_mode": target.screenshot_mode,
640
+ "port": target.port,
641
+ }