A seat's verb keeps a node of its own; only a module's tool gives it to the subject

The console moved every call's node into the subject (ADR 0159), so mesh-controller.push {node: x}
became a call to the seat's verb on machine x, which nothing serves — the mesh's own verbs could not
be given a machine from the console at all. A role's verb takes no machine from the console; its
arguments are its own.
This commit is contained in:
2026-10-01 15:42:35 +02:00
parent 56f82588af
commit 670b486ac0
2 changed files with 29 additions and 8 deletions
+10 -6
View File
@@ -15,7 +15,7 @@
*/
import type { Broker } from "@novox/mesh-sdk/messaging";
import { callTool, seatsIn, toolsOn, whyItFailed, type Listing, type Seats } from "./client.js";
import { callTool, seatsIn, toolKey, toolsOn, whyItFailed, type Listing, type Seats } from "./client.js";
/** The protocol version this speaks. Stated, because a host that wants another should be told so
* rather than discovering it through a shape it did not expect. */
@@ -134,13 +134,17 @@ export function mcpSurface(bus: Broker, who: string): Surface {
const given = String(request.params?.name ?? "");
const args = { ...((request.params?.arguments as Record<string, unknown> | undefined) ?? {}) };
// The machine, when the caller names one, travels in the subject and never reaches the
// module's arguments (novox/hq ADR 0159).
const node = typeof args.node === "string" && args.node !== "" ? args.node : "";
delete args.node;
// module's arguments (novox/hq ADR 0159) — for a module's tool. A seat's verb takes no
// machine from the console (the seat's scope decides), so a `node` among its arguments
// is the verb's own, as `push` and `assign` take one, and is handed through untouched.
const have = await listing().catch(() => undefined);
const roles = have && seatsIn(have);
const isSeatVerb = roles ? toolKey(given.split("@", 1)[0], roles).startsWith("seat:") : false;
const node = !isSeatVerb && typeof args.node === "string" && args.node !== "" ? args.node : "";
if (!isSeatVerb) delete args.node;
const name = node && !given.includes("@") ? `${given}@${node}` : given;
try {
const have = await listing().catch(() => undefined);
const { result, node: answeredBy } = await callTool(bus, name, args, have && seatsIn(have), have);
const { result, node: answeredBy } = await callTool(bus, name, args, roles, have);
// Text, because that is what every host renders. The content is the module's answer
// as JSON, unshaped: an adapter that flattened it would be deciding what matters in
// somebody else's answer. Which machine answered follows it as its own line.