Skip to content

Python: Enforce approval_mode in FunctionTool.invoke()#5494

Open
chetantoshniwal wants to merge 1 commit intomicrosoft:mainfrom
chetantoshniwal:fix/approval-mode-bypass-109288-upstream
Open

Python: Enforce approval_mode in FunctionTool.invoke()#5494
chetantoshniwal wants to merge 1 commit intomicrosoft:mainfrom
chetantoshniwal:fix/approval-mode-bypass-109288-upstream

Conversation

@chetantoshniwal
Copy link
Copy Markdown
Contributor

Motivation and Context

FunctionTool.invoke() previously ignored approval_mode='always_require', allowing direct callers (e.g., Claude/Copilot integrations) to bypass the human approval gate that only existed in _try_execute_function_calls().

Description

  • Add ToolApprovalRequiredException to exceptions.py
  • Add approval_mode check at the top of FunctionTool.invoke()
  • Add _approved parameter (default False) to invoke() signature and overloads
  • Pass _approved=True from _auto_invoke_function() call sites (both direct and middleware pipeline paths)
  • Export ToolApprovalRequiredException from agent_framework package
  • Add comprehensive tests for the new guard

Contribution Checklist

  • The code builds clean without any errors or warnings
  • The PR follows the Contribution Guidelines
  • All unit tests pass, and I have added new tests where possible
  • Is this a breaking change? If yes, add "[BREAKING]" prefix to the title of the PR.

FunctionTool.invoke() previously ignored approval_mode='always_require',
allowing direct callers (e.g., Claude/Copilot integrations) to bypass
the human approval gate that only existed in _try_execute_function_calls().

Changes:
- Add ToolApprovalRequiredException to exceptions.py
- Add approval_mode check at the top of FunctionTool.invoke()
- Add _approved parameter (default False) to invoke() signature and overloads
- Pass _approved=True from _auto_invoke_function() call sites
  (both direct and middleware pipeline paths)
- Export ToolApprovalRequiredException from agent_framework package
- Add comprehensive tests for the new guard

Based on upstream microsoft/agent-framework main branch.
Fixes MSRC Case 109288 (VULN-176822)

Co-authored-by: Azure SRE Agent <noreply@microsoft.com>
Copilot AI review requested due to automatic review settings April 26, 2026 06:40
@chetantoshniwal chetantoshniwal requested review from eavanvalkenburg and removed request for Copilot April 26, 2026 06:40
@github-actions github-actions Bot changed the title Fix CWE-862: Enforce approval_mode in FunctionTool.invoke() Python: Fix CWE-862: Enforce approval_mode in FunctionTool.invoke() Apr 26, 2026
@chetantoshniwal chetantoshniwal requested a review from giles17 April 26, 2026 06:40
@moonbox3
Copy link
Copy Markdown
Contributor

Python Test Coverage

Python Test Coverage Report •
FileStmtsMissCoverMissing
packages/core/agent_framework
   _tools.py9848391%219–220, 395, 397, 410, 435–437, 445, 463, 477, 484, 491, 514, 516, 523, 531, 666, 706–708, 710, 716, 768–770, 795, 821, 825, 863–865, 869, 891, 1033–1039, 1075, 1087, 1089, 1091, 1094–1097, 1118, 1122, 1126, 1140–1142, 1483, 1594–1600, 1729, 1733, 1779, 1840–1841, 1956, 1976, 1978, 2034, 2097, 2269–2270, 2290, 2346–2347, 2485–2486, 2553, 2558, 2565
   exceptions.py680100% 
TOTAL29870347988% 

Python Unit Test Overview

Tests Skipped Failures Errors Time
6028 30 💤 0 ❌ 0 🔥 1m 37s ⏱️

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated Code Review

Reviewers: 4 | Confidence: 92%

✗ Correctness

The core approval-gate logic is correct: invoke() properly checks approval_mode == 'always_require' before the _approved flag, both auto-invocation paths pass _approved=True, and the new exception inherits from ToolException. Two issues found: (1) ToolApprovalRequiredException is inserted out of alphabetical order in __all__, breaking the clearly maintained sorted convention; (2) a test uses approval_mode="auto" which is not a valid ApprovalMode value (Literal["always_require", "never_require"] per line 93 of _tools.py), testing behavior on an undeclared mode.

✓ Security Reliability

This PR adds a defense-in-depth approval gate to FunctionTool.invoke() that raises ToolApprovalRequiredException when a tool with approval_mode='always_require' is called without the _approved flag. The real approval gate lives in _try_execute_function_calls (lines 1641-1672), which intercepts approval-required calls before execution and returns approval requests to the user. The _approved flag on invoke() is a secondary guard against direct calers bypassing the pipeline. The implementation is sound: the auto-invoke pipeline correctly passes _approved=True in both the direct (line 1522 in diff) and middleware (line 1554 in diff) paths, and _try_execute_function_calls already handles the approval flow upstream. No security or reliability issues found.

✓ Test Coverage

The new approval gate mechanism on FunctionTool.invoke() is well-tested at the unit level, with tests covering the blocked case, the _approved=True bypass, default/auto modes, and the call escape hatch. The pipeline integration path (_auto_invoke_function passing _approved=True) is already covered by existing tests in test_function_invocation_logic.py (e.g., test_approved_function_call_successful_execution). No bugs or missing critical coverage found. One minor suggestion for improving test robustness.

✓ Design Approach

The change does not enforce approval at the real tool execution boundary. It adds a new _approved boolean to the public invoke() API, but that flag is entirely caller-controlled and the underlying FunctionTool.__call__ path still executes approval_mode="always_require" tools without any check. Because the framework already has a first-class approval flow based on function_approval_request/function_approval_response content, this patch looks like a symptom-level guard one call path rather than a robust approval model.

Suggestions

  • Enforce the approval policy in the shared execution path (_tools.py:511-538) rather than only in invoke(). Currently FunctionTool.__call__ executes approval_mode='always_require' tools without any check, so alternative integrations can accidentally skip the approval contract.

Automated review by chetantoshniwal's agents

@eavanvalkenburg
Copy link
Copy Markdown
Member

I would prefer that we fix this in Claude and Copilot, because this makes it seem like a fix, that is not a actual fix, this line in the docstring gives away the game, so this adds a new parameter which can be spoofed with ease once you have access to code:

Callers outside the pipeline must obtain human approval and pass
``_approved=True`` to execute tools with ``approval_mode='always_require'``.

since there is nothing preventing you from calling with _approved=True without obtaining human approval, so let's fix Claude/Copilot and not add parameters that don't actually fix anything.

@moonbox3 moonbox3 changed the title Python: Fix CWE-862: Enforce approval_mode in FunctionTool.invoke() Python: Enforce approval_mode in FunctionTool.invoke() Apr 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants