2020from google .adk .tools .base_tool import BaseTool
2121from google .adk .agents .readonly_context import ReadonlyContext
2222
23+ from google_auth_oauthlib .flow import InstalledAppFlow
24+ from google .auth .transport .requests import Request
25+ from .credentials import CredentialConfig , CredentialType
26+ from .client import USER_TOKEN_CONTEXT_VAR
27+
2328
2429class ToolboxContext :
2530 """Context object passed to pre/post hooks."""
@@ -40,35 +45,21 @@ def __init__(
4045 core_tool : CoreToolboxTool ,
4146 pre_hook : Optional [Callable [[ToolboxContext ], Awaitable [None ]]] = None ,
4247 post_hook : Optional [Callable [[ToolboxContext ], Awaitable [None ]]] = None ,
48+ auth_config : Optional [CredentialConfig ] = None ,
4349 ):
4450 """
4551 Args:
4652 core_tool: The underlying toolbox_core.py tool instance.
4753 pre_hook: Async function called before execution. Can modify ctx.arguments.
4854 post_hook: Async function called after execution (finally block). Can inspect ctx.result/error.
55+ auth_config: Credential configuration to handle interactive flows.
4956 """
5057 # We act as a proxy.
5158 # We need to extract metadata from the core tool to satisfy BaseTool's contract.
52- # core_tool name, description etc. are available.
53-
54- # Note: BaseTool in adk-python typically requires tool_name, tool_description etc.
55- # We map them from the core tool.
56- # core_tool.tool_def is usually available or we use the wrapper's attributes if exposed.
57- # In toolbox-core 0.1.0+, tool instance is callable but metadata is also stored.
58- # Assuming core_tool has typical attributes or we introspect.
59- # For now, we rely on core_tool's string representation or assume it has name/doc.
6059
6160 name = getattr (core_tool , "__name__" , "unknown_tool" )
6261 description = getattr (core_tool , "__doc__" , "No description provided." ) or "No description provided."
6362
64- # toolbox_core tools might not expose explicit args schema in a way BaseTool expects
65- # (dict of name -> type). ADK tools usually define `tool_args` as a dict.
66- # We might need to inspect the signature if not explicitly available.
67- # For this implementation, we allow dynamic arguments similar to FunctionTool.
68- # If BaseTool enforces providing `tool_args`, we might need to introspect.
69- # However, BaseTool logic in adk-python often derives it.
70- # We'll call super with minimal known info or defaults.
71-
7263 super ().__init__ (
7364 name = name ,
7465 description = description ,
@@ -78,6 +69,8 @@ def __init__(
7869 self ._core_tool = core_tool
7970 self ._pre_hook = pre_hook
8071 self ._post_hook = post_hook
72+ self ._auth_config = auth_config
73+ self ._user_creds : Optional [Any ] = None
8174
8275 @override
8376 async def run_async (
@@ -93,75 +86,52 @@ async def run_async(
9386 await self ._pre_hook (ctx )
9487
9588 # 2. ADK Auth Integration (3LO)
96- # We check if we need to inject a user token managed by ADK.
97- # This typically happens if the toolset was configured with USER_IDENTITY.
98- # We don't know the configuration here explicitly unless passed,
99- # BUT the convention is: if we can get a token from tool_context, use it.
100- # Or better: The client (ToolboxClient) didn't set auth headers for USER_IDENTITY,
101- # so we must do it here.
102-
103- # How do we know if we logic requires it?
104- # We can try to fetch; if it exists, we assume we should use it?
105- # Or we rely on the fact that `core_tool` instance itself might have bound auth getters?
106- # Actually, toolbox-core tools support `add_auth_token_getters`.
107- # We need to bridge ADK's `get_auth_response` to toolbox-core's getter.
108-
109- # ADK 3LO flow:
110- # response = tool_context.get_auth_response()
111- # if not response:
112- # tool_context.request_credential(...)
113- # return "Please authenticate..."
114- # else:
115- # token = response.token
116-
117- # If we have a way to detect 3LO requirement, we trigger it.
118- # For now, we'll try to retrieve an auth response if one is potentially available.
119- # But `toolbox-core` tool will fail if it needs auth and doesn't get it.
120- # The proper pattern is: wrap the core tool with a dynamically injected getter
121- # that calls into ADK logic.
122-
123- # However, `toolbox-core`'s `auth_token_getters` expects a callable.
124- # Be careful: `tool_context` is available here in `run_async`, but `auth_token_getters`
125- # are usually bound locally.
126-
127- # We'll define a token getter that closes over `tool_context`.
128- # We'll define a token getter that closes over `tool_context` if needed in future.
129- # For now, unused.
89+ # Check if USER_IDENTITY is configured
90+ reset_token = None
91+ if self ._auth_config and self ._auth_config .type == CredentialType .USER_IDENTITY :
92+ # Handle interactive flow if credentials are missing or expired
93+ if not self ._user_creds or not self ._user_creds .valid :
94+ if self ._user_creds and self ._user_creds .expired and self ._user_creds .refresh_token :
95+ try :
96+ self ._user_creds .refresh (Request ())
97+ except Exception :
98+ self ._user_creds = None
99+
100+ if not self ._user_creds :
101+ # Trigger flow
102+ if not (self ._auth_config .client_id and self ._auth_config .client_secret ):
103+ raise ValueError ("USER_IDENTITY requires client_id and client_secret" )
104+
105+ config = {
106+ "installed" : {
107+ "client_id" : self ._auth_config .client_id ,
108+ "client_secret" : self ._auth_config .client_secret ,
109+ "auth_uri" : "https://accounts.google.com/o/oauth2/auth" ,
110+ "token_uri" : "https://oauth2.googleapis.com/token" ,
111+ }
112+ }
113+ scopes = self ._auth_config .scopes or ["https://www.googleapis.com/auth/cloud-platform" ]
114+
115+ flow = InstalledAppFlow .from_client_config (config , scopes = scopes )
116+ self ._user_creds = flow .run_local_server (port = 0 )
117+
118+ # Inject token into ContextVar
119+ if self ._user_creds :
120+ reset_token = USER_TOKEN_CONTEXT_VAR .set (self ._user_creds .token )
130121
131- # Let's try to get a token if available, but fundamentally, ADK expects the tool to return
132- # a request for credentials if missing.
133- auth_response = tool_context .get_auth_response ()
134-
135- # NOTE: We can't easily tell if this specific tool *needs* user auth without metadata.
136- # Strategy: We assume that if the tool fails with an auth error, we might fallback?
137- # OR we rely on configuration.
138- # Ideally, `ToolboxToolset` would have configured this tool with a special "trigger" if needed.
139-
140- # For now, we will proceed to execute.
141- # If `USER_IDENTITY` was configured, we might have injected a special marker or getter.
142- # Revisit this logic if explicit "Force Auth" flag is needed on the tool.
143-
144122 try :
145123 # Execute the core tool
146- # mapping: toolbox-core expects (arg1=..., arg2=...)
147- # We unpack ctx.arguments
148-
149- # For 3LO support: We really need to know if we should be requesting credentials.
150- # For this MVP, we will rely on static auth or pre-existing headers.
151- # If we want to support 3LO properly, we need to handle the `tool_context.request_credential` flow.
152- # We'll check if `auth_token` argument is present or if we have a bound getter.
153-
154- # We execute:
155124 result = await self ._core_tool (** ctx .arguments )
156125
157126 ctx .result = result
158127 return result
159128
160129 except Exception as e :
161- # TODO: Inspect e for Auth errors to trigger 3LO if configured?
162130 ctx .error = e
163131 raise e
164132 finally :
133+ if reset_token :
134+ USER_TOKEN_CONTEXT_VAR .reset (reset_token )
165135 if self ._post_hook :
166136 await self ._post_hook (ctx )
167137
@@ -172,5 +142,6 @@ def bind_params(self, bounded_params: Dict[str, Any]) -> 'ToolboxTool':
172142 return ToolboxTool (
173143 core_tool = new_core_tool ,
174144 pre_hook = self ._pre_hook ,
175- post_hook = self ._post_hook
145+ post_hook = self ._post_hook ,
146+ auth_config = self ._auth_config
176147 )
0 commit comments