cycode 3.17.3.dev2__tar.gz → 3.17.3.dev3__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 (216) hide show
  1. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/PKG-INFO +1 -1
  2. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/__init__.py +1 -1
  3. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_guardrails/hooks_manager.py +12 -5
  4. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_guardrails/ides/__init__.py +2 -1
  5. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_guardrails/ides/base.py +19 -0
  6. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_guardrails/ides/claude_code.py +5 -1
  7. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_guardrails/ides/codex.py +4 -5
  8. cycode-3.17.3.dev3/cycode/cli/apps/ai_guardrails/ides/copilot.py +436 -0
  9. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_guardrails/ides/cursor.py +2 -2
  10. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_guardrails/scan/scan_command.py +20 -7
  11. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/pyproject.toml +1 -1
  12. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/LICENCE +0 -0
  13. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/README.md +0 -0
  14. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/__main__.py +0 -0
  15. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/__init__.py +0 -0
  16. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/app.py +0 -0
  17. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/__init__.py +0 -0
  18. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/activation_manager.py +0 -0
  19. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_guardrails/__init__.py +0 -0
  20. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_guardrails/command_utils.py +0 -0
  21. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_guardrails/consts.py +0 -0
  22. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_guardrails/ides/_plugin_utils.py +0 -0
  23. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_guardrails/install_command.py +0 -0
  24. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_guardrails/scan/__init__.py +0 -0
  25. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_guardrails/scan/consts.py +0 -0
  26. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_guardrails/scan/handlers.py +0 -0
  27. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_guardrails/scan/payload.py +0 -0
  28. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_guardrails/scan/policy.py +0 -0
  29. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_guardrails/scan/types.py +0 -0
  30. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_guardrails/scan/utils.py +0 -0
  31. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_guardrails/session_start_command.py +0 -0
  32. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_guardrails/status_command.py +0 -0
  33. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_guardrails/uninstall_command.py +0 -0
  34. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_remediation/__init__.py +0 -0
  35. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_remediation/ai_remediation_command.py +0 -0
  36. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_remediation/apply_fix.py +0 -0
  37. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_remediation/print_remediation.py +0 -0
  38. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/api/__init__.py +0 -0
  39. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/api/api_command.py +0 -0
  40. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/api/openapi_spec.py +0 -0
  41. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/auth/__init__.py +0 -0
  42. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/auth/auth_command.py +0 -0
  43. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/auth/auth_common.py +0 -0
  44. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/auth/auth_manager.py +0 -0
  45. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/auth/models.py +0 -0
  46. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/configure/__init__.py +0 -0
  47. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/configure/configure_command.py +0 -0
  48. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/configure/consts.py +0 -0
  49. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/configure/messages.py +0 -0
  50. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/configure/prompts.py +0 -0
  51. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ignore/__init__.py +0 -0
  52. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ignore/ignore_command.py +0 -0
  53. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/mcp/__init__.py +0 -0
  54. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/mcp/mcp_command.py +0 -0
  55. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/report/__init__.py +0 -0
  56. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/report/report_command.py +0 -0
  57. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/report/sbom/__init__.py +0 -0
  58. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/report/sbom/common.py +0 -0
  59. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/report/sbom/path/__init__.py +0 -0
  60. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/report/sbom/path/path_command.py +0 -0
  61. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/report/sbom/repository_url/__init__.py +0 -0
  62. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/report/sbom/repository_url/repository_url_command.py +0 -0
  63. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/report/sbom/sbom_command.py +0 -0
  64. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/report/sbom/sbom_report_file.py +0 -0
  65. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/report_import/__init__.py +0 -0
  66. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/report_import/report_import_command.py +0 -0
  67. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/report_import/sbom/__init__.py +0 -0
  68. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/report_import/sbom/sbom_command.py +0 -0
  69. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/sca_options.py +0 -0
  70. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/__init__.py +0 -0
  71. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/aggregation_report.py +0 -0
  72. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/code_scanner.py +0 -0
  73. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/commit_history/__init__.py +0 -0
  74. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/commit_history/commit_history_command.py +0 -0
  75. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/commit_range_scanner.py +0 -0
  76. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/detection_excluder.py +0 -0
  77. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/path/__init__.py +0 -0
  78. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/path/path_command.py +0 -0
  79. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/pre_commit/__init__.py +0 -0
  80. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/pre_commit/pre_commit_command.py +0 -0
  81. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/pre_push/__init__.py +0 -0
  82. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/pre_push/pre_push_command.py +0 -0
  83. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/pre_receive/__init__.py +0 -0
  84. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/pre_receive/pre_receive_command.py +0 -0
  85. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/remote_url_resolver.py +0 -0
  86. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/repository/__init__.py +0 -0
  87. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/repository/repository_command.py +0 -0
  88. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/scan_ci/__init__.py +0 -0
  89. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/scan_ci/ci_integrations.py +0 -0
  90. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/scan_ci/scan_ci_command.py +0 -0
  91. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/scan_command.py +0 -0
  92. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/scan_parameters.py +0 -0
  93. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/scan_result.py +0 -0
  94. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/status/__init__.py +0 -0
  95. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/status/get_cli_status.py +0 -0
  96. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/status/models.py +0 -0
  97. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/status/status_command.py +0 -0
  98. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/status/version_command.py +0 -0
  99. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/cli_types.py +0 -0
  100. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/config.py +0 -0
  101. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/console.py +0 -0
  102. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/consts.py +0 -0
  103. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/exceptions/__init__.py +0 -0
  104. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/exceptions/custom_exceptions.py +0 -0
  105. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/exceptions/handle_ai_remediation_errors.py +0 -0
  106. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/exceptions/handle_auth_errors.py +0 -0
  107. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/exceptions/handle_errors.py +0 -0
  108. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/exceptions/handle_report_sbom_errors.py +0 -0
  109. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/exceptions/handle_scan_errors.py +0 -0
  110. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/__init__.py +0 -0
  111. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/commit_range_documents.py +0 -0
  112. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/documents_walk_ignore.py +0 -0
  113. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/file_excluder.py +0 -0
  114. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/iac/__init__.py +0 -0
  115. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/iac/tf_content_generator.py +0 -0
  116. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/models/__init__.py +0 -0
  117. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/models/in_memory_zip.py +0 -0
  118. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/path_documents.py +0 -0
  119. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/repository_documents.py +0 -0
  120. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/__init__.py +0 -0
  121. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/base_restore_dependencies.py +0 -0
  122. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/go/__init__.py +0 -0
  123. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/go/restore_go_dependencies.py +0 -0
  124. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/maven/__init__.py +0 -0
  125. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/maven/restore_gradle_dependencies.py +0 -0
  126. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/maven/restore_maven_dependencies.py +0 -0
  127. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/npm/__init__.py +0 -0
  128. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/npm/restore_bun_dependencies.py +0 -0
  129. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/npm/restore_deno_dependencies.py +0 -0
  130. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/npm/restore_npm_dependencies.py +0 -0
  131. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/npm/restore_pnpm_dependencies.py +0 -0
  132. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/npm/restore_yarn_dependencies.py +0 -0
  133. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/nuget/__init__.py +0 -0
  134. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/nuget/restore_nuget_dependencies.py +0 -0
  135. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/php/__init__.py +0 -0
  136. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/php/restore_composer_dependencies.py +0 -0
  137. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/python/__init__.py +0 -0
  138. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/python/restore_pipenv_dependencies.py +0 -0
  139. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/python/restore_poetry_dependencies.py +0 -0
  140. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/python/restore_uv_dependencies.py +0 -0
  141. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/ruby/__init__.py +0 -0
  142. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/ruby/restore_ruby_dependencies.py +0 -0
  143. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/sbt/__init__.py +0 -0
  144. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/sbt/restore_sbt_dependencies.py +0 -0
  145. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/sca_file_collector.py +0 -0
  146. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/walk_ignore.py +0 -0
  147. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/zip_documents.py +0 -0
  148. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/logger.py +0 -0
  149. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/main.py +0 -0
  150. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/models.py +0 -0
  151. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/printers/__init__.py +0 -0
  152. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/printers/console_printer.py +0 -0
  153. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/printers/json_printer.py +0 -0
  154. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/printers/printer_base.py +0 -0
  155. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/printers/rich_printer.py +0 -0
  156. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/printers/tables/__init__.py +0 -0
  157. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/printers/tables/sca_table_printer.py +0 -0
  158. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/printers/tables/table.py +0 -0
  159. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/printers/tables/table_models.py +0 -0
  160. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/printers/tables/table_printer.py +0 -0
  161. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/printers/tables/table_printer_base.py +0 -0
  162. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/printers/text_printer.py +0 -0
  163. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/printers/utils/__init__.py +0 -0
  164. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/printers/utils/code_snippet_syntax.py +0 -0
  165. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/printers/utils/detection_data.py +0 -0
  166. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/printers/utils/detection_ordering/__init__.py +0 -0
  167. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/printers/utils/detection_ordering/common_ordering.py +0 -0
  168. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/printers/utils/detection_ordering/sca_ordering.py +0 -0
  169. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/printers/utils/rich_helpers.py +0 -0
  170. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/user_settings/__init__.py +0 -0
  171. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/user_settings/base_file_manager.py +0 -0
  172. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/user_settings/config_file_manager.py +0 -0
  173. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/user_settings/configuration_manager.py +0 -0
  174. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/user_settings/credentials_manager.py +0 -0
  175. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/user_settings/jwt_creator.py +0 -0
  176. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/utils/__init__.py +0 -0
  177. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/utils/binary_utils.py +0 -0
  178. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/utils/enum_utils.py +0 -0
  179. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/utils/get_api_client.py +0 -0
  180. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/utils/git_proxy.py +0 -0
  181. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/utils/host_info.py +0 -0
  182. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/utils/ignore_utils.py +0 -0
  183. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/utils/jwt_utils.py +0 -0
  184. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/utils/path_utils.py +0 -0
  185. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/utils/progress_bar.py +0 -0
  186. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/utils/scan_batch.py +0 -0
  187. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/utils/scan_utils.py +0 -0
  188. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/utils/shell_executor.py +0 -0
  189. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/utils/string_utils.py +0 -0
  190. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/utils/task_timer.py +0 -0
  191. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/utils/url_utils.py +0 -0
  192. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/utils/version_checker.py +0 -0
  193. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/utils/yaml_utils.py +0 -0
  194. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/config.py +0 -0
  195. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cyclient/__init__.py +0 -0
  196. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cyclient/ai_security_manager_client.py +0 -0
  197. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cyclient/ai_security_manager_service_config.py +0 -0
  198. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cyclient/auth_client.py +0 -0
  199. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cyclient/base_token_auth_client.py +0 -0
  200. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cyclient/cli_activation_client.py +0 -0
  201. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cyclient/client_creator.py +0 -0
  202. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cyclient/config.py +0 -0
  203. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cyclient/config_dev.py +0 -0
  204. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cyclient/cycode_client.py +0 -0
  205. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cyclient/cycode_client_base.py +0 -0
  206. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cyclient/cycode_dev_based_client.py +0 -0
  207. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cyclient/cycode_oidc_based_client.py +0 -0
  208. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cyclient/cycode_token_based_client.py +0 -0
  209. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cyclient/headers.py +0 -0
  210. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cyclient/import_sbom_client.py +0 -0
  211. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cyclient/logger.py +0 -0
  212. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cyclient/models.py +0 -0
  213. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cyclient/report_client.py +0 -0
  214. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cyclient/scan_client.py +0 -0
  215. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cyclient/scan_config_base.py +0 -0
  216. {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/logger.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cycode
3
- Version: 3.17.3.dev2
3
+ Version: 3.17.3.dev3
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
@@ -5,4 +5,4 @@ import time as _time
5
5
  # end-to-end scan duration from the moment the user actually triggered it.
6
6
  _BOOT_WALL: float = _time.time()
7
7
 
8
- __version__ = '3.17.3.dev2' # DON'T TOUCH. Placeholder. Will be filled automatically on poetry build from Git Tag
8
+ __version__ = '3.17.3.dev3' # DON'T TOUCH. Placeholder. Will be filled automatically on poetry build from Git Tag
@@ -22,15 +22,22 @@ logger = get_logger('AI Guardrails Hooks')
22
22
 
23
23
  _CYCODE_COMMAND_MARKERS = ('cycode ai-guardrails',)
24
24
 
25
+ # Command-carrying fields of a flat hook entry. Copilot entries use per-OS
26
+ # `bash`/`powershell` fields instead of `command`.
27
+ _COMMAND_FIELDS = ('command', 'bash', 'powershell')
28
+
25
29
 
26
30
  def _is_cycode_command(command: str) -> bool:
27
31
  return any(marker in command for marker in _CYCODE_COMMAND_MARKERS)
28
32
 
29
33
 
34
+ def _has_cycode_command_field(entry: dict) -> bool:
35
+ return any(_is_cycode_command(entry.get(field, '')) for field in _COMMAND_FIELDS)
36
+
37
+
30
38
  def is_cycode_hook_entry(entry: dict) -> bool:
31
39
  """True if any hook inside ``entry`` is owned by Cycode."""
32
- command = entry.get('command', '')
33
- if _is_cycode_command(command):
40
+ if _has_cycode_command_field(entry):
34
41
  return True
35
42
 
36
43
  for hook in entry.get('hooks', []):
@@ -47,9 +54,9 @@ def _strip_cycode_from_entry(entry: dict) -> Optional[dict]:
47
54
  every nested hook was Cycode). Non-Cycode hooks co-located in the same
