From 0acb47fa55dfc57be937e3129058cd9941d6aaf6 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 1 Oct 2026 14:00:23 +0200 Subject: [PATCH] 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 --- ...ine-and-a-holder-serves-its-seats-verbs.md | 99 +++++++++++++++++++ 02-DECISIONS/README.md | 1 + .../01-to-be/33-the-tools-the-mesh-answers.md | 11 ++- 03-DESIGN/01-to-be/34-the-console.md | 9 +- .../00-report.md | 50 ++++++++++ 5 files changed, 168 insertions(+), 2 deletions(-) create mode 100644 02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md create mode 100644 04-ISSUES/182-a-tool-call-reaches-whichever-instance-answers-first/00-report.md diff --git a/02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md b/02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md new file mode 100644 index 0000000..d48452d --- /dev/null +++ b/02-DECISIONS/0159-a-tool-call-names-the-machine-and-a-holder-serves-its-seats-verbs.md @@ -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..tool.` in the queue group, and + `mesh.mod..tool..` served by the instance on that machine alone. In the + caller's words, `.@`. 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 * 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: [.]` 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) diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index 69fc022..f392b0c 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.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) - **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 diff --git a/03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md b/03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md index e1215ac..d759e58 100644 --- a/03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md +++ b/03-DESIGN/01-to-be/33-the-tools-the-mesh-answers.md @@ -2,8 +2,9 @@ layer: to-be status: implemented code: [mesh-controller, mesh-tools] -updated: 2026-09-30 +updated: 2026-10-01 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/0132-a-seat-carries-the-tools-its-holder-must-serve.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 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..tool..`, +a caller writes `.@`, 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 **What a role answers is a read.** The seats and their protocols are records, so the list is a query diff --git a/03-DESIGN/01-to-be/34-the-console.md b/03-DESIGN/01-to-be/34-the-console.md index e6e08d1..172e38c 100644 --- a/03-DESIGN/01-to-be/34-the-console.md +++ b/03-DESIGN/01-to-be/34-the-console.md @@ -2,8 +2,9 @@ layer: to-be status: implemented code: [mesh-catalog, mesh-tools, mesh-controller] -updated: 2026-09-30 +updated: 2026-10-01 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/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 @@ -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 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 * 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 that knows a tool's name asks for it by `.` and the module answers or the bus says why not. diff --git a/04-ISSUES/182-a-tool-call-reaches-whichever-instance-answers-first/00-report.md b/04-ISSUES/182-a-tool-call-reaches-whichever-instance-answers-first/00-report.md new file mode 100644 index 0000000..5b7418d --- /dev/null +++ b/04-ISSUES/182-a-tool-call-reaches-whichever-instance-answers-first/00-report.md @@ -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..tool.`, 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. `.@` 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.