refactor web search config ownership into extensions

This commit is contained in:
Tak Hoffman
2026-03-17 23:29:52 -05:00
parent 2fbf2c0a47
commit 112d1d3a7c
29 changed files with 856 additions and 304 deletions

View File

@@ -11,6 +11,19 @@ function asConfig(value: unknown): OpenClawConfig {
return value as OpenClawConfig;
}
function providerPluginId(provider: ProviderUnderTest): string {
switch (provider) {
case "gemini":
return "google";
case "grok":
return "xai";
case "kimi":
return "moonshot";
default:
return provider;
}
}
async function runRuntimeWebTools(params: { config: OpenClawConfig; env?: NodeJS.ProcessEnv }) {
const sourceConfig = structuredClone(params.config);
const resolvedConfig = structuredClone(params.config);
@@ -30,40 +43,32 @@ function createProviderSecretRefConfig(
provider: ProviderUnderTest,
envRefId: string,
): OpenClawConfig {
const search: Record<string, unknown> = {
enabled: true,
provider,
};
if (provider === "brave") {
search.apiKey = { source: "env", provider: "default", id: envRefId };
} else {
search[provider] = {
apiKey: { source: "env", provider: "default", id: envRefId },
};
}
return asConfig({
tools: {
web: {
search,
search: {
enabled: true,
provider,
},
},
},
plugins: {
entries: {
[providerPluginId(provider)]: {
enabled: true,
config: {
webSearch: {
apiKey: { source: "env", provider: "default", id: envRefId },
},
},
},
},
},
});
}
function readProviderKey(config: OpenClawConfig, provider: ProviderUnderTest): unknown {
if (provider === "brave") {
return config.tools?.web?.search?.apiKey;
}
if (provider === "gemini") {
return config.tools?.web?.search?.gemini?.apiKey;
}
if (provider === "grok") {
return config.tools?.web?.search?.grok?.apiKey;
}
if (provider === "kimi") {
return config.tools?.web?.search?.kimi?.apiKey;
}
return config.tools?.web?.search?.perplexity?.apiKey;
return config.plugins?.entries?.[providerPluginId(provider)]?.config?.webSearch?.apiKey;
}
function expectInactiveFirecrawlSecretRef(params: {
@@ -171,18 +176,40 @@ describe("runtime web tools resolution", () => {
tools: {
web: {
search: {
apiKey: { source: "env", provider: "default", id: "BRAVE_REF" },
gemini: {
apiKey: { source: "env", provider: "default", id: "GEMINI_REF" },
enabled: true,
},
},
},
plugins: {
entries: {
brave: {
enabled: true,
config: {
webSearch: { apiKey: { source: "env", provider: "default", id: "BRAVE_REF" } },
},
grok: {
apiKey: { source: "env", provider: "default", id: "GROK_REF" },
},
google: {
enabled: true,
config: {
webSearch: { apiKey: { source: "env", provider: "default", id: "GEMINI_REF" } },
},
kimi: {
apiKey: { source: "env", provider: "default", id: "KIMI_REF" },
},
xai: {
enabled: true,
config: {
webSearch: { apiKey: { source: "env", provider: "default", id: "GROK_REF" } },
},
perplexity: {
apiKey: { source: "env", provider: "default", id: "PERPLEXITY_REF" },
},
moonshot: {
enabled: true,
config: {
webSearch: { apiKey: { source: "env", provider: "default", id: "KIMI_REF" } },
},
},
perplexity: {
enabled: true,
config: {
webSearch: { apiKey: { source: "env", provider: "default", id: "PERPLEXITY_REF" } },
},
},
},
@@ -199,13 +226,13 @@ describe("runtime web tools resolution", () => {
expect(metadata.search.providerSource).toBe("auto-detect");
expect(metadata.search.selectedProvider).toBe("brave");
expect(resolvedConfig.tools?.web?.search?.apiKey).toBe("brave-precedence-key");
expect(readProviderKey(resolvedConfig, "brave")).toBe("brave-precedence-key");
expect(context.warnings).toEqual(
expect.arrayContaining([
expect.objectContaining({ path: "tools.web.search.gemini.apiKey" }),
expect.objectContaining({ path: "tools.web.search.grok.apiKey" }),
expect.objectContaining({ path: "tools.web.search.kimi.apiKey" }),
expect.objectContaining({ path: "tools.web.search.perplexity.apiKey" }),
expect.objectContaining({ path: "plugins.entries.google.config.webSearch.apiKey" }),
expect.objectContaining({ path: "plugins.entries.xai.config.webSearch.apiKey" }),
expect.objectContaining({ path: "plugins.entries.moonshot.config.webSearch.apiKey" }),
expect.objectContaining({ path: "plugins.entries.perplexity.config.webSearch.apiKey" }),
]),
);
});
@@ -216,12 +243,25 @@ describe("runtime web tools resolution", () => {
tools: {
web: {
search: {
apiKey: { source: "env", provider: "default", id: "BRAVE_API_KEY_REF" },
gemini: {
apiKey: {
source: "env",
provider: "default",
id: "MISSING_GEMINI_API_KEY_REF",
enabled: true,
},
},
},
plugins: {
entries: {
brave: {
enabled: true,
config: {
webSearch: {
apiKey: { source: "env", provider: "default", id: "BRAVE_API_KEY_REF" },
},
},
},
google: {
enabled: true,
config: {
webSearch: {
apiKey: { source: "env", provider: "default", id: "MISSING_GEMINI_API_KEY_REF" },
},
},
},
@@ -236,8 +276,8 @@ describe("runtime web tools resolution", () => {
expect(metadata.search.providerSource).toBe("auto-detect");
expect(metadata.search.selectedProvider).toBe("brave");
expect(metadata.search.selectedProviderKeySource).toBe("secretRef");
expect(resolvedConfig.tools?.web?.search?.apiKey).toBe("brave-runtime-key");
expect(resolvedConfig.tools?.web?.search?.gemini?.apiKey).toEqual({
expect(readProviderKey(resolvedConfig, "brave")).toBe("brave-runtime-key");
expect(readProviderKey(resolvedConfig, "gemini")).toEqual({
source: "env",
provider: "default",
id: "MISSING_GEMINI_API_KEY_REF",
@@ -246,7 +286,7 @@ describe("runtime web tools resolution", () => {
expect.arrayContaining([
expect.objectContaining({
code: "SECRETS_REF_IGNORED_INACTIVE_SURFACE",
path: "tools.web.search.gemini.apiKey",
path: "plugins.entries.google.config.webSearch.apiKey",
}),
]),
);
@@ -261,9 +301,26 @@ describe("runtime web tools resolution", () => {
tools: {
web: {
search: {
apiKey: { source: "env", provider: "default", id: "MISSING_BRAVE_API_KEY_REF" },
gemini: {
apiKey: { source: "env", provider: "default", id: "GEMINI_API_KEY_REF" },
enabled: true,
},
},
},
plugins: {
entries: {
brave: {
enabled: true,
config: {
webSearch: {
apiKey: { source: "env", provider: "default", id: "MISSING_BRAVE_API_KEY_REF" },
},
},
},
google: {
enabled: true,
config: {
webSearch: {
apiKey: { source: "env", provider: "default", id: "GEMINI_API_KEY_REF" },
},
},
},
},
@@ -276,12 +333,12 @@ describe("runtime web tools resolution", () => {
expect(metadata.search.providerSource).toBe("auto-detect");
expect(metadata.search.selectedProvider).toBe("gemini");
expect(resolvedConfig.tools?.web?.search?.gemini?.apiKey).toBe("gemini-runtime-key");
expect(readProviderKey(resolvedConfig, "gemini")).toBe("gemini-runtime-key");
expect(context.warnings).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: "SECRETS_REF_IGNORED_INACTIVE_SURFACE",
path: "tools.web.search.apiKey",
path: "plugins.entries.brave.config.webSearch.apiKey",
}),
]),
);
@@ -297,8 +354,17 @@ describe("runtime web tools resolution", () => {
web: {
search: {
provider: "invalid-provider",
gemini: {
apiKey: { source: "env", provider: "default", id: "GEMINI_API_KEY_REF" },
},
},
},
plugins: {
entries: {
google: {
enabled: true,
config: {
webSearch: {
apiKey: { source: "env", provider: "default", id: "GEMINI_API_KEY_REF" },
},
},
},
},
@@ -312,7 +378,7 @@ describe("runtime web tools resolution", () => {
expect(metadata.search.providerConfigured).toBeUndefined();
expect(metadata.search.providerSource).toBe("auto-detect");
expect(metadata.search.selectedProvider).toBe("gemini");
expect(resolvedConfig.tools?.web?.search?.gemini?.apiKey).toBe("gemini-runtime-key");
expect(readProviderKey(resolvedConfig, "gemini")).toBe("gemini-runtime-key");
expect(metadata.search.diagnostics).toEqual(
expect.arrayContaining([
expect.objectContaining({
@@ -337,8 +403,17 @@ describe("runtime web tools resolution", () => {
web: {
search: {
provider: "gemini",
gemini: {
apiKey: { source: "env", provider: "default", id: "MISSING_GEMINI_API_KEY_REF" },
},
},
},
plugins: {
entries: {
google: {
enabled: true,
config: {
webSearch: {
apiKey: { source: "env", provider: "default", id: "MISSING_GEMINI_API_KEY_REF" },
},
},
},
},
@@ -361,7 +436,7 @@ describe("runtime web tools resolution", () => {
expect.arrayContaining([
expect.objectContaining({
code: "WEB_SEARCH_KEY_UNRESOLVED_NO_FALLBACK",
path: "tools.web.search.gemini.apiKey",
path: "plugins.entries.google.config.webSearch.apiKey",
}),
]),
);

View File

@@ -221,6 +221,9 @@ function setResolvedWebSearchApiKey(params: {
env: params.env,
bundledAllowlistCompat: true,
}).find((entry) => entry.id === params.provider);
if (provider?.setConfiguredCredentialValue) {
provider.setConfiguredCredentialValue(params.resolvedConfig, params.value);
}
provider?.setCredentialValue(search, params.value);
}
@@ -318,7 +321,9 @@ export async function resolveRuntimeWebTools(params: {
for (const provider of candidates) {
const path = keyPathForProvider(provider);
const value = provider.getCredentialValue(search);
const value =
provider.getConfiguredCredentialValue?.(params.sourceConfig) ??
provider.getCredentialValue(search);
const resolution = await resolveSecretInputWithEnvFallback({
sourceConfig: params.sourceConfig,
context: params.context,
@@ -451,7 +456,9 @@ export async function resolveRuntimeWebTools(params: {
if (provider.id === searchMetadata.selectedProvider) {
continue;
}
const value = provider.getCredentialValue(search);
const value =
provider.getConfiguredCredentialValue?.(params.sourceConfig) ??
provider.getCredentialValue(search);
if (!hasConfiguredSecretRef(value, defaults)) {
continue;
}
@@ -465,7 +472,9 @@ export async function resolveRuntimeWebTools(params: {
}
} else if (search && !searchEnabled) {
for (const provider of providers) {
const value = provider.getCredentialValue(search);
const value =
provider.getConfiguredCredentialValue?.(params.sourceConfig) ??
provider.getCredentialValue(search);
if (!hasConfiguredSecretRef(value, defaults)) {
continue;
}
@@ -484,7 +493,9 @@ export async function resolveRuntimeWebTools(params: {
if (provider.id === configuredProvider) {
continue;
}
const value = provider.getCredentialValue(search);
const value =
provider.getConfiguredCredentialValue?.(params.sourceConfig) ??
provider.getCredentialValue(search);
if (!hasConfiguredSecretRef(value, defaults)) {
continue;
}

View File

@@ -733,6 +733,17 @@ const 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: "tools.web.search.gemini.apiKey",
targetType: "tools.web.search.gemini.apiKey",
@@ -744,6 +755,17 @@ const SECRET_TARGET_REGISTRY: SecretTargetRegistryEntry[] = [
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: "tools.web.search.grok.apiKey",
targetType: "tools.web.search.grok.apiKey",
@@ -755,6 +777,17 @@ const SECRET_TARGET_REGISTRY: SecretTargetRegistryEntry[] = [
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: "tools.web.search.kimi.apiKey",
targetType: "tools.web.search.kimi.apiKey",
@@ -766,6 +799,17 @@ const SECRET_TARGET_REGISTRY: SecretTargetRegistryEntry[] = [
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: "tools.web.search.perplexity.apiKey",
targetType: "tools.web.search.perplexity.apiKey",
@@ -777,6 +821,28 @@ const SECRET_TARGET_REGISTRY: SecretTargetRegistryEntry[] = [
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,
},
];
export { SECRET_TARGET_REGISTRY };