chore: import upstream snapshot with attribution
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:39:25 +08:00
commit db620d33df
5151 changed files with 925932 additions and 0 deletions
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
<RootNamespace>HostedToolboxAuthPathsClient</RootNamespace>
<AssemblyName>hosted-toolbox-authpaths-client</AssemblyName>
<NoWarn>$(NoWarn);NU1605;OPENAI001</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.Projects" />
<PackageReference Include="Azure.Core" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="DotNetEnv" />
<PackageReference Include="System.ClientModel" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,312 @@
// Copyright (c) Microsoft. All rights reserved.
// Hosted Toolbox Auth Paths — OAuth consent REPL client.
//
// This REPL drives the Hosted-Toolbox-AuthPaths agent and, unlike the plain
// Using-Samples/SimpleAgent client, it understands the OAuth user-consent path.
//
// 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 Foundry toolbox proxy cannot
// call the tool until the end user has consented. The hosted agent surfaces that as an
// oauth_consent_request output item carrying a consent link, and marks the response
// incomplete. This is the platform-canonical consent surface (the same item the Foundry
// Bot/Teams and A2A heads render as "open link + resume"), distinct from mcp_approval_request,
// which is the generic approve/deny tool gate. This client:
// 1. Detects the oauth_consent_request and extracts the consent link.
// 2. PRINTS the consent link and waits for the user to press Enter once they have completed
// the OAuth flow out of band. It never auto-opens a browser, so it works in headless,
// SSH, container, and other non-GUI environments.
// 3. RE-SENDS the original prompt on the same session. The proxy now holds the user's
// delegated token, so the retried tool call succeeds.
//
// Important: an OAuth consent request is resumed by RE-SENDING the prompt, NOT by replying
// with a ToolApprovalResponseContent. The consent request records no approval-id mapping on
// the server, so a CreateResponse(...) reply would be rejected. Only function-tool approvals
// (which this client also handles, for completeness) use the CreateResponse path.
//
// Required environment variables:
// AZURE_AI_PROJECT_ENDPOINT - Foundry project endpoint, or the local dev server base
// (e.g. http://localhost:8088/api/projects/local).
// AZURE_AI_AGENT_NAME - The registered server-side agent name
// (default: hosted-toolbox-auth-paths-agent).
using System.ClientModel.Primitives;
using System.Text.Json;
using Azure.AI.Projects;
using Azure.Identity;
using DotNetEnv;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Foundry;
using Microsoft.Extensions.AI;
// Load .env file if present (for local development)
Env.TraversePath().Load();
Uri projectEndpoint = new(Environment.GetEnvironmentVariable("AZURE_AI_PROJECT_ENDPOINT")
?? Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
?? throw new InvalidOperationException("AZURE_AI_PROJECT_ENDPOINT is not set."));
string agentName = Environment.GetEnvironmentVariable("AZURE_AI_AGENT_NAME")
?? "hosted-toolbox-auth-paths-agent";
// Derive the per-agent OpenAI endpoint that hosted Foundry agents require.
Uri agentEndpoint = new($"{projectEndpoint}/agents/{agentName}/endpoint/protocols/openai");
var options = new AIProjectClientOptions();
if (projectEndpoint.Scheme == "http")
{
// For local HTTP dev: present HTTPS to satisfy BearerTokenPolicy's TLS check, then swap
// the scheme back to HTTP right before the request hits the wire.
projectEndpoint = new UriBuilder(projectEndpoint) { Scheme = "https" }.Uri;
agentEndpoint = new UriBuilder(agentEndpoint) { Scheme = "https" }.Uri;
options.AddPolicy(new HttpSchemeRewritePolicy(), PipelinePosition.BeforeTransport);
}
var aiProjectClient = new AIProjectClient(projectEndpoint, new AzureCliCredential(), options);
FoundryAgent agent = aiProjectClient.AsAIAgent(agentEndpoint);
AgentSession session = await agent.CreateSessionAsync();
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine($"""
══════════════════════════════════════════════════════════
Hosted Toolbox Auth Paths — OAuth consent client
Connected to: {agentEndpoint}
Ask something that needs the OAuth-protected tool to trigger consent.
Type a message or 'quit' to exit.
══════════════════════════════════════════════════════════
""");
Console.ResetColor();
Console.WriteLine();
while (true)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.Write("You> ");
Console.ResetColor();
string? input = Console.ReadLine();
if (string.IsNullOrWhiteSpace(input)) { continue; }
if (input.Equals("quit", StringComparison.OrdinalIgnoreCase)) { break; }
try
{
AgentResponse response = await agent.RunAsync(input, session);
// Resolve any consent or approval requests before showing the final answer. Each
// round-trip either re-sends (OAuth consent) or replies with an approval decision
// (function tools).
while (true)
{
List<AIContent> contents = response.Messages
.SelectMany(m => m.Contents)
.ToList();
// An OAuth consent request surfaces as an oauth_consent_request output item carrying a
// consent link. It is resumed by RE-SENDING the prompt (no reply item), because the
// proxy stores the user's delegated token server-side keyed to the user.
List<string> consentUrls = contents
.Select(TryExtractConsentUrl)
.Where(url => url is not null)
.Select(url => url!)
.Distinct(StringComparer.Ordinal)
.ToList();
if (consentUrls.Count > 0)
{
Console.ForegroundColor = ConsoleColor.Magenta;
Console.WriteLine();
Console.WriteLine("The agent needs your OAuth consent before it can call a tool on your behalf.");
Console.WriteLine("Open the link(s) below in any browser and complete the sign-in / consent:");
foreach (string url in consentUrls)
{
Console.WriteLine();
Console.WriteLine(url);
}
Console.WriteLine();
Console.ResetColor();
// Wait for the user to finish consent out of band. They can press Enter once done.
// The value is not needed to resume — the proxy stores the delegated token
// server-side keyed to the user.
Console.Write("After completing consent, press Enter to continue... ");
_ = Console.ReadLine();
// Re-send the SAME prompt on the SAME session. The proxy now holds the user's
// delegated token, so the retried tool call succeeds. Do NOT CreateResponse here.
response = await agent.RunAsync(input, session);
continue;
}
// Function-tool approval path (Y/N), included so the client also handles agents
// that mix human-in-the-loop function approvals with OAuth consent.
List<ToolApprovalRequestContent> approvals = contents
.OfType<ToolApprovalRequestContent>()
.ToList();
if (approvals.Count == 0)
{
break;
}
List<ChatMessage> decisions = approvals.ConvertAll(approval =>
{
string name = (approval.ToolCall as FunctionCallContent)?.Name ?? "tool";
Console.ForegroundColor = ConsoleColor.Yellow;
Console.Write($"Approve tool call '{name}'? [Y/N] ");
Console.ResetColor();
bool approved = Console.ReadLine()?.Trim().Equals("Y", StringComparison.OrdinalIgnoreCase) ?? false;
return new ChatMessage(ChatRole.User, [approval.CreateResponse(approved)]);
});
response = await agent.RunAsync(decisions, session);
}
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"Agent> {response}");
Console.ResetColor();
}
catch (Exception ex)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"Error: {ex.Message}");
Console.ResetColor();
}
Console.WriteLine();
}
Console.WriteLine("Goodbye!");
// ── Helpers ─────────────────────────────────────────────────────────────────────
// Extracts an OAuth consent link from a response content, or returns null when the content is
// not a consent request. The canonical surface is an `oauth_consent_request` output item, which
// the high-level client exposes through AIContent.RawRepresentation (its `ConsentLink` field
// carries the URL). For resilience this also handles a consent URL surfaced inside a
// ToolApprovalRequestContent payload (the older mcp_approval_request shape). Plain
// human-in-the-loop function approvals carry no URL and return null.
static string? TryExtractConsentUrl(AIContent content)
{
// 1) Canonical: oauth_consent_request output item via RawRepresentation.
if (TryGetConsentLinkFromRaw(content.RawRepresentation) is { } fromRaw)
{
return fromRaw;
}
// 2) Back-compat: a consent URL carried in an approval request's arguments.
if (content is ToolApprovalRequestContent approval)
{
return TryExtractConsentUrlFromApproval(approval);
}
return null;
}
// Reads a consent link from a raw oauth_consent_request item. The high-level client surfaces this
// item as a base AIContent whose RawRepresentation is an SDK response item: in the typed case it
// exposes a `ConsentLink` member, but the OpenAI Responses client parses the (non-OpenAI)
// oauth_consent_request as an *unknown* item, so the link only lives in the item's JSON. We try the
// typed member first, then fall back to serializing the persistable model and reading `consent_link`.
static string? TryGetConsentLinkFromRaw(object? raw)
{
if (raw is null)
{
return null;
}
// Fast path: a typed consent item exposes ConsentLink directly (Uri or string).
switch (raw.GetType().GetProperty("ConsentLink")?.GetValue(raw))
{
case Uri uri when LooksLikeUrl(uri.AbsoluteUri):
return uri.AbsoluteUri;
case string s when LooksLikeUrl(s):
return s;
}
// General path: serialize the unknown response item back to JSON and read the wire fields.
try
{
BinaryData json = ModelReaderWriter.Write(raw, new ModelReaderWriterOptions("J"));
using JsonDocument doc = JsonDocument.Parse(json);
JsonElement root = doc.RootElement;
if (root.ValueKind == JsonValueKind.Object
&& root.TryGetProperty("type", out JsonElement typeProp)
&& typeProp.GetString() == "oauth_consent_request"
&& root.TryGetProperty("consent_link", out JsonElement linkProp)
&& linkProp.GetString() is string link
&& LooksLikeUrl(link))
{
return link;
}
}
catch
{
// Not a persistable model, or no consent_link present — fall through.
}
return null;
}
// Scans an approval request's tool-call arguments for a consent URL (older wire shape, where a
// toolbox OAuth consent was surfaced as an mcp_approval_request carrying a consent_url argument).
static string? TryExtractConsentUrlFromApproval(ToolApprovalRequestContent approval)
{
IDictionary<string, object?>? arguments = approval.ToolCall switch
{
McpServerToolCallContent mcpCall => mcpCall.Arguments,
FunctionCallContent functionCall => functionCall.Arguments,
_ => null,
};
// Only the explicit consent_url key counts (the legacy mcp_approval_request consent shape).
// We deliberately do NOT scan arbitrary argument values for URLs, so a normal function-tool
// approval that happens to carry a URL argument is never misread as an OAuth consent request.
if (arguments is null || !arguments.TryGetValue("consent_url", out object? value))
{
return null;
}
return value switch
{
string s when LooksLikeUrl(s) => s,
JsonElement { ValueKind: JsonValueKind.String } element
when element.GetString() is string elementString && LooksLikeUrl(elementString) => elementString,
_ => null,
};
}
static bool LooksLikeUrl(string value) =>
value.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
|| value.StartsWith("https://", StringComparison.OrdinalIgnoreCase);
/// <summary>
/// For Local Development Only.
/// Rewrites HTTPS URIs to HTTP right before transport, allowing AIProjectClient to target a
/// local HTTP dev server while satisfying BearerTokenPolicy's TLS check.
/// </summary>
internal sealed class HttpSchemeRewritePolicy : PipelinePolicy
{
public override void Process(PipelineMessage message, IReadOnlyList<PipelinePolicy> pipeline, int currentIndex)
{
RewriteScheme(message);
ProcessNext(message, pipeline, currentIndex);
}
public override async ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList<PipelinePolicy> pipeline, int currentIndex)
{
RewriteScheme(message);
await ProcessNextAsync(message, pipeline, currentIndex).ConfigureAwait(false);
}
private static void RewriteScheme(PipelineMessage message)
{
var uri = message.Request.Uri!;
if (uri.Scheme == Uri.UriSchemeHttps)
{
message.Request.Uri = new UriBuilder(uri) { Scheme = "http" }.Uri;
}
}
}
@@ -0,0 +1,76 @@
# Hosted Toolbox Auth Paths — OAuth consent client
A REPL client for the [`Hosted-Toolbox-AuthPaths/`](../../Hosted-Toolbox-AuthPaths/) agent that understands the **OAuth user-consent** path.
The plain [`SimpleAgent/`](../SimpleAgent/) REPL is enough for the key-based, agent-identity, and inline-`Authorization` tools. It is **not** enough for a tool fronted by a **per-user OAuth connection** (for example a delegated Microsoft Graph connector or a Logic Apps connector), because that tool cannot run until the end user has consented. This client handles that flow.
## What it does
When a toolbox tool source needs the user's delegated token, the hosted agent surfaces an `oauth_consent_request` output item that carries a **consent link** and marks the response `incomplete`. This client:
1. Detects the `oauth_consent_request` and extracts the consent link.
2. **Prints the consent link** so you can open it in any browser and complete the OAuth flow out of band. It never auto-opens a browser, so it works in headless, SSH, and container shells.
3. Waits for you to press Enter, then **re-sends the original prompt on the same session**. The toolbox proxy now holds your delegated token, so the retried tool call succeeds.
> **Why re-send instead of replying with an approval?** An OAuth consent request records no approval-id mapping on the server, so a `ToolApprovalResponseContent` (`CreateResponse(...)`) reply would be rejected. The user's token lives on the proxy after consent, so simply re-sending the same prompt resumes the call. Function-tool approvals are different: those *do* use `CreateResponse(...)`, and this client handles them too for completeness.
## How the consent flows (no token ever touches this client)
```mermaid
sequenceDiagram
actor You
participant Client
participant Agent as Hosted Agent
participant Proxy as Toolbox Proxy
participant IdP as Identity Provider
participant Target as Target Service
You->>Client: prompt
Client->>Agent: run prompt
Agent->>Proxy: call tool
Proxy-->>Agent: CONSENT_REQUIRED (no token yet)
Agent-->>Client: oauth_consent_request (consent link) + incomplete
Client-->>You: print consent link
You->>IdP: consent in browser
IdP->>Proxy: token (stored, bound to you)
You->>Client: press Enter
Client->>Agent: re-send same prompt
Agent->>Proxy: call tool
Proxy->>Target: OBO token
Target-->>Proxy: result
Proxy-->>Agent: result
Agent-->>Client: result
Client-->>You: result
```
The client never sees the user's token. Consent and the on-behalf-of token exchange happen entirely between the user, the identity provider, and the toolbox proxy.
## Prerequisites
- The [`Hosted-Toolbox-AuthPaths/`](../../Hosted-Toolbox-AuthPaths/) agent running (locally or deployed) with at least one toolbox tool source configured for a per-user OAuth connection. See that sample's README, **Auth path #4 (OAuth user consent)**.
- `az login` so the client can mint a bearer token to reach the agent endpoint.
## Run
```powershell
cd Hosted-Toolbox-AuthPaths-Client
# Against the local dev server (the {project} segment is a wildcard the server ignores):
$env:AZURE_AI_PROJECT_ENDPOINT = "http://localhost:8088/api/projects/local"
$env:AZURE_AI_AGENT_NAME = "hosted-toolbox-auth-paths-agent"
# Or against a deployed agent:
# $env:AZURE_AI_PROJECT_ENDPOINT = "https://<account>.services.ai.azure.com/api/projects/<project>"
# $env:AZURE_AI_AGENT_NAME = "hosted-toolbox-auth-paths-agent"
dotnet run --tl:off
```
Then ask something that needs the OAuth-protected tool. When the consent prompt appears, the client prints the consent link. Open it in any browser, complete sign-in, then press Enter and the client re-sends automatically.
## Environment variables
| Variable | Required | Default | Notes |
|---|---|---|---|
| `AZURE_AI_PROJECT_ENDPOINT` | yes | — | Foundry project endpoint, or the local dev server base. `FOUNDRY_PROJECT_ENDPOINT` is read as a fallback. |
| `AZURE_AI_AGENT_NAME` | no | `hosted-toolbox-auth-paths-agent` | Registered server-side agent name. |
@@ -0,0 +1,51 @@
# Using-Samples — client REPLs for the hosted agents
This folder holds small **client** console apps that connect to the **server** samples in the
sibling `Hosted-*` folders. Each `Hosted-*` project is an agent you host (locally with
`dotnet run` or deployed to Foundry); the projects here are the thing that *talks* to them.
## Why these exist
A hosted Foundry agent is an HTTP server, not a chat UI. It exposes only the per-agent OpenAI
endpoint shape that the platform routes to:
```
{FOUNDRY_PROJECT_ENDPOINT}/agents/{AZURE_AI_AGENT_NAME}/endpoint/protocols/openai
```
There is no built-in console to poke it with. To actually exercise an agent — send a prompt,
watch it call its tools, read the streamed answer — you need a client that builds a
`FoundryAgent` against that endpoint and drives a conversation. That is all these REPLs do:
1. Read `FOUNDRY_PROJECT_ENDPOINT` + `AZURE_AI_AGENT_NAME` from the environment.
2. Derive the per-agent OpenAI endpoint URL.
3. `AIProjectClient(...).AsAIAgent(agentEndpoint)``FoundryAgent`.
4. Loop: read a line, `RunStreamingAsync`, print the streamed reply.
The client is deliberately dumb. It knows nothing about tools, files, toolboxes, or auth — all
of that is the hosted agent's concern on the server side. Swapping which agent you chat with is
just a matter of changing `AZURE_AI_AGENT_NAME`.
## Local HTTP dev
When the target is a local `http://localhost:8088` dev server, the REPLs install a small
`HttpSchemeRewritePolicy`: `AIProjectClient`/`BearerTokenPolicy` require HTTPS, so the client
presents the endpoint as `https://` to satisfy the TLS check, then rewrites the scheme back to
`http://` right before the request hits the wire. This is local-development only.
## The clients
| Client | What it targets | Notes |
|---|---|---|
| [`SimpleAgent/`](./SimpleAgent/) | Any hosted agent | Generic, agent-agnostic REPL. Point it at any `Hosted-*` server via `AZURE_AI_AGENT_NAME`. Used by `Hosted-Toolbox`, `Hosted-Toolbox-AuthPaths`, and `Hosted-McpTools`. |
| [`SessionFilesClient/`](./SessionFilesClient/) | [`Hosted-Files`](../Hosted-Files/) | Same shape as `SimpleAgent`, framed around the bundled-files demo. |
## Configuration (common to all clients)
```env
FOUNDRY_PROJECT_ENDPOINT=https://<host>/api/projects/<project>
AZURE_AI_AGENT_NAME=<registered-server-side-agent-name>
```
Both are required. Authenticate with `az login` before running. See each client's own README for
its end-to-end walkthrough.
@@ -0,0 +1,121 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ClientModel.Primitives;
using Azure.AI.Projects;
using Azure.Identity;
using DotNetEnv;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Foundry;
// Load .env file if present (for local development)
Env.TraversePath().Load();
// FOUNDRY_PROJECT_ENDPOINT is the Foundry project endpoint. Shape:
// https://<host>/api/projects/<project>
Uri projectEndpoint = new(Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set."));
// AZURE_AI_AGENT_NAME is the registered server-side agent name.
string agentName = Environment.GetEnvironmentVariable("AZURE_AI_AGENT_NAME")
?? throw new InvalidOperationException("AZURE_AI_AGENT_NAME is not set.");
// Derive the per-agent OpenAI endpoint that hosted Foundry agents require.
Uri agentEndpoint = new($"{projectEndpoint}/agents/{agentName}/endpoint/protocols/openai");
// ── Create an agent-framework agent backed by the remote agent endpoint ──────
var options = new AIProjectClientOptions();
if (projectEndpoint.Scheme == "http")
{
// For local HTTP dev: tell AIProjectClient the endpoint is HTTPS (to satisfy
// BearerTokenPolicy's TLS check), then swap the scheme back to HTTP right
// before the request hits the wire.
projectEndpoint = new UriBuilder(projectEndpoint) { Scheme = "https" }.Uri;
agentEndpoint = new UriBuilder(agentEndpoint) { Scheme = "https" }.Uri;
options.AddPolicy(new HttpSchemeRewritePolicy(), PipelinePosition.BeforeTransport);
}
var aiProjectClient = new AIProjectClient(projectEndpoint, new AzureCliCredential(), options);
FoundryAgent agent = aiProjectClient.AsAIAgent(agentEndpoint);
AgentSession session = await agent.CreateSessionAsync();
// ── REPL ──────────────────────────────────────────────────────────────────────
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine($"""
══════════════════════════════════════════════════════════
Session Files Client
Connected to: {agentEndpoint}
Try: "Give me the total revenue in the contoso file."
Type a message or 'quit' to exit
""");
Console.ResetColor();
Console.WriteLine();
while (true)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.Write("You> ");
Console.ResetColor();
string? input = Console.ReadLine();
if (string.IsNullOrWhiteSpace(input)) { continue; }
if (input.Equals("quit", StringComparison.OrdinalIgnoreCase)) { break; }
try
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.Write("Agent> ");
Console.ResetColor();
await foreach (var update in agent.RunStreamingAsync(input, session))
{
Console.Write(update);
}
Console.WriteLine();
}
catch (Exception ex)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"Error: {ex.Message}");
Console.ResetColor();
}
Console.WriteLine();
}
Console.WriteLine("Goodbye!");
/// <summary>
/// For Local Development Only
/// Rewrites HTTPS URIs to HTTP right before transport, allowing AIProjectClient
/// to target a local HTTP dev server while satisfying BearerTokenPolicy's TLS check.
/// </summary>
internal sealed class HttpSchemeRewritePolicy : PipelinePolicy
{
public override void Process(PipelineMessage message, IReadOnlyList<PipelinePolicy> pipeline, int currentIndex)
{
RewriteScheme(message);
ProcessNext(message, pipeline, currentIndex);
}
public override async ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList<PipelinePolicy> pipeline, int currentIndex)
{
RewriteScheme(message);
await ProcessNextAsync(message, pipeline, currentIndex).ConfigureAwait(false);
}
private static void RewriteScheme(PipelineMessage message)
{
var uri = message.Request.Uri!;
if (uri.Scheme == Uri.UriSchemeHttps)
{
message.Request.Uri = new UriBuilder(uri) { Scheme = "http" }.Uri;
}
}
}
@@ -0,0 +1,50 @@
# SessionFilesClient
A thin chat REPL that connects to a deployed [`Hosted-Files`](../../Hosted-Files/) agent via `FoundryAgent` and lets you ask questions whose answers come from the files bundled with that agent. Same shape as [`SimpleAgent`](../SimpleAgent/) — point it at an `AGENT_ENDPOINT`, build a `FoundryAgent`, run.
The agent's container-side `ListFiles` and `ReadFile` tools surface the bundled file contents to the model. The client knows nothing about files; that is entirely the agent's concern.
## Prerequisites
- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
- A running [`Hosted-Files`](../../Hosted-Files/) agent (locally via `dotnet run` or deployed to Foundry)
- Azure CLI logged in (`az login`)
## Configuration
```env
FOUNDRY_PROJECT_ENDPOINT=https://<host>/api/projects/<project>
AZURE_AI_AGENT_NAME=hosted-files
```
Both are required. `FOUNDRY_PROJECT_ENDPOINT` is the Foundry project endpoint URL and `AZURE_AI_AGENT_NAME` is the registered server-side agent name. The sample builds the per-agent OpenAI endpoint URL from these.
## Run
```bash
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SessionFilesClient
$env:FOUNDRY_PROJECT_ENDPOINT = "http://localhost:8088/api/projects/local"
$env:AZURE_AI_AGENT_NAME = "hosted-files"
dotnet run
```
## End-to-end demo
With the [`Hosted-Files`](../../Hosted-Files/) agent running:
```text
══════════════════════════════════════════════════════════
Session Files Client
Connected to: http://localhost:8088/
Try: "Give me the total revenue in the contoso file."
Type a message or 'quit' to exit
══════════════════════════════════════════════════════════
You> Give me the total revenue in the contoso file.
Agent> The contoso file reports total revenue of "$1,482.6M".
You> quit
Goodbye!
```
The agent looked at its bundled files via `ListFiles`, picked `contoso_q1_2026_report.txt`, called `ReadFile`, and quoted the figure verbatim. The client only sent a chat prompt.
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
<RootNamespace>SessionFilesClient</RootNamespace>
<AssemblyName>session-files-client</AssemblyName>
<NoWarn>$(NoWarn);NU1605;OPENAI001</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.Projects" />
<PackageReference Include="Azure.Core" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="DotNetEnv" />
<PackageReference Include="System.ClientModel" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,120 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ClientModel.Primitives;
using Azure.AI.Projects;
using Azure.Identity;
using DotNetEnv;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Foundry;
// Load .env file if present (for local development)
Env.TraversePath().Load();
// FOUNDRY_PROJECT_ENDPOINT is the Foundry project endpoint. Shape:
// https://<host>/api/projects/<project>
Uri projectEndpoint = new(Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT")
?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set."));
// AZURE_AI_AGENT_NAME is the registered server-side agent name.
string agentName = Environment.GetEnvironmentVariable("AZURE_AI_AGENT_NAME")
?? throw new InvalidOperationException("AZURE_AI_AGENT_NAME is not set.");
// Derive the per-agent OpenAI endpoint that hosted Foundry agents require.
Uri agentEndpoint = new($"{projectEndpoint}/agents/{agentName}/endpoint/protocols/openai");
// ── Create an agent-framework agent backed by the remote agent endpoint ──────
var options = new AIProjectClientOptions();
if (projectEndpoint.Scheme == "http")
{
// For local HTTP dev: tell AIProjectClient the endpoint is HTTPS (to satisfy
// BearerTokenPolicy's TLS check), then swap the scheme back to HTTP right
// before the request hits the wire.
projectEndpoint = new UriBuilder(projectEndpoint) { Scheme = "https" }.Uri;
agentEndpoint = new UriBuilder(agentEndpoint) { Scheme = "https" }.Uri;
options.AddPolicy(new HttpSchemeRewritePolicy(), PipelinePosition.BeforeTransport);
}
var aiProjectClient = new AIProjectClient(projectEndpoint, new AzureCliCredential(), options);
FoundryAgent agent = aiProjectClient.AsAIAgent(agentEndpoint);
AgentSession session = await agent.CreateSessionAsync();
// ── REPL ──────────────────────────────────────────────────────────────────────
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine($"""
══════════════════════════════════════════════════════════
Simple Agent Sample
Connected to: {agentEndpoint}
Type a message or 'quit' to exit
══════════════════════════════════════════════════════════
""");
Console.ResetColor();
Console.WriteLine();
while (true)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.Write("You> ");
Console.ResetColor();
string? input = Console.ReadLine();
if (string.IsNullOrWhiteSpace(input)) { continue; }
if (input.Equals("quit", StringComparison.OrdinalIgnoreCase)) { break; }
try
{
Console.ForegroundColor = ConsoleColor.Yellow;
Console.Write("Agent> ");
Console.ResetColor();
await foreach (var update in agent.RunStreamingAsync(input, session))
{
Console.Write(update);
}
Console.WriteLine();
}
catch (Exception ex)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"Error: {ex.Message}");
Console.ResetColor();
}
Console.WriteLine();
}
Console.WriteLine("Goodbye!");
/// <summary>
/// For Local Development Only
/// Rewrites HTTPS URIs to HTTP right before transport, allowing AIProjectClient
/// to target a local HTTP dev server while satisfying BearerTokenPolicy's TLS check.
/// </summary>
internal sealed class HttpSchemeRewritePolicy : PipelinePolicy
{
public override void Process(PipelineMessage message, IReadOnlyList<PipelinePolicy> pipeline, int currentIndex)
{
RewriteScheme(message);
ProcessNext(message, pipeline, currentIndex);
}
public override async ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList<PipelinePolicy> pipeline, int currentIndex)
{
RewriteScheme(message);
await ProcessNextAsync(message, pipeline, currentIndex).ConfigureAwait(false);
}
private static void RewriteScheme(PipelineMessage message)
{
var uri = message.Request.Uri!;
if (uri.Scheme == Uri.UriSchemeHttps)
{
message.Request.Uri = new UriBuilder(uri) { Scheme = "http" }.Uri;
}
}
}
@@ -0,0 +1,63 @@
# SimpleAgent
A generic, agent-agnostic chat REPL for any hosted Foundry agent. Point it at a running
`Hosted-*` agent via `AZURE_AI_AGENT_NAME`, and it builds a `FoundryAgent` against that agent's
per-agent OpenAI endpoint and streams replies. This is the shared client that `Hosted-Toolbox`,
`Hosted-Toolbox-AuthPaths`, and `Hosted-McpTools` reference for their end-to-end demos.
It knows nothing about the agent's tools, toolboxes, files, or auth — those are entirely the
server's concern. Changing which agent you chat with is just a different `AZURE_AI_AGENT_NAME`.
See [`../README.md`](../README.md) for why these client REPLs exist at all.
## Prerequisites
- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
- A running hosted agent (any `Hosted-*` sample, locally via `dotnet run` or deployed to Foundry)
- Azure CLI logged in (`az login`)
## Configuration
```env
FOUNDRY_PROJECT_ENDPOINT=https://<host>/api/projects/<project>
AZURE_AI_AGENT_NAME=<registered-server-side-agent-name>
```
Both are required. `FOUNDRY_PROJECT_ENDPOINT` is the Foundry project endpoint URL and
`AZURE_AI_AGENT_NAME` is the registered server-side agent name. The sample builds the per-agent
OpenAI endpoint URL (`{FOUNDRY_PROJECT_ENDPOINT}/agents/{AZURE_AI_AGENT_NAME}/endpoint/protocols/openai`)
from these.
## Run
Against a local Hosted-Toolbox agent listening on `http://localhost:8088`:
```powershell
cd dotnet/samples/04-hosting/FoundryHostedAgents/responses/Using-Samples/SimpleAgent
$env:FOUNDRY_PROJECT_ENDPOINT = "http://localhost:8088/api/projects/local"
$env:AZURE_AI_AGENT_NAME = "hosted-toolbox-agent"
dotnet run
```
When the project endpoint is `http://`, the client presents it as `https://` to satisfy the
bearer-token TLS check, then rewrites the scheme back to `http://` right before transport
(local-development only).
## End-to-end demo
With a hosted agent running:
```text
══════════════════════════════════════════════════════════
Simple Agent Sample
Connected to: https://localhost:8088/api/projects/local/agents/hosted-toolbox-agent/endpoint/protocols/openai
Type a message or 'quit' to exit
══════════════════════════════════════════════════════════
You> What tools do you have available, and what can they do?
Agent> I have the following tools from the toolbox: ...
You> quit
Goodbye!
```
The client only sent a chat prompt; the agent resolved its toolbox tools server-side and answered.
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<CentralPackageTransitivePinningEnabled>false</CentralPackageTransitivePinningEnabled>
<RootNamespace>SimpleAgentClient</RootNamespace>
<AssemblyName>simple-agent-client</AssemblyName>
<NoWarn>$(NoWarn);NU1605;OPENAI001</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.Projects" />
<PackageReference Include="Azure.Core" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="DotNetEnv" />
<PackageReference Include="System.ClientModel" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
</ItemGroup>
</Project>