Skip to content

Commit 4cdb32b

Browse files
sellakumaranclaude
andauthored
fix: validate location config before endpoint registration and deletion (#281)
* fix: validate location before calling endpoint registration API Users with needDeployment:false do not require location in a365.config.json, but the endpoint registration API (both create and delete) requires the Location field. This caused a confusing BadRequest from the server when location was empty. Add early guards in BotConfigurator.CreateEndpointWithAgentBlueprintAsync and DeleteEndpointWithAgentBlueprintAsync that return a clear error message before making the API call. Add a matching guard in CleanupCommand.DeleteMessagingEndpointAsync so the error surfaces at the command layer with actionable instructions. Update CleanupBlueprint_WithEndpointOnlyAndInvalidLocation test to assert the corrected behavior: the API must not be called when location is missing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Require location for endpoint registration & cleanup Enforce presence of the location field for Bot Framework endpoint registration and deletion, including for external hosting scenarios. - Added LocationRequirementCheck and integrated it into requirements validation. - Updated commands and BotConfigurator to check for missing location and provide clear, actionable error messages. - Centralized error messages for location requirements. - Improved IBotConfigurator documentation regarding location. - Added and updated unit tests to cover location validation logic. * fix(config): always prompt for location in config init wizard regardless of needDeployment For external hosting (needDeployment: false), the wizard was silently deriving location from the resource group without asking the user. The Bot Framework endpoint registration API requires location in all cases, so the wizard now explicitly prompts for it in both paths, matching the same interactive region-selection UX used for new app service plans. Also removes the dead PromptForLocation(AzureAccountInfo) overload that was never called. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent f5e720b commit 4cdb32b

13 files changed

Lines changed: 344 additions & 249 deletions

File tree

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.CommandLine;
55
using System.Text.Json;
66
using Microsoft.Extensions.Logging;
7+
using Microsoft.Agents.A365.DevTools.Cli.Constants;
78
using Microsoft.Agents.A365.DevTools.Cli.Services.Helpers;
89
using Microsoft.Agents.A365.DevTools.Cli.Services;
910
using Microsoft.Agents.A365.DevTools.Cli.Services.Internal;
@@ -729,6 +730,16 @@ private static async Task<bool> DeleteMessagingEndpointAsync(
729730
return false;
730731
}
731732

733+
// Defense-in-depth: BotConfigurator also validates location, but catching it here gives
734+
// the user a clearer error before any authentication or HTTP work is attempted.
735+
if (string.IsNullOrWhiteSpace(config.Location))
736+
{
737+
logger.LogError(ErrorMessages.EndpointLocationRequiredForDelete);
738+
logger.LogInformation(ErrorMessages.EndpointLocationAddToConfig);
739+
logger.LogInformation(ErrorMessages.EndpointLocationExample);
740+
return false;
741+
}
742+
732743
logger.LogInformation("Deleting messaging endpoint registration...");
733744
var endpointName = EndpointHelper.GetEndpointName(config.BotName);
734745

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

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -337,17 +337,32 @@ public static async Task<BlueprintCreationResult> CreateBlueprintImplementationA
337337
string? correlationId = null,
338338
CancellationToken cancellationToken = default)
339339
{
340+
// Validate location before logging the header — prevents confusing output where the heading
341+
// appears but setup immediately fails due to a missing config value.
342+
if (!skipEndpointRegistration && string.IsNullOrWhiteSpace(setupConfig.Location))
343+
{
344+
logger.LogError(ErrorMessages.EndpointLocationRequiredForCreate);
345+
logger.LogInformation(ErrorMessages.EndpointLocationAddToConfig);
346+
logger.LogInformation(ErrorMessages.EndpointLocationExample);
347+
return new BlueprintCreationResult
348+
{
349+
BlueprintCreated = false,
350+
EndpointRegistered = false,
351+
EndpointRegistrationAttempted = false
352+
};
353+
}
354+
340355
logger.LogInformation("");
341356
logger.LogInformation("==> Creating Agent Blueprint");
342357

343358
// Validate Azure authentication
344359
if (!await azureValidator.ValidateAllAsync(setupConfig.SubscriptionId))
345360
{
346-
return new BlueprintCreationResult
347-
{
348-
BlueprintCreated = false,
349-
EndpointRegistered = false,
350-
EndpointRegistrationAttempted = false
361+
return new BlueprintCreationResult
362+
{
363+
BlueprintCreated = false,
364+
EndpointRegistered = false,
365+
EndpointRegistrationAttempted = false
351366
};
352367
}
353368

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

1720+
// Location is required by the endpoint registration API for both Azure and external hosting
1721+
if (string.IsNullOrWhiteSpace(setupConfig.Location))
1722+
{
1723+
logger.LogError(ErrorMessages.EndpointLocationRequiredForCreate);
1724+
logger.LogInformation(ErrorMessages.EndpointLocationAddToConfig);
1725+
logger.LogInformation(ErrorMessages.EndpointLocationExample);
1726+
Environment.Exit(1);
1727+
}
1728+
17051729
logger.LogInformation("Registering blueprint messaging endpoint...");
17061730
logger.LogInformation("");
17071731

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,9 @@ public static List<IRequirementCheck> GetRequirementChecks(IClientAppValidator c
163163
{
164164
return new List<IRequirementCheck>
165165
{
166+
// Location configuration — required for endpoint registration
167+
new LocationRequirementCheck(),
168+
166169
// Frontier Preview Program enrollment check
167170
new FrontierPreviewRequirementCheck(),
168171

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

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,14 +98,53 @@ public static List<string> GetGenericAppServicePlanMitigation()
9898

9999
#region Configuration Messages
100100

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

104-
public const string InvalidConfigFormat =
104+
public const string InvalidConfigFormat =
105105
"Configuration file has invalid JSON format";
106106

107107
#endregion
108108

109+
#region Endpoint Registration Messages
110+
111+
public const string EndpointLocationRequiredForCreate =
112+
"Location is required to register the messaging endpoint.";
113+
114+
public const string EndpointLocationRequiredForDelete =
115+
"Location is required to delete the messaging endpoint.";
116+
117+
public const string EndpointLocationAddToConfig =
118+
"Run 'a365 config init' to configure your location.";
119+
120+
public const string EndpointLocationExample =
121+
"Example: \"location\": \"eastus\"";
122+
123+
#endregion
124+
125+
#region Configuration Wizard Messages
126+
127+
/// <summary>
128+
/// Prompt header for region selection when creating a new App Service Plan.
129+
/// </summary>
130+
public const string WizardLocationPromptForAppServicePlan =
131+
"Select Azure region for the new App Service Plan:";
132+
133+
/// <summary>
134+
/// Prompt header for region selection when registering a Bot Framework endpoint without deployment.
135+
/// </summary>
136+
public const string WizardLocationPromptForEndpointRegistration =
137+
"Select Azure region for Bot Framework endpoint registration:";
138+
139+
/// <summary>
140+
/// Note explaining why location is required even for external hosting scenarios.
141+
/// </summary>
142+
public const string WizardLocationRequiredForExternalHostingNote =
143+
"NOTE: An Azure region is required to register the messaging endpoint with the Bot Framework,\n" +
144+
" even when the agent is hosted externally (needDeployment: false).";
145+
146+
#endregion
147+
109148
#region Client App Validation Messages
110149

111150
public const string ClientAppValidationFailed =

src/Microsoft.Agents.A365.DevTools.Cli/Services/BotConfigurator.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,14 @@ public async Task<EndpointRegistrationResult> CreateEndpointWithAgentBlueprintAs
5151
_logger.LogDebug(" Messaging Endpoint: {Endpoint}", messagingEndpoint);
5252
_logger.LogDebug(" Agent Blueprint ID: {AgentBlueprintId}", agentBlueprintId);
5353

54+
if (string.IsNullOrWhiteSpace(location))
55+
{
56+
_logger.LogError(ErrorMessages.EndpointLocationRequiredForCreate);
57+
_logger.LogInformation(ErrorMessages.EndpointLocationAddToConfig);
58+
_logger.LogInformation(ErrorMessages.EndpointLocationExample);
59+
return EndpointRegistrationResult.Failed;
60+
}
61+
5462
try
5563
{
5664
// Get subscription info for tenant ID
@@ -201,6 +209,14 @@ public async Task<bool> DeleteEndpointWithAgentBlueprintAsync(
201209
_logger.LogDebug(" Endpoint Name: {EndpointName}", endpointName);
202210
_logger.LogDebug(" Agent Blueprint ID: {AgentBlueprintId}", agentBlueprintId);
203211

212+
if (string.IsNullOrWhiteSpace(location))
213+
{
214+
_logger.LogError(ErrorMessages.EndpointLocationRequiredForDelete);
215+
_logger.LogInformation(ErrorMessages.EndpointLocationAddToConfig);
216+
_logger.LogInformation(ErrorMessages.EndpointLocationExample);
217+
return false;
218+
}
219+
204220
try
205221
{
206222
// Get subscription info for tenant ID

src/Microsoft.Agents.A365.DevTools.Cli/Services/ConfigurationWizardService.cs

Lines changed: 10 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -166,17 +166,21 @@ private static string ExtractDomainFromAccount(AzureAccountInfo accountInfo)
166166
}
167167
else
168168
{
169-
// External hosting - use resource group location for potential RG creation
170-
resourceLocation = resourceGroupLocation ?? existingConfig?.Location ?? ConfigConstants.DefaultAzureLocation;
171-
172169
messagingEndpoint = PromptForMessagingEndpoint(existingConfig);
173170
if (string.IsNullOrWhiteSpace(messagingEndpoint))
174171
{
175172
Console.WriteLine("ERROR: Configuration wizard cancelled: Messaging Endpoint not provided");
176173
_logger.LogDebug("Messaging endpoint not provided, configuration cancelled");
177174
return null;
178175
}
179-
} // Step 7: Get manager email (required for agent creation)
176+
177+
// Location is required for Bot Framework endpoint registration even when hosting externally
178+
Console.WriteLine();
179+
Console.WriteLine(ErrorMessages.WizardLocationRequiredForExternalHostingNote);
180+
resourceLocation = PromptForLocation(existingConfig, resourceGroupLocation, ErrorMessages.WizardLocationPromptForEndpointRegistration);
181+
}
182+
183+
// Step 7: Get manager email (required for agent creation)
180184
var managerEmail = PromptForManagerEmail(existingConfig, accountInfo);
181185
if (string.IsNullOrWhiteSpace(managerEmail))
182186
{
@@ -502,10 +506,10 @@ private string PromptForDeploymentPath(Agent365Config? existingConfig)
502506
}
503507
}
504508

505-
private string PromptForLocation(Agent365Config? existingConfig, string? resourceGroupLocation)
509+
private string PromptForLocation(Agent365Config? existingConfig, string? resourceGroupLocation, string header = ErrorMessages.WizardLocationPromptForAppServicePlan)
506510
{
507511
Console.WriteLine();
508-
Console.WriteLine("Select Azure region for the new App Service Plan:");
512+
Console.WriteLine(header);
509513
Console.WriteLine();
510514

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

650-
private string PromptForLocation(Agent365Config? existingConfig, AzureAccountInfo accountInfo)
651-
{
652-
// Try to get a smart default location
653-
var defaultLocation = existingConfig?.Location;
654-
655-
if (string.IsNullOrEmpty(defaultLocation))
656-
{
657-
// Try to get from resource group or common defaults
658-
defaultLocation = "westus"; // Conservative default
659-
}
660-
661-
return PromptWithDefault(
662-
"Azure location",
663-
defaultLocation,
664-
input => !string.IsNullOrWhiteSpace(input) ? (true, "") : (false, "Location cannot be empty")
665-
);
666-
}
667-
668654
private static string GenerateValidWebAppName(string cleanName, string timestamp)
669655
{
670656
// Reserve 9 chars for "-webapp-" and 9 for "-endpoint" (total 18), so max cleanName+timestamp is 33

src/Microsoft.Agents.A365.DevTools.Cli/Services/IBotConfigurator.cs

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) Microsoft Corporation.
1+
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

44

@@ -9,7 +9,32 @@ namespace Microsoft.Agents.A365.DevTools.Cli.Services
99
/// </summary>
1010
public interface IBotConfigurator
1111
{
12+
/// <summary>
13+
/// Registers a messaging endpoint with the Agent Blueprint identity.
14+
/// </summary>
15+
/// <param name="endpointName">Azure Bot Service instance name (4-42 characters).</param>
16+
/// <param name="location">
17+
/// Required. Azure region for the endpoint registration (e.g., "eastus").
18+
/// Must not be null or whitespace — an empty value returns <see cref="Models.EndpointRegistrationResult.Failed"/>
19+
/// without making any API call.
20+
/// </param>
21+
/// <param name="messagingEndpoint">HTTPS URL the Bot Framework will call.</param>
22+
/// <param name="agentDescription">Human-readable description of the agent.</param>
23+
/// <param name="agentBlueprintId">Entra ID application ID of the agent blueprint.</param>
24+
/// <param name="correlationId">Optional correlation ID for request tracing.</param>
1225
Task<Models.EndpointRegistrationResult> CreateEndpointWithAgentBlueprintAsync(string endpointName, string location, string messagingEndpoint, string agentDescription, string agentBlueprintId, string? correlationId = null);
26+
27+
/// <summary>
28+
/// Deletes a messaging endpoint registration associated with the Agent Blueprint identity.
29+
/// </summary>
30+
/// <param name="endpointName">Azure Bot Service instance name to delete.</param>
31+
/// <param name="location">
32+
/// Required. Azure region the endpoint was registered in (e.g., "eastus").
33+
/// Must not be null or whitespace — an empty value returns <c>false</c>
34+
/// without making any API call.
35+
/// </param>
36+
/// <param name="agentBlueprintId">Entra ID application ID of the agent blueprint.</param>
37+
/// <param name="correlationId">Optional correlation ID for request tracing.</param>
1338
Task<bool> DeleteEndpointWithAgentBlueprintAsync(string endpointName, string location, string agentBlueprintId, string? correlationId = null);
1439
}
15-
}
40+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using Microsoft.Agents.A365.DevTools.Cli.Constants;
5+
using Microsoft.Agents.A365.DevTools.Cli.Models;
6+
using Microsoft.Extensions.Logging;
7+
8+
namespace Microsoft.Agents.A365.DevTools.Cli.Services.Requirements.RequirementChecks;
9+
10+
/// <summary>
11+
/// Requirement check that validates the location is configured.
12+
/// Location is required by the endpoint registration API regardless of the needDeployment setting.
13+
/// </summary>
14+
public class LocationRequirementCheck : RequirementCheck
15+
{
16+
/// <inheritdoc />
17+
public override string Name => "Location Configuration";
18+
19+
/// <inheritdoc />
20+
public override string Description => "Validates that a location is configured for Bot Framework endpoint registration";
21+
22+
/// <inheritdoc />
23+
public override string Category => "Configuration";
24+
25+
/// <inheritdoc />
26+
public override async Task<RequirementCheckResult> CheckAsync(Agent365Config config, ILogger logger, CancellationToken cancellationToken = default)
27+
{
28+
return await ExecuteCheckWithLoggingAsync(config, logger, CheckImplementationAsync, cancellationToken);
29+
}
30+
31+
private static Task<RequirementCheckResult> CheckImplementationAsync(Agent365Config config, ILogger logger, CancellationToken cancellationToken)
32+
{
33+
if (string.IsNullOrWhiteSpace(config.Location))
34+
{
35+
return Task.FromResult(RequirementCheckResult.Failure(
36+
errorMessage: ErrorMessages.EndpointLocationRequiredForCreate,
37+
resolutionGuidance: $"{ErrorMessages.EndpointLocationAddToConfig} {ErrorMessages.EndpointLocationExample}",
38+
details: "The location field is required for the Bot Framework endpoint registration API, even when needDeployment is set to false (external hosting)."
39+
));
40+
}
41+
42+
return Task.FromResult(RequirementCheckResult.Success(
43+
details: $"Location is configured: {config.Location}"
44+
));
45+
}
46+
}

src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Commands/BlueprintSubcommandTests.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,8 @@ public async Task CreateBlueprintImplementation_WithAzureValidationFailure_Shoul
276276
TenantId = "00000000-0000-0000-0000-000000000000",
277277
ClientAppId = "a1b2c3d4-e5f6-a7b8-c9d0-e1f2a3b4c5d6", // Required for validation
278278
SubscriptionId = "test-sub",
279-
AgentBlueprintDisplayName = "Test Blueprint"
279+
AgentBlueprintDisplayName = "Test Blueprint",
280+
Location = "eastus" // Required for endpoint registration; location guard runs before Azure validation
280281
};
281282

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

480482
var configFile = new FileInfo("test-config.json");

0 commit comments

Comments
 (0)