fix(backend): system-health URLs post EU-migration + GA endpoint visibility

- Agent Binaries check + ops watcher now probe the canonical download alias
  (download.autopilotmonitor.com) AND the legacy blob keepalive; alias failure
  is unhealthy, legacy failure a warning
- McpServerBaseUrl fallback moved from the stale kindwave WEU ACA FQDN to the
  stable custom domain mcp.autopilotmonitor.com (CNAME to the new GWC instance)
- health/detailed and health/mcp include the probed endpoint URLs in check
  details for Global Admins only (storage table endpoint, WEBSITE_HOSTNAME,
  alias/legacy download URLs, MCP server URL)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Oliver Kieselbach
2026-07-19 11:42:40 +02:00
parent b005c20598
commit ef67dec263
6 changed files with 283 additions and 70 deletions
@@ -0,0 +1,107 @@
using System.Collections.Generic;
using System.Net;
using System.Threading.Tasks;
using AutopilotMonitor.Functions.Services;
using AutopilotMonitor.Functions.Tests.GraphResolution;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
namespace AutopilotMonitor.Functions.Tests;
/// <summary>
/// Service-level tests for the Agent Binaries HealthCheck card. The check probes the
/// canonical download alias (download.autopilotmonitor.com) AND the legacy blob keepalive
/// account: alias failure is unhealthy (new installs break), legacy failure is only a
/// warning (existing customers break until they migrate to the alias).
/// </summary>
public class HealthCheckServiceAgentBinariesTests
{
private const string AliasHost = "download.autopilotmonitor.com";
private const string LegacyHost = "autopilotmonitor.blob.core.windows.net";
[Fact]
public async Task Check_AllEndpointsAvailable_ReportsHealthyWithoutUrlDetails()
{
var handler = new StubHttpMessageHandler()
.When(AliasHost, HttpStatusCode.OK, "")
.When(LegacyHost, HttpStatusCode.OK, "");
var check = await BuildService(handler).CheckAgentBinariesAsync(includeEndpointUrls: false);
Assert.Equal("Agent Binaries", check.Name);
Assert.Equal("healthy", check.Status);
Assert.Contains("download alias", check.Message);
Assert.Null(check.Details);
// Both hosts must actually have been probed (ZIP + PS1 each).
Assert.Equal(2, handler.Requests.FindAll(u => u.Contains(AliasHost)).Count);
Assert.Equal(2, handler.Requests.FindAll(u => u.Contains(LegacyHost)).Count);
}
[Fact]
public async Task Check_WithEndpointUrls_SurfacesAliasAndLegacyUrlsInDetails()
{
var handler = new StubHttpMessageHandler()
.When(AliasHost, HttpStatusCode.OK, "")
.When(LegacyHost, HttpStatusCode.OK, "");
var check = await BuildService(handler).CheckAgentBinariesAsync(includeEndpointUrls: true);
Assert.NotNull(check.Details);
Assert.Equal("https://download.autopilotmonitor.com/agent/AutopilotMonitor-Agent.zip", check.Details!["Agent ZIP"]);
Assert.Equal("https://download.autopilotmonitor.com/agent/Install-AutopilotMonitor.ps1", check.Details["Bootstrap script"]);
Assert.Equal("https://autopilotmonitor.blob.core.windows.net/agent", check.Details["Legacy blob (keepalive)"]);
}
[Fact]
public async Task Check_LegacyBlobMissing_ReportsWarningNamingTheLegacyEndpoint()
{
var handler = new StubHttpMessageHandler()
.When(AliasHost, HttpStatusCode.OK, "")
.When(LegacyHost, HttpStatusCode.NotFound, "");
var check = await BuildService(handler).CheckAgentBinariesAsync(includeEndpointUrls: false);
Assert.Equal("warning", check.Status);
Assert.Contains("legacy", check.Message, System.StringComparison.OrdinalIgnoreCase);
Assert.Contains("404", check.Message);
}
[Fact]
public async Task Check_AliasMissing_ReportsUnhealthy()
{
var handler = new StubHttpMessageHandler()
.When(AliasHost, HttpStatusCode.NotFound, "")
.When(LegacyHost, HttpStatusCode.OK, "");
var check = await BuildService(handler).CheckAgentBinariesAsync(includeEndpointUrls: false);
Assert.Equal("unhealthy", check.Status);
Assert.Contains("download alias", check.Message);
}
[Fact]
public async Task Check_EndpointsUnreachable_ReportsUnhealthy()
{
// Un-scripted URLs make the stub throw — models a connection failure.
var check = await BuildService(new StubHttpMessageHandler()).CheckAgentBinariesAsync(includeEndpointUrls: false);
Assert.Equal("unhealthy", check.Status);
Assert.Contains("unreachable", check.Message);
}
private static HealthCheckService BuildService(StubHttpMessageHandler handler)
{
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>())
.Build();
return new HealthCheckService(
NullLogger<HealthCheckService>.Instance,
adminConfigService: null!,
httpClientFactory: new StubHttpClientFactory(handler),
metricsReader: null!,
poisonQueueProbe: null!,
configuration: config);
}
}
@@ -53,6 +53,29 @@ public class HealthCheckServiceMcpServerTests
Assert.DoesNotContain("http", check.Message, System.StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task Check_WithEndpointUrl_SurfacesServerUrlAlongsideVersion()
{
var svc = BuildService(HttpStatusCode.OK, "{\"status\":\"healthy\",\"version\":\"1.4.0\"}");
var check = await svc.CheckMcpServerAsync(includeEndpointUrl: true);
// The Version detail must merge into — not replace — the Server URL detail.
Assert.Equal("https://mcp.example.test", check.Details!["Server URL"]);
Assert.Equal("1.4.0", check.Details["Version"]);
}
[Fact]
public async Task Check_WithEndpointUrl_SurfacesServerUrlEvenWhenUnhealthy()
{
var svc = BuildService(HttpStatusCode.ServiceUnavailable, "");
var check = await svc.CheckMcpServerAsync(includeEndpointUrl: true);
Assert.Equal("unhealthy", check.Status);
Assert.Equal("https://mcp.example.test", check.Details!["Server URL"]);
}
[Fact]
public async Task Check_TimeoutOrConnectionFailure_ReportsWarningForColdStart()
{
@@ -102,17 +102,19 @@ namespace AutopilotMonitor.Functions.Functions.Infrastructure
// Authentication + GlobalAdminOnly authorization enforced by PolicyEnforcementMiddleware
// Perform comprehensive health checks
var healthCheckResult = await _healthCheckService.PerformAllChecksAsync();
// SignalR Quota check exposes the subscription/resource-group path of the
// backing SignalR resource, and Poison Queues exposes internal async-worker
// queue topology — restrict both to Global Admins. Non-GA callers
// (Tenant Admins, Operators) get the rest of the report unchanged.
// SignalR Quota exposes the subscription/resource-group path of the backing
// SignalR resource, Poison Queues exposes internal async-worker queue topology,
// and endpoint URLs in the check details expose infrastructure hostnames —
// restrict all of that to Global Admins. Non-GA callers (Tenant Admins,
// Operators) get the rest of the report unchanged.
// The route is registered as AuthenticatedUser, so PolicyEnforcementMiddleware
// does NOT compute IsGlobalAdmin; resolve it directly via GlobalAdminService.
var requestCtx = context.GetRequestContext();
var isGlobalAdmin = await _globalAdminService.IsGlobalAdminAsync(requestCtx.UserPrincipalName);
// Perform comprehensive health checks (endpoint URLs only for Global Admins)
var healthCheckResult = await _healthCheckService.PerformAllChecksAsync(includeEndpointUrls: isGlobalAdmin);
var visibleChecks = isGlobalAdmin
? healthCheckResult.Checks
: healthCheckResult.Checks
@@ -146,10 +148,14 @@ namespace AutopilotMonitor.Functions.Functions.Infrastructure
/// </summary>
[Function("McpHealthCheck")]
public async Task<HttpResponseData> GetMcpHealthCheck(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "health/mcp")] HttpRequestData req)
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "health/mcp")] HttpRequestData req,
FunctionContext context)
{
// Authentication enforced by PolicyEnforcementMiddleware (AuthenticatedUser).
var check = await _healthCheckService.CheckMcpServerAsync();
// The MCP server URL is infrastructure topology — include it only for Global Admins.
var requestCtx = context.GetRequestContext();
var isGlobalAdmin = await _globalAdminService.IsGlobalAdminAsync(requestCtx.UserPrincipalName);
var check = await _healthCheckService.CheckMcpServerAsync(includeEndpointUrl: isGlobalAdmin);
var response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(new
@@ -34,9 +34,12 @@ public class HealthCheckService
}
/// <summary>
/// Performs all health checks and returns the results
/// Performs all health checks and returns the results.
/// <paramref name="includeEndpointUrls"/> adds the probed/backing endpoint URLs to the
/// per-check details — infrastructure topology, so callers must only enable it for
/// Global Admins.
/// </summary>
public async Task<HealthCheckResult> PerformAllChecksAsync()
public async Task<HealthCheckResult> PerformAllChecksAsync(bool includeEndpointUrls = false)
{
var result = new HealthCheckResult
{
@@ -50,9 +53,9 @@ public class HealthCheckService
// one slow check. It is exposed via its own endpoint (GET health/mcp) that the
// frontend fetches independently and folds into the card grid incrementally.
var checks = await Task.WhenAll(
CheckStorageBackendAsync(),
CheckProcessingBackendAsync(),
CheckAgentBinariesAsync(),
CheckStorageBackendAsync(includeEndpointUrls),
CheckProcessingBackendAsync(includeEndpointUrls),
CheckAgentBinariesAsync(includeEndpointUrls),
CheckSignalRQuotaAsync(),
CheckPoisonQueuesAsync()
);
@@ -73,7 +76,7 @@ public class HealthCheckService
/// <summary>
/// Checks Azure Table Storage connectivity by querying the admin configuration table
/// </summary>
private async Task<HealthCheck> CheckStorageBackendAsync()
private async Task<HealthCheck> CheckStorageBackendAsync(bool includeEndpointUrls)
{
var check = new HealthCheck
{
@@ -97,29 +100,62 @@ public class HealthCheckService
_logger.LogError(ex, "Storage backend health check failed");
}
if (includeEndpointUrls)
{
// Same resolution order as TableStorageService: account name (Managed Identity)
// is the production path, connection string is the local-dev fallback.
var accountName = _configuration["AzureStorageAccountName"];
check.Details = new Dictionary<string, object>
{
["Table endpoint"] = string.IsNullOrWhiteSpace(accountName)
? "connection string (local dev)"
: $"https://{accountName}.table.core.windows.net"
};
}
return check;
}
/// <summary>
/// Checks that the Azure Functions host is running
/// </summary>
private static Task<HealthCheck> CheckProcessingBackendAsync()
private static Task<HealthCheck> CheckProcessingBackendAsync(bool includeEndpointUrls)
{
var uptime = DateTime.UtcNow - System.Diagnostics.Process.GetCurrentProcess().StartTime.ToUniversalTime();
return Task.FromResult(new HealthCheck
var check = new HealthCheck
{
Name = "Processing Backend",
Description = "Application host process",
Status = "healthy",
Message = $"Host running (uptime: {(int)uptime.TotalMinutes}m {uptime.Seconds}s)"
});
};
if (includeEndpointUrls)
{
// WEBSITE_HOSTNAME is stamped by App Service and reflects the host actually
// serving this process — no hardcoded URL to go stale after a migration.
var hostname = Environment.GetEnvironmentVariable("WEBSITE_HOSTNAME");
if (!string.IsNullOrWhiteSpace(hostname))
{
check.Details = new Dictionary<string, object>
{
["API endpoint"] = $"https://{hostname}"
};
}
}
return Task.FromResult(check);
}
/// <summary>
/// Checks that the agent binaries (ZIP) and bootstrap script (PS1) are reachable in blob storage
/// Checks that the agent binaries (ZIP) and bootstrap script (PS1) are reachable on the
/// canonical download alias (Front Door → current blob origin) AND on the legacy blob
/// keepalive account that already-deployed customer bootstrap scripts still use.
/// Alias failure is unhealthy (new installs break); legacy failure is a warning
/// (existing customers break until they migrate to the alias).
/// </summary>
private async Task<HealthCheck> CheckAgentBinariesAsync()
internal async Task<HealthCheck> CheckAgentBinariesAsync(bool includeEndpointUrls)
{
var check = new HealthCheck
{
@@ -127,8 +163,20 @@ public class HealthCheckService
Description = "Agent download package availability"
};
var zipUrl = $"{Constants.AgentBlobBaseUrl}/{Constants.AgentZipFileName}";
var ps1Url = $"{Constants.AgentBlobBaseUrl}/Install-AutopilotMonitor.ps1";
var aliasZipUrl = $"{Constants.AgentDownloadBaseUrl}/{Constants.AgentZipFileName}";
var aliasPs1Url = $"{Constants.AgentDownloadBaseUrl}/{Constants.BootstrapScriptName}";
var legacyZipUrl = $"{Constants.AgentBlobBaseUrl}/{Constants.AgentZipFileName}";
var legacyPs1Url = $"{Constants.AgentBlobBaseUrl}/{Constants.BootstrapScriptName}";
if (includeEndpointUrls)
{
check.Details = new Dictionary<string, object>
{
["Agent ZIP"] = aliasZipUrl,
["Bootstrap script"] = aliasPs1Url,
["Legacy blob (keepalive)"] = Constants.AgentBlobBaseUrl
};
}
try
{
@@ -138,40 +186,48 @@ public class HealthCheckService
var sw = System.Diagnostics.Stopwatch.StartNew();
// HEAD requests to check availability without downloading
var zipRequest = new HttpRequestMessage(HttpMethod.Head, zipUrl);
var ps1Request = new HttpRequestMessage(HttpMethod.Head, ps1Url);
var results = await Task.WhenAll(
client.SendAsync(zipRequest),
client.SendAsync(ps1Request)
client.SendAsync(new HttpRequestMessage(HttpMethod.Head, aliasZipUrl)),
client.SendAsync(new HttpRequestMessage(HttpMethod.Head, aliasPs1Url)),
client.SendAsync(new HttpRequestMessage(HttpMethod.Head, legacyZipUrl)),
client.SendAsync(new HttpRequestMessage(HttpMethod.Head, legacyPs1Url))
);
sw.Stop();
var zipResponse = results[0];
var ps1Response = results[1];
var aliasZipOk = results[0].IsSuccessStatusCode;
var aliasPs1Ok = results[1].IsSuccessStatusCode;
var legacyZipOk = results[2].IsSuccessStatusCode;
var legacyPs1Ok = results[3].IsSuccessStatusCode;
var zipOk = zipResponse.IsSuccessStatusCode;
var ps1Ok = ps1Response.IsSuccessStatusCode;
var issues = new List<string>();
if (!aliasZipOk) issues.Add($"Agent ZIP (download alias): {(int)results[0].StatusCode}");
if (!aliasPs1Ok) issues.Add($"Bootstrap script (download alias): {(int)results[1].StatusCode}");
if (!legacyZipOk) issues.Add($"Agent ZIP (legacy blob): {(int)results[2].StatusCode}");
if (!legacyPs1Ok) issues.Add($"Bootstrap script (legacy blob): {(int)results[3].StatusCode}");
if (zipOk && ps1Ok)
if (issues.Count == 0)
{
check.Status = "healthy";
check.Message = $"Agent package and bootstrap script available ({sw.ElapsedMilliseconds}ms)";
check.Message = $"Agent package and bootstrap script available via download alias + legacy blob ({sw.ElapsedMilliseconds}ms)";
}
else if (aliasZipOk && aliasPs1Ok)
{
// Alias intact, legacy keepalive degraded — existing customer bootstrap
// scripts still download from the legacy account until they migrate.
check.Status = "warning";
check.Message = $"Download alias OK, but legacy keepalive degraded — {string.Join(", ", issues)}";
}
else
{
check.Status = "unhealthy";
var issues = new List<string>();
if (!zipOk) issues.Add($"Agent ZIP: {(int)zipResponse.StatusCode}");
if (!ps1Ok) issues.Add($"Bootstrap script: {(int)ps1Response.StatusCode}");
check.Message = $"Missing: {string.Join(", ", issues)}";
}
}
catch (Exception ex)
{
check.Status = "unhealthy";
check.Message = $"Blob storage unreachable: {ex.Message}";
check.Message = $"Agent download endpoints unreachable: {ex.Message}";
_logger.LogError(ex, "Agent binaries health check failed");
}
@@ -182,7 +238,8 @@ public class HealthCheckService
/// Probes the MCP server's public <c>/health</c> endpoint (which sits in front of the
/// <c>/mcp</c> access guard, so no auth is required) and surfaces reachability + the
/// deployed MCP build version. Visible to all authenticated users, so the server URL is
/// deliberately NOT included in the message or details.
/// only included in the details when <paramref name="includeEndpointUrl"/> is set —
/// callers must only enable it for Global Admins.
/// <para>
/// The Container App runs with minReplicas=0 and scales to zero on idle. Azure Container
/// Apps holds an inbound request while it activates a cold replica, so this probe doubles
@@ -193,7 +250,7 @@ public class HealthCheckService
/// the blocking all-checks batch, so the dashboard never waits on the wake.
/// </para>
/// </summary>
internal async Task<HealthCheck> CheckMcpServerAsync()
internal async Task<HealthCheck> CheckMcpServerAsync(bool includeEndpointUrl = false)
{
var check = new HealthCheck
{
@@ -203,6 +260,11 @@ public class HealthCheckService
var baseUrl = (_configuration["McpServerUrl"] ?? Constants.McpServerBaseUrl).TrimEnd('/');
var healthUrl = $"{baseUrl}/health";
if (includeEndpointUrl)
{
check.Details = new Dictionary<string, object> { ["Server URL"] = baseUrl };
}
var timeoutSeconds = ParsePositiveInt("McpServerHealthTimeoutSeconds", 30);
// A cold start that takes longer than this is treated as a successful wake rather
@@ -233,7 +295,8 @@ public class HealthCheckService
: $"MCP server reachable ({sw.ElapsedMilliseconds}ms)";
if (!string.IsNullOrWhiteSpace(version))
{
check.Details = new Dictionary<string, object> { ["Version"] = version };
check.Details ??= new Dictionary<string, object>();
check.Details["Version"] = version;
}
}
catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException or HttpRequestException)
@@ -554,52 +554,53 @@ namespace AutopilotMonitor.Functions.Services
}
/// <summary>
/// Verifies that agent binaries and bootstrap script are available on blob storage.
/// Verifies that agent binaries and bootstrap script are available on the canonical
/// download alias (Front Door → current blob origin) AND on the legacy blob keepalive
/// account that already-deployed customer bootstrap scripts still use.
/// Records an OpsEvent for each missing item so Global Admins see it in the dashboard.
/// </summary>
private async Task CheckAgentBlobStorageAsync()
{
_logger.LogInformation("Checking agent blob storage availability...");
_logger.LogInformation("Checking agent download endpoint availability...");
var zipUrl = $"{AutopilotMonitor.Shared.Constants.AgentBlobBaseUrl}/{AutopilotMonitor.Shared.Constants.AgentZipFileName}";
var ps1Url = $"{AutopilotMonitor.Shared.Constants.AgentBlobBaseUrl}/Install-AutopilotMonitor.ps1";
var probes = new (string Label, string Url)[]
{
($"Agent ZIP ({AutopilotMonitor.Shared.Constants.AgentZipFileName}, download alias)",
$"{AutopilotMonitor.Shared.Constants.AgentDownloadBaseUrl}/{AutopilotMonitor.Shared.Constants.AgentZipFileName}"),
($"Bootstrap script ({AutopilotMonitor.Shared.Constants.BootstrapScriptName}, download alias)",
$"{AutopilotMonitor.Shared.Constants.AgentDownloadBaseUrl}/{AutopilotMonitor.Shared.Constants.BootstrapScriptName}"),
($"Agent ZIP ({AutopilotMonitor.Shared.Constants.AgentZipFileName}, legacy blob keepalive)",
$"{AutopilotMonitor.Shared.Constants.AgentBlobBaseUrl}/{AutopilotMonitor.Shared.Constants.AgentZipFileName}"),
($"Bootstrap script ({AutopilotMonitor.Shared.Constants.BootstrapScriptName}, legacy blob keepalive)",
$"{AutopilotMonitor.Shared.Constants.AgentBlobBaseUrl}/{AutopilotMonitor.Shared.Constants.BootstrapScriptName}")
};
try
{
var client = _httpClientFactory.CreateClient();
client.Timeout = TimeSpan.FromSeconds(15);
var zipRequest = new HttpRequestMessage(HttpMethod.Head, zipUrl);
var ps1Request = new HttpRequestMessage(HttpMethod.Head, ps1Url);
var results = await Task.WhenAll(
client.SendAsync(zipRequest),
client.SendAsync(ps1Request)
probes.Select(p => client.SendAsync(new HttpRequestMessage(HttpMethod.Head, p.Url)))
);
var zipResponse = results[0];
var ps1Response = results[1];
if (!zipResponse.IsSuccessStatusCode)
var allOk = true;
for (var i = 0; i < probes.Length; i++)
{
_logger.LogError("Agent ZIP not available on blob storage: HTTP {StatusCode}", (int)zipResponse.StatusCode);
await _opsEventService.RecordBlobStorageMissingAsync("Agent ZIP (AutopilotMonitor-Agent.zip)", (int)zipResponse.StatusCode);
if (results[i].IsSuccessStatusCode) continue;
allOk = false;
_logger.LogError("{Item} not available: HTTP {StatusCode}", probes[i].Label, (int)results[i].StatusCode);
await _opsEventService.RecordBlobStorageMissingAsync(probes[i].Label, (int)results[i].StatusCode);
}
if (!ps1Response.IsSuccessStatusCode)
if (allOk)
{
_logger.LogError("Bootstrap script not available on blob storage: HTTP {StatusCode}", (int)ps1Response.StatusCode);
await _opsEventService.RecordBlobStorageMissingAsync("Bootstrap script (Install-AutopilotMonitor.ps1)", (int)ps1Response.StatusCode);
}
if (zipResponse.IsSuccessStatusCode && ps1Response.IsSuccessStatusCode)
{
_logger.LogInformation("Agent blob storage check passed: all binaries available");
_logger.LogInformation("Agent download endpoint check passed: all binaries available on alias + legacy blob");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Agent blob storage check failed — storage unreachable");
_logger.LogError(ex, "Agent download endpoint check failed — endpoints unreachable");
await _opsEventService.RecordBlobStorageUnreachableAsync(ex.Message);
}
}
@@ -73,19 +73,32 @@ namespace AutopilotMonitor.Shared
public const string ApiBaseUrl = "https://autopilotmonitor-api-eu.azurewebsites.net";
/// <summary>
/// Public base URL of the MCP server Container App (no trailing slash, no /mcp suffix).
/// Used by the backend health check to probe <c>{McpServerBaseUrl}/health</c>. The
/// Container App runs with minReplicas=0, so the first probe after idle may incur a
/// cold start. Overridable via the <c>McpServerUrl</c> app setting (e.g. for dev).
/// Public base URL of the MCP server (no trailing slash, no /mcp suffix). Stable custom
/// domain in front of the Container App — survives region/environment moves, unlike the
/// generated *.azurecontainerapps.io FQDN. Used by the backend health check to probe
/// <c>{McpServerBaseUrl}/health</c>. The Container App runs with minReplicas=0, so the
/// first probe after idle may incur a cold start. Overridable via the
/// <c>McpServerUrl</c> app setting (e.g. for dev).
/// </summary>
public const string McpServerBaseUrl = "https://autopilotmonitor-mcp.kindwave-58b4b547.westeurope.azurecontainerapps.io";
public const string McpServerBaseUrl = "https://mcp.autopilotmonitor.com";
// -----------------------------------------------------------------------
// Agent self-update
// -----------------------------------------------------------------------
/// <summary>
/// Base URL for agent binaries in Azure Blob Storage (public read)
/// Canonical public base URL for agent binaries: Front Door alias in front of the
/// current blob origin (autopilotmonitoreu, container "agent"). Storage-account moves
/// happen behind this name without touching customers. New bootstrap scripts and all
/// availability probes use this URL.
/// </summary>
public const string AgentDownloadBaseUrl = "https://download.autopilotmonitor.com/agent";
/// <summary>
/// LEGACY base URL for agent binaries: the pre-migration storage account, kept alive
/// (dual-push on every release) because bootstrap scripts already deployed in customer
/// Intune tenants still download from it. Remove only after the customer migration to
/// <see cref="AgentDownloadBaseUrl"/> is complete and the legacy account is torn down.
/// </summary>
public const string AgentBlobBaseUrl = "https://autopilotmonitor.blob.core.windows.net/agent";