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:
+99
@@ -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)
|
||||||
@@ -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)
|
- **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)
|
- **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)
|
- **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
|
### Its tiers, from the bottom up
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
layer: to-be
|
layer: to-be
|
||||||
status: implemented
|
status: implemented
|
||||||
code: [mesh-controller, mesh-tools]
|
code: [mesh-controller, mesh-tools]
|
||||||
updated: 2026-09-30
|
updated: 2026-10-01
|
||||||
decisions:
|
decisions:
|
||||||
|
- 02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md
|
||||||
- 02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md
|
- 02-DECISIONS/0154-the-meshs-own-verbs-are-the-controller-seats-tools.md
|
||||||
- 02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md
|
- 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/0129-a-seat-carries-the-protocol-of-its-role.md
|
||||||
@@ -79,6 +80,14 @@ machine's holder and the holders' queue group would hand the call to whichever a
|
|||||||
node-scoped seat's tool therefore carries the node it is asked of. Nothing about a mesh-scoped seat
|
node-scoped seat's tool therefore carries the node it is asked of. Nothing about a mesh-scoped seat
|
||||||
changes.
|
changes.
|
||||||
|
|
||||||
|
*2026-10-01 ([ADR 0159](../../02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md)):*
|
||||||
|
the same shape now serves a **module's** tool on several machines, which had the queue-group fault
|
||||||
|
this section describes for seats: each instance also serves `mesh.mod.<module>.tool.<tool>.<node>`,
|
||||||
|
a caller writes `<module>.<tool>@<node>`, and every answer names the machine that gave it. And §3 is
|
||||||
|
built for every holder, not only the controller: the credential names the seats a module claims and
|
||||||
|
their verbs, the runtime serves each with the tool of the same name on the seat's subject, and the
|
||||||
|
bus admits it only where the module holds the seat.
|
||||||
|
|
||||||
## 5. Discovery
|
## 5. Discovery
|
||||||
|
|
||||||
**What a role answers is a read.** The seats and their protocols are records, so the list is a query
|
**What a role answers is a read.** The seats and their protocols are records, so the list is a query
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
layer: to-be
|
layer: to-be
|
||||||
status: implemented
|
status: implemented
|
||||||
code: [mesh-catalog, mesh-tools, mesh-controller]
|
code: [mesh-catalog, mesh-tools, mesh-controller]
|
||||||
updated: 2026-09-30
|
updated: 2026-10-01
|
||||||
decisions:
|
decisions:
|
||||||
|
- 02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md
|
||||||
- 02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md
|
- 02-DECISIONS/0152-the-operators-surface-is-a-module-the-console.md
|
||||||
- 02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md
|
- 02-DECISIONS/0132-a-seat-carries-the-tools-its-holder-must-serve.md
|
||||||
- 02-DECISIONS/0095-the-control-plane-is-the-way-to-ask-a-module.md
|
- 02-DECISIONS/0095-the-control-plane-is-the-way-to-ask-a-module.md
|
||||||
@@ -64,6 +65,12 @@ once a request nothing serves, so a module that is not running costs nothing and
|
|||||||
as not answering, rather than silently absent — *silence and success must never look alike*. The list is
|
as not answering, rather than silently absent — *silence and success must never look alike*. The list is
|
||||||
kept for a short while and refreshed, so an agent asking on every turn does not fan out on every turn.
|
kept for a short while and refreshed, so an agent asking on every turn does not fan out on every turn.
|
||||||
|
|
||||||
|
**Every module tool takes the machine to ask** (*2026-10-01*, [ADR 0159](../../02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md)):
|
||||||
|
an optional `node` the console lists on each one, puts into the subject and never hands to the module,
|
||||||
|
for a module that runs on several machines; without it whichever instance answers first does, and the
|
||||||
|
console appends *answered by <machine>* to every answer. A seat's verb takes none; the seat's scope
|
||||||
|
decides.
|
||||||
|
|
||||||
**A tool that was not listed can still be called.** Listing is discovery; calling is the grant. An agent
|
**A tool that was not listed can still be called.** Listing is discovery; calling is the grant. An agent
|
||||||
that knows a tool's name asks for it by `<module>.<tool>` and the module answers or the bus says why not.
|
that knows a tool's name asks for it by `<module>.<tool>` and the module answers or the bus says why not.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
---
|
||||||
|
status: resolved
|
||||||
|
opened: 2026-10-01
|
||||||
|
located-in: [mesh-tools src/broker-nats.ts (one subject, one queue group per module), mesh-tools src/mcp.ts (no way to name a machine), mesh-tools src/runtime.ts (a claimed seat's verbs served by nobody), mesh-controller internal/broker/nats.go (the grant for a tool named one subject)]
|
||||||
|
fixed-by: mesh-tools PR (feat/a-tool-call-names-the-machine) and mesh-controller PR (same branch) — see ADR 0159; the store seat's verbs and postgres's tools follow in the catalogue
|
||||||
|
amended-design: [03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md, 03-DESIGN/01-to-be/34-the-console.md]
|
||||||
|
---
|
||||||
|
|
||||||
|
# 182 — A tool call reaches whichever instance answers first, and a claimed seat's verbs are served by nobody
|
||||||
|
|
||||||
|
## What was observed
|
||||||
|
|
||||||
|
Asked how to list the databases of the store on one machine, the mesh had no answer. A module's tools
|
||||||
|
are served on one subject per module, `mesh.mod.<module>.tool.<name>`, and every instance of the
|
||||||
|
module joins one queue group on it, so a call to the database engine's tool while it runs on two
|
||||||
|
machines reaches whichever answered first, and the answer does not say which. There is no way to ask
|
||||||
|
the instance on one machine. The console lists the tool once and offers no machine.
|
||||||
|
|
||||||
|
And the seat half was missing too. Design 33 §3 says holding a seat means serving its tools, and
|
||||||
|
ADR 0154 built that for the controller's own seat alone. A module that holds a seat — the database
|
||||||
|
engine on the control node holding `mesh-store` — served none of the seat's verbs, because no seat
|
||||||
|
but the controller's declares any and no runtime knew which seats its module claimed.
|
||||||
|
|
||||||
|
## Why this is here
|
||||||
|
|
||||||
|
Both are the same omission: the tool surface was built as if every module ran on one machine and
|
||||||
|
held no seat. A queue group is the right default for a stateless module answering anywhere, and the
|
||||||
|
wrong only choice for a module whose instances are different things — two stores with different
|
||||||
|
databases. The architecture had the distinction: a module is a thing that runs on machines, a seat is
|
||||||
|
a role one of them holds. The tool surface did not carry it.
|
||||||
|
|
||||||
|
## Resolved, 2026-10-01
|
||||||
|
|
||||||
|
[ADR 0159](../../02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md).
|
||||||
|
Every instance serves its module's subject twice: in the queue group as before, and with its own
|
||||||
|
machine as the subject's last token. `<module>.<tool>@<node>` reaches one machine's instance; the
|
||||||
|
console lists `node` on every module tool and puts it in the subject, never in the module's
|
||||||
|
arguments; every answer carries the machine that gave it, and the console appends it as its own line.
|
||||||
|
The grant for a tool covers both subjects.
|
||||||
|
|
||||||
|
A holder's runtime serves its seat's verbs: the credential the mesh writes names the seats the module
|
||||||
|
claims and the verbs each 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 and with the machine for a node-scoped one, and
|
||||||
|
the bus admits the subscription only where the module holds the seat. The store seat's first verbs
|
||||||
|
and the database engine's tools for them are the catalogue's next step, recorded in the decision.
|
||||||
|
|
||||||
|
*How it is checked:* against a real bus, a module on two machines answers each by name and says who
|
||||||
|
answered when unnamed, and a claimant answers a seat's verb on the seat's subject; the console lists
|
||||||
|
`node` on a module's tool and not on a seat's; the grant for a tool covers both subjects; and, live,
|
||||||
|
the store's databases listed from one named machine through the console.
|
||||||
Reference in New Issue
Block a user