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
+19 -2
View File
@@ -30,9 +30,13 @@ async function aMeshAndACredential(t: { after: (fn: () => Promise<void> | void)
await shop.handle("price", async (body: { of?: string }) => ({ of: body.of ?? "nothing", cost: 12 }));
const controller = await connectNats({ url: url!, module: "mesh-controller" });
await controller.handle("seat:mesh-controller.tools", async () => ({
seats: [{ seat: "mesh-controller", scope: "mesh", tools: [{ name: "status", description: "what is wrong", input: {} }] }],
seats: [{ seat: "mesh-controller", scope: "mesh", tools: [
{ name: "status", description: "what is wrong", input: {} },
{ name: "push", description: "tell a machine", input: { node: { type: "string" } } },
] }],
}));
await controller.handle("seat:mesh-controller.status", async () => ({ output: "all quiet", ok: true }));
await controller.handle("seat:mesh-controller.push", async (body: { node?: string }) => ({ told: body.node ?? "nobody" }));
t.after(async () => {
await catalogue.close();
await shop.close();
@@ -95,7 +99,7 @@ test("a host initialises, lists the mesh's tools and calls one", async (t) => {
assert.match(hello.instructions, /ada/, "the handshake says whose authority a call is made under");
const listed = byId.get(2)!.result.tools;
assert.deepEqual(listed.map((x: { name: string }) => x.name), ["mesh-controller.status", "shop.price"],
assert.deepEqual(listed.map((x: { name: string }) => x.name), ["mesh-controller.push", "mesh-controller.status", "shop.price"],
"the modules' tools and the roles', named the way a person names them");
const price = listed[1];
assert.ok(price.inputSchema, "a tool with no schema is one an agent cannot call");
@@ -141,6 +145,19 @@ test("a method this surface does not have is refused, and a notification is not"
assert.match(replies[0].error.message, /resources\/list/);
});
// A seat's verb that takes a machine as its own argument — `push <node>` — keeps it: the console
// moves `node` into the subject for a module's tool only (ADR 0159), never for a role's verb.
test("a seat's verb keeps a node of its own; only a module's tool gives it to the subject", async (t) => {
if (!url) return t.skip("MESH_TEST_NATS unset");
const credential = await aMeshAndACredential(t);
const replies = await driving(credential, [
{ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "mesh-controller.push", arguments: { node: "anchor" } } },
]);
const result = replies[0].result;
assert.ok(!result.isError, JSON.stringify(replies[0]));
assert.deepEqual(JSON.parse(result.content[0].text), { told: "anchor" });
});
test("a host calls the mesh's own verb through the seat", async (t) => {
if (!url) return t.skip("MESH_TEST_NATS unset");
const credential = await aMeshAndACredential(t);