RestApiTool writes the tool's API key / OAuth token into the caller's args, so it lands in trace spans and after-tool callbacks

Abierto
#7,162 2 comentarios 0 reacciones 1 asignado Ver en GitHub

@llalitkumarrr ya está trabajando en esto.

Desde el 18/9/2026.

Evaluación

Este issue todavía no se ha evaluado.

Descripción

tools

🔴 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:

  1. pip install google-adk (reproduced on main @ f33d4923 / 2.9.0).
  2. 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 @ f33d4923)
  • 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.
Lenguaje dominante
Python
Estrellas
21.6k
Forks
4k
Merge medio
13 h 49 min
PR fusionados (30 d)
10

Guía de contribución

Abrir la guía de contribución

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de google/adk-python

Todos los issues de google/adk-python

Issues similares

Más issues de Python

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.