diff --git a/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step03_ScalingCapabilities/Program.cs b/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step03_ScalingCapabilities/Program.cs index 54c05c49b0b..9b971b11a30 100644 --- a/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step03_ScalingCapabilities/Program.cs +++ b/dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step03_ScalingCapabilities/Program.cs @@ -160,7 +160,9 @@ research before making decisions. // Turn the chat client into a HarnessAgent. On top of Post 2's file access and approvals we add the // four "scaling" capabilities: skills (our own provider), background agents, a confined shell, and // CodeAct. -List contextProviders = [skillsProvider, codeAct]; +// The shell is wired up in two parts: the ShellEnvironmentProvider injects OS/shell/CWD info into the +// system prompt, and the shell tool is registered below in ChatOptions. +List contextProviders = [skillsProvider, codeAct, new ShellEnvironmentProvider(shell)]; AIAgent agent = chatClient.AsHarnessAgent(new HarnessAgentOptions { @@ -170,8 +172,6 @@ research before making decisions. DisableAgentSkillsProvider = true, // Fan-out research is delegated to this background agent. BackgroundAgents = [researchAgent], - // The confined shell, exposed as the approval-gated run_shell tool. - ShellExecutor = shell, // Keep reading the portfolio frictionless while writes, trades, and shell commands still prompt. ToolApprovalAgentOptions = new ToolApprovalAgentOptions { @@ -179,7 +179,7 @@ research before making decisions. }, // Start in "execute" mode for quick lookups and actions; switch any time with /mode plan. AgentModeProviderOptions = new AgentModeProviderOptions { DefaultMode = "execute" }, - // Our skills provider plus CodeAct. + // Our skills provider, CodeAct, and the shell environment provider. AIContextProviders = contextProviders, ChatOptions = new ChatOptions { @@ -188,6 +188,8 @@ research before making decisions. [ StockTools.CreateGetStockPriceTool(), TradingTools.CreatePlaceTradeTool(), + // The confined shell, exposed as the approval-gated run_shell tool. + shell.AsAIFunction(requireApproval: true), ], Reasoning = new() { Effort = ReasoningEffort.Medium }, }, diff --git a/dotnet/src/Microsoft.Agents.AI.Harness/ChatClientHarnessExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Harness/ChatClientHarnessExtensions.cs index 3d5f037b2f8..38315f5e54c 100644 --- a/dotnet/src/Microsoft.Agents.AI.Harness/ChatClientHarnessExtensions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Harness/ChatClientHarnessExtensions.cs @@ -1,17 +1,14 @@ // Copyright (c) Microsoft. All rights reserved. using System; -using System.Diagnostics.CodeAnalysis; using Microsoft.Agents.AI; using Microsoft.Extensions.Logging; -using Microsoft.Shared.DiagnosticIds; namespace Microsoft.Extensions.AI; /// /// Provides extension methods for creating a from an . /// -[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] public static class ChatClientHarnessExtensions { /// diff --git a/dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgent.cs b/dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgent.cs index 87fa7d47b59..c7a64a9e3d7 100644 --- a/dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgent.cs @@ -2,16 +2,11 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using Microsoft.Agents.AI.Compaction; -#if NET -using Microsoft.Agents.AI.Tools.Shell; -#endif using Microsoft.Extensions.AI; using Microsoft.Extensions.Logging; -using Microsoft.Shared.DiagnosticIds; using Microsoft.Shared.Diagnostics; namespace Microsoft.Agents.AI; @@ -51,7 +46,6 @@ namespace Microsoft.Agents.AI; /// /// — shared file access providing read/write tools for a working directory. Enable by setting ; configure via . /// — enables delegation to background agents for parallel work. Enable by setting . -/// ShellEnvironmentProvider — injects OS/shell/CWD information and a shell execution tool. Enable by setting HarnessAgentOptions.ShellExecutor (.NET only). /// /// /// @@ -80,7 +74,6 @@ namespace Microsoft.Agents.AI; /// and combined with agent-specific instructions via . /// /// -[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] public sealed class HarnessAgent : DelegatingAIAgent { /// @@ -279,16 +272,6 @@ private static ChatOptions BuildChatOptions(HarnessAgentOptions? options, string result.Tools.Add(new HostedWebSearchTool()); } -#if NET - if (options?.ShellExecutor is ShellExecutor shellExecutor) - { - result.Tools ??= []; - result.Tools.Add(options.ShellToolName is { } shellToolName - ? shellExecutor.AsAIFunction(shellToolName, options.ShellToolDescription, !options.DisableShellToolApproval) - : shellExecutor.AsAIFunction(description: options.ShellToolDescription, requireApproval: !options.DisableShellToolApproval)); - } -#endif - return result; } @@ -343,13 +326,6 @@ private static List BuildContextProviders(HarnessAgentOptions } } -#if NET - if (options?.ShellExecutor is ShellExecutor shellExecutor) - { - providers.Add(new ShellEnvironmentProvider(shellExecutor, options.ShellEnvironmentProviderOptions)); - } -#endif - if (options?.AIContextProviders is IEnumerable userProviders) { providers.AddRange(userProviders); diff --git a/dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgentOptions.cs b/dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgentOptions.cs index be61687088a..b0c94130b9d 100644 --- a/dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgentOptions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgentOptions.cs @@ -3,9 +3,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using Microsoft.Agents.AI.Compaction; -#if NET -using Microsoft.Agents.AI.Tools.Shell; -#endif using Microsoft.Extensions.AI; using Microsoft.Shared.DiagnosticIds; @@ -14,7 +11,6 @@ namespace Microsoft.Agents.AI; /// /// Represents configuration options for a . /// -[Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] public sealed class HarnessAgentOptions { /// @@ -46,6 +42,7 @@ public sealed class HarnessAgentOptions /// . /// /// + [Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] public int? MaxContextWindowTokens { get; set; } /// @@ -62,6 +59,7 @@ public sealed class HarnessAgentOptions /// is provided and is . /// /// + [Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] public int? MaxOutputTokens { get; set; } /// @@ -81,6 +79,7 @@ public sealed class HarnessAgentOptions /// This property is ignored when is . /// /// + [Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] public CompactionStrategy? CompactionStrategy { get; set; } /// @@ -92,6 +91,7 @@ public sealed class HarnessAgentOptions /// is added to the chat client pipeline, and the default /// is configured without a chat reducer. /// + [Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] public bool DisableCompaction { get; set; } /// @@ -162,6 +162,7 @@ public sealed class HarnessAgentOptions /// as a single-shot agent. /// /// + [Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] public IEnumerable? LoopEvaluators { get; set; } /// @@ -171,6 +172,7 @@ public sealed class HarnessAgentOptions /// When , the uses its default settings. This property is ignored /// when is or empty. /// + [Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] public LoopAgentOptions? LoopAgentOptions { get; set; } /// @@ -234,6 +236,7 @@ public sealed class HarnessAgentOptions /// a default is created. /// This property is ignored when is . /// + [Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] public AgentFileStore? FileMemoryStore { get; set; } /// @@ -245,6 +248,7 @@ public sealed class HarnessAgentOptions /// included in the agent's context providers, backed by the supplied store and configured with /// when provided. /// + [Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] public AgentFileStore? FileAccessStore { get; set; } /// @@ -254,6 +258,7 @@ public sealed class HarnessAgentOptions /// This property is only used when is set (file access is opt-in). /// When , the provider uses its default options. /// + [Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] public FileAccessProviderOptions? FileAccessProviderOptions { get; set; } /// @@ -348,6 +353,7 @@ public sealed class HarnessAgentOptions /// (case-insensitive). If these requirements are not met, will throw /// an during construction. /// + [Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] public IEnumerable? BackgroundAgents { get; set; } /// @@ -357,76 +363,6 @@ public sealed class HarnessAgentOptions /// Use this to customize instructions or agent list formatting for the background agents feature. /// This property is ignored when is or empty. /// + [Experimental(DiagnosticIds.Experiments.AgentsAIExperiments)] public BackgroundAgentsProviderOptions? BackgroundAgentsProviderOptions { get; set; } - -#if NET - /// - /// Gets or sets the shell executor used to enable shell tool and environment probing via . - /// - /// - /// When non-null, a is automatically included in the agent's context - /// providers (injecting OS/shell/CWD information into the system prompt), and the executor's - /// is registered as a callable tool. - /// When (the default), no shell features are enabled. - /// - public ShellExecutor? ShellExecutor { get; set; } - - /// - /// Gets or sets the name of the shell execution tool exposed to the model. - /// - /// - /// - /// When (the default), the shell executor's default tool name (run_shell) is used. - /// This property is ignored when is . - /// - /// - /// Security warning: auto-approval rules may match tool calls solely by name. Pay attention to - /// the tool names approved by auto-approval rules for other features. Setting this property to a - /// value that collides with a tool name that is approved by an auto-approval rule for another feature will cause - /// the shell tool to also be auto-approved, bypassing the human approval boundary. Choose a unique - /// name that no other registered tool uses. - /// - /// - public string? ShellToolName { get; set; } - - /// - /// Gets or sets the description of the shell execution tool shown to the model. - /// - /// - /// When (the default), the shell executor's built-in description is used. - /// This property is ignored when is . - /// - public string? ShellToolDescription { get; set; } - - /// - /// Gets or sets a value indicating whether approval is disabled for the shell execution tool. - /// - /// - /// - /// When (the default), the shell tool is wrapped in an - /// so every command requires explicit approval before executing. When , the tool can be invoked - /// without approval. This property is ignored when is . - /// - /// - /// Setting this to also requires the underlying to permit - /// unapproved use. The inverse of this value is forwarded as the requireApproval argument to - /// , and some executors enforce their own security boundary: - /// throws an unless it was - /// constructed with set to , - /// because running unapproved commands directly on the host is inherently unsafe. Sandboxed executors such as - /// impose no such requirement. - /// - /// - public bool DisableShellToolApproval { get; set; } - - /// - /// Gets or sets optional configuration for the . - /// - /// - /// Use this to customize which tools are probed, the probe timeout, shell family override, - /// or the instructions formatter. - /// This property is ignored when is . - /// - public ShellEnvironmentProviderOptions? ShellEnvironmentProviderOptions { get; set; } -#endif } diff --git a/dotnet/src/Microsoft.Agents.AI.Harness/Microsoft.Agents.AI.Harness.csproj b/dotnet/src/Microsoft.Agents.AI.Harness/Microsoft.Agents.AI.Harness.csproj index 31e24d43245..e429447e894 100644 --- a/dotnet/src/Microsoft.Agents.AI.Harness/Microsoft.Agents.AI.Harness.csproj +++ b/dotnet/src/Microsoft.Agents.AI.Harness/Microsoft.Agents.AI.Harness.csproj @@ -1,24 +1,27 @@ - false + true true true true true true + $(NoWarn);MAAI001 + + + + false + + - - - - Microsoft Agent Framework Harness diff --git a/dotnet/src/Microsoft.Agents.AI/Harness/FileAccess/FileAccessProvider.cs b/dotnet/src/Microsoft.Agents.AI/Harness/FileAccess/FileAccessProvider.cs index 95594333aee..d7262d4bc40 100644 --- a/dotnet/src/Microsoft.Agents.AI/Harness/FileAccess/FileAccessProvider.cs +++ b/dotnet/src/Microsoft.Agents.AI/Harness/FileAccess/FileAccessProvider.cs @@ -184,8 +184,8 @@ public FileAccessProvider(AgentFileStore fileStore, FileAccessProviderOptions? o /// /// /// Security note: because matching is by tool name only, any other registered tool that - /// shares one of these names — for example a configurable-name tool such as the Harness shell - /// tool (HarnessAgentOptions.ShellToolName) that was assigned the same name — will also be auto-approved, bypassing the + /// shares one of these names — for example a configurable-name tool that was assigned the same + /// name — will also be auto-approved, bypassing the /// human approval boundary. Ensure no other tool collides with these reserved names. /// /// @@ -221,8 +221,8 @@ public FileAccessProvider(AgentFileStore fileStore, FileAccessProviderOptions? o /// /// /// Security note: because matching is by tool name only, any other registered tool that - /// shares one of these names — for example a configurable-name tool such as the Harness shell - /// tool (HarnessAgentOptions.ShellToolName) that was assigned the same name — will also be auto-approved, bypassing the + /// shares one of these names — for example a configurable-name tool that was assigned the same + /// name — will also be auto-approved, bypassing the /// human approval boundary. Ensure no other tool collides with these reserved names. /// /// diff --git a/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillsProvider.cs b/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillsProvider.cs index 09922b52f3a..1d33227cc8a 100644 --- a/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillsProvider.cs +++ b/dotnet/src/Microsoft.Agents.AI/Skills/AgentSkillsProvider.cs @@ -100,8 +100,8 @@ public sealed partial class AgentSkillsProvider : AIContextProvider, IDisposable /// /// /// Security note: because matching is by tool name only, any other registered tool that - /// shares one of these names — for example a configurable-name tool such as the Harness shell - /// tool (HarnessAgentOptions.ShellToolName) that was assigned the same name — will also be auto-approved, bypassing the + /// shares one of these names — for example a configurable-name tool that was assigned the same + /// name — will also be auto-approved, bypassing the /// human approval boundary. Ensure no other tool collides with these reserved names. /// /// @@ -133,8 +133,8 @@ public sealed partial class AgentSkillsProvider : AIContextProvider, IDisposable /// /// /// Security note: because matching is by tool name only, any other registered tool that - /// shares one of these names — for example a configurable-name tool such as the Harness shell - /// tool (HarnessAgentOptions.ShellToolName) that was assigned the same name — will also be auto-approved, bypassing the + /// shares one of these names — for example a configurable-name tool that was assigned the same + /// name — will also be auto-approved, bypassing the /// human approval boundary. Ensure no other tool collides with these reserved names. /// /// diff --git a/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentOptionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentOptionsTests.cs index dc54a92f0c3..fdb047a88c5 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentOptionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentOptionsTests.cs @@ -2,9 +2,6 @@ using System.Threading.Tasks; using Moq; -#if NET -using Microsoft.Agents.AI.Tools.Shell; -#endif namespace Microsoft.Agents.AI.UnitTests; @@ -46,10 +43,6 @@ public void DefaultPropertyValues() Assert.Null(options.AgentSkillsSource); Assert.Null(options.BackgroundAgents); Assert.Null(options.BackgroundAgentsProviderOptions); -#if NET - Assert.Null(options.ShellExecutor); - Assert.Null(options.ShellEnvironmentProviderOptions); -#endif } /// @@ -70,10 +63,6 @@ public void PropertiesCanBeSetAndRetrieved() var backgroundAgentsOptions = new BackgroundAgentsProviderOptions(); var loopEvaluators = new LoopEvaluator[] { new DelegateLoopEvaluator((_, _) => new ValueTask(LoopEvaluation.Stop())) }; var loopAgentOptions = new LoopAgentOptions(); -#if NET - var shellExecutor = new Mock().Object; - var shellEnvOptions = new ShellEnvironmentProviderOptions(); -#endif // Act var options = new HarnessAgentOptions @@ -104,10 +93,6 @@ public void PropertiesCanBeSetAndRetrieved() BackgroundAgentsProviderOptions = backgroundAgentsOptions, LoopEvaluators = loopEvaluators, LoopAgentOptions = loopAgentOptions, -#if NET - ShellExecutor = shellExecutor, - ShellEnvironmentProviderOptions = shellEnvOptions, -#endif }; // Assert @@ -139,9 +124,5 @@ public void PropertiesCanBeSetAndRetrieved() Assert.Same(backgroundAgentsOptions, options.BackgroundAgentsProviderOptions); Assert.Same(loopEvaluators, options.LoopEvaluators); Assert.Same(loopAgentOptions, options.LoopAgentOptions); -#if NET - Assert.Same(shellExecutor, options.ShellExecutor); - Assert.Same(shellEnvOptions, options.ShellEnvironmentProviderOptions); -#endif } } diff --git a/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentTests.cs index 2b9613d0610..2952510a2ce 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentTests.cs @@ -7,9 +7,6 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -#if NET -using Microsoft.Agents.AI.Tools.Shell; -#endif using Microsoft.Extensions.AI; using Microsoft.Extensions.Logging; using Moq; @@ -1618,235 +1615,6 @@ public async Task BackgroundAgentsProvider_IncludesMultipleAgentsAsync() #endregion -#if NET - #region Feature: ShellEnvironmentProvider - - /// - /// Verify that ShellEnvironmentProvider is included when ShellExecutor is provided. - /// - [Fact] - public void ShellEnvironmentProvider_IncludedWhenExecutorProvided() - { - // Arrange - var chatClient = new Mock().Object; - var executorMock = new Mock(); - executorMock.Setup(e => e.AsAIFunction(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(AIFunctionFactory.Create(() => "test", "run_shell")); - var options = CreateAllDisabledOptions(); - options.ShellExecutor = executorMock.Object; - - // Act - var agent = new HarnessAgent(chatClient, options); - var innerAgent = agent.GetService(); - - // Assert - Assert.NotNull(innerAgent?.AIContextProviders); - Assert.Contains(innerAgent!.AIContextProviders!, p => p is ShellEnvironmentProvider); - } - - /// - /// Verify that ShellEnvironmentProvider is not included when ShellExecutor is null. - /// - [Fact] - public void ShellEnvironmentProvider_ExcludedWhenExecutorNull() - { - // Arrange - var chatClient = new Mock().Object; - var options = CreateAllDisabledOptions(); - options.ShellExecutor = null; - - // Act - var agent = new HarnessAgent(chatClient, options); - var innerAgent = agent.GetService(); - - // Assert - Assert.NotNull(innerAgent); - Assert.NotNull(innerAgent!.AIContextProviders); - Assert.DoesNotContain(innerAgent.AIContextProviders!, p => p is ShellEnvironmentProvider); - } - - /// - /// Verify that the shell tool AIFunction is added to ChatOptions.Tools when ShellExecutor is provided. - /// - [Fact] - public async Task ShellExecutor_ToolAddedToChatOptionsAsync() - { - // Arrange - ChatOptions? capturedOptions = null; - var chatClientMock = new Mock(); - chatClientMock - .Setup(c => c.GetResponseAsync(It.IsAny>(), It.IsAny(), It.IsAny())) - .Callback, ChatOptions?, CancellationToken>((_, opts, _) => capturedOptions = opts) - .ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "done"))); - - var executorMock = new Mock(); - executorMock.Setup(e => e.AsAIFunction(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(AIFunctionFactory.Create(() => "shell output", "run_shell")); - - var options = CreateAllDisabledOptions(); - options.DisableWebSearch = true; - options.ShellExecutor = executorMock.Object; - - // Act - var agent = new HarnessAgent(chatClientMock.Object, options); - var session = await agent.CreateSessionAsync(); - await agent.RunAsync([new ChatMessage(ChatRole.User, "Hi")], session); - - // Assert — the shell tool should be present - Assert.NotNull(capturedOptions?.Tools); - Assert.Contains(capturedOptions!.Tools!, t => t is AIFunction f && f.Name == "run_shell"); - } - - /// - /// Verify that a custom shell tool name, description, and approval flag are forwarded to the executor. - /// - [Fact] - public async Task ShellExecutor_CustomToolNameDescriptionAndApprovalForwardedAsync() - { - // Arrange - ChatOptions? capturedOptions = null; - var chatClientMock = new Mock(); - chatClientMock - .Setup(c => c.GetResponseAsync(It.IsAny>(), It.IsAny(), It.IsAny())) - .Callback, ChatOptions?, CancellationToken>((_, opts, _) => capturedOptions = opts) - .ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "done"))); - - string? capturedName = null; - string? capturedDescription = null; - bool? capturedRequireApproval = null; - var executorMock = new Mock(); - executorMock.Setup(e => e.AsAIFunction(It.IsAny(), It.IsAny(), It.IsAny())) - .Callback((name, description, requireApproval) => - { - capturedName = name; - capturedDescription = description; - capturedRequireApproval = requireApproval; - }) - .Returns(AIFunctionFactory.Create(() => "shell output", "custom_shell")); - - var options = CreateAllDisabledOptions(); - options.DisableWebSearch = true; - options.ShellExecutor = executorMock.Object; - options.ShellToolName = "custom_shell"; - options.ShellToolDescription = "Run a custom command."; - options.DisableShellToolApproval = true; - - // Act - var agent = new HarnessAgent(chatClientMock.Object, options); - var session = await agent.CreateSessionAsync(); - await agent.RunAsync([new ChatMessage(ChatRole.User, "Hi")], session); - - // Assert — the configured values are passed through to the executor and the tool is registered. - Assert.Equal("custom_shell", capturedName); - Assert.Equal("Run a custom command.", capturedDescription); - Assert.False(capturedRequireApproval); - Assert.NotNull(capturedOptions?.Tools); - Assert.Contains(capturedOptions!.Tools!, t => t is AIFunction f && f.Name == "custom_shell"); - } - - /// - /// Verify that the shell tool defaults to requiring approval and the executor's default name when not configured. - /// - [Fact] - public async Task ShellExecutor_DefaultsToApprovalAndDefaultNameAsync() - { - // Arrange - var chatClientMock = new Mock(); - chatClientMock - .Setup(c => c.GetResponseAsync(It.IsAny>(), It.IsAny(), It.IsAny())) - .ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "done"))); - - bool? capturedRequireApproval = null; - string? capturedName = null; - var executorMock = new Mock(); - executorMock.Setup(e => e.AsAIFunction(It.IsAny(), It.IsAny(), It.IsAny())) - .Callback((name, _, requireApproval) => - { - capturedName = name; - capturedRequireApproval = requireApproval; - }) - .Returns(AIFunctionFactory.Create(() => "shell output", "run_shell")); - - var options = CreateAllDisabledOptions(); - options.DisableWebSearch = true; - options.ShellExecutor = executorMock.Object; - - // Act - var agent = new HarnessAgent(chatClientMock.Object, options); - var session = await agent.CreateSessionAsync(); - await agent.RunAsync([new ChatMessage(ChatRole.User, "Hi")], session); - - // Assert — approval is required by default and the executor's default name is used. - Assert.True(capturedRequireApproval); - Assert.Equal("run_shell", capturedName); - } - - /// - /// Verify that disabling shell approval is honored end-to-end when the underlying executor permits unapproved use: - /// a real constructed with - /// set to plus set to - /// yields a shell tool that is not wrapped in an . - /// - [Fact] - public async Task ShellExecutor_ApprovalDisabledWithAcknowledgedExecutorProducesNonApprovalToolAsync() - { - // Arrange - ChatOptions? capturedOptions = null; - var chatClientMock = new Mock(); - chatClientMock - .Setup(c => c.GetResponseAsync(It.IsAny>(), It.IsAny(), It.IsAny())) - .Callback, ChatOptions?, CancellationToken>((_, opts, _) => capturedOptions = opts) - .ReturnsAsync(new ChatResponse(new ChatMessage(ChatRole.Assistant, "done"))); - - await using var executor = new LocalShellExecutor(new LocalShellExecutorOptions { AcknowledgeUnsafe = true }); - - var options = CreateAllDisabledOptions(); - options.DisableWebSearch = true; - options.ShellExecutor = executor; - options.DisableShellToolApproval = true; - - // Act - var agent = new HarnessAgent(chatClientMock.Object, options); - var session = await agent.CreateSessionAsync(); - await agent.RunAsync([new ChatMessage(ChatRole.User, "Hi")], session); - - // Assert — the shell tool is registered but not gated by approval. - Assert.NotNull(capturedOptions?.Tools); - var shellTool = Assert.Single(capturedOptions!.Tools!, t => t is AIFunction f && f.Name == "run_shell"); - Assert.IsNotType(shellTool); - } - - /// - /// Verify that ShellEnvironmentProvider is present when ShellEnvironmentProviderOptions is also specified. - /// - [Fact] - public void ShellEnvironmentProvider_PresentWhenOptionsProvided() - { - // Arrange - var chatClient = new Mock().Object; - var executorMock = new Mock(); - executorMock.Setup(e => e.AsAIFunction(It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(AIFunctionFactory.Create(() => "test", "run_shell")); - var envOptions = new ShellEnvironmentProviderOptions - { - ProbeTools = ["git", "python"], - }; - var options = CreateAllDisabledOptions(); - options.ShellExecutor = executorMock.Object; - options.ShellEnvironmentProviderOptions = envOptions; - - // Act - var agent = new HarnessAgent(chatClient, options); - var innerAgent = agent.GetService(); - - // Assert — provider should exist (options wiring is validated by the provider's behavior) - Assert.NotNull(innerAgent?.AIContextProviders); - Assert.Contains(innerAgent!.AIContextProviders!, p => p is ShellEnvironmentProvider); - } - - #endregion -#endif - #region LoggerFactory and ServiceProvider ///