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,95 @@
|
|
|
1
|
+
"""
|
|
2
|
+
I18nAgent §11.7 — Internationalization Audit.
|
|
3
|
+
|
|
4
|
+
Finds hardcoded strings in UI code, missing translation keys, untranslated strings
|
|
5
|
+
via tree-sitter scan for string literals in JSX/TSX/Vue/Svelte.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
|
|
12
|
+
from patchi.core.agents.base import (
|
|
13
|
+
AgentGroup,
|
|
14
|
+
AgentInput,
|
|
15
|
+
AgentResult,
|
|
16
|
+
AgentStatus,
|
|
17
|
+
BaseAgent,
|
|
18
|
+
Severity,
|
|
19
|
+
make_finding,
|
|
20
|
+
register,
|
|
21
|
+
safe_rglob,
|
|
22
|
+
)
|
|
23
|
+
from patchi.core.brain.code_query import (
|
|
24
|
+
js_calls,
|
|
25
|
+
js_jsx_texts,
|
|
26
|
+
lang_for_file,
|
|
27
|
+
parse_js,
|
|
28
|
+
vue_template_texts,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
_log = logging.getLogger("patchi.agents.i18n")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _looks_hardcoded(text: str) -> bool:
|
|
35
|
+
words = text.split()
|
|
36
|
+
return len(text) > 10 and 2 <= len(words) <= 5 and text[0].isupper()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _has_i18n(tree, lang: str, raw: str) -> bool:
|
|
40
|
+
for call in js_calls(tree, lang):
|
|
41
|
+
if call.name == "useTranslation":
|
|
42
|
+
return True
|
|
43
|
+
if call.full.startswith("i18n."):
|
|
44
|
+
return True
|
|
45
|
+
if call.name == "t" and call.arg_kinds[:1] == ["string"]:
|
|
46
|
+
return True
|
|
47
|
+
markers = ("useTranslation", "i18n", "$t(", "t('", 't("')
|
|
48
|
+
return any(m in raw for m in markers)
|
|
49
|
+
|
|
50
|
+
@register
|
|
51
|
+
class I18nAgent(BaseAgent):
|
|
52
|
+
group = AgentGroup.SCANNER
|
|
53
|
+
name = "I18nAgent"
|
|
54
|
+
description = "i18n audit §11.7 — hardcoded strings vs translation keys"
|
|
55
|
+
timeout = 60
|
|
56
|
+
|
|
57
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
58
|
+
findings=[]
|
|
59
|
+
for pat in ("*.jsx","*.tsx","*.vue","*.svelte"):
|
|
60
|
+
for fp in safe_rglob(inp.root, pat):
|
|
61
|
+
rel=fp.relative_to(inp.root).as_posix()
|
|
62
|
+
if "node_modules" in rel:
|
|
63
|
+
continue
|
|
64
|
+
try:
|
|
65
|
+
txt=fp.read_text(encoding="utf-8", errors="replace")
|
|
66
|
+
except OSError:
|
|
67
|
+
continue
|
|
68
|
+
if fp.suffix in (".vue", ".svelte"):
|
|
69
|
+
texts = vue_template_texts(txt)
|
|
70
|
+
markers = ("useTranslation", "i18n", "$t(", "t('", 't("')
|
|
71
|
+
has_i18n = any(m in txt for m in markers)
|
|
72
|
+
else:
|
|
73
|
+
lang = lang_for_file(rel)
|
|
74
|
+
tree = parse_js(txt, lang)
|
|
75
|
+
if tree is None:
|
|
76
|
+
continue
|
|
77
|
+
texts = js_jsx_texts(tree, lang)
|
|
78
|
+
has_i18n = _has_i18n(tree, lang, txt)
|
|
79
|
+
if has_i18n:
|
|
80
|
+
continue
|
|
81
|
+
for s, line in texts:
|
|
82
|
+
if _looks_hardcoded(s):
|
|
83
|
+
findings.append(make_finding(
|
|
84
|
+
severity=Severity.LOW, file=rel, line_start=line,
|
|
85
|
+
title=f"Hardcoded UI string '{s[:30]}' without i18n",
|
|
86
|
+
description="Use t('key') / $t() / i18n key; check i18next-scanner",
|
|
87
|
+
finding_type="i18n_hardcoded",
|
|
88
|
+
))
|
|
89
|
+
break
|
|
90
|
+
if len(findings) >= 20:
|
|
91
|
+
break
|
|
92
|
+
if len(findings) >= 20:
|
|
93
|
+
break
|
|
94
|
+
result.status=AgentStatus.SUCCEEDED
|
|
95
|
+
result.findings=findings[:20]
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""LicenseComplianceAgent — checks dependency licenses against policy.
|
|
2
|
+
|
|
3
|
+
Covers §11.6:
|
|
4
|
+
- Parse package.json, pyproject.toml, Cargo.toml, go.mod, Gemfile
|
|
5
|
+
- Extract license field from each dependency
|
|
6
|
+
- Flag GPL/AGPL licenses (restrictive) and missing license fields
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import re
|
|
13
|
+
import urllib.request
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from patchi.core.constants import is_offline
|
|
17
|
+
|
|
18
|
+
from .base import (
|
|
19
|
+
AgentGroup,
|
|
20
|
+
AgentInput,
|
|
21
|
+
AgentResult,
|
|
22
|
+
AgentStatus,
|
|
23
|
+
BaseAgent,
|
|
24
|
+
Severity,
|
|
25
|
+
make_finding,
|
|
26
|
+
register,
|
|
27
|
+
safe_rglob,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
_RESTRICTED_LICENSES = {
|
|
31
|
+
"gpl",
|
|
32
|
+
"gpl-2.0",
|
|
33
|
+
"gpl-3.0",
|
|
34
|
+
"gplv2",
|
|
35
|
+
"gplv3",
|
|
36
|
+
"agpl",
|
|
37
|
+
"agpl-3.0",
|
|
38
|
+
"agplv3",
|
|
39
|
+
"lgpl",
|
|
40
|
+
"lgpl-3.0",
|
|
41
|
+
"cc-by-nc",
|
|
42
|
+
"cc-by-nc-nd",
|
|
43
|
+
"proprietary",
|
|
44
|
+
"commercial",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
_ALLOWED_LICENSES = {
|
|
48
|
+
"mit",
|
|
49
|
+
"apache-2.0",
|
|
50
|
+
"apache",
|
|
51
|
+
"bsd-2-clause",
|
|
52
|
+
"bsd-3-clause",
|
|
53
|
+
"isc",
|
|
54
|
+
"unlicense",
|
|
55
|
+
"cc0-1.0",
|
|
56
|
+
"cc-by-4.0",
|
|
57
|
+
"python-2.0",
|
|
58
|
+
"zlib",
|
|
59
|
+
"mpl-2.0",
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
_UNKNOWN_LICENSE_RISK = {
|
|
63
|
+
"gpl",
|
|
64
|
+
"agpl",
|
|
65
|
+
"lgpl",
|
|
66
|
+
"unknown",
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
_log = logging.getLogger("patchi.agents.license_compliance")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _normalize_license(license_str: str) -> str:
|
|
74
|
+
return (
|
|
75
|
+
license_str.strip()
|
|
76
|
+
.strip('"')
|
|
77
|
+
.strip("'")
|
|
78
|
+
.lower()
|
|
79
|
+
.replace("-or-later", "")
|
|
80
|
+
.replace("-only", "")
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _check_license(license_str: str | None) -> tuple[str, str]:
|
|
85
|
+
"""Returns (status, normalized)."""
|
|
86
|
+
if not license_str:
|
|
87
|
+
return "missing", "unknown"
|
|
88
|
+
norm = _normalize_license(license_str)
|
|
89
|
+
if norm in _RESTRICTED_LICENSES:
|
|
90
|
+
return "restricted", norm
|
|
91
|
+
if norm in _ALLOWED_LICENSES:
|
|
92
|
+
return "allowed", norm
|
|
93
|
+
if "gpl" in norm or "affero" in norm:
|
|
94
|
+
return "restricted", norm
|
|
95
|
+
return "unknown", norm
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# Cache for lookup results
|
|
99
|
+
_license_cache: dict[str, str] = {}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _lookup_license(package_name: str, eco: str) -> str | None:
|
|
103
|
+
if is_offline():
|
|
104
|
+
return None
|
|
105
|
+
cache_key = f"{eco}:{package_name}"
|
|
106
|
+
if cache_key in _license_cache:
|
|
107
|
+
return _license_cache[cache_key]
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
if eco == "npm":
|
|
111
|
+
url = f"https://registry.npmjs.org/{package_name}/latest"
|
|
112
|
+
req = urllib.request.Request(url, headers={"User-Agent": "Patchi/1.0"})
|
|
113
|
+
with urllib.request.urlopen(req, timeout=5) as resp:
|
|
114
|
+
data = json.loads(resp.read().decode())
|
|
115
|
+
lic = data.get("license")
|
|
116
|
+
if lic:
|
|
117
|
+
_license_cache[cache_key] = lic
|
|
118
|
+
return lic
|
|
119
|
+
except Exception as e:
|
|
120
|
+
_log.warning("_lookup_license failed: %s", e)
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _extract_deps(root: Path) -> list[dict]:
|
|
125
|
+
deps: list[dict] = []
|
|
126
|
+
# package.json
|
|
127
|
+
for fp in safe_rglob(root, "package.json"):
|
|
128
|
+
try:
|
|
129
|
+
data = json.loads(fp.read_text(encoding="utf-8"))
|
|
130
|
+
for name in {**data.get("dependencies", {}), **data.get("devDependencies", {})}:
|
|
131
|
+
lic = data.get("license") or _lookup_license(name, "npm")
|
|
132
|
+
deps.append(
|
|
133
|
+
{
|
|
134
|
+
"name": name,
|
|
135
|
+
"license": lic,
|
|
136
|
+
"ecosystem": "npm",
|
|
137
|
+
"file": fp.relative_to(root).as_posix(),
|
|
138
|
+
}
|
|
139
|
+
)
|
|
140
|
+
except Exception as e:
|
|
141
|
+
_log.warning("_extract_deps failed: %s", e)
|
|
142
|
+
# pyproject.toml
|
|
143
|
+
for fp in safe_rglob(root, "pyproject.toml"):
|
|
144
|
+
try:
|
|
145
|
+
content = fp.read_text(encoding="utf-8")
|
|
146
|
+
m = re.search(r'license\s*=\s*["\']([^"\']+)["\']', content)
|
|
147
|
+
lic = m.group(1) if m else None
|
|
148
|
+
for line in content.splitlines():
|
|
149
|
+
stripped = line.strip()
|
|
150
|
+
if stripped.startswith('"') and ">" in stripped:
|
|
151
|
+
name = stripped.split(">")[0].strip('"').strip("'").split("[")[0].strip()
|
|
152
|
+
if name and not name.startswith("python") and not name.startswith("#"):
|
|
153
|
+
deps.append(
|
|
154
|
+
{
|
|
155
|
+
"name": name,
|
|
156
|
+
"license": lic,
|
|
157
|
+
"ecosystem": "pypi",
|
|
158
|
+
"file": fp.relative_to(root).as_posix(),
|
|
159
|
+
}
|
|
160
|
+
)
|
|
161
|
+
except Exception as e:
|
|
162
|
+
_log.warning("_extract_deps failed: %s", e)
|
|
163
|
+
return deps
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@register
|
|
167
|
+
class LicenseComplianceAgent(BaseAgent):
|
|
168
|
+
"""Scans dependencies for license compliance and flags restricted licenses."""
|
|
169
|
+
|
|
170
|
+
group = AgentGroup.SCANNER
|
|
171
|
+
name = "LicenseComplianceAgent"
|
|
172
|
+
description = "Check dependency licenses against policy — flag GPL/AGPL, missing, unknown"
|
|
173
|
+
|
|
174
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
175
|
+
deps = _extract_deps(inp.root)
|
|
176
|
+
result.data["total_deps"] = len(deps)
|
|
177
|
+
result.data["deps"] = deps
|
|
178
|
+
|
|
179
|
+
restricted: list[dict] = []
|
|
180
|
+
missing: list[dict] = []
|
|
181
|
+
unknown: list[dict] = []
|
|
182
|
+
|
|
183
|
+
for dep in deps:
|
|
184
|
+
status, norm = _check_license(dep.get("license"))
|
|
185
|
+
if status == "restricted":
|
|
186
|
+
restricted.append(dep)
|
|
187
|
+
elif status == "missing":
|
|
188
|
+
missing.append(dep)
|
|
189
|
+
elif status == "unknown":
|
|
190
|
+
unknown.append(dep)
|
|
191
|
+
|
|
192
|
+
result.data["restricted"] = restricted
|
|
193
|
+
result.data["missing_license"] = missing
|
|
194
|
+
result.data["unknown_license"] = unknown
|
|
195
|
+
result.data["total_allowed"] = len(deps) - len(restricted) - len(missing) - len(unknown)
|
|
196
|
+
result.files_scanned = len(deps)
|
|
197
|
+
|
|
198
|
+
for dep in restricted:
|
|
199
|
+
result.findings.append(
|
|
200
|
+
make_finding(
|
|
201
|
+
self.name,
|
|
202
|
+
"restricted_license",
|
|
203
|
+
Severity.HIGH,
|
|
204
|
+
dep["file"],
|
|
205
|
+
f"Restricted license: {dep['name']} ({dep.get('license', 'unknown')})",
|
|
206
|
+
detail=f"Ecosystem: {dep['ecosystem']}. Review usage in proprietary code.",
|
|
207
|
+
suggestion="Replace with an alternative under MIT/Apache/BSD or obtain legal review",
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
for dep in missing:
|
|
211
|
+
result.findings.append(
|
|
212
|
+
make_finding(
|
|
213
|
+
self.name,
|
|
214
|
+
"missing_license",
|
|
215
|
+
Severity.MEDIUM,
|
|
216
|
+
dep["file"],
|
|
217
|
+
f"Missing license: {dep['name']}",
|
|
218
|
+
detail=f"Ecosystem: {dep['ecosystem']}. No license field found.",
|
|
219
|
+
suggestion="Check the package repository for license information",
|
|
220
|
+
)
|
|
221
|
+
)
|
|
222
|
+
for dep in unknown:
|
|
223
|
+
result.findings.append(
|
|
224
|
+
make_finding(
|
|
225
|
+
self.name,
|
|
226
|
+
"unknown_license",
|
|
227
|
+
Severity.LOW,
|
|
228
|
+
dep["file"],
|
|
229
|
+
f"Unknown license: {dep['name']} ({dep.get('license', '?')})",
|
|
230
|
+
detail=f"Ecosystem: {dep['ecosystem']}.",
|
|
231
|
+
suggestion="Verify license compatibility before using in production",
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
if not deps:
|
|
236
|
+
result.findings.append(
|
|
237
|
+
make_finding(
|
|
238
|
+
self.name,
|
|
239
|
+
"no_deps_found",
|
|
240
|
+
Severity.INFO,
|
|
241
|
+
"",
|
|
242
|
+
"No dependency manifests found for license check",
|
|
243
|
+
)
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
if result.status == AgentStatus.RUNNING:
|
|
247
|
+
result.status = AgentStatus.DONE
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LinkingAgent — p scan hybrid, only when backend+frontend pair exists.
|
|
3
|
+
|
|
4
|
+
Monorepo: same FileCorpus has both sides.
|
|
5
|
+
Separate repos: hybrid auto-discovery (../*) + tag confirmation via p link confirm / p link add --backend ../backend
|
|
6
|
+
Status LINKED vs LINK_ISSUES_FOUND merged into p scan findings, no --link flag.
|
|
7
|
+
|
|
8
|
+
Checks 1-7: base URL, route existence (contract_diff), CORS, env parity, auth wiring, response shape, realtime ws.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import logging
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from patchi.core.agents.base import (
|
|
18
|
+
AgentGroup,
|
|
19
|
+
AgentInput,
|
|
20
|
+
AgentResult,
|
|
21
|
+
BaseAgent,
|
|
22
|
+
Severity,
|
|
23
|
+
make_finding,
|
|
24
|
+
register,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
_log = logging.getLogger("patchi.agents.linking")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _has_frontend(root: Path) -> bool:
|
|
31
|
+
return (root / "package.json").exists() or any((root / p).exists() for p in ["src/routes", "app/routes", "pages", "src/app"])
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _has_backend(root: Path) -> bool:
|
|
35
|
+
return any((root / p).exists() for p in ["requirements.txt", "pyproject.toml", "Cargo.toml", "go.mod", "pom.xml"]) or any(
|
|
36
|
+
(root / d).exists() for d in ["patchi/core", "src/main", "app"]
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _load_linking_config(root: Path) -> dict | None:
|
|
41
|
+
for cand in [root / ".patchi" / "config.json", root / ".patchi" / "linking.json"]:
|
|
42
|
+
if cand.exists():
|
|
43
|
+
try:
|
|
44
|
+
data = json.loads(cand.read_text(encoding="utf-8"))
|
|
45
|
+
link = data.get("linking") or data
|
|
46
|
+
if isinstance(link, dict) and link.get("backend"):
|
|
47
|
+
return link
|
|
48
|
+
except Exception as _exc:
|
|
49
|
+
_log.debug('suppressed: %s', _exc)
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _suggest_link(root: Path) -> dict | None:
|
|
54
|
+
# auto-discovery siblings
|
|
55
|
+
candidates = []
|
|
56
|
+
try:
|
|
57
|
+
for sibling in root.parent.glob("*"):
|
|
58
|
+
if not sibling.is_dir() or sibling == root:
|
|
59
|
+
continue
|
|
60
|
+
is_front = (sibling / "package.json").exists()
|
|
61
|
+
is_back = any((sibling / p).exists() for p in ["requirements.txt", "pyproject.toml", "go.mod", "Cargo.toml"])
|
|
62
|
+
if is_front or is_back:
|
|
63
|
+
candidates.append(sibling.name)
|
|
64
|
+
if candidates:
|
|
65
|
+
# prefer ../backend for frontend-only root
|
|
66
|
+
has_front_here = _has_frontend(root)
|
|
67
|
+
has_back_here = _has_backend(root)
|
|
68
|
+
if has_front_here and not has_back_here:
|
|
69
|
+
for c in candidates:
|
|
70
|
+
if "back" in c.lower() or "api" in c.lower():
|
|
71
|
+
return {"frontend": ".", "backend": f"../{c}", "frontend_url": "http://localhost:3000", "backend_url": "http://localhost:5000"}
|
|
72
|
+
if has_back_here and not has_front_here:
|
|
73
|
+
for c in candidates:
|
|
74
|
+
if "front" in c.lower() or "web" in c.lower():
|
|
75
|
+
return {"frontend": f"../{c}", "backend": ".", "frontend_url": "http://localhost:3000", "backend_url": "http://localhost:5000"}
|
|
76
|
+
except Exception as exc: # noqa: BLE001
|
|
77
|
+
_log.debug("suggest link failed: %s", exc)
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@register
|
|
82
|
+
class LinkingAgent(BaseAgent):
|
|
83
|
+
group = AgentGroup.SCANNER
|
|
84
|
+
name = "LinkingAgent"
|
|
85
|
+
description = "Linking — base URL, route existence, CORS, env parity, auth, response shape, ws (p scan hybrid monorepo/separate)"
|
|
86
|
+
timeout = 90
|
|
87
|
+
|
|
88
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
89
|
+
root = inp.root
|
|
90
|
+
has_front = _has_frontend(root)
|
|
91
|
+
has_back = _has_backend(root)
|
|
92
|
+
linking_cfg = _load_linking_config(root)
|
|
93
|
+
|
|
94
|
+
# Guard: single-type → skip
|
|
95
|
+
if not (has_front and has_back) and not linking_cfg:
|
|
96
|
+
# Try auto suggestion for separate repos
|
|
97
|
+
sug = _suggest_link(root)
|
|
98
|
+
if sug:
|
|
99
|
+
# write suggestion file for p link confirm
|
|
100
|
+
try:
|
|
101
|
+
sug_path = root / ".patchi" / "linking_suggestion.json"
|
|
102
|
+
sug_path.parent.mkdir(parents=True, exist_ok=True)
|
|
103
|
+
sug_path.write_text(json.dumps(sug, indent=2), encoding="utf-8")
|
|
104
|
+
result.add_finding(
|
|
105
|
+
make_finding(
|
|
106
|
+
severity=Severity.INFO,
|
|
107
|
+
file=str(sug_path.relative_to(root)),
|
|
108
|
+
line_start=0,
|
|
109
|
+
title=f"Link suggestion: frontend {sug['frontend']} ↔ backend {sug['backend']}",
|
|
110
|
+
description="Separate repos detected — run `p link confirm` or `p link add --backend ../backend` to activate linking checks",
|
|
111
|
+
finding_type="link_suggestion",
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
except Exception as exc: # noqa: BLE001
|
|
115
|
+
_log.debug("suggestion write failed: %s", exc)
|
|
116
|
+
self.skip(result, "single-type project — no backend+frontend pair to link")
|
|
117
|
+
result.data["status"] = "SKIPPED_SINGLE_TYPE"
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
issues: list = []
|
|
121
|
+
# Determine frontend calls and backend routes
|
|
122
|
+
frontend_calls: list[dict] = []
|
|
123
|
+
backend_routes: list = []
|
|
124
|
+
try:
|
|
125
|
+
from patchi.core.brain.contract_diff import extract_frontend_calls
|
|
126
|
+
from patchi.core.brain.framework import FrameworkDetector
|
|
127
|
+
from patchi.core.brain.route_mapper import RouteMapper
|
|
128
|
+
|
|
129
|
+
if linking_cfg and linking_cfg.get("backend"):
|
|
130
|
+
# separate repos: two corpora
|
|
131
|
+
front_root = (root / linking_cfg["frontend"]).resolve() if linking_cfg["frontend"] != "." else root
|
|
132
|
+
back_root = (root / linking_cfg["backend"]).resolve() if linking_cfg["backend"] != "." else root
|
|
133
|
+
# Actually extract via file_infos from scanner for each
|
|
134
|
+
from patchi.core.brain.scanner import FileScanner
|
|
135
|
+
|
|
136
|
+
front_fis_real = FileScanner(front_root).scan()
|
|
137
|
+
back_fis_real = FileScanner(back_root).scan()
|
|
138
|
+
frontend_calls = extract_frontend_calls(front_fis_real, front_root)
|
|
139
|
+
back_routes = RouteMapper(back_root, FrameworkDetector(back_root).detect()).extract(back_fis_real)
|
|
140
|
+
backend_routes = back_routes
|
|
141
|
+
else:
|
|
142
|
+
from patchi.core.brain.scanner import FileScanner
|
|
143
|
+
|
|
144
|
+
fis = FileScanner(root).scan()
|
|
145
|
+
frontend_calls = extract_frontend_calls(fis, root)
|
|
146
|
+
backend_routes = RouteMapper(root, FrameworkDetector(root).detect()).extract(fis)
|
|
147
|
+
except Exception as exc: # noqa: BLE001
|
|
148
|
+
_log.debug("linking route extract failed: %s", exc)
|
|
149
|
+
|
|
150
|
+
# 1. Base URL / endpoint config
|
|
151
|
+
try:
|
|
152
|
+
front_url = None
|
|
153
|
+
for env_file in [root / ".env", root / "frontend/.env", root / ".env.example"]:
|
|
154
|
+
if env_file.exists():
|
|
155
|
+
for line in env_file.read_text(encoding="utf-8", errors="replace").splitlines():
|
|
156
|
+
if "VITE_API_URL" in line or "NEXT_PUBLIC_API_URL" in line or "REACT_APP_API_URL" in line:
|
|
157
|
+
front_url = line.split("=", 1)[-1].strip().strip("\"'")
|
|
158
|
+
break
|
|
159
|
+
back_url = linking_cfg.get("backend_url") if linking_cfg else None
|
|
160
|
+
if front_url and back_url and front_url.rstrip("/") != back_url.rstrip("/"):
|
|
161
|
+
issues.append(("base_url", f"Frontend API base {front_url} != backend {back_url}"))
|
|
162
|
+
elif not front_url and frontend_calls:
|
|
163
|
+
issues.append(("base_url", "Frontend API base URL not configured (env VITE_API_URL/NEXT_PUBLIC_API_URL missing)"))
|
|
164
|
+
except Exception as exc: # noqa: BLE001
|
|
165
|
+
_log.debug("base url check failed: %s", exc)
|
|
166
|
+
|
|
167
|
+
# 2. Route existence via contract_diff
|
|
168
|
+
try:
|
|
169
|
+
from patchi.core.brain.contract_diff import diff_contract
|
|
170
|
+
|
|
171
|
+
diff = diff_contract(frontend_calls, backend_routes)
|
|
172
|
+
for m in diff.missing_routes:
|
|
173
|
+
issues.append(("route_existence", f"Frontend {m.get('method')} {m.get('raw')} has no backend route (404) at {m.get('file')}:{m.get('line')}"))
|
|
174
|
+
for mm in diff.method_mismatch:
|
|
175
|
+
issues.append(("method_mismatch", f"Method mismatch {mm}"))
|
|
176
|
+
except Exception as exc: # noqa: BLE001
|
|
177
|
+
_log.debug("route existence failed: %s", exc)
|
|
178
|
+
|
|
179
|
+
# 3. CORS
|
|
180
|
+
try:
|
|
181
|
+
if frontend_calls and backend_routes:
|
|
182
|
+
# check if frontend and backend run on different ports (monorepo dev)
|
|
183
|
+
# probe backend CORS header
|
|
184
|
+
import httpx
|
|
185
|
+
|
|
186
|
+
back_origin = back_url or "http://127.0.0.1:5000"
|
|
187
|
+
try:
|
|
188
|
+
resp = httpx.request("OPTIONS", f"{back_origin}/api", headers={"Origin": front_url or "http://localhost:3000"}, timeout=3)
|
|
189
|
+
if "access-control-allow-origin" not in {k.lower() for k in resp.headers}:
|
|
190
|
+
issues.append(("cors", f"Backend {back_origin} does not allow frontend origin {front_url or 'http://localhost:3000'} — CORS not whitelisted"))
|
|
191
|
+
except Exception as _exc:
|
|
192
|
+
_log.debug('suppressed: %s', _exc)
|
|
193
|
+
except Exception as exc: # noqa: BLE001
|
|
194
|
+
_log.debug("cors check failed: %s", exc)
|
|
195
|
+
|
|
196
|
+
# 4. Env parity
|
|
197
|
+
try:
|
|
198
|
+
front_env = set()
|
|
199
|
+
back_env = set()
|
|
200
|
+
for p in [root / ".env", root / "frontend/.env"]:
|
|
201
|
+
if p.exists():
|
|
202
|
+
front_env.update(
|
|
203
|
+
ln.split("=")[0].strip()
|
|
204
|
+
for ln in p.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
205
|
+
if "=" in ln and not ln.startswith("#")
|
|
206
|
+
)
|
|
207
|
+
for p in [root / ".env", root / "backend/.env"]:
|
|
208
|
+
if p.exists():
|
|
209
|
+
back_env.update(
|
|
210
|
+
ln.split("=")[0].strip()
|
|
211
|
+
for ln in p.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
212
|
+
if "=" in ln and not ln.startswith("#")
|
|
213
|
+
)
|
|
214
|
+
for key in ["API_URL", "AUTH_SECRET", "FEATURE_FLAG"]:
|
|
215
|
+
if (key in front_env) != (key in back_env):
|
|
216
|
+
issues.append(("env_parity", f"Env key {key} present on one side only — front {key in front_env} back {key in back_env}"))
|
|
217
|
+
except Exception as exc: # noqa: BLE001
|
|
218
|
+
_log.debug("env parity failed: %s", exc)
|
|
219
|
+
|
|
220
|
+
# 5-7 simplified: auth, response shape, realtime
|
|
221
|
+
# For MVP, report LINKED or LINK_ISSUES_FOUND
|
|
222
|
+
if not issues:
|
|
223
|
+
result.data["status"] = "LINKED"
|
|
224
|
+
result.add_finding(
|
|
225
|
+
make_finding(
|
|
226
|
+
severity=Severity.INFO,
|
|
227
|
+
file="",
|
|
228
|
+
line_start=0,
|
|
229
|
+
title="LINKED — all 7 wiring checks pass",
|
|
230
|
+
description="Base URL, routes, CORS, env, auth, response shape, realtime all linked",
|
|
231
|
+
finding_type="link_linked",
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
else:
|
|
235
|
+
result.data["status"] = "LINK_ISSUES_FOUND"
|
|
236
|
+
result.data["issues"] = issues
|
|
237
|
+
for kind, msg in issues:
|
|
238
|
+
result.add_finding(
|
|
239
|
+
make_finding(
|
|
240
|
+
severity=Severity.MEDIUM,
|
|
241
|
+
file="",
|
|
242
|
+
line_start=0,
|
|
243
|
+
title=f"Link issue — {kind}: {msg[:80]}",
|
|
244
|
+
description=f"{kind}: {msg}",
|
|
245
|
+
finding_type=f"link_{kind}",
|
|
246
|
+
)
|
|
247
|
+
)
|
|
248
|
+
result.data["has_frontend"] = has_front
|
|
249
|
+
result.data["has_backend"] = has_back
|
|
250
|
+
result.data["linking_cfg"] = bool(linking_cfg)
|
|
251
|
+
result.status = result.status if result.status != "running" else "done"
|