cycode 3.15.3.dev8__tar.gz → 3.15.4.dev2__tar.gz

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 (224) hide show
  1. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/PKG-INFO +3 -1
  2. cycode-3.15.4.dev2/cycode/__init__.py +1 -0
  3. cycode-3.15.4.dev2/cycode/cli/apps/ai_guardrails/command_utils.py +25 -0
  4. cycode-3.15.4.dev2/cycode/cli/apps/ai_guardrails/consts.py +23 -0
  5. cycode-3.15.4.dev2/cycode/cli/apps/ai_guardrails/hooks_manager.py +253 -0
  6. cycode-3.15.4.dev2/cycode/cli/apps/ai_guardrails/ides/__init__.py +45 -0
  7. cycode-3.15.4.dev2/cycode/cli/apps/ai_guardrails/ides/_plugin_utils.py +73 -0
  8. cycode-3.15.4.dev2/cycode/cli/apps/ai_guardrails/ides/base.py +176 -0
  9. cycode-3.15.4.dev2/cycode/cli/apps/ai_guardrails/ides/claude_code.py +369 -0
  10. cycode-3.15.4.dev2/cycode/cli/apps/ai_guardrails/ides/codex.py +310 -0
  11. cycode-3.15.4.dev2/cycode/cli/apps/ai_guardrails/ides/cursor.py +119 -0
  12. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/ai_guardrails/install_command.py +14 -23
  13. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/ai_guardrails/scan/handlers.py +102 -101
  14. cycode-3.15.4.dev2/cycode/cli/apps/ai_guardrails/scan/payload.py +34 -0
  15. cycode-3.15.4.dev2/cycode/cli/apps/ai_guardrails/scan/scan_command.py +154 -0
  16. cycode-3.15.4.dev2/cycode/cli/apps/ai_guardrails/scan/types.py +43 -0
  17. cycode-3.15.4.dev2/cycode/cli/apps/ai_guardrails/session_start_command.py +91 -0
  18. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/ai_guardrails/status_command.py +13 -16
  19. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/ai_guardrails/uninstall_command.py +12 -22
  20. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/utils/jwt_utils.py +8 -0
  21. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/pyproject.toml +3 -1
  22. cycode-3.15.3.dev8/cycode/__init__.py +0 -1
  23. cycode-3.15.3.dev8/cycode/cli/apps/ai_guardrails/command_utils.py +0 -68
  24. cycode-3.15.3.dev8/cycode/cli/apps/ai_guardrails/consts.py +0 -155
  25. cycode-3.15.3.dev8/cycode/cli/apps/ai_guardrails/hooks_manager.py +0 -282
  26. cycode-3.15.3.dev8/cycode/cli/apps/ai_guardrails/scan/claude_config.py +0 -159
  27. cycode-3.15.3.dev8/cycode/cli/apps/ai_guardrails/scan/cursor_config.py +0 -36
  28. cycode-3.15.3.dev8/cycode/cli/apps/ai_guardrails/scan/payload.py +0 -275
  29. cycode-3.15.3.dev8/cycode/cli/apps/ai_guardrails/scan/response_builders.py +0 -135
  30. cycode-3.15.3.dev8/cycode/cli/apps/ai_guardrails/scan/scan_command.py +0 -142
  31. cycode-3.15.3.dev8/cycode/cli/apps/ai_guardrails/scan/types.py +0 -65
  32. cycode-3.15.3.dev8/cycode/cli/apps/ai_guardrails/session_start_command.py +0 -155
  33. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/LICENCE +0 -0
  34. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/README.md +0 -0
  35. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/__main__.py +0 -0
  36. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/__init__.py +0 -0
  37. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/app.py +0 -0
  38. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/__init__.py +0 -0
  39. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/activation_manager.py +0 -0
  40. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/ai_guardrails/__init__.py +0 -0
  41. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/ai_guardrails/scan/__init__.py +0 -0
  42. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/ai_guardrails/scan/consts.py +0 -0
  43. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/ai_guardrails/scan/policy.py +0 -0
  44. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/ai_guardrails/scan/utils.py +0 -0
  45. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/ai_remediation/__init__.py +0 -0
  46. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/ai_remediation/ai_remediation_command.py +0 -0
  47. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/ai_remediation/apply_fix.py +0 -0
  48. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/ai_remediation/print_remediation.py +0 -0
  49. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/api/__init__.py +0 -0
  50. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/api/api_command.py +0 -0
  51. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/api/openapi_spec.py +0 -0
  52. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/auth/__init__.py +0 -0
  53. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/auth/auth_command.py +0 -0
  54. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/auth/auth_common.py +0 -0
  55. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/auth/auth_manager.py +0 -0
  56. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/auth/models.py +0 -0
  57. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/configure/__init__.py +0 -0
  58. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/configure/configure_command.py +0 -0
  59. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/configure/consts.py +0 -0
  60. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/configure/messages.py +0 -0
  61. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/configure/prompts.py +0 -0
  62. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/ignore/__init__.py +0 -0
  63. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/ignore/ignore_command.py +0 -0
  64. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/mcp/__init__.py +0 -0
  65. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/mcp/mcp_command.py +0 -0
  66. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/report/__init__.py +0 -0
  67. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/report/report_command.py +0 -0
  68. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/report/sbom/__init__.py +0 -0
  69. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/report/sbom/common.py +0 -0
  70. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/report/sbom/path/__init__.py +0 -0
  71. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/report/sbom/path/path_command.py +0 -0
  72. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/report/sbom/repository_url/__init__.py +0 -0
  73. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/report/sbom/repository_url/repository_url_command.py +0 -0
  74. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/report/sbom/sbom_command.py +0 -0
  75. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/report/sbom/sbom_report_file.py +0 -0
  76. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/report_import/__init__.py +0 -0
  77. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/report_import/report_import_command.py +0 -0
  78. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/report_import/sbom/__init__.py +0 -0
  79. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/report_import/sbom/sbom_command.py +0 -0
  80. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/sca_options.py +0 -0
  81. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/scan/__init__.py +0 -0
  82. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/scan/aggregation_report.py +0 -0
  83. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/scan/code_scanner.py +0 -0
  84. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/scan/commit_history/__init__.py +0 -0
  85. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/scan/commit_history/commit_history_command.py +0 -0
  86. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/scan/commit_range_scanner.py +0 -0
  87. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/scan/detection_excluder.py +0 -0
  88. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/scan/path/__init__.py +0 -0
  89. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/scan/path/path_command.py +0 -0
  90. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/scan/pre_commit/__init__.py +0 -0
  91. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/scan/pre_commit/pre_commit_command.py +0 -0
  92. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/scan/pre_push/__init__.py +0 -0
  93. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/scan/pre_push/pre_push_command.py +0 -0
  94. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/scan/pre_receive/__init__.py +0 -0
  95. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/scan/pre_receive/pre_receive_command.py +0 -0
  96. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/scan/remote_url_resolver.py +0 -0
  97. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/scan/repository/__init__.py +0 -0
  98. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/scan/repository/repository_command.py +0 -0
  99. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/scan/scan_ci/__init__.py +0 -0
  100. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/scan/scan_ci/ci_integrations.py +0 -0
  101. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/scan/scan_ci/scan_ci_command.py +0 -0
  102. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/scan/scan_command.py +0 -0
  103. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/scan/scan_parameters.py +0 -0
  104. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/scan/scan_result.py +0 -0
  105. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/status/__init__.py +0 -0
  106. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/status/get_cli_status.py +0 -0
  107. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/status/models.py +0 -0
  108. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/status/status_command.py +0 -0
  109. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/apps/status/version_command.py +0 -0
  110. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/cli_types.py +0 -0
  111. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/config.py +0 -0
  112. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/console.py +0 -0
  113. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/consts.py +0 -0
  114. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/exceptions/__init__.py +0 -0
  115. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/exceptions/custom_exceptions.py +0 -0
  116. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/exceptions/handle_ai_remediation_errors.py +0 -0
  117. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/exceptions/handle_auth_errors.py +0 -0
  118. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/exceptions/handle_errors.py +0 -0
  119. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/exceptions/handle_report_sbom_errors.py +0 -0
  120. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/exceptions/handle_scan_errors.py +0 -0
  121. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/files_collector/__init__.py +0 -0
  122. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/files_collector/commit_range_documents.py +0 -0
  123. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/files_collector/documents_walk_ignore.py +0 -0
  124. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/files_collector/file_excluder.py +0 -0
  125. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/files_collector/iac/__init__.py +0 -0
  126. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/files_collector/iac/tf_content_generator.py +0 -0
  127. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/files_collector/models/__init__.py +0 -0
  128. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/files_collector/models/in_memory_zip.py +0 -0
  129. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/files_collector/path_documents.py +0 -0
  130. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/files_collector/repository_documents.py +0 -0
  131. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/files_collector/sca/__init__.py +0 -0
  132. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/files_collector/sca/base_restore_dependencies.py +0 -0
  133. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/files_collector/sca/go/__init__.py +0 -0
  134. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/files_collector/sca/go/restore_go_dependencies.py +0 -0
  135. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/files_collector/sca/maven/__init__.py +0 -0
  136. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/files_collector/sca/maven/restore_gradle_dependencies.py +0 -0
  137. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/files_collector/sca/maven/restore_maven_dependencies.py +0 -0
  138. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/files_collector/sca/npm/__init__.py +0 -0
  139. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/files_collector/sca/npm/restore_deno_dependencies.py +0 -0
  140. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/files_collector/sca/npm/restore_npm_dependencies.py +0 -0
  141. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/files_collector/sca/npm/restore_pnpm_dependencies.py +0 -0
  142. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/files_collector/sca/npm/restore_yarn_dependencies.py +0 -0
  143. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/files_collector/sca/nuget/__init__.py +0 -0
  144. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/files_collector/sca/nuget/restore_nuget_dependencies.py +0 -0
  145. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/files_collector/sca/php/__init__.py +0 -0
  146. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/files_collector/sca/php/restore_composer_dependencies.py +0 -0
  147. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/files_collector/sca/python/__init__.py +0 -0
  148. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/files_collector/sca/python/restore_pipenv_dependencies.py +0 -0
  149. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/files_collector/sca/python/restore_poetry_dependencies.py +0 -0
  150. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/files_collector/sca/python/restore_uv_dependencies.py +0 -0
  151. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/files_collector/sca/ruby/__init__.py +0 -0
  152. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/files_collector/sca/ruby/restore_ruby_dependencies.py +0 -0
  153. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/files_collector/sca/sbt/__init__.py +0 -0
  154. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/files_collector/sca/sbt/restore_sbt_dependencies.py +0 -0
  155. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/files_collector/sca/sca_file_collector.py +0 -0
  156. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/files_collector/walk_ignore.py +0 -0
  157. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/files_collector/zip_documents.py +0 -0
  158. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/logger.py +0 -0
  159. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/main.py +0 -0
  160. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/models.py +0 -0
  161. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/printers/__init__.py +0 -0
  162. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/printers/console_printer.py +0 -0
  163. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/printers/json_printer.py +0 -0
  164. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/printers/printer_base.py +0 -0
  165. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/printers/rich_printer.py +0 -0
  166. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/printers/tables/__init__.py +0 -0
  167. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/printers/tables/sca_table_printer.py +0 -0
  168. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/printers/tables/table.py +0 -0
  169. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/printers/tables/table_models.py +0 -0
  170. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/printers/tables/table_printer.py +0 -0
  171. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/printers/tables/table_printer_base.py +0 -0
  172. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/printers/text_printer.py +0 -0
  173. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/printers/utils/__init__.py +0 -0
  174. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/printers/utils/code_snippet_syntax.py +0 -0
  175. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/printers/utils/detection_data.py +0 -0
  176. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/printers/utils/detection_ordering/__init__.py +0 -0
  177. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/printers/utils/detection_ordering/common_ordering.py +0 -0
  178. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/printers/utils/detection_ordering/sca_ordering.py +0 -0
  179. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/printers/utils/rich_helpers.py +0 -0
  180. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/user_settings/__init__.py +0 -0
  181. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/user_settings/base_file_manager.py +0 -0
  182. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/user_settings/config_file_manager.py +0 -0
  183. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/user_settings/configuration_manager.py +0 -0
  184. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/user_settings/credentials_manager.py +0 -0
  185. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/user_settings/jwt_creator.py +0 -0
  186. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/utils/__init__.py +0 -0
  187. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/utils/binary_utils.py +0 -0
  188. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/utils/enum_utils.py +0 -0
  189. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/utils/get_api_client.py +0 -0
  190. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/utils/git_proxy.py +0 -0
  191. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/utils/ignore_utils.py +0 -0
  192. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/utils/path_utils.py +0 -0
  193. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/utils/progress_bar.py +0 -0
  194. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/utils/scan_batch.py +0 -0
  195. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/utils/scan_utils.py +0 -0
  196. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/utils/shell_executor.py +0 -0
  197. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/utils/string_utils.py +0 -0
  198. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/utils/task_timer.py +0 -0
  199. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/utils/url_utils.py +0 -0
  200. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/utils/version_checker.py +0 -0
  201. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cli/utils/yaml_utils.py +0 -0
  202. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/config.py +0 -0
  203. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cyclient/__init__.py +0 -0
  204. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cyclient/ai_security_manager_client.py +0 -0
  205. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cyclient/ai_security_manager_service_config.py +0 -0
  206. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cyclient/auth_client.py +0 -0
  207. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cyclient/base_token_auth_client.py +0 -0
  208. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cyclient/cli_activation_client.py +0 -0
  209. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cyclient/client_creator.py +0 -0
  210. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cyclient/config.py +0 -0
  211. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cyclient/config_dev.py +0 -0
  212. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cyclient/cycode_client.py +0 -0
  213. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cyclient/cycode_client_base.py +0 -0
  214. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cyclient/cycode_dev_based_client.py +0 -0
  215. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cyclient/cycode_oidc_based_client.py +0 -0
  216. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cyclient/cycode_token_based_client.py +0 -0
  217. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cyclient/headers.py +0 -0
  218. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cyclient/import_sbom_client.py +0 -0
  219. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cyclient/logger.py +0 -0
  220. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cyclient/models.py +0 -0
  221. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cyclient/report_client.py +0 -0
  222. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cyclient/scan_client.py +0 -0
  223. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/cyclient/scan_config_base.py +0 -0
  224. {cycode-3.15.3.dev8 → cycode-3.15.4.dev2}/cycode/logger.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cycode
