Files
hq/02-DECISIONS/0074-the-wire-is-specified-not-the-types.md
T
jschoubben 3d693a8735 ADR 0074 — the wire is specified; an SDK is what passes the suite
ADR 0039 settles what belongs in an SDK. It does not say what happens when there
is more than one, and there already is: the contracts are expressed as Go types
in the control plane and host and as TypeScript types in the SDK, and nobody has
felt it because both live in one repository.

They already disagree. The provision's field is "resource" in one and "Provision"
in the other; "consumer" means the module in one and the node in the other; the
envelope declares six headers on one side and emits four on both — the missing
two being x-causation-id and x-schema, the second of which is exactly what a body
needs in order to change shape without silent misreads.

That class of failure does not announce itself. Two implementations disagreeing
about an envelope do not fail to compile — they ignore each other's messages, and
a mesh where a module stops reacting looks like a mesh where nothing happened.

So the decision is to specify the wire rather than share the types, because the
shapes are the easy half. What two implementations actually disagree about is
behaviour: queue naming and durability, which headers are required and what an
unknown one means, taking identity from the sealed credential rather than the
environment, dedup on an id only the emitter can make, pinning a fingerprint
rather than trusting an authority.

And the suite is executable rather than prose, because a specification nobody can
run is a document two implementations drift from while both believe they conform.
The two existing implementations are the first made to pass it — a suite only new
SDKs must satisfy would certify every future language against a disagreement that
is already here.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
2026-09-15 13:26:57 +02:00

6.0 KiB

topic, status, date, deciders, reconstructed, extends
topic status date deciders reconstructed extends
the tiers proposed 2026-09-15 jochen false 0039-what-the-sdk-holds-and-refuses.md

74. The wire is specified; an SDK is whatever passes the conformance suite

Context

ADR 0039 says what the SDK holds: the tool-serving harness, the messaging and event framework, the contracts, and core primitives. It settles what belongs in an SDK. It does not say what happens when there is more than one.

There is already more than one. The contracts are expressed twice — as Go types in the control plane and the host, and as TypeScript types in the SDK — and nobody has felt it because both live in one repository and one head.

They already disagree. Not in some future where a second language is added; today:

TypeScript Go
the provision's field resource Provision
what consumer means the module the node; the module is From
event headers six, including x-causation-id and x-schema four — the other two are never written

So one word means two things in the two halves of one mesh, and the header that exists so a body's shape can change without silent misreads is declared on one side and emitted by neither.

A failure of this kind does not announce itself. Two implementations that disagree about an envelope do not fail to compile — they ignore each other's messages, and a mesh where a module stops reacting looks exactly like a mesh where nothing happened.

The question this settles

A module may be written in any language the mesh can build (18-building-a-module). Every language needs an SDK. What is an SDK of?

Two answers were available, and the obvious one is wrong.

Shared types, generated. Write the shapes once — a schema, an IDL — and generate Go, TypeScript, Rust. It is the familiar answer and it solves the smaller half of the problem. The shapes are not where the difficulty is.

A specified wire, with a conformance suite. The shapes are a consequence; what an SDK must get right is behaviour.

Decision

The wire is specified, and an SDK is any implementation that passes the conformance suite.

What the specification covers is what two implementations can disagree about:

  • the exchanges and queues — which exchanges exist, that a consumer's queue is durable and named <node>.<module>.events, that a tool is served from a shared durable serve.<key>
  • the envelope — every header, which are required, what an unknown x- header means, and that ignoring one is correct rather than lax
  • identity — that a module's node and module name come from its sealed credential and not from its environment, so what it emits matches what the mesh authorised
  • delivery — at-least-once, and that dedup is on x-event-id, which only the emitter can make
  • the credential — the sealed document's fields, and that a connection pins a certificate fingerprint rather than trusting an authority
  • provisioning — a grant in, a credential out, and what each carries
  • the vocabulary — that consumer is one thing, named once

And the suite is executable, not prose. A specification nobody can run is a document two implementations drift from while both believe they conform. Conformance is a set of fixtures — an emitted event, a served tool call, a grant and its answer — that every SDK must produce and consume byte-for-byte.

The existing two implementations are the first two to be made to pass it. Not a future language: the drift above is present, and a suite that only new SDKs must satisfy would leave the disagreement that already exists in place while certifying everything added afterwards against it.

Why not generated types

Generation makes the shapes agree and leaves everything that matters unspecified. Two SDKs generated from one schema can still name their queues differently, take identity from the environment, dedup on the wrong field, or omit a header the other requires — and every one of those is a mesh that runs and quietly does not work.

It also makes the contract into whatever the generator supports, which is a decision nobody made about a boundary everything else depends on.

The shapes are worth generating once the wire is specified. That is a convenience, and it comes second.

Consequences

A language is a commitment, and now a measurable one. Adding one means implementing the wire and passing the suite. That is more work than transliterating types, and it is the work that was always there — the difference is that it can now be finished rather than believed.

Versioning becomes possible. x-schema exists for it and is never written. A specified envelope with a version on the body is what lets a mesh hold a module built against an older SDK, which is the ordinary state of any mesh that has been running for a while.

The two current implementations will be found wrong. They disagree, so at least one is. Fixing that is the point rather than a cost, but it is not free: something is emitting or expecting something it should not.

This does not make the mesh polyglot by itself, and should not be reported as though it does. It makes polyglot possible to do correctly. A Rust SDK is still a Rust SDK.

How this is checked

Rule Checked by
One vocabulary A word means one thing across implementations, checked by the fixtures using it.
The wire is what is specified Both existing SDKs run the conformance suite in their own test suites, and a change to one that breaks a fixture fails there rather than in a mesh.
A new SDK is a passing SDK A language is not listed as buildable until its SDK passes; the toolchain list and the conformance results name the same set.
An unknown header is ignored A fixture carries one, and every implementation accepts it.
Identity comes from the credential A fixture sets an environment that disagrees with the credential, and the emitted event carries the credential's.