ADR 0159: a tool call names the machine, every answer says which answered, a holder's runtime serves its seat's verbs; issue 182; designs 33 and 34

This commit is contained in:
2026-10-01 14:00:23 +02:00
parent 7a633f2780
commit 0acb47fa55
5 changed files with 168 additions and 2 deletions
@@ -0,0 +1,99 @@
---
topic: the mesh
status: accepted
date: 2026-10-01
deciders: jochen
reconstructed: false
extends: 02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md
---
# 159. A tool call names the machine it is for, every answer says which machine answered, and a holder's runtime serves its seat's verbs
## Context
[ADR 0152](0152-the-operators-surface-is-a-module-the-console.md) made a module's tools subjects on
the bus and the console the place a person reaches them. [ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md)
made the mesh's own verbs the controller seat's tools, served by the controller. Design 33 said what
a seat's tools are, that holding a seat means serving them, and that a node-scoped seat's verb
carries the machine.
What was built stopped short in two places ([issue 182](../04-ISSUES/182-a-tool-call-reaches-whichever-instance-answers-first/00-report.md)).
A module's tools were one subject per module in one queue group, so with the database engine on two
machines a call reached whichever instance answered first, unnamed, and nobody could ask one machine's.
And no module served the verbs of a seat it held: the runtime did not know which seats its module
claimed, and no seat but the controller's declared verbs. The operator named it: a tool call must be
able to say *the store on the control node*, and the engine holding the store seat must serve the
store's tools as well as its own.
## Considered Options
1. **Leave the queue group and ask the controller which machine answered.** Nothing changes on the
bus; a caller cannot choose, only learn afterwards. Useless for the question that was asked.
2. **A subject per machine instead of one per module.** Every call names a machine; a stateless
module on three machines loses the one-of-them answer a queue group gives for free, and every
caller has to know where things run.
3. **Both subjects, and the machine in every answer.** An instance serves its module's subject in the
queue group as before, and the same subject with its machine as the last token. A caller that
names no machine gets one instance and is told which; a caller that names one gets that one. The
grant for a tool covers both. And a holder's runtime serves its seat's verbs by the same means,
from what the credential tells it.
## Decision
**Option 3.**
- **Two subjects per tool, one default.** `mesh.mod.<module>.tool.<tool>` in the queue group, and
`mesh.mod.<module>.tool.<tool>.<node>` served by the instance on that machine alone. In the
caller's words, `<module>.<tool>@<node>`. A runtime that does not know its machine serves only the
first, which is what it always did.
- **Every answer says which machine answered.** The reply carries the node; the console appends
*answered by <node>* as its own line after the module's unshaped answer, and `mesh call` prints it.
An answer from a module on several machines is never an answer from nowhere.
- **The console offers the machine on every module tool** as an optional `node` argument, lists it,
strips it into the subject and never passes it to the module. A seat's verb takes none: the seat's
scope decides where it is served.
- **The grant covers both subjects.** `invokes: [<module>.<tool>]` permits the plain subject and the
machine-addressed one; `*` already permitted everything beneath `tool`.
- **A holder's runtime serves its seat's verbs.** The broker credential the mesh writes names the
seats the module claims and, for each, its scope and the verbs the seat promises. The runtime
serves each verb with the module's tool of the same name on the seat's own subject — flat for a
mesh seat, with the machine for a node-scoped one — and the bus admits that subscription only
where the module holds the seat, because the holder's grant is composed from the holding. A
claimant that does not hold the seat here is refused the subscription and serves nothing. A
claimant missing a tool a seat promises is already refused at registration (design 33 §3).
- **The store seat's first verbs**, so the operator's question has an answer: `databases`, every
database the store holds with its owner and size, and `query`, one read-only statement against one
database. The database engine serves both as tools of those names and lists them in its definition.
Which verbs a seat serves is a decision per seat and binds every holder; these two are the smallest
set that makes the store askable.
## Consequences
- *List the databases of the store on the control node* is `mesh-store.databases` through the seat,
answered by its holder wherever it sits, or `postgres.databases@novox` through the module on one
named machine. Both say who answered.
- Every module's runtime serves one more subscription per tool and, for a claimant, one per promised
verb. No manifest changes for the per-machine half; the seat half needs each holder's definition to
list the seat's verbs among its tools, which registration already demands.
- The runtime change reaches a module when the module is rebuilt on the new runtime image; until
then that module answers only on its plain subject, and a call naming its machine is refused as
unserved, in words that say so.
- The credential gains `claims`; a module issued before this carries none and serves no seat verb
until it is issued again. `rollout mint` for the holders is the one-time cost.
## How this is checked
| Rule | Checked by |
|---|---|
| A call naming a machine reaches that machine's instance; an unnamed call reaches one and says which | mesh-tools, against a real bus: a module on two machines |
| A claimant serves a seat's verb on the seat's subject, and the answer names the machine | the same test |
| The console lists `node` on a module's tool and not on a seat's verb, and the answer carries *answered by* | mesh-tools, the MCP conformance test |
| The grant for a tool covers the plain and the machine-addressed subject | `TestInvokingAToolMayAddressTheMachineToo` (controller) |
| Live: the store's databases listed from the control node by name through the console, and through the store seat | done by hand after the roll-out and the catalogue's step |
## References
- [ADR 0152](0152-the-operators-surface-is-a-module-the-console.md) — extended: the surface carries the machine
- [ADR 0154](0154-the-meshs-own-verbs-are-the-controller-seats-tools.md), [ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md) — the seat half, now for every holder
- [Design 33 — The tools the mesh answers](../03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md) §3, §4; [Design 34 — The console](../03-DESIGN/01-to-be/34-the-console.md) §3
- [Issue 182](../04-ISSUES/182-a-tool-call-reaches-whichever-instance-answers-first/00-report.md)
+1
View File
@@ -172,6 +172,7 @@ python3 00-META/checks/index.py fail if stale
- **0156** — [An artifact is what a build produces, the artifact store serves every kind, and its seat is named for its scope](0156-an-artifact-is-what-a-build-produces-and-the-store-is-named-for-its-scope.md)
- **0157** — [A build says what it does on the bus, as it happens](0157-a-build-says-what-it-does-on-the-bus-as-it-happens.md)
- **0158** — [A provider with one credential shares it with every consumer, and the vault remakes it for all of them at once](0158-a-provider-with-one-credential-shares-it-with-every-consumer.md)
- **0159** — [A tool call names the machine it is for, every answer says which machine answered, and a holder's runtime serves its seat's verbs](0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md)
### Its tiers, from the bottom up