From 2476fa6158ee50c37611e929d4deb2ce5091df22 Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Wed, 23 Sep 2026 19:08:45 +0100
Subject: [PATCH 1/5] .NET: Allow Harness local chat history marker when stored
output is disabled
Foundry hosting rejected HarnessAgent requests with
agent_stored_output_not_disabled because CheckNotAllowedStoreUsage treated
the local history sentinel (PerServiceCallChatHistoryPersistingChatClient.
LocalHistoryConversationId) as a service-stored conversation ID. The
sentinel only means history is persisted locally; the host still sends
store=false. Real service conversation IDs are still rejected.
- Exempt the local history sentinel in CheckNotAllowedStoreUsage.
- Grant Hosting access to Microsoft.Agents.AI internals and use its shared
Throw and DiagnosticIds helpers instead of injecting copies, removing the
CS0436 conflict and the stale NU1903 suppression.
- Add unit tests for the allowed sentinel, a rejected service conversation
ID, and store=false on the default host.
- Add Hosted-Harness-Research, Hosted-Harness-DataProcessing, and
Hosted-Harness-ScalingCapabilities samples that run without
AllowStoredOutputEnabled.
Fixes #8707
---
dotnet/agent-framework-dotnet.slnx | 9 +
dotnet/samples/02-agents/Harness/README.md | 10 ++
.../.env.example | 4 +
.../Hosted-Harness-DataProcessing/.gitignore | 3 +
.../Hosted-Harness-DataProcessing/Dockerfile | 6 +
.../HostedHarnessDataProcessing.csproj | 26 +++
.../Hosted-Harness-DataProcessing/Program.cs | 89 +++++++++
.../Hosted-Harness-DataProcessing/README.md | 26 +++
.../Hosted-Harness-Research/.env.example | 4 +
.../Hosted-Harness-Research/.gitignore | 4 +
.../Hosted-Harness-Research/Dockerfile | 6 +
.../HostedHarnessResearch.csproj | 30 ++++
.../Hosted-Harness-Research/Program.cs | 75 ++++++++
.../Hosted-Harness-Research/README.md | 56 ++++++
.../StatelessWebSearchChatClient.cs | 48 +++++
.../.env.example | 7 +
.../.gitignore | 3 +
.../Dockerfile | 7 +
.../HostedHarnessScalingCapabilities.csproj | 44 +++++
.../Program.cs | 170 ++++++++++++++++++
.../README.md | 28 +++
.../AgentFrameworkResponseHandler.cs | 12 +-
.../FoundryResponsesOptions.cs | 4 +
...Microsoft.Agents.AI.Foundry.Hosting.csproj | 5 +-
.../ChatClient/ChatClientAgentSession.cs | 5 +
.../Microsoft.Agents.AI.csproj | 1 +
.../AgentFrameworkResponseHandlerTests.cs | 106 +++++++++++
...Agents.AI.Foundry.Hosting.UnitTests.csproj | 5 +
.../StatelessWebSearchChatClientTests.cs | 107 +++++++++++
29 files changed, 893 insertions(+), 7 deletions(-)
create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/.env.example
create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/.gitignore
create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/Dockerfile
create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/HostedHarnessDataProcessing.csproj
create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/Program.cs
create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/README.md
create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/.env.example
create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/.gitignore
create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/Dockerfile
create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/HostedHarnessResearch.csproj
create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/Program.cs
create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/README.md
create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/StatelessWebSearchChatClient.cs
create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/.env.example
create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/.gitignore
create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/Dockerfile
create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/HostedHarnessScalingCapabilities.csproj
create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/Program.cs
create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/README.md
create mode 100644 dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/StatelessWebSearchChatClientTests.cs
diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index 77e772cf214..9d2b34729b6 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -342,6 +342,15 @@
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/02-agents/Harness/README.md b/dotnet/samples/02-agents/Harness/README.md
index 9417ba42952..df65f177a69 100644
--- a/dotnet/samples/02-agents/Harness/README.md
+++ b/dotnet/samples/02-agents/Harness/README.md
@@ -22,6 +22,16 @@ Samples accompanying the [*Build your own agent harness or claw with Microsoft A
| [Claw_Step03_ScalingCapabilities](./BuildYourOwnClaw/Claw_Step03_ScalingCapabilities/README.md) | Post 3 — scaling the claw with skills (plus optional Foundry skills), a confined shell, CodeAct, and background agents |
| [Claw_Step04_ProductionReady](./BuildYourOwnClaw/Claw_Step04_ProductionReady/README.md) | Post 4 — production-ready shared claw library with observability, opt-in Purview governance, Foundry hosted deployment, and evals |
+## Foundry hosted Harness samples
+
+Samples that host a `HarnessAgent` as a Foundry hosted agent with the default `AllowStoredOutputEnabled = false`, so the model service never stores the conversation.
+
+| Sample | Description |
+| --- | --- |
+| [Hosted-Harness-Research](../../04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/README.md) | Hosts the research harness with web search, a browsing tool, planning, a todo loop, and session-scoped file memory |
+| [Hosted-Harness-DataProcessing](../../04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/README.md) | Hosts the data processing harness that reads a sales CSV and writes reports, with approval-gated file writes that resume on a later turn |
+| [Hosted-Harness-ScalingCapabilities](../../04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/README.md) | Hosts the Claw_Step03 finance harness with skills, a confined approval-gated shell, a background research agent, and simulated trade approvals |
+
## Security Considerations
Several harness providers extend the agent's trust boundary to external systems the developer
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/.env.example b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/.env.example
new file mode 100644
index 00000000000..f2ab64ba92e
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/.env.example
@@ -0,0 +1,4 @@
+FOUNDRY_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/
+AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-5.4-mini
+AZURE_TOKEN_CREDENTIALS=dev
+ASPNETCORE_URLS=http://localhost:8088
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/.gitignore b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/.gitignore
new file mode 100644
index 00000000000..d3fe066dacc
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/.gitignore
@@ -0,0 +1,3 @@
+.env
+out/
+.checkpoints/
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/Dockerfile
new file mode 100644
index 00000000000..5d4f48aa163
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/Dockerfile
@@ -0,0 +1,6 @@
+FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine
+WORKDIR /app
+COPY out/ .
+EXPOSE 8088
+ENV ASPNETCORE_URLS=http://+:8088
+ENTRYPOINT ["dotnet", "HostedHarnessDataProcessing.dll"]
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/HostedHarnessDataProcessing.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/HostedHarnessDataProcessing.csproj
new file mode 100644
index 00000000000..3f29cb95afa
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/HostedHarnessDataProcessing.csproj
@@ -0,0 +1,26 @@
+
+
+
+ net10.0
+ enable
+ $(NoWarn);MAAI001;OPENAI001
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/Program.cs
new file mode 100644
index 00000000000..f31d74c3103
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/Program.cs
@@ -0,0 +1,89 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// A hosted data-processing harness with approval-gated file writes and read-only auto-approval.
+
+using System.ClientModel.Primitives;
+using Azure.AI.Projects;
+using Azure.Identity;
+using DotNetEnv;
+using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.Foundry.Hosting;
+using Microsoft.Extensions.AI;
+
+Env.NoClobber().TraversePath().Load();
+
+string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
+ ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
+string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME")
+ ?? throw new InvalidOperationException("AZURE_AI_MODEL_DEPLOYMENT_NAME is not set.");
+
+string seedDir = Path.Combine(AppContext.BaseDirectory, "working");
+string workingDir;
+if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("FOUNDRY_HOSTING_ENVIRONMENT")))
+{
+ // Preserve files edited in prior turns; only seed files that do not yet exist in the
+ // per-session writable home. The application directory is read-only in Foundry.
+ string home = System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile);
+ Directory.SetCurrentDirectory(home);
+ workingDir = Path.Combine(home, "working");
+ foreach (string file in Directory.GetFiles(seedDir, "*", SearchOption.AllDirectories))
+ {
+ string destination = Path.Combine(workingDir, Path.GetRelativePath(seedDir, file));
+ if (!File.Exists(destination))
+ {
+ Directory.CreateDirectory(Path.GetDirectoryName(destination)!);
+ File.Copy(file, destination);
+ }
+ }
+}
+else
+{
+ workingDir = seedDir;
+}
+
+const int MaxContextWindowTokens = 1_050_000;
+const int MaxOutputTokens = 128_000;
+
+IChatClient chatClient = new AIProjectClient(
+ new Uri(endpoint),
+ new DefaultAzureCredential(),
+ new AIProjectClientOptions { RetryPolicy = new ClientRetryPolicy(3) })
+ .GetProjectOpenAIClient()
+ .GetProjectResponsesClientForModel(deploymentName)
+ .AsIChatClient();
+
+AIAgent agent = chatClient.AsHarnessAgent(new HarnessAgentOptions
+{
+ Name = "hosted-harness-data-processing",
+ Description = "Reads and analyzes CSV files and writes reports with approval.",
+ MaxContextWindowTokens = MaxContextWindowTokens,
+ MaxOutputTokens = MaxOutputTokens,
+ FileAccessStore = new FileSystemAgentFileStore(workingDir),
+ ToolApprovalAgentOptions = new ToolApprovalAgentOptions
+ {
+ AutoApprovalRules = [FileAccessProvider.ReadOnlyToolsAutoApprovalRule],
+ },
+ DisableTodoProvider = true,
+ DisableAgentModeProvider = true,
+ DisableFileMemory = true,
+ DisableWebSearch = true,
+ ChatOptions = new ChatOptions
+ {
+ ModelId = deploymentName,
+ MaxOutputTokens = MaxOutputTokens,
+ Instructions =
+ """
+ You analyze data files in the working folder. List and read the files before answering
+ questions about their contents. Present calculations and findings clearly. When asked
+ for a report, write it with the file access tools, but never overwrite source data
+ unless the user explicitly asks you to.
+ """,
+ },
+});
+
+var builder = WebApplication.CreateBuilder(args);
+builder.Services.AddFoundryResponses(agent);
+
+var app = builder.Build();
+app.MapFoundryResponses();
+app.Run();
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/README.md
new file mode 100644
index 00000000000..a46327ffbbf
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/README.md
@@ -0,0 +1,26 @@
+# Hosted Harness Data Processing
+
+This sample hosts a `HarnessAgent` that reads a sample sales CSV and writes requested reports. It reuses `dotnet/samples/02-agents/Harness/Harness_Step03_DataProcessing/working/sales.csv`. File reads are auto-approved; writes still require an explicit approval that can resume on a later hosted turn.
+
+The project references the framework **source in this repository**. `AddFoundryResponses(agent)` uses the default `AllowStoredOutputEnabled = false`; the agent keeps chat history in its session rather than in the model service.
+
+## Run locally
+
+1. Set `FOUNDRY_PROJECT_ENDPOINT` and `AZURE_AI_MODEL_DEPLOYMENT_NAME` for an existing Foundry model deployment, then sign in with `az login`. Copy `.env.example` to `.env` if you prefer a local file.
+2. From this directory, run `dotnet run --tl:off`, then call `POST http://localhost:8088/responses` with model `hosted-harness-data-processing` and a question such as "Which region sold the most units in sales.csv?".
+3. Send another request with the same conversation ID. To exercise approval, ask for a written report. The response contains an `mcp_approval_request`; send an `mcp_approval_response` with its ID in the **same conversation and hosted session**. Do not auto-approve writes. `azd ai agent invoke -f` does not send a structured Responses body; use authenticated `az rest --resource https://ai.azure.com` for the approval response.
+
+Hosted containers have a read-only application directory. On startup the sample copies **only missing** seed files into the session's writable home directory so edited files and generated reports survive later turns.
+
+## Container build from this checkout
+
+The project links its CSV from elsewhere in the MAF checkout, so publish from the repository before building this runtime image. Uploading this directory alone as Foundry source/ZIP is not supported:
+
+```powershell
+dotnet publish HostedHarnessDataProcessing.csproj -c Release -f net10.0 -r linux-musl-x64 --self-contained false -o out --tl:off
+docker build -t hosted-harness-data-processing .
+```
+
+The image includes the local framework assemblies. Rebuild it after a framework change rather than testing an older published package.
+
+For deployment to an existing Foundry project, follow the [persistent `azd` container workflow](../Hosted-Harness-Research/README.md#deploy-with-an-existing-foundry-project) with this project's publish output, bundle directory `src\hosted-harness-data`, and agent name `maf-harness-data`. A live hosted validation read the bundled CSV, recalled the answer on a second turn, requested approval before writing, and read the approved file on a later turn. The host kept `AllowStoredOutputEnabled = false`.
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/.env.example b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/.env.example
new file mode 100644
index 00000000000..f2ab64ba92e
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/.env.example
@@ -0,0 +1,4 @@
+FOUNDRY_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/
+AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-5.4-mini
+AZURE_TOKEN_CREDENTIALS=dev
+ASPNETCORE_URLS=http://localhost:8088
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/.gitignore b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/.gitignore
new file mode 100644
index 00000000000..1f5eea24dcf
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/.gitignore
@@ -0,0 +1,4 @@
+.env
+out/
+agent-files/
+.checkpoints/
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/Dockerfile
new file mode 100644
index 00000000000..97041df053e
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/Dockerfile
@@ -0,0 +1,6 @@
+FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine
+WORKDIR /app
+COPY out/ .
+EXPOSE 8088
+ENV ASPNETCORE_URLS=http://+:8088
+ENTRYPOINT ["dotnet", "HostedHarnessResearch.dll"]
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/HostedHarnessResearch.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/HostedHarnessResearch.csproj
new file mode 100644
index 00000000000..b25103a4f99
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/HostedHarnessResearch.csproj
@@ -0,0 +1,30 @@
+
+
+
+ net10.0
+ enable
+ $(NoWarn);MAAI001;OPENAI001
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/Program.cs
new file mode 100644
index 00000000000..f7fd02845ac
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/Program.cs
@@ -0,0 +1,75 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// A hosted research harness: planning, browsing, file memory, and an execute-mode todo loop.
+// The browsing tool is shared with the existing console research sample.
+
+using System.ClientModel.Primitives;
+using Azure.AI.Projects;
+using Azure.Identity;
+using DotNetEnv;
+using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.Foundry.Hosting;
+using Microsoft.Extensions.AI;
+using SampleApp;
+
+Env.NoClobber().TraversePath().Load();
+
+string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
+ ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
+string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME")
+ ?? throw new InvalidOperationException("AZURE_AI_MODEL_DEPLOYMENT_NAME is not set.");
+
+// Foundry mounts the home directory per session; the application directory is read-only there.
+bool isHosted = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("FOUNDRY_HOSTING_ENVIRONMENT"));
+if (isHosted)
+{
+ Directory.SetCurrentDirectory(System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile));
+}
+
+string fileMemoryDir = isHosted
+ ? Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile), "agent-files")
+ : Path.Combine(AppContext.BaseDirectory, "agent-files");
+
+const int MaxContextWindowTokens = 1_050_000;
+const int MaxOutputTokens = 128_000;
+
+IChatClient modelClient = new AIProjectClient(
+ new Uri(endpoint),
+ new DefaultAzureCredential(),
+ new AIProjectClientOptions { RetryPolicy = new ClientRetryPolicy(3) })
+ .GetProjectOpenAIClient()
+ .GetProjectResponsesClientForModel(deploymentName)
+ .AsIChatClient();
+
+IChatClient chatClient = new StatelessWebSearchChatClient(modelClient);
+
+AIAgent agent = chatClient.AsHarnessAgent(new HarnessAgentOptions
+{
+ Name = "hosted-harness-research",
+ Description = "Plans and performs research using web search and browsing.",
+ MaxContextWindowTokens = MaxContextWindowTokens,
+ MaxOutputTokens = MaxOutputTokens,
+ FileMemoryStore = new FileSystemAgentFileStore(fileMemoryDir),
+ LoopEvaluators = [new TodoCompletionLoopEvaluator(new TodoCompletionLoopEvaluatorOptions { Modes = ["execute"] })],
+ LoopAgentOptions = new LoopAgentOptions { MaxIterations = 10 },
+ ChatOptions = new ChatOptions
+ {
+ ModelId = deploymentName,
+ Instructions =
+ """
+ You are a research assistant. Plan complex research using the todo and mode tools.
+ Verify important claims with web search and browsing, cross-check sources, and cite
+ the sources you used. Save the final report to file memory so it survives compaction.
+ """,
+ Tools = [new WebBrowsingTool(new WebBrowsingToolOptions { AllowPublicNetworks = true })],
+ MaxOutputTokens = MaxOutputTokens,
+ Reasoning = new() { Effort = ReasoningEffort.Medium },
+ },
+});
+
+var builder = WebApplication.CreateBuilder(args);
+builder.Services.AddFoundryResponses(agent);
+
+var app = builder.Build();
+app.MapFoundryResponses();
+app.Run();
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/README.md
new file mode 100644
index 00000000000..62fb8febd2b
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/README.md
@@ -0,0 +1,56 @@
+# Hosted Harness Research
+
+This sample hosts a research `HarnessAgent` with web search, a public-network browsing tool, planning, a bounded todo loop, and session-scoped file memory. It links the browsing tool from `dotnet/samples/02-agents/Harness/Harness_Step01_Research` instead of maintaining a second implementation.
+
+The project references the framework **source in this repository** so it exercises the local Foundry hosting fix. `AddFoundryResponses(agent)` keeps the default `AllowStoredOutputEnabled = false`. The model must not store a second copy of the response; the harness's local history marker is not a service conversation ID.
+
+## Run locally
+
+1. Set `FOUNDRY_PROJECT_ENDPOINT` and `AZURE_AI_MODEL_DEPLOYMENT_NAME` for an existing Foundry model deployment, then sign in with `az login`. Copy `.env.example` to `.env` if you prefer a local file.
+2. From this directory, run `dotnet run --tl:off`. The host serves `POST http://localhost:8088/responses` when `ASPNETCORE_URLS=http://localhost:8088`.
+3. Send a research question with model `hosted-harness-research`. Send a second request with the same conversation ID to verify that local history continues without enabling agent-side storage. The platform `store` setting is independent of the model client's `store=false` setting.
+
+Browsing allows public networks only. File memory goes under `agent-files` locally and the session's writable home directory when hosted.
+
+## Container build from this checkout
+
+The project links source code outside this directory and therefore **cannot** be uploaded alone for Foundry source/ZIP deployment. Publish from the MAF checkout first, then build the included runtime Dockerfile from this folder:
+
+```powershell
+dotnet publish HostedHarnessResearch.csproj -c Release -f net10.0 -r linux-musl-x64 --self-contained false -o out --tl:off
+docker build -t hosted-harness-research .
+```
+
+The resulting image includes the local framework assemblies. Rebuild the image after changes to the framework; do not substitute currently published packages for this validation.
+
+## Deploy with an existing Foundry project
+
+Use a persistent directory **outside the repository** for the `azd` environment. The project and its ContainerRegistry connection must already exist. This flow builds the image remotely through that connection, so the developer does not need direct registry login or a local `docker build`. From this sample directory, after publishing:
+
+```powershell
+$state = ''
+$projectId = ''
+$model = ''
+$connection = ''
+$agent = 'maf-harness-research'
+$bundle = Join-Path $state 'src\hosted-harness-research'
+
+New-Item -ItemType Directory -Path (Join-Path $bundle 'out') -Force | Out-Null
+Copy-Item Dockerfile (Join-Path $bundle 'Dockerfile') -Force
+Copy-Item -Path 'out\*' -Destination (Join-Path $bundle 'out') -Recurse -Force
+
+# First deployment only. Keep the generated azure.yaml and .azure directory for later deployments.
+azd ai agent init --no-prompt --kind hosted --deploy-mode container `
+ --src $bundle --agent-name $agent --protocol responses `
+ --project-id $projectId --model-deployment $model `
+ --acr-connection $connection -C $state
+azd deploy $agent -C $state --no-prompt
+azd ai agent invoke $agent 'Name an official weather-data source.' `
+ --new-session --new-conversation -C $state --no-prompt
+```
+
+For later changes, re-publish, copy the updated `out\*` files into the same bundle, and run `azd deploy` again. `azd ai agent invoke -o raw` reveals the actual `response.completed` or `response.failed` event; a zero CLI exit code does not prove the turn succeeded.
+
+**Stateless web-search history:** The OpenAI chat adapter returns hosted search results with a raw `WebSearchCallResponseItem`. After another local function call, replaying that raw item to the Foundry model with `store=false` caused HTTP 400 `invalid_payload`. `StatelessWebSearchChatClient` omits only the raw search result from the *next model request*, leaving the original session history, assistant text, local function calls, and citations intact. The model does not receive that raw search metadata again, so later turns must rely on the cited assistant text for source details.
+
+**Live validation:** On tao-cace, the same compound request that failed twice before this filter completed in two new hosted sessions with a cited source, a completed todo, and a saved report. A subsequent turn read the report from file memory. The previously failing plan-then-execute two-turn sequence also completed. Hosted web search remains enabled; `AllowStoredOutputEnabled` remains `false`. Check the raw terminal event when repeating this scenario: `azd` can exit zero even if a response failed.
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/StatelessWebSearchChatClient.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/StatelessWebSearchChatClient.cs
new file mode 100644
index 00000000000..1574e2867db
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/StatelessWebSearchChatClient.cs
@@ -0,0 +1,48 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Extensions.AI;
+using OpenAI.Responses;
+
+namespace SampleApp;
+
+internal sealed class StatelessWebSearchChatClient(IChatClient innerClient) : DelegatingChatClient(innerClient)
+{
+ public override Task GetResponseAsync(
+ IEnumerable messages,
+ ChatOptions? options = null,
+ CancellationToken cancellationToken = default) =>
+ base.GetResponseAsync(FilterSearchResults(messages), options, cancellationToken);
+
+ public override IAsyncEnumerable GetStreamingResponseAsync(
+ IEnumerable messages,
+ ChatOptions? options = null,
+ CancellationToken cancellationToken = default) =>
+ base.GetStreamingResponseAsync(FilterSearchResults(messages), options, cancellationToken);
+
+ private static List FilterSearchResults(IEnumerable messages)
+ {
+ List result = [];
+ foreach (ChatMessage message in messages)
+ {
+ if (message.Role != ChatRole.Assistant ||
+ !message.Contents.Any(static content => content is WebSearchToolResultContent { RawRepresentation: WebSearchCallResponseItem }))
+ {
+ result.Add(message);
+ continue;
+ }
+
+ // In this sample, Foundry returned invalid_payload when the OpenAI adapter sent
+ // this raw web_search_call again after a local tool call with store=false.
+ // The public OpenAI API accepted that pattern. Copy the message so only the next
+ // model request omits the raw result; keep the original session history, cited
+ // text, and local tool calls unchanged.
+ ChatMessage filtered = message.Clone();
+ filtered.Contents = message.Contents
+ .Where(static content => content is not WebSearchToolResultContent { RawRepresentation: WebSearchCallResponseItem })
+ .ToList();
+ result.Add(filtered);
+ }
+
+ return result;
+ }
+}
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/.env.example b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/.env.example
new file mode 100644
index 00000000000..2f559af9929
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/.env.example
@@ -0,0 +1,7 @@
+FOUNDRY_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/
+AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-5.4-mini
+AZURE_TOKEN_CREDENTIALS=dev
+ASPNETCORE_URLS=http://localhost:8088
+# Optional, requires hardware virtualization:
+# ENABLE_HYPERLIGHT_CODEACT=true
+# TOOLBOX_MCP_SERVER_URL=https:///mcp
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/.gitignore b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/.gitignore
new file mode 100644
index 00000000000..d3fe066dacc
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/.gitignore
@@ -0,0 +1,3 @@
+.env
+out/
+.checkpoints/
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/Dockerfile
new file mode 100644
index 00000000000..936987b77a1
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/Dockerfile
@@ -0,0 +1,7 @@
+FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine
+RUN apk add --no-cache python3
+WORKDIR /app
+COPY out/ .
+EXPOSE 8088
+ENV ASPNETCORE_URLS=http://+:8088
+ENTRYPOINT ["dotnet", "HostedHarnessScalingCapabilities.dll"]
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/HostedHarnessScalingCapabilities.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/HostedHarnessScalingCapabilities.csproj
new file mode 100644
index 00000000000..b62b689a3c1
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/HostedHarnessScalingCapabilities.csproj
@@ -0,0 +1,44 @@
+
+
+
+ net10.0
+ enable
+ $(NoWarn);MAAI001;OPENAI001
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/Program.cs
new file mode 100644
index 00000000000..dcb8fce0570
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/Program.cs
@@ -0,0 +1,170 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// Host the finance harness from Claw_Step03_ScalingCapabilities with local skills,
+// a confined shell, approval-gated trades, background research, and optional CodeAct.
+
+using System.ClientModel.Primitives;
+using Azure.AI.Projects;
+using Azure.Identity;
+using ClawSample;
+using DotNetEnv;
+using HyperlightSandbox.Guest.Python;
+using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.Foundry.Hosting;
+using Microsoft.Agents.AI.Hyperlight;
+using Microsoft.Agents.AI.Tools.Shell;
+using Microsoft.Extensions.AI;
+using SampleApp;
+
+Env.NoClobber().TraversePath().Load();
+
+string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
+ ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
+string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME")
+ ?? throw new InvalidOperationException("AZURE_AI_MODEL_DEPLOYMENT_NAME is not set.");
+
+bool isHosted = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("FOUNDRY_HOSTING_ENVIRONMENT"));
+string seedDir = Path.Combine(AppContext.BaseDirectory, "working");
+string skillsDir = Path.Combine(AppContext.BaseDirectory, "skills");
+string workingDir = isHosted
+ ? Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile), "working")
+ : seedDir;
+
+if (isHosted)
+{
+ // Foundry mounts this home per session. Seed only missing files so previous edits,
+ // reports, and reorganized confirmations survive the next turn or container restart.
+ Directory.SetCurrentDirectory(System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile));
+ foreach (string file in Directory.GetFiles(seedDir, "*", SearchOption.AllDirectories))
+ {
+ string destination = Path.Combine(workingDir, Path.GetRelativePath(seedDir, file));
+ if (!File.Exists(destination))
+ {
+ Directory.CreateDirectory(Path.GetDirectoryName(destination)!);
+ File.Copy(file, destination);
+ }
+ }
+}
+
+string vaultDir = Path.Combine(workingDir, "confirmations");
+var credential = new DefaultAzureCredential(new DefaultAzureCredentialOptions
+{
+ ExcludeManagedIdentityCredential = !isHosted,
+});
+IChatClient modelClient = new AIProjectClient(
+ new Uri(endpoint),
+ credential,
+ new AIProjectClientOptions { RetryPolicy = new ClientRetryPolicy(3) })
+ .GetProjectOpenAIClient()
+ .GetProjectResponsesClientForModel(deploymentName)
+ .AsIChatClient();
+IChatClient chatClient = new StatelessWebSearchChatClient(modelClient);
+
+var skillsBuilder = new AgentSkillsProviderBuilder()
+ .UseFileSkills([skillsDir], scriptRunner: new SubprocessScriptRunner().RunAsync);
+
+HttpClient? toolboxHttpClient = null;
+ModelContextProtocol.Client.McpClient? toolboxMcpClient = null;
+string? toolboxUrl = Environment.GetEnvironmentVariable("TOOLBOX_MCP_SERVER_URL");
+if (!string.IsNullOrWhiteSpace(toolboxUrl))
+{
+ (toolboxMcpClient, toolboxHttpClient) = await FoundrySkills.ConnectAsync(toolboxUrl, credential);
+ skillsBuilder.UseMcpSkills(toolboxMcpClient);
+}
+
+AgentSkillsProvider skillsProvider = skillsBuilder.Build();
+AIAgent researchAgent = ResearchAgent.Create(chatClient);
+
+// The policy filters common mistakes, not adversarial shell input. The hosted
+// container is the isolation boundary; every shell invocation still requires approval.
+await using var shell = new LocalShellExecutor(new LocalShellExecutorOptions
+{
+ WorkingDirectory = vaultDir,
+ ConfineWorkingDirectory = true,
+ Policy = new ShellPolicy(denyList:
+ [
+ @"\brm\s+-rf\b",
+ @"\bsudo\b",
+ @":\(\)\s*\{",
+ @"\bmkfs\b",
+ @">\s*/dev/sd",
+ ]),
+ Timeout = TimeSpan.FromSeconds(15),
+});
+
+// Hosted containers may not expose nested virtualization. Enable CodeAct only
+// on a host that supports Hyperlight rather than silently dropping failures.
+HyperlightCodeActProvider? codeAct = null;
+if (string.Equals(Environment.GetEnvironmentVariable("ENABLE_HYPERLIGHT_CODEACT"), "true", StringComparison.OrdinalIgnoreCase))
+{
+ codeAct = new HyperlightCodeActProvider(
+ HyperlightCodeActProviderOptions.CreateForWasm(PythonGuestModule.GetModulePath()));
+}
+
+List providers = [skillsProvider, new ShellEnvironmentProvider(shell)];
+if (codeAct is not null)
+{
+ providers.Add(codeAct);
+}
+
+AIAgent agent = chatClient.AsHarnessAgent(new HarnessAgentOptions
+{
+ Name = "hosted-harness-scaling-capabilities",
+ Description = "Analyzes a sample portfolio with skills, shell, CodeAct, and background research.",
+ FileAccessStore = new FileSystemAgentFileStore(workingDir),
+ FileMemoryStore = new FileSystemAgentFileStore(Path.Combine(workingDir, "agent-file-memory")),
+ DisableAgentSkillsProvider = true,
+ BackgroundAgents = [researchAgent],
+ ToolApprovalAgentOptions = new ToolApprovalAgentOptions
+ {
+ AutoApprovalRules =
+ [
+ FileAccessProvider.ReadOnlyToolsAutoApprovalRule,
+ AgentSkillsProvider.ReadOnlyToolsAutoApprovalRule,
+ ],
+ },
+ AgentModeProviderOptions = new AgentModeProviderOptions { DefaultMode = "execute" },
+ AIContextProviders = providers,
+ ChatOptions = new ChatOptions
+ {
+ ModelId = deploymentName,
+ Instructions =
+ """
+ You help users examine a sample stock portfolio. Read portfolio.csv before answering
+ about holdings. Load the valuation and risk-scoring skills for relevant questions.
+ Delegate independent ticker research to the background agent. To organize trade
+ confirmations, inspect the vault first, then use the approved shell tool to copy
+ the originals into organized/year/month without changing the source files.
+ Explain any simulated trade before calling place_trade. You provide information,
+ not personalized investment advice.
+ """,
+ Tools =
+ [
+ StockTools.CreateGetStockPriceTool(),
+ TradingTools.CreatePlaceTradeTool(),
+ shell.AsAIFunction(requireApproval: true),
+ ],
+ Reasoning = new() { Effort = ReasoningEffort.Medium },
+ },
+});
+
+var builder = WebApplication.CreateBuilder(args);
+builder.Services.AddFoundryResponses(agent);
+
+var app = builder.Build();
+app.MapFoundryResponses();
+
+try
+{
+ await app.RunAsync();
+}
+finally
+{
+ codeAct?.Dispose();
+ if (toolboxMcpClient is not null)
+ {
+ await toolboxMcpClient.DisposeAsync().ConfigureAwait(false);
+ }
+
+ toolboxHttpClient?.Dispose();
+}
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/README.md
new file mode 100644
index 00000000000..2ca1fa2e81e
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/README.md
@@ -0,0 +1,28 @@
+# Hosted Harness Scaling Capabilities
+
+This sample hosts the finance harness from `dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step03_ScalingCapabilities`. Its existing stock tools, simulated trade approval, file skills and scripts, optional toolbox skills, confined shell, background research agent, and demo files are linked from that console example. The `HostedHarnessScalingCapabilities` project uses local framework source and does not set `AllowStoredOutputEnabled`; the hosted platform records the turn while the model service receives `store=false`.
+
+It shares the research sample's `StatelessWebSearchChatClient`: hosted search stays available to the main harness and its background agent, but the raw search result is not replayed into later stateless model requests. The original result remains in the session history.
+
+## Run locally
+
+1. Set `FOUNDRY_PROJECT_ENDPOINT` and `AZURE_AI_MODEL_DEPLOYMENT_NAME` for an existing Foundry model deployment, then sign in with `az login`. Copy `.env.example` to `.env` if preferred.
+2. From this directory, run `dotnet run --tl:off`. Call `POST http://localhost:8088/responses` with model `hosted-harness-scaling-capabilities`. Ask about `portfolio.csv` or a stock price, then send another turn with the same conversation ID.
+3. Ask for a simulated trade or to reorganize the confirmations. The shell, trade and skill-script functions require explicit approval. Read-only files and skill loading are auto-approved. To resume, send the returned `mcp_approval_request.id` in an `mcp_approval_response` with the same conversation and hosted session. `azd ai agent invoke -f` sends a text message rather than a structured Responses body; use authenticated `az rest --resource https://ai.azure.com` for approvals.
+
+Foundry's application directory is read-only. The sample seeds only missing files into the per-session writable home, keeping previously edited portfolio files, reports and confirmations. The shell is confined to the confirmations folder; its deny-list is **not** a security boundary. Run the image only in the externally isolated hosted container.
+
+`ENABLE_HYPERLIGHT_CODEACT=true` adds CodeAct where hardware virtualization is available. It is off by default because hosted containers typically do not provide nested virtualization; requesting it on an unsupported host fails explicitly. Optional toolbox skills use `TOOLBOX_MCP_SERVER_URL`. File skill scripts require Python 3 (included in the container image).
+
+## Container build from this checkout
+
+The source, skills and demonstration data link to other MAF sample directories. Publish from the complete checkout and build the runtime image, not from an isolated upload of this directory:
+
+```powershell
+dotnet publish HostedHarnessScalingCapabilities.csproj -c Release -f net10.0 -r linux-musl-x64 --self-contained false -o out --tl:off
+docker build -t hosted-harness-scaling .
+```
+
+The image includes the local framework fix. Rebuild after changing the framework; an older published package will still reject the local history marker.
+
+For deployment to an existing Foundry project, follow the [persistent `azd` container workflow](../Hosted-Harness-Research/README.md#deploy-with-an-existing-foundry-project) with this project's publish output, bundle directory `src\hosted-harness-scaling`, and agent name `maf-harness-scaling`. A hosted validation on the updated agent version confirmed the stock tool, multi-turn history, skill loading, a Python risk-scoring script **only after explicit approval**, and a confined read-only shell command **only after explicit approval**. Hyperlight CodeAct was not enabled on the hosted container because nested virtualization was unavailable.
diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/AgentFrameworkResponseHandler.cs b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/AgentFrameworkResponseHandler.cs
index c3e91a9d1a1..51a8406e5fe 100644
--- a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/AgentFrameworkResponseHandler.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/AgentFrameworkResponseHandler.cs
@@ -664,11 +664,15 @@ await sessionStore.SaveSessionAsync(
return stream.EmitInProgress();
}
- // Check whenever the agent is storing messages when it should not.
+ // A per-service-call history provider uses a local marker in ConversationId to keep
+ // function invocation working; only a real service conversation indicates stored output.
bool CheckNotAllowedStoreUsage() =>
- // For IChatClients implementations when the backend is set to not store (store = false) the returned responseMessage.ConversationId comes null.
- // If for any reason this property is set it means that the storage setting was enabled when it shouldn't.
- !allowStoredOutputEnabled && session is ChatClientAgentSession { ConversationId: not null };
+ !allowStoredOutputEnabled
+ && session is ChatClientAgentSession { ConversationId: not null } chatSession
+ && !string.Equals(
+ chatSession.ConversationId,
+ PerServiceCallChatHistoryPersistingChatClient.LocalHistoryConversationId,
+ StringComparison.Ordinal);
}
///
diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/FoundryResponsesOptions.cs b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/FoundryResponsesOptions.cs
index 984a624c945..2d400224ea4 100644
--- a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/FoundryResponsesOptions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/FoundryResponsesOptions.cs
@@ -29,6 +29,10 @@ public sealed class FoundryResponsesOptions
/// setting exactly as the container configured it, in which case hosting neither changes it nor
/// checks it.
///
+ ///
+ /// A local conversation marker used by the framework to persist chat history in the agent
+ /// session does not count as service-stored output and does not require this option.
+ ///
///
///
/// Default is .
diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/Microsoft.Agents.AI.Foundry.Hosting.csproj b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/Microsoft.Agents.AI.Foundry.Hosting.csproj
index 2e93db80801..c7d66433dca 100644
--- a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/Microsoft.Agents.AI.Foundry.Hosting.csproj
+++ b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/Microsoft.Agents.AI.Foundry.Hosting.csproj
@@ -10,12 +10,10 @@
- true
- true
true
true
true
- $(NoWarn);OPENAI001;MEAI001;MAAI001;NU1903
+ $(NoWarn);OPENAI001;MEAI001;MAAI001
false
@@ -41,6 +39,7 @@
+
diff --git a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentSession.cs b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentSession.cs
index 102aee83e68..df9648ca783 100644
--- a/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentSession.cs
+++ b/dotnet/src/Microsoft.Agents.AI/ChatClient/ChatClientAgentSession.cs
@@ -48,6 +48,11 @@ internal ChatClientAgentSession()
/// agent service managed chat history, and the default behavior of a service is
/// to fork the chat history with each iteration.
///
+ ///
+ /// Per-service-call chat history persistence uses an internal marker here to keep the
+ /// function-invocation loop in sync with locally managed history. That marker is not
+ /// an identifier for a conversation stored by the underlying service.
+ ///
///
[JsonPropertyName("conversationId")]
public string? ConversationId
diff --git a/dotnet/src/Microsoft.Agents.AI/Microsoft.Agents.AI.csproj b/dotnet/src/Microsoft.Agents.AI/Microsoft.Agents.AI.csproj
index 61a3225bbd2..f8f59241340 100644
--- a/dotnet/src/Microsoft.Agents.AI/Microsoft.Agents.AI.csproj
+++ b/dotnet/src/Microsoft.Agents.AI/Microsoft.Agents.AI.csproj
@@ -52,6 +52,7 @@
+
diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/AgentFrameworkResponseHandlerTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/AgentFrameworkResponseHandlerTests.cs
index 09d896c543c..631d2b8e6c1 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/AgentFrameworkResponseHandlerTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/AgentFrameworkResponseHandlerTests.cs
@@ -1086,6 +1086,100 @@ public async Task CreateAsync_AgentWhoseChatClientStoredTheTurn_FailsTheRequestA
Assert.Equal(501, failure.StatusCode);
}
+ [Fact]
+ public async Task CreateAsync_HarnessLocalHistory_CompletesBothTurnsWithoutServiceStorageAsync()
+ {
+ // Arrange
+ var requests = new List<(List Messages, ChatOptions? Options)>();
+ var client = new Mock();
+ client.Setup(c => c.GetStreamingResponseAsync(
+ It.IsAny>(), It.IsAny(), It.IsAny()))
+ .Returns((IEnumerable messages, ChatOptions? options, CancellationToken _) =>
+ {
+ requests.Add((messages.ToList(), options));
+ return ToAsyncEnumerableUpdatesAsync(new ChatResponseUpdate(ChatRole.Assistant, "answer")
+ {
+ MessageId = "resp_msg_1",
+ });
+ });
+
+ var agent = CreateHarnessAgent(client.Object);
+ var store = new InMemoryAgentSessionStore();
+ var handler = BuildHandlerWith(agent, new FakeHostedSessionIsolationKeyProvider(), store);
+
+ // Act
+ var firstEvents = new List();
+ await foreach (var evt in handler.CreateAsync(
+ NewConversationRequest("conv-harness-local", "first question", store: true),
+ NewContextServing("resp_" + new string('a', 46), []),
+ CancellationToken.None))
+ {
+ firstEvents.Add(evt);
+ }
+
+ var storageAgent = new FoundryHostingAgent(
+ agent,
+ FoundryHostingAgent.ResolveSessionStorageIdentity(agent, registrationKey: null, defaultAgent: agent));
+ var storageKey = new AgentSessionStoreKey("conv-harness-local")
+ .WithPartition("user", FakeHostedSessionIsolationKeyProvider.DefaultUserId);
+ var savedSession = Assert.IsType(
+ await store.GetSessionAsync(storageAgent, storageKey, CancellationToken.None));
+
+ var secondEvents = new List();
+ await foreach (var evt in handler.CreateAsync(
+ NewConversationRequest("conv-harness-local", "second question", store: true),
+ NewContextServing("resp_" + new string('b', 46), []),
+ CancellationToken.None))
+ {
+ secondEvents.Add(evt);
+ }
+
+ // Assert: the local marker survives the session round-trip, but is never sent to the model.
+ Assert.Contains(firstEvents, evt => evt is ResponseCompletedEvent);
+ Assert.Contains(secondEvents, evt => evt is ResponseCompletedEvent);
+ Assert.Equal("_agent_local_chat_history", savedSession.ConversationId);
+ Assert.Equal(2, requests.Count);
+ Assert.Single(requests[1].Messages, message => message.Text.Contains("first question", StringComparison.Ordinal));
+ Assert.Single(requests[1].Messages, message => message.Text.Contains("answer", StringComparison.Ordinal));
+ Assert.Null(requests[1].Options?.ConversationId);
+
+ // The default host still disables storage in each underlying service request.
+ foreach (var (_, options) in requests)
+ {
+ Assert.NotNull(options?.RawRepresentationFactory);
+ var raw = Assert.IsType(options.RawRepresentationFactory(client.Object));
+ Assert.False(raw.StoredOutputEnabled);
+ }
+ }
+
+ [Fact]
+ public async Task CreateAsync_HarnessServiceStoredConversationId_StillFailsTheRequestAsync()
+ {
+ // Arrange
+ var client = new Mock();
+ client.Setup(c => c.GetStreamingResponseAsync(
+ It.IsAny>(), It.IsAny(), It.IsAny()))
+ .Returns(() => ToAsyncEnumerableUpdatesAsync(
+ new ChatResponseUpdate(ChatRole.Assistant, "answer")
+ {
+ MessageId = "resp_msg_1",
+ ConversationId = "conv-downstream",
+ }));
+
+ var agent = CreateHarnessAgent(client.Object);
+ var handler = BuildHandlerWith(agent, new FakeHostedSessionIsolationKeyProvider(), new InMemoryAgentSessionStore());
+
+ // Act
+ var failure = await Assert.ThrowsAsync(() => DrainEventsAsync(handler.CreateAsync(
+ NewConversationRequest("conv-harness-rejected", "first question", store: true),
+ NewContextServing("resp_" + new string('c', 46), []),
+ CancellationToken.None)));
+
+ // Assert: a real service conversation ID remains a hosting misconfiguration.
+ Assert.Equal(HostedStoredOutputCompatibility.MisconfiguredAgentErrorCode, failure.Error.Code);
+ Assert.Equal(HostedStoredOutputCompatibility.MisconfiguredAgentStatusCode, failure.StatusCode);
+ }
+
[Fact]
public async Task CreateAsync_AgentWhoseChatClientStoredTheTurn_NeverReportsTheTurnCompletedAsync()
{
@@ -1485,6 +1579,18 @@ private static IChatClient CreateCapturingChatClient(List captured,
return mock.Object;
}
+ private static HarnessAgent CreateHarnessAgent(IChatClient client) =>
+ new(client, new HarnessAgentOptions
+ {
+ DisableToolAutoApproval = true,
+ DisableOpenTelemetry = true,
+ DisableFileMemory = true,
+ DisableWebSearch = true,
+ DisableTodoProvider = true,
+ DisableAgentModeProvider = true,
+ DisableAgentSkillsProvider = true,
+ });
+
private static async IAsyncEnumerable ToAsyncEnumerableUpdatesAsync(params ChatResponseUpdate[] updates)
{
foreach (var update in updates)
diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests.csproj b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests.csproj
index 41c9906b830..7677f6fa44e 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests.csproj
+++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests.csproj
@@ -19,6 +19,11 @@
+
+
+
+
+
diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/StatelessWebSearchChatClientTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/StatelessWebSearchChatClientTests.cs
new file mode 100644
index 00000000000..e1a6ba77fd8
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/StatelessWebSearchChatClientTests.cs
@@ -0,0 +1,107 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+#if NET10_0
+#pragma warning disable OPENAI001 // Web-search response items are experimental.
+
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.AI;
+using Moq;
+using OpenAI.Responses;
+using SampleApp;
+
+namespace Microsoft.Agents.AI.Foundry.Hosting.UnitTests;
+
+public class StatelessWebSearchChatClientTests
+{
+ [Fact]
+ public async Task GetStreamingResponseAsync_RemovesRawSearchResultWithoutMutatingHistoryAsync()
+ {
+ // Arrange
+ var rawSearch = ResponseItem.CreateWebSearchCallItem();
+ var searchResult = new WebSearchToolResultContent("search_1") { RawRepresentation = rawSearch };
+ var searchCall = new WebSearchToolCallContent("search_1");
+ var functionCall = new FunctionCallContent("call_1", "file_memory_write");
+ var assistant = new ChatMessage(ChatRole.Assistant,
+ [
+ new TextContent("Source citation"),
+ searchCall,
+ searchResult,
+ functionCall,
+ ]);
+ var tool = new ChatMessage(ChatRole.Tool, [new FunctionResultContent("call_1", "saved")]);
+ List? forwarded = null;
+ var inner = new Mock();
+ inner.Setup(client => client.GetStreamingResponseAsync(
+ It.IsAny>(), It.IsAny(), It.IsAny()))
+ .Returns((IEnumerable messages, ChatOptions? _, CancellationToken _) =>
+ {
+ forwarded = messages.ToList();
+ return GetUpdatesAsync();
+ });
+
+ // Act
+ var client = new StatelessWebSearchChatClient(inner.Object);
+ await foreach (var _ in client.GetStreamingResponseAsync([assistant, tool], new ChatOptions()))
+ {
+ }
+
+ // Assert: only the raw search item is withheld from the model; stored history and
+ // the following function call and result keep their original identities.
+ Assert.NotNull(forwarded);
+ Assert.Equal(2, forwarded.Count);
+ Assert.NotSame(assistant, forwarded[0]);
+ Assert.NotSame(assistant.Contents, forwarded[0].Contents);
+ Assert.DoesNotContain(forwarded[0].Contents, content => content is WebSearchToolResultContent);
+ Assert.Contains(forwarded[0].Contents, content => ReferenceEquals(content, searchCall));
+ Assert.Contains(forwarded[0].Contents, content => ReferenceEquals(content, functionCall));
+ Assert.Same(tool, forwarded[1]);
+ Assert.Same(searchResult, assistant.Contents[2]);
+ Assert.Same(rawSearch, searchResult.RawRepresentation);
+ }
+
+ [Fact]
+ public async Task GetResponseAsync_RemovesRawSearchResultButKeepsUnrelatedContentsAsync()
+ {
+ // Arrange
+ var searchResult = new WebSearchToolResultContent("search_1")
+ {
+ RawRepresentation = ResponseItem.CreateWebSearchCallItem(),
+ };
+ var otherSearchResult = new WebSearchToolResultContent("search_2");
+ var assistant = new ChatMessage(ChatRole.Assistant,
+ [
+ new TextContent("Summary"),
+ searchResult,
+ otherSearchResult,
+ ]);
+ List? forwarded = null;
+ var inner = new Mock();
+ inner.Setup(client => client.GetResponseAsync(
+ It.IsAny>(), It.IsAny(), It.IsAny()))
+ .Returns((IEnumerable messages, ChatOptions? _, CancellationToken _) =>
+ {
+ forwarded = messages.ToList();
+ return Task.FromResult(new ChatResponse(new ChatMessage(ChatRole.Assistant, "done")));
+ });
+
+ // Act
+ var client = new StatelessWebSearchChatClient(inner.Object);
+ _ = await client.GetResponseAsync([assistant], new ChatOptions());
+
+ // Assert
+ Assert.NotNull(forwarded);
+ Assert.DoesNotContain(forwarded[0].Contents, content => ReferenceEquals(content, searchResult));
+ Assert.Contains(forwarded[0].Contents, content => ReferenceEquals(content, otherSearchResult));
+ Assert.Contains(assistant.Contents, content => ReferenceEquals(content, searchResult));
+ }
+
+ private static async IAsyncEnumerable GetUpdatesAsync()
+ {
+ yield return new ChatResponseUpdate(ChatRole.Assistant, "done");
+ await Task.CompletedTask;
+ }
+}
+#endif
From 384138a51a2c55320d7b65aec5540e0db41cc70e Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Fri, 25 Sep 2026 10:41:08 +0100
Subject: [PATCH 2/5] .NET: Revalidate browsing redirects and document hosted
file isolation
- Follow WebBrowsingTool redirects manually and re-check the access policy on every hop.
- Connect only to the addresses approved by the policy check, closing the DNS rebinding window.
- Treat 0.0.0.0 and :: as non-public addresses.
- Add focused WebBrowsingTool tests.
- Document that hosted Harness file stores rely on Foundry per-session sandboxes.
---
.../WebBrowsingTool.cs | 207 +++++++++++++---
.../Hosted-Harness-DataProcessing/README.md | 2 +
.../Hosted-Harness-Research/README.md | 4 +-
.../README.md | 2 +
.../WebBrowsingToolTests.cs | 223 ++++++++++++++++++
5 files changed, 406 insertions(+), 32 deletions(-)
create mode 100644 dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/WebBrowsingToolTests.cs
diff --git a/dotnet/samples/02-agents/Harness/Harness_Step01_Research/WebBrowsingTool.cs b/dotnet/samples/02-agents/Harness/Harness_Step01_Research/WebBrowsingTool.cs
index b51739eca55..14ad46af5a8 100644
--- a/dotnet/samples/02-agents/Harness/Harness_Step01_Research/WebBrowsingTool.cs
+++ b/dotnet/samples/02-agents/Harness/Harness_Step01_Research/WebBrowsingTool.cs
@@ -15,17 +15,27 @@ namespace SampleApp;
///
internal sealed partial class WebBrowsingTool : AIFunction
{
- private static readonly HttpClient s_httpClient = new();
+ private const int MaxRedirects = 10;
private readonly AIFunction _inner;
private readonly WebBrowsingToolOptions _options;
+ private readonly Func> _resolveHostAddressesAsync;
///
/// Initializes a new instance of the class.
///
/// Options controlling which URLs are permitted. By default, no hosts are accessible.
public WebBrowsingTool(WebBrowsingToolOptions options)
+ : this(options, Dns.GetHostAddressesAsync)
+ {
+ }
+
+ internal WebBrowsingTool(
+ WebBrowsingToolOptions options,
+ Func> resolveHostAddressesAsync)
{
this._options = options ?? throw new ArgumentNullException(nameof(options));
+ this._resolveHostAddressesAsync = resolveHostAddressesAsync ??
+ throw new ArgumentNullException(nameof(resolveHostAddressesAsync));
this._inner = AIFunctionFactory.Create(this.DownloadUriAsync);
}
@@ -45,7 +55,7 @@ public WebBrowsingTool(WebBrowsingToolOptions options)
this._inner.InvokeAsync(arguments, cancellationToken);
[Description("Fetch the html from the given url as markdown")]
- private async Task DownloadUriAsync(
+ internal async Task DownloadUriAsync(
[Description("The URL to download")] string uri,
CancellationToken cancellationToken = default)
{
@@ -59,17 +69,58 @@ private async Task DownloadUriAsync(
return $"Error: Only HTTP and HTTPS URLs are supported. Got: '{parsedUri.Scheme}'.";
}
- // Check access policy.
- string? accessError = await this.CheckAccessAsync(parsedUri, cancellationToken);
- if (accessError is not null)
+ AccessCheckResult access = await this.CheckAccessAsync(parsedUri, cancellationToken);
+ if (access.Error is not null)
{
- return accessError;
+ return access.Error;
}
try
{
- string html = await s_httpClient.GetStringAsync(parsedUri, cancellationToken);
- return HtmlToMarkdownConverter.Convert(html);
+ // Follow redirects manually, re-checking the access policy on every hop so a permitted
+ // URL cannot redirect into a blocked (private/metadata/localhost) target.
+ Uri currentUri = parsedUri;
+ for (int hop = 0; ; hop++)
+ {
+ if (hop > MaxRedirects)
+ {
+ return $"Error downloading {uri}: too many redirects.";
+ }
+
+ using HttpClient httpClient = CreateHttpClient(currentUri, access.Addresses);
+ using var response = await httpClient.GetAsync(
+ currentUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
+
+ if (response.StatusCode is HttpStatusCode.Moved or HttpStatusCode.Found
+ or HttpStatusCode.SeeOther or HttpStatusCode.TemporaryRedirect
+ or HttpStatusCode.PermanentRedirect)
+ {
+ Uri? location = response.Headers.Location;
+ if (location is null)
+ {
+ return $"Error downloading {uri}: redirect with no location.";
+ }
+
+ // Resolve relative redirects against the current URL, then re-check access.
+ currentUri = new Uri(currentUri, location);
+ if (currentUri.Scheme is not "http" and not "https")
+ {
+ return $"Error: redirect to unsupported scheme '{currentUri.Scheme}'.";
+ }
+
+ access = await this.CheckAccessAsync(currentUri, cancellationToken);
+ if (access.Error is not null)
+ {
+ return access.Error;
+ }
+
+ continue;
+ }
+
+ response.EnsureSuccessStatusCode();
+ string html = await response.Content.ReadAsStringAsync(cancellationToken);
+ return HtmlToMarkdownConverter.Convert(html);
+ }
}
catch (HttpRequestException ex)
{
@@ -79,11 +130,14 @@ private async Task DownloadUriAsync(
///
/// Checks whether the given URI is permitted by the configured access policy.
- /// Returns null if allowed, or an error message string if blocked.
+ /// Returns the addresses that the HTTP connection may use, or an error message if blocked.
///
- private async Task CheckAccessAsync(Uri uri, CancellationToken cancellationToken)
+ private async Task CheckAccessAsync(
+ Uri uri,
+ CancellationToken cancellationToken)
{
string host = uri.Host;
+ bool isAllowedHost = false;
// 1. Check AllowedHosts.
if (this._options.AllowedHosts is { Count: > 0 } allowedHosts)
@@ -92,59 +146,145 @@ private async Task DownloadUriAsync(
{
if (HostMatchesPattern(host, pattern))
{
- return null; // Allowed by explicit host list.
+ isAllowedHost = true;
+ break;
}
}
}
// 2. Short-circuit when the policy is guaranteed to block.
- if (!this._options.AllowPublicNetworks &&
+ if (!isAllowedHost &&
+ !this._options.AllowPublicNetworks &&
!this._options.AllowPrivateNetworks &&
!this._options.AllowAllHosts)
{
- return $"Error: Access to '{host}' is blocked by the current access policy. Configure WebBrowsingToolOptions to allow access.";
+ return AccessCheckResult.Blocked(
+ $"Error: Access to '{host}' is blocked by the current access policy. Configure WebBrowsingToolOptions to allow access.");
}
- // 3. Resolve DNS to determine if the host is public or private.
+ // 3. Resolve exactly once. The resulting addresses are also used for the socket connection.
IPAddress[] addresses;
try
{
- addresses = await Dns.GetHostAddressesAsync(host, cancellationToken);
+ addresses = IPAddress.TryParse(uri.IdnHost, out IPAddress? literalAddress)
+ ? [literalAddress]
+ : await this._resolveHostAddressesAsync(uri.IdnHost, cancellationToken);
}
catch (SocketException)
{
- return $"Error: Could not resolve host '{host}'.";
+ return AccessCheckResult.Blocked($"Error: Could not resolve host '{host}'.");
}
if (addresses.Length == 0)
{
- return $"Error: Could not resolve host '{host}'.";
+ return AccessCheckResult.Blocked($"Error: Could not resolve host '{host}'.");
}
- bool isPrivate = Array.Exists(addresses, IsPrivateAddress);
+ IPAddress[] distinctAddresses = addresses.Distinct().ToArray();
+ if (isAllowedHost || this._options.AllowAllHosts)
+ {
+ return AccessCheckResult.Allowed(distinctAddresses);
+ }
- // 4. If public and AllowPublicNetworks is true → allow.
- if (!isPrivate && this._options.AllowPublicNetworks)
+ // Permit only addresses in enabled network classes. Mixed DNS answers cannot cause the
+ // connection callback to reach an address class that the policy did not authorize.
+ IPAddress[] allowedAddresses = distinctAddresses
+ .Where(address =>
+ IsPrivateAddress(address)
+ ? this._options.AllowPrivateNetworks
+ : this._options.AllowPublicNetworks)
+ .ToArray();
+ if (allowedAddresses.Length > 0)
{
- return null;
+ return AccessCheckResult.Allowed(allowedAddresses);
}
- // 5. If private and AllowPrivateNetworks is true → allow.
- if (isPrivate && this._options.AllowPrivateNetworks)
+ bool hasPrivateAddress = Array.Exists(distinctAddresses, IsPrivateAddress);
+ bool hasPublicAddress = Array.Exists(distinctAddresses, address => !IsPrivateAddress(address));
+ string networkType = (hasPrivateAddress, hasPublicAddress) switch
+ {
+ (true, true) => "private/internal and public network",
+ (true, false) => "private/internal network",
+ _ => "public network",
+ };
+ return AccessCheckResult.Blocked(
+ $"Error: Access to '{host}' is blocked. The host resolves to a {networkType} address and the current access policy does not permit this. " +
+ "Configure WebBrowsingToolOptions to allow access.");
+ }
+
+ private static HttpClient CreateHttpClient(Uri uri, IPAddress[] addresses)
+ {
+ // Redirects are followed by DownloadUriAsync so each hop is re-checked. A proxy is
+ // disabled because the connection callback would otherwise see the proxy endpoint and
+ // could no longer pin the socket to the addresses approved by the access policy.
+ var handler = new SocketsHttpHandler
+ {
+ AllowAutoRedirect = false,
+ UseProxy = false,
+ ConnectCallback = (context, cancellationToken) =>
+ ConnectAsync(context, uri.IdnHost, uri.Port, addresses, cancellationToken),
+ };
+ return new HttpClient(handler);
+ }
+
+ private static async ValueTask ConnectAsync(
+ SocketsHttpConnectionContext context,
+ string expectedHost,
+ int expectedPort,
+ IPAddress[] addresses,
+ CancellationToken cancellationToken)
+ {
+ if (!string.Equals(
+ context.DnsEndPoint.Host,
+ expectedHost,
+ StringComparison.OrdinalIgnoreCase) ||
+ context.DnsEndPoint.Port != expectedPort)
{
- return null;
+ throw new HttpRequestException(
+ $"Unexpected HTTP connection destination '{context.DnsEndPoint.Host}:{context.DnsEndPoint.Port}'.");
}
- // 6. If AllowAllHosts is true → allow.
- if (this._options.AllowAllHosts)
+ var failures = new List(addresses.Length);
+ foreach (IPAddress address in addresses)
{
- return null;
+ Socket? socket = null;
+ bool streamOwnsSocket = false;
+
+ try
+ {
+ socket = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp)
+ {
+ NoDelay = true,
+ };
+ await socket.ConnectAsync(
+ new IPEndPoint(address, expectedPort),
+ cancellationToken);
+ streamOwnsSocket = true;
+ return new NetworkStream(socket, ownsSocket: true);
+ }
+ catch (SocketException ex) when (!cancellationToken.IsCancellationRequested)
+ {
+ failures.Add(ex);
+ }
+ finally
+ {
+ if (!streamOwnsSocket)
+ {
+ socket?.Dispose();
+ }
+ }
}
- // 7. Block.
- string networkType = isPrivate ? "private/internal network" : "public network";
- return $"Error: Access to '{host}' is blocked. The host resolves to a {networkType} address and the current access policy does not permit this. " +
- "Configure WebBrowsingToolOptions to allow access.";
+ throw new HttpRequestException(
+ $"Could not connect to any policy-approved address for '{expectedHost}:{expectedPort}'.",
+ new AggregateException(failures));
+ }
+
+ private readonly record struct AccessCheckResult(string? Error, IPAddress[] Addresses)
+ {
+ public static AccessCheckResult Allowed(IPAddress[] addresses) => new(null, addresses);
+
+ public static AccessCheckResult Blocked(string error) => new(error, []);
}
///
@@ -177,6 +317,11 @@ private static bool IsPrivateAddress(IPAddress address)
address = address.MapToIPv4();
}
+ if (address.Equals(IPAddress.Any) || address.Equals(IPAddress.IPv6Any))
+ {
+ return true;
+ }
+
if (IPAddress.IsLoopback(address))
{
return true;
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/README.md
index a46327ffbbf..c07d2f6d3ca 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/README.md
@@ -12,6 +12,8 @@ The project references the framework **source in this repository**. `AddFoundryR
Hosted containers have a read-only application directory. On startup the sample copies **only missing** seed files into the session's writable home directory so edited files and generated reports survive later turns.
+These files rely on Foundry's session sandbox: by default each caller gets their own session with a private `$HOME` (see [Isolate hosted agent sessions per user](https://learn.microsoft.com/azure/foundry/agents/how-to/isolate-sessions-per-user)). If you [place several users in one session](https://learn.microsoft.com/azure/foundry/agents/how-to/multiplex-session-users), partition the working folder per user yourself.
+
## Container build from this checkout
The project links its CSV from elsewhere in the MAF checkout, so publish from the repository before building this runtime image. Uploading this directory alone as Foundry source/ZIP is not supported:
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/README.md
index 62fb8febd2b..2ef701d854c 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/README.md
@@ -10,7 +10,9 @@ The project references the framework **source in this repository** so it exercis
2. From this directory, run `dotnet run --tl:off`. The host serves `POST http://localhost:8088/responses` when `ASPNETCORE_URLS=http://localhost:8088`.
3. Send a research question with model `hosted-harness-research`. Send a second request with the same conversation ID to verify that local history continues without enabling agent-side storage. The platform `store` setting is independent of the model client's `store=false` setting.
-Browsing allows public networks only. File memory goes under `agent-files` locally and the session's writable home directory when hosted.
+Browsing allows public networks only. The browsing tool checks every redirect against the same policy and connects only to the addresses it approved, so a public page cannot send it to a private or metadata address. File memory goes under `agent-files` locally and the session's writable home directory when hosted.
+
+Hosted file memory relies on Foundry's session sandbox: by default each caller gets their own session with a private `$HOME` (see [Isolate hosted agent sessions per user](https://learn.microsoft.com/azure/foundry/agents/how-to/isolate-sessions-per-user)). If you [place several users in one session](https://learn.microsoft.com/azure/foundry/agents/how-to/multiplex-session-users), partition the file store per user yourself.
## Container build from this checkout
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/README.md
index 2ca1fa2e81e..349d018a95e 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/README.md
@@ -12,6 +12,8 @@ It shares the research sample's `StatelessWebSearchChatClient`: hosted search st
Foundry's application directory is read-only. The sample seeds only missing files into the per-session writable home, keeping previously edited portfolio files, reports and confirmations. The shell is confined to the confirmations folder; its deny-list is **not** a security boundary. Run the image only in the externally isolated hosted container.
+The working folder and file memory rely on Foundry's session sandbox: by default each caller gets their own session with a private `$HOME` (see [Isolate hosted agent sessions per user](https://learn.microsoft.com/azure/foundry/agents/how-to/isolate-sessions-per-user)). If you [place several users in one session](https://learn.microsoft.com/azure/foundry/agents/how-to/multiplex-session-users), partition both stores per user yourself.
+
`ENABLE_HYPERLIGHT_CODEACT=true` adds CodeAct where hardware virtualization is available. It is off by default because hosted containers typically do not provide nested virtualization; requesting it on an unsupported host fails explicitly. Optional toolbox skills use `TOOLBOX_MCP_SERVER_URL`. File skill scripts require Python 3 (included in the container image).
## Container build from this checkout
diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/WebBrowsingToolTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/WebBrowsingToolTests.cs
new file mode 100644
index 00000000000..375fd163893
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/WebBrowsingToolTests.cs
@@ -0,0 +1,223 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+#if NET10_0
+
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Net.Sockets;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using SampleApp;
+
+namespace Microsoft.Agents.AI.Foundry.Hosting.UnitTests;
+
+///
+/// Covers the browsing tool that the hosted research sample links from the Harness research sample.
+///
+public sealed class WebBrowsingToolTests
+{
+ [Fact]
+ public async Task DownloadUriAsync_UsesThePolicyValidatedAddressAsync()
+ {
+ // Arrange
+ await using var server = new OneShotHttpServer("pinned");
+ int resolutionCount = 0;
+ var tool = new WebBrowsingTool(
+ new WebBrowsingToolOptions { AllowedHosts = ["rebind.invalid"] },
+ (host, _) =>
+ {
+ Assert.Equal("rebind.invalid", host);
+ resolutionCount++;
+ return Task.FromResult(new[] { IPAddress.Loopback });
+ });
+
+ // Act
+ string result = await tool.DownloadUriAsync(
+ $"http://rebind.invalid:{server.Port}/",
+ TestContext.Current.CancellationToken);
+
+ // Assert: the connection reuses the checked address instead of resolving the host again.
+ Assert.Equal("pinned", result);
+ Assert.Equal(1, resolutionCount);
+ }
+
+ [Fact]
+ public async Task DownloadUriAsync_PreservesTheOriginalHostHeaderAsync()
+ {
+ // Arrange
+ await using var server = new OneShotHttpServer("host");
+ var tool = CreateTool(
+ ["original-host.invalid"],
+ (_, _) => Task.FromResult(new[] { IPAddress.Loopback }));
+
+ // Act
+ await tool.DownloadUriAsync(
+ $"http://original-host.invalid:{server.Port}/resource",
+ TestContext.Current.CancellationToken);
+
+ // Assert
+ string requestHeaders = await server.RequestHeaders;
+ Assert.Contains(
+ $"Host: original-host.invalid:{server.Port}",
+ requestHeaders,
+ StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public async Task DownloadUriAsync_TriesEveryPolicyValidatedAddressAsync()
+ {
+ // Arrange: the first approved address refuses the connection.
+ await using var server = new OneShotHttpServer("second");
+ var tool = CreateTool(
+ ["multiple.invalid"],
+ (_, _) => Task.FromResult(new[] { IPAddress.Parse("127.0.0.2"), IPAddress.Loopback }));
+
+ // Act
+ string result = await tool.DownloadUriAsync(
+ $"http://multiple.invalid:{server.Port}/",
+ TestContext.Current.CancellationToken);
+
+ // Assert
+ Assert.Equal("second", result);
+ }
+
+ [Fact]
+ public async Task DownloadUriAsync_RevalidatesRedirectDestinationsAsync()
+ {
+ // Arrange: an allowed host redirects to a host that resolves to a private address.
+ await using var server = new OneShotHttpServer(
+ "redirect",
+ $"http://blocked.invalid:{GetUnusedPort()}/private");
+ var resolvedHosts = new List();
+ var tool = new WebBrowsingTool(
+ new WebBrowsingToolOptions
+ {
+ AllowedHosts = ["allowed.invalid"],
+ AllowPublicNetworks = true,
+ },
+ (host, _) =>
+ {
+ resolvedHosts.Add(host);
+ return Task.FromResult(new[] { IPAddress.Loopback });
+ });
+
+ // Act
+ string result = await tool.DownloadUriAsync(
+ $"http://allowed.invalid:{server.Port}/",
+ TestContext.Current.CancellationToken);
+
+ // Assert
+ Assert.Contains(
+ "resolves to a private/internal network address",
+ result,
+ StringComparison.OrdinalIgnoreCase);
+ Assert.Equal(["allowed.invalid", "blocked.invalid"], resolvedHosts);
+ }
+
+ [Fact]
+ public async Task DownloadUriAsync_BlocksTheUnspecifiedAddressAsNonPublicAsync()
+ {
+ // Arrange
+ await using var server = new OneShotHttpServer("local");
+ var tool = new WebBrowsingTool(
+ new WebBrowsingToolOptions { AllowPublicNetworks = true },
+ (_, _) => throw new InvalidOperationException("IP literals must not use DNS."));
+
+ // Act
+ string result = await tool.DownloadUriAsync(
+ $"http://0.0.0.0:{server.Port}/",
+ TestContext.Current.CancellationToken);
+
+ // Assert
+ Assert.Contains(
+ "resolves to a private/internal network address",
+ result,
+ StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static WebBrowsingTool CreateTool(
+ IReadOnlyList allowedHosts,
+ Func> resolver) =>
+ new(new WebBrowsingToolOptions { AllowedHosts = allowedHosts }, resolver);
+
+ private static int GetUnusedPort()
+ {
+ var listener = new TcpListener(IPAddress.Loopback, 0);
+ listener.Start();
+ int port = ((IPEndPoint)listener.LocalEndpoint).Port;
+ listener.Stop();
+ return port;
+ }
+
+ ///
+ /// Serves one HTTP response on the loopback interface and records the request headers.
+ ///
+ private sealed class OneShotHttpServer : IAsyncDisposable
+ {
+ private readonly TcpListener _listener;
+ private readonly CancellationTokenSource _cancellationTokenSource = new();
+ private readonly Task _serverTask;
+ private readonly TaskCompletionSource _requestHeaders =
+ new(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ public OneShotHttpServer(string responseBody, string? redirectLocation = null)
+ {
+ this._listener = new TcpListener(IPAddress.Loopback, 0);
+ this._listener.Start();
+ this.Port = ((IPEndPoint)this._listener.LocalEndpoint).Port;
+ this._serverTask = this.ServeAsync(responseBody, redirectLocation);
+ }
+
+ public int Port { get; }
+
+ public Task RequestHeaders => this._requestHeaders.Task;
+
+ public async ValueTask DisposeAsync()
+ {
+ await this._cancellationTokenSource.CancelAsync();
+ this._listener.Dispose();
+
+ try
+ {
+ await this._serverTask;
+ }
+ catch (OperationCanceledException)
+ {
+ }
+ catch (SocketException) when (this._cancellationTokenSource.IsCancellationRequested)
+ {
+ }
+
+ this._cancellationTokenSource.Dispose();
+ }
+
+ private async Task ServeAsync(string responseBody, string? redirectLocation)
+ {
+ using TcpClient client = await this._listener.AcceptTcpClientAsync(this._cancellationTokenSource.Token);
+ await using NetworkStream stream = client.GetStream();
+
+ var request = new StringBuilder();
+ byte[] buffer = new byte[1024];
+ while (!request.ToString().Contains("\r\n\r\n", StringComparison.Ordinal))
+ {
+ int bytesRead = await stream.ReadAsync(buffer, this._cancellationTokenSource.Token);
+ if (bytesRead == 0)
+ {
+ break;
+ }
+
+ request.Append(Encoding.ASCII.GetString(buffer, 0, bytesRead));
+ }
+
+ this._requestHeaders.TrySetResult(request.ToString());
+
+ string response = redirectLocation is null
+ ? $"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: {Encoding.UTF8.GetByteCount(responseBody)}\r\nConnection: close\r\n\r\n{responseBody}"
+ : $"HTTP/1.1 302 Found\r\nLocation: {redirectLocation}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n";
+ await stream.WriteAsync(Encoding.UTF8.GetBytes(response), this._cancellationTokenSource.Token);
+ }
+ }
+}
+#endif
From 3013c84e9771651066e53248e7d9c5e4b3e43455 Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Thu, 1 Oct 2026 15:57:06 +0100
Subject: [PATCH 3/5] fix(hosting): use ZIP deployment for Harness samples
---
.../.agentignore | 19 +++++
.../Hosted-Harness-DataProcessing/Dockerfile | 6 --
.../HostedHarnessDataProcessing.csproj | 37 +++++++--
.../Hosted-Harness-DataProcessing/README.md | 55 +++++++++++--
.../Hosted-Harness-DataProcessing/azure.yaml | 30 +++++++
.../Hosted-Harness-Research/.agentignore | 20 +++++
.../Hosted-Harness-Research/Dockerfile | 6 --
.../HostedHarnessResearch.csproj | 33 ++++++--
.../Hosted-Harness-Research/README.md | 79 ++++++++++++-------
.../Hosted-Harness-Research/azure.yaml | 30 +++++++
.../README.md | 34 +++++++-
.../scripts/Add-LocalFrameworkFeed.ps1 | 1 +
.../scripts/SourceDeployment.targets | 27 +++++++
.../scripts/add-local-framework-feed.sh | 1 +
14 files changed, 316 insertions(+), 62 deletions(-)
create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/.agentignore
delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/Dockerfile
create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/azure.yaml
create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/.agentignore
delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/Dockerfile
create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/azure.yaml
create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/scripts/SourceDeployment.targets
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/.agentignore b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/.agentignore
new file mode 100644
index 00000000000..ef27c4a15fc
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/.agentignore
@@ -0,0 +1,19 @@
+# Controls which files are excluded from the source-deploy ZIP (.gitignore syntax).
+azure.yaml
+.agentignore
+
+.env
+.env.*
+.azure/
+.git/
+
+bin/
+obj/
+out/
+*.user
+*.suo
+.vs/
+
+.checkpoints/
+
+# Include working/sales.csv, local-feed/, and nuget.config in the upload.
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/Dockerfile
deleted file mode 100644
index 5d4f48aa163..00000000000
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/Dockerfile
+++ /dev/null
@@ -1,6 +0,0 @@
-FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine
-WORKDIR /app
-COPY out/ .
-EXPOSE 8088
-ENV ASPNETCORE_URLS=http://+:8088
-ENTRYPOINT ["dotnet", "HostedHarnessDataProcessing.dll"]
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/HostedHarnessDataProcessing.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/HostedHarnessDataProcessing.csproj
index 3f29cb95afa..b92c016a4bd 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/HostedHarnessDataProcessing.csproj
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/HostedHarnessDataProcessing.csproj
@@ -1,26 +1,49 @@
-
+
- net10.0
+ false
+
+
+
+
+
+ net10.0
+
+ enable
enable
$(NoWarn);MAAI001;OPENAI001
+ 1.20.0-preview.260831.1
+
+ true
+ false
-
-
-
+
-
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/README.md
index c07d2f6d3ca..f373ab83f84 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/README.md
@@ -4,6 +4,16 @@ This sample hosts a `HarnessAgent` that reads a sample sales CSV and writes requ
The project references the framework **source in this repository**. `AddFoundryResponses(agent)` uses the default `AllowStoredOutputEnabled = false`; the agent keeps chat history in its session rather than in the model service.
+Deployment uses a **source ZIP**, with no Dockerfile or container registry. The preparation step includes the linked sales CSV in a standalone source folder. No Python interpreter is needed.
+
+## Prerequisites
+
+- .NET 10 SDK.
+- An existing Foundry project and model deployment.
+- Azure CLI authenticated with `az login`.
+- Azure Developer CLI (`azd`) with the AI agents extension: `azd extension install azure.ai.agents`.
+- PowerShell 7 for the local framework packaging helper.
+
## Run locally
1. Set `FOUNDRY_PROJECT_ENDPOINT` and `AZURE_AI_MODEL_DEPLOYMENT_NAME` for an existing Foundry model deployment, then sign in with `az login`. Copy `.env.example` to `.env` if you prefer a local file.
@@ -14,15 +24,48 @@ Hosted containers have a read-only application directory. On startup the sample
These files rely on Foundry's session sandbox: by default each caller gets their own session with a private `$HOME` (see [Isolate hosted agent sessions per user](https://learn.microsoft.com/azure/foundry/agents/how-to/isolate-sessions-per-user)). If you [place several users in one session](https://learn.microsoft.com/azure/foundry/agents/how-to/multiplex-session-users), partition the working folder per user yourself.
-## Container build from this checkout
+## Deploy to Foundry (source ZIP)
-The project links its CSV from elsewhere in the MAF checkout, so publish from the repository before building this runtime image. Uploading this directory alone as Foundry source/ZIP is not supported:
+Start in this sample directory. Prepare the source in a working directory **outside the repository**, then initialize against an existing Foundry project:
```powershell
-dotnet publish HostedHarnessDataProcessing.csproj -c Release -f net10.0 -r linux-musl-x64 --self-contained false -o out --tl:off
-docker build -t hosted-harness-data-processing .
+$repo = (Resolve-Path '..\..\..\..\..\..').Path
+$work = Join-Path $env:TEMP 'hosted-harness-data-processing-work'
+$source = Join-Path $work 'source'
+
+New-Item -ItemType Directory -Path $work -Force | Out-Null
+dotnet msbuild HostedHarnessDataProcessing.csproj -target:PrepareSourceDeployment `
+ "-property:SourceDeploymentDirectory=$source"
+
+Set-Location $work
+azd auth login
+azd ai agent init -m (Join-Path $source 'azure.yaml') `
+ -p '' -d ''
```
-The image includes the local framework assemblies. Rebuild it after a framework change rather than testing an older published package.
+`PrepareSourceDeployment` includes `working/sales.csv` from the console sample and does not copy `.env`, local sessions, or build output. The copied project has one target framework and explicit package versions. Its content settings copy the CSV into the published application.
+
+**Do not skip the package helper when testing this branch.** It includes the local framework fix rather than relying on an older published package:
+
+```powershell
+& (Join-Path $repo 'dotnet\samples\04-hosting\FoundryHostedAgents\scripts\Add-LocalFrameworkFeed.ps1') `
+ -Path (Join-Path $work 'hosted-harness-data-processing')
+
+Set-Location (Join-Path $work 'hosted-harness-data-processing')
+dotnet build -c Debug --tl:off
+azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME ''
+azd provision
+azd deploy
+azd ai agent invoke 'Which region sold the most units in sales.csv?' `
+ --new-session --new-conversation -o raw
+```
+
+Foundry restores packages and publishes the application with `dependencyResolution: remote_build`. `.agentignore` includes the CSV and local framework feed while excluding secrets, build output, and local session files. Check for `response.completed` in the raw output; a zero CLI exit code alone is not evidence that the turn succeeded.
+
+See [Research's source ZIP instructions](../Hosted-Harness-Research/README.md#deploy-to-foundry-source-zip) for package preparation on Linux or macOS and for updating an existing deployment.
+
+## Related samples
-For deployment to an existing Foundry project, follow the [persistent `azd` container workflow](../Hosted-Harness-Research/README.md#deploy-with-an-existing-foundry-project) with this project's publish output, bundle directory `src\hosted-harness-data`, and agent name `maf-harness-data`. A live hosted validation read the bundled CSV, recalled the answer on a second turn, requested approval before writing, and read the approved file on a later turn. The host kept `AllowStoredOutputEnabled = false`.
+- [Hosted Harness Research](../Hosted-Harness-Research/README.md) uses the same source ZIP preparation.
+- [Hosted Harness Scaling Capabilities](../Hosted-Harness-ScalingCapabilities/README.md) retains a Dockerfile because its skill scripts need Python.
+- [Official source deployment guide](https://learn.microsoft.com/azure/foundry/agents/how-to/deploy-hosted-agent-code).
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/azure.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/azure.yaml
new file mode 100644
index 00000000000..8b0b1ef38e3
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/azure.yaml
@@ -0,0 +1,30 @@
+# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json
+
+name: hosted-harness-data-processing
+services:
+ ai-project:
+ host: azure.ai.project
+ hosted-harness-data-processing:
+ project: .
+ host: azure.ai.agent
+ language: csharp
+ uses:
+ - ai-project
+ codeConfiguration:
+ dependencyResolution: remote_build
+ entryPoint: HostedHarnessDataProcessing.dll
+ runtime: dotnet_10
+ env:
+ ASPNETCORE_URLS: http://+:8088
+ AZURE_AI_MODEL_DEPLOYMENT_NAME: ${AZURE_AI_MODEL_DEPLOYMENT_NAME}
+ container:
+ resources:
+ cpu: "0.5"
+ memory: 1Gi
+ description: |
+ A data-processing harness that reads sales data and writes reports with explicit approval.
+ kind: hosted
+ name: hosted-harness-data-processing
+ protocols:
+ - protocol: responses
+ version: 2.0.0
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/.agentignore b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/.agentignore
new file mode 100644
index 00000000000..cf4a635260a
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/.agentignore
@@ -0,0 +1,20 @@
+# Controls which files are excluded from the source-deploy ZIP (.gitignore syntax).
+azure.yaml
+.agentignore
+
+.env
+.env.*
+.azure/
+.git/
+
+bin/
+obj/
+out/
+*.user
+*.suo
+.vs/
+
+agent-files/
+.checkpoints/
+
+# Include local-feed/ and nuget.config so remote restore uses the prepared framework packages.
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/Dockerfile
deleted file mode 100644
index 97041df053e..00000000000
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/Dockerfile
+++ /dev/null
@@ -1,6 +0,0 @@
-FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine
-WORKDIR /app
-COPY out/ .
-EXPOSE 8088
-ENV ASPNETCORE_URLS=http://+:8088
-ENTRYPOINT ["dotnet", "HostedHarnessResearch.dll"]
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/HostedHarnessResearch.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/HostedHarnessResearch.csproj
index b25103a4f99..c3350d650e5 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/HostedHarnessResearch.csproj
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/HostedHarnessResearch.csproj
@@ -1,24 +1,40 @@
-
+
- net10.0
+ false
+
+
+
+
+
+ net10.0
+
+ enable
enable
$(NoWarn);MAAI001;OPENAI001
+ 1.20.0-preview.260831.1
+
+ true
+ false
-
-
-
+
-
+
+
+
+
+
+
+
-
+
@@ -27,4 +43,7 @@
+
+
+
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/README.md
index 2ef701d854c..1ee99eca9b1 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/README.md
@@ -4,6 +4,16 @@ This sample hosts a research `HarnessAgent` with web search, a public-network br
The project references the framework **source in this repository** so it exercises the local Foundry hosting fix. `AddFoundryResponses(agent)` keeps the default `AllowStoredOutputEnabled = false`. The model must not store a second copy of the response; the harness's local history marker is not a service conversation ID.
+Deployment uses a **source ZIP**, with no Dockerfile or container registry. The preparation step copies the linked browsing tool into a standalone source folder. Foundry restores the packages and publishes the .NET 10 application from that folder.
+
+## Prerequisites
+
+- .NET 10 SDK.
+- An existing Foundry project and model deployment.
+- Azure CLI authenticated with `az login`.
+- Azure Developer CLI (`azd`) with the AI agents extension: `azd extension install azure.ai.agents`.
+- PowerShell 7 for the local framework packaging helper.
+
## Run locally
1. Set `FOUNDRY_PROJECT_ENDPOINT` and `AZURE_AI_MODEL_DEPLOYMENT_NAME` for an existing Foundry model deployment, then sign in with `az login`. Copy `.env.example` to `.env` if you prefer a local file.
@@ -14,45 +24,58 @@ Browsing allows public networks only. The browsing tool checks every redirect ag
Hosted file memory relies on Foundry's session sandbox: by default each caller gets their own session with a private `$HOME` (see [Isolate hosted agent sessions per user](https://learn.microsoft.com/azure/foundry/agents/how-to/isolate-sessions-per-user)). If you [place several users in one session](https://learn.microsoft.com/azure/foundry/agents/how-to/multiplex-session-users), partition the file store per user yourself.
-## Container build from this checkout
+## Deploy to Foundry (source ZIP)
-The project links source code outside this directory and therefore **cannot** be uploaded alone for Foundry source/ZIP deployment. Publish from the MAF checkout first, then build the included runtime Dockerfile from this folder:
+Start in this sample directory. Prepare the source in a working directory **outside the repository** so the upload includes the shared browsing tool without copying its implementation into the checked-in sample:
```powershell
-dotnet publish HostedHarnessResearch.csproj -c Release -f net10.0 -r linux-musl-x64 --self-contained false -o out --tl:off
-docker build -t hosted-harness-research .
+$repo = (Resolve-Path '..\..\..\..\..\..').Path
+$work = Join-Path $env:TEMP 'hosted-harness-research-work'
+$source = Join-Path $work 'source'
+
+New-Item -ItemType Directory -Path $work -Force | Out-Null
+dotnet msbuild HostedHarnessResearch.csproj -target:PrepareSourceDeployment `
+ "-property:SourceDeploymentDirectory=$source"
+
+Set-Location $work
+azd auth login
+azd ai agent init -m (Join-Path $source 'azure.yaml') `
+ -p '' -d ''
```
-The resulting image includes the local framework assemblies. Rebuild the image after changes to the framework; do not substitute currently published packages for this validation.
+`PrepareSourceDeployment` copies the project, local C# files, deployment configuration, and linked browsing files. It does not copy `.env`, local sessions, or build output. The copied project has one target framework and explicit package versions, so it can build without the repository's shared project configuration.
+
+`azd ai agent init` adopts the prepared `azure.yaml` and creates `hosted-harness-research` under `$work`. Passing `-p` selects an existing project rather than provisioning a new one.
-## Deploy with an existing Foundry project
+### Include the local framework fix
-Use a persistent directory **outside the repository** for the `azd` environment. The project and its ContainerRegistry connection must already exist. This flow builds the image remotely through that connection, so the developer does not need direct registry login or a local `docker build`. From this sample directory, after publishing:
+**Do not skip this step when testing this branch.** Published packages may not contain the change that allows the harness to keep local chat history with model storage disabled.
+
+The existing contributor helper packs the local framework, including `Microsoft.Agents.AI.Harness`, into the scaffolded folder. It creates `local-feed/` and `nuget.config` and updates `AgentFrameworkVersion`. Both the feed and configuration travel inside the ZIP.
```powershell
-$state = ''
-$projectId = ''
-$model = ''
-$connection = ''
-$agent = 'maf-harness-research'
-$bundle = Join-Path $state 'src\hosted-harness-research'
-
-New-Item -ItemType Directory -Path (Join-Path $bundle 'out') -Force | Out-Null
-Copy-Item Dockerfile (Join-Path $bundle 'Dockerfile') -Force
-Copy-Item -Path 'out\*' -Destination (Join-Path $bundle 'out') -Recurse -Force
-
-# First deployment only. Keep the generated azure.yaml and .azure directory for later deployments.
-azd ai agent init --no-prompt --kind hosted --deploy-mode container `
- --src $bundle --agent-name $agent --protocol responses `
- --project-id $projectId --model-deployment $model `
- --acr-connection $connection -C $state
-azd deploy $agent -C $state --no-prompt
-azd ai agent invoke $agent 'Name an official weather-data source.' `
- --new-session --new-conversation -C $state --no-prompt
+& (Join-Path $repo 'dotnet\samples\04-hosting\FoundryHostedAgents\scripts\Add-LocalFrameworkFeed.ps1') `
+ -Path (Join-Path $work 'hosted-harness-research')
+
+Set-Location (Join-Path $work 'hosted-harness-research')
+dotnet build -c Debug --tl:off
+azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME ''
+azd provision
+azd deploy
+azd ai agent invoke 'Name an official weather-data source.' `
+ --new-session --new-conversation -o raw
```
-For later changes, re-publish, copy the updated `out\*` files into the same bundle, and run `azd deploy` again. `azd ai agent invoke -o raw` reveals the actual `response.completed` or `response.failed` event; a zero CLI exit code does not prove the turn succeeded.
+Foundry runs `dotnet restore` and `dotnet publish` during deployment because `azure.yaml` sets `dependencyResolution: remote_build`. `.agentignore` excludes secrets, build output, and local session files. No Dockerfile or registry connection is needed.
+
+For Linux or macOS, use the same `dotnet msbuild` preparation command with an absolute destination path and the sibling [`add-local-framework-feed.sh`](../../scripts/add-local-framework-feed.sh) helper after initialization. See [`Hosted-ChatClientAgent`](../Hosted-ChatClientAgent/README.md#deploy-your-local-framework-changes-contributors) for the package helper details.
+
+For later changes, prepare a fresh source folder, copy its `.cs` and `.csproj` files and any `working/` data into the scaffolded agent directory, rerun the package helper, and run `azd deploy` from that directory. Keep the scaffolded `azure.yaml` and `.azure` environment rather than replacing them with the template. Check the raw terminal event for `response.completed` or `response.failed`; a zero CLI exit code does not prove the turn succeeded.
**Stateless web-search history:** The OpenAI chat adapter returns hosted search results with a raw `WebSearchCallResponseItem`. After another local function call, replaying that raw item to the Foundry model with `store=false` caused HTTP 400 `invalid_payload`. `StatelessWebSearchChatClient` omits only the raw search result from the *next model request*, leaving the original session history, assistant text, local function calls, and citations intact. The model does not receive that raw search metadata again, so later turns must rely on the cited assistant text for source details.
-**Live validation:** On tao-cace, the same compound request that failed twice before this filter completed in two new hosted sessions with a cited source, a completed todo, and a saved report. A subsequent turn read the report from file memory. The previously failing plan-then-execute two-turn sequence also completed. Hosted web search remains enabled; `AllowStoredOutputEnabled` remains `false`. Check the raw terminal event when repeating this scenario: `azd` can exit zero even if a response failed.
+## Related samples
+
+- [Hosted Harness Data Processing](../Hosted-Harness-DataProcessing/README.md) also uses source ZIP deployment.
+- [Hosted Harness Scaling Capabilities](../Hosted-Harness-ScalingCapabilities/README.md) retains a Dockerfile to install Python for its skill scripts.
+- [Official source deployment guide](https://learn.microsoft.com/azure/foundry/agents/how-to/deploy-hosted-agent-code).
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/azure.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/azure.yaml
new file mode 100644
index 00000000000..e5a1a79cf50
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/azure.yaml
@@ -0,0 +1,30 @@
+# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json
+
+name: hosted-harness-research
+services:
+ ai-project:
+ host: azure.ai.project
+ hosted-harness-research:
+ project: .
+ host: azure.ai.agent
+ language: csharp
+ uses:
+ - ai-project
+ codeConfiguration:
+ dependencyResolution: remote_build
+ entryPoint: HostedHarnessResearch.dll
+ runtime: dotnet_10
+ env:
+ ASPNETCORE_URLS: http://+:8088
+ AZURE_AI_MODEL_DEPLOYMENT_NAME: ${AZURE_AI_MODEL_DEPLOYMENT_NAME}
+ container:
+ resources:
+ cpu: "0.5"
+ memory: 1Gi
+ description: |
+ A research harness with web search, browsing, planning, a todo loop, and session-scoped file memory.
+ kind: hosted
+ name: hosted-harness-research
+ protocols:
+ - protocol: responses
+ version: 2.0.0
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/README.md
index 349d018a95e..c8726d86707 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/README.md
@@ -4,6 +4,8 @@ This sample hosts the finance harness from `dotnet/samples/02-agents/Harness/Bui
It shares the research sample's `StatelessWebSearchChatClient`: hosted search stays available to the main harness and its background agent, but the raw search result is not replayed into later stateless model requests. The original result remains in the session history.
+Unlike [Research](../Hosted-Harness-Research/README.md) and [Data Processing](../Hosted-Harness-DataProcessing/README.md), this sample retains **Dockerfile deployment**. Its valuation and risk-scoring skill scripts run through `SubprocessScriptRunner`, which launches Python 3. The Dockerfile installs that interpreter; Foundry's documented `dotnet_10` source runtime does not guarantee Python is available.
+
## Run locally
1. Set `FOUNDRY_PROJECT_ENDPOINT` and `AZURE_AI_MODEL_DEPLOYMENT_NAME` for an existing Foundry model deployment, then sign in with `az login`. Copy `.env.example` to `.env` if preferred.
@@ -16,15 +18,43 @@ The working folder and file memory rely on Foundry's session sandbox: by default
`ENABLE_HYPERLIGHT_CODEACT=true` adds CodeAct where hardware virtualization is available. It is off by default because hosted containers typically do not provide nested virtualization; requesting it on an unsupported host fails explicitly. Optional toolbox skills use `TOOLBOX_MCP_SERVER_URL`. File skill scripts require Python 3 (included in the container image).
+The optional Hyperlight provider does not replace the skill script runner. Enabling it still leaves the skill scripts using the container's Python interpreter.
+
## Container build from this checkout
The source, skills and demonstration data link to other MAF sample directories. Publish from the complete checkout and build the runtime image, not from an isolated upload of this directory:
```powershell
-dotnet publish HostedHarnessScalingCapabilities.csproj -c Release -f net10.0 -r linux-musl-x64 --self-contained false -o out --tl:off
+dotnet publish HostedHarnessScalingCapabilities.csproj -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out --tl:off
docker build -t hosted-harness-scaling .
```
The image includes the local framework fix. Rebuild after changing the framework; an older published package will still reject the local history marker.
-For deployment to an existing Foundry project, follow the [persistent `azd` container workflow](../Hosted-Harness-Research/README.md#deploy-with-an-existing-foundry-project) with this project's publish output, bundle directory `src\hosted-harness-scaling`, and agent name `maf-harness-scaling`. A hosted validation on the updated agent version confirmed the stock tool, multi-turn history, skill loading, a Python risk-scoring script **only after explicit approval**, and a confined read-only shell command **only after explicit approval**. Hyperlight CodeAct was not enabled on the hosted container because nested virtualization was unavailable.
+## Deploy with an existing Foundry project
+
+Use a persistent directory **outside the repository** for the `azd` environment. The Foundry project, model deployment, and ContainerRegistry connection must already exist. After publishing from this sample directory, copy the runtime output and Dockerfile into the deployment folder:
+
+```powershell
+$state = ''
+$projectId = ''
+$model = ''
+$connection = ''
+$agent = 'maf-harness-scaling'
+$bundle = Join-Path $state 'src\hosted-harness-scaling'
+
+New-Item -ItemType Directory -Path (Join-Path $bundle 'out') -Force | Out-Null
+Copy-Item Dockerfile (Join-Path $bundle 'Dockerfile') -Force
+Copy-Item -Path 'out\*' -Destination (Join-Path $bundle 'out') -Recurse -Force
+
+# First deployment only. Keep azure.yaml and .azure for later deployments.
+azd ai agent init --no-prompt --kind hosted --deploy-mode container `
+ --src $bundle --agent-name $agent --protocol responses `
+ --project-id $projectId --model-deployment $model `
+ --acr-connection $connection -C $state
+azd deploy $agent -C $state --no-prompt
+azd ai agent invoke $agent 'What is the current stock price of MSFT?' `
+ --new-session --new-conversation -o raw -C $state --no-prompt
+```
+
+This flow builds the image remotely through the registry connection. For later changes, republish, copy the updated output into the same deployment folder, and run `azd deploy` again. Check the raw terminal event for `response.completed` or `response.failed`; a zero CLI exit code does not prove the turn succeeded.
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1 b/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1
index 75ba7f25ca4..c61884be751 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1
@@ -56,6 +56,7 @@ $frameworkProjects = @(
'Microsoft.Agents.AI'
'Microsoft.Agents.AI.Workflows'
'Microsoft.Agents.AI.Hosting'
+ 'Microsoft.Agents.AI.Harness'
'Microsoft.Agents.AI.LocalCodeAct'
'Microsoft.Agents.AI.Mcp'
'Microsoft.Agents.AI.Foundry'
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/SourceDeployment.targets b/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/SourceDeployment.targets
new file mode 100644
index 00000000000..811afffa8b7
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/SourceDeployment.targets
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+ <_SourceDeploymentDirectory>$([MSBuild]::NormalizeDirectory('$(SourceDeploymentDirectory)'))
+ <_SourceDeploymentProjectDirectory>$([MSBuild]::NormalizeDirectory('$(MSBuildProjectDirectory)'))
+
+
+
+
+ <_SourceDeploymentRootFile Include="*.cs;$(MSBuildProjectFile);azure.yaml;.agentignore;.env.example;README.md" />
+
+ <_SourceDeploymentLinkedFile Include="@(Compile->WithMetadataValue('DefiningProjectFullPath', '$(MSBuildProjectFullPath)'))" />
+ <_SourceDeploymentLinkedFile Include="@(Content->WithMetadataValue('DefiningProjectFullPath', '$(MSBuildProjectFullPath)'))" />
+ <_SourceDeploymentLinkedFile Remove="@(_SourceDeploymentLinkedFile)" Condition="'%(_SourceDeploymentLinkedFile.Link)' == ''" />
+
+
+
+
+
+
+
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/add-local-framework-feed.sh b/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/add-local-framework-feed.sh
index 71c6043c341..3a9ac98a0a3 100755
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/add-local-framework-feed.sh
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/add-local-framework-feed.sh
@@ -44,6 +44,7 @@ framework_projects=(
Microsoft.Agents.AI
Microsoft.Agents.AI.Workflows
Microsoft.Agents.AI.Hosting
+ Microsoft.Agents.AI.Harness
Microsoft.Agents.AI.LocalCodeAct
Microsoft.Agents.AI.Mcp
Microsoft.Agents.AI.Foundry
From 0d3f9e428fa8b83b597976e7fdebab59d87ec7c1 Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Wed, 28 Oct 2026 12:08:16 -0700
Subject: [PATCH 4/5] fix(samples): keep only hosted Harness Research
---
dotnet/agent-framework-dotnet.slnx | 6 -
dotnet/samples/02-agents/Harness/README.md | 6 +-
.../.agentignore | 19 --
.../.env.example | 4 -
.../Hosted-Harness-DataProcessing/.gitignore | 3 -
.../HostedHarnessDataProcessing.csproj | 49 -----
.../Hosted-Harness-DataProcessing/Program.cs | 89 ---------
.../Hosted-Harness-DataProcessing/README.md | 71 --------
.../Hosted-Harness-DataProcessing/azure.yaml | 30 ----
.../Hosted-Harness-Research/README.md | 7 +-
.../.env.example | 7 -
.../.gitignore | 3 -
.../Dockerfile | 7 -
.../HostedHarnessScalingCapabilities.csproj | 44 -----
.../Program.cs | 170 ------------------
.../README.md | 60 -------
16 files changed, 5 insertions(+), 570 deletions(-)
delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/.agentignore
delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/.env.example
delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/.gitignore
delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/HostedHarnessDataProcessing.csproj
delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/Program.cs
delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/README.md
delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/azure.yaml
delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/.env.example
delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/.gitignore
delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/Dockerfile
delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/HostedHarnessScalingCapabilities.csproj
delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/Program.cs
delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/README.md
diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index 9d2b34729b6..4b4abd7b25f 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -342,15 +342,9 @@
-
-
-
-
-
-
diff --git a/dotnet/samples/02-agents/Harness/README.md b/dotnet/samples/02-agents/Harness/README.md
index df65f177a69..51c03e47fb6 100644
--- a/dotnet/samples/02-agents/Harness/README.md
+++ b/dotnet/samples/02-agents/Harness/README.md
@@ -22,15 +22,13 @@ Samples accompanying the [*Build your own agent harness or claw with Microsoft A
| [Claw_Step03_ScalingCapabilities](./BuildYourOwnClaw/Claw_Step03_ScalingCapabilities/README.md) | Post 3 — scaling the claw with skills (plus optional Foundry skills), a confined shell, CodeAct, and background agents |
| [Claw_Step04_ProductionReady](./BuildYourOwnClaw/Claw_Step04_ProductionReady/README.md) | Post 4 — production-ready shared claw library with observability, opt-in Purview governance, Foundry hosted deployment, and evals |
-## Foundry hosted Harness samples
+## Foundry hosted Harness sample
-Samples that host a `HarnessAgent` as a Foundry hosted agent with the default `AllowStoredOutputEnabled = false`, so the model service never stores the conversation.
+The Research sample hosts a `HarnessAgent` as a Foundry hosted agent using source ZIP deployment with the default `AllowStoredOutputEnabled = false`, so the model service never stores the conversation.
| Sample | Description |
| --- | --- |
| [Hosted-Harness-Research](../../04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/README.md) | Hosts the research harness with web search, a browsing tool, planning, a todo loop, and session-scoped file memory |
-| [Hosted-Harness-DataProcessing](../../04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/README.md) | Hosts the data processing harness that reads a sales CSV and writes reports, with approval-gated file writes that resume on a later turn |
-| [Hosted-Harness-ScalingCapabilities](../../04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/README.md) | Hosts the Claw_Step03 finance harness with skills, a confined approval-gated shell, a background research agent, and simulated trade approvals |
## Security Considerations
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/.agentignore b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/.agentignore
deleted file mode 100644
index ef27c4a15fc..00000000000
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/.agentignore
+++ /dev/null
@@ -1,19 +0,0 @@
-# Controls which files are excluded from the source-deploy ZIP (.gitignore syntax).
-azure.yaml
-.agentignore
-
-.env
-.env.*
-.azure/
-.git/
-
-bin/
-obj/
-out/
-*.user
-*.suo
-.vs/
-
-.checkpoints/
-
-# Include working/sales.csv, local-feed/, and nuget.config in the upload.
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/.env.example b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/.env.example
deleted file mode 100644
index f2ab64ba92e..00000000000
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/.env.example
+++ /dev/null
@@ -1,4 +0,0 @@
-FOUNDRY_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/
-AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-5.4-mini
-AZURE_TOKEN_CREDENTIALS=dev
-ASPNETCORE_URLS=http://localhost:8088
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/.gitignore b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/.gitignore
deleted file mode 100644
index d3fe066dacc..00000000000
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-.env
-out/
-.checkpoints/
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/HostedHarnessDataProcessing.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/HostedHarnessDataProcessing.csproj
deleted file mode 100644
index b92c016a4bd..00000000000
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/HostedHarnessDataProcessing.csproj
+++ /dev/null
@@ -1,49 +0,0 @@
-
-
-
- false
-
-
-
-
-
- net10.0
-
- enable
- enable
- $(NoWarn);MAAI001;OPENAI001
- 1.20.0-preview.260831.1
-
- true
- false
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/Program.cs
deleted file mode 100644
index f31d74c3103..00000000000
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/Program.cs
+++ /dev/null
@@ -1,89 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-// A hosted data-processing harness with approval-gated file writes and read-only auto-approval.
-
-using System.ClientModel.Primitives;
-using Azure.AI.Projects;
-using Azure.Identity;
-using DotNetEnv;
-using Microsoft.Agents.AI;
-using Microsoft.Agents.AI.Foundry.Hosting;
-using Microsoft.Extensions.AI;
-
-Env.NoClobber().TraversePath().Load();
-
-string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
- ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
-string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME")
- ?? throw new InvalidOperationException("AZURE_AI_MODEL_DEPLOYMENT_NAME is not set.");
-
-string seedDir = Path.Combine(AppContext.BaseDirectory, "working");
-string workingDir;
-if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("FOUNDRY_HOSTING_ENVIRONMENT")))
-{
- // Preserve files edited in prior turns; only seed files that do not yet exist in the
- // per-session writable home. The application directory is read-only in Foundry.
- string home = System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile);
- Directory.SetCurrentDirectory(home);
- workingDir = Path.Combine(home, "working");
- foreach (string file in Directory.GetFiles(seedDir, "*", SearchOption.AllDirectories))
- {
- string destination = Path.Combine(workingDir, Path.GetRelativePath(seedDir, file));
- if (!File.Exists(destination))
- {
- Directory.CreateDirectory(Path.GetDirectoryName(destination)!);
- File.Copy(file, destination);
- }
- }
-}
-else
-{
- workingDir = seedDir;
-}
-
-const int MaxContextWindowTokens = 1_050_000;
-const int MaxOutputTokens = 128_000;
-
-IChatClient chatClient = new AIProjectClient(
- new Uri(endpoint),
- new DefaultAzureCredential(),
- new AIProjectClientOptions { RetryPolicy = new ClientRetryPolicy(3) })
- .GetProjectOpenAIClient()
- .GetProjectResponsesClientForModel(deploymentName)
- .AsIChatClient();
-
-AIAgent agent = chatClient.AsHarnessAgent(new HarnessAgentOptions
-{
- Name = "hosted-harness-data-processing",
- Description = "Reads and analyzes CSV files and writes reports with approval.",
- MaxContextWindowTokens = MaxContextWindowTokens,
- MaxOutputTokens = MaxOutputTokens,
- FileAccessStore = new FileSystemAgentFileStore(workingDir),
- ToolApprovalAgentOptions = new ToolApprovalAgentOptions
- {
- AutoApprovalRules = [FileAccessProvider.ReadOnlyToolsAutoApprovalRule],
- },
- DisableTodoProvider = true,
- DisableAgentModeProvider = true,
- DisableFileMemory = true,
- DisableWebSearch = true,
- ChatOptions = new ChatOptions
- {
- ModelId = deploymentName,
- MaxOutputTokens = MaxOutputTokens,
- Instructions =
- """
- You analyze data files in the working folder. List and read the files before answering
- questions about their contents. Present calculations and findings clearly. When asked
- for a report, write it with the file access tools, but never overwrite source data
- unless the user explicitly asks you to.
- """,
- },
-});
-
-var builder = WebApplication.CreateBuilder(args);
-builder.Services.AddFoundryResponses(agent);
-
-var app = builder.Build();
-app.MapFoundryResponses();
-app.Run();
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/README.md
deleted file mode 100644
index f373ab83f84..00000000000
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/README.md
+++ /dev/null
@@ -1,71 +0,0 @@
-# Hosted Harness Data Processing
-
-This sample hosts a `HarnessAgent` that reads a sample sales CSV and writes requested reports. It reuses `dotnet/samples/02-agents/Harness/Harness_Step03_DataProcessing/working/sales.csv`. File reads are auto-approved; writes still require an explicit approval that can resume on a later hosted turn.
-
-The project references the framework **source in this repository**. `AddFoundryResponses(agent)` uses the default `AllowStoredOutputEnabled = false`; the agent keeps chat history in its session rather than in the model service.
-
-Deployment uses a **source ZIP**, with no Dockerfile or container registry. The preparation step includes the linked sales CSV in a standalone source folder. No Python interpreter is needed.
-
-## Prerequisites
-
-- .NET 10 SDK.
-- An existing Foundry project and model deployment.
-- Azure CLI authenticated with `az login`.
-- Azure Developer CLI (`azd`) with the AI agents extension: `azd extension install azure.ai.agents`.
-- PowerShell 7 for the local framework packaging helper.
-
-## Run locally
-
-1. Set `FOUNDRY_PROJECT_ENDPOINT` and `AZURE_AI_MODEL_DEPLOYMENT_NAME` for an existing Foundry model deployment, then sign in with `az login`. Copy `.env.example` to `.env` if you prefer a local file.
-2. From this directory, run `dotnet run --tl:off`, then call `POST http://localhost:8088/responses` with model `hosted-harness-data-processing` and a question such as "Which region sold the most units in sales.csv?".
-3. Send another request with the same conversation ID. To exercise approval, ask for a written report. The response contains an `mcp_approval_request`; send an `mcp_approval_response` with its ID in the **same conversation and hosted session**. Do not auto-approve writes. `azd ai agent invoke -f` does not send a structured Responses body; use authenticated `az rest --resource https://ai.azure.com` for the approval response.
-
-Hosted containers have a read-only application directory. On startup the sample copies **only missing** seed files into the session's writable home directory so edited files and generated reports survive later turns.
-
-These files rely on Foundry's session sandbox: by default each caller gets their own session with a private `$HOME` (see [Isolate hosted agent sessions per user](https://learn.microsoft.com/azure/foundry/agents/how-to/isolate-sessions-per-user)). If you [place several users in one session](https://learn.microsoft.com/azure/foundry/agents/how-to/multiplex-session-users), partition the working folder per user yourself.
-
-## Deploy to Foundry (source ZIP)
-
-Start in this sample directory. Prepare the source in a working directory **outside the repository**, then initialize against an existing Foundry project:
-
-```powershell
-$repo = (Resolve-Path '..\..\..\..\..\..').Path
-$work = Join-Path $env:TEMP 'hosted-harness-data-processing-work'
-$source = Join-Path $work 'source'
-
-New-Item -ItemType Directory -Path $work -Force | Out-Null
-dotnet msbuild HostedHarnessDataProcessing.csproj -target:PrepareSourceDeployment `
- "-property:SourceDeploymentDirectory=$source"
-
-Set-Location $work
-azd auth login
-azd ai agent init -m (Join-Path $source 'azure.yaml') `
- -p '' -d ''
-```
-
-`PrepareSourceDeployment` includes `working/sales.csv` from the console sample and does not copy `.env`, local sessions, or build output. The copied project has one target framework and explicit package versions. Its content settings copy the CSV into the published application.
-
-**Do not skip the package helper when testing this branch.** It includes the local framework fix rather than relying on an older published package:
-
-```powershell
-& (Join-Path $repo 'dotnet\samples\04-hosting\FoundryHostedAgents\scripts\Add-LocalFrameworkFeed.ps1') `
- -Path (Join-Path $work 'hosted-harness-data-processing')
-
-Set-Location (Join-Path $work 'hosted-harness-data-processing')
-dotnet build -c Debug --tl:off
-azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME ''
-azd provision
-azd deploy
-azd ai agent invoke 'Which region sold the most units in sales.csv?' `
- --new-session --new-conversation -o raw
-```
-
-Foundry restores packages and publishes the application with `dependencyResolution: remote_build`. `.agentignore` includes the CSV and local framework feed while excluding secrets, build output, and local session files. Check for `response.completed` in the raw output; a zero CLI exit code alone is not evidence that the turn succeeded.
-
-See [Research's source ZIP instructions](../Hosted-Harness-Research/README.md#deploy-to-foundry-source-zip) for package preparation on Linux or macOS and for updating an existing deployment.
-
-## Related samples
-
-- [Hosted Harness Research](../Hosted-Harness-Research/README.md) uses the same source ZIP preparation.
-- [Hosted Harness Scaling Capabilities](../Hosted-Harness-ScalingCapabilities/README.md) retains a Dockerfile because its skill scripts need Python.
-- [Official source deployment guide](https://learn.microsoft.com/azure/foundry/agents/how-to/deploy-hosted-agent-code).
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/azure.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/azure.yaml
deleted file mode 100644
index 8b0b1ef38e3..00000000000
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-DataProcessing/azure.yaml
+++ /dev/null
@@ -1,30 +0,0 @@
-# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json
-
-name: hosted-harness-data-processing
-services:
- ai-project:
- host: azure.ai.project
- hosted-harness-data-processing:
- project: .
- host: azure.ai.agent
- language: csharp
- uses:
- - ai-project
- codeConfiguration:
- dependencyResolution: remote_build
- entryPoint: HostedHarnessDataProcessing.dll
- runtime: dotnet_10
- env:
- ASPNETCORE_URLS: http://+:8088
- AZURE_AI_MODEL_DEPLOYMENT_NAME: ${AZURE_AI_MODEL_DEPLOYMENT_NAME}
- container:
- resources:
- cpu: "0.5"
- memory: 1Gi
- description: |
- A data-processing harness that reads sales data and writes reports with explicit approval.
- kind: hosted
- name: hosted-harness-data-processing
- protocols:
- - protocol: responses
- version: 2.0.0
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/README.md
index 1ee99eca9b1..258b5ceec20 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-Research/README.md
@@ -70,12 +70,11 @@ Foundry runs `dotnet restore` and `dotnet publish` during deployment because `az
For Linux or macOS, use the same `dotnet msbuild` preparation command with an absolute destination path and the sibling [`add-local-framework-feed.sh`](../../scripts/add-local-framework-feed.sh) helper after initialization. See [`Hosted-ChatClientAgent`](../Hosted-ChatClientAgent/README.md#deploy-your-local-framework-changes-contributors) for the package helper details.
-For later changes, prepare a fresh source folder, copy its `.cs` and `.csproj` files and any `working/` data into the scaffolded agent directory, rerun the package helper, and run `azd deploy` from that directory. Keep the scaffolded `azure.yaml` and `.azure` environment rather than replacing them with the template. Check the raw terminal event for `response.completed` or `response.failed`; a zero CLI exit code does not prove the turn succeeded.
+For later changes, prepare a fresh source folder, copy its `.cs` and `.csproj` files into the scaffolded agent directory, rerun the package helper, and run `azd deploy` from that directory. Keep the scaffolded `azure.yaml` and `.azure` environment rather than replacing them with the template. Check the raw terminal event for `response.completed` or `response.failed`; a zero CLI exit code does not prove the turn succeeded.
**Stateless web-search history:** The OpenAI chat adapter returns hosted search results with a raw `WebSearchCallResponseItem`. After another local function call, replaying that raw item to the Foundry model with `store=false` caused HTTP 400 `invalid_payload`. `StatelessWebSearchChatClient` omits only the raw search result from the *next model request*, leaving the original session history, assistant text, local function calls, and citations intact. The model does not receive that raw search metadata again, so later turns must rely on the cited assistant text for source details.
-## Related samples
+## Related resources
-- [Hosted Harness Data Processing](../Hosted-Harness-DataProcessing/README.md) also uses source ZIP deployment.
-- [Hosted Harness Scaling Capabilities](../Hosted-Harness-ScalingCapabilities/README.md) retains a Dockerfile to install Python for its skill scripts.
+- [Research console sample](../../../../02-agents/Harness/Harness_Step01_Research/README.md).
- [Official source deployment guide](https://learn.microsoft.com/azure/foundry/agents/how-to/deploy-hosted-agent-code).
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/.env.example b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/.env.example
deleted file mode 100644
index 2f559af9929..00000000000
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/.env.example
+++ /dev/null
@@ -1,7 +0,0 @@
-FOUNDRY_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/
-AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-5.4-mini
-AZURE_TOKEN_CREDENTIALS=dev
-ASPNETCORE_URLS=http://localhost:8088
-# Optional, requires hardware virtualization:
-# ENABLE_HYPERLIGHT_CODEACT=true
-# TOOLBOX_MCP_SERVER_URL=https:///mcp
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/.gitignore b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/.gitignore
deleted file mode 100644
index d3fe066dacc..00000000000
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-.env
-out/
-.checkpoints/
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/Dockerfile
deleted file mode 100644
index 936987b77a1..00000000000
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/Dockerfile
+++ /dev/null
@@ -1,7 +0,0 @@
-FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine
-RUN apk add --no-cache python3
-WORKDIR /app
-COPY out/ .
-EXPOSE 8088
-ENV ASPNETCORE_URLS=http://+:8088
-ENTRYPOINT ["dotnet", "HostedHarnessScalingCapabilities.dll"]
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/HostedHarnessScalingCapabilities.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/HostedHarnessScalingCapabilities.csproj
deleted file mode 100644
index b62b689a3c1..00000000000
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/HostedHarnessScalingCapabilities.csproj
+++ /dev/null
@@ -1,44 +0,0 @@
-
-
-
- net10.0
- enable
- $(NoWarn);MAAI001;OPENAI001
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/Program.cs
deleted file mode 100644
index dcb8fce0570..00000000000
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/Program.cs
+++ /dev/null
@@ -1,170 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-// Host the finance harness from Claw_Step03_ScalingCapabilities with local skills,
-// a confined shell, approval-gated trades, background research, and optional CodeAct.
-
-using System.ClientModel.Primitives;
-using Azure.AI.Projects;
-using Azure.Identity;
-using ClawSample;
-using DotNetEnv;
-using HyperlightSandbox.Guest.Python;
-using Microsoft.Agents.AI;
-using Microsoft.Agents.AI.Foundry.Hosting;
-using Microsoft.Agents.AI.Hyperlight;
-using Microsoft.Agents.AI.Tools.Shell;
-using Microsoft.Extensions.AI;
-using SampleApp;
-
-Env.NoClobber().TraversePath().Load();
-
-string endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
- ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
-string deploymentName = Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME")
- ?? throw new InvalidOperationException("AZURE_AI_MODEL_DEPLOYMENT_NAME is not set.");
-
-bool isHosted = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("FOUNDRY_HOSTING_ENVIRONMENT"));
-string seedDir = Path.Combine(AppContext.BaseDirectory, "working");
-string skillsDir = Path.Combine(AppContext.BaseDirectory, "skills");
-string workingDir = isHosted
- ? Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile), "working")
- : seedDir;
-
-if (isHosted)
-{
- // Foundry mounts this home per session. Seed only missing files so previous edits,
- // reports, and reorganized confirmations survive the next turn or container restart.
- Directory.SetCurrentDirectory(System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile));
- foreach (string file in Directory.GetFiles(seedDir, "*", SearchOption.AllDirectories))
- {
- string destination = Path.Combine(workingDir, Path.GetRelativePath(seedDir, file));
- if (!File.Exists(destination))
- {
- Directory.CreateDirectory(Path.GetDirectoryName(destination)!);
- File.Copy(file, destination);
- }
- }
-}
-
-string vaultDir = Path.Combine(workingDir, "confirmations");
-var credential = new DefaultAzureCredential(new DefaultAzureCredentialOptions
-{
- ExcludeManagedIdentityCredential = !isHosted,
-});
-IChatClient modelClient = new AIProjectClient(
- new Uri(endpoint),
- credential,
- new AIProjectClientOptions { RetryPolicy = new ClientRetryPolicy(3) })
- .GetProjectOpenAIClient()
- .GetProjectResponsesClientForModel(deploymentName)
- .AsIChatClient();
-IChatClient chatClient = new StatelessWebSearchChatClient(modelClient);
-
-var skillsBuilder = new AgentSkillsProviderBuilder()
- .UseFileSkills([skillsDir], scriptRunner: new SubprocessScriptRunner().RunAsync);
-
-HttpClient? toolboxHttpClient = null;
-ModelContextProtocol.Client.McpClient? toolboxMcpClient = null;
-string? toolboxUrl = Environment.GetEnvironmentVariable("TOOLBOX_MCP_SERVER_URL");
-if (!string.IsNullOrWhiteSpace(toolboxUrl))
-{
- (toolboxMcpClient, toolboxHttpClient) = await FoundrySkills.ConnectAsync(toolboxUrl, credential);
- skillsBuilder.UseMcpSkills(toolboxMcpClient);
-}
-
-AgentSkillsProvider skillsProvider = skillsBuilder.Build();
-AIAgent researchAgent = ResearchAgent.Create(chatClient);
-
-// The policy filters common mistakes, not adversarial shell input. The hosted
-// container is the isolation boundary; every shell invocation still requires approval.
-await using var shell = new LocalShellExecutor(new LocalShellExecutorOptions
-{
- WorkingDirectory = vaultDir,
- ConfineWorkingDirectory = true,
- Policy = new ShellPolicy(denyList:
- [
- @"\brm\s+-rf\b",
- @"\bsudo\b",
- @":\(\)\s*\{",
- @"\bmkfs\b",
- @">\s*/dev/sd",
- ]),
- Timeout = TimeSpan.FromSeconds(15),
-});
-
-// Hosted containers may not expose nested virtualization. Enable CodeAct only
-// on a host that supports Hyperlight rather than silently dropping failures.
-HyperlightCodeActProvider? codeAct = null;
-if (string.Equals(Environment.GetEnvironmentVariable("ENABLE_HYPERLIGHT_CODEACT"), "true", StringComparison.OrdinalIgnoreCase))
-{
- codeAct = new HyperlightCodeActProvider(
- HyperlightCodeActProviderOptions.CreateForWasm(PythonGuestModule.GetModulePath()));
-}
-
-List providers = [skillsProvider, new ShellEnvironmentProvider(shell)];
-if (codeAct is not null)
-{
- providers.Add(codeAct);
-}
-
-AIAgent agent = chatClient.AsHarnessAgent(new HarnessAgentOptions
-{
- Name = "hosted-harness-scaling-capabilities",
- Description = "Analyzes a sample portfolio with skills, shell, CodeAct, and background research.",
- FileAccessStore = new FileSystemAgentFileStore(workingDir),
- FileMemoryStore = new FileSystemAgentFileStore(Path.Combine(workingDir, "agent-file-memory")),
- DisableAgentSkillsProvider = true,
- BackgroundAgents = [researchAgent],
- ToolApprovalAgentOptions = new ToolApprovalAgentOptions
- {
- AutoApprovalRules =
- [
- FileAccessProvider.ReadOnlyToolsAutoApprovalRule,
- AgentSkillsProvider.ReadOnlyToolsAutoApprovalRule,
- ],
- },
- AgentModeProviderOptions = new AgentModeProviderOptions { DefaultMode = "execute" },
- AIContextProviders = providers,
- ChatOptions = new ChatOptions
- {
- ModelId = deploymentName,
- Instructions =
- """
- You help users examine a sample stock portfolio. Read portfolio.csv before answering
- about holdings. Load the valuation and risk-scoring skills for relevant questions.
- Delegate independent ticker research to the background agent. To organize trade
- confirmations, inspect the vault first, then use the approved shell tool to copy
- the originals into organized/year/month without changing the source files.
- Explain any simulated trade before calling place_trade. You provide information,
- not personalized investment advice.
- """,
- Tools =
- [
- StockTools.CreateGetStockPriceTool(),
- TradingTools.CreatePlaceTradeTool(),
- shell.AsAIFunction(requireApproval: true),
- ],
- Reasoning = new() { Effort = ReasoningEffort.Medium },
- },
-});
-
-var builder = WebApplication.CreateBuilder(args);
-builder.Services.AddFoundryResponses(agent);
-
-var app = builder.Build();
-app.MapFoundryResponses();
-
-try
-{
- await app.RunAsync();
-}
-finally
-{
- codeAct?.Dispose();
- if (toolboxMcpClient is not null)
- {
- await toolboxMcpClient.DisposeAsync().ConfigureAwait(false);
- }
-
- toolboxHttpClient?.Dispose();
-}
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/README.md
deleted file mode 100644
index c8726d86707..00000000000
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Harness-ScalingCapabilities/README.md
+++ /dev/null
@@ -1,60 +0,0 @@
-# Hosted Harness Scaling Capabilities
-
-This sample hosts the finance harness from `dotnet/samples/02-agents/Harness/BuildYourOwnClaw/Claw_Step03_ScalingCapabilities`. Its existing stock tools, simulated trade approval, file skills and scripts, optional toolbox skills, confined shell, background research agent, and demo files are linked from that console example. The `HostedHarnessScalingCapabilities` project uses local framework source and does not set `AllowStoredOutputEnabled`; the hosted platform records the turn while the model service receives `store=false`.
-
-It shares the research sample's `StatelessWebSearchChatClient`: hosted search stays available to the main harness and its background agent, but the raw search result is not replayed into later stateless model requests. The original result remains in the session history.
-
-Unlike [Research](../Hosted-Harness-Research/README.md) and [Data Processing](../Hosted-Harness-DataProcessing/README.md), this sample retains **Dockerfile deployment**. Its valuation and risk-scoring skill scripts run through `SubprocessScriptRunner`, which launches Python 3. The Dockerfile installs that interpreter; Foundry's documented `dotnet_10` source runtime does not guarantee Python is available.
-
-## Run locally
-
-1. Set `FOUNDRY_PROJECT_ENDPOINT` and `AZURE_AI_MODEL_DEPLOYMENT_NAME` for an existing Foundry model deployment, then sign in with `az login`. Copy `.env.example` to `.env` if preferred.
-2. From this directory, run `dotnet run --tl:off`. Call `POST http://localhost:8088/responses` with model `hosted-harness-scaling-capabilities`. Ask about `portfolio.csv` or a stock price, then send another turn with the same conversation ID.
-3. Ask for a simulated trade or to reorganize the confirmations. The shell, trade and skill-script functions require explicit approval. Read-only files and skill loading are auto-approved. To resume, send the returned `mcp_approval_request.id` in an `mcp_approval_response` with the same conversation and hosted session. `azd ai agent invoke -f` sends a text message rather than a structured Responses body; use authenticated `az rest --resource https://ai.azure.com` for approvals.
-
-Foundry's application directory is read-only. The sample seeds only missing files into the per-session writable home, keeping previously edited portfolio files, reports and confirmations. The shell is confined to the confirmations folder; its deny-list is **not** a security boundary. Run the image only in the externally isolated hosted container.
-
-The working folder and file memory rely on Foundry's session sandbox: by default each caller gets their own session with a private `$HOME` (see [Isolate hosted agent sessions per user](https://learn.microsoft.com/azure/foundry/agents/how-to/isolate-sessions-per-user)). If you [place several users in one session](https://learn.microsoft.com/azure/foundry/agents/how-to/multiplex-session-users), partition both stores per user yourself.
-
-`ENABLE_HYPERLIGHT_CODEACT=true` adds CodeAct where hardware virtualization is available. It is off by default because hosted containers typically do not provide nested virtualization; requesting it on an unsupported host fails explicitly. Optional toolbox skills use `TOOLBOX_MCP_SERVER_URL`. File skill scripts require Python 3 (included in the container image).
-
-The optional Hyperlight provider does not replace the skill script runner. Enabling it still leaves the skill scripts using the container's Python interpreter.
-
-## Container build from this checkout
-
-The source, skills and demonstration data link to other MAF sample directories. Publish from the complete checkout and build the runtime image, not from an isolated upload of this directory:
-
-```powershell
-dotnet publish HostedHarnessScalingCapabilities.csproj -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out --tl:off
-docker build -t hosted-harness-scaling .
-```
-
-The image includes the local framework fix. Rebuild after changing the framework; an older published package will still reject the local history marker.
-
-## Deploy with an existing Foundry project
-
-Use a persistent directory **outside the repository** for the `azd` environment. The Foundry project, model deployment, and ContainerRegistry connection must already exist. After publishing from this sample directory, copy the runtime output and Dockerfile into the deployment folder:
-
-```powershell
-$state = ''
-$projectId = ''
-$model = ''
-$connection = ''
-$agent = 'maf-harness-scaling'
-$bundle = Join-Path $state 'src\hosted-harness-scaling'
-
-New-Item -ItemType Directory -Path (Join-Path $bundle 'out') -Force | Out-Null
-Copy-Item Dockerfile (Join-Path $bundle 'Dockerfile') -Force
-Copy-Item -Path 'out\*' -Destination (Join-Path $bundle 'out') -Recurse -Force
-
-# First deployment only. Keep azure.yaml and .azure for later deployments.
-azd ai agent init --no-prompt --kind hosted --deploy-mode container `
- --src $bundle --agent-name $agent --protocol responses `
- --project-id $projectId --model-deployment $model `
- --acr-connection $connection -C $state
-azd deploy $agent -C $state --no-prompt
-azd ai agent invoke $agent 'What is the current stock price of MSFT?' `
- --new-session --new-conversation -o raw -C $state --no-prompt
-```
-
-This flow builds the image remotely through the registry connection. For later changes, republish, copy the updated output into the same deployment folder, and run `azd deploy` again. Check the raw terminal event for `response.completed` or `response.failed`; a zero CLI exit code does not prove the turn succeeded.
From c05ef270c6187c48f69eba3f8da716bd4ea38c18 Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Thu, 29 Oct 2026 11:50:02 -0700
Subject: [PATCH 5/5] fix(dotnet): expose local history marker without IVT
---
.../Microsoft.Agents.AI.Foundry.Hosting.csproj | 2 ++
...rviceCallChatHistoryPersistingChatClient.cs | 8 +++++---
.../Microsoft.Agents.AI.csproj | 1 -
.../PublicAPI/net10.0/PublicAPI.Unshipped.txt | 4 ++++
.../PublicAPI/net472/PublicAPI.Unshipped.txt | 4 ++++
.../PublicAPI/net8.0/PublicAPI.Unshipped.txt | 4 ++++
.../PublicAPI/net9.0/PublicAPI.Unshipped.txt | 4 ++++
.../netstandard2.0/PublicAPI.Unshipped.txt | 4 ++++
...CallChatHistoryPersistingChatClientTests.cs | 18 ++++++++++++++++++
9 files changed, 45 insertions(+), 4 deletions(-)
diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/Microsoft.Agents.AI.Foundry.Hosting.csproj b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/Microsoft.Agents.AI.Foundry.Hosting.csproj
index c7d66433dca..bdf92960bde 100644
--- a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/Microsoft.Agents.AI.Foundry.Hosting.csproj
+++ b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/Microsoft.Agents.AI.Foundry.Hosting.csproj
@@ -10,6 +10,8 @@
+ true
+ true
true
true
true
diff --git a/dotnet/src/Microsoft.Agents.AI/ChatClient/PerServiceCallChatHistoryPersistingChatClient.cs b/dotnet/src/Microsoft.Agents.AI/ChatClient/PerServiceCallChatHistoryPersistingChatClient.cs
index c2087b2d829..94de688243d 100644
--- a/dotnet/src/Microsoft.Agents.AI/ChatClient/PerServiceCallChatHistoryPersistingChatClient.cs
+++ b/dotnet/src/Microsoft.Agents.AI/ChatClient/PerServiceCallChatHistoryPersistingChatClient.cs
@@ -19,6 +19,8 @@ namespace Microsoft.Agents.AI;
/// This decorator is intended to operate between the and the leaf
/// in a pipeline. It is activated when
/// is .
+/// For custom pipelines, use
+/// to add this decorator.
///
///
/// When active, it handles two complementary scenarios:
@@ -54,7 +56,7 @@ namespace Microsoft.Agents.AI;
/// available or if the agent is not a .
///
///
-internal sealed class PerServiceCallChatHistoryPersistingChatClient : DelegatingChatClient
+public sealed class PerServiceCallChatHistoryPersistingChatClient : DelegatingChatClient
{
///
/// A sentinel value returned on to signal
@@ -72,13 +74,13 @@ internal sealed class PerServiceCallChatHistoryPersistingChatClient : Delegating
/// requests before forwarding to the inner client, so the underlying model never sees it.
///
///
- internal const string LocalHistoryConversationId = "_agent_local_chat_history";
+ public const string LocalHistoryConversationId = "_agent_local_chat_history";
///
/// Initializes a new instance of the class.
///
/// The underlying chat client that will handle the core operations.
- public PerServiceCallChatHistoryPersistingChatClient(IChatClient innerClient)
+ internal PerServiceCallChatHistoryPersistingChatClient(IChatClient innerClient)
: base(innerClient)
{
}
diff --git a/dotnet/src/Microsoft.Agents.AI/Microsoft.Agents.AI.csproj b/dotnet/src/Microsoft.Agents.AI/Microsoft.Agents.AI.csproj
index f8f59241340..61a3225bbd2 100644
--- a/dotnet/src/Microsoft.Agents.AI/Microsoft.Agents.AI.csproj
+++ b/dotnet/src/Microsoft.Agents.AI/Microsoft.Agents.AI.csproj
@@ -52,7 +52,6 @@
-
diff --git a/dotnet/src/Microsoft.Agents.AI/PublicAPI/net10.0/PublicAPI.Unshipped.txt b/dotnet/src/Microsoft.Agents.AI/PublicAPI/net10.0/PublicAPI.Unshipped.txt
index fada975c025..31ecabf0bff 100644
--- a/dotnet/src/Microsoft.Agents.AI/PublicAPI/net10.0/PublicAPI.Unshipped.txt
+++ b/dotnet/src/Microsoft.Agents.AI/PublicAPI/net10.0/PublicAPI.Unshipped.txt
@@ -1,4 +1,8 @@
#nullable enable
+Microsoft.Agents.AI.PerServiceCallChatHistoryPersistingChatClient
+const Microsoft.Agents.AI.PerServiceCallChatHistoryPersistingChatClient.LocalHistoryConversationId = "_agent_local_chat_history" -> string!
+override Microsoft.Agents.AI.PerServiceCallChatHistoryPersistingChatClient.GetResponseAsync(System.Collections.Generic.IEnumerable! messages, Microsoft.Extensions.AI.ChatOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
+override Microsoft.Agents.AI.PerServiceCallChatHistoryPersistingChatClient.GetStreamingResponseAsync(System.Collections.Generic.IEnumerable! messages, Microsoft.Extensions.AI.ChatOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IAsyncEnumerable!
[MAAI001]Microsoft.Agents.AI.DelegatingAgentSessionStore
[MAAI001]Microsoft.Agents.AI.DelegatingAgentSessionStore.DelegatingAgentSessionStore(Microsoft.Agents.AI.AgentSessionStore! innerStore) -> void
[MAAI001]Microsoft.Agents.AI.DelegatingAgentSessionStore.InnerStore.get -> Microsoft.Agents.AI.AgentSessionStore!
diff --git a/dotnet/src/Microsoft.Agents.AI/PublicAPI/net472/PublicAPI.Unshipped.txt b/dotnet/src/Microsoft.Agents.AI/PublicAPI/net472/PublicAPI.Unshipped.txt
index fada975c025..31ecabf0bff 100644
--- a/dotnet/src/Microsoft.Agents.AI/PublicAPI/net472/PublicAPI.Unshipped.txt
+++ b/dotnet/src/Microsoft.Agents.AI/PublicAPI/net472/PublicAPI.Unshipped.txt
@@ -1,4 +1,8 @@
#nullable enable
+Microsoft.Agents.AI.PerServiceCallChatHistoryPersistingChatClient
+const Microsoft.Agents.AI.PerServiceCallChatHistoryPersistingChatClient.LocalHistoryConversationId = "_agent_local_chat_history" -> string!
+override Microsoft.Agents.AI.PerServiceCallChatHistoryPersistingChatClient.GetResponseAsync(System.Collections.Generic.IEnumerable! messages, Microsoft.Extensions.AI.ChatOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
+override Microsoft.Agents.AI.PerServiceCallChatHistoryPersistingChatClient.GetStreamingResponseAsync(System.Collections.Generic.IEnumerable! messages, Microsoft.Extensions.AI.ChatOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IAsyncEnumerable!
[MAAI001]Microsoft.Agents.AI.DelegatingAgentSessionStore
[MAAI001]Microsoft.Agents.AI.DelegatingAgentSessionStore.DelegatingAgentSessionStore(Microsoft.Agents.AI.AgentSessionStore! innerStore) -> void
[MAAI001]Microsoft.Agents.AI.DelegatingAgentSessionStore.InnerStore.get -> Microsoft.Agents.AI.AgentSessionStore!
diff --git a/dotnet/src/Microsoft.Agents.AI/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/dotnet/src/Microsoft.Agents.AI/PublicAPI/net8.0/PublicAPI.Unshipped.txt
index fada975c025..31ecabf0bff 100644
--- a/dotnet/src/Microsoft.Agents.AI/PublicAPI/net8.0/PublicAPI.Unshipped.txt
+++ b/dotnet/src/Microsoft.Agents.AI/PublicAPI/net8.0/PublicAPI.Unshipped.txt
@@ -1,4 +1,8 @@
#nullable enable
+Microsoft.Agents.AI.PerServiceCallChatHistoryPersistingChatClient
+const Microsoft.Agents.AI.PerServiceCallChatHistoryPersistingChatClient.LocalHistoryConversationId = "_agent_local_chat_history" -> string!
+override Microsoft.Agents.AI.PerServiceCallChatHistoryPersistingChatClient.GetResponseAsync(System.Collections.Generic.IEnumerable! messages, Microsoft.Extensions.AI.ChatOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
+override Microsoft.Agents.AI.PerServiceCallChatHistoryPersistingChatClient.GetStreamingResponseAsync(System.Collections.Generic.IEnumerable! messages, Microsoft.Extensions.AI.ChatOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IAsyncEnumerable!
[MAAI001]Microsoft.Agents.AI.DelegatingAgentSessionStore
[MAAI001]Microsoft.Agents.AI.DelegatingAgentSessionStore.DelegatingAgentSessionStore(Microsoft.Agents.AI.AgentSessionStore! innerStore) -> void
[MAAI001]Microsoft.Agents.AI.DelegatingAgentSessionStore.InnerStore.get -> Microsoft.Agents.AI.AgentSessionStore!
diff --git a/dotnet/src/Microsoft.Agents.AI/PublicAPI/net9.0/PublicAPI.Unshipped.txt b/dotnet/src/Microsoft.Agents.AI/PublicAPI/net9.0/PublicAPI.Unshipped.txt
index fada975c025..31ecabf0bff 100644
--- a/dotnet/src/Microsoft.Agents.AI/PublicAPI/net9.0/PublicAPI.Unshipped.txt
+++ b/dotnet/src/Microsoft.Agents.AI/PublicAPI/net9.0/PublicAPI.Unshipped.txt
@@ -1,4 +1,8 @@
#nullable enable
+Microsoft.Agents.AI.PerServiceCallChatHistoryPersistingChatClient
+const Microsoft.Agents.AI.PerServiceCallChatHistoryPersistingChatClient.LocalHistoryConversationId = "_agent_local_chat_history" -> string!
+override Microsoft.Agents.AI.PerServiceCallChatHistoryPersistingChatClient.GetResponseAsync(System.Collections.Generic.IEnumerable! messages, Microsoft.Extensions.AI.ChatOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
+override Microsoft.Agents.AI.PerServiceCallChatHistoryPersistingChatClient.GetStreamingResponseAsync(System.Collections.Generic.IEnumerable! messages, Microsoft.Extensions.AI.ChatOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IAsyncEnumerable!
[MAAI001]Microsoft.Agents.AI.DelegatingAgentSessionStore
[MAAI001]Microsoft.Agents.AI.DelegatingAgentSessionStore.DelegatingAgentSessionStore(Microsoft.Agents.AI.AgentSessionStore! innerStore) -> void
[MAAI001]Microsoft.Agents.AI.DelegatingAgentSessionStore.InnerStore.get -> Microsoft.Agents.AI.AgentSessionStore!
diff --git a/dotnet/src/Microsoft.Agents.AI/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/dotnet/src/Microsoft.Agents.AI/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt
index fada975c025..31ecabf0bff 100644
--- a/dotnet/src/Microsoft.Agents.AI/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt
+++ b/dotnet/src/Microsoft.Agents.AI/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt
@@ -1,4 +1,8 @@
#nullable enable
+Microsoft.Agents.AI.PerServiceCallChatHistoryPersistingChatClient
+const Microsoft.Agents.AI.PerServiceCallChatHistoryPersistingChatClient.LocalHistoryConversationId = "_agent_local_chat_history" -> string!
+override Microsoft.Agents.AI.PerServiceCallChatHistoryPersistingChatClient.GetResponseAsync(System.Collections.Generic.IEnumerable! messages, Microsoft.Extensions.AI.ChatOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
+override Microsoft.Agents.AI.PerServiceCallChatHistoryPersistingChatClient.GetStreamingResponseAsync(System.Collections.Generic.IEnumerable! messages, Microsoft.Extensions.AI.ChatOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Collections.Generic.IAsyncEnumerable!
[MAAI001]Microsoft.Agents.AI.DelegatingAgentSessionStore
[MAAI001]Microsoft.Agents.AI.DelegatingAgentSessionStore.DelegatingAgentSessionStore(Microsoft.Agents.AI.AgentSessionStore! innerStore) -> void
[MAAI001]Microsoft.Agents.AI.DelegatingAgentSessionStore.InnerStore.get -> Microsoft.Agents.AI.AgentSessionStore!
diff --git a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/PerServiceCallChatHistoryPersistingChatClientTests.cs b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/PerServiceCallChatHistoryPersistingChatClientTests.cs
index a24b8453e1a..4d890e49978 100644
--- a/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/PerServiceCallChatHistoryPersistingChatClientTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.UnitTests/ChatClient/PerServiceCallChatHistoryPersistingChatClientTests.cs
@@ -20,6 +20,24 @@ namespace Microsoft.Agents.AI.UnitTests;
///
public class PerServiceCallChatHistoryPersistingChatClientTests
{
+ [Fact]
+ public void PublicContract_ExposesLocalHistoryMarkerWithoutPublicConstructors()
+ {
+ // Arrange
+ Type clientType = typeof(PerServiceCallChatHistoryPersistingChatClient);
+
+ // Act
+ var constructors = clientType.GetConstructors();
+ var marker = clientType.GetField(nameof(PerServiceCallChatHistoryPersistingChatClient.LocalHistoryConversationId));
+
+ // Assert
+ Assert.True(clientType.IsPublic);
+ Assert.Empty(constructors);
+ Assert.NotNull(marker);
+ Assert.True(marker.IsLiteral);
+ Assert.Equal("_agent_local_chat_history", marker.GetRawConstantValue());
+ }
+
///
/// Verifies that by default (RequirePerServiceCallChatHistoryPersistence is false),
/// the ChatHistoryProvider receives messages after a successful non-streaming call.