.NET: Hosted-Files sample + AgentSessionFiles SDK companion + integration test#5698
.NET: Hosted-Files sample + AgentSessionFiles SDK companion + integration test#5698rogerbarreto wants to merge 10 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. |
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
… 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
- 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).
The previous Hosted-Files agent only exposed bundled (image-baked) file
knowledge. The platform also surfaces session-uploaded files at \C:\Users\rbarreto
inside the per-session container per container-image-spec.md line 172
(verified live by SessionFilesHostedAgentTests). The sample now teaches
both patterns.
Two distinct tool pairs, each scoped to its own root:
Bundled (image-baked): ListBundledFiles, ReadBundledFile
-> /app/resources/ (BUNDLED_FILES_DIR override)
Session-uploaded (\C:\Users\rbarreto): ListSessionFiles, ReadSessionFile
-> \C:\Users\rbarreto (default /home/session per container spec)
Security model -- distinct tools, distinct sandboxes:
- Tool input is a fileName, not a path. Schema-level: model cannot
request directories or traversals.
- Path.GetFileName(input) strips any directory components.
- Path.GetFullPath + StartsWith(root) check rejects anything outside
the tool's root, mirroring FileSystemAgentFileStore.ResolveSafePath.
- Read-only, non-recursive listing. No glob, no '..'.
- Failures non-revealing: 'File <name> not found in <scope>.'
The two roots are physically isolated (image-baked vs platform-mounted
per-session volume). A bundled-root tool can never reach a session file
and vice-versa, even if the implementation has a bug.
README updated to document both flows, the security pattern, and cite
the container-image-spec.md line 172 contract for \C:\Users\rbarreto. Live IT
SessionFilesHostedAgentTests.UploadedFile_IsReadByHostedAgentAsync
re-passed in 42s after the change (TestContainer is unchanged; the
sample-agent split does not affect the IT).
SergeyMenshykh
left a comment
There was a problem hiding this comment.
Review — .NET Hosted-Files sample + AgentSessionFiles SDK + integration test
Overall this is a well-structured, thorough PR. The security model (3-layer path traversal defense) is solid, the integration test exercises the full end-to-end flow, and the docs are detailed. A few items to address:
Issues
1. agent.manifest.yaml description is stale
The manifest describes only two tools (ListFiles, ReadFile), but the actual Program.cs exposes four tools: ListBundledFiles, ReadBundledFile, ListSessionFiles, ReadSessionFile. The manifest description should be updated to reflect the dual-source tool surface.
2. SessionFilesClient/README.md references wrong tool names
The agent's container-side
ListFilesandReadFiletools surface the bundled file contents to the model.
These tool names don't exist on the Hosted-Files agent anymore — they should be ListBundledFiles/ReadBundledFile (and ListSessionFiles/ReadSessionFile for session files).
3. Missing trailing newline in Hosted-Files/README.md
The file ends without a newline (line 543 of the diff: \ No newline at end of file). Minor, but will cause noise in future diffs and may trip linters.
Nits / suggestions
-
DevTemporaryTokenCredentialis duplicated across samples (also exists in other Hosted-* samples). Consider extracting to a shared helper if the sample set keeps growing — though duplication is acceptable for standalone samples. -
In
SessionFilesHostedAgentTests, the retry loop for the 400 "modified concurrently" race is well-guarded (the 5th attempt's exception propagates naturally), but a brief comment noting whyrawResponseis declarednull!(the compiler can't see through the retry control flow) would help future readers.
What looks good
- Path traversal defense: The
GetFileName→GetFullPath→StartsWith(root + sep)pattern is textbook defense-in-depth, and the controlled error message avoids leaking canonical paths. - Separate tool pairs instead of a single
ReadFile(path)— excellent attack-surface minimization. - Test data sharing: Linking the
contoso_q1_2026_report.txtvia<Content Include="..." Link="...">keeps a single source of truth. - Retry logic for the platform's concurrent-modification race is pragmatic and bounded.
- Documentation is thorough — the READMEs, inline comments, and the security rationale in the code header are all high quality.
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).