diff --git a/CHANGELOG.md b/CHANGELOG.md index f48245320ec..04010308c61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ Docs: https://docs.openclaw.ai - Agents/models: keep legacy CLI runtime model refs such as `claude-cli/*` in the configured allowlist after canonical runtime migration, so cron `payload.model` overrides keep working. Fixes #75753. Thanks @RyanSandoval. - Plugin SDK: re-export `isPrivateIpAddress` from `plugin-sdk/ssrf-runtime`, restoring source-checkout builds for SearXNG and Firecrawl private-network guards. Thanks @vincentkoc. - CLI/directory: report unsupported directory operations for installed channel plugins instead of prompting to reinstall the plugin when it lacks a directory adapter. Fixes #75770. Thanks @lawong888. +- Web search/SearXNG: show the JSON API `search.formats` prerequisite during SearXNG setup before prompting for the base URL. Supersedes #65592. Thanks @evanpaul14. - Web search: keep public provider requests on the strict SSRF guard and reserve private-network access for explicit self-hosted SearXNG/Firecrawl endpoints. Fixes #74357 and supersedes #74360. Thanks @fede-kamel. - Web search/Firecrawl: allow self-hosted private/internal Firecrawl `baseUrl` endpoints, including HTTP for private targets, while keeping hosted Firecrawl on the strict official endpoint. Fixes #63877 and supersedes #59666, #63941, and #74013. Thanks @jhthompson12, @jzakirov, @Mlightsnow, and @shad0wca7. - Providers/OpenRouter: strip trailing assistant prefill turns from verified OpenRouter Anthropic model requests when reasoning is enabled, so Claude 4.6 routes no longer fail with Anthropic's prefill rejection through the OpenAI-compatible adapter. Fixes #75395. Thanks @sbmilburn. diff --git a/extensions/searxng/src/searxng-search-provider.test.ts b/extensions/searxng/src/searxng-search-provider.test.ts index c7b1cc0c8ad..20db0fdf142 100644 --- a/extensions/searxng/src/searxng-search-provider.test.ts +++ b/extensions/searxng/src/searxng-search-provider.test.ts @@ -145,6 +145,13 @@ describe("searxng web search provider", () => { expect(resolveSearxngLanguage(config)).toBe("de"); }); + it("exposes a credentialNote with JSON format guidance", () => { + const provider = createSearxngWebSearchProvider(); + + expect(provider.credentialNote).toContain("json format enabled"); + expect(provider.credentialNote).toContain("search.formats"); + }); + it("persists base URL to plugin config via setConfiguredCredentialValue", () => { const provider = createSearxngWebSearchProvider(); const config = {} as Record; diff --git a/extensions/searxng/src/searxng-search-provider.ts b/extensions/searxng/src/searxng-search-provider.ts index b68aec488ee..a47f400d8d3 100644 --- a/extensions/searxng/src/searxng-search-provider.ts +++ b/extensions/searxng/src/searxng-search-provider.ts @@ -56,6 +56,10 @@ export function createSearxngWebSearchProvider(): WebSearchProviderPlugin { configuredCredential: { pluginId: "searxng", field: "baseUrl" }, selectionPluginId: "searxng", }), + credentialNote: [ + "For the SearXNG JSON API to work, make sure your SearXNG instance", + "has the json format enabled in its settings.yml under search.formats.", + ].join("\n"), createTool: (ctx) => ({ description: "Search the web using a self-hosted SearXNG instance. Returns titles, URLs, and snippets.", diff --git a/src/flows/search-setup.test.ts b/src/flows/search-setup.test.ts index f930927f67e..6147f3bab6e 100644 --- a/src/flows/search-setup.test.ts +++ b/src/flows/search-setup.test.ts @@ -16,6 +16,7 @@ const mockGrokProvider = vi.hoisted(() => ({ envVars: ["XAI_API_KEY"], onboardingScopes: ["text-inference"], credentialPath: "plugins.entries.xai.config.webSearch.apiKey", + credentialNote: "Configure Grok web search prerequisites before entering the credential.", getCredentialValue: (search?: Record) => search?.apiKey, setCredentialValue: (searchConfigTarget: Record, value: unknown) => { searchConfigTarget.apiKey = value; @@ -128,6 +129,79 @@ describe("runSearchSetupFlow", () => { }); }); + it("shows provider credential notes before plaintext credential prompts", async () => { + const select = vi.fn().mockResolvedValueOnce("grok").mockResolvedValueOnce("no"); + const text = vi.fn().mockResolvedValue("xai-test-key"); + const note = vi.fn(async () => {}); + const prompter = createWizardPrompter({ + note: note as never, + select: select as never, + text: text as never, + }); + + await runSearchSetupFlow({ plugins: { allow: ["xai"] } }, createNonExitingRuntime(), prompter); + + expect(note).toHaveBeenCalledWith(mockGrokProvider.credentialNote, mockGrokProvider.label); + expect(text).toHaveBeenCalledTimes(1); + expect(note.mock.invocationCallOrder[1]).toBeLessThan(text.mock.invocationCallOrder[0]); + }); + + it("shows provider credential notes before SecretRef setup notes", async () => { + const select = vi.fn().mockResolvedValueOnce("grok").mockResolvedValueOnce("no"); + const note = vi.fn(async () => {}); + const prompter = createWizardPrompter({ + note: note as never, + select: select as never, + }); + + await runSearchSetupFlow({ plugins: { allow: ["xai"] } }, createNonExitingRuntime(), prompter, { + secretInputMode: "ref", + }); + + expect(note).toHaveBeenNthCalledWith( + 2, + mockGrokProvider.credentialNote, + mockGrokProvider.label, + ); + expect(note).toHaveBeenNthCalledWith( + 3, + expect.stringContaining("Secret references enabled"), + "Web search", + ); + }); + + it("skips provider credential notes in quickstart fast path", async () => { + const select = vi.fn().mockResolvedValueOnce("grok").mockResolvedValueOnce("no"); + const note = vi.fn(async () => {}); + const prompter = createWizardPrompter({ + note: note as never, + select: select as never, + }); + + await runSearchSetupFlow( + { + plugins: { + allow: ["xai"], + entries: { + xai: { + enabled: true, + config: { + webSearch: { + apiKey: "xai-test-key", + }, + }, + }, + }, + }, + }, + createNonExitingRuntime(), + prompter, + { quickstartDefaults: true }, + ); + + expect(note).not.toHaveBeenCalledWith(mockGrokProvider.credentialNote, mockGrokProvider.label); + }); + it("preserves disabled web_search state while still allowing provider-owned x_search setup", async () => { const select = vi .fn() diff --git a/src/flows/search-setup.ts b/src/flows/search-setup.ts index 947911482a0..13c8cece965 100644 --- a/src/flows/search-setup.ts +++ b/src/flows/search-setup.ts @@ -434,6 +434,10 @@ export async function runSearchSetupFlow( }); } + if (entry.credentialNote) { + await prompter.note(entry.credentialNote, entry.label); + } + const useSecretRefMode = opts?.secretInputMode === "ref"; // pragma: allowlist secret if (useSecretRefMode) { if (keyConfigured) { diff --git a/src/plugins/web-provider-types.ts b/src/plugins/web-provider-types.ts index 703ad31a5a4..380cebbb698 100644 --- a/src/plugins/web-provider-types.ts +++ b/src/plugins/web-provider-types.ts @@ -92,6 +92,8 @@ export type WebSearchProviderPlugin = { placeholder: string; signupUrl: string; docsUrl?: string; + /** Optional note shown before credential collection for provider-specific prerequisites. */ + credentialNote?: string; autoDetectOrder?: number; credentialPath: string; inactiveSecretPaths?: string[];