Skip to content

Commit 315b526

Browse files
committed
Refactor MOS prerequisite setup for idempotency & clarity
- Split MOS prerequisite logic into idempotent steps: check, create service principals, configure permissions, and grant admin consent - Streamline error and mitigation messages for clarity and Azure CLI style output - Centralize MOS permission constants in MosConstants for maintainability - Store MOS token cache securely in user home directory with proper permissions - Make GraphApiService methods virtual for improved testability - Update tests to verify idempotency and correct permission handling - Improve logging and user-facing output for better troubleshooting - Simplify and clarify error handling, privilege checks, and mitigation steps
1 parent c9e65b8 commit 315b526

16 files changed

Lines changed: 818 additions & 484 deletions

File tree

src/Microsoft.Agents.A365.DevTools.Cli/Commands/ConfigCommand.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,11 +107,9 @@ await clientAppValidator.EnsureValidClientAppAsync(
107107
}
108108
if (ex.MitigationSteps.Count > 0)
109109
{
110-
logger.LogError("");
111-
logger.LogError(ErrorMessages.ClientAppValidationFixHeader);
112110
foreach (var step in ex.MitigationSteps)
113111
{
114-
logger.LogError($" - {step}");
112+
logger.LogError(step);
115113
}
116114
}
117115
logger.LogError("");

src/Microsoft.Agents.A365.DevTools.Cli/Commands/DevelopSubcommands/GetTokenSubcommand.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,22 @@ private static void DisplayRawResults(List<McpServerTokenResult> results, bool v
332332
{
333333
if (result.Success && !string.IsNullOrWhiteSpace(result.Token))
334334
{
335+
// Write metadata to stderr so it doesn't interfere with token parsing on stdout
336+
// but remains available for troubleshooting when multiple tokens are returned
337+
if (verbose)
338+
{
339+
Console.Error.WriteLine($"# {result.ServerName}");
340+
Console.Error.WriteLine($"# Scope: {result.Scope}");
341+
Console.Error.WriteLine($"# Audience: {result.Audience}");
342+
}
343+
344+
// Write token to stdout for piping to other tools
335345
Console.WriteLine(result.Token);
346+
347+
if (verbose)
348+
{
349+
Console.Error.WriteLine();
350+
}
336351
}
337352
}
338353
}

