Skip to content
Merged
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
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",
};
}
}
53 changes: 48 additions & 5 deletions dotnet/w365-computer-use/sample-agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Response to User
- `computer-use-preview` or `gpt-5.4` / `gpt-5.4-mini`
- [Request access to gpt-5.4](https://aka.ms/OAI/gpt54access) if needed
- Access to the W365 Computer Use MCP server (via [Agent 365 MCP Platform](https://learn.microsoft.com/en-us/microsoft-agent-365/developer/))
- A bearer token with `McpServers.W365ComputerUse.All` scope
- A bearer token with `Tools.ListInvoke.All` scope

## Setup

Expand Down 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 = Read-Host "Agent Blueprint client secret" -AsSecureString
$blueprintClientSecretPlainText = [System.Net.NetworkCredential]::new("", $blueprintClientSecret).Password
$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 $blueprintClientSecretPlainText `
-AgentClientId $agentClientId `
-AgentUsername $agentUpn `
-InformationAction Continue
```

The script 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.

#### 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 All @@ -131,7 +174,7 @@ dotnet run
| `AIServices:Provider` | Model provider | `AzureOpenAI` |
| `AIServices:AzureOpenAI:Endpoint` | Azure OpenAI resource URL | - |
| `AIServices:AzureOpenAI:ApiKey` | API key | - |
| `AIServices:AzureOpenAI:DeploymentName` | Deployment name (for deployment-based URLs) | `computer-use-preview` |
| `AIServices:AzureOpenAI:DeploymentName` | Backward-compatible model identifier fallback when `ModelName` is not set | `computer-use-preview` |
| `AIServices:AzureOpenAI:ModelName` | Model name (for model-based URLs, e.g., `gpt-5.4-mini`) | - |
| `McpServer:Url` | MCP server URL (dev only; omit for production) | - |
| `W365:GatewayUrl` | W365 Computer Use MCP gateway URL (production) | `https://agent365.svc.cloud.microsoft/agents/servers/mcp_W365ComputerUse` |
Expand All @@ -141,7 +184,7 @@ dotnet run
| `Screenshots:LocalPath` | Local path to save screenshots | `./Screenshots` |
| `Screenshots:OneDriveFolder` | OneDrive folder for screenshot upload | `CUA-Sessions` |
| `Screenshots:OneDriveUserId` | UPN/email to upload screenshots to a specific user's OneDrive (instead of token owner) | - |
| `BEARER_TOKEN` (env var) | MCP Platform token with `McpServers.W365ComputerUse.All` scope (dev only) | - |
| `BEARER_TOKEN` (env var) | MCP Platform token with `Tools.ListInvoke.All` scope (dev only) | - |
| `GRAPH_TOKEN` (env var) | Graph API token with `Files.ReadWrite` scope for OneDrive upload (dev only) | - |

## Supported Models
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,192 @@
# 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 assigns
the final CUA access 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 "<agent-blueprint-client-secret>" -AgentClientId "11111111-1111-1111-1111-111111111111" -AgentUsername "user@contoso.com"

Assigns the final CUA access token to $env:BEARER_TOKEN in the current
PowerShell process.

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

Assigns the final CUA access token to $env:BEARER_TOKEN in the current
PowerShell process.

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

Assigns the final CUA access token to $env:BEARER_TOKEN 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 = $true,
[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 [System.Text.Encoding]::UTF8.GetString($bytes)
}

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.
}
else {
Write-Information "Token acquired. `$env:BEARER_TOKEN was not set because -SetBearerToken was false."
}
Loading