ADR 0132: a seat carries the tools its holder must serve #158
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user