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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

### Added

- Added a Cargo credential provider for Cloudsmith registries. `cloudsmith credential-helper install cargo` installs a `cargo-credential-cloudsmith` launcher binary and registers it in `$CARGO_HOME/config.toml`, so Cargo authenticates to Cloudsmith registries automatically using your existing CLI credentials — no `cargo login` and no token in `credentials.toml`. `cloudsmith credential-helper cargo` speaks Cargo's [credential provider protocol](https://doc.rust-lang.org/cargo/reference/credential-provider-protocol.html): a newline-delimited JSON exchange that answers `get` with the resolved token, and answers a registry that is not a Cloudsmith one with `url-not-supported` so Cargo falls through to the next configured provider — registering globally cannot break authentication to crates.io. The provider is appended to `registry.global-credential-providers` (keeping `cargo:token` as the fallback) and pinned on any `[registries.*]` entry whose index points at a known Cloudsmith Cargo host. Custom Cloudsmith registry domains are discovered via the API and cached locally; add extra hostnames with `--domain` (repeatable), disable discovery with `--no-discover`, or preview changes with `--dry-run`. Manage installed helpers with `cloudsmith credential-helper uninstall cargo` and `cloudsmith credential-helper list`.
- Added Nix package and upstream support. Use `cloudsmith push nix` to upload Nix packages and `cloudsmith upstream nix` to manage Nix channel upstreams.
- Added a pnpm credential helper. `cloudsmith credential-helper install pnpm` registers `pnpm-credential-cloudsmith` in the user-level `.npmrc`, using existing CLI credentials for Cloudsmith registries. It supports custom-domain discovery, additional `--domain` values, `--no-discover`, `--dry-run`, listing, and uninstalling.
- Added `CLOUDSMITH_KEYRING_FILE_PATH` and `CLOUDSMITH_KEYRING_DIR` to relocate tokens stored by the bundled file-based keyring backends. An explicit file path takes precedence over the directory, and `KEYRING_PROPERTY_FILE_PATH` takes precedence over its Cloudsmith alias.
Expand Down
5 changes: 5 additions & 0 deletions cloudsmith_cli/cli/commands/credential_helper/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import click

from ..main import main
from .cargo import cargo as cargo_cmd
from .docker import docker as docker_cmd
from .generic import generic as generic_cmd
from .manage import install_cmd, list_cmd, uninstall_cmd
Expand All @@ -31,6 +32,9 @@ def credential_helper():
# Install pnpm credential helper
$ cloudsmith credential-helper install pnpm

# Install cargo credential helper
$ cloudsmith credential-helper install cargo

# Test Docker credential helper directly
$ echo "docker.cloudsmith.io" | cloudsmith credential-helper docker

Expand All @@ -45,5 +49,6 @@ def credential_helper():
credential_helper.add_command(install_cmd, name="install")
credential_helper.add_command(uninstall_cmd, name="uninstall")
credential_helper.add_command(list_cmd, name="list")
credential_helper.add_command(cargo_cmd, name="cargo")

main.add_command(credential_helper, name="credential-helper")
93 changes: 93 additions & 0 deletions cloudsmith_cli/cli/commands/credential_helper/cargo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Copyright 2026 Cloudsmith Ltd
"""
Cargo credential provider command.

Implements the Cargo credential provider protocol for Cloudsmith registries.

See: https://doc.rust-lang.org/cargo/reference/credential-provider-protocol.html
"""

import sys

import click

from ....credential_helpers.cargo import execute
from ...decorators import common_api_auth_options, resolve_credentials