3
- Version: 3.15.3.dev8
3
+ Version: 3.15.4.dev2
4
4
  Summary: Boost security in your dev lifecycle via SAST, SCA, Secrets & IaC scanning.
5
5
  License-Expression: MIT
6
6
  License-File: LICENCE
@@ -34,6 +34,8 @@ Requires-Dist: pyyaml (>=6.0,<7.0)
34
34
  Requires-Dist: requests (>=2.32.4,<3.0)
35
35
  Requires-Dist: rich (>=13.9.4,<14)
36
36
  Requires-Dist: tenacity (>=9.0.0,<9.1.0)
37
+ Requires-Dist: tomli (>=2.0.0,<3.0.0) ; python_version < "3.11"
38
+ Requires-Dist: tomli-w (>=1.0.0,<2.0.0)
37
39
  Requires-Dist: typer (>=0.15.3,<0.16.0)
38
40
  Requires-Dist: urllib3 (>=2.4.0,<3.0.0)
39
41
  Project-URL: Repository, https://github.com/cycodehq/cycode-cli
@@ -0,0 +1 @@
1
+ __version__ = '3.15.4.dev2' # DON'T TOUCH. Placeholder. Will be filled automatically on poetry build from Git Tag
@@ -0,0 +1,25 @@
1
+ """Common utilities for AI guardrails commands."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ import typer
8
+ from rich.console import Console
9
+
10
+ console = Console()
11
+
12
+
13
+ def validate_scope(scope: str, allowed_scopes: tuple[str, ...] = ('user', 'repo')) -> None:
14
+ """Validate scope parameter."""
15
+ if scope not in allowed_scopes:
16
+ scopes_list = ', '.join(f'"{s}"' for s in allowed_scopes)
17
+ console.print(f'[red]Error:[/] Invalid scope. Use {scopes_list}.', style='bold red')
18
+ raise typer.Exit(1)
19
+
20
+
21
+ def resolve_repo_path(scope: str, repo_path: Optional[Path]) -> Optional[Path]:
22
+ """Default repo_path to cwd for 'repo' scope; leave None for 'user' scope."""
23
+ if scope == 'repo' and repo_path is None:
24
+ return Path(os.getcwd())
25
+ return repo_path
@@ -0,0 +1,23 @@
1
+ """Shared constants and policy/mode enums for AI guardrails."""
2
+
3
+ from enum import Enum
4
+
5
+
6
+ class PolicyMode(str, Enum):
7
+ """Policy enforcement mode for global mode and per-feature actions."""
8
+
9
+ BLOCK = 'block'
10
+ WARN = 'warn'
11
+
12
+
13
+ class InstallMode(str, Enum):
14
+ """Installation mode for ai-guardrails install command."""
15
+
16
+ REPORT = 'report'
17
+ BLOCK = 'block'
18
+
19
+
20
+ # Base CLI commands invoked from installed hooks. IDE classes append --ide flags
21
+ # (and any other suffix) on top of these.
22
+ CYCODE_SCAN_PROMPT_COMMAND = 'cycode ai-guardrails scan'
23
+ CYCODE_SESSION_START_COMMAND = 'cycode ai-guardrails session-start'
@@ -0,0 +1,253 @@
1
+ """Hooks manager for AI guardrails.
2
+
3
+ Generic install/uninstall/status logic. All IDE-specific concerns (settings
4
+ paths, hooks template shape) live on the `IDE` instance; this module is
5
+ agent-agnostic.
6
+ """
7
+
8
+ import copy
9
+ import json
10
+ from pathlib import Path
11
+ from typing import Optional
12
+
13
+ import yaml
14
+
15
+ from cycode.cli.apps.ai_guardrails.consts import PolicyMode
16
+ from cycode.cli.apps.ai_guardrails.ides.base import IDE
17
+ from cycode.cli.apps.ai_guardrails.scan.consts import DEFAULT_POLICY, POLICY_FILE_NAME
18
+ from cycode.logger import get_logger
19
+
20
+ logger = get_logger('AI Guardrails Hooks')
21
+
22
+
23
+ _CYCODE_COMMAND_MARKERS = ('cycode ai-guardrails',)
24
+
25
+
26
+ def _is_cycode_command(command: str) -> bool:
27
+ return any(marker in command for marker in _CYCODE_COMMAND_MARKERS)
28
+
29
+
30
+ def is_cycode_hook_entry(entry: dict) -> bool:
31
+ """True if any hook inside ``entry`` is owned by Cycode."""
32
+ command = entry.get('command', '')
33
+ if _is_cycode_command(command):
34
+ return True
35
+
36
+ for hook in entry.get('hooks', []):
37
+ if isinstance(hook, dict) and _is_cycode_command(hook.get('command', '')):
38
+ return True
39
+
40
+ return False
41
+
42
+
43
+ def _strip_cycode_from_entry(entry: dict) -> Optional[dict]:
44
+ """Remove Cycode hooks from ``entry`` and return the remainder.
45
+
46
+ Returns ``None`` when nothing useful remains (Cursor-flat Cycode entry, or
47
+ every nested hook was Cycode). Non-Cycode hooks co-located in the same
48
+ entry are preserved.
49
+ """
50
+ # Cursor format: the entry itself IS a single hook command.
51
+ if 'command' in entry and 'hooks' not in entry:
52
+ return None if _is_cycode_command(entry.get('command', '')) else entry
53
+
54
+ # Claude Code / Codex format: nested `hooks` list inside the entry.
55
+ nested = entry.get('hooks')
56
+ if isinstance(nested, list):
57
+ kept = [h for h in nested if not (isinstance(h, dict) and _is_cycode_command(h.get('command', '')))]
58
+ if not kept:
59
+ return None
60
+ if len(kept) == len(nested):
61
+ return entry # nothing Cycode-shaped inside; preserve identity
62
+ return {**entry, 'hooks': kept}
63
+
64
+ # Entry has neither shape we recognize — leave it alone defensively.
65
+ return entry
66
+
67
+
68
+ def _load_hooks_file(hooks_path: Path) -> Optional[dict]:
69
+ if not hooks_path.exists():
70
+ return None
71
+ try:
72
+ return json.loads(hooks_path.read_text(encoding='utf-8'))
73
+ except Exception as e:
74
+ logger.debug('Failed to load hooks file', exc_info=e)
75
+ return None
76
+
77
+
78
+ def _save_hooks_file(hooks_path: Path, hooks_config: dict) -> bool:
79
+ try:
80
+ hooks_path.parent.mkdir(parents=True, exist_ok=True)
81
+ hooks_path.write_text(json.dumps(hooks_config, indent=2), encoding='utf-8')
82
+ return True
83
+ except Exception as e:
84
+ logger.error('Failed to save hooks file', exc_info=e)
85
+ return False
86
+
87
+
88
+ def _load_policy_dict(policy_path: Path) -> dict:
89
+ if not policy_path.exists():
90
+ return copy.deepcopy(DEFAULT_POLICY)
91
+ try:
92
+ existing = yaml.safe_load(policy_path.read_text(encoding='utf-8')) or {}
93
+ except Exception:
94
+ existing = {}
95
+ return {**copy.deepcopy(DEFAULT_POLICY), **existing}
96
+
97
+
98
+ def create_policy_file(scope: str, mode: PolicyMode, repo_path: Optional[Path] = None) -> tuple[bool, str]:
99
+ """Create or update the ai-guardrails.yaml policy file.
100
+
101
+ If the file already exists, only the mode field is updated; otherwise a new
102
+ file is created from the default policy.
103
+ """
104
+ config_dir = repo_path / '.cycode' if scope == 'repo' and repo_path else Path.home() / '.cycode'
105
+ policy_path = config_dir / POLICY_FILE_NAME
106
+
107
+ policy = _load_policy_dict(policy_path)
108
+ policy['mode'] = mode.value
109
+
110
+ try:
111
+ config_dir.mkdir(parents=True, exist_ok=True)
112
+ policy_path.write_text(yaml.dump(policy, default_flow_style=False, sort_keys=False), encoding='utf-8')
113
+ return True, f'AI guardrails policy ({mode.value} mode) set: {policy_path}'
114
+ except Exception as e:
115
+ logger.error('Failed to create policy file', exc_info=e)
116
+ return False, f'Failed to create policy file: {policy_path}'
117
+
118
+
119
+ def install_hooks(
120
+ ide: IDE,
121
+ scope: str = 'user',
122
+ repo_path: Optional[Path] = None,
123
+ report_mode: bool = False,
124
+ ) -> tuple[bool, str]:
125
+ """Install Cycode AI guardrails hooks for ``ide``."""
126
+ hooks_path = ide.settings_path(scope, repo_path)
127
+
128
+ existing = _load_hooks_file(hooks_path) or {'version': 1, 'hooks': {}}
129
+ existing.setdefault('version', 1)
130
+ existing.setdefault('hooks', {})
131
+
132
+ rendered = ide.render_hooks_config(async_mode=report_mode)
133
+
134
+ for event, entries in rendered['hooks'].items():
135
+ existing['hooks'].setdefault(event, [])
136
+ existing['hooks'][event] = [
137
+ stripped for e in existing['hooks'][event] if (stripped := _strip_cycode_from_entry(e)) is not None
138
+ ]
139
+ for entry in entries:
140
+ existing['hooks'][event].append(entry)
141
+
142
+ if not _save_hooks_file(hooks_path, existing):
143
+ return False, f'Failed to install hooks to {hooks_path}'
144
+
145
+ message = f'AI guardrails hooks installed: {hooks_path}'
146
+
147
+ # IDE-specific extras (e.g. Codex enables a TOML feature flag).
148
+ extra_ok, extra_message = ide.post_install(scope, repo_path)
149
+ if not extra_ok:
150
+ return False, extra_message
151
+ if extra_message:
152
+ message = f'{message}\n {extra_message}'
153
+
154
+ return True, message
155
+
156
+
157
+ def _strip_cycode_entries(existing: dict) -> bool:
158
+ """Mutate ``existing`` to drop Cycode hooks (surgically). Return True if anything changed."""
159
+ modified = False
160
+ for event in list(existing.get('hooks', {}).keys()):
161
+ before = existing['hooks'][event]
162
+ after: list = []
163
+ for e in before:
164
+ stripped = _strip_cycode_from_entry(e)
165
+ if stripped is None:
166
+ modified = True
167
+ continue
168
+ if stripped is not e:
169
+ modified = True
170
+ after.append(stripped)
171
+ if not after:
172
+ del existing['hooks'][event]
173
+ else:
174
+ existing['hooks'][event] = after
175
+ return modified
176
+
177
+
178
+ def _persist_uninstall(hooks_path: Path, existing: dict, modified: bool) -> tuple[bool, str]:
179
+ """Apply the uninstall result to disk and return ``(success, message)``."""
180
+ if not modified:
181
+ return True, 'No Cycode hooks found to remove'
182
+ if not existing.get('hooks'):
183
+ try:
184
+ hooks_path.unlink()
185
+ except Exception as e:
186
+ logger.debug('Failed to delete hooks file', exc_info=e)
187
+ return False, f'Failed to remove hooks file: {hooks_path}'
188
+ return True, f'Removed hooks file: {hooks_path}'
189
+ if not _save_hooks_file(hooks_path, existing):
190
+ return False, f'Failed to update hooks file: {hooks_path}'
191
+ return True, f'Cycode hooks removed from: {hooks_path}'
192
+
193
+
194
+ def uninstall_hooks(ide: IDE, scope: str = 'user', repo_path: Optional[Path] = None) -> tuple[bool, str]:
195
+ """Remove Cycode AI guardrails hooks for ``ide``."""
196
+ hooks_path = ide.settings_path(scope, repo_path)
197
+
198
+ existing = _load_hooks_file(hooks_path)
199
+ if existing is None:
200
+ return True, f'No hooks file found at {hooks_path}'
201
+
202
+ modified = _strip_cycode_entries(existing)
203
+ file_ok, message = _persist_uninstall(hooks_path, existing, modified)
204
+ if not file_ok:
205
+ return False, message
206
+
207
+ extra_ok, extra_message = ide.post_uninstall(scope, repo_path)
208
+ if not extra_ok:
209
+ return False, extra_message
210
+ if extra_message:
211
+ message = f'{message}\n {extra_message}'
212
+ return True, message
213
+
214
+
215
+ def get_hooks_status(ide: IDE, scope: str = 'user', repo_path: Optional[Path] = None) -> dict:
216
+ """Return installation status of Cycode hooks for ``ide``."""
217
+ hooks_path = ide.settings_path(scope, repo_path)
218
+
219
+ status: dict = {
220
+ 'scope': scope,
221
+ 'ide': ide.name,
222
+ 'ide_name': ide.display_name,
223
+ 'hooks_path': str(hooks_path),
224
+ 'file_exists': hooks_path.exists(),
225
+ 'cycode_installed': False,
226
+ 'hooks': {},
227
+ }
228
+
229
+ existing = _load_hooks_file(hooks_path)
230
+ if existing is None:
231
+ return status
232
+
233
+ has_cycode_hooks = False
234
+ for event in ide.hook_events:
235
+ # '<event>:<matcher>' filters entries to a specific tool/matcher.
236
+ if ':' in event:
237
+ actual_event, matcher_prefix = event.split(':', 1)
238
+ all_entries = existing.get('hooks', {}).get(actual_event, [])
239
+ entries = [e for e in all_entries if e.get('matcher', '').startswith(matcher_prefix)]
240
+ else:
241
+ entries = existing.get('hooks', {}).get(event, [])
242
+
243
+ cycode_entries = [e for e in entries if is_cycode_hook_entry(e)]
244
+ if cycode_entries:
245
+ has_cycode_hooks = True
246
+ status['hooks'][event] = {
247
+ 'total_entries': len(entries),
248
+ 'cycode_entries': len(cycode_entries),
249
+ 'enabled': len(cycode_entries) > 0,
250
+ }
251
+
252
+ status['cycode_installed'] = has_cycode_hooks
253
+ return status
@@ -0,0 +1,45 @@
1
+ """Registry of supported AI guardrails IDE integrations.
2
+
3
+ Adding a new IDE: create `ides/<name>.py` with a subclass of `IDE`, import it
4
+ here, and include an instance in the `IDES` tuple. Nothing else in the package
5
+ needs to change.
6
+ """
7
+
8
+ import typer
9
+
10
+ from cycode.cli.apps.ai_guardrails.ides.base import IDE
11
+ from cycode.cli.apps.ai_guardrails.ides.claude_code import ClaudeCode
12
+ from cycode.cli.apps.ai_guardrails.ides.codex import Codex
13
+ from cycode.cli.apps.ai_guardrails.ides.cursor import Cursor
14
+
15
+ # Single source of truth: name → singleton instance.
16
+ # `--ide` choices and install/uninstall/status iteration both derive from this.
17
+ IDES: dict[str, IDE] = {ide.name: ide for ide in (Cursor(), ClaudeCode(), Codex())}
18
+
19
+ # Default IDE used when `--ide` is omitted. Kept here so the value is colocated
20
+ # with the registry; no module outside `ides/` needs to know which IDE wins.
21
+ DEFAULT_IDE_NAME = 'cursor'
22
+
23
+
24
+ def get_ide(name: str) -> IDE:
25
+ """Look up the IDE integration registered under ``name``.
26
+
27
+ Raises ``typer.BadParameter`` when the name is unknown — surfaces as a
28
+ user-friendly CLI error rather than a KeyError stack trace.
29
+ """
30
+ ide = IDES.get(name.lower())
31
+ if ide is None:
32
+ valid = ', '.join(IDES.keys())
33
+ raise typer.BadParameter(f'Unknown IDE "{name}". Supported: {valid}.')
34
+ return ide
35
+
36
+
37
+ def resolve_ides(name: str) -> list[IDE]:
38
+ """Resolve an ``--ide`` argument to one or all IDE instances.
39
+
40
+ ``"all"`` returns every registered IDE; anything else returns a single
41
+ matching IDE (raising ``typer.BadParameter`` for unknown names).
42
+ """
43
+ if name.lower() == 'all':
44
+ return list(IDES.values())
45
+ return [get_ide(name)]
@@ -0,0 +1,73 @@
1
+ """Shared plugin-resolution helpers for IDE integrations.
2
+
3
+ Both Claude Code and Codex use the same ``<plugin>@<marketplace>`` key convention
4
+ and emit the same telemetry shape — only the marketplace layout and manifest
5
+ location differ. ``walk_enabled_plugins`` is the IDE-agnostic loop; each IDE
6
+ supplies the two callables that vary (``locate_dir`` + ``read_plugin``).
7
+ """
8
+
9
+ import json
10
+ from pathlib import Path
11
+ from typing import Any, Callable, Optional
12
+
13
+ from cycode.logger import get_logger
14
+
15
+ logger = get_logger('AI Guardrails Plugins')
16
+
17
+
18
+ def load_plugin_json(path: Path) -> Optional[dict]:
19
+ """Load a JSON file inside a plugin directory; None if missing or invalid."""
20
+ if not path.exists():
21
+ return None
22
+ try:
23
+ return json.loads(path.read_text(encoding='utf-8'))
24
+ except Exception as e:
25
+ logger.debug('Failed to load plugin file, %s', {'path': str(path)}, exc_info=e)
26
+ return None
27
+
28
+
29
+ def walk_enabled_plugins(
30
+ plugin_entries: dict[str, Any],
31
+ is_enabled: Callable[[Any], bool],
32
+ locate_dir: Callable[[str, str], Optional[Path]],
33
+ read_plugin: Callable[[Path], tuple[dict, dict]],
34
+ ) -> tuple[dict, dict]:
35
+ """Iterate enabled plugins; merge their MCP servers and metadata.
36
+
37
+ Args:
38
+ plugin_entries: ``{<plugin>@<marketplace>: settings}`` map from the IDE config.
39
+ is_enabled: returns True if ``settings`` indicates the plugin is on
40
+ (e.g. ``bool(settings)`` for Claude, ``settings.get('enabled')`` for Codex).
41
+ locate_dir: given ``(plugin_name, marketplace)``, returns the plugin's
42
+ filesystem path or None if it can't be resolved.
43
+ read_plugin: given the plugin path, returns ``(entry_fields, servers)``:
44
+ ``entry_fields`` are extra metadata to attach to the inventory entry
45
+ (name/version/description/...), ``servers`` are MCP servers contributed.
46
+
47
+ Returns ``(merged_mcp_servers, enriched_plugins)``. Plugin keys without
48
+ ``@`` (or that fail to resolve to a directory) still appear in the
49
+ inventory with just ``{'enabled': True}`` so we don't silently drop them.
50
+ """
51
+ merged_mcp: dict = {}
52
+ enriched: dict = {}
53
+
54
+ for plugin_key, settings in plugin_entries.items():
55
+ if not is_enabled(settings):
56
+ continue
57
+
58
+ entry: dict = {'enabled': True}
59
+ enriched[plugin_key] = entry
60
+
61
+ if '@' not in plugin_key:
62
+ continue
63
+ plugin_name, marketplace = plugin_key.split('@', 1)
64
+
65
+ plugin_dir = locate_dir(plugin_name, marketplace)
66
+ if plugin_dir is None:
67
+ continue
68
+
69
+ plugin_fields, servers = read_plugin(plugin_dir)
70
+ entry.update(plugin_fields)
71
+ merged_mcp.update(servers)
72
+
73
+ return merged_mcp, enriched
@@ -0,0 +1,176 @@
1
+ """Base abstractions for AI guardrails IDE integrations.
2
+
3
+ Each AI IDE (Cursor, Claude Code, …) is represented by a subclass of `IDE`
4
+ that consolidates every IDE-specific concern in a single module: settings file
5
+ paths, hooks template rendering, payload parsing, response building, and any
6
+ IDE-specific session-context lookup.
7
+
8
+ Adding a new IDE is a matter of:
9
+ 1. Subclassing `IDE` and implementing the abstract methods.
10
+ 2. Registering the instance in `cycode/cli/apps/ai_guardrails/ides/__init__.py`.
11
+
12
+ The `HookDecision` dataclass is the canonical, IDE-agnostic return type for
13
+ event handlers; `IDE.build_hook_response` translates it into the IDE-specific
14
+ JSON response shape that the IDE expects on stdout.
15
+ """
16
+
17
+ from abc import ABC, abstractmethod
18
+ from dataclasses import dataclass
19
+ from enum import Enum
20
+ from pathlib import Path
21
+ from typing import ClassVar, Optional
22
+
23
+ from cycode.cli.apps.ai_guardrails.scan.payload import AIHookPayload
24
+ from cycode.cli.apps.ai_guardrails.scan.types import AiHookEventType
25
+
26
+
27
+ class DecisionAction(str, Enum):
28
+ """Canonical decision action returned by event handlers."""
29
+
30
+ ALLOW = 'allow'
31
+ DENY = 'deny'
32
+ ASK = 'ask'
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class HookDecision:
37
+ """Canonical, IDE-agnostic decision returned by event handlers.
38
+
39
+ Carries the event type so `IDE.build_hook_response` can pick the right
40
+ IDE-specific response shape (Cursor's "permission" style for tool events
41
+ vs. "continue" style for prompts; Claude Code's "hookSpecificOutput"
42
+ vs. "decision: block").
43
+ """
44
+
45
+ action: DecisionAction
46
+ event_type: AiHookEventType
47
+ user_message: Optional[str] = None
48
+ agent_message: Optional[str] = None
49
+
50
+ @classmethod
51
+ def allow(cls, event_type: AiHookEventType) -> 'HookDecision':
52
+ return cls(action=DecisionAction.ALLOW, event_type=event_type)
53
+
54
+ @classmethod
55
+ def deny(
56
+ cls, event_type: AiHookEventType, user_message: str, agent_message: Optional[str] = None
57
+ ) -> 'HookDecision':
58
+ return cls(
59
+ action=DecisionAction.DENY,
60
+ event_type=event_type,
61
+ user_message=user_message,
62
+ agent_message=agent_message,
63
+ )
64
+
65
+ @classmethod
66
+ def ask(cls, event_type: AiHookEventType, user_message: str, agent_message: Optional[str] = None) -> 'HookDecision':
67
+ return cls(
68
+ action=DecisionAction.ASK,
69
+ event_type=event_type,
70
+ user_message=user_message,
71
+ agent_message=agent_message,
72
+ )
73
+
74
+
75
+ class IDE(ABC):
76
+ """Per-IDE integration. Owns every IDE-specific concern in a single module.
77
+
78
+ Subclasses declare identity via class attributes and implement the abstract
79
+ methods. Defaults are provided for `get_user_email` and `get_session_context`
80
+ so IDEs without those capabilities (e.g. no plugin system, no local
81
+ account file) can skip them.
82
+ """
83
+
84
+ # CLI value passed to --ide (e.g. 'cursor', 'claude-code').
85
+ name: ClassVar[str]
86
+ # Human-friendly name for output ('Cursor', 'Claude Code').
87
+ display_name: ClassVar[str]
88
+ # Event names for status display. Use '<event>:<matcher>' for IDEs that
89
+ # qualify a single hook by a sub-matcher (e.g. Claude Code's PreToolUse:Read).
90
+ hook_events: ClassVar[list[str]]
91
+
92
+ # --- install / status ---
93
+
94
+ @abstractmethod
95
+ def settings_path(self, scope: str, repo_path: Optional[Path] = None) -> Path:
96
+ """Return the hooks/settings file path for the given scope.
97
+
98
+ `scope` is 'user' or 'repo'. `repo_path` is required when scope == 'repo'.
99
+ """
100
+
101
+ @abstractmethod
102
+ def render_hooks_config(self, async_mode: bool = False) -> dict:
103
+ """Return the settings blob to merge into the IDE's settings file.
104
+
105
+ Shape is IDE-specific (Cursor uses a flat ``{event: [{command}]}`` dict;
106
+ Claude Code uses a nested ``{event: [{hooks: [{type, command}]}]}``
107
+ dict). Both share the outer ``{"hooks": ...}`` wrapper so
108
+ ``hooks_manager`` can treat them uniformly.
109
+ """
110
+
111
+ def post_install(self, scope: str, repo_path: Optional[Path] = None) -> tuple[bool, str]:
112
+ """Run IDE-specific actions after the hooks file is written.
113
+
114
+ Default: no-op success. Override to perform extra setup that doesn't
115
+ belong in the hooks file itself — e.g. Codex enables a
116
+ ``[features] codex_hooks = true`` flag in its TOML config.
117
+
118
+ Returns ``(success, message)``. If ``success`` is False, the overall
119
+ install is considered failed.
120
+ """
121
+ return True, ''
122
+
123
+ def post_uninstall(self, scope: str, repo_path: Optional[Path] = None) -> tuple[bool, str]:
124
+ """Run IDE-specific cleanup after the hooks file is removed.
125
+
126
+ Default: no-op success. Override to undo whatever ``post_install``
127
+ wrote outside the hooks file.
128
+ """
129
+ return True, ''
130
+
131
+ # --- runtime scan ---
132
+
133
+ @abstractmethod
134
+ def matches_payload(self, raw_payload: dict) -> bool:
135
+ """Return True if ``raw_payload`` originated from this IDE.
136
+
137
+ Prevents double-processing when an IDE forwards another IDE's hook
138
+ event (e.g. Cursor reading Claude Code hooks from ~/.claude/settings.json).
139
+ """
140
+
141
+ @abstractmethod
142
+ def parse_hook_payload(self, raw_payload: dict) -> AIHookPayload:
143
+ """Normalize a raw stdin payload into the canonical ``AIHookPayload``."""
144
+
145
+ @abstractmethod
146
+ def build_hook_response(self, decision: HookDecision) -> dict:
147
+ """Translate a canonical ``HookDecision`` into the IDE-specific JSON.
148
+
149
+ The result is what ``scan_command`` writes to stdout for the IDE to
150
+ act on.
151
+ """
152
+
153
+ # --- session lifecycle (optional; sensible defaults) ---
154
+
155
+ def build_session_payload(self, raw_payload: dict) -> AIHookPayload:
156
+ """Build a session-start payload from the raw stdin payload.
157
+
158
+ Default: a minimal payload tagged with this IDE's ``name``. IDEs
159
+ that need to enrich with transcript/version info should override.
160
+ """
161
+ return AIHookPayload(ide_provider=self.name)
162
+
163
+ def get_user_email(self) -> Optional[str]:
164
+ """Best-effort read of the user's email from IDE-specific config.
165
+
166
+ Default: None. Override if the IDE stores a usable account locally.
167
+ """
168
+ return None
169
+
170
+ def get_session_context(self) -> tuple[dict, dict]:
171
+ """Return ``(mcp_servers, enabled_plugins)`` for session-context reporting.
172
+
173
+ Default: empty dicts (no plugin system, no discoverable MCP config).
174
+ Override to surface MCP/plugin inventory.
175
+ """
176
+ return {}, {}