fix(a2a): scrub upstream url from /public/agent_hub cards
Some checks are pending
Unit Tests: Proxy DB Operations / endpoints-and-responses (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / assert-shard-coverage (push) Waiting to run
Unit Tests: Proxy DB Operations / auth-checks (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / budgets (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / custom-logging (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / db-and-spend (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / guardrails-hooks (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / jwt-and-keys (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / key-generation (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / logging-misc (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-runtime (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-server-core (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / schema-migration (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-utils (push) Blocked by required conditions
Unit Tests: Security / security (push) Waiting to run

Public agent_hub returned agent_card_params verbatim, exposing the
retained upstream backend url to unauthenticated callers. Rewrite the
url to the proxy /a2a/{agent_id} entrypoint on response, matching the
behavior of the authenticated well-known agent-card endpoint, so the
backend cannot be reached outside LiteLLM's auth, budget, and logging
path.
This commit is contained in:
mateo-berri 2026-05-28 08:10:50 +00:00
parent 65484a831c
commit 70fe8e58de
2 changed files with 73 additions and 5 deletions

View file

@ -5,7 +5,7 @@ from importlib.resources import files
from typing import Any, Dict, List, Optional
import litellm
from fastapi import APIRouter, HTTPException
from fastapi import APIRouter, HTTPException, Request
from litellm._logging import verbose_logger
from litellm.litellm_core_utils.get_blog_posts import (
@ -211,7 +211,7 @@ async def public_model_hub():
tags=["[beta] Agents", "public"],
response_model=List[AgentCard],
)
async def get_agents():
async def get_agents(request: Request):
import litellm
from litellm.proxy.agent_endpoints.agent_registry import global_agent_registry
@ -219,12 +219,16 @@ async def get_agents():
if litellm.public_agent_groups is None:
return []
agent_card_list = [
agent.agent_card_params
proxy_base = str(request.base_url).rstrip("/")
return [
{
**(agent.agent_card_params or {}),
"url": f"{proxy_base}/a2a/{agent.agent_id}",
}
for agent in agents
if agent.agent_id in litellm.public_agent_groups
]
return agent_card_list
@router.get(

View file

@ -463,6 +463,70 @@ def test_public_model_hub_mixed_health_statuses():
app.dependency_overrides.clear()
# ---------------------------------------------------------------------------
# /public/agent_hub
# ---------------------------------------------------------------------------
def test_public_agent_hub_rewrites_upstream_url_to_proxy():
"""Public agent hub must not leak the upstream backend URL retained on the
stored card. The ``url`` field has to be overwritten with the proxy
``/a2a/{agent_id}`` entrypoint, matching the well-known card endpoint, so
an unauthenticated client cannot call the backend directly."""
from litellm.types.agents import AgentResponse
upstream_url = "https://upstream.internal.example.com/a2a"
agent = AgentResponse(
agent_id="agent-123",
agent_name="public-agent",
agent_card_params={"name": "public-agent", "url": upstream_url},
)
app = FastAPI()
app.include_router(router)
client = TestClient(app)
mock_registry = MagicMock()
mock_registry.get_public_agent_list.return_value = [agent]
with (
patch("litellm.public_agent_groups", ["agent-123"]),
patch(
"litellm.proxy.agent_endpoints.agent_registry.global_agent_registry",
mock_registry,
),
):
response = client.get("/public/agent_hub")
assert response.status_code == 200, response.text
payload = response.json()
assert len(payload) == 1
card = payload[0]
assert upstream_url not in card.get("url", "")
assert card["url"].endswith("/a2a/agent-123")
def test_public_agent_hub_returns_empty_when_no_public_groups():
app = FastAPI()
app.include_router(router)
client = TestClient(app)
mock_registry = MagicMock()
mock_registry.get_public_agent_list.return_value = []
with (
patch("litellm.public_agent_groups", None),
patch(
"litellm.proxy.agent_endpoints.agent_registry.global_agent_registry",
mock_registry,
),
):
response = client.get("/public/agent_hub")
assert response.status_code == 200
assert response.json() == []
# ---------------------------------------------------------------------------
# /public/endpoints
# ---------------------------------------------------------------------------