Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
104 commits
Select commit Hold shift + click to select a range
74daf17
security: add RestrictedUnpickler for safe v0 pickle deserialization
daridor9 May 7, 2026
41df699
security: use RestrictedUnpickler in v0 runtime read path
daridor9 May 7, 2026
99c79fb
chore: retrigger CLA check after signing
daridor9 May 7, 2026
e0ccf64
security: add enum types to RestrictedUnpickler allowlist
daridor9 May 7, 2026
7aeba1b
style: run pyink + isort on _safe_unpickle.py
daridor9 May 9, 2026
37b8fed
style: run pyink + isort on v0.py
daridor9 May 9, 2026
fc1cad5
style: run pyink + isort on migrate_from_sqlalchemy_pickle.py
daridor9 May 9, 2026
c5218fa
security: override result_processor for SQLite safe deserialization
daridor9 May 11, 2026
a9b863e
Add unit tests for RestrictedUnpickler functionality
daridor9 May 11, 2026
9f9c67e
test: add real EventActions round-trip smoke tests
daridor9 May 18, 2026
5c0ba06
fix(sessions): annotate result_processor for mypy-diff; pyink-format …
daridor9 May 30, 2026
705e2bf
Merge branch 'main' into fix/restricted-unpickler-v0
daridor9 Jun 7, 2026
8098d76
ADK changes
sasha-gitg May 19, 2026
ba69174
chore: update runtime config
google-genai-bot May 19, 2026
abdbb30
chore: Remove experimental tag from SkillToolset
wukath May 19, 2026
ddc0d82
fix(cli): add missing newline at end of runtime-config.json
GWeale May 20, 2026
8537ccc
fix(deps): pin yarl to <1.24
GWeale May 20, 2026
8c7145b
fix: constrain Agent Builder roots and block file path escapes
GWeale May 20, 2026
ffef2d9
fix(flows): preserve transparent config on live session reconnect
GWeale May 20, 2026
1a356ef
fix: constrain Agent Builder roots and block file path escapes
google-genai-bot May 20, 2026
cde8156
feat: Add support for creating sandboxes from templates and snapshots
google-genai-bot May 21, 2026
a279a2f
feat: Preserve transcription event order in conversation trajectory
google-genai-bot May 21, 2026
bb6d132
fix: resolve circular import caused by llm_request
GWeale May 21, 2026
3a5035f
feat(samples): Add chart generation and artifact loading to data agent
google-genai-bot May 22, 2026
1c8e831
fix: Use Pydantic TypeAdapter in FunctionTool for automatic argument …
enisher May 22, 2026
2f698ea
feat: emit OTel gen_ai.client.* metrics natively
google-genai-bot May 22, 2026
c5fb92f
fix: Use Pydantic TypeAdapter in FunctionTool for automatic argument …
google-genai-bot May 22, 2026
ce33b98
ADK changes
DeanChensj May 26, 2026
7310464
feat: support custom request metadata inside local Dev UI
google-genai-bot May 27, 2026
8da01ef
Chore: Updates the `open_source_workspace/` directory to align with t…
DeanChensj May 27, 2026
8a3f7e1
fix: support PEP 604 union syntax in function tool parameters and ens…
google-genai-bot May 27, 2026
a3257d0
chore: Synchronize packages under integrations/ and its unittests wit…
DeanChensj May 27, 2026
708dc0e
chore: Sync contributing/samples with main branch
DeanChensj May 27, 2026
3ee7c0b
fix(tools): don't close parent's plugins from AgentTool's sub-Runner
google-genai-bot May 27, 2026
d3841d6
chore: Provide a default value for the `author` field in Event
DeanChensj May 28, 2026
1fbba3b
fix: Append trailing newline to runtime-config.json in ADK Web Server
google-genai-bot May 28, 2026
eada370
feat: Add RubricBasedMultiTurnTrajectoryEvaluator which evaluates an …
google-genai-bot May 28, 2026
d3167aa
chore: Provide a default value for the `author` field in Event
google-genai-bot May 29, 2026
2084efc
ADK changes
google-genai-bot May 29, 2026
2e998d8
feat(plugins): add AutoTracingPlugin for OpenTelemetry auto-instrumen…
shukladivyansh May 29, 2026
ae44435
perf(flows): resolve agent tool unions in parallel
google-genai-bot May 29, 2026
3b7d7a7
feat: Preserved A2A message metadata field into ADK event
google-genai-bot Jun 1, 2026
13ca145
feat: Preserved A2A message metadata field into ADK event
google-genai-bot Jun 1, 2026
c7cacec
chore: bring over v2
GWeale Jun 1, 2026
1ff5342
fix: Disable JSON_SCHEMA_FOR_FUNC_DECL by default
xuanyang15 Jun 2, 2026
05635a2
fix: guard peer agent mode access in agent transfer
GWeale Jun 2, 2026
72e0a74
feat(plugins): drop-event observability, cross-region writes, and no …
haiyuan-eng-google Jun 2, 2026
db6cc52
fix: populate user_content in resumed invocations
GWeale Jun 2, 2026
ef19613
chore: Add cli, integrations, and skills to GitHub issue triaging rubric
DeanChensj Jun 3, 2026
750ea83
fix(runners): fall back to root agent when a resumed call author is n…
google-genai-bot Jun 3, 2026
7fd75b2
feat: Distinguish between input-required and auth-required states in …
Tongzhou-Jiang Jun 3, 2026
a1b7a4b
feat: Preserved A2A message metadata field into ADK event
google-genai-bot Jun 4, 2026
061d20c
fix: scope telemetry metric assertions to the test's own agent
GWeale Jun 4, 2026
8b564f5
fix: block path traversal in Agent Builder file tools
GWeale Jun 4, 2026
56f5641
fix: Disable JSON_SCHEMA_FOR_FUNC_DECL by default
google-genai-bot Jun 4, 2026
9133858
feat: Add sample agent demonstrating 2LO, 3LO, and API Key auth via G…
google-genai-bot Jun 5, 2026
428e789
fix(workflow): Resolve raw Content output crash on rehydration
wyf7107 Jun 5, 2026
2ad2005
test(events): add unit tests for Event helper methods
wyf7107 Jun 5, 2026
8befdb8
fix(streaming): Ensure final partial=False frame is always yielded
wyf7107 Jun 5, 2026
1d055e3
fix(migration): restrict unpickling of v0 actions blobs
wyf7107 Jun 5, 2026
5b06baf
fix: retry TypeAdapter creation without config if it fails
google-genai-bot Jun 5, 2026
cd507c1
feat: include thoughts and tool calls in compaction summaries
wyf7107 Jun 5, 2026
7e0c7e0
fix: allow internal builder assistant app name
wyf7107 Jun 5, 2026
50d00d2
chore: update adk web, support a2ui and fix thought part
wyf7107 Jun 5, 2026
8f43f9c
refactor(tests): Consolidate event tests into test_event.py
wyf7107 Jun 5, 2026
f2d70c2
chore: remove gemini-cli github action workflows due to security vuln…
wyf7107 Jun 5, 2026
09003fe
fix: add missing crop helper to data file helper lib
wyf7107 Jun 5, 2026
e248323
Merge branch 'main' into fix/restricted-unpickler-v0
daridor9 Jun 8, 2026
34b4de0
fix(cli): Serialize LiteLlm graph models safely
wyf7107 Jun 5, 2026
4eb337e
ADK changes
DeanChensj Jun 5, 2026
e7eb5fe
fix: Support generalized history config injection for Gemini 3.1 Live…
wyf7107 Jun 5, 2026
77356d8
No public description
wyf7107 Jun 5, 2026
faca170
feat: Support additional scopes and custom discovery doc in Google AP…
wyf7107 Jun 5, 2026
74cfad9
fix: accept Azure assistant file ids
wyf7107 Jun 5, 2026
483b545
feat: expose httpx_client_factory on RestApiTool and OpenAPIToolset
wyf7107 Jun 5, 2026
254401d
fix(a2a): Support to_a2a(Workflow) and reject non-agent root nodes
wyf7107 Jun 5, 2026
7ab188c
chore: 2.2.0 release
DeanChensj Jun 5, 2026
9799b49
fix: Fix path traversal in GCS skill extraction (Zip Slip)
wyf7107 Jun 5, 2026
1321bf4
fix(models): default grounding_metadata to empty for Gemini 3.1 live …
wukath Jun 6, 2026
0113324
feat(cli): Add --trigger_sources and service URIs to cli_deploy and a…
GWeale Jun 6, 2026
3e588cf
ADK changes
DeanChensj Jun 6, 2026
77506f2
feat: align OTel gen_ai.usage.* token telemetry with semconv v1.41.0
google-genai-bot Jun 6, 2026
851e888
fix: parse noncanonical litellm tool call arguments
wyf7107 Jun 7, 2026
8251dba
feat: raise explicit error for unsupported LiteLlm file attachments
wyf7107 Jun 7, 2026
63ce7d3
fix: preserve media blocks in ollama content flattening
wyf7107 Jun 7, 2026
07a66b5
chore: fix CI pre-commit
DeanChensj Jun 7, 2026
b29caaa
No public description
DeanChensj Jun 8, 2026
8ce0181
fix: validate session_id and enforce ownership in delete_session
wyf7107 Jun 8, 2026
1be1bec
fix: add artifacts in each agent's .adk folder
wyf7107 Jun 8, 2026
ba4180f
fix: fix compaction tests visibility
wikaaaaa Jun 8, 2026
53ede14
refactor: centralize token usage telemetry logic
google-genai-bot Jun 8, 2026
8fa47fb
docs(skills): Update skill file formatting
Jacksunwei Jun 8, 2026
c9006ad
ADK changes
DeanChensj Jun 8, 2026
d2e4d92
chore: Sync missing cli changes from ffa057c on GitHub
DeanChensj Jun 8, 2026
aaa1bbb
fix: restore Gemini 3.1 Flash Live 1007-error handling on Vertex
GWeale Jun 8, 2026
5346305
fix: restore GitHub-only changes dropped during v2 bring-over
GWeale Jun 8, 2026
e1b513b
Merge branch 'main' into fix/restricted-unpickler-v0
daridor9 Jun 8, 2026
60ef035
Merge branch 'main' into fix/restricted-unpickler-v0
daridor9 Jun 9, 2026
f2e2f71
Merge branch 'main' into fix/restricted-unpickler-v0
daridor9 Jun 9, 2026
6fe02fb
Merge branch 'main' into fix/restricted-unpickler-v0
daridor9 Jun 9, 2026
f00bc63
Merge branch 'main' into fix/restricted-unpickler-v0
daridor9 Jun 9, 2026
fb2bdba
Merge branch 'main' into fix/restricted-unpickler-v0
daridor9 Jun 12, 2026
2e02d4e
Merge branch 'main' into fix/restricted-unpickler-v0
daridor9 Jun 13, 2026
0b909d0
Merge branch 'main' into fix/restricted-unpickler-v0
daridor9 Jun 13, 2026
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
101 changes: 101 additions & 0 deletions src/google/adk/sessions/schemas/_safe_unpickle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# 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.
"""Restricted unpickler for safe deserialization of v0 EventActions data.

The v0 schema stored EventActions as pickle blobs. This module provides a
safe deserialization path that only allows known ADK and standard types,
blocking arbitrary code execution via crafted pickle payloads.

See: https://docs.python.org/3/library/pickle.html#restricting-globals
"""

