Compare commits
1
Commits
5d395c5473
...
d4a4f4d1ba
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d4a4f4d1ba |
@@ -11,6 +11,7 @@
|
||||
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { resolve } from "node:path";
|
||||
import { register } from "node:module";
|
||||
import { useBroker } from "@novox/mesh-sdk/messaging";
|
||||
import { collectTools, toolKey, type ToolDefinition } from "@novox/mesh-sdk/tools";
|
||||
import type { Broker } from "@novox/mesh-sdk/messaging";
|
||||
@@ -109,6 +110,9 @@ export async function runTools(opts: RuntimeOptions): Promise<() => void> {
|
||||
// to the module whose bundle made it.
|
||||
// A bundle that is not plain JavaScript — or is marked executable — is launched as a process
|
||||
// and spoken to over MCP on stdio instead (ADR 0188); what it lists is registered the same way.
|
||||
// Every bundle's import of the SDK resolves to this runtime's copy (04-ISSUES/209): one registry
|
||||
// of tools, one broker. Installed before the first bundle is imported.
|
||||
oneSdk();
|
||||
const failed = new Map<string, string>();
|
||||
const owner: string[] = []; // registration index → the module whose bundle registered it
|
||||
const launched: { module: string; owner: string; tools: ToolDefinition[] }[] = [];
|
||||
@@ -313,3 +317,11 @@ async function serveClaimedSeats(
|
||||
if (typeof broker.onMembership === "function") broker.onMembership(() => void serve());
|
||||
return () => stops.forEach((s) => s());
|
||||
}
|
||||
|
||||
let sdkHooked = false;
|
||||
/** Resolve `@novox/mesh-sdk` for every bundle to the copy this runtime imported (sdk-hooks.ts). */
|
||||
function oneSdk(): void {
|
||||
if (sdkHooked) return;
|
||||
sdkHooked = true;
|
||||
register("./sdk-hooks.js", { parentURL: import.meta.url, data: { runtimeURL: import.meta.url } });
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// The one SDK in a node's runtime (novox/hq ADR 0175, 04-ISSUES/209).
|
||||
//
|
||||
// A bundle carries its own dependencies — the toolchain copies them in so a bundle starts anywhere
|
||||
// (to-be 38 WP3) — and among them is a copy of @novox/mesh-sdk. Imported in this process, that copy
|
||||
// would be a second SDK: its own registry of tools, its own broker handle. A bundle calling
|
||||
// registerModuleTools through it registers into a list this runtime never reads, and its tools are
|
||||
// silently not served. So every import of the SDK, from whichever bundle, is resolved as if this
|
||||
// runtime had written it: one registry, one broker — the runtime's. Everything else a bundle
|
||||
// carries resolves from the bundle's own tree, as before.
|
||||
//
|
||||
// Installed through module.register(), whose resolve hook sees every import that follows.
|
||||
|
||||
interface ResolveContext {
|
||||
parentURL?: string;
|
||||
conditions: string[];
|
||||
importAttributes: Record<string, string>;
|
||||
}
|
||||
interface Resolved {
|
||||
url: string;
|
||||
format?: string | null;
|
||||
shortCircuit?: boolean;
|
||||
}
|
||||
type NextResolve = (specifier: string, context?: Partial<ResolveContext>) => Promise<Resolved>;
|
||||
|
||||
const SDK = "@novox/mesh-sdk";
|
||||
let runtimeURL = "";
|
||||
|
||||
/** Told, once, which file's tree holds the runtime's SDK. */
|
||||
export function initialize(data: { runtimeURL: string }): void {
|
||||
runtimeURL = data.runtimeURL;
|
||||
}
|
||||
|
||||
export async function resolve(specifier: string, context: ResolveContext, next: NextResolve): Promise<Resolved> {
|
||||
if (runtimeURL && (specifier === SDK || specifier.startsWith(SDK + "/"))) {
|
||||
return next(specifier, { ...context, parentURL: runtimeURL });
|
||||
}
|
||||
return next(specifier, context);
|
||||
}
|
||||
@@ -11,6 +11,9 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { cpSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { connect, StringCodec } from "nats";
|
||||
|
||||
import { resetTools } from "@novox/mesh-sdk/tools";
|
||||
@@ -192,3 +195,40 @@ test("the node's runtime serves five modules' bundles on one credential — two
|
||||
resetTools();
|
||||
}
|
||||
});
|
||||
|
||||
test("a bundle carrying its own copy of the SDK registers into the runtime's registry, and its tools are served (issue 209)", async (t) => {
|
||||
if (!url) return t.skip("MESH_TEST_NATS unset");
|
||||
resetTools();
|
||||
// A bundle as the toolchain packs one: its compiled entrypoint, a package.json saying ES modules,
|
||||
// and its dependencies copied in — the SDK among them, a second copy beside the runtime's own.
|
||||
const dir = mkdtempSync(join(tmpdir(), "mesh-bundle-"));
|
||||
const sdk = realpathSync(fileURLToPath(new URL("../node_modules/@novox/mesh-sdk/", import.meta.url)));
|
||||
cpSync(sdk, join(dir, "node_modules", "@novox", "mesh-sdk"), { recursive: true });
|
||||
writeFileSync(join(dir, "package.json"), '{"type":"module","private":true}\n');
|
||||
writeFileSync(join(dir, "index.js"),
|
||||
'import { registerModuleTools } from "@novox/mesh-sdk/tools";\n' +
|
||||
'registerModuleTools("zeta", () => [{ name: "probe", description: "answers", input: {}, run: async () => ({ zeta: true }) }]);\n');
|
||||
const mesh = await aMesh();
|
||||
await mesh.issue(membershipOf("zeta", "anchor"));
|
||||
const credential = { url, node: "anchor", module: "node-tools" };
|
||||
const nodeTools = await connectNats(credential);
|
||||
const asker = await connectNats({ url, module: "console", node: "workstation" });
|
||||
const said: string[] = [];
|
||||
const log = console.log;
|
||||
console.log = (...a: unknown[]) => said.push(a.join(" "));
|
||||
let stop = () => {};
|
||||
try {
|
||||
stop = await runTools({ broker: nodeTools, credential, serves: [{ module: "zeta", entrypoints: [join(dir, "index.js")] }] });
|
||||
console.log = log;
|
||||
assert.ok(said.some((s) => /serving 1 tool\(s\) for 1 module\(s\): zeta\.probe/.test(s)), said.join("\n"));
|
||||
assert.deepEqual((await callTool(asker, "zeta.probe@anchor", {})).result, { zeta: true });
|
||||
} finally {
|
||||
console.log = log;
|
||||
stop();
|
||||
await asker.close();
|
||||
await nodeTools.close();
|
||||
await mesh.close();
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
resetTools();
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user