Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions eval/harbor/clawcodex_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
):
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions eval/harbor/run_tb21_nano_max.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
5 changes: 3 additions & 2 deletions src/nano/prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
31 changes: 31 additions & 0 deletions src/nano/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
7 changes: 7 additions & 0 deletions src/nano/tool_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)."
),
}
21 changes: 21 additions & 0 deletions tests/nano/test_nano_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading