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
33 changes: 32 additions & 1 deletion src/google/adk/cli/cli_deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import importlib
import json
import os
import re
import shutil
import stat
import subprocess
Expand Down Expand Up @@ -483,6 +484,36 @@ def _validate_gcloud_extra_args(
)


def _validate_agent_folder_name(agent_folder: str) -> str:
"""Validates that the agent directory name is a valid Python identifier.

Agent Engine requires the agent folder to be importable as a Python module.
Directory names containing hyphens (-) or other non-identifier characters
lead to syntax and import errors at deployment runtime.

Args:
agent_folder: Path to the agent directory.

Returns:
The validated folder name.

Raises:
click.ClickException: If the folder name is not a valid Python identifier.
"""
folder_name = os.path.basename(os.path.normpath(agent_folder))
if not folder_name.isidentifier():
suggested = re.sub(r'\W|^(?=\d)', '_', folder_name)
if not suggested.isidentifier():
suggested = f'_{suggested}'
raise click.ClickException(
f"Agent directory name '{folder_name}' is not a valid Python"
" identifier (cannot contain dashes '-' or special characters)."
" Agent Engine requires the directory name to be a valid Python"
f" module name. Please rename your agent folder (e.g., '{suggested}')."
)
return folder_name


def _validate_agent_import(
agent_src_path: str,
adk_app_object: str,
Expand Down Expand Up @@ -980,7 +1011,7 @@ def to_agent_engine(
extra_packages (list[str]): Optional. Additional local file or directory
paths to stage alongside the agent and make importable in the image.
"""
app_name = os.path.basename(agent_folder)
app_name = _validate_agent_folder_name(agent_folder)
display_name = display_name or app_name
parent_folder = os.path.dirname(agent_folder)
if adk_app_object:
Expand Down
53 changes: 52 additions & 1 deletion tests/unittests/cli/utils/test_cli_deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -1195,7 +1195,6 @@ def test_removes_directory_tree(self, tmp_path: Path) -> None:

def test_removes_readonly_files(self, tmp_path: Path) -> None:
"""It should remove a tree containing read-only files."""
import os
import stat

d = tmp_path / "ro_dir"
Expand All @@ -1219,3 +1218,55 @@ def test_on_rm_error_clears_readonly_and_retries(

cli_deploy._on_rm_error(os.remove, str(ro_file), None)
assert not ro_file.exists()


# _validate_agent_folder_name tests


class TestValidateAgentFolderName:
"""Tests for the _validate_agent_folder_name helper."""

def test_valid_identifier_succeeds(self) -> None:
"""A valid identifier folder name should succeed and return basename."""
assert (
cli_deploy._validate_agent_folder_name("/path/to/my_agent")
== "my_agent"
)
assert cli_deploy._validate_agent_folder_name("agent123") == "agent123"
assert (
cli_deploy._validate_agent_folder_name("/path/to/my_agent/")
== "my_agent"
)

def test_invalid_folder_name_with_dashes_raises(self) -> None:
"""A folder name with dashes should raise ClickException with suggestions."""
with pytest.raises(click.ClickException) as exc_info:
cli_deploy._validate_agent_folder_name("my-agent-app")
assert "not a valid Python identifier" in str(exc_info.value)
assert "my_agent_app" in str(exc_info.value)

def test_invalid_folder_name_starting_with_number_raises(self) -> None:
"""A folder name starting with a number should raise ClickException."""
with pytest.raises(click.ClickException) as exc_info:
cli_deploy._validate_agent_folder_name("123agent")
assert "not a valid Python identifier" in str(exc_info.value)
assert "_123agent" in str(exc_info.value)

def test_to_agent_engine_raises_on_dashed_folder_name(
self, tmp_path: Path
) -> None:
"""to_agent_engine should fail early when agent directory contains dashes."""
dashed_agent = tmp_path / "dashed-agent-name"
dashed_agent.mkdir()
(dashed_agent / "agent.py").write_text("root_agent = 'test'\n")
(dashed_agent / "__init__.py").touch()

with pytest.raises(click.ClickException) as exc_info:
cli_deploy.to_agent_engine(
agent_folder=str(dashed_agent),
project="test-project",
region="us-central1",
)
assert "not a valid Python identifier" in str(exc_info.value)
assert "dashed_agent_name" in str(exc_info.value)