mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:20:43 +00:00
Secrets: avoid broad web search discovery for single plugin config
Add an Exa web-search contract artifact and use single bundled plugin-scoped webSearch config as a provider hint. This keeps runtime secret resolution on metadata-only surfaces instead of importing full provider tool implementations.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createExaWebSearchProvider as createContractExaWebSearchProvider } from "../web-search-contract-api.js";
|
||||
import { __testing, createExaWebSearchProvider } from "./exa-web-search-provider.js";
|
||||
|
||||
describe("exa web search provider", () => {
|
||||
@@ -15,6 +16,31 @@ describe("exa web search provider", () => {
|
||||
expect(applied.plugins?.entries?.exa?.enabled).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps the lightweight contract surface aligned with provider metadata", () => {
|
||||
const provider = createExaWebSearchProvider();
|
||||
const contractProvider = createContractExaWebSearchProvider();
|
||||
if (!contractProvider.applySelectionConfig) {
|
||||
throw new Error("Expected contract applySelectionConfig to be defined");
|
||||
}
|
||||
const applied = contractProvider.applySelectionConfig({});
|
||||
|
||||
expect(contractProvider).toMatchObject({
|
||||
id: provider.id,
|
||||
label: provider.label,
|
||||
hint: provider.hint,
|
||||
onboardingScopes: provider.onboardingScopes,
|
||||
credentialLabel: provider.credentialLabel,
|
||||
envVars: provider.envVars,
|
||||
placeholder: provider.placeholder,
|
||||
signupUrl: provider.signupUrl,
|
||||
docsUrl: provider.docsUrl,
|
||||
autoDetectOrder: provider.autoDetectOrder,
|
||||
credentialPath: provider.credentialPath,
|
||||
});
|
||||
expect(contractProvider.createTool({ config: {}, searchConfig: {} })).toBeNull();
|
||||
expect(applied.plugins?.entries?.exa?.enabled).toBe(true);
|
||||
});
|
||||
|
||||
it("prefers scoped configured api keys over environment fallbacks", () => {
|
||||
expect(__testing.resolveExaApiKey({ apiKey: "exa-secret" })).toBe("exa-secret");
|
||||
});
|
||||
|
||||
29
extensions/exa/web-search-contract-api.ts
Normal file
29
extensions/exa/web-search-contract-api.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import {
|
||||
createWebSearchProviderContractFields,
|
||||
type WebSearchProviderPlugin,
|
||||
} from "openclaw/plugin-sdk/provider-web-search-contract";
|
||||
|
||||
export function createExaWebSearchProvider(): WebSearchProviderPlugin {
|
||||
const credentialPath = "plugins.entries.exa.config.webSearch.apiKey";
|
||||
|
||||
return {
|
||||
id: "exa",
|
||||
label: "Exa Search",
|
||||
hint: "Neural + keyword search with date filters and content extraction",
|
||||
onboardingScopes: ["text-inference"],
|
||||
credentialLabel: "Exa API key",
|
||||
envVars: ["EXA_API_KEY"],
|
||||
placeholder: "exa-...",
|
||||
signupUrl: "https://exa.ai/",
|
||||
docsUrl: "https://docs.openclaw.ai/tools/web",
|
||||
autoDetectOrder: 65,
|
||||
credentialPath,
|
||||
...createWebSearchProviderContractFields({
|
||||
credentialPath,
|
||||
searchCredential: { type: "scoped", scopeId: "exa" },
|
||||
configuredCredential: { pluginId: "exa" },
|
||||
selectionPluginId: "exa",
|
||||
}),
|
||||
createTool: () => null,
|
||||
};
|
||||
}
|
||||
@@ -871,6 +871,36 @@ describe("runtime web tools resolution", () => {
|
||||
expect(resolvePluginWebSearchProvidersMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses single plugin-scoped web search config as a bundled provider hint", async () => {
|
||||
const { metadata } = await runRuntimeWebTools({
|
||||
config: asConfig({
|
||||
plugins: {
|
||||
entries: {
|
||||
google: {
|
||||
enabled: true,
|
||||
config: {
|
||||
webSearch: {
|
||||
apiKey: { source: "env", provider: "default", id: "GOOGLE_PROVIDER_REF" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: {
|
||||
GOOGLE_PROVIDER_REF: "google-provider-key",
|
||||
},
|
||||
});
|
||||
|
||||
expect(metadata.search.selectedProvider).toBe("gemini");
|
||||
expect(resolveBundledExplicitWebSearchProvidersFromPublicArtifactsMock).toHaveBeenCalledWith({
|
||||
onlyPluginIds: ["google"],
|
||||
});
|
||||
expect(resolveManifestContractOwnerPluginIdMock).not.toHaveBeenCalled();
|
||||
expect(resolveBundledWebSearchProvidersFromPublicArtifactsMock).not.toHaveBeenCalled();
|
||||
expect(resolvePluginWebSearchProvidersMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("limits legacy top-level web search apiKey auto-detect to compatibility owners", async () => {
|
||||
const { metadata } = await runRuntimeWebTools({
|
||||
config: asConfig({
|
||||
|
||||
@@ -542,18 +542,18 @@ export async function resolveRuntimeWebTools(params: {
|
||||
}
|
||||
const rawProvider = normalizeLowercaseStringOrEmpty(search?.provider);
|
||||
let configuredBundledWebSearchPluginIdHint: string | undefined;
|
||||
if (rawProvider && hasPluginWebSearchConfig) {
|
||||
configuredBundledWebSearchPluginIdHint = inferExactBundledPluginScopedWebToolConfigOwner({
|
||||
config: params.sourceConfig,
|
||||
key: "webSearch",
|
||||
pluginId: rawProvider,
|
||||
});
|
||||
if (!configuredBundledWebSearchPluginIdHint && !(await getHasCustomWebSearchRisk())) {
|
||||
configuredBundledWebSearchPluginIdHint = inferSingleBundledPluginScopedWebToolConfigOwner(
|
||||
params.sourceConfig,
|
||||
"webSearch",
|
||||
);
|
||||
if (hasPluginWebSearchConfig && !(await getHasCustomWebSearchRisk())) {
|
||||
if (rawProvider) {
|
||||
configuredBundledWebSearchPluginIdHint = inferExactBundledPluginScopedWebToolConfigOwner({
|
||||
config: params.sourceConfig,
|
||||
key: "webSearch",
|
||||
pluginId: rawProvider,
|
||||
});
|
||||
}
|
||||
configuredBundledWebSearchPluginIdHint ??= inferSingleBundledPluginScopedWebToolConfigOwner(
|
||||
params.sourceConfig,
|
||||
"webSearch",
|
||||
);
|
||||
}
|
||||
const searchMetadata: RuntimeWebSearchMetadata = {
|
||||
providerSource: "none",
|
||||
|
||||
Reference in New Issue
Block a user