feat(google): support service-account auth for TTS + Imagen, fix false availability
Google's TTS and Imagen tools advertised service-account auth
(GOOGLE_APPLICATION_CREDENTIALS) but only ever authenticated with an API
key string, so users with a service-account JSON could not use either tool.
google_tts.get_status() also over-reported availability when the JSON was
set, then failed at execute() — a silent-availability bug.
Separately, both hand-rolled _load_dotenv parsers kept inline comments as
values, so after `cp .env.example .env` every keyed tool falsely reported
"available" with no real credentials.
Changes:
- Add tools/google_credentials.py: lazy google-auth Bearer-token helper.
- google_tts: authenticate via Cloud TTS Bearer token when only a service
account is configured; make get_status() honest.
- google_imagen: route service-account auth to Vertex AI
({location}-aiplatform.googleapis.com) with project/location resolution,
alongside the existing AI Studio API-key path.
- Fix both _load_dotenv parsers to strip inline comments (quote-aware).
- Add google-auth to requirements; document the new env vars in .env.example.
- .gitignore: never commit GCP service-account key files.
Verified locally with a real service account: TTS produced a valid MP3 and
Imagen produced a valid 1408x768 PNG via Vertex AI. Existing test suite
passes (2 unrelated pre-existing failures only).
Closes #131
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
"""Shared Google service-account authentication for OpenMontage tools.
|
||||
|
||||
Lets the Google provider tools (``google_tts``, ``google_imagen``)
|
||||
authenticate with a service-account JSON key file via OAuth Bearer tokens —
|
||||
in addition to the existing API-key path. This is what makes
|
||||
``GOOGLE_APPLICATION_CREDENTIALS`` actually work end to end.
|
||||
|
||||
The ``google-auth`` package is imported lazily so this module never adds an
|
||||
import-time cost for tools that only use API keys, and so a missing dependency
|
||||
surfaces as an actionable runtime error rather than a hard import failure.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
# Broad scope that covers Cloud Text-to-Speech and Vertex AI prediction.
|
||||
CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform"
|
||||
|
||||
|
||||
def service_account_configured() -> bool:
|
||||
"""True when GOOGLE_APPLICATION_CREDENTIALS points to an existing file."""
|
||||
path = os.environ.get("GOOGLE_APPLICATION_CREDENTIALS")
|
||||
return bool(path and os.path.exists(path))
|
||||
|
||||
|
||||
def resolve_project_id(creds_project_id: str | None = None) -> str | None:
|
||||
"""Resolve the GCP project id from env vars, falling back to the key file's.
|
||||
|
||||
Vertex AI needs an explicit project id; TTS does not. We prefer an explicit
|
||||
env override so users can target a project other than the key's own.
|
||||
"""
|
||||
return (
|
||||
os.environ.get("GOOGLE_CLOUD_PROJECT")
|
||||
or os.environ.get("GOOGLE_CLOUD_PROJECT_ID")
|
||||
or os.environ.get("GCLOUD_PROJECT")
|
||||
or creds_project_id
|
||||
)
|
||||
|
||||
|
||||
def get_access_token(scopes: list[str] | None = None) -> tuple[str, str | None]:
|
||||
"""Mint an OAuth access token from the service-account JSON.
|
||||
|
||||
Returns ``(access_token, project_id)``. ``project_id`` is the one embedded
|
||||
in the key file (callers should still prefer :func:`resolve_project_id`).
|
||||
|
||||
Raises:
|
||||
RuntimeError: if ``google-auth`` is missing or the credentials cannot
|
||||
be loaded/refreshed — with a message the agent can surface verbatim.
|
||||
"""
|
||||
if scopes is None:
|
||||
scopes = [CLOUD_PLATFORM_SCOPE]
|
||||
|
||||
try:
|
||||
from google.auth.transport.requests import Request
|
||||
from google.oauth2 import service_account
|
||||
except ImportError as exc: # pragma: no cover - depends on optional dep
|
||||
raise RuntimeError(
|
||||
"Service-account auth requires the 'google-auth' package. "
|
||||
"Install it with: pip install google-auth"
|
||||
) from exc
|
||||
|
||||
path = os.environ.get("GOOGLE_APPLICATION_CREDENTIALS")
|
||||
if not path or not os.path.exists(path):
|
||||
raise RuntimeError(
|
||||
"GOOGLE_APPLICATION_CREDENTIALS is not set or points to a missing "
|
||||
"file; cannot use service-account authentication."
|
||||
)
|
||||
|
||||
try:
|
||||
creds = service_account.Credentials.from_service_account_file(
|
||||
path, scopes=scopes
|
||||
)
|
||||
creds.refresh(Request())
|
||||
except Exception as exc: # noqa: BLE001 - re-raised as actionable message
|
||||
raise RuntimeError(
|
||||
f"Failed to load/refresh service-account credentials from {path}: {exc}"
|
||||
) from exc
|
||||
|
||||
return creds.token, getattr(creds, "project_id", None)
|
||||
Reference in New Issue
Block a user