diff --git a/src/anthropic/_client.py b/src/anthropic/_client.py index 76b840608..3f527902c 100644 --- a/src/anthropic/_client.py +++ b/src/anthropic/_client.py @@ -211,8 +211,8 @@ def __init__( or profile is not None ) if not has_explicit_credential: - api_key = os.environ.get("ANTHROPIC_API_KEY") - auth_token = os.environ.get("ANTHROPIC_AUTH_TOKEN") + api_key = os.environ.get("ANTHROPIC_API_KEY") or None + auth_token = os.environ.get("ANTHROPIC_AUTH_TOKEN") or None self.api_key = api_key self.auth_token = auth_token # --- end credentials support --- @@ -628,8 +628,8 @@ def __init__( or profile is not None ) if not has_explicit_credential: - api_key = os.environ.get("ANTHROPIC_API_KEY") - auth_token = os.environ.get("ANTHROPIC_AUTH_TOKEN") + api_key = os.environ.get("ANTHROPIC_API_KEY") or None + auth_token = os.environ.get("ANTHROPIC_AUTH_TOKEN") or None self.api_key = api_key self.auth_token = auth_token # --- end credentials support --- diff --git a/tests/lib/tools/test_agent_toolset.py b/tests/lib/tools/test_agent_toolset.py index a4ebfd435..e2b3d0e2f 100644 --- a/tests/lib/tools/test_agent_toolset.py +++ b/tests/lib/tools/test_agent_toolset.py @@ -22,6 +22,7 @@ ) needs_pydantic_v2 = pytest.mark.skipif(PYDANTIC_V1, reason="tool functions are only supported with pydantic v2") +needs_symlinks = pytest.mark.skipif(sys.platform == "win32", reason="symlink fixtures need a POSIX filesystem") @pytest.mark.parametrize( @@ -373,6 +374,7 @@ async def test_bash_outer_cancel_closes_subprocess_no_stale_state(tmp_path: Path await s.close() +@needs_symlinks @needs_pydantic_v2 async def test_read_through_symlink_escape_is_rejected(tmp_path: Path) -> None: """resolve_path realpaths, so a symlink that escapes the workdir is caught.""" @@ -401,6 +403,7 @@ async def test_glob_rejects_dotdot_pattern(tmp_path: Path) -> None: await beta_glob_tool(env).call({"pattern": "../outside/*.txt"}) +@needs_symlinks @needs_pydantic_v2 async def test_glob_post_filters_symlink_escape(tmp_path: Path) -> None: """A symlink traversed mid-pattern must not let a glob result escape the workdir.""" @@ -415,6 +418,7 @@ async def test_glob_post_filters_symlink_escape(tmp_path: Path) -> None: assert res == "no matches" +@needs_symlinks @needs_pydantic_v2 async def test_grep_skips_symlinked_files(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: """The fallback walker must not read through a symlink that escapes the workdir.""" diff --git a/tests/test_client.py b/tests/test_client.py index b6ebf9e4d..61fd0994b 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -434,6 +434,17 @@ def test_validate_headers(self) -> None: request2 = client2._build_request(FinalRequestOptions(method="get", url="/foo", headers={"X-Api-Key": Omit()})) assert request2.headers.get("X-Api-Key") is None + def test_empty_string_env_credentials_treated_as_absent(self) -> None: + # An empty-string env var must not be used as a credential — it would + # produce a malformed "Authorization: Bearer " header rejected by h11. + with mock.patch("anthropic._client.default_credentials", return_value=None): + with update_env(ANTHROPIC_API_KEY="", ANTHROPIC_AUTH_TOKEN=""): + client = Anthropic(base_url=base_url, _strict_response_validation=True) + + assert client.api_key is None + assert client.auth_token is None + assert client.auth_headers == {} + def test_default_query_option(self) -> None: client = Anthropic( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"} @@ -1546,6 +1557,17 @@ def test_validate_headers(self) -> None: request2 = client2._build_request(FinalRequestOptions(method="get", url="/foo", headers={"X-Api-Key": Omit()})) assert request2.headers.get("X-Api-Key") is None + def test_empty_string_env_credentials_treated_as_absent(self) -> None: + # An empty-string env var must not be used as a credential — it would + # produce a malformed "Authorization: Bearer " header rejected by h11. + with mock.patch("anthropic._client.default_credentials", return_value=None): + with update_env(ANTHROPIC_API_KEY="", ANTHROPIC_AUTH_TOKEN=""): + client = AsyncAnthropic(base_url=base_url, _strict_response_validation=True) + + assert client.api_key is None + assert client.auth_token is None + assert client.auth_headers == {} + async def test_default_query_option(self) -> None: client = AsyncAnthropic( base_url=base_url, api_key=api_key, _strict_response_validation=True, default_query={"query_param": "bar"}