Skip to content

🎛️ refactor: Scope App-Config Override Cache by Isolation Context - #13455

Merged
danny-avila merged 1 commit into
devfrom
app-config-tenant-cache-scope
Jun 1, 2026
Merged

danny-avila merged 1 commit into
devfrom
app-config-tenant-cache-scope

Conversation

@danny-avila

Copy link
Copy Markdown
Collaborator

Summary

getAppConfig caches per-principal merged config overrides (model specs, endpoints, interface flags) under a key produced by overrideCacheKey(role, userId, tenantId):

const tenant = tenantId || '__default__';   // ← only the tenantId *argument*

The key derives the tenant from the tenantId argument only. But callers that go through the tenant-context middleware — the common request path — pass no explicit tenantId and rely on the AsyncLocalStorage tenant context. Those calls are keyed under the shared __default__ bucket:

  1. Tenant A request (no tenantId arg, ALS = A) → getApplicableConfigs runs under A's context (the Mongoose plugin scopes the DB query to A), the merged result is cached under _OVERRIDE_:__default__:USER.
  2. Tenant B request (no tenantId arg, ALS = B) → same __default__ key → cache hit → tenant A's merged config is served to tenant B.

So the cache key disagreed with the DB scoping, leaking config across tenants. (This affects strict mode too whenever principals are non-empty — the existing empty-principals early-return doesn't cover the role/user case.)

Fix

Fall back to getTenantId() (the ALS tenant) before __default__, so the cache key reflects the actual scope the DB query already uses:

const tenant = tenantId || getTenantId() || '__default__';

getTenantId() is undefined for single-tenant deployments (no ALS context, no DEFAULT_TENANT_ID), so the key stays __default__ and behavior is unchanged there. The strict-mode "no tenantId" warning is also tightened to fire only when there's genuinely no tenant anywhere (neither argument nor ALS), since the ALS case is now scoped rather than defaulted.

Tests

Adds two tests to the existing service.spec.ts (real Map-backed cache):

  • the override cache key is scoped to the ALS tenant (_OVERRIDE_:tenant-a:USER, never __default__) when no tenantId argument is passed;
  • two tenants resolving the same role each receive their own merged config, and the second tenant's request is not short-circuited by a cache collision (getApplicableConfigs called once per tenant).

Verified both fail without the fix and pass with it. The existing 27 service.spec.ts tests are unchanged and green.

getAppConfig caches per-principal merged config overrides under a key built by
overrideCacheKey(role, userId, tenantId). The key used the tenantId *argument*
only — but callers that go through the tenant middleware (the common path)
pass no explicit tenantId and rely on the AsyncLocalStorage tenant context.
Those calls were keyed under the shared '__default__' bucket, so the DB query
(correctly scoped to the ALS tenant by the Mongoose plugin) produced a merged
config that was then cached and served to the next tenant resolving the same
role/user — leaking model specs, endpoints, and interface flags across tenants.

Fall back to getTenantId() before '__default__' so the cache key reflects the
actual tenant scope (param or ALS). Tighten the strict-mode warning to fire
only when there is genuinely no tenant anywhere (param nor ALS), since the ALS
case is now scoped rather than defaulted. No-op for single-tenant deployments,
where getTenantId() is undefined and the key stays '__default__'.

Adds tests (real Map-backed cache) proving the ALS tenant scopes the key and
that two tenants resolving the same role each get their own config with no
cache collision.
Copilot AI review requested due to automatic review settings June 1, 2026 17:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a multi-tenant cache leak in getAppConfig: the per-principal override cache key only considered the explicit tenantId argument, so tenant-scoped requests (which rely on the ALS tenant context) all collapsed to a shared __default__ bucket, allowing tenant A's merged config to be served to tenant B. The fix falls back to the ALS tenant via getTenantId() before __default__, aligning the cache key with the DB scoping already applied by the Mongoose tenant plugin.

Changes:

  • overrideCacheKey now uses tenantId || getTenantId() || '__default__' so ALS-scoped calls get tenant-specific cache keys.
  • Strict-mode "no tenantId" warning is tightened to fire only when neither the argument nor ALS provides a tenant.
  • Adds two tests covering ALS-keyed cache scoping and the cross-tenant isolation guarantee.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
packages/api/src/app/service.ts Cache key falls back to ALS tenant; strict-mode warning gated on missing ALS tenant too; comment updated.
packages/api/src/app/service.spec.ts Adds two tests verifying ALS-scoped cache key and no cross-tenant config bleed.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

GitNexus: 🚀 deployed

The LibreChat-pr-13455 index is now live on the MCP server.
Deploy run

@danny-avila danny-avila changed the title 🛡️ fix: Scope app-config override cache to the ALS tenant 🛡️ fix: Scope app-config override cache Jun 1, 2026
@danny-avila
danny-avila changed the base branch from main to dev June 1, 2026 19:40
@danny-avila
danny-avila marked this pull request as draft June 1, 2026 19:40
@danny-avila danny-avila changed the title 🛡️ fix: Scope app-config override cache 🛡️ refactor: Scope app-config override cache Jun 1, 2026
@danny-avila

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Nice work!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@danny-avila
danny-avila marked this pull request as ready for review June 1, 2026 22:00
@danny-avila danny-avila changed the title 🛡️ refactor: Scope app-config override cache 🎛️ refactor: Scope App-Config Override Cache by Isolation Context Jun 1, 2026
@danny-avila
danny-avila merged commit 983a33f into dev Jun 1, 2026
12 checks passed
@danny-avila
danny-avila deleted the app-config-tenant-cache-scope branch June 1, 2026 22:00
fuuuzzy pushed a commit to fuuuzzy/LibreChat that referenced this pull request Jun 4, 2026
…breChat-AI#13455)

getAppConfig caches per-principal merged config overrides under a key built by
overrideCacheKey(role, userId, tenantId). The key used the tenantId *argument*
only — but callers that go through the tenant middleware (the common path)
pass no explicit tenantId and rely on the AsyncLocalStorage tenant context.
Those calls were keyed under the shared '__default__' bucket, so the DB query
(correctly scoped to the ALS tenant by the Mongoose plugin) produced a merged
config that was then cached and served to the next tenant resolving the same
role/user — leaking model specs, endpoints, and interface flags across tenants.

Fall back to getTenantId() before '__default__' so the cache key reflects the
actual tenant scope (param or ALS). Tighten the strict-mode warning to fire
only when there is genuinely no tenant anywhere (param nor ALS), since the ALS
case is now scoped rather than defaulted. No-op for single-tenant deployments,
where getTenantId() is undefined and the key stays '__default__'.

Adds tests (real Map-backed cache) proving the ALS tenant scopes the key and
that two tenants resolving the same role each get their own config with no
cache collision.
ThomasVuNguyen pushed a commit to ThomasVuNguyen/LibreChat that referenced this pull request Jul 15, 2026
…breChat-AI#13455)

getAppConfig caches per-principal merged config overrides under a key built by
overrideCacheKey(role, userId, tenantId). The key used the tenantId *argument*
only — but callers that go through the tenant middleware (the common path)
pass no explicit tenantId and rely on the AsyncLocalStorage tenant context.
Those calls were keyed under the shared '__default__' bucket, so the DB query
(correctly scoped to the ALS tenant by the Mongoose plugin) produced a merged
config that was then cached and served to the next tenant resolving the same
role/user — leaking model specs, endpoints, and interface flags across tenants.

Fall back to getTenantId() before '__default__' so the cache key reflects the
actual tenant scope (param or ALS). Tighten the strict-mode warning to fire
only when there is genuinely no tenant anywhere (param nor ALS), since the ALS
case is now scoped rather than defaulted. No-op for single-tenant deployments,
where getTenantId() is undefined and the key stays '__default__'.

Adds tests (real Map-backed cache) proving the ALS tenant scopes the key and
that two tenants resolving the same role each get their own config with no
cache collision.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants