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,621 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tool Executor — Executes AI tool calls with validation, confirmation, and error handling.
|
|
3
|
+
|
|
4
|
+
Handles:
|
|
5
|
+
- Parameter validation against JSON schemas
|
|
6
|
+
- User confirmation for destructive operations
|
|
7
|
+
- Execution with timeout and error recovery
|
|
8
|
+
- Result formatting for AI consumption
|
|
9
|
+
- Audit logging of all tool invocations
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
import inspect
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
import time
|
|
19
|
+
import uuid
|
|
20
|
+
from collections.abc import Callable
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from datetime import UTC, datetime
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
from patchi.core.ai.tools.registry import ToolDefinition, ToolParameter, get_tool_registry
|
|
27
|
+
|
|
28
|
+
_log = logging.getLogger("patchi.ai.tool_executor")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class ToolInvocation:
|
|
33
|
+
"""A single tool invocation record for audit logging."""
|
|
34
|
+
|
|
35
|
+
id: str
|
|
36
|
+
tool_name: str
|
|
37
|
+
parameters: dict
|
|
38
|
+
invoked_by: str # "council", "persona", "cli", "user"
|
|
39
|
+
persona_name: str | None = None
|
|
40
|
+
timestamp: str = field(default_factory=lambda: datetime.now(UTC).isoformat())
|
|
41
|
+
started_at: float = 0
|
|
42
|
+
completed_at: float = 0
|
|
43
|
+
success: bool = False
|
|
44
|
+
result: dict | None = None
|
|
45
|
+
error: str | None = None
|
|
46
|
+
confirmation_given: bool = False
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def duration_ms(self) -> int:
|
|
50
|
+
"""Wall-clock duration of the invocation in milliseconds."""
|
|
51
|
+
if not self.completed_at:
|
|
52
|
+
return 0
|
|
53
|
+
return int((self.completed_at - self.started_at) * 1000)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class ExecutionResult:
|
|
58
|
+
"""Result of a tool execution."""
|
|
59
|
+
|
|
60
|
+
success: bool
|
|
61
|
+
result: Any = None
|
|
62
|
+
error: str | None = None
|
|
63
|
+
invocation_id: str = ""
|
|
64
|
+
duration_ms: int = 0
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ConfirmationProvider:
|
|
68
|
+
"""Interface for getting user confirmation."""
|
|
69
|
+
|
|
70
|
+
async def confirm(
|
|
71
|
+
self, tool_name: str, parameters: dict, side_effects: str, description: str
|
|
72
|
+
) -> bool:
|
|
73
|
+
"""Return True if user confirms, False otherwise."""
|
|
74
|
+
raise NotImplementedError
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class CLIConfirmationProvider(ConfirmationProvider):
|
|
78
|
+
"""CLI-based confirmation using rich prompts."""
|
|
79
|
+
|
|
80
|
+
def __init__(self, auto_confirm: bool = False):
|
|
81
|
+
self.auto_confirm = auto_confirm
|
|
82
|
+
|
|
83
|
+
async def confirm(
|
|
84
|
+
self, tool_name: str, parameters: dict, side_effects: str, description: str
|
|
85
|
+
) -> bool:
|
|
86
|
+
if self.auto_confirm:
|
|
87
|
+
return True
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
from rich.console import Console
|
|
91
|
+
from rich.prompt import Confirm
|
|
92
|
+
|
|
93
|
+
console = Console()
|
|
94
|
+
console.print(f"\n[yellow]⚠ Tool requires confirmation:[/yellow] {tool_name}")
|
|
95
|
+
console.print(f" Description: {description}")
|
|
96
|
+
console.print(f" Parameters: {json.dumps(parameters, indent=2)}")
|
|
97
|
+
if side_effects:
|
|
98
|
+
console.print(f" [red]Side effects:[/red] {side_effects}")
|
|
99
|
+
|
|
100
|
+
return Confirm.ask(" Proceed?", default=False)
|
|
101
|
+
except Exception:
|
|
102
|
+
# Fallback to simple input
|
|
103
|
+
if side_effects:
|
|
104
|
+
pass
|
|
105
|
+
response = input(" Proceed? (y/N): ").strip().lower()
|
|
106
|
+
return response in ("y", "yes")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class WebConfirmationProvider(ConfirmationProvider):
|
|
110
|
+
"""Web confirmation — auto-confirms since the web UI handles confirmation via its own modal."""
|
|
111
|
+
|
|
112
|
+
async def confirm(
|
|
113
|
+
self, tool_name: str, parameters: dict, side_effects: str, description: str
|
|
114
|
+
) -> bool:
|
|
115
|
+
# Web UI handles confirmation through its own modal/dialog
|
|
116
|
+
# Auto-confirm here since the web route already validated the request
|
|
117
|
+
return True
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@dataclass
|
|
121
|
+
class StateSnapshot:
|
|
122
|
+
"""Snapshot of file system state for rollback."""
|
|
123
|
+
|
|
124
|
+
id: str
|
|
125
|
+
files: dict[str, bytes] = field(default_factory=dict) # path -> content
|
|
126
|
+
created_at: str = field(default_factory=lambda: datetime.now(UTC).isoformat())
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class ToolExecutor:
|
|
130
|
+
"""
|
|
131
|
+
Executes tool calls with full validation, confirmation, and error handling.
|
|
132
|
+
|
|
133
|
+
Features:
|
|
134
|
+
- JSON schema validation for parameters
|
|
135
|
+
- Confirmation gates for destructive operations
|
|
136
|
+
- Timeout enforcement
|
|
137
|
+
- Structured error handling
|
|
138
|
+
- Audit logging
|
|
139
|
+
- Result normalization for AI consumption
|
|
140
|
+
- State snapshots and rollback on failure
|
|
141
|
+
- Tool chain execution with automatic rollback
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
def __init__(
|
|
145
|
+
self,
|
|
146
|
+
root: Path,
|
|
147
|
+
confirmation_provider: ConfirmationProvider | None = None,
|
|
148
|
+
default_timeout: float = 120.0,
|
|
149
|
+
on_progress: Callable[[str], None] | None = None,
|
|
150
|
+
):
|
|
151
|
+
self.root = root
|
|
152
|
+
self.registry = get_tool_registry()
|
|
153
|
+
self.confirmation_provider = confirmation_provider or CLIConfirmationProvider()
|
|
154
|
+
self.default_timeout = default_timeout
|
|
155
|
+
self.on_progress = on_progress or (lambda _: None)
|
|
156
|
+
|
|
157
|
+
# Audit log
|
|
158
|
+
self.invocation_log: list[ToolInvocation] = []
|
|
159
|
+
self._log_path = root / ".patchi" / "logs" / "tool_invocations.jsonl"
|
|
160
|
+
self._log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
161
|
+
|
|
162
|
+
# Rollback state
|
|
163
|
+
self._snapshots: dict[str, StateSnapshot] = {}
|
|
164
|
+
self._snapshot_dir = root / ".patchi" / "snapshots"
|
|
165
|
+
self._snapshot_dir.mkdir(parents=True, exist_ok=True)
|
|
166
|
+
|
|
167
|
+
async def execute(
|
|
168
|
+
self,
|
|
169
|
+
tool_name: str,
|
|
170
|
+
parameters: dict,
|
|
171
|
+
invoked_by: str = "user",
|
|
172
|
+
persona_name: str | None = None,
|
|
173
|
+
timeout: float | None = None,
|
|
174
|
+
skip_confirmation: bool = False,
|
|
175
|
+
) -> ExecutionResult:
|
|
176
|
+
"""
|
|
177
|
+
Execute a tool call with full validation and safety checks.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
tool_name: Name of the tool to execute
|
|
181
|
+
parameters: Parameters for the tool
|
|
182
|
+
invoked_by: Who is invoking ("council", "persona", "cli", "user")
|
|
183
|
+
persona_name: Name of the persona if invoked by one
|
|
184
|
+
timeout: Custom timeout in seconds
|
|
185
|
+
skip_confirmation: Skip confirmation even if required (for testing)
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
ExecutionResult with success status, result, or error
|
|
189
|
+
"""
|
|
190
|
+
invocation_id = str(uuid.uuid4())[:8]
|
|
191
|
+
start_time = time.monotonic()
|
|
192
|
+
|
|
193
|
+
# Create invocation record
|
|
194
|
+
invocation = ToolInvocation(
|
|
195
|
+
id=invocation_id,
|
|
196
|
+
tool_name=tool_name,
|
|
197
|
+
parameters=parameters,
|
|
198
|
+
invoked_by=invoked_by,
|
|
199
|
+
persona_name=persona_name,
|
|
200
|
+
started_at=start_time,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
self.on_progress(f"[EXEC] Executing tool: {tool_name}")
|
|
204
|
+
_log.info(f"Tool invocation {invocation_id}: {tool_name}({parameters}) by {invoked_by}")
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
# 1. Validate tool exists
|
|
208
|
+
tool_def = self.registry.get_tool(tool_name)
|
|
209
|
+
if not tool_def:
|
|
210
|
+
raise ValueError(f"Unknown tool: {tool_name}")
|
|
211
|
+
|
|
212
|
+
handler = self.registry.get_handler(tool_name)
|
|
213
|
+
if not handler:
|
|
214
|
+
raise ValueError(f"No handler for tool: {tool_name}")
|
|
215
|
+
|
|
216
|
+
# 2. Validate parameters against schema
|
|
217
|
+
validated_params = self._validate_parameters(tool_def, parameters)
|
|
218
|
+
|
|
219
|
+
# 3. Confirmation gate
|
|
220
|
+
if tool_def.requires_confirmation and not skip_confirmation:
|
|
221
|
+
confirmed = await self.confirmation_provider.confirm(
|
|
222
|
+
tool_name=tool_name,
|
|
223
|
+
parameters=validated_params,
|
|
224
|
+
side_effects=tool_def.side_effects,
|
|
225
|
+
description=tool_def.description,
|
|
226
|
+
)
|
|
227
|
+
invocation.confirmation_given = confirmed
|
|
228
|
+
if not confirmed:
|
|
229
|
+
return ExecutionResult(
|
|
230
|
+
success=False,
|
|
231
|
+
error="User declined confirmation",
|
|
232
|
+
invocation_id=invocation_id,
|
|
233
|
+
duration_ms=int((time.monotonic() - start_time) * 1000),
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
# 4. Execute with timeout
|
|
237
|
+
exec_timeout = timeout or self.default_timeout
|
|
238
|
+
result = await self._execute_with_timeout(handler, validated_params, exec_timeout)
|
|
239
|
+
|
|
240
|
+
# 5. Normalize result
|
|
241
|
+
normalized = self._normalize_result(result)
|
|
242
|
+
|
|
243
|
+
invocation.success = True
|
|
244
|
+
invocation.result = normalized
|
|
245
|
+
invocation.completed_at = time.monotonic()
|
|
246
|
+
|
|
247
|
+
self._log_invocation(invocation)
|
|
248
|
+
|
|
249
|
+
self.on_progress(f"[OK] {tool_name} completed in {invocation.duration_ms}ms")
|
|
250
|
+
|
|
251
|
+
return ExecutionResult(
|
|
252
|
+
success=True,
|
|
253
|
+
result=normalized,
|
|
254
|
+
invocation_id=invocation_id,
|
|
255
|
+
duration_ms=invocation.duration_ms,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
except Exception as e:
|
|
259
|
+
_log.error(f"Tool {tool_name} failed: {e}", exc_info=True)
|
|
260
|
+
invocation.success = False
|
|
261
|
+
invocation.error = str(e)
|
|
262
|
+
invocation.completed_at = time.monotonic()
|
|
263
|
+
self._log_invocation(invocation)
|
|
264
|
+
|
|
265
|
+
return ExecutionResult(
|
|
266
|
+
success=False,
|
|
267
|
+
error=str(e),
|
|
268
|
+
invocation_id=invocation_id,
|
|
269
|
+
duration_ms=int((time.monotonic() - start_time) * 1000),
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
def _validate_parameters(self, tool_def: ToolDefinition, parameters: dict) -> dict:
|
|
273
|
+
"""Validate and coerce parameters against tool schema."""
|
|
274
|
+
validated = {}
|
|
275
|
+
|
|
276
|
+
for param in tool_def.parameters:
|
|
277
|
+
if param.name in parameters:
|
|
278
|
+
value = parameters[param.name]
|
|
279
|
+
validated[param.name] = self._coerce_value(value, param)
|
|
280
|
+
elif param.required:
|
|
281
|
+
raise ValueError(f"Missing required parameter: {param.name}")
|
|
282
|
+
elif param.default is not None:
|
|
283
|
+
validated[param.name] = param.default
|
|
284
|
+
|
|
285
|
+
# Check for unknown parameters
|
|
286
|
+
known_params = {p.name for p in tool_def.parameters}
|
|
287
|
+
unknown = set(parameters.keys()) - known_params
|
|
288
|
+
if unknown:
|
|
289
|
+
_log.warning(f"Tool {tool_def.name} received unknown parameters: {unknown}")
|
|
290
|
+
|
|
291
|
+
return validated
|
|
292
|
+
|
|
293
|
+
def _coerce_value(self, value: Any, param: ToolParameter) -> Any:
|
|
294
|
+
"""Coerce a value to the expected type."""
|
|
295
|
+
if param.type == "string":
|
|
296
|
+
return str(value)
|
|
297
|
+
elif param.type == "integer":
|
|
298
|
+
return int(value)
|
|
299
|
+
elif param.type == "number":
|
|
300
|
+
return float(value)
|
|
301
|
+
elif param.type == "boolean":
|
|
302
|
+
if isinstance(value, str):
|
|
303
|
+
return value.lower() in ("true", "1", "yes", "on")
|
|
304
|
+
return bool(value)
|
|
305
|
+
elif param.type == "array":
|
|
306
|
+
if value is None:
|
|
307
|
+
return None
|
|
308
|
+
if isinstance(value, list):
|
|
309
|
+
return value
|
|
310
|
+
return [value]
|
|
311
|
+
elif param.type == "object":
|
|
312
|
+
if isinstance(value, dict):
|
|
313
|
+
return value
|
|
314
|
+
return {}
|
|
315
|
+
return value
|
|
316
|
+
|
|
317
|
+
async def _execute_with_timeout(
|
|
318
|
+
self,
|
|
319
|
+
handler: Callable,
|
|
320
|
+
parameters: dict,
|
|
321
|
+
timeout: float,
|
|
322
|
+
) -> Any:
|
|
323
|
+
"""Execute handler with timeout."""
|
|
324
|
+
# Add root to parameters if handler expects it
|
|
325
|
+
sig = inspect.signature(handler)
|
|
326
|
+
if "root" in sig.parameters:
|
|
327
|
+
parameters["root"] = self.root
|
|
328
|
+
|
|
329
|
+
# Check if handler is async
|
|
330
|
+
if inspect.iscoroutinefunction(handler):
|
|
331
|
+
return await asyncio.wait_for(handler(**parameters), timeout=timeout)
|
|
332
|
+
else:
|
|
333
|
+
# Run sync handlers directly in the event loop's thread.
|
|
334
|
+
#
|
|
335
|
+
# We deliberately do NOT use loop.run_in_executor here: many security
|
|
336
|
+
# agents call signal.signal() / rely on main-thread-only behavior, and
|
|
337
|
+
# tools like scan_vulnerabilities spawn their own thread pools — both
|
|
338
|
+
# break or deadlock when the handler runs in a worker thread (every
|
|
339
|
+
# agent raises -> the scan reports 0 findings). The agent loop is
|
|
340
|
+
# single-purpose, so blocking it briefly is acceptable; per-tool
|
|
341
|
+
# internal timeouts (e.g. realize's PATCHI_AGENT_TIMEOUT) still bound
|
|
342
|
+
# any individual tool that would otherwise hang.
|
|
343
|
+
return handler(**parameters)
|
|
344
|
+
|
|
345
|
+
def _normalize_result(self, result: Any) -> dict:
|
|
346
|
+
"""Normalize tool result to a standard dict format."""
|
|
347
|
+
if isinstance(result, dict):
|
|
348
|
+
# Ensure success field
|
|
349
|
+
if "success" not in result:
|
|
350
|
+
result = {"success": True, "data": result}
|
|
351
|
+
return result
|
|
352
|
+
elif isinstance(result, (list, str, int, float, bool)):
|
|
353
|
+
return {"success": True, "data": result}
|
|
354
|
+
else:
|
|
355
|
+
return {"success": True, "data": str(result)}
|
|
356
|
+
|
|
357
|
+
def _log_invocation(self, invocation: ToolInvocation) -> None:
|
|
358
|
+
"""Log invocation to audit trail."""
|
|
359
|
+
self.invocation_log.append(invocation)
|
|
360
|
+
|
|
361
|
+
# Write to JSONL file
|
|
362
|
+
try:
|
|
363
|
+
log_entry = {
|
|
364
|
+
"id": invocation.id,
|
|
365
|
+
"tool": invocation.tool_name,
|
|
366
|
+
"parameters": invocation.parameters,
|
|
367
|
+
"invoked_by": invocation.invoked_by,
|
|
368
|
+
"persona": invocation.persona_name,
|
|
369
|
+
"timestamp": invocation.timestamp,
|
|
370
|
+
"duration_ms": int((invocation.completed_at - invocation.started_at) * 1000),
|
|
371
|
+
"success": invocation.success,
|
|
372
|
+
"error": invocation.error,
|
|
373
|
+
"confirmed": invocation.confirmation_given,
|
|
374
|
+
}
|
|
375
|
+
with open(self._log_path, "a", encoding="utf-8") as f:
|
|
376
|
+
f.write(json.dumps(log_entry) + "\n")
|
|
377
|
+
except Exception as e:
|
|
378
|
+
_log.warning(f"Failed to write invocation log: {e}")
|
|
379
|
+
|
|
380
|
+
def get_invocation_history(self, limit: int = 100) -> list[ToolInvocation]:
|
|
381
|
+
"""Get recent invocation history."""
|
|
382
|
+
return self.invocation_log[-limit:]
|
|
383
|
+
|
|
384
|
+
def get_tool_schemas(self, category: str = None) -> list[dict]:
|
|
385
|
+
"""Get JSON schemas for tools (for AI consumption)."""
|
|
386
|
+
return self.registry.get_schemas(category)
|
|
387
|
+
|
|
388
|
+
def list_available_tools(self, category: str = None) -> list[dict]:
|
|
389
|
+
"""List available tools with metadata."""
|
|
390
|
+
tools = self.registry.list_tools(category)
|
|
391
|
+
return [
|
|
392
|
+
{
|
|
393
|
+
"name": t.name,
|
|
394
|
+
"description": t.description,
|
|
395
|
+
"category": t.category,
|
|
396
|
+
"requires_confirmation": t.requires_confirmation,
|
|
397
|
+
"side_effects": t.side_effects,
|
|
398
|
+
}
|
|
399
|
+
for t in tools
|
|
400
|
+
]
|
|
401
|
+
|
|
402
|
+
# ── Snapshot & Rollback ─────────────────────────────────────────────────
|
|
403
|
+
|
|
404
|
+
def create_snapshot(self, description: str = "") -> str:
|
|
405
|
+
"""Create a snapshot of files that might be modified.
|
|
406
|
+
|
|
407
|
+
Snapshots key project files (config, source, etc.) so they can be
|
|
408
|
+
restored if a tool chain fails.
|
|
409
|
+
|
|
410
|
+
Returns:
|
|
411
|
+
Snapshot ID for later rollback.
|
|
412
|
+
"""
|
|
413
|
+
snapshot_id = str(uuid.uuid4())[:8]
|
|
414
|
+
files: dict[str, bytes] = {}
|
|
415
|
+
|
|
416
|
+
# Snapshot common tool targets
|
|
417
|
+
patterns = [
|
|
418
|
+
"**/*.py",
|
|
419
|
+
"**/*.yaml",
|
|
420
|
+
"**/*.yml",
|
|
421
|
+
"**/*.json",
|
|
422
|
+
"**/*.toml",
|
|
423
|
+
"**/*.cfg",
|
|
424
|
+
"**/*.ini",
|
|
425
|
+
]
|
|
426
|
+
|
|
427
|
+
for pattern in patterns:
|
|
428
|
+
for path in self.root.glob(pattern):
|
|
429
|
+
# Skip .patchi, .git, __pycache__, node_modules, venv
|
|
430
|
+
rel = path.relative_to(self.root)
|
|
431
|
+
parts = rel.parts
|
|
432
|
+
if any(
|
|
433
|
+
p.startswith(".") or p in ("__pycache__", "node_modules", ".venv", "venv")
|
|
434
|
+
for p in parts
|
|
435
|
+
):
|
|
436
|
+
continue
|
|
437
|
+
try:
|
|
438
|
+
content = path.read_bytes()
|
|
439
|
+
# Only snapshot small files (< 100KB)
|
|
440
|
+
if len(content) < 100_000:
|
|
441
|
+
files[str(rel)] = content
|
|
442
|
+
except Exception:
|
|
443
|
+
pass
|
|
444
|
+
|
|
445
|
+
snapshot = StateSnapshot(id=snapshot_id, files=files)
|
|
446
|
+
self._snapshots[snapshot_id] = snapshot
|
|
447
|
+
|
|
448
|
+
# Persist to disk for cross-session rollback
|
|
449
|
+
snapshot_path = self._snapshot_dir / f"{snapshot_id}.json"
|
|
450
|
+
try:
|
|
451
|
+
import base64
|
|
452
|
+
|
|
453
|
+
data = {
|
|
454
|
+
"id": snapshot_id,
|
|
455
|
+
"description": description,
|
|
456
|
+
"created_at": snapshot.created_at,
|
|
457
|
+
"files": {k: base64.b64encode(v).decode() for k, v in files.items()},
|
|
458
|
+
}
|
|
459
|
+
snapshot_path.write_text(json.dumps(data), encoding="utf-8")
|
|
460
|
+
except Exception as e:
|
|
461
|
+
_log.warning(f"Failed to persist snapshot: {e}")
|
|
462
|
+
|
|
463
|
+
self.on_progress(f"[SNAP] Snapshot {snapshot_id} created ({len(files)} files)")
|
|
464
|
+
return snapshot_id
|
|
465
|
+
|
|
466
|
+
def rollback(self, snapshot_id: str) -> bool:
|
|
467
|
+
"""Restore files from a snapshot.
|
|
468
|
+
|
|
469
|
+
Returns:
|
|
470
|
+
True if rollback succeeded.
|
|
471
|
+
"""
|
|
472
|
+
snapshot = self._snapshots.get(snapshot_id)
|
|
473
|
+
if not snapshot:
|
|
474
|
+
# Try loading from disk
|
|
475
|
+
snapshot = self._load_snapshot(snapshot_id)
|
|
476
|
+
|
|
477
|
+
if not snapshot:
|
|
478
|
+
_log.error(f"Snapshot {snapshot_id} not found")
|
|
479
|
+
return False
|
|
480
|
+
|
|
481
|
+
restored = 0
|
|
482
|
+
for rel_path, content in snapshot.files.items():
|
|
483
|
+
full_path = self.root / rel_path
|
|
484
|
+
try:
|
|
485
|
+
full_path.parent.mkdir(parents=True, exist_ok=True)
|
|
486
|
+
full_path.write_bytes(content)
|
|
487
|
+
restored += 1
|
|
488
|
+
except Exception as e:
|
|
489
|
+
_log.warning(f"Failed to restore {rel_path}: {e}")
|
|
490
|
+
|
|
491
|
+
self.on_progress(f"↩️ Rollback {snapshot_id} restored {restored} files")
|
|
492
|
+
_log.info(f"Rollback {snapshot_id}: restored {restored}/{len(snapshot.files)} files")
|
|
493
|
+
return True
|
|
494
|
+
|
|
495
|
+
def _load_snapshot(self, snapshot_id: str) -> StateSnapshot | None:
|
|
496
|
+
"""Load a snapshot from disk."""
|
|
497
|
+
snapshot_path = self._snapshot_dir / f"{snapshot_id}.json"
|
|
498
|
+
if not snapshot_path.exists():
|
|
499
|
+
return None
|
|
500
|
+
|
|
501
|
+
try:
|
|
502
|
+
import base64
|
|
503
|
+
|
|
504
|
+
data = json.loads(snapshot_path.read_text(encoding="utf-8"))
|
|
505
|
+
files = {k: base64.b64decode(v) for k, v in data["files"].items()}
|
|
506
|
+
return StateSnapshot(
|
|
507
|
+
id=data["id"],
|
|
508
|
+
files=files,
|
|
509
|
+
created_at=data["created_at"],
|
|
510
|
+
)
|
|
511
|
+
except Exception as e:
|
|
512
|
+
_log.warning(f"Failed to load snapshot {snapshot_id}: {e}")
|
|
513
|
+
return None
|
|
514
|
+
|
|
515
|
+
async def execute_with_rollback(
|
|
516
|
+
self,
|
|
517
|
+
tool_name: str,
|
|
518
|
+
parameters: dict,
|
|
519
|
+
invoked_by: str = "user",
|
|
520
|
+
persona_name: str | None = None,
|
|
521
|
+
) -> ExecutionResult:
|
|
522
|
+
"""Execute a tool with automatic snapshot and rollback on failure.
|
|
523
|
+
|
|
524
|
+
Creates a state snapshot before execution, and rolls back if the
|
|
525
|
+
tool fails.
|
|
526
|
+
"""
|
|
527
|
+
# Create snapshot before execution
|
|
528
|
+
snapshot_id = self.create_snapshot(f"pre-{tool_name}")
|
|
529
|
+
|
|
530
|
+
# Execute the tool
|
|
531
|
+
result = await self.execute(tool_name, parameters, invoked_by, persona_name)
|
|
532
|
+
|
|
533
|
+
# Rollback on failure
|
|
534
|
+
if not result.success:
|
|
535
|
+
self.on_progress(f"⚠️ {tool_name} failed, rolling back...")
|
|
536
|
+
self.rollback(snapshot_id)
|
|
537
|
+
else:
|
|
538
|
+
# Clean up old snapshot on success
|
|
539
|
+
self._cleanup_snapshot(snapshot_id)
|
|
540
|
+
|
|
541
|
+
return result
|
|
542
|
+
|
|
543
|
+
def _cleanup_snapshot(self, snapshot_id: str) -> None:
|
|
544
|
+
"""Remove a snapshot after successful use."""
|
|
545
|
+
self._snapshots.pop(snapshot_id, None)
|
|
546
|
+
snapshot_path = self._snapshot_dir / f"{snapshot_id}.json"
|
|
547
|
+
try:
|
|
548
|
+
snapshot_path.unlink(missing_ok=True)
|
|
549
|
+
except Exception:
|
|
550
|
+
pass
|
|
551
|
+
|
|
552
|
+
async def execute_chain(
|
|
553
|
+
self,
|
|
554
|
+
steps: list[dict],
|
|
555
|
+
invoked_by: str = "user",
|
|
556
|
+
stop_on_failure: bool = True,
|
|
557
|
+
) -> list[ExecutionResult]:
|
|
558
|
+
"""Execute a chain of tools with rollback on failure.
|
|
559
|
+
|
|
560
|
+
Args:
|
|
561
|
+
steps: List of {"tool": name, "parameters": {...}} dicts
|
|
562
|
+
invoked_by: Who is invoking the chain
|
|
563
|
+
stop_on_failure: If True, stop and rollback on first failure
|
|
564
|
+
|
|
565
|
+
Returns:
|
|
566
|
+
List of ExecutionResult for each step
|
|
567
|
+
"""
|
|
568
|
+
results = []
|
|
569
|
+
snapshot_id = self.create_snapshot("chain-pre-execution")
|
|
570
|
+
|
|
571
|
+
for i, step in enumerate(steps):
|
|
572
|
+
tool_name = step.get("tool")
|
|
573
|
+
parameters = step.get("parameters", {})
|
|
574
|
+
|
|
575
|
+
self.on_progress(f"[CHAIN] Chain step {i + 1}/{len(steps)}: {tool_name}")
|
|
576
|
+
|
|
577
|
+
result = await self.execute(tool_name, parameters, invoked_by)
|
|
578
|
+
results.append(result)
|
|
579
|
+
|
|
580
|
+
if not result.success and stop_on_failure:
|
|
581
|
+
self.on_progress(f"[FAIL] Chain failed at step {i + 1}, rolling back...")
|
|
582
|
+
self.rollback(snapshot_id)
|
|
583
|
+
# Mark remaining steps as skipped
|
|
584
|
+
for _j in range(i + 1, len(steps)):
|
|
585
|
+
results.append(
|
|
586
|
+
ExecutionResult(
|
|
587
|
+
success=False,
|
|
588
|
+
error=f"Skipped: chain stopped at step {i + 1}",
|
|
589
|
+
)
|
|
590
|
+
)
|
|
591
|
+
break
|
|
592
|
+
|
|
593
|
+
# Clean up snapshot if chain succeeded
|
|
594
|
+
all_success = all(r.success for r in results)
|
|
595
|
+
if all_success:
|
|
596
|
+
self._cleanup_snapshot(snapshot_id)
|
|
597
|
+
|
|
598
|
+
return results
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
# Convenience function for synchronous execution (for non-async contexts)
|
|
602
|
+
def execute_tool_sync(
|
|
603
|
+
root: Path,
|
|
604
|
+
tool_name: str,
|
|
605
|
+
parameters: dict,
|
|
606
|
+
invoked_by: str = "cli",
|
|
607
|
+
timeout: float = 120.0,
|
|
608
|
+
) -> ExecutionResult:
|
|
609
|
+
"""Synchronous tool execution for CLI use."""
|
|
610
|
+
executor = ToolExecutor(root)
|
|
611
|
+
|
|
612
|
+
# Run async executor in event loop
|
|
613
|
+
try:
|
|
614
|
+
loop = asyncio.get_event_loop()
|
|
615
|
+
except RuntimeError:
|
|
616
|
+
loop = asyncio.new_event_loop()
|
|
617
|
+
asyncio.set_event_loop(loop)
|
|
618
|
+
|
|
619
|
+
return loop.run_until_complete(
|
|
620
|
+
executor.execute(tool_name, parameters, invoked_by, timeout=timeout)
|
|
621
|
+
)
|