Files
hq/02-DECISIONS/0074-the-wire-is-specified-not-the-types.md
T
jschoubben 89302aa3e0 The protocol is split per capability, and an SDK implements it
Two corrections, both from the operator and both better than what was written.

An SDK is an implementation of the mesh's module protocol in one language, and
nothing more. The first draft defined it by the test it passes, which describes
how you check one rather than what one is — and leaves it sounding like a library
that helpers could accumulate in.

And the protocol is split per capability, which was missing entirely. A module
that only consumes events uses the event capability; one that serves tools uses
the tool capability; a provider uses provisioning. Nothing about consuming an
event requires knowing how a grant is answered, so an SDK need not implement all
of it to be real.

That has a precedent here: a host declares which resource kinds it can apply, and
a partial host is a real thing rather than a broken one (ADR 0005). An SDK
implementing the floor and events is exactly as legitimate, and a module written
against it is a module that does events.

Which changes what adding a language costs. A Rust SDK doing connection and
events is useful the day it exists, with tools and provisioning following when
something needs them — rather than a language being unsupported until it is
entirely supported, which is what makes adding one a project instead of a
contribution.

Conformance is therefore per capability too: a monolithic pass or fail would make
a partial implementation indistinguishable from a broken one, which is the
distinction the whole thing rests on.

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

8.9 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 mesh defines a module protocol; an SDK is an implementation of it

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 mesh defines a module protocol. An SDK is an implementation of that protocol in one language, and nothing more.

That is the whole of what an SDK is. Not a library a language happens to have, not a convenience layer, not a place for helpers to accumulate — an implementation of a specified protocol, finished when it implements it and correct when it agrees with every other implementation.

The protocol is split per capability

A module does not use all of it, so an SDK need not implement all of it. A module that only consumes events uses the event capability. One that serves tools uses the tool capability. A provider uses provisioning. Nothing about consuming an event requires knowing how a grant is answered.

So the protocol is a floor plus capabilities:

part what it covers who needs it
connection — the floor reading the sealed credential, pinning the certificate fingerprint, taking identity from the credential rather than the environment everything
events the envelope and its headers, the durable per-consumer queue, binding, at-least-once with dedup on x-event-id a module that emits or consumes
tools registration, the shared durable serve.<key> queue, request and reply a module with a surface
provisioning a grant in, a credential out, and what each carries a module that provides something

This is the same shape the host already has. A host declares which resource kinds it can apply, and a partial host — one that can write files and run things but not manage users or containers — is a real thing rather than a broken one (ADR 0005). An SDK that implements the floor and events is exactly as legitimate, and a module written against it is a module that does events.

So a language arrives in pieces rather than all at once. A Rust SDK implementing connection and events is useful the day it exists; tools and provisioning follow when something needs them. The alternative — a language is unsupported until it is entirely supported — is what makes adding one a project rather than a contribution.

And what a language can be used for is then a fact the mesh can state, rather than something an author discovers by writing a module that cannot be built: the toolchain list says which languages exist, and the conformance results say what each can do.

What the specification covers

Per capability, 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

Conformance is per capability

A suite per part, and an SDK claims the parts it passes. A monolithic pass/fail would make a partial implementation indistinguishable from a broken one, which is the distinction this is built on.

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 divisible one. Adding one means implementing the protocol and passing the suites for the parts it claims. That is more work than transliterating types, and it is the work that was always there — the difference is that it can be finished, and finished in pieces, 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 for a capability until its SDK passes that capability's suite; the toolchain list and the conformance results name the same set.
A partial SDK is a real thing An SDK implementing the floor and one capability passes, is listed for that capability, and a module using another is refused with the reason — rather than failing at runtime in a language nobody said was finished.
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.