From f899484e6fc2663f29e1c90b78f2cb27c3093f8d Mon Sep 17 00:00:00 2001 From: Gurney Buchanan Date: Fri, 6 Nov 2026 14:12:57 -0500 Subject: [PATCH 1/2] feat(config): the model-registry provider connection block; 0.63.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sci#287 (source-code-intelligence) routes the runner pool's model delivery through a provider's connection block: the wire it speaks, its endpoint, its harness and where its credential lives. The Go executor parses the block leniently, but the engine's config-completeness audit validates the registry against config/v1/model-registry.schema.json, whose providers carried additionalProperties: false — so a registry declaring a connection failed startup (live on the dev cluster, 2026-11-06: "providers/enmaas: Additional properties are not allowed ('connection' was unexpected)"; a dev-only Containerfile patch worked around it). The block is optional and mirrors the executor's validate() exactly: - type (required): anthropic, anthropic-compatible, gemini, openai, openai-compatible — the wire and credential format, not the model family. Registered as provider_connection_type. - base_url: required on the compatible types (they exist for endpoint overrides), empty on the proprietary types (an empty string reads as absent, as the executor does). - harness: the harness a gateway connection fronts its wire with (omp, claude); empty keeps the default. - credential (required): {source: env|secret, name?, key?} — a reference, never the credential itself; source secret requires the Secret's name and key. Registered as credential_source. The block is routing data only, never credentials: any other key on it is refused. Providers without one load unchanged (backward compat); traust-core/engine pin bumps pick this up. Tests: tests/test_model_registry_connections.py — valid blocks (including the dev cluster's enmaas shape), unknown type, secret credential without name/key, base_url on a proprietary type, and the no-connection backward-compat case. 💘 Generated with Crush Assisted-by: Crush:rits/zai-org/glm-5-3 --- CHANGELOG.md | 30 +++++ VERSION | 2 +- config/v1/model-registry.schema.json | 121 +++++++++++++++++ enums/v1/credential-source.json | 16 +++ enums/v1/provider-connection-type.json | 22 +++ pyproject.toml | 2 +- tests/test_model_registry_connections.py | 165 +++++++++++++++++++++++ uv.lock | 2 +- 8 files changed, 357 insertions(+), 3 deletions(-) create mode 100644 enums/v1/credential-source.json create mode 100644 enums/v1/provider-connection-type.json create mode 100644 tests/test_model_registry_connections.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 48aadbe..4200d7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,36 @@ All notable changes to traust-contracts are documented here. +## [0.63.0] + +### Added + +- **The provider `connection` block in the model registry** + (`config/v1/model-registry.schema.json`). Optional, so existing + registries load unchanged. sci#287 (source-code-intelligence) routes the + runner pool's model delivery through it; the Go executor parses the block + leniently, but the engine's config-completeness audit validates the + registry against this schema, so a registry carrying one failed startup + with `Additional properties are not allowed ('connection' was unexpected)` + (live on the dev cluster, 2026-11-06; a dev-only Containerfile patch + worked around it). The schema mirrors the executor's `validate()`: + - `type` (required): `anthropic`, `anthropic-compatible`, `gemini`, + `openai`, `openai-compatible` — the wire and credential format, not the + model family. Registered as `provider_connection_type`. + - `base_url`: required on the compatible types (they exist for endpoint + overrides), empty on the proprietary types (the default endpoint + serves; an empty string reads as absent, as the executor does). + - `harness`: the harness a gateway connection fronts its wire with + (`omp`, `claude`); empty keeps the default. + - `credential` (required): `{source: env | secret, name?, key?}` — a + reference, never the credential itself. `source: secret` requires the + Secret's `name` and `key`. Registered as `credential_source`. +- The block is routing data only, never credentials: the typed sections + stay permissive (`ProviderSpec` keeps it as an extra field) and the + schema refuses any other key on the block. +- traust-core/engine pin bumps pick this up (a parallel engine PR notes + the dependency). + ## [0.62.3] ### Fixed diff --git a/VERSION b/VERSION index eb324e6..70cd226 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.62.3 +0.63.0 diff --git a/config/v1/model-registry.schema.json b/config/v1/model-registry.schema.json index 7966eba..9a5c21a 100644 --- a/config/v1/model-registry.schema.json +++ b/config/v1/model-registry.schema.json @@ -52,6 +52,127 @@ "type": "string", "minLength": 4 }, + "connection": { + "type": "object", + "description": "Provider routing: the wire this provider speaks, its endpoint, its harness and where its credential lives. Routing data only, never the credentials themselves \u2014 a secret credential names the Secret and the key within it, and the credential reaches the worker on the deployment's existing mechanism. Absent keeps the default routing (the Anthropic wire, the pod's existing credential).", + "required": [ + "type", + "credential" + ], + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "enum": [ + "anthropic", + "anthropic-compatible", + "gemini", + "openai", + "openai-compatible" + ], + "description": "The wire and credential format, not the model family: proprietary endpoints (anthropic, gemini, openai) or compatible gateways speaking an incumbent's wire (anthropic-compatible, openai-compatible). A gateway serving both wires appears as two providers." + }, + "base_url": { + "type": "string", + "description": "The gateway endpoint. Required on the compatible types, which exist for endpoint overrides; empty on the proprietary types, whose default endpoint serves." + }, + "harness": { + "type": "string", + "description": "The harness a gateway connection fronts its wire with (omp or claude); empty keeps the default." + }, + "credential": { + "type": "object", + "description": "Where the provider's credential lives \u2014 a reference, never the credential itself. Source env reads the pod's existing environment; source secret names a Kubernetes Secret and the key within it, both required.", + "required": [ + "source" + ], + "additionalProperties": false, + "properties": { + "source": { + "type": "string", + "enum": [ + "env", + "secret" + ] + }, + "name": { + "type": "string", + "description": "The Secret's name (source secret)." + }, + "key": { + "type": "string", + "description": "The credential's key within the Secret (source secret)." + } + }, + "if": { + "properties": { + "source": { + "const": "secret" + } + }, + "required": [ + "source" + ] + }, + "then": { + "required": [ + "name", + "key" + ] + } + } + }, + "allOf": [ + { + "if": { + "properties": { + "type": { + "enum": [ + "anthropic-compatible", + "openai-compatible" + ] + } + }, + "required": [ + "type" + ] + }, + "then": { + "required": [ + "base_url" + ], + "properties": { + "base_url": { + "minLength": 1 + } + } + } + }, + { + "if": { + "properties": { + "type": { + "enum": [ + "anthropic", + "gemini", + "openai" + ] + } + }, + "required": [ + "type" + ] + }, + "then": { + "properties": { + "base_url": { + "maxLength": 0 + } + } + } + } + ] + }, "models": { "type": "object", "minProperties": 1, diff --git a/enums/v1/credential-source.json b/enums/v1/credential-source.json new file mode 100644 index 0000000..e59ca17 --- /dev/null +++ b/enums/v1/credential-source.json @@ -0,0 +1,16 @@ +{ + "name": "credential_source", + "source_schema": "model-registry.schema.json#/properties/providers/additionalProperties/properties/connection/properties/credential/properties/source", + "description": "Values of `connection.credential.source` in model-registry.schema.json \u2014 where a provider's credential lives.", + "values": [ + "env", + "secret" + ], + "used_in_schemas": [ + "model-registry.schema.json" + ], + "definitions": { + "env": "The pod's existing environment (the model Secret's EnvFrom where the deployment wires one).", + "secret": "A Kubernetes Secret in the Job's namespace; the Secret's name and the key within it are both required." + } +} diff --git a/enums/v1/provider-connection-type.json b/enums/v1/provider-connection-type.json new file mode 100644 index 0000000..93e3a3b --- /dev/null +++ b/enums/v1/provider-connection-type.json @@ -0,0 +1,22 @@ +{ + "name": "provider_connection_type", + "source_schema": "model-registry.schema.json#/properties/providers/additionalProperties/properties/connection/properties/type", + "description": "Values of `connection.type` in model-registry.schema.json \u2014 the wire a registry provider speaks.", + "values": [ + "anthropic", + "anthropic-compatible", + "gemini", + "openai", + "openai-compatible" + ], + "used_in_schemas": [ + "model-registry.schema.json" + ], + "definitions": { + "anthropic": "The proprietary Anthropic endpoint; no base_url (the default endpoint serves).", + "anthropic-compatible": "A gateway speaking the Anthropic wire; requires a base_url endpoint override.", + "gemini": "Google's proprietary API; no base_url (the default endpoint serves).", + "openai": "The proprietary OpenAI endpoint; no base_url (the default endpoint serves).", + "openai-compatible": "A gateway speaking the OpenAI wire; requires a base_url endpoint override." + } +} diff --git a/pyproject.toml b/pyproject.toml index 0c20665..f69aabc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "traust-contracts" -version = "0.62.3" +version = "0.63.0" license = "Apache-2.0" description = "Contract surface for the AI security ledger ecosystem: schemas and shared enums" requires-python = ">=3.11" diff --git a/tests/test_model_registry_connections.py b/tests/test_model_registry_connections.py new file mode 100644 index 0000000..7ebab09 --- /dev/null +++ b/tests/test_model_registry_connections.py @@ -0,0 +1,165 @@ +"""Provider connection blocks in the model registry. + +sci#287 (source-code-intelligence) routes the runner pool's model delivery +through a provider's connection block: the wire it speaks, its endpoint, the +harness fronting it and where its credential lives. The Go executor parses +the block leniently, but the engine's config-completeness audit validates the +registry against this schema, so a registry carrying a connection block +failed startup until the block was declared here. The schema mirrors the +executor's validate() exactly: the type vocabulary, the compatible types' +required endpoint, the proprietary types' refusal of one, and a secret +credential's required Secret name and key. The block is routing data only — +never the credentials themselves. +""" + +from __future__ import annotations + +from pathlib import Path + +import jsonschema +import pytest +import yaml + +from traust_contracts.config import load_section + + +def _registry(connection: dict | None) -> dict: + provider: dict = { + "data_handling": "test", + "models": {"m": {"tier": "haiku-class", "batch_eligible": True}}, + } + if connection is not None: + provider["connection"] = connection + return { + "version": 1, + "updated": "2026-11-06", + "tiers": {"haiku-class": {"description": "h"}}, + "providers": {"enmaas": provider}, + "roles": { + "r": { + "floor": "haiku-class", + "approved": ["m"], + "candidates": [], + "ledger_validity_writer": False, + } + }, + } + + +def _load(tmp_path: Path, connection: dict | None): + (tmp_path / "model-registry.yaml").write_text(yaml.safe_dump(_registry(connection))) + return load_section("model-registry.yaml", config_home=tmp_path) + + +@pytest.mark.parametrize( + "connection", + [ + # The dev cluster's shape (providers/enmaas): an OpenAI-compatible + # gateway fronted by the omp harness, its credential a named Secret. + { + "type": "openai-compatible", + "base_url": "https://api.enmaas.devshift.net", + "harness": "omp", + "credential": { + "source": "secret", + "name": "sci-enmaas-model", + "key": "ANTHROPIC_API_KEY", + }, + }, + { + "type": "anthropic-compatible", + "base_url": "https://gw.example", + "harness": "claude", + "credential": {"source": "env"}, + }, + # A proprietary endpoint: no base_url, the credential already in the + # pod's environment. + {"type": "anthropic", "credential": {"source": "env"}}, + # An empty base_url reads as absent, as the executor's validate() does. + {"type": "anthropic", "base_url": "", "credential": {"source": "env"}}, + # Known vocabulary the worker has no lane for yet: the shape is valid, + # the executor refuses at delivery until the worker grows the lane. + {"type": "gemini", "credential": {"source": "env"}}, + {"type": "openai", "credential": {"source": "env"}}, + ], + ids=[ + "openai-compatible gateway with a secret credential", + "anthropic-compatible gateway with the claude harness", + "proprietary anthropic endpoint", + "empty base_url reads as absent", + "gemini vocabulary", + "openai vocabulary", + ], +) +def test_a_connection_block_validates(tmp_path, connection): + reg = _load(tmp_path, connection) + # The typed section is permissive by design (the schema is the + # authority); the block survives the load for the section's consumers. + assert reg.providers["enmaas"].connection == connection + + +def test_a_provider_without_a_connection_still_loads(tmp_path): + """Registries predating connections are exactly as valid as before.""" + reg = _load(tmp_path, None) + assert reg.all_models()["m"].tier == "haiku-class" + + +@pytest.mark.parametrize( + "connection", + [ + # An unknown type or credential source would silently misroute. + {"type": "vertex-ai", "credential": {"source": "env"}}, + {"type": "anthropic", "credential": {"source": "vault"}}, + # The type names the wire; the credential names where it lives. + {"credential": {"source": "env"}}, + {"type": "anthropic"}, + # The compatible types exist for endpoint overrides: they need one. + {"type": "openai-compatible", "credential": {"source": "env"}}, + {"type": "anthropic-compatible", "base_url": "", "credential": {"source": "env"}}, + # A base_url on a proprietary type contradicts the type: the default + # endpoint serves. + {"type": "anthropic", "base_url": "https://gw.example", "credential": {"source": "env"}}, + {"type": "gemini", "base_url": "https://gw.example", "credential": {"source": "env"}}, + {"type": "openai", "base_url": "https://gw.example", "credential": {"source": "env"}}, + # A secret credential names nothing verifiable without the Secret's + # name and the key within it. + { + "type": "openai-compatible", + "base_url": "https://gw.example", + "credential": {"source": "secret", "name": "sci-enmaas-model"}, + }, + { + "type": "openai-compatible", + "base_url": "https://gw.example", + "credential": {"source": "secret", "key": "ANTHROPIC_API_KEY"}, + }, + { + "type": "openai-compatible", + "base_url": "https://gw.example", + "credential": {"source": "secret"}, + }, + # The block is routing data, never the credentials themselves: an + # inline credential or any other stray key is refused. + {"type": "anthropic", "credential": {"source": "env"}, "api_key": "sk-ant-example"}, + {"type": "anthropic", "credential": {"source": "env"}, "endpoint": "https://gw.example"}, + ], + ids=[ + "unknown connection type", + "unknown credential source", + "missing type", + "missing credential", + "compatible type without a base_url", + "compatible type with an empty base_url", + "base_url on anthropic", + "base_url on gemini", + "base_url on openai", + "secret credential without the key", + "secret credential without the Secret name", + "secret credential without both", + "inline credential", + "unknown connection property", + ], +) +def test_unroutable_connections_are_rejected(tmp_path, connection): + with pytest.raises(jsonschema.ValidationError): + _load(tmp_path, connection) diff --git a/uv.lock b/uv.lock index 1aa4822..56f50cc 100644 --- a/uv.lock +++ b/uv.lock @@ -641,7 +641,7 @@ wheels = [ [[package]] name = "traust-contracts" -version = "0.62.3" +version = "0.63.0" source = { editable = "." } dependencies = [ { name = "jsonschema", extra = ["format-nongpl"] }, From 85257ba1e05e6a73a768ffa44ad9d86ac6188789 Mon Sep 17 00:00:00 2001 From: Gurney Buchanan Date: Fri, 6 Nov 2026 14:51:32 -0500 Subject: [PATCH 2/2] fix(config): refuse empty secret references in provider connections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Qodo's review of the connection block caught a divergence from the executor's validate(): the schema required the secret credential's name and key to be present but not non-empty, so name: "" or key: "" passed the engine's audit while the executor refuses them at dispatch (sci#287's internal/traustcore/model.go: "a %q credential needs the Secret's name and key" — Name == "" || Key == ""). Mirror the Go check exactly: minLength 1 on both, inside the source: secret then-branch — the executor ignores name/key entirely on source: env, so the schema must not grow stricter there (the same conditional idiom the base_url rules use). Two new negative tests cover the empty name and the empty key; 0.63.0 is unreleased, so its CHANGELOG bullet now says non-empty. 💘 Generated with Crush Assisted-by: Crush:rits/zai-org/glm-5-3 --- CHANGELOG.md | 2 +- config/v1/model-registry.schema.json | 10 +++++++++- tests/test_model_registry_connections.py | 15 ++++++++++++++- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4200d7a..147a6f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,7 @@ All notable changes to traust-contracts are documented here. (`omp`, `claude`); empty keeps the default. - `credential` (required): `{source: env | secret, name?, key?}` — a reference, never the credential itself. `source: secret` requires the - Secret's `name` and `key`. Registered as `credential_source`. + Secret's non-empty `name` and `key`. Registered as `credential_source`. - The block is routing data only, never credentials: the typed sections stay permissive (`ProviderSpec` keeps it as an extra field) and the schema refuses any other key on the block. diff --git a/config/v1/model-registry.schema.json b/config/v1/model-registry.schema.json index 9a5c21a..a7bf599 100644 --- a/config/v1/model-registry.schema.json +++ b/config/v1/model-registry.schema.json @@ -118,7 +118,15 @@ "required": [ "name", "key" - ] + ], + "properties": { + "name": { + "minLength": 1 + }, + "key": { + "minLength": 1 + } + } } } }, diff --git a/tests/test_model_registry_connections.py b/tests/test_model_registry_connections.py index 7ebab09..9f9068b 100644 --- a/tests/test_model_registry_connections.py +++ b/tests/test_model_registry_connections.py @@ -122,7 +122,8 @@ def test_a_provider_without_a_connection_still_loads(tmp_path): {"type": "gemini", "base_url": "https://gw.example", "credential": {"source": "env"}}, {"type": "openai", "base_url": "https://gw.example", "credential": {"source": "env"}}, # A secret credential names nothing verifiable without the Secret's - # name and the key within it. + # name and the key within it — and an empty string names nothing + # either, as the executor's validate() refuses. { "type": "openai-compatible", "base_url": "https://gw.example", @@ -138,6 +139,16 @@ def test_a_provider_without_a_connection_still_loads(tmp_path): "base_url": "https://gw.example", "credential": {"source": "secret"}, }, + { + "type": "openai-compatible", + "base_url": "https://gw.example", + "credential": {"source": "secret", "name": "", "key": "ANTHROPIC_API_KEY"}, + }, + { + "type": "openai-compatible", + "base_url": "https://gw.example", + "credential": {"source": "secret", "name": "sci-enmaas-model", "key": ""}, + }, # The block is routing data, never the credentials themselves: an # inline credential or any other stray key is refused. {"type": "anthropic", "credential": {"source": "env"}, "api_key": "sk-ant-example"}, @@ -156,6 +167,8 @@ def test_a_provider_without_a_connection_still_loads(tmp_path): "secret credential without the key", "secret credential without the Secret name", "secret credential without both", + "secret credential with an empty Secret name", + "secret credential with an empty key", "inline credential", "unknown connection property", ],