from __future__ import annotations

import io
import logging
import os
import pickle
from typing import Any

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

_ALLOWED_MODULE_PREFIXES: tuple[str, ...] = (
"google.adk.",
"google.genai.",
"pydantic.",
"pydantic_core.",
)

_ALLOWED_GLOBALS: dict[str, set[str]] = {
"builtins": {
"dict",
"list",
"set",
"tuple",
"frozenset",
"bytes",
"bytearray",
"True",
"False",
"None",
"type",
"object",
"complex",
"slice",
"range",
"int",
"float",
"str",
"bool",
},
"collections": {"OrderedDict", "defaultdict"},
"datetime": {"datetime", "date", "time", "timedelta", "timezone"},
"copy_reg": {"_reconstructor"},
"copyreg": {"_reconstructor", "__newobj__"},
"_codecs": {"encode"},
"enum": {"__new__", "Enum", "IntEnum", "StrEnum"},
}


class _RestrictedUnpickler(pickle.Unpickler):
"""Unpickler that only allows reconstruction of known-safe types."""

def find_class(self, module: str, name: str) -> Any:
for prefix in _ALLOWED_MODULE_PREFIXES:
if module.startswith(prefix):
return super().find_class(module, name)
allowed_names = _ALLOWED_GLOBALS.get(module)
if allowed_names and name in allowed_names:
return super().find_class(module, name)
raise pickle.UnpicklingError(
f"Blocked unsafe pickle global: {module}.{name}. "
"If this is a legitimate ADK type, please file an issue at "
"https://github.com/google/adk-python/issues"
)


