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,625 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Patchi Orchestrator — The central brain that turns p chat into an orchestra.
|
|
3
|
+
|
|
4
|
+
The chat IS the brain. It uses LLM-powered reasoning to:
|
|
5
|
+
1. Understand natural language requests (no keyword matching)
|
|
6
|
+
2. Decide what tools/agents to use
|
|
7
|
+
3. Execute them with full control
|
|
8
|
+
4. Report results with context
|
|
9
|
+
|
|
10
|
+
The orchestrator gives the chat direct access to:
|
|
11
|
+
- Read/write any file in the project
|
|
12
|
+
- Run any CLI command
|
|
13
|
+
- Spawn any agent (security, testing, fixing, etc.)
|
|
14
|
+
- Access the full brain/memory state
|
|
15
|
+
- Make decisions based on context
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import asyncio
|
|
21
|
+
import json
|
|
22
|
+
import logging
|
|
23
|
+
import subprocess
|
|
24
|
+
import time
|
|
25
|
+
from collections.abc import Callable
|
|
26
|
+
from datetime import UTC, datetime
|
|
27
|
+
from enum import StrEnum
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
from pydantic import BaseModel, Field
|
|
32
|
+
|
|
33
|
+
_log = logging.getLogger("patchi.ai.orchestrator")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ── Pydantic Models ───────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
class ToolCall(BaseModel):
|
|
39
|
+
"""A single tool invocation with validated parameters."""
|
|
40
|
+
tool: str
|
|
41
|
+
params: dict[str, Any] = Field(default_factory=dict)
|
|
42
|
+
reason: str = ""
|
|
43
|
+
timeout_seconds: float = 120.0
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class StepResult(BaseModel):
|
|
47
|
+
"""Result of executing a single step."""
|
|
48
|
+
tool: str
|
|
49
|
+
success: bool
|
|
50
|
+
result: Any = None
|
|
51
|
+
error: str | None = None
|
|
52
|
+
duration_ms: int = 0
|
|
53
|
+
output_summary: str = ""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Plan(BaseModel):
|
|
57
|
+
"""A multi-step execution plan."""
|
|
58
|
+
goal: str
|
|
59
|
+
steps: list[ToolCall] = Field(default_factory=list)
|
|
60
|
+
reasoning: str = "" # LLM's reasoning for why these steps
|
|
61
|
+
created_at: str = Field(default_factory=lambda: datetime.now(UTC).isoformat())
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class PlanResult(BaseModel):
|
|
65
|
+
"""Complete result of executing a plan."""
|
|
66
|
+
plan: Plan
|
|
67
|
+
steps: list[StepResult] = Field(default_factory=list)
|
|
68
|
+
success: bool = False
|
|
69
|
+
total_duration_ms: int = 0
|
|
70
|
+
findings_count: int = 0
|
|
71
|
+
summary: str = ""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class IntentType(StrEnum):
|
|
75
|
+
"""Types of intent — but this is just for categorization, not routing."""
|
|
76
|
+
CHAT = "chat" # General conversation
|
|
77
|
+
ACTION = "action" # User wants to DO something
|
|
78
|
+
QUESTION = "question" # User wants to KNOW something
|
|
79
|
+
COMPLEX = "complex" # Multi-step task
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class Intent(BaseModel):
|
|
83
|
+
"""Parsed user intent — determined by LLM, not keywords."""
|
|
84
|
+
type: IntentType
|
|
85
|
+
goal: str # What the user wants to achieve
|
|
86
|
+
needs_tools: bool = False # Whether tools are needed
|
|
87
|
+
needs_files: bool = False # Whether file access is needed
|
|
88
|
+
needs_agents: bool = False # Whether agent spawning is needed
|
|
89
|
+
context: dict[str, Any] = Field(default_factory=dict) # Additional context
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# ── Brain: The Core Intelligence ──────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
class Brain:
|
|
95
|
+
"""
|
|
96
|
+
The Brain — direct access to everything.
|
|
97
|
+
|
|
98
|
+
This is what makes the chat the central orchestrator.
|
|
99
|
+
It can read files, run commands, spawn agents, and make decisions.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
def __init__(self, root: Path, on_progress: Callable[[str], None] | None = None):
|
|
103
|
+
self.root = root
|
|
104
|
+
self.on_progress = on_progress or (lambda _: None)
|
|
105
|
+
|
|
106
|
+
# ── File Operations ───────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
def read_file(self, path: str) -> str:
|
|
109
|
+
"""Read a file's contents."""
|
|
110
|
+
target = Path(path)
|
|
111
|
+
if not target.is_absolute():
|
|
112
|
+
target = self.root / target
|
|
113
|
+
try:
|
|
114
|
+
return target.read_text(encoding="utf-8")
|
|
115
|
+
except Exception as e:
|
|
116
|
+
return f"ERROR: Could not read {path}: {e}"
|
|
117
|
+
|
|
118
|
+
def write_file(self, path: str, content: str) -> dict:
|
|
119
|
+
"""Write content to a file."""
|
|
120
|
+
target = Path(path)
|
|
121
|
+
if not target.is_absolute():
|
|
122
|
+
target = self.root / target
|
|
123
|
+
try:
|
|
124
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
125
|
+
target.write_text(content, encoding="utf-8")
|
|
126
|
+
return {"success": True, "path": str(target), "size": len(content)}
|
|
127
|
+
except Exception as e:
|
|
128
|
+
return {"success": False, "error": str(e)}
|
|
129
|
+
|
|
130
|
+
def list_files(self, pattern: str = "**/*.py", max_files: int = 100) -> list[str]:
|
|
131
|
+
"""List files matching a pattern."""
|
|
132
|
+
files = []
|
|
133
|
+
for f in self.root.glob(pattern):
|
|
134
|
+
if f.is_file() and ".patchi" not in str(f) and "node_modules" not in str(f):
|
|
135
|
+
files.append(str(f.relative_to(self.root)))
|
|
136
|
+
if len(files) >= max_files:
|
|
137
|
+
break
|
|
138
|
+
return files
|
|
139
|
+
|
|
140
|
+
# ── Command Execution ─────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
def run_command(self, command: str, timeout: int = 60) -> dict:
|
|
143
|
+
"""Run a shell command and return the result."""
|
|
144
|
+
try:
|
|
145
|
+
result = subprocess.run(
|
|
146
|
+
command,
|
|
147
|
+
shell=True,
|
|
148
|
+
cwd=str(self.root),
|
|
149
|
+
capture_output=True,
|
|
150
|
+
text=True,
|
|
151
|
+
timeout=timeout,
|
|
152
|
+
)
|
|
153
|
+
return {
|
|
154
|
+
"success": result.returncode == 0,
|
|
155
|
+
"stdout": result.stdout,
|
|
156
|
+
"stderr": result.stderr,
|
|
157
|
+
"returncode": result.returncode,
|
|
158
|
+
}
|
|
159
|
+
except subprocess.TimeoutExpired:
|
|
160
|
+
return {"success": False, "error": f"Command timed out after {timeout}s"}
|
|
161
|
+
except Exception as e:
|
|
162
|
+
return {"success": False, "error": str(e)}
|
|
163
|
+
|
|
164
|
+
# ── Agent Spawning ────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
def spawn_agent(self, agent_type: str, **kwargs) -> dict:
|
|
167
|
+
"""Spawn a specialized agent for a specific task."""
|
|
168
|
+
self.on_progress(f"Spawning {agent_type} agent...")
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
if agent_type == "security":
|
|
172
|
+
from patchi.core.security.orchestrator import SecurityOrchestrator
|
|
173
|
+
orchestrator = SecurityOrchestrator(self.root)
|
|
174
|
+
result = orchestrator.run()
|
|
175
|
+
return {"success": True, "result": result}
|
|
176
|
+
|
|
177
|
+
elif agent_type == "scanner":
|
|
178
|
+
from patchi.core.scanner import scan_project
|
|
179
|
+
result = scan_project(self.root)
|
|
180
|
+
return {"success": True, "result": result}
|
|
181
|
+
|
|
182
|
+
elif agent_type == "proactive":
|
|
183
|
+
from patchi.core.brain.proactive import run_proactive
|
|
184
|
+
files = kwargs.get("files", [])
|
|
185
|
+
apply = kwargs.get("apply", False)
|
|
186
|
+
result = run_proactive(self.root, files, apply=apply)
|
|
187
|
+
return {"success": True, "result": result}
|
|
188
|
+
|
|
189
|
+
elif agent_type == "council":
|
|
190
|
+
from patchi.core.brain.council import Council
|
|
191
|
+
|
|
192
|
+
async def _deliberate():
|
|
193
|
+
council = Council(self.root, on_progress=self.on_progress)
|
|
194
|
+
session = await council.deliberate(kwargs.get("goal", ""), {})
|
|
195
|
+
return {
|
|
196
|
+
"synthesis": session.synthesis,
|
|
197
|
+
"action_plan": [
|
|
198
|
+
{"tool": s.get("tool", ""), "params": s.get("parameters", {})}
|
|
199
|
+
for s in (session.action_plan or [])
|
|
200
|
+
],
|
|
201
|
+
"consensus": session.consensus_reached,
|
|
202
|
+
"personas": [d.persona_name for d in (session.persona_decisions or [])],
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return asyncio.run(_deliberate())
|
|
206
|
+
|
|
207
|
+
elif agent_type == "smart":
|
|
208
|
+
from patchi.core.ai.smart import run_smart_agent
|
|
209
|
+
result = run_smart_agent(
|
|
210
|
+
self.root,
|
|
211
|
+
kwargs.get("goal", ""),
|
|
212
|
+
max_steps=kwargs.get("max_steps", 6),
|
|
213
|
+
)
|
|
214
|
+
return {"success": True, "result": result}
|
|
215
|
+
|
|
216
|
+
else:
|
|
217
|
+
return {"success": False, "error": f"Unknown agent type: {agent_type}"}
|
|
218
|
+
|
|
219
|
+
except Exception as e:
|
|
220
|
+
return {"success": False, "error": str(e)}
|
|
221
|
+
|
|
222
|
+
# ── Brain Access ──────────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
def get_brain_state(self) -> dict:
|
|
225
|
+
"""Get the current brain/memory state."""
|
|
226
|
+
from patchi.core import memory as mem
|
|
227
|
+
return mem.get_brain(self.root)
|
|
228
|
+
|
|
229
|
+
def get_findings(self, severity: str | None = None) -> list[dict]:
|
|
230
|
+
"""Get security findings."""
|
|
231
|
+
from patchi.core import memory as mem
|
|
232
|
+
results = mem.get_scan_results(self.root)
|
|
233
|
+
findings = []
|
|
234
|
+
for agent_name, data in results.items():
|
|
235
|
+
for f in data.get("findings", []):
|
|
236
|
+
f["agent"] = agent_name
|
|
237
|
+
if severity and f.get("severity") != severity:
|
|
238
|
+
continue
|
|
239
|
+
findings.append(f)
|
|
240
|
+
return findings
|
|
241
|
+
|
|
242
|
+
def get_layers(self) -> dict:
|
|
243
|
+
"""Get the layered brain data."""
|
|
244
|
+
from patchi.core import memory as mem
|
|
245
|
+
return mem.get_layers(self.root)
|
|
246
|
+
|
|
247
|
+
# ── Tool Execution ────────────────────────────────────────────────
|
|
248
|
+
|
|
249
|
+
def execute_tool(self, tool_name: str, params: dict) -> StepResult:
|
|
250
|
+
"""Execute a tool via the ToolExecutor."""
|
|
251
|
+
start = time.monotonic()
|
|
252
|
+
self.on_progress(f"Executing {tool_name}...")
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
from patchi.core.ai.tools.executor import CLIConfirmationProvider, ToolExecutor
|
|
256
|
+
|
|
257
|
+
async def _run():
|
|
258
|
+
executor = ToolExecutor(
|
|
259
|
+
self.root,
|
|
260
|
+
confirmation_provider=CLIConfirmationProvider(auto_confirm=True),
|
|
261
|
+
on_progress=self.on_progress,
|
|
262
|
+
)
|
|
263
|
+
return await executor.execute(tool_name, params, invoked_by="brain", skip_confirmation=True)
|
|
264
|
+
|
|
265
|
+
result = asyncio.run(_run())
|
|
266
|
+
duration = int((time.monotonic() - start) * 1000)
|
|
267
|
+
|
|
268
|
+
return StepResult(
|
|
269
|
+
tool=tool_name,
|
|
270
|
+
success=getattr(result, "success", False),
|
|
271
|
+
result=getattr(result, "result", None),
|
|
272
|
+
error=getattr(result, "error", None),
|
|
273
|
+
duration_ms=duration,
|
|
274
|
+
output_summary=self._summarize(tool_name, getattr(result, "result", None)),
|
|
275
|
+
)
|
|
276
|
+
except Exception as e:
|
|
277
|
+
duration = int((time.monotonic() - start) * 1000)
|
|
278
|
+
return StepResult(
|
|
279
|
+
tool=tool_name,
|
|
280
|
+
success=False,
|
|
281
|
+
error=str(e),
|
|
282
|
+
duration_ms=duration,
|
|
283
|
+
output_summary=f"ERROR: {e}",
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
def _summarize(self, tool: str, data: Any) -> str:
|
|
287
|
+
"""Summarize tool output for display."""
|
|
288
|
+
if not isinstance(data, dict):
|
|
289
|
+
return str(data)[:200] if data else "Done"
|
|
290
|
+
|
|
291
|
+
if tool == "scan_vulnerabilities":
|
|
292
|
+
sev = data.get("by_severity", {})
|
|
293
|
+
return f"{data.get('agent_count', 0)} agents, {data.get('total_findings', 0)} findings (C={sev.get('critical', 0)} H={sev.get('high', 0)} M={sev.get('medium', 0)})"
|
|
294
|
+
elif tool == "run_tests":
|
|
295
|
+
return f"passed={data.get('passed', 0)} failed={data.get('failed', 0)}"
|
|
296
|
+
elif tool == "attack_simulate":
|
|
297
|
+
return f"{data.get('total_findings', 0)} offensive findings"
|
|
298
|
+
elif tool == "stress_test":
|
|
299
|
+
return f"{data.get('requests', 0)} reqs @ {data.get('rps', 0)} rps"
|
|
300
|
+
elif tool == "analyze_project":
|
|
301
|
+
return f"{data.get('file_count', 0)} files, {data.get('route_count', 0)} routes"
|
|
302
|
+
elif tool == "write_file":
|
|
303
|
+
return f"Wrote {data.get('size', 0)} bytes to {data.get('path', '?')}"
|
|
304
|
+
return (data.get("message") or data.get("error") or str(data))[:200]
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# ── LLM-Powered Intent Parser ─────────────────────────────────────────
|
|
308
|
+
|
|
309
|
+
def parse_intent_llm(message: str, brain: Brain, config: dict) -> Intent:
|
|
310
|
+
"""
|
|
311
|
+
Use the LLM to parse natural language into structured intent.
|
|
312
|
+
|
|
313
|
+
This is NOT keyword matching — the LLM understands context and nuance.
|
|
314
|
+
"""
|
|
315
|
+
from patchi.core.ai.client import call_ai
|
|
316
|
+
|
|
317
|
+
# Get brain context for the LLM
|
|
318
|
+
brain.get_brain_state()
|
|
319
|
+
findings = brain.get_findings()[:10] # Top 10 findings
|
|
320
|
+
files = brain.list_files(max_files=20)
|
|
321
|
+
|
|
322
|
+
# Build the parsing prompt
|
|
323
|
+
system_prompt = """You are Patchi's intent parser. Analyze the user's message and determine:
|
|
324
|
+
|
|
325
|
+
1. intent_type: "chat" (general conversation), "action" (user wants to DO something), "question" (user wants to KNOW something), or "complex" (multi-step task)
|
|
326
|
+
2. goal: What the user wants to achieve (one sentence)
|
|
327
|
+
3. needs_tools: Whether tool execution is needed (scan, test, fix, etc.)
|
|
328
|
+
4. needs_files: Whether file reading/writing is needed
|
|
329
|
+
5. needs_agents: Whether agent spawning is needed (security, council, etc.)
|
|
330
|
+
6. context: Any relevant context (file paths, severity, etc.)
|
|
331
|
+
|
|
332
|
+
Respond with ONLY a JSON object, no other text."""
|
|
333
|
+
|
|
334
|
+
user_prompt = f"""User message: "{message}"
|
|
335
|
+
|
|
336
|
+
Project context:
|
|
337
|
+
- {len(files)} files in project
|
|
338
|
+
- {len(findings)} security findings
|
|
339
|
+
- Brain state available
|
|
340
|
+
|
|
341
|
+
Respond with JSON:
|
|
342
|
+
{{
|
|
343
|
+
"intent_type": "action|question|chat|complex",
|
|
344
|
+
"goal": "what the user wants",
|
|
345
|
+
"needs_tools": true/false,
|
|
346
|
+
"needs_files": true/false,
|
|
347
|
+
"needs_agents": true/false,
|
|
348
|
+
"context": {{}}
|
|
349
|
+
}}"""
|
|
350
|
+
|
|
351
|
+
response = call_ai(config, system_prompt, user_prompt)
|
|
352
|
+
|
|
353
|
+
if not response:
|
|
354
|
+
# Fallback to keyword-based parsing if LLM unavailable
|
|
355
|
+
return _parse_intent_fallback(message)
|
|
356
|
+
|
|
357
|
+
try:
|
|
358
|
+
# Parse LLM response
|
|
359
|
+
# Handle markdown code blocks
|
|
360
|
+
cleaned = response.strip()
|
|
361
|
+
if cleaned.startswith("```"):
|
|
362
|
+
lines = cleaned.split("\n")
|
|
363
|
+
cleaned = "\n".join(lines[1:-1])
|
|
364
|
+
if cleaned.startswith("```json"):
|
|
365
|
+
cleaned = cleaned[7:]
|
|
366
|
+
|
|
367
|
+
data = json.loads(cleaned)
|
|
368
|
+
return Intent(
|
|
369
|
+
type=IntentType(data.get("intent_type", "chat")),
|
|
370
|
+
goal=data.get("goal", message),
|
|
371
|
+
needs_tools=data.get("needs_tools", False),
|
|
372
|
+
needs_files=data.get("needs_files", False),
|
|
373
|
+
needs_agents=data.get("needs_agents", False),
|
|
374
|
+
context=data.get("context", {}),
|
|
375
|
+
)
|
|
376
|
+
except Exception as e:
|
|
377
|
+
_log.info("LLM intent parsing failed: %s, using fallback", e)
|
|
378
|
+
return _parse_intent_fallback(message)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _parse_intent_fallback(message: str) -> Intent:
|
|
382
|
+
"""Simple fallback intent parsing when LLM is unavailable."""
|
|
383
|
+
msg = message.lower().strip()
|
|
384
|
+
|
|
385
|
+
# Action words
|
|
386
|
+
action_words = ["scan", "test", "fix", "run", "execute", "build", "create", "write", "generate", "attack", "stress"]
|
|
387
|
+
is_action = any(w in msg for w in action_words)
|
|
388
|
+
|
|
389
|
+
# Question words
|
|
390
|
+
question_words = ["what", "how", "why", "where", "which", "who", "when", "is", "are", "does", "do"]
|
|
391
|
+
is_question = any(msg.startswith(w) for w in question_words)
|
|
392
|
+
|
|
393
|
+
# Multi-step indicators
|
|
394
|
+
multi_step_words = ["and then", "also", "plus", "after that", "first.*then"]
|
|
395
|
+
is_complex = any(w in msg for w in multi_step_words)
|
|
396
|
+
|
|
397
|
+
if is_complex:
|
|
398
|
+
return Intent(type=IntentType.COMPLEX, goal=message, needs_tools=True)
|
|
399
|
+
elif is_action:
|
|
400
|
+
return Intent(type=IntentType.ACTION, goal=message, needs_tools=True)
|
|
401
|
+
elif is_question:
|
|
402
|
+
return Intent(type=IntentType.QUESTION, goal=message, needs_files=True)
|
|
403
|
+
else:
|
|
404
|
+
return Intent(type=IntentType.CHAT, goal=message)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
# ── LLM-Powered Planner ───────────────────────────────────────────────
|
|
408
|
+
|
|
409
|
+
def plan_with_llm(intent: Intent, brain: Brain, config: dict) -> Plan:
|
|
410
|
+
"""
|
|
411
|
+
Use the LLM to create an execution plan.
|
|
412
|
+
|
|
413
|
+
The LLM decides which tools to use based on the intent and context.
|
|
414
|
+
"""
|
|
415
|
+
from patchi.core.ai.client import call_ai
|
|
416
|
+
|
|
417
|
+
# Get available tools
|
|
418
|
+
try:
|
|
419
|
+
from patchi.core.ai.tools.registry import get_tool_registry
|
|
420
|
+
registry = get_tool_registry()
|
|
421
|
+
tools = [t.name for t in registry.list_tools()]
|
|
422
|
+
except Exception:
|
|
423
|
+
tools = []
|
|
424
|
+
|
|
425
|
+
# Get brain context
|
|
426
|
+
brain_state = brain.get_brain_state()
|
|
427
|
+
findings = brain.get_findings()[:5]
|
|
428
|
+
|
|
429
|
+
system_prompt = f"""You are Patchi's planner. Given a user goal, create an execution plan.
|
|
430
|
+
|
|
431
|
+
Available tools: {', '.join(tools)}
|
|
432
|
+
|
|
433
|
+
For each step, specify:
|
|
434
|
+
- tool: tool name from the available list
|
|
435
|
+
- params: parameters for the tool
|
|
436
|
+
- reason: why this step is needed
|
|
437
|
+
|
|
438
|
+
Respond with ONLY a JSON object:
|
|
439
|
+
{{
|
|
440
|
+
"reasoning": "why these steps achieve the goal",
|
|
441
|
+
"steps": [
|
|
442
|
+
{{"tool": "tool_name", "params": {{}}, "reason": "why"}}
|
|
443
|
+
]
|
|
444
|
+
}}"""
|
|
445
|
+
|
|
446
|
+
user_prompt = f"""Goal: {intent.goal}
|
|
447
|
+
|
|
448
|
+
Context:
|
|
449
|
+
- {len(findings)} security findings
|
|
450
|
+
- Brain state: {json.dumps(brain_state, default=str)[:500]}
|
|
451
|
+
|
|
452
|
+
Create a plan to achieve this goal."""
|
|
453
|
+
|
|
454
|
+
response = call_ai(config, system_prompt, user_prompt)
|
|
455
|
+
|
|
456
|
+
if not response:
|
|
457
|
+
# Fallback to default plan
|
|
458
|
+
return _plan_fallback(intent)
|
|
459
|
+
|
|
460
|
+
try:
|
|
461
|
+
cleaned = response.strip()
|
|
462
|
+
if cleaned.startswith("```"):
|
|
463
|
+
lines = cleaned.split("\n")
|
|
464
|
+
cleaned = "\n".join(lines[1:-1])
|
|
465
|
+
if cleaned.startswith("```json"):
|
|
466
|
+
cleaned = cleaned[7:]
|
|
467
|
+
|
|
468
|
+
data = json.loads(cleaned)
|
|
469
|
+
steps = [
|
|
470
|
+
ToolCall(
|
|
471
|
+
tool=s["tool"],
|
|
472
|
+
params=s.get("params", {}),
|
|
473
|
+
reason=s.get("reason", ""),
|
|
474
|
+
)
|
|
475
|
+
for s in data.get("steps", [])
|
|
476
|
+
if s.get("tool") in tools # Only valid tools
|
|
477
|
+
]
|
|
478
|
+
return Plan(
|
|
479
|
+
goal=intent.goal,
|
|
480
|
+
steps=steps,
|
|
481
|
+
reasoning=data.get("reasoning", ""),
|
|
482
|
+
)
|
|
483
|
+
except Exception as e:
|
|
484
|
+
_log.info("LLM planning failed: %s, using fallback", e)
|
|
485
|
+
return _plan_fallback(intent)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def _plan_fallback(intent: Intent) -> Plan:
|
|
489
|
+
"""Simple fallback plan when LLM is unavailable."""
|
|
490
|
+
steps = []
|
|
491
|
+
|
|
492
|
+
if intent.needs_tools:
|
|
493
|
+
# Default to scan + test
|
|
494
|
+
steps.append(ToolCall(tool="scan_vulnerabilities", params={"domains": None}, reason="Security scan"))
|
|
495
|
+
steps.append(ToolCall(tool="run_tests", params={"test_types": ["unit"]}, reason="Run tests"))
|
|
496
|
+
|
|
497
|
+
if intent.needs_files:
|
|
498
|
+
# Just analyze the project
|
|
499
|
+
steps.append(ToolCall(tool="analyze_project", params={}, reason="Analyze project"))
|
|
500
|
+
|
|
501
|
+
if not steps:
|
|
502
|
+
steps.append(ToolCall(tool="analyze_project", params={}, reason="Analyze project"))
|
|
503
|
+
|
|
504
|
+
return Plan(goal=intent.goal, steps=steps, reasoning="Fallback plan")
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
# ── Orchestrator ───────────────────────────────────────────────────────
|
|
508
|
+
|
|
509
|
+
class Orchestrator:
|
|
510
|
+
"""
|
|
511
|
+
The central brain of Patchi.
|
|
512
|
+
|
|
513
|
+
The chat IS the orchestrator. It uses LLM reasoning to understand
|
|
514
|
+
requests and has direct control over everything.
|
|
515
|
+
"""
|
|
516
|
+
|
|
517
|
+
def __init__(
|
|
518
|
+
self,
|
|
519
|
+
root: Path,
|
|
520
|
+
on_progress: Callable[[str], None] | None = None,
|
|
521
|
+
on_event: Callable[[dict], None] | None = None,
|
|
522
|
+
):
|
|
523
|
+
self.root = root
|
|
524
|
+
self.on_progress = on_progress or (lambda _: None)
|
|
525
|
+
self.on_event = on_event or (lambda _: None)
|
|
526
|
+
self.brain = Brain(root, on_progress=on_progress)
|
|
527
|
+
self._config: dict | None = None
|
|
528
|
+
|
|
529
|
+
@property
|
|
530
|
+
def config(self) -> dict:
|
|
531
|
+
"""Lazy-load config."""
|
|
532
|
+
if self._config is None:
|
|
533
|
+
from patchi.core import config as cfg
|
|
534
|
+
self._config = cfg.load(self.root)
|
|
535
|
+
return self._config
|
|
536
|
+
|
|
537
|
+
async def run(self, message: str, max_steps: int = 10) -> PlanResult:
|
|
538
|
+
"""
|
|
539
|
+
Process a natural language message using LLM-powered reasoning.
|
|
540
|
+
|
|
541
|
+
This is the main entry point. The LLM decides:
|
|
542
|
+
1. What the user wants (intent parsing)
|
|
543
|
+
2. What tools to use (planning)
|
|
544
|
+
3. How to execute them (orchestration)
|
|
545
|
+
"""
|
|
546
|
+
start = time.monotonic()
|
|
547
|
+
|
|
548
|
+
# Step 1: Parse intent with LLM
|
|
549
|
+
self.on_progress("Understanding your request...")
|
|
550
|
+
intent = parse_intent_llm(message, self.brain, self.config)
|
|
551
|
+
|
|
552
|
+
# Step 2: Plan with LLM
|
|
553
|
+
self.on_progress("Planning execution...")
|
|
554
|
+
plan = plan_with_llm(intent, self.brain, self.config)
|
|
555
|
+
plan.steps = plan.steps[:max_steps]
|
|
556
|
+
|
|
557
|
+
self.on_event({
|
|
558
|
+
"event": "orchestrator.planned",
|
|
559
|
+
"data": {
|
|
560
|
+
"goal": intent.goal,
|
|
561
|
+
"steps": len(plan.steps),
|
|
562
|
+
"reasoning": plan.reasoning,
|
|
563
|
+
},
|
|
564
|
+
})
|
|
565
|
+
|
|
566
|
+
# Step 3: Execute plan
|
|
567
|
+
plan_result = PlanResult(plan=plan)
|
|
568
|
+
for i, step in enumerate(plan.steps):
|
|
569
|
+
self.on_progress(f"Step {i+1}/{len(plan.steps)}: {step.tool}")
|
|
570
|
+
result = self.brain.execute_tool(step.tool, step.params)
|
|
571
|
+
plan_result.steps.append(result)
|
|
572
|
+
|
|
573
|
+
if result.tool == "scan_vulnerabilities" and result.result:
|
|
574
|
+
plan_result.findings_count += int(result.result.get("total_findings", 0) or 0)
|
|
575
|
+
|
|
576
|
+
# Step 4: Summarize
|
|
577
|
+
plan_result.total_duration_ms = int((time.monotonic() - start) * 1000)
|
|
578
|
+
successes = sum(1 for s in plan_result.steps if s.success)
|
|
579
|
+
plan_result.success = successes == len(plan_result.steps)
|
|
580
|
+
plan_result.summary = f"{successes}/{len(plan_result.steps)} steps succeeded"
|
|
581
|
+
|
|
582
|
+
self.on_event({
|
|
583
|
+
"event": "orchestrator.completed",
|
|
584
|
+
"data": {
|
|
585
|
+
"goal": intent.goal,
|
|
586
|
+
"steps": len(plan_result.steps),
|
|
587
|
+
"success": plan_result.success,
|
|
588
|
+
"findings": plan_result.findings_count,
|
|
589
|
+
},
|
|
590
|
+
})
|
|
591
|
+
|
|
592
|
+
return plan_result
|
|
593
|
+
|
|
594
|
+
def read_file(self, path: str) -> str:
|
|
595
|
+
"""Direct file access from chat."""
|
|
596
|
+
return self.brain.read_file(path)
|
|
597
|
+
|
|
598
|
+
def write_file(self, path: str, content: str) -> dict:
|
|
599
|
+
"""Direct file writing from chat."""
|
|
600
|
+
return self.brain.write_file(path, content)
|
|
601
|
+
|
|
602
|
+
def run_command(self, command: str, timeout: int = 60) -> dict:
|
|
603
|
+
"""Direct command execution from chat."""
|
|
604
|
+
return self.brain.run_command(command, timeout)
|
|
605
|
+
|
|
606
|
+
def spawn_agent(self, agent_type: str, **kwargs) -> dict:
|
|
607
|
+
"""Direct agent spawning from chat."""
|
|
608
|
+
return self.brain.spawn_agent(agent_type, **kwargs)
|
|
609
|
+
|
|
610
|
+
def get_findings(self, severity: str | None = None) -> list[dict]:
|
|
611
|
+
"""Get security findings."""
|
|
612
|
+
return self.brain.get_findings(severity)
|
|
613
|
+
|
|
614
|
+
def get_brain_state(self) -> dict:
|
|
615
|
+
"""Get brain state."""
|
|
616
|
+
return self.brain.get_brain_state()
|
|
617
|
+
|
|
618
|
+
def get_tools(self) -> list[dict]:
|
|
619
|
+
"""Get all available tools."""
|
|
620
|
+
try:
|
|
621
|
+
from patchi.core.ai.tools.registry import get_tool_registry
|
|
622
|
+
registry = get_tool_registry()
|
|
623
|
+
return [t.to_schema() for t in registry.list_tools()]
|
|
624
|
+
except Exception:
|
|
625
|
+
return []
|