ADR 0160: the mesh issues an assignment's subjects, and a runtime serves what it is issued; designs 25, 32, 33, 34

This commit is contained in:
2026-10-01 14:33:56 +02:00
parent 5460681117
commit 99eb322de5
6 changed files with 137 additions and 2 deletions
@@ -0,0 +1,110 @@
---
topic: the mesh
status: accepted
date: 2026-10-01
deciders: jochen
reconstructed: false
extends: 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](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md)
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
1. **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.
2. **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.
3. **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, and `mesh.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](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md)
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 `toolSubject`
and reads subjects from the listing. The SDK's `invokeTool` reads 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 |
## References
- [ADR 0159](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md) — extended: the same facts, issued rather than derived
- [ADR 0152](0152-the-operators-surface-is-a-module-the-console.md), [ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md) — the surface and the seat's tools this applies to
- [Design 25 — The bus on NATS](../03-DESIGN/01-to-be/25-the-bus-on-nats.md) §2, §3; [Design 32 — What a module declares](../03-DESIGN/01-to-be/32-what-a-module-declares.md) §1; [Design 33](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md); [Design 34](../03-DESIGN/01-to-be/34-the-console.md)
+1
View File
@@ -173,6 +173,7 @@ python3 00-META/checks/index.py fail if stale
- **0157** — [A build says what it does on the bus, as it happens](0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md)
- **0158** — [A provider with one credential shares it with every consumer, and the vault remakes it for all of them at once](0158-a-provider-with-one-credential-shares-it-with-every-consumer.md)
- **0159** — [A tool call names the machine it is for, every answer says which machine answered, and a holder's runtime serves its seat's verbs](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md)
- **0160** — [The mesh issues an assignment's subjects, and a runtime serves what it is issued](0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)
### Its tiers, from the bottom up
+11
View File
@@ -9,6 +9,7 @@ code:
- mesh-sdk src (the protocol's NATS binding, step 3)
updated: 2026-10-01
decisions:
- 02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md
- 02-DECISIONS/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md
- 02-DECISIONS/0106-the-bus-is-nats.md
- 02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md
@@ -81,8 +82,18 @@ mesh.seat.<seat>.accept.<verb> work submitted to a role (JetStream: per-s
mesh.seat.<seat>.event.<verb> a role's own event (JetStream: EVENTS)
mesh.seat.<seat>.tool.<verb> a role's tool (core request/reply)
mesh.ask.<node>.<command> the controller's command api (core request/reply)
mesh.assignment.<node>.<module> an assignment's membership (JetStream: ASSIGNMENTS, last-per-subject)
```
**Revised 2026-10-01** ([ADR 0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)): the rows above
for a module's and a seat's tools are the shapes the controller *issues*, not rules a runtime carries.
Every assignment is published a membership — what it serves and where, in which queue, its seat verbs,
where its events land, what it may reach — on `mesh.assignment.<node>.<module>`, kept last per subject
like a declaration, republished when the assignment's facts change. The runtime serves exactly that
list; the account's grant is the same membership read the other way; the console's listing carries each
tool's subject. The one rule a runtime keeps is the membership's own subject, from the two names in its
credential.
**Revised 2026-09-27** ([ADR 0129](../../02-DECISIONS/0129-a-seat-carries-the-protocol-of-its-role.md)):
**`mesh.build.request`, `mesh.control.built` and the BUILDS stream are gone.** A build is work submitted to a role, and the
mesh already has a shape for that — a seat's `accept` subjects, on a work queue with a queue group of
@@ -13,6 +13,7 @@ code:
- mesh-catalog modules/mesh-catalog
updated: 2026-09-28
decisions:
- 02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md
- 02-DECISIONS/0126-a-module-declares-its-own-seats.md
- 02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.md
- 02-DECISIONS/0128-the-mesh-bus-is-required-not-ambient.md
@@ -120,6 +121,12 @@ and a module consuming one event from two emitters could tell them apart only by
The subject already carries the emitter, so the key a module sees names it too — which makes a
disagreement between a manifest and the code a typo rather than a category error.
*2026-10-01 ([ADR 0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)):* where a name lands is now
*issued* to each assignment as a membership the controller publishes, rather than derived by a rule
the runtime carries; a module still declares only names, and gains one fact about itself — whether its
instances are interchangeable — which decides whether the mesh issues it the module's plain subject
beside its machine's.
## 2. Three namespaces, and nothing else
**Its own** — `mesh.mod.<module>.>`. Its events and its tools. Nothing else may publish into it,
@@ -4,6 +4,7 @@ status: implemented
code: [mesh-controller, mesh-tools]
updated: 2026-10-01
decisions:
- 02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md
- 02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md
- 02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md
- 02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md
@@ -86,7 +87,9 @@ this section describes for seats: each instance also serves `mesh.mod.<module>.t
a caller writes `<module>.<tool>@<node>`, and every answer names the machine that gave it. And §3 is
built for every holder, not only the controller: the credential names the seats a module claims and
their verbs, the runtime serves each with the tool of the same name on the seat's subject, and the
bus admits it only where the module holds the seat.
bus admits it only where the module holds the seat. *Later the same day ([ADR 0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)):*
the subjects a holder serves, and a module's own, stop being derived in the runtime and are issued to
the assignment as a membership the controller publishes; the shape stays, the deciding moves.
## 5. Discovery
+4 -1
View File
@@ -4,6 +4,7 @@ status: implemented
code: [mesh-catalog, mesh-tools, mesh-controller]
updated: 2026-10-01
decisions:
- 02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md
- 02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md
- 02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md
- 02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md
@@ -69,7 +70,9 @@ kept for a short while and refreshed, so an agent asking on every turn does not
an optional `node` the console lists on each one, puts into the subject and never hands to the module,
for a module that runs on several machines; without it whichever instance answers first does, and the
console appends *answered by <machine>* to every answer. A seat's verb takes none; the seat's scope
decides.
decides. *Later the same day ([ADR 0160](../../02-DECISIONS/0160-the-mesh-issues-an-assignments-subjects-and-a-runtime-serves-what-it-is-issued.md)):* the console composes no
subject at all; each tool's subject comes with the listing, and a stateful module on two machines is
listed once per machine because the mesh issued it no plain subject.
**A tool that was not listed can still be called.** Listing is discovery; calling is the grant. An agent
that knows a tool's name asks for it by `<module>.<tool>` and the module answers or the bus says why not.