def safe_loads(data: bytes) -> Any:
"""Deserialize pickle bytes using a restricted unpickler.

If ADK_ALLOW_UNSAFE_V0_PICKLE=1 is set, falls back to unrestricted
pickle.loads() for compatibility. A deprecation warning is logged.
"""
if os.environ.get("ADK_ALLOW_UNSAFE_V0_PICKLE") == "1":
logger.warning(
"ADK_ALLOW_UNSAFE_V0_PICKLE is set - using unrestricted "
"pickle.loads(). This is unsafe and will be removed in a "
"future release. Migrate to the v1 JSON schema."
)
return pickle.loads(data) # noqa: S301
return _RestrictedUnpickler(io.BytesIO(data)).load()
16 changes: 15 additions & 1 deletion src/google/adk/sessions/schemas/v0.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
from ...events.event import Event
from ...events.event_actions import EventActions
from ..session import Session
from ._safe_unpickle import safe_loads as _safe_pickle_loads
from .shared import DEFAULT_MAX_KEY_LENGTH
from .shared import DEFAULT_MAX_VARCHAR_LENGTH
from .shared import DynamicJSON
Expand Down Expand Up @@ -110,11 +111,24 @@ def process_bind_param(self, value, dialect):
return pickle.dumps(value)
return value

def result_processor(self, dialect: Any, coltype: Any) -> Any:
if dialect.name in ("mysql", "spanner+spanner"):
return super().result_processor(dialect, coltype)

