fix(plugins): load packaged runtime mirrors from canonical sources

This commit is contained in:
Peter Steinberger
2026-04-25 09:16:05 +01:00
parent 8503935a21
commit 689a353621
4 changed files with 142 additions and 18 deletions

View File

@@ -5,6 +5,7 @@ import {
existsSync,
mkdtempSync,
mkdirSync,
realpathSync,
readdirSync,
readFileSync,
rmSync,
@@ -17,6 +18,10 @@ import {
PACKAGE_DIST_INVENTORY_RELATIVE_PATH,
writePackageDistInventory,
} from "../src/infra/package-dist-inventory.ts";
import {
resolveBundledRuntimeDependencyInstallRoot,
resolveBundledRuntimeDependencyPackageInstallRoot,
} from "../src/plugins/bundled-runtime-deps.ts";
import {
collectBundledExtensionManifestErrors,
type BundledExtension,
@@ -317,28 +322,48 @@ function bundledRuntimeDependencySentinelPath(
);
}
function bundledRuntimeDependencySentinelCandidates(
export function bundledRuntimeDependencySentinelCandidates(
packageRoot: string,
pluginId: string,
dependencyName: string,
env: NodeJS.ProcessEnv = process.env,
): string[] {
const dependencyParts = dependencyName.split("/");
const packageRoots = [
packageRoot,
(() => {
try {
return realpathSync(packageRoot);
} catch {
return packageRoot;
}
})(),
];
const runtimeRoots = packageRoots.flatMap((root) => [
resolveBundledRuntimeDependencyPackageInstallRoot(root, { env }),
resolveBundledRuntimeDependencyInstallRoot(join(root, "dist", "extensions", pluginId), {
env,
}),
]);
return [
bundledRuntimeDependencySentinelPath(packageRoot, pluginId, dependencyName),
join(packageRoot, "dist", "extensions", "node_modules", ...dependencyParts, "package.json"),
join(packageRoot, "node_modules", ...dependencyParts, "package.json"),
];
...runtimeRoots.map((root) => join(root, "node_modules", ...dependencyParts, "package.json")),
].filter((candidate, index, candidates) => candidates.indexOf(candidate) === index);
}
function assertBundledRuntimeDependencyAbsent(params: {
packageRoot: string;
pluginId: string;
dependencyName: string;
env?: NodeJS.ProcessEnv;
}): void {
const sentinelPath = bundledRuntimeDependencySentinelCandidates(
params.packageRoot,
params.pluginId,
params.dependencyName,
params.env,
).find((candidate) => existsSync(candidate));
if (sentinelPath) {
throw new Error(
@@ -351,11 +376,13 @@ function assertBundledRuntimeDependencyPresent(params: {
packageRoot: string;
pluginId: string;
dependencyName: string;
env?: NodeJS.ProcessEnv;
}): void {
const sentinelPath = bundledRuntimeDependencySentinelCandidates(
params.packageRoot,
params.pluginId,
params.dependencyName,
params.env,
).find((candidate) => existsSync(candidate));
if (sentinelPath) {
return;
@@ -413,24 +440,25 @@ function runPackedBundledPluginActivationSmoke(packageRoot: string, tmpRoot: str
{ pluginId: "feishu", dependencyName: "@larksuiteoapi/node-sdk" },
] as const;
for (const dep of lazyDeps) {
assertBundledRuntimeDependencyAbsent({ packageRoot, ...dep });
}
const homeDir = join(tmpRoot, "activation-home");
mkdirSync(homeDir, { recursive: true });
const env = createPackedCliSmokeEnv(process.env, {
HOME: homeDir,
OPENAI_API_KEY: "sk-openclaw-release-check",
});
for (const dep of lazyDeps) {
assertBundledRuntimeDependencyAbsent({ packageRoot, env, ...dep });
}
writePackedBundledPluginActivationConfig(homeDir);
execFileSync(process.execPath, [join(packageRoot, "openclaw.mjs"), "plugins", "doctor"], {
cwd: packageRoot,
stdio: "inherit",
env: createPackedCliSmokeEnv(process.env, {
HOME: homeDir,
OPENAI_API_KEY: "sk-openclaw-release-check",
}),
env,
});
for (const dep of lazyDeps) {
assertBundledRuntimeDependencyPresent({ packageRoot, ...dep });
assertBundledRuntimeDependencyPresent({ packageRoot, env, ...dep });
}
}

View File

@@ -1504,6 +1504,60 @@ module.exports = {
expect(registry.plugins.find((entry) => entry.id === "alpha")?.status).toBe("loaded");
});
it("loads bundled plugins from symlinked package roots with an external stage dir", () => {
const packageRoot = makeTempDir();
const stageDir = makeTempDir();
const aliasRoot = path.join(makeTempDir(), "openclaw-alias");
const bundledDir = path.join(packageRoot, "dist", "extensions");
const plugin = writePlugin({
id: "alpha",
dir: path.join(bundledDir, "alpha"),
filename: "index.cjs",
body: `module.exports = { id: "alpha", register(api) { api.registerCommand({ name: "alpha", handler: () => "ok" }); } };`,
});
fs.writeFileSync(
path.join(packageRoot, "package.json"),
JSON.stringify({ name: "openclaw", version: "2026.4.24", type: "module" }),
"utf-8",
);
fs.writeFileSync(
path.join(plugin.dir, "package.json"),
JSON.stringify(
{
name: "@openclaw/alpha",
version: "1.0.0",
openclaw: { extensions: ["./index.cjs"] },
},
null,
2,
),
"utf-8",
);
fs.writeFileSync(
path.join(plugin.dir, "openclaw.plugin.json"),
JSON.stringify(
{
id: "alpha",
enabledByDefault: true,
configSchema: EMPTY_PLUGIN_SCHEMA,
},
null,
2,
),
"utf-8",
);
fs.symlinkSync(packageRoot, aliasRoot, "dir");
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = path.join(aliasRoot, "dist", "extensions");
process.env.OPENCLAW_PLUGIN_STAGE_DIR = stageDir;
const registry = loadOpenClawPlugins({
cache: false,
config: { plugins: { enabled: true } },
});
expect(registry.plugins.find((entry) => entry.id === "alpha")?.status).toBe("loaded");
});
it("loads bundled plugins with plugin-sdk imports from an external stage dir", () => {
const packageRoot = makeTempDir();
const stageDir = makeTempDir();

View File

@@ -2276,8 +2276,12 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
};
const pluginRoot = safeRealpathOrResolve(candidate.rootDir);
let runtimePluginRoot = pluginRoot;
let runtimeCandidateSource = candidate.source;
let runtimeSetupSource = manifestRecord.setupSource;
let runtimeCandidateSource =
candidate.origin === "bundled" ? safeRealpathOrResolve(candidate.source) : candidate.source;
let runtimeSetupSource =
candidate.origin === "bundled" && manifestRecord.setupSource
? safeRealpathOrResolve(manifestRecord.setupSource)
: manifestRecord.setupSource;
const scopedSetupOnlyChannelPluginRequested =
includeSetupOnlyChannelPlugins &&
@@ -2381,12 +2385,12 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
});
runtimeCandidateSource =
remapBundledPluginRuntimePath({
source: candidate.source,
source: runtimeCandidateSource,
pluginRoot,
mirroredRoot: runtimePluginRoot,
}) ?? candidate.source;
}) ?? runtimeCandidateSource;
runtimeSetupSource = remapBundledPluginRuntimePath({
source: manifestRecord.setupSource,
source: runtimeSetupSource,
pluginRoot,
mirroredRoot: runtimePluginRoot,
});
@@ -3186,7 +3190,11 @@ export async function loadOpenClawPluginCliRegistry(
const pluginRoot = safeRealpathOrResolve(candidate.rootDir);
const cliMetadataSource = resolveCliMetadataEntrySource(candidate.rootDir);
const sourceForCliMetadata =
candidate.origin === "bundled" ? cliMetadataSource : (cliMetadataSource ?? candidate.source);
candidate.origin === "bundled"
? cliMetadataSource
? safeRealpathOrResolve(cliMetadataSource)
: safeRealpathOrResolve(candidate.source)
: (cliMetadataSource ?? candidate.source);
if (!sourceForCliMetadata) {
record.status = "loaded";
registry.plugins.push(record);

View File

@@ -1,4 +1,4 @@
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { mkdtempSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { describe, expect, it } from "vitest";
@@ -12,6 +12,7 @@ import {
collectBundledPluginRootRuntimeMirrorErrors,
collectForbiddenPackContentPaths,
collectInstalledBundledPluginRuntimeDepErrors,
bundledRuntimeDependencySentinelCandidates,
collectRootDistBundledRuntimeMirrors,
collectForbiddenPackPaths,
collectMissingPackPaths,
@@ -673,3 +674,36 @@ describe("collectInstalledBundledPluginRuntimeDepErrors", () => {
}
});
});
describe("bundledRuntimeDependencySentinelCandidates", () => {
it("checks canonical external runtime-deps roots for packed installs", () => {
const root = mkdtempSync(join(tmpdir(), "release-check-runtime-candidates-"));
const packageRoot = join(root, "package");
const aliasRoot = join(root, "package-alias");
const homeRoot = join(root, "home");
try {
mkdirSync(join(packageRoot, "dist", "extensions", "browser"), { recursive: true });
writeFileSync(
join(packageRoot, "package.json"),
JSON.stringify({ name: "openclaw", version: "2026.4.24-beta.1" }, null, 2),
);
symlinkSync(packageRoot, aliasRoot, "dir");
const candidates = bundledRuntimeDependencySentinelCandidates(
aliasRoot,
"browser",
"playwright-core",
{ HOME: homeRoot } as NodeJS.ProcessEnv,
);
const externalCandidates = candidates.filter(
(candidate) =>
candidate.startsWith(join(homeRoot, ".openclaw", "plugin-runtime-deps")) &&
candidate.endsWith(join("node_modules", "playwright-core", "package.json")),
);
expect(externalCandidates.length).toBeGreaterThanOrEqual(2);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
});