Hacktoberfest 2026: những issue maintainer đã đánh dấu cho tháng Mười, đang mở và phù hợp người mới. Xem issue Hacktoberfest

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

Đang mở
#7,162 2 bình luận 0 reaction 1 người được giao Xem trên GitHub

@llalitkumarrr đang làm issue này rồi.

Từ ngày 18/9/2026.

Đánh giá

Issue này chưa được đánh giá.

Mô tả

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.
Ngôn ngữ chính
Python
Star
21.6k
Fork
4k
Merge trung bình
13 giờ 49 phút
Pull request đã merge (30 ngày)
10

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Issue khác của google/adk-python

Tất cả issue của google/adk-python

Issue tương tự

Thêm issue về Python

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.