Mesh wire contracts — DRAFT for promotion to the main repo's specification

Status: SUPERSEDED — promotion merged. This draft has been promoted and merged to the main repo's main: docs/specification/mesh.md there is now the normative text for the mesh contracts, with the three mesh-*-cases.json conformance fixtures (vendored back into this repo's conformance/ and passing). This file is kept only as the historical draft it was authored from. Differences between this draft and the promoted spec (e.g. the conformance-fixture formats, which the promotion made concrete, and the promoted §9 reconciliation with the .NET Benzene.Mesh.* packages) are resolved in the promoted spec's favor.

Everything below follows the existing wire conventions: camelCase field names, flat string→string headers, pre-serialized string bodies inside envelopes, the shared status vocabulary (wire-contracts.md §3), and RFC 3339 timestamps.


1. Reserved topic: mesh

A mesh-enabled service intercepts the reserved topic mesh (plus app-chosen aliases), exactly as healthcheck interception works (core-concepts.md §5): interception is by topic id alone, ignoring version; any other topic passes through. The response payload is the ServiceDescriptor (§2), status Ok.

Provisioning this endpoint is a deployment decision. A service that must not expose it (e.g. pending security review) simply does not register the interception middleware — every other mesh feed keeps working (§6).

2. ServiceDescriptor

The service's self-description, derived at startup from its handler registry — never hand-maintained. Also the body of a mesh:register message.

{
  "service": "orders",
  "serviceVersion": "1.4.2",
  "instanceId": "orders-7f9c",
  "runtime": "go",
  "binding": "http",
  "placement": { "cloud": "aws", "region": "eu-west-1" },
  "topics": [
    {
      "id": "order:create",
      "version": "v2",
      "requestSchema":  { "type": "object", "properties": { "name": { "type": "string" } }, "required": ["name"] },
      "responseSchema": { "type": "object", "properties": { "id":   { "type": "string" } }, "required": ["id"] }
    }
  ],
  "descriptorHash": "sha256:…",
  "degraded": ["registry"]
}

2.1 Schema derivation

requestSchema/responseSchema describe the marshaled JSON form of the registered request/response types, as a subset of the JSON Schema 2020-12 vocabulary. A port derives them once at startup. The mapping every port MUST follow (source language constructs on the left are each port's equivalents):

Construct Schema
string {"type":"string"}
boolean {"type":"boolean"}
integer kinds {"type":"integer"}
floating kinds {"type":"number"}
timestamp type (marshals RFC 3339) {"type":"string","format":"date-time"}
byte array (marshals base64) {"type":"string"}
text-marshaling custom type {"type":"string"}
raw/unknown JSON, dynamic values, custom serializers {} (unconstrained)
nullable/pointer of T T's schema with "null" added to its type
list/array of T {"type":"array","items":<T>}
string-keyed map of T {"type":"object","additionalProperties":<T>}
object/record {"type":"object","properties":{…},"required":[…]}

Object rules: serialization attributes/tags control names and omission exactly as the port's JSON marshaler does; fields the marshaler always emits are listed in required (declaration order — determinism feeds the hash); embedded/inherited members are flattened the way the marshaler flattens them; recursive types are cut at the cycle with {} (schemas stay self-contained; no $ref); constructs the marshaler cannot serialize are {}.

2.2 descriptorHash

"sha256:" + hex(sha256(canonicalJSON(descriptor))) where the hashed descriptor has instanceId, degraded, and descriptorHash itself blanked — the hash covers the contract (identity, placement, topics, schemas), so two instances of one build hash identically and the hash changes exactly when the contract changes. Canonical JSON: object members in a fixed documented order (struct declaration order for fixed shapes, lexicographic for maps), no insignificant whitespace.

3. TraceEvent

One pipeline invocation as the mesh sees it — semantic (topic + Benzene status), not transport-shaped.

{
  "traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
  "spanId": "00f067aa0ba902b7",
  "parentSpanId": "0af7651916cd43dd",
  "service": "orders",
  "instanceId": "orders-7f9c",
  "topic": "order:create",
  "topicVersion": "v2",
  "status": "ValidationError",
  "durationMs": 12.4,
  "startedAt": "2026-07-16T09:14:03.120Z",
  "correlationId": "abc-123"
}

4. Collector ingest topics

A collector is an ordinary Benzene service serving these topics over any envelope-capable transport:

Topic Body Success payload
mesh:register ServiceDescriptor (§2) {"accepted":1}
mesh:heartbeat Heartbeat (§5) {"accepted":1}
mesh:traces {"events":[TraceEvent…]} {"accepted":<count>}

Validation: service (register/heartbeat) is required → BadRequest when missing. A mesh:traces batch of any size, including empty, is accepted.

Re-registration replaces the previous registration wholesale, including provider edges — a redeploy that drops a topic drops the claim to provide it.

Sender behavior (normative for ports): trace export MUST be asynchronous, non-blocking and lossy under backpressure — a full buffer drops events, a failed send drops the batch, and no mesh feed may ever fail, slow, or block the invocation it observed.

5. Heartbeat

The health-check aggregate response (wire-contracts.md §5) reused byte-for-byte, wrapped with identity:

{
  "service": "orders",
  "instanceId": "orders-7f9c",
  "descriptorHash": "sha256:…",
  "sentAt": "2026-07-16T09:14:03Z",
  "health": { "isHealthy": true, "healthChecks": { "db": { "status": "ok", "type": "postgres" } } }
}

A heartbeat's descriptorHash differing from the registered descriptor's hash means the instance runs a contract the collector hasn't learned — the collector MUST surface this (the Go collector reports per-instance hashMatches) rather than silently keeping stale topics.

6. Degradation (normative)

Every mesh feed — descriptor endpoint, registration, heartbeats, traces — is independent and optional, on both sides:

7. Conformance fixtures (sketch)

To be authored alongside promotion, in the style of the existing docs/specification/conformance/ fixtures, and vendored back into each port:

8. Out of scope (this draft)

Query read-model shapes (mesh:query:*) are implemented by the Go collector but deliberately left out of the promoted contract for now: they are one collector's read models, not cross-port interop surfaces. They join the spec if/when a second collector implementation or third-party view needs them pinned.