CollectLogsQuickConfigEnabled fires when a tenant admin switches on Hosted
diagnostics upload through the session Collect Logs quick-config dialog
(PUT ?intent=collect-logs -> write source portal-collect-logs); every other
enable/disable path emits DiagnosticsUploadEnabled/Disabled (PUT, PATCH,
revert, MCP). SessionActionQueued records portal-queued server actions.
DiagnosticsUploadConfigChange is the single definition behind the
diagnosticsUploadConfigured feature flag and the flip detection. Event types
dual-registered in the OpsAlertRules catalog.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Backend: POST config/{id}/trial now denies with 409 ContactEmailRequired
when no contact address is stored (extracted EvaluateTrialStart verdict,
order test-pinned: consumed/already-Pro win over the contact prompt). The
gate is live at the API even while the trial CTA stays behind its
"coming soon" flag; GA plan assignment stays unblocked by design. The
feature-flags payload gains contactEmailSet (bool only, never the
address - contract test updated).
Web: trial CTA shows a "set a contact address first" note instead of the
start button when the flag flips on; dashboard gets an amber non-blocking
banner for Pro tenants without a contact (admins only, fail-safe: only an
explicit contactEmailSet:false nags, so loading/error/older-backend states
stay quiet); GA tenant editor warns when assigning Pro to a tenant
without a contact address.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Re-consent, verify and flip collapse into the existing consent flow: the
consent URL funnels legacy-homed tenants to the primary app registration,
and consent-status/access-check verify admin consent via an app-only token
probe under explicit primary credentials, then auto-flip HomedAppClientId.
- AdminConfiguration.SelfServiceAppHomingEnabled: kill switch (default off,
cache-bypassing fail-closed read); gates funnel + auto-flip + tenant-admin
manual flip. GA path deliberately survives the kill switch.
- POST config/{tenantId}/app-homing (TenantAdminOrGA): tenant admins flip to
primary only, probe-gated; GA gets force + revert. Pure decision helper
EvaluateManualFlip shared by function and tests.
- GraphTokenService.GetAccessTokenForAppAsync: probe seam with explicit app
credentials, cache-safe via app-scoped token keys.
- EntraAppRolesEnabled does not block a flip; it emits a Warning ops event
(AppHomingFlippedWithEntraRoles) reminding to re-assign enterprise-app roles.
- Web: homing badge + "App Registration Homing" panel with confirm dialog in
GA tenant management (typed homedAppClientId hardens the PUT round-trip),
funnel/success banners in tenant settings with switchAuthApp re-login,
admin-config kill-switch round-trip, ops event types dual-registered.
- Browser telemetry (App Insights): pre-init event buffer so module-init auth
events survive; strategic events across the whole flow (app_homing_flipped,
consent_flow_*, consent_verify_failed, auth_app_fallback_*, auth_app_switched,
auth_app_homing_learned, app_homing_manual_flip*).
Backend must deploy before web (additive JSON fields); everything is inert
while EntraId:LegacyClientId is unset. Enable the kill switch in the admin
portal after the dual app-reg config swap.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- New EndpointPolicy.TenantAdminOrOperator (Admin/Operator/GA, no Viewer) with
middleware evaluator; POST sessions/{sessionId}/actions moves to this tier so
troubleshooting operators can queue request_diagnostics ("Collect Logs").
- QueueSessionActionFunction re-gates per type: terminate_session and
rotate_config stay Admin/GA-only (403 for operators).
- GET config/{tenantId}/feature-flags exposes diagnosticsUploadConfigured so
members below Admin can see whether an on-demand upload would work.
- Offboarding cascade: hosted diagnostics deletion now sweeps the session
prefix {tenantId}/AgentDiagnostics-{sessionId}- (plus the manifest-named
blob) so on-demand packages overwritten on the Sessions row by the terminal
package no longer survive deletion. CustomerSas branch unchanged.
- Notification dismiss routes deliberately NOT migrated: dismissal is still
tenant-shared; operators would clear admin-tier notifications they cannot see.
Tests: middleware role matrix on the actions route, per-type gate matrix,
feature-flag matrix, prefix-sweep + real hosted-branch cascade tests.
Backend suite 3253/3253.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>