perf(test): lazy-load bundled channel secrets

This commit is contained in:
Vincent Koc
2026-04-06 16:40:30 +01:00
parent 349a1c58f9
commit c3b19d204a
41 changed files with 282 additions and 14 deletions

View File

@@ -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",

View File

@@ -6,4 +6,8 @@ export default defineBundledChannelSetupEntry({
specifier: "./api.js",
exportName: "bluebubblesSetupPlugin",
},
secrets: {
specifier: "./src/secret-contract.js",
exportName: "channelSecrets",
},
});

View File

@@ -52,3 +52,8 @@ export function collectRuntimeConfigAssignments(params: {
accountInactiveReason: "BlueBubbles account is disabled.",
});
}
export const channelSecrets = {
secretTargetRegistryEntries,
collectRuntimeConfigAssignments,
};

View File

@@ -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",

View File

@@ -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",

View File

@@ -6,4 +6,8 @@ export default defineBundledChannelSetupEntry({
specifier: "./api.js",
exportName: "feishuPlugin",
},
secrets: {
specifier: "./src/secret-contract.js",
exportName: "channelSecrets",
},
});

View File

@@ -138,3 +138,8 @@ export function collectRuntimeConfigAssignments(params: {
accountInactiveReason: "Feishu account is disabled or not running in webhook mode.",
});
}
export const channelSecrets = {
secretTargetRegistryEntries,
collectRuntimeConfigAssignments,
};

View File

@@ -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",

View File

@@ -6,4 +6,8 @@ export default defineBundledChannelSetupEntry({
specifier: "./api.js",
exportName: "googlechatPlugin",
},
secrets: {
specifier: "./src/secret-contract.js",
exportName: "channelSecrets",
},
});

View File

@@ -154,3 +154,8 @@ export function collectRuntimeConfigAssignments(params: {
});
}
}
export const channelSecrets = {
secretTargetRegistryEntries,
collectRuntimeConfigAssignments,
};

View File

@@ -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",

View File

@@ -6,4 +6,8 @@ export default defineBundledChannelSetupEntry({
specifier: "./channel-plugin-api.js",
exportName: "ircPlugin",
},
secrets: {
specifier: "./src/secret-contract.js",
exportName: "channelSecrets",
},
});

View File

@@ -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,
};

View File

@@ -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",

View File

@@ -6,4 +6,8 @@ export default defineBundledChannelSetupEntry({
specifier: "./channel-plugin-api.js",
exportName: "matrixPlugin",
},
secrets: {
specifier: "./src/secret-contract.js",
exportName: "channelSecrets",
},
});

View File

@@ -167,3 +167,8 @@ export function collectRuntimeConfigAssignments(params: {
});
}
}
export const channelSecrets = {
secretTargetRegistryEntries,
collectRuntimeConfigAssignments,
};

View File

@@ -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",

View File

@@ -6,4 +6,8 @@ export default defineBundledChannelSetupEntry({
specifier: "./channel-plugin-api.js",
exportName: "mattermostSetupPlugin",
},
secrets: {
specifier: "./src/secret-contract.js",
exportName: "channelSecrets",
},
});

View File

@@ -52,3 +52,8 @@ export function collectRuntimeConfigAssignments(params: {
accountInactiveReason: "Mattermost account is disabled.",
});
}
export const channelSecrets = {
secretTargetRegistryEntries,
collectRuntimeConfigAssignments,
};

View File

@@ -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",

View File

@@ -6,4 +6,8 @@ export default defineBundledChannelSetupEntry({
specifier: "./api.js",
exportName: "msteamsPlugin",
},
secrets: {
specifier: "./src/secret-contract.js",
exportName: "channelSecrets",
},
});

View File

@@ -42,3 +42,8 @@ export function collectRuntimeConfigAssignments(params: {
},
});
}
export const channelSecrets = {
secretTargetRegistryEntries,
collectRuntimeConfigAssignments,
};

View File

@@ -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",

View File

@@ -6,4 +6,8 @@ export default defineBundledChannelSetupEntry({
specifier: "./api.js",
exportName: "nextcloudTalkPlugin",
},
secrets: {
specifier: "./src/secret-contract.js",
exportName: "channelSecrets",
},
});

View File

@@ -96,3 +96,8 @@ export function collectRuntimeConfigAssignments(params: {
accountInactiveReason: "Nextcloud Talk account is disabled.",
});
}
export const channelSecrets = {
secretTargetRegistryEntries,
collectRuntimeConfigAssignments,
};

View File

@@ -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",

View File

@@ -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",

View File

@@ -6,4 +6,8 @@ export default defineBundledChannelSetupEntry({
specifier: "./channel-plugin-api.js",
exportName: "slackSetupPlugin",
},
secrets: {
specifier: "./src/secret-contract.js",
exportName: "channelSecrets",
},
});

View File

@@ -156,3 +156,8 @@ export function collectRuntimeConfigAssignments(params: {
accountInactiveReason: "Slack account is disabled or not running in HTTP mode.",
});
}
export const channelSecrets = {
secretTargetRegistryEntries,
collectRuntimeConfigAssignments,
};

View File

@@ -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",

View File