src/Microsoft.Agents.A365.DevTools.Cli/Commands/PublishCommand.cs

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ public static Command CreateCommand(
324324
catch (MsalServiceException ex) when (ex.ErrorCode == "invalid_client" &&
325325
ex.Message.Contains("AADSTS650052"))
326326
{
327-
logger.LogError("MOS token acquisition failed: Missing service principal or admin consent");
327+
logger.LogError("MOS token acquisition failed: Missing service principal or admin consent (Error: {ErrorCode})", ex.ErrorCode);
328328
logger.LogInformation("");
329329
logger.LogInformation("The MOS service principals exist, but admin consent may not be granted.");
330330
logger.LogInformation("Grant admin consent at:");
@@ -335,6 +335,52 @@ public static Command CreateCommand(
335335
logger.LogInformation("");
336336
return;
337337
}
338+
catch (MsalServiceException ex) when (ex.ErrorCode == "unauthorized_client" &&
339+
ex.Message.Contains("AADSTS50194"))
340+
{
341+
logger.LogError("MOS token acquisition failed: Single-tenant app cannot use /common endpoint (Error: {ErrorCode})", ex.ErrorCode);
342+
logger.LogInformation("");
343+
logger.LogInformation("AADSTS50194: The application is configured as single-tenant but is trying to use the /common authority.");
344+
logger.LogInformation("This should be automatically handled by using tenant-specific authority URLs.");
345+
logger.LogInformation("");
346+
logger.LogInformation("If this error persists:");
347+
logger.LogInformation("1. Verify your app registration is configured correctly in Azure Portal");
348+
logger.LogInformation("2. Check that tenantId in a365.config.json matches your app's home tenant");
349+
logger.LogInformation("3. Ensure the app's 'Supported account types' setting matches your use case");
350+
logger.LogInformation("");
351+
return;
352+
}
353+
catch (MsalServiceException ex) when (ex.ErrorCode == "invalid_grant")
354+
{
355+
logger.LogError("MOS token acquisition failed: Invalid or expired credentials (Error: {ErrorCode})", ex.ErrorCode);
356+
logger.LogInformation("");
357+
logger.LogInformation("The authentication failed due to invalid credentials or expired tokens.");
358+
logger.LogInformation("Try clearing the token cache and re-authenticating:");
359+
logger.LogInformation(" - Delete: ~/.a365/mos-token-cache.json");
360+
logger.LogInformation(" - Run: a365 publish");
361+
logger.LogInformation("");
362+
return;
363+
}
364+
catch (MsalServiceException ex)
365+
{
366+
// Log all MSAL-specific errors with full context for debugging
367+
logger.LogError("MOS token acquisition failed with MSAL error");
368+
logger.LogError("Error Code: {ErrorCode}", ex.ErrorCode);
369+
logger.LogError("Error Message: {Message}", ex.Message);
370+
logger.LogDebug("Stack Trace: {StackTrace}", ex.StackTrace);
371+
372+
logger.LogInformation("");
373+
logger.LogInformation("Authentication failed. Common issues:");
374+
logger.LogInformation("1. Missing admin consent - Grant at:");
375+
logger.LogInformation(" {PortalUrl}",
376+
MosConstants.GetApiPermissionsPortalUrl(config.ClientAppId));
377+
logger.LogInformation("2. Insufficient permissions - Verify required API permissions are configured");
378+
logger.LogInformation("3. Tenant configuration - Ensure app registration matches your tenant setup");
379+
logger.LogInformation("");
380+
logger.LogInformation("For detailed troubleshooting, search for error code: {ErrorCode}", ex.ErrorCode);
381+
logger.LogInformation("");
382+
return;
383+
}
338384
catch (Exception ex)
339385
{
340386
logger.LogError(ex, "Failed to acquire MOS token: {Message}", ex.Message);
@@ -351,11 +397,13 @@ public static Command CreateCommand(
351397
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", mosToken);
352398
http.DefaultRequestHeaders.UserAgent.ParseAdd($"Agent365Publish/{Assembly.GetExecutingAssembly().GetName().Version}");
353399

354-
// Log token info for debugging (first/last 10 chars only for security)
355-
if (mosToken.Length > 20)
400+
// Log token info for debugging (first/last chars only for security)
401+
if (mosToken.Length >= 20)
356402
{
403+
var prefixLen = Math.Min(10, mosToken.Length / 2);
404+
var suffixLen = Math.Min(10, mosToken.Length / 2);
357405
logger.LogDebug("Using MOS token: {TokenStart}...{TokenEnd} (length: {Length})",
358-
mosToken[..10], mosToken[^10..], mosToken.Length);
406+
mosToken[..prefixLen], mosToken[^suffixLen..], mosToken.Length);
359407
}
360408

361409
// Step 2: POST packages (multipart form) - using tenant-specific URL

src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/AllSubcommand.cs

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,6 @@ public static Command CreateCommand(
131131
if (infraErrors.Count > 0)
132132
{
133133
allErrors.AddRange(infraErrors.Select(e => $"Infrastructure: {e}"));
134-
foreach (var error in infraErrors)
135-
{
136-
logger.LogError(" - {Error}", error);
137-
}
138134
}
139135
else
140136
{
@@ -148,10 +144,6 @@ public static Command CreateCommand(
148144
if (blueprintErrors.Count > 0)
149145
{
150146
allErrors.AddRange(blueprintErrors.Select(e => $"Blueprint: {e}"));
151-
foreach (var error in blueprintErrors)
152-
{
153-
logger.LogError(" - {Error}", error);
154-
}
155147
}
156148
else
157149
{

src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/BlueprintSubcommand.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,15 @@ await clientAppValidator.EnsureValidClientAppAsync(
7777
// Add mitigation steps if available
7878
if (ex.MitigationSteps.Count > 0)
7979
{
80-
errors.Add("");
81-
errors.Add(ErrorMessages.ClientAppValidationFixHeader);
82-
errors.AddRange(ex.MitigationSteps.Select(m => $" - {m}"));
80+
errors.AddRange(ex.MitigationSteps);
8381
}
8482
}
83+
catch (Exception ex)
84+
{
85+
// Catch any unexpected validation errors (Graph API failures, etc.)
86+
errors.Add($"Client app validation failed: {ex.Message}");
87+
errors.Add("Ensure Azure CLI is authenticated and you have access to the tenant.");
88+
}
8589

8690
return errors;
8791
}

src/Microsoft.Agents.A365.DevTools.Cli/Constants/ErrorMessages.cs

Lines changed: 32 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,10 @@ public static List<string> GetQuotaExceededMitigation(string location)
2020

2121
return new List<string>
2222
{
23-
"Your Azure subscription has reached its quota limit for App Service Plans in this SKU tier",
24-
"Option 1: Request a quota increase in Azure Portal > Subscriptions > Usage + quotas",
25-
"Option 2: Use a Free tier (F1) for development/testing by updating 'planSku' to 'F1' in a365.config.json",
26-
"Option 3: Use a different Azure subscription with available quota",
27-
"Option 4: Delete unused App Service Plans to free up quota",
28-
$"Option 5: Try a different region - update 'location' in a365.config.json (current: {locationDisplay})",
29-
"Learn more: https://learn.microsoft.com/azure/app-service/app-service-plan-manage#quotas"
23+
"Your Azure subscription has reached its quota limit for App Service Plans in this SKU tier.",
24+
"For development/testing, update 'planSku' to 'F1' in a365.config.json to use Free tier.",
25+
"For production, request quota increase at: Azure Portal > Subscriptions > Usage + quotas",
26+
$"Current location: {locationDisplay}. Consider trying a different region if quota is unavailable."
3027
};
3128
}
3229

@@ -40,11 +37,9 @@ public static List<string> GetSkuNotAvailableMitigation(string location, string
4037

4138
return new List<string>
4239
{
43-
$"The SKU '{skuDisplay}' is not available in region '{locationDisplay}'",
44-
"Option 1: Change the 'planSku' in a365.config.json to a supported SKU (F1, B1, B2, S1, S2, P1V2, P2V2)",
45-
$"Option 2: Change the 'location' in a365.config.json to a region that supports '{skuDisplay}'",
46-
"Option 3: Use Free tier (F1) for development/testing",
47-
"Check SKU availability: https://azure.microsoft.com/pricing/details/app-service/"
40+
$"SKU '{skuDisplay}' is not available in region '{locationDisplay}'.",
41+
"Update 'planSku' in a365.config.json to a supported SKU: F1, B1, B2, S1, S2, P1V2, P2V2",
42+
$"Or change 'location' in a365.config.json to a region that supports '{skuDisplay}'."
4843
};
4944
}
5045

@@ -55,11 +50,9 @@ public static List<string> GetAuthorizationFailedMitigation()
5550
{
5651
return new List<string>
5752
{
58-
"You don't have sufficient permissions to create App Service Plans in this subscription or resource group",
59-
"Required role: Contributor or Owner on the subscription or resource group",
60-
"Check your current role: Run 'az role assignment list --assignee $(az account show --query user.name -o tsv) --all'",
61-
"Contact your Azure administrator to grant the required permissions",
62-
"Verify you're using the correct subscription: 'az account show'"
53+
"Insufficient permissions to create App Service Plans in this subscription or resource group.",
54+
"Required role: Contributor or Owner on the subscription or resource group.",
55+
"Contact your Azure administrator to grant the required permissions."
6356
};
6457
}
6558

@@ -70,12 +63,10 @@ public static List<string> GetVerificationTimeoutMitigation()
7063
{
7164
return new List<string>
7265
{
73-
"The App Service Plan was created but is taking longer than expected to appear in Azure",
74-
"This usually indicates an Azure propagation delay or regional issue",
75-
"Option 1: Wait a few minutes and check Azure Portal to confirm the plan exists",
76-
"Option 2: If the plan exists in Portal, run the setup command again (it will skip creation)",
77-
"Option 3: If the plan doesn't exist after 5+ minutes, delete the resource group and retry",
78-
"Check Azure status: https://status.azure.com"
66+
"App Service Plan creation is taking longer than expected.",
67+
"Wait a few minutes and check Azure Portal to confirm the plan exists.",
68+
"If the plan exists, run the command again (it will skip creation).",
69+
"If issues persist, check Azure service status at https://status.azure.com"
7970
};
8071
}
8172

@@ -86,12 +77,10 @@ public static List<string> GetGenericAppServicePlanMitigation()
8677
{
8778
return new List<string>
8879
{
89-
"App Service Plan creation failed due to an unexpected Azure error",
90-
"Option 1: Check the error details above for specific Azure error messages",
91-
"Option 2: Verify your Azure subscription is active and has no billing issues",
92-
"Option 3: Try a different region by updating 'location' in a365.config.json",
93-
"Option 4: Check Azure service health: https://status.azure.com",
94-
"Learn more: https://learn.microsoft.com/azure/app-service/app-service-plan-manage"
80+
"App Service Plan creation failed.",
81+
"Verify your Azure subscription is active and has no billing issues.",
82+
"Try a different region by updating 'location' in a365.config.json.",
83+
"Check Azure service status at https://status.azure.com"
9584
};
9685
}
9786

@@ -148,20 +137,9 @@ public static List<string> GetMosServicePrincipalMitigation(string appId)
148137
{
149138
return new List<string>
150139
{
151-
$"Failed to create service principal for application {appId}",
152-
"",
153-
"You need one of these roles to create service principals:",
154-
" - Application Administrator (recommended)",
155-
" - Cloud Application Administrator",
156-
" - Global Administrator",
157-
"",
158-
"To check your current roles:",
159-
" az rest --method GET --url https://graph.microsoft.com/v1.0/me/memberOf",
160-
"",
161-
"Contact your tenant administrator if you don't have these roles.",
162-
"",
163-
"Alternatively, ask your admin to run this Azure CLI command:",
164-
$" az ad sp create --id {appId}"
140+
$"Insufficient privileges to create service principal for {appId}.",
141+
"Required role: Application Administrator, Cloud Application Administrator, or Global Administrator.",
142+
$"Ask your tenant administrator to run: az ad sp create --id {appId}"
165143
};
166144
}
167145

@@ -172,18 +150,10 @@ public static List<string> GetFirstPartyClientAppServicePrincipalMitigation()
172150
{
173151
return new List<string>
174152
{
175-
"Failed to create service principal for Microsoft first-party client app",
176-
"This app is required for MOS token acquisition",
177-
"",
178-
"You need one of these roles:",
179-
" - Application Administrator (recommended)",
180-
" - Cloud Application Administrator",
181-
" - Global Administrator",
182-
"",
183-
"Ask your tenant administrator to run:",
184-
$" az ad sp create --id {MosConstants.TpsAppServicesClientAppId}",
185-
"",
186-
"Or grant you one of the required roles above."
153+
"Insufficient privileges to create service principal for Microsoft first-party client app.",
154+
"This app is required for MOS token acquisition.",
155+
"Required role: Application Administrator, Cloud Application Administrator, or Global Administrator.",
156+
$"Ask your tenant administrator to run: az ad sp create --id {MosConstants.TpsAppServicesClientAppId}"
187157
};
188158
}
189159

@@ -194,20 +164,12 @@ public static List<string> GetMosResourceAppsServicePrincipalMitigation()
194164
{
195165
return new List<string>
196166
{
197-
"Failed to create service principals for MOS resource applications",
198-
"These service principals are required for MOS API access",
199-
"",
200-
"You need one of these roles:",
201-
" - Application Administrator (recommended)",
202-
" - Cloud Application Administrator",
203-
" - Global Administrator",
204-
"",
205-
"Ask your tenant administrator to run these commands:",
167+
"Insufficient privileges to create service principals for MOS resource applications.",
168+
"Required role: Application Administrator, Cloud Application Administrator, or Global Administrator.",
169+
"Ask your tenant administrator to run:",
206170
" az ad sp create --id 6ec511af-06dc-4fe2-b493-63a37bc397b1",
207171
" az ad sp create --id 8578e004-a5c6-46e7-913e-12f58912df43",
208-
" az ad sp create --id e8be65d6-d430-4289-a665-51bf2a194bda",
209-
"",
210-
"Or grant you one of the required roles above."
172+
" az ad sp create --id e8be65d6-d430-4289-a665-51bf2a194bda"
211173
};
212174
}
213175

@@ -218,11 +180,9 @@ public static List<string> GetMosAdminConsentMitigation(string clientAppId)
218180
{
219181
return new List<string>
220182
{
221-
"Grant admin consent in Azure Portal:",
222-
$" 1. Go to: https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/CallAnAPI/appId/{clientAppId}",
223-
" 2. Click 'Grant admin consent for [Your Organization]'",
224-
" 3. Wait 1-2 minutes for consent to propagate",
225-
"Alternatively, authenticate interactively when running 'a365 publish' and consent when prompted."
183+
"Admin consent required for MOS API permissions.",
184+
$"Grant consent at: https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/CallAnAPI/appId/{clientAppId}",
185+
"Click 'Grant admin consent for [Your Organization]' and wait 1-2 minutes for propagation."
226186
};
227187
}
228188

0 commit comments

Comments
 (0)