A role's tools belong to the role, not to whichever module holds it today: the seat declares them with their schemas, serving them is a condition of occupying the seat, and what the mesh can do becomes a read of its own records rather than a question nothing answers. A module keeps its own tools — the same module may run without the seat, and then only its own name is true. Design 33 follows: the three families, addressing a node-scoped seat, discovery, and what serves this to an agent.
151 lines
9.9 KiB
Markdown
151 lines
9.9 KiB
Markdown
---
|
|
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
|