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
patchi/core/queue.py
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Queue system for Patchi.
|
|
3
|
+
|
|
4
|
+
All tasks go through the queue. Nothing runs immediately.
|
|
5
|
+
Queue state persists to .patchi/queue.json between CLI invocations.
|
|
6
|
+
|
|
7
|
+
Three modes:
|
|
8
|
+
single — one task at a time, safest for low-end hardware
|
|
9
|
+
multi — parallel up to hardware-detected limit
|
|
10
|
+
off — no queue, tasks run immediately (power user mode)
|
|
11
|
+
|
|
12
|
+
The queue stores task definitions as JSON records.
|
|
13
|
+
Actual task execution is wired up in Agent System.
|
|
14
|
+
The queue here is the data layer + control layer — fully functional.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import platform
|
|
19
|
+
import uuid
|
|
20
|
+
from contextlib import contextmanager
|
|
21
|
+
from datetime import UTC, datetime
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from patchi.core.config import require_project_root
|
|
26
|
+
from patchi.core.constants import (
|
|
27
|
+
QUEUE_FILE,
|
|
28
|
+
QUEUE_MAX_DEPTH,
|
|
29
|
+
QUEUE_WARN_DEPTH,
|
|
30
|
+
QueueItemState,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# ── Cross-platform file lock ──────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@contextmanager
|
|
37
|
+
def _file_lock(path: Path):
|
|
38
|
+
"""Simple cross-platform file lock using lock file + atomic create."""
|
|
39
|
+
lock_path = path.with_suffix(".lock")
|
|
40
|
+
lock_path.parent.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
# Ensure lock file exists before opening
|
|
42
|
+
lock_path.touch(exist_ok=True)
|
|
43
|
+
|
|
44
|
+
if platform.system() == "Windows":
|
|
45
|
+
# Windows: use msvcrt for file locking
|
|
46
|
+
import msvcrt
|
|
47
|
+
|
|
48
|
+
f = open(lock_path, "r+")
|
|
49
|
+
try:
|
|
50
|
+
msvcrt.locking(f.fileno(), msvcrt.LK_LOCK, 1)
|
|
51
|
+
yield
|
|
52
|
+
finally:
|
|
53
|
+
try:
|
|
54
|
+
msvcrt.locking(f.fileno(), msvcrt.LK_UNLCK, 1)
|
|
55
|
+
except OSError:
|
|
56
|
+
pass
|
|
57
|
+
f.close()
|
|
58
|
+
else:
|
|
59
|
+
# Unix: use fcntl
|
|
60
|
+
import fcntl
|
|
61
|
+
|
|
62
|
+
f = open(lock_path, "r+")
|
|
63
|
+
try:
|
|
64
|
+
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
|
|
65
|
+
yield
|
|
66
|
+
finally:
|
|
67
|
+
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
|
|
68
|
+
f.close()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ── Data model ─────────────────────────────────────────────────────────────────
|
|
72
|
+
#
|
|
73
|
+
# Queue file structure:
|
|
74
|
+
# {
|
|
75
|
+
# "paused": false,
|
|
76
|
+
# "items": [
|
|
77
|
+
# {
|
|
78
|
+
# "id": "a1b2c3",
|
|
79
|
+
# "type": "scan | fix | test | security | ...",
|
|
80
|
+
# "target": "full | src/auth/ | ...",
|
|
81
|
+
# "state": "waiting | active | done | failed | skipped",
|
|
82
|
+
# "created": "2025-01-01T00:00:00+00:00",
|
|
83
|
+
# "started": null,
|
|
84
|
+
# "completed": null,
|
|
85
|
+
# "result": null,
|
|
86
|
+
# "error": null,
|
|
87
|
+
# "agent": null,
|
|
88
|
+
# "priority": 0
|
|
89
|
+
# }
|
|
90
|
+
# ]
|
|
91
|
+
# }
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _queue_path(root: Path) -> Path:
|
|
95
|
+
return root / QUEUE_FILE
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _root(r: Path | None) -> Path:
|
|
99
|
+
return r or require_project_root()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# ── File I/O ───────────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _read(root: Path) -> dict:
|
|
106
|
+
path = _queue_path(root)
|
|
107
|
+
if not path.exists():
|
|
108
|
+
return {"paused": False, "items": []}
|
|
109
|
+
with path.open("r", encoding="utf-8") as f:
|
|
110
|
+
return json.load(f)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _write(root: Path, data: dict) -> None:
|
|
114
|
+
path = _queue_path(root)
|
|
115
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
116
|
+
tmp = path.with_suffix(".tmp")
|
|
117
|
+
with tmp.open("w", encoding="utf-8") as f:
|
|
118
|
+
json.dump(data, f, indent=2)
|
|
119
|
+
tmp.replace(path)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# ── Queue CRUD ─────────────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def enqueue(
|
|
126
|
+
task_type: str,
|
|
127
|
+
target: str = "full",
|
|
128
|
+
agent: str | None = None,
|
|
129
|
+
priority: int = 0,
|
|
130
|
+
extra: dict | None = None,
|
|
131
|
+
root: Path | None = None,
|
|
132
|
+
) -> str:
|
|
133
|
+
"""
|
|
134
|
+
Add a task to the queue. Returns the new item's ID.
|
|
135
|
+
Raises QueueFullError if the queue is at max depth.
|
|
136
|
+
Warns (via return metadata) if approaching warn depth.
|
|
137
|
+
"""
|
|
138
|
+
r = _root(root)
|
|
139
|
+
queue_path = r / QUEUE_FILE
|
|
140
|
+
with _file_lock(queue_path):
|
|
141
|
+
data = _read(r)
|
|
142
|
+
|
|
143
|
+
waiting = [i for i in data["items"] if i["state"] == QueueItemState.WAITING.value]
|
|
144
|
+
if len(waiting) >= QUEUE_MAX_DEPTH:
|
|
145
|
+
raise QueueFullError(
|
|
146
|
+
f"Queue is full ({QUEUE_MAX_DEPTH} items). "
|
|
147
|
+
"Clear some tasks or increase queue_max_depth in settings."
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
item_id = str(uuid.uuid4())[:8]
|
|
151
|
+
item: dict[str, Any] = {
|
|
152
|
+
"id": item_id,
|
|
153
|
+
"type": task_type,
|
|
154
|
+
"target": target,
|
|
155
|
+
"state": QueueItemState.WAITING.value,
|
|
156
|
+
"created": _now(),
|
|
157
|
+
"started": None,
|
|
158
|
+
"completed": None,
|
|
159
|
+
"result": None,
|
|
160
|
+
"error": None,
|
|
161
|
+
"agent": agent,
|
|
162
|
+
"priority": priority,
|
|
163
|
+
**(extra or {}),
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
data["items"].append(item)
|
|
167
|
+
_write(r, data)
|
|
168
|
+
return item_id
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def get_item(item_id: str, root: Path | None = None) -> dict | None:
|
|
172
|
+
data = _read(_root(root))
|
|
173
|
+
for item in data["items"]:
|
|
174
|
+
if item["id"] == item_id:
|
|
175
|
+
return item
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def list_items(
|
|
180
|
+
state: QueueItemState | None = None,
|
|
181
|
+
root: Path | None = None,
|
|
182
|
+
) -> list[dict]:
|
|
183
|
+
"""Return all items, optionally filtered by state."""
|
|
184
|
+
data = _read(_root(root))
|
|
185
|
+
items = data["items"]
|
|
186
|
+
if state is not None:
|
|
187
|
+
items = [i for i in items if i["state"] == state.value]
|
|
188
|
+
return items
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def list_all(root: Path | None = None) -> dict:
|
|
192
|
+
"""Return full queue data including paused flag and all items."""
|
|
193
|
+
return _read(_root(root))
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# ── State transitions ──────────────────────────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def mark_active(item_id: str, root: Path | None = None) -> bool:
|
|
200
|
+
return _update_item(
|
|
201
|
+
item_id,
|
|
202
|
+
{
|
|
203
|
+
"state": QueueItemState.ACTIVE.value,
|
|
204
|
+
"started": _now(),
|
|
205
|
+
},
|
|
206
|
+
root,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def mark_done(item_id: str, result: Any = None, root: Path | None = None) -> bool:
|
|
211
|
+
return _update_item(
|
|
212
|
+
item_id,
|
|
213
|
+
{
|
|
214
|
+
"state": QueueItemState.DONE.value,
|
|
215
|
+
"completed": _now(),
|
|
216
|
+
"result": result,
|
|
217
|
+
},
|
|
218
|
+
root,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def mark_failed(item_id: str, error: str, root: Path | None = None) -> bool:
|
|
223
|
+
return _update_item(
|
|
224
|
+
item_id,
|
|
225
|
+
{
|
|
226
|
+
"state": QueueItemState.FAILED.value,
|
|
227
|
+
"completed": _now(),
|
|
228
|
+
"error": error,
|
|
229
|
+
},
|
|
230
|
+
root,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def mark_skipped(item_id: str, root: Path | None = None) -> bool:
|
|
235
|
+
return _update_item(
|
|
236
|
+
item_id,
|
|
237
|
+
{
|
|
238
|
+
"state": QueueItemState.SKIPPED.value,
|
|
239
|
+
"completed": _now(),
|
|
240
|
+
},
|
|
241
|
+
root,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _update_item(item_id: str, updates: dict, root: Path | None = None) -> bool:
|
|
246
|
+
r = _root(root)
|
|
247
|
+
queue_path = r / QUEUE_FILE
|
|
248
|
+
with _file_lock(queue_path):
|
|
249
|
+
data = _read(r)
|
|
250
|
+
for item in data["items"]:
|
|
251
|
+
if item["id"] == item_id:
|
|
252
|
+
item.update(updates)
|
|
253
|
+
_write(r, data)
|
|
254
|
+
return True
|
|
255
|
+
return False
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# ── Control ────────────────────────────────────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def pause(root: Path | None = None) -> None:
|
|
262
|
+
r = _root(root)
|
|
263
|
+
queue_path = r / QUEUE_FILE
|
|
264
|
+
with _file_lock(queue_path):
|
|
265
|
+
data = _read(r)
|
|
266
|
+
data["paused"] = True
|
|
267
|
+
_write(r, data)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def resume(root: Path | None = None) -> None:
|
|
271
|
+
r = _root(root)
|
|
272
|
+
queue_path = r / QUEUE_FILE
|
|
273
|
+
with _file_lock(queue_path):
|
|
274
|
+
data = _read(r)
|
|
275
|
+
data["paused"] = False
|
|
276
|
+
_write(r, data)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def is_paused(root: Path | None = None) -> bool:
|
|
280
|
+
return _read(_root(root)).get("paused", False)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def skip_active(root: Path | None = None) -> str | None:
|
|
284
|
+
"""Mark the current active item as skipped. Returns its ID or None."""
|
|
285
|
+
r = _root(root)
|
|
286
|
+
queue_path = r / QUEUE_FILE
|
|
287
|
+
with _file_lock(queue_path):
|
|
288
|
+
data = _read(r)
|
|
289
|
+
for item in data["items"]:
|
|
290
|
+
if item["state"] == QueueItemState.ACTIVE.value:
|
|
291
|
+
item["state"] = QueueItemState.SKIPPED.value
|
|
292
|
+
item["completed"] = _now()
|
|
293
|
+
_write(r, data)
|
|
294
|
+
return item["id"]
|
|
295
|
+
return None
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def clear(root: Path | None = None) -> int:
|
|
299
|
+
"""
|
|
300
|
+
Remove all WAITING items. Active/done/failed are preserved for history.
|
|
301
|
+
Returns the number of items removed.
|
|
302
|
+
"""
|
|
303
|
+
r = _root(root)
|
|
304
|
+
data = _read(r)
|
|
305
|
+
before = len([i for i in data["items"] if i["state"] == QueueItemState.WAITING.value])
|
|
306
|
+
data["items"] = [i for i in data["items"] if i["state"] != QueueItemState.WAITING.value]
|
|
307
|
+
_write(r, data)
|
|
308
|
+
return before
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def clear_all(root: Path | None = None) -> int:
|
|
312
|
+
"""Remove everything including done/failed history. Returns count removed."""
|
|
313
|
+
r = _root(root)
|
|
314
|
+
data = _read(r)
|
|
315
|
+
count = len(data["items"])
|
|
316
|
+
data["items"] = []
|
|
317
|
+
_write(r, data)
|
|
318
|
+
return count
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
# ── Stats ──────────────────────────────────────────────────────────────────────
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def depth(root: Path | None = None) -> int:
|
|
325
|
+
"""Number of WAITING items."""
|
|
326
|
+
return len(list_items(QueueItemState.WAITING, root))
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def is_near_limit(root: Path | None = None) -> bool:
|
|
330
|
+
return depth(root) >= QUEUE_WARN_DEPTH
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def stats(root: Path | None = None) -> dict:
|
|
334
|
+
data = _read(_root(root))
|
|
335
|
+
counts: dict[str, int] = {s.value: 0 for s in QueueItemState}
|
|
336
|
+
for item in data["items"]:
|
|
337
|
+
counts[item["state"]] = counts.get(item["state"], 0) + 1
|
|
338
|
+
return {
|
|
339
|
+
"paused": data.get("paused", False),
|
|
340
|
+
"total": len(data["items"]),
|
|
341
|
+
**counts,
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
# ── Next item picker ─────────────────────────────────
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def next_waiting(root: Path | None = None) -> dict | None:
|
|
349
|
+
"""
|
|
350
|
+
Return the next WAITING item (highest priority first, then FIFO).
|
|
351
|
+
Returns None if queue is empty or paused.
|
|
352
|
+
"""
|
|
353
|
+
r = _root(root)
|
|
354
|
+
if is_paused(r):
|
|
355
|
+
return None
|
|
356
|
+
waiting = list_items(QueueItemState.WAITING, r)
|
|
357
|
+
if not waiting:
|
|
358
|
+
return None
|
|
359
|
+
# Sort by priority descending, then created ascending
|
|
360
|
+
waiting.sort(key=lambda i: (-i.get("priority", 0), i.get("created", "")))
|
|
361
|
+
return waiting[0]
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
# ── Error types ────────────────────────────────────────────────────────────────
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
class QueueFullError(Exception):
|
|
368
|
+
pass
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
# ── Helpers ────────────────────────────────────────────────────────────────────
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _now() -> str:
|
|
375
|
+
return datetime.now(UTC).isoformat()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Reliability testing module — fault injection, chaos, idempotency, recovery."""
|
|
2
|
+
|
|
3
|
+
from .chaos import ChaosScenario
|
|
4
|
+
from .fault_injector import FaultInjector
|
|
5
|
+
from .idempotency import IdempotencyAnalyzer
|
|
6
|
+
from .recovery import RecoveryAnalyzer
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"FaultInjector",
|
|
10
|
+
"ChaosScenario",
|
|
11
|
+
"IdempotencyAnalyzer",
|
|
12
|
+
"RecoveryAnalyzer",
|
|
13
|
+
]
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""ChaosScenario — identify chaos engineering opportunities.
|
|
2
|
+
|
|
3
|
+
Maps data flows and identifies points where failures would cascade,
|
|
4
|
+
suggesting chaos experiments to validate resilience.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from patchi.core.assurance.graph import AssuranceGraph
|
|
14
|
+
|
|
15
|
+
_log = logging.getLogger("patchi.reliability.chaos")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class ChaosExperiment:
|
|
20
|
+
"""A suggested chaos experiment."""
|
|
21
|
+
|
|
22
|
+
name: str
|
|
23
|
+
target: str # file or function
|
|
24
|
+
fault_type: str # "network_partition", "disk_full", "cpu_spike", "memory_pressure"
|
|
25
|
+
description: str
|
|
26
|
+
blast_radius: str # "low", "medium", "high"
|
|
27
|
+
expected_impact: str
|
|
28
|
+
recovery_check: str
|
|
29
|
+
|
|
30
|
+
def to_dict(self) -> dict:
|
|
31
|
+
return {
|
|
32
|
+
"name": self.name,
|
|
33
|
+
"target": self.target,
|
|
34
|
+
"fault_type": self.fault_type,
|
|
35
|
+
"description": self.description,
|
|
36
|
+
"blast_radius": self.blast_radius,
|
|
37
|
+
"expected_impact": self.expected_impact,
|
|
38
|
+
"recovery_check": self.recovery_check,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ChaosScenario:
|
|
43
|
+
"""Analyze the project for chaos engineering opportunities."""
|
|
44
|
+
|
|
45
|
+
def __init__(self, root: Path, graph: AssuranceGraph | None = None):
|
|
46
|
+
self._root = root
|
|
47
|
+
self._graph = graph
|
|
48
|
+
self._flows: list[dict] = []
|
|
49
|
+
|
|
50
|
+
def load_flows(self, flows: list[dict]) -> None:
|
|
51
|
+
"""Load data flows from brain data."""
|
|
52
|
+
self._flows = flows
|
|
53
|
+
|
|
54
|
+
def analyze(self, max_experiments: int = 20) -> list[ChaosExperiment]:
|
|
55
|
+
"""Generate chaos experiments based on the project's data flows."""
|
|
56
|
+
experiments: list[ChaosExperiment] = []
|
|
57
|
+
|
|
58
|
+
# Analyze from graph if available
|
|
59
|
+
if self._graph:
|
|
60
|
+
experiments.extend(self._graph_based_experiments())
|
|
61
|
+
|
|
62
|
+
# Analyze from data flows
|
|
63
|
+
experiments.extend(self._flow_based_experiments())
|
|
64
|
+
|
|
65
|
+
# General resilience checks
|
|
66
|
+
experiments.extend(self._general_resilience_experiments())
|
|
67
|
+
|
|
68
|
+
return experiments[:max_experiments]
|
|
69
|
+
|
|
70
|
+
def _graph_based_experiments(self) -> list[ChaosExperiment]:
|
|
71
|
+
"""Generate experiments from assurance graph data flows."""
|
|
72
|
+
experiments: list[ChaosExperiment] = []
|
|
73
|
+
|
|
74
|
+
# Find critical paths (data flows through auth boundaries)
|
|
75
|
+
for claim in self._graph.claims.values():
|
|
76
|
+
for ev in claim.evidence:
|
|
77
|
+
if not ev.supports:
|
|
78
|
+
continue
|
|
79
|
+
artifact = ev.artifact
|
|
80
|
+
if "sink" in artifact and "source" in artifact:
|
|
81
|
+
source = artifact["source"]
|
|
82
|
+
sink = artifact["sink"]
|
|
83
|
+
experiments.append(
|
|
84
|
+
ChaosExperiment(
|
|
85
|
+
name=f"partition_{source}_to_{sink}",
|
|
86
|
+
target=source,
|
|
87
|
+
fault_type="network_partition",
|
|
88
|
+
description=f"Simulate network partition between {source} and {sink}",
|
|
89
|
+
blast_radius="medium",
|
|
90
|
+
expected_impact=f"Requests from {source} to {sink} should timeout gracefully",
|
|
91
|
+
recovery_check=f"Verify {sink} returns 503 or cached response",
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return experiments
|
|
96
|
+
|
|
97
|
+
def _flow_based_experiments(self) -> list[ChaosExperiment]:
|
|
98
|
+
"""Generate experiments from data flow analysis."""
|
|
99
|
+
experiments: list[ChaosExperiment] = []
|
|
100
|
+
|
|
101
|
+
# Group flows by source
|
|
102
|
+
by_source: dict[str, list[dict]] = {}
|
|
103
|
+
for flow in self._flows:
|
|
104
|
+
source = flow.get("source", "unknown")
|
|
105
|
+
by_source.setdefault(source, []).append(flow)
|
|
106
|
+
|
|
107
|
+
# For each source with multiple outgoing flows, test dependency failure
|
|
108
|
+
for source, flows in by_source.items():
|
|
109
|
+
if len(flows) >= 2:
|
|
110
|
+
experiments.append(
|
|
111
|
+
ChaosExperiment(
|
|
112
|
+
name=f"dependency_failure_{source}",
|
|
113
|
+
target=source,
|
|
114
|
+
fault_type="dependency_failure",
|
|
115
|
+
description=f"Simulate failure of one dependency of {source} (has {len(flows)} outgoing flows)",
|
|
116
|
+
blast_radius="medium",
|
|
117
|
+
expected_impact=f"{source} should degrade gracefully when one dependency fails",
|
|
118
|
+
recovery_check="Verify partial results are returned, not a crash",
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
return experiments
|
|
123
|
+
|
|
124
|
+
def _general_resilience_experiments(self) -> list[ChaosExperiment]:
|
|
125
|
+
"""Generate general resilience experiments."""
|
|
126
|
+
return [
|
|
127
|
+
ChaosExperiment(
|
|
128
|
+
name="disk_full_on_scan",
|
|
129
|
+
target=".patchi/",
|
|
130
|
+
fault_type="disk_full",
|
|
131
|
+
description="Simulate disk full during scan result persistence",
|
|
132
|
+
blast_radius="low",
|
|
133
|
+
expected_impact="Scan should complete in memory and warn about persistence failure",
|
|
134
|
+
recovery_check="Verify scan results are still available in memory",
|
|
135
|
+
),
|
|
136
|
+
ChaosExperiment(
|
|
137
|
+
name="memory_pressure_large_project",
|
|
138
|
+
target="scanner",
|
|
139
|
+
fault_type="memory_pressure",
|
|
140
|
+
description="Test scanner behavior with very large file corpus",
|
|
141
|
+
blast_radius="medium",
|
|
142
|
+
expected_impact="Scanner should process files in batches, not load all into memory",
|
|
143
|
+
recovery_check="Verify scan completes without OOM",
|
|
144
|
+
),
|
|
145
|
+
ChaosExperiment(
|
|
146
|
+
name="database_locked",
|
|
147
|
+
target="history",
|
|
148
|
+
fault_type="database_lock",
|
|
149
|
+
description="Simulate SQLite database locked by another process",
|
|
150
|
+
blast_radius="low",
|
|
151
|
+
expected_impact="History writes should retry or queue, not crash",
|
|
152
|
+
recovery_check="Verify scan completes and results are eventually persisted",
|
|
153
|
+
),
|
|
154
|
+
ChaosExperiment(
|
|
155
|
+
name="network_timeout_ai",
|
|
156
|
+
target="llm_security_agent",
|
|
157
|
+
fault_type="network_partition",
|
|
158
|
+
description="Simulate AI provider timeout during LLM security analysis",
|
|
159
|
+
blast_radius="low",
|
|
160
|
+
expected_impact="LLM analysis should timeout gracefully, deterministic agents still work",
|
|
161
|
+
recovery_check="Verify deterministic findings are still produced",
|
|
162
|
+
),
|
|
163
|
+
]
|