fix: attest immutable runtime model versions
This commit is contained in:
@@ -69,9 +69,10 @@ The Go preflight (`internal/gateway/preflight.go`) fails closed unless:
|
||||
|
||||
- `catalog_model_id` — the VoiceStudio TTS engine id (`omnivoice`,
|
||||
`voxcpm2`, …) from `services.tts_backend`'s registry.
|
||||
- `model_version` — the installed Hugging Face revision (40-char commit SHA)
|
||||
recorded by `services.hf_revisions` (curated pin or the
|
||||
`voicestudio-revision` marker).
|
||||
- `model_version` — an immutable catalog version comprising the installed
|
||||
Hugging Face revision (40-char commit SHA) and the first 16 hex characters
|
||||
of the attested snapshot digest. This creates a new catalog identity when
|
||||
snapshot bytes change; it never rewrites an identity retained by a Job.
|
||||
- `model_digest` — `sha256:<hex>` computed over the installed snapshot files
|
||||
(sorted relative path + per-file SHA-256), cached next to the repo cache
|
||||
keyed by (revision, file list, sizes, mtimes) so multi-GB weights are
|
||||
|
||||
@@ -63,6 +63,20 @@ ENGINE_MODEL_REPOS: dict[str, str] = {
|
||||
}
|
||||
|
||||
|
||||
def catalog_model_version(revision: str, model_digest: str) -> str:
|
||||
"""Return the immutable catalog version for an attested model snapshot.
|
||||
|
||||
A Hugging Face revision names source history, not necessarily the exact
|
||||
snapshot bytes installed on a node. The catalog version therefore carries
|
||||
a short, deterministic digest suffix. A changed snapshot becomes a new
|
||||
catalog identity instead of mutating an identity retained by Jobs.
|
||||
"""
|
||||
digest = model_digest.removeprefix("sha256:")
|
||||
if len(revision) != 40 or len(digest) != 64:
|
||||
raise ValueError("model identity requires a SHA revision and SHA-256 digest")
|
||||
return f"{revision}+sha256-{digest[:16]}"
|
||||
|
||||
|
||||
def slots_per_device(default: int = 1) -> int:
|
||||
raw = os.environ.get(SLOTS_ENV, "").strip()
|
||||
try:
|
||||
@@ -233,7 +247,7 @@ class ProductionInventory:
|
||||
return _replace_state(base, STATE_LOADING)
|
||||
return ModelInfo(
|
||||
catalog_model_id=base.catalog_model_id,
|
||||
model_version=base.model_version,
|
||||
model_version=catalog_model_version(base.model_version, model_digest),
|
||||
model_digest=model_digest,
|
||||
precisions=base.precisions,
|
||||
features=base.features,
|
||||
|
||||
@@ -27,6 +27,7 @@ from runtime_adapter.inventory import (
|
||||
STATE_INSTALLED,
|
||||
STATE_LOADING,
|
||||
ModelInfo,
|
||||
catalog_model_version,
|
||||
)
|
||||
from runtime_adapter.selfcheck import PreflightError, run_preflight
|
||||
from runtime_adapter.server import prepare_socket
|
||||
@@ -76,7 +77,7 @@ def test_capabilities_report_device_and_ready_model_evidence(tmp_path):
|
||||
by_id = {model.catalog_model_id: model for model in caps.models}
|
||||
ready = by_id[READY_MODEL.catalog_model_id]
|
||||
assert ready.state == pb2.RUNTIME_MODEL_STATE_READY
|
||||
assert len(ready.model_version) == 40
|
||||
assert ready.model_version.startswith("d" * 40 + "+sha256-")
|
||||
assert ready.model_digest.startswith("sha256:")
|
||||
assert list(ready.precisions)
|
||||
# A loading/failed/installed model is reported truthfully, never READY.
|
||||
@@ -144,6 +145,15 @@ def test_snapshot_digest_is_stable_and_content_sensitive(tmp_path):
|
||||
snapshot_digest(tmp_path / "empty-none")
|
||||
|
||||
|
||||
def test_catalog_model_version_changes_when_attested_snapshot_changes():
|
||||
revision = "d" * 40
|
||||
first = catalog_model_version(revision, "sha256:" + "a" * 64)
|
||||
second = catalog_model_version(revision, "sha256:" + "b" * 64)
|
||||
|
||||
assert first.startswith(revision + "+sha256-")
|
||||
assert first != second
|
||||
|
||||
|
||||
def test_file_sha256_matches_hashlib(tmp_path):
|
||||
import hashlib
|
||||
|
||||
|
||||
Reference in New Issue
Block a user