fix: honor source plugin activation at startup

This commit is contained in:
Shakker
2026-04-27 13:18:43 +01:00
parent a88f2ba939
commit a964dcbddb
7 changed files with 185 additions and 11 deletions

View File

@@ -188,6 +188,34 @@ describe("applyPluginAutoEnable core", () => {
).toBe(false);
});
it("does not load disabled setup plugin manifests when another setup signal exists", () => {
const readFileSync = vi.spyOn(fs, "readFileSync");
const result = applyPluginAutoEnable({
config: {
plugins: {
allow: ["telegram"],
entries: {
browser: { enabled: false },
},
},
tools: {
allow: ["browser"],
},
},
env,
});
expect(result.config.plugins?.allow).toEqual(["telegram"]);
expect(result.config.plugins?.entries?.browser?.enabled).toBe(false);
expect(result.changes).toEqual([]);
expect(
readFileSync.mock.calls.some(
([filePath]) => typeof filePath === "string" && filePath.endsWith("openclaw.plugin.json"),
),
).toBe(false);
});
it("still treats a non-disabled browser plugin entry as setup auto-enable input", () => {
const result = applyPluginAutoEnable({
config: {

View File

@@ -351,7 +351,7 @@ function collectConfiguredPluginEntryIds(cfg: OpenClawConfig): string[] {
}
return Object.keys(entries)
.map((pluginId) => pluginId.trim())
.filter(Boolean);
.filter((pluginId) => pluginId && !isPluginEntryExplicitlyDisabled(cfg, pluginId));
}
function hasOwnPluginEntry(cfg: OpenClawConfig, pluginId: string): boolean {
@@ -359,16 +359,22 @@ function hasOwnPluginEntry(cfg: OpenClawConfig, pluginId: string): boolean {
return !!entries && typeof entries === "object" && Object.hasOwn(entries, pluginId);
}
function isPluginEntryExplicitlyDisabled(cfg: OpenClawConfig, pluginId: string): boolean {
return cfg.plugins?.entries?.[pluginId]?.enabled === false;
}
function hasNonDisabledPluginEntry(cfg: OpenClawConfig, pluginId: string): boolean {
if (!hasOwnPluginEntry(cfg, pluginId)) {
return false;
}
const entry = cfg.plugins?.entries?.[pluginId];
return !isRecord(entry) || entry.enabled !== false;
return !isPluginEntryExplicitlyDisabled(cfg, pluginId);
}
function hasBrowserSetupAutoEnableRelevantConfig(cfg: OpenClawConfig): boolean {
if (isRecord(cfg.browser) && cfg.browser.enabled !== false) {
if (cfg.browser?.enabled === false || isPluginEntryExplicitlyDisabled(cfg, "browser")) {
return false;
}
if (isRecord(cfg.browser)) {
return true;
}
if (hasNonDisabledPluginEntry(cfg, "browser")) {
@@ -378,6 +384,9 @@ function hasBrowserSetupAutoEnableRelevantConfig(cfg: OpenClawConfig): boolean {
}
function hasAcpxSetupAutoEnableRelevantConfig(cfg: OpenClawConfig): boolean {
if (isPluginEntryExplicitlyDisabled(cfg, "acpx")) {
return false;
}
if (!isRecord(cfg.acp)) {
return false;
}
@@ -390,6 +399,9 @@ function hasAcpxSetupAutoEnableRelevantConfig(cfg: OpenClawConfig): boolean {
}
function hasXaiSetupAutoEnableRelevantConfig(cfg: OpenClawConfig): boolean {
if (isPluginEntryExplicitlyDisabled(cfg, "xai")) {
return false;
}
const pluginConfig = cfg.plugins?.entries?.xai?.config;
return (
(isRecord(pluginConfig) &&

View File

@@ -47,6 +47,21 @@ function installGatewayPluginRuntimeEnvironment(cfg: OpenClawConfig) {
setGatewayNodesRuntime(createGatewayNodesRuntime());
}
function applyActivationSectionsToRuntimeConfig(params: {
runtimeConfig: OpenClawConfig;
activationConfig: OpenClawConfig;
}): OpenClawConfig {
return {
...params.runtimeConfig,
...(params.activationConfig.channels !== undefined
? { channels: params.activationConfig.channels }
: {}),
...(params.activationConfig.plugins !== undefined
? { plugins: params.activationConfig.plugins }
: {}),
};
}
function logGatewayPluginDiagnostics(params: {
diagnostics: PluginRegistry["diagnostics"];
log: Pick<GatewayPluginBootstrapLog, "error" | "info">;
@@ -78,7 +93,13 @@ export function prepareGatewayPluginLoad(params: GatewayPluginBootstrapParams) {
? { manifestRegistry: params.pluginLookUpTable.manifestRegistry }
: {}),
});
const resolvedConfig = autoEnabled.config;
const resolvedConfig =
activationSourceConfig === params.cfg
? autoEnabled.config
: applyActivationSectionsToRuntimeConfig({
runtimeConfig: params.cfg,
activationConfig: autoEnabled.config,
});
installGatewayPluginRuntimeEnvironment(resolvedConfig);
const loaded = loadGatewayPlugins({
cfg: resolvedConfig,

View File

@@ -1,4 +1,5 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/types.openclaw.js";
const applyPluginAutoEnable = vi.hoisted(() =>
vi.fn((params: { config: unknown }) => ({
@@ -193,6 +194,59 @@ describe("prepareGatewayPluginBootstrap runtime-deps staging", () => {
);
});
it("derives startup activation from source config instead of runtime plugin defaults", async () => {
const sourceConfig = {
plugins: {
allow: ["bench-plugin"],
},
} as OpenClawConfig;
const runtimeConfig = {
plugins: {
allow: ["bench-plugin"],
entries: {
"memory-core": {
config: {
dreaming: {
enabled: false,
},
},
},
},
},
} as OpenClawConfig;
const log = createLog();
const { prepareGatewayPluginBootstrap } = await import("./server-startup-plugins.js");
await prepareGatewayPluginBootstrap({
cfgAtStart: runtimeConfig,
activationSourceConfig: sourceConfig,
startupRuntimeConfig: runtimeConfig,
minimalTestGateway: false,
log,
});
expect(applyPluginAutoEnable).toHaveBeenCalledWith({
config: sourceConfig,
env: process.env,
});
expect(loadPluginLookUpTable).toHaveBeenCalledWith(
expect.objectContaining({
activationSourceConfig: sourceConfig,
config: expect.objectContaining({
plugins: sourceConfig.plugins,
}),
}),
);
expect(loadGatewayStartupPlugins).toHaveBeenCalledWith(
expect.objectContaining({
activationSourceConfig: sourceConfig,
cfg: expect.objectContaining({
plugins: sourceConfig.plugins,
}),
}),
);
});
it("falls back to per-plugin runtime-deps installs after failed pre-start scan", async () => {
scanBundledPluginRuntimeDeps.mockImplementationOnce(() => {
throw new Error("unsupported runtime dependency spec");

View File

@@ -23,6 +23,21 @@ type GatewayPluginBootstrapLog = {
debug: (message: string) => void;
};
function applyActivationSectionsToRuntimeConfig(params: {
runtimeConfig: OpenClawConfig;
activationConfig: OpenClawConfig;
}): OpenClawConfig {
return {
...params.runtimeConfig,
...(params.activationConfig.channels !== undefined
? { channels: params.activationConfig.channels }
: {}),
...(params.activationConfig.plugins !== undefined
? { plugins: params.activationConfig.plugins }
: {}),
};
}
async function prestageGatewayBundledRuntimeDeps(params: {
cfg: OpenClawConfig;
pluginIds: readonly string[];
@@ -92,10 +107,12 @@ async function prestageGatewayBundledRuntimeDeps(params: {
export async function prepareGatewayPluginBootstrap(params: {
cfgAtStart: OpenClawConfig;
activationSourceConfig?: OpenClawConfig;
startupRuntimeConfig: OpenClawConfig;
minimalTestGateway: boolean;
log: GatewayPluginBootstrapLog;
}) {
const activationSourceConfig = params.activationSourceConfig ?? params.cfgAtStart;
const startupMaintenanceConfig =
params.cfgAtStart.channels === undefined && params.startupRuntimeConfig.channels !== undefined
? {
@@ -130,10 +147,13 @@ export async function prepareGatewayPluginBootstrap(params: {
const gatewayPluginConfig = params.minimalTestGateway
? params.cfgAtStart
: applyPluginAutoEnable({
config: params.cfgAtStart,
env: process.env,
}).config;
: applyActivationSectionsToRuntimeConfig({
runtimeConfig: params.cfgAtStart,
activationConfig: applyPluginAutoEnable({
config: activationSourceConfig,
env: process.env,
}).config,
});
const defaultAgentId = resolveDefaultAgentId(gatewayPluginConfig);
const defaultWorkspaceDir = resolveAgentWorkspaceDir(gatewayPluginConfig, defaultAgentId);
const pluginLookUpTable = params.minimalTestGateway
@@ -142,7 +162,7 @@ export async function prepareGatewayPluginBootstrap(params: {
config: gatewayPluginConfig,
workspaceDir: defaultWorkspaceDir,
env: process.env,
activationSourceConfig: params.cfgAtStart,
activationSourceConfig,
});
const deferredConfiguredChannelPluginIds = [
...(pluginLookUpTable?.startup.configuredDeferredChannelPluginIds ?? []),
@@ -162,7 +182,7 @@ export async function prepareGatewayPluginBootstrap(params: {
});
({ pluginRegistry, gatewayMethods: baseGatewayMethods } = loadGatewayStartupPlugins({
cfg: gatewayPluginConfig,
activationSourceConfig: params.cfgAtStart,
activationSourceConfig,
workspaceDir: defaultWorkspaceDir,
log: params.log,
coreGatewayMethodNames: baseMethods,

View File

@@ -336,6 +336,7 @@ export async function startGatewayServer(
let cfgAtStart: OpenClawConfig;
let startupInternalWriteHash: string | null = null;
let startupLastGoodSnapshot = configSnapshot;
const startupActivationSourceConfig = configSnapshot.sourceConfig;
const startupRuntimeConfig = applyConfigOverrides(configSnapshot.config);
const authBootstrap = await startupTrace.measure("config.auth", () =>
prepareGatewayStartupConfig({
@@ -408,6 +409,7 @@ export async function startGatewayServer(
const pluginBootstrap = await startupTrace.measure("plugins.bootstrap", () =>
prepareGatewayPluginBootstrap({
cfgAtStart,
activationSourceConfig: startupActivationSourceConfig,
startupRuntimeConfig,
minimalTestGateway,
log,
@@ -856,6 +858,7 @@ export async function startGatewayServer(
const { reloadDeferredGatewayPlugins } = await import("./server-plugin-bootstrap.js");
({ pluginRegistry, gatewayMethods: baseGatewayMethods } = reloadDeferredGatewayPlugins({
cfg: gatewayPluginConfigAtStart,
activationSourceConfig: startupActivationSourceConfig,
workspaceDir: defaultWorkspaceDir,
log,
coreGatewayMethodNames: baseMethods,

View File

@@ -488,6 +488,42 @@ describe("resolveGatewayStartupPluginIds", () => {
});
});
it("does not let runtime-default plugin entries bypass the authored startup allowlist", () => {
const activationSourceConfig = {
channels: {},
plugins: {
allow: ["bench-plugin"],
entries: {
browser: {
enabled: false,
},
},
},
} as OpenClawConfig;
const runtimeConfig = {
...activationSourceConfig,
plugins: {
...activationSourceConfig.plugins,
entries: {
...activationSourceConfig.plugins?.entries,
"memory-core": {
config: {
dreaming: {
enabled: false,
},
},
},
},
},
} as OpenClawConfig;
expectStartupPluginIdsCase({
config: runtimeConfig,
activationSourceConfig,
expected: [],
});
});
it("starts bundled sidecars selected by root config activation paths", () => {
const rawConfig = {
browser: {