fix: include memory plugins in gateway startup (openclaw#64423)

Verified:
- pnpm build
- pnpm check
- pnpm test -- src/plugins/channel-plugin-ids.test.ts

Co-authored-by: EronFan <50734013+EronFan@users.noreply.github.com>
This commit is contained in:
EronFan
2026-04-11 05:02:44 +08:00
committed by GitHub
parent 241c63c7e0
commit 5e2136c6ae
3 changed files with 88 additions and 81 deletions

View File

@@ -44,6 +44,7 @@ Docs: https://docs.openclaw.ai
- Security/nodes: keep `nodes` tool output paths inside the workspace boundary so model-driven node writes cannot escape the intended workspace. (#63551) Thanks @pgondhi987.
- Security/QQBot: enforce media storage boundaries for all outbound local file paths and route image-size probes through SSRF-guarded media fetching instead of raw `fetch()`. (#63271, #63495) Thanks @pgondhi987.
- Channel setup: ignore workspace plugin shadows when resolving trusted channel setup catalog entries so onboarding and setup flows keep using the bundled, trusted setup contract.
- Gateway/memory startup: load the explicitly selected memory-slot plugin during gateway startup, while keeping restrictive allowlists and implicit default memory slots from auto-starting unrelated memory plugins. (#64423) Thanks @EronFan.
- Config/plugins: let config writes keep disabled plugin entries without forcing required plugin config schemas or crashing raw plugin validation, and avoid re-activating plugin registry state during schema checks. (#54971, #63296) Thanks @fuller-stack-dev.
- Config validation: surface the actual offending field for strict-schema union failures in bindings, including top-level unexpected keys on the matching ACP branch. (#40841) Thanks @Hollychou924.
- Wizard/plugin config: coerce integer-typed plugin config fields from interactive text input so integer schema values persist as numbers instead of failing validation. (#63346) Thanks @jalehman.

View File

@@ -49,6 +49,14 @@ function createManifestRegistryFixture() {
providers: ["demo-provider"],
cliBackends: ["demo-cli"],
},
{
id: "voice-call",
channels: [],
origin: "bundled",
enabledByDefault: undefined,
providers: [],
cliBackends: [],
},
{
id: "memory-core",
kind: "memory",
@@ -67,14 +75,6 @@ function createManifestRegistryFixture() {
providers: [],
cliBackends: [],
},
{
id: "voice-call",
channels: [],
origin: "bundled",
enabledByDefault: undefined,
providers: [],
cliBackends: [],
},
{
id: "demo-global-sidecar",
channels: [],
@@ -121,6 +121,7 @@ function createStartupConfig(params: {
channelIds?: string[];
allowPluginIds?: string[];
noConfiguredChannels?: boolean;
memorySlot?: string;
}) {
return {
...(params.noConfiguredChannels
@@ -138,6 +139,7 @@ function createStartupConfig(params: {
? {
plugins: {
...(params.allowPluginIds?.length ? { allow: params.allowPluginIds } : {}),
...(params.memorySlot ? { slots: { memory: params.memorySlot } } : {}),
entries: Object.fromEntries(
params.enabledPluginIds.map((pluginId) => [pluginId, { enabled: true }]),
),
@@ -149,6 +151,14 @@ function createStartupConfig(params: {
allow: params.allowPluginIds,
},
}
: params.memorySlot
? {
plugins: {
slots: {
memory: params.memorySlot,
},
},
}
: {}),
...(params.providerIds?.length
? {
@@ -250,6 +260,9 @@ describe("resolveGatewayStartupPluginIds", () => {
"voice-call": {
enabled: true,
},
"memory-core": {
enabled: true,
},
},
},
} as OpenClawConfig;
@@ -261,74 +274,32 @@ describe("resolveGatewayStartupPluginIds", () => {
});
});
it("includes memory-core at startup when dreaming is enabled", () => {
it("includes the explicitly selected memory slot plugin in startup scope", () => {
expectStartupPluginIdsCase({
config: {
channels: {},
plugins: {
entries: {
"memory-core": {
enabled: true,
config: {
dreaming: {
enabled: true,
},
},
},
},
},
} as OpenClawConfig,
expected: ["browser", "memory-core"],
config: createStartupConfig({
enabledPluginIds: ["memory-lancedb"],
memorySlot: "memory-lancedb",
}),
expected: ["demo-channel", "browser", "memory-lancedb"],
});
});
it("includes the selected memory-slot plugin and memory-core when dreaming is enabled", () => {
it("normalizes the raw memory slot id before startup filtering", () => {
expectStartupPluginIdsCase({
config: {
plugins: {
slots: {
memory: "memory-lancedb",
},
entries: {
"memory-core": {
enabled: true,
},
"memory-lancedb": {
enabled: true,
config: {
dreaming: {
enabled: true,
},
},
},
},
},
} as OpenClawConfig,
expected: ["demo-channel", "browser", "memory-core", "memory-lancedb"],
config: createStartupConfig({
enabledPluginIds: ["memory-core"],
memorySlot: "Memory-Core",
}),
expected: ["demo-channel", "browser", "memory-core"],
});
});
it("does not bypass activation policy for dreaming startup owners", () => {
it("does not include non-selected memory plugins only because they are enabled", () => {
expectStartupPluginIdsCase({
config: {
channels: {},
plugins: {
slots: {
memory: "memory-lancedb",
},
entries: {
"memory-lancedb": {
enabled: false,
config: {
dreaming: {
enabled: true,
},
},
},
},
},
} as OpenClawConfig,
expected: ["browser"],
config: createStartupConfig({
enabledPluginIds: ["memory-lancedb"],
}),
expected: ["demo-channel", "browser"],
});
});
});

View File

@@ -7,6 +7,7 @@ import {
} from "../memory-host-sdk/dreaming.js";
import {
createPluginActivationSource,
normalizePluginId,
normalizePluginsConfig,
resolveEffectivePluginActivationState,
} from "./config-state.js";
@@ -29,6 +30,10 @@ function hasRuntimeContractSurface(plugin: PluginManifestRecord): boolean {
);
}
function isGatewayStartupMemoryPlugin(plugin: PluginManifestRecord): boolean {
return hasKind(plugin.kind, "memory");
}
function isGatewayStartupSidecar(plugin: PluginManifestRecord): boolean {
return plugin.channels.length === 0 && !hasRuntimeContractSurface(plugin);
}
@@ -44,6 +49,33 @@ function resolveGatewayStartupDreamingPluginIds(config: OpenClawConfig): Set<str
return new Set(["memory-core", resolveMemoryDreamingPluginId(config)]);
}
function resolveExplicitMemorySlotStartupPluginId(
config: OpenClawConfig,
): string | undefined {
const configuredSlot = config.plugins?.slots?.memory?.trim();
if (!configuredSlot || configuredSlot.toLowerCase() === "none") {
return undefined;
}
return normalizePluginId(configuredSlot);
}
function shouldConsiderForGatewayStartup(params: {
plugin: PluginManifestRecord;
startupDreamingPluginIds: ReadonlySet<string>;
explicitMemorySlotStartupPluginId?: string;
}): boolean {
if (isGatewayStartupSidecar(params.plugin)) {
return true;
}
if (!isGatewayStartupMemoryPlugin(params.plugin)) {
return false;
}
if (params.startupDreamingPluginIds.has(params.plugin.id)) {
return true;
}
return params.explicitMemorySlotStartupPluginId === params.plugin.id;
}
export function resolveChannelPluginIds(params: {
config: OpenClawConfig;
workspaceDir?: string;
@@ -113,6 +145,9 @@ export function resolveGatewayStartupPluginIds(params: {
config: params.activationSourceConfig ?? params.config,
});
const startupDreamingPluginIds = resolveGatewayStartupDreamingPluginIds(params.config);
const explicitMemorySlotStartupPluginId = resolveExplicitMemorySlotStartupPluginId(
params.activationSourceConfig ?? params.config,
);
return loadPluginManifestRegistry({
config: params.config,
workspaceDir: params.workspaceDir,
@@ -122,6 +157,15 @@ export function resolveGatewayStartupPluginIds(params: {
if (plugin.channels.some((channelId) => configuredChannelIds.has(channelId))) {
return true;
}
if (
!shouldConsiderForGatewayStartup({
plugin,
startupDreamingPluginIds,
explicitMemorySlotStartupPluginId,
})
) {
return false;
}
const activationState = resolveEffectivePluginActivationState({
id: plugin.id,
origin: plugin.origin,
@@ -130,22 +174,13 @@ export function resolveGatewayStartupPluginIds(params: {
enabledByDefault: plugin.enabledByDefault,
activationSource,
});
const isAllowedStartupActivation = (): boolean => {
if (!activationState.enabled) {
return false;
}
if (plugin.origin !== "bundled") {
return activationState.explicitlyEnabled;
}
return activationState.source === "explicit" || activationState.source === "default";
};
if (startupDreamingPluginIds.has(plugin.id)) {
return isAllowedStartupActivation();
}
if (!isGatewayStartupSidecar(plugin)) {
if (!activationState.enabled) {
return false;
}
return isAllowedStartupActivation();
if (plugin.origin !== "bundled") {
return activationState.explicitlyEnabled;
}
return activationState.source === "explicit" || activationState.source === "default";
})
.map((plugin) => plugin.id);
}