Skip to content

Commit 6e9895c

Browse files
GWealecopybara-github
authored andcommitted
fix: preserve ADK behavior on Windows
Several paths misbehaved when ADK was imported or run on Windows: drive-letter paths were split at the wrong colon (including for paths that did not exist yet), text writes could rewrite explicit newlines, telemetry detection assumed POSIX separators, and the Bash tool imported the POSIX-only resource module at import time. This splits eval selectors from the right while preserving drive paths, writes text with exact newline handling, normalizes telemetry paths before matching, and imports the Bash tool safely on Windows with an explicit POSIX-only error after confirmation. Co-authored-by: George Weale <gweale@google.com> PiperOrigin-RevId: 952417974
1 parent 13e311b commit 6e9895c

7 files changed

Lines changed: 118 additions & 10 deletions

File tree

src/google/adk/environment/_local_environment.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@ def _sync_read(path: Path) -> bytes:
162162
def _sync_write(path: Path, content: str | bytes) -> None:
163163
os.makedirs(path.parent, exist_ok=True)
164164
mode = 'w' if isinstance(content, str) else 'wb'
165-
kwargs = {'encoding': 'utf-8'} if isinstance(content, str) else {}
165+
kwargs = (
166+
{'encoding': 'utf-8', 'newline': ''} if isinstance(content, str) else {}
167+
)
166168
with open(path, mode, **kwargs) as f:
167169
f.write(content)

src/google/adk/telemetry/tracing.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -652,7 +652,7 @@ def _instrumented_with_opentelemetry_instrumentation_google_genai() -> bool:
652652
while wrapped := getattr(maybe_wrapped_function, "__wrapped__", None):
653653
if (
654654
"opentelemetry/instrumentation/google_genai"
655-
in maybe_wrapped_function.__code__.co_filename
655+
in maybe_wrapped_function.__code__.co_filename.replace("\\", "/")
656656
):
657657
return True
658658
maybe_wrapped_function = wrapped # pyright: ignore[reportAny]

src/google/adk/tools/bash_tool.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@
1818

1919
import asyncio
2020
import dataclasses
21+
import importlib
2122
import logging
2223
import os
2324
import pathlib
24-
import resource
2525
import shlex
2626
import signal
2727
from typing import Any
@@ -34,6 +34,8 @@
3434

3535
logger = logging.getLogger("google_adk." + __name__)
3636

37+
_resource = importlib.import_module("resource") if os.name == "posix" else None
38+
3739

3840
@dataclasses.dataclass(frozen=True)
3941
class BashToolPolicy:
@@ -77,21 +79,23 @@ def _validate_command(command: str, policy: BashToolPolicy) -> Optional[str]:
7779

