Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
cbdfdba
fix: device code fallback for macOS browser auth and cross-platform U…
sellakumaran Mar 4, 2026
6c8fe1d
fix: address PR review comments and handle Linux xdg_open_failed error
sellakumaran Mar 4, 2026
4da9b55
fix: auto-fix public client flows, add requirements check to blueprin…
sellakumaran Mar 4, 2026
3272376
fix: eliminate double auth on Linux, improve PS module error handling
sellakumaran Mar 4, 2026
2eae60e
fix: add system requirement checks to all setup commands that use Gra…
sellakumaran Mar 4, 2026
35bd6a0
Skip requirements on dry run; clarify browser fallback
sellakumaran Mar 4, 2026
f10f636
fix: skip requirements on dry run, self-correct missing PS modules in…
sellakumaran Mar 4, 2026
1550c0e
fix: extract real JWT from Graph request headers; unify PS auth scope…
sellakumaran Mar 4, 2026
bae2765
fix: extract last stdout line as JWT token; add clean error for missi…
sellakumaran Mar 4, 2026
5b58ab7
fix: unify PS token cache key in RemoveStale and DeployCommand; updat…
sellakumaran Mar 4, 2026
ed2ace6
fix: improve auth prompt UX and clarify custom permissions message
sellakumaran Mar 5, 2026
4fd11c7
fix: retry blueprint SP lookup on Azure AD propagation delay
sellakumaran Mar 5, 2026
ba0e867
fix: fix admin consent polling via MSAL token with Application.Read.All
sellakumaran Mar 5, 2026
c1e2486
fix: fall back to MSAL when PS Connect-MgGraph fails on any platform
sellakumaran Mar 5, 2026
ac390bf
fix: address PR review comments - disposal, logging, exit handling, d…
sellakumaran Mar 5, 2026
538f309
Merge with main.
sellakumaran Mar 5, 2026
1867f3a
fix: address second round of PR review comments
sellakumaran Mar 5, 2026
64b8c2d
chore: add CHANGELOG, NuGet release notes, and review process updates
sellakumaran Mar 5, 2026
cccd810
fix: address PR review comments - StringComparison, PSGallery, encodi…
sellakumaran Mar 5, 2026
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 @@ -414,8 +414,9 @@ private static async Task EnsureMcpInheritablePermissionsAsync(

var resourceAppId = ConfigConstants.GetAgent365ToolsResourceAppId(config.Environment);

// Use custom client app auth for inheritable permissions - Azure CLI doesn't support this operation
var requiredPermissions = new[] { "AgentIdentityBlueprint.UpdateAuthProperties.All", "Application.ReadWrite.All" };
// Use custom client app auth for inheritable permissions - Azure CLI doesn't support this operation.
// Use RequiredPermissionGrantScopes so all callers share the same PS token cache key.
var requiredPermissions = AuthenticationConstants.RequiredPermissionGrantScopes;

var (ok, alreadyExists, err) = await blueprintService.SetInheritablePermissionsAsync(
config.TenantId, config.AgentBlueprintId, resourceAppId, scopes, requiredScopes: requiredPermissions, ct);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,14 +154,20 @@ public static Command CreateCommand(
"--update-endpoint",
description: "Delete the existing messaging endpoint and register a new one with the specified URL");

var skipRequirementsOption = new Option<bool>(
"--skip-requirements",
description: "Skip requirements validation check\n" +
"Use with caution: setup may fail if prerequisites are not met");

command.AddOption(configOption);
command.AddOption(verboseOption);
command.AddOption(dryRunOption);
command.AddOption(skipEndpointRegistrationOption);
command.AddOption(endpointOnlyOption);
command.AddOption(updateEndpointOption);
command.AddOption(skipRequirementsOption);

command.SetHandler(async (config, verbose, dryRun, skipEndpointRegistration, endpointOnly, updateEndpoint) =>
command.SetHandler(async (config, verbose, dryRun, skipEndpointRegistration, endpointOnly, updateEndpoint, skipRequirements) =>
{
// Generate correlation ID at workflow entry point
var correlationId = HttpClientFactory.GenerateCorrelationId();
Expand Down Expand Up @@ -215,6 +221,37 @@ await UpdateEndpointAsync(
return;
}

// Run all requirements checks: system checks (PowerShell modules, Frontier Preview)
// and config checks (Location, ClientApp — includes isFallbackPublicClient auto-fix
// required for device code auth on macOS/Linux/WSL).
// Skip when dryRun is true: ClientAppRequirementCheck can mutate the app registration
// (e.g., set isFallbackPublicClient), which violates dry-run semantics.
if (!skipRequirements && !dryRun)
{
try
{
var requirementsResult = await RequirementsSubcommand.RunRequirementChecksAsync(
RequirementsSubcommand.GetRequirementChecks(clientAppValidator),
setupConfig,
logger,
category: null,
CancellationToken.None);
Comment thread
sellakumaran marked this conversation as resolved.

if (!requirementsResult)
{
logger.LogError("Setup cannot proceed due to the failed requirement checks above. Please fix the issues above and then try again.");
logger.LogError("Use the resolution guidance provided for each failed check.");
ExceptionHandler.ExitWithCleanup(1);
}
}
catch (Exception reqEx)
{
logger.LogError(reqEx, "Requirements check failed with an unexpected error: {Message}", reqEx.Message);
logger.LogError("If you want to bypass requirement validation, rerun this command with the --skip-requirements flag.");
ExceptionHandler.ExitWithCleanup(1);
}
}

if (dryRun)
{
logger.LogInformation("DRY RUN: Create Agent Blueprint");
Expand Down Expand Up @@ -281,7 +318,7 @@ await CreateBlueprintImplementationAsync(
correlationId: correlationId
);

}, configOption, verboseOption, dryRunOption, skipEndpointRegistrationOption, endpointOnlyOption, updateEndpointOption);
}, configOption, verboseOption, dryRunOption, skipEndpointRegistrationOption, endpointOnlyOption, updateEndpointOption, skipRequirementsOption);

return command;
}
Expand Down Expand Up @@ -773,7 +810,9 @@ public static async Task<bool> EnsureDelegatedConsentWithRetriesAsync(
if (string.IsNullOrWhiteSpace(existingServicePrincipalId))
{
logger.LogDebug("Looking up service principal for blueprint...");
var spLookup = await blueprintLookupService.GetServicePrincipalByAppIdAsync(tenantId, existingAppId, ct);
var spLookup = await blueprintLookupService.GetServicePrincipalByAppIdAsync(
tenantId, existingAppId, ct,
scopes: AuthenticationConstants.RequiredPermissionGrantScopes);

if (spLookup.Found)
{
Expand Down Expand Up @@ -1339,29 +1378,35 @@ private static List<string> GetApplicationScopes(Models.Agent365Config setupConf
var applicationScopes = GetApplicationScopes(setupConfig, logger);
bool consentAlreadyExists = false;

// Resolve blueprint SP object ID once — reused by both pre-check and polling.
// servicePrincipalId comes from generated config (persisted on previous runs).
// If absent, look it up using MSAL scopes that include Application.Read.All.
// Without Application.Read.All the az CLI token causes Graph to return empty results silently.
var blueprintSpId = servicePrincipalId;
if (string.IsNullOrWhiteSpace(blueprintSpId))
{
logger.LogDebug("Looking up service principal for blueprint...");
var spLookup = await blueprintLookupService.GetServicePrincipalByAppIdAsync(
tenantId, appId, ct,
scopes: AuthenticationConstants.RequiredPermissionGrantScopes);
blueprintSpId = spLookup.ObjectId;
}

// Only check for existing consent if blueprint already existed
// New blueprints cannot have consent yet, so skip the verification
if (alreadyExisted)
{
logger.LogInformation("Verifying admin consent for application");
logger.LogDebug(" - Application scopes: {Scopes}", string.Join(", ", applicationScopes));

// Check if consent already exists with required scopes
var blueprintSpId = servicePrincipalId;
if (string.IsNullOrWhiteSpace(blueprintSpId))
{
logger.LogDebug("Looking up service principal for blueprint to check consent...");
var spLookup = await blueprintLookupService.GetServicePrincipalByAppIdAsync(tenantId, appId, ct);
blueprintSpId = spLookup.ObjectId;
}

if (!string.IsNullOrWhiteSpace(blueprintSpId))
{
// Get Microsoft Graph service principal ID
// Get Microsoft Graph service principal ID (needs Application.Read.All)
var graphSpId = await graphApiService.LookupServicePrincipalByAppIdAsync(
tenantId,
AuthenticationConstants.MicrosoftGraphResourceAppId,
ct);
ct,
AuthenticationConstants.RequiredPermissionGrantScopes);

if (!string.IsNullOrWhiteSpace(graphSpId))
{
Expand All @@ -1373,7 +1418,8 @@ private static List<string> GetApplicationScopes(Models.Agent365Config setupConf
graphSpId,
applicationScopes,
logger,
ct);
ct,
scopes: AuthenticationConstants.RequiredPermissionGrantScopes);
}
}

Expand Down Expand Up @@ -1428,9 +1474,21 @@ await SetupHelpers.EnsureResourcePermissionsAsync(
logger.LogInformation("Requesting admin consent for application");
logger.LogInformation(" - Application scopes: {Scopes}", string.Join(", ", applicationScopes));
logger.LogInformation("Opening browser for Graph API admin consent...");
TryOpenBrowser(consentUrlGraph);
logger.LogInformation("If the browser does not open automatically, navigate to this URL to grant consent: {ConsentUrl}", consentUrlGraph);
BrowserHelper.TryOpenUrl(consentUrlGraph, logger);

var consentSuccess = await AdminConsentHelper.PollAdminConsentAsync(executor, logger, appId, "Graph API Scopes", 180, 5, ct);
bool consentSuccess;
if (!string.IsNullOrWhiteSpace(blueprintSpId))
{
consentSuccess = await AdminConsentHelper.PollAdminConsentAsync(
graphApiService, logger, tenantId, blueprintSpId,
"Graph API Scopes", 180, 5, ct);
}
else
{
logger.LogDebug("Could not resolve blueprint service principal. Falling back to az rest polling.");
consentSuccess = await AdminConsentHelper.PollAdminConsentAsync(executor, logger, appId, "Graph API Scopes", 180, 5, ct);
}

bool graphInheritablePermissionsConfigured = false;
string? graphInheritablePermissionsError = null;
Expand Down Expand Up @@ -1541,23 +1599,6 @@ private async static Task<GraphServiceClient> GetAuthenticatedGraphClientAsync(I
}
}

private static void TryOpenBrowser(string url)
{
try
{
using var p = new System.Diagnostics.Process();
p.StartInfo = new System.Diagnostics.ProcessStartInfo
{
FileName = url,
UseShellExecute = true
};
p.Start();
}
catch
{
// non-fatal
}
}

/// <summary>
/// Creates client secret for Agent Blueprint (Phase 2.5)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

using Microsoft.Agents.A365.DevTools.Cli.Constants;
using Microsoft.Agents.A365.DevTools.Cli.Exceptions;
using Microsoft.Agents.A365.DevTools.Cli.Helpers;
using Microsoft.Agents.A365.DevTools.Cli.Models;
using Microsoft.Agents.A365.DevTools.Cli.Services;
Expand Down Expand Up @@ -63,7 +64,7 @@ public static Command CreateCommand(
if (string.IsNullOrWhiteSpace(setupConfig.AgentBlueprintId))
{
logger.LogError("Blueprint ID not found. Run 'a365 setup blueprint' first.");
Environment.Exit(1);
ExceptionHandler.ExitWithCleanup(1);
}

// Configure GraphApiService with custom client app ID if available
Expand All @@ -72,6 +73,20 @@ public static Command CreateCommand(
graphApiService.CustomClientAppId = setupConfig.ClientAppId;
}

// Verify system requirements (PowerShell modules are required for Graph operations).
// Skipped in dry-run: PowerShellModulesRequirementCheck can auto-install modules,
// which would be a side effect in a mode that is supposed to be non-mutating.
if (!dryRun)
{
var systemChecksOk = await RequirementsSubcommand.RunRequirementChecksAsync(
RequirementsSubcommand.GetSystemRequirementChecks(), setupConfig, logger, category: null, CancellationToken.None);
if (!systemChecksOk)
{
logger.LogError("Setup cannot proceed due to failed requirement checks above. Please fix the issues and retry.");
ExceptionHandler.ExitWithCleanup(1);
}
Comment thread
sellakumaran marked this conversation as resolved.
}

if (dryRun)
{
logger.LogInformation("DRY RUN: Configure CopilotStudio Permissions");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ private static Command CreateMcpSubcommand(
if (string.IsNullOrWhiteSpace(setupConfig.AgentBlueprintId))
{
logger.LogError("Blueprint ID not found. Run 'a365 setup blueprint' first.");
Environment.Exit(1);
ExceptionHandler.ExitWithCleanup(1);
}

// Configure GraphApiService with custom client app ID if available
Expand All @@ -85,6 +85,20 @@ private static Command CreateMcpSubcommand(
graphApiService.CustomClientAppId = setupConfig.ClientAppId;
}

// Verify system requirements (PowerShell modules are required for Graph operations).
// Skipped in dry-run: PowerShellModulesRequirementCheck can auto-install modules,
// which would be a side effect in a mode that is supposed to be non-mutating.
if (!dryRun)
{
var mcpSystemChecksOk = await RequirementsSubcommand.RunRequirementChecksAsync(
RequirementsSubcommand.GetSystemRequirementChecks(), setupConfig, logger, category: null, CancellationToken.None);
if (!mcpSystemChecksOk)
{
logger.LogError("Setup cannot proceed due to failed requirement checks above. Please fix the issues and retry.");
ExceptionHandler.ExitWithCleanup(1);
}
Comment thread
sellakumaran marked this conversation as resolved.
}

if (dryRun)
{
// Read scopes from ToolingManifest.json
Expand Down Expand Up @@ -154,7 +168,7 @@ private static Command CreateBotSubcommand(
if (string.IsNullOrWhiteSpace(setupConfig.AgentBlueprintId))
{
logger.LogError("Blueprint ID not found. Run 'a365 setup blueprint' first.");
Environment.Exit(1);
ExceptionHandler.ExitWithCleanup(1);
}

// Configure GraphApiService with custom client app ID if available
Expand All @@ -163,6 +177,20 @@ private static Command CreateBotSubcommand(
graphApiService.CustomClientAppId = setupConfig.ClientAppId;
}

// Verify system requirements (PowerShell modules are required for Graph operations).
// Skipped in dry-run: PowerShellModulesRequirementCheck can auto-install modules,
// which would be a side effect in a mode that is supposed to be non-mutating.
if (!dryRun)
{
var botSystemChecksOk = await RequirementsSubcommand.RunRequirementChecksAsync(
RequirementsSubcommand.GetSystemRequirementChecks(), setupConfig, logger, category: null, CancellationToken.None);
if (!botSystemChecksOk)
{
logger.LogError("Setup cannot proceed due to failed requirement checks above. Please fix the issues and retry.");
ExceptionHandler.ExitWithCleanup(1);
}
}

if (dryRun)
{
logger.LogInformation("DRY RUN: Configure Bot API Permissions");
Expand Down Expand Up @@ -228,7 +256,7 @@ private static Command CreateCustomSubcommand(
if (string.IsNullOrWhiteSpace(setupConfig.AgentBlueprintId))
{
logger.LogError("Blueprint ID not found. Run 'a365 setup blueprint' first.");
Environment.Exit(1);
ExceptionHandler.ExitWithCleanup(1);
}

// Configure GraphApiService with custom client app ID if available
Expand All @@ -237,6 +265,20 @@ private static Command CreateCustomSubcommand(
graphApiService.CustomClientAppId = setupConfig.ClientAppId;
}

// Verify system requirements (PowerShell modules are required for Graph operations).
// Skipped in dry-run: PowerShellModulesRequirementCheck can auto-install modules,
// which would be a side effect in a mode that is supposed to be non-mutating.
if (!dryRun)
{
var customSystemChecksOk = await RequirementsSubcommand.RunRequirementChecksAsync(
RequirementsSubcommand.GetSystemRequirementChecks(), setupConfig, logger, category: null, CancellationToken.None);
if (!customSystemChecksOk)
{
logger.LogError("Setup cannot proceed due to failed requirement checks above. Please fix the issues and retry.");
ExceptionHandler.ExitWithCleanup(1);
}
}

if (dryRun)
{
logger.LogInformation("DRY RUN: Configure Custom Blueprint Permissions");
Expand Down Expand Up @@ -466,7 +508,10 @@ private static async Task RemoveStaleCustomPermissionsAsync(
AuthenticationConstants.MicrosoftGraphResourceAppId,
};

var requiredPermissions = new[] { "AgentIdentityBlueprint.UpdateAuthProperties.All", "Application.ReadWrite.All" };
// Must match RequiredPermissionGrantScopes exactly so the PowerShell token acquired
// for inheritable permissions is reused (same cache key) rather than triggering
// a second Connect-MgGraph prompt.
var requiredPermissions = AuthenticationConstants.RequiredPermissionGrantScopes;

List<(string ResourceAppId, List<string> Scopes)> currentPermissions;
try
Expand Down Expand Up @@ -614,7 +659,7 @@ await RemoveStaleCustomPermissionsAsync(

if (setupConfig.CustomBlueprintPermissions == null || setupConfig.CustomBlueprintPermissions.Count == 0)
{
logger.LogInformation("No custom blueprint permissions configured.");
logger.LogInformation("No custom blueprint permissions specified in config. Skipping.");
await configService.SaveStateAsync(setupConfig);
return true;
}
Expand Down
Loading