馃敶 Required Information
Describe the Bug:
RestApiTool.call() adds the tool's credential to the args dict it was given, in place:
# src/google/adk/tools/openapi_tool/openapi_spec_parser/rest_api_tool.py
api_params, api_args = self._operation_parser.get_parameters().copy(), args
...
api_args.update(auth_args) # e.g. {"_auth_prefix_vaf_X-API-Key": "<key>"} or "Bearer <token>"
That dict is not private to the tool. The tool pipeline in flows/llm_flows/tools/_caller.py hands the same function_args object to:
- every plugin and agent
after_tool_callback (as tool_args / args), and
trace_tool_call(), which runs after the tool returns and writes it to the execute_tool span as gcp.vertex.agent.tool_call_args. Content capture is on by default (ADK_CAPTURE_MESSAGE_CONTENT_IN_SPANS).
So the API key, or the end user's OAuth bearer token, ends up in trace backends (Cloud Trace or any OTel exporter) and in whatever after-tool plugins record, for example analytics or logging plugins. That covers every toolset built on RestApiTool: OpenAPIToolset, APIHubToolset, GoogleApiToolset, and ApplicationIntegrationToolset.
IntegrationConnectorTool.run_async() does the same thing one level up. It writes dynamic_auth_config = {"oauth2_auth_code_flow.access_token": <end-user token>} into the caller's args, then logs the whole dict at INFO:
logger.info('Running tool: %s with args: %s', self.name, args)
AuthenticatedFunctionTool already avoids this (args_to_call = args.copy()); these two tools don't.
The session is not affected: the persisted FunctionCall args are deep-copied before the tool runs.
Steps to Reproduce:
pip install google-adk (reproduced on main @ f33d492 / 2.9.0).
- Run the script below: a real
Runner plus LlmAgent plus OpenAPIToolset with API-key auth. The model is mocked and the HTTP call is stubbed, so no API key or network is needed.
Expected Behavior:
The credential is sent in the HTTP request only. The after_tool_callback and the execute_tool span see the model's arguments: {"order_id": "A-42"}.
Observed Behavior:
HTTP headers sent : {'User-Agent': 'google-adk/2.9.0 (tool: get_order)', 'X-API-Key': 'sk-live-SECRET-API-KEY-123'}
after_tool_callback args : {'order_id': 'A-42', '_auth_prefix_vaf_X-API-Key': 'sk-live-SECRET-API-KEY-123'}
execute_tool span args : ['{"order_id": "A-42", "_auth_prefix_vaf_X-API-Key": "sk-live-SECRET-API-KEY-123"}']
secret in callback args : True
secret in span attribute : True
Environment Details:
- ADK Library Version (pip show google-adk): 2.9.0 (
main @ f33d492)
- Desktop OS: Windows 11
- Python Version (python -V): 3.12.10
Model Information:
- Are you using LiteLLM: No
- Which model is being used: N/A (mocked model; the bug is in the tool layer)
馃煛 Optional Information
Minimal Reproduction Code:
import asyncio, json
from unittest.mock import MagicMock, patch
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
exporter = InMemorySpanExporter()
provider = TracerProvider()
provider.add_span_processor(SimpleSpanProcessor(exporter))
trace.set_tracer_provider(provider)
from google.adk.agents.llm_agent import LlmAgent
from google.adk.models.base_llm import BaseLlm
from google.adk.models.llm_response import LlmResponse
from google.adk.runners import InMemoryRunner
from google.adk.tools.openapi_tool.auth.auth_helpers import token_to_scheme_credential
from google.adk.tools.openapi_tool.openapi_spec_parser.openapi_toolset import OpenAPIToolset
from google.genai import types
SECRET = "sk-live-SECRET-API-KEY-123"
spec = {
"openapi": "3.0.0",
"info": {"title": "Orders", "version": "1"},
"servers": [{"url": "https://erp.example.com"}],
"paths": {"/orders/{order_id}": {"get": {
"operationId": "getOrder",
"parameters": [{"name": "order_id", "in": "path", "required": True,
"schema": {"type": "string"}}],
"responses": {"200": {"description": "ok"}},
}}},
}
scheme, cred = token_to_scheme_credential("apikey", "header", "X-API-Key", SECRET)
class FakeModel(BaseLlm):
model: str = "fake"
calls: int = 0
async def generate_content_async(self, llm_request, stream=False):
self.calls += 1
part = (types.Part.from_function_call(name="get_order", args={"order_id": "A-42"})
if self.calls == 1 else types.Part.from_text(text="done"))
yield LlmResponse(content=types.Content(role="model", parts=[part]))
seen = {}
agent = LlmAgent(
name="erp_agent",
model=FakeModel(),
tools=[OpenAPIToolset(spec_dict=spec, auth_scheme=scheme, auth_credential=cred)],
after_tool_callback=lambda tool, args, tool_context, tool_response: seen.update(args),
)
async def main():
resp = MagicMock(status_code=200)
resp.json.return_value = {"order": "A-42", "status": "shipped"}
with patch("google.adk.tools.openapi_tool.openapi_spec_parser.rest_api_tool._request",
return_value=resp):
runner = InMemoryRunner(agent=agent)
session = await runner.session_service.create_session(app_name=runner.app_name, user_id="u")
async for _ in runner.run_async(
user_id="u", session_id=session.id,
new_message=types.Content(role="user", parts=[types.Part(text="status of A-42?")])):
pass
span_args = [s.attributes.get("gcp.vertex.agent.tool_call_args")
for s in exporter.get_finished_spans() if s.name.startswith("execute_tool")]
print("after_tool_callback args :", seen)
print("execute_tool span args :", span_args)
print("secret in callback args :", SECRET in json.dumps(seen))
print("secret in span attribute :", any(SECRET in (a or "") for a in span_args))
asyncio.run(main())
Suggested fix: do what AuthenticatedFunctionTool does and add the auth params to a copy of args, in both RestApiTool.call() and IntegrationConnectorTool.run_async(), and log the connector's args before the token is added. I have a PR ready with regression tests.
How often has this issue occurred?:
- Always (100%): any
RestApiTool-based tool call that has an auth credential configured.
馃敶 Required Information
Describe the Bug:
RestApiTool.call()adds the tool's credential to theargsdict it was given, in place:That dict is not private to the tool. The tool pipeline in
flows/llm_flows/tools/_caller.pyhands the samefunction_argsobject to:after_tool_callback(astool_args/args), andtrace_tool_call(), which runs after the tool returns and writes it to theexecute_toolspan asgcp.vertex.agent.tool_call_args. Content capture is on by default (ADK_CAPTURE_MESSAGE_CONTENT_IN_SPANS).So the API key, or the end user's OAuth bearer token, ends up in trace backends (Cloud Trace or any OTel exporter) and in whatever after-tool plugins record, for example analytics or logging plugins. That covers every toolset built on
RestApiTool:OpenAPIToolset,APIHubToolset,GoogleApiToolset, andApplicationIntegrationToolset.IntegrationConnectorTool.run_async()does the same thing one level up. It writesdynamic_auth_config = {"oauth2_auth_code_flow.access_token": <end-user token>}into the caller'sargs, then logs the whole dict at INFO:AuthenticatedFunctionToolalready avoids this (args_to_call = args.copy()); these two tools don't.The session is not affected: the persisted
FunctionCallargs are deep-copied before the tool runs.Steps to Reproduce:
pip install google-adk(reproduced onmain@ f33d492 / 2.9.0).RunnerplusLlmAgentplusOpenAPIToolsetwith API-key auth. The model is mocked and the HTTP call is stubbed, so no API key or network is needed.Expected Behavior:
The credential is sent in the HTTP request only. The
after_tool_callbackand theexecute_toolspan see the model's arguments:{"order_id": "A-42"}.Observed Behavior:
Environment Details:
main@ f33d492)Model Information:
馃煛 Optional Information
Minimal Reproduction Code:
Suggested fix: do what
AuthenticatedFunctionTooldoes and add the auth params to a copy ofargs, in bothRestApiTool.call()andIntegrationConnectorTool.run_async(), and log the connector's args before the token is added. I have a PR ready with regression tests.How often has this issue occurred?:
RestApiTool-based tool call that has an auth credential configured.