.NET: Foundry hosted-agent toolbox OAuth consent support#6718
.NET: Foundry hosted-agent toolbox OAuth consent support#6718rogerbarreto wants to merge 5 commits into
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. Fixes microsoft#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. |
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
|
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
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.
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.