48
55
  entry are preserved.
49
56
  """
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
57
+ # Cursor/Copilot format: the entry itself IS a single hook command.
58
+ if 'hooks' not in entry and any(field in entry for field in _COMMAND_FIELDS):
59
+ return None if _has_cycode_command_field(entry) else entry
53
60
 
54
61
  # Claude Code / Codex format: nested `hooks` list inside the entry.
55
62
  nested = entry.get('hooks')
@@ -10,11 +10,12 @@ import typer
10
10
  from cycode.cli.apps.ai_guardrails.ides.base import IDE
11
11
  from cycode.cli.apps.ai_guardrails.ides.claude_code import ClaudeCode
12
12
  from cycode.cli.apps.ai_guardrails.ides.codex import Codex
13
+ from cycode.cli.apps.ai_guardrails.ides.copilot import Copilot
13
14
  from cycode.cli.apps.ai_guardrails.ides.cursor import Cursor
14
15
 
15
16
  # Single source of truth: name → singleton instance.
16
17
  # `--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
+ IDES: dict[str, IDE] = {ide.name: ide for ide in (Cursor(), ClaudeCode(), Codex(), Copilot())}
18
19
 
19
20
  # Default IDE used when `--ide` is omitted. Kept here so the value is colocated
20
21
  # with the registry; no module outside `ides/` needs to know which IDE wins.
@@ -14,6 +14,7 @@ event handlers; `IDE.build_hook_response` translates it into the IDE-specific
14
14
  JSON response shape that the IDE expects on stdout.
15
15
  """
