mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 17:20:45 +00:00
fix: scope runtime plugin preload to effective plugins
This commit is contained in:
@@ -8,6 +8,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
|
|
||||||
- Gateway/startup: skip plugin-backed auth-profile overlays during startup secrets preflight, reducing gateway readiness latency while keeping reload and OAuth recovery paths overlay-capable. (#68327) Thanks @JIRBOY.
|
- Gateway/startup: skip plugin-backed auth-profile overlays during startup secrets preflight, reducing gateway readiness latency while keeping reload and OAuth recovery paths overlay-capable. (#68327) Thanks @JIRBOY.
|
||||||
- Plugins/onboarding: carry ClawHub install metadata through channel setup catalogs so missing channel plugins can install from ClawHub before npm/local fallback. Thanks @vincentkoc.
|
- Plugins/onboarding: carry ClawHub install metadata through channel setup catalogs so missing channel plugins can install from ClawHub before npm/local fallback. Thanks @vincentkoc.
|
||||||
|
- Plugins/runtime: scope broad runtime preloads to the effective plugin ids derived from config, startup planning, configured channels, slots, and auto-enable rules instead of importing every discoverable plugin.
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,12 @@ to narrow plugin loading before broader registry materialization:
|
|||||||
imports and startup opt-outs; plugins without startup metadata load only
|
imports and startup opt-outs; plugins without startup metadata load only
|
||||||
through narrower activation triggers
|
through narrower activation triggers
|
||||||
|
|
||||||
|
Request-time runtime preloads that ask for the broad `all` scope still derive an
|
||||||
|
explicit effective plugin id set from config, startup planning, configured
|
||||||
|
channels, slots, and auto-enable rules. If that derived set is empty, OpenClaw
|
||||||
|
loads an empty runtime registry instead of widening to every discoverable
|
||||||
|
plugin.
|
||||||
|
|
||||||
The activation planner exposes both an ids-only API for existing callers and a
|
The activation planner exposes both an ids-only API for existing callers and a
|
||||||
plan API for new diagnostics. Plan entries report why a plugin was selected,
|
plan API for new diagnostics. Plan entries report why a plugin was selected,
|
||||||
separating explicit `activation.*` planner hints from manifest ownership
|
separating explicit `activation.*` planner hints from manifest ownership
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ const mocks = vi.hoisted(() => ({
|
|||||||
>(),
|
>(),
|
||||||
resolveChannelPluginIds:
|
resolveChannelPluginIds:
|
||||||
vi.fn<typeof import("../plugins/channel-plugin-ids.js").resolveChannelPluginIds>(),
|
vi.fn<typeof import("../plugins/channel-plugin-ids.js").resolveChannelPluginIds>(),
|
||||||
|
resolveEffectivePluginIds:
|
||||||
|
vi.fn<typeof import("../plugins/effective-plugin-ids.js").resolveEffectivePluginIds>(),
|
||||||
resolvePluginRuntimeLoadContext:
|
resolvePluginRuntimeLoadContext:
|
||||||
vi.fn<typeof import("../plugins/runtime/load-context.js").resolvePluginRuntimeLoadContext>(),
|
vi.fn<typeof import("../plugins/runtime/load-context.js").resolvePluginRuntimeLoadContext>(),
|
||||||
}));
|
}));
|
||||||
@@ -75,6 +77,11 @@ vi.mock("../plugins/channel-plugin-ids.js", () => ({
|
|||||||
mocks.resolveChannelPluginIds(...args),
|
mocks.resolveChannelPluginIds(...args),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock("../plugins/effective-plugin-ids.js", () => ({
|
||||||
|
resolveEffectivePluginIds: (...args: Parameters<typeof mocks.resolveEffectivePluginIds>) =>
|
||||||
|
mocks.resolveEffectivePluginIds(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock("../plugins/runtime/load-context.js", () => ({
|
vi.mock("../plugins/runtime/load-context.js", () => ({
|
||||||
resolvePluginRuntimeLoadContext: (
|
resolvePluginRuntimeLoadContext: (
|
||||||
...args: Parameters<typeof mocks.resolvePluginRuntimeLoadContext>
|
...args: Parameters<typeof mocks.resolvePluginRuntimeLoadContext>
|
||||||
@@ -134,6 +141,7 @@ describe("ensurePluginRegistryLoaded", () => {
|
|||||||
mocks.resolveConfiguredChannelPluginIds.mockReset();
|
mocks.resolveConfiguredChannelPluginIds.mockReset();
|
||||||
mocks.resolveDiscoverableScopedChannelPluginIds.mockReset();
|
mocks.resolveDiscoverableScopedChannelPluginIds.mockReset();
|
||||||
mocks.resolveChannelPluginIds.mockReset();
|
mocks.resolveChannelPluginIds.mockReset();
|
||||||
|
mocks.resolveEffectivePluginIds.mockReset();
|
||||||
mocks.resolvePluginRuntimeLoadContext.mockReset();
|
mocks.resolvePluginRuntimeLoadContext.mockReset();
|
||||||
resetPluginRegistryLoadedForTests();
|
resetPluginRegistryLoadedForTests();
|
||||||
|
|
||||||
@@ -141,6 +149,7 @@ describe("ensurePluginRegistryLoaded", () => {
|
|||||||
mocks.resolveCompatibleRuntimePluginRegistry.mockReturnValue(undefined);
|
mocks.resolveCompatibleRuntimePluginRegistry.mockReturnValue(undefined);
|
||||||
mocks.resolveRuntimePluginRegistry.mockReturnValue(undefined);
|
mocks.resolveRuntimePluginRegistry.mockReturnValue(undefined);
|
||||||
mocks.resolveDiscoverableScopedChannelPluginIds.mockReturnValue([]);
|
mocks.resolveDiscoverableScopedChannelPluginIds.mockReturnValue([]);
|
||||||
|
mocks.resolveEffectivePluginIds.mockReturnValue(["demo"]);
|
||||||
mocks.resolvePluginRuntimeLoadContext.mockImplementation((options) => {
|
mocks.resolvePluginRuntimeLoadContext.mockImplementation((options) => {
|
||||||
const rawConfig = (options?.config ?? {}) as Record<string, unknown>;
|
const rawConfig = (options?.config ?? {}) as Record<string, unknown>;
|
||||||
return {
|
return {
|
||||||
@@ -270,6 +279,7 @@ describe("ensurePluginRegistryLoaded", () => {
|
|||||||
expect(mocks.loadOpenClawPlugins).toHaveBeenCalledWith(
|
expect(mocks.loadOpenClawPlugins).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
config,
|
config,
|
||||||
|
onlyPluginIds: ["demo"],
|
||||||
throwOnLoadError: true,
|
throwOnLoadError: true,
|
||||||
workspaceDir: "/tmp/workspace",
|
workspaceDir: "/tmp/workspace",
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ function expectSetupSnapshotDoesNotScopeToPlugin(params: {
|
|||||||
const firstLoadCall = vi.mocked(loadOpenClawPlugins).mock.calls[0]?.[0] as
|
const firstLoadCall = vi.mocked(loadOpenClawPlugins).mock.calls[0]?.[0] as
|
||||||
| { onlyPluginIds?: string[] }
|
| { onlyPluginIds?: string[] }
|
||||||
| undefined;
|
| undefined;
|
||||||
expect(firstLoadCall?.onlyPluginIds).toBeUndefined();
|
expect(firstLoadCall?.onlyPluginIds).toEqual([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -729,7 +729,7 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps full reloads when the active plugin registry is already populated", () => {
|
it("does not widen channel reloads when the active plugin registry is already populated", () => {
|
||||||
const runtime = makeRuntime();
|
const runtime = makeRuntime();
|
||||||
const cfg: OpenClawConfig = {};
|
const cfg: OpenClawConfig = {};
|
||||||
const registry = createEmptyPluginRegistry();
|
const registry = createEmptyPluginRegistry();
|
||||||
@@ -752,8 +752,8 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(loadOpenClawPlugins).toHaveBeenCalledWith(
|
expect(loadOpenClawPlugins).toHaveBeenCalledWith(
|
||||||
expect.not.objectContaining({
|
expect.objectContaining({
|
||||||
onlyPluginIds: expect.anything(),
|
onlyPluginIds: [],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -880,7 +880,7 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
|||||||
expect(getChannelPluginCatalogEntry).toHaveBeenCalledTimes(1);
|
expect(getChannelPluginCatalogEntry).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not scope by raw channel id when no trusted plugin mapping exists", () => {
|
it("does not widen setup snapshots when no trusted plugin mapping exists", () => {
|
||||||
const runtime = makeRuntime();
|
const runtime = makeRuntime();
|
||||||
const cfg: OpenClawConfig = {};
|
const cfg: OpenClawConfig = {};
|
||||||
|
|
||||||
@@ -892,8 +892,8 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(loadOpenClawPlugins).toHaveBeenCalledWith(
|
expect(loadOpenClawPlugins).toHaveBeenCalledWith(
|
||||||
expect.not.objectContaining({
|
expect.objectContaining({
|
||||||
onlyPluginIds: expect.anything(),
|
onlyPluginIds: [],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ import type { ChannelPluginCatalogEntry } from "../../channels/plugins/catalog.j
|
|||||||
import { applyPluginAutoEnable } from "../../config/plugin-auto-enable.js";
|
import { applyPluginAutoEnable } from "../../config/plugin-auto-enable.js";
|
||||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||||
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||||
import { resolveDiscoverableScopedChannelPluginIds } from "../../plugins/channel-plugin-ids.js";
|
import {
|
||||||
|
resolveConfiguredChannelPluginIds,
|
||||||
|
resolveDiscoverableScopedChannelPluginIds,
|
||||||
|
} from "../../plugins/channel-plugin-ids.js";
|
||||||
import { loadOpenClawPlugins } from "../../plugins/loader.js";
|
import { loadOpenClawPlugins } from "../../plugins/loader.js";
|
||||||
import { createPluginLoaderLogger } from "../../plugins/logger.js";
|
import { createPluginLoaderLogger } from "../../plugins/logger.js";
|
||||||
import type { PluginRegistry } from "../../plugins/registry.js";
|
import type { PluginRegistry } from "../../plugins/registry.js";
|
||||||
import { getActivePluginChannelRegistry } from "../../plugins/runtime.js";
|
|
||||||
import type { RuntimeEnv } from "../../runtime.js";
|
import type { RuntimeEnv } from "../../runtime.js";
|
||||||
import type { WizardPrompter } from "../../wizard/prompts.js";
|
import type { WizardPrompter } from "../../wizard/prompts.js";
|
||||||
import {
|
import {
|
||||||
@@ -83,6 +85,14 @@ function loadChannelSetupPluginRegistry(params: {
|
|||||||
const workspaceDir =
|
const workspaceDir =
|
||||||
params.workspaceDir ??
|
params.workspaceDir ??
|
||||||
resolveAgentWorkspaceDir(resolvedConfig, resolveDefaultAgentId(resolvedConfig));
|
resolveAgentWorkspaceDir(resolvedConfig, resolveDefaultAgentId(resolvedConfig));
|
||||||
|
const onlyPluginIds =
|
||||||
|
params.onlyPluginIds ??
|
||||||
|
resolveConfiguredChannelPluginIds({
|
||||||
|
config: resolvedConfig,
|
||||||
|
activationSourceConfig: params.cfg,
|
||||||
|
workspaceDir,
|
||||||
|
env: process.env,
|
||||||
|
});
|
||||||
const log = createSubsystemLogger("plugins");
|
const log = createSubsystemLogger("plugins");
|
||||||
return loadOpenClawPlugins({
|
return loadOpenClawPlugins({
|
||||||
config: resolvedConfig,
|
config: resolvedConfig,
|
||||||
@@ -91,7 +101,7 @@ function loadChannelSetupPluginRegistry(params: {
|
|||||||
workspaceDir,
|
workspaceDir,
|
||||||
cache: false,
|
cache: false,
|
||||||
logger: createPluginLoaderLogger(log),
|
logger: createPluginLoaderLogger(log),
|
||||||
onlyPluginIds: params.onlyPluginIds,
|
onlyPluginIds,
|
||||||
includeSetupOnlyChannelPlugins: true,
|
includeSetupOnlyChannelPlugins: true,
|
||||||
forceSetupOnlyChannelPlugins: params.forceSetupOnlyChannelPlugins,
|
forceSetupOnlyChannelPlugins: params.forceSetupOnlyChannelPlugins,
|
||||||
activate: params.activate,
|
activate: params.activate,
|
||||||
@@ -137,21 +147,15 @@ export function reloadChannelSetupPluginRegistryForChannel(params: {
|
|||||||
pluginId?: string;
|
pluginId?: string;
|
||||||
workspaceDir?: string;
|
workspaceDir?: string;
|
||||||
}): void {
|
}): void {
|
||||||
const activeRegistry = getActivePluginChannelRegistry();
|
|
||||||
const scopedPluginId = resolveScopedChannelPluginId({
|
const scopedPluginId = resolveScopedChannelPluginId({
|
||||||
cfg: params.cfg,
|
cfg: params.cfg,
|
||||||
channel: params.channel,
|
channel: params.channel,
|
||||||
pluginId: params.pluginId,
|
pluginId: params.pluginId,
|
||||||
workspaceDir: params.workspaceDir,
|
workspaceDir: params.workspaceDir,
|
||||||
});
|
});
|
||||||
// On low-memory hosts, the empty-registry fallback should only recover the selected
|
|
||||||
// plugin when we have a trusted channel -> plugin mapping. Otherwise fall back
|
|
||||||
// to an unscoped reload instead of trusting manifest-declared channel ids.
|
|
||||||
const onlyPluginIds =
|
|
||||||
activeRegistry?.plugins.length || !scopedPluginId ? undefined : [scopedPluginId];
|
|
||||||
loadChannelSetupPluginRegistry({
|
loadChannelSetupPluginRegistry({
|
||||||
...params,
|
...params,
|
||||||
onlyPluginIds,
|
...(scopedPluginId ? { onlyPluginIds: [scopedPluginId] } : {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
148
src/plugins/effective-plugin-ids.test.ts
Normal file
148
src/plugins/effective-plugin-ids.test.ts
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||||
|
import type { PluginMetadataSnapshot } from "./plugin-metadata-snapshot.js";
|
||||||
|
|
||||||
|
const mocks = vi.hoisted(() => ({
|
||||||
|
applyPluginAutoEnable:
|
||||||
|
vi.fn<typeof import("../config/plugin-auto-enable.js").applyPluginAutoEnable>(),
|
||||||
|
listExplicitlyDisabledChannelIdsForConfig: vi.fn(),
|
||||||
|
listPotentialConfiguredChannelIds: vi.fn(),
|
||||||
|
listExplicitConfiguredChannelIdsForConfig: vi.fn(),
|
||||||
|
loadGatewayStartupPluginPlan:
|
||||||
|
vi.fn<typeof import("./channel-plugin-ids.js").loadGatewayStartupPluginPlan>(),
|
||||||
|
resolveConfiguredChannelPluginIds:
|
||||||
|
vi.fn<typeof import("./channel-plugin-ids.js").resolveConfiguredChannelPluginIds>(),
|
||||||
|
loadManifestMetadataSnapshot:
|
||||||
|
vi.fn<typeof import("./manifest-contract-eligibility.js").loadManifestMetadataSnapshot>(),
|
||||||
|
passesManifestOwnerBasePolicy:
|
||||||
|
vi.fn<typeof import("./manifest-owner-policy.js").passesManifestOwnerBasePolicy>(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../config/plugin-auto-enable.js", () => ({
|
||||||
|
applyPluginAutoEnable: (...args: Parameters<typeof mocks.applyPluginAutoEnable>) =>
|
||||||
|
mocks.applyPluginAutoEnable(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("../channels/config-presence.js", () => ({
|
||||||
|
listExplicitlyDisabledChannelIdsForConfig: (
|
||||||
|
...args: Parameters<typeof mocks.listExplicitlyDisabledChannelIdsForConfig>
|
||||||
|
) => mocks.listExplicitlyDisabledChannelIdsForConfig(...args),
|
||||||
|
listPotentialConfiguredChannelIds: (
|
||||||
|
...args: Parameters<typeof mocks.listPotentialConfiguredChannelIds>
|
||||||
|
) => mocks.listPotentialConfiguredChannelIds(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("./channel-plugin-ids.js", () => ({
|
||||||
|
listExplicitConfiguredChannelIdsForConfig: (
|
||||||
|
...args: Parameters<typeof mocks.listExplicitConfiguredChannelIdsForConfig>
|
||||||
|
) => mocks.listExplicitConfiguredChannelIdsForConfig(...args),
|
||||||
|
loadGatewayStartupPluginPlan: (...args: Parameters<typeof mocks.loadGatewayStartupPluginPlan>) =>
|
||||||
|
mocks.loadGatewayStartupPluginPlan(...args),
|
||||||
|
resolveConfiguredChannelPluginIds: (
|
||||||
|
...args: Parameters<typeof mocks.resolveConfiguredChannelPluginIds>
|
||||||
|
) => mocks.resolveConfiguredChannelPluginIds(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("./manifest-contract-eligibility.js", () => ({
|
||||||
|
loadManifestMetadataSnapshot: (...args: Parameters<typeof mocks.loadManifestMetadataSnapshot>) =>
|
||||||
|
mocks.loadManifestMetadataSnapshot(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("./manifest-owner-policy.js", () => ({
|
||||||
|
passesManifestOwnerBasePolicy: (
|
||||||
|
...args: Parameters<typeof mocks.passesManifestOwnerBasePolicy>
|
||||||
|
) => mocks.passesManifestOwnerBasePolicy(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { resolveEffectivePluginIds } from "./effective-plugin-ids.js";
|
||||||
|
|
||||||
|
function resolve(config: OpenClawConfig): string[] {
|
||||||
|
return resolveEffectivePluginIds({
|
||||||
|
config,
|
||||||
|
env: {},
|
||||||
|
workspaceDir: "/workspace",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("resolveEffectivePluginIds", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mocks.applyPluginAutoEnable.mockReset();
|
||||||
|
mocks.listExplicitlyDisabledChannelIdsForConfig.mockReset();
|
||||||
|
mocks.listPotentialConfiguredChannelIds.mockReset();
|
||||||
|
mocks.listExplicitConfiguredChannelIdsForConfig.mockReset();
|
||||||
|
mocks.loadGatewayStartupPluginPlan.mockReset();
|
||||||
|
mocks.resolveConfiguredChannelPluginIds.mockReset();
|
||||||
|
mocks.loadManifestMetadataSnapshot.mockReset();
|
||||||
|
mocks.passesManifestOwnerBasePolicy.mockReset();
|
||||||
|
|
||||||
|
mocks.applyPluginAutoEnable.mockImplementation((params) => ({
|
||||||
|
config: params.config ?? {},
|
||||||
|
changes: [],
|
||||||
|
autoEnabledReasons: {},
|
||||||
|
}));
|
||||||
|
mocks.listExplicitlyDisabledChannelIdsForConfig.mockReturnValue([]);
|
||||||
|
mocks.listPotentialConfiguredChannelIds.mockReturnValue([]);
|
||||||
|
mocks.listExplicitConfiguredChannelIdsForConfig.mockReturnValue([]);
|
||||||
|
mocks.loadGatewayStartupPluginPlan.mockReturnValue({
|
||||||
|
channelPluginIds: [],
|
||||||
|
configuredDeferredChannelPluginIds: [],
|
||||||
|
pluginIds: [],
|
||||||
|
});
|
||||||
|
mocks.resolveConfiguredChannelPluginIds.mockReturnValue([]);
|
||||||
|
mocks.loadManifestMetadataSnapshot.mockReturnValue({
|
||||||
|
plugins: [],
|
||||||
|
} as unknown as PluginMetadataSnapshot);
|
||||||
|
mocks.passesManifestOwnerBasePolicy.mockReturnValue(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes a selected context-engine slot even when omitted from explicit allow and entries", () => {
|
||||||
|
expect(
|
||||||
|
resolve({
|
||||||
|
plugins: {
|
||||||
|
slots: { contextEngine: "lossless-claw" },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toEqual(["lossless-claw"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps the built-in legacy context engine out of plugin preload ids", () => {
|
||||||
|
expect(
|
||||||
|
resolve({
|
||||||
|
plugins: {
|
||||||
|
slots: { contextEngine: "legacy" },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{
|
||||||
|
name: "plugins disabled",
|
||||||
|
plugins: {
|
||||||
|
enabled: false,
|
||||||
|
slots: { contextEngine: "lossless-claw" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "denylisted",
|
||||||
|
plugins: {
|
||||||
|
deny: ["lossless-claw"],
|
||||||
|
slots: { contextEngine: "lossless-claw" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "entry disabled",
|
||||||
|
plugins: {
|
||||||
|
entries: {
|
||||||
|
"lossless-claw": { enabled: false },
|
||||||
|
},
|
||||||
|
slots: { contextEngine: "lossless-claw" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] satisfies Array<{ name: string; plugins: NonNullable<OpenClawConfig["plugins"]> }>)(
|
||||||
|
"does not preload a selected context-engine slot when $name",
|
||||||
|
({ plugins }) => {
|
||||||
|
expect(resolve({ plugins })).toEqual([]);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
import { normalizePluginsConfig } from "./config-state.js";
|
import { normalizePluginsConfig } from "./config-state.js";
|
||||||
import { loadManifestMetadataSnapshot } from "./manifest-contract-eligibility.js";
|
import { loadManifestMetadataSnapshot } from "./manifest-contract-eligibility.js";
|
||||||
import { passesManifestOwnerBasePolicy } from "./manifest-owner-policy.js";
|
import { passesManifestOwnerBasePolicy } from "./manifest-owner-policy.js";
|
||||||
|
import { defaultSlotIdForKey } from "./slots.js";
|
||||||
|
|
||||||
function collectConfiguredChannelIds(
|
function collectConfiguredChannelIds(
|
||||||
config: OpenClawConfig,
|
config: OpenClawConfig,
|
||||||
@@ -120,6 +121,24 @@ function collectExplicitEffectivePluginIds(config: OpenClawConfig): string[] {
|
|||||||
return [...ids].toSorted((left, right) => left.localeCompare(right));
|
return [...ids].toSorted((left, right) => left.localeCompare(right));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function collectSelectedContextEnginePluginIds(config: OpenClawConfig): string[] {
|
||||||
|
const plugins = normalizePluginsConfig(config.plugins);
|
||||||
|
if (!plugins.enabled) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const pluginId = plugins.slots.contextEngine;
|
||||||
|
if (!pluginId || pluginId === defaultSlotIdForKey("contextEngine")) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (plugins.deny.includes(pluginId)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (plugins.entries[pluginId]?.enabled === false) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [pluginId];
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveEffectivePluginIds(params: {
|
export function resolveEffectivePluginIds(params: {
|
||||||
config: OpenClawConfig;
|
config: OpenClawConfig;
|
||||||
env: NodeJS.ProcessEnv;
|
env: NodeJS.ProcessEnv;
|
||||||
@@ -132,6 +151,9 @@ export function resolveEffectivePluginIds(params: {
|
|||||||
});
|
});
|
||||||
const effectiveConfig = autoEnabled.config;
|
const effectiveConfig = autoEnabled.config;
|
||||||
const ids = new Set(collectExplicitEffectivePluginIds(effectiveConfig));
|
const ids = new Set(collectExplicitEffectivePluginIds(effectiveConfig));
|
||||||
|
for (const pluginId of collectSelectedContextEnginePluginIds(effectiveConfig)) {
|
||||||
|
ids.add(pluginId);
|
||||||
|
}
|
||||||
const configuredChannelIds = collectConfiguredChannelIds(
|
const configuredChannelIds = collectConfiguredChannelIds(
|
||||||
effectiveConfig,
|
effectiveConfig,
|
||||||
params.config,
|
params.config,
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ const mocks = vi.hoisted(() => ({
|
|||||||
vi.fn<typeof import("../channel-plugin-ids.js").resolveDiscoverableScopedChannelPluginIds>(),
|
vi.fn<typeof import("../channel-plugin-ids.js").resolveDiscoverableScopedChannelPluginIds>(),
|
||||||
resolveChannelPluginIds:
|
resolveChannelPluginIds:
|
||||||
vi.fn<typeof import("../channel-plugin-ids.js").resolveChannelPluginIds>(),
|
vi.fn<typeof import("../channel-plugin-ids.js").resolveChannelPluginIds>(),
|
||||||
|
resolveEffectivePluginIds:
|
||||||
|
vi.fn<typeof import("../effective-plugin-ids.js").resolveEffectivePluginIds>(),
|
||||||
applyPluginAutoEnable:
|
applyPluginAutoEnable:
|
||||||
vi.fn<typeof import("../../config/plugin-auto-enable.js").applyPluginAutoEnable>(),
|
vi.fn<typeof import("../../config/plugin-auto-enable.js").applyPluginAutoEnable>(),
|
||||||
resolveAgentWorkspaceDir: vi.fn<
|
resolveAgentWorkspaceDir: vi.fn<
|
||||||
@@ -55,6 +57,11 @@ vi.mock("../channel-plugin-ids.js", () => ({
|
|||||||
mocks.resolveChannelPluginIds(...args),
|
mocks.resolveChannelPluginIds(...args),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock("../effective-plugin-ids.js", () => ({
|
||||||
|
resolveEffectivePluginIds: (...args: Parameters<typeof mocks.resolveEffectivePluginIds>) =>
|
||||||
|
mocks.resolveEffectivePluginIds(...args),
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock("../../config/plugin-auto-enable.js", () => ({
|
vi.mock("../../config/plugin-auto-enable.js", () => ({
|
||||||
applyPluginAutoEnable: (...args: Parameters<typeof mocks.applyPluginAutoEnable>) =>
|
applyPluginAutoEnable: (...args: Parameters<typeof mocks.applyPluginAutoEnable>) =>
|
||||||
mocks.applyPluginAutoEnable(...args),
|
mocks.applyPluginAutoEnable(...args),
|
||||||
@@ -82,6 +89,7 @@ describe("ensurePluginRegistryLoaded", () => {
|
|||||||
mocks.resolveConfiguredChannelPluginIds.mockReset();
|
mocks.resolveConfiguredChannelPluginIds.mockReset();
|
||||||
mocks.resolveDiscoverableScopedChannelPluginIds.mockReset();
|
mocks.resolveDiscoverableScopedChannelPluginIds.mockReset();
|
||||||
mocks.resolveChannelPluginIds.mockReset();
|
mocks.resolveChannelPluginIds.mockReset();
|
||||||
|
mocks.resolveEffectivePluginIds.mockReset();
|
||||||
mocks.applyPluginAutoEnable.mockReset();
|
mocks.applyPluginAutoEnable.mockReset();
|
||||||
mocks.resolveAgentWorkspaceDir.mockClear();
|
mocks.resolveAgentWorkspaceDir.mockClear();
|
||||||
mocks.resolveDefaultAgentId.mockClear();
|
mocks.resolveDefaultAgentId.mockClear();
|
||||||
@@ -111,6 +119,7 @@ describe("ensurePluginRegistryLoaded", () => {
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
mocks.resolveDiscoverableScopedChannelPluginIds.mockReturnValue([]);
|
mocks.resolveDiscoverableScopedChannelPluginIds.mockReturnValue([]);
|
||||||
|
mocks.resolveEffectivePluginIds.mockReturnValue(["demo"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses the shared runtime load context for configured-channel loads", () => {
|
it("uses the shared runtime load context for configured-channel loads", () => {
|
||||||
@@ -328,8 +337,63 @@ describe("ensurePluginRegistryLoaded", () => {
|
|||||||
).toBeUndefined();
|
).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("derives all-scope runtime loads from effective plugin ids", () => {
|
||||||
|
const config = {
|
||||||
|
plugins: { enabled: true },
|
||||||
|
channels: { "demo-channel-a": { enabled: true } },
|
||||||
|
};
|
||||||
|
const env = { HOME: "/tmp/openclaw-home" } as NodeJS.ProcessEnv;
|
||||||
|
|
||||||
|
mocks.resolveEffectivePluginIds.mockReturnValue(["demo-effective", "demo-hook"]);
|
||||||
|
|
||||||
|
ensurePluginRegistryLoaded({ scope: "all", config: config as never, env });
|
||||||
|
|
||||||
|
expect(mocks.resolveEffectivePluginIds).toHaveBeenCalledWith({
|
||||||
|
config,
|
||||||
|
env,
|
||||||
|
workspaceDir: "/resolved-workspace",
|
||||||
|
});
|
||||||
|
expect(mocks.loadOpenClawPlugins).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
config: expect.objectContaining({
|
||||||
|
...config,
|
||||||
|
plugins: expect.objectContaining({
|
||||||
|
entries: expect.objectContaining({
|
||||||
|
demo: { enabled: true },
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
onlyPluginIds: ["demo-effective", "demo-hook"],
|
||||||
|
throwOnLoadError: true,
|
||||||
|
workspaceDir: "/resolved-workspace",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves empty all-scope loads instead of widening to all discovered plugins", () => {
|
||||||
|
mocks.resolveEffectivePluginIds.mockReturnValue([]);
|
||||||
|
|
||||||
|
ensurePluginRegistryLoaded({
|
||||||
|
scope: "all",
|
||||||
|
config: { plugins: { enabled: true } } as never,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mocks.loadOpenClawPlugins).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
onlyPluginIds: [],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("reuses a compatible active registry instead of forcing a broad reload", () => {
|
it("reuses a compatible active registry instead of forcing a broad reload", () => {
|
||||||
const activeRegistry = createEmptyPluginRegistry();
|
const activeRegistry = createEmptyPluginRegistry();
|
||||||
|
activeRegistry.plugins.push({
|
||||||
|
id: "demo",
|
||||||
|
source: "/tmp/demo.js",
|
||||||
|
origin: "workspace",
|
||||||
|
enabled: true,
|
||||||
|
status: "loaded",
|
||||||
|
} as never);
|
||||||
mocks.getActivePluginRegistry.mockReturnValue(activeRegistry);
|
mocks.getActivePluginRegistry.mockReturnValue(activeRegistry);
|
||||||
mocks.resolveCompatibleRuntimePluginRegistry.mockReturnValue(activeRegistry);
|
mocks.resolveCompatibleRuntimePluginRegistry.mockReturnValue(activeRegistry);
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
resolveConfiguredChannelPluginIds,
|
resolveConfiguredChannelPluginIds,
|
||||||
resolveDiscoverableScopedChannelPluginIds,
|
resolveDiscoverableScopedChannelPluginIds,
|
||||||
} from "../channel-plugin-ids.js";
|
} from "../channel-plugin-ids.js";
|
||||||
|
import { resolveEffectivePluginIds } from "../effective-plugin-ids.js";
|
||||||
import { loadOpenClawPlugins } from "../loader.js";
|
import { loadOpenClawPlugins } from "../loader.js";
|
||||||
import {
|
import {
|
||||||
hasExplicitPluginIdScope,
|
hasExplicitPluginIdScope,
|
||||||
@@ -75,6 +76,35 @@ function shouldForwardChannelScope(params: {
|
|||||||
return !params.scopedLoad && params.scope === "configured-channels";
|
return !params.scopedLoad && params.scope === "configured-channels";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveScopePluginIds(params: {
|
||||||
|
scope: PluginRegistryScope;
|
||||||
|
context: ReturnType<typeof resolvePluginRuntimeLoadContext>;
|
||||||
|
}): string[] {
|
||||||
|
switch (params.scope) {
|
||||||
|
case "configured-channels":
|
||||||
|
return resolveConfiguredChannelPluginIds({
|
||||||
|
config: params.context.config,
|
||||||
|
activationSourceConfig: params.context.activationSourceConfig,
|
||||||
|
workspaceDir: params.context.workspaceDir,
|
||||||
|
env: params.context.env,
|
||||||
|
});
|
||||||
|
case "channels":
|
||||||
|
return resolveChannelPluginIds({
|
||||||
|
config: params.context.config,
|
||||||
|
workspaceDir: params.context.workspaceDir,
|
||||||
|
env: params.context.env,
|
||||||
|
});
|
||||||
|
case "all":
|
||||||
|
return resolveEffectivePluginIds({
|
||||||
|
config: params.context.rawConfig,
|
||||||
|
workspaceDir: params.context.workspaceDir,
|
||||||
|
env: params.context.env,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const unreachableScope: never = params.scope;
|
||||||
|
return unreachableScope;
|
||||||
|
}
|
||||||
|
|
||||||
function resolveOrLoadRuntimePluginRegistry(
|
function resolveOrLoadRuntimePluginRegistry(
|
||||||
loadOptions: NonNullable<Parameters<typeof loadOpenClawPlugins>[0]>,
|
loadOptions: NonNullable<Parameters<typeof loadOpenClawPlugins>[0]>,
|
||||||
): void {
|
): void {
|
||||||
@@ -121,33 +151,21 @@ export function ensurePluginRegistryLoaded(options?: {
|
|||||||
...requestedChannelOwnerPluginIds,
|
...requestedChannelOwnerPluginIds,
|
||||||
]);
|
]);
|
||||||
const scopedLoad = hasExplicitPluginIdScope(requestedPluginIds);
|
const scopedLoad = hasExplicitPluginIdScope(requestedPluginIds);
|
||||||
const expectedChannelPluginIds = scopedLoad
|
const expectedPluginIds = scopedLoad
|
||||||
? (requestedPluginIds ?? [])
|
? (requestedPluginIds ?? [])
|
||||||
: scope === "configured-channels"
|
: resolveScopePluginIds({ scope, context });
|
||||||
? resolveConfiguredChannelPluginIds({
|
|
||||||
config: context.config,
|
|
||||||
activationSourceConfig: context.activationSourceConfig,
|
|
||||||
workspaceDir: context.workspaceDir,
|
|
||||||
env: context.env,
|
|
||||||
})
|
|
||||||
: scope === "channels"
|
|
||||||
? resolveChannelPluginIds({
|
|
||||||
config: context.config,
|
|
||||||
workspaceDir: context.workspaceDir,
|
|
||||||
env: context.env,
|
|
||||||
})
|
|
||||||
: [];
|
|
||||||
const active = getActivePluginRegistry();
|
const active = getActivePluginRegistry();
|
||||||
|
const requestedPluginIdsForScope = scope === "all" ? expectedPluginIds : undefined;
|
||||||
if (
|
if (
|
||||||
!scopedLoad &&
|
!scopedLoad &&
|
||||||
scopeRank(pluginRegistryLoaded) >= scopeRank(scope) &&
|
scopeRank(pluginRegistryLoaded) >= scopeRank(scope) &&
|
||||||
activeRegistrySatisfiesScope(scope, active, expectedChannelPluginIds, undefined)
|
activeRegistrySatisfiesScope(scope, active, expectedPluginIds, requestedPluginIdsForScope)
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
(pluginRegistryLoaded === "none" || scopedLoad) &&
|
(pluginRegistryLoaded === "none" || scopedLoad) &&
|
||||||
activeRegistrySatisfiesScope(scope, active, expectedChannelPluginIds, requestedPluginIds)
|
activeRegistrySatisfiesScope(scope, active, expectedPluginIds, requestedPluginIds)
|
||||||
) {
|
) {
|
||||||
if (!scopedLoad) {
|
if (!scopedLoad) {
|
||||||
pluginRegistryLoaded = scope;
|
pluginRegistryLoaded = scope;
|
||||||
@@ -156,20 +174,20 @@ export function ensurePluginRegistryLoaded(options?: {
|
|||||||
}
|
}
|
||||||
const scopedConfig =
|
const scopedConfig =
|
||||||
scope === "configured-channels" &&
|
scope === "configured-channels" &&
|
||||||
expectedChannelPluginIds.length > 0 &&
|
expectedPluginIds.length > 0 &&
|
||||||
(!scopedLoad || requestedChannelOwnerPluginIds !== undefined)
|
(!scopedLoad || requestedChannelOwnerPluginIds !== undefined)
|
||||||
? (withActivatedPluginIds({
|
? (withActivatedPluginIds({
|
||||||
config: context.config,
|
config: context.config,
|
||||||
pluginIds: expectedChannelPluginIds,
|
pluginIds: expectedPluginIds,
|
||||||
}) ?? context.config)
|
}) ?? context.config)
|
||||||
: context.config;
|
: context.config;
|
||||||
const scopedActivationSourceConfig =
|
const scopedActivationSourceConfig =
|
||||||
scope === "configured-channels" &&
|
scope === "configured-channels" &&
|
||||||
expectedChannelPluginIds.length > 0 &&
|
expectedPluginIds.length > 0 &&
|
||||||
(!scopedLoad || requestedChannelOwnerPluginIds !== undefined)
|
(!scopedLoad || requestedChannelOwnerPluginIds !== undefined)
|
||||||
? (withActivatedPluginIds({
|
? (withActivatedPluginIds({
|
||||||
config: context.activationSourceConfig,
|
config: context.activationSourceConfig,
|
||||||
pluginIds: expectedChannelPluginIds,
|
pluginIds: expectedPluginIds,
|
||||||
}) ?? context.activationSourceConfig)
|
}) ?? context.activationSourceConfig)
|
||||||
: context.activationSourceConfig;
|
: context.activationSourceConfig;
|
||||||
const loadOptions = buildPluginRuntimeLoadOptionsFromValues(
|
const loadOptions = buildPluginRuntimeLoadOptionsFromValues(
|
||||||
@@ -182,8 +200,9 @@ export function ensurePluginRegistryLoaded(options?: {
|
|||||||
throwOnLoadError: true,
|
throwOnLoadError: true,
|
||||||
...(hasExplicitPluginIdScope(requestedPluginIds) ||
|
...(hasExplicitPluginIdScope(requestedPluginIds) ||
|
||||||
shouldForwardChannelScope({ scope, scopedLoad }) ||
|
shouldForwardChannelScope({ scope, scopedLoad }) ||
|
||||||
hasNonEmptyPluginIdScope(expectedChannelPluginIds)
|
hasNonEmptyPluginIdScope(expectedPluginIds) ||
|
||||||
? { onlyPluginIds: expectedChannelPluginIds }
|
scope === "all"
|
||||||
|
? { onlyPluginIds: expectedPluginIds }
|
||||||
: {}),
|
: {}),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user