diff --git a/src/Backend/AutopilotMonitor.Functions.Tests/AuthFunctionSideEffectTests.cs b/src/Backend/AutopilotMonitor.Functions.Tests/AuthFunctionSideEffectTests.cs
index 9f4fed41..a2dd24ca 100644
--- a/src/Backend/AutopilotMonitor.Functions.Tests/AuthFunctionSideEffectTests.cs
+++ b/src/Backend/AutopilotMonitor.Functions.Tests/AuthFunctionSideEffectTests.cs
@@ -44,7 +44,7 @@ public class AuthFunctionSideEffectTests
var delegatedAdminMock = new Mock(
adminRepo,
- new StubTenantEntitlementService(AutopilotMonitor.Functions.Security.TenantEdition.Enterprise),
+ new StubTenantEntitlementService(AutopilotMonitor.Functions.Security.TenantEdition.Pro),
cache, Mock.Of>())
{ CallBase = false };
delegatedAdminMock.Setup(x => x.GetScopeAsync(It.IsAny(), It.IsAny()))
diff --git a/src/Backend/AutopilotMonitor.Functions.Tests/DelegatedAdminEditionGateTests.cs b/src/Backend/AutopilotMonitor.Functions.Tests/DelegatedAdminEditionGateTests.cs
index 2e3abf40..2144af3f 100644
--- a/src/Backend/AutopilotMonitor.Functions.Tests/DelegatedAdminEditionGateTests.cs
+++ b/src/Backend/AutopilotMonitor.Functions.Tests/DelegatedAdminEditionGateTests.cs
@@ -44,7 +44,7 @@ public class DelegatedAdminEditionGateTests
return new DelegatedAdminService(
repo.Object,
new StubTenantEntitlementService(
- tenantId => tenantId == EnterpriseHomeTenant ? TenantEdition.Enterprise : TenantEdition.Community),
+ tenantId => tenantId == EnterpriseHomeTenant ? TenantEdition.Pro : TenantEdition.Community),
new MemoryCache(new MemoryCacheOptions()),
NullLogger.Instance);
}
@@ -118,7 +118,7 @@ public class DelegatedAdminEditionGateTests
var svc = new DelegatedAdminService(
repo.Object,
new StubTenantEntitlementService(
- tenantId => tenantId == EnterpriseHomeTenant ? TenantEdition.Enterprise : TenantEdition.Community),
+ tenantId => tenantId == EnterpriseHomeTenant ? TenantEdition.Pro : TenantEdition.Community),
new MemoryCache(new MemoryCacheOptions()),
NullLogger.Instance);
diff --git a/src/Backend/AutopilotMonitor.Functions.Tests/DelegatedAdminServiceTests.cs b/src/Backend/AutopilotMonitor.Functions.Tests/DelegatedAdminServiceTests.cs
index d757ffc4..d3406760 100644
--- a/src/Backend/AutopilotMonitor.Functions.Tests/DelegatedAdminServiceTests.cs
+++ b/src/Backend/AutopilotMonitor.Functions.Tests/DelegatedAdminServiceTests.cs
@@ -36,7 +36,7 @@ public class DelegatedAdminServiceTests
// the edition filter has its own dedicated tests (DelegatedAdminEditionGateTests).
var svc = new DelegatedAdminService(
repo.Object,
- new StubTenantEntitlementService(AutopilotMonitor.Functions.Security.TenantEdition.Enterprise),
+ new StubTenantEntitlementService(AutopilotMonitor.Functions.Security.TenantEdition.Pro),
cache,
NullLogger.Instance);
return (svc, repo);
diff --git a/src/Backend/AutopilotMonitor.Functions.Tests/FeatureEntitlementCatalogTests.cs b/src/Backend/AutopilotMonitor.Functions.Tests/FeatureEntitlementCatalogTests.cs
index 8c5af094..aac2c75a 100644
--- a/src/Backend/AutopilotMonitor.Functions.Tests/FeatureEntitlementCatalogTests.cs
+++ b/src/Backend/AutopilotMonitor.Functions.Tests/FeatureEntitlementCatalogTests.cs
@@ -6,8 +6,9 @@ namespace AutopilotMonitor.Functions.Tests;
///
/// Pins the edition-resolution matrix and the per-edition entitlement values of
/// . Fail-closed contract: ONLY the exact tier
-/// "enterprise" (or an active trial) yields Enterprise — legacy stored tiers ("free", "pro"),
-/// null/empty and unknown values all resolve to Community without any data migration.
+/// "pro" / the legacy stored value "enterprise" (or an active trial) yields Pro — the legacy
+/// stored tier "free", null/empty and unknown values all resolve to Community without any
+/// data migration.
///
public class FeatureEntitlementCatalogTests
{
@@ -16,31 +17,34 @@ public class FeatureEntitlementCatalogTests
// ── ResolveEdition matrix ────────────────────────────────────────────────────
[Theory]
- [InlineData("enterprise")]
+ [InlineData("pro")]
+ [InlineData("Pro")]
+ [InlineData("PRO")]
+ [InlineData(" pro ")]
+ [InlineData("enterprise")] // legacy stored value — must stay readable as Pro
[InlineData("Enterprise")]
[InlineData("ENTERPRISE")]
[InlineData(" enterprise ")]
- public void ResolveEdition_EnterpriseTier_IsEnterprise(string tier)
+ public void ResolveEdition_ProOrLegacyEnterpriseTier_IsPro(string tier)
{
- Assert.Equal(TenantEdition.Enterprise, FeatureEntitlementCatalog.ResolveEdition(tier, null, Now));
+ Assert.Equal(TenantEdition.Pro, FeatureEntitlementCatalog.ResolveEdition(tier, null, Now));
}
[Theory]
[InlineData("free")]
- [InlineData("pro")]
[InlineData("community")]
[InlineData("")]
[InlineData(null)]
[InlineData("premium")] // unknown value → fail-closed
- public void ResolveEdition_NonEnterpriseTier_IsCommunity(string? tier)
+ public void ResolveEdition_NonProTier_IsCommunity(string? tier)
{
Assert.Equal(TenantEdition.Community, FeatureEntitlementCatalog.ResolveEdition(tier, null, Now));
}
[Fact]
- public void ResolveEdition_ActiveTrial_IsEnterprise_EvenOnFreeTier()
+ public void ResolveEdition_ActiveTrial_IsPro_EvenOnFreeTier()
{
- Assert.Equal(TenantEdition.Enterprise,
+ Assert.Equal(TenantEdition.Pro,
FeatureEntitlementCatalog.ResolveEdition("free", Now.AddSeconds(1), Now));
}
@@ -59,11 +63,28 @@ public class FeatureEntitlementCatalogTests
FeatureEntitlementCatalog.ResolveEdition(null, Now.AddDays(-1), Now));
}
- [Fact]
- public void ResolveEdition_ExpiredTrial_ButEnterpriseTier_StaysEnterprise()
+ [Theory]
+ [InlineData("pro")]
+ [InlineData("enterprise")]
+ public void ResolveEdition_ExpiredTrial_ButPermanentProTier_StaysPro(string tier)
{
- Assert.Equal(TenantEdition.Enterprise,
- FeatureEntitlementCatalog.ResolveEdition("enterprise", Now.AddDays(-1), Now));
+ Assert.Equal(TenantEdition.Pro,
+ FeatureEntitlementCatalog.ResolveEdition(tier, Now.AddDays(-1), Now));
+ }
+
+ // ── IsPermanentProTier ───────────────────────────────────────────────────────
+
+ [Theory]
+ [InlineData("pro", true)]
+ [InlineData(" PRO ", true)]
+ [InlineData("enterprise", true)] // legacy stored value
+ [InlineData("free", false)]
+ [InlineData("community", false)]
+ [InlineData("", false)]
+ [InlineData(null, false)]
+ public void IsPermanentProTier_Matrix(string? tier, bool expected)
+ {
+ Assert.Equal(expected, FeatureEntitlementCatalog.IsPermanentProTier(tier));
}
// ── Entitlement values ───────────────────────────────────────────────────────
@@ -76,20 +97,24 @@ public class FeatureEntitlementCatalogTests
Assert.Null(e.UserRateLimitPerMinute);
Assert.Null(e.DeviceRateLimitPerMinute);
Assert.False(e.DelegatedAdminAllowed);
+ Assert.False(e.BootstrapIncluded);
+ Assert.False(e.UnrestrictedModeAvailable);
Assert.Equal("community", e.McpUsagePlanName);
Assert.Equal(100, e.McpDailyRequestLimit);
Assert.Equal(3000, e.McpMonthlyRequestLimit);
}
[Fact]
- public void Enterprise_Entitlements_MatchMatrix()
+ public void Pro_Entitlements_MatchMatrix()
{
- var e = FeatureEntitlementCatalog.Get(TenantEdition.Enterprise);
+ var e = FeatureEntitlementCatalog.Get(TenantEdition.Pro);
Assert.Equal(365, e.RetentionCapDays);
Assert.Equal(150, e.UserRateLimitPerMinute);
Assert.Equal(150, e.DeviceRateLimitPerMinute);
Assert.True(e.DelegatedAdminAllowed);
- Assert.Equal("enterprise", e.McpUsagePlanName);
+ Assert.True(e.BootstrapIncluded);
+ Assert.True(e.UnrestrictedModeAvailable);
+ Assert.Equal("pro", e.McpUsagePlanName);
Assert.Equal(1000, e.McpDailyRequestLimit);
Assert.Equal(20000, e.McpMonthlyRequestLimit);
}
diff --git a/src/Backend/AutopilotMonitor.Functions.Tests/GetTenantFeatureFlagsPayloadTests.cs b/src/Backend/AutopilotMonitor.Functions.Tests/GetTenantFeatureFlagsPayloadTests.cs
index d3d728e4..81ea201e 100644
--- a/src/Backend/AutopilotMonitor.Functions.Tests/GetTenantFeatureFlagsPayloadTests.cs
+++ b/src/Backend/AutopilotMonitor.Functions.Tests/GetTenantFeatureFlagsPayloadTests.cs
@@ -54,11 +54,13 @@ public class GetTenantFeatureFlagsPayloadTests
{
var config = new TenantConfiguration
{
+ PlanTier = "pro", // unrestrictedMode is EFFECTIVE — requires Pro + gate + toggle
BootstrapTokenEnabled = true,
ValidateAutopilotDevice = true,
ShowScriptOutput = false,
EnableSoftwareInventoryAnalyzer = true,
EnableIntegrityBypassAnalyzer = false,
+ UnrestrictedModeEnabled = true,
UnrestrictedMode = true,
};
@@ -72,6 +74,55 @@ public class GetTenantFeatureFlagsPayloadTests
Assert.True(element.GetProperty("unrestrictedMode").GetBoolean());
}
+ // ── Effective feature gates (plan-derived) ──────────────────────────────
+
+ [Fact]
+ public void Payload_Bootstrap_IncludedInPro_WithoutGaFlag()
+ {
+ var element = Serialize(new TenantConfiguration { PlanTier = "pro", BootstrapTokenEnabled = false });
+ Assert.True(element.GetProperty("bootstrapTokenEnabled").GetBoolean());
+ }
+
+ [Fact]
+ public void Payload_Bootstrap_GaFlagStillWorksForCommunity()
+ {
+ var element = Serialize(new TenantConfiguration { PlanTier = "free", BootstrapTokenEnabled = true });
+ Assert.True(element.GetProperty("bootstrapTokenEnabled").GetBoolean());
+ }
+
+ [Fact]
+ public void Payload_Bootstrap_CommunityWithoutFlag_Disabled()
+ {
+ var element = Serialize(new TenantConfiguration { PlanTier = "free", BootstrapTokenEnabled = false });
+ Assert.False(element.GetProperty("bootstrapTokenEnabled").GetBoolean());
+ }
+
+ [Fact]
+ public void Payload_UnrestrictedMode_CommunityTenant_ForcedOff_DespiteBothFlags()
+ {
+ // Fail-closed re-gate: a downgraded/expired tenant keeps its stored flags but the
+ // effective value must be false.
+ var element = Serialize(new TenantConfiguration
+ {
+ PlanTier = "free",
+ UnrestrictedModeEnabled = true,
+ UnrestrictedMode = true,
+ });
+ Assert.False(element.GetProperty("unrestrictedMode").GetBoolean());
+ }
+
+ [Fact]
+ public void Payload_UnrestrictedMode_ProWithoutGaGate_Off()
+ {
+ var element = Serialize(new TenantConfiguration
+ {
+ PlanTier = "pro",
+ UnrestrictedModeEnabled = false,
+ UnrestrictedMode = true,
+ });
+ Assert.False(element.GetProperty("unrestrictedMode").GetBoolean());
+ }
+
[Fact]
public void Payload_NullableFlags_FallBackToAgentSideDefaults()
{
@@ -159,12 +210,14 @@ public class GetTenantFeatureFlagsPayloadTests
Assert.Equal("community", element.GetProperty("entitlements").GetProperty("mcpUsagePlan").GetString());
}
- [Fact]
- public void Payload_EnterpriseTier_NotTrial()
+ [Theory]
+ [InlineData("pro")]
+ [InlineData("enterprise")] // legacy stored value resolves — and reports — as "pro"
+ public void Payload_PermanentProTier_NotTrial(string storedTier)
{
- var element = Serialize(new TenantConfiguration { PlanTier = "enterprise" });
+ var element = Serialize(new TenantConfiguration { PlanTier = storedTier });
- Assert.Equal("enterprise", element.GetProperty("edition").GetString());
+ Assert.Equal("pro", element.GetProperty("edition").GetString());
Assert.False(element.GetProperty("isTrial").GetBoolean());
Assert.False(element.GetProperty("trialAvailable").GetBoolean());
Assert.Equal(365, element.GetProperty("entitlements").GetProperty("retentionCapDays").GetInt32());
@@ -178,7 +231,7 @@ public class GetTenantFeatureFlagsPayloadTests
var expiry = Now.AddDays(10);
var element = Serialize(new TenantConfiguration { TrialExpiresUtc = expiry, TrialConsumed = true });
- Assert.Equal("enterprise", element.GetProperty("edition").GetString());
+ Assert.Equal("pro", element.GetProperty("edition").GetString());
Assert.True(element.GetProperty("isTrial").GetBoolean());
Assert.False(element.GetProperty("trialAvailable").GetBoolean());
Assert.Equal(expiry, element.GetProperty("trialExpiresUtc").GetDateTime());
diff --git a/src/Backend/AutopilotMonitor.Functions.Tests/McpQuotaServiceTests.cs b/src/Backend/AutopilotMonitor.Functions.Tests/McpQuotaServiceTests.cs
index 002a27c5..8f08ba3e 100644
--- a/src/Backend/AutopilotMonitor.Functions.Tests/McpQuotaServiceTests.cs
+++ b/src/Backend/AutopilotMonitor.Functions.Tests/McpQuotaServiceTests.cs
@@ -131,14 +131,14 @@ public class McpQuotaServiceTests
}
[Fact]
- public async Task Check_EnterpriseEdition_UsesEnterpriseFallbackLimits()
+ public async Task Check_ProEdition_UsesProFallbackLimits()
{
- var svc = Build(UsageRepo(("20260707", 100)), edition: TenantEdition.Enterprise);
+ var svc = Build(UsageRepo(("20260707", 100)), edition: TenantEdition.Pro);
var d = await svc.CheckAsync(Oid, Upn, TenantId);
Assert.True(d.Allowed);
- Assert.Equal("enterprise", d.Plan);
+ Assert.Equal("pro", d.Plan);
Assert.Equal(1000, d.DailyLimit);
Assert.Equal(20000, d.MonthlyLimit);
}
diff --git a/src/Backend/AutopilotMonitor.Functions.Tests/McpUserServiceDelegatedTests.cs b/src/Backend/AutopilotMonitor.Functions.Tests/McpUserServiceDelegatedTests.cs
index 241a2992..760a7ded 100644
--- a/src/Backend/AutopilotMonitor.Functions.Tests/McpUserServiceDelegatedTests.cs
+++ b/src/Backend/AutopilotMonitor.Functions.Tests/McpUserServiceDelegatedTests.cs
@@ -36,7 +36,7 @@ public class McpUserServiceDelegatedTests
_adminRepo.Object, cache, NullLogger.Instance) { CallBase = false };
_delegatedAdmin = new Mock(
_adminRepo.Object,
- new StubTenantEntitlementService(AutopilotMonitor.Functions.Security.TenantEdition.Enterprise),
+ new StubTenantEntitlementService(AutopilotMonitor.Functions.Security.TenantEdition.Pro),
cache, NullLogger.Instance) { CallBase = false };
_adminConfig = new Mock(
Mock.Of(), NullLogger.Instance, cache) { CallBase = false };
diff --git a/src/Backend/AutopilotMonitor.Functions.Tests/PolicyEnforcementMiddlewareTests.cs b/src/Backend/AutopilotMonitor.Functions.Tests/PolicyEnforcementMiddlewareTests.cs
index 2644f2f5..c78182cb 100644
--- a/src/Backend/AutopilotMonitor.Functions.Tests/PolicyEnforcementMiddlewareTests.cs
+++ b/src/Backend/AutopilotMonitor.Functions.Tests/PolicyEnforcementMiddlewareTests.cs
@@ -89,7 +89,7 @@ public class PolicyEnforcementMiddlewareTests
// mechanics; the Enterprise-only edition gate is covered by DelegatedAdminEditionGateTests.
var delegatedAdmin = new DelegatedAdminService(
repo.Object,
- new StubTenantEntitlementService(AutopilotMonitor.Functions.Security.TenantEdition.Enterprise),
+ new StubTenantEntitlementService(AutopilotMonitor.Functions.Security.TenantEdition.Pro),
cache,
NullLogger.Instance);
var tenantAdmins = new TenantAdminsService(repo.Object, cache, NullLogger.Instance);
diff --git a/src/Backend/AutopilotMonitor.Functions.Tests/RevokeEnforcementTests.cs b/src/Backend/AutopilotMonitor.Functions.Tests/RevokeEnforcementTests.cs
index fdab1fef..f3b35c1d 100644
--- a/src/Backend/AutopilotMonitor.Functions.Tests/RevokeEnforcementTests.cs
+++ b/src/Backend/AutopilotMonitor.Functions.Tests/RevokeEnforcementTests.cs
@@ -29,7 +29,7 @@ public class RevokeEnforcementTests
private static DelegatedAdminService BuildService(Mock repo) =>
new(
repo.Object,
- new StubTenantEntitlementService(AutopilotMonitor.Functions.Security.TenantEdition.Enterprise),
+ new StubTenantEntitlementService(AutopilotMonitor.Functions.Security.TenantEdition.Pro),
new MemoryCache(new MemoryCacheOptions()),
NullLogger.Instance);
diff --git a/src/Backend/AutopilotMonitor.Functions.Tests/TenantEntitlementServiceTests.cs b/src/Backend/AutopilotMonitor.Functions.Tests/TenantEntitlementServiceTests.cs
index 7293ae65..f68b8227 100644
--- a/src/Backend/AutopilotMonitor.Functions.Tests/TenantEntitlementServiceTests.cs
+++ b/src/Backend/AutopilotMonitor.Functions.Tests/TenantEntitlementServiceTests.cs
@@ -34,7 +34,7 @@ public class TenantEntitlementServiceTests
public async Task GetEdition_EnterpriseTier_ReturnsEnterprise()
{
var (svc, _) = Build(new TenantConfiguration { TenantId = TenantId, PlanTier = "enterprise" });
- Assert.Equal(TenantEdition.Enterprise, await svc.GetEditionAsync(TenantId));
+ Assert.Equal(TenantEdition.Pro, await svc.GetEditionAsync(TenantId));
}
[Fact]
@@ -46,7 +46,7 @@ public class TenantEntitlementServiceTests
PlanTier = "free",
TrialExpiresUtc = Now.AddDays(3)
});
- Assert.Equal(TenantEdition.Enterprise, await svc.GetEditionAsync(TenantId));
+ Assert.Equal(TenantEdition.Pro, await svc.GetEditionAsync(TenantId));
}
[Fact]
@@ -111,15 +111,78 @@ public class TenantEntitlementServiceTests
[InlineData("free", 60, 60)] // below the cap — unchanged
[InlineData("free", 91, 90)] // above the Community cap — clamped
[InlineData("free", 180, 90)] // legacy stored value above cap — clamped
- [InlineData("enterprise", 180, 180)]
- [InlineData("enterprise", 365, 365)]
- [InlineData("enterprise", 400, 365)] // above the Enterprise cap — clamped
+ [InlineData("pro", 180, 180)]
+ [InlineData("pro", 365, 365)]
+ [InlineData("pro", 400, 365)] // above the Pro cap — clamped
+ [InlineData("enterprise", 180, 180)] // legacy stored value — Pro cap applies
+ [InlineData("enterprise", 400, 365)]
public void GetEffectiveRetentionDays_ClampsToEditionCap(string tier, int stored, int expected)
{
var config = new TenantConfiguration { TenantId = TenantId, PlanTier = tier, DataRetentionDays = stored };
Assert.Equal(expected, TenantEntitlementService.GetEffectiveRetentionDays(config, Now));
}
+ // ── IsBootstrapEnabled / IsUnrestrictedModeActive (effective feature gates) ──
+
+ [Theory]
+ [InlineData("pro", false, true)] // included in the plan — no GA flag needed
+ [InlineData("enterprise", false, true)] // legacy stored value counts as Pro
+ [InlineData("free", true, true)] // Community + GA flag (additive escape hatch)
+ [InlineData("free", false, false)] // Community without flag → off
+ public void IsBootstrapEnabled_PlanOrFlag(string tier, bool gaFlag, bool expected)
+ {
+ var config = new TenantConfiguration { TenantId = TenantId, PlanTier = tier, BootstrapTokenEnabled = gaFlag };
+ Assert.Equal(expected, TenantEntitlementService.IsBootstrapEnabled(config, Now));
+ }
+
+ [Fact]
+ public void IsBootstrapEnabled_ActiveTrial_CountsAsPro_AndExpiryTurnsItOff()
+ {
+ var config = new TenantConfiguration
+ {
+ TenantId = TenantId,
+ PlanTier = "free",
+ BootstrapTokenEnabled = false,
+ TrialExpiresUtc = Now.AddDays(1)
+ };
+ Assert.True(TenantEntitlementService.IsBootstrapEnabled(config, Now));
+ Assert.False(TenantEntitlementService.IsBootstrapEnabled(config, Now.AddDays(2)));
+ }
+
+ [Theory]
+ [InlineData("pro", true, true, true)] // all three conditions met
+ [InlineData("pro", true, false, false)] // tenant admin has not opted in
+ [InlineData("pro", false, true, false)] // GA on-request gate missing
+ [InlineData("free", true, true, false)] // edition re-gate: Community never unrestricted
+ public void IsUnrestrictedModeActive_RequiresProAndGateAndToggle(
+ string tier, bool gate, bool toggle, bool expected)
+ {
+ var config = new TenantConfiguration
+ {
+ TenantId = TenantId,
+ PlanTier = tier,
+ UnrestrictedModeEnabled = gate,
+ UnrestrictedMode = toggle
+ };
+ Assert.Equal(expected, TenantEntitlementService.IsUnrestrictedModeActive(config, Now));
+ }
+
+ [Fact]
+ public void IsUnrestrictedModeActive_TrialExpiry_RearmsGuardrails()
+ {
+ // A trial tenant may have Unrestricted Mode granted+enabled; expiry must fail closed.
+ var config = new TenantConfiguration
+ {
+ TenantId = TenantId,
+ PlanTier = "free",
+ UnrestrictedModeEnabled = true,
+ UnrestrictedMode = true,
+ TrialExpiresUtc = Now.AddDays(1)
+ };
+ Assert.True(TenantEntitlementService.IsUnrestrictedModeActive(config, Now));
+ Assert.False(TenantEntitlementService.IsUnrestrictedModeActive(config, Now.AddDays(2)));
+ }
+
[Fact]
public void GetEffectiveRetentionDays_ZeroInfinite_IsNeverClamped()
{
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Admin/DelegatedAdminManagementFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Admin/DelegatedAdminManagementFunction.cs
index 5240b5d9..939a8b99 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Admin/DelegatedAdminManagementFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Admin/DelegatedAdminManagementFunction.cs
@@ -68,11 +68,11 @@ public class DelegatedAdminManagementFunction
if (validationError != null)
return await Bad(req, validationError);
- // Edition note: TARGET tenants may be any edition — an MSP on Enterprise may manage Community
- // customers. The Enterprise requirement applies to the delegated admin's HOME tenant and is
+ // Edition note: TARGET tenants may be any edition — an MSP on Pro may manage Community
+ // customers. The Pro requirement applies to the delegated admin's HOME tenant and is
// enforced at resolve time (DelegatedAdminService.GetScopeAsync gates on the JWT tid). It cannot
// be checked here: at grant time only the UPN is known, and UPN-domain → tenant mapping is not
- // reliable (multi-domain tenants). Grants to non-Enterprise-homed admins are simply inert.
+ // reliable (multi-domain tenants). Grants to non-Pro-homed admins are simply inert.
var entry = await _delegatedAdminService.UpsertAsync(
body!.Upn, body.TenantId, role,
Constants.DelegatedStatus.Active, Constants.DelegatedSource.OperatorGranted, currentUpn ?? "");
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Bootstrap/BootstrapGetAgentConfigFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Bootstrap/BootstrapGetAgentConfigFunction.cs
index 9941a59e..097c1769 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Bootstrap/BootstrapGetAgentConfigFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Bootstrap/BootstrapGetAgentConfigFunction.cs
@@ -73,7 +73,7 @@ namespace AutopilotMonitor.Functions.Functions.Bootstrap
// Feature gate: bootstrap endpoints only available when explicitly enabled for this tenant
var (config, tenantExists) = await _configService.TryGetConfigurationAsync(tenantId);
- if (!tenantExists || !config.BootstrapTokenEnabled)
+ if (!tenantExists || !TenantEntitlementService.IsBootstrapEnabled(config, DateTime.UtcNow))
{
return req.CreateResponse(HttpStatusCode.NotFound);
}
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Bootstrap/BootstrapRegisterSessionFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Bootstrap/BootstrapRegisterSessionFunction.cs
index 050f5e88..900d85bc 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Bootstrap/BootstrapRegisterSessionFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Bootstrap/BootstrapRegisterSessionFunction.cs
@@ -88,7 +88,7 @@ namespace AutopilotMonitor.Functions.Functions.Bootstrap
// Feature gate: bootstrap endpoints only available when explicitly enabled for this tenant
var (config, tenantExists) = await _configService.TryGetConfigurationAsync(registration.TenantId);
- if (!tenantExists || !config.BootstrapTokenEnabled)
+ if (!tenantExists || !TenantEntitlementService.IsBootstrapEnabled(config, DateTime.UtcNow))
{
return new RegisterSessionOutput { HttpResponse = req.CreateResponse(HttpStatusCode.NotFound) };
}
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Bootstrap/BootstrapReportAgentErrorFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Bootstrap/BootstrapReportAgentErrorFunction.cs
index d597d074..be615c6e 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Bootstrap/BootstrapReportAgentErrorFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Bootstrap/BootstrapReportAgentErrorFunction.cs
@@ -72,7 +72,7 @@ namespace AutopilotMonitor.Functions.Functions.Bootstrap
// Feature gate: bootstrap endpoints only available when explicitly enabled for this tenant
var (config, tenantExists) = await _configService.TryGetConfigurationAsync(tenantId);
- if (!tenantExists || !config.BootstrapTokenEnabled)
+ if (!tenantExists || !TenantEntitlementService.IsBootstrapEnabled(config, DateTime.UtcNow))
{
return req.CreateResponse(HttpStatusCode.NotFound);
}
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Bootstrap/CreateBootstrapSessionFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Bootstrap/CreateBootstrapSessionFunction.cs
index 267c5926..ebac0b10 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Bootstrap/CreateBootstrapSessionFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Bootstrap/CreateBootstrapSessionFunction.cs
@@ -62,9 +62,9 @@ namespace AutopilotMonitor.Functions.Functions.Bootstrap
return badReq;
}
- // Check if bootstrap token feature is enabled for this tenant
+ // Check if the bootstrap feature is enabled for this tenant (Pro plan or GA flag)
var tenantConfig = await _configService.GetConfigurationAsync(tenantId);
- if (!tenantConfig.BootstrapTokenEnabled)
+ if (!TenantEntitlementService.IsBootstrapEnabled(tenantConfig, DateTime.UtcNow))
{
var disabled = req.CreateResponse(HttpStatusCode.Forbidden);
await disabled.WriteAsJsonAsync(new { error = "Bootstrap token feature is not enabled for this tenant" });
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Bootstrap/GetBootstrapScriptFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Bootstrap/GetBootstrapScriptFunction.cs
index 42eb104d..3d65ab0f 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Bootstrap/GetBootstrapScriptFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Bootstrap/GetBootstrapScriptFunction.cs
@@ -95,7 +95,7 @@ namespace AutopilotMonitor.Functions.Functions.Bootstrap
// Feature gate — same generic message as not-found so a disabled tenant is
// indistinguishable from an unknown code (no enumeration oracle).
var tenantConfig = await _configService.GetConfigurationAsync(session.TenantId);
- if (!tenantConfig.BootstrapTokenEnabled)
+ if (!TenantEntitlementService.IsBootstrapEnabled(tenantConfig, DateTime.UtcNow))
{
_logger.LogWarning("Bootstrap script for code {Code} rejected — feature disabled for tenant {TenantId}", code, session.TenantId);
return await ScriptResponseAsync(
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Bootstrap/ListBootstrapSessionsFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Bootstrap/ListBootstrapSessionsFunction.cs
index 1108091a..35c410d3 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Bootstrap/ListBootstrapSessionsFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Bootstrap/ListBootstrapSessionsFunction.cs
@@ -41,9 +41,9 @@ namespace AutopilotMonitor.Functions.Functions.Bootstrap
var requestCtx = req.GetRequestContext();
var tenantId = requestCtx.TargetTenantId;
- // Check if bootstrap token feature is enabled for this tenant
+ // Check if the bootstrap feature is enabled for this tenant (Pro plan or GA flag)
var tenantConfig = await _configService.GetConfigurationAsync(tenantId);
- if (!tenantConfig.BootstrapTokenEnabled)
+ if (!TenantEntitlementService.IsBootstrapEnabled(tenantConfig, DateTime.UtcNow))
{
// Return empty list instead of error — feature simply not visible
var emptyResponse = req.CreateResponse(HttpStatusCode.OK);
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Bootstrap/RevokeBootstrapSessionFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Bootstrap/RevokeBootstrapSessionFunction.cs
index 47e461fd..75cd48f9 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Bootstrap/RevokeBootstrapSessionFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Bootstrap/RevokeBootstrapSessionFunction.cs
@@ -45,9 +45,9 @@ namespace AutopilotMonitor.Functions.Functions.Bootstrap
var tenantId = requestCtx.TargetTenantId;
var userIdentifier = requestCtx.UserPrincipalName;
- // Check if bootstrap token feature is enabled for this tenant
+ // Check if the bootstrap feature is enabled for this tenant (Pro plan or GA flag)
var tenantConfig = await _configService.GetConfigurationAsync(tenantId);
- if (!tenantConfig.BootstrapTokenEnabled)
+ if (!TenantEntitlementService.IsBootstrapEnabled(tenantConfig, DateTime.UtcNow))
{
var disabled = req.CreateResponse(HttpStatusCode.Forbidden);
await disabled.WriteAsJsonAsync(new { error = "Bootstrap token feature is not enabled for this tenant" });
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Bootstrap/ValidateBootstrapCodeFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Bootstrap/ValidateBootstrapCodeFunction.cs
index 8b4b112b..627dfd37 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Bootstrap/ValidateBootstrapCodeFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Bootstrap/ValidateBootstrapCodeFunction.cs
@@ -79,9 +79,9 @@ namespace AutopilotMonitor.Functions.Functions.Bootstrap
return notFound;
}
- // Check if bootstrap token feature is enabled for the session's tenant
+ // Check if the bootstrap feature is enabled for the session's tenant (Pro plan or GA flag)
var tenantConfig = await _configService.GetConfigurationAsync(session.TenantId);
- if (!tenantConfig.BootstrapTokenEnabled)
+ if (!TenantEntitlementService.IsBootstrapEnabled(tenantConfig, DateTime.UtcNow))
{
_logger.LogWarning("Bootstrap code {Code} rejected — feature disabled for tenant {TenantId}", code, session.TenantId);
var disabled = req.CreateResponse(HttpStatusCode.NotFound);
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Config/GetAgentConfigFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Config/GetAgentConfigFunction.cs
index dae9b319..2a26c5a6 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Config/GetAgentConfigFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Config/GetAgentConfigFunction.cs
@@ -308,7 +308,7 @@ namespace AutopilotMonitor.Functions.Functions.Config
NtpServer = string.IsNullOrEmpty(tenantConfig.NtpServer) ? "time.windows.com" : tenantConfig.NtpServer,
EnableTimezoneAutoSet = tenantConfig.EnableTimezoneAutoSet ?? false,
SendTraceEvents = tenantConfig.SendTraceEvents,
- UnrestrictedMode = tenantConfig.UnrestrictedModeEnabled && tenantConfig.UnrestrictedMode,
+ UnrestrictedMode = TenantEntitlementService.IsUnrestrictedModeActive(tenantConfig, DateTime.UtcNow),
GatherRules = gatherRules,
ImeLogPatterns = imeLogPatterns,
WhiteGloveSealingPatternIds = adminConfig.GetWhiteGloveSealingPatternIds(),
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Config/GetTenantFeatureFlagsFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Config/GetTenantFeatureFlagsFunction.cs
index 5867b8c4..44d8f8a6 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Config/GetTenantFeatureFlagsFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Config/GetTenantFeatureFlagsFunction.cs
@@ -70,12 +70,14 @@ namespace AutopilotMonitor.Functions.Functions.Config
{
var edition = FeatureEntitlementCatalog.ResolveEdition(config.PlanTier, config.TrialExpiresUtc, nowUtc);
var entitlements = FeatureEntitlementCatalog.Get(edition);
- var isTrial = edition == TenantEdition.Enterprise &&
- !string.Equals(config.PlanTier?.Trim(), FeatureEntitlementCatalog.EnterpriseTierName, StringComparison.OrdinalIgnoreCase);
+ var isTrial = edition == TenantEdition.Pro &&
+ !FeatureEntitlementCatalog.IsPermanentProTier(config.PlanTier);
return new
{
- bootstrapTokenEnabled = config.BootstrapTokenEnabled,
+ // EFFECTIVE bootstrap availability (Pro includes it; the GA flag is the additive
+ // Community enable) — field name kept for web compatibility.
+ bootstrapTokenEnabled = TenantEntitlementService.IsBootstrapEnabled(config, nowUtc),
// Session-detail "Collect Logs" button: whether an on-demand diagnostics upload can
// succeed right now (mode not Off + a usable destination). Members below Admin cannot
// read the full config, so this boolean is their only signal. Deliberately exposes no
@@ -92,10 +94,10 @@ namespace AutopilotMonitor.Functions.Functions.Config
showScriptOutput = config.ShowScriptOutput ?? true,
enableSoftwareInventoryAnalyzer = config.EnableSoftwareInventoryAnalyzer ?? false,
enableIntegrityBypassAnalyzer = config.EnableIntegrityBypassAnalyzer ?? true,
- // Gather-rules page validation indicator. UnrestrictedMode itself is just a display
- // hint — the privileged toggle is UnrestrictedModeEnabled (admin-only, stays in
- // the full config response).
- unrestrictedMode = config.UnrestrictedMode,
+ // Gather-rules page validation indicator. EFFECTIVE value (requires Pro edition +
+ // GA gate + tenant toggle) — the privileged toggle is UnrestrictedModeEnabled
+ // (admin-only, stays in the full config response).
+ unrestrictedMode = TenantEntitlementService.IsUnrestrictedModeActive(config, nowUtc),
// Edition/entitlement surface (read-time resolution — non-sensitive by design):
// drives the EditionBadge, trial CTA and retention hint in the web UI.
edition = edition.ToString().ToLowerInvariant(),
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Config/PlanManagementFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Config/PlanManagementFunction.cs
index 93396f49..30ec5759 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Config/PlanManagementFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Config/PlanManagementFunction.cs
@@ -54,7 +54,9 @@ namespace AutopilotMonitor.Functions.Functions.Config
///
/// PATCH /api/config/{tenantId}/plan — GlobalAdminOnly (catalog-enforced).
- /// Body: { "planTier"?: "community"|"enterprise", "trialExpiresUtc"?: ISO-8601 | null }.
+ /// Body: { "planTier"?: "community"|"pro", "trialExpiresUtc"?: ISO-8601 | null }.
+ /// The legacy stored value "enterprise" stays readable (resolves as Pro) but is no longer
+ /// accepted on writes.
/// Setting a trial date grants/extends the trial (TrialConsumed is NOT touched — GA
/// re-grants stay possible); explicit null ends the trial. Absent properties are unchanged.
///
@@ -96,10 +98,10 @@ namespace AutopilotMonitor.Functions.Functions.Config
newPlanTier = tierProp.GetString()!.Trim().ToLowerInvariant();
if (newPlanTier != FeatureEntitlementCatalog.CommunityTierName &&
- newPlanTier != FeatureEntitlementCatalog.EnterpriseTierName)
+ newPlanTier != FeatureEntitlementCatalog.ProTierName)
{
return await BadRequestAsync(req,
- $"Invalid planTier. Valid values: {FeatureEntitlementCatalog.CommunityTierName}, {FeatureEntitlementCatalog.EnterpriseTierName}");
+ $"Invalid planTier. Valid values: {FeatureEntitlementCatalog.CommunityTierName}, {FeatureEntitlementCatalog.ProTierName}");
}
}
@@ -194,8 +196,8 @@ namespace AutopilotMonitor.Functions.Functions.Config
///
/// POST /api/config/{tenantId}/trial — TenantAdminOrGA (catalog-enforced). Self-service
- /// 30-day Enterprise trial, exactly once per tenant. 409 when the trial was already
- /// consumed or the tenant is already effectively Enterprise.
+ /// 30-day Pro trial, exactly once per tenant. 409 when the trial was already
+ /// consumed or the tenant is already effectively Pro.
///
[Function("StartTenantTrial")]
public async Task StartTrial(
@@ -224,18 +226,18 @@ namespace AutopilotMonitor.Functions.Functions.Config
await conflict.WriteAsJsonAsync(new
{
error = "TrialAlreadyConsumed",
- message = "This tenant has already used its one self-service Enterprise trial. Contact support to extend."
+ message = "This tenant has already used its one self-service Pro trial. Contact support to extend."
});
return conflict;
}
- if (FeatureEntitlementCatalog.ResolveEdition(config.PlanTier, config.TrialExpiresUtc, nowUtc) == TenantEdition.Enterprise)
+ if (FeatureEntitlementCatalog.ResolveEdition(config.PlanTier, config.TrialExpiresUtc, nowUtc) == TenantEdition.Pro)
{
var conflict = req.CreateResponse(HttpStatusCode.Conflict);
await conflict.WriteAsJsonAsync(new
{
- error = "AlreadyEnterprise",
- message = "This tenant is already on the Enterprise edition."
+ error = "AlreadyPro",
+ message = "This tenant is already on the Pro plan."
});
return conflict;
}
@@ -267,7 +269,7 @@ namespace AutopilotMonitor.Functions.Functions.Config
tenantId = requestCtx.TargetTenantId,
trialStartedUtc = config.TrialStartedUtc,
trialExpiresUtc = config.TrialExpiresUtc,
- effectiveEdition = FeatureEntitlementCatalog.EnterpriseTierName
+ effectiveEdition = FeatureEntitlementCatalog.ProTierName
});
return response;
}
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Config/UpdateTenantConfigurationFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Config/UpdateTenantConfigurationFunction.cs
index cef4a656..25dee074 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Config/UpdateTenantConfigurationFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Config/UpdateTenantConfigurationFunction.cs
@@ -223,7 +223,7 @@ namespace AutopilotMonitor.Functions.Functions.Config
await badRequest.WriteAsJsonAsync(new
{
success = false,
- message = $"Data retention must be between 7 and {cap} days for your plan. Upgrade to Enterprise for up to 365 days."
+ message = $"Data retention must be between 7 and {cap} days for your plan. Upgrade to Pro for up to 365 days."
});
return badRequest;
}
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Infrastructure/AuthFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Infrastructure/AuthFunction.cs
index 56d81896..7622dc49 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Infrastructure/AuthFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Infrastructure/AuthFunction.cs
@@ -94,7 +94,7 @@ public class AuthFunction
// gate below). A genuine first-user still gets their config created by HandleNewTenantDomainAsync.
var tenantConfigTask = _tenantConfigService.TryGetConfigurationAsync(tenantId);
var globalRoleTask = _globalAdminService.GetGlobalRoleAsync(upn);
- // tenantId = JWT tid = the caller's home tenant — gates the delegated (MSP) scope (Enterprise-only seat).
+ // tenantId = JWT tid = the caller's home tenant — gates the delegated (MSP) scope (Pro-only seat).
var delegatedScopeTask = _delegatedAdminService.GetScopeAsync(upn, tenantId);
var isApprovedTask = _previewWhitelistService.IsApprovedAsync(tenantId);
var membershipTask = _tenantAdminsService.GetTableMembershipAsync(tenantId, upn);
@@ -469,8 +469,12 @@ public class AuthFunction
role,
canManageBootstrapTokens,
hasMcpAccess = mcpCheck.IsAllowed,
- bootstrapTokenEnabled = tenantConfig.BootstrapTokenEnabled,
- unrestrictedModeEnabled = tenantConfig.UnrestrictedModeEnabled
+ // EFFECTIVE availability flags (drive sidebar/section visibility): bootstrap is
+ // included in Pro, Unrestricted Mode requires Pro + the GA on-request gate.
+ bootstrapTokenEnabled = TenantEntitlementService.IsBootstrapEnabled(tenantConfig, DateTime.UtcNow),
+ unrestrictedModeEnabled =
+ FeatureEntitlementCatalog.Get(TenantEntitlementService.ResolveEdition(tenantConfig, DateTime.UtcNow)).UnrestrictedModeAvailable
+ && tenantConfig.UnrestrictedModeEnabled
}, needsAutoAdmin);
}
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Infrastructure/McpUserFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Infrastructure/McpUserFunction.cs
index c5fd2566..27b8d98e 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Infrastructure/McpUserFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Infrastructure/McpUserFunction.cs
@@ -163,7 +163,7 @@ public class McpUserFunction
var principal = context.GetUser();
var upn = principal?.GetUserPrincipalName();
- // JWT tid = the caller's home tenant — gates the delegated (MSP) auto-grant (Enterprise-only seat).
+ // JWT tid = the caller's home tenant — gates the delegated (MSP) auto-grant (Pro-only seat).
var result = await _mcpUserService.IsAllowedAsync(upn, principal?.GetTenantId());
var response = req.CreateResponse(result.IsAllowed ? HttpStatusCode.OK : HttpStatusCode.Forbidden);
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Infrastructure/SignalRAddToGroupFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Infrastructure/SignalRAddToGroupFunction.cs
index 94c7df15..98d8fe4b 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Infrastructure/SignalRAddToGroupFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Infrastructure/SignalRAddToGroupFunction.cs
@@ -100,7 +100,7 @@ namespace AutopilotMonitor.Functions.Functions.Infrastructure
{
// realtime/groups/join is not a tenant-scoped route, so the middleware did not
// resolve the delegated scope onto RequestContext — resolve it here, bounded to
- // the requested tenant only. Home tid gates the MSP seat (Enterprise-only).
+ // the requested tenant only. Home tid gates the MSP seat (Pro-only).
var scope = await _delegatedAdminService.GetScopeAsync(userEmail, userTenantId);
allowedCrossTenant = scope.RoleFor(requestedTenantId) != null;
if (allowedCrossTenant)
diff --git a/src/Backend/AutopilotMonitor.Functions/Functions/Maintenance/TrialExpirySweepFunction.cs b/src/Backend/AutopilotMonitor.Functions/Functions/Maintenance/TrialExpirySweepFunction.cs
index 9bfb52f0..cee6f3a2 100644
--- a/src/Backend/AutopilotMonitor.Functions/Functions/Maintenance/TrialExpirySweepFunction.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Functions/Maintenance/TrialExpirySweepFunction.cs
@@ -10,7 +10,7 @@ using Microsoft.Extensions.Logging;
namespace AutopilotMonitor.Functions.Functions.Maintenance;
///
-/// Daily informational sweep over tenant Enterprise trials. NOT load-bearing — trial expiry
+/// Daily informational sweep over tenant Pro trials. NOT load-bearing — trial expiry
/// degrades a tenant to Community at READ time (FeatureEntitlementCatalog.ResolveEdition);
/// this timer only surfaces the transitions as ops events so the operator has visibility:
///
@@ -20,7 +20,7 @@ namespace AutopilotMonitor.Functions.Functions.Maintenance;
/// days (re-emitted each daily run until expiry —
/// acceptable for an Info-tier heads-up, no dedupe state to maintain).
///
-/// Tenants whose stored PlanTier is already the permanent "enterprise" are skipped —
+/// Tenants whose stored PlanTier is already a permanent Pro tier ("pro"/legacy "enterprise") are skipped —
/// their trial timestamps are inert leftovers and expiry changes nothing.
/// Failure never poisons the timer; the next run re-sweeps.
///
@@ -93,8 +93,8 @@ public class TrialExpirySweepFunction
if (config.TrialExpiresUtc is not DateTime expiry)
continue;
- // Permanent Enterprise: the trial timestamps are inert leftovers — expiry changes nothing.
- if (string.Equals(config.PlanTier?.Trim(), FeatureEntitlementCatalog.EnterpriseTierName, StringComparison.OrdinalIgnoreCase))
+ // Permanent Pro: the trial timestamps are inert leftovers — expiry changes nothing.
+ if (FeatureEntitlementCatalog.IsPermanentProTier(config.PlanTier))
continue;
result.TrialsSeen++;
diff --git a/src/Backend/AutopilotMonitor.Functions/Middleware/PolicyEnforcementMiddleware.cs b/src/Backend/AutopilotMonitor.Functions/Middleware/PolicyEnforcementMiddleware.cs
index cc80a5f3..0e85c67b 100644
--- a/src/Backend/AutopilotMonitor.Functions/Middleware/PolicyEnforcementMiddleware.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Middleware/PolicyEnforcementMiddleware.cs
@@ -166,7 +166,7 @@ public class PolicyEnforcementMiddleware : IFunctionsWorkerMiddleware
var isScopedRoute = catalogEntry.TenantScoping is TenantScoping.RouteParam or TenantScoping.QueryParam;
if (!hasGlobalScope && !string.IsNullOrEmpty(upn) && isScopedRoute && crossTenant)
{
- // Home-tenant (JWT tid) gates the delegated scope: MSP seats require an Enterprise home tenant.
+ // Home-tenant (JWT tid) gates the delegated scope: MSP seats require a Pro home tenant.
var scope = await _delegatedAdminService.GetScopeAsync(upn, jwtTenantId);
delegatedRole = scope.RoleFor(namedTarget);
if (delegatedRole != null)
@@ -579,7 +579,7 @@ public class PolicyEnforcementMiddleware : IFunctionsWorkerMiddleware
if (globalRole != null)
return CatalogDecisionResult.Allow(userIdentifier, globalRole, "GlobalScope");
- // Home-tenant (JWT tid) gates the delegated scope: MSP seats require an Enterprise home tenant.
+ // Home-tenant (JWT tid) gates the delegated scope: MSP seats require a Pro home tenant.
var scope = await _delegatedAdminService.GetScopeAsync(upn, jwtTenantId);
if (!scope.IsEmpty)
return CatalogDecisionResult.Allow(
diff --git a/src/Backend/AutopilotMonitor.Functions/Middleware/UserRateLimitMiddleware.cs b/src/Backend/AutopilotMonitor.Functions/Middleware/UserRateLimitMiddleware.cs
index 9ec4fc57..2c6ff794 100644
--- a/src/Backend/AutopilotMonitor.Functions/Middleware/UserRateLimitMiddleware.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Middleware/UserRateLimitMiddleware.cs
@@ -83,7 +83,7 @@ public class UserRateLimitMiddleware : IFunctionsWorkerMiddleware
tenantOverride = tenantConfig?.CustomUserRateLimitRequestsPerMinute;
}
- // Edition entitlement floor (Enterprise: 150/min): follows the caller's HOME tenant
+ // Edition entitlement floor (Pro: 150/min): follows the caller's HOME tenant
// (JWT tid) — an MSP/delegated user rides on the edition of the tenant that pays for
// their seat, not the tenant they happen to be viewing. Resolution is fail-closed
// (any error → Community → null floor → admin default applies) and served from the
diff --git a/src/Backend/AutopilotMonitor.Functions/Security/EndpointAccessPolicyCatalog.cs b/src/Backend/AutopilotMonitor.Functions/Security/EndpointAccessPolicyCatalog.cs
index d5e92462..f775948d 100644
--- a/src/Backend/AutopilotMonitor.Functions/Security/EndpointAccessPolicyCatalog.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Security/EndpointAccessPolicyCatalog.cs
@@ -519,8 +519,8 @@ public static class EndpointAccessPolicyCatalog
new("POST", "global/tenant-groups/{groupId}/assignees", EndpointPolicy.GlobalAdminOnly),
new("DELETE", "global/tenant-groups/{groupId}/assignees/{upn}", EndpointPolicy.GlobalAdminOnly),
new("PATCH", "config/{tenantId}/plan", EndpointPolicy.GlobalAdminOnly, TenantScoping.RouteParam),
- // Self-service Enterprise trial — a tenant admin may start their own tenant's one-time
- // trial; the handler enforces the once-per-tenant + already-Enterprise conflicts (409).
+ // Self-service Pro trial — a tenant admin may start their own tenant's one-time
+ // trial; the handler enforces the once-per-tenant + already-Pro conflicts (409).
new("POST", "config/{tenantId}/trial", EndpointPolicy.TenantAdminOrGA, TenantScoping.RouteParam),
new("GET", "global/config/plan-tiers", EndpointPolicy.GlobalReadOrAdmin),
new("PUT", "global/config/plan-tiers", EndpointPolicy.GlobalAdminOnly),
diff --git a/src/Backend/AutopilotMonitor.Functions/Security/FeatureEntitlementCatalog.cs b/src/Backend/AutopilotMonitor.Functions/Security/FeatureEntitlementCatalog.cs
index 5cce156d..9953cd24 100644
--- a/src/Backend/AutopilotMonitor.Functions/Security/FeatureEntitlementCatalog.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Security/FeatureEntitlementCatalog.cs
@@ -3,13 +3,13 @@ using System;
namespace AutopilotMonitor.Functions.Security
{
///
- /// The two effective tenant editions. Anything that is not provably Enterprise is Community
- /// (fail-closed) — including the legacy stored tiers "free" and "pro" and unknown values.
+ /// The two effective tenant editions. Anything that is not provably Pro is Community
+ /// (fail-closed) — including the legacy stored tier "free" and unknown values.
///
public enum TenantEdition
{
Community,
- Enterprise
+ Pro
}
///
@@ -43,11 +43,27 @@ namespace AutopilotMonitor.Functions.Security
///
/// Whether users HOMED in this tenant may hold delegated ("MSP") admin scopes over other
/// tenants. The gate applies to the delegated admin's home tenant (JWT tid) — the managed
- /// TARGET tenants may be any edition (an Enterprise MSP may manage Community customers).
+ /// TARGET tenants may be any edition (a Pro MSP may manage Community customers).
/// Enforced at resolve time in DelegatedAdminService.GetScopeAsync.
///
public bool DelegatedAdminAllowed { get; init; }
+ ///
+ /// Whether the OOBE bootstrap feature (bootstrap sessions/codes) is included in the plan.
+ /// When true, bootstrap endpoints work without the per-tenant GA flag
+ /// (TenantConfiguration.BootstrapTokenEnabled remains an additive per-tenant enable for
+ /// Community). Resolved via .
+ ///
+ public bool BootstrapIncluded { get; init; }
+
+ ///
+ /// Whether Unrestricted Mode may be active for tenants of this edition. Availability stays
+ /// on-request (the GA-only UnrestrictedModeEnabled gate) — this flag additionally binds the
+ /// feature to the plan so a trial expiry / downgrade re-arms the guardrails (fail-closed).
+ /// Resolved via .
+ ///
+ public bool UnrestrictedModeAvailable { get; init; }
+
/// Default MCP usage plan name for users of this tenant (per-user override wins).
public string McpUsagePlanName { get; init; } = string.Empty;
@@ -61,16 +77,23 @@ namespace AutopilotMonitor.Functions.Security
///
/// Single source of truth for edition resolution and the per-edition entitlement matrix.
///
- /// Resolution is computed at READ time: Enterprise ⇔ PlanTier == "enterprise" OR an active
- /// trial (TrialExpiresUtc strictly in the future). Trial expiry therefore degrades the tenant
- /// automatically without any timer or sweep. Everything else — null, empty, "free", "pro",
+ /// Resolution is computed at READ time: Pro ⇔ PlanTier == "pro" (or the legacy stored value
+ /// "enterprise", kept readable so existing rows need no migration) OR an active trial
+ /// (TrialExpiresUtc strictly in the future). Trial expiry therefore degrades the tenant
+ /// automatically without any timer or sweep. Everything else — null, empty, "free",
/// unknown — is Community (fail-closed, no data migration required).
///
public static class FeatureEntitlementCatalog
{
/// Write-side canonical tier names (the only values the plan endpoint accepts).
public const string CommunityTierName = "community";
- public const string EnterpriseTierName = "enterprise";
+ public const string ProTierName = "pro";
+
+ ///
+ /// Stored tier value written before the 2026-07 Enterprise→Pro rename. Read-side alias for
+ /// only — the plan endpoint no longer accepts it on writes.
+ ///
+ public const string LegacyEnterpriseTierName = "enterprise";
private static readonly EditionEntitlements Community = new()
{
@@ -79,41 +102,57 @@ namespace AutopilotMonitor.Functions.Security
UserRateLimitPerMinute = null, // AdminConfiguration default (120) applies
DeviceRateLimitPerMinute = null, // AdminConfiguration default (100) applies
DelegatedAdminAllowed = false,
+ BootstrapIncluded = false,
+ UnrestrictedModeAvailable = false,
McpUsagePlanName = CommunityTierName,
McpDailyRequestLimit = 100,
McpMonthlyRequestLimit = 3000
};
- private static readonly EditionEntitlements Enterprise = new()
+ private static readonly EditionEntitlements Pro = new()
{
- Edition = TenantEdition.Enterprise,
+ Edition = TenantEdition.Pro,
RetentionCapDays = 365,
UserRateLimitPerMinute = 150,
DeviceRateLimitPerMinute = 150,
DelegatedAdminAllowed = true,
- McpUsagePlanName = EnterpriseTierName,
+ BootstrapIncluded = true,
+ UnrestrictedModeAvailable = true,
+ McpUsagePlanName = ProTierName,
McpDailyRequestLimit = 1000,
McpMonthlyRequestLimit = 20000
};
+ ///
+ /// True when the stored tier is a PERMANENT Pro tier ("pro", or the legacy stored value
+ /// "enterprise"), independent of any trial. Used to separate paid tenants from trial
+ /// tenants (feature-flags isTrial, trial-expiry sweep skip).
+ ///
+ public static bool IsPermanentProTier(string? planTier)
+ {
+ var trimmed = planTier?.Trim();
+ return string.Equals(trimmed, ProTierName, StringComparison.OrdinalIgnoreCase)
+ || string.Equals(trimmed, LegacyEnterpriseTierName, StringComparison.OrdinalIgnoreCase);
+ }
+
///
/// Resolves the effective edition from the stored plan tier and trial expiry.
- /// Enterprise ⇔ exact tier "enterprise" (case-insensitive) OR trialExpiresUtc > nowUtc.
+ /// Pro ⇔ tier "pro"/legacy "enterprise" (case-insensitive) OR trialExpiresUtc > nowUtc.
/// A trial expiring exactly at is already over (strict >).
///
public static TenantEdition ResolveEdition(string? planTier, DateTime? trialExpiresUtc, DateTime nowUtc)
{
- if (string.Equals(planTier?.Trim(), EnterpriseTierName, StringComparison.OrdinalIgnoreCase))
- return TenantEdition.Enterprise;
+ if (IsPermanentProTier(planTier))
+ return TenantEdition.Pro;
if (trialExpiresUtc.HasValue && trialExpiresUtc.Value > nowUtc)
- return TenantEdition.Enterprise;
+ return TenantEdition.Pro;
return TenantEdition.Community;
}
/// Returns the entitlement set for an edition. Unknown enum values → Community (fail-closed).
public static EditionEntitlements Get(TenantEdition edition) =>
- edition == TenantEdition.Enterprise ? Enterprise : Community;
+ edition == TenantEdition.Pro ? Pro : Community;
}
}
diff --git a/src/Backend/AutopilotMonitor.Functions/Security/SecurityValidator.cs b/src/Backend/AutopilotMonitor.Functions/Security/SecurityValidator.cs
index e1cc5ae0..c942c15a 100644
--- a/src/Backend/AutopilotMonitor.Functions/Security/SecurityValidator.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Security/SecurityValidator.cs
@@ -73,7 +73,7 @@ namespace AutopilotMonitor.Functions.Security
///
/// Resolves the effective device (agent/cert) rate limit for a tenant:
/// the per-tenant override if set, otherwise the global default raised to the tenant
- /// edition's entitlement floor (Enterprise: 150/min — see FeatureEntitlementCatalog).
+ /// edition's entitlement floor (Pro: 150/min — see FeatureEntitlementCatalog).
/// The global AdminConfiguration read is served from a 5-minute in-memory cache.
///
private async Task ResolveDeviceRateLimitAsync(TenantConfiguration config)
@@ -165,8 +165,8 @@ namespace AutopilotMonitor.Functions.Security
};
}
- // Verify bootstrap token feature is enabled for this tenant
- if (!config.BootstrapTokenEnabled)
+ // Verify the bootstrap feature is enabled for this tenant (Pro plan or GA flag)
+ if (!Services.TenantEntitlementService.IsBootstrapEnabled(config, DateTime.UtcNow))
{
_logger.LogWarning("Rejected bootstrap token: feature disabled for tenant {TenantId}", tenantId);
return new SecurityValidationResult
diff --git a/src/Backend/AutopilotMonitor.Functions/Services/DelegatedAdminService.cs b/src/Backend/AutopilotMonitor.Functions/Services/DelegatedAdminService.cs
index a8f3fa1a..d38f18b1 100644
--- a/src/Backend/AutopilotMonitor.Functions/Services/DelegatedAdminService.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Services/DelegatedAdminService.cs
@@ -13,8 +13,8 @@ namespace AutopilotMonitor.Functions.Services;
/// Externally this is surfaced as "MSP mode".
///
/// The scope is keyed on UPN; assignment RESOLUTION is tid-agnostic (identical to ),
-/// but the effective scope is GATED on the caller's home tenant (JWT tid) being Enterprise — the MSP
-/// seat is an Enterprise capability of the tenant the admin is homed in, while the managed targets may
+/// but the effective scope is GATED on the caller's home tenant (JWT tid) being Pro — the MSP
+/// seat is a Pro capability of the tenant the admin is homed in, while the managed targets may
/// be any edition. Resolution is cached briefly (UPN-keyed, gate applied per call); every mutation
/// invalidates the cache.
///
@@ -50,10 +50,10 @@ public class DelegatedAdminService
/// Returns an empty (never null) scope for a UPN with no effective assignments.
///
/// Entitlement gate (single choke-point for ALL delegated-scope consumers): delegated ("MSP")
- /// access is an Enterprise capability of the caller's HOME tenant — the tenant that pays for the MSP
+ /// access is a Pro capability of the caller's HOME tenant — the tenant that pays for the MSP
/// seat ( = the validated JWT tid, unforgeable). The MANAGED
- /// (target) tenants may be any edition — an MSP on Enterprise may manage Community customers.
- /// Null/empty home tid or a non-Enterprise home tenant ⇒ empty scope (fail-closed). Grant rows stay
+ /// (target) tenants may be any edition — an MSP on Pro may manage Community customers.
+ /// Null/empty home tid or a non-Pro home tenant ⇒ empty scope (fail-closed). Grant rows stay
/// untouched; they become inert and resurrect when the home tenant upgrades / trials.
/// The gate sits AFTER the cache read so the cached scope stays pure (keyed on UPN only) and a
/// home-tenant edition change converges within the entitlement service's own config-cache TTL.
@@ -114,9 +114,9 @@ public class DelegatedAdminService
}
// Entitlement gate (single choke-point for ALL delegated-scope consumers — policy rescue,
- // AllowedTenantIds, delegated config/all, MCP auto-grant): delegated ("MSP") access is an
- // Enterprise capability of the caller's HOME tenant (the tenant paying for the MSP seat) —
- // NOT of the managed targets. An Enterprise MSP may manage Community customers. Resolution
+ // AllowedTenantIds, delegated config/all, MCP auto-grant): delegated ("MSP") access is a
+ // Pro capability of the caller's HOME tenant (the tenant paying for the MSP seat) —
+ // NOT of the managed targets. A Pro MSP may manage Community customers. Resolution
// failure / unknown home tenant counts as Community (fail-closed inside the entitlement
// service), which empties the scope. Grant rows stay untouched — inert until upgrade/trial.
var scope = new DelegatedScope(tenantRoles);
@@ -125,7 +125,7 @@ public class DelegatedAdminService
}
///
- /// Empties a non-empty scope when the caller's home tenant is not Enterprise. Applied after
+ /// Empties a non-empty scope when the caller's home tenant is not Pro. Applied after
/// every cache read/write so the cached scope stays pure (UPN-keyed) — the edition lookup has
/// its own 5-minute config cache, so this adds no per-request storage I/O in the steady state.
///
@@ -134,11 +134,11 @@ public class DelegatedAdminService
if (scope.IsEmpty)
return scope;
- if (await _entitlementService.GetEditionAsync(callerHomeTenantId) == Security.TenantEdition.Enterprise)
+ if (await _entitlementService.GetEditionAsync(callerHomeTenantId) == Security.TenantEdition.Pro)
return scope;
_logger.LogInformation(
- "[DelegatedAdmin] Suppressing delegated scope for {Upn} — home tenant {HomeTenantId} is not Enterprise (MSP seat requires Enterprise)",
+ "[DelegatedAdmin] Suppressing delegated scope for {Upn} — home tenant {HomeTenantId} is not Pro (MSP seat requires the Pro plan)",
upn, string.IsNullOrWhiteSpace(callerHomeTenantId) ? "(unknown)" : callerHomeTenantId);
return DelegatedScope.Empty;
}
diff --git a/src/Backend/AutopilotMonitor.Functions/Services/Deletion/SessionRetentionFanoutService.cs b/src/Backend/AutopilotMonitor.Functions/Services/Deletion/SessionRetentionFanoutService.cs
index 5b66337b..13d2af2e 100644
--- a/src/Backend/AutopilotMonitor.Functions/Services/Deletion/SessionRetentionFanoutService.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Services/Deletion/SessionRetentionFanoutService.cs
@@ -158,7 +158,7 @@ namespace AutopilotMonitor.Functions.Services.Deletion
{
var config = await _tenantConfig.GetConfigurationAsync(tenantId).ConfigureAwait(false);
// Edition retention cap (fail-closed backstop): the stored value is clamped to the
- // effective edition's cap at read time (Community 90 / Enterprise 365). A tenant whose
+ // effective edition's cap at read time (Community 90 / Pro 365). A tenant whose
// trial expired, or whose stored value predates the cap, is enforced here even though
// the stored DataRetentionDays is left untouched. days <= 0 stays the GA-only
// "infinite" escape hatch (skipped below, never clamped).
diff --git a/src/Backend/AutopilotMonitor.Functions/Services/McpQuotaService.cs b/src/Backend/AutopilotMonitor.Functions/Services/McpQuotaService.cs
index 7d84a2d7..728ae058 100644
--- a/src/Backend/AutopilotMonitor.Functions/Services/McpQuotaService.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Services/McpQuotaService.cs
@@ -144,8 +144,8 @@ namespace AutopilotMonitor.Functions.Services
_logger.LogWarning(ex, "[McpQuota] Plan definitions unavailable — using catalog fallback for plan {Plan}", planName);
}
- var fallback = string.Equals(planName, FeatureEntitlementCatalog.EnterpriseTierName, StringComparison.OrdinalIgnoreCase)
- ? FeatureEntitlementCatalog.Get(TenantEdition.Enterprise)
+ var fallback = FeatureEntitlementCatalog.IsPermanentProTier(planName)
+ ? FeatureEntitlementCatalog.Get(TenantEdition.Pro)
: FeatureEntitlementCatalog.Get(TenantEdition.Community);
return (planName, fallback.McpDailyRequestLimit, fallback.McpMonthlyRequestLimit);
}
diff --git a/src/Backend/AutopilotMonitor.Functions/Services/McpUserService.cs b/src/Backend/AutopilotMonitor.Functions/Services/McpUserService.cs
index ed0b3939..8dc5662c 100644
--- a/src/Backend/AutopilotMonitor.Functions/Services/McpUserService.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Services/McpUserService.cs
@@ -50,7 +50,7 @@ public class McpUserService
///
/// Checks if a user is allowed to access the MCP server based on current policy.
/// is the caller's JWT tid (home tenant) — it gates the
- /// delegated (MSP) auto-grant path, which requires an Enterprise home tenant. Null (unknown)
+ /// delegated (MSP) auto-grant path, which requires a Pro home tenant. Null (unknown)
/// fails closed to an empty delegated scope; the other grant paths are unaffected.
///
public virtual async Task IsAllowedAsync(string? upn, string? homeTenantId = null)
diff --git a/src/Backend/AutopilotMonitor.Functions/Services/OpsEventService.cs b/src/Backend/AutopilotMonitor.Functions/Services/OpsEventService.cs
index 68a3e44a..2fdc9ac3 100644
--- a/src/Backend/AutopilotMonitor.Functions/Services/OpsEventService.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Services/OpsEventService.cs
@@ -464,17 +464,17 @@ namespace AutopilotMonitor.Functions.Services
// Both types are dual-registered in OpsAlertRulesSection.tsx OPS_EVENT_TYPES
// (memory feedback_ops_event_types_dual_register). Dispatched by TrialExpirySweepFunction.
- /// Heads-up: an Enterprise trial ends within the next few days. Info-tier visibility signal.
+ /// Heads-up: a Pro trial ends within the next few days. Info-tier visibility signal.
public Task RecordTenantTrialExpiringAsync(string tenantId, string? domainName, DateTime trialExpiresUtc, int daysLeft)
{
var tenantLabel = string.IsNullOrWhiteSpace(domainName) ? tenantId : $"{domainName} ({tenantId})";
return WriteAsync(OpsEventCategory.Tenant, "TenantTrialExpiring", OpsEventSeverity.Info,
- $"Enterprise trial for tenant {tenantLabel} expires in {daysLeft} day(s) ({trialExpiresUtc:yyyy-MM-dd HH:mm}Z)",
+ $"Pro trial for tenant {tenantLabel} expires in {daysLeft} day(s) ({trialExpiresUtc:yyyy-MM-dd HH:mm}Z)",
tenantId, "System.TrialSweep", new { domainName, trialExpiresUtc, daysLeft });
}
///
- /// An Enterprise trial expired within the last sweep window — the tenant silently degraded
+ /// A Pro trial expired within the last sweep window — the tenant silently degraded
/// to Community at read time (retention cap, rate limits, MSP delegation, MCP plan).
/// Warning-tier: a conversion moment an operator likely wants a Telegram ping for.
///
@@ -482,7 +482,7 @@ namespace AutopilotMonitor.Functions.Services
{
var tenantLabel = string.IsNullOrWhiteSpace(domainName) ? tenantId : $"{domainName} ({tenantId})";
return WriteAsync(OpsEventCategory.Tenant, "TenantTrialExpired", OpsEventSeverity.Warning,
- $"Enterprise trial for tenant {tenantLabel} expired ({trialExpiredUtc:yyyy-MM-dd HH:mm}Z) — tenant is now Community",
+ $"Pro trial for tenant {tenantLabel} expired ({trialExpiredUtc:yyyy-MM-dd HH:mm}Z) — tenant is now Community",
tenantId, "System.TrialSweep", new { domainName, trialExpiredUtc });
}
diff --git a/src/Backend/AutopilotMonitor.Functions/Services/RateLimitResolver.cs b/src/Backend/AutopilotMonitor.Functions/Services/RateLimitResolver.cs
index 3720c8ec..dc0ad1a5 100644
--- a/src/Backend/AutopilotMonitor.Functions/Services/RateLimitResolver.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Services/RateLimitResolver.cs
@@ -7,7 +7,7 @@ namespace AutopilotMonitor.Functions.Services
/// no background sync job. Both the device path () and the user path
/// (UserRateLimitMiddleware) resolve through here so the precedence stays consistent.
///
- /// Edition entitlement floor (FeatureEntitlementCatalog): Enterprise raises the DEFAULT to
+ /// Edition entitlement floor (FeatureEntitlementCatalog): Pro raises the DEFAULT to
/// at least the floor — it never lowers an admin-raised global default, and an explicit per-tenant
/// override still wins outright (a Global Admin must stay able to throttle a specific tenant
/// below the floor). Precedence: override ?? max(globalDefault, entitlementFloor).
diff --git a/src/Backend/AutopilotMonitor.Functions/Services/TenantEntitlementService.cs b/src/Backend/AutopilotMonitor.Functions/Services/TenantEntitlementService.cs
index 93721939..207de5e0 100644
--- a/src/Backend/AutopilotMonitor.Functions/Services/TenantEntitlementService.cs
+++ b/src/Backend/AutopilotMonitor.Functions/Services/TenantEntitlementService.cs
@@ -7,12 +7,12 @@ using Microsoft.Extensions.Logging;
namespace AutopilotMonitor.Functions.Services
{
///
- /// Resolves a tenant's effective edition (Community/Enterprise) and its entitlements at read
+ /// Resolves a tenant's effective edition (Community/Pro) and its entitlements at read
/// time. Rides on 's 5-minute config cache — no cache
/// of its own, so plan/trial mutations become visible within the existing staleness budget.
///
/// Fail-closed: any storage/resolution failure yields Community. A broken entitlement lookup
- /// must never grant Enterprise capabilities.
+ /// must never grant Pro capabilities.
///
public class TenantEntitlementService
{
@@ -74,6 +74,29 @@ namespace AutopilotMonitor.Functions.Services
public static TenantEdition ResolveEdition(TenantConfiguration config, DateTime nowUtc)
=> FeatureEntitlementCatalog.ResolveEdition(config.PlanTier, config.TrialExpiresUtc, nowUtc);
+ ///
+ /// Whether the OOBE bootstrap feature is effectively enabled for this config: included in
+ /// the tenant's edition (Pro, incl. active trials) OR explicitly enabled per tenant via
+ /// the GA-only flag (additive —
+ /// the Community escape hatch). Read-time resolution: a trial expiry or downgrade turns
+ /// the feature off automatically.
+ ///
+ public static bool IsBootstrapEnabled(TenantConfiguration config, DateTime nowUtc)
+ => FeatureEntitlementCatalog.Get(ResolveEdition(config, nowUtc)).BootstrapIncluded
+ || config.BootstrapTokenEnabled;
+
+ ///
+ /// Whether Unrestricted Mode is effectively ACTIVE for this config. Requires all three:
+ /// the edition allows it (Pro — read-time, so trial expiry / downgrade re-arms the
+ /// guardrails fail-closed), the GA-only on-request gate
+ /// (), and the tenant-admin
+ /// opt-in toggle ().
+ ///
+ public static bool IsUnrestrictedModeActive(TenantConfiguration config, DateTime nowUtc)
+ => FeatureEntitlementCatalog.Get(ResolveEdition(config, nowUtc)).UnrestrictedModeAvailable
+ && config.UnrestrictedModeEnabled
+ && config.UnrestrictedMode;
+
///
/// The retention days the platform actually enforces for this config: the stored value
/// clamped to the edition's cap. days <= 0 is the GA-only "infinite" escape hatch
diff --git a/src/Shared/AutopilotMonitor.Shared/Models/Config/TenantConfiguration.cs b/src/Shared/AutopilotMonitor.Shared/Models/Config/TenantConfiguration.cs
index 74ac7f40..84f95b0a 100644
--- a/src/Shared/AutopilotMonitor.Shared/Models/Config/TenantConfiguration.cs
+++ b/src/Shared/AutopilotMonitor.Shared/Models/Config/TenantConfiguration.cs
@@ -110,21 +110,21 @@ namespace AutopilotMonitor.Shared.Models
///
/// Tenant plan tier. Determines default API rate limits and feature gates.
- /// Write-side values: "community", "enterprise". Legacy stored values ("free", "pro")
- /// remain readable and resolve to Community (fail-closed — see FeatureEntitlementCatalog).
- /// Managed by Global Admins.
+ /// Write-side values: "community", "pro". The legacy stored values "enterprise" (resolves
+ /// to Pro) and "free" (resolves to Community) remain readable — see
+ /// FeatureEntitlementCatalog. Managed by Global Admins.
///
public string PlanTier { get; set; } = "free";
///
- /// End of the tenant's Enterprise trial (UTC). While this is in the future the tenant's
- /// effective edition is Enterprise regardless of . Null = no trial.
+ /// End of the tenant's Pro trial (UTC). While this is in the future the tenant's
+ /// effective edition is Pro regardless of . Null = no trial.
/// Expiry degrades the tenant to Community at read time — no timer involved.
///
public DateTime? TrialExpiresUtc { get; set; }
///
- /// When the tenant's Enterprise trial was started (UTC). Informational/audit only.
+ /// When the tenant's Pro trial was started (UTC). Informational/audit only.
///
public DateTime? TrialStartedUtc { get; set; }
@@ -460,27 +460,30 @@ namespace AutopilotMonitor.Shared.Models
// ===== BOOTSTRAP TOKEN =====
///
- /// Whether OOBE Bootstrap Sessions are enabled for this tenant.
- /// When false (default), the Bootstrap Sessions feature is hidden in the UI
- /// and all bootstrap API endpoints reject requests for this tenant.
- /// Only configurable by Global Admins.
+ /// Per-tenant ADDITIVE enable for OOBE Bootstrap Sessions (Global Admin only). The Pro
+ /// plan includes the feature regardless of this flag; for Community tenants this is the
+ /// on-request escape hatch. Effective value = plan-included OR this flag — resolved via
+ /// TenantEntitlementService.IsBootstrapEnabled (backend); when effectively false, the
+ /// feature is hidden in the UI and all bootstrap API endpoints reject requests.
///
public bool BootstrapTokenEnabled { get; set; } = false;
// ===== UNRESTRICTED MODE =====
///
- /// Whether the Unrestricted Mode feature is available for this tenant.
- /// When false (default), the Unrestricted Mode section is hidden in the tenant settings UI
- /// and UnrestrictedMode cannot be activated by tenant admins.
+ /// Whether the Unrestricted Mode feature is available for this tenant (the Pro-plan
+ /// on-request gate). When false (default), the Unrestricted Mode section is hidden in the
+ /// tenant settings UI and UnrestrictedMode cannot be activated by tenant admins.
/// Only configurable by Global Admins.
///
public bool UnrestrictedModeEnabled { get; set; } = false;
///
- /// When enabled, agent guardrails are relaxed: all registry paths, WMI queries, and commands
+ /// When effective, agent guardrails are relaxed: all registry paths, WMI queries, and commands
/// are allowed via GatherRules. File paths and diagnostics paths are allowed except C:\Users.
/// Default: false. Can only be toggled by tenant admins when UnrestrictedModeEnabled is true.
+ /// EFFECTIVE only while the tenant's edition is Pro (read-time re-gate, fail-closed on
+ /// trial expiry/downgrade) — see TenantEntitlementService.IsUnrestrictedModeActive.
///
public bool UnrestrictedMode { get; set; } = false;
diff --git a/src/Web/autopilot-monitor-web/app/admin/components/TenantManagementSection.tsx b/src/Web/autopilot-monitor-web/app/admin/components/TenantManagementSection.tsx
index 6ef31bca..c671f57d 100644
--- a/src/Web/autopilot-monitor-web/app/admin/components/TenantManagementSection.tsx
+++ b/src/Web/autopilot-monitor-web/app/admin/components/TenantManagementSection.tsx
@@ -31,7 +31,7 @@ export interface TenantConfiguration {
dataRetentionDays: number;
sessionTimeoutHours: number;
planTier?: string;
- /** Enterprise-trial end (ISO, UTC). Null/undefined = no trial. Managed via PATCH plan. */
+ /** Pro-trial end (ISO, UTC). Null/undefined = no trial. Managed via PATCH plan. */
trialExpiresUtc?: string | null;
/** Whether the tenant has used its one self-service trial. */
trialConsumed?: boolean;
@@ -190,9 +190,9 @@ function TenantManagementSectionInner({
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
- // Legacy stored tiers (free/pro) resolve to Community server-side; the select only
- // offers the two canonical values, so normalize on save.
- planTier: tenant.planTier === "enterprise" ? "enterprise" : "community",
+ // The select only offers the two canonical write values (community/pro); legacy stored
+ // "enterprise" normalizes to "pro" on save, legacy "free" to "community".
+ planTier: tenant.planTier === "pro" || tenant.planTier === "enterprise" ? "pro" : "community",
trialExpiresUtc: tenant.trialExpiresUtc ?? null,
}),
});
@@ -598,17 +598,17 @@ function TenantManagementSectionInner({
Stored legacy tier "{editingTenant.planTier}" resolves to Community. Saving normalizes it.
@@ -653,7 +658,7 @@ function TenantManagementSectionInner({
)}
- Set a date to grant/extend an Enterprise trial; clear to end it. Saving does not reset trial consumption.
+ Set a date to grant/extend a Pro trial; clear to end it. Saving does not reset trial consumption.
⚠ Outside tenant range (7–365) — field will be locked for tenant admins
) : (
-
Tenant range: 7–90 (Community) / 7–365 (Enterprise). Values above the plan cap are enforced at the cap. Set 0 for infinite retention (Global only).
+
Tenant range: 7–90 (Community) / 7–365 (Pro). Values above the plan cap are enforced at the cap. Set 0 for infinite retention (Global only).
)}
diff --git a/src/Web/autopilot-monitor-web/app/admin/settings/sections/SectionUsagePlans.tsx b/src/Web/autopilot-monitor-web/app/admin/settings/sections/SectionUsagePlans.tsx
index 26bca11b..2e331197 100644
--- a/src/Web/autopilot-monitor-web/app/admin/settings/sections/SectionUsagePlans.tsx
+++ b/src/Web/autopilot-monitor-web/app/admin/settings/sections/SectionUsagePlans.tsx
@@ -202,7 +202,7 @@ export function SectionUsagePlans() {
type="text"
value={tier.name}
onChange={(e) => updateTier(index, "name", e.target.value)}
- placeholder="e.g., free, pro, enterprise"
+ placeholder="e.g., community, pro"
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-green-500"
/>
diff --git a/src/Web/autopilot-monitor-web/app/get-started/page.tsx b/src/Web/autopilot-monitor-web/app/get-started/page.tsx
index af9d15f0..0fe0973a 100644
--- a/src/Web/autopilot-monitor-web/app/get-started/page.tsx
+++ b/src/Web/autopilot-monitor-web/app/get-started/page.tsx
@@ -108,7 +108,7 @@ export default function GetStartedPage() {
rel="noopener noreferrer"
className="text-[var(--lp-accent-ink)] hover:opacity-80 underline"
>
- Enterprise plan
+ Pro plan
{" "}
adds SLAs, support commitments, MSP delegation, higher operating limits and more —
same service, nothing to migrate.
diff --git a/src/Web/autopilot-monitor-web/app/privacy/page.tsx b/src/Web/autopilot-monitor-web/app/privacy/page.tsx
index 8258c9b3..5afa55ca 100644
--- a/src/Web/autopilot-monitor-web/app/privacy/page.tsx
+++ b/src/Web/autopilot-monitor-web/app/privacy/page.tsx
@@ -2,7 +2,7 @@ import { LandingNavbar } from "../../components/landing/LandingNavbar";
import { SiteFooter } from "../../components/SiteFooter";
import { DOCS_URL } from "@/utils/config";
-const LAST_UPDATED = "29 July 2026";
+const LAST_UPDATED = "31 July 2026";
const DOCS_SECURITY_FAQ = `${DOCS_URL}/trust/security-faq`;
const DOCS_DATA_FLOWS = `${DOCS_URL}/trust/data-flows`;
@@ -60,7 +60,7 @@ export default function PrivacyPage() {
A data processing agreement (DPA / AVV) is available on request, concluded with
- glueckkanja AG. On the Enterprise plan it forms part of the written agreement.
+ glueckkanja AG. On the Pro plan it forms part of the written agreement.
What We Collect
@@ -133,7 +133,7 @@ export default function PrivacyPage() {
Delegated (MSP) administrators you or your provider have been granted — read-only, limited to exactly the tenants in scope, with configuration secrets redacted, and with every grant and revocation written to your tenant's audit log so you can always see who was given access to your data
- Delegated administration is an Enterprise capability and is off unless explicitly granted. Full detail on the
+ Delegated administration is a Pro capability and is off unless explicitly granted. Full detail on the
isolation model is in the Security & Privacy FAQ.
@@ -175,7 +175,7 @@ export default function PrivacyPage() {
How Long We Keep Data
Retention is configured by you per tenant, defaulting to 90 days — 7 to 90 days on the
- Community plan and 7 to 365 days on Enterprise. Expired sessions are purged automatically. You additionally
+ Community plan and 7 to 365 days on Pro. Expired sessions are purged automatically. You additionally
control:
diff --git a/src/Web/autopilot-monitor-web/app/settings/TenantConfigContext.tsx b/src/Web/autopilot-monitor-web/app/settings/TenantConfigContext.tsx
index ea3f0e2b..01a03ee7 100644
--- a/src/Web/autopilot-monitor-web/app/settings/TenantConfigContext.tsx
+++ b/src/Web/autopilot-monitor-web/app/settings/TenantConfigContext.tsx
@@ -111,7 +111,7 @@ interface TenantConfigContextValue {
// Edition / trial (read-time server resolution via feature-flags; fail-closed Community)
editionInfo: EditionInfo;
startingTrial: boolean;
- /** Self-service 30-day Enterprise trial (once per tenant). Returns success. */
+ /** Self-service 30-day Pro trial (once per tenant). Returns success. */
startTrial: () => Promise;
// Validation
@@ -641,9 +641,12 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
}, [tenantId, getAccessToken]);
useEffect(() => {
- if (!tenantId || !config?.bootstrapTokenEnabled) return;
+ // Effective availability (mirrors backend IsBootstrapEnabled): Pro plan includes bootstrap;
+ // the per-tenant GA flag is the additive Community enable.
+ const bootstrapAvailable = editionInfo.edition === "pro" || config?.bootstrapTokenEnabled;
+ if (!tenantId || !bootstrapAvailable) return;
fetchBootstrapSessions();
- }, [tenantId, config?.bootstrapTokenEnabled, fetchBootstrapSessions]);
+ }, [tenantId, editionInfo.edition, config?.bootstrapTokenEnabled, fetchBootstrapSessions]);
// -----------------------------------------------------------------------
// Fetch global diagnostics paths (global-admin only)
@@ -1472,7 +1475,7 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
}, [logout]);
// -----------------------------------------------------------------------
- // Self-service Enterprise trial (once per tenant — backend enforces via 409)
+ // Self-service Pro trial (once per tenant — backend enforces via 409)
// -----------------------------------------------------------------------
const startTrial = useCallback(async (): Promise => {
if (!tenantId) return false;
@@ -1494,9 +1497,9 @@ export function TenantConfigProvider({ children }: { children: React.ReactNode }
if (flagsResponse.ok) {
setEditionInfo(parseEditionInfo(await flagsResponse.json()));
}
- setSuccessMessage("Enterprise trial started — all Enterprise features are now active for 30 days.");
+ setSuccessMessage("Pro trial started — all Pro features are now active for 30 days.");
setTimeout(() => setSuccessMessage(null), 5000);
- trackEvent("EnterpriseTrialStarted", { tenantId });
+ trackEvent("ProTrialStarted", { tenantId });
return true;
} catch (err) {
if (err instanceof TokenExpiredError) {
diff --git a/src/Web/autopilot-monitor-web/app/settings/agent/sections/SectionUnrestrictedMode.tsx b/src/Web/autopilot-monitor-web/app/settings/agent/sections/SectionUnrestrictedMode.tsx
index 89cc59c0..c804bca4 100644
--- a/src/Web/autopilot-monitor-web/app/settings/agent/sections/SectionUnrestrictedMode.tsx
+++ b/src/Web/autopilot-monitor-web/app/settings/agent/sections/SectionUnrestrictedMode.tsx
@@ -6,7 +6,7 @@ import UnrestrictedModeSection from "../../components/UnrestrictedModeSection";
export function SectionUnrestrictedMode() {
const {
- canEditConfig, config,
+ canEditConfig, config, editionInfo,
unrestrictedMode, setUnrestrictedMode,
handleSaveUnrestrictedMode,
savingSection,
@@ -22,10 +22,15 @@ export function SectionUnrestrictedMode() {
);
}
- if (!config?.unrestrictedModeEnabled) {
+ // Pro-plan feature, activated on request: requires the effective Pro edition AND the GA-set
+ // gate (mirrors the backend's read-time re-gate — a downgraded tenant loses the section).
+ if (editionInfo.edition !== "pro" || !config?.unrestrictedModeEnabled) {
return (
-
Unrestricted Mode is not available for this tenant.
+
+ Unrestricted Mode is not available for this tenant. It is a Pro-plan feature and is
+ activated on request.
+
- Minimum: 7 days, Maximum: {retentionCapDays} days{retentionCapDays < 365 ? " (up to 365 with Enterprise)" : ""}
+ Minimum: 7 days, Maximum: {retentionCapDays} days{retentionCapDays < 365 ? " (up to 365 with Pro)" : ""}
)}
diff --git a/src/Web/autopilot-monitor-web/app/settings/components/EditionBadge.tsx b/src/Web/autopilot-monitor-web/app/settings/components/EditionBadge.tsx
index be746307..8dfdc5a5 100644
--- a/src/Web/autopilot-monitor-web/app/settings/components/EditionBadge.tsx
+++ b/src/Web/autopilot-monitor-web/app/settings/components/EditionBadge.tsx
@@ -6,36 +6,36 @@ import { useTenantConfig } from "../TenantConfigContext";
/**
* Edition badge for the settings header.
- * - "Enterprise" / "Enterprise Trial — X days left" / "Community"
+ * - "Pro" / "Pro Trial — X days left" / "Community"
* - For Community tenants, an "Upgrade plan" link to the Plan section, where the full picture
- * (what Enterprise adds) and the trial CTA live. The badge itself only reports status.
+ * (what Pro adds) and the trial CTA live. The badge itself only reports status.
*/
export default function EditionBadge() {
const { editionInfo } = useTenantConfig();
- const isEnterprise = editionInfo.edition === "enterprise";
+ const isPro = editionInfo.edition === "pro";
const label = editionLabel(editionInfo);
return (
{label}
- {!isEnterprise && (
+ {!isPro && (
Bootstrap Sessions are not available for this tenant.
diff --git a/src/Web/autopilot-monitor-web/app/settings/tenant/sections/SectionPlan.tsx b/src/Web/autopilot-monitor-web/app/settings/tenant/sections/SectionPlan.tsx
index 1d54b5b0..0579a470 100644
--- a/src/Web/autopilot-monitor-web/app/settings/tenant/sections/SectionPlan.tsx
+++ b/src/Web/autopilot-monitor-web/app/settings/tenant/sections/SectionPlan.tsx
@@ -5,7 +5,7 @@ import { useTenantConfig } from "../../TenantConfigContext";
import { trialDaysLeft } from "@/lib/edition";
/**
- * Self-service Enterprise trial switch. The Enterprise feature set is not finalized yet, so the
+ * Self-service Pro trial switch. The Pro feature set is not finalized yet, so the
* trial CTA is teased but not actionable. Flip this to `true` (a one-line change) to open the
* self-service 30-day trial — the backend POST /trial endpoint and the startTrial() wiring are
* already in place; only this gate keeps the button inert.
@@ -15,10 +15,10 @@ const TRIAL_SELF_SERVICE_ENABLED = false;
/**
* Retention baselines per plan tier. These describe the two PLANS on the comparison cards, so they
* must not depend on the viewer's current edition — they mirror the backend catalog
- * (FeatureEntitlementCatalog: Community RetentionCapDays = 90, Enterprise = 365).
+ * (FeatureEntitlementCatalog: Community RetentionCapDays = 90, Pro = 365).
*/
const COMMUNITY_RETENTION_DAYS = 90;
-const ENTERPRISE_RETENTION_DAYS = 365;
+const PRO_RETENTION_DAYS = 365;
function CheckIcon({ className }: { className: string }) {
return (
@@ -49,7 +49,7 @@ function FeatureList({ features, checkClass, muted = false }: { features: string
);
}
-/** The Enterprise delta: same layout as FeatureList but "+" bullets and emphasized text. */
+/** The Pro delta: same layout as FeatureList but "+" bullets and emphasized text. */
function PlusList({ features }: { features: string[] }) {
return (
@@ -64,8 +64,8 @@ function PlusList({ features }: { features: string[] }) {
}
/**
- * Plan section: two side-by-side plan cards (Community and Enterprise). The tenant's current plan
- * is highlighted so the edition is obvious at a glance; the Enterprise card teases what it adds.
+ * Plan section: two side-by-side plan cards (Community and Pro). The tenant's current plan
+ * is highlighted so the edition is obvious at a glance; the Pro card teases what it adds.
*
* Dark mode: the tinted card surfaces use slash-opacity utilities (bg-purple-50/40), which the
* global `.dark .bg-*` override map in globals.css does NOT match — they are separate class tokens.
@@ -77,15 +77,15 @@ export function SectionPlan() {
const { editionInfo, startTrial, startingTrial, user } = useTenantConfig();
const [confirming, setConfirming] = useState(false);
- const isEnterprise = editionInfo.edition === "enterprise";
+ const isPro = editionInfo.edition === "pro";
const daysLeft = editionInfo.isTrial ? trialDaysLeft(editionInfo.trialExpiresUtc) : 0;
- const trialConsumed = !isEnterprise && !editionInfo.trialAvailable;
+ const trialConsumed = !isPro && !editionInfo.trialAvailable;
const canStartTrial =
editionInfo.trialAvailable && (user?.isTenantAdmin === true || user?.isGlobalAdmin === true);
// Features shared by both plans verbatim. Plan-specific items (retention, support tier) are NOT
- // in here — repeating "90-day retention" on the Enterprise card would be factually wrong; their
- // Enterprise counterparts live in enterpriseExtras instead.
+ // in here — repeating "90-day retention" on the Pro card would be factually wrong; their
+ // Pro counterparts live in proExtras instead.
const sharedFeatures = [
"Live session monitoring & progress portal",
"Full rules engine, including custom rules",
@@ -99,11 +99,13 @@ export function SectionPlan() {
"Community support (GitHub)",
];
- const enterpriseExtras = [
- `Extended data retention — ${ENTERPRISE_RETENTION_DAYS} days (vs ${COMMUNITY_RETENTION_DAYS})`,
+ const proExtras = [
+ `Extended data retention — ${PRO_RETENTION_DAYS} days (vs ${COMMUNITY_RETENTION_DAYS})`,
"Higher portal & agent API rate limits",
"Larger AI (MCP) usage quota",
"Delegated (MSP) administration across tenants",
+ "OOBE bootstrap sessions — test the agent without an Intune deployment",
+ "Unrestricted Mode for advanced data collection (activated on request)",
"Reliability commitments & priority support",
];
@@ -116,7 +118,7 @@ export function SectionPlan() {
Plan
-
Your current plan and what Enterprise adds
+
Your current plan and what Pro adds
@@ -126,7 +128,7 @@ export function SectionPlan() {
{/* Community card */}
Community
The full product, free
- {!isEnterprise && (
+ {!isPro && (
Current plan
@@ -151,20 +153,20 @@ export function SectionPlan() {
- {/* Enterprise card */}
+ {/* Pro card */}
diff --git a/src/Web/autopilot-monitor-web/app/settings/tenant/tenantNavSections.ts b/src/Web/autopilot-monitor-web/app/settings/tenant/tenantNavSections.ts
index 77e1a1c8..64337fae 100644
--- a/src/Web/autopilot-monitor-web/app/settings/tenant/tenantNavSections.ts
+++ b/src/Web/autopilot-monitor-web/app/settings/tenant/tenantNavSections.ts
@@ -7,7 +7,7 @@ export const TENANT_NAV_SECTIONS = [
{ id: "bootstrap-sessions", label: "Bootstrap Sessions", description: "Create and manage bootstrap tokens" },
{ id: "graph-permissions", label: "Optional Graph capabilities", description: "Grant additional Microsoft Graph permissions for optional features" },
{ id: "support", label: "Submit Logs", description: "Send diagnostic files to the Autopilot Monitor team" },
- { id: "plan", label: "Plan", description: "Your current plan and what Enterprise adds" },
+ { id: "plan", label: "Plan", description: "Your current plan and what Pro adds" },
{ id: "contact", label: "Contact", description: "Where we reach you about the service" },
] as const;
diff --git a/src/Web/autopilot-monitor-web/app/terms/page.tsx b/src/Web/autopilot-monitor-web/app/terms/page.tsx
index 8263b14b..c3c8a281 100644
--- a/src/Web/autopilot-monitor-web/app/terms/page.tsx
+++ b/src/Web/autopilot-monitor-web/app/terms/page.tsx
@@ -2,7 +2,7 @@ import { LandingNavbar } from "../../components/landing/LandingNavbar";
import { SiteFooter } from "../../components/SiteFooter";
import { DOCS_URL } from "@/utils/config";
-const LAST_UPDATED = "21 July 2026";
+const LAST_UPDATED = "31 July 2026";
const DOCS_SECURITY_FAQ = `${DOCS_URL}/trust/security-faq`;
const DOCS_PLANS = `${DOCS_URL}/plans`;
const GITHUB_ADVISORY = "https://github.com/okieselbach/AutopilotMonitor/security/advisories/new";
@@ -44,7 +44,7 @@ export default function TermsPage() {
-
Enterprise
+
Pro
Commercial plan under a written agreement with glueckkanja AG. Includes support and reliability
commitments, a data processing agreement, higher operating limits, extended retention, and delegated
@@ -62,7 +62,7 @@ export default function TermsPage() {
Who can use it. Autopilot Monitor requires a Microsoft Entra ID tenant with Intune and
registered Autopilot devices. The Community plan is open to anyone with such a tenant —
organizations, non-profits, and individuals running their own lab or personal tenant alike. The{" "}
- Enterprise plan is a commercial agreement and is offered to businesses and organizations
+ Pro plan is a commercial agreement and is offered to businesses and organizations
acting in a commercial or professional capacity.
@@ -111,7 +111,7 @@ export default function TermsPage() {
Delegated (MSP) Administration
Delegated administration lets a managing organization see a defined set of customer tenants from one place. It
- is an Enterprise capability and applies only where access has been granted.
+ is a Pro capability and applies only where access has been granted.
Delegated access is read-only and limited to exactly the tenants in scope. Configuration secrets are redacted.
@@ -136,10 +136,10 @@ export default function TermsPage() {
-
Availability. The Community plan carries no uptime or availability commitment; interruptions, maintenance, and changes can occur without prior notice. The Enterprise plan carries the availability commitments set out in its agreement.
-
Support. Community support is best-effort via GitHub issues, provided by the project maintainer and the community, with no guaranteed response or resolution time; built-in rules and IME log patterns are community-maintained. Enterprise support follows the response commitments in its agreement.
+
Availability. The Community plan carries no uptime or availability commitment; interruptions, maintenance, and changes can occur without prior notice. The Pro plan carries the availability commitments set out in its agreement.
+
Support. Community support is best-effort via GitHub issues, provided by the project maintainer and the community, with no guaranteed response or resolution time; built-in rules and IME log patterns are community-maintained. Pro support follows the response commitments in its agreement.
Data durability. Autopilot Monitor is a monitoring system, not a system of record. Configuration, authorization, and rule data is backed up daily; session and event telemetry is time-bounded operational data and is not backed up. Retain anything you need for compliance or reporting purposes in your own systems.
-
Liability. To the extent permitted by law, glueckkanja AG is not liable for indirect, incidental, special, consequential, or punitive damages, or for loss of data, profit, or business, arising from use of or inability to use the service. For the Community plan, which is provided free of charge, liability is limited to intent and gross negligence. Liability for injury to life, body or health and under mandatory statutory provisions remains unaffected. For the Enterprise plan, the liability provisions of the written agreement apply.
+
Liability. To the extent permitted by law, glueckkanja AG is not liable for indirect, incidental, special, consequential, or punitive damages, or for loss of data, profit, or business, arising from use of or inability to use the service. For the Community plan, which is provided free of charge, liability is limited to intent and gross negligence. Liability for injury to life, body or health and under mandatory statutory provisions remains unaffected. For the Pro plan, the liability provisions of the written agreement apply.
Use at your own risk. The service reports on enrollments; it does not perform them. Operational decisions you take based on its output remain your responsibility.
@@ -151,7 +151,7 @@ export default function TermsPage() {
Your controls. You set the retention period, delete individual sessions, and offboard your tenant entirely at any time — no support ticket required.
Rules you contribute. Analyze rules, gather rules, and IME log patterns you author are detection definitions, not device data. By creating them you grant a non-exclusive, royalty-free right to retain them and to include them in the shared community rule pool, so that other organizations can benefit from a detection you built. This is the reciprocal side of a product whose built-in rules are community-maintained. You keep the right to use your own rules however you like, and you can request removal from the pool at any time.
Suspension. Access may be suspended where use threatens the integrity, security, or availability of the service or of other tenants — for example abusive request volumes or attempts to reach other tenants' data. Where circumstances permit, notice is given first.
-
Termination. You may stop using the service and offboard at any time. For the Community plan, the operator may discontinue the service or an individual tenant's access; reasonable advance notice will be given except where security requires immediate action. Enterprise termination follows its agreement.
+
Termination. You may stop using the service and offboard at any time. For the Community plan, the operator may discontinue the service or an individual tenant's access; reasonable advance notice will be given except where security requires immediate action. Pro termination follows its agreement.
After offboarding. Your tenant's data is removed through a verified multi-phase cascade. Product feedback you submitted, and custom rules and IME log patterns you authored, are intentionally retained — neither contains enrollment telemetry or personal data. Both are removed on request.
@@ -162,7 +162,7 @@ export default function TermsPage() {
The security architecture, data residency, external services, retention and deletion behaviour, and an explicit
statement of what the service does not do are published in the{" "}
Security & Privacy FAQ.
- A signed data processing agreement is part of the Enterprise plan.
+ A signed data processing agreement is part of the Pro plan.
@@ -205,7 +205,7 @@ export default function TermsPage() {
Changes and Governing Law
Changes. These terms may be updated; the "last updated" date above reflects the current version, and material changes are announced through the service announcements in the portal. Continued use after a change constitutes acceptance.
-
Governing law. These terms are governed by German law. For the Enterprise plan, the governing-law and venue provisions of the written agreement apply.
+
Governing law. These terms are governed by German law. For the Pro plan, the governing-law and venue provisions of the written agreement apply.
Severability. If a provision is found unenforceable, the remaining provisions stay in effect.
Contact. Company details are in the Imprint. For the project and the Community edition, reach the maintainer via LinkedIn or open a GitHub issue.
diff --git a/src/Web/autopilot-monitor-web/components/landing/FinalCta.tsx b/src/Web/autopilot-monitor-web/components/landing/FinalCta.tsx
index 5e12a392..54b7889f 100644
--- a/src/Web/autopilot-monitor-web/components/landing/FinalCta.tsx
+++ b/src/Web/autopilot-monitor-web/components/landing/FinalCta.tsx
@@ -24,7 +24,7 @@ export function FinalCta() {
rel="noopener noreferrer"
className="text-[var(--lp-accent-ink)] hover:opacity-80 underline"
>
- There's an Enterprise plan
+ There's a Pro plan
.
diff --git a/src/Web/autopilot-monitor-web/lib/__tests__/edition.test.ts b/src/Web/autopilot-monitor-web/lib/__tests__/edition.test.ts
index 8f19be37..c5c26877 100644
--- a/src/Web/autopilot-monitor-web/lib/__tests__/edition.test.ts
+++ b/src/Web/autopilot-monitor-web/lib/__tests__/edition.test.ts
@@ -9,9 +9,9 @@ import {
const NOW = new Date("2026-07-07T12:00:00Z");
describe("parseEditionInfo", () => {
- it("parses a full enterprise-trial payload", () => {
+ it("parses a full pro-trial payload", () => {
const info = parseEditionInfo({
- edition: "enterprise",
+ edition: "pro",
isTrial: true,
trialExpiresUtc: "2026-07-20T12:00:00Z",
trialAvailable: false,
@@ -19,11 +19,11 @@ describe("parseEditionInfo", () => {
retentionCapDays: 365,
userRateLimitPerMinute: 150,
delegatedAdminAllowed: true,
- mcpUsagePlan: "enterprise",
+ mcpUsagePlan: "pro",
},
});
- expect(info.edition).toBe("enterprise");
+ expect(info.edition).toBe("pro");
expect(info.isTrial).toBe(true);
expect(info.trialExpiresUtc).toBe("2026-07-20T12:00:00Z");
expect(info.entitlements.retentionCapDays).toBe(365);
@@ -31,6 +31,11 @@ describe("parseEditionInfo", () => {
expect(info.entitlements.delegatedAdminAllowed).toBe(true);
});
+ it("accepts the pre-rename edition value 'enterprise' as pro (deploy-order safety)", () => {
+ const info = parseEditionInfo({ edition: "enterprise" });
+ expect(info.edition).toBe("pro");
+ });
+
it("fails closed to Community for malformed payloads", () => {
expect(parseEditionInfo(null)).toEqual(COMMUNITY_DEFAULT);
expect(parseEditionInfo(undefined)).toEqual(COMMUNITY_DEFAULT);
@@ -45,7 +50,7 @@ describe("parseEditionInfo", () => {
});
it("defaults missing entitlements to Community values", () => {
- const info = parseEditionInfo({ edition: "enterprise" });
+ const info = parseEditionInfo({ edition: "pro" });
expect(info.entitlements.retentionCapDays).toBe(90);
expect(info.entitlements.userRateLimitPerMinute).toBeNull();
});
@@ -74,24 +79,24 @@ describe("editionLabel", () => {
expect(editionLabel(COMMUNITY_DEFAULT, NOW)).toBe("Community");
});
- it("labels permanent enterprise", () => {
- const info = parseEditionInfo({ edition: "enterprise", isTrial: false });
- expect(editionLabel(info, NOW)).toBe("Enterprise");
+ it("labels permanent pro", () => {
+ const info = parseEditionInfo({ edition: "pro", isTrial: false });
+ expect(editionLabel(info, NOW)).toBe("Pro");
});
it("labels a trial with a day countdown (singular/plural)", () => {
const plural = parseEditionInfo({
- edition: "enterprise",
+ edition: "pro",
isTrial: true,
trialExpiresUtc: "2026-07-10T12:00:00Z",
});
- expect(editionLabel(plural, NOW)).toBe("Enterprise Trial — 3 days left");
+ expect(editionLabel(plural, NOW)).toBe("Pro Trial — 3 days left");
const singular = parseEditionInfo({
- edition: "enterprise",
+ edition: "pro",
isTrial: true,
trialExpiresUtc: "2026-07-07T18:00:00Z",
});
- expect(editionLabel(singular, NOW)).toBe("Enterprise Trial — 1 day left");
+ expect(editionLabel(singular, NOW)).toBe("Pro Trial — 1 day left");
});
});
diff --git a/src/Web/autopilot-monitor-web/lib/api.ts b/src/Web/autopilot-monitor-web/lib/api.ts
index e2e51d23..97dc0c4e 100644
--- a/src/Web/autopilot-monitor-web/lib/api.ts
+++ b/src/Web/autopilot-monitor-web/lib/api.ts
@@ -111,7 +111,7 @@ export const api = {
featureFlags: (tenantId: string) => `${API_BASE_URL}/api/config/${tenantId}/feature-flags`,
/** PATCH — GA-only plan/trial mutation: { planTier?, trialExpiresUtc?: ISO | null }. */
plan: (tenantId: string) => `${API_BASE_URL}/api/config/${tenantId}/plan`,
- /** POST — tenant-admin self-service 30-day Enterprise trial (once per tenant, 409 after). */
+ /** POST — tenant-admin self-service 30-day Pro trial (once per tenant, 409 after). */
trial: (tenantId: string) => `${API_BASE_URL}/api/config/${tenantId}/trial`,
autopilotConsentUrl: (tenantId: string, redirectUri: string) =>
`${API_BASE_URL}/api/config/${tenantId}/autopilot-device-validation/consent-url${qs({ redirectUri })}`,
diff --git a/src/Web/autopilot-monitor-web/lib/edition.ts b/src/Web/autopilot-monitor-web/lib/edition.ts
index 7699214c..434e531f 100644
--- a/src/Web/autopilot-monitor-web/lib/edition.ts
+++ b/src/Web/autopilot-monitor-web/lib/edition.ts
@@ -1,11 +1,11 @@
/**
- * Pure edition/trial math for the Community/Enterprise entitlement surface.
+ * Pure edition/trial math for the Community/Pro entitlement surface.
* Mirrors the backend's read-time resolution (FeatureEntitlementCatalog): the UI never
* decides entitlements itself — it renders what /feature-flags reports — but needs small
* pure helpers for countdown/labels. Kept free of React so it is unit-testable.
*/
-export type TenantEditionName = "community" | "enterprise";
+export type TenantEditionName = "community" | "pro";
/** Edition/trial surface returned by GET /api/config/{tenantId}/feature-flags. */
export interface EditionInfo {
@@ -35,11 +35,16 @@ export const COMMUNITY_DEFAULT: EditionInfo = {
},
};
-/** Parse the feature-flags payload's edition surface; malformed → Community default. */
+/**
+ * Parse the feature-flags payload's edition surface; malformed → Community default.
+ * Accepts both "pro" and the pre-rename value "enterprise" (deploy-order safety: this web
+ * build may briefly run against a backend that still reports "enterprise").
+ */
export function parseEditionInfo(flags: unknown): EditionInfo {
if (!flags || typeof flags !== "object") return COMMUNITY_DEFAULT;
const f = flags as Record;
- const edition: TenantEditionName = f.edition === "enterprise" ? "enterprise" : "community";
+ const edition: TenantEditionName =
+ f.edition === "pro" || f.edition === "enterprise" ? "pro" : "community";
const ent = (f.entitlements && typeof f.entitlements === "object"
? (f.entitlements as Record)
: {}) as Record;
@@ -71,10 +76,10 @@ export function trialDaysLeft(trialExpiresUtc: string | null | undefined, now: D
return ms <= 0 ? 0 : Math.ceil(ms / 86_400_000);
}
-/** Badge label: "Enterprise", "Enterprise Trial — X days left", or "Community". */
+/** Badge label: "Pro", "Pro Trial — X days left", or "Community". */
export function editionLabel(info: EditionInfo, now: Date = new Date()): string {
- if (info.edition !== "enterprise") return "Community";
- if (!info.isTrial) return "Enterprise";
+ if (info.edition !== "pro") return "Community";
+ if (!info.isTrial) return "Pro";
const days = trialDaysLeft(info.trialExpiresUtc, now);
- return `Enterprise Trial — ${days} day${days === 1 ? "" : "s"} left`;
+ return `Pro Trial — ${days} day${days === 1 ? "" : "s"} left`;
}