Problem
The 2026-07-22 local entitlement sweep (#2980) found two enforcement mechanisms that never touch the license/entitlement system at all, despite the catalog declaring paid editions for the features they gate:
1. Alerts/Channels — parallel, license-disconnected edition system.
alerts.enter-exit / alerts.evaluation (Pro) and alerts.dwell / alerts.threshold / channels.email / channels.slack / channels.teams / channels.aws-sns / channels.azure-eventgrid / channels.digest (Enterprise) are enforced exclusively through IAlertEditionPolicy (AlertEditionPolicy.cs), which reads a standalone Alerts:Edition config option — not ILicenseEntitlementService, not Licensing:DevGrantEdition, not anything derived from the active license. There is no LicenseGate call anywhere in the Alerts feature area.
Alerts:Edition defaults to Pro. Concretely:
- A Community-licensed deployment gets Enter/Exit triggers and webhook delivery (both Pro) for free by default, because the Alerts config default outranks Community.
- Conversely, a genuinely Enterprise-licensed deployment does not get dwell/threshold triggers or the Enterprise channels unless an operator separately flips
Alerts:Edition=Enterprise in config — the license alone does not unlock them.
Both directions are real product bugs: the two knobs (license edition, Alerts:Edition) can disagree in either direction, and today nothing keeps them in sync.
2. caching.output-cache — unenforced everywhere.
ASP.NET Core output caching is registered unconditionally in Program.cs for every edition. Zero LicenseGate/entitlement check exists anywhere in the caching feature area. Unlike caching.redis (correctly boot-gated via StartupConfigurationHelpers.IsRedisCacheEntitledAsync, just not observable as a single HTTP 402), nothing turns output caching off for Community — it is simply always on.
Why it matters now
Pre-release is the cheap moment to fix mistiered/untiered enforcement. Both of these ship a real pricing bug today: Alerts/Channels can silently give away (or silently withhold) paid features depending on which of two disconnected config knobs an operator happened to set, and output caching is a Pro feature that has never been gated at all.
Decision needed (release owner)
Alerts/Channels:
- Unify onto the license system: replace (or derive)
AlertOptions.Edition from the active ILicenseEntitlementService snapshot so IAlertEditionPolicy reflects the real license, removing the standalone Alerts:Edition config knob (or keeping it only as a downward-only override, never an upward grant).
- Keep them deliberately separate: if
Alerts:Edition is intentionally an independent operational knob (e.g. for on-prem deployments without live license telemetry), record that decision explicitly in ADR-0024 and reclassify the affected keys so the local entitlement sweep's known-gap exceptions can be replaced with an explicit "enforced via a documented parallel mechanism, not LicenseGate" allowlist entry instead.
caching.output-cache:
- Gate as declared: check
caching.output-cache before enabling the ASP.NET Core output-cache middleware/policies for Community, mirroring caching.redis's boot-time pattern.
- Re-tier as free: if output caching is intended to be a baseline Community capability, move
caching.output-cache to Community in the catalog and record the rationale in ADR-0024.
Acceptance criteria
Affected repos
honua-server. Cross-links: #2980 (local entitlement sweep, encodes these as loud known-gap exceptions), #2978 (SAML/SCIM, same "silently ungated" shape), #2981 (geocoding, same shape).
Non-goals
- Alerts/channels feature work — enforcement/config-model only.
- Output-cache feature work (policies, invalidation) — entitlement gating only.
Problem
The 2026-07-22 local entitlement sweep (#2980) found two enforcement mechanisms that never touch the license/entitlement system at all, despite the catalog declaring paid editions for the features they gate:
1. Alerts/Channels — parallel, license-disconnected edition system.
alerts.enter-exit/alerts.evaluation(Pro) andalerts.dwell/alerts.threshold/channels.email/channels.slack/channels.teams/channels.aws-sns/channels.azure-eventgrid/channels.digest(Enterprise) are enforced exclusively throughIAlertEditionPolicy(AlertEditionPolicy.cs), which reads a standaloneAlerts:Editionconfig option — notILicenseEntitlementService, notLicensing:DevGrantEdition, not anything derived from the active license. There is noLicenseGatecall anywhere in the Alerts feature area.Alerts:Editiondefaults toPro. Concretely:Alerts:Edition=Enterprisein config — the license alone does not unlock them.Both directions are real product bugs: the two knobs (license edition,
Alerts:Edition) can disagree in either direction, and today nothing keeps them in sync.2.
caching.output-cache— unenforced everywhere.ASP.NET Core output caching is registered unconditionally in
Program.csfor every edition. ZeroLicenseGate/entitlement check exists anywhere in the caching feature area. Unlikecaching.redis(correctly boot-gated viaStartupConfigurationHelpers.IsRedisCacheEntitledAsync, just not observable as a single HTTP 402), nothing turns output caching off for Community — it is simply always on.Why it matters now
Pre-release is the cheap moment to fix mistiered/untiered enforcement. Both of these ship a real pricing bug today: Alerts/Channels can silently give away (or silently withhold) paid features depending on which of two disconnected config knobs an operator happened to set, and output caching is a Pro feature that has never been gated at all.
Decision needed (release owner)
Alerts/Channels:
AlertOptions.Editionfrom the activeILicenseEntitlementServicesnapshot soIAlertEditionPolicyreflects the real license, removing the standaloneAlerts:Editionconfig knob (or keeping it only as a downward-only override, never an upward grant).Alerts:Editionis intentionally an independent operational knob (e.g. for on-prem deployments without live license telemetry), record that decision explicitly in ADR-0024 and reclassify the affected keys so the local entitlement sweep's known-gap exceptions can be replaced with an explicit "enforced via a documented parallel mechanism, not LicenseGate" allowlist entry instead.caching.output-cache:
caching.output-cachebefore enabling the ASP.NET Core output-cache middleware/policies for Community, mirroringcaching.redis's boot-time pattern.caching.output-cacheto Community in the catalog and record the rationale in ADR-0024.Acceptance criteria
caching.output-cache: decision recorded in ADR-0024 and reflected inFeatureCatalog/capability-keys.v1.json.LicenseGatecheck is introduced.Affected repos
honua-server. Cross-links: #2980 (local entitlement sweep, encodes these as loud known-gap exceptions), #2978 (SAML/SCIM, same "silently ungated" shape), #2981 (geocoding, same shape).Non-goals