From a0298497240498400dbe88a87f1987f2b3c014c5 Mon Sep 17 00:00:00 2001 From: Patrick Buckley Date: Mon, 6 Jul 2026 21:04:12 -0700 Subject: [PATCH] fix(models): keep raw exception text out of client-construction 503s Review: the wrapped ValueError is echoed in 503 bodies, and arbitrary SDK exception text can embed filesystem paths. Echo the exception type only; log the full exception with traceback at the raise site. --- tests/test_model_registry.py | 7 ++++++- turnstone/core/model_registry.py | 14 ++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/tests/test_model_registry.py b/tests/test_model_registry.py index 255a12c1..2ff9c326 100644 --- a/tests/test_model_registry.py +++ b/tests/test_model_registry.py @@ -176,12 +176,17 @@ class TestModelRegistry: with ( patch( "turnstone.core.model_registry.create_client", - side_effect=FileNotFoundError(2, "No such file or directory"), + side_effect=FileNotFoundError(2, "No such file", "/gone/cacert.pem"), ), pytest.raises(ValueError, match="'default'.*FileNotFoundError") as excinfo, ): reg.get_client("default") assert isinstance(excinfo.value.__cause__, FileNotFoundError) + # The message is echoed in 503 bodies: exception TYPE only — the + # raw exception text can embed filesystem paths and must stay in + # the server log. + assert "/gone/cacert.pem" not in str(excinfo.value) + assert "No such file" not in str(excinfo.value) # Nothing half-constructed may be cached — a later call with a # repaired environment must construct for real. assert "default" not in reg._clients diff --git a/turnstone/core/model_registry.py b/turnstone/core/model_registry.py index 3e1fcaf6..ec107deb 100644 --- a/turnstone/core/model_registry.py +++ b/turnstone/core/model_registry.py @@ -159,10 +159,20 @@ class ModelRegistry: # path that a venv rebuild deleted (FileNotFoundError). # Routes map ValueError to a 503 with the message; # anything else surfaces as an opaque 500, so re-type - # here where the alias is known. + # here where the alias is known. The ValueError text is + # echoed to HTTP callers, so it carries only the + # exception TYPE — arbitrary SDK exception text can + # embed filesystem paths; the full detail goes to the + # server log instead. + log.warning( + "Client construction failed for model alias %r (provider %s)", + alias, + cfg.provider, + exc_info=True, + ) raise ValueError( f"failed to construct {cfg.provider} client for model " - f"alias {alias!r}: {type(exc).__name__}: {exc}" + f"alias {alias!r}: {type(exc).__name__} (details in server log)" ) from exc return self._clients[alias]