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
3 changes: 2 additions & 1 deletion src/agent_env/agents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion src/agent_env/agents/agents/claude-code/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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; \
Expand Down
10 changes: 6 additions & 4 deletions src/agent_env/agents/agents/claude-code/claude_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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:
Expand Down
6 changes: 4 additions & 2 deletions tst/integration/cli/run_bundle_claude_code_install_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -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())
Expand Down
4 changes: 2 additions & 2 deletions tst/unit/agents/claude_code/test_claude_bridge_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]"

Expand Down
7 changes: 6 additions & 1 deletion tst/unit/agents/claude_code/test_cli_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -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
Expand Down
Loading