diff --git a/EvoScientist/image_gen/config.py b/EvoScientist/image_gen/config.py index 98ea282..805356b 100644 --- a/EvoScientist/image_gen/config.py +++ b/EvoScientist/image_gen/config.py @@ -17,7 +17,7 @@ from pathlib import Path from typing import Any, Literal import yaml -from pydantic import BaseModel, Field, field_validator +from pydantic import BaseModel, Field, ValidationError, field_validator IMAGE_GENERATION_SECTION = "image_generation" DEFAULT_IMAGE_GENERATION_TIMEOUT_SECONDS = 120.0 @@ -117,4 +117,18 @@ def load_image_generation_settings( section = data.get(IMAGE_GENERATION_SECTION) if not isinstance(section, dict): return ImageGenerationSettings() - return ImageGenerationSettings.model_validate(section) + try: + return ImageGenerationSettings.model_validate(section) + except ValidationError as exc: + # Never echo input values: a mis-indented yaml can put a literal API + # key under the wrong field and pydantic's default message embeds it + # verbatim. Report only field locations and error types. + from EvoScientist.image_gen.adapters.base import ImageGenError + + details = "; ".join( + f"{'.'.join(str(part) for part in error['loc'])}: {error['type']}" + for error in exc.errors(include_url=False, include_input=False) + ) + raise ImageGenError( + f"invalid {IMAGE_GENERATION_SECTION} section: {details}" + ) from None diff --git a/EvoScientist/model_registry/store.py b/EvoScientist/model_registry/store.py index 43c52e1..b6550f2 100644 --- a/EvoScientist/model_registry/store.py +++ b/EvoScientist/model_registry/store.py @@ -244,16 +244,16 @@ class ModelRuntimeStore: def save_registry( self, *, - expected_revision: int, + expected_revision: int | None, registry: RegistryV4, credential_writes: list[CredentialWrite] | None = None, validate: SaveValidator | None = None, ) -> RegistryV4: """Save the registry inside one ``BEGIN IMMEDIATE`` transaction. - ``expected_revision`` is accepted for API compatibility but ignored: - saves are last-write-wins and the stored revision is always - ``current + 1``. Credential versions are written first (immutable, + ``expected_revision`` (``int`` or ``None``) is accepted for API + compatibility but ignored: saves are last-write-wins and the stored + revision is always ``current + 1``. Credential versions are written first (immutable, with incrementing revisions), then the optional ``validate`` hook runs the section 9.2 save-time checks against the post-write state, then the registry row is replaced. A bootstrap document atomically diff --git a/tests/test_image_gen_config.py b/tests/test_image_gen_config.py index a3e95d2..8344a70 100644 --- a/tests/test_image_gen_config.py +++ b/tests/test_image_gen_config.py @@ -2,6 +2,9 @@ import os +import pytest + +from EvoScientist.image_gen.adapters.base import ImageGenError from EvoScientist.image_gen.config import ( ImageGenerationSettings, ImageModelEntry, @@ -93,6 +96,26 @@ image_generation: assert settings.models[0].resolved_api_key() == "sk-literal" +def test_validation_error_never_echoes_config_values(tmp_path): + """A malformed section must raise a sanitized error: field locations and + error types only, never the offending input values (key-leak red line).""" + path = _write_config( + tmp_path, + """ +image_generation: + models: + - id: gpt-image-2 + provider: sk-secret-value-123 + api_key: sk-secret-value-123 +""", + ) + with pytest.raises(ImageGenError) as excinfo: + load_image_generation_settings(config_path=path) + message = str(excinfo.value) + assert "sk-secret-value-123" not in message + assert "provider" in message + + def test_is_image_generation_model(): assert is_image_generation_model("gpt-image-2") is True assert is_image_generation_model("dall-e-3") is True