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,383 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Risk Gate for Patchi.
|
|
3
|
+
|
|
4
|
+
Every patch passes through the risk gate before anything touches disk.
|
|
5
|
+
The gate decides: auto-apply | ask user | block
|
|
6
|
+
|
|
7
|
+
Three modes (from config):
|
|
8
|
+
CONFIRM — every patch requires explicit user approval (Y/N)
|
|
9
|
+
AUTO — patches with risk_score ≤ threshold auto-apply; riskier ones ask
|
|
10
|
+
AUTOPILOT — all patches auto-apply; gate only blocks if contract is broken
|
|
11
|
+
|
|
12
|
+
Risk threshold for AUTO mode: config["risk_threshold"] (default 30).
|
|
13
|
+
|
|
14
|
+
Additional hard blocks (fire regardless of mode):
|
|
15
|
+
1. App contract not locked — no fix runs until user confirms the contract
|
|
16
|
+
2. No-touch restriction violated — patch targets a restricted path
|
|
17
|
+
3. Blast radius report required — config["require_blast_radius_on_high_risk"] = True
|
|
18
|
+
and risk_score ≥ 61 — blast radius must be computed and shown before applying
|
|
19
|
+
|
|
20
|
+
Gate decisions:
|
|
21
|
+
ALLOW_AUTO — apply without asking (low risk + AUTO/AUTOPILOT mode)
|
|
22
|
+
REQUIRE_REVIEW — surface to user for approval (confirm mode OR high risk)
|
|
23
|
+
BLOCK — refuse entirely (contract not locked, restricted path)
|
|
24
|
+
|
|
25
|
+
The gate NEVER raises exceptions. It always returns a GateDecision.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import datetime
|
|
31
|
+
import logging
|
|
32
|
+
from dataclasses import dataclass, field
|
|
33
|
+
from enum import StrEnum
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
|
|
36
|
+
from patchi.core import config as cfg
|
|
37
|
+
from patchi.core import memory as mem
|
|
38
|
+
from patchi.core.constants import Mode, RiskLevel
|
|
39
|
+
from patchi.core.fix.patch import Patch
|
|
40
|
+
|
|
41
|
+
_log = logging.getLogger("patchi.fix.risk_gate")
|
|
42
|
+
|
|
43
|
+
# ── Gate decision ──────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class GateDecision(StrEnum):
|
|
47
|
+
ALLOW_AUTO = "allow_auto" # apply immediately
|
|
48
|
+
REQUIRE_REVIEW = "require_review" # surface to user
|
|
49
|
+
BLOCK = "block" # hard stop
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class GateResult:
|
|
54
|
+
decision: GateDecision
|
|
55
|
+
reason: str
|
|
56
|
+
patch_id: str
|
|
57
|
+
risk_score: int
|
|
58
|
+
risk_level: str
|
|
59
|
+
confidence: int
|
|
60
|
+
blast_radius: int
|
|
61
|
+
blocks: list[str] = field(default_factory=list)
|
|
62
|
+
warnings: list[str] = field(default_factory=list)
|
|
63
|
+
requires_blast_report: bool = False
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def is_blocked(self) -> bool:
|
|
67
|
+
return self.decision == GateDecision.BLOCK
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def is_auto(self) -> bool:
|
|
71
|
+
return self.decision == GateDecision.ALLOW_AUTO
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def needs_review(self) -> bool:
|
|
75
|
+
return self.decision == GateDecision.REQUIRE_REVIEW
|
|
76
|
+
|
|
77
|
+
def to_dict(self) -> dict:
|
|
78
|
+
return {
|
|
79
|
+
"decision": self.decision.value,
|
|
80
|
+
"reason": self.reason,
|
|
81
|
+
"patch_id": self.patch_id,
|
|
82
|
+
"risk_score": self.risk_score,
|
|
83
|
+
"risk_level": self.risk_level,
|
|
84
|
+
"confidence": self.confidence,
|
|
85
|
+
"blast_radius": self.blast_radius,
|
|
86
|
+
"blocks": self.blocks,
|
|
87
|
+
"warnings": self.warnings,
|
|
88
|
+
"requires_blast_report": self.requires_blast_report,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# ── Risk gate ──────────────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class RiskGate:
|
|
96
|
+
"""
|
|
97
|
+
Evaluates every patch before it is applied.
|
|
98
|
+
|
|
99
|
+
Usage:
|
|
100
|
+
gate = RiskGate(project_root)
|
|
101
|
+
result = gate.evaluate(patch)
|
|
102
|
+
if result.is_blocked:
|
|
103
|
+
# tell user why
|
|
104
|
+
elif result.is_auto:
|
|
105
|
+
# apply now
|
|
106
|
+
else:
|
|
107
|
+
# surface for review
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
def __init__(self, root: Path):
|
|
111
|
+
self.root = root
|
|
112
|
+
try:
|
|
113
|
+
self._config = cfg.load(root)
|
|
114
|
+
except Exception as e:
|
|
115
|
+
_log.warning(
|
|
116
|
+
"Failed to load config for %s — falling back to defaults (restricted-path "
|
|
117
|
+
"protections may be affected): %s",
|
|
118
|
+
root,
|
|
119
|
+
e,
|
|
120
|
+
)
|
|
121
|
+
self._config = {}
|
|
122
|
+
try:
|
|
123
|
+
self._brain = mem.get_brain(root)
|
|
124
|
+
except Exception as e:
|
|
125
|
+
_log.warning("Failed to load brain data for %s — falling back to empty: %s", root, e)
|
|
126
|
+
self._brain = {}
|
|
127
|
+
|
|
128
|
+
def evaluate(self, patch: Patch) -> GateResult:
|
|
129
|
+
"""Run all gate checks and return a decision."""
|
|
130
|
+
blocks: list[str] = []
|
|
131
|
+
warnings: list[str] = []
|
|
132
|
+
|
|
133
|
+
mode = self._get_mode()
|
|
134
|
+
risk_threshold = self._config.get("risk_threshold", 30)
|
|
135
|
+
risk_level = RiskLevel.from_score(patch.risk_score).value
|
|
136
|
+
requires_blast = False
|
|
137
|
+
|
|
138
|
+
# ── Hard blocks ────────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
# 1. Contract must be locked before any fix runs
|
|
141
|
+
if not self._brain.get("contract_locked"):
|
|
142
|
+
blocks.append(
|
|
143
|
+
"App contract not confirmed. Run 'p scan' and confirm your critical flows first. "
|
|
144
|
+
"No fixes will apply until the contract is locked."
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# 2. Restricted path check
|
|
148
|
+
restricted = self._restricted_paths()
|
|
149
|
+
for change in patch.changes:
|
|
150
|
+
for path, rtype in restricted:
|
|
151
|
+
if change.path.startswith(path.rstrip("/")):
|
|
152
|
+
if rtype == "sensitive":
|
|
153
|
+
warnings.append(
|
|
154
|
+
f"'{change.path}' is in a sensitive zone ({path}). "
|
|
155
|
+
"Proceed with caution."
|
|
156
|
+
)
|
|
157
|
+
else:
|
|
158
|
+
blocks.append(
|
|
159
|
+
f"'{change.path}' is in a restricted {rtype} zone ({path}). "
|
|
160
|
+
"Modify restrictions with 'p restrict'."
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# 3. Charter boundary check — agent fixes must not violate guard rails
|
|
164
|
+
try:
|
|
165
|
+
from patchi.core.security.charter import (
|
|
166
|
+
RuleType,
|
|
167
|
+
check_boundary_violations,
|
|
168
|
+
load_charter,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
charter = load_charter(self.root)
|
|
172
|
+
if charter.rules:
|
|
173
|
+
# Build import edges from proposed changes
|
|
174
|
+
import_edges: list[tuple[str, str]] = []
|
|
175
|
+
for change in patch.changes:
|
|
176
|
+
if change.proposed:
|
|
177
|
+
# Extract import statements from proposed code
|
|
178
|
+
import re as _re
|
|
179
|
+
for m in _re.finditer(
|
|
180
|
+
r'from\s+(\S+)\s+import\s+\w+', change.proposed
|
|
181
|
+
):
|
|
182
|
+
import_edges.append((change.path, m.group(1)))
|
|
183
|
+
for m in _re.finditer(
|
|
184
|
+
r'import\s+(\S+)', change.proposed
|
|
185
|
+
):
|
|
186
|
+
import_edges.append((change.path, m.group(1)))
|
|
187
|
+
|
|
188
|
+
# Check boundary rules
|
|
189
|
+
boundary_violations = check_boundary_violations(charter, import_edges)
|
|
190
|
+
for v in boundary_violations:
|
|
191
|
+
blocks.append(
|
|
192
|
+
f"Charter violation [{v.rule_id}]: {v.message}. "
|
|
193
|
+
f"{v.suggestion}"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# Check convention rules (file size limits)
|
|
197
|
+
max_lines = 0
|
|
198
|
+
for rule in charter.rules:
|
|
199
|
+
if rule.type == RuleType.CONVENTION and rule.enabled:
|
|
200
|
+
if rule.max_value > 0 and rule.metric == "lines":
|
|
201
|
+
max_lines = rule.max_value
|
|
202
|
+
if max_lines > 0:
|
|
203
|
+
for change in patch.changes:
|
|
204
|
+
if change.path.endswith((".py", ".ts", ".js")):
|
|
205
|
+
new_lines = (
|
|
206
|
+
(change.proposed or "").count("\n") + 1
|
|
207
|
+
)
|
|
208
|
+
if new_lines > max_lines:
|
|
209
|
+
blocks.append(
|
|
210
|
+
f"Charter violation [{rule.id}]: "
|
|
211
|
+
f"{change.path} would be {new_lines} lines "
|
|
212
|
+
f"(max {max_lines}). {rule.description}"
|
|
213
|
+
)
|
|
214
|
+
except Exception as e:
|
|
215
|
+
_log.debug("Charter check skipped: %s", e)
|
|
216
|
+
|
|
217
|
+
# 4. Blast radius report required for high-risk patches
|
|
218
|
+
if (
|
|
219
|
+
self._config.get("require_blast_radius_on_high_risk", True)
|
|
220
|
+
and patch.risk_score >= 61
|
|
221
|
+
and patch.blast_radius == 0
|
|
222
|
+
):
|
|
223
|
+
blocks.append(
|
|
224
|
+
f"High-risk patch (score {patch.risk_score}) requires a blast radius report. "
|
|
225
|
+
"Patchi is computing it — this patch will be re-evaluated once ready."
|
|
226
|
+
)
|
|
227
|
+
requires_blast = True
|
|
228
|
+
|
|
229
|
+
# ── Warnings ───────────────────────────────────────────────────────────
|
|
230
|
+
|
|
231
|
+
if patch.confidence < 70:
|
|
232
|
+
warnings.append(
|
|
233
|
+
f"Low confidence ({patch.confidence}%). "
|
|
234
|
+
"Patchi is less certain than usual about this fix."
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
if patch.blast_radius > 10:
|
|
238
|
+
warnings.append(
|
|
239
|
+
f"High blast radius: {patch.blast_radius} files depend on the changed file(s). "
|
|
240
|
+
"Thorough testing is recommended."
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
if patch.total_lines_changed > 100:
|
|
244
|
+
warnings.append(
|
|
245
|
+
f"Large patch: {patch.total_lines_changed} lines changed across "
|
|
246
|
+
f"{patch.file_count} file(s)."
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
# Secrets gate: check if proposed code introduces new secrets
|
|
250
|
+
try:
|
|
251
|
+
from patchi.core.security.secrets_guard import gate_check_proposed_code
|
|
252
|
+
|
|
253
|
+
for change in patch.changes:
|
|
254
|
+
if change.proposed:
|
|
255
|
+
safe, secret_findings = gate_check_proposed_code(change.proposed, change.path)
|
|
256
|
+
if not safe:
|
|
257
|
+
blocks.append(
|
|
258
|
+
f"Secrets guard: proposed change to '{change.path}' introduces "
|
|
259
|
+
f"{len(secret_findings)} secret(s). Fix blocked."
|
|
260
|
+
)
|
|
261
|
+
except ImportError:
|
|
262
|
+
pass
|
|
263
|
+
|
|
264
|
+
# ── Determine decision ─────────────────────────────────────────────────
|
|
265
|
+
|
|
266
|
+
if blocks:
|
|
267
|
+
return GateResult(
|
|
268
|
+
decision=GateDecision.BLOCK,
|
|
269
|
+
reason=blocks[0],
|
|
270
|
+
patch_id=patch.id,
|
|
271
|
+
risk_score=patch.risk_score,
|
|
272
|
+
risk_level=risk_level,
|
|
273
|
+
confidence=patch.confidence,
|
|
274
|
+
blast_radius=patch.blast_radius,
|
|
275
|
+
blocks=blocks,
|
|
276
|
+
warnings=warnings,
|
|
277
|
+
requires_blast_report=requires_blast,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
# Test-weakening guard: a patch that only edits test files (set by the
|
|
281
|
+
# verify loop / fix command) must NEVER auto-apply — it goes to the
|
|
282
|
+
# human review queue regardless of mode, even AUTOPILOT.
|
|
283
|
+
if patch.requires_review:
|
|
284
|
+
return GateResult(
|
|
285
|
+
decision=GateDecision.REQUIRE_REVIEW,
|
|
286
|
+
reason=(
|
|
287
|
+
"Patch only changes test file(s) — flagged for human review "
|
|
288
|
+
"(test-weakening edits are never auto-applied)."
|
|
289
|
+
),
|
|
290
|
+
patch_id=patch.id,
|
|
291
|
+
risk_score=patch.risk_score,
|
|
292
|
+
risk_level=risk_level,
|
|
293
|
+
confidence=patch.confidence,
|
|
294
|
+
blast_radius=patch.blast_radius,
|
|
295
|
+
blocks=[],
|
|
296
|
+
warnings=warnings + ["Test-weakening patch: only test files changed."],
|
|
297
|
+
requires_blast_report=requires_blast,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
# Mode-based routing
|
|
301
|
+
if mode == Mode.CONFIRM:
|
|
302
|
+
# Always ask in CONFIRM mode
|
|
303
|
+
decision = GateDecision.REQUIRE_REVIEW
|
|
304
|
+
reason = "CONFIRM mode — every fix requires approval."
|
|
305
|
+
|
|
306
|
+
elif mode == Mode.AUTO:
|
|
307
|
+
if patch.risk_score <= risk_threshold:
|
|
308
|
+
decision = GateDecision.ALLOW_AUTO
|
|
309
|
+
reason = f"AUTO mode — risk score {patch.risk_score} ≤ threshold {risk_threshold}."
|
|
310
|
+
else:
|
|
311
|
+
decision = GateDecision.REQUIRE_REVIEW
|
|
312
|
+
reason = (
|
|
313
|
+
f"AUTO mode — risk score {patch.risk_score} exceeds threshold {risk_threshold}. "
|
|
314
|
+
"Surfaced for review."
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
else: # AUTOPILOT
|
|
318
|
+
decision = GateDecision.ALLOW_AUTO
|
|
319
|
+
reason = "AUTOPILOT mode — all fixes apply automatically."
|
|
320
|
+
if patch.risk_score >= 61:
|
|
321
|
+
warnings.append(
|
|
322
|
+
f"High-risk fix applied automatically (AUTOPILOT). "
|
|
323
|
+
f"Risk score: {patch.risk_score}. Switch to AUTO mode for manual review."
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
# ── Quiet hours check ─────────────────────────────────────────────────
|
|
327
|
+
# In quiet hours, auto-apply is blocked even in AUTO/AUTOPILOT modes
|
|
328
|
+
if decision == GateDecision.ALLOW_AUTO and self._is_quiet_hours():
|
|
329
|
+
decision = GateDecision.REQUIRE_REVIEW
|
|
330
|
+
reason = f"Quiet hours active — auto-apply deferred to {self._config.get('quiet_hours_end', 'end')}."
|
|
331
|
+
warnings.append("Quiet hours: auto-apply blocked. Patch queued for review.")
|
|
332
|
+
|
|
333
|
+
return GateResult(
|
|
334
|
+
decision=decision,
|
|
335
|
+
reason=reason,
|
|
336
|
+
patch_id=patch.id,
|
|
337
|
+
risk_score=patch.risk_score,
|
|
338
|
+
risk_level=risk_level,
|
|
339
|
+
confidence=patch.confidence,
|
|
340
|
+
blast_radius=patch.blast_radius,
|
|
341
|
+
blocks=blocks,
|
|
342
|
+
warnings=warnings,
|
|
343
|
+
requires_blast_report=requires_blast,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
def _is_quiet_hours(self) -> bool:
|
|
347
|
+
"""Check if current local time falls within configured quiet hours."""
|
|
348
|
+
quiet_hours = self._config.get("quiet_hours", {})
|
|
349
|
+
if not quiet_hours.get("enabled", False):
|
|
350
|
+
return False
|
|
351
|
+
start = quiet_hours.get("start", "")
|
|
352
|
+
end = quiet_hours.get("end", "")
|
|
353
|
+
if not start or not end:
|
|
354
|
+
return False
|
|
355
|
+
try:
|
|
356
|
+
now = datetime.datetime.now()
|
|
357
|
+
now_minutes = now.hour * 60 + now.minute
|
|
358
|
+
start_parts = start.split(":")
|
|
359
|
+
end_parts = end.split(":")
|
|
360
|
+
start_minutes = int(start_parts[0]) * 60 + int(start_parts[1])
|
|
361
|
+
end_minutes = int(end_parts[0]) * 60 + int(end_parts[1])
|
|
362
|
+
if start_minutes <= end_minutes:
|
|
363
|
+
return start_minutes <= now_minutes < end_minutes
|
|
364
|
+
return now_minutes >= start_minutes or now_minutes < end_minutes
|
|
365
|
+
except (ValueError, IndexError, AttributeError):
|
|
366
|
+
return False
|
|
367
|
+
|
|
368
|
+
def _get_mode(self) -> Mode:
|
|
369
|
+
try:
|
|
370
|
+
return cfg.get_mode(self.root)
|
|
371
|
+
except Exception as e:
|
|
372
|
+
_log.warning(
|
|
373
|
+
"Failed to read mode from config — defaulting to CONFIRM (safest option): %s", e
|
|
374
|
+
)
|
|
375
|
+
return Mode.CONFIRM
|
|
376
|
+
|
|
377
|
+
def _restricted_paths(self) -> list[tuple[str, str]]:
|
|
378
|
+
restrictions = self._config.get("restrictions", [])
|
|
379
|
+
return [
|
|
380
|
+
(r["path"], r.get("type", "no_touch"))
|
|
381
|
+
for r in restrictions
|
|
382
|
+
if r.get("enabled", True) and r.get("type") in ("no_touch", "scan_only", "sensitive")
|
|
383
|
+
]
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SecurityFixer - Patches confirmed vulnerabilities from security scanner agents.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from patchi.core.agents.base import (
|
|
8
|
+
AgentGroup,
|
|
9
|
+
AgentInput,
|
|
10
|
+
AgentResult,
|
|
11
|
+
BaseAgent,
|
|
12
|
+
register,
|
|
13
|
+
)
|
|
14
|
+
from patchi.core.fix.patch import FileChange, Patch, PatchType
|
|
15
|
+
|
|
16
|
+
# Import common functionality from base module
|
|
17
|
+
from .base import _make_patch, _read_file, compute_blast_radius
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@register
|
|
21
|
+
class SecurityFixer(BaseAgent):
|
|
22
|
+
"""Patches confirmed vulnerabilities from security scanner agents."""
|
|
23
|
+
|
|
24
|
+
name = "SecurityFixer"
|
|
25
|
+
group = AgentGroup.FIX
|
|
26
|
+
timeout = 120
|
|
27
|
+
|
|
28
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
29
|
+
from patchi.core.ai.prompts import Skill
|
|
30
|
+
from patchi.core.fix.fix_agents import _ai_fix
|
|
31
|
+
|
|
32
|
+
findings = [
|
|
33
|
+
f
|
|
34
|
+
for f in inp.extra.get("findings", [])
|
|
35
|
+
if (f.get("fix_agent") == self.name or f.get("severity") in ("critical", "high"))
|
|
36
|
+
and f.get("type") not in ("hardcoded_secret",)
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
patches: list[Patch] = []
|
|
40
|
+
|
|
41
|
+
for finding in findings[:5]:
|
|
42
|
+
fpath = finding.get("file", "")
|
|
43
|
+
path = inp.root / fpath
|
|
44
|
+
original = _read_file(path)
|
|
45
|
+
if not original:
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
framework = finding.get("framework", "")
|
|
49
|
+
blast_radius = 0
|
|
50
|
+
if hasattr(inp, "extra") and "brain_report" in inp.extra:
|
|
51
|
+
brain_report = inp.extra["brain_report"]
|
|
52
|
+
if (
|
|
53
|
+
hasattr(brain_report, "blast_radius_map")
|
|
54
|
+
and fpath in brain_report.blast_radius_map
|
|
55
|
+
):
|
|
56
|
+
blast_radius = len(brain_report.blast_radius_map[fpath].all_dependents)
|
|
57
|
+
else:
|
|
58
|
+
blast_radius = compute_blast_radius(fpath, inp.root)
|
|
59
|
+
|
|
60
|
+
proposed = _ai_fix(
|
|
61
|
+
inp.config,
|
|
62
|
+
Skill.SECURITY_FIX,
|
|
63
|
+
fpath,
|
|
64
|
+
original,
|
|
65
|
+
finding,
|
|
66
|
+
blast_radius,
|
|
67
|
+
root=inp.root,
|
|
68
|
+
framework=framework,
|
|
69
|
+
)
|
|
70
|
+
if not proposed:
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
change = FileChange(path=fpath, original=original, proposed=proposed)
|
|
74
|
+
if not change.diff.strip():
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
cwe = finding.get("cwe", "")
|
|
78
|
+
issue_desc = finding.get("message", "Security vulnerability")
|
|
79
|
+
patch = _make_patch(
|
|
80
|
+
agent_name=self.name,
|
|
81
|
+
patch_type=PatchType.SECURITY,
|
|
82
|
+
changes=[change],
|
|
83
|
+
description=f"Security fix: {issue_desc[:60]}",
|
|
84
|
+
ai_explanation=f"CWE-{cwe}: {issue_desc}",
|
|
85
|
+
finding_id=finding.get("type", ""),
|
|
86
|
+
blast_radius=blast_radius,
|
|
87
|
+
)
|
|
88
|
+
patches.append(patch)
|
|
89
|
+
result.data.setdefault("patches", []).append(patch.to_dict())
|
|
90
|
+
|
|
91
|
+
result.files_scanned = len(findings)
|
|
92
|
+
result.data["patch_count"] = len(patches)
|
|
93
|
+
result.ai_calls_made = len(patches)
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Verify Loop — apply → re-run the failing test → retry → never weaken tests.
|
|
3
|
+
|
|
4
|
+
Reconstructed module (the original was lost before any commit existed;
|
|
5
|
+
this build follows the exact call contract in cli/commands/fix_cmd.py):
|
|
6
|
+
|
|
7
|
+
outcome = run_verify_loop(patch, root=r, config=config, brain=brain,
|
|
8
|
+
applier=applier, log=callable)
|
|
9
|
+
outcome.review_required # patch only touches tests -> human must review
|
|
10
|
+
outcome.applied # final state applied
|
|
11
|
+
outcome.verified # the originally-failing test now passes
|
|
12
|
+
outcome.retries_used # number of RETRIES after the first attempt
|
|
13
|
+
outcome.reason # why it failed / could not be verified
|
|
14
|
+
|
|
15
|
+
Honesty rules carried over from the fix_cmd contract:
|
|
16
|
+
- A patch whose changes are exclusively in test files is NEVER applied
|
|
17
|
+
automatically (test-weakening guard) — flagged for human review.
|
|
18
|
+
- Verification means the specific failing test passes again, not just
|
|
19
|
+
"the applier said ok".
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import fnmatch
|
|
25
|
+
import logging
|
|
26
|
+
import subprocess
|
|
27
|
+
import sys
|
|
28
|
+
from dataclasses import dataclass
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
_log = logging.getLogger("patchi.fix.verify_loop")
|
|
32
|
+
|
|
33
|
+
MAX_RETRIES = 2 # attempts after the first one ("up to 2 retries")
|
|
34
|
+
_VERIFY_TIMEOUT_S = 120 # per-attempt cap for the targeted test re-run
|
|
35
|
+
|
|
36
|
+
_TEST_FILE_PATTERNS = (
|
|
37
|
+
"test_*.py",
|
|
38
|
+
"*_test.py",
|
|
39
|
+
"conftest.py",
|
|
40
|
+
"*.spec.ts",
|
|
41
|
+
"*.spec.tsx",
|
|
42
|
+
"*.spec.js",
|
|
43
|
+
"*.spec.jsx",
|
|
44
|
+
"*.test.ts",
|
|
45
|
+
"*.test.tsx",
|
|
46
|
+
"*.test.js",
|
|
47
|
+
"*.test.jsx",
|
|
48
|
+
"*_test.go",
|
|
49
|
+
"*_test.rb",
|
|
50
|
+
"*Test.java",
|
|
51
|
+
"*Test.kt",
|
|
52
|
+
"*Tests.swift",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
_TEST_DIR_MARKERS = ("test", "tests", "spec", "specs", "__tests__", "testing")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def is_test_path(path: str) -> bool:
|
|
59
|
+
"""True if a relative path points into test code."""
|
|
60
|
+
import posixpath
|
|
61
|
+
|
|
62
|
+
p = path.replace("\\", "/")
|
|
63
|
+
name = posixpath.basename(p)
|
|
64
|
+
if any(fnmatch.fnmatch(name, pat) for pat in _TEST_FILE_PATTERNS):
|
|
65
|
+
return True
|
|
66
|
+
dirs = {seg.lower() for seg in posixpath.dirname(p).split("/") if seg}
|
|
67
|
+
return bool(dirs & set(_TEST_DIR_MARKERS))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def should_flag_for_review(patch) -> bool:
|
|
71
|
+
"""Test-weakening guard: only-test-file patches always need a human."""
|
|
72
|
+
changes = getattr(patch, "changes", None) or []
|
|
73
|
+
if not changes:
|
|
74
|
+
return False
|
|
75
|
+
return all(is_test_path(getattr(c, "path", "")) for c in changes)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class VerifyOutcome:
|
|
80
|
+
"""Result of one fix → verify → retry campaign."""
|
|
81
|
+
|
|
82
|
+
applied: bool = False
|
|
83
|
+
verified: bool = False
|
|
84
|
+
review_required: bool = False
|
|
85
|
+
patch: object | None = None
|
|
86
|
+
reason: str = ""
|
|
87
|
+
retries_used: int = 0
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _rerun_failing_test(
|
|
91
|
+
root: Path,
|
|
92
|
+
test_file: str,
|
|
93
|
+
log=None,
|
|
94
|
+
) -> tuple[bool, str]:
|
|
95
|
+
"""Independently re-run one test file. Returns (passed, tail_of_output)."""
|
|
96
|
+
tf = Path(test_file)
|
|
97
|
+
target = tf if tf.is_absolute() else root / tf
|
|
98
|
+
if not target.is_file():
|
|
99
|
+
return False, f"cannot re-run: {test_file} not found"
|
|
100
|
+
try:
|
|
101
|
+
proc = subprocess.run( # noqa: S603 — fixed argv, no shell
|
|
102
|
+
[sys.executable, "-m", "pytest", "-x", "-q", str(target)],
|
|
103
|
+
capture_output=True,
|
|
104
|
+
text=True,
|
|
105
|
+
timeout=_VERIFY_TIMEOUT_S,
|
|
106
|
+
cwd=str(root),
|
|
107
|
+
)
|
|
108
|
+
tail = (proc.stdout or "").strip().splitlines()[-6:]
|
|
109
|
+
output = " | ".join(tail)[:300]
|
|
110
|
+
passed = proc.returncode == 0
|
|
111
|
+
if log:
|
|
112
|
+
log(f"re-ran {test_file}: {'PASS' if passed else 'FAIL'}")
|
|
113
|
+
return passed, output
|
|
114
|
+
except subprocess.TimeoutExpired:
|
|
115
|
+
return False, f"test re-run timed out after {_VERIFY_TIMEOUT_S}s"
|
|
116
|
+
except OSError as exc:
|
|
117
|
+
return False, f"could not launch test runner: {exc}"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def recheck_test_file(root: Path, test_file: str) -> dict:
|
|
121
|
+
"""Re-run one previously-verified test file (REVERIFY phase helper).
|
|
122
|
+
|
|
123
|
+
Contract used by governor._recheck_applied_patches:
|
|
124
|
+
{"passed": True} — test ran and passed
|
|
125
|
+
{"passed": False} — test ran and failed (regression)
|
|
126
|
+
{"passed": None} — could not run (file missing, runner error, timeout)
|
|
127
|
+
Always includes "output" (tail of runner output) and "error" (message).
|
|
128
|
+
"""
|
|
129
|
+
if not test_file or not is_test_path(test_file):
|
|
130
|
+
return {"passed": None, "output": "", "error": f"not a runnable test path: {test_file}"}
|
|
131
|
+
|
|
132
|
+
passed, output = _rerun_failing_test(Path(root), test_file)
|
|
133
|
+
if "cannot re-run" in output or "could not launch" in output or "timed out" in output:
|
|
134
|
+
return {"passed": None, "output": output, "error": output}
|
|
135
|
+
return {"passed": bool(passed), "output": output, "error": "" if passed else output}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def run_verify_loop(
|
|
139
|
+
patch,
|
|
140
|
+
root: Path,
|
|
141
|
+
config: dict | None = None,
|
|
142
|
+
brain=None,
|
|
143
|
+
applier=None,
|
|
144
|
+
log=None,
|
|
145
|
+
) -> VerifyOutcome:
|
|
146
|
+
"""Apply *patch*, re-verify the failing test, retry up to MAX_RETRIES.
|
|
147
|
+
|
|
148
|
+
Never raises: every failure mode becomes an honest VerifyOutcome.
|
|
149
|
+
"""
|
|
150
|
+
say = log or (lambda _msg: None)
|
|
151
|
+
|
|
152
|
+
# Guard first: test-only patches go to a human, full stop.
|
|
153
|
+
if should_flag_for_review(patch):
|
|
154
|
+
return VerifyOutcome(review_required=True, patch=patch)
|
|
155
|
+
|
|
156
|
+
if applier is None:
|
|
157
|
+
from patchi.core.fix.applier import PatchApplier
|
|
158
|
+
|
|
159
|
+
applier = PatchApplier(root)
|
|
160
|
+
|
|
161
|
+
source_finding = getattr(patch, "source_finding", {}) or {}
|
|
162
|
+
test_file = source_finding.get("file", "")
|
|
163
|
+
|
|
164
|
+
attempts = 1 + MAX_RETRIES
|
|
165
|
+
last_reason = ""
|
|
166
|
+
|
|
167
|
+
for attempt in range(1, attempts + 1):
|
|
168
|
+
result = applier.apply(patch) # never raises; rolls back on its own
|
|
169
|
+
|
|
170
|
+
if not getattr(result, "success", False):
|
|
171
|
+
last_reason = getattr(result, "error", "") or "apply failed"
|
|
172
|
+
say(f"attempt {attempt}/{attempts} failed to apply: {last_reason}")
|
|
173
|
+
patch.verify_retries = attempt - 1
|
|
174
|
+
continue
|
|
175
|
+
|
|
176
|
+
# Applied — now prove it: the original failing test must pass.
|
|
177
|
+
if test_file and is_test_path(test_file):
|
|
178
|
+
passed, detail = _rerun_failing_test(Path(root), test_file, log=say)
|
|
179
|
+
verified = passed
|
|
180
|
+
reason = "" if passed else f"verification failed: {detail}"
|
|
181
|
+
else:
|
|
182
|
+
verified = bool(getattr(result, "test_passed", False))
|
|
183
|
+
reason = "" if verified else "applied but applier-level tests did not pass"
|
|
184
|
+
|
|
185
|
+
if verified:
|
|
186
|
+
patch.verify_retries = attempt - 1
|
|
187
|
+
return VerifyOutcome(
|
|
188
|
+
applied=True,
|
|
189
|
+
verified=True,
|
|
190
|
+
patch=patch,
|
|
191
|
+
retries_used=attempt - 1,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
last_reason = reason
|
|
195
|
+
say(f"attempt {attempt}/{attempts} applied but unverified: {reason}")
|
|
196
|
+
patch.verify_retries = attempt - 1
|
|
197
|
+
|
|
198
|
+
return VerifyOutcome(
|
|
199
|
+
applied=False,
|
|
200
|
+
verified=False,
|
|
201
|
+
patch=patch,
|
|
202
|
+
reason=last_reason or "exhausted retries",
|
|
203
|
+
retries_used=min(MAX_RETRIES, attempts - 1),
|
|
204
|
+
)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Fuzzing module — input mutation, sequence reordering, state-space exploration."""
|
|
2
|
+
|
|
3
|
+
from .corpus import FuzzCorpus
|
|
4
|
+
from .input_fuzzer import InputFuzzer
|
|
5
|
+
from .sequence_fuzzer import SequenceFuzzer
|
|
6
|
+
from .state_fuzzer import StateFuzzer
|
|
7
|
+
|
|
8
|
+
__all__ = ["InputFuzzer", "SequenceFuzzer", "StateFuzzer", "FuzzCorpus"]
|