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,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
|