From bbdd5516537d4482e88c0826703cacc721c48dae Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Tue, 14 Jul 2026 10:18:46 +0000 Subject: [PATCH 1/2] Graduate ToolApprovalAgent and introduce tool auto approval context --- .../Harness/Harness_Step05_Loop/Program.cs | 4 +- .../Harness/FileAccess/FileAccessProvider.cs | 8 +-- ...lwaysApproveToolApprovalResponseContent.cs | 3 -- .../Harness/ToolApproval/ToolApprovalAgent.cs | 47 ++++++++++------ .../ToolApprovalAgentBuilderExtensions.cs | 3 -- .../ToolApproval/ToolApprovalAgentOptions.cs | 11 ++-- .../ToolApprovalRequestContentExtensions.cs | 3 -- .../Harness/ToolApproval/ToolApprovalRule.cs | 3 -- .../Harness/ToolApproval/ToolApprovalState.cs | 3 -- .../ToolAutoApprovalRuleContext.cs | 54 +++++++++++++++++++ .../Skills/AgentSkillsProvider.cs | 8 +-- .../HarnessAgentTests.cs | 2 +- .../AgentSkills/AgentSkillsProviderTests.cs | 19 ++++--- .../FileAccess/FileAccessProviderTests.cs | 7 ++- .../ToolApproval/ToolApprovalAgentTests.cs | 19 ++++--- 15 files changed, 126 insertions(+), 68 deletions(-) create mode 100644 dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/ToolAutoApprovalRuleContext.cs diff --git a/dotnet/samples/02-agents/Harness/Harness_Step05_Loop/Program.cs b/dotnet/samples/02-agents/Harness/Harness_Step05_Loop/Program.cs index 98b18172fad..4bb77901e01 100644 --- a/dotnet/samples/02-agents/Harness/Harness_Step05_Loop/Program.cs +++ b/dotnet/samples/02-agents/Harness/Harness_Step05_Loop/Program.cs @@ -174,9 +174,9 @@ async Task ApprovalLoopAsync() { AutoApprovalRules = [ - functionCall => + context => { - Console.WriteLine($" Auto-approving: {functionCall.Name}"); + Console.WriteLine($" Auto-approving: {context.FunctionCallContent.Name}"); return ValueTask.FromResult(true); }, ], diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/FileAccess/FileAccessProvider.cs b/dotnet/src/Microsoft.Agents.AI/Harness/FileAccess/FileAccessProvider.cs index 95594333aee..9f43cb39f62 100644 --- a/dotnet/src/Microsoft.Agents.AI/Harness/FileAccess/FileAccessProvider.cs +++ b/dotnet/src/Microsoft.Agents.AI/Harness/FileAccess/FileAccessProvider.cs @@ -189,8 +189,8 @@ public FileAccessProvider(AgentFileStore fileStore, FileAccessProviderOptions? o /// human approval boundary. Ensure no other tool collides with these reserved names. /// /// - public static Func> ReadOnlyToolsAutoApprovalRule { get; } = - functionCall => new ValueTask(s_readOnlyToolNames.Contains(functionCall.Name)); + public static Func> ReadOnlyToolsAutoApprovalRule { get; } = + context => new ValueTask(s_readOnlyToolNames.Contains(context.FunctionCallContent.Name)); /// /// Gets an auto-approval rule that approves all file access tools, including the tools that modify the @@ -226,8 +226,8 @@ public FileAccessProvider(AgentFileStore fileStore, FileAccessProviderOptions? o /// human approval boundary. Ensure no other tool collides with these reserved names. /// /// - public static Func> AllToolsAutoApprovalRule { get; } = - functionCall => new ValueTask(s_allToolNames.Contains(functionCall.Name)); + public static Func> AllToolsAutoApprovalRule { get; } = + context => new ValueTask(s_allToolNames.Contains(context.FunctionCallContent.Name)); /// public override IReadOnlyList StateKeys => []; diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/AlwaysApproveToolApprovalResponseContent.cs b/dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/AlwaysApproveToolApprovalResponseContent.cs index df9631713e2..929eb41bfe0 100644 --- a/dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/AlwaysApproveToolApprovalResponseContent.cs +++ b/dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/AlwaysApproveToolApprovalResponseContent.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.AI; -using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.AI; @@ -25,7 +23,6 @@ namespace Microsoft.Agents.AI; /// entries in the session state. /// /// -[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] public sealed class AlwaysApproveToolApprovalResponseContent : AIContent { /// diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/ToolApprovalAgent.cs b/dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/ToolApprovalAgent.cs index 1143fae9c51..9527d2d406f 100644 --- a/dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/ToolApprovalAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/ToolApprovalAgent.cs @@ -2,14 +2,12 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.AI; -using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Agents.AI; @@ -46,12 +44,11 @@ namespace Microsoft.Agents.AI; /// Tool+arguments: Approve all calls to a specific tool with exactly matching arguments. /// /// -[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] public sealed class ToolApprovalAgent : DelegatingAIAgent { private readonly ProviderSessionState _sessionState; private readonly JsonSerializerOptions _jsonSerializerOptions; - private readonly Func>[]? _autoApprovalRules; + private readonly Func>[]? _autoApprovalRules; /// /// Initializes a new instance of the class. @@ -96,7 +93,7 @@ public ToolApprovalAgent(AIAgent innerAgent, ToolApprovalAgentOptions? options = /// of the tool name () or arguments. Only use this rule in a fully trusted context. /// /// - public static Func> AllToolsAutoApprovalRule { get; } = + public static Func> AllToolsAutoApprovalRule { get; } = _ => new ValueTask(true); /// @@ -106,8 +103,10 @@ protected override async Task RunCoreAsync( AgentRunOptions? options = null, CancellationToken cancellationToken = default) { + var requestMessages = messages as IReadOnlyCollection ?? messages.ToList(); + // Steps 1–2: Unwrap AlwaysApprove wrappers, process any queued approval requests. - var (state, callerMessages, nextQueuedItem) = await this.PrepareInboundMessagesAsync(messages, session).ConfigureAwait(false); + var (state, callerMessages, nextQueuedItem) = await this.PrepareInboundMessagesAsync(requestMessages, session, options).ConfigureAwait(false); if (nextQueuedItem is not null) { @@ -126,7 +125,7 @@ protected override async Task RunCoreAsync( var response = await this.InnerAgent.RunAsync(processedMessages, session, options, cancellationToken).ConfigureAwait(false); // Classify approval requests: auto-approve matching, queue excess, keep first unapproved. - bool allAutoApproved = await this.ProcessAndQueueOutboundApprovalRequestsAsync(response.Messages, state, session).ConfigureAwait(false); + bool allAutoApproved = await this.ProcessAndQueueOutboundApprovalRequestsAsync(response.Messages, state, session, options, requestMessages).ConfigureAwait(false); if (!allAutoApproved) { @@ -146,8 +145,10 @@ protected override async IAsyncEnumerable RunCoreStreamingA AgentRunOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { + var requestMessages = messages as IReadOnlyCollection ?? messages.ToList(); + // Steps 1–2: Unwrap AlwaysApprove wrappers, process any queued approval requests. - var (state, callerMessages, nextQueuedItem) = await this.PrepareInboundMessagesAsync(messages, session).ConfigureAwait(false); + var (state, callerMessages, nextQueuedItem) = await this.PrepareInboundMessagesAsync(requestMessages, session, options).ConfigureAwait(false); if (nextQueuedItem is not null) { @@ -234,7 +235,7 @@ protected override async IAsyncEnumerable RunCoreStreamingA state.CollectedApprovalResponses.Add( tarc.CreateResponse(approved: true, reason: "Auto-approved by standing rule")); } - else if (await this.MatchesAutoApprovalRuleAsync(tarc).ConfigureAwait(false)) + else if (await this.MatchesAutoApprovalRuleAsync(tarc, session, options, requestMessages).ConfigureAwait(false)) { state.CollectedApprovalResponses.Add( tarc.CreateResponse(approved: true, reason: "Auto-approved by auto-approval rule")); @@ -326,7 +327,11 @@ private static void CollectApprovalResponsesFromMessages( /// /// Re-evaluates queued approval requests against current rules and auto-approval rules, and auto-approves any that now match. /// - private async ValueTask DrainAutoApprovableFromQueueAsync(ToolApprovalState state) + private async ValueTask DrainAutoApprovableFromQueueAsync( + ToolApprovalState state, + AgentSession? session, + AgentRunOptions? options, + IReadOnlyCollection requestMessages) { for (int i = state.QueuedApprovalRequests.Count - 1; i >= 0; i--) { @@ -336,7 +341,7 @@ private async ValueTask DrainAutoApprovableFromQueueAsync(ToolApprovalState stat state.QueuedApprovalRequests[i].CreateResponse(approved: true, reason: "Auto-approved by standing rule")); state.QueuedApprovalRequests.RemoveAt(i); } - else if (await this.MatchesAutoApprovalRuleAsync(state.QueuedApprovalRequests[i]).ConfigureAwait(false)) + else if (await this.MatchesAutoApprovalRuleAsync(state.QueuedApprovalRequests[i], session, options, requestMessages).ConfigureAwait(false)) { state.CollectedApprovalResponses.Add( state.QueuedApprovalRequests[i].CreateResponse(approved: true, reason: "Auto-approved by auto-approval rule")); @@ -358,7 +363,7 @@ private async ValueTask DrainAutoApprovableFromQueueAsync(ToolApprovalState stat /// When the returned item is non-null, the caller should return/yield it without calling the inner agent. /// private async ValueTask<(ToolApprovalState State, List CallerMessages, ToolApprovalRequestContent? NextQueuedItem)> - PrepareInboundMessagesAsync(IEnumerable messages, AgentSession? session) + PrepareInboundMessagesAsync(IReadOnlyCollection messages, AgentSession? session, AgentRunOptions? options) { var state = this._sessionState.GetOrInitializeState(session); @@ -376,7 +381,7 @@ private async ValueTask DrainAutoApprovableFromQueueAsync(ToolApprovalState stat // Re-evaluate remaining queued items — the caller may have added new rules // (e.g., "always approve this tool") that resolve additional items. - await this.DrainAutoApprovableFromQueueAsync(state).ConfigureAwait(false); + await this.DrainAutoApprovableFromQueueAsync(state, session, options, messages).ConfigureAwait(false); if (state.QueuedApprovalRequests.Count > 0) { @@ -428,7 +433,9 @@ private List InjectCollectedResponses( private async ValueTask ProcessAndQueueOutboundApprovalRequestsAsync( IList responseMessages, ToolApprovalState state, - AgentSession? session) + AgentSession? session, + AgentRunOptions? options, + IReadOnlyCollection requestMessages) { // Pass 1: Scan all response messages and classify each approval request. // Auto-approved requests (matching a standing rule or auto-approval rule) have their @@ -451,7 +458,7 @@ private async ValueTask ProcessAndQueueOutboundApprovalRequestsAsync( toRemove.Add(tarc); autoApprovedCount++; } - else if (await this.MatchesAutoApprovalRuleAsync(tarc).ConfigureAwait(false)) + else if (await this.MatchesAutoApprovalRuleAsync(tarc, session, options, requestMessages).ConfigureAwait(false)) { state.CollectedApprovalResponses.Add( tarc.CreateResponse(approved: true, reason: "Auto-approved by auto-approval rule")); @@ -712,7 +719,11 @@ internal static bool MatchesRule( /// if any auto-approval rule returns for the function call; /// if no rules are configured, the request is not a function call, or no rule approves it. /// - private async ValueTask MatchesAutoApprovalRuleAsync(ToolApprovalRequestContent request) + private async ValueTask MatchesAutoApprovalRuleAsync( + ToolApprovalRequestContent request, + AgentSession? session, + AgentRunOptions? options, + IReadOnlyCollection requestMessages) { if (this._autoApprovalRules is not { Length: > 0 }) { @@ -724,9 +735,11 @@ private async ValueTask MatchesAutoApprovalRuleAsync(ToolApprovalRequestCo return false; } + var context = new ToolAutoApprovalRuleContext(functionCall, this, session, requestMessages, options); + foreach (var rule in this._autoApprovalRules) { - if (await rule(functionCall).ConfigureAwait(false)) + if (await rule(context).ConfigureAwait(false)) { return true; } diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/ToolApprovalAgentBuilderExtensions.cs b/dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/ToolApprovalAgentBuilderExtensions.cs index ea0a2b658f7..4d5ae288928 100644 --- a/dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/ToolApprovalAgentBuilderExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/ToolApprovalAgentBuilderExtensions.cs @@ -1,7 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Diagnostics.CodeAnalysis; -using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.AI; @@ -9,7 +7,6 @@ namespace Microsoft.Agents.AI; /// /// Provides extension methods for adding tool approval middleware to instances. /// -[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] public static class ToolApprovalAgentBuilderExtensions { /// diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/ToolApprovalAgentOptions.cs b/dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/ToolApprovalAgentOptions.cs index 2db2b69b981..41817fd9514 100644 --- a/dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/ToolApprovalAgentOptions.cs +++ b/dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/ToolApprovalAgentOptions.cs @@ -2,18 +2,14 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Threading.Tasks; -using Microsoft.Extensions.AI; -using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Agents.AI; /// /// Options for configuring the middleware. /// -[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] public class ToolApprovalAgentOptions { /// @@ -31,8 +27,9 @@ public class ToolApprovalAgentOptions /// /// /// - /// Each function receives a representing the tool call that requires approval - /// and returns a that resolves to to auto-approve + /// Each function receives a describing the tool call that requires + /// approval (via ) along with the surrounding run + /// context, and returns a that resolves to to auto-approve /// the call, or to continue evaluating the next rule. /// /// @@ -48,5 +45,5 @@ public class ToolApprovalAgentOptions /// otherwise that tool will be auto-approved without a human prompt, bypassing the approval boundary. /// /// - public IEnumerable>>? AutoApprovalRules { get; set; } + public IEnumerable>>? AutoApprovalRules { get; set; } } diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/ToolApprovalRequestContentExtensions.cs b/dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/ToolApprovalRequestContentExtensions.cs index 9974962ed59..09ad2cf87f4 100644 --- a/dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/ToolApprovalRequestContentExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/ToolApprovalRequestContentExtensions.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.AI; -using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.AI; @@ -12,7 +10,6 @@ namespace Microsoft.Agents.AI; /// instances that instruct the /// middleware to record standing approval rules. /// -[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] public static class ToolApprovalRequestContentExtensions { /// diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/ToolApprovalRule.cs b/dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/ToolApprovalRule.cs index 2a8ab690bb7..26b5befe5d6 100644 --- a/dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/ToolApprovalRule.cs +++ b/dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/ToolApprovalRule.cs @@ -1,9 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; -using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Agents.AI; @@ -29,7 +27,6 @@ namespace Microsoft.Agents.AI; /// as a non-null dictionary so it cannot be silently broadened to all invocations of the tool. /// /// -[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] internal sealed class ToolApprovalRule { /// diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/ToolApprovalState.cs b/dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/ToolApprovalState.cs index b740dc03c73..47b63f79ed5 100644 --- a/dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/ToolApprovalState.cs +++ b/dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/ToolApprovalState.cs @@ -1,10 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; using Microsoft.Extensions.AI; -using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Agents.AI; @@ -12,7 +10,6 @@ namespace Microsoft.Agents.AI; /// Represents the persisted state of standing tool approval rules, /// stored in the session's . /// -[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] internal sealed class ToolApprovalState { /// diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/ToolAutoApprovalRuleContext.cs b/dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/ToolAutoApprovalRuleContext.cs new file mode 100644 index 00000000000..c848ee835f0 --- /dev/null +++ b/dotnet/src/Microsoft.Agents.AI/Harness/ToolApproval/ToolAutoApprovalRuleContext.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using Microsoft.Extensions.AI; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Agents.AI; + +/// +/// Provides context for a tool auto-approval rule evaluated by the . +/// +/// +/// This type wraps the that requires approval together with the +/// surrounding run context (agent, session, request messages, and run options). +/// +public sealed class ToolAutoApprovalRuleContext +{ + /// + /// Initializes a new instance of the class. + /// + /// The representing the tool call that requires approval. + /// The that is evaluating the tool call. + /// The that is associated with the current run, if any. + /// The request messages passed into the current run. + /// The that was passed to the current run, if any. + public ToolAutoApprovalRuleContext( + FunctionCallContent functionCallContent, + AIAgent agent, + AgentSession? session, + IReadOnlyCollection requestMessages, + AgentRunOptions? agentRunOptions) + { + this.FunctionCallContent = Throw.IfNull(functionCallContent); + this.Agent = Throw.IfNull(agent); + this.Session = session; + this.RequestMessages = Throw.IfNull(requestMessages); + this.RunOptions = agentRunOptions; + } + + /// Gets the representing the tool call that requires approval. + public FunctionCallContent FunctionCallContent { get; } + + /// Gets the that is evaluating the tool call. + public AIAgent Agent { get; } + + /// Gets the that is associated with the current run. + public AgentSession? Session { get; } + + /// Gets the request messages passed into the current run. + public IReadOnlyCollection RequestMessages { get; } + + /// Gets the that was passed to the current run. + public AgentRunOptions? RunOptions { get; } +} diff --git a/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillsProvider.cs b/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillsProvider.cs index 09922b52f3a..04fc0d00339 100644 --- a/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillsProvider.cs +++ b/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillsProvider.cs @@ -105,8 +105,8 @@ public sealed partial class AgentSkillsProvider : AIContextProvider, IDisposable /// human approval boundary. Ensure no other tool collides with these reserved names. /// /// - public static Func> ReadOnlyToolsAutoApprovalRule { get; } = - functionCall => new ValueTask(s_readOnlyToolNames.Contains(functionCall.Name)); + public static Func> ReadOnlyToolsAutoApprovalRule { get; } = + context => new ValueTask(s_readOnlyToolNames.Contains(context.FunctionCallContent.Name)); /// /// Gets an auto-approval rule that approves all skill tools, including the script execution tool @@ -138,8 +138,8 @@ public sealed partial class AgentSkillsProvider : AIContextProvider, IDisposable /// human approval boundary. Ensure no other tool collides with these reserved names. /// /// - public static Func> AllToolsAutoApprovalRule { get; } = - functionCall => new ValueTask(s_allToolNames.Contains(functionCall.Name)); + public static Func> AllToolsAutoApprovalRule { get; } = + context => new ValueTask(s_allToolNames.Contains(context.FunctionCallContent.Name)); /// /// Placeholder token for the generated skills list in the prompt template. diff --git a/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentTests.cs index 2b9613d0610..3aee394dc21 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentTests.cs @@ -683,7 +683,7 @@ public async Task ToolApproval_AutoApprovalRulesAreAppliedAsync() options.DisableToolAutoApproval = false; options.ToolApprovalAgentOptions = new ToolApprovalAgentOptions { - AutoApprovalRules = [fcc => new ValueTask(fcc.Name == "ReadTool")] + AutoApprovalRules = [context => new ValueTask(context.FunctionCallContent.Name == "ReadTool")] }; var agent = new HarnessAgent(mockClient.Object, options); diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentSkillsProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentSkillsProviderTests.cs index ed79909f9fc..df64c81adab 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentSkillsProviderTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/AgentSkills/AgentSkillsProviderTests.cs @@ -20,6 +20,9 @@ public sealed class AgentSkillsProviderTests : IDisposable private readonly string _testRoot; private readonly TestAIAgent _agent = new(); + private static ToolAutoApprovalRuleContext CreateRuleContext(FunctionCallContent functionCall) => + new(functionCall, new TestAIAgent(), session: null, requestMessages: [], agentRunOptions: null); + public AgentSkillsProviderTests() { this._testRoot = Path.Combine(Path.GetTempPath(), "skills-provider-tests-" + Guid.NewGuid().ToString("N")); @@ -1155,12 +1158,12 @@ public async Task ReadOnlyToolsAutoApprovalRule_ApprovesReadOnlyToolsAsync() var unrelatedCall = new FunctionCallContent("call4", "some_other_tool", null); // Act & Assert — read-only tools should be approved - Assert.True(await AgentSkillsProvider.ReadOnlyToolsAutoApprovalRule(loadSkillCall)); - Assert.True(await AgentSkillsProvider.ReadOnlyToolsAutoApprovalRule(readResourceCall)); + Assert.True(await AgentSkillsProvider.ReadOnlyToolsAutoApprovalRule(CreateRuleContext(loadSkillCall))); + Assert.True(await AgentSkillsProvider.ReadOnlyToolsAutoApprovalRule(CreateRuleContext(readResourceCall))); // Act & Assert — script tool and unrelated tools should not be approved - Assert.False(await AgentSkillsProvider.ReadOnlyToolsAutoApprovalRule(runScriptCall)); - Assert.False(await AgentSkillsProvider.ReadOnlyToolsAutoApprovalRule(unrelatedCall)); + Assert.False(await AgentSkillsProvider.ReadOnlyToolsAutoApprovalRule(CreateRuleContext(runScriptCall))); + Assert.False(await AgentSkillsProvider.ReadOnlyToolsAutoApprovalRule(CreateRuleContext(unrelatedCall))); } [Fact] @@ -1173,12 +1176,12 @@ public async Task AllToolsAutoApprovalRule_ApprovesAllSkillToolsAsync() var unrelatedCall = new FunctionCallContent("call4", "some_other_tool", null); // Act & Assert — all skill tools should be approved - Assert.True(await AgentSkillsProvider.AllToolsAutoApprovalRule(loadSkillCall)); - Assert.True(await AgentSkillsProvider.AllToolsAutoApprovalRule(readResourceCall)); - Assert.True(await AgentSkillsProvider.AllToolsAutoApprovalRule(runScriptCall)); + Assert.True(await AgentSkillsProvider.AllToolsAutoApprovalRule(CreateRuleContext(loadSkillCall))); + Assert.True(await AgentSkillsProvider.AllToolsAutoApprovalRule(CreateRuleContext(readResourceCall))); + Assert.True(await AgentSkillsProvider.AllToolsAutoApprovalRule(CreateRuleContext(runScriptCall))); // Act & Assert — unrelated tools should not be approved - Assert.False(await AgentSkillsProvider.AllToolsAutoApprovalRule(unrelatedCall)); + Assert.False(await AgentSkillsProvider.AllToolsAutoApprovalRule(CreateRuleContext(unrelatedCall))); } [Fact] diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/FileAccess/FileAccessProviderTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/FileAccess/FileAccessProviderTests.cs index a79e2f8882f..e8797a4bc9b 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/FileAccess/FileAccessProviderTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/FileAccess/FileAccessProviderTests.cs @@ -12,6 +12,9 @@ namespace Microsoft.Agents.AI.UnitTests.Harness.FileAccess; public class FileAccessProviderTests { + private static ToolAutoApprovalRuleContext CreateRuleContext(FunctionCallContent functionCall) => + new(functionCall, new Mock().Object, session: null, requestMessages: [], agentRunOptions: null); + #region Constructor Validation [Fact] @@ -127,7 +130,7 @@ public async Task ReadOnlyToolsAutoApprovalRule_ApprovesOnlyReadOnlyToolsAsync(s var functionCall = new FunctionCallContent("call1", toolName); // Act - bool approved = await FileAccessProvider.ReadOnlyToolsAutoApprovalRule(functionCall); + bool approved = await FileAccessProvider.ReadOnlyToolsAutoApprovalRule(CreateRuleContext(functionCall)); // Assert Assert.Equal(expected, approved); @@ -148,7 +151,7 @@ public async Task AllToolsAutoApprovalRule_ApprovesAllFileAccessToolsAsync(strin var functionCall = new FunctionCallContent("call1", toolName); // Act - bool approved = await FileAccessProvider.AllToolsAutoApprovalRule(functionCall); + bool approved = await FileAccessProvider.AllToolsAutoApprovalRule(CreateRuleContext(functionCall)); // Assert Assert.Equal(expected, approved); diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/ToolApproval/ToolApprovalAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/ToolApproval/ToolApprovalAgentTests.cs index e5721ac299a..719819e5be0 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/ToolApproval/ToolApprovalAgentTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/ToolApproval/ToolApprovalAgentTests.cs @@ -18,6 +18,9 @@ namespace Microsoft.Agents.AI.UnitTests; /// public class ToolApprovalAgentTests { + private static ToolAutoApprovalRuleContext CreateRuleContext(FunctionCallContent functionCall) => + new(functionCall, new Mock().Object, session: null, requestMessages: [], agentRunOptions: null); + #region Constructor /// @@ -1690,7 +1693,7 @@ public async Task AllToolsAutoApprovalRule_ApprovesAnyToolAsync(string toolName) var functionCall = new FunctionCallContent("call1", toolName); // Act - bool approved = await ToolApprovalAgent.AllToolsAutoApprovalRule(functionCall); + bool approved = await ToolApprovalAgent.AllToolsAutoApprovalRule(CreateRuleContext(functionCall)); // Assert Assert.True(approved); @@ -1778,7 +1781,7 @@ public async Task RunAsync_AutoApprovalRule_ApprovesMatchingToolAsync() var options = new ToolApprovalAgentOptions { - AutoApprovalRules = [fcc => new ValueTask(fcc.Name == "ReadTool")] + AutoApprovalRules = [context => new ValueTask(context.FunctionCallContent.Name == "ReadTool")] }; var agent = new ToolApprovalAgent(innerAgent.Object, options); @@ -1806,7 +1809,7 @@ public async Task RunAsync_AutoApprovalRule_DoesNotMatchSurfacesToCallerAsync() var options = new ToolApprovalAgentOptions { - AutoApprovalRules = [fcc => new ValueTask(fcc.Name == "ReadTool")] // Only approves ReadTool + AutoApprovalRules = [context => new ValueTask(context.FunctionCallContent.Name == "ReadTool")] // Only approves ReadTool }; var agent = new ToolApprovalAgent(innerAgent.Object, options); @@ -1857,8 +1860,8 @@ public async Task RunAsync_MultipleAutoApprovalRules_FirstMatchWinsAsync() { AutoApprovalRules = [ - fcc => { rule1Called = true; return new ValueTask(fcc.Name == "SpecialTool"); }, - fcc => { rule2Called = true; return new ValueTask(true); } // Should not be reached + context => { rule1Called = true; return new ValueTask(context.FunctionCallContent.Name == "SpecialTool"); }, + context => { rule2Called = true; return new ValueTask(true); } // Should not be reached ] }; var agent = new ToolApprovalAgent(innerAgent.Object, options); @@ -1904,7 +1907,7 @@ public async Task RunAsync_StandingRuleTakesPrecedenceOverAutoApprovalRuleAsync( var heuristicCalled = false; var options = new ToolApprovalAgentOptions { - AutoApprovalRules = [fcc => { heuristicCalled = true; return new ValueTask(true); }] + AutoApprovalRules = [context => { heuristicCalled = true; return new ValueTask(true); }] }; var agent = new ToolApprovalAgent(innerAgent.Object, options); @@ -1969,7 +1972,7 @@ public async Task RunAsync_MixedAutoApprovals_PreserveOriginalOrderAsync() var options = new ToolApprovalAgentOptions { - AutoApprovalRules = [fcc => new ValueTask(fcc.Name == "HeuristicTool")] + AutoApprovalRules = [context => new ValueTask(context.FunctionCallContent.Name == "HeuristicTool")] }; var agent = new ToolApprovalAgent(innerAgent.Object, options); @@ -2031,7 +2034,7 @@ public async Task RunStreamingAsync_AutoApprovalRule_ApprovesMatchingToolAsync() var options = new ToolApprovalAgentOptions { - AutoApprovalRules = [fcc => new ValueTask(fcc.Name == "ReadTool")] + AutoApprovalRules = [context => new ValueTask(context.FunctionCallContent.Name == "ReadTool")] }; var agent = new ToolApprovalAgent(innerAgent.Object, options); From 984dc36280a1383ea60cc4cd61f6a5bf3b03e822 Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Tue, 14 Jul 2026 10:51:27 +0000 Subject: [PATCH 2/2] Address PR comments --- .../ToolApproval/ToolApprovalAgentTests.cs | 43 +++++++++++++++++-- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/ToolApproval/ToolApprovalAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/ToolApproval/ToolApprovalAgentTests.cs index 719819e5be0..b09ade3625d 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/ToolApproval/ToolApprovalAgentTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/Harness/ToolApproval/ToolApprovalAgentTests.cs @@ -1779,20 +1779,38 @@ public async Task RunAsync_AutoApprovalRule_ApprovesMatchingToolAsync() return new AgentResponse([new ChatMessage(ChatRole.Assistant, "Done")]); }); + ToolAutoApprovalRuleContext? capturedContext = null; + var runOptions = new AgentRunOptions(); var options = new ToolApprovalAgentOptions { - AutoApprovalRules = [context => new ValueTask(context.FunctionCallContent.Name == "ReadTool")] + AutoApprovalRules = + [ + context => + { + capturedContext = context; + return new ValueTask(context.FunctionCallContent.Name == "ReadTool"); + } + ] }; var agent = new ToolApprovalAgent(innerAgent.Object, options); // Act var response = await agent.RunAsync( [new ChatMessage(ChatRole.User, "Hi")], - session); + session, + runOptions); // Assert — the approval request was auto-approved, inner agent called twice Assert.Equal(2, callCount); Assert.Equal("Done", response.Text); + + // Assert — the auto-approval rule received a fully populated context (non-streaming path). + Assert.NotNull(capturedContext); + Assert.Same(agent, capturedContext!.Agent); + Assert.Same(session, capturedContext.Session); + Assert.Same(runOptions, capturedContext.RunOptions); + Assert.Equal("ReadTool", capturedContext.FunctionCallContent.Name); + Assert.Contains(capturedContext.RequestMessages, m => m.Text == "Hi"); } /// @@ -2032,15 +2050,24 @@ public async Task RunStreamingAsync_AutoApprovalRule_ApprovesMatchingToolAsync() return ToAsyncEnumerableAsync([new AgentResponseUpdate(ChatRole.Assistant, "Done")]); }); + ToolAutoApprovalRuleContext? capturedContext = null; + var runOptions = new AgentRunOptions(); var options = new ToolApprovalAgentOptions { - AutoApprovalRules = [context => new ValueTask(context.FunctionCallContent.Name == "ReadTool")] + AutoApprovalRules = + [ + context => + { + capturedContext = context; + return new ValueTask(context.FunctionCallContent.Name == "ReadTool"); + } + ] }; var agent = new ToolApprovalAgent(innerAgent.Object, options); // Act var updates = new List(); - await foreach (var update in agent.RunStreamingAsync([new ChatMessage(ChatRole.User, "Hi")], session)) + await foreach (var update in agent.RunStreamingAsync([new ChatMessage(ChatRole.User, "Hi")], session, runOptions)) { updates.Add(update); } @@ -2049,6 +2076,14 @@ public async Task RunStreamingAsync_AutoApprovalRule_ApprovesMatchingToolAsync() Assert.Equal(2, callCount); Assert.Single(updates); Assert.Equal("Done", updates[0].Text); + + // Assert — the auto-approval rule received a fully populated context (streaming path). + Assert.NotNull(capturedContext); + Assert.Same(agent, capturedContext!.Agent); + Assert.Same(session, capturedContext.Session); + Assert.Same(runOptions, capturedContext.RunOptions); + Assert.Equal("ReadTool", capturedContext.FunctionCallContent.Name); + Assert.Contains(capturedContext.RequestMessages, m => m.Text == "Hi"); } #endregion