From 36974cddf9e7653be6c05e116404eefb75872e48 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 11 Nov 2026 21:46:17 +0100 Subject: [PATCH] G C-1 (TS): a bundle reads its tool runner's capability words once and names a word not stated MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mesh/capabilities answered {words}, an unknown call read as none, the node from MESH_NODE; the finding writer's bundle transport requires finding-calls; a runtime refusal keeps its code. Builds: design 47 G §576, §622, §488 Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ --- src/runtime/index.ts | 40 ++++++++++++++++++++++++++++++++++++++++ src/stdio/index.ts | 4 ++-- src/views/bundle.ts | 10 +++++++--- test/sdk.test.ts | 24 ++++++++++++++++++++++++ test/stdio.test.ts | 4 ++++ 5 files changed, 77 insertions(+), 5 deletions(-) diff --git a/src/runtime/index.ts b/src/runtime/index.ts index 3365ebdc..c12af654 100644 --- a/src/runtime/index.ts +++ b/src/runtime/index.ts @@ -11,6 +11,46 @@ let bound: RuntimeAsk | undefined; /** Bind the channel. The stdio session calls this when the runtime launched the process; a test binds its own. */ export function useRuntime(ask: RuntimeAsk | undefined): void { bound = ask; + words = undefined; +} + +/** The call a bundle reads its tool runner's capability words with: answered with `{words}` (design 47 G §576, §622). */ +export const CAPABILITIES_CALL = "mesh/capabilities"; +/** The word of the acknowledged publish and the subscription for new messages the finding writer uses (§559). */ +export const CAP_FINDING_CALLS = "finding-calls"; +/** The code of a runtime's answer to a call it does not know (JSON-RPC's method not found). */ +const CODE_NO_METHOD = -32601; + +// The words the tool runner states, asked once at the first call that needs one and kept for as long as the bundle runs: a +// bundle lives no longer than the tool runner that started it (design 47 G §576). +let words: Promise | undefined; + +async function readWords(): Promise { + try { + const a = (await askRuntime(CAPABILITIES_CALL, {})) as { words?: string[] } | null; + return a?.words ?? []; + } catch (e) { + // a tool runner of before G answers the call as an unknown one, which reads as no word stated + if ((e as { code?: number }).code === CODE_NO_METHOD) return []; + throw new Error(`reading the tool runner's capability words: ${e instanceof Error ? e.message : String(e)}`); + } +} + +/** + * Fail at once, naming the word and the node (from MESH_NODE), when the tool runner this bundle runs under does not state + * `word`; the words are read once (design 47 G §488, §576). A failed read is asked again at the next call. + */ +export async function requireCapability(word: string): Promise { + const reading = (words ??= readWords()); + let stated: string[]; + try { + stated = await reading; + } catch (e) { + if (words === reading) words = undefined; + throw e; + } + if (stated.includes(word)) return; + throw new Error(`the tool runner of node ${process.env.MESH_NODE || "unknown"} does not state the capability word "${word}": a call of its family is refused until it does`); } /** Ask the runtime one of its methods; refused with the reason when no runtime launched this bundle. */ diff --git a/src/stdio/index.ts b/src/stdio/index.ts index 759a6139..dc2f6528 100644 --- a/src/stdio/index.ts +++ b/src/stdio/index.ts @@ -230,11 +230,11 @@ function startSession(): Session { continue; // not framed; nobody to answer } // The runtime's answer to something this bundle asked. - const reply = request as unknown as { id?: string; method?: string; result?: unknown; error?: { message?: string } }; + const reply = request as unknown as { id?: string; method?: string; result?: unknown; error?: { message?: string; code?: number } }; if (reply.method === undefined && typeof reply.id === "string" && awaiting.has(reply.id)) { const waiting = awaiting.get(reply.id)!; awaiting.delete(reply.id); - if (reply.error) waiting.reject(new Error(reply.error.message ?? "the runtime refused")); + if (reply.error) waiting.reject(Object.assign(new Error(reply.error.message ?? "the runtime refused"), { code: reply.error.code })); else waiting.resolve(reply.result); continue; } diff --git a/src/views/bundle.ts b/src/views/bundle.ts index 227c3632..f5ccfd1e 100644 --- a/src/views/bundle.ts +++ b/src/views/bundle.ts @@ -5,6 +5,7 @@ // `.`), not subjects. import { broker } from "../messaging/index.js"; +import { CAP_FINDING_CALLS, requireCapability } from "../runtime/index.js"; import type { EventHeaders } from "../contracts/index.js"; import { randomUUID } from "node:crypto"; @@ -15,6 +16,7 @@ export function bundleTransport(module = process.env.MESH_MODULE ?? "unknown", n } { return { publish: async (event, body, id) => { + await requireCapability(CAP_FINDING_CALLS); const headers: EventHeaders = { "x-event-id": id || randomUUID(), "x-source": module, @@ -24,13 +26,15 @@ export function bundleTransport(module = process.env.MESH_MODULE ?? "unknown", n }; await broker().publish({ key: event, node, body, headers }, { ack: true }); }, - listen: (event, handle) => - broker().subscribe( + listen: async (event, handle) => { + await requireCapability(CAP_FINDING_CALLS); + return broker().subscribe( event, async (env) => { handle(env.body); }, { deliver: "new" }, - ), + ); + }, }; } diff --git a/test/sdk.test.ts b/test/sdk.test.ts index f75271fe..2ca4e66b 100644 --- a/test/sdk.test.ts +++ b/test/sdk.test.ts @@ -981,3 +981,27 @@ test("the finding writer owns its kinds, keeps a views reader's findings in its await vw.delete(vf); assert.equal(store.has("module.m=2eo=2es.view-waits"), false); }); + +test("a bundle reads its tool runner's capability words once, reads an unknown call as none and names a word not stated (design 47 G §576)", async () => { + const { useRuntime, requireCapability } = await import("../dist/runtime/index.js"); + const was = process.env.MESH_NODE; + process.env.MESH_NODE = "ace"; + let asked = 0; + useRuntime(async (method) => { + asked++; + assert.equal(method, "mesh/capabilities"); + return { words: ["finding-calls"] }; + }); + await requireCapability("finding-calls"); + await assert.rejects(requireCapability("put-sessions"), /"put-sessions".*ace|ace.*"put-sessions"/); + assert.equal(asked, 1); + useRuntime(async () => { throw Object.assign(new Error("no mesh/capabilities"), { code: -32601 }); }); + await assert.rejects(requireCapability("finding-calls"), /"finding-calls"/); + let tries = 0; + useRuntime(async () => { if (++tries === 1) throw new Error("pipe closed"); return { words: ["finding-calls"] }; }); + await assert.rejects(requireCapability("finding-calls"), /pipe closed/); + await requireCapability("finding-calls"); // a failed read is asked again + useRuntime(undefined); + if (was === undefined) delete process.env.MESH_NODE; + else process.env.MESH_NODE = was; +}); diff --git a/test/stdio.test.ts b/test/stdio.test.ts index 31741f91..5ae849d4 100644 --- a/test/stdio.test.ts +++ b/test/stdio.test.ts @@ -353,6 +353,9 @@ test("a bundle's finding writer publishes with ack and listens with deliver new }; const send = (m: unknown) => child.stdin.write(JSON.stringify(m) + "\n"); try { + // the capability words are read once, before the first call that needs one (design 47 G §576) + const caps = await until((m) => m.method === "mesh/capabilities"); + send({ jsonrpc: "2.0", id: caps.id, result: { words: ["finding-calls"] } }); const sub = await until((m) => m.method === "mesh/subscribe"); assert.deepEqual(sub.params, { pattern: "mesh-conditions.findings-asked", deliver: "new" }); send({ jsonrpc: "2.0", id: sub.id, result: {} }); @@ -366,6 +369,7 @@ test("a bundle's finding writer publishes with ack and listens with deliver new // The runtime answers an unacknowledged publish with {error}: the writer's publish fails, so it sends again. send({ jsonrpc: "2.0", id: pub.id, result: { error: "no stream" } }); const answered = await until((m) => m.id === 2); + assert.equal(seen.filter((m) => m.method === "mesh/capabilities").length, 1, "the words are asked once"); assert.deepEqual(JSON.parse(answered.result.content[0].text), { said: false, why: "the bus did not acknowledge finding-written: no stream" }); } finally { child.kill();