Skip to content

.NET: Foundry hosted-agent toolbox OAuth consent support#6718

Open
rogerbarreto wants to merge 5 commits into
microsoft:mainfrom
rogerbarreto:issues/6562-foundry-toolbox-oauth-consent
Open

.NET: Foundry hosted-agent toolbox OAuth consent support#6718
rogerbarreto wants to merge 5 commits into
microsoft:mainfrom
rogerbarreto:issues/6562-foundry-toolbox-oauth-consent

Conversation

@rogerbarreto

Copy link
Copy Markdown
Member

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_REQUIRED until the end user has consented. Two problems blocked this scenario:

  1. A hosted agent that pre-registers such a toolbox bricked at container startup. Eager tools/list enumeration runs with no user context, the proxy hard errors, readiness goes 503, and every invocation fails with HTTP 424.
  2. Even when the container stayed alive, the consent requirement was surfaced as an mcp_approval_request, which is the generic approve/deny tool gate rather than the platform-canonical OAuth consent surface. The Foundry platform heads render oauth_consent_request for 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?

    • Startup deferral: a generic toolbox open failure at startup is no longer fatal. A new Degraded startup status keeps the container routable, and the toolbox is retried per request (RetryDeferredToolboxesAsync) where the platform injects the per-user isolation key on egress.
    • Canonical consent emission: both consent paths now emit an oauth_consent_request output item (OAuthConsentRequestOutputItem) carrying the consent link and mark the response incomplete, replacing the previous mcp_approval_request emission. This matches Python and how the platform renders consent (verified against the vienna toolbox proxy source and the AgentServer Responses SDK).
    • Consent parsing: ToolboxConsentParser parses the toolbox CONSENT_REQUIRED (-32006) error to extract the consent link.
    • Sample client: a new Hosted-Toolbox-AuthPaths OAuth consent REPL that detects oauth_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.
    • Tests: consent parser, startup deferral and Degraded health, and oauth_consent_request emission.
  • What is the impact of these changes?

    • Hosted agents that pre-register a per-user OAuth toolbox no longer brick at startup and correctly surface consent on the first user request.
    • The consent wire shape changes from mcp_approval_request to oauth_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.
    • Validated live on a Foundry project end to end: a raw invoke returns oauth_consent_request with the consent link, and the REPL client intercepts and renders it.
  • What do you want reviewers to focus on?

    • The deferral semantics in FoundryToolboxService (Degraded vs ConsentRequired vs Unhealthy) and the health-check mapping.
    • The oauth_consent_request emission and item id generation in AgentFrameworkResponseHandler.

Related Issue

Fixes #6562

Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR is linked to an issue and there is no other open PR for this issue (see Related Issue above).
  • This is not a breaking change. If it is a breaking change, add the breaking change label (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.

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
Copilot AI review requested due to automatic review settings June 24, 2026 19:25
@moonbox3 moonbox3 added documentation Usage: [Issues, PRs], Target: documentation in the code base and learn docs .NET Usage: [Issues, PRs], Target: .Net labels Jun 24, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/list aggregate CONSENT_REQUIRED errors to extract consent links and emit oauth_consent_request output items, marking responses incomplete.
  • 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.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_request surface, when the PR explicitly changes emission to oauth_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.TryGetValue is read without the lock in GetToolboxToolsAsync (line 423), while the new RetryDeferredToolboxesAsync and ResolvePendingConsentsAsync methods write to _toolboxes under the lock during request handling. Before this PR, writes to _toolboxes only 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_request for pre-registered toolboxes, but it silently skips the same consent case for lazily resolved foundry-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.StrictMode is false (FoundryToolboxOptions.cs:30-35), but GetToolboxToolsAsync now records CONSENT_REQUIRED and returns an empty tool list (FoundryToolboxService.cs:450-458). AgentFrameworkResponseHandler.CreateAsync only checks ResolvePendingConsentsAsync before 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 returning oauth_consent_request. That is a silent behavior break on a documented input path.

Automated review by rogerbarreto's agents

Comment thread dotnet/src/Microsoft.Agents.AI.Foundry.Hosting/FoundryToolboxService.cs Outdated
@github-actions

Copy link
Copy Markdown
Contributor

Flagged issue

Lazy toolbox markers are a supported scenario when FoundryToolboxOptions.StrictMode is false (FoundryToolboxOptions.cs:30-35), but GetToolboxToolsAsync now records CONSENT_REQUIRED and returns an empty tool list (FoundryToolboxService.cs:450-458). AgentFrameworkResponseHandler.CreateAsync only checks ResolvePendingConsentsAsync before 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 returning oauth_consent_request. That is a silent behavior break on a documented input path.


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.
@rogerbarreto rogerbarreto marked this pull request as ready for review June 24, 2026 20:33
@rogerbarreto rogerbarreto self-assigned this Jun 24, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Usage: [Issues, PRs], Target: documentation in the code base and learn docs .NET Usage: [Issues, PRs], Target: .Net

Projects

None yet

Development

Successfully merging this pull request may close these issues.

.NET: [Bug]: OAuth MCP Tool Consent Required Flow not supported by toolbox for Hosted Agents

3 participants