refactor: replace plugin-sdk dist env hacks with loader option

This commit is contained in:
Peter Steinberger
2026-03-27 13:38:40 +00:00
parent ad89fa669c
commit 546a1aad98
12 changed files with 129 additions and 81 deletions

View File

@@ -7,12 +7,14 @@ let monolithicSdk = null;
let diagnosticEventsModule = null;
const jitiLoaders = new Map();
const pluginSdkSubpathsCache = new Map();
const isDistRootAlias = __filename.includes(
`${path.sep}dist${path.sep}plugin-sdk${path.sep}root-alias.cjs`,
);
const shouldPreferSourceInTests =
process.env.OPENCLAW_PLUGIN_SDK_PREFER_DIST === "1"
? false
: Boolean(process.env.VITEST) ||
process.env.NODE_ENV === "test" ||
process.env.OPENCLAW_PLUGIN_SDK_SOURCE_IN_TESTS === "1";
!isDistRootAlias &&
(Boolean(process.env.VITEST) ||
process.env.NODE_ENV === "test" ||
process.env.OPENCLAW_PLUGIN_SDK_SOURCE_IN_TESTS === "1");
function emptyPluginConfigSchema() {
function error(message) {

View File

@@ -25,11 +25,13 @@ export function buildBundledCapabilityRuntimeConfig(
export function loadBundledCapabilityRuntimeRegistry(params: {
pluginIds: readonly string[];
env?: PluginLoadOptions["env"];
pluginSdkResolution?: PluginLoadOptions["pluginSdkResolution"];
}) {
return loadOpenClawPlugins({
config: buildBundledCapabilityRuntimeConfig(params.pluginIds, params.env),
env: params.env,
onlyPluginIds: [...params.pluginIds],
pluginSdkResolution: params.pluginSdkResolution,
cache: false,
activate: false,
logger: {

View File

@@ -1,14 +1,11 @@
import { Command } from "commander";
import { afterAll, afterEach, beforeEach, describe, expect, it } from "vitest";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import { registerPluginCliCommands } from "./cli.js";
import { clearPluginLoaderCache } from "./loader.js";
import { clearPluginManifestRegistryCache } from "./manifest-registry.js";
import { resetPluginRuntimeStateForTest } from "./runtime.js";
const previousPreferDistPluginSdk = process.env.OPENCLAW_PLUGIN_SDK_PREFER_DIST;
process.env.OPENCLAW_PLUGIN_SDK_PREFER_DIST = "1";
function resetPluginState() {
clearPluginLoaderCache();
clearPluginManifestRegistryCache();
@@ -24,37 +21,39 @@ describe("registerPluginCliCommands browser plugin integration", () => {
resetPluginState();
});
afterAll(() => {
if (previousPreferDistPluginSdk === undefined) {
delete process.env.OPENCLAW_PLUGIN_SDK_PREFER_DIST;
} else {
process.env.OPENCLAW_PLUGIN_SDK_PREFER_DIST = previousPreferDistPluginSdk;
}
});
it("registers the browser command from the bundled browser plugin", () => {
const program = new Command();
registerPluginCliCommands(program, {
plugins: {
allow: ["browser"],
},
} as OpenClawConfig);
registerPluginCliCommands(
program,
{
plugins: {
allow: ["browser"],
},
} as OpenClawConfig,
undefined,
{ pluginSdkResolution: "dist" },
);
expect(program.commands.map((command) => command.name())).toContain("browser");
});
it("omits the browser command when the bundled browser plugin is disabled", () => {
const program = new Command();
registerPluginCliCommands(program, {
plugins: {
allow: ["browser"],
entries: {
browser: {
enabled: false,
registerPluginCliCommands(
program,
{
plugins: {
allow: ["browser"],
entries: {
browser: {
enabled: false,
},
},
},
},
} as OpenClawConfig);
} as OpenClawConfig,
undefined,
{ pluginSdkResolution: "dist" },
);
expect(program.commands.map((command) => command.name())).not.toContain("browser");
});

View File

@@ -3,13 +3,17 @@ import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent
import type { OpenClawConfig } from "../config/config.js";
import { loadConfig } from "../config/config.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import { loadOpenClawPlugins } from "./loader.js";
import { loadOpenClawPlugins, type PluginLoadOptions } from "./loader.js";
import type { OpenClawPluginCliCommandDescriptor } from "./types.js";
import type { PluginLogger } from "./types.js";
const log = createSubsystemLogger("plugins");
function loadPluginCliRegistry(cfg?: OpenClawConfig, env?: NodeJS.ProcessEnv) {
function loadPluginCliRegistry(
cfg?: OpenClawConfig,
env?: NodeJS.ProcessEnv,
loaderOptions?: Pick<PluginLoadOptions, "pluginSdkResolution">,
) {
const config = cfg ?? loadConfig();
const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config));
const logger: PluginLogger = {
@@ -27,6 +31,7 @@ function loadPluginCliRegistry(cfg?: OpenClawConfig, env?: NodeJS.ProcessEnv) {
workspaceDir,
env,
logger,
...loaderOptions,
}),
};
}
@@ -58,8 +63,9 @@ export function registerPluginCliCommands(
program: Command,
cfg?: OpenClawConfig,
env?: NodeJS.ProcessEnv,
loaderOptions?: Pick<PluginLoadOptions, "pluginSdkResolution">,
) {
const { config, workspaceDir, logger, registry } = loadPluginCliRegistry(cfg, env);
const { config, workspaceDir, logger, registry } = loadPluginCliRegistry(cfg, env, loaderOptions);
const existingCommands = new Set(program.commands.map((cmd) => cmd.name()));

View File

@@ -1,18 +1,6 @@
import { afterAll, describe, expect, it } from "vitest";
const previousPreferDistPluginSdk = process.env.OPENCLAW_PLUGIN_SDK_PREFER_DIST;
process.env.OPENCLAW_PLUGIN_SDK_PREFER_DIST = "1";
const { providerContractLoadError, providerContractRegistry } = await import("./registry.js");
const { installProviderPluginContractSuite } = await import("./suites.js");
afterAll(() => {
if (previousPreferDistPluginSdk === undefined) {
delete process.env.OPENCLAW_PLUGIN_SDK_PREFER_DIST;
} else {
process.env.OPENCLAW_PLUGIN_SDK_PREFER_DIST = previousPreferDistPluginSdk;
}
});
import { describe, expect, it } from "vitest";
import { providerContractLoadError, providerContractRegistry } from "./registry.js";
import { installProviderPluginContractSuite } from "./suites.js";
describe("provider contract registry load", () => {
it("loads bundled providers without import-time registry failure", () => {

View File

@@ -120,6 +120,7 @@ function loadProviderContractEntriesForPluginId(pluginId: string): ProviderContr
bundledProviderAllowlistCompat: true,
bundledProviderVitestCompat: true,
onlyPluginIds: [pluginId],
pluginSdkResolution: "dist",
cache: false,
activate: false,
}).map((provider) => ({
@@ -186,6 +187,7 @@ function loadWebSearchProviderContractRegistry(): WebSearchProviderContractEntry
if (!webSearchProviderContractRegistryCache) {
const registry = loadBundledCapabilityRuntimeRegistry({
pluginIds: BUNDLED_WEB_SEARCH_PLUGIN_IDS,
pluginSdkResolution: "dist",
});
webSearchProviderContractRegistryCache = registry.webSearchProviders.map((entry) => ({
pluginId: entry.pluginId,

View File

@@ -1,18 +1,6 @@
import { afterAll, describe, expect, it } from "vitest";
const previousPreferDistPluginSdk = process.env.OPENCLAW_PLUGIN_SDK_PREFER_DIST;
process.env.OPENCLAW_PLUGIN_SDK_PREFER_DIST = "1";
const { webSearchProviderContractRegistry } = await import("./registry.js");
const { installWebSearchProviderContractSuite } = await import("./suites.js");
afterAll(() => {
if (previousPreferDistPluginSdk === undefined) {
delete process.env.OPENCLAW_PLUGIN_SDK_PREFER_DIST;
} else {
process.env.OPENCLAW_PLUGIN_SDK_PREFER_DIST = previousPreferDistPluginSdk;
}
});
import { describe, expect, it } from "vitest";
import { webSearchProviderContractRegistry } from "./registry.js";
import { installWebSearchProviderContractSuite } from "./suites.js";
describe("web search provider contract registry load", () => {
it("loads bundled web search providers", () => {

View File

@@ -54,8 +54,6 @@ function mkdirSafe(dir: string) {
const fixtureRoot = mkdtempSafe(path.join(os.tmpdir(), "openclaw-plugin-"));
let tempDirIndex = 0;
const prevBundledDir = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
const prevPreferDistPluginSdk = process.env.OPENCLAW_PLUGIN_SDK_PREFER_DIST;
process.env.OPENCLAW_PLUGIN_SDK_PREFER_DIST = "1";
const EMPTY_PLUGIN_SCHEMA = { type: "object", additionalProperties: false, properties: {} };
let cachedBundledTelegramDir = "";
let cachedBundledMemoryDir = "";
@@ -723,11 +721,6 @@ afterAll(() => {
} finally {
cachedBundledTelegramDir = "";
cachedBundledMemoryDir = "";
if (prevPreferDistPluginSdk === undefined) {
delete process.env.OPENCLAW_PLUGIN_SDK_PREFER_DIST;
} else {
process.env.OPENCLAW_PLUGIN_SDK_PREFER_DIST = prevPreferDistPluginSdk;
}
}
});
@@ -1053,6 +1046,7 @@ module.exports = { id: "skipped-scoped-only", register() { throw new Error("skip
const scoped = loadOpenClawPlugins({
cache: false,
activate: false,
pluginSdkResolution: "dist",
config: {
plugins: {
enabled: true,

View File

@@ -46,6 +46,7 @@ import {
buildPluginLoaderJitiOptions,
listPluginSdkAliasCandidates,
listPluginSdkExportedSubpaths,
type PluginSdkResolutionPreference,
resolveExtensionApiAlias,
resolvePluginSdkAliasCandidateOrder,
resolvePluginSdkAliasFile,
@@ -73,6 +74,7 @@ export type PluginLoadOptions = {
logger?: PluginLogger;
coreGatewayHandlers?: Record<string, GatewayRequestHandler>;
runtimeOptions?: CreatePluginRuntimeOptions;
pluginSdkResolution?: PluginSdkResolutionPreference;
cache?: boolean;
mode?: "full" | "validate";
onlyPluginIds?: string[];
@@ -195,6 +197,7 @@ function buildCacheKey(params: {
includeSetupOnlyChannelPlugins?: boolean;
preferSetupRuntimeForChannelPlugins?: boolean;
runtimeSubagentMode?: "default" | "explicit" | "gateway-bindable";
pluginSdkResolution?: PluginSdkResolutionPreference;
}): string {
const { roots, loadPaths } = resolvePluginCacheInputs({
workspaceDir: params.workspaceDir,
@@ -225,7 +228,7 @@ function buildCacheKey(params: {
...params.plugins,
installs,
loadPaths,
})}::${scopeKey}::${setupOnlyKey}::${startupChannelMode}::${params.runtimeSubagentMode ?? "default"}`;
})}::${scopeKey}::${setupOnlyKey}::${startupChannelMode}::${params.runtimeSubagentMode ?? "default"}::${params.pluginSdkResolution ?? "auto"}`;
}
function normalizeScopedPluginIds(ids?: string[]): string[] | undefined {
@@ -714,6 +717,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
: options.runtimeOptions?.subagent
? "explicit"
: "default",
pluginSdkResolution: options.pluginSdkResolution,
});
const cacheEnabled = options.cache !== false;
if (cacheEnabled) {
@@ -746,7 +750,12 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
const tryNative = shouldPreferNativeJiti(modulePath);
// Pass loader's moduleUrl so the openclaw root can always be resolved even when
// loading external plugins from outside the installation directory (e.g. ~/.openclaw/extensions/).
const aliasMap = buildPluginLoaderAliasMap(modulePath, process.argv[1], import.meta.url);
const aliasMap = buildPluginLoaderAliasMap(
modulePath,
process.argv[1],
import.meta.url,
options.pluginSdkResolution,
);
const cacheKey = JSON.stringify({
tryNative,
aliasMap: Object.entries(aliasMap).toSorted(([left], [right]) => left.localeCompare(right)),
@@ -775,7 +784,9 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
if (createPluginRuntimeFactory) {
return createPluginRuntimeFactory;
}
const runtimeModulePath = resolvePluginRuntimeModulePath();
const runtimeModulePath = resolvePluginRuntimeModulePath({
pluginSdkResolution: options.pluginSdkResolution,
});
if (!runtimeModulePath) {
throw new Error("Unable to resolve plugin runtime module");
}

View File

@@ -23,6 +23,7 @@ export function resolvePluginProviders(params: {
onlyPluginIds?: string[];
activate?: boolean;
cache?: boolean;
pluginSdkResolution?: PluginLoadOptions["pluginSdkResolution"];
}): ProviderPlugin[] {
const env = params.env ?? process.env;
const bundledProviderCompatPluginIds =
@@ -59,6 +60,7 @@ export function resolvePluginProviders(params: {
workspaceDir: params.workspaceDir,
env,
onlyPluginIds: params.onlyPluginIds,
pluginSdkResolution: params.pluginSdkResolution,
cache: params.cache ?? false,
activate: params.activate ?? false,
logger: createPluginLoaderLogger(log),

View File

@@ -469,6 +469,34 @@ describe("plugin sdk alias helpers", () => {
);
});
it("applies explicit dist resolution to plugin-sdk subpath aliases too", () => {
const fixture = createPluginSdkAliasFixture({
srcFile: "channel-runtime.ts",
distFile: "channel-runtime.js",
packageExports: {
"./plugin-sdk/channel-runtime": { default: "./dist/plugin-sdk/channel-runtime.js" },
},
});
const sourceRootAlias = path.join(fixture.root, "src", "plugin-sdk", "root-alias.cjs");
const distRootAlias = path.join(fixture.root, "dist", "plugin-sdk", "root-alias.cjs");
fs.writeFileSync(sourceRootAlias, "module.exports = {};\n", "utf-8");
fs.writeFileSync(distRootAlias, "module.exports = {};\n", "utf-8");
const sourcePluginEntry = path.join(fixture.root, "extensions", "demo", "src", "index.ts");
fs.mkdirSync(path.dirname(sourcePluginEntry), { recursive: true });
fs.writeFileSync(sourcePluginEntry, 'export const plugin = "demo";\n', "utf-8");
const distAliases = withEnv({ NODE_ENV: undefined }, () =>
buildPluginLoaderAliasMap(sourcePluginEntry, undefined, undefined, "dist"),
);
expect(fs.realpathSync(distAliases["openclaw/plugin-sdk"] ?? "")).toBe(
fs.realpathSync(distRootAlias),
);
expect(fs.realpathSync(distAliases["openclaw/plugin-sdk/channel-runtime"] ?? "")).toBe(
fs.realpathSync(path.join(fixture.root, "dist", "plugin-sdk", "channel-runtime.js")),
);
});
it("resolves plugin-sdk aliases for user-installed plugins via the running openclaw argv hint", () => {
const { externalPluginEntry, externalPluginRoot, fixture, sourceRootAlias } =
createUserInstalledPluginSdkAliasFixture();

View File

@@ -4,12 +4,14 @@ import { fileURLToPath } from "node:url";
import { resolveOpenClawPackageRootSync } from "../infra/openclaw-root.js";
type PluginSdkAliasCandidateKind = "dist" | "src";
export type PluginSdkResolutionPreference = "auto" | "dist" | "src";
export type LoaderModuleResolveParams = {
modulePath?: string;
argv1?: string;
cwd?: string;
moduleUrl?: string;
pluginSdkResolution?: PluginSdkResolutionPreference;
};
type PluginSdkPackageJson = {
@@ -158,13 +160,17 @@ function resolveLoaderPluginSdkPackageRoot(
export function resolvePluginSdkAliasCandidateOrder(params: {
modulePath: string;
isProduction: boolean;
pluginSdkResolution?: PluginSdkResolutionPreference;
}): PluginSdkAliasCandidateKind[] {
if (params.pluginSdkResolution === "dist") {
return ["dist", "src"];
}
if (params.pluginSdkResolution === "src") {
return ["src", "dist"];
}
const normalizedModulePath = params.modulePath.replace(/\\/g, "/");
const isDistRuntime = normalizedModulePath.includes("/dist/");
const preferDistInTests = process.env.OPENCLAW_PLUGIN_SDK_PREFER_DIST === "1";
return isDistRuntime || params.isProduction || preferDistInTests
? ["dist", "src"]
: ["src", "dist"];
return isDistRuntime || params.isProduction ? ["dist", "src"] : ["src", "dist"];
}
export function listPluginSdkAliasCandidates(params: {
@@ -174,10 +180,12 @@ export function listPluginSdkAliasCandidates(params: {
argv1?: string;
cwd?: string;
moduleUrl?: string;
pluginSdkResolution?: PluginSdkResolutionPreference;
}) {
const orderedKinds = resolvePluginSdkAliasCandidateOrder({
modulePath: params.modulePath,
isProduction: process.env.NODE_ENV === "production",
pluginSdkResolution: params.pluginSdkResolution,
});
const packageRoot = resolveLoaderPluginSdkPackageRoot(params);
if (packageRoot) {
@@ -213,6 +221,7 @@ export function resolvePluginSdkAliasFile(params: {
argv1?: string;
cwd?: string;
moduleUrl?: string;
pluginSdkResolution?: PluginSdkResolutionPreference;
}): string | null {
try {
const modulePath = resolveLoaderModulePath(params);
@@ -223,6 +232,7 @@ export function resolvePluginSdkAliasFile(params: {
argv1: params.argv1,
cwd: params.cwd,
moduleUrl: params.moduleUrl,
pluginSdkResolution: params.pluginSdkResolution,
})) {
if (fs.existsSync(candidate)) {
return candidate;
@@ -238,7 +248,12 @@ const cachedPluginSdkExportedSubpaths = new Map<string, string[]>();
const cachedPluginSdkScopedAliasMaps = new Map<string, Record<string, string>>();
export function listPluginSdkExportedSubpaths(
params: { modulePath?: string; argv1?: string; moduleUrl?: string } = {},
params: {
modulePath?: string;
argv1?: string;
moduleUrl?: string;
pluginSdkResolution?: PluginSdkResolutionPreference;
} = {},
): string[] {
const modulePath = params.modulePath ?? fileURLToPath(import.meta.url);
const packageRoot = resolveLoaderPluginSdkPackageRoot({
@@ -259,7 +274,12 @@ export function listPluginSdkExportedSubpaths(
}
export function resolvePluginSdkScopedAliasMap(
params: { modulePath?: string; argv1?: string; moduleUrl?: string } = {},
params: {
modulePath?: string;
argv1?: string;
moduleUrl?: string;
pluginSdkResolution?: PluginSdkResolutionPreference;
} = {},
): Record<string, string> {
const modulePath = params.modulePath ?? fileURLToPath(import.meta.url);
const packageRoot = resolveLoaderPluginSdkPackageRoot({
@@ -273,6 +293,7 @@ export function resolvePluginSdkScopedAliasMap(
const orderedKinds = resolvePluginSdkAliasCandidateOrder({
modulePath,
isProduction: process.env.NODE_ENV === "production",
pluginSdkResolution: params.pluginSdkResolution,
});
const cacheKey = `${packageRoot}::${orderedKinds.join(",")}`;
const cached = cachedPluginSdkScopedAliasMaps.get(cacheKey);
@@ -284,6 +305,7 @@ export function resolvePluginSdkScopedAliasMap(
modulePath,
argv1: params.argv1,
moduleUrl: params.moduleUrl,
pluginSdkResolution: params.pluginSdkResolution,
})) {
const candidateMap = {
src: path.join(packageRoot, "src", "plugin-sdk", `${subpath}.ts`),
@@ -312,6 +334,7 @@ export function resolveExtensionApiAlias(params: LoaderModuleResolveParams = {})
const orderedKinds = resolvePluginSdkAliasCandidateOrder({
modulePath,
isProduction: process.env.NODE_ENV === "production",
pluginSdkResolution: params.pluginSdkResolution,
});
const candidateMap = {
src: path.join(packageRoot, "src", "extensionAPI.ts"),
@@ -333,6 +356,7 @@ export function buildPluginLoaderAliasMap(
modulePath: string,
argv1: string | undefined = STARTUP_ARGV1,
moduleUrl?: string,
pluginSdkResolution: PluginSdkResolutionPreference = "auto",
): Record<string, string> {
const pluginSdkAlias = resolvePluginSdkAliasFile({
srcFile: "root-alias.cjs",
@@ -340,12 +364,13 @@ export function buildPluginLoaderAliasMap(
modulePath,
argv1,
moduleUrl,
pluginSdkResolution,
});
const extensionApiAlias = resolveExtensionApiAlias({ modulePath });
const extensionApiAlias = resolveExtensionApiAlias({ modulePath, pluginSdkResolution });
return {
...(extensionApiAlias ? { "openclaw/extension-api": extensionApiAlias } : {}),
...(pluginSdkAlias ? { "openclaw/plugin-sdk": pluginSdkAlias } : {}),
...resolvePluginSdkScopedAliasMap({ modulePath, argv1, moduleUrl }),
...resolvePluginSdkScopedAliasMap({ modulePath, argv1, moduleUrl, pluginSdkResolution }),
};
}
@@ -357,6 +382,7 @@ export function resolvePluginRuntimeModulePath(
const orderedKinds = resolvePluginSdkAliasCandidateOrder({
modulePath,
isProduction: process.env.NODE_ENV === "production",
pluginSdkResolution: params.pluginSdkResolution,
});
const packageRoot = resolveLoaderPackageRoot({ ...params, modulePath });
const candidates = packageRoot