Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
Expand Up @@ -22,27 +22,12 @@ public AzureOpenAIModelProvider(IHttpClientFactory httpClientFactory, IConfigura
{
_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}";
}
var options = AzureOpenAIModelProviderOptions.FromConfiguration(configuration);
ModelName = options.ModelName;
_url = options.Url;
}

public async Task<string> SendAsync(string requestBody, CancellationToken cancellationToken)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace W365ComputerUseSample.ComputerUse;

internal sealed class AzureOpenAIModelProviderOptions
{
public required string Url { get; init; }

public required string ModelName { get; init; }

public static AzureOpenAIModelProviderOptions FromConfiguration(IConfiguration configuration)
{
var endpoint = configuration["AIServices:AzureOpenAI:Endpoint"]
?? throw new InvalidOperationException("AIServices:AzureOpenAI:Endpoint is required.");

var configuredModelName = configuration["AIServices:AzureOpenAI:ModelName"];
var deploymentName = configuration["AIServices:AzureOpenAI:DeploymentName"];
var modelName = !string.IsNullOrWhiteSpace(configuredModelName)
? configuredModelName
: !string.IsNullOrWhiteSpace(deploymentName)
? deploymentName
: "computer-use-preview";

return new AzureOpenAIModelProviderOptions
{
ModelName = modelName,
Url = $"{endpoint.TrimEnd('/')}/openai/v1/responses",
};
}
}
47 changes: 45 additions & 2 deletions dotnet/w365-computer-use/sample-agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ Create `appsettings.Development.json` (this file is gitignored):
}
```

`DeploymentName` is treated as the model identifier fallback for compatibility with existing local settings. Azure OpenAI requests are sent to the v1 Responses endpoint (`/openai/v1/responses`), not the legacy deployment-style Responses URL. The selected `ModelName` or fallback `DeploymentName` is sent as the request body `model`.
Comment thread
denzelpfeifer marked this conversation as resolved.


**For `gpt-5.4-mini` model:**
```json
{
Expand All @@ -101,7 +104,48 @@ Create `appsettings.Development.json` (this file is gitignored):

### 4. Obtain a bearer token

Get a token with the `McpServers.W365ComputerUse.All` scope for your tenant. See the [Agent 365 MCP Platform docs](https://learn.microsoft.com/en-us/microsoft-agent-365/developer/) for details.
> **Note:** Running locally requires an agent identity. Create an Agent Blueprint with an Agent Identity for local development, then use that identity's client ID and the Agent Blueprint client credentials in the commands below.

#### Get the Windows 365 for Agents MCP token

Use the helper script to get a CUA user token for the MCP server, then set it as `BEARER_TOKEN`:

```powershell
$tenantId = "<tenant-id-or-domain>"
$blueprintClientId = "<agent-blueprint-client-id>"
$blueprintClientSecret = "<agent-blueprint-client-secret>"
$agentClientId = "<agent-identity-client-id>"
$agentUpn = "<agent-upn-from-teams-instance>"
Comment thread
Copilot marked this conversation as resolved.

.\scripts\Get-CuaAgentUserToken.ps1 `
-TenantId $tenantId `
-AgentBlueprintClientId $blueprintClientId `
-AgentBlueprintClientSecret $blueprintClientSecret `
-AgentClientId $agentClientId `
-AgentUsername $agentUpn `
-SetBearerToken `
-InformationAction Continue
```

`-SetBearerToken` assigns the generated token to `$env:BEARER_TOKEN` for the current PowerShell process and writes an informational message. To use a different token audience, pass `-Scope "<scope>"`; by default the script requests `da81128c-e5b5-4f9e-8d89-50d906f107c5/.default`.

The script requests scopes for the Windows 365 for Agents MCP server. For this sample, use the `Tools.ListInvoke.All` scope. The script writes only the access token to stdout, so it can be assigned directly to `$env:BEARER_TOKEN`. See the [Agent 365 MCP Platform docs](https://learn.microsoft.com/en-us/microsoft-agent-365/developer/) for details.
Comment thread
Copilot marked this conversation as resolved.
Outdated

#### Optional: Get a Microsoft Graph token for OneDrive screenshots

This token is optional and is only needed when you want the sample to upload screenshots to OneDrive.

```powershell
Install-Module MSAL.PS -Scope CurrentUser

$token = Get-MsalToken `
-ClientId "<your-app-registration-client-id>" `
-TenantId "organizations" `
-Scopes "https://graph.microsoft.com/Files.ReadWrite" `
-Interactive

$env:GRAPH_TOKEN = $token.AccessToken
```

### 5. Start the MCP Platform server

Expand All @@ -112,7 +156,6 @@ Ensure the MCP Platform is running locally on port 52857, or update the `McpServ
```powershell
cd sample-agent
$env:ASPNETCORE_ENVIRONMENT = "Development"
$env:BEARER_TOKEN = "<your-mcp-platform-token>"
$env:GRAPH_TOKEN = "<optional-graph-token-for-onedrive-upload>"
dotnet run
```
Expand Down
3 changes: 1 addition & 2 deletions dotnet/w365-computer-use/sample-agent/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,7 @@
"DeploymentName": "<<YOUR_DEPLOYMENT_NAME>>",
"ModelName": "",
"Endpoint": "<<YOUR_AZURE_OPENAI_ENDPOINT>>",
"ApiKey": "<<YOUR_API_KEY>>",
"ApiVersion": "2025-04-01-preview"
"ApiKey": "<<YOUR_API_KEY>>"
}
},

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

