Skip to content

.NET: [BREAKING] Bind tool-approval responses to surfaced approval requests#7111

Open
rogerbarreto wants to merge 5 commits into
microsoft:mainfrom
rogerbarreto:rogerbarreto/harden-tool-approval-response-binding
Open

.NET: [BREAKING] Bind tool-approval responses to surfaced approval requests#7111
rogerbarreto wants to merge 5 commits into
microsoft:mainfrom
rogerbarreto:rogerbarreto/harden-tool-approval-response-binding

Conversation

@rogerbarreto

@rogerbarreto rogerbarreto commented Jul 14, 2026

Copy link
Copy Markdown
Member

Motivation and Context

Tool approvals in the .NET stack are honored from the ToolApprovalResponseContent carried in the message stream. This change hardens that flow so an approved tool call always corresponds to the approval request the framework actually surfaced, keeping the human-in-the-loop control aligned with what a user was asked to approve.

Description

Adds ApprovalResponseBindingChatClient, registered as the outermost decorator above FunctionInvokingChatClient in the default ChatClientAgent pipeline:

  • Outbound: records each model-originated ToolApprovalRequestContent into the session AgentSessionStateBag, keyed by request id.
  • Inbound: binds each ToolApprovalResponseContent to its recorded request, rebinds the response tool call to the recorded call, consumes matched entries for one-time use, and honors only approvals tied to a framework-issued request.
  • Outside an agent run (no session): no-op passthrough with a warning.

Applies the same binding in the ToolApprovalAgent harness by tracking surfaced requests and binding collected responses to them during a queue cycle.

Adds ChatClientAgentOptions.DisableApprovalResponseBinding (default off) and a UseApprovalResponseBinding builder extension for custom chat client stacks. The Foundry hosting path already reconstructs the call from the server-side approval map and is unaffected.

Contribution Checklist

  • Unit tests for the decorator and the harness; full unit suite green
  • dotnet format clean on changed files

Breaking change

Approval-response binding introduces new session state for surfaced/recorded approval requests. A session serialized by an older build while an approval was still outstanding deserializes with this state empty, so its approval responses no longer bind to a request and are ignored on resume. Re-issue the request to obtain a fresh approval.

Harden the tool-approval flow so an approved tool call always matches the
request the framework surfaced for approval.

Add ApprovalResponseBindingChatClient as the outermost decorator above
FunctionInvokingChatClient. It records each model-originated
ToolApprovalRequestContent in the session state and, on the next request,
binds every ToolApprovalResponseContent to its recorded request: the
response tool call is rebound to the recorded call, matched entries are
consumed for one-time use, and only approvals tied to a framework-issued
request take effect.

Apply the same binding in the ToolApprovalAgent harness by tracking the
requests it surfaces and binding collected responses to them during a
queue cycle.

Add ChatClientAgentOptions.DisableApprovalResponseBinding (default off) and
a UseApprovalResponseBinding builder extension for custom chat client stacks.
Includes unit tests for the decorator and the harness.
Copilot AI review requested due to automatic review settings July 14, 2026 13:08
@giles17 giles17 added the .NET Usage: [Issues, PRs], Target: .Net label Jul 14, 2026
@rogerbarreto rogerbarreto self-assigned this Jul 14, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Automated Code Review

Reviewers: 5 | Confidence: 87%

✓ Correctness

The PR correctly implements approval-response binding at two levels: (1) AprovalResponseBindingChatClient as a DelegatingChatClient decorator that records model-originated requests and validates/rebinds inbound responses against them, and (2) ToolAprovalAgent harness binding during queue cycles. The logic for recording, matching, rebinding, consuming entries, and dropping forged/unbound content is sound. State management via AgentSessionStateBag and ToolApprovalState persists correctly across turns. The streaming and non-streaming paths are consistent. No correctness, security, or race condition issues found.

✓ Security Reliability

