Skip to content

.NET: Enforce ApprovalRequiredAIFunction in GitHub Copilot provider#6674

Draft
giles17 wants to merge 2 commits into
microsoft:mainfrom
giles17:fix-copilot-approval-enforcement
Draft

.NET: Enforce ApprovalRequiredAIFunction in GitHub Copilot provider#6674
giles17 wants to merge 2 commits into
microsoft:mainfrom
giles17:fix-copilot-approval-enforcement

Conversation

@giles17

@giles17 giles17 commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Motivation & Context

The .NET Microsoft.Agents.AI.GitHub.Copilot provider did not honor the Agent Framework approval boundary for custom function tools wrapped in ApprovalRequiredAIFunction. The GitHub Copilot SDK owns the model tool-calling loop and invokes registered custom functions directly, so the standard FunctionInvokingChatClient approval round-trip (emitting a ToolApprovalRequestContent and waiting for a ToolApprovalResponseContent) never runs for this provider. ApprovalRequiredAIFunction is only a marker and does not enforce approval on its own, so a function a developer explicitly marked as approval-required could execute with no Agent Framework approval.

This restores the expected Tool Approval contract and brings the .NET provider in line with the Python GitHub Copilot provider, which already gates approval_mode="always_require" tools through a dedicated function-approval callback.

