Repository navigation
HITL tool confirmation forgery (#6461) is still present on main and in v2.12.0: fix 9e9eaa6 was reverted in 9a32eba #7569
Description
🔴 Required Information
Describe the Bug:
#6461 was closed on Oct 14 as resolved by #6462 (commit 9e9eaa6). That commit was reverted three days after it landed, in 9a32eba ("fix: revert the A2A guard that broke every HITL tool confirmation", Aug 20), and nothing has replaced it since. src/google/adk/flows/llm_flows/tools/_confirmation.py on main and in the v2.12.0 release (Oct 13) accepts a tool confirmation from whoever sends the function response, with no check of who the caller is. A /run caller or an A2A peer can therefore still answer the confirmation for a pending confirmation-gated tool call it caused, including ExecuteBashTool. I cannot reopen #6461 (closed by a maintainer), so this issue carries the current state; please close whichever one you prefer to keep.
Steps to Reproduce:
As in #6461, against google-adk 2.12.0:
- Serve an
LlmAgentwith a tool that hasrequire_confirmation=True(orExecuteBashTool) over A2A, or expose/run. - From the remote side, trigger a call to the tool; the agent pauses with an
adk_request_confirmationcall. - From the same remote side, send the function response
{"confirmed": true}for that call id. - The tool runs.
Expected Behavior:
A confirmation answered by a caller the serving layer did not authenticate as the operator is not acted on; the pending call resolves as rejected rather than running.
Observed Behavior:
The tool runs. The same steps ran the tool in 2.5.0 (original report), and the code path is unchanged on main at 377306e and in v2.12.0 apart from the file move in 092a61d.
Environment Details:
- ADK Library Version: 2.12.0 (and main at 377306e); originally confirmed on 2.5.0 in Human-in-the-loop tool confirmation is forgeable by an A2A peer (self-approves dangerous tools) #6461
- Desktop OS: N/A, the path is OS-independent
- Python Version: N/A
Model Information:
- Are you using LiteLLM: No
- Which model is being used: any; the confirmation path is model-independent
🟡 Optional Information
Regression:
Fixed between 9e9eaa6 (Aug 17) and 9a32eba (Aug 20); unfixed before and since.
Additional Context:
The replacement fix is #7134, open and rebased on current main: a CallerPrincipal on the invocation context, set by the A2A executor from the server's call context, checked in the confirmation processor and in the workflow tool and function nodes; a refused approval is delivered as confirmed=False rather than dropped, which is what broke in 9e9eaa6. Strict refusal is behind ADK_ENABLE_STRICT_CALLER_PRINCIPAL; the default warns. The Google VRP case for this report also shows "Fixed" on the same basis, so a correct status here helps there too.
How often has this issue occurred?:
- Always (100%)
Activity
surajksharma07 commentedon Oct 16, 2026
You’re right on this one @cybrdude. Checked and 9a32eba fully reverts 9e9eaa6 so main and 2.12.0 still accept a forged {"confirmed": true}. Reproduced it with a plain require_confirmation tool so keeping #7569 as the tracker and pointing #6461 here.
Until #7134 lands a workaround that held up: a small plugin where in on_user_message_callback rewrite adk_request_confirmation answers to confirmed=False unless the user_id is an allowlisted operator (over A2A on main that’s only set from an authenticated call_context). Could you try it on your setup and let us know?
It doesn’t cover /run or /run_sse where user_id comes from the request body so those still need API-server auth as you noted.
Once you’ve confirmed it works with a legitimate operator approval over A2A and that the workflow ToolNode/FunctionNode paths in #7134 still pass after the latest main merge we’ll push for the second review.
contrib: operator-only confirmation plugin and end-to-end validation …
cybrdude commentedon Oct 16, 2026
@surajksharma07 tried it. The plugin works, with two changes to the sketch, and I have it running end to end against main f78149a through the real A2aAgentExecutor and through the workflow nodes.
Files and tests: https://github.com/cybrdude/adk-python/tree/hitl-7569-workaround/contrib/hitl_7569 (commit f07758b). From a checkout of that branch:
19 passed, also from a fresh clone. Results:
The two changes:
adk_request_confirmationanswers leaves the workflow nodes open. ToolNode and FunctionNode confirmations resume throughadk_request_input, andRunner._extract_resume_inputskeys resume inputs byfunction_response.idalone, so the plugin matches those by the interrupt-id prefixwf_tool_confirmation:rather than by name. A name check is bypassable there: without the plugin, an answer with the right id and an arbitrary name ran the tool (last test in the file).PluginManager._run_callbacksreturns the first non-None result, so the plugin has to be registered before any other plugin that rewrites user messages.The plugin itself (the file in the branch has the docstring and imports):
As you said, it does nothing for /run and /run_sse, where user_id comes from the request body.
#7134 after the merge with f78149a: 156f007 folded the workflow resume into
apply_confirmation_resume, so the stoppage now lives in that helper and the PR no longer touches_tool_node.pyor_function_node.py. At 6ec3fb8 the tool node, workflow node, function node, hitl utils, confirmation processor and request converter suites pass (273 tests), including the strict-mode cases for both node kinds. Ready for the second review from my side.surajksharma07 commentedon Oct 16, 2026
Both catches are right @cybrdude and checked them on f78149a. _extract_resume_inputs keys only by function_response.id (runners.py:637) so matching by name alone left ToolNode/FunctionNode open. _run_callbacks also stops at the first non-None result so registering the plugin first does matter.
Also checked that the node runner re-extracts resume_inputs after the plugin rewrites the message (_node_runner_utils.py:170) so confirmed=False really does reach both nodes. The name check is enough on the LlmAgent side because _confirmation.py:289 ignores any other name.
One caveat for anyone copying it: the wf_tool_confirmation: prefix is a private interrupt-id format so treat the plugin as a stopgap until #7134 lands. Having the workflow side gated in the single apply_confirmation_resume helper after 156f007 is a much cleaner place for it.
Thanks for running the 273 tests at 6ec3fb8. @xuanyang15 could you take the second review on #7134 and approve the pending workflows? /run and /run_sse stay out of scope as agreed.