diff --git a/CHANGELOG.md b/CHANGELOG.md index 072241c17e5..c906638553e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -249,6 +249,7 @@ Docs: https://docs.openclaw.ai - Channels/Telegram: require an observed Telegram send, edit, or fallback before treating a forum-topic final as delivered, so final replies generated in transcript no longer disappear from Telegram topics. Fixes #76554. (#76764) Thanks @bubucilo and @obviyus. - Plugins/update: keep externalized bundled npm bridge updates on the normal plugin security scanner path instead of granting source-linked official trust without artifact provenance. (#76765) Thanks @Lucenx9. - Agents/reply context: label replied-to messages as the current user message target in model-visible metadata, so short replies are grounded to their explicit reply target instead of nearby chat history. (#76817) Thanks @obviyus. +- Config/validation: skip the `plugin not found` warning for `plugins.allow` entries that match a known channel id, so packaged installs where the gateway auto-enables a channel id (e.g. `discord`) without a corresponding plugin manifest no longer emit an unfixable doctor warning. Fixes #76872. Thanks @jack-stormentswe. ## 2026.5.2 diff --git a/src/config/config.plugin-validation.test.ts b/src/config/config.plugin-validation.test.ts index e8b8329ebc0..8d03504125b 100644 --- a/src/config/config.plugin-validation.test.ts +++ b/src/config/config.plugin-validation.test.ts @@ -467,6 +467,25 @@ describe("config plugin validation", () => { expect(res.warnings).not.toContainEqual(expect.objectContaining({ path: "channels.telegarm" })); }); + it("does not warn when plugins.allow contains a known channel id without a plugin manifest (#76872)", async () => { + const res = validateInSuite({ + agents: { list: [{ id: "pi" }] }, + channels: { + discord: { token: "xxx" }, + }, + plugins: { + allow: ["discord"], + }, + }); + + expect(res.ok).toBe(true); + expect(res.warnings ?? []).not.toContainEqual({ + path: "plugins.allow", + message: + "plugin not found: discord (stale config entry ignored; remove it from plugins config)", + }); + }); + it("uses persisted installed-plugin records as stale channel evidence", async () => { const installedPluginIndexPath = path.join(suiteHome, ".openclaw", "plugins", "installs.json"); await mkdirSafe(path.dirname(installedPluginIndexPath)); diff --git a/src/config/validation.ts b/src/config/validation.ts index 60d4375cff7..86327b645c6 100644 --- a/src/config/validation.ts +++ b/src/config/validation.ts @@ -794,6 +794,7 @@ function validateConfigObjectWithPluginsBase( type RegistryInfo = { registry: PluginManifestRegistry; knownIds?: Set; + knownChannelIds?: Set; overriddenPluginIds?: Set; normalizedPlugins?: ReturnType; channelSchemas?: Map< @@ -908,6 +909,29 @@ function validateConfigObjectWithPluginsBase( return info.knownIds; }; + const ensureKnownChannelIds = (): Set => { + const info = ensureRegistry(); + if (!info.knownChannelIds) { + const ids = new Set(); + for (const channelId of CHANNEL_IDS) { + const normalized = normalizePluginId(channelId); + if (normalized) { + ids.add(normalized); + } + } + for (const plugin of info.registry.plugins) { + for (const channelId of plugin.channels) { + const normalized = normalizePluginId(channelId); + if (normalized) { + ids.add(normalized); + } + } + } + info.knownChannelIds = ids; + } + return info.knownChannelIds; + }; + const ensureOverriddenPluginIds = (): Set => { const info = ensureRegistry(); if (!info.overriddenPluginIds) { @@ -1522,11 +1546,15 @@ function validateConfigObjectWithPluginsBase( } const allow = pluginsConfig?.allow ?? []; + const knownChannelIds = ensureKnownChannelIds(); for (const pluginId of allow) { if (typeof pluginId !== "string" || !pluginId.trim()) { continue; } if (!knownIds.has(pluginId)) { + if (knownChannelIds.has(normalizePluginId(pluginId))) { + continue; + } const commandAlias = resolveManifestCommandAliasOwnerInRegistry({ command: pluginId, registry,