15 KiB
DGR-002 — Adopt the versioned gRPC Shard protocol
Status: done. Every acceptance criterion is met with real command output. Evidence class: synthetic/unit — this story defines a schema and proves both languages agree on it. No model, GPU, network peer or benchmark is involved, and none is claimed.
1. Summary
packages/node/native/proto/shard_runtime.proto is now the semantic contract for
the native Shard data plane: Protocol Buffers over gRPC/HTTP2 (ADR-0020). Python
and C++ both generate from it, and a shared committed conformance vector proves
they encode it identically — byte for byte.
Design decisions worth carrying forward:
- Everything gRPC gives you is also in the schema. Deadline, cancellation, identity and flow control are carried as fields, not left to HTTP/2 metadata, because the existing relay carries these frames as opaque binary. A relayed frame has no HTTP/2 context to inherit a deadline or a channel identity from. If it is not in the schema, it does not survive the relay.
- Cancellation is both in-band and out-of-band.
CancelSignalrides the stream;Cancelis also a unary RPC. A cancel that can only travel down a stream that flow control has wedged is not a cancel. - Checksums cover the uncompressed payload. Compression is a per-hop
transport decision (reusing the existing
activation_compressionpolicies), so a checksum over the compressed frame would be invalidated by a hop that merely chose differently. - Application-level flow-control credits, not just HTTP/2 windows. HTTP/2 bounds bytes in flight; it does not bound how much work a worker has queued, and a relayed frame gets no window at all. Credits bound queue occupancy and KV pressure, and negotiation takes the strictest bound of either peer so a sender cannot talk a worker into unbounded queues.
2. Files changed
New:
| Path | What |
|---|---|
packages/node/native/proto/shard_runtime.proto |
The schema (sha256 9e211660…, see protocol.json) |
packages/node/native/CMakeLists.txt |
C++ generation + build wiring + ctest |
packages/node/native/tests/test_shard_protocol_conformance.cpp |
C++ conformance test |
packages/node/native/testdata/*.binpb |
Committed cross-language vectors |
packages/node/native/README.md |
How to regenerate and build |
packages/node/meshnet_node/native_protocol/__init__.py |
Public Python surface |
packages/node/meshnet_node/native_protocol/codec.py |
Bundle encode/decode, fragmentation, CRC32C, chunking, FC negotiation |
packages/node/meshnet_node/native_protocol/conformance.py |
Canonical vectors shared by both languages |
packages/node/meshnet_node/native_protocol/generated/ |
Generated Python stubs (committed) |
scripts/generate_native_protocol.py |
Python generation, with --check |
scripts/generate_protocol_goldens.py |
Vector generation, with --check |
scripts/bootstrap_native_toolchain.sh |
Builds protobuf C++ from source |
tests/test_native_shard_protocol.py |
45 Python tests |
Modified:
packages/node/pyproject.toml— added runtime floorsgrpcio>=1.82.1andprotobuf>=7.35.0, matching the committed generated-code requirements; newprotoextra pinninggrpcio-tools==1.82.1.packages/node/meshnet_node/activation_compression.py— optional bounded zstd output for untrusted protocol frames; existing callers remain compatible.packages/node/meshnet_node/native_protocol/__init__.py— exports negotiated bound constants and whole-session-message validation.
The canonical PRD marks only DGR-002 passed. git status before this story was clean.
3. Commands and real results
See commands.txt for the exact ordered list. Results:
python scripts/generate_native_protocol.py --check -> generated stubs are up to date
python scripts/generate_protocol_goldens.py --check -> conformance vectors are up to date
cmake -S packages/node/native -B build/native -DCMAKE_PREFIX_PATH=/tmp/pbsrc/install
-- gRPC C++ not found: building message types only (sufficient for the conformance test)
cmake --build build/native -j -> Built target shard_protocol_conformance
ctest --test-dir build/native --output-on-failure -> 1/1 Test #1: shard_protocol_conformance ... Passed
100% tests passed out of 1
cmp build/native/cpp_roundtrip.binpb \
packages/node/native/testdata/session_request_golden.binpb -> identical (exit 0)
pytest -q tests/test_native_shard_protocol.py -> 45 passed
pytest -q tests/test_native_shard_protocol.py \
tests/test_activation_compression.py -> 51 passed
pytest -q (final full suite) -> 728 passed, 12 skipped
pytest -q tests/test_tracker_routing.py::test_tracker_dashboard_can_cancel_inflight_proxy
(after an earlier flaky full-suite failure) -> 1 passed, 1 passed, 1 passed
clean minimum-runtime import + codec smoke test -> passed
grpcio==1.82.1, protobuf==7.35.0
compileall -q packages tests -> OK (exit 0)
git diff --check -> clean (exit 0)
The C++ lane was rebuilt from scratch by Ralph (rm -rf build/native) using only
the documented commands, and reproduced the same result. During controller
review the user explicitly chose not to repeat the destructive build-directory
cleanup, so the independent controller relied on the recorded CMake/CTest run
while reproducing every Python/generation/full-suite gate.
Controller review corrections
Independent controller review found and fixed two classes of issue before integration:
- Generated stubs required gRPC 1.82.1 and Protobuf 7.35.0, while the initial package metadata allowed much older runtimes that could fail at import time.
- Flow-control bounds were described but not enforced by the reference decoder. Tensor declarations, shape rank/dimensions, fragment/tensor counts, fragments, wire bodies, whole bundles, complete session messages (including envelope overhead), and zstd window/output expansion are now fail-closed against the negotiated/default bounds. Unspecified bundle versions, compression and checksums are rejected rather than interpreted as valid data.
- Negotiated initial credits could exceed
max_inflight_chunks; credits are now capped by the settled in-flight limit.
Controller results: protocol tests 45 passed; protocol plus shared compression
tests 51 passed; final full suite 728 passed, 12 skipped. A clean environment
at the declared minimum gRPC/Protobuf runtime versions imported both generated
stub modules and round-tripped the codec. Generation checks, compileall, static
secret scan, and git diff --check all passed.
Full-suite note — a pre-existing flaky test
tests/test_tracker_routing.py::test_tracker_dashboard_can_cancel_inflight_proxy
is flaky on a clean tree, independent of this story. Reproduction, run
before any DGR-002 file existed (working tree clean, git status empty):
pytest -q -> 1 failed, 682 passed, 12 skipped
FAILED tests/test_tracker_routing.py::test_tracker_dashboard_can_cancel_inflight_proxy
# same test, three consecutive isolated runs on the same clean tree:
pytest -q tests/test_tracker_routing.py::test_tracker_dashboard_can_cancel_inflight_proxy
-> 1 passed in 1.76s
-> 1 failed in 4.39s
-> 1 passed in 1.10s
It is a timing race in proxy cancellation (a 3-second in-flight generation raced
against the cancel assertion), not a deterministic failure, and it touches no code
this story changes. One controller full-suite run reported exactly that one failure
(1 failed, 719 passed, 12 skipped); three immediate isolated retries all passed
in 1.11 seconds, and the final exact-code full suite was green (728 passed, 12 skipped). It is flagged for whoever owns the tracker cancel path and is not
fixed here, since silently touching another story's code is out of scope.
4. Acceptance criteria
| Criterion | Where it is proven |
|---|---|
| Schema for capability, health, session stream, release, cancellation | shard_runtime.proto service ShardRuntime; test_service_exposes_capability_health_session_release_and_cancel |
| One long-lived bidi stream per Activation Seam, with deadlines, cancellation, flow control, structured errors | rpc Session (stream) returns (stream); test_session_is_one_long_lived_bidirectional_stream; Envelope.deadline_unix_nanos, CancelSignal + unary Cancel, FlowControl, ShardError |
| Bounded chunking for prefill; small decode fast path | ChunkInfo + plan_prefill_chunks (128-token bound, ADR-0008); DecodeStep; test_prefill_is_split_into_bounded_token_aligned_chunks, test_decode_fast_path_is_much_smaller_than_a_full_envelope_chunk |
| Envelope carries schema version, work id, session id, epoch, fingerprint, range/effective start, phase, position, idempotency step, cache expectation, compression, checksum | Envelope + NamedTensor; test_envelope_carries_every_field_the_protocol_promises asserts against the descriptor, so deleting a field from the .proto fails the test |
| Versioned named-tensor bundle: name, shape, dtype, byte order, fragments | TensorBundle/NamedTensor/TensorFragment; test_named_tensor_bundle_is_versioned_and_fully_described, test_bundle_round_trips_multiple_named_tensors |
| Round-trip + compatibility tests in Python and C++ | 45 Python tests; C++ ctest 1/1; cross-language byte equality |
| Targeted pytest passes | 45 passed |
compileall packages tests |
exit 0 |
git diff --check |
exit 0 |
| Default tests deterministic, download-free, credit-free, GPU-free | Pure in-memory protobuf; no model, no network, no GPU |
| Full deterministic pytest passes, or pre-existing failure recorded | Final exact-code run: 728 passed, 12 skipped; earlier sole flaky failure documented with clean-tree reproduction and 3/3 passing retries |
5. How the cross-language claim is actually earned
Two codecs that each round-trip their own output prove only that each is self-consistent. Instead:
- Python builds the canonical
SessionRequestand commits its bytes. - The C++ test parses those bytes, asserts every field, recomputes the CRC32C
from the polynomial in independent C++ code, reassembles the multi-fragment
tensor, and re-serializes to
cpp_roundtrip.binpb. test_cpp_and_python_agree_byte_for_byteasserts that file equals the golden.
Compatibility is tested in both languages: an unknown field from a newer peer survives a parse/serialize hop (a Shard forwards activations — silently stripping fields would corrupt a route it is merely a waypoint on), and a sparse message from an older peer parses to proto3 defaults.
6. Limitations and deferred work
- gRPC C++ was not built or linked. The C++ lane verifies the schema (message
types), not a running gRPC C++ server, because this machine has no gRPC C++ stack
and building it is a large dependency the conformance test does not need.
CMakeLists.txtalready generates and exportsshard_runtime_grpcwhenfind_package(gRPC)succeeds. DGR-008 must install gRPC C++ and extendscripts/bootstrap_native_toolchain.sh. - No wire is exercised. No client, server, or stream lifecycle exists yet — no deadline actually fires, no credit is actually consumed. This story defines and proves the contract; DGR-008/DGR-009 implement it.
- The protobuf C++ toolchain used here was installed to
/tmp/pbsrc/install(ephemeral).scripts/bootstrap_native_toolchain.shreproduces it; prefer a durable prefix such asbuild/native-toolchain. crc32chas a pure-Python fallback (used here) and picks upgoogle_crc32cwhen present. The fallback is byte-exact but slow; a worker on the hot path should install the native package. Not a correctness limitation.- Compression on the wire is zstd-or-none only, matching the existing seam.
7. Compatibility and migration notes
- This does not change the existing HTTP activation wire.
X-Meshnet-Wirestays at2and the legacy/forwardpath is untouched. The native protocol is a separate contract with its ownSchemaVersion, starting at 1. Nothing in this story is on any live request path — it is additive. - Semantics are deliberately preserved from the existing ADRs so the two transports
mean the same thing:
effective_start_layer(ADR-0012),CacheMode/expected_past_lenandERROR_CODE_CACHE_MISSmapping to today's HTTP 409cache_miss(ADR-0022), bfloat16 boundary dtype and 128-token prefill chunks (ADR-0008), fingerprint/recipe identity mirroring the capability report (ADR-0023). TensorFragmentfield 5 (uncompressed_size) is reserved: it was removed becauseNamedTensor.total_bytesis the single source of truth. Never recycle it — a recycled field number is the one schema change peers cannot detect, because the bytes still parse.- Committed Python stubs are guarded by
--checkin the test suite, so they cannot drift from the schema unnoticed.
8. Handoff to dependent stories
- DGR-003 (runtime recipe/fingerprint): populate
Fingerprint(model_artifact_digest,runtime_recipe_digest,recipe_id,recipe_version,catalogue_version). The mismatch outcome is already specified:ERROR_CODE_FINGERPRINT_MISMATCH. Do not invent a second identity struct. - DGR-005/006 (range loading, architecture boundary): the boundary payload is a
named bundle, not a bare tensor — a boundary needing more than one tensor is
already representable. Execute
[effective_start_layer, end_layer), never fromstart_layer. - DGR-007 (concurrent sessions/KV): isolate on
(route_session_id, route_epoch).CacheExpectation/CacheResultandERROR_CODE_CACHE_MISSare the contract; a decode step whoseexpected_past_lendoes not match must miss, never fall back to a silent stateless forward.idempotency_stepmeans a retried step is acknowledged (Ack.duplicate), not re-applied — re-applying advances the KV cache twice and desynchronises the route. - DGR-008 (C++ worker): link
shard_runtime_grpcfromCMakeLists.txt; you must first install gRPC C++ (see limitations). HonourFlowControlcredits and themax_chunk_bytesbound. Usepackages/node/meshnet_node/native_protocol/codec.pyas the reference for fragment reassembly and checksum validation. - DGR-009 (Meshnet integration): the relay may carry these serialized frames as opaque binary — that is exactly why deadline/cancel/identity are in-band. Do not add a second control plane.
- Anyone editing the schema: run both
--checkscripts; if a vector legitimately changes, regenerate it and say so, because the C++ test asserts those exact bytes.