Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
0c6c974
fix: improve non-admin setup flow with self-healing permissions and a…
sellakumaran Mar 16, 2026
76983b1
feat: batch permissions orchestrator for non-admin setup flow
sellakumaran Mar 17, 2026
fa2c0c7
fix: address PR review comments and align setup summary with batch flow
sellakumaran Mar 17, 2026
9aa34a4
fix: use MSAL/WAM as primary Graph token path to fix cross-user conta…
sellakumaran Mar 18, 2026
e80c293
fix: address Copilot PR review comments
sellakumaran Mar 18, 2026
f8c6908
Improve changelog, auth flows, and admin consent handling
sellakumaran Mar 18, 2026
a270a89
fix: correct user identity for ATG and Graph token acquisition
sellakumaran Mar 18, 2026
b5be53e
fix: update test override signature for CreateBrowserCredential login…
sellakumaran Mar 19, 2026
4820d73
fix: address remaining Copilot PR review comments (#2-5)
sellakumaran Mar 19, 2026
40fe88f
fix: resolve Agent ID Admin setup failures for WAM auth, owner assign…
sellakumaran Mar 19, 2026
1576643
Support login hint for MSAL Graph token acquisition
sellakumaran Mar 19, 2026
f37d1aa
fix: pass login hint to blueprint httpClient token to prevent WAM cro…
sellakumaran Mar 19, 2026
80bf137
fix: address Copilot review comments on token cache, log levels, and …
sellakumaran Mar 19, 2026
3b0aa2f
fix: consent URL Graph-only scopes, SP retry, transitiveMemberOf role…
sellakumaran Mar 19, 2026
8027051
chore: remove docs/plans from version control (internal working docum…
sellakumaran Mar 19, 2026
4cc1c30
feat: add a365 setup admin command for Global Administrator OAuth2 gr…
sellakumaran Mar 20, 2026
6a19be9
feat: add consent URL generation, fix SP retry, and improve setup output
sellakumaran Mar 20, 2026
7430bff
Improve admin consent URLs, retry logic, and testability
sellakumaran Mar 20, 2026
960517a
fix: Copilot review comments, GA role detection, combined consent URL…
sellakumaran Mar 20, 2026
3dc1e2e
perf: eliminate real process/network/delay costs in test paths
sellakumaran Mar 21, 2026
a688b6a
Improve auth reliability, logging, and test rigor
sellakumaran Mar 21, 2026
da6f750
perf: eliminate repeated az CLI subprocess spawns across setup phases
sellakumaran Mar 22, 2026
5b05e37
Refactor infra/client validation: direct ARM/Graph HTTP
sellakumaran Mar 22, 2026
9366a5f
Add --field option to config command and standardize endpoint key
sellakumaran Mar 22, 2026
78a70e0
Handle PR comments
sellakumaran Mar 22, 2026
8af956b
Fix PR comments.
sellakumaran Mar 22, 2026
7a3b4b7
Add sovereign cloud support and stderr filtering improvements
sellakumaran Mar 24, 2026
6c117db
Improve cancellation, exit handling, and test isolation
sellakumaran Mar 24, 2026
60fd2e0
Use dynamic GraphBaseUrl for scope URL construction
sellakumaran Mar 24, 2026
c6cdaad
Refactor AzCliHelper process handling; clarify test comment
sellakumaran Mar 24, 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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- `a365 publish` updates manifest IDs, creates `manifest.zip`, and prints concise upload instructions for Microsoft 365 Admin Center (Agents > All agents > Upload custom agent). Interactive prompts only occur in interactive terminals; redirect stdin to suppress them in scripts.

### Fixed
- `a365 cleanup` now uses correct Graph scopes for blueprint deletion (`AgentIdentityBlueprint.DeleteRestore.All`) and federated credential deletion (`AgentIdentityBlueprint.AddRemoveCreds.All`); the previous scopes (`AgentIdentityBlueprint.ReadWrite.All` and `Application.ReadWrite.All`) no longer allow write operations to Agent ID entities per a breaking change in the permissions model
- Token cache now isolates per-user so a cached token from one account is not reused when a different account runs a subsequent command
Comment thread
sellakumaran marked this conversation as resolved.
Outdated
- macOS/Linux: device code fallback when browser authentication is unavailable (#309)
- Linux: MSAL fallback when PowerShell `Connect-MgGraph` fails in non-TTY environments (#309)
- Admin consent polling no longer times out after 180s — blueprint service principal now resolved with correct MSAL token (#309)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public static Command CreateCommand(

// Add subcommands
command.AddCommand(RequirementsSubcommand.CreateCommand(
logger, configService, authValidator, clientAppValidator));
logger, configService, authValidator, clientAppValidator, executor));

command.AddCommand(InfrastructureSubcommand.CreateCommand(
logger, configService, authValidator, platformDetector, executor));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,32 +231,18 @@ await RequirementsSubcommand.RunChecksOrExitAsync(
blueprintService,
blueprintLookupService,
federatedCredentialService,
skipEndpointRegistration: false,
correlationId: correlationId
);
skipEndpointRegistration: true,
correlationId: correlationId,
options: new BlueprintCreationOptions(DeferConsent: true));

setupResults.BlueprintCreated = result.BlueprintCreated;
setupResults.BlueprintAlreadyExisted = result.BlueprintAlreadyExisted;
setupResults.MessagingEndpointRegistered = result.EndpointRegistered;
setupResults.EndpointAlreadyExisted = result.EndpointAlreadyExisted;

if (result.EndpointAlreadyExisted)
{
setupResults.Warnings.Add("Messaging endpoint already exists (not newly created)");
}

// If endpoint registration was attempted but failed, add to errors
// Do NOT add error if registration was skipped (--no-endpoint or missing config)
if (result.EndpointRegistrationAttempted && !result.EndpointRegistered)
{
setupResults.Errors.Add("Messaging endpoint registration failed");
}

// Track Graph permissions status - critical for agent token exchange
setupResults.GraphPermissionsConfigured = result.GraphPermissionsConfigured;
// Graph permissions and admin consent are deferred to the batch orchestrator
// (DeferConsent: true above). Flags are updated in Step 4 after the orchestrator runs.
if (result.GraphInheritablePermissionsFailed)
{
setupResults.GraphInheritablePermissionsError = result.GraphInheritablePermissionsError
setupResults.GraphInheritablePermissionsError = result.GraphInheritablePermissionsError
?? "Microsoft Graph inheritable permissions failed to configure";
setupResults.Warnings.Add($"Microsoft Graph inheritable permissions: {setupResults.GraphInheritablePermissionsError}");
}
Expand All @@ -265,6 +251,14 @@ await RequirementsSubcommand.RunChecksOrExitAsync(
setupResults.GraphInheritablePermissionsConfigured = true;
}

// Track Federated Identity Credential status
setupResults.FederatedCredentialConfigured = result.FederatedCredentialConfigured;
if (!result.FederatedCredentialConfigured && !string.IsNullOrWhiteSpace(result.FederatedCredentialError))
{
setupResults.FederatedCredentialError = result.FederatedCredentialError;
setupResults.Warnings.Add($"Federated Identity Credential: {result.FederatedCredentialError}");
}

if (!result.BlueprintCreated)
{
throw new GraphApiException(
Expand All @@ -275,8 +269,8 @@ await RequirementsSubcommand.RunChecksOrExitAsync(

// CRITICAL: Wait for file system to ensure config file is fully written
// Blueprint creation writes directly to disk and may not be immediately readable
logger.LogInformation("Ensuring configuration file is synchronized...");
await Task.Delay(2000); // 2 second delay to ensure file write is complete
logger.LogDebug("Waiting for config file write to complete...");
await Task.Delay(2000);
Comment thread
sellakumaran marked this conversation as resolved.
Outdated

// Reload config to get blueprint ID
// Use full path to ensure we're reading from the correct location
Expand Down Expand Up @@ -308,85 +302,116 @@ await RequirementsSubcommand.RunChecksOrExitAsync(
throw;
}

// Step 3: MCP Permissions
// Step 3: Configure all permissions (Graph + MCP + Bot x3 + Custom) in a single batch.
// Phase 1 — update blueprint requiredResourceAccess + resolve SPs once (non-admin).
// Phase 2 — create OAuth2 grants and inheritable permissions (non-admin).
// Phase 3 — single admin consent browser or one consolidated URL for non-admins.
try
{
bool mcpPermissionSetup = await PermissionsSubcommand.ConfigureMcpPermissionsAsync(
config.FullName,
logger,
configService,
executor,
graphApiService,
blueprintService,
setupConfig,
true,
setupResults);

setupResults.McpPermissionsConfigured = mcpPermissionSetup;
if (mcpPermissionSetup)
// Pre-step: remove stale custom permissions before building the spec list.
var desiredCustomIds = new HashSet<string>(
(setupConfig.CustomBlueprintPermissions ?? new List<CustomResourcePermission>())
.Select(p => p.ResourceAppId),
StringComparer.OrdinalIgnoreCase);
await PermissionsSubcommand.RemoveStaleCustomPermissionsAsync(
logger, graphApiService, blueprintService, setupConfig, desiredCustomIds, CancellationToken.None);
Comment thread
sellakumaran marked this conversation as resolved.
Outdated

// Build combined spec list.
var mcpManifestPath = Path.Combine(
setupConfig.DeploymentProjectPath ?? string.Empty,
McpConstants.ToolingManifestFileName);
var mcpScopes = await PermissionsSubcommand.ReadMcpScopesAsync(mcpManifestPath, logger);
var mcpResourceAppId = ConfigConstants.GetAgent365ToolsResourceAppId(setupConfig.Environment);

var specs = new List<ResourcePermissionSpec>
{
setupResults.InheritablePermissionsConfigured = setupConfig.IsInheritanceConfigured();
new ResourcePermissionSpec(
AuthenticationConstants.MicrosoftGraphResourceAppId,
"Microsoft Graph",
setupConfig.AgentApplicationScopes.ToArray(),
SetInheritable: true),
new ResourcePermissionSpec(
mcpResourceAppId,
"Agent 365 Tools",
mcpScopes,
SetInheritable: true),
new ResourcePermissionSpec(
ConfigConstants.MessagingBotApiAppId,
"Messaging Bot API",
new[] { "Authorization.ReadWrite", "user_impersonation" },
SetInheritable: true),
new ResourcePermissionSpec(
ConfigConstants.ObservabilityApiAppId,
"Observability API",
new[] { "user_impersonation" },
SetInheritable: true),
new ResourcePermissionSpec(
PowerPlatformConstants.PowerPlatformApiResourceAppId,
"Power Platform API",
new[] { "Connectivity.Connections.Read" },
SetInheritable: true),
};

foreach (var customPerm in setupConfig.CustomBlueprintPermissions ?? new List<CustomResourcePermission>())
{
var (isValid, _) = customPerm.Validate();
if (isValid && !string.IsNullOrWhiteSpace(customPerm.ResourceAppId))
{
var resourceName = string.IsNullOrWhiteSpace(customPerm.ResourceName)
? customPerm.ResourceAppId
: customPerm.ResourceName;
specs.Add(new ResourcePermissionSpec(
customPerm.ResourceAppId,
resourceName,
customPerm.Scopes.ToArray(),
SetInheritable: true));
}
}
}
catch (Exception mcpPermEx)
{
setupResults.McpPermissionsConfigured = false;
setupResults.Errors.Add($"MCP Permissions: {mcpPermEx.Message}");
logger.LogWarning("MCP permissions failed: {Message}. Setup will continue, but MCP server permissions must be configured manually", mcpPermEx.Message);
}

// Step 4: Bot API Permissions
try
{
bool botPermissionSetup = await PermissionsSubcommand.ConfigureBotPermissionsAsync(
config.FullName,
logger,
configService,
executor,
setupConfig,
graphApiService,
blueprintService,
true,
setupResults);
var (blueprintPermissionsUpdated, inheritedPermissionsConfigured, consentGranted, adminConsentUrl) =
await BatchPermissionsOrchestrator.ConfigureAllPermissionsAsync(
graphApiService, blueprintService, setupConfig,
setupConfig.AgentBlueprintId!, setupConfig.TenantId,
specs, logger, setupResults, CancellationToken.None,
Comment thread
sellakumaran marked this conversation as resolved.
Outdated
knownBlueprintSpObjectId: setupConfig.AgentBlueprintServicePrincipalObjectId);
Comment thread
sellakumaran marked this conversation as resolved.

setupResults.BotApiPermissionsConfigured = botPermissionSetup;
if (botPermissionSetup)
{
setupResults.BotInheritablePermissionsConfigured = setupConfig.IsBotInheritanceConfigured();
}
setupResults.BatchPermissionsPhase1Completed = blueprintPermissionsUpdated;
setupResults.BatchPermissionsPhase2Completed = inheritedPermissionsConfigured;
setupResults.AdminConsentGranted = consentGranted;
setupResults.AdminConsentUrl = adminConsentUrl;

await configService.SaveStateAsync(setupConfig);
}
catch (Exception botPermEx)
catch (Exception permEx)
{
setupResults.BotApiPermissionsConfigured = false;
setupResults.Errors.Add($"Bot API Permissions: {botPermEx.Message}");
logger.LogWarning("Bot permissions failed: {Message}. Setup will continue, but Bot API permissions must be configured manually", botPermEx.Message);
setupResults.BatchPermissionsPhase2Completed = false;
setupResults.AdminConsentGranted = false;
setupResults.Errors.Add($"Permissions: {permEx.Message}");
logger.LogWarning("Permissions configuration failed: {Message}. Setup will continue, but permissions must be configured manually.", permEx.Message);
}

// Step 5: Reconcile custom blueprint permissions — apply desired and remove stale entries.
// Always run (even when config is empty) to clean up any permissions no longer in config.
// Step 4: Register messaging endpoint — runs after blueprint is fully configured with permissions.
logger.LogInformation("");
logger.LogInformation("Registering blueprint messaging endpoint...");
try
{
bool customPermissionsSetup = await PermissionsSubcommand.ConfigureCustomPermissionsAsync(
config.FullName,
logger,
configService,
executor,
graphApiService,
blueprintService,
setupConfig,
true,
setupResults);
var (endpointSuccess, endpointAlreadyExisted) =
await SetupHelpers.RegisterBlueprintMessagingEndpointAsync(
setupConfig, logger, botConfigurator, correlationId: correlationId);

setupResults.CustomPermissionsConfigured = customPermissionsSetup;
setupResults.MessagingEndpointRegistered = endpointSuccess;
setupResults.EndpointAlreadyExisted = endpointAlreadyExisted;
}
catch (Exception customPermEx)
catch (Exception endpointEx)
{
setupResults.CustomPermissionsConfigured = false;
setupResults.Errors.Add($"Custom Blueprint Permissions: {customPermEx.Message}");
logger.LogWarning("Custom permissions failed: {Message}. Setup will continue, but custom permissions must be configured manually", customPermEx.Message);
setupResults.MessagingEndpointRegistered = false;
setupResults.Errors.Add($"Messaging endpoint registration failed: {endpointEx.Message}");
logger.LogWarning("Endpoint registration failed: {Message}", endpointEx.Message);
logger.LogWarning("To retry after resolving the issue: a365 setup blueprint --endpoint-only");
}

// Display setup summary
// Display verification URLs and setup summary
await SetupHelpers.DisplayVerificationInfoAsync(config, logger);
logger.LogInformation("");
SetupHelpers.DisplaySetupSummary(setupResults, logger);
}
Expand Down
Loading
Loading