refactor: share plugin runtime load context

This commit is contained in:
Peter Steinberger
2026-04-06 15:26:12 +01:00
parent 9568cceee3
commit 58f4099a4f
14 changed files with 507 additions and 214 deletions

View File

@@ -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,

View File

@@ -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<PluginLoadOptions, "pluginSdkResolution">;
export type PluginCliPublicLoadParams = {
@@ -24,13 +24,7 @@ export type PluginCliPublicLoadParams = {
logger?: PluginLogger;
};
export type PluginCliLoadContext = {
rawConfig: OpenClawConfig;
config: OpenClawConfig;
autoEnabledReasons: Readonly<Record<string, string[]>>;
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<PluginCliRegistryLoadResult> {
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<PluginCliRegistryLoadResult> {
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,
});

View File

@@ -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([

View File

@@ -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();
}

View File

@@ -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 [];
}

View File

@@ -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<LoadOpenClawPlugins>) =>
loadOpenClawPluginsMock(...args),

View File

@@ -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"],
}),
);
});
});

View File

@@ -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<Record<string, string[]>>;
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>,
): PluginLoadOptions {
return buildPluginRuntimeLoadOptionsFromValues(context, overrides);
}
export function buildPluginRuntimeLoadOptionsFromValues(
values: PluginRuntimeResolvedLoadValues,
overrides?: Partial<PluginLoadOptions>,
): PluginLoadOptions {
return {
config: values.config,
activationSourceConfig: values.activationSourceConfig,
autoEnabledReasons: values.autoEnabledReasons,
workspaceDir: values.workspaceDir,
env: values.env,
logger: values.logger,
...overrides,
};
}

View File

@@ -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 } : {}),
}),
);
}

View File

@@ -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"] }),
);
});
});

View File

@@ -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;
}

View File

@@ -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"] }]);
});

View File

@@ -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<typeof loadConfig>,
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({

View File

@@ -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,