This PR adds a well-designed security hardening layer that binds inbound tool-approval responses to model-originated approval requests the framework surfaced, preventing forged or substituted tool calls from being honored. The implementation is sound: it uses AsyncLocal-based session scoping for thread safety, consumes matched entries to prevent replay, and rebinds tool calls to the framework-recorded originals. One minor defense-in-depth gap exists where duplicate responses with the same RequestId in a single inbound batch could all pass through (though rebound to the same legitimate tool call).

✓ Test Coverage

The PR adds comprehensive unit tests for the non-streaming path of AprovalResponseBindingChatClient (7 tests covering passthrough, recording, forged responses, rebinding, rejection, consumption, and no-session scenarios) and two security-focused ToolApprovalAgent tests. However, the streaming path (GetStreamingResponseAsync) of the ApprovalResponseBindingChatClient has a completely distinct recording mechanism (accumulating requests across async iterations in a try/finally block) that is entirely untested. There is also no test verifying the DisableApprovalResponseBinding option actually prevents the decorator from being registered.

✓ Failure Modes

The PR adds a well-structured two-layer approval response binding mechanism: AprovalResponseBindingChatClient as a pipeline decorator (handling the general case and single-request flows via the inner agent's session state) and ToolApprovalAgent harness binding (handling the multi-request queue cycle). The layers use independent state stores (session StateBag vs ToolApprovalState.SurfacedApprovalRequests) and don't interfere. Pending entries are consumed atomically after binding, preventing replay. The no-session passthrough is intentional (tested) and logged. Exception paths between consume-and-execute are acceptable: if FICC throws after an approval is consumed, a retry naturally re-surfaces the request for fresh approval. No silent failures, lost errors, or exploitable races identified.

✓ Design Approach

The new binding layer closes the forged/substituted-response hole, but both implementations still honor the same approval request more than once when a caller duplicates the same RequestId in a single inbound turn. That leaves a replay path open inside the very code path that is supposed to enforce one-time use of surfaced approvals.


Automated review by rogerbarreto's agents

Comment thread dotnet/src/Microsoft.Agents.AI/ChatClient/ApprovalResponseBindingChatClient.cs Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens the .NET tool-approval flow by binding approval responses to the specific approval requests that the framework surfaced, preventing forged or substituted tool calls from being honored downstream (notably by FunctionInvokingChatClient).

Changes:

  • Adds ApprovalResponseBindingChatClient and wires it into the default ChatClientAgent pipeline as the outermost decorator (with opt-out via ChatClientAgentOptions.DisableApprovalResponseBinding).
  • Adds a UseApprovalResponseBinding builder extension for custom chat client stacks.
  • Extends the ToolApprovalAgent harness and adds new unit tests covering forged/substituted approval response scenarios.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
dotnet/src/Microsoft.Agents.AI/ChatClient/ApprovalResponseBindingChatClient.cs New decorator that records surfaced approval requests and binds inbound approval responses to them.
dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientExtensions.cs Registers the binding decorator as the outermost default middleware (unless disabled).
dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientBuilderExtensions.cs Adds UseApprovalResponseBinding for custom pipelines.
dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentOptions.cs Adds DisableApprovalResponseBinding option and includes it in Clone().
dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/ToolApprovalState.cs Persists surfaced approval requests for binding in the harness.
dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/ToolApprovalAgent.cs Tracks surfaced requests and attempts to bind collected responses to surfaced requests during approval cycles.
dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ApprovalResponseBindingChatClientTests.cs New unit tests for request recording, binding/rebinding, replay prevention, and no-session behavior.
dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientAgentTests.cs Updates expectations for the default outermost decorator type.
dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/ToolApproval/ToolApprovalAgentTests.cs Adds harness security tests for forged/substituted approvals during queue cycles.
Comments suppressed due to low confidence (1)

dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/ToolApprovalAgent.cs:266

  • In the streaming path, surfaced approval requests are only recorded when unapproved.Count > 1. With approval-response binding now keyed off SurfacedApprovalRequests, a single unapproved request can be surfaced without ever being recorded, leaving subsequent responses unbindable (and allowing forged/substituted approvals to pass through unchanged). Record the surfaced request(s) whenever unapproved.Count > 0, and keep the queueing of excess requests conditional.
            // 5. Queue excess unapproved requests and yield only the first to the caller.
            if (unapproved.Count > 1)
            {
                // Record every unapproved request as surfaced so the caller's responses can be bound to a
                // model-originated request during the queue cycle.
                RecordSurfacedApprovalRequests(state, unapproved);

                state.QueuedApprovalRequests.AddRange(unapproved.GetRange(1, unapproved.Count - 1));
            }

            this._sessionState.SaveState(session, state);
            yield return new AgentResponseUpdate(ChatRole.Assistant, [unapproved[0]]);

Comment thread dotnet/src/Microsoft.Agents.AI/ChatClient/ApprovalResponseBindingChatClient.cs Outdated
Address review feedback on the approval-response binding decorator:
consume a matched request from the per-turn lookup so a duplicate response
with the same request id in one turn is honored only once, and return the
materialized message list instead of the original enumerable so a single-use
sequence is not enumerated twice. Rename the local pending list to
pendingRequests for clarity. Adds a duplicate-response regression test.
Comment thread dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/ToolApprovalAgent.cs Outdated
Comment thread dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/ToolApprovalAgent.cs Outdated
…he harness

Address review feedback on ToolApprovalAgent:
store a snapshot of each surfaced/pending approval request (cloned tool call
with a copied arguments dictionary) so a later mutation of the caller-visible
instance cannot change the recorded call used to bind the response, and consume
a surfaced request on match so a duplicate response with the same request id in
one pass is honored only once. Apply both symmetrically in the harness and the
ApprovalResponseBindingChatClient decorator. Adds regression tests for the
snapshot and duplicate-response cases.
Comment thread dotnet/src/Microsoft.Agents.AI/ChatClient/ApprovalResponseBindingChatClient.cs Outdated
Comment thread dotnet/src/Microsoft.Agents.AI/ChatClient/ApprovalResponseBindingChatClient.cs Outdated
Comment thread dotnet/src/Microsoft.Agents.AI/ChatClient/ApprovalResponseBindingChatClient.cs Outdated
Comment thread dotnet/src/Microsoft.Agents.AI/ChatClient/ApprovalResponseBindingChatClient.cs Outdated
Comment thread dotnet/src/Microsoft.Agents.AI/ChatClient/ApprovalResponseBindingChatClient.cs Outdated
Comment thread dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentOptions.cs
Comment thread dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/ToolApprovalAgent.cs Outdated
Comment thread dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/ToolApprovalAgent.cs Outdated
Comment thread dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/ToolApprovalAgent.cs Outdated
Comment thread dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/ToolApprovalAgent.cs Outdated
Comment thread dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/ToolApprovalState.cs Outdated
- Harness: store surfaced approval requests in a dictionary and consume matches directly, drop the extra hashset and the redundant record-time dedup; replace clear-on-resolution with a debug assert.
- Harness pipeline: add UseApprovalResponseBinding() as the outermost decorator in HarnessAgent (it uses UseProvidedChatClientAsIs) behind a new DisableApprovalResponseBinding option, with tests.
- Decorator: avoid message/content allocations when nothing changes, keep the original content when a response already matches the recorded call, clear pending each inbound turn, and shorten helpers.
@rogerbarreto rogerbarreto added the breaking change Usage: [PRs], Target: all PRs that introduce changes that are not backward compatible label Jul 15, 2026
@github-actions github-actions Bot changed the title .NET: Bind tool-approval responses to surfaced approval requests .NET: [BREAKING] Bind tool-approval responses to surfaced approval requests Jul 15, 2026
…ool-approval-response-binding

# Conflicts:
#	dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/ToolApprovalAgent.cs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking change Usage: [PRs], Target: all PRs that introduce changes that are not backward compatible .NET Usage: [Issues, PRs], Target: .Net

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants