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,210 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import logging
5
+ import subprocess
6
+ import tempfile
7
+ import textwrap
8
+ from pathlib import Path
9
+
10
+ _log = logging.getLogger("patchi.debug.adapters.powershell")
11
+
12
+
13
+ class PowerShellDebugAdapter:
14
+ def __init__(self, script: str | Path, args: list[str] | None = None):
15
+ self._script = Path(script).resolve()
16
+ self._args = args or []
17
+
18
+ def available(self) -> bool:
19
+ return _find_pwsh() is not None
20
+
21
+ def capture_exception(self, timeout: float = 60) -> dict | None:
22
+ pwsh_path = _find_pwsh()
23
+ if pwsh_path is None:
24
+ _log.warning("pwsh not found on PATH")
25
+ return None
26
+
27
+ harness_code = _build_harness(self._script, self._args)
28
+
29
+ with tempfile.NamedTemporaryFile(
30
+ mode="w", suffix=".ps1", delete=False, encoding="utf-8"
31
+ ) as f:
32
+ f.write(harness_code)
33
+ tmp_path = Path(f.name)
34
+
35
+ try:
36
+ proc = subprocess.run(
37
+ [str(pwsh_path), "-NoProfile", "-File", str(tmp_path)],
38
+ capture_output=True,
39
+ text=True,
40
+ timeout=timeout,
41
+ )
42
+ except subprocess.TimeoutExpired:
43
+ _log.warning("pwsh harness timed out after %s seconds", timeout)
44
+ return None
45
+ finally:
46
+ tmp_path.unlink(missing_ok=True)
47
+
48
+ if proc.returncode == 0:
49
+ return None
50
+
51
+ for line in proc.stdout.splitlines():
52
+ stripped = line.strip()
53
+ prefix = "PATCHI_DEBUG_CAPTURE:"
54
+ if stripped.startswith(prefix):
55
+ try:
56
+ return json.loads(stripped[len(prefix) :])
57
+ except json.JSONDecodeError:
58
+ _log.warning("PATCHI_DEBUG_CAPTURE JSON parse error: %.200s", stripped)
59
+ return None
60
+
61
+ _log.debug(
62
+ "pwsh exited code=%d but no PATCHI_DEBUG_CAPTURE token in stdout",
63
+ proc.returncode,
64
+ )
65
+ return None
66
+
67
+ def _resolve_variables(self, var_ref: int, max_depth: int = 2, depth: int = 0) -> dict:
68
+ # PowerShell adapter uses harness-based debugging, not DAP protocol variables
69
+ msg = "PowerShell adapter uses harness-based debugging; variable resolution not available via DAP"
70
+ return {"__not_supported__": msg}
71
+
72
+
73
+ def _find_pwsh() -> Path | None:
74
+ try:
75
+ result = subprocess.run(
76
+ ["where", "pwsh"],
77
+ capture_output=True,
78
+ text=True,
79
+ timeout=10,
80
+ )
81
+ if result.returncode == 0:
82
+ for line in result.stdout.splitlines():
83
+ p = Path(line.strip())
84
+ if p.is_file():
85
+ return p
86
+ except (subprocess.SubprocessError, OSError):
87
+ pass
88
+ return None
89
+
90
+
91
+ def _ps_quote(s: str) -> str:
92
+ """PowerShell single-quote a string, doubling any embedded single quotes."""
93
+ return "'" + s.replace("'", "''") + "'"
94
+
95
+
96
+ def _build_harness(script: Path, args: list[str] | None) -> str:
97
+ script_quoted = _ps_quote(str(script))
98
+ # args passed as a param block at top of target script, or use splatting
99
+ # For simplicity, we'll set $args before dot-sourcing
100
+ args_setup = ""
101
+ if args:
102
+ args_vals = ", ".join(_ps_quote(a) for a in args)
103
+ args_setup = f"$args = @({args_vals})"
104
+ return textwrap.dedent(f"""\
105
+ $ErrorActionPreference = 'Stop'
106
+ {args_setup}
107
+
108
+ $__skip = @(
109
+ '$', '^', '?', '_', 'args', 'input', 'true', 'false', 'null',
110
+ 'Host', 'MyInvocation', 'ExecutionContext', 'PSBoundParameters',
111
+ 'PSVersionTable', 'PSCulture', 'PSUICulture', 'PSEdition', 'ShellId',
112
+ 'HOME', 'PID', 'IsCoreCLR', 'IsWindows', 'IsLinux', 'IsMacOS',
113
+ 'PSCommandPath', 'PSScriptRoot', 'PSHOME', 'PSModulePath',
114
+ 'EnabledExperimentalFeatures', 'Error', 'ErrorActionPreference',
115
+ 'ProgressPreference', 'VerbosePreference', 'DebugPreference',
116
+ 'WarningPreference', 'InformationPreference', 'ConfirmPreference',
117
+ 'WhatIfPreference', 'NestedPromptLevel', 'FormatEnumerationLimit',
118
+ 'MaximumHistoryCount', 'OutputEncoding', 'PSDefaultParameterValues',
119
+ 'PSEmailServer', 'PSNativeCommandArgumentPassing',
120
+ 'PSNativeCommandUseErrorActionPreference', 'PSSessionApplicationName',
121
+ 'PSSessionConfigurationName', 'PSSessionOption', 'PSStyle', 'PWD',
122
+ 'PROFILE', 'StackTrace', 'PSItem'
123
+ )
124
+
125
+ try {{
126
+ . {script_quoted}
127
+ }} catch {{
128
+ $__err = $_
129
+ $__capture = @{{
130
+ exception = "$($__err.Exception.GetType().Name): $($__err.Exception.Message)"
131
+ thread_id = 1
132
+ frames = @()
133
+ }}
134
+
135
+ # Parse ScriptStackTrace for frames
136
+ $__stackText = $__err.ScriptStackTrace
137
+ if ($__stackText) {{
138
+ $__lines = $__stackText -split "`r?`n" | Where-Object {{ $_ -match '^at\\s' }}
139
+ foreach ($__line in $__lines) {{
140
+ # Format: "at <Function>, <Path>: line <Number>"
141
+ if ($__line -match '^at\\s+(.+?),\\s+(.+?):\\s*line\\s+(\\d+)') {{
142
+ $__func = $matches[1].Trim()
143
+ $__path = $matches[2].Trim()
144
+ $__lineNum = [int]$matches[3]
145
+ $__capture.frames += @{{
146
+ name = $__func
147
+ path = $__path
148
+ line = $__lineNum
149
+ locals = @{{}}
150
+ }}
151
+ }}
152
+ }}
153
+ }}
154
+
155
+ # Add InvocationInfo frame if not already in stack
156
+ $__inv = $__err.InvocationInfo
157
+ if ($__inv -and $__inv.ScriptName) {{
158
+ $__found = $false
159
+ foreach ($__f in $__capture.frames) {{
160
+ if ($__f.path -eq $__inv.ScriptName -and $__f.line -eq $__inv.ScriptLineNumber) {{
161
+ $__found = $true
162
+ break
163
+ }}
164
+ }}
165
+ if (-not $__found) {{
166
+ $__capture.frames += @{{
167
+ name = if ($__inv.CommandName) {{ $__inv.CommandName }} else {{ '<ScriptBlock>' }}
168
+ path = $__inv.ScriptName
169
+ line = $__inv.ScriptLineNumber
170
+ locals = @{{}}
171
+ }}
172
+ }}
173
+ }}
174
+
175
+ # Capture script-level variables from scope 0 (catch scope = script scope due to dot-source)
176
+ $__locals = @{{}}
177
+ $__vars = Get-Variable -Scope 0 -ErrorAction SilentlyContinue
178
+ foreach ($__v in $__vars) {{
179
+ $__n = $__v.Name
180
+ # Skip internal harness vars (__*), automatic vars, preference vars, and regex match vars
181
+ if ($__n -notlike '__*' -and $__n -notlike '*Preference' -and $__n -notin $__skip -and $__n -notin @('foreach','switch','Error','ErrorView','MyInvocation','PSBoundParameters','PSVersionTable','PSCulture','PSUICulture','PSEdition','ShellId','HOME','PID','IsCoreCLR','IsWindows','IsLinux','IsMacOS','PSCommandPath','PSScriptRoot','PSHOME','PSModulePath','EnabledExperimentalFeatures','ErrorActionPreference','ProgressPreference','VerbosePreference','DebugPreference','WarningPreference','InformationPreference','ConfirmPreference','WhatIfPreference','NestedPromptLevel','FormatEnumerationLimit','MaximumHistoryCount','OutputEncoding','PSDefaultParameterValues','PSEmailServer','PSNativeCommandArgumentPassing','PSNativeCommandUseErrorActionPreference','PSSessionApplicationName','PSSessionConfigurationName','PSSessionOption','PSStyle','PWD','PROFILE','StackTrace','PSItem','?','_','args','input','true','false','null','$','^','Host','ExecutionContext','matches')) {{
182
+ try {{
183
+ if ($null -ne $__v.Value) {{
184
+ $__val = "$($__v.Value)"
185
+ if ($__val.Length -gt 200) {{ $__val = $__val.Substring(0,200) + '...' }}
186
+ }} else {{
187
+ $__val = '$null'
188
+ }}
189
+ }} catch {{
190
+ $__val = '{{?}}'
191
+ }}
192
+ $__locals[$__n] = $__val
193
+ }}
194
+ }}
195
+ # Assign captured locals to the SCRIPT frame (index 1 = script level), not function frame
196
+ if ($__capture.frames.Count -gt 1) {{
197
+ $__capture.frames[1].locals = $__locals
198
+ }} elseif ($__capture.frames.Count -gt 0) {{
199
+ $__capture.frames[0].locals = $__locals
200
+ }}
201
+
202
+ try {{
203
+ $__json = $__capture | ConvertTo-Json -Depth 10 -Compress
204
+ Write-Output "PATCHI_DEBUG_CAPTURE:$__json"
205
+ }} catch {{
206
+ Write-Output "PATCHI_DEBUG_CAPTURE:SERIALIZE_FAILED"
207
+ }}
208
+ exit 1
209
+ }}
210
+ """)
@@ -0,0 +1,244 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import logging
5
+ import subprocess
6
+ import sys
7
+ import textwrap
8
+ from pathlib import Path
9
+
10
+ _LOG = logging.getLogger("patchi.debug.adapters.python")
11
+
12
+
13
+ class PythonDebugAdapter:
14
+ def __init__(self, script: str | Path, args: list[str] | None = None):
15
+ self._script = Path(script).resolve()
16
+ self._args = args or []
17
+
18
+ def capture_exception(self, timeout: float = 60) -> dict | None:
19
+ # 1) Standalone exec harness — catches module-level crashes in plain
20
+ # scripts (the fast, dependency-free path).
21
+ cap = self._capture_standalone(timeout)
22
+ if cap is not None:
23
+ return cap
24
+
25
+ # 2) Pytest harness — a real test file imports cleanly but only fails
26
+ # *inside* a test function, which the standalone harness can't see.
27
+ if _looks_like_test_file(self._script):
28
+ cap = self._capture_pytest(timeout)
29
+ if cap is not None:
30
+ return cap
31
+
32
+ return None
33
+
34
+ def _capture_standalone(self, timeout: float) -> dict | None:
35
+ harness = _build_harness(self._script)
36
+ cmd = [sys.executable, "-Xfrozen_modules=off", "-c", harness, *self._args]
37
+ _LOG.debug("running python harness: %s …", " ".join(cmd[:4]))
38
+
39
+ try:
40
+ proc = subprocess.run(
41
+ cmd,
42
+ capture_output=True,
43
+ text=True,
44
+ timeout=timeout,
45
+ )
46
+ except subprocess.TimeoutExpired:
47
+ _LOG.warning("python harness timed out after %s seconds", timeout)
48
+ return None
49
+ except OSError as e:
50
+ _LOG.warning("python harness failed to start: %s", e)
51
+ return None
52
+ if proc.returncode == 0:
53
+ return None
54
+
55
+ for stream in (proc.stdout, proc.stderr):
56
+ for line in stream.splitlines():
57
+ stripped = line.strip()
58
+ prefix = "PATCHI_DEBUG_CAPTURE:"
59
+ if stripped.startswith(prefix):
60
+ try:
61
+ return json.loads(stripped[len(prefix) :])
62
+ except json.JSONDecodeError:
63
+ _LOG.warning("PATCHI_DEBUG_CAPTURE JSON parse error: %.200s", stripped)
64
+ return None
65
+
66
+ _LOG.debug("python exited code=%d but no capture token", proc.returncode)
67
+ return None
68
+
69
+ def _capture_pytest(self, timeout: float) -> dict | None:
70
+ harness = _build_pytest_harness(self._script)
71
+ cmd = [sys.executable, "-Xfrozen_modules=off", "-c", harness, *self._args]
72
+ _LOG.debug("running pytest harness: %s …", " ".join(cmd[:4]))
73
+
74
+ try:
75
+ proc = subprocess.run(
76
+ cmd,
77
+ capture_output=True,
78
+ text=True,
79
+ timeout=timeout,
80
+ )
81
+ except subprocess.TimeoutExpired:
82
+ _LOG.warning("pytest harness timed out after %s seconds", timeout)
83
+ return None
84
+
85
+ for stream in (proc.stderr, proc.stdout):
86
+ for line in stream.splitlines():
87
+ stripped = line.strip()
88
+ prefix = "PATCHI_DEBUG_CAPTURE:"
89
+ if stripped.startswith(prefix):
90
+ try:
91
+ return json.loads(stripped[len(prefix) :])
92
+ except json.JSONDecodeError:
93
+ _LOG.warning("PATCHI_DEBUG_CAPTURE JSON parse error: %.200s", stripped)
94
+ return None
95
+
96
+ _LOG.debug("pytest exited code=%d but no capture token", proc.returncode)
97
+ return None
98
+
99
+ def _resolve_variables(self, var_ref: int, max_depth: int = 2, depth: int = 0) -> dict:
100
+ # Python adapter uses harness-based debugging, not DAP protocol variables
101
+ msg = "Python adapter uses harness-based debugging; variable resolution not available via DAP"
102
+ return {"__not_supported__": msg}
103
+
104
+
105
+ def _looks_like_test_file(script: Path) -> bool:
106
+ """Heuristic: does the file look like a pytest/unittest test module?"""
107
+ try:
108
+ text = script.read_text(encoding="utf-8-sig", errors="replace")
109
+ except OSError:
110
+ return False
111
+ if "pytest" in text or "unittest" in text:
112
+ return True
113
+ if "def test_" in text or "class Test" in text:
114
+ return True
115
+ return False
116
+
117
+
118
+ def _build_harness(script: Path) -> str:
119
+ """Generate a Python script that wraps the target in an exception hook
120
+ that captures local variables and prints them as JSON on stdout.
121
+
122
+ ``__file__`` and ``__name__`` are injected so scripts that read them at
123
+ module level don't blow up (``python -c`` otherwise has no ``__file__``).
124
+ """
125
+ script_code = script.read_text(encoding="utf-8-sig", errors="replace")
126
+ return textwrap.dedent(f"""\
127
+ import json, sys
128
+
129
+ def _patchi_capture(exc_type, exc_value, exc_tb):
130
+ frames = []
131
+ tb = exc_tb
132
+ while tb:
133
+ frame = tb.tb_frame
134
+ fname = frame.f_code.co_name
135
+ fpath = frame.f_code.co_filename
136
+ lineno = tb.tb_lineno
137
+ # Only include user frames, not the harness itself
138
+ if '<string>' not in fpath:
139
+ # Capture locals for every user frame, redacting secrets
140
+ # so credentials never reach the AI prompt.
141
+ locals_ = {{k: _patchi_redact(k, v) for k, v in frame.f_locals.items()
142
+ if not k.startswith('_')}}
143
+ frames.append({{
144
+ "name": fname,
145
+ "path": fpath,
146
+ "line": lineno,
147
+ "locals": locals_,
148
+ }})
149
+ tb = tb.tb_next
150
+ frames.reverse()
151
+ data = {{
152
+ "exception": f"{{exc_type.__name__}}: {{exc_value}}",
153
+ "thread_id": 1,
154
+ "frames": frames,
155
+ }}
156
+ # stderr + token prefix: the target script may print to stdout
157
+ # before crashing, so a bare JSON dump would be polluted.
158
+ print("PATCHI_DEBUG_CAPTURE:" + json.dumps(data), file=sys.stderr, flush=True)
159
+
160
+ import re as _re
161
+ _SECRET_NAMES = _re.compile(
162
+ r"(password|passwd|secret|token|api[_-]?key|apikey|credential|authorization|private[_-]?key|access[_-]?key|session[_-]?id|aws[_-]?secret)", _re.I)
163
+
164
+ def _patchi_redact(k, v):
165
+ if _SECRET_NAMES.search(k):
166
+ return "{{REDACTED}}"
167
+ return repr(v)
168
+
169
+ sys.excepthook = _patchi_capture
170
+
171
+ _globals = {{'__name__': '__main__', '__file__': {str(script)!r}, '__builtins__': __builtins__}}
172
+ exec(compile({script_code!r}, {str(script)!r}, "exec"), _globals)
173
+ """)
174
+
175
+
176
+ def _build_pytest_harness(script: Path) -> str:
177
+ """Generate a harness that runs the target file under pytest and captures
178
+ the first test failure's traceback + locals via a small in-process plugin.
179
+
180
+ The plugin serializes the capture and prints a single line prefixed with
181
+ ``PATCHI_DEBUG_CAPTURE:`` so the adapter can parse it reliably.
182
+ """
183
+ return textwrap.dedent(f"""\
184
+ import json, sys
185
+ import pytest
186
+
187
+ _captured = {{}}
188
+
189
+ def _patchi_capture_frames(exc_tb):
190
+ import re as _re
191
+ import sys as _sys
192
+ _SECRET_NAMES = _re.compile(
193
+ r"(password|passwd|secret|token|api[_-]?key|apikey|credential|authorization|private[_-]?key|access[_-]?key|session[_-]?id|aws[_-]?secret)", _re.I)
194
+
195
+ def _redact(k, v):
196
+ if _SECRET_NAMES.search(k):
197
+ return "{{REDACTED}}"
198
+ return repr(v)
199
+
200
+ frames = []
201
+ tb = exc_tb
202
+ while tb:
203
+ frame = tb.tb_frame
204
+ fname = frame.f_code.co_name
205
+ fpath = frame.f_code.co_filename
206
+ lineno = tb.tb_lineno
207
+ # Keep user frames only: skip the harness and Python internals.
208
+ norm = fpath.replace("\\\\", "/")
209
+ is_stdlib = norm.startswith(_sys.prefix.replace("\\\\", "/") + "/")
210
+ if "<string>" not in fpath and "site-packages" not in norm and not is_stdlib:
211
+ locals_ = {{k: _redact(k, v) for k, v in frame.f_locals.items()
212
+ if not k.startswith('_')}}
213
+ frames.append({{
214
+ "name": fname,
215
+ "path": fpath,
216
+ "line": lineno,
217
+ "locals": locals_,
218
+ }})
219
+ tb = tb.tb_next
220
+ frames.reverse()
221
+ return frames
222
+
223
+ class _CapturePlugin:
224
+ def pytest_runtest_makereport(self, item, call):
225
+ global _captured
226
+ if _captured:
227
+ return
228
+ if call.when == "call" and call.excinfo is not None:
229
+ exc = call.excinfo
230
+ _captured = {{
231
+ "exception": f"{{exc.type.__name__}}: {{exc.value}}",
232
+ "thread_id": 1,
233
+ "frames": _patchi_capture_frames(exc.tb),
234
+ }}
235
+
236
+ def pytest_sessionfinish(self, session, exitstatus):
237
+ if _captured:
238
+ # stderr: pytest writes its own progress chars to stdout,
239
+ # so a token on stdout would be prefixed with them.
240
+ print("PATCHI_DEBUG_CAPTURE:" + json.dumps(_captured), file=sys.stderr, flush=True)
241
+
242
+ sys.exit(pytest.main([{str(script)!r}, "-x", "-q", "-s", "--no-header", "--tb=short"],
243
+ plugins=[_CapturePlugin()]))
244
+ """)
@@ -0,0 +1,210 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from pathlib import Path
5
+
6
+ from patchi.core.debug.adapters.codelldb import CodeLLDBAdapter
7
+ from patchi.core.debug.adapters.node import NodeDebugAdapter
8
+ from patchi.core.debug.adapters.powershell import PowerShellDebugAdapter
9
+ from patchi.core.debug.adapters.python import PythonDebugAdapter
10
+
11
+ _log = logging.getLogger("patchi.core.debug.capture")
12
+
13
+ # File types the harnesses can actually run. Anything else is refused so a
14
+ # finding pointing at, say, a log file can never be executed as code.
15
+ _CAPTURE_EXTENSIONS = frozenset({".py", ".js", ".ts", ".ps1"})
16
+
17
+
18
+ class DebugCapture:
19
+ """Structured capture of runtime state at the moment of an exception.
20
+
21
+ Designed for AI fix-agent consumption — compact, information-dense,
22
+ variable-oriented. Every method returns plain dicts / lists so the
23
+ caller can serialize or format however it likes.
24
+ """
25
+
26
+ def __init__(self, raw: dict) -> None:
27
+ self._raw = raw
28
+
29
+ @property
30
+ def exception(self) -> str:
31
+ return self._raw.get("exception", "")
32
+
33
+ @property
34
+ def thread_id(self) -> int:
35
+ return self._raw.get("thread_id", 0)
36
+
37
+ @property
38
+ def frames(self) -> list[dict]:
39
+ return self._raw.get("frames", [])
40
+
41
+ @property
42
+ def trigger_frame(self) -> int:
43
+ """Index of the deepest non-library frame (the 'real' crash site)."""
44
+ for i, f in enumerate(self.frames):
45
+ if f.get("path") and "site-packages" not in f["path"].replace("\\", "/"):
46
+ return i
47
+ return 0
48
+
49
+ def variable_summary(self, max_var_len: int = 80) -> str:
50
+ """Human- and AI-readable one-line-per-frame summary.
51
+
52
+ Example output::
53
+
54
+ #1 process_order myapp/orders.py:42
55
+ total -> None (expected int)
56
+ user -> User(id=123, name='Alice')
57
+ items -> [<list: 3 items>]
58
+ """
59
+ lines: list[str] = []
60
+ for i, f in enumerate(self.frames):
61
+ marker = " <-- trigger" if i == self.trigger_frame else ""
62
+ lines.append(
63
+ f"#{i} {f['name']} {_short_path(f.get('path', ''))}:{f.get('line', 0)}{marker}"
64
+ )
65
+ for k, v in f.get("locals", {}).items():
66
+ val = _short_value(v, max_var_len)
67
+ lines.append(f" {k} -> {val}")
68
+ return "\n".join(lines)
69
+
70
+ def to_dict(self, max_vars: int = 15) -> dict:
71
+ """Compact dict ready for JSON serialisation.
72
+
73
+ ``max_vars`` limits the total number of variable entries across all
74
+ frames to keep the payload small for LLM context windows.
75
+ """
76
+ capped = []
77
+ remaining = max_vars
78
+ for f in self.frames:
79
+ frame: dict = {
80
+ "name": f["name"],
81
+ "path": _short_path(f.get("path", "")),
82
+ "line": f.get("line", 0),
83
+ }
84
+ if remaining > 0:
85
+ locals_capped = {}
86
+ for k, v in list(f.get("locals", {}).items())[:remaining]:
87
+ locals_capped[k] = _short_value(v, 60)
88
+ frame["locals"] = locals_capped
89
+ remaining -= len(locals_capped)
90
+ capped.append(frame)
91
+ return {
92
+ "exception": self.exception,
93
+ "trigger_frame": self.trigger_frame,
94
+ "frames": capped,
95
+ }
96
+
97
+
98
+ def capture_exception(
99
+ script: str | Path,
100
+ args: list[str] | None = None,
101
+ timeout: float = 60,
102
+ ) -> DebugCapture | None:
103
+ """Convenience: run *script* under the debugger and return a
104
+ :class:`DebugCapture` or *None* if it exited cleanly.
105
+
106
+ This is the primary entry point for Patchi fix agents.
107
+ """
108
+ ext = Path(script).suffix.lower()
109
+
110
+ if ext == ".py":
111
+ adapter = PythonDebugAdapter(script, args)
112
+ elif ext == ".ps1":
113
+ adapter = PowerShellDebugAdapter(script, args)
114
+ elif ext in (".js", ".ts"):
115
+ adapter = NodeDebugAdapter(script, args)
116
+ else:
117
+ _log.warning("unsupported file extension %s – defaulting to Python harness", ext)
118
+ adapter = PythonDebugAdapter(script, args)
119
+
120
+ raw = adapter.capture_exception(timeout=timeout)
121
+ if raw is None:
122
+ return None
123
+ return DebugCapture(raw)
124
+
125
+
126
+ def capture_rust_crash(
127
+ binary: str | Path,
128
+ args: list[str] | None = None,
129
+ timeout: float = 60,
130
+ ) -> DebugCapture | None:
131
+ """Run a compiled Rust (or C++) binary under CodeLLDB and return a
132
+ :class:`DebugCapture` if it crashes, or *None* if it exits cleanly.
133
+
134
+ If codelldb is not installed, returns *None* gracefully.
135
+ """
136
+ adapter = CodeLLDBAdapter(binary, args)
137
+ raw = adapter.capture_crash(timeout=timeout)
138
+ if raw is None:
139
+ return None
140
+ return DebugCapture(raw)
141
+
142
+
143
+ def capture_powershell_crash(
144
+ script: str | Path,
145
+ args: list[str] | None = None,
146
+ timeout: float = 60,
147
+ ) -> DebugCapture | None:
148
+ """Run a PowerShell script under the debug harness and return a
149
+ :class:`DebugCapture` if it crashes, or *None* if it exits cleanly.
150
+
151
+ If pwsh is not installed, returns *None* gracefully.
152
+ """
153
+ adapter = PowerShellDebugAdapter(script, args)
154
+ raw = adapter.capture_exception(timeout=timeout)
155
+ if raw is None:
156
+ return None
157
+ return DebugCapture(raw)
158
+
159
+
160
+ def debug_context_from_finding(finding: dict, root: Path) -> dict | None:
161
+ """Return a debug-context dict if *finding* is a test failure, else *None*.
162
+
163
+ Runs the failing test file under the debugger harness and returns compact
164
+ runtime-state data for the AI prompt.
165
+ """
166
+ if finding.get("type") != "test_failure":
167
+ return None
168
+ test_file = finding.get("file", "")
169
+ if not test_file:
170
+ return None
171
+ # The finding path comes from the scanned repo — never let it escape the
172
+ # project root: an absolute path or "../" traversal would make us execute
173
+ # an arbitrary file outside the project as code.
174
+ root_resolved = Path(root).resolve()
175
+ test_path = (root_resolved / test_file).resolve()
176
+ try:
177
+ test_path.relative_to(root_resolved)
178
+ except ValueError:
179
+ _log.warning("refusing to capture outside project root: %s", test_file)
180
+ return None
181
+ if not test_path.is_file():
182
+ return None
183
+ if test_path.suffix.lower() not in _CAPTURE_EXTENSIONS:
184
+ _log.warning("refusing to capture unsupported file type: %s", test_file)
185
+ return None
186
+
187
+ cap = capture_exception(test_path, timeout=30)
188
+ if cap is None:
189
+ return None
190
+ return cap.to_dict(max_vars=15)
191
+
192
+
193
+ def _short_path(p: str) -> str:
194
+ try:
195
+ return str(Path(p).relative_to(Path.cwd()))
196
+ except ValueError:
197
+ return p
198
+
199
+
200
+ def _short_value(val, max_len: int) -> str:
201
+ if isinstance(val, dict):
202
+ if val.get("__depth_limit__"):
203
+ return "{...}"
204
+ if val.get("__error__"):
205
+ return "{?}"
206
+ return f"{{{', '.join(_short_value(v, 20) for v in list(val.values())[:3])}{'…' if len(val) > 3 else ''}}}"
207
+ s = str(val)
208
+ if len(s) > max_len:
209
+ return s[: max_len - 3] + "..."
210
+ return s