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.
92 lines
5.2 KiB
Markdown
92 lines
5.2 KiB
Markdown
---
|
|
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.
|