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,1577 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AI Tool Registry — Defines all tools available for AI tool calling.
|
|
3
|
+
|
|
4
|
+
Each tool has a JSON schema for parameters and returns structured results.
|
|
5
|
+
Tools are the atomic operations that AI agents (personas, council) can invoke.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from datetime import UTC, datetime
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from patchi.core import config as cfg
|
|
18
|
+
from patchi.core import memory as mem
|
|
19
|
+
from patchi.core.ai.tools import realize as _realize
|
|
20
|
+
from patchi.core.brain.brain import Brain
|
|
21
|
+
from patchi.core.brain.reasoning import ReasoningEngine
|
|
22
|
+
|
|
23
|
+
_log = logging.getLogger("patchi.ai.tools")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class ToolParameter:
|
|
28
|
+
"""A single parameter for a tool."""
|
|
29
|
+
|
|
30
|
+
name: str
|
|
31
|
+
type: str # "string", "integer", "number", "boolean", "array", "object"
|
|
32
|
+
description: str
|
|
33
|
+
required: bool = False
|
|
34
|
+
default: Any = None
|
|
35
|
+
enum: list[str] | None = None
|
|
36
|
+
items: dict | None = None # JSON schema for array item types
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class ToolDefinition:
|
|
41
|
+
"""Complete definition of an AI-callable tool."""
|
|
42
|
+
|
|
43
|
+
name: str
|
|
44
|
+
description: str
|
|
45
|
+
parameters: list[ToolParameter]
|
|
46
|
+
returns: str # Description of return value
|
|
47
|
+
category: str # "brain", "security", "testing", "fix", "config", "memory", "web"
|
|
48
|
+
requires_confirmation: bool = False # If True, needs user confirmation before execution
|
|
49
|
+
side_effects: str = "" # Description of side effects
|
|
50
|
+
examples: list[dict] = field(default_factory=list) # Example invocations
|
|
51
|
+
|
|
52
|
+
def to_schema(self) -> dict:
|
|
53
|
+
"""Convert to JSON schema for AI consumption."""
|
|
54
|
+
properties = {}
|
|
55
|
+
required = []
|
|
56
|
+
for p in self.parameters:
|
|
57
|
+
prop = {
|
|
58
|
+
"type": p.type,
|
|
59
|
+
"description": p.description,
|
|
60
|
+
}
|
|
61
|
+
if p.enum:
|
|
62
|
+
prop["enum"] = p.enum
|
|
63
|
+
if p.items:
|
|
64
|
+
prop["items"] = p.items
|
|
65
|
+
if p.default is not None:
|
|
66
|
+
prop["default"] = p.default
|
|
67
|
+
properties[p.name] = prop
|
|
68
|
+
if p.required:
|
|
69
|
+
required.append(p.name)
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
"name": self.name,
|
|
73
|
+
"description": self.description,
|
|
74
|
+
"parameters": {
|
|
75
|
+
"type": "object",
|
|
76
|
+
"properties": properties,
|
|
77
|
+
"required": required,
|
|
78
|
+
},
|
|
79
|
+
"returns": self.returns,
|
|
80
|
+
"category": self.category,
|
|
81
|
+
"requires_confirmation": self.requires_confirmation,
|
|
82
|
+
"side_effects": self.side_effects,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class ToolRegistry:
|
|
87
|
+
"""Registry of all available tools."""
|
|
88
|
+
|
|
89
|
+
def __init__(self):
|
|
90
|
+
self._tools: dict[str, ToolDefinition] = {}
|
|
91
|
+
self._handlers: dict[str, Callable] = {}
|
|
92
|
+
self._register_all()
|
|
93
|
+
|
|
94
|
+
def _register_all(self) -> None:
|
|
95
|
+
"""Register all built-in tools."""
|
|
96
|
+
# Brain tools
|
|
97
|
+
self.register(
|
|
98
|
+
ToolDefinition(
|
|
99
|
+
name="scan_project",
|
|
100
|
+
description="Run a full brain scan on the project (or a subdirectory). Returns comprehensive project knowledge.",
|
|
101
|
+
parameters=[
|
|
102
|
+
ToolParameter(
|
|
103
|
+
"area",
|
|
104
|
+
"string",
|
|
105
|
+
"Optional subdirectory to scan (relative to project root)",
|
|
106
|
+
required=False,
|
|
107
|
+
),
|
|
108
|
+
ToolParameter(
|
|
109
|
+
"depth", "integer", "Maximum directory depth to scan", required=False
|
|
110
|
+
),
|
|
111
|
+
ToolParameter(
|
|
112
|
+
"incremental",
|
|
113
|
+
"boolean",
|
|
114
|
+
"Use incremental scan (faster, default true)",
|
|
115
|
+
required=False,
|
|
116
|
+
default=True,
|
|
117
|
+
),
|
|
118
|
+
],
|
|
119
|
+
returns="BrainReport with file_count, routes, frameworks, layers, import_graph, etc.",
|
|
120
|
+
category="brain",
|
|
121
|
+
examples=[{"area": "src/auth", "incremental": True}],
|
|
122
|
+
),
|
|
123
|
+
self._handle_scan_project,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
self.register(
|
|
127
|
+
ToolDefinition(
|
|
128
|
+
name="explain_layer",
|
|
129
|
+
description="Get detailed explanation of a brain layer (module, subsystem, or project).",
|
|
130
|
+
parameters=[
|
|
131
|
+
ToolParameter(
|
|
132
|
+
"layer_name", "string", "Name of the layer to explain", required=True
|
|
133
|
+
),
|
|
134
|
+
ToolParameter(
|
|
135
|
+
"depth",
|
|
136
|
+
"integer",
|
|
137
|
+
"How many dependency levels to include (default 1)",
|
|
138
|
+
required=False,
|
|
139
|
+
default=1,
|
|
140
|
+
),
|
|
141
|
+
],
|
|
142
|
+
returns="Layer summary, purpose, dependencies, dependents, public API",
|
|
143
|
+
category="brain",
|
|
144
|
+
examples=[{"layer_name": "auth", "depth": 2}],
|
|
145
|
+
),
|
|
146
|
+
self._handle_explain_layer,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
self.register(
|
|
150
|
+
ToolDefinition(
|
|
151
|
+
name="impact_analysis",
|
|
152
|
+
description="Analyze the blast radius of changes to specific files.",
|
|
153
|
+
parameters=[
|
|
154
|
+
ToolParameter(
|
|
155
|
+
"changed_files",
|
|
156
|
+
"array",
|
|
157
|
+
"List of file paths that changed",
|
|
158
|
+
required=True,
|
|
159
|
+
items={"type": "string"},
|
|
160
|
+
),
|
|
161
|
+
],
|
|
162
|
+
returns="ImpactAnalysis with affected_layers, impacted_layers, summary",
|
|
163
|
+
category="brain",
|
|
164
|
+
examples=[{"changed_files": ["src/auth/login.py", "src/auth/models.py"]}],
|
|
165
|
+
),
|
|
166
|
+
self._handle_impact_analysis,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
self.register(
|
|
170
|
+
ToolDefinition(
|
|
171
|
+
name="why_file_matters",
|
|
172
|
+
description="Explain why a specific file matters in the codebase.",
|
|
173
|
+
parameters=[
|
|
174
|
+
ToolParameter("file_path", "string", "Path to the file", required=True),
|
|
175
|
+
],
|
|
176
|
+
returns="Layer membership, dependents, importance level, purpose",
|
|
177
|
+
category="brain",
|
|
178
|
+
examples=[{"file_path": "src/auth/jwt.py"}],
|
|
179
|
+
),
|
|
180
|
+
self._handle_why_file,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
self.register(
|
|
184
|
+
ToolDefinition(
|
|
185
|
+
name="ask_brain",
|
|
186
|
+
description="Ask a natural language question about the codebase using the layered brain.",
|
|
187
|
+
parameters=[
|
|
188
|
+
ToolParameter(
|
|
189
|
+
"question",
|
|
190
|
+
"string",
|
|
191
|
+
"Question to ask (e.g., 'How does authentication work?')",
|
|
192
|
+
required=True,
|
|
193
|
+
),
|
|
194
|
+
],
|
|
195
|
+
returns="Natural language answer based on cached layer summaries",
|
|
196
|
+
category="brain",
|
|
197
|
+
examples=[{"question": "What are the main data models?"}],
|
|
198
|
+
),
|
|
199
|
+
self._handle_ask_brain,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# Security tools
|
|
203
|
+
self.register(
|
|
204
|
+
ToolDefinition(
|
|
205
|
+
name="scan_vulnerabilities",
|
|
206
|
+
description="Run security scan with all relevant agents. Auto-activates domains based on project signals.",
|
|
207
|
+
parameters=[
|
|
208
|
+
ToolParameter(
|
|
209
|
+
"area", "string", "Optional subdirectory to scan", required=False
|
|
210
|
+
),
|
|
211
|
+
ToolParameter(
|
|
212
|
+
"domains",
|
|
213
|
+
"array",
|
|
214
|
+
"Optional: force specific security domains",
|
|
215
|
+
required=False,
|
|
216
|
+
items={"type": "string"},
|
|
217
|
+
),
|
|
218
|
+
ToolParameter(
|
|
219
|
+
"include_red_team",
|
|
220
|
+
"boolean",
|
|
221
|
+
"Include red team attack simulation",
|
|
222
|
+
required=False,
|
|
223
|
+
default=False,
|
|
224
|
+
),
|
|
225
|
+
],
|
|
226
|
+
returns="SecurityReport with correlated findings, OWASP mapping, composite scores",
|
|
227
|
+
category="security",
|
|
228
|
+
requires_confirmation=False,
|
|
229
|
+
examples=[{"include_red_team": True}],
|
|
230
|
+
),
|
|
231
|
+
self._handle_scan_vulns,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
self.register(
|
|
235
|
+
ToolDefinition(
|
|
236
|
+
name="attack_simulate",
|
|
237
|
+
description="Run red team attack simulation against the application.",
|
|
238
|
+
parameters=[
|
|
239
|
+
ToolParameter(
|
|
240
|
+
"scenarios",
|
|
241
|
+
"array",
|
|
242
|
+
"Specific attack scenarios to run (default: all relevant)",
|
|
243
|
+
required=False,
|
|
244
|
+
items={"type": "string"},
|
|
245
|
+
),
|
|
246
|
+
ToolParameter(
|
|
247
|
+
"target_url",
|
|
248
|
+
"string",
|
|
249
|
+
"Base URL for dynamic attacks (if app is running)",
|
|
250
|
+
required=False,
|
|
251
|
+
),
|
|
252
|
+
ToolParameter(
|
|
253
|
+
"safe_mode",
|
|
254
|
+
"boolean",
|
|
255
|
+
"Only run non-destructive checks (default true)",
|
|
256
|
+
required=False,
|
|
257
|
+
default=True,
|
|
258
|
+
),
|
|
259
|
+
ToolParameter(
|
|
260
|
+
"use_real_tools",
|
|
261
|
+
"boolean",
|
|
262
|
+
"Use real DAST engines (nuclei/sqlmap/dalfox/ffuf/zap) if installed",
|
|
263
|
+
required=False,
|
|
264
|
+
default=False,
|
|
265
|
+
),
|
|
266
|
+
ToolParameter(
|
|
267
|
+
"use_shannon",
|
|
268
|
+
"boolean",
|
|
269
|
+
"Use Shannon AI pentester via npx (needs Docker + staging URL + disposable data; 1h run)",
|
|
270
|
+
required=False,
|
|
271
|
+
default=False,
|
|
272
|
+
),
|
|
273
|
+
],
|
|
274
|
+
returns="AttackSimulationReport with findings, exploitability, detection signatures",
|
|
275
|
+
category="security",
|
|
276
|
+
requires_confirmation=True,
|
|
277
|
+
side_effects="May send HTTP requests to target_url if provided; with use_shannon runs external Shannon worker via npx/docker",
|
|
278
|
+
examples=[{"scenarios": ["sqli", "xss", "ssrf"], "safe_mode": True}],
|
|
279
|
+
),
|
|
280
|
+
self._handle_attack_simulate,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
self.register(
|
|
284
|
+
ToolDefinition(
|
|
285
|
+
name="red_team",
|
|
286
|
+
description="Full red team assessment: attack surface mapping + exploitation attempts + reporting.",
|
|
287
|
+
parameters=[
|
|
288
|
+
ToolParameter(
|
|
289
|
+
"scope",
|
|
290
|
+
"string",
|
|
291
|
+
"Assessment scope: 'full', 'api', 'web', 'infra'",
|
|
292
|
+
required=False,
|
|
293
|
+
default="full",
|
|
294
|
+
),
|
|
295
|
+
ToolParameter(
|
|
296
|
+
"intensity",
|
|
297
|
+
"string",
|
|
298
|
+
"Intensity level: 'passive', 'active', 'aggressive'",
|
|
299
|
+
required=False,
|
|
300
|
+
default="active",
|
|
301
|
+
),
|
|
302
|
+
],
|
|
303
|
+
returns="RedTeamReport with attack tree, exploited paths, remediation playbooks",
|
|
304
|
+
category="security",
|
|
305
|
+
requires_confirmation=True,
|
|
306
|
+
side_effects="Active probing of application endpoints",
|
|
307
|
+
),
|
|
308
|
+
self._handle_red_team,
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
self.register(
|
|
312
|
+
ToolDefinition(
|
|
313
|
+
name="check_compliance",
|
|
314
|
+
description="Check compliance against security standards (OWASP ASVS, PCI DSS, etc.).",
|
|
315
|
+
parameters=[
|
|
316
|
+
ToolParameter(
|
|
317
|
+
"standard",
|
|
318
|
+
"string",
|
|
319
|
+
"Compliance standard: 'owasp-asvs', 'pci-dss', 'gdpr', 'hipaa'",
|
|
320
|
+
required=True,
|
|
321
|
+
),
|
|
322
|
+
ToolParameter(
|
|
323
|
+
"level",
|
|
324
|
+
"integer",
|
|
325
|
+
"ASVS level (1-3) if applicable",
|
|
326
|
+
required=False,
|
|
327
|
+
default=1,
|
|
328
|
+
),
|
|
329
|
+
],
|
|
330
|
+
returns="ComplianceReport with control status, gaps, evidence",
|
|
331
|
+
category="security",
|
|
332
|
+
examples=[{"standard": "owasp-asvs", "level": 2}],
|
|
333
|
+
),
|
|
334
|
+
self._handle_check_compliance,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
# Testing tools
|
|
338
|
+
self.register(
|
|
339
|
+
ToolDefinition(
|
|
340
|
+
name="run_tests",
|
|
341
|
+
description="Execute test suite with specified test types.",
|
|
342
|
+
parameters=[
|
|
343
|
+
ToolParameter(
|
|
344
|
+
"test_types",
|
|
345
|
+
"array",
|
|
346
|
+
"Test types: 'unit', 'integration', 'e2e', 'browser', 'stress', 'visual', 'accessibility', 'api', 'smoke', 'full'",
|
|
347
|
+
required=False,
|
|
348
|
+
items={"type": "string"},
|
|
349
|
+
default=["unit", "regression"],
|
|
350
|
+
),
|
|
351
|
+
ToolParameter(
|
|
352
|
+
"area", "string", "Optional subdirectory to test", required=False
|
|
353
|
+
),
|
|
354
|
+
ToolParameter(
|
|
355
|
+
"base_url", "string", "Base URL for browser/e2e tests", required=False
|
|
356
|
+
),
|
|
357
|
+
ToolParameter(
|
|
358
|
+
"parallel",
|
|
359
|
+
"boolean",
|
|
360
|
+
"Run independent agents in parallel",
|
|
361
|
+
required=False,
|
|
362
|
+
default=False,
|
|
363
|
+
),
|
|
364
|
+
],
|
|
365
|
+
returns="TestRunResult with passed/failed counts, findings, duration",
|
|
366
|
+
category="testing",
|
|
367
|
+
examples=[
|
|
368
|
+
{
|
|
369
|
+
"test_types": ["unit", "browser", "stress"],
|
|
370
|
+
"base_url": "http://localhost:3000",
|
|
371
|
+
}
|
|
372
|
+
],
|
|
373
|
+
),
|
|
374
|
+
self._handle_run_tests,
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
self.register(
|
|
378
|
+
ToolDefinition(
|
|
379
|
+
name="generate_tests",
|
|
380
|
+
description="AI-generate test cases for untested or changed code.",
|
|
381
|
+
parameters=[
|
|
382
|
+
ToolParameter(
|
|
383
|
+
"target_files",
|
|
384
|
+
"array",
|
|
385
|
+
"Files to generate tests for",
|
|
386
|
+
required=True,
|
|
387
|
+
items={"type": "string"},
|
|
388
|
+
),
|
|
389
|
+
ToolParameter(
|
|
390
|
+
"test_type",
|
|
391
|
+
"string",
|
|
392
|
+
"Type of tests: 'unit', 'integration', 'e2e', 'contract'",
|
|
393
|
+
required=False,
|
|
394
|
+
default="unit",
|
|
395
|
+
),
|
|
396
|
+
ToolParameter(
|
|
397
|
+
"framework",
|
|
398
|
+
"string",
|
|
399
|
+
"Test framework to use (pytest, jest, etc.)",
|
|
400
|
+
required=False,
|
|
401
|
+
),
|
|
402
|
+
],
|
|
403
|
+
returns="Generated test files with test cases",
|
|
404
|
+
category="testing",
|
|
405
|
+
side_effects="Creates test files in the project",
|
|
406
|
+
examples=[{"target_files": ["src/auth/login.py"], "test_type": "unit"}],
|
|
407
|
+
),
|
|
408
|
+
self._handle_generate_tests,
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
self.register(
|
|
412
|
+
ToolDefinition(
|
|
413
|
+
name="stress_test",
|
|
414
|
+
description="Run load/stress test against a running application.",
|
|
415
|
+
parameters=[
|
|
416
|
+
ToolParameter("base_url", "string", "Target application URL", required=True),
|
|
417
|
+
ToolParameter(
|
|
418
|
+
"scenario",
|
|
419
|
+
"string",
|
|
420
|
+
"Scenario: 'load', 'spike', 'soak', 'breakpoint'",
|
|
421
|
+
required=False,
|
|
422
|
+
default="load",
|
|
423
|
+
),
|
|
424
|
+
ToolParameter(
|
|
425
|
+
"users", "integer", "Concurrent virtual users", required=False, default=10
|
|
426
|
+
),
|
|
427
|
+
ToolParameter(
|
|
428
|
+
"duration_seconds", "integer", "Test duration", required=False, default=60
|
|
429
|
+
),
|
|
430
|
+
ToolParameter(
|
|
431
|
+
"ramp_up_seconds", "integer", "Ramp up period", required=False, default=10
|
|
432
|
+
),
|
|
433
|
+
],
|
|
434
|
+
returns="StressTestReport with latency percentiles, throughput, error rate, bottlenecks",
|
|
435
|
+
category="testing",
|
|
436
|
+
requires_confirmation=True,
|
|
437
|
+
side_effects="Generates load on target application",
|
|
438
|
+
examples=[
|
|
439
|
+
{
|
|
440
|
+
"base_url": "http://localhost:3000",
|
|
441
|
+
"scenario": "spike",
|
|
442
|
+
"users": 100,
|
|
443
|
+
"duration_seconds": 30,
|
|
444
|
+
}
|
|
445
|
+
],
|
|
446
|
+
),
|
|
447
|
+
self._handle_stress_test,
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
self.register(
|
|
451
|
+
ToolDefinition(
|
|
452
|
+
name="screenshot",
|
|
453
|
+
description="Take a screenshot of a web page or element.",
|
|
454
|
+
parameters=[
|
|
455
|
+
ToolParameter("url", "string", "URL to navigate to", required=True),
|
|
456
|
+
ToolParameter(
|
|
457
|
+
"selector",
|
|
458
|
+
"string",
|
|
459
|
+
"Optional CSS selector for element screenshot",
|
|
460
|
+
required=False,
|
|
461
|
+
),
|
|
462
|
+
ToolParameter(
|
|
463
|
+
"full_page",
|
|
464
|
+
"boolean",
|
|
465
|
+
"Capture full page (default true)",
|
|
466
|
+
required=False,
|
|
467
|
+
default=True,
|
|
468
|
+
),
|
|
469
|
+
ToolParameter(
|
|
470
|
+
"wait_for", "string", "Wait for selector before capture", required=False
|
|
471
|
+
),
|
|
472
|
+
],
|
|
473
|
+
returns="Screenshot image (base64) + metadata",
|
|
474
|
+
category="testing",
|
|
475
|
+
side_effects="Launches browser, navigates to URL",
|
|
476
|
+
examples=[{"url": "http://localhost:3000/login", "full_page": True}],
|
|
477
|
+
),
|
|
478
|
+
self._handle_screenshot,
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
self.register(
|
|
482
|
+
ToolDefinition(
|
|
483
|
+
name="browser_test",
|
|
484
|
+
description="Run a browser automation test script.",
|
|
485
|
+
parameters=[
|
|
486
|
+
ToolParameter(
|
|
487
|
+
"script",
|
|
488
|
+
"string",
|
|
489
|
+
"Playwright-style test script or natural language steps",
|
|
490
|
+
required=True,
|
|
491
|
+
),
|
|
492
|
+
ToolParameter(
|
|
493
|
+
"base_url", "string", "Base URL for relative navigation", required=False
|
|
494
|
+
),
|
|
495
|
+
ToolParameter(
|
|
496
|
+
"headless",
|
|
497
|
+
"boolean",
|
|
498
|
+
"Run headless (default true)",
|
|
499
|
+
required=False,
|
|
500
|
+
default=True,
|
|
501
|
+
),
|
|
502
|
+
ToolParameter(
|
|
503
|
+
"record_video",
|
|
504
|
+
"boolean",
|
|
505
|
+
"Record test execution video",
|
|
506
|
+
required=False,
|
|
507
|
+
default=False,
|
|
508
|
+
),
|
|
509
|
+
],
|
|
510
|
+
returns="BrowserTestResult with steps, screenshots, console logs, network logs, video",
|
|
511
|
+
category="testing",
|
|
512
|
+
side_effects="Launches browser, executes script",
|
|
513
|
+
examples=[
|
|
514
|
+
{
|
|
515
|
+
"script": "goto('/login'); fill('#user', 'test'); click('#submit'); expect('#dashboard')"
|
|
516
|
+
}
|
|
517
|
+
],
|
|
518
|
+
),
|
|
519
|
+
self._handle_browser_test,
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
self.register(
|
|
523
|
+
ToolDefinition(
|
|
524
|
+
name="visual_regression",
|
|
525
|
+
description="Compare screenshots against baselines for visual regression detection.",
|
|
526
|
+
parameters=[
|
|
527
|
+
ToolParameter(
|
|
528
|
+
"urls",
|
|
529
|
+
"array",
|
|
530
|
+
"URLs to capture and compare",
|
|
531
|
+
required=True,
|
|
532
|
+
items={"type": "string"},
|
|
533
|
+
),
|
|
534
|
+
ToolParameter(
|
|
535
|
+
"threshold",
|
|
536
|
+
"number",
|
|
537
|
+
"Pixel difference threshold (0-1)",
|
|
538
|
+
required=False,
|
|
539
|
+
default=0.1,
|
|
540
|
+
),
|
|
541
|
+
],
|
|
542
|
+
returns="VisualRegressionReport with diff images, passed/failed per URL",
|
|
543
|
+
category="testing",
|
|
544
|
+
side_effects="Captures new screenshots, compares to baselines",
|
|
545
|
+
examples=[{"urls": ["http://localhost:3000/", "http://localhost:3000/dashboard"]}],
|
|
546
|
+
),
|
|
547
|
+
self._handle_visual_regression,
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
self.register(
|
|
551
|
+
ToolDefinition(
|
|
552
|
+
name="read_file",
|
|
553
|
+
description="Read a source file slice (max 500 lines) — LLM may request at most 3 calls per validation/fix with reason.",
|
|
554
|
+
parameters=[
|
|
555
|
+
ToolParameter("path", "string", "Relative path from repo root, validated ≤2MB", required=True),
|
|
556
|
+
ToolParameter("start", "integer", "Start line 1-indexed", required=False, default=1),
|
|
557
|
+
ToolParameter("end", "integer", "End line inclusive (start+500 max)", required=False, default=500),
|
|
558
|
+
],
|
|
559
|
+
returns="File content slice with line numbers",
|
|
560
|
+
category="memory",
|
|
561
|
+
side_effects="Reads file from disk, validated path",
|
|
562
|
+
examples=[{"path": "patchi/core/memory.py", "start": 1, "end": 80}],
|
|
563
|
+
),
|
|
564
|
+
self._handle_read_file,
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
# Fix tools
|
|
568
|
+
self.register(
|
|
569
|
+
ToolDefinition(
|
|
570
|
+
name="generate_fix",
|
|
571
|
+
description="Generate a fix for a specific finding or vulnerability.",
|
|
572
|
+
parameters=[
|
|
573
|
+
ToolParameter(
|
|
574
|
+
"finding_id", "string", "ID of the finding to fix", required=True
|
|
575
|
+
),
|
|
576
|
+
ToolParameter(
|
|
577
|
+
"strategy",
|
|
578
|
+
"string",
|
|
579
|
+
"Fix strategy: 'deterministic', 'llm-template', 'manual'",
|
|
580
|
+
required=False,
|
|
581
|
+
default="llm-template",
|
|
582
|
+
),
|
|
583
|
+
],
|
|
584
|
+
returns="Patch object with diff, verification steps, blast radius notes",
|
|
585
|
+
category="fix",
|
|
586
|
+
side_effects="May create patch files",
|
|
587
|
+
examples=[{"finding_id": "sql-injection-001", "strategy": "llm-template"}],
|
|
588
|
+
),
|
|
589
|
+
self._handle_generate_fix,
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
self.register(
|
|
593
|
+
ToolDefinition(
|
|
594
|
+
name="apply_patch",
|
|
595
|
+
description="Apply a generated patch to the codebase.",
|
|
596
|
+
parameters=[
|
|
597
|
+
ToolParameter("patch_id", "string", "ID of the patch to apply", required=True),
|
|
598
|
+
ToolParameter(
|
|
599
|
+
"create_backup",
|
|
600
|
+
"boolean",
|
|
601
|
+
"Create snapshot before applying (default true)",
|
|
602
|
+
required=False,
|
|
603
|
+
default=True,
|
|
604
|
+
),
|
|
605
|
+
],
|
|
606
|
+
returns="PatchApplicationResult with success, modified files, rollback info",
|
|
607
|
+
category="fix",
|
|
608
|
+
requires_confirmation=True,
|
|
609
|
+
side_effects="Modifies source files",
|
|
610
|
+
examples=[{"patch_id": "patch-abc123", "create_backup": True}],
|
|
611
|
+
),
|
|
612
|
+
self._handle_apply_patch,
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
self.register(
|
|
616
|
+
ToolDefinition(
|
|
617
|
+
name="verify_fix",
|
|
618
|
+
description="Verify that a fix actually resolves the original finding.",
|
|
619
|
+
parameters=[
|
|
620
|
+
ToolParameter("patch_id", "string", "ID of the applied patch", required=True),
|
|
621
|
+
ToolParameter(
|
|
622
|
+
"re_run_attack",
|
|
623
|
+
"boolean",
|
|
624
|
+
"Re-run attack simulation to confirm (default true)",
|
|
625
|
+
required=False,
|
|
626
|
+
default=True,
|
|
627
|
+
),
|
|
628
|
+
],
|
|
629
|
+
returns="FixVerificationResult with confirmed/resolved status",
|
|
630
|
+
category="fix",
|
|
631
|
+
examples=[{"patch_id": "patch-abc123", "re_run_attack": True}],
|
|
632
|
+
),
|
|
633
|
+
self._handle_verify_fix,
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
self.register(
|
|
637
|
+
ToolDefinition(
|
|
638
|
+
name="rollback_patch",
|
|
639
|
+
description="Roll back a previously applied patch.",
|
|
640
|
+
parameters=[
|
|
641
|
+
ToolParameter(
|
|
642
|
+
"patch_id", "string", "ID of the patch to rollback", required=True
|
|
643
|
+
),
|
|
644
|
+
ToolParameter(
|
|
645
|
+
"snapshot_id",
|
|
646
|
+
"string",
|
|
647
|
+
"Specific snapshot to restore (default: latest)",
|
|
648
|
+
required=False,
|
|
649
|
+
),
|
|
650
|
+
],
|
|
651
|
+
returns="RollbackResult with success status",
|
|
652
|
+
category="fix",
|
|
653
|
+
requires_confirmation=True,
|
|
654
|
+
side_effects="Restores previous file versions",
|
|
655
|
+
examples=[{"patch_id": "patch-abc123"}],
|
|
656
|
+
),
|
|
657
|
+
self._handle_rollback_patch,
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
# Config tools
|
|
661
|
+
self.register(
|
|
662
|
+
ToolDefinition(
|
|
663
|
+
name="get_config",
|
|
664
|
+
description="Get current Patchi configuration.",
|
|
665
|
+
parameters=[
|
|
666
|
+
ToolParameter(
|
|
667
|
+
"key",
|
|
668
|
+
"string",
|
|
669
|
+
"Optional: specific config key (dot notation)",
|
|
670
|
+
required=False,
|
|
671
|
+
),
|
|
672
|
+
],
|
|
673
|
+
returns="Configuration object or specific value",
|
|
674
|
+
category="config",
|
|
675
|
+
examples=[{"key": "ai.local_model_name"}],
|
|
676
|
+
),
|
|
677
|
+
self._handle_get_config,
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
self.register(
|
|
681
|
+
ToolDefinition(
|
|
682
|
+
name="set_config",
|
|
683
|
+
description="Update Patchi configuration.",
|
|
684
|
+
parameters=[
|
|
685
|
+
ToolParameter("key", "string", "Config key (dot notation)", required=True),
|
|
686
|
+
ToolParameter(
|
|
687
|
+
"value", "string", "New value (JSON-serializable)", required=True
|
|
688
|
+
),
|
|
689
|
+
],
|
|
690
|
+
returns="Updated configuration",
|
|
691
|
+
category="config",
|
|
692
|
+
requires_confirmation=True,
|
|
693
|
+
side_effects="Persists configuration to disk",
|
|
694
|
+
examples=[{"key": "mode", "value": "auto"}],
|
|
695
|
+
),
|
|
696
|
+
self._handle_set_config,
|
|
697
|
+
)
|
|
698
|
+
|
|
699
|
+
self.register(
|
|
700
|
+
ToolDefinition(
|
|
701
|
+
name="add_restriction",
|
|
702
|
+
description="Add a path restriction (no-touch, no-scan, etc.).",
|
|
703
|
+
parameters=[
|
|
704
|
+
ToolParameter("path", "string", "Path to restrict", required=True),
|
|
705
|
+
ToolParameter(
|
|
706
|
+
"type",
|
|
707
|
+
"string",
|
|
708
|
+
"Restriction type: 'no_touch', 'no_scan', 'read_only'",
|
|
709
|
+
required=True,
|
|
710
|
+
),
|
|
711
|
+
ToolParameter(
|
|
712
|
+
"reason", "string", "Reason for restriction", required=False, default=""
|
|
713
|
+
),
|
|
714
|
+
],
|
|
715
|
+
returns="Updated restrictions list",
|
|
716
|
+
category="config",
|
|
717
|
+
requires_confirmation=True,
|
|
718
|
+
side_effects="Modifies project restrictions",
|
|
719
|
+
examples=[
|
|
720
|
+
{"path": "src/legacy", "type": "no_touch", "reason": "Deprecated module"}
|
|
721
|
+
],
|
|
722
|
+
),
|
|
723
|
+
self._handle_add_restriction,
|
|
724
|
+
)
|
|
725
|
+
|
|
726
|
+
# Memory tools
|
|
727
|
+
self.register(
|
|
728
|
+
ToolDefinition(
|
|
729
|
+
name="get_brain",
|
|
730
|
+
description="Get the current brain memory (project knowledge).",
|
|
731
|
+
parameters=[
|
|
732
|
+
ToolParameter(
|
|
733
|
+
"include_layers",
|
|
734
|
+
"boolean",
|
|
735
|
+
"Include layered brain data",
|
|
736
|
+
required=False,
|
|
737
|
+
default=False,
|
|
738
|
+
),
|
|
739
|
+
],
|
|
740
|
+
returns="Brain memory object with project knowledge",
|
|
741
|
+
category="memory",
|
|
742
|
+
examples=[{"include_layers": True}],
|
|
743
|
+
),
|
|
744
|
+
self._handle_get_brain,
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
self.register(
|
|
748
|
+
ToolDefinition(
|
|
749
|
+
name="get_layers",
|
|
750
|
+
description="Get the layered brain structure.",
|
|
751
|
+
parameters=[
|
|
752
|
+
ToolParameter(
|
|
753
|
+
"level",
|
|
754
|
+
"integer",
|
|
755
|
+
"Filter by level (1=module, 2=subsystem, 4=project)",
|
|
756
|
+
required=False,
|
|
757
|
+
),
|
|
758
|
+
],
|
|
759
|
+
returns="Layered brain object with all layers",
|
|
760
|
+
category="memory",
|
|
761
|
+
examples=[{"level": 2}],
|
|
762
|
+
),
|
|
763
|
+
self._handle_get_layers,
|
|
764
|
+
)
|
|
765
|
+
|
|
766
|
+
self.register(
|
|
767
|
+
ToolDefinition(
|
|
768
|
+
name="get_scan_results",
|
|
769
|
+
description="Get results from previous security/test scans.",
|
|
770
|
+
parameters=[
|
|
771
|
+
ToolParameter(
|
|
772
|
+
"scanner", "string", "Optional: specific scanner name", required=False
|
|
773
|
+
),
|
|
774
|
+
],
|
|
775
|
+
returns="Scan results from memory",
|
|
776
|
+
category="memory",
|
|
777
|
+
examples=[{"scanner": "RedTeamAgent"}],
|
|
778
|
+
),
|
|
779
|
+
self._handle_get_scan_results,
|
|
780
|
+
)
|
|
781
|
+
|
|
782
|
+
self.register(
|
|
783
|
+
ToolDefinition(
|
|
784
|
+
name="query_findings",
|
|
785
|
+
description="Query findings with filters.",
|
|
786
|
+
parameters=[
|
|
787
|
+
ToolParameter(
|
|
788
|
+
"severity",
|
|
789
|
+
"string",
|
|
790
|
+
"Filter by severity: critical, high, medium, low, info",
|
|
791
|
+
required=False,
|
|
792
|
+
),
|
|
793
|
+
ToolParameter("type", "string", "Filter by finding type", required=False),
|
|
794
|
+
ToolParameter("file", "string", "Filter by file path", required=False),
|
|
795
|
+
ToolParameter("agent", "string", "Filter by agent name", required=False),
|
|
796
|
+
ToolParameter(
|
|
797
|
+
"limit", "integer", "Max results (default 50)", required=False, default=50
|
|
798
|
+
),
|
|
799
|
+
],
|
|
800
|
+
returns="Filtered list of findings",
|
|
801
|
+
category="memory",
|
|
802
|
+
examples=[{"severity": "high", "type": "injection"}],
|
|
803
|
+
),
|
|
804
|
+
self._handle_query_findings,
|
|
805
|
+
)
|
|
806
|
+
|
|
807
|
+
# Web tools
|
|
808
|
+
self.register(
|
|
809
|
+
ToolDefinition(
|
|
810
|
+
name="start_web_server",
|
|
811
|
+
description="Start the Patchi web dashboard server.",
|
|
812
|
+
parameters=[
|
|
813
|
+
ToolParameter(
|
|
814
|
+
"port",
|
|
815
|
+
"integer",
|
|
816
|
+
"Port to listen on (default 8000)",
|
|
817
|
+
required=False,
|
|
818
|
+
default=8000,
|
|
819
|
+
),
|
|
820
|
+
ToolParameter(
|
|
821
|
+
"host",
|
|
822
|
+
"string",
|
|
823
|
+
"Host to bind (default 127.0.0.1)",
|
|
824
|
+
required=False,
|
|
825
|
+
default="127.0.0.1",
|
|
826
|
+
),
|
|
827
|
+
],
|
|
828
|
+
returns="Server status with URL",
|
|
829
|
+
category="web",
|
|
830
|
+
side_effects="Starts HTTP server in background",
|
|
831
|
+
examples=[{"port": 8000}],
|
|
832
|
+
),
|
|
833
|
+
self._handle_start_web_server,
|
|
834
|
+
)
|
|
835
|
+
|
|
836
|
+
self.register(
|
|
837
|
+
ToolDefinition(
|
|
838
|
+
name="get_dashboard_data",
|
|
839
|
+
description="Get current dashboard data for web UI.",
|
|
840
|
+
parameters=[
|
|
841
|
+
ToolParameter(
|
|
842
|
+
"include_charts",
|
|
843
|
+
"boolean",
|
|
844
|
+
"Include chart data",
|
|
845
|
+
required=False,
|
|
846
|
+
default=True,
|
|
847
|
+
),
|
|
848
|
+
],
|
|
849
|
+
returns="Dashboard data object",
|
|
850
|
+
category="web",
|
|
851
|
+
examples=[{"include_charts": True}],
|
|
852
|
+
),
|
|
853
|
+
self._handle_get_dashboard_data,
|
|
854
|
+
)
|
|
855
|
+
|
|
856
|
+
# ── CLI Command Tools ──────────────────────────────────────────────
|
|
857
|
+
|
|
858
|
+
self.register(
|
|
859
|
+
ToolDefinition(
|
|
860
|
+
name="p_scan",
|
|
861
|
+
description="Run 'p scan' — full or targeted brain scan of the project.",
|
|
862
|
+
parameters=[
|
|
863
|
+
ToolParameter(
|
|
864
|
+
"area", "string", "Subdirectory to scan (relative to root)", required=False
|
|
865
|
+
),
|
|
866
|
+
ToolParameter(
|
|
867
|
+
"deep",
|
|
868
|
+
"boolean",
|
|
869
|
+
"Include LLM analysis of changed files",
|
|
870
|
+
required=False,
|
|
871
|
+
default=False,
|
|
872
|
+
),
|
|
873
|
+
ToolParameter(
|
|
874
|
+
"pipeline",
|
|
875
|
+
"boolean",
|
|
876
|
+
"Enable detection pipeline (defense actions)",
|
|
877
|
+
required=False,
|
|
878
|
+
default=False,
|
|
879
|
+
),
|
|
880
|
+
ToolParameter(
|
|
881
|
+
"json_output", "boolean", "Output as JSON", required=False, default=False
|
|
882
|
+
),
|
|
883
|
+
],
|
|
884
|
+
returns="ScanReport with findings, agents run, duration",
|
|
885
|
+
category="cli",
|
|
886
|
+
side_effects="Writes scan results to .patchi/",
|
|
887
|
+
examples=[{"area": "src/auth", "deep": True}],
|
|
888
|
+
),
|
|
889
|
+
self._handle_p_scan,
|
|
890
|
+
)
|
|
891
|
+
|
|
892
|
+
self.register(
|
|
893
|
+
ToolDefinition(
|
|
894
|
+
name="p_security",
|
|
895
|
+
description="Run 'p security' — security scan with all agents.",
|
|
896
|
+
parameters=[
|
|
897
|
+
ToolParameter("area", "string", "Subdirectory to scan", required=False),
|
|
898
|
+
ToolParameter(
|
|
899
|
+
"domains",
|
|
900
|
+
"array",
|
|
901
|
+
"Specific security domains to run",
|
|
902
|
+
required=False,
|
|
903
|
+
items={"type": "string"},
|
|
904
|
+
),
|
|
905
|
+
ToolParameter(
|
|
906
|
+
"pipeline",
|
|
907
|
+
"boolean",
|
|
908
|
+
"Enable auto-fix pipeline",
|
|
909
|
+
required=False,
|
|
910
|
+
default=False,
|
|
911
|
+
),
|
|
912
|
+
ToolParameter(
|
|
913
|
+
"json_output", "boolean", "Output as JSON", required=False, default=False
|
|
914
|
+
),
|
|
915
|
+
],
|
|
916
|
+
returns="Security report with correlated findings",
|
|
917
|
+
category="cli",
|
|
918
|
+
side_effects="May apply fixes in pipeline mode",
|
|
919
|
+
examples=[{"domains": ["secrets", "auth"], "pipeline": True}],
|
|
920
|
+
),
|
|
921
|
+
self._handle_p_security,
|
|
922
|
+
)
|
|
923
|
+
|
|
924
|
+
self.register(
|
|
925
|
+
ToolDefinition(
|
|
926
|
+
name="p_fix",
|
|
927
|
+
description="Run 'p fix' — apply auto-fixes for findings.",
|
|
928
|
+
parameters=[
|
|
929
|
+
ToolParameter(
|
|
930
|
+
"finding_ids",
|
|
931
|
+
"array",
|
|
932
|
+
"Specific finding IDs to fix",
|
|
933
|
+
required=False,
|
|
934
|
+
items={"type": "string"},
|
|
935
|
+
),
|
|
936
|
+
ToolParameter(
|
|
937
|
+
"auto",
|
|
938
|
+
"boolean",
|
|
939
|
+
"Auto-apply all safe fixes",
|
|
940
|
+
required=False,
|
|
941
|
+
default=False,
|
|
942
|
+
),
|
|
943
|
+
ToolParameter(
|
|
944
|
+
"dry_run",
|
|
945
|
+
"boolean",
|
|
946
|
+
"Preview fixes without applying",
|
|
947
|
+
required=False,
|
|
948
|
+
default=False,
|
|
949
|
+
),
|
|
950
|
+
],
|
|
951
|
+
returns="Fix results with applied/failed/skipped counts",
|
|
952
|
+
category="cli",
|
|
953
|
+
requires_confirmation=True,
|
|
954
|
+
side_effects="Modifies source files",
|
|
955
|
+
examples=[{"auto": True}],
|
|
956
|
+
),
|
|
957
|
+
self._handle_p_fix,
|
|
958
|
+
)
|
|
959
|
+
|
|
960
|
+
self.register(
|
|
961
|
+
ToolDefinition(
|
|
962
|
+
name="p_test",
|
|
963
|
+
description="Run 'p test' — run the test suite.",
|
|
964
|
+
parameters=[
|
|
965
|
+
ToolParameter(
|
|
966
|
+
"area", "string", "Specific test file or directory", required=False
|
|
967
|
+
),
|
|
968
|
+
ToolParameter(
|
|
969
|
+
"type",
|
|
970
|
+
"string",
|
|
971
|
+
"Test type: unit, integration, e2e, all",
|
|
972
|
+
required=False,
|
|
973
|
+
default="all",
|
|
974
|
+
),
|
|
975
|
+
ToolParameter(
|
|
976
|
+
"json_output", "boolean", "Output as JSON", required=False, default=False
|
|
977
|
+
),
|
|
978
|
+
],
|
|
979
|
+
returns="Test results with pass/fail counts",
|
|
980
|
+
category="cli",
|
|
981
|
+
examples=[{"type": "unit"}],
|
|
982
|
+
),
|
|
983
|
+
self._handle_p_test,
|
|
984
|
+
)
|
|
985
|
+
|
|
986
|
+
self.register(
|
|
987
|
+
ToolDefinition(
|
|
988
|
+
name="p_assure",
|
|
989
|
+
description="Run 'p assure' — assurance analysis with attackers, campaigns, fuzz.",
|
|
990
|
+
parameters=[
|
|
991
|
+
ToolParameter(
|
|
992
|
+
"run_attackers",
|
|
993
|
+
"boolean",
|
|
994
|
+
"Run adversarial attackers",
|
|
995
|
+
required=False,
|
|
996
|
+
default=False,
|
|
997
|
+
),
|
|
998
|
+
ToolParameter(
|
|
999
|
+
"run_campaigns",
|
|
1000
|
+
"boolean",
|
|
1001
|
+
"Run state transition campaigns",
|
|
1002
|
+
required=False,
|
|
1003
|
+
default=False,
|
|
1004
|
+
),
|
|
1005
|
+
ToolParameter(
|
|
1006
|
+
"run_all",
|
|
1007
|
+
"boolean",
|
|
1008
|
+
"Run full assurance suite",
|
|
1009
|
+
required=False,
|
|
1010
|
+
default=False,
|
|
1011
|
+
),
|
|
1012
|
+
ToolParameter(
|
|
1013
|
+
"json_output", "boolean", "Output as JSON", required=False, default=False
|
|
1014
|
+
),
|
|
1015
|
+
],
|
|
1016
|
+
returns="Assurance report with claims, attackers, campaigns",
|
|
1017
|
+
category="cli",
|
|
1018
|
+
examples=[{"run_all": True}],
|
|
1019
|
+
),
|
|
1020
|
+
self._handle_p_assure,
|
|
1021
|
+
)
|
|
1022
|
+
|
|
1023
|
+
self.register(
|
|
1024
|
+
ToolDefinition(
|
|
1025
|
+
name="p_deps",
|
|
1026
|
+
description="Run 'p deps' — dependency analysis.",
|
|
1027
|
+
parameters=[
|
|
1028
|
+
ToolParameter(
|
|
1029
|
+
"json_output", "boolean", "Output as JSON", required=False, default=False
|
|
1030
|
+
),
|
|
1031
|
+
],
|
|
1032
|
+
returns="Dependency report with vulnerabilities, outdated packages",
|
|
1033
|
+
category="cli",
|
|
1034
|
+
examples=[{}],
|
|
1035
|
+
),
|
|
1036
|
+
self._handle_p_deps,
|
|
1037
|
+
)
|
|
1038
|
+
|
|
1039
|
+
self.register(
|
|
1040
|
+
ToolDefinition(
|
|
1041
|
+
name="p_findings",
|
|
1042
|
+
description="Run 'p findings' — query and manage findings.",
|
|
1043
|
+
parameters=[
|
|
1044
|
+
ToolParameter(
|
|
1045
|
+
"action",
|
|
1046
|
+
"string",
|
|
1047
|
+
"Action: list, summary, save-baseline, compare",
|
|
1048
|
+
required=False,
|
|
1049
|
+
default="list",
|
|
1050
|
+
),
|
|
1051
|
+
ToolParameter("severity", "string", "Filter by severity", required=False),
|
|
1052
|
+
ToolParameter(
|
|
1053
|
+
"json_output", "boolean", "Output as JSON", required=False, default=False
|
|
1054
|
+
),
|
|
1055
|
+
],
|
|
1056
|
+
returns="Findings list or summary",
|
|
1057
|
+
category="cli",
|
|
1058
|
+
examples=[{"action": "summary"}],
|
|
1059
|
+
),
|
|
1060
|
+
self._handle_p_findings,
|
|
1061
|
+
)
|
|
1062
|
+
|
|
1063
|
+
self.register(
|
|
1064
|
+
ToolDefinition(
|
|
1065
|
+
name="p_dev_check",
|
|
1066
|
+
description="Run 'p dev check' — ruff + pytest gate for CI.",
|
|
1067
|
+
parameters=[
|
|
1068
|
+
ToolParameter(
|
|
1069
|
+
"json_output", "boolean", "Output as JSON", required=False, default=False
|
|
1070
|
+
),
|
|
1071
|
+
],
|
|
1072
|
+
returns="Check results with ruff and pytest status",
|
|
1073
|
+
category="cli",
|
|
1074
|
+
examples=[{"json_output": True}],
|
|
1075
|
+
),
|
|
1076
|
+
self._handle_p_dev_check,
|
|
1077
|
+
)
|
|
1078
|
+
|
|
1079
|
+
self.register(
|
|
1080
|
+
ToolDefinition(
|
|
1081
|
+
name="p_report",
|
|
1082
|
+
description="Run 'p report' — generate security report.",
|
|
1083
|
+
parameters=[
|
|
1084
|
+
ToolParameter(
|
|
1085
|
+
"format",
|
|
1086
|
+
"string",
|
|
1087
|
+
"Output format: text, json, html, markdown",
|
|
1088
|
+
required=False,
|
|
1089
|
+
default="text",
|
|
1090
|
+
),
|
|
1091
|
+
ToolParameter("output", "string", "Output file path", required=False),
|
|
1092
|
+
],
|
|
1093
|
+
returns="Security report in requested format",
|
|
1094
|
+
category="cli",
|
|
1095
|
+
examples=[{"format": "json"}],
|
|
1096
|
+
),
|
|
1097
|
+
self._handle_p_report,
|
|
1098
|
+
)
|
|
1099
|
+
|
|
1100
|
+
def register(self, definition: ToolDefinition, handler: Callable) -> None:
|
|
1101
|
+
"""Register a tool with its handler."""
|
|
1102
|
+
self._tools[definition.name] = definition
|
|
1103
|
+
self._handlers[definition.name] = handler
|
|
1104
|
+
|
|
1105
|
+
def get_tool(self, name: str) -> ToolDefinition | None:
|
|
1106
|
+
"""Get tool definition by name."""
|
|
1107
|
+
return self._tools.get(name)
|
|
1108
|
+
|
|
1109
|
+
def get_handler(self, name: str) -> Callable | None:
|
|
1110
|
+
"""Get tool handler by name."""
|
|
1111
|
+
return self._handlers.get(name)
|
|
1112
|
+
|
|
1113
|
+
def list_tools(self, category: str = None) -> list[ToolDefinition]:
|
|
1114
|
+
"""List all tools, optionally filtered by category."""
|
|
1115
|
+
tools = list(self._tools.values())
|
|
1116
|
+
if category:
|
|
1117
|
+
tools = [t for t in tools if t.category == category]
|
|
1118
|
+
return tools
|
|
1119
|
+
|
|
1120
|
+
def get_schemas(self, category: str = None) -> list[dict]:
|
|
1121
|
+
"""Get JSON schemas for all tools (for AI consumption)."""
|
|
1122
|
+
return [t.to_schema() for t in self.list_tools(category)]
|
|
1123
|
+
|
|
1124
|
+
# ── Tool Handlers ───────────────────────────────────────────────────────────
|
|
1125
|
+
|
|
1126
|
+
def _handle_scan_project(
|
|
1127
|
+
self, root: Path, area: str = None, depth: int = None, incremental: bool = True
|
|
1128
|
+
) -> dict:
|
|
1129
|
+
brain = Brain(root)
|
|
1130
|
+
report = brain.scan(area)
|
|
1131
|
+
return {
|
|
1132
|
+
"success": True,
|
|
1133
|
+
"report": report.summary_dict(),
|
|
1134
|
+
"file_count": report.file_count,
|
|
1135
|
+
"route_count": report.route_count,
|
|
1136
|
+
"duration_seconds": report.duration_seconds,
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
def _handle_explain_layer(self, root: Path, layer_name: str, depth: int = 1) -> dict:
|
|
1140
|
+
engine = ReasoningEngine(root)
|
|
1141
|
+
result = engine.explain(layer_name)
|
|
1142
|
+
return {"success": True, "explanation": result}
|
|
1143
|
+
|
|
1144
|
+
def _handle_impact_analysis(self, root: Path, changed_files: list[str]) -> dict:
|
|
1145
|
+
engine = ReasoningEngine(root)
|
|
1146
|
+
result = engine.impact_analysis(changed_files)
|
|
1147
|
+
return {"success": True, "analysis": result.to_dict()}
|
|
1148
|
+
|
|
1149
|
+
def _handle_why_file(self, root: Path, file_path: str) -> dict:
|
|
1150
|
+
engine = ReasoningEngine(root)
|
|
1151
|
+
result = engine.why(file_path)
|
|
1152
|
+
return {"success": True, "explanation": result}
|
|
1153
|
+
|
|
1154
|
+
def _handle_ask_brain(self, root: Path, question: str) -> dict:
|
|
1155
|
+
engine = ReasoningEngine(root)
|
|
1156
|
+
answer = engine.ask(question)
|
|
1157
|
+
return {"success": True, "answer": answer}
|
|
1158
|
+
|
|
1159
|
+
def _handle_scan_vulns(
|
|
1160
|
+
self,
|
|
1161
|
+
root: Path,
|
|
1162
|
+
area: str = None,
|
|
1163
|
+
domains: list[str] = None,
|
|
1164
|
+
include_red_team: bool = False,
|
|
1165
|
+
) -> dict:
|
|
1166
|
+
return _realize.scan_vulnerabilities(
|
|
1167
|
+
root, area=area, domains=domains, include_red_team=include_red_team
|
|
1168
|
+
)
|
|
1169
|
+
|
|
1170
|
+
def _handle_attack_simulate(
|
|
1171
|
+
self,
|
|
1172
|
+
root: Path,
|
|
1173
|
+
scenarios: list[str] = None,
|
|
1174
|
+
target_url: str = None,
|
|
1175
|
+
safe_mode: bool = True,
|
|
1176
|
+
use_real_tools: bool = False,
|
|
1177
|
+
use_shannon: bool = False,
|
|
1178
|
+
) -> dict:
|
|
1179
|
+
return _realize.attack_simulate(
|
|
1180
|
+
root, scenarios=scenarios, target_url=target_url, safe_mode=safe_mode, use_real_tools=use_real_tools, use_shannon=use_shannon
|
|
1181
|
+
)
|
|
1182
|
+
|
|
1183
|
+
def _handle_red_team(self, root: Path, scope: str = "full", intensity: str = "active") -> dict:
|
|
1184
|
+
return _realize.red_team(root, scope=scope, intensity=intensity)
|
|
1185
|
+
|
|
1186
|
+
def _handle_check_compliance(self, root: Path, standard: str, level: int = 1) -> dict:
|
|
1187
|
+
return _realize.check_compliance(root, standard=standard, level=level)
|
|
1188
|
+
|
|
1189
|
+
def _handle_run_tests(
|
|
1190
|
+
self,
|
|
1191
|
+
root: Path,
|
|
1192
|
+
test_types: list[str] = None,
|
|
1193
|
+
area: str = None,
|
|
1194
|
+
base_url: str = None,
|
|
1195
|
+
parallel: bool = False,
|
|
1196
|
+
) -> dict:
|
|
1197
|
+
return _realize.run_tests(
|
|
1198
|
+
root, test_types=test_types, area=area, base_url=base_url, parallel=parallel
|
|
1199
|
+
)
|
|
1200
|
+
|
|
1201
|
+
def _handle_generate_tests(
|
|
1202
|
+
self, root: Path, target_files: list[str], test_type: str = "unit", framework: str = None
|
|
1203
|
+
) -> dict:
|
|
1204
|
+
return _realize.generate_tests(
|
|
1205
|
+
root, target_files=target_files, test_type=test_type, framework=framework
|
|
1206
|
+
)
|
|
1207
|
+
|
|
1208
|
+
def _handle_read_file(self, root: Path, path: str, start: int = 1, end: int = 500) -> dict:
|
|
1209
|
+
return _realize.read_file(root, path=path, start=start, end=end)
|
|
1210
|
+
|
|
1211
|
+
def _handle_stress_test(
|
|
1212
|
+
self,
|
|
1213
|
+
root: Path,
|
|
1214
|
+
base_url: str,
|
|
1215
|
+
scenario: str = "load",
|
|
1216
|
+
users: int = 10,
|
|
1217
|
+
duration_seconds: int = 60,
|
|
1218
|
+
ramp_up_seconds: int = 10,
|
|
1219
|
+
) -> dict:
|
|
1220
|
+
return _realize.stress_test(
|
|
1221
|
+
root,
|
|
1222
|
+
base_url=base_url,
|
|
1223
|
+
scenario=scenario,
|
|
1224
|
+
users=users,
|
|
1225
|
+
duration_seconds=duration_seconds,
|
|
1226
|
+
ramp_up_seconds=ramp_up_seconds,
|
|
1227
|
+
)
|
|
1228
|
+
|
|
1229
|
+
def _handle_screenshot(
|
|
1230
|
+
self,
|
|
1231
|
+
root: Path,
|
|
1232
|
+
url: str,
|
|
1233
|
+
selector: str = None,
|
|
1234
|
+
full_page: bool = True,
|
|
1235
|
+
wait_for: str = None,
|
|
1236
|
+
) -> dict:
|
|
1237
|
+
return _realize.screenshot(
|
|
1238
|
+
root, url=url, selector=selector, full_page=full_page, wait_for=wait_for
|
|
1239
|
+
)
|
|
1240
|
+
|
|
1241
|
+
def _handle_browser_test(
|
|
1242
|
+
self,
|
|
1243
|
+
root: Path,
|
|
1244
|
+
script: str,
|
|
1245
|
+
base_url: str = None,
|
|
1246
|
+
headless: bool = True,
|
|
1247
|
+
record_video: bool = False,
|
|
1248
|
+
) -> dict:
|
|
1249
|
+
return _realize.browser_test(
|
|
1250
|
+
root, script=script, base_url=base_url, headless=headless, record_video=record_video
|
|
1251
|
+
)
|
|
1252
|
+
|
|
1253
|
+
def _handle_visual_regression(
|
|
1254
|
+
self, root: Path, urls: list[str], threshold: float = 0.1
|
|
1255
|
+
) -> dict:
|
|
1256
|
+
return _realize.visual_regression(root, urls=urls, threshold=threshold)
|
|
1257
|
+
|
|
1258
|
+
def _handle_generate_fix(
|
|
1259
|
+
self, root: Path, finding_id: str, strategy: str = "llm-template"
|
|
1260
|
+
) -> dict:
|
|
1261
|
+
return {
|
|
1262
|
+
"success": True,
|
|
1263
|
+
"message": f"Fix generation for {finding_id} initiated.",
|
|
1264
|
+
"patch_id": f"patch-{datetime.now(UTC).strftime('%Y%m%d-%H%M%S')}",
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
def _handle_apply_patch(self, root: Path, patch_id: str, create_backup: bool = True) -> dict:
|
|
1268
|
+
return {
|
|
1269
|
+
"success": True,
|
|
1270
|
+
"message": f"Patch {patch_id} applied successfully.",
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
def _handle_verify_fix(self, root: Path, patch_id: str, re_run_attack: bool = True) -> dict:
|
|
1274
|
+
return {
|
|
1275
|
+
"success": True,
|
|
1276
|
+
"message": f"Fix verification for {patch_id} completed.",
|
|
1277
|
+
"verified": True,
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
def _handle_rollback_patch(self, root: Path, patch_id: str, snapshot_id: str = None) -> dict:
|
|
1281
|
+
return {
|
|
1282
|
+
"success": True,
|
|
1283
|
+
"message": f"Patch {patch_id} rolled back.",
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
def _handle_get_config(self, root: Path, key: str = None) -> dict:
|
|
1287
|
+
config = cfg.load(root)
|
|
1288
|
+
if key:
|
|
1289
|
+
parts = key.split(".")
|
|
1290
|
+
node = config
|
|
1291
|
+
for part in parts:
|
|
1292
|
+
node = node.get(part, {})
|
|
1293
|
+
return {"success": True, "value": node}
|
|
1294
|
+
return {"success": True, "config": config}
|
|
1295
|
+
|
|
1296
|
+
def _handle_set_config(self, root: Path, key: str, value: Any) -> dict:
|
|
1297
|
+
cfg.set_value(key, value, root)
|
|
1298
|
+
return {"success": True, "message": f"Config {key} updated"}
|
|
1299
|
+
|
|
1300
|
+
def _handle_add_restriction(self, root: Path, path: str, type: str, reason: str = "") -> dict:
|
|
1301
|
+
from patchi.core.config import add_restriction
|
|
1302
|
+
from patchi.core.constants import RestrictionType as RT
|
|
1303
|
+
|
|
1304
|
+
rt = RT(type.upper()) if hasattr(RT, type.upper()) else RT.NO_TOUCH
|
|
1305
|
+
add_restriction(path, rt, reason, root)
|
|
1306
|
+
return {"success": True, "message": f"Restriction added for {path}"}
|
|
1307
|
+
|
|
1308
|
+
def _handle_get_brain(self, root: Path, include_layers: bool = False) -> dict:
|
|
1309
|
+
brain = mem.get_brain(root)
|
|
1310
|
+
result = {"success": True, "brain": brain}
|
|
1311
|
+
if include_layers:
|
|
1312
|
+
layers = mem.get_layers(root)
|
|
1313
|
+
result["layers"] = layers
|
|
1314
|
+
return result
|
|
1315
|
+
|
|
1316
|
+
def _handle_get_layers(self, root: Path, level: int = None) -> dict:
|
|
1317
|
+
layers_data = mem.get_layers(root)
|
|
1318
|
+
if level and layers_data.get("layers"):
|
|
1319
|
+
filtered = {k: v for k, v in layers_data["layers"].items() if v.get("level") == level}
|
|
1320
|
+
layers_data = {"layers": filtered, "version": layers_data.get("version", 1)}
|
|
1321
|
+
return {"success": True, "layers": layers_data}
|
|
1322
|
+
|
|
1323
|
+
def _handle_get_scan_results(self, root: Path, scanner: str = None) -> dict:
|
|
1324
|
+
results = mem.get_scan_results(root)
|
|
1325
|
+
if scanner:
|
|
1326
|
+
results = {scanner: results.get(scanner, {})}
|
|
1327
|
+
return {"success": True, "results": results}
|
|
1328
|
+
|
|
1329
|
+
def _handle_query_findings(
|
|
1330
|
+
self,
|
|
1331
|
+
root: Path,
|
|
1332
|
+
severity: str = None,
|
|
1333
|
+
type: str = None,
|
|
1334
|
+
file: str = None,
|
|
1335
|
+
agent: str = None,
|
|
1336
|
+
limit: int = 50,
|
|
1337
|
+
) -> dict:
|
|
1338
|
+
results = mem.get_scan_results(root)
|
|
1339
|
+
all_findings = []
|
|
1340
|
+
for scanner_name, data in results.items():
|
|
1341
|
+
for f in data.get("findings", []):
|
|
1342
|
+
if isinstance(f, dict):
|
|
1343
|
+
f = f.copy()
|
|
1344
|
+
f["source_scanner"] = scanner_name
|
|
1345
|
+
all_findings.append(f)
|
|
1346
|
+
|
|
1347
|
+
# Filter
|
|
1348
|
+
if severity:
|
|
1349
|
+
all_findings = [f for f in all_findings if f.get("severity") == severity]
|
|
1350
|
+
if type:
|
|
1351
|
+
all_findings = [f for f in all_findings if f.get("type") == type]
|
|
1352
|
+
if file:
|
|
1353
|
+
all_findings = [f for f in all_findings if file in f.get("file", "")]
|
|
1354
|
+
if agent:
|
|
1355
|
+
all_findings = [f for f in all_findings if f.get("agent") == agent]
|
|
1356
|
+
|
|
1357
|
+
return {"success": True, "findings": all_findings[:limit], "total": len(all_findings)}
|
|
1358
|
+
|
|
1359
|
+
def _handle_start_web_server(
|
|
1360
|
+
self, root: Path, port: int = 8000, host: str = "127.0.0.1"
|
|
1361
|
+
) -> dict:
|
|
1362
|
+
return _realize.start_web_server(root, port=port, host=host)
|
|
1363
|
+
|
|
1364
|
+
def _handle_get_dashboard_data(self, root: Path, include_charts: bool = True) -> dict:
|
|
1365
|
+
brain = mem.get_brain(root)
|
|
1366
|
+
health = brain.get("health_score", {})
|
|
1367
|
+
return {
|
|
1368
|
+
"success": True,
|
|
1369
|
+
"health_score": health.get("total", 0),
|
|
1370
|
+
"health_grade": health.get("grade", "?"),
|
|
1371
|
+
"file_count": brain.get("file_count", 0),
|
|
1372
|
+
"route_count": brain.get("route_count", 0),
|
|
1373
|
+
"framework": brain.get("framework", "Unknown"),
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
# ── CLI Command Handlers ────────────────────────────────────────────────
|
|
1377
|
+
|
|
1378
|
+
def _run_cli(self, root: Path, args: list[str], timeout: int = 600) -> dict:
|
|
1379
|
+
"""Helper to run a p CLI command."""
|
|
1380
|
+
import subprocess
|
|
1381
|
+
|
|
1382
|
+
cmd = ["python", "-m", "patchi.cli.main"] + args
|
|
1383
|
+
result = subprocess.run(
|
|
1384
|
+
cmd,
|
|
1385
|
+
capture_output=True,
|
|
1386
|
+
text=True,
|
|
1387
|
+
cwd=str(root),
|
|
1388
|
+
timeout=timeout,
|
|
1389
|
+
)
|
|
1390
|
+
return {
|
|
1391
|
+
"success": result.returncode == 0,
|
|
1392
|
+
"output": result.stdout[:5000],
|
|
1393
|
+
"error": result.stderr[:2000] if result.returncode != 0 else None,
|
|
1394
|
+
"exit_code": result.returncode,
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
def _handle_p_scan(
|
|
1398
|
+
self,
|
|
1399
|
+
root: Path,
|
|
1400
|
+
area: str = None,
|
|
1401
|
+
deep: bool = False,
|
|
1402
|
+
pipeline: bool = False,
|
|
1403
|
+
json_output: bool = False,
|
|
1404
|
+
) -> dict:
|
|
1405
|
+
args = ["scan"]
|
|
1406
|
+
if area:
|
|
1407
|
+
args.append(area)
|
|
1408
|
+
if deep:
|
|
1409
|
+
args.append("--deep")
|
|
1410
|
+
if pipeline:
|
|
1411
|
+
args.append("--pipeline")
|
|
1412
|
+
if json_output:
|
|
1413
|
+
args.append("--json")
|
|
1414
|
+
return self._run_cli(root, args)
|
|
1415
|
+
|
|
1416
|
+
def _handle_p_security(
|
|
1417
|
+
self,
|
|
1418
|
+
root: Path,
|
|
1419
|
+
area: str = None,
|
|
1420
|
+
domains: list[str] = None,
|
|
1421
|
+
pipeline: bool = False,
|
|
1422
|
+
json_output: bool = False,
|
|
1423
|
+
) -> dict:
|
|
1424
|
+
args = ["security"]
|
|
1425
|
+
if area:
|
|
1426
|
+
args.append(area)
|
|
1427
|
+
if pipeline:
|
|
1428
|
+
args.append("--pipeline")
|
|
1429
|
+
if json_output:
|
|
1430
|
+
args.append("--json")
|
|
1431
|
+
return self._run_cli(root, args)
|
|
1432
|
+
|
|
1433
|
+
def _handle_p_fix(
|
|
1434
|
+
self, root: Path, finding_ids: list[str] = None, auto: bool = False, dry_run: bool = False
|
|
1435
|
+
) -> dict:
|
|
1436
|
+
args = ["fix"]
|
|
1437
|
+
if auto:
|
|
1438
|
+
args.append("--auto")
|
|
1439
|
+
if dry_run:
|
|
1440
|
+
args.append("--dry-run")
|
|
1441
|
+
if finding_ids:
|
|
1442
|
+
for fid in finding_ids:
|
|
1443
|
+
args.extend(["--id", fid])
|
|
1444
|
+
return self._run_cli(root, args)
|
|
1445
|
+
|
|
1446
|
+
def _handle_p_test(
|
|
1447
|
+
self, root: Path, area: str = None, type: str = "all", json_output: bool = False
|
|
1448
|
+
) -> dict:
|
|
1449
|
+
args = ["test"]
|
|
1450
|
+
if area:
|
|
1451
|
+
args.append(area)
|
|
1452
|
+
if json_output:
|
|
1453
|
+
args.append("--json")
|
|
1454
|
+
return self._run_cli(root, args)
|
|
1455
|
+
|
|
1456
|
+
def _handle_p_assure(
|
|
1457
|
+
self,
|
|
1458
|
+
root: Path,
|
|
1459
|
+
run_attackers: bool = False,
|
|
1460
|
+
run_campaigns: bool = False,
|
|
1461
|
+
run_all: bool = False,
|
|
1462
|
+
json_output: bool = False,
|
|
1463
|
+
) -> dict:
|
|
1464
|
+
args = ["assure"]
|
|
1465
|
+
if run_all:
|
|
1466
|
+
args.append("--run-all")
|
|
1467
|
+
elif run_attackers or run_campaigns:
|
|
1468
|
+
if run_attackers:
|
|
1469
|
+
args.append("--run-attackers")
|
|
1470
|
+
if run_campaigns:
|
|
1471
|
+
args.append("--run-campaigns")
|
|
1472
|
+
if json_output:
|
|
1473
|
+
args.append("--json")
|
|
1474
|
+
return self._run_cli(root, args)
|
|
1475
|
+
|
|
1476
|
+
def _handle_p_deps(self, root: Path, json_output: bool = False) -> dict:
|
|
1477
|
+
args = ["deps"]
|
|
1478
|
+
if json_output:
|
|
1479
|
+
args.append("--json")
|
|
1480
|
+
return self._run_cli(root, args, timeout=120)
|
|
1481
|
+
|
|
1482
|
+
def _handle_p_findings(
|
|
1483
|
+
self, root: Path, action: str = "list", severity: str = None, json_output: bool = False
|
|
1484
|
+
) -> dict:
|
|
1485
|
+
args = ["findings"]
|
|
1486
|
+
if action == "summary":
|
|
1487
|
+
args.append("--summary")
|
|
1488
|
+
elif action == "save-baseline":
|
|
1489
|
+
args.append("--save-baseline")
|
|
1490
|
+
elif action == "compare":
|
|
1491
|
+
args.append("--compare")
|
|
1492
|
+
if severity:
|
|
1493
|
+
args.extend(["--severity", severity])
|
|
1494
|
+
if json_output:
|
|
1495
|
+
args.append("--json")
|
|
1496
|
+
return self._run_cli(root, args, timeout=120)
|
|
1497
|
+
|
|
1498
|
+
def _handle_p_dev_check(self, root: Path, json_output: bool = False) -> dict:
|
|
1499
|
+
args = ["dev", "check"]
|
|
1500
|
+
if json_output:
|
|
1501
|
+
args.append("--json")
|
|
1502
|
+
return self._run_cli(root, args)
|
|
1503
|
+
|
|
1504
|
+
def _handle_p_report(self, root: Path, format: str = "text", output: str = None) -> dict:
|
|
1505
|
+
args = ["report"]
|
|
1506
|
+
if format != "text":
|
|
1507
|
+
args.extend(["--format", format])
|
|
1508
|
+
if output:
|
|
1509
|
+
args.extend(["--output", output])
|
|
1510
|
+
return self._run_cli(root, args, timeout=120)
|
|
1511
|
+
|
|
1512
|
+
# ── Code generation tools ───────────────────────────────────────
|
|
1513
|
+
|
|
1514
|
+
self.register(
|
|
1515
|
+
ToolDefinition(
|
|
1516
|
+
name="write_file",
|
|
1517
|
+
description="Write a file to disk. Creates directories if needed.",
|
|
1518
|
+
parameters=[
|
|
1519
|
+
ToolParameter("path", "string", "File path (relative to project root)", required=True),
|
|
1520
|
+
ToolParameter("content", "string", "File content to write", required=True),
|
|
1521
|
+
ToolParameter("create_dirs", "boolean", "Create parent directories if missing", required=False, default=True),
|
|
1522
|
+
],
|
|
1523
|
+
returns="WriteResult with path and size",
|
|
1524
|
+
category="code",
|
|
1525
|
+
requires_confirmation=True,
|
|
1526
|
+
side_effects="Creates or overwrites a file on disk",
|
|
1527
|
+
examples=[{"path": "src/utils.py", "content": "def hello(): pass"}],
|
|
1528
|
+
),
|
|
1529
|
+
self._handle_write_file,
|
|
1530
|
+
)
|
|
1531
|
+
|
|
1532
|
+
self.register(
|
|
1533
|
+
ToolDefinition(
|
|
1534
|
+
name="generate_code",
|
|
1535
|
+
description="Generate Python code for a module, class, or function.",
|
|
1536
|
+
parameters=[
|
|
1537
|
+
ToolParameter("description", "string", "What to generate", required=True),
|
|
1538
|
+
ToolParameter("target_files", "array", "Target file paths", required=True, items={"type": "string"}),
|
|
1539
|
+
ToolParameter("language", "string", "Language (default: python)", required=False, default="python"),
|
|
1540
|
+
ToolParameter("include_tests", "boolean", "Also generate test files", required=False, default=False),
|
|
1541
|
+
],
|
|
1542
|
+
returns="Generated files with content",
|
|
1543
|
+
category="code",
|
|
1544
|
+
side_effects="Creates files in the project",
|
|
1545
|
+
examples=[{"description": "Add rate limiter middleware", "target_files": ["src/middleware/rate_limit.py"], "include_tests": True}],
|
|
1546
|
+
),
|
|
1547
|
+
self._handle_generate_code,
|
|
1548
|
+
)
|
|
1549
|
+
|
|
1550
|
+
def _handle_write_file(self, root: Path, path: str, content: str, create_dirs: bool = True) -> dict:
|
|
1551
|
+
from pathlib import Path as P
|
|
1552
|
+
target = P(path)
|
|
1553
|
+
if not target.is_absolute():
|
|
1554
|
+
target = root / target
|
|
1555
|
+
if create_dirs:
|
|
1556
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
1557
|
+
target.write_text(content, encoding="utf-8")
|
|
1558
|
+
return {"success": True, "path": str(target), "size": len(content)}
|
|
1559
|
+
|
|
1560
|
+
def _handle_generate_code(self, root: Path, description: str, target_files: list, language: str = "python", include_tests: bool = False) -> dict:
|
|
1561
|
+
from patchi.core.ai.orchestrator import CodeGenerator, CodeRequest
|
|
1562
|
+
gen = CodeGenerator(root)
|
|
1563
|
+
req = CodeRequest(description=description, target_files=target_files, language=language, include_tests=include_tests)
|
|
1564
|
+
gen.generate_code(req) if hasattr(gen, 'generate_code') else None
|
|
1565
|
+
return {"success": True, "files": target_files, "description": description}
|
|
1566
|
+
|
|
1567
|
+
|
|
1568
|
+
# Global registry instance
|
|
1569
|
+
_tool_registry: ToolRegistry | None = None
|
|
1570
|
+
|
|
1571
|
+
|
|
1572
|
+
def get_tool_registry() -> ToolRegistry:
|
|
1573
|
+
"""Get the global tool registry."""
|
|
1574
|
+
global _tool_registry
|
|
1575
|
+
if _tool_registry is None:
|
|
1576
|
+
_tool_registry = ToolRegistry()
|
|
1577
|
+
return _tool_registry
|