def process(value: Any) -> Any:
if value is None:
return None
if isinstance(value, memoryview):
value = bytes(value)
return _safe_pickle_loads(value)

return process

def process_result_value(self, value, dialect):
"""Ensures the raw bytes from the database are unpickled back into a Python object."""
if value is not None:
if dialect.name in ("spanner+spanner", "mysql"):
return pickle.loads(value)
return _safe_pickle_loads(value)
return value


Expand Down
16 changes: 16 additions & 0 deletions tests/unittests/plugins/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
load("//third_party/py/pytest:pytest_defs.bzl", "pytest_test")

package(
default_applicable_licenses = ["//third_party/py/google/adk:package_license"],
default_visibility = ["//visibility:private"],
)

pytest_test(
name = "test_auto_tracing_plugin",
srcs = ["test_auto_tracing_plugin.py"],
deps = [
"//third_party/py/google/adk",
"//third_party/py/opentelemetry:opentelemetry_api",
"//third_party/py/opentelemetry:opentelemetry_sdk",
],
)
190 changes: 190 additions & 0 deletions tests/unittests/sessions/test_safe_unpickle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
# 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.

"""Tests for _safe_unpickle RestrictedUnpickler."""

from __future__ import annotations

import io
import os
import pickle
import struct
import unittest

from google.adk.events.event_actions import EventActions
from google.adk.sessions.schemas._safe_unpickle import safe_loads


def _make_global_payload(module: str, func: str, *args: str) -> bytes:
"""Craft a pickle stream that calls module.func(*args)."""
buf = io.BytesIO()
buf.write(pickle.PROTO + struct.pack("B", 2))
buf.write(b"c" + f"{module}\n{func}\n".encode())
buf.write(b"(")
for arg in args:
encoded = arg.encode("utf-8")
buf.write(
pickle.SHORT_BINUNICODE + struct.pack("<B", len(encoded)) + encoded
)
buf.write(b"t")
buf.write(b"R")
buf.write(b".")
return buf.getvalue()


class TestBlockedPayloads(unittest.TestCase):
"""Malicious pickle payloads must be blocked."""

def test_os_system(self):
with self.assertRaises(pickle.UnpicklingError):
safe_loads(_make_global_payload("os", "system", "echo pwned"))

def test_subprocess_popen(self):
with self.assertRaises(pickle.UnpicklingError):
safe_loads(_make_global_payload("subprocess", "Popen", "id"))

def test_builtins_import(self):
with self.assertRaises(pickle.UnpicklingError):
safe_loads(_make_global_payload("builtins", "__import__", "os"))

def test_posix_system(self):
with self.assertRaises(pickle.UnpicklingError):
safe_loads(_make_global_payload("posix", "system", "whoami"))

def test_nt_system(self):
with self.assertRaises(pickle.UnpicklingError):
safe_loads(_make_global_payload("nt", "system", "whoami"))

def test_builtins_eval(self):
with self.assertRaises(pickle.UnpicklingError):
safe_loads(
_make_global_payload(
"builtins", "eval", "__import__('os').system('id')"
)
)


class TestEventActionsRoundTrip(unittest.TestCase):
"""Legitimate EventActions data must survive pickle -> safe_loads."""

def _round_trip(self, obj):
return safe_loads(pickle.dumps(obj))

def test_string_values(self):
original = {"state_delta": {"key": "value"}, "artifact_delta": {}}
self.assertEqual(self._round_trip(original), original)

