From eb91ee35bbd9d7762d807ef21b8b0e42b0247115 Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Fri, 18 Sep 2026 14:43:51 +0000 Subject: [PATCH 1/2] Pass ChatClientAgent tools per run only rather than setting on FICC --- .../ChatClient/ChatClientExtensions.cs | 13 +--------- .../ChatClient/ChatClientExtensionsTests.cs | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientExtensions.cs b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientExtensions.cs index fcca066848e..699b98c2e02 100644 --- a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientExtensions.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using Microsoft.Agents.AI; using Microsoft.Extensions.AI; using Microsoft.Extensions.DependencyInjection; @@ -134,16 +133,6 @@ internal static IChatClient WithDefaultAgentMiddleware(this IChatClient chatClie // so FICC emits execute_tool spans on the agent source. chatBuilder.Use(innerClient => new DeferredOpenTelemetryChatClient(innerClient)); - var agentChatClient = chatBuilder.Build(services); - - if (options?.ChatOptions?.Tools is { Count: > 0 }) - { - // When tools are provided in the constructor, set the tools for the whole lifecycle of the chat client - var functionService = agentChatClient.GetService(); - Debug.Assert(functionService is not null, "FunctionInvokingChatClient should be registered in the chat client."); - functionService!.AdditionalTools = options.ChatOptions.Tools; - } - - return agentChatClient; + return chatBuilder.Build(services); } } diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientExtensionsTests.cs index 04937c23e8e..f6fc856e0b8 100644 --- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/ChatClientExtensionsTests.cs @@ -109,6 +109,32 @@ public void CreateAIAgent_WithExistingFunctionInvokingChatClient_ConfiguresConcu Assert.True(functionInvokingClient.AllowConcurrentInvocation); } + [Fact] + public void CreateAIAgent_SharedChatClient_DoesNotLeakToolsBetweenAgents() + { + // Arrange: a single pre-decorated IChatClient shared by two independently-constructed agents, + // one "privileged" and one "public", each with their own distinct tool. + var chatClientMock = new Mock(); + var sharedChatClient = chatClientMock.Object.AsBuilder().UseFunctionInvocation().Build(); + + AITool publicTool = AIFunctionFactory.Create(() => "public", name: "public_read"); + AITool privilegedTool = AIFunctionFactory.Create(() => "privileged", name: "privileged_write"); + + // Act: construct the low-privilege agent first, then the privileged agent on the same shared client. + var publicAgent = sharedChatClient.AsAIAgent(tools: [publicTool]); + var privilegedAgent = sharedChatClient.AsAIAgent(tools: [privilegedTool]); + + // Assert: neither agent mutated the shared FunctionInvokingChatClient's AdditionalTools, so + // constructing the privileged agent cannot overwrite/leak tools into the public agent's execution scope. + var functionInvokingClient = sharedChatClient.GetService(); + Assert.NotNull(functionInvokingClient); + Assert.True(functionInvokingClient.AdditionalTools is null or { Count: 0 }); + + // Each agent's own configured tools remain scoped to itself. + Assert.Equal([publicTool], publicAgent.ChatOptions!.Tools); + Assert.Equal([privilegedTool], privilegedAgent.ChatOptions!.Tools); + } + [Fact] public void CreateAIAgent_WithNullClient_Throws() { From c4ba7fe565c72c3e9657ce09fca582b67bd1a7b8 Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Fri, 18 Sep 2026 14:57:19 +0000 Subject: [PATCH 2/2] Fix test errors --- .../AnthropicBetaServiceExtensionsTests.cs | 22 +++++++++---------- .../AnthropicClientExtensionsTests.cs | 22 +++++++++---------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicBetaServiceExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicBetaServiceExtensionsTests.cs index 45d027fc261..2e49e588cd1 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicBetaServiceExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicBetaServiceExtensionsTests.cs @@ -210,11 +210,11 @@ public void CreateAIAgent_WithTools_AssignsToolsCorrectly() // When tools are provided, ChatOptions is created but instructions remain null Assert.Null(agent.Instructions); - // Verify that tools are registered in the FunctionInvokingChatClient - var functionInvokingClient = agent.GetService(); - Assert.NotNull(functionInvokingClient); - Assert.NotNull(functionInvokingClient.AdditionalTools); - Assert.Contains(functionInvokingClient.AdditionalTools, t => t is AIFunction func && func.Name == "TestFunction"); + // Verify that tools are registered on the agent's ChatOptions, which are merged into the + // invocation-scoped ChatOptions on every call (see ChatClientAgent.CreateConfiguredChatOptions). + var agentTools = agent.GetService()?.Tools; + Assert.NotNull(agentTools); + Assert.Contains(agentTools, t => t is AIFunction func && func.Name == "TestFunction"); } /// @@ -310,11 +310,11 @@ public void CreateAIAgent_WithToolsAndInstructions_AssignsBothCorrectly() Assert.Equal("Test Agent", agent.Name); Assert.Equal("Test instructions", agent.Instructions); - // Verify that tools are registered in the FunctionInvokingChatClient - var functionInvokingClient = agent.GetService(); - Assert.NotNull(functionInvokingClient); - Assert.NotNull(functionInvokingClient.AdditionalTools); - Assert.Contains(functionInvokingClient.AdditionalTools, t => t is AIFunction func && func.Name == "TestFunction"); + // Verify that tools are registered on the agent's ChatOptions, which are merged into the + // invocation-scoped ChatOptions on every call (see ChatClientAgent.CreateConfiguredChatOptions). + var agentTools = agent.GetService()?.Tools; + Assert.NotNull(agentTools); + Assert.Contains(agentTools, t => t is AIFunction func && func.Name == "TestFunction"); } /// @@ -339,7 +339,7 @@ public void CreateAIAgent_WithEmptyTools_DoesNotAssignTools() // With empty tools and no instructions, agent instructions remain null Assert.Null(agent.Instructions); - // Verify that FunctionInvokingChatClient has no additional tools assigned + // Verify that the FunctionInvokingChatClient is not used as a fallback tool store. var functionInvokingClient = agent.GetService(); Assert.NotNull(functionInvokingClient); Assert.True(functionInvokingClient.AdditionalTools is null or { Count: 0 }); diff --git a/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicClientExtensionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicClientExtensionsTests.cs index abbbe6111fa..876d71936e2 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicClientExtensionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Anthropic.UnitTests/Extensions/AnthropicClientExtensionsTests.cs @@ -284,11 +284,11 @@ public void CreateAIAgent_WithTools_AssignsToolsCorrectly() // When tools are provided, ChatOptions is created but instructions remain null Assert.Null(agent.Instructions); - // Verify that tools are registered in the FunctionInvokingChatClient - var functionInvokingClient = agent.GetService(); - Assert.NotNull(functionInvokingClient); - Assert.NotNull(functionInvokingClient.AdditionalTools); - Assert.Contains(functionInvokingClient.AdditionalTools, t => t is AIFunction func && func.Name == "TestFunction"); + // Verify that tools are registered on the agent's ChatOptions, which are merged into the + // invocation-scoped ChatOptions on every call (see ChatClientAgent.CreateConfiguredChatOptions). + var agentTools = agent.GetService()?.Tools; + Assert.NotNull(agentTools); + Assert.Contains(agentTools, t => t is AIFunction func && func.Name == "TestFunction"); } /// @@ -384,11 +384,11 @@ public void CreateAIAgent_WithToolsAndInstructions_AssignsBothCorrectly() Assert.Equal("Test Agent", agent.Name); Assert.Equal("Test instructions", agent.Instructions); - // Verify that tools are registered in the FunctionInvokingChatClient - var functionInvokingClient = agent.GetService(); - Assert.NotNull(functionInvokingClient); - Assert.NotNull(functionInvokingClient.AdditionalTools); - Assert.Contains(functionInvokingClient.AdditionalTools, t => t is AIFunction func && func.Name == "TestFunction"); + // Verify that tools are registered on the agent's ChatOptions, which are merged into the + // invocation-scoped ChatOptions on every call (see ChatClientAgent.CreateConfiguredChatOptions). + var agentTools = agent.GetService()?.Tools; + Assert.NotNull(agentTools); + Assert.Contains(agentTools, t => t is AIFunction func && func.Name == "TestFunction"); } /// @@ -413,7 +413,7 @@ public void CreateAIAgent_WithEmptyTools_DoesNotAssignTools() // With empty tools and no instructions, agent instructions remain null Assert.Null(agent.Instructions); - // Verify that FunctionInvokingChatClient has no additional tools assigned + // Verify that the FunctionInvokingChatClient is not used as a fallback tool store. var functionInvokingClient = agent.GetService(); Assert.NotNull(functionInvokingClient); Assert.True(functionInvokingClient.AdditionalTools is null or { Count: 0 });