ADR 0132: a seat carries the tools its holder must serve #158

Merged
mesh-admin merged 1 commits from decision/0132-a-seat-carries-the-tools-its-holder-must-serve into main 2026-09-28 08:17:02 +00:00
3 changed files with 288 additions and 0 deletions
@@ -0,0 +1,150 @@
---
topic: the mesh
status: accepted
date: 2026-09-28
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0129-a-seat-carries-the-protocol-of-its-role.md
---
# 132. A seat carries the tools its holder must serve
## Context
[ADR 0129](0129-a-seat-carries-the-protocol-of-its-role.md) gave a seat the protocol of its role in
three parts: the work it accepts, the events it emits, and the verbs it **serves** — request and
reply, awaited. The bus already derives authority from all three: a holder subscribes
`mesh.seat.<seat>.tool.<verb>`, and a module that uses the seat may publish it and nothing else.
**The serving third has never been used.** The mesh defines 14 seats, 8 mesh-scoped and 6
node-scoped. Exactly one carries a protocol at all — the build machine, which accepts `build` and
emits `built`. Not one seat declares a single verb it serves. The mechanism is built, enforced, and
empty.
Meanwhile every tool on the mesh is addressed to a module. Of 72 modules in the catalogue, 45 serve
tools, about 203 of them, each on `mesh.mod.<module>.tool.<name>`. So a caller binds to the module
that happens to hold a role rather than to the role, and replacing that module breaks every caller —
which is the thing seats exist to prevent everywhere else.
**Nothing can say what tools exist.** Measured on 2026-09-28, with the bus carrying the whole mesh: a
workstation client holding an operator credential connected, the bus accepted the account, and
`mesh call gitea.gitea_list_repos` answered with real repositories. The same client's `mesh tools`
found nothing, because it asks `mesh-catalog.catalog_tools` and no module serves that: the catalogue
serves `catalog_modules`, `catalog_module`, `catalog_provides`, `catalog_dependents` and
`catalog_stale`. An agent can therefore call any tool it already knows the name of and discover none.
MCP's `tools/list` is that same question, so the MCP surface is a working transport over an empty
catalogue.
**And there is nowhere for a tool's definition to live.** A manifest has a `tools` field: 0 of the 45
modules that serve tools fill it. That is not neglect, it is the arrangement failing — the field was
the bus grant's source for what a module may subscribe, and because nothing filled it every module
that served a tool was refused its own subscription on the new bus, live, until the grant was changed
to the module's own namespace. Today a tool's name, description and argument schema exist only in the
module's code.
Two facts about the machinery matter for what follows. A seat's protocol is not in the store: the seat
rows lack the ADR 0129 columns, so the protocol comes from compiled defaults and is merged in when a
row is read. And `seatSubject` is flat — `mesh.seat.<seat>.<kind>.<verb>` with no node in it — so a
node-scoped seat's tool call would reach every node's holder at once, and the holders' queue group
would hand it to whichever answered first.
## Decision
**A seat's protocol carries its tools in full**: the verb, what it does, and the schema of its
arguments and of its answer. The seat is the definition of the role's interface; the holder is an
implementation of it.
**Serving the seat's tools is a condition of holding the seat.** A module that does not serve every
verb the seat declares may not occupy it. This is checked where the other conditions of holding are
checked — registration and handover — and refused by naming the verbs that are missing.
**A role's tools are addressed to the role.** `mesh.seat.<seat>.tool.<verb>` mesh-wide. A node-scoped
seat carries the node in the address, because one subject reaching six machines' holders is not an
address, and the queue group that made it look like one would silently pick a winner.
**A module keeps its own tools, and both exist.** `gitea_list_repos` stays, because gitea can run
without holding the `git` seat — a second forge, an instance kept for one purpose. The module's name
answers *this gitea*; the seat's verb answers *whoever is the forge*. Which of the two a caller wants
is a decision in the running session, not one the mesh makes for it.
**What answers "what tools exist" follows where the definition lives.** A seat's tools are read from
the mesh's own records. A module's own tools are answered by the module, from the code that defines
them. Discovery is therefore a read for the durable half and a question to the running mesh for the
free half.
**A seat's tools are an interface, and change like one.** Additive within a version; a change that
would break a caller takes the version token the subject already has room for (design 29 §8), and the
two run side by side until nothing is bound to the old one.
**The mesh's own verbs are the `mesh-controller` seat's tools.** `status`, `push`, `build`, `assign`
and the rest are a role's interface, not a container's, and the audit point [ADR 0095](0095-the-control-plane-is-the-way-to-ask-a-module.md)
asks for is the seat's holder.
## Options considered
1. **The manifest declares each module's tools.** Rejected. The list is then written twice — in the
manifest and in the code — and a schema in a manifest goes stale silently, which is the worst kind
of wrong for something an agent reads to decide what to call. It is also the arrangement that has
already failed once: the field exists, 0 of 45 modules fill it, and the grant that depended on it
refused every tool subscription on the mesh.
2. **Every runtime answers an introspection call, and something aggregates them.** Rejected as the
shape for a role's tools, kept for a module's own. An aggregator needs permission to publish into
every module's namespace, which is a widening the mesh otherwise gives only to the control plane;
and the answer is only as available as the modules are, so a mesh whose catalogue cannot say what a
role answers while its holder is down cannot plan against it.
3. **The control plane answers everything.** Rejected. It puts a tool surface on the control plane for
tools it does not implement, and makes discovery depend on the one component that must stay
answerable while it is itself being replaced. The mesh's own verbs are its to answer, and it answers
them as the holder of a seat.
4. **Seats only; no module tools.** Rejected. Most modules hold no seat, and inventing a seat per
module to give its tools a home would dilute what a seat is: one holder of a role the mesh needs
exactly one of.
## Consequences
**One capability can have two names, deliberately.** A forge that holds the `git` seat answers both
`mesh.seat.git.tool.list_repos` and `mesh.mod.gitea.tool.gitea_list_repos`. This is the one place the
mesh accepts two names for one thing, because they are answers to different questions and the second
one survives the module not holding the seat. The glossary rule stands everywhere else.
**A seat becomes a contract to implement.** Adding a verb to a seat is a change every holder must
make, and a claim that was valid becomes invalid until it does. That is the point, and it is also the
reason a seat's tools should be few and durable while a module's own stay free.
**Three prerequisites, none of them in place.** The seat's protocol must be in the store rather than in
compiled defaults, or discovery reads a binary rather than the mesh. The protocol must become richer
than a list of verbs, because a verb without a schema is not something an agent can call. And a
node-scoped seat needs the node in its subject before any of its tools can exist.
**Discovery becomes cheap for the half that matters.** What roles the mesh has and what each answers is
a query, with no fan-out and nothing to be up. An agent's authority can then be role-shaped — *the
forge's tools* — rather than a list of module-specific names that changes when a module is replaced.
**The MCP surface belongs inside the mesh.** Once the tools are the mesh's own records, the thing that
serves them to an agent is a module the mesh assigns to the machine where the agent sits, with a
credential the mesh minted and authority derived from what it may call — not a program started by hand
with a credential printed to a terminal.
## How this is checked
- **Holding is refused without the verbs.** The condition sits with the other conditions of holding a
seat, so registration and a handover both refuse a module that does not serve what the seat declares,
and the refusal names the missing verbs. A test per condition, as the other seat conditions have.
- **The grant is derived from the seat, and already is.** A holder's subscription and a user's publish
come from the seat's protocol, so a verb nobody declared is a subject nobody may use, and a verb the
seat declares reaches exactly its holder. The golden composition of the bus's user list is the test
that keeps it honest.
- **Discovery is a read, and is tested as one.** What the mesh answers for a seat's tools equals what
the seat's records declare — no call to a module in the path, so the test needs no running module.
- **A node-scoped seat's subject carries its node**, checked by the same test that checks the subject
table: two nodes holding one node-scoped seat derive two addresses.
## References
- [ADR 0129](0129-a-seat-carries-the-protocol-of-its-role.md) — the protocol this widens
- [ADR 0121](0121-a-system-seat-is-named-for-its-scope-and-modules-define-their-own.md) — the role, its work and its events
- [ADR 0095](0095-the-control-plane-is-the-way-to-ask-a-module.md) — a tool call passes one process where an audit belongs
- [ADR 0126](0126-a-module-declares-its-own-seats.md) — an event is addressed to its emitter, for the same reason a role's verb is addressed to its role
- [`03-DESIGN/01-to-be/26-the-seats.md`](../03-DESIGN/01-to-be/26-the-seats.md) — how a seat is held and handed over
- mesh-controller #116, #117, #118 — the grants as they now stand: a module serves its own namespace, the control plane may ask any tool
- Measured 2026-09-28 on the live mesh: an operator credential calling a module's tool over the bus answers; `tools/list` finds nothing
+1
View File
@@ -140,6 +140,7 @@ python3 00-META/checks/index.py fail if stale
- **0129** — [A seat carries the protocol of its role](0129-a-seat-carries-the-protocol-of-its-role.md)
- **0130** — [The predecessor is ending, and its broker goes with it](0130-the-predecessor-is-ending-and-its-broker-goes-with-it.md)
- **0131** — [Everything on the mesh speaks to the broker seat, and AMQP is not a provision](0131-everything-on-the-mesh-speaks-to-the-broker-seat.md)
- **0132** — [A seat carries the tools its holder must serve](0132-a-seat-carries-the-tools-its-holder-must-serve.md)
### Its tiers, from the bottom up
@@ -0,0 +1,137 @@
---
layer: to-be
status: designed
code: []
updated: 2026-09-28
decisions:
- 02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md
- 02-DECISIONS/0129-a-seat-carries-the-protocol-of-its-role.md
- 02-DECISIONS/0095-the-control-plane-is-the-way-to-ask-a-module.md
---
# 33 — The tools the mesh answers
**An agent can call the mesh's tools and cannot find out what they are.** Both halves were measured
on the live mesh on 2026-09-28: a client holding an operator credential connected to the bus, asked a
module for its repositories and got them; the same client's request for the tool list found nothing
serving it. The transport works, the account model works, the adapter that speaks the agent protocol
works. What is missing is the mesh being able to say what it can do.
This design is the answer to that question, and it has three families in it, because a tool belongs to
whoever is accountable for answering it.
## 1. Three families, and why the split is not arbitrary
| Family | Addressed to | Where the definition lives | Example |
|---|---|---|---|
| A **role's** tools | the seat: `mesh.seat.<seat>.tool.<verb>` | the seat's protocol, in the mesh's records | ask *the forge* to list its repositories |
| A **module's** tools | the module: `mesh.mod.<module>.tool.<name>` | that module's code | ask *this gitea* for `gitea_list_repos` |
| The **mesh's** own verbs | the `mesh-controller` seat | the seat's protocol, as above | `status`, `push`, `build`, `assign` |
The split follows accountability. A role is something the mesh guarantees exactly one holder of, so
what the role answers is the mesh's to define and a holder's to implement
([ADR 0132](../../02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md)). A module's
own tools are nobody's business but the module's, and their definitions live where they are
implemented, because a copy kept anywhere else drifts from the code that answers.
The mesh's own verbs are the third family only in where they come from, not in kind: the control plane
holds a seat like anything else, and its tools are that seat's. This is what keeps them addressable
while the control plane is being replaced, which is the moment they are most needed.
**Both names for one capability is deliberate and bounded to this.** A forge holding the `git` seat
answers the role's `list_repos` and its own `gitea_list_repos`, because the same module may run
without the seat — a second instance, kept for one purpose — and then only the second name is true.
The caller chooses which question it is asking. Nothing else in the mesh gets two names.
## 2. What a seat's tool is
A verb, what it does, and the schema of its arguments and its answer. A name alone is not callable by
something that has never seen the mesh before, which is the whole population this surface exists for.
The protocol a seat carries today is three lists of bare verbs
([ADR 0129](../../02-DECISIONS/0129-a-seat-carries-the-protocol-of-its-role.md)), and it must widen to
carry the rest. Two constraints on that widening:
- **It lives in the mesh's records, not in the control plane's binary.** Today a seat's protocol comes
from compiled defaults, merged in as a row is read, because the seat rows never gained the columns.
Discovery that reads a binary is discovery that disagrees with the mesh the moment the two are on
different versions.
- **The schema is stated in the form an agent protocol already uses**, so nothing translates between a
seat's idea of an argument and the caller's. A translation layer would be a second definition of
what a tool is.
## 3. Holding a seat means serving its tools
A module may not occupy a seat unless it serves every verb that seat declares. This joins the
conditions of holding that already exist — providing what the seat delivers, being assigned at the
seat's scope — and is refused the same way: at registration and at handover, naming the verbs that are
missing rather than the fact that something is.
A module knows which seats it claims, so knowing which tools it must serve is not a discovery problem
for the module: the seat says, the module implements, and anything beyond that is its own.
## 4. Addressing a node-scoped seat
A seat's subject is flat today — `mesh.seat.<seat>.<kind>.<verb>` — which is correct for a seat the
mesh has one holder of and wrong for the six node-scoped seats, where one subject would reach every
machine's holder and the holders' queue group would hand the call to whichever answered first. A
node-scoped seat's tool therefore carries the node it is asked of. Nothing about a mesh-scoped seat
changes.
## 5. Discovery
**What a role answers is a read.** The seats and their protocols are records, so the list is a query
against the mesh's own store: no call to a module in the path, nothing that has to be running, and an
answer that stays true while a holder is restarting or being replaced.
**What a module answers comes from the module.** Its definitions live in its code, so it is asked, and
the answer is as available as the module is — which is the right coupling for a tool that only exists
while that module does.
A caller therefore gets one list assembled from two sources, and the difference is visible in it: a
role's tool names a seat, a module's names a module. An agent that wants to survive a holder being
replaced binds to the first.
## 6. What serves this to an agent
A module the mesh assigns to the machine where the agent runs, holding a credential the mesh minted,
with authority derived from what it may call — not a program started by hand with a credential printed
to a terminal. The adapter itself already exists and is thin by design; what changes is that it stops
being something a person carries and becomes something the mesh runs, on a node, like everything else.
An agent's authority can then be role-shaped: *the forge's tools*, rather than a list of
module-specific names that changes the day the forge is replaced.
## 7. Versioning
A seat's tools are an interface and change like one. Additive within a version. A change that would
break a caller takes the version token the subject already has room for, and the two versions run side
by side until nothing is bound to the old one.
## How it is checked
- **A holder missing a verb cannot take the seat.** One test per condition of holding, as the existing
conditions have, and the live refusal names the verbs.
- **A verb nobody declared is a subject nobody may use.** The bus grants are derived from the seat's
protocol already, and the golden composition of the user list is what keeps that honest: a holder is
granted exactly the seat's verbs, a user of the seat exactly the publish side.
- **Discovery needs no running module.** The test for a role's tools reads records and asserts the
answer equals what the seats declare — if it needed a module up, it would not be a read.
- **Two nodes holding one node-scoped seat derive two addresses.** Checked by the same test as the rest
of the subject table.
## What this does not settle
- Which verbs each seat should serve. That is a decision per seat, and the reason to do it slowly: a
seat's tools bind every future holder.
- Whether a module's own tool definitions should also be recorded when a build resolves its manifest.
There is an argument for it — the mesh could then answer for a module that is down — and an argument
against, which is that a recorded copy of a live definition is a copy that can be wrong.
## References
- [ADR 0132](../../02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md) — the decision this designs
- [ADR 0129](../../02-DECISIONS/0129-a-seat-carries-the-protocol-of-its-role.md) — a seat carries the protocol of its role
- [ADR 0095](../../02-DECISIONS/0095-the-control-plane-is-the-way-to-ask-a-module.md) — a tool call passes one process where an audit belongs
- [`26-the-seats.md`](26-the-seats.md) — what a seat is, how it is held and handed over
- [`25-the-bus-on-nats.md`](25-the-bus-on-nats.md) §7 — a person's account, their inbox, and the adapter