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.
- patchi/__init__.py +1 -0
- patchi/__main__.py +13 -0
- patchi/cli/__init__.py +0 -0
- patchi/cli/command_families.py +235 -0
- patchi/cli/commands/__init__.py +0 -0
- patchi/cli/commands/access_cmd.py +65 -0
- patchi/cli/commands/agent_stats_cmd.py +273 -0
- patchi/cli/commands/agents_cmd.py +187 -0
- patchi/cli/commands/ai_cmd.py +308 -0
- patchi/cli/commands/assure_cmd.py +538 -0
- patchi/cli/commands/audit_cmd.py +286 -0
- patchi/cli/commands/auto_cmd.py +110 -0
- patchi/cli/commands/blame_cmd.py +53 -0
- patchi/cli/commands/chains_cmd.py +327 -0
- patchi/cli/commands/charter_cmd.py +173 -0
- patchi/cli/commands/chat_cmd.py +597 -0
- patchi/cli/commands/check_cmd.py +232 -0
- patchi/cli/commands/cleanup_cmd.py +328 -0
- patchi/cli/commands/cockpit_cmd.py +208 -0
- patchi/cli/commands/commands_cmd.py +160 -0
- patchi/cli/commands/cross_repo_cmd.py +113 -0
- patchi/cli/commands/deps_cmd.py +110 -0
- patchi/cli/commands/dev_check_cmd.py +342 -0
- patchi/cli/commands/dev_cmd.py +445 -0
- patchi/cli/commands/doctor_cmd.py +457 -0
- patchi/cli/commands/findings_cmd.py +187 -0
- patchi/cli/commands/fix_cmd.py +648 -0
- patchi/cli/commands/fix_review_cmd.py +273 -0
- patchi/cli/commands/heatmap_cmd.py +62 -0
- patchi/cli/commands/help_cmd.py +159 -0
- patchi/cli/commands/hosted_cmd.py +980 -0
- patchi/cli/commands/init.py +456 -0
- patchi/cli/commands/key_cmd.py +571 -0
- patchi/cli/commands/learn_cmd.py +140 -0
- patchi/cli/commands/link_cmd.py +104 -0
- patchi/cli/commands/log_cmd.py +57 -0
- patchi/cli/commands/memory_cmd.py +215 -0
- patchi/cli/commands/model_cmd.py +207 -0
- patchi/cli/commands/notify_cmd.py +213 -0
- patchi/cli/commands/notify_prompts.py +29 -0
- patchi/cli/commands/patch_cmd.py +231 -0
- patchi/cli/commands/plan_cmd.py +128 -0
- patchi/cli/commands/plugins_cmd.py +307 -0
- patchi/cli/commands/queue_cmd.py +152 -0
- patchi/cli/commands/quick_cmd.py +159 -0
- patchi/cli/commands/ready_cmd.py +292 -0
- patchi/cli/commands/report_cmd.py +416 -0
- patchi/cli/commands/restrict_cmd.py +127 -0
- patchi/cli/commands/review_cmd.py +225 -0
- patchi/cli/commands/rules_cmd.py +233 -0
- patchi/cli/commands/scan_cmd.py +1908 -0
- patchi/cli/commands/settings_cmd.py +45 -0
- patchi/cli/commands/status_cmd.py +679 -0
- patchi/cli/commands/test_cmd.py +1052 -0
- patchi/cli/commands/trend_cmd.py +296 -0
- patchi/cli/commands/undo_cmd.py +235 -0
- patchi/cli/commands/update_cmd.py +345 -0
- patchi/cli/commands/verify_cmd.py +56 -0
- patchi/cli/commands/vr_cmd.py +440 -0
- patchi/cli/commands/watch_cmd.py +304 -0
- patchi/cli/commands/web_cmd.py +114 -0
- patchi/cli/console.py +101 -0
- patchi/cli/display/__init__.py +1 -0
- patchi/cli/display/live_audit.py +269 -0
- patchi/cli/display/live_progress.py +169 -0
- patchi/cli/framework.py +241 -0
- patchi/cli/logo.py +128 -0
- patchi/cli/main.py +222 -0
- patchi/cli/registry.py +1171 -0
- patchi/cli/style.py +68 -0
- patchi/cli/themes.py +187 -0
- patchi/cli/ux.py +444 -0
- patchi/core/__init__.py +0 -0
- patchi/core/agents/__init__.py +0 -0
- patchi/core/agents/api_fuzzer_agent.py +141 -0
- patchi/core/agents/attack_agent.py +242 -0
- patchi/core/agents/base.py +1012 -0
- patchi/core/agents/bug_predictor_agent.py +58 -0
- patchi/core/agents/build_tool_validator.py +150 -0
- patchi/core/agents/cache.py +192 -0
- patchi/core/agents/chaos_agent.py +133 -0
- patchi/core/agents/cicd_generator.py +240 -0
- patchi/core/agents/comment_scanner.py +280 -0
- patchi/core/agents/console_logging_agent.py +156 -0
- patchi/core/agents/coordinator.py +858 -0
- patchi/core/agents/core_scanner.py +269 -0
- patchi/core/agents/coverage_prioritizer.py +181 -0
- patchi/core/agents/dead_code_hygiene.py +296 -0
- patchi/core/agents/dead_code_scanner.py +692 -0
- patchi/core/agents/dependency_scanner.py +725 -0
- patchi/core/agents/doc_claim_agent.py +573 -0
- patchi/core/agents/duplicate_scanner.py +568 -0
- patchi/core/agents/env_scanner.py +410 -0
- patchi/core/agents/feature_flag_agent.py +89 -0
- patchi/core/agents/frontend_framework_agent.py +133 -0
- patchi/core/agents/goroutine_agent.py +82 -0
- patchi/core/agents/governor.py +1972 -0
- patchi/core/agents/i18n_agent.py +95 -0
- patchi/core/agents/license_compliance.py +247 -0
- patchi/core/agents/linking_agent.py +251 -0
- patchi/core/agents/memory_profiler_agent.py +102 -0
- patchi/core/agents/modernization_agent.py +84 -0
- patchi/core/agents/mutation_agent.py +174 -0
- patchi/core/agents/nl_query_agent.py +74 -0
- patchi/core/agents/promise_rejection_agent.py +100 -0
- patchi/core/agents/refactoring_agent.py +286 -0
- patchi/core/agents/resource_leak_agent.py +91 -0
- patchi/core/agents/route_graph_scanner.py +951 -0
- patchi/core/agents/run_agent.py +106 -0
- patchi/core/agents/rust_unwrap_agent.py +100 -0
- patchi/core/agents/sbom_generator.py +284 -0
- patchi/core/agents/scanners.py +67 -0
- patchi/core/agents/side/__init__.py +1 -0
- patchi/core/agents/side/build_agent.py +65 -0
- patchi/core/agents/side/format_agent.py +50 -0
- patchi/core/agents/side/install_agent.py +63 -0
- patchi/core/agents/side_file_scanner.py +549 -0
- patchi/core/agents/snapshot_drift_detector.py +146 -0
- patchi/core/agents/spa_route_inventory.py +203 -0
- patchi/core/agents/test_scanner.py +442 -0
- patchi/core/agents/tool_health.py +109 -0
- patchi/core/agents/tool_runner.py +162 -0
- patchi/core/agents/type_scanner.py +168 -0
- patchi/core/agents/ui_scanner.py +558 -0
- patchi/core/ai/__init__.py +20 -0
- patchi/core/ai/agent_audit.py +153 -0
- patchi/core/ai/agent_profiler.py +277 -0
- patchi/core/ai/client.py +499 -0
- patchi/core/ai/cost_tracker.py +107 -0
- patchi/core/ai/model_router.py +318 -0
- patchi/core/ai/orchestrator.py +625 -0
- patchi/core/ai/prompts.py +681 -0
- patchi/core/ai/smart.py +302 -0
- patchi/core/ai/tool_executor.py +24 -0
- patchi/core/ai/tools/__init__.py +10 -0
- patchi/core/ai/tools/executor.py +621 -0
- patchi/core/ai/tools/realize.py +1160 -0
- patchi/core/ai/tools/registry.py +1577 -0
- patchi/core/assurance/__init__.py +33 -0
- patchi/core/assurance/builder.py +211 -0
- patchi/core/assurance/graph.py +342 -0
- patchi/core/assurance/invariants.py +168 -0
- patchi/core/atomic.py +61 -0
- patchi/core/attackers/__init__.py +17 -0
- patchi/core/attackers/api_abuse.py +89 -0
- patchi/core/attackers/auth_bypass.py +73 -0
- patchi/core/attackers/base.py +91 -0
- patchi/core/attackers/business_logic.py +88 -0
- patchi/core/attackers/planner.py +108 -0
- patchi/core/attackers/priv_escalation.py +88 -0
- patchi/core/attackers/recon.py +87 -0
- patchi/core/brain/__init__.py +0 -0
- patchi/core/brain/ast_utils/__init__.py +81 -0
- patchi/core/brain/ast_utils/assignments.py +219 -0
- patchi/core/brain/ast_utils/calls.py +139 -0
- patchi/core/brain/ast_utils/config.py +132 -0
- patchi/core/brain/ast_utils/control_flow.py +74 -0
- patchi/core/brain/ast_utils/dead_symbols.py +205 -0
- patchi/core/brain/ast_utils/decorators.py +108 -0
- patchi/core/brain/ast_utils/helpers.py +144 -0
- patchi/core/brain/ast_utils/imports.py +169 -0
- patchi/core/brain/ast_utils/scan.py +143 -0
- patchi/core/brain/ast_utils/taint.py +310 -0
- patchi/core/brain/audit.py +379 -0
- patchi/core/brain/baseline.py +195 -0
- patchi/core/brain/blast_radius.py +312 -0
- patchi/core/brain/body_tags.py +144 -0
- patchi/core/brain/brain.py +1161 -0
- patchi/core/brain/brain_context.py +393 -0
- patchi/core/brain/brain_watcher.py +340 -0
- patchi/core/brain/charter.py +473 -0
- patchi/core/brain/classifier.py +214 -0
- patchi/core/brain/cockpit.py +350 -0
- patchi/core/brain/code_query.py +710 -0
- patchi/core/brain/contract.py +889 -0
- patchi/core/brain/contract_diff.py +155 -0
- patchi/core/brain/council.py +800 -0
- patchi/core/brain/cross_repo.py +104 -0
- patchi/core/brain/dead_code_tools.py +157 -0
- patchi/core/brain/doc_validator.py +197 -0
- patchi/core/brain/domain_activator.py +2183 -0
- patchi/core/brain/enriched_context.py +220 -0
- patchi/core/brain/file_corpus.py +220 -0
- patchi/core/brain/framework.py +953 -0
- patchi/core/brain/freshness.py +273 -0
- patchi/core/brain/git_aware.py +206 -0
- patchi/core/brain/heatmap.py +104 -0
- patchi/core/brain/ignore_parser.py +153 -0
- patchi/core/brain/import_graph.py +486 -0
- patchi/core/brain/languages.py +397 -0
- patchi/core/brain/layered_brain.py +419 -0
- patchi/core/brain/learning.py +229 -0
- patchi/core/brain/orphaned_endpoints.py +313 -0
- patchi/core/brain/personas/__init__.py +648 -0
- patchi/core/brain/personas/bad_user.py +165 -0
- patchi/core/brain/personas/base.py +349 -0
- patchi/core/brain/proactive.py +807 -0
- patchi/core/brain/project_context.py +121 -0
- patchi/core/brain/project_reader.py +521 -0
- patchi/core/brain/reasoning.py +269 -0
- patchi/core/brain/route_detector/__init__.py +15 -0
- patchi/core/brain/route_detector/base.py +31 -0
- patchi/core/brain/route_detector/csharp.py +209 -0
- patchi/core/brain/route_detector/file_based.py +81 -0
- patchi/core/brain/route_detector/go.py +152 -0
- patchi/core/brain/route_detector/java.py +195 -0
- patchi/core/brain/route_detector/javascript.py +197 -0
- patchi/core/brain/route_detector/php.py +158 -0
- patchi/core/brain/route_detector/python.py +193 -0
- patchi/core/brain/route_detector/registry.py +25 -0
- patchi/core/brain/route_detector/ruby.py +134 -0
- patchi/core/brain/route_detector/rust.py +216 -0
- patchi/core/brain/route_detector/swift.py +118 -0
- patchi/core/brain/route_mapper.py +265 -0
- patchi/core/brain/scanner.py +2136 -0
- patchi/core/brain/secrets.py +280 -0
- patchi/core/brain/symbol_graph.py +1955 -0
- patchi/core/brain/team.py +28 -0
- patchi/core/brain/trace_log.py +132 -0
- patchi/core/brain/type_checker/__init__.py +65 -0
- patchi/core/brain/type_checker/base.py +60 -0
- patchi/core/brain/type_checker/csharp.py +59 -0
- patchi/core/brain/type_checker/dart.py +59 -0
- patchi/core/brain/type_checker/go.py +81 -0
- patchi/core/brain/type_checker/java.py +75 -0
- patchi/core/brain/type_checker/kotlin.py +75 -0
- patchi/core/brain/type_checker/php.py +88 -0
- patchi/core/brain/type_checker/python.py +87 -0
- patchi/core/brain/type_checker/rust.py +81 -0
- patchi/core/brain/type_checker/swift.py +59 -0
- patchi/core/brain/type_checker/typescript.py +298 -0
- patchi/core/brain/understander.py +130 -0
- patchi/core/brain/verify.py +379 -0
- patchi/core/campaigns/__init__.py +14 -0
- patchi/core/campaigns/base.py +62 -0
- patchi/core/campaigns/data_flow_abuse.py +153 -0
- patchi/core/campaigns/orchestrator.py +140 -0
- patchi/core/campaigns/state_transitions.py +178 -0
- patchi/core/ci_bundle.py +104 -0
- patchi/core/config.py +311 -0
- patchi/core/constants.py +245 -0
- patchi/core/debug/__init__.py +25 -0
- patchi/core/debug/adapters/__init__.py +11 -0
- patchi/core/debug/adapters/codelldb.py +379 -0
- patchi/core/debug/adapters/node.py +222 -0
- patchi/core/debug/adapters/powershell.py +210 -0
- patchi/core/debug/adapters/python.py +244 -0
- patchi/core/debug/capture.py +210 -0
- patchi/core/debug/dap_client.py +268 -0
- patchi/core/detector/__init__.py +0 -0
- patchi/core/detector/audit.py +194 -0
- patchi/core/detector/bus.py +226 -0
- patchi/core/detector/dispatcher.py +275 -0
- patchi/core/detector/event.py +254 -0
- patchi/core/detector/sigma_engine.py +378 -0
- patchi/core/detector/triage.py +289 -0
- patchi/core/export/sarif.py +113 -0
- patchi/core/fix/__init__.py +17 -0
- patchi/core/fix/applier.py +574 -0
- patchi/core/fix/base.py +510 -0
- patchi/core/fix/code_fixer.py +169 -0
- patchi/core/fix/dead_code_remover.py +131 -0
- patchi/core/fix/fix_agents.py +775 -0
- patchi/core/fix/patch.py +444 -0
- patchi/core/fix/risk_gate.py +383 -0
- patchi/core/fix/security_fixer.py +93 -0
- patchi/core/fix/verify_loop.py +204 -0
- patchi/core/fuzz/__init__.py +8 -0
- patchi/core/fuzz/corpus.py +137 -0
- patchi/core/fuzz/input_fuzzer.py +256 -0
- patchi/core/fuzz/sequence_fuzzer.py +162 -0
- patchi/core/fuzz/state_fuzzer.py +236 -0
- patchi/core/health.py +407 -0
- patchi/core/hosted/__init__.py +1 -0
- patchi/core/hosted/anomaly.py +298 -0
- patchi/core/hosted/audit_log.py +102 -0
- patchi/core/hosted/compliance_report.py +148 -0
- patchi/core/hosted/ip_reputation.py +169 -0
- patchi/core/hosted/log_parsers.py +251 -0
- patchi/core/hosted/tokens.py +127 -0
- patchi/core/hosted/watchlist.py +164 -0
- patchi/core/hosted/webhooks.py +156 -0
- patchi/core/memory.py +437 -0
- patchi/core/notifications/__init__.py +18 -0
- patchi/core/notifications/channels.py +192 -0
- patchi/core/notifications/digest.py +141 -0
- patchi/core/notifications/escalation.py +158 -0
- patchi/core/notifications/notifier.py +182 -0
- patchi/core/notifications/quiet_hours.py +69 -0
- patchi/core/plugins/__init__.py +48 -0
- patchi/core/plugins/analyzer.py +326 -0
- patchi/core/plugins/analyzers/__init__.py +6 -0
- patchi/core/plugins/analyzers/dead_code_analyzer.py +120 -0
- patchi/core/plugins/analyzers/secret_scanner.py +158 -0
- patchi/core/plugins/analyzers/todo_scanner.py +115 -0
- patchi/core/plugins/registry.py +335 -0
- patchi/core/queue.py +375 -0
- patchi/core/reliability/__init__.py +13 -0
- patchi/core/reliability/chaos.py +163 -0
- patchi/core/reliability/fault_injector.py +172 -0
- patchi/core/reliability/idempotency.py +128 -0
- patchi/core/reliability/recovery.py +147 -0
- patchi/core/runtime/__init__.py +5 -0
- patchi/core/runtime/tracer.py +230 -0
- patchi/core/scan_bus.py +156 -0
- patchi/core/security/__init__.py +1 -0
- patchi/core/security/ai_validator.py +462 -0
- patchi/core/security/app_mapper.py +216 -0
- patchi/core/security/app_profile.py +464 -0
- patchi/core/security/attack_feedback.py +141 -0
- patchi/core/security/attack_scenarios/__init__.py +1 -0
- patchi/core/security/attack_tree.py +176 -0
- patchi/core/security/auth_audit_agent.py +344 -0
- patchi/core/security/authz_agent.py +602 -0
- patchi/core/security/auto_fix_proactive.py +620 -0
- patchi/core/security/auto_fixer.py +660 -0
- patchi/core/security/auto_ticket.py +53 -0
- patchi/core/security/bandit_agent.py +152 -0
- patchi/core/security/blast_radius.py +226 -0
- patchi/core/security/browser_tester.py +333 -0
- patchi/core/security/business_logic_agent.py +263 -0
- patchi/core/security/catch_block_auditor.py +395 -0
- patchi/core/security/cdn_cache_agent.py +357 -0
- patchi/core/security/chain_analyzer.py +321 -0
- patchi/core/security/chain_to_assurance.py +168 -0
- patchi/core/security/charter.py +510 -0
- patchi/core/security/cloud_waf_detector.py +295 -0
- patchi/core/security/codeql_agent.py +233 -0
- patchi/core/security/compliance_agent.py +348 -0
- patchi/core/security/confidence_gate.py +423 -0
- patchi/core/security/container_scanner.py +282 -0
- patchi/core/security/crypto_agent.py +378 -0
- patchi/core/security/cve_monitor.py +262 -0
- patchi/core/security/dast_agent.py +865 -0
- patchi/core/security/defectdojo.py +238 -0
- patchi/core/security/defenders/__init__.py +75 -0
- patchi/core/security/defenders/auth_middleware.py +115 -0
- patchi/core/security/defenders/base.py +97 -0
- patchi/core/security/defenders/block_ip.py +71 -0
- patchi/core/security/defenders/block_ws_origin.py +36 -0
- patchi/core/security/defenders/crypto_fix.py +118 -0
- patchi/core/security/defenders/enforce_rate_limit.py +108 -0
- patchi/core/security/defenders/fix_code.py +93 -0
- patchi/core/security/defenders/invalidate_session.py +40 -0
- patchi/core/security/defenders/patch_config.py +70 -0
- patchi/core/security/defenders/rotate_secret.py +54 -0
- patchi/core/security/defenders/suspend_account.py +41 -0
- patchi/core/security/defenders/update_dependency.py +160 -0
- patchi/core/security/defense_layer.py +217 -0
- patchi/core/security/dependency_vulnerability_agent.py +616 -0
- patchi/core/security/detection_pipeline.py +289 -0
- patchi/core/security/dns_security_agent.py +497 -0
- patchi/core/security/domain_activator_v2.py +913 -0
- patchi/core/security/domain_loader.py +1004 -0
- patchi/core/security/domains/__init__.py +0 -0
- patchi/core/security/email_authentication_agent.py +417 -0
- patchi/core/security/env_var_validator.py +304 -0
- patchi/core/security/evidence.py +100 -0
- patchi/core/security/falco_runtime_agent.py +387 -0
- patchi/core/security/fix-playbooks/__init__.py +0 -0
- patchi/core/security/gated_finding.py +85 -0
- patchi/core/security/git_diff_activator.py +367 -0
- patchi/core/security/governance.py +208 -0
- patchi/core/security/gradual_gate.py +34 -0
- patchi/core/security/history.py +315 -0
- patchi/core/security/iac_scanner.py +250 -0
- patchi/core/security/ignore_expiry.py +111 -0
- patchi/core/security/ignore_learner.py +416 -0
- patchi/core/security/injection_agent.py +319 -0
- patchi/core/security/insecure_randomness_agent.py +288 -0
- patchi/core/security/intent_analyzer.py +326 -0
- patchi/core/security/jwt_agent.py +453 -0
- patchi/core/security/kubernetes_agent.py +638 -0
- patchi/core/security/layer2_orchestrator.py +341 -0
- patchi/core/security/llm_security_agent.py +252 -0
- patchi/core/security/misconfig_agent.py +600 -0
- patchi/core/security/mobile_security_agent.py +474 -0
- patchi/core/security/network_agent.py +376 -0
- patchi/core/security/noise_filter.py +274 -0
- patchi/core/security/orchestrator.py +313 -0
- patchi/core/security/pattern_context.py +125 -0
- patchi/core/security/pattern_loader.py +101 -0
- patchi/core/security/pentest/__init__.py +4 -0
- patchi/core/security/pentest/base.py +58 -0
- patchi/core/security/pentest/dalfox_adapter.py +40 -0
- patchi/core/security/pentest/ffuf_adapter.py +48 -0
- patchi/core/security/pentest/nuclei_adapter.py +50 -0
- patchi/core/security/pentest/registry.py +80 -0
- patchi/core/security/pentest/shannon_adapter.py +107 -0
- patchi/core/security/pentest/sqlmap_adapter.py +56 -0
- patchi/core/security/pentest/zap_adapter.py +52 -0
- patchi/core/security/plan_auditor.py +276 -0
- patchi/core/security/policy_engine.py +272 -0
- patchi/core/security/prechecks.py +172 -0
- patchi/core/security/precommit_hook.py +247 -0
- patchi/core/security/privacy_agent.py +361 -0
- patchi/core/security/push_notification_agent.py +417 -0
- patchi/core/security/pysa_agent.py +143 -0
- patchi/core/security/reasoning.py +442 -0
- patchi/core/security/red_team_agent.py +184 -0
- patchi/core/security/red_team_engine.py +1142 -0
- patchi/core/security/remediation.py +360 -0
- patchi/core/security/request_interceptor.py +294 -0
- patchi/core/security/runtime_validator.py +245 -0
- patchi/core/security/saml_sso_agent.py +401 -0
- patchi/core/security/sast_agent.py +213 -0
- patchi/core/security/sast_gate.py +95 -0
- patchi/core/security/scheduler.py +287 -0
- patchi/core/security/secrets_runtime_agent.py +476 -0
- patchi/core/security/security_agents.py +175 -0
- patchi/core/security/security_config.py +551 -0
- patchi/core/security/security_probe.py +762 -0
- patchi/core/security/security_taint.py +1005 -0
- patchi/core/security/sensitive_data_agent.py +271 -0
- patchi/core/security/service_mesh_agent.py +412 -0
- patchi/core/security/session_management_agent.py +245 -0
- patchi/core/security/ssrf_agent.py +422 -0
- patchi/core/security/supply_chain.py +449 -0
- patchi/core/security/threat_model_generator.py +378 -0
- patchi/core/security/threat_model_updater.py +196 -0
- patchi/core/security/tool_adapters.py +135 -0
- patchi/core/security/tool_verify.py +173 -0
- patchi/core/security/websocket_security_agent.py +188 -0
- patchi/core/snapshot.py +242 -0
- patchi/core/tenant.py +386 -0
- patchi/core/testing/__init__.py +17 -0
- patchi/core/testing/_browser.py +147 -0
- patchi/core/testing/ads_agent.py +141 -0
- patchi/core/testing/api_contract_agent.py +450 -0
- patchi/core/testing/app_discovery_agent.py +641 -0
- patchi/core/testing/app_launcher.py +184 -0
- patchi/core/testing/browser_test_agent.py +516 -0
- patchi/core/testing/e2e_flow_agent.py +354 -0
- patchi/core/testing/flake_detector_agent.py +292 -0
- patchi/core/testing/gate.py +31 -0
- patchi/core/testing/live_test_runner.py +304 -0
- patchi/core/testing/live_v2/__init__.py +1 -0
- patchi/core/testing/live_v2/browser_pool.py +349 -0
- patchi/core/testing/live_v2/browser_test_runner.py +313 -0
- patchi/core/testing/live_v2/runner.py +626 -0
- patchi/core/testing/live_v2/screenshot_manager.py +474 -0
- patchi/core/testing/live_v2/stress_orchestrator.py +523 -0
- patchi/core/testing/live_v2/video_recorder.py +303 -0
- patchi/core/testing/regression_agent.py +302 -0
- patchi/core/testing/security_test_agent.py +253 -0
- patchi/core/testing/stress_test_agent.py +203 -0
- patchi/core/testing/test_agents.py +50 -0
- patchi/core/testing/test_config.py +194 -0
- patchi/core/testing/ui_accessibility_agent.py +324 -0
- patchi/core/testing/ui_button_agent.py +294 -0
- patchi/core/testing/ui_layout_agent.py +330 -0
- patchi/core/testing/unit_test_agent.py +1149 -0
- patchi/core/testing/visual_regression_agent.py +260 -0
- patchi/web/__init__.py +1 -0
- patchi/web/api/__init__.py +1 -0
- patchi/web/api/brain_map.py +114 -0
- patchi/web/api/charts.py +47 -0
- patchi/web/api/chat.py +224 -0
- patchi/web/api/cicd.py +576 -0
- patchi/web/api/dast_evidence.py +49 -0
- patchi/web/api/dev_check.py +320 -0
- patchi/web/api/fix.py +134 -0
- patchi/web/api/guard.py +65 -0
- patchi/web/api/hosted.py +128 -0
- patchi/web/api/hosted_v2.py +234 -0
- patchi/web/api/live_testing.py +699 -0
- patchi/web/api/scan.py +582 -0
- patchi/web/api/smart.py +111 -0
- patchi/web/api/tenant.py +117 -0
- patchi/web/api/tools.py +85 -0
- patchi/web/api_legacy.py +2401 -0
- patchi/web/app.py +310 -0
- patchi/web/events.py +483 -0
- patchi/web/routes/__init__.py +1 -0
- patchi/web/routes/assurance.py +541 -0
- patchi/web/routes/brain.py +36 -0
- patchi/web/routes/charter.py +227 -0
- patchi/web/routes/chat.py +23 -0
- patchi/web/routes/dashboard.py +558 -0
- patchi/web/routes/dashboard_v2.py +402 -0
- patchi/web/routes/findings.py +330 -0
- patchi/web/routes/guard.py +35 -0
- patchi/web/routes/history.py +126 -0
- patchi/web/routes/landing.py +52 -0
- patchi/web/routes/live_testing.py +24 -0
- patchi/web/routes/review.py +57 -0
- patchi/web/routes/self_improvement.py +110 -0
- patchi/web/routes/settings.py +49 -0
- patchi/web/routes/tokens.py +29 -0
- patchi/web/spawn.py +139 -0
- patchi/web/ws.py +141 -0
- patchi-0.7.5.dist-info/METADATA +315 -0
- patchi-0.7.5.dist-info/RECORD +496 -0
- patchi-0.7.5.dist-info/WHEEL +5 -0
- patchi-0.7.5.dist-info/licenses/LICENSE +60 -0
- 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
|
+
""")
|