diff --git a/src/agent_env/agents/README.md b/src/agent_env/agents/README.md index 43872d88c..1be4e6582 100644 --- a/src/agent_env/agents/README.md +++ b/src/agent_env/agents/README.md @@ -24,7 +24,8 @@ run it. The CLI's version is the Dockerfile's `node`, without npm or npx, and the Python packages `model-requirements.txt` pins. A `deploy_agent` `builtin_tools` selection picks the CLI's own tools: `{"disable": "*"}` and `{"only": [...]}` become its `--tools` list, a list to turn off becomes `--disallowedTools`, which keeps the CLI's defaults for the rest, and any selection adds -`--strict-mcp-config`, so its MCP servers are only the task env's. The names are `ClaudeCodeTool`'s in +`--strict-mcp-config`, so its MCP servers are only the task env's. Tool search is off unless the agent's environment sets +`ENABLE_TOOL_SEARCH`, so the CLI sends the env's tools and its own up front. The names are `ClaudeCodeTool`'s in `claude_bridge.py`: every tool the pinned CLI has, though which it offers by default depends on the model. A test fails when the Dockerfile's `CLAUDE_CODE_VERSION` moves, until `ClaudeCodeTool` and `TOOLS_CLI_VERSION` move with it. diff --git a/src/agent_env/agents/agents/claude-code/Dockerfile b/src/agent_env/agents/agents/claude-code/Dockerfile index 99d8539ae..cbe95d6e6 100644 --- a/src/agent_env/agents/agents/claude-code/Dockerfile +++ b/src/agent_env/agents/agents/claude-code/Dockerfile @@ -6,7 +6,7 @@ FROM public.ecr.aws/docker/library/python:3.11-slim@sha256:c20888b6acdd1e63e1c43 RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl poppler-utils \ && rm -rf /var/lib/apt/lists/* # Claude Code's native build for this machine's platform, checked against its release manifest. -ARG CLAUDE_CODE_VERSION=2.1.280 +ARG CLAUDE_CODE_VERSION=2.1.322 RUN set -eu; \ case "$(uname -m)" in x86_64) platform=linux-x64 ;; aarch64) platform=linux-arm64 ;; \ *) echo "Claude Code has no build for $(uname -m)" >&2; exit 1 ;; esac; \ diff --git a/src/agent_env/agents/agents/claude-code/claude_bridge.py b/src/agent_env/agents/agents/claude-code/claude_bridge.py index cecd8286f..fad10df25 100644 --- a/src/agent_env/agents/agents/claude-code/claude_bridge.py +++ b/src/agent_env/agents/agents/claude-code/claude_bridge.py @@ -56,11 +56,11 @@ def project_dir(workspace: Path) -> Path: # The CLI version ClaudeCodeTool names the tools of; a test holds it to the Dockerfile's CLAUDE_CODE_VERSION. -TOOLS_CLI_VERSION = "2.1.280" +TOOLS_CLI_VERSION = "2.1.322" # The CLI's own tools, by the names its model sees. Which of them it offers by default depends on the model, and its MCP # resource tools come only with MCP servers. ClaudeCodeTool = Literal["Agent", "Bash", "CronCreate", "CronDelete", "CronList", "Edit", "EnterWorktree", - "ExitWorktree", "ListAgents", "ListMcpResourcesTool", "NotebookEdit", "Read", + "ExitWorktree", "ListAgents", "ListMcpResourcesTool", "Monitor", "NotebookEdit", "Read", "ReadMcpResourceDirTool", "ReadMcpResourceTool", "ReportFindings", "ScheduleWakeup", "SendMessage", "Skill", "TaskCreate", "TaskGet", "TaskList", "TaskStop", "TaskUpdate", "WebFetch", "WebSearch", "Workflow", "Write"] @@ -194,6 +194,8 @@ def cli_env(environ: Mapping[str, str], *, model: str | None = None, project_id: env["ANTHROPIC_CUSTOM_HEADERS"] = "\n".join(f"{name}: {value}" for name, value in headers.items()) if max_thinking_tokens: env["MAX_THINKING_TOKENS"] = str(max_thinking_tokens) + # Left on, the CLI sends no MCP tool and only some of its own until the model searches for them. + env.setdefault("ENABLE_TOOL_SEARCH", "false") # Older CLIs prepend a changing attribution block to the system prompt on custom base URLs, which defeats the # provider's prefix cache. env["CLAUDE_CODE_ATTRIBUTION_HEADER"] = "0" @@ -593,8 +595,8 @@ async def feed_stdin() -> None: # Behind a gateway the CLI gives a bare id of these a 200K window, and envs whose tool definitions alone pass that # make it exit with "Autocompact is thrashing". Add a model only once the pinned CLI maps its [1m] form. -_NATIVE_1M_MODELS = frozenset({"claude-opus-5", "claude-opus-5-5", "claude-fable-5-1", "claude-opus-4-8", - "claude-opus-4-7"}) +_NATIVE_1M_MODELS = frozenset({"claude-opus-5", "claude-opus-5-5", "claude-fable-5-1", "claude-fable-5-2", + "claude-opus-4-8", "claude-opus-4-7"}) def _with_1m_window(model: str) -> str: diff --git a/tst/integration/cli/run_bundle_claude_code_install_test.py b/tst/integration/cli/run_bundle_claude_code_install_test.py index f3b76d0af..47b4fc068 100644 --- a/tst/integration/cli/run_bundle_claude_code_install_test.py +++ b/tst/integration/cli/run_bundle_claude_code_install_test.py @@ -135,7 +135,8 @@ def test_a_retried_install_replaces_the_agent_an_earlier_attempt_left_running_in """The first turn fails, and its retry installs again into the same container, on the same port, while the first install's agent still runs there.""" endpoint.respond = tool_then_answer("Bash", {"command": f"cat notes.txt; pwd; {READ_SKILL}"}) - endpoint.fail_next(Fault("status", status=400)) + # A 403, which the CLI never resends; a 400 it resends without some of its betas. + endpoint.fail_next(Fault("status", status=403)) bundle = _bundle(state / "bundle", "debian") task = bundle / "tasks/install.json" steps = json.loads(task.read_text()) @@ -190,7 +191,8 @@ def test_a_retried_host_install_stops_the_agent_an_earlier_attempt_started(monke """install_agent empties its work dir before each attempt, so the first install's agent must live outside it: found there, it is stopped, rather than keeping the port the retry's agent needs.""" endpoint.respond = tool_then_answer("Bash", {"command": f"echo {NOTES}; {READ_SKILL}"}) - endpoint.fail_next(Fault("status", status=400)) + # A 403, which the CLI never resends; a 400 it resends without some of its betas. + endpoint.fail_next(Fault("status", status=403)) bundle = _bundle(state / "bundle", None, agents=("helper",)) task = bundle / "tasks/install.json" steps = json.loads(task.read_text()) diff --git a/tst/unit/agents/claude_code/test_claude_bridge_model.py b/tst/unit/agents/claude_code/test_claude_bridge_model.py index 702f2208f..c8f876a3b 100644 --- a/tst/unit/agents/claude_code/test_claude_bridge_model.py +++ b/tst/unit/agents/claude_code/test_claude_bridge_model.py @@ -11,8 +11,8 @@ def _cli_model(model: str | None) -> str | None: return cmd[cmd.index("--model") + 1] if "--model" in cmd else None -@pytest.mark.parametrize("model", ["claude-opus-5-5", "claude-opus-5", "claude-fable-5-1", "claude-opus-4-8", - "claude-opus-4-7"]) +@pytest.mark.parametrize("model", ["claude-opus-5-5", "claude-opus-5", "claude-fable-5-1", "claude-fable-5-2", + "claude-opus-4-8", "claude-opus-4-7"]) def test_a_native_1m_model_gets_its_1m_window(model): assert _cli_model(model) == f"{model}[1m]" diff --git a/tst/unit/agents/claude_code/test_cli_env.py b/tst/unit/agents/claude_code/test_cli_env.py index 443f4940a..7c92fcc82 100644 --- a/tst/unit/agents/claude_code/test_cli_env.py +++ b/tst/unit/agents/claude_code/test_cli_env.py @@ -41,6 +41,11 @@ def test_anthropic_variables_already_set_are_kept(): assert (env["ANTHROPIC_AUTH_TOKEN"], env["ANTHROPIC_BASE_URL"]) == ("own", "https://own.example") +def test_the_cli_offers_its_tools_up_front_unless_tool_search_is_asked_for(): + assert claude_bridge.cli_env(GATEWAY)["ENABLE_TOOL_SEARCH"] == "false" + assert claude_bridge.cli_env({**GATEWAY, "ENABLE_TOOL_SEARCH": "auto"})["ENABLE_TOOL_SEARCH"] == "auto" + + def test_no_endpoint_leaves_the_cli_to_its_own_and_an_api_key_alone(): env = claude_bridge.cli_env({"ANTHROPIC_API_KEY": "direct"}, model="claude-x") @@ -141,7 +146,7 @@ async def _exec(*cmd, **kwargs): @pytest.mark.parametrize("workspace, project", [ ("/app", "-app"), - ("/workspaces/my_ws.x-y/Sub Dir", "-workspaces-my-ws-x-y-Sub-Dir"), # as the CLI names it, checked against 2.1.280 + ("/workspaces/my_ws.x-y/Sub Dir", "-workspaces-my-ws-x-y-Sub-Dir"), # as the CLI names it, checked against 2.1.322 ]) def test_a_sessions_transcripts_are_where_the_cli_keeps_them_for_its_workspace(workspace, project): assert claude_bridge.project_dir(Path(workspace)) == claude_bridge.CLAUDE_STATE_DIR / "projects" / project