diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupCommand.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupCommand.cs index 450d37e2..8d5e9712 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupCommand.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupCommand.cs @@ -86,11 +86,17 @@ public static Command CreateCommand( { // Load configuration - ConfigService automatically finds generated config in same directory var setupConfig = await configService.LoadAsync(config.FullName); - - // Validate Azure CLI authentication, subscription, and environment - if (!await azureValidator.ValidateAllAsync(setupConfig.SubscriptionId)) + if (setupConfig.NeedDeployment) { - Environment.Exit(1); + // Validate Azure CLI authentication, subscription, and environment + if (!await azureValidator.ValidateAllAsync(setupConfig.SubscriptionId)) + { + Environment.Exit(1); + } + } + else + { + logger.LogInformation("NeedDeployment=false – skipping Azure subscription validation."); } logger.LogInformation(""); @@ -141,7 +147,7 @@ public static Command CreateCommand( logger.LogError("Agent blueprint creation failed"); setupResults.BlueprintCreated = false; setupResults.Errors.Add("Agent blueprint creation failed"); - throw new InvalidOperationException("Setup runner execution failed"); + throw new SetupValidationException("Setup runner execution failed"); } setupResults.BlueprintCreated = true; @@ -218,10 +224,10 @@ await EnsureMcpInheritablePermissionsAsync( logger.LogInformation(""); if (string.IsNullOrWhiteSpace(setupConfig.AgentBlueprintId)) - throw new InvalidOperationException("AgentBlueprintId is required."); + throw new SetupValidationException("AgentBlueprintId is required."); var blueprintSpObjectId = await graphApiService.LookupServicePrincipalByAppIdAsync(setupConfig.TenantId, setupConfig.AgentBlueprintId) - ?? throw new InvalidOperationException($"Blueprint Service Principal not found for appId {setupConfig.AgentBlueprintId}"); + ?? throw new SetupValidationException($"Blueprint Service Principal not found for appId {setupConfig.AgentBlueprintId}"); // Ensure Messaging Bot API SP exists var botApiResourceSpObjectId = await graphApiService.EnsureServicePrincipalForAppIdAsync( @@ -401,27 +407,100 @@ private static async Task RegisterBlueprintMessagingEndpointAsync( if (string.IsNullOrEmpty(setupConfig.AgentBlueprintId)) { logger.LogError("Agent Blueprint ID not found. Blueprint creation may have failed."); - throw new InvalidOperationException("Agent Blueprint ID is required for messaging endpoint registration"); + throw new SetupValidationException( + issueDescription: "Agent blueprint was not found – messaging endpoint cannot be registered.", + errorDetails: new List + { + "AgentBlueprintId is missing from configuration. This usually means the blueprint creation step failed or a365.generated.config.json is out of sync." + }, + mitigationSteps: new List + { + "Verify that 'a365 setup' completed Step 1 (Agent blueprint creation) without errors.", + "Check a365.generated.config.json for 'agentBlueprintId'. If it's missing or incorrect, re-run 'a365 setup'." + }, + context: new Dictionary + { + ["AgentBlueprintId"] = setupConfig.AgentBlueprintId ?? "" + }); } - if (string.IsNullOrEmpty(setupConfig.WebAppName)) + string messagingEndpoint; + string endpointName; + if (setupConfig.NeedDeployment) { - logger.LogError("Web App Name not configured in a365.config.json"); - throw new InvalidOperationException("Web App Name is required for messaging endpoint registration"); + if (string.IsNullOrEmpty(setupConfig.WebAppName)) + { + logger.LogError("Web App Name not configured in a365.config.json"); + throw new SetupValidationException( + issueDescription: "Web App name is required to register a messaging endpoint when needDeployment is 'yes'.", + errorDetails: new List + { + "NeedDeployment is true, but 'webAppName' was not provided in a365.config.json." + }, + mitigationSteps: new List + { + "Open a365.config.json and ensure 'webAppName' is set to the Azure Web App name.", + "If you do not want the CLI to deploy an Azure Web App, set \"needDeployment\": \"no\" and provide \"MessagingEndpoint\" instead.", + "Re-run 'a365 setup'." + }, + context: new Dictionary + { + ["needDeployment"] = setupConfig.NeedDeployment.ToString(), + ["webAppName"] = setupConfig.WebAppName ?? "" + }); + } + + // Generate endpoint name with Azure Bot Service constraints (4-42 chars) + var baseEndpointName = $"{setupConfig.WebAppName}-endpoint"; + endpointName = EndpointHelper.GetEndpointName(baseEndpointName); + + // Construct messaging endpoint URL from web app name + messagingEndpoint = $"https://{setupConfig.WebAppName}.azurewebsites.net/api/messages"; + } + else // Non-Azure hosting + { + // No deployment – use the provided MessagingEndpoint + if (string.IsNullOrWhiteSpace(setupConfig.MessagingEndpoint)) + { + logger.LogError("MessagingEndpoint must be provided in a365.config.json for non-Azure hosting."); + throw new SetupValidationException( + issueDescription: "Messaging endpoint is required for messaging endpoint registration.", + errorDetails: new List + { + "needDeployment is set to 'no', but MessagingEndpoint was not provided in a365.config.json." + }, + mitigationSteps: new List + { + "Open your a365.config.json file.", + "If you want the CLI to deploy an Azure Web App, set \"needDeployment\": \"yes\" and provide \"webAppName\".", + "If your agent is hosted elsewhere, keep \"needDeployment\": \"no\" and add a \"MessagingEndpoint\" with a valid HTTPS URL (e.g. \"https://your-host/api/messages\").", + "Re-run 'a365 setup'." + } + ); + } + + if (!Uri.TryCreate(setupConfig.MessagingEndpoint, UriKind.Absolute, out var uri) || + uri.Scheme != Uri.UriSchemeHttps) + { + logger.LogError("MessagingEndpoint must be a valid HTTPS URL. Current value: {Endpoint}", + setupConfig.MessagingEndpoint); + throw new SetupValidationException("MessagingEndpoint must be a valid HTTPS URL."); + } + + messagingEndpoint = setupConfig.MessagingEndpoint; + + // Derive endpoint name from host when there's no WebAppName + var hostPart = uri.Host.Replace('.', '-'); + var baseEndpointName = $"{hostPart}-endpoint"; + endpointName = EndpointHelper.GetEndpointName(baseEndpointName); } - // Generate endpoint name with Azure Bot Service constraints (4-42 chars) - var baseEndpointName = $"{setupConfig.WebAppName}-endpoint"; - var endpointName = EndpointHelper.GetEndpointName(baseEndpointName); if (endpointName.Length < 4) { logger.LogError("Bot endpoint name '{EndpointName}' is too short (must be at least 4 characters)", endpointName); - throw new InvalidOperationException($"Bot endpoint name '{endpointName}' is too short (must be at least 4 characters)"); + throw new SetupValidationException($"Bot endpoint name '{endpointName}' is too short (must be at least 4 characters)"); } - // Register messaging endpoint using agent blueprint identity and deployed web app URL - var messagingEndpoint = $"https://{setupConfig.WebAppName}.azurewebsites.net/api/messages"; - logger.LogInformation(" - Registering blueprint messaging endpoint"); logger.LogInformation(" * Endpoint Name: {EndpointName}", endpointName); logger.LogInformation(" * Messaging Endpoint: {Endpoint}", messagingEndpoint); @@ -437,78 +516,84 @@ private static async Task RegisterBlueprintMessagingEndpointAsync( if (!endpointRegistered) { logger.LogError("Failed to register blueprint messaging endpoint"); - throw new InvalidOperationException("Blueprint messaging endpoint registration failed"); + throw new SetupValidationException("Blueprint messaging endpoint registration failed"); } } + /// + /// Ensure OAuth2 permission grants are set from blueprint to MCP server + /// private static async Task EnsureMcpOauth2PermissionGrantsAsync( GraphApiService graph, - Agent365Config cfg, + Agent365Config config, string[] scopes, ILogger logger, CancellationToken ct = default) { - if (string.IsNullOrWhiteSpace(cfg.AgentBlueprintId)) - throw new InvalidOperationException("AgentBlueprintId (appId) is required."); + if (string.IsNullOrWhiteSpace(config.AgentBlueprintId)) + throw new SetupValidationException("AgentBlueprintId (appId) is required."); - var blueprintSpObjectId = await graph.LookupServicePrincipalByAppIdAsync(cfg.TenantId, cfg.AgentBlueprintId, ct); + var blueprintSpObjectId = await graph.LookupServicePrincipalByAppIdAsync(config.TenantId, config.AgentBlueprintId, ct); if (string.IsNullOrWhiteSpace(blueprintSpObjectId)) { - throw new InvalidOperationException($"Blueprint Service Principal not found for appId {cfg.AgentBlueprintId}. " + + throw new SetupValidationException($"Blueprint Service Principal not found for appId {config.AgentBlueprintId}. " + "The service principal may not have propagated yet. Wait a few minutes and retry."); } - var resourceAppId = ConfigConstants.GetAgent365ToolsResourceAppId(cfg.Environment); - var Agent365ToolsSpObjectId = await graph.LookupServicePrincipalByAppIdAsync(cfg.TenantId, resourceAppId, ct); + var resourceAppId = ConfigConstants.GetAgent365ToolsResourceAppId(config.Environment); + var Agent365ToolsSpObjectId = await graph.LookupServicePrincipalByAppIdAsync(config.TenantId, resourceAppId, ct); if (string.IsNullOrWhiteSpace(Agent365ToolsSpObjectId)) { - throw new InvalidOperationException($"Agent 365 Tools Service Principal not found for appId {resourceAppId}. " + - $"Ensure the Agent 365 Tools application is available in your tenant for environment: {cfg.Environment}"); + throw new SetupValidationException($"Agent 365 Tools Service Principal not found for appId {resourceAppId}. " + + $"Ensure the Agent 365 Tools application is available in your tenant for environment: {config.Environment}"); } logger.LogInformation(" - OAuth2 grant: client {ClientId} to resource {ResourceId} scopes [{Scopes}]", blueprintSpObjectId, Agent365ToolsSpObjectId, string.Join(' ', scopes)); var response = await graph.CreateOrUpdateOauth2PermissionGrantAsync( - cfg.TenantId, blueprintSpObjectId, Agent365ToolsSpObjectId, scopes, ct); + config.TenantId, blueprintSpObjectId, Agent365ToolsSpObjectId, scopes, ct); if (!response) { - throw new InvalidOperationException( - $"Failed to create/update OAuth2 permission grant from blueprint {cfg.AgentBlueprintId} to Agent 365 Tools {resourceAppId}. " + + throw new SetupValidationException( + $"Failed to create/update OAuth2 permission grant from blueprint {config.AgentBlueprintId} to Agent 365 Tools {resourceAppId}. " + "This may be due to insufficient permissions. Ensure you have DelegatedPermissionGrant.ReadWrite.All or Application.ReadWrite.All permissions."); } } + /// + /// Ensure inheritable permissions are set from blueprint to MCP server + /// private static async Task EnsureMcpInheritablePermissionsAsync( GraphApiService graph, - Agent365Config cfg, + Agent365Config config, string[] scopes, ILogger logger, CancellationToken ct = default) { - if (string.IsNullOrWhiteSpace(cfg.AgentBlueprintId)) - throw new InvalidOperationException("AgentBlueprintId (appId) is required."); + if (string.IsNullOrWhiteSpace(config.AgentBlueprintId)) + throw new SetupValidationException("AgentBlueprintId (appId) is required."); - var resourceAppId = ConfigConstants.GetAgent365ToolsResourceAppId(cfg.Environment); + var resourceAppId = ConfigConstants.GetAgent365ToolsResourceAppId(config.Environment); logger.LogInformation(" - Inheritable permissions: blueprint {Blueprint} to resourceAppId {ResourceAppId} scopes [{Scopes}]", - cfg.AgentBlueprintId, resourceAppId, string.Join(' ', scopes)); + config.AgentBlueprintId, resourceAppId, string.Join(' ', scopes)); var (ok, alreadyExists, err) = await graph.SetInheritablePermissionsAsync( - cfg.TenantId, cfg.AgentBlueprintId, resourceAppId, scopes, new List() { "AgentIdentityBlueprint.ReadWrite.All" }, ct); + config.TenantId, config.AgentBlueprintId, resourceAppId, scopes, new List() { "AgentIdentityBlueprint.ReadWrite.All" }, ct); if (!ok && !alreadyExists) { - cfg.InheritanceConfigured = false; - cfg.InheritanceConfigError = err; - throw new InvalidOperationException($"Failed to set inheritable permissions: {err}. " + + config.InheritanceConfigured = false; + config.InheritanceConfigError = err; + throw new SetupValidationException($"Failed to set inheritable permissions: {err}. " + "Ensure you have Application.ReadWrite.All permissions and the blueprint supports inheritable permissions."); } - cfg.InheritanceConfigured = true; - cfg.InheritablePermissionsAlreadyExist = alreadyExists; - cfg.InheritanceConfigError = null; + config.InheritanceConfigured = true; + config.InheritablePermissionsAlreadyExist = alreadyExists; + config.InheritanceConfigError = null; } /// diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Constants/ErrorCodes.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Constants/ErrorCodes.cs index 8c2db1be..5625538a 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Constants/ErrorCodes.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Constants/ErrorCodes.cs @@ -11,5 +11,6 @@ public static class ErrorCodes public const string DeploymentScopesFailed = "DEPLOYMENT_SCOPES_FAILED"; public const string DeploymentMcpFailed = "DEPLOYMENT_MCP_FAILED"; public const string HighPrivilegeScopeDetected = "HIGH_PRIVILEGE_SCOPE_DETECTED"; + public const string SetupValidationFailed = "SETUP_VALIDATION_FAILED"; } } diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Exceptions/Agent365Exception.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Exceptions/Agent365Exception.cs index 28e8ccbc..15ff1e8b 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Exceptions/Agent365Exception.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Exceptions/Agent365Exception.cs @@ -1,6 +1,6 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + using System.Text; namespace Microsoft.Agents.A365.DevTools.Cli.Exceptions; @@ -80,7 +80,7 @@ private static string BuildMessage(string errorCode, string issueDescription, Li sb.AppendLine(); foreach (var detail in errorDetails) { - sb.AppendLine($" � {detail}"); + sb.AppendLine($" * {detail}"); } } diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Exceptions/SetupValidationException.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Exceptions/SetupValidationException.cs new file mode 100644 index 00000000..63a414a0 --- /dev/null +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Exceptions/SetupValidationException.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Agents.A365.DevTools.Cli.Constants; + +namespace Microsoft.Agents.A365.DevTools.Cli.Exceptions; + +/// +/// Validation errors that occur during `a365 setup` (user-fixable issues). +/// +public sealed class SetupValidationException : Agent365Exception +{ + public override int ExitCode => 2; + + public SetupValidationException( + string issueDescription, + List? errorDetails = null, + List? mitigationSteps = null, + Dictionary? context = null, + Exception? innerException = null) + : base( + errorCode: ErrorCodes.SetupValidationFailed, + issueDescription: issueDescription, + errorDetails: errorDetails, + mitigationSteps: mitigationSteps, + context: context, + innerException: innerException) + { + } +} diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Models/Agent365Config.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Models/Agent365Config.cs index ea4e4680..6b5e5ab5 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Models/Agent365Config.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Models/Agent365Config.cs @@ -24,14 +24,27 @@ public class Agent365Config public List Validate() { var errors = new List(); + if (string.IsNullOrWhiteSpace(TenantId)) errors.Add("tenantId is required."); - if (string.IsNullOrWhiteSpace(SubscriptionId)) errors.Add("subscriptionId is required."); - if (string.IsNullOrWhiteSpace(ResourceGroup)) errors.Add("resourceGroup is required."); - if (string.IsNullOrWhiteSpace(Location)) errors.Add("location is required."); - if (string.IsNullOrWhiteSpace(AppServicePlanName)) errors.Add("appServicePlanName is required."); - if (string.IsNullOrWhiteSpace(WebAppName)) errors.Add("webAppName is required."); + + if (NeedDeployment) + { + if (string.IsNullOrWhiteSpace(SubscriptionId)) errors.Add("subscriptionId is required."); + if (string.IsNullOrWhiteSpace(ResourceGroup)) errors.Add("resourceGroup is required."); + if (string.IsNullOrWhiteSpace(Location)) errors.Add("location is required."); + if (string.IsNullOrWhiteSpace(AppServicePlanName)) errors.Add("appServicePlanName is required."); + if (string.IsNullOrWhiteSpace(WebAppName)) errors.Add("webAppName is required."); + } + else + { + // Non-Azure hosting + if (string.IsNullOrWhiteSpace(MessagingEndpoint)) + errors.Add("messagingEndpoint is required when needDeployment is 'no'."); + } + if (string.IsNullOrWhiteSpace(AgentIdentityDisplayName)) errors.Add("agentIdentityDisplayName is required."); if (string.IsNullOrWhiteSpace(DeploymentProjectPath)) errors.Add("deploymentProjectPath is required."); + // agentIdentityScopes and agentApplicationScopes are now hardcoded defaults // botName and botDisplayName are now derived, not required in config // Add more validation as needed (e.g., GUID format, allowed values, etc.) @@ -76,6 +89,22 @@ public List Validate() [JsonPropertyName("environment")] public string Environment { get; init; } = "preprod"; + /// + /// For External hosting, this is the HTTPS messaging endpoint that Bot Framework will call. + /// For AzureAppService, this is optional; the CLI derives the endpoint from webAppName. + /// + [JsonPropertyName("messagingEndpoint")] + public string? MessagingEndpoint { get; init; } + + /// + /// Whether the CLI should create and deploy an Azure Web App for this agent. + /// Backed by the 'needDeployment' config value: + /// - true (default) => CLI provisions App Service + MSI, a365 deploy app is active. + /// - false => CLI does NOT create a web app; a365 deploy app is a no-op and MessagingEndpoint must be provided. + /// + [JsonPropertyName("needDeployment")] + public bool NeedDeployment { get; init; } = true; + #endregion #region App Service Configuration @@ -163,13 +192,32 @@ public List Validate() // BotName and BotDisplayName are now derived properties /// - /// Gets the internal name for the endpoint registration, derived from WebAppName. + /// Gets the internal name for the endpoint registration. + /// - For AzureAppService, derived from WebAppName. + /// - For non-Azure hosting, derived from MessagingEndpoint host if possible. /// [JsonIgnore] - public string BotName => string.IsNullOrWhiteSpace(WebAppName) ? string.Empty : $"{WebAppName}-endpoint"; + public string BotName + { + get + { + if (!string.IsNullOrWhiteSpace(WebAppName)) + { + return $"{WebAppName}-endpoint"; + } + + if (!string.IsNullOrWhiteSpace(MessagingEndpoint) && + Uri.TryCreate(MessagingEndpoint, UriKind.Absolute, out var uri)) + { + return $"{uri.Host.Replace('.', '-')}-endpoint"; + } + + return string.Empty; + } + } /// - /// Gets the display name for the bot, derived from AgentBlueprintDisplayName. + /// Gets the display name for the bot, derived from AgentBlueprintDisplayName or WebAppName. /// [JsonIgnore] public string BotDisplayName => !string.IsNullOrWhiteSpace(AgentBlueprintDisplayName) ? AgentBlueprintDisplayName! : WebAppName; @@ -530,4 +578,4 @@ public class AtgConfiguration public List McpServers { get; set; } = new(); public List ToolsServers { get; set; } = new(); public string Agent365ToolsEndpoint { get; set; } = string.Empty; -} +} \ No newline at end of file diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Services/A365SetupRunner.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Services/A365SetupRunner.cs index ae697658..b1134575 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Services/A365SetupRunner.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Services/A365SetupRunner.cs @@ -136,11 +136,30 @@ public async Task RunAsync(string configPath, string generatedConfigPath, if (string.IsNullOrWhiteSpace(planSku)) planSku = "B1"; var deploymentProjectPath = Get("deploymentProjectPath"); + + bool needDeployment = CheckNeedDeployment(cfg); - if (new[] { subscriptionId, tenantId, resourceGroup, planName, webAppName, location }.Any(string.IsNullOrWhiteSpace)) + var skipInfra = blueprintOnly || !needDeployment; + if (!skipInfra) { - _logger.LogError("Config missing required properties. Need subscriptionId, tenantId, resourceGroup, appServicePlanName, webAppName, location."); - return false; + // Azure hosting scenario – need full infra details + if (new[] { subscriptionId, resourceGroup, planName, webAppName, location }.Any(string.IsNullOrWhiteSpace)) + { + _logger.LogError( + "Config missing required properties for Azure hosting. " + + "Need subscriptionId, resourceGroup, appServicePlanName, webAppName, location."); + return false; + } + } + else + { + // Non-Azure hosting or --blueprint: no infra required + if (string.IsNullOrWhiteSpace(subscriptionId)) + { + _logger.LogWarning( + "subscriptionId is not set. This is acceptable for blueprint-only or External hosting mode " + + "as Azure infrastructure will not be provisioned."); + } } // Detect project platform for appropriate runtime configuration @@ -166,86 +185,93 @@ public async Task RunAsync(string configPath, string generatedConfigPath, // ======================================================================== // Phase 0: Ensure Azure CLI is logged in with proper scope // ======================================================================== - _logger.LogInformation("==> [0/5] Verifying Azure CLI authentication"); - - // Check if logged in - var accountCheck = await _executor.ExecuteAsync("az", "account show", captureOutput: true, suppressErrorLogging: true); - if (!accountCheck.Success) + if (!skipInfra) { - _logger.LogInformation("Azure CLI not authenticated. Initiating login with management scope..."); - _logger.LogInformation("A browser window will open for authentication."); + _logger.LogInformation("==> [0/5] Verifying Azure CLI authentication"); - // Use standard login without scope parameter (more reliable) - var loginResult = await _executor.ExecuteAsync("az", $"login --tenant {tenantId}", cancellationToken: cancellationToken); - - if (!loginResult.Success) + // Check if logged in + var accountCheck = await _executor.ExecuteAsync("az", "account show", captureOutput: true, suppressErrorLogging: true); + if (!accountCheck.Success) { - _logger.LogError("Azure CLI login failed. Please run manually: az login --scope https://management.core.windows.net//.default"); - return false; + _logger.LogInformation("Azure CLI not authenticated. Initiating login with management scope..."); + _logger.LogInformation("A browser window will open for authentication."); + + // Use standard login without scope parameter (more reliable) + var loginResult = await _executor.ExecuteAsync("az", $"login --tenant {tenantId}", cancellationToken: cancellationToken); + + if (!loginResult.Success) + { + _logger.LogError("Azure CLI login failed. Please run manually: az login --scope https://management.core.windows.net//.default"); + return false; + } + + _logger.LogInformation("Azure CLI login successful!"); + + // Wait a moment for the login to fully complete + await Task.Delay(2000, cancellationToken); } - - _logger.LogInformation("Azure CLI login successful!"); - - // Wait a moment for the login to fully complete - await Task.Delay(2000, cancellationToken); - } - else - { - _logger.LogInformation("Azure CLI already authenticated"); - } - - // Verify we have the management scope - if not, try to acquire it - _logger.LogInformation("Verifying access to Azure management resources..."); - var tokenCheck = await _executor.ExecuteAsync( - "az", - "account get-access-token --resource https://management.core.windows.net/ --query accessToken -o tsv", - captureOutput: true, - suppressErrorLogging: true, - cancellationToken: cancellationToken); - - if (!tokenCheck.Success) - { - _logger.LogWarning("Unable to acquire management scope token. Attempting re-authentication..."); - _logger.LogInformation("A browser window will open for authentication."); - - // Try standard login first (more reliable than scope-specific login) - var loginResult = await _executor.ExecuteAsync("az", $"login --tenant {tenantId}", cancellationToken: cancellationToken); - - if (!loginResult.Success) + else { - _logger.LogError("Azure CLI login with management scope failed. Please run manually: az login --scope https://management.core.windows.net//.default"); - return false; + _logger.LogInformation("Azure CLI already authenticated"); } - _logger.LogInformation("Azure CLI re-authentication successful!"); - - // Wait a moment for the token cache to update - await Task.Delay(2000, cancellationToken); - - // Verify management token is now available - var retryTokenCheck = await _executor.ExecuteAsync( + // Verify we have the management scope - if not, try to acquire it + _logger.LogInformation("Verifying access to Azure management resources..."); + var tokenCheck = await _executor.ExecuteAsync( "az", "account get-access-token --resource https://management.core.windows.net/ --query accessToken -o tsv", captureOutput: true, suppressErrorLogging: true, cancellationToken: cancellationToken); - if (!retryTokenCheck.Success) + if (!tokenCheck.Success) { - _logger.LogWarning("Still unable to acquire management scope token after re-authentication."); - _logger.LogWarning("Continuing anyway - you may encounter permission errors later."); + _logger.LogWarning("Unable to acquire management scope token. Attempting re-authentication..."); + _logger.LogInformation("A browser window will open for authentication."); + + // Try standard login first (more reliable than scope-specific login) + var loginResult = await _executor.ExecuteAsync("az", $"login --tenant {tenantId}", cancellationToken: cancellationToken); + + if (!loginResult.Success) + { + _logger.LogError("Azure CLI login with management scope failed. Please run manually: az login --scope https://management.core.windows.net//.default"); + return false; + } + + _logger.LogInformation("Azure CLI re-authentication successful!"); + + // Wait a moment for the token cache to update + await Task.Delay(2000, cancellationToken); + + // Verify management token is now available + var retryTokenCheck = await _executor.ExecuteAsync( + "az", + "account get-access-token --resource https://management.core.windows.net/ --query accessToken -o tsv", + captureOutput: true, + suppressErrorLogging: true, + cancellationToken: cancellationToken); + + if (!retryTokenCheck.Success) + { + _logger.LogWarning("Still unable to acquire management scope token after re-authentication."); + _logger.LogWarning("Continuing anyway - you may encounter permission errors later."); + } + else + { + _logger.LogInformation("Management scope token acquired successfully!"); + } } else { - _logger.LogInformation("Management scope token acquired successfully!"); + _logger.LogInformation("Management scope verified successfully"); } + + _logger.LogInformation(""); } else { - _logger.LogInformation("Management scope verified successfully"); + _logger.LogInformation("==> [0/5] Skipping Azure management authentication (blueprint-only or External hosting)"); } - - _logger.LogInformation(""); // ======================================================================== // Phase 1: Deploy Agent runtime (App Service) + System-assigned Managed Identity @@ -253,9 +279,14 @@ public async Task RunAsync(string configPath, string generatedConfigPath, string? principalId = null; JsonObject generatedConfig = new JsonObject(); - if (blueprintOnly) + if (skipInfra) { - _logger.LogInformation("==> [1/5] Skipping Azure infrastructure (--blueprint mode)"); + // Covers BOTH: + // - blueprintOnly == true + // - hostingMode == External (non-Azure) + var modeMessage = blueprintOnly ? "--blueprint mode" : "External hosting (non-Azure)"; + + _logger.LogInformation("==> [1/5] Skipping Azure infrastructure ({Mode})", modeMessage); _logger.LogInformation("Loading existing configuration..."); // Load existing generated config if available @@ -1717,6 +1748,29 @@ private async Task EnsureDelegatedConsentWithRetriesAsync( return false; } + + private bool CheckNeedDeployment(JsonObject setupConfig) + { + bool needDeployment = true; // default + if (setupConfig.TryGetPropertyValue("needDeployment", out var needDeploymentNode) && + needDeploymentNode is JsonValue nv) + { + // Prefer native bool + if (nv.TryGetValue(out var boolVal)) + { + needDeployment = boolVal; + } + else if (nv.TryGetValue(out var strVal) && !string.IsNullOrWhiteSpace(strVal)) + { + // Backward compatibility with "yes"/"no" / "true"/"false" + needDeployment = + !string.Equals(strVal, "no", StringComparison.OrdinalIgnoreCase) && + !string.Equals(strVal, "false", StringComparison.OrdinalIgnoreCase); + } + } + + return needDeployment; + } /// /// Get the Azure Web App runtime string based on the detected platform diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Services/ConfigService.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Services/ConfigService.cs index d0d524b0..27cd914d 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Services/ConfigService.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Services/ConfigService.cs @@ -312,7 +312,7 @@ public async Task LoadAsync( _logger?.LogError("Configuration validation failed:"); foreach (var error in validationResult.Errors) { - _logger?.LogError(" � {Error}", error); + _logger?.LogError(" * {Error}", error); } // Convert validation errors to structured exception @@ -328,7 +328,7 @@ public async Task LoadAsync( { foreach (var warning in validationResult.Warnings) { - _logger?.LogWarning(" � {Warning}", warning); + _logger?.LogWarning(" * {Warning}", warning); } } @@ -377,20 +377,32 @@ public async Task ValidateAsync(Agent365Config config) var errors = new List(); var warnings = new List(); - // Validate required static properties ValidateRequired(config.TenantId, nameof(config.TenantId), errors); - ValidateRequired(config.SubscriptionId, nameof(config.SubscriptionId), errors); - ValidateRequired(config.ResourceGroup, nameof(config.ResourceGroup), errors); - ValidateRequired(config.Location, nameof(config.Location), errors); - - // Validate GUID formats ValidateGuid(config.TenantId, nameof(config.TenantId), errors); - ValidateGuid(config.SubscriptionId, nameof(config.SubscriptionId), errors); - // Validate Azure naming conventions - ValidateResourceGroupName(config.ResourceGroup, errors); - ValidateAppServicePlanName(config.AppServicePlanName, errors); - ValidateWebAppName(config.WebAppName, errors); + if (config.NeedDeployment) + { + // Validate required static properties + ValidateRequired(config.SubscriptionId, nameof(config.SubscriptionId), errors); + ValidateRequired(config.ResourceGroup, nameof(config.ResourceGroup), errors); + ValidateRequired(config.Location, nameof(config.Location), errors); + ValidateRequired(config.AppServicePlanName, nameof(config.AppServicePlanName), errors); + ValidateRequired(config.WebAppName, nameof(config.WebAppName), errors); + + // Validate GUID formats + ValidateGuid(config.SubscriptionId, nameof(config.SubscriptionId), errors); + + // Validate Azure naming conventions + ValidateResourceGroupName(config.ResourceGroup, errors); + ValidateAppServicePlanName(config.AppServicePlanName, errors); + ValidateWebAppName(config.WebAppName, errors); + } + else + { + // Only validate bot messaging endpoint + ValidateRequired(config.MessagingEndpoint, nameof(config.MessagingEndpoint), errors); + ValidateUrl(config.MessagingEndpoint, nameof(config.MessagingEndpoint), errors); + } // Validate dynamic properties if they exist if (config.ManagedIdentityPrincipalId != null)