diff --git a/Directory.Packages.props b/Directory.Packages.props
index 39dd734e..8590388c 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -32,6 +32,7 @@
+
diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Protos/deployment_artifact.proto b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Protos/deployment_artifact.proto
new file mode 100644
index 00000000..a097ca2c
--- /dev/null
+++ b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Protos/deployment_artifact.proto
@@ -0,0 +1,26 @@
+syntax = "proto3";
+
+package deployment_artifact.v1;
+
+option csharp_namespace = "ZB.MOM.WW.OtOpcUa.Commons.Protos.DeploymentArtifact.V1";
+
+// Central serves the deployed-configuration artifact bytes to a driver node (per-cluster mesh
+// Phase 3). Only admin-role nodes — which hold the central SQL connection — serve; driver nodes in
+// FetchAndCache mode are the clients. The bytes travel here, out-of-band, precisely so they never
+// ride an Akka message and hit the frame-size limit.
+service DeploymentArtifactService {
+ // Streams the artifact for a sealed deployment as ordered chunks. NotFound when the id is
+ // unknown, the deployment is not sealed, or the blob is empty (existence-hiding + the #485
+ // "empty bytes are never an empty config" rule). The client verifies
+ // SHA-256(reassembled) == the RevisionHash it already holds from DispatchDeployment, so no hash
+ // is carried here.
+ rpc Fetch(FetchRequest) returns (stream ArtifactChunk);
+}
+
+message FetchRequest {
+ string deployment_id = 1;
+}
+
+message ArtifactChunk {
+ bytes data = 1;
+}
diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/ZB.MOM.WW.OtOpcUa.Commons.csproj b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/ZB.MOM.WW.OtOpcUa.Commons.csproj
index 686d3f42..acad5d43 100644
--- a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/ZB.MOM.WW.OtOpcUa.Commons.csproj
+++ b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/ZB.MOM.WW.OtOpcUa.Commons.csproj
@@ -11,6 +11,24 @@
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
diff --git a/tests/Core/ZB.MOM.WW.OtOpcUa.Commons.Tests/DeploymentArtifactProtoTests.cs b/tests/Core/ZB.MOM.WW.OtOpcUa.Commons.Tests/DeploymentArtifactProtoTests.cs
new file mode 100644
index 00000000..50f28bf0
--- /dev/null
+++ b/tests/Core/ZB.MOM.WW.OtOpcUa.Commons.Tests/DeploymentArtifactProtoTests.cs
@@ -0,0 +1,38 @@
+using Shouldly;
+using Xunit;
+using ZB.MOM.WW.OtOpcUa.Commons.Protos.DeploymentArtifact.V1;
+
+namespace ZB.MOM.WW.OtOpcUa.Commons.Tests;
+
+///
+/// A compile-touch test: it exists so that the repo's first locally-compiled proto
+/// (deployment_artifact.proto) is proven to generate both the client stub and the server
+/// base and the message types. If Grpc.Tools codegen regresses, this file stops compiling —
+/// which, under TreatWarningsAsErrors, breaks the build loudly rather than silently.
+///
+public class DeploymentArtifactProtoTests
+{
+ [Fact]
+ public void The_request_message_carries_the_deployment_id()
+ {
+ var request = new FetchRequest { DeploymentId = "abc" };
+
+ request.DeploymentId.ShouldBe("abc");
+ }
+
+ [Fact]
+ public void The_chunk_message_carries_bytes()
+ {
+ var chunk = new ArtifactChunk { Data = Google.Protobuf.ByteString.CopyFromUtf8("x") };
+
+ chunk.Data.Length.ShouldBe(1);
+ }
+
+ [Fact]
+ public void The_service_base_and_client_types_generate()
+ {
+ // Referencing these types is the whole assertion — GrpcServices="Both" must emit both.
+ typeof(DeploymentArtifactService.DeploymentArtifactServiceBase).ShouldNotBeNull();
+ typeof(DeploymentArtifactService.DeploymentArtifactServiceClient).ShouldNotBeNull();
+ }
+}