diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f8e1ac61..9017f58b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: lint: timeout-minutes: 10 name: lint - runs-on: ${{ github.repository == 'stainless-sdks/anthropic-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + runs-on: 'ubuntu-latest' if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -41,7 +41,7 @@ jobs: permissions: contents: read id-token: write - runs-on: ${{ github.repository == 'stainless-sdks/anthropic-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + runs-on: 'ubuntu-latest' steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -58,7 +58,7 @@ jobs: - name: Get GitHub OIDC Token if: |- - github.repository == 'stainless-sdks/anthropic-python' && + github.repository == 'anthropics/anthropic-sdk-python-private' && !startsWith(github.ref, 'refs/heads/stl/') id: github-oidc uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 @@ -67,7 +67,7 @@ jobs: - name: Upload tarball if: |- - github.repository == 'stainless-sdks/anthropic-python' && + github.repository == 'anthropics/anthropic-sdk-python-private' && !startsWith(github.ref, 'refs/heads/stl/') env: URL: https://pkg.stainless.com/s @@ -78,7 +78,7 @@ jobs: test: timeout-minutes: 10 name: test - runs-on: ${{ github.repository == 'stainless-sdks/anthropic-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} + runs-on: 'ubuntu-latest' if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 diff --git a/.release-please-manifest.json b/.release-please-manifest.json index e2f2f789..936a0575 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.105.2" + ".": "0.105.3" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 9de2815a..bbb00e55 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 106 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/anthropic/anthropic-3044bdebca823a5e350accb2a228438e6ca5fb06cbac2cb1e0f98baa2bcc2359.yml +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/anthropic/anthropic-1b27a5198555a9d1544d42e56bbda5b2e3aeae5b96d98b8c0a8ce1304f0ddc0d.yml openapi_spec_hash: 2790eecb0b38738a32441184273601a7 -config_hash: 54a8475666d840df59a2a85a8c02b274 +config_hash: c7e3e2123ce7ea9cb44b5acb2fe2f708 diff --git a/CHANGELOG.md b/CHANGELOG.md index 98718897..81a14d9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## 0.105.3 (2026-06-04) + +Full Changelog: [v0.105.2...v0.105.3](https://github.com/anthropics/anthropic-sdk-python/compare/v0.105.2...v0.105.3) + +### Bug Fixes + +* **client:** make Foundry client copy() and with_options() work ([94146ac](https://github.com/anthropics/anthropic-sdk-python/commit/94146acdc1c6f66f187d5a42e4afbb911e692fe8)) +* **transform schema:** preserve $defs when schema root is a $ref ([#1642](https://github.com/anthropics/anthropic-sdk-python/issues/1642)) ([fc58e06](https://github.com/anthropics/anthropic-sdk-python/commit/fc58e06b78407b447c50dfea109c6fb300f4b97d)) + + +### Chores + +* **internal:** fix artifact url ([a6ed0c4](https://github.com/anthropics/anthropic-sdk-python/commit/a6ed0c4124d29989a568a27293dadf66e7ebcd6f)) +* **internal:** fix branch names ([3b03370](https://github.com/anthropics/anthropic-sdk-python/commit/3b0337074f0bbab47bf7f5a2b76b4d240cff719a)) +* **internal:** update private repo name ([7dbcb05](https://github.com/anthropics/anthropic-sdk-python/commit/7dbcb05706f1865afeee62fb06e400f5c4bf619e)) + + +### Documentation + +* point security reports to Anthropic's HackerOne program ([#10](https://github.com/anthropics/anthropic-sdk-python/issues/10)) ([80f2c97](https://github.com/anthropics/anthropic-sdk-python/commit/80f2c97b8e9534f9879945de11c11aba00cf8704)) + ## 0.105.2 (2026-05-29) Full Changelog: [v0.105.1...v0.105.2](https://github.com/anthropics/anthropic-sdk-python/compare/v0.105.1...v0.105.2) diff --git a/SECURITY.md b/SECURITY.md index 49f2cc77..95b204aa 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,27 +1,15 @@ # Security Policy -## Reporting Security Issues - -This SDK is generated by [Stainless Software Inc](http://stainless.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken. - -To report a security issue, please contact the Stainless team at security@stainless.com. - -## Responsible Disclosure - -We appreciate the efforts of security researchers and individuals who help us maintain the security of -SDKs we generate. If you believe you have found a security vulnerability, please adhere to responsible -disclosure practices by allowing us a reasonable amount of time to investigate and address the issue -before making any information public. +Thank you for helping us keep the SDKs and systems they interact with secure. -## Reporting Non-SDK Related Security Issues +## Reporting Security Issues -If you encounter security issues that are not directly related to SDKs but pertain to the services -or products provided by Anthropic, please follow the respective company's security reporting guidelines. +This SDK is maintained by [Anthropic](https://www.anthropic.com/). -### Anthropic Terms and Policies +The security of our systems and user data is Anthropic’s top priority. We appreciate the work of security researchers acting in good faith in identifying and reporting potential vulnerabilities. -Please contact support@anthropic.com for any questions or concerns regarding the security of our services. +Our security program is managed on HackerOne and we ask that any validated vulnerability in this functionality be reported through their [submission form](https://hackerone.com/4f1f16ba-10d3-4d09-9ecc-c721aad90f24/embedded_submissions/new). ---- +## Anthropic Bug Bounty -Thank you for helping us keep the SDKs and systems they interact with secure. +Our Bug Bounty Program Guidelines are defined on our [HackerOne program page](https://hackerone.com/anthropic). diff --git a/pyproject.toml b/pyproject.toml index dcdb61c5..27a3f1e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "anthropic" -version = "0.105.2" +version = "0.105.3" description = "The official Python library for the anthropic API" dynamic = ["readme"] license = "MIT" diff --git a/src/anthropic/_version.py b/src/anthropic/_version.py index c63f6427..cf6af52c 100644 --- a/src/anthropic/_version.py +++ b/src/anthropic/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "anthropic" -__version__ = "0.105.2" # x-release-please-version +__version__ = "0.105.3" # x-release-please-version diff --git a/src/anthropic/lib/foundry.py b/src/anthropic/lib/foundry.py index fde5be54..0a9a00a9 100644 --- a/src/anthropic/lib/foundry.py +++ b/src/anthropic/lib/foundry.py @@ -8,7 +8,7 @@ import httpx -from .._types import NOT_GIVEN, Omit, Timeout, NotGiven +from .._types import NOT_GIVEN, Omit, Headers, Timeout, NotGiven from .._utils import is_given from .._client import Anthropic, AsyncAnthropic from .._compat import model_copy @@ -196,12 +196,11 @@ def beta(self) -> Beta: # type: ignore[override] return BetaFoundry(self) @override - def copy( # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride] — subclass intentionally drops `credentials` + def copy( # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride] — subclass intentionally drops `credentials` & `auth_token` self, *, api_key: str | None = None, azure_ad_token_provider: AzureADTokenProvider | None = None, - auth_token: str | None = None, webhook_key: str | None = None, base_url: str | httpx.URL | None = None, timeout: float | Timeout | None | NotGiven = NOT_GIVEN, @@ -216,22 +215,35 @@ def copy( # type: ignore[override] # pyright: ignore[reportIncompatibleMethodO """ Create a new client instance re-using the same options given to the current client with optional overriding. """ - return super().copy( - api_key=api_key, - auth_token=auth_token, - webhook_key=webhook_key, - base_url=base_url, - timeout=timeout, - http_client=http_client, - max_retries=max_retries, - default_headers=default_headers, - set_default_headers=set_default_headers, - default_query=default_query, - set_default_query=set_default_query, - _extra_kwargs={ - "azure_ad_token_provider": azure_ad_token_provider or self._azure_ad_token_provider, - **_extra_kwargs, - }, + if default_headers is not None and set_default_headers is not None: + raise ValueError("The `default_headers` and `set_default_headers` arguments are mutually exclusive") + + if default_query is not None and set_default_query is not None: + raise ValueError("The `default_query` and `set_default_query` arguments are mutually exclusive") + + headers = self._custom_headers + if default_headers is not None: + headers = {**headers, **default_headers} + elif set_default_headers is not None: + headers = set_default_headers + + params = self._custom_query + if default_query is not None: + params = {**params, **default_query} + elif set_default_query is not None: + params = set_default_query + + return self.__class__( + api_key=api_key or self.api_key, + azure_ad_token_provider=azure_ad_token_provider or self._azure_ad_token_provider, + webhook_key=webhook_key or self.webhook_key, + base_url=str(base_url or self.base_url), + timeout=self.timeout if isinstance(timeout, NotGiven) else timeout, + http_client=http_client or self._client, + max_retries=max_retries if is_given(max_retries) else self.max_retries, + default_headers=headers, + default_query=params, + **_extra_kwargs, ) with_options = copy # type: ignore[assignment] @@ -269,6 +281,24 @@ def _prepare_options(self, options: FinalRequestOptions) -> FinalRequestOptions: return options + @property + @override + def auth_headers(self) -> dict[str, str]: + # Auth is attached per-request in `_prepare_options` (an `api-key` header for + # API-key auth, or a bearer `Authorization` header for the Azure AD token + # provider). Emitting nothing here stops the base client from sending an + # `X-Api-Key` derived from `self.api_key`: when only an Azure AD token + # provider is configured, `self.api_key` can be populated from an + # `ANTHROPIC_API_KEY` in the environment, which must not be sent to the + # Foundry endpoint. + return {} + + @override + def _validate_headers(self, headers: Headers, custom_headers: Headers) -> None: + # Foundry attaches its own auth header in `_prepare_options`, so the base + # requirement that `X-Api-Key`/`Authorization` already be present does not apply. + return + class AsyncAnthropicFoundry(BaseFoundryClient[httpx.AsyncClient, AsyncStream[Any]], AsyncAnthropic): @overload @@ -379,12 +409,11 @@ def beta(self) -> AsyncBetaFoundry: # type: ignore[override] return AsyncBetaFoundry(client=self) @override - def copy( # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride] — subclass intentionally drops `credentials` + def copy( # type: ignore[override] # pyright: ignore[reportIncompatibleMethodOverride] — subclass intentionally drops `credentials` & `auth_token` self, *, api_key: str | None = None, azure_ad_token_provider: AsyncAzureADTokenProvider | None = None, - auth_token: str | None = None, webhook_key: str | None = None, base_url: str | httpx.URL | None = None, timeout: float | Timeout | None | NotGiven = NOT_GIVEN, @@ -399,22 +428,35 @@ def copy( # type: ignore[override] # pyright: ignore[reportIncompatibleMethodO """ Create a new client instance re-using the same options given to the current client with optional overriding. """ - return super().copy( - api_key=api_key, - auth_token=auth_token, - webhook_key=webhook_key, - base_url=base_url, - timeout=timeout, - http_client=http_client, - max_retries=max_retries, - default_headers=default_headers, - set_default_headers=set_default_headers, - default_query=default_query, - set_default_query=set_default_query, - _extra_kwargs={ - "azure_ad_token_provider": azure_ad_token_provider or self._azure_ad_token_provider, - **_extra_kwargs, - }, + if default_headers is not None and set_default_headers is not None: + raise ValueError("The `default_headers` and `set_default_headers` arguments are mutually exclusive") + + if default_query is not None and set_default_query is not None: + raise ValueError("The `default_query` and `set_default_query` arguments are mutually exclusive") + + headers = self._custom_headers + if default_headers is not None: + headers = {**headers, **default_headers} + elif set_default_headers is not None: + headers = set_default_headers + + params = self._custom_query + if default_query is not None: + params = {**params, **default_query} + elif set_default_query is not None: + params = set_default_query + + return self.__class__( + api_key=api_key or self.api_key, + azure_ad_token_provider=azure_ad_token_provider or self._azure_ad_token_provider, + webhook_key=webhook_key or self.webhook_key, + base_url=str(base_url or self.base_url), + timeout=self.timeout if isinstance(timeout, NotGiven) else timeout, + http_client=http_client or self._client, + max_retries=max_retries if is_given(max_retries) else self.max_retries, + default_headers=headers, + default_query=params, + **_extra_kwargs, ) with_options = copy # type: ignore[assignment] @@ -453,3 +495,15 @@ async def _prepare_options(self, options: FinalRequestOptions) -> FinalRequestOp raise ValueError("Unable to handle auth") return options + + @property + @override + def auth_headers(self) -> dict[str, str]: + # See AnthropicFoundry.auth_headers: prevents leaking an environment + # ANTHROPIC_API_KEY as X-Api-Key to the Foundry endpoint. + return {} + + @override + def _validate_headers(self, headers: Headers, custom_headers: Headers) -> None: + # Foundry attaches its own auth header in `_prepare_options`. + return diff --git a/tests/lib/test_azure.py b/tests/lib/test_azure.py index ec3ef7b1..abb86cf2 100644 --- a/tests/lib/test_azure.py +++ b/tests/lib/test_azure.py @@ -2,6 +2,7 @@ import pytest +from anthropic._models import FinalRequestOptions from anthropic._exceptions import AnthropicError from anthropic.lib.foundry import AnthropicFoundry, AsyncAnthropicFoundry @@ -65,6 +66,41 @@ def test_missing_resource_error(self) -> None: api_key="test-key", ) + def test_copy(self) -> None: + """Test copy() carries over the client configuration.""" + + def token_provider() -> str: + return "test-token" + + client = AnthropicFoundry( + azure_ad_token_provider=token_provider, + resource="example-resource", + default_headers={"x-app": "1"}, + max_retries=5, + ) + + copied = client.copy() + assert str(copied.base_url) == str(client.base_url) + assert copied._azure_ad_token_provider is token_provider + assert copied.default_headers.get("x-app") == "1" + assert copied.max_retries == 5 + assert copied._client is client._client + + def test_with_options_overrides(self) -> None: + """Test with_options() applies overrides while keeping everything else.""" + client = AnthropicFoundry( + api_key="test-key", + resource="example-resource", + default_headers={"x-app": "1"}, + ) + + derived = client.with_options(timeout=10, default_headers={"x-extra": "2"}) + assert derived.timeout == 10 + assert derived.api_key == "test-key" + assert str(derived.base_url) == str(client.base_url) + assert derived.default_headers.get("x-app") == "1" + assert derived.default_headers.get("x-extra") == "2" + class TestAsyncAnthropicFoundry: @pytest.mark.asyncio @@ -104,3 +140,61 @@ async def test_initialization_from_environment_variables(self, monkeypatch: pyte assert client.api_key == "env-key" assert "env-resource.services.ai.azure.com" in str(client.base_url) + + def test_copy(self) -> None: + """Test copy() carries over the client configuration.""" + + async def async_token_provider() -> str: + return "async-test-token" + + client = AsyncAnthropicFoundry( + azure_ad_token_provider=async_token_provider, + resource="example-resource", + max_retries=5, + ) + + copied = client.with_options(timeout=10) + assert str(copied.base_url) == str(client.base_url) + assert copied._azure_ad_token_provider is async_token_provider + assert copied.max_retries == 5 + assert copied.timeout == 10 + + +class TestFoundryDoesNotLeakAnthropicAPIKey: + """A stray ANTHROPIC_API_KEY in the environment must never be sent to the + Foundry endpoint as X-Api-Key, regardless of which auth mode is in use.""" + + def test_no_x_api_key_with_azure_ad_token_provider(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-should-not-leak") + client = AnthropicFoundry(resource="example-resource", azure_ad_token_provider=lambda: "azure-token") + + options = client._prepare_options(FinalRequestOptions(method="post", url="/v1/messages")) + headers = client._build_request(options).headers + + assert headers.get("x-api-key") is None + assert headers.get("authorization") == "Bearer azure-token" + + def test_api_key_mode_sends_only_lowercase_api_key(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-should-not-leak") + client = AnthropicFoundry(api_key="foundry-key", resource="example-resource") + + options = client._prepare_options(FinalRequestOptions(method="post", url="/v1/messages")) + headers = client._build_request(options).headers + + assert headers.get("x-api-key") is None + assert headers.get("api-key") == "foundry-key" + + @pytest.mark.asyncio + async def test_async_no_x_api_key_with_azure_ad_token_provider(self, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-should-not-leak") + + async def token_provider() -> str: + return "azure-token" + + client = AsyncAnthropicFoundry(resource="example-resource", azure_ad_token_provider=token_provider) + + options = await client._prepare_options(FinalRequestOptions(method="post", url="/v1/messages")) + headers = client._build_request(options).headers + + assert headers.get("x-api-key") is None + assert headers.get("authorization") == "Bearer azure-token"