From bc457fd1b81d527ea64655fd1397cb7fc17831ea Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 4 Apr 2026 04:52:53 +0100 Subject: [PATCH] refactor(channels): move bootstrap channel logic behind extension seams --- extensions/bluebubbles/src/channel.ts | 2 +- extensions/discord/contract-surfaces.ts | 7 + extensions/irc/contract-surfaces.ts | 8 + extensions/slack/contract-surfaces.ts | 6 + extensions/telegram/contract-api.ts | 11 +- extensions/telegram/contract-surfaces.ts | 7 + extensions/whatsapp/contract-surfaces.ts | 8 + src/channels/chat-meta.ts | 7 +- src/channels/ids.ts | 84 +++++++---- src/channels/model-overrides.ts | 35 +---- src/channels/plugins/contract-surfaces.ts | 139 ++++++++++++++++-- .../channel-import-guardrails.test.ts | 29 ++++ src/config/channel-configured.test.ts | 42 ++++++ src/config/channel-configured.ts | 139 +++--------------- src/config/telegram-command-config.ts | 61 ++++++++ src/config/zod-schema.providers-core.ts | 2 +- src/config/zod-schema.providers.ts | 31 +--- src/infra/outbound/message-action-params.ts | 8 +- src/plugin-sdk/bluebubbles.ts | 99 ++++++++++++- src/plugin-sdk/channel-status.ts | 1 - src/plugin-sdk/command-auth.ts | 33 ++++- src/plugin-sdk/compat.ts | 2 +- src/plugin-sdk/matrix-runtime-heavy.ts | 84 ++++++++++- src/plugin-sdk/telegram-command-config.ts | 4 +- .../contracts/plugin-sdk-subpaths.test.ts | 2 + 25 files changed, 602 insertions(+), 249 deletions(-) create mode 100644 extensions/irc/contract-surfaces.ts create mode 100644 src/config/channel-configured.test.ts create mode 100644 src/config/telegram-command-config.ts diff --git a/extensions/bluebubbles/src/channel.ts b/extensions/bluebubbles/src/channel.ts index 7e3331fe21f..2ddb21b455a 100644 --- a/extensions/bluebubbles/src/channel.ts +++ b/extensions/bluebubbles/src/channel.ts @@ -1,4 +1,5 @@ import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id"; +import { collectBlueBubblesStatusIssues } from "openclaw/plugin-sdk/bluebubbles"; import { createScopedDmSecurityResolver } from "openclaw/plugin-sdk/channel-config-helpers"; import { createAccountStatusSink } from "openclaw/plugin-sdk/channel-lifecycle"; import { createPairingPrefixStripper } from "openclaw/plugin-sdk/channel-pairing"; @@ -9,7 +10,6 @@ import { import { createAttachedChannelResultAdapter } from "openclaw/plugin-sdk/channel-send-result"; import { buildProbeChannelStatusSummary, - collectBlueBubblesStatusIssues, PAIRING_APPROVED_MESSAGE, } from "openclaw/plugin-sdk/channel-status"; import { createChatChannelPlugin } from "openclaw/plugin-sdk/core"; diff --git a/extensions/discord/contract-surfaces.ts b/extensions/discord/contract-surfaces.ts index b271964d1e6..9c7a54a9052 100644 --- a/extensions/discord/contract-surfaces.ts +++ b/extensions/discord/contract-surfaces.ts @@ -8,3 +8,10 @@ export { collectUnsupportedSecretRefConfigCandidates, } from "./src/security-contract.js"; export { deriveLegacySessionChatType } from "./src/session-contract.js"; + +export function hasConfiguredState(params: { env?: NodeJS.ProcessEnv }): boolean { + return ( + typeof params.env?.DISCORD_BOT_TOKEN === "string" && + params.env.DISCORD_BOT_TOKEN.trim().length > 0 + ); +} diff --git a/extensions/irc/contract-surfaces.ts b/extensions/irc/contract-surfaces.ts new file mode 100644 index 00000000000..37854d58028 --- /dev/null +++ b/extensions/irc/contract-surfaces.ts @@ -0,0 +1,8 @@ +export function hasConfiguredState(params: { env?: NodeJS.ProcessEnv }): boolean { + return ( + typeof params.env?.IRC_HOST === "string" && + params.env.IRC_HOST.trim().length > 0 && + typeof params.env?.IRC_NICK === "string" && + params.env.IRC_NICK.trim().length > 0 + ); +} diff --git a/extensions/slack/contract-surfaces.ts b/extensions/slack/contract-surfaces.ts index 274710269c3..94d02e7cd46 100644 --- a/extensions/slack/contract-surfaces.ts +++ b/extensions/slack/contract-surfaces.ts @@ -3,3 +3,9 @@ export { collectRuntimeConfigAssignments, secretTargetRegistryEntries, } from "./src/secret-contract.js"; + +export function hasConfiguredState(params: { env?: NodeJS.ProcessEnv }): boolean { + return ["SLACK_APP_TOKEN", "SLACK_BOT_TOKEN", "SLACK_USER_TOKEN"].some( + (key) => typeof params.env?.[key] === "string" && params.env[key]?.trim().length > 0, + ); +} diff --git a/extensions/telegram/contract-api.ts b/extensions/telegram/contract-api.ts index a1cefdd4130..7bf8b522c73 100644 --- a/extensions/telegram/contract-api.ts +++ b/extensions/telegram/contract-api.ts @@ -3,9 +3,18 @@ export { collectRuntimeConfigAssignments, secretTargetRegistryEntries, } from "./src/secret-contract.js"; +export { + TELEGRAM_COMMAND_NAME_PATTERN, + normalizeTelegramCommandDescription, + normalizeTelegramCommandName, + resolveTelegramCustomCommands, +} from "./src/command-config.js"; export { parseTelegramTopicConversation } from "./src/topic-conversation.js"; export { singleAccountKeysToMove } from "./src/setup-contract.js"; -export { buildTelegramModelsProviderChannelData } from "./src/command-ui.js"; +export { + buildCommandsPaginationKeyboard, + buildTelegramModelsProviderChannelData, +} from "./src/command-ui.js"; export type { TelegramInteractiveHandlerContext, TelegramInteractiveHandlerRegistration, diff --git a/extensions/telegram/contract-surfaces.ts b/extensions/telegram/contract-surfaces.ts index ff4d211e34e..680801f3468 100644 --- a/extensions/telegram/contract-surfaces.ts +++ b/extensions/telegram/contract-surfaces.ts @@ -4,3 +4,10 @@ export { secretTargetRegistryEntries, } from "./src/secret-contract.js"; export { singleAccountKeysToMove } from "./src/setup-contract.js"; + +export function hasConfiguredState(params: { env?: NodeJS.ProcessEnv }): boolean { + return ( + typeof params.env?.TELEGRAM_BOT_TOKEN === "string" && + params.env.TELEGRAM_BOT_TOKEN.trim().length > 0 + ); +} diff --git a/extensions/whatsapp/contract-surfaces.ts b/extensions/whatsapp/contract-surfaces.ts index 8ae1451fb40..76829318e3a 100644 --- a/extensions/whatsapp/contract-surfaces.ts +++ b/extensions/whatsapp/contract-surfaces.ts @@ -3,6 +3,8 @@ type UnsupportedSecretRefConfigCandidate = { value: unknown; }; +import { hasAnyWhatsAppAuth } from "./src/accounts.js"; + function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null; } @@ -14,6 +16,12 @@ export const unsupportedSecretRefSurfacePatterns = [ export { resolveLegacyGroupSessionKey } from "./src/group-session-contract.js"; +export function hasPersistedAuthState(params: { + cfg: import("openclaw/plugin-sdk/config-runtime").OpenClawConfig; +}): boolean { + return hasAnyWhatsAppAuth(params.cfg); +} + export function collectUnsupportedSecretRefConfigCandidates( raw: unknown, ): UnsupportedSecretRefConfigCandidate[] { diff --git a/src/channels/chat-meta.ts b/src/channels/chat-meta.ts index 1be1776520b..209449fb5a8 100644 --- a/src/channels/chat-meta.ts +++ b/src/channels/chat-meta.ts @@ -79,7 +79,7 @@ function buildChatChannelMetaById(): Record { if (!rawId || !CHAT_CHANNEL_ID_SET.has(rawId)) { continue; } - const id = rawId as ChatChannelId; + const id = rawId; entries.set( id, toChatChannelMeta({ @@ -89,11 +89,6 @@ function buildChatChannelMetaById(): Record { ); } - const missingIds = CHAT_CHANNEL_ORDER.filter((id) => !entries.has(id)); - if (missingIds.length > 0) { - throw new Error(`Missing bundled chat channel metadata for: ${missingIds.join(", ")}`); - } - return Object.freeze(Object.fromEntries(entries)) as Record; } diff --git a/src/channels/ids.ts b/src/channels/ids.ts index 6ed1fa383b6..ff00e1f5f4e 100644 --- a/src/channels/ids.ts +++ b/src/channels/ids.ts @@ -1,38 +1,66 @@ -// Keep built-in channel IDs in a leaf module so shared config/sandbox code can -// reference them without importing channel registry helpers that may pull in -// plugin runtime state. -export const CHAT_CHANNEL_ORDER = [ - "telegram", - "whatsapp", - "discord", - "irc", - "googlechat", - "slack", - "signal", - "imessage", - "line", -] as const; +import { listBundledPluginMetadata } from "../plugins/bundled-plugin-metadata.js"; -export type ChatChannelId = (typeof CHAT_CHANNEL_ORDER)[number]; +export type ChatChannelId = string; -export const CHANNEL_IDS = [...CHAT_CHANNEL_ORDER] as const; - -const BUILT_IN_CHAT_CHANNEL_ALIAS_ENTRIES = [ - ["gchat", "googlechat"], - ["google-chat", "googlechat"], - ["imsg", "imessage"], - ["internet-relay-chat", "irc"], -] as const satisfies ReadonlyArray; - -export const CHAT_CHANNEL_ALIASES: Record = Object.freeze( - Object.fromEntries(BUILT_IN_CHAT_CHANNEL_ALIAS_ENTRIES), -) as Record; +type BundledChatChannelEntry = { + id: ChatChannelId; + aliases: readonly string[]; + order: number; +}; function normalizeChannelKey(raw?: string | null): string | undefined { const normalized = raw?.trim().toLowerCase(); return normalized || undefined; } +function listBundledChatChannelEntries(): BundledChatChannelEntry[] { + return listBundledPluginMetadata({ + includeChannelConfigs: false, + includeSyntheticChannelConfigs: false, + }) + .flatMap((entry) => { + const channel = + entry.packageManifest && "channel" in entry.packageManifest + ? entry.packageManifest.channel + : undefined; + const id = normalizeChannelKey(channel?.id); + if (!channel || !id) { + return []; + } + const aliases = (channel.aliases ?? []) + .map((alias) => normalizeChannelKey(alias)) + .filter((alias): alias is string => Boolean(alias)); + return [ + { + id, + aliases, + order: typeof channel.order === "number" ? channel.order : Number.MAX_SAFE_INTEGER, + }, + ]; + }) + .toSorted( + (left, right) => + left.order - right.order || left.id.localeCompare(right.id, "en", { sensitivity: "base" }), + ); +} + +const BUNDLED_CHAT_CHANNEL_ENTRIES = Object.freeze(listBundledChatChannelEntries()); +const CHAT_CHANNEL_ID_SET = new Set(BUNDLED_CHAT_CHANNEL_ENTRIES.map((entry) => entry.id)); + +export const CHAT_CHANNEL_ORDER = Object.freeze( + BUNDLED_CHAT_CHANNEL_ENTRIES.map((entry) => entry.id), +); + +export const CHANNEL_IDS = CHAT_CHANNEL_ORDER; + +export const CHAT_CHANNEL_ALIASES: Record = Object.freeze( + Object.fromEntries( + BUNDLED_CHAT_CHANNEL_ENTRIES.flatMap((entry) => + entry.aliases.map((alias) => [alias, entry.id] as const), + ), + ), +) as Record; + export function listChatChannelAliases(): string[] { return Object.keys(CHAT_CHANNEL_ALIASES); } @@ -43,5 +71,5 @@ export function normalizeChatChannelId(raw?: string | null): ChatChannelId | nul return null; } const resolved = CHAT_CHANNEL_ALIASES[normalized] ?? normalized; - return CHAT_CHANNEL_ORDER.includes(resolved) ? resolved : null; + return CHAT_CHANNEL_ID_SET.has(resolved) ? resolved : null; } diff --git a/src/channels/model-overrides.ts b/src/channels/model-overrides.ts index fbbdce72bfd..629875aec55 100644 --- a/src/channels/model-overrides.ts +++ b/src/channels/model-overrides.ts @@ -1,5 +1,4 @@ import type { OpenClawConfig } from "../config/config.js"; -import { parseFeishuConversationId } from "../plugin-sdk/feishu-conversation.js"; import { normalizeMessageChannel } from "../utils/message-channel.js"; import { buildChannelKeyCandidates, @@ -59,10 +58,6 @@ function buildChannelCandidates( normalizeMessageChannel(params.channel ?? "") ?? params.channel?.trim().toLowerCase(); const groupId = params.groupId?.trim(); const sessionConversation = resolveSessionConversationRef(params.parentSessionKey); - const bundledParentOverrideFallbacks = resolveBundledParentOverrideFallbacks({ - channel: normalizedChannel, - parentConversationId: sessionConversation?.rawId, - }); const parentOverrideFallbacks = (normalizedChannel ? getChannelPlugin( @@ -70,7 +65,7 @@ function buildChannelCandidates( )?.conversationBindings?.buildModelOverrideParentCandidates?.({ parentConversationId: sessionConversation?.rawId, }) - : null) ?? bundledParentOverrideFallbacks; + : null) ?? []; const groupConversationKind = normalizeChatType(params.groupChatType ?? undefined) === "channel" ? "channel" @@ -108,34 +103,6 @@ function buildChannelCandidates( }; } -function resolveBundledParentOverrideFallbacks(params: { - channel?: string | null; - parentConversationId?: string | null; -}): string[] { - if (params.channel !== "feishu") { - return []; - } - const parsed = parseFeishuConversationId({ - conversationId: params.parentConversationId ?? "", - }); - if (!parsed) { - return []; - } - switch (parsed.scope) { - case "group_topic_sender": - return buildChannelKeyCandidates( - parsed.topicId ? `${parsed.chatId}:topic:${parsed.topicId}` : undefined, - parsed.chatId, - ); - case "group_topic": - case "group_sender": - return buildChannelKeyCandidates(parsed.chatId); - case "group": - default: - return []; - } -} - export function resolveChannelModelOverride( params: ChannelModelOverrideParams, ): ChannelModelOverride | null { diff --git a/src/channels/plugins/contract-surfaces.ts b/src/channels/plugins/contract-surfaces.ts index 18d65cdb0e9..0c102c01825 100644 --- a/src/channels/plugins/contract-surfaces.ts +++ b/src/channels/plugins/contract-surfaces.ts @@ -1,11 +1,13 @@ import fs from "node:fs"; import path from "node:path"; +import { fileURLToPath } from "node:url"; import { createJiti } from "jiti"; import { discoverOpenClawPlugins } from "../../plugins/discovery.js"; import { loadPluginManifestRegistry } from "../../plugins/manifest-registry.js"; import { buildPluginLoaderAliasMap, buildPluginLoaderJitiOptions, + resolveLoaderPackageRoot, shouldPreferNativeJiti, } from "../../plugins/sdk-alias.js"; @@ -15,12 +17,25 @@ const CONTRACT_SURFACE_BASENAMES = [ "contract-api.ts", "contract-api.js", ] as const; +const PUBLIC_SURFACE_SOURCE_EXTENSIONS = [".ts", ".mts", ".js", ".mjs", ".cts", ".cjs"] as const; +const OPENCLAW_PACKAGE_ROOT = + resolveLoaderPackageRoot({ + modulePath: fileURLToPath(import.meta.url), + moduleUrl: import.meta.url, + }) ?? fileURLToPath(new URL("../../..", import.meta.url)); +const CURRENT_MODULE_PATH = fileURLToPath(import.meta.url); +const RUNNING_FROM_BUILT_ARTIFACT = + CURRENT_MODULE_PATH.includes(`${path.sep}dist${path.sep}`) || + CURRENT_MODULE_PATH.includes(`${path.sep}dist-runtime${path.sep}`); + +type ContractSurfaceBasename = (typeof CONTRACT_SURFACE_BASENAMES)[number]; let cachedSurfaces: unknown[] | null = null; let cachedSurfaceEntries: Array<{ pluginId: string; surface: unknown; }> | null = null; +const cachedPreferredSurfaceModules = new Map(); function createModuleLoader() { const jitiLoaders = new Map>(); @@ -46,28 +61,87 @@ function createModuleLoader() { const loadModule = createModuleLoader(); -function resolveContractSurfaceModulePaths(rootDir: string | undefined): string[] { +function matchesPreferredBasename( + basename: ContractSurfaceBasename, + preferredBasename: ContractSurfaceBasename | undefined, +): boolean { + if (!preferredBasename) { + return true; + } + return basename.replace(/\.[^.]+$/u, "") === preferredBasename.replace(/\.[^.]+$/u, ""); +} + +function resolveDistPreferredModulePath(modulePath: string): string { + const compiledDistModulePath = modulePath.replace( + `${path.sep}dist-runtime${path.sep}`, + `${path.sep}dist${path.sep}`, + ); + return compiledDistModulePath !== modulePath && fs.existsSync(compiledDistModulePath) + ? compiledDistModulePath + : modulePath; +} + +function resolveContractSurfaceModulePaths( + rootDir: string | undefined, + preferredBasename?: ContractSurfaceBasename, +): string[] { if (typeof rootDir !== "string" || rootDir.length === 0) { return []; } const modulePaths: string[] = []; for (const basename of CONTRACT_SURFACE_BASENAMES) { + if (!matchesPreferredBasename(basename, preferredBasename)) { + continue; + } const modulePath = path.join(rootDir, basename); if (!fs.existsSync(modulePath)) { continue; } - const compiledDistModulePath = modulePath.replace( - `${path.sep}dist-runtime${path.sep}`, - `${path.sep}dist${path.sep}`, - ); - // Prefer the compiled dist module over the dist-runtime shim so Jiti sees - // the full named export surface instead of only local wrapper exports. - if (compiledDistModulePath !== modulePath && fs.existsSync(compiledDistModulePath)) { - modulePaths.push(compiledDistModulePath); + modulePaths.push(resolveDistPreferredModulePath(modulePath)); + } + return modulePaths; +} + +function resolveSourceFirstContractSurfaceModulePaths(params: { + rootDir: string | undefined; + preferredBasename?: ContractSurfaceBasename; +}): string[] { + if (typeof params.rootDir !== "string" || params.rootDir.length === 0) { + return []; + } + if (RUNNING_FROM_BUILT_ARTIFACT) { + return resolveContractSurfaceModulePaths(params.rootDir, params.preferredBasename); + } + + const dirName = path.basename(path.resolve(params.rootDir)); + const sourceRoot = path.resolve(OPENCLAW_PACKAGE_ROOT, "extensions", dirName); + const modulePaths: string[] = []; + + for (const basename of CONTRACT_SURFACE_BASENAMES) { + if (!matchesPreferredBasename(basename, params.preferredBasename)) { continue; } - modulePaths.push(modulePath); + + const sourceBaseName = basename.replace(/\.[^.]+$/u, ""); + let sourceCandidatePath: string | null = null; + for (const ext of PUBLIC_SURFACE_SOURCE_EXTENSIONS) { + const candidate = path.join(sourceRoot, `${sourceBaseName}${ext}`); + if (fs.existsSync(candidate)) { + sourceCandidatePath = candidate; + break; + } + } + if (sourceCandidatePath) { + modulePaths.push(sourceCandidatePath); + continue; + } + + const builtCandidates = resolveContractSurfaceModulePaths(params.rootDir, basename); + if (builtCandidates[0]) { + modulePaths.push(builtCandidates[0]); + } } + return modulePaths; } @@ -91,7 +165,9 @@ function loadBundledChannelContractSurfaceEntries(): Array<{ if (manifest.origin !== "bundled" || manifest.channels.length === 0) { continue; } - const modulePaths = resolveContractSurfaceModulePaths(manifest.rootDir); + const modulePaths = resolveSourceFirstContractSurfaceModulePaths({ + rootDir: manifest.rootDir, + }); if (modulePaths.length === 0) { continue; } @@ -123,3 +199,44 @@ export function getBundledChannelContractSurfaceEntries(): Array<{ cachedSurfaceEntries ??= loadBundledChannelContractSurfaceEntries(); return cachedSurfaceEntries; } + +export function getBundledChannelContractSurfaceModule(params: { + pluginId: string; + preferredBasename?: ContractSurfaceBasename; +}): T | null { + const cacheKey = `${params.pluginId}:${params.preferredBasename ?? "*"}`; + if (cachedPreferredSurfaceModules.has(cacheKey)) { + return (cachedPreferredSurfaceModules.get(cacheKey) ?? null) as T | null; + } + const discovery = discoverOpenClawPlugins({ cache: false }); + const manifestRegistry = loadPluginManifestRegistry({ + cache: false, + config: {}, + candidates: discovery.candidates, + diagnostics: discovery.diagnostics, + }); + const manifest = manifestRegistry.plugins.find( + (entry) => + entry.origin === "bundled" && entry.channels.length > 0 && entry.id === params.pluginId, + ); + if (!manifest) { + cachedPreferredSurfaceModules.set(cacheKey, null); + return null; + } + const modulePath = resolveSourceFirstContractSurfaceModulePaths({ + rootDir: manifest.rootDir, + preferredBasename: params.preferredBasename, + })[0]; + if (!modulePath) { + cachedPreferredSurfaceModules.set(cacheKey, null); + return null; + } + try { + const module = loadModule(modulePath)(modulePath) as T; + cachedPreferredSurfaceModules.set(cacheKey, module); + return module; + } catch { + cachedPreferredSurfaceModules.set(cacheKey, null); + return null; + } +} diff --git a/src/channels/plugins/contracts/channel-import-guardrails.test.ts b/src/channels/plugins/contracts/channel-import-guardrails.test.ts index 4c0471d7edb..b192b4d95f5 100644 --- a/src/channels/plugins/contracts/channel-import-guardrails.test.ts +++ b/src/channels/plugins/contracts/channel-import-guardrails.test.ts @@ -474,6 +474,26 @@ function expectNoCrossPluginSdkFacadeImports(file: string, imports: string[]): v } } +function expectCoreSourceStaysOffPluginSpecificSdkFacades(file: string, imports: string[]): void { + for (const specifier of imports) { + if (!specifier.includes("/plugin-sdk/")) { + continue; + } + const targetSubpath = specifier.split("/plugin-sdk/")[1]?.replace(/\.[cm]?[jt]sx?$/u, "") ?? ""; + const targetExtensionId = + BUNDLED_EXTENSION_IDS.find( + (extensionId) => + targetSubpath === extensionId || targetSubpath.startsWith(`${extensionId}-`), + ) ?? null; + if (!targetExtensionId) { + continue; + } + expect.fail( + `${file} should not import plugin-specific SDK facades (${specifier}) from core production code. Use a neutral contract surface or plugin hook instead.`, + ); + } +} + describe("channel import guardrails", () => { it("keeps channel helper modules off their own SDK barrels", () => { for (const source of SAME_CHANNEL_SDK_GUARDS) { @@ -553,6 +573,15 @@ describe("channel import guardrails", () => { } }); + it("keeps core production files off plugin-specific sdk facades", () => { + for (const file of collectCoreSourceFiles()) { + expectCoreSourceStaysOffPluginSpecificSdkFacades( + file, + getSourceAnalysis(file).importSpecifiers, + ); + } + }); + it("keeps extension-to-extension imports limited to approved public surfaces", () => { for (const file of collectExtensionSourceFiles()) { expectOnlyApprovedExtensionSeams(file, getSourceAnalysis(file).extensionImports); diff --git a/src/config/channel-configured.test.ts b/src/config/channel-configured.test.ts new file mode 100644 index 00000000000..719a506a8ff --- /dev/null +++ b/src/config/channel-configured.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from "vitest"; +import { isChannelConfigured } from "./channel-configured.js"; + +describe("isChannelConfigured", () => { + it("detects Telegram env configuration through the channel plugin seam", () => { + expect(isChannelConfigured({}, "telegram", { TELEGRAM_BOT_TOKEN: "token" })).toBe(true); + }); + + it("detects Discord env configuration through the channel plugin seam", () => { + expect(isChannelConfigured({}, "discord", { DISCORD_BOT_TOKEN: "token" })).toBe(true); + }); + + it("detects Slack env configuration through the channel plugin seam", () => { + expect(isChannelConfigured({}, "slack", { SLACK_BOT_TOKEN: "xoxb-test" })).toBe(true); + }); + + it("requires both IRC host and nick env vars through the channel plugin seam", () => { + expect(isChannelConfigured({}, "irc", { IRC_HOST: "irc.example.com" })).toBe(false); + expect( + isChannelConfigured({}, "irc", { + IRC_HOST: "irc.example.com", + IRC_NICK: "openclaw", + }), + ).toBe(true); + }); + + it("still falls back to generic config presence for channels without a custom hook", () => { + expect( + isChannelConfigured( + { + channels: { + signal: { + httpPort: 8080, + }, + }, + }, + "signal", + {}, + ), + ).toBe(true); + }); +}); diff --git a/src/config/channel-configured.ts b/src/config/channel-configured.ts index 38d1088c7d5..08a1d980fc0 100644 --- a/src/config/channel-configured.ts +++ b/src/config/channel-configured.ts @@ -1,28 +1,12 @@ import { hasMeaningfulChannelConfig } from "../channels/config-presence.js"; -import { getChannelPlugin } from "../channels/plugins/index.js"; +import { getBundledChannelContractSurfaceModule } from "../channels/plugins/contract-surfaces.js"; import { isRecord } from "../utils.js"; import type { OpenClawConfig } from "./config.js"; -function hasNonEmptyString(value: unknown): boolean { - return typeof value === "string" && value.trim().length > 0; -} - -function accountsHaveKeys(value: unknown, keys: readonly string[]): boolean { - if (!isRecord(value)) { - return false; - } - for (const account of Object.values(value)) { - if (!isRecord(account)) { - continue; - } - for (const key of keys) { - if (hasNonEmptyString(account[key])) { - return true; - } - } - } - return false; -} +type ChannelConfiguredSurface = { + hasConfiguredState?: (params: { cfg: OpenClawConfig; env?: NodeJS.ProcessEnv }) => boolean; + hasPersistedAuthState?: (params: { cfg: OpenClawConfig; env?: NodeJS.ProcessEnv }) => boolean; +}; function resolveChannelConfig( cfg: OpenClawConfig, @@ -33,120 +17,31 @@ function resolveChannelConfig( return isRecord(entry) ? entry : null; } -type StructuredChannelConfigSpec = { - envAny?: readonly string[]; - envAll?: readonly string[]; - stringKeys?: readonly string[]; - numberKeys?: readonly string[]; - accountStringKeys?: readonly string[]; -}; - -const STRUCTURED_CHANNEL_CONFIG_SPECS: Record = { - telegram: { - envAny: ["TELEGRAM_BOT_TOKEN"], - stringKeys: ["botToken", "tokenFile"], - accountStringKeys: ["botToken", "tokenFile"], - }, - discord: { - envAny: ["DISCORD_BOT_TOKEN"], - stringKeys: ["token"], - accountStringKeys: ["token"], - }, - irc: { - envAll: ["IRC_HOST", "IRC_NICK"], - stringKeys: ["host", "nick"], - accountStringKeys: ["host", "nick"], - }, - slack: { - envAny: ["SLACK_BOT_TOKEN", "SLACK_APP_TOKEN", "SLACK_USER_TOKEN"], - stringKeys: ["botToken", "appToken", "userToken"], - accountStringKeys: ["botToken", "appToken", "userToken"], - }, - signal: { - stringKeys: ["account", "httpUrl", "httpHost", "cliPath"], - numberKeys: ["httpPort"], - accountStringKeys: ["account", "httpUrl", "httpHost", "cliPath"], - }, - imessage: { - stringKeys: ["cliPath"], - }, -}; - -function envHasAnyKeys(env: NodeJS.ProcessEnv, keys: readonly string[]): boolean { - for (const key of keys) { - if (hasNonEmptyString(env[key])) { - return true; - } - } - return false; -} - -function envHasAllKeys(env: NodeJS.ProcessEnv, keys: readonly string[]): boolean { - for (const key of keys) { - if (!hasNonEmptyString(env[key])) { - return false; - } - } - return keys.length > 0; -} - -function hasAnyNumberKeys(entry: Record, keys: readonly string[]): boolean { - for (const key of keys) { - if (typeof entry[key] === "number") { - return true; - } - } - return false; -} - -function isStructuredChannelConfigured( - cfg: OpenClawConfig, - channelId: string, - env: NodeJS.ProcessEnv, - spec: StructuredChannelConfigSpec, -): boolean { - if (spec.envAny && envHasAnyKeys(env, spec.envAny)) { - return true; - } - if (spec.envAll && envHasAllKeys(env, spec.envAll)) { - return true; - } - const entry = resolveChannelConfig(cfg, channelId); - if (!entry) { - return false; - } - if (spec.stringKeys && spec.stringKeys.some((key) => hasNonEmptyString(entry[key]))) { - return true; - } - if (spec.numberKeys && hasAnyNumberKeys(entry, spec.numberKeys)) { - return true; - } - if (spec.accountStringKeys && accountsHaveKeys(entry.accounts, spec.accountStringKeys)) { - return true; - } - return hasMeaningfulChannelConfig(entry); -} - function isGenericChannelConfigured(cfg: OpenClawConfig, channelId: string): boolean { const entry = resolveChannelConfig(cfg, channelId); return hasMeaningfulChannelConfig(entry); } +function getChannelConfiguredSurface(channelId: string): ChannelConfiguredSurface | null { + return getBundledChannelContractSurfaceModule({ + pluginId: channelId, + preferredBasename: "contract-surfaces.ts", + }); +} + export function isChannelConfigured( cfg: OpenClawConfig, channelId: string, env: NodeJS.ProcessEnv = process.env, ): boolean { - const pluginConfigured = getChannelPlugin(channelId)?.config.hasPersistedAuthState?.({ - cfg, - env, - }); + const surface = getChannelConfiguredSurface(channelId); + const pluginConfigured = surface?.hasConfiguredState?.({ cfg, env }); if (pluginConfigured) { return true; } - const spec = STRUCTURED_CHANNEL_CONFIG_SPECS[channelId]; - if (spec) { - return isStructuredChannelConfigured(cfg, channelId, env, spec); + const pluginPersistedAuthState = surface?.hasPersistedAuthState?.({ cfg, env }); + if (pluginPersistedAuthState) { + return true; } return isGenericChannelConfigured(cfg, channelId); } diff --git a/src/config/telegram-command-config.ts b/src/config/telegram-command-config.ts new file mode 100644 index 00000000000..e523dadded6 --- /dev/null +++ b/src/config/telegram-command-config.ts @@ -0,0 +1,61 @@ +import { getBundledChannelContractSurfaceModule } from "../channels/plugins/contract-surfaces.js"; + +export type TelegramCustomCommandInput = { + command?: string | null; + description?: string | null; +}; + +export type TelegramCustomCommandIssue = { + index: number; + field: "command" | "description"; + message: string; +}; + +type TelegramCommandConfigContract = { + TELEGRAM_COMMAND_NAME_PATTERN: RegExp; + normalizeTelegramCommandName: (value: string) => string; + normalizeTelegramCommandDescription: (value: string) => string; + resolveTelegramCustomCommands: (params: { + commands?: TelegramCustomCommandInput[] | null; + reservedCommands?: Set; + checkReserved?: boolean; + checkDuplicates?: boolean; + }) => { + commands: Array<{ command: string; description: string }>; + issues: TelegramCustomCommandIssue[]; + }; +}; + +function loadTelegramCommandConfigContract(): TelegramCommandConfigContract { + const contract = getBundledChannelContractSurfaceModule({ + pluginId: "telegram", + preferredBasename: "contract-api.ts", + }); + if (!contract) { + throw new Error("telegram command config contract surface is unavailable"); + } + return contract; +} + +export const TELEGRAM_COMMAND_NAME_PATTERN = + loadTelegramCommandConfigContract().TELEGRAM_COMMAND_NAME_PATTERN; + +export function normalizeTelegramCommandName(value: string): string { + return loadTelegramCommandConfigContract().normalizeTelegramCommandName(value); +} + +export function normalizeTelegramCommandDescription(value: string): string { + return loadTelegramCommandConfigContract().normalizeTelegramCommandDescription(value); +} + +export function resolveTelegramCustomCommands(params: { + commands?: TelegramCustomCommandInput[] | null; + reservedCommands?: Set; + checkReserved?: boolean; + checkDuplicates?: boolean; +}): { + commands: Array<{ command: string; description: string }>; + issues: TelegramCustomCommandIssue[]; +} { + return loadTelegramCommandConfigContract().resolveTelegramCustomCommands(params); +} diff --git a/src/config/zod-schema.providers-core.ts b/src/config/zod-schema.providers-core.ts index 032062f493a..4931049a062 100644 --- a/src/config/zod-schema.providers-core.ts +++ b/src/config/zod-schema.providers-core.ts @@ -5,7 +5,7 @@ import { normalizeTelegramCommandDescription, normalizeTelegramCommandName, resolveTelegramCustomCommands, -} from "../plugin-sdk/telegram-command-config.js"; +} from "./telegram-command-config.js"; import { ToolPolicySchema } from "./zod-schema.agent-runtime.js"; import { ChannelHealthMonitorSchema, diff --git a/src/config/zod-schema.providers.ts b/src/config/zod-schema.providers.ts index 8b486e7f560..0113120cff5 100644 --- a/src/config/zod-schema.providers.ts +++ b/src/config/zod-schema.providers.ts @@ -1,19 +1,8 @@ import { z } from "zod"; +import { getBundledChannelRuntimeMap } from "./bundled-channel-config-runtime.js"; import type { ChannelsConfig } from "./types.channels.js"; import { ChannelHeartbeatVisibilitySchema } from "./zod-schema.channels.js"; import { ContextVisibilityModeSchema, GroupPolicySchema } from "./zod-schema.core.js"; -import { - BlueBubblesConfigSchema, - DiscordConfigSchema, - GoogleChatConfigSchema, - IMessageConfigSchema, - IrcConfigSchema, - MSTeamsConfigSchema, - SignalConfigSchema, - SlackConfigSchema, - TelegramConfigSchema, -} from "./zod-schema.providers-core.js"; -import { WhatsAppConfigSchema } from "./zod-schema.providers-whatsapp.js"; export * from "./zod-schema.providers-core.js"; export * from "./zod-schema.providers-whatsapp.js"; @@ -23,21 +12,7 @@ const ChannelModelByChannelSchema = z .record(z.string(), z.record(z.string(), z.string())) .optional(); -const directChannelRuntimeSchemas = new Map< - string, - { safeParse: (value: unknown) => ReturnType } ->([ - ["bluebubbles", { safeParse: (value) => BlueBubblesConfigSchema.safeParse(value) }], - ["discord", { safeParse: (value) => DiscordConfigSchema.safeParse(value) }], - ["googlechat", { safeParse: (value) => GoogleChatConfigSchema.safeParse(value) }], - ["imessage", { safeParse: (value) => IMessageConfigSchema.safeParse(value) }], - ["irc", { safeParse: (value) => IrcConfigSchema.safeParse(value) }], - ["msteams", { safeParse: (value) => MSTeamsConfigSchema.safeParse(value) }], - ["signal", { safeParse: (value) => SignalConfigSchema.safeParse(value) }], - ["slack", { safeParse: (value) => SlackConfigSchema.safeParse(value) }], - ["telegram", { safeParse: (value) => TelegramConfigSchema.safeParse(value) }], - ["whatsapp", { safeParse: (value) => WhatsAppConfigSchema.safeParse(value) }], -]); +const directChannelRuntimeSchemas = getBundledChannelRuntimeMap(); function addLegacyChannelAcpBindingIssues( value: unknown, @@ -86,7 +61,7 @@ function normalizeBundledChannelConfigs( } const parsed = runtimeSchema.safeParse(value[channelId]); if (!parsed.success) { - for (const issue of parsed.error.issues) { + for (const issue of parsed.issues) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: issue.message ?? `Invalid channels.${channelId} config.`, diff --git a/src/infra/outbound/message-action-params.ts b/src/infra/outbound/message-action-params.ts index 707f553f396..e4c036bab63 100644 --- a/src/infra/outbound/message-action-params.ts +++ b/src/infra/outbound/message-action-params.ts @@ -311,12 +311,11 @@ export async function hydrateAttachmentParamsForAction(params: { dryRun?: boolean; mediaPolicy: AttachmentMediaPolicy; }): Promise { - const shouldHydrateBlueBubblesUploadFile = - params.action === "upload-file" && params.channel === "bluebubbles"; + const shouldHydrateUploadFile = params.action === "upload-file"; if ( params.action !== "sendAttachment" && params.action !== "setGroupIcon" && - !shouldHydrateBlueBubblesUploadFile + !shouldHydrateUploadFile ) { return; } @@ -327,8 +326,7 @@ export async function hydrateAttachmentParamsForAction(params: { args: params.args, dryRun: params.dryRun, mediaPolicy: params.mediaPolicy, - allowMessageCaptionFallback: - params.action === "sendAttachment" || shouldHydrateBlueBubblesUploadFile, + allowMessageCaptionFallback: params.action === "sendAttachment" || shouldHydrateUploadFile, }); } diff --git a/src/plugin-sdk/bluebubbles.ts b/src/plugin-sdk/bluebubbles.ts index 8cf6cebbd34..d95cc04379e 100644 --- a/src/plugin-sdk/bluebubbles.ts +++ b/src/plugin-sdk/bluebubbles.ts @@ -1,3 +1,5 @@ +import type { ChannelAccountSnapshot } from "../channels/plugins/types.core.js"; +import type { ChannelStatusIssue } from "../channels/plugins/types.js"; import type { OpenClawConfig } from "../config/config.js"; import { parseChatTargetPrefixesOrThrow, @@ -5,6 +7,7 @@ import { type ParsedChatTarget, } from "./channel-targets.js"; import { loadBundledPluginPublicSurfaceModuleSync } from "./facade-runtime.js"; +import { asString, collectIssuesForEnabledAccounts, isRecord } from "./status-helpers.js"; // Narrow plugin-sdk surface for the bundled BlueBubbles plugin. // Keep this list additive and scoped to the conversation-binding seam only. @@ -263,6 +266,101 @@ export function resolveBlueBubblesConversationIdFromTarget(target: string): stri return normalizeBlueBubblesAcpConversationId(target)?.conversationId; } +type BlueBubblesAccountStatus = { + accountId?: unknown; + enabled?: unknown; + configured?: unknown; + running?: unknown; + baseUrl?: unknown; + lastError?: unknown; + probe?: unknown; +}; + +type BlueBubblesProbeResult = { + ok?: boolean; + status?: number | null; + error?: string | null; +}; + +function readBlueBubblesAccountStatus( + value: ChannelAccountSnapshot, +): BlueBubblesAccountStatus | null { + if (!isRecord(value)) { + return null; + } + return { + accountId: value.accountId, + enabled: value.enabled, + configured: value.configured, + running: value.running, + baseUrl: value.baseUrl, + lastError: value.lastError, + probe: value.probe, + }; +} + +function readBlueBubblesProbeResult(value: unknown): BlueBubblesProbeResult | null { + if (!isRecord(value)) { + return null; + } + return { + ok: typeof value.ok === "boolean" ? value.ok : undefined, + status: typeof value.status === "number" ? value.status : null, + error: asString(value.error) ?? null, + }; +} + +export function collectBlueBubblesStatusIssues( + accounts: ChannelAccountSnapshot[], +): ChannelStatusIssue[] { + return collectIssuesForEnabledAccounts({ + accounts, + readAccount: readBlueBubblesAccountStatus, + collectIssues: ({ account, accountId, issues }) => { + const configured = account.configured === true; + const running = account.running === true; + const lastError = asString(account.lastError); + const probe = readBlueBubblesProbeResult(account.probe); + + if (!configured) { + issues.push({ + channel: "bluebubbles", + accountId, + kind: "config", + message: "Not configured (missing serverUrl or password).", + fix: "Run: openclaw channels add bluebubbles --http-url --password ", + }); + return; + } + + if (probe && probe.ok === false) { + const errorDetail = probe.error + ? `: ${probe.error}` + : probe.status + ? ` (HTTP ${probe.status})` + : ""; + issues.push({ + channel: "bluebubbles", + accountId, + kind: "runtime", + message: `BlueBubbles server unreachable${errorDetail}`, + fix: "Check that the BlueBubbles server is running and accessible. Verify serverUrl and password in your config.", + }); + } + + if (running && lastError) { + issues.push({ + channel: "bluebubbles", + accountId, + kind: "runtime", + message: `Channel error: ${lastError}`, + fix: "Check gateway logs for details. If the webhook is failing, verify the webhook URL is configured in BlueBubbles server settings.", + }); + } + }, + }); +} + export { resolveAckReaction } from "../agents/identity.js"; export { createActionGate, @@ -305,7 +403,6 @@ export { patchScopedAccountConfig, } from "../channels/plugins/setup-helpers.js"; export { createAccountListHelpers } from "../channels/plugins/account-helpers.js"; -export { collectBlueBubblesStatusIssues } from "../channels/plugins/status-issues/bluebubbles.js"; export type { BaseProbeResult, ChannelAccountSnapshot, diff --git a/src/plugin-sdk/channel-status.ts b/src/plugin-sdk/channel-status.ts index 12a22755944..5d4e83df8e6 100644 --- a/src/plugin-sdk/channel-status.ts +++ b/src/plugin-sdk/channel-status.ts @@ -1,5 +1,4 @@ export { PAIRING_APPROVED_MESSAGE } from "../channels/plugins/pairing-message.js"; -export { collectBlueBubblesStatusIssues } from "../channels/plugins/status-issues/bluebubbles.js"; export { projectCredentialSnapshotFields, resolveConfiguredFromCredentialStatuses, diff --git a/src/plugin-sdk/command-auth.ts b/src/plugin-sdk/command-auth.ts index c56953ea071..3b57057d88d 100644 --- a/src/plugin-sdk/command-auth.ts +++ b/src/plugin-sdk/command-auth.ts @@ -1,6 +1,6 @@ +import { getBundledChannelContractSurfaceModule } from "../channels/plugins/contract-surfaces.js"; import type { OpenClawConfig } from "../config/config.js"; import { resolveDmGroupAccessWithLists } from "../security/dm-policy-shared.js"; -export { buildCommandsPaginationKeyboard } from "../../extensions/telegram/api.js"; export { createPreCryptoDirectDmAuthorizer, resolveInboundDirectDmAccessWithRuntime, @@ -86,6 +86,37 @@ export { buildHelpMessage, } from "../auto-reply/status.js"; +type TelegramCommandUiContract = { + buildCommandsPaginationKeyboard: ( + currentPage: number, + totalPages: number, + agentId?: string, + ) => Array>; +}; + +function loadTelegramCommandUiContract(): TelegramCommandUiContract { + const contract = getBundledChannelContractSurfaceModule({ + pluginId: "telegram", + preferredBasename: "contract-api.ts", + }); + if (!contract) { + throw new Error("telegram command ui contract surface is unavailable"); + } + return contract; +} + +export function buildCommandsPaginationKeyboard( + currentPage: number, + totalPages: number, + agentId?: string, +): Array> { + return loadTelegramCommandUiContract().buildCommandsPaginationKeyboard( + currentPage, + totalPages, + agentId, + ); +} + export type ResolveSenderCommandAuthorizationParams = { cfg: OpenClawConfig; rawBody: string; diff --git a/src/plugin-sdk/compat.ts b/src/plugin-sdk/compat.ts index cb4d35baf6a..06bc400d4ba 100644 --- a/src/plugin-sdk/compat.ts +++ b/src/plugin-sdk/compat.ts @@ -49,4 +49,4 @@ export { resolveBlueBubblesGroupRequireMention, resolveBlueBubblesGroupToolPolicy, } from "./bluebubbles-policy.js"; -export { collectBlueBubblesStatusIssues } from "../channels/plugins/status-issues/bluebubbles.js"; +export { collectBlueBubblesStatusIssues } from "./bluebubbles.js"; diff --git a/src/plugin-sdk/matrix-runtime-heavy.ts b/src/plugin-sdk/matrix-runtime-heavy.ts index 98259b70eeb..162c30c3793 100644 --- a/src/plugin-sdk/matrix-runtime-heavy.ts +++ b/src/plugin-sdk/matrix-runtime-heavy.ts @@ -1,13 +1,83 @@ +import type { OpenClawConfig } from "./config-runtime.js"; import { loadBundledPluginPublicSurfaceModuleSync } from "./facade-runtime.js"; +type MatrixLegacyLog = { + info?: (message: string) => void; + warn?: (message: string) => void; +}; + +type MatrixLegacyCryptoPlan = { + accountId: string; + rootDir: string; + recoveryKeyPath: string; + statePath: string; + legacyCryptoPath: string; + homeserver: string; + userId: string; + accessToken: string; + deviceId: string | null; +}; + +type MatrixLegacyCryptoDetection = { + plans: MatrixLegacyCryptoPlan[]; + warnings: string[]; +}; + +type MatrixLegacyMigrationResult = { + migrated: boolean; + changes: string[]; + warnings: string[]; +}; + +type MatrixLegacyStatePlan = { + accountId: string; + legacyStoragePath: string; + legacyCryptoPath: string; + targetRootDir: string; + targetStoragePath: string; + targetCryptoPath: string; + selectionNote?: string; +}; + +type MatrixLegacyStateDetection = MatrixLegacyStatePlan | { warning: string } | null; + +type MatrixMigrationSnapshotResult = { + created: boolean; + archivePath: string; + markerPath: string; +}; + type MatrixRuntimeHeavyModule = { - autoPrepareLegacyMatrixCrypto: (typeof import("../../extensions/matrix/src/runtime-heavy-api.js"))["autoPrepareLegacyMatrixCrypto"]; - detectLegacyMatrixCrypto: (typeof import("../../extensions/matrix/src/runtime-heavy-api.js"))["detectLegacyMatrixCrypto"]; - autoMigrateLegacyMatrixState: (typeof import("../../extensions/matrix/src/runtime-heavy-api.js"))["autoMigrateLegacyMatrixState"]; - detectLegacyMatrixState: (typeof import("../../extensions/matrix/src/runtime-heavy-api.js"))["detectLegacyMatrixState"]; - hasActionableMatrixMigration: (typeof import("../../extensions/matrix/src/runtime-heavy-api.js"))["hasActionableMatrixMigration"]; - hasPendingMatrixMigration: (typeof import("../../extensions/matrix/src/runtime-heavy-api.js"))["hasPendingMatrixMigration"]; - maybeCreateMatrixMigrationSnapshot: (typeof import("../../extensions/matrix/src/runtime-heavy-api.js"))["maybeCreateMatrixMigrationSnapshot"]; + autoPrepareLegacyMatrixCrypto: (params: { + cfg: OpenClawConfig; + env?: NodeJS.ProcessEnv; + log?: MatrixLegacyLog; + deps?: Partial>; + }) => Promise; + detectLegacyMatrixCrypto: (params: { + cfg: OpenClawConfig; + env?: NodeJS.ProcessEnv; + }) => MatrixLegacyCryptoDetection; + autoMigrateLegacyMatrixState: (params: { + cfg: OpenClawConfig; + env?: NodeJS.ProcessEnv; + log?: MatrixLegacyLog; + }) => Promise; + detectLegacyMatrixState: (params: { + cfg: OpenClawConfig; + env?: NodeJS.ProcessEnv; + }) => MatrixLegacyStateDetection; + hasActionableMatrixMigration: (params: { + cfg: OpenClawConfig; + env?: NodeJS.ProcessEnv; + }) => boolean; + hasPendingMatrixMigration: (params: { cfg: OpenClawConfig; env?: NodeJS.ProcessEnv }) => boolean; + maybeCreateMatrixMigrationSnapshot: (params: { + trigger: string; + env?: NodeJS.ProcessEnv; + outputDir?: string; + log?: MatrixLegacyLog; + }) => Promise; }; function loadFacadeModule(): MatrixRuntimeHeavyModule { diff --git a/src/plugin-sdk/telegram-command-config.ts b/src/plugin-sdk/telegram-command-config.ts index c5a32b686f8..bad29b2ced2 100644 --- a/src/plugin-sdk/telegram-command-config.ts +++ b/src/plugin-sdk/telegram-command-config.ts @@ -3,4 +3,6 @@ export { normalizeTelegramCommandDescription, normalizeTelegramCommandName, resolveTelegramCustomCommands, -} from "../../extensions/telegram/src/command-config.js"; + type TelegramCustomCommandInput, + type TelegramCustomCommandIssue, +} from "../config/telegram-command-config.js"; diff --git a/src/plugins/contracts/plugin-sdk-subpaths.test.ts b/src/plugins/contracts/plugin-sdk-subpaths.test.ts index 68e7c5ba9f6..3e6b3116233 100644 --- a/src/plugins/contracts/plugin-sdk-subpaths.test.ts +++ b/src/plugins/contracts/plugin-sdk-subpaths.test.ts @@ -607,6 +607,8 @@ describe("plugin-sdk subpath exports", () => { "shouldComputeCommandAuthorized", "shouldHandleTextCommands", ]); + expectSourceOmitsSnippet("command-auth", "../../extensions/"); + expectSourceOmitsSnippet("matrix-runtime-heavy", "../../extensions/"); expectSourceMentions("channel-send-result", [ "attachChannelToResult", "buildChannelSendResult",