refactor: derive web credential secret targets from manifests

This commit is contained in:
Peter Steinberger
2026-04-22 05:03:55 +01:00
parent de616055f7
commit bc9c2cc162
3 changed files with 82 additions and 112 deletions

View File

@@ -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/,
],
},
{

View File

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

View File

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