.NET: Hosted-Files sample + AgentSessionFiles SDK companion + integration test [WIP]#5698
.NET: Hosted-Files sample + AgentSessionFiles SDK companion + integration test [WIP]#5698rogerbarreto wants to merge 8 commits intomicrosoft:mainfrom
Conversation
… + integration test Closes microsoft#5691 - Hosted-Files server sample (mirrors python 06_files): 3 local tools reading the per-session \C:\Users\rbarreto sandbox volume. - SessionFilesClient REPL companion: code-first equivalent of �zd ai agent files upload using the alpha Azure.AI.Projects.AgentSessionFiles SDK (upload/ls/download/rm + session lifecycle with isolation key). - session-files scenario added to the Foundry.Hosting.IntegrationTests multi-scenario harness (PR microsoft#5598): SessionFilesHostedAgentFixture + SessionFilesHostedAgentTests.UploadAndAgentReadsFileAsync, end-to-end validating upload then agent-reads-file (agent_session_id pinned via CreateResponseOptions.Patch). Bundled testdata is linked from the sample so there is a single source of truth.
There was a problem hiding this comment.
Pull request overview
Adds a .NET “Hosted-Files” hosted-agent sample (mirroring Python 06_files), a code-first REPL companion using the alpha Azure.AI.Projects.Agents.AgentSessionFiles API, and a new end-to-end session-files scenario in the Foundry.Hosting integration test harness.
Changes:
- Introduces a Hosted-Files sample agent with local function tools that read from the per-session
$HOMEsandbox volume. - Adds
SessionFilesClient, a console REPL to upload/list/download/delete per-session sandbox files viaAgentSessionFiles. - Extends
Foundry.Hosting.IntegrationTestswith a newsession-filesscenario (fixture, container branch, and E2E test), plus bootstrap script + docs updates.
Reviewed changes
Copilot reviewed 20 out of 20 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| dotnet/tests/Foundry.Hosting.IntegrationTests/SessionFilesHostedAgentTests.cs | New E2E test that creates a session, uploads a file via AgentSessionFiles, pins agent_session_id, and asserts the agent reads the file. |
| dotnet/tests/Foundry.Hosting.IntegrationTests/Fixtures/SessionFilesHostedAgentFixture.cs | New fixture selecting IT_SCENARIO=session-files. |
| dotnet/tests/Foundry.Hosting.IntegrationTests/Foundry.Hosting.IntegrationTests.csproj | Adds Azure.AI.Projects override and links shared test data from the sample. |
| dotnet/tests/Foundry.Hosting.IntegrationTests/README.md | Documents the new session-files scenario. |
| dotnet/tests/Foundry.Hosting.IntegrationTests/scripts/it-bootstrap-agents.ps1 | Adds session-files to the stable agent bootstrap list. |
| dotnet/tests/Foundry.Hosting.IntegrationTests.TestContainer/Program.cs | Wires a new session-files agent branch and exposes file tools (GetHomeDirectory, ListFiles, ReadFile). |
| dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/Program.cs | New hosted agent sample implementing the file tools against $HOME. |
| dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/README.md | Sample documentation including how to upload files and invoke the agent. |
| dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/HostedFiles.csproj | New web sample project configuration and dependencies. |
| dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/resources/contoso_q1_2026_report.txt | Shared deterministic sample test data. |
| dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/Dockerfile | Standard container build for end-users (NuGet-based scenario). |
| dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/Dockerfile.contributor | Contributor-oriented container workflow (pre-published output). |
| dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/.dockerignore | Docker context hygiene for the sample. |
| dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/agent.yaml | Hosted agent deployment descriptor. |
| dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/agent.manifest.yaml | Agent manifest metadata for discovery/gallery use. |
| dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/.env.example | Local/dev environment variable template. |
| dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SessionFilesClient/Program.cs | New REPL implementing upload/ls/download/rm against a specific session. |
| dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SessionFilesClient/README.md | REPL usage and CLI parity documentation. |
| dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SessionFilesClient/SessionFilesClient.csproj | New console sample project configuration and dependencies. |
| dotnet/agent-framework-dotnet.slnx | Registers the two new sample projects in the solution. |
| SessionFileWriteResponse writeResponse = await sessionFiles.UploadSessionFileAsync( | ||
| agentName: this._fixture.AgentName, | ||
| sessionId: session.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: session.AgentSessionId, | ||
| sessionStoragePath: "."); | ||
|
|
||
| Assert.Contains( | ||
| listing.Entries, |
| var deadline = DateTimeOffset.UtcNow + TimeSpan.FromMinutes(2); | ||
| ProjectAgentSession session = (await client.GetSessionAsync(agentName, sessionId)).Value; | ||
| while (session.Status != AgentSessionStatus.Active && session.Status != AgentSessionStatus.Failed) | ||
| { | ||
| if (DateTimeOffset.UtcNow > deadline) | ||
| { | ||
| throw new TimeoutException( | ||
| $"Session '{sessionId}' did not become Active within 120s. Last status: {session.Status}."); | ||
| } | ||
|
|
||
| await Task.Delay(TimeSpan.FromMilliseconds(500)); | ||
| session = (await client.GetSessionAsync(agentName, sessionId)).Value; | ||
| } |
| Console.WriteLine($"Created session: {session.AgentSessionId} (waiting for Active state...)"); | ||
|
|
||
| while (session.Status != AgentSessionStatus.Active && session.Status != AgentSessionStatus.Failed) | ||
| { | ||
| await Task.Delay(TimeSpan.FromMilliseconds(500)); | ||
| session = await agentsClient.GetSessionAsync(agentVersion.Name, session.AgentSessionId); | ||
| } | ||
|
|
| if (latest is null || string.CompareOrdinal(version.Version, latest.Version) > 0) | ||
| { | ||
| latest = version; | ||
| } |
| { | ||
| message.Request.Headers.Add(FeatureHeader, this._feature); | ||
| ProcessNext(message, pipeline, currentIndex); | ||
| } | ||
|
|
||
| public override async ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList<PipelinePolicy> pipeline, int currentIndex) | ||
| { |
|
|
||
| ```bash | ||
| cd ../Using-Samples/SessionFilesClient | ||
| AGENT_NAME=hosted-files AGENT_ENDPOINT=http://localhost:8088 dotnet run |
| string ResolveSessionPath(string path) | ||
| { | ||
| if (string.IsNullOrWhiteSpace(path)) | ||
| { | ||
| return Home(); | ||
| } | ||
|
|
||
| return Path.IsPathRooted(path) ? path : Path.Combine(Home(), path); | ||
| } |
| static string ResolveSessionPath(string path) => | ||
| string.IsNullOrWhiteSpace(path) | ||
| ? SessionHome() | ||
| : Path.IsPathRooted(path) ? path : Path.Combine(SessionHome(), path); |
There was a problem hiding this comment.
Automated Code Review
Reviewers: 4 | Confidence: 87%
✓ Correctness
The PR adds a well-structured session-files sample, companion REPL, and integration test that correctly follows established patterns in the codebase. The test properly uses the HostedAgentFixture infrastructure, ChatClientAgentRunOptions for injecting agent_session_id via JSON patch, and the standard Azure SDK ClientResult implicit conversion pattern. No correctness bugs found in the logic, error handling, or API usage.
✓ Security Reliability
The PR is well-structured with proper resource cleanup and timeout handling. The primary security concern is that
ResolveSessionPathin both the sample and test container accepts absolute paths and..traversals without validation, unlike the framework's ownFileSystemAgentFileStore.ResolveSafePathwhich canonicalizes and prefix-checks. In the intended deployment (isolated per-session container), this is mitigated by isolation. However, when run locally withdotnet run(as the README instructs for development), the LLM's tool calls can read arbitrary files from the developer's machine — exploitable via indirect prompt injection through uploaded file content.
✓ Test Coverage
The test coverage for this PR is solid. The integration test
UploadAndAgentReadsFileAsyncexercises the complete end-to-end flow: session creation, file upload with byte-count verification, file listing with multi-field assertion, agent invocation with session pinning via JsonPatch, and content verification using a deterministic token from the test data file. Assertions are meaningful (not just 'no exception' checks), cleanup is handled properly in finally blocks, and the test infrastructure reuse (HostedAgentFixture base class, TestConfiguration, TestAzureCliCredentials) is consistent with existing patterns. The test container correctly registers the three file tools only with the session-files scenario agent. No blocking issues found.
✗ Design Approach
I found one design-level issue. The new SessionFilesClient sample reimplements “latest deployed version” selection by lexicographically comparing version strings, which can silently choose the wrong agent version once version numbers reach double digits. The rest of the hosted-files sample and integration-test wiring lines up with the repo’s existing hosted-agent patterns.
Flagged Issues
- dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SessionFilesClient/Program.cs:263 picks the latest version with string.CompareOrdinal(version.Version, latest.Version), so a deployed set like versions 2 and 10 will incorrectly resolve to 2. The repo already uses the service-provided latest-version path instead: dotnet/src/Microsoft.Agents.AI.Workflows.Declarative.Foundry/AzureAgentProvider.cs:152-160 and dotnet/samples/02-agents/AgentProviders/Agent_With_AzureAIProject/Program.cs:42-46.
Automated review by rogerbarreto's agents
| { | ||
| return Home(); | ||
| } | ||
|
|
There was a problem hiding this comment.
Path traversal / arbitrary file read (defense-in-depth): This allows absolute paths (/etc/passwd) and relative traversals (../../.ssh/id_rsa) without validation. When running locally with dotnet run (as the README suggests for development), the LLM can be tricked via indirect prompt injection in an uploaded file to exfiltrate sensitive data from the developer's filesystem. The framework's own FileSystemAgentFileStore.ResolveSafePath (in FileStore/FileSystemAgentFileStore.cs:236-254) demonstrates the correct pattern: canonicalize with Path.GetFullPath then verify fullPath.StartsWith(rootPath). Consider adding equivalent validation here.
| string resolved = Path.IsPathRooted(path) ? path : Path.Combine(Home(), path); | |
| string canonical = Path.GetFullPath(resolved); | |
| string homeDir = Path.GetFullPath(Home()) + Path.DirectorySeparatorChar; | |
| if (!canonical.StartsWith(homeDir, StringComparison.Ordinal) && canonical != Path.GetFullPath(Home()) | |
| { | |
| return Home(); // Reject traversal attempts — fall back to $HOME. | |
| } | |
| return canonical; |
| } | ||
|
|
||
| static void PrintHelp() | ||
| { |
There was a problem hiding this comment.
This manual string.CompareOrdinal version ordering silently mis-selects the agent once versions reach double digits ("2" sorts after "10"). Since this sample promises to default to the latest deployed version, it should follow the repo’s existing pattern and ask the service for the agent record, then call GetLatestVersion() (see AzureAgentProvider.cs:152-160 and Agent_With_AzureAIProject/Program.cs:42-46).
… end-to-end Adds an 'ask <prompt>' command to SessionFilesClient that pins agent_session_id (via CreateResponseOptions.Patch) so the agent invoked from the REPL reads files this REPL just uploaded. Surfaces the file content as agent knowledge in the same in-process loop instead of telling the user to shell out to azd ai agent invoke.
…esClient becomes thin chat REPL The previous SessionFilesClient leaned on the alpha AgentSessionFiles SDK to upload files at runtime, which made it diverge from the canonical Using-Samples shape (SimpleAgent / SimpleInvocationsAgent: tiny chat REPLs). This change: - Bakes the sample resources/ directory into the published output via a Content Include in HostedFiles.csproj. Inside the container the files live at /app/resources/. Two local function tools (ListFiles, ReadFile) surface them to the model. - Reshapes SessionFilesClient as a thin FoundryAgent chat REPL, identical shape to SimpleAgent. AGENT_ENDPOINT + AGENT_NAME, that is it. - Demo flow: user asks 'Give me the total revenue in the contoso file' and the agent answers with the figure read from its bundled file. Validated end-to-end locally against Hosted-Files on http://localhost:60419. - Bypasses SampleEnvironment alias on optional env vars to avoid stdin prompts when running unattended. The Foundry.Hosting.IntegrationTests session-files scenario continues to validate the alpha AgentSessionFiles SDK end-to-end (upload + agent reads from session HOME) and is unchanged.
…ion-files tools to $HOME Addresses the path-traversal review comment on the session-files scenario: ResolveSessionPath in TestContainer used to allow absolute paths and .. traversals, which (when chained with indirect prompt injection in an uploaded file) would let the model read or list arbitrary container files via the ReadFile / ListFiles tools. Mirrors the canonicalize + StartsWith(home) pattern from the framework's own FileSystemAgentFileStore.ResolveSafePath: rejects rooted paths, calls Path.GetFullPath, and verifies the result stays under $HOME, throwing ArgumentException otherwise. The Hosted-Files sample is already safe (uses Path.GetFileName which strips any directory component) so no change there. The integration test continues to upload and read 'contoso_q1_2026_report.txt', a single relative filename which passes the new validation unchanged.
The previous test attempted to pin agent_session_id into the /responses payload via JsonPatch so the agent would read the file uploaded through AgentSessionFiles. The Foundry alpha service now consistently rejects the explicit-session-id pin with HTTP 400 conflict on /responses, regardless of whether the session was pre-created via AgentAdministrationClient or left to be auto-provisioned, so the agent leg of the test is no longer reachable from the SDK surface. Reshape the test to exercise what the alpha SDK actually guarantees: create session, upload, list (assert presence + size), download (assert deterministic token), delete (assert removed), cleanup. Everything stays inside Azure.AI.Projects.Agents.AgentSessionFiles. Verified live against tao-foundry-prj: UploadListDownloadAndDeleteAsync passed in 30s. Full Foundry.Hosting.IntegrationTests run: 25 total, 6 passed, 19 skipped (existing placeholders), 0 failed.
…ent.RunAsync e2e
Per review feedback the integration test must validate the hosted agent
itself: client uploads a file via the alpha AgentSessionFiles SDK, then
FoundryAgent.RunAsync invokes the deployed agent and the agent's
container-side ReadFile tool surfaces the uploaded file content into the
response.
Test flow:
1. agent.RunAsync(warmup) - platform provisions a per-session container.
2. AgentAdministrationClient.GetSessionsAsync(latest) - resolve the
just-provisioned agent_session_id.
3. AgentSessionFiles.UploadSessionFileAsync - upload contoso file to
that session, asserts BytesWritten + GetSessionFiles listing.
4. agent.RunAsync(real prompt, options=PreviousResponseId chain) -
chained to warmup so the platform routes back to the same container.
5. Assert response contains '1,482.6' (deterministic token from file).
6. Best-effort cleanup.
The test is annotated with [Fact(Skip=...)] right now: the Foundry alpha
service consistently returns HTTP 400 conflict on /responses requests
that link to a prior session via previous_response_id, conversation_id,
or agent_session_id pinning - verified across multiple retries with
multiple chaining strategies. Without that link we cannot route the
second invocation to the same container the file was uploaded to. When
the platform regression is resolved, removing the Skip will exercise
the full flow.
Full Foundry.Hosting.IntegrationTests run with this change: 25 total,
5 passed, 20 skipped (existing placeholders + this one), 0 failed.
…ent.RunAsync now passes
The blocker was a routing problem combined with a platform race:
1. Routing two /responses calls to the same per-session container.
- agent_session_id pin in body -> 400 (platform treats it as create)
- conversation_id created at project root -> 404 at agent endpoint
- previous_response_id chain -> different session
The working answer is to create the conversation on a per-agent
ProjectOpenAIClient (AgentName option, URL becomes
/agents/{name}/endpoint/protocols/openai/conversations) and pass that
conversation_id on both calls. Both then resolve to the SAME
x-agent-session-id (verified by capturing the response header).
2. Race after AgentSessionFiles upload. The upload mutates session/
conversation revision; a /responses call issued immediately after
400-conflicts with 'modified concurrently. Please retry.' Bounded
exponential retry handles it (5 attempts, 2*attempt seconds).
Test flow:
1. Create per-agent OpenAI client + ProjectConversationsClient + ProjectResponsesClient.
2. CreateProjectConversationAsync on the per-agent client.
3. Warm-up agent.RunAsync(prompt, ChatOptions { ConversationId = ... })
- captures x-agent-session-id from the response header via a custom pipeline policy.
4. AgentSessionFiles.UploadSessionFileAsync to that session id.
5. ProjectResponsesClient.CreateResponseAsync (raw, retry-on-conflict)
with the same conversation_id -> routes back to the same container.
6. Assert response contains '1,482.6' (deterministic token from file).
7. Cleanup: delete file, leave session for TTL.
Verified live against tao-foundry-prj:
UploadedFile_IsReadByHostedAgentAsync passed in 24.9s.
Full Foundry.Hosting.IntegrationTests run: 25 total, 6 passed, 19
skipped (existing placeholders), 0 failed.
There was a problem hiding this comment.
Automated Code Review
Reviewers: 3 | Confidence: 87%
✓ Security Reliability
The PR introduces solid defense-in-depth path traversal protection in the TestContainer's ResolveSessionPath (canonicalize + prefix check). The Hosted-Files sample safely uses Path.GetFileName to strip directory components. Most security issues are already covered by existing review comments. One new finding: the agent.manifest.yaml description and tool list are incorrect for the Hosted-Files sample — they describe the session-files scenario instead.
✓ Test Coverage
The integration test
UploadedFile_IsReadByHostedAgentAsyncis comprehensive for the happy path (upload → list → invoke → assert deterministic token), with meaningful assertions and bounded retry for platform race conditions. However, there are two notable test coverage gaps: (1) the server-side conversation created by the test is never cleaned up, unlike the established pattern inHappyPathHostedAgentTestswhich uses try/finally withDeleteConversationAsync, and (2) theResponseHeaderCapturePolicyhelper capturesLastRequestBodybut no test reads or asserts it, making it dead test code.
✓ Design Approach
I found two design-level mismatches in the new sample set. First, the shipped Hosted-Files manifest advertises a session-sandbox file agent, but the actual implementation only reads baked
/app/resourcescontent, so uploaded session files are never reachable through the deployed sample. Second, the new SessionFilesClient companion does not exerciseAgentSessionFilesat all; it is just a chat REPL overFoundryAgent, so the PR still lacks the code-first SDK companion described in the change rationale.
Automated review by rogerbarreto's agents
| // 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); |
There was a problem hiding this comment.
Missing conversation cleanup: A server-side conversation is created here but never deleted. The established pattern in HappyPathHostedAgentTests.MultiTurn_WithConversationId_PreservesContextAsync (lines 76–96) wraps the body in try/finally with DeleteConversationAsync. Without this, each test run leaks a conversation. Consider wrapping the remainder of the test in try/finally with a best-effort delete of conversationId.
There was a problem hiding this comment.
fix in a74d4cb. body wrap try/finally. DeleteConversationAsync at end. mirror HappyPathHostedAgentTests pattern. live test still pass.
| agentEndpoint = new UriBuilder(agentEndpoint) { Scheme = "https" }.Uri; | ||
| options.AddPolicy(new HttpSchemeRewritePolicy(), PipelinePosition.BeforeTransport); | ||
| } | ||
|
|
There was a problem hiding this comment.
No AgentSessionFiles usage: This companion currently does not exercise AgentSessionFiles at all — it only builds an AIProjectClient, creates a FoundryAgent, and forwards user input to agent.RunStreamingAsync. There is no AgentAdministrationClient, no GetAgentSessionFiles(), and no upload/list/download/remove flow. The PR still lacks the code-first SDK companion for session files that it is supposed to introduce.
There was a problem hiding this comment.
by design. SessionFilesClient = thin chat REPL like SimpleAgent. files baked in image at /app/resources/. AgentSessionFiles SDK exercised in Foundry.Hosting.IntegrationTests/SessionFilesHostedAgentTests instead. agent.manifest.yaml fixed in a74d4cb to match.
- agent.manifest.yaml: description + tags now reflect bundled-files agent (image-baked /app/resources), not the obsolete session-sandbox tools the prior shape claimed. - SessionFilesHostedAgentTests: wrap test body in try/finally to call DeleteConversationAsync on the conversation we created (matches HappyPathHostedAgentTests pattern; prevents conversation leakage across runs). - ResponseHeaderCapturePolicy: drop unused LastRequestBody capture left over from diagnosis. Test still passes live (40s).
Closes #5691.
What
Adds a .NET parallel of the Python
06_filesHosted Agents sample plus a code-first companion that exercises the alphaAzure.AI.Projects.AgentSessionFilesSDK, and an end-to-end integration test in the newFoundry.Hosting.IntegrationTestsharness (#5598).Why
Issue #5691 asks for a
.NET Hosted Agents - File Sampleparalleling the Python one. The newAzure.AI.Projects2.1.0-beta.1 ships an alphaAgentSessionFilesAPI for managing per-session sandbox files; we showcase it here as the code-first equivalent ofazd ai agent files upload.Changes
samples/04-hosting/FoundryHostedAgents/responses/Hosted-Files/— server agent with three local function tools (GetHomeDirectory,ListFiles,ReadFile) reading from the per-session$HOMEsandbox volume. Mirrors python06_files.samples/.../Using-Samples/SessionFilesClient/— REPL companion usingAgentSessionFiles(upload / ls / download / rm) and the explicit-isolation-key session lifecycle fromSample_SessionFiles.md.tests/Foundry.Hosting.IntegrationTests— adds asession-filesscenario alongside the other six in the multi-scenario harness:TestContainer/Program.cs— newCreateSessionFilesAgentswitch case + the same three file tools.Fixtures/SessionFilesHostedAgentFixture.cs— setsIT_SCENARIO=session-files.SessionFilesHostedAgentTests.cs—UploadAndAgentReadsFileAsyncexercises the full flow: create session with isolation key, upload, list, invoke agent (withagent_session_idpinned viaCreateResponseOptions.Patch), assert the response contains a deterministic token from the file, cleanup.scripts/it-bootstrap-agents.ps1+README.md—session-filesadded to the scenario list.Foundry.Hosting.IntegrationTests.csproj—Azure.AI.Projects 2.1.0-beta.1override; testdata linked from the sample so there is one source of truth.agent-framework-dotnet.slnx— registers the two new sample projects.Validation
dotnet format --verify-no-changes(CI parity viamcr.microsoft.com/dotnet/sdk:10.0Docker) on all four changed projects: clean.dotnet buildon all four projects: clean.tao-foundry-prj(image pushed toacrhostedagenttao,it-session-filesagent bootstrapped + RBAC propagated):UploadAndAgentReadsFileAsyncpassed in 35 s.Foundry.Hosting.IntegrationTests: 25 / 6 passed / 19 skipped (existing placeholders) / 0 failed.Foundry.IntegrationTests: 67 / 43 passed / 24 skipped / 0 failed (no regression).