Prevent disabled plugins from warming the gateway plugin graph

A local containment profile uses plugins.enabled=false to stop plugin and channel runtime churn. The previous startup path still built plugin lookup tables and doctor stale scans despite the global disable, which made the switch noisy and slow.

Constraint: plugins.enabled=false must leave channel blocker warnings intact while treating stale plugin config as inert.
Rejected: Clear user plugin config automatically | would mutate a reversible containment setting.
Confidence: high
Scope-risk: narrow
Directive: Do not reintroduce plugin registry discovery before checking plugins.enabled.
Tested: pnpm test src/gateway/server-startup-plugins.test.ts src/config/plugin-auto-enable.core.test.ts src/commands/doctor/shared/stale-plugin-config.test.ts src/commands/doctor/shared/preview-warnings.test.ts
Tested: pnpm check:changed
Tested: pnpm build
This commit is contained in:
Intern Dev
2026-04-27 16:58:49 -04:00
committed by Peter Steinberger
parent 5bdfc251ff
commit f07844450c
8 changed files with 143 additions and 10 deletions

View File

@@ -362,4 +362,34 @@ describe("doctor preview warnings", () => {
]);
expect(warnings[0]).not.toContain("first-time setup mode");
});
it("keeps global plugin-disable blocker warnings but omits stale plugin cleanup warnings", async () => {
manifestState.plugins = [channelManifest("telegram", "telegram")];
const warnings = await collectDoctorPreviewWarnings({
cfg: {
channels: {
telegram: {
botToken: "123:abc",
groupPolicy: "allowlist",
},
},
plugins: {
enabled: false,
allow: ["acpx"],
entries: {
acpx: { enabled: true },
},
},
},
doctorFixCommand: "openclaw doctor --fix",
});
expect(warnings).toEqual([
expect.stringContaining(
"channels.telegram: channel is configured, but plugins.enabled=false blocks channel plugins globally.",
),
]);
expect(warnings.join("\n")).not.toContain("stale plugin reference");
});
});

View File

