diff --git a/rogue/run_cli.py b/rogue/run_cli.py index 9ea760c1..f6b4fc38 100644 --- a/rogue/run_cli.py +++ b/rogue/run_cli.py @@ -410,7 +410,18 @@ async def get_a2a_agent_card( agent_url: str, headers: dict[str, str] | None = None, ) -> AgentCard: - if transport == Transport.HTTP: + """Fetch and validate an A2A agent's AgentCard from its well-known path. + + The A2A AgentCard is always served over HTTP at `/.well-known/agent.json` + regardless of the RPC transport used for agent calls, so HTTP, STREAMABLE_HTTP + and SSE all expose the card the same way. Accept those and only reject + transports that cannot serve an A2A card (e.g. CHAT_COMPLETIONS). + """ + if transport in ( + Transport.HTTP, + Transport.STREAMABLE_HTTP, + Transport.SSE, + ): try: response = requests.get( f"{agent_url}/.well-known/agent.json", diff --git a/rogue/tests/test_run_cli.py b/rogue/tests/test_run_cli.py index d8f23afa..cc813e0a 100644 --- a/rogue/tests/test_run_cli.py +++ b/rogue/tests/test_run_cli.py @@ -1,3 +1,4 @@ +import asyncio import json from argparse import Namespace from pathlib import Path @@ -7,7 +8,7 @@ from pytest_mock import MockerFixture from rogue.models.cli_input import CLIInput -from rogue.run_cli import get_cli_input +from rogue.run_cli import get_a2a_agent_card, get_cli_input from rogue_sdk.types import AuthType, Protocol, Transport @@ -101,3 +102,58 @@ def test_get_cli_input( cli_args.config_file = config_file assert get_cli_input(cli_args) == expected + + +_AGENT_CARD_JSON = { + "name": "test-agent", + "description": "A test agent", + "url": "https://localhost:10001", + "version": "0.2.0", + "capabilities": {"streaming": False, "pushNotifications": False}, + "defaultInputModes": ["text/plain"], + "defaultOutputModes": ["text/plain"], + "skills": [], +} + + +@pytest.mark.parametrize( + "transport", + [ + Transport.HTTP, + Transport.STREAMABLE_HTTP, + Transport.SSE, + ], +) +async def test_get_a2a_agent_card_accepts_http_transports( + transport: Transport, + mocker: MockerFixture, +) -> None: + """Regression test for issue #150. + + The A2A agent card is served over HTTP at the well-known path regardless of + the RPC transport, so STREAMABLE_HTTP/SSE must not raise + 'Unsupported transport'. Before the fix only Transport.HTTP was accepted. + """ + mock_get = mocker.patch("rogue.run_cli.requests.get") + mock_get.return_value.json.return_value = _AGENT_CARD_JSON + mock_get.return_value.status_code = 200 + + card = asyncio.run( + get_a2a_agent_card(agent_url="https://localhost:10001", transport=transport) + ) + assert card is not None + assert card.name == "test-agent" + mock_get.assert_called_once() + + +def test_get_a2a_agent_card_rejects_unsupported_transport(mocker: MockerFixture) -> None: + """Non-HTTP transports for A2A (e.g. CHAT_COMPLETIONS) must still raise.""" + mock_get = mocker.patch("rogue.run_cli.requests.get") + with pytest.raises(ValueError): + asyncio.run( + get_a2a_agent_card( + agent_url="https://localhost:10001", + transport=Transport.CHAT_COMPLETIONS, + ) + ) + mock_get.assert_not_called()