<#
.SYNOPSIS
Acquires a CUA user access token for an agent using the Entra user_fic flow.

.DESCRIPTION
Requests an application token, exchanges it for an agent identity token, and then
requests a CUA user token from Microsoft Entra ID. By default, the script writes
only the final CUA access token to stdout. Use -SetBearerToken to also assign
the token to $env:BEARER_TOKEN in the current PowerShell process. Use -ShowOid
to decode the final token payload and write the oid claim to the information
stream.

.EXAMPLE
.\Get-CuaAgentUserToken.ps1 -TenantId "contoso.onmicrosoft.com" -AgentBlueprintClientId "00000000-0000-0000-0000-000000000000" -AgentBlueprintClientSecret "secret" -AgentClientId "11111111-1111-1111-1111-111111111111" -AgentUsername "user@contoso.com"
Comment thread
denzelpfeifer marked this conversation as resolved.
Outdated

Writes only the final CUA access token to stdout.

.EXAMPLE
.\Get-CuaAgentUserToken.ps1 -TenantId "contoso.onmicrosoft.com" -AgentBlueprintClientId "00000000-0000-0000-0000-000000000000" -AgentBlueprintClientSecret "secret" -AgentClientId "11111111-1111-1111-1111-111111111111" -AgentUsername "user@contoso.com" -SetBearerToken -InformationAction Continue

Writes the final CUA access token to stdout and assigns it to $env:BEARER_TOKEN
in the current PowerShell process.

.EXAMPLE
.\Get-CuaAgentUserToken.ps1 -TenantId "contoso.onmicrosoft.com" -AgentBlueprintClientId "00000000-0000-0000-0000-000000000000" -AgentBlueprintClientSecret "secret" -AgentClientId "11111111-1111-1111-1111-111111111111" -AgentUsername "user@contoso.com" -ShowOid -InformationAction Continue

Writes the final CUA access token to stdout and writes the token oid claim to the information stream.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$TenantId,
[Parameter(Mandatory = $true)]
[string]$AgentBlueprintClientId,
[Parameter(Mandatory = $true)]
[string]$AgentBlueprintClientSecret,
[Parameter(Mandatory = $true)]
[string]$AgentClientId,
[Parameter(Mandatory = $true)]
[string]$AgentUsername,
[string]$AuthorityHost = "https://login.microsoftonline.com",
[string]$Scope = "da81128c-e5b5-4f9e-8d89-50d906f107c5/.default",
[switch]$SetBearerToken,
Comment thread
denzelpfeifer marked this conversation as resolved.
Outdated
[switch]$ShowOid
)

$ErrorActionPreference = "Stop"

function Assert-RequiredParameter {
param(
[Parameter(Mandatory = $true)]
[string]$Name,

[AllowNull()]
[string]$Value
)

if ([string]::IsNullOrWhiteSpace($Value)) {
throw "Parameter validation failed: -$Name is required."
}
}

function Get-AccessTokenFromResponse {
param(
[Parameter(Mandatory = $true)]
[object]$Response,

[Parameter(Mandatory = $true)]
[string]$StepLabel
)

if ($null -eq $Response -or [string]::IsNullOrWhiteSpace($Response.access_token)) {
throw "$StepLabel failed: token response missing required field 'access_token'."
}

return $Response.access_token
}

