Skip to content

Commit 29f3359

Browse files
authored
Merge branch 'main' into feature/subagent-escalation
2 parents 9566737 + 064f0d2 commit 29f3359

File tree

32 files changed

+614
-98
lines changed

32 files changed

+614
-98
lines changed

contributing/samples/agent_registry_agent/agent.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
from google.adk.agents.llm_agent import LlmAgent
2020
from google.adk.integrations.agent_registry import AgentRegistry
21+
from google.adk.models.google_llm import Gemini
2122

2223
# Project and location can be set via environment variables:
2324
# GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_LOCATION
@@ -27,6 +28,8 @@
2728
# Initialize Agent Registry client
2829
registry = AgentRegistry(project_id=project_id, location=location)
2930

31+
# List agents, MCP servers, and endpoints resource names from the registry.
32+
# They can be used to initialize the agent, toolset, and model below.
3033
print(f"Listing agents in {project_id}/{location}...")
3134
agents = registry.list_agents()
3235
for agent in agents.get("agents", []):
@@ -37,6 +40,11 @@
3740
for server in mcp_servers.get("mcpServers", []):
3841
print(f"- MCP Server: {server.get('displayName')} ({server.get('name')})")
3942

43+
print(f"\nListing endpoints in {project_id}/{location}...")
44+
endpoints = registry.list_endpoints()
45+
for endpoint in endpoints.get("endpoints", []):
46+
print(f"- Endpoint: {endpoint.get('displayName')} ({endpoint.get('name')})")
47+
4048
# Example of using a specific agent or MCP server from the registry:
4149
# (Note: These names should be full resource names as returned by list methods)
4250

@@ -52,8 +60,19 @@
5260
f"projects/{project_id}/locations/{location}/mcpServers/MCP_SERVER_NAME"
5361
)
5462

