From 940db565b3f57c987e4365b2d3cc4bfb977a38ed Mon Sep 17 00:00:00 2001 From: m4 Date: Tue, 21 Jul 2026 09:38:37 +0800 Subject: [PATCH] fix(model-registry): close review gaps in save-time validation - add the missing rule-8 counterexample test: declared capabilities exceeding the adapter protocol are rejected with CAPABILITY_UNSUPPORTED_BY_ADAPTER (all eight section 9.2 checks now have at least one negative test) - raise CREDENTIAL_NOT_CONFIGURED explicitly in _check_enabled_model when a required credential reference is null instead of relying on resolve_parameters call ordering --- EvoScientist/model_registry/http_api.py | 15 ++++++++++++- tests/test_model_registry_http.py | 28 +++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/EvoScientist/model_registry/http_api.py b/EvoScientist/model_registry/http_api.py index a8b3fef..cd54372 100644 --- a/EvoScientist/model_registry/http_api.py +++ b/EvoScientist/model_registry/http_api.py @@ -273,7 +273,20 @@ def _check_enabled_model( ) auth_spec = spec.auth_specs[provider.auth.mode] credential_revision = 0 - if auth_spec.credential_required and provider.auth.credential_id is not None: + if auth_spec.credential_required: + if provider.auth.credential_id is None: + # resolve_parameters already rejects this case; raise explicitly + # here so the five-tuple check never depends on call ordering. + raise ModelRegistryError( + CREDENTIAL_NOT_CONFIGURED, + f"Auth mode {provider.auth.mode!r} requires a credential reference.", + details=[ + { + "path": "auth.credential_id", + "code": CREDENTIAL_NOT_CONFIGURED, + } + ], + ) row = connection.execute( "SELECT current_revision FROM credential_pointers WHERE credential_id = ?", (provider.auth.credential_id,), diff --git a/tests/test_model_registry_http.py b/tests/test_model_registry_http.py index 46f1954..bedcdd9 100644 --- a/tests/test_model_registry_http.py +++ b/tests/test_model_registry_http.py @@ -443,6 +443,34 @@ def test_put_registry_rejects_unsupported_auth_mode(active_client): assert response.json()["code"] == "AUTH_MODE_UNSUPPORTED" +def test_put_registry_rejects_capability_unsupported_by_adapter(active_client): + # Rule 8: declared capabilities must not exceed the adapter contract's + # protocol capabilities — the glm-5.2 contract declares vision=False. + registry = _current_registry(active_client)["registry"] + registry["providers"][0]["models"][0]["runtime"]["declared_capabilities"][ + "vision" + ] = True + + response = _put(active_client, registry) + assert response.status_code == 422 + body = response.json() + assert body["code"] == "CAPABILITY_UNSUPPORTED_BY_ADAPTER" + assert "providers[0].models[0]" in body["details"][0]["path"] + + +def test_put_registry_rejects_missing_credential_reference(active_client): + # The api_key AuthSpec requires a credential: credential_id=None must + # fail with CREDENTIAL_NOT_CONFIGURED, not a verification-tuple miss. + registry = _current_registry(active_client)["registry"] + registry["providers"][0]["auth"] = {"mode": "api_key", "credential_id": None} + + response = _put(active_client, registry) + assert response.status_code == 422 + body = response.json() + assert body["code"] == "CREDENTIAL_NOT_CONFIGURED" + assert "auth.credential_id" in body["details"][0]["path"] + + def test_put_registry_rejects_defaults_to_disabled_model(active_client): # With a bootstrap-state payload the schema-level check does not fire, so # the store's rule-7 guard (enforced on every save) is what rejects it.