mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:50:43 +00:00
fix: preserve runtime config during source plugin activation
This commit is contained in:
105
src/gateway/plugin-activation-runtime-config.ts
Normal file
105
src/gateway/plugin-activation-runtime-config.ts
Normal 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),
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user