Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions .github/workflows/docker-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,17 @@ jobs:
git checkout ${{ steps.latest_tag.outputs.LATEST_TAG }}

- name: Set up Python
id: setup-python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.9'
python-version: '3.10'

- name: Load cached Poetry setup
id: cached_poetry
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.local
key: poetry-ubuntu-1 # increment to reset cache
key: poetry-ubuntu-${{ steps.setup-python.outputs.python-version }}

- name: Setup Poetry
if: steps.cached_poetry.outputs.cache-hit != 'true'
Expand All @@ -61,10 +62,10 @@ jobs:
echo "CLI_VERSION=$(poetry version --short)" >> $GITHUB_OUTPUT

- name: Set up QEMU
uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1

- name: Login to Docker Hub
if: ${{ github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v') }}
Expand All @@ -76,7 +77,7 @@ jobs:
- name: Build and push
id: docker_build
if: ${{ github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v') }}
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
context: .
platforms: linux/amd64,linux/arm64
Expand All @@ -86,7 +87,7 @@ jobs:
- name: Verify build
id: docker_verify_build
if: ${{ github.event_name != 'workflow_dispatch' && !startsWith(github.ref, 'refs/tags/v') }}
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
context: .
platforms: linux/amd64,linux/arm64
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/pre_release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,17 @@ jobs:
fetch-depth: 0

- name: Set up Python
id: setup-python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.9'
python-version: '3.10'

- name: Load cached Poetry setup
id: cached-poetry
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.local
key: poetry-ubuntu-1 # increment to reset cache
key: poetry-ubuntu-${{ steps.setup-python.outputs.python-version }}

- name: Setup Poetry
if: steps.cached-poetry.outputs.cache-hit != 'true'
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,17 @@ jobs:
fetch-depth: 0

- name: Set up Python
id: setup-python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.9'
python-version: '3.10'

- name: Load cached Poetry setup
id: cached-poetry
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.local
key: poetry-ubuntu-1 # increment to reset cache
key: poetry-ubuntu-${{ steps.setup-python.outputs.python-version }}

- name: Setup Poetry
if: steps.cached-poetry.outputs.cache-hit != 'true'
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/ruff.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,17 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Setup Python
id: setup-python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: 3.9
python-version: '3.10'

- name: Load cached Poetry setup
id: cached-poetry
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.local
key: poetry-ubuntu-1 # increment to reset cache
key: poetry-ubuntu-${{ steps.setup-python.outputs.python-version }}

- name: Setup Poetry
if: steps.cached-poetry.outputs.cache-hit != 'true'
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,17 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Set up Python
id: setup-python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.9'
python-version: '3.10'

- name: Load cached Poetry setup
id: cached-poetry
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.local
key: poetry-ubuntu-1 # increment to reset cache
key: poetry-ubuntu-${{ steps.setup-python.outputs.python-version }}

- name: Setup Poetry
if: steps.cached-poetry.outputs.cache-hit != 'true'
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/tests_full.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
strategy:
matrix:
os: [ macos-latest, ubuntu-latest, windows-latest ]
python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13", "3.14" ]
python-version: [ "3.10", "3.11", "3.12", "3.13", "3.14" ]

runs-on: ${{matrix.os}}

Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

## How to contribute to Cycode CLI

The minimum version of Python that we support is 3.9.
The minimum version of Python that we support is 3.10.
We recommend using this version for local development.
But it’s fine to use a higher version without using new features from these versions.

Expand Down
10 changes: 2 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ This guide walks you through both installation and usage.

# Prerequisites

- The Cycode CLI application requires Python version 3.9 or later. The MCP command is available only for Python 3.10 and above. If you're using an earlier Python version, this command will not be available.
- The Cycode CLI application requires Python version 3.10 or later.
- Use the [`cycode auth` command](#using-the-auth-command) to authenticate to Cycode with the CLI
- Alternatively, you can get a Cycode Client ID and Client Secret Key by following the steps detailed in the [Service Account Token](https://docs.cycode.com/docs/en/service-accounts) and [Personal Access Token](https://docs.cycode.com/v1/docs/managing-personal-access-tokens) pages, which contain details on getting these values.

Expand Down Expand Up @@ -246,7 +246,7 @@ Perform the following steps to install the pre-commit hook:

### Installing Pre-Commit Hook

1. Install the pre-commit framework (Python 3.9 or higher must be installed):
1. Install the pre-commit framework (Python 3.10 or higher must be installed):

```bash
pip3 install pre-commit
Expand Down Expand Up @@ -395,19 +395,13 @@ The two options combine: with `CYCODE_CLI_ENABLE_TRUSTSTORE=1`, certificates fro

Notes:

- `CYCODE_CLI_ENABLE_TRUSTSTORE` requires Python 3.10 or newer. On Python 3.9 the CLI logs a warning
and falls back to the bundled CA bundle; use `REQUESTS_CA_BUNDLE` instead, or upgrade Python. The
standalone executables and the Docker image already ship a supported Python.
- On Windows, the CLI has always fallen back to the system certificate store when neither
`REQUESTS_CA_BUNDLE` nor `CURL_CA_BUNDLE` is set. That behavior is unchanged.
- Proxies themselves are configured with the standard `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY`
environment variables.

# MCP Command \[EXPERIMENT\]

> [!WARNING]
> The MCP command is available only for Python 3.10 and above. If you're using an earlier Python version, this command will not be available.

The Model Context Protocol (MCP) command allows you to start an MCP server that exposes Cycode's scanning capabilities to AI systems and applications. This enables AI models to interact with Cycode CLI tools via a standardized protocol.

> [!TIP]
Expand Down
19 changes: 9 additions & 10 deletions cycode/cli/app.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import importlib
import logging
import sys
from typing import Annotated, Optional
from typing import Annotated

import click
import typer
Expand Down Expand Up @@ -51,13 +51,12 @@
'auth': 'cycode.cli.apps.auth',
'configure': 'cycode.cli.apps.configure',
'ignore': 'cycode.cli.apps.ignore',
'mcp': 'cycode.cli.apps.mcp',
'report': 'cycode.cli.apps.report',
'import': 'cycode.cli.apps.report_import',
'scan': 'cycode.cli.apps.scan',
'status': 'cycode.cli.apps.status',
}
if sys.version_info >= (3, 10):
_SUBAPP_MODULES['mcp'] = 'cycode.cli.apps.mcp'

# Aliases: alternate spellings that resolve to a primary subcommand key.
_SUBAPP_ALIASES: dict[str, str] = {
Expand All @@ -80,7 +79,7 @@
)


def _detect_invocation() -> tuple[Optional[str], Optional[str]]:
def _detect_invocation() -> tuple[str | None, str | None]:
"""Return (top-level-subapp, second-level-subcommand) parsed from sys.argv.

Both values may be None: when no positional arg matches a known subapp,
Expand Down Expand Up @@ -113,7 +112,7 @@ def _detect_invocation() -> tuple[Optional[str], Optional[str]]:
_INVOKED_SUBAPP, _INVOKED_SUBCOMMAND = _detect_invocation()


def _register_subapps(only: Optional[str]) -> None:
def _register_subapps(only: str | None) -> None:
if only is not None:
app.add_typer(importlib.import_module(_SUBAPP_MODULES[only]).app)
return
Expand Down Expand Up @@ -189,32 +188,32 @@ def app_callback(
OutputTypeOption, typer.Option('--output', '-o', case_sensitive=False, help='Specify the output type.')
] = OutputTypeOption.RICH,
user_agent: Annotated[
Optional[str],
str | None,
typer.Option(hidden=True, help='Characteristic JSON object that lets servers identify the application.'),
] = None,
client_secret: Annotated[
Optional[str],
str | None,
typer.Option(
help='Specify a Cycode client secret for this specific scan execution.',
rich_help_panel=_AUTH_RICH_HELP_PANEL,
),
] = None,
client_id: Annotated[
Optional[str],
str | None,
typer.Option(
help='Specify a Cycode client ID for this specific scan execution.',
rich_help_panel=_AUTH_RICH_HELP_PANEL,
),
] = None,
id_token: Annotated[
Optional[str],
str | None,
typer.Option(
help='Specify a Cycode OIDC ID token for this specific scan execution.',
rich_help_panel=_AUTH_RICH_HELP_PANEL,
),
] = None,
_: Annotated[
Optional[bool],
bool | None,
typer.Option(
'--install-completion',
callback=install_callback,
Expand Down
12 changes: 6 additions & 6 deletions cycode/cli/apps/activation_manager.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import TYPE_CHECKING, Optional
from typing import TYPE_CHECKING

from cycode import __version__
from cycode.cli.config import configuration_manager
Expand All @@ -13,22 +13,22 @@
_CLI_CLIENT_NAME = 'cli'


def _get_client_and_version(plugin_app_name: Optional[str], plugin_app_version: Optional[str]) -> tuple[str, str]:
def _get_client_and_version(plugin_app_name: str | None, plugin_app_version: str | None) -> tuple[str, str]:
return plugin_app_name or _CLI_CLIENT_NAME, plugin_app_version or __version__


def should_report_cli_activation(
plugin_app_name: Optional[str] = None,
plugin_app_version: Optional[str] = None,
plugin_app_name: str | None = None,
plugin_app_version: str | None = None,
) -> bool:
client, version = _get_client_and_version(plugin_app_name, plugin_app_version)
return configuration_manager.get_last_reported_activation_version(client) != version


def report_cli_activation(
cycode_client: 'CycodeClientBase',
plugin_app_name: Optional[str] = None,
plugin_app_version: Optional[str] = None,
plugin_app_name: str | None = None,
plugin_app_version: str | None = None,
) -> None:
"""Report CLI/IDE activation to the backend if the (client, version) pair is new.

Expand Down
3 changes: 1 addition & 2 deletions cycode/cli/apps/ai_guardrails/command_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import os
from pathlib import Path
from typing import Optional

import typer
from rich.console import Console
Expand All @@ -18,7 +17,7 @@ def validate_scope(scope: str, allowed_scopes: tuple[str, ...] = ('user', 'repo'
raise typer.Exit(1)


def resolve_repo_path(scope: str, repo_path: Optional[Path]) -> Optional[Path]:
def resolve_repo_path(scope: str, repo_path: Path | None) -> Path | None:
"""Default repo_path to cwd for 'repo' scope; leave None for 'user' scope."""
if scope == 'repo' and repo_path is None:
return Path(os.getcwd())
Expand Down
13 changes: 6 additions & 7 deletions cycode/cli/apps/ai_guardrails/hooks_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import copy
import json
from pathlib import Path
from typing import Optional

import yaml

Expand Down Expand Up @@ -47,7 +46,7 @@ def is_cycode_hook_entry(entry: dict) -> bool:
return False


def _strip_cycode_from_entry(entry: dict) -> Optional[dict]:
def _strip_cycode_from_entry(entry: dict) -> dict | None:
"""Remove Cycode hooks from ``entry`` and return the remainder.

Returns ``None`` when nothing useful remains (Cursor-flat Cycode entry, or
Expand All @@ -72,7 +71,7 @@ def _strip_cycode_from_entry(entry: dict) -> Optional[dict]:
return entry


def _load_hooks_file(hooks_path: Path) -> Optional[dict]:
def _load_hooks_file(hooks_path: Path) -> dict | None:
if not hooks_path.exists():
return None
try:
Expand Down Expand Up @@ -102,7 +101,7 @@ def _load_policy_dict(policy_path: Path) -> dict:
return {**copy.deepcopy(DEFAULT_POLICY), **existing}


def create_policy_file(scope: str, repo_path: Optional[Path] = None) -> tuple[bool, str]:
def create_policy_file(scope: str, repo_path: Path | None = None) -> tuple[bool, str]:
"""Create or update the ai-guardrails.yaml policy file (operational knobs only).

Enforcement mode and sensitive-path globs are platform-managed; those keys are stripped
Expand All @@ -125,7 +124,7 @@ def create_policy_file(scope: str, repo_path: Optional[Path] = None) -> tuple[bo
def install_hooks(
ide: IDE,
scope: str = 'user',
repo_path: Optional[Path] = None,
repo_path: Path | None = None,
) -> tuple[bool, str]:
"""Install Cycode AI guardrails hooks for ``ide``."""
hooks_path = ide.settings_path(scope, repo_path)
Expand Down Expand Up @@ -197,7 +196,7 @@ def _persist_uninstall(hooks_path: Path, existing: dict, modified: bool) -> tupl
return True, f'Cycode hooks removed from: {hooks_path}'


def uninstall_hooks(ide: IDE, scope: str = 'user', repo_path: Optional[Path] = None) -> tuple[bool, str]:
def uninstall_hooks(ide: IDE, scope: str = 'user', repo_path: Path | None = None) -> tuple[bool, str]:
"""Remove Cycode AI guardrails hooks for ``ide``."""
hooks_path = ide.settings_path(scope, repo_path)

Expand All @@ -218,7 +217,7 @@ def uninstall_hooks(ide: IDE, scope: str = 'user', repo_path: Optional[Path] = N
return True, message


def get_hooks_status(ide: IDE, scope: str = 'user', repo_path: Optional[Path] = None) -> dict:
def get_hooks_status(ide: IDE, scope: str = 'user', repo_path: Path | None = None) -> dict:
"""Return installation status of Cycode hooks for ``ide``."""
hooks_path = ide.settings_path(scope, repo_path)

Expand Down
Loading
Loading