16
16
 
17
+ import platform
17
18
  from abc import ABC, abstractmethod
18
19
  from dataclasses import dataclass
19
20
  from enum import Enum
@@ -24,6 +25,24 @@ from cycode.cli.apps.ai_guardrails.scan.payload import AIHookPayload
24
25
  from cycode.cli.apps.ai_guardrails.scan.types import AiHookEventType
25
26
 
26
27
 
28
+ def shell_background_suffix(async_mode: bool) -> str:
29
+ """`' &'` when backgrounding is requested and the platform's shell supports it.
30
+
31
+ Only valid for hooks whose runner is stdin-safe under backgrounding (zsh keeps
32
+ a backgrounded command's stdin; verified for Cursor/Codex). bash/sh reattach it
33
+ to /dev/null, silently emptying the payload — hooks that run under bash (e.g.
34
+ Copilot's `bash` field) must add an explicit `<&0` redirect instead.
35
+
36
+ Windows gets no suffix: depending on the IDE, hooks may run under cmd (where a
37
+ trailing `&` is a no-op separator) or Windows PowerShell (where it's a parse
38
+ error that would fail the hook). Until the CLI can self-detach in report mode,
39
+ Windows hooks run synchronously.
40
+ """
41
+ if not async_mode or platform.system() == 'Windows':
42
+ return ''
43
+ return ' &'
44
+
45
+
27
46
  class DecisionAction(str, Enum):
28
47
  """Canonical decision action returned by event handlers."""
29
48
 
@@ -278,7 +278,11 @@ class ClaudeCode(IDE):
278
278
  }
279
279
 
280
280
  def matches_payload(self, raw_payload: dict) -> bool:
281
- return raw_payload.get('hook_event_name', '') in _CLAUDE_CODE_EVENT_NAMES
281
+ # transcript_path is a documented Claude Code common field, present on every
282
+ # hook event. VS Code Copilot emits near-identical payloads (same event names,
283
+ # snake_case fields) without it — requiring it keeps those from being
284
+ # processed as Claude Code events.
285
+ return raw_payload.get('hook_event_name', '') in _CLAUDE_CODE_EVENT_NAMES and 'transcript_path' in raw_payload
282
286
 
