From 57bfb8f16c74a1086b5a45e86874de7597436e6d Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Thu, 23 Jul 2026 19:15:58 +0100
Subject: [PATCH 01/23] Add zip/code-deploy POC for Hosted-ChatClientAgent
(.NET)
Migrate the sample to Foundry source (ZIP) deployment as the default: add azure.yaml with codeConfiguration (remote_build, dotnet_10) and the tool-generated .agentignore, make the csproj self-contained (single target, CPM off, published PackageReferences), and simplify Program.cs to the pristine end-user hosting path. Container files are kept for now; contributor and remaining samples handled in follow-ups.
---
.../Hosted-ChatClientAgent/.agentignore | 28 ++++++++++++
.../HostedChatClientAgent.csproj | 40 ++++++++---------
.../Hosted-ChatClientAgent/Program.cs | 43 ++++++++-----------
.../Hosted-ChatClientAgent/azure.yaml | 37 ++++++++++++++++
4 files changed, 103 insertions(+), 45 deletions(-)
create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/.agentignore
create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/azure.yaml
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/.agentignore b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/.agentignore
new file mode 100644
index 00000000000..0978a1253f1
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/.agentignore
@@ -0,0 +1,28 @@
+# Controls which files are excluded from the code-deploy ZIP upload (.gitignore syntax).
+# Note: only the root .agentignore is read; subdirectory files are not supported.
+#
+# To include a file that is excluded by default, use negation: !filename
+
+# azd tooling files
+agent.yaml
+agent.manifest.yaml
+azure.yaml
+.agentignore
+
+# Security / secrets
+.env
+.env.*
+.azure/
+.git/
+
+# .NET
+bin/
+obj/
+*.user
+*.suo
+.vs/
+
+# Docker (not used in code deploy)
+Dockerfile
+Dockerfile.contributor
+.dockerignore
\ No newline at end of file
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/HostedChatClientAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/HostedChatClientAgent.csproj
index 1cd4c33e746..0f270837500 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/HostedChatClientAgent.csproj
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/HostedChatClientAgent.csproj
@@ -1,33 +1,33 @@
+
+
- net10.0
+
+ net10.0
+
enable
enable
- false
HostedChatClientAgent
HostedChatClientAgent
- $(NoWarn);
+ false
-
-
-
-
-
-
-
-
+
+
+
+
+
-
-
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Program.cs
index 7e074ca02ba..3ec0330246a 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Program.cs
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Program.cs
@@ -1,36 +1,35 @@
// Copyright (c) Microsoft. All rights reserved.
+// Sample: a minimal general-purpose AI assistant hosted as a Foundry Hosted Agent
+// using the Responses protocol. It is deployed to Foundry directly from source
+// (code / ZIP upload), so the platform builds and runs it with no container image.
+
using Azure.AI.Projects;
-using Azure.Core;
using Azure.Identity;
using DotNetEnv;
-using Hosted_Shared_Contributor_Setup;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Foundry.Hosting;
-// Load .env file if present (for local development)
+// Load a local .env file when present (local development only). In Foundry the
+// platform injects the required environment variables at runtime.
Env.TraversePath().Load();
-var projectEndpoint = new Uri(Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
+var projectEndpoint = new Uri(System.Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set."));
-var agentName = Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-chat-client-agent";
-
-var deployment = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-4o";
+var model = System.Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME")
+ ?? System.Environment.GetEnvironmentVariable("FOUNDRY_MODEL")
+ ?? "gpt-4o";
-// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
-// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
-// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
-// Use a chained credential: try a temporary dev token first (for local Docker debugging),
-// then fall back to DefaultAzureCredential (for local dev via dotnet run / managed identity running in foundry).
-TokenCredential credential = new ChainedTokenCredential(
- new DevTemporaryTokenCredential(),
- new DefaultAzureCredential());
+var agentName = System.Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-chat-client-agent";
-// Create the agent via the AI project client using the Responses API.
-AIAgent agent = new AIProjectClient(projectEndpoint, credential)
+// WARNING: DefaultAzureCredential is convenient for development but requires careful
+// consideration in production. Consider a specific credential (for example
+// ManagedIdentityCredential) to avoid latency, unintended credential probing, and
+// fallback security risks.
+AIAgent agent = new AIProjectClient(projectEndpoint, new DefaultAzureCredential())
.AsAIAgent(
- model: deployment,
+ model: model,
instructions: """
You are a helpful AI assistant hosted as a Foundry Hosted Agent.
You can help with a wide range of tasks including answering questions,
@@ -40,16 +39,10 @@ You are a helpful AI assistant hosted as a Foundry Hosted Agent.
name: agentName,
description: "A simple general-purpose AI assistant");
-// Host the agent as a Foundry Hosted Agent using the Responses API.
+// Host the agent using the Responses protocol.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddFoundryResponses(agent);
var app = builder.Build();
app.MapFoundryResponses();
-
-// Contributor-only: in Development, also map the per-agent OpenAI route shape that live Foundry uses
-// so a local REPL client can target this server via AIProjectClient.AsAIAgent(Uri agentEndpoint).
-// Do not use this in production. Hosted Foundry agents only support the agent-endpoint path.
-app.MapDevTemporaryLocalAgentEndpoint();
-
app.Run();
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/azure.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/azure.yaml
new file mode 100644
index 00000000000..a9721d7c766
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/azure.yaml
@@ -0,0 +1,37 @@
+# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json
+name: hosted-chat-client-agent
+
+services:
+ ai-project:
+ host: azure.ai.project
+ deployments:
+ - name: gpt-4o
+ model:
+ format: OpenAI
+ name: gpt-4o
+ version: "2024-11-20"
+ sku:
+ name: GlobalStandard
+ capacity: 10
+
+ hosted-chat-client-agent:
+ host: azure.ai.agent
+ project: .
+ kind: hosted
+ uses:
+ - ai-project
+ protocols:
+ - protocol: responses
+ version: 2.0.0
+ codeConfiguration:
+ runtime: dotnet_10
+ entryPoint:
+ - dotnet
+ - HostedChatClientAgent.dll
+ dependencyResolution: remote_build
+ container:
+ resources:
+ cpu: "0.25"
+ memory: 0.5Gi
+ env:
+ FOUNDRY_MODEL: ${FOUNDRY_MODEL}
\ No newline at end of file
From a4e02e0d1cad80d825a7f1dbfa1d3b07230be1e3 Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Fri, 24 Jul 2026 20:15:46 +0100
Subject: [PATCH 02/23] .NET: Auto-bind Foundry hosted port for zip/code
deploy; migrate Hosted-ChatClientAgent to source (ZIP)
Foundry.Hosting: AddFoundryResponses now binds Kestrel to FoundryEnvironment.Port (the PORT env var, default 8088) for a plain WebApplication.CreateBuilder (Tier 3) host, mirroring AgentHostBuilder. This lets a source/ZIP-deployed .NET agent pass the readiness probe with no Dockerfile. It respects an explicit ASPNETCORE_URLS override and is idempotent. Adds FoundryListenPortTests plus a serialized env-var collection.
Hosted-ChatClientAgent: migrate to source (ZIP) deploy as the default. Add azure.yaml with codeConfiguration (remote_build, dotnet_10) and the tool-generated .agentignore, make the csproj self-contained (single target, CPM off) with a local Directory.Packages.props, embed the local-dev per-agent route so the Using-Samples REPL can reach the local server, and rewrite the README around the azd flow. Documents AZURE_TOKEN_CREDENTIALS=dev for local runs.
Using-Samples/SimpleAgent: fix the per-agent endpoint scheme rewrite so the local HTTP dev port is preserved (the policy now lives on the per-agent ProjectOpenAIClientOptions that actually serves the request).
---
.../Hosted-ChatClientAgent/.env.example | 19 ++-
.../Directory.Packages.props | 12 ++
.../HostedChatClientAgent.csproj | 1 +
.../LocalDevEndpoint.cs | 44 +++++
.../Hosted-ChatClientAgent/Program.cs | 7 +
.../Hosted-ChatClientAgent/README.md | 157 ++++++++----------
.../Hosted-ChatClientAgent/azure.yaml | 65 ++++----
.../Using-Samples/SimpleAgent/Program.cs | 53 +++++-
.../ServiceCollectionExtensions.cs | 44 +++++
.../AspNetCoreUrlsEnvFixture.cs | 16 ++
.../FoundryListenPortTests.cs | 155 +++++++++++++++++
11 files changed, 436 insertions(+), 137 deletions(-)
create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Directory.Packages.props
create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/LocalDevEndpoint.cs
create mode 100644 dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/AspNetCoreUrlsEnvFixture.cs
create mode 100644 dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryListenPortTests.cs
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/.env.example b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/.env.example
index 99a2f75c03c..e130659c742 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/.env.example
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/.env.example
@@ -1,6 +1,19 @@
+# Foundry project endpoint (shape: https:///api/projects/)
FOUNDRY_PROJECT_ENDPOINT=
+
+# Model deployment name in your Foundry project.
+AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
+
+# Local development only. Bind the app to port 8088 (the readiness/probe port) and
+# enable the Development environment so the per-agent OpenAI route is exposed for the
+# code-first Using-Samples REPL. In Foundry the platform sets the environment to
+# Production, so that route is never exposed on the deployed agent.
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
-FOUNDRY_MODEL=gpt-4o
-AGENT_NAME=hosted-chat-client-agent
-AZURE_BEARER_TOKEN=DefaultAzureCredential
+
+# Local development only. Restrict DefaultAzureCredential to developer credentials
+# (Azure CLI, Visual Studio, azd) and skip the Managed Identity probe. Without this,
+# on a machine with no managed identity DefaultAzureCredential hangs for a long time
+# probing the IMDS endpoint (169.254.169.254) before every model call. Not set in
+# Foundry, where the platform-injected managed identity is used.
+AZURE_TOKEN_CREDENTIALS=dev
\ No newline at end of file
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Directory.Packages.props b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Directory.Packages.props
new file mode 100644
index 00000000000..44240eeeb12
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Directory.Packages.props
@@ -0,0 +1,12 @@
+
+
+
+ false
+
+
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/HostedChatClientAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/HostedChatClientAgent.csproj
index 0f270837500..8ef4e5f3634 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/HostedChatClientAgent.csproj
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/HostedChatClientAgent.csproj
@@ -20,6 +20,7 @@
HostedChatClientAgent
HostedChatClientAgent
false
+ 222d2622-da26-4da0-99b4-0507fb8d41b0
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/LocalDevEndpoint.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/LocalDevEndpoint.cs
new file mode 100644
index 00000000000..86a3c84b15c
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/LocalDevEndpoint.cs
@@ -0,0 +1,44 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Agents.AI.Foundry.Hosting;
+
+namespace HostedChatClientAgent;
+
+///
+/// Local-development routing helper for this hosted-agent sample.
+///
+internal static class LocalDevEndpoint
+{
+ ///
+ /// In Development only, maps the per-agent OpenAI route shape that live Foundry uses
+ /// (/api/projects/{project}/agents/{agentName}/endpoint/protocols/openai/responses)
+ /// on top of the default MapFoundryResponses(), so a code-first client that talks to
+ /// the agent through AIProjectClient.AsAIAgent(Uri agentEndpoint) (see the sibling
+ /// Using-Samples REPLs) can reach this server while it runs locally on
+ /// http://localhost:8088.
+ ///
+ ///
+ /// The {project} and {agentName} segments are route-parameter wildcards; the
+ /// handler does not consume them, so any value the client sends is accepted.
+ ///
+ ///
+ ///
+ /// This is a no-op outside Development. The Foundry hosted runtime runs in the Production
+ /// environment, so the deployed agent never exposes this route; hosted agents are always
+ /// reached through the platform-routed per-agent endpoint.
+ ///
+ ///
+ /// The to attach the route to.
+ /// The same for chaining.
+ public static WebApplication MapLocalDevAgentEndpoint(this WebApplication app)
+ {
+ ArgumentNullException.ThrowIfNull(app);
+
+ if (app.Environment.IsDevelopment())
+ {
+ app.MapFoundryResponses("api/projects/{project}/agents/{agentName}/endpoint/protocols/openai");
+ }
+
+ return app;
+ }
+}
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Program.cs
index 3ec0330246a..64a640f5e14 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Program.cs
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Program.cs
@@ -7,6 +7,7 @@
using Azure.AI.Projects;
using Azure.Identity;
using DotNetEnv;
+using HostedChatClientAgent;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Foundry.Hosting;
@@ -45,4 +46,10 @@ You are a helpful AI assistant hosted as a Foundry Hosted Agent.
var app = builder.Build();
app.MapFoundryResponses();
+
+// Local development only: also expose the per-agent OpenAI route shape that live Foundry uses,
+// so the code-first Using-Samples REPLs can reach this server on http://localhost:8088.
+// This is a no-op in the deployed (Production) environment.
+app.MapLocalDevAgentEndpoint();
+
app.Run();
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
index 9a91909177a..823a150941c 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
@@ -1,12 +1,27 @@
-# Hosted-ChatClientAgent
+# Hosted-ChatClientAgent
-A simple general-purpose AI assistant hosted as a Foundry Hosted Agent using the Agent Framework instance hosting pattern. The agent is created inline via `AIProjectClient.AsAIAgent(model, instructions)` and served using the Responses protocol.
+A minimal general-purpose AI assistant hosted as a Foundry Hosted Agent using the Responses protocol. The agent is created inline via `AIProjectClient.AsAIAgent(model, instructions)` and served with `AddFoundryResponses` / `MapFoundryResponses`.
+
+This sample deploys to Foundry **directly from source (code / ZIP upload)**: the platform builds and runs your code with no container image, so there is no Dockerfile to author or container registry to manage.
## Prerequisites
- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
-- A Foundry project with a deployed model (e.g., `gpt-4o`)
+- A Foundry project with a deployed model (for example `gpt-4o`)
- Azure CLI logged in (`az login`)
+- Azure Developer CLI (`azd`) with the AI agents extension: `azd extension install azure.ai.agents`
+
+## Files
+
+| File | Purpose |
+|------|---------|
+| `Program.cs` | The agent: builds the agent, hosts it with the Responses protocol. |
+| `LocalDevEndpoint.cs` | Local-development-only route helper (see [Run and test locally](#run-and-test-locally)). |
+| `azure.yaml` | The unified `azd` project file. Declares the Foundry project, the model deployment, and the hosted agent with `codeConfiguration` (source/ZIP deploy). |
+| `.agentignore` | Controls which files are excluded from the code-deploy ZIP upload (`.gitignore` syntax). |
+| `HostedChatClientAgent.csproj` | Self-contained project: single target framework, central package management off, explicit package versions (there is no repo-level props file inside the ZIP). |
+| `Directory.Packages.props` | Stops inheritance of the repository's central package management so the in-repo build matches the server-side build of the uploaded ZIP. |
+| `.env.example` | Template for local configuration. |
## Configuration
@@ -16,120 +31,80 @@ Copy the template and fill in your project endpoint:
cp .env.example .env
```
-Edit `.env` and set your Foundry project endpoint:
-
```env
FOUNDRY_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/
+AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
ASPNETCORE_URLS=http://+:8088
ASPNETCORE_ENVIRONMENT=Development
-FOUNDRY_MODEL=gpt-4o
+AZURE_TOKEN_CREDENTIALS=dev
```
-> **Note:** `.env` is gitignored. The `.env.example` template is checked in as a reference.
-
-## Running directly (contributors)
-
-This project uses `ProjectReference` to build against the local Agent Framework source.
+> `.env` is gitignored. The `.env.example` template is checked in as a reference.
-```bash
-cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent
-dotnet run
-```
-
-The agent will start on `http://localhost:8088`.
+> **Local development on a machine without a managed identity:** set `AZURE_TOKEN_CREDENTIALS=dev`.
+> `Program.cs` authenticates with `DefaultAzureCredential` (the pattern the hosted platform
+> expects, where a managed identity is injected). On a developer machine with no managed identity,
+> `DefaultAzureCredential` probes the Azure Instance Metadata Service (IMDS, `169.254.169.254`) and
+> blocks for a long time on the network timeout before every model call, so requests appear to
+> hang. Setting `AZURE_TOKEN_CREDENTIALS=dev` restricts `DefaultAzureCredential` to developer
+> credentials (Azure CLI, Visual Studio, `azd`) and skips the managed-identity probe. This variable
+> is only for local runs; the deployed agent in Foundry uses the platform-injected managed identity.
-### Test it
+## Run and test locally
-Using the Azure Developer CLI:
+Local runs use two terminals: one hosts the agent, the other is a code-first client that talks to it
+using Agent Framework components (`AIProjectClient.AsAIAgent`), see the sibling
+[`Using-Samples`](../Using-Samples/) REPLs.
-```bash
-azd ai agent invoke --local "Hello!"
-```
-
-Or with curl (specifying the agent name explicitly):
-
-```bash
-curl -X POST http://localhost:8088/responses \
- -H "Content-Type: application/json" \
- -d '{"input": "Hello!", "model": "hosted-chat-client-agent"}'
-```
-
-## Running with Docker
-
-Since this project uses `ProjectReference`, the standard `Dockerfile` cannot resolve dependencies outside this folder. Use `Dockerfile.contributor` which takes a pre-published output.
-
-### 1. Publish for the container runtime (Linux Alpine)
-
-```bash
-dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
-```
+`Program.cs` maps the default `/responses` route and, **only in the Development environment**, also
+maps the per-agent OpenAI route shape that live Foundry uses
+(`/api/projects/{project}/agents/{agent}/endpoint/protocols/openai`) so the `Using-Samples` client
+can reach the local server. The deployed agent runs in the Production environment and never exposes
+that extra route.
-### 2. Build the Docker image
+**Terminal 1 — host the agent:**
```bash
-docker build -f Dockerfile.contributor -t hosted-chat-client-agent .
-```
-
-### 3. Run the container
-
-Generate a bearer token on your host and pass it to the container:
-
-```bash
-# Generate token (expires in ~1 hour)
-export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv)
-
-# Run with token
-docker run --rm -p 8088:8088 \
- -e AGENT_NAME=hosted-chat-client-agent \
- -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \
- --env-file .env \
- hosted-chat-client-agent
+cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent
+az login
+dotnet run
```
-> **Note:** `AGENT_NAME` is passed via `-e` to simulate the platform injection. `AZURE_BEARER_TOKEN` provides Azure credentials to the container (tokens expire after ~1 hour). The `.env` file provides the remaining configuration.
-
-### 4. Test it
-
-Using the Azure Developer CLI:
-
-```bash
-azd ai agent invoke --local "Hello!"
-```
+The agent starts on `http://localhost:8088`.
-Or with curl (specifying the agent name explicitly):
+**Terminal 2 — chat with it (code-first REPL):**
-```bash
-curl -X POST http://localhost:8088/responses \
- -H "Content-Type: application/json" \
- -d '{"input": "Hello!", "model": "hosted-chat-client-agent"}'
+```powershell
+cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent
+$env:FOUNDRY_PROJECT_ENDPOINT = "http://localhost:8088/api/projects/local"
+$env:AZURE_AI_AGENT_NAME = "hosted-chat-client-agent"
+dotnet run
```
-## Deploying to Foundry (azd spec)
+Type a message; the REPL builds a `FoundryAgent` against the local server and streams the reply.
-This sample includes an `azd` manifest (`agent.manifest.yaml`) and hosted agent spec (`agent.yaml`) for deployment to Foundry.
-
-Initialize an `azd` project from this sample's manifest:
+## Deploy to Foundry (source / ZIP)
```bash
-mkdir hosted-chat-client-agent && cd hosted-chat-client-agent
-azd ai agent init -m https://github.com/microsoft/agent-framework/blob/main/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/agent.manifest.yaml
+cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent
+azd auth login
+azd ai agent init # or target an existing project: -p -d
+azd up # provision (if needed) + deploy: zips the source, builds it remotely
```
-Then deploy:
+`azd` packages the source into a ZIP (honoring `.agentignore`), uploads it, and Foundry runs
+`dotnet restore` + `dotnet publish` on it during provisioning (`dependencyResolution: remote_build`
+in `azure.yaml`). No Dockerfile, no container registry.
-```bash
-azd deploy
-```
-
-If you need to override defaults, set deployment-time environment variables in the `azd` environment before deploying:
+Test the deployed agent with the REPL (against the real endpoint):
-```bash
-azd env set AGENT_NAME hosted-chat-client-agent
-azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME gpt-4o
+```powershell
+cd ../Using-Samples/SimpleAgent
+$env:FOUNDRY_PROJECT_ENDPOINT = "https://.services.ai.azure.com/api/projects/"
+$env:AZURE_AI_AGENT_NAME = "hosted-chat-client-agent"
+dotnet run
```
-For end-to-end hosted agent deployment guidance, see the [official deployment guide](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent).
-
-## NuGet package users
+Or use the `azd` shortcut: `azd ai agent invoke "Hello!"`.
-If you are consuming the Agent Framework as a NuGet package (not building from source), use the standard `Dockerfile` instead of `Dockerfile.contributor` — it performs a full `dotnet restore` and `dotnet publish` inside the container. See the commented section in `HostedChatClientAgent.csproj` for the `PackageReference` alternative.
+For the full hosted-agent deployment guide, see the [official source-code deployment doc](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent-code).
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/azure.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/azure.yaml
index a9721d7c766..8e186d052a2 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/azure.yaml
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/azure.yaml
@@ -1,37 +1,34 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json
-name: hosted-chat-client-agent
+name: hosted-chat-client-agent
services:
- ai-project:
- host: azure.ai.project
- deployments:
- - name: gpt-4o
- model:
- format: OpenAI
- name: gpt-4o
- version: "2024-11-20"
- sku:
- name: GlobalStandard
- capacity: 10
-
- hosted-chat-client-agent:
- host: azure.ai.agent
- project: .
- kind: hosted
- uses:
- - ai-project
- protocols:
- - protocol: responses
- version: 2.0.0
- codeConfiguration:
- runtime: dotnet_10
- entryPoint:
- - dotnet
- - HostedChatClientAgent.dll
- dependencyResolution: remote_build
- container:
- resources:
- cpu: "0.25"
- memory: 0.5Gi
- env:
- FOUNDRY_MODEL: ${FOUNDRY_MODEL}
\ No newline at end of file
+ ai-project:
+ host: azure.ai.project
+ hosted-chat-client-agent:
+ project: .
+ host: azure.ai.agent
+ language: csharp
+ uses:
+ - ai-project
+ codeConfiguration:
+ dependencyResolution: remote_build
+ entryPoint: HostedChatClientAgent.dll
+ runtime: dotnet_10
+ container:
+ resources:
+ cpu: "0.5"
+ memory: 1Gi
+ description: |
+ A simple general-purpose AI assistant hosted as a Foundry Hosted Agent using the Agent Framework instance hosting pattern.
+ kind: hosted
+ metadata:
+ tags:
+ - AI Agent Hosting
+ - Azure AI AgentServer
+ - Responses Protocol
+ - Streaming
+ - Agent Framework
+ name: hosted-chat-client-agent
+ protocols:
+ - protocol: responses
+ version: 2.0.0
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/Program.cs
index b74e85e19d3..e940f2342bc 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/Program.cs
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/Program.cs
@@ -1,7 +1,10 @@
// Copyright (c) Microsoft. All rights reserved.
+using System.ClientModel;
using System.ClientModel.Primitives;
+using Azure.AI.Extensions.OpenAI;
using Azure.AI.Projects;
+using Azure.Core;
using Azure.Identity;
using DotNetEnv;
using Microsoft.Agents.AI;
@@ -24,20 +27,26 @@
// ── Create an agent-framework agent backed by the remote agent endpoint ──────
-var options = new AIProjectClientOptions();
+// Per-agent client options. The scheme-rewrite policy for local HTTP dev must live on
+// THIS options bag (the per-agent responses pipeline), not on AIProjectClientOptions:
+// FoundryAgent builds the responses client from these options, so a policy added here
+// is the one that actually runs on the /responses request.
+var clientOptions = new ProjectOpenAIClientOptions();
-if (projectEndpoint.Scheme == "http")
+if (agentEndpoint.Scheme == "http")
{
- // For local HTTP dev: tell AIProjectClient the endpoint is HTTPS (to satisfy
- // BearerTokenPolicy's TLS check), then swap the scheme back to HTTP right
- // before the request hits the wire.
- projectEndpoint = new UriBuilder(projectEndpoint) { Scheme = "https" }.Uri;
+ // For local HTTP dev: present the endpoint as HTTPS (to satisfy BearerTokenPolicy's
+ // TLS check), then swap the scheme back to HTTP right before the request hits the
+ // wire. Rewriting the agent endpoint preserves its explicit port (for example 8088).
agentEndpoint = new UriBuilder(agentEndpoint) { Scheme = "https" }.Uri;
- options.AddPolicy(new HttpSchemeRewritePolicy(), PipelinePosition.BeforeTransport);
+ clientOptions.AddPolicy(new HttpSchemeRewritePolicy(), PipelinePosition.BeforeTransport);
}
-var aiProjectClient = new AIProjectClient(projectEndpoint, new AzureCliCredential(), options);
-FoundryAgent agent = aiProjectClient.AsAIAgent(agentEndpoint);
+// FoundryAgent's agent-endpoint constructor builds the responses client directly from
+// agentEndpoint (port preserved) and applies clientOptions to that pipeline. It expects a
+// System.ClientModel AuthenticationTokenProvider, so adapt the Azure CLI credential.
+var credential = new AzureCliAuthenticationTokenProvider(new AzureCliCredential(), "https://ai.azure.com/.default");
+FoundryAgent agent = new(agentEndpoint, credential, clientOptions);
AgentSession session = await agent.CreateSessionAsync();
@@ -118,3 +127,29 @@ private static void RewriteScheme(PipelineMessage message)
}
}
}
+
+///
+/// For Local Development Only
+/// Adapts an Azure.Core (for example )
+/// to the System.ClientModel that the
+/// agent-endpoint constructor expects. Uses the fixed Azure AI resource
+/// scope that the Foundry control plane accepts.
+///
+internal sealed class AzureCliAuthenticationTokenProvider(TokenCredential credential, params string[] scopes)
+ : AuthenticationTokenProvider
+{
+ public override GetTokenOptions? CreateTokenOptions(IReadOnlyDictionary properties)
+ => new(properties);
+
+ public override AuthenticationToken GetToken(GetTokenOptions options, CancellationToken cancellationToken)
+ {
+ AccessToken token = credential.GetToken(new TokenRequestContext(scopes), cancellationToken);
+ return new AuthenticationToken(token.Token, "Bearer", token.ExpiresOn);
+ }
+
+ public override async ValueTask GetTokenAsync(GetTokenOptions options, CancellationToken cancellationToken)
+ {
+ AccessToken token = await credential.GetTokenAsync(new TokenRequestContext(scopes), cancellationToken).ConfigureAwait(false);
+ return new AuthenticationToken(token.Token, "Bearer", token.ExpiresOn);
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ServiceCollectionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ServiceCollectionExtensions.cs
index 8e2a6986f92..79c09d9dc76 100644
--- a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ServiceCollectionExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ServiceCollectionExtensions.cs
@@ -5,10 +5,12 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
+using Azure.AI.AgentServer.Core;
using Azure.AI.AgentServer.Responses;
using Azure.Core;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
+using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
@@ -54,6 +56,7 @@ public static IServiceCollection AddFoundryResponses(this IServiceCollection ser
ArgumentNullException.ThrowIfNull(services);
services.AddResponsesServer();
services.AddHealthChecks();
+ ConfigureFoundryListenPort(services);
services.TryAddSingleton(_ => FileSystemAgentSessionStore.CreateDefault());
services.TryAddSingleton();
return services;
@@ -90,6 +93,7 @@ public static IServiceCollection AddFoundryResponses(this IServiceCollection ser
services.AddResponsesServer();
services.AddHealthChecks();
+ ConfigureFoundryListenPort(services);
agentSessionStore ??= FileSystemAgentSessionStore.CreateDefault();
if (!string.IsNullOrWhiteSpace(agent.Name))
@@ -249,6 +253,46 @@ public static IEndpointRouteBuilder MapFoundryResponses(this IEndpointRouteBuild
return endpoints;
}
+ ///
+ /// Marker registered once per so the Foundry listen-port
+ /// configuration is applied at most once, even across multiple AddFoundryResponses calls.
+ ///
+ private sealed class FoundryListenPortMarker;
+
+ ///
+ /// Binds Kestrel to the port the Foundry hosted runtime probes and routes to, so a plain
+ /// WebApplication.CreateBuilder host (Tier 3) works with no Dockerfile and no
+ /// ASPNETCORE_URLS. Mirrors AgentHostBuilder, which listens on
+ /// (the PORT environment variable, default 8088).
+ ///
+ ///
+ ///
+ /// No-op when ASPNETCORE_URLS is set: an explicit ASP.NET Core URL binding always wins,
+ /// so a developer can override the port locally (for example ASPNETCORE_URLS=http://+:9000).
+ /// The Foundry hosted runtime injects PORT (not ASPNETCORE_URLS), so the container
+ /// path always flows through here.
+ ///
+ ///
+ /// Idempotent and harmless when no Kestrel server is present (for example unit tests): the
+ /// options callback is only invoked when Kestrel is resolved.
+ ///
+ ///
+ private static void ConfigureFoundryListenPort(IServiceCollection services)
+ {
+ if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("ASPNETCORE_URLS")))
+ {
+ return;
+ }
+
+ if (services.Any(static d => d.ServiceType == typeof(FoundryListenPortMarker)))
+ {
+ return;
+ }
+
+ services.AddSingleton();
+ services.Configure(static options => options.ListenAnyIP(FoundryEnvironment.Port));
+ }
+
///
/// Maps GET /readiness to the AspNetCore HealthChecks pipeline only when no
/// route already serves that path. The duplicate guard scans
diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/AspNetCoreUrlsEnvFixture.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/AspNetCoreUrlsEnvFixture.cs
new file mode 100644
index 00000000000..15ba4df1277
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/AspNetCoreUrlsEnvFixture.cs
@@ -0,0 +1,16 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Xunit;
+
+namespace Microsoft.Agents.AI.Foundry.Hosting.UnitTests;
+
+///
+/// xUnit collection that serializes tests mutating the ASPNETCORE_URLS process
+/// environment variable. Without this, parallel test execution causes flaky races between
+/// tests that set / unset the variable.
+///
+[CollectionDefinition(Name, DisableParallelization = true)]
+public sealed class AspNetCoreUrlsEnvFixture
+{
+ public const string Name = "AspNetCoreUrlsEnv";
+}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryListenPortTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryListenPortTests.cs
new file mode 100644
index 00000000000..2884e8138e2
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryListenPortTests.cs
@@ -0,0 +1,155 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Reflection;
+using Azure.AI.AgentServer.Core;
+using Microsoft.AspNetCore.Server.Kestrel.Core;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using Moq;
+
+namespace Microsoft.Agents.AI.Foundry.Hosting.UnitTests;
+
+///
+/// Verifies that AddFoundryResponses binds Kestrel to the Foundry hosted-runtime port
+/// (, the PORT env var, default 8088) for a plain
+/// WebApplication.CreateBuilder (Tier 3) host, so the sample works with no Dockerfile and
+/// no ASPNETCORE_URLS, while still respecting an explicit ASPNETCORE_URLS override.
+///
+[Collection(AspNetCoreUrlsEnvFixture.Name)]
+public sealed class FoundryListenPortTests
+{
+ private const string AspNetCoreUrls = "ASPNETCORE_URLS";
+
+ [Fact]
+ public void AddFoundryResponses_NoAspNetCoreUrls_BindsFoundryPort()
+ {
+ // Arrange
+ var original = Environment.GetEnvironmentVariable(AspNetCoreUrls);
+ try
+ {
+ Environment.SetEnvironmentVariable(AspNetCoreUrls, null);
+ var services = new ServiceCollection();
+ services.AddLogging();
+
+ // Act
+ services.AddFoundryResponses();
+
+ // Assert
+ var ports = GetCodeBackedPorts(services);
+ Assert.Contains(FoundryEnvironment.Port, ports);
+ }
+ finally
+ {
+ Environment.SetEnvironmentVariable(AspNetCoreUrls, original);
+ }
+ }
+
+ [Fact]
+ public void AddFoundryResponses_WithAgent_NoAspNetCoreUrls_BindsFoundryPort()
+ {
+ // Arrange
+ var original = Environment.GetEnvironmentVariable(AspNetCoreUrls);
+ try
+ {
+ Environment.SetEnvironmentVariable(AspNetCoreUrls, null);
+ var services = new ServiceCollection();
+ services.AddLogging();
+ var mockAgent = new Mock();
+ mockAgent.SetupGet(a => a.Name).Returns("test-agent");
+
+ // Act
+ services.AddFoundryResponses(mockAgent.Object);
+
+ // Assert
+ var ports = GetCodeBackedPorts(services);
+ Assert.Contains(FoundryEnvironment.Port, ports);
+ }
+ finally
+ {
+ Environment.SetEnvironmentVariable(AspNetCoreUrls, original);
+ }
+ }
+
+ [Fact]
+ public void AddFoundryResponses_WithAspNetCoreUrls_DoesNotBindFoundryPort()
+ {
+ // Arrange: an explicit ASP.NET Core URL binding must win; the package adds no listener.
+ var original = Environment.GetEnvironmentVariable(AspNetCoreUrls);
+ try
+ {
+ Environment.SetEnvironmentVariable(AspNetCoreUrls, "http://+:9123");
+ var services = new ServiceCollection();
+ services.AddLogging();
+
+ // Act
+ services.AddFoundryResponses();
+
+ // Assert
+ var ports = GetCodeBackedPorts(services);
+ Assert.DoesNotContain(FoundryEnvironment.Port, ports);
+ Assert.DoesNotContain(9123, ports);
+ }
+ finally
+ {
+ Environment.SetEnvironmentVariable(AspNetCoreUrls, original);
+ }
+ }
+
+ [Fact]
+ public void AddFoundryResponses_CalledTwice_BindsFoundryPortOnce()
+ {
+ // Arrange
+ var original = Environment.GetEnvironmentVariable(AspNetCoreUrls);
+ try
+ {
+ Environment.SetEnvironmentVariable(AspNetCoreUrls, null);
+ var services = new ServiceCollection();
+ services.AddLogging();
+
+ // Act
+ services.AddFoundryResponses();
+ services.AddFoundryResponses();
+
+ // Assert: the listen port is configured exactly once, not duplicated (a duplicate
+ // ListenAnyIP on the same port would fail Kestrel startup with "address already in use").
+ var ports = GetCodeBackedPorts(services);
+ Assert.Equal(1, ports.Count(p => p == FoundryEnvironment.Port));
+ }
+ finally
+ {
+ Environment.SetEnvironmentVariable(AspNetCoreUrls, original);
+ }
+ }
+
+ ///
+ /// Builds the service provider, resolves the applied , and
+ /// returns the ports of every code-configured listener (those added via ListenAnyIP).
+ ///
+ private static List GetCodeBackedPorts(IServiceCollection services)
+ {
+ using var provider = services.BuildServiceProvider();
+ var options = provider.GetRequiredService>().Value;
+
+ var property = typeof(KestrelServerOptions).GetProperty(
+ "CodeBackedListenOptions",
+ BindingFlags.Instance | BindingFlags.NonPublic);
+ Assert.NotNull(property);
+
+ var listenOptions = (IEnumerable)property!.GetValue(options)!;
+ var ports = new List();
+ foreach (var listenOption in listenOptions)
+ {
+ if (listenOption.GetType().GetProperty("IPEndPoint")?.GetValue(listenOption) is IPEndPoint endpoint)
+ {
+ ports.Add(endpoint.Port);
+ }
+ }
+
+ return ports;
+ }
+}
From c760aa3091889e8cb81b309f5191af3946c32132 Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Tue, 28 Jul 2026 11:35:13 +0100
Subject: [PATCH 03/23] .NET: Bind Foundry hosted port unconditionally; drop
container files and the local-only agent route
Zip/code deploy runs the sample as a plain ASP.NET app, so the Foundry readiness
port was never bound and every invoke returned HTTP 424 session_not_ready. The
first attempt skipped the binding when ASPNETCORE_URLS was already set, but the
.NET base image always sets it to port 80, so the skip always tripped. Kestrel
ListenAnyIP overrides ASPNETCORE_URLS, so the binding is now unconditional and
PORT stays the only knob.
Sample cleanup for zip deploy:
* Remove Dockerfile, Dockerfile.contributor, agent.manifest.yaml and agent.yaml.
Source deploy needs none of them.
* Remove LocalDevEndpoint.cs and the invented per-agent local route. The local
server already serves the standard POST /responses route, so the client can
reach it directly.
* Trim .env.example: the port and environment variables are no longer needed.
* Exclude .checkpoints/ from the upload so local session state does not ship.
SimpleAgent now asks at startup whether to chat with the local server or the
deployed agent, the same choice azd ai agent invoke exposes through --local.
Local uses an OpenAI responses client pointed at http://localhost:8088; Foundry
uses the per-agent endpoint.
Add scripts/New-ContributorStage.ps1, which stages a sample to a temp folder with
the local Agent Framework source packed into a feed inside the upload, so
contributors can deploy framework changes through the same azd flow end users run.
---
.../Hosted-ChatClientAgent/.agentignore | 16 +-
.../Hosted-ChatClientAgent/.env.example | 9 +-
.../Hosted-ChatClientAgent/Dockerfile | 17 --
.../Dockerfile.contributor | 19 --
.../HostedChatClientAgent.csproj | 17 +-
.../LocalDevEndpoint.cs | 44 -----
.../Hosted-ChatClientAgent/Program.cs | 6 -
.../Hosted-ChatClientAgent/README.md | 58 ++++--
.../agent.manifest.yaml | 28 ---
.../Hosted-ChatClientAgent/agent.yaml | 9 -
.../Using-Samples/SimpleAgent/Program.cs | 120 +++++-------
.../Using-Samples/SimpleAgent/README.md | 40 ++--
.../SimpleAgent/SimpleAgent.csproj | 3 +-
.../scripts/New-ContributorStage.ps1 | 176 ++++++++++++++++++
.../ServiceCollectionExtensions.cs | 28 +--
.../FoundryListenPortTests.cs | 95 ++++------
16 files changed, 356 insertions(+), 329 deletions(-)
delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Dockerfile
delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Dockerfile.contributor
delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/LocalDevEndpoint.cs
delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/agent.manifest.yaml
delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/agent.yaml
create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/scripts/New-ContributorStage.ps1
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/.agentignore b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/.agentignore
index 0978a1253f1..365ce70ffe8 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/.agentignore
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/.agentignore
@@ -4,8 +4,6 @@
# To include a file that is excluded by default, use negation: !filename
# azd tooling files
-agent.yaml
-agent.manifest.yaml
azure.yaml
.agentignore
@@ -15,14 +13,18 @@ azure.yaml
.azure/
.git/
-# .NET
+# .NET build output
bin/
obj/
*.user
*.suo
.vs/
-# Docker (not used in code deploy)
-Dockerfile
-Dockerfile.contributor
-.dockerignore
\ No newline at end of file
+# Agent session state written by FileSystemAgentSessionStore during local runs. The hosted
+# runtime writes its own under the container's home directory, so uploading the local copy
+# would ship stale sessions with the agent.
+.checkpoints/
+
+# Contributor staging (scripts/New-ContributorStage.ps1) generates local-feed/, nuget.config,
+# and local-feed.props. Those are deliberately NOT excluded: the server-side restore needs them
+# to resolve the Agent Framework from the packages shipped in this upload instead of nuget.org.
\ No newline at end of file
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/.env.example b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/.env.example
index e130659c742..ecf3a6c31ad 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/.env.example
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/.env.example
@@ -4,16 +4,9 @@ FOUNDRY_PROJECT_ENDPOINT=
# Model deployment name in your Foundry project.
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
-# Local development only. Bind the app to port 8088 (the readiness/probe port) and
-# enable the Development environment so the per-agent OpenAI route is exposed for the
-# code-first Using-Samples REPL. In Foundry the platform sets the environment to
-# Production, so that route is never exposed on the deployed agent.
-ASPNETCORE_URLS=http://+:8088
-ASPNETCORE_ENVIRONMENT=Development
-
# Local development only. Restrict DefaultAzureCredential to developer credentials
# (Azure CLI, Visual Studio, azd) and skip the Managed Identity probe. Without this,
# on a machine with no managed identity DefaultAzureCredential hangs for a long time
# probing the IMDS endpoint (169.254.169.254) before every model call. Not set in
# Foundry, where the platform-injected managed identity is used.
-AZURE_TOKEN_CREDENTIALS=dev
\ No newline at end of file
+AZURE_TOKEN_CREDENTIALS=dev
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Dockerfile
deleted file mode 100644
index 6f1be8ee8e2..00000000000
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Dockerfile
+++ /dev/null
@@ -1,17 +0,0 @@
-# Use the official .NET 10.0 ASP.NET runtime as a parent image
-FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
-WORKDIR /app
-
-FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
-WORKDIR /src
-COPY . .
-RUN dotnet restore
-RUN dotnet publish -c Release -o /app/publish
-
-# Final stage
-FROM base AS final
-WORKDIR /app
-COPY --from=build /app/publish .
-EXPOSE 8088
-ENV ASPNETCORE_URLS=http://+:8088
-ENTRYPOINT ["dotnet", "HostedChatClientAgent.dll"]
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Dockerfile.contributor b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Dockerfile.contributor
deleted file mode 100644
index 200f674bdd0..00000000000
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Dockerfile.contributor
+++ /dev/null
@@ -1,19 +0,0 @@
-# Dockerfile for contributors building from the agent-framework repository source.
-#
-# This project uses ProjectReference to the local Microsoft.Agents.AI.Foundry source,
-# which means a standard multi-stage Docker build cannot resolve dependencies outside
-# this folder. Instead, pre-publish the app targeting the container runtime and copy
-# the output into the container:
-#
-# dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
-# docker build -f Dockerfile.contributor -t hosted-chat-client-agent .
-# docker run --rm -p 8088:8088 -e AGENT_NAME=hosted-chat-client-agent --env-file .env hosted-chat-client-agent
-#
-# For end-users consuming the NuGet package (not ProjectReference), use the standard
-# Dockerfile which performs a full dotnet restore + publish inside the container.
-FROM mcr.microsoft.com/dotnet/aspnet:10.0-alpine AS final
-WORKDIR /app
-COPY out/ .
-EXPOSE 8088
-ENV ASPNETCORE_URLS=http://+:8088
-ENTRYPOINT ["dotnet", "HostedChatClientAgent.dll"]
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/HostedChatClientAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/HostedChatClientAgent.csproj
index 8ef4e5f3634..3b1534c722c 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/HostedChatClientAgent.csproj
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/HostedChatClientAgent.csproj
@@ -9,6 +9,17 @@
(dependencyResolution: remote_build in azure.yaml).
-->
+
+
+
+ 1.15.0-preview.260722.1
-
-
+
+
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/LocalDevEndpoint.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/LocalDevEndpoint.cs
deleted file mode 100644
index 86a3c84b15c..00000000000
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/LocalDevEndpoint.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using Microsoft.Agents.AI.Foundry.Hosting;
-
-namespace HostedChatClientAgent;
-
-///
-/// Local-development routing helper for this hosted-agent sample.
-///
-internal static class LocalDevEndpoint
-{
- ///
- /// In Development only, maps the per-agent OpenAI route shape that live Foundry uses
- /// (/api/projects/{project}/agents/{agentName}/endpoint/protocols/openai/responses)
- /// on top of the default MapFoundryResponses(), so a code-first client that talks to
- /// the agent through AIProjectClient.AsAIAgent(Uri agentEndpoint) (see the sibling
- /// Using-Samples REPLs) can reach this server while it runs locally on
- /// http://localhost:8088.
- ///
- ///
- /// The {project} and {agentName} segments are route-parameter wildcards; the
- /// handler does not consume them, so any value the client sends is accepted.
- ///
- ///
- ///
- /// This is a no-op outside Development. The Foundry hosted runtime runs in the Production
- /// environment, so the deployed agent never exposes this route; hosted agents are always
- /// reached through the platform-routed per-agent endpoint.
- ///
- ///
- /// The to attach the route to.
- /// The same for chaining.
- public static WebApplication MapLocalDevAgentEndpoint(this WebApplication app)
- {
- ArgumentNullException.ThrowIfNull(app);
-
- if (app.Environment.IsDevelopment())
- {
- app.MapFoundryResponses("api/projects/{project}/agents/{agentName}/endpoint/protocols/openai");
- }
-
- return app;
- }
-}
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Program.cs
index 64a640f5e14..9a2f4577994 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Program.cs
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Program.cs
@@ -7,7 +7,6 @@
using Azure.AI.Projects;
using Azure.Identity;
using DotNetEnv;
-using HostedChatClientAgent;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Foundry.Hosting;
@@ -47,9 +46,4 @@ You are a helpful AI assistant hosted as a Foundry Hosted Agent.
var app = builder.Build();
app.MapFoundryResponses();
-// Local development only: also expose the per-agent OpenAI route shape that live Foundry uses,
-// so the code-first Using-Samples REPLs can reach this server on http://localhost:8088.
-// This is a no-op in the deployed (Production) environment.
-app.MapLocalDevAgentEndpoint();
-
app.Run();
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
index 823a150941c..0e95bdd8e61 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
@@ -16,8 +16,7 @@ This sample deploys to Foundry **directly from source (code / ZIP upload)**: the
| File | Purpose |
|------|---------|
| `Program.cs` | The agent: builds the agent, hosts it with the Responses protocol. |
-| `LocalDevEndpoint.cs` | Local-development-only route helper (see [Run and test locally](#run-and-test-locally)). |
-| `azure.yaml` | The unified `azd` project file. Declares the Foundry project, the model deployment, and the hosted agent with `codeConfiguration` (source/ZIP deploy). |
+| `azure.yaml` | The unified `azd` project file. Declares the Foundry project and the hosted agent with `codeConfiguration` (source/ZIP deploy). |
| `.agentignore` | Controls which files are excluded from the code-deploy ZIP upload (`.gitignore` syntax). |
| `HostedChatClientAgent.csproj` | Self-contained project: single target framework, central package management off, explicit package versions (there is no repo-level props file inside the ZIP). |
| `Directory.Packages.props` | Stops inheritance of the repository's central package management so the in-repo build matches the server-side build of the uploaded ZIP. |
@@ -34,8 +33,6 @@ cp .env.example .env
```env
FOUNDRY_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
-ASPNETCORE_URLS=http://+:8088
-ASPNETCORE_ENVIRONMENT=Development
AZURE_TOKEN_CREDENTIALS=dev
```
@@ -53,14 +50,13 @@ AZURE_TOKEN_CREDENTIALS=dev
## Run and test locally
Local runs use two terminals: one hosts the agent, the other is a code-first client that talks to it
-using Agent Framework components (`AIProjectClient.AsAIAgent`), see the sibling
-[`Using-Samples`](../Using-Samples/) REPLs.
+using Agent Framework components, see the sibling [`Using-Samples`](../Using-Samples/) REPLs.
-`Program.cs` maps the default `/responses` route and, **only in the Development environment**, also
-maps the per-agent OpenAI route shape that live Foundry uses
-(`/api/projects/{project}/agents/{agent}/endpoint/protocols/openai`) so the `Using-Samples` client
-can reach the local server. The deployed agent runs in the Production environment and never exposes
-that extra route.
+`AddFoundryResponses` binds the app to the port Foundry probes for readiness (8088 by default,
+overridable with the `PORT` environment variable), and `MapFoundryResponses` serves the standard
+`POST /responses` route. That is the same route the platform routes to for a deployed agent, so the
+local server needs no extra wiring: the client just points an OpenAI responses client at
+`http://localhost:8088`.
**Terminal 1 — host the agent:**
@@ -76,30 +72,36 @@ The agent starts on `http://localhost:8088`.
```powershell
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent
-$env:FOUNDRY_PROJECT_ENDPOINT = "http://localhost:8088/api/projects/local"
-$env:AZURE_AI_AGENT_NAME = "hosted-chat-client-agent"
+$env:AZURE_AI_AGENT_NAME = "hosted-chat-client-agent"
dotnet run
```
-Type a message; the REPL builds a `FoundryAgent` against the local server and streams the reply.
+The REPL asks which agent to chat with; choose **2 (Local)**. It then points an OpenAI responses
+client at the local server and streams the reply.
## Deploy to Foundry (source / ZIP)
-```bash
-cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent
+The Azure Developer CLI scaffolds the project into a working folder, so run `init` from an empty
+directory outside the repo and point `-m` at this sample's `azure.yaml`:
+
+```powershell
+mkdir ; cd
azd auth login
-azd ai agent init # or target an existing project: -p -d
-azd up # provision (if needed) + deploy: zips the source, builds it remotely
+azd ai agent init -m /dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/azure.yaml `
+ -p -d
+cd hosted-chat-client-agent
+azd provision
+azd deploy
```
`azd` packages the source into a ZIP (honoring `.agentignore`), uploads it, and Foundry runs
`dotnet restore` + `dotnet publish` on it during provisioning (`dependencyResolution: remote_build`
in `azure.yaml`). No Dockerfile, no container registry.
-Test the deployed agent with the REPL (against the real endpoint):
+Test the deployed agent with the REPL, choosing **1 (Foundry)** at the prompt:
```powershell
-cd ../Using-Samples/SimpleAgent
+cd /dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent
$env:FOUNDRY_PROJECT_ENDPOINT = "https://.services.ai.azure.com/api/projects/"
$env:AZURE_AI_AGENT_NAME = "hosted-chat-client-agent"
dotnet run
@@ -107,4 +109,20 @@ dotnet run
Or use the `azd` shortcut: `azd ai agent invoke "Hello!"`.
+## Deploy your local framework changes (contributors)
+
+By default the project restores the published Agent Framework packages, so local changes to the
+framework are not exercised. To deploy them, stage the sample first:
+
+```powershell
+cd dotnet/samples/04-hosting/FoundryHostedAgents/scripts
+./New-ContributorStage.ps1 -Sample Hosted-ChatClientAgent
+```
+
+The script copies the sample to a temp folder, packs the local Agent Framework source into a
+`local-feed` folder next to it, and writes the `nuget.config` and `local-feed.props` that point the
+build at those packages. All three travel inside the ZIP, so the server-side restore resolves the
+framework from the upload. It prints the same `azd` commands as above, with `-m` pointing at the
+staged copy.
+
For the full hosted-agent deployment guide, see the [official source-code deployment doc](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent-code).
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/agent.manifest.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/agent.manifest.yaml
deleted file mode 100644
index bc699d3b4b0..00000000000
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/agent.manifest.yaml
+++ /dev/null
@@ -1,28 +0,0 @@
-# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml
-name: hosted-chat-client-agent
-displayName: "Hosted Chat Client Agent"
-
-description: >
- A simple general-purpose AI assistant hosted as a Foundry Hosted Agent
- using the Agent Framework instance hosting pattern.
-
-metadata:
- tags:
- - AI Agent Hosting
- - Azure AI AgentServer
- - Responses Protocol
- - Streaming
- - Agent Framework
-
-template:
- name: hosted-chat-client-agent
- kind: hosted
- protocols:
- - protocol: responses
- version: 2.0.0
- resources:
- cpu: "0.25"
- memory: 0.5Gi
-parameters:
- properties: []
-resources: []
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/agent.yaml
deleted file mode 100644
index 0f97e93fed7..00000000000
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/agent.yaml
+++ /dev/null
@@ -1,9 +0,0 @@
-# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
-kind: hosted
-name: hosted-chat-client-agent
-protocols:
- - protocol: responses
- version: 2.0.0
-resources:
- cpu: "0.25"
- memory: 0.5Gi
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/Program.cs
index e940f2342bc..6686bf81c9d 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/Program.cs
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/Program.cs
@@ -1,52 +1,30 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ClientModel;
-using System.ClientModel.Primitives;
-using Azure.AI.Extensions.OpenAI;
using Azure.AI.Projects;
-using Azure.Core;
using Azure.Identity;
using DotNetEnv;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Foundry;
+using OpenAI;
+using OpenAI.Responses;
// Load .env file if present (for local development)
Env.TraversePath().Load();
-// FOUNDRY_PROJECT_ENDPOINT is the Foundry project endpoint. Shape:
-// https:///api/projects/
-Uri projectEndpoint = new(Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
- ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set."));
+// Port the Hosted-* samples listen on when run locally with `dotnet run`.
+const int LocalAgentPort = 8088;
// AZURE_AI_AGENT_NAME is the registered server-side agent name.
string agentName = Environment.GetEnvironmentVariable("AZURE_AI_AGENT_NAME")
?? throw new InvalidOperationException("AZURE_AI_AGENT_NAME is not set.");
-// Derive the per-agent OpenAI endpoint that hosted Foundry agents require.
-Uri agentEndpoint = new($"{projectEndpoint}/agents/{agentName}/endpoint/protocols/openai");
+// Ask which server to talk to. This is the same choice `azd ai agent invoke` exposes through its
+// --local flag, asked at startup instead of passed as an argument.
+bool useLocalAgent = PromptForLocalTarget();
-// ── Create an agent-framework agent backed by the remote agent endpoint ──────
-
-// Per-agent client options. The scheme-rewrite policy for local HTTP dev must live on
-// THIS options bag (the per-agent responses pipeline), not on AIProjectClientOptions:
-// FoundryAgent builds the responses client from these options, so a policy added here
-// is the one that actually runs on the /responses request.
-var clientOptions = new ProjectOpenAIClientOptions();
-
-if (agentEndpoint.Scheme == "http")
-{
- // For local HTTP dev: present the endpoint as HTTPS (to satisfy BearerTokenPolicy's
- // TLS check), then swap the scheme back to HTTP right before the request hits the
- // wire. Rewriting the agent endpoint preserves its explicit port (for example 8088).
- agentEndpoint = new UriBuilder(agentEndpoint) { Scheme = "https" }.Uri;
- clientOptions.AddPolicy(new HttpSchemeRewritePolicy(), PipelinePosition.BeforeTransport);
-}
-
-// FoundryAgent's agent-endpoint constructor builds the responses client directly from
-// agentEndpoint (port preserved) and applies clientOptions to that pipeline. It expects a
-// System.ClientModel AuthenticationTokenProvider, so adapt the Azure CLI credential.
-var credential = new AzureCliAuthenticationTokenProvider(new AzureCliCredential(), "https://ai.azure.com/.default");
-FoundryAgent agent = new(agentEndpoint, credential, clientOptions);
+AIAgent agent = useLocalAgent ? CreateLocalAgent() : CreateHostedAgent(agentName);
+string target = useLocalAgent ? $"http://localhost:{LocalAgentPort}" : agentName;
AgentSession session = await agent.CreateSessionAsync();
@@ -56,7 +34,7 @@
Console.WriteLine($"""
══════════════════════════════════════════════════════════
Simple Agent Sample
- Connected to: {agentEndpoint}
+ Connected to: {target}
Type a message or 'quit' to exit
══════════════════════════════════════════════════════════
""");
@@ -99,57 +77,45 @@ Type a message or 'quit' to exit
Console.WriteLine("Goodbye!");
-///
-/// For Local Development Only
-/// Rewrites HTTPS URIs to HTTP right before transport, allowing AIProjectClient
-/// to target a local HTTP dev server while satisfying BearerTokenPolicy's TLS check.
-///
-internal sealed class HttpSchemeRewritePolicy : PipelinePolicy
+// Asks whether to target a locally running agent or the one deployed to Foundry, and returns
+// true for local. Defaults to remote on an empty answer, matching `azd ai agent invoke`, which
+// targets Foundry unless --local is passed.
+static bool PromptForLocalTarget()
{
- public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex)
- {
- RewriteScheme(message);
- ProcessNext(message, pipeline, currentIndex);
- }
+ Console.ForegroundColor = ConsoleColor.Cyan;
+ Console.WriteLine("Which agent do you want to chat with?");
+ Console.ResetColor();
+ Console.WriteLine(" [1] Foundry (deployed agent) [default]");
+ Console.WriteLine($" [2] Local (dotnet run, http://localhost:{LocalAgentPort})");
+ Console.Write("Choice: ");
- public override async ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex)
- {
- RewriteScheme(message);
- await ProcessNextAsync(message, pipeline, currentIndex).ConfigureAwait(false);
- }
+ string? choice = Console.ReadLine()?.Trim();
+ Console.WriteLine();
- private static void RewriteScheme(PipelineMessage message)
- {
- var uri = message.Request.Uri!;
- if (uri.Scheme == Uri.UriSchemeHttps)
- {
- message.Request.Uri = new UriBuilder(uri) { Scheme = "http" }.Uri;
- }
- }
+ return choice is "2";
}
-///
-/// For Local Development Only
-/// Adapts an Azure.Core (for example )
-/// to the System.ClientModel that the
-/// agent-endpoint constructor expects. Uses the fixed Azure AI resource
-/// scope that the Foundry control plane accepts.
-///
-internal sealed class AzureCliAuthenticationTokenProvider(TokenCredential credential, params string[] scopes)
- : AuthenticationTokenProvider
+// Builds an agent against a Hosted-* sample running locally. The sample serves the standard
+// Responses route (POST /responses), so an OpenAI responses client pointed at the server reaches
+// it directly. The server hosts its own agent and ignores both the model id and the api key, but
+// the SDK requires them to shape the request.
+static AIAgent CreateLocalAgent()
{
- public override GetTokenOptions? CreateTokenOptions(IReadOnlyDictionary properties)
- => new(properties);
+ var options = new OpenAIClientOptions { Endpoint = new Uri($"http://localhost:{LocalAgentPort}") };
- public override AuthenticationToken GetToken(GetTokenOptions options, CancellationToken cancellationToken)
- {
- AccessToken token = credential.GetToken(new TokenRequestContext(scopes), cancellationToken);
- return new AuthenticationToken(token.Token, "Bearer", token.ExpiresOn);
- }
+ return new OpenAIClient(new ApiKeyCredential("not-needed"), options)
+ .GetResponsesClient()
+ .AsAIAgent(model: "hosted-agent", name: "LocalHostedAgent");
+}
- public override async ValueTask GetTokenAsync(GetTokenOptions options, CancellationToken cancellationToken)
- {
- AccessToken token = await credential.GetTokenAsync(new TokenRequestContext(scopes), cancellationToken).ConfigureAwait(false);
- return new AuthenticationToken(token.Token, "Bearer", token.ExpiresOn);
- }
+// Builds an agent against an agent deployed to Foundry. Hosted agents are reached through their
+// per-agent endpoint, which the platform routes to the container's /responses route.
+static AIAgent CreateHostedAgent(string agentName)
+{
+ Uri projectEndpoint = new(Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
+ ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set."));
+
+ Uri agentEndpoint = new($"{projectEndpoint}/agents/{agentName}/endpoint/protocols/openai");
+
+ return new AIProjectClient(projectEndpoint, new AzureCliCredential()).AsAIAgent(agentEndpoint);
}
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/README.md
index 6d98560bb27..d3b9abeaf39 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/README.md
@@ -1,8 +1,7 @@
# SimpleAgent
A generic, agent-agnostic chat REPL for any hosted Foundry agent. Point it at a running
-`Hosted-*` agent via `AZURE_AI_AGENT_NAME`, and it builds a `FoundryAgent` against that agent's
-per-agent OpenAI endpoint and streams replies. This is the shared client that `Hosted-Toolbox`,
+`Hosted-*` agent and it streams replies. This is the shared client that `Hosted-Toolbox`,
`Hosted-Toolbox-AuthPaths`, and `Hosted-McpTools` reference for their end-to-end demos.
It knows nothing about the agent's tools, toolboxes, files, or auth — those are entirely the
@@ -18,29 +17,40 @@ See [`../README.md`](../README.md) for why these client REPLs exist at all.
## Configuration
```env
-FOUNDRY_PROJECT_ENDPOINT=https:///api/projects/
AZURE_AI_AGENT_NAME=
+FOUNDRY_PROJECT_ENDPOINT=https:///api/projects/
```
-Both are required. `FOUNDRY_PROJECT_ENDPOINT` is the Foundry project endpoint URL and
-`AZURE_AI_AGENT_NAME` is the registered server-side agent name. The sample builds the per-agent
-OpenAI endpoint URL (`{FOUNDRY_PROJECT_ENDPOINT}/agents/{AZURE_AI_AGENT_NAME}/endpoint/protocols/openai`)
-from these.
+`AZURE_AI_AGENT_NAME` is always required. `FOUNDRY_PROJECT_ENDPOINT` is the Foundry project
+endpoint URL, required only when you target the deployed agent.
## Run
-Against a local Hosted-Toolbox agent listening on `http://localhost:8088`:
-
```powershell
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent
-$env:FOUNDRY_PROJECT_ENDPOINT = "http://localhost:8088/api/projects/local"
-$env:AZURE_AI_AGENT_NAME = "hosted-toolbox-agent"
+$env:AZURE_AI_AGENT_NAME = "hosted-chat-client-agent"
dotnet run
```
-When the project endpoint is `http://`, the client presents it as `https://` to satisfy the
-bearer-token TLS check, then rewrites the scheme back to `http://` right before transport
-(local-development only).
+On startup the client asks which agent to chat with:
+
+```text
+Which agent do you want to chat with?
+ [1] Foundry (deployed agent) [default]
+ [2] Local (dotnet run, http://localhost:8088)
+Choice:
+```
+
+This is the same choice `azd ai agent invoke` exposes through its `--local` flag, asked at
+startup instead of passed as an argument. Pick **2** while a `Hosted-*` sample is running locally
+with `dotnet run`; pick **1** (or press Enter) to reach the agent deployed to Foundry.
+
+The two choices differ only in how the agent is built:
+
+| Target | How the client reaches it |
+|--------|---------------------------|
+| Local | An `OpenAIClient` pointed at `http://localhost:8088`, then `GetResponsesClient().AsAIAgent(...)`. That hits the standard `POST /responses` route the local server already serves. The model id and api key are placeholders: the server runs its own agent and ignores both, but the SDK requires them to shape the request. |
+| Foundry | An `AIProjectClient` plus the agent's per-agent endpoint (`{projectEndpoint}/agents/{AZURE_AI_AGENT_NAME}/endpoint/protocols/openai`), which the platform routes to the container's `/responses` route. |
## End-to-end demo
@@ -49,7 +59,7 @@ With a hosted agent running:
```text
══════════════════════════════════════════════════════════
Simple Agent Sample
-Connected to: https://localhost:8088/api/projects/local/agents/hosted-toolbox-agent/endpoint/protocols/openai
+Connected to: http://localhost:8088
Type a message or 'quit' to exit
══════════════════════════════════════════════════════════
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/SimpleAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/SimpleAgent.csproj
index 5e6d95dbf64..68f98dd6147 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/SimpleAgent.csproj
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/SimpleAgent.csproj
@@ -13,14 +13,15 @@
-
+
+
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/New-ContributorStage.ps1 b/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/New-ContributorStage.ps1
new file mode 100644
index 00000000000..d9411f7440c
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/New-ContributorStage.ps1
@@ -0,0 +1,176 @@
+#requires -Version 7
+<#
+.SYNOPSIS
+ Stages a hosted-agent sample in a temporary folder wired to build against the local
+ Agent Framework source, so `azd` deploys your framework changes instead of the published packages.
+.DESCRIPTION
+ Source (ZIP) deploy uploads the sample folder and Foundry runs `dotnet restore` + `dotnet publish`
+ on it in the cloud. That restore pulls the Agent Framework from nuget.org, so a contributor's local
+ framework changes are never exercised.
+
+ This script produces a staging copy whose restore resolves the Agent Framework from packages
+ carried inside the upload:
+
+ 1. Copies the sample into a fresh temp folder (the repo working tree is left untouched).
+ 2. Builds and packs the Agent Framework projects the samples depend on into `src/local-feed`,
+ stamped with a version derived from the repo's current VersionPrefix plus a `-preview-local`
+ suffix. The whole closure is packed: packing only the leaf packages lets NuGet fill the rest
+ from nuget.org, mixing a published core with a locally built host.
+ 3. Writes `src/nuget.config`, mapping Microsoft.Agents.AI* to that folder feed and everything
+ else to nuget.org.
+ 4. Writes `src/local-feed.props`, which the sample's project file imports to pin the Agent
+ Framework version to the one just packed.
+
+ `local-feed/`, `nuget.config`, and `local-feed.props` are not excluded by `.agentignore`, so they
+ travel inside the ZIP and the server-side restore uses them.
+
+ The staged layout keeps the standard end-user flow intact: `run/` is the empty working directory
+ you run `azd` from, and `src/` is the sample that `azd ai agent init -m` adopts. The script prints
+ the exact commands to run when it finishes.
+.PARAMETER Sample
+ Sample folder name under `responses/` or `invocations/`, for example `Hosted-ChatClientAgent`.
+.PARAMETER ProjectId
+ Optional Foundry project resource ID, used only to print a ready-to-paste `init` command.
+.PARAMETER ModelDeployment
+ Optional model deployment name, used only to print a ready-to-paste `init` command.
+.EXAMPLE
+ ./New-ContributorStage.ps1 -Sample Hosted-ChatClientAgent
+.EXAMPLE
+ ./New-ContributorStage.ps1 -Sample Hosted-ChatClientAgent -ProjectId "/subscriptions/.../projects/my-project" -ModelDeployment gpt-5.4-mini
+.NOTES
+ For contributors validating framework changes end to end. End users deploy the sample directly
+ from the repo folder and get the published packages.
+#>
+
+[CmdletBinding()]
+param(
+ [Parameter(Mandatory)]
+ [string]$Sample,
+
+ [string]$ProjectId,
+
+ [string]$ModelDeployment
+)
+
+$ErrorActionPreference = 'Stop'
+
+# The Agent Framework closure the hosted samples resolve. Packing only the leaf packages makes
+# NuGet satisfy the rest from nuget.org, producing assembly-reference errors at build time.
+$frameworkProjects = @(
+ 'Microsoft.Agents.AI.Abstractions'
+ 'Microsoft.Agents.AI'
+ 'Microsoft.Agents.AI.Workflows'
+ 'Microsoft.Agents.AI.Foundry'
+ 'Microsoft.Agents.AI.Foundry.Hosting'
+)
+
+$hostedRoot = Split-Path -Parent $PSScriptRoot
+$dotnetRoot = (Resolve-Path (Join-Path $hostedRoot '..\..\..')).Path
+$srcRoot = Join-Path $dotnetRoot 'src'
+
+$samplePath = @('responses', 'invocations')
+| ForEach-Object { Join-Path $hostedRoot "$_\$Sample" }
+| Where-Object { Test-Path $_ }
+| Select-Object -First 1
+
+if (-not $samplePath) {
+ throw "Sample '$Sample' not found under $hostedRoot\responses or $hostedRoot\invocations."
+}
+
+# Derive the package version from the repo so the staged packages track the current release line.
+# The timestamp keeps every run unique: NuGet caches by id and version, so reusing a version would
+# silently restore the previously packed bits instead of the build you just made.
+$packagePropsPath = Join-Path $dotnetRoot 'nuget\nuget-package.props'
+$versionPrefix = (Select-String -Path $packagePropsPath -Pattern '(.+?)').Matches[0].Groups[1].Value
+$version = "$versionPrefix-preview-local.$(Get-Date -Format 'yyyyMMddHHmmss')"
+
+$stageRoot = Join-Path ([System.IO.Path]::GetTempPath()) "af-hosted-$Sample-$([System.IO.Path]::GetRandomFileName().Substring(0, 8))"
+$stageSrc = Join-Path $stageRoot 'src'
+$stageRun = Join-Path $stageRoot 'run'
+$feedPath = Join-Path $stageSrc 'local-feed'
+
+New-Item -ItemType Directory -Path $stageSrc, $stageRun, $feedPath -Force | Out-Null
+
+Write-Host "Staging $Sample" -ForegroundColor Cyan
+Write-Host " version: $version"
+Write-Host " stage: $stageRoot"
+Write-Host ''
+
+# Local-only state (.env, build output, azd environments) must not leak into the staged copy.
+$excludedDirs = @('.azure', '.checkpoints', 'bin', 'obj', 'scripts')
+$excludedFiles = @('.env')
+robocopy $samplePath $stageSrc /E /XD $excludedDirs /XF $excludedFiles /NFL /NDL /NP /NJH /NJS | Out-Null
+if ($LASTEXITCODE -ge 8) {
+ throw "Failed to copy the sample (robocopy exit code $LASTEXITCODE)."
+}
+
+foreach ($project in $frameworkProjects) {
+ $projectPath = Join-Path $srcRoot "$project\$project.csproj"
+ Write-Host "Packing $project..."
+
+ # Debug, not Release: the Release configuration runs the repo's formatting and analyzer passes,
+ # which rewrite source files and fail the build on style violations. Staging only needs runnable
+ # binaries, so Debug keeps the working tree untouched.
+ #
+ # PackageVersion (not Version) is the property the repo's packaging props use to stamp both the
+ # package version and its dependency ranges, so the packed packages reference each other at this
+ # version instead of the bare VersionPrefix.
+ dotnet build $projectPath -c Debug -p:PackageVersion=$version --tl:off | Out-Null
+ if ($LASTEXITCODE -ne 0) { throw "Build failed for $project." }
+
+ dotnet pack $projectPath -c Debug --no-build -o $feedPath -p:PackageVersion=$version --tl:off | Out-Null
+ if ($LASTEXITCODE -ne 0) { throw "Pack failed for $project." }
+}
+
+$utf8NoBom = [System.Text.UTF8Encoding]::new($false)
+
+$nugetConfig = @'
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+'@
+[System.IO.File]::WriteAllText((Join-Path $stageSrc 'nuget.config'), ($nugetConfig -replace "`r`n", "`n"), $utf8NoBom)
+
+$props = @"
+
+
+
+ $version
+
+
+"@
+[System.IO.File]::WriteAllText((Join-Path $stageSrc 'local-feed.props'), ($props -replace "`r`n", "`n"), $utf8NoBom)
+
+$initArguments = "-m `"$stageSrc\azure.yaml`""
+if ($ProjectId) { $initArguments += " -p `"$ProjectId`"" }
+if ($ModelDeployment) { $initArguments += " -d $ModelDeployment" }
+
+# `azd ai agent init -m` scaffolds into a folder named after the top-level `name` in azure.yaml,
+# which is the agent name and does not have to match the sample's folder name.
+$scaffoldName = (Select-String -Path (Join-Path $stageSrc 'azure.yaml') -Pattern '(?m)^name:\s*(\S+)').Matches[0].Groups[1].Value
+
+Write-Host ''
+Write-Host 'Stage ready. Run the standard flow against it:' -ForegroundColor Green
+Write-Host ''
+Write-Host " cd `"$stageRun`""
+Write-Host " azd ai agent init $initArguments"
+Write-Host " cd $scaffoldName"
+Write-Host ' azd provision'
+Write-Host ' azd deploy'
+Write-Host ' azd ai agent invoke "Hello!"'
+Write-Host ''
+Write-Host "Delete $stageRoot when you are done."
diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ServiceCollectionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ServiceCollectionExtensions.cs
index 79c09d9dc76..ec908e59c93 100644
--- a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ServiceCollectionExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ServiceCollectionExtensions.cs
@@ -261,29 +261,29 @@ private sealed class FoundryListenPortMarker;
///
/// Binds Kestrel to the port the Foundry hosted runtime probes and routes to, so a plain
- /// WebApplication.CreateBuilder host (Tier 3) works with no Dockerfile and no
- /// ASPNETCORE_URLS. Mirrors AgentHostBuilder, which listens on
- /// (the PORT environment variable, default 8088).
+ /// WebApplication.CreateBuilder host (Tier 3) works with no Dockerfile. Mirrors
+ /// AgentHostBuilder, which listens on (the
+ /// PORT environment variable, default 8088).
///
///
///
- /// No-op when ASPNETCORE_URLS is set: an explicit ASP.NET Core URL binding always wins,
- /// so a developer can override the port locally (for example ASPNETCORE_URLS=http://+:9000).
- /// The Foundry hosted runtime injects PORT (not ASPNETCORE_URLS), so the container
- /// path always flows through here.
+ /// The binding is unconditional, which is what makes source (ZIP) deploy work: the .NET base
+ /// image sets ASPNETCORE_URLS to port 80, and a listener configured in code takes
+ /// precedence over that setting (Kestrel logs "Overriding address(es)"). Skipping the binding
+ /// whenever ASPNETCORE_URLS was set would therefore always leave the container on port
+ /// 80 and fail the readiness probe with HTTP 424.
///
///
- /// Idempotent and harmless when no Kestrel server is present (for example unit tests): the
- /// options callback is only invoked when Kestrel is resolved.
+ /// To listen on a different port, set PORT, the same knob the Agent Server SDK uses.
+ ///
+ ///
+ /// Idempotent, and harmless when no Kestrel server is present (for example under
+ /// TestServer): the callback only runs when Kestrel
+ /// is resolved.
///
///
private static void ConfigureFoundryListenPort(IServiceCollection services)
{
- if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("ASPNETCORE_URLS")))
- {
- return;
- }
-
if (services.Any(static d => d.ServiceType == typeof(FoundryListenPortMarker)))
{
return;
diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryListenPortTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryListenPortTests.cs
index 2884e8138e2..d5a641a7194 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryListenPortTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryListenPortTests.cs
@@ -17,8 +17,8 @@ namespace Microsoft.Agents.AI.Foundry.Hosting.UnitTests;
///
/// Verifies that AddFoundryResponses binds Kestrel to the Foundry hosted-runtime port
/// (, the PORT env var, default 8088) for a plain
-/// WebApplication.CreateBuilder (Tier 3) host, so the sample works with no Dockerfile and
-/// no ASPNETCORE_URLS, while still respecting an explicit ASPNETCORE_URLS override.
+/// WebApplication.CreateBuilder (Tier 3) host, so a source (ZIP) deployed agent passes the
+/// platform readiness probe with no Dockerfile pinning the port.
///
[Collection(AspNetCoreUrlsEnvFixture.Name)]
public sealed class FoundryListenPortTests
@@ -26,63 +26,46 @@ public sealed class FoundryListenPortTests
private const string AspNetCoreUrls = "ASPNETCORE_URLS";
[Fact]
- public void AddFoundryResponses_NoAspNetCoreUrls_BindsFoundryPort()
+ public void AddFoundryResponses_BindsFoundryPort()
{
// Arrange
- var original = Environment.GetEnvironmentVariable(AspNetCoreUrls);
- try
- {
- Environment.SetEnvironmentVariable(AspNetCoreUrls, null);
- var services = new ServiceCollection();
- services.AddLogging();
+ var services = new ServiceCollection();
+ services.AddLogging();
- // Act
- services.AddFoundryResponses();
+ // Act
+ services.AddFoundryResponses();
- // Assert
- var ports = GetCodeBackedPorts(services);
- Assert.Contains(FoundryEnvironment.Port, ports);
- }
- finally
- {
- Environment.SetEnvironmentVariable(AspNetCoreUrls, original);
- }
+ // Assert
+ Assert.Contains(FoundryEnvironment.Port, GetCodeBackedPorts(services));
}
[Fact]
- public void AddFoundryResponses_WithAgent_NoAspNetCoreUrls_BindsFoundryPort()
+ public void AddFoundryResponses_WithAgent_BindsFoundryPort()
{
// Arrange
- var original = Environment.GetEnvironmentVariable(AspNetCoreUrls);
- try
- {
- Environment.SetEnvironmentVariable(AspNetCoreUrls, null);
- var services = new ServiceCollection();
- services.AddLogging();
- var mockAgent = new Mock();
- mockAgent.SetupGet(a => a.Name).Returns("test-agent");
+ var services = new ServiceCollection();
+ services.AddLogging();
+ var mockAgent = new Mock();
+ mockAgent.SetupGet(a => a.Name).Returns("test-agent");
- // Act
- services.AddFoundryResponses(mockAgent.Object);
+ // Act
+ services.AddFoundryResponses(mockAgent.Object);
- // Assert
- var ports = GetCodeBackedPorts(services);
- Assert.Contains(FoundryEnvironment.Port, ports);
- }
- finally
- {
- Environment.SetEnvironmentVariable(AspNetCoreUrls, original);
- }
+ // Assert
+ Assert.Contains(FoundryEnvironment.Port, GetCodeBackedPorts(services));
}
[Fact]
- public void AddFoundryResponses_WithAspNetCoreUrls_DoesNotBindFoundryPort()
+ public void AddFoundryResponses_WithAspNetCoreUrlsSet_StillBindsFoundryPort()
{
- // Arrange: an explicit ASP.NET Core URL binding must win; the package adds no listener.
+ // Arrange: the .NET base image used by source (ZIP) deploy sets ASPNETCORE_URLS to port 80.
+ // The binding must still be applied, because a listener configured in code takes precedence
+ // over that setting. Skipping it here would leave the container on port 80 and fail every
+ // invocation with HTTP 424 session_not_ready.
var original = Environment.GetEnvironmentVariable(AspNetCoreUrls);
try
{
- Environment.SetEnvironmentVariable(AspNetCoreUrls, "http://+:9123");
+ Environment.SetEnvironmentVariable(AspNetCoreUrls, "http://+:80");
var services = new ServiceCollection();
services.AddLogging();
@@ -90,9 +73,7 @@ public void AddFoundryResponses_WithAspNetCoreUrls_DoesNotBindFoundryPort()
services.AddFoundryResponses();
// Assert
- var ports = GetCodeBackedPorts(services);
- Assert.DoesNotContain(FoundryEnvironment.Port, ports);
- Assert.DoesNotContain(9123, ports);
+ Assert.Contains(FoundryEnvironment.Port, GetCodeBackedPorts(services));
}
finally
{
@@ -104,26 +85,16 @@ public void AddFoundryResponses_WithAspNetCoreUrls_DoesNotBindFoundryPort()
public void AddFoundryResponses_CalledTwice_BindsFoundryPortOnce()
{
// Arrange
- var original = Environment.GetEnvironmentVariable(AspNetCoreUrls);
- try
- {
- Environment.SetEnvironmentVariable(AspNetCoreUrls, null);
- var services = new ServiceCollection();
- services.AddLogging();
+ var services = new ServiceCollection();
+ services.AddLogging();
- // Act
- services.AddFoundryResponses();
- services.AddFoundryResponses();
+ // Act
+ services.AddFoundryResponses();
+ services.AddFoundryResponses();
- // Assert: the listen port is configured exactly once, not duplicated (a duplicate
- // ListenAnyIP on the same port would fail Kestrel startup with "address already in use").
- var ports = GetCodeBackedPorts(services);
- Assert.Equal(1, ports.Count(p => p == FoundryEnvironment.Port));
- }
- finally
- {
- Environment.SetEnvironmentVariable(AspNetCoreUrls, original);
- }
+ // Assert: a duplicate ListenAnyIP on the same port fails Kestrel startup with
+ // "address already in use", so the binding must be applied exactly once.
+ Assert.Equal(1, GetCodeBackedPorts(services).Count(port => port == FoundryEnvironment.Port));
}
///
From ea1b14aace5eb1e4fcb078e6cc47baa5964df786 Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Tue, 28 Jul 2026 13:06:06 +0100
Subject: [PATCH 04/23] Pin hosted agent listen port in azure.yaml
---
.../responses/Hosted-ChatClientAgent/README.md | 6 +++++-
.../responses/Hosted-ChatClientAgent/azure.yaml | 9 +++++++++
2 files changed, 14 insertions(+), 1 deletion(-)
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
index 0e95bdd8e61..f984fac920c 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
@@ -16,7 +16,7 @@ This sample deploys to Foundry **directly from source (code / ZIP upload)**: the
| File | Purpose |
|------|---------|
| `Program.cs` | The agent: builds the agent, hosts it with the Responses protocol. |
-| `azure.yaml` | The unified `azd` project file. Declares the Foundry project and the hosted agent with `codeConfiguration` (source/ZIP deploy). |
+| `azure.yaml` | The unified `azd` project file. Declares the Foundry project and the hosted agent with `codeConfiguration` (source/ZIP deploy), and pins the listen port through `environment_variables`. |
| `.agentignore` | Controls which files are excluded from the code-deploy ZIP upload (`.gitignore` syntax). |
| `HostedChatClientAgent.csproj` | Self-contained project: single target framework, central package management off, explicit package versions (there is no repo-level props file inside the ZIP). |
| `Directory.Packages.props` | Stops inheritance of the repository's central package management so the in-repo build matches the server-side build of the uploaded ZIP. |
@@ -38,6 +38,10 @@ AZURE_TOKEN_CREDENTIALS=dev
> `.env` is gitignored. The `.env.example` template is checked in as a reference.
+> **Windows note:** write `.env` as UTF-8 **without** a byte order mark. `azd` reads the file
+> during `azd ai agent init` and fails with `unexpected character "»" in variable name` when a mark
+> is present. PowerShell's `Set-Content -Encoding UTF8BOM` adds one; use `-Encoding utf8NoBOM`.
+
> **Local development on a machine without a managed identity:** set `AZURE_TOKEN_CREDENTIALS=dev`.
> `Program.cs` authenticates with `DefaultAzureCredential` (the pattern the hosted platform
> expects, where a managed identity is injected). On a developer machine with no managed identity,
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/azure.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/azure.yaml
index 8e186d052a2..a907f02352d 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/azure.yaml
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/azure.yaml
@@ -14,6 +14,15 @@ services:
dependencyResolution: remote_build
entryPoint: HostedChatClientAgent.dll
runtime: dotnet_10
+ # Pins the port the app listens on to the one Foundry probes for readiness.
+ # Source deploy runs this project as a plain ASP.NET app, and the .NET base image
+ # defaults ASPNETCORE_URLS to port 80, so without this the readiness probe never
+ # succeeds and every invoke fails with HTTP 424 session_not_ready.
+ # Recent Microsoft.Agents.AI.Foundry.Hosting versions bind the port themselves, which
+ # takes precedence over this value, so keeping it only helps when the project is pinned
+ # to an older published package.
+ environment_variables:
+ ASPNETCORE_URLS: http://+:8088
container:
resources:
cpu: "0.5"
From b331394e9eb109c0a750efe15377c618764a6524 Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Tue, 28 Jul 2026 13:26:13 +0100
Subject: [PATCH 05/23] Use the documented env map in azure.yaml
---
.../responses/Hosted-ChatClientAgent/azure.yaml | 15 +++++++--------
1 file changed, 7 insertions(+), 8 deletions(-)
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/azure.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/azure.yaml
index a907f02352d..c862d3d9f35 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/azure.yaml
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/azure.yaml
@@ -14,14 +14,13 @@ services:
dependencyResolution: remote_build
entryPoint: HostedChatClientAgent.dll
runtime: dotnet_10
- # Pins the port the app listens on to the one Foundry probes for readiness.
- # Source deploy runs this project as a plain ASP.NET app, and the .NET base image
- # defaults ASPNETCORE_URLS to port 80, so without this the readiness probe never
- # succeeds and every invoke fails with HTTP 424 session_not_ready.
- # Recent Microsoft.Agents.AI.Foundry.Hosting versions bind the port themselves, which
- # takes precedence over this value, so keeping it only helps when the project is pinned
- # to an older published package.
- environment_variables:
+ # Passes the listen port to the container. Source deploy runs this project as a plain
+ # ASP.NET app, and the .NET base image defaults ASPNETCORE_URLS to port 80, while Foundry
+ # probes port 8088 for readiness, so without this every invoke fails with HTTP 424
+ # session_not_ready. Recent Microsoft.Agents.AI.Foundry.Hosting versions bind the port
+ # themselves and take precedence over this value, so it only matters when the project is
+ # pinned to an older published package.
+ env:
ASPNETCORE_URLS: http://+:8088
container:
resources:
From c9c70e811735f0bf74d8e7cd59333b0881a18da6 Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Tue, 28 Jul 2026 14:21:08 +0100
Subject: [PATCH 06/23] Make the contributor flow an extra step inside the
end-user flow
---
.../Hosted-ChatClientAgent/.agentignore | 2 +-
.../HostedChatClientAgent.csproj | 4 +-
.../Hosted-ChatClientAgent/README.md | 34 +++-
.../scripts/Add-LocalFrameworkFeed.ps1 | 152 +++++++++++++++
.../scripts/New-ContributorStage.ps1 | 176 ------------------
5 files changed, 181 insertions(+), 187 deletions(-)
create mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1
delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/scripts/New-ContributorStage.ps1
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/.agentignore b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/.agentignore
index 365ce70ffe8..9833ed3d78f 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/.agentignore
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/.agentignore
@@ -25,6 +25,6 @@ obj/
# would ship stale sessions with the agent.
.checkpoints/
-# Contributor staging (scripts/New-ContributorStage.ps1) generates local-feed/, nuget.config,
+# Contributor mode (scripts/Add-LocalFrameworkFeed.ps1) generates local-feed/, nuget.config,
# and local-feed.props. Those are deliberately NOT excluded: the server-side restore needs them
# to resolve the Agent Framework from the packages shipped in this upload instead of nuget.org.
\ No newline at end of file
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/HostedChatClientAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/HostedChatClientAgent.csproj
index 3b1534c722c..b0a9713ae66 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/HostedChatClientAgent.csproj
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/HostedChatClientAgent.csproj
@@ -10,8 +10,8 @@
-->
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+'@
+[System.IO.File]::WriteAllText((Join-Path $target 'nuget.config'), ($nugetConfig -replace "`r`n", "`n"), $utf8NoBom)
+
+$props = @"
+
+
+
+ $version
+
+
+"@
+[System.IO.File]::WriteAllText((Join-Path $target 'local-feed.props'), ($props -replace "`r`n", "`n"), $utf8NoBom)
+
+Write-Host ''
+Write-Host 'Done. Continue with the standard flow:' -ForegroundColor Green
+Write-Host ''
+Write-Host " cd `"$target`""
+Write-Host ' azd provision'
+Write-Host ' azd deploy'
+Write-Host ' azd ai agent invoke "Hello!"'
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/New-ContributorStage.ps1 b/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/New-ContributorStage.ps1
deleted file mode 100644
index d9411f7440c..00000000000
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/New-ContributorStage.ps1
+++ /dev/null
@@ -1,176 +0,0 @@
-#requires -Version 7
-<#
-.SYNOPSIS
- Stages a hosted-agent sample in a temporary folder wired to build against the local
- Agent Framework source, so `azd` deploys your framework changes instead of the published packages.
-.DESCRIPTION
- Source (ZIP) deploy uploads the sample folder and Foundry runs `dotnet restore` + `dotnet publish`
- on it in the cloud. That restore pulls the Agent Framework from nuget.org, so a contributor's local
- framework changes are never exercised.
-
- This script produces a staging copy whose restore resolves the Agent Framework from packages
- carried inside the upload:
-
- 1. Copies the sample into a fresh temp folder (the repo working tree is left untouched).
- 2. Builds and packs the Agent Framework projects the samples depend on into `src/local-feed`,
- stamped with a version derived from the repo's current VersionPrefix plus a `-preview-local`
- suffix. The whole closure is packed: packing only the leaf packages lets NuGet fill the rest
- from nuget.org, mixing a published core with a locally built host.
- 3. Writes `src/nuget.config`, mapping Microsoft.Agents.AI* to that folder feed and everything
- else to nuget.org.
- 4. Writes `src/local-feed.props`, which the sample's project file imports to pin the Agent
- Framework version to the one just packed.
-
- `local-feed/`, `nuget.config`, and `local-feed.props` are not excluded by `.agentignore`, so they
- travel inside the ZIP and the server-side restore uses them.
-
- The staged layout keeps the standard end-user flow intact: `run/` is the empty working directory
- you run `azd` from, and `src/` is the sample that `azd ai agent init -m` adopts. The script prints
- the exact commands to run when it finishes.
-.PARAMETER Sample
- Sample folder name under `responses/` or `invocations/`, for example `Hosted-ChatClientAgent`.
-.PARAMETER ProjectId
- Optional Foundry project resource ID, used only to print a ready-to-paste `init` command.
-.PARAMETER ModelDeployment
- Optional model deployment name, used only to print a ready-to-paste `init` command.
-.EXAMPLE
- ./New-ContributorStage.ps1 -Sample Hosted-ChatClientAgent
-.EXAMPLE
- ./New-ContributorStage.ps1 -Sample Hosted-ChatClientAgent -ProjectId "/subscriptions/.../projects/my-project" -ModelDeployment gpt-5.4-mini
-.NOTES
- For contributors validating framework changes end to end. End users deploy the sample directly
- from the repo folder and get the published packages.
-#>
-
-[CmdletBinding()]
-param(
- [Parameter(Mandatory)]
- [string]$Sample,
-
- [string]$ProjectId,
-
- [string]$ModelDeployment
-)
-
-$ErrorActionPreference = 'Stop'
-
-# The Agent Framework closure the hosted samples resolve. Packing only the leaf packages makes
-# NuGet satisfy the rest from nuget.org, producing assembly-reference errors at build time.
-$frameworkProjects = @(
- 'Microsoft.Agents.AI.Abstractions'
- 'Microsoft.Agents.AI'
- 'Microsoft.Agents.AI.Workflows'
- 'Microsoft.Agents.AI.Foundry'
- 'Microsoft.Agents.AI.Foundry.Hosting'
-)
-
-$hostedRoot = Split-Path -Parent $PSScriptRoot
-$dotnetRoot = (Resolve-Path (Join-Path $hostedRoot '..\..\..')).Path
-$srcRoot = Join-Path $dotnetRoot 'src'
-
-$samplePath = @('responses', 'invocations')
-| ForEach-Object { Join-Path $hostedRoot "$_\$Sample" }
-| Where-Object { Test-Path $_ }
-| Select-Object -First 1
-
-if (-not $samplePath) {
- throw "Sample '$Sample' not found under $hostedRoot\responses or $hostedRoot\invocations."
-}
-
-# Derive the package version from the repo so the staged packages track the current release line.
-# The timestamp keeps every run unique: NuGet caches by id and version, so reusing a version would
-# silently restore the previously packed bits instead of the build you just made.
-$packagePropsPath = Join-Path $dotnetRoot 'nuget\nuget-package.props'
-$versionPrefix = (Select-String -Path $packagePropsPath -Pattern '(.+?)').Matches[0].Groups[1].Value
-$version = "$versionPrefix-preview-local.$(Get-Date -Format 'yyyyMMddHHmmss')"
-
-$stageRoot = Join-Path ([System.IO.Path]::GetTempPath()) "af-hosted-$Sample-$([System.IO.Path]::GetRandomFileName().Substring(0, 8))"
-$stageSrc = Join-Path $stageRoot 'src'
-$stageRun = Join-Path $stageRoot 'run'
-$feedPath = Join-Path $stageSrc 'local-feed'
-
-New-Item -ItemType Directory -Path $stageSrc, $stageRun, $feedPath -Force | Out-Null
-
-Write-Host "Staging $Sample" -ForegroundColor Cyan
-Write-Host " version: $version"
-Write-Host " stage: $stageRoot"
-Write-Host ''
-
-# Local-only state (.env, build output, azd environments) must not leak into the staged copy.
-$excludedDirs = @('.azure', '.checkpoints', 'bin', 'obj', 'scripts')
-$excludedFiles = @('.env')
-robocopy $samplePath $stageSrc /E /XD $excludedDirs /XF $excludedFiles /NFL /NDL /NP /NJH /NJS | Out-Null
-if ($LASTEXITCODE -ge 8) {
- throw "Failed to copy the sample (robocopy exit code $LASTEXITCODE)."
-}
-
-foreach ($project in $frameworkProjects) {
- $projectPath = Join-Path $srcRoot "$project\$project.csproj"
- Write-Host "Packing $project..."
-
- # Debug, not Release: the Release configuration runs the repo's formatting and analyzer passes,
- # which rewrite source files and fail the build on style violations. Staging only needs runnable
- # binaries, so Debug keeps the working tree untouched.
- #
- # PackageVersion (not Version) is the property the repo's packaging props use to stamp both the
- # package version and its dependency ranges, so the packed packages reference each other at this
- # version instead of the bare VersionPrefix.
- dotnet build $projectPath -c Debug -p:PackageVersion=$version --tl:off | Out-Null
- if ($LASTEXITCODE -ne 0) { throw "Build failed for $project." }
-
- dotnet pack $projectPath -c Debug --no-build -o $feedPath -p:PackageVersion=$version --tl:off | Out-Null
- if ($LASTEXITCODE -ne 0) { throw "Pack failed for $project." }
-}
-
-$utf8NoBom = [System.Text.UTF8Encoding]::new($false)
-
-$nugetConfig = @'
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-'@
-[System.IO.File]::WriteAllText((Join-Path $stageSrc 'nuget.config'), ($nugetConfig -replace "`r`n", "`n"), $utf8NoBom)
-
-$props = @"
-
-
-
- $version
-
-
-"@
-[System.IO.File]::WriteAllText((Join-Path $stageSrc 'local-feed.props'), ($props -replace "`r`n", "`n"), $utf8NoBom)
-
-$initArguments = "-m `"$stageSrc\azure.yaml`""
-if ($ProjectId) { $initArguments += " -p `"$ProjectId`"" }
-if ($ModelDeployment) { $initArguments += " -d $ModelDeployment" }
-
-# `azd ai agent init -m` scaffolds into a folder named after the top-level `name` in azure.yaml,
-# which is the agent name and does not have to match the sample's folder name.
-$scaffoldName = (Select-String -Path (Join-Path $stageSrc 'azure.yaml') -Pattern '(?m)^name:\s*(\S+)').Matches[0].Groups[1].Value
-
-Write-Host ''
-Write-Host 'Stage ready. Run the standard flow against it:' -ForegroundColor Green
-Write-Host ''
-Write-Host " cd `"$stageRun`""
-Write-Host " azd ai agent init $initArguments"
-Write-Host " cd $scaffoldName"
-Write-Host ' azd provision'
-Write-Host ' azd deploy'
-Write-Host ' azd ai agent invoke "Hello!"'
-Write-Host ''
-Write-Host "Delete $stageRoot when you are done."
From a8b505dd01ff0c6b99ee43e82b49cbec076d6f4e Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Tue, 28 Jul 2026 14:27:13 +0100
Subject: [PATCH 07/23] Keep contributor scaffolding out of the sample project
file
---
.../Hosted-ChatClientAgent/.agentignore | 6 +--
.../HostedChatClientAgent.csproj | 14 +------
.../Hosted-ChatClientAgent/README.md | 9 +++--
.../scripts/Add-LocalFrameworkFeed.ps1 | 40 +++++++++----------
4 files changed, 29 insertions(+), 40 deletions(-)
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/.agentignore b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/.agentignore
index 9833ed3d78f..f45144759ff 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/.agentignore
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/.agentignore
@@ -25,6 +25,6 @@ obj/
# would ship stale sessions with the agent.
.checkpoints/
-# Contributor mode (scripts/Add-LocalFrameworkFeed.ps1) generates local-feed/, nuget.config,
-# and local-feed.props. Those are deliberately NOT excluded: the server-side restore needs them
-# to resolve the Agent Framework from the packages shipped in this upload instead of nuget.org.
\ No newline at end of file
+# Contributor mode (scripts/Add-LocalFrameworkFeed.ps1) generates local-feed/ and nuget.config.
+# Those are deliberately NOT excluded: the server-side restore needs them to resolve the Agent
+# Framework from the packages shipped in this upload instead of nuget.org.
\ No newline at end of file
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/HostedChatClientAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/HostedChatClientAgent.csproj
index b0a9713ae66..04f99e71ce3 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/HostedChatClientAgent.csproj
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/HostedChatClientAgent.csproj
@@ -9,17 +9,6 @@
(dependencyResolution: remote_build in azure.yaml).
-->
-
-
-
- 1.15.0-preview.260722.1
+ 1.15.0-preview.260722.1
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
index 2b3359422ad..8a82cf0470b 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
@@ -125,10 +125,11 @@ after `azd ai agent init` and before `azd provision`:
```
The script packs the local Agent Framework source into a `local-feed` folder inside the scaffolded
-agent folder and writes the `nuget.config` and `local-feed.props` that point the build at those
-packages. All three travel inside the ZIP, so the server-side restore resolves the framework from
-the upload. Everything else, including `azd provision`, `azd deploy`, and `azd ai agent invoke`,
-is unchanged.
+agent folder, writes the `nuget.config` that points the build at those packages, and repoints the
+project file's `AgentFrameworkVersion` at the version it just packed. Both generated files travel
+inside the ZIP, so the server-side restore resolves the framework from the upload. The scaffolded
+folder is a throwaway copy, so the repository is left untouched. Everything else, including
+`azd provision`, `azd deploy`, and `azd ai agent invoke`, is unchanged.
## Troubleshooting
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1 b/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1
index e55ee51b8da..90885a2a050 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1
@@ -8,22 +8,25 @@
on it in the cloud. That restore pulls the Agent Framework from nuget.org, so a contributor's
local framework changes are never exercised.
- Run this after `azd ai agent init` and before `azd provision`. It drops three files into the
+ Run this after `azd ai agent init` and before `azd provision`. It changes two things in the
folder that `init` scaffolded:
- local-feed/ The Agent Framework packed from the local source tree, stamped with a version
- derived from the repo's current VersionPrefix plus a `-preview-local` suffix.
- The whole closure is packed: packing only the leaf packages lets NuGet fill
- the rest from nuget.org, mixing a published core with a locally built host.
- nuget.config Maps Microsoft.Agents.AI* to that folder feed and everything else to nuget.org.
- local-feed.props Pins the Agent Framework version to the one just packed. The sample's project
- file imports it when present.
+ local-feed/ New. The Agent Framework packed from the local source tree, stamped with a
+ version derived from the repo's current VersionPrefix plus a `-preview-local`
+ suffix. The whole closure is packed: packing only the leaf packages lets NuGet
+ fill the rest from nuget.org, mixing a published core with a locally built host.
+ nuget.config New. Maps Microsoft.Agents.AI* to that folder feed and everything else to
+ nuget.org.
+ the .csproj Edited. Its AgentFrameworkVersion property is repointed at the version just
+ packed.
- None of the three are excluded by `.agentignore`, so they travel inside the ZIP and the
+ Neither generated file is excluded by `.agentignore`, so they travel inside the ZIP and the
server-side restore uses them.
Everything else stays identical to the end-user flow: you create the working directory, run
`azd ai agent init`, and finish with `azd provision`, `azd deploy`, and `azd ai agent invoke`.
+ The scaffolded folder is a throwaway copy, so editing its project file leaves the repository
+ untouched.
.PARAMETER Path
The folder `azd ai agent init` scaffolded, for example `./hosted-chat-client-agent`.
Defaults to the current directory.
@@ -67,8 +70,8 @@ if (-not $projectFile) {
throw "No .csproj in '$target'. This script targets .NET hosted agents."
}
-if (-not (Select-String -Path $projectFile.FullName -Pattern 'local-feed\.props' -Quiet)) {
- throw "$($projectFile.Name) does not import local-feed.props, so it cannot pick up a local build."
+if (-not (Select-String -Path $projectFile.FullName -Pattern '' -Quiet)) {
+ throw "$($projectFile.Name) has no property to repoint at a local build."
}
$hostedRoot = Split-Path -Parent $PSScriptRoot
@@ -133,15 +136,12 @@ $nugetConfig = @'
'@
[System.IO.File]::WriteAllText((Join-Path $target 'nuget.config'), ($nugetConfig -replace "`r`n", "`n"), $utf8NoBom)
-$props = @"
-
-
-
- $version
-
-
-"@
-[System.IO.File]::WriteAllText((Join-Path $target 'local-feed.props'), ($props -replace "`r`n", "`n"), $utf8NoBom)
+# The scaffolded copy is disposable, so repointing its project file at the local build is safe and
+# keeps the checked-in sample free of contributor-only scaffolding. Reruns are safe: the pattern
+# matches whatever version is currently there.
+$projectXml = [System.IO.File]::ReadAllText($projectFile.FullName)
+$projectXml = $projectXml -replace '(?)[^<]*(?)', "`${open}$version`${close}"
+[System.IO.File]::WriteAllText($projectFile.FullName, $projectXml, [System.Text.UTF8Encoding]::new($true))
Write-Host ''
Write-Host 'Done. Continue with the standard flow:' -ForegroundColor Green
From e374758ceffd9555e1b603a7f223837df5ebab15 Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Tue, 28 Jul 2026 14:54:57 +0100
Subject: [PATCH 08/23] Document the full deploy walkthrough and add a bash
contributor script
---
.../Hosted-ChatClientAgent/README.md | 191 ++++++++++++++++--
.../scripts/add-local-framework-feed.sh | 152 ++++++++++++++
2 files changed, 322 insertions(+), 21 deletions(-)
create mode 100755 dotnet/samples/04-hosting/FoundryHostedAgents/scripts/add-local-framework-feed.sh
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
index 8a82cf0470b..b649d0e20d7 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
@@ -21,11 +21,20 @@ This sample deploys to Foundry **directly from source (code / ZIP upload)**: the
| `HostedChatClientAgent.csproj` | Self-contained project: single target framework, central package management off, explicit package versions (there is no repo-level props file inside the ZIP). |
| `Directory.Packages.props` | Stops inheritance of the repository's central package management so the in-repo build matches the server-side build of the uploaded ZIP. |
| `.env.example` | Template for local configuration. |
+| `../../scripts/Add-LocalFrameworkFeed.ps1`, `../../scripts/add-local-framework-feed.sh` | Contributor-only helpers, see [Deploy your local framework changes](#deploy-your-local-framework-changes-contributors). |
## Configuration
Copy the template and fill in your project endpoint:
+PowerShell:
+
+```powershell
+copy .env.example .env
+```
+
+Bash:
+
```bash
cp .env.example .env
```
@@ -64,7 +73,7 @@ local server needs no extra wiring: the client just points an OpenAI responses c
**Terminal 1 — host the agent:**
-```bash
+```
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent
az login
dotnet run
@@ -74,35 +83,94 @@ The agent starts on `http://localhost:8088`.
**Terminal 2 — chat with it (code-first REPL):**
+PowerShell:
+
```powershell
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent
$env:AZURE_AI_AGENT_NAME = "hosted-chat-client-agent"
dotnet run
```
+Bash:
+
+```bash
+cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent
+export AZURE_AI_AGENT_NAME="hosted-chat-client-agent"
+dotnet run
+```
+
The REPL asks which agent to chat with; choose **2 (Local)**. It then points an OpenAI responses
client at the local server and streams the reply.
## Deploy to Foundry (source / ZIP)
-The Azure Developer CLI scaffolds the project into a working folder, so run `init` from an empty
-directory outside the repo and point `-m` at this sample's `azure.yaml`:
+`azd` scaffolds the project into a working folder, so every step below runs from an **empty
+directory outside the repository**, and `-m` points at this sample's `azure.yaml`.
+
+### Step 1: create the working directory and enter it
+
+PowerShell:
+
+```powershell
+$work = Join-Path $env:TEMP "hosted-chat-work"
+mkdir $work
+cd $work
+```
+
+Bash:
+
+```bash
+WORK="${TMPDIR:-/tmp}/hosted-chat-work"
+mkdir -p "$WORK"
+cd "$WORK"
+```
+
+### Step 2: scaffold the project
+
+`azd ai agent init` copies the sample into a subfolder named after the top-level `name:` in
+`azure.yaml`, which is `hosted-chat-client-agent`. It also writes the adopted `azure.yaml` and the
+`azd` environment there.
+
+PowerShell:
```powershell
-mkdir ; cd
+$sample = "/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/azure.yaml"
+$projectId = "/subscriptions//resourceGroups//providers/Microsoft.CognitiveServices/accounts//projects/"
+
+azd auth login
+azd ai agent init -m $sample -p $projectId -d
+```
+
+Bash:
+
+```bash
+SAMPLE="/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/azure.yaml"
+PROJECT_ID="/subscriptions//resourceGroups//providers/Microsoft.CognitiveServices/accounts//projects/"
+
azd auth login
-azd ai agent init -m /dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/azure.yaml `
- -p -d
+azd ai agent init -m "$SAMPLE" -p "$PROJECT_ID" -d
+```
+
+Reusing an agent name creates a **new version** of that agent rather than a separate one. To deploy
+a separate agent, change the agent service's `name` in the generated `azure.yaml` before deploying.
+
+### Step 3: provision and deploy
+
+```
cd hosted-chat-client-agent
azd provision
azd deploy
+azd ai agent invoke "Hello!"
```
`azd` packages the source into a ZIP (honoring `.agentignore`), uploads it, and Foundry runs
`dotnet restore` + `dotnet publish` on it during provisioning (`dependencyResolution: remote_build`
-in `azure.yaml`). No Dockerfile, no container registry.
+in `azure.yaml`). No Dockerfile, no container registry. A real deploy takes roughly a minute; see
+[Troubleshooting](#troubleshooting) if it returns in a few seconds.
-Test the deployed agent with the REPL, choosing **1 (Foundry)** at the prompt:
+You can also test the deployed agent with the REPL, choosing **1 (Foundry)** at the prompt:
+
+PowerShell:
```powershell
cd /dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent
@@ -111,25 +179,95 @@ $env:AZURE_AI_AGENT_NAME = "hosted-chat-client-agent"
dotnet run
```
-Or use the `azd` shortcut: `azd ai agent invoke "Hello!"`.
+Bash:
+
+```bash
+cd /dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent
+export FOUNDRY_PROJECT_ENDPOINT="https://.services.ai.azure.com/api/projects/"
+export AZURE_AI_AGENT_NAME="hosted-chat-client-agent"
+dotnet run
+```
+
+### Step 4: clean up
+
+```
+azd down
+```
+
+Then delete the working directory.
## Deploy your local framework changes (contributors)
-By default the project restores the published Agent Framework packages, so local changes to the
-framework are not exercised. To deploy them, run one extra step in the middle of the flow above,
-after `azd ai agent init` and before `azd provision`:
+By default the project restores the **published** Agent Framework packages, so local changes to the
+framework are never exercised: Foundry restores from nuget.org when it builds the upload.
+
+To deploy your local build instead, run one extra step in the flow above, **between step 2 and
+step 3**. Everything else is unchanged.
+
+PowerShell:
```powershell
-/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1 `
- -Path ./hosted-chat-client-agent
+/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1 -Path ./hosted-chat-client-agent
+```
+
+Bash:
+
+```bash
+/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/add-local-framework-feed.sh ./hosted-chat-client-agent
```
-The script packs the local Agent Framework source into a `local-feed` folder inside the scaffolded
-agent folder, writes the `nuget.config` that points the build at those packages, and repoints the
-project file's `AgentFrameworkVersion` at the version it just packed. Both generated files travel
-inside the ZIP, so the server-side restore resolves the framework from the upload. The scaffolded
-folder is a throwaway copy, so the repository is left untouched. Everything else, including
-`azd provision`, `azd deploy`, and `azd ai agent invoke`, is unchanged.
+The script changes three things in the scaffolded folder:
+
+| Change | Detail |
+|--------|--------|
+| Creates `local-feed/` | The Agent Framework packed from your local source, stamped with a version like `1.15.0-preview-local.` |
+| Creates `nuget.config` | Resolves `Microsoft.Agents.AI*` from that folder and everything else from nuget.org |
+| Edits the `.csproj` | Repoints its `AgentFrameworkVersion` property at the version just packed |
+
+Both generated files ship inside the ZIP, so the server-side restore resolves the framework from
+the upload. The scaffolded folder is a throwaway copy, so the repository is left untouched.
+
+Two details worth knowing:
+
+- The version carries a timestamp because NuGet caches by package id and version. Reusing a version
+ would silently restore the previously packed bits instead of the build you just made. It also
+ changes the ZIP on every run, which matters for the deploy deduplication described under
+ [Troubleshooting](#troubleshooting).
+- The whole package closure is packed, not just the two packages the sample references. Packing
+ only the leaf packages lets NuGet fill the rest from nuget.org, mixing a published core with a
+ locally built host, which fails to compile.
+
+Before spending a deploy, build the scaffolded folder locally. A restore problem surfaces in
+seconds instead of after the server-side build:
+
+```
+dotnet build -c Debug --tl:off
+```
+
+Reaching `active` is itself proof that the upload was used: the packed version does not exist on
+nuget.org, so a restore that ignored the bundled `nuget.config` would have failed with a missing
+package. To see exactly what was deployed, download the ZIP and list it:
+
+PowerShell:
+
+```powershell
+$token = az account get-access-token --resource "https://ai.azure.com" --query accessToken -o tsv
+$ep = "https://.services.ai.azure.com/api/projects/"
+curl.exe -sS -H "Authorization: Bearer $token" -H "Accept: application/zip" `
+ "$ep/agents/hosted-chat-client-agent/code:download?api-version=v1" -o deployed.zip
+Expand-Archive deployed.zip -DestinationPath deployed -Force
+dir deployed/local-feed
+```
+
+Bash:
+
+```bash
+TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv)
+EP="https://.services.ai.azure.com/api/projects/"
+curl -sS -H "Authorization: Bearer $TOKEN" -H "Accept: application/zip" \
+ "$EP/agents/hosted-chat-client-agent/code:download?api-version=v1" -o deployed.zip
+unzip -l deployed.zip
+```
## Troubleshooting
@@ -139,7 +277,9 @@ folder is a throwaway copy, so the repository is left untouched. Everything else
| `azd ai agent invoke` returns HTTP 424 `session_not_ready` | The container is listening on a port Foundry does not probe | Stream the container logs and check the `Now listening on` line; it must be port 8088 |
Stream the logs of a failing session, using the session id from the error message or the
-`x-agent-session-id` response header:
+`x-agent-session-id` response header. The line that matters is `Now listening on`.
+
+PowerShell:
```powershell
$token = az account get-access-token --resource "https://ai.azure.com" --query accessToken -o tsv
@@ -148,4 +288,13 @@ curl.exe -sS -N --max-time 60 -H "Authorization: Bearer $token" -H "Accept: text
"$ep/agents/hosted-chat-client-agent/sessions/:logstream?api-version=v1"
```
+Bash:
+
+```bash
+TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv)
+EP="https://.services.ai.azure.com/api/projects/"
+curl -sS -N --max-time 60 -H "Authorization: Bearer $TOKEN" -H "Accept: text/event-stream" \
+ "$EP/agents/hosted-chat-client-agent/sessions/:logstream?api-version=v1"
+```
+
For the full hosted-agent deployment guide, see the [official source-code deployment doc](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent-code).
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
new file mode 100755
index 00000000000..e3556c8a695
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/add-local-framework-feed.sh
@@ -0,0 +1,152 @@
+#!/usr/bin/env bash
+#
+# Rewires an already-scaffolded hosted-agent folder to build against the local Agent Framework
+# source, so `azd deploy` ships your framework changes instead of the published packages.
+#
+# Source (ZIP) deploy uploads the agent folder and Foundry runs `dotnet restore` + `dotnet publish`
+# on it in the cloud. That restore pulls the Agent Framework from nuget.org, so a contributor's
+# local framework changes are never exercised.
+#
+# Run this after `azd ai agent init` and before `azd provision`. It changes two things in the
+# folder that `init` scaffolded:
+#
+# local-feed/ New. The Agent Framework packed from the local source tree, stamped with a
+# version derived from the repo's current VersionPrefix plus a `-preview-local`
+# suffix. The whole closure is packed: packing only the leaf packages lets NuGet
+# fill the rest from nuget.org, mixing a published core with a locally built host.
+# nuget.config New. Maps Microsoft.Agents.AI* to that folder feed and everything else to
+# nuget.org.
+# the .csproj Edited. Its AgentFrameworkVersion property is repointed at the version just
+# packed.
+#
+# Neither generated file is excluded by `.agentignore`, so they travel inside the ZIP and the
+# server-side restore uses them.
+#
+# Everything else stays identical to the end-user flow: you create the working directory, run
+# `azd ai agent init`, and finish with `azd provision`, `azd deploy`, and `azd ai agent invoke`.
+# The scaffolded folder is a throwaway copy, so editing its project file leaves the repository
+# untouched.
+#
+# Usage:
+# add-local-framework-feed.sh [path-to-scaffolded-folder]
+#
+# The path defaults to the current directory.
+#
+# This is the bash counterpart of Add-LocalFrameworkFeed.ps1. For contributors validating framework
+# changes end to end. End users skip this script entirely and get the published packages.
+
+set -euo pipefail
+
+# The Agent Framework closure the hosted samples resolve. Packing only the leaf packages makes
+# NuGet satisfy the rest from nuget.org, producing assembly-reference errors at build time.
+framework_projects=(
+ Microsoft.Agents.AI.Abstractions
+ Microsoft.Agents.AI
+ Microsoft.Agents.AI.Workflows
+ Microsoft.Agents.AI.Foundry
+ Microsoft.Agents.AI.Foundry.Hosting
+)
+
+target_input="${1:-.}"
+
+if [[ ! -d "$target_input" ]]; then
+ echo "Error: '$target_input' is not a directory." >&2
+ exit 1
+fi
+
+target="$(cd "$target_input" && pwd)"
+
+if [[ ! -f "$target/azure.yaml" ]]; then
+ echo "Error: no azure.yaml in '$target'. Point the path at the folder 'azd ai agent init' scaffolded." >&2
+ exit 1
+fi
+
+project_file="$(find "$target" -maxdepth 1 -name '*.csproj' | head -n 1)"
+
+if [[ -z "$project_file" ]]; then
+ echo "Error: no .csproj in '$target'. This script targets .NET hosted agents." >&2
+ exit 1
+fi
+
+if ! grep -q '' "$project_file"; then
+ echo "Error: $(basename "$project_file") has no property to repoint at a local build." >&2
+ exit 1
+fi
+
+script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+hosted_root="$(dirname "$script_dir")"
+dotnet_root="$(cd "$hosted_root/../../.." && pwd)"
+src_root="$dotnet_root/src"
+
+# Derive the package version from the repo so the packages track the current release line.
+# The timestamp keeps every run unique: NuGet caches by id and version, so reusing a version would
+# silently restore the previously packed bits instead of the build you just made. It also changes
+# the ZIP contents on every run, which matters because Foundry mints a new agent version only when
+# the uploaded ZIP changes.
+package_props="$dotnet_root/nuget/nuget-package.props"
+version_prefix="$(sed -n 's/.*\([^<]*\)<\/VersionPrefix>.*/\1/p' "$package_props" | head -n 1)"
+
+if [[ -z "$version_prefix" ]]; then
+ echo "Error: could not read VersionPrefix from $package_props." >&2
+ exit 1
+fi
+
+version="$version_prefix-preview-local.$(date +%Y%m%d%H%M%S)"
+
+feed_path="$target/local-feed"
+rm -rf "$feed_path"
+mkdir -p "$feed_path"
+
+echo "Wiring $(basename "$target") to the local Agent Framework"
+echo " version: $version"
+echo
+
+for project in "${framework_projects[@]}"; do
+ project_path="$src_root/$project/$project.csproj"
+ echo "Packing $project..."
+
+ # Debug, not Release: the Release configuration runs the repo's formatting and analyzer passes,
+ # which rewrite source files and fail the build on style violations. Packing only needs runnable
+ # binaries, so Debug keeps the working tree untouched.
+ #
+ # PackageVersion (not Version) is the property the repo's packaging props use to stamp both the
+ # package version and its dependency ranges, so the packed packages reference each other at this
+ # version instead of the bare VersionPrefix.
+ dotnet build "$project_path" -c Debug "-p:PackageVersion=$version" --tl:off >/dev/null
+ dotnet pack "$project_path" -c Debug --no-build -o "$feed_path" "-p:PackageVersion=$version" --tl:off >/dev/null
+done
+
+cat > "$target/nuget.config" <<'EOF'
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+EOF
+
+# The scaffolded copy is disposable, so repointing its project file at the local build is safe and
+# keeps the checked-in sample free of contributor-only scaffolding. Reruns are safe: the pattern
+# matches whatever version is currently there. sed rewrites the line in place, leaving the file's
+# leading byte order mark untouched.
+sed -i.bak "s|[^<]*|$version|" "$project_file"
+rm -f "$project_file.bak"
+
+echo
+echo "Done. Continue with the standard flow:"
+echo
+echo " cd \"$target\""
+echo " azd provision"
+echo " azd deploy"
+echo " azd ai agent invoke \"Hello!\""
From 335e8221d268350f7ff0d20cf3e0a24ca9322012 Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Tue, 28 Jul 2026 14:58:02 +0100
Subject: [PATCH 09/23] Trim troubleshooting detail from the sample README
---
.../Hosted-ChatClientAgent/README.md | 61 +------------------
1 file changed, 3 insertions(+), 58 deletions(-)
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
index b649d0e20d7..73d07a317af 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
@@ -151,9 +151,6 @@ azd auth login
azd ai agent init -m "$SAMPLE" -p "$PROJECT_ID" -d
```
-Reusing an agent name creates a **new version** of that agent rather than a separate one. To deploy
-a separate agent, change the agent service's `name` in the generated `azure.yaml` before deploying.
-
### Step 3: provision and deploy
```
@@ -165,8 +162,7 @@ azd ai agent invoke "Hello!"
`azd` packages the source into a ZIP (honoring `.agentignore`), uploads it, and Foundry runs
`dotnet restore` + `dotnet publish` on it during provisioning (`dependencyResolution: remote_build`
-in `azure.yaml`). No Dockerfile, no container registry. A real deploy takes roughly a minute; see
-[Troubleshooting](#troubleshooting) if it returns in a few seconds.
+in `azure.yaml`). No Dockerfile, no container registry.
You can also test the deployed agent with the REPL, choosing **1 (Foundry)** at the prompt:
@@ -230,9 +226,7 @@ the upload. The scaffolded folder is a throwaway copy, so the repository is left
Two details worth knowing:
- The version carries a timestamp because NuGet caches by package id and version. Reusing a version
- would silently restore the previously packed bits instead of the build you just made. It also
- changes the ZIP on every run, which matters for the deploy deduplication described under
- [Troubleshooting](#troubleshooting).
+ would silently restore the previously packed bits instead of the build you just made.
- The whole package closure is packed, not just the two packages the sample references. Packing
only the leaf packages lets NuGet fill the rest from nuget.org, mixing a published core with a
locally built host, which fails to compile.
@@ -246,55 +240,6 @@ dotnet build -c Debug --tl:off
Reaching `active` is itself proof that the upload was used: the packed version does not exist on
nuget.org, so a restore that ignored the bundled `nuget.config` would have failed with a missing
-package. To see exactly what was deployed, download the ZIP and list it:
-
-PowerShell:
-
-```powershell
-$token = az account get-access-token --resource "https://ai.azure.com" --query accessToken -o tsv
-$ep = "https://.services.ai.azure.com/api/projects/"
-curl.exe -sS -H "Authorization: Bearer $token" -H "Accept: application/zip" `
- "$ep/agents/hosted-chat-client-agent/code:download?api-version=v1" -o deployed.zip
-Expand-Archive deployed.zip -DestinationPath deployed -Force
-dir deployed/local-feed
-```
-
-Bash:
-
-```bash
-TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv)
-EP="https://.services.ai.azure.com/api/projects/"
-curl -sS -H "Authorization: Bearer $TOKEN" -H "Accept: application/zip" \
- "$EP/agents/hosted-chat-client-agent/code:download?api-version=v1" -o deployed.zip
-unzip -l deployed.zip
-```
-
-## Troubleshooting
-
-| Symptom | Cause | Fix |
-|---------|-------|-----|
-| `azd deploy` finishes in a few seconds and reports the previous version number | Foundry mints a new version only when the uploaded ZIP changes, so edits confined to `azure.yaml` (which `.agentignore` keeps out of the ZIP) are discarded | Change a file that ships in the ZIP, or delete the agent and deploy again |
-| `azd ai agent invoke` returns HTTP 424 `session_not_ready` | The container is listening on a port Foundry does not probe | Stream the container logs and check the `Now listening on` line; it must be port 8088 |
-
-Stream the logs of a failing session, using the session id from the error message or the
-`x-agent-session-id` response header. The line that matters is `Now listening on`.
-
-PowerShell:
-
-```powershell
-$token = az account get-access-token --resource "https://ai.azure.com" --query accessToken -o tsv
-$ep = "https://.services.ai.azure.com/api/projects/"
-curl.exe -sS -N --max-time 60 -H "Authorization: Bearer $token" -H "Accept: text/event-stream" `
- "$ep/agents/hosted-chat-client-agent/sessions/:logstream?api-version=v1"
-```
-
-Bash:
-
-```bash
-TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv)
-EP="https://.services.ai.azure.com/api/projects/"
-curl -sS -N --max-time 60 -H "Authorization: Bearer $TOKEN" -H "Accept: text/event-stream" \
- "$EP/agents/hosted-chat-client-agent/sessions/:logstream?api-version=v1"
-```
+package.
For the full hosted-agent deployment guide, see the [official source-code deployment doc](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent-code).
From 01d050cfe3690079f27fb876f4c8e254f36c26e1 Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Tue, 28 Jul 2026 15:24:28 +0100
Subject: [PATCH 10/23] Pass the model deployment name to the hosted container
---
.../responses/Hosted-ChatClientAgent/README.md | 10 ++++++++--
.../responses/Hosted-ChatClientAgent/azure.yaml | 16 ++++++++++------
2 files changed, 18 insertions(+), 8 deletions(-)
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
index 73d07a317af..bf409cdb31e 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
@@ -7,7 +7,10 @@ This sample deploys to Foundry **directly from source (code / ZIP upload)**: the
## Prerequisites
- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
-- A Foundry project with a deployed model (for example `gpt-4o`)
+- An **existing** Foundry project with an **existing** model deployment (for example `gpt-4o`).
+ This sample's `azure.yaml` declares no `deployments:` block, so `azd` connects to a project and
+ a deployment you already have rather than creating them. `azd ai agent init` takes both as
+ arguments (`-p` and `-d`) and prompts for them when they are omitted.
- Azure CLI logged in (`az login`)
- Azure Developer CLI (`azd`) with the AI agents extension: `azd extension install azure.ai.agents`
@@ -16,7 +19,7 @@ This sample deploys to Foundry **directly from source (code / ZIP upload)**: the
| File | Purpose |
|------|---------|
| `Program.cs` | The agent: builds the agent, hosts it with the Responses protocol. |
-| `azure.yaml` | The unified `azd` project file. Declares the Foundry project and the hosted agent with `codeConfiguration` (source/ZIP deploy), and pins the listen port through `environment_variables`. |
+| `azure.yaml` | The unified `azd` project file. Declares the Foundry project and the hosted agent with `codeConfiguration` (source/ZIP deploy), and passes the listen port and the model deployment name to the container through `env`. |
| `.agentignore` | Controls which files are excluded from the code-deploy ZIP upload (`.gitignore` syntax). |
| `HostedChatClientAgent.csproj` | Self-contained project: single target framework, central package management off, explicit package versions (there is no repo-level props file inside the ZIP). |
| `Directory.Packages.props` | Stops inheritance of the repository's central package management so the in-repo build matches the server-side build of the uploaded ZIP. |
@@ -131,6 +134,9 @@ cd "$WORK"
`azure.yaml`, which is `hosted-chat-client-agent`. It also writes the adopted `azure.yaml` and the
`azd` environment there.
+`-p` is the resource ID of an existing Foundry project and `-d` the name of an existing model
+deployment in it. Omit either one and `azd` prompts for it.
+
PowerShell:
```powershell
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/azure.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/azure.yaml
index c862d3d9f35..c12b5aaf94d 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/azure.yaml
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/azure.yaml
@@ -14,14 +14,18 @@ services:
dependencyResolution: remote_build
entryPoint: HostedChatClientAgent.dll
runtime: dotnet_10
- # Passes the listen port to the container. Source deploy runs this project as a plain
- # ASP.NET app, and the .NET base image defaults ASPNETCORE_URLS to port 80, while Foundry
- # probes port 8088 for readiness, so without this every invoke fails with HTTP 424
- # session_not_ready. Recent Microsoft.Agents.AI.Foundry.Hosting versions bind the port
- # themselves and take precedence over this value, so it only matters when the project is
- # pinned to an older published package.
+ # ASPNETCORE_URLS pins the listen port. Source deploy runs this project as a plain ASP.NET
+ # app, and the .NET base image defaults it to port 80, while Foundry probes port 8088 for
+ # readiness, so without it every invoke fails with HTTP 424 session_not_ready. Recent
+ # Microsoft.Agents.AI.Foundry.Hosting versions bind the port themselves and take precedence
+ # over this value, so it only matters when the project is pinned to an older package.
+ #
+ # ${AZURE_AI_MODEL_DEPLOYMENT_NAME} reads the model deployment `azd ai agent init` recorded
+ # in the active azd environment. Without it the container falls back to the default model
+ # name hardcoded in Program.cs, which may not exist in the target project.
env:
ASPNETCORE_URLS: http://+:8088
+ AZURE_AI_MODEL_DEPLOYMENT_NAME: ${AZURE_AI_MODEL_DEPLOYMENT_NAME}
container:
resources:
cpu: "0.5"
From ab98c2f25948a53bc584975d6401f3d7056b1c99 Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Tue, 28 Jul 2026 15:40:26 +0100
Subject: [PATCH 11/23] Add --local and --remote flags to the SimpleAgent REPL
---
.../Hosted-ChatClientAgent/.env.example | 5 +++++
.../Hosted-ChatClientAgent/README.md | 19 ++++++++++++-------
.../responses/Using-Samples/README.md | 12 ++++++++----
.../Using-Samples/SimpleAgent/Program.cs | 16 +++++++++++++---
.../Using-Samples/SimpleAgent/README.md | 13 ++++++++++---
5 files changed, 48 insertions(+), 17 deletions(-)
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/.env.example b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/.env.example
index ecf3a6c31ad..1677d79d830 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/.env.example
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/.env.example
@@ -4,6 +4,11 @@ FOUNDRY_PROJECT_ENDPOINT=
# Model deployment name in your Foundry project.
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
+# Local development only. Bind the app to the port Foundry probes for readiness, which is the
+# port the Using-Samples REPLs expect. Recent Microsoft.Agents.AI.Foundry.Hosting versions bind
+# it themselves, so this only matters while the project is pinned to an older published package.
+ASPNETCORE_URLS=http://+:8088
+
# Local development only. Restrict DefaultAzureCredential to developer credentials
# (Azure CLI, Visual Studio, azd) and skip the Managed Identity probe. Without this,
# on a machine with no managed identity DefaultAzureCredential hangs for a long time
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
index bf409cdb31e..35f142393d0 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
@@ -45,11 +45,16 @@ cp .env.example .env
```env
FOUNDRY_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/
AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
+ASPNETCORE_URLS=http://+:8088
AZURE_TOKEN_CREDENTIALS=dev
```
> `.env` is gitignored. The `.env.example` template is checked in as a reference.
+> `ASPNETCORE_URLS` pins the local run to the port the `Using-Samples` REPLs expect. Recent
+> `Microsoft.Agents.AI.Foundry.Hosting` versions bind that port themselves, so it only matters
+> while this project is pinned to an older published package.
+
> **Windows note:** write `.env` as UTF-8 **without** a byte order mark. `azd` reads the file
> during `azd ai agent init` and fails with `unexpected character "»" in variable name` when a mark
> is present. PowerShell's `Set-Content -Encoding UTF8BOM` adds one; use `-Encoding utf8NoBOM`.
@@ -91,7 +96,7 @@ PowerShell:
```powershell
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent
$env:AZURE_AI_AGENT_NAME = "hosted-chat-client-agent"
-dotnet run
+dotnet run -- --local
```
Bash:
@@ -99,11 +104,11 @@ Bash:
```bash
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent
export AZURE_AI_AGENT_NAME="hosted-chat-client-agent"
-dotnet run
+dotnet run -- --local
```
-The REPL asks which agent to chat with; choose **2 (Local)**. It then points an OpenAI responses
-client at the local server and streams the reply.
+Without `--local` the REPL asks which agent to chat with; choose **2 (Local)**. Either way it
+points an OpenAI responses client at the local server and streams the reply.
## Deploy to Foundry (source / ZIP)
@@ -170,7 +175,7 @@ azd ai agent invoke "Hello!"
`dotnet restore` + `dotnet publish` on it during provisioning (`dependencyResolution: remote_build`
in `azure.yaml`). No Dockerfile, no container registry.
-You can also test the deployed agent with the REPL, choosing **1 (Foundry)** at the prompt:
+You can also test the deployed agent with the REPL:
PowerShell:
@@ -178,7 +183,7 @@ PowerShell:
cd /dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent
$env:FOUNDRY_PROJECT_ENDPOINT = "https://.services.ai.azure.com/api/projects/"
$env:AZURE_AI_AGENT_NAME = "hosted-chat-client-agent"
-dotnet run
+dotnet run -- --remote
```
Bash:
@@ -187,7 +192,7 @@ Bash:
cd /dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent
export FOUNDRY_PROJECT_ENDPOINT="https://.services.ai.azure.com/api/projects/"
export AZURE_AI_AGENT_NAME="hosted-chat-client-agent"
-dotnet run
+dotnet run -- --remote
```
### Step 4: clean up
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/README.md
index 656c2f247e0..a9c2e24f68f 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/README.md
@@ -28,10 +28,14 @@ just a matter of changing `AZURE_AI_AGENT_NAME`.
## Local HTTP dev
-When the target is a local `http://localhost:8088` dev server, the REPLs install a small
-`HttpSchemeRewritePolicy`: `AIProjectClient`/`BearerTokenPolicy` require HTTPS, so the client
-presents the endpoint as `https://` to satisfy the TLS check, then rewrites the scheme back to
-`http://` right before the request hits the wire. This is local-development only.
+`SessionFilesClient` and `Hosted-Toolbox-AuthPaths-Client` reach their agent through
+`AIProjectClient`, whose bearer-token policy requires HTTPS. To target a local `http://` dev server
+they install a small `HttpSchemeRewritePolicy`: the endpoint is presented as `https://` to satisfy
+the TLS check, then rewritten back to `http://` right before the request hits the wire. This is
+local-development only.
+
+`SimpleAgent` needs none of that. For a local target it points an `OpenAIClient` at the server's
+standard `POST /responses` route, which carries no bearer token and therefore no TLS check.
## The clients
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/Program.cs
index 6686bf81c9d..567a78fc40d 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/Program.cs
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/Program.cs
@@ -19,9 +19,9 @@
string agentName = Environment.GetEnvironmentVariable("AZURE_AI_AGENT_NAME")
?? throw new InvalidOperationException("AZURE_AI_AGENT_NAME is not set.");
-// Ask which server to talk to. This is the same choice `azd ai agent invoke` exposes through its
-// --local flag, asked at startup instead of passed as an argument.
-bool useLocalAgent = PromptForLocalTarget();
+// Pick the server to talk to. `--local` and `--remote` mirror the flag `azd ai agent invoke`
+// exposes; with neither, ask at startup.
+bool useLocalAgent = ResolveTarget(args);
AIAgent agent = useLocalAgent ? CreateLocalAgent() : CreateHostedAgent(agentName);
string target = useLocalAgent ? $"http://localhost:{LocalAgentPort}" : agentName;
@@ -77,6 +77,16 @@ Type a message or 'quit' to exit
Console.WriteLine("Goodbye!");
+// Returns true when the client should target a locally running agent. `--local` and `--remote`
+// answer the question up front, which is what non-interactive runs need; with neither, ask.
+static bool ResolveTarget(string[] args)
+{
+ if (args.Contains("--local", StringComparer.OrdinalIgnoreCase)) { return true; }
+ if (args.Contains("--remote", StringComparer.OrdinalIgnoreCase)) { return false; }
+
+ return PromptForLocalTarget();
+}
+
// Asks whether to target a locally running agent or the one deployed to Foundry, and returns
// true for local. Defaults to remote on an empty answer, matching `azd ai agent invoke`, which
// targets Foundry unless --local is passed.
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/README.md
index d3b9abeaf39..ba63c0a8689 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/README.md
@@ -41,9 +41,16 @@ Which agent do you want to chat with?
Choice:
```
-This is the same choice `azd ai agent invoke` exposes through its `--local` flag, asked at
-startup instead of passed as an argument. Pick **2** while a `Hosted-*` sample is running locally
-with `dotnet run`; pick **1** (or press Enter) to reach the agent deployed to Foundry.
+Pass `--local` or `--remote` to answer up front and skip the prompt, which is what scripted runs
+need:
+
+```
+dotnet run -- --local
+dotnet run -- --remote
+```
+
+This mirrors the `--local` flag on `azd ai agent invoke`. Use local while a `Hosted-*` sample is
+running with `dotnet run`; use remote to reach the agent deployed to Foundry.
The two choices differ only in how the agent is built:
From c42887e89203b93d548a553fd81b0b9a20df60a7 Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Tue, 28 Jul 2026 15:52:01 +0100
Subject: [PATCH 12/23] Use central package management in the hosted sample
---
.../Directory.Packages.props | 25 +++++++++++++-----
.../HostedChatClientAgent.csproj | 20 ++++++--------
.../Hosted-ChatClientAgent/README.md | 6 ++---
.../scripts/Add-LocalFrameworkFeed.ps1 | 26 +++++++++----------
.../scripts/add-local-framework-feed.sh | 18 ++++++-------
5 files changed, 52 insertions(+), 43 deletions(-)
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Directory.Packages.props b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Directory.Packages.props
index 44240eeeb12..57132b3344f 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Directory.Packages.props
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Directory.Packages.props
@@ -1,12 +1,25 @@
+
+
- false
+ true
+ false
+ 1.15.0-preview.260722.1
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/HostedChatClientAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/HostedChatClientAgent.csproj
index 04f99e71ce3..bc0f76b9ffa 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/HostedChatClientAgent.csproj
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/HostedChatClientAgent.csproj
@@ -2,11 +2,9 @@
@@ -19,17 +17,15 @@
enable
HostedChatClientAgent
HostedChatClientAgent
- false
222d2622-da26-4da0-99b4-0507fb8d41b0
- 1.15.0-preview.260722.1
-
-
-
-
-
+
+
+
+
+
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
index 35f142393d0..641f6cbabb4 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
@@ -21,8 +21,8 @@ This sample deploys to Foundry **directly from source (code / ZIP upload)**: the
| `Program.cs` | The agent: builds the agent, hosts it with the Responses protocol. |
| `azure.yaml` | The unified `azd` project file. Declares the Foundry project and the hosted agent with `codeConfiguration` (source/ZIP deploy), and passes the listen port and the model deployment name to the container through `env`. |
| `.agentignore` | Controls which files are excluded from the code-deploy ZIP upload (`.gitignore` syntax). |
-| `HostedChatClientAgent.csproj` | Self-contained project: single target framework, central package management off, explicit package versions (there is no repo-level props file inside the ZIP). |
-| `Directory.Packages.props` | Stops inheritance of the repository's central package management so the in-repo build matches the server-side build of the uploaded ZIP. |
+| `HostedChatClientAgent.csproj` | Project file. Package references carry no versions: they come from the sample's own `Directory.Packages.props`. |
+| `Directory.Packages.props` | Central package management for the sample. The ZIP has no repo-level props, so the sample carries its own copy of the versions it needs. |
| `.env.example` | Template for local configuration. |
| `../../scripts/Add-LocalFrameworkFeed.ps1`, `../../scripts/add-local-framework-feed.sh` | Contributor-only helpers, see [Deploy your local framework changes](#deploy-your-local-framework-changes-contributors). |
@@ -229,7 +229,7 @@ The script changes three things in the scaffolded folder:
|--------|--------|
| Creates `local-feed/` | The Agent Framework packed from your local source, stamped with a version like `1.15.0-preview-local.` |
| Creates `nuget.config` | Resolves `Microsoft.Agents.AI*` from that folder and everything else from nuget.org |
-| Edits the `.csproj` | Repoints its `AgentFrameworkVersion` property at the version just packed |
+| Edits `Directory.Packages.props` | Repoints its `AgentFrameworkVersion` property at the version just packed |
Both generated files ship inside the ZIP, so the server-side restore resolves the framework from
the upload. The scaffolded folder is a throwaway copy, so the repository is left untouched.
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1 b/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1
index 90885a2a050..6cd5beda26d 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1
@@ -17,15 +17,15 @@
fill the rest from nuget.org, mixing a published core with a locally built host.
nuget.config New. Maps Microsoft.Agents.AI* to that folder feed and everything else to
nuget.org.
- the .csproj Edited. Its AgentFrameworkVersion property is repointed at the version just
- packed.
+ the versions Edited. Directory.Packages.props has its AgentFrameworkVersion property
+ repointed at the version just packed.
Neither generated file is excluded by `.agentignore`, so they travel inside the ZIP and the
server-side restore uses them.
Everything else stays identical to the end-user flow: you create the working directory, run
`azd ai agent init`, and finish with `azd provision`, `azd deploy`, and `azd ai agent invoke`.
- The scaffolded folder is a throwaway copy, so editing its project file leaves the repository
+ The scaffolded folder is a throwaway copy, so editing its package versions leaves the repository
untouched.
.PARAMETER Path
The folder `azd ai agent init` scaffolded, for example `./hosted-chat-client-agent`.
@@ -65,13 +65,13 @@ if (-not (Test-Path (Join-Path $target 'azure.yaml'))) {
throw "No azure.yaml in '$target'. Point -Path at the folder 'azd ai agent init' scaffolded."
}
-$projectFile = Get-ChildItem $target -Filter *.csproj -File | Select-Object -First 1
-if (-not $projectFile) {
- throw "No .csproj in '$target'. This script targets .NET hosted agents."
+$versionsFile = Join-Path $target 'Directory.Packages.props'
+if (-not (Test-Path $versionsFile)) {
+ throw "No Directory.Packages.props in '$target'. This script targets .NET hosted agents."
}
-if (-not (Select-String -Path $projectFile.FullName -Pattern '' -Quiet)) {
- throw "$($projectFile.Name) has no property to repoint at a local build."
+if (-not (Select-String -Path $versionsFile -Pattern '' -Quiet)) {
+ throw "Directory.Packages.props has no property to repoint at a local build."
}
$hostedRoot = Split-Path -Parent $PSScriptRoot
@@ -136,12 +136,12 @@ $nugetConfig = @'
'@
[System.IO.File]::WriteAllText((Join-Path $target 'nuget.config'), ($nugetConfig -replace "`r`n", "`n"), $utf8NoBom)
-# The scaffolded copy is disposable, so repointing its project file at the local build is safe and
-# keeps the checked-in sample free of contributor-only scaffolding. Reruns are safe: the pattern
+# The scaffolded copy is disposable, so repointing its package versions at the local build is safe
+# and keeps the checked-in sample free of contributor-only scaffolding. Reruns are safe: the pattern
# matches whatever version is currently there.
-$projectXml = [System.IO.File]::ReadAllText($projectFile.FullName)
-$projectXml = $projectXml -replace '(?)[^<]*(?)', "`${open}$version`${close}"
-[System.IO.File]::WriteAllText($projectFile.FullName, $projectXml, [System.Text.UTF8Encoding]::new($true))
+$versionsXml = [System.IO.File]::ReadAllText($versionsFile)
+$versionsXml = $versionsXml -replace '(?)[^<]*(?)', "`${open}$version`${close}"
+[System.IO.File]::WriteAllText($versionsFile, $versionsXml, [System.Text.UTF8Encoding]::new($true))
Write-Host ''
Write-Host 'Done. Continue with the standard flow:' -ForegroundColor Green
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 e3556c8a695..591f177bc84 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
@@ -16,15 +16,15 @@
# fill the rest from nuget.org, mixing a published core with a locally built host.
# nuget.config New. Maps Microsoft.Agents.AI* to that folder feed and everything else to
# nuget.org.
-# the .csproj Edited. Its AgentFrameworkVersion property is repointed at the version just
-# packed.
+# the versions Edited. Directory.Packages.props has its AgentFrameworkVersion property
+# repointed at the version just packed.
#
# Neither generated file is excluded by `.agentignore`, so they travel inside the ZIP and the
# server-side restore uses them.
#
# Everything else stays identical to the end-user flow: you create the working directory, run
# `azd ai agent init`, and finish with `azd provision`, `azd deploy`, and `azd ai agent invoke`.
-# The scaffolded folder is a throwaway copy, so editing its project file leaves the repository
+# The scaffolded folder is a throwaway copy, so editing its package versions leaves the repository
# untouched.
#
# Usage:
@@ -61,15 +61,15 @@ if [[ ! -f "$target/azure.yaml" ]]; then
exit 1
fi
-project_file="$(find "$target" -maxdepth 1 -name '*.csproj' | head -n 1)"
+project_file="$target/Directory.Packages.props"
-if [[ -z "$project_file" ]]; then
- echo "Error: no .csproj in '$target'. This script targets .NET hosted agents." >&2
+if [[ ! -f "$project_file" ]]; then
+ echo "Error: no Directory.Packages.props in '$target'. This script targets .NET hosted agents." >&2
exit 1
fi
if ! grep -q '' "$project_file"; then
- echo "Error: $(basename "$project_file") has no property to repoint at a local build." >&2
+ echo "Error: Directory.Packages.props has no property to repoint at a local build." >&2
exit 1
fi
@@ -136,8 +136,8 @@ cat > "$target/nuget.config" <<'EOF'
EOF
-# The scaffolded copy is disposable, so repointing its project file at the local build is safe and
-# keeps the checked-in sample free of contributor-only scaffolding. Reruns are safe: the pattern
+# The scaffolded copy is disposable, so repointing its package versions at the local build is safe
+# and keeps the checked-in sample free of contributor-only scaffolding. Reruns are safe: the pattern
# matches whatever version is currently there. sed rewrites the line in place, leaving the file's
# leading byte order mark untouched.
sed -i.bak "s|[^<]*|$version|" "$project_file"
From ce1d993b6b054b229bc429c0ae2ebe0d66013823 Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Tue, 28 Jul 2026 16:01:09 +0100
Subject: [PATCH 13/23] Clarify where the contributor step fits in the deploy
walkthrough
---
.../responses/Hosted-ChatClientAgent/README.md | 18 ++++++++++++++----
1 file changed, 14 insertions(+), 4 deletions(-)
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
index 641f6cbabb4..3ff6dbcc95c 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
@@ -164,6 +164,11 @@ azd ai agent init -m "$SAMPLE" -p "$PROJECT_ID" -d
### Step 3: provision and deploy
+Contributors: to run the agent against your **local** Agent Framework build instead of the
+published packages, those packages have to be built and packed into the upload first. Do that now,
+before the commands below, following
+[Deploy your local framework changes](#deploy-your-local-framework-changes-contributors).
+
```
cd hosted-chat-client-agent
azd provision
@@ -211,18 +216,26 @@ framework are never exercised: Foundry restores from nuget.org when it builds th
To deploy your local build instead, run one extra step in the flow above, **between step 2 and
step 3**. Everything else is unchanged.
+Run it from `$work`, the working directory created in step 1, which now holds the
+`hosted-chat-client-agent` folder that `azd ai agent init` scaffolded:
+
PowerShell:
```powershell
+cd $work
/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1 -Path ./hosted-chat-client-agent
```
Bash:
```bash
+cd "$WORK"
/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/add-local-framework-feed.sh ./hosted-chat-client-agent
```
+Then continue with step 3. The path argument is optional: called without it, the script uses the
+current directory, so you can also run it from inside `hosted-chat-client-agent`.
+
The script changes three things in the scaffolded folder:
| Change | Detail |
@@ -246,11 +259,8 @@ Before spending a deploy, build the scaffolded folder locally. A restore problem
seconds instead of after the server-side build:
```
+cd hosted-chat-client-agent
dotnet build -c Debug --tl:off
```
-Reaching `active` is itself proof that the upload was used: the packed version does not exist on
-nuget.org, so a restore that ignored the bundled `nuget.config` would have failed with a missing
-package.
-
For the full hosted-agent deployment guide, see the [official source-code deployment doc](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent-code).
From fce7016b63290c7bc0bd578538fc55596e95c470 Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Tue, 28 Jul 2026 16:10:28 +0100
Subject: [PATCH 14/23] Restore the HTTP scheme rewrite for local
AIProjectClient runs
---
.../responses/Using-Samples/README.md | 19 ++++----
.../Using-Samples/SimpleAgent/Program.cs | 45 ++++++++++++++++++-
.../Using-Samples/SimpleAgent/README.md | 4 ++
3 files changed, 59 insertions(+), 9 deletions(-)
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/README.md
index a9c2e24f68f..29536a9d552 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/README.md
@@ -28,14 +28,17 @@ just a matter of changing `AZURE_AI_AGENT_NAME`.
## Local HTTP dev
-`SessionFilesClient` and `Hosted-Toolbox-AuthPaths-Client` reach their agent through
-`AIProjectClient`, whose bearer-token policy requires HTTPS. To target a local `http://` dev server
-they install a small `HttpSchemeRewritePolicy`: the endpoint is presented as `https://` to satisfy
-the TLS check, then rewritten back to `http://` right before the request hits the wire. This is
-local-development only.
-
-`SimpleAgent` needs none of that. For a local target it points an `OpenAIClient` at the server's
-standard `POST /responses` route, which carries no bearer token and therefore no TLS check.
+`AIProjectClient` authenticates with a bearer token, and the client pipeline refuses to attach one
+to a plain `http://` endpoint, failing with `InvalidOperationException: Bearer token authentication
+is not permitted for non TLS protected (https) endpoints.` before the request is even sent. To
+target a local dev server over HTTP, the REPLs install a small `HttpSchemeRewritePolicy`: the
+client is pointed at an `https://` URI to satisfy that check, and the policy puts the scheme back
+to `http://` right before the request hits the wire. This is local-development only.
+
+`SimpleAgent` applies it only on the Foundry path, and only when `FOUNDRY_PROJECT_ENDPOINT` is an
+`http://` URL. Its `--local` path needs nothing of the sort: it points an `OpenAIClient` at the
+server's standard `POST /responses` route with an api key, which carries no bearer token and so
+never hits the TLS check.
## The clients
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/Program.cs
index 567a78fc40d..00194c5406c 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/Program.cs
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/Program.cs
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ClientModel;
+using System.ClientModel.Primitives;
using Azure.AI.Projects;
using Azure.Identity;
using DotNetEnv;
@@ -127,5 +128,47 @@ static AIAgent CreateHostedAgent(string agentName)
Uri agentEndpoint = new($"{projectEndpoint}/agents/{agentName}/endpoint/protocols/openai");
- return new AIProjectClient(projectEndpoint, new AzureCliCredential()).AsAIAgent(agentEndpoint);
+ var options = new AIProjectClientOptions();
+
+ if (projectEndpoint.Scheme == Uri.UriSchemeHttp)
+ {
+ // For local HTTP dev: the client pipeline refuses to attach a bearer token to a plain
+ // HTTP endpoint, so point the client at an https:// URI to satisfy that check, then swap
+ // the scheme back to http:// right before the request hits the wire.
+ projectEndpoint = new UriBuilder(projectEndpoint) { Scheme = Uri.UriSchemeHttps }.Uri;
+ agentEndpoint = new UriBuilder(agentEndpoint) { Scheme = Uri.UriSchemeHttps }.Uri;
+ options.AddPolicy(new HttpSchemeRewritePolicy(), PipelinePosition.BeforeTransport);
+ }
+
+ return new AIProjectClient(projectEndpoint, new AzureCliCredential(), options).AsAIAgent(agentEndpoint);
+}
+
+///
+/// For Local Development Only.
+/// Rewrites HTTPS URIs to HTTP right before transport, allowing to
+/// target a local HTTP dev server while satisfying the pipeline's TLS check: bearer tokens are
+/// only attached to TLS-protected endpoints, so a plain http:// endpoint is rejected outright.
+///
+internal sealed class HttpSchemeRewritePolicy : PipelinePolicy
+{
+ public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex)
+ {
+ RewriteScheme(message);
+ ProcessNext(message, pipeline, currentIndex);
+ }
+
+ public override async ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex)
+ {
+ RewriteScheme(message);
+ await ProcessNextAsync(message, pipeline, currentIndex).ConfigureAwait(false);
+ }
+
+ private static void RewriteScheme(PipelineMessage message)
+ {
+ var uri = message.Request.Uri!;
+ if (uri.Scheme == Uri.UriSchemeHttps)
+ {
+ message.Request.Uri = new UriBuilder(uri) { Scheme = Uri.UriSchemeHttp }.Uri;
+ }
+ }
}
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/README.md
index ba63c0a8689..1ab2b63e3a6 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/README.md
@@ -59,6 +59,10 @@ The two choices differ only in how the agent is built:
| Local | An `OpenAIClient` pointed at `http://localhost:8088`, then `GetResponsesClient().AsAIAgent(...)`. That hits the standard `POST /responses` route the local server already serves. The model id and api key are placeholders: the server runs its own agent and ignores both, but the SDK requires them to shape the request. |
| Foundry | An `AIProjectClient` plus the agent's per-agent endpoint (`{projectEndpoint}/agents/{AZURE_AI_AGENT_NAME}/endpoint/protocols/openai`), which the platform routes to the container's `/responses` route. |
+The Foundry path also works against a local server that maps the per-agent route: set
+`FOUNDRY_PROJECT_ENDPOINT` to an `http://` URL and the client installs a scheme-rewrite policy so
+the bearer-token pipeline accepts it. See [Local HTTP dev](../README.md#local-http-dev).
+
## End-to-end demo
With a hosted agent running:
From 7b2b72f6849bdd9c0ef085362ac3a57eb2f27de7 Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Tue, 28 Jul 2026 17:47:33 +0100
Subject: [PATCH 15/23] Keep the sample package versions in the project file
---
.../Directory.Packages.props | 26 +++++++------------
.../HostedChatClientAgent.csproj | 22 +++++++++-------
.../Hosted-ChatClientAgent/README.md | 6 ++---
.../scripts/Add-LocalFrameworkFeed.ps1 | 26 +++++++++----------
.../scripts/add-local-framework-feed.sh | 18 ++++++-------
5 files changed, 46 insertions(+), 52 deletions(-)
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Directory.Packages.props b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Directory.Packages.props
index 57132b3344f..3e00b7e16e0 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Directory.Packages.props
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Directory.Packages.props
@@ -1,25 +1,17 @@
- true
- false
- 1.15.0-preview.260722.1
+ false
-
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/HostedChatClientAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/HostedChatClientAgent.csproj
index bc0f76b9ffa..0e5d3ea811b 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/HostedChatClientAgent.csproj
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/HostedChatClientAgent.csproj
@@ -1,10 +1,11 @@
@@ -18,14 +19,15 @@
HostedChatClientAgent
HostedChatClientAgent
222d2622-da26-4da0-99b4-0507fb8d41b0
+ 1.15.0-preview.260722.1
-
-
-
-
-
+
+
+
+
+
-
+
\ No newline at end of file
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
index 3ff6dbcc95c..05868a1fe45 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
@@ -21,8 +21,8 @@ This sample deploys to Foundry **directly from source (code / ZIP upload)**: the
| `Program.cs` | The agent: builds the agent, hosts it with the Responses protocol. |
| `azure.yaml` | The unified `azd` project file. Declares the Foundry project and the hosted agent with `codeConfiguration` (source/ZIP deploy), and passes the listen port and the model deployment name to the container through `env`. |
| `.agentignore` | Controls which files are excluded from the code-deploy ZIP upload (`.gitignore` syntax). |
-| `HostedChatClientAgent.csproj` | Project file. Package references carry no versions: they come from the sample's own `Directory.Packages.props`. |
-| `Directory.Packages.props` | Central package management for the sample. The ZIP has no repo-level props, so the sample carries its own copy of the versions it needs. |
+| `HostedChatClientAgent.csproj` | Self-contained project: single target framework, central package management off, explicit package versions (there is no repo-level props file inside the ZIP). |
+| `Directory.Packages.props` | Stops MSBuild from picking up the repository's own props file, so the in-repo build resolves the same packages as the server-side build of the uploaded ZIP. |
| `.env.example` | Template for local configuration. |
| `../../scripts/Add-LocalFrameworkFeed.ps1`, `../../scripts/add-local-framework-feed.sh` | Contributor-only helpers, see [Deploy your local framework changes](#deploy-your-local-framework-changes-contributors). |
@@ -242,7 +242,7 @@ The script changes three things in the scaffolded folder:
|--------|--------|
| Creates `local-feed/` | The Agent Framework packed from your local source, stamped with a version like `1.15.0-preview-local.` |
| Creates `nuget.config` | Resolves `Microsoft.Agents.AI*` from that folder and everything else from nuget.org |
-| Edits `Directory.Packages.props` | Repoints its `AgentFrameworkVersion` property at the version just packed |
+| Edits the `.csproj` | Repoints its `AgentFrameworkVersion` property at the version just packed |
Both generated files ship inside the ZIP, so the server-side restore resolves the framework from
the upload. The scaffolded folder is a throwaway copy, so the repository is left untouched.
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1 b/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1
index 6cd5beda26d..90885a2a050 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1
@@ -17,15 +17,15 @@
fill the rest from nuget.org, mixing a published core with a locally built host.
nuget.config New. Maps Microsoft.Agents.AI* to that folder feed and everything else to
nuget.org.
- the versions Edited. Directory.Packages.props has its AgentFrameworkVersion property
- repointed at the version just packed.
+ the .csproj Edited. Its AgentFrameworkVersion property is repointed at the version just
+ packed.
Neither generated file is excluded by `.agentignore`, so they travel inside the ZIP and the
server-side restore uses them.
Everything else stays identical to the end-user flow: you create the working directory, run
`azd ai agent init`, and finish with `azd provision`, `azd deploy`, and `azd ai agent invoke`.
- The scaffolded folder is a throwaway copy, so editing its package versions leaves the repository
+ The scaffolded folder is a throwaway copy, so editing its project file leaves the repository
untouched.
.PARAMETER Path
The folder `azd ai agent init` scaffolded, for example `./hosted-chat-client-agent`.
@@ -65,13 +65,13 @@ if (-not (Test-Path (Join-Path $target 'azure.yaml'))) {
throw "No azure.yaml in '$target'. Point -Path at the folder 'azd ai agent init' scaffolded."
}
-$versionsFile = Join-Path $target 'Directory.Packages.props'
-if (-not (Test-Path $versionsFile)) {
- throw "No Directory.Packages.props in '$target'. This script targets .NET hosted agents."
+$projectFile = Get-ChildItem $target -Filter *.csproj -File | Select-Object -First 1
+if (-not $projectFile) {
+ throw "No .csproj in '$target'. This script targets .NET hosted agents."
}
-if (-not (Select-String -Path $versionsFile -Pattern '' -Quiet)) {
- throw "Directory.Packages.props has no property to repoint at a local build."
+if (-not (Select-String -Path $projectFile.FullName -Pattern '' -Quiet)) {
+ throw "$($projectFile.Name) has no property to repoint at a local build."
}
$hostedRoot = Split-Path -Parent $PSScriptRoot
@@ -136,12 +136,12 @@ $nugetConfig = @'
'@
[System.IO.File]::WriteAllText((Join-Path $target 'nuget.config'), ($nugetConfig -replace "`r`n", "`n"), $utf8NoBom)
-# The scaffolded copy is disposable, so repointing its package versions at the local build is safe
-# and keeps the checked-in sample free of contributor-only scaffolding. Reruns are safe: the pattern
+# The scaffolded copy is disposable, so repointing its project file at the local build is safe and
+# keeps the checked-in sample free of contributor-only scaffolding. Reruns are safe: the pattern
# matches whatever version is currently there.
-$versionsXml = [System.IO.File]::ReadAllText($versionsFile)
-$versionsXml = $versionsXml -replace '(?)[^<]*(?)', "`${open}$version`${close}"
-[System.IO.File]::WriteAllText($versionsFile, $versionsXml, [System.Text.UTF8Encoding]::new($true))
+$projectXml = [System.IO.File]::ReadAllText($projectFile.FullName)
+$projectXml = $projectXml -replace '(?)[^<]*(?)', "`${open}$version`${close}"
+[System.IO.File]::WriteAllText($projectFile.FullName, $projectXml, [System.Text.UTF8Encoding]::new($true))
Write-Host ''
Write-Host 'Done. Continue with the standard flow:' -ForegroundColor Green
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 591f177bc84..e3556c8a695 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
@@ -16,15 +16,15 @@
# fill the rest from nuget.org, mixing a published core with a locally built host.
# nuget.config New. Maps Microsoft.Agents.AI* to that folder feed and everything else to
# nuget.org.
-# the versions Edited. Directory.Packages.props has its AgentFrameworkVersion property
-# repointed at the version just packed.
+# the .csproj Edited. Its AgentFrameworkVersion property is repointed at the version just
+# packed.
#
# Neither generated file is excluded by `.agentignore`, so they travel inside the ZIP and the
# server-side restore uses them.
#
# Everything else stays identical to the end-user flow: you create the working directory, run
# `azd ai agent init`, and finish with `azd provision`, `azd deploy`, and `azd ai agent invoke`.
-# The scaffolded folder is a throwaway copy, so editing its package versions leaves the repository
+# The scaffolded folder is a throwaway copy, so editing its project file leaves the repository
# untouched.
#
# Usage:
@@ -61,15 +61,15 @@ if [[ ! -f "$target/azure.yaml" ]]; then
exit 1
fi
-project_file="$target/Directory.Packages.props"
+project_file="$(find "$target" -maxdepth 1 -name '*.csproj' | head -n 1)"
-if [[ ! -f "$project_file" ]]; then
- echo "Error: no Directory.Packages.props in '$target'. This script targets .NET hosted agents." >&2
+if [[ -z "$project_file" ]]; then
+ echo "Error: no .csproj in '$target'. This script targets .NET hosted agents." >&2
exit 1
fi
if ! grep -q '' "$project_file"; then
- echo "Error: Directory.Packages.props has no property to repoint at a local build." >&2
+ echo "Error: $(basename "$project_file") has no property to repoint at a local build." >&2
exit 1
fi
@@ -136,8 +136,8 @@ cat > "$target/nuget.config" <<'EOF'
EOF
-# The scaffolded copy is disposable, so repointing its package versions at the local build is safe
-# and keeps the checked-in sample free of contributor-only scaffolding. Reruns are safe: the pattern
+# The scaffolded copy is disposable, so repointing its project file at the local build is safe and
+# keeps the checked-in sample free of contributor-only scaffolding. Reruns are safe: the pattern
# matches whatever version is currently there. sed rewrites the line in place, leaving the file's
# leading byte order mark untouched.
sed -i.bak "s|[^<]*|$version|" "$project_file"
From f3e3a8163b327f5fd08ec1a28f04efecd4fd984f Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Tue, 28 Jul 2026 18:16:02 +0100
Subject: [PATCH 16/23] Drop the sample Directory.Packages.props
---
.../Directory.Packages.props | 17 --------------
.../HostedChatClientAgent.csproj | 22 +++++++++++++++----
.../Hosted-ChatClientAgent/README.md | 3 +--
3 files changed, 19 insertions(+), 23 deletions(-)
delete mode 100644 dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Directory.Packages.props
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Directory.Packages.props b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Directory.Packages.props
deleted file mode 100644
index 3e00b7e16e0..00000000000
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Directory.Packages.props
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-
-
-
- false
-
-
-
\ No newline at end of file
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/HostedChatClientAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/HostedChatClientAgent.csproj
index 0e5d3ea811b..9c1561bfee4 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/HostedChatClientAgent.csproj
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/HostedChatClientAgent.csproj
@@ -1,13 +1,25 @@
-
+
+
+ false
+
+
+
+
+
+
+ false
+
+
+
+
+
+
+ net10.0
+
+ enable
+ enable
+ HostedChatClientAgentDocker
+ HostedChatClientAgentDocker
+ 7b1c3f04-24a1-4f0e-9a5e-0d2f6b8c1e57
+ 1.15.0-preview.260722.1
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/Program.cs
new file mode 100644
index 00000000000..df4f36cb0d9
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/Program.cs
@@ -0,0 +1,53 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// Sample: a minimal general-purpose AI assistant hosted as a Foundry Hosted Agent
+// using the Responses protocol. It is deployed to Foundry as a container image built
+// from the Dockerfile in this folder.
+//
+// The sibling Hosted-ChatClientAgent sample is the same agent deployed the other way,
+// straight from source with no container image. Compare the two folders to see exactly
+// what the container path adds.
+
+using Azure.AI.Projects;
+using Azure.Identity;
+using DotNetEnv;
+using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.Foundry.Hosting;
+
+// Load a local .env file when present (local development only). In Foundry the
+// platform injects the required environment variables at runtime.
+Env.TraversePath().Load();
+
+var projectEndpoint = new Uri(System.Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
+ ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set."));
+
+var model = System.Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME")
+ ?? System.Environment.GetEnvironmentVariable("FOUNDRY_MODEL")
+ ?? "gpt-4o";
+
+var agentName = System.Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-chat-client-agent-docker";
+
+// WARNING: DefaultAzureCredential is convenient for development but requires careful
+// consideration in production. Consider a specific credential (for example
+// ManagedIdentityCredential) to avoid latency, unintended credential probing, and
+// fallback security risks.
+AIAgent agent = new AIProjectClient(projectEndpoint, new DefaultAzureCredential())
+ .AsAIAgent(
+ model: model,
+ instructions: """
+ You are a helpful AI assistant hosted as a Foundry Hosted Agent.
+ You can help with a wide range of tasks including answering questions,
+ providing explanations, brainstorming ideas, and offering guidance.
+ Be concise, clear, and helpful in your responses.
+ """,
+ name: agentName,
+ description: "A simple general-purpose AI assistant");
+
+// Host the agent using the Responses protocol.
+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-ChatClientAgent-Dockerfile/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/README.md
new file mode 100644
index 00000000000..11a6fcae92e
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/README.md
@@ -0,0 +1,291 @@
+# Hosted-ChatClientAgent-Dockerfile
+
+A minimal general-purpose AI assistant hosted as a Foundry Hosted Agent using the Responses protocol. The agent is created inline via `AIProjectClient.AsAIAgent(model, instructions)` and served with `AddFoundryResponses` / `MapFoundryResponses`.
+
+This sample deploys to Foundry as a **container image** built from the `Dockerfile` in this folder.
+
+The sibling [`Hosted-ChatClientAgent`](../Hosted-ChatClientAgent/) sample is the same agent deployed the other way, straight from source with no container image. That is the default for .NET and needs no Docker, so prefer it unless you need control over the runtime image.
+
+| | Source (ZIP) | Container (this sample) |
+|---|---|---|
+| Deploy mode | `code`, the default for .NET | `container`, opt in with `--deploy-mode container` |
+| Extra files | none | `Dockerfile`, `.dockerignore` |
+| Who builds | Foundry runs `dotnet restore` + `dotnet publish` on the upload | Foundry builds the `Dockerfile` |
+| Docker required | no | no, `azd` builds remotely in Azure Container Registry |
+| Listen port | the package binds it, or `env` in `azure.yaml` | `ENV ASPNETCORE_URLS` in the `Dockerfile` |
+| Extra Azure resource | none | an Azure Container Registry |
+
+## Prerequisites
+
+- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
+- An **existing** Foundry project with an **existing** model deployment (for example `gpt-4o`).
+ This sample's `azure.yaml` declares no `deployments:` block, so `azd` connects to a project and
+ a deployment you already have rather than creating them. `azd ai agent init` takes both as
+ arguments (`-p` and `-d`) and prompts for them when they are omitted.
+- Azure CLI logged in (`az login`)
+- Azure Developer CLI (`azd`) with the AI agents extension: `azd extension install azure.ai.agents`
+- Docker Desktop **only** if you switch to local image builds by setting `remoteBuild: false` under
+ the `docker:` block in `azure.yaml`. By default `azd` builds the image in Azure Container
+ Registry, so no local Docker is needed.
+
+## Files
+
+| File | Purpose |
+|------|---------|
+| `Program.cs` | The agent: builds the agent, hosts it with the Responses protocol. |
+| `Dockerfile` | Builds the image Foundry runs. Restores and publishes the project inside the container, and pins the listen port to 8088. |
+| `.dockerignore` | Keeps local-only files (notably `.env`) out of the image build context. |
+| `azure.yaml` | The unified `azd` project file. Declares the Foundry project and the hosted agent with `language: docker` and no `codeConfiguration`, which is what selects the container path. |
+| `HostedChatClientAgentDocker.csproj` | Self-contained project: single target framework and explicit package versions. It also opts out of the repository's central package management, which does not travel inside the image build context. |
+| `.env.example` | Template for local configuration. |
+
+## Configuration
+
+Copy the template and fill in your project endpoint:
+
+PowerShell:
+
+```powershell
+copy .env.example .env
+```
+
+Bash:
+
+```bash
+cp .env.example .env
+```
+
+```env
+FOUNDRY_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/
+AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
+ASPNETCORE_URLS=http://+:8088
+AZURE_TOKEN_CREDENTIALS=dev
+```
+
+> `.env` is gitignored, and `.dockerignore` keeps it out of the image. The `.env.example` template
+> is checked in as a reference.
+
+> **Windows note:** write `.env` as UTF-8 **without** a byte order mark. `azd` reads the file
+> during `azd ai agent init` and fails with `unexpected character "»" in variable name` when a mark
+> is present. PowerShell's `Set-Content -Encoding UTF8BOM` adds one; use `-Encoding utf8NoBOM`.
+
+> **Local development on a machine without a managed identity:** set `AZURE_TOKEN_CREDENTIALS=dev`.
+> `Program.cs` authenticates with `DefaultAzureCredential` (the pattern the hosted platform
+> expects, where a managed identity is injected). On a developer machine with no managed identity,
+> `DefaultAzureCredential` probes the Azure Instance Metadata Service (IMDS, `169.254.169.254`) and
+> blocks for a long time on the network timeout before every model call, so requests appear to
+> hang. Setting `AZURE_TOKEN_CREDENTIALS=dev` restricts `DefaultAzureCredential` to developer
+> credentials (Azure CLI, Visual Studio, `azd`) and skips the managed-identity probe. This variable
+> is only for local runs; the deployed agent in Foundry uses the platform-injected managed identity.
+
+## Run and test locally
+
+Local runs use two terminals: one hosts the agent, the other is a code-first client that talks to it
+using Agent Framework components, see the sibling [`Using-Samples`](../Using-Samples/) REPLs.
+
+**Terminal 1 — host the agent:**
+
+```
+cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile
+az login
+dotnet run
+```
+
+The agent starts on `http://localhost:8088`.
+
+**Terminal 2 — chat with it (code-first REPL):**
+
+PowerShell:
+
+```powershell
+cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent
+$env:AZURE_AI_AGENT_NAME = "hosted-chat-client-agent-docker"
+dotnet run -- --local
+```
+
+Bash:
+
+```bash
+cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent
+export AZURE_AI_AGENT_NAME="hosted-chat-client-agent-docker"
+dotnet run -- --local
+```
+
+To exercise the image instead of the host build, build and run the container directly:
+
+```
+docker build -t hosted-chat-client-agent-docker .
+docker run --rm -p 8088:8088 --env-file .env hosted-chat-client-agent-docker
+```
+
+## Deploy to Foundry (container)
+
+`azd` scaffolds the project into a working folder, so every step below runs from an **empty
+directory outside the repository**, and `-m` points at this sample's `azure.yaml`.
+
+### Step 1: create the working directory and enter it
+
+PowerShell:
+
+```powershell
+$work = Join-Path $env:TEMP "hosted-chat-docker-work"
+mkdir $work
+cd $work
+```
+
+Bash:
+
+```bash
+WORK="${TMPDIR:-/tmp}/hosted-chat-docker-work"
+mkdir -p "$WORK"
+cd "$WORK"
+```
+
+### Step 2: scaffold the project
+
+`--deploy-mode container` is the argument that selects the container path. Without it `azd`
+defaults to `code` for .NET, which ignores the `Dockerfile` and deploys the source as a ZIP.
+
+`azd ai agent init` copies the sample into a subfolder named after the top-level `name:` in
+`azure.yaml`, which is `hosted-chat-client-agent-docker`. It also writes the adopted `azure.yaml`
+and the `azd` environment there.
+
+`-p` is the resource ID of an existing Foundry project and `-d` the name of an existing model
+deployment in it. Omit either one and `azd` prompts for it.
+
+PowerShell:
+
+```powershell
+$sample = "/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/azure.yaml"
+$projectId = "/subscriptions//resourceGroups//providers/Microsoft.CognitiveServices/accounts//projects/"
+
+azd auth login
+azd ai agent init -m $sample -p $projectId -d --deploy-mode container
+```
+
+Bash:
+
+```bash
+SAMPLE="/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/azure.yaml"
+PROJECT_ID="/subscriptions//resourceGroups//providers/Microsoft.CognitiveServices/accounts//projects/"
+
+azd auth login
+azd ai agent init -m "$SAMPLE" -p "$PROJECT_ID" -d --deploy-mode container
+```
+
+### Step 3: provision and deploy
+
+Contributors: to run the agent against your **local** Agent Framework build instead of the
+published packages, those packages have to be built and packed into the build context first. Do
+that now, before the commands below, following
+[Deploy your local framework changes](#deploy-your-local-framework-changes-contributors).
+
+```
+cd hosted-chat-client-agent-docker
+azd provision
+azd deploy
+azd ai agent invoke "Hello!"
+```
+
+`azd provision` creates the Azure Container Registry the image is pushed to, alongside the rest of
+the environment. `azd deploy` builds the image (remotely in that registry by default), pushes it,
+and creates the agent version.
+
+To build the image on your own machine instead, flip `remoteBuild` to `false` in `azure.yaml`:
+
+```yaml
+ docker:
+ remoteBuild: false
+```
+
+That requires Docker Desktop, and on Apple Silicon or other ARM machines you must produce an
+x86_64 image, since the hosting platform only runs `linux/amd64`.
+
+You can also test the deployed agent with the REPL:
+
+PowerShell:
+
+```powershell
+cd /dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent
+$env:FOUNDRY_PROJECT_ENDPOINT = "https://.services.ai.azure.com/api/projects/"
+$env:AZURE_AI_AGENT_NAME = "hosted-chat-client-agent-docker"
+dotnet run -- --remote
+```
+
+Bash:
+
+```bash
+cd /dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent
+export FOUNDRY_PROJECT_ENDPOINT="https://.services.ai.azure.com/api/projects/"
+export AZURE_AI_AGENT_NAME="hosted-chat-client-agent-docker"
+dotnet run -- --remote
+```
+
+### Step 4: clean up
+
+```
+azd down
+```
+
+Then delete the working directory.
+
+## Deploy your local framework changes (contributors)
+
+By default the project restores the **published** Agent Framework packages, so local changes to the
+framework are never exercised: the `dotnet restore` inside the image build pulls them from
+nuget.org.
+
+To deploy your local build instead, run one extra step in the flow above, **between step 2 and
+step 3**. Everything else is unchanged, and it is the same script the source-deploy sample uses:
+the `Dockerfile` copies the whole folder before restoring, so a feed dropped in this folder is
+picked up by the restore inside the image build with no change to the `Dockerfile`.
+
+Run it from `$work`, the working directory created in step 1, which now holds the
+`hosted-chat-client-agent-docker` folder that `azd ai agent init` scaffolded:
+
+PowerShell:
+
+```powershell
+cd $work
+/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1 -Path ./hosted-chat-client-agent-docker
+```
+
+Bash:
+
+```bash
+cd "$WORK"
+/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/add-local-framework-feed.sh ./hosted-chat-client-agent-docker
+```
+
+Then continue with step 3. The path argument is optional: called without it, the script uses the
+current directory, so you can also run it from inside `hosted-chat-client-agent-docker`.
+
+The script changes three things in the scaffolded folder:
+
+| Change | Detail |
+|--------|--------|
+| Creates `local-feed/` | The Agent Framework packed from your local source, stamped with a version like `1.15.0-preview-local.` |
+| Creates `nuget.config` | Resolves `Microsoft.Agents.AI*` from that folder and everything else from nuget.org |
+| Edits the `.csproj` | Repoints its `AgentFrameworkVersion` property at the version just packed |
+
+Neither generated file is excluded by `.dockerignore`, so both reach the image build context and
+the restore inside the build uses them. The scaffolded folder is a throwaway copy, so the
+repository is left untouched.
+
+Two details worth knowing:
+
+- The version carries a timestamp because NuGet caches by package id and version. Reusing a version
+ would silently restore the previously packed bits instead of the build you just made.
+- The whole package closure is packed, not just the two packages the sample references. Packing
+ only the leaf packages lets NuGet fill the rest from nuget.org, mixing a published core with a
+ locally built host, which fails to compile.
+
+Before spending a deploy, build the scaffolded folder locally. A restore problem surfaces in
+seconds instead of after the image build:
+
+```
+cd hosted-chat-client-agent-docker
+dotnet build -c Debug --tl:off
+```
+
+For the full hosted-agent deployment guide, see the [official container deployment doc](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent).
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/azure.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/azure.yaml
new file mode 100644
index 00000000000..53ce81e2192
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/azure.yaml
@@ -0,0 +1,44 @@
+# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json
+
+name: hosted-chat-client-agent-docker
+services:
+ ai-project:
+ host: azure.ai.project
+ hosted-chat-client-agent-docker:
+ project: .
+ host: azure.ai.agent
+ # `docker` selects the container build path: Foundry builds the Dockerfile in this folder
+ # instead of restoring a plain source folder. There is no codeConfiguration block here,
+ # which is what tells the tooling this is a container deploy rather than a source deploy.
+ language: docker
+ docker:
+ # azd builds the image in Azure Container Registry by default. Set this to false to
+ # build on your own machine instead, which requires Docker Desktop.
+ remoteBuild: true
+ uses:
+ - ai-project
+ # ${AZURE_AI_MODEL_DEPLOYMENT_NAME} reads the model deployment `azd ai agent init` recorded
+ # in the active azd environment. Without it the container falls back to the default model
+ # name hardcoded in Program.cs, which may not exist in the target project.
+ #
+ # The listen port is not set here: the Dockerfile already sets ASPNETCORE_URLS.
+ env:
+ AZURE_AI_MODEL_DEPLOYMENT_NAME: ${AZURE_AI_MODEL_DEPLOYMENT_NAME}
+ container:
+ resources:
+ cpu: "0.5"
+ memory: 1Gi
+ description: |
+ A simple general-purpose AI assistant hosted as a Foundry Hosted Agent, deployed as a container image.
+ kind: hosted
+ metadata:
+ tags:
+ - AI Agent Hosting
+ - Azure AI AgentServer
+ - Responses Protocol
+ - Streaming
+ - Agent Framework
+ name: hosted-chat-client-agent-docker
+ protocols:
+ - protocol: responses
+ version: 2.0.0
\ No newline at end of file
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
index eae5e736068..b4bf12a81f3 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
@@ -4,6 +4,8 @@ A minimal general-purpose AI assistant hosted as a Foundry Hosted Agent using th
This sample deploys to Foundry **directly from source (code / ZIP upload)**: the platform builds and runs your code with no container image, so there is no Dockerfile to author or container registry to manage.
+The sibling [`Hosted-ChatClientAgent-Dockerfile`](../Hosted-ChatClientAgent-Dockerfile/) sample is the same agent deployed the other way, as a container image built from a `Dockerfile`. Source deploy is the default for .NET, so start here and switch only if you need control over the runtime image.
+
## Prerequisites
- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
From d892540b75d282f8b236f63e65f184d332869cfc Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Tue, 28 Jul 2026 19:13:10 +0100
Subject: [PATCH 18/23] Treat a blank model deployment variable as unset
---
.../Hosted-ChatClientAgent-Dockerfile/Program.cs | 14 +++++++++++---
.../Hosted-ChatClientAgent-Dockerfile/README.md | 8 ++++++++
.../responses/Hosted-ChatClientAgent/Program.cs | 14 +++++++++++---
.../responses/Hosted-ChatClientAgent/README.md | 8 ++++++++
4 files changed, 38 insertions(+), 6 deletions(-)
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/Program.cs
index df4f36cb0d9..2461d871dcb 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/Program.cs
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/Program.cs
@@ -21,9 +21,13 @@
var projectEndpoint = new Uri(System.Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set."));
-var model = System.Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME")
- ?? System.Environment.GetEnvironmentVariable("FOUNDRY_MODEL")
- ?? "gpt-4o";
+// Environment variables can arrive set but blank: azd substitutes an empty string when the azd
+// environment does not define the variable referenced from azure.yaml. An empty string is not
+// null, so a plain ?? chain would pass the blank straight through and fail deep inside the SDK.
+var model = FirstNonBlank(
+ System.Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME"),
+ System.Environment.GetEnvironmentVariable("FOUNDRY_MODEL"),
+ "gpt-4o");
var agentName = System.Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-chat-client-agent-docker";
@@ -51,3 +55,7 @@ You are a helpful AI assistant hosted as a Foundry Hosted Agent.
app.MapFoundryResponses();
app.Run();
+
+// Returns the first candidate that has an actual value, ignoring null and blank entries.
+static string FirstNonBlank(params string?[] candidates) =>
+ Array.Find(candidates, c => !string.IsNullOrWhiteSpace(c))!;
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/README.md
index 11a6fcae92e..775ceed80dc 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/README.md
@@ -153,6 +153,14 @@ and the `azd` environment there.
`-p` is the resource ID of an existing Foundry project and `-d` the name of an existing model
deployment in it. Omit either one and `azd` prompts for it.
+`azure.yaml` passes the model deployment to the container by reading it from the `azd` environment.
+Confirm it landed there, and set it yourself if it did not:
+
+```
+azd env get-values
+azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME
+```
+
PowerShell:
```powershell
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Program.cs
index 9a2f4577994..9b2484b19fe 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Program.cs
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/Program.cs
@@ -17,9 +17,13 @@
var projectEndpoint = new Uri(System.Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set."));
-var model = System.Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME")
- ?? System.Environment.GetEnvironmentVariable("FOUNDRY_MODEL")
- ?? "gpt-4o";
+// Environment variables can arrive set but blank: azd substitutes an empty string when the azd
+// environment does not define the variable referenced from azure.yaml. An empty string is not
+// null, so a plain ?? chain would pass the blank straight through and fail deep inside the SDK.
+var model = FirstNonBlank(
+ System.Environment.GetEnvironmentVariable("AZURE_AI_MODEL_DEPLOYMENT_NAME"),
+ System.Environment.GetEnvironmentVariable("FOUNDRY_MODEL"),
+ "gpt-4o");
var agentName = System.Environment.GetEnvironmentVariable("AGENT_NAME") ?? "hosted-chat-client-agent";
@@ -47,3 +51,7 @@ You are a helpful AI assistant hosted as a Foundry Hosted Agent.
app.MapFoundryResponses();
app.Run();
+
+// Returns the first candidate that has an actual value, ignoring null and blank entries.
+static string FirstNonBlank(params string?[] candidates) =>
+ Array.Find(candidates, c => !string.IsNullOrWhiteSpace(c))!;
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
index b4bf12a81f3..e48787fdec8 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
@@ -143,6 +143,14 @@ cd "$WORK"
`-p` is the resource ID of an existing Foundry project and `-d` the name of an existing model
deployment in it. Omit either one and `azd` prompts for it.
+`azure.yaml` passes the model deployment to the container by reading it from the `azd` environment.
+Confirm it landed there, and set it yourself if it did not:
+
+```
+azd env get-values
+azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME
+```
+
PowerShell:
```powershell
From 568bcc58bf1ca6a17548d4eb13ea247bc0d4eb13 Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Tue, 28 Jul 2026 19:57:08 +0100
Subject: [PATCH 19/23] Let azd prompt for the Foundry project and expand the
contributor section
---
.../README.md | 50 ++++++++++++-------
.../Hosted-ChatClientAgent/README.md | 46 +++++++++++------
2 files changed, 62 insertions(+), 34 deletions(-)
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/README.md
index 775ceed80dc..6e0108598e3 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/README.md
@@ -20,8 +20,8 @@ The sibling [`Hosted-ChatClientAgent`](../Hosted-ChatClientAgent/) sample is the
- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
- An **existing** Foundry project with an **existing** model deployment (for example `gpt-4o`).
This sample's `azure.yaml` declares no `deployments:` block, so `azd` connects to a project and
- a deployment you already have rather than creating them. `azd ai agent init` takes both as
- arguments (`-p` and `-d`) and prompts for them when they are omitted.
+ a deployment you already have rather than creating them. `azd ai agent init` prompts you to pick
+ the project, and takes the deployment name as the `-d` argument.
- Azure CLI logged in (`az login`)
- Azure Developer CLI (`azd`) with the AI agents extension: `azd extension install azure.ai.agents`
- Docker Desktop **only** if you switch to local image builds by setting `remoteBuild: false` under
@@ -150,8 +150,14 @@ defaults to `code` for .NET, which ignores the `Dockerfile` and deploys the sour
`azure.yaml`, which is `hosted-chat-client-agent-docker`. It also writes the adopted `azure.yaml`
and the `azd` environment there.
-`-p` is the resource ID of an existing Foundry project and `-d` the name of an existing model
-deployment in it. Omit either one and `azd` prompts for it.
+`azd ai agent init` prompts you to pick the Foundry project, so no project argument is needed.
+`-d` is the name of an existing model deployment in that project; omit it and `azd` prompts for
+that too.
+
+> Pick an **existing** project at the prompt. The prompt needs an interactive terminal: run
+> non-interactively (in CI, for example) and `azd` skips it and provisions a brand new Foundry
+> project and resource group instead. Pass `-p ` when you need that to be
+> unattended.
`azure.yaml` passes the model deployment to the container by reading it from the `azd` environment.
Confirm it landed there, and set it yourself if it did not:
@@ -165,28 +171,26 @@ PowerShell:
```powershell
$sample = "/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/azure.yaml"
-$projectId = "/subscriptions//resourceGroups//providers/Microsoft.CognitiveServices/accounts//projects/"
azd auth login
-azd ai agent init -m $sample -p $projectId -d --deploy-mode container
+azd ai agent init -m $sample -d --deploy-mode container
```
Bash:
```bash
SAMPLE="/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/azure.yaml"
-PROJECT_ID="/subscriptions//resourceGroups//providers/Microsoft.CognitiveServices/accounts//projects/"
azd auth login
-azd ai agent init -m "$SAMPLE" -p "$PROJECT_ID" -d --deploy-mode container
+azd ai agent init -m "$SAMPLE" -d --deploy-mode container
```
### Step 3: provision and deploy
-Contributors: to run the agent against your **local** Agent Framework build instead of the
-published packages, those packages have to be built and packed into the build context first. Do
-that now, before the commands below, following
-[Deploy your local framework changes](#deploy-your-local-framework-changes-contributors).
+Contributors: if you are changing the Agent Framework source in this repository and want the
+deployed agent to run **your** build rather than the published packages, do the extra step in
+[Deploy your local framework changes](#deploy-your-local-framework-changes-contributors) now,
+before the commands below. Everyone else can ignore it.
```
cd hosted-chat-client-agent-docker
@@ -239,14 +243,24 @@ Then delete the working directory.
## Deploy your local framework changes (contributors)
-By default the project restores the **published** Agent Framework packages, so local changes to the
-framework are never exercised: the `dotnet restore` inside the image build pulls them from
+**Skip this section unless you are changing the Agent Framework itself.** Everything above is the
+complete flow for using the sample. This section only applies when you are working on the framework
+source in this repository, or when you otherwise need a build of it that is not published on
+nuget.org.
+
+The reason it exists: the project restores the **published** Agent Framework packages, and the
+`dotnet restore` inside the image build pulls them from nuget.org. So editing framework source in
+this repository changes nothing about the deployed agent, no matter how many times you rebuild
+locally. The image build context is self-contained and knows nothing about your working tree.
+
+The extra step packs your local framework source into NuGet packages and puts them **inside the
+build context**, together with a `nuget.config` that points the restore at them. The restore inside
+the image build then resolves the framework from the packages you shipped instead of from
nuget.org.
-To deploy your local build instead, run one extra step in the flow above, **between step 2 and
-step 3**. Everything else is unchanged, and it is the same script the source-deploy sample uses:
-the `Dockerfile` copies the whole folder before restoring, so a feed dropped in this folder is
-picked up by the restore inside the image build with no change to the `Dockerfile`.
+Run it in the flow above, **between step 2 and step 3**. Nothing else changes, and it is the same
+script the source-deploy sample uses: the `Dockerfile` copies the whole folder before restoring, so
+a feed dropped in this folder is picked up with no change to the `Dockerfile`.
Run it from `$work`, the working directory created in step 1, which now holds the
`hosted-chat-client-agent-docker` folder that `azd ai agent init` scaffolded:
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
index e48787fdec8..f5f5af01a8d 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
@@ -11,8 +11,8 @@ The sibling [`Hosted-ChatClientAgent-Dockerfile`](../Hosted-ChatClientAgent-Dock
- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
- An **existing** Foundry project with an **existing** model deployment (for example `gpt-4o`).
This sample's `azure.yaml` declares no `deployments:` block, so `azd` connects to a project and
- a deployment you already have rather than creating them. `azd ai agent init` takes both as
- arguments (`-p` and `-d`) and prompts for them when they are omitted.
+ a deployment you already have rather than creating them. `azd ai agent init` prompts you to pick
+ the project, and takes the deployment name as the `-d` argument.
- Azure CLI logged in (`az login`)
- Azure Developer CLI (`azd`) with the AI agents extension: `azd extension install azure.ai.agents`
@@ -140,8 +140,14 @@ cd "$WORK"
`azure.yaml`, which is `hosted-chat-client-agent`. It also writes the adopted `azure.yaml` and the
`azd` environment there.
-`-p` is the resource ID of an existing Foundry project and `-d` the name of an existing model
-deployment in it. Omit either one and `azd` prompts for it.
+`azd ai agent init` prompts you to pick the Foundry project, so no project argument is needed.
+`-d` is the name of an existing model deployment in that project; omit it and `azd` prompts for
+that too.
+
+> Pick an **existing** project at the prompt. The prompt needs an interactive terminal: run
+> non-interactively (in CI, for example) and `azd` skips it and provisions a brand new Foundry
+> project and resource group instead. Pass `-p ` when you need that to be
+> unattended.
`azure.yaml` passes the model deployment to the container by reading it from the `azd` environment.
Confirm it landed there, and set it yourself if it did not:
@@ -155,28 +161,26 @@ PowerShell:
```powershell
$sample = "/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/azure.yaml"
-$projectId = "/subscriptions//resourceGroups//providers/Microsoft.CognitiveServices/accounts//projects/"
azd auth login
-azd ai agent init -m $sample -p $projectId -d
+azd ai agent init -m $sample -d
```
Bash:
```bash
SAMPLE="/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/azure.yaml"
-PROJECT_ID="/subscriptions//resourceGroups//providers/Microsoft.CognitiveServices/accounts//projects/"
azd auth login
-azd ai agent init -m "$SAMPLE" -p "$PROJECT_ID" -d
+azd ai agent init -m "$SAMPLE" -d
```
### Step 3: provision and deploy
-Contributors: to run the agent against your **local** Agent Framework build instead of the
-published packages, those packages have to be built and packed into the upload first. Do that now,
-before the commands below, following
-[Deploy your local framework changes](#deploy-your-local-framework-changes-contributors).
+Contributors: if you are changing the Agent Framework source in this repository and want the
+deployed agent to run **your** build rather than the published packages, do the extra step in
+[Deploy your local framework changes](#deploy-your-local-framework-changes-contributors) now,
+before the commands below. Everyone else can ignore it.
```
cd hosted-chat-client-agent
@@ -219,11 +223,21 @@ Then delete the working directory.
## Deploy your local framework changes (contributors)
-By default the project restores the **published** Agent Framework packages, so local changes to the
-framework are never exercised: Foundry restores from nuget.org when it builds the upload.
+**Skip this section unless you are changing the Agent Framework itself.** Everything above is the
+complete flow for using the sample. This section only applies when you are working on the framework
+source in this repository, or when you otherwise need a build of it that is not published on
+nuget.org.
+
+The reason it exists: the project restores the **published** Agent Framework packages, and Foundry
+restores from nuget.org when it builds the upload. So editing framework source in this repository
+changes nothing about the deployed agent, no matter how many times you rebuild locally. The
+uploaded folder is self-contained and knows nothing about your working tree.
+
+The extra step packs your local framework source into NuGet packages and puts them **inside the
+upload**, together with a `nuget.config` that points the restore at them. The server-side restore
+then resolves the framework from the packages you shipped instead of from nuget.org.
-To deploy your local build instead, run one extra step in the flow above, **between step 2 and
-step 3**. Everything else is unchanged.
+Run it in the flow above, **between step 2 and step 3**. Nothing else changes.
Run it from `$work`, the working directory created in step 1, which now holds the
`hosted-chat-client-agent` folder that `azd ai agent init` scaffolded:
From 2e3b46f7f446e0c1fdec0b18d096b842313cf4d0 Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Tue, 28 Jul 2026 20:14:44 +0100
Subject: [PATCH 20/23] Document the stale conversation 404 in the hosted agent
samples
---
.../responses/Hosted-AzureSearchRag/README.md | 14 ++++++++++++++
.../Hosted-ChatClientAgent-Dockerfile/README.md | 14 ++++++++++++++
.../responses/Hosted-ChatClientAgent/README.md | 14 ++++++++++++++
.../responses/Hosted-FoundryAgent/README.md | 14 ++++++++++++++
.../responses/Hosted-LocalCodeAct/README.md | 14 ++++++++++++++
.../responses/Hosted-LocalTools/README.md | 14 ++++++++++++++
.../responses/Hosted-McpTools/README.md | 13 +++++++++++++
.../responses/Hosted-Observability/README.md | 14 ++++++++++++++
.../responses/Hosted-TextRag/README.md | 14 ++++++++++++++
.../responses/Hosted-Toolbox-AuthPaths/README.md | 1 +
.../responses/Hosted-Toolbox/README.md | 14 ++++++++++++++
.../responses/Hosted-ToolboxMcpSkills/README.md | 14 ++++++++++++++
.../responses/Hosted-Workflow-Handoff/README.md | 14 ++++++++++++++
.../responses/Hosted-Workflow-Simple/README.md | 14 ++++++++++++++
.../responses/Using-Samples/SimpleAgent/README.md | 14 ++++++++++++++
15 files changed, 196 insertions(+)
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/README.md
index e57b90201d0..b68f7c3a87d 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-AzureSearchRag/README.md
@@ -205,3 +205,17 @@ For end-to-end hosted agent deployment guidance, see the [official deployment gu
## NuGet package users
If you are consuming the Agent Framework as a NuGet package (not building from source), use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in `HostedAzureSearchRag.csproj` for the `PackageReference` alternative.
+
+## Troubleshooting
+
+**`azd ai agent invoke` fails with `404 not_found: Conversation '' not found`**
+
+`azd` saves the session and conversation per agent and reuses them on the next invoke. Once the
+agent is redeployed, deleted, or restarted, that saved conversation no longer exists on the server,
+so every following invoke fails even though the agent itself is healthy. Start a fresh one:
+
+```
+azd ai agent invoke --new-conversation "Hello!"
+```
+
+Add `--new-session` as well if the failure persists.
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/README.md
index 6e0108598e3..048302530fc 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/README.md
@@ -310,4 +310,18 @@ cd hosted-chat-client-agent-docker
dotnet build -c Debug --tl:off
```
+## Troubleshooting
+
+**`azd ai agent invoke` fails with `404 not_found: Conversation '' not found`**
+
+`azd` saves the session and conversation per agent and reuses them on the next invoke. Once the
+agent is redeployed, deleted, or restarted, that saved conversation no longer exists on the server,
+so every following invoke fails even though the agent itself is healthy. Start a fresh one:
+
+```
+azd ai agent invoke --new-conversation "Hello!"
+```
+
+Add `--new-session` as well if the failure persists.
+
For the full hosted-agent deployment guide, see the [official container deployment doc](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent).
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
index f5f5af01a8d..2c14251a87a 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/README.md
@@ -286,4 +286,18 @@ cd hosted-chat-client-agent
dotnet build -c Debug --tl:off
```
+## Troubleshooting
+
+**`azd ai agent invoke` fails with `404 not_found: Conversation '' not found`**
+
+`azd` saves the session and conversation per agent and reuses them on the next invoke. Once the
+agent is redeployed, deleted, or restarted, that saved conversation no longer exists on the server,
+so every following invoke fails even though the agent itself is healthy. Start a fresh one:
+
+```
+azd ai agent invoke --new-conversation "Hello!"
+```
+
+Add `--new-session` as well if the failure persists.
+
For the full hosted-agent deployment guide, see the [official source-code deployment doc](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/deploy-hosted-agent-code).
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/README.md
index 88a8cec48e9..63bad683488 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-FoundryAgent/README.md
@@ -145,3 +145,17 @@ If you are consuming the Agent Framework as a NuGet package (not building from s
| **Model/instructions** | Set in `Program.cs` | Set in Foundry UI/CLI/API |
| **Tools** | Defined in code | Configured in the platform |
| **Use case** | Full control over agent behavior | Platform-managed agent with centralized config |
+
+## Troubleshooting
+
+**`azd ai agent invoke` fails with `404 not_found: Conversation '' not found`**
+
+`azd` saves the session and conversation per agent and reuses them on the next invoke. Once the
+agent is redeployed, deleted, or restarted, that saved conversation no longer exists on the server,
+so every following invoke fails even though the agent itself is healthy. Start a fresh one:
+
+```
+azd ai agent invoke --new-conversation "Hello!"
+```
+
+Add `--new-session` as well if the failure persists.
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/README.md
index bc0dfdd581a..e58149f4de2 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalCodeAct/README.md
@@ -157,3 +157,17 @@ If you are consuming the Agent Framework as a NuGet package (not building from
source), use the standard `Dockerfile` instead of `Dockerfile.contributor`. See
the commented section in `HostedLocalCodeAct.csproj` for the `PackageReference`
alternative.
+
+## Troubleshooting
+
+**`azd ai agent invoke` fails with `404 not_found: Conversation '' not found`**
+
+`azd` saves the session and conversation per agent and reuses them on the next invoke. Once the
+agent is redeployed, deleted, or restarted, that saved conversation no longer exists on the server,
+so every following invoke fails even though the agent itself is healthy. Start a fresh one:
+
+```
+azd ai agent invoke --new-conversation "Hello!"
+```
+
+Add `--new-session` as well if the failure persists.
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/README.md
index f1b132fbcfa..95718065f31 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-LocalTools/README.md
@@ -139,3 +139,17 @@ For end-to-end hosted agent deployment guidance, see the [official deployment gu
## NuGet package users
If you are consuming the Agent Framework as a NuGet package (not building from source), use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in `HostedLocalTools.csproj` for the `PackageReference` alternative.
+
+## Troubleshooting
+
+**`azd ai agent invoke` fails with `404 not_found: Conversation '' not found`**
+
+`azd` saves the session and conversation per agent and reuses them on the next invoke. Once the
+agent is redeployed, deleted, or restarted, that saved conversation no longer exists on the server,
+so every following invoke fails even though the agent itself is healthy. Start a fresh one:
+
+```
+azd ai agent invoke --new-conversation "Hello!"
+```
+
+Add `--new-session` as well if the failure persists.
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/README.md
index 337366a563f..a67d31c3881 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-McpTools/README.md
@@ -115,3 +115,16 @@ Use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the comme
- [`Hosted-Toolbox/`](../Hosted-Toolbox/) — connects to a single Foundry Toolbox via the AF Foundry hosting bridge (`AddFoundryToolboxes` + `FoundryAITool.CreateHostedMcpToolbox`).
- [`Hosted-Toolbox-AuthPaths/`](../Hosted-Toolbox-AuthPaths/) — same hosting bones as `Hosted-Toolbox/`, but the toolbox bundles three MCP tools each authenticated differently (key, Entra agent identity, inline `Authorization`), driven by the shared `Using-Samples/SimpleAgent/` REPL.
+## Troubleshooting
+
+**`azd ai agent invoke` fails with `404 not_found: Conversation '' not found`**
+
+`azd` saves the session and conversation per agent and reuses them on the next invoke. Once the
+agent is redeployed, deleted, or restarted, that saved conversation no longer exists on the server,
+so every following invoke fails even though the agent itself is healthy. Start a fresh one:
+
+```
+azd ai agent invoke --new-conversation "Hello!"
+```
+
+Add `--new-session` as well if the failure persists.
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/README.md
index dd954c493cf..987d768e5eb 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Observability/README.md
@@ -135,3 +135,17 @@ For end-to-end hosted agent deployment guidance, see the [official deployment gu
## NuGet package users
If consuming the Agent Framework as a NuGet package, use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in `HostedObservability.csproj` for the `PackageReference` alternative.
+
+## Troubleshooting
+
+**`azd ai agent invoke` fails with `404 not_found: Conversation '' not found`**
+
+`azd` saves the session and conversation per agent and reuses them on the next invoke. Once the
+agent is redeployed, deleted, or restarted, that saved conversation no longer exists on the server,
+so every following invoke fails even though the agent itself is healthy. Start a fresh one:
+
+```
+azd ai agent invoke --new-conversation "Hello!"
+```
+
+Add `--new-session` as well if the failure persists.
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/README.md
index 792b980db72..f38a7a4a943 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-TextRag/README.md
@@ -142,3 +142,17 @@ For end-to-end hosted agent deployment guidance, see the [official deployment gu
## NuGet package users
If you are consuming the Agent Framework as a NuGet package (not building from source), use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in `HostedTextRag.csproj` for the `PackageReference` alternative.
+
+## Troubleshooting
+
+**`azd ai agent invoke` fails with `404 not_found: Conversation '' not found`**
+
+`azd` saves the session and conversation per agent and reuses them on the next invoke. Once the
+agent is redeployed, deleted, or restarted, that saved conversation no longer exists on the server,
+so every following invoke fails even though the agent itself is healthy. Start a fresh one:
+
+```
+azd ai agent invoke --new-conversation "Hello!"
+```
+
+Add `--new-session` as well if the failure persists.
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/README.md
index 48763f08990..ca869e687b8 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/README.md
@@ -190,6 +190,7 @@ Send a test email to myself. # path #4 —
| **HTTP 404 from a tool call** | Toolbox name mismatch (`TOOLBOX_NAME` vs the name in the portal), or the toolbox was deleted. |
| **Server logs a warning "Neither FOUNDRY_PROJECT_ENDPOINT nor AZURE_AI_PROJECT_ENDPOINT is set; toolbox support is disabled"** | Local dev without the env var set. The agent will load with zero tools and respond as if it has none. Set `AZURE_AI_PROJECT_ENDPOINT` (local-dev fallback) or `FOUNDRY_PROJECT_ENDPOINT` to your project endpoint. |
| **Tools appear but model never invokes them** | `instructions:` in `Program.cs` may not surface what each tool is for. Tighten the `allowed_tools` lists and rephrase prompts to mention the upstream service by name. |
+| **`azd ai agent invoke` returns `404 not_found: Conversation '' not found`** | `azd` saves the session and conversation per agent and reuses them on the next invoke. Once the agent is redeployed, deleted, or restarted, that saved conversation no longer exists on the server. Pass `--new-conversation` (and `--new-session` if it persists) to start a fresh one. |
## Region and model compatibility
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/README.md
index c8f86575dea..a825bf576d6 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox/README.md
@@ -109,3 +109,17 @@ Use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the comme
- [`Hosted-Toolbox-AuthPaths/`](../Hosted-Toolbox-AuthPaths/) — same hosting bones as this sample, but the toolbox bundles three MCP tools each authenticated differently (key, Entra agent identity, inline `Authorization`), driven by the shared `Using-Samples/SimpleAgent/` REPL.
- [`Hosted-McpTools/`](../Hosted-McpTools/) — contrasts client-side `McpClient` vs server-side `HostedMcpServerTool` for non-toolbox MCP servers.
+
+## Troubleshooting
+
+**`azd ai agent invoke` fails with `404 not_found: Conversation '' not found`**
+
+`azd` saves the session and conversation per agent and reuses them on the next invoke. Once the
+agent is redeployed, deleted, or restarted, that saved conversation no longer exists on the server,
+so every following invoke fails even though the agent itself is healthy. Start a fresh one:
+
+```
+azd ai agent invoke --new-conversation "Hello!"
+```
+
+Add `--new-session` as well if the failure persists.
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/README.md
index 76fe45866d8..4695dd937d1 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ToolboxMcpSkills/README.md
@@ -129,3 +129,17 @@ For end-to-end hosted agent deployment guidance, see the [official deployment gu
## NuGet package users
If you are consuming the Agent Framework as a NuGet package (not building from source), use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in `HostedToolboxMcpSkills.csproj` for the `PackageReference` alternative.
+
+## Troubleshooting
+
+**`azd ai agent invoke` fails with `404 not_found: Conversation '' not found`**
+
+`azd` saves the session and conversation per agent and reuses them on the next invoke. Once the
+agent is redeployed, deleted, or restarted, that saved conversation no longer exists on the server,
+so every following invoke fails even though the agent itself is healthy. Start a fresh one:
+
+```
+azd ai agent invoke --new-conversation "Hello!"
+```
+
+Add `--new-session` as well if the failure persists.
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/README.md
index 0d722aef7e1..ba34b3e573b 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Handoff/README.md
@@ -152,3 +152,17 @@ For end-to-end hosted agent deployment guidance, see the [official deployment gu
## NuGet package users
Use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in `HostedWorkflowHandoff.csproj` for the `PackageReference` alternative.
+
+## Troubleshooting
+
+**`azd ai agent invoke` fails with `404 not_found: Conversation '' not found`**
+
+`azd` saves the session and conversation per agent and reuses them on the next invoke. Once the
+agent is redeployed, deleted, or restarted, that saved conversation no longer exists on the server,
+so every following invoke fails even though the agent itself is healthy. Start a fresh one:
+
+```
+azd ai agent invoke --new-conversation "Hello!"
+```
+
+Add `--new-session` as well if the failure persists.
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/README.md
index 8facce34395..643e59f67a2 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Workflow-Simple/README.md
@@ -135,3 +135,17 @@ For end-to-end hosted agent deployment guidance, see the [official deployment gu
## NuGet package users
Use the standard `Dockerfile` instead of `Dockerfile.contributor`. See the commented section in `HostedWorkflowSimple.csproj` for the `PackageReference` alternative.
+
+## Troubleshooting
+
+**`azd ai agent invoke` fails with `404 not_found: Conversation '' not found`**
+
+`azd` saves the session and conversation per agent and reuses them on the next invoke. Once the
+agent is redeployed, deleted, or restarted, that saved conversation no longer exists on the server,
+so every following invoke fails even though the agent itself is healthy. Start a fresh one:
+
+```
+azd ai agent invoke --new-conversation "Hello!"
+```
+
+Add `--new-session` as well if the failure persists.
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/README.md
index 1ab2b63e3a6..c25d1221a4e 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/README.md
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/README.md
@@ -82,3 +82,17 @@ Goodbye!
```
The client only sent a chat prompt; the agent resolved its toolbox tools server-side and answered.
+
+## Troubleshooting
+
+**`azd ai agent invoke` fails with `404 not_found: Conversation '' not found`**
+
+`azd` saves the session and conversation per agent and reuses them on the next invoke. Once the
+agent is redeployed, deleted, or restarted, that saved conversation no longer exists on the server,
+so every following invoke fails even though the agent itself is healthy. Start a fresh one:
+
+```
+azd ai agent invoke --new-conversation "Hello!"
+```
+
+Add `--new-session` as well if the failure persists.
From 3ac87c30d2b122afa2cf04291c96eab22589d4cc Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Tue, 28 Jul 2026 20:55:25 +0100
Subject: [PATCH 21/23] Remove using directives already covered by global
usings
---
.../responses/Using-Samples/SimpleAgent/Program.cs | 1 -
.../ServiceCollectionExtensions.cs | 1 -
.../AspNetCoreUrlsEnvFixture.cs | 2 --
.../FoundryListenPortTests.cs | 1 -
4 files changed, 5 deletions(-)
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/Program.cs
index 00194c5406c..ee0c01cc689 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/Program.cs
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent/Program.cs
@@ -6,7 +6,6 @@
using Azure.Identity;
using DotNetEnv;
using Microsoft.Agents.AI;
-using Microsoft.Agents.AI.Foundry;
using OpenAI;
using OpenAI.Responses;
diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ServiceCollectionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ServiceCollectionExtensions.cs
index ec908e59c93..3da942f6662 100644
--- a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ServiceCollectionExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ServiceCollectionExtensions.cs
@@ -5,7 +5,6 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
-using Azure.AI.AgentServer.Core;
using Azure.AI.AgentServer.Responses;
using Azure.Core;
using Microsoft.AspNetCore.Builder;
diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/AspNetCoreUrlsEnvFixture.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/AspNetCoreUrlsEnvFixture.cs
index 15ba4df1277..56c34451945 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/AspNetCoreUrlsEnvFixture.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/AspNetCoreUrlsEnvFixture.cs
@@ -1,7 +1,5 @@
// Copyright (c) Microsoft. All rights reserved.
-using Xunit;
-
namespace Microsoft.Agents.AI.Foundry.Hosting.UnitTests;
///
diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryListenPortTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryListenPortTests.cs
index d5a641a7194..b0789593f3e 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryListenPortTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryListenPortTests.cs
@@ -6,7 +6,6 @@
using System.Linq;
using System.Net;
using System.Reflection;
-using Azure.AI.AgentServer.Core;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
From 15a7235e199db60981094684d7373fa597863332 Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Tue, 28 Jul 2026 21:27:49 +0100
Subject: [PATCH 22/23] Bind the Foundry listen port only inside a hosted
container
---
.../scripts/Add-LocalFrameworkFeed.ps1 | 13 ++--
.../ServiceCollectionExtensions.cs | 30 ++++++++--
.../AspNetCoreUrlsEnvFixture.cs | 14 -----
.../FoundryListenPortTests.cs | 59 ++++++++++++++++---
.../HostingEnvFixture.cs | 14 +++++
5 files changed, 98 insertions(+), 32 deletions(-)
delete mode 100644 dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/AspNetCoreUrlsEnvFixture.cs
create mode 100644 dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/HostingEnvFixture.cs
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1 b/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1
index 90885a2a050..4468960ab9d 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/scripts/Add-LocalFrameworkFeed.ps1
@@ -75,7 +75,7 @@ if (-not (Select-String -Path $projectFile.FullName -Pattern '(.+?)').Matches[0].Groups[1].Value
+$packagePropsPath = Join-Path $dotnetRoot 'nuget' 'nuget-package.props'
+$versionMatch = Select-String -Path $packagePropsPath -Pattern '(.+?)' | Select-Object -First 1
+if (-not $versionMatch) {
+ throw "Could not read from $packagePropsPath."
+}
+
+$versionPrefix = $versionMatch.Matches[0].Groups[1].Value
$version = "$versionPrefix-preview-local.$(Get-Date -Format 'yyyyMMddHHmmss')"
$feedPath = Join-Path $target 'local-feed'
@@ -96,7 +101,7 @@ Write-Host " version: $version"
Write-Host ''
foreach ($project in $frameworkProjects) {
- $projectPath = Join-Path $srcRoot "$project\$project.csproj"
+ $projectPath = Join-Path $srcRoot $project "$project.csproj"
Write-Host "Packing $project..."
# Debug, not Release: the Release configuration runs the repo's formatting and analyzer passes,
diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ServiceCollectionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ServiceCollectionExtensions.cs
index 3da942f6662..4e35ec1a155 100644
--- a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ServiceCollectionExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ServiceCollectionExtensions.cs
@@ -252,6 +252,12 @@ public static IEndpointRouteBuilder MapFoundryResponses(this IEndpointRouteBuild
return endpoints;
}
+ ///
+ /// Environment variable the Foundry hosting platform injects with a non-empty value inside a
+ /// hosted container. It is the documented way for container code to detect a Foundry context.
+ ///
+ internal const string FoundryHostingEnvironmentVariable = "FOUNDRY_HOSTING_ENVIRONMENT";
+
///
/// Marker registered once per so the Foundry listen-port
/// configuration is applied at most once, even across multiple AddFoundryResponses calls.
@@ -266,11 +272,17 @@ private sealed class FoundryListenPortMarker;
///
///
///
- /// The binding is unconditional, which is what makes source (ZIP) deploy work: the .NET base
- /// image sets ASPNETCORE_URLS to port 80, and a listener configured in code takes
- /// precedence over that setting (Kestrel logs "Overriding address(es)"). Skipping the binding
- /// whenever ASPNETCORE_URLS was set would therefore always leave the container on port
- /// 80 and fail the readiness probe with HTTP 424.
+ /// Applied only when reports a Foundry container,
+ /// which the platform signals with the FOUNDRY_HOSTING_ENVIRONMENT variable. A listener
+ /// configured in code overrides the addresses a host resolves from configuration, so applying
+ /// it everywhere would silently move any non-Foundry app off its configured address.
+ ///
+ ///
+ /// Inside a Foundry container the binding cannot be skipped based on ASPNETCORE_URLS:
+ /// the .NET base image always sets it to port 80, so such a guard would always trip and leave
+ /// the container failing the readiness probe with HTTP 424. It cannot key off the presence of
+ /// PORT either, because the platform sets that variable only when it needs a port other
+ /// than the default.
///
///
/// To listen on a different port, set PORT, the same knob the Agent Server SDK uses.
@@ -283,6 +295,14 @@ private sealed class FoundryListenPortMarker;
///
private static void ConfigureFoundryListenPort(IServiceCollection services)
{
+ // FoundryEnvironment.IsHosted exposes the same signal, but it caches every value in a
+ // static constructor, so read the variable directly to keep this decision observable at
+ // call time. Matches how FoundryToolboxService reads the other FOUNDRY_* variables.
+ if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable(FoundryHostingEnvironmentVariable)))
+ {
+ return;
+ }
+
if (services.Any(static d => d.ServiceType == typeof(FoundryListenPortMarker)))
{
return;
diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/AspNetCoreUrlsEnvFixture.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/AspNetCoreUrlsEnvFixture.cs
deleted file mode 100644
index 56c34451945..00000000000
--- a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/AspNetCoreUrlsEnvFixture.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-namespace Microsoft.Agents.AI.Foundry.Hosting.UnitTests;
-
-///
-/// xUnit collection that serializes tests mutating the ASPNETCORE_URLS process
-/// environment variable. Without this, parallel test execution causes flaky races between
-/// tests that set / unset the variable.
-///
-[CollectionDefinition(Name, DisableParallelization = true)]
-public sealed class AspNetCoreUrlsEnvFixture
-{
- public const string Name = "AspNetCoreUrlsEnv";
-}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryListenPortTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryListenPortTests.cs
index b0789593f3e..b0cd3752d2e 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryListenPortTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryListenPortTests.cs
@@ -17,17 +17,19 @@ namespace Microsoft.Agents.AI.Foundry.Hosting.UnitTests;
/// Verifies that AddFoundryResponses binds Kestrel to the Foundry hosted-runtime port
/// (, the PORT env var, default 8088) for a plain
/// WebApplication.CreateBuilder (Tier 3) host, so a source (ZIP) deployed agent passes the
-/// platform readiness probe with no Dockerfile pinning the port.
+/// platform readiness probe with no Dockerfile pinning the port, and that it leaves the addresses
+/// of a host running outside Foundry alone.
///
-[Collection(AspNetCoreUrlsEnvFixture.Name)]
+[Collection(HostingEnvFixture.Name)]
public sealed class FoundryListenPortTests
{
private const string AspNetCoreUrls = "ASPNETCORE_URLS";
[Fact]
- public void AddFoundryResponses_BindsFoundryPort()
+ public void AddFoundryResponses_WhenHosted_BindsFoundryPort()
{
// Arrange
+ using var hosted = new FoundryHostingScope();
var services = new ServiceCollection();
services.AddLogging();
@@ -39,9 +41,10 @@ public void AddFoundryResponses_BindsFoundryPort()
}
[Fact]
- public void AddFoundryResponses_WithAgent_BindsFoundryPort()
+ public void AddFoundryResponses_WithAgentWhenHosted_BindsFoundryPort()
{
// Arrange
+ using var hosted = new FoundryHostingScope();
var services = new ServiceCollection();
services.AddLogging();
var mockAgent = new Mock();
@@ -55,12 +58,29 @@ public void AddFoundryResponses_WithAgent_BindsFoundryPort()
}
[Fact]
- public void AddFoundryResponses_WithAspNetCoreUrlsSet_StillBindsFoundryPort()
+ public void AddFoundryResponses_WhenNotHosted_LeavesAddressesAlone()
+ {
+ // Arrange: outside a Foundry container the host keeps whatever addresses it resolved from
+ // configuration, so registering the Responses protocol must not add a listener.
+ using var notHosted = new FoundryHostingScope(hosted: false);
+ var services = new ServiceCollection();
+ services.AddLogging();
+
+ // Act
+ services.AddFoundryResponses();
+
+ // Assert
+ Assert.Empty(GetCodeBackedPorts(services));
+ }
+
+ [Fact]
+ public void AddFoundryResponses_WhenHostedWithAspNetCoreUrlsSet_StillBindsFoundryPort()
{
// Arrange: the .NET base image used by source (ZIP) deploy sets ASPNETCORE_URLS to port 80.
- // The binding must still be applied, because a listener configured in code takes precedence
- // over that setting. Skipping it here would leave the container on port 80 and fail every
- // invocation with HTTP 424 session_not_ready.
+ // Inside Foundry the binding must still be applied, because a listener configured in code
+ // takes precedence over that setting. Skipping it here would leave the container on port 80
+ // and fail every invocation with HTTP 424 session_not_ready.
+ using var hosted = new FoundryHostingScope();
var original = Environment.GetEnvironmentVariable(AspNetCoreUrls);
try
{
@@ -81,9 +101,10 @@ public void AddFoundryResponses_WithAspNetCoreUrlsSet_StillBindsFoundryPort()
}
[Fact]
- public void AddFoundryResponses_CalledTwice_BindsFoundryPortOnce()
+ public void AddFoundryResponses_CalledTwiceWhenHosted_BindsFoundryPortOnce()
{
// Arrange
+ using var hosted = new FoundryHostingScope();
var services = new ServiceCollection();
services.AddLogging();
@@ -122,4 +143,24 @@ private static List GetCodeBackedPorts(IServiceCollection services)
return ports;
}
+
+ ///
+ /// Sets the variable the Foundry platform injects into a hosted container for the duration of a
+ /// test, and restores the previous value on dispose.
+ ///
+ private sealed class FoundryHostingScope : IDisposable
+ {
+ private readonly string? _original;
+
+ public FoundryHostingScope(bool hosted = true)
+ {
+ this._original = Environment.GetEnvironmentVariable(FoundryHostingExtensions.FoundryHostingEnvironmentVariable);
+ Environment.SetEnvironmentVariable(
+ FoundryHostingExtensions.FoundryHostingEnvironmentVariable,
+ hosted ? "foundry" : null);
+ }
+
+ public void Dispose() =>
+ Environment.SetEnvironmentVariable(FoundryHostingExtensions.FoundryHostingEnvironmentVariable, this._original);
+ }
}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/HostingEnvFixture.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/HostingEnvFixture.cs
new file mode 100644
index 00000000000..efc19e52af6
--- /dev/null
+++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/HostingEnvFixture.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace Microsoft.Agents.AI.Foundry.Hosting.UnitTests;
+
+///
+/// xUnit collection that serializes tests mutating the ASPNETCORE_URLS and
+/// FOUNDRY_HOSTING_ENVIRONMENT process environment variables. Without this, parallel test
+/// execution causes flaky races between tests that set / unset those variables.
+///
+[CollectionDefinition(Name, DisableParallelization = true)]
+public sealed class HostingEnvFixture
+{
+ public const string Name = "HostingEnv";
+}
From 8b0dbd58d5cf1cd83cddebe00917151b498a434f Mon Sep 17 00:00:00 2001
From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com>
Date: Wed, 29 Jul 2026 10:36:29 +0100
Subject: [PATCH 23/23] Resolve the Foundry listen port from IConfiguration
---
.../.dockerignore | 2 +-
.../.env.example | 2 +-
.../Dockerfile | 2 +-
.../HostedChatClientAgentDocker.csproj | 2 +-
.../azure.yaml | 2 +-
.../Hosted-ChatClientAgent/.agentignore | 2 +-
.../HostedChatClientAgent.csproj | 2 +-
.../ServiceCollectionExtensions.cs | 82 +++++++---
.../FoundryListenPortTests.cs | 144 ++++++++++--------
.../HostingEnvFixture.cs | 14 --
10 files changed, 149 insertions(+), 105 deletions(-)
delete mode 100644 dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/HostingEnvFixture.cs
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/.dockerignore b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/.dockerignore
index f4d46f9d573..6caa228241c 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/.dockerignore
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/.dockerignore
@@ -17,4 +17,4 @@ obj/
.checkpoints/
# Note: local-feed/ and nuget.config are deliberately NOT excluded. When present (contributor mode)
-# the `dotnet restore` inside the image build resolves the Agent Framework from them.
\ No newline at end of file
+# the `dotnet restore` inside the image build resolves the Agent Framework from them.
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/.env.example b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/.env.example
index 83157b0c102..77c6b4cfa36 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/.env.example
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/.env.example
@@ -14,4 +14,4 @@ ASPNETCORE_URLS=http://+:8088
# on a machine with no managed identity DefaultAzureCredential hangs for a long time
# probing the IMDS endpoint (169.254.169.254) before every model call. Not set in
# Foundry, where the platform-injected managed identity is used.
-AZURE_TOKEN_CREDENTIALS=dev
\ No newline at end of file
+AZURE_TOKEN_CREDENTIALS=dev
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/Dockerfile
index bca49080f2b..e8681bb4e35 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/Dockerfile
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/Dockerfile
@@ -16,4 +16,4 @@ COPY --from=build /app/publish .
EXPOSE 8088
ENV ASPNETCORE_URLS=http://+:8088
-ENTRYPOINT ["dotnet", "HostedChatClientAgentDocker.dll"]
\ No newline at end of file
+ENTRYPOINT ["dotnet", "HostedChatClientAgentDocker.dll"]
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/HostedChatClientAgentDocker.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/HostedChatClientAgentDocker.csproj
index c044c0d2548..31a65fd0b7a 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/HostedChatClientAgentDocker.csproj
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/HostedChatClientAgentDocker.csproj
@@ -43,4 +43,4 @@
-
\ No newline at end of file
+
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/azure.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/azure.yaml
index 53ce81e2192..c0f45b87026 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/azure.yaml
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent-Dockerfile/azure.yaml
@@ -41,4 +41,4 @@ services:
name: hosted-chat-client-agent-docker
protocols:
- protocol: responses
- version: 2.0.0
\ No newline at end of file
+ version: 2.0.0
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/.agentignore b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/.agentignore
index f45144759ff..7f9f197c84f 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/.agentignore
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/.agentignore
@@ -27,4 +27,4 @@ obj/
# Contributor mode (scripts/Add-LocalFrameworkFeed.ps1) generates local-feed/ and nuget.config.
# Those are deliberately NOT excluded: the server-side restore needs them to resolve the Agent
-# Framework from the packages shipped in this upload instead of nuget.org.
\ No newline at end of file
+# Framework from the packages shipped in this upload instead of nuget.org.
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/HostedChatClientAgent.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/HostedChatClientAgent.csproj
index 9c1561bfee4..4fe1a8567bd 100644
--- a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/HostedChatClientAgent.csproj
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-ChatClientAgent/HostedChatClientAgent.csproj
@@ -44,4 +44,4 @@
-
\ No newline at end of file
+
diff --git a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ServiceCollectionExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ServiceCollectionExtensions.cs
index 4e35ec1a155..8bd86abee5e 100644
--- a/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ServiceCollectionExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ServiceCollectionExtensions.cs
@@ -3,6 +3,7 @@
using System;
using System.ClientModel.Primitives;
using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
using System.Linq;
using System.Runtime.CompilerServices;
using Azure.AI.AgentServer.Responses;
@@ -11,6 +12,7 @@
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.Extensions.AI;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Diagnostics.HealthChecks;
@@ -253,10 +255,21 @@ public static IEndpointRouteBuilder MapFoundryResponses(this IEndpointRouteBuild
}
///
- /// Environment variable the Foundry hosting platform injects with a non-empty value inside a
+ /// Configuration key the Foundry hosting platform populates with a non-empty value inside a
/// hosted container. It is the documented way for container code to detect a Foundry context.
///
- internal const string FoundryHostingEnvironmentVariable = "FOUNDRY_HOSTING_ENVIRONMENT";
+ internal const string FoundryHostingEnvironmentKey = "FOUNDRY_HOSTING_ENVIRONMENT";
+
+ ///
+ /// Configuration key holding the HTTP listen port, matching the Agent Server SDK.
+ ///
+ internal const string ListenPortKey = "PORT";
+
+ ///
+ /// Port the Foundry hosted runtime probes and routes to when is
+ /// not set, matching .
+ ///
+ internal const int DefaultListenPort = 8088;
///
/// Marker registered once per so the Foundry listen-port
@@ -267,27 +280,30 @@ private sealed class FoundryListenPortMarker;
///
/// Binds Kestrel to the port the Foundry hosted runtime probes and routes to, so a plain
/// WebApplication.CreateBuilder host (Tier 3) works with no Dockerfile. Mirrors
- /// AgentHostBuilder, which listens on (the
- /// PORT environment variable, default 8088).
+ /// AgentHostBuilder, which listens on the PORT value (default 8088).
///
///
///
- /// Applied only when reports a Foundry container,
- /// which the platform signals with the FOUNDRY_HOSTING_ENVIRONMENT variable. A listener
- /// configured in code overrides the addresses a host resolves from configuration, so applying
- /// it everywhere would silently move any non-Foundry app off its configured address.
+ /// The listener is added only when configuration reports a Foundry container through
+ /// . A listener configured in code overrides the
+ /// addresses a host resolves from configuration, so adding it everywhere would silently move
+ /// any non-Foundry app off its configured address.
+ ///
+ ///
+ /// Both values come from rather than from
+ /// , which caches every value in a static constructor. Reading
+ /// through configuration keeps the decision observable when the host is built, honours the
+ /// host's configuration sources, and lets tests supply values without mutating the process
+ /// environment.
///
///
- /// Inside a Foundry container the binding cannot be skipped based on ASPNETCORE_URLS:
+ /// Inside a Foundry container the listener cannot be skipped based on ASPNETCORE_URLS:
/// the .NET base image always sets it to port 80, so such a guard would always trip and leave
/// the container failing the readiness probe with HTTP 424. It cannot key off the presence of
- /// PORT either, because the platform sets that variable only when it needs a port other
+ /// PORT either, because the platform sets that value only when it needs a port other
/// than the default.
///
///
- /// To listen on a different port, set PORT, the same knob the Agent Server SDK uses.
- ///
- ///
/// Idempotent, and harmless when no Kestrel server is present (for example under
/// TestServer): the callback only runs when Kestrel
/// is resolved.
@@ -295,21 +311,45 @@ private sealed class FoundryListenPortMarker;
///
private static void ConfigureFoundryListenPort(IServiceCollection services)
{
- // FoundryEnvironment.IsHosted exposes the same signal, but it caches every value in a
- // static constructor, so read the variable directly to keep this decision observable at
- // call time. Matches how FoundryToolboxService reads the other FOUNDRY_* variables.
- if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable(FoundryHostingEnvironmentVariable)))
+ if (services.Any(static d => d.ServiceType == typeof(FoundryListenPortMarker)))
{
return;
}
- if (services.Any(static d => d.ServiceType == typeof(FoundryListenPortMarker)))
+ services.AddSingleton();
+ services.AddOptions()
+ .Configure(static (options, configuration) =>
+ {
+ if (string.IsNullOrEmpty(configuration[FoundryHostingEnvironmentKey]))
+ {
+ return;
+ }
+
+ options.ListenAnyIP(ResolveListenPort(configuration));
+ });
+ }
+
+ ///
+ /// Reads the listen port from configuration, applying the same contract as
+ /// : when unset, otherwise
+ /// a port number in the range 1-65535.
+ ///
+ /// The configured value is not a valid port.
+ private static int ResolveListenPort(IConfiguration configuration)
+ {
+ var value = configuration[ListenPortKey];
+ if (string.IsNullOrEmpty(value))
{
- return;
+ return DefaultListenPort;
}
- services.AddSingleton();
- services.Configure(static options => options.ListenAnyIP(FoundryEnvironment.Port));
+ if (!int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var port) || port is < 1 or > 65535)
+ {
+ throw new InvalidOperationException(
+ $"The {ListenPortKey} environment variable value '{value}' is not a valid port number (1-65535).");
+ }
+
+ return port;
}
///
diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryListenPortTests.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryListenPortTests.cs
index b0cd3752d2e..5ea783b29eb 100644
--- a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryListenPortTests.cs
+++ b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryListenPortTests.cs
@@ -3,10 +3,10 @@
using System;
using System.Collections;
using System.Collections.Generic;
-using System.Linq;
using System.Net;
using System.Reflection;
using Microsoft.AspNetCore.Server.Kestrel.Core;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Moq;
@@ -14,39 +14,37 @@
namespace Microsoft.Agents.AI.Foundry.Hosting.UnitTests;
///
-/// Verifies that AddFoundryResponses binds Kestrel to the Foundry hosted-runtime port
-/// (, the PORT env var, default 8088) for a plain
-/// WebApplication.CreateBuilder (Tier 3) host, so a source (ZIP) deployed agent passes the
-/// platform readiness probe with no Dockerfile pinning the port, and that it leaves the addresses
-/// of a host running outside Foundry alone.
+/// Verifies that AddFoundryResponses adds a Kestrel listener on the Foundry hosted-runtime
+/// port for a plain WebApplication.CreateBuilder (Tier 3) host, so a source (ZIP) deployed
+/// agent passes the platform readiness probe with no Dockerfile pinning the port, and that it
+/// leaves the addresses of a host running outside Foundry alone.
///
-[Collection(HostingEnvFixture.Name)]
+///
+/// Every case supplies its values through an in-memory , so no test
+/// mutates the process environment and the class stays safe to run in parallel.
+///
public sealed class FoundryListenPortTests
{
- private const string AspNetCoreUrls = "ASPNETCORE_URLS";
+ private const string AspNetCoreUrlsKey = "ASPNETCORE_URLS";
[Fact]
- public void AddFoundryResponses_WhenHosted_BindsFoundryPort()
+ public void AddFoundryResponses_WhenHosted_ListensOnFoundryPort()
{
// Arrange
- using var hosted = new FoundryHostingScope();
- var services = new ServiceCollection();
- services.AddLogging();
+ var services = CreateServices();
// Act
services.AddFoundryResponses();
// Assert
- Assert.Contains(FoundryEnvironment.Port, GetCodeBackedPorts(services));
+ Assert.Equal([FoundryHostingExtensions.DefaultListenPort], GetCodeBackedPorts(services));
}
[Fact]
- public void AddFoundryResponses_WithAgentWhenHosted_BindsFoundryPort()
+ public void AddFoundryResponses_WithAgentWhenHosted_ListensOnFoundryPort()
{
// Arrange
- using var hosted = new FoundryHostingScope();
- var services = new ServiceCollection();
- services.AddLogging();
+ var services = CreateServices();
var mockAgent = new Mock();
mockAgent.SetupGet(a => a.Name).Returns("test-agent");
@@ -54,7 +52,7 @@ public void AddFoundryResponses_WithAgentWhenHosted_BindsFoundryPort()
services.AddFoundryResponses(mockAgent.Object);
// Assert
- Assert.Contains(FoundryEnvironment.Port, GetCodeBackedPorts(services));
+ Assert.Equal([FoundryHostingExtensions.DefaultListenPort], GetCodeBackedPorts(services));
}
[Fact]
@@ -62,9 +60,7 @@ public void AddFoundryResponses_WhenNotHosted_LeavesAddressesAlone()
{
// Arrange: outside a Foundry container the host keeps whatever addresses it resolved from
// configuration, so registering the Responses protocol must not add a listener.
- using var notHosted = new FoundryHostingScope(hosted: false);
- var services = new ServiceCollection();
- services.AddLogging();
+ var services = CreateServices(hosted: false);
// Act
services.AddFoundryResponses();
@@ -74,47 +70,89 @@ public void AddFoundryResponses_WhenNotHosted_LeavesAddressesAlone()
}
[Fact]
- public void AddFoundryResponses_WhenHostedWithAspNetCoreUrlsSet_StillBindsFoundryPort()
+ public void AddFoundryResponses_WhenHostedWithAspNetCoreUrlsSet_StillListensOnFoundryPort()
{
// Arrange: the .NET base image used by source (ZIP) deploy sets ASPNETCORE_URLS to port 80.
- // Inside Foundry the binding must still be applied, because a listener configured in code
+ // Inside Foundry the listener must still be added, because a listener configured in code
// takes precedence over that setting. Skipping it here would leave the container on port 80
// and fail every invocation with HTTP 424 session_not_ready.
- using var hosted = new FoundryHostingScope();
- var original = Environment.GetEnvironmentVariable(AspNetCoreUrls);
- try
+ var services = CreateServices(settings: new Dictionary
{
- Environment.SetEnvironmentVariable(AspNetCoreUrls, "http://+:80");
- var services = new ServiceCollection();
- services.AddLogging();
+ [AspNetCoreUrlsKey] = "http://+:80",
+ });
- // Act
- services.AddFoundryResponses();
+ // Act
+ services.AddFoundryResponses();
- // Assert
- Assert.Contains(FoundryEnvironment.Port, GetCodeBackedPorts(services));
- }
- finally
+ // Assert
+ Assert.Equal([FoundryHostingExtensions.DefaultListenPort], GetCodeBackedPorts(services));
+ }
+
+ [Fact]
+ public void AddFoundryResponses_WhenHostedWithPortSet_ListensOnConfiguredPort()
+ {
+ // Arrange: the platform sets PORT only when it needs a port other than the default.
+ var services = CreateServices(settings: new Dictionary
{
- Environment.SetEnvironmentVariable(AspNetCoreUrls, original);
- }
+ [FoundryHostingExtensions.ListenPortKey] = "9099",
+ });
+
+ // Act
+ services.AddFoundryResponses();
+
+ // Assert
+ Assert.Equal([9099], GetCodeBackedPorts(services));
+ }
+
+ [Theory]
+ [InlineData("0")]
+ [InlineData("65536")]
+ [InlineData("not-a-port")]
+ public void AddFoundryResponses_WhenHostedWithInvalidPort_Throws(string port)
+ {
+ // Arrange
+ var services = CreateServices(settings: new Dictionary
+ {
+ [FoundryHostingExtensions.ListenPortKey] = port,
+ });
+ services.AddFoundryResponses();
+
+ // Act & Assert
+ var exception = Assert.Throws(() => GetCodeBackedPorts(services));
+ Assert.Contains(port, exception.Message, StringComparison.Ordinal);
}
[Fact]
- public void AddFoundryResponses_CalledTwiceWhenHosted_BindsFoundryPortOnce()
+ public void AddFoundryResponses_CalledTwiceWhenHosted_ListensOnFoundryPortOnce()
{
// Arrange
- using var hosted = new FoundryHostingScope();
- var services = new ServiceCollection();
- services.AddLogging();
+ var services = CreateServices();
// Act
services.AddFoundryResponses();
services.AddFoundryResponses();
// Assert: a duplicate ListenAnyIP on the same port fails Kestrel startup with
- // "address already in use", so the binding must be applied exactly once.
- Assert.Equal(1, GetCodeBackedPorts(services).Count(port => port == FoundryEnvironment.Port));
+ // "address already in use", so the listener must be added exactly once.
+ Assert.Equal([FoundryHostingExtensions.DefaultListenPort], GetCodeBackedPorts(services));
+ }
+
+ ///
+ /// Builds a service collection whose carries the supplied values,
+ /// marking the process as Foundry-hosted unless says otherwise.
+ ///
+ private static ServiceCollection CreateServices(bool hosted = true, Dictionary? settings = null)
+ {
+ settings ??= [];
+ if (hosted)
+ {
+ settings[FoundryHostingExtensions.FoundryHostingEnvironmentKey] = "foundry";
+ }
+
+ var services = new ServiceCollection();
+ services.AddLogging();
+ services.AddSingleton(new ConfigurationBuilder().AddInMemoryCollection(settings).Build());
+ return services;
}
///
@@ -143,24 +181,4 @@ private static List GetCodeBackedPorts(IServiceCollection services)
return ports;
}
-
- ///
- /// Sets the variable the Foundry platform injects into a hosted container for the duration of a
- /// test, and restores the previous value on dispose.
- ///
- private sealed class FoundryHostingScope : IDisposable
- {
- private readonly string? _original;
-
- public FoundryHostingScope(bool hosted = true)
- {
- this._original = Environment.GetEnvironmentVariable(FoundryHostingExtensions.FoundryHostingEnvironmentVariable);
- Environment.SetEnvironmentVariable(
- FoundryHostingExtensions.FoundryHostingEnvironmentVariable,
- hosted ? "foundry" : null);
- }
-
- public void Dispose() =>
- Environment.SetEnvironmentVariable(FoundryHostingExtensions.FoundryHostingEnvironmentVariable, this._original);
- }
}
diff --git a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/HostingEnvFixture.cs b/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/HostingEnvFixture.cs
deleted file mode 100644
index efc19e52af6..00000000000
--- a/dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/HostingEnvFixture.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-namespace Microsoft.Agents.AI.Foundry.Hosting.UnitTests;
-
-///
-/// xUnit collection that serializes tests mutating the ASPNETCORE_URLS and
-/// FOUNDRY_HOSTING_ENVIRONMENT process environment variables. Without this, parallel test
-/// execution causes flaky races between tests that set / unset those variables.
-///
-[CollectionDefinition(Name, DisableParallelization = true)]
-public sealed class HostingEnvFixture
-{
- public const string Name = "HostingEnv";
-}