Skip to content
Open
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
17 changes: 14 additions & 3 deletions src/cwsandbox/_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
from collections.abc import Callable
from dataclasses import dataclass

from cwsandbox._client_metadata import (
HEADER_CWSANDBOX_CLIENT_VERSION,
client_metadata_headers,
)
from cwsandbox.exceptions import CWSandboxAuthenticationError

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -107,11 +111,18 @@ def resolve_auth() -> AuthHeaders:
def resolve_auth_metadata() -> tuple[tuple[str, str], ...]:
"""Resolve authentication credentials and return as gRPC metadata tuples.

Convenience wrapper around resolve_auth() that returns metadata in the
format expected by gRPC calls (lowercase key-value tuples).
Convenience wrapper around resolve_auth() that returns auth headers plus
cwsandbox-managed client metadata in the format expected by gRPC calls
(lowercase key-value tuples).

Returns:
Tuple of (key, value) pairs suitable for gRPC metadata parameter
"""
auth = resolve_auth()
return tuple((k.lower(), v) for k, v in auth.headers.items())
metadata = {k.lower(): v for k, v in auth.headers.items()}
for key, value in client_metadata_headers():
if key == HEADER_CWSANDBOX_CLIENT_VERSION:
metadata[key] = value
else:
metadata.setdefault(key, value)
return tuple(metadata.items())
46 changes: 46 additions & 0 deletions src/cwsandbox/_client_metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# SPDX-FileCopyrightText: 2025 CoreWeave, Inc.
# SPDX-License-Identifier: Apache-2.0
# SPDX-PackageName: cwsandbox-client

"""Request-scoped client metadata sent as gRPC metadata."""

from __future__ import annotations

from importlib import metadata as importlib_metadata

HEADER_CWSANDBOX_CLIENT_VERSION = "x-cwsandbox-client-version"
HEADER_SANDBOX_INTEGRATION = "x-sandbox-integration"

_INTEGRATION_METADATA_VALUE = ""


def _cwsandbox_version() -> str:
try:
return importlib_metadata.version("cwsandbox")
except importlib_metadata.PackageNotFoundError:
from cwsandbox import __version__

return __version__


def set_integration_metadata(integration: str) -> None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this going to be used by external callers like the wandb library? If so we should import it within __init__.py, like we do for set_auth_mode.

"""Set the integration name attached to future sandbox API requests.

The value is process-global and optional. Passing an empty string clears the
integration metadata.
"""
global _INTEGRATION_METADATA_VALUE
_INTEGRATION_METADATA_VALUE = integration


def _reset_client_metadata_for_testing() -> None:
"""Reset process-global client metadata for unit-test isolation."""
set_integration_metadata("")


def client_metadata_headers() -> tuple[tuple[str, str], ...]:
"""Return cwsandbox-managed client metadata headers."""
headers = [(HEADER_CWSANDBOX_CLIENT_VERSION, _cwsandbox_version())]
if _INTEGRATION_METADATA_VALUE:
headers.append((HEADER_SANDBOX_INTEGRATION, _INTEGRATION_METADATA_VALUE))
return tuple(headers)
7 changes: 6 additions & 1 deletion tests/unit/cwsandbox/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@

import asyncio
import concurrent.futures
from collections.abc import Generator
from typing import TypeVar
from unittest.mock import MagicMock

import pytest

from cwsandbox import OperationRef
from cwsandbox._client_metadata import _reset_client_metadata_for_testing
from cwsandbox._types import Process, ProcessResult, StreamReader

T = TypeVar("T")
Expand All @@ -33,7 +35,7 @@


@pytest.fixture(autouse=True)
def clean_auth_env(monkeypatch: pytest.MonkeyPatch) -> None:
def clean_auth_env(monkeypatch: pytest.MonkeyPatch) -> Generator[None, None, None]:
"""Clear all auth-related env vars before each test.

This runs automatically for every test (autouse=True) and ensures:
Expand All @@ -43,6 +45,9 @@ def clean_auth_env(monkeypatch: pytest.MonkeyPatch) -> None:
"""
for var in AUTH_ENV_VARS:
monkeypatch.delenv(var, raising=False)
_reset_client_metadata_for_testing()
yield
_reset_client_metadata_for_testing()


@pytest.fixture(autouse=True)
Expand Down
57 changes: 52 additions & 5 deletions tests/unit/cwsandbox/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,19 @@

import pytest

from cwsandbox import __version__
from cwsandbox._auth import (
AuthHeaders,
_reset_auth_mode_for_testing,
resolve_auth,
resolve_auth_metadata,
set_auth_mode,
)
from cwsandbox._client_metadata import (
HEADER_CWSANDBOX_CLIENT_VERSION,
HEADER_SANDBOX_INTEGRATION,
set_integration_metadata,
)
from cwsandbox.exceptions import CWSandboxAuthenticationError


Expand Down Expand Up @@ -206,7 +212,10 @@ def test_returns_lowercased_metadata_tuples(self, monkeypatch: pytest.MonkeyPatc

result = resolve_auth_metadata()

assert result == (("authorization", "Bearer test-key"),)
assert result == (
("authorization", "Bearer test-key"),
(HEADER_CWSANDBOX_CLIENT_VERSION, __version__),
)

def test_returns_registered_auth_mode_metadata(
self,
Expand All @@ -224,12 +233,50 @@ def test_returns_registered_auth_mode_metadata(
finally:
_reset_auth_mode_for_testing()

assert result == (("x-api-key", "mode-key"),)
assert result == (
("x-api-key", "mode-key"),
(HEADER_CWSANDBOX_CLIENT_VERSION, __version__),
)

def test_returns_empty_tuple_when_no_auth(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""Test returns empty tuple when no credentials found."""
def test_returns_client_metadata_when_no_auth(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""Test cwsandbox client metadata is sent even without auth credentials."""
monkeypatch.delenv("CWSANDBOX_API_KEY", raising=False)

result = resolve_auth_metadata()

assert result == ()
assert result == ((HEADER_CWSANDBOX_CLIENT_VERSION, __version__),)

def test_returns_integration_metadata_when_set(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""Test integration metadata is included only when explicitly set."""
monkeypatch.setenv("CWSANDBOX_API_KEY", "test-key")
set_integration_metadata("harbor")

result = resolve_auth_metadata()

assert result == (
("authorization", "Bearer test-key"),
(HEADER_CWSANDBOX_CLIENT_VERSION, __version__),
(HEADER_SANDBOX_INTEGRATION, "harbor"),
)

def test_auth_mode_metadata_can_override_integration_metadata(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Test auth modes may provide their own integration metadata header."""
monkeypatch.delenv("CWSANDBOX_API_KEY", raising=False)
set_integration_metadata("client-default")
set_auth_mode(
"auth-mode-test",
lambda: AuthHeaders(
headers={HEADER_SANDBOX_INTEGRATION: "auth-integration"},
strategy="auth_mode",
),
)

try:
result = dict(resolve_auth_metadata())
finally:
_reset_auth_mode_for_testing()

assert result[HEADER_SANDBOX_INTEGRATION] == "auth-integration"
assert result[HEADER_CWSANDBOX_CLIENT_VERSION] == __version__
Loading