9.5 KiB
topic, status, date, deciders, reconstructed, extends
| topic | status | date | deciders | reconstructed | extends |
|---|---|---|---|---|---|
| the mesh | accepted | 2026-10-01 | jochen | false | 02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md |
160. The mesh issues an assignment's subjects, and a runtime serves what it is issued
Context
A module's code names no subject. It registers tools by name and emits events by name, and design 29
§1 says the rest: the module names its event and the mesh decides where it lands. What was built
decided it twice. The runtime derives mesh.mod.<module>.tool.<name> from the module's name by a rule
compiled into it; the controller derives the same subject by the same rule compiled into it, and grants
it. They agree because two binaries carry one convention, which is the failure design 33 §2 names for
seat protocols: discovery that reads a binary disagrees with the mesh the moment the two are on
different versions. ADR 0159
extended the convention this morning — a second subject per tool with the machine as its last token, a
seat's verbs served from the credential's claims — and extending it made the shape plain: every such
change is written in the runtime and in the controller, and a module whose instances must not be
confused is told apart by a rule in a binary rather than by the mesh that assigned it.
The operator put it in one sentence: the mesh knows the subjects, the modules do not; a module should ask what to listen on. This record decides exactly that.
Considered Options
- Keep the convention, keep it in two places. Cheap until the next change; every change is two changes, and the mesh cannot vary a subject for one assignment without a rule for all.
- Keep the convention in one place by putting it in the SDK alone, and have the controller call the SDK's rule. The controller is Go and the SDK is TypeScript; one of them would still carry a copy.
- The mesh issues the subjects. For every assignment the controller composes a membership: what this instance serves, where, in which queue if any; the seat verbs it holds; where its events land; what it may reach and at which subjects. It publishes it to a subject only that assignment may read, kept last-per-subject so a runtime that connects late reads the current one. The runtime serves exactly the list and nothing it did not receive. The grant is composed from the same membership, in the same act, so the two cannot drift.
Decision
Option 3.
- A membership per assignment. The controller composes, for a module on a machine, one document: the tools the module serves with the subject each is served on and the queue group if any; the seat verbs this instance serves and their subjects; the subject each of its events lands on; what it may reach — the tools it invokes, resolved to the subjects the mesh issued to those modules' instances — and what it consumes. The runtime registers tools and events by name; the membership says where.
- Published, not written into the definition. The membership is a message on
mesh.assignment.<node>.<module>in a stream that keeps the last per subject, like a node's declaration. The controller publishes it whenever the assignment's facts change: a push, a seat handover, an instance added elsewhere, an upgrade. A runtime reads the current one when it connects, serves it, and keeps reading, so a change reaches a running instance as a re-subscription rather than a restart. - One bootstrap rule, and only one. The credential names the node and the module; the membership's subject follows from those two names and nothing else, and the account may subscribe it. Every other subject is data in the membership. This is the one convention the runtime keeps, the way a resolver keeps the address of a root.
- The grant is the membership, read the other way. What an account may subscribe is what its membership says it serves plus its own membership's subject; what it may publish is what its membership says it emits and reaches. One composition yields both, so a subject the runtime serves without a grant, or a grant for a subject nothing serves, cannot be written.
- Whether an instance answers for the module, or only for its machine, is the mesh's to decide. A module on one machine is issued the module's plain subject and its machine's. A module on several is issued only its machine's unless its definition says its instances are interchangeable, a fact about the software and not about the bus; then every instance is issued the plain subject in one queue group as well. The console lists what the memberships say: a stateful module on two machines appears once per machine; a stateless one appears once.
- A caller composes nothing. The console's listing carries each tool's subject; the SDK's call by name reads the subject from the caller's own membership, where the mesh wrote what it may reach. The shape of a subject is the controller's business and may change without any module or runtime changing.
- Today's shape is the shape issued first.
mesh.mod.<module>.tool.<name>, with the machine as the last token for an instance, andmesh.seat.<seat>.tool.<verb>with the machine for a node-scoped seat, are what the controller composes on day one, so nothing on the mesh moves when the membership arrives; only who decides it moves. ADR 0159 stands for what it decided — a call names the machine, every answer names it, a holder serves its seat — and is extended in how: those facts are now issued, not derived.
Consequences
- The runtime loses its subject rule and its claims rule; it serves a list. The controller gains one
composition and one stream; design 25 §2 and §3 gain a line each. The console loses
toolSubjectand reads subjects from the listing. The SDK'sinvokeToolreads the caller's membership. - A subject scheme change is a controller release and a republish of every membership, with no module rebuilt — the opposite of this morning's forty-three builds.
- A membership can differ per assignment on purpose: an instance that holds a seat serves more; an instance the mesh wants quiet serves less; a module the mesh is retiring can be issued nothing and told so.
- During the move, a runtime that finds no membership for its assignment falls back to the derived shape and says so in its log, so the wave of this change is a controller release followed by one push, and a runtime older than the change keeps working on the convention it carries.
How this is checked
| Rule | Checked by |
|---|---|
| A membership composed for an assignment and the grant composed for its account name the same subjects, both ways | a controller test over a module on one machine, on two, holding a seat, and declared interchangeable |
| A runtime serves exactly the subjects its membership lists, and re-subscribes when the membership changes | a runtime test against a real bus: a membership published, served; republished with a subject removed and one added, followed |
| A runtime with no membership says so and serves the derived shape | the same test, before any membership is published |
| The console lists a stateful module on two machines once per machine, and composes no subject | the MCP conformance test |
| Live: the store's databases asked of one named machine and through the seat, after a controller release and one push, with no module rebuilt | by hand |
Built, 2026-10-01
Progressive insight — 2026-10-01. The decision stands; these are the facts of its building.
- The controller's half: mesh-controller 188 — the membership, its subject, the assignments stream read directly, a module's account granted its own membership and nothing else of the stream, a membership published after each push.
- The runtime's half: mesh-tools 23 — the one address derived, the membership read and followed, exactly the issued subjects served and re-served, the derived shape with a log line until one is issued, a seat's verbs implemented under the seat's name and never listed as the module's, the listing carrying subjects and the console composing none. A claim may now name the verbs it serves for its seat (mesh-controller 186), so a holder's own tools need not be the seat's.
- What the first roll-out taught: the controller's own grant did not name the assignments it issues,
so the first memberships were refused by the server and every runtime kept the derived shape —
which is exactly the fallback this record asked for, and exactly why nobody noticed
(issue 183).
The SDK's
invokeToolstill composes a subject; it reaches a membership through the runtime's broker, which does, so the caller-side rule is met there and not yet in the SDK's own words.
References
- ADR 0159 — extended: the same facts, issued rather than derived
- ADR 0152, ADR 0132 — the surface and the seat's tools this applies to
- Design 25 — The bus on NATS §2, §3; Design 32 — What a module declares §1; Design 33; Design 34