From 8839162b97b4efee76666be7ee68cf88728384f5 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 22 Feb 2026 19:18:08 +0100 Subject: [PATCH] fix(config): persist built-in channel enable state in channels Co-authored-by: HirokiKobayashi-R --- CHANGELOG.md | 1 + src/config/plugin-auto-enable.test.ts | 28 +++++++++---- src/config/plugin-auto-enable.ts | 58 +++++++++++++++++++++++++-- src/plugins/enable.test.ts | 8 ++++ src/plugins/enable.ts | 32 +++++++++++++-- 5 files changed, 112 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4517a13f57..716c6676718 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ Docs: https://docs.openclaw.ai - Feishu/Commands: in group chats, command authorization now falls back to top-level `channels.feishu.allowFrom` when per-group `allowFrom` is not set, so `/command` no longer gets blocked by an unintended empty allowlist. (#23756) - Feishu/Plugins: restore bundled Feishu SDK availability for global installs and strip `openclaw: workspace:*` from plugin `devDependencies` during plugin-version sync so npm-installed Feishu plugins do not fail dependency install. (#23611, #23645, #23603) - Plugins/Install: strip `workspace:*` devDependency entries from copied plugin manifests before `npm install --omit=dev`, preventing `EUNSUPPORTEDPROTOCOL` install failures for npm-published channel plugins (including Feishu and MS Teams). +- Config/Channels: auto-enable built-in channels by writing `channels..enabled=true` (not `plugins.entries.`), and stop adding built-ins to `plugins.allow`, preventing `plugins.entries.telegram: plugin not found` validation failures. - Dev tooling: prevent `CLAUDE.md` symlink target regressions by excluding CLAUDE symlink sentinels from `oxfmt` and marking them `-text` in `.gitattributes`, so formatter/EOL normalization cannot reintroduce trailing-newline targets. Thanks @vincentkoc. - Cron: honor `cron.maxConcurrentRuns` in the timer loop so due jobs can execute up to the configured parallelism instead of always running serially. (#11595) Thanks @Takhoffman. - Agents/Compaction: restore embedded compaction safeguard/context-pruning extension loading in production by wiring bundled extension factories into the resource loader instead of runtime file-path resolution. (#22349) Thanks @Glucksberg. diff --git a/src/config/plugin-auto-enable.test.ts b/src/config/plugin-auto-enable.test.ts index f8312901f49..284aea923dd 100644 --- a/src/config/plugin-auto-enable.test.ts +++ b/src/config/plugin-auto-enable.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest"; import { applyPluginAutoEnable } from "./plugin-auto-enable.js"; describe("applyPluginAutoEnable", () => { - it("auto-enables channel plugins and updates allowlist", () => { + it("auto-enables built-in channels without touching plugins allowlist", () => { const result = applyPluginAutoEnable({ config: { channels: { slack: { botToken: "x" } }, @@ -11,8 +11,9 @@ describe("applyPluginAutoEnable", () => { env: {}, }); - expect(result.config.plugins?.entries?.slack?.enabled).toBe(true); - expect(result.config.plugins?.allow).toEqual(["telegram", "slack"]); + expect(result.config.channels?.slack?.enabled).toBe(true); + expect(result.config.plugins?.entries?.slack).toBeUndefined(); + expect(result.config.plugins?.allow).toEqual(["telegram"]); expect(result.changes.join("\n")).toContain("Slack configured, enabled automatically."); }); @@ -48,6 +49,19 @@ describe("applyPluginAutoEnable", () => { expect(result.changes).toEqual([]); }); + it("respects built-in channel explicit disable via channels..enabled", () => { + const result = applyPluginAutoEnable({ + config: { + channels: { slack: { botToken: "x", enabled: false } }, + }, + env: {}, + }); + + expect(result.config.channels?.slack?.enabled).toBe(false); + expect(result.config.plugins?.entries?.slack).toBeUndefined(); + expect(result.changes).toEqual([]); + }); + it("auto-enables irc when configured via env", () => { const result = applyPluginAutoEnable({ config: {}, @@ -57,7 +71,7 @@ describe("applyPluginAutoEnable", () => { }, }); - expect(result.config.plugins?.entries?.irc?.enabled).toBe(true); + expect(result.config.channels?.irc?.enabled).toBe(true); expect(result.changes.join("\n")).toContain("IRC configured, enabled automatically."); }); @@ -141,7 +155,7 @@ describe("applyPluginAutoEnable", () => { }); expect(result.config.plugins?.entries?.bluebubbles?.enabled).toBe(false); - expect(result.config.plugins?.entries?.imessage?.enabled).toBe(true); + expect(result.config.channels?.imessage?.enabled).toBe(true); expect(result.changes.join("\n")).toContain("iMessage configured, enabled automatically."); }); @@ -158,7 +172,7 @@ describe("applyPluginAutoEnable", () => { }); expect(result.config.plugins?.entries?.bluebubbles?.enabled).toBeUndefined(); - expect(result.config.plugins?.entries?.imessage?.enabled).toBe(true); + expect(result.config.channels?.imessage?.enabled).toBe(true); }); it("auto-enables imessage when only imessage is configured", () => { @@ -169,7 +183,7 @@ describe("applyPluginAutoEnable", () => { env: {}, }); - expect(result.config.plugins?.entries?.imessage?.enabled).toBe(true); + expect(result.config.channels?.imessage?.enabled).toBe(true); expect(result.changes.join("\n")).toContain("iMessage configured, enabled automatically."); }); }); diff --git a/src/config/plugin-auto-enable.ts b/src/config/plugin-auto-enable.ts index 55eab9905e4..46365fc7853 100644 --- a/src/config/plugin-auto-enable.ts +++ b/src/config/plugin-auto-enable.ts @@ -322,7 +322,7 @@ function resolveConfiguredPlugins( if (key === "defaults" || key === "modelByChannel") { continue; } - channelIds.add(key); + channelIds.add(normalizeChatChannelId(key) ?? key); } } for (const channelId of channelIds) { @@ -348,6 +348,19 @@ function resolveConfiguredPlugins( } function isPluginExplicitlyDisabled(cfg: OpenClawConfig, pluginId: string): boolean { + const builtInChannelId = normalizeChatChannelId(pluginId); + if (builtInChannelId) { + const channels = cfg.channels as Record | undefined; + const channelConfig = channels?.[builtInChannelId]; + if ( + channelConfig && + typeof channelConfig === "object" && + !Array.isArray(channelConfig) && + (channelConfig as { enabled?: unknown }).enabled === false + ) { + return true; + } + } const entry = cfg.plugins?.entries?.[pluginId]; return entry?.enabled === false; } @@ -390,6 +403,25 @@ function shouldSkipPreferredPluginAutoEnable( } function registerPluginEntry(cfg: OpenClawConfig, pluginId: string): OpenClawConfig { + const builtInChannelId = normalizeChatChannelId(pluginId); + if (builtInChannelId) { + const channels = cfg.channels as Record | undefined; + const existing = channels?.[builtInChannelId]; + const existingRecord = + existing && typeof existing === "object" && !Array.isArray(existing) + ? (existing as Record) + : {}; + return { + ...cfg, + channels: { + ...cfg.channels, + [builtInChannelId]: { + ...existingRecord, + enabled: true, + }, + }, + }; + } const entries = { ...cfg.plugins?.entries, [pluginId]: { @@ -434,6 +466,7 @@ export function applyPluginAutoEnable(params: { } for (const entry of configured) { + const builtInChannelId = normalizeChatChannelId(entry.pluginId); if (isPluginDenied(next, entry.pluginId)) { continue; } @@ -444,13 +477,30 @@ export function applyPluginAutoEnable(params: { continue; } const allow = next.plugins?.allow; - const allowMissing = Array.isArray(allow) && !allow.includes(entry.pluginId); - const alreadyEnabled = next.plugins?.entries?.[entry.pluginId]?.enabled === true; + const allowMissing = + !builtInChannelId && Array.isArray(allow) && !allow.includes(entry.pluginId); + const alreadyEnabled = + builtInChannelId != null + ? (() => { + const channels = next.channels as Record | undefined; + const channelConfig = channels?.[builtInChannelId]; + if ( + !channelConfig || + typeof channelConfig !== "object" || + Array.isArray(channelConfig) + ) { + return false; + } + return (channelConfig as { enabled?: unknown }).enabled === true; + })() + : next.plugins?.entries?.[entry.pluginId]?.enabled === true; if (alreadyEnabled && !allowMissing) { continue; } next = registerPluginEntry(next, entry.pluginId); - next = ensurePluginAllowlisted(next, entry.pluginId); + if (!builtInChannelId) { + next = ensurePluginAllowlisted(next, entry.pluginId); + } changes.push(formatAutoEnableChange(entry)); } diff --git a/src/plugins/enable.test.ts b/src/plugins/enable.test.ts index 920b524e1ee..73dbfce8462 100644 --- a/src/plugins/enable.test.ts +++ b/src/plugins/enable.test.ts @@ -31,4 +31,12 @@ describe("enablePluginInConfig", () => { expect(result.enabled).toBe(false); expect(result.reason).toBe("blocked by denylist"); }); + + it("writes built-in channels to channels..enabled instead of plugins.entries", () => { + const cfg: OpenClawConfig = {}; + const result = enablePluginInConfig(cfg, "telegram"); + expect(result.enabled).toBe(true); + expect(result.config.channels?.telegram?.enabled).toBe(true); + expect(result.config.plugins?.entries?.telegram).toBeUndefined(); + }); }); diff --git a/src/plugins/enable.ts b/src/plugins/enable.ts index 1602af22cca..6df4a6cbe01 100644 --- a/src/plugins/enable.ts +++ b/src/plugins/enable.ts @@ -1,3 +1,4 @@ +import { normalizeChatChannelId } from "../channels/registry.js"; import type { OpenClawConfig } from "../config/config.js"; import { ensurePluginAllowlisted } from "../config/plugins-allowlist.js"; @@ -8,17 +9,40 @@ export type PluginEnableResult = { }; export function enablePluginInConfig(cfg: OpenClawConfig, pluginId: string): PluginEnableResult { + const builtInChannelId = normalizeChatChannelId(pluginId); + const resolvedId = builtInChannelId ?? pluginId; if (cfg.plugins?.enabled === false) { return { config: cfg, enabled: false, reason: "plugins disabled" }; } - if (cfg.plugins?.deny?.includes(pluginId)) { + if (cfg.plugins?.deny?.includes(pluginId) || cfg.plugins?.deny?.includes(resolvedId)) { return { config: cfg, enabled: false, reason: "blocked by denylist" }; } + if (builtInChannelId) { + const channels = cfg.channels as Record | undefined; + const existing = channels?.[builtInChannelId]; + const existingRecord = + existing && typeof existing === "object" && !Array.isArray(existing) + ? (existing as Record) + : {}; + return { + config: { + ...cfg, + channels: { + ...cfg.channels, + [builtInChannelId]: { + ...existingRecord, + enabled: true, + }, + }, + }, + enabled: true, + }; + } const entries = { ...cfg.plugins?.entries, - [pluginId]: { - ...(cfg.plugins?.entries?.[pluginId] as Record | undefined), + [resolvedId]: { + ...(cfg.plugins?.entries?.[resolvedId] as Record | undefined), enabled: true, }, }; @@ -29,6 +53,6 @@ export function enablePluginInConfig(cfg: OpenClawConfig, pluginId: string): Plu entries, }, }; - next = ensurePluginAllowlisted(next, pluginId); + next = ensurePluginAllowlisted(next, resolvedId); return { config: next, enabled: true }; }