Fail-closed entitlement layer (FeatureEntitlementCatalog + TenantEntitlementService): edition resolves at read time (PlanTier == 'enterprise' OR active trial; legacy free/pro => Community, no migration).
- Plan/trial endpoints: PATCH config/{tid}/plan (GA: tier + grant/extend/end trial, audited, cache-safe) and POST config/{tid}/trial (tenant-admin self-service 30d, once per tenant, 409 after). Generic PUT preserves plan/trial fields for all callers. Fixes SetPlanTier writing past the config cache without audit/UpdatedBy.
- Rate limits: Enterprise floor 150/min for BOTH user (portal/JWT) and device (agent/cert) paths - floor raises the global default, an explicit per-tenant override still wins; MSP users follow their home tenant.
- Retention cap: Community 90d / Enterprise 365d, clamped in the retention fanout (warn log) + non-GA write validation 7..cap (only on changed values; 0=infinite stays GA-only).
- Delegated admin (MSP): Enterprise gate on the delegated admin's HOME tenant (JWT tid, fail-closed on unknown) - managed target tenants may be any edition. Grant-time target check removed; inert grants are suppressed at resolve time (single choke point in GetScopeAsync, applied after the UPN-keyed cache).
- MCP quota: McpQuotaService + McpQuotaEnforcementMiddleware (X-Client-Source: mcp only, GA exempt) enforce daily/monthly budgets - per-user plan override > tenant edition plan; limits from SectionUsagePlans with catalog fallbacks (community 100/3000, enterprise 1000/20000); 429 with X-MCP-Quota-* headers + Retry-After; fail-open on counter reads, fail-closed on plan resolution. /api/metrics/mcp-usage/me reports effective plan + counters.
- Trial visibility: TrialExpirySweepFunction (daily 03:30 UTC, informational) emits TenantTrialExpired (Warning, 24h look-back) + TenantTrialExpiring (Info, <=3d heads-up); dual-registered in web OPS_EVENT_TYPES.
- Web: EditionBadge + one-time trial CTA in settings header, Plan & Trial card in the GA tenant edit modal (own PATCH save), retention cap hints from entitlements, pure lib/edition.ts (vitest), feature-flags surface edition/trial/entitlements; MCP TENANT_SAFE_FIELDS + trial fields.
Verified: solution build clean; backend 2653/2653 tests; web tsc + 516/516 vitest; MCP server tsc + tests green.
Three frontend hooks fetched /api/config/{tenantId} from non-admin contexts
(dashboard banner, session detail UI flags, gather-rules validation indicator)
and silently 403'd for Operator and Viewer because the endpoint is gated
TenantAdminOrGA — same anti-pattern as the tenant notifications bell.
The full /api/config/{tenantId} response cannot be relaxed because it carries
admin-only data (SAS URLs, webhook URLs). Instead, the already-MemberRead
/api/config/{tenantId}/feature-flags endpoint is extended with the five UI
display flags those hooks actually need:
- validateAutopilotDevice (Autopilot Device Validation banner)
- showScriptOutput (session PowerShell output visibility)
- enableSoftwareInventoryAnalyzer (session analyzer flag)
- enableIntegrityBypassAnalyzer (session analyzer flag)
- unrestrictedMode (gather-rules validation indicator)
Nullable model fields are flattened to their agent-side defaults so the UI
does not need a second nullable layer. The projection lives in a static
BuildPayload helper for unit-testability.
Frontend hooks switched from api.config.tenant() to api.config.featureFlags();
gather-rules GA-override path keeps using globalConfig.tenant (GA-only).
Tests: 4 new xUnit cases including a sentinel that fails if SAS URLs or
webhook URLs ever leak into the payload. Backend 886/886 green, web 234/234.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>