From 253f6cff988da63d261fa5154531c393847d300e Mon Sep 17 00:00:00 2001 From: HirokiKobayashi-R Date: Thu, 29 Jan 2026 17:07:15 +0900 Subject: [PATCH] fix(config): write built-in channel enabled state to channels, not plugins.entries Built-in channels (telegram, slack, discord, etc.) were being auto-enabled via `plugins.entries..enabled`, which fails config validation because these channels are not plugins. Now built-in channels are enabled via `channels..enabled` and plugin channels continue to use `plugins.entries..enabled`. This also means built-in channels no longer need to be added to `plugins.allow` since they're not loaded through the plugin system. Fixes #3741 --- src/config/plugin-auto-enable.test.ts | 45 +++++++++++++------ src/config/plugin-auto-enable.ts | 62 ++++++++++++++++++++++++--- 2 files changed, 90 insertions(+), 17 deletions(-) diff --git a/src/config/plugin-auto-enable.test.ts b/src/config/plugin-auto-enable.test.ts index 8399389e3..0d86097b0 100644 --- a/src/config/plugin-auto-enable.test.ts +++ b/src/config/plugin-auto-enable.test.ts @@ -11,21 +11,27 @@ describe("applyPluginAutoEnable", () => { env: {}, }); - expect(result.config.plugins?.entries?.slack?.enabled).toBe(true); - expect(result.config.plugins?.allow).toEqual(["telegram", "slack"]); + // Built-in channels (slack) are enabled via channels..enabled, not plugins.entries. + expect((result.config.channels as Record)?.slack).toMatchObject({ + enabled: true, + }); + // Built-in channels don't need plugins.allow entry. + expect(result.config.plugins?.allow).toEqual(["telegram"]); expect(result.changes.join("\n")).toContain("Slack configured, not enabled yet."); }); it("respects explicit disable", () => { const result = applyPluginAutoEnable({ config: { - channels: { slack: { botToken: "x" } }, - plugins: { entries: { slack: { enabled: false } } }, + channels: { slack: { botToken: "x", enabled: false } }, }, env: {}, }); - expect(result.config.plugins?.entries?.slack?.enabled).toBe(false); + // Built-in channels check enabled in channels..enabled. + expect((result.config.channels as Record)?.slack).toMatchObject({ + enabled: false, + }); expect(result.changes).toEqual([]); }); @@ -72,8 +78,12 @@ describe("applyPluginAutoEnable", () => { env: {}, }); + // bluebubbles is a plugin channel, so it goes to plugins.entries. expect(result.config.plugins?.entries?.bluebubbles?.enabled).toBe(true); - expect(result.config.plugins?.entries?.imessage?.enabled).toBeUndefined(); + // imessage is a built-in channel, but it's skipped due to preferOver. + expect((result.config.channels as Record)?.imessage).not.toMatchObject({ + enabled: true, + }); expect(result.changes.join("\n")).toContain("bluebubbles configured, not enabled yet."); expect(result.changes.join("\n")).not.toContain("iMessage configured, not enabled yet."); }); @@ -83,15 +93,17 @@ describe("applyPluginAutoEnable", () => { config: { channels: { bluebubbles: { serverUrl: "http://localhost:1234", password: "x" }, - imessage: { cliPath: "/usr/local/bin/imsg" }, + imessage: { cliPath: "/usr/local/bin/imsg", enabled: true }, }, - plugins: { entries: { imessage: { enabled: true } } }, }, env: {}, }); expect(result.config.plugins?.entries?.bluebubbles?.enabled).toBe(true); - expect(result.config.plugins?.entries?.imessage?.enabled).toBe(true); + // imessage was already enabled in channels, stays enabled. + expect((result.config.channels as Record)?.imessage).toMatchObject({ + enabled: true, + }); }); it("allows imessage auto-enable when bluebubbles is explicitly disabled", () => { @@ -107,7 +119,10 @@ describe("applyPluginAutoEnable", () => { }); expect(result.config.plugins?.entries?.bluebubbles?.enabled).toBe(false); - expect(result.config.plugins?.entries?.imessage?.enabled).toBe(true); + // imessage is a built-in channel, so it goes to channels.imessage.enabled. + expect((result.config.channels as Record)?.imessage).toMatchObject({ + enabled: true, + }); expect(result.changes.join("\n")).toContain("iMessage configured, not enabled yet."); }); @@ -124,7 +139,10 @@ describe("applyPluginAutoEnable", () => { }); expect(result.config.plugins?.entries?.bluebubbles?.enabled).toBeUndefined(); - expect(result.config.plugins?.entries?.imessage?.enabled).toBe(true); + // imessage is a built-in channel, so it goes to channels.imessage.enabled. + expect((result.config.channels as Record)?.imessage).toMatchObject({ + enabled: true, + }); }); it("enables imessage normally when only imessage is configured", () => { @@ -135,7 +153,10 @@ describe("applyPluginAutoEnable", () => { env: {}, }); - expect(result.config.plugins?.entries?.imessage?.enabled).toBe(true); + // imessage is a built-in channel, so it goes to channels.imessage.enabled. + expect((result.config.channels as Record)?.imessage).toMatchObject({ + enabled: true, + }); expect(result.changes.join("\n")).toContain("iMessage configured, not enabled yet."); }); }); diff --git a/src/config/plugin-auto-enable.ts b/src/config/plugin-auto-enable.ts index a7632e41f..a2fe496a2 100644 --- a/src/config/plugin-auto-enable.ts +++ b/src/config/plugin-auto-enable.ts @@ -270,10 +270,28 @@ function resolveConfiguredPlugins( } function isPluginExplicitlyDisabled(cfg: MoltbotConfig, pluginId: string): boolean { + // Built-in channels use channels..enabled, not plugins.entries. + const builtinChannelId = normalizeChatChannelId(pluginId); + if (builtinChannelId) { + const channels = cfg.channels as Record | undefined; + const entry = channels?.[builtinChannelId]; + return isRecord(entry) && entry.enabled === false; + } const entry = cfg.plugins?.entries?.[pluginId]; return entry?.enabled === false; } +function isPluginAlreadyEnabled(cfg: MoltbotConfig, pluginId: string): boolean { + // Built-in channels use channels..enabled, not plugins.entries. + const builtinChannelId = normalizeChatChannelId(pluginId); + if (builtinChannelId) { + const channels = cfg.channels as Record | undefined; + const entry = channels?.[builtinChannelId]; + return isRecord(entry) && entry.enabled === true; + } + return cfg.plugins?.entries?.[pluginId]?.enabled === true; +} + function isPluginDenied(cfg: MoltbotConfig, pluginId: string): boolean { const deny = cfg.plugins?.deny; return Array.isArray(deny) && deny.includes(pluginId); @@ -317,7 +335,29 @@ function ensureAllowlisted(cfg: MoltbotConfig, pluginId: string): MoltbotConfig }; } +function enableBuiltinChannel(cfg: MoltbotConfig, channelId: string): MoltbotConfig { + const channels = cfg.channels as Record | undefined; + const existingEntry = channels?.[channelId]; + const channelEntry = isRecord(existingEntry) ? existingEntry : {}; + return { + ...cfg, + channels: { + ...channels, + [channelId]: { + ...channelEntry, + enabled: true, + }, + }, + }; +} + function enablePluginEntry(cfg: MoltbotConfig, pluginId: string): MoltbotConfig { + // Built-in channels should be enabled via channels..enabled, not plugins.entries. + const builtinChannelId = normalizeChatChannelId(pluginId); + if (builtinChannelId) { + return enableBuiltinChannel(cfg, builtinChannelId); + } + const entries = { ...cfg.plugins?.entries, [pluginId]: { @@ -366,12 +406,24 @@ export function applyPluginAutoEnable(params: { if (isPluginDenied(next, entry.pluginId)) continue; if (isPluginExplicitlyDisabled(next, entry.pluginId)) continue; if (shouldSkipPreferredPluginAutoEnable(next, entry, configured)) continue; - const allow = next.plugins?.allow; - const allowMissing = Array.isArray(allow) && !allow.includes(entry.pluginId); - const alreadyEnabled = next.plugins?.entries?.[entry.pluginId]?.enabled === true; - if (alreadyEnabled && !allowMissing) continue; + + const isBuiltinChannel = normalizeChatChannelId(entry.pluginId) !== null; + const alreadyEnabled = isPluginAlreadyEnabled(next, entry.pluginId); + + // For plugin channels, also check plugins.allow list. + if (!isBuiltinChannel) { + const allow = next.plugins?.allow; + const allowMissing = Array.isArray(allow) && !allow.includes(entry.pluginId); + if (alreadyEnabled && !allowMissing) continue; + } else { + if (alreadyEnabled) continue; + } + next = enablePluginEntry(next, entry.pluginId); - next = ensureAllowlisted(next, entry.pluginId); + // Built-in channels don't need plugins.allow entry. + if (!isBuiltinChannel) { + next = ensureAllowlisted(next, entry.pluginId); + } changes.push(formatAutoEnableChange(entry)); }