From bc9c2cc1622bc9062f6ad2c1ee82c30fb098b35a Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 22 Apr 2026 05:03:55 +0100 Subject: [PATCH] refactor: derive web credential secret targets from manifests --- ...hannel-contract-surface-guardrails.test.ts | 1 + src/secrets/target-registry-data.ts | 175 +++++++----------- src/secrets/target-registry.test.ts | 18 +- 3 files changed, 82 insertions(+), 112 deletions(-) diff --git a/src/secrets/channel-contract-surface-guardrails.test.ts b/src/secrets/channel-contract-surface-guardrails.test.ts index dd04b65ac13..7ca4d9a691e 100644 --- a/src/secrets/channel-contract-surface-guardrails.test.ts +++ b/src/secrets/channel-contract-surface-guardrails.test.ts @@ -23,6 +23,7 @@ const CORE_SECRET_SURFACE_GUARDS = [ /channels\.bluebubbles\./, /channels\.msteams\./, /channels\.nextcloud-talk\./, + /plugins\.entries\.(?:brave|google|exa|xai|moonshot|perplexity|firecrawl|tavily|minimax)\.config\.web(?:Search|Fetch)\.apiKey/, ], }, { diff --git a/src/secrets/target-registry-data.ts b/src/secrets/target-registry-data.ts index 8347139510f..968b365023a 100644 --- a/src/secrets/target-registry-data.ts +++ b/src/secrets/target-registry-data.ts @@ -1,10 +1,72 @@ -import { loadPluginManifestRegistry } from "../plugins/manifest-registry.js"; +import { + loadPluginManifestRegistry, + type PluginManifestRecord, +} from "../plugins/manifest-registry.js"; import { loadBundledChannelSecretContractApi } from "./channel-contract-api.js"; import type { SecretTargetRegistryEntry } from "./target-registry-types.js"; const SECRET_INPUT_SHAPE = "secret_input"; // pragma: allowlist secret const SIBLING_REF_SHAPE = "sibling_ref"; // pragma: allowlist secret +const WEB_PROVIDER_SECRET_CONFIGS = [ + { contract: "webSearchProviders", configPath: "webSearch.apiKey" }, + { contract: "webFetchProviders", configPath: "webFetch.apiKey" }, +] as const; + +type WebProviderSecretConfig = (typeof WEB_PROVIDER_SECRET_CONFIGS)[number]; + +function createPluginOpenClawConfigSecretTargetEntry( + pluginId: string, + configPath: string, +): SecretTargetRegistryEntry { + const pathPattern = ["plugins", "entries", pluginId, "config", ...configPath.split(".")].join( + ".", + ); + return { + id: pathPattern, + targetType: pathPattern, + configFile: "openclaw.json", + pathPattern, + secretShape: SECRET_INPUT_SHAPE, + expectedResolvedValue: "string", + includeInPlan: true, + includeInConfigure: true, + includeInAudit: true, + }; +} + +function hasSensitiveConfigHint( + plugin: PluginManifestRecord, + configPath: WebProviderSecretConfig["configPath"], +): boolean { + return plugin.configUiHints?.[configPath]?.sensitive === true; +} + +function hasWebProviderContract( + plugin: PluginManifestRecord, + contract: WebProviderSecretConfig["contract"], +): boolean { + return (plugin.contracts?.[contract]?.length ?? 0) > 0; +} + +function listBundledWebProviderSecretTargetRegistryEntries(): SecretTargetRegistryEntry[] { + const entries: SecretTargetRegistryEntry[] = []; + for (const record of loadPluginManifestRegistry({}).plugins) { + if (record.origin !== "bundled") { + continue; + } + for (const config of WEB_PROVIDER_SECRET_CONFIGS) { + if ( + hasWebProviderContract(record, config.contract) && + hasSensitiveConfigHint(record, config.configPath) + ) { + entries.push(createPluginOpenClawConfigSecretTargetEntry(record.id, config.configPath)); + } + } + } + return entries.toSorted((left, right) => left.id.localeCompare(right.id)); +} + function listChannelSecretTargetRegistryEntries(): SecretTargetRegistryEntry[] { const entries: SecretTargetRegistryEntry[] = []; @@ -347,116 +409,6 @@ const CORE_SECRET_TARGET_REGISTRY: SecretTargetRegistryEntry[] = [ includeInConfigure: true, includeInAudit: true, }, - { - id: "plugins.entries.brave.config.webSearch.apiKey", - targetType: "plugins.entries.brave.config.webSearch.apiKey", - configFile: "openclaw.json", - pathPattern: "plugins.entries.brave.config.webSearch.apiKey", - secretShape: SECRET_INPUT_SHAPE, - expectedResolvedValue: "string", - includeInPlan: true, - includeInConfigure: true, - includeInAudit: true, - }, - { - id: "plugins.entries.google.config.webSearch.apiKey", - targetType: "plugins.entries.google.config.webSearch.apiKey", - configFile: "openclaw.json", - pathPattern: "plugins.entries.google.config.webSearch.apiKey", - secretShape: SECRET_INPUT_SHAPE, - expectedResolvedValue: "string", - includeInPlan: true, - includeInConfigure: true, - includeInAudit: true, - }, - { - id: "plugins.entries.exa.config.webSearch.apiKey", - targetType: "plugins.entries.exa.config.webSearch.apiKey", - configFile: "openclaw.json", - pathPattern: "plugins.entries.exa.config.webSearch.apiKey", - secretShape: SECRET_INPUT_SHAPE, - expectedResolvedValue: "string", - includeInPlan: true, - includeInConfigure: true, - includeInAudit: true, - }, - { - id: "plugins.entries.xai.config.webSearch.apiKey", - targetType: "plugins.entries.xai.config.webSearch.apiKey", - configFile: "openclaw.json", - pathPattern: "plugins.entries.xai.config.webSearch.apiKey", - secretShape: SECRET_INPUT_SHAPE, - expectedResolvedValue: "string", - includeInPlan: true, - includeInConfigure: true, - includeInAudit: true, - }, - { - id: "plugins.entries.moonshot.config.webSearch.apiKey", - targetType: "plugins.entries.moonshot.config.webSearch.apiKey", - configFile: "openclaw.json", - pathPattern: "plugins.entries.moonshot.config.webSearch.apiKey", - secretShape: SECRET_INPUT_SHAPE, - expectedResolvedValue: "string", - includeInPlan: true, - includeInConfigure: true, - includeInAudit: true, - }, - { - id: "plugins.entries.perplexity.config.webSearch.apiKey", - targetType: "plugins.entries.perplexity.config.webSearch.apiKey", - configFile: "openclaw.json", - pathPattern: "plugins.entries.perplexity.config.webSearch.apiKey", - secretShape: SECRET_INPUT_SHAPE, - expectedResolvedValue: "string", - includeInPlan: true, - includeInConfigure: true, - includeInAudit: true, - }, - { - id: "plugins.entries.firecrawl.config.webSearch.apiKey", - targetType: "plugins.entries.firecrawl.config.webSearch.apiKey", - configFile: "openclaw.json", - pathPattern: "plugins.entries.firecrawl.config.webSearch.apiKey", - secretShape: SECRET_INPUT_SHAPE, - expectedResolvedValue: "string", - includeInPlan: true, - includeInConfigure: true, - includeInAudit: true, - }, - { - id: "plugins.entries.firecrawl.config.webFetch.apiKey", - targetType: "plugins.entries.firecrawl.config.webFetch.apiKey", - configFile: "openclaw.json", - pathPattern: "plugins.entries.firecrawl.config.webFetch.apiKey", - secretShape: SECRET_INPUT_SHAPE, - expectedResolvedValue: "string", - includeInPlan: true, - includeInConfigure: true, - includeInAudit: true, - }, - { - id: "plugins.entries.tavily.config.webSearch.apiKey", - targetType: "plugins.entries.tavily.config.webSearch.apiKey", - configFile: "openclaw.json", - pathPattern: "plugins.entries.tavily.config.webSearch.apiKey", - secretShape: SECRET_INPUT_SHAPE, - expectedResolvedValue: "string", - includeInPlan: true, - includeInConfigure: true, - includeInAudit: true, - }, - { - id: "plugins.entries.minimax.config.webSearch.apiKey", - targetType: "plugins.entries.minimax.config.webSearch.apiKey", - configFile: "openclaw.json", - pathPattern: "plugins.entries.minimax.config.webSearch.apiKey", - secretShape: SECRET_INPUT_SHAPE, - expectedResolvedValue: "string", - includeInPlan: true, - includeInConfigure: true, - includeInAudit: true, - }, ]; let cachedSecretTargetRegistry: SecretTargetRegistryEntry[] | null = null; @@ -471,6 +423,7 @@ export function getSecretTargetRegistry(): SecretTargetRegistryEntry[] { } cachedSecretTargetRegistry = [ ...CORE_SECRET_TARGET_REGISTRY, + ...listBundledWebProviderSecretTargetRegistryEntries(), ...listChannelSecretTargetRegistryEntries(), ]; return cachedSecretTargetRegistry; diff --git a/src/secrets/target-registry.test.ts b/src/secrets/target-registry.test.ts index 0bec69118af..9f93f28638e 100644 --- a/src/secrets/target-registry.test.ts +++ b/src/secrets/target-registry.test.ts @@ -5,6 +5,7 @@ import { TALK_TEST_PROVIDER_API_KEY_PATH, TALK_TEST_PROVIDER_ID, } from "../test-utils/talk-test-provider.js"; +import { getCoreSecretTargetRegistry } from "./target-registry-data.js"; import { discoverConfigSecretTargetsByIds, resolveConfigSecretTargetByPath, @@ -43,7 +44,11 @@ describe("secret target registry", () => { expect(target).toBeNull(); }); - it("includes exa webSearch api key target path", () => { + it("derives bundled web provider api key target paths from plugin manifests", () => { + const coreTargetIds = new Set(getCoreSecretTargetRegistry().map((entry) => entry.id)); + expect(coreTargetIds.has("plugins.entries.exa.config.webSearch.apiKey")).toBe(false); + expect(coreTargetIds.has("plugins.entries.firecrawl.config.webFetch.apiKey")).toBe(false); + const target = resolveConfigSecretTargetByPath([ "plugins", "entries", @@ -55,5 +60,16 @@ describe("secret target registry", () => { expect(target).not.toBeNull(); expect(target?.entry?.id).toBe("plugins.entries.exa.config.webSearch.apiKey"); + + const fetchTarget = resolveConfigSecretTargetByPath([ + "plugins", + "entries", + "firecrawl", + "config", + "webFetch", + "apiKey", + ]); + expect(fetchTarget).not.toBeNull(); + expect(fetchTarget?.entry?.id).toBe("plugins.entries.firecrawl.config.webFetch.apiKey"); }); });