diff --git a/headroom/cli/wrap.py b/headroom/cli/wrap.py index 822414c6a..14885a9b6 100644 --- a/headroom/cli/wrap.py +++ b/headroom/cli/wrap.py @@ -87,6 +87,7 @@ from headroom.providers.claude import ( from headroom.providers.claude import ( proxy_base_url as _claude_proxy_base_url, ) +from headroom.providers.claude.runtime import TOOL_SEARCH_FOUNDRY_DEFAULT from headroom.providers.codex import build_launch_env as _build_codex_launch_env from headroom.providers.codex.install import codex_uses_chatgpt_auth from headroom.providers.codex.threads import retag_to_headroom, retag_to_native @@ -269,13 +270,14 @@ _WRAP_PROXY_TIMEOUT_ML_MODULES = ("torch", "sentence_transformers", "spacy") # Issue #746: Claude Code disables on-demand tool loading (deferral) when # ANTHROPIC_BASE_URL is a custom host and ENABLE_TOOL_SEARCH is unset, which # inflates the local context window by tens of K tokens. Setting the env var -# when we launch Claude Code keeps deferral on. Default to "true" — defer the -# MCP/system tools for maximum context savings, matching native first-party -# behaviour (core built-ins like Read/Edit/Bash are never deferred by Claude -# Code, so the agent loop is unaffected). The key/default are shared with -# `init` and `install` via the Claude provider package to prevent drift. +# when we launch Claude Code keeps deferral on. The generic default stays +# "true" for non-Foundry sessions, while Foundry uses a dedicated compatibility +# default of "false" because its upstream does not support the deferred-tool +# shape. The key/defaults are shared with `init` and `install` via the Claude +# provider package to prevent drift. _TOOL_SEARCH_ENV = TOOL_SEARCH_ENV _TOOL_SEARCH_DEFAULT = TOOL_SEARCH_DEFAULT +_TOOL_SEARCH_FOUNDRY_DEFAULT = TOOL_SEARCH_FOUNDRY_DEFAULT _AGENT_SAVINGS_WRAP_AGENTS = {"claude", "codex", "cursor", "grok", "grok_build"} # 1M context window for `wrap claude` (#1158). Claude Code only sends the @@ -358,7 +360,8 @@ def _configure_tool_search_env(env: dict[str, str], flag_value: str | None) -> s 1. explicit ``--tool-search`` flag — wins (the user asked for it on the CLI), 2. a pre-existing ``ENABLE_TOOL_SEARCH`` in the environment — respected and left untouched (the user's own Claude Code knob), - 3. the built-in default (``true``). + 3. the built-in mode-specific default (``true`` normally, ``false`` on + Foundry). Returns the value written, or ``None`` when an existing environment value was deliberately left in place. @@ -373,8 +376,11 @@ def _configure_tool_search_env(env: dict[str, str], flag_value: str | None) -> s existing = env.get(_TOOL_SEARCH_ENV) if existing is not None and existing.strip(): return None - env[_TOOL_SEARCH_ENV] = _TOOL_SEARCH_DEFAULT - return _TOOL_SEARCH_DEFAULT + default = ( + _TOOL_SEARCH_FOUNDRY_DEFAULT if env.get("CLAUDE_CODE_USE_FOUNDRY") else _TOOL_SEARCH_DEFAULT + ) + env[_TOOL_SEARCH_ENV] = default + return default # ENABLE_TOOL_SEARCH modes that turn deferral OFF. Everything else Claude Code diff --git a/headroom/providers/claude/runtime.py b/headroom/providers/claude/runtime.py index 023357d01..3bd6fca91 100644 --- a/headroom/providers/claude/runtime.py +++ b/headroom/providers/claude/runtime.py @@ -15,6 +15,7 @@ DEFAULT_API_URL = "https://api.anthropic.com" # single source of truth shared by `wrap`, `init`, and `install`. TOOL_SEARCH_ENV = "ENABLE_TOOL_SEARCH" TOOL_SEARCH_DEFAULT = "true" +TOOL_SEARCH_FOUNDRY_DEFAULT = "false" REMOTE_CONTROL_BASE_URL_ENV = "ANTHROPIC_BASE_URL" REMOTE_CONTROL_FEATURE = "Remote Control" diff --git a/tests/test_cli/test_wrap_claude.py b/tests/test_cli/test_wrap_claude.py new file mode 100644 index 000000000..fa722eb2d --- /dev/null +++ b/tests/test_cli/test_wrap_claude.py @@ -0,0 +1,59 @@ +"""Focused tests for Claude wrap tool-search defaults.""" + +from __future__ import annotations + +import pytest + +from headroom.cli.wrap import ( + _TOOL_SEARCH_DEFAULT, + _TOOL_SEARCH_ENV, + _configure_tool_search_env, +) + + +def test_foundry_without_override_disables_tool_search() -> None: + env = {"CLAUDE_CODE_USE_FOUNDRY": "1"} + + result = _configure_tool_search_env(env, None) + + assert result == "false" + assert env[_TOOL_SEARCH_ENV] == "false" + + +@pytest.mark.parametrize("flag_value", ["auto", "false"]) +def test_explicit_flag_wins_in_foundry(flag_value: str) -> None: + env = {"CLAUDE_CODE_USE_FOUNDRY": "1"} + + result = _configure_tool_search_env(env, flag_value) + + assert result == flag_value + assert env[_TOOL_SEARCH_ENV] == flag_value + + +def test_existing_environment_value_wins_in_foundry() -> None: + env = {"CLAUDE_CODE_USE_FOUNDRY": "1", _TOOL_SEARCH_ENV: "auto:30"} + + result = _configure_tool_search_env(env, None) + + assert result is None + assert env[_TOOL_SEARCH_ENV] == "auto:30" + + +@pytest.mark.parametrize("blank", ["", " ", "\t"]) +def test_blank_environment_uses_mode_default(blank: str) -> None: + foundry_env = {"CLAUDE_CODE_USE_FOUNDRY": "1", _TOOL_SEARCH_ENV: blank} + generic_env = {_TOOL_SEARCH_ENV: blank} + + assert _configure_tool_search_env(foundry_env, None) == "false" + assert foundry_env[_TOOL_SEARCH_ENV] == "false" + assert _configure_tool_search_env(generic_env, None) == _TOOL_SEARCH_DEFAULT + assert generic_env[_TOOL_SEARCH_ENV] == _TOOL_SEARCH_DEFAULT + + +def test_non_foundry_without_override_keeps_generic_default() -> None: + env: dict[str, str] = {} + + result = _configure_tool_search_env(env, None) + + assert result == _TOOL_SEARCH_DEFAULT + assert env[_TOOL_SEARCH_ENV] == _TOOL_SEARCH_DEFAULT