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,260 @@
|
|
|
1
|
+
"""
|
|
2
|
+
VisualRegressionAgent — Screenshot comparison testing via Playwright.
|
|
3
|
+
|
|
4
|
+
Takes screenshots at multiple viewports, waits for full render, and compares
|
|
5
|
+
them against stored baselines:
|
|
6
|
+
|
|
7
|
+
- Baselines stored in ``.patchi/visual_baselines/{route}/``
|
|
8
|
+
- Current shots saved to ``.patchi/evidence/screenshots/visual_regression/``
|
|
9
|
+
- On change, a **pixel-diff overlay** (red boxes around changed regions) is
|
|
10
|
+
generated so reviewers can see *what* moved, not just that it did.
|
|
11
|
+
- HTTP status (>=400) and console/page errors are surfaced as findings.
|
|
12
|
+
- Every finding carries the screenshot path so the UI can display evidence.
|
|
13
|
+
|
|
14
|
+
Requires: playwright, Pillow
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from ..agents.base import (
|
|
23
|
+
AgentDomain,
|
|
24
|
+
AgentGroup,
|
|
25
|
+
AgentInput,
|
|
26
|
+
AgentResult,
|
|
27
|
+
BaseAgent,
|
|
28
|
+
Severity,
|
|
29
|
+
make_finding,
|
|
30
|
+
register,
|
|
31
|
+
)
|
|
32
|
+
from ._browser import (
|
|
33
|
+
discover_routes,
|
|
34
|
+
find_server,
|
|
35
|
+
open_page,
|
|
36
|
+
save_screenshot,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
VIEWPORTS = [
|
|
40
|
+
{"width": 1440, "height": 900, "label": "desktop"},
|
|
41
|
+
{"width": 768, "height": 1024, "label": "tablet"},
|
|
42
|
+
{"width": 375, "height": 812, "label": "mobile"},
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
_log = logging.getLogger("patchi.testing.visual_regression_agent")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@register
|
|
49
|
+
class VisualRegressionAgent(BaseAgent):
|
|
50
|
+
"""Screenshot diff testing: baseline comparison, layout shifts, visual regressions."""
|
|
51
|
+
|
|
52
|
+
group = AgentGroup.TEST
|
|
53
|
+
domain = AgentDomain.VISUAL
|
|
54
|
+
name = "VisualRegressionAgent"
|
|
55
|
+
description = "Screenshot diff testing: baseline comparison, layout shifts, visual regressions"
|
|
56
|
+
timeout = 240
|
|
57
|
+
|
|
58
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
59
|
+
try:
|
|
60
|
+
from playwright.sync_api import sync_playwright # noqa: F401
|
|
61
|
+
except ImportError:
|
|
62
|
+
self.skip(result, "playwright not installed — pip install playwright && playwright install chromium")
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
base_url = find_server(inp.root, inp.config, inp.extra)
|
|
66
|
+
if not base_url:
|
|
67
|
+
self.skip(result, "no running server found — start `p web` (default :1612)")
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
routes = discover_routes(inp.config, inp.extra)
|
|
71
|
+
if not routes:
|
|
72
|
+
self.skip(result, "no routes discovered")
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
baseline_root = inp.root / ".patchi" / "visual_baselines"
|
|
76
|
+
evidence_dir = inp.root / ".patchi" / "evidence" / "screenshots" / "visual_regression"
|
|
77
|
+
evidence_dir.mkdir(parents=True, exist_ok=True)
|
|
78
|
+
|
|
79
|
+
shots = 0
|
|
80
|
+
regressions = 0
|
|
81
|
+
new_baselines = 0
|
|
82
|
+
error_pages = 0
|
|
83
|
+
|
|
84
|
+
with sync_playwright() as pw:
|
|
85
|
+
browser = pw.chromium.launch(headless=True)
|
|
86
|
+
for route in routes[:15]:
|
|
87
|
+
for vp in VIEWPORTS:
|
|
88
|
+
browser.new_context(viewport={"width": vp["width"], "height": vp["height"]})
|
|
89
|
+
page_session = open_page(browser, f"{base_url}{route}")
|
|
90
|
+
page = page_session.page
|
|
91
|
+
try:
|
|
92
|
+
slug = route.strip("/").replace("/", "_") or "root"
|
|
93
|
+
shot_name = f"{slug}_{vp['label']}"
|
|
94
|
+
shot_path = save_screenshot(page, evidence_dir, shot_name)
|
|
95
|
+
if shot_path is None:
|
|
96
|
+
continue
|
|
97
|
+
rel = str(shot_path.relative_to(inp.root))
|
|
98
|
+
shots += 1
|
|
99
|
+
|
|
100
|
+
# Surface server / runtime errors as findings + annotation
|
|
101
|
+
if page_session.status and page_session.status >= 400:
|
|
102
|
+
error_pages += 1
|
|
103
|
+
result.add_finding(
|
|
104
|
+
make_finding(
|
|
105
|
+
agent=self.name,
|
|
106
|
+
finding_type="page_error",
|
|
107
|
+
severity=Severity.HIGH,
|
|
108
|
+
file=route,
|
|
109
|
+
message=f"HTTP {page_session.status} on {route} ({vp['label']})",
|
|
110
|
+
extra={"viewport": vp["label"], "status": page_session.status,
|
|
111
|
+
"screenshot": rel},
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
for ce in page_session.console_errors[:5]:
|
|
115
|
+
result.add_finding(
|
|
116
|
+
make_finding(
|
|
117
|
+
agent=self.name,
|
|
118
|
+
finding_type="console_error",
|
|
119
|
+
severity=Severity.MEDIUM,
|
|
120
|
+
file=route,
|
|
121
|
+
message=f"Console error on {route} ({vp['label']}): {ce[:160]}",
|
|
122
|
+
extra={"viewport": vp["label"], "screenshot": rel},
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Baseline compare
|
|
127
|
+
base_shot = baseline_root / slug / f"{vp['label']}.png"
|
|
128
|
+
if not base_shot.exists():
|
|
129
|
+
base_shot.parent.mkdir(parents=True, exist_ok=True)
|
|
130
|
+
page.screenshot(path=str(base_shot), full_page=True)
|
|
131
|
+
new_baselines += 1
|
|
132
|
+
result.add_finding(
|
|
133
|
+
make_finding(
|
|
134
|
+
agent=self.name,
|
|
135
|
+
finding_type="new_baseline",
|
|
136
|
+
severity=Severity.INFO,
|
|
137
|
+
file=route,
|
|
138
|
+
message=f"New baseline created: {slug} ({vp['label']})",
|
|
139
|
+
extra={"viewport": vp["label"], "screenshot": rel,
|
|
140
|
+
"baseline": str(base_shot.relative_to(inp.root))},
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
else:
|
|
144
|
+
diff_path, changed = self._diff(base_shot, shot_path, evidence_dir, slug, vp["label"])
|
|
145
|
+
if changed:
|
|
146
|
+
regressions += 1
|
|
147
|
+
result.add_finding(
|
|
148
|
+
make_finding(
|
|
149
|
+
agent=self.name,
|
|
150
|
+
finding_type="visual_regression",
|
|
151
|
+
severity=Severity.MEDIUM,
|
|
152
|
+
file=route,
|
|
153
|
+
message=f"Visual change on {route} ({vp['label']}): {len(changed)} region(s) changed",
|
|
154
|
+
detail=f"Baseline: {base_shot.name}\nCurrent: {shot_path.name}",
|
|
155
|
+
extra={"viewport": vp["label"], "screenshot": rel,
|
|
156
|
+
"baseline": str(base_shot.relative_to(inp.root)),
|
|
157
|
+
"diff": str(diff_path.relative_to(inp.root)) if diff_path else None,
|
|
158
|
+
"regions": len(changed)},
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
# refresh baseline so next run compares against now-current
|
|
162
|
+
page.screenshot(path=str(base_shot), full_page=True)
|
|
163
|
+
finally:
|
|
164
|
+
page.close()
|
|
165
|
+
browser.close()
|
|
166
|
+
|
|
167
|
+
if regressions == 0 and error_pages == 0 and shots > 0:
|
|
168
|
+
result.add_finding(
|
|
169
|
+
make_finding(
|
|
170
|
+
agent=self.name,
|
|
171
|
+
finding_type="visual_summary",
|
|
172
|
+
severity=Severity.INFO,
|
|
173
|
+
file="(all pages)",
|
|
174
|
+
message=f"Visual regression PASSED: {shots} screenshots, 0 regressions, {new_baselines} new baselines",
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
elif shots > 0:
|
|
178
|
+
result.add_finding(
|
|
179
|
+
make_finding(
|
|
180
|
+
agent=self.name,
|
|
181
|
+
finding_type="visual_summary",
|
|
182
|
+
severity=Severity.MEDIUM,
|
|
183
|
+
file="(all pages)",
|
|
184
|
+
message=f"Visual regression: {regressions} changes, {error_pages} error pages across {shots} screenshots",
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
result.data["suite"] = {
|
|
189
|
+
"runner": "visual_regression",
|
|
190
|
+
"screenshots": shots,
|
|
191
|
+
"regressions": regressions,
|
|
192
|
+
"new_baselines": new_baselines,
|
|
193
|
+
"error_pages": error_pages,
|
|
194
|
+
}
|
|
195
|
+
result.files_scanned = shots
|
|
196
|
+
|
|
197
|
+
# ── pixel diff ──────────────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
def _diff(self, base_path: Path, curr_path: Path, out_dir: Path, slug: str, label: str):
|
|
200
|
+
"""Return (diff_image_path, changed_regions) using a downscaled pixel diff.
|
|
201
|
+
|
|
202
|
+
Requires Pillow. If Pillow is unavailable, falls back to a file-size /
|
|
203
|
+
hash comparison and reports diffing as unavailable (no crash).
|
|
204
|
+
"""
|
|
205
|
+
try:
|
|
206
|
+
from PIL import Image, ImageDraw
|
|
207
|
+
except Exception:
|
|
208
|
+
_log.warning("VisualRegressionAgent: Pillow not installed — pixel diff disabled")
|
|
209
|
+
return None, []
|
|
210
|
+
|
|
211
|
+
try:
|
|
212
|
+
base = Image.open(base_path).convert("RGB")
|
|
213
|
+
curr = Image.open(curr_path).convert("RGB")
|
|
214
|
+
except Exception as e:
|
|
215
|
+
_log.warning("VisualRegressionAgent._diff open failed: %s", e)
|
|
216
|
+
return None, []
|
|
217
|
+
|
|
218
|
+
# Downscale for speed + stable diffing (the overlay is illustrative).
|
|
219
|
+
max_w = 900
|
|
220
|
+
for im in (base, curr):
|
|
221
|
+
if im.width > max_w:
|
|
222
|
+
h = int(im.height * max_w / im.width)
|
|
223
|
+
im.thumbnail((max_w, h))
|
|
224
|
+
|
|
225
|
+
if base.size != curr.size:
|
|
226
|
+
# Layout changed dramatically — treat whole image as changed.
|
|
227
|
+
out = curr.copy()
|
|
228
|
+
d = ImageDraw.Draw(out)
|
|
229
|
+
d.rectangle([0, 0, out.width - 1, out.height - 1], outline=(255, 0, 0), width=3)
|
|
230
|
+
diff_path = out_dir / f"{slug}_{label}_diff.png"
|
|
231
|
+
out.save(str(diff_path))
|
|
232
|
+
return diff_path, [list(out.getbbox())]
|
|
233
|
+
|
|
234
|
+
bpx, cpx = base.load(), curr.load()
|
|
235
|
+
bw, bh = base.size
|
|
236
|
+
cell = 16
|
|
237
|
+
changed = []
|
|
238
|
+
for y in range(0, bh, cell):
|
|
239
|
+
for x in range(0, bw, cell):
|
|
240
|
+
diff = 0
|
|
241
|
+
n = 0
|
|
242
|
+
for yy in range(y, min(y + cell, bh)):
|
|
243
|
+
for xx in range(x, min(x + cell, bw)):
|
|
244
|
+
br, bg, bb = bpx[xx, yy]
|
|
245
|
+
cr, cg, cb = cpx[xx, yy]
|
|
246
|
+
diff += abs(br - cr) + abs(bg - cg) + abs(bb - cb)
|
|
247
|
+
n += 1
|
|
248
|
+
if n and diff / n > 55: # perceptible change in this cell
|
|
249
|
+
changed.append((x, y, min(cell, bw - x), min(cell, bh - y)))
|
|
250
|
+
|
|
251
|
+
if not changed:
|
|
252
|
+
return None, []
|
|
253
|
+
|
|
254
|
+
out = curr.copy()
|
|
255
|
+
d = ImageDraw.Draw(out)
|
|
256
|
+
for (x, y, w, h) in changed:
|
|
257
|
+
d.rectangle([x, y, x + w, y + h], outline=(255, 0, 0), width=2)
|
|
258
|
+
diff_path = out_dir / f"{slug}_{label}_diff.png"
|
|
259
|
+
out.save(str(diff_path))
|
|
260
|
+
return diff_path, [list(c) for c in changed]
|
patchi/web/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Patchi Web UI — FastAPI + htmx security dashboard."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Patchi web API endpoints."""
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Brain Map API — node and edge data for the canvas."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, Request
|
|
8
|
+
from fastapi.responses import JSONResponse
|
|
9
|
+
|
|
10
|
+
router = APIRouter(prefix="/api/brain-map")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@router.get("/nodes")
|
|
14
|
+
async def get_nodes(request: Request) -> JSONResponse:
|
|
15
|
+
root = request.app.state.root
|
|
16
|
+
from collections import Counter
|
|
17
|
+
|
|
18
|
+
from patchi.core import memory as mem
|
|
19
|
+
|
|
20
|
+
brain = mem.get_brain(root)
|
|
21
|
+
import_graph = brain.get("import_graph", {})
|
|
22
|
+
nodes_raw = import_graph.get("nodes", [])
|
|
23
|
+
health = brain.get("health_score", {})
|
|
24
|
+
dead_files = set(brain.get("dead_files", []))
|
|
25
|
+
|
|
26
|
+
# Compute finding counts per file from scan results
|
|
27
|
+
scan_results = mem.get_scan_results(root)
|
|
28
|
+
finding_counts: dict[str, int] = Counter()
|
|
29
|
+
finding_severities: dict[str, str] = {}
|
|
30
|
+
for _agent_name, data in scan_results.items():
|
|
31
|
+
for f in data.get("findings", []):
|
|
32
|
+
file_path = f.get("file", "")
|
|
33
|
+
if file_path:
|
|
34
|
+
finding_counts[file_path] += 1
|
|
35
|
+
sev = f.get("severity", "info")
|
|
36
|
+
existing = finding_severities.get(file_path)
|
|
37
|
+
sev_order = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
|
|
38
|
+
if existing is None or sev_order.get(sev, 4) < sev_order.get(existing, 4):
|
|
39
|
+
finding_severities[file_path] = sev
|
|
40
|
+
|
|
41
|
+
# Build node list with metadata
|
|
42
|
+
nodes = []
|
|
43
|
+
for _i, node_id in enumerate(nodes_raw):
|
|
44
|
+
# Determine type (maps to BrainMap node shape/color)
|
|
45
|
+
if any(x in node_id for x in ["main.py", "app.py", "server.py", "__main__.py"]):
|
|
46
|
+
node_type = "entry_point"
|
|
47
|
+
elif any(x in node_id for x in ["base.py", "coordinator.py", "brain.py", "config.py"]):
|
|
48
|
+
node_type = "component"
|
|
49
|
+
elif any(x in node_id for x in ["test_", "tests/"]):
|
|
50
|
+
node_type = "test"
|
|
51
|
+
elif any(x in node_id for x in [".json", ".yaml", ".yml", ".toml", ".env"]):
|
|
52
|
+
node_type = "config"
|
|
53
|
+
else:
|
|
54
|
+
node_type = "default"
|
|
55
|
+
|
|
56
|
+
fcount = finding_counts.get(node_id, 0)
|
|
57
|
+
fsev = finding_severities.get(node_id, "info")
|
|
58
|
+
|
|
59
|
+
# Determine status
|
|
60
|
+
if node_id in dead_files:
|
|
61
|
+
status = "dead"
|
|
62
|
+
elif fcount > 0 and fsev == "critical":
|
|
63
|
+
status = "critical"
|
|
64
|
+
elif fcount > 5:
|
|
65
|
+
status = "warning"
|
|
66
|
+
elif health.get("total", 0) < 30:
|
|
67
|
+
status = "critical"
|
|
68
|
+
elif health.get("total", 0) < 70:
|
|
69
|
+
status = "warning"
|
|
70
|
+
else:
|
|
71
|
+
status = "healthy"
|
|
72
|
+
|
|
73
|
+
# Position hint based on type
|
|
74
|
+
x = 0
|
|
75
|
+
y = 0
|
|
76
|
+
if node_type == "entry_point":
|
|
77
|
+
y = -100 # float to top
|
|
78
|
+
elif node_type == "dead":
|
|
79
|
+
y = 100 # sink to bottom
|
|
80
|
+
|
|
81
|
+
nodes.append(
|
|
82
|
+
{
|
|
83
|
+
"id": node_id,
|
|
84
|
+
"label": os.path.basename(node_id),
|
|
85
|
+
"file": node_id,
|
|
86
|
+
"type": node_type,
|
|
87
|
+
"status": status,
|
|
88
|
+
"finding_count": fcount,
|
|
89
|
+
"severity": fsev,
|
|
90
|
+
"x": x,
|
|
91
|
+
"y": y,
|
|
92
|
+
}
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return JSONResponse({"nodes": nodes})
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@router.get("/edges")
|
|
99
|
+
async def get_edges(request: Request) -> JSONResponse:
|
|
100
|
+
root = request.app.state.root
|
|
101
|
+
from patchi.core import memory as mem
|
|
102
|
+
|
|
103
|
+
brain = mem.get_brain(root)
|
|
104
|
+
import_graph = brain.get("import_graph", {})
|
|
105
|
+
edges_raw = import_graph.get("edges", {})
|
|
106
|
+
|
|
107
|
+
# Convert {source: [targets]} to [{from, to}] format
|
|
108
|
+
edges = []
|
|
109
|
+
for source, targets in edges_raw.items():
|
|
110
|
+
if isinstance(targets, list):
|
|
111
|
+
for target in targets:
|
|
112
|
+
edges.append({"from": source, "to": target, "source": source, "target": target})
|
|
113
|
+
|
|
114
|
+
return JSONResponse({"edges": edges})
|
patchi/web/api/charts.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Charts API — data for timeline and trends."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import sqlite3
|
|
8
|
+
|
|
9
|
+
from fastapi import APIRouter, Request
|
|
10
|
+
from fastapi.responses import JSONResponse
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger("patchi.web.api.charts")
|
|
13
|
+
|
|
14
|
+
router = APIRouter(prefix="/api/charts")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@router.get("/timeline")
|
|
18
|
+
async def get_timeline(request: Request) -> JSONResponse:
|
|
19
|
+
root = request.app.state.root
|
|
20
|
+
db_path = root / ".patchi" / "patchi_history.db"
|
|
21
|
+
|
|
22
|
+
history = []
|
|
23
|
+
if db_path.exists():
|
|
24
|
+
conn = None
|
|
25
|
+
try:
|
|
26
|
+
conn = sqlite3.connect(str(db_path))
|
|
27
|
+
cursor = conn.execute(
|
|
28
|
+
"SELECT severity_breakdown, findings_count FROM scan_history ORDER BY rowid DESC LIMIT 20"
|
|
29
|
+
)
|
|
30
|
+
for row in cursor.fetchall():
|
|
31
|
+
breakdown = json.loads(row[0] or "{}")
|
|
32
|
+
history.append(
|
|
33
|
+
{
|
|
34
|
+
"total": row[1],
|
|
35
|
+
"critical": breakdown.get("critical", 0),
|
|
36
|
+
"high": breakdown.get("high", 0),
|
|
37
|
+
"medium": breakdown.get("medium", 0),
|
|
38
|
+
"low": breakdown.get("low", 0),
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
except Exception as e:
|
|
42
|
+
logger.warning("Failed to fetch chart history: %s", e)
|
|
43
|
+
finally:
|
|
44
|
+
if conn:
|
|
45
|
+
conn.close()
|
|
46
|
+
|
|
47
|
+
return JSONResponse({"history": list(reversed(history))})
|
patchi/web/api/chat.py
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""Chat API — AI chat with smart brain context injection."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
from fastapi import APIRouter, Request
|
|
7
|
+
from fastapi.responses import JSONResponse
|
|
8
|
+
|
|
9
|
+
_log = logging.getLogger("patchi.web.api.chat")
|
|
10
|
+
|
|
11
|
+
router = APIRouter(prefix="/api/chat")
|
|
12
|
+
|
|
13
|
+
BASE_PROMPT = """You are Patchi, a security assistant. You help developers understand and fix security issues.
|
|
14
|
+
|
|
15
|
+
Answer concisely. If asked about specific findings, explain what the issue is and how to fix it.
|
|
16
|
+
Use markdown for formatting."""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _build_context(brain: dict, message: str) -> str:
|
|
20
|
+
"""Build relevant context from brain data based on the user's question."""
|
|
21
|
+
m_lower = message.lower()
|
|
22
|
+
parts = []
|
|
23
|
+
|
|
24
|
+
file_count = brain.get("file_count", 0)
|
|
25
|
+
route_count = brain.get("route_count", 0)
|
|
26
|
+
framework = brain.get("framework", "Unknown")
|
|
27
|
+
health = brain.get("health_score", {})
|
|
28
|
+
health_total = health.get("total", 0) if isinstance(health, dict) else health
|
|
29
|
+
parts.append(
|
|
30
|
+
f"Project: {file_count} files, {route_count} routes, framework: {framework}, health: {health_total}/100"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
if any(
|
|
34
|
+
w in m_lower
|
|
35
|
+
for w in ["finding", "issue", "vulnerability", "bug", "critical", "high", "error"]
|
|
36
|
+
):
|
|
37
|
+
issues = brain.get("issues", []) or brain.get("findings", [])
|
|
38
|
+
if issues and isinstance(issues, list):
|
|
39
|
+
by_sev: dict[str, int] = {}
|
|
40
|
+
for f in issues:
|
|
41
|
+
sev = f.get("severity", "info") if isinstance(f, dict) else "info"
|
|
42
|
+
by_sev[sev] = by_sev.get(sev, 0) + 1
|
|
43
|
+
if by_sev:
|
|
44
|
+
parts.append(
|
|
45
|
+
f"Issues ({len(issues)} total): {', '.join(f'{c} {s}' for s, c in sorted(by_sev.items()))}"
|
|
46
|
+
)
|
|
47
|
+
top = [
|
|
48
|
+
f
|
|
49
|
+
for f in issues
|
|
50
|
+
if isinstance(f, dict) and f.get("severity") in ("critical", "high")
|
|
51
|
+
][:5]
|
|
52
|
+
for f in top:
|
|
53
|
+
parts.append(
|
|
54
|
+
f" [{f['severity']}] {f.get('file', '')}:{f.get('line', 0)} — {f.get('message', '')[:80]}"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if any(w in m_lower for w in ["route", "endpoint", "api", "path"]):
|
|
58
|
+
routes = brain.get("routes", [])
|
|
59
|
+
if routes and isinstance(routes, list):
|
|
60
|
+
samples = []
|
|
61
|
+
for r in routes[:15]:
|
|
62
|
+
if isinstance(r, dict):
|
|
63
|
+
samples.append(f" {r.get('method', '?')} {r.get('path', '?')}")
|
|
64
|
+
else:
|
|
65
|
+
samples.append(f" {r}")
|
|
66
|
+
if samples:
|
|
67
|
+
parts.append(f"Routes ({len(routes)} total):\n" + "\n".join(samples[:15]))
|
|
68
|
+
|
|
69
|
+
if any(w in m_lower for w in ["security", "cve", "secret", "injection", "xss", "sqli"]):
|
|
70
|
+
sec = brain.get("security_report", brain.get("security_str", {}))
|
|
71
|
+
if isinstance(sec, dict):
|
|
72
|
+
parts.append(
|
|
73
|
+
f"Security: {sec.get('critical', 0)}c, {sec.get('high', 0)}h, {sec.get('medium', 0)}m"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if any(w in m_lower for w in ["patch", "fix", "change"]):
|
|
77
|
+
patches = brain.get("patches", [])
|
|
78
|
+
if patches and isinstance(patches, list):
|
|
79
|
+
applied = sum(
|
|
80
|
+
1
|
|
81
|
+
for p in patches
|
|
82
|
+
if isinstance(p, dict) and p.get("state") in ("applied", "auto_applied")
|
|
83
|
+
)
|
|
84
|
+
pending = sum(1 for p in patches if isinstance(p, dict) and p.get("state") == "pending")
|
|
85
|
+
parts.append(f"Patches: {applied} applied, {pending} pending")
|
|
86
|
+
|
|
87
|
+
if any(w in m_lower for w in ["test", "coverage"]):
|
|
88
|
+
tests = brain.get("test_results", {})
|
|
89
|
+
if tests:
|
|
90
|
+
parts.append(f"Tests: {tests.get('passed', 0)} passed, {tests.get('failed', 0)} failed")
|
|
91
|
+
|
|
92
|
+
return "\n".join(parts) if parts else ""
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _local_fallback(message: str, brain: dict) -> str:
|
|
96
|
+
"""Rule-based fallback when no AI provider is available."""
|
|
97
|
+
m = message.lower()
|
|
98
|
+
file_count = brain.get("file_count", 0)
|
|
99
|
+
framework = brain.get("framework", "Unknown")
|
|
100
|
+
health = brain.get("health_score", {})
|
|
101
|
+
health_total = health.get("total", 0) if isinstance(health, dict) else health
|
|
102
|
+
|
|
103
|
+
# Greeting
|
|
104
|
+
if any(w in m for w in ["hello", "hi", "hey", "greetings"]):
|
|
105
|
+
return (
|
|
106
|
+
f"Hi! I'm Patchi. This project has {file_count} files, "
|
|
107
|
+
f"uses {framework}, and has a health score of {health_total}/100. "
|
|
108
|
+
f"No AI provider is configured — run `p key add` for full AI chat."
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Project overview
|
|
112
|
+
if any(w in m for w in ["project", "about", "what is", "overview"]):
|
|
113
|
+
purpose = brain.get("project_purpose", "")
|
|
114
|
+
parts = [f"This is a {framework} project with {file_count} files."]
|
|
115
|
+
if purpose:
|
|
116
|
+
parts.append(f"Purpose: {purpose}")
|
|
117
|
+
parts.append(f"Health score: {health_total}/100.")
|
|
118
|
+
routes = brain.get("routes", [])
|
|
119
|
+
if routes:
|
|
120
|
+
parts.append(f"{len(routes)} routes detected.")
|
|
121
|
+
return " ".join(parts)
|
|
122
|
+
|
|
123
|
+
# Findings / security
|
|
124
|
+
if any(w in m for w in ["finding", "issue", "vulnerability", "security", "critical", "bug"]):
|
|
125
|
+
scans = brain.get("scan_results", {})
|
|
126
|
+
if not scans:
|
|
127
|
+
from pathlib import Path
|
|
128
|
+
|
|
129
|
+
from patchi.core import memory as mem
|
|
130
|
+
try:
|
|
131
|
+
scans = mem.get_scan_results(Path(brain.get("_root", ".")))
|
|
132
|
+
except Exception:
|
|
133
|
+
scans = {}
|
|
134
|
+
total = sum(len(v.get("findings", [])) for v in scans.values()) if scans else 0
|
|
135
|
+
if total:
|
|
136
|
+
return (
|
|
137
|
+
f"Found {total} findings across {len(scans)} scanners. "
|
|
138
|
+
f"Run `p findings` in the CLI for details, or configure an AI provider "
|
|
139
|
+
f"with `p key add` for AI-powered analysis."
|
|
140
|
+
)
|
|
141
|
+
return "No scan results yet. Run `p scan` first to detect findings."
|
|
142
|
+
|
|
143
|
+
# Health
|
|
144
|
+
if any(w in m for w in ["health", "score", "grade"]):
|
|
145
|
+
grade = health.get("grade", "?") if isinstance(health, dict) else "?"
|
|
146
|
+
return (
|
|
147
|
+
f"Health score: {health_total}/100 (grade {grade}). "
|
|
148
|
+
f"{file_count} files, {framework} framework. "
|
|
149
|
+
f"Run `p doctor` for a full health check."
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Routes
|
|
153
|
+
if any(w in m for w in ["route", "endpoint", "api", "path"]):
|
|
154
|
+
routes = brain.get("routes", [])
|
|
155
|
+
if routes:
|
|
156
|
+
samples = [f" {r.get('method', '?')} {r.get('path', '?')}" for r in routes[:10] if isinstance(r, dict)]
|
|
157
|
+
return f"{len(routes)} routes detected:\n" + "\n".join(samples)
|
|
158
|
+
return "No routes detected yet. Run `p scan` to discover routes."
|
|
159
|
+
|
|
160
|
+
# Help
|
|
161
|
+
if any(w in m for w in ["help", "how", "what can"]):
|
|
162
|
+
return (
|
|
163
|
+
"I can help with:\n"
|
|
164
|
+
"• **Findings** — ask about security issues and vulnerabilities\n"
|
|
165
|
+
"• **Project** — overview of your codebase\n"
|
|
166
|
+
"• **Health** — project health score and grade\n"
|
|
167
|
+
"• **Routes** — API endpoints and paths\n"
|
|
168
|
+
"\nFor AI-powered analysis, run `p key add` to configure an AI provider."
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
f"I'm Patchi (local mode — no AI provider configured). "
|
|
173
|
+
f"This {framework} project has {file_count} files with health score {health_total}/100. "
|
|
174
|
+
f"Ask about findings, project, health, or routes. "
|
|
175
|
+
f"Run `p key add` for full AI chat."
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@router.post("")
|
|
180
|
+
async def chat_message(request: Request) -> JSONResponse:
|
|
181
|
+
body = await request.json()
|
|
182
|
+
message = body.get("message", "")
|
|
183
|
+
|
|
184
|
+
if not message:
|
|
185
|
+
return JSONResponse({"ok": False, "error": "No message"}, status_code=400)
|
|
186
|
+
|
|
187
|
+
root = request.app.state.root
|
|
188
|
+
from patchi.core import config as cfg
|
|
189
|
+
from patchi.core import memory as mem
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
config = cfg.load(root)
|
|
193
|
+
except Exception:
|
|
194
|
+
config = {}
|
|
195
|
+
|
|
196
|
+
brain = mem.get_brain(root)
|
|
197
|
+
|
|
198
|
+
context = _build_context(brain, message)
|
|
199
|
+
system = f"{BASE_PROMPT}\n\n{context}" if context else BASE_PROMPT
|
|
200
|
+
|
|
201
|
+
# Skip AI call if no real providers configured — use instant local fallback.
|
|
202
|
+
# AI Horde is too slow for chat; only use it with a custom key, not the anon fallback.
|
|
203
|
+
ai_cfg = config.get("ai", {})
|
|
204
|
+
ai_keys = ai_cfg.get("keys", [])
|
|
205
|
+
has_provider = (
|
|
206
|
+
ai_keys
|
|
207
|
+
or ai_cfg.get("local_model_name")
|
|
208
|
+
)
|
|
209
|
+
if has_provider:
|
|
210
|
+
try:
|
|
211
|
+
from patchi.core.ai.client import call_ai
|
|
212
|
+
|
|
213
|
+
response = call_ai(
|
|
214
|
+
config, system, message,
|
|
215
|
+
max_tokens=1000, timeout=25,
|
|
216
|
+
)
|
|
217
|
+
if response:
|
|
218
|
+
return JSONResponse({"ok": True, "response": response})
|
|
219
|
+
except Exception as e:
|
|
220
|
+
_log.debug("AI call failed: %s", e)
|
|
221
|
+
|
|
222
|
+
# Local fallback — rule-based responses when no AI provider is available
|
|
223
|
+
fallback = _local_fallback(message, brain)
|
|
224
|
+
return JSONResponse({"ok": True, "response": fallback})
|