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
1 change: 0 additions & 1 deletion contributing/samples/gepa/experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@
from tau_bench.types import EnvRunResult
from tau_bench.types import RunConfig
import tau_bench_agent as tau_bench_agent_lib

import utils


Expand Down
1 change: 0 additions & 1 deletion contributing/samples/gepa/run_experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
from absl import flags
import experiment
from google.genai import types

import utils

_OUTPUT_DIR = flags.DEFINE_string(
Expand Down
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ test = [
"a2a-sdk>=0.3.0,<0.4.0",
"anthropic>=0.43.0", # For anthropic model tests
"crewai[tools];python_version>='3.11' and python_version<'3.12'", # For CrewaiTool tests; chromadb/pypika fail on 3.12+
"google-cloud-iamconnectorcredentials>=0.1.0, <0.2.0",
"google-cloud-parametermanager>=0.4.0, <1.0.0",
"kubernetes>=29.0.0", # For GkeCodeExecutor
"langchain-community>=0.3.17",
Expand Down Expand Up @@ -176,6 +177,10 @@ toolbox = ["toolbox-adk>=1.0.0, <2.0.0"]

slack = ["slack-bolt>=1.22.0"]

agent-identity = [
"google-cloud-iamconnectorcredentials>=0.1.0, <0.2.0",
]

[tool.pyink]
# Format py files following Google style-guide
line-length = 80
Expand Down
27 changes: 27 additions & 0 deletions src/google/adk/artifacts/file_artifact_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,13 +138,39 @@ def _is_user_scoped(session_id: Optional[str], filename: str) -> bool:
return session_id is None or _file_has_user_namespace(filename)


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

Args:
value: The caller-supplied identifier (e.g. user_id or session_id).
field_name: Human-readable name used in the error message.

Raises:
InputValidationError: If the value contains path separators, traversal
segments, or null bytes.
"""
if not value:
raise InputValidationError(f"{field_name} must not be empty.")
if "\x00" in value:
raise InputValidationError(f"{field_name} must not contain null bytes.")
if "/" in value or "\\" in value:
raise InputValidationError(
f"{field_name} {value!r} must not contain path separators."
)
if value in (".", "..") or ".." in value.split("/"):
raise InputValidationError(
f"{field_name} {value!r} must not contain traversal segments."
)


def _user_artifacts_dir(base_root: Path) -> Path:
"""Returns the path that stores user-scoped artifacts."""
return base_root / "artifacts"


def _session_artifacts_dir(base_root: Path, session_id: str) -> Path:
"""Returns the path that stores session-scoped artifacts."""
_validate_path_segment(session_id, "session_id")
return base_root / "sessions" / session_id / "artifacts"


Expand Down Expand Up @@ -220,6 +246,7 @@ def __init__(self, root_dir: Path | str):

def _base_root(self, user_id: str, /) -> Path:
"""Returns the artifacts root directory for a user."""
_validate_path_segment(user_id, "user_id")
return self.root_dir / "users" / user_id

def _scope_root(
Expand Down
20 changes: 20 additions & 0 deletions src/google/adk/cli/cli_deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,24 @@ def to_cloud_run(
shutil.rmtree(temp_folder)


def _print_agent_engine_url(resource_name: str) -> None:
"""Prints the Google Cloud Console URL for the deployed agent."""
parts = resource_name.split('/')
if len(parts) >= 6 and parts[0] == 'projects' and parts[2] == 'locations':
project_id = parts[1]
region = parts[3]
engine_id = parts[5]

url = (
'https://console.cloud.google.com/agent-platform/runtimes'
f'/locations/{region}/agent-engines/{engine_id}/playground'
f'?project={project_id}'
)
click.secho(
f'\n🎉 View your deployed agent here:\n{url}\n', fg='cyan', bold=True
)


def to_agent_engine(
*,
agent_folder: str,
Expand Down Expand Up @@ -1150,11 +1168,13 @@ def to_agent_engine(
f'✅ Created agent engine: {agent_engine.api_resource.name}',
fg='green',
)
_print_agent_engine_url(agent_engine.api_resource.name)
else:
if project and region and not agent_engine_id.startswith('projects/'):
agent_engine_id = f'projects/{project}/locations/{region}/reasoningEngines/{agent_engine_id}'
client.agent_engines.update(name=agent_engine_id, config=agent_config)
click.secho(f'✅ Updated agent engine: {agent_engine_id}', fg='green')
_print_agent_engine_url(agent_engine_id)
finally:
click.echo(f'Cleaning up the temp folder: {temp_folder}')
shutil.rmtree(agent_src_path)
Expand Down
14 changes: 11 additions & 3 deletions src/google/adk/flows/llm_flows/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,13 @@
_TOOL_THREAD_POOLS: dict[int, ThreadPoolExecutor] = {}
_TOOL_THREAD_POOL_LOCK = threading.Lock()

# Sentinel object used to distinguish a FunctionTool that legitimately returns
# None from a non-FunctionTool sync tool that skips thread-pool execution.
# Using None as a sentinel would cause tools whose underlying function has no
# explicit return statement (implicit None) to fall through to the async
# fallback path and execute a second time.
_SYNC_TOOL_RESULT_UNSET = object()


def _is_live_request_queue_annotation(param: inspect.Parameter) -> bool:
"""Check whether a parameter is annotated as LiveRequestQueue.
Expand Down Expand Up @@ -159,13 +166,14 @@ def run_sync_tool():
}
return tool.func(**args_to_call)
else:
# For other sync tool types, we can't easily run them in thread pool
return None
# For other sync tool types, we can't easily run them in thread pool.
# Return the sentinel so the caller knows to fall back to run_async.
return _SYNC_TOOL_RESULT_UNSET

result = await loop.run_in_executor(
executor, lambda: ctx.run(run_sync_tool)
)
if result is not None:
if result is not _SYNC_TOOL_RESULT_UNSET:
return result
else:
# For async tools, run them in a new event loop in a background thread.
Expand Down
35 changes: 35 additions & 0 deletions src/google/adk/integrations/agent_identity/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# GCP IAM Connector Auth

Manages the complete lifecycle of an access token using the Google Cloud
Platform Agent Identity Credentials service.

## Usage

1. **Install Dependencies:**
```bash
pip install "google-adk[agent-identity]"
```

2. **Register the provider:**
Register the `GcpAuthProvider` with the `CredentialManager`. This is to be
done one time.

``` py
# user_agent_app.py
from google.adk.auth.credential_manager import CredentialManager
from google.adk.integrations.agent_identity import GcpAuthProvider

CredentialManager.register_auth_provider(GcpAuthProvider())
```

3. **Configure the Auth provider:**
Specify the Agent Identity provider configuration using the
`GcpAuthProviderScheme`.
``` py
# user_agent_app.py
from google.adk.integrations.agent_identity import GcpAuthProviderScheme

# Configures Toolset
auth_scheme = GcpAuthProviderScheme(name="my-jira-auth_provider")
mcp_toolset_jira = McpToolset(..., auth_scheme=auth_scheme)
```
21 changes: 21 additions & 0 deletions src/google/adk/integrations/agent_identity/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# 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 .gcp_auth_provider import GcpAuthProvider
from .gcp_auth_provider_scheme import GcpAuthProviderScheme

__all__ = [
"GcpAuthProvider",
"GcpAuthProviderScheme",
]
Loading