From fb7cdb629f07f7db9ad499577b7cbd546fcd29ab Mon Sep 17 00:00:00 2001 From: Sameer Pashikanti <63326129+spashii@users.noreply.github.com> Date: Thu, 2 Jul 2026 14:33:32 +0200 Subject: [PATCH] fix(agent): accept base64-encoded GCP_SA_JSON The echo-next/prod secret stores GCP_SA_JSON base64-encoded, but the agent's settings validator only did json.loads(), crashing the pod on boot (ValidationError: Expecting value). Mirror the server's _coerce_service_account: try JSON, fall back to base64-decode. Latent since the Vertex model swap (#747); surfaced once the import/lock crashes were cleared. Co-Authored-By: Claude Fable 5 --- echo/agent/settings.py | 16 ++++++++++++++-- echo/agent/tests/test_settings.py | 15 +++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) 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()