fix: preserve runtime config during source plugin activation

This commit is contained in:
Shakker
2026-04-27 13:52:35 +01:00
parent a964dcbddb
commit f88c330657
5 changed files with 303 additions and 35 deletions

View File

@@ -0,0 +1,105 @@
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { isRecord } from "../utils.js";
function hasOwnValue(record: Record<string, unknown>, key: string): boolean {
return Object.prototype.hasOwnProperty.call(record, key);
}
function mergeChannelActivationSections(params: {
runtimeConfig: OpenClawConfig;
activationConfig: OpenClawConfig;
}): OpenClawConfig {
const activationChannels = params.activationConfig.channels;
if (!isRecord(activationChannels)) {
return params.runtimeConfig;
}
const runtimeChannels = isRecord(params.runtimeConfig.channels)
? params.runtimeConfig.channels
: {};
let nextChannels: Record<string, unknown> | undefined;
for (const [channelId, activationChannel] of Object.entries(activationChannels)) {
if (!isRecord(activationChannel) || !hasOwnValue(activationChannel, "enabled")) {
continue;
}
const runtimeChannel = runtimeChannels[channelId];
const runtimeChannelRecord = isRecord(runtimeChannel) ? runtimeChannel : {};
nextChannels ??= { ...runtimeChannels };
nextChannels[channelId] = {
...runtimeChannelRecord,
enabled: activationChannel.enabled,
};
}
if (nextChannels === undefined) {
return params.runtimeConfig;
}
return {
...params.runtimeConfig,
channels: nextChannels as OpenClawConfig["channels"],
};
}
function mergePluginActivationSections(params: {
runtimeConfig: OpenClawConfig;
activationConfig: OpenClawConfig;
}): OpenClawConfig {
const activationPlugins = params.activationConfig.plugins;
if (!isRecord(activationPlugins)) {
return params.runtimeConfig;
}
const runtimePlugins = isRecord(params.runtimeConfig.plugins) ? params.runtimeConfig.plugins : {};
let nextPlugins: Record<string, unknown> | undefined;
if (Array.isArray(activationPlugins.allow)) {
nextPlugins = {
...runtimePlugins,
allow: [...activationPlugins.allow],
};
}
const activationEntries = activationPlugins.entries;
if (isRecord(activationEntries)) {
const runtimeEntries = isRecord(runtimePlugins.entries) ? runtimePlugins.entries : {};
let nextEntries: Record<string, unknown> | undefined;
for (const [pluginId, activationEntry] of Object.entries(activationEntries)) {
if (!isRecord(activationEntry) || !hasOwnValue(activationEntry, "enabled")) {
continue;
}
const runtimeEntry = runtimeEntries[pluginId];
const runtimeEntryRecord = isRecord(runtimeEntry) ? runtimeEntry : {};
nextEntries ??= { ...runtimeEntries };
nextEntries[pluginId] = {
...runtimeEntryRecord,
enabled: activationEntry.enabled,
};
}
if (nextEntries !== undefined) {
nextPlugins = {
...runtimePlugins,
...nextPlugins,
entries: nextEntries,
};
}
}
if (nextPlugins === undefined) {
return params.runtimeConfig;
}
return {
...params.runtimeConfig,
plugins: nextPlugins as OpenClawConfig["plugins"],
};
}
export function mergeActivationSectionsIntoRuntimeConfig(params: {
runtimeConfig: OpenClawConfig;
activationConfig: OpenClawConfig;
}): OpenClawConfig {
return mergePluginActivationSections({
...params,
runtimeConfig: mergeChannelActivationSections(params),
});
}

View File

