Foreign-Language Bridge (Python, Bun/Node) over gRPC

How a non-.NET process joins the mesh and uses mesh features natively. The model is the MAUI / Blazor-WASM participant: a remote process attaches at its own address, speaks the mesh's IMessageDelivery envelopes, and becomes a first-class participant — Post/Observe requests, subscribe to live node streams, read and write nodes under its own identity. The only thing we swap is the transport skin: gRPC instead of SignalR.

Core idea

A single gRPC bidirectional stream IS one mesh participant connection — the exact role SignalRConnectionHub plays for MAUI/WASM. The foreign-language package is the in-language equivalent of IMessageHub + MeshOperations speaking that stream.

 Python / Bun process                      Portal (.NET)
 ┌─────────────────────┐   gRPC  Open()    ┌───────────────────────────┐
 │ meshweaver SDK      │◄═════ bidi ══════►│ MeshGrpcService           │
 │  connection (xport) │   ClientFrame      │  GrpcConnectionRegistry   │
 │  mesh (operations)  │   ServerFrame      │   → IRoutingService       │
 └─────────────────────┘                    │   → the mesh (hubs)       │
        py/<uuid>                            └───────────────────────────┘

The decisive choice: protobuf frames, JSON carries

We do not re-model the mesh's hundreds of polymorphic $type messages in protobuf — that schema would drift instantly. Instead:

mesh.proto therefore carries JSON strings, mirroring SignalR's Connect(addressJson) / DeliverMessage(deliveryJson) / ReceiveMessage(json) one-to-one.

Server transport = the SignalR host, re-skinned

MeshWeaver.Hosting.Grpc is a near-mechanical mirror of MeshWeaver.Hosting.SignalR:

SignalR gRPC
SignalRConnectionHub.Connect(addressJson) first ClientFrame.connect
SignalRConnectionHub.DeliverMessage(json) each ClientFrame.deliver
IHubContext…SendAsync("ReceiveMessage", json) ServerFrame.receive written by the per-connection pump
SignalRConnectionRegistry.Authenticate(token) bearer token in gRPC call metadata, validated at stream open
registry.ConnectroutingService.RegisterStream(address, push) identical
registry.Deliver re-stamps AccessContext identical

Two differences fall out of gRPC's shape:

  1. No out-of-band push. SignalR's IHubContext can push to any connection id; a gRPC bidi response stream is only writable inside the Open call. So each connection owns an outbound Channel<ServerFrame> that the Open call drains to the wire on one writer (gRPC forbids concurrent writes); the registry's push handler enqueues onto it.
  2. async/await at the boundary. MeshGrpcService.Open is async Task exactly as SignalRConnectionHub's methods are — the transport edge. Once a frame enters GrpcConnectionRegistry everything is reactive and runs off the boundary.

Security invariant

The bearer API token travels in gRPC call metadata (authorization: Bearer …), is validated once at stream open (the same ValidateTokenRequest the SignalR/MCP paths use), and every inbound delivery's AccessContext is overwritten server-side with the resolved identity before it touches the mesh. A client-supplied identity is never trusted. No token ⇒ anonymous (writes cleanly RLS-denied). This is what keeps "never write as hub" intact across the foreign boundary.

Streams ride the one connection

A stream subscription is just a message exchange: the participant posts a subscribe request as a deliver frame; the owner's change events come back as receive frames addressed to the participant. The SDK demuxes by stream id and folds Full → RFC 7396 merge-patches into live state — exactly what ISynchronizationStream does in C#. No separate RPC.

Foreign packages

Each package = "a remote participant + the MeshOperations surface, in-language", built on three primitives over the bidi stream:

  1. request/response — send a delivery, await the one whose properties.RequestId matches.
  2. fire-and-forget — send a delivery.
  3. stream — send a subscribe request, demux change events by stream id.

get/search/watch/patch/… are thin compositions of these over the existing mesh request types — a port of MeshWeaver.Mesh.Operations.MeshOperations.

Build note — Apple Silicon

Grpc.Tools ships no macOS-arm64 protoc/grpc_csharp_plugin (only macosx_x64, needing Rosetta). CI (Linux) is unaffected. For local arm64 builds either install Rosetta (softwareupdate --install-rosetta) or brew install grpc protobuf — the MeshWeaver.Hosting.Grpc project auto-points codegen at /opt/homebrew/bin native binaries when present (guarded by Exists() so other machines fall back to the bundled tools).

Participant reachability (monolith vs Orleans)

The server registers a participant's inbound route with routingService.RegisterStream(address, push) — exactly as SignalRConnectionRegistry does. A reply addressed to that participant is delivered by MonolithRoutingService.RouteImpl's streams lookup. But RouteImpl is only reached when RouteMessage → PathResolver.ResolvePath succeeds with an empty remainder; a bare participant address with no backing mesh node resolves to NotFound first, so the streams check is skipped. Two paths make a participant reachable:

GrpcConnectionRegistry.Connect does both: it RegisterStreams the address (Orleans) AND hosts a proxy hub at the participant address (monolith). The proxy installs a catch-all WithRoutes handler — routes run for every delivery a hub receives — that forwards each message addressed to the participant from elsewhere to its gRPC Open stream; the hub's own self/lifecycle messages (InitializeHubRequest, disposal — sender == participant) are left for the hub to process. This closes the full mesh→participant response path; the round-trip test verifies it end-to-end.

Phasing

  1. mesh.proto + MeshWeaver.Hosting.Grpc (transport + hosted-participant response path) + a network-free full round-trip test (drives the service over in-memory duplex streams against a real mesh). ← done
  2. Python meshweaver: transport + correlation + get/search/watch. ← skeleton done; wire-shape pinned against a captured sample
  3. @meshweaver/client (Bun/Node) — same surface, AsyncIterable streams, proto loaded at runtime. ← skeleton done
  4. A live end-to-end test over a real Kestrel gRPC endpoint (HTTP/2 cleartext) with a real GrpcChannel. ← done (MeshGrpcLiveTest). Still open: an Orleans round-trip to confirm the RoutingGrain path.
  5. Widen operations (move/copy/execute/threads) + the hosted-Code-node subprocess path (the kernel spawns python/bun for an executable Code node and hands it MESH_GRPC_URL + a scoped token, so in-mesh scripts reach back through the same SDK over loopback).
Reconnecting…
The server was updated. Reloading the page to pick up the latest version.