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.
Files changed (496) hide show
  1. patchi/__init__.py +1 -0
  2. patchi/__main__.py +13 -0
  3. patchi/cli/__init__.py +0 -0
  4. patchi/cli/command_families.py +235 -0
  5. patchi/cli/commands/__init__.py +0 -0
  6. patchi/cli/commands/access_cmd.py +65 -0
  7. patchi/cli/commands/agent_stats_cmd.py +273 -0
  8. patchi/cli/commands/agents_cmd.py +187 -0
  9. patchi/cli/commands/ai_cmd.py +308 -0
  10. patchi/cli/commands/assure_cmd.py +538 -0
  11. patchi/cli/commands/audit_cmd.py +286 -0
  12. patchi/cli/commands/auto_cmd.py +110 -0
  13. patchi/cli/commands/blame_cmd.py +53 -0
  14. patchi/cli/commands/chains_cmd.py +327 -0
  15. patchi/cli/commands/charter_cmd.py +173 -0
  16. patchi/cli/commands/chat_cmd.py +597 -0
  17. patchi/cli/commands/check_cmd.py +232 -0
  18. patchi/cli/commands/cleanup_cmd.py +328 -0
  19. patchi/cli/commands/cockpit_cmd.py +208 -0
  20. patchi/cli/commands/commands_cmd.py +160 -0
  21. patchi/cli/commands/cross_repo_cmd.py +113 -0
  22. patchi/cli/commands/deps_cmd.py +110 -0
  23. patchi/cli/commands/dev_check_cmd.py +342 -0
  24. patchi/cli/commands/dev_cmd.py +445 -0
  25. patchi/cli/commands/doctor_cmd.py +457 -0
  26. patchi/cli/commands/findings_cmd.py +187 -0
  27. patchi/cli/commands/fix_cmd.py +648 -0
  28. patchi/cli/commands/fix_review_cmd.py +273 -0
  29. patchi/cli/commands/heatmap_cmd.py +62 -0
  30. patchi/cli/commands/help_cmd.py +159 -0
  31. patchi/cli/commands/hosted_cmd.py +980 -0
  32. patchi/cli/commands/init.py +456 -0
  33. patchi/cli/commands/key_cmd.py +571 -0
  34. patchi/cli/commands/learn_cmd.py +140 -0
  35. patchi/cli/commands/link_cmd.py +104 -0
  36. patchi/cli/commands/log_cmd.py +57 -0
  37. patchi/cli/commands/memory_cmd.py +215 -0
  38. patchi/cli/commands/model_cmd.py +207 -0
  39. patchi/cli/commands/notify_cmd.py +213 -0
  40. patchi/cli/commands/notify_prompts.py +29 -0
  41. patchi/cli/commands/patch_cmd.py +231 -0
  42. patchi/cli/commands/plan_cmd.py +128 -0
  43. patchi/cli/commands/plugins_cmd.py +307 -0
  44. patchi/cli/commands/queue_cmd.py +152 -0
  45. patchi/cli/commands/quick_cmd.py +159 -0
  46. patchi/cli/commands/ready_cmd.py +292 -0
  47. patchi/cli/commands/report_cmd.py +416 -0
  48. patchi/cli/commands/restrict_cmd.py +127 -0
  49. patchi/cli/commands/review_cmd.py +225 -0
  50. patchi/cli/commands/rules_cmd.py +233 -0
  51. patchi/cli/commands/scan_cmd.py +1908 -0
  52. patchi/cli/commands/settings_cmd.py +45 -0
  53. patchi/cli/commands/status_cmd.py +679 -0
  54. patchi/cli/commands/test_cmd.py +1052 -0
  55. patchi/cli/commands/trend_cmd.py +296 -0
  56. patchi/cli/commands/undo_cmd.py +235 -0
  57. patchi/cli/commands/update_cmd.py +345 -0
  58. patchi/cli/commands/verify_cmd.py +56 -0
  59. patchi/cli/commands/vr_cmd.py +440 -0
  60. patchi/cli/commands/watch_cmd.py +304 -0
  61. patchi/cli/commands/web_cmd.py +114 -0
  62. patchi/cli/console.py +101 -0
  63. patchi/cli/display/__init__.py +1 -0
  64. patchi/cli/display/live_audit.py +269 -0
  65. patchi/cli/display/live_progress.py +169 -0
  66. patchi/cli/framework.py +241 -0
  67. patchi/cli/logo.py +128 -0
  68. patchi/cli/main.py +222 -0
  69. patchi/cli/registry.py +1171 -0
  70. patchi/cli/style.py +68 -0
  71. patchi/cli/themes.py +187 -0
  72. patchi/cli/ux.py +444 -0
  73. patchi/core/__init__.py +0 -0
  74. patchi/core/agents/__init__.py +0 -0
  75. patchi/core/agents/api_fuzzer_agent.py +141 -0
  76. patchi/core/agents/attack_agent.py +242 -0
  77. patchi/core/agents/base.py +1012 -0
  78. patchi/core/agents/bug_predictor_agent.py +58 -0
  79. patchi/core/agents/build_tool_validator.py +150 -0
  80. patchi/core/agents/cache.py +192 -0
  81. patchi/core/agents/chaos_agent.py +133 -0
  82. patchi/core/agents/cicd_generator.py +240 -0
  83. patchi/core/agents/comment_scanner.py +280 -0
  84. patchi/core/agents/console_logging_agent.py +156 -0
  85. patchi/core/agents/coordinator.py +858 -0
  86. patchi/core/agents/core_scanner.py +269 -0
  87. patchi/core/agents/coverage_prioritizer.py +181 -0
  88. patchi/core/agents/dead_code_hygiene.py +296 -0
  89. patchi/core/agents/dead_code_scanner.py +692 -0
  90. patchi/core/agents/dependency_scanner.py +725 -0
  91. patchi/core/agents/doc_claim_agent.py +573 -0
  92. patchi/core/agents/duplicate_scanner.py +568 -0
  93. patchi/core/agents/env_scanner.py +410 -0
  94. patchi/core/agents/feature_flag_agent.py +89 -0
  95. patchi/core/agents/frontend_framework_agent.py +133 -0
  96. patchi/core/agents/goroutine_agent.py +82 -0
  97. patchi/core/agents/governor.py +1972 -0
  98. patchi/core/agents/i18n_agent.py +95 -0
  99. patchi/core/agents/license_compliance.py +247 -0
  100. patchi/core/agents/linking_agent.py +251 -0
  101. patchi/core/agents/memory_profiler_agent.py +102 -0
  102. patchi/core/agents/modernization_agent.py +84 -0
  103. patchi/core/agents/mutation_agent.py +174 -0
  104. patchi/core/agents/nl_query_agent.py +74 -0
  105. patchi/core/agents/promise_rejection_agent.py +100 -0
  106. patchi/core/agents/refactoring_agent.py +286 -0
  107. patchi/core/agents/resource_leak_agent.py +91 -0
  108. patchi/core/agents/route_graph_scanner.py +951 -0
  109. patchi/core/agents/run_agent.py +106 -0
  110. patchi/core/agents/rust_unwrap_agent.py +100 -0
  111. patchi/core/agents/sbom_generator.py +284 -0
  112. patchi/core/agents/scanners.py +67 -0
  113. patchi/core/agents/side/__init__.py +1 -0
  114. patchi/core/agents/side/build_agent.py +65 -0
  115. patchi/core/agents/side/format_agent.py +50 -0
  116. patchi/core/agents/side/install_agent.py +63 -0
  117. patchi/core/agents/side_file_scanner.py +549 -0
  118. patchi/core/agents/snapshot_drift_detector.py +146 -0
  119. patchi/core/agents/spa_route_inventory.py +203 -0
  120. patchi/core/agents/test_scanner.py +442 -0
  121. patchi/core/agents/tool_health.py +109 -0
  122. patchi/core/agents/tool_runner.py +162 -0
  123. patchi/core/agents/type_scanner.py +168 -0
  124. patchi/core/agents/ui_scanner.py +558 -0
  125. patchi/core/ai/__init__.py +20 -0
  126. patchi/core/ai/agent_audit.py +153 -0
  127. patchi/core/ai/agent_profiler.py +277 -0
  128. patchi/core/ai/client.py +499 -0
  129. patchi/core/ai/cost_tracker.py +107 -0
  130. patchi/core/ai/model_router.py +318 -0
  131. patchi/core/ai/orchestrator.py +625 -0
  132. patchi/core/ai/prompts.py +681 -0
  133. patchi/core/ai/smart.py +302 -0
  134. patchi/core/ai/tool_executor.py +24 -0
  135. patchi/core/ai/tools/__init__.py +10 -0
  136. patchi/core/ai/tools/executor.py +621 -0
  137. patchi/core/ai/tools/realize.py +1160 -0
  138. patchi/core/ai/tools/registry.py +1577 -0
  139. patchi/core/assurance/__init__.py +33 -0
  140. patchi/core/assurance/builder.py +211 -0
  141. patchi/core/assurance/graph.py +342 -0
  142. patchi/core/assurance/invariants.py +168 -0
  143. patchi/core/atomic.py +61 -0
  144. patchi/core/attackers/__init__.py +17 -0
  145. patchi/core/attackers/api_abuse.py +89 -0
  146. patchi/core/attackers/auth_bypass.py +73 -0
  147. patchi/core/attackers/base.py +91 -0
  148. patchi/core/attackers/business_logic.py +88 -0
  149. patchi/core/attackers/planner.py +108 -0
  150. patchi/core/attackers/priv_escalation.py +88 -0
  151. patchi/core/attackers/recon.py +87 -0
  152. patchi/core/brain/__init__.py +0 -0
  153. patchi/core/brain/ast_utils/__init__.py +81 -0
  154. patchi/core/brain/ast_utils/assignments.py +219 -0
  155. patchi/core/brain/ast_utils/calls.py +139 -0
  156. patchi/core/brain/ast_utils/config.py +132 -0
  157. patchi/core/brain/ast_utils/control_flow.py +74 -0
  158. patchi/core/brain/ast_utils/dead_symbols.py +205 -0
  159. patchi/core/brain/ast_utils/decorators.py +108 -0
  160. patchi/core/brain/ast_utils/helpers.py +144 -0
  161. patchi/core/brain/ast_utils/imports.py +169 -0
  162. patchi/core/brain/ast_utils/scan.py +143 -0
  163. patchi/core/brain/ast_utils/taint.py +310 -0
  164. patchi/core/brain/audit.py +379 -0
  165. patchi/core/brain/baseline.py +195 -0
  166. patchi/core/brain/blast_radius.py +312 -0
  167. patchi/core/brain/body_tags.py +144 -0
  168. patchi/core/brain/brain.py +1161 -0
  169. patchi/core/brain/brain_context.py +393 -0
  170. patchi/core/brain/brain_watcher.py +340 -0
  171. patchi/core/brain/charter.py +473 -0
  172. patchi/core/brain/classifier.py +214 -0
  173. patchi/core/brain/cockpit.py +350 -0
  174. patchi/core/brain/code_query.py +710 -0
  175. patchi/core/brain/contract.py +889 -0
  176. patchi/core/brain/contract_diff.py +155 -0
  177. patchi/core/brain/council.py +800 -0
  178. patchi/core/brain/cross_repo.py +104 -0
  179. patchi/core/brain/dead_code_tools.py +157 -0
  180. patchi/core/brain/doc_validator.py +197 -0
  181. patchi/core/brain/domain_activator.py +2183 -0
  182. patchi/core/brain/enriched_context.py +220 -0
  183. patchi/core/brain/file_corpus.py +220 -0
  184. patchi/core/brain/framework.py +953 -0
  185. patchi/core/brain/freshness.py +273 -0
  186. patchi/core/brain/git_aware.py +206 -0
  187. patchi/core/brain/heatmap.py +104 -0
  188. patchi/core/brain/ignore_parser.py +153 -0
  189. patchi/core/brain/import_graph.py +486 -0
  190. patchi/core/brain/languages.py +397 -0
  191. patchi/core/brain/layered_brain.py +419 -0
  192. patchi/core/brain/learning.py +229 -0
  193. patchi/core/brain/orphaned_endpoints.py +313 -0
  194. patchi/core/brain/personas/__init__.py +648 -0
  195. patchi/core/brain/personas/bad_user.py +165 -0
  196. patchi/core/brain/personas/base.py +349 -0
  197. patchi/core/brain/proactive.py +807 -0
  198. patchi/core/brain/project_context.py +121 -0
  199. patchi/core/brain/project_reader.py +521 -0
  200. patchi/core/brain/reasoning.py +269 -0
  201. patchi/core/brain/route_detector/__init__.py +15 -0
  202. patchi/core/brain/route_detector/base.py +31 -0
  203. patchi/core/brain/route_detector/csharp.py +209 -0
  204. patchi/core/brain/route_detector/file_based.py +81 -0
  205. patchi/core/brain/route_detector/go.py +152 -0
  206. patchi/core/brain/route_detector/java.py +195 -0
  207. patchi/core/brain/route_detector/javascript.py +197 -0
  208. patchi/core/brain/route_detector/php.py +158 -0
  209. patchi/core/brain/route_detector/python.py +193 -0
  210. patchi/core/brain/route_detector/registry.py +25 -0
  211. patchi/core/brain/route_detector/ruby.py +134 -0
  212. patchi/core/brain/route_detector/rust.py +216 -0
  213. patchi/core/brain/route_detector/swift.py +118 -0
  214. patchi/core/brain/route_mapper.py +265 -0
  215. patchi/core/brain/scanner.py +2136 -0
  216. patchi/core/brain/secrets.py +280 -0
  217. patchi/core/brain/symbol_graph.py +1955 -0
  218. patchi/core/brain/team.py +28 -0
  219. patchi/core/brain/trace_log.py +132 -0
  220. patchi/core/brain/type_checker/__init__.py +65 -0
  221. patchi/core/brain/type_checker/base.py +60 -0
  222. patchi/core/brain/type_checker/csharp.py +59 -0
  223. patchi/core/brain/type_checker/dart.py +59 -0
  224. patchi/core/brain/type_checker/go.py +81 -0
  225. patchi/core/brain/type_checker/java.py +75 -0
  226. patchi/core/brain/type_checker/kotlin.py +75 -0
  227. patchi/core/brain/type_checker/php.py +88 -0
  228. patchi/core/brain/type_checker/python.py +87 -0
  229. patchi/core/brain/type_checker/rust.py +81 -0
  230. patchi/core/brain/type_checker/swift.py +59 -0
  231. patchi/core/brain/type_checker/typescript.py +298 -0
  232. patchi/core/brain/understander.py +130 -0
  233. patchi/core/brain/verify.py +379 -0
  234. patchi/core/campaigns/__init__.py +14 -0
  235. patchi/core/campaigns/base.py +62 -0
  236. patchi/core/campaigns/data_flow_abuse.py +153 -0
  237. patchi/core/campaigns/orchestrator.py +140 -0
  238. patchi/core/campaigns/state_transitions.py +178 -0
  239. patchi/core/ci_bundle.py +104 -0
  240. patchi/core/config.py +311 -0
  241. patchi/core/constants.py +245 -0
  242. patchi/core/debug/__init__.py +25 -0
  243. patchi/core/debug/adapters/__init__.py +11 -0
  244. patchi/core/debug/adapters/codelldb.py +379 -0
  245. patchi/core/debug/adapters/node.py +222 -0
  246. patchi/core/debug/adapters/powershell.py +210 -0
  247. patchi/core/debug/adapters/python.py +244 -0
  248. patchi/core/debug/capture.py +210 -0
  249. patchi/core/debug/dap_client.py +268 -0
  250. patchi/core/detector/__init__.py +0 -0
  251. patchi/core/detector/audit.py +194 -0
  252. patchi/core/detector/bus.py +226 -0
  253. patchi/core/detector/dispatcher.py +275 -0
  254. patchi/core/detector/event.py +254 -0
  255. patchi/core/detector/sigma_engine.py +378 -0
  256. patchi/core/detector/triage.py +289 -0
  257. patchi/core/export/sarif.py +113 -0
  258. patchi/core/fix/__init__.py +17 -0
  259. patchi/core/fix/applier.py +574 -0
  260. patchi/core/fix/base.py +510 -0
  261. patchi/core/fix/code_fixer.py +169 -0
  262. patchi/core/fix/dead_code_remover.py +131 -0
  263. patchi/core/fix/fix_agents.py +775 -0
  264. patchi/core/fix/patch.py +444 -0
  265. patchi/core/fix/risk_gate.py +383 -0
  266. patchi/core/fix/security_fixer.py +93 -0
  267. patchi/core/fix/verify_loop.py +204 -0
  268. patchi/core/fuzz/__init__.py +8 -0
  269. patchi/core/fuzz/corpus.py +137 -0
  270. patchi/core/fuzz/input_fuzzer.py +256 -0
  271. patchi/core/fuzz/sequence_fuzzer.py +162 -0
  272. patchi/core/fuzz/state_fuzzer.py +236 -0
  273. patchi/core/health.py +407 -0
  274. patchi/core/hosted/__init__.py +1 -0
  275. patchi/core/hosted/anomaly.py +298 -0
  276. patchi/core/hosted/audit_log.py +102 -0
  277. patchi/core/hosted/compliance_report.py +148 -0
  278. patchi/core/hosted/ip_reputation.py +169 -0
  279. patchi/core/hosted/log_parsers.py +251 -0
  280. patchi/core/hosted/tokens.py +127 -0
  281. patchi/core/hosted/watchlist.py +164 -0
  282. patchi/core/hosted/webhooks.py +156 -0
  283. patchi/core/memory.py +437 -0
  284. patchi/core/notifications/__init__.py +18 -0
  285. patchi/core/notifications/channels.py +192 -0
  286. patchi/core/notifications/digest.py +141 -0
  287. patchi/core/notifications/escalation.py +158 -0
  288. patchi/core/notifications/notifier.py +182 -0
  289. patchi/core/notifications/quiet_hours.py +69 -0
  290. patchi/core/plugins/__init__.py +48 -0
  291. patchi/core/plugins/analyzer.py +326 -0
  292. patchi/core/plugins/analyzers/__init__.py +6 -0
  293. patchi/core/plugins/analyzers/dead_code_analyzer.py +120 -0
  294. patchi/core/plugins/analyzers/secret_scanner.py +158 -0
  295. patchi/core/plugins/analyzers/todo_scanner.py +115 -0
  296. patchi/core/plugins/registry.py +335 -0
  297. patchi/core/queue.py +375 -0
  298. patchi/core/reliability/__init__.py +13 -0
  299. patchi/core/reliability/chaos.py +163 -0
  300. patchi/core/reliability/fault_injector.py +172 -0
  301. patchi/core/reliability/idempotency.py +128 -0
  302. patchi/core/reliability/recovery.py +147 -0
  303. patchi/core/runtime/__init__.py +5 -0
  304. patchi/core/runtime/tracer.py +230 -0
  305. patchi/core/scan_bus.py +156 -0
  306. patchi/core/security/__init__.py +1 -0
  307. patchi/core/security/ai_validator.py +462 -0
  308. patchi/core/security/app_mapper.py +216 -0
  309. patchi/core/security/app_profile.py +464 -0
  310. patchi/core/security/attack_feedback.py +141 -0
  311. patchi/core/security/attack_scenarios/__init__.py +1 -0
  312. patchi/core/security/attack_tree.py +176 -0
  313. patchi/core/security/auth_audit_agent.py +344 -0
  314. patchi/core/security/authz_agent.py +602 -0
  315. patchi/core/security/auto_fix_proactive.py +620 -0
  316. patchi/core/security/auto_fixer.py +660 -0
  317. patchi/core/security/auto_ticket.py +53 -0
  318. patchi/core/security/bandit_agent.py +152 -0
  319. patchi/core/security/blast_radius.py +226 -0
  320. patchi/core/security/browser_tester.py +333 -0
  321. patchi/core/security/business_logic_agent.py +263 -0
  322. patchi/core/security/catch_block_auditor.py +395 -0
  323. patchi/core/security/cdn_cache_agent.py +357 -0
  324. patchi/core/security/chain_analyzer.py +321 -0
  325. patchi/core/security/chain_to_assurance.py +168 -0
  326. patchi/core/security/charter.py +510 -0
  327. patchi/core/security/cloud_waf_detector.py +295 -0
  328. patchi/core/security/codeql_agent.py +233 -0
  329. patchi/core/security/compliance_agent.py +348 -0
  330. patchi/core/security/confidence_gate.py +423 -0
  331. patchi/core/security/container_scanner.py +282 -0
  332. patchi/core/security/crypto_agent.py +378 -0
  333. patchi/core/security/cve_monitor.py +262 -0
  334. patchi/core/security/dast_agent.py +865 -0
  335. patchi/core/security/defectdojo.py +238 -0
  336. patchi/core/security/defenders/__init__.py +75 -0
  337. patchi/core/security/defenders/auth_middleware.py +115 -0
  338. patchi/core/security/defenders/base.py +97 -0
  339. patchi/core/security/defenders/block_ip.py +71 -0
  340. patchi/core/security/defenders/block_ws_origin.py +36 -0
  341. patchi/core/security/defenders/crypto_fix.py +118 -0
  342. patchi/core/security/defenders/enforce_rate_limit.py +108 -0
  343. patchi/core/security/defenders/fix_code.py +93 -0
  344. patchi/core/security/defenders/invalidate_session.py +40 -0
  345. patchi/core/security/defenders/patch_config.py +70 -0
  346. patchi/core/security/defenders/rotate_secret.py +54 -0
  347. patchi/core/security/defenders/suspend_account.py +41 -0
  348. patchi/core/security/defenders/update_dependency.py +160 -0
  349. patchi/core/security/defense_layer.py +217 -0
  350. patchi/core/security/dependency_vulnerability_agent.py +616 -0
  351. patchi/core/security/detection_pipeline.py +289 -0
  352. patchi/core/security/dns_security_agent.py +497 -0
  353. patchi/core/security/domain_activator_v2.py +913 -0
  354. patchi/core/security/domain_loader.py +1004 -0
  355. patchi/core/security/domains/__init__.py +0 -0
  356. patchi/core/security/email_authentication_agent.py +417 -0
  357. patchi/core/security/env_var_validator.py +304 -0
  358. patchi/core/security/evidence.py +100 -0
  359. patchi/core/security/falco_runtime_agent.py +387 -0
  360. patchi/core/security/fix-playbooks/__init__.py +0 -0
  361. patchi/core/security/gated_finding.py +85 -0
  362. patchi/core/security/git_diff_activator.py +367 -0
  363. patchi/core/security/governance.py +208 -0
  364. patchi/core/security/gradual_gate.py +34 -0
  365. patchi/core/security/history.py +315 -0
  366. patchi/core/security/iac_scanner.py +250 -0
  367. patchi/core/security/ignore_expiry.py +111 -0
  368. patchi/core/security/ignore_learner.py +416 -0
  369. patchi/core/security/injection_agent.py +319 -0
  370. patchi/core/security/insecure_randomness_agent.py +288 -0
  371. patchi/core/security/intent_analyzer.py +326 -0
  372. patchi/core/security/jwt_agent.py +453 -0
  373. patchi/core/security/kubernetes_agent.py +638 -0
  374. patchi/core/security/layer2_orchestrator.py +341 -0
  375. patchi/core/security/llm_security_agent.py +252 -0
  376. patchi/core/security/misconfig_agent.py +600 -0
  377. patchi/core/security/mobile_security_agent.py +474 -0
  378. patchi/core/security/network_agent.py +376 -0
  379. patchi/core/security/noise_filter.py +274 -0
  380. patchi/core/security/orchestrator.py +313 -0
  381. patchi/core/security/pattern_context.py +125 -0
  382. patchi/core/security/pattern_loader.py +101 -0
  383. patchi/core/security/pentest/__init__.py +4 -0
  384. patchi/core/security/pentest/base.py +58 -0
  385. patchi/core/security/pentest/dalfox_adapter.py +40 -0
  386. patchi/core/security/pentest/ffuf_adapter.py +48 -0
  387. patchi/core/security/pentest/nuclei_adapter.py +50 -0
  388. patchi/core/security/pentest/registry.py +80 -0
  389. patchi/core/security/pentest/shannon_adapter.py +107 -0
  390. patchi/core/security/pentest/sqlmap_adapter.py +56 -0
  391. patchi/core/security/pentest/zap_adapter.py +52 -0
  392. patchi/core/security/plan_auditor.py +276 -0
  393. patchi/core/security/policy_engine.py +272 -0
  394. patchi/core/security/prechecks.py +172 -0
  395. patchi/core/security/precommit_hook.py +247 -0
  396. patchi/core/security/privacy_agent.py +361 -0
  397. patchi/core/security/push_notification_agent.py +417 -0
  398. patchi/core/security/pysa_agent.py +143 -0
  399. patchi/core/security/reasoning.py +442 -0
  400. patchi/core/security/red_team_agent.py +184 -0
  401. patchi/core/security/red_team_engine.py +1142 -0
  402. patchi/core/security/remediation.py +360 -0
  403. patchi/core/security/request_interceptor.py +294 -0
  404. patchi/core/security/runtime_validator.py +245 -0
  405. patchi/core/security/saml_sso_agent.py +401 -0
  406. patchi/core/security/sast_agent.py +213 -0
  407. patchi/core/security/sast_gate.py +95 -0
  408. patchi/core/security/scheduler.py +287 -0
  409. patchi/core/security/secrets_runtime_agent.py +476 -0
  410. patchi/core/security/security_agents.py +175 -0
  411. patchi/core/security/security_config.py +551 -0
  412. patchi/core/security/security_probe.py +762 -0
  413. patchi/core/security/security_taint.py +1005 -0
  414. patchi/core/security/sensitive_data_agent.py +271 -0
  415. patchi/core/security/service_mesh_agent.py +412 -0
  416. patchi/core/security/session_management_agent.py +245 -0
  417. patchi/core/security/ssrf_agent.py +422 -0
  418. patchi/core/security/supply_chain.py +449 -0
  419. patchi/core/security/threat_model_generator.py +378 -0
  420. patchi/core/security/threat_model_updater.py +196 -0
  421. patchi/core/security/tool_adapters.py +135 -0
  422. patchi/core/security/tool_verify.py +173 -0
  423. patchi/core/security/websocket_security_agent.py +188 -0
  424. patchi/core/snapshot.py +242 -0
  425. patchi/core/tenant.py +386 -0
  426. patchi/core/testing/__init__.py +17 -0
  427. patchi/core/testing/_browser.py +147 -0
  428. patchi/core/testing/ads_agent.py +141 -0
  429. patchi/core/testing/api_contract_agent.py +450 -0
  430. patchi/core/testing/app_discovery_agent.py +641 -0
  431. patchi/core/testing/app_launcher.py +184 -0
  432. patchi/core/testing/browser_test_agent.py +516 -0
  433. patchi/core/testing/e2e_flow_agent.py +354 -0
  434. patchi/core/testing/flake_detector_agent.py +292 -0
  435. patchi/core/testing/gate.py +31 -0
  436. patchi/core/testing/live_test_runner.py +304 -0
  437. patchi/core/testing/live_v2/__init__.py +1 -0
  438. patchi/core/testing/live_v2/browser_pool.py +349 -0
  439. patchi/core/testing/live_v2/browser_test_runner.py +313 -0
  440. patchi/core/testing/live_v2/runner.py +626 -0
  441. patchi/core/testing/live_v2/screenshot_manager.py +474 -0
  442. patchi/core/testing/live_v2/stress_orchestrator.py +523 -0
  443. patchi/core/testing/live_v2/video_recorder.py +303 -0
  444. patchi/core/testing/regression_agent.py +302 -0
  445. patchi/core/testing/security_test_agent.py +253 -0
  446. patchi/core/testing/stress_test_agent.py +203 -0
  447. patchi/core/testing/test_agents.py +50 -0
  448. patchi/core/testing/test_config.py +194 -0
  449. patchi/core/testing/ui_accessibility_agent.py +324 -0
  450. patchi/core/testing/ui_button_agent.py +294 -0
  451. patchi/core/testing/ui_layout_agent.py +330 -0
  452. patchi/core/testing/unit_test_agent.py +1149 -0
  453. patchi/core/testing/visual_regression_agent.py +260 -0
  454. patchi/web/__init__.py +1 -0
  455. patchi/web/api/__init__.py +1 -0
  456. patchi/web/api/brain_map.py +114 -0
  457. patchi/web/api/charts.py +47 -0
  458. patchi/web/api/chat.py +224 -0
  459. patchi/web/api/cicd.py +576 -0
  460. patchi/web/api/dast_evidence.py +49 -0
  461. patchi/web/api/dev_check.py +320 -0
  462. patchi/web/api/fix.py +134 -0
  463. patchi/web/api/guard.py +65 -0
  464. patchi/web/api/hosted.py +128 -0
  465. patchi/web/api/hosted_v2.py +234 -0
  466. patchi/web/api/live_testing.py +699 -0
  467. patchi/web/api/scan.py +582 -0
  468. patchi/web/api/smart.py +111 -0
  469. patchi/web/api/tenant.py +117 -0
  470. patchi/web/api/tools.py +85 -0
  471. patchi/web/api_legacy.py +2401 -0
  472. patchi/web/app.py +310 -0
  473. patchi/web/events.py +483 -0
  474. patchi/web/routes/__init__.py +1 -0
  475. patchi/web/routes/assurance.py +541 -0
  476. patchi/web/routes/brain.py +36 -0
  477. patchi/web/routes/charter.py +227 -0
  478. patchi/web/routes/chat.py +23 -0
  479. patchi/web/routes/dashboard.py +558 -0
  480. patchi/web/routes/dashboard_v2.py +402 -0
  481. patchi/web/routes/findings.py +330 -0
  482. patchi/web/routes/guard.py +35 -0
  483. patchi/web/routes/history.py +126 -0
  484. patchi/web/routes/landing.py +52 -0
  485. patchi/web/routes/live_testing.py +24 -0
  486. patchi/web/routes/review.py +57 -0
  487. patchi/web/routes/self_improvement.py +110 -0
  488. patchi/web/routes/settings.py +49 -0
  489. patchi/web/routes/tokens.py +29 -0
  490. patchi/web/spawn.py +139 -0
  491. patchi/web/ws.py +141 -0
  492. patchi-0.7.5.dist-info/METADATA +315 -0
  493. patchi-0.7.5.dist-info/RECORD +496 -0
  494. patchi-0.7.5.dist-info/WHEEL +5 -0
  495. patchi-0.7.5.dist-info/licenses/LICENSE +60 -0
  496. 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