diff --git a/crates/uv-auth/src/lib.rs b/crates/uv-auth/src/lib.rs index 90a957630..8e8a0e057 100644 --- a/crates/uv-auth/src/lib.rs +++ b/crates/uv-auth/src/lib.rs @@ -15,6 +15,7 @@ mod credentials; mod index; mod keyring; mod middleware; +mod providers; mod realm; // TODO(zanieb): Consider passing a cache explicitly throughout diff --git a/crates/uv-auth/src/middleware.rs b/crates/uv-auth/src/middleware.rs index 1842effb3..605675b61 100644 --- a/crates/uv-auth/src/middleware.rs +++ b/crates/uv-auth/src/middleware.rs @@ -7,6 +7,7 @@ use reqwest::{Request, Response}; use reqwest_middleware::{Error, Middleware, Next}; use tracing::{debug, trace, warn}; +use crate::providers::HuggingFaceProvider; use crate::{ CREDENTIALS_CACHE, CredentialsCache, KeyringProvider, cache::FetchUrl, @@ -457,9 +458,8 @@ impl AuthMiddleware { Some(credentials) }; - return self - .complete_request(credentials, request, extensions, next, auth_policy) - .await; + self.complete_request(credentials, request, extensions, next, auth_policy) + .await } /// Fetch credentials for a URL. @@ -503,6 +503,13 @@ impl AuthMiddleware { return credentials; } + // Support for known providers, like Hugging Face. + if let Some(credentials) = HuggingFaceProvider::credentials_for(url).map(Arc::new) { + debug!("Found Hugging Face credentials for {url}"); + self.cache().fetches.done(key, Some(credentials.clone())); + return Some(credentials); + } + // Netrc support based on: . let credentials = if let Some(credentials) = self.netrc.get().and_then(|netrc| { debug!("Checking netrc for credentials for {url}"); diff --git a/crates/uv-auth/src/providers.rs b/crates/uv-auth/src/providers.rs new file mode 100644 index 000000000..85a5a0ec7 --- /dev/null +++ b/crates/uv-auth/src/providers.rs @@ -0,0 +1,49 @@ +use std::sync::LazyLock; +use tracing::debug; +use url::Url; + +use uv_static::EnvVars; + +use crate::Credentials; +use crate::realm::Realm; + +/// The [`Realm`] for the Hugging Face platform. +static HUGGING_FACE_REALM: LazyLock = LazyLock::new(|| { + let url = Url::parse("https://huggingface.co").expect("Failed to parse Hugging Face URL"); + Realm::from(&url) +}); + +/// The authentication token for the Hugging Face platform, if set. +static HUGGING_FACE_TOKEN: LazyLock>> = LazyLock::new(|| { + // Extract the Hugging Face token from the environment variable, if it exists. + let hf_token = std::env::var(EnvVars::HF_TOKEN) + .ok() + .map(String::into_bytes) + .filter(|token| !token.is_empty())?; + + if std::env::var_os(EnvVars::UV_NO_HF_TOKEN).is_some() { + debug!("Ignoring Hugging Face token from environment due to `UV_NO_HF_TOKEN`"); + return None; + } + + debug!("Found Hugging Face token in environment"); + Some(hf_token) +}); + +/// A provider for authentication credentials for the Hugging Face platform. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct HuggingFaceProvider; + +impl HuggingFaceProvider { + /// Returns the credentials for the Hugging Face platform, if available. + pub(crate) fn credentials_for(url: &Url) -> Option { + if Realm::from(url) == *HUGGING_FACE_REALM { + if let Some(token) = HUGGING_FACE_TOKEN.as_ref() { + return Some(Credentials::Bearer { + token: token.clone(), + }); + } + } + None + } +} diff --git a/crates/uv-static/src/env_vars.rs b/crates/uv-static/src/env_vars.rs index f7fa6cb31..0e99ec549 100644 --- a/crates/uv-static/src/env_vars.rs +++ b/crates/uv-static/src/env_vars.rs @@ -765,4 +765,11 @@ impl EnvVars { /// Disable GitHub-specific requests that allow uv to skip `git fetch` in some circumstances. pub const UV_NO_GITHUB_FAST_PATH: &'static str = "UV_NO_GITHUB_FAST_PATH"; + + /// Authentication token for Hugging Face requests. When set, uv will use this token + /// when making requests to `https://huggingface.co/` and any subdomains. + pub const HF_TOKEN: &'static str = "HF_TOKEN"; + + /// Disable Hugging Face authentication, even if `HF_TOKEN` is set. + pub const UV_NO_HF_TOKEN: &'static str = "UV_NO_HF_TOKEN"; } diff --git a/docs/concepts/authentication.md b/docs/concepts/authentication.md index 10bf57c21..fe5314b85 100644 --- a/docs/concepts/authentication.md +++ b/docs/concepts/authentication.md @@ -151,3 +151,18 @@ insecure. Use `allow-insecure-host` with caution and only in trusted environments, as it can expose you to security risks due to the lack of certificate verification. + +## Hugging Face support + +uv supports automatic authentication for the Hugging Face Hub. Specifically, if the `HF_TOKEN` +environment variable is set, uv will propagate it to requests to `huggingface.co`. + +This is particularly useful for accessing private scripts in Hugging Face Datasets. For example, you +can run the following command to execute the script `main.py` script from a private dataset: + +```console +$ HF_TOKEN=hf_... uv run https://huggingface.co/datasets///resolve//main.py +``` + +You can disable automatic Hugging Face authentication by setting the `UV_NO_HF_TOKEN=1` environment +variable. diff --git a/docs/reference/environment.md b/docs/reference/environment.md index e848d4a41..a4d686192 100644 --- a/docs/reference/environment.md +++ b/docs/reference/environment.md @@ -252,6 +252,10 @@ Ignore `.env` files when executing `uv run` commands. Disable GitHub-specific requests that allow uv to skip `git fetch` in some circumstances. +### `UV_NO_HF_TOKEN` + +Disable Hugging Face authentication, even if `HF_TOKEN` is set. + ### `UV_NO_INSTALLER_METADATA` Skip writing `uv` installer metadata files (e.g., `INSTALLER`, `REQUESTED`, and `direct_url.json`) to site-packages `.dist-info` directories. @@ -528,6 +532,11 @@ See [force-color.org](https://force-color.org). Used for trusted publishing via `uv publish`. +### `HF_TOKEN` + +Authentication token for Hugging Face requests. When set, uv will use this token +when making requests to `https://huggingface.co/` and any subdomains. + ### `HOME` The standard `HOME` env var.