diff --git a/package.json b/package.json index 3ca13b9..71c3592 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@novox/mesh-sdk", - "version": "0.1.2", + "version": "0.1.3", "description": "The stable spine a Novox Mesh module's own code builds against.", "type": "module", "exports": { diff --git a/src/tools/index.ts b/src/tools/index.ts index bfeb155..f4a8c91 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -49,6 +49,27 @@ export function collectTools(env: NodeJS.ProcessEnv = process.env): { module: st }); } +/** + * Collect every registered module's tools, each contributor given ITS OWN environment (novox/hq ADR + * 0192): `envFor` is asked with the registration's module and its index in registration order, and + * whatever it answers is all that contributor sees. A node's runtime serves many modules in one + * process, and a module's paths and words must reach its own tools and no other's — so the runtime + * that knows which bundle made which registration says which environment each gets, and the shared + * `collectTools(env)` stays what it was for a runtime serving one module. + */ +export function collectToolsEach( + envFor: (module: string, index: number) => NodeJS.ProcessEnv, +): { module: string; tools: ToolDefinition[] }[] { + return registrations.map(({ module, contribute }, index) => { + try { + return { module, tools: contribute(envFor(module, index)) }; + } catch (err) { + console.error(`[tools] ${module}: contributor failed, exposing none: ${err}`); + return { module, tools: [] }; + } + }); +} + /** List the tools every registered module exposes — for discovery, without invoking anything. */ export function listTools(env: NodeJS.ProcessEnv = process.env): { module: string; name: string; description: string }[] { return collectTools(env).flatMap(({ module, tools }) => diff --git a/test/sdk.test.ts b/test/sdk.test.ts index 209ae01..39034ad 100644 --- a/test/sdk.test.ts +++ b/test/sdk.test.ts @@ -5,7 +5,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { compareVersions } from "../dist/primitives/index.js"; -import { registerModuleTools, collectTools, resetTools, serveTools, listTools, invokeTool } from "../dist/tools/index.js"; +import { registerModuleTools, collectTools, collectToolsEach, resetTools, serveTools, listTools, invokeTool } from "../dist/tools/index.js"; import { runProvisioner } from "../dist/provisioner/index.js"; import { emit, on, type Event } from "../dist/events/index.js"; import { useBroker } from "../dist/messaging/index.js"; @@ -482,3 +482,18 @@ async function waitFor(cond: () => boolean, ms: number): Promise { await new Promise((r) => setTimeout(r, 10)); } } + +test("each registration is given the environment asked for it, and sees no other's (ADR 0192)", () => { + resetTools(); + const seen: Record = {}; + registerModuleTools("alpha", (env) => { seen.alpha = { ...env }; return [{ name: "a", description: "", input: {}, run: async () => env.ALPHA_FILE }]; }); + registerModuleTools("beta", (env) => { seen.beta = { ...env }; return [{ name: "b", description: "", input: {}, run: async () => env.BETA_URL }]; }); + const envs: Record = { alpha: { ALPHA_FILE: "/a/config.json" }, beta: { BETA_URL: "http://127.0.0.1:3000" } }; + const asked: string[] = []; + const got = collectToolsEach((module, index) => { asked.push(`${module}#${index}`); return envs[module] ?? {}; }); + assert.deepEqual(got.map((g) => g.module), ["alpha", "beta"]); + assert.deepEqual(asked, ["alpha#0", "beta#1"]); + assert.deepEqual(seen.alpha, { ALPHA_FILE: "/a/config.json" }); + assert.deepEqual(seen.beta, { BETA_URL: "http://127.0.0.1:3000" }); + resetTools(); +});