diff --git a/echo/agent/settings.py b/echo/agent/settings.py index abd0da1d..de73b415 100644 --- a/echo/agent/settings.py +++ b/echo/agent/settings.py @@ -1,3 +1,4 @@ +import base64 import json from functools import lru_cache from typing import Any, Optional @@ -37,13 +38,24 @@ class Settings(BaseSettings): @field_validator("vertex_credentials", "gcp_sa_json", mode="before") @classmethod def _parse_service_account_json(cls, value: Any) -> Optional[dict[str, Any]]: + # Accept a dict, a raw JSON string, or a base64-encoded JSON string. + # The prod/echo-next secret stores GCP_SA_JSON base64-encoded, matching + # the server's _coerce_service_account behavior. if value is None or isinstance(value, dict): return value if isinstance(value, str): stripped = value.strip() - if not stripped: + if stripped in {"", "null", "None"}: return None - return json.loads(stripped) + try: + return json.loads(stripped) + except json.JSONDecodeError: + try: + return json.loads(base64.b64decode(stripped)) + except Exception as exc: + raise ValueError( + "Service account JSON must be valid JSON or base64-encoded JSON" + ) from exc raise ValueError("Expected a JSON object or JSON string") class Config: diff --git a/echo/agent/tests/test_settings.py b/echo/agent/tests/test_settings.py index 2c042444..3de74692 100644 --- a/echo/agent/tests/test_settings.py +++ b/echo/agent/tests/test_settings.py @@ -21,3 +21,18 @@ def test_settings_reads_env(monkeypatch): assert settings.agent_cors_origins == "http://localhost:1111,http://localhost:2222" get_settings.cache_clear() + + +def test_settings_decodes_base64_gcp_sa_json(monkeypatch): + import base64 + import json as _json + + get_settings.cache_clear() + raw = {"type": "service_account", "project_id": "b64-proj"} + b64 = base64.b64encode(_json.dumps(raw).encode()).decode() + monkeypatch.setenv("GCP_SA_JSON", b64) + monkeypatch.setenv("VERTEX_LOCATION", "eu") + + settings = get_settings() + assert settings.gcp_sa_json == raw + get_settings.cache_clear()