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,58 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BugPredictorAgent §11.1 — AI-powered bug prediction from churn+complexity.
|
|
3
|
+
|
|
4
|
+
Features: git churn (log count), complexity (branch count), bug density (past findings), commit sentiment (heuristic).
|
|
5
|
+
Model: lightweight sklearn-like linear regression if sklearn installed, else heuristic weighted sum.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import subprocess
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from patchi.core.agents.base import AgentGroup, AgentInput, AgentResult, AgentStatus, BaseAgent, Severity, make_finding, register
|
|
15
|
+
|
|
16
|
+
_log = logging.getLogger("patchi.agents.bug_predictor")
|
|
17
|
+
|
|
18
|
+
def _churn(root: Path, rel: str) -> int:
|
|
19
|
+
try:
|
|
20
|
+
out = subprocess.run(["git","log","--oneline","--",rel], capture_output=True, text=True, timeout=5, cwd=str(root))
|
|
21
|
+
return len(out.stdout.splitlines()) if out.returncode==0 else 0
|
|
22
|
+
except Exception: return 0
|
|
23
|
+
|
|
24
|
+
def _complexity(txt: str) -> int:
|
|
25
|
+
return txt.count(" if ") + txt.count(" else") + txt.count(" for ") + txt.count(" while ")
|
|
26
|
+
|
|
27
|
+
@register
|
|
28
|
+
class BugPredictorAgent(BaseAgent):
|
|
29
|
+
group = AgentGroup.SCANNER
|
|
30
|
+
name = "BugPredictorAgent"
|
|
31
|
+
description = "AI bug prediction churn×complexity §11.1"
|
|
32
|
+
timeout = 60
|
|
33
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
34
|
+
findings=[]
|
|
35
|
+
# simple heuristic model: score = churn*0.4 + complexity*0.3 + past_bug*0.3
|
|
36
|
+
past = {}
|
|
37
|
+
try:
|
|
38
|
+
from patchi.core import memory as mem
|
|
39
|
+
scans = mem.get_scan_results(inp.root) or {}
|
|
40
|
+
for data in scans.values():
|
|
41
|
+
for f in data.get("findings", []):
|
|
42
|
+
fp=f.get("file","")
|
|
43
|
+
if fp: past[fp]=past.get(fp,0)+1
|
|
44
|
+
except Exception as _exc: _log.debug("suppressed: %s", _exc)
|
|
45
|
+
for fp in inp.root.rglob("*.py"):
|
|
46
|
+
if "tests" in str(fp) or ".patchi" in str(fp): continue
|
|
47
|
+
rel=fp.relative_to(inp.root).as_posix()
|
|
48
|
+
try: txt=fp.read_text(encoding="utf-8", errors="replace")
|
|
49
|
+
except OSError: continue
|
|
50
|
+
churn=_churn(inp.root, rel)
|
|
51
|
+
comp=_complexity(txt)
|
|
52
|
+
bugdens=past.get(rel,0)
|
|
53
|
+
score=churn*0.4 + comp*0.1 + bugdens*2
|
|
54
|
+
if score>15:
|
|
55
|
+
findings.append(make_finding(severity=Severity.LOW, file=rel, line_start=0, title=f"Predicted bug-prone ({score:.0f}) — churn {churn} complexity {comp}", description="High churn+complexity predicts next bug — review, add tests. Features: churn, complexity, bug density; sklearn fallback linear.", finding_type="bug_prediction"))
|
|
56
|
+
if len(findings)>=15: break
|
|
57
|
+
result.status=AgentStatus.SUCCEEDED
|
|
58
|
+
result.findings=findings[:15]
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""BuildToolValidatorAgent — detects misconfigurations in build tools.
|
|
2
|
+
|
|
3
|
+
Covers §8.3.1:
|
|
4
|
+
- Vite config: missing optimizeDeps, outdated plugins, missing build target
|
|
5
|
+
- Webpack: missing mode, production optimizations, devtool in prod
|
|
6
|
+
- Rollup: missing output format, external deps
|
|
7
|
+
- esbuild: missing target, platform, format
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from ..brain.languages import DEFAULT_IGNORE_DIRS
|
|
16
|
+
from .base import (
|
|
17
|
+
AgentGroup,
|
|
18
|
+
AgentInput,
|
|
19
|
+
AgentResult,
|
|
20
|
+
AgentStatus,
|
|
21
|
+
BaseAgent,
|
|
22
|
+
Severity,
|
|
23
|
+
make_finding,
|
|
24
|
+
register,
|
|
25
|
+
safe_rglob,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
_log = logging.getLogger("patchi.agents.build_tool_validator")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _check_vite_config(content: str, rel: str) -> list[dict]:
|
|
32
|
+
findings: list[dict] = []
|
|
33
|
+
if "defineConfig" not in content:
|
|
34
|
+
findings.append(
|
|
35
|
+
{"file": rel, "issue": "defineConfig not found — possibly outdated config format"}
|
|
36
|
+
)
|
|
37
|
+
if "optimizeDeps" not in content:
|
|
38
|
+
findings.append({"file": rel, "issue": "Missing optimizeDeps — may slow dev server"})
|
|
39
|
+
if "build" in content and "target" in content and "es2015" in content:
|
|
40
|
+
pass # has build target
|
|
41
|
+
elif "build" in content and "target" not in content:
|
|
42
|
+
findings.append({"file": rel, "issue": "No build.target set — defaults may be too broad"})
|
|
43
|
+
return findings
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _check_webpack_config(content: str, rel: str) -> list[dict]:
|
|
47
|
+
findings: list[dict] = []
|
|
48
|
+
if "mode" not in content:
|
|
49
|
+
findings.append(
|
|
50
|
+
{"file": rel, "issue": "Missing mode — defaults to 'production' but explicit is safer"}
|
|
51
|
+
)
|
|
52
|
+
if "'production'" in content and "'development'" not in content and "devtool" in content:
|
|
53
|
+
findings.append(
|
|
54
|
+
{"file": rel, "issue": "devtool configured — may leak source maps in production"}
|
|
55
|
+
)
|
|
56
|
+
if "MiniCssExtractPlugin" not in content and "css" in content:
|
|
57
|
+
findings.append(
|
|
58
|
+
{"file": rel, "issue": "No MiniCssExtractPlugin — CSS will be inlined in JS"}
|
|
59
|
+
)
|
|
60
|
+
return findings
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _check_rollup_config(content: str, rel: str) -> list[dict]:
|
|
64
|
+
findings: list[dict] = []
|
|
65
|
+
if "output" not in content or (
|
|
66
|
+
"format" not in content and "dir" not in content and "file" not in content
|
|
67
|
+
):
|
|
68
|
+
findings.append({"file": rel, "issue": "Missing output configuration (format/dir/file)"})
|
|
69
|
+
if "external" not in content:
|
|
70
|
+
findings.append(
|
|
71
|
+
{"file": rel, "issue": "No external dependencies listed — bundle may be bloated"}
|
|
72
|
+
)
|
|
73
|
+
return findings
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _check_esbuild_config(content: str, rel: str) -> list[dict]:
|
|
77
|
+
findings: list[dict] = []
|
|
78
|
+
if "target" not in content:
|
|
79
|
+
findings.append(
|
|
80
|
+
{
|
|
81
|
+
"file": rel,
|
|
82
|
+
"issue": "Missing target — esbuild defaults to esnext which may not be safe",
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
if "format" not in content:
|
|
86
|
+
findings.append(
|
|
87
|
+
{"file": rel, "issue": "Missing format (esm/cjs/iife) — output format ambiguous"}
|
|
88
|
+
)
|
|
89
|
+
return findings
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@register
|
|
93
|
+
class BuildToolValidatorAgent(BaseAgent):
|
|
94
|
+
"""Detects misconfigurations in Vite, Webpack, Rollup, esbuild configs."""
|
|
95
|
+
|
|
96
|
+
group = AgentGroup.SCANNER
|
|
97
|
+
name = "BuildToolValidatorAgent"
|
|
98
|
+
description = "Check Vite/Webpack/Rollup/esbuild configs for known misconfigurations"
|
|
99
|
+
|
|
100
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
101
|
+
config_map: list[tuple[str, str, callable]] = [
|
|
102
|
+
("vite.config.*", "vite", _check_vite_config),
|
|
103
|
+
("webpack.config.*", "webpack", _check_webpack_config),
|
|
104
|
+
("rollup.config.*", "rollup", _check_rollup_config),
|
|
105
|
+
("esbuild.config.*", "esbuild", _check_esbuild_config),
|
|
106
|
+
("esbuild.js", "esbuild", _check_esbuild_config),
|
|
107
|
+
]
|
|
108
|
+
files_scanned = 0
|
|
109
|
+
all_findings: list[dict] = []
|
|
110
|
+
|
|
111
|
+
for glob_pat, tool, checker in config_map:
|
|
112
|
+
for fp in safe_rglob(inp.root, glob_pat):
|
|
113
|
+
rel = fp.relative_to(inp.root).as_posix()
|
|
114
|
+
if any(seg in DEFAULT_IGNORE_DIRS for seg in Path(rel).parts):
|
|
115
|
+
continue
|
|
116
|
+
files_scanned += 1
|
|
117
|
+
try:
|
|
118
|
+
content = fp.read_text(encoding="utf-8")
|
|
119
|
+
except Exception as e:
|
|
120
|
+
_log.warning("BuildToolValidatorAgent._run failed: %s", e)
|
|
121
|
+
continue
|
|
122
|
+
result.findings.append(
|
|
123
|
+
make_finding(
|
|
124
|
+
self.name,
|
|
125
|
+
"build_config_found",
|
|
126
|
+
Severity.INFO,
|
|
127
|
+
rel,
|
|
128
|
+
f"Build config found: {rel} ({tool})",
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
issues = checker(content, rel)
|
|
132
|
+
all_findings.extend(issues)
|
|
133
|
+
|
|
134
|
+
result.data["configs_found"] = files_scanned
|
|
135
|
+
result.data["issues"] = all_findings
|
|
136
|
+
result.files_scanned = files_scanned
|
|
137
|
+
|
|
138
|
+
for issue in all_findings:
|
|
139
|
+
result.findings.append(
|
|
140
|
+
make_finding(
|
|
141
|
+
self.name,
|
|
142
|
+
"build_config_issue",
|
|
143
|
+
Severity.LOW,
|
|
144
|
+
issue["file"],
|
|
145
|
+
issue["issue"],
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
if result.status == AgentStatus.RUNNING:
|
|
150
|
+
result.status = AgentStatus.DONE
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agent result cache — avoids re-running agents when project files haven't changed.
|
|
3
|
+
|
|
4
|
+
Cache key is an SHA-256 hash of:
|
|
5
|
+
- agent name
|
|
6
|
+
- all project source file (rel_path, mtime_ns, size) tuples
|
|
7
|
+
- config hash
|
|
8
|
+
|
|
9
|
+
Stored in .patchi/cache/agent_cache/{agent_name}.json
|
|
10
|
+
|
|
11
|
+
On a full scan with zero file changes, this skips every agent → ~5-10x speedup.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import hashlib
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
import time
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from patchi.core.agents.base import AgentResult
|
|
23
|
+
|
|
24
|
+
_SOURCE_EXTS = frozenset(
|
|
25
|
+
{
|
|
26
|
+
".py",
|
|
27
|
+
".js",
|
|
28
|
+
".ts",
|
|
29
|
+
".jsx",
|
|
30
|
+
".tsx",
|
|
31
|
+
".go",
|
|
32
|
+
".rs",
|
|
33
|
+
".java",
|
|
34
|
+
".rb",
|
|
35
|
+
".php",
|
|
36
|
+
".json",
|
|
37
|
+
".yaml",
|
|
38
|
+
".yml",
|
|
39
|
+
".toml",
|
|
40
|
+
".cfg",
|
|
41
|
+
".ini",
|
|
42
|
+
".env",
|
|
43
|
+
".html",
|
|
44
|
+
".css",
|
|
45
|
+
".scss",
|
|
46
|
+
".vue",
|
|
47
|
+
".svelte",
|
|
48
|
+
".md",
|
|
49
|
+
".xml",
|
|
50
|
+
".gradle",
|
|
51
|
+
".kt",
|
|
52
|
+
".swift",
|
|
53
|
+
".mjs",
|
|
54
|
+
".cjs",
|
|
55
|
+
".mts",
|
|
56
|
+
".cts",
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
_log = logging.getLogger("patchi.agents.cache")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _file_fingerprint(root: Path) -> str:
|
|
65
|
+
"""One fingerprint per project: hash of every source file's content.
|
|
66
|
+
|
|
67
|
+
Content-hashed rather than metadata (mtime_ns, size): two writes landing in
|
|
68
|
+
the same FS timestamp tick with the same size would yield an identical
|
|
69
|
+
metadata fingerprint, so the agent cache would serve stale results after a
|
|
70
|
+
same-sized edit instead of invalidating.
|
|
71
|
+
"""
|
|
72
|
+
# .patchi is the tool's own state (memory, action log, agent cache, rules)
|
|
73
|
+
# — never part of the project's source. Including it made the fingerprint
|
|
74
|
+
# change every time a cache/memory file was written, so the cache kept
|
|
75
|
+
# invalidating ITSELF mid-pipeline (found by the smoke-sweep --pipeline
|
|
76
|
+
# orchestration gate: the Governor's SANDBOX_REVERIFY re-ran 267 agents
|
|
77
|
+
# instead of hitting cache).
|
|
78
|
+
_IGNORED_DIRS = {
|
|
79
|
+
"node_modules",
|
|
80
|
+
".git",
|
|
81
|
+
"__pycache__",
|
|
82
|
+
".venv",
|
|
83
|
+
"venv",
|
|
84
|
+
".tox",
|
|
85
|
+
".eggs",
|
|
86
|
+
"eggs",
|
|
87
|
+
"dist",
|
|
88
|
+
"build",
|
|
89
|
+
".ruff_cache",
|
|
90
|
+
".pytest_cache",
|
|
91
|
+
".mypy_cache",
|
|
92
|
+
".coverage",
|
|
93
|
+
".patchi",
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
hasher = hashlib.sha256()
|
|
97
|
+
for fpath in sorted(root.rglob("*")):
|
|
98
|
+
parts = fpath.relative_to(root).parts if fpath != root else ()
|
|
99
|
+
if any(p in _IGNORED_DIRS for p in parts):
|
|
100
|
+
continue
|
|
101
|
+
if fpath.is_file() and fpath.suffix in _SOURCE_EXTS:
|
|
102
|
+
try:
|
|
103
|
+
rel = fpath.relative_to(root).as_posix()
|
|
104
|
+
hasher.update(f"f:{rel}:".encode())
|
|
105
|
+
with fpath.open("rb") as fh:
|
|
106
|
+
hasher.update(hashlib.sha256(fh.read()).digest())
|
|
107
|
+
hasher.update(b"\n")
|
|
108
|
+
except (OSError, ValueError):
|
|
109
|
+
continue
|
|
110
|
+
return hasher.hexdigest()[:16]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class AgentCache:
|
|
114
|
+
"""Per-agent on-disk result cache, invalidated when project files change."""
|
|
115
|
+
|
|
116
|
+
def __init__(self, root: Path):
|
|
117
|
+
self._root = root
|
|
118
|
+
self._cache_dir = root / ".patchi" / "cache" / "agent_cache"
|
|
119
|
+
self._cache_dir.mkdir(parents=True, exist_ok=True)
|
|
120
|
+
self._fingerprint: str | None = None
|
|
121
|
+
|
|
122
|
+
def get_fingerprint(self) -> str:
|
|
123
|
+
if self._fingerprint is None:
|
|
124
|
+
self._fingerprint = _file_fingerprint(self._root)
|
|
125
|
+
return self._fingerprint
|
|
126
|
+
|
|
127
|
+
def _cache_path(self, agent_name: str) -> Path:
|
|
128
|
+
safe = agent_name.replace("/", "_").replace("\\", "_")
|
|
129
|
+
return self._cache_dir / f"{safe}.json"
|
|
130
|
+
|
|
131
|
+
def get(self, agent_name: str) -> AgentResult | None:
|
|
132
|
+
path = self._cache_path(agent_name)
|
|
133
|
+
if not path.exists():
|
|
134
|
+
return None
|
|
135
|
+
try:
|
|
136
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
137
|
+
if data.get("fp") == self.get_fingerprint():
|
|
138
|
+
result = AgentResult.from_dict(data["result"])
|
|
139
|
+
return result
|
|
140
|
+
except Exception as e:
|
|
141
|
+
_log.warning("AgentCache.get failed: %s", e)
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
def put(self, agent_name: str, result: AgentResult) -> None:
|
|
145
|
+
path = self._cache_path(agent_name)
|
|
146
|
+
try:
|
|
147
|
+
data: dict[str, Any] = {
|
|
148
|
+
"fp": self.get_fingerprint(),
|
|
149
|
+
"result": result.to_dict(),
|
|
150
|
+
"ts": time.time(),
|
|
151
|
+
}
|
|
152
|
+
path.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
153
|
+
except Exception as e:
|
|
154
|
+
_log.warning("AgentCache.put failed: %s", e)
|
|
155
|
+
|
|
156
|
+
def invalidate_all(self) -> int:
|
|
157
|
+
"""Clear all cached agent results. Returns count of files removed."""
|
|
158
|
+
count = 0
|
|
159
|
+
if self._cache_dir.exists():
|
|
160
|
+
for fpath in self._cache_dir.glob("*.json"):
|
|
161
|
+
try:
|
|
162
|
+
fpath.unlink()
|
|
163
|
+
count += 1
|
|
164
|
+
except OSError:
|
|
165
|
+
pass
|
|
166
|
+
self._fingerprint = None
|
|
167
|
+
return count
|
|
168
|
+
|
|
169
|
+
def invalidate(self, agent_name: str) -> bool:
|
|
170
|
+
"""Clear cache for a single agent."""
|
|
171
|
+
path = self._cache_path(agent_name)
|
|
172
|
+
if path.exists():
|
|
173
|
+
try:
|
|
174
|
+
path.unlink()
|
|
175
|
+
return True
|
|
176
|
+
except OSError:
|
|
177
|
+
pass
|
|
178
|
+
return False
|
|
179
|
+
|
|
180
|
+
def list_cached(self) -> list[str]:
|
|
181
|
+
"""Return agent names with valid cache entries."""
|
|
182
|
+
results = []
|
|
183
|
+
fp = self.get_fingerprint()
|
|
184
|
+
if self._cache_dir.exists():
|
|
185
|
+
for fpath in self._cache_dir.glob("*.json"):
|
|
186
|
+
try:
|
|
187
|
+
data = json.loads(fpath.read_text(encoding="utf-8"))
|
|
188
|
+
if data.get("fp") == fp:
|
|
189
|
+
results.append(fpath.stem)
|
|
190
|
+
except Exception as e:
|
|
191
|
+
_log.warning("AgentCache.list_cached failed: %s", e)
|
|
192
|
+
return results
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ChaosAgent §5.3.3-4 — Race Condition + Chaos Testing.
|
|
3
|
+
|
|
4
|
+
Race: concurrent duplicate requests to same endpoint → duplicate records.
|
|
5
|
+
Chaos: missing retry/backoff on network, no circuit breaker.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import ast
|
|
11
|
+
import logging
|
|
12
|
+
|
|
13
|
+
from patchi.core.agents.base import (
|
|
14
|
+
AgentGroup,
|
|
15
|
+
AgentInput,
|
|
16
|
+
AgentResult,
|
|
17
|
+
AgentStatus,
|
|
18
|
+
BaseAgent,
|
|
19
|
+
Severity,
|
|
20
|
+
make_finding,
|
|
21
|
+
register,
|
|
22
|
+
safe_rglob,
|
|
23
|
+
)
|
|
24
|
+
from patchi.core.brain.code_query import (
|
|
25
|
+
js_calls,
|
|
26
|
+
js_identifiers,
|
|
27
|
+
js_string_literals,
|
|
28
|
+
lang_for_file,
|
|
29
|
+
parse_js,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
_log = logging.getLogger("patchi.agents.chaos")
|
|
33
|
+
|
|
34
|
+
_RACE_WORDS = ("create", "insert", "update", "upsert", "transaction")
|
|
35
|
+
_RETRY_WORDS = ("retry", "backoff", "circuit", "breaker", "tenacity", "resilience")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _py_tokens(content: str) -> tuple[set[str], set[str], list[tuple[str, int]]]:
|
|
39
|
+
"""(call names, word tokens, [(call, line)]) for Python via stdlib ast."""
|
|
40
|
+
calls: list[tuple[str, int]] = []
|
|
41
|
+
words: set[str] = set()
|
|
42
|
+
try:
|
|
43
|
+
tree = ast.parse(content)
|
|
44
|
+
except SyntaxError:
|
|
45
|
+
return set(), set(), []
|
|
46
|
+
for node in ast.walk(tree):
|
|
47
|
+
if isinstance(node, ast.Call):
|
|
48
|
+
func = node.func
|
|
49
|
+
name = func.id if isinstance(func, ast.Name) else getattr(func, "attr", "")
|
|
50
|
+
if name:
|
|
51
|
+
calls.append((name, node.lineno))
|
|
52
|
+
words.add(name.lower())
|
|
53
|
+
elif isinstance(node, ast.Name):
|
|
54
|
+
words.add(node.id.lower())
|
|
55
|
+
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
56
|
+
words.add(node.name.lower())
|
|
57
|
+
elif isinstance(node, ast.Constant) and isinstance(node.value, str):
|
|
58
|
+
for word in node.value.lower().split():
|
|
59
|
+
words.add(word.strip(".,:;!?()[]{}\"'"))
|
|
60
|
+
return {c[0] for c in calls}, words, calls
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _has_race_words(words: set[str]) -> bool:
|
|
64
|
+
return any(any(rw in w for w in words) for rw in _RACE_WORDS)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _has_retry_words(words: set[str]) -> bool:
|
|
68
|
+
return any(any(rw in w for w in words) for rw in _RETRY_WORDS)
|
|
69
|
+
|
|
70
|
+
@register
|
|
71
|
+
class ChaosAgent(BaseAgent):
|
|
72
|
+
group = AgentGroup.TEST
|
|
73
|
+
name = "ChaosAgent"
|
|
74
|
+
description = "Race duplicate requests + Chaos missing retry/backoff §5.3.3-4"
|
|
75
|
+
timeout = 60
|
|
76
|
+
|
|
77
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
78
|
+
findings=[]
|
|
79
|
+
for pat in ("*.py","*.js","*.ts"):
|
|
80
|
+
for fp in safe_rglob(inp.root, pat):
|
|
81
|
+
rel=fp.relative_to(inp.root).as_posix()
|
|
82
|
+
if "tests" in rel or "node_modules" in rel:
|
|
83
|
+
continue
|
|
84
|
+
try:
|
|
85
|
+
txt=fp.read_text(encoding="utf-8", errors="replace")
|
|
86
|
+
except OSError:
|
|
87
|
+
continue
|
|
88
|
+
if fp.suffix == ".py":
|
|
89
|
+
_, words, calls = _py_tokens(txt)
|
|
90
|
+
open_lines = [ln for n, ln in calls if n == "open"]
|
|
91
|
+
net_lines = [
|
|
92
|
+
ln for n, ln in calls if n in ("get", "post", "request", "urlopen")
|
|
93
|
+
]
|
|
94
|
+
else:
|
|
95
|
+
lang = lang_for_file(rel)
|
|
96
|
+
tree = parse_js(txt, lang)
|
|
97
|
+
if tree is None:
|
|
98
|
+
continue
|
|
99
|
+
words = {i.lower() for i in js_identifiers(tree, lang)}
|
|
100
|
+
words |= {s.lower() for s, _ in js_string_literals(tree, lang)}
|
|
101
|
+
js_call_list = js_calls(tree, lang)
|
|
102
|
+
open_lines = [c.line for c in js_call_list if c.name == "open"]
|
|
103
|
+
net_lines = [
|
|
104
|
+
c.line for c in js_call_list
|
|
105
|
+
if c.full == "fetch" or c.full.startswith("axios.")
|
|
106
|
+
]
|
|
107
|
+
# Race: file write without lock/atomic
|
|
108
|
+
if open_lines and not words & {"lock", "atomic", "mutex"}:
|
|
109
|
+
if _has_race_words(words):
|
|
110
|
+
findings.append(make_finding(
|
|
111
|
+
severity=Severity.MEDIUM, file=rel, line_start=open_lines[0],
|
|
112
|
+
title="Possible race — file write without lock",
|
|
113
|
+
description=(
|
|
114
|
+
"Concurrent requests may duplicate records; "
|
|
115
|
+
"use atomic write or DB transaction"
|
|
116
|
+
),
|
|
117
|
+
finding_type="race_file_write",
|
|
118
|
+
))
|
|
119
|
+
# Chaos: fetch without retry
|
|
120
|
+
if len(net_lines) > 2 and not _has_retry_words(words):
|
|
121
|
+
findings.append(make_finding(
|
|
122
|
+
severity=Severity.LOW, file=rel, line_start=0,
|
|
123
|
+
title="No retry/backoff on network calls",
|
|
124
|
+
description="Add retry with backoff + circuit breaker for chaos resilience",
|
|
125
|
+
finding_type="chaos_no_retry",
|
|
126
|
+
))
|
|
127
|
+
break
|
|
128
|
+
if len(findings) >= 20:
|
|
129
|
+
break
|
|
130
|
+
if len(findings) >= 20:
|
|
131
|
+
break
|
|
132
|
+
result.status=AgentStatus.SUCCEEDED
|
|
133
|
+
result.findings=findings[:20]
|