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,156 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hosted Webhooks — Outbound alert forwarding for Patchi hosted mode.
|
|
3
|
+
|
|
4
|
+
Stores webhook subscriptions in .patchi/hosted/webhooks.json and delivers
|
|
5
|
+
finding/anomaly alerts via signed POST requests. Delivery failures are
|
|
6
|
+
logged to the audit log; repeated failures auto-disable a webhook.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import hmac
|
|
13
|
+
import json
|
|
14
|
+
import logging
|
|
15
|
+
import time
|
|
16
|
+
import uuid
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
_log = logging.getLogger("patchi.hosted.webhooks")
|
|
20
|
+
|
|
21
|
+
MAX_FAILURES = 5 # consecutive failures before auto-disable
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _path(root: Path) -> Path:
|
|
25
|
+
return root / ".patchi" / "hosted" / "webhooks.json"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _load(root: Path) -> list[dict]:
|
|
29
|
+
p = _path(root)
|
|
30
|
+
if not p.exists():
|
|
31
|
+
return []
|
|
32
|
+
try:
|
|
33
|
+
return json.loads(p.read_text(encoding="utf-8"))
|
|
34
|
+
except Exception as e:
|
|
35
|
+
_log.warning("webhook load failed: %s", e)
|
|
36
|
+
return []
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _save(root: Path, hooks: list[dict]) -> None:
|
|
40
|
+
p = _path(root)
|
|
41
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
42
|
+
tmp = p.with_suffix(".tmp")
|
|
43
|
+
tmp.write_text(json.dumps(hooks, indent=2), encoding="utf-8")
|
|
44
|
+
tmp.replace(p)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def list_webhooks(root: Path) -> list[dict]:
|
|
48
|
+
"""All webhooks with secret redacted."""
|
|
49
|
+
out = []
|
|
50
|
+
for h in _load(root):
|
|
51
|
+
out.append(
|
|
52
|
+
{k: v for k, v in h.items() if k != "secret"} | {"has_secret": bool(h.get("secret"))}
|
|
53
|
+
)
|
|
54
|
+
return out
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def add_webhook(
|
|
58
|
+
root: Path, url: str, events: list[str] | None = None, name: str = "", secret: str = ""
|
|
59
|
+
) -> dict:
|
|
60
|
+
"""Register a webhook. Returns its record (with id)."""
|
|
61
|
+
hooks = _load(root)
|
|
62
|
+
record = {
|
|
63
|
+
"id": uuid.uuid4().hex[:10],
|
|
64
|
+
"url": url,
|
|
65
|
+
"events": events or ["finding", "anomaly", "scan_completed"],
|
|
66
|
+
"name": name or url[:40],
|
|
67
|
+
"secret": secret,
|
|
68
|
+
"enabled": True,
|
|
69
|
+
"consecutive_failures": 0,
|
|
70
|
+
"last_delivery": None,
|
|
71
|
+
"created_at": time.time(),
|
|
72
|
+
}
|
|
73
|
+
hooks.append(record)
|
|
74
|
+
_save(root, hooks)
|
|
75
|
+
return {k: v for k, v in record.items() if k != "secret"}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def remove_webhook(root: Path, hook_id: str) -> bool:
|
|
79
|
+
hooks = _load(root)
|
|
80
|
+
before = len(hooks)
|
|
81
|
+
hooks = [h for h in hooks if h.get("id") != hook_id]
|
|
82
|
+
if len(hooks) < before:
|
|
83
|
+
_save(root, hooks)
|
|
84
|
+
return True
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _sign(payload_bytes: bytes, secret: str) -> str:
|
|
89
|
+
return hmac.new(secret.encode("utf-8"), payload_bytes, hashlib.sha256).hexdigest()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def dispatch_event(root: Path, event: str, data: dict) -> int:
|
|
93
|
+
"""Fan an event out to matching enabled webhooks. Returns delivery count.
|
|
94
|
+
|
|
95
|
+
Never raises — delivery issues are logged and counted per-hook.
|
|
96
|
+
"""
|
|
97
|
+
delivered = 0
|
|
98
|
+
hooks = _load(root)
|
|
99
|
+
dirty = False
|
|
100
|
+
|
|
101
|
+
for h in hooks:
|
|
102
|
+
if not h.get("enabled"):
|
|
103
|
+
continue
|
|
104
|
+
if event not in h.get("events", []) and "*" not in h.get("events", []):
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
body = json.dumps({"event": event, "data": data, "ts": time.time()}).encode("utf-8")
|
|
108
|
+
headers = {"Content-Type": "application/json"}
|
|
109
|
+
if h.get("secret"):
|
|
110
|
+
headers["X-Patchi-Signature"] = _sign(body, h["secret"])
|
|
111
|
+
|
|
112
|
+
ok = False
|
|
113
|
+
try:
|
|
114
|
+
import urllib.request
|
|
115
|
+
|
|
116
|
+
req = urllib.request.Request(h["url"], data=body, headers=headers, method="POST")
|
|
117
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
118
|
+
ok = 200 <= resp.status < 300
|
|
119
|
+
except Exception as e:
|
|
120
|
+
_log.debug("webhook %s delivery failed: %s", h["id"], e)
|
|
121
|
+
|
|
122
|
+
if ok:
|
|
123
|
+
h["consecutive_failures"] = 0
|
|
124
|
+
h["last_delivery"] = time.time()
|
|
125
|
+
delivered += 1
|
|
126
|
+
else:
|
|
127
|
+
h["consecutive_failures"] = h.get("consecutive_failures", 0) + 1
|
|
128
|
+
if h["consecutive_failures"] >= MAX_FAILURES:
|
|
129
|
+
h["enabled"] = False
|
|
130
|
+
try:
|
|
131
|
+
from patchi.core.hosted.audit_log import write as audit_write
|
|
132
|
+
|
|
133
|
+
audit_write(
|
|
134
|
+
root,
|
|
135
|
+
"webhook.disabled",
|
|
136
|
+
data={
|
|
137
|
+
"id": h["id"],
|
|
138
|
+
"reason": f"{MAX_FAILURES} consecutive delivery failures",
|
|
139
|
+
},
|
|
140
|
+
)
|
|
141
|
+
except Exception as _exc:
|
|
142
|
+
_log.warning('dispatch_event failed: %s', _exc)
|
|
143
|
+
dirty = True
|
|
144
|
+
|
|
145
|
+
if dirty:
|
|
146
|
+
_save(root, hooks)
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
from patchi.core.hosted.audit_log import write as audit_write
|
|
150
|
+
|
|
151
|
+
if delivered:
|
|
152
|
+
audit_write(root, "webhook.delivered", data={"event": event, "count": delivered})
|
|
153
|
+
except Exception as _exc:
|
|
154
|
+
_log.warning('dispatch_event failed: %s', _exc)
|
|
155
|
+
|
|
156
|
+
return delivered
|
patchi/core/memory.py
ADDED
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Memory system for Patchi.
|
|
3
|
+
|
|
4
|
+
All persistent data outside config lives here. Organised JSON files,
|
|
5
|
+
one per category. Human-readable. Viewable and deletable from CLI and web UI.
|
|
6
|
+
|
|
7
|
+
Rules:
|
|
8
|
+
- Every write is atomic (write to temp, rename to target).
|
|
9
|
+
- Deleting any category triggers a rescan of that area on next run.
|
|
10
|
+
- Callers never read the JSON files directly — always go through these functions.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
|
|
15
|
+
# ── Low-level read/write ───────────────────────────────────────────────────────
|
|
16
|
+
import logging
|
|
17
|
+
import threading
|
|
18
|
+
import time
|
|
19
|
+
import uuid
|
|
20
|
+
from datetime import UTC
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from patchi.core.atomic import atomic_replace
|
|
25
|
+
from patchi.core.config import require_project_root
|
|
26
|
+
from patchi.core.constants import (
|
|
27
|
+
MEMORY_FILES,
|
|
28
|
+
MemoryCategory,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
_log = logging.getLogger("patchi.core.memory")
|
|
32
|
+
|
|
33
|
+
# Serializes read-modify-write ops (e.g. save_scan_result) that the Coordinator
|
|
34
|
+
# calls from parallel agent threads — without it, concurrent writers race on
|
|
35
|
+
# the same .tmp file (PermissionError/FileNotFoundError on Windows).
|
|
36
|
+
_WRITE_LOCK = threading.RLock()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _mem_path(category: MemoryCategory, root: Path) -> Path:
|
|
40
|
+
return root / MEMORY_FILES[category]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _read(category: MemoryCategory, root: Path) -> Any:
|
|
44
|
+
path = _mem_path(category, root)
|
|
45
|
+
if not path.exists():
|
|
46
|
+
return [] if category in _list_categories() else {}
|
|
47
|
+
with path.open("r", encoding="utf-8") as f:
|
|
48
|
+
return json.load(f)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _write(category: MemoryCategory, data: Any, root: Path) -> None:
|
|
52
|
+
with _WRITE_LOCK:
|
|
53
|
+
path = _mem_path(category, root)
|
|
54
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
tmp = path.with_suffix(".tmp")
|
|
56
|
+
with tmp.open("w", encoding="utf-8") as f:
|
|
57
|
+
json.dump(data, f, indent=2)
|
|
58
|
+
atomic_replace(tmp, path)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _update(category: MemoryCategory, fn: Any, root: Path) -> Any:
|
|
62
|
+
"""Thread-safe read-modify-write: lock, read, mutate via fn, write back."""
|
|
63
|
+
r = _root(root)
|
|
64
|
+
with _WRITE_LOCK:
|
|
65
|
+
data = _read(category, r)
|
|
66
|
+
result = fn(data)
|
|
67
|
+
_write(category, result, r)
|
|
68
|
+
return result
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _list_categories() -> set[MemoryCategory]:
|
|
72
|
+
return {
|
|
73
|
+
MemoryCategory.PATCHES,
|
|
74
|
+
MemoryCategory.FAILED,
|
|
75
|
+
MemoryCategory.ISSUES,
|
|
76
|
+
MemoryCategory.RESTRICTIONS,
|
|
77
|
+
MemoryCategory.TOKENS,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _root(root: Path | None) -> Path:
|
|
82
|
+
return root or require_project_root()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ── Generic read / delete ──────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def read(category: MemoryCategory, root: Path | None = None) -> Any:
|
|
89
|
+
"""Return all memory data for a category."""
|
|
90
|
+
return _read(category, _root(root))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def delete(category: MemoryCategory, root: Path | None = None) -> None:
|
|
94
|
+
"""
|
|
95
|
+
Clear all memory for a category.
|
|
96
|
+
Resets to empty list or dict (not deleting the file itself,
|
|
97
|
+
so Patchi knows to re-scan on next run).
|
|
98
|
+
"""
|
|
99
|
+
r = _root(root)
|
|
100
|
+
empty: Any = [] if category in _list_categories() else {}
|
|
101
|
+
_write(category, empty, r)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def delete_all(root: Path | None = None) -> None:
|
|
105
|
+
"""Wipe everything. Called after user types CONFIRM."""
|
|
106
|
+
r = _root(root)
|
|
107
|
+
for category in MemoryCategory:
|
|
108
|
+
delete(category, r)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def summary(root: Path | None = None) -> dict[str, Any]:
|
|
112
|
+
"""High-level counts per category — used by `p memory` and Overview panel."""
|
|
113
|
+
r = _root(root)
|
|
114
|
+
out: dict[str, Any] = {}
|
|
115
|
+
for category in MemoryCategory:
|
|
116
|
+
data = _read(category, r)
|
|
117
|
+
if isinstance(data, list):
|
|
118
|
+
out[category.value] = {"count": len(data)}
|
|
119
|
+
elif isinstance(data, dict):
|
|
120
|
+
out[category.value] = {"keys": list(data.keys())}
|
|
121
|
+
else:
|
|
122
|
+
out[category.value] = {}
|
|
123
|
+
return out
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# ── Brain ──────────────────────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def get_brain(root: Path | None = None) -> dict:
|
|
130
|
+
return _read(MemoryCategory.BRAIN, _root(root))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def save_brain(brain: dict, root: Path | None = None) -> None:
|
|
134
|
+
_write(MemoryCategory.BRAIN, brain, _root(root))
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# ── Layered brain ─────────────────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def get_layers(root: Path | None = None) -> dict:
|
|
141
|
+
return _read(MemoryCategory.LAYERS, _root(root))
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def save_layers(layers: dict, root: Path | None = None) -> None:
|
|
145
|
+
_write(MemoryCategory.LAYERS, layers, _root(root))
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# ── Project charter (guard rails) ─────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def get_charter(root: Path | None = None) -> dict:
|
|
152
|
+
return _read(MemoryCategory.CHARTER, _root(root))
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def save_charter(charter: dict, root: Path | None = None) -> None:
|
|
156
|
+
_write(MemoryCategory.CHARTER, charter, _root(root))
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def mark_brain_stale(
|
|
160
|
+
changed_files: list[str],
|
|
161
|
+
root: Path | None = None,
|
|
162
|
+
stale_layers: list[str] | None = None,
|
|
163
|
+
) -> None:
|
|
164
|
+
r = _root(root)
|
|
165
|
+
brain = _read(MemoryCategory.BRAIN, r)
|
|
166
|
+
brain["stale"] = True
|
|
167
|
+
brain["stale_reason"] = f"{len(changed_files)} file(s) changed"
|
|
168
|
+
brain["changed_files"] = changed_files
|
|
169
|
+
if stale_layers is not None:
|
|
170
|
+
brain["stale_layers"] = sorted(stale_layers)
|
|
171
|
+
_write(MemoryCategory.BRAIN, brain, r)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# ── Patches ────────────────────────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def list_patches(root: Path | None = None) -> list[dict]:
|
|
178
|
+
return _read(MemoryCategory.PATCHES, _root(root))
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def save_patch(patch: dict, root: Path | None = None) -> str:
|
|
182
|
+
"""Append a patch record. Returns the patch ID."""
|
|
183
|
+
r = _root(root)
|
|
184
|
+
patches = _read(MemoryCategory.PATCHES, r)
|
|
185
|
+
if "id" not in patch:
|
|
186
|
+
patch["id"] = str(uuid.uuid4())[:8]
|
|
187
|
+
patch.setdefault("timestamp", _now())
|
|
188
|
+
patches.append(patch)
|
|
189
|
+
_write(MemoryCategory.PATCHES, patches, r)
|
|
190
|
+
return patch["id"]
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def get_patch(patch_id: str, root: Path | None = None) -> dict | None:
|
|
194
|
+
for p in list_patches(root):
|
|
195
|
+
if p.get("id") == patch_id:
|
|
196
|
+
return p
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def delete_patches(root: Path | None = None) -> None:
|
|
201
|
+
delete(MemoryCategory.PATCHES, root)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# ── Failed patches ─────────────────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def list_failed(root: Path | None = None) -> list[dict]:
|
|
208
|
+
return _read(MemoryCategory.FAILED, _root(root))
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def save_failed(patch_id: str, reason: str, diff: str = "", root: Path | None = None) -> None:
|
|
212
|
+
r = _root(root)
|
|
213
|
+
failed = _read(MemoryCategory.FAILED, r)
|
|
214
|
+
failed.append(
|
|
215
|
+
{
|
|
216
|
+
"id": patch_id,
|
|
217
|
+
"reason": reason,
|
|
218
|
+
"diff": diff,
|
|
219
|
+
"timestamp": _now(),
|
|
220
|
+
}
|
|
221
|
+
)
|
|
222
|
+
_write(MemoryCategory.FAILED, failed, r)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def delete_failed(root: Path | None = None) -> None:
|
|
226
|
+
delete(MemoryCategory.FAILED, root)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# ── Scan results ───────────────────────────────────────────────────────────────
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def get_scan_results(root: Path | None = None) -> dict:
|
|
233
|
+
return _read(MemoryCategory.SCANS, _root(root))
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def save_scan_result(scanner_name: str, result: dict, root: Path | None = None) -> None:
|
|
237
|
+
def _mutate(scans: dict) -> dict:
|
|
238
|
+
scans[scanner_name] = {**result, "timestamp": _now()}
|
|
239
|
+
return scans
|
|
240
|
+
|
|
241
|
+
_update(MemoryCategory.SCANS, _mutate, root)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
# ── Scan history ──────────────────────────────────────────────────────────────
|
|
245
|
+
SCAN_HISTORY_FILE = ".patchi/scan_history.json"
|
|
246
|
+
SCAN_HISTORY_MAX = 50 # keep last 50 scans
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def get_scan_history(root: Path | None = None) -> list[dict]:
|
|
250
|
+
"""Return the list of past scan summaries, newest first."""
|
|
251
|
+
r = _root(root)
|
|
252
|
+
path = r / SCAN_HISTORY_FILE
|
|
253
|
+
if not path.exists():
|
|
254
|
+
return []
|
|
255
|
+
try:
|
|
256
|
+
with path.open("r", encoding="utf-8") as f:
|
|
257
|
+
data = json.load(f)
|
|
258
|
+
return data if isinstance(data, list) else []
|
|
259
|
+
except Exception:
|
|
260
|
+
return []
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def record_scan(summary: dict, root: Path | None = None) -> None:
|
|
264
|
+
"""Append a scan summary to history. Keeps only the last SCAN_HISTORY_MAX entries."""
|
|
265
|
+
r = _root(root)
|
|
266
|
+
path = r / SCAN_HISTORY_FILE
|
|
267
|
+
with _WRITE_LOCK:
|
|
268
|
+
r.mkdir(parents=True, exist_ok=True)
|
|
269
|
+
history = []
|
|
270
|
+
if path.exists():
|
|
271
|
+
try:
|
|
272
|
+
with path.open("r", encoding="utf-8") as f:
|
|
273
|
+
history = json.load(f)
|
|
274
|
+
except Exception:
|
|
275
|
+
history = []
|
|
276
|
+
if not isinstance(history, list):
|
|
277
|
+
history = []
|
|
278
|
+
history.insert(0, summary)
|
|
279
|
+
history = history[:SCAN_HISTORY_MAX]
|
|
280
|
+
tmp = path.with_suffix(".tmp")
|
|
281
|
+
with tmp.open("w", encoding="utf-8") as f:
|
|
282
|
+
json.dump(history, f, indent=2, default=str)
|
|
283
|
+
atomic_replace(tmp, path)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
# ── Known issues ───────────────────────────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def list_issues(root: Path | None = None) -> list[dict]:
|
|
290
|
+
return _read(MemoryCategory.ISSUES, _root(root))
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def save_issue(issue: dict, root: Path | None = None) -> None:
|
|
294
|
+
r = _root(root)
|
|
295
|
+
issues = _read(MemoryCategory.ISSUES, r)
|
|
296
|
+
issue.setdefault("id", str(uuid.uuid4())[:8])
|
|
297
|
+
issue.setdefault("timestamp", _now())
|
|
298
|
+
issues.append(issue)
|
|
299
|
+
_write(MemoryCategory.ISSUES, issues, r)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def resolve_issue(issue_id: str, root: Path | None = None) -> bool:
|
|
303
|
+
r = _root(root)
|
|
304
|
+
issues = _read(MemoryCategory.ISSUES, r)
|
|
305
|
+
before = len(issues)
|
|
306
|
+
issues = [i for i in issues if i.get("id") != issue_id]
|
|
307
|
+
_write(MemoryCategory.ISSUES, issues, r)
|
|
308
|
+
return len(issues) < before
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def delete_issues(root: Path | None = None) -> None:
|
|
312
|
+
delete(MemoryCategory.ISSUES, root)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# ── Dev tokens (names only, not values) ───────────────────────────────────────
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def list_tokens(root: Path | None = None) -> list[dict]:
|
|
319
|
+
return _read(MemoryCategory.TOKENS, _root(root))
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def save_token(name: str, env_var: str, root: Path | None = None) -> None:
|
|
323
|
+
r = _root(root)
|
|
324
|
+
tokens = _read(MemoryCategory.TOKENS, r)
|
|
325
|
+
if not any(t["name"] == name for t in tokens):
|
|
326
|
+
tokens.append({"name": name, "env_var": env_var, "added": _now()})
|
|
327
|
+
_write(MemoryCategory.TOKENS, tokens, r)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def remove_token(name: str, root: Path | None = None) -> bool:
|
|
331
|
+
r = _root(root)
|
|
332
|
+
tokens = _read(MemoryCategory.TOKENS, r)
|
|
333
|
+
before = len(tokens)
|
|
334
|
+
tokens = [t for t in tokens if t["name"] != name]
|
|
335
|
+
_write(MemoryCategory.TOKENS, tokens, r)
|
|
336
|
+
return len(tokens) < before
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
# ── Helpers ────────────────────────────────────────────────────────────────────
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _now() -> str:
|
|
343
|
+
from datetime import datetime
|
|
344
|
+
|
|
345
|
+
return datetime.now(UTC).isoformat()
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def clear_all(root: "Path | None" = None) -> None:
|
|
349
|
+
"""Wipe every memory category. Irreversible."""
|
|
350
|
+
try:
|
|
351
|
+
r = root or require_project_root()
|
|
352
|
+
mem_dir = r / _PATCHI_DIR
|
|
353
|
+
for f in mem_dir.glob("*.json"):
|
|
354
|
+
if f.name != "config.json":
|
|
355
|
+
tmp = f.with_suffix(".tmp")
|
|
356
|
+
with tmp.open("w", encoding="utf-8") as fh:
|
|
357
|
+
json.dump({}, fh)
|
|
358
|
+
atomic_replace(tmp, f)
|
|
359
|
+
except Exception as e:
|
|
360
|
+
_log.warning("clear_all failed: %s", e)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
# ── Health score history ───────────────────────────────────────────────────────
|
|
364
|
+
|
|
365
|
+
_PATCHI_DIR = ".patchi"
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def log_health_score(score: int, finding_count: int, root: "Path | None" = None) -> None:
|
|
369
|
+
"""Append a health score data point to history.json after every scan."""
|
|
370
|
+
r = root or require_project_root()
|
|
371
|
+
path = r / _PATCHI_DIR / "history.json"
|
|
372
|
+
try:
|
|
373
|
+
history: list[dict] = json.loads(path.read_text(encoding="utf-8")) if path.exists() else []
|
|
374
|
+
except Exception as e:
|
|
375
|
+
_log.warning("log_health_score failed: %s", e)
|
|
376
|
+
history = []
|
|
377
|
+
|
|
378
|
+
history.append(
|
|
379
|
+
{
|
|
380
|
+
"ts": int(time.time()),
|
|
381
|
+
"health_score": score,
|
|
382
|
+
"finding_count": finding_count,
|
|
383
|
+
"type": "scan",
|
|
384
|
+
}
|
|
385
|
+
)
|
|
386
|
+
# Keep last 500 entries
|
|
387
|
+
history = history[-500:]
|
|
388
|
+
# Atomic write: tmp + rename
|
|
389
|
+
tmp = path.with_suffix(".tmp")
|
|
390
|
+
tmp.write_text(json.dumps(history, indent=2), encoding="utf-8")
|
|
391
|
+
atomic_replace(tmp, path)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def get_health_history(root: "Path | None" = None) -> list[dict]:
|
|
395
|
+
"""Return all health score history entries."""
|
|
396
|
+
r = root or require_project_root()
|
|
397
|
+
path = r / _PATCHI_DIR / "history.json"
|
|
398
|
+
try:
|
|
399
|
+
return json.loads(path.read_text(encoding="utf-8")) if path.exists() else []
|
|
400
|
+
except Exception as e:
|
|
401
|
+
_log.warning("get_health_history failed: %s", e)
|
|
402
|
+
return []
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
# ── Rejection learning ─────────────────────────────────────────────────────────
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def record_rejection(finding_type: str, root: "Path | None" = None) -> int:
|
|
409
|
+
"""
|
|
410
|
+
Track how many times the user has rejected a finding type.
|
|
411
|
+
Returns the new rejection count for that type.
|
|
412
|
+
"""
|
|
413
|
+
r = root or require_project_root()
|
|
414
|
+
path = r / _PATCHI_DIR / "rejection_counts.json"
|
|
415
|
+
try:
|
|
416
|
+
counts: dict = json.loads(path.read_text(encoding="utf-8")) if path.exists() else {}
|
|
417
|
+
except Exception as e:
|
|
418
|
+
_log.warning("record_rejection failed: %s", e)
|
|
419
|
+
counts = {}
|
|
420
|
+
|
|
421
|
+
counts[finding_type] = counts.get(finding_type, 0) + 1
|
|
422
|
+
# Atomic write: tmp + rename
|
|
423
|
+
tmp = path.with_suffix(".tmp")
|
|
424
|
+
tmp.write_text(json.dumps(counts, indent=2), encoding="utf-8")
|
|
425
|
+
atomic_replace(tmp, path)
|
|
426
|
+
return counts[finding_type]
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def get_rejection_counts(root: "Path | None" = None) -> dict[str, int]:
|
|
430
|
+
"""Return all rejection counts by finding type."""
|
|
431
|
+
r = root or require_project_root()
|
|
432
|
+
path = r / _PATCHI_DIR / "rejection_counts.json"
|
|
433
|
+
try:
|
|
434
|
+
return json.loads(path.read_text(encoding="utf-8")) if path.exists() else {}
|
|
435
|
+
except Exception as e:
|
|
436
|
+
_log.warning("get_rejection_counts failed: %s", e)
|
|
437
|
+
return {}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Patchi notification system — public API."""
|
|
2
|
+
|
|
3
|
+
from patchi.core.notifications.channels import ChannelConfig, ChannelType, load_channels
|
|
4
|
+
from patchi.core.notifications.digest import DigestQueue
|
|
5
|
+
from patchi.core.notifications.escalation import EscalationTracker
|
|
6
|
+
from patchi.core.notifications.notifier import Notifier
|
|
7
|
+
from patchi.core.notifications.quiet_hours import is_quiet_now, should_suppress
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"Notifier",
|
|
11
|
+
"ChannelConfig",
|
|
12
|
+
"ChannelType",
|
|
13
|
+
"load_channels",
|
|
14
|
+
"DigestQueue",
|
|
15
|
+
"EscalationTracker",
|
|
16
|
+
"should_suppress",
|
|
17
|
+
"is_quiet_now",
|
|
18
|
+
]
|