7880
def _set_resource_limits(policy: BashToolPolicy) -> None:
7981
"""Sets resource limits for the subprocess based on the provided policy."""
82+
if _resource is None:
83+
return
8084
try:
81-
resource.setrlimit(resource.RLIMIT_CORE, (0, 0))
85+
_resource.setrlimit(_resource.RLIMIT_CORE, (0, 0))
8286
if policy.max_memory_bytes:
83-
resource.setrlimit(
84-
resource.RLIMIT_AS,
87+
_resource.setrlimit(
88+
_resource.RLIMIT_AS,
8589
(policy.max_memory_bytes, policy.max_memory_bytes),
8690
)
8791
if policy.max_file_size_bytes:
88-
resource.setrlimit(
89-
resource.RLIMIT_FSIZE,
92+
_resource.setrlimit(
93+
_resource.RLIMIT_FSIZE,
9094
(policy.max_file_size_bytes, policy.max_file_size_bytes),
9195
)
9296
if policy.max_child_processes:
93-
resource.setrlimit(
94-
resource.RLIMIT_NPROC,
97+
_resource.setrlimit(
98+
_resource.RLIMIT_NPROC,
9599
(policy.max_child_processes, policy.max_child_processes),
96100
)
97101
except (ValueError, OSError) as e:
@@ -171,6 +175,9 @@ async def run_async(
171175
elif not tool_context.tool_confirmation.confirmed:
172176
return {"error": "This tool call is rejected."}
173177

178+
if os.name != "posix":
179+
return {"error": "ExecuteBashTool is only supported on POSIX systems."}
180+
174181
stdout = None
175182
stderr = None
176183
try:

tests/unittests/cli/utils/test_cli_eval.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,30 @@ async def test_get_root_agent_raises_without_supported_entrypoint(monkeypatch):
148148
)
149149
with pytest.raises(ValueError, match="root_agent|get_agent_async"):
150150
await get_root_agent("some/dir")
151+
152+
153+
def test_parse_evals_preserves_windows_drive_in_file_path(tmp_path):
154+
from google.adk.cli.cli_eval import parse_and_get_evals_to_run
155+
156+
eval_set_file = tmp_path / "evals.json"
157+
eval_set_file.write_text("{}", encoding="utf-8")
158+
159+
assert parse_and_get_evals_to_run([str(eval_set_file)]) == {
160+
str(eval_set_file): []
161+
}
162+
163+
164+
def test_parse_evals_preserves_missing_windows_drive_path():
165+
from google.adk.cli.cli_eval import parse_and_get_evals_to_run
166+
167+
eval_set_file = r"C:\missing\evals.json"
168+
169+
assert parse_and_get_evals_to_run([eval_set_file]) == {eval_set_file: []}
170+
171+
172+
def test_parse_evals_splits_case_selector_from_right():
173+
from google.adk.cli.cli_eval import parse_and_get_evals_to_run
174+
175+
assert parse_and_get_evals_to_run([r"C:\evals\set.json:case1,case2"]) == {
176+
r"C:\evals\set.json": ["case1", "case2"]
177+
}

tests/unittests/telemetry/test_functional.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,26 @@ def test_instrumented_with_opentelemetry_instrumentation_google_genai():
175175
)
176176

177177

178+
def test_instrumented_detection_normalizes_windows_path_separators(
179+
monkeypatch: pytest.MonkeyPatch,
180+
):
181+
"""Backslash-separated instrumentation paths are matched on Windows."""
182+
windows_path = r"C:\pkg\opentelemetry\instrumentation\google_genai\patch.py"
183+
184+
class _FakeCode:
185+
co_filename = windows_path
186+
187+
class _FakeInstrumentedFunction:
188+
__code__ = _FakeCode
189+
__wrapped__ = object()
190+
191+
monkeypatch.setattr(
192+
tracing.Models, "generate_content", _FakeInstrumentedFunction
193+
)
194+
195+
assert tracing._instrumented_with_opentelemetry_instrumentation_google_genai()
196+
197+
178198
# ---------------------------------------------------------------------------
179199
# MCP integration: telemetry adds zero ``list_tools()`` calls of its own.
180200
#
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from __future__ import annotations
16+
17+
from unittest import mock
18+
19+
from google.adk.tools import bash_tool
20+
from google.adk.tools.tool_confirmation import ToolConfirmation
21+
from google.adk.tools.tool_context import ToolContext
22+
import pytest
23+
24+
25+
@pytest.mark.asyncio
26+
async def test_execute_bash_tool_reports_unsupported_platform(
27+
tmp_path, monkeypatch: pytest.MonkeyPatch
28+
):
29+
"""A confirmed Bash command returns a clear error on non-POSIX hosts."""
30+
tool = bash_tool.ExecuteBashTool(workspace=tmp_path)
31+
tool_context = mock.create_autospec(ToolContext, instance=True)
32+
confirmation = mock.create_autospec(ToolConfirmation, instance=True)
33+
confirmation.confirmed = True
34+
tool_context.tool_confirmation = confirmation
35+
monkeypatch.setattr(bash_tool.os, "name", "nt")
36+
37+
result = await tool.run_async(
38+
args={"command": "echo hello"}, tool_context=tool_context
39+
)
40+
41+
assert result == {
42+
"error": "ExecuteBashTool is only supported on POSIX systems."
43+
}

tests/unittests/tools/test_local_environment.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,15 @@ async def test_write_bytes_content(self, env: LocalEnvironment):
6969
data = await env.read_file("binary.bin")
7070
assert data == raw
7171

72+
@pytest.mark.asyncio
73+
async def test_write_preserves_explicit_crlf(self, env: LocalEnvironment):
74+
"""Explicit CRLF sequences are written without newline translation."""
75+
await env.write_file("crlf.txt", "first\r\nsecond\r\n")
76+
77+
data = await env.read_file("crlf.txt")
78+
79+
assert data == b"first\r\nsecond\r\n"
80+
7281
@pytest.mark.asyncio
7382
async def test_write_creates_parent_dirs(self, env: LocalEnvironment):
7483
"""Parent directories are created automatically."""

0 commit comments

Comments
 (0)