63+
# 3. Getting a specific model endpoint configuration
64+
# This returns a string like:
65+
# "projects/adk12345/locations/us-central1/publishers/google/models/gemini-2.5-flash"
66+
# TODO: Replace ENDPOINT_NAME with your endpoint name
67+
model_name = registry.get_model_name(
68+
f"projects/{project_id}/locations/{location}/endpoints/ENDPOINT_NAME"
69+
)
70+
71+
# Initialize the model using the resolved model name from registry.
72+
gemini_model = Gemini(model=model_name)
73+
5574
root_agent = LlmAgent(
56-
model="gemini-2.5-flash",
75+
model=gemini_model,
5776
name="discovery_agent",
5877
instruction=(
5978
"You have access to tools and sub-agents discovered via Registry."

contributing/samples/skills_agent/agent.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,9 @@ async def run_async(self, *, args: dict, tool_context) -> str:
5454
return f"The timezone for {args['location']} is UTC+00:00."
5555

5656

57-
def get_current_humidity(location: str) -> str:
58-
"""Returns the current humidity for a given location."""
59-
return f"The humidity in {location} is 45%."
57+
def get_wind_speed(location: str) -> str:
58+
"""Returns the current wind speed for a given location."""
59+
return f"The wind speed in {location} is 10 mph."
6060

6161

6262
greeting_skill = models.Skill(
@@ -87,7 +87,7 @@ def get_current_humidity(location: str) -> str:
8787
# be used in production environments.
8888
my_skill_toolset = SkillToolset(
8989
skills=[greeting_skill, weather_skill],
90-
additional_tools=[GetTimezoneTool(), get_current_humidity],
90+
additional_tools=[GetTimezoneTool(), get_wind_speed],
9191
code_executor=UnsafeLocalCodeExecutor(),
9292
)
9393

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
---
22
name: weather-skill
33
description: A skill that provides weather information based on reference data.
4+
metadata:
5+
adk_additional_tools:
6+
- get_wind_speed
47
---
58

69
Step 1: Check 'references/weather_info.md' for the current weather.
710
Step 2: If humidity is requested, use run 'scripts/get_humidity.py' with the `location` argument.
8-
Step 3: Provide the update to the user.
11+
Step 3: If wind speed is requested, use the `get_wind_speed` tool.
12+
Step 4: Provide the update to the user.

contributing/samples/skills_agent/skills/weather_skill/SKILL.md

Lines changed: 0 additions & 10 deletions
This file was deleted.

src/google/adk/a2a/agent/config.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
from __future__ import annotations
1818

19+
import copy
1920
from typing import Any
2021
from typing import Awaitable
2122
from typing import Callable
@@ -108,3 +109,16 @@ class A2aRemoteAgentConfig(BaseModel):
108109
)
109110

110111
request_interceptors: Optional[list[RequestInterceptor]] = None
112+
113+
def __deepcopy__(self, memo):
114+
cls = self.__class__
115+
copied_values = {}
116+
for k, v in self.__dict__.items():
117+
if not k.startswith('_'):
118+
if callable(v):
119+
copied_values[k] = v
120+
else:
121+
copied_values[k] = copy.deepcopy(v, memo)
122+
result = cls.model_construct(**copied_values)
123+
memo[id(self)] = result
124+
return result

src/google/adk/a2a/executor/a2a_agent_executor_impl.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
from typing_extensions import override
3939

4040
from ...runners import Runner
41+
from ...sessions import base_session_service
4142
from ...utils.context_utils import Aclosing
4243
from ..agent.interceptors.new_integration_extension import _NEW_A2A_ADK_INTEGRATION_EXTENSION
4344
from ..converters.from_adk_event import create_error_status_event
@@ -287,6 +288,8 @@ async def _resolve_session(
287288
app_name=runner.app_name,
288289
user_id=user_id,
289290
session_id=session_id,
291+
# Checking existence doesn't require event history.
292+
config=base_session_service.GetSessionConfig(num_recent_events=0),
290293
)
291294
if session is None:
292295
session = await runner.session_service.create_session(

src/google/adk/auth/auth_credential.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ class OAuth2Auth(BaseModelWithConfig):
7373
# tool or adk can generate the auth_uri with the state info thus client
7474
# can verify the state
7575
auth_uri: Optional[str] = None
76+
# A unique value generated at the start of the OAuth flow to bind the user's
77+
# session to the authorization request. This value is typically stored with
78+
# user session and passed to backend for validation.
79+
nonce: Optional[str] = None
7680
state: Optional[str] = None
7781
# tool or adk can decide the redirect_uri if they don't want client to decide
7882
redirect_uri: Optional[str] = None

src/google/adk/cli/cli_tools_click.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1713,7 +1713,8 @@ def cli_api_server(
17131713
default=False,
17141714
help=(
17151715
"Optional. Deploy ADK Web UI if set. (default: deploy ADK API server"
1716-
" only)"
1716+
" only). WARNING: The web UI is for development and testing only — do"
1717+
" not use in production."
17171718
),
17181719
)
17191720
@click.option(
@@ -2229,7 +2230,8 @@ def cli_deploy_agent_engine(
22292230
default=False,
22302231
help=(
22312232
"Optional. Deploy ADK Web UI if set. (default: deploy ADK API server"
2232-
" only)"
2233+
" only). WARNING: The web UI is for development and testing only — do"
2234+
" not use in production."
22332235
),
22342236
)
22352237
@click.option(

src/google/adk/cli/fast_api.py

Lines changed: 60 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
import click
2929
from fastapi import FastAPI
30+
from fastapi import HTTPException
3031
from fastapi import UploadFile
3132
from fastapi.responses import FileResponse
3233
from fastapi.responses import PlainTextResponse
@@ -293,6 +294,39 @@ def _has_parent_reference(path: str) -> bool:
293294

294295
_ALLOWED_EXTENSIONS = frozenset({".yaml", ".yml"})
295296

297+
# --- YAML content security ---
298+
# The `args` key in agent YAML configs (CodeConfig.args, ToolConfig.args)
299+
# allows callers to pass arbitrary arguments to Python constructors and
300+
# functions, which is an RCE vector when exposed through the builder UI.
301+
# Block any upload that contains an `args` key anywhere in the document.
302+
_BLOCKED_YAML_KEYS = frozenset({"args"})
303+
304+
def _check_yaml_for_blocked_keys(content: bytes, filename: str) -> None:
305+
"""Raise if the YAML document contains any blocked keys."""
306+
import yaml
307+
308+
try:
309+
docs = list(yaml.safe_load_all(content))
310+
except yaml.YAMLError as exc:
311+
raise ValueError(f"Invalid YAML in {filename!r}: {exc}") from exc
312+
313+
def _walk(node: Any) -> None:
314+
if isinstance(node, dict):
315+
for key, value in node.items():
316+
if key in _BLOCKED_YAML_KEYS:
317+
raise ValueError(
318+
f"Blocked key {key!r} found in {filename!r}. "
319+
f"The '{key}' field is not allowed in builder uploads "
320+
"because it can execute arbitrary code."
321+
)
322+
_walk(value)
323+
elif isinstance(node, list):
324+
for item in node:
325+
_walk(item)
326+
327+
for doc in docs:
328+
_walk(doc)
329+
296330
def _parse_upload_filename(filename: Optional[str]) -> tuple[str, str]:
297331
if not filename:
298332
raise ValueError("Upload filename is missing.")
@@ -430,40 +464,14 @@ async def builder_build(
430464
files: list[UploadFile], tmp: Optional[bool] = False
431465
) -> bool:
432466
try:
433-
if tmp:
434-
app_names = set()
435-
uploads = []
436-
for file in files:
437-
app_name, rel_path = _parse_upload_filename(file.filename)
438-
app_names.add(app_name)
439-
uploads.append((rel_path, file))
440-
441-
if len(app_names) != 1:
442-
logger.error(
443-
"Exactly one app name is required, found: %s",
444-
sorted(app_names),
445-
)
446-
return False
447-
448-
app_name = next(iter(app_names))
449-
app_root = _get_app_root(app_name)
450-
tmp_agent_root = _get_tmp_agent_root(app_root, app_name)
451-
tmp_agent_root.mkdir(parents=True, exist_ok=True)
452-
453-
for rel_path, file in uploads:
454-
destination_path = _resolve_under_dir(tmp_agent_root, rel_path)
455-
destination_path.parent.mkdir(parents=True, exist_ok=True)
456-
with destination_path.open("wb") as buffer:
457-
shutil.copyfileobj(file.file, buffer)
458-
459-
return True
460-
461-
app_names = set()
462-
uploads = []
467+
# Phase 1: parse filenames and read content into memory.
468+
app_names: set[str] = set()
469+
uploads: list[tuple[str, bytes]] = []
463470
for file in files:
464471
app_name, rel_path = _parse_upload_filename(file.filename)
465472
app_names.add(app_name)
466-
uploads.append((rel_path, file))
473+
content = await file.read()
474+
uploads.append((rel_path, content))
467475

468476
if len(app_names) != 1:
469477
logger.error(
@@ -473,23 +481,40 @@ async def builder_build(
473481
return False
474482

475483
app_name = next(iter(app_names))
484+
485+
# Phase 2: validate every file *before* writing anything to disk.
486+
for rel_path, content in uploads:
487+
_check_yaml_for_blocked_keys(content, f"{app_name}/{rel_path}")
488+
489+
# Phase 3: write validated files to disk.
490+
if tmp:
491+
app_root = _get_app_root(app_name)
492+
tmp_agent_root = _get_tmp_agent_root(app_root, app_name)
493+
tmp_agent_root.mkdir(parents=True, exist_ok=True)
494+
495+
for rel_path, content in uploads:
496+
destination_path = _resolve_under_dir(tmp_agent_root, rel_path)
497+
destination_path.parent.mkdir(parents=True, exist_ok=True)
498+
destination_path.write_bytes(content)
499+
500+
return True
501+
476502
app_root = _get_app_root(app_name)
477503
app_root.mkdir(parents=True, exist_ok=True)
478504

479505
tmp_agent_root = _get_tmp_agent_root(app_root, app_name)
480506
if tmp_agent_root.is_dir():
481507
copy_dir_contents(tmp_agent_root, app_root)
482508

483-
for rel_path, file in uploads:
509+
for rel_path, content in uploads:
484510
destination_path = _resolve_under_dir(app_root, rel_path)
485511
destination_path.parent.mkdir(parents=True, exist_ok=True)
486-
with destination_path.open("wb") as buffer:
487-
shutil.copyfileobj(file.file, buffer)
512+
destination_path.write_bytes(content)
488513

489514
return cleanup_tmp(app_name)
490515
except ValueError as exc:
491516
logger.exception("Error in builder_build: %s", exc)
492-
return False
517+
raise HTTPException(status_code=400, detail=str(exc))
493518
except OSError as exc:
494519
logger.exception("Error in builder_build: %s", exc)
495520
return False

src/google/adk/features/_feature_registry.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ class FeatureName(str, Enum):
5353
TOOL_CONFIRMATION = "TOOL_CONFIRMATION"
5454
PLUGGABLE_AUTH = "PLUGGABLE_AUTH"
5555
SNAKE_CASE_SKILL_NAME = "SNAKE_CASE_SKILL_NAME"
56+
IN_MEMORY_SESSION_SERVICE_LIGHT_COPY = "IN_MEMORY_SESSION_SERVICE_LIGHT_COPY"
5657

5758

5859
class FeatureStage(Enum):
@@ -166,6 +167,9 @@ class FeatureConfig:
166167
FeatureName.SNAKE_CASE_SKILL_NAME: FeatureConfig(
167168
FeatureStage.EXPERIMENTAL, default_on=False
168169
),
170+
FeatureName.IN_MEMORY_SESSION_SERVICE_LIGHT_COPY: FeatureConfig(
171+
FeatureStage.WIP, default_on=False
172+
),
169173
}
170174

171175
# Track which experimental features have already warned (warn only once)

0 commit comments

Comments
 (0)