Files
hq/02-DECISIONS/0129-a-seat-carries-the-protocol-of-its-role.md
jschoubben ce6ae943b7 Merge main: renumber this branch's records around the trunk's
Both lines of work numbered from the same point, so four decision records and one design
document existed twice with different content. The trunk keeps its numbers and this branch
yields — the only rule that scales, because the trunk's are already cited by what merged
before them.

  0117 the bus is the only broker        -> 0125
  0118 a module declares its own seats   -> 0126
  0119 amqp is a provision, not the bus  -> 0127
  0120 the mesh bus is required          -> 0128
  0123 a seat carries its role's protocol -> 0129
  0124 the predecessor is ending          -> 0130
  design 29, what a module declares       -> design 32

Applied to the code repositories too, because a stale reference is worse when numbers
collide than when they dangle: the reader lands on a real record that decided something
else.

Two reconciliations the merge forced, both real:

**0110 was marked wholly superseded and was not.** Its successor says in as many words that
everything 0110 decided about what a seat *is* stands untouched — and two records that
landed on the trunk rest on exactly that part. So it is accepted again, extended rather than
replaced, with a note saying which of its claims moved and where.

**A seat's protocol becomes columns, not fields.** The trunk moved the seat set out of
compiled code into a table the controller owns. This branch had added what a role accepts,
emits and serves to the Go slice. The decision is unaffected and the mechanism is better for
it: giving a role a protocol is now a write rather than a rebuild, which is the trunk's own
argument applied to what this branch added.

One check still fails and it fails on main too: a record resting on ADR 0112 while that is
still 'proposed'. Left alone — it is not this merge's to answer.
2026-09-27 18:23:41 +02:00

5.9 KiB

topic, status, date, deciders, reconstructed, extends
topic status date deciders reconstructed extends
the mesh accepted 2026-09-27 jochen false 02-DECISIONS/0122-a-seat-is-data-a-rename-is-a-database-update.md

129. A seat carries the protocol of its role

Context

ADR 0126 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). 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 §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.

Reconciled with 0122, which landed in parallel

Added 2026-09-27, on merging. ADR 0122 moved the seat set out of compiled code and into a table the controller owns. This record was written against the slice, and says the mesh-* set "gains the same three fields a declared seat has".

The decision is unaffected and the mechanism is better for it. What a seat accepts, emits and serves becomes three columns beside its name and scope, so giving a role a protocol is a write rather than a rebuild — which is the whole argument of 0122 applied to the thing this record adds. Where this text says the set gains fields, read: the table gains columns.

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.