Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions dotnet/w365-computer-use/W365ComputerUseSample.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.14.36623.8
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "W365ComputerUseSample", "sample-agent\W365ComputerUseSample.csproj", "{B72D1A3E-4F8C-9E56-A1B2-C3D4E5F60789}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{B72D1A3E-4F8C-9E56-A1B2-C3D4E5F60789}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B72D1A3E-4F8C-9E56-A1B2-C3D4E5F60789}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B72D1A3E-4F8C-9E56-A1B2-C3D4E5F60789}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B72D1A3E-4F8C-9E56-A1B2-C3D4E5F60789}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D4E5F6A7-B8C9-0D1E-2F3A-4B5C6D7E8F90}
EndGlobalSection
EndGlobal
6 changes: 6 additions & 0 deletions dotnet/w365-computer-use/sample-agent/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
appsettings.Development.json
Screenshots/
a365.config.json
a365.generated.config.json
app.zip
publish/
346 changes: 346 additions & 0 deletions dotnet/w365-computer-use/sample-agent/Agent/MyAgent.cs

Large diffs are not rendered by default.

170 changes: 170 additions & 0 deletions dotnet/w365-computer-use/sample-agent/AspNetExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.Agents.Authentication;
using Microsoft.Agents.Core;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
using Microsoft.IdentityModel.Validators;
using System.Collections.Concurrent;
using System.Globalization;
using System.IdentityModel.Tokens.Jwt;

namespace W365ComputerUseSample;

public static class AspNetExtensions
{
private static readonly ConcurrentDictionary<string, ConfigurationManager<OpenIdConnectConfiguration>> _openIdMetadataCache = new();

public static void AddAgentAspNetAuthentication(this IServiceCollection services, IConfiguration configuration, string tokenValidationSectionName = "TokenValidation")
{
IConfigurationSection tokenValidationSection = configuration.GetSection(tokenValidationSectionName);

if (!tokenValidationSection.Exists() || !tokenValidationSection.GetValue("Enabled", true))
{
System.Diagnostics.Trace.WriteLine("AddAgentAspNetAuthentication: Auth disabled");
return;
}

services.AddAgentAspNetAuthentication(tokenValidationSection.Get<TokenValidationOptions>()!);
}

public static void AddAgentAspNetAuthentication(this IServiceCollection services, TokenValidationOptions validationOptions)
{
AssertionHelpers.ThrowIfNull(validationOptions, nameof(validationOptions));

if (validationOptions.Audiences == null || validationOptions.Audiences.Count == 0)
{
throw new ArgumentException($"{nameof(TokenValidationOptions)}:Audiences requires at least one ClientId");
}

foreach (var audience in validationOptions.Audiences)
{
if (!Guid.TryParse(audience, out _))
{
throw new ArgumentException($"{nameof(TokenValidationOptions)}:Audiences values must be a GUID");
}
}

if (validationOptions.ValidIssuers == null || validationOptions.ValidIssuers.Count == 0)
{
validationOptions.ValidIssuers =
[
"https://api.botframework.com",
"https://sts.windows.net/d6d49420-f39b-4df7-a1dc-d59a935871db/",
"https://login.microsoftonline.com/d6d49420-f39b-4df7-a1dc-d59a935871db/v2.0",
"https://sts.windows.net/f8cdef31-a31e-4b4a-93e4-5f571e91255a/",
"https://login.microsoftonline.com/f8cdef31-a31e-4b4a-93e4-5f571e91255a/v2.0",
"https://sts.windows.net/69e9b82d-4842-4902-8d1e-abc5b98a55e8/",
"https://login.microsoftonline.com/69e9b82d-4842-4902-8d1e-abc5b98a55e8/v2.0",
];

if (!string.IsNullOrEmpty(validationOptions.TenantId) && Guid.TryParse(validationOptions.TenantId, out _))
{
validationOptions.ValidIssuers.Add(string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ValidTokenIssuerUrlTemplateV1, validationOptions.TenantId));
validationOptions.ValidIssuers.Add(string.Format(CultureInfo.InvariantCulture, AuthenticationConstants.ValidTokenIssuerUrlTemplateV2, validationOptions.TenantId));
}
}

if (string.IsNullOrEmpty(validationOptions.AzureBotServiceOpenIdMetadataUrl))
{
validationOptions.AzureBotServiceOpenIdMetadataUrl = validationOptions.IsGov ? AuthenticationConstants.GovAzureBotServiceOpenIdMetadataUrl : AuthenticationConstants.PublicAzureBotServiceOpenIdMetadataUrl;
}

if (string.IsNullOrEmpty(validationOptions.OpenIdMetadataUrl))
{
validationOptions.OpenIdMetadataUrl = validationOptions.IsGov ? AuthenticationConstants.GovOpenIdMetadataUrl : AuthenticationConstants.PublicOpenIdMetadataUrl;
}

var openIdMetadataRefresh = validationOptions.OpenIdMetadataRefresh ?? BaseConfigurationManager.DefaultAutomaticRefreshInterval;

_ = services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(5),
ValidIssuers = validationOptions.ValidIssuers,
ValidAudiences = validationOptions.Audiences,
ValidateIssuerSigningKey = true,
RequireSignedTokens = true,
};

options.TokenValidationParameters.EnableAadSigningKeyIssuerValidation();

