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.
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/PKG-INFO +1 -1
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/__init__.py +1 -1
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_guardrails/hooks_manager.py +12 -5
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_guardrails/ides/__init__.py +2 -1
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_guardrails/ides/base.py +19 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_guardrails/ides/claude_code.py +5 -1
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_guardrails/ides/codex.py +4 -5
- cycode-3.17.3.dev3/cycode/cli/apps/ai_guardrails/ides/copilot.py +436 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_guardrails/ides/cursor.py +2 -2
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_guardrails/scan/scan_command.py +20 -7
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/pyproject.toml +1 -1
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/LICENCE +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/README.md +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/__main__.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/__init__.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/app.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/__init__.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/activation_manager.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_guardrails/__init__.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_guardrails/command_utils.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_guardrails/consts.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_guardrails/ides/_plugin_utils.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_guardrails/install_command.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_guardrails/scan/__init__.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_guardrails/scan/consts.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_guardrails/scan/handlers.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_guardrails/scan/payload.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_guardrails/scan/policy.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_guardrails/scan/types.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_guardrails/scan/utils.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_guardrails/session_start_command.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_guardrails/status_command.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_guardrails/uninstall_command.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_remediation/__init__.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_remediation/ai_remediation_command.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_remediation/apply_fix.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_remediation/print_remediation.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/api/__init__.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/api/api_command.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/api/openapi_spec.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/auth/__init__.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/auth/auth_command.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/auth/auth_common.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/auth/auth_manager.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/auth/models.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/configure/__init__.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/configure/configure_command.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/configure/consts.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/configure/messages.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/configure/prompts.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ignore/__init__.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ignore/ignore_command.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/mcp/__init__.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/mcp/mcp_command.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/report/__init__.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/report/report_command.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/report/sbom/__init__.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/report/sbom/common.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/report/sbom/path/__init__.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/report/sbom/path/path_command.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/report/sbom/repository_url/__init__.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/report/sbom/repository_url/repository_url_command.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/report/sbom/sbom_command.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/report/sbom/sbom_report_file.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/report_import/__init__.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/report_import/report_import_command.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/report_import/sbom/__init__.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/report_import/sbom/sbom_command.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/sca_options.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/__init__.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/aggregation_report.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/code_scanner.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/commit_history/__init__.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/commit_history/commit_history_command.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/commit_range_scanner.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/detection_excluder.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/path/__init__.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/path/path_command.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/pre_commit/__init__.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/pre_commit/pre_commit_command.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/pre_push/__init__.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/pre_push/pre_push_command.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/pre_receive/__init__.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/pre_receive/pre_receive_command.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/remote_url_resolver.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/repository/__init__.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/repository/repository_command.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/scan_ci/__init__.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/scan_ci/ci_integrations.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/scan_ci/scan_ci_command.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/scan_command.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/scan_parameters.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/scan/scan_result.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/status/__init__.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/status/get_cli_status.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/status/models.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/status/status_command.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/status/version_command.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/cli_types.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/config.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/console.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/consts.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/exceptions/__init__.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/exceptions/custom_exceptions.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/exceptions/handle_ai_remediation_errors.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/exceptions/handle_auth_errors.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/exceptions/handle_errors.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/exceptions/handle_report_sbom_errors.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/exceptions/handle_scan_errors.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/__init__.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/commit_range_documents.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/documents_walk_ignore.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/file_excluder.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/iac/__init__.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/iac/tf_content_generator.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/models/__init__.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/models/in_memory_zip.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/path_documents.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/repository_documents.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/__init__.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/base_restore_dependencies.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/go/__init__.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/go/restore_go_dependencies.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/maven/__init__.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/maven/restore_gradle_dependencies.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/maven/restore_maven_dependencies.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/npm/__init__.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/npm/restore_bun_dependencies.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/npm/restore_deno_dependencies.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/npm/restore_npm_dependencies.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/npm/restore_pnpm_dependencies.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/npm/restore_yarn_dependencies.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/nuget/__init__.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/nuget/restore_nuget_dependencies.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/php/__init__.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/php/restore_composer_dependencies.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/python/__init__.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/python/restore_pipenv_dependencies.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/python/restore_poetry_dependencies.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/python/restore_uv_dependencies.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/ruby/__init__.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/ruby/restore_ruby_dependencies.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/sbt/__init__.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/sbt/restore_sbt_dependencies.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/sca/sca_file_collector.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/walk_ignore.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/files_collector/zip_documents.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/logger.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/main.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/models.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/printers/__init__.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/printers/console_printer.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/printers/json_printer.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/printers/printer_base.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/printers/rich_printer.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/printers/tables/__init__.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/printers/tables/sca_table_printer.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/printers/tables/table.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/printers/tables/table_models.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/printers/tables/table_printer.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/printers/tables/table_printer_base.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/printers/text_printer.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/printers/utils/__init__.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/printers/utils/code_snippet_syntax.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/printers/utils/detection_data.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/printers/utils/detection_ordering/__init__.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/printers/utils/detection_ordering/common_ordering.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/printers/utils/detection_ordering/sca_ordering.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/printers/utils/rich_helpers.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/user_settings/__init__.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/user_settings/base_file_manager.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/user_settings/config_file_manager.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/user_settings/configuration_manager.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/user_settings/credentials_manager.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/user_settings/jwt_creator.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/utils/__init__.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/utils/binary_utils.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/utils/enum_utils.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/utils/get_api_client.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/utils/git_proxy.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/utils/host_info.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/utils/ignore_utils.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/utils/jwt_utils.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/utils/path_utils.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/utils/progress_bar.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/utils/scan_batch.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/utils/scan_utils.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/utils/shell_executor.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/utils/string_utils.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/utils/task_timer.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/utils/url_utils.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/utils/version_checker.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/utils/yaml_utils.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/config.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cyclient/__init__.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cyclient/ai_security_manager_client.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cyclient/ai_security_manager_service_config.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cyclient/auth_client.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cyclient/base_token_auth_client.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cyclient/cli_activation_client.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cyclient/client_creator.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cyclient/config.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cyclient/config_dev.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cyclient/cycode_client.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cyclient/cycode_client_base.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cyclient/cycode_dev_based_client.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cyclient/cycode_oidc_based_client.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cyclient/cycode_token_based_client.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cyclient/headers.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cyclient/import_sbom_client.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cyclient/logger.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cyclient/models.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cyclient/report_client.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cyclient/scan_client.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cyclient/scan_config_base.py +0 -0
- {cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/logger.py +0 -0
|
@@ -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.
|
|
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
|
-
|
|
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 '
|
|
52
|
-
return None if
|
|
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
|
-
|
|
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
|
|
195
|
-
# conversation context is registered before any scan hook fires.
|
|
196
|
-
|
|
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}
|
|
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}
|
{cycode-3.17.3.dev2 → cycode-3.17.3.dev3}/cycode/cli/apps/ai_guardrails/scan/scan_command.py
RENAMED
|
@@ -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(
|
|
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))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|