Skip to content

fix(samples): make the a2a_human_in_loop approval threshold config-derived, not caller-bypassable - #6613

Open
Kapustin2000 wants to merge 1 commit into
google:mainfrom
Kapustin2000:fix/a2a-human-in-loop-approval-guard
Open

fix(samples): make the a2a_human_in_loop approval threshold config-derived, not caller-bypassable#6613
Kapustin2000 wants to merge 1 commit into
google:mainfrom
Kapustin2000:fix/a2a-human-in-loop-approval-guard

Conversation

@Kapustin2000

Copy link
Copy Markdown

Problem:
contributing/samples/a2a/a2a_human_in_loop/ demonstrates human-in-the-loop approval for large reimbursements, but the "$100 auto-approve, otherwise ask a human" rule lives only in the agent's instruction text. Nothing in code stops a direct call to reimburse() — from a model that ignores its own instructions, or a caller crafting tool-call arguments — from reimbursing any amount with no approval at all. This is the same shape of bug as a real, previously-observed production incident where a caller-controlled value defeated an approval gate that existed only as a convention rather than as an enforced check.

Solution:
Adds a small, additive, config-derived guard, kept idiomatic to this sample and to the rest of this repo's HITL samples:

  • approval_config.py (new, one copy per agent directory, matching this sample's existing pattern of duplicating reimburse() itself): a pure module exposing get_approval_threshold_usd() (reads REIMBURSEMENT_APPROVAL_THRESHOLD_USD from the environment, default $100) and requires_manager_approval(amount). No tool-call argument can influence this value.
  • reimburse() in both agent.py and remote_a2a/human_in_loop/agent.py now takes a tool_context: ToolContext and uses ADK's existing request_confirmation() / tool_confirmation.confirmed mechanism (the same one already demonstrated in contributing/samples/hitl/tool_confirmation/) to park/refuse execution for amounts at or above the threshold, rather than relying solely on the prompt.

No sample behavior changes for amounts under the threshold. For amounts at/above it, a direct call to reimburse() now returns pending_confirmation/rejected instead of ok until confirmed — closing the gap the sample previously left open.

Testing Plan

Unit Tests:

  • I have added unit tests for my change (tests/unittests/test_a2a_human_in_loop_approval_guard.py, 13 cases).
  • All unit tests pass locally.
$ pytest tests/unittests/test_a2a_human_in_loop_approval_guard.py tests/unittests/test_samples.py -v
...
274 passed, 19 skipped, 3 xfailed, 1 xpassed, 202 warnings in 23.54s

I additionally verified the new test is not vacuous by temporarily reverting the two agent.py changes: all 13 new test cases fail against the pre-fix code (AttributeError: ... has no attribute 'get_approval_threshold_usd'), then pass again once the fix is restored.

Manual E2E: Not run for this PR (no network calls, no adk web/adk api_server instance started) — the change is scoped to tool-function logic covered by the unit tests above; happy to add manual E2E notes (approve/reject via the Web UI) if a maintainer wants them before merge.

Checklist

  • I have read CONTRIBUTING.md.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have added tests that prove my fix is effective.
  • New and existing unit tests pass locally with my changes.
  • I have manually tested my changes end-to-end. (not done for this PR — see above)
  • No dependent changes.

(No issue number linked — none exists yet for this specific gap; happy to open one first if a maintainer prefers that over describing the change inline.)

…rived, not caller-bypassable

contributing/samples/a2a/a2a_human_in_loop/ demonstrates human-in-the-loop
approval for large reimbursements, but the "$100 auto-approve, otherwise
ask a human" rule lived only in the agent's instruction text. Nothing in
code stopped a direct call to reimburse() -- from a model that ignores its
own instructions, or a caller crafting tool-call arguments -- from
reimbursing any amount with no approval at all.

Adds approval_config.py (one copy per agent directory, matching this
sample's existing pattern of duplicating reimburse() itself): a pure
module exposing get_approval_threshold_usd() and
requires_manager_approval(amount). No tool-call argument can influence
this value -- the only input is an environment variable read at call time.

reimburse() in both agent.py and remote_a2a/human_in_loop/agent.py now
takes a ToolContext and uses ADK's existing request_confirmation() /
tool_confirmation.confirmed mechanism (the same one already demonstrated
in contributing/samples/hitl/tool_confirmation/) to park/refuse execution
for amounts at or above the threshold, rather than relying solely on the
prompt.

No sample behavior changes for amounts under the threshold. For amounts
at/above it, a direct call to reimburse() now returns
pending_confirmation/rejected instead of ok until confirmed.
@google-cla

google-cla Bot commented Aug 6, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@adk-bot adk-bot added the tools [Component] This issue is related to tools label Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

tools [Component] This issue is related to tools

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants