diff --git a/extensions/bluebubbles/index.ts b/extensions/bluebubbles/index.ts index eb2ac20c3d7..f37faeae223 100644 --- a/extensions/bluebubbles/index.ts +++ b/extensions/bluebubbles/index.ts @@ -9,6 +9,10 @@ export default defineBundledChannelEntry({ specifier: "./api.js", exportName: "bluebubblesPlugin", }, + secrets: { + specifier: "./src/secret-contract.js", + exportName: "channelSecrets", + }, runtime: { specifier: "./runtime-api.js", exportName: "setBlueBubblesRuntime", diff --git a/extensions/bluebubbles/setup-entry.ts b/extensions/bluebubbles/setup-entry.ts index 26502734095..40ef304ef41 100644 --- a/extensions/bluebubbles/setup-entry.ts +++ b/extensions/bluebubbles/setup-entry.ts @@ -6,4 +6,8 @@ export default defineBundledChannelSetupEntry({ specifier: "./api.js", exportName: "bluebubblesSetupPlugin", }, + secrets: { + specifier: "./src/secret-contract.js", + exportName: "channelSecrets", + }, }); diff --git a/extensions/bluebubbles/src/secret-contract.ts b/extensions/bluebubbles/src/secret-contract.ts index a8a895574e7..2f760dfdc11 100644 --- a/extensions/bluebubbles/src/secret-contract.ts +++ b/extensions/bluebubbles/src/secret-contract.ts @@ -52,3 +52,8 @@ export function collectRuntimeConfigAssignments(params: { accountInactiveReason: "BlueBubbles account is disabled.", }); } + +export const channelSecrets = { + secretTargetRegistryEntries, + collectRuntimeConfigAssignments, +}; diff --git a/extensions/feishu/channel-entry.ts b/extensions/feishu/channel-entry.ts index 549404e18a8..99fd860c7fb 100644 --- a/extensions/feishu/channel-entry.ts +++ b/extensions/feishu/channel-entry.ts @@ -9,6 +9,10 @@ export default defineBundledChannelEntry({ specifier: "./api.js", exportName: "feishuPlugin", }, + secrets: { + specifier: "./src/secret-contract.js", + exportName: "channelSecrets", + }, runtime: { specifier: "./runtime-api.js", exportName: "setFeishuRuntime", diff --git a/extensions/feishu/index.ts b/extensions/feishu/index.ts index 4f82b13482a..0f39644c8bc 100644 --- a/extensions/feishu/index.ts +++ b/extensions/feishu/index.ts @@ -70,6 +70,10 @@ export default defineBundledChannelEntry({ specifier: "./api.js", exportName: "feishuPlugin", }, + secrets: { + specifier: "./src/secret-contract.js", + exportName: "channelSecrets", + }, runtime: { specifier: "./runtime-api.js", exportName: "setFeishuRuntime", diff --git a/extensions/feishu/setup-entry.ts b/extensions/feishu/setup-entry.ts index e4da3147d24..44a9dbf62c3 100644 --- a/extensions/feishu/setup-entry.ts +++ b/extensions/feishu/setup-entry.ts @@ -6,4 +6,8 @@ export default defineBundledChannelSetupEntry({ specifier: "./api.js", exportName: "feishuPlugin", }, + secrets: { + specifier: "./src/secret-contract.js", + exportName: "channelSecrets", + }, }); diff --git a/extensions/feishu/src/secret-contract.ts b/extensions/feishu/src/secret-contract.ts index fd10b6c8f95..e4ec70d888e 100644 --- a/extensions/feishu/src/secret-contract.ts +++ b/extensions/feishu/src/secret-contract.ts @@ -138,3 +138,8 @@ export function collectRuntimeConfigAssignments(params: { accountInactiveReason: "Feishu account is disabled or not running in webhook mode.", }); } + +export const channelSecrets = { + secretTargetRegistryEntries, + collectRuntimeConfigAssignments, +}; diff --git a/extensions/googlechat/index.ts b/extensions/googlechat/index.ts index f5d18e80ab9..2b1e541ccf9 100644 --- a/extensions/googlechat/index.ts +++ b/extensions/googlechat/index.ts @@ -9,6 +9,10 @@ export default defineBundledChannelEntry({ specifier: "./api.js", exportName: "googlechatPlugin", }, + secrets: { + specifier: "./src/secret-contract.js", + exportName: "channelSecrets", + }, runtime: { specifier: "./runtime-api.js", exportName: "setGoogleChatRuntime", diff --git a/extensions/googlechat/setup-entry.ts b/extensions/googlechat/setup-entry.ts index b2824ca2fc1..a8e2558dae3 100644 --- a/extensions/googlechat/setup-entry.ts +++ b/extensions/googlechat/setup-entry.ts @@ -6,4 +6,8 @@ export default defineBundledChannelSetupEntry({ specifier: "./api.js", exportName: "googlechatPlugin", }, + secrets: { + specifier: "./src/secret-contract.js", + exportName: "channelSecrets", + }, }); diff --git a/extensions/googlechat/src/secret-contract.ts b/extensions/googlechat/src/secret-contract.ts index e3d3d04ee7c..89e9389d69e 100644 --- a/extensions/googlechat/src/secret-contract.ts +++ b/extensions/googlechat/src/secret-contract.ts @@ -154,3 +154,8 @@ export function collectRuntimeConfigAssignments(params: { }); } } + +export const channelSecrets = { + secretTargetRegistryEntries, + collectRuntimeConfigAssignments, +}; diff --git a/extensions/irc/index.ts b/extensions/irc/index.ts index 0df2821a8d8..2c1f7e48d75 100644 --- a/extensions/irc/index.ts +++ b/extensions/irc/index.ts @@ -9,6 +9,10 @@ export default defineBundledChannelEntry({ specifier: "./channel-plugin-api.js", exportName: "ircPlugin", }, + secrets: { + specifier: "./src/secret-contract.js", + exportName: "channelSecrets", + }, runtime: { specifier: "./runtime-api.js", exportName: "setIrcRuntime", diff --git a/extensions/irc/setup-entry.ts b/extensions/irc/setup-entry.ts index 16e7732960a..83687c75784 100644 --- a/extensions/irc/setup-entry.ts +++ b/extensions/irc/setup-entry.ts @@ -6,4 +6,8 @@ export default defineBundledChannelSetupEntry({ specifier: "./channel-plugin-api.js", exportName: "ircPlugin", }, + secrets: { + specifier: "./src/secret-contract.js", + exportName: "channelSecrets", + }, }); diff --git a/extensions/irc/src/secret-contract.ts b/extensions/irc/src/secret-contract.ts index d42e98db557..72f14bd2204 100644 --- a/extensions/irc/src/secret-contract.ts +++ b/extensions/irc/src/secret-contract.ts @@ -96,3 +96,8 @@ export function collectRuntimeConfigAssignments(params: { accountInactiveReason: "IRC account is disabled or NickServ is disabled for this account.", }); } + +export const channelSecrets = { + secretTargetRegistryEntries, + collectRuntimeConfigAssignments, +}; diff --git a/extensions/matrix/index.ts b/extensions/matrix/index.ts index 82534d7b99e..b7e868f2c5e 100644 --- a/extensions/matrix/index.ts +++ b/extensions/matrix/index.ts @@ -10,6 +10,10 @@ export default defineBundledChannelEntry({ specifier: "./channel-plugin-api.js", exportName: "matrixPlugin", }, + secrets: { + specifier: "./src/secret-contract.js", + exportName: "channelSecrets", + }, runtime: { specifier: "./runtime-api.js", exportName: "setMatrixRuntime", diff --git a/extensions/matrix/setup-entry.ts b/extensions/matrix/setup-entry.ts index fd5747b3556..11922509d4e 100644 --- a/extensions/matrix/setup-entry.ts +++ b/extensions/matrix/setup-entry.ts @@ -6,4 +6,8 @@ export default defineBundledChannelSetupEntry({ specifier: "./channel-plugin-api.js", exportName: "matrixPlugin", }, + secrets: { + specifier: "./src/secret-contract.js", + exportName: "channelSecrets", + }, }); diff --git a/extensions/matrix/src/secret-contract.ts b/extensions/matrix/src/secret-contract.ts index 4837d76dcff..4193abed0b7 100644 --- a/extensions/matrix/src/secret-contract.ts +++ b/extensions/matrix/src/secret-contract.ts @@ -167,3 +167,8 @@ export function collectRuntimeConfigAssignments(params: { }); } } + +export const channelSecrets = { + secretTargetRegistryEntries, + collectRuntimeConfigAssignments, +}; diff --git a/extensions/mattermost/index.ts b/extensions/mattermost/index.ts index 4e97e7b2e07..7293c22ea0d 100644 --- a/extensions/mattermost/index.ts +++ b/extensions/mattermost/index.ts @@ -21,6 +21,10 @@ export default defineBundledChannelEntry({ specifier: "./channel-plugin-api.js", exportName: "mattermostPlugin", }, + secrets: { + specifier: "./src/secret-contract.js", + exportName: "channelSecrets", + }, runtime: { specifier: "./runtime-api.js", exportName: "setMattermostRuntime", diff --git a/extensions/mattermost/setup-entry.ts b/extensions/mattermost/setup-entry.ts index fe9a329085b..73b914b1c40 100644 --- a/extensions/mattermost/setup-entry.ts +++ b/extensions/mattermost/setup-entry.ts @@ -6,4 +6,8 @@ export default defineBundledChannelSetupEntry({ specifier: "./channel-plugin-api.js", exportName: "mattermostSetupPlugin", }, + secrets: { + specifier: "./src/secret-contract.js", + exportName: "channelSecrets", + }, }); diff --git a/extensions/mattermost/src/secret-contract.ts b/extensions/mattermost/src/secret-contract.ts index 6249e084dae..228a4363fcd 100644 --- a/extensions/mattermost/src/secret-contract.ts +++ b/extensions/mattermost/src/secret-contract.ts @@ -52,3 +52,8 @@ export function collectRuntimeConfigAssignments(params: { accountInactiveReason: "Mattermost account is disabled.", }); } + +export const channelSecrets = { + secretTargetRegistryEntries, + collectRuntimeConfigAssignments, +}; diff --git a/extensions/msteams/index.ts b/extensions/msteams/index.ts index 38101a3f281..20df747f3b6 100644 --- a/extensions/msteams/index.ts +++ b/extensions/msteams/index.ts @@ -9,6 +9,10 @@ export default defineBundledChannelEntry({ specifier: "./api.js", exportName: "msteamsPlugin", }, + secrets: { + specifier: "./src/secret-contract.js", + exportName: "channelSecrets", + }, runtime: { specifier: "./runtime-api.js", exportName: "setMSTeamsRuntime", diff --git a/extensions/msteams/setup-entry.ts b/extensions/msteams/setup-entry.ts index a9c5cb040f4..ca2041131f4 100644 --- a/extensions/msteams/setup-entry.ts +++ b/extensions/msteams/setup-entry.ts @@ -6,4 +6,8 @@ export default defineBundledChannelSetupEntry({ specifier: "./api.js", exportName: "msteamsPlugin", }, + secrets: { + specifier: "./src/secret-contract.js", + exportName: "channelSecrets", + }, }); diff --git a/extensions/msteams/src/secret-contract.ts b/extensions/msteams/src/secret-contract.ts index bb27717c38f..9ad6893c00b 100644 --- a/extensions/msteams/src/secret-contract.ts +++ b/extensions/msteams/src/secret-contract.ts @@ -42,3 +42,8 @@ export function collectRuntimeConfigAssignments(params: { }, }); } + +export const channelSecrets = { + secretTargetRegistryEntries, + collectRuntimeConfigAssignments, +}; diff --git a/extensions/nextcloud-talk/index.ts b/extensions/nextcloud-talk/index.ts index cb26a275c40..63c0091270e 100644 --- a/extensions/nextcloud-talk/index.ts +++ b/extensions/nextcloud-talk/index.ts @@ -9,6 +9,10 @@ export default defineBundledChannelEntry({ specifier: "./api.js", exportName: "nextcloudTalkPlugin", }, + secrets: { + specifier: "./src/secret-contract.js", + exportName: "channelSecrets", + }, runtime: { specifier: "./runtime-api.js", exportName: "setNextcloudTalkRuntime", diff --git a/extensions/nextcloud-talk/setup-entry.ts b/extensions/nextcloud-talk/setup-entry.ts index 67206bc9056..94ccd110887 100644 --- a/extensions/nextcloud-talk/setup-entry.ts +++ b/extensions/nextcloud-talk/setup-entry.ts @@ -6,4 +6,8 @@ export default defineBundledChannelSetupEntry({ specifier: "./api.js", exportName: "nextcloudTalkPlugin", }, + secrets: { + specifier: "./src/secret-contract.js", + exportName: "channelSecrets", + }, }); diff --git a/extensions/nextcloud-talk/src/secret-contract.ts b/extensions/nextcloud-talk/src/secret-contract.ts index b321804e422..53baa6b318c 100644 --- a/extensions/nextcloud-talk/src/secret-contract.ts +++ b/extensions/nextcloud-talk/src/secret-contract.ts @@ -96,3 +96,8 @@ export function collectRuntimeConfigAssignments(params: { accountInactiveReason: "Nextcloud Talk account is disabled.", }); } + +export const channelSecrets = { + secretTargetRegistryEntries, + collectRuntimeConfigAssignments, +}; diff --git a/extensions/slack/channel-entry.ts b/extensions/slack/channel-entry.ts index b23f1672e49..4eaba4b7866 100644 --- a/extensions/slack/channel-entry.ts +++ b/extensions/slack/channel-entry.ts @@ -9,6 +9,10 @@ export default defineBundledChannelEntry({ specifier: "./api.js", exportName: "slackPlugin", }, + secrets: { + specifier: "./src/secret-contract.js", + exportName: "channelSecrets", + }, runtime: { specifier: "./runtime-api.js", exportName: "setSlackRuntime", diff --git a/extensions/slack/index.ts b/extensions/slack/index.ts index 4409528f127..91efdfdce49 100644 --- a/extensions/slack/index.ts +++ b/extensions/slack/index.ts @@ -21,6 +21,10 @@ export default defineBundledChannelEntry({ specifier: "./channel-plugin-api.js", exportName: "slackPlugin", }, + secrets: { + specifier: "./src/secret-contract.js", + exportName: "channelSecrets", + }, runtime: { specifier: "./runtime-api.js", exportName: "setSlackRuntime", diff --git a/extensions/slack/setup-entry.ts b/extensions/slack/setup-entry.ts index bf5bc927594..2b68ad93514 100644 --- a/extensions/slack/setup-entry.ts +++ b/extensions/slack/setup-entry.ts @@ -6,4 +6,8 @@ export default defineBundledChannelSetupEntry({ specifier: "./channel-plugin-api.js", exportName: "slackSetupPlugin", }, + secrets: { + specifier: "./src/secret-contract.js", + exportName: "channelSecrets", + }, }); diff --git a/extensions/slack/src/secret-contract.ts b/extensions/slack/src/secret-contract.ts index 13fdbbe4ae4..d86225de026 100644 --- a/extensions/slack/src/secret-contract.ts +++ b/extensions/slack/src/secret-contract.ts @@ -156,3 +156,8 @@ export function collectRuntimeConfigAssignments(params: { accountInactiveReason: "Slack account is disabled or not running in HTTP mode.", }); } + +export const channelSecrets = { + secretTargetRegistryEntries, + collectRuntimeConfigAssignments, +}; diff --git a/extensions/telegram/index.ts b/extensions/telegram/index.ts index b727d21f808..8bedf364a21 100644 --- a/extensions/telegram/index.ts +++ b/extensions/telegram/index.ts @@ -9,6 +9,10 @@ export default defineBundledChannelEntry({ specifier: "./channel-plugin-api.js", exportName: "telegramPlugin", }, + secrets: { + specifier: "./src/secret-contract.js", + exportName: "channelSecrets", + }, runtime: { specifier: "./runtime-api.js", exportName: "setTelegramRuntime", diff --git a/extensions/telegram/setup-entry.ts b/extensions/telegram/setup-entry.ts index 9be767fdf35..45d5e8aaaeb 100644 --- a/extensions/telegram/setup-entry.ts +++ b/extensions/telegram/setup-entry.ts @@ -6,4 +6,8 @@ export default defineBundledChannelSetupEntry({ specifier: "./channel-plugin-api.js", exportName: "telegramSetupPlugin", }, + secrets: { + specifier: "./src/secret-contract.js", + exportName: "channelSecrets", + }, }); diff --git a/extensions/telegram/src/secret-contract.ts b/extensions/telegram/src/secret-contract.ts index a55d9c3b08f..e10e49e39b2 100644 --- a/extensions/telegram/src/secret-contract.ts +++ b/extensions/telegram/src/secret-contract.ts @@ -115,3 +115,8 @@ export function collectRuntimeConfigAssignments(params: { "Telegram account is disabled or webhook mode is not active for this account.", }); } + +export const channelSecrets = { + secretTargetRegistryEntries, + collectRuntimeConfigAssignments, +}; diff --git a/extensions/zalo/index.ts b/extensions/zalo/index.ts index 08ebb19beb3..1c81a2f93fe 100644 --- a/extensions/zalo/index.ts +++ b/extensions/zalo/index.ts @@ -9,6 +9,10 @@ export default defineBundledChannelEntry({ specifier: "./api.js", exportName: "zaloPlugin", }, + secrets: { + specifier: "./src/secret-contract.js", + exportName: "channelSecrets", + }, runtime: { specifier: "./runtime-api.js", exportName: "setZaloRuntime", diff --git a/extensions/zalo/setup-entry.ts b/extensions/zalo/setup-entry.ts index 54b158b3bf5..21f28135814 100644 --- a/extensions/zalo/setup-entry.ts +++ b/extensions/zalo/setup-entry.ts @@ -6,4 +6,8 @@ export default defineBundledChannelSetupEntry({ specifier: "./api.js", exportName: "zaloPlugin", }, + secrets: { + specifier: "./src/secret-contract.js", + exportName: "channelSecrets", + }, }); diff --git a/extensions/zalo/src/secret-contract.ts b/extensions/zalo/src/secret-contract.ts index 8a928321cb1..53870ba2d02 100644 --- a/extensions/zalo/src/secret-contract.ts +++ b/extensions/zalo/src/secret-contract.ts @@ -102,3 +102,8 @@ export function collectRuntimeConfigAssignments(params: { "Zalo account is disabled or webhook mode is not active for this account.", }); } + +export const channelSecrets = { + secretTargetRegistryEntries, + collectRuntimeConfigAssignments, +}; diff --git a/src/channels/plugins/bootstrap-registry.ts b/src/channels/plugins/bootstrap-registry.ts index 676d693f361..b5ccbb28e64 100644 --- a/src/channels/plugins/bootstrap-registry.ts +++ b/src/channels/plugins/bootstrap-registry.ts @@ -1,10 +1,16 @@ import { listBundledChannelPluginIds } from "./bundled-ids.js"; -import { getBundledChannelPlugin, getBundledChannelSetupPlugin } from "./bundled.js"; +import { + getBundledChannelPlugin, + getBundledChannelSecrets, + getBundledChannelSetupPlugin, + getBundledChannelSetupSecrets, +} from "./bundled.js"; import type { ChannelId, ChannelPlugin } from "./types.js"; type CachedBootstrapPlugins = { sortedIds: string[]; byId: Map; + secretsById: Map; missingIds: Set; }; @@ -52,6 +58,7 @@ function buildBootstrapPlugins(): CachedBootstrapPlugins { return { sortedIds: listBundledChannelPluginIds(), byId: new Map(), + secretsById: new Map(), missingIds: new Set(), }; } @@ -105,6 +112,26 @@ export function getBootstrapChannelPlugin(id: ChannelId): ChannelPlugin | undefi return merged; } +export function getBootstrapChannelSecrets(id: ChannelId): ChannelPlugin["secrets"] | undefined { + const resolvedId = String(id).trim(); + if (!resolvedId) { + return undefined; + } + const registry = getBootstrapPlugins(); + const cached = registry.secretsById.get(resolvedId); + if (cached) { + return cached; + } + if (registry.secretsById.has(resolvedId)) { + return undefined; + } + const runtimeSecrets = getBundledChannelSecrets(resolvedId); + const setupSecrets = getBundledChannelSetupSecrets(resolvedId); + const merged = mergePluginSection(runtimeSecrets, setupSecrets); + registry.secretsById.set(resolvedId, merged ?? null); + return merged; +} + export function clearBootstrapChannelPluginCache(): void { cachedBootstrapPlugins = null; } diff --git a/src/channels/plugins/bundled.ts b/src/channels/plugins/bundled.ts index 873fe393162..0f7f221b088 100644 --- a/src/channels/plugins/bundled.ts +++ b/src/channels/plugins/bundled.ts @@ -191,6 +191,8 @@ type BundledChannelState = { sortedIds: readonly ChannelId[]; pluginsById: Map; setupPluginsById: Map; + secretsById: Map; + setupSecretsById: Map; runtimeSettersById: Map>; }; @@ -201,6 +203,8 @@ const EMPTY_BUNDLED_CHANNEL_STATE: BundledChannelState = { sortedIds: [], pluginsById: new Map(), setupPluginsById: new Map(), + secretsById: new Map(), + setupSecretsById: new Map(), runtimeSettersById: new Map(), }; @@ -247,6 +251,8 @@ function getBundledChannelState(): BundledChannelState { sortedIds: [...entriesById.keys()].toSorted((left, right) => left.localeCompare(right)), pluginsById: new Map(), setupPluginsById: new Map(), + secretsById: new Map(), + setupSecretsById: new Map(), runtimeSettersById, }; return cachedBundledChannelState; @@ -294,6 +300,20 @@ export function getBundledChannelPlugin(id: ChannelId): ChannelPlugin | undefine } } +export function getBundledChannelSecrets(id: ChannelId): ChannelPlugin["secrets"] | undefined { + const state = getBundledChannelState(); + if (state.secretsById.has(id)) { + return state.secretsById.get(id) ?? undefined; + } + const entry = state.entriesById.get(id); + if (!entry) { + return undefined; + } + const secrets = entry.loadChannelSecrets?.() ?? getBundledChannelPlugin(id)?.secrets; + state.secretsById.set(id, secrets ?? null); + return secrets; +} + export function getBundledChannelSetupPlugin(id: ChannelId): ChannelPlugin | undefined { const state = getBundledChannelState(); const cached = state.setupPluginsById.get(id); @@ -317,6 +337,20 @@ export function getBundledChannelSetupPlugin(id: ChannelId): ChannelPlugin | und } } +export function getBundledChannelSetupSecrets(id: ChannelId): ChannelPlugin["secrets"] | undefined { + const state = getBundledChannelState(); + if (state.setupSecretsById.has(id)) { + return state.setupSecretsById.get(id) ?? undefined; + } + const entry = state.setupEntriesById.get(id); + if (!entry) { + return undefined; + } + const secrets = entry.loadSetupSecrets?.() ?? getBundledChannelSetupPlugin(id)?.secrets; + state.setupSecretsById.set(id, secrets ?? null); + return secrets; +} + export function requireBundledChannelPlugin(id: ChannelId): ChannelPlugin { const plugin = getBundledChannelPlugin(id); if (!plugin) { diff --git a/src/plugin-sdk/channel-entry-contract.ts b/src/plugin-sdk/channel-entry-contract.ts index 75333bac9c1..1f55fb52a96 100644 --- a/src/plugin-sdk/channel-entry-contract.ts +++ b/src/plugin-sdk/channel-entry-contract.ts @@ -32,6 +32,7 @@ type DefineBundledChannelEntryOptions = { description: string; importMetaUrl: string; plugin: BundledEntryModuleRef; + secrets?: BundledEntryModuleRef; configSchema?: ChannelEntryConfigSchema | (() => ChannelEntryConfigSchema); runtime?: BundledEntryModuleRef; registerCliMetadata?: (api: OpenClawPluginApi) => void; @@ -41,6 +42,7 @@ type DefineBundledChannelEntryOptions = { type DefineBundledChannelSetupEntryOptions = { importMetaUrl: string; plugin: BundledEntryModuleRef; + secrets?: BundledEntryModuleRef; }; export type BundledChannelEntryContract = { @@ -51,12 +53,14 @@ export type BundledChannelEntryContract = { configSchema: ChannelEntryConfigSchema; register: (api: OpenClawPluginApi) => void; loadChannelPlugin: () => TPlugin; + loadChannelSecrets?: () => ChannelPlugin["secrets"] | undefined; setChannelRuntime?: (runtime: PluginRuntime) => void; }; export type BundledChannelSetupEntryContract = { kind: "bundled-channel-setup-entry"; loadSetupPlugin: () => TPlugin; + loadSetupSecrets?: () => ChannelPlugin["secrets"] | undefined; }; const nodeRequire = createRequire(import.meta.url); @@ -172,6 +176,7 @@ export function defineBundledChannelEntry({ description, importMetaUrl, plugin, + secrets, configSchema, runtime, registerCliMetadata, @@ -182,6 +187,9 @@ export function defineBundledChannelEntry({ ? configSchema() : ((configSchema ?? emptyChannelConfigSchema()) as ChannelEntryConfigSchema); const loadChannelPlugin = () => loadBundledEntryExportSync(importMetaUrl, plugin); + const loadChannelSecrets = secrets + ? () => loadBundledEntryExportSync(importMetaUrl, secrets) + : undefined; const setChannelRuntime = runtime ? (pluginRuntime: PluginRuntime) => { const setter = loadBundledEntryExportSync<(runtime: PluginRuntime) => void>( @@ -212,6 +220,7 @@ export function defineBundledChannelEntry({ registerFull?.(api); }, loadChannelPlugin, + ...(loadChannelSecrets ? { loadChannelSecrets } : {}), ...(setChannelRuntime ? { setChannelRuntime } : {}), }; } @@ -219,9 +228,19 @@ export function defineBundledChannelEntry({ export function defineBundledChannelSetupEntry({ importMetaUrl, plugin, + secrets, }: DefineBundledChannelSetupEntryOptions): BundledChannelSetupEntryContract { return { kind: "bundled-channel-setup-entry", loadSetupPlugin: () => loadBundledEntryExportSync(importMetaUrl, plugin), + ...(secrets + ? { + loadSetupSecrets: () => + loadBundledEntryExportSync( + importMetaUrl, + secrets, + ), + } + : {}), }; } diff --git a/src/secrets/runtime-config-collectors-channels.ts b/src/secrets/runtime-config-collectors-channels.ts index a0a5a293997..faf87ac5240 100644 --- a/src/secrets/runtime-config-collectors-channels.ts +++ b/src/secrets/runtime-config-collectors-channels.ts @@ -1,4 +1,4 @@ -import { getBootstrapChannelPlugin } from "../channels/plugins/bootstrap-registry.js"; +import { getBootstrapChannelSecrets } from "../channels/plugins/bootstrap-registry.js"; import type { OpenClawConfig } from "../config/config.js"; import { type ResolverContext, type SecretDefaults } from "./runtime-shared.js"; @@ -12,10 +12,10 @@ export function collectChannelConfigAssignments(params: { return; } for (const channelId of channelIds) { - const plugin = getBootstrapChannelPlugin(channelId); - if (!plugin) { + const secrets = getBootstrapChannelSecrets(channelId); + if (!secrets) { continue; } - plugin.secrets?.collectRuntimeConfigAssignments?.(params); + secrets.collectRuntimeConfigAssignments?.(params); } } diff --git a/src/secrets/runtime.coverage.test.ts b/src/secrets/runtime.coverage.test.ts index e1b5e86d7f7..3fb484a38f7 100644 --- a/src/secrets/runtime.coverage.test.ts +++ b/src/secrets/runtime.coverage.test.ts @@ -50,12 +50,14 @@ function loadCoverageRegistryEntries(): SecretRegistryEntry[] { } const COVERAGE_REGISTRY_ENTRIES = loadCoverageRegistryEntries(); +const DEBUG_COVERAGE_BATCHES = process.env.OPENCLAW_DEBUG_RUNTIME_COVERAGE === "1"; let applyResolvedAssignments: typeof import("./runtime-shared.js").applyResolvedAssignments; let collectAuthStoreAssignments: typeof import("./runtime-auth-collectors.js").collectAuthStoreAssignments; let collectConfigAssignments: typeof import("./runtime-config-collectors.js").collectConfigAssignments; let createResolverContext: typeof import("./runtime-shared.js").createResolverContext; let resolveSecretRefValues: typeof import("./resolve.js").resolveSecretRefValues; +let resolveRuntimeWebTools: typeof import("./runtime-web-tools.js").resolveRuntimeWebTools; function toConcretePathSegments(pathPattern: string, wildcardToken = "sample"): string[] { const segments = pathPattern.split(".").filter(Boolean); @@ -75,7 +77,8 @@ function toConcretePathSegments(pathPattern: string, wildcardToken = "sample"): } function resolveCoverageEnvId(entry: SecretRegistryEntry, fallbackEnvId: string): string { - return entry.id === "plugins.entries.firecrawl.config.webFetch.apiKey" + return entry.id === "plugins.entries.firecrawl.config.webFetch.apiKey" || + entry.id === "tools.web.fetch.firecrawl.apiKey" ? "FIRECRAWL_API_KEY" : fallbackEnvId; } @@ -126,11 +129,15 @@ function resolveCoverageBatchKey(entry: SecretRegistryEntry): string { } if (entry.id.startsWith("channels.")) { const segments = entry.id.split("."); + const channelId = segments[1] ?? "unknown"; const field = segments.at(-1); - if (field === "accessToken" || field === "password") { + if ( + field === "accessToken" || + field === "password" || + (channelId === "slack" && field === "signingSecret") + ) { return entry.id; } - const channelId = segments[1] ?? "unknown"; const scope = segments[2] === "accounts" ? "accounts" : "root"; return `channels.${channelId}.${scope}`; } @@ -169,6 +176,15 @@ function buildCoverageBatches(entries: readonly SecretRegistryEntry[]): SecretRe return [...batches.values()]; } +function logCoverageBatch(label: string, batch: readonly SecretRegistryEntry[]): void { + if (!DEBUG_COVERAGE_BATCHES || batch.length === 0) { + return; + } + process.stderr.write( + `[runtime.coverage] ${label} batch (${batch.length}): ${batch.map((entry) => entry.id).join(", ")}\n`, + ); +} + function applyConfigForOpenClawTarget( config: OpenClawConfig, entry: SecretRegistryEntry, @@ -193,6 +209,12 @@ function applyConfigForOpenClawTarget( ); setPathCreateStrict(config, ["models", "providers", wildcardToken, "models"], []); } + if (entry.id.startsWith("plugins.entries.")) { + const pluginId = entry.id.split(".")[2]; + if (pluginId) { + setPathCreateStrict(config, ["plugins", "entries", pluginId, "enabled"], true); + } + } if (entry.id === "agents.defaults.memorySearch.remote.apiKey") { setPathCreateStrict(config, ["agents", "list", "0", "id"], "sample-agent"); } @@ -382,6 +404,12 @@ async function prepareCoverageSnapshot(params: { }); } + await resolveRuntimeWebTools({ + sourceConfig, + resolvedConfig, + context, + }); + return { config: resolvedConfig, authStores, @@ -391,16 +419,20 @@ async function prepareCoverageSnapshot(params: { describe("secrets runtime target coverage", () => { beforeAll(async () => { - const [sharedRuntime, authCollectors, configCollectors, resolver] = await Promise.all([ - import("./runtime-shared.js"), - import("./runtime-auth-collectors.js"), - import("./runtime-config-collectors.js"), - import("./resolve.js"), - ]); + const [sharedRuntime, authCollectors, configCollectors, resolver, webTools] = await Promise.all( + [ + import("./runtime-shared.js"), + import("./runtime-auth-collectors.js"), + import("./runtime-config-collectors.js"), + import("./resolve.js"), + import("./runtime-web-tools.js"), + ], + ); ({ applyResolvedAssignments, createResolverContext } = sharedRuntime); ({ collectAuthStoreAssignments } = authCollectors); ({ collectConfigAssignments } = configCollectors); ({ resolveSecretRefValues } = resolver); + ({ resolveRuntimeWebTools } = webTools); }); it("handles every openclaw.json registry target when configured as active", async () => { @@ -408,6 +440,7 @@ describe("secrets runtime target coverage", () => { (entry) => entry.configFile === "openclaw.json", ); for (const batch of buildCoverageBatches(entries)) { + logCoverageBatch("openclaw.json", batch); const config = {} as OpenClawConfig; const env: Record = {}; for (const [index, entry] of batch.entries()) { @@ -440,6 +473,7 @@ describe("secrets runtime target coverage", () => { (entry) => entry.configFile === "auth-profiles.json", ); for (const batch of buildCoverageBatches(entries)) { + logCoverageBatch("auth-profiles.json", batch); const env: Record = {}; const authStore: AuthProfileStore = { version: 1, diff --git a/src/secrets/target-registry-test-helpers.ts b/src/secrets/target-registry-test-helpers.ts index 88292fd3ae9..09b1212fb2d 100644 --- a/src/secrets/target-registry-test-helpers.ts +++ b/src/secrets/target-registry-test-helpers.ts @@ -2,5 +2,8 @@ export function canonicalizeSecretTargetCoverageId(id: string): string { if (id === "tools.web.x_search.apiKey") { return "plugins.entries.xai.config.webSearch.apiKey"; } + if (id === "tools.web.fetch.firecrawl.apiKey") { + return "plugins.entries.firecrawl.config.webFetch.apiKey"; + } return id; }