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.
138 lines
8.0 KiB
Markdown
138 lines
8.0 KiB
Markdown
---
|
|
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
|