perf(plugins): keep gateway startup channel-only (#59754)

* perf(plugins): keep gateway startup channel-only

* fix(gateway): preserve startup sidecars in plugin scope
This commit is contained in:
Vincent Koc
2026-04-03 00:28:15 +09:00
committed by GitHub
parent 988f7627de
commit d2ce3e9acc
4 changed files with 116 additions and 270 deletions

View File

@@ -277,6 +277,22 @@ describe("loadGatewayPlugins", () => {
);
});
test("treats an empty startup scope as no plugin load instead of an unscoped load", async () => {
resolveGatewayStartupPluginIds.mockReturnValue([]);
const result = serverPluginsModule.loadGatewayPlugins({
cfg: {},
workspaceDir: "/tmp",
log: createTestLog(),
coreGatewayHandlers: {},
baseMethods: ["sessions.get"],
});
expect(loadOpenClawPlugins).not.toHaveBeenCalled();
expect(result.pluginRegistry.plugins).toEqual([]);
expect(result.gatewayMethods).toEqual(["sessions.get"]);
});
test("loads gateway plugins from the auto-enabled config snapshot", async () => {
const autoEnabledConfig = { channels: { slack: { enabled: true } }, autoEnabled: true };
applyPluginAutoEnable.mockReturnValue({

View File

@@ -5,6 +5,8 @@ import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js";
import { resolveGatewayStartupPluginIds } from "../plugins/channel-plugin-ids.js";
import { normalizePluginsConfig } from "../plugins/config-state.js";
import { loadOpenClawPlugins } from "../plugins/loader.js";
import { createEmptyPluginRegistry } from "../plugins/registry-empty.js";
import { setActivePluginRegistry } from "../plugins/runtime.js";
import { getPluginRuntimeGatewayRequestScope } from "../plugins/runtime/gateway-request-scope.js";
import type { PluginRuntime } from "../plugins/runtime/types.js";
import { resolveGlobalSingleton } from "../shared/global-singleton.js";
@@ -424,6 +426,14 @@ export function loadGatewayPlugins(params: {
workspaceDir: params.workspaceDir,
env: process.env,
});
if (pluginIds.length === 0) {
const pluginRegistry = createEmptyPluginRegistry();
setActivePluginRegistry(pluginRegistry, undefined, "gateway-bindable");
return {
pluginRegistry,
gatewayMethods: [...params.baseMethods],
};
}
const pluginRegistry = loadOpenClawPlugins({
config: resolvedConfig,
activationSourceConfig: params.activationSourceConfig ?? params.cfg,

View File

@@ -26,7 +26,15 @@ function createManifestRegistryFixture() {
cliBackends: [],
},
{
id: "demo-default-on-sidecar",
id: "demo-other-channel",
channels: ["demo-other-channel"],
origin: "bundled",
enabledByDefault: undefined,
providers: [],
cliBackends: [],
},
{
id: "browser",
channels: [],
origin: "bundled",
enabledByDefault: true,
@@ -42,7 +50,7 @@ function createManifestRegistryFixture() {
cliBackends: ["demo-cli"],
},
{
id: "demo-bundled-sidecar",
id: "voice-call",
channels: [],
origin: "bundled",
enabledByDefault: undefined,
@@ -84,17 +92,38 @@ function createStartupConfig(params: {
enabledPluginIds?: string[];
providerIds?: string[];
modelId?: string;
channelIds?: string[];
allowPluginIds?: string[];
noConfiguredChannels?: boolean;
}) {
return {
...(params.noConfiguredChannels
? {
channels: {},
}
: params.channelIds?.length
? {
channels: Object.fromEntries(
params.channelIds.map((channelId) => [channelId, { enabled: true }]),
),
}
: {}),
...(params.enabledPluginIds?.length
? {
plugins: {
...(params.allowPluginIds?.length ? { allow: params.allowPluginIds } : {}),
entries: Object.fromEntries(
params.enabledPluginIds.map((pluginId) => [pluginId, { enabled: true }]),
),
},
}
: {}),
: params.allowPluginIds?.length
? {
plugins: {
allow: params.allowPluginIds,
},
}
: {}),
...(params.providerIds?.length
? {
models: {
@@ -127,37 +156,57 @@ function createStartupConfig(params: {
describe("resolveGatewayStartupPluginIds", () => {
beforeEach(() => {
listPotentialConfiguredChannelIds.mockReset().mockReturnValue(["demo-channel"]);
listPotentialConfiguredChannelIds.mockReset().mockImplementation((config: OpenClawConfig) => {
if (Object.prototype.hasOwnProperty.call(config, "channels")) {
return Object.keys(config.channels ?? {});
}
return ["demo-channel"];
});
loadPluginManifestRegistry.mockReset().mockReturnValue(createManifestRegistryFixture());
});
it.each([
[
"includes configured channels and explicitly enabled bundled sidecars",
"includes only configured channel plugins at idle startup",
createStartupConfig({
enabledPluginIds: ["demo-bundled-sidecar"],
enabledPluginIds: ["voice-call"],
modelId: "demo-cli/demo-model",
}),
["demo-channel", "demo-provider-plugin", "demo-bundled-sidecar"],
["demo-channel", "browser", "voice-call"],
],
[
"skips bundled plugins with enabledByDefault: true until something references them",
"keeps bundled startup sidecars with enabledByDefault at idle startup",
{} as OpenClawConfig,
["demo-channel"],
["demo-channel", "browser"],
],
[
"auto-loads bundled plugins referenced by configured provider ids",
"keeps provider plugins out of idle startup when only provider config references them",
createStartupConfig({
providerIds: ["demo-provider"],
}),
["demo-channel", "demo-provider-plugin"],
["demo-channel", "browser"],
],
[
"keeps non-bundled sidecars out of startup unless explicitly enabled",
"includes explicitly enabled non-channel sidecars in startup scope",
createStartupConfig({
enabledPluginIds: ["demo-global-sidecar"],
enabledPluginIds: ["demo-global-sidecar", "voice-call"],
}),
["demo-channel", "demo-global-sidecar"],
["demo-channel", "browser", "voice-call", "demo-global-sidecar"],
],
[
"keeps default-enabled startup sidecars when a restrictive allowlist permits them",
createStartupConfig({
allowPluginIds: ["browser"],
noConfiguredChannels: true,
}),
["browser"],
],
[
"includes every configured channel plugin and excludes other channels",
createStartupConfig({
channelIds: ["demo-channel", "demo-other-channel"],
}),
["demo-channel", "demo-other-channel", "browser"],
],
] as const)("%s", (_name, config, expected) => {
expectStartupPluginIdsCase({ config, expected });

View File

@@ -1,239 +1,24 @@
import { DEFAULT_PROVIDER } from "../agents/defaults.js";
import {
buildModelAliasIndex,
normalizeProviderId,
resolveModelRefFromString,
} from "../agents/model-selection.js";
import { listPotentialConfiguredChannelIds } from "../channels/config-presence.js";
import type { OpenClawConfig } from "../config/config.js";
import { resolvePluginWebSearchConfig } from "../config/legacy-web-search.js";
import {
resolveAgentModelFallbackValues,
resolveAgentModelPrimaryValue,
} from "../config/model-input.js";
import { normalizePluginsConfig, resolveEffectiveEnableState } from "./config-state.js";
import { loadPluginManifestRegistry } from "./manifest-registry.js";
import { normalizePluginsConfig, resolveEffectivePluginActivationState } from "./config-state.js";
import { loadPluginManifestRegistry, type PluginManifestRecord } from "./manifest-registry.js";
import { hasKind } from "./slots.js";
type ModelListLike = string | { primary?: string; fallbacks?: string[] } | undefined;
function addResolvedActivationId(params: {
raw: string | undefined;
activationIds: Set<string>;
aliasIndex: ReturnType<typeof buildModelAliasIndex>;
}): void {
const raw = params.raw?.trim();
if (!raw) {
return;
}
const resolved = resolveModelRefFromString({
raw,
defaultProvider: DEFAULT_PROVIDER,
aliasIndex: params.aliasIndex,
});
if (!resolved) {
return;
}
params.activationIds.add(normalizeProviderId(resolved.ref.provider));
function hasRuntimeContractSurface(plugin: PluginManifestRecord): boolean {
return Boolean(
plugin.providers.length > 0 ||
plugin.cliBackends.length > 0 ||
plugin.contracts?.speechProviders?.length ||
plugin.contracts?.mediaUnderstandingProviders?.length ||
plugin.contracts?.imageGenerationProviders?.length ||
plugin.contracts?.webFetchProviders?.length ||
plugin.contracts?.webSearchProviders?.length ||
hasKind(plugin.kind, "memory"),
);
}
function addModelListActivationIds(params: {
value: ModelListLike;
activationIds: Set<string>;
aliasIndex: ReturnType<typeof buildModelAliasIndex>;
}): void {
addResolvedActivationId({
raw: resolveAgentModelPrimaryValue(params.value),
activationIds: params.activationIds,
aliasIndex: params.aliasIndex,
});
for (const fallback of resolveAgentModelFallbackValues(params.value)) {
addResolvedActivationId({
raw: fallback,
activationIds: params.activationIds,
aliasIndex: params.aliasIndex,
});
}
}
function addProviderModelPairActivationId(params: {
provider: string | undefined;
model: string | undefined;
activationIds: Set<string>;
}): void {
const provider = normalizeProviderId(params.provider ?? "");
const model = params.model?.trim();
if (!provider || !model) {
return;
}
params.activationIds.add(provider);
}
function collectConfiguredActivationIds(config: OpenClawConfig): Set<string> {
const activationIds = new Set<string>();
const aliasIndex = buildModelAliasIndex({
cfg: config,
defaultProvider: DEFAULT_PROVIDER,
});
addModelListActivationIds({ value: config.agents?.defaults?.model, activationIds, aliasIndex });
addModelListActivationIds({
value: config.agents?.defaults?.imageModel,
activationIds,
aliasIndex,
});
addModelListActivationIds({
value: config.agents?.defaults?.imageGenerationModel,
activationIds,
aliasIndex,
});
addModelListActivationIds({
value: config.agents?.defaults?.pdfModel,
activationIds,
aliasIndex,
});
addResolvedActivationId({
raw: config.agents?.defaults?.compaction?.model,
activationIds,
aliasIndex,
});
addResolvedActivationId({
raw: config.agents?.defaults?.heartbeat?.model,
activationIds,
aliasIndex,
});
addModelListActivationIds({
value: config.agents?.defaults?.subagents?.model,
activationIds,
aliasIndex,
});
addResolvedActivationId({
raw: config.messages?.tts?.summaryModel,
activationIds,
aliasIndex,
});
addResolvedActivationId({
raw: config.hooks?.gmail?.model,
activationIds,
aliasIndex,
});
for (const modelRef of Object.keys(config.agents?.defaults?.models ?? {})) {
addResolvedActivationId({
raw: modelRef,
activationIds,
aliasIndex,
});
}
for (const providerId of Object.keys(config.agents?.defaults?.cliBackends ?? {})) {
const normalized = normalizeProviderId(providerId);
if (normalized) {
activationIds.add(normalized);
}
}
for (const providerId of Object.keys(config.models?.providers ?? {})) {
const normalized = normalizeProviderId(providerId);
if (normalized) {
activationIds.add(normalized);
}
}
for (const agent of config.agents?.list ?? []) {
addModelListActivationIds({ value: agent.model, activationIds, aliasIndex });
addModelListActivationIds({ value: agent.subagents?.model, activationIds, aliasIndex });
addResolvedActivationId({
raw: agent.heartbeat?.model,
activationIds,
aliasIndex,
});
}
for (const mapping of config.hooks?.mappings ?? []) {
addResolvedActivationId({
raw: mapping.model,
activationIds,
aliasIndex,
});
}
for (const channelMap of Object.values(config.channels?.modelByChannel ?? {})) {
if (!channelMap || typeof channelMap !== "object") {
continue;
}
for (const raw of Object.values(channelMap)) {
addResolvedActivationId({
raw: typeof raw === "string" ? raw : undefined,
activationIds,
aliasIndex,
});
}
}
addResolvedActivationId({
raw: config.tools?.subagents?.model
? resolveAgentModelPrimaryValue(config.tools?.subagents?.model)
: undefined,
activationIds,
aliasIndex,
});
if (config.tools?.subagents?.model) {
for (const fallback of resolveAgentModelFallbackValues(config.tools.subagents.model)) {
addResolvedActivationId({ raw: fallback, activationIds, aliasIndex });
}
}
addResolvedActivationId({
raw: resolvePluginWebSearchConfig(config, "google")?.model as string | undefined,
activationIds,
aliasIndex,
});
addResolvedActivationId({
raw: resolvePluginWebSearchConfig(config, "xai")?.model as string | undefined,
activationIds,
aliasIndex,
});
addResolvedActivationId({
raw: resolvePluginWebSearchConfig(config, "moonshot")?.model as string | undefined,
activationIds,
aliasIndex,
});
addResolvedActivationId({
raw: resolvePluginWebSearchConfig(config, "perplexity")?.model as string | undefined,
activationIds,
aliasIndex,
});
for (const entry of config.tools?.media?.models ?? []) {
addProviderModelPairActivationId({
provider: entry.provider,
model: entry.model,
activationIds,
});
}
for (const entry of config.tools?.media?.image?.models ?? []) {
addProviderModelPairActivationId({
provider: entry.provider,
model: entry.model,
activationIds,
});
}
for (const entry of config.tools?.media?.audio?.models ?? []) {
addProviderModelPairActivationId({
provider: entry.provider,
model: entry.model,
activationIds,
});
}
for (const entry of config.tools?.media?.video?.models ?? []) {
addProviderModelPairActivationId({
provider: entry.provider,
model: entry.model,
activationIds,
});
}
return activationIds;
function isGatewayStartupSidecar(plugin: PluginManifestRecord): boolean {
return plugin.channels.length === 0 && !hasRuntimeContractSurface(plugin);
}
export function resolveChannelPluginIds(params: {
@@ -297,46 +82,32 @@ export function resolveGatewayStartupPluginIds(params: {
listPotentialConfiguredChannelIds(params.config, params.env).map((id) => id.trim()),
);
const pluginsConfig = normalizePluginsConfig(params.config.plugins);
const manifestRegistry = loadPluginManifestRegistry({
return loadPluginManifestRegistry({
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
});
const configuredActivationIds = collectConfiguredActivationIds(params.config);
return manifestRegistry.plugins
.filter((plugin) => {
})
.plugins.filter((plugin) => {
if (plugin.channels.some((channelId) => configuredChannelIds.has(channelId))) {
return true;
}
if (plugin.channels.length > 0) {
if (!isGatewayStartupSidecar(plugin)) {
return false;
}
if (
plugin.origin === "bundled" &&
(plugin.providers.some((providerId) =>
configuredActivationIds.has(normalizeProviderId(providerId)),
) ||
plugin.cliBackends.some((backendId) =>
configuredActivationIds.has(normalizeProviderId(backendId)),
))
) {
return true;
}
const enabled = resolveEffectiveEnableState({
const activationState = resolveEffectivePluginActivationState({
id: plugin.id,
origin: plugin.origin,
config: pluginsConfig,
rootConfig: params.config,
enabledByDefault: plugin.enabledByDefault,
}).enabled;
if (!enabled) {
});
if (!activationState.enabled) {
return false;
}
return (
pluginsConfig.allow.includes(plugin.id) ||
pluginsConfig.entries[plugin.id]?.enabled === true ||
pluginsConfig.slots.memory === plugin.id
);
if (plugin.origin !== "bundled") {
return activationState.explicitlyEnabled;
}
return activationState.source === "explicit" || activationState.source === "default";
})
.map((plugin) => plugin.id);
}