@@ -123,7 +123,7 @@ export async function collectDoctorPreviewWarnings(params: {
}
}
if (hasPluginConfig || hasChannelConfig) {
if ((hasPluginConfig || hasChannelConfig) && params.cfg.plugins?.enabled !== false) {
const {
collectStalePluginConfigWarnings,
isStalePluginAutoRepairBlocked,

View File

@@ -198,6 +198,27 @@ describe("doctor stale plugin config helpers", () => {
expect(maybeRepairStalePluginConfig(cfg)).toEqual({ config: cfg, changes: [] });
});
it("treats stale plugin refs as inert while plugins are globally disabled", () => {
const cfg = {
plugins: {
enabled: false,
allow: ["acpx"],
entries: {
acpx: { enabled: true },
},
},
channels: {
"openclaw-weixin": {
enabled: true,
},
},
} as OpenClawConfig;
expect(scanStalePluginConfig(cfg)).toEqual([]);
expect(maybeRepairStalePluginConfig(cfg)).toEqual({ config: cfg, changes: [] });
expect(manifestRegistry.loadPluginManifestRegistry).not.toHaveBeenCalled();
});
it("uses missing persisted install records as stale channel evidence", () => {
installedPluginIndexMocks.loadInstalledPluginIndexInstallRecordsSync.mockReturnValue({
"openclaw-weixin": {

View File

@@ -74,6 +74,9 @@ export function isStalePluginAutoRepairBlocked(
cfg: OpenClawConfig,
env?: NodeJS.ProcessEnv,
): boolean {
if (cfg.plugins?.enabled === false) {
return false;
}
return collectPluginRegistryState(cfg, env).hasDiscoveryErrors;
}
@@ -81,6 +84,9 @@ export function scanStalePluginConfig(
cfg: OpenClawConfig,
env?: NodeJS.ProcessEnv,
): StalePluginConfigHit[] {
if (cfg.plugins?.enabled === false) {
return [];
}
return scanStalePluginConfigWithState(cfg, collectPluginRegistryState(cfg, env));
}
@@ -268,6 +274,9 @@ export function maybeRepairStalePluginConfig(
config: OpenClawConfig;
changes: string[];
} {
if (cfg.plugins?.enabled === false) {
return { config: cfg, changes: [] };
}
const registryState = collectPluginRegistryState(cfg, env);
if (registryState.hasDiscoveryErrors) {
return { config: cfg, changes: [] };

View File

@@ -720,6 +720,21 @@ describe("applyPluginAutoEnable core", () => {
});
it("skips when plugins are globally disabled", () => {
expect(
detectPluginAutoEnableCandidates({
config: {
channels: { slack: { botToken: "x" } },
plugins: {
enabled: false,
allow: ["slack"],
entries: { slack: { config: { botToken: "x" } } },
},
},
env,
manifestRegistry: makeRegistry([{ id: "slack", channels: ["slack"] }]),
}),
).toEqual([]);
const result = applyPluginAutoEnable({
config: {
channels: { slack: { botToken: "x" } },

View File

@@ -466,7 +466,14 @@ function hasConfiguredProviderModelOrHarness(cfg: OpenClawConfig, env: NodeJS.Pr
return hasConfiguredEmbeddedHarnessRuntime(cfg, env);
}
function arePluginsGloballyDisabled(cfg: OpenClawConfig): boolean {
return cfg.plugins?.enabled === false;
}
function configMayNeedPluginManifestRegistry(cfg: OpenClawConfig, env: NodeJS.ProcessEnv): boolean {
if (arePluginsGloballyDisabled(cfg)) {
return false;
}
if (hasPluginAllowlistWithMaterialEntries(cfg)) {
return true;
}
@@ -493,6 +500,9 @@ export function configMayNeedPluginAutoEnable(
cfg: OpenClawConfig,
env: NodeJS.ProcessEnv,
): boolean {
if (arePluginsGloballyDisabled(cfg)) {
return false;
}
if (hasPluginAllowlistWithMaterialEntries(cfg)) {
return true;
}

View File

@@ -410,4 +410,50 @@ describe("prepareGatewayPluginBootstrap runtime-deps staging", () => {
"bundledRuntimeDepsInstaller",
);
});
it("bypasses plugin lookup and runtime-deps staging when plugins are globally disabled", async () => {
const cfg = {
channels: {
telegram: {
botToken: "token",
},
},
plugins: {
enabled: false,
allow: ["telegram"],
entries: {
telegram: { enabled: true },
},
},
} as OpenClawConfig;
const log = createLog();
const { prepareGatewayPluginBootstrap } = await import("./server-startup-plugins.js");
await expect(
prepareGatewayPluginBootstrap({
cfgAtStart: cfg,
startupRuntimeConfig: cfg,
minimalTestGateway: false,
log,
}),
).resolves.toMatchObject({
startupPluginIds: [],
deferredConfiguredChannelPluginIds: [],
pluginLookUpTable: undefined,
baseGatewayMethods: ["ping"],
});
expect(loadPluginLookUpTable).not.toHaveBeenCalled();
expect(scanBundledPluginRuntimeDeps).not.toHaveBeenCalled();
expect(repairBundledRuntimeDepsInstallRootAsync).not.toHaveBeenCalled();
expect(loadGatewayStartupPlugins).toHaveBeenCalledWith(
expect.objectContaining({
cfg,
pluginIds: [],
pluginLookUpTable: undefined,
preferSetupRuntimeForChannelPlugins: false,
suppressPluginInfoLogs: false,
}),
);
});
});

View File

@@ -155,17 +155,19 @@ export async function prepareGatewayPluginBootstrap(params: {
: {}),
}).config,
});
const pluginsGloballyDisabled = gatewayPluginConfig.plugins?.enabled === false;
const defaultAgentId = resolveDefaultAgentId(gatewayPluginConfig);
const defaultWorkspaceDir = resolveAgentWorkspaceDir(gatewayPluginConfig, defaultAgentId);
const pluginLookUpTable = params.minimalTestGateway
? undefined
: loadPluginLookUpTable({
config: gatewayPluginConfig,
workspaceDir: defaultWorkspaceDir,
env: process.env,
activationSourceConfig,
metadataSnapshot: params.pluginMetadataSnapshot,
});
const pluginLookUpTable =
params.minimalTestGateway || pluginsGloballyDisabled
? undefined
: loadPluginLookUpTable({
config: gatewayPluginConfig,
workspaceDir: defaultWorkspaceDir,
env: process.env,
activationSourceConfig,
metadataSnapshot: params.pluginMetadataSnapshot,
});
const deferredConfiguredChannelPluginIds = [
...(pluginLookUpTable?.startup.configuredDeferredChannelPluginIds ?? []),
];