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:
+110
@@ -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)
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user