Description & Review Guide

  • What are the major changes?

    • Added an optional onFunctionApproval callback to both GitHubCopilotAgent constructors, distinct from SessionConfig.OnPermissionRequest (which gates the Copilot SDK's built-in shell/file actions).
    • Added WrapApprovalRequiredTools, which replaces every tool resolving to an ApprovalRequiredAIFunction with a new private ApprovalGatedAIFunction before the tools reach the SDK. Non-approval tools pass through unchanged, and a caller-supplied SessionConfig is only cloned when wrapping is needed.
    • ApprovalGatedAIFunction (a DelegatingAIFunction) enforces approval in InvokeCoreAsync and forwards all tool metadata (name, description, JSON schema, and additional properties such as the Copilot skip_permission flag) so it stays transparent to the SDK.
    • Suppressed MEAI001 (experimental ApprovalRequiredAIFunction API) in the affected projects.
    • Added unit tests and one positive-path integration test.
  • What is the impact of these changes?

    • Secure-by-default: an approval-required tool is denied unless an onFunctionApproval callback explicitly approves the specific call; a missing callback, or a callback that denies or throws, results in denial.
    • No impact on existing callers: the new parameter is optional and tools that are not approval-required are unaffected.
  • What do you want reviewers to focus on?

    • The enforcement choke point in the constructor/WrapApprovalRequiredTools and that the gate remains transparent to the SDK (metadata + skip_permission preserved).

Related Issue

Fixes #6671

Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR is linked to an issue and there is no other open PR for this issue (see Related Issue above).
  • This is not a breaking change. If it is a breaking change, add the breaking change label (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.

The GitHub Copilot SDK owns the tool-calling loop and invokes registered
custom functions directly, so the standard FunctionInvokingChatClient
approval round-trip never runs for this provider. As a result a tool wrapped
in ApprovalRequiredAIFunction (only a marker) could execute without any
Agent Framework approval.

Add an agent-level onFunctionApproval callback and wrap approval-required
tools in an ApprovalGatedAIFunction that enforces approval before invoking
the underlying function. Secure-by-default: with no callback, or when the
callback denies or throws, execution is denied. The gate forwards tool
metadata (including the Copilot skip_permission flag) so it stays
transparent to the SDK. This mirrors the Python provider's behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 22, 2026 20:36
@moonbox3 moonbox3 added the .NET Usage: [Issues, PRs], Target: .Net label Jun 22, 2026

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 restores the Agent Framework tool-approval boundary for the .NET Microsoft.Agents.AI.GitHub.Copilot provider by wrapping ApprovalRequiredAIFunction tools in an approval-enforcing delegating function before they reach the Copilot SDK’s tool loop.

Changes:

  • Added an optional onFunctionApproval callback to GitHubCopilotAgent constructors and applied tool wrapping via WrapApprovalRequiredTools.
  • Introduced ApprovalGatedAIFunction to enforce approval checks at invocation time while preserving tool metadata (e.g., skip_permission).
  • Added unit tests and an integration test validating deny-by-default, approve/deny/throw behaviors, and metadata preservation; suppressed MEAI001 where needed.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/GitHubCopilotAgent.cs Wraps approval-required tools and enforces approval in a delegating function before Copilot SDK invocation.
dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/Microsoft.Agents.AI.GitHub.Copilot.csproj Suppresses MEAI001 for experimental approval-required function API usage.
dotnet/tests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests/GitHubCopilotAgentTests.cs Adds unit tests covering deny-by-default, approve/deny/throw, skip-permission preservation, and pass-through behavior.
dotnet/tests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests/Microsoft.Agents.AI.GitHub.Copilot.UnitTests.csproj Suppresses MEAI001 for experimental API usage in tests.
dotnet/tests/Microsoft.Agents.AI.GitHub.Copilot.IntegrationTests/GitHubCopilotAgentTests.cs Adds an integration test verifying approval-required tool execution when the callback approves.
dotnet/tests/Microsoft.Agents.AI.GitHub.Copilot.IntegrationTests/Microsoft.Agents.AI.GitHub.Copilot.IntegrationTests.csproj Suppresses MEAI001 for experimental API usage in tests.

Comment thread dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/GitHubCopilotAgent.cs

@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 is well-structured and the core approval-gate mechanism is correct. The type hierarchy (AprovalRequiredAIFunction → DelegatingAIFunction → AIFunction → AIFunctionDeclaration) ensures detection via GetService<AprovalRequiredAIFunction>() works, metadata forwarding through DelegatingAIFunction is sound, and the secure-by-default deny policy is properly implemented. One correctness concern: the catch (Exception) in IsApprovedAsync swallows OperationCanceledException, which breaks .NET's cooperative cancellation pattern — a cancelled operation would silently return a denial string instead of propagating the cancellation to the caller.

✓ Security Reliability

This PR correctly enforces the AprovalRequiredAIFunction contract for the GitHub Copilot provider. The security design is sound: secure-by-default denial when no callback is configured, when the callback returns false, and when the callback throws. The approval gate is properly applied at construction time and the inner class is private sealed, preventing bypasses. One reliability concern: the catch-all in IsApprovedAsync swallows OperationCanceledException, which prevents proper cancellation propagation — though this is a tradeoff against the secure-by-default posture, not a security hole.

✓ Test Coverage

Test coverage is thorough for the new approval-gating feature. All four approval decision paths (approve, deny, throw, no callback) are covered, metadata preservation is verified, and non-approval tool passthrough is tested. One minor gap: there is no test with a mixed tools list (both approval-required and regular tools in the same configuration), which is the realistic deployment scenario and exercises the WrapApprovalRequiredTools loop's per-tool branching in a single pass.

✗ Failure Modes

The implementation is solid overall — secure-by-default denial, proper metadata forwarding, clone-on-write for SessionConfig, and comprehensive test coverage. One concrete failure mode: the catch-all catch (Exception) in IsApprovedAsync (line 688) silently swallows OperationCanceledException, converting caller-requested cancellation into a tool-denial string instead of propagating it. This hides timeouts and cancellation from the SDK tool loop and the caller.

✓ Design Approach

The approval wrapper fixes the main bypass, but it currently collapses cancellation into a normal approval denial. That changes the control-flow contract for aborted runs and can surface a misleading "request was denied" result where the rest of the repo preserves OperationCanceledException.

Flagged Issues

  • catch (Exception) at GitHubCopilotAgent.cs:688 swallows OperationCanceledException, silently converting cancellation into a denial string. Callers cancelling via CancellationToken will never observe the cancellation — the tool loop continues with a fake 'denied' result.

Automated review by giles17's agents

@github-actions

Copy link
Copy Markdown
Contributor

Flagged issue

catch (Exception) at GitHubCopilotAgent.cs:688 swallows OperationCanceledException, silently converting cancellation into a denial string. Callers cancelling via CancellationToken will never observe the cancellation — the tool loop continues with a fake 'denied' result.


Source: automated DevFlow PR review

Let OperationCanceledException propagate from the approval callback instead
of swallowing it into a denial, so cooperative cancellation is honored.
Other callback failures still deny by default. Added a unit test.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@giles17 giles17 marked this pull request as ready for review June 22, 2026 21:21

@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: 89% | Result: All clear

Reviewed: Correctness, Security Reliability, Test Coverage, Failure Modes, Design Approach


Automated review by giles17's agents

string? description = null,
JsonSerializerOptions? jsonSerializerOptions = null)
JsonSerializerOptions? jsonSerializerOptions = null,
Func<FunctionCallContent, CancellationToken, ValueTask<bool>>? onFunctionApproval = null)

@westey-m westey-m Jun 23, 2026

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.

The SDK supports the OnPreToolUse hook, which seems to already provide the same functionality:
https://docs.github.com/en/copilot/how-tos/copilot-sdk/hooks/pre-tool-use

There is also the OnPermissionRequest hook. See: https://docs.github.com/en/copilot/how-tos/copilot-sdk/features/hooks

Do we really need our own?

@giles17 giles17 marked this pull request as draft June 24, 2026 15:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

.NET Usage: [Issues, PRs], Target: .Net

Projects

None yet

Development

Successfully merging this pull request may close these issues.

.NET: GitHub Copilot provider doesn't enforce ApprovalRequiredAIFunction for custom function tools

4 participants