From 853676b67e3401e9ca6707bb2e5a2c3f00842b01 Mon Sep 17 00:00:00 2001 From: agentforce314 <273884145+agentforce314@users.noreply.github.com> Date: Sun, 16 Aug 2026 02:36:09 -0700 Subject: [PATCH] feat(nano): WebSearch as an explicit opt-in conditional tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes capability parity with the pi TB run, which made 6 websearch calls (mteb-leaderboard, video-processing, cobol-modernization) via its TB extension on tasks nano lost. Nano registers clawcodex's WebSearch (with a pi-length doc, ~150 est. tokens) IFF global config nano.websearch is True — seeded by the adapter's --ak websearch=1, or NANO_WEBSEARCH=1 through the runner. Key-presence alone deliberately does NOT register it: TAVILY_API_KEY is forwarded into benchmark containers by default, and a key that happens to exist must not grow nano's surface — the same explicitness bar as the vision config block. The key itself rides the (post-#893, actually-working) env-block seed; a missing key surfaces at call time with the tool's clear 'not configured' error. Tests: opted-in registration with the doc override; absent without the explicit config even though is_enabled defaults True. 65 pass. Co-Authored-By: Claude Fable 5 --- eval/harbor/clawcodex_agent.py | 15 +++++++++++++++ eval/harbor/run_tb21_nano_max.sh | 7 +++++++ src/nano/prompt.py | 5 +++-- src/nano/registry.py | 31 +++++++++++++++++++++++++++++++ src/nano/tool_docs.py | 7 +++++++ tests/nano/test_nano_registry.py | 21 +++++++++++++++++++++ 6 files changed, 84 insertions(+), 2 deletions(-) diff --git a/eval/harbor/clawcodex_agent.py b/eval/harbor/clawcodex_agent.py index 40fe11df..0d5f915e 100644 --- a/eval/harbor/clawcodex_agent.py +++ b/eval/harbor/clawcodex_agent.py @@ -390,6 +390,7 @@ def __init__( advisor_effort: str | None = None, vision: str | None = None, nano: bool | str = False, + websearch: bool | str = False, *args, **kwargs, ): @@ -406,6 +407,15 @@ def __init__( nano if isinstance(nano, bool) else parse_bool_env_value(str(nano).lower(), name="nano") ) + # ``--ak websearch=1`` — opt nano into the WebSearch tool (seeded as + # config ``nano.websearch``; the tool additionally needs a resolvable + # TAVILY_API_KEY, which forward_keys carries by default). Mirrors + # pi's TB extension, whose websearch registers when the key is + # exported. No effect outside nano mode. + self._websearch = ( + websearch if isinstance(websearch, bool) + else parse_bool_env_value(str(websearch).lower(), name="websearch") + ) self._source = source # A ``source`` that resolves to a real file on the host is a # working-tree build to upload rather than a spec for uv to resolve @@ -855,6 +865,11 @@ async def _seed_container_settings( config: dict[str, Any] = {} if settings: config["settings"] = settings + if self._websearch: + # Nano's explicit WebSearch opt-in (src/nano/registry.py + # _nano_websearch_configured) — key-presence alone must not + # grow the nano surface. + config["nano"] = {"websearch": True} env_block = self._host_env_keys() if env_block: config["env"] = env_block diff --git a/eval/harbor/run_tb21_nano_max.sh b/eval/harbor/run_tb21_nano_max.sh index 7802af3e..41ca96cc 100755 --- a/eval/harbor/run_tb21_nano_max.sh +++ b/eval/harbor/run_tb21_nano_max.sh @@ -63,6 +63,13 @@ if [ -n "${NANO_VISION:-}" ]; then AK_EXTRA+=(--ak "vision=$NANO_VISION") echo "vision: $NANO_VISION (nano registers vision_analyze)" fi +# NANO_WEBSEARCH=1 opts nano into the WebSearch tool (needs a resolvable +# TAVILY_API_KEY — forward_keys carries the host config env block by +# default). Matches the pi TB extension's websearch. +if [ "${NANO_WEBSEARCH:-0}" = "1" ]; then + AK_EXTRA+=(--ak websearch=1) + echo "websearch: enabled (nano registers WebSearch)" +fi PYTHONPATH="$ROOT/eval/harbor" harbor run \ --dataset terminal-bench/terminal-bench-2-1 \ diff --git a/src/nano/prompt.py b/src/nano/prompt.py index ec13bfb6..0576ee41 100644 --- a/src/nano/prompt.py +++ b/src/nano/prompt.py @@ -46,12 +46,13 @@ "Write": "create new files or fully overwrite existing ones", "Grep": "search file contents with regex (respects .gitignore)", "Glob": "find files by glob pattern", - # Conditional seventh tool — listed only when a vision model is - # configured and the registry registered it (see nano/registry.py). + # Conditional tools — listed only when configured and registered + # (see nano/registry.py). "vision_analyze": ( "ask a configured vision model a question about a local image " "(screenshots, plots, photos, rendered output)" ), + "WebSearch": "search the web (titles, URLs, snippets)", } # The three non-tool guidelines at the end are distilled from clawcodex's diff --git a/src/nano/registry.py b/src/nano/registry.py index 22b5b734..99f4f49a 100644 --- a/src/nano/registry.py +++ b/src/nano/registry.py @@ -73,4 +73,35 @@ def build_nano_registry() -> ToolRegistry: registry.register(VisionAnalyzeTool) except Exception: # noqa: BLE001 — a broken vision config never blocks nano pass + try: + from src.tool_system.tools import WebSearchTool + + if _nano_websearch_configured() and WebSearchTool.is_enabled(): + doc = NANO_TOOL_DOCS.get(WebSearchTool.name) + registry.register( + replace(WebSearchTool, prompt=lambda _doc=doc: _doc) + if doc else WebSearchTool + ) + except Exception: # noqa: BLE001 — a broken config never blocks nano + pass return registry + + +def _nano_websearch_configured() -> bool: + """Explicit opt-in for WebSearch on the nano surface. + + Reads global config ``nano.websearch is True`` (seeded by the harbor + adapter's ``--ak websearch=1``). Deliberately NOT keyed on the + TAVILY_API_KEY alone: keys are forwarded into benchmark containers by + default, and a key that happens to exist must not silently grow nano's + surface — the same explicitness bar as the vision config block. pi's + TB extension ships the same tool for the same reason (its runbook + exports TAVILY_API_KEY as a deliberate act). + """ + try: + from src import config as cfg_mod + + block = cfg_mod._get_default_manager().load_global().get("nano") + return isinstance(block, dict) and block.get("websearch") is True + except Exception: # noqa: BLE001 + return False diff --git a/src/nano/tool_docs.py b/src/nano/tool_docs.py index bc97c3a1..05c36d38 100644 --- a/src/nano/tool_docs.py +++ b/src/nano/tool_docs.py @@ -42,4 +42,11 @@ "Finds files by glob pattern (e.g. '**/*.py'), sorted by " "modification time. Use for locating files by name or path shape." ), + # Conditional tool — registered only when explicitly opted in + # (nano.websearch config) AND a search key resolves; see registry.py. + "WebSearch": ( + "Searches the web and returns result titles, URLs, and snippets. " + "Use for information you cannot derive locally (current versions, " + "external docs, error messages)." + ), } diff --git a/tests/nano/test_nano_registry.py b/tests/nano/test_nano_registry.py index 6a65f67d..ffffcc55 100644 --- a/tests/nano/test_nano_registry.py +++ b/tests/nano/test_nano_registry.py @@ -76,6 +76,27 @@ def test_vision_snippet_renders_in_prompt(vision_configured, tmp_path): assert "- vision_analyze: " in text +def test_websearch_joins_only_when_opted_in(monkeypatch): + import src.nano.registry as registry_mod + + monkeypatch.setattr( + registry_mod, "_nano_websearch_configured", lambda: True + ) + names = [t.name for t in build_nano_registry().list_tools()] + assert names[-1] == "WebSearch" + ws = next(t for t in build_nano_registry().list_tools() if t.name == "WebSearch") + assert len(ws.prompt()) < 400 # pi-length doc override applied + + +def test_websearch_absent_without_explicit_config(): + # WebSearch.is_enabled defaults True (key errors surface at call + # time), so registration must hinge purely on the explicit + # nano.websearch config — a resolvable TAVILY key alone (the default + # forwarded-keys benchmark environment) must not grow the surface. + names = {t.name for t in build_nano_registry().list_tools()} + assert "WebSearch" not in names + + def test_default_registry_instances_untouched(): # The nano registry carries copies; the shared static instances (and # therefore the default registry) must keep their full docs.