feat(webui): add BYOK web search settings

Let WebUI users configure the single web search provider credential from BYOK while keeping saved secrets masked and hot-reloaded for new searches.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Xubin Ren
2026-05-09 14:52:48 +08:00
committed by Xubin Ren
co-authored by Cursor
parent 7c1aa5ae31
commit 56eee06736
19 changed files with 861 additions and 66 deletions
+24
View File
@@ -524,6 +524,8 @@ async def test_settings_api_returns_safe_subset_and_updates_whitelist(
config = Config()
config.agents.defaults.model = "openai/gpt-4o"
config.providers.openai.api_key = "secret-key"
config.tools.web.search.provider = "brave"
config.tools.web.search.api_key = "brave-secret"
save_config(config, config_path)
monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)
@@ -547,7 +549,13 @@ async def test_settings_api_returns_safe_subset_and_updates_whitelist(
assert providers["openai"]["api_key_hint"] == "secr••••-key"
assert providers["openrouter"]["configured"] is False
assert body["agent"]["has_api_key"] is True
assert body["web_search"]["provider"] == "brave"
assert body["web_search"]["api_key_hint"] == "brav••••cret"
search_providers = {provider["name"]: provider for provider in body["web_search"]["providers"]}
assert search_providers["duckduckgo"]["credential"] == "none"
assert search_providers["searxng"]["credential"] == "base_url"
assert "secret-key" not in settings.text
assert "brave-secret" not in settings.text
provider_updated = await _http_get(
"http://127.0.0.1:"
@@ -571,11 +579,27 @@ async def test_settings_api_returns_safe_subset_and_updates_whitelist(
assert updated.status_code == 200
assert updated.json()["requires_restart"] is False
search_updated = await _http_get(
"http://127.0.0.1:"
f"{port}/api/settings/web-search/update?provider=searxng"
"&base_url=https%3A%2F%2Fsearch.example.com",
headers={"Authorization": "Bearer tok"},
)
assert search_updated.status_code == 200
search_body = search_updated.json()
assert search_body["requires_restart"] is False
assert search_body["web_search"]["provider"] == "searxng"
assert search_body["web_search"]["api_key_hint"] is None
assert search_body["web_search"]["base_url"] == "https://search.example.com"
saved = load_config(config_path)
assert saved.agents.defaults.model == "openrouter/test"
assert saved.agents.defaults.provider == "openrouter"
assert saved.providers.openrouter.api_key == "sk-or-test"
assert saved.providers.openrouter.api_base == "https://openrouter.ai/api/v1"
assert saved.tools.web.search.provider == "searxng"
assert saved.tools.web.search.api_key == ""
assert saved.tools.web.search.base_url == "https://search.example.com"
finally:
await channel.stop()
await server_task
+18 -1
View File
@@ -13,7 +13,24 @@ import pytest
from nanobot.agent.loop import AgentLoop
from nanobot.agent.subagent import SubagentManager, SubagentStatus
from nanobot.agent.tools.search import GlobTool, GrepTool
from nanobot.agent.tools.web import WebSearchTool
from nanobot.bus.queue import MessageBus
from nanobot.config.schema import WebSearchConfig
@pytest.mark.asyncio
async def test_web_search_tool_refreshes_dynamic_config_loader(monkeypatch) -> None:
tool = WebSearchTool(
config=WebSearchConfig(provider="brave"),
config_loader=lambda: WebSearchConfig(provider="duckduckgo", max_results=3),
)
async def fake_duckduckgo(self, query: str, n: int) -> str:
return f"{self.config.provider}:{query}:{n}"
monkeypatch.setattr(WebSearchTool, "_search_duckduckgo", fake_duckduckgo)
assert await tool.execute("nanobot") == "duckduckgo:nanobot:3"
@pytest.mark.asyncio
@@ -185,7 +202,7 @@ async def test_grep_files_with_matches_supports_head_limit_and_offset(tmp_path:
# 2. The pagination info is correct
assert "pagination: limit=1, offset=1" in result
# Count non-empty lines that start with src/ (file paths)
file_lines = [l for l in result.splitlines() if l.startswith("src/")]
file_lines = [line for line in result.splitlines() if line.startswith("src/")]
assert len(file_lines) == 1