The record claimed the two implementations already disagreed. Inspection showed the live wire agrees: the disagreeing grant types were dead (removed), and the envelope's two extra headers are optional and set when relevant, not missing. The danger was dead types contradicting the wire, not live disagreement — which is a sharper reason for specifying the wire and checking against it, not a weaker one. The model stands; the conformance suite's job is prevention rather than repairing a present break. Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
9.8 KiB
topic, status, date, deciders, reconstructed, extends
| topic | status | date | deciders | reconstructed | extends |
|---|---|---|---|---|---|
| the tiers | accepted | 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.
A correction, made after inspecting the wire rather than the types (2026-09-16). This record
first claimed the two implementations already disagreed — resource vs Provision, consumer
meaning the module in one and the node in the other, headers declared on one side and emitted by
neither. On inspection the live wire agrees, and the claim was wrong:
- The grant types that disagreed (
Grant,Interface,Credentialin the SDK'scontracts) were dead — exported and imported by nothing. The live provisioning wire is the contributions file, whose shape (as,secret,node,at,values) is the same on both sides. Those dead types have been removed. - The envelope agrees too: Go emits all five required headers, and
x-causation-id/x-schemaare optional — the SDK sets them when a handler has a causation or a schema, and a bare event carrying neither is correct, not a drift.
So the danger was never live disagreement. It was dead types that contradicted the live wire, which read as the contract and were not — and are exactly what led this record to assert a drift that inspection did not find. That is a sharper reason for the decision below, not a weaker one: a type is only as good as its being the wire, and the way to guarantee that is to specify the wire and check implementations against it, rather than to trust a hand-kept type to still describe it.
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 durableserve.<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
consumeris 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 agree on the live wire — inspection showed it. What was wrong was a set of dead types beside the wire, now removed. The suite's job here is therefore prevention: to keep that agreement true as the wire changes, and to hold a new language's SDK to it, rather than to repair a break that exists today.
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. |