@click.command(context_settings={"ignore_unknown_options": True})
@click.option(
"--cargo-plugin",
is_flag=True,
default=False,
hidden=True,
help="Passed by Cargo when invoking this command as a credential provider.",
)
@click.argument("provider_args", nargs=-1, type=click.UNPROCESSED)
@common_api_auth_options
@resolve_credentials
def cargo(opts, cargo_plugin, provider_args): # pylint: disable=unused-argument
"""
Cargo credential provider for Cloudsmith registries.

Speaks the Cargo credential provider protocol: a newline-delimited JSON
conversation on stdin/stdout, starting with a hello message that announces
the supported protocol versions, then one response per request.

Provides credentials for all Cloudsmith Cargo registries: ``*.cloudsmith.io``,
``*.cloudsmith.com``, and any custom domains configured for the organisation
(requires an organisation - ``--org``, CLOUDSMITH_ORG or ``org`` in
``config.ini`` - and a valid API key/token).

A registry that is not a Cloudsmith one is answered with
``url-not-supported`` so Cargo falls through to the next configured
credential provider. ``cargo login``/``cargo logout`` are answered with
``operation-not-supported``: credentials come from the Cloudsmith CLI's own
provider chain, so there is nothing to store or clear.

\b
Input (stdin):
One JSON request per line, e.g.
{"v":1,"kind":"get","operation":"read",
"registry":{"index-url":"sparse+https://cargo.cloudsmith.io/org/repo/"}}

\b
Output (stdout):
{"v":[1]}
{"Ok":{"kind":"get","token":"<cloudsmith-token>","cache":"session",
"operation_independent":true}}

\b
Exit codes:
0: Session completed
1: No credentials available, or the session broke down

\b
Examples:
# Manual testing
$ echo '{"v":1,"kind":"get","operation":"read","registry":{"index-url":"sparse+https://cargo.cloudsmith.io/org/repo/"}}' \\
| cloudsmith credential-helper cargo

# Called by Cargo via the launcher
$ cargo-credential-cloudsmith --cargo-plugin

\b
Environment variables:
CLOUDSMITH_API_KEY: API key for authentication (optional)
CLOUDSMITH_ORG: Organisation slug (required for custom domain support)
"""
# `provider_args` collects the extra arguments Cargo appends from the
# credential-provider config entry. This provider takes no configuration
# of its own, so they are accepted and ignored rather than rejected — an
# unknown-option error would surface as an authentication failure.
exit_code, stderr = execute(
sys.stdin,
sys.stdout,
credential=opts.credential,
api_host=opts.api_host,
org=opts.org,
)

if stderr is not None:
click.echo(stderr, err=True)
sys.exit(exit_code)
2 changes: 2 additions & 0 deletions cloudsmith_cli/cli/commands/credential_helper/manage.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import click

from cloudsmith_cli.credential_helpers.cargo.installer import CargoInstaller
from cloudsmith_cli.credential_helpers.generic import PartialInstallError
from cloudsmith_cli.credential_helpers.pnpm.installer import PNPMInstaller

Expand All @@ -31,6 +32,7 @@
_INSTALLERS: dict[str, type] = {
"docker": DockerInstaller,
"pnpm": PNPMInstaller,
"cargo": CargoInstaller,
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
import stat
import sys
from pathlib import Path
from typing import TYPE_CHECKING
from unittest.mock import patch

import click.testing
import pytest
from _pytest.monkeypatch import MonkeyPatch

from cloudsmith_cli.credential_helpers.generic import PartialInstallError
from cloudsmith_cli.credential_helpers.pnpm.installer import PNPMInstaller
Expand All @@ -30,6 +30,9 @@
write_launcher,
)

if TYPE_CHECKING:
from _pytest.monkeypatch import MonkeyPatch

# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
Expand Down
5 changes: 4 additions & 1 deletion cloudsmith_cli/cli/tests/commands/test_mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from pathlib import Path
from unittest.mock import patch

import cloudsmith_api
import pytest

from ....cli.commands.mcp import (
Expand Down Expand Up @@ -291,6 +290,8 @@ def test_server_generates_tools_from_openapi_spec(self):
}
}

import cloudsmith_api

# Create API config
api_config = cloudsmith_api.Configuration()
api_config.host = "https://api.cloudsmith.io"
Expand Down Expand Up @@ -349,6 +350,8 @@ def test_server_respects_tool_filtering(self):
}
}

import cloudsmith_api

api_config = cloudsmith_api.Configuration()
api_config.host = "https://api.cloudsmith.io"
api_config.api_key = {"X-Api-Key": "test-key"}
Expand Down
Loading
Loading