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,95 @@
1
+ """
2
+ I18nAgent §11.7 — Internationalization Audit.
3
+
4
+ Finds hardcoded strings in UI code, missing translation keys, untranslated strings
5
+ via tree-sitter scan for string literals in JSX/TSX/Vue/Svelte.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+
12
+ from patchi.core.agents.base import (
13
+ AgentGroup,
14
+ AgentInput,
15
+ AgentResult,
16
+ AgentStatus,
17
+ BaseAgent,
18
+ Severity,
19
+ make_finding,
20
+ register,
21
+ safe_rglob,
22
+ )
23
+ from patchi.core.brain.code_query import (
24
+ js_calls,
25
+ js_jsx_texts,
26
+ lang_for_file,
27
+ parse_js,
28
+ vue_template_texts,
29
+ )
30
+
31
+ _log = logging.getLogger("patchi.agents.i18n")
32
+
33
+
34
+ def _looks_hardcoded(text: str) -> bool:
35
+ words = text.split()
36
+ return len(text) > 10 and 2 <= len(words) <= 5 and text[0].isupper()
37
+
38
+
39
+ def _has_i18n(tree, lang: str, raw: str) -> bool:
40
+ for call in js_calls(tree, lang):
41
+ if call.name == "useTranslation":
42
+ return True
43
+ if call.full.startswith("i18n."):
44
+ return True
45
+ if call.name == "t" and call.arg_kinds[:1] == ["string"]:
46
+ return True
47
+ markers = ("useTranslation", "i18n", "$t(", "t('", 't("')
48
+ return any(m in raw for m in markers)
49
+
50
+ @register
51
+ class I18nAgent(BaseAgent):
52
+ group = AgentGroup.SCANNER
53
+ name = "I18nAgent"
54
+ description = "i18n audit §11.7 — hardcoded strings vs translation keys"
55
+ timeout = 60
56
+
57
+ def _run(self, inp: AgentInput, result: AgentResult) -> None:
58
+ findings=[]
59
+ for pat in ("*.jsx","*.tsx","*.vue","*.svelte"):
60
+ for fp in safe_rglob(inp.root, pat):
61
+ rel=fp.relative_to(inp.root).as_posix()
62
+ if "node_modules" in rel:
63
+ continue
64
+ try:
65
+ txt=fp.read_text(encoding="utf-8", errors="replace")
66
+ except OSError:
67
+ continue
68
+ if fp.suffix in (".vue", ".svelte"):
69
+ texts = vue_template_texts(txt)
70
+ markers = ("useTranslation", "i18n", "$t(", "t('", 't("')
71
+ has_i18n = any(m in txt for m in markers)
72
+ else:
73
+ lang = lang_for_file(rel)
74
+ tree = parse_js(txt, lang)
75
+ if tree is None:
76
+ continue
77
+ texts = js_jsx_texts(tree, lang)
78
+ has_i18n = _has_i18n(tree, lang, txt)
79
+ if has_i18n:
80
+ continue
81
+ for s, line in texts:
82
+ if _looks_hardcoded(s):
83
+ findings.append(make_finding(
84
+ severity=Severity.LOW, file=rel, line_start=line,
85
+ title=f"Hardcoded UI string '{s[:30]}' without i18n",
86
+ description="Use t('key') / $t() / i18n key; check i18next-scanner",
87
+ finding_type="i18n_hardcoded",
88
+ ))
89
+ break
90
+ if len(findings) >= 20:
91
+ break
92
+ if len(findings) >= 20:
93
+ break
94
+ result.status=AgentStatus.SUCCEEDED
95
+ result.findings=findings[:20]
@@ -0,0 +1,247 @@
1
+ """LicenseComplianceAgent — checks dependency licenses against policy.
2
+
3
+ Covers §11.6:
4
+ - Parse package.json, pyproject.toml, Cargo.toml, go.mod, Gemfile
5
+ - Extract license field from each dependency
6
+ - Flag GPL/AGPL licenses (restrictive) and missing license fields
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import logging
12
+ import re
13
+ import urllib.request
14
+ from pathlib import Path
15
+
16
+ from patchi.core.constants import is_offline
17
+
18
+ from .base import (
19
+ AgentGroup,
20
+ AgentInput,
21
+ AgentResult,
22
+ AgentStatus,
23
+ BaseAgent,
24
+ Severity,
25
+ make_finding,
26
+ register,
27
+ safe_rglob,
28
+ )
29
+
30
+ _RESTRICTED_LICENSES = {
31
+ "gpl",
32
+ "gpl-2.0",
33
+ "gpl-3.0",
34
+ "gplv2",
35
+ "gplv3",
36
+ "agpl",
37
+ "agpl-3.0",
38
+ "agplv3",
39
+ "lgpl",
40
+ "lgpl-3.0",
41
+ "cc-by-nc",
42
+ "cc-by-nc-nd",
43
+ "proprietary",
44
+ "commercial",
45
+ }
46
+
47
+ _ALLOWED_LICENSES = {
48
+ "mit",
49
+ "apache-2.0",
50
+ "apache",
51
+ "bsd-2-clause",
52
+ "bsd-3-clause",
53
+ "isc",
54
+ "unlicense",
55
+ "cc0-1.0",
56
+ "cc-by-4.0",
57
+ "python-2.0",
58
+ "zlib",
59
+ "mpl-2.0",
60
+ }
61
+
62
+ _UNKNOWN_LICENSE_RISK = {
63
+ "gpl",
64
+ "agpl",
65
+ "lgpl",
66
+ "unknown",
67
+ }
68
+
69
+
70
+ _log = logging.getLogger("patchi.agents.license_compliance")
71
+
72
+
73
+ def _normalize_license(license_str: str) -> str:
74
+ return (
75
+ license_str.strip()
76
+ .strip('"')
77
+ .strip("'")
78
+ .lower()
79
+ .replace("-or-later", "")
80
+ .replace("-only", "")
81
+ )
82
+
83
+
84
+ def _check_license(license_str: str | None) -> tuple[str, str]:
85
+ """Returns (status, normalized)."""
86
+ if not license_str:
87
+ return "missing", "unknown"
88
+ norm = _normalize_license(license_str)
89
+ if norm in _RESTRICTED_LICENSES:
90
+ return "restricted", norm
91
+ if norm in _ALLOWED_LICENSES:
92
+ return "allowed", norm
93
+ if "gpl" in norm or "affero" in norm:
94
+ return "restricted", norm
95
+ return "unknown", norm
96
+
97
+
98
+ # Cache for lookup results
99
+ _license_cache: dict[str, str] = {}
100
+
101
+
102
+ def _lookup_license(package_name: str, eco: str) -> str | None:
103
+ if is_offline():
104
+ return None
105
+ cache_key = f"{eco}:{package_name}"
106
+ if cache_key in _license_cache:
107
+ return _license_cache[cache_key]
108
+
109
+ try:
110
+ if eco == "npm":
111
+ url = f"https://registry.npmjs.org/{package_name}/latest"
112
+ req = urllib.request.Request(url, headers={"User-Agent": "Patchi/1.0"})
113
+ with urllib.request.urlopen(req, timeout=5) as resp:
114
+ data = json.loads(resp.read().decode())
115
+ lic = data.get("license")
116
+ if lic:
117
+ _license_cache[cache_key] = lic
118
+ return lic
119
+ except Exception as e:
120
+ _log.warning("_lookup_license failed: %s", e)
121
+ return None
122
+
123
+
124
+ def _extract_deps(root: Path) -> list[dict]:
125
+ deps: list[dict] = []
126
+ # package.json
127
+ for fp in safe_rglob(root, "package.json"):
128
+ try:
129
+ data = json.loads(fp.read_text(encoding="utf-8"))
130
+ for name in {**data.get("dependencies", {}), **data.get("devDependencies", {})}:
131
+ lic = data.get("license") or _lookup_license(name, "npm")
132
+ deps.append(
133
+ {
134
+ "name": name,
135
+ "license": lic,
136
+ "ecosystem": "npm",
137
+ "file": fp.relative_to(root).as_posix(),
138
+ }
139
+ )
140
+ except Exception as e:
141
+ _log.warning("_extract_deps failed: %s", e)
142
+ # pyproject.toml
143
+ for fp in safe_rglob(root, "pyproject.toml"):
144
+ try:
145
+ content = fp.read_text(encoding="utf-8")
146
+ m = re.search(r'license\s*=\s*["\']([^"\']+)["\']', content)
147
+ lic = m.group(1) if m else None
148
+ for line in content.splitlines():
149
+ stripped = line.strip()
150
+ if stripped.startswith('"') and ">" in stripped:
151
+ name = stripped.split(">")[0].strip('"').strip("'").split("[")[0].strip()
152
+ if name and not name.startswith("python") and not name.startswith("#"):
153
+ deps.append(
154
+ {
155
+ "name": name,
156
+ "license": lic,
157
+ "ecosystem": "pypi",
158
+ "file": fp.relative_to(root).as_posix(),
159
+ }
160
+ )
161
+ except Exception as e:
162
+ _log.warning("_extract_deps failed: %s", e)
163
+ return deps
164
+
165
+
166
+ @register
167
+ class LicenseComplianceAgent(BaseAgent):
168
+ """Scans dependencies for license compliance and flags restricted licenses."""
169
+
170
+ group = AgentGroup.SCANNER
171
+ name = "LicenseComplianceAgent"
172
+ description = "Check dependency licenses against policy — flag GPL/AGPL, missing, unknown"
173
+
174
+ def _run(self, inp: AgentInput, result: AgentResult) -> None:
175
+ deps = _extract_deps(inp.root)
176
+ result.data["total_deps"] = len(deps)
177
+ result.data["deps"] = deps
178
+
179
+ restricted: list[dict] = []
180
+ missing: list[dict] = []
181
+ unknown: list[dict] = []
182
+
183
+ for dep in deps:
184
+ status, norm = _check_license(dep.get("license"))
185
+ if status == "restricted":
186
+ restricted.append(dep)
187
+ elif status == "missing":
188
+ missing.append(dep)
189
+ elif status == "unknown":
190
+ unknown.append(dep)
191
+
192
+ result.data["restricted"] = restricted
193
+ result.data["missing_license"] = missing
194
+ result.data["unknown_license"] = unknown
195
+ result.data["total_allowed"] = len(deps) - len(restricted) - len(missing) - len(unknown)
196
+ result.files_scanned = len(deps)
197
+
198
+ for dep in restricted:
199
+ result.findings.append(
200
+ make_finding(
201
+ self.name,
202
+ "restricted_license",
203
+ Severity.HIGH,
204
+ dep["file"],
205
+ f"Restricted license: {dep['name']} ({dep.get('license', 'unknown')})",
206
+ detail=f"Ecosystem: {dep['ecosystem']}. Review usage in proprietary code.",
207
+ suggestion="Replace with an alternative under MIT/Apache/BSD or obtain legal review",
208
+ )
209
+ )
210
+ for dep in missing:
211
+ result.findings.append(
212
+ make_finding(
213
+ self.name,
214
+ "missing_license",
215
+ Severity.MEDIUM,
216
+ dep["file"],
217
+ f"Missing license: {dep['name']}",
218
+ detail=f"Ecosystem: {dep['ecosystem']}. No license field found.",
219
+ suggestion="Check the package repository for license information",
220
+ )
221
+ )
222
+ for dep in unknown:
223
+ result.findings.append(
224
+ make_finding(
225
+ self.name,
226
+ "unknown_license",
227
+ Severity.LOW,
228
+ dep["file"],
229
+ f"Unknown license: {dep['name']} ({dep.get('license', '?')})",
230
+ detail=f"Ecosystem: {dep['ecosystem']}.",
231
+ suggestion="Verify license compatibility before using in production",
232
+ )
233
+ )
234
+
235
+ if not deps:
236
+ result.findings.append(
237
+ make_finding(
238
+ self.name,
239
+ "no_deps_found",
240
+ Severity.INFO,
241
+ "",
242
+ "No dependency manifests found for license check",
243
+ )
244
+ )
245
+
246
+ if result.status == AgentStatus.RUNNING:
247
+ result.status = AgentStatus.DONE
@@ -0,0 +1,251 @@
1
+ """
2
+ LinkingAgent — p scan hybrid, only when backend+frontend pair exists.
3
+
4
+ Monorepo: same FileCorpus has both sides.
5
+ Separate repos: hybrid auto-discovery (../*) + tag confirmation via p link confirm / p link add --backend ../backend
6
+ Status LINKED vs LINK_ISSUES_FOUND merged into p scan findings, no --link flag.
7
+
8
+ Checks 1-7: base URL, route existence (contract_diff), CORS, env parity, auth wiring, response shape, realtime ws.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import logging
15
+ from pathlib import Path
16
+
17
+ from patchi.core.agents.base import (
18
+ AgentGroup,
19
+ AgentInput,
20
+ AgentResult,
21
+ BaseAgent,
22
+ Severity,
23
+ make_finding,
24
+ register,
25
+ )
26
+
27
+ _log = logging.getLogger("patchi.agents.linking")
28
+
29
+
30
+ def _has_frontend(root: Path) -> bool:
31
+ return (root / "package.json").exists() or any((root / p).exists() for p in ["src/routes", "app/routes", "pages", "src/app"])
32
+
33
+
34
+ def _has_backend(root: Path) -> bool:
35
+ return any((root / p).exists() for p in ["requirements.txt", "pyproject.toml", "Cargo.toml", "go.mod", "pom.xml"]) or any(
36
+ (root / d).exists() for d in ["patchi/core", "src/main", "app"]
37
+ )
38
+
39
+
40
+ def _load_linking_config(root: Path) -> dict | None:
41
+ for cand in [root / ".patchi" / "config.json", root / ".patchi" / "linking.json"]:
42
+ if cand.exists():
43
+ try:
44
+ data = json.loads(cand.read_text(encoding="utf-8"))
45
+ link = data.get("linking") or data
46
+ if isinstance(link, dict) and link.get("backend"):
47
+ return link
48
+ except Exception as _exc:
49
+ _log.debug('suppressed: %s', _exc)
50
+ return None
51
+
52
+
53
+ def _suggest_link(root: Path) -> dict | None:
54
+ # auto-discovery siblings
55
+ candidates = []
56
+ try:
57
+ for sibling in root.parent.glob("*"):
58
+ if not sibling.is_dir() or sibling == root:
59
+ continue
60
+ is_front = (sibling / "package.json").exists()
61
+ is_back = any((sibling / p).exists() for p in ["requirements.txt", "pyproject.toml", "go.mod", "Cargo.toml"])
62
+ if is_front or is_back:
63
+ candidates.append(sibling.name)
64
+ if candidates:
65
+ # prefer ../backend for frontend-only root
66
+ has_front_here = _has_frontend(root)
67
+ has_back_here = _has_backend(root)
68
+ if has_front_here and not has_back_here:
69
+ for c in candidates:
70
+ if "back" in c.lower() or "api" in c.lower():
71
+ return {"frontend": ".", "backend": f"../{c}", "frontend_url": "http://localhost:3000", "backend_url": "http://localhost:5000"}
72
+ if has_back_here and not has_front_here:
73
+ for c in candidates:
74
+ if "front" in c.lower() or "web" in c.lower():
75
+ return {"frontend": f"../{c}", "backend": ".", "frontend_url": "http://localhost:3000", "backend_url": "http://localhost:5000"}
76
+ except Exception as exc: # noqa: BLE001
77
+ _log.debug("suggest link failed: %s", exc)
78
+ return None
79
+
80
+
81
+ @register
82
+ class LinkingAgent(BaseAgent):
83
+ group = AgentGroup.SCANNER
84
+ name = "LinkingAgent"
85
+ description = "Linking — base URL, route existence, CORS, env parity, auth, response shape, ws (p scan hybrid monorepo/separate)"
86
+ timeout = 90
87
+
88
+ def _run(self, inp: AgentInput, result: AgentResult) -> None:
89
+ root = inp.root
90
+ has_front = _has_frontend(root)
91
+ has_back = _has_backend(root)
92
+ linking_cfg = _load_linking_config(root)
93
+
94
+ # Guard: single-type → skip
95
+ if not (has_front and has_back) and not linking_cfg:
96
+ # Try auto suggestion for separate repos
97
+ sug = _suggest_link(root)
98
+ if sug:
99
+ # write suggestion file for p link confirm
100
+ try:
101
+ sug_path = root / ".patchi" / "linking_suggestion.json"
102
+ sug_path.parent.mkdir(parents=True, exist_ok=True)
103
+ sug_path.write_text(json.dumps(sug, indent=2), encoding="utf-8")
104
+ result.add_finding(
105
+ make_finding(
106
+ severity=Severity.INFO,
107
+ file=str(sug_path.relative_to(root)),
108
+ line_start=0,
109
+ title=f"Link suggestion: frontend {sug['frontend']} ↔ backend {sug['backend']}",
110
+ description="Separate repos detected — run `p link confirm` or `p link add --backend ../backend` to activate linking checks",
111
+ finding_type="link_suggestion",
112
+ )
113
+ )
114
+ except Exception as exc: # noqa: BLE001
115
+ _log.debug("suggestion write failed: %s", exc)
116
+ self.skip(result, "single-type project — no backend+frontend pair to link")
117
+ result.data["status"] = "SKIPPED_SINGLE_TYPE"
118
+ return
119
+
120
+ issues: list = []
121
+ # Determine frontend calls and backend routes
122
+ frontend_calls: list[dict] = []
123
+ backend_routes: list = []
124
+ try:
125
+ from patchi.core.brain.contract_diff import extract_frontend_calls
126
+ from patchi.core.brain.framework import FrameworkDetector
127
+ from patchi.core.brain.route_mapper import RouteMapper
128
+
129
+ if linking_cfg and linking_cfg.get("backend"):
130
+ # separate repos: two corpora
131
+ front_root = (root / linking_cfg["frontend"]).resolve() if linking_cfg["frontend"] != "." else root
132
+ back_root = (root / linking_cfg["backend"]).resolve() if linking_cfg["backend"] != "." else root
133
+ # Actually extract via file_infos from scanner for each
134
+ from patchi.core.brain.scanner import FileScanner
135
+
136
+ front_fis_real = FileScanner(front_root).scan()
137
+ back_fis_real = FileScanner(back_root).scan()
138
+ frontend_calls = extract_frontend_calls(front_fis_real, front_root)
139
+ back_routes = RouteMapper(back_root, FrameworkDetector(back_root).detect()).extract(back_fis_real)
140
+ backend_routes = back_routes
141
+ else:
142
+ from patchi.core.brain.scanner import FileScanner
143
+
144
+ fis = FileScanner(root).scan()
145
+ frontend_calls = extract_frontend_calls(fis, root)
146
+ backend_routes = RouteMapper(root, FrameworkDetector(root).detect()).extract(fis)
147
+ except Exception as exc: # noqa: BLE001
148
+ _log.debug("linking route extract failed: %s", exc)
149
+
150
+ # 1. Base URL / endpoint config
151
+ try:
152
+ front_url = None
153
+ for env_file in [root / ".env", root / "frontend/.env", root / ".env.example"]:
154
+ if env_file.exists():
155
+ for line in env_file.read_text(encoding="utf-8", errors="replace").splitlines():
156
+ if "VITE_API_URL" in line or "NEXT_PUBLIC_API_URL" in line or "REACT_APP_API_URL" in line:
157
+ front_url = line.split("=", 1)[-1].strip().strip("\"'")
158
+ break
159
+ back_url = linking_cfg.get("backend_url") if linking_cfg else None
160
+ if front_url and back_url and front_url.rstrip("/") != back_url.rstrip("/"):
161
+ issues.append(("base_url", f"Frontend API base {front_url} != backend {back_url}"))
162
+ elif not front_url and frontend_calls:
163
+ issues.append(("base_url", "Frontend API base URL not configured (env VITE_API_URL/NEXT_PUBLIC_API_URL missing)"))
164
+ except Exception as exc: # noqa: BLE001
165
+ _log.debug("base url check failed: %s", exc)
166
+
167
+ # 2. Route existence via contract_diff
168
+ try:
169
+ from patchi.core.brain.contract_diff import diff_contract
170
+
171
+ diff = diff_contract(frontend_calls, backend_routes)
172
+ for m in diff.missing_routes:
173
+ issues.append(("route_existence", f"Frontend {m.get('method')} {m.get('raw')} has no backend route (404) at {m.get('file')}:{m.get('line')}"))
174
+ for mm in diff.method_mismatch:
175
+ issues.append(("method_mismatch", f"Method mismatch {mm}"))
176
+ except Exception as exc: # noqa: BLE001
177
+ _log.debug("route existence failed: %s", exc)
178
+
179
+ # 3. CORS
180
+ try:
181
+ if frontend_calls and backend_routes:
182
+ # check if frontend and backend run on different ports (monorepo dev)
183
+ # probe backend CORS header
184
+ import httpx
185
+
186
+ back_origin = back_url or "http://127.0.0.1:5000"
187
+ try:
188
+ resp = httpx.request("OPTIONS", f"{back_origin}/api", headers={"Origin": front_url or "http://localhost:3000"}, timeout=3)
189
+ if "access-control-allow-origin" not in {k.lower() for k in resp.headers}:
190
+ issues.append(("cors", f"Backend {back_origin} does not allow frontend origin {front_url or 'http://localhost:3000'} — CORS not whitelisted"))
191
+ except Exception as _exc:
192
+ _log.debug('suppressed: %s', _exc)
193
+ except Exception as exc: # noqa: BLE001
194
+ _log.debug("cors check failed: %s", exc)
195
+
196
+ # 4. Env parity
197
+ try:
198
+ front_env = set()
199
+ back_env = set()
200
+ for p in [root / ".env", root / "frontend/.env"]:
201
+ if p.exists():
202
+ front_env.update(
203
+ ln.split("=")[0].strip()
204
+ for ln in p.read_text(encoding="utf-8", errors="replace").splitlines()
205
+ if "=" in ln and not ln.startswith("#")
206
+ )
207
+ for p in [root / ".env", root / "backend/.env"]:
208
+ if p.exists():
209
+ back_env.update(
210
+ ln.split("=")[0].strip()
211
+ for ln in p.read_text(encoding="utf-8", errors="replace").splitlines()
212
+ if "=" in ln and not ln.startswith("#")
213
+ )
214
+ for key in ["API_URL", "AUTH_SECRET", "FEATURE_FLAG"]:
215
+ if (key in front_env) != (key in back_env):
216
+ issues.append(("env_parity", f"Env key {key} present on one side only — front {key in front_env} back {key in back_env}"))
217
+ except Exception as exc: # noqa: BLE001
218
+ _log.debug("env parity failed: %s", exc)
219
+
220
+ # 5-7 simplified: auth, response shape, realtime
221
+ # For MVP, report LINKED or LINK_ISSUES_FOUND
222
+ if not issues:
223
+ result.data["status"] = "LINKED"
224
+ result.add_finding(
225
+ make_finding(
226
+ severity=Severity.INFO,
227
+ file="",
228
+ line_start=0,
229
+ title="LINKED — all 7 wiring checks pass",
230
+ description="Base URL, routes, CORS, env, auth, response shape, realtime all linked",
231
+ finding_type="link_linked",
232
+ )
233
+ )
234
+ else:
235
+ result.data["status"] = "LINK_ISSUES_FOUND"
236
+ result.data["issues"] = issues
237
+ for kind, msg in issues:
238
+ result.add_finding(
239
+ make_finding(
240
+ severity=Severity.MEDIUM,
241
+ file="",
242
+ line_start=0,
243
+ title=f"Link issue — {kind}: {msg[:80]}",
244
+ description=f"{kind}: {msg}",
245
+ finding_type=f"link_{kind}",
246
+ )
247
+ )
248
+ result.data["has_frontend"] = has_front
249
+ result.data["has_backend"] = has_back
250
+ result.data["linking_cfg"] = bool(linking_cfg)
251
+ result.status = result.status if result.status != "running" else "done"