diff --git a/dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/GitHubCopilotAgent.cs b/dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/GitHubCopilotAgent.cs
index 006e191b3eb..53834bad754 100644
--- a/dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/GitHubCopilotAgent.cs
+++ b/dotnet/src/Microsoft.Agents.AI.GitHub.Copilot/GitHubCopilotAgent.cs
@@ -42,6 +42,12 @@ public sealed class GitHubCopilotAgent : AIAgent, IAsyncDisposable
/// The name of the agent.
/// The description of the agent.
/// Optional JSON serializer options. Defaults to .
+ ///
+ /// Optional callback that approves or denies execution of custom function tools wrapped in
+ /// ; it must return to allow the call.
+ /// When not configured, or when it denies or throws, such tools are denied (secure-by-default). This is the
+ /// agent-level approval gate for this provider, independent of SessionConfig.OnPermissionRequest.
+ ///
public GitHubCopilotAgent(
CopilotClient copilotClient,
SessionConfig? sessionConfig = null,
@@ -49,12 +55,13 @@ public GitHubCopilotAgent(
string? id = null,
string? name = null,
string? description = null,
- JsonSerializerOptions? jsonSerializerOptions = null)
+ JsonSerializerOptions? jsonSerializerOptions = null,
+ Func>? onFunctionApproval = null)
{
_ = Throw.IfNull(copilotClient);
this._copilotClient = copilotClient;
- this._sessionConfig = sessionConfig;
+ this._sessionConfig = WrapApprovalRequiredTools(sessionConfig, onFunctionApproval);
this._ownsClient = ownsClient;
this._id = id;
this._name = name ?? DefaultName;
@@ -73,6 +80,11 @@ public GitHubCopilotAgent(
/// The tools to make available to the agent.
/// Optional instructions to append as a system message.
/// Optional JSON serializer options. Defaults to .
+ ///
+ /// Optional callback that approves or denies execution of custom function tools wrapped in
+ /// ; it must return to allow the call.
+ /// When not configured, or when it denies or throws, such tools are denied (secure-by-default).
+ ///
public GitHubCopilotAgent(
CopilotClient copilotClient,
bool ownsClient = false,
@@ -81,7 +93,8 @@ public GitHubCopilotAgent(
string? description = null,
IList? tools = null,
string? instructions = null,
- JsonSerializerOptions? jsonSerializerOptions = null)
+ JsonSerializerOptions? jsonSerializerOptions = null,
+ Func>? onFunctionApproval = null)
: this(
copilotClient,
GetSessionConfig(tools, instructions),
@@ -89,7 +102,8 @@ public GitHubCopilotAgent(
id,
name,
description,
- jsonSerializerOptions)
+ jsonSerializerOptions,
+ onFunctionApproval)
{
}
@@ -519,6 +533,49 @@ private AgentResponseUpdate ConvertToAgentResponseUpdate(SessionEvent sessionEve
return new SessionConfig { Tools = mappedTools, SystemMessage = systemMessage };
}
+ ///
+ /// Returns a in which every tool wrapped in
+ /// is replaced with an , so the GitHub Copilot SDK tool loop cannot invoke
+ /// an approval-required function without first passing the Agent Framework approval callback.
+ ///
+ ///
+ /// The source is returned unchanged when it contains no approval-required tools.
+ /// Otherwise a clone is returned so the caller-supplied configuration is not mutated.
+ ///
+ private static SessionConfig? WrapApprovalRequiredTools(
+ SessionConfig? sessionConfig,
+ Func>? onFunctionApproval)
+ {
+ if (sessionConfig?.Tools is not { Count: > 0 } tools)
+ {
+ return sessionConfig;
+ }
+
+ List mappedTools = new(tools.Count);
+ bool wrappedAny = false;
+ foreach (AIFunctionDeclaration tool in tools)
+ {
+ if (tool is AIFunction function && function.GetService() is not null)
+ {
+ mappedTools.Add(new ApprovalGatedAIFunction(function, onFunctionApproval));
+ wrappedAny = true;
+ }
+ else
+ {
+ mappedTools.Add(tool);
+ }
+ }
+
+ if (!wrappedAny)
+ {
+ return sessionConfig;
+ }
+
+ SessionConfig wrapped = sessionConfig.Clone();
+ wrapped.Tools = mappedTools;
+ return wrapped;
+ }
+
private static async Task<(List? Attachments, string? TempDir)> ProcessDataContentAttachmentsAsync(
IEnumerable messages,
CancellationToken cancellationToken)
@@ -563,4 +620,82 @@ private static void CleanupTempDir(string? tempDir)
}
}
}
+
+ ///
+ /// An decorator that enforces an Agent Framework function-approval check
+ /// before invoking a tool wrapped in .
+ ///
+ ///
+ ///
+ /// 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. As a result, an — which is only a marker
+ /// and does not enforce approval on its own — would otherwise execute without any Agent Framework approval.
+ ///
+ ///
+ /// This decorator restores the approval boundary: the underlying function is only invoked after the
+ /// configured approval callback explicitly approves the call. When no callback is configured, or the
+ /// callback denies or throws, execution is denied (secure-by-default). The decorator forwards all tool
+ /// metadata (name, description, JSON schema, and additional properties such as the Copilot
+ /// skip_permission flag) from the inner function so the tool remains transparent to the SDK.
+ ///
+ ///
+ private sealed class ApprovalGatedAIFunction : DelegatingAIFunction
+ {
+ private readonly Func>? _onFunctionApproval;
+
+ public ApprovalGatedAIFunction(
+ AIFunction innerFunction,
+ Func>? onFunctionApproval)
+ : base(innerFunction)
+ {
+ this._onFunctionApproval = onFunctionApproval;
+ }
+
+ protected override async ValueTask