Skip to content

Commit e95b1ad

Browse files
authored
fix: gate workflow tool node confirmation resume on the caller principal (3/5)
_ToolNode._apply_confirmation_resume (ce132b9) stored ctx.resume_inputs on ctx.tool_confirmation with no principal check, and apply_confirmation_gate is a no-op for tools whose check_require_confirmation is False yet read tool_confirmation themselves, ExecuteBashTool included. With strict mode on, an unauthenticated caller could resume a paused tool node with confirmed=True and the tool ran. The answer is now rewritten to confirmed=False before it is stored, so every reader sees the refusal.
1 parent 51775f4 commit e95b1ad

1 file changed

Lines changed: 13 additions & 3 deletions

File tree

‎src/google/adk/workflow/_tool_node.py‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from pydantic import Field
2727
from typing_extensions import override
2828

29+
from ..agents._caller_principal import caller_may_confirm
2930
from ..agents.context import Context
3031
from ..auth.auth_tool import AuthConfig
3132
from ..events.event import Event
@@ -258,11 +259,20 @@ def _apply_confirmation_resume(self, *, ctx: Context) -> None:
258259
"""Stores the user's answer to this node's confirmation request.
259260
260261
The answer is set on `ctx.tool_confirmation`, so the confirmation gate and
261-
the tool see it the same way they do inside an agent.
262+
the tool see it the same way they do inside an agent. An answer from a
263+
caller the serving layer could not authenticate is stored as a rejection,
264+
the same decision the agent pipeline makes for a confirmation carried in a
265+
user event; it is applied here, before the answer is stored, because tools
266+
such as `ExecuteBashTool` read `tool_confirmation` themselves rather than
267+
through `apply_confirmation_gate`.
262268
"""
263269
response = ctx.resume_inputs.get(self._confirmation_interrupt_id(ctx))
264-
if response is not None:
265-
ctx.tool_confirmation = _parse_tool_confirmation(response)
270+
if response is None:
271+
return
272+
confirmation = _parse_tool_confirmation(response)
273+
if not caller_may_confirm(ctx.get_invocation_context().caller_principal):
274+
confirmation = confirmation.model_copy(update={'confirmed': False})
275+
ctx.tool_confirmation = confirmation
266276

267277
def _take_requested_confirmation(
268278
self, *, ctx: Context, args: dict[str, Any]

0 commit comments

Comments
 (0)