Skip to content

Commit 05982b8

Browse files
committed
Add VeADK A2A auth switch and utility enhancements
Introduces a new `to_a2a` utility in `veadk.a2a.utils.agent_to_a2a` to wrap Google ADK's A2A conversion with optional VeADK authentication and credential service integration. Adds comprehensive tests for the auth switch, refactors `RemoteVeAgent` to ensure pre-run initialization/auth logic always executes, and renames `credential_service.py` to `ve_credential_service.py` for clarity.
1 parent d4b5bad commit 05982b8

File tree

8 files changed

+352
-30
lines changed

8 files changed

+352
-30
lines changed

test_to_a2a_auth_switch.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
"""Test to verify the enable_veadk_auth switch in to_a2a function."""
2+
3+
import pytest
4+
from veadk import Agent, Runner
5+
from veadk.a2a.utils.agent_to_a2a import to_a2a
6+
from veadk.auth.ve_credential_service import VeCredentialService
7+
from google.adk.auth.credential_service.base_credential_service import BaseCredentialService
8+
9+
10+
class MockCredentialService(BaseCredentialService):
11+
"""Mock credential service for testing."""
12+
pass
13+
14+
15+
def test_to_a2a_without_veadk_auth():
16+
"""Test to_a2a with enable_veadk_auth=False (default)."""
17+
agent = Agent(name="test_agent")
18+
app = to_a2a(agent, enable_veadk_auth=False)
19+
20+
# App should be created successfully
21+
assert app is not None
22+
print("✅ Test 1 passed: to_a2a works without VeADK auth")
23+
24+
25+
def test_to_a2a_with_veadk_auth_no_runner():
26+
"""Test to_a2a with enable_veadk_auth=True and no runner provided."""
27+
agent = Agent(name="test_agent")
28+
app = to_a2a(agent, enable_veadk_auth=True)
29+
30+
# App should be created successfully with VeCredentialService
31+
assert app is not None
32+
print("✅ Test 2 passed: to_a2a creates runner with VeCredentialService")
33+
34+
35+
def test_to_a2a_with_veadk_auth_runner_with_ve_credential_service():
36+
"""Test to_a2a with enable_veadk_auth=True and runner with VeCredentialService."""
37+
agent = Agent(name="test_agent")
38+
credential_service = VeCredentialService()
39+
runner = Runner(agent=agent, credential_service=credential_service)
40+
41+
app = to_a2a(agent, runner=runner, enable_veadk_auth=True)
42+
43+
# App should be created successfully
44+
assert app is not None
45+
# Runner should still have the same credential service
46+
assert runner.credential_service is credential_service
47+
print("✅ Test 3 passed: to_a2a accepts runner with VeCredentialService")
48+
49+
50+
def test_to_a2a_with_veadk_auth_runner_without_credential_service():
51+
"""Test to_a2a with enable_veadk_auth=True and runner without credential_service."""
52+
agent = Agent(name="test_agent")
53+
runner = Runner(agent=agent)
54+
55+
# Runner initially has no credential_service (or None)
56+
initial_credential_service = getattr(runner, 'credential_service', None)
57+
58+
app = to_a2a(agent, runner=runner, enable_veadk_auth=True)
59+
60+
# App should be created successfully
61+
assert app is not None
62+
# Runner should now have a VeCredentialService
63+
assert hasattr(runner, 'credential_service')
64+
assert isinstance(runner.credential_service, VeCredentialService)
65+
print("✅ Test 4 passed: to_a2a adds VeCredentialService to runner")
66+
67+
68+
def test_to_a2a_with_veadk_auth_runner_with_wrong_credential_service():
69+
"""Test to_a2a with enable_veadk_auth=True and runner with non-VeCredentialService."""
70+
agent = Agent(name="test_agent")
71+
mock_credential_service = MockCredentialService()
72+
runner = Runner(agent=agent, credential_service=mock_credential_service)
73+
74+
# Should raise TypeError
75+
with pytest.raises(TypeError) as exc_info:
76+
to_a2a(agent, runner=runner, enable_veadk_auth=True)
77+
78+
assert "must be a VeCredentialService instance" in str(exc_info.value)
79+
assert "MockCredentialService" in str(exc_info.value)
80+
print("✅ Test 5 passed: to_a2a raises TypeError for wrong credential service type")
81+
82+
83+
def test_to_a2a_without_veadk_auth_accepts_any_credential_service():
84+
"""Test to_a2a with enable_veadk_auth=False accepts any credential service."""
85+
agent = Agent(name="test_agent")
86+
mock_credential_service = MockCredentialService()
87+
runner = Runner(agent=agent, credential_service=mock_credential_service)
88+
89+
# Should work fine when VeADK auth is disabled
90+
app = to_a2a(agent, runner=runner, enable_veadk_auth=False)
91+
92+
assert app is not None
93+
# Runner should still have the mock credential service
94+
assert runner.credential_service is mock_credential_service
95+
print("✅ Test 6 passed: to_a2a accepts any credential service when auth disabled")
96+
97+
98+
if __name__ == "__main__":
99+
print("Running to_a2a auth switch tests...\n")
100+
101+
test_to_a2a_without_veadk_auth()
102+
test_to_a2a_with_veadk_auth_no_runner()
103+
test_to_a2a_with_veadk_auth_runner_with_ve_credential_service()
104+
test_to_a2a_with_veadk_auth_runner_without_credential_service()
105+
test_to_a2a_with_veadk_auth_runner_with_wrong_credential_service()
106+
test_to_a2a_without_veadk_auth_accepts_any_credential_service()
107+
108+
print("\n🎉 All tests passed!")
109+

