Hacktoberfest 2026:维护者为十月标记出来的 issue,仍然开放、适合新手。 浏览 Hacktoberfest issue

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

未关闭
#7,162 2 条评论 0 个 reaction 已指派 1 人 在 GitHub 查看

@llalitkumarrr 已经在做这个了。

开始于 2026年9月18日。

评估

这个 Issue 还没有评估数据。

描述

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.
主要语言
Python
星标
21.6k
派生
4k
平均合并
13 小时 49 分钟
30 天内合并 PR
10

贡献指南

打开贡献指南

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

google/adk-python 的其他 Issue

查看 google/adk-python 的全部 Issue

相似的 Issue

更多 Python Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。