function ConvertFrom-Base64Url {
param(
[Parameter(Mandatory = $true)]
[string]$Value
)

$base64 = $Value.Replace("-", "+").Replace("_", "/")
$padding = (4 - ($base64.Length % 4)) % 4
if ($padding -gt 0) {
$base64 = $base64 + ("=" * $padding)
}

$bytes = [Convert]::FromBase64String($base64)
return [Text.Encoding]::UTF8.GetString($bytes)
}
Comment thread
Copilot marked this conversation as resolved.

function Write-OidInformation {
param(
[Parameter(Mandatory = $true)]
[string]$AccessToken
)

try {
$segments = $AccessToken.Split(".")
if ($segments.Count -ne 3) {
Write-Warning "ShowOid decode failed: access token is not a valid JWT (expected 3 segments)."
return
}

$payloadJson = ConvertFrom-Base64Url -Value $segments[1]
$claims = $payloadJson | ConvertFrom-Json
if ([string]::IsNullOrWhiteSpace($claims.oid)) {
Write-Warning "ShowOid decode completed but JWT payload did not contain an 'oid' claim."
return
}

Write-Information $claims.oid
}
catch {
Write-Warning "ShowOid decode failed: $($_.Exception.Message)"
}
}

Assert-RequiredParameter -Name "TenantId" -Value $TenantId
Assert-RequiredParameter -Name "AgentBlueprintClientId" -Value $AgentBlueprintClientId
Assert-RequiredParameter -Name "AgentBlueprintClientSecret" -Value $AgentBlueprintClientSecret
Assert-RequiredParameter -Name "AgentClientId" -Value $AgentClientId
Assert-RequiredParameter -Name "AgentUsername" -Value $AgentUsername
Assert-RequiredParameter -Name "AuthorityHost" -Value $AuthorityHost
Assert-RequiredParameter -Name "Scope" -Value $Scope

$tokenUrl = "$($AuthorityHost.TrimEnd('/'))/$TenantId/oauth2/v2.0/token"
$clientAssertionType = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"

try {
$applicationTokenResponse = Invoke-RestMethod -Method Post -Uri $tokenUrl -ContentType "application/x-www-form-urlencoded" -Body @{
client_id = $AgentBlueprintClientId
scope = "api://AzureADTokenExchange/.default"
grant_type = "client_credentials"
client_secret = $AgentBlueprintClientSecret
fmi_path = $AgentClientId
}
$applicationToken = Get-AccessTokenFromResponse -Response $applicationTokenResponse -StepLabel "Application token request"
}
catch {
throw "Application token request failed: $($_.Exception.Message)"
}

try {
$agentIdentityTokenResponse = Invoke-RestMethod -Method Post -Uri $tokenUrl -ContentType "application/x-www-form-urlencoded" -Body @{
client_id = $AgentClientId
scope = "api://AzureADTokenExchange/.default"
grant_type = "client_credentials"
client_assertion_type = $clientAssertionType
client_assertion = $applicationToken
}
$agentIdentityToken = Get-AccessTokenFromResponse -Response $agentIdentityTokenResponse -StepLabel "Agent identity token request"
}
catch {
throw "Agent identity token request failed: $($_.Exception.Message)"
}

try {
$cuaTokenResponse = Invoke-RestMethod -Method Post -Uri $tokenUrl -ContentType "application/x-www-form-urlencoded" -Body @{
client_id = $AgentClientId
scope = $Scope
grant_type = "user_fic"
client_assertion_type = $clientAssertionType
client_assertion = $applicationToken
username = $AgentUsername
user_federated_identity_credential = $agentIdentityToken
}
$cuaAccessToken = Get-AccessTokenFromResponse -Response $cuaTokenResponse -StepLabel "CUA token request"
}
catch {
throw "CUA token request failed: $($_.Exception.Message)"
}

if ($ShowOid) {
Write-OidInformation -AccessToken $cuaAccessToken
}

if ($SetBearerToken) {
$env:BEARER_TOKEN = $cuaAccessToken
Write-Information "Set `$env:BEARER_TOKEN for the current PowerShell process."
Comment thread
denzelpfeifer marked this conversation as resolved.
}

Write-Output $cuaAccessToken
Comment thread
denzelpfeifer marked this conversation as resolved.
Outdated
Loading