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,379 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import os
5
+ import re
6
+ import socket
7
+ import subprocess
8
+ import time
9
+ from pathlib import Path
10
+
11
+ from patchi.core.debug.dap_client import DAPClient, DAPError
12
+
13
+ _log = logging.getLogger("patchi.debug.adapters.codelldb")
14
+
15
+ # Well-known installation paths for CodeLLDB
16
+ _CODELDB_CANDIDATES: list[str] = []
17
+
18
+ _vscode_base = Path(os.environ.get("USERPROFILE", "~")) / ".vscode" / "extensions"
19
+ if _vscode_base.is_dir():
20
+ for entry in _vscode_base.iterdir():
21
+ if entry.name.startswith("vadimcn.vscode-lldb"):
22
+ _CODELDB_CANDIDATES.append(str(entry / "codelldb.exe"))
23
+ _CODELDB_CANDIDATES.append(str(entry / "codelldb"))
24
+ _CODELDB_CANDIDATES.append(str(entry / "adapter" / "codelldb.exe"))
25
+ _CODELDB_CANDIDATES.append(str(entry / "lldb" / "bin" / "lldb-vscode.exe"))
26
+
27
+ _vscode_insiders = Path(os.environ.get("USERPROFILE", "~")) / ".vscode-insiders" / "extensions"
28
+ if _vscode_insiders.is_dir():
29
+ for entry in _vscode_insiders.iterdir():
30
+ if entry.name.startswith("vadimcn.vscode-lldb"):
31
+ _CODELDB_CANDIDATES.append(str(entry / "codelldb.exe"))
32
+ _CODELDB_CANDIDATES.append(str(entry / "codelldb"))
33
+ _CODELDB_CANDIDATES.append(str(entry / "adapter" / "codelldb.exe"))
34
+
35
+ # Also check PATH / common standalone installs
36
+ _PATH_CANDIDATES = ["codelldb", "codelldb.exe", "lldb-vscode", "lldb-vscode.exe"]
37
+
38
+ # Symbol reached for every Rust panic (unwinding and abort alike).
39
+ _PANIC_SYMBOL = "__rustc::__rust_start_panic"
40
+
41
+ # ``frame #N: 0xADDR module`func(args) at file:line[:col]`` (line may carry
42
+ # trailing markers like "[inlined]"). The location group is lazy so Windows
43
+ # drive-letter colons in absolute paths don't truncate it.
44
+ _FRAME_RE = re.compile(
45
+ r"frame #(\d+): 0x[0-9a-f]+ [^`]+`([^ ]+)"
46
+ r"(?: at (.+?):(\d+)(?::(\d+))?)?"
47
+ )
48
+
49
+
50
+ def find_codelldb() -> str | None:
51
+ for p in _CODELDB_CANDIDATES:
52
+ if Path(p).is_file():
53
+ return p
54
+ for name in _PATH_CANDIDATES:
55
+ which = Path(name)
56
+ if which.is_file():
57
+ return str(which.resolve())
58
+ import shutil
59
+
60
+ found = shutil.which(name)
61
+ if found:
62
+ return found
63
+ return None
64
+
65
+
66
+ def _find_lldb_cli(exe: str) -> str | None:
67
+ """Bundled LLDB command-line binary that ships inside the extension."""
68
+ root = Path(exe).resolve()
69
+ candidates = [
70
+ root.parent.parent / "lldb" / "bin" / "lldb.exe", # adapter\codelldb.exe
71
+ root.parent / "lldb" / "bin" / "lldb.exe", # extension-root codelldb.exe
72
+ root.parent.parent / "lldb" / "bin" / "lldb", # non-Windows
73
+ root.parent / "lldb" / "bin" / "lldb",
74
+ ]
75
+ for c in candidates:
76
+ if c.is_file():
77
+ return str(c)
78
+ return None
79
+
80
+
81
+ class CodeLLDBAdapter:
82
+ """Drives CodeLLDB (LLDB debugger for Rust / C++) to capture post-crash state.
83
+
84
+ Uses the DAP server first (richer data: locals per frame), then falls back
85
+ to the bundled LLDB command line, which is deterministic. Usage::
86
+
87
+ adapter = CodeLLDBAdapter("/path/to/binary")
88
+ capture = adapter.capture_crash(timeout=60)
89
+ print(capture.get("exception", ""))
90
+ """
91
+
92
+ def __init__(self, binary: str | Path, args: list[str] | None = None):
93
+ self._binary = Path(binary).resolve()
94
+ self._args = args or []
95
+
96
+ def available(self) -> bool:
97
+ """Check if a usable codelldb binary can be found."""
98
+ return find_codelldb() is not None
99
+
100
+ def capture_crash(self, timeout: float = 60) -> dict | None:
101
+ """Return a structured capture dict if the target crashes, else *None*."""
102
+ exe = find_codelldb()
103
+ if not exe:
104
+ _log.info("codelldb not found — skipping Rust/C++ debug capture")
105
+ return None
106
+
107
+ deadline = time.monotonic() + max(timeout, 1)
108
+ cap = self._capture_dap(exe, min(6.0, max(3.0, timeout / 4)))
109
+ if cap is not None:
110
+ return cap
111
+ if time.monotonic() < deadline:
112
+ cap = self._capture_cli(exe, deadline - time.monotonic())
113
+ return cap
114
+
115
+ # ── DAP attempt (best effort, bounded) ──────────────────────────────
116
+
117
+ def _capture_dap(self, exe: str, budget: float) -> dict | None:
118
+ port = self._pick_port()
119
+ client = DAPClient(port=port)
120
+ proc = self._start_server(exe, port)
121
+ deadline = time.monotonic() + budget
122
+ try:
123
+ client.connect(timeout=min(10.0, budget))
124
+ client.initialize()
125
+ client.send_request(
126
+ "launch",
127
+ {
128
+ "type": "lldb",
129
+ "request": "launch",
130
+ "name": "Patchi Debug",
131
+ "program": str(self._binary),
132
+ "args": self._args or [],
133
+ "console": "internalConsole",
134
+ "sourceLanguages": ["rust"],
135
+ # Deterministic entry stop; stopOnEntry alone is racy on Windows.
136
+ "processCreateCommands": ["process launch -s"],
137
+ },
138
+ )
139
+ # Set the panic breakpoint before configurationDone so its
140
+ # response is not starved by the deferred launch response.
141
+ client.send_request(
142
+ "setFunctionBreakpoints",
143
+ {
144
+ "breakpoints": [{"name": _PANIC_SYMBOL}],
145
+ },
146
+ )
147
+ client.send_request("configurationDone", {})
148
+
149
+ entry = _pump_until(client, deadline, "stopped")
150
+ if entry is None:
151
+ _log.info("DAP: no entry stop")
152
+ return None
153
+ entry_desc = entry.get("description") or entry.get("reason") or ""
154
+ # Launch/configDone responses are deferred until the process is
155
+ # resumed — continue first, then pump everything.
156
+ client.send_request("continue", {"threadId": entry.get("threadId", 1)})
157
+ stop = _pump_until(client, deadline, "stopped")
158
+ if stop is None:
159
+ _log.info("DAP: no panic stop after continue")
160
+ return None
161
+ desc = stop.get("description") or stop.get("text") or stop.get("reason") or "rust panic"
162
+ # codelldb sometimes re-delivers the entry int3 stop instead of
163
+ # the breakpoint hit — treat that as a failed attempt.
164
+ if desc == entry_desc:
165
+ _log.info("DAP: duplicate entry stop, not the panic")
166
+ return None
167
+
168
+ time.sleep(0.5) # let the stop state settle before querying
169
+ tid = stop.get("threadId", 1)
170
+ if tid <= 0:
171
+ tids = _try_threads(client)
172
+ tid = tids[0] if tids else 1
173
+ frames = self._capture_frames_retry(client, tid)
174
+ if not frames:
175
+ _log.info("DAP: stackTrace failed on tid %s", tid)
176
+ return None
177
+ return {"thread_id": tid, "exception": desc, "frames": frames}
178
+ except (ConnectionError, DAPError, OSError, TimeoutError, KeyError) as exc:
179
+ _log.info("codelldb DAP capture attempt failed: %s", exc)
180
+ return None
181
+ finally:
182
+ client.close()
183
+ _kill_after(proc)
184
+
185
+ # ── CLI fallback (deterministic) ────────────────────────────────────
186
+
187
+ def _capture_cli(self, exe: str, budget: float) -> dict | None:
188
+ cli = _find_lldb_cli(exe)
189
+ if not cli:
190
+ _log.info("no bundled lldb CLI next to codelldb — skipping CLI capture")
191
+ return None
192
+ cmd = [
193
+ cli,
194
+ "-b",
195
+ "-o",
196
+ f"break set -n {_PANIC_SYMBOL}",
197
+ "-o",
198
+ "run",
199
+ "-o",
200
+ "bt 30",
201
+ "--",
202
+ str(self._binary),
203
+ *self._args,
204
+ ]
205
+ try:
206
+ proc = subprocess.run(
207
+ cmd,
208
+ capture_output=True,
209
+ text=True,
210
+ errors="replace",
211
+ timeout=budget,
212
+ )
213
+ except (OSError, subprocess.TimeoutExpired) as exc:
214
+ _log.warning("lldb CLI capture failed: %s", exc)
215
+ return None
216
+ out = (proc.stdout or "") + "\n" + (proc.stderr or "")
217
+ frames = _parse_cli_frames(out)
218
+ if not frames:
219
+ # Clean exit or no breakpoint hit — nothing to capture.
220
+ return None
221
+ return {
222
+ "thread_id": 0,
223
+ "exception": _parse_cli_exception(out),
224
+ "frames": frames,
225
+ }
226
+
227
+ # ── internals ────────────────────────────────────────────────────────
228
+
229
+ def _pick_port(self) -> int:
230
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
231
+ s.bind(("127.0.0.1", 0))
232
+ return s.getsockname()[1]
233
+
234
+ def _start_server(self, exe: str, port: int) -> subprocess.Popen:
235
+ cmd = [exe, "--port", str(port)]
236
+ _log.debug("launching codelldb: %s", " ".join(cmd))
237
+ return subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
238
+
239
+ def _capture_frames_retry(self, client: DAPClient, thread_id: int) -> list[dict]:
240
+ for _ in range(3):
241
+ try:
242
+ frames = self._capture_frames(client, thread_id)
243
+ if frames:
244
+ return frames
245
+ except (DAPError, ConnectionError, OSError, TimeoutError, KeyError):
246
+ time.sleep(1.0)
247
+ return []
248
+
249
+ def _capture_frames(self, client: DAPClient, thread_id: int) -> list[dict]:
250
+ try:
251
+ st = client.stack_trace(thread_id)
252
+ except Exception:
253
+ return []
254
+ frames = []
255
+ for sf in st.get("stackFrames", []):
256
+ try:
257
+ locals_vars = self._frame_locals(client, sf["id"])
258
+ except Exception:
259
+ locals_vars = {}
260
+ frames.append(
261
+ {
262
+ "id": sf["id"],
263
+ "name": sf.get("name", ""),
264
+ "path": (sf.get("source") or {}).get("path", ""),
265
+ "line": sf.get("line", 0),
266
+ "locals": locals_vars,
267
+ }
268
+ )
269
+ return frames
270
+
271
+ def _frame_locals(self, client: DAPClient, frame_id: int) -> dict:
272
+ scope_list = client.scopes(frame_id)
273
+ for sc in scope_list.get("scopes", []):
274
+ if sc.get("name") in ("Local", "Locals"):
275
+ return _resolve_variables(client, sc["variablesReference"])
276
+ return {}
277
+
278
+
279
+ # ── helpers ──────────────────────────────────────────────────────────────
280
+
281
+
282
+ def _pump_until(client: DAPClient, deadline: float, event: str) -> dict | None:
283
+ """Read messages until *event* arrives or *deadline* passes."""
284
+ while time.monotonic() < deadline:
285
+ remaining = deadline - time.monotonic()
286
+ if remaining <= 0:
287
+ break
288
+ try:
289
+ client._sock.settimeout(min(max(remaining, 0.1), 3.0))
290
+ msg = client.read_message()
291
+ except TimeoutError:
292
+ continue
293
+ except (ConnectionError, OSError):
294
+ break
295
+ if msg.get("type") == "event" and msg.get("event") == event:
296
+ return msg.get("body", {})
297
+ return None
298
+
299
+
300
+ def _try_threads(client: DAPClient) -> list[int]:
301
+ try:
302
+ body = client.request("threads", {})
303
+ return [t.get("id", 0) for t in body.get("threads", []) if t.get("id", 0) > 0]
304
+ except Exception:
305
+ return []
306
+
307
+
308
+ def _resolve_variables(client: DAPClient, var_ref: int, max_depth: int = 2, depth: int = 0) -> dict:
309
+ if depth >= max_depth or var_ref <= 0:
310
+ return {"__depth_limit__": True}
311
+ try:
312
+ vars_list = client.variables(var_ref)
313
+ except Exception:
314
+ return {"__error__": "variables request failed"}
315
+ result = {}
316
+ for v in vars_list.get("variables", []):
317
+ name = v.get("name", "")
318
+ value = v.get("value", "")
319
+ ref = v.get("variablesReference", 0)
320
+ if ref > 0:
321
+ value = _resolve_variables(client, ref, max_depth, depth + 1)
322
+ result[name] = value
323
+ return result
324
+
325
+
326
+ def _parse_cli_exception(out: str) -> str:
327
+ """Extract the Rust panic message from lldb batch output."""
328
+ lines = out.splitlines()
329
+ for i, line in enumerate(lines):
330
+ if "panicked at " in line:
331
+ msg = line.split("panicked at ", 1)[1].strip()
332
+ # Rust prints the message on the next line when it contains
333
+ # backticks / spaces.
334
+ for nxt in lines[i + 1 :]:
335
+ nxt = nxt.strip()
336
+ if not nxt:
337
+ continue
338
+ if nxt.startswith(("note:", "warning:", "Process ", "thread ", "(lldb)")):
339
+ break
340
+ msg += " " + nxt
341
+ break
342
+ return "panicked at " + msg
343
+ return "rust panic"
344
+
345
+
346
+ def _parse_cli_frames(out: str) -> list[dict]:
347
+ """Parse ``bt`` output into the same frame dict shape as the DAP path."""
348
+ frames = []
349
+ for m in _FRAME_RE.finditer(out):
350
+ name = m.group(2)
351
+ name = re.sub(r"\(.*\)$", "", name) # strip argument lists
352
+ frames.append(
353
+ {
354
+ "id": int(m.group(1)),
355
+ "name": name,
356
+ "path": m.group(3) or "",
357
+ "line": int(m.group(4) or 0),
358
+ "locals": {},
359
+ }
360
+ )
361
+ return frames
362
+
363
+
364
+ def _kill_after(proc: subprocess.Popen, timeout: float = 3) -> None:
365
+ """Kill the codelldb server (and its child tree on Windows)."""
366
+ try:
367
+ proc.wait(timeout=timeout)
368
+ except subprocess.TimeoutExpired:
369
+ if os.name == "nt":
370
+ subprocess.run(
371
+ ["taskkill", "/PID", str(proc.pid), "/T", "/F"],
372
+ capture_output=True,
373
+ )
374
+ else:
375
+ proc.kill()
376
+ try:
377
+ proc.wait(timeout=5)
378
+ except subprocess.TimeoutExpired:
379
+ pass
@@ -0,0 +1,222 @@
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.node")
11
+
12
+ _BUILTIN_VARS = frozenset(
13
+ {
14
+ "arguments",
15
+ "console",
16
+ "process",
17
+ "require",
18
+ "module",
19
+ "__filename",
20
+ "__dirname",
21
+ "global",
22
+ "Buffer",
23
+ "clearTimeout",
24
+ "setTimeout",
25
+ "setInterval",
26
+ "clearInterval",
27
+ "setImmediate",
28
+ "Promise",
29
+ "JSON",
30
+ "Object",
31
+ "String",
32
+ "Number",
33
+ "Boolean",
34
+ "Function",
35
+ "Array",
36
+ "Date",
37
+ "Math",
38
+ "RegExp",
39
+ "Error",
40
+ "SyntaxError",
41
+ "TypeError",
42
+ "ReferenceError",
43
+ "RangeError",
44
+ "URIError",
45
+ "EvalError",
46
+ "Symbol",
47
+ "Map",
48
+ "Set",
49
+ "WeakMap",
50
+ "WeakSet",
51
+ "Proxy",
52
+ "Reflect",
53
+ }
54
+ )
55
+
56
+
57
+ class NodeDebugAdapter:
58
+ def __init__(self, script: str | Path, args: list[str] | None = None):
59
+ self._script = Path(script).resolve()
60
+ self._args = args or []
61
+
62
+ def available(self) -> bool:
63
+ return _find_node() is not None
64
+
65
+ def capture_exception(self, timeout: float = 60) -> dict | None:
66
+ node_path = _find_node()
67
+ if node_path is None:
68
+ _LOG.warning("node not found on PATH")
69
+ return None
70
+
71
+ harness_code = _build_harness(self._script, self._args)
72
+
73
+ with tempfile.NamedTemporaryFile(
74
+ mode="w", suffix=".js", delete=False, encoding="utf-8"
75
+ ) as f:
76
+ f.write(harness_code)
77
+ tmp_path = Path(f.name)
78
+
79
+ try:
80
+ proc = subprocess.run(
81
+ [str(node_path), "--no-warnings", str(tmp_path)],
82
+ capture_output=True,
83
+ text=True,
84
+ timeout=timeout,
85
+ cwd=self._script.parent,
86
+ )
87
+ except subprocess.TimeoutExpired:
88
+ _LOG.warning("node harness timed out after %s seconds", timeout)
89
+ return None
90
+ finally:
91
+ tmp_path.unlink(missing_ok=True)
92
+
93
+ if proc.returncode == 0:
94
+ return None
95
+
96
+ for line in (proc.stderr + "\n" + proc.stdout).splitlines():
97
+ stripped = line.strip()
98
+ prefix = "PATCHI_DEBUG_CAPTURE:"
99
+ if stripped.startswith(prefix):
100
+ try:
101
+ return json.loads(stripped[len(prefix) :])
102
+ except json.JSONDecodeError:
103
+ _LOG.warning("PATCHI_DEBUG_CAPTURE JSON parse error: %.200s", stripped)
104
+ return None
105
+
106
+ _LOG.debug(
107
+ "node exited code=%d but no PATCHI_DEBUG_CAPTURE token in stderr/stdout",
108
+ proc.returncode,
109
+ )
110
+ return None
111
+
112
+ def _resolve_variables(self, var_ref: int, max_depth: int = 2, depth: int = 0) -> dict:
113
+ # Node adapter uses harness-based debugging, not DAP protocol variables
114
+ msg = "Node adapter uses harness-based debugging; variable resolution not available via DAP"
115
+ return {"__not_supported__": msg}
116
+
117
+
118
+ def _find_node() -> Path | None:
119
+ try:
120
+ result = subprocess.run(
121
+ ["where", "node"],
122
+ capture_output=True,
123
+ text=True,
124
+ timeout=10,
125
+ )
126
+ if result.returncode == 0:
127
+ for line in result.stdout.splitlines():
128
+ p = Path(line.strip())
129
+ if p.is_file():
130
+ return p
131
+ except (subprocess.SubprocessError, OSError):
132
+ pass
133
+ return None
134
+
135
+
136
+ def _js_quote(s: str) -> str:
137
+ """JavaScript double-quoted string with proper escaping.
138
+
139
+ Backslash must be escaped FIRST — escaping `"` before `\\` turns the
140
+ generated `\"` into `\\"`, leaving the quote unescaped and allowing
141
+ arbitrary JS injection via a script path or argument containing a quote.
142
+ """
143
+ return '"' + s.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n") + '"'
144
+
145
+
146
+ def _build_harness(script: Path, args: list[str] | None) -> str:
147
+ script_quoted = _js_quote(str(script))
148
+ args_repr = ", ".join(_js_quote(a) for a in (args or []))
149
+
150
+ return textwrap.dedent(f"""\
151
+ 'use strict';
152
+
153
+ const $script = {script_quoted};
154
+ const $args = [{args_repr}];
155
+
156
+ // Route process.argv through the target script so scripts that read
157
+ // process.argv[2..] see their real arguments, not the harness's.
158
+ process.argv = [process.argv[0], $script].concat($args);
159
+
160
+ let $capture = {{
161
+ exception: null,
162
+ thread_id: 1,
163
+ frames: [],
164
+ stack: null
165
+ }};
166
+
167
+ function $parseStack(stack) {{
168
+ if (!stack) return [];
169
+ const lines = stack.split('\\n');
170
+ const frames = [];
171
+ for (const line of lines) {{
172
+ const match = line.match(/^\\s+at\\s+(.+?)\\s+\\((.+?):(\\d+):(\\d+)\\)/);
173
+ if (match) {{
174
+ frames.push({{
175
+ name: match[1],
176
+ path: match[2],
177
+ line: parseInt(match[3]),
178
+ lineEnd: parseInt(match[4]),
179
+ locals: {{}}
180
+ }});
181
+ }} else {{
182
+ const match2 = line.match(/^\\s+at\\s+(.+?):(\\d+):(\\d+)/);
183
+ if (match2) {{
184
+ frames.push({{
185
+ name: '<anonymous>',
186
+ path: match2[1],
187
+ line: parseInt(match2[2]),
188
+ lineEnd: parseInt(match2[3]),
189
+ locals: {{}}
190
+ }});
191
+ }}
192
+ }}
193
+ }}
194
+ return frames;
195
+ }}
196
+
197
+ function $emit(capture) {{
198
+ console.error('PATCHI_DEBUG_CAPTURE:' + JSON.stringify(capture));
199
+ process.exit(1);
200
+ }}
201
+
202
+ process.on('uncaughtException', (exc) => {{
203
+ $capture.exception = exc.message || String(exc);
204
+ $capture.frames = $parseStack(exc.stack);
205
+ $emit($capture);
206
+ }});
207
+
208
+ process.on('unhandledRejection', (reason, promise) => {{
209
+ const exc = reason instanceof Error ? reason : new Error(String(reason));
210
+ $capture.exception = exc.message || String(exc);
211
+ $capture.frames = $parseStack(exc.stack);
212
+ $emit($capture);
213
+ }});
214
+
215
+ try {{
216
+ require($script);
217
+ }} catch (exc) {{
218
+ $capture.exception = exc.message || String(exc);
219
+ $capture.frames = $parseStack(exc.stack);
220
+ $emit($capture);
221
+ }}
222
+ """)