#pragma once /// TRACES: GR-004 | SR-001 // // Gallery ↔ embedder binding. // // A gallery is only valid for the embedder that built it. Cosine similarities // between embeddings from two different models are meaningless but *look* // plausible — nothing crashes, nothing is obviously wrong, and every number // measured downstream is quietly garbage. So the embedder's identity is stamped // into the gallery at build time and checked by every consumer at load time. // // ── What identifies an embedder ─────────────────────────────────────────────── // Two fields, carried together: // // model_name basename of the model file, e.g. "LVFace-B_Glint360K.onnx" // model_sha256 hex SHA-256 of that file's bytes // // The hash is what *decides*; the name is what a human *reads*. Neither alone is // enough: // // • A name alone is a promise, not a fact. Models get re-exported, re-quantised // and overwritten in place under an unchanged filename — which is precisely // the case where the weights differ and nothing else does. A name-only stamp // is blind to exactly the failure it exists to catch. // • A hash alone is correct but unreadable: "expected 3f2a… got 9c1b…" tells an // operator nothing about what to do next. // // SHA-256 over the file bytes is derived from the artefact rather than asserted // about it, is stable across machines and filesystems, and needs no registry to // be kept up to date. Cost is ~0.1 s for a 250 MB ONNX, paid once per process // (results are memoised on path+mtime+size), which is noise next to model load. // // ── Degraded and legacy cases ───────────────────────────────────────────────── // A TRT-backend deployment may run from a prebuilt .engine with the source .onnx // absent, so the hash cannot be computed. Then the name is compared alone and the // result is reported as a *weak* match — believed, not proven. // // Galleries built before GR-004 carry no stamp at all. They warn loudly rather // than fail, because the state is unknown rather than known-bad, and because // hard-failing every pre-existing gallery would make the check something people // route around rather than trust. Set require_stamp (or SAE_REQUIRE_GALLERY_STAMP=1) // to promote "unknown" to a hard error — that is the mode measurement work runs in. // // A *mismatch* is always fatal, in every mode, with no bypass. #include #include struct EmbedderStamp { std::string model_name; // basename of the model file std::string model_sha256; // lowercase hex SHA-256 of the file's bytes ("" = unavailable) int32_t embed_dim{512}; bool empty() const { return model_name.empty() && model_sha256.empty(); } // "LVFace-B_Glint360K.onnx (sha256 3f2a1c4d…)" — for error messages. std::string describe() const; }; // Identify the model at `model_path`. Missing/unreadable file → name filled from // the path, hash left empty (the weak-match path). Empty path → empty stamp. EmbedderStamp make_embedder_stamp(const std::string& model_path); enum class StampVerdict { match, // hashes agree — binding proven weak_match, // names agree, no hash on one side — believed, unproven unstamped, // gallery predates GR-004 / was written without a stamp unknown_embedder, // gallery is stamped but the loaded embedder can't be identified mismatch, // proven different models — always fatal }; struct StampCheck { StampVerdict verdict{StampVerdict::match}; std::string message; // human-readable, names BOTH sides // A mismatch is fatal unconditionally. The three "cannot prove it" verdicts // are fatal only in strict mode. bool fatal(bool require_stamp) const { return verdict == StampVerdict::mismatch || (require_stamp && verdict != StampVerdict::match); } }; // Pure comparison — no file I/O, no model loading. This is the unit under test. // `gallery_desc`/`embedder_desc` are only used to make the message locatable // (a gallery path, a dump path, "the embedder being loaded", …). StampCheck compare_embedder_stamps(const EmbedderStamp& built_with, const EmbedderStamp& loading_with, const std::string& gallery_desc = "gallery", const std::string& embedder_desc = "embedder"); // Apply the comparison: throw std::runtime_error on a fatal verdict, otherwise // log to stderr. `require_stamp` is OR-ed with SAE_REQUIRE_GALLERY_STAMP. void enforce_embedder_stamp(const EmbedderStamp& built_with, const EmbedderStamp& loading_with, const std::string& gallery_desc, const std::string& embedder_desc, bool require_stamp); // Convenience for the common consumer shape: "I loaded this gallery and I am // about to embed with this model file." Hashes the model, then enforces. struct ActorGallery; void verify_gallery_embedder(const ActorGallery& gallery, const std::string& gallery_path, const std::string& arcface_model_path, bool require_stamp); // SAE_REQUIRE_GALLERY_STAMP=1 → treat an unprovable binding as fatal. bool require_gallery_stamp_from_env(); // Lowercase hex SHA-256. Exposed so a test can pin the digest against the // published vectors, which is what guarantees the C++ and Python (hashlib) // stamps of the same file agree. std::string sha256_hex(const std::string& bytes); std::string sha256_file_hex(const std::string& path); // "" if unreadable