G C-1 (TS): a bundle reads its tool runner's capability words once and names a word not stated
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
This commit is contained in:
@@ -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<string[]> | undefined;
|
||||
|
||||
async function readWords(): Promise<string[]> {
|
||||
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<void> {
|
||||
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. */
|
||||
|
||||
+2
-2
@@ -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;
|
||||
}
|
||||
|
||||
+7
-3
@@ -5,6 +5,7 @@
|
||||
// `<seat>.<event>`), 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" },
|
||||
),
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user