You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat(ENG-13680): add a NuGet credential provider - #494
Adds a NuGet credential provider (ENG-13680). After a one-off install, dotnet restore, MSBuild and Visual Studio authenticate to Cloudsmith feeds with the CLI's own credentials: an API key, a SAML session from the keyring, or OIDC. No token goes into nuget.config.
Install.credential-helper install nuget writes a nuget-plugin-cloudsmith launcher next to cloudsmith, as a .bat on Windows. NuGet 6.13 and later (.NET SDK 9.0.200+) finds any nuget-plugin-* executable on PATH, so no NuGet config changes.
NuGet often runs the plugin from an IDE or a build agent that does not have your shell's environment. So the launcher keeps the CLOUDSMITH_* settings that you gave at install time as a flag or an environment variable, with the shared code from #511. These values win over the runtime environment. Secrets and the proxy URL are never written.
Runtime.credential-helper nuget speaks NuGet's cross-platform plugin protocol. NuGet asks every plugin about every feed:
An https feed on *.cloudsmith.io, *.cloudsmith.com or a NuGet domain of the Workspace → Basic token:<credential>.
Any other feed, or a failed domain lookup → NotFound, so NuGet tries its other credential sources.
A Cloudsmith feed with no credential → Error, with a message that says how to authenticate.
A retry after a 401 resolves the credential again, so a renewed SAML or OIDC token is used.
--domain <host> adds hosts that always match. --no-discover turns off the domain lookup, so only those hosts and the Cloudsmith hosts match. Stdout carries only protocol messages, also under --debug.
Changes outside NuGet.
core/api/init.py writes its --debug credential messages to stderr. One of them now says "SAML" instead of "SSO".
resolve_credentials keeps its CredentialContext on opts for the retry. is_cloudsmith_domain passes configure_api through, so the domain lookup keeps the CLI's proxy and TLS settings.
Type of Change
Bug fix
New feature
Breaking change
Documentation update
Refactoring
Other (please describe)
Additional Notes
This replaces #458, which has the earlier review history.
Testing. A temporary workflow ran a real dotnet restore (SDK 10) on Linux, macOS and Windows, from both a pip install and the PyInstaller binary. It covered a standard feed, a custom domain found by the live lookup with --debug on, and --domain --no-discover with an unreachable API host. Each job checks that the restore fails with 401 without the plugin and succeeds with it. All 18 jobs passed: run 38967385354.
I also ran dotnet nuget push and dotnet restore locally against a private repo with only a SAML session. Both worked with no API key.
Known limits.
The pnpm and cargo launchers do not keep install-time settings yet.
NuGet's plugin timeouts are 30 seconds in ProtocolConstants, not the 5 seconds in the Microsoft docs. Credentials resolve before the handshake, so the plugin needs no threads or progress messages.
The keyring module read CLOUDSMITH_NO_KEYRING, CLOUDSMITH_KEYRING_BACKEND,
CLOUDSMITH_KEYRING_KEY, CLOUDSMITH_KEYRING_FILE_PATH and
CLOUDSMITH_KEYRING_DIR with os.environ.get(). These settings have no
flags, so Click did not know about them.
- `keyring_options` on the `main` group adds hidden options that read
the five variables. It gives the values to the keyring module through
`keyring.configure(KeyringSettings(...))`.
- The keyring module reads its settings from `configure()`, not from the
environment. It still copies them to PYTHON_KEYRING_BACKEND and
KEYRING_PROPERTY_*, because the keyring library reads only those.
- The path settings are expanded and made absolute when Click parses
them, not when the keyring uses them.
- CLOUDSMITH_NO_KEYRING keeps its parsing: only 1, true and yes, in any
case, turn off the keyring. Other values leave it on, as before.
- A root conftest fixture resets the settings before each test, so tests
no longer depend on the keyring variables of the developer shell.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ttings
A package manager can run its credential helper from an IDE, Docker
Desktop or a build agent that does not have the environment of the
install. This change adds the shared code that writes the install-time
CLOUDSMITH_* settings into a launcher.
- `write_launcher(env=...)` in `launchers.py` writes the settings as
`export` lines, or as `set` lines in a Windows batch file. The settings
replace the values of the caller.
- `batch_value()` refuses a value that a batch file cannot hold. A
non-ASCII path under the user profile becomes `%USERPROFILE%`.
- `install` passes `launcher_env` to an installer that sets
`KEEPS_INSTALL_TIME_SETTINGS`. It holds each option with a
CLOUDSMITH_* environment variable whose value came from a flag or the
environment, also on a parent command, so the keyring settings of
`main` are kept too. The API key, the custom headers, the keyring key
and the proxy never go into it.
No installer sets `KEEPS_INSTALL_TIME_SETTINGS` yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`cloudsmith credential-helper install nuget` writes a
`nuget-plugin-cloudsmith` launcher (`.bat` on Windows) on PATH. NuGet 6.13+
finds it and speaks the cross-platform plugin protocol to
`cloudsmith credential-helper nuget`. The plugin answers for Cloudsmith hosts
and the NuGet domains of the Workspace that is written into the launcher.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
# CHANGELOG.md
# cloudsmith_cli/cli/commands/credential_helper/manage.py
# cloudsmith_cli/cli/tests/commands/test_credential_helper_install.py
# cloudsmith_cli/core/api/init.py
# cloudsmith_cli/credential_helpers/launchers.py
# Conflicts:
# cloudsmith_cli/cli/commands/credential_helper/manage.py
# cloudsmith_cli/cli/tests/commands/test_credential_helper_install.py
The pnpm install wrote .npmrc without a final newline. A shell append
such as `echo ... >> ~/.npmrc` then joined the last tokenHelper line.
Write a final newline after each line. Add the newline to an existing
.npmrc that does not have one. Show each tokenHelper line that a real
install sets, as the dry run does.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: cloudsmith-iduffy <178375997+cloudsmith-iduffy@users.noreply.github.com>
…ttings
A package manager can run its credential helper from an IDE, Docker
Desktop or a build agent that does not have the environment of the
install. This change adds the shared code that writes the install-time
CLOUDSMITH_* settings into a launcher.
- `write_launcher(env=...)` in `launchers.py` writes the settings as
`export` lines, or as `set` lines in a Windows batch file. The settings
replace the values of the caller.
- `batch_value()` refuses a value that a batch file cannot hold. A
non-ASCII path under the user profile becomes `%USERPROFILE%`.
- `install` passes `launcher_env` to an installer that sets
`KEEPS_INSTALL_TIME_SETTINGS`. It holds each option with a
CLOUDSMITH_* environment variable whose value came from a flag or the
environment, also on a parent command, so the keyring settings of
`main` are kept too. The API key, the custom headers, the keyring key
and the proxy never go into it.
No installer sets `KEEPS_INSTALL_TIME_SETTINGS` yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The test runs `install nuget` with no --bin-dir and does not patch the
resolver, so `list` must look in ~/.cloudsmith/bin.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base automatically changed from
iduffy/launcher-install-time-settings to
masterNovember 3, 2026 10:51
A worker thread answers GetAuthenticationCredentials. The main thread
sends Progress at half of the Initialize request timeout until the
answer is ready. It keeps reading stdin, so a Cancel gets a Cancel at
once and the late answer is not sent.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`initialise_api` and `resolve_credentials` take a `defer` option. The
NuGet command uses it, so it sends the handshake before the provider
chain runs. The first credentials request resolves the credential in
the worker thread, which sends Progress. The command keeps the
credential on `opts`, where telemetry redacts it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
Non-interactive credential lookup can still trigger keyring password prompts, and in-flight lookups can delay shutdown or emit responses after NuGet disconnects.
Close, the end of stdin or a stdin error now ends the session while a
request runs. The plugin sends no more Progress and drops the late
answer. Each request runs in a daemon thread, so a slow lookup cannot
delay the exit. A request that raises gets a Fault.
The tests now play NuGet: they send Close or end stdin only after the
plugin answers each request.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The credential callback takes the request's IsNonInteractive value.
When it is true, the command calls keyring.forbid_password_prompts(),
so an encrypted keyring refuses to prompt also at a terminal.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Initial credential failures persist on subsequent non-retry requests, and unrelated feeds can trigger unnecessary network token retrieval when discovery is disabled.
With --no-discover, an unrelated feed still triggers credential resolution before its host is rejected. A cold OIDC cache can therefore cause network token retrieval and exchange, delaying authentication for another provider's feed just to return NotFound. Reject hosts outside the standard and explicit allowlists before resolving credentials when discovery is disabled.
If the first credential resolution returns None, later non-retry requests reuse that failure even if credentials have since become available. Only mark the credential resolved when a credential was returned, so an initial failure can recover on a subsequent request. Add a regression test where resolution returns None and then succeeds on a non-retry request.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Adds a NuGet credential provider (ENG-13680). After a one-off install,
dotnet restore, MSBuild and Visual Studio authenticate to Cloudsmith feeds with the CLI's own credentials: an API key, a SAML session from the keyring, or OIDC. No token goes intonuget.config.Install.
credential-helper install nugetwrites anuget-plugin-cloudsmithlauncher next tocloudsmith, as a.baton Windows. NuGet 6.13 and later (.NET SDK 9.0.200+) finds anynuget-plugin-*executable on PATH, so no NuGet config changes.NuGet often runs the plugin from an IDE or a build agent that does not have your shell's environment. So the launcher keeps the
CLOUDSMITH_*settings that you gave at install time as a flag or an environment variable, with the shared code from #511. These values win over the runtime environment. Secrets and the proxy URL are never written.Runtime.
credential-helper nugetspeaks NuGet's cross-platform plugin protocol. NuGet asks every plugin about every feed:httpsfeed on*.cloudsmith.io,*.cloudsmith.comor a NuGet domain of the Workspace → Basictoken:<credential>.NotFound, so NuGet tries its other credential sources.Error, with a message that says how to authenticate.--domain <host>adds hosts that always match.--no-discoverturns off the domain lookup, so only those hosts and the Cloudsmith hosts match. Stdout carries only protocol messages, also under--debug.Changes outside NuGet.
core/api/init.pywrites its--debugcredential messages to stderr. One of them now says "SAML" instead of "SSO".resolve_credentialskeeps itsCredentialContextonoptsfor the retry.is_cloudsmith_domainpassesconfigure_apithrough, so the domain lookup keeps the CLI's proxy and TLS settings.Type of Change
Additional Notes
This replaces #458, which has the earlier review history.
Testing. A temporary workflow ran a real
dotnet restore(SDK 10) on Linux, macOS and Windows, from both a pip install and the PyInstaller binary. It covered a standard feed, a custom domain found by the live lookup with--debugon, and--domain --no-discoverwith an unreachable API host. Each job checks that the restore fails with 401 without the plugin and succeeds with it. All 18 jobs passed: run 38967385354.I also ran
dotnet nuget pushanddotnet restorelocally against a private repo with only a SAML session. Both worked with no API key.Known limits.
NuGet's plugin timeouts are 30 seconds in
ProtocolConstants, not the 5 seconds in the Microsoft docs. Credentials resolve before the handshake, so the plugin needs no threads or progress messages.🤖 Generated with Claude Code