Skip to content

Commit db11dbe

Browse files
caohy1988claude
andcommitted
fix: preserve picklable credentials, drop only non-picklable ones
The previous commit cleared both _credentials and _user_credentials unconditionally in __getstate__, silently dropping picklable user credentials (service-account, AnonymousCredentials) after unpickle. This caused the plugin to fall back to ADC instead of the user's configured identity. Now __getstate__ tests whether _user_credentials is picklable: - Picklable: preserved — survives pickle and restored on unpickle - Non-picklable: dropped gracefully — falls back to ADC Adds two tests covering both paths. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent babf2a7 commit db11dbe

2 files changed

Lines changed: 66 additions & 8 deletions

File tree

src/google/adk/plugins/bigquery_agent_analytics_plugin.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2546,12 +2546,18 @@ def __getstate__(self):
25462546
state["_startup_error"] = None
25472547
state["_is_shutting_down"] = False
25482548
state["_init_pid"] = 0
2549-
# Credential objects may hold non-picklable transport state
2550-
# (e.g., requests.Session in compute_engine.Credentials).
2551-
# Clear both so pickle succeeds regardless of credential type.
2552-
# After unpickle, credentials are re-resolved via ADC.
2549+
# _credentials is always runtime-resolved; clear unconditionally.
25532550
state["_credentials"] = None
2554-
state["_user_credentials"] = None
2551+
# Preserve _user_credentials if they are picklable (e.g.,
2552+
# service-account, AnonymousCredentials). Drop only when
2553+
# pickle would fail (e.g., compute_engine.Credentials holding
2554+
# a requests.Session).
2555+
import pickle as _pickle
2556+
2557+
try:
2558+
_pickle.dumps(state.get("_user_credentials"))
2559+
except Exception:
2560+
state["_user_credentials"] = None
25552561
return state
25562562

25572563
def __setstate__(self, state):
@@ -2561,9 +2567,11 @@ def __setstate__(self, state):
25612567
state.setdefault("_init_pid", 0)
25622568
state.setdefault("_user_credentials", None)
25632569
state.setdefault("_credentials", None)
2564-
# Both _credentials and _user_credentials are cleared during
2565-
# pickle. Credentials will be re-resolved via ADC on the next
2566-
# _create_loop_state call.
2570+
# Restore _credentials from _user_credentials if available so
2571+
# _create_loop_state uses the user's identity. When both are
2572+
# None (non-picklable credentials were dropped), ADC is used.
2573+
if state["_credentials"] is None and state["_user_credentials"]:
2574+
state["_credentials"] = state["_user_credentials"]
25672575
self.__dict__.update(state)
25682576

25692577
def _reset_runtime_state(self) -> None:

tests/unittests/plugins/test_bigquery_agent_analytics_plugin.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2093,6 +2093,56 @@ async def test_pickle_safety(self, mock_auth_default, mock_bq_client):
20932093
finally:
20942094
await plugin.shutdown()
20952095

2096+
@pytest.mark.asyncio
2097+
async def test_pickle_preserves_picklable_credentials(
2098+
self, mock_auth_default, mock_bq_client
2099+
):
2100+
"""Picklable user credentials survive pickle/unpickle."""
2101+
import pickle
2102+
2103+
picklable_creds = FakeCredentials()
2104+
plugin = bigquery_agent_analytics_plugin.BigQueryAgentAnalyticsPlugin(
2105+
PROJECT_ID,
2106+
DATASET_ID,
2107+
table_id=TABLE_ID,
2108+
credentials=picklable_creds,
2109+
)
2110+
pickled = pickle.dumps(plugin)
2111+
unpickled = pickle.loads(pickled)
2112+
# User-provided picklable credentials are preserved.
2113+
assert unpickled._user_credentials is not None
2114+
assert unpickled._credentials is not None
2115+
await plugin.shutdown()
2116+
2117+
@pytest.mark.asyncio
2118+
async def test_pickle_drops_non_picklable_credentials(
2119+
self, mock_auth_default, mock_bq_client
2120+
):
2121+
"""Non-picklable user credentials are dropped gracefully."""
2122+
import pickle
2123+
2124+
class NonPicklableCreds(google.auth.credentials.Credentials):
2125+
2126+
def refresh(self, request):
2127+
pass
2128+
2129+
def __getstate__(self):
2130+
raise TypeError("cannot pickle")
2131+
2132+
plugin = bigquery_agent_analytics_plugin.BigQueryAgentAnalyticsPlugin(
2133+
PROJECT_ID,
2134+
DATASET_ID,
2135+
table_id=TABLE_ID,
2136+
credentials=NonPicklableCreds(),
2137+
)
2138+
# Should not raise — non-picklable credentials are dropped.
2139+
pickled = pickle.dumps(plugin)
2140+
unpickled = pickle.loads(pickled)
2141+
# Credentials fall back to None (ADC on next use).
2142+
assert unpickled._user_credentials is None
2143+
assert unpickled._credentials is None
2144+
await plugin.shutdown()
2145+
20962146
@pytest.mark.asyncio
20972147
async def test_span_hierarchy_llm_call(
20982148
self,

0 commit comments

Comments
 (0)