ADR 0121: a seat carries the protocol of its role
The mesh's own seats said who does a job and nothing about what may be said to them or by them, and that gap showed up three times in one day looking like three different problems: a build machine with three audiences for one outcome and no way to derive a grant for any of them; an event genuinely about a role with nowhere to live but the namespace of whichever module holds that role today; and a catalogue catching up on builds, where every option needed a grant the design refuses. One cause — the mesh has roles it cannot describe. So the `mesh-*` seats take the same three fields a module's seat has, and the machinery that already derives authority, queues and consumers from a declared seat does it for these too. Builds become work submitted to a role, and `mesh.build.request`, `mesh.control.built` and the BUILDS stream retire. A work queue shared by several build machines is exactly what a seat's `accepts` is, so a second mechanism for it was two places a permission could be wrong. The outcome is the seat's own event, which means one publish still reaches whoever asked, the controller that records it and the catalogue that places it — the fan-out a shared exchange gave for free, written as a subject the mesh derived rather than a topology somebody configured. That also avoids the grant that ruled out the alternatives: no holder needs permission to publish into an asker's inbox. The blocking gap is now named rather than incidental: the shared library has no way for a module to publish on a seat. The build machine is Go and reaches the bus directly, so it is unaffected; the artifact-store event waits.
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
---
|
||||
topic: the mesh
|
||||
status: accepted
|
||||
date: 2026-09-27
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0118-a-module-declares-its-own-seats.md
|
||||
---
|
||||
|
||||
# 121. A seat carries the protocol of its role
|
||||
|
||||
## Context
|
||||
|
||||
[ADR 0118](0118-a-module-declares-its-own-seats.md) let a module declare a seat with its protocol:
|
||||
what work the role accepts, what it emits, what it serves. A module's own seats work that way today.
|
||||
**The mesh's own seats — the `mesh-*` set — carry no protocol at all**, only a name, a scope and the
|
||||
provision they deliver. They say who does a job and nothing about what may be said to them or by
|
||||
them.
|
||||
|
||||
That gap surfaced three times in one day, each time as a different-looking problem.
|
||||
|
||||
**A build machine.** On the bus the mesh runs on today a builder has its own account kind, created by
|
||||
its own command, with permissions written by hand: read the build queue, write to two exchanges. One
|
||||
publish to a shared exchange reached all three audiences a finished build has — whoever asked, the
|
||||
controller that records it, and the catalogue that places it in the module graph. On a bus where
|
||||
permissions are per subject those are three separate grants, and nothing derives them, because a
|
||||
builder is not a module and holds a seat that promises nothing.
|
||||
|
||||
**An event about a role rather than about a module.** The module holding the artifact-store seat
|
||||
declared an event named after a *different* module
|
||||
([issue 127](../04-ISSUES/127-a-module-event-derives-a-subject-nothing-publishes/00-report.md)). The
|
||||
bus refuses that, because a namespace belongs to who it is named for. The event is genuinely about the
|
||||
role — "the artifact store accepted an image" — and a consumer written against whichever module holds
|
||||
that role today breaks when the holder changes. There was nowhere else to put it.
|
||||
|
||||
**A catalogue catching up.** The controller answers a request for builds it may have missed by
|
||||
re-publishing them under its own name, which no consumer of the builder's subject hears. Publishing
|
||||
them under the builder's name would be the controller signing an event as another module. Answering
|
||||
into the asker's inbox needs a grant over every inbox in the mesh, which
|
||||
[design 25](../03-DESIGN/01-to-be/25-the-bus-on-nats.md) §4 refuses.
|
||||
|
||||
Three symptoms, one cause: **the mesh has roles it cannot describe.**
|
||||
|
||||
## Decision
|
||||
|
||||
**A seat carries the protocol of its role, whether the seat is a module's or the mesh's own.** The
|
||||
`mesh-*` set gains the same three fields a declared seat has — what it accepts, what it emits, what it
|
||||
serves — and the holder's authority, its work queue and its consumers are derived from them by the
|
||||
machinery that already does this for a module's seats.
|
||||
|
||||
**Builds become work submitted to a role.** The build machine seat accepts a build and emits an
|
||||
outcome. The dedicated `mesh.build.*` branch and the stream behind it retire: a work queue shared by
|
||||
several build machines is exactly what a seat's `accepts` already is, and keeping a second mechanism
|
||||
for it means two things to reason about and two places for a permission to be wrong.
|
||||
|
||||
**One publish still reaches three audiences, and now the mesh derived the subject.** A build's outcome
|
||||
is the seat's own event. Whoever asked matches it by the id their request carried; the controller
|
||||
records it; the catalogue places it. That is the fan-out the shared exchange gave for free, expressed
|
||||
as a subject rather than as a topology, and it means no holder needs permission to publish into
|
||||
anybody's inbox.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**A dedicated principal kind for a builder**, mirroring the account the old bus issues it. Smaller: one
|
||||
addition to the composer, no change to seats, and it matches how a builder is treated today. Not taken
|
||||
because it answers one of the three symptoms and leaves the other two, and because "the builder is
|
||||
special" is a claim nobody could justify from the design — a build machine is a role the mesh has, and
|
||||
the mesh has a word for a role.
|
||||
|
||||
**Leaving the outcome as a reply to the asker's inbox.** Rejected on authority: a holder able to answer
|
||||
any asker needs a grant across the whole inbox space, which is the one grant design 25 §4 refuses by
|
||||
name. The seat's event costs the asker a filter and costs the mesh nothing.
|
||||
|
||||
## Consequences
|
||||
|
||||
**A seat is now the mesh's unit of "a role that talks".** A role that accepts work, announces outcomes
|
||||
or answers questions says so where it is defined, and everything about permissions, queues and
|
||||
consumers follows. Nothing hand-writes a grant for a role again.
|
||||
|
||||
**The shared library cannot yet publish on a seat, and that is now the blocking gap rather than a
|
||||
curiosity.** A module holding a seat has the authority and no way to use it; the build machine is
|
||||
written in Go and reaches the bus directly, so it is unaffected, but the artifact-store event stays
|
||||
under its module's own name until the library has a surface for this. That is a task, and this record
|
||||
is what makes it one.
|
||||
|
||||
**A second mechanism disappears.** `mesh.build.*`, the BUILDS stream and the builder's hand-written
|
||||
account all retire. Fewer things, and the ones left are derived.
|
||||
|
||||
**The catch-up question is not settled by this**, only made answerable: a seat that serves something
|
||||
gives the controller a way to be asked, which the mesh did not have. Whether catch-up should be a
|
||||
question at all remains open.
|
||||
@@ -136,6 +136,7 @@ python3 00-META/checks/index.py fail if stale
|
||||
- **0117** — [The bus is the only broker](0117-the-bus-is-the-only-broker.md) *(superseded)*
|
||||
- **0119** — [AMQP is a provision, not the bus](0119-amqp-is-a-provision-not-the-bus.md)
|
||||
- **0120** — [The mesh bus is required, not ambient](0120-the-mesh-bus-is-required-not-ambient.md)
|
||||
- **0121** — [A seat carries the protocol of its role](0121-a-seat-carries-the-protocol-of-its-role.md)
|
||||
|
||||
### Its tiers, from the bottom up
|
||||
|
||||
|
||||
Reference in New Issue
Block a user