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,775 @@
|
|
|
1
|
+
"""
|
|
2
|
+
All 8 fix agents for Patchi.
|
|
3
|
+
|
|
4
|
+
Fix agents use AI to generate surgical patches for findings from scanner agents.
|
|
5
|
+
Each fix agent:
|
|
6
|
+
1. Receives a Finding + the current file content
|
|
7
|
+
2. Calls AI with a structured system prompt + skill-specific user prompt
|
|
8
|
+
3. Parses the AI response into a Patch with FileChange(s)
|
|
9
|
+
4. Computes risk_score and confidence
|
|
10
|
+
5. Returns the Patch (never applies it — applier does that)
|
|
11
|
+
|
|
12
|
+
AI call budget: fix agents make 1 AI call per finding. No loops, no re-asks.
|
|
13
|
+
If the AI response can't be parsed into a clean diff, the finding is tagged
|
|
14
|
+
'fix_failed' and returned to the user with the raw AI explanation.
|
|
15
|
+
|
|
16
|
+
Agents:
|
|
17
|
+
1. CodeFixer — bug fixes, missing error handling, null checks
|
|
18
|
+
2. SecurityFixer — patches confirmed vulnerabilities
|
|
19
|
+
3. DeadCodeRemover — proposes deletion of confirmed dead files/functions
|
|
20
|
+
4. DependencyFixer — version bumps, lockfile fixes, Dependabot setup
|
|
21
|
+
5. EnvFixer — .env.example generation, secret removal from source
|
|
22
|
+
6. TypeFixer — TypeScript type errors, unsafe casts (TS only)
|
|
23
|
+
7. RefactorAgent — extracts duplicated logic to shared functions
|
|
24
|
+
8. UnitTestRunner — generates test skeletons for uncovered functions
|
|
25
|
+
|
|
26
|
+
Fix agents are registered in the same registry as scanner agents.
|
|
27
|
+
The coordinator runs them sequentially (not in parallel — spec is explicit).
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import json
|
|
33
|
+
import logging
|
|
34
|
+
import re
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
|
|
37
|
+
from patchi.core.agents.base import (
|
|
38
|
+
AgentDomain,
|
|
39
|
+
AgentGroup,
|
|
40
|
+
AgentInput,
|
|
41
|
+
AgentResult,
|
|
42
|
+
BaseAgent,
|
|
43
|
+
register,
|
|
44
|
+
)
|
|
45
|
+
from patchi.core.ai.client import call_ai
|
|
46
|
+
from patchi.core.ai.prompts import (
|
|
47
|
+
Skill,
|
|
48
|
+
build_prompt,
|
|
49
|
+
get_system_prompt,
|
|
50
|
+
)
|
|
51
|
+
from patchi.core.fix.base import compute_blast_radius
|
|
52
|
+
from patchi.core.fix.patch import (
|
|
53
|
+
FileChange,
|
|
54
|
+
Patch,
|
|
55
|
+
PatchState,
|
|
56
|
+
PatchType,
|
|
57
|
+
compute_confidence,
|
|
58
|
+
compute_risk_score,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
_log = logging.getLogger("patchi.fix.fix_agents")
|
|
62
|
+
|
|
63
|
+
# ── Shared helpers ─────────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _read_file(path: Path) -> str:
|
|
67
|
+
try:
|
|
68
|
+
return path.read_text(encoding="utf-8", errors="replace")
|
|
69
|
+
except OSError:
|
|
70
|
+
return ""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _extract_code_block(text: str) -> str:
|
|
74
|
+
"""Extract the first ```...``` block from an AI response."""
|
|
75
|
+
m = re.search(r"```(?:\w+)?\n(.*?)```", text, re.DOTALL)
|
|
76
|
+
return m.group(1) if m else text.strip()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _try_apply_partial(ai_response: str, original: str) -> str | None:
|
|
80
|
+
"""
|
|
81
|
+
If AI returned partial lines (e.g., '42: fixed_code'), apply them to the original.
|
|
82
|
+
Returns the full file with changes applied, or None if not a partial response.
|
|
83
|
+
"""
|
|
84
|
+
lines = original.splitlines()
|
|
85
|
+
partial_pattern = re.compile(r"^(\d+):\s*(.*)$")
|
|
86
|
+
changes = {}
|
|
87
|
+
for line in ai_response.splitlines():
|
|
88
|
+
line = line.strip()
|
|
89
|
+
m = partial_pattern.match(line)
|
|
90
|
+
if m:
|
|
91
|
+
line_num = int(m.group(1))
|
|
92
|
+
new_code = m.group(2)
|
|
93
|
+
if 1 <= line_num <= len(lines):
|
|
94
|
+
changes[line_num - 1] = new_code
|
|
95
|
+
|
|
96
|
+
if not changes:
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
for idx, new_code in changes.items():
|
|
100
|
+
lines[idx] = new_code
|
|
101
|
+
return "\n".join(lines)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _detect_language(file_path: str, project_root: str | None = None) -> str:
|
|
105
|
+
"""Detect language from file extension for prompt formatting."""
|
|
106
|
+
ext = Path(file_path).suffix.lower()
|
|
107
|
+
# .h files are ambiguous (C vs C++). Default to "c", but if project has .cpp files, assume C++
|
|
108
|
+
if ext == ".h" and project_root:
|
|
109
|
+
root = Path(project_root)
|
|
110
|
+
if root.exists():
|
|
111
|
+
cpp_files = (
|
|
112
|
+
list(root.rglob("*.cpp")) + list(root.rglob("*.cxx")) + list(root.rglob("*.cc"))
|
|
113
|
+
)
|
|
114
|
+
if cpp_files:
|
|
115
|
+
return "cpp"
|
|
116
|
+
return {
|
|
117
|
+
".py": "python",
|
|
118
|
+
".pyw": "python",
|
|
119
|
+
".js": "javascript",
|
|
120
|
+
".jsx": "javascript",
|
|
121
|
+
".mjs": "javascript",
|
|
122
|
+
".cjs": "javascript",
|
|
123
|
+
".ts": "typescript",
|
|
124
|
+
".tsx": "typescript",
|
|
125
|
+
".mts": "typescript",
|
|
126
|
+
".rs": "rust",
|
|
127
|
+
".java": "java",
|
|
128
|
+
".go": "go",
|
|
129
|
+
".rb": "ruby",
|
|
130
|
+
".swift": "swift",
|
|
131
|
+
".kt": "kotlin",
|
|
132
|
+
".scala": "scala",
|
|
133
|
+
".cs": "csharp",
|
|
134
|
+
".dart": "dart",
|
|
135
|
+
".c": "c",
|
|
136
|
+
".h": "c",
|
|
137
|
+
".cpp": "cpp",
|
|
138
|
+
".cxx": "cpp",
|
|
139
|
+
".cc": "cpp",
|
|
140
|
+
".hpp": "cpp",
|
|
141
|
+
".php": "php",
|
|
142
|
+
".php3": "php",
|
|
143
|
+
".php4": "php",
|
|
144
|
+
".php5": "php",
|
|
145
|
+
".phtml": "php",
|
|
146
|
+
".svelte": "svelte",
|
|
147
|
+
".sh": "bash",
|
|
148
|
+
".bash": "bash",
|
|
149
|
+
".zsh": "bash",
|
|
150
|
+
".yml": "yaml",
|
|
151
|
+
".yaml": "yaml",
|
|
152
|
+
".json": "json",
|
|
153
|
+
".html": "html",
|
|
154
|
+
".htm": "html",
|
|
155
|
+
".css": "css",
|
|
156
|
+
".sql": "sql",
|
|
157
|
+
".md": "markdown",
|
|
158
|
+
".dockerfile": "dockerfile",
|
|
159
|
+
}.get(ext, "")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _make_patch(
|
|
163
|
+
agent_name: str,
|
|
164
|
+
patch_type: PatchType,
|
|
165
|
+
changes: list[FileChange],
|
|
166
|
+
description: str,
|
|
167
|
+
ai_explanation: str,
|
|
168
|
+
finding_id: str = "",
|
|
169
|
+
blast_radius: int = 0,
|
|
170
|
+
agent_certainty: float = 0.9,
|
|
171
|
+
has_test_coverage: bool = True,
|
|
172
|
+
scanner_agent: str = "",
|
|
173
|
+
source_finding: dict | None = None,
|
|
174
|
+
) -> Patch:
|
|
175
|
+
risk = compute_risk_score(changes, blast_radius)
|
|
176
|
+
conf = compute_confidence(changes, agent_certainty, has_test_coverage=has_test_coverage)
|
|
177
|
+
return Patch(
|
|
178
|
+
agent=agent_name,
|
|
179
|
+
patch_type=patch_type,
|
|
180
|
+
finding_id=finding_id,
|
|
181
|
+
changes=changes,
|
|
182
|
+
description=description,
|
|
183
|
+
ai_explanation=ai_explanation,
|
|
184
|
+
risk_score=risk,
|
|
185
|
+
confidence=conf,
|
|
186
|
+
blast_radius=blast_radius,
|
|
187
|
+
state=PatchState.PROPOSED,
|
|
188
|
+
scanner_agent=scanner_agent,
|
|
189
|
+
source_finding=source_finding or {},
|
|
190
|
+
verify_retries=2,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _ai_fix(
|
|
195
|
+
config: dict,
|
|
196
|
+
skill: Skill,
|
|
197
|
+
file_path: str,
|
|
198
|
+
file_content: str,
|
|
199
|
+
finding: dict,
|
|
200
|
+
blast_radius: int = 0,
|
|
201
|
+
root=None,
|
|
202
|
+
framework: str = "",
|
|
203
|
+
) -> str | None:
|
|
204
|
+
"""
|
|
205
|
+
Call AI with structured system prompt + skill-specific prompt.
|
|
206
|
+
Returns the corrected file content, or None if AI unavailable/failed.
|
|
207
|
+
Handles both full-file and partial-line responses.
|
|
208
|
+
"""
|
|
209
|
+
lang = _detect_language(file_path)
|
|
210
|
+
system = get_system_prompt(skill)
|
|
211
|
+
|
|
212
|
+
fw = framework or finding.get("framework", "")
|
|
213
|
+
|
|
214
|
+
from patchi.core.debug import debug_context_from_finding
|
|
215
|
+
|
|
216
|
+
dbg = debug_context_from_finding(finding, root or Path.cwd())
|
|
217
|
+
|
|
218
|
+
ctx: dict = {
|
|
219
|
+
"debug_context": (json.dumps(dbg, indent=2) if dbg else ""),
|
|
220
|
+
"file_path": file_path,
|
|
221
|
+
"file_content": file_content[:8000],
|
|
222
|
+
"finding_message": finding.get("message", ""),
|
|
223
|
+
"finding_line": finding.get("line", 0),
|
|
224
|
+
"suggestion": finding.get("suggestion", ""),
|
|
225
|
+
"code_snippet": finding.get("code_snippet", ""),
|
|
226
|
+
"cwe": finding.get("cwe", ""),
|
|
227
|
+
"blast_radius": str(blast_radius),
|
|
228
|
+
"language": lang,
|
|
229
|
+
"framework": fw,
|
|
230
|
+
"pattern_type": finding.get("pattern_type", ""),
|
|
231
|
+
"issue_type": finding.get("type", ""),
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
user_prompt = build_prompt(skill, ctx)
|
|
235
|
+
|
|
236
|
+
# Log AI call for audit trail (WIRE-04)
|
|
237
|
+
try:
|
|
238
|
+
from patchi.core.security.governance import patchi_action_log
|
|
239
|
+
|
|
240
|
+
r = root or Path.cwd()
|
|
241
|
+
patchi_action_log(
|
|
242
|
+
r,
|
|
243
|
+
"ai_call",
|
|
244
|
+
file_path,
|
|
245
|
+
agent=skill.value if hasattr(skill, "value") else "fix_agent",
|
|
246
|
+
detail=finding.get("type", ""),
|
|
247
|
+
)
|
|
248
|
+
except Exception as e:
|
|
249
|
+
_log.warning("Failed to write AI-call audit log entry for %s: %s", file_path, e)
|
|
250
|
+
|
|
251
|
+
response = call_ai(config, system, user_prompt)
|
|
252
|
+
if not response:
|
|
253
|
+
return None
|
|
254
|
+
|
|
255
|
+
proposed = _extract_code_block(response)
|
|
256
|
+
if not proposed.strip() or proposed.strip() == file_content.strip():
|
|
257
|
+
return None
|
|
258
|
+
|
|
259
|
+
# Check if AI returned partial lines (e.g., "42: fixed_code")
|
|
260
|
+
# Apply partial changes to the original file
|
|
261
|
+
partial = _try_apply_partial(proposed, file_content)
|
|
262
|
+
if partial is not None:
|
|
263
|
+
return partial
|
|
264
|
+
|
|
265
|
+
return proposed
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
# CodeFixer, SecurityFixer, and DeadCodeRemover live in their own files
|
|
269
|
+
# (code_fixer.py, security_fixer.py, dead_code_remover.py) imported via fix/__init__.py.
|
|
270
|
+
# They are registered there with @register.
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
# ── 4. DependencyFixer ────────────────────────────────────────────────────────
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@register
|
|
277
|
+
class DependencyFixer(BaseAgent):
|
|
278
|
+
"""
|
|
279
|
+
Version bumps for vulnerable dependencies.
|
|
280
|
+
Reads the OSV result and proposes the patched version.
|
|
281
|
+
"""
|
|
282
|
+
|
|
283
|
+
name = "DependencyFixer"
|
|
284
|
+
group = AgentGroup.FIX
|
|
285
|
+
domain = AgentDomain.CODE_QUALITY
|
|
286
|
+
timeout = 60
|
|
287
|
+
|
|
288
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
289
|
+
vuln_findings = [
|
|
290
|
+
f for f in inp.extra.get("findings", []) if f.get("type") == "vulnerable_dependency"
|
|
291
|
+
]
|
|
292
|
+
|
|
293
|
+
patches: list[Patch] = []
|
|
294
|
+
|
|
295
|
+
for finding in vuln_findings[:10]:
|
|
296
|
+
pkg_name = self._extract_package_name(finding.get("message", ""))
|
|
297
|
+
if not pkg_name:
|
|
298
|
+
continue
|
|
299
|
+
|
|
300
|
+
for dep_file in [
|
|
301
|
+
"requirements.txt",
|
|
302
|
+
"package.json",
|
|
303
|
+
"pyproject.toml",
|
|
304
|
+
"Cargo.toml",
|
|
305
|
+
"go.mod",
|
|
306
|
+
"Gemfile",
|
|
307
|
+
"composer.json",
|
|
308
|
+
"pom.xml",
|
|
309
|
+
"build.gradle",
|
|
310
|
+
"build.gradle.kts",
|
|
311
|
+
"pubspec.yaml",
|
|
312
|
+
"Package.swift",
|
|
313
|
+
"Package.resolved",
|
|
314
|
+
]:
|
|
315
|
+
dep_path = inp.root / dep_file
|
|
316
|
+
if not dep_path.exists():
|
|
317
|
+
continue
|
|
318
|
+
|
|
319
|
+
original = _read_file(dep_path)
|
|
320
|
+
if not original:
|
|
321
|
+
continue
|
|
322
|
+
|
|
323
|
+
system = get_system_prompt(Skill.DEPENDENCY_FIX)
|
|
324
|
+
user_prompt = build_prompt(
|
|
325
|
+
Skill.DEPENDENCY_FIX,
|
|
326
|
+
{
|
|
327
|
+
"package_name": pkg_name,
|
|
328
|
+
"finding_message": finding.get("message", ""),
|
|
329
|
+
"cwe": finding.get("cwe", finding.get("detail", "")),
|
|
330
|
+
"package_file": dep_file,
|
|
331
|
+
"package_file_content": original[:3000],
|
|
332
|
+
},
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
# Log AI call for audit trail (WIRE-04)
|
|
336
|
+
try:
|
|
337
|
+
from patchi.core.security.governance import patchi_action_log
|
|
338
|
+
|
|
339
|
+
patchi_action_log(
|
|
340
|
+
inp.root, "ai_call", dep_file, agent="DependencyFixer", detail=pkg_name
|
|
341
|
+
)
|
|
342
|
+
except Exception as e:
|
|
343
|
+
_log.warning("Failed to write AI-call audit log entry for %s: %s", dep_file, e)
|
|
344
|
+
|
|
345
|
+
response = call_ai(inp.config, system, user_prompt)
|
|
346
|
+
if not response:
|
|
347
|
+
continue
|
|
348
|
+
|
|
349
|
+
proposed = _extract_code_block(response)
|
|
350
|
+
if proposed == original or not proposed.strip():
|
|
351
|
+
continue
|
|
352
|
+
|
|
353
|
+
change = FileChange(path=dep_file, original=original, proposed=proposed)
|
|
354
|
+
if not change.diff.strip():
|
|
355
|
+
break # AI made no change, skip this file
|
|
356
|
+
|
|
357
|
+
blast_radius = compute_blast_radius(dep_file, inp.root)
|
|
358
|
+
|
|
359
|
+
patch = _make_patch(
|
|
360
|
+
agent_name=self.name,
|
|
361
|
+
patch_type=PatchType.DEPENDENCY,
|
|
362
|
+
changes=[change],
|
|
363
|
+
description=f"Update vulnerable dependency: {pkg_name}",
|
|
364
|
+
ai_explanation=finding.get("detail", finding.get("message", "")),
|
|
365
|
+
finding_id=finding.get("type", ""),
|
|
366
|
+
blast_radius=blast_radius,
|
|
367
|
+
)
|
|
368
|
+
patches.append(patch)
|
|
369
|
+
result.data.setdefault("patches", []).append(patch.to_dict())
|
|
370
|
+
break
|
|
371
|
+
|
|
372
|
+
result.data["patch_count"] = len(patches)
|
|
373
|
+
result.ai_calls_made = len(patches)
|
|
374
|
+
|
|
375
|
+
def _extract_package_name(self, message: str) -> str:
|
|
376
|
+
m = re.match(r"^([\w\-\.\/]+)\s", message)
|
|
377
|
+
return m.group(1) if m else ""
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
# ── 5. EnvFixer ───────────────────────────────────────────────────────────────
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
@register
|
|
384
|
+
class EnvFixer(BaseAgent):
|
|
385
|
+
"""
|
|
386
|
+
Fixes environment variable issues:
|
|
387
|
+
- Removes hardcoded secrets from source and replaces with os.environ / process.env
|
|
388
|
+
- Generates .env.example from .env variable names
|
|
389
|
+
- Adds missing .gitignore entries
|
|
390
|
+
"""
|
|
391
|
+
|
|
392
|
+
name = "EnvFixer"
|
|
393
|
+
group = AgentGroup.FIX
|
|
394
|
+
timeout = 60
|
|
395
|
+
|
|
396
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
397
|
+
secret_findings = [
|
|
398
|
+
f for f in inp.extra.get("findings", []) if f.get("type") == "hardcoded_secret"
|
|
399
|
+
]
|
|
400
|
+
|
|
401
|
+
patches: list[Patch] = []
|
|
402
|
+
|
|
403
|
+
for finding in secret_findings[:10]:
|
|
404
|
+
fpath = finding.get("file", "")
|
|
405
|
+
path = inp.root / fpath
|
|
406
|
+
original = _read_file(path)
|
|
407
|
+
if not original:
|
|
408
|
+
continue
|
|
409
|
+
|
|
410
|
+
framework = finding.get("framework", "")
|
|
411
|
+
proposed = _ai_fix(
|
|
412
|
+
inp.config,
|
|
413
|
+
Skill.ENV_FIX,
|
|
414
|
+
fpath,
|
|
415
|
+
original,
|
|
416
|
+
finding,
|
|
417
|
+
root=inp.root,
|
|
418
|
+
framework=framework,
|
|
419
|
+
)
|
|
420
|
+
if not proposed:
|
|
421
|
+
continue
|
|
422
|
+
|
|
423
|
+
change = FileChange(path=fpath, original=original, proposed=proposed)
|
|
424
|
+
if not change.diff.strip():
|
|
425
|
+
continue
|
|
426
|
+
|
|
427
|
+
blast_radius = compute_blast_radius(fpath, inp.root)
|
|
428
|
+
|
|
429
|
+
patch = _make_patch(
|
|
430
|
+
agent_name=self.name,
|
|
431
|
+
patch_type=PatchType.ENV_FIX,
|
|
432
|
+
changes=[change],
|
|
433
|
+
description=f"Replace hardcoded secret in {fpath}",
|
|
434
|
+
ai_explanation="Moved secret to environment variable reference.",
|
|
435
|
+
finding_id=finding.get("type", ""),
|
|
436
|
+
blast_radius=blast_radius,
|
|
437
|
+
agent_certainty=0.9,
|
|
438
|
+
)
|
|
439
|
+
patches.append(patch)
|
|
440
|
+
result.data.setdefault("patches", []).append(patch.to_dict())
|
|
441
|
+
|
|
442
|
+
# Also generate .env.example if not present
|
|
443
|
+
self._generate_env_example(inp, result)
|
|
444
|
+
|
|
445
|
+
result.data["patch_count"] = len(patches)
|
|
446
|
+
result.ai_calls_made = len(patches)
|
|
447
|
+
|
|
448
|
+
def _generate_env_example(self, inp: AgentInput, result: AgentResult) -> None:
|
|
449
|
+
env_example = inp.root / ".env.example"
|
|
450
|
+
if env_example.exists():
|
|
451
|
+
return
|
|
452
|
+
|
|
453
|
+
env_vars = inp.extra.get("env_vars", [])
|
|
454
|
+
if not env_vars:
|
|
455
|
+
return
|
|
456
|
+
|
|
457
|
+
content = "# Environment variables required by this application\n"
|
|
458
|
+
content += "# Copy to .env and fill in the values\n\n"
|
|
459
|
+
for var in env_vars:
|
|
460
|
+
content += f"{var}=\n"
|
|
461
|
+
|
|
462
|
+
change = FileChange(path=".env.example", original="", proposed=content)
|
|
463
|
+
patch = _make_patch(
|
|
464
|
+
agent_name=self.name,
|
|
465
|
+
patch_type=PatchType.ENV_FIX,
|
|
466
|
+
changes=[change],
|
|
467
|
+
description="Generate .env.example from discovered env variables",
|
|
468
|
+
ai_explanation="Created .env.example so contributors know which variables are required.",
|
|
469
|
+
agent_certainty=1.0,
|
|
470
|
+
)
|
|
471
|
+
result.data.setdefault("patches", []).append(patch.to_dict())
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
# ── 6. TypeFixer ──────────────────────────────────────────────────────────────
|
|
475
|
+
|
|
476
|
+
# Multi-language type-issue finding types produced by the type checker
|
|
477
|
+
# (patchi.core.brain.type_checker). TypeFixer now handles every language the
|
|
478
|
+
# checker supports, not just TypeScript.
|
|
479
|
+
TYPE_ISSUE_TYPES = frozenset(
|
|
480
|
+
{
|
|
481
|
+
"any_type",
|
|
482
|
+
"dynamic_type",
|
|
483
|
+
"empty_interface",
|
|
484
|
+
"explicit_any",
|
|
485
|
+
"explicit_object",
|
|
486
|
+
"missing_param_type",
|
|
487
|
+
"missing_return_type",
|
|
488
|
+
"missing_type",
|
|
489
|
+
"missing_prop_types",
|
|
490
|
+
"non_null_assertion",
|
|
491
|
+
"trait_object",
|
|
492
|
+
"ts_ignore",
|
|
493
|
+
"unsafe_cast",
|
|
494
|
+
"untyped_return",
|
|
495
|
+
"unwrap_call",
|
|
496
|
+
}
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
@register
|
|
501
|
+
class TypeFixer(BaseAgent):
|
|
502
|
+
"""Multi-language type fixer: explicit any, unsafe casts, missing return types, etc.
|
|
503
|
+
|
|
504
|
+
Handles type-issue findings from every language the type checker supports
|
|
505
|
+
(TypeScript, Python, Go, Java, Kotlin, C#, Dart, PHP, Swift, Rust). The AI
|
|
506
|
+
prompt receives the file's language so the corrected code is idiomatic.
|
|
507
|
+
"""
|
|
508
|
+
|
|
509
|
+
name = "TypeFixer"
|
|
510
|
+
group = AgentGroup.FIX
|
|
511
|
+
timeout = 120
|
|
512
|
+
|
|
513
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
514
|
+
# TypeFixer handles any type-issue finding. The type scanner does not
|
|
515
|
+
# always tag fix_agent, so accept both tagged and untagged type issues.
|
|
516
|
+
type_findings = [
|
|
517
|
+
f
|
|
518
|
+
for f in inp.extra.get("findings", [])
|
|
519
|
+
if f.get("type") in TYPE_ISSUE_TYPES
|
|
520
|
+
and (f.get("fix_agent") == self.name or f.get("fix_agent") in (None, ""))
|
|
521
|
+
]
|
|
522
|
+
|
|
523
|
+
patches: list[Patch] = []
|
|
524
|
+
|
|
525
|
+
for finding in type_findings[:10]:
|
|
526
|
+
fpath = finding.get("file", "")
|
|
527
|
+
path = inp.root / fpath
|
|
528
|
+
original = _read_file(path)
|
|
529
|
+
if not original:
|
|
530
|
+
continue
|
|
531
|
+
|
|
532
|
+
framework = finding.get("framework", "")
|
|
533
|
+
proposed = _ai_fix(
|
|
534
|
+
inp.config,
|
|
535
|
+
Skill.TYPE_FIX,
|
|
536
|
+
fpath,
|
|
537
|
+
original,
|
|
538
|
+
finding,
|
|
539
|
+
root=inp.root,
|
|
540
|
+
framework=framework,
|
|
541
|
+
)
|
|
542
|
+
if not proposed:
|
|
543
|
+
continue
|
|
544
|
+
|
|
545
|
+
change = FileChange(path=fpath, original=original, proposed=proposed)
|
|
546
|
+
if not change.diff.strip():
|
|
547
|
+
continue
|
|
548
|
+
|
|
549
|
+
blast_radius = compute_blast_radius(fpath, inp.root)
|
|
550
|
+
|
|
551
|
+
patch = _make_patch(
|
|
552
|
+
agent_name=self.name,
|
|
553
|
+
patch_type=PatchType.TYPE_FIX,
|
|
554
|
+
changes=[change],
|
|
555
|
+
description=f"Type fix ({finding.get('type', '')}): {fpath}",
|
|
556
|
+
ai_explanation="Fixed type issue using the project's type system.",
|
|
557
|
+
finding_id=finding.get("type", ""),
|
|
558
|
+
blast_radius=blast_radius,
|
|
559
|
+
)
|
|
560
|
+
patches.append(patch)
|
|
561
|
+
result.data.setdefault("patches", []).append(patch.to_dict())
|
|
562
|
+
|
|
563
|
+
result.data["patch_count"] = len(patches)
|
|
564
|
+
result.ai_calls_made = len(patches)
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
# ── 7. RefactorAgent ──────────────────────────────────────────────────────────
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
@register
|
|
571
|
+
class RefactorAgent(BaseAgent):
|
|
572
|
+
"""
|
|
573
|
+
Extracts duplicated logic into shared functions.
|
|
574
|
+
Only runs after DuplicateScanner confirms high similarity (>= 85%).
|
|
575
|
+
"""
|
|
576
|
+
|
|
577
|
+
name = "RefactorAgent"
|
|
578
|
+
group = AgentGroup.FIX
|
|
579
|
+
timeout = 120
|
|
580
|
+
|
|
581
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
582
|
+
dup_findings = [
|
|
583
|
+
f
|
|
584
|
+
for f in inp.extra.get("findings", [])
|
|
585
|
+
if f.get("type") == "duplicate_function" and f.get("fix_agent") == self.name
|
|
586
|
+
]
|
|
587
|
+
|
|
588
|
+
patches: list[Patch] = []
|
|
589
|
+
|
|
590
|
+
for finding in dup_findings[:5]:
|
|
591
|
+
file1 = finding.get("file", "")
|
|
592
|
+
file2 = finding.get("similar_to", "").split(":")[0] if finding.get("similar_to") else ""
|
|
593
|
+
|
|
594
|
+
if not file1 or not file2:
|
|
595
|
+
continue
|
|
596
|
+
|
|
597
|
+
orig1 = _read_file(inp.root / file1)
|
|
598
|
+
orig2 = _read_file(inp.root / file2)
|
|
599
|
+
if not orig1 or not orig2:
|
|
600
|
+
continue
|
|
601
|
+
|
|
602
|
+
score = finding.get("similarity_score", 0)
|
|
603
|
+
|
|
604
|
+
system = get_system_prompt(Skill.REFACTOR)
|
|
605
|
+
user_prompt = build_prompt(
|
|
606
|
+
Skill.REFACTOR,
|
|
607
|
+
{
|
|
608
|
+
"file1_path": file1,
|
|
609
|
+
"file1_content": orig1[:2000],
|
|
610
|
+
"file2_path": file2,
|
|
611
|
+
"file2_content": orig2[:2000],
|
|
612
|
+
"similarity_score": str(score),
|
|
613
|
+
"language": _detect_language(file1),
|
|
614
|
+
},
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
# Log AI call for audit trail (WIRE-04)
|
|
618
|
+
try:
|
|
619
|
+
from patchi.core.security.governance import patchi_action_log
|
|
620
|
+
|
|
621
|
+
patchi_action_log(inp.root, "ai_call", f"{file1}, {file2}", agent="RefactorAgent")
|
|
622
|
+
except Exception as e:
|
|
623
|
+
_log.warning(
|
|
624
|
+
"Failed to write AI-call audit log entry for %s, %s: %s", file1, file2, e
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
response = call_ai(inp.config, system, user_prompt, max_tokens=3000)
|
|
628
|
+
if not response:
|
|
629
|
+
continue
|
|
630
|
+
|
|
631
|
+
blocks = re.findall(
|
|
632
|
+
r"(?://|#)?\s*FILE:\s*([^\n]+)\n```(?:\w+)?\n(.*?)```",
|
|
633
|
+
response,
|
|
634
|
+
re.DOTALL,
|
|
635
|
+
)
|
|
636
|
+
if not blocks:
|
|
637
|
+
blocks = re.findall(
|
|
638
|
+
r"FILE:\s*([^\n]+)\n```(?:\w+)?\n(.*?)```",
|
|
639
|
+
response,
|
|
640
|
+
re.DOTALL,
|
|
641
|
+
)
|
|
642
|
+
if len(blocks) < 2:
|
|
643
|
+
continue
|
|
644
|
+
|
|
645
|
+
changes: list[FileChange] = []
|
|
646
|
+
for fname, proposed_code in blocks:
|
|
647
|
+
fname = fname.strip()
|
|
648
|
+
if fname == file1:
|
|
649
|
+
changes.append(FileChange(path=file1, original=orig1, proposed=proposed_code))
|
|
650
|
+
elif fname == file2:
|
|
651
|
+
changes.append(FileChange(path=file2, original=orig2, proposed=proposed_code))
|
|
652
|
+
|
|
653
|
+
if not changes:
|
|
654
|
+
continue
|
|
655
|
+
|
|
656
|
+
br1 = compute_blast_radius(file1, inp.root)
|
|
657
|
+
br2 = compute_blast_radius(file2, inp.root)
|
|
658
|
+
blast_radius = max(br1, br2)
|
|
659
|
+
|
|
660
|
+
patch = _make_patch(
|
|
661
|
+
agent_name=self.name,
|
|
662
|
+
patch_type=PatchType.REFACTOR,
|
|
663
|
+
changes=changes,
|
|
664
|
+
description=f"Extract shared logic from {file1} and {file2}",
|
|
665
|
+
ai_explanation=f"Functions are {int(score * 100)}% similar. Extracted to shared utility.",
|
|
666
|
+
finding_id=finding.get("type", ""),
|
|
667
|
+
blast_radius=blast_radius,
|
|
668
|
+
agent_certainty=0.8,
|
|
669
|
+
has_test_coverage=False,
|
|
670
|
+
)
|
|
671
|
+
patches.append(patch)
|
|
672
|
+
result.data.setdefault("patches", []).append(patch.to_dict())
|
|
673
|
+
|
|
674
|
+
result.data["patch_count"] = len(patches)
|
|
675
|
+
result.ai_calls_made = len(patches)
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
# ── 8. UnitTestRunner ─────────────────────────────────────────────────────────
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
@register
|
|
682
|
+
class UnitTestRunner(BaseAgent):
|
|
683
|
+
"""
|
|
684
|
+
Generates test skeletons for uncovered source files.
|
|
685
|
+
Writes to tests/ directory. Never modifies source files.
|
|
686
|
+
"""
|
|
687
|
+
|
|
688
|
+
name = "UnitTestRunner"
|
|
689
|
+
group = AgentGroup.FIX
|
|
690
|
+
timeout = 120
|
|
691
|
+
|
|
692
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
693
|
+
uncovered_findings = [
|
|
694
|
+
f
|
|
695
|
+
for f in inp.extra.get("findings", [])
|
|
696
|
+
if f.get("type") == "uncovered_file" and f.get("fix_agent") == self.name
|
|
697
|
+
]
|
|
698
|
+
|
|
699
|
+
patches: list[Patch] = []
|
|
700
|
+
|
|
701
|
+
for finding in uncovered_findings[:5]:
|
|
702
|
+
source_path = finding.get("file", "")
|
|
703
|
+
path = inp.root / source_path
|
|
704
|
+
original = _read_file(path)
|
|
705
|
+
if not original:
|
|
706
|
+
continue
|
|
707
|
+
|
|
708
|
+
lang = _detect_language(source_path)
|
|
709
|
+
test_path = self._test_path(source_path, lang)
|
|
710
|
+
|
|
711
|
+
if (inp.root / test_path).exists():
|
|
712
|
+
continue
|
|
713
|
+
|
|
714
|
+
system = get_system_prompt(Skill.TEST_GENERATE)
|
|
715
|
+
user_prompt = build_prompt(
|
|
716
|
+
Skill.TEST_GENERATE,
|
|
717
|
+
{
|
|
718
|
+
"file_path": source_path,
|
|
719
|
+
"file_content": original[:3000],
|
|
720
|
+
"existing_tests_section": "Write tests for all exported functions.",
|
|
721
|
+
"language": lang,
|
|
722
|
+
},
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
# Log AI call for audit trail (WIRE-04)
|
|
726
|
+
try:
|
|
727
|
+
from patchi.core.security.governance import patchi_action_log
|
|
728
|
+
|
|
729
|
+
patchi_action_log(inp.root, "ai_call", source_path, agent="UnitTestRunner")
|
|
730
|
+
except Exception as e:
|
|
731
|
+
_log.warning("Failed to write AI-call audit log entry for %s: %s", source_path, e)
|
|
732
|
+
|
|
733
|
+
response = call_ai(inp.config, system, user_prompt)
|
|
734
|
+
if not response:
|
|
735
|
+
continue
|
|
736
|
+
|
|
737
|
+
proposed = _extract_code_block(response)
|
|
738
|
+
if not proposed.strip():
|
|
739
|
+
continue
|
|
740
|
+
|
|
741
|
+
change = FileChange(path=test_path, original="", proposed=proposed)
|
|
742
|
+
|
|
743
|
+
blast_radius = compute_blast_radius(source_path, inp.root)
|
|
744
|
+
|
|
745
|
+
patch = _make_patch(
|
|
746
|
+
agent_name=self.name,
|
|
747
|
+
patch_type=PatchType.TEST_ADD,
|
|
748
|
+
changes=[change],
|
|
749
|
+
description=f"Add tests for {source_path}",
|
|
750
|
+
ai_explanation="Generated unit test skeleton covering exported functions.",
|
|
751
|
+
finding_id=finding.get("type", ""),
|
|
752
|
+
blast_radius=blast_radius,
|
|
753
|
+
agent_certainty=0.85,
|
|
754
|
+
)
|
|
755
|
+
patches.append(patch)
|
|
756
|
+
result.data.setdefault("patches", []).append(patch.to_dict())
|
|
757
|
+
|
|
758
|
+
result.data["patch_count"] = len(patches)
|
|
759
|
+
result.ai_calls_made = len(patches)
|
|
760
|
+
|
|
761
|
+
def _test_path(self, source_path: str, lang: str) -> str:
|
|
762
|
+
from pathlib import PurePosixPath
|
|
763
|
+
|
|
764
|
+
p = PurePosixPath(source_path)
|
|
765
|
+
stem = p.stem
|
|
766
|
+
parent = str(p.parent)
|
|
767
|
+
if lang == "python":
|
|
768
|
+
test_name = f"test_{stem}.py"
|
|
769
|
+
else:
|
|
770
|
+
ext = p.suffix
|
|
771
|
+
test_name = f"{stem}.test{ext}"
|
|
772
|
+
# Mirror source directory structure under tests/
|
|
773
|
+
if parent and parent != ".":
|
|
774
|
+
return f"tests/{parent}/{test_name}"
|
|
775
|
+
return f"tests/{test_name}"
|