tests/auth/test_credential_service.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from google.adk.auth.auth_tool import AuthConfig
2727
from google.adk.agents.callback_context import CallbackContext
2828

29-
from veadk.auth.credential_service import VeCredentialService
29+
from veadk.auth.ve_credential_service import VeCredentialService
3030

3131

3232
@pytest.fixture

tests/test_ve_a2a_middlewares.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from starlette.responses import Response
2222

2323
from veadk.a2a.ve_middlewares import A2AAuthMiddleware, build_a2a_auth_middleware
24-
from veadk.auth.credential_service import VeCredentialService
24+
from veadk.auth.ve_credential_service import VeCredentialService
2525
from veadk.utils.auth import VE_TIP_TOKEN_HEADER
2626

2727

veadk/a2a/remote_ve_agent.py

Lines changed: 55 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515
import json
16+
import functools
1617
from typing import AsyncGenerator, Literal, Optional
1718

1819
from a2a.client.base_client import BaseClient
@@ -221,41 +222,70 @@ def __init__(
221222
if auth_method:
222223
self.auth_method = auth_method
223224

224-
async def _run_async_impl(
225-
self, ctx: InvocationContext
226-
) -> AsyncGenerator[Event, None]:
227-
"""Run the remote agent with credential injection support.
225+
# Wrap _run_async_impl with pre-run hook to ensure initialization
226+
# and authentication logic always executes, even if users override _run_async_impl
227+
self._wrap_run_async_impl()
228228

229-
This method:
230-
1. Ensures the agent is resolved (agent card fetched, client initialized)
231-
2. Injects authentication token from credential service if available
232-
3. Delegates to parent class for actual execution
229+
def _wrap_run_async_impl(self) -> None:
230+
"""Wrap _run_async_impl with a decorator that ensures pre-run logic executes.
231+
232+
This method wraps the _run_async_impl method with a decorator that:
233+
1. Executes _pre_run before the actual implementation
234+
2. Handles errors from _pre_run and yields error events
235+
3. Ensures the wrapper works even if users override _run_async_impl
236+
237+
The wrapper is applied by replacing the bound method on the instance.
238+
"""
239+
# Store the original _run_async_impl method
240+
original_run_async_impl = self._run_async_impl
241+
242+
@functools.wraps(original_run_async_impl)
243+
async def wrapped_run_async_impl(
244+
ctx: InvocationContext,
245+
) -> AsyncGenerator[Event, None]:
246+
"""Wrapped version of _run_async_impl with pre-run hook."""
247+
# Execute pre-run initialization
248+
try:
249+
await self._pre_run(ctx)
250+
except Exception as e:
251+
yield Event(
252+
author=self.name,
253+
error_message=f"Failed to initialize remote A2A agent: {e}",
254+
invocation_id=ctx.invocation_id,
255+
branch=ctx.branch,
256+
)
257+
return
258+
259+
# Call the original (or overridden) _run_async_impl
260+
async with Aclosing(original_run_async_impl(ctx)) as agen:
261+
async for event in agen:
262+
yield event
263+
264+
# Replace the instance method with the wrapped version
265+
self._run_async_impl = wrapped_run_async_impl
266+
267+
async def _pre_run(self, ctx: InvocationContext) -> None:
268+
"""Pre-run initialization and authentication setup.
269+
270+
This method is called before the actual agent execution to:
271+
1. Ensure the agent is resolved (agent card fetched, client initialized)
272+
2. Inject authentication token from credential service if available
273+
274+
This method is separated from _run_async_impl to ensure these critical
275+
initialization steps are always executed, even if users override _run_async_impl.
233276
234277
Args:
235278
ctx: Invocation context containing session and user information
236279
237-
Yields:
238-
Events from the remote agent execution
280+
Raises:
281+
Exception: If agent initialization fails
239282
"""
240-
try:
241-
await self._ensure_resolved()
242-
except Exception as e:
243-
yield Event(
244-
author=self.name,
245-
error_message=f"Failed to initialize remote A2A agent: {e}",
246-
invocation_id=ctx.invocation_id,
247-
branch=ctx.branch,
248-
)
249-
return
283+
# Ensure agent is resolved
284+
await self._ensure_resolved()
250285

251286
# Inject auth token if credential service is available
252287
await self._inject_auth_token(ctx)
253288

254-
# Delegate to parent class for execution
255-
async with Aclosing(super()._run_async_impl(ctx)) as agen:
256-
async for event in agen:
257-
yield event
258-
259289
async def _inject_auth_token(self, ctx: InvocationContext) -> None:
260290
"""Inject authentication token from credential service into the HTTP client.
261291

veadk/a2a/utils/__init__.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.

0 commit comments

Comments
 (0)