diff --git a/src/cli/plugin-registry.test.ts b/src/cli/plugin-registry.test.ts index f36849a761e..05644770264 100644 --- a/src/cli/plugin-registry.test.ts +++ b/src/cli/plugin-registry.test.ts @@ -142,7 +142,7 @@ describe("ensurePluginRegistryLoaded", () => { expect(mocks.loadOpenClawPlugins).toHaveBeenCalledTimes(2); expect(mocks.loadOpenClawPlugins).toHaveBeenNthCalledWith( 1, - expect.objectContaining({ onlyPluginIds: [], throwOnLoadError: true }), + expect.objectContaining({ throwOnLoadError: true }), ); expect(mocks.loadOpenClawPlugins).toHaveBeenNthCalledWith( 2, diff --git a/src/plugins/cli-registry-loader.ts b/src/plugins/cli-registry-loader.ts index 92b7ff53fcd..b50c3e5c363 100644 --- a/src/plugins/cli-registry-loader.ts +++ b/src/plugins/cli-registry-loader.ts @@ -1,20 +1,20 @@ -import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js"; import { collectUniqueCommandDescriptors } from "../cli/program/command-descriptor-utils.js"; import type { OpenClawConfig } from "../config/config.js"; -import { loadConfig } from "../config/config.js"; -import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js"; -import { createSubsystemLogger } from "../logging/subsystem.js"; import type { PluginLoadOptions } from "./loader.js"; import { loadOpenClawPluginCliRegistry, loadOpenClawPlugins } from "./loader.js"; import type { PluginRegistry } from "./registry.js"; +import { + buildPluginRuntimeLoadOptions, + createPluginRuntimeLoaderLogger, + resolvePluginRuntimeLoadContext, + type PluginRuntimeLoadContext, +} from "./runtime/load-context.js"; import type { OpenClawPluginCliCommandDescriptor, OpenClawPluginCliContext, PluginLogger, } from "./types.js"; -const log = createSubsystemLogger("plugins"); - export type PluginCliLoaderOptions = Pick; export type PluginCliPublicLoadParams = { @@ -24,13 +24,7 @@ export type PluginCliPublicLoadParams = { logger?: PluginLogger; }; -export type PluginCliLoadContext = { - rawConfig: OpenClawConfig; - config: OpenClawConfig; - autoEnabledReasons: Readonly>; - workspaceDir: string | undefined; - logger: PluginLogger; -}; +export type PluginCliLoadContext = PluginRuntimeLoadContext; export type PluginCliRegistryLoadResult = PluginCliLoadContext & { registry: PluginRegistry; @@ -44,12 +38,7 @@ export type PluginCliCommandGroupEntry = { }; export function createPluginCliLogger(): PluginLogger { - return { - info: (message: string) => log.info(message), - warn: (message: string) => log.warn(message), - error: (message: string) => log.error(message), - debug: (message: string) => log.debug(message), - }; + return createPluginRuntimeLoaderLogger(); } function resolvePluginCliLogger(logger?: PluginLogger): PluginLogger { @@ -80,18 +69,9 @@ function mergeCliRegistrars(params: { function buildPluginCliLoaderParams( context: PluginCliLoadContext, - env?: NodeJS.ProcessEnv, loaderOptions?: PluginCliLoaderOptions, ) { - return { - config: context.config, - activationSourceConfig: context.rawConfig, - autoEnabledReasons: context.autoEnabledReasons, - workspaceDir: context.workspaceDir, - env, - logger: context.logger, - ...loaderOptions, - }; + return buildPluginRuntimeLoadOptions(context, loaderOptions); } export function resolvePluginCliLoadContext(params: { @@ -99,40 +79,32 @@ export function resolvePluginCliLoadContext(params: { env?: NodeJS.ProcessEnv; logger: PluginLogger; }): PluginCliLoadContext { - const rawConfig = params.cfg ?? loadConfig(); - const autoEnabled = applyPluginAutoEnable({ config: rawConfig, env: params.env ?? process.env }); - const config = autoEnabled.config; - const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config)); - return { - rawConfig, - config, - autoEnabledReasons: autoEnabled.autoEnabledReasons, - workspaceDir, + return resolvePluginRuntimeLoadContext({ + config: params.cfg, + env: params.env, logger: params.logger, - }; + }); } export async function loadPluginCliMetadataRegistryWithContext( context: PluginCliLoadContext, - env?: NodeJS.ProcessEnv, loaderOptions?: PluginCliLoaderOptions, ): Promise { return { ...context, registry: await loadOpenClawPluginCliRegistry( - buildPluginCliLoaderParams(context, env, loaderOptions), + buildPluginCliLoaderParams(context, loaderOptions), ), }; } export async function loadPluginCliCommandRegistryWithContext(params: { context: PluginCliLoadContext; - env?: NodeJS.ProcessEnv; loaderOptions?: PluginCliLoaderOptions; onMetadataFallbackError: (error: unknown) => void; }): Promise { const runtimeRegistry = loadOpenClawPlugins( - buildPluginCliLoaderParams(params.context, params.env, params.loaderOptions), + buildPluginCliLoaderParams(params.context, params.loaderOptions), ); if (!hasIgnoredAsyncPluginRegistration(runtimeRegistry)) { @@ -144,7 +116,7 @@ export async function loadPluginCliCommandRegistryWithContext(params: { try { const metadataRegistry = await loadOpenClawPluginCliRegistry( - buildPluginCliLoaderParams(params.context, params.env, params.loaderOptions), + buildPluginCliLoaderParams(params.context, params.loaderOptions), ); return { ...params.context, @@ -202,7 +174,6 @@ export async function loadPluginCliDescriptors( }); const { registry } = await loadPluginCliMetadataRegistryWithContext( context, - params.env, params.loaderOptions, ); return collectUniqueCommandDescriptors( @@ -228,7 +199,6 @@ export async function loadPluginCliRegistrationEntries(params: { }); const { config, workspaceDir, logger, registry } = await loadPluginCliCommandRegistryWithContext({ context, - env: params.env, loaderOptions: params.loaderOptions, onMetadataFallbackError: params.onMetadataFallbackError, }); diff --git a/src/plugins/memory-runtime.test.ts b/src/plugins/memory-runtime.test.ts index 0082587f608..59cfdc738a4 100644 --- a/src/plugins/memory-runtime.test.ts +++ b/src/plugins/memory-runtime.test.ts @@ -3,11 +3,18 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; const resolveRuntimePluginRegistryMock = vi.fn(); const applyPluginAutoEnableMock = vi.fn(); const getMemoryRuntimeMock = vi.fn(); +const resolveAgentWorkspaceDirMock = vi.fn(); +const resolveDefaultAgentIdMock = vi.fn(() => "default"); vi.mock("../config/plugin-auto-enable.js", () => ({ applyPluginAutoEnable: (...args: unknown[]) => applyPluginAutoEnableMock(...args), })); +vi.mock("../agents/agent-scope.js", () => ({ + resolveAgentWorkspaceDir: (...args: unknown[]) => resolveAgentWorkspaceDirMock(...args), + resolveDefaultAgentId: (...args: unknown[]) => resolveDefaultAgentIdMock(...args), +})); + vi.mock("./loader.js", () => ({ resolveRuntimePluginRegistry: (...args: unknown[]) => resolveRuntimePluginRegistryMock(...args), })); @@ -116,11 +123,14 @@ describe("memory runtime auto-enable loading", () => { resolveRuntimePluginRegistryMock.mockReset(); applyPluginAutoEnableMock.mockReset(); getMemoryRuntimeMock.mockReset(); + resolveAgentWorkspaceDirMock.mockReset(); + resolveDefaultAgentIdMock.mockClear(); applyPluginAutoEnableMock.mockImplementation((params: { config: unknown }) => ({ config: params.config, changes: [], autoEnabledReasons: {}, })); + resolveAgentWorkspaceDirMock.mockReturnValue(undefined); }); it.each([ diff --git a/src/plugins/memory-runtime.ts b/src/plugins/memory-runtime.ts index 76aa48b8542..b3d401036ab 100644 --- a/src/plugins/memory-runtime.ts +++ b/src/plugins/memory-runtime.ts @@ -1,19 +1,19 @@ import type { OpenClawConfig } from "../config/config.js"; -import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js"; import { resolveRuntimePluginRegistry } from "./loader.js"; import { getMemoryRuntime } from "./memory-state.js"; +import { + buildPluginRuntimeLoadOptions, + resolvePluginRuntimeLoadContext, +} from "./runtime/load-context.js"; function ensureMemoryRuntime(cfg?: OpenClawConfig) { const current = getMemoryRuntime(); if (current || !cfg) { return current; } - const autoEnabled = applyPluginAutoEnable({ config: cfg, env: process.env }); - resolveRuntimePluginRegistry({ - config: autoEnabled.config, - activationSourceConfig: cfg, - autoEnabledReasons: autoEnabled.autoEnabledReasons, - }); + resolveRuntimePluginRegistry( + buildPluginRuntimeLoadOptions(resolvePluginRuntimeLoadContext({ config: cfg })), + ); return getMemoryRuntime(); } diff --git a/src/plugins/providers.runtime.ts b/src/plugins/providers.runtime.ts index f088e12781f..b0f5550f820 100644 --- a/src/plugins/providers.runtime.ts +++ b/src/plugins/providers.runtime.ts @@ -1,4 +1,3 @@ -import { createSubsystemLogger } from "../logging/subsystem.js"; import { withActivatedPluginIds } from "./activation-context.js"; import { resolveBundledPluginCompatibleActivationInputs } from "./activation-context.js"; import { @@ -6,7 +5,6 @@ import { resolveRuntimePluginRegistry, type PluginLoadOptions, } from "./loader.js"; -import { createPluginLoaderLogger } from "./logger.js"; import { resolveDiscoveredProviderPluginIds, resolveEnabledProviderPluginIds, @@ -16,9 +14,12 @@ import { withBundledProviderVitestCompat, } from "./providers.js"; import { getActivePluginRegistryWorkspaceDir } from "./runtime.js"; +import { + buildPluginRuntimeLoadOptionsFromValues, + createPluginRuntimeLoaderLogger, +} from "./runtime/load-context.js"; import type { ProviderPlugin } from "./types.js"; -const log = createSubsystemLogger("plugins"); export function resolvePluginProviders(params: { config?: PluginLoadOptions["config"]; workspaceDir?: string; @@ -87,21 +88,27 @@ export function resolvePluginProviders(params: { if (providerPluginIds.length === 0) { return []; } - const registry = loadOpenClawPlugins({ - config: withActivatedPluginIds({ - config: runtimeConfig, - pluginIds: providerPluginIds, - }), - activationSourceConfig: runtimeConfig, - autoEnabledReasons: {}, - workspaceDir, - env, - onlyPluginIds: providerPluginIds, - pluginSdkResolution: params.pluginSdkResolution, - cache: params.cache ?? false, - activate: params.activate ?? false, - logger: createPluginLoaderLogger(log), - }); + const registry = loadOpenClawPlugins( + buildPluginRuntimeLoadOptionsFromValues( + { + config: withActivatedPluginIds({ + config: runtimeConfig, + pluginIds: providerPluginIds, + }), + activationSourceConfig: runtimeConfig, + autoEnabledReasons: {}, + workspaceDir, + env, + logger: createPluginRuntimeLoaderLogger(), + }, + { + onlyPluginIds: providerPluginIds, + pluginSdkResolution: params.pluginSdkResolution, + cache: params.cache ?? false, + activate: params.activate ?? false, + }, + ), + ); return registry.providers.map((entry) => ({ ...entry.provider, pluginId: entry.pluginId, @@ -133,18 +140,24 @@ export function resolvePluginProviders(params: { env, onlyPluginIds: requestedPluginIds, }); - const registry = resolveRuntimePluginRegistry({ - config, - activationSourceConfig: activation.activationSourceConfig, - autoEnabledReasons: activation.autoEnabledReasons, - workspaceDir, - env, - onlyPluginIds: providerPluginIds, - pluginSdkResolution: params.pluginSdkResolution, - cache: params.cache ?? false, - activate: params.activate ?? false, - logger: createPluginLoaderLogger(log), - }); + const registry = resolveRuntimePluginRegistry( + buildPluginRuntimeLoadOptionsFromValues( + { + config, + activationSourceConfig: activation.activationSourceConfig, + autoEnabledReasons: activation.autoEnabledReasons, + workspaceDir, + env, + logger: createPluginRuntimeLoaderLogger(), + }, + { + onlyPluginIds: providerPluginIds, + pluginSdkResolution: params.pluginSdkResolution, + cache: params.cache ?? false, + activate: params.activate ?? false, + }, + ), + ); if (!registry) { return []; } diff --git a/src/plugins/providers.test.ts b/src/plugins/providers.test.ts index b0e3e09b872..0ecbcf24376 100644 --- a/src/plugins/providers.test.ts +++ b/src/plugins/providers.test.ts @@ -63,6 +63,7 @@ function setOwningProviderManifestPlugins() { createManifestProviderPlugin({ id: "openai", providerIds: ["openai", "openai-codex"], + cliBackends: ["codex-cli"], modelSupport: { modelPrefixes: ["gpt-", "o1", "o3", "o4"], }, @@ -87,6 +88,7 @@ function setOwningProviderManifestPluginsWithWorkspace() { createManifestProviderPlugin({ id: "openai", providerIds: ["openai", "openai-codex"], + cliBackends: ["codex-cli"], modelSupport: { modelPrefixes: ["gpt-", "o1", "o3", "o4"], }, @@ -255,6 +257,10 @@ function expectProviderRuntimeRegistryLoad(params?: { config?: unknown; env?: No describe("resolvePluginProviders", () => { beforeAll(async () => { vi.resetModules(); + loadPluginManifestRegistryMock.mockReturnValue({ + plugins: [], + diagnostics: [], + }); vi.doMock("./loader.js", () => ({ loadOpenClawPlugins: (...args: Parameters) => loadOpenClawPluginsMock(...args), diff --git a/src/plugins/runtime/load-context.test.ts b/src/plugins/runtime/load-context.test.ts new file mode 100644 index 00000000000..421c11b8761 --- /dev/null +++ b/src/plugins/runtime/load-context.test.ts @@ -0,0 +1,115 @@ +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +const loadConfigMock = vi.fn(); +const applyPluginAutoEnableMock = vi.fn(); +const resolveAgentWorkspaceDirMock = vi.fn(() => "/resolved-workspace"); +const resolveDefaultAgentIdMock = vi.fn(() => "default"); + +let resolvePluginRuntimeLoadContext: typeof import("./load-context.js").resolvePluginRuntimeLoadContext; +let buildPluginRuntimeLoadOptions: typeof import("./load-context.js").buildPluginRuntimeLoadOptions; + +vi.mock("../../config/config.js", () => ({ + loadConfig: () => loadConfigMock(), +})); + +vi.mock("../../config/plugin-auto-enable.js", () => ({ + applyPluginAutoEnable: (...args: unknown[]) => applyPluginAutoEnableMock(...args), +})); + +vi.mock("../../agents/agent-scope.js", () => ({ + resolveAgentWorkspaceDir: (...args: unknown[]) => resolveAgentWorkspaceDirMock(...args), + resolveDefaultAgentId: (...args: unknown[]) => resolveDefaultAgentIdMock(...args), +})); + +describe("resolvePluginRuntimeLoadContext", () => { + beforeAll(async () => { + ({ resolvePluginRuntimeLoadContext, buildPluginRuntimeLoadOptions } = + await import("./load-context.js")); + }); + + beforeEach(() => { + loadConfigMock.mockReset(); + applyPluginAutoEnableMock.mockReset(); + resolveAgentWorkspaceDirMock.mockClear(); + resolveDefaultAgentIdMock.mockClear(); + + loadConfigMock.mockReturnValue({ plugins: {} }); + applyPluginAutoEnableMock.mockImplementation((params: { config: unknown }) => ({ + config: params.config, + changes: [], + autoEnabledReasons: {}, + })); + }); + + it("builds the runtime plugin load context from the auto-enabled config", () => { + const rawConfig = { plugins: {} }; + const resolvedConfig = { + plugins: { + entries: { + demo: { enabled: true }, + }, + }, + }; + const env = { HOME: "/tmp/openclaw-home" } as NodeJS.ProcessEnv; + + applyPluginAutoEnableMock.mockReturnValue({ + config: resolvedConfig, + changes: [], + autoEnabledReasons: { + demo: ["demo configured"], + }, + }); + + const context = resolvePluginRuntimeLoadContext({ + config: rawConfig, + env, + }); + + expect(context).toEqual( + expect.objectContaining({ + rawConfig, + config: resolvedConfig, + activationSourceConfig: rawConfig, + env, + workspaceDir: "/resolved-workspace", + autoEnabledReasons: { + demo: ["demo configured"], + }, + }), + ); + expect(applyPluginAutoEnableMock).toHaveBeenCalledWith({ + config: rawConfig, + env, + }); + expect(resolveDefaultAgentIdMock).toHaveBeenCalledWith(resolvedConfig); + expect(resolveAgentWorkspaceDirMock).toHaveBeenCalledWith(resolvedConfig, "default"); + }); + + it("builds plugin load options from the shared runtime context", () => { + const context = resolvePluginRuntimeLoadContext({ + config: { plugins: {} }, + env: { HOME: "/tmp/openclaw-home" } as NodeJS.ProcessEnv, + workspaceDir: "/explicit-workspace", + }); + + expect( + buildPluginRuntimeLoadOptions(context, { + cache: false, + activate: false, + onlyPluginIds: ["demo"], + }), + ).toEqual( + expect.objectContaining({ + config: context.config, + activationSourceConfig: context.activationSourceConfig, + autoEnabledReasons: context.autoEnabledReasons, + workspaceDir: "/explicit-workspace", + env: context.env, + logger: context.logger, + cache: false, + activate: false, + onlyPluginIds: ["demo"], + }), + ); + }); +}); diff --git a/src/plugins/runtime/load-context.ts b/src/plugins/runtime/load-context.ts new file mode 100644 index 00000000000..ea79e96d00e --- /dev/null +++ b/src/plugins/runtime/load-context.ts @@ -0,0 +1,83 @@ +import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js"; +import type { OpenClawConfig } from "../../config/config.js"; +import { loadConfig } from "../../config/config.js"; +import { applyPluginAutoEnable } from "../../config/plugin-auto-enable.js"; +import { createSubsystemLogger } from "../../logging.js"; +import type { PluginLoadOptions } from "../loader.js"; +import type { PluginLogger } from "../types.js"; + +const log = createSubsystemLogger("plugins"); + +export type PluginRuntimeLoadContext = { + rawConfig: OpenClawConfig; + config: OpenClawConfig; + activationSourceConfig: OpenClawConfig; + autoEnabledReasons: Readonly>; + workspaceDir: string | undefined; + env: NodeJS.ProcessEnv; + logger: PluginLogger; +}; + +export type PluginRuntimeResolvedLoadValues = Pick< + PluginLoadOptions, + "config" | "activationSourceConfig" | "autoEnabledReasons" | "workspaceDir" | "env" | "logger" +>; + +export type PluginRuntimeLoadContextOptions = { + config?: OpenClawConfig; + activationSourceConfig?: OpenClawConfig; + env?: NodeJS.ProcessEnv; + workspaceDir?: string; + logger?: PluginLogger; +}; + +export function createPluginRuntimeLoaderLogger(): PluginLogger { + return { + info: (message) => log.info(message), + warn: (message) => log.warn(message), + error: (message) => log.error(message), + debug: (message) => log.debug(message), + }; +} + +export function resolvePluginRuntimeLoadContext( + options?: PluginRuntimeLoadContextOptions, +): PluginRuntimeLoadContext { + const env = options?.env ?? process.env; + const rawConfig = options?.config ?? loadConfig(); + const autoEnabled = applyPluginAutoEnable({ config: rawConfig, env }); + const config = autoEnabled.config; + const workspaceDir = + options?.workspaceDir ?? resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config)); + return { + rawConfig, + config, + activationSourceConfig: options?.activationSourceConfig ?? rawConfig, + autoEnabledReasons: autoEnabled.autoEnabledReasons, + workspaceDir, + env, + logger: options?.logger ?? createPluginRuntimeLoaderLogger(), + }; +} + +export function buildPluginRuntimeLoadOptions( + context: PluginRuntimeLoadContext, + overrides?: Partial, +): PluginLoadOptions { + return buildPluginRuntimeLoadOptionsFromValues(context, overrides); +} + +export function buildPluginRuntimeLoadOptionsFromValues( + values: PluginRuntimeResolvedLoadValues, + overrides?: Partial, +): PluginLoadOptions { + return { + config: values.config, + activationSourceConfig: values.activationSourceConfig, + autoEnabledReasons: values.autoEnabledReasons, + workspaceDir: values.workspaceDir, + env: values.env, + logger: values.logger, + ...overrides, + }; +} diff --git a/src/plugins/runtime/metadata-registry-loader.ts b/src/plugins/runtime/metadata-registry-loader.ts index ae186427bb0..3e4c005ecab 100644 --- a/src/plugins/runtime/metadata-registry-loader.ts +++ b/src/plugins/runtime/metadata-registry-loader.ts @@ -1,13 +1,7 @@ -import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js"; import type { OpenClawConfig } from "../../config/config.js"; -import { loadConfig } from "../../config/config.js"; -import { applyPluginAutoEnable } from "../../config/plugin-auto-enable.js"; -import { createSubsystemLogger } from "../../logging.js"; import { loadOpenClawPlugins } from "../loader.js"; import type { PluginRegistry } from "../registry.js"; -import type { PluginLogger } from "../types.js"; - -const log = createSubsystemLogger("plugins"); +import { buildPluginRuntimeLoadOptions, resolvePluginRuntimeLoadContext } from "./load-context.js"; export function loadPluginMetadataRegistrySnapshot(options?: { config?: OpenClawConfig; @@ -17,32 +11,16 @@ export function loadPluginMetadataRegistrySnapshot(options?: { onlyPluginIds?: string[]; loadModules?: boolean; }): PluginRegistry { - const env = options?.env ?? process.env; - const baseConfig = options?.config ?? loadConfig(); - const autoEnabled = applyPluginAutoEnable({ config: baseConfig, env }); - const resolvedConfig = autoEnabled.config; - const workspaceDir = - options?.workspaceDir ?? - resolveAgentWorkspaceDir(resolvedConfig, resolveDefaultAgentId(resolvedConfig)); - const logger: PluginLogger = { - info: (message) => log.info(message), - warn: (message) => log.warn(message), - error: (message) => log.error(message), - debug: (message) => log.debug(message), - }; + const context = resolvePluginRuntimeLoadContext(options); - return loadOpenClawPlugins({ - config: resolvedConfig, - activationSourceConfig: options?.activationSourceConfig ?? baseConfig, - autoEnabledReasons: autoEnabled.autoEnabledReasons, - workspaceDir, - env, - logger, - throwOnLoadError: true, - cache: false, - activate: false, - mode: "validate", - loadModules: options?.loadModules, - ...(options?.onlyPluginIds?.length ? { onlyPluginIds: options.onlyPluginIds } : {}), - }); + return loadOpenClawPlugins( + buildPluginRuntimeLoadOptions(context, { + throwOnLoadError: true, + cache: false, + activate: false, + mode: "validate", + loadModules: options?.loadModules, + ...(options?.onlyPluginIds?.length ? { onlyPluginIds: options.onlyPluginIds } : {}), + }), + ); } diff --git a/src/plugins/runtime/runtime-registry-loader.test.ts b/src/plugins/runtime/runtime-registry-loader.test.ts new file mode 100644 index 00000000000..809898c008b --- /dev/null +++ b/src/plugins/runtime/runtime-registry-loader.test.ts @@ -0,0 +1,145 @@ +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +const loadOpenClawPluginsMock = vi.fn(); +const getActivePluginRegistryMock = vi.fn(); +const resolveConfiguredChannelPluginIdsMock = vi.fn(); +const resolveChannelPluginIdsMock = vi.fn(); +const applyPluginAutoEnableMock = vi.fn(); +const resolveAgentWorkspaceDirMock = vi.fn(() => "/resolved-workspace"); +const resolveDefaultAgentIdMock = vi.fn(() => "default"); + +let ensurePluginRegistryLoaded: typeof import("./runtime-registry-loader.js").ensurePluginRegistryLoaded; +let resetPluginRegistryLoadedForTests: typeof import("./runtime-registry-loader.js").__testing.resetPluginRegistryLoadedForTests; + +vi.mock("../loader.js", () => ({ + loadOpenClawPlugins: (...args: unknown[]) => loadOpenClawPluginsMock(...args), +})); + +vi.mock("../runtime.js", () => ({ + getActivePluginRegistry: (...args: unknown[]) => getActivePluginRegistryMock(...args), +})); + +vi.mock("../channel-plugin-ids.js", () => ({ + resolveConfiguredChannelPluginIds: (...args: unknown[]) => + resolveConfiguredChannelPluginIdsMock(...args), + resolveChannelPluginIds: (...args: unknown[]) => resolveChannelPluginIdsMock(...args), +})); + +vi.mock("../../config/plugin-auto-enable.js", () => ({ + applyPluginAutoEnable: (...args: unknown[]) => applyPluginAutoEnableMock(...args), +})); + +vi.mock("../../agents/agent-scope.js", () => ({ + resolveAgentWorkspaceDir: (...args: unknown[]) => resolveAgentWorkspaceDirMock(...args), + resolveDefaultAgentId: (...args: unknown[]) => resolveDefaultAgentIdMock(...args), +})); + +describe("ensurePluginRegistryLoaded", () => { + beforeAll(async () => { + const mod = await import("./runtime-registry-loader.js"); + ensurePluginRegistryLoaded = mod.ensurePluginRegistryLoaded; + resetPluginRegistryLoadedForTests = () => mod.__testing.resetPluginRegistryLoadedForTests(); + }); + + beforeEach(() => { + loadOpenClawPluginsMock.mockReset(); + getActivePluginRegistryMock.mockReset(); + resolveConfiguredChannelPluginIdsMock.mockReset(); + resolveChannelPluginIdsMock.mockReset(); + applyPluginAutoEnableMock.mockReset(); + resolveAgentWorkspaceDirMock.mockClear(); + resolveDefaultAgentIdMock.mockClear(); + resetPluginRegistryLoadedForTests(); + + getActivePluginRegistryMock.mockReturnValue({ + plugins: [], + channels: [], + tools: [], + }); + applyPluginAutoEnableMock.mockImplementation((params: { config: unknown }) => ({ + config: + params.config && typeof params.config === "object" + ? { + ...params.config, + plugins: { + entries: { + demo: { enabled: true }, + }, + }, + } + : params.config, + changes: [], + autoEnabledReasons: { + demo: ["demo configured"], + }, + })); + }); + + it("uses the shared runtime load context for configured-channel loads", () => { + const rawConfig = { channels: { demo: { enabled: true } } }; + const resolvedConfig = { + ...rawConfig, + plugins: { + entries: { + demo: { enabled: true }, + }, + }, + }; + const env = { HOME: "/tmp/openclaw-home" } as NodeJS.ProcessEnv; + + resolveConfiguredChannelPluginIdsMock.mockReturnValue(["demo-channel"]); + ensurePluginRegistryLoaded({ + scope: "configured-channels", + config: rawConfig as never, + env, + activationSourceConfig: { plugins: { allow: ["demo-channel"] } } as never, + }); + + expect(resolveConfiguredChannelPluginIdsMock).toHaveBeenCalledWith( + expect.objectContaining({ + config: resolvedConfig, + env, + workspaceDir: "/resolved-workspace", + }), + ); + expect(applyPluginAutoEnableMock).toHaveBeenCalledWith({ + config: rawConfig, + env, + }); + expect(loadOpenClawPluginsMock).toHaveBeenCalledWith( + expect.objectContaining({ + config: resolvedConfig, + activationSourceConfig: { plugins: { allow: ["demo-channel"] } }, + autoEnabledReasons: { + demo: ["demo configured"], + }, + workspaceDir: "/resolved-workspace", + onlyPluginIds: ["demo-channel"], + throwOnLoadError: true, + }), + ); + }); + + it("does not cache scoped loads by explicit plugin ids", () => { + ensurePluginRegistryLoaded({ + scope: "configured-channels", + config: {} as never, + onlyPluginIds: ["demo-a"], + }); + ensurePluginRegistryLoaded({ + scope: "configured-channels", + config: {} as never, + onlyPluginIds: ["demo-b"], + }); + + expect(loadOpenClawPluginsMock).toHaveBeenCalledTimes(2); + expect(loadOpenClawPluginsMock).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ onlyPluginIds: ["demo-a"] }), + ); + expect(loadOpenClawPluginsMock).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ onlyPluginIds: ["demo-b"] }), + ); + }); +}); diff --git a/src/plugins/runtime/runtime-registry-loader.ts b/src/plugins/runtime/runtime-registry-loader.ts index 091358f4c9d..f953c9cde0a 100644 --- a/src/plugins/runtime/runtime-registry-loader.ts +++ b/src/plugins/runtime/runtime-registry-loader.ts @@ -1,17 +1,12 @@ -import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js"; import type { OpenClawConfig } from "../../config/config.js"; -import { loadConfig } from "../../config/config.js"; -import { applyPluginAutoEnable } from "../../config/plugin-auto-enable.js"; -import { createSubsystemLogger } from "../../logging.js"; import { resolveChannelPluginIds, resolveConfiguredChannelPluginIds, } from "../channel-plugin-ids.js"; import { loadOpenClawPlugins } from "../loader.js"; import { getActivePluginRegistry } from "../runtime.js"; -import type { PluginLogger } from "../types.js"; +import { buildPluginRuntimeLoadOptions, resolvePluginRuntimeLoadContext } from "./load-context.js"; -const log = createSubsystemLogger("plugins"); let pluginRegistryLoaded: "none" | "configured-channels" | "channels" | "all" = "none"; export type PluginRegistryScope = "configured-channels" | "channels" | "all"; @@ -71,27 +66,20 @@ export function ensurePluginRegistryLoaded(options?: { if (!scopedLoad && scopeRank(pluginRegistryLoaded) >= scopeRank(scope)) { return; } - const env = options?.env ?? process.env; - const baseConfig = options?.config ?? loadConfig(); - const autoEnabled = applyPluginAutoEnable({ config: baseConfig, env }); - const resolvedConfig = autoEnabled.config; - const workspaceDir = resolveAgentWorkspaceDir( - resolvedConfig, - resolveDefaultAgentId(resolvedConfig), - ); + const context = resolvePluginRuntimeLoadContext(options); const expectedChannelPluginIds = scopedLoad ? requestedPluginIds : scope === "configured-channels" ? resolveConfiguredChannelPluginIds({ - config: resolvedConfig, - workspaceDir, - env, + config: context.config, + workspaceDir: context.workspaceDir, + env: context.env, }) : scope === "channels" ? resolveChannelPluginIds({ - config: resolvedConfig, - workspaceDir, - env, + config: context.config, + workspaceDir: context.workspaceDir, + env: context.env, }) : []; const active = getActivePluginRegistry(); @@ -104,21 +92,12 @@ export function ensurePluginRegistryLoaded(options?: { } return; } - const logger: PluginLogger = { - info: (msg) => log.info(msg), - warn: (msg) => log.warn(msg), - error: (msg) => log.error(msg), - debug: (msg) => log.debug(msg), - }; - loadOpenClawPlugins({ - config: resolvedConfig, - activationSourceConfig: options?.activationSourceConfig ?? baseConfig, - autoEnabledReasons: autoEnabled.autoEnabledReasons, - workspaceDir, - logger, - throwOnLoadError: true, - ...(expectedChannelPluginIds.length > 0 ? { onlyPluginIds: expectedChannelPluginIds } : {}), - }); + loadOpenClawPlugins( + buildPluginRuntimeLoadOptions(context, { + throwOnLoadError: true, + ...(expectedChannelPluginIds.length > 0 ? { onlyPluginIds: expectedChannelPluginIds } : {}), + }), + ); if (!scopedLoad) { pluginRegistryLoaded = scope; } diff --git a/src/plugins/status.test.ts b/src/plugins/status.test.ts index 379dfe89663..999978d6ab3 100644 --- a/src/plugins/status.test.ts +++ b/src/plugins/status.test.ts @@ -674,7 +674,7 @@ describe("plugin status reports", () => { expectCapabilityKinds(inspect[1], ["text-inference", "web-search"]); }); - it("treats a CLI-command-only plugin as a non-capability", () => { + it("treats a CLI-command-only plugin as a plain capability", () => { setSinglePluginLoadResult( createPluginRecord({ id: "anthropic", @@ -686,9 +686,9 @@ describe("plugin status reports", () => { const inspect = expectInspectReport("anthropic"); expectInspectShape(inspect, { - shape: "non-capability", - capabilityMode: "none", - capabilityKinds: [], + shape: "plain-capability", + capabilityMode: "plain", + capabilityKinds: ["cli-backend"], }); expect(inspect.capabilities).toEqual([{ kind: "cli-backend", ids: ["claude-cli"] }]); }); diff --git a/src/plugins/status.ts b/src/plugins/status.ts index 4e45c9f4221..3712a431d41 100644 --- a/src/plugins/status.ts +++ b/src/plugins/status.ts @@ -1,9 +1,6 @@ -import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js"; import { resolveDefaultAgentWorkspaceDir } from "../agents/workspace.js"; import { loadConfig } from "../config/config.js"; -import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js"; import { normalizeOpenClawVersionBase } from "../config/version.js"; -import { createSubsystemLogger } from "../logging/subsystem.js"; import { listImportedBundledPluginFacadeIds } from "../plugin-sdk/facade-runtime.js"; import { resolveCompatibilityHostVersion } from "../version.js"; import { inspectBundleLspRuntimeSupport } from "./bundle-lsp.js"; @@ -14,10 +11,13 @@ import { } from "./bundled-compat.js"; import { normalizePluginsConfig } from "./config-state.js"; import { loadOpenClawPlugins } from "./loader.js"; -import { createPluginLoaderLogger } from "./logger.js"; import { resolveBundledProviderCompatPluginIds } from "./providers.js"; import type { PluginRegistry } from "./registry.js"; import { listImportedRuntimePluginIds } from "./runtime.js"; +import { + buildPluginRuntimeLoadOptions, + resolvePluginRuntimeLoadContext, +} from "./runtime/load-context.js"; import { loadPluginMetadataRegistrySnapshot } from "./runtime/metadata-registry-loader.js"; import type { PluginDiagnostic, PluginHookName } from "./types.js"; @@ -126,18 +126,6 @@ function buildCompatibilityNoticesForInspect( return warnings; } -const log = createSubsystemLogger("plugins"); - -function resolveStatusConfig( - config: ReturnType, - env: NodeJS.ProcessEnv | undefined, -) { - return applyPluginAutoEnable({ - config, - env: env ?? process.env, - }); -} - function resolveReportedPluginVersion( plugin: PluginRegistry["plugins"][number], env: NodeJS.ProcessEnv | undefined, @@ -163,13 +151,21 @@ function buildPluginReport( params: PluginReportParams | undefined, loadModules: boolean, ): PluginStatusReport { - const rawConfig = params?.config ?? loadConfig(); - const autoEnabled = resolveStatusConfig(rawConfig, params?.env); - const config = autoEnabled.config; - const workspaceDir = params?.workspaceDir - ? params.workspaceDir - : (resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config)) ?? - resolveDefaultAgentWorkspaceDir()); + const baseContext = resolvePluginRuntimeLoadContext({ + config: params?.config ?? loadConfig(), + env: params?.env, + workspaceDir: params?.workspaceDir, + }); + const workspaceDir = baseContext.workspaceDir ?? resolveDefaultAgentWorkspaceDir(); + const context = + workspaceDir === baseContext.workspaceDir + ? baseContext + : { + ...baseContext, + workspaceDir, + }; + const rawConfig = context.rawConfig; + const config = context.config; // Apply bundled-provider allowlist compat so that `plugins list` and `doctor` // report the same loaded/disabled status the gateway uses at runtime. Without @@ -192,20 +188,21 @@ function buildPluginReport( }); const registry = loadModules - ? loadOpenClawPlugins({ - config: runtimeCompatConfig, - activationSourceConfig: rawConfig, - autoEnabledReasons: autoEnabled.autoEnabledReasons, - workspaceDir, - env: params?.env, - logger: createPluginLoaderLogger(log), - activate: false, - cache: false, - loadModules, - }) + ? loadOpenClawPlugins( + buildPluginRuntimeLoadOptions(context, { + config: runtimeCompatConfig, + activationSourceConfig: rawConfig, + workspaceDir, + env: params?.env, + loadModules, + activate: false, + cache: false, + }), + ) : loadPluginMetadataRegistrySnapshot({ config: runtimeCompatConfig, activationSourceConfig: rawConfig, + activate: false, workspaceDir, env: params?.env, loadModules: false, @@ -292,8 +289,11 @@ export function buildPluginInspectReport(params: { report?: PluginStatusReport; }): PluginInspectReport | null { const rawConfig = params.config ?? loadConfig(); - const resolvedConfig = resolveStatusConfig(rawConfig, params.env); - const config = resolvedConfig.config; + const config = resolvePluginRuntimeLoadContext({ + config: rawConfig, + env: params.env, + workspaceDir: params.workspaceDir, + }).config; const report = params.report ?? buildPluginDiagnosticsReport({ diff --git a/src/plugins/tools.ts b/src/plugins/tools.ts index 5b8441861bc..97ef9df8bdd 100644 --- a/src/plugins/tools.ts +++ b/src/plugins/tools.ts @@ -1,19 +1,18 @@ import { normalizeToolName } from "../agents/tool-policy.js"; import type { AnyAgentTool } from "../agents/tools/common.js"; -import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js"; -import { createSubsystemLogger } from "../logging/subsystem.js"; import { applyTestPluginDefaults, normalizePluginsConfig } from "./config-state.js"; import { resolveRuntimePluginRegistry, type PluginLoadOptions } from "./loader.js"; -import { createPluginLoaderLogger } from "./logger.js"; import { getActivePluginRegistry, getActivePluginRegistryKey, getActivePluginRuntimeSubagentMode, } from "./runtime.js"; +import { + buildPluginRuntimeLoadOptions, + resolvePluginRuntimeLoadContext, +} from "./runtime/load-context.js"; import type { OpenClawPluginToolContext } from "./types.js"; -const log = createSubsystemLogger("plugins"); - type PluginToolMeta = { pluginId: string; optional: boolean; @@ -81,9 +80,12 @@ export function resolvePluginTools(params: { // This matters a lot for unit tests and for tool construction hot paths. const env = params.env ?? process.env; const baseConfig = applyTestPluginDefaults(params.context.config ?? {}, env); - const autoEnabled = applyPluginAutoEnable({ config: baseConfig, env }); - const effectiveConfig = autoEnabled.config; - const normalized = normalizePluginsConfig(effectiveConfig.plugins); + const context = resolvePluginRuntimeLoadContext({ + config: baseConfig, + env, + workspaceDir: params.context.workspaceDir, + }); + const normalized = normalizePluginsConfig(context.config.plugins); if (!normalized.enabled) { return []; } @@ -91,15 +93,7 @@ export function resolvePluginTools(params: { const runtimeOptions = params.allowGatewaySubagentBinding ? { allowGatewaySubagentBinding: true as const } : undefined; - const loadOptions = { - config: effectiveConfig, - activationSourceConfig: baseConfig, - autoEnabledReasons: autoEnabled.autoEnabledReasons, - workspaceDir: params.context.workspaceDir, - runtimeOptions, - env, - logger: createPluginLoaderLogger(log), - }; + const loadOptions = buildPluginRuntimeLoadOptions(context, { runtimeOptions }); const registry = resolvePluginToolRegistry({ loadOptions, allowGatewaySubagentBinding: params.allowGatewaySubagentBinding, @@ -122,7 +116,7 @@ export function resolvePluginTools(params: { if (existingNormalized.has(pluginIdKey)) { const message = `plugin id conflicts with core tool name (${entry.pluginId})`; if (!params.suppressNameConflicts) { - log.error(message); + context.logger.error(message); registry.diagnostics.push({ level: "error", pluginId: entry.pluginId, @@ -137,12 +131,12 @@ export function resolvePluginTools(params: { try { resolved = entry.factory(params.context); } catch (err) { - log.error(`plugin tool failed (${entry.pluginId}): ${String(err)}`); + context.logger.error(`plugin tool failed (${entry.pluginId}): ${String(err)}`); continue; } if (!resolved) { if (entry.names.length > 0) { - log.debug( + context.logger.debug( `plugin tool factory returned null (${entry.pluginId}): [${entry.names.join(", ")}]`, ); } @@ -166,7 +160,7 @@ export function resolvePluginTools(params: { if (nameSet.has(tool.name) || existing.has(tool.name)) { const message = `plugin tool name conflict (${entry.pluginId}): ${tool.name}`; if (!params.suppressNameConflicts) { - log.error(message); + context.logger.error(message); registry.diagnostics.push({ level: "error", pluginId: entry.pluginId,