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,23 @@
// Copyright (c) Microsoft. All rights reserved.
using Harness.Shared.Console.ToolFormatters;
using Microsoft.Extensions.AI;
namespace SampleApp;
/// <summary>
/// Formats <c>DownloadUri</c> tool calls, showing the target URI.
/// </summary>
public sealed class DownloadUriToolFormatter : ToolCallFormatter
{
/// <inheritdoc/>
public override bool CanFormat(FunctionCallContent call) =>
call.Name is "DownloadUri";
/// <inheritdoc/>
public override string? FormatDetail(FunctionCallContent call)
{
string? value = GetStringArgumentValue(call, "uri");
return value is not null ? $"({value})" : null;
}
}
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net10.0</TargetFrameworks>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.Identity" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Foundry\Microsoft.Agents.AI.Foundry.csproj" />
<ProjectReference Include="..\..\..\..\src\Microsoft.Agents.AI.Harness\Microsoft.Agents.AI.Harness.csproj" />
<ProjectReference Include="..\Harness_Shared_Console\Harness_Shared_Console.csproj" />
<ProjectReference Include="..\Harness_Shared_Console_OpenAI\Harness_Shared_Console_OpenAI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,129 @@
// Copyright (c) Microsoft. All rights reserved.
// This sample demonstrates how to use a HarnessAgent for interactive research tasks.
// The HarnessAgent comes pre-configured with TodoProvider, AgentModeProvider, FileMemoryProvider,
// ToolApproval, WebSearch, and OpenTelemetry — so this sample only needs custom instructions
// and a WebBrowsingTool.
// The agent plans research tasks, creates a todo list, gets user approval,
// and then executes each step — all within an interactive conversation loop.
//
// Special commands:
// /todos — Display the current todo list without invoking the agent.
// /mode — Get or set the current agent mode.
// /exit — End the session.
#pragma warning disable OPENAI001 // Suppress experimental API warnings for Responses API usage.
#pragma warning disable MAAI001 // Suppress experimental API warnings for Agents AI experiments.
using System.ClientModel.Primitives;
using Azure.AI.Projects;
using Azure.Identity;
using Harness.Shared.Console;
using Harness.Shared.Console.OpenAI;
using Harness.Shared.Console.ToolFormatters;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using SampleApp;
var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4";
const int MaxContextWindowTokens = 1_050_000;
const int MaxOutputTokens = 128_000;
const string TracingSourceName = "Harness.Research";
// Set up OpenTelemetry tracing that writes spans to a text file.
// This captures all agent activity (tool calls, model invocations, compaction, etc.)
// as well as HTTP requests made by the underlying HttpClient transport.
using var tracerProvider = HarnessTracing.CreateFileTracerProvider(TracingSourceName);
// Create a HarnessAgent with the Harness providers (TodoProvider and AgentModeProvider)
// and research-focused instructions including the mandatory planning workflow.
var instructions =
"""
## Research Assistant Instructions
You are a research assistant. When given a research topic, research it thoroughly using web search and web browsing.
Use your knowledge to form good search queries and hypotheses, but always verify claims with the tools available to you rather than relying on memory alone.
### Research quality
Consult multiple sources when possible and cross-reference key claims.
When sources disagree, note the discrepancy and explain which source you consider more reliable and why.
If a web page fails to load or a search returns irrelevant results, try alternative search queries or sources before moving on.
Track your sources you will need them when presenting results.
### Presenting results
When presenting your final findings:
- Use Markdown formatting for clarity.
- Use clear sections with headings for each major topic or sub-question.
- Cite your sources inline (e.g., "According to [source name](URL), ...").
- End with a brief summary of key takeaways.
- In addition to returning the results to the user, save the final research report to file memory so it survives compaction and can be referenced later.
""";
// Create the agent using AsHarnessAgent, which pre-configures function invocation,
// per-service-call chat history persistence, in-loop compaction, TodoProvider, AgentModeProvider,
// FileMemoryProvider, ToolApproval, WebSearch, AgentSkillsProvider, and OpenTelemetry.
// Only custom instructions, a WebBrowsingTool, and FileAccess opt-out are needed.
AIAgent agent =
// Create an OpenAIClient that communicates with the Foundry responses service.
new AIProjectClient(
new Uri(endpoint),
// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
new DefaultAzureCredential(),
new AIProjectClientOptions { RetryPolicy = new ClientRetryPolicy(3) }) // Enable retries to improve resiliency.
.GetProjectOpenAIClient()
.GetResponsesClient()
.AsIChatClient(deploymentName)
.AsHarnessAgent(new HarnessAgentOptions
{
MaxContextWindowTokens = MaxContextWindowTokens,
MaxOutputTokens = MaxOutputTokens,
Name = "ResearchAgent",
Description = "A research assistant that plans and executes research tasks.",
DisableFileAccess = true, // If enabled, this would allow the agent to read/write files in a working directory
OpenTelemetrySourceName = TracingSourceName, // Use our custom source name so spans are captured by the TracerProvider above.
FileMemoryStore = new FileSystemAgentFileStore( // Configure the file memory provider to store files in a local folder called "agent-files".
Path.Combine(AppContext.BaseDirectory, "agent-files")),
// The built in ModeProvider has two default modes: "plan" and "execute".
// Adding a loop evaluator so that in "execute" mode, the harness keeps re-invoking itself until every todo item is complete.
LoopEvaluators =
[
new TodoCompletionLoopEvaluator(new TodoCompletionLoopEvaluatorOptions { Modes = ["execute"] }),
],
LoopAgentOptions = new LoopAgentOptions { MaxIterations = 10 }, // Safety cap on the number of autonomous passes per turn.
ChatOptions = new ChatOptions
{
Instructions = instructions,
Tools =
[
new WebBrowsingTool( // Add a local web browsing tool that converts html to markdown.
new WebBrowsingToolOptions { AllowPublicNetworks = true }),
],
MaxOutputTokens = MaxOutputTokens, // Set a high token limit for long research tasks with many tool calls and long outputs.
Reasoning = new() { Effort = ReasoningEffort.Medium },
},
});
// Run the interactive console session using the shared HarnessConsole helper.
await HarnessConsole.RunAgentAsync(
agent,
userPrompt: "Enter a research topic to get started.",
new HarnessConsoleOptions
{
Observers = [
new OpenAIResponsesWebSearchDisplayObserver(),
new OpenAIResponsesErrorObserver(),
.. HarnessConsoleOptions.BuildObserversWithPlanning(
agent,
planModeName: "plan",
executionModeName: "execute",
maxContextWindowTokens: MaxContextWindowTokens,
maxOutputTokens: MaxOutputTokens,
toolFormatters: [new DownloadUriToolFormatter(), .. ToolCallFormatter.BuildDefaultToolFormatters()])],
CommandHandlers = HarnessConsoleOptions.BuildDefaultCommandHandlers(agent),
});
@@ -0,0 +1,54 @@
# What this sample demonstrates
This sample demonstrates how to use a `HarnessAgent` with the Harness `AIContextProviders` (`TodoProvider` and `AgentModeProvider`) for interactive research tasks with web search capabilities powered by Azure AI Foundry. The `HarnessAgent` pre-configures function invocation, per-service-call chat history persistence, and context-window compaction.
Key features showcased:
- **HarnessAgent** — a pre-configured agent that wraps a `ChatClientAgent` with function invocation, per-service-call persistence, and context-window compaction
- **ToolApproval** — the agent is wrapped with `UseToolApproval()` to allow auto-approving tools once confirmed
- **Web Search** — the agent can search the web for current information via `ResponseTool.CreateWebSearchTool()`
- **TodoProvider** — the agent creates and manages a todo list to track research questions
- **AgentModeProvider** — the agent switches between "plan" mode (breaking down the topic) and "execute" mode (answering each research question)
- **TodoCompletionLoopEvaluator** — in "execute" mode the agent loops automatically, re-invoking itself until every todo item is complete (capped by `LoopAgentOptions.MaxIterations`). The loop is scoped to "execute" mode, so "plan" mode stays interactive. The `HarnessAgent` wraps itself in a `LoopAgent` automatically whenever `LoopEvaluators` is supplied.
- **Interactive conversation** — you can review the agent's plan, provide feedback, and approve before execution begins
- **Streaming output** — responses are streamed token-by-token for a natural experience
- **`/todos` command** — view the current todo list at any time without invoking the agent
- **Mode-based coloring** — console output is colored based on the agent's current mode (cyan for plan, green for execute)
## Prerequisites
Before running this sample, ensure you have:
1. An Azure AI Foundry project with a deployed model (e.g., `gpt-5.4`)
2. Azure CLI installed and authenticated (`az login`)
## Environment Variables
Set the following environment variables:
```bash
# Required: Your Azure AI Foundry OpenAI endpoint
export AZURE_FOUNDRY_OPENAI_ENDPOINT="https://your-project.services.ai.azure.com/openai/v1/"
# Optional: Model deployment name (defaults to gpt-5.4)
export FOUNDRY_MODEL="gpt-5.4"
```
## Running the Sample
```bash
cd dotnet
dotnet run --project samples/02-agents/Harness/Harness_Step01_Research
```
## What to Expect
The sample starts an interactive conversation loop. You can:
1. **Enter a research topic** — the agent will analyze it and create a plan with todos
2. **Review and adjust** — provide feedback on the plan, ask for changes, or approve it
3. **Type `/todos`** — to see the current todo list at any time
4. **Watch execution** — once approved, the agent will switch to "execute" mode and process each todo autonomously until the whole plan is complete
5. **Type `exit`** — to end the session
The prompt and agent output are colored by the current mode: **cyan** during planning, **green** during execution.
@@ -0,0 +1,439 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel;
using System.Net;
using System.Net.Sockets;
using System.Text.Json;
using System.Text.RegularExpressions;
using Microsoft.Extensions.AI;
namespace SampleApp;
/// <summary>
/// An AI function that downloads HTML pages and converts them to markdown.
/// Access is controlled by <see cref="WebBrowsingToolOptions"/> — by default, no hosts are accessible.
/// </summary>
internal sealed partial class WebBrowsingTool : AIFunction
{
private static readonly HttpClient s_httpClient = new();
private readonly AIFunction _inner;
private readonly WebBrowsingToolOptions _options;
/// <summary>
/// Initializes a new instance of the <see cref="WebBrowsingTool"/> class.
/// </summary>
/// <param name="options">Options controlling which URLs are permitted. By default, no hosts are accessible.</param>
public WebBrowsingTool(WebBrowsingToolOptions options)
{
this._options = options ?? throw new ArgumentNullException(nameof(options));
this._inner = AIFunctionFactory.Create(this.DownloadUriAsync);
}
/// <inheritdoc/>
public override string Name => this._inner.Name;
/// <inheritdoc/>
public override string Description => this._inner.Description;
/// <inheritdoc/>
public override JsonElement JsonSchema => this._inner.JsonSchema;
/// <inheritdoc/>
protected override ValueTask<object?> InvokeCoreAsync(
AIFunctionArguments arguments,
CancellationToken cancellationToken) =>
this._inner.InvokeAsync(arguments, cancellationToken);
[Description("Fetch the html from the given url as markdown")]
private async Task<string> DownloadUriAsync(
[Description("The URL to download")] string uri,
CancellationToken cancellationToken = default)
{
if (!Uri.TryCreate(uri, UriKind.Absolute, out Uri? parsedUri))
{
return $"Error: '{uri}' is not a valid URL.";
}
if (parsedUri.Scheme is not "http" and not "https")
{
return $"Error: Only HTTP and HTTPS URLs are supported. Got: '{parsedUri.Scheme}'.";
}
// Check access policy.
string? accessError = await this.CheckAccessAsync(parsedUri, cancellationToken);
if (accessError is not null)
{
return accessError;
}
try
{
string html = await s_httpClient.GetStringAsync(parsedUri, cancellationToken);
return HtmlToMarkdownConverter.Convert(html);
}
catch (HttpRequestException ex)
{
return $"Error downloading {uri}: {ex.Message}";
}
}
/// <summary>
/// Checks whether the given URI is permitted by the configured access policy.
/// Returns null if allowed, or an error message string if blocked.
/// </summary>
private async Task<string?> CheckAccessAsync(Uri uri, CancellationToken cancellationToken)
{
string host = uri.Host;
// 1. Check AllowedHosts.
if (this._options.AllowedHosts is { Count: > 0 } allowedHosts)
{
foreach (string pattern in allowedHosts)
{
if (HostMatchesPattern(host, pattern))
{
return null; // Allowed by explicit host list.
}
}
}
// 2. Short-circuit when the policy is guaranteed to block.
if (!this._options.AllowPublicNetworks &&
!this._options.AllowPrivateNetworks &&
!this._options.AllowAllHosts)
{
return $"Error: Access to '{host}' is blocked by the current access policy. Configure WebBrowsingToolOptions to allow access.";
}
// 3. Resolve DNS to determine if the host is public or private.
IPAddress[] addresses;
try
{
addresses = await Dns.GetHostAddressesAsync(host, cancellationToken);
}
catch (SocketException)
{
return $"Error: Could not resolve host '{host}'.";
}
if (addresses.Length == 0)
{
return $"Error: Could not resolve host '{host}'.";
}
bool isPrivate = Array.Exists(addresses, IsPrivateAddress);
// 4. If public and AllowPublicNetworks is true → allow.
if (!isPrivate && this._options.AllowPublicNetworks)
{
return null;
}
// 5. If private and AllowPrivateNetworks is true → allow.
if (isPrivate && this._options.AllowPrivateNetworks)
{
return null;
}
// 6. If AllowAllHosts is true → allow.
if (this._options.AllowAllHosts)
{
return null;
}
// 7. Block.
string networkType = isPrivate ? "private/internal network" : "public network";
return $"Error: Access to '{host}' is blocked. The host resolves to a {networkType} address and the current access policy does not permit this. " +
"Configure WebBrowsingToolOptions to allow access.";
}
/// <summary>
/// Checks whether a host matches a pattern. Supports exact match and wildcard prefix (e.g., "*.example.com").
/// </summary>
private static bool HostMatchesPattern(string host, string pattern)
{
if (string.Equals(host, pattern, StringComparison.OrdinalIgnoreCase))
{
return true;
}
// Wildcard prefix: "*.example.com" matches "sub.example.com" and "a.b.example.com".
if (pattern.StartsWith("*.", StringComparison.Ordinal))
{
string suffix = pattern[1..]; // ".example.com"
return host.EndsWith(suffix, StringComparison.OrdinalIgnoreCase);
}
return false;
}
/// <summary>
/// Determines whether an IP address is private, loopback, or link-local.
/// </summary>
private static bool IsPrivateAddress(IPAddress address)
{
if (address.IsIPv4MappedToIPv6)
{
address = address.MapToIPv4();
}
if (IPAddress.IsLoopback(address))
{
return true;
}
if (address.AddressFamily == AddressFamily.InterNetwork)
{
byte[] bytes = address.GetAddressBytes();
return bytes[0] switch
{
10 => true, // 10.0.0.0/8
172 => bytes[1] >= 16 && bytes[1] <= 31, // 172.16.0.0/12
192 => bytes[1] == 168, // 192.168.0.0/16
169 => bytes[1] == 254, // 169.254.0.0/16 (link-local + metadata)
_ => false
};
}
if (address.AddressFamily == AddressFamily.InterNetworkV6)
{
// fe80::/10 (link-local) or fc00::/7 (unique local).
byte[] bytes = address.GetAddressBytes();
if (bytes[0] == 0xfe && (bytes[1] & 0xc0) == 0x80)
{
return true; // Link-local
}
if ((bytes[0] & 0xfe) == 0xfc)
{
return true; // Unique local
}
}
return false;
}
/// <summary>
/// A simple HTML to Markdown converter using regex-based transformations.
/// Handles the most common HTML elements without requiring external dependencies.
/// </summary>
private static partial class HtmlToMarkdownConverter
{
public static string Convert(string html)
{
// Extract body content if present, otherwise use the full HTML.
var bodyMatch = BodyRegex().Match(html);
string content = bodyMatch.Success ? bodyMatch.Groups[1].Value : html;
// Remove script, style, and head blocks.
content = ScriptRegex().Replace(content, string.Empty);
content = StyleRegex().Replace(content, string.Empty);
content = HeadRegex().Replace(content, string.Empty);
content = CommentRegex().Replace(content, string.Empty);
// Convert block elements before inline elements.
content = ConvertHeadings(content);
content = ConvertCodeBlocks(content);
content = ConvertBlockquotes(content);
content = ConvertLists(content);
content = ConvertHorizontalRules(content);
// Convert inline elements.
content = ConvertLinks(content);
content = ConvertImages(content);
content = ConvertBold(content);
content = ConvertItalic(content);
content = ConvertInlineCode(content);
// Convert structural elements.
content = ConvertParagraphs(content);
content = ConvertLineBreaks(content);
// Strip remaining HTML tags.
content = StripTagsRegex().Replace(content, string.Empty);
// Decode HTML entities.
content = WebUtility.HtmlDecode(content);
// Clean up excessive whitespace.
content = ExcessiveNewlinesRegex().Replace(content, "\n\n");
return content.Trim();
}
private static string ConvertHeadings(string html)
{
html = H1Regex().Replace(html, m => $"\n# {StripInnerTags(m.Groups[1].Value).Trim()}\n");
html = H2Regex().Replace(html, m => $"\n## {StripInnerTags(m.Groups[1].Value).Trim()}\n");
html = H3Regex().Replace(html, m => $"\n### {StripInnerTags(m.Groups[1].Value).Trim()}\n");
html = H4Regex().Replace(html, m => $"\n#### {StripInnerTags(m.Groups[1].Value).Trim()}\n");
html = H5Regex().Replace(html, m => $"\n##### {StripInnerTags(m.Groups[1].Value).Trim()}\n");
html = H6Regex().Replace(html, m => $"\n###### {StripInnerTags(m.Groups[1].Value).Trim()}\n");
return html;
}
private static string ConvertLinks(string html) =>
LinkRegex().Replace(html, m =>
{
string href = m.Groups[1].Value;
string text = StripInnerTags(m.Groups[2].Value).Trim();
// Skip javascript and data links.
if (href.StartsWith("javascript:", StringComparison.OrdinalIgnoreCase) ||
href.StartsWith("data:", StringComparison.OrdinalIgnoreCase))
{
return text;
}
return string.IsNullOrWhiteSpace(text) ? string.Empty : $"[{text}]({href})";
});
private static string ConvertImages(string html) =>
ImageRegex().Replace(html, m =>
{
string src = m.Groups[1].Value;
string alt = m.Groups[2].Value;
// Truncate data URIs.
if (src.StartsWith("data:", StringComparison.OrdinalIgnoreCase))
{
src = src.Split(',')[0] + "...";
}
return $"![{alt}]({src})";
});
private static string ConvertBold(string html) =>
BoldRegex().Replace(html, m => $"**{m.Groups[2].Value}**");
private static string ConvertItalic(string html) =>
ItalicRegex().Replace(html, m => $"*{m.Groups[2].Value}*");
private static string ConvertInlineCode(string html) =>
InlineCodeRegex().Replace(html, m => $"`{m.Groups[1].Value}`");
private static string ConvertCodeBlocks(string html) =>
CodeBlockRegex().Replace(html, m => $"\n```\n{StripInnerTags(m.Groups[1].Value).Trim()}\n```\n");
private static string ConvertBlockquotes(string html) =>
BlockquoteRegex().Replace(html, m =>
{
string inner = StripInnerTags(m.Groups[1].Value).Trim();
// Prefix each line with "> ".
string quoted = string.Join("\n", inner.Split('\n').Select(line => $"> {line.Trim()}"));
return $"\n{quoted}\n";
});
private static string ConvertLists(string html)
{
// Unordered lists.
html = UlRegex().Replace(html, m =>
{
string items = LiRegex().Replace(m.Groups[1].Value, li => $"- {StripInnerTags(li.Groups[1].Value).Trim()}\n");
return $"\n{items}";
});
// Ordered lists.
html = OlRegex().Replace(html, m =>
{
int index = 1;
string items = LiRegex().Replace(m.Groups[1].Value, li => $"{index++}. {StripInnerTags(li.Groups[1].Value).Trim()}\n");
return $"\n{items}";
});
return html;
}
private static string ConvertHorizontalRules(string html) =>
HrRegex().Replace(html, "\n---\n");
private static string ConvertParagraphs(string html) =>
ParagraphRegex().Replace(html, m => $"\n\n{m.Groups[1].Value}\n\n");
private static string ConvertLineBreaks(string html) =>
BrRegex().Replace(html, "\n");
private static string StripInnerTags(string html) =>
StripTagsRegex().Replace(html, string.Empty);
// Source-generated regex patterns for performance and AOT compatibility.
[GeneratedRegex(@"<body[^>]*>(.*?)</body>", RegexOptions.Singleline | RegexOptions.IgnoreCase)]
private static partial Regex BodyRegex();
[GeneratedRegex(@"<script[^>]*>.*?</script>", RegexOptions.Singleline | RegexOptions.IgnoreCase)]
private static partial Regex ScriptRegex();
[GeneratedRegex(@"<style[^>]*>.*?</style>", RegexOptions.Singleline | RegexOptions.IgnoreCase)]
private static partial Regex StyleRegex();
[GeneratedRegex(@"<head[^>]*>.*?</head>", RegexOptions.Singleline | RegexOptions.IgnoreCase)]
private static partial Regex HeadRegex();
[GeneratedRegex(@"<!--.*?-->", RegexOptions.Singleline)]
private static partial Regex CommentRegex();
[GeneratedRegex(@"<h1[^>]*>(.*?)</h1>", RegexOptions.Singleline | RegexOptions.IgnoreCase)]
private static partial Regex H1Regex();
[GeneratedRegex(@"<h2[^>]*>(.*?)</h2>", RegexOptions.Singleline | RegexOptions.IgnoreCase)]
private static partial Regex H2Regex();
[GeneratedRegex(@"<h3[^>]*>(.*?)</h3>", RegexOptions.Singleline | RegexOptions.IgnoreCase)]
private static partial Regex H3Regex();
[GeneratedRegex(@"<h4[^>]*>(.*?)</h4>", RegexOptions.Singleline | RegexOptions.IgnoreCase)]
private static partial Regex H4Regex();
[GeneratedRegex(@"<h5[^>]*>(.*?)</h5>", RegexOptions.Singleline | RegexOptions.IgnoreCase)]
private static partial Regex H5Regex();
[GeneratedRegex(@"<h6[^>]*>(.*?)</h6>", RegexOptions.Singleline | RegexOptions.IgnoreCase)]
private static partial Regex H6Regex();
[GeneratedRegex(@"<a\s[^>]*href=[""']([^""']*)[""'][^>]*>(.*?)</a>", RegexOptions.Singleline | RegexOptions.IgnoreCase)]
private static partial Regex LinkRegex();
[GeneratedRegex(@"<img\s[^>]*src=[""']([^""']*)[""'][^>]*?(?:alt=[""']([^""']*)[""'])?[^>]*/?>", RegexOptions.Singleline | RegexOptions.IgnoreCase)]
private static partial Regex ImageRegex();
[GeneratedRegex(@"<(strong|b)\b[^>]*>(.*?)</\1>", RegexOptions.Singleline | RegexOptions.IgnoreCase)]
private static partial Regex BoldRegex();
[GeneratedRegex(@"<(em|i)\b[^>]*>(.*?)</\1>", RegexOptions.Singleline | RegexOptions.IgnoreCase)]
private static partial Regex ItalicRegex();
[GeneratedRegex(@"<code[^>]*>(.*?)</code>", RegexOptions.Singleline | RegexOptions.IgnoreCase)]
private static partial Regex InlineCodeRegex();
[GeneratedRegex(@"<pre[^>]*>(.*?)</pre>", RegexOptions.Singleline | RegexOptions.IgnoreCase)]
private static partial Regex CodeBlockRegex();
[GeneratedRegex(@"<blockquote[^>]*>(.*?)</blockquote>", RegexOptions.Singleline | RegexOptions.IgnoreCase)]
private static partial Regex BlockquoteRegex();
[GeneratedRegex(@"<ul[^>]*>(.*?)</ul>", RegexOptions.Singleline | RegexOptions.IgnoreCase)]
private static partial Regex UlRegex();
[GeneratedRegex(@"<ol[^>]*>(.*?)</ol>", RegexOptions.Singleline | RegexOptions.IgnoreCase)]
private static partial Regex OlRegex();
[GeneratedRegex(@"<li[^>]*>(.*?)</li>", RegexOptions.Singleline | RegexOptions.IgnoreCase)]
private static partial Regex LiRegex();
[GeneratedRegex(@"<hr\s*/?>", RegexOptions.IgnoreCase)]
private static partial Regex HrRegex();
[GeneratedRegex(@"<p[^>]*>(.*?)</p>", RegexOptions.Singleline | RegexOptions.IgnoreCase)]
private static partial Regex ParagraphRegex();
[GeneratedRegex(@"<br\s*/?>", RegexOptions.IgnoreCase)]
private static partial Regex BrRegex();
[GeneratedRegex(@"<[^>]+>")]
private static partial Regex StripTagsRegex();
[GeneratedRegex(@"\n{3,}")]
private static partial Regex ExcessiveNewlinesRegex();
}
}
@@ -0,0 +1,60 @@
// Copyright (c) Microsoft. All rights reserved.
namespace SampleApp;
/// <summary>
/// Options that control which URLs the <see cref="WebBrowsingTool"/> is permitted to access.
/// </summary>
/// <remarks>
/// <para>
/// By default, <b>no hosts are accessible</b>. You must explicitly opt in to one or more
/// of the access modes below. The validation order is:
/// </para>
/// <list type="number">
/// <item><description>If the host matches an entry in <see cref="AllowedHosts"/>, the request is allowed.</description></item>
/// <item><description>If the resolved IP is a public address and <see cref="AllowPublicNetworks"/> is <see langword="true"/>, the request is allowed.</description></item>
/// <item><description>If the resolved IP is a private/loopback/link-local address and <see cref="AllowPrivateNetworks"/> is <see langword="true"/>, the request is allowed.</description></item>
/// <item><description>If <see cref="AllowAllHosts"/> is <see langword="true"/>, the request is allowed.</description></item>
/// <item><description>Otherwise, the request is blocked.</description></item>
/// </list>
/// </remarks>
internal sealed class WebBrowsingToolOptions
{
/// <summary>
/// Gets or sets a list of host patterns that are always permitted, regardless of other settings.
/// Patterns support wildcard prefix matching (e.g., <c>"*.example.com"</c> matches <c>"docs.example.com"</c>).
/// Exact host names (e.g., <c>"docs.microsoft.com"</c>) are also supported.
/// </summary>
/// <remarks>This has the highest priority — if a host matches, it is allowed immediately.</remarks>
public IReadOnlyList<string>? AllowedHosts { get; set; }
/// <summary>
/// Gets or sets a value indicating whether public internet hosts (non-private, non-loopback, non-link-local IPs) are permitted.
/// Default is <see langword="false"/>.
/// </summary>
public bool AllowPublicNetworks { get; set; }
/// <summary>
/// Gets or sets a value indicating whether private network hosts are permitted.
/// This includes RFC 1918 addresses (10.x.x.x, 172.16-31.x.x, 192.168.x.x),
/// loopback (127.x.x.x, ::1), link-local (169.254.x.x, fe80::),
/// and cloud metadata endpoints (169.254.169.254).
/// Default is <see langword="false"/>.
/// </summary>
/// <remarks>
/// <b>Warning:</b> Enabling this allows the agent to make requests to internal services,
/// localhost, and cloud metadata endpoints. Only enable this if you understand the SSRF risks.
/// </remarks>
public bool AllowPrivateNetworks { get; set; }
/// <summary>
/// Gets or sets a value indicating whether all hosts are permitted without any restriction.
/// Default is <see langword="false"/>.
/// </summary>
/// <remarks>
/// <b>⚠️ UNSAFE:</b> Enabling this disables all network boundary checks and allows the agent
/// to access any URL, including internal services, cloud metadata endpoints, and localhost.
/// Only use this for trusted, isolated environments where SSRF is not a concern.
/// </remarks>
public bool AllowAllHosts { get; set; }
}