diff --git a/src/plugin-sdk/root-alias.cjs b/src/plugin-sdk/root-alias.cjs index 7c4375c3324..d94e21f2282 100644 --- a/src/plugin-sdk/root-alias.cjs +++ b/src/plugin-sdk/root-alias.cjs @@ -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) { diff --git a/src/plugins/bundled-capability-runtime.ts b/src/plugins/bundled-capability-runtime.ts index a0e70df6cbc..110c5c07dc9 100644 --- a/src/plugins/bundled-capability-runtime.ts +++ b/src/plugins/bundled-capability-runtime.ts @@ -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: { diff --git a/src/plugins/cli.browser-plugin.integration.test.ts b/src/plugins/cli.browser-plugin.integration.test.ts index fb23b2bad87..cd9eafb2c78 100644 --- a/src/plugins/cli.browser-plugin.integration.test.ts +++ b/src/plugins/cli.browser-plugin.integration.test.ts @@ -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"); }); diff --git a/src/plugins/cli.ts b/src/plugins/cli.ts index 414a3657e6a..24b91afc392 100644 --- a/src/plugins/cli.ts +++ b/src/plugins/cli.ts @@ -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, +) { 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, ) { - 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())); diff --git a/src/plugins/contracts/provider.contract.test.ts b/src/plugins/contracts/provider.contract.test.ts index 803ab030560..db5ce6e3c03 100644 --- a/src/plugins/contracts/provider.contract.test.ts +++ b/src/plugins/contracts/provider.contract.test.ts @@ -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", () => { diff --git a/src/plugins/contracts/registry.ts b/src/plugins/contracts/registry.ts index ec872a5e158..d325a3b8bc8 100644 --- a/src/plugins/contracts/registry.ts +++ b/src/plugins/contracts/registry.ts @@ -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, diff --git a/src/plugins/contracts/web-search-provider.contract.test.ts b/src/plugins/contracts/web-search-provider.contract.test.ts index 582608d9676..ca51d97862e 100644 --- a/src/plugins/contracts/web-search-provider.contract.test.ts +++ b/src/plugins/contracts/web-search-provider.contract.test.ts @@ -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", () => { diff --git a/src/plugins/loader.test.ts b/src/plugins/loader.test.ts index 9064a6f4373..48ea86566a3 100644 --- a/src/plugins/loader.test.ts +++ b/src/plugins/loader.test.ts @@ -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, diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index 531ac6d5be7..5e881b4c4aa 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -46,6 +46,7 @@ import { buildPluginLoaderJitiOptions, listPluginSdkAliasCandidates, listPluginSdkExportedSubpaths, + type PluginSdkResolutionPreference, resolveExtensionApiAlias, resolvePluginSdkAliasCandidateOrder, resolvePluginSdkAliasFile, @@ -73,6 +74,7 @@ export type PluginLoadOptions = { logger?: PluginLogger; coreGatewayHandlers?: Record; 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"); } diff --git a/src/plugins/providers.runtime.ts b/src/plugins/providers.runtime.ts index 7b46b6221f4..4658274039d 100644 --- a/src/plugins/providers.runtime.ts +++ b/src/plugins/providers.runtime.ts @@ -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), diff --git a/src/plugins/sdk-alias.test.ts b/src/plugins/sdk-alias.test.ts index af43354c4a6..575f9e7a5c5 100644 --- a/src/plugins/sdk-alias.test.ts +++ b/src/plugins/sdk-alias.test.ts @@ -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(); diff --git a/src/plugins/sdk-alias.ts b/src/plugins/sdk-alias.ts index 1b9475984a8..fbeb3538063 100644 --- a/src/plugins/sdk-alias.ts +++ b/src/plugins/sdk-alias.ts @@ -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(); const cachedPluginSdkScopedAliasMaps = new Map>(); 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 { 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 { 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