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:
2026-09-27 15:24:17 +02:00
parent c4a8455e2e
commit c8f430290e
4 changed files with 125 additions and 5 deletions
@@ -11,6 +11,7 @@ decisions:
- 02-DECISIONS/0041-events-are-a-relationship.md
- 02-DECISIONS/0112-a-module-definition-names-no-node-mesh-or-path.md
- 02-DECISIONS/0083-one-push-leaves-the-mesh-consistent.md
- 02-DECISIONS/0121-a-seat-carries-the-protocol-of-its-role.md
---
# 29. What a module declares, and what the bus makes of it
@@ -130,6 +131,25 @@ seat. The module does not name them, does not know their names, and cannot misco
and the controller stays the only writer of stream and consumer definitions
([design 25](25-the-bus-on-nats.md) §3).
**The mesh's own seats carry protocol too.** *Added 2026-09-27,
[ADR 0121](../../02-DECISIONS/0121-a-seat-carries-the-protocol-of-its-role.md).* A seat declared by a
module says what it accepts, emits and serves; the `mesh-*` set said only who does a job. So the mesh
had roles it could not describe — a build machine with three audiences for one outcome and no way to
derive a grant for any of them, and an event genuinely about a role with nowhere to live but the
namespace of whichever module happens to hold it. The mesh's seats now take the same three fields, and
the same machinery derives the holder's authority, its work queue and its consumers.
**So a build is work submitted to a role, like any other.** The build machine seat accepts a build and
emits an outcome, and the dedicated branch that carried builds retires: a work queue shared by several
machines is exactly what `accepts` already is, and a second mechanism for it is two places a permission
can be wrong.
**And one publish reaches three audiences without anybody's inbox being opened.** 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 in the graph. That is the fan-out a shared exchange gave for free, written
as a subject the mesh derived instead of a topology somebody configured — and it is why a holder needs
no permission to publish into an asker's inbox, which is the one grant design 25 §4 refuses by name.
**Retention belongs to whoever owns the namespace, not to a consumer.** A seat declares how long
its inbound backlog survives, because that is a property of the service: