Skip to content
Open
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
40 changes: 40 additions & 0 deletions src/google/adk/evaluation/_path_validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import annotations


def validate_path_segment(value: str, field_name: str) -> None:
"""Rejects values that could alter a filesystem path.

Args:
value: The caller-supplied identifier.
field_name: Human-readable field name used in error messages.

Raises:
ValueError: If the value contains path separators, traversal segments, or
null bytes.
"""
if not value:
raise ValueError(f"{field_name} must not be empty.")
if "\x00" in value:
raise ValueError(f"{field_name} must not contain null bytes.")
if "/" in value or "\\" in value:
raise ValueError(
f"{field_name} {value!r} must not contain path separators."
)
if value in (".", ".."):
raise ValueError(
f"{field_name} {value!r} must not contain traversal segments."
)
5 changes: 5 additions & 0 deletions src/google/adk/evaluation/local_eval_set_results_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from ..errors.not_found_error import NotFoundError
from ._eval_set_results_manager_utils import create_eval_set_result
from ._eval_set_results_manager_utils import parse_eval_set_result_json
from ._path_validation import validate_path_segment
from .eval_result import EvalCaseResult
from .eval_result import EvalSetResult
from .eval_set_results_manager import EvalSetResultsManager
Expand All @@ -46,6 +47,8 @@ def save_eval_set_result(
eval_case_results: list[EvalCaseResult],
) -> None:
"""Creates and saves a new EvalSetResult given eval_case_results."""
validate_path_segment(app_name, "app_name")
validate_path_segment(eval_set_id, "eval_set_id")
eval_set_result = create_eval_set_result(
app_name, eval_set_id, eval_case_results
)
Expand All @@ -67,6 +70,7 @@ def get_eval_set_result(
self, app_name: str, eval_set_result_id: str
) -> EvalSetResult:
"""Returns an EvalSetResult identified by app_name and eval_set_result_id."""
validate_path_segment(eval_set_result_id, "eval_set_result_id")
# Load the eval set result file data.
maybe_eval_result_file_path = (
os.path.join(
Expand Down Expand Up @@ -97,4 +101,5 @@ def list_eval_set_results(self, app_name: str) -> list[str]:
return eval_result_files

def _get_eval_history_dir(self, app_name: str) -> str:
validate_path_segment(app_name, "app_name")
return os.path.join(self._agents_dir, app_name, _ADK_EVAL_HISTORY_DIR)
4 changes: 4 additions & 0 deletions src/google/adk/evaluation/local_eval_sets_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from ._eval_sets_manager_utils import get_eval_case_from_eval_set
from ._eval_sets_manager_utils import get_eval_set_from_app_and_id
from ._eval_sets_manager_utils import update_eval_case_in_eval_set
from ._path_validation import validate_path_segment
from .eval_case import EvalCase
from .eval_case import IntermediateData
from .eval_case import Invocation
Expand Down Expand Up @@ -247,6 +248,7 @@ def list_eval_sets(self, app_name: str) -> list[str]:
Raises:
NotFoundError: If the eval directory for the app is not found.
"""
validate_path_segment(app_name, "app_name")
eval_set_file_path = os.path.join(self._agents_dir, app_name)
eval_sets = []
try:
Expand Down Expand Up @@ -310,6 +312,8 @@ def delete_eval_case(
self._save_eval_set(app_name, eval_set_id, updated_eval_set)

def _get_eval_set_file_path(self, app_name: str, eval_set_id: str) -> str:
validate_path_segment(app_name, "app_name")
validate_path_segment(eval_set_id, "eval_set_id")
return os.path.join(
self._agents_dir,
app_name,
Expand Down
30 changes: 30 additions & 0 deletions tests/unittests/evaluation/test_local_eval_set_results_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,22 @@ def test_save_eval_set_result(self, mocker):
expected_eval_set_result_data = self.eval_set_result.model_dump(mode="json")
assert expected_eval_set_result_data == actual_eval_set_result_data

@pytest.mark.parametrize("app_name", ["", ".", "..", "foo/bar", "foo\\bar"])
def test_save_eval_set_result_rejects_invalid_app_name(self, app_name):
with pytest.raises(ValueError):
self.manager.save_eval_set_result(
app_name, self.eval_set_id, self.eval_case_results
)

@pytest.mark.parametrize(
"eval_set_id", ["", ".", "..", "foo/bar", "foo\\bar"]
)
def test_save_eval_set_result_rejects_invalid_eval_set_id(self, eval_set_id):
with pytest.raises(ValueError):
self.manager.save_eval_set_result(
self.app_name, eval_set_id, self.eval_case_results
)

def test_get_eval_set_result(self, mocker):
mock_time = mocker.patch("time.time")
mock_time.return_value = self.timestamp
Expand All @@ -103,6 +119,20 @@ def test_get_eval_set_result(self, mocker):
)
assert retrieved_result == self.eval_set_result

@pytest.mark.parametrize("app_name", ["", ".", "..", "foo/bar", "foo\\bar"])
def test_get_eval_set_result_rejects_invalid_app_name(self, app_name):
with pytest.raises(ValueError):
self.manager.get_eval_set_result(app_name, self.eval_set_result_name)

@pytest.mark.parametrize(
"eval_set_result_id", ["", ".", "..", "foo/bar", "foo\\bar"]
)
def test_get_eval_set_result_rejects_invalid_eval_set_result_id(
self, eval_set_result_id
):
with pytest.raises(ValueError):
self.manager.get_eval_set_result(self.app_name, eval_set_result_id)

def test_get_eval_set_result_double_encoded_legacy(self):
eval_history_dir = os.path.join(
self.agents_dir, self.app_name, _ADK_EVAL_HISTORY_DIR
Expand Down
23 changes: 23 additions & 0 deletions tests/unittests/evaluation/test_local_eval_sets_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,29 @@ def test_local_eval_sets_manager_create_eval_set_invalid_id(
with pytest.raises(ValueError, match="Invalid Eval Set ID"):
local_eval_sets_manager.create_eval_set(app_name, eval_set_id)

@pytest.mark.parametrize("app_name", ["", ".", "..", "foo/bar", "foo\\bar"])
def test_local_eval_sets_manager_create_eval_set_rejects_invalid_app_name(
self, local_eval_sets_manager, app_name
):
with pytest.raises(ValueError):
local_eval_sets_manager.create_eval_set(app_name, "test_eval_set")

@pytest.mark.parametrize("app_name", ["", ".", "..", "foo/bar", "foo\\bar"])
def test_local_eval_sets_manager_list_eval_sets_rejects_invalid_app_name(
self, local_eval_sets_manager, app_name
):
with pytest.raises(ValueError):
local_eval_sets_manager.list_eval_sets(app_name)

@pytest.mark.parametrize(
"eval_set_id", ["", ".", "..", "foo/bar", "foo\\bar"]
)
def test_local_eval_sets_manager_get_eval_set_rejects_invalid_eval_set_id(
self, local_eval_sets_manager, eval_set_id
):
with pytest.raises(ValueError):
local_eval_sets_manager.get_eval_set("test_app", eval_set_id)

def test_local_eval_sets_manager_create_eval_set_already_exists(
self, local_eval_sets_manager, mocker
):
Expand Down