Uh oh!
There was an error while loading. Please reload this page.
.NET: Foundry hosted-agent toolbox OAuth consent support - #6718
Conversation
Add per-user OAuth (MCP CONSENT_REQUIRED) support for Foundry hosted agents. * Defer hard toolbox startup failures so a per-user OAuth-gated toolbox no longer bricks the container at startup (new Degraded status, retried per request). The container stays routable and surfaces consent on the first user request. * Emit the platform-canonical oauth_consent_request output item (instead of mcp_approval_request) for toolbox OAuth consent, matching the Python implementation and how the Foundry platform heads render consent. * Parse the toolbox CONSENT_REQUIRED (-32006) error and surface the consent link; resume by re-sending the prompt with no reply item needed. * Add the Hosted-Toolbox-AuthPaths OAuth consent REPL client sample that detects oauth_consent_request, prints the consent link, and re-sends. * Add tests for the consent parser, startup deferral, and oauth_consent_request emission. Fixesmicrosoft#6562
There was a problem hiding this comment.
Pull request overview
This PR enables Foundry hosted agents (.NET) to properly handle per-user OAuth consent flows for toolbox MCP tool sources by (1) keeping containers routable when tool enumeration can’t succeed at startup and (2) emitting the platform-canonical oauth_consent_request output item (instead of mcp_approval_request) when consent is required.
Changes:
- Add startup deferral semantics for toolbox enumeration failures (
Degraded/ConsentRequired) and retry deferred toolboxes per request. - Parse
tools/listaggregateCONSENT_REQUIREDerrors to extract consent links and emitoauth_consent_requestoutput items, marking responsesincomplete. - Add unit tests and a new OAuth-consent-aware sample REPL client that prints consent links and resumes by re-sending the prompt.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/ToolboxConsentParserTests.cs | Adds coverage for parsing tools/list aggregate CONSENT_REQUIRED payloads. |
| dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/OAuthConsentEmissionTests.cs | Verifies oauth_consent_request emission shape and id generation. |
| dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryToolboxServiceTests.cs | Updates tests for startup deferral and per-request retry behavior. |
| dotnet/tests/Microsoft.Agents.AI.Foundry.Hosting.UnitTests/FoundryToolboxHealthCheckTests.cs | Updates health-check expectations to keep containers routable when deferred. |
| dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/ToolboxConsentParser.cs | Introduces parser to extract consent URLs from aggregate tools/list failures. |
| dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/FoundryToolboxStartupStatus.cs | Adds ConsentRequired and Degraded startup statuses. |
| dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/FoundryToolboxService.cs | Implements deferral, pending-consent tracking, and per-request retry/resolution. |
| dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/FoundryToolboxHealthCheck.cs | Maps ConsentRequired/Degraded to Healthy readiness with descriptive messages. |
| dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/AgentFrameworkResponseHandler.cs | Emits oauth_consent_request and short-circuits with incomplete when consent is pending. |
| dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/Hosted-Toolbox-AuthPaths-Client/README.md | Documents the OAuth-consent-aware REPL flow. |
| dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/Hosted-Toolbox-AuthPaths-Client/Program.cs | Adds REPL client that prints consent links and resumes by re-sending prompts. |
| dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/Hosted-Toolbox-AuthPaths-Client/Hosted-Toolbox-AuthPaths-Client.csproj | Adds new sample project for the OAuth consent REPL. |
| dotnet/samples/04-hosting/FoundryHostedAgents/responses/Hosted-Toolbox-AuthPaths/README.md | Updates sample guidance and troubleshooting to include OAuth consent path (#4). |
| dotnet/agent-framework-dotnet.slnx | Adds the new sample client project to the solution. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Automated Code Review
Reviewers: 5 | Confidence: 84%
✓ Correctness
The PR is well-implemented with correct concurrency handling (SemaphoreSlim + double-check pattern), proper resource disposal in failure paths, and sound consent-parsing logic. The only issue found is a stale doc comment in the newly created ToolboxConsentParser.cs that references the old
mcp_approval_requestsurface, when the PR explicitly changes emission tooauth_consent_request.
✓ Security Reliability
The PR is well-structured with proper resource cleanup, lock-based concurrency protection, and defensive JSON parsing. The consent parser correctly validates structure and rejects partial/malformed payloads. Two moderate concerns: (1) consent URLs extracted from the proxy error message are not validated as HTTP(S) before being surfaced to users, relying solely on the proxy being a trusted component; (2) per-request retry of deferred toolboxes holds the service-wide semaphore while performing network I/O, which could serialize all requests behind a slow-to-fail endpoint. A minor doc comment inconsistency also exists.
✓ Test Coverage
The PR adds good unit tests for the new ToolboxConsentParser, the EmitOAuthConsentRequest helper, the Degraded health check path, and the RetryDeferredToolboxesAsync still-unreachable scenario. However, there are two notable test coverage gaps: (1) ResolvePendingConsentsAsync has no unit test at all — its success path, still-pending path, and exception-during-retry path are untested; (2) the ConsentRequired health check branch in FoundryToolboxHealthCheck has no test (only Degraded, Healthy, NoEndpoint, and Pending are covered). These are non-trivial code paths with branching logic that would benefit from explicit tests.
✓ Failure Modes
The PR's failure-mode handling is generally well-structured, with proper lock use around state mutations and clean exception handling in OpenToolboxAsync. The main concern is a pre-existing concurrent Dictionary access pattern that this PR significantly amplifies:
_toolboxes.TryGetValueis read without the lock inGetToolboxToolsAsync(line 423), while the newRetryDeferredToolboxesAsyncandResolvePendingConsentsAsyncmethods write to_toolboxesunder the lock during request handling. Before this PR, writes to_toolboxesonly happened during startup or during lazy open (same code path), making the race extremely narrow. Now, with every request retrying deferred toolboxes, the window for concurrent read+write on the non-thread-safe Dictionary is materially wider.
✗ Design Approach
I found one design-level gap in the new consent flow: it correctly surfaces
oauth_consent_requestfor pre-registered toolboxes, but it silently skips the same consent case for lazily resolvedfoundry-toolbox://markers. On that supported non-strict path, the request proceeds without the requested toolbox instead of stopping with an OAuth consent prompt.
Flagged Issues
- Lazy toolbox markers are a supported scenario when
FoundryToolboxOptions.StrictModeisfalse(FoundryToolboxOptions.cs:30-35), butGetToolboxToolsAsyncnow recordsCONSENT_REQUIREDand returns an empty tool list (FoundryToolboxService.cs:450-458).AgentFrameworkResponseHandler.CreateAsynconly checksResolvePendingConsentsAsyncbefore it resolves markers (AgentFrameworkResponseHandler.cs:175-196, 217-258), so the first request that discovers consent on a lazy marker runs without that toolbox instead of returningoauth_consent_request. That is a silent behavior break on a documented input path.
Automated review by rogerbarreto's agents
Uh oh!
There was an error while loading. Please reload this page.
Flagged issue Lazy toolbox markers are a supported scenario when Source: automated DevFlow PR review |
* Make RecomputeStatus the single source that refreshes ConsentRequiredToolboxNames from the pending-consent set, so a per-request marker that records consent via GetToolboxToolsAsync no longer leaves ConsentRequiredToolboxNames stale (which made ResolvePendingConsentsAsync skip surfacing it). * Surface lazy / per-request marker consent in the same request: after resolving markers the handler now emits oauth_consent_request + incomplete when a marker hit CONSENT_REQUIRED, instead of silently running without that toolbox. * Add FoundryToolboxService.GetPendingConsents() snapshot accessor. * Fix stale ToolboxConsentParser doc comment (mcp_approval_request -> oauth_consent_request).
* Thread-safety: GetPendingConsents() now returns an immutable snapshot rebuilt in RecomputeStatus under the lock, instead of enumerating the live _pendingConsents dictionary off-lock (which could throw under concurrent requests). * Resource leak: OpenToolboxAsync builds the endpoint Uri before allocating the HttpClient and now disposes the HttpClient when McpClient.CreateAsync throws (the unreachable/deferred case retried per request), not only when ListToolsAsync fails. * StrictMode now gates on the pre-registered ToolboxNames set rather than the opened-toolbox cache, so a registered-but-deferred toolbox is no longer rejected as unknown. * Sample REPL: the legacy approval-args consent fallback only reads the explicit consent_url key, so a normal function-tool approval carrying a URL argument is not misread as an OAuth consent request.
There was a problem hiding this comment.
Automated Code Review
Reviewers: 4 | Confidence: 89%
✓ Correctness
This is a well-structured PR that correctly implements OAuth consent deferral for Foundry hosted-agent toolboxes. The startup deferral semantics (Degraded vs ConsentRequired vs Unhealthy), the retry-per-request flow, and the canonical oauth_consent_request emission are all logically sound. The previously resolved review comments (lazy-marker consent gap, stale ConsentRequiredToolboxNames, XML doc mismatch) have been properly addressed in the current code. Thread safety is maintained through the SemaphoreSlim lock with appropriate double-check patterns, and the immutable snapshot approach for GetPendingConsents avoids concurrent-mutation issues. The ToolboxConsentParser correctly enforces the 'all errors must be CONSENT_REQUIRED' invariant. No correctness bugs found.
✓ Security Reliability
The PR is well-structured and addresses the stated goals cleanly. Resource leak handling in OpenToolboxAsync is improved, locking patterns are correct (double-checked locking with _lazyOpenLock), and the consent/deferred state machine is coherent. The previously resolved review comments are properly addressed: marker-discovered consents are now surfaced (line 260), ConsentRequiredToolboxNames is refreshed in RecomputeStatus (line 240), and the XML doc is updated. One defense-in-depth suggestion on consent URL scheme validation.
✓ Test Coverage
The PR has strong unit-test coverage for the new ToolboxConsentParser (8 tests, good edge-case coverage) and for EmitOAuthConsentRequest (event shape, types, IDs). The Degraded/deferred startup path is well-tested. However, the ConsentRequired startup status path is never exercised by any test — the consent-specific branch in StartAsync (line 173), ResolvePendingConsentsAsync, and the corresponding HealthCheck case are all untested. These are the core new consent-handling paths and represent the main coverage gap.
✓ Design Approach
I found one design issue in the new consent-retry flow: a toolbox that was introduced only through a per-request marker can become permanently global after consent succeeds, so later requests get that toolbox injected even when they did not request it. The rest of the consent-surface work looks internally consistent.
Automated review by rogerbarreto's agents
Uh oh!
There was an error while loading. Please reload this page.
Addresses review feedback that a marker-originated toolbox could leak into global scope after consent. GetToolboxToolsAsync now returns a request-scoped ToolboxResolution (tools or consent requirements) instead of recording marker consent in the container-global _pendingConsents and appending resolved tools to the service-wide Tools list. * Marker consent is surfaced as oauth_consent_request for the requesting turn only and collected in the handler's marker loop; it no longer injects tools into, or raises a consent prompt on, a later request that did not reference the marker. * Marker resolution no longer flips the container StartupStatus to ConsentRequired (per-request markers must not affect readiness, per the StartupStatus contract). * Remove the now-unused GetPendingConsents()/snapshot path; _pendingConsents is once again exclusively the pre-registered/startup consent set.
…n test Unit tests (Microsoft.Agents.AI.Foundry.Hosting.UnitTests): * New FoundryToolboxMarkerScopingTests proves per-request marker resolution is request-scoped: a marker consent is returned to the caller without mutating ConsentRequiredToolboxNames, StartupStatus, or the service-wide Tools cache, and marker-resolved tools are returned to the caller rather than injected globally (so a request with no marker sees neither the tools nor the consent). * Adds a test-only ToolboxOpener seam on FoundryToolboxService so the consent/tools resolution can be exercised without a live MCP proxy. Makes ToolboxOpenResult and CachedToolbox internal (CachedToolbox.Client nullable, guarded at dispose). Integration test (Foundry.Hosting.IntegrationTests): * New toolbox-oauth-consent scenario wired into the TestContainer (pre-registers a Foundry toolbox via AddFoundryToolboxes from IT_TOOLBOX_NAME), a ToolboxOAuthConsentHostedAgentFixture, and a ToolboxOAuthConsentHostedAgentTests that invokes the deployed agent and asserts the consumer captures an oauth_consent_request consent link (container stays routable, no 424). Skipped by default per the IT convention; documents the consent-gated toolbox prerequisite. * Adds the scenario to it-bootstrap-agents.ps1 and the README scenario table.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Motivation & Context
Foundry hosted agents could not complete the per-user OAuth consent flow for toolbox MCP tools. When a toolbox tool source is fronted by a per-user OAuth connection (for example a delegated Microsoft Graph or a Logic Apps connector), the toolbox proxy returns
CONSENT_REQUIREDuntil the end user has consented. Two problems blocked this scenario:tools/listenumeration runs with no user context, the proxy hard errors, readiness goes 503, and every invocation fails with HTTP 424.mcp_approval_request, which is the generic approve/deny tool gate rather than the platform-canonical OAuth consent surface. The Foundry platform heads renderoauth_consent_requestfor consent, and the Python implementation already emits it.The result makes the customer scenario (frontend user, then a hosted agent calling a per-user OAuth toolbox) work end to end.
Description & Review Guide
What are the major changes?
Degradedstartup status keeps the container routable, and the toolbox is retried per request (RetryDeferredToolboxesAsync) where the platform injects the per-user isolation key on egress.oauth_consent_requestoutput item (OAuthConsentRequestOutputItem) carrying the consent link and mark the responseincomplete, replacing the previousmcp_approval_requestemission. This matches Python and how the platform renders consent (verified against the vienna toolbox proxy source and the AgentServer Responses SDK).ToolboxConsentParserparses the toolboxCONSENT_REQUIRED(-32006) error to extract the consent link.Hosted-Toolbox-AuthPathsOAuth consent REPL that detectsoauth_consent_request, prints the consent link, waits for the user, and re-sends the prompt to resume. It works in headless and non-GUI shells.Degradedhealth, andoauth_consent_requestemission.What is the impact of these changes?
mcp_approval_requesttooauth_consent_request, the platform-canonical, Python-aligned surface. The package is experimental (OPENAI001), and the consent flow did not work before, so no working behavior is removed.oauth_consent_requestwith the consent link, and the REPL client intercepts and renders it.What do you want reviewers to focus on?
FoundryToolboxService(DegradedvsConsentRequiredvsUnhealthy) and the health-check mapping.oauth_consent_requestemission and item id generation inAgentFrameworkResponseHandler.Related Issue
Fixes#6562
Contribution Checklist
breaking changelabel (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.