Skip to content
Open
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 @@ -4,6 +4,7 @@
using System.CommandLine;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Microsoft.Agents.A365.DevTools.Cli.Constants;
using Microsoft.Agents.A365.DevTools.Cli.Services.Helpers;
using Microsoft.Agents.A365.DevTools.Cli.Services;
using Microsoft.Agents.A365.DevTools.Cli.Services.Internal;
Expand Down Expand Up @@ -729,6 +730,16 @@ private static async Task<bool> DeleteMessagingEndpointAsync(
return false;
}

// Defense-in-depth: BotConfigurator also validates location, but catching it here gives
// the user a clearer error before any authentication or HTTP work is attempted.
if (string.IsNullOrWhiteSpace(config.Location))
{
logger.LogError(ErrorMessages.EndpointLocationRequiredForDelete);
logger.LogInformation(ErrorMessages.EndpointLocationAddToConfig);
logger.LogInformation(ErrorMessages.EndpointLocationExample);
return false;
}

logger.LogInformation("Deleting messaging endpoint registration...");
var endpointName = EndpointHelper.GetEndpointName(config.BotName);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -337,17 +337,32 @@ public static async Task<BlueprintCreationResult> CreateBlueprintImplementationA
string? correlationId = null,
CancellationToken cancellationToken = default)
{
// Validate location before logging the header — prevents confusing output where the heading
// appears but setup immediately fails due to a missing config value.
if (!skipEndpointRegistration && string.IsNullOrWhiteSpace(setupConfig.Location))
{
logger.LogError(ErrorMessages.EndpointLocationRequiredForCreate);
logger.LogInformation(ErrorMessages.EndpointLocationAddToConfig);
logger.LogInformation(ErrorMessages.EndpointLocationExample);
return new BlueprintCreationResult
{
BlueprintCreated = false,
EndpointRegistered = false,
EndpointRegistrationAttempted = false
};
}

logger.LogInformation("");
logger.LogInformation("==> Creating Agent Blueprint");

// Validate Azure authentication
if (!await azureValidator.ValidateAllAsync(setupConfig.SubscriptionId))
{
return new BlueprintCreationResult
{
BlueprintCreated = false,
EndpointRegistered = false,
EndpointRegistrationAttempted = false
return new BlueprintCreationResult
{
BlueprintCreated = false,
EndpointRegistered = false,
EndpointRegistrationAttempted = false
};
}

Expand Down Expand Up @@ -1702,6 +1717,15 @@ private static async Task<bool> ValidateClientSecretAsync(
Environment.Exit(1);
}

// Location is required by the endpoint registration API for both Azure and external hosting
if (string.IsNullOrWhiteSpace(setupConfig.Location))
{
logger.LogError(ErrorMessages.EndpointLocationRequiredForCreate);
logger.LogInformation(ErrorMessages.EndpointLocationAddToConfig);
logger.LogInformation(ErrorMessages.EndpointLocationExample);
Environment.Exit(1);
}

logger.LogInformation("Registering blueprint messaging endpoint...");
logger.LogInformation("");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,9 @@ public static List<IRequirementCheck> GetRequirementChecks(IClientAppValidator c
{
return new List<IRequirementCheck>
{
// Location configuration — required for endpoint registration
new LocationRequirementCheck(),

// Frontier Preview Program enrollment check
new FrontierPreviewRequirementCheck(),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,14 +98,53 @@ public static List<string> GetGenericAppServicePlanMitigation()

#region Configuration Messages

public const string ConfigFileNotFound =
public const string ConfigFileNotFound =
"Configuration file not found. Run 'a365 config init' to create one";

public const string InvalidConfigFormat =
public const string InvalidConfigFormat =
"Configuration file has invalid JSON format";

#endregion

#region Endpoint Registration Messages

public const string EndpointLocationRequiredForCreate =
"Location is required to register the messaging endpoint.";

public const string EndpointLocationRequiredForDelete =
"Location is required to delete the messaging endpoint.";

public const string EndpointLocationAddToConfig =
"Run 'a365 config init' to configure your location.";

public const string EndpointLocationExample =
"Example: \"location\": \"eastus\"";

#endregion

#region Configuration Wizard Messages

/// <summary>
/// Prompt header for region selection when creating a new App Service Plan.
/// </summary>
public const string WizardLocationPromptForAppServicePlan =
"Select Azure region for the new App Service Plan:";

/// <summary>
/// Prompt header for region selection when registering a Bot Framework endpoint without deployment.
/// </summary>
public const string WizardLocationPromptForEndpointRegistration =
"Select Azure region for Bot Framework endpoint registration:";

/// <summary>
/// Note explaining why location is required even for external hosting scenarios.
/// </summary>
public const string WizardLocationRequiredForExternalHostingNote =
"NOTE: An Azure region is required to register the messaging endpoint with the Bot Framework,\n" +
" even when the agent is hosted externally (needDeployment: false).";

#endregion

#region Client App Validation Messages

public const string ClientAppValidationFailed =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ public async Task<EndpointRegistrationResult> CreateEndpointWithAgentBlueprintAs
_logger.LogDebug(" Messaging Endpoint: {Endpoint}", messagingEndpoint);
_logger.LogDebug(" Agent Blueprint ID: {AgentBlueprintId}", agentBlueprintId);

if (string.IsNullOrWhiteSpace(location))
{
_logger.LogError(ErrorMessages.EndpointLocationRequiredForCreate);
_logger.LogInformation(ErrorMessages.EndpointLocationAddToConfig);
_logger.LogInformation(ErrorMessages.EndpointLocationExample);
return EndpointRegistrationResult.Failed;
}

try
{
// Get subscription info for tenant ID
Expand Down Expand Up @@ -201,6 +209,14 @@ public async Task<bool> DeleteEndpointWithAgentBlueprintAsync(
_logger.LogDebug(" Endpoint Name: {EndpointName}", endpointName);
_logger.LogDebug(" Agent Blueprint ID: {AgentBlueprintId}", agentBlueprintId);

if (string.IsNullOrWhiteSpace(location))
{
_logger.LogError(ErrorMessages.EndpointLocationRequiredForDelete);
_logger.LogInformation(ErrorMessages.EndpointLocationAddToConfig);
_logger.LogInformation(ErrorMessages.EndpointLocationExample);
return false;
}

try
{
// Get subscription info for tenant ID
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,17 +166,21 @@ private static string ExtractDomainFromAccount(AzureAccountInfo accountInfo)
}
else
{
// External hosting - use resource group location for potential RG creation
resourceLocation = resourceGroupLocation ?? existingConfig?.Location ?? ConfigConstants.DefaultAzureLocation;

messagingEndpoint = PromptForMessagingEndpoint(existingConfig);
if (string.IsNullOrWhiteSpace(messagingEndpoint))
{
Console.WriteLine("ERROR: Configuration wizard cancelled: Messaging Endpoint not provided");
_logger.LogDebug("Messaging endpoint not provided, configuration cancelled");
return null;
}
} // Step 7: Get manager email (required for agent creation)

// Location is required for Bot Framework endpoint registration even when hosting externally
Console.WriteLine();
Console.WriteLine(ErrorMessages.WizardLocationRequiredForExternalHostingNote);
resourceLocation = PromptForLocation(existingConfig, resourceGroupLocation, ErrorMessages.WizardLocationPromptForEndpointRegistration);
}

// Step 7: Get manager email (required for agent creation)
var managerEmail = PromptForManagerEmail(existingConfig, accountInfo);
if (string.IsNullOrWhiteSpace(managerEmail))
{
Expand Down Expand Up @@ -502,10 +506,10 @@ private string PromptForDeploymentPath(Agent365Config? existingConfig)
}
}

private string PromptForLocation(Agent365Config? existingConfig, string? resourceGroupLocation)
private string PromptForLocation(Agent365Config? existingConfig, string? resourceGroupLocation, string header = ErrorMessages.WizardLocationPromptForAppServicePlan)
{
Console.WriteLine();
Console.WriteLine("Select Azure region for the new App Service Plan:");
Console.WriteLine(header);
Console.WriteLine();

// Use RG location as default if available, otherwise use existing config or default location
Expand Down Expand Up @@ -647,24 +651,6 @@ private string PromptForMessagingEndpoint(Agent365Config? existingConfig)
);
}

private string PromptForLocation(Agent365Config? existingConfig, AzureAccountInfo accountInfo)
{
// Try to get a smart default location
var defaultLocation = existingConfig?.Location;

if (string.IsNullOrEmpty(defaultLocation))
{
// Try to get from resource group or common defaults
defaultLocation = "westus"; // Conservative default
}

return PromptWithDefault(
"Azure location",
defaultLocation,
input => !string.IsNullOrWhiteSpace(input) ? (true, "") : (false, "Location cannot be empty")
);
}

private static string GenerateValidWebAppName(string cleanName, string timestamp)
{
// Reserve 9 chars for "-webapp-" and 9 for "-endpoint" (total 18), so max cleanName+timestamp is 33
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation.
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.


Expand All @@ -9,7 +9,32 @@ namespace Microsoft.Agents.A365.DevTools.Cli.Services
/// </summary>
public interface IBotConfigurator
{
/// <summary>
/// Registers a messaging endpoint with the Agent Blueprint identity.
/// </summary>
/// <param name="endpointName">Azure Bot Service instance name (4-42 characters).</param>
/// <param name="location">
/// Required. Azure region for the endpoint registration (e.g., "eastus").
/// Must not be null or whitespace — an empty value returns <see cref="Models.EndpointRegistrationResult.Failed"/>
/// without making any API call.
/// </param>
/// <param name="messagingEndpoint">HTTPS URL the Bot Framework will call.</param>
/// <param name="agentDescription">Human-readable description of the agent.</param>
/// <param name="agentBlueprintId">Entra ID application ID of the agent blueprint.</param>
/// <param name="correlationId">Optional correlation ID for request tracing.</param>
Task<Models.EndpointRegistrationResult> CreateEndpointWithAgentBlueprintAsync(string endpointName, string location, string messagingEndpoint, string agentDescription, string agentBlueprintId, string? correlationId = null);

/// <summary>
/// Deletes a messaging endpoint registration associated with the Agent Blueprint identity.
/// </summary>
/// <param name="endpointName">Azure Bot Service instance name to delete.</param>
/// <param name="location">
/// Required. Azure region the endpoint was registered in (e.g., "eastus").
/// Must not be null or whitespace — an empty value returns <c>false</c>
/// without making any API call.
/// </param>
/// <param name="agentBlueprintId">Entra ID application ID of the agent blueprint.</param>
/// <param name="correlationId">Optional correlation ID for request tracing.</param>
Task<bool> DeleteEndpointWithAgentBlueprintAsync(string endpointName, string location, string agentBlueprintId, string? correlationId = null);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.Agents.A365.DevTools.Cli.Constants;
using Microsoft.Agents.A365.DevTools.Cli.Models;
using Microsoft.Extensions.Logging;

namespace Microsoft.Agents.A365.DevTools.Cli.Services.Requirements.RequirementChecks;

/// <summary>
/// Requirement check that validates the location is configured.
/// Location is required by the endpoint registration API regardless of the needDeployment setting.
/// </summary>
public class LocationRequirementCheck : RequirementCheck
{
/// <inheritdoc />
public override string Name => "Location Configuration";

/// <inheritdoc />
public override string Description => "Validates that a location is configured for Bot Framework endpoint registration";

/// <inheritdoc />
public override string Category => "Configuration";

/// <inheritdoc />
public override async Task<RequirementCheckResult> CheckAsync(Agent365Config config, ILogger logger, CancellationToken cancellationToken = default)
{
return await ExecuteCheckWithLoggingAsync(config, logger, CheckImplementationAsync, cancellationToken);
}

private static Task<RequirementCheckResult> CheckImplementationAsync(Agent365Config config, ILogger logger, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(config.Location))
{
return Task.FromResult(RequirementCheckResult.Failure(
errorMessage: ErrorMessages.EndpointLocationRequiredForCreate,
resolutionGuidance: $"{ErrorMessages.EndpointLocationAddToConfig} {ErrorMessages.EndpointLocationExample}",
details: "The location field is required for the Bot Framework endpoint registration API, even when needDeployment is set to false (external hosting)."
));
}

return Task.FromResult(RequirementCheckResult.Success(
details: $"Location is configured: {config.Location}"
));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,8 @@ public async Task CreateBlueprintImplementation_WithAzureValidationFailure_Shoul
TenantId = "00000000-0000-0000-0000-000000000000",
ClientAppId = "a1b2c3d4-e5f6-a7b8-c9d0-e1f2a3b4c5d6", // Required for validation
SubscriptionId = "test-sub",
AgentBlueprintDisplayName = "Test Blueprint"
AgentBlueprintDisplayName = "Test Blueprint",
Location = "eastus" // Required for endpoint registration; location guard runs before Azure validation
};

var configFile = new FileInfo("test-config.json");
Expand Down Expand Up @@ -474,7 +475,8 @@ public async Task CreateBlueprintImplementation_ShouldLogProgressMessages()
{
TenantId = "00000000-0000-0000-0000-000000000000",
SubscriptionId = "test-sub",
AgentBlueprintDisplayName = "Test Blueprint"
AgentBlueprintDisplayName = "Test Blueprint",
Location = "eastus" // Required for endpoint registration; location guard runs before the header is logged
};

var configFile = new FileInfo("test-config.json");
Expand Down
Loading
Loading