diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index 01c28beef8..d58a691599 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -313,6 +313,9 @@
+
+
+
@@ -332,6 +335,7 @@
+
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/.dockerignore b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/.dockerignore
new file mode 100644
index 0000000000..cf85b06faa
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/.dockerignore
@@ -0,0 +1,6 @@
+**/bin
+**/obj
+**/.vs
+**/.vscode
+.env
+*.user
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/.env.example b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/.env.example
new file mode 100644
index 0000000000..b8fe9e8e7a
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/.env.example
@@ -0,0 +1,5 @@
+AZURE_AI_PROJECT_ENDPOINT=
+ASPNETCORE_URLS=http://+:8088
+ASPNETCORE_ENVIRONMENT=Development
+AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
+AZURE_BEARER_TOKEN=DefaultAzureCredential
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/Dockerfile b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/Dockerfile
new file mode 100644
index 0000000000..82f5e1b85c
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/Dockerfile
@@ -0,0 +1,17 @@
+# 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", "HostedFiles.dll"]
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/Dockerfile.contributor b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/Dockerfile.contributor
new file mode 100644
index 0000000000..7a34f9361d
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/Dockerfile.contributor
@@ -0,0 +1,19 @@
+# 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-files .
+# docker run --rm -p 8088:8088 -e AGENT_NAME=hosted-files -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN --env-file .env hosted-files
+#
+# 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", "HostedFiles.dll"]
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/HostedFiles.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/HostedFiles.csproj
new file mode 100644
index 0000000000..fe36ca8ba2
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/HostedFiles.csproj
@@ -0,0 +1,40 @@
+
+
+
+ net10.0
+ enable
+ enable
+ false
+ HostedFiles
+ HostedFiles
+ $(NoWarn);
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/Program.cs
new file mode 100644
index 0000000000..aba8f4ebef
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/Program.cs
@@ -0,0 +1,223 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// Hosted Files Agent - A hosted agent that exposes two distinct file knowledge sources
+// through scoped, security-hardened tools:
+//
+// * Bundled files (image-baked) — files copied into the published output via the csproj
+// rule. Live at /app/resources/ inside the container.
+// Author-shipped knowledge that ships with every session.
+//
+// * Session files (per-session $HOME volume) — files uploaded at runtime via the alpha
+// Azure.AI.Projects.AgentSessionFiles SDK. Live at $HOME inside the per-session
+// container, which the platform sets to /home/session by default
+// (container-image-spec.md line 127, "If you use the session files API, $HOME is
+// also the base path for those operations").
+//
+// Each source is exposed via a separate tool pair, each rooted at its own directory.
+// Tools take a fileName, not a path: Path.GetFileName strips any directory components,
+// then a canonicalize + StartsWith(root) check enforces the boundary. The model cannot
+// be tricked into reading /etc/passwd or any path outside its tool's root, even via
+// indirect prompt injection in an uploaded file.
+//
+// Required environment variables:
+// AZURE_AI_PROJECT_ENDPOINT - Azure AI Foundry project endpoint
+// AZURE_AI_MODEL_DEPLOYMENT_NAME - Model deployment name (default: gpt-4o)
+//
+// Optional:
+// AGENT_NAME - Agent name (default: hosted-files)
+// BUNDLED_FILES_DIR - Override the bundled-files root
+// (default: /resources, i.e. /app/resources/)
+// HOME - Standard env var; the per-session sandbox volume
+// (default: /home/session in the platform-managed container)
+
+using System.ComponentModel;
+using Azure.AI.Projects;
+using Azure.Core;
+using Azure.Identity;
+using DotNetEnv;
+using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.Foundry.Hosting;
+using Microsoft.Extensions.AI;
+
+// Load .env file if present (for local development)
+Env.TraversePath().Load();
+
+// Bypass SampleEnvironment alias (which prompts on missing env vars) for optional values.
+string? GetOptionalEnv(string key) => System.Environment.GetEnvironmentVariable(key);
+
+string endpoint = Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT")
+ ?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set.");
+string deploymentName = GetOptionalEnv("AZURE_AI_MODEL_DEPLOYMENT_NAME") ?? "gpt-4o";
+
+// 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 in production).
+TokenCredential credential = new ChainedTokenCredential(
+ new DevTemporaryTokenCredential(),
+ new DefaultAzureCredential());
+
+// ── File roots (canonicalized once) ──────────────────────────────────────────
+
+// Bundled root: where csproj lands at runtime.
+// In the container that resolves to /app/resources/.
+string bundledRoot = Path.GetFullPath(
+ GetOptionalEnv("BUNDLED_FILES_DIR")
+ ?? Path.Combine(AppContext.BaseDirectory, "resources"));
+
+// Session root: the per-session $HOME volume mounted by the Foundry platform.
+// Files uploaded via AgentSessionFiles.UploadSessionFileAsync(sessionStoragePath: "foo")
+// land at $HOME/foo per container-image-spec.md line 172.
+string sessionRoot = Path.GetFullPath(
+ GetOptionalEnv("HOME")
+ ?? "/home/session");
+
+// ── Tools: bundled files (image-baked, /app/resources/) ──────────────────────
+
+[Description("List the names of files bundled with the agent (built-in knowledge that ships with the image).")]
+string ListBundledFiles() => SafeListNames(bundledRoot);
+
+[Description("Read the full text contents of a bundled file by name. Bundled files are built-in knowledge shipped with the agent image.")]
+string ReadBundledFile(
+ [Description("Name of the bundled file (no directory components). Must be one of the names returned by ListBundledFiles.")] string fileName)
+ => SafeRead(bundledRoot, fileName, scope: "bundled files");
+
+// ── Tools: session files (per-session $HOME) ─────────────────────────────────
+
+[Description("List the names of files uploaded into the current session sandbox by the user (e.g., via AgentSessionFiles.UploadSessionFileAsync).")]
+string ListSessionFiles() => SafeListNames(sessionRoot);
+
+[Description("Read the full text contents of a file uploaded into the current session by name. Session files are user-supplied data that lives only for the lifetime of this session.")]
+string ReadSessionFile(
+ [Description("Name of the session file (no directory components). Must be one of the names returned by ListSessionFiles.")] string fileName)
+ => SafeRead(sessionRoot, fileName, scope: "session files");
+
+// ── Path-safe helpers (defense-in-depth: GetFileName + canonicalize + StartsWith(root)) ──
+
+string SafeListNames(string root)
+{
+ try
+ {
+ if (!Directory.Exists(root))
+ {
+ return string.Empty;
+ }
+
+ return string.Join(
+ Environment.NewLine,
+ Directory.EnumerateFiles(root).Select(Path.GetFileName));
+ }
+ catch (Exception ex)
+ {
+ return $"Error listing files: {ex.Message}";
+ }
+}
+
+string SafeRead(string root, string fileName, string scope)
+{
+ try
+ {
+ // Step 1: strip any directory components the model might have included.
+ string safeName = Path.GetFileName(fileName);
+ if (string.IsNullOrEmpty(safeName))
+ {
+ return $"File '{fileName}' not found in {scope}.";
+ }
+
+ // Step 2: combine with the root and canonicalize.
+ string fullPath = Path.GetFullPath(Path.Combine(root, safeName));
+
+ // Step 3: enforce the prefix boundary so a crafted name still cannot escape.
+ string rootPrefix = root.EndsWith(Path.DirectorySeparatorChar)
+ ? root
+ : root + Path.DirectorySeparatorChar;
+ if (!fullPath.StartsWith(rootPrefix, StringComparison.Ordinal))
+ {
+ return $"File '{fileName}' not found in {scope}.";
+ }
+
+ return File.Exists(fullPath)
+ ? File.ReadAllText(fullPath)
+ : $"File '{fileName}' not found in {scope}.";
+ }
+ catch (Exception ex)
+ {
+ return $"Error reading '{fileName}': {ex.Message}";
+ }
+}
+
+// ── Create and host the agent ────────────────────────────────────────────────
+
+AIAgent agent = new AIProjectClient(new Uri(endpoint), credential)
+ .AsAIAgent(
+ model: deploymentName,
+ instructions: """
+ You are a friendly assistant that answers questions over two file sources:
+
+ - Bundled files: built-in knowledge that ships with the agent image
+ (e.g., reference reports the author packaged with you). Tools:
+ ListBundledFiles, ReadBundledFile.
+
+ - Session files: user-uploaded data for this session only (e.g., a CSV
+ the user wants you to analyse). Tools: ListSessionFiles, ReadSessionFile.
+
+ Pick the tool pair by intent. If a name could match either source, list
+ both first. Always read the file before answering; do not guess. Quote
+ numbers and figures verbatim from the file.
+ """,
+ name: GetOptionalEnv("AGENT_NAME") ?? "hosted-files",
+ description: "Hosted agent that answers questions over bundled (image-baked) and session-uploaded files via two scoped tool pairs.",
+ tools:
+ [
+ AIFunctionFactory.Create(ListBundledFiles),
+ AIFunctionFactory.Create(ReadBundledFile),
+ AIFunctionFactory.Create(ListSessionFiles),
+ AIFunctionFactory.Create(ReadSessionFile),
+ ]);
+
+var builder = WebApplication.CreateBuilder(args);
+builder.Services.AddFoundryResponses(agent);
+
+var app = builder.Build();
+app.MapFoundryResponses();
+
+if (app.Environment.IsDevelopment())
+{
+ app.MapFoundryResponses("openai/v1");
+}
+
+app.Run();
+
+///
+/// A for local Docker debugging only.
+/// Reads a pre-fetched bearer token from the AZURE_BEARER_TOKEN environment variable
+/// once at startup. This should NOT be used in production.
+///
+/// Generate a token on your host and pass it to the container:
+/// export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv)
+/// docker run -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN ...
+///
+internal sealed class DevTemporaryTokenCredential : TokenCredential
+{
+ private const string EnvironmentVariable = "AZURE_BEARER_TOKEN";
+ private readonly string? _token;
+
+ public DevTemporaryTokenCredential()
+ {
+ this._token = System.Environment.GetEnvironmentVariable(EnvironmentVariable);
+ }
+
+ public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
+ => this.GetAccessToken();
+
+ public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
+ => new(this.GetAccessToken());
+
+ private AccessToken GetAccessToken()
+ {
+ if (string.IsNullOrEmpty(this._token) || this._token == "DefaultAzureCredential")
+ {
+ throw new CredentialUnavailableException($"{EnvironmentVariable} environment variable is not set.");
+ }
+
+ return new AccessToken(this._token, DateTimeOffset.UtcNow.AddHours(1));
+ }
+}
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/README.md
new file mode 100644
index 0000000000..729aca5c5f
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/README.md
@@ -0,0 +1,128 @@
+# Hosted-Files
+
+A hosted agent that demonstrates **two distinct file knowledge sources** through scoped, security-hardened tools:
+
+- **Bundled files** (image-baked) — files the author packages with the agent at build time. Live at `/app/resources/` inside the container, copied from this project's [`resources/`](./resources/) folder via the csproj `` rule.
+- **Session files** (per-session `$HOME` volume) — files the user uploads at runtime via the alpha `Azure.AI.Projects.AgentSessionFiles` SDK. Live at `$HOME` inside the per-session container. The Foundry platform sets `HOME=/home/session` by default and roots the session-files API there per [`container-image-spec.md` line 172](https://github.com/microsoft/foundrysdk-specs/blob/main/specs/agents/hosted_agents/container-spec/docs/container-image-spec.md): *"If you use the session files API, `$HOME` is also the base path for those operations; any paths given in those API endpoints will be relative to `$HOME`."*
+
+## Tool surface
+
+Each source is exposed via its own tool pair, rooted at its own directory. The model picks by intent.
+
+| Tool | Source | Root |
+|------|--------|------|
+| `ListBundledFiles` | Bundled (image-baked) | `/app/resources/` |
+| `ReadBundledFile` | Bundled (image-baked) | `/app/resources/` |
+| `ListSessionFiles` | Session-uploaded | `$HOME` (`/home/session`) |
+| `ReadSessionFile` | Session-uploaded | `$HOME` (`/home/session`) |
+
+## Security model — distinct tools, distinct sandboxes
+
+Each tool takes a `fileName` (no directory components allowed) and enforces three layers of defence inside the implementation:
+
+1. **`Path.GetFileName(input)`** strips any directory parts from the model-supplied name. `"../../etc/passwd"` becomes `"passwd"`.
+2. **`Path.GetFullPath(Combine(root, name))`** canonicalises the path.
+3. **`fullPath.StartsWith(root + DirectorySeparatorChar)`** rejects anything that resolves outside the tool's root.
+
+Failures return a controlled `"File '' not found in ."` rather than throwing or exposing the canonical path.
+
+This is why the agent has four narrowly-scoped tools instead of a single `ReadFile(path)`:
+
+- **Smaller per-tool attack surface.** Each tool has one purpose, one root, and no path-typed parameter. Even a buggy implementation can only leak its own directory.
+- **Cross-boundary access is impossible by schema.** A prompt-injection attempt to make the bundled tool read a session path (or vice versa) does not even compile in the tool schema the model sees.
+- **Read-only, non-recursive listing.** No write tools, no glob, no `..`.
+
+## Companion
+
+[`Using-Samples/SessionFilesClient`](../Using-Samples/SessionFilesClient/) — a thin chat REPL (same shape as [`SimpleAgent`](../Using-Samples/SimpleAgent/)) that points at the deployed Hosted-Files endpoint via `FoundryAgent` and lets you ask questions whose answers come from either file source.
+
+## Live proof of the session-files contract
+
+The end-to-end alpha-SDK round trip (client uploads via `AgentSessionFiles.UploadSessionFileAsync` → file arrives at `$HOME/` inside the per-session container → agent's `ReadSessionFile` tool reads it → response quotes the verbatim contents) is exercised live by [`SessionFilesHostedAgentTests.UploadedFile_IsReadByHostedAgentAsync`](../../../../../tests/Foundry.Hosting.IntegrationTests/SessionFilesHostedAgentTests.cs) against the matching `session-files` scenario in the integration test container.
+
+## Prerequisites
+
+- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
+- An Azure AI Foundry project with a deployed model (e.g., `gpt-4o`)
+- Azure CLI logged in (`az login`)
+
+## Configuration
+
+Copy the template and fill in your project endpoint:
+
+```bash
+cp .env.example .env
+```
+
+Edit `.env`:
+
+```env
+AZURE_AI_PROJECT_ENDPOINT=https://.services.ai.azure.com/api/projects/
+ASPNETCORE_URLS=http://+:8088
+ASPNETCORE_ENVIRONMENT=Development
+AZURE_AI_MODEL_DEPLOYMENT_NAME=gpt-4o
+```
+
+> `.env` is gitignored. The `.env.example` template is checked in as a reference.
+
+## Running directly (contributors)
+
+```bash
+cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files
+AGENT_NAME=hosted-files dotnet run
+```
+
+The agent starts on `http://localhost:8088`.
+
+## Try it from the SessionFilesClient REPL
+
+### Bundled files (works against any deployment, including local)
+
+```bash
+cd ../Using-Samples/SessionFilesClient
+$env:AGENT_ENDPOINT = "http://localhost:8088"
+$env:AGENT_NAME = "hosted-files"
+dotnet run
+
+You> What is the total revenue in the contoso file?
+Agent> The contoso file reports total revenue of "$1,482.6M".
+```
+
+The agent calls `ListBundledFiles`, sees `contoso_q1_2026_report.txt`, calls `ReadBundledFile("contoso_q1_2026_report.txt")` (which resolves under `/app/resources/`), and quotes the figure verbatim.
+
+### Session files (against a deployed agent)
+
+Upload a file to a specific session via `azd ai agent files upload` or via the alpha `AgentSessionFiles` SDK (see the integration test for the SDK call), then ask the agent about it. The agent's `ReadSessionFile` tool reads from `$HOME` and surfaces the content the same way.
+
+## Running with Docker
+
+This project uses `ProjectReference`, so use `Dockerfile.contributor` which takes a pre-published output:
+
+```bash
+dotnet publish -c Debug -f net10.0 -r linux-musl-x64 --self-contained false -o out
+docker build -f Dockerfile.contributor -t hosted-files .
+
+export AZURE_BEARER_TOKEN=$(az account get-access-token --resource https://ai.azure.com --query accessToken -o tsv)
+docker run --rm -p 8088:8088 \
+ -e AGENT_NAME=hosted-files \
+ -e AZURE_BEARER_TOKEN=$AZURE_BEARER_TOKEN \
+ --env-file .env \
+ hosted-files
+```
+
+The bundled `resources/` folder is part of the published output and ships inside the image.
+
+## NuGet package users
+
+If consuming the Agent Framework as a NuGet package, use the standard `Dockerfile` instead of `Dockerfile.contributor` and switch the `ProjectReference` entries in `HostedFiles.csproj` to `PackageReference` (commented section in the csproj).
+
+## Adding more bundled files
+
+Drop additional text files into [`resources/`](./resources/). The csproj `` rule picks them up on the next `dotnet build` / `docker build`.
+
+## Overrides
+
+| Env var | Purpose | Default |
+|---------|---------|---------|
+| `BUNDLED_FILES_DIR` | Override the bundled-files root the tools read from. | `/resources` (`/app/resources/` in container) |
+| `HOME` | The per-session sandbox volume root the session-files tools read from. Set by the Foundry platform; can be overridden for local testing. | `/home/session` |
\ No newline at end of file
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/agent.manifest.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/agent.manifest.yaml
new file mode 100644
index 0000000000..cda1ba6494
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/agent.manifest.yaml
@@ -0,0 +1,30 @@
+# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml
+name: hosted-files
+displayName: "Hosted Files Agent"
+
+description: >
+ A hosted agent that answers questions over a small set of files bundled
+ with its container image (under /app/resources/). Two local C# function
+ tools (ListFiles, ReadFile) surface the bundled file contents to the model.
+
+metadata:
+ tags:
+ - AI Agent Hosting
+ - Azure AI AgentServer
+ - Responses Protocol
+ - Bundled Files
+ - Local Tools
+ - Agent Framework
+
+template:
+ name: hosted-files
+ kind: hosted
+ protocols:
+ - protocol: responses
+ version: 1.0.0
+ resources:
+ cpu: "0.25"
+ memory: 0.5Gi
+parameters:
+ properties: []
+resources: []
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/agent.yaml b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/agent.yaml
new file mode 100644
index 0000000000..f949ac09ee
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/agent.yaml
@@ -0,0 +1,9 @@
+# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml
+kind: hosted
+name: hosted-files
+protocols:
+ - protocol: responses
+ version: 1.0.0
+resources:
+ cpu: "0.25"
+ memory: 0.5Gi
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/resources/contoso_q1_2026_report.txt b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/resources/contoso_q1_2026_report.txt
new file mode 100644
index 0000000000..858192a7d3
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/resources/contoso_q1_2026_report.txt
@@ -0,0 +1,121 @@
+Contoso Corporation
+Quarterly Report — Q1 2026 (Three months ended March 31, 2026)
+
+DISCLAIMER
+This document contains fictional data for sample/demo purposes only.
+Contoso is a fictional company; all figures below are fabricated.
+
+------------------------------------------------------------
+1. EXECUTIVE SUMMARY
+------------------------------------------------------------
+Contoso delivered a solid first quarter, with total revenue of
+$1,482.6M, up 11.4% year-over-year. Growth was led by the Cloud
+Services segment (+22.7% YoY) and continued double-digit expansion
+in International markets. Operating margin expanded 140 basis points
+to 23.8% on disciplined cost management and improved gross margin.
+
+Key highlights:
+ - Revenue: $1,482.6M (YoY +11.4%)
+ - Gross profit: $912.0M (gross margin 61.5%)
+ - Operating income: $352.9M (operating margin 23.8%)
+ - Net income: $268.4M (net margin 18.1%)
+ - Diluted EPS: $1.27 (vs. $1.04 prior year)
+ - Free cash flow: $311.5M
+ - Cash & equivalents: $2,140.8M
+
+------------------------------------------------------------
+2. INCOME STATEMENT (USD millions, unaudited)
+------------------------------------------------------------
+ Q1 2026 Q1 2025 YoY %
+Revenue 1,482.6 1,330.7 +11.4%
+Cost of revenue 570.6 538.9 +5.9%
+Gross profit 912.0 791.8 +15.2%
+ Gross margin 61.5% 59.5% +200 bps
+Operating expenses
+ Research & development 241.4 220.5 +9.5%
+ Sales & marketing 218.7 205.1 +6.6%
+ General & administrative 99.0 88.6 +11.7%
+Total operating expenses 559.1 514.2 +8.7%
+Operating income 352.9 277.6 +27.1%
+ Operating margin 23.8% 20.9% +290 bps
+Other income / (expense), net 8.4 5.1
+Income before taxes 361.3 282.7
+Provision for income taxes 92.9 72.6
+Net income 268.4 210.1 +27.7%
+Diluted EPS (USD) 1.27 1.04 +22.1%
+
+------------------------------------------------------------
+3. REVENUE BY SEGMENT (USD millions)
+------------------------------------------------------------
+Segment Q1 2026 Q1 2025 YoY %
+Cloud Services 612.4 499.1 +22.7%
+Productivity Software 448.9 422.6 +6.2%
+Devices & Hardware 267.0 260.4 +2.5%
+Professional Services 154.3 148.6 +3.8%
+Total revenue 1,482.6 1,330.7 +11.4%
+
+------------------------------------------------------------
+4. REVENUE BY GEOGRAPHY (USD millions)
+------------------------------------------------------------
+Region Q1 2026 Q1 2025 YoY %
+North America 812.1 756.0 +7.4%
+EMEA 388.5 340.2 +14.2%
+Asia-Pacific 221.7 183.4 +20.9%
+Latin America 60.3 51.1 +18.0%
+Total revenue 1,482.6 1,330.7 +11.4%
+
+------------------------------------------------------------
+5. SELECTED BALANCE SHEET ITEMS (USD millions)
+------------------------------------------------------------
+ Mar 31, Dec 31,
+ 2026 2025
+Cash & equivalents 2,140.8 1,902.3
+Short-term investments 845.6 820.4
+Accounts receivable, net 1,012.7 988.5
+Total current assets 4,510.2 4,190.6
+Goodwill & intangibles 2,330.1 2,338.9
+Total assets 9,884.5 9,512.0
+Total current liabilities 2,118.4 2,054.7
+Long-term debt 1,750.0 1,750.0
+Total liabilities 4,402.6 4,310.5
+Total stockholders' equity 5,481.9 5,201.5
+
+------------------------------------------------------------
+6. CASH FLOW HIGHLIGHTS (USD millions)
+------------------------------------------------------------
+ Q1 2026 Q1 2025
+Net cash from operating activities 382.0 298.7
+Capital expenditures (70.5) (62.1)
+Free cash flow 311.5 236.6
+Share repurchases (120.0) (90.0)
+Dividends paid (54.2) (48.6)
+
+------------------------------------------------------------
+7. KEY OPERATING METRICS
+------------------------------------------------------------
+Cloud paid seats (millions) 48.6 39.7 +22.4%
+Cloud net revenue retention 118% 114%
+Active enterprise customers 18,420 16,905 +9.0%
+Headcount (end of period) 22,140 20,610 +7.4%
+
+------------------------------------------------------------
+8. OUTLOOK — Q2 2026 GUIDANCE
+------------------------------------------------------------
+Revenue: $1,520M – $1,560M (YoY +10% to +13%)
+Operating margin: 23.5% – 24.5%
+Diluted EPS: $1.30 – $1.36
+Capital expenditures: ~$80M
+
+Management remains confident in the full-year plan and reiterates
+fiscal-year 2026 revenue growth of 10–12% and operating-margin
+expansion of 100–150 basis points versus FY 2025.
+
+------------------------------------------------------------
+9. NOTES
+------------------------------------------------------------
+- All figures are unaudited and rounded to one decimal place.
+- Year-over-year comparisons are versus the same period in 2025.
+- "Free cash flow" is defined as net cash from operating activities
+ less capital expenditures, and is a non-GAAP measure.
+- This sample report is intended solely for demonstration of an
+ agent-driven document analysis pipeline.
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SessionFilesClient/Program.cs b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SessionFilesClient/Program.cs
new file mode 100644
index 0000000000..e05e025c0d
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SessionFilesClient/Program.cs
@@ -0,0 +1,116 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.ClientModel.Primitives;
+using Azure.AI.Extensions.OpenAI;
+using Azure.AI.Projects;
+using Azure.Identity;
+using DotNetEnv;
+using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.Foundry;
+
+// Load .env file if present (for local development)
+Env.TraversePath().Load();
+
+Uri agentEndpoint = new(Environment.GetEnvironmentVariable("AGENT_ENDPOINT")
+ ?? "http://localhost:8088");
+
+var agentName = Environment.GetEnvironmentVariable("AGENT_NAME")
+ ?? throw new InvalidOperationException("AGENT_NAME is not set.");
+
+// ── Create an agent-framework agent backed by the remote Hosted-Files agent ──
+
+var options = new AIProjectClientOptions();
+
+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.
+
+ agentEndpoint = new UriBuilder(agentEndpoint) { Scheme = "https" }.Uri;
+ options.AddPolicy(new HttpSchemeRewritePolicy(), PipelinePosition.BeforeTransport);
+}
+
+var aiProjectClient = new AIProjectClient(agentEndpoint, new AzureCliCredential(), options);
+FoundryAgent agent = aiProjectClient.AsAIAgent(new AgentReference(agentName));
+
+AgentSession session = await agent.CreateSessionAsync();
+
+// ── REPL ──────────────────────────────────────────────────────────────────────
+
+Console.ForegroundColor = ConsoleColor.Cyan;
+Console.WriteLine($"""
+ ══════════════════════════════════════════════════════════
+ Session Files Client
+ Connected to: {agentEndpoint}
+ Try: "Give me the total revenue in the contoso file."
+ Type a message or 'quit' to exit
+ ══════════════════════════════════════════════════════════
+ """);
+Console.ResetColor();
+Console.WriteLine();
+
+while (true)
+{
+ Console.ForegroundColor = ConsoleColor.Green;
+ Console.Write("You> ");
+ Console.ResetColor();
+
+ string? input = Console.ReadLine();
+
+ if (string.IsNullOrWhiteSpace(input)) { continue; }
+ if (input.Equals("quit", StringComparison.OrdinalIgnoreCase)) { break; }
+
+ try
+ {
+ Console.ForegroundColor = ConsoleColor.Yellow;
+ Console.Write("Agent> ");
+ Console.ResetColor();
+
+ await foreach (var update in agent.RunStreamingAsync(input, session))
+ {
+ Console.Write(update);
+ }
+
+ Console.WriteLine();
+ }
+ catch (Exception ex)
+ {
+ Console.ForegroundColor = ConsoleColor.Red;
+ Console.WriteLine($"Error: {ex.Message}");
+ Console.ResetColor();
+ }
+
+ Console.WriteLine();
+}
+
+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
+{
+ 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 = "http" }.Uri;
+ }
+ }
+}
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SessionFilesClient/README.md b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SessionFilesClient/README.md
new file mode 100644
index 0000000000..dbf9262ae8
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SessionFilesClient/README.md
@@ -0,0 +1,50 @@
+# SessionFilesClient
+
+A thin chat REPL that connects to a deployed [`Hosted-Files`](../../Hosted-Files/) agent via `FoundryAgent` and lets you ask questions whose answers come from the files bundled with that agent. Same shape as [`SimpleAgent`](../SimpleAgent/) — point it at an `AGENT_ENDPOINT`, build a `FoundryAgent`, run.
+
+The agent's container-side `ListFiles` and `ReadFile` tools surface the bundled file contents to the model. The client knows nothing about files; that is entirely the agent's concern.
+
+## Prerequisites
+
+- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
+- A running [`Hosted-Files`](../../Hosted-Files/) agent (locally via `dotnet run` or deployed to Foundry)
+- Azure CLI logged in (`az login`)
+
+## Configuration
+
+```env
+AGENT_ENDPOINT=http://localhost:8088
+AGENT_NAME=hosted-files
+```
+
+`AGENT_ENDPOINT` defaults to `http://localhost:8088`. Override with the deployed agent endpoint when chatting against Foundry.
+
+## Run
+
+```bash
+cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SessionFilesClient
+$env:AGENT_ENDPOINT = "http://localhost:8088"
+$env:AGENT_NAME = "hosted-files"
+dotnet run
+```
+
+## End-to-end demo
+
+With the [`Hosted-Files`](../../Hosted-Files/) agent running:
+
+```text
+══════════════════════════════════════════════════════════
+Session Files Client
+Connected to: http://localhost:8088/
+Try: "Give me the total revenue in the contoso file."
+Type a message or 'quit' to exit
+══════════════════════════════════════════════════════════
+
+You> Give me the total revenue in the contoso file.
+Agent> The contoso file reports total revenue of "$1,482.6M".
+
+You> quit
+Goodbye!
+```
+
+The agent looked at its bundled files via `ListFiles`, picked `contoso_q1_2026_report.txt`, called `ReadFile`, and quoted the figure verbatim. The client only sent a chat prompt.
diff --git a/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SessionFilesClient/SessionFilesClient.csproj b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SessionFilesClient/SessionFilesClient.csproj
new file mode 100644
index 0000000000..954036ba3b
--- /dev/null
+++ b/dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SessionFilesClient/SessionFilesClient.csproj
@@ -0,0 +1,24 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ false
+ SessionFilesClient
+ session-files-client
+ $(NoWarn);NU1605;OPENAI001
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/tests/Foundry.Hosting.IntegrationTests.TestContainer/Program.cs b/dotnet/tests/Foundry.Hosting.IntegrationTests.TestContainer/Program.cs
index 3ec4665672..052786890b 100644
--- a/dotnet/tests/Foundry.Hosting.IntegrationTests.TestContainer/Program.cs
+++ b/dotnet/tests/Foundry.Hosting.IntegrationTests.TestContainer/Program.cs
@@ -32,6 +32,7 @@
"toolbox" => CreateToolboxAgent(projectClient, deployment),
"mcp-toolbox" => CreateMcpToolboxAgent(projectClient, deployment),
"custom-storage" => CreateCustomStorageAgent(projectClient, deployment),
+ "session-files" => CreateSessionFilesAgent(projectClient, deployment),
_ => throw new InvalidOperationException($"Unknown IT_SCENARIO '{scenario}'.")
};
@@ -106,6 +107,30 @@ static AIAgent CreateCustomStorageAgent(AIProjectClient client, string deploymen
name: "custom-storage-agent",
description: "Custom storage test agent (placeholder).");
+// session-files scenario: agent reads files from $HOME inside the per-session sandbox volume.
+// Mirrors the dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files sample.
+static AIAgent CreateSessionFilesAgent(AIProjectClient client, string deployment) =>
+ client.AsAIAgent(
+ model: deployment,
+ instructions: """
+ You are a friendly assistant that helps users inspect and summarise
+ files stored in the session sandbox at $HOME.
+
+ Always answer file-related questions by calling the available tools
+ (GetHomeDirectory, ListFiles, ReadFile). Do not guess file paths or
+ contents — read the file before answering.
+
+ Quote numbers and figures verbatim from the file rather than
+ paraphrasing them.
+ """,
+ name: "session-files-agent",
+ description: "Reads files from the per-session $HOME volume.",
+ tools: [
+ AIFunctionFactory.Create(GetHomeDirectory),
+ AIFunctionFactory.Create(ListFiles),
+ AIFunctionFactory.Create(ReadFile)
+ ]);
+
[Description("Returns the current UTC date and time as an ISO 8601 string.")]
static string GetUtcNow() => DateTime.UtcNow.ToString("o");
@@ -120,3 +145,74 @@ static string SendEmail(
[Description("Returns the deployment environment name.")]
static string GetEnvironmentName() => "integration-test";
+
+// session-files tools: resolve paths against $HOME (the per-session sandbox volume).
+[Description("Get the absolute path of the session home directory ($HOME).")]
+static string GetHomeDirectory() => SessionHome();
+
+[Description("List files and directories under the given path inside the session sandbox. Pass an empty string to list $HOME.")]
+static string[] ListFiles(
+ [Description("Path relative to $HOME. Absolute paths and traversals (..) are rejected.")] string path)
+{
+ try
+ {
+ return Directory.EnumerateFileSystemEntries(ResolveSessionPath(path)).ToArray();
+ }
+ catch (Exception ex)
+ {
+ return [$"Error listing '{path}': {ex.Message}"];
+ }
+}
+
+[Description("Read the full text contents of a file inside the session sandbox.")]
+static string ReadFile(
+ [Description("Path relative to $HOME. Absolute paths and traversals (..) are rejected.")] string path)
+{
+ try
+ {
+ return File.ReadAllText(ResolveSessionPath(path));
+ }
+ catch (Exception ex)
+ {
+ return $"Error reading '{path}': {ex.Message}";
+ }
+}
+
+static string SessionHome() =>
+ Environment.GetEnvironmentVariable("HOME")
+ ?? Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
+
+// Resolve a caller-supplied path against $HOME, rejecting absolute paths and traversal segments
+// so that the model cannot read or list arbitrary container files via the ReadFile/ListFiles
+// tools (defense-in-depth against indirect prompt injection). Mirrors the canonicalize +
+// startsWith($HOME) pattern used by FileSystemAgentFileStore.ResolveSafePath.
+static string ResolveSessionPath(string path)
+{
+ string home = SessionHome();
+ string homeFull = Path.GetFullPath(home);
+ string homePrefix = homeFull.EndsWith(Path.DirectorySeparatorChar)
+ ? homeFull
+ : homeFull + Path.DirectorySeparatorChar;
+
+ if (string.IsNullOrWhiteSpace(path))
+ {
+ return homeFull;
+ }
+
+ if (Path.IsPathRooted(path))
+ {
+ throw new ArgumentException($"Absolute paths are not allowed: '{path}'.", nameof(path));
+ }
+
+ string combined = Path.Combine(homeFull, path);
+ string fullPath = Path.GetFullPath(combined);
+
+ if (!fullPath.Equals(homeFull, StringComparison.Ordinal) &&
+ !fullPath.StartsWith(homePrefix, StringComparison.Ordinal))
+ {
+ throw new ArgumentException(
+ $"Path '{path}' resolves outside the session sandbox.", nameof(path));
+ }
+
+ return fullPath;
+}
diff --git a/dotnet/tests/Foundry.Hosting.IntegrationTests/Fixtures/SessionFilesHostedAgentFixture.cs b/dotnet/tests/Foundry.Hosting.IntegrationTests/Fixtures/SessionFilesHostedAgentFixture.cs
new file mode 100644
index 0000000000..1470a23fba
--- /dev/null
+++ b/dotnet/tests/Foundry.Hosting.IntegrationTests/Fixtures/SessionFilesHostedAgentFixture.cs
@@ -0,0 +1,17 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace Foundry.Hosting.IntegrationTests.Fixtures;
+
+///
+/// Provisions a hosted agent that runs the test container in IT_SCENARIO=session-files mode.
+/// The container exposes three local function tools (GetHomeDirectory, ListFiles,
+/// ReadFile) that read from the per-session $HOME sandbox volume — mirroring the
+/// Hosted-Files sample. Tests use the alpha
+/// API to upload a file into the session
+/// sandbox, then invoke the agent (pinned to the same agent_session_id) and assert that the
+/// agent's tools observed the uploaded file.
+///
+public sealed class SessionFilesHostedAgentFixture : HostedAgentFixture
+{
+ protected override string ScenarioName => "session-files";
+}
diff --git a/dotnet/tests/Foundry.Hosting.IntegrationTests/Foundry.Hosting.IntegrationTests.csproj b/dotnet/tests/Foundry.Hosting.IntegrationTests/Foundry.Hosting.IntegrationTests.csproj
index 18710dc791..a8ad919198 100644
--- a/dotnet/tests/Foundry.Hosting.IntegrationTests/Foundry.Hosting.IntegrationTests.csproj
+++ b/dotnet/tests/Foundry.Hosting.IntegrationTests/Foundry.Hosting.IntegrationTests.csproj
@@ -20,8 +20,16 @@
+
+
+
+
+
+
diff --git a/dotnet/tests/Foundry.Hosting.IntegrationTests/README.md b/dotnet/tests/Foundry.Hosting.IntegrationTests/README.md
index 764a366289..91f2f6983d 100644
--- a/dotnet/tests/Foundry.Hosting.IntegrationTests/README.md
+++ b/dotnet/tests/Foundry.Hosting.IntegrationTests/README.md
@@ -138,6 +138,7 @@ human-only operation; CI only adds and deletes versions under existing agents.
| `ToolboxHostedAgentFixture` | `toolbox` | `it-toolbox` | Server registered toolbox tool callable; client side additions visible (placeholder). |
| `McpToolboxHostedAgentFixture` | `mcp-toolbox` | `it-mcp-toolbox` | MCP backed tool invocation against `https://learn.microsoft.com/api/mcp` (placeholder). |
| `CustomStorageHostedAgentFixture` | `custom-storage` | `it-custom-storage` | Round trip with custom `IResponsesStorageProvider`; multi turn reads from the custom store (placeholder). |
+| `SessionFilesHostedAgentFixture` | `session-files` | `it-session-files` | End-to-end: upload via `AgentSessionFiles` (alpha) into a pinned `agent_session_id`, invoke the agent, assert it reads the file via the container's `ReadFile` tool. |
The placeholder scenarios will be wired up in the test container `Program.cs` once the
relevant `Microsoft.Agents.AI.Foundry.Hosting` API surfaces stabilize.
diff --git a/dotnet/tests/Foundry.Hosting.IntegrationTests/SessionFilesHostedAgentTests.cs b/dotnet/tests/Foundry.Hosting.IntegrationTests/SessionFilesHostedAgentTests.cs
new file mode 100644
index 0000000000..7c1407ac65
--- /dev/null
+++ b/dotnet/tests/Foundry.Hosting.IntegrationTests/SessionFilesHostedAgentTests.cs
@@ -0,0 +1,238 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+#pragma warning disable AAIP001 // AgentSessionFiles is experimental
+#pragma warning disable OPENAI001 // CreateResponseOptions is experimental
+
+using System;
+using System.ClientModel;
+using System.ClientModel.Primitives;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using AgentConformance.IntegrationTests.Support;
+using Azure.AI.Extensions.OpenAI;
+using Azure.AI.Projects;
+using Azure.AI.Projects.Agents;
+using Foundry.Hosting.IntegrationTests.Fixtures;
+using Microsoft.Agents.AI;
+using Microsoft.Extensions.AI;
+using OpenAI.Responses;
+using Shared.IntegrationTests;
+
+namespace Foundry.Hosting.IntegrationTests;
+
+///
+/// End-to-end integration test for the Hosted-Files style scenario: a file uploaded by the client
+/// via the alpha SDK is read by the deployed hosted agent's
+/// container-side ReadFile tool and surfaces in .
+///
+///
+///
+/// Routing both invocations to the same per-session container requires two clients on the same
+/// agent-scoped : a to
+/// pre-create a conversation bound to the agent endpoint, and a
+/// for invocation. The session id resolved by the platform on the first call is captured from the
+/// x-agent-session-id response header and used to target the
+/// upload at the same session's $HOME. The second call
+/// carries the same conversation_id so it lands in the same container and the agent's
+/// ReadFile tool sees the upload.
+///
+///
+[Trait("Category", "FoundryHostedAgents")]
+public sealed class SessionFilesHostedAgentTests(SessionFilesHostedAgentFixture fixture) : IClassFixture
+{
+ private const string FoundryFeaturesHeader = "Foundry-Features";
+ private const string HostedAgentsFeatureValue = "HostedAgents=V1Preview,AgentEndpoints=V1Preview";
+ private const string SessionIdHeader = "x-agent-session-id";
+
+ private const string TestDataFileName = "contoso_q1_2026_report.txt";
+
+ /// Token that appears verbatim in the test data file. Proof the agent read what we uploaded.
+ private const string ExpectedTokenInFile = "1,482.6";
+
+ private readonly SessionFilesHostedAgentFixture _fixture = fixture;
+
+ [Fact]
+ public async Task UploadedFile_IsReadByHostedAgentAsync()
+ {
+ // Arrange
+ string localPath = Path.Combine(AppContext.BaseDirectory, "TestData", TestDataFileName);
+ Assert.True(
+ File.Exists(localPath),
+ $"Test data file not found at '{localPath}'. Confirm the linked Content entry in the csproj.");
+
+ var endpoint = new Uri(TestConfiguration.GetRequiredValue(TestSettings.AzureAIProjectEndpoint));
+ var credential = TestAzureCliCredentials.CreateAzureCliCredential();
+
+ // Admin client + AgentSessionFiles for upload/list/delete (alpha SDK).
+ var adminOptions = new AgentAdministrationClientOptions();
+ adminOptions.AddPolicy(new FoundryFeaturesPolicy(HostedAgentsFeatureValue), PipelinePosition.PerCall);
+ var adminClient = new AgentAdministrationClient(endpoint, credential, adminOptions);
+ var sessionFiles = adminClient.GetAgentSessionFiles();
+
+ // Build the per-agent OpenAI client. The conversation is created on this client so it is
+ // bound to the agent endpoint URL (`/agents/{name}/endpoint/protocols/openai/conversations`).
+ // A header-capture policy reads the `x-agent-session-id` the platform stamps on every reply.
+ var headerCapture = new ResponseHeaderCapturePolicy(SessionIdHeader);
+ var openAIOptions = new ProjectOpenAIClientOptions { AgentName = this._fixture.AgentName };
+ openAIOptions.AddPolicy(new FoundryFeaturesPolicy(HostedAgentsFeatureValue), PipelinePosition.PerCall);
+ openAIOptions.AddPolicy(headerCapture, PipelinePosition.PerCall);
+ var openAIClient = new ProjectOpenAIClient(endpoint, credential, openAIOptions);
+ var conversations = openAIClient.GetProjectConversationsClient();
+ var responses = openAIClient.GetProjectResponsesClient();
+
+ // Step 1 — create a conversation bound to the agent endpoint. Subsequent /responses calls
+ // tagged with this conversation_id route to the same per-session container.
+ var conversation = await conversations.CreateProjectConversationAsync();
+ string conversationId = conversation.Value.Id;
+
+ try
+ {
+ // Step 2 — warm-up call. Provisions the per-session container under the conversation and
+ // lets us read back the resolved agent_session_id from the response header.
+ var agent = responses.AsIChatClient().AsAIAgent(name: this._fixture.AgentName);
+ var convOptions = new ChatClientAgentRunOptions(new ChatOptions { ConversationId = conversationId });
+
+ var warmup = await agent.RunAsync(
+ "Reply with the single word 'ready' and nothing else.",
+ options: convOptions);
+ Assert.False(string.IsNullOrWhiteSpace(warmup.Text));
+
+ string agentSessionId = headerCapture.LastValue
+ ?? throw new InvalidOperationException(
+ $"Expected '{SessionIdHeader}' response header on warm-up but got none.");
+
+ try
+ {
+ // Step 3 — upload the file via the alpha AgentSessionFiles SDK to that exact session's $HOME.
+ SessionFileWriteResponse writeResponse = await sessionFiles.UploadSessionFileAsync(
+ agentName: this._fixture.AgentName,
+ sessionId: agentSessionId,
+ sessionStoragePath: TestDataFileName,
+ localPath: localPath);
+
+ long expectedBytes = new FileInfo(localPath).Length;
+ Assert.Equal(expectedBytes, writeResponse.BytesWritten);
+
+ SessionDirectoryListResponse listing = await sessionFiles.GetSessionFilesAsync(
+ agentName: this._fixture.AgentName,
+ sessionId: agentSessionId,
+ sessionStoragePath: ".");
+ Assert.Contains(
+ listing.Entries,
+ e => e.Name == TestDataFileName && !e.IsDirectory && e.Size == expectedBytes);
+
+ // Step 4 — invoke the agent again on the SAME conversation. The platform routes back to
+ // the same agent_session_id container, so the agent's ReadFile tool sees the upload.
+ // The platform mutates session/conversation revision when AgentSessionFiles uploads land,
+ // so an immediate /responses follow-up races and 400's with "modified concurrently. Please
+ // retry." — the response message literally tells us to retry. Bounded retry handles it.
+ var readOptions = new CreateResponseOptions { AgentConversationId = conversationId };
+ readOptions.InputItems.Add(ResponseItem.CreateUserMessageItem(
+ $"Read {TestDataFileName} from $HOME and quote the headline total revenue figure verbatim, no commentary."));
+
+ ClientResult rawResponse = null!;
+ const int MaxAttempts = 5;
+ for (int attempt = 1; attempt <= MaxAttempts; attempt++)
+ {
+ try
+ {
+ rawResponse = await responses.CreateResponseAsync(readOptions);
+ break;
+ }
+ catch (ClientResultException ex) when (
+ ex.Status == 400 &&
+ ex.Message.Contains("modified concurrently", StringComparison.OrdinalIgnoreCase) &&
+ attempt < MaxAttempts)
+ {
+ await Task.Delay(TimeSpan.FromSeconds(2 * attempt));
+ }
+ }
+
+ string responseText = rawResponse.Value.GetOutputText() ?? string.Empty;
+
+ Assert.Equal(agentSessionId, headerCapture.LastValue);
+
+ // Assert: the response contains the deterministic token from the file.
+ Assert.False(string.IsNullOrWhiteSpace(responseText));
+ Assert.Contains(ExpectedTokenInFile, responseText);
+ }
+ finally
+ {
+ // Best-effort cleanup of the uploaded file. The session itself is left for TTL expiry —
+ // the platform owns its lifecycle (no isolation key in our hands).
+ try
+ {
+ await sessionFiles.DeleteSessionFileAsync(
+ agentName: this._fixture.AgentName,
+ sessionId: agentSessionId,
+ path: TestDataFileName);
+ }
+ catch
+ {
+ // Ignore.
+ }
+ }
+ }
+ finally
+ {
+ await this._fixture.DeleteConversationAsync(conversationId);
+ }
+ }
+
+ ///
+ /// Captures a response header value on every pipeline call. Latest value is read after the
+ /// response completes. Used to grab the platform's x-agent-session-id stamp.
+ ///
+ private sealed class ResponseHeaderCapturePolicy(string headerName) : PipelinePolicy
+ {
+ private readonly string _headerName = headerName;
+ private string? _lastValue;
+
+ public string? LastValue => Volatile.Read(ref this._lastValue);
+
+ public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex)
+ {
+ ProcessNext(message, pipeline, currentIndex);
+ this.Capture(message);
+ }
+
+ public override async ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex)
+ {
+ await ProcessNextAsync(message, pipeline, currentIndex).ConfigureAwait(false);
+ this.Capture(message);
+ }
+
+ private void Capture(PipelineMessage message)
+ {
+ if (message.Response is not null &&
+ message.Response.Headers.TryGetValue(this._headerName, out var value) &&
+ !string.IsNullOrEmpty(value))
+ {
+ Volatile.Write(ref this._lastValue, value);
+ }
+ }
+ }
+
+ private sealed class FoundryFeaturesPolicy(string features) : PipelinePolicy
+ {
+ public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex)
+ {
+ this.SetHeader(message);
+ ProcessNext(message, pipeline, currentIndex);
+ }
+
+ public override async ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex)
+ {
+ this.SetHeader(message);
+ await ProcessNextAsync(message, pipeline, currentIndex).ConfigureAwait(false);
+ }
+
+ private void SetHeader(PipelineMessage message)
+ {
+ message.Request.Headers.Remove(FoundryFeaturesHeader);
+ message.Request.Headers.Add(FoundryFeaturesHeader, features);
+ }
+ }
+}
diff --git a/dotnet/tests/Foundry.Hosting.IntegrationTests/scripts/it-bootstrap-agents.ps1 b/dotnet/tests/Foundry.Hosting.IntegrationTests/scripts/it-bootstrap-agents.ps1
index c683a2a4e3..1896a8407c 100644
--- a/dotnet/tests/Foundry.Hosting.IntegrationTests/scripts/it-bootstrap-agents.ps1
+++ b/dotnet/tests/Foundry.Hosting.IntegrationTests/scripts/it-bootstrap-agents.ps1
@@ -38,7 +38,8 @@ $Scenarios = @(
'tool-calling-approval',
'toolbox',
'mcp-toolbox',
- 'custom-storage'
+ 'custom-storage',
+ 'session-files'
)
# Resolve project ARM scope from the endpoint.