fix: prepare public artifact runtime deps

This commit is contained in:
Peter Steinberger
2026-04-28 03:34:38 +01:00
parent 35685e9960
commit 09a2ffc47a
4 changed files with 177 additions and 47 deletions

View File

@@ -4,10 +4,7 @@ import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { openBoundaryFileSync } from "../infra/boundary-file-read.js";
import { resolveBundledPluginsDir } from "../plugins/bundled-dir.js";
import {
isBuiltBundledPluginRuntimeRoot,
prepareBundledPluginRuntimeRoot,
} from "../plugins/bundled-runtime-root.js";
import { prepareBuiltBundledPluginPublicSurfaceLocation } from "../plugins/bundled-public-surface-runtime-root.js";
import {
getCachedPluginJitiLoader,
type PluginJitiLoaderCache,
@@ -174,30 +171,6 @@ export type FacadeModuleLocation = {
boundaryRoot: string;
};
function resolveBuiltBundledPluginRoot(params: {
modulePath: string;
pluginId: string;
}): string | null {
const resolvedModulePath = path.resolve(params.modulePath);
let currentDir = path.dirname(resolvedModulePath);
while (true) {
if (
path.basename(currentDir) === params.pluginId &&
isBuiltBundledPluginRuntimeRoot(currentDir)
) {
const relativePath = path.relative(currentDir, resolvedModulePath);
if (!relativePath.startsWith("..") && !path.isAbsolute(relativePath)) {
return currentDir;
}
}
const parentDir = path.dirname(currentDir);
if (parentDir === currentDir) {
return null;
}
currentDir = parentDir;
}
}
function prepareFacadeLocationForBundledRuntimeDeps(params: {
location: FacadeModuleLocation;
runtimeDeps?: {
@@ -208,23 +181,11 @@ function prepareFacadeLocationForBundledRuntimeDeps(params: {
if (!params.runtimeDeps) {
return params.location;
}
const pluginRoot = resolveBuiltBundledPluginRoot({
modulePath: params.location.modulePath,
return prepareBuiltBundledPluginPublicSurfaceLocation({
location: params.location,
pluginId: params.runtimeDeps.pluginId,
});
if (!pluginRoot) {
return params.location;
}
const prepared = prepareBundledPluginRuntimeRoot({
pluginId: params.runtimeDeps.pluginId,
pluginRoot,
modulePath: params.location.modulePath,
...(params.runtimeDeps.env ? { env: params.runtimeDeps.env } : {}),
});
return {
modulePath: prepared.modulePath,
boundaryRoot: prepared.pluginRoot,
};
}
export function loadFacadeModuleAtLocationSync<T extends object>(params: {

View File

@@ -0,0 +1,58 @@
import path from "node:path";
import {
isBuiltBundledPluginRuntimeRoot,
prepareBundledPluginRuntimeRoot,
} from "./bundled-runtime-root.js";
export type BundledPublicSurfaceLocation = {
modulePath: string;
boundaryRoot: string;
};
export function resolveBuiltBundledPluginRootFromModulePath(params: {
modulePath: string;
pluginId: string;
}): string | null {
const resolvedModulePath = path.resolve(params.modulePath);
let currentDir = path.dirname(resolvedModulePath);
while (true) {
if (
path.basename(currentDir) === params.pluginId &&
isBuiltBundledPluginRuntimeRoot(currentDir)
) {
const relativePath = path.relative(currentDir, resolvedModulePath);
if (!relativePath.startsWith("..") && !path.isAbsolute(relativePath)) {
return currentDir;
}
}
const parentDir = path.dirname(currentDir);
if (parentDir === currentDir) {
return null;
}
currentDir = parentDir;
}
}
export function prepareBuiltBundledPluginPublicSurfaceLocation(params: {
location: BundledPublicSurfaceLocation;
pluginId: string;
env?: NodeJS.ProcessEnv;
}): BundledPublicSurfaceLocation {
const pluginRoot = resolveBuiltBundledPluginRootFromModulePath({
modulePath: params.location.modulePath,
pluginId: params.pluginId,
});
if (!pluginRoot) {
return params.location;
}
const prepared = prepareBundledPluginRuntimeRoot({
pluginId: params.pluginId,
pluginRoot,
modulePath: params.location.modulePath,
...(params.env ? { env: params.env } : {}),
});
return {
modulePath: prepared.modulePath,
boundaryRoot: prepared.pluginRoot,
};
}

View File

@@ -3,9 +3,14 @@ import os from "node:os";
import path from "node:path";
import { importFreshModule } from "openclaw/plugin-sdk/test-fixtures";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
clearBundledRuntimeDependencyNodePaths,
resolveBundledRuntimeDependencyInstallRoot,
} from "./bundled-runtime-deps.js";
const tempDirs: string[] = [];
const originalBundledPluginsDir = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
const originalPluginStageDir = process.env.OPENCLAW_PLUGIN_STAGE_DIR;
function createTempDir(): string {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-public-surface-loader-"));
@@ -13,6 +18,77 @@ function createTempDir(): string {
return tempDir;
}
function createPackagedPublicArtifactWithStagedRuntimeDep(): {
bundledPluginsDir: string;
pluginRoot: string;
stageRoot: string;
} {
const packageRoot = createTempDir();
const pluginRoot = path.join(packageRoot, "dist", "extensions", "demo");
const stageRoot = path.join(packageRoot, "stage");
fs.mkdirSync(pluginRoot, { recursive: true });
fs.writeFileSync(
path.join(packageRoot, "package.json"),
JSON.stringify({ name: "openclaw", version: "0.0.0", type: "module" }, null, 2),
"utf8",
);
fs.writeFileSync(
path.join(pluginRoot, "package.json"),
JSON.stringify(
{
name: "@openclaw/plugin-demo",
version: "0.0.0",
type: "module",
dependencies: {
"public-artifact-runtime-dep": "1.0.0",
},
},
null,
2,
),
"utf8",
);
fs.writeFileSync(
path.join(pluginRoot, "provider-policy-api.js"),
[
'import { marker as depMarker } from "public-artifact-runtime-dep";',
"export const marker = `artifact:${depMarker}`;",
"",
].join("\n"),
"utf8",
);
const installRoot = resolveBundledRuntimeDependencyInstallRoot(pluginRoot, {
env: {
...process.env,
OPENCLAW_PLUGIN_STAGE_DIR: stageRoot,
},
});
const depRoot = path.join(installRoot, "node_modules", "public-artifact-runtime-dep");
fs.mkdirSync(depRoot, { recursive: true });
fs.writeFileSync(
path.join(depRoot, "package.json"),
JSON.stringify(
{
name: "public-artifact-runtime-dep",
version: "1.0.0",
type: "module",
exports: "./index.js",
},
null,
2,
),
"utf8",
);
fs.writeFileSync(path.join(depRoot, "index.js"), 'export const marker = "staged";\n', "utf8");
return {
bundledPluginsDir: path.join(packageRoot, "dist", "extensions"),
pluginRoot,
stageRoot,
};
}
afterEach(() => {
for (const tempDir of tempDirs.splice(0)) {
fs.rmSync(tempDir, { recursive: true, force: true });
@@ -20,11 +96,18 @@ afterEach(() => {
vi.restoreAllMocks();
vi.resetModules();
vi.doUnmock("jiti");
vi.doUnmock("node:module");
clearBundledRuntimeDependencyNodePaths();
if (originalBundledPluginsDir === undefined) {
delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
} else {
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = originalBundledPluginsDir;
}
if (originalPluginStageDir === undefined) {
delete process.env.OPENCLAW_PLUGIN_STAGE_DIR;
} else {
process.env.OPENCLAW_PLUGIN_STAGE_DIR = originalPluginStageDir;
}
});
describe("bundled plugin public surface loader", () => {
@@ -140,6 +223,25 @@ describe("bundled plugin public surface loader", () => {
expect(createJiti).toHaveBeenCalledTimes(1);
});
it("loads built public artifacts through staged runtime deps", async () => {
const publicSurfaceLoader = await importFreshModule<
typeof import("./public-surface-loader.js")
>(import.meta.url, "./public-surface-loader.js?scope=runtime-deps");
const fixture = createPackagedPublicArtifactWithStagedRuntimeDep();
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = fixture.bundledPluginsDir;
process.env.OPENCLAW_PLUGIN_STAGE_DIR = fixture.stageRoot;
const loaded = publicSurfaceLoader.loadBundledPluginPublicArtifactModuleSync<{
marker: string;
}>({
dirName: "demo",
artifactBasename: "provider-policy-api.js",
});
expect(loaded.marker).toBe("artifact:staged");
expect(fs.existsSync(path.join(fixture.pluginRoot, "node_modules"))).toBe(false);
});
it("rejects public artifacts that change after boundary validation", async () => {
const createJiti = vi.fn(() => vi.fn(() => ({ marker: "should-not-load" })));
vi.doMock("jiti", () => ({

View File

@@ -5,6 +5,7 @@ import { fileURLToPath } from "node:url";
import { openBoundaryFileSync } from "../infra/boundary-file-read.js";
import { sameFileIdentity } from "../infra/file-identity.js";
import { resolveBundledPluginsDir } from "./bundled-dir.js";
import { prepareBuiltBundledPluginPublicSurfaceLocation } from "./bundled-public-surface-runtime-root.js";
import { getCachedPluginJitiLoader, type PluginJitiLoaderCache } from "./jiti-loader-cache.js";
import { resolveBundledPluginPublicSurfacePath } from "./public-surface-runtime.js";
import {
@@ -150,18 +151,24 @@ export function loadBundledPluginPublicArtifactModuleSync<T extends object>(para
`Unable to resolve bundled plugin public surface ${params.dirName}/${params.artifactBasename}`,
);
}
const cached = loadedPublicSurfaceModules.get(location.modulePath);
const preparedLocation = prepareBuiltBundledPluginPublicSurfaceLocation({
location,
pluginId: params.dirName,
});
const cached =
loadedPublicSurfaceModules.get(location.modulePath) ??
loadedPublicSurfaceModules.get(preparedLocation.modulePath);
if (cached) {
return cached as T;
}
const opened = openBoundaryFileSync({
absolutePath: location.modulePath,
rootPath: location.boundaryRoot,
absolutePath: preparedLocation.modulePath,
rootPath: preparedLocation.boundaryRoot,
boundaryLabel:
location.boundaryRoot === OPENCLAW_PACKAGE_ROOT
preparedLocation.boundaryRoot === OPENCLAW_PACKAGE_ROOT
? "OpenClaw package root"
: "bundled plugin directory",
: "plugin root",
rejectHardlinks: true,
});
if (!opened.ok) {
@@ -183,6 +190,7 @@ export function loadBundledPluginPublicArtifactModuleSync<T extends object>(para
const sentinel = {} as T;
loadedPublicSurfaceModules.set(location.modulePath, sentinel);
loadedPublicSurfaceModules.set(preparedLocation.modulePath, sentinel);
loadedPublicSurfaceModules.set(validatedPath, sentinel);
try {
const loaded = loadPublicSurfaceModule(validatedPath) as T;
@@ -190,6 +198,7 @@ export function loadBundledPluginPublicArtifactModuleSync<T extends object>(para
return sentinel;
} catch (error) {
loadedPublicSurfaceModules.delete(location.modulePath);
loadedPublicSurfaceModules.delete(preparedLocation.modulePath);
loadedPublicSurfaceModules.delete(validatedPath);
throw error;
}