diff --git a/.scratch/distributed-gguf-runtime/evidence/DGR-025/README.md b/.scratch/distributed-gguf-runtime/evidence/DGR-025/README.md index 893c104..bc12063 100644 --- a/.scratch/distributed-gguf-runtime/evidence/DGR-025/README.md +++ b/.scratch/distributed-gguf-runtime/evidence/DGR-025/README.md @@ -1,8 +1,8 @@ # DGR-025 evidence — exact artifact and runtime recipe identity -**Completed:** 2026-07-17 -**Branch:** `ralph/fable-architecture-loop` (Claude Fable architecture lane) -**Authority:** `.scratch/distributed-gguf-runtime/prd.json` +**Status:** in progress — controller gates pass; final independent P0/P1 re-review is pending. +**Branch:** fixed detached Claude Fable provider lane +**Authority:** live Gitea #9; the local PRD is a secondary projection. **Dependencies:** DGR-018 (`evidence/DGR-018/README.md` — canonical backlog schema and issue projection), DGR-021 (`evidence/DGR-021/README.md` — versioned activation envelope). Both read before changing code. @@ -208,3 +208,240 @@ artifact was touched and nothing was written under `/home`. the same way `glm_alpha_artifact` does — read locked manifests, never restate digests — and note `layer_count` must count the routed transformer stack the route tiles, excluding MTP (reserved for beta). + +## Reopened P1 repair — 2026-07-18 + +The earlier evidence above is provenance only. Its stated limitation — that +the identity seam could not attest the executing runtime — was reproduced in +late review, along with the tokenizer-label weakness. This repair replaces +both claims at the production identity boundary. + +### Changed files + +- `packages/node/meshnet_node/runtime_recipe.py` — replaces the moving-ref + denylist with the sole valid `tokenizer.v1:` form, derived from an + ordered map of named tokenizer/config byte digests. A label, tag, branch, or + symbolic ref cannot be a valid identity. +- `packages/tracker/meshnet_tracker/recipe.py` — independent tracker + derivation and validation of the same tokenizer byte identity; it does not + import node code. +- `packages/node/meshnet_node/runtime_pin.py` — adds patched source-tree and + numerically relevant build-recipe digest to the lock-derived runtime pin. +- `packages/node/meshnet_node/native_backend.py` — + `NativeLoadedArtifactReport` now requires an executing-runtime attestation: + runtime/source-tree/patch-stack/build-recipe digests and boundary/protocol + ABI versions. `shard_identity_from_native_report` compares every field to + the lock/build-derived expectation before emitting an identity. +- `scripts/gen_recipe_fingerprint_vectors.py` and + `tests/data/recipe_fingerprint_vectors.json` — regenerate canonical vectors + for the strengthened wire contract. +- `tests/test_runtime_pin_identity.py`, + `tests/test_runtime_recipe_identity.py`, and + `tests/test_native_identity_emission.py` — cover mutable labels including + `origin/main`, `stable`, `release`, a tag, and `HEAD`; independent node and + tracker validation; distinct byte sets under one label; one-byte fingerprint + change; build-recipe change; and each executing-runtime attestation mismatch. + +### Verification + +```bash +PYTHONPATH=packages/node:packages/tracker python3 scripts/gen_recipe_fingerprint_vectors.py +PYTHONPATH=packages/node:packages/tracker python3 -m pytest -q \ + tests/test_runtime_pin_identity.py tests/test_native_identity_emission.py \ + tests/test_runtime_recipe_identity.py +``` +```text +92 passed +``` + +```bash +PYTHONPATH=packages/node:packages/tracker python3 -m pytest -q \ + tests/test_runtime_pin_identity.py tests/test_runtime_recipe_identity.py \ + tests/test_native_identity_emission.py tests/test_node_admission.py \ + tests/test_node_capability.py tests/test_recipe_benchmark.py +``` +```text +188 passed, 1 pre-existing pytest thread warning +``` + +`python3 scripts/ralph_prd_schema.py validate +.scratch/distributed-gguf-runtime/prd.json`, `python3 -m compileall -q packages +tests`, and `git diff --check` each exit 0. The broader PRD pytest projection +suite has two unrelated existing DGR-023 failures: its `passes: true` entry has +no completion notes and its generated issue file is stale. The socket-backed +subset of `test_tracker_capability_admission.py` is additionally un-runnable in +this sandbox (`PermissionError: [Errno 1] Operation not permitted` creating an +AF_INET socket); its deterministic non-socket identity coverage is included in +the passing runs above. + +### Remaining boundary (superseded 2026-07-18, same day — see below) + +The attestation at this point was a native runtime report *contract*: a +Python dataclass the worker was trusted to populate. Late review reproduced +the obvious hole — `load_runtime_pin()` is world-readable, so any operator +could copy the lock's values into the dataclass and pass every comparison. +The section below closes that hole. + +## Executing-artifact evidence binding — 2026-07-18 (this repair) + +The executing native runtime's identity must not be forgeable by copying +repository lock values into a Python self-report. Attestation values are now +accepted only when *extracted from the native artifact itself*, through two +channels that must agree, and the seam fails closed until such native +evidence exists. + +### The boundary + +`meshnet_node.native_backend` now defines the attestation extraction +contract: + +- **Static channel** — the artifact's bytes must embed exactly one + NUL-terminated `MESHNET-RUNTIME-ATTESTATION.v1:` marker. + The canonical payload (`attestation_payload` / + `expected_attestation_payload`) commits to runtime name, upstream commit, + patched tree, ordered patch-stack digest, build-recipe digest, and + boundary/protocol ABI versions; the DGR-027 CMake ABI-marker lane is where + a real native build bakes it in from the lock at configure time. +- **Dynamic channel** — the artifact must actually `dlopen`, and its exported + `llama_meshnet_runtime_attestation` symbol must return byte-identically the + embedded marker. A marker pasted into a plain file is not an executing + runtime. +- **Evidence capability** — `attest_loaded_runtime(artifact_path)` is the + only mint for `NativeArtifactEvidence` (module-private token). The evidence + records the artifact path, a sha256 over the artifact bytes + (`binary_digest`), and a sha256 over the extracted payload + (`payload_digest`). `NativeRuntimeAttestation` requires the evidence and + re-derives the canonical payload from its own field values on + construction: if the digest disagrees, construction fails — so + `dataclasses.replace`-style laundering of a mismatched runtime with copied + lock values also fails. +- `shard_identity_from_native_report` is unchanged downstream: it still + compares every attested field to the lock/build-derived expectation and + the `runtime_version` axis stays lock-derived, so the committed + conformance vectors are unchanged by this repair (regenerated and + byte-stable). + +Fail-closed consequence: in a workspace with no built native artifact (this +one — the DGR-028 patch defect still blocks a native build), no attestation +and therefore no native identity can exist at all. + +### Changed files + +- `packages/node/meshnet_node/native_backend.py` — marker/symbol contract, + canonical payload encoding, `NativeArtifactEvidence` (token-guarded), + evidence-bound `NativeRuntimeAttestation`, `attest_loaded_runtime` + extractor with strict payload parsing (exact key set, types, canonical + re-encoding). +- `tests/test_native_identity_emission.py` — rewritten around real compiled + fixture artifacts: tests build tiny genuine/forged shared objects with + `cc -shared` at test time (skipped cleanly if no C compiler; one is + present here) and prove copied lock values alone cannot pass anywhere. + +### Behavior tests proving copied lock values cannot pass + +- Bare `NativeRuntimeAttestation(**lock_values)` (the pre-repair forgery) is + unconstructible; `evidence=None` and hand-authored/`object()`-token + `NativeArtifactEvidence` each raise. +- The true marker bytes written into a plain file fail (`not a loadable`). +- A loadable artifact with no marker, with conflicting markers, without the + exported symbol, whose symbol disagrees with its marker, or whose payload + is non-canonical (wrong keys, or right keys re-encoded with whitespace) + each fail closed. +- A self-consistent artifact built from the *wrong* values attests, then + fails identity emission per-field (runtime name, upstream commit, patched + tree, patch stack, build recipe, boundary/protocol ABI), and + `dataclasses.replace`-ing it with the lock's true values fails the + evidence binding (`edited after extraction`). +- The genuine path: an artifact embedding + `expected_attestation_payload(load_runtime_pin())` attests, emits the + lock-derived identity, and its evidence `binary_digest` equals the sha256 + of the artifact bytes. + +### Verification (all in this worktree, 2026-07-18) + +```bash +PYTHONPATH=packages/node:packages/tracker python3 -m pytest -q \ + tests/test_native_identity_emission.py tests/test_runtime_pin_identity.py \ + tests/test_runtime_recipe_identity.py +``` +```text +104 passed +``` + +```bash +PYTHONPATH=packages/node:packages/tracker python3 -m pytest -q \ + tests/test_node_admission.py tests/test_node_capability.py \ + tests/test_recipe_benchmark.py +``` +```text +96 passed, 1 pre-existing pytest thread warning +``` + +```bash +PYTHONPATH=packages/node:packages/tracker python3 -m pytest -q \ + tests/test_tracker_capability_admission.py +``` +```text +34 passed # socket-backed subset ran in this session's sandbox +``` + +`PYTHONPATH=packages/node:packages/tracker python3 +scripts/gen_recipe_fingerprint_vectors.py` reproduces the committed vectors +byte-for-byte; `python3 -m compileall -q packages tests` and +`git diff --check` each exit 0. + +A controller full-suite run (`python3 -m pytest -q`) was also executed and is +not represented as green: `13 failed, 1104 passed, 22 skipped, 2 warnings`. +The failures are outside the DGR-025 changed paths: unavailable optional +`zstandard`/`langchain_openai` dependencies, unrelated billing/dynamic-routing/ +tracker expectations, and the already recorded stale DGR-023 local projection. +The exact DGR-025 identity suites and broader admission coverage remain green as +recorded above. + +### Remaining boundary + +What is now proven: no identity can be constructed, registered, admitted, or +certified without evidence extracted from an actual loadable native artifact +that both embeds and reports the attestation, and the extracted values cannot +be edited afterward. What is deliberately not claimed: a cross-compiler +bit-reproducible binary SHA, defense against an adversary who *builds* a +native artifact that embeds lock-true values while lying about its source +(a categorically higher bar than authoring a Python dict), an OS-level swap +of the artifact file between the byte read and the `dlopen` (documented +residual race), or in-process tampering below Python semantics. Real +distributed certification (the registered-but-dark ledger) remains the final +backstop behind this boundary; the DGR-028+ native build lane must embed the +marker via the reserved CMake ABI-marker hook. + +## Executing-byte identity repair — 2026-07-18 controller follow-up + +A later controller review rejected the preceding remaining-boundary claim as +insufficient for DGR-025: a separately built loadable shared object could copy +all public lock values into both marker channels and receive the same +`runtime_version` as a certified artifact. The repair now appends +`+artifact.` to the llama.cpp runtime axis, where the digest is computed +from the exact bytes read by `attest_loaded_runtime`. Node and tracker parsers +independently require this suffix. Consequently, copying lock values into a +different loadable artifact produces a different recipe fingerprint; only the +same artifact bytes can retain the same identity, and every new binary remains +dark until certified. + +`test_copying_public_lock_values_cannot_forge_the_certified_runtime_identity` +builds a second loadable artifact with byte-identical lock attestation but +different executable bytes, and proves both its `runtime_version` and recipe +digest differ from the accepted artifact. Conformance vectors were regenerated +for the strengthened wire identity. + +Controller verification: + +```text +python3 scripts/gen_recipe_fingerprint_vectors.py +python3 -m pytest -q tests/test_native_identity_emission.py \ + tests/test_runtime_pin_identity.py tests/test_runtime_recipe_identity.py +# 105 passed in 0.52s +python3 -m compileall -q packages/node/meshnet_node \ + packages/tracker/meshnet_tracker tests scripts/gen_recipe_fingerprint_vectors.py +# exit 0 +git diff --check +# exit 0 +``` diff --git a/packages/node/meshnet_node/native_backend.py b/packages/node/meshnet_node/native_backend.py index 9bebe74..8b11bcd 100644 --- a/packages/node/meshnet_node/native_backend.py +++ b/packages/node/meshnet_node/native_backend.py @@ -9,10 +9,15 @@ authoritative immutable GGUF artifact pin and must remain identity-free. from __future__ import annotations -from dataclasses import dataclass +import ctypes +import hashlib +import json +import re +from dataclasses import dataclass, field +from pathlib import Path from .native_protocol import BUNDLE_VERSION, SCHEMA_VERSION, pb -from .runtime_pin import load_runtime_pin +from .runtime_pin import DEFAULT_LOCK_DIR, RuntimePin, load_runtime_pin from .runtime_recipe import ( ArtifactIdentity, DerivativeBinding, @@ -23,6 +28,323 @@ from .runtime_recipe import ( handshake_error, ) +_HEX40 = re.compile(r"^[0-9a-f]{40}$") +_HEX64 = re.compile(r"^[0-9a-f]{64}$") + +# The executing-runtime attestation contract. +# +# The repository lock is world-readable, so a Python object holding +# lock-shaped values proves nothing about the runtime that will execute: +# copying `load_runtime_pin()` into a self-report is exactly the forgery +# DGR-025 forbids. Attestation values are therefore accepted only when +# *extracted from the native artifact itself*, through two channels that must +# agree: +# +# 1. static — the artifact's bytes embed exactly one +# ``MESHNET-RUNTIME-ATTESTATION.v1:`` marker (NUL +# terminated). The DGR-027 CMake ABI-marker lane is where the native +# build bakes it in from the lock at configure time. +# 2. dynamic — the artifact must actually dlopen, and its exported +# ``llama_meshnet_runtime_attestation`` symbol must return that same +# marker. A marker pasted into a plain file is not an executing runtime. +# +# What this cannot prove: a cross-compiler bit-reproducible binary SHA, or +# that an adversary did not *build* a native artifact that embeds lock-true +# values while lying about its source. Manufacturing a lying native build is +# a categorically higher bar than authoring a Python dict, and real +# distributed certification (DGR-025's registered-but-dark ledger) remains +# the final backstop behind this boundary. +ATTESTATION_MARKER_PREFIX = b"MESHNET-RUNTIME-ATTESTATION.v1:" +ATTESTATION_SYMBOL = "llama_meshnet_runtime_attestation" + +_ATTESTATION_STR_FIELDS = ( + "runtime_name", + "upstream_commit", + "patched_tree", + "patch_stack_digest", + "build_recipe_digest", +) +_ATTESTATION_INT_FIELDS = ("boundary_schema_version", "protocol_schema_version") + +# Module-private capability: evidence can only be minted where an artifact +# was actually read, scanned, loaded, and queried. +_EVIDENCE_TOKEN = object() + + +def attestation_payload( + *, + runtime_name: str, + upstream_commit: str, + patched_tree: str, + patch_stack_digest: str, + build_recipe_digest: str, + boundary_schema_version: int, + protocol_schema_version: int, +) -> bytes: + """The canonical marker payload for one exact runtime. + + This single encoding is shared by the build lane that embeds the marker, + the extractor that parses it, and the binding check that ties attestation + fields to the extracted evidence — so there is exactly one byte string a + given runtime identity can legitimately embed. + """ + return json.dumps( + { + "runtime_name": runtime_name, + "upstream_commit": upstream_commit, + "patched_tree": patched_tree, + "patch_stack_digest": patch_stack_digest, + "build_recipe_digest": build_recipe_digest, + "boundary_schema_version": boundary_schema_version, + "protocol_schema_version": protocol_schema_version, + }, + sort_keys=True, + separators=(",", ":"), + ensure_ascii=False, + ).encode("utf-8") + + +def expected_attestation_payload( + pin: RuntimePin, + *, + boundary_schema_version: int = BUNDLE_VERSION, + protocol_schema_version: int = int(SCHEMA_VERSION), +) -> bytes: + """The marker payload a native build of this lock workspace must embed.""" + return attestation_payload( + runtime_name=pin.runtime_name, + upstream_commit=pin.upstream_commit, + patched_tree=pin.patched_tree, + patch_stack_digest=pin.patch_stack_digest, + build_recipe_digest=pin.build_recipe_digest, + boundary_schema_version=boundary_schema_version, + protocol_schema_version=protocol_schema_version, + ) + + +@dataclass(frozen=True) +class NativeArtifactEvidence: + """Proof that attestation values came out of a loadable native artifact. + + ``binary_digest`` pins *which* artifact bytes were attested; + ``payload_digest`` pins *what* those bytes attested, and is re-derived + from the attestation's own fields on construction so the values cannot be + edited after extraction (``dataclasses.replace`` laundering fails). + Only :func:`attest_loaded_runtime` can mint this object. + """ + + artifact_path: str + binary_digest: str + payload_digest: str + _token: object = field(default=None, repr=False, compare=False) + + def __post_init__(self) -> None: + if self._token is not _EVIDENCE_TOKEN: + raise RecipeIdentityError( + "native artifact evidence can only be minted by " + "attest_loaded_runtime() from an actually loaded native " + "artifact; it cannot be authored from repository lock values" + ) + if not isinstance(self.artifact_path, str) or not self.artifact_path: + raise RecipeIdentityError("native artifact evidence must name the artifact") + for field_name in ("binary_digest", "payload_digest"): + value = getattr(self, field_name) + if not isinstance(value, str) or not _HEX64.fullmatch(value): + raise RecipeIdentityError( + f"native artifact evidence {field_name!r} must be a 64-hex sha256" + ) + + +@dataclass(frozen=True) +class NativeRuntimeAttestation: + """What the *executing* runtime reports about itself, at load time. + + The repository lock says what the runtime is supposed to be; this says + what the loaded runtime *is* — the source tree it was built from, the + patch stack compiled into it, the numerically relevant build recipe, and + the boundary/protocol schema (ABI) it speaks. The values are never + accepted from a caller: they must arrive bound to + :class:`NativeArtifactEvidence`, which only + :func:`attest_loaded_runtime` can produce by reading, loading, and + querying the native artifact itself. Identity construction then compares + them to the lock/build-derived expectation and refuses on any difference, + so a worker cannot serve a lock it is not actually running — and cannot + fake one by copying the lock into a Python self-report. + + Deliberately *not* attested: a compiler-specific binary SHA. Binding the + recorded build recipe is honest about what the manifest can prove; + bit-reproducible binary attestation is not claimed. + """ + + runtime_name: str + upstream_commit: str + patched_tree: str + patch_stack_digest: str + build_recipe_digest: str + boundary_schema_version: int + protocol_schema_version: int + evidence: NativeArtifactEvidence + + def __post_init__(self) -> None: + if not isinstance(self.evidence, NativeArtifactEvidence): + raise RecipeIdentityError( + "runtime attestation values must be extracted from the loaded " + "native artifact via attest_loaded_runtime(); a Python " + "self-report carrying copied lock values is not an attestation" + ) + if not isinstance(self.runtime_name, str) or not self.runtime_name.strip(): + raise RecipeIdentityError( + "runtime attestation must name the executing runtime" + ) + for field_name, pattern, what in ( + ("upstream_commit", _HEX40, "40-hex upstream commit"), + ("patched_tree", _HEX40, "40-hex patched source tree id"), + ("patch_stack_digest", _HEX64, "64-hex patch-stack digest"), + ("build_recipe_digest", _HEX64, "64-hex build-recipe digest"), + ): + value = getattr(self, field_name) + if not isinstance(value, str) or not pattern.fullmatch(value): + raise RecipeIdentityError( + f"runtime attestation {field_name!r} must be an exact {what}" + ) + for field_name in _ATTESTATION_INT_FIELDS: + value = getattr(self, field_name) + if isinstance(value, bool) or not isinstance(value, int) or value < 1: + raise RecipeIdentityError( + f"runtime attestation {field_name!r} must be a positive integer" + ) + payload = attestation_payload( + **{name: getattr(self, name) for name in _ATTESTATION_STR_FIELDS}, + **{name: getattr(self, name) for name in _ATTESTATION_INT_FIELDS}, + ) + if hashlib.sha256(payload).hexdigest() != self.evidence.payload_digest: + raise RecipeIdentityError( + "runtime attestation fields do not match the attestation " + "extracted from the native artifact; refusing values edited " + "after extraction" + ) + + +def _parse_attestation_payload(payload: bytes) -> dict[str, object]: + """Strictly parse one embedded marker payload, or refuse.""" + try: + doc = json.loads(payload.decode("utf-8")) + except (UnicodeDecodeError, json.JSONDecodeError) as exc: + raise RecipeIdentityError( + f"embedded runtime attestation marker is not valid JSON: {exc}" + ) from exc + expected_keys = set(_ATTESTATION_STR_FIELDS) | set(_ATTESTATION_INT_FIELDS) + if not isinstance(doc, dict) or set(doc) != expected_keys: + raise RecipeIdentityError( + "embedded runtime attestation marker must record exactly the " + "attestation fields" + ) + for name in _ATTESTATION_STR_FIELDS: + if not isinstance(doc[name], str): + raise RecipeIdentityError( + f"embedded runtime attestation field {name!r} must be a string" + ) + for name in _ATTESTATION_INT_FIELDS: + if isinstance(doc[name], bool) or not isinstance(doc[name], int): + raise RecipeIdentityError( + f"embedded runtime attestation field {name!r} must be an integer" + ) + if attestation_payload(**doc) != payload: # type: ignore[arg-type] + raise RecipeIdentityError( + "embedded runtime attestation marker is not in canonical form" + ) + return doc + + +def attest_loaded_runtime(artifact_path: Path | str) -> NativeRuntimeAttestation: + """Extract the executing runtime's attestation from its native artifact. + + Fails closed when the artifact is missing or empty, embeds no attestation + marker (a runtime built without the attestation lane cannot prove what it + is), embeds conflicting markers, is not a loadable shared object, does + not export :data:`ATTESTATION_SYMBOL`, or reports through that symbol + anything other than the embedded marker. + + The returned attestation is bound to the artifact by its byte digest and + to the extracted values by the payload digest. Loading the artifact does + execute its initializers — this is the same artifact the worker is about + to run inference with, so that adds no new execution. An OS-level swap + of the file between the byte read and the dlopen is a documented + residual race; distributed certification remains the final backstop. + """ + path = Path(artifact_path) + try: + data = path.read_bytes() + except FileNotFoundError: + raise RecipeIdentityError( + f"native runtime artifact not found at {path}; without the built " + "native runtime there is no executing identity to attest" + ) from None + except OSError as exc: + raise RecipeIdentityError( + f"native runtime artifact at {path} is unreadable: {exc}" + ) from exc + if not data: + raise RecipeIdentityError(f"native runtime artifact at {path} is empty") + + payloads: list[bytes] = [] + cursor = 0 + while (start := data.find(ATTESTATION_MARKER_PREFIX, cursor)) >= 0: + end = data.find(b"\x00", start) + if end < 0: + raise RecipeIdentityError( + "embedded runtime attestation marker is not NUL-terminated" + ) + payloads.append(data[start + len(ATTESTATION_MARKER_PREFIX) : end]) + cursor = end + if not payloads: + raise RecipeIdentityError( + f"native runtime artifact at {path} embeds no runtime attestation " + "marker; a runtime built without the attestation lane cannot " + "prove what it is" + ) + if len(set(payloads)) != 1: + raise RecipeIdentityError( + f"native runtime artifact at {path} embeds conflicting runtime " + "attestation markers" + ) + payload = payloads[0] + doc = _parse_attestation_payload(payload) + + try: + library = ctypes.CDLL(str(path), mode=ctypes.RTLD_LOCAL) + except OSError as exc: + raise RecipeIdentityError( + f"native runtime artifact at {path} is not a loadable native " + "artifact; an attestation marker copied into a plain file is not " + "an executing runtime" + ) from exc + try: + symbol = getattr(library, ATTESTATION_SYMBOL) + except AttributeError: + raise RecipeIdentityError( + f"native runtime artifact at {path} does not export " + f"{ATTESTATION_SYMBOL}; the loaded runtime itself must report " + "its attestation" + ) from None + symbol.restype = ctypes.c_char_p + symbol.argtypes = [] + reported = symbol() + if reported != ATTESTATION_MARKER_PREFIX + payload: + raise RecipeIdentityError( + f"the runtime loaded from {path} reports a different attestation " + "than its artifact embeds; refusing an artifact that disagrees " + "with itself" + ) + + evidence = NativeArtifactEvidence( + artifact_path=str(path), + binary_digest=hashlib.sha256(data).hexdigest(), + payload_digest=hashlib.sha256(payload).hexdigest(), + _token=_EVIDENCE_TOKEN, + ) + return NativeRuntimeAttestation(evidence=evidence, **doc) # type: ignore[arg-type] + @dataclass(frozen=True) class NativeLoadedArtifactReport: @@ -32,6 +354,10 @@ class NativeLoadedArtifactReport: parsed GGUF metadata while the model is live. Byte counts are operational evidence rather than compatibility axes, but keeping them beside the range prevents a caller from substituting an unverified range declaration. + ``runtime_attestation`` must be the evidence-bound attestation extracted + from the loaded native artifact by :func:`attest_loaded_runtime`; a + report without one cannot be turned into an identity at all, and one + cannot exist without an actual native artifact to extract it from. """ owned_start_layer: int @@ -42,6 +368,7 @@ class NativeLoadedArtifactReport: architecture: str architecture_digest: str layer_count: int + runtime_attestation: NativeRuntimeAttestation def __post_init__(self) -> None: if self.owned_start_layer < 0 or self.owned_end_layer <= self.owned_start_layer: @@ -50,6 +377,10 @@ class NativeLoadedArtifactReport: raise RecipeIdentityError("native report range is outside GGUF layer metadata") if min(self.mapped_bytes, self.resident_bytes, self.registered_bytes) < 0: raise RecipeIdentityError("native report byte counts must be non-negative") + if not isinstance(self.runtime_attestation, NativeRuntimeAttestation): + raise RecipeIdentityError( + "native report must carry the executing runtime's attestation" + ) @dataclass(frozen=True) @@ -82,7 +413,12 @@ class NativeNumericalRecipe: @dataclass(frozen=True) class NativeIdentityInputs: - """Everything a native backend needs to emit one exact identity.""" + """Everything a native backend needs to emit one exact identity. + + ``tokenizer_revision`` must be the content-addressed identity computed by + :func:`meshnet_node.runtime_recipe.tokenizer_identity` over the loaded + tokenizer/config bytes; identity construction rejects anything else. + """ loaded_artifact: NativeLoadedArtifactReport artifact_pin: ImmutableArtifactPin @@ -90,8 +426,72 @@ class NativeIdentityInputs: numerical_recipe: NativeNumericalRecipe -def shard_identity_from_native_report(inputs: NativeIdentityInputs) -> ShardIdentity: - """Derive identity only from the native report and immutable pinned inputs.""" +def _require_attested_runtime( + attested: NativeRuntimeAttestation, + expected: RuntimePin, + recipe: NativeNumericalRecipe, +) -> None: + """Fail closed unless the executing runtime is the locked, built runtime. + + Every comparison is exact and every difference is separately fatal: an + attestation that agrees on the commit but not the patch stack (or the + patched tree, or the build recipe, or the ABI) is a different runtime + wearing the lock's name, and letting it emit the lock's identity is + exactly the substitution DGR-025 exists to prevent. + """ + for what, got, want in ( + ("runtime name", attested.runtime_name, expected.runtime_name), + ("upstream commit", attested.upstream_commit, expected.upstream_commit), + ("patched source tree", attested.patched_tree, expected.patched_tree), + ( + "ordered patch stack", + attested.patch_stack_digest, + expected.patch_stack_digest, + ), + ( + "build recipe", + attested.build_recipe_digest, + expected.build_recipe_digest, + ), + ): + if got != want: + raise RecipeIdentityError( + f"the executing runtime's attested {what} does not match the " + "lock/build-derived expectation; refusing to emit an identity " + "for a runtime this node is not provably running" + ) + for what, got, want in ( + ( + "boundary schema", + attested.boundary_schema_version, + recipe.boundary_schema_version, + ), + ( + "protocol schema", + attested.protocol_schema_version, + recipe.protocol_schema_version, + ), + ): + if got != want: + raise RecipeIdentityError( + f"the executing runtime's attested {what} version ({got}) does " + f"not match the recipe's ({want}); an ABI the runtime does not " + "actually speak cannot be part of its identity" + ) + + +def shard_identity_from_native_report( + inputs: NativeIdentityInputs, + *, + lock_dir: Path = DEFAULT_LOCK_DIR, +) -> ShardIdentity: + """Derive identity only from the native report and immutable pinned inputs. + + The ``runtime_version`` axis is never accepted from a caller: it is derived + from the committed lock workspace, and the loaded runtime's attestation + must match that lock/build-derived expectation exactly — otherwise this + raises and no identity exists to register, admit, or certify. + """ report = inputs.loaded_artifact pin = inputs.artifact_pin recipe = inputs.numerical_recipe @@ -99,7 +499,17 @@ def shard_identity_from_native_report(inputs: NativeIdentityInputs) -> ShardIden raise RecipeIdentityError( "native llama.cpp identity requires backend_id 'llama.cpp' or 'llama-cpp'" ) - runtime_version = load_runtime_pin().runtime_version + runtime_pin = load_runtime_pin(lock_dir) + _require_attested_runtime(report.runtime_attestation, runtime_pin, recipe) + # The lock-derived prefix identifies the intended source/patch/build + # recipe. The executing artifact digest identifies the bytes that actually + # supplied the attestation. Without this suffix, any independently built + # shared object could copy the public lock values into its marker and claim + # the exact same compatibility identity as the certified artifact. + runtime_version = ( + f"{runtime_pin.runtime_version}" + f"+artifact.{report.runtime_attestation.evidence.binary_digest}" + ) artifact = ArtifactIdentity( artifact_id=pin.artifact_id, revision=pin.revision, diff --git a/packages/node/meshnet_node/runtime_pin.py b/packages/node/meshnet_node/runtime_pin.py index 720e84e..48f91da 100644 --- a/packages/node/meshnet_node/runtime_pin.py +++ b/packages/node/meshnet_node/runtime_pin.py @@ -8,13 +8,16 @@ still hash to the same recipe. The DGR-027 lock manifest and the ordered patch stack the native runtime is built from, so the axis value is *derived* from that manifest, never asserted. -The derived value has three load-bearing parts, and each is separately fatal +The derived value has four load-bearing parts, and each is separately fatal to compatibility: the runtime name (from the upstream URL), the exact -40-character upstream commit, and a digest over the ordered patch-stack bytes. -A different upstream pin, a reordered stack, or a single changed patch byte -each produce a different axis value, which produces a different recipe digest, +40-character upstream commit, a digest over the ordered patch-stack bytes, and +a digest over the numerically relevant build recipe. A different upstream pin, +a reordered stack, a single changed patch byte, or a changed build flag each +produce a different axis value, which produces a different recipe digest, which partitions the route — exactly the fail-closed behavior DGR-025 asks -for. +for. The pin also records the patched source tree's git tree id, which the +executing runtime's attestation is compared against +(:mod:`meshnet_node.native_backend`). Every consistency check here fails closed. The manifest keeps three records of the stack — ``UPSTREAM_LOCK.json``'s ``patch_series``, ``patches/series``, and @@ -36,6 +39,7 @@ from pathlib import Path # Domain separation, matching the runtime_recipe digest convention: a patch # stack digest can never be confused with an artifact or recipe digest. PATCH_STACK_DIGEST_DOMAIN = "meshnet.runtime-patch-stack.v1" +BUILD_RECIPE_DIGEST_DOMAIN = "meshnet.runtime-build-recipe.v1" # The UPSTREAM_LOCK.json layout this reader understands (DGR-027 schema). RUNTIME_PIN_SCHEMA_VERSION = 1 @@ -58,14 +62,42 @@ def _canonical_sha256(value: object) -> str: return hashlib.sha256(payload.encode("utf-8")).hexdigest() +def build_recipe_digest(build: object) -> str: + """A digest over the numerically relevant build recipe. + + The lock's ``build`` object records what the runtime is compiled *as* — + the configure flags, standards, and targets that select kernels and + numeric behavior. Two binaries built from one patched tree with different + build recipes can disagree numerically, so the recipe is part of runtime + identity. This is deliberately a digest over the *recorded recipe*, not a + compiler-specific binary SHA: reproducible-binary attestation is not + claimed here. + """ + if not isinstance(build, dict) or not build: + raise RuntimePinError( + "the lock's 'build' section must be a non-empty JSON object; a " + "runtime with an unstated build recipe has an unknowable identity" + ) + return _canonical_sha256( + {"domain": BUILD_RECIPE_DIGEST_DOMAIN, "body": build} + ) + + @dataclass(frozen=True) class RuntimePin: - """One exact runtime: a name, an upstream commit, and an ordered patch stack.""" + """One exact runtime: name, upstream commit, patched tree, patch stack, build. + + ``patched_tree`` is the git tree object id of the source tree *after* the + ordered patch stack is applied — what the runtime was actually compiled + from, as distinct from the upstream commit it started from. + """ runtime_name: str upstream_commit: str + patched_tree: str patch_series: tuple[str, ...] patch_digests: tuple[str, ...] + build_recipe_digest: str @property def patch_stack_digest(self) -> str: @@ -89,10 +121,16 @@ class RuntimePin: @property def runtime_version(self) -> str: - """The exact ``runtime_version`` recipe axis value for this pin.""" + """The exact ``runtime_version`` recipe axis value for this pin. + + Commits to the runtime name, the exact upstream commit, the ordered + patch stack, and the numerically relevant build recipe — each + separately fatal to compatibility. + """ return ( f"{self.runtime_name}@{self.upstream_commit}" f"+patchstack.{self.patch_stack_digest}" + f"+build.{self.build_recipe_digest}" ) @@ -166,6 +204,15 @@ def load_runtime_pin(lock_dir: Path = DEFAULT_LOCK_DIR) -> RuntimePin: "hexadecimal object id; a moving reference is not a pin" ) + patched_tree = lock.get("patched_tree") + if not isinstance(patched_tree, str) or not _HEX40.match(patched_tree): + raise RuntimePinError( + "UPSTREAM_LOCK.json patched_tree must be the exact 40-character git " + "tree object id of the source tree after the patch stack is applied" + ) + + build_digest = build_recipe_digest(lock.get("build")) + commit_file = _read_text(lock_dir / "UPSTREAM_COMMIT", "UPSTREAM_COMMIT") recorded = commit_file.strip().splitlines()[0].strip() if commit_file.strip() else "" if recorded != commit: @@ -216,6 +263,8 @@ def load_runtime_pin(lock_dir: Path = DEFAULT_LOCK_DIR) -> RuntimePin: return RuntimePin( runtime_name=runtime_name, upstream_commit=commit, + patched_tree=patched_tree, patch_series=tuple(lock_series), patch_digests=tuple(digests), + build_recipe_digest=build_digest, ) diff --git a/packages/node/meshnet_node/runtime_recipe.py b/packages/node/meshnet_node/runtime_recipe.py index b5e8ce2..bb9889b 100644 --- a/packages/node/meshnet_node/runtime_recipe.py +++ b/packages/node/meshnet_node/runtime_recipe.py @@ -73,6 +73,7 @@ RECIPE_IDENTITY_SCHEMA_VERSION = 1 ARTIFACT_DIGEST_DOMAIN = "meshnet.model-artifact.v1" RECIPE_DIGEST_DOMAIN = "meshnet.runtime-recipe.v1" SHARD_BINDING_DIGEST_DOMAIN = "meshnet.shard-binding.v1" +TOKENIZER_DIGEST_DOMAIN = "meshnet.tokenizer-identity.v1" # The axes of a runtime recipe. Every one of these changes the numbers a Shard # produces, so every one of them is part of identity and none of them may be @@ -126,12 +127,22 @@ _AXIS_MISMATCH: Mapping[str, str] = { _HEX64 = re.compile(r"^[0-9a-f]{64}$") _LLAMA_CPP_RUNTIME_PIN = re.compile( - r"^llama\.cpp@[0-9a-f]{40}\+patchstack\.[0-9a-f]{64}$" + r"^llama\.cpp@[0-9a-f]{40}\+patchstack\.[0-9a-f]{64}" + r"\+build\.[0-9a-f]{64}\+artifact\.[0-9a-f]{64}$" ) _LLAMA_CPP_BACKEND_IDS = frozenset({"llama.cpp", "llama-cpp"}) -# A revision that can move is not a pin. DGR-017 learned this on the artifact; -# it is just as true of a tokenizer. +# The one shape a tokenizer identity may take: a digest over the tokenizer's +# actual bytes (see `tokenizer_identity`). Any *label* — `origin/main`, +# `stable`, `release`, a tag, a symbolic ref — names a mutable pointer, and a +# denylist of known-mutable names can never enumerate them all. So the check is +# inverted: instead of rejecting labels we recognize as moving, accept only a +# value that could not be a label in the first place. +_TOKENIZER_IDENTITY = re.compile(r"^tokenizer\.v1:[0-9a-f]{64}$") + +# A revision that can move is not a pin. DGR-017 learned this on the artifact. +# Used for diagnosis-only fields (`artifact.revision`); the digested tokenizer +# axis requires the strictly stronger `_TOKENIZER_IDENTITY` form. _MOVING_REFS = frozenset({"main", "master", "head", "latest", "dev", "trunk"}) @@ -204,7 +215,50 @@ def _require_runtime_pin(value: Any, backend_id: Any) -> str: if backend in _LLAMA_CPP_BACKEND_IDS and not _LLAMA_CPP_RUNTIME_PIN.fullmatch(text): raise RecipeIdentityError( "'recipe.runtime_version' for llama.cpp must be " - "'llama.cpp@<40-hex commit>+patchstack.<64-hex digest>'" + "'llama.cpp@<40-hex commit>+patchstack.<64-hex digest>" + "+build.<64-hex digest>+artifact.<64-hex digest>'" + ) + return text + + +def tokenizer_identity(files: Mapping[str, bytes]) -> str: + """The content-addressed identity of a tokenizer: a digest over its bytes. + + `files` maps each numerically relevant tokenizer/config file name — for a + GGUF, the embedded tokenizer metadata blob; for a safetensors deployment, + `tokenizer.json`, `tokenizer_config.json`, `special_tokens_map.json` — to + that file's exact bytes. The identity commits to each name and each byte + set, so two tokenizers published under one label differ, and a one-byte + edit is a different tokenizer. + """ + if not isinstance(files, Mapping) or not files: + raise RecipeIdentityError( + "tokenizer identity requires at least one named tokenizer/config " + "byte set; an identity over nothing pins nothing" + ) + digests: dict[str, str] = {} + for name, body in files.items(): + if not isinstance(name, str) or not name.strip(): + raise RecipeIdentityError( + "tokenizer identity file names must be non-empty strings" + ) + if not isinstance(body, (bytes, bytearray)): + raise RecipeIdentityError( + f"tokenizer identity for {name!r} requires the file's bytes, " + "not a path or label" + ) + digests[name] = hashlib.sha256(bytes(body)).hexdigest() + return "tokenizer.v1:" + _digest(TOKENIZER_DIGEST_DOMAIN, {"files": digests}) + + +def _require_tokenizer_identity(value: Any, what: str) -> str: + text = _require_text(value, what) + if not _TOKENIZER_IDENTITY.fullmatch(text): + raise RecipeIdentityError( + f"{what!r} must be a content-addressed tokenizer identity " + "'tokenizer.v1:<64-hex digest>' derived from the tokenizer's bytes " + "(tokenizer_identity); a repository label, tag, branch, or symbolic " + "ref names a mutable pointer, not the bytes it currently resolves to" ) return text @@ -389,10 +443,13 @@ class RuntimeRecipe: one in fp16, produce different logits from the same bytes. Keeping the axes apart is the entire safety property; see :data:`RECIPE_AXES`. - `tokenizer_revision` and `runtime_version` must be exact pins, never moving - references. For the native runtime the canonical `runtime_version` value — - committing to the exact upstream commit *and* the ordered patch stack — is - derived from the DGR-027 lock manifest by :mod:`meshnet_node.runtime_pin`. + `tokenizer_revision` must be a content-addressed tokenizer identity + (:func:`tokenizer_identity`) — a digest over the tokenizer's actual bytes, + never a repository label that merely points at bytes. `runtime_version` + must be an exact pin; for the native runtime the canonical value — + committing to the exact upstream commit, the ordered patch stack, *and* + the numerically relevant build recipe — is derived from the DGR-027 lock + manifest by :mod:`meshnet_node.runtime_pin`. The three label fields are diagnosis only and are not digested. """ @@ -419,7 +476,7 @@ class RuntimeRecipe: _require_int(value, f"recipe.{axis}", 1) else: _require_text(value, f"recipe.{axis}") - _require_pin(self.tokenizer_revision, "recipe.tokenizer_revision") + _require_tokenizer_identity(self.tokenizer_revision, "recipe.tokenizer_revision") _require_runtime_pin(self.runtime_version, self.backend_id) _require_text(self.recipe_id, "recipe.recipe_id") _require_text(self.recipe_version, "recipe.recipe_version") diff --git a/packages/tracker/meshnet_tracker/recipe.py b/packages/tracker/meshnet_tracker/recipe.py index 50b5cc3..b129b08 100644 --- a/packages/tracker/meshnet_tracker/recipe.py +++ b/packages/tracker/meshnet_tracker/recipe.py @@ -43,6 +43,7 @@ RECIPE_IDENTITY_SCHEMA_VERSION = 1 ARTIFACT_DIGEST_DOMAIN = "meshnet.model-artifact.v1" RECIPE_DIGEST_DOMAIN = "meshnet.runtime-recipe.v1" SHARD_BINDING_DIGEST_DOMAIN = "meshnet.shard-binding.v1" +TOKENIZER_DIGEST_DOMAIN = "meshnet.tokenizer-identity.v1" # The axes a recipe digest commits to. Order is irrelevant (the canonical JSON # sorts keys); membership is not — an axis missing here is an axis the tracker @@ -71,11 +72,18 @@ MIN_CERTIFYING_NODES = 2 _HEX64 = re.compile(r"^[0-9a-f]{64}$") _LLAMA_CPP_RUNTIME_PIN = re.compile( - r"^llama\.cpp@[0-9a-f]{40}\+patchstack\.[0-9a-f]{64}$" + r"^llama\.cpp@[0-9a-f]{40}\+patchstack\.[0-9a-f]{64}" + r"\+build\.[0-9a-f]{64}\+artifact\.[0-9a-f]{64}$" ) _LLAMA_CPP_BACKEND_IDS = frozenset({"llama.cpp", "llama-cpp"}) _MOVING_REFS = frozenset({"main", "master", "head", "latest", "dev", "trunk"}) +# The only admissible tokenizer identity: a digest over the tokenizer's bytes. +# A denylist of moving refs cannot enumerate every mutable label (`origin/main`, +# `stable`, `release`, a re-taggable tag…), so the tracker accepts nothing that +# *could* be a label, independently of the node's identical rule. +_TOKENIZER_IDENTITY = re.compile(r"^tokenizer\.v1:[0-9a-f]{64}$") + class RecipeIdentityError(ValueError): """A presented identity block is malformed or internally inconsistent.""" @@ -137,8 +145,44 @@ def _runtime_pin(value: Any, backend_id: Any) -> str: backend = _text(backend_id, "recipe.backend_id").strip().lower() if backend in _LLAMA_CPP_BACKEND_IDS and not _LLAMA_CPP_RUNTIME_PIN.fullmatch(text): raise RecipeIdentityError( - "'recipe.runtime_version' for llama.cpp must bind a 40-hex commit " - "and a 64-hex ordered patch-stack digest" + "'recipe.runtime_version' for llama.cpp must bind a 40-hex commit, " + "a 64-hex ordered patch-stack digest, a 64-hex build-recipe digest, " + "and the executing native artifact's 64-hex byte digest" + ) + return text + + +def tokenizer_identity(files: Mapping[str, bytes]) -> str: + """Independent tracker derivation of a content-addressed tokenizer identity. + + Deliberately re-implemented (no `meshnet_node` import); the committed + conformance vectors pin the two derivations to each other. + """ + if not isinstance(files, Mapping) or not files: + raise RecipeIdentityError( + "tokenizer identity requires at least one named tokenizer/config byte set" + ) + digests: dict[str, str] = {} + for name, body in files.items(): + if not isinstance(name, str) or not name.strip(): + raise RecipeIdentityError( + "tokenizer identity file names must be non-empty strings" + ) + if not isinstance(body, (bytes, bytearray)): + raise RecipeIdentityError( + f"tokenizer identity for {name!r} requires the file's bytes" + ) + digests[name] = hashlib.sha256(bytes(body)).hexdigest() + return "tokenizer.v1:" + _digest(TOKENIZER_DIGEST_DOMAIN, {"files": digests}) + + +def _tokenizer_identity_value(value: Any, what: str) -> str: + text = _text(value, what) + if not _TOKENIZER_IDENTITY.fullmatch(text): + raise RecipeIdentityError( + f"{what!r} must be a content-addressed 'tokenizer.v1:<64-hex digest>' " + "identity; a label, tag, branch, or symbolic ref is a mutable pointer, " + "not the tokenizer bytes it currently resolves to" ) return text @@ -347,7 +391,7 @@ def parse_identity(data: Any) -> PresentedIdentity: axes[axis] = _integer(value, f"recipe.{axis}", 1) else: axes[axis] = _text(value, f"recipe.{axis}") - _pin(axes["tokenizer_revision"], "recipe.tokenizer_revision") + _tokenizer_identity_value(axes["tokenizer_revision"], "recipe.tokenizer_revision") _runtime_pin(axes["runtime_version"], axes["backend_id"]) identity = PresentedIdentity( diff --git a/scripts/gen_recipe_fingerprint_vectors.py b/scripts/gen_recipe_fingerprint_vectors.py index fcbba17..793b1d9 100644 --- a/scripts/gen_recipe_fingerprint_vectors.py +++ b/scripts/gen_recipe_fingerprint_vectors.py @@ -34,11 +34,25 @@ from meshnet_node.runtime_recipe import ( # noqa: E402 DerivativeBinding, RuntimeRecipe, ShardIdentity, + tokenizer_identity, +) +from meshnet_tracker.recipe import ( # noqa: E402 + parse_identity, + tokenizer_identity as tracker_tokenizer_identity, ) -from meshnet_tracker.recipe import parse_identity # noqa: E402 VECTORS = _ROOT / "tests" / "data" / "recipe_fingerprint_vectors.json" -SCHEMA_VERSION = 1 +SCHEMA_VERSION = 2 + +# The tokenizer axis is content-addressed: a digest over the tokenizer's actual +# bytes, never a repository label. These example bytes are part of the wire +# contract exactly like the digests derived from them. +_TOKENIZER_FILES = { + "tokenizer.json": b'{"version":"example","vocab":{"a":0,"b":1}}\n', + "tokenizer_config.json": b'{"model_max_length":8}\n', +} +_TOKENIZER_IDENTITY = tokenizer_identity(_TOKENIZER_FILES) +assert _TOKENIZER_IDENTITY == tracker_tokenizer_identity(_TOKENIZER_FILES) _RECIPE = RuntimeRecipe( weight_quantization="Q4_K_M", @@ -46,10 +60,13 @@ _RECIPE = RuntimeRecipe( compute_dtype="float32", kv_dtype="q8_0", kv_layout="paged-v1", - tokenizer_revision="0123456789abcdef", + tokenizer_revision=_TOKENIZER_IDENTITY, architecture_adapter="llama/range-v1", backend_id="llama.cpp", - runtime_version="llama.cpp@" + "d" * 40 + "+patchstack." + "e" * 64, + runtime_version=( + "llama.cpp@" + "d" * 40 + "+patchstack." + "e" * 64 + + "+build." + "f" * 64 + "+artifact." + "a" * 64 + ), recipe_id="example-gguf", recipe_version="1", catalogue_version="2026.07.1", diff --git a/tests/data/recipe_fingerprint_vectors.json b/tests/data/recipe_fingerprint_vectors.json index 04d6a84..84f84ed 100644 --- a/tests/data/recipe_fingerprint_vectors.json +++ b/tests/data/recipe_fingerprint_vectors.json @@ -1,5 +1,5 @@ { - "schema_version": 1, + "schema_version": 2, "vectors": [ { "description": "An undivided artifact: content digest is the source digest.", @@ -8,9 +8,9 @@ "model_artifact_digest": "8a0f43d6aa49d77834bdb47bcae9f42c886b7ccfe0ac014932b2a2b38697a47b", "recipe_id": "example-gguf", "recipe_version": "1", - "runtime_recipe_digest": "63001e0efeada5b97f2f3562562dc0fd2d9bd8904dc6b7b99a71d98f3e938bd0" + "runtime_recipe_digest": "73133ee50866da5b26e94a39e3e10865c41651d0b1fd1655ceae09d2234cb8eb" }, - "fingerprint_proto_hex": "0a40386130663433643661613439643737383334626462343762636165396634326338383662376363666530616330313439333262326132623338363937613437621240363330303165306566656164613562393766326633353632353632646330666432643962643839303464633662376239396137316439386633653933386264301a0c6578616d706c652d676775662201312a09323032362e30372e31", + "fingerprint_proto_hex": "0a40386130663433643661613439643737383334626462343762636165396634326338383662376363666530616330313439333262326132623338363937613437621240373331333365653530383636646135623236653934613339653365313038363563343136353164306231666431363535636561653039643232333463623865621a0c6578616d706c652d676775662201312a09323032362e30372e31", "identity": { "artifact": { "architecture": "dense-llama", @@ -26,7 +26,7 @@ "model_artifact_digest": "8a0f43d6aa49d77834bdb47bcae9f42c886b7ccfe0ac014932b2a2b38697a47b", "recipe_id": "example-gguf", "recipe_version": "1", - "runtime_recipe_digest": "63001e0efeada5b97f2f3562562dc0fd2d9bd8904dc6b7b99a71d98f3e938bd0" + "runtime_recipe_digest": "73133ee50866da5b26e94a39e3e10865c41651d0b1fd1655ceae09d2234cb8eb" }, "recipe": { "activation_dtype": "bfloat16", @@ -40,8 +40,8 @@ "protocol_schema_version": 1, "recipe_id": "example-gguf", "recipe_version": "1", - "runtime_version": "llama.cpp@dddddddddddddddddddddddddddddddddddddddd+patchstack.eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", - "tokenizer_revision": "0123456789abcdef", + "runtime_version": "llama.cpp@dddddddddddddddddddddddddddddddddddddddd+patchstack.eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee+build.ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff+artifact.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "tokenizer_revision": "tokenizer.v1:2d4e25eb9dd1a5e6ecef2fb36c0cb49a6cbe61161c807ea07a62ba268e9fb665", "weight_quantization": "Q4_K_M" }, "schema_version": 1, @@ -58,9 +58,9 @@ "model_artifact_digest": "8a0f43d6aa49d77834bdb47bcae9f42c886b7ccfe0ac014932b2a2b38697a47b", "recipe_id": "example-gguf", "recipe_version": "1", - "runtime_recipe_digest": "63001e0efeada5b97f2f3562562dc0fd2d9bd8904dc6b7b99a71d98f3e938bd0" + "runtime_recipe_digest": "73133ee50866da5b26e94a39e3e10865c41651d0b1fd1655ceae09d2234cb8eb" }, - "fingerprint_proto_hex": "0a40386130663433643661613439643737383334626462343762636165396634326338383662376363666530616330313439333262326132623338363937613437621240363330303165306566656164613562393766326633353632353632646330666432643962643839303464633662376239396137316439386633653933386264301a0c6578616d706c652d676775662201312a09323032362e30372e31", + "fingerprint_proto_hex": "0a40386130663433643661613439643737383334626462343762636165396634326338383662376363666530616330313439333262326132623338363937613437621240373331333365653530383636646135623236653934613339653365313038363563343136353164306231666431363535636561653039643232333463623865621a0c6578616d706c652d676775662201312a09323032362e30372e31", "identity": { "artifact": { "architecture": "dense-llama", @@ -80,7 +80,7 @@ "model_artifact_digest": "8a0f43d6aa49d77834bdb47bcae9f42c886b7ccfe0ac014932b2a2b38697a47b", "recipe_id": "example-gguf", "recipe_version": "1", - "runtime_recipe_digest": "63001e0efeada5b97f2f3562562dc0fd2d9bd8904dc6b7b99a71d98f3e938bd0" + "runtime_recipe_digest": "73133ee50866da5b26e94a39e3e10865c41651d0b1fd1655ceae09d2234cb8eb" }, "recipe": { "activation_dtype": "bfloat16", @@ -94,8 +94,8 @@ "protocol_schema_version": 1, "recipe_id": "example-gguf", "recipe_version": "1", - "runtime_version": "llama.cpp@dddddddddddddddddddddddddddddddddddddddd+patchstack.eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", - "tokenizer_revision": "0123456789abcdef", + "runtime_version": "llama.cpp@dddddddddddddddddddddddddddddddddddddddd+patchstack.eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee+build.ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff+artifact.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "tokenizer_revision": "tokenizer.v1:2d4e25eb9dd1a5e6ecef2fb36c0cb49a6cbe61161c807ea07a62ba268e9fb665", "weight_quantization": "Q4_K_M" }, "schema_version": 1, diff --git a/tests/test_native_identity_emission.py b/tests/test_native_identity_emission.py index 903c78a..c603e6f 100644 --- a/tests/test_native_identity_emission.py +++ b/tests/test_native_identity_emission.py @@ -1,30 +1,137 @@ -"""DGR-003 production-native identity emission boundary tests.""" +"""DGR-003 production-native identity emission boundary tests. + +The executing-runtime attestation must be *extracted from the native artifact +itself* — a Python object holding values copied out of the world-readable +repository lock proves nothing and must not pass. These tests compile real +(tiny) shared objects that embed the attestation marker and export the +attestation symbol, then prove the positive path works and that every +lock-copying forgery path fails closed. +""" from __future__ import annotations +import dataclasses +import hashlib +import shutil +import subprocess + import pytest from meshnet_node.doctor import DoctorSelection, validate_loaded_backend from meshnet_node.native_backend import ( + ATTESTATION_MARKER_PREFIX, + ATTESTATION_SYMBOL, ImmutableArtifactPin, + NativeArtifactEvidence, NativeIdentityInputs, NativeLoadedArtifactReport, NativeNumericalRecipe, + NativeRuntimeAttestation, NativeSessionRejected, NativeWorkerBackendAdapter, + attest_loaded_runtime, + attestation_payload, + expected_attestation_payload, shard_identity_from_native_report, ) from meshnet_node.native_protocol import SCHEMA_VERSION, pb -from meshnet_node.runtime_pin import load_runtime_pin from meshnet_node.recipe_manifest import parse_recipe_manifest +from meshnet_node.runtime_pin import load_runtime_pin +from meshnet_node.runtime_recipe import RecipeIdentityError, tokenizer_identity from meshnet_tracker.capability import STATE_UNCERTIFIED, evaluate_report +CC = shutil.which("cc") +requires_cc = pytest.mark.skipif( + CC is None, reason="no C compiler to build a native attestation fixture" +) + def _digest(letter: str) -> str: return letter * 64 -def _inputs(**changes: object) -> NativeIdentityInputs: +def _c_literal(data: bytes) -> str: + # Every byte as \xNN; the next escape's backslash terminates each escape. + return '"' + "".join(f"\\x{b:02x}" for b in data) + '"' + + +def _build_native_artifact( + directory, + payload: bytes | None, + *, + export_symbol: bool = True, + symbol_returns: bytes | None = None, + extra_payloads: tuple[bytes, ...] = (), +): + """Compile a real shared object carrying the requested attestation shape.""" + lines = [] + if payload is not None: + marker = ATTESTATION_MARKER_PREFIX + payload + lines.append( + "__attribute__((used)) const char marker[] = " f"{_c_literal(marker)};" + ) + for index, extra in enumerate(extra_payloads): + lines.append( + f"__attribute__((used)) const char extra{index}[] = " + f"{_c_literal(ATTESTATION_MARKER_PREFIX + extra)};" + ) + if export_symbol: + if symbol_returns is None: + body = "return marker;" + else: + lines.append( + "__attribute__((used)) const char other[] = " + f"{_c_literal(symbol_returns)};" + ) + body = "return other;" + lines.append(f"const char *{ATTESTATION_SYMBOL}(void) {{ {body} }}") + source = directory / "attesting_runtime.c" + source.write_text("\n".join(lines) + "\n", encoding="utf-8") + artifact = directory / "libattesting_runtime.so" + subprocess.run( + [CC, "-shared", "-fPIC", "-O0", "-o", str(artifact), str(source)], + check=True, + capture_output=True, + ) + return artifact + + +def _payload(**overrides: object) -> bytes: + pin = load_runtime_pin() + values: dict[str, object] = { + "runtime_name": pin.runtime_name, + "upstream_commit": pin.upstream_commit, + "patched_tree": pin.patched_tree, + "patch_stack_digest": pin.patch_stack_digest, + "build_recipe_digest": pin.build_recipe_digest, + "boundary_schema_version": 1, + "protocol_schema_version": int(SCHEMA_VERSION), + } + values.update(overrides) + return attestation_payload(**values) # type: ignore[arg-type] + + +def _forged_attestation(directory, **overrides: object) -> NativeRuntimeAttestation: + """A self-consistent native artifact whose embedded values are wrong.""" + return attest_loaded_runtime(_build_native_artifact(directory, _payload(**overrides))) + + +@pytest.fixture(scope="session") +def genuine_artifact(tmp_path_factory): + if CC is None: + pytest.skip("no C compiler to build a native attestation fixture") + directory = tmp_path_factory.mktemp("genuine-runtime") + return _build_native_artifact( + directory, expected_attestation_payload(load_runtime_pin()) + ) + + +@pytest.fixture(scope="session") +def genuine_attestation(genuine_artifact): + return attest_loaded_runtime(genuine_artifact) + + +def _inputs(attestation: NativeRuntimeAttestation, **changes: object) -> NativeIdentityInputs: report = NativeLoadedArtifactReport( owned_start_layer=2, owned_end_layer=6, @@ -34,6 +141,7 @@ def _inputs(**changes: object) -> NativeIdentityInputs: architecture="llama", architecture_digest=_digest("a"), layer_count=8, + runtime_attestation=attestation, ) recipe = NativeNumericalRecipe( weight_quantization="Q4_K_M", @@ -54,13 +162,21 @@ def _inputs(**changes: object) -> NativeIdentityInputs: revision="0123456789abcdef", content_digest=_digest("b"), ), - "tokenizer_revision": "abcdef0123456789", + "tokenizer_revision": tokenizer_identity( + {"tokenizer.json": b'{"vocab":{"a":0}}\n'} + ), "numerical_recipe": recipe, } values.update(changes) return NativeIdentityInputs(**values) # type: ignore[arg-type] +def _report_with(attestation: NativeRuntimeAttestation) -> NativeLoadedArtifactReport: + return NativeLoadedArtifactReport( + 2, 6, 1024, 768, 640, "llama", _digest("a"), 8, attestation + ) + + def _open(adapter: NativeWorkerBackendAdapter, **changes: object) -> pb.SessionOpen: identity = adapter.identity fields: dict[str, object] = { @@ -78,26 +194,217 @@ def _open(adapter: NativeWorkerBackendAdapter, **changes: object) -> pb.SessionO return pb.SessionOpen(**fields) # type: ignore[arg-type] -def test_native_identity_uses_loaded_report_not_a_caller_range(): - identity = shard_identity_from_native_report(_inputs()) +# --- copied lock values alone must never pass ------------------------------ + + +def test_copied_lock_values_cannot_author_an_attestation(): + pin = load_runtime_pin() + values: dict[str, object] = { + "runtime_name": pin.runtime_name, + "upstream_commit": pin.upstream_commit, + "patched_tree": pin.patched_tree, + "patch_stack_digest": pin.patch_stack_digest, + "build_recipe_digest": pin.build_recipe_digest, + "boundary_schema_version": 1, + "protocol_schema_version": int(SCHEMA_VERSION), + } + # The pre-repair forgery: a bare self-report of lock values. + with pytest.raises(TypeError): + NativeRuntimeAttestation(**values) # type: ignore[arg-type] + with pytest.raises(RecipeIdentityError, match="attest_loaded_runtime"): + NativeRuntimeAttestation(evidence=None, **values) # type: ignore[arg-type] + + +def test_native_artifact_evidence_cannot_be_authored_in_python(): + with pytest.raises(RecipeIdentityError, match="attest_loaded_runtime"): + NativeArtifactEvidence("lib.so", _digest("a"), _digest("b")) + with pytest.raises(RecipeIdentityError, match="attest_loaded_runtime"): + NativeArtifactEvidence("lib.so", _digest("a"), _digest("b"), object()) + + +def test_marker_bytes_in_a_plain_file_are_not_an_executing_runtime(tmp_path): + fake = tmp_path / "fake.so" + fake.write_bytes(ATTESTATION_MARKER_PREFIX + _payload() + b"\x00") + with pytest.raises(RecipeIdentityError, match="not a loadable"): + attest_loaded_runtime(fake) + + +def test_missing_native_artifact_fails_closed(tmp_path): + with pytest.raises(RecipeIdentityError, match="not found"): + attest_loaded_runtime(tmp_path / "never-built.so") + + +@requires_cc +def test_artifact_without_a_marker_fails_closed(tmp_path): + artifact = _build_native_artifact(tmp_path, None, symbol_returns=b"no marker") + with pytest.raises(RecipeIdentityError, match="embeds no runtime attestation"): + attest_loaded_runtime(artifact) + + +@requires_cc +def test_artifact_without_the_symbol_fails_closed(tmp_path): + artifact = _build_native_artifact(tmp_path, _payload(), export_symbol=False) + with pytest.raises(RecipeIdentityError, match="does not export"): + attest_loaded_runtime(artifact) + + +@requires_cc +def test_loaded_runtime_disagreeing_with_its_marker_fails_closed(tmp_path): + artifact = _build_native_artifact( + tmp_path, _payload(), symbol_returns=b"not the marker" + ) + with pytest.raises(RecipeIdentityError, match="different attestation"): + attest_loaded_runtime(artifact) + + +@requires_cc +def test_conflicting_markers_fail_closed(tmp_path): + artifact = _build_native_artifact( + tmp_path, _payload(), extra_payloads=(_payload(patched_tree="f" * 40),) + ) + with pytest.raises(RecipeIdentityError, match="conflicting"): + attest_loaded_runtime(artifact) + + +@requires_cc +def test_noncanonical_marker_payload_fails_closed(tmp_path): + (tmp_path / "a").mkdir() + wrong_keys = _build_native_artifact(tmp_path / "a", b'{ "spaced": true }') + with pytest.raises(RecipeIdentityError, match="exactly the attestation fields"): + attest_loaded_runtime(wrong_keys) + # Right keys, non-canonical encoding: the digest binding would be + # ambiguous, so the extractor refuses. + spaced = _payload().replace(b":", b": ").replace(b",", b", ") + (tmp_path / "b").mkdir() + noncanonical = _build_native_artifact(tmp_path / "b", spaced) + with pytest.raises(RecipeIdentityError, match="canonical form"): + attest_loaded_runtime(noncanonical) + + +@requires_cc +def test_lock_values_cannot_launder_a_mismatched_runtime(tmp_path): + # A real (loadable, self-consistent) artifact built from the *wrong* tree + # attests fine — then editing the Python object to the lock's values must + # fail, or evidence extraction would be decorative. + forged = _forged_attestation(tmp_path, patched_tree="f" * 40) + with pytest.raises(RecipeIdentityError, match="edited after extraction"): + dataclasses.replace(forged, patched_tree=load_runtime_pin().patched_tree) + + +def test_evidence_binds_the_attested_artifact_bytes(genuine_artifact, genuine_attestation): + assert genuine_attestation.evidence.binary_digest == hashlib.sha256( + genuine_artifact.read_bytes() + ).hexdigest() + assert genuine_attestation.evidence.artifact_path == str(genuine_artifact) + + +# --- the executing runtime must match the lock, field by field ------------- + + +@pytest.mark.parametrize( + "field,value", + [ + ("runtime_name", "other.cpp"), + ("upstream_commit", "f" * 40), + ("patched_tree", "f" * 40), + ("patch_stack_digest", _digest("f")), + ("build_recipe_digest", _digest("f")), + ("boundary_schema_version", 2), + ("protocol_schema_version", 2), + ], +) +@requires_cc +def test_native_identity_fails_closed_when_executing_runtime_disagrees( + field, value, tmp_path +): + attestation = _forged_attestation(tmp_path, **{field: value}) + with pytest.raises(RecipeIdentityError, match="attested"): + shard_identity_from_native_report( + _inputs(attestation, loaded_artifact=_report_with(attestation)) + ) + + +@requires_cc +def test_distinguishable_runtime_attestations_cannot_emit_one_accepted_identity( + tmp_path, genuine_attestation +): + accepted = shard_identity_from_native_report(_inputs(genuine_attestation)) + forged = _forged_attestation(tmp_path, patched_tree="f" * 40) + with pytest.raises(RecipeIdentityError, match="patched source tree"): + shard_identity_from_native_report( + _inputs(forged, loaded_artifact=_report_with(forged)) + ) + assert accepted.recipe.runtime_version == ( + load_runtime_pin().runtime_version + + "+artifact." + + genuine_attestation.evidence.binary_digest + ) + + +# --- the attested positive path --------------------------------------------- + + +def test_native_identity_uses_loaded_report_not_a_caller_range(genuine_attestation): + identity = shard_identity_from_native_report(_inputs(genuine_attestation)) assert (identity.shard_start, identity.shard_end) == (2, 6) assert identity.artifact.architecture == "llama" assert identity.artifact.layer_count == 8 - assert identity.recipe.runtime_version == load_runtime_pin().runtime_version + assert identity.recipe.runtime_version == ( + load_runtime_pin().runtime_version + + "+artifact." + + genuine_attestation.evidence.binary_digest + ) -def test_native_identity_requires_an_immutable_pin_and_gguf_range(): +@requires_cc +def test_copying_public_lock_values_cannot_forge_the_certified_runtime_identity( + tmp_path, genuine_attestation +): + """A second loadable artifact with lock-true self-report gets a new identity.""" + accepted = shard_identity_from_native_report(_inputs(genuine_attestation)) + copied_artifact = _build_native_artifact( + tmp_path, expected_attestation_payload(load_runtime_pin()) + ) + # Keep the same exported marker/symbol while making this a different set of + # executing artifact bytes, exactly like a separately built binary that + # copied the public lock values into its self-report. + copied_artifact.write_bytes(copied_artifact.read_bytes() + b"copied-lock-forgery") + copied_lock_values = attest_loaded_runtime(copied_artifact) + copied = shard_identity_from_native_report( + _inputs( + copied_lock_values, + loaded_artifact=_report_with(copied_lock_values), + ) + ) + + assert copied_lock_values.evidence.binary_digest != ( + genuine_attestation.evidence.binary_digest + ) + assert copied.recipe.runtime_version != accepted.recipe.runtime_version + assert copied.fingerprint.runtime_recipe_digest != ( + accepted.fingerprint.runtime_recipe_digest + ) + + +def test_native_identity_requires_an_immutable_pin_and_gguf_range(genuine_attestation): with pytest.raises(Exception, match="moving reference"): shard_identity_from_native_report( - _inputs(artifact_pin=ImmutableArtifactPin("a", "main", _digest("b"))) + _inputs( + genuine_attestation, + artifact_pin=ImmutableArtifactPin("a", "main", _digest("b")), + ) ) with pytest.raises(Exception, match="outside GGUF"): - NativeLoadedArtifactReport(0, 9, 1, 1, 1, "llama", _digest("a"), 8) + NativeLoadedArtifactReport( + 0, 9, 1, 1, 1, "llama", _digest("a"), 8, genuine_attestation + ) -def test_native_worker_rejects_bad_session_open_before_session_acceptance(): - adapter = NativeWorkerBackendAdapter(_inputs()) +def test_native_worker_rejects_bad_session_open_before_session_acceptance( + genuine_attestation, +): + adapter = NativeWorkerBackendAdapter(_inputs(genuine_attestation)) accepted = adapter.on_session_open( _open(adapter), expected_route_session_id="tracker-session", expected_route_epoch=4 ) @@ -112,14 +419,16 @@ def test_native_worker_rejects_bad_session_open_before_session_acceptance(): assert rejected.value.error.code == pb.ERROR_CODE_EPOCH_STALE -def test_doctor_emits_native_identity_but_keeps_legacy_backend_dark(): +def test_doctor_emits_native_identity_but_keeps_legacy_backend_dark( + genuine_attestation, +): manifest = parse_recipe_manifest( {"schema_version": 1, "catalogue_version": "2026.07.1", "recipes": [ {"id": "native", "version": "1", "backend_id": "llama-cpp"} ]} ) selection = DoctorSelection("acme/llama.gguf", 2, 5) - native = NativeWorkerBackendAdapter(_inputs()) + native = NativeWorkerBackendAdapter(_inputs(genuine_attestation)) # The probe needs only the normal backend shape; identity is supplied by the adapter. native.hidden_size = 8 native.is_head = False diff --git a/tests/test_runtime_pin_identity.py b/tests/test_runtime_pin_identity.py index f66802f..b3a79b2 100644 --- a/tests/test_runtime_pin_identity.py +++ b/tests/test_runtime_pin_identity.py @@ -21,15 +21,21 @@ from meshnet_node.runtime_pin import ( RuntimePinError, load_runtime_pin, ) -from meshnet_node.runtime_recipe import RecipeIdentityError, RuntimeRecipe +from meshnet_node.runtime_recipe import ( + RecipeIdentityError, + RuntimeRecipe, + tokenizer_identity, +) from meshnet_tracker.recipe import ( RecipeIdentityError as TrackerRecipeIdentityError, parse_identity, + tokenizer_identity as tracker_tokenizer_identity, ) REPO_LOCK_DIR = ( Path(__file__).resolve().parent.parent / "packages" / "node" / "native" / "llama" ) +TOKENIZER = tokenizer_identity({"tokenizer.json": b'{"vocab":{"a":0}}\n'}) def _recipe(**changes: object) -> RuntimeRecipe: @@ -39,10 +45,14 @@ def _recipe(**changes: object) -> RuntimeRecipe: "compute_dtype": "float32", "kv_dtype": "q8_0", "kv_layout": "paged-v1", - "tokenizer_revision": "0123456789abcdef", + "tokenizer_revision": TOKENIZER, "architecture_adapter": "llama/range-v1", "backend_id": "llama.cpp", - "runtime_version": "llama.cpp@" + "d" * 40 + "+patchstack." + "e" * 64, + "runtime_version": ( + "llama.cpp@" + "d" * 40 + "+patchstack." + "e" * 64 + + "+build." + "f" * 64 + + "+artifact." + "a" * 64 + ), "recipe_id": "example-gguf", "recipe_version": "1", "catalogue_version": "2026.07.1", @@ -62,6 +72,8 @@ def _write_fixture_workspace( schema_version: int = 1, upstream: str = "https://github.com/ggml-org/llama.cpp.git", upstream_commit_file: str | None = None, + patched_tree: str = "a" * 40, + build: dict[str, object] | None = None, ) -> Path: """A minimal DGR-027-shaped lock workspace; overrides create disagreement.""" if patches is None: @@ -85,6 +97,8 @@ def _write_fixture_workspace( "schema_version": schema_version, "upstream": upstream, "commit": commit, + "patched_tree": patched_tree, + "build": build or {"configure_flags": ["-DTEST=ON"]}, "patch_series": names if lock_series is None else lock_series, } ), @@ -124,18 +138,19 @@ def test_committed_manifest_derives_a_deterministic_runtime_pin(): ) assert pin.upstream_commit == lock["commit"] assert list(pin.patch_series) == lock["patch_series"] - # The axis value names the runtime, the exact commit, and the stack digest, - # so changing any of the three changes every downstream recipe digest. + # The axis value names the runtime, exact commit, stack, and build recipe. assert pin.runtime_version == ( f"llama.cpp@{lock['commit']}+patchstack.{pin.patch_stack_digest}" + f"+build.{pin.build_recipe_digest}" ) assert len(pin.patch_stack_digest) == 64 def test_derived_axis_value_is_a_valid_recipe_pin(): pin = load_runtime_pin(REPO_LOCK_DIR) - recipe = _recipe(runtime_version=pin.runtime_version) - assert recipe.runtime_version == pin.runtime_version + runtime_version = pin.runtime_version + "+artifact." + "a" * 64 + recipe = _recipe(runtime_version=runtime_version) + assert recipe.runtime_version == runtime_version assert len(recipe.runtime_recipe_digest) == 64 @@ -179,6 +194,16 @@ def test_patch_order_is_part_of_the_stack_identity(tmp_path): assert forward.patch_stack_digest != swapped.patch_stack_digest +def test_build_recipe_change_changes_runtime_identity(tmp_path): + baseline = load_runtime_pin(_write_fixture_workspace(tmp_path / "a")) + changed = load_runtime_pin( + _write_fixture_workspace( + tmp_path / "b", build={"configure_flags": ["-DTEST=OFF"]} + ) + ) + assert baseline.runtime_version != changed.runtime_version + + # --- every manifest disagreement fails closed ------------------------------ @@ -257,6 +282,53 @@ def test_empty_patch_series_requires_empty_series_files(tmp_path): load_runtime_pin(tmp_path) +# --- tokenizer identities are bytes, never labels ------------------------- + + +def _vector_identity() -> dict[str, object]: + vectors = json.loads( + (Path(__file__).parent / "data" / "recipe_fingerprint_vectors.json").read_text( + encoding="utf-8" + ) + ) + return json.loads(json.dumps(vectors["vectors"][0]["identity"])) + + +@pytest.mark.parametrize("moving", ["origin/main", "stable", "release", "v1", "HEAD"]) +def test_node_rejects_every_label_as_a_tokenizer_identity(moving): + with pytest.raises(RecipeIdentityError, match="content-addressed tokenizer"): + _recipe(tokenizer_revision=moving) + + +@pytest.mark.parametrize("moving", ["origin/main", "stable", "release", "v1", "HEAD"]) +def test_tracker_rejects_every_label_as_a_tokenizer_identity(moving): + doc = _vector_identity() + doc["recipe"]["tokenizer_revision"] = moving # type: ignore[index] + doc.pop("fingerprint", None) + with pytest.raises(TrackerRecipeIdentityError, match="content-addressed"): + parse_identity(doc) + + +def test_node_and_tracker_independently_derive_identical_tokenizer_bytes_identity(): + files = { + "tokenizer.json": b'{"vocab":{"a":0}}\n', + "tokenizer_config.json": b'{"bos_token":""}\n', + } + assert tokenizer_identity(files) == tracker_tokenizer_identity(files) + + +def test_tokenizer_bytes_under_one_label_and_one_byte_change_get_new_recipe_fingerprints(): + label = "stable" + first = {"tokenizer.json": b'{"vocab":{"a":0}}\n'} + second = {"tokenizer.json": b'{"vocab":{"a":1}}\n'} + first_id = tokenizer_identity(first) + second_id = tokenizer_identity(second) + assert first_id != second_id, label + assert _recipe(tokenizer_revision=first_id).runtime_recipe_digest != _recipe( + tokenizer_revision=second_id + ).runtime_recipe_digest + + # --- both identity implementations reject a moving runtime ----------------- diff --git a/tests/test_runtime_recipe_identity.py b/tests/test_runtime_recipe_identity.py index fc1abe7..523ae06 100644 --- a/tests/test_runtime_recipe_identity.py +++ b/tests/test_runtime_recipe_identity.py @@ -21,6 +21,7 @@ from meshnet_node.runtime_recipe import ( check_session_open, check_route, handshake_error, + tokenizer_identity, ) from meshnet_tracker.capability import ( POLICY_COMPAT, @@ -49,6 +50,7 @@ from meshnet_tracker.recipe import ( ) VECTORS = Path(__file__).parent / "data" / "recipe_fingerprint_vectors.json" +TOKENIZER = tokenizer_identity({"tokenizer.json": b'{"vocab":{"a":0}}\n'}) def _digest(char: str) -> str: @@ -62,10 +64,14 @@ def _recipe(**changes: object) -> RuntimeRecipe: "compute_dtype": "float32", "kv_dtype": "q8_0", "kv_layout": "paged-v1", - "tokenizer_revision": "0123456789abcdef", + "tokenizer_revision": TOKENIZER, "architecture_adapter": "llama/range-v1", "backend_id": "llama.cpp", - "runtime_version": "llama.cpp@" + "d" * 40 + "+patchstack." + "e" * 64, + "runtime_version": ( + "llama.cpp@" + "d" * 40 + "+patchstack." + "e" * 64 + + "+build." + "f" * 64 + + "+artifact." + "a" * 64 + ), "recipe_id": "example-gguf", "recipe_version": "1", "catalogue_version": "2026.07.1", @@ -238,10 +244,17 @@ def test_committed_vectors_cover_a_whole_model_and_a_derivative_shard(): ("compute_dtype", "float16"), ("kv_dtype", "float16"), ("kv_layout", "contiguous-v2"), - ("tokenizer_revision", "fedcba9876543210"), + ( + "tokenizer_revision", + tokenizer_identity({"tokenizer.json": b'{"vocab":{"b":0}}\n'}), + ), ("architecture_adapter", "llama/range-v2"), ("backend_id", "other-backend"), - ("runtime_version", "llama.cpp@" + "c" * 40 + "+patchstack." + "b" * 64), + ( + "runtime_version", + "llama.cpp@" + "c" * 40 + "+patchstack." + "b" * 64 + + "+build." + "a" * 64 + "+artifact." + "c" * 64, + ), ("boundary_schema_version", 2), ("protocol_schema_version", 2), ],