283
287
  def parse_hook_payload(self, raw_payload: dict) -> AIHookPayload:
284
288
  hook_event_name = raw_payload.get('hook_event_name', '')
@@ -20,7 +20,7 @@ from cycode.cli.apps.ai_guardrails.ides._plugin_utils import (
20
20
  resolve_cached_plugin_dir,
21
21
  walk_enabled_plugins,
22
22
  )
23
- from cycode.cli.apps.ai_guardrails.ides.base import IDE, DecisionAction, HookDecision
23
+ from cycode.cli.apps.ai_guardrails.ides.base import IDE, DecisionAction, HookDecision, shell_background_suffix
24
24
  from cycode.cli.apps.ai_guardrails.scan.payload import AIHookPayload
25
25
  from cycode.cli.apps.ai_guardrails.scan.types import AiHookEventType
26
26
  from cycode.cli.utils.jwt_utils import decode_jwt_unverified
@@ -191,10 +191,9 @@ class Codex(IDE):
191
191
 
192
192
  def render_hooks_config(self, async_mode: bool = False) -> dict:
193
193
  # Codex's TOML `async: true` flag is unimplemented; shell-background via
194
- # `&` is the working mechanism. SessionStart stays sync so the
195
- # conversation context is registered before any scan hook fires.
196
- bg = ' &' if async_mode else ''
197
- scan_cmd = f'{_SCAN_COMMAND}{bg}'
194
+ # `&` is the working mechanism (unix only). SessionStart stays sync so
195
+ # the conversation context is registered before any scan hook fires.
196
+ scan_cmd = f'{_SCAN_COMMAND}{shell_background_suffix(async_mode)}'
198
197
  return {
199
198
  'hooks': {
200
199
  'SessionStart': [
@@ -0,0 +1,436 @@
1
+ """GitHub Copilot (VS Code extension) integration for AI guardrails.
2
+
3
+ Hooks are installed in Copilot's native format to ``~/.copilot/hooks/cycode.json``
4
+ (user scope) or ``<repo>/.github/hooks/cycode.json`` (repo scope). Both locations
5
+ are also read by Copilot CLI and the Copilot cloud coding agent, but only the
6
+ VS Code payload dialect is parsed here — CLI payloads (camelCase, no event name)
7
+ are rejected by ``matches_payload`` and fall through to the allow-and-skip path.
8
+
9
+ VS Code sends Claude-style payloads (``hook_event_name``, ``tool_name``,
10
+ ``tool_input``) with structural differences that ``matches_payload`` keys on:
11
+ a top-level ISO ``timestamp`` and no ``transcript_path``. Copilot hooks have no
12
+ matchers, so ``preToolUse`` fires for every tool; tools we don't scan pass
13
+ through as raw event names, which match no handler and allow immediately.
14
+ """
15
+
16
+ import json
17
+ import os
18
+ import platform
19
+ import re
20
+ from collections.abc import Iterable
21
+ from pathlib import Path
22
+ from typing import ClassVar, Optional, Union
23
+ from urllib.parse import urlparse
24
+ from urllib.request import url2pathname
25
+
26
+ from cycode.cli.apps.ai_guardrails.consts import CYCODE_SCAN_PROMPT_COMMAND, CYCODE_SESSION_START_COMMAND
27
+ from cycode.cli.apps.ai_guardrails.ides._plugin_utils import (
28
+ build_global_config_file,
29
+ load_plugin_json,
30
+ walk_enabled_plugins,
31
+ )
32
+ from cycode.cli.apps.ai_guardrails.ides.base import IDE, DecisionAction, HookDecision
33
+ from cycode.cli.apps.ai_guardrails.scan.payload import AIHookPayload
34
+ from cycode.cli.apps.ai_guardrails.scan.types import AiHookEventType
35
+ from cycode.logger import get_logger
36
+
37
+ logger = get_logger('AI Guardrails Copilot')
38
+
39
+ # Payload dialect (VS Code sends Claude-style PascalCase event names).
40
+ _COPILOT_SCAN_EVENT_NAMES = frozenset({'UserPromptSubmit', 'PreToolUse'})
41
+ _READ_FILE_TOOL = 'read_file'
42
+ # VS Code names MCP tools `mcp_<server>_<tool>` (single underscores).
43
+ _MCP_TOOL_PREFIX = 'mcp_'
44
+
45
+ # Hooks-file dialect (Copilot-native camelCase event names).
46
+ _HOOK_EVENTS = ['userPromptSubmitted', 'preToolUse']
47
+
48
+ _COPILOT_HOME_ENV_VAR = 'COPILOT_HOME'
49
+ _HOOKS_FILE_NAME = 'cycode.json'
50
+ _REPO_HOOKS_SUBDIR = Path('.github') / 'hooks'
51
+ _HOOK_TIMEOUT_SEC = 20
52
+ _MCP_CONFIG_FILENAME = 'mcp.json'
53
+
54
+ # Plugin sources. CLI installs register in ~/.copilot/config.json and auto-surface
55
+ # in VS Code; VS Code UI installs register in ~/.vscode/agent-plugins/installed.json;
56
+ # local-directory plugins are declared via the chat.pluginLocations setting.
57
+ _VSCODE_PLUGINS_REGISTRY_NAME = 'installed.json'
58
+ _PLUGIN_LOCATIONS_SETTING = 'chat.pluginLocations'
59
+ _LOCAL_PLUGINS_MARKETPLACE = 'local'
60
+
61
+ # Manifest locations in VS Code's documented detection order. Plugins may ship
62
+ # several manifest dialects at once — first hit wins, matching VS Code's probing.
63
+ _PLUGIN_MANIFEST_LOCATIONS = (
64
+ Path('.plugin') / 'plugin.json',
65
+ Path('plugin.json'),
66
+ Path('.github') / 'plugin' / 'plugin.json',
67
+ Path('.claude-plugin') / 'plugin.json',
68
+ )
69
+
70
+ # --event is ignored by the VS Code payload parsing (the payload self-describes)
71
+ # but Copilot CLI payloads carry no event name at all — baking the flag in now
72
+ # means CLI support won't require customers to re-install hooks. Values use the
73
+ # payload-dialect spelling so a future CLI path can inject them straight into
74
+ # hook_event_name and reuse the existing parsing.
75
+ _SCAN_PROMPT_COMMAND = f'{CYCODE_SCAN_PROMPT_COMMAND} --ide copilot --event UserPromptSubmit'
76
+ _SCAN_TOOL_COMMAND = f'{CYCODE_SCAN_PROMPT_COMMAND} --ide copilot --event PreToolUse'
77
+ _SESSION_START_COMMAND = f'{CYCODE_SESSION_START_COMMAND} --ide copilot'
78
+
79
+
80
+ def _copilot_home() -> Path:
81
+ """Resolve Copilot's user-scope home directory (honors ``$COPILOT_HOME``)."""
82
+ override = os.environ.get(_COPILOT_HOME_ENV_VAR)
83
+ if override:
84
+ return Path(override)
85
+ return Path.home() / '.copilot'
86
+
87
+
88
+ def _vscode_agent_plugins_dir() -> Path:
89
+ # Resolved at call time (not a module-level Path constant): on py<=3.10 a Path
90
+ # instance binds its filesystem accessor at creation, which breaks fake-fs tests
91
+ # and ignores home changes.
92
+ return Path.home() / '.vscode' / 'agent-plugins'
93
+
94
+
95
+ def _vscode_user_dir() -> Path:
96
+ """Per-platform VS Code user settings directory."""
97
+ if platform.system() == 'Darwin':
98
+ return Path.home() / 'Library' / 'Application Support' / 'Code' / 'User'
99
+ if platform.system() == 'Windows':
100
+ return Path.home() / 'AppData' / 'Roaming' / 'Code' / 'User'
101
+ return Path.home() / '.config' / 'Code' / 'User'
102
+
103
+
104
+ def _vscode_mcp_config_path() -> Path:
105
+ return _vscode_user_dir() / _MCP_CONFIG_FILENAME
106
+
107
+
108
+ def _load_vscode_mcp_config(config_path: Optional[Path] = None) -> Optional[dict]:
109
+ """Load and parse VS Code's user-level ``mcp.json``. Returns None if missing/invalid."""
110
+ path = config_path or _vscode_mcp_config_path()
111
+ if not path.exists():
112
+ logger.debug('VS Code MCP config file not found, %s', {'path': str(path)})
113
+ return None
114
+ try:
115
+ return json.loads(path.read_text(encoding='utf-8'))
116
+ except Exception as e:
117
+ logger.debug('Failed to load VS Code MCP config file', exc_info=e)
118
+ return None
119
+
120
+
121
+ def _load_jsonc(path: Path) -> Optional[dict]:
122
+ """Parse a JSON file tolerating //-comment lines (Copilot's config.json ships
123
+ with a comment header; VS Code's settings.json is JSONC).
124
+
125
+ Best-effort: JSONC constructs beyond full-line comments (trailing commas,
126
+ inline comments) read as a missing file.
127
+ """
128
+ if not path.exists():
129
+ logger.debug('Config file not found, %s', {'path': str(path)})
130
+ return None
131
+ try:
132
+ text = path.read_text(encoding='utf-8')
133
+ stripped = '\n'.join(line for line in text.splitlines() if not line.lstrip().startswith('//'))
134
+ return json.loads(stripped)
135
+ except Exception as e:
136
+ logger.debug('Failed to load config file, %s', {'path': str(path)}, exc_info=e)
137
+ return None
138
+
139
+
140
+ # --- plugins inventory ----------------------------------------------------------
141
+
142
+
143
+ def _read_copilot_plugin(plugin_dir: Path) -> tuple[dict, dict]:
144
+ """Read one Copilot plugin's manifest + MCP servers.
145
+
146
+ The manifest's ``mcpServers`` field, when present, is a path string to the MCP
147
+ file; otherwise the root ``.mcp.json`` convention applies (same as Claude
148
+ plugins). Both forms exist in marketplace plugins.
149
+ """
150
+ manifest: dict = {}
151
+ for location in _PLUGIN_MANIFEST_LOCATIONS:
152
+ manifest = load_plugin_json(plugin_dir / location) or {}
153
+ if manifest:
154
+ break
155
+
156
+ entry: dict = {}
157
+ for field in ('name', 'version', 'description'):
158
+ if field in manifest:
159
+ entry[field] = manifest[field]
160
+
161
+ mcp_ref = manifest.get('mcpServers')
162
+ mcp_config_path = plugin_dir / mcp_ref if isinstance(mcp_ref, str) else plugin_dir / '.mcp.json'
163
+ mcp_doc = load_plugin_json(mcp_config_path) or {}
164
+ servers = mcp_doc.get('mcpServers')
165
+ if not isinstance(servers, dict):
166
+ servers = {}
167
+ if servers:
168
+ entry['mcp_server_names'] = list(servers.keys())
169
+ entry['mcp_config_file_path'] = str(mcp_config_path)
170
+ entry['mcp_config_file'] = json.dumps({'mcpServers': servers})
171
+ return entry, servers
172
+
173
+
174
+ def _walk_registry_plugins(entries: dict[str, dict], dirs: dict[str, Path], is_enabled: bool = True) -> dict:
175
+ """Walk plugins whose directories are known up front (registry-provided)."""
176
+ return walk_enabled_plugins(
177
+ plugin_entries=entries,
178
+ is_enabled=lambda p: p.get('enabled', True) if is_enabled else True,
179
+ locate_dir=lambda name, marketplace: dirs.get(f'{name}@{marketplace}'),
180
+ read_plugin=_read_copilot_plugin,
181
+ )
182
+
183
+
184
+ def _cli_registry_plugins() -> dict:
185
+ """Plugins installed via Copilot CLI: ``~/.copilot/config.json`` → ``installedPlugins``."""
186
+ config = _load_jsonc(_copilot_home() / 'config.json') or {}
187
+ entries: dict[str, dict] = {}
188
+ dirs: dict[str, Path] = {}
189
+ for plugin in config.get('installedPlugins') or []:
190
+ if not isinstance(plugin, dict) or not plugin.get('name'):
191
+ continue
192
+ key = f'{plugin["name"]}@{plugin.get("marketplace", "")}'
193
+ entries[key] = plugin
194
+ if plugin.get('cache_path'):
195
+ dirs[key] = Path(plugin['cache_path'])
196
+ return _walk_registry_plugins(entries, dirs)
197
+
198
+
199
+ def _vscode_registry_plugins() -> dict:
200
+ """Plugins installed via the VS Code UI (@agentPlugins): ``~/.vscode/agent-plugins/installed.json``.
201
+
202
+ Registry-driven only — the directory also holds marketplace clones that are
203
+ not installed. ``pluginUri`` is the authoritative location (the registry's
204
+ ``marketplace`` label is unreliable); presence in the registry means enabled.
205
+ """
206
+ registry = load_plugin_json(_vscode_agent_plugins_dir() / _VSCODE_PLUGINS_REGISTRY_NAME) or {}
207
+ entries: dict[str, dict] = {}
208
+ dirs: dict[str, Path] = {}
209
+ for plugin in registry.get('installed') or []:
210
+ if not isinstance(plugin, dict) or not plugin.get('name'):
211
+ continue
212
+ key = f'{plugin["name"]}@{plugin.get("marketplace", "")}'
213
+ entries[key] = plugin
214
+ uri = plugin.get('pluginUri', '')
215
+ if uri.startswith('file://'):
216
+ # url2pathname unquotes and handles Windows drive-letter URIs (file:///C:/...).
217
+ dirs[key] = Path(url2pathname(urlparse(uri).path))
218
+ return _walk_registry_plugins(entries, dirs, is_enabled=False)
219
+
220
+
221
+ def _local_dir_plugins() -> dict:
222
+ """Local-directory plugins declared via the ``chat.pluginLocations`` setting."""
223
+ settings = _load_jsonc(_vscode_user_dir() / 'settings.json') or {}
224
+ locations = settings.get(_PLUGIN_LOCATIONS_SETTING)
225
+ if not isinstance(locations, dict):
226
+ return {}
227
+ entries: dict[str, bool] = {}
228
+ dirs: dict[str, Path] = {}
229
+ for raw_path, enabled in locations.items():
230
+ path = Path(raw_path).expanduser()
231
+ key = f'{path.name}@{_LOCAL_PLUGINS_MARKETPLACE}'
232
+ entries[key] = bool(enabled)
233
+ dirs[key] = path
234
+ return walk_enabled_plugins(
235
+ plugin_entries=entries,
236
+ is_enabled=bool,
237
+ locate_dir=lambda name, marketplace: dirs.get(f'{name}@{marketplace}'),
238
+ read_plugin=_read_copilot_plugin,
239
+ )
240
+
241
+
242
+ def _collect_installed_plugins() -> dict:
243
+ """Merge the three plugin sources (first source wins on a duplicate key)."""
244
+ plugins: dict = {}
245
+ for source in (_cli_registry_plugins, _vscode_registry_plugins, _local_dir_plugins):
246
+ for key, entry in source().items():
247
+ plugins.setdefault(key, entry)
248
+ return plugins
249
+
250
+
251
+ # --- MCP tool-name splitting ------------------------------------------------------
252
+
253
+
254
+ def _known_mcp_server_names() -> list[str]:
255
+ """Config-declared MCP server names: user-level ``mcp.json`` + plugin configs.
256
+
257
+ Best-effort inventory: servers contributed by extensions, ``chat.mcp.discovery``
258
+ imports, dev containers, or non-default profiles are not discoverable from disk.
259
+ """
260
+ config = _load_vscode_mcp_config()
261
+ servers = (config or {}).get('servers')
262
+ names = list(servers.keys()) if isinstance(servers, dict) else []
263
+ for plugin in _collect_installed_plugins().values():
264
+ names.extend(plugin.get('mcp_server_names') or [])
265
+ return names
266
+
267
+
268
+ def _server_name_variants(server_name: str) -> set[str]:
269
+ """Normalized forms a config name may take inside a VS Code tool-name prefix.
270
+
271
+ The prefix derives from the server's self-reported handshake name, which often
272
+ resembles the config name modulo case and separators (a server configured as
273
+ ``dummy-tracker`` self-reporting ``DummyTracker`` yields prefix ``dummytracker``).
274
+ """
275
+ lowered = server_name.lower()
276
+ underscored = re.sub(r'[^a-z0-9]+', '_', lowered).strip('_')
277
+ collapsed = re.sub(r'[^a-z0-9]', '', lowered)
278
+ return {v for v in (server_name, underscored, collapsed) if v}
279
+
280
+
281
+ def split_mcp_tool_name(tool_name: str, server_names: Iterable[str]) -> tuple[Optional[str], Optional[str]]:
282
+ """Split ``mcp_<server>_<tool>`` into ``(server, tool)``.
283
+
284
+ The ``<server>`` part is VS Code's sanitized (and possibly truncated) form of
285
+ the server's SELF-REPORTED handshake name, not the config key — so matching
286
+ against known config names (and their normalized variants) is best-effort.
287
+ When nothing matches, return the unsplit remainder as the tool rather than
288
+ fabricating a server from a guessed split.
289
+ """
290
+ rest = tool_name[len(_MCP_TOOL_PREFIX) :]
291
+
292
+ best_server = None
293
+ best_variant_len = -1
294
+ for server in server_names:
295
+ for variant in _server_name_variants(server):
296
+ if (rest == variant or rest.startswith(f'{variant}_')) and len(variant) > best_variant_len:
297
+ best_server = server
298
+ best_variant_len = len(variant)
299
+ if best_server is not None:
300
+ return best_server, rest[best_variant_len + 1 :] or None
301
+
302
+ return None, rest or None
303
+
304
+
305
+ class Copilot(IDE):
306
+ name: ClassVar[str] = 'copilot'
307
+ display_name: ClassVar[str] = 'GitHub Copilot'
308
+ hook_events: ClassVar[list[str]] = list(_HOOK_EVENTS)
309
+
310
+ def settings_path(self, scope: str, repo_path: Optional[Path] = None) -> Path:
311
+ # Dedicated Cycode-owned file (Copilot reads every *.json in the hooks
312
+ # dir), unlike the shared settings files of other IDEs.
313
+ if scope == 'repo' and repo_path:
314
+ return repo_path / _REPO_HOOKS_SUBDIR / _HOOKS_FILE_NAME
315
+ return _copilot_home() / 'hooks' / _HOOKS_FILE_NAME
316
+
317
+ def render_hooks_config(self, async_mode: bool = False) -> dict:
318
+ def entry(command: str) -> dict:
319
+ if async_mode:
320
+ # Copilot has no async hook flag; background via shell on unix. The
321
+ # explicit <&0 keeps the payload flowing: a bare `cmd &` gets its stdin
322
+ # reattached to /dev/null by the shell (job control is off in hooks).
323
+ # Windows PowerShell has no trailing-& operator, so it stays sync.
324
+ return {
325
+ 'type': 'command',
326
+ 'bash': f'{command} <&0 &',
327
+ 'powershell': command,
328
+ 'timeoutSec': _HOOK_TIMEOUT_SEC,
329
+ }
330
+ # Single cross-platform `command` field, copied to both shells by Copilot.
331
+ return {'type': 'command', 'command': command, 'timeoutSec': _HOOK_TIMEOUT_SEC}
332
+
333
+ return {
334
+ 'version': 1,
335
+ 'hooks': {
336
+ 'sessionStart': [
337
+ {'type': 'command', 'command': _SESSION_START_COMMAND, 'timeoutSec': _HOOK_TIMEOUT_SEC}
338
+ ],
339
+ 'userPromptSubmitted': [entry(_SCAN_PROMPT_COMMAND)],
340
+ 'preToolUse': [entry(_SCAN_TOOL_COMMAND)],
341
+ },
342
+ }
343
+
344
+ def matches_payload(self, raw_payload: dict) -> bool:
345
+ # Structural discrimination, no magic strings: VS Code Copilot events carry
346
+ # a top-level ISO timestamp and no transcript_path; real Claude Code events
347
+ # always carry transcript_path; Copilot CLI payloads have no hook_event_name.
348
+ return (
349
+ raw_payload.get('hook_event_name', '') in _COPILOT_SCAN_EVENT_NAMES
350
+ and 'timestamp' in raw_payload
351
+ and 'transcript_path' not in raw_payload
352
+ )
353
+
354
+ def parse_hook_payload(self, raw_payload: dict) -> AIHookPayload:
355
+ hook_event_name = raw_payload.get('hook_event_name', '')
356
+ tool_name = raw_payload.get('tool_name', '')
357
+ tool_input = raw_payload.get('tool_input')
358
+
359
+ if hook_event_name == 'UserPromptSubmit':
360
+ canonical_event: Union[AiHookEventType, str] = AiHookEventType.PROMPT
361
+ elif hook_event_name == 'PreToolUse' and tool_name == _READ_FILE_TOOL:
362
+ canonical_event = AiHookEventType.FILE_READ
363
+ elif hook_event_name == 'PreToolUse' and tool_name.startswith(_MCP_TOOL_PREFIX):
364
+ canonical_event = AiHookEventType.MCP_EXECUTION
365
+ else:
366
+ # No matchers in Copilot hooks: preToolUse fires for every tool. Pass
367
+ # the raw tool name through — it matches no handler, so scan_command
368
+ # answers with a neutral allow before any policy/network work.
369
+ canonical_event = tool_name or hook_event_name
370
+
371
+ file_path = None
372
+ if canonical_event == AiHookEventType.FILE_READ and isinstance(tool_input, dict):
373
+ file_path = tool_input.get('filePath')
374
+
375
+ mcp_server_name = None
376
+ mcp_tool_name = None
377
+ mcp_arguments = None
378
+ if canonical_event == AiHookEventType.MCP_EXECUTION:
379
+ mcp_server_name, mcp_tool_name = split_mcp_tool_name(tool_name, _known_mcp_server_names())
380
+ mcp_arguments = tool_input if isinstance(tool_input, dict) else None
381
+
382
+ return AIHookPayload(
383
+ event_name=canonical_event,
384
+ conversation_id=raw_payload.get('session_id'),
385
+ ide_provider=self.name,
386
+ prompt=raw_payload.get('prompt', ''),
387
+ file_path=file_path,
388
+ mcp_server_name=mcp_server_name,
389
+ mcp_tool_name=mcp_tool_name,
390
+ mcp_arguments=mcp_arguments,
391
+ )
392
+
393
+ def build_hook_response(self, decision: HookDecision) -> dict:
394
+ if decision.action == DecisionAction.ALLOW:
395
+ # Neutral allow: {} means "no objection", leaving VS Code's own
396
+ # permission flow intact. An explicit permissionDecision "allow" would
397
+ # pre-approve the tool past the user's confirmation prompts — and with
398
+ # no matchers that would cover every tool, not just scanned ones.
399
+ return {}
400
+
401
+ if decision.event_type == AiHookEventType.PROMPT:
402
+ reason = decision.user_message or ''
403
+ # decision/reason is what VS Code acts on; continue/stopReason/systemMessage
404
+ # are the generic top-level fields — the combo is what was verified live.
405
+ return {
406
+ 'decision': 'block',
407
+ 'reason': reason,
408
+ 'continue': False,
409
+ 'stopReason': reason,
410
+ 'systemMessage': reason,
411
+ }
412
+
413
+ return {
414
+ 'hookSpecificOutput': {
415
+ 'hookEventName': 'PreToolUse',
416
+ 'permissionDecision': decision.action.value, # 'deny' or 'ask'
417
+ 'permissionDecisionReason': decision.user_message or '',
418
+ }
419
+ }
420
+
421
+ def build_session_payload(self, raw_payload: dict) -> AIHookPayload:
422
+ return AIHookPayload(
423
+ conversation_id=raw_payload.get('session_id'),
424
+ model=raw_payload.get('model'),
425
+ ide_provider=self.name,
426
+ source=raw_payload.get('source'),
427
+ )
428
+
429
+ def get_session_context(self) -> tuple[Optional[dict], dict]:
430
+ # VS Code's mcp.json uses `servers` as its top-level key; normalized to the
431
+ # canonical mcpServers shape by build_global_config_file.
432
+ config = _load_vscode_mcp_config()
433
+ global_config_file = (
434
+ build_global_config_file(_vscode_mcp_config_path(), config.get('servers')) if config else None
435
+ )
436
+ return global_config_file, _collect_installed_plugins()
@@ -7,7 +7,7 @@ from typing import ClassVar, Optional
7
7
 
8
8
  from cycode.cli.apps.ai_guardrails.consts import CYCODE_SCAN_PROMPT_COMMAND, CYCODE_SESSION_START_COMMAND
9
9
  from cycode.cli.apps.ai_guardrails.ides._plugin_utils import build_global_config_file
10
- from cycode.cli.apps.ai_guardrails.ides.base import IDE, DecisionAction, HookDecision
10
+ from cycode.cli.apps.ai_guardrails.ides.base import IDE, DecisionAction, HookDecision, shell_background_suffix
11
11
  from cycode.cli.apps.ai_guardrails.scan.payload import AIHookPayload
12
12
  from cycode.cli.apps.ai_guardrails.scan.types import AiHookEventType
13
13
  from cycode.logger import get_logger
@@ -69,7 +69,7 @@ class Cursor(IDE):
69
69
  return _user_hooks_dir() / _HOOKS_FILE_NAME
70
70
 
71
71
  def render_hooks_config(self, async_mode: bool = False) -> dict:
72
- command = f'{_SCAN_COMMAND} &' if async_mode else _SCAN_COMMAND
72
+ command = f'{_SCAN_COMMAND}{shell_background_suffix(async_mode)}'
73
73
  hooks = {event: [{'command': command}] for event in self.hook_events}
74
74
  hooks['sessionStart'] = [{'command': _SESSION_START_COMMAND}]
75
75
  return {'version': 1, 'hooks': hooks}
@@ -82,6 +82,14 @@ def scan_command(
82
82
  hidden=True,
83
83
  ),
84
84
  ] = DEFAULT_IDE_NAME,
85
+ event: Annotated[
86
+ Optional[str],
87
+ typer.Option(
88
+ '--event',
89
+ help='Hook event that triggered the scan, for IDEs whose payloads omit it (e.g. Copilot CLI).',
90
+ hidden=True,
91
+ ),
92
+ ] = None,
85
93
  ) -> None:
86
94
  """Scan content from AI IDE hooks for secrets.
87
95
 
@@ -110,7 +118,18 @@ def scan_command(
110
118
 
111
119
  unified_payload = ide_integration.parse_hook_payload(payload)
112
120
  event_name = unified_payload.event_name
113
- logger.debug('Processing AI guardrails hook', extra={'event_name': event_name, 'ide': ide_integration.name})
121
+ logger.debug(
122
+ 'Processing AI guardrails hook',
123
+ extra={'event_name': event_name, 'ide': ide_integration.name, 'cli_event_hint': event},
124
+ )
125
+
126
+ # Resolved before any policy/client work: Copilot hooks have no matchers, so
127
+ # every tool call arrives here and unmatched tools must exit fast.
128
+ handler = get_handler_for_event(event_name)
129
+ if handler is None:
130
+ logger.debug('Unknown hook event, allowing by default', extra={'event_name': event_name})
131
+ output_json(ide_integration.build_hook_response(HookDecision.allow(AiHookEventType.PROMPT)))
132
+ return
114
133
 
115
134
  # `or` (not a .get default) - Cursor sends workspace_roots=[] when no folder is open.
116
135
  workspace_roots = payload.get('workspace_roots') or ['.']
@@ -119,12 +138,6 @@ def scan_command(
119
138
  try:
120
139
  _initialize_clients(ctx)
121
140
 
122
- handler = get_handler_for_event(event_name)
123
- if handler is None:
124
- logger.debug('Unknown hook event, allowing by default', extra={'event_name': event_name})
125
- output_json(ide_integration.build_hook_response(HookDecision.allow(AiHookEventType.PROMPT)))
126
- return
127
-
128
141
  decision = handler(ctx, unified_payload, policy)
129
142
  logger.debug('Hook handler completed', extra={'event_name': event_name, 'action': decision.action.value})
130
143
  output_json(ide_integration.build_hook_response(decision))
@@ -21,7 +21,7 @@ classifiers = [
21
21
  "Programming Language :: Python :: 3.14",
22
22
  ]
23
23
  dynamic = ["dependencies"]
24
- version = "3.17.3.dev2"
24
+ version = "3.17.3.dev3"
25
25
 
26
26
  [project.scripts]
27
27
  cycode = "cycode.cli.app:app"
File without changes
File without changes