mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-26 16:06:16 +00:00
Plugins: decouple bundled web search discovery
This commit is contained in:
14
src/plugins/provider-api-key-auth.runtime.ts
Normal file
14
src/plugins/provider-api-key-auth.runtime.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { normalizeApiKeyInput, validateApiKeyInput } from "../commands/auth-choice.api-key.js";
|
||||
import { ensureApiKeyFromOptionEnvOrPrompt } from "../commands/auth-choice.apply-helpers.js";
|
||||
import { applyPrimaryModel } from "../commands/model-picker.js";
|
||||
import { buildApiKeyCredential } from "../commands/onboard-auth.credentials.js";
|
||||
import { applyAuthProfileConfig } from "../commands/onboard-auth.js";
|
||||
|
||||
export {
|
||||
applyAuthProfileConfig,
|
||||
applyPrimaryModel,
|
||||
buildApiKeyCredential,
|
||||
ensureApiKeyFromOptionEnvOrPrompt,
|
||||
normalizeApiKeyInput,
|
||||
validateApiKeyInput,
|
||||
};
|
||||
@@ -1,9 +1,4 @@
|
||||
import { upsertAuthProfile } from "../agents/auth-profiles.js";
|
||||
import { normalizeApiKeyInput, validateApiKeyInput } from "../commands/auth-choice.api-key.js";
|
||||
import { ensureApiKeyFromOptionEnvOrPrompt } from "../commands/auth-choice.apply-helpers.js";
|
||||
import { applyPrimaryModel } from "../commands/model-picker.js";
|
||||
import { buildApiKeyCredential } from "../commands/onboard-auth.credentials.js";
|
||||
import { applyAuthProfileConfig } from "../commands/onboard-auth.js";
|
||||
import { upsertAuthProfile } from "../agents/auth-profiles/profiles.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { SecretInput } from "../config/types.secrets.js";
|
||||
import { normalizeOptionalSecretInput } from "../utils/normalize-secret-input.js";
|
||||
@@ -34,6 +29,15 @@ type ProviderApiKeyAuthMethodOptions = {
|
||||
applyConfig?: (cfg: OpenClawConfig) => OpenClawConfig;
|
||||
};
|
||||
|
||||
let providerApiKeyAuthRuntimePromise:
|
||||
| Promise<typeof import("./provider-api-key-auth.runtime.js")>
|
||||
| undefined;
|
||||
|
||||
function loadProviderApiKeyAuthRuntime() {
|
||||
providerApiKeyAuthRuntimePromise ??= import("./provider-api-key-auth.runtime.js");
|
||||
return providerApiKeyAuthRuntimePromise;
|
||||
}
|
||||
|
||||
function resolveStringOption(opts: Record<string, unknown> | undefined, optionKey: string) {
|
||||
return normalizeOptionalSecretInput(opts?.[optionKey]);
|
||||
}
|
||||
@@ -56,13 +60,14 @@ function resolveProfileIds(params: {
|
||||
return [resolveProfileId(params)];
|
||||
}
|
||||
|
||||
function applyApiKeyConfig(params: {
|
||||
async function applyApiKeyConfig(params: {
|
||||
ctx: ProviderAuthMethodNonInteractiveContext;
|
||||
providerId: string;
|
||||
profileIds: string[];
|
||||
defaultModel?: string;
|
||||
applyConfig?: (cfg: OpenClawConfig) => OpenClawConfig;
|
||||
}) {
|
||||
const { applyAuthProfileConfig, applyPrimaryModel } = await loadProviderApiKeyAuthRuntime();
|
||||
let next = params.ctx.config;
|
||||
for (const profileId of params.profileIds) {
|
||||
next = applyAuthProfileConfig(next, {
|
||||
@@ -92,6 +97,12 @@ export function createProviderApiKeyAuthMethod(
|
||||
let capturedSecretInput: SecretInput | undefined;
|
||||
let capturedCredential = false;
|
||||
let capturedMode: "plaintext" | "ref" | undefined;
|
||||
const {
|
||||
buildApiKeyCredential,
|
||||
ensureApiKeyFromOptionEnvOrPrompt,
|
||||
normalizeApiKeyInput,
|
||||
validateApiKeyInput,
|
||||
} = await loadProviderApiKeyAuthRuntime();
|
||||
|
||||
await ensureApiKeyFromOptionEnvOrPrompt({
|
||||
token: flagValue ?? normalizeOptionalSecretInput(ctx.opts?.token),
|
||||
@@ -171,7 +182,7 @@ export function createProviderApiKeyAuthMethod(
|
||||
}
|
||||
}
|
||||
|
||||
return applyApiKeyConfig({
|
||||
return await applyApiKeyConfig({
|
||||
ctx,
|
||||
providerId: params.providerId,
|
||||
profileIds,
|
||||
|
||||
@@ -1,64 +1,22 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolvePluginWebSearchProviders } from "./web-search-providers.js";
|
||||
|
||||
const loadOpenClawPluginsMock = vi.fn();
|
||||
|
||||
vi.mock("./loader.js", () => ({
|
||||
loadOpenClawPlugins: (...args: unknown[]) => loadOpenClawPluginsMock(...args),
|
||||
}));
|
||||
|
||||
describe("resolvePluginWebSearchProviders", () => {
|
||||
beforeEach(() => {
|
||||
loadOpenClawPluginsMock.mockReset();
|
||||
loadOpenClawPluginsMock.mockReturnValue({
|
||||
webSearchProviders: [
|
||||
{
|
||||
pluginId: "google",
|
||||
provider: {
|
||||
id: "gemini",
|
||||
label: "Gemini",
|
||||
hint: "hint",
|
||||
envVars: ["GEMINI_API_KEY"],
|
||||
placeholder: "AIza...",
|
||||
signupUrl: "https://example.com",
|
||||
autoDetectOrder: 20,
|
||||
},
|
||||
},
|
||||
{
|
||||
pluginId: "brave",
|
||||
provider: {
|
||||
id: "brave",
|
||||
label: "Brave",
|
||||
hint: "hint",
|
||||
envVars: ["BRAVE_API_KEY"],
|
||||
placeholder: "BSA...",
|
||||
signupUrl: "https://example.com",
|
||||
autoDetectOrder: 10,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
it("returns bundled providers in auto-detect order", () => {
|
||||
const providers = resolvePluginWebSearchProviders({});
|
||||
|
||||
it("forwards an explicit env to plugin loading", () => {
|
||||
const env = { OPENCLAW_HOME: "/srv/openclaw-home" } as NodeJS.ProcessEnv;
|
||||
|
||||
const providers = resolvePluginWebSearchProviders({
|
||||
workspaceDir: "/workspace/explicit",
|
||||
env,
|
||||
});
|
||||
|
||||
expect(providers.map((provider) => provider.id)).toEqual(["brave", "gemini"]);
|
||||
expect(loadOpenClawPluginsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
workspaceDir: "/workspace/explicit",
|
||||
env,
|
||||
}),
|
||||
);
|
||||
expect(providers.map((provider) => `${provider.pluginId}:${provider.id}`)).toEqual([
|
||||
"brave:brave",
|
||||
"google:gemini",
|
||||
"xai:grok",
|
||||
"moonshot:kimi",
|
||||
"perplexity:perplexity",
|
||||
"firecrawl:firecrawl",
|
||||
]);
|
||||
});
|
||||
|
||||
it("can augment restrictive allowlists for bundled compatibility", () => {
|
||||
resolvePluginWebSearchProviders({
|
||||
const providers = resolvePluginWebSearchProviders({
|
||||
config: {
|
||||
plugins: {
|
||||
allow: ["openrouter"],
|
||||
@@ -67,49 +25,30 @@ describe("resolvePluginWebSearchProviders", () => {
|
||||
bundledAllowlistCompat: true,
|
||||
});
|
||||
|
||||
expect(loadOpenClawPluginsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
config: expect.objectContaining({
|
||||
plugins: expect.objectContaining({
|
||||
allow: expect.arrayContaining(["openrouter", "brave", "perplexity"]),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(providers.map((provider) => provider.pluginId)).toEqual([
|
||||
"brave",
|
||||
"google",
|
||||
"xai",
|
||||
"moonshot",
|
||||
"perplexity",
|
||||
"firecrawl",
|
||||
]);
|
||||
});
|
||||
|
||||
it("auto-enables bundled web search provider plugins when entries are missing", () => {
|
||||
resolvePluginWebSearchProviders({
|
||||
it("does not return bundled providers excluded by a restrictive allowlist without compat", () => {
|
||||
const providers = resolvePluginWebSearchProviders({
|
||||
config: {
|
||||
plugins: {
|
||||
entries: {
|
||||
openrouter: { enabled: true },
|
||||
},
|
||||
allow: ["openrouter"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(loadOpenClawPluginsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
config: expect.objectContaining({
|
||||
plugins: expect.objectContaining({
|
||||
entries: expect.objectContaining({
|
||||
openrouter: { enabled: true },
|
||||
brave: { enabled: true },
|
||||
firecrawl: { enabled: true },
|
||||
google: { enabled: true },
|
||||
moonshot: { enabled: true },
|
||||
perplexity: { enabled: true },
|
||||
xai: { enabled: true },
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(providers).toEqual([]);
|
||||
});
|
||||
|
||||
it("preserves explicit bundled provider entry state", () => {
|
||||
resolvePluginWebSearchProviders({
|
||||
const providers = resolvePluginWebSearchProviders({
|
||||
config: {
|
||||
plugins: {
|
||||
entries: {
|
||||
@@ -119,16 +58,18 @@ describe("resolvePluginWebSearchProviders", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(loadOpenClawPluginsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
config: expect.objectContaining({
|
||||
plugins: expect.objectContaining({
|
||||
entries: expect.objectContaining({
|
||||
perplexity: { enabled: false },
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(providers.map((provider) => provider.pluginId)).not.toContain("perplexity");
|
||||
});
|
||||
|
||||
it("returns no providers when plugins are globally disabled", () => {
|
||||
const providers = resolvePluginWebSearchProviders({
|
||||
config: {
|
||||
plugins: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(providers).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { createFirecrawlWebSearchProvider } from "../../extensions/firecrawl/src/firecrawl-search-provider.js";
|
||||
import {
|
||||
createPluginBackedWebSearchProvider,
|
||||
getScopedCredentialValue,
|
||||
getTopLevelCredentialValue,
|
||||
setScopedCredentialValue,
|
||||
setTopLevelCredentialValue,
|
||||
} from "../agents/tools/web-search-plugin-factory.js";
|
||||
import {
|
||||
withBundledPluginAllowlistCompat,
|
||||
withBundledPluginEnablementCompat,
|
||||
} from "./bundled-compat.js";
|
||||
import { loadOpenClawPlugins, type PluginLoadOptions } from "./loader.js";
|
||||
import { createPluginLoaderLogger } from "./logger.js";
|
||||
import { normalizePluginsConfig, resolveEffectiveEnableState } from "./config-state.js";
|
||||
import type { PluginLoadOptions } from "./loader.js";
|
||||
import type { WebSearchProviderPlugin } from "./types.js";
|
||||
|
||||
const log = createSubsystemLogger("plugins");
|
||||
|
||||
const BUNDLED_WEB_SEARCH_ALLOWLIST_COMPAT_PLUGIN_IDS = [
|
||||
"brave",
|
||||
"firecrawl",
|
||||
@@ -18,6 +23,92 @@ const BUNDLED_WEB_SEARCH_ALLOWLIST_COMPAT_PLUGIN_IDS = [
|
||||
"xai",
|
||||
] as const;
|
||||
|
||||
const BUNDLED_WEB_SEARCH_PROVIDER_REGISTRY = [
|
||||
{
|
||||
pluginId: "brave",
|
||||
provider: createPluginBackedWebSearchProvider({
|
||||
id: "brave",
|
||||
label: "Brave Search",
|
||||
hint: "Structured results · country/language/time filters",
|
||||
envVars: ["BRAVE_API_KEY"],
|
||||
placeholder: "BSA...",
|
||||
signupUrl: "https://brave.com/search/api/",
|
||||
docsUrl: "https://docs.openclaw.ai/brave-search",
|
||||
autoDetectOrder: 10,
|
||||
getCredentialValue: getTopLevelCredentialValue,
|
||||
setCredentialValue: setTopLevelCredentialValue,
|
||||
}),
|
||||
},
|
||||
{
|
||||
pluginId: "google",
|
||||
provider: createPluginBackedWebSearchProvider({
|
||||
id: "gemini",
|
||||
label: "Gemini (Google Search)",
|
||||
hint: "Google Search grounding · AI-synthesized",
|
||||
envVars: ["GEMINI_API_KEY"],
|
||||
placeholder: "AIza...",
|
||||
signupUrl: "https://aistudio.google.com/apikey",
|
||||
docsUrl: "https://docs.openclaw.ai/tools/web",
|
||||
autoDetectOrder: 20,
|
||||
getCredentialValue: (searchConfig) => getScopedCredentialValue(searchConfig, "gemini"),
|
||||
setCredentialValue: (searchConfigTarget, value) =>
|
||||
setScopedCredentialValue(searchConfigTarget, "gemini", value),
|
||||
}),
|
||||
},
|
||||
{
|
||||
pluginId: "xai",
|
||||
provider: createPluginBackedWebSearchProvider({
|
||||
id: "grok",
|
||||
label: "Grok (xAI)",
|
||||
hint: "xAI web-grounded responses",
|
||||
envVars: ["XAI_API_KEY"],
|
||||
placeholder: "xai-...",
|
||||
signupUrl: "https://console.x.ai/",
|
||||
docsUrl: "https://docs.openclaw.ai/tools/web",
|
||||
autoDetectOrder: 30,
|
||||
getCredentialValue: (searchConfig) => getScopedCredentialValue(searchConfig, "grok"),
|
||||
setCredentialValue: (searchConfigTarget, value) =>
|
||||
setScopedCredentialValue(searchConfigTarget, "grok", value),
|
||||
}),
|
||||
},
|
||||
{
|
||||
pluginId: "moonshot",
|
||||
provider: createPluginBackedWebSearchProvider({
|
||||
id: "kimi",
|
||||
label: "Kimi (Moonshot)",
|
||||
hint: "Moonshot web search",
|
||||
envVars: ["KIMI_API_KEY", "MOONSHOT_API_KEY"],
|
||||
placeholder: "sk-...",
|
||||
signupUrl: "https://platform.moonshot.cn/",
|
||||
docsUrl: "https://docs.openclaw.ai/tools/web",
|
||||
autoDetectOrder: 40,
|
||||
getCredentialValue: (searchConfig) => getScopedCredentialValue(searchConfig, "kimi"),
|
||||
setCredentialValue: (searchConfigTarget, value) =>
|
||||
setScopedCredentialValue(searchConfigTarget, "kimi", value),
|
||||
}),
|
||||
},
|
||||
{
|
||||
pluginId: "perplexity",
|
||||
provider: createPluginBackedWebSearchProvider({
|
||||
id: "perplexity",
|
||||
label: "Perplexity Search",
|
||||
hint: "Structured results · domain/country/language/time filters",
|
||||
envVars: ["PERPLEXITY_API_KEY", "OPENROUTER_API_KEY"],
|
||||
placeholder: "pplx-...",
|
||||
signupUrl: "https://www.perplexity.ai/settings/api",
|
||||
docsUrl: "https://docs.openclaw.ai/perplexity",
|
||||
autoDetectOrder: 50,
|
||||
getCredentialValue: (searchConfig) => getScopedCredentialValue(searchConfig, "perplexity"),
|
||||
setCredentialValue: (searchConfigTarget, value) =>
|
||||
setScopedCredentialValue(searchConfigTarget, "perplexity", value),
|
||||
}),
|
||||
},
|
||||
{
|
||||
pluginId: "firecrawl",
|
||||
provider: createFirecrawlWebSearchProvider(),
|
||||
},
|
||||
] as const;
|
||||
|
||||
export function resolvePluginWebSearchProviders(params: {
|
||||
config?: PluginLoadOptions["config"];
|
||||
workspaceDir?: string;
|
||||
@@ -34,17 +125,17 @@ export function resolvePluginWebSearchProviders(params: {
|
||||
config: allowlistCompat,
|
||||
pluginIds: BUNDLED_WEB_SEARCH_ALLOWLIST_COMPAT_PLUGIN_IDS,
|
||||
});
|
||||
const registry = loadOpenClawPlugins({
|
||||
config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
logger: createPluginLoaderLogger(log),
|
||||
activate: false,
|
||||
cache: false,
|
||||
onlyPluginIds: [...BUNDLED_WEB_SEARCH_ALLOWLIST_COMPAT_PLUGIN_IDS],
|
||||
});
|
||||
const normalizedPlugins = normalizePluginsConfig(config?.plugins);
|
||||
|
||||
return registry.webSearchProviders
|
||||
return BUNDLED_WEB_SEARCH_PROVIDER_REGISTRY.filter(
|
||||
({ pluginId }) =>
|
||||
resolveEffectiveEnableState({
|
||||
id: pluginId,
|
||||
origin: "bundled",
|
||||
config: normalizedPlugins,
|
||||
rootConfig: config,
|
||||
}).enabled,
|
||||
)
|
||||
.map((entry) => ({
|
||||
...entry.provider,
|
||||
pluginId: entry.pluginId,
|
||||
|
||||
Reference in New Issue
Block a user