@@ -9,6 +9,7 @@ import {
setGatewayNodesRuntime,
setGatewaySubagentRuntime,
} from "../plugins/runtime/gateway-bindings.js";
import { mergeActivationSectionsIntoRuntimeConfig } from "./plugin-activation-runtime-config.js";
import type { GatewayRequestHandler } from "./server-methods/types.js";
import {
createGatewayNodesRuntime,
@@ -47,21 +48,6 @@ 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">;
@@ -96,7 +82,7 @@ export function prepareGatewayPluginLoad(params: GatewayPluginBootstrapParams) {
const resolvedConfig =
activationSourceConfig === params.cfg
? autoEnabled.config
: applyActivationSectionsToRuntimeConfig({
: mergeActivationSectionsIntoRuntimeConfig({
runtimeConfig: params.cfg,
activationConfig: autoEnabled.config,
});

View File

@@ -506,6 +506,111 @@ describe("loadGatewayPlugins", () => {
);
});
test("preserves runtime defaults while applying source activation to startup loads", async () => {
const rawConfig = {
channels: {
telegram: {
botToken: "token",
},
},
plugins: {
allow: ["bench-plugin"],
},
};
const runtimeConfig = {
channels: {
telegram: {
botToken: "token",
dmPolicy: "pairing" as const,
groupPolicy: "allowlist" as const,
},
},
plugins: {
allow: ["bench-plugin", "memory-core"],
entries: {
"bench-plugin": {
config: {
runtimeDefault: true,
},
},
"memory-core": {
config: {
dreaming: {
enabled: false,
},
},
},
},
},
};
const activationConfig = {
channels: {
telegram: {
botToken: "token",
enabled: true,
},
},
plugins: {
allow: ["bench-plugin"],
entries: {
"bench-plugin": {
enabled: true,
},
},
},
};
applyPluginAutoEnable.mockReturnValue({
config: activationConfig,
changes: [],
autoEnabledReasons: {
telegram: ["telegram configured"],
},
});
loadOpenClawPlugins.mockReturnValue(createRegistry([]));
loadGatewayStartupPluginsForTest({
cfg: runtimeConfig,
activationSourceConfig: rawConfig,
pluginIds: ["telegram"],
});
expect(loadOpenClawPlugins).toHaveBeenCalledWith(
expect.objectContaining({
config: expect.objectContaining({
channels: expect.objectContaining({
telegram: expect.objectContaining({
enabled: true,
dmPolicy: "pairing",
groupPolicy: "allowlist",
}),
}),
plugins: expect.objectContaining({
allow: ["bench-plugin"],
entries: expect.objectContaining({
"bench-plugin": expect.objectContaining({
enabled: true,
config: {
runtimeDefault: true,
},
}),
"memory-core": {
config: {
dreaming: {
enabled: false,
},
},
},
}),
}),
}),
activationSourceConfig: rawConfig,
autoEnabledReasons: {
telegram: ["telegram configured"],
},
}),
);
});
test("treats an empty startup scope as no plugin load instead of an unscoped load", async () => {
loadPluginLookUpTable.mockReturnValue({
startup: {

View File

@@ -196,14 +196,47 @@ describe("prepareGatewayPluginBootstrap runtime-deps staging", () => {
it("derives startup activation from source config instead of runtime plugin defaults", async () => {
const sourceConfig = {
channels: {
telegram: {
botToken: "token",
},
},
plugins: {
allow: ["bench-plugin"],
},
} as OpenClawConfig;
const runtimeConfig = {
const activationConfig = {
channels: {
telegram: {
botToken: "token",
enabled: true,
},
},
plugins: {
allow: ["bench-plugin"],
entries: {
"bench-plugin": {
enabled: true,
},
},
},
} as OpenClawConfig;
const runtimeConfig = {
channels: {
telegram: {
botToken: "token",
dmPolicy: "pairing",
groupPolicy: "allowlist",
},
},
plugins: {
allow: ["bench-plugin", "memory-core"],
entries: {
"bench-plugin": {
config: {
runtimeDefault: true,
},
},
"memory-core": {
config: {
dreaming: {
@@ -214,6 +247,11 @@ describe("prepareGatewayPluginBootstrap runtime-deps staging", () => {
},
},
} as OpenClawConfig;
applyPluginAutoEnable.mockReturnValueOnce({
config: activationConfig,
changes: [],
autoEnabledReasons: {},
});
const log = createLog();
const { prepareGatewayPluginBootstrap } = await import("./server-startup-plugins.js");
@@ -233,7 +271,31 @@ describe("prepareGatewayPluginBootstrap runtime-deps staging", () => {
expect.objectContaining({
activationSourceConfig: sourceConfig,
config: expect.objectContaining({
plugins: sourceConfig.plugins,
channels: expect.objectContaining({
telegram: expect.objectContaining({
enabled: true,
dmPolicy: "pairing",
groupPolicy: "allowlist",
}),
}),
plugins: expect.objectContaining({
allow: ["bench-plugin"],
entries: expect.objectContaining({
"bench-plugin": expect.objectContaining({
enabled: true,
config: {
runtimeDefault: true,
},
}),
"memory-core": {
config: {
dreaming: {
enabled: false,
},
},
},
}),
}),
}),
}),
);
@@ -241,7 +303,31 @@ describe("prepareGatewayPluginBootstrap runtime-deps staging", () => {
expect.objectContaining({
activationSourceConfig: sourceConfig,
cfg: expect.objectContaining({
plugins: sourceConfig.plugins,
channels: expect.objectContaining({
telegram: expect.objectContaining({
enabled: true,
dmPolicy: "pairing",
groupPolicy: "allowlist",
}),
}),
plugins: expect.objectContaining({
allow: ["bench-plugin"],
entries: expect.objectContaining({
"bench-plugin": expect.objectContaining({
enabled: true,
config: {
runtimeDefault: true,
},
}),
"memory-core": {
config: {
dreaming: {
enabled: false,
},
},
},
}),
}),
}),
}),
);

View File

@@ -12,6 +12,7 @@ import {
import { loadPluginLookUpTable } from "../plugins/plugin-lookup-table.js";
import { createEmptyPluginRegistry } from "../plugins/registry.js";
import { getActivePluginRegistry, setActivePluginRegistry } from "../plugins/runtime.js";
import { mergeActivationSectionsIntoRuntimeConfig } from "./plugin-activation-runtime-config.js";
import { listGatewayMethods } from "./server-methods-list.js";
import { loadGatewayStartupPlugins } from "./server-plugin-bootstrap.js";
import { runStartupSessionMigration } from "./server-startup-session-migration.js";
@@ -23,21 +24,6 @@ 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[];
@@ -147,7 +133,7 @@ export async function prepareGatewayPluginBootstrap(params: {
const gatewayPluginConfig = params.minimalTestGateway
? params.cfgAtStart
: applyActivationSectionsToRuntimeConfig({
: mergeActivationSectionsIntoRuntimeConfig({
runtimeConfig: params.cfgAtStart,
activationConfig: applyPluginAutoEnable({
config: activationSourceConfig,