Skip to content

Commit 9631358

Browse files
authored
feat(api_server): record a CallerPrincipal on /run and /run_sse
The API server does not authenticate callers. A deployment that does can set request.state.caller_principal from middleware; without one, /run and /run_sse now run the invocation as CallerPrincipal(authenticated=False, source="api_server"), so a tool confirmation carried by the request is warned about (default) or refused (ADK_ENABLE_STRICT_CALLER_PRINCIPAL=1), the same decision made for an unauthenticated A2A caller. The body user_id is a claim, not a verified identity, and is not consulted. The kwarg is only passed when runner.run_async accepts it. Stacked on #7134 (caller-principal-gate). Relates to #7569.
1 parent 6ec3fb8 commit 9631358

1 file changed

Lines changed: 65 additions & 8 deletions

File tree

‎src/google/adk/cli/api_server.py‎

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
from typing_extensions import override
7171
import yaml
7272

73+
from ..agents._caller_principal import CallerPrincipal
7374
from ..agents._streaming_mode import StreamingMode
7475
from ..agents.base_agent import BaseAgent
7576
from ..agents.llm_agent import LlmAgent
@@ -447,23 +448,68 @@ def _accepts_kwargs(func: Callable[..., Any], kwargs: dict[str, Any]) -> bool:
447448
return True
448449

449450

451+
def _run_async_accepts(runner: Runner, name: str) -> bool:
452+
"""Whether runner.run_async takes the keyword ``name`` (or ``**kwargs``)."""
453+
try:
454+
params = inspect.signature(runner.run_async).parameters
455+
except (ValueError, TypeError):
456+
return True
457+
return name in params or any(
458+
p.kind == inspect.Parameter.VAR_KEYWORD for p in params.values()
459+
)
460+
461+
450462
def _with_abort_signal_kwarg(
451463
runner: Runner,
452464
kwargs: dict[str, Any],
453465
abort_signal: asyncio.Event,
454466
) -> dict[str, Any]:
455467
"""Adds abort_signal to kwargs when runner.run_async accepts it."""
456-
try:
457-
params = inspect.signature(runner.run_async).parameters
458-
if "abort_signal" in params or any(
459-
p.kind == inspect.Parameter.VAR_KEYWORD for p in params.values()
460-
):
461-
kwargs["abort_signal"] = abort_signal
462-
except (ValueError, TypeError):
468+
if _run_async_accepts(runner, "abort_signal"):
463469
kwargs["abort_signal"] = abort_signal
464470
return kwargs
465471

466472

473+
def _with_caller_principal_kwarg(
474+
runner: Runner,
475+
kwargs: dict[str, Any],
476+
caller_principal: CallerPrincipal,
477+
) -> dict[str, Any]:
478+
"""Adds caller_principal to kwargs when runner.run_async accepts it."""
479+
if _run_async_accepts(runner, "caller_principal"):
480+
kwargs["caller_principal"] = caller_principal
481+
return kwargs
482+
483+
484+
_API_SERVER_PRINCIPAL_SOURCE = "api_server"
485+
486+
487+
def _caller_principal_for_request(
488+
request: Optional[Request],
489+
) -> CallerPrincipal:
490+
"""Returns the caller identity to run a /run or /run_sse request as.
491+
492+
The API server does not authenticate callers itself. A deployment that does,
493+
in middleware or a dependency in front of these routes, states the result on
494+
``request.state.caller_principal`` and that value is used as is. Without
495+
one, the request is recorded as unauthenticated from the ``api_server``
496+
edge, so a tool confirmation it carries is warned about (default) or refused
497+
(strict mode), the same decision made for an unauthenticated A2A caller.
498+
The ``user_id`` in the request body is a claim the caller made rather than
499+
one the server checked, so it is not consulted here.
500+
"""
501+
principal = (
502+
getattr(request.state, "caller_principal", None)
503+
if request is not None
504+
else None
505+
)
506+
if isinstance(principal, CallerPrincipal):
507+
return principal
508+
return CallerPrincipal(
509+
authenticated=False, source=_API_SERVER_PRINCIPAL_SOURCE
510+
)
511+
512+
467513
_current_session_options: contextvars.ContextVar[Optional[dict[str, Any]]] = (
468514
contextvars.ContextVar("current_session_options", default=None)
469515
)
@@ -2123,6 +2169,7 @@ async def run_agent(req: RunAgentRequest, request: Request) -> Response:
21232169
)
21242170

21252171
abort_signal = asyncio.Event()
2172+
caller_principal = _caller_principal_for_request(request)
21262173

21272174
async def worker() -> list[Event]:
21282175
run_async_kwargs = _with_abort_signal_kwarg(
@@ -2137,6 +2184,9 @@ async def worker() -> list[Event]:
21372184
},
21382185
abort_signal,
21392186
)
2187+
run_async_kwargs = _with_caller_principal_kwarg(
2188+
runner, run_async_kwargs, caller_principal
2189+
)
21402190
try:
21412191
async with Aclosing(runner.run_async(**run_async_kwargs)) as agen:
21422192
return [public_event(event) async for event in agen]
@@ -2186,7 +2236,9 @@ async def monitor() -> None:
21862236
monitor_task.cancel()
21872237

21882238
@app.post("/run_sse")
2189-
async def run_agent_sse(req: RunAgentRequest) -> StreamingResponse:
2239+
async def run_agent_sse(
2240+
req: RunAgentRequest, request: Request = None
2241+
) -> StreamingResponse:
21902242
app_name = req.app_name or self.default_app_name
21912243
if not app_name:
21922244
raise HTTPException(
@@ -2235,6 +2287,8 @@ async def run_agent_sse(req: RunAgentRequest) -> StreamingResponse:
22352287
detail=f"Session not found: {req.session_id}",
22362288
)
22372289

2290+
caller_principal = _caller_principal_for_request(request)
2291+
22382292
# Convert the events to properly formatted SSE
22392293
async def event_generator() -> AsyncIterator[str]:
22402294
is_closing = False
@@ -2257,6 +2311,9 @@ async def _produce_events() -> None:
22572311
},
22582312
abort_signal,
22592313
)
2314+
run_async_kwargs = _with_caller_principal_kwarg(
2315+
runner, run_async_kwargs, caller_principal
2316+
)
22602317
try:
22612318
async with Aclosing(runner.run_async(**run_async_kwargs)) as agen:
22622319
try:

0 commit comments

Comments
 (0)