def test_nested_dict(self):
original = {
"state_delta": {
"user_prefs": {"theme": "dark", "lang": "en"},
"counter": 42,
},
"artifact_delta": {"files": ["a.txt", "b.txt"]},
}
self.assertEqual(self._round_trip(original), original)

def test_none_and_bool(self):
original = {
"skip_summarization": True,
"requested_auth_configs": None,
"escalate": False,
}
self.assertEqual(self._round_trip(original), original)

def test_empty_dict(self):
self.assertEqual(self._round_trip({}), {})


class TestRealEventActionsRoundTrip(unittest.TestCase):
"""Smoke test: real EventActions instances survive pickle -> safe_loads."""

def _round_trip(self, obj):
return safe_loads(pickle.dumps(obj))

def test_minimal_event_actions(self):
original = EventActions()
result = self._round_trip(original)
self.assertIsInstance(result, EventActions)
self.assertEqual(result.state_delta, {})
self.assertEqual(result.artifact_delta, {})

def test_event_actions_with_state_delta(self):
original = EventActions(
state_delta={"user_name": "alice", "turn_count": 3, "active": True},
artifact_delta={"report.pdf": 2},
)
result = self._round_trip(original)
self.assertIsInstance(result, EventActions)
self.assertEqual(result.state_delta, original.state_delta)
self.assertEqual(result.artifact_delta, original.artifact_delta)

def test_event_actions_with_transfer_and_escalate(self):
original = EventActions(
transfer_to_agent="specialist_agent",
escalate=True,
skip_summarization=True,
)
result = self._round_trip(original)
self.assertIsInstance(result, EventActions)
self.assertEqual(result.transfer_to_agent, "specialist_agent")
self.assertTrue(result.escalate)
self.assertTrue(result.skip_summarization)

def test_event_actions_with_complex_state_values(self):
original = EventActions(
state_delta={
"nested": {"a": [1, 2, 3], "b": None},
"count": 42,
"tags": ["ml", "security"],
},
)
result = self._round_trip(original)
self.assertIsInstance(result, EventActions)
self.assertEqual(result.state_delta["nested"]["a"], [1, 2, 3])
self.assertIsNone(result.state_delta["nested"]["b"])


class TestEnvVarFallback(unittest.TestCase):
"""ADK_ALLOW_UNSAFE_V0_PICKLE=1 must bypass RestrictedUnpickler."""

_ENV_KEY = "ADK_ALLOW_UNSAFE_V0_PICKLE"
_PAYLOAD = _make_global_payload("collections", "Counter")

def test_blocked_without_env_var(self):
old = os.environ.pop(self._ENV_KEY, None)
try:
with self.assertRaises(pickle.UnpicklingError):
safe_loads(self._PAYLOAD)
finally:
if old is not None:
os.environ[self._ENV_KEY] = old

def test_allowed_with_env_var(self):
old = os.environ.get(self._ENV_KEY)
try:
os.environ[self._ENV_KEY] = "1"
from collections import Counter

result = safe_loads(self._PAYLOAD)
self.assertIsInstance(result, Counter)
finally:
if old is None:
os.environ.pop(self._ENV_KEY, None)
else:
os.environ[self._ENV_KEY] = old


if __name__ == "__main__":
unittest.main()
55 changes: 55 additions & 0 deletions tests/unittests/tools/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
load("//third_party/py/pytest:pytest_defs.bzl", "pytest_test")

package(
default_applicable_licenses = ["//third_party/py/google/adk:package_license"],
default_visibility = ["//visibility:private"],
)

pytest_test(
name = "test_local_environment",
srcs = ["test_local_environment.py"],
args = [
"-p",
"pytest_asyncio.plugin",
],
deps = [
"//third_party/py/google/adk",
"//third_party/py/pytest_asyncio",
],
)

pytest_test(
name = "test_skill_toolset",
srcs = ["test_skill_toolset.py"],
args = [
"-p",
"pytest_asyncio.plugin",
],
deps = [
"//third_party/py/google/adk",
"//third_party/py/google/genai",
"//third_party/py/pytest_asyncio",
],
)

pytest_test(
name = "test_environment_toolset",
srcs = ["test_environment_toolset.py"],
args = [
"-p",
"pytest_asyncio.plugin",
],
deps = [
"//third_party/py/google/adk",
"//third_party/py/pytest_asyncio",
],
)

pytest_test(
name = "test_function_tool_declarations",
srcs = ["test_function_tool_declarations.py"],
deps = [
"//third_party/py/absl/testing:parameterized",
"//third_party/py/google/adk",
],
)
Loading