Skip to content

bug: Alerts/Channels edition enforcement is disconnected from the license system; caching.output-cache is unenforced #2998

Description

@mikemcdougall

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:

  1. 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).
  2. 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:

  1. 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.
  2. 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

  • Alerts/Channels: decision recorded in ADR-0024; either the license/edition knobs are unified, or the parallel-mechanism decision is documented and the sweep's allowlist updated accordingly.
  • caching.output-cache: decision recorded in ADR-0024 and reflected in FeatureCatalog/capability-keys.v1.json.
  • Enforcement (or the allowlisted parallel mechanism) matches the decision; gate tests added where a real LicenseGate check is introduced.
  • The corresponding known-gap exceptions are deleted from the local entitlement sweep (test: local entitlement sweep — catalog-generated 402/200 matrix across all license-gated surfaces #2980) once resolution lands.

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions