ADR 0160: the mesh issues an assignment's subjects, and a runtime serves what it is issued #249
+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)
|
- **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)
|
- **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)
|
- **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
|
### Its tiers, from the bottom up
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ code:
|
|||||||
- mesh-sdk src (the protocol's NATS binding, step 3)
|
- mesh-sdk src (the protocol's NATS binding, step 3)
|
||||||
updated: 2026-10-01
|
updated: 2026-10-01
|
||||||
decisions:
|
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/0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md
|
||||||
- 02-DECISIONS/0106-the-bus-is-nats.md
|
- 02-DECISIONS/0106-the-bus-is-nats.md
|
||||||
- 02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.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>.event.<verb> a role's own event (JetStream: EVENTS)
|
||||||
mesh.seat.<seat>.tool.<verb> a role's tool (core request/reply)
|
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.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)):
|
**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.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
|
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
|
- mesh-catalog modules/mesh-catalog
|
||||||
updated: 2026-09-28
|
updated: 2026-09-28
|
||||||
decisions:
|
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/0126-a-module-declares-its-own-seats.md
|
||||||
- 02-DECISIONS/0131-everything-on-the-mesh-speaks-to-the-broker-seat.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
|
- 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
|
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.
|
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
|
## 2. Three namespaces, and nothing else
|
||||||
|
|
||||||
**Its own** — `mesh.mod.<module>.>`. Its events and its tools. Nothing else may publish into it,
|
**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]
|
code: [mesh-controller, mesh-tools]
|
||||||
updated: 2026-10-01
|
updated: 2026-10-01
|
||||||
decisions:
|
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/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/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md
|
||||||
- 02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.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
|
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
|
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
|
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
|
## 5. Discovery
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ status: implemented
|
|||||||
code: [mesh-catalog, mesh-tools, mesh-controller]
|
code: [mesh-catalog, mesh-tools, mesh-controller]
|
||||||
updated: 2026-10-01
|
updated: 2026-10-01
|
||||||
decisions:
|
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/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/0152-the-operators-surface-is-a-module-the-console.md
|
||||||
- 02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.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,
|
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
|
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
|
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
|
**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.
|
that knows a tool's name asks for it by `<module>.<tool>` and the module answers or the bus says why not.
|
||||||
|
|||||||
Reference in New Issue
Block a user