@@ -6,4 +6,8 @@ export default defineBundledChannelSetupEntry({
specifier: "./channel-plugin-api.js",
exportName: "telegramSetupPlugin",
},
secrets: {
specifier: "./src/secret-contract.js",
exportName: "channelSecrets",
},
});

View File

@@ -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,
};

View File

@@ -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",

View File

@@ -6,4 +6,8 @@ export default defineBundledChannelSetupEntry({
specifier: "./api.js",
exportName: "zaloPlugin",
},
secrets: {
specifier: "./src/secret-contract.js",
exportName: "channelSecrets",
},
});

View File

@@ -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,
};

View File

@@ -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<string, ChannelPlugin>;
secretsById: Map<string, ChannelPlugin["secrets"] | null>;
missingIds: Set<string>;
};
@@ -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;
}

View File

@@ -191,6 +191,8 @@ type BundledChannelState = {
sortedIds: readonly ChannelId[];
pluginsById: Map<ChannelId, ChannelPlugin>;
setupPluginsById: Map<ChannelId, ChannelPlugin>;
secretsById: Map<ChannelId, ChannelPlugin["secrets"] | null>;
setupSecretsById: Map<ChannelId, ChannelPlugin["secrets"] | null>;
runtimeSettersById: Map<ChannelId, NonNullable<BundledChannelEntryContract["setChannelRuntime"]>>;
};
@@ -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) {

View File

@@ -32,6 +32,7 @@ type DefineBundledChannelEntryOptions<TPlugin = ChannelPlugin> = {
description: string;
importMetaUrl: string;
plugin: BundledEntryModuleRef;
secrets?: BundledEntryModuleRef;
configSchema?: ChannelEntryConfigSchema<TPlugin> | (() => ChannelEntryConfigSchema<TPlugin>);
runtime?: BundledEntryModuleRef;
registerCliMetadata?: (api: OpenClawPluginApi) => void;
@@ -41,6 +42,7 @@ type DefineBundledChannelEntryOptions<TPlugin = ChannelPlugin> = {
type DefineBundledChannelSetupEntryOptions = {
importMetaUrl: string;
plugin: BundledEntryModuleRef;
secrets?: BundledEntryModuleRef;
};
export type BundledChannelEntryContract<TPlugin = ChannelPlugin> = {
@@ -51,12 +53,14 @@ export type BundledChannelEntryContract<TPlugin = ChannelPlugin> = {
configSchema: ChannelEntryConfigSchema<TPlugin>;
register: (api: OpenClawPluginApi) => void;
loadChannelPlugin: () => TPlugin;
loadChannelSecrets?: () => ChannelPlugin["secrets"] | undefined;
setChannelRuntime?: (runtime: PluginRuntime) => void;
};
export type BundledChannelSetupEntryContract<TPlugin = ChannelPlugin> = {
kind: "bundled-channel-setup-entry";
loadSetupPlugin: () => TPlugin;
loadSetupSecrets?: () => ChannelPlugin["secrets"] | undefined;
};
const nodeRequire = createRequire(import.meta.url);
@@ -172,6 +176,7 @@ export function defineBundledChannelEntry<TPlugin = ChannelPlugin>({
description,
importMetaUrl,
plugin,
secrets,
configSchema,
runtime,
registerCliMetadata,
@@ -182,6 +187,9 @@ export function defineBundledChannelEntry<TPlugin = ChannelPlugin>({
? configSchema()
: ((configSchema ?? emptyChannelConfigSchema()) as ChannelEntryConfigSchema<TPlugin>);
const loadChannelPlugin = () => loadBundledEntryExportSync<TPlugin>(importMetaUrl, plugin);
const loadChannelSecrets = secrets
? () => loadBundledEntryExportSync<ChannelPlugin["secrets"] | undefined>(importMetaUrl, secrets)
: undefined;
const setChannelRuntime = runtime
? (pluginRuntime: PluginRuntime) => {
const setter = loadBundledEntryExportSync<(runtime: PluginRuntime) => void>(
@@ -212,6 +220,7 @@ export function defineBundledChannelEntry<TPlugin = ChannelPlugin>({
registerFull?.(api);
},
loadChannelPlugin,
...(loadChannelSecrets ? { loadChannelSecrets } : {}),
...(setChannelRuntime ? { setChannelRuntime } : {}),
};
}
@@ -219,9 +228,19 @@ export function defineBundledChannelEntry<TPlugin = ChannelPlugin>({
export function defineBundledChannelSetupEntry<TPlugin = ChannelPlugin>({
importMetaUrl,
plugin,
secrets,
}: DefineBundledChannelSetupEntryOptions): BundledChannelSetupEntryContract<TPlugin> {
return {
kind: "bundled-channel-setup-entry",
loadSetupPlugin: () => loadBundledEntryExportSync<TPlugin>(importMetaUrl, plugin),
...(secrets
? {
loadSetupSecrets: () =>
loadBundledEntryExportSync<ChannelPlugin["secrets"] | undefined>(
importMetaUrl,
secrets,
),
}
: {}),
};
}

View File

@@ -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);
}
}

View File

@@ -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<string, string> = {};
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<string, string> = {};
const authStore: AuthProfileStore = {
version: 1,

View File

@@ -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;
}