From 6b4c73c6dffe4e72fe797e7b122ec168910afb9a Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 7 Aug 2026 15:01:59 +0000 Subject: [PATCH] scope Google TTS credentials safely --- tests/tools/test_google_tts_scoped_key.py | 36 +++++++++++++++++++++++ tools/audio/google_tts.py | 22 +++++++++----- 2 files changed, 50 insertions(+), 8 deletions(-) create mode 100644 tests/tools/test_google_tts_scoped_key.py diff --git a/tests/tools/test_google_tts_scoped_key.py b/tests/tools/test_google_tts_scoped_key.py new file mode 100644 index 00000000..fbc15af2 --- /dev/null +++ b/tests/tools/test_google_tts_scoped_key.py @@ -0,0 +1,36 @@ +from tools.audio.google_tts import GoogleTTS +from tools.google_credentials import has_google_credentials + + +def test_tts_only_key_does_not_enable_shared_google_providers(monkeypatch): + monkeypatch.setenv("GOOGLE_TTS_API_KEY", "test-tts-only-key") + monkeypatch.delenv("GOOGLE_API_KEY", raising=False) + monkeypatch.delenv("GEMINI_API_KEY", raising=False) + monkeypatch.delenv("GOOGLE_APPLICATION_CREDENTIALS", raising=False) + + assert GoogleTTS().get_status().value == "available" + assert has_google_credentials() is False + + +def test_tts_key_uses_header_and_is_redacted_from_errors(monkeypatch, tmp_path): + import requests + + secret = "test-production-shaped-tts-key" + monkeypatch.setenv("GOOGLE_TTS_API_KEY", secret) + monkeypatch.delenv("GOOGLE_API_KEY", raising=False) + monkeypatch.delenv("GEMINI_API_KEY", raising=False) + monkeypatch.delenv("GOOGLE_APPLICATION_CREDENTIALS", raising=False) + + def fail_request(url, **kwargs): + assert kwargs["headers"]["x-goog-api-key"] == secret + assert "params" not in kwargs + raise requests.HTTPError(f"403 for {url}?key={secret}") + + monkeypatch.setattr(requests, "post", fail_request) + result = GoogleTTS().execute( + {"text": "safe test sentence", "output_path": str(tmp_path / "speech.mp3")} + ) + + assert result.success is False + assert secret not in result.error + assert "[REDACTED]" in result.error diff --git a/tools/audio/google_tts.py b/tools/audio/google_tts.py index 424e4b30..f47dd4d6 100644 --- a/tools/audio/google_tts.py +++ b/tools/audio/google_tts.py @@ -27,7 +27,6 @@ from tools.base_tool import ( from tools.google_credentials import ( get_access_token, service_account_configured, - has_google_credentials, ) @@ -44,7 +43,8 @@ class GoogleTTS(BaseTool): dependencies = [] install_instructions = ( - "Auth option A — API key: set GOOGLE_API_KEY (or GEMINI_API_KEY) to a\n" + "Auth option A — TTS-only API key: set GOOGLE_TTS_API_KEY.\n" + " GOOGLE_API_KEY or GEMINI_API_KEY remain supported for broader Google setups.\n" " Google Cloud API key with Text-to-Speech enabled.\n" " Enable the API at https://console.cloud.google.com/apis/library/texttospeech.googleapis.com\n" "Auth option B — service account: set GOOGLE_APPLICATION_CREDENTIALS to the\n" @@ -149,12 +149,16 @@ class GoogleTTS(BaseTool): } def _get_api_key(self) -> str | None: - return os.environ.get("GOOGLE_API_KEY") or os.environ.get("GEMINI_API_KEY") + return ( + os.environ.get("GOOGLE_TTS_API_KEY") + or os.environ.get("GOOGLE_API_KEY") + or os.environ.get("GEMINI_API_KEY") + ) def get_status(self) -> ToolStatus: # Available via either an API key or a service-account JSON. Both paths # are honoured by execute() — so this no longer over-reports. - if has_google_credentials(): + if self._get_api_key() or service_account_configured(): return ToolStatus.AVAILABLE return ToolStatus.UNAVAILABLE @@ -204,7 +208,11 @@ class GoogleTTS(BaseTool): try: result = self._generate(inputs, api_key=api_key, bearer_token=bearer_token) except Exception as exc: - return ToolResult(success=False, error=f"Google TTS failed: {exc}") + safe_error = str(exc) + for credential in (api_key, bearer_token): + if credential: + safe_error = safe_error.replace(credential, "[REDACTED]") + return ToolResult(success=False, error=f"Google TTS failed: {safe_error}") result.duration_seconds = round(time.time() - start, 2) result.cost_usd = self.estimate_cost(inputs) @@ -266,16 +274,14 @@ class GoogleTTS(BaseTool): url = f"https://texttospeech.googleapis.com/{api_version}/text:synthesize" headers = {"Content-Type": "application/json"} - params: dict[str, str] = {} if bearer_token: headers["Authorization"] = f"Bearer {bearer_token}" elif api_key: - params["key"] = api_key + headers["x-goog-api-key"] = api_key response = requests.post( url, headers=headers, - params=params, json=payload, timeout=120, )