From 3813820d32a50f799aef304b97a9c7ac9164ac94 Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Mon, 13 Jul 2026 14:56:53 +0000 Subject: [PATCH 1/2] Switch FileAcessProvider on Harness to opt-in --- .../Harness_Step01_Research/Program.cs | 1 - .../Program.cs | 2 - .../Harness_Step03_DataProcessing/Program.cs | 6 +-- .../Harness_Step03_DataProcessing/README.md | 4 +- .../Harness_Step04_CodeExecution/Program.cs | 7 +++- .../Harness/Harness_Step05_Loop/Program.cs | 1 - .../HarnessAgent.cs | 10 ++--- .../HarnessAgentOptions.cs | 20 +++++----- .../HarnessAgentOptionsTests.cs | 7 ++-- .../HarnessAgentTests.cs | 37 +++++++++++++------ 10 files changed, 53 insertions(+), 42 deletions(-) diff --git a/dotnet/samples/02-agents/Harness/Harness_Step01_Research/Program.cs b/dotnet/samples/02-agents/Harness/Harness_Step01_Research/Program.cs index f2fcfa52daf..fda733d635d 100644 --- a/dotnet/samples/02-agents/Harness/Harness_Step01_Research/Program.cs +++ b/dotnet/samples/02-agents/Harness/Harness_Step01_Research/Program.cs @@ -85,7 +85,6 @@ Track your sources — you will need them when presenting results. MaxOutputTokens = MaxOutputTokens, Name = "ResearchAgent", Description = "A research assistant that plans and executes research tasks.", - DisableFileAccess = true, // If enabled, this would allow the agent to read/write files in a working directory OpenTelemetrySourceName = TracingSourceName, // Use our custom source name so spans are captured by the TracerProvider above. FileMemoryStore = new FileSystemAgentFileStore( // Configure the file memory provider to store files in a local folder called "agent-files". Path.Combine(AppContext.BaseDirectory, "agent-files")), diff --git a/dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithBackgroundAgents/Program.cs b/dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithBackgroundAgents/Program.cs index aa2ad5c7d12..daaa61a9a19 100644 --- a/dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithBackgroundAgents/Program.cs +++ b/dotnet/samples/02-agents/Harness/Harness_Step02_Research_WithBackgroundAgents/Program.cs @@ -57,7 +57,6 @@ DisableTodoProvider = true, DisableAgentModeProvider = true, DisableFileMemory = true, // If enabled, this would allow the agent to store memories as files in a directory associated with the current session - DisableFileAccess = true, // If enabled, this would allow the agent to read/write files in a working directory DisableToolAutoApproval = true, // If true, this disables the don't-ask-again approval functionality. ChatOptions = new ChatOptions { @@ -107,7 +106,6 @@ 5. Clear all completed tasks to free memory. DisableTodoProvider = true, DisableAgentModeProvider = true, DisableFileMemory = true, // If enabled, this would allow the agent to store memories as files in a directory associated with the current session - DisableFileAccess = true, // If enabled, this would allow the agent to read/write files in a working directory DisableToolAutoApproval = true, // If true, this disables the don't-ask-again approval functionality. DisableWebSearch = true, BackgroundAgents = [webSearchAgent], diff --git a/dotnet/samples/02-agents/Harness/Harness_Step03_DataProcessing/Program.cs b/dotnet/samples/02-agents/Harness/Harness_Step03_DataProcessing/Program.cs index 00f50c17b37..78a41fd3ed4 100644 --- a/dotnet/samples/02-agents/Harness/Harness_Step03_DataProcessing/Program.cs +++ b/dotnet/samples/02-agents/Harness/Harness_Step03_DataProcessing/Program.cs @@ -1,12 +1,12 @@ // Copyright (c) Microsoft. All rights reserved. -// This sample demonstrates how to use a HarnessAgent with the default FileAccessProvider +// This sample demonstrates how to use a HarnessAgent with the FileAccessProvider // to give an agent access to a folder of CSV data files. The agent can read, analyze, // and extract information from the data, then write results back as new files. // // The sample includes a pre-populated `working/` folder with sales transaction data. -// The HarnessAgent's default FileAccessProvider uses `{cwd}/working` as its working directory, -// which matches this sample's folder layout. +// File access is opt-in: setting HarnessAgentOptions.FileAccessStore enables the +// FileAccessProvider, and this sample points it at the `working/` folder below the location of the executable. // Ask the agent to analyze the data, produce summaries, or create new output files. // // Special commands: diff --git a/dotnet/samples/02-agents/Harness/Harness_Step03_DataProcessing/README.md b/dotnet/samples/02-agents/Harness/Harness_Step03_DataProcessing/README.md index d06348fa728..b8ca80b188c 100644 --- a/dotnet/samples/02-agents/Harness/Harness_Step03_DataProcessing/README.md +++ b/dotnet/samples/02-agents/Harness/Harness_Step03_DataProcessing/README.md @@ -1,11 +1,11 @@ # What this sample demonstrates -This sample demonstrates how to use a `HarnessAgent` with the default `FileAccessProvider` to give an agent access to a folder of data files for reading, analyzing, and writing results. The `HarnessAgent` pre-configures function invocation, per-service-call chat history persistence, in-loop compaction, tool approval, and OpenTelemetry — so the sample only needs to supply the chat client, token limits, custom instructions, and opt out of unused features. +This sample demonstrates how to use a `HarnessAgent` with the `FileAccessProvider` to give an agent access to a folder of data files for reading, analyzing, and writing results. The `HarnessAgent` pre-configures function invocation, per-service-call chat history persistence, in-loop compaction, tool approval, and OpenTelemetry — so the sample only needs to supply the chat client, token limits, custom instructions, a `FileAccessStore`, and opt out of unused features. Key features showcased: - **HarnessAgent** — a pre-configured agent that wraps a `ChatClientAgent` with function invocation, per-service-call persistence, and context-window compaction -- **FileAccessProvider** — the HarnessAgent's default file access provider uses `{cwd}/working` as its working directory, matching this sample's `working/` folder +- **FileAccessProvider** — file access is opt-in; setting `HarnessAgentOptions.FileAccessStore` to the sample's `working/` folder enables the provider's read/write tools - **CSV data processing** — the agent reads sales transaction data and performs analysis on demand - **Output file creation** — the agent can write summaries, filtered data, or reports back to the data folder - **Streaming output** — responses are streamed token-by-token for a natural experience diff --git a/dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/Program.cs b/dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/Program.cs index 46f4c32c81e..b074089d198 100644 --- a/dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/Program.cs +++ b/dotnet/samples/02-agents/Harness/Harness_Step04_CodeExecution/Program.cs @@ -82,8 +82,9 @@ to solve problems precisely rather than guessing. You also have access to skills // In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid // latency issues, unintended credential probing, and potential security risks from fallback mechanisms. // Create the agent with ALL HarnessAgent features enabled plus Hyperlight CodeAct. -// No Disable* flags are set — TodoProvider, AgentModeProvider, FileMemory, FileAccess, -// ToolApproval, WebSearch, and AgentSkillsProvider are all active. +// TodoProvider, AgentModeProvider, FileMemory, ToolApproval, WebSearch, and +// AgentSkillsProvider are on by default. File access is opt-in, so it is enabled here by +// supplying a FileAccessStore. AIAgent agent = new AIProjectClient( new Uri(endpoint), @@ -101,6 +102,8 @@ to solve problems precisely rather than guessing. You also have access to skills OpenTelemetrySourceName = TracingSourceName, // Point the file memory at a local folder for persistent memory across sessions. FileMemoryStore = new FileSystemAgentFileStore(Path.Combine(AppContext.BaseDirectory, "agent-files")), + // Enable file access (opt-in) by rooting the file access tools at a local working folder. + FileAccessStore = new FileSystemAgentFileStore(Path.Combine(AppContext.BaseDirectory, "working")), // Add the HyperlightCodeActProvider so the agent can execute Python code in a sandbox. AIContextProviders = [codeAct], ChatOptions = new ChatOptions 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 5236302a451..98b18172fad 100644 --- a/dotnet/samples/02-agents/Harness/Harness_Step05_Loop/Program.cs +++ b/dotnet/samples/02-agents/Harness/Harness_Step05_Loop/Program.cs @@ -252,7 +252,6 @@ AIAgent CreateLeanHarnessAgent( DisableAgentModeProvider = true, DisableTodoProvider = disableTodoProvider, DisableFileMemory = true, - DisableFileAccess = true, DisableWebSearch = true, ToolApprovalAgentOptions = toolApprovalAgentOptions, ChatOptions = new ChatOptions diff --git a/dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgent.cs b/dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgent.cs index 9bb00ce88ca..87fa7d47b59 100644 --- a/dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgent.cs +++ b/dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgent.cs @@ -43,13 +43,13 @@ namespace Microsoft.Agents.AI; /// — persistent todo list that the agent uses to track multi-step plans. Disable with . /// — mode tracking (e.g., "plan" vs "execute") that the agent uses to structure its work. Disable with . /// — file-based session memory allowing the agent to persist notes and artifacts across turns. Disable with . -/// — shared file access providing read/write tools for a working directory. Disable with . /// — discovers and loads skill definitions from the file system, enabling dynamic tool sets. Disable with . /// /// /// /// Optional context providers (enabled via ): /// +/// — 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). /// @@ -320,13 +320,9 @@ private static List BuildContextProviders(HarnessAgentOptions })); } - if (options?.DisableFileAccess is not true) + if (options?.FileAccessStore is AgentFileStore fileAccessStore) { - AgentFileStore fileAccessStore = options?.FileAccessStore - ?? new FileSystemAgentFileStore( - Path.Combine(Directory.GetCurrentDirectory(), "working")); - - providers.Add(new FileAccessProvider(fileAccessStore)); + providers.Add(new FileAccessProvider(fileAccessStore, options.FileAccessProviderOptions)); } if (options?.DisableAgentSkillsProvider is not true) diff --git a/dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgentOptions.cs b/dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgentOptions.cs index 07bc054f733..a639188926a 100644 --- a/dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgentOptions.cs +++ b/dotnet/src/Microsoft.Agents.AI.Harness/HarnessAgentOptions.cs @@ -237,24 +237,24 @@ public sealed class HarnessAgentOptions public AgentFileStore? FileMemoryStore { get; set; } /// - /// Gets or sets a value indicating whether the is disabled. + /// Gets or sets the that enables the . /// /// - /// When (the default), a is included in the - /// agent's context providers, using either or a default - /// rooted at {cwd}/working. + /// File access is opt-in. When (the default), no + /// is added and the agent has no file access tools. When set, a is + /// included in the agent's context providers, backed by the supplied store and configured with + /// when provided. /// - public bool DisableFileAccess { get; set; } + public AgentFileStore? FileAccessStore { get; set; } /// - /// Gets or sets a custom for the . + /// Gets or sets the used to configure the . /// /// - /// When and is , - /// a default is created. - /// This property is ignored when is . + /// This property is only used when is set (file access is opt-in). + /// When , the provider uses its default options. /// - public AgentFileStore? FileAccessStore { get; set; } + public FileAccessProviderOptions? FileAccessProviderOptions { get; set; } /// /// Gets or sets a value indicating whether the is disabled. diff --git a/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentOptionsTests.cs b/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentOptionsTests.cs index 21e158138af..dc54a92f0c3 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentOptionsTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentOptionsTests.cs @@ -32,7 +32,6 @@ public void DefaultPropertyValues() Assert.False(options.DisableToolAutoApproval); Assert.False(options.DisableApprovalNotRequiredFunctionBypassing); Assert.False(options.DisableFileMemory); - Assert.False(options.DisableFileAccess); Assert.False(options.DisableWebSearch); Assert.False(options.DisableTodoProvider); Assert.False(options.DisableAgentModeProvider); @@ -42,6 +41,7 @@ public void DefaultPropertyValues() Assert.Null(options.MaximumIterationsPerRequest); Assert.Null(options.FileMemoryStore); Assert.Null(options.FileAccessStore); + Assert.Null(options.FileAccessProviderOptions); Assert.Null(options.AgentModeProviderOptions); Assert.Null(options.AgentSkillsSource); Assert.Null(options.BackgroundAgents); @@ -63,6 +63,7 @@ public void PropertiesCanBeSetAndRetrieved() var contextProviders = new AIContextProvider[] { new TodoProvider() }; var fileMemoryStore = new Mock().Object; var fileAccessStore = new Mock().Object; + var fileAccessOptions = new FileAccessProviderOptions(); var agentModeOptions = new AgentModeProviderOptions(); var skillsSource = new Mock().Object; var backgroundAgents = new AIAgent[] { new Mock().Object }; @@ -89,8 +90,8 @@ public void PropertiesCanBeSetAndRetrieved() DisableApprovalNotRequiredFunctionBypassing = true, DisableFileMemory = true, FileMemoryStore = fileMemoryStore, - DisableFileAccess = true, FileAccessStore = fileAccessStore, + FileAccessProviderOptions = fileAccessOptions, DisableWebSearch = true, DisableTodoProvider = true, DisableAgentModeProvider = true, @@ -124,8 +125,8 @@ public void PropertiesCanBeSetAndRetrieved() Assert.True(options.DisableApprovalNotRequiredFunctionBypassing); Assert.True(options.DisableFileMemory); Assert.Same(fileMemoryStore, options.FileMemoryStore); - Assert.True(options.DisableFileAccess); Assert.Same(fileAccessStore, options.FileAccessStore); + Assert.Same(fileAccessOptions, options.FileAccessProviderOptions); Assert.True(options.DisableWebSearch); Assert.True(options.DisableTodoProvider); Assert.True(options.DisableAgentModeProvider); diff --git a/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentTests.cs index cf64bfb711c..8c13024db08 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentTests.cs @@ -32,7 +32,6 @@ public class HarnessAgentTests DisableToolAutoApproval = true, DisableOpenTelemetry = true, DisableFileMemory = true, - DisableFileAccess = true, DisableWebSearch = true, DisableTodoProvider = true, DisableAgentModeProvider = true, @@ -1212,15 +1211,15 @@ public void FileMemoryProvider_UsesCustomStore() #region Feature: FileAccessProvider /// - /// Verify that FileAccessProvider is included in AIContextProviders by default. + /// Verify that FileAccessProvider is included in AIContextProviders when a FileAccessStore is provided. /// [Fact] - public void FileAccessProvider_IncludedByDefault() + public void FileAccessProvider_IncludedWhenStoreProvided() { // Arrange var chatClient = new Mock().Object; var options = CreateAllDisabledOptions(); - options.DisableFileAccess = false; + options.FileAccessStore = new Mock().Object; // Act var agent = new HarnessAgent(chatClient, options); @@ -1232,10 +1231,10 @@ public void FileAccessProvider_IncludedByDefault() } /// - /// Verify that FileAccessProvider is excluded when disabled. + /// Verify that FileAccessProvider is excluded by default (opt-in: no store provided). /// [Fact] - public void FileAccessProvider_ExcludedWhenDisabled() + public void FileAccessProvider_ExcludedByDefault() { // Arrange var chatClient = new Mock().Object; @@ -1262,7 +1261,6 @@ public void FileAccessProvider_UsesCustomStore() var chatClient = new Mock().Object; var customStore = new Mock().Object; var options = CreateAllDisabledOptions(); - options.DisableFileAccess = false; options.FileAccessStore = customStore; // Act @@ -1274,6 +1272,27 @@ public void FileAccessProvider_UsesCustomStore() Assert.Contains(innerAgent!.AIContextProviders!, p => p is FileAccessProvider); } + /// + /// Verify that FileAccessProviderOptions are honored when a FileAccessStore is provided. + /// + [Fact] + public void FileAccessProvider_UsesProvidedOptions() + { + // Arrange + var chatClient = new Mock().Object; + var options = CreateAllDisabledOptions(); + options.FileAccessStore = new Mock().Object; + options.FileAccessProviderOptions = new FileAccessProviderOptions { DisableWriteTools = true }; + + // Act + var agent = new HarnessAgent(chatClient, options); + var innerAgent = agent.GetService(); + + // Assert — FileAccessProvider should be present when options are supplied alongside a store. + Assert.NotNull(innerAgent?.AIContextProviders); + Assert.Contains(innerAgent!.AIContextProviders!, p => p is FileAccessProvider); + } + #endregion #region Feature: AgentSkillsProvider @@ -1419,7 +1438,6 @@ public async Task AllDefaults_AllFeaturesEnabledAsync() Assert.Contains(providers, p => p is TodoProvider); Assert.Contains(providers, p => p is AgentModeProvider); Assert.Contains(providers, p => p is FileMemoryProvider); - Assert.Contains(providers, p => p is FileAccessProvider); Assert.Contains(providers, p => p is AgentSkillsProvider); // Assert — HostedWebSearchTool is present in the tools sent to the model @@ -1903,7 +1921,6 @@ public void Constructor_LoggerFactoryIsUsedByDownstreamComponents() DisableToolAutoApproval = true, DisableOpenTelemetry = true, DisableFileMemory = true, - DisableFileAccess = true, DisableWebSearch = true, DisableTodoProvider = true, DisableAgentModeProvider = true, @@ -1954,7 +1971,6 @@ public void Constructor_SucceedsWithoutTokenValues() DisableToolAutoApproval = true, DisableOpenTelemetry = true, DisableFileMemory = true, - DisableFileAccess = true, DisableWebSearch = true, DisableTodoProvider = true, DisableAgentModeProvider = true, @@ -1986,7 +2002,6 @@ public void Constructor_SucceedsWithOnlyMaxContextWindowTokens() DisableToolAutoApproval = true, DisableOpenTelemetry = true, DisableFileMemory = true, - DisableFileAccess = true, DisableWebSearch = true, DisableTodoProvider = true, DisableAgentModeProvider = true, From 5f010d52799f7bbd6ec66ae39927eb830c95f6ef Mon Sep 17 00:00:00 2001 From: westey <164392973+westey-m@users.noreply.github.com> Date: Mon, 13 Jul 2026 15:35:02 +0000 Subject: [PATCH 2/2] Address PR comment --- .../HarnessAgentTests.cs | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentTests.cs b/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentTests.cs index 8c13024db08..2b9613d0610 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentTests.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Harness.UnitTests/HarnessAgentTests.cs @@ -1273,24 +1273,43 @@ public void FileAccessProvider_UsesCustomStore() } /// - /// Verify that FileAccessProviderOptions are honored when a FileAccessStore is provided. + /// Verify that FileAccessProviderOptions are honored: setting DisableWriteTools must remove the + /// write tools from the provider that HarnessAgent wires up, while the read-only tools remain. /// [Fact] - public void FileAccessProvider_UsesProvidedOptions() + public async Task FileAccessProvider_UsesProvidedOptionsAsync() { // Arrange var chatClient = new Mock().Object; var options = CreateAllDisabledOptions(); - options.FileAccessStore = new Mock().Object; + options.FileAccessStore = new InMemoryAgentFileStore(); options.FileAccessProviderOptions = new FileAccessProviderOptions { DisableWriteTools = true }; // Act var agent = new HarnessAgent(chatClient, options); var innerAgent = agent.GetService(); - // Assert — FileAccessProvider should be present when options are supplied alongside a store. + // Assert — the FileAccessProvider is present and honors the supplied options. Assert.NotNull(innerAgent?.AIContextProviders); - Assert.Contains(innerAgent!.AIContextProviders!, p => p is FileAccessProvider); + var fileAccessProvider = Assert.IsType( + Assert.Single(innerAgent!.AIContextProviders!, p => p is FileAccessProvider)); + + var mockAgent = new Mock().Object; + var session = await agent.CreateSessionAsync(); +#pragma warning disable MAAI001 + var context = new AIContextProvider.InvokingContext(mockAgent, session, new AIContext()); +#pragma warning restore MAAI001 + AIContext result = await fileAccessProvider.InvokingAsync(context); + var toolNames = result.Tools!.OfType().Select(t => t.Name).ToList(); + + // DisableWriteTools = true => only the read-only tools are exposed. + Assert.Contains(FileAccessProvider.ReadFileToolName, toolNames); + Assert.Contains(FileAccessProvider.LsToolName, toolNames); + Assert.Contains(FileAccessProvider.GrepToolName, toolNames); + Assert.DoesNotContain(FileAccessProvider.WriteToolName, toolNames); + Assert.DoesNotContain(FileAccessProvider.DeleteFileToolName, toolNames); + Assert.DoesNotContain(FileAccessProvider.ReplaceToolName, toolNames); + Assert.DoesNotContain(FileAccessProvider.ReplaceLinesToolName, toolNames); } #endregion