options.Events = new JwtBearerEvents
{
OnMessageReceived = async context =>
{
string authorizationHeader = context.Request.Headers.Authorization.ToString();

if (string.IsNullOrEmpty(authorizationHeader))
{
context.Options.TokenValidationParameters.ConfigurationManager ??= options.ConfigurationManager as BaseConfigurationManager;
await Task.CompletedTask.ConfigureAwait(false);
return;
}

string[] parts = authorizationHeader?.Split(' ')!;
if (parts.Length != 2 || parts[0] != "Bearer")
{
context.Options.TokenValidationParameters.ConfigurationManager ??= options.ConfigurationManager as BaseConfigurationManager;
await Task.CompletedTask.ConfigureAwait(false);
return;
}

JwtSecurityToken token = new(parts[1]);
string issuer = token.Claims.FirstOrDefault(claim => claim.Type == AuthenticationConstants.IssuerClaim)?.Value!;

if (validationOptions.AzureBotServiceTokenHandling && AuthenticationConstants.BotFrameworkTokenIssuer.Equals(issuer))
{
context.Options.TokenValidationParameters.ConfigurationManager = _openIdMetadataCache.GetOrAdd(validationOptions.AzureBotServiceOpenIdMetadataUrl, key =>
{
return new ConfigurationManager<OpenIdConnectConfiguration>(validationOptions.AzureBotServiceOpenIdMetadataUrl, new OpenIdConnectConfigurationRetriever(), new HttpClient())
{
AutomaticRefreshInterval = openIdMetadataRefresh
};
});
}
else
{
context.Options.TokenValidationParameters.ConfigurationManager = _openIdMetadataCache.GetOrAdd(validationOptions.OpenIdMetadataUrl, key =>
{
return new ConfigurationManager<OpenIdConnectConfiguration>(validationOptions.OpenIdMetadataUrl, new OpenIdConnectConfigurationRetriever(), new HttpClient())
{
AutomaticRefreshInterval = openIdMetadataRefresh
};
});
}

await Task.CompletedTask.ConfigureAwait(false);
},
OnTokenValidated = context => Task.CompletedTask,
OnForbidden = context => Task.CompletedTask,
OnAuthenticationFailed = context => Task.CompletedTask
};
});
}

public class TokenValidationOptions
{
public IList<string>? Audiences { get; set; }
public string? TenantId { get; set; }
public IList<string>? ValidIssuers { get; set; }
public bool IsGov { get; set; } = false;
public string? AzureBotServiceOpenIdMetadataUrl { get; set; }
public string? OpenIdMetadataUrl { get; set; }
public bool AzureBotServiceTokenHandling { get; set; } = true;
public TimeSpan? OpenIdMetadataRefresh { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Text;

namespace W365ComputerUseSample.ComputerUse;

/// <summary>
/// Sends CUA model requests to Azure OpenAI using an API key.
/// This is the default provider for external customers.
/// </summary>
public class AzureOpenAIModelProvider : ICuaModelProvider
{
private readonly HttpClient _httpClient;
private readonly string _url;
private readonly string _apiKey;
private readonly ILogger<AzureOpenAIModelProvider> _logger;

public string ModelName { get; }

public AzureOpenAIModelProvider(IHttpClientFactory httpClientFactory, IConfiguration configuration, ILogger<AzureOpenAIModelProvider> logger)
{
_httpClient = httpClientFactory.CreateClient("WebClient");
_logger = logger;
var endpoint = configuration["AIServices:AzureOpenAI:Endpoint"]
?? throw new InvalidOperationException("AIServices:AzureOpenAI:Endpoint is required.");
_apiKey = configuration["AIServices:AzureOpenAI:ApiKey"]
?? throw new InvalidOperationException("AIServices:AzureOpenAI:ApiKey is required.");
var apiVersion = configuration["AIServices:AzureOpenAI:ApiVersion"] ?? "2025-04-01-preview";

// DeploymentName = deployment-based URL; ModelName = model-based URL (model sent in body)
var deploymentName = configuration["AIServices:AzureOpenAI:DeploymentName"];
ModelName = configuration["AIServices:AzureOpenAI:ModelName"]
?? deploymentName
?? "computer-use-preview";

if (!string.IsNullOrEmpty(deploymentName))
{
_url = $"{endpoint.TrimEnd('/')}/openai/deployments/{deploymentName}/responses?api-version={apiVersion}";
}
else
{
// Model-based endpoint — model name goes in the request body, not the URL
_url = $"{endpoint.TrimEnd('/')}/openai/responses?api-version={apiVersion}";
}
}

public async Task<string> SendAsync(string requestBody, CancellationToken cancellationToken)
{
_logger.LogInformation("Azure OpenAI request URL: {Url}", _url);
using var req = new HttpRequestMessage(HttpMethod.Post, _url);
req.Headers.Add("api-key", _apiKey);
req.Content = new StringContent(requestBody, Encoding.UTF8, "application/json");

var resp = await _httpClient.SendAsync(req, cancellationToken);
if (!resp.IsSuccessStatusCode)
{
var err = await resp.Content.ReadAsStringAsync(cancellationToken);
throw new HttpRequestException($"Azure OpenAI returned {resp.StatusCode}: {err}");
}

return await resp.Content.ReadAsStringAsync(cancellationToken);
}
}
Loading
Loading