diff --git a/.scratch/distributed-gguf-runtime/evidence/DGR-003/README.md b/.scratch/distributed-gguf-runtime/evidence/DGR-003/README.md index f5b96a7..1449b51 100644 --- a/.scratch/distributed-gguf-runtime/evidence/DGR-003/README.md +++ b/.scratch/distributed-gguf-runtime/evidence/DGR-003/README.md @@ -138,3 +138,49 @@ schema versions, and owned range. At `SessionOpen`, compare its A digest match is not certification. Only tracker-recorded evidence from the same exact fingerprint and a real complete distributed forward can move that recipe out of dark status. + +## Native emission closure — 2026-07-14 + +Status: **done**. DGR-004/DGR-005's native loaded-artifact seam now reaches the +production capability-report path through `NativeWorkerBackendAdapter`. + +### Files changed + +- `packages/node/meshnet_node/native_backend.py` — immutable loaded-GGUF report, + immutable artifact and numerical pins, exact identity derivation, and the + SessionOpen boundary. +- `packages/node/meshnet_node/doctor.py` — includes exact identity only for the + native adapter and derives all matching capability-proof fields from it. +- `tests/test_native_identity_emission.py` — deterministic native report, + immutable-pin, SessionOpen, capability emission, legacy-dark, and + tracker-uncertified tests. +- This issue, `prd.json`, and this evidence directory. + +### Correctness and trust boundary + +The native report carries the end-exclusive owned range, mapped/resident/ +registered bytes, GGUF architecture metadata digest, and layer count. The +adapter constructs `ShardIdentity` only from that report plus immutable artifact +pin, tokenizer revision, and numerical recipe inputs. It does not accept a +caller-supplied shard range. + +`on_session_open()` calls `check_session_open()` before returning +`SessionAccepted`, preserving fingerprint, schema, range, tracker-session, and +epoch fail-closed behavior. The legacy Transformers backend is deliberately not +an adapter and its doctor report remains identity-free. + +The tracker evaluates a self-consistent native report as `uncertified`: digest +equality is canonical consistency, not node authenticity. Only its owned +certification ledger can promote a real distributed forward. + +### Verification + +- Focused/adversarial DGR-003, node/tracker capability, doctor, and native + dependency suites: **171 passed, 1 skipped**. +- Native protocol CMake configure/build plus CTest: **1/1 passed**. +- `compileall`, Ruff, and `git diff --check`: pass. +- Full deterministic suite: **902 passed, 13 skipped** (255.01s). + +No model payload, GPU, external API, network node, or real distributed forward +was run or claimed. The standalone gRPC process remains DGR-008 work; this +story supplies its exact native identity and fail-closed SessionOpen contract. diff --git a/.scratch/distributed-gguf-runtime/evidence/DGR-003/commands.txt b/.scratch/distributed-gguf-runtime/evidence/DGR-003/commands.txt index fbfe2d6..f68fbbb 100644 --- a/.scratch/distributed-gguf-runtime/evidence/DGR-003/commands.txt +++ b/.scratch/distributed-gguf-runtime/evidence/DGR-003/commands.txt @@ -1,5 +1,26 @@ # DGR-003 final verification — 2026-07-14 +# Native emission closure — 2026-07-14 +PYTHONPATH=packages/node:packages/tracker:packages/contracts /run/media/popov/d/DEV/repos/d-popov.com/AI/.venv/bin/python -m pytest -q tests/test_native_identity_emission.py tests/test_runtime_recipe_identity.py tests/test_node_capability.py tests/test_tracker_capability_admission.py tests/test_node_doctor.py tests/test_llama_cpp_dependency.py +# result: 171 passed, 1 skipped in 7.07s + +ruff check packages/node/meshnet_node/native_backend.py packages/node/meshnet_node/doctor.py tests/test_native_identity_emission.py +# result: All checks passed + +git diff --check +# result: pass + +/run/media/popov/d/DEV/repos/d-popov.com/AI/.venv/bin/python -m compileall packages tests +# result: pass + +/run/media/popov/d/DEV/repos/d-popov.com/AI/.venv/bin/cmake -S packages/node/native -B build/dgr-003-native-protocol -DCMAKE_PREFIX_PATH=/tmp/pbsrc/install +/run/media/popov/d/DEV/repos/d-popov.com/AI/.venv/bin/cmake --build build/dgr-003-native-protocol -j2 +/run/media/popov/d/DEV/repos/d-popov.com/AI/.venv/bin/ctest --test-dir build/dgr-003-native-protocol --output-on-failure +# result: configured and built shard_protocol_conformance; 1/1 CTest passed + +/run/media/popov/d/DEV/repos/d-popov.com/AI/.venv/bin/python -m pytest -q +# result: 902 passed, 13 skipped in 255.01s + PYTHONPATH=packages/node:packages/tracker:packages/contracts /run/media/popov/d/DEV/repos/d-popov.com/AI/.venv/bin/python -m pytest -q tests/test_runtime_recipe_identity.py tests/test_node_capability.py tests/test_tracker_capability_admission.py # result: 99 passed in 4.76s diff --git a/.scratch/distributed-gguf-runtime/issues/03-define-exact-artifact-and-runtime-recipe-identity.md b/.scratch/distributed-gguf-runtime/issues/03-define-exact-artifact-and-runtime-recipe-identity.md index 38e5dbb..85a0e48 100644 --- a/.scratch/distributed-gguf-runtime/issues/03-define-exact-artifact-and-runtime-recipe-identity.md +++ b/.scratch/distributed-gguf-runtime/issues/03-define-exact-artifact-and-runtime-recipe-identity.md @@ -1,6 +1,6 @@ # 03 — Define exact Artifact and runtime recipe identity -Status: ready-for-agent +Status: done ## Mandatory fresh-session context @@ -23,7 +23,7 @@ As the Tracker, I need exact compatibility identity so that only numerically and - [x] Separate weight quantization, activation dtype, compute dtype, KV dtype/layout, tokenizer revision, architecture adapter, backend, and runtime version. - [x] Bind derivative or split artifacts to an exact source Model Artifact hash and Shard range. -- [ ] Produce a stable compatibility fingerprint used by live capability emission/admission and the gRPC handshake. The tracker parses, re-derives, admits, partitions, and certifies supplied exact identities, but the current production doctor/backend path does not yet derive one from authoritative loaded-artifact/runtime state. It must stay dark rather than be claimed complete. +- [x] Produce a stable compatibility fingerprint used by live capability emission/admission and the gRPC handshake. The native backend adapter derives it only from its immutable loaded-artifact report and immutable artifact/runtime pins; the legacy Transformers doctor path remains identity-free. - [x] Fail closed on mismatched artifact, tokenizer, architecture, range, boundary schema, activation recipe, or cache layout. - [x] Keep unsupported recipes registered-but-dark until a real distributed forward certifies them. - [x] Targeted pytest tests pass @@ -35,7 +35,7 @@ As the Tracker, I need exact compatibility identity so that only numerically and - [x] Read and verify every dependency evidence README before relying on dependency behavior - [x] Preserve all pre-existing working-tree changes and stage only files belonging to this story - [x] Write .scratch/distributed-gguf-runtime/evidence/DGR-003/README.md with files changed, exact commands and real results, limitations, compatibility notes, and dependent-story handoff -- [ ] Update only this story issue to Status: done after every acceptance criterion and quality gate passes +- [x] Update only this story issue to Status: done after every acceptance criterion and quality gate passes ## Dependency handoff diff --git a/.scratch/distributed-gguf-runtime/prd.json b/.scratch/distributed-gguf-runtime/prd.json index 71e47ad..cd83a95 100644 --- a/.scratch/distributed-gguf-runtime/prd.json +++ b/.scratch/distributed-gguf-runtime/prd.json @@ -80,8 +80,8 @@ "Update only this story issue to Status: done after every acceptance criterion and quality gate passes" ], "priority": 4, - "passes": false, - "notes": "Delayed-review repair: tracker-side exact identity contract is covered by deterministic tests, but live doctor/backend capability emission has no authoritative ShardIdentity construction yet. DGR-003 remains incomplete and exact recipes stay dark; see .scratch/distributed-gguf-runtime/issues/03-define-exact-artifact-and-runtime-recipe-identity.md.", + "passes": true, + "notes": "DGR-003-emission: native loaded-artifact adapter derives exact identity from immutable GGUF/runtime inputs, doctor emits it only for that adapter, and SessionOpen fails closed before acceptance. Exact identities remain tracker-uncertified and dark until a real distributed forward is certified.", "dependsOn": [ "DGR-002", "DGR-017" diff --git a/packages/node/meshnet_node/doctor.py b/packages/node/meshnet_node/doctor.py index 5ceff88..6e25a4f 100644 --- a/packages/node/meshnet_node/doctor.py +++ b/packages/node/meshnet_node/doctor.py @@ -36,6 +36,7 @@ from .capability import ( CapabilityReport, build_capability_report, ) +from .native_backend import NativeWorkerBackendAdapter from .recipe_manifest import ( DEFAULT_RECIPE_ID, Recipe, @@ -449,11 +450,9 @@ def _validate_recipe( category: str | None = None error: BaseException | None = None diagnostics: list[str] = [] - detail: dict = {} - try: backend = load_backend(selection, recipe) - detail = probe_forward(backend) + probe_forward(backend) except DoctorError as exc: category, error = exc.category, exc diagnostics = [str(exc), exc.hint] @@ -464,23 +463,48 @@ def _validate_recipe( duration_ms = int((time.monotonic() - started) * 1000) device = _backend_device(backend, selection) + # Only the native adapter has an authoritative immutable GGUF report and + # deployment pin. The Transformers path deliberately remains dark: a + # model/config fingerprint is not an exact ArtifactIdentity. + identity = backend.identity if isinstance(backend, NativeWorkerBackendAdapter) else None + model_id = selection.model_id if identity is None else identity.artifact.artifact_id + shard_start = selection.shard_start if identity is None else identity.shard_start + shard_end = selection.shard_end if identity is None else identity.shard_end - 1 + recipe_id = recipe.id if identity is None else identity.recipe.recipe_id + recipe_version = recipe.version if identity is None else identity.recipe.recipe_version + catalogue_version = ( + manifest.catalogue_version if identity is None else identity.recipe.catalogue_version + ) + backend_id = recipe.backend_id if identity is None else identity.recipe.backend_id + quantization = ( + selection.quantization if identity is None else identity.recipe.weight_quantization + ) + runtime = _runtime_versions() + model_config = _model_config(backend) + revision = None + if identity is not None: + revision = identity.artifact.revision + model_config = "sha256:" + identity.artifact.architecture_digest + runtime = {**runtime, "native_runtime": identity.recipe.runtime_version} report = build_capability_report( - model_id=selection.model_id, - shard_start=selection.shard_start, - shard_end=selection.shard_end, - recipe_id=recipe.id, - recipe_version=recipe.version, - catalogue_version=manifest.catalogue_version, - backend_id=recipe.backend_id, + model_id=model_id, + shard_start=shard_start, + shard_end=shard_end, + recipe_id=recipe_id, + recipe_version=recipe_version, + catalogue_version=catalogue_version, + backend_id=backend_id, device=device, device_name=_backend_device_name(device), - quantization=selection.quantization, - runtime=_runtime_versions(), - model_config=_model_config(backend), + quantization=quantization, + runtime=runtime, + revision=revision, + model_config=model_config, status=STATUS_FAILED if category else STATUS_PASSED, duration_ms=duration_ms, diagnostics=[d for d in diagnostics if d] or None, validated_at=clock(), + identity=identity, ) if category: return RecipeResult( diff --git a/packages/node/meshnet_node/native_backend.py b/packages/node/meshnet_node/native_backend.py new file mode 100644 index 0000000..33c6841 --- /dev/null +++ b/packages/node/meshnet_node/native_backend.py @@ -0,0 +1,185 @@ +"""Authoritative identity boundary for a native GGUF Shard backend. + +The native loader owns the facts about the mapped artifact. This module turns +that immutable report and separately pinned deployment inputs into the one +``ShardIdentity`` DGR-003 permits a native worker to emit. It is intentionally +not an adapter for the legacy Transformers backend: that backend has no +authoritative immutable GGUF artifact pin and must remain identity-free. +""" + +from __future__ import annotations + +from dataclasses import dataclass + +from .native_protocol import BUNDLE_VERSION, SCHEMA_VERSION, pb +from .runtime_recipe import ( + ArtifactIdentity, + DerivativeBinding, + RecipeIdentityError, + RuntimeRecipe, + ShardIdentity, + check_session_open, + handshake_error, +) + + +@dataclass(frozen=True) +class NativeLoadedArtifactReport: + """Immutable GGUF facts returned by the loaded native model. + + The report is copied from ``llama_model_meshnet_range_report`` plus the + 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. + """ + + owned_start_layer: int + owned_end_layer: int + mapped_bytes: int + resident_bytes: int + registered_bytes: int + architecture: str + architecture_digest: str + layer_count: int + + def __post_init__(self) -> None: + if self.owned_start_layer < 0 or self.owned_end_layer <= self.owned_start_layer: + raise RecipeIdentityError("native report has an invalid owned layer range") + if self.layer_count < 1 or self.owned_end_layer > self.layer_count: + 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") + + +@dataclass(frozen=True) +class ImmutableArtifactPin: + """Deployment-supplied immutable bytes pin, never inferred from a model name.""" + + artifact_id: str + revision: str + content_digest: str + derived_from: DerivativeBinding | None = None + + +@dataclass(frozen=True) +class NativeNumericalRecipe: + """Immutable numerical inputs selected for this native worker instance.""" + + weight_quantization: str + activation_dtype: str + compute_dtype: str + kv_dtype: str + kv_layout: str + architecture_adapter: str + backend_id: str + runtime_version: str + recipe_id: str + recipe_version: str + catalogue_version: str + boundary_schema_version: int = BUNDLE_VERSION + protocol_schema_version: int = int(SCHEMA_VERSION) + + +@dataclass(frozen=True) +class NativeIdentityInputs: + """Everything a native backend needs to emit one exact identity.""" + + loaded_artifact: NativeLoadedArtifactReport + artifact_pin: ImmutableArtifactPin + tokenizer_revision: str + numerical_recipe: NativeNumericalRecipe + + +def shard_identity_from_native_report(inputs: NativeIdentityInputs) -> ShardIdentity: + """Derive identity only from the native report and immutable pinned inputs.""" + report = inputs.loaded_artifact + pin = inputs.artifact_pin + recipe = inputs.numerical_recipe + artifact = ArtifactIdentity( + artifact_id=pin.artifact_id, + revision=pin.revision, + content_digest=pin.content_digest, + architecture=report.architecture, + architecture_digest=report.architecture_digest, + layer_count=report.layer_count, + derived_from=pin.derived_from, + ) + return ShardIdentity( + artifact=artifact, + recipe=RuntimeRecipe( + weight_quantization=recipe.weight_quantization, + activation_dtype=recipe.activation_dtype, + compute_dtype=recipe.compute_dtype, + kv_dtype=recipe.kv_dtype, + kv_layout=recipe.kv_layout, + tokenizer_revision=inputs.tokenizer_revision, + architecture_adapter=recipe.architecture_adapter, + backend_id=recipe.backend_id, + runtime_version=recipe.runtime_version, + boundary_schema_version=recipe.boundary_schema_version, + protocol_schema_version=recipe.protocol_schema_version, + recipe_id=recipe.recipe_id, + recipe_version=recipe.recipe_version, + catalogue_version=recipe.catalogue_version, + ), + shard_start=report.owned_start_layer, + shard_end=report.owned_end_layer, + ) + + +class NativeSessionRejected(RecipeIdentityError): + """A native worker refused a ``SessionOpen`` before allocating session state.""" + + def __init__(self, error: "pb.ShardError") -> None: + super().__init__(f"native SessionOpen rejected: {error.code}") + self.error = error + + +class NativeWorkerBackendAdapter: + """Small backend-facing adapter around the native loaded-artifact seam.""" + + def __init__(self, identity_inputs: NativeIdentityInputs) -> None: + self.identity_inputs = identity_inputs + self.identity = shard_identity_from_native_report(identity_inputs) + + @property + def loaded_artifact_report(self) -> NativeLoadedArtifactReport: + return self.identity_inputs.loaded_artifact + + def check_session_open( + self, + opened: "pb.SessionOpen", + *, + expected_route_session_id: str | None = None, + expected_route_epoch: int | None = None, + ) -> None: + """Reject incompatible/stale opens at the native worker boundary.""" + mismatches = check_session_open( + self.identity, + opened, + expected_route_session_id=expected_route_session_id, + expected_route_epoch=expected_route_epoch, + ) + error = handshake_error(mismatches) + if error is not None: + raise NativeSessionRejected(error) + + def on_session_open( + self, + opened: "pb.SessionOpen", + *, + expected_route_session_id: str | None = None, + expected_route_epoch: int | None = None, + ) -> "pb.SessionAccepted": + """The native worker's SessionOpen boundary, before session allocation.""" + self.check_session_open( + opened, + expected_route_session_id=expected_route_session_id, + expected_route_epoch=expected_route_epoch, + ) + return pb.SessionAccepted( + schema_version=SCHEMA_VERSION, + route_session_id=opened.route_session_id, + route_epoch=opened.route_epoch, + fingerprint=self.identity.fingerprint.to_proto(), + ) diff --git a/tests/test_native_identity_emission.py b/tests/test_native_identity_emission.py new file mode 100644 index 0000000..5cc19e9 --- /dev/null +++ b/tests/test_native_identity_emission.py @@ -0,0 +1,157 @@ +"""DGR-003 production-native identity emission boundary tests.""" + +from __future__ import annotations + +import pytest + +from meshnet_node.doctor import DoctorSelection, validate_loaded_backend +from meshnet_node.native_backend import ( + ImmutableArtifactPin, + NativeIdentityInputs, + NativeLoadedArtifactReport, + NativeNumericalRecipe, + NativeSessionRejected, + NativeWorkerBackendAdapter, + shard_identity_from_native_report, +) +from meshnet_node.native_protocol import SCHEMA_VERSION, pb +from meshnet_node.recipe_manifest import parse_recipe_manifest +from meshnet_tracker.capability import STATE_UNCERTIFIED, evaluate_report + + +def _digest(letter: str) -> str: + return letter * 64 + + +def _inputs(**changes: object) -> NativeIdentityInputs: + report = NativeLoadedArtifactReport( + owned_start_layer=2, + owned_end_layer=6, + mapped_bytes=1024, + resident_bytes=768, + registered_bytes=640, + architecture="llama", + architecture_digest=_digest("a"), + layer_count=8, + ) + recipe = NativeNumericalRecipe( + weight_quantization="Q4_K_M", + activation_dtype="bfloat16", + compute_dtype="float32", + kv_dtype="q8_0", + kv_layout="llama-kv-v1", + architecture_adapter="dense-llama-v1", + backend_id="llama-cpp", + runtime_version="llama.cpp:e920c523", + recipe_id="native", + recipe_version="1", + catalogue_version="2026.07.1", + ) + values: dict[str, object] = { + "loaded_artifact": report, + "artifact_pin": ImmutableArtifactPin( + artifact_id="acme/llama.gguf", + revision="0123456789abcdef", + content_digest=_digest("b"), + ), + "tokenizer_revision": "abcdef0123456789", + "numerical_recipe": recipe, + } + values.update(changes) + return NativeIdentityInputs(**values) # type: ignore[arg-type] + + +def _open(adapter: NativeWorkerBackendAdapter, **changes: object) -> pb.SessionOpen: + identity = adapter.identity + fields: dict[str, object] = { + "schema_version": SCHEMA_VERSION, + "route_session_id": "tracker-session", + "route_epoch": 4, + "fingerprint": identity.fingerprint.to_proto(), + "shard_range": pb.ShardRange( + start_layer=identity.shard_start, + end_layer=identity.shard_end, + effective_start_layer=identity.shard_start, + ), + } + fields.update(changes) + 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()) + + assert (identity.shard_start, identity.shard_end) == (2, 6) + assert identity.artifact.architecture == "llama" + assert identity.artifact.layer_count == 8 + + +def test_native_identity_requires_an_immutable_pin_and_gguf_range(): + with pytest.raises(Exception, match="moving reference"): + shard_identity_from_native_report( + _inputs(artifact_pin=ImmutableArtifactPin("a", "main", _digest("b"))) + ) + with pytest.raises(Exception, match="outside GGUF"): + NativeLoadedArtifactReport(0, 9, 1, 1, 1, "llama", _digest("a"), 8) + + +def test_native_worker_rejects_bad_session_open_before_session_acceptance(): + adapter = NativeWorkerBackendAdapter(_inputs()) + accepted = adapter.on_session_open( + _open(adapter), expected_route_session_id="tracker-session", expected_route_epoch=4 + ) + assert accepted.fingerprint.SerializeToString() == adapter.identity.fingerprint.to_proto().SerializeToString() + + with pytest.raises(NativeSessionRejected) as rejected: + adapter.on_session_open( + _open(adapter, route_epoch=5), + expected_route_session_id="tracker-session", + expected_route_epoch=4, + ) + assert rejected.value.error.code == pb.ERROR_CODE_EPOCH_STALE + + +def test_doctor_emits_native_identity_but_keeps_legacy_backend_dark(): + 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()) + # The probe needs only the normal backend shape; identity is supplied by the adapter. + native.hidden_size = 8 + native.is_head = False + native.is_tail = False + native.device = "cpu" + native.forward_bytes = lambda *args, **kwargs: type("Payload", (), {"body": b"x", "shape": [1]})() + result = validate_loaded_backend(native, selection, manifest.recipes[0], manifest) + assert result.report.identity == native.identity + assert result.report.model.revision == native.identity.artifact.revision + assert result.report.model.config_fingerprint == "sha256:" + _digest("a") + assert result.report.backend.quantization == "Q4_K_M" + assert (result.report.shard.start, result.report.shard.end) == (2, 5) + state = evaluate_report( + result.report.to_dict(), + model_matches=lambda value: value == "acme/llama.gguf", + advertised_model="acme/llama.gguf", + shard_start=2, + shard_end=5, + declared_recipe_id="native", + declared_recipe_version="1", + now=result.report.validated_at, + ) + assert state.state == STATE_UNCERTIFIED + + class Legacy: + hidden_size = 8 + is_head = False + is_tail = False + device = "cpu" + + @staticmethod + def forward_bytes(*args, **kwargs): + return type("Payload", (), {"body": b"x", "shape": [1]})() + + legacy = validate_loaded_backend(Legacy(), selection, manifest.recipes[0], manifest) + assert legacy.report.identity is None