|
16 | 16 |
|
17 | 17 | from __future__ import annotations |
18 | 18 |
|
| 19 | +import logging |
19 | 20 | from typing import Optional |
20 | 21 |
|
21 | 22 | from pydantic import BaseModel |
22 | 23 | from pydantic import ConfigDict |
23 | 24 |
|
| 25 | +from ..features import FeatureName |
| 26 | +from ..features import is_feature_enabled |
| 27 | + |
| 28 | +logger = logging.getLogger("google_adk." + __name__) |
| 29 | + |
24 | 30 |
|
25 | 31 | class CallerPrincipal(BaseModel): |
26 | 32 | """Who sent this invocation, and whether a serving layer vouched for them. |
@@ -56,3 +62,65 @@ class CallerPrincipal(BaseModel): |
56 | 62 | Informational only. Trust decisions must key off ``authenticated``, never |
57 | 63 | off the source, so a transport can not be used as a proxy for identity. |
58 | 64 | """ |
| 65 | + |
| 66 | + |
| 67 | +def caller_may_confirm(principal: Optional[CallerPrincipal]) -> bool: |
| 68 | + """Decides whether a tool confirmation from this caller may be honored. |
| 69 | +
|
| 70 | + Every consumer of a human-in-the-loop answer asks this one question, so the |
| 71 | + agent pipeline and the workflow tool node refuse the same callers. Three |
| 72 | + states, and only the middle one is a refusal: |
| 73 | +
|
| 74 | + - No principal: nothing vouched for the caller because nothing had to. The |
| 75 | + invocation was started in process, so the caller is the operator. |
| 76 | + - Principal present, not authenticated: a serving layer handled this request |
| 77 | + and could not say who sent it, so the approval is not known to be the |
| 78 | + operator's. |
| 79 | + - Principal present and authenticated: the serving layer verified the caller. |
| 80 | +
|
| 81 | + The question asked here is deliberately about authentication and not about |
| 82 | + transport. A transport is a proxy for identity, and a proxy for identity |
| 83 | + fails in both directions: it refuses authenticated peers that happen to |
| 84 | + arrive over the wire, and it admits anyone who reaches an ungated path. |
| 85 | +
|
| 86 | + Strict mode is the STRICT_CALLER_PRINCIPAL feature, off by default for now |
| 87 | + so that upgrading cannot break a working deployment that runs its A2A server |
| 88 | + without an authenticator; those get a warning instead. The intent is to flip |
| 89 | + the registry default at the next major version. |
| 90 | +
|
| 91 | + Callers that get ``False`` should store the answer as ``confirmed=False`` |
| 92 | + rather than drop it. Dropping it leaves the confirmation request pending with |
| 93 | + nothing left to resolve it, which is what made an earlier attempt at this |
| 94 | + guard stall every human-in-the-loop tool. ``confirmed=False`` is a state the |
| 95 | + framework already has a contract for -- it is what a human decline produces |
| 96 | + -- so the tool returns its rejection response and the turn ends with a |
| 97 | + reason the caller can see. |
| 98 | +
|
| 99 | + Args: |
| 100 | + principal: The ``caller_principal`` of the current invocation context. |
| 101 | +
|
| 102 | + Returns: |
| 103 | + True if the answer may be honored as given, False if it must be treated as |
| 104 | + a rejection. |
| 105 | + """ |
| 106 | + if principal is None or principal.authenticated: |
| 107 | + return True |
| 108 | + |
| 109 | + if not is_feature_enabled(FeatureName.STRICT_CALLER_PRINCIPAL): |
| 110 | + logger.warning( |
| 111 | + "Honoring a tool confirmation from an unauthenticated caller" |
| 112 | + " (principal source %r). The serving layer could not say who sent this" |
| 113 | + " approval, so it is not known to be the operator's. Enable the" |
| 114 | + " STRICT_CALLER_PRINCIPAL feature to refuse these instead; that is" |
| 115 | + " intended to become the default in a future major version.", |
| 116 | + principal.source, |
| 117 | + ) |
| 118 | + return True |
| 119 | + |
| 120 | + logger.error( |
| 121 | + "Refusing a tool confirmation from an unauthenticated caller (principal" |
| 122 | + " source %r). Enable authentication on the serving layer, or disable the" |
| 123 | + " STRICT_CALLER_PRINCIPAL feature to downgrade this to a warning.", |
| 124 | + principal.source, |
| 125 | + ) |
| 126 | + return False |
0 commit comments